| @@ -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); | |||
| } | |||
| @@ -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); | |||
| @@ -226,7 +226,7 @@ bool Project::validateForRunning(std::string *error) const | |||
| return false; | |||
| } | |||
| } | |||
| if (!validateTimerReferencesForRunning(controlLogics, error)) | |||
| if (!validateLogicResourceReferencesForRunning(controlLogics, error)) | |||
| { | |||
| return false; | |||
| } | |||
| @@ -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, ¤t_object, state) | |||
| || !parseAddress( | |||
| current_object, | |||
| context + ".currentValueAddress", | |||
| ¤t, | |||
| 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); | |||
| @@ -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(); | |||
| @@ -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, | |||