Explorar el Código

feat: 增加计数与字操作指令模型

main
suyu hace 1 mes
padre
commit
ab1457cd68
Se han modificado 6 ficheros con 937 adiciones y 10 borrados
  1. +290
    -4
      app/src/domain/control_logic_model.cpp
  2. +89
    -1
      app/src/domain/control_logic_model.h
  3. +1
    -1
      app/src/domain/project_model.cpp
  4. +299
    -4
      app/src/infrastructure/json_project_storage.cpp
  5. +118
    -0
      app/tests/domain_tests.cpp
  6. +140
    -0
      app/tests/project_management_tests.cpp

+ 290
- 4
app/src/domain/control_logic_model.cpp Ver fichero

@@ -31,6 +31,35 @@ bool validateConfig(const ContactNodeConfig &config, std::string *error)
return true;
}

bool validateWordAddress(
const RegisterAddress &address,
const std::string &description,
std::string *error)
{
if (!address.isValid() || address.area() != RegisterArea::D)
{
setError(error, description + "必须使用有效的 D 区地址");
return false;
}
return true;
}

bool validateConfig(const CounterContactNodeConfig &config, std::string *error)
{
if (!config.address.isValid())
{
setError(error, "计数器触点必须使用有效的 C 地址");
return false;
}
if (config.mode != ContactMode::NormallyOpen
&& config.mode != ContactMode::NormallyClosed)
{
setError(error, "计数器触点使用了不支持的模式");
return false;
}
return true;
}

bool validateConfig(const EdgeContactNodeConfig &config, std::string *error)
{
if (!config.address.isValid() || config.address.area() != RegisterArea::M)
@@ -115,6 +144,56 @@ bool validateConfig(const TonNodeConfig &config, std::string *error)
return true;
}

bool validateConfig(const CounterNodeConfig &config, std::string *error)
{
if (!config.address.isValid())
{
setError(error, "计数器指令必须使用有效的 C 地址");
return false;
}
if (config.mode != CounterMode::Up && config.mode != CounterMode::Down)
{
setError(error, "计数器指令使用了不支持的方向");
return false;
}
if (!validateWordAddress(config.currentValueAddress, "计数当前值", error)
|| !config.preset.validate(error))
{
return false;
}
if (!config.resetAddress.isValid()
|| config.resetAddress.area() != RegisterArea::M)
{
setError(error, "计数器复位输入必须使用有效的 M 区地址");
return false;
}
return true;
}

bool validateConfig(const MoveNodeConfig &config, std::string *error)
{
if (!config.source.validate(error))
{
return false;
}
return validateWordAddress(config.destination, "MOVE 目标", error);
}

bool validateConfig(const ArithmeticNodeConfig &config, std::string *error)
{
if (config.operation != ArithmeticOperation::Add
&& config.operation != ArithmeticOperation::Subtract)
{
setError(error, "算术指令使用了不支持的运算");
return false;
}
if (!config.left.validate(error) || !config.right.validate(error))
{
return false;
}
return validateWordAddress(config.destination, "算术指令目标", error);
}

template<typename TItem>
bool hasDuplicateId(const std::vector<TItem> &items)
{
@@ -181,6 +260,16 @@ void normalizeExpression(ConditionExpression *expression)
}
}

void appendValidAddress(
std::vector<RegisterAddress> *addresses,
const RegisterAddress &address)
{
if (address.isValid())
{
addresses->push_back(address);
}
}

} // namespace

TimerAddress::TimerAddress(int index)
@@ -188,6 +277,55 @@ TimerAddress::TimerAddress(int index)
{
}

bool WordOperand::validate(std::string *error) const
{
if (kind == WordOperandKind::Constant)
{
return true;
}
if (kind != WordOperandKind::Register)
{
setError(error, "字操作数使用了不支持的类型");
return false;
}
if (!address.isValid() || address.area() != RegisterArea::D)
{
setError(error, "字操作数必须使用有效的 D 区地址");
return false;
}
return true;
}

CounterAddress::CounterAddress(int index)
: index_(index)
{
}

int CounterAddress::index() const
{
return index_;
}

