| @@ -3,7 +3,6 @@ | |||||
| #include "project_limits.h" | #include "project_limits.h" | ||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <map> | |||||
| #include <type_traits> | #include <type_traits> | ||||
| #include <utility> | #include <utility> | ||||
| @@ -52,22 +51,6 @@ bool validateWordAddress( | |||||
| return true; | return true; | ||||
| } | } | ||||
| bool validateConfig(const CounterContactNodeConfig &config, std::string *error) | |||||
| { | |||||
| if (!config.address.isValid()) | |||||
| { | |||||
| setError(error, "计数器触点必须使用有效的 C 地址"); | |||||
| return false; | |||||
| } | |||||
| if (config.mode != ContactMode::NormallyOpen | |||||
| && config.mode != ContactMode::NormallyClosed) | |||||
| { | |||||
| setError(error, "计数器触点使用了不支持的模式"); | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| bool validateConfig(const EdgeContactNodeConfig &config, std::string *error) | bool validateConfig(const EdgeContactNodeConfig &config, std::string *error) | ||||
| { | { | ||||
| if (!config.address.isValid() || config.address.area() != RegisterArea::M) | if (!config.address.isValid() || config.address.area() != RegisterArea::M) | ||||
| @@ -120,70 +103,6 @@ bool validateConfig(const CompareNodeConfig &config, std::string *error) | |||||
| return true; | 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) | bool validateConfig(const MoveNodeConfig &config, std::string *error) | ||||
| { | { | ||||
| if (!config.source.validate(error)) | if (!config.source.validate(error)) | ||||
| @@ -419,11 +338,6 @@ bool validateConditionExpression( | |||||
| } // namespace | } // namespace | ||||
| TimerAddress::TimerAddress(int index) | |||||
| : index_(index) | |||||
| { | |||||
| } | |||||
| bool WordOperand::validate(std::string *error) const | bool WordOperand::validate(std::string *error) const | ||||
| { | { | ||||
| if (kind == WordOperandKind::Constant) | if (kind == WordOperandKind::Constant) | ||||
| @@ -443,61 +357,6 @@ bool WordOperand::validate(std::string *error) const | |||||
| return true; | 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( | std::optional<RegisterAddress> registerAddressForLogicNode( | ||||
| const LogicNodeConfig &config) | const LogicNodeConfig &config) | ||||
| { | { | ||||
| @@ -512,10 +371,6 @@ std::optional<RegisterAddress> registerAddressForLogicNode( | |||||
| { | { | ||||
| return value.address; | return value.address; | ||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, CounterNodeConfig>) | |||||
| { | |||||
| return value.currentValueAddress; | |||||
| } | |||||
| else if constexpr (std::is_same_v<Config, MoveNodeConfig> | else if constexpr (std::is_same_v<Config, MoveNodeConfig> | ||||
| || std::is_same_v<Config, ArithmeticNodeConfig>) | || std::is_same_v<Config, ArithmeticNodeConfig>) | ||||
| { | { | ||||
| @@ -545,15 +400,6 @@ void collectRegisterAddressesForLogicNode( | |||||
| { | { | ||||
| appendValidAddress(addresses, value.address); | 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>) | else if constexpr (std::is_same_v<Config, MoveNodeConfig>) | ||||
| { | { | ||||
| appendValidAddress(addresses, value.destination); | appendValidAddress(addresses, value.destination); | ||||
| @@ -574,10 +420,6 @@ void collectRegisterAddressesForLogicNode( | |||||
| appendValidAddress(addresses, value.right.address); | appendValidAddress(addresses, value.right.address); | ||||
| } | } | ||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | |||||
| { | |||||
| return; | |||||
| } | |||||
| }, | }, | ||||
| config); | config); | ||||
| } | } | ||||
| @@ -609,8 +451,6 @@ bool LogicNode::isCondition() const | |||||
| { | { | ||||
| return std::holds_alternative<ContactNodeConfig>(config) | return std::holds_alternative<ContactNodeConfig>(config) | ||||
| || std::holds_alternative<EdgeContactNodeConfig>(config) | || std::holds_alternative<EdgeContactNodeConfig>(config) | ||||
| || std::holds_alternative<TimerContactNodeConfig>(config) | |||||
| || std::holds_alternative<CounterContactNodeConfig>(config) | |||||
| || std::holds_alternative<CompareNodeConfig>(config); | || std::holds_alternative<CompareNodeConfig>(config); | ||||
| } | } | ||||
| @@ -618,8 +458,6 @@ bool LogicNode::isCondition() const | |||||
| bool LogicNode::isOutput() const | bool LogicNode::isOutput() const | ||||
| { | { | ||||
| return std::holds_alternative<CoilNodeConfig>(config) | return std::holds_alternative<CoilNodeConfig>(config) | ||||
| || std::holds_alternative<TonNodeConfig>(config) | |||||
| || std::holds_alternative<CounterNodeConfig>(config) | |||||
| || std::holds_alternative<MoveNodeConfig>(config) | || std::holds_alternative<MoveNodeConfig>(config) | ||||
| || std::holds_alternative<ArithmeticNodeConfig>(config); | || std::holds_alternative<ArithmeticNodeConfig>(config); | ||||
| } | } | ||||
| @@ -984,148 +822,3 @@ bool ControlLogic::validateForRunning(std::string *error) const | |||||
| } | } | ||||
| return true; | 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 寄存器中的数值 | Register // 使用 D 寄存器中的数值 | ||||
| }; | }; | ||||
| // MOVE、ADD/SUB、计数器预置值共用的字操作数 | |||||
| // MOVE、ADD/SUB 共用的字操作数 | |||||
| struct WordOperand | struct WordOperand | ||||
| { | { | ||||
| WordOperandKind kind = WordOperandKind::Constant; // 操作数来源 | WordOperandKind kind = WordOperandKind::Constant; // 操作数来源 | ||||
| @@ -94,90 +94,6 @@ struct CompareNodeConfig | |||||
| std::int16_t value = 0; | 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 地址 | // MOVE 输出:把一个字操作数写入目标 D 地址 | ||||
| struct MoveNodeConfig | struct MoveNodeConfig | ||||
| { | { | ||||
| @@ -204,12 +120,8 @@ struct ArithmeticNodeConfig | |||||
| using LogicNodeConfig = std::variant< | using LogicNodeConfig = std::variant< | ||||
| ContactNodeConfig, // 普通常开或常闭触点 | ContactNodeConfig, // 普通常开或常闭触点 | ||||
| EdgeContactNodeConfig, // 上升沿或下降沿触点 | EdgeContactNodeConfig, // 上升沿或下降沿触点 | ||||
| TimerContactNodeConfig, // 定时器完成触点 | |||||
| CounterContactNodeConfig, // 计数器完成触点 | |||||
| CoilNodeConfig, // 普通、置位或复位线圈 | CoilNodeConfig, // 普通、置位或复位线圈 | ||||
| CompareNodeConfig, // D 寄存器比较节点 | CompareNodeConfig, // D 寄存器比较节点 | ||||
| TonNodeConfig, // TON 定时器输出 | |||||
| CounterNodeConfig, // CTU 或 CTD 计数器输出 | |||||
| MoveNodeConfig, // MOVE 数据传送输出 | MoveNodeConfig, // MOVE 数据传送输出 | ||||
| ArithmeticNodeConfig>; // ADD 或 SUB 算术输出 | ArithmeticNodeConfig>; // ADD 或 SUB 算术输出 | ||||
| @@ -333,16 +245,3 @@ struct ControlLogic | |||||
| // 校验运行所需资源和网络配置 | // 校验运行所需资源和网络配置 | ||||
| bool validateForRunning(std::string *error = nullptr) const; | 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; | return false; | ||||
| } | } | ||||
| } | } | ||||
| if (!validateLogicResourceReferencesForRunning(controlLogics, error)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| return true; | return true; | ||||
| } | } | ||||
| @@ -270,64 +270,6 @@ bool parseAddress( | |||||
| return true; | 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) | QString wordOperandKindName(WordOperandKind kind) | ||||
| { | { | ||||
| return kind == WordOperandKind::Constant | return kind == WordOperandKind::Constant | ||||
| @@ -1113,24 +1055,6 @@ QJsonObject serializeNodeConfig(const EdgeContactNodeConfig &config) | |||||
| return object; | 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) | QJsonObject serializeNodeConfig(const CoilNodeConfig &config) | ||||
| { | { | ||||
| @@ -1152,35 +1076,6 @@ QJsonObject serializeNodeConfig(const CompareNodeConfig &config) | |||||
| return object; | 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 serializeNodeConfig(const MoveNodeConfig &config) | ||||
| { | { | ||||
| QJsonObject object; | QJsonObject object; | ||||
| @@ -1238,113 +1133,6 @@ bool parseNodeConfig( | |||||
| return false; | 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") | if (type == "move") | ||||
| { | { | ||||
| QJsonObject source_object; | QJsonObject source_object; | ||||
| @@ -1410,6 +1198,14 @@ bool parseNodeConfig( | |||||
| return true; | return true; | ||||
| } | } | ||||
| if (type != "contact" && type != "edgeContact" | |||||
| && type != "coil" && type != "compare") | |||||
| { | |||||
| return state->fail( | |||||
| ProjectStorageError::InvalidField, | |||||
| "不支持的逻辑节点类型:" + type); | |||||
| } | |||||
| QJsonObject address_object; | QJsonObject address_object; | ||||
| if (!readObject(object, "address", context, &address_object, state)) | if (!readObject(object, "address", context, &address_object, state)) | ||||
| { | { | ||||
| @@ -1481,11 +1277,7 @@ bool parseNodeConfig( | |||||
| address, comparison, static_cast<std::int16_t>(value)}; | address, comparison, static_cast<std::int16_t>(value)}; | ||||
| return true; | 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 | return left_config.address == right_config.address | ||||
| && left_config.mode == right_config.mode; | && 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>) | else if constexpr (std::is_same_v<Left, CoilNodeConfig>) | ||||
| { | { | ||||
| return left_config.address == right_config.address | return left_config.address == right_config.address | ||||
| @@ -720,21 +710,6 @@ bool LogicEditorService::configsEqual( | |||||
| && left_config.comparison == right_config.comparison | && left_config.comparison == right_config.comparison | ||||
| && left_config.value == right_config.value; | && 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>) | else if constexpr (std::is_same_v<Left, MoveNodeConfig>) | ||||
| { | { | ||||
| return left_config.source.kind == right_config.source.kind | 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) | return std::holds_alternative<ContactNodeConfig>(config) | ||||
| || std::holds_alternative<EdgeContactNodeConfig>(config) | || std::holds_alternative<EdgeContactNodeConfig>(config) | ||||
| || std::holds_alternative<TimerContactNodeConfig>(config) | |||||
| || std::holds_alternative<CounterContactNodeConfig>(config) | |||||
| || std::holds_alternative<CompareNodeConfig>(config); | || std::holds_alternative<CompareNodeConfig>(config); | ||||
| } | } | ||||
| bool LogicEditorService::isOutputConfig(const LogicNodeConfig &config) | bool LogicEditorService::isOutputConfig(const LogicNodeConfig &config) | ||||
| { | { | ||||
| return std::holds_alternative<CoilNodeConfig>(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<MoveNodeConfig>(config) | ||||
| || std::holds_alternative<ArithmeticNodeConfig>(config); | || std::holds_alternative<ArithmeticNodeConfig>(config); | ||||
| } | } | ||||
| @@ -2462,26 +2433,10 @@ std::string LogicEditorService::nodePrefix(const LogicNodeConfig &config) | |||||
| { | { | ||||
| return "edge"; | 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>) | else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | ||||
| { | { | ||||
| return "coil"; | 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>) | else if constexpr (std::is_same_v<Config, MoveNodeConfig>) | ||||
| { | { | ||||
| return "move"; | return "move"; | ||||
| @@ -31,7 +31,7 @@ SimulationStartResult OfflineSimulationService::start( | |||||
| return {false, SimulationStartError::InvalidLogic, validation}; | return {false, SimulationStartError::InvalidLogic, validation}; | ||||
| } | } | ||||
| // 重启会话时清空 T/C/沿触发状态和虚拟 M/D,避免继承上次运行现场 | |||||
| // 重启会话时清空沿触发状态和虚拟 M/D,避免继承上次运行现场 | |||||
| timer_.stop(); | timer_.stop(); | ||||
| executor_.resetRuntime(); | executor_.resetRuntime(); | ||||
| repository_.clear(); | repository_.clear(); | ||||
| @@ -79,8 +79,6 @@ void LogicTraceValues::clear() | |||||
| expressionInputValues.clear(); | expressionInputValues.clear(); | ||||
| expressionPowerValues.clear(); | expressionPowerValues.clear(); | ||||
| rungValues.clear(); | rungValues.clear(); | ||||
| tonValues.clear(); | |||||
| counterValues.clear(); | |||||
| wordValues.clear(); | wordValues.clear(); | ||||
| } | } | ||||
| @@ -103,8 +101,6 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||||
| projection.expressionInputValues = values->second.expressionInputValues; | projection.expressionInputValues = values->second.expressionInputValues; | ||||
| projection.expressionPowerValues = values->second.expressionPowerValues; | projection.expressionPowerValues = values->second.expressionPowerValues; | ||||
| projection.rungValues = values->second.rungValues; | projection.rungValues = values->second.rungValues; | ||||
| projection.tonValues = values->second.tonValues; | |||||
| projection.counterValues = values->second.counterValues; | |||||
| projection.wordValues = values->second.wordValues; | projection.wordValues = values->second.wordValues; | ||||
| } | } | ||||
| return projection; | return projection; | ||||
| @@ -113,7 +109,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||||
| LogicScanResult SoftwareLogicExecutor::validate( | LogicScanResult SoftwareLogicExecutor::validate( | ||||
| const std::vector<ControlLogic> &logics) const | const std::vector<ControlLogic> &logics) const | ||||
| { | { | ||||
| // 先检查每个逻辑自身,再检查跨网络共享线圈和 T/C 资源的冲突 | |||||
| // 先检查每个逻辑自身,再检查跨网络共享线圈的冲突 | |||||
| std::map<int, CoilMode> output_modes; | std::map<int, CoilMode> output_modes; | ||||
| for (const ControlLogic &logic : logics) | for (const ControlLogic &logic : logics) | ||||
| { | { | ||||
| @@ -145,9 +141,7 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config); | const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config); | ||||
| if (output == nullptr) | 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>( | || std::holds_alternative<ArithmeticNodeConfig>( | ||||
| rung.output->config)) | rung.output->config)) | ||||
| { | { | ||||
| @@ -174,35 +168,18 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| output_modes[output->address.index()] = output->mode; | output_modes[output->address.index()] = output->mode; | ||||
| } | } | ||||
| } | } | ||||
| std::string resource_error; | |||||
| if (!validateLogicResourceReferencesForRunning(logics, &resource_error)) | |||||
| { | |||||
| return failure(LogicScanError::InvalidLogic, resource_error); | |||||
| } | |||||
| return success(); | return success(); | ||||
| } | } | ||||
| void SoftwareLogicExecutor::resetRuntime() | void SoftwareLogicExecutor::resetRuntime() | ||||
| { | { | ||||
| previous_edge_inputs_.clear(); | previous_edge_inputs_.clear(); | ||||
| previous_counter_inputs_.clear(); | |||||
| ton_states_.clear(); | |||||
| counter_states_.clear(); | |||||
| } | } | ||||
| LogicScanResult SoftwareLogicExecutor::executeScan( | LogicScanResult SoftwareLogicExecutor::executeScan( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceSnapshot *trace) | 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); | const LogicScanResult validation = validate(logics); | ||||
| if (!validation.succeeded) | if (!validation.succeeded) | ||||
| @@ -258,11 +235,9 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| } | } | ||||
| bool output_value = false; | bool output_value = false; | ||||
| result = executeOutput( | result = executeOutput( | ||||
| logic.id, | |||||
| *rung.output, | *rung.output, | ||||
| rung_value, | rung_value, | ||||
| repository, | repository, | ||||
| now, | |||||
| logic_trace, | logic_trace, | ||||
| &output_value); | &output_value); | ||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| @@ -294,8 +269,6 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| trace->expressionInputValues = values->second.expressionInputValues; | trace->expressionInputValues = values->second.expressionInputValues; | ||||
| trace->expressionPowerValues = values->second.expressionPowerValues; | trace->expressionPowerValues = values->second.expressionPowerValues; | ||||
| trace->rungValues = values->second.rungValues; | trace->rungValues = values->second.rungValues; | ||||
| trace->tonValues = values->second.tonValues; | |||||
| trace->counterValues = values->second.counterValues; | |||||
| trace->wordValues = values->second.wordValues; | trace->wordValues = values->second.wordValues; | ||||
| } | } | ||||
| } | } | ||||
| @@ -383,7 +356,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| bool *value) | bool *value) | ||||
| { | { | ||||
| // 根据条件节点的具体配置读取 M/D 或内部 T/C 状态,并计算触点结果 | |||||
| // 根据条件节点的具体配置读取 M/D 状态,并计算触点结果 | |||||
| // 边沿触点使用 logic_id 和 node.id 组成运行时状态键,避免同名节点互相影响 | // 边沿触点使用 logic_id 和 node.id 组成运行时状态键,避免同名节点互相影响 | ||||
| if (value == nullptr) | if (value == nullptr) | ||||
| { | { | ||||
| @@ -437,20 +410,6 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| previous_edge_inputs_[runtime_key] = read.value; | previous_edge_inputs_[runtime_key] = read.value; | ||||
| return success(); | 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>) | else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | ||||
| { | { | ||||
| // 字比较触点读取 D 字,再按配置的比较运算符与目标值比较 | // 字比较触点读取 D 字,再按配置的比较运算符与目标值比较 | ||||
| @@ -525,11 +484,9 @@ LogicScanResult SoftwareLogicExecutor::readWordOperand( | |||||
| } | } | ||||
| LogicScanResult SoftwareLogicExecutor::executeOutput( | LogicScanResult SoftwareLogicExecutor::executeOutput( | ||||
| const std::string &logic_id, | |||||
| const LogicNode &node, | const LogicNode &node, | ||||
| bool rung_value, | bool rung_value, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| TimePoint now, | |||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool *output_value) | bool *output_value) | ||||
| { | { | ||||
| @@ -542,136 +499,6 @@ LogicScanResult SoftwareLogicExecutor::executeOutput( | |||||
| {}, | {}, | ||||
| node.id); | 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 (const auto *move = std::get_if<MoveNodeConfig>(&node.config)) | ||||
| { | { | ||||
| if (!rung_value) | if (!rung_value) | ||||
| @@ -3,7 +3,6 @@ | |||||
| #include "domain/control_logic_model.h" | #include "domain/control_logic_model.h" | ||||
| #include "domain/register_repository.h" | #include "domain/register_repository.h" | ||||
| #include <chrono> | |||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <map> | #include <map> | ||||
| #include <string> | #include <string> | ||||
| @@ -32,25 +31,6 @@ struct LogicScanResult | |||||
| std::string nodeId; // 失败关联的节点 ID | 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 节点的结果值和溢出标记 | // MOVE/ADD/SUB 节点的结果值和溢出标记 | ||||
| struct WordTraceValue | struct WordTraceValue | ||||
| { | { | ||||
| @@ -67,8 +47,6 @@ struct LogicTraceValues | |||||
| std::unordered_map<std::string, bool> expressionInputValues; // 表达式输入状态 | std::unordered_map<std::string, bool> expressionInputValues; // 表达式输入状态 | ||||
| std::unordered_map<std::string, bool> expressionPowerValues; // 表达式输出电源状态 | std::unordered_map<std::string, bool> expressionPowerValues; // 表达式输出电源状态 | ||||
| std::unordered_map<std::string, bool> rungValues; // 网络最终逻辑值 | 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 节点状态 | std::unordered_map<std::string, WordTraceValue> wordValues; // MOVE/ADD/SUB 节点状态 | ||||
| // 清除当前逻辑或网络的全部轨迹 | // 清除当前逻辑或网络的全部轨迹 | ||||
| @@ -90,22 +68,19 @@ struct LogicTraceSnapshot : LogicTraceValues | |||||
| class SoftwareLogicExecutor | class SoftwareLogicExecutor | ||||
| { | { | ||||
| public: | public: | ||||
| using Clock = std::chrono::steady_clock; | |||||
| using TimePoint = Clock::time_point; | |||||
| /** | /** | ||||
| * @brief 检查逻辑是否满足软件扫描的运行要求 | * @brief 检查逻辑是否满足软件扫描的运行要求 | ||||
| * @param logics 待检查的控制逻辑集合 | * @param logics 待检查的控制逻辑集合 | ||||
| * @return 成功或包含逻辑、网络、节点定位信息的失败结果 | * @return 成功或包含逻辑、网络、节点定位信息的失败结果 | ||||
| * | * | ||||
| * 检查逻辑结构、启用逻辑的运行配置、共享输出冲突以及 T/C 资源引用 | |||||
| * 检查逻辑结构、启用逻辑的运行配置和共享输出冲突 | |||||
| */ | */ | ||||
| LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | ||||
| /** | /** | ||||
| * @brief 清除所有跨扫描运行状态 | * @brief 清除所有跨扫描运行状态 | ||||
| * | * | ||||
| * 重置边沿触发、TON 定时器和 CTU/CTD 计数器;不修改寄存器仓库和轨迹对象 | |||||
| * 重置边沿触发状态;不修改寄存器仓库和轨迹对象 | |||||
| */ | */ | ||||
| void resetRuntime(); | void resetRuntime(); | ||||
| @@ -120,35 +95,12 @@ public: | |||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceSnapshot *trace = nullptr); | 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: | private: | ||||
| // 单个 TON 定时器跨扫描保存的计时状态 | |||||
| struct TonRuntimeState | |||||
| { | |||||
| bool timing = false; // 是否正在累计输入有效时间 | |||||
| bool done = false; // 是否已经完成预置时间 | |||||
| TimePoint startedAt{}; // 本次计时开始时间 | |||||
| std::chrono::milliseconds elapsed{0}; // 最近一次计算的累计时间 | |||||
| }; | |||||
| /** | /** | ||||
| * @brief 计算单个条件节点的导通结果 | * @brief 计算单个条件节点的导通结果 | ||||
| * @param logic_id 所属控制逻辑 ID,用于隔离边沿触点的跨扫描状态 | * @param logic_id 所属控制逻辑 ID,用于隔离边沿触点的跨扫描状态 | ||||
| * @param node 待读取和计算的条件节点 | * @param node 待读取和计算的条件节点 | ||||
| * @param repository 提供 M/D 值的寄存器仓库;T/C 条件读取执行器内部状态 | |||||
| * @param repository 提供 M/D 值的寄存器仓库 | |||||
| * @param value 输出节点导通结果,不能为空 | * @param value 输出节点导通结果,不能为空 | ||||
| * @return 成功结果,或包含节点 ID 的逻辑/寄存器读取失败结果 | * @return 成功结果,或包含节点 ID 的逻辑/寄存器读取失败结果 | ||||
| */ | */ | ||||
| @@ -176,21 +128,17 @@ private: | |||||
| bool *value); | bool *value); | ||||
| /** | /** | ||||
| * @brief 在网络结果驱动下执行单个输出节点 | * @brief 在网络结果驱动下执行单个输出节点 | ||||
| * @param logic_id 所属控制逻辑 ID,用于隔离计数器等跨扫描状态 | |||||
| * @param node 待读取配置并执行的输出节点 | * @param node 待读取配置并执行的输出节点 | ||||
| * @param rung_value 当前网络的逻辑结果,决定输出是否动作 | * @param rung_value 当前网络的逻辑结果,决定输出是否动作 | ||||
| * @param repository 提供输出读写所需的 M/D 寄存器仓库 | * @param repository 提供输出读写所需的 M/D 寄存器仓库 | ||||
| * @param now 本轮扫描的单调时钟时间点,TON 使用它计算累计时间 | |||||
| * @param trace 可选的轨迹输出,用于记录 TON、计数器和字操作结果 | |||||
| * @param trace 可选的轨迹输出,用于记录字操作结果 | |||||
| * @param output_value 输出节点最终逻辑值,不能为空 | * @param output_value 输出节点最终逻辑值,不能为空 | ||||
| * @return 成功结果,或包含节点 ID 的逻辑/寄存器读写失败结果 | * @return 成功结果,或包含节点 ID 的逻辑/寄存器读写失败结果 | ||||
| */ | */ | ||||
| LogicScanResult executeOutput( | LogicScanResult executeOutput( | ||||
| const std::string &logic_id, | |||||
| const LogicNode &node, | const LogicNode &node, | ||||
| bool rung_value, | bool rung_value, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| TimePoint now, | |||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool *output_value); | bool *output_value); | ||||
| /** | /** | ||||
| @@ -209,14 +157,4 @@ private: | |||||
| // 按逻辑 ID 和节点 ID 保存上一轮边沿输入 | // 按逻辑 ID 和节点 ID 保存上一轮边沿输入 | ||||
| std::map<std::pair<std::string, std::string>, bool> previous_edge_inputs_; | 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()); | 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) | QString wordOperandText(const WordOperand &operand) | ||||
| { | { | ||||
| return operand.kind == WordOperandKind::Register | return operand.kind == WordOperandKind::Register | ||||
| @@ -119,16 +109,6 @@ QString nodeToolTip(const LogicNodeConfig &config) | |||||
| : LogicEditorWidget::tr("下降")) | : LogicEditorWidget::tr("下降")) | ||||
| .arg(registerAddressText(value.address)); | .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>) | else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | ||||
| { | { | ||||
| return LogicEditorWidget::tr("输出线圈:%1") | return LogicEditorWidget::tr("输出线圈:%1") | ||||
| @@ -141,22 +121,6 @@ QString nodeToolTip(const LogicNodeConfig &config) | |||||
| .arg(comparisonText(value.comparison)) | .arg(comparisonText(value.comparison)) | ||||
| .arg(value.value); | .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>) | else if constexpr (std::is_same_v<Config, MoveNodeConfig>) | ||||
| { | { | ||||
| return LogicEditorWidget::tr("MOVE:%1 -> %2") | return LogicEditorWidget::tr("MOVE:%1 -> %2") | ||||
| @@ -358,41 +322,6 @@ public: | |||||
| configured_ ? registerAddressText(edge->address) : tr("< M 地址 >")); | configured_ ? registerAddressText(edge->address) : tr("< M 地址 >")); | ||||
| drawRegisterComment(painter, register_comment_); | 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_)) | else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_)) | ||||
| { | { | ||||
| painter->fillRect(QRectF(-35, -23, 70, 46), Qt::white); | painter->fillRect(QRectF(-35, -23, 70, 46), Qt::white); | ||||
| @@ -439,54 +368,6 @@ public: | |||||
| configured_ ? QString::number(comparison->value) : tr("< 常量 >")); | configured_ ? QString::number(comparison->value) : tr("< 常量 >")); | ||||
| drawRegisterComment(painter, register_comment_, 40.0); | 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_)) | else if (const auto *move = std::get_if<MoveNodeConfig>(&config_)) | ||||
| { | { | ||||
| const QRectF box(-47, -18, 94, 36); | const QRectF box(-47, -18, 94, 36); | ||||
| @@ -1228,17 +1109,7 @@ void LogicEditorWidget::reloadLogic() | |||||
| editor_service_.registerCommentFor(*address)) | editor_service_.registerCommentFor(*address)) | ||||
| : QString{}; | : 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_, | rung.output->id == fault_node_id_, | ||||
| false)); | false)); | ||||
| } | } | ||||
| @@ -31,14 +31,12 @@ LogicInstructionDialog::LogicInstructionDialog( | |||||
| original_config_(config) | original_config_(config) | ||||
| { | { | ||||
| ui_->setupUi(this); | ui_->setupUi(this); | ||||
| configureOperandCombo(ui_->counterPresetKindComboBox); | |||||
| configureOperandCombo(ui_->leftOperandKindComboBox); | configureOperandCombo(ui_->leftOperandKindComboBox); | ||||
| configureOperandCombo(ui_->rightOperandKindComboBox); | configureOperandCombo(ui_->rightOperandKindComboBox); | ||||
| connect(ui_->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); | connect(ui_->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); | ||||
| connect(ui_->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); | connect(ui_->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); | ||||
| for (QComboBox *combo : { | for (QComboBox *combo : { | ||||
| ui_->counterPresetKindComboBox, | |||||
| ui_->leftOperandKindComboBox, | ui_->leftOperandKindComboBox, | ||||
| ui_->rightOperandKindComboBox}) | ui_->rightOperandKindComboBox}) | ||||
| { | { | ||||
| @@ -49,11 +47,7 @@ LogicInstructionDialog::LogicInstructionDialog( | |||||
| &LogicInstructionDialog::updateOperandRanges); | &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); | configureMove(*move); | ||||
| } | } | ||||
| @@ -68,21 +62,6 @@ LogicInstructionDialog::~LogicInstructionDialog() = default; | |||||
| LogicNodeConfig LogicInstructionDialog::config() const | 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_)) | if (std::holds_alternative<MoveNodeConfig>(original_config_)) | ||||
| { | { | ||||
| return MoveNodeConfig{ | return MoveNodeConfig{ | ||||
| @@ -101,25 +80,6 @@ LogicNodeConfig LogicInstructionDialog::config() const | |||||
| ui_->destinationAddressSpinBox->value()}}; | 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) | void LogicInstructionDialog::configureMove(const MoveNodeConfig &config) | ||||
| { | { | ||||
| setWindowTitle(tr("配置 MOVE")); | setWindowTitle(tr("配置 MOVE")); | ||||
| @@ -165,13 +125,11 @@ void LogicInstructionDialog::updateOperandRanges() | |||||
| spin->setPrefix(register_operand ? QStringLiteral("D") : QString{}); | spin->setPrefix(register_operand ? QStringLiteral("D") : QString{}); | ||||
| spin->setMinimum(register_operand | spin->setMinimum(register_operand | ||||
| ? RegisterAddress::kMinimumIndex | ? 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 | spin->setMaximum(register_operand | ||||
| ? RegisterAddress::kMaximumIndex | ? RegisterAddress::kMaximumIndex | ||||
| : std::numeric_limits<std::int16_t>::max()); | : std::numeric_limits<std::int16_t>::max()); | ||||
| }; | }; | ||||
| update(ui_->counterPresetKindComboBox, ui_->counterPresetValueSpinBox); | |||||
| update(ui_->leftOperandKindComboBox, ui_->leftOperandValueSpinBox); | update(ui_->leftOperandKindComboBox, ui_->leftOperandValueSpinBox); | ||||
| update(ui_->rightOperandKindComboBox, ui_->rightOperandValueSpinBox); | update(ui_->rightOperandKindComboBox, ui_->rightOperandValueSpinBox); | ||||
| } | } | ||||
| @@ -38,8 +38,6 @@ public: | |||||
| LogicNodeConfig config() const; | LogicNodeConfig config() const; | ||||
| private: | private: | ||||
| /** 根据计数器配置初始化计数器输入项 */ | |||||
| void configureCounter(const CounterNodeConfig &config); | |||||
| /** 根据传送配置初始化传送输入项 */ | /** 根据传送配置初始化传送输入项 */ | ||||
| void configureMove(const MoveNodeConfig &config); | void configureMove(const MoveNodeConfig &config); | ||||
| /** 根据算术配置初始化运算输入项 */ | /** 根据算术配置初始化运算输入项 */ | ||||
| @@ -9,27 +9,6 @@ | |||||
| <layout class="QVBoxLayout" name="verticalLayout"> | <layout class="QVBoxLayout" name="verticalLayout"> | ||||
| <item> | <item> | ||||
| <widget class="QStackedWidget" name="instructionStackedWidget"> | <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"> | <widget class="QWidget" name="dataInstructionPage"> | ||||
| <layout class="QFormLayout" name="dataInstructionFormLayout"> | <layout class="QFormLayout" name="dataInstructionFormLayout"> | ||||
| <item row="0" column="0"><widget class="QLabel" name="instructionCaptionLabel"><property name="text"><string>指令</string></property></widget></item> | <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_closed = parallel_menu->addAction(tr("并联常闭触点")); | ||||
| QAction *parallel_rising = parallel_menu->addAction(tr("并联上升沿触点")); | QAction *parallel_rising = parallel_menu->addAction(tr("并联上升沿触点")); | ||||
| QAction *parallel_falling = 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("并联比较条件")); | QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件")); | ||||
| connect(parallel_open, &QAction::triggered, | connect(parallel_open, &QAction::triggered, | ||||
| this, | this, | ||||
| @@ -456,20 +454,6 @@ void MainWindow::configureActions() | |||||
| addLogicParallelBranch(EdgeContactNodeConfig{ | addLogicParallelBranch(EdgeContactNodeConfig{ | ||||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling}); | 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, | connect(parallel_compare, &QAction::triggered, | ||||
| this, | this, | ||||
| [this] | [this] | ||||
| @@ -515,20 +499,6 @@ void MainWindow::configureActions() | |||||
| addLogicCondition(EdgeContactNodeConfig{ | addLogicCondition(EdgeContactNodeConfig{ | ||||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling}); | 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, | connect(ui_->addNormalCoilAction, &QAction::triggered, | ||||
| this, | this, | ||||
| [this] | [this] | ||||
| @@ -553,40 +523,6 @@ void MainWindow::configureActions() | |||||
| RegisterAddress{RegisterArea::M, 0}, | RegisterAddress{RegisterArea::M, 0}, | ||||
| CoilMode::Reset}); | 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, | connect(ui_->addMoveAction, &QAction::triggered, | ||||
| this, | this, | ||||
| [this] | [this] | ||||
| @@ -650,21 +586,13 @@ void MainWindow::configureActions() | |||||
| makeUiIcon(UiIcon::NormallyOpenContact), | makeUiIcon(UiIcon::NormallyOpenContact), | ||||
| QList<QAction *>{ | QList<QAction *>{ | ||||
| ui_->addRisingEdgeAction, | ui_->addRisingEdgeAction, | ||||
| ui_->addFallingEdgeAction, | |||||
| ui_->addTimerContactAction, | |||||
| ui_->addCounterContactAction}); | |||||
| ui_->addFallingEdgeAction}); | |||||
| addToolbarMenu( | addToolbarMenu( | ||||
| ui_->logicToolBar, | ui_->logicToolBar, | ||||
| tr("更多输出"), | tr("更多输出"), | ||||
| QStringLiteral("logicOutputMenuButton"), | QStringLiteral("logicOutputMenuButton"), | ||||
| makeUiIcon(UiIcon::Coil), | makeUiIcon(UiIcon::Coil), | ||||
| QList<QAction *>{ui_->addSetCoilAction, ui_->addResetCoilAction}); | QList<QAction *>{ui_->addSetCoilAction, ui_->addResetCoilAction}); | ||||
| addToolbarMenu( | |||||
| ui_->logicToolBar, | |||||
| tr("定时/计数"), | |||||
| QStringLiteral("logicTimerCounterMenuButton"), | |||||
| makeUiIcon(UiIcon::Ton), | |||||
| QList<QAction *>{ui_->addTonAction, ui_->addCtuAction, ui_->addCtdAction}); | |||||
| addToolbarMenu( | addToolbarMenu( | ||||
| ui_->logicToolBar, | ui_->logicToolBar, | ||||
| tr("数据运算"), | tr("数据运算"), | ||||
| @@ -749,14 +677,9 @@ void MainWindow::configureAppearance() | |||||
| ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact)); | ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact)); | ||||
| ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact)); | ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact)); | ||||
| ui_->addFallingEdgeAction->setIcon(makeUiIcon(UiIcon::FallingEdgeContact)); | 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_->addNormalCoilAction->setIcon(makeUiIcon(UiIcon::Coil)); | ||||
| ui_->addSetCoilAction->setIcon(makeUiIcon(UiIcon::SetCoil)); | ui_->addSetCoilAction->setIcon(makeUiIcon(UiIcon::SetCoil)); | ||||
| ui_->addResetCoilAction->setIcon(makeUiIcon(UiIcon::ResetCoil)); | 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_->addMoveAction->setIcon(makeUiIcon(UiIcon::Move)); | ||||
| ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add)); | ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add)); | ||||
| ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract)); | ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract)); | ||||
| @@ -1601,14 +1524,9 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); | ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addRisingEdgeAction->setEnabled(policy.allowsProjectEditing); | ui_->addRisingEdgeAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addFallingEdgeAction->setEnabled(policy.allowsProjectEditing); | ui_->addFallingEdgeAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addTimerContactAction->setEnabled(policy.allowsProjectEditing); | |||||
| ui_->addCounterContactAction->setEnabled(policy.allowsProjectEditing); | |||||
| ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing); | ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing); | ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addResetCoilAction->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_->addMoveAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addAddAction->setEnabled(policy.allowsProjectEditing); | ui_->addAddAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addSubAction->setEnabled(policy.allowsProjectEditing); | ui_->addSubAction->setEnabled(policy.allowsProjectEditing); | ||||
| @@ -922,27 +922,7 @@ | |||||
| </property> | </property> | ||||
| </widget> | </widget> | ||||
| </item> | </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"> | <widget class="QPushButton" name="applyLogicPropertiesButton"> | ||||
| <property name="text"> | <property name="text"> | ||||
| <string>应用属性</string> | <string>应用属性</string> | ||||
| @@ -1252,18 +1232,6 @@ | |||||
| <string>在选中的空网格或横线处添加一个扫描周期的下降沿触点</string> | <string>在选中的空网格或横线处添加一个扫描周期的下降沿触点</string> | ||||
| </property> | </property> | ||||
| </action> | </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"> | <action name="addNormalCoilAction"> | ||||
| <property name="text"> | <property name="text"> | ||||
| <string>线圈</string> | <string>线圈</string> | ||||
| @@ -1288,22 +1256,6 @@ | |||||
| <string>设置当前网络的复位线圈</string> | <string>设置当前网络的复位线圈</string> | ||||
| </property> | </property> | ||||
| </action> | </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"> | <action name="addMoveAction"> | ||||
| <property name="text"><string>MOVE</string></property> | <property name="text"><string>MOVE</string></property> | ||||
| <property name="toolTip"><string>设置 D 数据传送指令</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_.logicModeComboBox), | ||||
| static_cast<QWidget *>(ui_.logicComparisonComboBox), | static_cast<QWidget *>(ui_.logicComparisonComboBox), | ||||
| static_cast<QWidget *>(ui_.logicValueSpinBox), | static_cast<QWidget *>(ui_.logicValueSpinBox), | ||||
| static_cast<QWidget *>(ui_.logicPresetSpinBox), | |||||
| static_cast<QWidget *>(ui_.applyLogicPropertiesButton)}) | static_cast<QWidget *>(ui_.applyLogicPropertiesButton)}) | ||||
| { | { | ||||
| widget->setEnabled(has_node); | widget->setEnabled(has_node); | ||||
| @@ -391,7 +390,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id | |||||
| ui_.logicNodeTypeLabel->clear(); | ui_.logicNodeTypeLabel->clear(); | ||||
| ui_.logicAddressAreaLabel->clear(); | ui_.logicAddressAreaLabel->clear(); | ||||
| ui_.logicModeComboBox->clear(); | ui_.logicModeComboBox->clear(); | ||||
| ui_.logicPresetSpinBox->setValue(TonNodeConfig::kMinimumPresetMs); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -399,7 +397,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id | |||||
| ui_.logicModeComboBox->clear(); | ui_.logicModeComboBox->clear(); | ||||
| ui_.logicComparisonComboBox->setEnabled(false); | ui_.logicComparisonComboBox->setEnabled(false); | ||||
| ui_.logicValueSpinBox->setEnabled(false); | ui_.logicValueSpinBox->setEnabled(false); | ||||
| ui_.logicPresetSpinBox->setEnabled(false); | |||||
| if (const auto *contact = std::get_if<ContactNodeConfig>(&node->config)) | if (const auto *contact = std::get_if<ContactNodeConfig>(&node->config)) | ||||
| { | { | ||||
| ui_.logicNodeTypeLabel->setText(QObject::tr("触点")); | 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->findData(static_cast<int>(edge->mode))); | ||||
| ui_.logicModeComboBox->setEnabled(true); | 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)) | else if (const auto *coil = std::get_if<CoilNodeConfig>(&node->config)) | ||||
| { | { | ||||
| ui_.logicNodeTypeLabel->setText(QObject::tr("线圈")); | ui_.logicNodeTypeLabel->setText(QObject::tr("线圈")); | ||||
| @@ -482,27 +452,6 @@ void PropertyPanelController::showLogicNodeProperties(const std::string &node_id | |||||
| ui_.logicValueSpinBox->setEnabled(true); | ui_.logicValueSpinBox->setEnabled(true); | ||||
| ui_.logicValueSpinBox->setValue(compare->value); | 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)) | else if (const auto *move = std::get_if<MoveNodeConfig>(&node->config)) | ||||
| { | { | ||||
| ui_.logicNodeTypeLabel->setText(QStringLiteral("MOVE")); | ui_.logicNodeTypeLabel->setText(QStringLiteral("MOVE")); | ||||
| @@ -726,18 +675,6 @@ void PropertyPanelController::applySelectedLogicNodeProperties() | |||||
| RegisterAddress{RegisterArea::M, ui_.logicAddressSpinBox->value()}, | RegisterAddress{RegisterArea::M, ui_.logicAddressSpinBox->value()}, | ||||
| static_cast<EdgeMode>(ui_.logicModeComboBox->currentData().toInt())}; | 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)) | else if (std::holds_alternative<CoilNodeConfig>(config)) | ||||
| { | { | ||||
| config = CoilNodeConfig{ | config = CoilNodeConfig{ | ||||
| @@ -752,12 +689,6 @@ void PropertyPanelController::applySelectedLogicNodeProperties() | |||||
| ui_.logicComparisonComboBox->currentData().toInt()), | ui_.logicComparisonComboBox->currentData().toInt()), | ||||
| static_cast<std::int16_t>(ui_.logicValueSpinBox->value())}; | 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 | else | ||||
| { | { | ||||
| LogicInstructionDialog dialog(node->config, &parent_); | LogicInstructionDialog dialog(node->config, &parent_); | ||||
| @@ -410,16 +410,6 @@ QPixmap renderIcon(UiIcon icon, int size) | |||||
| drawContact(painter, QStringLiteral("N"), false); | drawContact(painter, QStringLiteral("N"), false); | ||||
| break; | break; | ||||
| } | } | ||||
| case UiIcon::TimerContact: | |||||
| { | |||||
| drawContact(painter, QStringLiteral("T"), false); | |||||
| break; | |||||
| } | |||||
| case UiIcon::CounterContact: | |||||
| { | |||||
| drawContact(painter, QStringLiteral("C"), false); | |||||
| break; | |||||
| } | |||||
| case UiIcon::Coil: | case UiIcon::Coil: | ||||
| { | { | ||||
| drawCoil(painter, {}); | drawCoil(painter, {}); | ||||
| @@ -435,21 +425,6 @@ QPixmap renderIcon(UiIcon icon, int size) | |||||
| drawCoil(painter, QStringLiteral("R")); | drawCoil(painter, QStringLiteral("R")); | ||||
| break; | 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: | case UiIcon::Move: | ||||
| { | { | ||||
| painter.drawRect(QRectF(2, 6, 6, 12)); | painter.drawRect(QRectF(2, 6, 6, 12)); | ||||
| @@ -44,14 +44,9 @@ enum class UiIcon | |||||
| NormallyClosedContact, // 常闭触点 | NormallyClosedContact, // 常闭触点 | ||||
| RisingEdgeContact, // 上升沿触点 | RisingEdgeContact, // 上升沿触点 | ||||
| FallingEdgeContact, // 下降沿触点 | FallingEdgeContact, // 下降沿触点 | ||||
| TimerContact, // 定时器触点 | |||||
| CounterContact, // 计数器触点 | |||||
| Coil, // 普通线圈 | Coil, // 普通线圈 | ||||
| SetCoil, // 置位线圈 | SetCoil, // 置位线圈 | ||||
| ResetCoil, // 复位线圈 | ResetCoil, // 复位线圈 | ||||
| Ton, // TON 定时器 | |||||
| Ctu, // CTU 加计数器 | |||||
| Ctd, // CTD 减计数器 | |||||
| Move, // MOVE 指令 | Move, // MOVE 指令 | ||||
| Add, // ADD 指令 | Add, // ADD 指令 | ||||
| Subtract, // SUB 指令 | Subtract, // SUB 指令 | ||||
| @@ -434,14 +434,6 @@ void testQuantityBoundaries() | |||||
| require(!excessive_depth.validate(), | require(!excessive_depth.validate(), | ||||
| "an expression depth of 21 must be rejected"); | "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; | (void)leaf; | ||||
| } | } | ||||
| @@ -469,7 +461,7 @@ void testLogicNodeConfigurationBoundaries() | |||||
| require(comparison.validate(), "comparison node bound to D address must be valid"); | require(comparison.validate(), "comparison node bound to D address must be valid"); | ||||
| } | } | ||||
| void testTimerAndCommentBoundaries() | |||||
| void testEdgeAndCommentBoundaries() | |||||
| { | { | ||||
| LogicNode edge; | LogicNode edge; | ||||
| edge.id = "edge"; | edge.id = "edge"; | ||||
| @@ -477,25 +469,6 @@ void testTimerAndCommentBoundaries() | |||||
| RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising}; | RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising}; | ||||
| require(edge.validate(), "a valid rising edge contact must pass validation"); | 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}, "启动按钮"}; | RegisterComment comment{RegisterAddress{RegisterArea::M, 0}, "启动按钮"}; | ||||
| require(comment.validate(), "a nonblank register comment must be valid"); | require(comment.validate(), "a nonblank register comment must be valid"); | ||||
| comment.text.assign(ProjectLimits::kMaximumRegisterCommentBytes, 'a'); | comment.text.assign(ProjectLimits::kMaximumRegisterCommentBytes, 'a'); | ||||
| @@ -528,124 +501,8 @@ void testTimerAndCommentBoundaries() | |||||
| require(!project.validate(), "duplicate register comments must be rejected"); | 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; | LogicNode move; | ||||
| move.id = "move"; | move.id = "move"; | ||||
| move.config = MoveNodeConfig{ | move.config = MoveNodeConfig{ | ||||
| @@ -673,39 +530,6 @@ void testCounterAndDataInstructionBoundaries() | |||||
| require(add.validate() && add.isOutput(), | require(add.validate() && add.isOutput(), | ||||
| "ADD must allow the same D register as source and destination"); | "ADD must allow the same D register as source and destination"); | ||||
| ControlLogic valid; | |||||
| valid.id = "counter-valid"; | |||||
| valid.name = "Counter valid"; | |||||
| valid.rungs.push_back(makeCounterRung("counter-rung", "ctu-0", 0)); | |||||
| require(validateCounterReferencesForRunning({valid}), | |||||
| "a counter output without contacts must pass reference validation"); | |||||
| ControlLogic duplicate = valid; | |||||
| duplicate.id = "counter-duplicate"; | |||||
| duplicate.name = "Counter duplicate"; | |||||
| duplicate.rungs.front().output->id = "ctu-duplicate"; | |||||
| require(!validateCounterReferencesForRunning({valid, duplicate}), | |||||
| "the same C resource must not have multiple enabled drivers"); | |||||
| ControlLogic missing; | |||||
| missing.id = "counter-missing"; | |||||
| missing.name = "Counter missing"; | |||||
| LogicNode missing_contact; | |||||
| missing_contact.id = "missing-counter-contact"; | |||||
| missing_contact.config = CounterContactNodeConfig{ | |||||
| CounterAddress{7}, ContactMode::NormallyOpen}; | |||||
| LogicNode output; | |||||
| output.id = "missing-counter-coil"; | |||||
| output.config = CoilNodeConfig{ | |||||
| RegisterAddress{RegisterArea::M, 7}, CoilMode::Normal}; | |||||
| LadderRung missing_rung; | |||||
| missing_rung.id = "missing-counter-rung"; | |||||
| missing_rung.name = "Missing counter rung"; | |||||
| missing_rung.condition = ConditionExpression::fromNode(missing_contact); | |||||
| missing_rung.output = output; | |||||
| missing.rungs.push_back(missing_rung); | |||||
| require(!validateCounterReferencesForRunning({missing}), | |||||
| "a C contact without an enabled counter driver must be rejected"); | |||||
| } | } | ||||
| void testLadderLogicBoundaries() | void testLadderLogicBoundaries() | ||||
| @@ -914,9 +738,8 @@ int main() | |||||
| testProgressBarConfigurationBoundaries(); | testProgressBarConfigurationBoundaries(); | ||||
| testHmiAppearancePropertyBoundaries(); | testHmiAppearancePropertyBoundaries(); | ||||
| testLogicNodeConfigurationBoundaries(); | testLogicNodeConfigurationBoundaries(); | ||||
| testTimerAndCommentBoundaries(); | |||||
| testTimerReferencesForRunning(); | |||||
| testCounterAndDataInstructionBoundaries(); | |||||
| testEdgeAndCommentBoundaries(); | |||||
| testDataInstructionBoundaries(); | |||||
| testLadderLogicBoundaries(); | testLadderLogicBoundaries(); | ||||
| testModelsValidateBindingsAndIdentifiers(); | testModelsValidateBindingsAndIdentifiers(); | ||||
| testMultiPageAndLogicDomainRules(); | testMultiPageAndLogicDomainRules(); | ||||
| @@ -883,7 +883,7 @@ void testLogicLifecycleAndOrdering() | |||||
| "the project must retain at least one control logic module"); | "the project must retain at least one control logic module"); | ||||
| } | } | ||||
| void testEdgeTimerNodesAndRungComments() | |||||
| void testEdgeNodesAndRungComments() | |||||
| { | { | ||||
| TestProjectStorage storage; | TestProjectStorage storage; | ||||
| ProjectService project_service(storage); | ProjectService project_service(storage); | ||||
| @@ -898,18 +898,6 @@ void testEdgeTimerNodesAndRungComments() | |||||
| RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising}); | RegisterAddress{RegisterArea::M, 3}, EdgeMode::Rising}); | ||||
| require(rising.succeeded && rising.id == "edge-1", | require(rising.succeeded && rising.id == "edge-1", | ||||
| "the editor must create rising edge nodes with a stable prefix"); | "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( | require(service.updateNodeConfig( | ||||
| logic_id, | logic_id, | ||||
| rising.id, | rising.id, | ||||
| @@ -917,18 +905,6 @@ void testEdgeTimerNodesAndRungComments() | |||||
| RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling}) | RegisterAddress{RegisterArea::M, 4}, EdgeMode::Falling}) | ||||
| .succeeded, | .succeeded, | ||||
| "the editor must apply edge mode and M address properties"); | "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, | require(service.updateRungComment(logic_id, rung_id, "延时启动网络").succeeded, | ||||
| "the editor must update a network comment by stable rung id"); | "the editor must update a network comment by stable rung id"); | ||||
| require(!service.updateRungComment( | require(!service.updateRungComment( | ||||
| @@ -942,15 +918,12 @@ void testEdgeTimerNodesAndRungComments() | |||||
| "the editor must reject oversized network comments"); | "the editor must reject oversized network comments"); | ||||
| const LadderRung *rung = service.findRung(logic_id, rung_id); | const LadderRung *rung = service.findRung(logic_id, rung_id); | ||||
| const LogicNode *edge = service.findNode(logic_id, rising.id); | |||||
| require(rung != nullptr && rung->comment == "延时启动网络" | 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() | void testHistoryAndAtomicBatchDelete() | ||||
| @@ -1045,7 +1018,7 @@ int main() | |||||
| testWireColumnReplacement(); | testWireColumnReplacement(); | ||||
| testSequentialConditionInsertionConsumesFollowingWire(); | testSequentialConditionInsertionConsumesFollowingWire(); | ||||
| testLogicLifecycleAndOrdering(); | testLogicLifecycleAndOrdering(); | ||||
| testEdgeTimerNodesAndRungComments(); | |||||
| testEdgeNodesAndRungComments(); | |||||
| testHistoryAndAtomicBatchDelete(); | testHistoryAndAtomicBatchDelete(); | ||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| @@ -32,27 +32,6 @@ LogicNode edgeContact(const std::string &id, int address, EdgeMode mode) | |||||
| true}; | 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) | WordOperand constantOperand(std::int16_t value) | ||||
| { | { | ||||
| return { | return { | ||||
| @@ -69,25 +48,6 @@ WordOperand registerOperand(int address) | |||||
| 0}; | 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( | LogicNode move( | ||||
| const std::string &id, | const std::string &id, | ||||
| WordOperand source, | WordOperand source, | ||||
| @@ -479,221 +439,6 @@ void testEdgeContactsAreOneScanPulsesAndAreLogicScoped() | |||||
| "a restarted runtime must treat current ON inputs as fresh rising edges"); | "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() | void testMoveAndSaturatingArithmetic() | ||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| @@ -924,9 +669,6 @@ int main(int argc, char *argv[]) | |||||
| testSetResetAndDisabledLogic(); | testSetResetAndDisabledLogic(); | ||||
| testMultipleLogicScanOrderAndTraceIsolation(); | testMultipleLogicScanOrderAndTraceIsolation(); | ||||
| testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); | testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); | ||||
| testTonUsesElapsedTimeAndResetsAsNonRetentive(); | |||||
| testMultipleTimersAndNetworkOrder(); | |||||
| testCountersUseRisingEdgesAndExternalDValues(); | |||||
| testMoveAndSaturatingArithmetic(); | testMoveAndSaturatingArithmetic(); | ||||
| testSetResetPairOnSameAddress(); | testSetResetPairOnSameAddress(); | ||||
| testConflictingCoilsAreRejected(); | testConflictingCoilsAreRejected(); | ||||
| @@ -159,80 +159,17 @@ Project makeExampleProject() | |||||
| rising_edge.id = "rising-edge"; | rising_edge.id = "rising-edge"; | ||||
| rising_edge.config = EdgeContactNodeConfig{ | rising_edge.config = EdgeContactNodeConfig{ | ||||
| RegisterAddress{RegisterArea::M, 4}, EdgeMode::Rising}; | 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}; | 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 auto addDataRung = [&logic]( | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -452,10 +389,6 @@ void testExampleProjectRoundTrip() | |||||
| && saved_json.contains("\"registerComments\"") | && saved_json.contains("\"registerComments\"") | ||||
| && saved_json.contains("\"text\": \"启动按钮\"") | && saved_json.contains("\"text\": \"启动按钮\"") | ||||
| && saved_json.contains("\"type\": \"edgeContact\"") | && 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\": \"move\"") | ||||
| && saved_json.contains("\"type\": \"arithmetic\"") | && saved_json.contains("\"type\": \"arithmetic\"") | ||||
| && saved_json.contains("\"operation\": \"add\"") | && saved_json.contains("\"operation\": \"add\"") | ||||
| @@ -550,56 +483,26 @@ void testExampleProjectRoundTrip() | |||||
| require(rung.output.has_value(), | require(rung.output.has_value(), | ||||
| "ladder output must survive round trip"); | "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>( | 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, | == EdgeMode::Rising, | ||||
| "rising edge configuration must survive round trip"); | "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>( | require(std::get<MoveNodeConfig>( | ||||
| project.controlLogics.front().rungs.at(5).output->config) | |||||
| project.controlLogics.front().rungs.at(2).output->config) | |||||
| .source.constant == 25, | .source.constant == 25, | ||||
| "MOVE operands must survive round trip"); | "MOVE operands must survive round trip"); | ||||
| require(std::get<ArithmeticNodeConfig>( | require(std::get<ArithmeticNodeConfig>( | ||||
| project.controlLogics.front().rungs.at(6).output->config) | |||||
| project.controlLogics.front().rungs.at(3).output->config) | |||||
| .operation == ArithmeticOperation::Add | .operation == ArithmeticOperation::Add | ||||
| && std::get<ArithmeticNodeConfig>( | && std::get<ArithmeticNodeConfig>( | ||||
| project.controlLogics.front().rungs.at(7).output->config) | |||||
| project.controlLogics.front().rungs.at(4).output->config) | |||||
| .operation == ArithmeticOperation::Subtract, | .operation == ArithmeticOperation::Subtract, | ||||
| "ADD and SUB operations must survive round trip"); | "ADD and SUB operations must survive round trip"); | ||||
| @@ -143,54 +143,6 @@ void testModeTransitions() | |||||
| comparison_rung.condition = ConditionExpression::fromNode(comparison); | comparison_rung.condition = ConditionExpression::fromNode(comparison); | ||||
| comparison_rung.output = comparison_coil; | comparison_rung.output = comparison_coil; | ||||
| logic.rungs.push_back(comparison_rung); | 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; | LogicNode move_input; | ||||
| move_input.id = "poll-move-input"; | move_input.id = "poll-move-input"; | ||||
| move_input.config = ContactNodeConfig{ | move_input.config = ContactNodeConfig{ | ||||
| @@ -264,14 +216,11 @@ void testModeTransitions() | |||||
| == std::vector<RegisterAddress>({ | == std::vector<RegisterAddress>({ | ||||
| {RegisterArea::M, 12}, {RegisterArea::M, 20}, | {RegisterArea::M, 12}, {RegisterArea::M, 20}, | ||||
| {RegisterArea::M, 21}, {RegisterArea::M, 22}, | {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::M, 27}, {RegisterArea::M, 28}, | ||||
| {RegisterArea::D, 34}, {RegisterArea::D, 35}, | {RegisterArea::D, 34}, {RegisterArea::D, 35}, | ||||
| {RegisterArea::D, 36}, {RegisterArea::D, 37}, | |||||
| {RegisterArea::D, 38}, {RegisterArea::D, 39}, | {RegisterArea::D, 38}, {RegisterArea::D, 39}, | ||||
| {RegisterArea::D, 40}, {RegisterArea::D, 41}}), | {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(); | gateway.completeInitialRead(); | ||||
| require(service.initialPlcReadCompleted(), | require(service.initialPlcReadCompleted(), | ||||
| "service must retain the initial PLC read state"); | "service must retain the initial PLC read state"); | ||||