| @@ -3,7 +3,6 @@ | |||
| #include "project_limits.h" | |||
| #include <algorithm> | |||
| #include <map> | |||
| #include <type_traits> | |||
| #include <utility> | |||
| @@ -52,22 +51,6 @@ bool validateWordAddress( | |||
| 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) | |||
| @@ -120,70 +103,6 @@ bool validateConfig(const CompareNodeConfig &config, std::string *error) | |||
| return true; | |||
| } | |||
| bool validateConfig(const TimerContactNodeConfig &config, std::string *error) | |||
| { | |||
| if (!config.address.isValid()) | |||
| { | |||
| setError(error, "定时器触点必须使用有效的 T 地址"); | |||
| return false; | |||
| } | |||
| if (config.mode != ContactMode::NormallyOpen | |||
| && config.mode != ContactMode::NormallyClosed) | |||
| { | |||
| setError(error, "定时器触点使用了不支持的模式"); | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| bool validateConfig(const TonNodeConfig &config, std::string *error) | |||
| { | |||
| if (!config.address.isValid()) | |||
| { | |||
| setError(error, "TON 必须使用有效的 T 地址"); | |||
| return false; | |||
| } | |||
| if (config.presetMs < TonNodeConfig::kMinimumPresetMs | |||
| || config.presetMs > TonNodeConfig::kMaximumPresetMs) | |||
| { | |||
| setError(error, "TON 预设时间必须在 1~86400000 ms 范围内"); | |||
| return false; | |||
| } | |||
| 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.preset.kind == WordOperandKind::Constant | |||
| && config.preset.constant < 0) | |||
| { | |||
| setError(error, "计数器常量预设值必须在 0~32767 范围内"); | |||
| 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)) | |||
| @@ -419,11 +338,6 @@ bool validateConditionExpression( | |||
| } // namespace | |||
| TimerAddress::TimerAddress(int index) | |||
| : index_(index) | |||
| { | |||
| } | |||
| bool WordOperand::validate(std::string *error) const | |||
| { | |||
| if (kind == WordOperandKind::Constant) | |||
| @@ -443,61 +357,6 @@ bool WordOperand::validate(std::string *error) const | |||
| 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_; | |||
| } | |||
| bool TimerAddress::isValid() const | |||
| { | |||
| return index_ >= kMinimumIndex && index_ <= kMaximumIndex; | |||
| } | |||
| std::string TimerAddress::toString() const | |||
| { | |||
| return isValid() ? "T" + std::to_string(index_) : "InvalidTimerAddress"; | |||
| } | |||
| bool TimerAddress::operator==(const TimerAddress &other) const | |||
| { | |||
| return index_ == other.index_; | |||
| } | |||
| bool TimerAddress::operator!=(const TimerAddress &other) const | |||
| { | |||
| return !(*this == other); | |||
| } | |||
| std::optional<RegisterAddress> registerAddressForLogicNode( | |||
| const LogicNodeConfig &config) | |||
| { | |||
| @@ -512,10 +371,6 @@ 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>) | |||
| { | |||
| @@ -545,15 +400,6 @@ void collectRegisterAddressesForLogicNode( | |||
| { | |||
| 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); | |||
| @@ -574,10 +420,6 @@ void collectRegisterAddressesForLogicNode( | |||
| appendValidAddress(addresses, value.right.address); | |||
| } | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | |||
| { | |||
| return; | |||
| } | |||
| }, | |||
| config); | |||
| } | |||
| @@ -609,8 +451,6 @@ bool LogicNode::isCondition() const | |||
| { | |||
| 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); | |||
| } | |||
| @@ -618,8 +458,6 @@ bool LogicNode::isCondition() const | |||
| bool LogicNode::isOutput() const | |||
| { | |||
| return std::holds_alternative<CoilNodeConfig>(config) | |||
| || std::holds_alternative<TonNodeConfig>(config) | |||
| || std::holds_alternative<CounterNodeConfig>(config) | |||
| || std::holds_alternative<MoveNodeConfig>(config) | |||
| || std::holds_alternative<ArithmeticNodeConfig>(config); | |||
| } | |||
| @@ -984,148 +822,3 @@ bool ControlLogic::validateForRunning(std::string *error) const | |||
| } | |||
| return true; | |||
| } | |||
| bool validateTimerReferencesForRunning( | |||
| const std::vector<ControlLogic> &logics, | |||
| std::string *error) | |||
| { | |||
| std::map<int, std::string> timer_outputs; | |||
| for (const ControlLogic &logic : logics) | |||
| { | |||
| if (!logic.enabled) | |||
| { | |||
| continue; | |||
| } | |||
| for (const LadderRung &rung : logic.rungs) | |||
| { | |||
| if (!rung.output.has_value()) | |||
| { | |||
| continue; | |||
| } | |||
| const auto *ton = std::get_if<TonNodeConfig>(&rung.output->config); | |||
| if (ton == nullptr) | |||
| { | |||
| continue; | |||
| } | |||
| const auto inserted = timer_outputs.emplace( | |||
| ton->address.index(), rung.output->id); | |||
| if (!inserted.second) | |||
| { | |||
| setError( | |||
| error, | |||
| "定时器 " + ton->address.toString() | |||
| + " 只能由一个已启用 TON 指令驱动"); | |||
| 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<TimerContactNodeConfig>( | |||
| &node->config); | |||
| if (contact != nullptr | |||
| && timer_outputs.count(contact->address.index()) == 0U) | |||
| { | |||
| setError( | |||
| error, | |||
| "定时器触点 " + contact->address.toString() | |||
| + " 没有对应的已启用 TON 指令"); | |||
| return false; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| 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); | |||
| } | |||
| @@ -49,7 +49,7 @@ enum class WordOperandKind | |||
| Register // 使用 D 寄存器中的数值 | |||
| }; | |||
| // MOVE、ADD/SUB、计数器预置值共用的字操作数 | |||
| // MOVE、ADD/SUB 共用的字操作数 | |||
| struct WordOperand | |||
| { | |||
| WordOperandKind kind = WordOperandKind::Constant; // 操作数来源 | |||
| @@ -94,90 +94,6 @@ struct CompareNodeConfig | |||
| std::int16_t value = 0; | |||
| }; | |||
| // 定时器 T 编号,内部使用,不映射 M/D 寄存器;限制编号范围,判断是否合法,可转字符串 | |||
| class TimerAddress | |||
| { | |||
| public: | |||
| static constexpr int kMinimumIndex = 0; // 允许使用的最小 T 编号 | |||
| static constexpr int kMaximumIndex = 4000; // 允许使用的最大 T 编号 | |||
| explicit TimerAddress(int index = 0); // 使用编号创建 T 地址 | |||
| int index() const; // 返回地址编号 | |||
| bool isValid() const; // 判断地址编号是否在有效范围内 | |||
| std::string toString() const; // 转成 T0 这样的显示文本 | |||
| bool operator==(const TimerAddress &other) const; // 判断两个 T 地址是否相同 | |||
| bool operator!=(const TimerAddress &other) const; // 判断两个 T 地址是否不同 | |||
| private: | |||
| int index_ = 0; // 保存 T 地址编号 | |||
| }; | |||
| // 读取 TON 完成状态的条件触点 | |||
| struct TimerContactNodeConfig | |||
| { | |||
| // 要读取完成状态的定时器地址 | |||
| TimerAddress address; | |||
| // 按常开或常闭方式判断完成状态 | |||
| ContactMode mode = ContactMode::NormallyOpen; | |||
| }; | |||
| // 离线执行器内部使用的 C 区地址,不会直接映射 PLC C 区 | |||
| class CounterAddress | |||
| { | |||
| public: | |||
| static constexpr int kMinimumIndex = 0; // 允许使用的最小 C 编号 | |||
| static constexpr int kMaximumIndex = 4000; // 允许使用的最大 C 编号 | |||
| explicit CounterAddress(int index = 0); // 使用编号创建 C 地址 | |||
| int index() const; // 返回地址编号 | |||
| bool isValid() const; // 判断地址编号是否在有效范围内 | |||
| std::string toString() const; // 转成 C0 这样的显示文本 | |||
| bool operator==(const CounterAddress &other) const; // 判断两个 C 地址是否相同 | |||
| bool operator!=(const CounterAddress &other) const; // 判断两个 C 地址是否不同 | |||
| private: | |||
| int index_ = 0; // 保存 C 地址编号 | |||
| }; | |||
| enum class CounterMode | |||
| { | |||
| Up, // 加计数 | |||
| Down // 减计数 | |||
| }; | |||
| // 读取计数器完成状态的条件触点 | |||
| struct CounterContactNodeConfig | |||
| { | |||
| // 要读取完成状态的计数器地址 | |||
| CounterAddress address; | |||
| // 按常开或常闭方式判断完成状态 | |||
| ContactMode mode = ContactMode::NormallyOpen; | |||
| }; | |||
| // TON 输出的定时器和预置时间,单位为毫秒 | |||
| struct TonNodeConfig | |||
| { | |||
| static constexpr int kMinimumPresetMs = 1; | |||
| static constexpr int kMaximumPresetMs = 86400000; | |||
| TimerAddress address; // 定时器地址 | |||
| int presetMs = 1000; // 延时时间,单位为毫秒 | |||
| }; | |||
| // CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D | |||
| struct CounterNodeConfig | |||
| { | |||
| CounterAddress address; // 计数器地址 | |||
| CounterMode mode = CounterMode::Up; // 加计数或减计数 | |||
| RegisterAddress currentValueAddress{RegisterArea::D, 0}; // 当前值保存到的 D 地址 | |||
| WordOperand preset; // 计数预置值 | |||
| RegisterAddress resetAddress{RegisterArea::M, 0}; // 复位输入使用的 M 地址 | |||
| }; | |||
| // MOVE 输出:把一个字操作数写入目标 D 地址 | |||
| struct MoveNodeConfig | |||
| { | |||
| @@ -204,12 +120,8 @@ struct ArithmeticNodeConfig | |||
| using LogicNodeConfig = std::variant< | |||
| ContactNodeConfig, // 普通常开或常闭触点 | |||
| EdgeContactNodeConfig, // 上升沿或下降沿触点 | |||
| TimerContactNodeConfig, // 定时器完成触点 | |||
| CounterContactNodeConfig, // 计数器完成触点 | |||
| CoilNodeConfig, // 普通、置位或复位线圈 | |||
| CompareNodeConfig, // D 寄存器比较节点 | |||
| TonNodeConfig, // TON 定时器输出 | |||
| CounterNodeConfig, // CTU 或 CTD 计数器输出 | |||
| MoveNodeConfig, // MOVE 数据传送输出 | |||
| ArithmeticNodeConfig>; // ADD 或 SUB 算术输出 | |||
| @@ -333,16 +245,3 @@ struct ControlLogic | |||
| // 校验运行所需资源和网络配置 | |||
| bool validateForRunning(std::string *error = nullptr) const; | |||
| }; | |||
| // 检查同一 T 地址是否被多个逻辑节点以冲突方式使用 | |||
| bool validateTimerReferencesForRunning( | |||
| const std::vector<ControlLogic> &logics, | |||
| std::string *error = nullptr); | |||
| // 检查同一 C 地址是否被多个计数器配置重复占用 | |||
| bool validateCounterReferencesForRunning( | |||
| const std::vector<ControlLogic> &logics, | |||
| std::string *error = nullptr); | |||
| // 一次性执行 T/C 资源引用冲突检查 | |||
| bool validateLogicResourceReferencesForRunning( | |||
| const std::vector<ControlLogic> &logics, | |||
| std::string *error = nullptr); | |||
| @@ -334,9 +334,5 @@ bool Project::validateForRunning(std::string *error) const | |||
| return false; | |||
| } | |||
| } | |||
| if (!validateLogicResourceReferencesForRunning(controlLogics, error)) | |||
| { | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| @@ -270,64 +270,6 @@ bool parseAddress( | |||
| return true; | |||
| } | |||
| QJsonObject serializeTimerAddress(const TimerAddress &address) | |||
| { | |||
| QJsonObject object; | |||
| object.insert(QStringLiteral("index"), address.index()); | |||
| return object; | |||
| } | |||
| bool parseTimerAddress( | |||
| const QJsonObject &object, | |||
| const std::string &context, | |||
| TimerAddress *address, | |||
| ParseState *state) | |||
| { | |||
| int index = 0; | |||
| if (!readInt( | |||
| object, | |||
| "index", | |||
| context, | |||
| TimerAddress::kMinimumIndex, | |||
| TimerAddress::kMaximumIndex, | |||
| &index, | |||
| state)) | |||
| { | |||
| return false; | |||
| } | |||
| *address = TimerAddress{index}; | |||
| 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 | |||
| @@ -1113,24 +1055,6 @@ QJsonObject serializeNodeConfig(const EdgeContactNodeConfig &config) | |||
| return object; | |||
| } | |||
| QJsonObject serializeNodeConfig(const TimerContactNodeConfig &config) | |||
| { | |||
| QJsonObject object; | |||
| object.insert(QStringLiteral("type"), QStringLiteral("timerContact")); | |||
| object.insert(QStringLiteral("timer"), serializeTimerAddress(config.address)); | |||
| object.insert(QStringLiteral("mode"), contactModeName(config.mode)); | |||
| 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) | |||
| { | |||
| @@ -1152,35 +1076,6 @@ QJsonObject serializeNodeConfig(const CompareNodeConfig &config) | |||
| return object; | |||
| } | |||
| QJsonObject serializeNodeConfig(const TonNodeConfig &config) | |||
| { | |||
| QJsonObject object; | |||
| object.insert(QStringLiteral("type"), QStringLiteral("ton")); | |||
| object.insert(QStringLiteral("timer"), serializeTimerAddress(config.address)); | |||
| object.insert(QStringLiteral("presetMs"), config.presetMs); | |||
| 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; | |||
| @@ -1238,113 +1133,6 @@ bool parseNodeConfig( | |||
| return false; | |||
| } | |||
| if (type == "timerContact" || type == "ton" | |||
| || type == "counterContact" || type == "counter") | |||
| { | |||
| 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(resource_object, context + ".timer", &address, state)) | |||
| { | |||
| return false; | |||
| } | |||
| if (type == "timerContact") | |||
| { | |||
| std::string mode_text; | |||
| ContactMode mode = ContactMode::NormallyOpen; | |||
| if (!readString(object, "mode", context, &mode_text, state) | |||
| || !parseContactMode(mode_text, &mode, state)) | |||
| { | |||
| return false; | |||
| } | |||
| *config = TimerContactNodeConfig{address, mode}; | |||
| return true; | |||
| } | |||
| int preset_ms = 0; | |||
| if (!readInt( | |||
| object, | |||
| "presetMs", | |||
| context, | |||
| TonNodeConfig::kMinimumPresetMs, | |||
| TonNodeConfig::kMaximumPresetMs, | |||
| &preset_ms, | |||
| state)) | |||
| { | |||
| return false; | |||
| } | |||
| *config = TonNodeConfig{address, preset_ms}; | |||
| return true; | |||
| } | |||
| if (type == "move") | |||
| { | |||
| QJsonObject source_object; | |||
| @@ -1410,6 +1198,14 @@ bool parseNodeConfig( | |||
| return true; | |||
| } | |||
| if (type != "contact" && type != "edgeContact" | |||
| && type != "coil" && type != "compare") | |||
| { | |||
| return state->fail( | |||
| ProjectStorageError::InvalidField, | |||
| "不支持的逻辑节点类型:" + type); | |||
| } | |||
| QJsonObject address_object; | |||
| if (!readObject(object, "address", context, &address_object, state)) | |||
| { | |||
| @@ -1481,11 +1277,7 @@ bool parseNodeConfig( | |||
| address, comparison, static_cast<std::int16_t>(value)}; | |||
| return true; | |||
| } | |||
| return state->fail( | |||
| ProjectStorageError::InvalidField, | |||
| "不支持的逻辑节点类型:" + type); | |||
| return state->fail(ProjectStorageError::InvalidField, "逻辑节点类型解析失败"); | |||
| } | |||
| // 解析逻辑节点标识及其多态配置 | |||
| @@ -699,16 +699,6 @@ bool LogicEditorService::configsEqual( | |||
| return left_config.address == right_config.address | |||
| && left_config.mode == right_config.mode; | |||
| } | |||
| else if constexpr (std::is_same_v<Left, TimerContactNodeConfig>) | |||
| { | |||
| return left_config.address == right_config.address | |||
| && left_config.mode == right_config.mode; | |||
| } | |||
| else if constexpr (std::is_same_v<Left, CounterContactNodeConfig>) | |||
| { | |||
| return left_config.address == right_config.address | |||
| && left_config.mode == right_config.mode; | |||
| } | |||
| else if constexpr (std::is_same_v<Left, CoilNodeConfig>) | |||
| { | |||
| return left_config.address == right_config.address | |||
| @@ -720,21 +710,6 @@ bool LogicEditorService::configsEqual( | |||
| && left_config.comparison == right_config.comparison | |||
| && left_config.value == right_config.value; | |||
| } | |||
| else if constexpr (std::is_same_v<Left, TonNodeConfig>) | |||
| { | |||
| return left_config.address == right_config.address | |||
| && left_config.presetMs == right_config.presetMs; | |||
| } | |||
| else if constexpr (std::is_same_v<Left, CounterNodeConfig>) | |||
| { | |||
| return left_config.address == right_config.address | |||
| && left_config.mode == right_config.mode | |||
| && left_config.currentValueAddress == right_config.currentValueAddress | |||
| && left_config.preset.kind == right_config.preset.kind | |||
| && left_config.preset.address == right_config.preset.address | |||
| && left_config.preset.constant == right_config.preset.constant | |||
| && left_config.resetAddress == right_config.resetAddress; | |||
| } | |||
| else if constexpr (std::is_same_v<Left, MoveNodeConfig>) | |||
| { | |||
| return left_config.source.kind == right_config.source.kind | |||
| @@ -2434,16 +2409,12 @@ bool LogicEditorService::isConditionConfig(const LogicNodeConfig &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 LogicEditorService::isOutputConfig(const LogicNodeConfig &config) | |||
| { | |||
| return std::holds_alternative<CoilNodeConfig>(config) | |||
| || std::holds_alternative<TonNodeConfig>(config) | |||
| || std::holds_alternative<CounterNodeConfig>(config) | |||
| || std::holds_alternative<MoveNodeConfig>(config) | |||
| || std::holds_alternative<ArithmeticNodeConfig>(config); | |||
| } | |||
| @@ -2462,26 +2433,10 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) | |||
| { | |||
| return "edge"; | |||
| } | |||
| else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>) | |||
| { | |||
| return "timer-contact"; | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | |||
| { | |||
| return "counter-contact"; | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | |||
| { | |||
| return "coil"; | |||
| } | |||
| else if constexpr (std::is_same_v<Config, TonNodeConfig>) | |||
| { | |||
| return "ton"; | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CounterNodeConfig>) | |||
| { | |||
| return "counter"; | |||
| } | |||
| else if constexpr (std::is_same_v<Config, MoveNodeConfig>) | |||
| { | |||
| return "move"; | |||
| @@ -31,7 +31,7 @@ SimulationStartResult OfflineSimulationService::start( | |||
| return {false, SimulationStartError::InvalidLogic, validation}; | |||
| } | |||
| // 重启会话时清空 T/C/沿触发状态和虚拟 M/D,避免继承上次运行现场 | |||
| // 重启会话时清空沿触发状态和虚拟 M/D,避免继承上次运行现场 | |||
| timer_.stop(); | |||
| executor_.resetRuntime(); | |||
| repository_.clear(); | |||
| @@ -79,8 +79,6 @@ void LogicTraceValues::clear() | |||
| expressionInputValues.clear(); | |||
| expressionPowerValues.clear(); | |||
| rungValues.clear(); | |||
| tonValues.clear(); | |||
| counterValues.clear(); | |||
| wordValues.clear(); | |||
| } | |||
| @@ -103,8 +101,6 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||
| projection.expressionInputValues = values->second.expressionInputValues; | |||
| projection.expressionPowerValues = values->second.expressionPowerValues; | |||
| projection.rungValues = values->second.rungValues; | |||
| projection.tonValues = values->second.tonValues; | |||
| projection.counterValues = values->second.counterValues; | |||
| projection.wordValues = values->second.wordValues; | |||
| } | |||
| return projection; | |||
| @@ -113,7 +109,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||
| LogicScanResult SoftwareLogicExecutor::validate( | |||
| const std::vector<ControlLogic> &logics) const | |||
| { | |||
| // 先检查每个逻辑自身,再检查跨网络共享线圈和 T/C 资源的冲突 | |||
| // 先检查每个逻辑自身,再检查跨网络共享线圈的冲突 | |||
| std::map<int, CoilMode> output_modes; | |||
| for (const ControlLogic &logic : logics) | |||
| { | |||
| @@ -145,9 +141,7 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||
| const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config); | |||
| if (output == nullptr) | |||
| { | |||
| if (std::holds_alternative<TonNodeConfig>(rung.output->config) | |||
| || std::holds_alternative<CounterNodeConfig>(rung.output->config) | |||
| || std::holds_alternative<MoveNodeConfig>(rung.output->config) | |||
| if (std::holds_alternative<MoveNodeConfig>(rung.output->config) | |||
| || std::holds_alternative<ArithmeticNodeConfig>( | |||
| rung.output->config)) | |||
| { | |||
| @@ -174,35 +168,18 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||
| output_modes[output->address.index()] = output->mode; | |||
| } | |||
| } | |||
| std::string resource_error; | |||
| if (!validateLogicResourceReferencesForRunning(logics, &resource_error)) | |||
| { | |||
| return failure(LogicScanError::InvalidLogic, resource_error); | |||
| } | |||
| return success(); | |||
| } | |||
| void SoftwareLogicExecutor::resetRuntime() | |||
| { | |||
| previous_edge_inputs_.clear(); | |||
| previous_counter_inputs_.clear(); | |||
| ton_states_.clear(); | |||
| counter_states_.clear(); | |||
| } | |||
| LogicScanResult SoftwareLogicExecutor::executeScan( | |||
| const std::vector<ControlLogic> &logics, | |||
| RegisterRepository &repository, | |||
| LogicTraceSnapshot *trace) | |||
| { | |||
| return executeScanAt(logics, repository, Clock::now(), trace); | |||
| } | |||
| LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||
| const std::vector<ControlLogic> &logics, | |||
| RegisterRepository &repository, | |||
| TimePoint now, | |||
| LogicTraceSnapshot *trace) | |||
| { | |||
| const LogicScanResult validation = validate(logics); | |||
| if (!validation.succeeded) | |||
| @@ -258,11 +235,9 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||
| } | |||
| bool output_value = false; | |||
| result = executeOutput( | |||
| logic.id, | |||
| *rung.output, | |||
| rung_value, | |||
| repository, | |||
| now, | |||
| logic_trace, | |||
| &output_value); | |||
| if (!result.succeeded) | |||
| @@ -294,8 +269,6 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||
| trace->expressionInputValues = values->second.expressionInputValues; | |||
| trace->expressionPowerValues = values->second.expressionPowerValues; | |||
| trace->rungValues = values->second.rungValues; | |||
| trace->tonValues = values->second.tonValues; | |||
| trace->counterValues = values->second.counterValues; | |||
| trace->wordValues = values->second.wordValues; | |||
| } | |||
| } | |||
| @@ -383,7 +356,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||
| RegisterRepository &repository, | |||
| bool *value) | |||
| { | |||
| // 根据条件节点的具体配置读取 M/D 或内部 T/C 状态,并计算触点结果 | |||
| // 根据条件节点的具体配置读取 M/D 状态,并计算触点结果 | |||
| // 边沿触点使用 logic_id 和 node.id 组成运行时状态键,避免同名节点互相影响 | |||
| if (value == nullptr) | |||
| { | |||
| @@ -437,20 +410,6 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||
| previous_edge_inputs_[runtime_key] = read.value; | |||
| return success(); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>) | |||
| { | |||
| // 定时器触点不读 PLC 地址,而是读取本地 TON 的完成状态 | |||
| const bool done = ton_states_[config.address.index()].done; | |||
| *value = config.mode == ContactMode::NormallyOpen ? done : !done; | |||
| return success(); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | |||
| { | |||
| // 计数器触点读取本地 CTU/CTD 的完成状态 | |||
| const bool done = counter_states_[config.address.index()].done; | |||
| *value = config.mode == ContactMode::NormallyOpen ? done : !done; | |||
| return success(); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | |||
| { | |||
| // 字比较触点读取 D 字,再按配置的比较运算符与目标值比较 | |||
| @@ -525,11 +484,9 @@ LogicScanResult SoftwareLogicExecutor::readWordOperand( | |||
| } | |||
| LogicScanResult SoftwareLogicExecutor::executeOutput( | |||
| const std::string &logic_id, | |||
| const LogicNode &node, | |||
| bool rung_value, | |||
| RegisterRepository &repository, | |||
| TimePoint now, | |||
| LogicTraceValues *trace, | |||
| bool *output_value) | |||
| { | |||
| @@ -542,136 +499,6 @@ LogicScanResult SoftwareLogicExecutor::executeOutput( | |||
| {}, | |||
| node.id); | |||
| } | |||
| if (const auto *ton = std::get_if<TonNodeConfig>(&node.config)) | |||
| { | |||
| TonRuntimeState &state = ton_states_[ton->address.index()]; | |||
| if (!rung_value) | |||
| { | |||
| state = TonRuntimeState{}; | |||
| } | |||
| else if (!state.timing) | |||
| { | |||
| state.timing = true; | |||
| state.startedAt = now; | |||
| state.elapsed = std::chrono::milliseconds{0}; | |||
| state.done = false; | |||
| } | |||
| else | |||
| { | |||
| state.elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( | |||
| now >= state.startedAt ? now - state.startedAt : TimePoint::duration::zero()); | |||
| state.done = state.elapsed.count() >= ton->presetMs; | |||
| } | |||
| *output_value = state.done; | |||
| if (trace != nullptr) | |||
| { | |||
| trace->tonValues[node.id] = { | |||
| rung_value, | |||
| state.done, | |||
| state.elapsed.count(), | |||
| ton->presetMs}; | |||
| } | |||
| return success(); | |||
| } | |||
| if (const auto *counter = std::get_if<CounterNodeConfig>(&node.config)) | |||
| { | |||
| const BitReadResult reset_read = repository.readBit(counter->resetAddress); | |||
| if (!reset_read.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterReadFailed, | |||
| "读取计数器复位输入失败:" + counter->resetAddress.toString(), | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| } | |||
| std::int16_t preset = 0; | |||
| LogicScanResult result = readWordOperand( | |||
| counter->preset, repository, &preset, node.id); | |||
| if (!result.succeeded) | |||
| { | |||
| return result; | |||
| } | |||
| const std::int32_t bounded_preset = std::max<std::int32_t>(0, preset); | |||
| preset = static_cast<std::int16_t>( | |||
| std::min<std::int32_t>(bounded_preset, std::numeric_limits<std::int16_t>::max())); | |||
| const WordReadResult current_read = repository.readWord( | |||
| counter->currentValueAddress); | |||
| if (!current_read.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterReadFailed, | |||
| "读取计数当前值失败:" + counter->currentValueAddress.toString(), | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| } | |||
| std::int32_t current = std::max<std::int32_t>(0, current_read.value); | |||
| current = std::min<std::int32_t>(current, std::numeric_limits<std::int16_t>::max()); | |||
| const auto runtime_key = std::make_pair(logic_id, node.id); | |||
| const bool previous = previous_counter_inputs_[runtime_key]; | |||
| const bool rising = rung_value && !previous; | |||
| previous_counter_inputs_[runtime_key] = rung_value; | |||
| bool done = counter_states_[counter->address.index()].done; | |||
| if (reset_read.value) | |||
| { | |||
| current = counter->mode == CounterMode::Up ? 0 : preset; | |||
| done = counter->mode == CounterMode::Down && current == 0; | |||
| } | |||
| else if (rising) | |||
| { | |||
| if (counter->mode == CounterMode::Up) | |||
| { | |||
| if (current < preset) | |||
| { | |||
| ++current; | |||
| } | |||
| done = current >= preset; | |||
| } | |||
| else | |||
| { | |||
| if (current > 0) | |||
| { | |||
| --current; | |||
| } | |||
| done = current <= 0; | |||
| } | |||
| } | |||
| if (counter->mode == CounterMode::Up && current >= preset) | |||
| { | |||
| done = true; | |||
| } | |||
| if (counter->mode == CounterMode::Down && current <= 0) | |||
| { | |||
| done = true; | |||
| } | |||
| const std::int16_t current_value = static_cast<std::int16_t>(current); | |||
| const RegisterWriteResult write = repository.writeWord( | |||
| counter->currentValueAddress, current_value); | |||
| if (!write.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterWriteFailed, | |||
| "写入计数当前值失败:" + counter->currentValueAddress.toString(), | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| } | |||
| counter_states_[counter->address.index()].done = done; | |||
| *output_value = done; | |||
| if (trace != nullptr) | |||
| { | |||
| trace->counterValues[node.id] = { | |||
| rung_value, | |||
| reset_read.value, | |||
| done, | |||
| current_value, | |||
| preset}; | |||
| } | |||
| return success(); | |||
| } | |||
| if (const auto *move = std::get_if<MoveNodeConfig>(&node.config)) | |||
| { | |||
| if (!rung_value) | |||
| @@ -3,7 +3,6 @@ | |||
| #include "domain/control_logic_model.h" | |||
| #include "domain/register_repository.h" | |||
| #include <chrono> | |||
| #include <cstdint> | |||
| #include <map> | |||
| #include <string> | |||
| @@ -32,25 +31,6 @@ struct LogicScanResult | |||
| std::string nodeId; // 失败关联的节点 ID | |||
| }; | |||
| // TON 节点在最近一轮扫描中的可视化状态 | |||
| struct TonTraceValue | |||
| { | |||
| bool input = false; // 本轮 TON 输入状态 | |||
| bool done = false; // 当前是否已经到达预置时间 | |||
| std::int64_t elapsedMs = 0; // 已累计时间,单位为毫秒 | |||
| int presetMs = 0; // 预置时间,单位为毫秒 | |||
| }; | |||
| // CTU/CTD 节点在最近一轮扫描中的可视化状态 | |||
| struct CounterTraceValue | |||
| { | |||
| bool input = false; // 本轮计数输入状态 | |||
| bool reset = false; // 本轮复位输入状态 | |||
| bool done = false; // 当前计数值是否达到预置值 | |||
| std::int16_t value = 0; // 当前计数值 | |||
| std::int16_t preset = 0; // 计数预置值 | |||
| }; | |||
| // MOVE/ADD/SUB 节点的结果值和溢出标记 | |||
| struct WordTraceValue | |||
| { | |||
| @@ -67,8 +47,6 @@ struct LogicTraceValues | |||
| std::unordered_map<std::string, bool> expressionInputValues; // 表达式输入状态 | |||
| std::unordered_map<std::string, bool> expressionPowerValues; // 表达式输出电源状态 | |||
| std::unordered_map<std::string, bool> rungValues; // 网络最终逻辑值 | |||
| std::unordered_map<std::string, TonTraceValue> tonValues; // TON 节点状态 | |||
| std::unordered_map<std::string, CounterTraceValue> counterValues; // CTU/CTD 节点状态 | |||
| std::unordered_map<std::string, WordTraceValue> wordValues; // MOVE/ADD/SUB 节点状态 | |||
| // 清除当前逻辑或网络的全部轨迹 | |||
| @@ -90,22 +68,19 @@ struct LogicTraceSnapshot : LogicTraceValues | |||
| class SoftwareLogicExecutor | |||
| { | |||
| public: | |||
| using Clock = std::chrono::steady_clock; | |||
| using TimePoint = Clock::time_point; | |||
| /** | |||
| * @brief 检查逻辑是否满足软件扫描的运行要求 | |||
| * @param logics 待检查的控制逻辑集合 | |||
| * @return 成功或包含逻辑、网络、节点定位信息的失败结果 | |||
| * | |||
| * 检查逻辑结构、启用逻辑的运行配置、共享输出冲突以及 T/C 资源引用 | |||
| * 检查逻辑结构、启用逻辑的运行配置和共享输出冲突 | |||
| */ | |||
| LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | |||
| /** | |||
| * @brief 清除所有跨扫描运行状态 | |||
| * | |||
| * 重置边沿触发、TON 定时器和 CTU/CTD 计数器;不修改寄存器仓库和轨迹对象 | |||
| * 重置边沿触发状态;不修改寄存器仓库和轨迹对象 | |||
| */ | |||
| void resetRuntime(); | |||
| @@ -120,35 +95,12 @@ public: | |||
| const std::vector<ControlLogic> &logics, | |||
| RegisterRepository &repository, | |||
| LogicTraceSnapshot *trace = nullptr); | |||
| /** | |||
| * @brief 使用指定时间执行一轮扫描 | |||
| * @param logics 按工程顺序扫描的控制逻辑集合 | |||
| * @param repository 扫描读取和写入的寄存器仓库 | |||
| * @param now 本轮扫描使用的单调时钟时间点 | |||
| * @param trace 可选的轨迹输出;适合测试 TON 等时间相关逻辑 | |||
| * @return 扫描结果 | |||
| */ | |||
| LogicScanResult executeScanAt( | |||
| const std::vector<ControlLogic> &logics, | |||
| RegisterRepository &repository, | |||
| TimePoint now, | |||
| LogicTraceSnapshot *trace = nullptr); | |||
| private: | |||
| // 单个 TON 定时器跨扫描保存的计时状态 | |||
| struct TonRuntimeState | |||
| { | |||
| bool timing = false; // 是否正在累计输入有效时间 | |||
| bool done = false; // 是否已经完成预置时间 | |||
| TimePoint startedAt{}; // 本次计时开始时间 | |||
| std::chrono::milliseconds elapsed{0}; // 最近一次计算的累计时间 | |||
| }; | |||
| /** | |||
| * @brief 计算单个条件节点的导通结果 | |||
| * @param logic_id 所属控制逻辑 ID,用于隔离边沿触点的跨扫描状态 | |||
| * @param node 待读取和计算的条件节点 | |||
| * @param repository 提供 M/D 值的寄存器仓库;T/C 条件读取执行器内部状态 | |||
| * @param repository 提供 M/D 值的寄存器仓库 | |||
| * @param value 输出节点导通结果,不能为空 | |||
| * @return 成功结果,或包含节点 ID 的逻辑/寄存器读取失败结果 | |||
| */ | |||
| @@ -176,21 +128,17 @@ private: | |||
| bool *value); | |||
| /** | |||
| * @brief 在网络结果驱动下执行单个输出节点 | |||
| * @param logic_id 所属控制逻辑 ID,用于隔离计数器等跨扫描状态 | |||
| * @param node 待读取配置并执行的输出节点 | |||
| * @param rung_value 当前网络的逻辑结果,决定输出是否动作 | |||
| * @param repository 提供输出读写所需的 M/D 寄存器仓库 | |||
| * @param now 本轮扫描的单调时钟时间点,TON 使用它计算累计时间 | |||
| * @param trace 可选的轨迹输出,用于记录 TON、计数器和字操作结果 | |||
| * @param trace 可选的轨迹输出,用于记录字操作结果 | |||
| * @param output_value 输出节点最终逻辑值,不能为空 | |||
| * @return 成功结果,或包含节点 ID 的逻辑/寄存器读写失败结果 | |||
| */ | |||
| LogicScanResult executeOutput( | |||
| const std::string &logic_id, | |||
| const LogicNode &node, | |||
| bool rung_value, | |||
| RegisterRepository &repository, | |||
| TimePoint now, | |||
| LogicTraceValues *trace, | |||
| bool *output_value); | |||
| /** | |||
| @@ -209,14 +157,4 @@ private: | |||
| // 按逻辑 ID 和节点 ID 保存上一轮边沿输入 | |||
| std::map<std::pair<std::string, std::string>, bool> previous_edge_inputs_; | |||
| // 按逻辑 ID 和节点 ID 保存上一轮计数输入 | |||
| std::map<std::pair<std::string, std::string>, bool> previous_counter_inputs_; | |||
| // 按定时器编号保存跨扫描状态 | |||
| std::unordered_map<int, TonRuntimeState> ton_states_; | |||
| // 单个计数器跨扫描保存的完成状态 | |||
| struct CounterRuntimeState | |||
| { | |||
| bool done = false; // 是否已经达到预置值 | |||
| }; | |||
| std::unordered_map<int, CounterRuntimeState> counter_states_; // 按计数器编号保存状态 | |||
| }; | |||
| @@ -48,16 +48,6 @@ QString registerAddressText(const RegisterAddress &address) | |||
| return QString::fromStdString(address.toString()); | |||
| } | |||
| QString timerAddressText(const TimerAddress &address) | |||
| { | |||
| return QString::fromStdString(address.toString()); | |||
| } | |||
| QString counterAddressText(const CounterAddress &address) | |||
| { | |||
| return QString::fromStdString(address.toString()); | |||
| } | |||
| QString wordOperandText(const WordOperand &operand) | |||
| { | |||
| return operand.kind == WordOperandKind::Register | |||
| @@ -119,16 +109,6 @@ QString nodeToolTip(const LogicNodeConfig &config) | |||
| : LogicEditorWidget::tr("下降")) | |||
| .arg(registerAddressText(value.address)); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("T 触点:%1") | |||
| .arg(timerAddressText(value.address)); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("C 触点:%1") | |||
| .arg(counterAddressText(value.address)); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("输出线圈:%1") | |||
| @@ -141,22 +121,6 @@ QString nodeToolTip(const LogicNodeConfig &config) | |||
| .arg(comparisonText(value.comparison)) | |||
| .arg(value.value); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, TonNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("TON:%1,预设 %2 ms") | |||
| .arg(timerAddressText(value.address)) | |||
| .arg(value.presetMs); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CounterNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("%1:%2,CV %3,PV %4,复位 %5") | |||
| .arg(value.mode == CounterMode::Up | |||
| ? QStringLiteral("CTU") : QStringLiteral("CTD")) | |||
| .arg(counterAddressText(value.address)) | |||
| .arg(registerAddressText(value.currentValueAddress)) | |||
| .arg(wordOperandText(value.preset)) | |||
| .arg(registerAddressText(value.resetAddress)); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, MoveNodeConfig>) | |||
| { | |||
| return LogicEditorWidget::tr("MOVE:%1 -> %2") | |||
| @@ -358,41 +322,6 @@ public: | |||
| configured_ ? registerAddressText(edge->address) : tr("< M 地址 >")); | |||
| drawRegisterComment(painter, register_comment_); | |||
| } | |||
| else if (const auto *timer_contact = | |||
| std::get_if<TimerContactNodeConfig>(&config_)) | |||
| { | |||
| painter->fillRect(QRectF(-25, -22, 50, 44), Qt::white); | |||
| painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-18, 0)); | |||
| painter->drawLine(QPointF(18, 0), QPointF(kNodeTerminalX, 0)); | |||
| painter->drawLine(QPointF(-18, -15), QPointF(-18, 15)); | |||
| painter->drawLine(QPointF(18, -15), QPointF(18, 15)); | |||
| if (timer_contact->mode == ContactMode::NormallyClosed) | |||
| { | |||
| painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); | |||
| } | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? timerAddressText(timer_contact->address) : tr("< T 地址 >")); | |||
| } | |||
| else if (const auto *counter_contact = | |||
| std::get_if<CounterContactNodeConfig>(&config_)) | |||
| { | |||
| painter->fillRect(QRectF(-25, -22, 50, 44), Qt::white); | |||
| painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-18, 0)); | |||
| painter->drawLine(QPointF(18, 0), QPointF(kNodeTerminalX, 0)); | |||
| painter->drawLine(QPointF(-18, -15), QPointF(-18, 15)); | |||
| painter->drawLine(QPointF(18, -15), QPointF(18, 15)); | |||
| if (counter_contact->mode == ContactMode::NormallyClosed) | |||
| { | |||
| painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); | |||
| } | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? counterAddressText(counter_contact->address) | |||
| : tr("< C 地址 >")); | |||
| } | |||
| else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_)) | |||
| { | |||
| painter->fillRect(QRectF(-35, -23, 70, 46), Qt::white); | |||
| @@ -439,54 +368,6 @@ public: | |||
| configured_ ? QString::number(comparison->value) : tr("< 常量 >")); | |||
| drawRegisterComment(painter, register_comment_, 40.0); | |||
| } | |||
| else if (const auto *ton = std::get_if<TonNodeConfig>(&config_)) | |||
| { | |||
| const QRectF box(-47, -18, 94, 36); | |||
| painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); | |||
| painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); | |||
| painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); | |||
| painter->drawRect(box); | |||
| painter->drawText(box, Qt::AlignCenter, QStringLiteral("TON")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? timerAddressText(ton->address) : tr("< T 地址 >")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? QStringLiteral("PT %1 ms").arg(ton->presetMs) | |||
| : tr("< 预设时间 >")); | |||
| } | |||
| else if (const auto *counter = std::get_if<CounterNodeConfig>(&config_)) | |||
| { | |||
| const QRectF box(-47, -18, 94, 36); | |||
| painter->fillRect(box.adjusted(-1, -1, 1, 1), Qt::white); | |||
| painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-47, 0)); | |||
| painter->drawLine(QPointF(47, 0), QPointF(kNodeTerminalX, 0)); | |||
| painter->drawRect(box); | |||
| painter->drawText( | |||
| box, | |||
| Qt::AlignCenter, | |||
| counter->mode == CounterMode::Up | |||
| ? QStringLiteral("CTU") : QStringLiteral("CTD")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, -40, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? counterAddressText(counter->address) | |||
| : tr("< C 地址 >")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0 + 4.0, 20, kCellWidth - 8.0, 18), | |||
| Qt::AlignCenter | Qt::TextSingleLine, | |||
| configured_ ? QStringLiteral("CV %1") | |||
| .arg(registerAddressText(counter->currentValueAddress)) | |||
| : tr("< CV >")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0 + 4.0, 38, kCellWidth - 8.0, 18), | |||
| Qt::AlignCenter | Qt::TextSingleLine, | |||
| configured_ ? QStringLiteral("PV %1") | |||
| .arg(wordOperandText(counter->preset)) | |||
| : tr("< PV >")); | |||
| } | |||
| else if (const auto *move = std::get_if<MoveNodeConfig>(&config_)) | |||
| { | |||
| const QRectF box(-47, -18, 94, 36); | |||
| @@ -1228,17 +1109,7 @@ void LogicEditorWidget::reloadLogic() | |||
| editor_service_.registerCommentFor(*address)) | |||
| : QString{}; | |||
| }(), | |||
| [&rung, this, rung_active] | |||
| { | |||
| return (std::holds_alternative<TonNodeConfig>(rung.output->config) | |||
| || std::holds_alternative<CounterNodeConfig>( | |||
| rung.output->config)) | |||
| ? traceValue( | |||
| trace_, | |||
| &LogicTraceSnapshot::nodeValues, | |||
| rung.output->id) | |||
| : rung_active; | |||
| }(), | |||
| rung_active, | |||
| rung.output->id == fault_node_id_, | |||
| false)); | |||
| } | |||
| @@ -31,14 +31,12 @@ LogicInstructionDialog::LogicInstructionDialog( | |||
| original_config_(config) | |||
| { | |||
| ui_->setupUi(this); | |||
| configureOperandCombo(ui_->counterPresetKindComboBox); | |||
| configureOperandCombo(ui_->leftOperandKindComboBox); | |||
| configureOperandCombo(ui_->rightOperandKindComboBox); | |||
| connect(ui_->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); | |||
| connect(ui_->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); | |||
| for (QComboBox *combo : { | |||
| ui_->counterPresetKindComboBox, | |||
| ui_->leftOperandKindComboBox, | |||
| ui_->rightOperandKindComboBox}) | |||
| { | |||
| @@ -49,11 +47,7 @@ LogicInstructionDialog::LogicInstructionDialog( | |||
| &LogicInstructionDialog::updateOperandRanges); | |||
| } | |||
| if (const auto *counter = std::get_if<CounterNodeConfig>(&config)) | |||
| { | |||
| configureCounter(*counter); | |||
| } | |||
| else if (const auto *move = std::get_if<MoveNodeConfig>(&config)) | |||
| if (const auto *move = std::get_if<MoveNodeConfig>(&config)) | |||
| { | |||
| configureMove(*move); | |||
| } | |||
| @@ -68,21 +62,6 @@ LogicInstructionDialog::~LogicInstructionDialog() = default; | |||
| LogicNodeConfig LogicInstructionDialog::config() const | |||
| { | |||
| if (std::holds_alternative<CounterNodeConfig>(original_config_)) | |||
| { | |||
| return CounterNodeConfig{ | |||
| CounterAddress{ui_->counterAddressSpinBox->value()}, | |||
| static_cast<CounterMode>(ui_->counterModeComboBox->currentData().toInt()), | |||
| RegisterAddress{ | |||
| RegisterArea::D, | |||
| ui_->counterCurrentAddressSpinBox->value()}, | |||
| operandFrom( | |||
| ui_->counterPresetKindComboBox, | |||
| ui_->counterPresetValueSpinBox), | |||
| RegisterAddress{ | |||
| RegisterArea::M, | |||
| ui_->counterResetAddressSpinBox->value()}}; | |||
| } | |||
| if (std::holds_alternative<MoveNodeConfig>(original_config_)) | |||
| { | |||
| return MoveNodeConfig{ | |||
| @@ -101,25 +80,6 @@ LogicNodeConfig LogicInstructionDialog::config() const | |||
| ui_->destinationAddressSpinBox->value()}}; | |||
| } | |||
| void LogicInstructionDialog::configureCounter(const CounterNodeConfig &config) | |||
| { | |||
| setWindowTitle(tr("配置计数器")); | |||
| ui_->instructionStackedWidget->setCurrentWidget(ui_->counterPage); | |||
| ui_->counterModeComboBox->clear(); | |||
| ui_->counterModeComboBox->addItem(tr("CTU 加计数"), static_cast<int>(CounterMode::Up)); | |||
| ui_->counterModeComboBox->addItem( | |||
| tr("CTD 减计数"), static_cast<int>(CounterMode::Down)); | |||
| ui_->counterModeComboBox->setCurrentIndex( | |||
| ui_->counterModeComboBox->findData(static_cast<int>(config.mode))); | |||
| ui_->counterAddressSpinBox->setValue(config.address.index()); | |||
| ui_->counterCurrentAddressSpinBox->setValue(config.currentValueAddress.index()); | |||
| ui_->counterResetAddressSpinBox->setValue(config.resetAddress.index()); | |||
| setOperand( | |||
| ui_->counterPresetKindComboBox, | |||
| ui_->counterPresetValueSpinBox, | |||
| config.preset); | |||
| } | |||
| void LogicInstructionDialog::configureMove(const MoveNodeConfig &config) | |||
| { | |||
| setWindowTitle(tr("配置 MOVE")); | |||
| @@ -165,13 +125,11 @@ void LogicInstructionDialog::updateOperandRanges() | |||
| spin->setPrefix(register_operand ? QStringLiteral("D") : QString{}); | |||
| spin->setMinimum(register_operand | |||
| ? RegisterAddress::kMinimumIndex | |||
| : (combo->objectName() == QStringLiteral("counterPresetKindComboBox") | |||
| ? 0 : std::numeric_limits<std::int16_t>::min())); | |||
| : std::numeric_limits<std::int16_t>::min()); | |||
| spin->setMaximum(register_operand | |||
| ? RegisterAddress::kMaximumIndex | |||
| : std::numeric_limits<std::int16_t>::max()); | |||
| }; | |||
| update(ui_->counterPresetKindComboBox, ui_->counterPresetValueSpinBox); | |||
| update(ui_->leftOperandKindComboBox, ui_->leftOperandValueSpinBox); | |||
| update(ui_->rightOperandKindComboBox, ui_->rightOperandValueSpinBox); | |||
| } | |||
| @@ -38,8 +38,6 @@ public: | |||
| LogicNodeConfig config() const; | |||
| private: | |||
| /** 根据计数器配置初始化计数器输入项 */ | |||
| void configureCounter(const CounterNodeConfig &config); | |||
| /** 根据传送配置初始化传送输入项 */ | |||
| void configureMove(const MoveNodeConfig &config); | |||
| /** 根据算术配置初始化运算输入项 */ | |||
| @@ -9,27 +9,6 @@ | |||
| <layout class="QVBoxLayout" name="verticalLayout"> | |||
| <item> | |||
| <widget class="QStackedWidget" name="instructionStackedWidget"> | |||
| <widget class="QWidget" name="counterPage"> | |||
| <layout class="QFormLayout" name="counterFormLayout"> | |||
| <item row="0" column="0"><widget class="QLabel" name="counterModeLabel"><property name="text"><string>计数方式</string></property></widget></item> | |||
| <item row="0" column="1"><widget class="QComboBox" name="counterModeComboBox"/></item> | |||
| <item row="1" column="0"><widget class="QLabel" name="counterAddressLabel"><property name="text"><string>计数器资源</string></property></widget></item> | |||
| <item row="1" column="1"><widget class="QSpinBox" name="counterAddressSpinBox"><property name="prefix"><string>C</string></property><property name="maximum"><number>4000</number></property></widget></item> | |||
| <item row="2" column="0"><widget class="QLabel" name="counterCurrentAddressLabel"><property name="text"><string>当前值 CV</string></property></widget></item> | |||
| <item row="2" column="1"><widget class="QSpinBox" name="counterCurrentAddressSpinBox"><property name="prefix"><string>D</string></property><property name="maximum"><number>4000</number></property></widget></item> | |||
| <item row="3" column="0"><widget class="QLabel" name="counterPresetLabel"><property name="text"><string>预设值 PV</string></property></widget></item> | |||
| <item row="3" column="1"> | |||
| <widget class="QWidget" name="counterPresetEditor" native="true"> | |||
| <layout class="QHBoxLayout" name="counterPresetLayout"><property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property> | |||
| <item><widget class="QComboBox" name="counterPresetKindComboBox"/></item> | |||
| <item><widget class="QSpinBox" name="counterPresetValueSpinBox"><property name="maximum"><number>32767</number></property><property name="value"><number>10</number></property></widget></item> | |||
| </layout> | |||
| </widget> | |||
| </item> | |||
| <item row="4" column="0"><widget class="QLabel" name="counterResetAddressLabel"><property name="text"><string>复位输入</string></property></widget></item> | |||
| <item row="4" column="1"><widget class="QSpinBox" name="counterResetAddressSpinBox"><property name="prefix"><string>M</string></property><property name="maximum"><number>4000</number></property></widget></item> | |||
| </layout> | |||
| </widget> | |||
| <widget class="QWidget" name="dataInstructionPage"> | |||
| <layout class="QFormLayout" name="dataInstructionFormLayout"> | |||
| <item row="0" column="0"><widget class="QLabel" name="instructionCaptionLabel"><property name="text"><string>指令</string></property></widget></item> | |||
| @@ -423,8 +423,6 @@ void MainWindow::configureActions() | |||
| QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点")); | |||
| QAction *parallel_rising = parallel_menu->addAction(tr("并联上升沿触点")); | |||
| QAction *parallel_falling = parallel_menu->addAction(tr("并联下降沿触点")); | |||
| QAction *parallel_timer = parallel_menu->addAction(tr("并联 T 触点")); | |||
| QAction *parallel_counter = parallel_menu->addAction(tr("并联 C 触点")); | |||
| QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件")); | |||
| connect(parallel_open, &QAction::triggered, | |||
| this, | |||
| @@ -456,20 +454,6 @@ void MainWindow::configureActions() | |||
| addLogicParallelBranch(EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling}); | |||
| }); | |||
| connect(parallel_timer, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicParallelBranch(TimerContactNodeConfig{ | |||
| TimerAddress{0}, ContactMode::NormallyOpen}); | |||
| }); | |||
| connect(parallel_counter, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicParallelBranch(CounterContactNodeConfig{ | |||
| CounterAddress{0}, ContactMode::NormallyOpen}); | |||
| }); | |||
| connect(parallel_compare, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| @@ -515,20 +499,6 @@ void MainWindow::configureActions() | |||
| addLogicCondition(EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling}); | |||
| }); | |||
| connect(ui_->addTimerContactAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicCondition(TimerContactNodeConfig{ | |||
| TimerAddress{0}, ContactMode::NormallyOpen}); | |||
| }); | |||
| connect(ui_->addCounterContactAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| addLogicCondition(CounterContactNodeConfig{ | |||
| CounterAddress{0}, ContactMode::NormallyOpen}); | |||
| }); | |||
| connect(ui_->addNormalCoilAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| @@ -553,40 +523,6 @@ void MainWindow::configureActions() | |||
| RegisterAddress{RegisterArea::M, 0}, | |||
| CoilMode::Reset}); | |||
| }); | |||
| connect(ui_->addTonAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| setLogicOutput(TonNodeConfig{TimerAddress{0}, 1000}); | |||
| }); | |||
| connect(ui_->addCtuAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| configureAndSetLogicOutput(CounterNodeConfig{ | |||
| CounterAddress{0}, | |||
| CounterMode::Up, | |||
| RegisterAddress{RegisterArea::D, 0}, | |||
| WordOperand{ | |||
| WordOperandKind::Constant, | |||
| RegisterAddress{RegisterArea::D, 0}, | |||
| 10}, | |||
| RegisterAddress{RegisterArea::M, 0}}); | |||
| }); | |||
| connect(ui_->addCtdAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| { | |||
| configureAndSetLogicOutput(CounterNodeConfig{ | |||
| CounterAddress{0}, | |||
| CounterMode::Down, | |||
| RegisterAddress{RegisterArea::D, 0}, | |||
| WordOperand{ | |||
| WordOperandKind::Constant, | |||
| RegisterAddress{RegisterArea::D, 0}, | |||
| 10}, | |||
| RegisterAddress{RegisterArea::M, 0}}); | |||
| }); | |||
| connect(ui_->addMoveAction, &QAction::triggered, | |||
| this, | |||
| [this] | |||
| @@ -650,21 +586,13 @@ void MainWindow::configureActions() | |||
| makeUiIcon(UiIcon::NormallyOpenContact), | |||
| QList<QAction *>{ | |||
| ui_->addRisingEdgeAction, | |||
| ui_->addFallingEdgeAction, | |||
| ui_->addTimerContactAction, | |||
| ui_->addCounterContactAction}); | |||
| ui_->addFallingEdgeAction}); | |||
| addToolbarMenu( | |||
| ui_->logicToolBar, | |||
| tr("更多输出"), | |||
| QStringLiteral("logicOutputMenuButton"), | |||
| makeUiIcon(UiIcon::Coil), | |||
| QList<QAction *>{ui_->addSetCoilAction, ui_->addResetCoilAction}); | |||
| addToolbarMenu( | |||
| ui_->logicToolBar, | |||
| tr("定时/计数"), | |||
| QStringLiteral("logicTimerCounterMenuButton"), | |||
| makeUiIcon(UiIcon::Ton), | |||
| QList<QAction *>{ui_->addTonAction, ui_->addCtuAction, ui_->addCtdAction}); | |||
| addToolbarMenu( | |||
| ui_->logicToolBar, | |||
| tr("数据运算"), | |||
| @@ -749,14 +677,9 @@ void MainWindow::configureAppearance() | |||
| ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact)); | |||
| ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact)); | |||
| ui_->addFallingEdgeAction->setIcon(makeUiIcon(UiIcon::FallingEdgeContact)); | |||
| ui_->addTimerContactAction->setIcon(makeUiIcon(UiIcon::TimerContact)); | |||
| ui_->addCounterContactAction->setIcon(makeUiIcon(UiIcon::CounterContact)); | |||
| ui_->addNormalCoilAction->setIcon(makeUiIcon(UiIcon::Coil)); | |||
| ui_->addSetCoilAction->setIcon(makeUiIcon(UiIcon::SetCoil)); | |||
| ui_->addResetCoilAction->setIcon(makeUiIcon(UiIcon::ResetCoil)); | |||
| ui_->addTonAction->setIcon(makeUiIcon(UiIcon::Ton)); | |||
| ui_->addCtuAction->setIcon(makeUiIcon(UiIcon::Ctu)); | |||
| ui_->addCtdAction->setIcon(makeUiIcon(UiIcon::Ctd)); | |||
| ui_->addMoveAction->setIcon(makeUiIcon(UiIcon::Move)); | |||
| ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add)); | |||
| ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract)); | |||
| @@ -1601,14 +1524,9 @@ void MainWindow::updateModeUi(const QString &message) | |||
| ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addRisingEdgeAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addFallingEdgeAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addTimerContactAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addCounterContactAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addTonAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addCtuAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addCtdAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addMoveAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addAddAction->setEnabled(policy.allowsProjectEditing); | |||
| ui_->addSubAction->setEnabled(policy.allowsProjectEditing); | |||
| @@ -922,27 +922,7 @@ | |||
| </property> | |||
| </widget> | |||
| </item> | |||
| <item row="7" column="0"> | |||
| <widget class="QLabel" name="logicPresetLabel"> | |||
| <property name="text"> | |||
| <string>预设时间(ms)</string> | |||
| </property> | |||
| </widget> | |||
| </item> | |||
| <item row="7" column="1"> | |||
| <widget class="QSpinBox" name="logicPresetSpinBox"> | |||
| <property name="minimum"> | |||
| <number>1</number> | |||
| </property> | |||
| <property name="maximum"> | |||
| <number>86400000</number> | |||
| </property> | |||
| <property name="value"> | |||
| <number>1000</number> | |||
| </property> | |||
| </widget> | |||
| </item> | |||
| <item row="8" column="0" colspan="2"> | |||
| <item row="7" column="0" colspan="2"> | |||
| <widget class="QPushButton" name="applyLogicPropertiesButton"> | |||
| <property name="text"> | |||
| <string>应用属性</string> | |||
| @@ -1252,18 +1232,6 @@ | |||
| <string>在选中的空网格或横线处添加一个扫描周期的下降沿触点</string> | |||
| </property> | |||
| </action> | |||
| <action name="addTimerContactAction"> | |||
| <property name="text"> | |||
| <string>T 触点</string> | |||
| </property> | |||
| <property name="toolTip"> | |||
| <string>在选中的空网格或横线处添加离线仿真定时器触点</string> | |||
| </property> | |||
| </action> | |||
| <action name="addCounterContactAction"> | |||
| <property name="text"><string>C 触点</string></property> | |||
| <property name="toolTip"><string>在选中的空网格或横线处添加离线仿真计数器完成触点</string></property> | |||
| </action> | |||
| <action name="addNormalCoilAction"> | |||
| <property name="text"> | |||
| <string>线圈</string> | |||
| @@ -1288,22 +1256,6 @@ | |||
| <string>设置当前网络的复位线圈</string> | |||
| </property> | |||
| </action> | |||
| <action name="addTonAction"> | |||
| <property name="text"> | |||
| <string>TON</string> | |||
| </property> | |||
| <property name="toolTip"> | |||
| <string>设置非保持接通延时定时器输出</string> | |||
| </property> | |||
| </action> | |||
| <action name="addCtuAction"> | |||
| <property name="text"><string>CTU</string></property> | |||
| <property name="toolTip"><string>设置按上升沿执行的加计数器</string></property> | |||
| </action> | |||
| <action name="addCtdAction"> | |||
| <property name="text"><string>CTD</string></property> | |||
| <property name="toolTip"><string>设置按上升沿执行的减计数器</string></property> | |||
| </action> | |||
| <action name="addMoveAction"> | |||
| <property name="text"><string>MOVE</string></property> | |||
| <property name="toolTip"><string>设置 D 数据传送指令</string></property> | |||
| @@ -378,7 +378,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id | |||
| static_cast<QWidget *>(ui_.logicModeComboBox), | |||
| static_cast<QWidget *>(ui_.logicComparisonComboBox), | |||
| static_cast<QWidget *>(ui_.logicValueSpinBox), | |||
| static_cast<QWidget *>(ui_.logicPresetSpinBox), | |||
| static_cast<QWidget *>(ui_.applyLogicPropertiesButton)}) | |||
| { | |||
| widget->setEnabled(has_node); | |||
| @@ -391,7 +390,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id | |||
| ui_.logicNodeTypeLabel->clear(); | |||
| ui_.logicAddressAreaLabel->clear(); | |||
| ui_.logicModeComboBox->clear(); | |||
| ui_.logicPresetSpinBox->setValue(TonNodeConfig::kMinimumPresetMs); | |||
| return; | |||
| } | |||
| @@ -399,7 +397,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id | |||
| ui_.logicModeComboBox->clear(); | |||
| ui_.logicComparisonComboBox->setEnabled(false); | |||
| ui_.logicValueSpinBox->setEnabled(false); | |||
| ui_.logicPresetSpinBox->setEnabled(false); | |||
| if (const auto *contact = std::get_if<ContactNodeConfig>(&node->config)) | |||
| { | |||
| ui_.logicNodeTypeLabel->setText(QObject::tr("触点")); | |||
| @@ -426,33 +423,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id | |||
| ui_.logicModeComboBox->findData(static_cast<int>(edge->mode))); | |||
| ui_.logicModeComboBox->setEnabled(true); | |||
| } | |||
| else if (const auto *timer = std::get_if<TimerContactNodeConfig>(&node->config)) | |||
| { | |||
| ui_.logicNodeTypeLabel->setText(QObject::tr("T 触点")); | |||
| ui_.logicAddressAreaLabel->setText(QStringLiteral("T")); | |||
| ui_.logicAddressSpinBox->setValue(timer->address.index()); | |||
| ui_.logicModeComboBox->addItem( | |||
| QObject::tr("常开"), static_cast<int>(ContactMode::NormallyOpen)); | |||
| ui_.logicModeComboBox->addItem( | |||
| QObject::tr("常闭"), static_cast<int>(ContactMode::NormallyClosed)); | |||
| ui_.logicModeComboBox->setCurrentIndex( | |||
| ui_.logicModeComboBox->findData(static_cast<int>(timer->mode))); | |||
| ui_.logicModeComboBox->setEnabled(true); | |||
| } | |||
| else if (const auto *counter_contact = | |||
| std::get_if<CounterContactNodeConfig>(&node->config)) | |||
| { | |||
| ui_.logicNodeTypeLabel->setText(QObject::tr("C 触点")); | |||
| ui_.logicAddressAreaLabel->setText(QStringLiteral("C")); | |||
| ui_.logicAddressSpinBox->setValue(counter_contact->address.index()); | |||
| ui_.logicModeComboBox->addItem( | |||
| QObject::tr("常开"), static_cast<int>(ContactMode::NormallyOpen)); | |||
| ui_.logicModeComboBox->addItem( | |||
| QObject::tr("常闭"), static_cast<int>(ContactMode::NormallyClosed)); | |||
| ui_.logicModeComboBox->setCurrentIndex(ui_.logicModeComboBox->findData( | |||
| static_cast<int>(counter_contact->mode))); | |||
| ui_.logicModeComboBox->setEnabled(true); | |||
| } | |||
| else if (const auto *coil = std::get_if<CoilNodeConfig>(&node->config)) | |||
| { | |||
| ui_.logicNodeTypeLabel->setText(QObject::tr("线圈")); | |||
| @@ -482,27 +452,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id | |||
| ui_.logicValueSpinBox->setEnabled(true); | |||
| ui_.logicValueSpinBox->setValue(compare->value); | |||
| } | |||
| else if (const auto *ton = std::get_if<TonNodeConfig>(&node->config)) | |||
| { | |||
| ui_.logicNodeTypeLabel->setText(QObject::tr("TON")); | |||
| ui_.logicAddressAreaLabel->setText(QStringLiteral("T")); | |||
| ui_.logicAddressSpinBox->setValue(ton->address.index()); | |||
| ui_.logicModeComboBox->addItem(QObject::tr("非保持接通延时")); | |||
| ui_.logicModeComboBox->setEnabled(false); | |||
| ui_.logicPresetSpinBox->setEnabled(true); | |||
| ui_.logicPresetSpinBox->setValue(ton->presetMs); | |||
| } | |||
| else if (const auto *counter = std::get_if<CounterNodeConfig>(&node->config)) | |||
| { | |||
| ui_.logicNodeTypeLabel->setText( | |||
| counter->mode == CounterMode::Up ? QStringLiteral("CTU") | |||
| : QStringLiteral("CTD")); | |||
| ui_.logicAddressAreaLabel->setText(QStringLiteral("C")); | |||
| ui_.logicAddressSpinBox->setValue(counter->address.index()); | |||
| ui_.logicAddressSpinBox->setEnabled(false); | |||
| ui_.logicModeComboBox->addItem(QObject::tr("使用配置对话框编辑")); | |||
| ui_.logicModeComboBox->setEnabled(false); | |||
| } | |||
| else if (const auto *move = std::get_if<MoveNodeConfig>(&node->config)) | |||
| { | |||
| ui_.logicNodeTypeLabel->setText(QStringLiteral("MOVE")); | |||
| @@ -726,18 +675,6 @@ void PropertyPanelController::applySelectedLogicNodeProperties() | |||
| RegisterAddress{RegisterArea::M, ui_.logicAddressSpinBox->value()}, | |||
| static_cast<EdgeMode>(ui_.logicModeComboBox->currentData().toInt())}; | |||
| } | |||
| else if (std::holds_alternative<TimerContactNodeConfig>(config)) | |||
| { | |||
| config = TimerContactNodeConfig{ | |||
| TimerAddress{ui_.logicAddressSpinBox->value()}, | |||
| static_cast<ContactMode>(ui_.logicModeComboBox->currentData().toInt())}; | |||
| } | |||
| else if (std::holds_alternative<CounterContactNodeConfig>(config)) | |||
| { | |||
| config = CounterContactNodeConfig{ | |||
| CounterAddress{ui_.logicAddressSpinBox->value()}, | |||
| static_cast<ContactMode>(ui_.logicModeComboBox->currentData().toInt())}; | |||
| } | |||
| else if (std::holds_alternative<CoilNodeConfig>(config)) | |||
| { | |||
| config = CoilNodeConfig{ | |||
| @@ -752,12 +689,6 @@ void PropertyPanelController::applySelectedLogicNodeProperties() | |||
| ui_.logicComparisonComboBox->currentData().toInt()), | |||
| static_cast<std::int16_t>(ui_.logicValueSpinBox->value())}; | |||
| } | |||
| else if (std::holds_alternative<TonNodeConfig>(config)) | |||
| { | |||
| config = TonNodeConfig{ | |||
| TimerAddress{ui_.logicAddressSpinBox->value()}, | |||
| ui_.logicPresetSpinBox->value()}; | |||
| } | |||
| else | |||
| { | |||
| LogicInstructionDialog dialog(node->config, &parent_); | |||
| @@ -410,16 +410,6 @@ QPixmap renderIcon(UiIcon icon, int size) | |||
| drawContact(painter, QStringLiteral("N"), false); | |||
| break; | |||
| } | |||
| case UiIcon::TimerContact: | |||
| { | |||
| drawContact(painter, QStringLiteral("T"), false); | |||
| break; | |||
| } | |||
| case UiIcon::CounterContact: | |||
| { | |||
| drawContact(painter, QStringLiteral("C"), false); | |||
| break; | |||
| } | |||
| case UiIcon::Coil: | |||
| { | |||
| drawCoil(painter, {}); | |||
| @@ -435,21 +425,6 @@ QPixmap renderIcon(UiIcon icon, int size) | |||
| drawCoil(painter, QStringLiteral("R")); | |||
| break; | |||
| } | |||
| case UiIcon::Ton: | |||
| { | |||
| drawInstructionBlock(painter, QStringLiteral("TON")); | |||
| break; | |||
| } | |||
| case UiIcon::Ctu: | |||
| { | |||
| drawInstructionBlock(painter, QStringLiteral("CTU")); | |||
| break; | |||
| } | |||
| case UiIcon::Ctd: | |||
| { | |||
| drawInstructionBlock(painter, QStringLiteral("CTD")); | |||
| break; | |||
| } | |||
| case UiIcon::Move: | |||
| { | |||
| painter.drawRect(QRectF(2, 6, 6, 12)); | |||
| @@ -44,14 +44,9 @@ enum class UiIcon | |||
| NormallyClosedContact, // 常闭触点 | |||
| RisingEdgeContact, // 上升沿触点 | |||
| FallingEdgeContact, // 下降沿触点 | |||
| TimerContact, // 定时器触点 | |||
| CounterContact, // 计数器触点 | |||
| Coil, // 普通线圈 | |||
| SetCoil, // 置位线圈 | |||
| ResetCoil, // 复位线圈 | |||
| Ton, // TON 定时器 | |||
| Ctu, // CTU 加计数器 | |||
| Ctd, // CTD 减计数器 | |||
| Move, // MOVE 指令 | |||
| Add, // ADD 指令 | |||
| Subtract, // SUB 指令 | |||
| @@ -434,14 +434,6 @@ void testQuantityBoundaries() | |||
| require(!excessive_depth.validate(), | |||
| "an expression depth of 21 must be rejected"); | |||
| CounterNodeConfig counter_config{ | |||
| CounterAddress{0}, | |||
| CounterMode::Up, | |||
| RegisterAddress{RegisterArea::D, 0}, | |||
| WordOperand{WordOperandKind::Constant, RegisterAddress{RegisterArea::D, 0}, -1}, | |||
| RegisterAddress{RegisterArea::M, 0}}; | |||
| LogicNode counter{"counter-negative-preset", counter_config, true}; | |||
| require(!counter.validate(), "a negative constant counter preset must be rejected"); | |||
| (void)leaf; | |||
| } | |||
| @@ -469,7 +461,7 @@ void testLogicNodeConfigurationBoundaries() | |||
| require(comparison.validate(), "comparison node bound to D address must be valid"); | |||
| } | |||
| void testTimerAndCommentBoundaries() | |||
| void testEdgeAndCommentBoundaries() | |||
| { | |||
| LogicNode edge; | |||
| edge.id = "edge"; | |||
| @@ -477,25 +469,6 @@ void testTimerAndCommentBoundaries() | |||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising}; | |||
| require(edge.validate(), "a valid rising edge contact must pass validation"); | |||
| LogicNode timer_contact; | |||
| timer_contact.id = "timer-contact"; | |||
| timer_contact.config = TimerContactNodeConfig{ | |||
| TimerAddress{4000}, ContactMode::NormallyOpen}; | |||
| require(timer_contact.validate(), "T4000 must be a valid timer contact"); | |||
| timer_contact.config = TimerContactNodeConfig{ | |||
| TimerAddress{-1}, ContactMode::NormallyOpen}; | |||
| require(!timer_contact.validate(), "a negative T address must be rejected"); | |||
| LogicNode ton; | |||
| ton.id = "ton"; | |||
| ton.config = TonNodeConfig{TimerAddress{0}, TonNodeConfig::kMinimumPresetMs}; | |||
| require(ton.validate(), "the minimum TON preset must be valid"); | |||
| ton.config = TonNodeConfig{TimerAddress{0}, 0}; | |||
| require(!ton.validate(), "a zero TON preset must be rejected"); | |||
| ton.config = TonNodeConfig{ | |||
| TimerAddress{0}, TonNodeConfig::kMaximumPresetMs + 1}; | |||
| require(!ton.validate(), "an oversized TON preset must be rejected"); | |||
| RegisterComment comment{RegisterAddress{RegisterArea::M, 0}, "启动按钮"}; | |||
| require(comment.validate(), "a nonblank register comment must be valid"); | |||
| comment.text.assign(ProjectLimits::kMaximumRegisterCommentBytes, 'a'); | |||
| @@ -528,124 +501,8 @@ void testTimerAndCommentBoundaries() | |||
| require(!project.validate(), "duplicate register comments must be rejected"); | |||
| } | |||
| LadderRung makeTimerRung( | |||
| const std::string &rung_id, | |||
| const std::string &condition_id, | |||
| const std::string &output_id, | |||
| int timer_index, | |||
| int preset_ms) | |||
| { | |||
| LogicNode condition; | |||
| condition.id = condition_id; | |||
| condition.config = ContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, timer_index}, ContactMode::NormallyOpen}; | |||
| LogicNode output; | |||
| output.id = output_id; | |||
| output.config = TonNodeConfig{TimerAddress{timer_index}, preset_ms}; | |||
| LadderRung rung; | |||
| rung.id = rung_id; | |||
| rung.name = rung_id; | |||
| rung.condition = ConditionExpression::fromNode(condition); | |||
| rung.output = output; | |||
| return rung; | |||
| } | |||
| void testTimerReferencesForRunning() | |||
| void testDataInstructionBoundaries() | |||
| { | |||
| ControlLogic valid; | |||
| valid.id = "timer-valid"; | |||
| valid.name = "Timer valid"; | |||
| valid.rungs.push_back(makeTimerRung("rung-0", "input-0", "ton-0", 0, 100)); | |||
| require(validateTimerReferencesForRunning({valid}), | |||
| "a timer contact-free TON network must pass timer reference validation"); | |||
| ControlLogic duplicate = valid; | |||
| duplicate.id = "timer-duplicate"; | |||
| duplicate.name = "Timer duplicate"; | |||
| duplicate.rungs.front().output->id = "ton-duplicate"; | |||
| require(!validateTimerReferencesForRunning({valid, duplicate}), | |||
| "the same T must not have two enabled TON drivers"); | |||
| ControlLogic missing; | |||
| missing.id = "timer-missing"; | |||
| missing.name = "Timer missing"; | |||
| LogicNode contact; | |||
| contact.id = "missing-contact"; | |||
| contact.config = TimerContactNodeConfig{ | |||
| TimerAddress{7}, ContactMode::NormallyOpen}; | |||
| LogicNode output; | |||
| output.id = "missing-coil"; | |||
| output.config = CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 7}, CoilMode::Normal}; | |||
| LadderRung missing_rung; | |||
| missing_rung.id = "missing-rung"; | |||
| missing_rung.name = "Missing rung"; | |||
| missing_rung.condition = ConditionExpression::fromNode(contact); | |||
| missing_rung.output = output; | |||
| missing.rungs.push_back(missing_rung); | |||
| require(!validateTimerReferencesForRunning({missing}), | |||
| "a T contact without an enabled TON driver must be rejected"); | |||
| missing.enabled = false; | |||
| require(validateTimerReferencesForRunning({missing}), | |||
| "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{ | |||
| @@ -673,39 +530,6 @@ void testCounterAndDataInstructionBoundaries() | |||
| 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() | |||
| @@ -914,9 +738,8 @@ int main() | |||
| testProgressBarConfigurationBoundaries(); | |||
| testHmiAppearancePropertyBoundaries(); | |||
| testLogicNodeConfigurationBoundaries(); | |||
| testTimerAndCommentBoundaries(); | |||
| testTimerReferencesForRunning(); | |||
| testCounterAndDataInstructionBoundaries(); | |||
| testEdgeAndCommentBoundaries(); | |||
| testDataInstructionBoundaries(); | |||
| testLadderLogicBoundaries(); | |||
| testModelsValidateBindingsAndIdentifiers(); | |||
| testMultiPageAndLogicDomainRules(); | |||
| @@ -883,7 +883,7 @@ void testLogicLifecycleAndOrdering() | |||
| "the project must retain at least one control logic module"); | |||
| } | |||
| void testEdgeTimerNodesAndRungComments() | |||
| void testEdgeNodesAndRungComments() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| @@ -898,18 +898,6 @@ void testEdgeTimerNodesAndRungComments() | |||
| RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising}); | |||
| require(rising.succeeded && rising.id == "edge-1", | |||
| "the editor must create rising edge nodes with a stable prefix"); | |||
| const LogicEditorResult timer = service.addParallelBranch( | |||
| logic_id, | |||
| rung_id, | |||
| {rising.id}, | |||
| TimerContactNodeConfig{TimerAddress{2}, ContactMode::NormallyOpen}); | |||
| require(timer.succeeded && timer.id == "timer-contact-1", | |||
| "the editor must insert T contacts as conditions"); | |||
| const LogicEditorResult ton = service.setOutput( | |||
| logic_id, rung_id, TonNodeConfig{TimerAddress{2}, 500}); | |||
| require(ton.succeeded && ton.id == "ton-1", | |||
| "the editor must create TON outputs with a stable prefix"); | |||
| require(service.updateNodeConfig( | |||
| logic_id, | |||
| rising.id, | |||
| @@ -917,18 +905,6 @@ void testEdgeTimerNodesAndRungComments() | |||
| RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling}) | |||
| .succeeded, | |||
| "the editor must apply edge mode and M address properties"); | |||
| require(service.updateNodeConfig( | |||
| logic_id, | |||
| timer.id, | |||
| TimerContactNodeConfig{TimerAddress{4}, ContactMode::NormallyClosed}) | |||
| .succeeded, | |||
| "the editor must apply T contact mode and address properties"); | |||
| require(service.updateNodeConfig( | |||
| logic_id, | |||
| ton.id, | |||
| TonNodeConfig{TimerAddress{4}, 750}) | |||
| .succeeded, | |||
| "the editor must apply TON preset properties"); | |||
| require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded, | |||
| "the editor must update a network comment by stable rung id"); | |||
| require(!service.updateRungComment( | |||
| @@ -942,15 +918,12 @@ void testEdgeTimerNodesAndRungComments() | |||
| "the editor must reject oversized network comments"); | |||
| const LadderRung *rung = service.findRung(logic_id, rung_id); | |||
| const LogicNode *edge = service.findNode(logic_id, rising.id); | |||
| require(rung != nullptr && rung->comment == "延时启动网络" | |||
| && std::get<EdgeContactNodeConfig>( | |||
| rung->condition->children.front().node->config).mode | |||
| == EdgeMode::Falling | |||
| && std::get<TimerContactNodeConfig>( | |||
| rung->condition->children.at(1).node->config).address | |||
| == TimerAddress{4} | |||
| && std::get<TonNodeConfig>(rung->output->config).presetMs == 750, | |||
| "edge, T contact, TON and rung comment updates must remain in the model"); | |||
| && edge != nullptr | |||
| && std::get<EdgeContactNodeConfig>(edge->config).mode | |||
| == EdgeMode::Falling, | |||
| "edge and rung comment updates must remain in the model"); | |||
| } | |||
| void testHistoryAndAtomicBatchDelete() | |||
| @@ -1045,7 +1018,7 @@ int main() | |||
| testWireColumnReplacement(); | |||
| testSequentialConditionInsertionConsumesFollowingWire(); | |||
| testLogicLifecycleAndOrdering(); | |||
| testEdgeTimerNodesAndRungComments(); | |||
| testEdgeNodesAndRungComments(); | |||
| testHistoryAndAtomicBatchDelete(); | |||
| } | |||
| catch (const std::exception &error) | |||
| @@ -32,27 +32,6 @@ LogicNode edgeContact(const std::string &id, int address, EdgeMode mode) | |||
| true}; | |||
| } | |||
| LogicNode timerContact( | |||
| const std::string &id, | |||
| int timer_index, | |||
| ContactMode mode = ContactMode::NormallyOpen) | |||
| { | |||
| return {id, TimerContactNodeConfig{TimerAddress{timer_index}, mode}, true}; | |||
| } | |||
| LogicNode counterContact( | |||
| const std::string &id, | |||
| int counter_index, | |||
| ContactMode mode = ContactMode::NormallyOpen) | |||
| { | |||
| return {id, CounterContactNodeConfig{CounterAddress{counter_index}, mode}, true}; | |||
| } | |||
| LogicNode ton(const std::string &id, int timer_index, int preset_ms) | |||
| { | |||
| return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true}; | |||
| } | |||
| WordOperand constantOperand(std::int16_t value) | |||
| { | |||
| return { | |||
| @@ -69,25 +48,6 @@ WordOperand registerOperand(int address) | |||
| 0}; | |||
| } | |||
| LogicNode counter( | |||
| const std::string &id, | |||
| int counter_index, | |||
| CounterMode mode, | |||
| int current_address, | |||
| WordOperand preset, | |||
| int reset_address) | |||
| { | |||
| return { | |||
| id, | |||
| CounterNodeConfig{ | |||
| CounterAddress{counter_index}, | |||
| mode, | |||
| RegisterAddress{RegisterArea::D, current_address}, | |||
| preset, | |||
| RegisterAddress{RegisterArea::M, reset_address}}, | |||
| true}; | |||
| } | |||
| LogicNode move( | |||
| const std::string &id, | |||
| WordOperand source, | |||
| @@ -479,221 +439,6 @@ void testEdgeContactsAreOneScanPulsesAndAreLogicScoped() | |||
| "a restarted runtime must treat current ON inputs as fresh rising edges"); | |||
| } | |||
| void testTonUsesElapsedTimeAndResetsAsNonRetentive() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| SoftwareLogicExecutor executor; | |||
| const ControlLogic program = logic({ | |||
| rung("ton-rung", {{contact("ton-input", 0)}}, ton("ton-0", 0, 100)), | |||
| rung("ton-feedback-rung", {{timerContact("timer-done", 0)}}, | |||
| coil("ton-output", 30))}); | |||
| const auto start = SoftwareLogicExecutor::TimePoint{}; | |||
| LogicTraceSnapshot trace; | |||
| require(executor.executeScanAt({program}, repository, start, &trace).succeeded, | |||
| "an inactive TON must scan successfully"); | |||
| require(!readBit(repository, 30) | |||
| && trace.tonValues.at("ton-0").elapsedMs == 0 | |||
| && !trace.tonValues.at("ton-0").done, | |||
| "an inactive TON must have Q false and ET zero"); | |||
| writeBit(repository, 0, true); | |||
| require(executor.executeScanAt({program}, repository, start, &trace).succeeded, | |||
| "the first active TON scan must start timing"); | |||
| require(!readBit(repository, 30) && trace.tonValues.at("ton-0").elapsedMs == 0, | |||
| "TON must not complete on its starting scan"); | |||
| require(executor.executeScanAt( | |||
| {program}, repository, | |||
| start + std::chrono::milliseconds{99}, &trace) | |||
| .succeeded, | |||
| "TON must use elapsed monotonic time before the preset"); | |||
| require(!readBit(repository, 30) | |||
| && trace.tonValues.at("ton-0").elapsedMs == 99, | |||
| "TON must remain false before PT"); | |||
| require(executor.executeScanAt( | |||
| {program}, repository, | |||
| start + std::chrono::milliseconds{100}, &trace) | |||
| .succeeded, | |||
| "TON must complete at its preset time"); | |||
| require(readBit(repository, 30) | |||
| && trace.tonValues.at("ton-0").done, | |||
| "the T contact must observe TON Q in a later network of the same scan"); | |||
| writeBit(repository, 0, false); | |||
| require(executor.executeScanAt( | |||
| {program}, repository, | |||
| start + std::chrono::milliseconds{150}, &trace) | |||
| .succeeded, | |||
| "disconnecting TON input must scan successfully"); | |||
| require(!readBit(repository, 30) | |||
| && trace.tonValues.at("ton-0").elapsedMs == 0 | |||
| && !trace.tonValues.at("ton-0").done, | |||
| "a non-retentive TON must reset ET and Q when input opens"); | |||
| writeBit(repository, 0, true); | |||
| require(executor.executeScanAt( | |||
| {program}, repository, | |||
| start + std::chrono::milliseconds{200}, &trace) | |||
| .succeeded, | |||
| "TON must restart timing after an input reset"); | |||
| require(!readBit(repository, 30), "TON restart must not retain the old done state"); | |||
| executor.resetRuntime(); | |||
| require(executor.executeScanAt( | |||
| {program}, repository, | |||
| start + std::chrono::milliseconds{1000}, &trace) | |||
| .succeeded, | |||
| "resetRuntime must clear TON state"); | |||
| require(!readBit(repository, 30), | |||
| "a restarted runtime must start a currently active TON from zero"); | |||
| } | |||
| void testMultipleTimersAndNetworkOrder() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| SoftwareLogicExecutor executor; | |||
| const ControlLogic independent = logic({ | |||
| rung("ton-1-rung", {{contact("input-1", 1)}}, ton("ton-1", 1, 100)), | |||
| rung("ton-2-rung", {{contact("input-2", 2)}}, ton("ton-2", 2, 200))}); | |||
| writeBit(repository, 1, true); | |||
| writeBit(repository, 2, true); | |||
| const auto start = SoftwareLogicExecutor::TimePoint{}; | |||
| LogicTraceSnapshot trace; | |||
| require(executor.executeScanAt({independent}, repository, start, &trace).succeeded, | |||
| "multiple TON resources must start independently"); | |||
| require(executor.executeScanAt( | |||
| {independent}, repository, | |||
| start + std::chrono::milliseconds{100}, &trace) | |||
| .succeeded, | |||
| "the first independent TON must reach PT without completing the second"); | |||
| require(trace.tonValues.at("ton-1").done | |||
| && !trace.tonValues.at("ton-2").done, | |||
| "different T resources must retain independent elapsed time"); | |||
| VirtualRegisterRepository ordered_repository; | |||
| SoftwareLogicExecutor ordered_executor; | |||
| const ControlLogic ordered = logic({ | |||
| rung("feedback-before-ton", {{timerContact("early-timer", 4)}}, | |||
| coil("early-output", 40)), | |||
| rung("ton-after-feedback", {{contact("late-input", 4)}}, ton("ton-4", 4, 100))}); | |||
| writeBit(ordered_repository, 4, true); | |||
| require(ordered_executor.executeScanAt( | |||
| {ordered}, ordered_repository, start, nullptr) | |||
| .succeeded, | |||
| "network order test must start TON after its earlier T contact"); | |||
| require(!readBit(ordered_repository, 40), | |||
| "an earlier T contact must see the previous scan state"); | |||
| require(ordered_executor.executeScanAt( | |||
| {ordered}, ordered_repository, | |||
| start + std::chrono::milliseconds{100}, nullptr) | |||
| .succeeded, | |||
| "network order test must complete TON on the next scan"); | |||
| require(!readBit(ordered_repository, 40), | |||
| "the earlier T contact must remain false on the completion scan"); | |||
| require(ordered_executor.executeScanAt( | |||
| {ordered}, ordered_repository, | |||
| start + std::chrono::milliseconds{101}, nullptr) | |||
| .succeeded, | |||
| "network order test must expose TON Q on the following scan"); | |||
| require(readBit(ordered_repository, 40), | |||
| "a later scan must observe the completed TON through T contact"); | |||
| } | |||
| void testCountersUseRisingEdgesAndExternalDValues() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| SoftwareLogicExecutor executor; | |||
| const ControlLogic up_program = logic({ | |||
| rung( | |||
| "ctu-rung", | |||
| {{contact("count-input", 0)}}, | |||
| counter( | |||
| "ctu-0", | |||
| 0, | |||
| CounterMode::Up, | |||
| 0, | |||
| constantOperand(3), | |||
| 1)), | |||
| rung( | |||
| "ctu-done-rung", | |||
| {{counterContact("ctu-done", 0)}}, | |||
| coil("ctu-done-output", 2))}); | |||
| LogicTraceSnapshot trace; | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "an inactive CTU must scan successfully"); | |||
| require(readWord(repository, 0) == 0 && !readBit(repository, 2), | |||
| "an inactive CTU must keep CV zero and Q false"); | |||
| writeBit(repository, 0, true); | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "the first CTU rising edge must scan successfully"); | |||
| require(readWord(repository, 0) == 1, | |||
| "CTU must increment the external D current value on a rising edge"); | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "a held CTU input must scan successfully"); | |||
| require(readWord(repository, 0) == 1, | |||
| "a held CTU input must not count every scan"); | |||
| for (int expected = 2; expected <= 3; ++expected) | |||
| { | |||
| writeBit(repository, 0, false); | |||
| require(executor.executeScan({up_program}, repository).succeeded, | |||
| "CTU falling preparation scan must succeed"); | |||
| writeBit(repository, 0, true); | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "CTU repeated rising edge must succeed"); | |||
| require(readWord(repository, 0) == expected, | |||
| "CTU must increment exactly once for each rising edge"); | |||
| } | |||
| require(readBit(repository, 2) | |||
| && trace.counterValues.at("ctu-0").done, | |||
| "the C contact must observe CTU completion in a later network"); | |||
| writeBit(repository, 1, true); | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "CTU reset must scan successfully"); | |||
| require(readWord(repository, 0) == 0 && !readBit(repository, 2), | |||
| "CTU reset must clear CV and the C completion state"); | |||
| executor.resetRuntime(); | |||
| repository.clear(); | |||
| const ControlLogic down_program = logic({ | |||
| rung( | |||
| "ctd-rung", | |||
| {{contact("down-input", 3)}}, | |||
| counter( | |||
| "ctd-1", | |||
| 1, | |||
| CounterMode::Down, | |||
| 1, | |||
| constantOperand(2), | |||
| 4)), | |||
| rung( | |||
| "ctd-done-rung", | |||
| {{counterContact("ctd-done", 1)}}, | |||
| coil("ctd-done-output", 5))}); | |||
| writeBit(repository, 4, true); | |||
| require(executor.executeScan({down_program}, repository).succeeded, | |||
| "CTD load reset must scan successfully"); | |||
| require(readWord(repository, 1) == 2, | |||
| "CTD reset must load PV into the external D current value"); | |||
| writeBit(repository, 4, false); | |||
| for (int expected = 1; expected >= 0; --expected) | |||
| { | |||
| writeBit(repository, 3, false); | |||
| require(executor.executeScan({down_program}, repository).succeeded, | |||
| "CTD falling preparation scan must succeed"); | |||
| writeBit(repository, 3, true); | |||
| require(executor.executeScan({down_program}, repository).succeeded, | |||
| "CTD rising edge must scan successfully"); | |||
| require(readWord(repository, 1) == expected, | |||
| "CTD must decrement exactly once for each rising edge"); | |||
| } | |||
| require(readBit(repository, 5), | |||
| "CTD completion contact must turn on when CV reaches zero"); | |||
| } | |||
| void testMoveAndSaturatingArithmetic() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| @@ -924,9 +669,6 @@ int main(int argc, char *argv[]) | |||
| testSetResetAndDisabledLogic(); | |||
| testMultipleLogicScanOrderAndTraceIsolation(); | |||
| testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); | |||
| testTonUsesElapsedTimeAndResetsAsNonRetentive(); | |||
| testMultipleTimersAndNetworkOrder(); | |||
| testCountersUseRisingEdgesAndExternalDValues(); | |||
| testMoveAndSaturatingArithmetic(); | |||
| testSetResetPairOnSameAddress(); | |||
| testConflictingCoilsAreRejected(); | |||
| @@ -159,80 +159,17 @@ Project makeExampleProject() | |||
| rising_edge.id = "rising-edge"; | |||
| rising_edge.config = EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 4}, EdgeMode::Rising}; | |||
| LogicNode ton_output; | |||
| ton_output.id = "ton-output"; | |||
| ton_output.config = TonNodeConfig{TimerAddress{7}, 2500}; | |||
| LadderRung ton_rung; | |||
| ton_rung.id = "ton-rung"; | |||
| ton_rung.name = "TON network"; | |||
| ton_rung.comment = "延时启动"; | |||
| ton_rung.condition = ConditionExpression::fromNode(rising_edge); | |||
| ton_rung.output = ton_output; | |||
| logic.rungs.push_back(ton_rung); | |||
| LogicNode falling_edge; | |||
| falling_edge.id = "falling-edge"; | |||
| falling_edge.config = EdgeContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 5}, EdgeMode::Falling}; | |||
| LogicNode timer_contact; | |||
| timer_contact.id = "timer-contact"; | |||
| timer_contact.config = TimerContactNodeConfig{ | |||
| TimerAddress{7}, ContactMode::NormallyOpen}; | |||
| LogicNode timer_coil; | |||
| timer_coil.id = "timer-coil"; | |||
| timer_coil.config = CoilNodeConfig{ | |||
| LogicNode edge_output; | |||
| edge_output.id = "edge-output"; | |||
| edge_output.config = CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 6}, CoilMode::Normal}; | |||
| LadderRung timer_rung; | |||
| timer_rung.id = "timer-rung"; | |||
| timer_rung.name = "Timer feedback"; | |||
| timer_rung.comment = "定时器完成反馈"; | |||
| ConditionExpression timer_series; | |||
| timer_series.id = "timer-series"; | |||
| timer_series.kind = ConditionExpressionKind::Series; | |||
| timer_series.children = { | |||
| ConditionExpression::fromNode(falling_edge), | |||
| ConditionExpression::fromNode(timer_contact)}; | |||
| timer_rung.condition = std::move(timer_series); | |||
| 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); | |||
| LadderRung edge_rung; | |||
| edge_rung.id = "edge-rung"; | |||
| edge_rung.name = "Edge network"; | |||
| edge_rung.comment = "上升沿输出"; | |||
| edge_rung.condition = ConditionExpression::fromNode(rising_edge); | |||
| edge_rung.output = edge_output; | |||
| logic.rungs.push_back(edge_rung); | |||
| const auto addDataRung = [&logic]( | |||
| const std::string &rung_id, | |||
| @@ -452,10 +389,6 @@ void testExampleProjectRoundTrip() | |||
| && saved_json.contains("\"registerComments\"") | |||
| && saved_json.contains("\"text\": \"启动按钮\"") | |||
| && 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\"") | |||
| @@ -550,56 +483,26 @@ void testExampleProjectRoundTrip() | |||
| require(rung.output.has_value(), | |||
| "ladder output must survive round trip"); | |||
| const LadderRung &ton_rung = project.controlLogics.front().rungs.at(1); | |||
| require(ton_rung.output.has_value() | |||
| && std::holds_alternative<TonNodeConfig>(ton_rung.output->config) | |||
| && std::get<TonNodeConfig>(ton_rung.output->config).address | |||
| == TimerAddress{7} | |||
| && std::get<TonNodeConfig>(ton_rung.output->config).presetMs == 2500, | |||
| "TON timer address and preset must survive round trip"); | |||
| const LadderRung &edge_rung = project.controlLogics.front().rungs.at(1); | |||
| require(edge_rung.output.has_value() | |||
| && std::holds_alternative<CoilNodeConfig>(edge_rung.output->config) | |||
| && std::get<CoilNodeConfig>(edge_rung.output->config).address | |||
| == RegisterAddress{RegisterArea::M, 6}, | |||
| "edge network output must survive round trip"); | |||
| require(std::holds_alternative<EdgeContactNodeConfig>( | |||
| ton_rung.condition->node->config) | |||
| && std::get<EdgeContactNodeConfig>(ton_rung.condition->node->config).mode | |||
| edge_rung.condition->node->config) | |||
| && std::get<EdgeContactNodeConfig>(edge_rung.condition->node->config).mode | |||
| == EdgeMode::Rising, | |||
| "rising edge configuration must survive round trip"); | |||
| const LadderRung &timer_rung = project.controlLogics.front().rungs.at(2); | |||
| require(timer_rung.comment == "定时器完成反馈" | |||
| && std::holds_alternative<TimerContactNodeConfig>( | |||
| timer_rung.condition->children.at(1).node->config) | |||
| && std::get<TimerContactNodeConfig>( | |||
| timer_rung.condition->children.at(1).node->config).address | |||
| == TimerAddress{7} | |||
| && std::holds_alternative<EdgeContactNodeConfig>( | |||
| timer_rung.condition->children.front().node->config) | |||
| && std::get<EdgeContactNodeConfig>( | |||
| timer_rung.condition->children.front().node->config).mode | |||
| == 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) | |||
| project.controlLogics.front().rungs.at(2).output->config) | |||
| .source.constant == 25, | |||
| "MOVE operands must survive round trip"); | |||
| require(std::get<ArithmeticNodeConfig>( | |||
| project.controlLogics.front().rungs.at(6).output->config) | |||
| project.controlLogics.front().rungs.at(3).output->config) | |||
| .operation == ArithmeticOperation::Add | |||
| && std::get<ArithmeticNodeConfig>( | |||
| project.controlLogics.front().rungs.at(7).output->config) | |||
| project.controlLogics.front().rungs.at(4).output->config) | |||
| .operation == ArithmeticOperation::Subtract, | |||
| "ADD and SUB operations must survive round trip"); | |||
| @@ -143,54 +143,6 @@ void testModeTransitions() | |||
| comparison_rung.condition = ConditionExpression::fromNode(comparison); | |||
| comparison_rung.output = comparison_coil; | |||
| logic.rungs.push_back(comparison_rung); | |||
| LogicNode timer_input; | |||
| timer_input.id = "poll-timer-input"; | |||
| timer_input.config = ContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 23}, ContactMode::NormallyOpen}; | |||
| LogicNode ton; | |||
| ton.id = "poll-ton"; | |||
| ton.config = TonNodeConfig{TimerAddress{4}, 100}; | |||
| LadderRung ton_rung; | |||
| ton_rung.id = "poll-ton-rung"; | |||
| ton_rung.name = "Poll TON"; | |||
| ton_rung.condition = ConditionExpression::fromNode(timer_input); | |||
| ton_rung.output = ton; | |||
| logic.rungs.push_back(ton_rung); | |||
| LogicNode timer_contact; | |||
| timer_contact.id = "poll-timer-contact"; | |||
| timer_contact.config = TimerContactNodeConfig{ | |||
| TimerAddress{4}, ContactMode::NormallyOpen}; | |||
| LogicNode timer_coil; | |||
| timer_coil.id = "poll-timer-coil"; | |||
| timer_coil.config = CoilNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 24}, CoilMode::Normal}; | |||
| LadderRung timer_rung; | |||
| timer_rung.id = "poll-timer-rung"; | |||
| timer_rung.name = "Poll timer contact"; | |||
| timer_rung.condition = ConditionExpression::fromNode(timer_contact); | |||
| timer_rung.output = timer_coil; | |||
| logic.rungs.push_back(timer_rung); | |||
| LogicNode counter_input; | |||
| counter_input.id = "poll-counter-input"; | |||
| counter_input.config = ContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 25}, ContactMode::NormallyOpen}; | |||
| LogicNode counter_output; | |||
| counter_output.id = "poll-counter"; | |||
| counter_output.config = CounterNodeConfig{ | |||
| CounterAddress{2}, | |||
| CounterMode::Up, | |||
| RegisterAddress{RegisterArea::D, 36}, | |||
| WordOperand{ | |||
| WordOperandKind::Register, | |||
| RegisterAddress{RegisterArea::D, 37}, | |||
| 0}, | |||
| RegisterAddress{RegisterArea::M, 26}}; | |||
| LadderRung counter_rung; | |||
| counter_rung.id = "poll-counter-rung"; | |||
| counter_rung.name = "Poll counter"; | |||
| counter_rung.condition = ConditionExpression::fromNode(counter_input); | |||
| counter_rung.output = counter_output; | |||
| logic.rungs.push_back(counter_rung); | |||
| LogicNode move_input; | |||
| move_input.id = "poll-move-input"; | |||
| move_input.config = ContactNodeConfig{ | |||
| @@ -264,14 +216,11 @@ void testModeTransitions() | |||
| == std::vector<RegisterAddress>({ | |||
| {RegisterArea::M, 12}, {RegisterArea::M, 20}, | |||
| {RegisterArea::M, 21}, {RegisterArea::M, 22}, | |||
| {RegisterArea::M, 23}, {RegisterArea::M, 24}, | |||
| {RegisterArea::M, 25}, {RegisterArea::M, 26}, | |||
| {RegisterArea::M, 27}, {RegisterArea::M, 28}, | |||
| {RegisterArea::D, 34}, {RegisterArea::D, 35}, | |||
| {RegisterArea::D, 36}, {RegisterArea::D, 37}, | |||
| {RegisterArea::D, 38}, {RegisterArea::D, 39}, | |||
| {RegisterArea::D, 40}, {RegisterArea::D, 41}}), | |||
| "all instruction M/D references must be polled while T/C resources and comments stay offline-only"); | |||
| "all instruction M/D references must be polled while comments stay metadata-only"); | |||
| gateway.completeInitialRead(); | |||
| require(service.initialPlcReadCompleted(), | |||
| "service must retain the initial PLC read state"); | |||