bool CounterAddress::isValid() const
{
return index_ >= kMinimumIndex && index_ <= kMaximumIndex;
}

std::string CounterAddress::toString() const
{
return isValid() ? "C" + std::to_string(index_) : "InvalidCounterAddress";
}

bool CounterAddress::operator==(const CounterAddress &other) const
{
return index_ == other.index_;
}

bool CounterAddress::operator!=(const CounterAddress &other) const
{
return !(*this == other);
}

int TimerAddress::index() const
{
return index_;
@@ -227,11 +365,76 @@ std::optional<RegisterAddress> registerAddressForLogicNode(
{
return value.address;
}
else if constexpr (std::is_same_v<Config, CounterNodeConfig>)
{
return value.currentValueAddress;
}
else if constexpr (std::is_same_v<Config, MoveNodeConfig>
|| std::is_same_v<Config, ArithmeticNodeConfig>)
{
return value.destination;
}
return std::nullopt;
},
config);
}

void collectRegisterAddressesForLogicNode(
const LogicNodeConfig &config,
std::vector<RegisterAddress> *addresses)
{
if (addresses == nullptr)
{
return;
}
std::visit(
[addresses](const auto &value)
{
using Config = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<Config, ContactNodeConfig>
|| std::is_same_v<Config, EdgeContactNodeConfig>
|| std::is_same_v<Config, CoilNodeConfig>
|| std::is_same_v<Config, CompareNodeConfig>)
{
appendValidAddress(addresses, value.address);
}
else if constexpr (std::is_same_v<Config, CounterNodeConfig>)
{
appendValidAddress(addresses, value.currentValueAddress);
appendValidAddress(addresses, value.resetAddress);
if (value.preset.kind == WordOperandKind::Register)
{
appendValidAddress(addresses, value.preset.address);
}
}
else if constexpr (std::is_same_v<Config, MoveNodeConfig>)
{
appendValidAddress(addresses, value.destination);
if (value.source.kind == WordOperandKind::Register)
{
appendValidAddress(addresses, value.source.address);
}
}
else if constexpr (std::is_same_v<Config, ArithmeticNodeConfig>)
{
appendValidAddress(addresses, value.destination);
if (value.left.kind == WordOperandKind::Register)
{
appendValidAddress(addresses, value.left.address);
}
if (value.right.kind == WordOperandKind::Register)
{
appendValidAddress(addresses, value.right.address);
}
}
else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>)
{
return;
}
},
config);
}

bool LogicNode::validate(std::string *error) const
{
if (id.empty())
@@ -251,14 +454,20 @@ bool LogicNode::isConfigured() const

bool LogicNode::isCondition() const
{
return !std::holds_alternative<CoilNodeConfig>(config)
&& !std::holds_alternative<TonNodeConfig>(config);
return std::holds_alternative<ContactNodeConfig>(config)
|| std::holds_alternative<EdgeContactNodeConfig>(config)
|| std::holds_alternative<TimerContactNodeConfig>(config)
|| std::holds_alternative<CounterContactNodeConfig>(config)
|| std::holds_alternative<CompareNodeConfig>(config);
}

bool LogicNode::isOutput() const
{
return std::holds_alternative<CoilNodeConfig>(config)
|| std::holds_alternative<TonNodeConfig>(config);
|| std::holds_alternative<TonNodeConfig>(config)
|| std::holds_alternative<CounterNodeConfig>(config)
|| std::holds_alternative<MoveNodeConfig>(config)
|| std::holds_alternative<ArithmeticNodeConfig>(config);
}

ConditionExpression ConditionExpression::fromNode(LogicNode logic_node)
@@ -483,7 +692,7 @@ bool LadderRung::validateStructure(std::string *error) const
{
if (!output->validate(error) || !output->isOutput())
{
setError(error, "梯形图网络输出必须是线圈或 TON 指令");
setError(error, "梯形图网络输出必须是有效的输出指令");
return false;
}
node_ids.push_back(output->id);
@@ -647,3 +856,80 @@ bool validateTimerReferencesForRunning(
}
return true;
}

bool validateCounterReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error)
{
std::map<int, std::string> counter_outputs;
for (const ControlLogic &logic : logics)
{
if (!logic.enabled)
{
continue;
}
for (const LadderRung &rung : logic.rungs)
{
if (!rung.output.has_value())
{
continue;
}
const auto *counter = std::get_if<CounterNodeConfig>(
&rung.output->config);
if (counter == nullptr)
{
continue;
}
const auto inserted = counter_outputs.emplace(
counter->address.index(), rung.output->id);
if (!inserted.second)
{
setError(
error,
"计数器 " + counter->address.toString()
+ " 只能由一个已启用计数指令驱动");
return false;
}
}
}

for (const ControlLogic &logic : logics)
{
if (!logic.enabled)
{
continue;
}
for (const LadderRung &rung : logic.rungs)
{
if (!rung.condition.has_value())
{
continue;
}
std::vector<const LogicNode *> nodes;
collectConditionNodes(*rung.condition, &nodes);
for (const LogicNode *node : nodes)
{
const auto *contact = std::get_if<CounterContactNodeConfig>(
&node->config);
if (contact != nullptr
&& counter_outputs.count(contact->address.index()) == 0U)
{
setError(
error,
"计数器触点 " + contact->address.toString()
+ " 没有对应的已启用计数指令");
return false;
}
}
}
}
return true;
}

