| @@ -31,6 +31,35 @@ bool validateConfig(const ContactNodeConfig &config, std::string *error) | |||||
| return true; | 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) | bool validateConfig(const EdgeContactNodeConfig &config, std::string *error) | ||||
| { | { | ||||
| if (!config.address.isValid() || config.address.area() != RegisterArea::M) | if (!config.address.isValid() || config.address.area() != RegisterArea::M) | ||||
| @@ -115,6 +144,56 @@ bool validateConfig(const TonNodeConfig &config, std::string *error) | |||||
| return true; | 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> | template<typename TItem> | ||||
| bool hasDuplicateId(const std::vector<TItem> &items) | 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 | } // namespace | ||||
| TimerAddress::TimerAddress(int index) | 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 | int TimerAddress::index() const | ||||
| { | { | ||||
| return index_; | return index_; | ||||
| @@ -227,11 +365,76 @@ std::optional<RegisterAddress> registerAddressForLogicNode( | |||||
| { | { | ||||
| return value.address; | 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; | return std::nullopt; | ||||
| }, | }, | ||||
| config); | 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 | bool LogicNode::validate(std::string *error) const | ||||
| { | { | ||||
| if (id.empty()) | if (id.empty()) | ||||
| @@ -251,14 +454,20 @@ bool LogicNode::isConfigured() const | |||||
| bool LogicNode::isCondition() 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 | bool LogicNode::isOutput() const | ||||
| { | { | ||||
| return std::holds_alternative<CoilNodeConfig>(config) | 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) | ConditionExpression ConditionExpression::fromNode(LogicNode logic_node) | ||||
| @@ -483,7 +692,7 @@ bool LadderRung::validateStructure(std::string *error) const | |||||
| { | { | ||||
| if (!output->validate(error) || !output->isOutput()) | if (!output->validate(error) || !output->isOutput()) | ||||
| { | { | ||||
| setError(error, "梯形图网络输出必须是线圈或 TON 指令"); | |||||
| setError(error, "梯形图网络输出必须是有效的输出指令"); | |||||
| return false; | return false; | ||||
| } | } | ||||
| node_ids.push_back(output->id); | node_ids.push_back(output->id); | ||||
| @@ -647,3 +856,80 @@ bool validateTimerReferencesForRunning( | |||||
| } | } | ||||
| return true; | 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 | 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 | struct ContactNodeConfig | ||||
| { | { | ||||
| RegisterAddress address{RegisterArea::M, 0}; | RegisterAddress address{RegisterArea::M, 0}; | ||||
| @@ -87,6 +102,37 @@ struct TimerContactNodeConfig | |||||
| ContactMode mode = ContactMode::NormallyOpen; | 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 | struct TonNodeConfig | ||||
| { | { | ||||
| static constexpr int kMinimumPresetMs = 1; | static constexpr int kMinimumPresetMs = 1; | ||||
| @@ -96,16 +142,52 @@ struct TonNodeConfig | |||||
| int presetMs = 1000; | 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< | using LogicNodeConfig = std::variant< | ||||
| ContactNodeConfig, | ContactNodeConfig, | ||||
| EdgeContactNodeConfig, | EdgeContactNodeConfig, | ||||
| TimerContactNodeConfig, | TimerContactNodeConfig, | ||||
| CounterContactNodeConfig, | |||||
| CoilNodeConfig, | CoilNodeConfig, | ||||
| CompareNodeConfig, | CompareNodeConfig, | ||||
| TonNodeConfig>; | |||||
| TonNodeConfig, | |||||
| CounterNodeConfig, | |||||
| MoveNodeConfig, | |||||
| ArithmeticNodeConfig>; | |||||
| std::optional<RegisterAddress> registerAddressForLogicNode( | std::optional<RegisterAddress> registerAddressForLogicNode( | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| void collectRegisterAddressesForLogicNode( | |||||
| const LogicNodeConfig &config, | |||||
| std::vector<RegisterAddress> *addresses); | |||||
| struct LogicNode | struct LogicNode | ||||
| { | { | ||||
| @@ -183,3 +265,9 @@ struct ControlLogic | |||||
| bool validateTimerReferencesForRunning( | bool validateTimerReferencesForRunning( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| std::string *error = nullptr); | 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; | return false; | ||||
| } | } | ||||
| } | } | ||||
| if (!validateTimerReferencesForRunning(controlLogics, error)) | |||||
| if (!validateLogicResourceReferencesForRunning(controlLogics, error)) | |||||
| { | { | ||||
| return false; | return false; | ||||
| } | } | ||||
| @@ -282,6 +282,113 @@ bool parseTimerAddress( | |||||
| return true; | 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 控件类型枚举转换为工程文件中的稳定字符串 | // 将 HMI 控件类型枚举转换为工程文件中的稳定字符串 | ||||
| QString hmiControlTypeName(HmiControlType type) | QString hmiControlTypeName(HmiControlType type) | ||||
| { | { | ||||
| @@ -988,6 +1095,15 @@ QJsonObject serializeNodeConfig(const TimerContactNodeConfig &config) | |||||
| return object; | 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) | QJsonObject serializeNodeConfig(const CoilNodeConfig &config) | ||||
| { | { | ||||
| @@ -1018,6 +1134,52 @@ QJsonObject serializeNodeConfig(const TonNodeConfig &config) | |||||
| return object; | 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 中的实际配置类型选择对应重载 | // 将逻辑节点序列化,并根据 variant 中的实际配置类型选择对应重载 | ||||
| QJsonObject serializeLogicNode(const LogicNode &node) | QJsonObject serializeLogicNode(const LogicNode &node) | ||||
| { | { | ||||
| @@ -1049,15 +1211,82 @@ bool parseNodeConfig( | |||||
| return false; | 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; | 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; | TimerAddress address; | ||||
| if (!parseTimerAddress(timer_object, context + ".timer", &address, state)) | |||||
| if (!parseTimerAddress(resource_object, context + ".timer", &address, state)) | |||||
| { | { | ||||
| return false; | return false; | ||||
| } | } | ||||
| @@ -1089,6 +1318,71 @@ bool parseNodeConfig( | |||||
| return true; | 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; | QJsonObject address_object; | ||||
| if (!readObject(object, "address", context, &address_object, state)) | if (!readObject(object, "address", context, &address_object, state)) | ||||
| { | { | ||||
| @@ -1161,6 +1455,7 @@ bool parseNodeConfig( | |||||
| return true; | return true; | ||||
| } | } | ||||
| return state->fail( | return state->fail( | ||||
| ProjectStorageError::InvalidField, | ProjectStorageError::InvalidField, | ||||
| "不支持的逻辑节点类型:" + type); | "不支持的逻辑节点类型:" + type); | ||||
| @@ -310,6 +310,123 @@ void testTimerReferencesForRunning() | |||||
| "disabled timer drafts must not block runtime timer validation"); | "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() | void testLadderLogicBoundaries() | ||||
| { | { | ||||
| LogicNode stop; | LogicNode stop; | ||||
| @@ -470,6 +587,7 @@ int main() | |||||
| testLogicNodeConfigurationBoundaries(); | testLogicNodeConfigurationBoundaries(); | ||||
| testTimerAndCommentBoundaries(); | testTimerAndCommentBoundaries(); | ||||
| testTimerReferencesForRunning(); | testTimerReferencesForRunning(); | ||||
| testCounterAndDataInstructionBoundaries(); | |||||
| testLadderLogicBoundaries(); | testLadderLogicBoundaries(); | ||||
| testModelsValidateBindingsAndIdentifiers(); | testModelsValidateBindingsAndIdentifiers(); | ||||
| testMultiPageAndLogicDomainRules(); | testMultiPageAndLogicDomainRules(); | ||||
| @@ -13,6 +13,7 @@ | |||||
| #include <iostream> | #include <iostream> | ||||
| #include <stdexcept> | #include <stdexcept> | ||||
| #include <string> | #include <string> | ||||
| #include <utility> | |||||
| namespace { | namespace { | ||||
| @@ -184,6 +185,112 @@ Project makeExampleProject() | |||||
| timer_rung.output = timer_coil; | timer_rung.output = timer_coil; | ||||
| logic.rungs.push_back(timer_rung); | 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 project; | ||||
| project.metadata = {"example-project", "Example project", "1.0"}; | project.metadata = {"example-project", "Example project", "1.0"}; | ||||
| project.hmiPages.push_back(page); | project.hmiPages.push_back(page); | ||||
| @@ -297,6 +404,12 @@ void testExampleProjectRoundTrip() | |||||
| && saved_json.contains("\"type\": \"edgeContact\"") | && saved_json.contains("\"type\": \"edgeContact\"") | ||||
| && saved_json.contains("\"type\": \"timerContact\"") | && saved_json.contains("\"type\": \"timerContact\"") | ||||
| && saved_json.contains("\"type\": \"ton\"") | && 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("\"comment\": \"启动条件与温度检查\"") | ||||
| && saved_json.contains("\"type\": \"alarmList\""), | && saved_json.contains("\"type\": \"alarmList\""), | ||||
| "version 1.0 projects must persist pages and alarm definitions"); | "version 1.0 projects must persist pages and alarm definitions"); | ||||
| @@ -390,6 +503,33 @@ void testExampleProjectRoundTrip() | |||||
| == EdgeMode::Falling, | == EdgeMode::Falling, | ||||
| "falling edge and T contact configurations must survive round trip"); | "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>( | const auto &compare = std::get<CompareNodeConfig>( | ||||
| rung.condition->children.at(1).node->config); | rung.condition->children.at(1).node->config); | ||||
| require(compare.address.index() == 2 && compare.value == 100, | require(compare.address.index() == 2 && compare.value == 100, | ||||