bool validateLogicResourceReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error)
{
return validateTimerReferencesForRunning(logics, error)
&& validateCounterReferencesForRunning(logics, error);
}

+ 89
- 1
app/src/domain/control_logic_model.h Ver fichero

@@ -37,6 +37,21 @@ enum class ComparisonOperator
GreaterThanOrEqual
};

enum class WordOperandKind
{
Constant,
Register
};

struct WordOperand
{
WordOperandKind kind = WordOperandKind::Constant;
RegisterAddress address{RegisterArea::D, 0};
std::int16_t constant = 0;

bool validate(std::string *error = nullptr) const;
};

struct ContactNodeConfig
{
RegisterAddress address{RegisterArea::M, 0};
@@ -87,6 +102,37 @@ struct TimerContactNodeConfig
ContactMode mode = ContactMode::NormallyOpen;
};

class CounterAddress
{
public:
static constexpr int kMinimumIndex = 0;
static constexpr int kMaximumIndex = 4000;

explicit CounterAddress(int index = 0);

int index() const;
bool isValid() const;
std::string toString() const;

bool operator==(const CounterAddress &other) const;
bool operator!=(const CounterAddress &other) const;

private:
int index_ = 0;
};

enum class CounterMode
{
Up,
Down
};

struct CounterContactNodeConfig
{
CounterAddress address;
ContactMode mode = ContactMode::NormallyOpen;
};

struct TonNodeConfig
{
static constexpr int kMinimumPresetMs = 1;
@@ -96,16 +142,52 @@ struct TonNodeConfig
int presetMs = 1000;
};

struct CounterNodeConfig
{
CounterAddress address;
CounterMode mode = CounterMode::Up;
RegisterAddress currentValueAddress{RegisterArea::D, 0};
WordOperand preset;
RegisterAddress resetAddress{RegisterArea::M, 0};
};

struct MoveNodeConfig
{
WordOperand source;
RegisterAddress destination{RegisterArea::D, 0};
};

enum class ArithmeticOperation
{
Add,
Subtract
};

struct ArithmeticNodeConfig
{
ArithmeticOperation operation = ArithmeticOperation::Add;
WordOperand left;
WordOperand right;
RegisterAddress destination{RegisterArea::D, 0};
};

using LogicNodeConfig = std::variant<
ContactNodeConfig,
EdgeContactNodeConfig,
TimerContactNodeConfig,
CounterContactNodeConfig,
CoilNodeConfig,
CompareNodeConfig,
TonNodeConfig>;
TonNodeConfig,
CounterNodeConfig,
MoveNodeConfig,
ArithmeticNodeConfig>;

std::optional<RegisterAddress> registerAddressForLogicNode(
const LogicNodeConfig &config);
void collectRegisterAddressesForLogicNode(
const LogicNodeConfig &config,
std::vector<RegisterAddress> *addresses);

struct LogicNode
{
@@ -183,3 +265,9 @@ struct ControlLogic
bool validateTimerReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);
bool validateCounterReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);
bool validateLogicResourceReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);

+ 1
- 1
app/src/domain/project_model.cpp Ver fichero

@@ -226,7 +226,7 @@ bool Project::validateForRunning(std::string *error) const
return false;
}
}
if (!validateTimerReferencesForRunning(controlLogics, error))
if (!validateLogicResourceReferencesForRunning(controlLogics, error))
{
return false;
}


+ 299
- 4
app/src/infrastructure/json_project_storage.cpp Ver fichero

@@ -282,6 +282,113 @@ bool parseTimerAddress(
return true;
}

QJsonObject serializeCounterAddress(const CounterAddress &address)
{
QJsonObject object;
object.insert(QStringLiteral("index"), address.index());
return object;
}

bool parseCounterAddress(
const QJsonObject &object,
const std::string &context,
CounterAddress *address,
ParseState *state)
{
int index = 0;
if (!readInt(
object,
"index",
context,
CounterAddress::kMinimumIndex,
CounterAddress::kMaximumIndex,
&index,
state))
{
return false;
}
*address = CounterAddress{index};
return true;
}

QString wordOperandKindName(WordOperandKind kind)
{
return kind == WordOperandKind::Constant
? QStringLiteral("constant") : QStringLiteral("register");
}

QJsonObject serializeWordOperand(const WordOperand &operand)
{
QJsonObject object;
object.insert(QStringLiteral("kind"), wordOperandKindName(operand.kind));
if (operand.kind == WordOperandKind::Constant)
{
object.insert(QStringLiteral("value"), operand.constant);
}
else
{
object.insert(QStringLiteral("address"), serializeAddress(operand.address));
}
return object;
}

bool parseWordOperand(
const QJsonObject &object,
const std::string &context,
WordOperand *operand,
ParseState *state)
{
std::string kind;
if (!readString(object, "kind", context, &kind, state))
{
return false;
}
if (kind == "constant")
{
int value = 0;
if (!readInt(
object,
"value",
context,
std::numeric_limits<std::int16_t>::min(),
std::numeric_limits<std::int16_t>::max(),
&value,
state))
{
return false;
}
*operand = WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
static_cast<std::int16_t>(value)};
return true;
}
if (kind != "register")
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".kind 必须是 constant 或 register");
}
QJsonObject address_object;
RegisterAddress address{RegisterArea::D, 0};
if (!readObject(object, "address", context, &address_object, state)
|| !parseAddress(address_object, context + ".address", &address, state))
{
return false;
}
if (address.area() != RegisterArea::D)
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".address 必须使用 D 区地址");
}
*operand = WordOperand{
WordOperandKind::Register,
address,
0};
return true;
}

// 将 HMI 控件类型枚举转换为工程文件中的稳定字符串
QString hmiControlTypeName(HmiControlType type)
{
@@ -988,6 +1095,15 @@ QJsonObject serializeNodeConfig(const TimerContactNodeConfig &config)
return object;
}

QJsonObject serializeNodeConfig(const CounterContactNodeConfig &config)
{
QJsonObject object;
object.insert(QStringLiteral("type"), QStringLiteral("counterContact"));
object.insert(QStringLiteral("counter"), serializeCounterAddress(config.address));
object.insert(QStringLiteral("mode"), contactModeName(config.mode));
return object;
}

// 将线圈节点配置序列化,并写入用于反序列化分派的类型标记
QJsonObject serializeNodeConfig(const CoilNodeConfig &config)
{
@@ -1018,6 +1134,52 @@ QJsonObject serializeNodeConfig(const TonNodeConfig &config)
return object;
}

QString counterModeName(CounterMode mode)
{
return mode == CounterMode::Up
? QStringLiteral("up") : QStringLiteral("down");
}

QJsonObject serializeNodeConfig(const CounterNodeConfig &config)
{
QJsonObject object;
object.insert(QStringLiteral("type"), QStringLiteral("counter"));
object.insert(QStringLiteral("counter"), serializeCounterAddress(config.address));
object.insert(QStringLiteral("mode"), counterModeName(config.mode));
object.insert(
QStringLiteral("currentValueAddress"),
serializeAddress(config.currentValueAddress));
object.insert(QStringLiteral("preset"), serializeWordOperand(config.preset));
object.insert(QStringLiteral("resetAddress"), serializeAddress(config.resetAddress));
return object;
}

QJsonObject serializeNodeConfig(const MoveNodeConfig &config)
{
QJsonObject object;
object.insert(QStringLiteral("type"), QStringLiteral("move"));
object.insert(QStringLiteral("source"), serializeWordOperand(config.source));
object.insert(QStringLiteral("destination"), serializeAddress(config.destination));
return object;
}

QString arithmeticOperationName(ArithmeticOperation operation)
{
return operation == ArithmeticOperation::Add
? QStringLiteral("add") : QStringLiteral("subtract");
}

QJsonObject serializeNodeConfig(const ArithmeticNodeConfig &config)
{
QJsonObject object;
object.insert(QStringLiteral("type"), QStringLiteral("arithmetic"));
object.insert(QStringLiteral("operation"), arithmeticOperationName(config.operation));
object.insert(QStringLiteral("left"), serializeWordOperand(config.left));
object.insert(QStringLiteral("right"), serializeWordOperand(config.right));
object.insert(QStringLiteral("destination"), serializeAddress(config.destination));
return object;
}

// 将逻辑节点序列化,并根据 variant 中的实际配置类型选择对应重载
QJsonObject serializeLogicNode(const LogicNode &node)
{
@@ -1049,15 +1211,82 @@ bool parseNodeConfig(
return false;
}

if (type == "timerContact" || type == "ton")
if (type == "timerContact" || type == "ton"
|| type == "counterContact" || type == "counter")
{
QJsonObject timer_object;
if (!readObject(object, "timer", context, &timer_object, state))
const char *resource_field =
type == "timerContact" || type == "ton" ? "timer" : "counter";
QJsonObject resource_object;
if (!readObject(object, resource_field, context, &resource_object, state))
{
return false;
}
if (type == "counterContact" || type == "counter")
{
CounterAddress address;
if (!parseCounterAddress(
resource_object, context + ".counter", &address, state))
{
return false;
}
if (type == "counterContact")
{
std::string mode_text;
ContactMode mode = ContactMode::NormallyOpen;
if (!readString(object, "mode", context, &mode_text, state)
|| !parseContactMode(mode_text, &mode, state))
{
return false;
}
*config = CounterContactNodeConfig{address, mode};
return true;
}
std::string mode_text;
CounterMode mode = CounterMode::Up;
if (!readString(object, "mode", context, &mode_text, state))
{
return false;
}
if (mode_text == "up")
{
mode = CounterMode::Up;
}
else if (mode_text == "down")
{
mode = CounterMode::Down;
}
else
{
return state->fail(
ProjectStorageError::InvalidField,
"不支持的计数器方向:" + mode_text);
}
QJsonObject current_object;
QJsonObject preset_object;
QJsonObject reset_object;
RegisterAddress current{RegisterArea::D, 0};
RegisterAddress reset{RegisterArea::M, 0};
WordOperand preset;
if (!readObject(
object, "currentValueAddress", context, &current_object, state)
|| !parseAddress(
current_object,
context + ".currentValueAddress",
&current,
state)
|| !readObject(object, "preset", context, &preset_object, state)
|| !parseWordOperand(preset_object, context + ".preset", &preset, state)
|| !readObject(object, "resetAddress", context, &reset_object, state)
|| !parseAddress(
reset_object, context + ".resetAddress", &reset, state))
{
return false;
}
*config = CounterNodeConfig{address, mode, current, preset, reset};
return true;
}
TimerAddress address;
if (!parseTimerAddress(timer_object, context + ".timer", &address, state))
if (!parseTimerAddress(resource_object, context + ".timer", &address, state))
{
return false;
}
@@ -1089,6 +1318,71 @@ bool parseNodeConfig(
return true;
}

if (type == "move")
{
QJsonObject source_object;
QJsonObject destination_object;
WordOperand source;
RegisterAddress destination{RegisterArea::D, 0};
if (!readObject(object, "source", context, &source_object, state)
|| !parseWordOperand(source_object, context + ".source", &source, state)
|| !readObject(object, "destination", context, &destination_object, state)
|| !parseAddress(
destination_object,
context + ".destination",
&destination,
state))
{
return false;
}
*config = MoveNodeConfig{source, destination};
return true;
}
if (type == "arithmetic")
{
std::string operation_text;
QJsonObject left_object;
QJsonObject right_object;
QJsonObject destination_object;
WordOperand left;
WordOperand right;
RegisterAddress destination{RegisterArea::D, 0};
ArithmeticOperation operation = ArithmeticOperation::Add;
if (!readString(object, "operation", context, &operation_text, state))
{
return false;
}
if (operation_text == "add")
{
operation = ArithmeticOperation::Add;
}
else if (operation_text == "subtract")
{
operation = ArithmeticOperation::Subtract;
}
else
{
return state->fail(
ProjectStorageError::InvalidField,
"不支持的算术运算:" + operation_text);
}
if (!readObject(object, "left", context, &left_object, state)
|| !parseWordOperand(left_object, context + ".left", &left, state)
|| !readObject(object, "right", context, &right_object, state)
|| !parseWordOperand(right_object, context + ".right", &right, state)
|| !readObject(object, "destination", context, &destination_object, state)
|| !parseAddress(
destination_object,
context + ".destination",
&destination,
state))
{
return false;
}
*config = ArithmeticNodeConfig{operation, left, right, destination};
return true;
}

QJsonObject address_object;
if (!readObject(object, "address", context, &address_object, state))
{
@@ -1161,6 +1455,7 @@ bool parseNodeConfig(
return true;
}


return state->fail(
ProjectStorageError::InvalidField,
"不支持的逻辑节点类型:" + type);


+ 118
- 0
app/tests/domain_tests.cpp Ver fichero

@@ -310,6 +310,123 @@ void testTimerReferencesForRunning()
"disabled timer drafts must not block runtime timer validation");
}

LadderRung makeCounterRung(
const std::string &rung_id,
const std::string &output_id,
int counter_index)
{
LogicNode condition;
condition.id = rung_id + "-input";
condition.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, counter_index},
ContactMode::NormallyOpen};
LogicNode output;
output.id = output_id;
output.config = CounterNodeConfig{
CounterAddress{counter_index},
CounterMode::Up,
RegisterAddress{RegisterArea::D, counter_index},
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
10},
RegisterAddress{RegisterArea::M, counter_index + 1}};
LadderRung rung;
rung.id = rung_id;
rung.name = rung_id;
rung.condition = ConditionExpression::fromNode(condition);
rung.output = output;
return rung;
}

void testCounterAndDataInstructionBoundaries()
{
require(CounterAddress{0}.isValid() && CounterAddress{4000}.isValid(),
"C0 and C4000 must be valid counter resources");
require(!CounterAddress{-1}.isValid() && !CounterAddress{4001}.isValid(),
"counter resources outside 0 through 4000 must be rejected");

LogicNode counter;
counter.id = "counter";
counter.config = CounterNodeConfig{
CounterAddress{0},
CounterMode::Up,
RegisterAddress{RegisterArea::D, 10},
WordOperand{
WordOperandKind::Register,
RegisterAddress{RegisterArea::D, 11},
0},
RegisterAddress{RegisterArea::M, 12}};
require(counter.validate(),
"a counter with C identity and external M/D addresses must be valid");

CounterNodeConfig invalid_counter = std::get<CounterNodeConfig>(counter.config);
invalid_counter.currentValueAddress = RegisterAddress{RegisterArea::M, 10};
counter.config = invalid_counter;
require(!counter.validate(), "counter CV must reject M addresses");

LogicNode move;
move.id = "move";
move.config = MoveNodeConfig{
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
-100},
RegisterAddress{RegisterArea::D, 20}};
require(move.validate() && move.isOutput(),
"MOVE with a constant source and D destination must be a valid output");

LogicNode add;
add.id = "add";
add.config = ArithmeticNodeConfig{
ArithmeticOperation::Add,
WordOperand{
WordOperandKind::Register,
RegisterAddress{RegisterArea::D, 20},
0},
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
1},
RegisterAddress{RegisterArea::D, 20}};
require(add.validate() && add.isOutput(),
"ADD must allow the same D register as source and destination");

ControlLogic valid;
valid.id = "counter-valid";
valid.name = "Counter valid";
valid.rungs.push_back(makeCounterRung("counter-rung", "ctu-0", 0));
require(validateCounterReferencesForRunning({valid}),
"a counter output without contacts must pass reference validation");

ControlLogic duplicate = valid;
duplicate.id = "counter-duplicate";
duplicate.name = "Counter duplicate";
duplicate.rungs.front().output->id = "ctu-duplicate";
require(!validateCounterReferencesForRunning({valid, duplicate}),
"the same C resource must not have multiple enabled drivers");

ControlLogic missing;
missing.id = "counter-missing";
missing.name = "Counter missing";
LogicNode missing_contact;
missing_contact.id = "missing-counter-contact";
missing_contact.config = CounterContactNodeConfig{
CounterAddress{7}, ContactMode::NormallyOpen};
LogicNode output;
output.id = "missing-counter-coil";
output.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 7}, CoilMode::Normal};
LadderRung missing_rung;
missing_rung.id = "missing-counter-rung";
missing_rung.name = "Missing counter rung";
missing_rung.condition = ConditionExpression::fromNode(missing_contact);
missing_rung.output = output;
missing.rungs.push_back(missing_rung);
require(!validateCounterReferencesForRunning({missing}),
"a C contact without an enabled counter driver must be rejected");
}

void testLadderLogicBoundaries()
{
LogicNode stop;
@@ -470,6 +587,7 @@ int main()
testLogicNodeConfigurationBoundaries();
testTimerAndCommentBoundaries();
testTimerReferencesForRunning();
testCounterAndDataInstructionBoundaries();
testLadderLogicBoundaries();
testModelsValidateBindingsAndIdentifiers();
testMultiPageAndLogicDomainRules();


+ 140
- 0
app/tests/project_management_tests.cpp Ver fichero

@@ -13,6 +13,7 @@
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>

namespace {

@@ -184,6 +185,112 @@ Project makeExampleProject()
timer_rung.output = timer_coil;
logic.rungs.push_back(timer_rung);

LogicNode counter_input;
counter_input.id = "counter-input";
counter_input.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, 7}, ContactMode::NormallyOpen};
LogicNode counter_output;
counter_output.id = "counter-output";
counter_output.config = CounterNodeConfig{
CounterAddress{3},
CounterMode::Up,
RegisterAddress{RegisterArea::D, 10},
WordOperand{
WordOperandKind::Register,
RegisterAddress{RegisterArea::D, 11},
0},
RegisterAddress{RegisterArea::M, 8}};
LadderRung counter_rung;
counter_rung.id = "counter-rung";
counter_rung.name = "Counter network";
counter_rung.comment = "计数器上升沿计数";
counter_rung.condition = ConditionExpression::fromNode(counter_input);
counter_rung.output = counter_output;
logic.rungs.push_back(counter_rung);

LogicNode counter_contact;
counter_contact.id = "counter-contact";
counter_contact.config = CounterContactNodeConfig{
CounterAddress{3}, ContactMode::NormallyOpen};
LogicNode counter_coil;
counter_coil.id = "counter-coil";
counter_coil.config = CoilNodeConfig{
RegisterAddress{RegisterArea::M, 9}, CoilMode::Normal};
LadderRung counter_feedback_rung;
counter_feedback_rung.id = "counter-feedback-rung";
counter_feedback_rung.name = "Counter feedback";
counter_feedback_rung.condition = ConditionExpression::fromNode(counter_contact);
counter_feedback_rung.output = counter_coil;
logic.rungs.push_back(counter_feedback_rung);

const auto addDataRung = [&logic](
const std::string &rung_id,
const std::string &input_id,
int input_address,
LogicNode output)
{
LogicNode input;
input.id = input_id;
input.config = ContactNodeConfig{
RegisterAddress{RegisterArea::M, input_address},
ContactMode::NormallyOpen};
LadderRung data_rung;
data_rung.id = rung_id;
data_rung.name = rung_id;
data_rung.condition = ConditionExpression::fromNode(input);
data_rung.output = std::move(output);
logic.rungs.push_back(std::move(data_rung));
};
addDataRung(
"move-rung",
"move-input",
20,
LogicNode{
"move-output",
MoveNodeConfig{
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
25},
RegisterAddress{RegisterArea::D, 20}},
true});
addDataRung(
"add-rung",
"add-input",
21,
LogicNode{
"add-output",
ArithmeticNodeConfig{
ArithmeticOperation::Add,
WordOperand{
WordOperandKind::Register,
RegisterAddress{RegisterArea::D, 20},
0},
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
1},
RegisterAddress{RegisterArea::D, 21}},
true});
addDataRung(
"sub-rung",
"sub-input",
22,
LogicNode{
"sub-output",
ArithmeticNodeConfig{
ArithmeticOperation::Subtract,
WordOperand{
WordOperandKind::Register,
RegisterAddress{RegisterArea::D, 21},
0},
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
1},
RegisterAddress{RegisterArea::D, 22}},
true});

Project project;
project.metadata = {"example-project", "Example project", "1.0"};
project.hmiPages.push_back(page);
@@ -297,6 +404,12 @@ void testExampleProjectRoundTrip()
&& saved_json.contains("\"type\": \"edgeContact\"")
&& saved_json.contains("\"type\": \"timerContact\"")
&& saved_json.contains("\"type\": \"ton\"")
&& saved_json.contains("\"type\": \"counterContact\"")
&& saved_json.contains("\"type\": \"counter\"")
&& saved_json.contains("\"type\": \"move\"")
&& saved_json.contains("\"type\": \"arithmetic\"")
&& saved_json.contains("\"operation\": \"add\"")
&& saved_json.contains("\"operation\": \"subtract\"")
&& saved_json.contains("\"comment\": \"启动条件与温度检查\"")
&& saved_json.contains("\"type\": \"alarmList\""),
"version 1.0 projects must persist pages and alarm definitions");
@@ -390,6 +503,33 @@ void testExampleProjectRoundTrip()
== EdgeMode::Falling,
"falling edge and T contact configurations must survive round trip");

const LadderRung &counter_rung = project.controlLogics.front().rungs.at(3);
const auto &counter = std::get<CounterNodeConfig>(counter_rung.output->config);
require(counter.address == CounterAddress{3}
&& counter.currentValueAddress == RegisterAddress{RegisterArea::D, 10}
&& counter.preset.kind == WordOperandKind::Register
&& counter.preset.address == RegisterAddress{RegisterArea::D, 11}
&& counter.resetAddress == RegisterAddress{RegisterArea::M, 8},
"counter resource and external M/D addresses must survive round trip");
const LadderRung &counter_feedback = project.controlLogics.front().rungs.at(4);
require(std::holds_alternative<CounterContactNodeConfig>(
counter_feedback.condition->node->config)
&& std::get<CounterContactNodeConfig>(
counter_feedback.condition->node->config).address
== CounterAddress{3},
"counter contacts must survive round trip");
require(std::get<MoveNodeConfig>(
project.controlLogics.front().rungs.at(5).output->config)
.source.constant == 25,
"MOVE operands must survive round trip");
require(std::get<ArithmeticNodeConfig>(
project.controlLogics.front().rungs.at(6).output->config)
.operation == ArithmeticOperation::Add
&& std::get<ArithmeticNodeConfig>(
project.controlLogics.front().rungs.at(7).output->config)
.operation == ArithmeticOperation::Subtract,
"ADD and SUB operations must survive round trip");

const auto &compare = std::get<CompareNodeConfig>(
rung.condition->children.at(1).node->config);
require(compare.address.index() == 2 && compare.value == 100,


Cargando…
Cancelar
Guardar