| @@ -23,6 +23,7 @@ ui_*.h | |||||
| *.ilk | *.ilk | ||||
| *.exp | *.exp | ||||
| *.manifest | *.manifest | ||||
| *.autosave | |||||
| # Qt Creator local user settings | # Qt Creator local user settings | ||||
| *.pro.user | *.pro.user | ||||
| @@ -63,3 +64,5 @@ Desktop.ini | |||||
| 设计方案书.doc | 设计方案书.doc | ||||
| /学习 | /学习 | ||||
| /json | /json | ||||
| /代码 | |||||
| /other_version | |||||
| @@ -8,19 +8,19 @@ | |||||
| // 报警触发条件:M 位为 1,或 D 字高于/低于阈值 | // 报警触发条件:M 位为 1,或 D 字高于/低于阈值 | ||||
| enum class AlarmCondition | enum class AlarmCondition | ||||
| { | { | ||||
| MOn, | |||||
| DHigh, | |||||
| DLow | |||||
| MOn, // M 位为 1 时触发 | |||||
| DHigh, // D 寄存器值高于阈值时触发 | |||||
| DLow // D 寄存器值低于阈值时触发 | |||||
| }; | }; | ||||
| // 可保存的报警定义;运行时 AlarmService 根据此定义生成报警记录 | // 可保存的报警定义;运行时 AlarmService 根据此定义生成报警记录 | ||||
| struct AlarmDefinition | struct AlarmDefinition | ||||
| { | { | ||||
| std::string id; | |||||
| RegisterAddress address{RegisterArea::M, 0}; | |||||
| AlarmCondition condition = AlarmCondition::MOn; | |||||
| std::int16_t threshold = 0; | |||||
| std::string message; | |||||
| std::string id; // 报警定义的唯一 ID | |||||
| RegisterAddress address{RegisterArea::M, 0}; // 报警监控的 M 或 D 地址 | |||||
| AlarmCondition condition = AlarmCondition::MOn; // 报警触发条件 | |||||
| std::int16_t threshold = 0; // D 地址比较时使用的阈值 | |||||
| std::string message; // 报警触发后显示的提示文字 | |||||
| // 校验标识、地址区域、消息长度和条件之间的组合是否有效 | // 校验标识、地址区域、消息长度和条件之间的组合是否有效 | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| @@ -604,6 +604,7 @@ bool LogicNode::isConfigured() const | |||||
| return configured; | return configured; | ||||
| } | } | ||||
| // 判断当前节点是不是条件触点类节点 | |||||
| bool LogicNode::isCondition() const | bool LogicNode::isCondition() const | ||||
| { | { | ||||
| return std::holds_alternative<ContactNodeConfig>(config) | return std::holds_alternative<ContactNodeConfig>(config) | ||||
| @@ -613,6 +614,7 @@ bool LogicNode::isCondition() const | |||||
| || std::holds_alternative<CompareNodeConfig>(config); | || std::holds_alternative<CompareNodeConfig>(config); | ||||
| } | } | ||||
| // 判断当前节点是不是输出执行类节点 | |||||
| bool LogicNode::isOutput() const | bool LogicNode::isOutput() const | ||||
| { | { | ||||
| return std::holds_alternative<CoilNodeConfig>(config) | return std::holds_alternative<CoilNodeConfig>(config) | ||||
| @@ -12,103 +12,114 @@ | |||||
| // 普通触点的导通方式;常闭触点会对读取到的位值取反 | // 普通触点的导通方式;常闭触点会对读取到的位值取反 | ||||
| enum class ContactMode | enum class ContactMode | ||||
| { | { | ||||
| NormallyOpen, | |||||
| NormallyClosed | |||||
| NormallyOpen, // 常开:位值为 1 时触点导通 | |||||
| NormallyClosed // 常闭:位值为 0 时触点导通 | |||||
| }; | }; | ||||
| // 线圈写入方式:普通写入、置位保持、复位清零 | // 线圈写入方式:普通写入、置位保持、复位清零 | ||||
| enum class CoilMode | enum class CoilMode | ||||
| { | { | ||||
| Normal, | |||||
| Set, | |||||
| Reset | |||||
| Normal, // 普通写入:直接写入当前逻辑结果 | |||||
| Set, // 置位:逻辑结果为 1 时保持为 1 | |||||
| Reset // 复位:逻辑结果为 1 时清零 | |||||
| }; | }; | ||||
| // 上升沿/下降沿触点的边沿方向 | // 上升沿/下降沿触点的边沿方向 | ||||
| enum class EdgeMode | enum class EdgeMode | ||||
| { | { | ||||
| Rising, | |||||
| Falling | |||||
| Rising, // 上升沿:信号从 0 变成 1 | |||||
| Falling // 下降沿:信号从 1 变成 0 | |||||
| }; | }; | ||||
| // D 字比较节点支持的比较运算 | |||||
| // D 寄存器字比较,等于、不等于、大于、小于等 6 种比较 | |||||
| enum class ComparisonOperator | enum class ComparisonOperator | ||||
| { | { | ||||
| Equal, | |||||
| NotEqual, | |||||
| LessThan, | |||||
| LessThanOrEqual, | |||||
| GreaterThan, | |||||
| GreaterThanOrEqual | |||||
| Equal, // 等于 | |||||
| NotEqual, // 不等于 | |||||
| LessThan, // 小于 | |||||
| LessThanOrEqual, // 小于或等于 | |||||
| GreaterThan, // 大于 | |||||
| GreaterThanOrEqual // 大于或等于 | |||||
| }; | }; | ||||
| // 字操作数可以来自常量,也可以来自 D 寄存器 | // 字操作数可以来自常量,也可以来自 D 寄存器 | ||||
| enum class WordOperandKind | enum class WordOperandKind | ||||
| { | { | ||||
| Constant, | |||||
| Register | |||||
| Constant, // 使用固定数值 | |||||
| Register // 使用 D 寄存器中的数值 | |||||
| }; | }; | ||||
| // MOVE、ADD/SUB、计数器预置值共用的字操作数 | // MOVE、ADD/SUB、计数器预置值共用的字操作数 | ||||
| struct WordOperand | struct WordOperand | ||||
| { | { | ||||
| WordOperandKind kind = WordOperandKind::Constant; | |||||
| RegisterAddress address{RegisterArea::D, 0}; | |||||
| std::int16_t constant = 0; | |||||
| WordOperandKind kind = WordOperandKind::Constant; // 操作数来源 | |||||
| RegisterAddress address{RegisterArea::D, 0}; // kind 为 Register 时使用的 D 地址 | |||||
| std::int16_t constant = 0; // kind 为 Constant 时使用的数值 | |||||
| // 常量始终有效;寄存器操作数必须是有效 D 地址 | // 常量始终有效;寄存器操作数必须是有效 D 地址 | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| // 普通 M 寄存器触点,地址 + 常开常闭 | |||||
| struct ContactNodeConfig | struct ContactNodeConfig | ||||
| { | { | ||||
| RegisterAddress address{RegisterArea::M, 0}; | |||||
| ContactMode mode = ContactMode::NormallyOpen; | |||||
| RegisterAddress address{RegisterArea::M, 0}; // 触点读取的 M 地址 | |||||
| ContactMode mode = ContactMode::NormallyOpen; // 常开或常闭 | |||||
| }; | }; | ||||
| // 边沿触点,地址 + 上升 / 下降沿 | |||||
| struct EdgeContactNodeConfig | struct EdgeContactNodeConfig | ||||
| { | { | ||||
| // 用于检测 M 寄存器的上升沿或下降沿 | |||||
| RegisterAddress address{RegisterArea::M, 0}; | RegisterAddress address{RegisterArea::M, 0}; | ||||
| EdgeMode mode = EdgeMode::Rising; | EdgeMode mode = EdgeMode::Rising; | ||||
| }; | }; | ||||
| // 线圈输出配置,决定如何写入 M 寄存器 | |||||
| struct CoilNodeConfig | struct CoilNodeConfig | ||||
| { | { | ||||
| // 线圈要写入的 M 寄存器地址 | |||||
| RegisterAddress address{RegisterArea::M, 0}; | RegisterAddress address{RegisterArea::M, 0}; | ||||
| CoilMode mode = CoilMode::Normal; | CoilMode mode = CoilMode::Normal; | ||||
| }; | }; | ||||
| // D 寄存器比较配置 | |||||
| struct CompareNodeConfig | struct CompareNodeConfig | ||||
| { | { | ||||
| // 要参与比较的 D 寄存器地址 | |||||
| RegisterAddress address{RegisterArea::D, 0}; | RegisterAddress address{RegisterArea::D, 0}; | ||||
| // 比较运算符 | |||||
| ComparisonOperator comparison = ComparisonOperator::Equal; | ComparisonOperator comparison = ComparisonOperator::Equal; | ||||
| // 与寄存器值比较的常量 | |||||
| std::int16_t value = 0; | std::int16_t value = 0; | ||||
| }; | }; | ||||
| // 离线执行器内部使用的 T 区地址,不会加入 PLC M/D 轮询 | |||||
| // 定时器 T 编号,内部使用,不映射 M/D 寄存器;限制编号范围,判断是否合法,可转字符串 | |||||
| class TimerAddress | class TimerAddress | ||||
| { | { | ||||
| public: | public: | ||||
| static constexpr int kMinimumIndex = 0; | |||||
| static constexpr int kMaximumIndex = 4000; | |||||
| static constexpr int kMinimumIndex = 0; // 允许使用的最小 T 编号 | |||||
| static constexpr int kMaximumIndex = 4000; // 允许使用的最大 T 编号 | |||||
| explicit TimerAddress(int index = 0); | |||||
| explicit TimerAddress(int index = 0); // 使用编号创建 T 地址 | |||||
| int index() const; | |||||
| bool isValid() const; | |||||
| std::string toString() const; | |||||
| int index() const; // 返回地址编号 | |||||
| bool isValid() const; // 判断地址编号是否在有效范围内 | |||||
| std::string toString() const; // 转成 T0 这样的显示文本 | |||||
| bool operator==(const TimerAddress &other) const; | |||||
| bool operator!=(const TimerAddress &other) const; | |||||
| bool operator==(const TimerAddress &other) const; // 判断两个 T 地址是否相同 | |||||
| bool operator!=(const TimerAddress &other) const; // 判断两个 T 地址是否不同 | |||||
| private: | private: | ||||
| int index_ = 0; | |||||
| int index_ = 0; // 保存 T 地址编号 | |||||
| }; | }; | ||||
| // 读取 TON 完成状态的条件触点 | // 读取 TON 完成状态的条件触点 | ||||
| struct TimerContactNodeConfig | struct TimerContactNodeConfig | ||||
| { | { | ||||
| // 要读取完成状态的定时器地址 | |||||
| TimerAddress address; | TimerAddress address; | ||||
| // 按常开或常闭方式判断完成状态 | |||||
| ContactMode mode = ContactMode::NormallyOpen; | ContactMode mode = ContactMode::NormallyOpen; | ||||
| }; | }; | ||||
| @@ -116,32 +127,34 @@ struct TimerContactNodeConfig | |||||
| class CounterAddress | class CounterAddress | ||||
| { | { | ||||
| public: | public: | ||||
| static constexpr int kMinimumIndex = 0; | |||||
| static constexpr int kMaximumIndex = 4000; | |||||
| static constexpr int kMinimumIndex = 0; // 允许使用的最小 C 编号 | |||||
| static constexpr int kMaximumIndex = 4000; // 允许使用的最大 C 编号 | |||||
| explicit CounterAddress(int index = 0); | |||||
| explicit CounterAddress(int index = 0); // 使用编号创建 C 地址 | |||||
| int index() const; | |||||
| bool isValid() const; | |||||
| std::string toString() const; | |||||
| int index() const; // 返回地址编号 | |||||
| bool isValid() const; // 判断地址编号是否在有效范围内 | |||||
| std::string toString() const; // 转成 C0 这样的显示文本 | |||||
| bool operator==(const CounterAddress &other) const; | |||||
| bool operator!=(const CounterAddress &other) const; | |||||
| bool operator==(const CounterAddress &other) const; // 判断两个 C 地址是否相同 | |||||
| bool operator!=(const CounterAddress &other) const; // 判断两个 C 地址是否不同 | |||||
| private: | private: | ||||
| int index_ = 0; | |||||
| int index_ = 0; // 保存 C 地址编号 | |||||
| }; | }; | ||||
| enum class CounterMode | enum class CounterMode | ||||
| { | { | ||||
| Up, | |||||
| Down | |||||
| Up, // 加计数 | |||||
| Down // 减计数 | |||||
| }; | }; | ||||
| // 读取计数器完成状态的条件触点 | // 读取计数器完成状态的条件触点 | ||||
| struct CounterContactNodeConfig | struct CounterContactNodeConfig | ||||
| { | { | ||||
| // 要读取完成状态的计数器地址 | |||||
| CounterAddress address; | CounterAddress address; | ||||
| // 按常开或常闭方式判断完成状态 | |||||
| ContactMode mode = ContactMode::NormallyOpen; | ContactMode mode = ContactMode::NormallyOpen; | ||||
| }; | }; | ||||
| @@ -151,54 +164,54 @@ struct TonNodeConfig | |||||
| static constexpr int kMinimumPresetMs = 1; | static constexpr int kMinimumPresetMs = 1; | ||||
| static constexpr int kMaximumPresetMs = 86400000; | static constexpr int kMaximumPresetMs = 86400000; | ||||
| TimerAddress address; | |||||
| int presetMs = 1000; | |||||
| TimerAddress address; // 定时器地址 | |||||
| int presetMs = 1000; // 延时时间,单位为毫秒 | |||||
| }; | }; | ||||
| // CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D | // CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D | ||||
| struct CounterNodeConfig | struct CounterNodeConfig | ||||
| { | { | ||||
| CounterAddress address; | |||||
| CounterMode mode = CounterMode::Up; | |||||
| RegisterAddress currentValueAddress{RegisterArea::D, 0}; | |||||
| WordOperand preset; | |||||
| RegisterAddress resetAddress{RegisterArea::M, 0}; | |||||
| 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 | ||||
| { | { | ||||
| WordOperand source; | |||||
| RegisterAddress destination{RegisterArea::D, 0}; | |||||
| WordOperand source; // 要写入的源操作数 | |||||
| RegisterAddress destination{RegisterArea::D, 0}; // 接收数据的 D 地址 | |||||
| }; | }; | ||||
| enum class ArithmeticOperation | enum class ArithmeticOperation | ||||
| { | { | ||||
| Add, | |||||
| Subtract | |||||
| Add, // 加法 | |||||
| Subtract // 减法 | |||||
| }; | }; | ||||
| // ADD/SUB 输出:计算两个字操作数并写入目标 D 地址 | // ADD/SUB 输出:计算两个字操作数并写入目标 D 地址 | ||||
| struct ArithmeticNodeConfig | struct ArithmeticNodeConfig | ||||
| { | { | ||||
| ArithmeticOperation operation = ArithmeticOperation::Add; | |||||
| WordOperand left; | |||||
| WordOperand right; | |||||
| RegisterAddress destination{RegisterArea::D, 0}; | |||||
| ArithmeticOperation operation = ArithmeticOperation::Add; // 加法或减法 | |||||
| WordOperand left; // 左操作数 | |||||
| WordOperand right; // 右操作数 | |||||
| RegisterAddress destination{RegisterArea::D, 0}; // 运算结果写入的 D 地址 | |||||
| }; | }; | ||||
| // 所有梯形图指令的强类型配置联合,避免用无关字段拼装指令 | // 所有梯形图指令的强类型配置联合,避免用无关字段拼装指令 | ||||
| using LogicNodeConfig = std::variant< | using LogicNodeConfig = std::variant< | ||||
| ContactNodeConfig, | |||||
| EdgeContactNodeConfig, | |||||
| TimerContactNodeConfig, | |||||
| CounterContactNodeConfig, | |||||
| CoilNodeConfig, | |||||
| CompareNodeConfig, | |||||
| TonNodeConfig, | |||||
| CounterNodeConfig, | |||||
| MoveNodeConfig, | |||||
| ArithmeticNodeConfig>; | |||||
| ContactNodeConfig, // 普通常开或常闭触点 | |||||
| EdgeContactNodeConfig, // 上升沿或下降沿触点 | |||||
| TimerContactNodeConfig, // 定时器完成触点 | |||||
| CounterContactNodeConfig, // 计数器完成触点 | |||||
| CoilNodeConfig, // 普通、置位或复位线圈 | |||||
| CompareNodeConfig, // D 寄存器比较节点 | |||||
| TonNodeConfig, // TON 定时器输出 | |||||
| CounterNodeConfig, // CTU 或 CTD 计数器输出 | |||||
| MoveNodeConfig, // MOVE 数据传送输出 | |||||
| ArithmeticNodeConfig>; // ADD 或 SUB 算术输出 | |||||
| // 返回节点最主要的 M/D 地址;复合输出请使用下面的收集函数 | // 返回节点最主要的 M/D 地址;复合输出请使用下面的收集函数 | ||||
| std::optional<RegisterAddress> registerAddressForLogicNode( | std::optional<RegisterAddress> registerAddressForLogicNode( | ||||
| @@ -211,9 +224,9 @@ void collectRegisterAddressesForLogicNode( | |||||
| // 表达式中的一个指令节点;configured=false 表示编辑中的未完成草稿 | // 表达式中的一个指令节点;configured=false 表示编辑中的未完成草稿 | ||||
| struct LogicNode | struct LogicNode | ||||
| { | { | ||||
| std::string id; | |||||
| LogicNodeConfig config; | |||||
| bool configured = true; | |||||
| std::string id; // 节点唯一 ID | |||||
| LogicNodeConfig config; // 节点的具体指令配置 | |||||
| bool configured = true; // 是否已完成配置 | |||||
| // 校验节点 ID、配置类型和配置内容 | // 校验节点 ID、配置类型和配置内容 | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| @@ -225,10 +238,10 @@ struct LogicNode | |||||
| enum class ConditionExpressionKind | enum class ConditionExpressionKind | ||||
| { | { | ||||
| Node, | |||||
| Wire, | |||||
| Series, | |||||
| Parallel | |||||
| Node, // 一个实际的条件节点 | |||||
| Wire, // 一段横线 | |||||
| Series, // 多个条件串联,必须全部满足 | |||||
| Parallel // 多个条件并联,满足任意一条即可 | |||||
| }; | }; | ||||
| // 条件区中的持久化横线;columnSpan 表示跨越的网格列数 | // 条件区中的持久化横线;columnSpan 表示跨越的网格列数 | ||||
| @@ -238,7 +251,7 @@ struct WireSegment | |||||
| static constexpr int kMaximumColumnSpan = | static constexpr int kMaximumColumnSpan = | ||||
| ProjectLimits::kMaximumConditionColumns; | ProjectLimits::kMaximumConditionColumns; | ||||
| int columnSpan = 1; | |||||
| int columnSpan = 1; // 横线占用的网格列数 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| @@ -246,13 +259,15 @@ struct WireSegment | |||||
| // 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接 | // 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接 | ||||
| struct ConditionExpression | struct ConditionExpression | ||||
| { | { | ||||
| std::string id; | |||||
| ConditionExpressionKind kind = ConditionExpressionKind::Node; | |||||
| std::optional<LogicNode> node; | |||||
| std::optional<WireSegment> wire; | |||||
| std::vector<ConditionExpression> children; | |||||
| std::string id; // 表达式唯一 ID | |||||
| ConditionExpressionKind kind = ConditionExpressionKind::Node; // 表达式类型 | |||||
| std::optional<LogicNode> node; // kind 为 Node 时保存的节点 | |||||
| std::optional<WireSegment> wire; // kind 为 Wire 时保存的横线 | |||||
| std::vector<ConditionExpression> children; // 串联或并联的子表达式 | |||||
| // 把一个逻辑节点包装成条件表达式 | |||||
| static ConditionExpression fromNode(LogicNode node); | static ConditionExpression fromNode(LogicNode node); | ||||
| // 创建一段指定列数的横线表达式 | |||||
| static ConditionExpression fromWire(std::string id, int column_span = 1); | static ConditionExpression fromWire(std::string id, int column_span = 1); | ||||
| // 编辑态校验允许空配置节点,但必须保持树结构合法 | // 编辑态校验允许空配置节点,但必须保持树结构合法 | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| @@ -260,29 +275,41 @@ struct ConditionExpression | |||||
| bool validateForRunning(std::string *error = nullptr) const; | bool validateForRunning(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| // 删除后折叠单子项容器并合并相邻同类容器,保持表达式的规范形态 | |||||
| // 删除节点后整理表达式结构 | |||||
| void normalizeConditionExpression(std::optional<ConditionExpression> *expression); | void normalizeConditionExpression(std::optional<ConditionExpression> *expression); | ||||
| // 按节点 ID 查找条件节点,只读版本 | |||||
| const LogicNode *findConditionNode( | const LogicNode *findConditionNode( | ||||
| const ConditionExpression &expression, const std::string &node_id); | const ConditionExpression &expression, const std::string &node_id); | ||||
| // 按节点 ID 查找条件节点,可修改版本 | |||||
| LogicNode *findConditionNode( | LogicNode *findConditionNode( | ||||
| ConditionExpression &expression, const std::string &node_id); | ConditionExpression &expression, const std::string &node_id); | ||||
| // 按表达式 ID 查找子表达式,只读版本 | |||||
| const ConditionExpression *findConditionExpression( | const ConditionExpression *findConditionExpression( | ||||
| const ConditionExpression &expression, const std::string &expression_id); | const ConditionExpression &expression, const std::string &expression_id); | ||||
| // 按表达式 ID 查找子表达式,可修改版本 | |||||
| ConditionExpression *findConditionExpression( | ConditionExpression *findConditionExpression( | ||||
| ConditionExpression &expression, const std::string &expression_id); | ConditionExpression &expression, const std::string &expression_id); | ||||
| // 收集表达式中的所有条件节点 | |||||
| void collectConditionNodes( | void collectConditionNodes( | ||||
| const ConditionExpression &expression, std::vector<const LogicNode *> *nodes); | const ConditionExpression &expression, std::vector<const LogicNode *> *nodes); | ||||
| // 收集表达式和子表达式的所有 ID | |||||
| void collectConditionExpressionIds( | void collectConditionExpressionIds( | ||||
| const ConditionExpression &expression, std::vector<std::string> *ids); | const ConditionExpression &expression, std::vector<std::string> *ids); | ||||
| // 条件表达式为空时表示恒真网络,输出指令固定在最右侧 | // 条件表达式为空时表示恒真网络,输出指令固定在最右侧 | ||||
| struct LadderRung | struct LadderRung | ||||
| { | { | ||||
| std::string id; | |||||
| std::string name; | |||||
| std::string comment; | |||||
| std::optional<ConditionExpression> condition; | |||||
| std::optional<LogicNode> output; | |||||
| std::string id; // 网络唯一 ID | |||||
| std::string name; // 网络名称 | |||||
| std::string comment; // 网络注释 | |||||
| std::optional<ConditionExpression> condition; // 条件表达式,没有时表示恒真 | |||||
| std::optional<LogicNode> output; // 网络右侧的输出指令 | |||||
| // 编辑态允许没有条件或输出,便于逐步搭建网络 | // 编辑态允许没有条件或输出,便于逐步搭建网络 | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| @@ -295,10 +322,10 @@ struct LadderRung | |||||
| // 一组按顺序扫描的梯形图网络;enabled=false 时运行校验会跳过它 | // 一组按顺序扫描的梯形图网络;enabled=false 时运行校验会跳过它 | ||||
| struct ControlLogic | struct ControlLogic | ||||
| { | { | ||||
| std::string id; | |||||
| std::string name; | |||||
| std::vector<LadderRung> rungs; | |||||
| bool enabled = true; | |||||
| std::string id; // 控制逻辑唯一 ID | |||||
| std::string name; // 控制逻辑名称 | |||||
| std::vector<LadderRung> rungs; // 很多条 LadderRung(梯形图网络/梯级) | |||||
| bool enabled = true; // 是否参与运行扫描 | |||||
| // 校验可保存结构 | // 校验可保存结构 | ||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| @@ -17,9 +17,9 @@ | |||||
| */ | */ | ||||
| enum class HmiBindingKind | enum class HmiBindingKind | ||||
| { | { | ||||
| None, | |||||
| Bit, | |||||
| Word | |||||
| None, // 不支持寄存器绑定 | |||||
| Bit, // 绑定 M 区位地址 | |||||
| Word // 绑定 D 区字地址 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -27,9 +27,9 @@ enum class HmiBindingKind | |||||
| */ | */ | ||||
| enum class HmiRuntimeValueKind | enum class HmiRuntimeValueKind | ||||
| { | { | ||||
| None, | |||||
| Bit, | |||||
| Word | |||||
| None, // 运行态不读取寄存器值 | |||||
| Bit, // 运行态读取绑定的 M 位值 | |||||
| Word // 运行态读取绑定的 D 字值 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -37,15 +37,15 @@ enum class HmiRuntimeValueKind | |||||
| */ | */ | ||||
| struct HmiControlDescriptor | struct HmiControlDescriptor | ||||
| { | { | ||||
| HmiControlType type; | |||||
| const char *storageName; | |||||
| const char *displayName; | |||||
| const char *idPrefix; | |||||
| const char *defaultText; | |||||
| HmiRect defaultBounds; | |||||
| HmiBindingKind bindingKind; | |||||
| HmiRuntimeValueKind runtimeValueKind; | |||||
| bool requiresBindingForRunning; | |||||
| HmiControlType type; // 控件的领域类型 | |||||
| const char *storageName; // 工程 JSON 中持久化的稳定类型名 | |||||
| const char *displayName; // 编辑器和属性面板使用的显示名称 | |||||
| const char *idPrefix; // 自动生成控件 ID 时使用的前缀 | |||||
| const char *defaultText; // 新建控件时使用的默认显示文本 | |||||
| HmiRect defaultBounds; // 新建控件时使用的默认位置和尺寸 | |||||
| HmiBindingKind bindingKind; // 编辑态允许的寄存器绑定类型 | |||||
| HmiRuntimeValueKind runtimeValueKind; // 运行态读取的寄存器值类型 | |||||
| bool requiresBindingForRunning; // 运行前是否必须完成有效寄存器绑定 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -5,62 +5,62 @@ | |||||
| // 所有跨层共享的数量和范围边界集中在这里,避免 UI、服务和 JSON 各自写数字 | // 所有跨层共享的数量和范围边界集中在这里,避免 UI、服务和 JSON 各自写数字 | ||||
| namespace ProjectLimits { | namespace ProjectLimits { | ||||
| constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U; | |||||
| constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U; // 一个工程文件最大 16 MiB,防止异常大文件占满内存 | |||||
| constexpr std::size_t kMaximumHmiPages = 128U; | |||||
| constexpr std::size_t kMaximumHmiControlsPerPage = 512U; | |||||
| constexpr std::size_t kMaximumAlarmDefinitions = 512U; | |||||
| constexpr std::size_t kMaximumRegisterComments = 8002U; | |||||
| constexpr std::size_t kMaximumControlLogics = 128U; | |||||
| constexpr std::size_t kMaximumRungsPerLogic = 1024U; | |||||
| constexpr std::size_t kMaximumHmiPages = 128U; // 一个工程最多放 128 个 HMI 页面 | |||||
| constexpr std::size_t kMaximumHmiControlsPerPage = 512U; // 一个 HMI 页面最多放 512 个控件 | |||||
| constexpr std::size_t kMaximumAlarmDefinitions = 512U; // 一个工程最多配置 512 条报警 | |||||
| constexpr std::size_t kMaximumRegisterComments = 8002U; // M0~M4000 和 D0~D4000 最多各写一条注释 | |||||
| constexpr std::size_t kMaximumControlLogics = 128U; // 一个工程最多放 128 组梯形图控制逻辑 | |||||
| constexpr std::size_t kMaximumRungsPerLogic = 1024U; // 一组控制逻辑最多放 1024 个网络 | |||||
| constexpr std::size_t kMaximumExpressionNodesPerRung = 4096U; | |||||
| constexpr std::size_t kMaximumExpressionDepth = 20U; | |||||
| constexpr std::size_t kMaximumExpressionChildren = 256U; | |||||
| constexpr int kMaximumConditionColumns = 10; | |||||
| constexpr int kMaximumLadderColumns = 11; | |||||
| constexpr int kMaximumLogicRows = 256; | |||||
| constexpr std::size_t kMaximumExpressionNodesPerRung = 4096U; // 一个网络里的触点、横线和内部结构最多共 4096 个 | |||||
| constexpr std::size_t kMaximumExpressionDepth = 20U; // 并联套并联时最多套 20 层,防止结构无限变复杂 | |||||
| constexpr std::size_t kMaximumExpressionChildren = 256U; // 一个串联组或并联组最多放 256 个子项 | |||||
| constexpr int kMaximumConditionColumns = 10; // 一个网络前面的条件区最多 10 列 | |||||
| constexpr int kMaximumLadderColumns = 11; // 一个网络总共 11 列,最后 1 列专门放输出 | |||||
| constexpr int kMaximumLogicRows = 256; // 一个网络上下最多 256 行并联支路 | |||||
| static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1); | |||||
| static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1); // 确保总列数始终等于条件列加输出列 | |||||
| constexpr std::size_t kMaximumHmiProperties = 64U; | |||||
| constexpr std::size_t kMaximumIdBytes = 128U; | |||||
| constexpr std::size_t kMaximumTextBytes = 4096U; | |||||
| constexpr std::size_t kMaximumHmiControlTextCharacters = 12U; | |||||
| constexpr std::size_t kMaximumAlarmTitleCharacters = 12U; | |||||
| constexpr std::size_t kMaximumAlarmMessageCharacters = 20U; | |||||
| constexpr std::size_t kMaximumPropertyKeyBytes = 128U; | |||||
| constexpr std::size_t kMaximumPropertyValueBytes = 4096U; | |||||
| constexpr std::size_t kMaximumRegisterCommentBytes = 64U; | |||||
| constexpr std::size_t kMaximumRungCommentBytes = 128U; | |||||
| constexpr std::size_t kMaximumHmiProperties = 64U; // 一个 HMI 控件最多保存 64 对扩展属性 | |||||
| constexpr std::size_t kMaximumIdBytes = 128U; // ID 最多 128 个 UTF-8 字节,够用来做稳定标识 | |||||
| constexpr std::size_t kMaximumTextBytes = 4096U; // 工程名称等普通文本最多 4096 个 UTF-8 字节 | |||||
| constexpr std::size_t kMaximumHmiControlTextCharacters = 12U; // HMI 控件显示文字最多输入 12 个字符 | |||||
| constexpr std::size_t kMaximumAlarmTitleCharacters = 12U; // 报警列表标题最多输入 12 个字符 | |||||
| constexpr std::size_t kMaximumAlarmMessageCharacters = 20U; // 一条报警内容最多输入 20 个字符 | |||||
| constexpr std::size_t kMaximumPropertyKeyBytes = 128U; // HMI 属性名最多 128 个 UTF-8 字节 | |||||
| constexpr std::size_t kMaximumPropertyValueBytes = 4096U; // HMI 属性值最多 4096 个 UTF-8 字节 | |||||
| constexpr std::size_t kMaximumRegisterCommentBytes = 64U; // M/D 注释最多 64 个 UTF-8 字节,保证节点下方放得下 | |||||
| constexpr std::size_t kMaximumRungCommentBytes = 128U; // 网络注释最多 128 个 UTF-8 字节,并且保持单行显示 | |||||
| constexpr int kMinimumHmiPageWidth = 320; | |||||
| constexpr int kMaximumHmiPageWidth = 1600; | |||||
| constexpr int kMinimumHmiPageHeight = 200; | |||||
| constexpr int kMaximumHmiPageHeight = 800; | |||||
| constexpr int kDefaultHmiPageWidth = 800; | |||||
| constexpr int kDefaultHmiPageHeight = 400; | |||||
| constexpr int kMaximumHmiControlWidth = 8192; | |||||
| constexpr int kMaximumHmiControlHeight = 8192; | |||||
| constexpr int kMinimumHmiFontPointSize = 6; | |||||
| constexpr int kMaximumHmiFontPointSize = 72; | |||||
| constexpr int kAlarmDefaultFontPointReduction = 2; | |||||
| constexpr int kMaximumVisibleAlarmRows = 5; | |||||
| constexpr int kMinimumHmiPageWidth = 320; // HMI 页面最窄 320 像素,太窄会不好编辑 | |||||
| constexpr int kMaximumHmiPageWidth = 1600; // HMI 页面最宽 1600 像素,避免画布无限变大 | |||||
| constexpr int kMinimumHmiPageHeight = 200; // HMI 页面最矮 200 像素,保证基本可用空间 | |||||
| constexpr int kMaximumHmiPageHeight = 800; // HMI 页面最高 800 像素,适合当前桌面编辑器 | |||||
| constexpr int kDefaultHmiPageWidth = 800; // 新建 HMI 页面默认宽度 800 像素 | |||||
| constexpr int kDefaultHmiPageHeight = 400; // 新建 HMI 页面默认高度 400 像素 | |||||
| constexpr int kMaximumHmiControlWidth = 8192; // 单个 HMI 控件最大宽度,防止异常尺寸撑坏画布 | |||||
| constexpr int kMaximumHmiControlHeight = 8192; // 单个 HMI 控件最大高度,防止异常尺寸撑坏画布 | |||||
| constexpr int kMinimumHmiFontPointSize = 6; // HMI 字体最小 6 磅,太小基本看不清 | |||||
| constexpr int kMaximumHmiFontPointSize = 72; // HMI 字体最大 72 磅,防止文字撑坏布局 | |||||
| constexpr int kAlarmDefaultFontPointReduction = 2; // 报警列表默认比普通文字小 2 磅,给表头留空间 | |||||
| constexpr int kMaximumVisibleAlarmRows = 5; // 报警列表默认最多同时显示 5 行,更多内容通过翻页查看 | |||||
| constexpr std::size_t kMaximumPollAddresses = 1024U; | |||||
| constexpr std::size_t kMaximumPollBlocks = 64U; | |||||
| constexpr int kMaximumModbusReadCount = 120; | |||||
| constexpr std::size_t kMaximumPollAddresses = 1024U; // PLC 一轮轮询最多读取 1024 个去重后的地址 | |||||
| constexpr std::size_t kMaximumPollBlocks = 64U; // PLC 一轮轮询最多拆成 64 个读块 | |||||
| constexpr int kMaximumModbusReadCount = 120; // 一条 Modbus 读指令最多读 120 个数据项 | |||||
| constexpr int kMinimumPlcServerAddress = 1; | |||||
| constexpr int kMaximumPlcServerAddress = 247; | |||||
| constexpr int kMinimumResponseTimeoutMs = 100; | |||||
| constexpr int kMaximumResponseTimeoutMs = 30000; | |||||
| constexpr int kMinimumRetries = 0; | |||||
| constexpr int kMaximumRetries = 5; | |||||
| constexpr int kMinimumPollIntervalMs = 50; | |||||
| constexpr int kMaximumPollIntervalMs = 10000; | |||||
| constexpr int kMinimumPlcServerAddress = 1; // Modbus 站号从 1 开始 | |||||
| constexpr int kMaximumPlcServerAddress = 247; // 站号最大 247,避开 Modbus 保留站号 | |||||
| constexpr int kMinimumResponseTimeoutMs = 100; // PLC 最短等待回复 100 毫秒,避免过早判定超时 | |||||
| constexpr int kMaximumResponseTimeoutMs = 30000; // PLC 最长等待回复 30 秒,避免故障时一直卡住 | |||||
| constexpr int kMinimumRetries = 0; // 最少可以不重试,收到失败就直接返回 | |||||
| constexpr int kMaximumRetries = 5; // PLC 通信最多自动重试 5 次 | |||||
| constexpr int kMinimumPollIntervalMs = 50; // PLC 最快每 50 毫秒轮询一次,避免压满串口 | |||||
| constexpr int kMaximumPollIntervalMs = 10000; // PLC 最慢每 10 秒轮询一次,避免画面长时间不刷新 | |||||
| constexpr std::size_t kMaximumPendingWrites = 1U; | |||||
| constexpr int kMaximumOutputMessages = 1000; | |||||
| constexpr std::size_t kMaximumPendingWrites = 1U; // 同时只允许等待 1 个 PLC 写入,避免写入顺序混乱 | |||||
| constexpr int kMaximumOutputMessages = 1000; // 输出日志最多保留 1000 条,超过后删除最旧的一条 | |||||
| } // namespace ProjectLimits | |||||
| } | |||||
| @@ -15,26 +15,31 @@ | |||||
| namespace { | namespace { | ||||
| // 故障恢复时,两次轻量探测之间等待的时间 | |||||
| constexpr int kRecoveryProbeIntervalMs = 2000; | constexpr int kRecoveryProbeIntervalMs = 2000; | ||||
| // Qt 使用 QString,项目内部错误信息统一使用 UTF-8 std::string | |||||
| std::string toUtf8(const QString &value) | std::string toUtf8(const QString &value) | ||||
| { | { | ||||
| const QByteArray bytes = value.toUtf8(); | const QByteArray bytes = value.toUtf8(); | ||||
| return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | ||||
| } | } | ||||
| // M 区在 Modbus 中按线圈读取,D 区按保持寄存器读取 | |||||
| QModbusDataUnit::RegisterType registerType(RegisterArea area) | QModbusDataUnit::RegisterType registerType(RegisterArea area) | ||||
| { | { | ||||
| return area == RegisterArea::M | return area == RegisterArea::M | ||||
| ? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters; | ? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters; | ||||
| } | } | ||||
| // 只有超时和 PLC 不响应属于可以自动探测恢复的故障 | |||||
| bool isRecoverableTimeout(PlcCommunicationError error) | bool isRecoverableTimeout(PlcCommunicationError error) | ||||
| { | { | ||||
| return error == PlcCommunicationError::PlcNotResponding | return error == PlcCommunicationError::PlcNotResponding | ||||
| || error == PlcCommunicationError::CommunicationTimeout; | || error == PlcCommunicationError::CommunicationTimeout; | ||||
| } | } | ||||
| // Connected 正常轮询,Recovering 正在恢复后的重新首读 | |||||
| bool isReadingState(PlcConnectionState state) | bool isReadingState(PlcConnectionState state) | ||||
| { | { | ||||
| return state == PlcConnectionState::Connected | return state == PlcConnectionState::Connected | ||||
| @@ -46,6 +51,7 @@ bool normalizePollAddresses( | |||||
| std::vector<RegisterAddress> *normalized, | std::vector<RegisterAddress> *normalized, | ||||
| std::string *error) | std::string *error) | ||||
| { | { | ||||
| // 先复制,后续排序和去重不能改变调用方传入的地址列表 | |||||
| *normalized = addresses; | *normalized = addresses; | ||||
| if (std::any_of( | if (std::any_of( | ||||
| normalized->cbegin(), normalized->cend(), | normalized->cbegin(), normalized->cend(), | ||||
| @@ -76,6 +82,7 @@ std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses) | |||||
| { | { | ||||
| if (addresses.empty()) | if (addresses.empty()) | ||||
| { | { | ||||
| // 空集合也会保留 M0、D0 两个默认读块 | |||||
| return 2U; | return 2U; | ||||
| } | } | ||||
| std::size_t blocks = 0U; | std::size_t blocks = 0U; | ||||
| @@ -84,6 +91,7 @@ std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses) | |||||
| int count = 0; | int count = 0; | ||||
| for (const RegisterAddress &address : addresses) | for (const RegisterAddress &address : addresses) | ||||
| { | { | ||||
| // 跨区域、出现地址间隔或达到 Modbus 单次上限时,必须开始新读块 | |||||
| if (count == 0 | if (count == 0 | ||||
| || address.area() != current_area | || address.area() != current_area | ||||
| || address.index() > start_address + count | || address.index() > start_address + count | ||||
| @@ -111,6 +119,7 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| repository_(repository), | repository_(repository), | ||||
| master_(std::make_unique<QModbusRtuSerialMaster>()) | master_(std::make_unique<QModbusRtuSerialMaster>()) | ||||
| { | { | ||||
| // 仓库只负责缓存;写入请求通过这里注入的回调转给通信服务 | |||||
| repository_.setWriteHandlers( | repository_.setWriteHandlers( | ||||
| [this](const RegisterAddress &address, bool value) | [this](const RegisterAddress &address, bool value) | ||||
| { | { | ||||
| @@ -120,6 +129,7 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| { | { | ||||
| return sendWordWrite(address, value); | return sendWordWrite(address, value); | ||||
| }); | }); | ||||
| // 轮询定时器每次只推动一个读块,避免一次压入大量异步请求 | |||||
| connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock); | connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock); | ||||
| recovery_timer_.setSingleShot(true); | recovery_timer_.setSingleShot(true); | ||||
| connect( | connect( | ||||
| @@ -127,12 +137,14 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| &QTimer::timeout, | &QTimer::timeout, | ||||
| this, | this, | ||||
| &PlcCommunicationService::probeRecovery); | &PlcCommunicationService::probeRecovery); | ||||
| // Qt 串口状态变化是异步通知,所有连接状态都从这里统一处理 | |||||
| connect(master_.get(), &QModbusClient::stateChanged, | connect(master_.get(), &QModbusClient::stateChanged, | ||||
| this, | this, | ||||
| [this](QModbusDevice::State device_state) | [this](QModbusDevice::State device_state) | ||||
| { | { | ||||
| if (device_state == QModbusDevice::ConnectedState) | if (device_state == QModbusDevice::ConnectedState) | ||||
| { | { | ||||
| // 串口连接成功后立即开始轮询,并不代表首读已经完成 | |||||
| serial_session_opened_ = true; | serial_session_opened_ = true; | ||||
| setState(PlcConnectionState::Connected); | setState(PlcConnectionState::Connected); | ||||
| poll_timer_.start(configuration_.pollIntervalMs); | poll_timer_.start(configuration_.pollIntervalMs); | ||||
| @@ -145,6 +157,7 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| } | } | ||||
| else if (device_state == QModbusDevice::UnconnectedState) | else if (device_state == QModbusDevice::UnconnectedState) | ||||
| { | { | ||||
| // 主动断开和意外断线要区分,后者需要报告故障 | |||||
| if (disconnecting_) | if (disconnecting_) | ||||
| { | { | ||||
| serial_session_opened_ = false; | serial_session_opened_ = false; | ||||
| @@ -159,6 +172,7 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| } | } | ||||
| } | } | ||||
| }); | }); | ||||
| // Qt 报告底层错误时,转换为项目自己的错误和状态 | |||||
| connect(master_.get(), &QModbusClient::errorOccurred, | connect(master_.get(), &QModbusClient::errorOccurred, | ||||
| this, | this, | ||||
| [this](QModbusDevice::Error error) | [this](QModbusDevice::Error error) | ||||
| @@ -198,6 +212,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||||
| received_valid_response_ = false; | received_valid_response_ = false; | ||||
| last_error_type_ = PlcCommunicationError::None; | last_error_type_ = PlcCommunicationError::None; | ||||
| last_error_.clear(); | last_error_.clear(); | ||||
| // 把项目配置转换成 Qt 串口参数 | |||||
| master_->setConnectionParameter( | master_->setConnectionParameter( | ||||
| QModbusDevice::SerialPortNameParameter, | QModbusDevice::SerialPortNameParameter, | ||||
| QString::fromStdString(configuration.portName)); | QString::fromStdString(configuration.portName)); | ||||
| @@ -234,6 +249,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||||
| void PlcCommunicationService::disconnectDevice() | void PlcCommunicationService::disconnectDevice() | ||||
| { | { | ||||
| // 关闭串口、清除缓存有效标记,防止断开后继续使用旧值 | |||||
| closeSerialSession(); | closeSerialSession(); | ||||
| repository_.invalidate(); | repository_.invalidate(); | ||||
| updateInitialReadCompleted(false, true); | updateInitialReadCompleted(false, true); | ||||
| @@ -258,6 +274,7 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( | |||||
| // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复 | // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复 | ||||
| if (pending_reply_ != nullptr) | if (pending_reply_ != nullptr) | ||||
| { | { | ||||
| // 当前回复仍在解析旧集合,等它结束后再切换到新集合 | |||||
| pending_poll_addresses_ = std::move(normalized); | pending_poll_addresses_ = std::move(normalized); | ||||
| poll_update_pending_ = true; | poll_update_pending_ = true; | ||||
| return {true, {}}; | return {true, {}}; | ||||
| @@ -297,10 +314,11 @@ void PlcCommunicationService::setCallbacks( | |||||
| std::function<void()> cache_updated, | std::function<void()> cache_updated, | ||||
| std::function<void(const std::string &)> error_reported) | std::function<void(const std::string &)> error_reported) | ||||
| { | { | ||||
| state_changed_callback_ = std::move(state_changed); | |||||
| initial_read_changed_callback_ = std::move(initial_read_changed); | |||||
| cache_updated_callback_ = std::move(cache_updated); | |||||
| error_reported_callback_ = std::move(error_reported); | |||||
| // 保存外部通知函数,通信事件发生时再调用对应函数 | |||||
| state_changed_callback_ = std::move(state_changed); // 通信状态变化时通知外部 | |||||
| initial_read_changed_callback_ = std::move(initial_read_changed); // 首读资格变化时通知外部 | |||||
| cache_updated_callback_ = std::move(cache_updated); // PLC 缓存更新后通知外部 | |||||
| error_reported_callback_ = std::move(error_reported); // 发生通信错误时通知外部 | |||||
| } | } | ||||
| void PlcCommunicationService::rebuildPollBlocks() | void PlcCommunicationService::rebuildPollBlocks() | ||||
| @@ -308,6 +326,7 @@ void PlcCommunicationService::rebuildPollBlocks() | |||||
| std::vector<RegisterAddress> addresses = poll_addresses_; | std::vector<RegisterAddress> addresses = poll_addresses_; | ||||
| if (addresses.empty()) | if (addresses.empty()) | ||||
| { | { | ||||
| // 没有指定地址时保留 M0/D0,保证连接后仍能验证设备是否响应 | |||||
| addresses = { | addresses = { | ||||
| RegisterAddress{RegisterArea::M, 0}, | RegisterAddress{RegisterArea::M, 0}, | ||||
| RegisterAddress{RegisterArea::D, 0}}; | RegisterAddress{RegisterArea::D, 0}}; | ||||
| @@ -349,6 +368,7 @@ void PlcCommunicationService::rebuildPollBlocks() | |||||
| next_poll_block_ = 0; | next_poll_block_ = 0; | ||||
| if (!initial_read_completed_) | if (!initial_read_completed_) | ||||
| { | { | ||||
| // 地址集合变化或重新连接后,首读资格从头统计 | |||||
| initial_blocks_read_.assign(poll_blocks_.size(), false); | initial_blocks_read_.assign(poll_blocks_.size(), false); | ||||
| } | } | ||||
| else | else | ||||
| @@ -360,6 +380,7 @@ void PlcCommunicationService::rebuildPollBlocks() | |||||
| void PlcCommunicationService::applyPollAddresses( | void PlcCommunicationService::applyPollAddresses( | ||||
| const std::vector<RegisterAddress> &addresses) | const std::vector<RegisterAddress> &addresses) | ||||
| { | { | ||||
| // 只有在没有读请求占用时,新的地址集合才会真正生效 | |||||
| poll_addresses_ = addresses; | poll_addresses_ = addresses; | ||||
| rebuildPollBlocks(); | rebuildPollBlocks(); | ||||
| poll_update_pending_ = false; | poll_update_pending_ = false; | ||||
| @@ -390,6 +411,7 @@ void PlcCommunicationService::pollNextBlock() | |||||
| return; | return; | ||||
| } | } | ||||
| pending_reply_ = reply; | pending_reply_ = reply; | ||||
| // 把当前连接代次带进回调,防止断线重连后旧回复误更新新连接 | |||||
| const std::uint64_t generation = connection_generation_; | const std::uint64_t generation = connection_generation_; | ||||
| connect(reply, &QModbusReply::finished, | connect(reply, &QModbusReply::finished, | ||||
| this, | this, | ||||
| @@ -424,6 +446,7 @@ void PlcCommunicationService::pollNextBlock() | |||||
| } | } | ||||
| if (pending_reply_ == reply) | if (pending_reply_ == reply) | ||||
| { | { | ||||
| // 只有当前指针仍指向本次回复时才清空,避免误清新请求 | |||||
| pending_reply_ = nullptr; | pending_reply_ = nullptr; | ||||
| } | } | ||||
| reply->deleteLater(); | reply->deleteLater(); | ||||
| @@ -437,6 +460,7 @@ void PlcCommunicationService::pollNextBlock() | |||||
| void PlcCommunicationService::probeRecovery() | void PlcCommunicationService::probeRecovery() | ||||
| { | { | ||||
| // 只在可恢复的超时故障中探测;串口拔出等故障不会反复探测 | |||||
| if (state_ != PlcConnectionState::Faulted | if (state_ != PlcConnectionState::Faulted | ||||
| || !isRecoverableTimeout(last_error_type_)) | || !isRecoverableTimeout(last_error_type_)) | ||||
| { | { | ||||
| @@ -448,6 +472,7 @@ void PlcCommunicationService::probeRecovery() | |||||
| } | } | ||||
| if (pending_reply_ != nullptr) | if (pending_reply_ != nullptr) | ||||
| { | { | ||||
| // 已有读请求在途,稍后再探测,保证同一时间只有一个读回复 | |||||
| recovery_timer_.start(kRecoveryProbeIntervalMs); | recovery_timer_.start(kRecoveryProbeIntervalMs); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -455,6 +480,7 @@ void PlcCommunicationService::probeRecovery() | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| // 恢复探测只读取一个地址,确认链路恢复后再进行完整首读 | |||||
| const PollBlock block = poll_blocks_.front(); | const PollBlock block = poll_blocks_.front(); | ||||
| const QModbusDataUnit request( | const QModbusDataUnit request( | ||||
| registerType(block.area), block.startAddress, 1); | registerType(block.area), block.startAddress, 1); | ||||
| @@ -521,6 +547,7 @@ void PlcCommunicationService::handleRecoveryProbeFailure(QModbusDevice::Error er | |||||
| void PlcCommunicationService::restoreCommunication() | void PlcCommunicationService::restoreCommunication() | ||||
| { | { | ||||
| // 探测成功只说明链路恢复,仍要重新读取全部读块才能恢复真机资格 | |||||
| received_valid_response_ = true; | received_valid_response_ = true; | ||||
| last_error_type_ = PlcCommunicationError::None; | last_error_type_ = PlcCommunicationError::None; | ||||
| last_error_.clear(); | last_error_.clear(); | ||||
| @@ -568,6 +595,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock | |||||
| RegisterWriteResult PlcCommunicationService::sendBitWrite( | RegisterWriteResult PlcCommunicationService::sendBitWrite( | ||||
| const RegisterAddress &address, bool value) | const RegisterAddress &address, bool value) | ||||
| { | { | ||||
| // 写入只允许在正常 Connected 状态进行,Recovering/Faulted 都拒绝 | |||||
| if (state_ != PlcConnectionState::Connected) | if (state_ != PlcConnectionState::Connected) | ||||
| { | { | ||||
| return {false, RegisterError::Unavailable}; | return {false, RegisterError::Unavailable}; | ||||
| @@ -576,6 +604,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite( | |||||
| { | { | ||||
| return {false, RegisterError::WriteRejected}; | return {false, RegisterError::WriteRejected}; | ||||
| } | } | ||||
| // M 区对应 Modbus Coils,单次只写一个地址 | |||||
| QModbusDataUnit unit(QModbusDataUnit::Coils, address.index(), 1); | QModbusDataUnit unit(QModbusDataUnit::Coils, address.index(), 1); | ||||
| unit.setValue(0, value ? 1U : 0U); | unit.setValue(0, value ? 1U : 0U); | ||||
| QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | ||||
| @@ -585,6 +614,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite( | |||||
| return {false, RegisterError::WriteRejected}; | return {false, RegisterError::WriteRejected}; | ||||
| } | } | ||||
| pending_write_reply_ = reply; | pending_write_reply_ = reply; | ||||
| // 写回复完成后只处理成功或错误,不直接改缓存;后续轮询负责确认真实值 | |||||
| connect(reply, &QModbusReply::finished, | connect(reply, &QModbusReply::finished, | ||||
| this, | this, | ||||
| [this, reply, generation = connection_generation_] | [this, reply, generation = connection_generation_] | ||||
| @@ -610,6 +640,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite( | |||||
| RegisterWriteResult PlcCommunicationService::sendWordWrite( | RegisterWriteResult PlcCommunicationService::sendWordWrite( | ||||
| const RegisterAddress &address, std::int16_t value) | const RegisterAddress &address, std::int16_t value) | ||||
| { | { | ||||
| // D 区写入流程与 M 区相同,只是 Modbus 类型不同 | |||||
| if (state_ != PlcConnectionState::Connected) | if (state_ != PlcConnectionState::Connected) | ||||
| { | { | ||||
| return {false, RegisterError::Unavailable}; | return {false, RegisterError::Unavailable}; | ||||
| @@ -618,6 +649,7 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite( | |||||
| { | { | ||||
| return {false, RegisterError::WriteRejected}; | return {false, RegisterError::WriteRejected}; | ||||
| } | } | ||||
| // D 区对应 Modbus HoldingRegisters,单次只写一个字 | |||||
| QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 1); | QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 1); | ||||
| unit.setValue(0, static_cast<quint16>(value)); | unit.setValue(0, static_cast<quint16>(value)); | ||||
| QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | ||||
| @@ -656,6 +688,7 @@ void PlcCommunicationService::updateInitialReadCompleted( | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| // 只有状态真的变化,或调用方明确要求通知时才发出信号 | |||||
| initial_read_completed_ = completed; | initial_read_completed_ = completed; | ||||
| emit initialReadCompletedChanged(completed); | emit initialReadCompletedChanged(completed); | ||||
| if (initial_read_changed_callback_) | if (initial_read_changed_callback_) | ||||
| @@ -666,6 +699,7 @@ void PlcCommunicationService::updateInitialReadCompleted( | |||||
| void PlcCommunicationService::handleUnexpectedDisconnect() | void PlcCommunicationService::handleUnexpectedDisconnect() | ||||
| { | { | ||||
| // 这里表示设备原本连上过,后来串口意外断开 | |||||
| serial_session_opened_ = false; | serial_session_opened_ = false; | ||||
| const QString port_name = QString::fromStdString(configuration_.portName).trimmed(); | const QString port_name = QString::fromStdString(configuration_.portName).trimmed(); | ||||
| setError({ | setError({ | ||||
| @@ -677,6 +711,7 @@ void PlcCommunicationService::handleUnexpectedDisconnect() | |||||
| void PlcCommunicationService::handleModbusError(QModbusDevice::Error error) | void PlcCommunicationService::handleModbusError(QModbusDevice::Error error) | ||||
| { | { | ||||
| // 主动断开、已处理的故障和旧回复错误都不重复上报 | |||||
| if (disconnecting_ | if (disconnecting_ | ||||
| || (error == QModbusDevice::ReplyAbortedError | || (error == QModbusDevice::ReplyAbortedError | ||||
| && state_ == PlcConnectionState::Disconnected) | && state_ == PlcConnectionState::Disconnected) | ||||
| @@ -695,6 +730,7 @@ void PlcCommunicationService::handleModbusError(QModbusDevice::Error error) | |||||
| void PlcCommunicationService::closeSerialSession() | void PlcCommunicationService::closeSerialSession() | ||||
| { | { | ||||
| // 先递增代次并停止定时器,再断开串口;旧异步回调会因此失效 | |||||
| ++connection_generation_; | ++connection_generation_; | ||||
| disconnecting_ = true; | disconnecting_ = true; | ||||
| poll_timer_.stop(); | poll_timer_.stop(); | ||||
| @@ -718,6 +754,7 @@ void PlcCommunicationService::setState(PlcConnectionState state) | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| // 状态集中从这里修改,确保 Qt 信号和 std::function 回调同步触发 | |||||
| state_ = state; | state_ = state; | ||||
| emit stateChanged(); | emit stateChanged(); | ||||
| if (state_changed_callback_) | if (state_changed_callback_) | ||||
| @@ -728,6 +765,7 @@ void PlcCommunicationService::setState(PlcConnectionState state) | |||||
| void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | ||||
| { | { | ||||
| // 统一保存错误、停止正常轮询、撤销首读资格并通知 UI | |||||
| last_error_type_ = failure.type; | last_error_type_ = failure.type; | ||||
| last_error_ = toUtf8(failure.message); | last_error_ = toUtf8(failure.message); | ||||
| poll_timer_.stop(); | poll_timer_.stop(); | ||||
| @@ -754,3 +792,5 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | |||||
| recovery_timer_.start(kRecoveryProbeIntervalMs); | recovery_timer_.start(kRecoveryProbeIntervalMs); | ||||
| } | } | ||||
| } | } | ||||
| // 按区域和地址排序,方便后面合并连续读块 | |||||
| // 相同区域和编号的地址只保留一个 | |||||
| @@ -24,22 +24,33 @@ class PlcCommunicationService final : public QObject, public PlcCommunicationGat | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| // 创建通信服务,并把 PLC 缓存仓库交给它管理 | |||||
| explicit PlcCommunicationService( | explicit PlcCommunicationService( | ||||
| PlcRegisterRepository &repository, | PlcRegisterRepository &repository, | ||||
| QObject *parent = nullptr); | QObject *parent = nullptr); | ||||
| // 释放 Modbus 主站和未完成的通信资源 | |||||
| ~PlcCommunicationService() override; | ~PlcCommunicationService() override; | ||||
| // 按给定串口参数连接 PLC,并开始异步轮询 | |||||
| PlcCommunicationResult connectDevice( | PlcCommunicationResult connectDevice( | ||||
| const PlcSerialConfiguration &configuration) override; | const PlcSerialConfiguration &configuration) override; | ||||
| // 停止轮询并断开 PLC | |||||
| void disconnectDevice() override; | void disconnectDevice() override; | ||||
| // 设置需要周期性读取的 M/D 地址集合 | |||||
| PlcCommunicationResult setPollAddresses( | PlcCommunicationResult setPollAddresses( | ||||
| const std::vector<RegisterAddress> &addresses) override; | const std::vector<RegisterAddress> &addresses) override; | ||||
| // 返回当前连接状态 | |||||
| PlcConnectionState state() const override; | PlcConnectionState state() const override; | ||||
| // 判断本次连接是否已经完成所有轮询块的首次读取 | |||||
| bool initialReadCompleted() const override; | bool initialReadCompleted() const override; | ||||
| // 返回最近一次通信错误的类型 | |||||
| PlcCommunicationError lastErrorType() const override; | PlcCommunicationError lastErrorType() const override; | ||||
| // 返回最近一次通信错误的文字 | |||||
| const std::string &lastError() const override; | const std::string &lastError() const override; | ||||
| // 返回当前使用的串口和 Modbus 参数 | |||||
| const PlcSerialConfiguration &configuration() const; | const PlcSerialConfiguration &configuration() const; | ||||
| // 注册状态、首读、缓存更新和错误通知回调 | |||||
| void setCallbacks( | void setCallbacks( | ||||
| std::function<void()> state_changed, | std::function<void()> state_changed, | ||||
| std::function<void(bool)> initial_read_changed, | std::function<void(bool)> initial_read_changed, | ||||
| @@ -47,18 +58,22 @@ public: | |||||
| std::function<void(const std::string &)> error_reported) override; | std::function<void(const std::string &)> error_reported) override; | ||||
| signals: | signals: | ||||
| // 连接状态发生变化 | |||||
| void stateChanged(); | void stateChanged(); | ||||
| // 首次完整读取资格发生变化 | |||||
| void initialReadCompletedChanged(bool completed); | void initialReadCompletedChanged(bool completed); | ||||
| // PLC 缓存收到新的成功读数 | |||||
| void cacheUpdated(); | void cacheUpdated(); | ||||
| // 通信错误的可读提示文字 | |||||
| void communicationError(const QString &message); | void communicationError(const QString &message); | ||||
| private: | private: | ||||
| struct PollBlock | struct PollBlock | ||||
| { | { | ||||
| // 同一区域的一段连续 Modbus 原始地址 | // 同一区域的一段连续 Modbus 原始地址 | ||||
| RegisterArea area = RegisterArea::M; | |||||
| int startAddress = 0; | |||||
| int count = 1; | |||||
| RegisterArea area = RegisterArea::M; // M 区或 D 区 | |||||
| int startAddress = 0; // 读块的第一个原始地址 | |||||
| int count = 1; // 从起始地址连续读取的数量 | |||||
| }; | }; | ||||
| // 把去重后的地址集合压缩为有限数量的连续读块 | // 把去重后的地址集合压缩为有限数量的连续读块 | ||||
| @@ -69,7 +84,9 @@ private: | |||||
| void pollNextBlock(); | void pollNextBlock(); | ||||
| // Faulted 状态下用轻量探测判断通信是否恢复 | // Faulted 状态下用轻量探测判断通信是否恢复 | ||||
| void probeRecovery(); | void probeRecovery(); | ||||
| // 处理恢复探测失败,并安排下一次探测 | |||||
| void handleRecoveryProbeFailure(QModbusDevice::Error error); | void handleRecoveryProbeFailure(QModbusDevice::Error error); | ||||
| // 探测成功后恢复轮询,但仍需重新完成首读 | |||||
| void restoreCommunication(); | void restoreCommunication(); | ||||
| // 处理读回复并把值写入 PLC 缓存 | // 处理读回复并把值写入 PLC 缓存 | ||||
| void handleReadFinished(QModbusReply *reply, PollBlock block); | void handleReadFinished(QModbusReply *reply, PollBlock block); | ||||
| @@ -79,36 +96,41 @@ private: | |||||
| const RegisterAddress &address, std::int16_t value); | const RegisterAddress &address, std::int16_t value); | ||||
| // 更新“所有轮询块均成功读取”的真机进入资格 | // 更新“所有轮询块均成功读取”的真机进入资格 | ||||
| void updateInitialReadCompleted(bool completed, bool force_notification = false); | void updateInitialReadCompleted(bool completed, bool force_notification = false); | ||||
| // 处理没有主动断开时发生的串口断线 | |||||
| void handleUnexpectedDisconnect(); | void handleUnexpectedDisconnect(); | ||||
| // 把 Qt Modbus 错误转换成项目自己的通信错误 | |||||
| void handleModbusError(QModbusDevice::Error error); | void handleModbusError(QModbusDevice::Error error); | ||||
| // 关闭串口并清理当前连接的请求和状态 | |||||
| void closeSerialSession(); | void closeSerialSession(); | ||||
| // 修改连接状态并通知外部观察者 | |||||
| void setState(PlcConnectionState state); | void setState(PlcConnectionState state); | ||||
| // 保存错误信息、更新状态并通知外部观察者 | |||||
| void setError(const PlcCommunicationFailure &failure); | void setError(const PlcCommunicationFailure &failure); | ||||
| PlcRegisterRepository &repository_; | |||||
| std::unique_ptr<QModbusRtuSerialMaster> master_; | |||||
| QTimer poll_timer_; | |||||
| QTimer recovery_timer_; | |||||
| PlcSerialConfiguration configuration_; | |||||
| std::vector<RegisterAddress> poll_addresses_; | |||||
| std::vector<RegisterAddress> pending_poll_addresses_; | |||||
| std::vector<PollBlock> poll_blocks_; | |||||
| std::size_t next_poll_block_ = 0; | |||||
| QModbusReply *pending_reply_ = nullptr; | |||||
| QModbusReply *pending_write_reply_ = nullptr; | |||||
| bool poll_update_pending_ = false; | |||||
| bool disconnecting_ = false; | |||||
| bool serial_session_opened_ = false; | |||||
| bool received_valid_response_ = false; | |||||
| std::uint64_t connection_generation_ = 0; | |||||
| PlcConnectionState state_ = PlcConnectionState::Disconnected; | |||||
| bool initial_read_completed_ = false; | |||||
| PlcRegisterRepository &repository_; // 用于保存 PLC 最近一次成功读回的 M/D 值 | |||||
| std::unique_ptr<QModbusRtuSerialMaster> master_; // Qt Modbus RTU 主站对象 | |||||
| QTimer poll_timer_; // 周期性触发下一轮轮询 | |||||
| QTimer recovery_timer_; // Faulted 状态下定时发起恢复探测 | |||||
| PlcSerialConfiguration configuration_; // 当前串口和站号配置 | |||||
| std::vector<RegisterAddress> poll_addresses_; // 当前生效的轮询地址 | |||||
| std::vector<RegisterAddress> pending_poll_addresses_; // 请求在途时暂存的新地址 | |||||
| std::vector<PollBlock> poll_blocks_; // 根据地址合并出的连续读块 | |||||
| std::size_t next_poll_block_ = 0; // 下一次要读取的读块下标 | |||||
| QModbusReply *pending_reply_ = nullptr; // 当前未完成的读请求或恢复探测 | |||||
| QModbusReply *pending_write_reply_ = nullptr; // 当前未完成的单点写请求 | |||||
| bool poll_update_pending_ = false; // 是否有等待请求完成后应用的新轮询集合 | |||||
| bool disconnecting_ = false; // 是否正在执行主动断开 | |||||
| bool serial_session_opened_ = false; // 本次串口会话是否曾经成功打开 | |||||
| bool received_valid_response_ = false; // 本次连接是否收到过有效 PLC 回复 | |||||
| std::uint64_t connection_generation_ = 0; // 连接代次,用于丢弃旧连接的异步回复 | |||||
| PlcConnectionState state_ = PlcConnectionState::Disconnected; // 当前通信状态 | |||||
| bool initial_read_completed_ = false; // 是否已完成首读,可作为真机运行门槛 | |||||
| // 首读阶段每个读块的完成标记,全部为 true 后才允许进入真机 | // 首读阶段每个读块的完成标记,全部为 true 后才允许进入真机 | ||||
| std::vector<bool> initial_blocks_read_; | std::vector<bool> initial_blocks_read_; | ||||
| PlcCommunicationError last_error_type_ = PlcCommunicationError::None; | |||||
| std::string last_error_; | |||||
| std::function<void()> state_changed_callback_; | |||||
| std::function<void(bool)> initial_read_changed_callback_; | |||||
| std::function<void()> cache_updated_callback_; | |||||
| std::function<void(const std::string &)> error_reported_callback_; | |||||
| PlcCommunicationError last_error_type_ = PlcCommunicationError::None; // 最近一次错误类型 | |||||
| std::string last_error_; // 最近一次错误的可读文字 | |||||
| std::function<void()> state_changed_callback_; // 状态变化时调用 | |||||
| std::function<void(bool)> initial_read_changed_callback_; // 首读资格变化时调用 | |||||
| std::function<void()> cache_updated_callback_; // 缓存更新后调用 | |||||
| std::function<void(const std::string &)> error_reported_callback_; // 发生错误时调用 | |||||
| }; | }; | ||||
| @@ -31,12 +31,13 @@ public: | |||||
| bool hasAnyValidValue() const; | bool hasAnyValidValue() const; | ||||
| private: | private: | ||||
| // M/D 区共用的数组长度,覆盖 0 到最大地址 | |||||
| static constexpr std::size_t kRegisterCount = | static constexpr std::size_t kRegisterCount = | ||||
| static_cast<std::size_t>(RegisterAddress::kMaximumIndex + 1); | static_cast<std::size_t>(RegisterAddress::kMaximumIndex + 1); | ||||
| std::array<bool, kRegisterCount> bits_{}; | |||||
| std::array<std::int16_t, kRegisterCount> words_{}; | |||||
| std::array<bool, kRegisterCount> valid_bits_{}; | |||||
| std::array<bool, kRegisterCount> valid_words_{}; | |||||
| std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler_; | |||||
| std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler_; | |||||
| std::array<bool, kRegisterCount> bits_{}; // 最近一次读到的 M 区位值 | |||||
| std::array<std::int16_t, kRegisterCount> words_{}; // 最近一次读到的 D 区字值 | |||||
| std::array<bool, kRegisterCount> valid_bits_{}; // 对应 M 地址是否读到过有效值 | |||||
| std::array<bool, kRegisterCount> valid_words_{}; // 对应 D 地址是否读到过有效值 | |||||
| std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler_; // M 区异步写入回调 | |||||
| std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler_; // D 区异步写入回调 | |||||
| }; | }; | ||||
| @@ -26,36 +26,59 @@ | |||||
| #include "services/register_comment_service.h" | #include "services/register_comment_service.h" | ||||
| #include "ui/main_window.h" | #include "ui/main_window.h" | ||||
| // 程序从这里开始,argc 和 argv 保存用户启动程序时附带的命令行参数 | |||||
| int main(int argc, char *argv[]) | int main(int argc, char *argv[]) | ||||
| { | { | ||||
| // 创建整个 Qt 应用,后面的窗口、按钮和消息循环都要依靠它 | |||||
| QApplication application(argc, argv); | QApplication application(argc, argv); | ||||
| // 设置程序名称,Qt 的窗口信息和配置中会用到这个名字 | |||||
| application.setApplicationName(QObject::tr("综合平台编程器")); | application.setApplicationName(QObject::tr("综合平台编程器")); | ||||
| // 设置组织名称,用来区分这个程序保存的 Qt 配置 | |||||
| application.setOrganizationName(QStringLiteral("QtProXinJe")); | application.setOrganizationName(QStringLiteral("QtProXinJe")); | ||||
| // 组合根负责创建具体实现并将抽象依赖注入服务和 UI | |||||
| // 负责把工程保存到 JSON 文件,也负责从 JSON 文件读取工程 | |||||
| JsonProjectStorage project_storage; | JsonProjectStorage project_storage; | ||||
| // 统一管理新建、打开、保存和校验工程,实际读写文件交给上面的对象 | |||||
| ProjectService project_service(project_storage); | ProjectService project_service(project_storage); | ||||
| // 处理 HMI 页面和控件的添加、修改、删除、撤销与重做 | |||||
| HmiEditorService hmi_editor_service(project_service); | HmiEditorService hmi_editor_service(project_service); | ||||
| // 处理运行画面在不同 HMI 页面之间的跳转 | |||||
| HmiNavigationService hmi_navigation_service(project_service); | HmiNavigationService hmi_navigation_service(project_service); | ||||
| // 处理梯形图网络和节点的编辑、校验、撤销与重做 | |||||
| LogicEditorService logic_editor_service(project_service); | LogicEditorService logic_editor_service(project_service); | ||||
| // 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现 | |||||
| // 在内存中保存离线仿真使用的 M、D 寄存器值,不会写入真实 PLC | |||||
| VirtualRegisterRepository virtual_register_repository; | VirtualRegisterRepository virtual_register_repository; | ||||
| // 保存从真实 PLC 最近一次读取到的 M、D 值,并转交写入请求 | |||||
| PlcRegisterRepository plc_register_repository; | PlcRegisterRepository plc_register_repository; | ||||
| // 给界面提供统一的寄存器入口,启动时先使用上面的离线寄存器 | |||||
| ActiveRegisterRepository active_register_repository(virtual_register_repository); | ActiveRegisterRepository active_register_repository(virtual_register_repository); | ||||
| // 负责通过串口异步连接、读取和写入真实 PLC | |||||
| PlcCommunicationService plc_communication_service(plc_register_repository); | PlcCommunicationService plc_communication_service(plc_register_repository); | ||||
| // 处理运行画面读取和写入寄存器,数据来自当前选中的寄存器入口 | |||||
| HmiRuntimeService hmi_runtime_service(active_register_repository); | HmiRuntimeService hmi_runtime_service(active_register_repository); | ||||
| // 处理报警规则的添加、修改和删除 | |||||
| AlarmEditorService alarm_editor_service(project_service); | AlarmEditorService alarm_editor_service(project_service); | ||||
| // 根据当前寄存器值判断报警是否触发,并处理报警确认 | |||||
| AlarmService alarm_service(project_service, active_register_repository); | AlarmService alarm_service(project_service, active_register_repository); | ||||
| // 管理工程中 M、D 地址对应的文字备注 | |||||
| RegisterCommentService register_comment_service(project_service); | RegisterCommentService register_comment_service(project_service); | ||||
| // 处理自由监控列表中的寄存器读取和单点写入 | |||||
| RegisterMonitorService register_monitor_service(active_register_repository); | RegisterMonitorService register_monitor_service(active_register_repository); | ||||
| // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 | |||||
| OfflineSimulationService offline_simulation_service(virtual_register_repository); | OfflineSimulationService offline_simulation_service(virtual_register_repository); | ||||
| // 管理编辑、离线运行和真机运行三种模式,并控制离线仿真的启停 | |||||
| RuntimeModeService runtime_mode_service( | RuntimeModeService runtime_mode_service( | ||||
| project_service, offline_simulation_service); | project_service, offline_simulation_service); | ||||
| // 把 PLC 通信和两套寄存器接入运行模式,切换模式时才能切换数据来源 | |||||
| runtime_mode_service.configurePlc( | runtime_mode_service.configurePlc( | ||||
| plc_communication_service, | plc_communication_service, | ||||
| active_register_repository, | active_register_repository, | ||||
| virtual_register_repository, | virtual_register_repository, | ||||
| plc_register_repository); | plc_register_repository); | ||||
| // 创建主窗口,并把界面操作需要的各项服务交给它使用 | |||||
| MainWindow main_window( | MainWindow main_window( | ||||
| runtime_mode_service, | runtime_mode_service, | ||||
| project_service, | project_service, | ||||
| @@ -67,7 +90,9 @@ int main(int argc, char *argv[]) | |||||
| hmi_navigation_service, | hmi_navigation_service, | ||||
| register_comment_service, | register_comment_service, | ||||
| register_monitor_service); | register_monitor_service); | ||||
| // 把主窗口显示到屏幕上 | |||||
| main_window.show(); | main_window.show(); | ||||
| // 启动 Qt 消息循环,持续响应鼠标、键盘、定时器和串口事件,窗口关闭后才返回 | |||||
| return application.exec(); | return application.exec(); | ||||
| } | } | ||||
| @@ -21,11 +21,13 @@ void AlarmService::refresh() | |||||
| for (const AlarmDefinition &definition | for (const AlarmDefinition &definition | ||||
| : project_service_.project().alarmDefinitions) | : project_service_.project().alarmDefinitions) | ||||
| { | { | ||||
| // 空值表示寄存器读取失败;保留原记录,等待下一轮成功读回 | |||||
| const std::optional<bool> active = evaluate(definition); | const std::optional<bool> active = evaluate(definition); | ||||
| if (!active.has_value()) | if (!active.has_value()) | ||||
| { | { | ||||
| continue; | continue; | ||||
| } | } | ||||
| // 同一报警定义在当前会话中最多保留一条活动记录 | |||||
| const auto current = std::find_if( | const auto current = std::find_if( | ||||
| records_.begin(), records_.end(), | records_.begin(), records_.end(), | ||||
| [&definition](const AlarmRecord &record) | [&definition](const AlarmRecord &record) | ||||
| @@ -34,12 +36,14 @@ void AlarmService::refresh() | |||||
| }); | }); | ||||
| if (*active && current == records_.end()) | if (*active && current == records_.end()) | ||||
| { | { | ||||
| // 首次检测到触发时创建未确认记录,并置于列表头部 | |||||
| records_.insert( | records_.insert( | ||||
| records_.begin(), | records_.begin(), | ||||
| AlarmRecord{definition.id, definition.message, false, now}); | AlarmRecord{definition.id, definition.message, false, now}); | ||||
| } | } | ||||
| else if (!*active && current != records_.end()) | else if (!*active && current != records_.end()) | ||||
| { | { | ||||
| // 条件恢复后立即移除对应记录,下一次再次触发会生成新记录 | |||||
| records_.erase(current); | records_.erase(current); | ||||
| } | } | ||||
| } | } | ||||
| @@ -76,21 +80,27 @@ std::optional<bool> AlarmService::evaluate( | |||||
| { | { | ||||
| if (definition.condition == AlarmCondition::MOn) | if (definition.condition == AlarmCondition::MOn) | ||||
| { | { | ||||
| // MOn 报警直接读取 M 位;读取失败返回空值,交由 refresh 保留旧记录 | |||||
| const BitReadResult value = repository_.readBit(definition.address); | const BitReadResult value = repository_.readBit(definition.address); | ||||
| return value.succeeded ? std::optional<bool>{value.value} : std::nullopt; | return value.succeeded ? std::optional<bool>{value.value} : std::nullopt; | ||||
| } | } | ||||
| // DHigh 和 DLow 报警读取 D 字,比较方向由定义中的条件决定 | |||||
| const WordReadResult value = repository_.readWord(definition.address); | const WordReadResult value = repository_.readWord(definition.address); | ||||
| if (!value.succeeded) | if (!value.succeeded) | ||||
| { | { | ||||
| // 通信或仓库读取失败不等同于报警解除 | |||||
| return std::nullopt; | return std::nullopt; | ||||
| } | } | ||||
| if (definition.condition == AlarmCondition::DHigh) | if (definition.condition == AlarmCondition::DHigh) | ||||
| { | { | ||||
| // 达到阈值即触发高限报警 | |||||
| return value.value >= definition.threshold; | return value.value >= definition.threshold; | ||||
| } | } | ||||
| if (definition.condition == AlarmCondition::DLow) | if (definition.condition == AlarmCondition::DLow) | ||||
| { | { | ||||
| // 降到阈值即触发低限报警 | |||||
| return value.value <= definition.threshold; | return value.value <= definition.threshold; | ||||
| } | } | ||||
| // 防御未来新增但尚未实现的报警条件 | |||||
| return std::nullopt; | return std::nullopt; | ||||
| } | } | ||||
| @@ -9,36 +9,66 @@ class ProjectService; | |||||
| class RegisterRepository; | class RegisterRepository; | ||||
| struct AlarmDefinition; | struct AlarmDefinition; | ||||
| // 运行时产生的报警记录,不会写入工程 JSON | |||||
| // 运行时产生的报警记录,只存在当前会话,不会写入工程 JSON | |||||
| struct AlarmRecord | struct AlarmRecord | ||||
| { | { | ||||
| std::string definitionId; | |||||
| std::string message; | |||||
| bool acknowledged = false; | |||||
| std::chrono::system_clock::time_point occurredAt; | |||||
| std::string definitionId; // 对应 AlarmDefinition 的稳定 ID | |||||
| std::string message; // 触发时复制的报警提示文本 | |||||
| bool acknowledged = false; // 当前记录是否已被操作员确认 | |||||
| std::chrono::system_clock::time_point occurredAt; // 本次触发时间 | |||||
| }; | }; | ||||
| // 根据当前活动寄存器仓库评估报警,并维护确认状态 | |||||
| /** | |||||
| * @brief 根据当前活动寄存器仓库评估报警并维护会话记录 | |||||
| * | |||||
| * 服务只读取仓库,不修改工程中的报警定义;仓库可以由运行模式注入虚拟或 PLC 数据源 | |||||
| */ | |||||
| class AlarmService | class AlarmService | ||||
| { | { | ||||
| public: | public: | ||||
| /** | |||||
| * @brief 创建报警服务 | |||||
| * @param project_service 只读工程服务,提供报警定义集合 | |||||
| * @param repository 当前运行模式使用的寄存器仓库 | |||||
| */ | |||||
| AlarmService( | AlarmService( | ||||
| const ProjectService &project_service, | const ProjectService &project_service, | ||||
| RegisterRepository &repository); | RegisterRepository &repository); | ||||
| // 重新评估全部定义;仍处于触发状态的记录会保留确认状态 | |||||
| /** | |||||
| * @brief 重新评估全部报警定义 | |||||
| * | |||||
| * 仍处于触发状态的记录会保留确认状态;读取失败时跳过该定义并保留原记录。 | |||||
| * 新触发记录插入列表头部,解除触发的记录立即移除 | |||||
| */ | |||||
| void refresh(); | void refresh(); | ||||
| // 确认指定报警;不存在时返回 false | |||||
| /** | |||||
| * @brief 确认当前会话中的指定报警记录 | |||||
| * @param definition_id 报警定义的稳定 ID | |||||
| * @return 找到当前活动记录并完成确认时返回 true,否则返回 false | |||||
| */ | |||||
| bool acknowledge(const std::string &definition_id); | bool acknowledge(const std::string &definition_id); | ||||
| // 清空当前运行会话的报警记录 | |||||
| /** | |||||
| * @brief 清空当前运行会话的全部报警记录和确认状态 | |||||
| */ | |||||
| void reset(); | void reset(); | ||||
| // 返回按触发时间保存的报警记录 | |||||
| /** | |||||
| * @brief 返回当前会话的报警记录 | |||||
| * @return 按触发时间倒序排列的只读记录列表,列表由服务持有 | |||||
| */ | |||||
| const std::vector<AlarmRecord> &records() const; | const std::vector<AlarmRecord> &records() const; | ||||
| private: | private: | ||||
| /** | |||||
| * @brief 使用当前寄存器值评估单条报警定义 | |||||
| * @param definition 待评估的报警定义 | |||||
| * @return true 表示触发,false 表示未触发;读取失败时返回空值 | |||||
| * | |||||
| * MOn 比较 M 位是否为 1;DHigh 使用大于等于阈值,DLow 使用小于等于阈值 | |||||
| */ | |||||
| std::optional<bool> evaluate(const AlarmDefinition &definition) const; | std::optional<bool> evaluate(const AlarmDefinition &definition) const; | ||||
| const ProjectService &project_service_; | |||||
| RegisterRepository &repository_; | |||||
| std::vector<AlarmRecord> records_; | |||||
| const ProjectService &project_service_; // 不拥有的只读工程服务 | |||||
| RegisterRepository &repository_; // 当前运行模式的活动寄存器仓库 | |||||
| std::vector<AlarmRecord> records_; // 当前会话记录,最新触发项位于列表头部 | |||||
| }; | }; | ||||
| @@ -8,16 +8,27 @@ | |||||
| /** | /** | ||||
| * @brief 保存有限数量的编辑前快照并提供撤销、重做 | * @brief 保存有限数量的编辑前快照并提供撤销、重做 | ||||
| * | * | ||||
| * 历史只存在当前编辑会话内。新编辑会清空重做栈,超过容量时丢弃最早记录。 | |||||
| * State 由具体编辑服务决定,可以是 HMI 页面集合或控制逻辑集合 | |||||
| * 历史只存在当前编辑会话内,不写入工程文件 | |||||
| * 新编辑会清空重做栈,超过容量时丢弃最早记录 | |||||
| */ | */ | ||||
| template <typename State> | template <typename State> | ||||
| class EditorHistory final | class EditorHistory final | ||||
| { | { | ||||
| public: | public: | ||||
| // 单个编辑服务最多保留 100 条撤销和重做历史 | |||||
| static constexpr std::size_t kMaximumEntries = 100U; | static constexpr std::size_t kMaximumEntries = 100U; | ||||
| template <typename Equal> | template <typename Equal> | ||||
| // 只有前后状态不同才记录;新编辑会清空重做栈 | |||||
| /** | |||||
| * @brief 记录一次编辑前状态 | |||||
| * @param before 编辑前状态 | |||||
| * @param after 编辑完成后的当前状态 | |||||
| * @param equal 比较前后状态是否相同的可调用对象 | |||||
| * | |||||
| * 状态没有变化时不创建空历史 | |||||
| * 新编辑成功记录后清空重做栈 | |||||
| */ | |||||
| void record(State before, const State &after, Equal equal) | void record(State before, const State &after, Equal equal) | ||||
| { | { | ||||
| if (equal(before, after)) | if (equal(before, after)) | ||||
| @@ -29,7 +40,13 @@ public: | |||||
| redo_states_.clear(); | redo_states_.clear(); | ||||
| } | } | ||||
| // 返回撤销后的目标状态,并把当前状态放入重做栈 | |||||
| /** | |||||
| * @brief 撤销最近一次编辑 | |||||
| * @param current 当前状态 | |||||
| * @return 撤销后的目标状态;没有可撤销历史时返回空值 | |||||
| * | |||||
| * 当前状态会先进入重做栈,之后取出撤销栈顶的编辑前快照 | |||||
| */ | |||||
| std::optional<State> undo(State current) | std::optional<State> undo(State current) | ||||
| { | { | ||||
| if (undo_states_.empty()) | if (undo_states_.empty()) | ||||
| @@ -43,7 +60,13 @@ public: | |||||
| return target; | return target; | ||||
| } | } | ||||
| // 返回重做后的目标状态,并把当前状态放回撤销栈 | |||||
| /** | |||||
| * @brief 重做最近一次被撤销的编辑 | |||||
| * @param current 当前状态 | |||||
| * @return 重做后的目标状态;没有可重做历史时返回空值 | |||||
| * | |||||
| * 当前状态会先放回撤销栈,之后取出重做栈顶的目标状态 | |||||
| */ | |||||
| std::optional<State> redo(State current) | std::optional<State> redo(State current) | ||||
| { | { | ||||
| if (redo_states_.empty()) | if (redo_states_.empty()) | ||||
| @@ -57,23 +80,33 @@ public: | |||||
| return target; | return target; | ||||
| } | } | ||||
| /** | |||||
| * @brief 清空撤销栈和重做栈 | |||||
| */ | |||||
| void clear() | void clear() | ||||
| { | { | ||||
| undo_states_.clear(); | undo_states_.clear(); | ||||
| redo_states_.clear(); | redo_states_.clear(); | ||||
| } | } | ||||
| /** | |||||
| * @brief 判断是否存在可撤销历史 | |||||
| */ | |||||
| bool canUndo() const | bool canUndo() const | ||||
| { | { | ||||
| return !undo_states_.empty(); | return !undo_states_.empty(); | ||||
| } | } | ||||
| /** | |||||
| * @brief 判断是否存在可重做历史 | |||||
| */ | |||||
| bool canRedo() const | bool canRedo() const | ||||
| { | { | ||||
| return !redo_states_.empty(); | return !redo_states_.empty(); | ||||
| } | } | ||||
| private: | private: | ||||
| // 保持历史不超过上限,超出时从最早的记录开始丢弃 | |||||
| static void trim(std::vector<State> *states) | static void trim(std::vector<State> *states) | ||||
| { | { | ||||
| while (states->size() > kMaximumEntries) | while (states->size() > kMaximumEntries) | ||||
| @@ -82,6 +115,6 @@ private: | |||||
| } | } | ||||
| } | } | ||||
| std::vector<State> undo_states_; | |||||
| std::vector<State> redo_states_; | |||||
| std::vector<State> undo_states_; // 编辑前快照栈,末尾是下一次撤销要恢复的状态 | |||||
| std::vector<State> redo_states_; // 撤销后状态栈,末尾是下一次重做要恢复的状态 | |||||
| }; | }; | ||||
| @@ -21,17 +21,17 @@ struct HmiControlDescriptor; | |||||
| */ | */ | ||||
| enum class HmiEditorError | enum class HmiEditorError | ||||
| { | { | ||||
| None, | |||||
| PageNotFound, | |||||
| ControlNotFound, | |||||
| DuplicateId, | |||||
| DuplicateName, | |||||
| InvalidPage, | |||||
| InvalidControl, | |||||
| LastPageRequired, | |||||
| InitialPageCannotBeRemoved, | |||||
| PageReferenced, | |||||
| InvalidOperation | |||||
| None, // 操作成功或没有错误 | |||||
| PageNotFound, // 目标页面不存在 | |||||
| ControlNotFound, // 目标控件不存在 | |||||
| DuplicateId, // 控件 ID 与同页其他控件重复 | |||||
| DuplicateName, // 页面名称与其他页面重复 | |||||
| InvalidPage, // 页面名称、尺寸或页面配置无效 | |||||
| InvalidControl, // 控件属性、绑定或页面边界无效 | |||||
| LastPageRequired, // 删除后不能少于一个 HMI 页面 | |||||
| InitialPageCannotBeRemoved, // 初始页面仍未切换,不能删除 | |||||
| PageReferenced, // 页面仍被页面跳转控件引用 | |||||
| InvalidOperation // 操作参数或撤销/重做状态不允许 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -41,10 +41,10 @@ enum class HmiEditorError | |||||
| */ | */ | ||||
| struct HmiEditorResult | struct HmiEditorResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| HmiEditorError error = HmiEditorError::None; | |||||
| std::string message; | |||||
| std::string id; | |||||
| bool succeeded = false; // 操作是否成功 | |||||
| HmiEditorError error = HmiEditorError::None; // 失败时的分类 | |||||
| std::string message; // 面向用户的 UTF-8 成功说明或失败原因 | |||||
| std::string id; // 成功时返回新建或更新后的页面/控件 ID | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -55,6 +55,10 @@ struct HmiEditorResult | |||||
| class HmiEditorService | class HmiEditorService | ||||
| { | { | ||||
| public: | public: | ||||
| /** | |||||
| * @brief 创建 HMI 编辑服务 | |||||
| * @param project_service 用于读取和原子修改当前工程的项目服务 | |||||
| */ | |||||
| explicit HmiEditorService(ProjectService &project_service); | explicit HmiEditorService(ProjectService &project_service); | ||||
| /** | /** | ||||
| @@ -82,13 +86,52 @@ public: | |||||
| * @return 已有或新建页面的成功结果及其页面标识 | * @return 已有或新建页面的成功结果及其页面标识 | ||||
| */ | */ | ||||
| HmiEditorResult ensureDefaultPage(); | HmiEditorResult ensureDefaultPage(); | ||||
| /** | |||||
| * @brief 添加一个 HMI 页面 | |||||
| * @param name 页面显示名称,不能为空且必须在工程内唯一 | |||||
| * @return 成功时返回新页面 ID | |||||
| */ | |||||
| HmiEditorResult addPage(const std::string &name); | HmiEditorResult addPage(const std::string &name); | ||||
| /** | |||||
| * @brief 调整页面尺寸 | |||||
| * @param page_id 目标页面唯一标识 | |||||
| * @param width 页面宽度,必须满足页面边界且不能使已有控件越界 | |||||
| * @param height 页面高度,必须满足页面边界且不能使已有控件越界 | |||||
| * @return 页面不存在或新尺寸无效时返回失败结果 | |||||
| */ | |||||
| HmiEditorResult resizePage( | HmiEditorResult resizePage( | ||||
| const std::string &page_id, int width, int height); | const std::string &page_id, int width, int height); | ||||
| /** | |||||
| * @brief 修改页面显示名称 | |||||
| * @param page_id 目标页面唯一标识 | |||||
| * @param name 新页面名称,不能为空且必须在工程内唯一 | |||||
| * @return 页面不存在或名称无效/重复时返回失败结果 | |||||
| */ | |||||
| HmiEditorResult renamePage( | HmiEditorResult renamePage( | ||||
| const std::string &page_id, const std::string &name); | const std::string &page_id, const std::string &name); | ||||
| /** | |||||
| * @brief 删除一个 HMI 页面 | |||||
| * @param page_id 待删除页面唯一标识 | |||||
| * @return 页面不存在、页面是唯一页面、仍为初始页面或仍被引用时返回失败结果 | |||||
| */ | |||||
| HmiEditorResult removePage(const std::string &page_id); | HmiEditorResult removePage(const std::string &page_id); | ||||
| /** | |||||
| * @brief 将页面在工程页面列表中上移或下移一位 | |||||
| * @param page_id 目标页面唯一标识 | |||||
| * @param offset 只能为 -1(上移)或 1(下移) | |||||
| * @return 页面不存在或已经位于目标边界时返回失败结果 | |||||
| */ | |||||
| HmiEditorResult movePage(const std::string &page_id, int offset); | HmiEditorResult movePage(const std::string &page_id, int offset); | ||||
| /** | |||||
| * @brief 设置工程的初始 HMI 页面 | |||||
| * @param page_id 要作为启动页面的页面唯一标识 | |||||
| * @return 页面不存在时返回 PageNotFound | |||||
| */ | |||||
| HmiEditorResult setInitialPage(const std::string &page_id); | HmiEditorResult setInitialPage(const std::string &page_id); | ||||
| /** | /** | ||||
| * @brief 向指定页面添加待配置的基础控件 | * @brief 向指定页面添加待配置的基础控件 | ||||
| @@ -108,6 +151,13 @@ public: | |||||
| */ | */ | ||||
| HmiEditorResult removeControl( | HmiEditorResult removeControl( | ||||
| const std::string &page_id, const std::string &control_id); | const std::string &page_id, const std::string &control_id); | ||||
| /** | |||||
| * @brief 一次删除同一页面中的多个控件 | |||||
| * @param page_id 所属页面唯一标识 | |||||
| * @param control_ids 待删除控件 ID 列表,不能为空且不能包含重复 ID | |||||
| * @return 页面、控件不存在或 ID 列表无效时返回失败结果 | |||||
| */ | |||||
| HmiEditorResult removeControls( | HmiEditorResult removeControls( | ||||
| const std::string &page_id, | const std::string &page_id, | ||||
| const std::vector<std::string> &control_ids); | const std::vector<std::string> &control_ids); | ||||
| @@ -136,26 +186,51 @@ public: | |||||
| const std::string &control_id, | const std::string &control_id, | ||||
| const HmiControl &control); | const HmiControl &control); | ||||
| /** @brief 判断是否存在可撤销的 HMI 编辑操作 */ | |||||
| bool canUndo() const; | bool canUndo() const; | ||||
| /** @brief 判断是否存在可重做的 HMI 编辑操作 */ | |||||
| bool canRedo() const; | bool canRedo() const; | ||||
| /** | |||||
| * @brief 撤销最近一次成功的编辑操作 | |||||
| * @return 没有可撤销操作时返回 InvalidOperation | |||||
| */ | |||||
| HmiEditorResult undo(); | HmiEditorResult undo(); | ||||
| /** | |||||
| * @brief 重做最近一次被撤销的编辑操作 | |||||
| * @return 没有可重做操作时返回 InvalidOperation | |||||
| */ | |||||
| HmiEditorResult redo(); | HmiEditorResult redo(); | ||||
| /** | |||||
| * @brief 清空撤销和重做历史 | |||||
| * | |||||
| * 不修改当前工程内容 | |||||
| */ | |||||
| void clearHistory(); | void clearHistory(); | ||||
| private: | private: | ||||
| // 撤销/重做需要保存的完整 HMI 编辑状态 | |||||
| struct HistoryState | struct HistoryState | ||||
| { | { | ||||
| std::vector<HmiPage> pages; | |||||
| std::string initial_page_id; | |||||
| std::vector<HmiPage> pages; // 页面及其控件的深拷贝 | |||||
| std::string initial_page_id; // 工程初始页面 ID | |||||
| }; | }; | ||||
| // 捕获当前工程状态,作为一次编辑操作的前后快照 | |||||
| HistoryState captureState() const; | HistoryState captureState() const; | ||||
| // 比较编辑前后状态并记录实际发生的修改 | |||||
| void recordHistory(HistoryState before); | void recordHistory(HistoryState before); | ||||
| // 比较两个历史快照是否完全相同 | |||||
| static bool statesEqual( | static bool statesEqual( | ||||
| const HistoryState &left, const HistoryState &right); | const HistoryState &left, const HistoryState &right); | ||||
| // 比较两个 HMI 控件的全部可保存属性 | |||||
| static bool controlsEqual( | static bool controlsEqual( | ||||
| const HmiControl &left, const HmiControl &right); | const HmiControl &left, const HmiControl &right); | ||||
| // 比较两个 HMI 页面及其控件集合 | |||||
| static bool pagesEqual(const HmiPage &left, const HmiPage &right); | static bool pagesEqual(const HmiPage &left, const HmiPage &right); | ||||
| // 生成统一的历史操作失败结果 | |||||
| static HmiEditorResult historyFailure(const std::string &message); | static HmiEditorResult historyFailure(const std::string &message); | ||||
| /** | /** | ||||
| @@ -183,7 +258,7 @@ private: | |||||
| /** | /** | ||||
| * @brief 按控件类型创建带默认属性的新控件 | * @brief 按控件类型创建带默认属性的新控件 | ||||
| * @param page 新控件所属页面,用于确定初始位置和唯一标识 | * @param page 新控件所属页面,用于确定初始位置和唯一标识 | ||||
| * @param type 新控件类型 | |||||
| * @param descriptor 新控件类型对应的注册描述 | |||||
| * @return 未绑定寄存器的默认控件配置 | * @return 未绑定寄存器的默认控件配置 | ||||
| */ | */ | ||||
| static HmiControl makeControl( | static HmiControl makeControl( | ||||
| @@ -18,10 +18,10 @@ | |||||
| enum class HmiRuntimeError | enum class HmiRuntimeError | ||||
| { | { | ||||
| None, // 无错误 | None, // 无错误 | ||||
| UnsupportedControl, // 控件类型不支持寄存器读写 | |||||
| UnsupportedControl, // 控件类型不支持寄存器读写或写入操作 | |||||
| MissingBinding, // 控件未配置寄存器绑定 | MissingBinding, // 控件未配置寄存器绑定 | ||||
| InvalidBinding, // 控件绑定地址无效或地址区域不匹配 | InvalidBinding, // 控件绑定地址无效或地址区域不匹配 | ||||
| RepositoryFailure // 寄存器仓库读取或写入失败 | |||||
| RepositoryFailure // 寄存器仓库不可用或拒绝读写 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -31,10 +31,10 @@ enum class HmiRuntimeError | |||||
| */ | */ | ||||
| struct HmiRuntimeReadResult | struct HmiRuntimeReadResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| HmiRuntimeError error = HmiRuntimeError::None; | |||||
| bool bit_value = false; | |||||
| std::int16_t word_value = 0; | |||||
| bool succeeded = false; // 是否成功读到控件绑定值 | |||||
| HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类 | |||||
| bool bit_value = false; // 位控件的读回值;字控件读取成功时无效 | |||||
| std::int16_t word_value = 0; // 字控件的读回值;位控件读取成功时无效 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -42,8 +42,8 @@ struct HmiRuntimeReadResult | |||||
| */ | */ | ||||
| struct HmiRuntimeWriteResult | struct HmiRuntimeWriteResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| HmiRuntimeError error = HmiRuntimeError::None; | |||||
| bool succeeded = false; // 写入请求是否被仓库接受 | |||||
| HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -51,8 +51,8 @@ struct HmiRuntimeWriteResult | |||||
| */ | */ | ||||
| enum class HmiButtonEvent | enum class HmiButtonEvent | ||||
| { | { | ||||
| Pressed, | |||||
| Released | |||||
| Pressed, // 按钮按下事件 | |||||
| Released // 按钮释放事件 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -64,6 +64,10 @@ enum class HmiButtonEvent | |||||
| class HmiRuntimeService | class HmiRuntimeService | ||||
| { | { | ||||
| public: | public: | ||||
| /** | |||||
| * @brief 创建 HMI 运行态读写服务 | |||||
| * @param repository 当前运行模式提供的寄存器仓库,不由本服务拥有 | |||||
| */ | |||||
| explicit HmiRuntimeService(RegisterRepository &repository); | explicit HmiRuntimeService(RegisterRepository &repository); | ||||
| /** | /** | ||||
| @@ -79,6 +83,9 @@ public: | |||||
| * @param control 按钮控件,必须绑定有效 M 地址 | * @param control 按钮控件,必须绑定有效 M 地址 | ||||
| * @param event 当前输入事件 | * @param event 当前输入事件 | ||||
| * @return 无需写入的事件返回成功,配置无效或仓库拒绝写入时返回失败 | * @return 无需写入的事件返回成功,配置无效或仓库拒绝写入时返回失败 | ||||
| * | |||||
| * SetOn、SetOff 和 Toggle 只在按下时写入;MomentaryOn 在按下时写 1、 | |||||
| * 释放时写 0。写入结果以仓库返回值为准,不直接假定缓存已经更新 | |||||
| */ | */ | ||||
| HmiRuntimeWriteResult operateButton( | HmiRuntimeWriteResult operateButton( | ||||
| const HmiControl &control, HmiButtonEvent event); | const HmiControl &control, HmiButtonEvent event); | ||||
| @@ -87,14 +94,29 @@ public: | |||||
| * @param control 数值输入控件,必须绑定有效 D 地址 | * @param control 数值输入控件,必须绑定有效 D 地址 | ||||
| * @param value 要写入的 D 字值 | * @param value 要写入的 D 字值 | ||||
| * @return 非数值输入、绑定无效或仓库拒绝写入时返回失败结果 | * @return 非数值输入、绑定无效或仓库拒绝写入时返回失败结果 | ||||
| * | |||||
| * 该服务只负责提交写入请求,实际读回值由后续仓库轮询确认 | |||||
| */ | */ | ||||
| HmiRuntimeWriteResult writeNumericInput( | HmiRuntimeWriteResult writeNumericInput( | ||||
| const HmiControl &control, std::int16_t value); | const HmiControl &control, std::int16_t value); | ||||
| /** | |||||
| * @brief 读取指定 M 位 | |||||
| * @param address 必须是有效的 M 区地址 | |||||
| * @return 仓库原始读取结果,不转换为 HMI 错误分类 | |||||
| */ | |||||
| BitReadResult readBit(const RegisterAddress &address) const; | BitReadResult readBit(const RegisterAddress &address) const; | ||||
| /** | |||||
| * @brief 读取指定 D 字 | |||||
| * @param address 必须是有效的 D 区地址 | |||||
| * @return 仓库原始读取结果,不转换为 HMI 错误分类 | |||||
| */ | |||||
| WordReadResult readWord(const RegisterAddress &address) const; | WordReadResult readWord(const RegisterAddress &address) const; | ||||
| private: | private: | ||||
| // 将寄存器仓库错误统一转换为 HMI 运行态错误 | |||||
| static HmiRuntimeError repositoryError(RegisterError error); | static HmiRuntimeError repositoryError(RegisterError error); | ||||
| RegisterRepository &repository_; | |||||
| RegisterRepository &repository_; // 当前运行模式使用的寄存器仓库,不由本服务拥有 | |||||
| }; | }; | ||||
| @@ -12,164 +12,233 @@ class ProjectService; | |||||
| // 梯形图编辑失败分类 | // 梯形图编辑失败分类 | ||||
| enum class LogicEditorError | enum class LogicEditorError | ||||
| { | { | ||||
| None, | |||||
| LogicNotFound, | |||||
| RungNotFound, | |||||
| ExpressionNotFound, | |||||
| NodeNotFound, | |||||
| InvalidNode, | |||||
| InvalidOperation, | |||||
| UnsupportedNodeChange, | |||||
| DuplicateName, | |||||
| LastLogicRequired | |||||
| None, // 操作成功或没有错误 | |||||
| LogicNotFound, // 控制逻辑不存在 | |||||
| RungNotFound, // 网络不存在 | |||||
| ExpressionNotFound, // 条件表达式不存在 | |||||
| NodeNotFound, // 节点不存在 | |||||
| InvalidNode, // 节点配置或节点位置无效 | |||||
| InvalidOperation, // 操作参数或当前结构不允许该操作 | |||||
| UnsupportedNodeChange, // 不允许把条件节点改成输出节点,或反向修改 | |||||
| DuplicateName, // 控制逻辑名称与其他逻辑重复 | |||||
| LastLogicRequired // 删除后不能少于一个控制逻辑 | |||||
| }; | }; | ||||
| // 梯形图编辑结果;成功时 id 通常是新建或更新对象的稳定 ID | // 梯形图编辑结果;成功时 id 通常是新建或更新对象的稳定 ID | ||||
| struct LogicEditorResult | struct LogicEditorResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| LogicEditorError error = LogicEditorError::None; | |||||
| std::string message; | |||||
| std::string id; | |||||
| bool succeeded = false; // 操作是否成功 | |||||
| LogicEditorError error = LogicEditorError::None; // 失败时的分类 | |||||
| std::string message; // 面向用户的 UTF-8 成功说明或失败原因 | |||||
| std::string id; // 成功时返回新建或更新对象的稳定 ID | |||||
| }; | }; | ||||
| // 负责把 UI 编辑命令转换为结构化表达式树操作,并维护撤销/重做 | // 负责把 UI 编辑命令转换为结构化表达式树操作,并维护撤销/重做 | ||||
| class LogicEditorService | class LogicEditorService | ||||
| { | { | ||||
| public: | public: | ||||
| /** | |||||
| * @brief 创建梯形图编辑服务 | |||||
| * @param project_service 用于读取和修改当前工程的项目服务 | |||||
| */ | |||||
| explicit LogicEditorService(ProjectService &project_service); | explicit LogicEditorService(ProjectService &project_service); | ||||
| // 以下查询接口只读工程模型,供编辑器投影和属性面板使用 | // 以下查询接口只读工程模型,供编辑器投影和属性面板使用 | ||||
| /** @brief 按 ID 查找控制逻辑,未找到时返回空指针 */ | |||||
| const ControlLogic *findLogic(const std::string &logic_id) const; | const ControlLogic *findLogic(const std::string &logic_id) const; | ||||
| /** @brief 在指定逻辑中按 ID 查找网络,未找到时返回空指针 */ | |||||
| const LadderRung *findRung( | const LadderRung *findRung( | ||||
| const std::string &logic_id, const std::string &rung_id) const; | const std::string &logic_id, const std::string &rung_id) const; | ||||
| /** @brief 在指定逻辑的所有网络中按 ID 查找节点,未找到时返回空指针 */ | |||||
| const LogicNode *findNode( | const LogicNode *findNode( | ||||
| const std::string &logic_id, const std::string &node_id) const; | const std::string &logic_id, const std::string &node_id) const; | ||||
| /** @brief 在指定网络中按 ID 查找条件表达式,未找到时返回空指针 */ | |||||
| const ConditionExpression *findExpression( | const ConditionExpression *findExpression( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &expression_id) const; | const std::string &expression_id) const; | ||||
| /** @brief 返回工程中第一个控制逻辑 ID,没有逻辑时返回空字符串 */ | |||||
| std::string firstLogicId() const; | std::string firstLogicId() const; | ||||
| /** @brief 返回指定逻辑中第一个网络 ID,没有网络或逻辑不存在时返回空字符串 */ | |||||
| std::string firstRungId(const std::string &logic_id) const; | std::string firstRungId(const std::string &logic_id) const; | ||||
| /** @brief 查找节点所属网络 ID,未找到时返回空字符串 */ | |||||
| std::string rungIdForNode( | std::string rungIdForNode( | ||||
| const std::string &logic_id, const std::string &node_id) const; | const std::string &logic_id, const std::string &node_id) const; | ||||
| /** @brief 返回指定 M/D 地址的工程注释,没有注释时返回空字符串 */ | |||||
| std::string registerCommentFor(const RegisterAddress &address) const; | std::string registerCommentFor(const RegisterAddress &address) const; | ||||
| // 确保新工程至少有一组可编辑逻辑;逻辑组初始允许没有网络 | |||||
| /** | |||||
| * @brief 确保工程至少有一组可编辑逻辑 | |||||
| * @return 已有或新建逻辑的成功结果及其 ID | |||||
| * | |||||
| * 新建逻辑时允许暂时没有网络,第一次实际编辑网络内容时再创建网络 | |||||
| */ | |||||
| LogicEditorResult ensureDefaultLogic(); | LogicEditorResult ensureDefaultLogic(); | ||||
| // 逻辑组管理 | |||||
| /** @brief 添加控制逻辑;名称不能为空且必须唯一 */ | |||||
| LogicEditorResult addLogic(const std::string &name); | LogicEditorResult addLogic(const std::string &name); | ||||
| /** @brief 修改控制逻辑名称;名称不能为空且必须唯一 */ | |||||
| LogicEditorResult renameLogic( | LogicEditorResult renameLogic( | ||||
| const std::string &logic_id, const std::string &name); | const std::string &logic_id, const std::string &name); | ||||
| /** @brief 删除控制逻辑;工程至少保留一组逻辑 */ | |||||
| LogicEditorResult removeLogic(const std::string &logic_id); | LogicEditorResult removeLogic(const std::string &logic_id); | ||||
| /** @brief 将逻辑在工程列表中上移或下移一位,offset 只能为 -1 或 1 */ | |||||
| LogicEditorResult moveLogic(const std::string &logic_id, int offset); | LogicEditorResult moveLogic(const std::string &logic_id, int offset); | ||||
| /** @brief 启用或停用指定控制逻辑 */ | |||||
| LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled); | LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled); | ||||
| // 网络管理 | |||||
| /** @brief 在指定逻辑末尾添加空网络 */ | |||||
| LogicEditorResult addRung(const std::string &logic_id); | LogicEditorResult addRung(const std::string &logic_id); | ||||
| /** @brief 删除指定网络 */ | |||||
| LogicEditorResult removeRung( | LogicEditorResult removeRung( | ||||
| const std::string &logic_id, const std::string &rung_id); | const std::string &logic_id, const std::string &rung_id); | ||||
| /** @brief 修改网络注释;注释长度和换行规则由领域校验约束 */ | |||||
| LogicEditorResult updateRungComment( | LogicEditorResult updateRungComment( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &comment); | const std::string &comment); | ||||
| // 条件区编辑:串联、按列插入、横线和并联分支 | // 条件区编辑:串联、按列插入、横线和并联分支 | ||||
| /** @brief 在网络条件末尾追加一个条件节点 */ | |||||
| LogicEditorResult appendCondition( | LogicEditorResult appendCondition( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| /** | |||||
| * @brief 按绝对条件列插入条件节点 | |||||
| * @param column 从 0 开始的条件列号;插入位置必须位于允许的条件区 | |||||
| */ | |||||
| LogicEditorResult insertConditionAtColumn( | LogicEditorResult insertConditionAtColumn( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| int column, | int column, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| /** | |||||
| * @brief 在直属并联分支的指定视觉列插入条件节点 | |||||
| * | |||||
| * 目标列可以是分支已有横线或 UI 投影出的补线格,服务会原子补齐必要横线 | |||||
| */ | |||||
| LogicEditorResult insertConditionInBranchAtColumn( | LogicEditorResult insertConditionInBranchAtColumn( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &branch_expression_id, | const std::string &branch_expression_id, | ||||
| int column, | int column, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| /** @brief 在网络条件末尾追加指定列宽的横线 */ | |||||
| LogicEditorResult appendWire( | LogicEditorResult appendWire( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| int column_span = 1); | int column_span = 1); | ||||
| /** @brief 在目标节点后串联插入条件节点 */ | |||||
| LogicEditorResult insertConditionAfter( | LogicEditorResult insertConditionAfter( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &target_node_id, | const std::string &target_node_id, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| /** @brief 在目标表达式后串联插入横线 */ | |||||
| LogicEditorResult insertWireAfter( | LogicEditorResult insertWireAfter( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &target_expression_id, | const std::string &target_expression_id, | ||||
| int column_span = 1); | int column_span = 1); | ||||
| /** @brief 用条件节点整体替换一条横线表达式 */ | |||||
| LogicEditorResult replaceWireWithCondition( | LogicEditorResult replaceWireWithCondition( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &wire_expression_id, | const std::string &wire_expression_id, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| /** @brief 只替换横线表达式中的一个指定列单元格 */ | |||||
| LogicEditorResult replaceWireColumnWithCondition( | LogicEditorResult replaceWireColumnWithCondition( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &wire_expression_id, | const std::string &wire_expression_id, | ||||
| int column_offset, | int column_offset, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| /** | |||||
| * @brief 将选中的条件节点建立为并联分支 | |||||
| * @param selected_node_ids 同一网络中按视觉连续范围选择的条件节点 ID | |||||
| */ | |||||
| LogicEditorResult addParallelBranch( | LogicEditorResult addParallelBranch( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::vector<std::string> &selected_node_ids, | const std::vector<std::string> &selected_node_ids, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| /** @brief 将选中的横线表达式建立为并联旁路 */ | |||||
| LogicEditorResult addParallelWireBranch( | LogicEditorResult addParallelWireBranch( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::vector<std::string> &selected_expression_ids); | const std::vector<std::string> &selected_expression_ids); | ||||
| // 按选中的横线网格建立旁路;网格必须来自同一条横线且列号连续 | |||||
| /** | |||||
| * @brief 按选中的横线网格建立并联旁路 | |||||
| * | |||||
| * 所有网格必须来自同一条横线,且列号连续;操作失败时不保留部分修改 | |||||
| */ | |||||
| LogicEditorResult addParallelWireBranchAtCells( | LogicEditorResult addParallelWireBranchAtCells( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::vector<std::pair<std::string, int>> &selected_wire_cells); | const std::vector<std::pair<std::string, int>> &selected_wire_cells); | ||||
| // 输出槽编辑;每个网络最多一个输出节点 | |||||
| /** | |||||
| * @brief 设置网络唯一的输出节点 | |||||
| * @param configured 是否将新输出标记为已完成配置 | |||||
| * | |||||
| * 输出节点固定位于网络输出槽,替换已有输出时保持一次原子编辑 | |||||
| */ | |||||
| LogicEditorResult setOutput( | LogicEditorResult setOutput( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const LogicNodeConfig &config, | const LogicNodeConfig &config, | ||||
| bool configured = false); | bool configured = false); | ||||
| // 节点属性和删除操作 | // 节点属性和删除操作 | ||||
| /** @brief 更新节点配置;条件节点和输出节点不能互相改型 */ | |||||
| LogicEditorResult updateNodeConfig( | LogicEditorResult updateNodeConfig( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &node_id, | const std::string &node_id, | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| /** @brief 删除指定节点并归一化受影响的表达式树 */ | |||||
| LogicEditorResult removeNode( | LogicEditorResult removeNode( | ||||
| const std::string &logic_id, const std::string &node_id); | const std::string &logic_id, const std::string &node_id); | ||||
| /** | |||||
| * @brief 批量删除节点 | |||||
| * @param node_ids 节点 ID 列表,不能为空且不能包含重复或不存在的 ID | |||||
| * @return 成功时作为一次编辑记录,失败时整体回滚 | |||||
| */ | |||||
| LogicEditorResult removeNodes( | LogicEditorResult removeNodes( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::vector<std::string> &node_ids); | const std::vector<std::string> &node_ids); | ||||
| /** @brief 删除指定条件表达式并归一化表达式树 */ | |||||
| LogicEditorResult removeExpression( | LogicEditorResult removeExpression( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &expression_id); | const std::string &expression_id); | ||||
| /** @brief 批量删除条件表达式,失败时整体回滚 */ | |||||
| LogicEditorResult removeExpressions( | LogicEditorResult removeExpressions( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::vector<std::string> &expression_ids); | const std::vector<std::string> &expression_ids); | ||||
| // 当前编辑会话的撤销/重做 | // 当前编辑会话的撤销/重做 | ||||
| /** @brief 判断是否存在可撤销的梯形图编辑操作 */ | |||||
| bool canUndo() const; | bool canUndo() const; | ||||
| /** @brief 判断是否存在可重做的梯形图编辑操作 */ | |||||
| bool canRedo() const; | bool canRedo() const; | ||||
| /** @brief 撤销最近一次成功的梯形图编辑操作 */ | |||||
| LogicEditorResult undo(); | LogicEditorResult undo(); | ||||
| /** @brief 重做最近一次被撤销的梯形图编辑操作 */ | |||||
| LogicEditorResult redo(); | LogicEditorResult redo(); | ||||
| /** @brief 清空撤销/重做历史,不修改当前工程内容 */ | |||||
| void clearHistory(); | void clearHistory(); | ||||
| private: | private: | ||||
| // 只保存逻辑集合快照,当前选择等 UI 会话状态不进入历史 | // 只保存逻辑集合快照,当前选择等 UI 会话状态不进入历史 | ||||
| struct HistoryState | struct HistoryState | ||||
| { | { | ||||
| std::vector<ControlLogic> logics; | |||||
| std::vector<ControlLogic> logics; // 编辑前或编辑后的完整逻辑集合 | |||||
| }; | }; | ||||
| // 捕获当前工程中的全部控制逻辑 | |||||
| HistoryState captureState() const; | HistoryState captureState() const; | ||||
| // 比较编辑前后状态并记录实际发生的修改 | |||||
| void recordHistory(HistoryState before); | void recordHistory(HistoryState before); | ||||
| // 失败时恢复工程内容和操作前脏标记,保证编辑原子性 | // 失败时恢复工程内容和操作前脏标记,保证编辑原子性 | ||||
| void rollbackEdit(HistoryState before, bool modified_before); | void rollbackEdit(HistoryState before, bool modified_before); | ||||
| // 比较两个历史快照是否完全相同 | |||||
| static bool statesEqual( | static bool statesEqual( | ||||
| const HistoryState &left, const HistoryState &right); | const HistoryState &left, const HistoryState &right); | ||||
| static bool logicsEqual( | static bool logicsEqual( | ||||
| @@ -183,17 +252,25 @@ private: | |||||
| const LogicNodeConfig &left, const LogicNodeConfig &right); | const LogicNodeConfig &left, const LogicNodeConfig &right); | ||||
| static LogicEditorResult historyFailure(const std::string &message); | static LogicEditorResult historyFailure(const std::string &message); | ||||
| // 判断节点配置是否属于条件节点 | |||||
| static bool isConditionConfig(const LogicNodeConfig &config); | static bool isConditionConfig(const LogicNodeConfig &config); | ||||
| // 判断节点配置是否属于输出节点 | |||||
| static bool isOutputConfig(const LogicNodeConfig &config); | static bool isOutputConfig(const LogicNodeConfig &config); | ||||
| // 返回节点配置对应的稳定 ID 前缀 | |||||
| static std::string nodePrefix(const LogicNodeConfig &config); | static std::string nodePrefix(const LogicNodeConfig &config); | ||||
| // 在一个控制逻辑的全部网络中生成唯一节点 ID | |||||
| static std::string makeUniqueNodeId( | static std::string makeUniqueNodeId( | ||||
| const ControlLogic &logic, const std::string &prefix); | const ControlLogic &logic, const std::string &prefix); | ||||
| // 生成唯一横线表达式 ID | |||||
| static std::string makeUniqueWireId(const ControlLogic &logic); | static std::string makeUniqueWireId(const ControlLogic &logic); | ||||
| // 生成唯一容器表达式 ID | |||||
| static std::string makeUniqueExpressionId(const ControlLogic &logic); | static std::string makeUniqueExpressionId(const ControlLogic &logic); | ||||
| // 生成唯一网络 ID | |||||
| static std::string makeUniqueRungId(const ControlLogic &logic); | static std::string makeUniqueRungId(const ControlLogic &logic); | ||||
| // 创建统一的失败结果 | |||||
| static LogicEditorResult failure( | static LogicEditorResult failure( | ||||
| LogicEditorError error, const std::string &message); | LogicEditorError error, const std::string &message); | ||||
| ProjectService &project_service_; | |||||
| EditorHistory<HistoryState> history_; | |||||
| ProjectService &project_service_; // 不拥有的工程服务依赖 | |||||
| EditorHistory<HistoryState> history_; // 当前编辑会话的撤销/重做历史 | |||||
| }; | }; | ||||
| @@ -12,25 +12,25 @@ | |||||
| // 离线扫描器的生命周期状态 | // 离线扫描器的生命周期状态 | ||||
| enum class SimulationState | enum class SimulationState | ||||
| { | { | ||||
| Stopped, | |||||
| Running, | |||||
| Faulted | |||||
| Stopped, // 未运行或已主动停止 | |||||
| Running, // 定时器正在驱动离线扫描 | |||||
| Faulted // 最近一轮扫描失败,定时器已停止 | |||||
| }; | }; | ||||
| // 启动离线仿真失败的原因 | // 启动离线仿真失败的原因 | ||||
| enum class SimulationStartError | enum class SimulationStartError | ||||
| { | { | ||||
| None, | |||||
| AlreadyRunning, | |||||
| InvalidLogic | |||||
| None, // 启动成功 | |||||
| AlreadyRunning, // 当前已经处于运行态 | |||||
| InvalidLogic // 启动前逻辑校验失败 | |||||
| }; | }; | ||||
| // 离线仿真启动结果;detail 保存领域执行器的具体校验错误 | // 离线仿真启动结果;detail 保存领域执行器的具体校验错误 | ||||
| struct SimulationStartResult | struct SimulationStartResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| SimulationStartError error = SimulationStartError::None; | |||||
| LogicScanResult detail; | |||||
| bool succeeded = false; // 是否成功启动仿真 | |||||
| SimulationStartError error = SimulationStartError::None; // 启动失败分类 | |||||
| LogicScanResult detail; // 领域执行器的详细校验或执行结果 | |||||
| }; | }; | ||||
| // 管理离线逻辑快照、固定周期扫描和实际执行状态 | // 管理离线逻辑快照、固定周期扫描和实际执行状态 | ||||
| @@ -39,40 +39,54 @@ class OfflineSimulationService final : public QObject | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| // 默认离线扫描周期,单位为毫秒 | |||||
| static constexpr int kDefaultScanIntervalMs = 50; | static constexpr int kDefaultScanIntervalMs = 50; | ||||
| /** | |||||
| * @brief 创建离线仿真服务 | |||||
| * @param repository 离线模式使用的虚拟 M/D 寄存器仓库 | |||||
| * @param parent Qt 对象父级 | |||||
| */ | |||||
| explicit OfflineSimulationService( | explicit OfflineSimulationService( | ||||
| VirtualRegisterRepository &repository, | VirtualRegisterRepository &repository, | ||||
| QObject *parent = nullptr); | QObject *parent = nullptr); | ||||
| // 复制工程逻辑作为本次会话快照并启动定时扫描 | |||||
| // 复制工程逻辑作为本次会话快照并启动定时扫描;启动失败时不进入运行态 | |||||
| SimulationStartResult start(const std::vector<ControlLogic> &logics); | SimulationStartResult start(const std::vector<ControlLogic> &logics); | ||||
| // 停止扫描但保留最近一次轨迹供 UI 显示 | |||||
| // 停止扫描并清除运行快照,但保留最近一次轨迹供 UI 显示 | |||||
| void stop(); | void stop(); | ||||
| // 立即执行一轮扫描,主要供定时器和调试调用 | |||||
| // 立即执行一轮扫描;非 Running 状态调用会返回失败结果 | |||||
| LogicScanResult executeOnce(); | LogicScanResult executeOnce(); | ||||
| // 返回当前离线仿真生命周期状态 | |||||
| SimulationState state() const; | SimulationState state() const; | ||||
| // 返回当前定时器扫描周期,单位为毫秒 | |||||
| int scanIntervalMs() const; | int scanIntervalMs() const; | ||||
| // 返回本次会话成功完成的扫描轮数 | |||||
| std::uint64_t successfulScanCount() const; | std::uint64_t successfulScanCount() const; | ||||
| // 返回最近一次扫描错误;无错误时为成功结果 | |||||
| const LogicScanResult &lastError() const; | const LogicScanResult &lastError() const; | ||||
| // 返回最近一轮按逻辑 ID 隔离的运行轨迹 | // 返回最近一轮按逻辑 ID 隔离的运行轨迹 | ||||
| const LogicTraceSnapshot &traceSnapshot() const; | const LogicTraceSnapshot &traceSnapshot() const; | ||||
| signals: | signals: | ||||
| // 仿真状态从 Stopped、Running 或 Faulted 之一切换时发出 | |||||
| void stateChanged(); | void stateChanged(); | ||||
| // 一轮扫描成功完成并更新轨迹后发出 | |||||
| void scanCompleted(); | void scanCompleted(); | ||||
| private: | private: | ||||
| // 定时器到期时触发一轮扫描 | |||||
| void handleTimeout(); | void handleTimeout(); | ||||
| // 保存错误、停止定时器并切换到 Faulted | |||||
| void enterFault(const LogicScanResult &error); | void enterFault(const LogicScanResult &error); | ||||
| VirtualRegisterRepository &repository_; | |||||
| SoftwareLogicExecutor executor_; | |||||
| QTimer timer_; | |||||
| std::vector<ControlLogic> logic_snapshot_; | |||||
| SimulationState state_ = SimulationState::Stopped; | |||||
| std::uint64_t successful_scan_count_ = 0; | |||||
| LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}}; | |||||
| LogicTraceSnapshot trace_snapshot_; | |||||
| VirtualRegisterRepository &repository_; // 不拥有的虚拟寄存器仓库 | |||||
| SoftwareLogicExecutor executor_; // 执行逻辑校验和扫描的领域执行器 | |||||
| QTimer timer_; // 按固定周期触发离线扫描 | |||||
| std::vector<ControlLogic> logic_snapshot_; // 本次仿真使用的逻辑快照 | |||||
| SimulationState state_ = SimulationState::Stopped; // 当前仿真状态 | |||||
| std::uint64_t successful_scan_count_ = 0; // 当前会话成功扫描次数 | |||||
| LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}}; // 最近一次错误 | |||||
| LogicTraceSnapshot trace_snapshot_; // 最近一次扫描的运行轨迹 | |||||
| }; | }; | ||||
| @@ -12,52 +12,58 @@ | |||||
| // PLC 串口和轮询参数;parity 使用 Qt 约定值:0 无、2 偶、3 奇 | // PLC 串口和轮询参数;parity 使用 Qt 约定值:0 无、2 偶、3 奇 | ||||
| struct PlcSerialConfiguration | struct PlcSerialConfiguration | ||||
| { | { | ||||
| std::string portName; | |||||
| int serverAddress = 1; | |||||
| int baudRate = 9600; | |||||
| int dataBits = 8; | |||||
| int parity = 2; | |||||
| int stopBits = 1; | |||||
| int responseTimeoutMs = 1000; | |||||
| int retries = 2; | |||||
| int pollIntervalMs = 200; | |||||
| std::string portName; // 串口名称,例如 COM3 | |||||
| int serverAddress = 1; // Modbus 从站地址,范围为 1~247 | |||||
| int baudRate = 9600; // 串口波特率,支持 9600、19200、38400、57600 和 115200 | |||||
| int dataBits = 8; // 数据位,支持 7 或 8 | |||||
| int parity = 2; // 校验方式,0 为无校验、2 为偶校验、3 为奇校验 | |||||
| int stopBits = 1; // 停止位,支持 1 或 2 | |||||
| int responseTimeoutMs = 1000; // 单次 Modbus 请求的响应超时时间,单位为毫秒 | |||||
| int retries = 2; // 请求失败后的重试次数,范围为 0~5 | |||||
| int pollIntervalMs = 200; // 轮询周期,单位为毫秒,范围为 50~10000 | |||||
| }; | }; | ||||
| // PLC 异步通信生命周期 | // PLC 异步通信生命周期 | ||||
| enum class PlcConnectionState | enum class PlcConnectionState | ||||
| { | { | ||||
| Disconnected, | |||||
| Connecting, | |||||
| Connected, | |||||
| Recovering, | |||||
| Faulted | |||||
| Disconnected, // 未建立 PLC 连接 | |||||
| Connecting, // 正在异步建立 PLC 连接 | |||||
| Connected, // 串口已连接并进行正常轮询,不代表首读已完成 | |||||
| Recovering, // 从可恢复通信故障中探测恢复并重新进行首读 | |||||
| Faulted // 当前通信发生故障,等待恢复探测或重新连接 | |||||
| }; | }; | ||||
| // 通信故障分类,用于状态栏提示和真机运行资格撤销 | // 通信故障分类,用于状态栏提示和真机运行资格撤销 | ||||
| enum class PlcCommunicationError | enum class PlcCommunicationError | ||||
| { | { | ||||
| None, | |||||
| SerialPortOpenFailed, | |||||
| PlcNotResponding, | |||||
| UsbSerialAdapterRemoved, | |||||
| SerialConnectionLost, | |||||
| CommunicationTimeout, | |||||
| ProtocolError, | |||||
| ReadFailed, | |||||
| WriteFailed, | |||||
| ConfigurationError, | |||||
| RequestAborted, | |||||
| Unknown | |||||
| None, // 没有待报告的通信错误 | |||||
| SerialPortOpenFailed, // 串口打开失败 | |||||
| PlcNotResponding, // PLC 未响应请求 | |||||
| UsbSerialAdapterRemoved, // USB 转串口设备被移除 | |||||
| SerialConnectionLost, // 已建立的串口连接意外中断 | |||||
| CommunicationTimeout, // 请求等待响应超时 | |||||
| ProtocolError, // 收到的 Modbus 数据不符合协议 | |||||
| ReadFailed, // 读取请求失败 | |||||
| WriteFailed, // 写入请求失败 | |||||
| ConfigurationError, // 串口或通信参数无效 | |||||
| RequestAborted, // 请求在完成前被中止 | |||||
| Unknown // 未分类的通信错误 | |||||
| }; | }; | ||||
| // 通信命令的受理结果;真正的读写完成由回调和缓存更新通知 | // 通信命令的受理结果;真正的读写完成由回调和缓存更新通知 | ||||
| struct PlcCommunicationResult | struct PlcCommunicationResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| std::string message; | |||||
| bool succeeded = false; // 命令是否被接受或参数校验是否通过 | |||||
| std::string message; // 失败原因或补充说明,成功时通常为空 | |||||
| }; | }; | ||||
| // 校验串口配置,不打开串口也不产生副作用 | |||||
| /** | |||||
| * @brief 校验 PLC 串口、Modbus 和轮询参数 | |||||
| * | |||||
| * 此函数只检查配置,不打开串口、不启动通信,也不会修改传入对象 | |||||
| * @param configuration 待校验的连接配置 | |||||
| * @return 校验通过时返回 succeeded 为 true,否则返回 false 和 UTF-8 错误说明 | |||||
| */ | |||||
| inline PlcCommunicationResult validatePlcSerialConfiguration( | inline PlcCommunicationResult validatePlcSerialConfiguration( | ||||
| const PlcSerialConfiguration &configuration) | const PlcSerialConfiguration &configuration) | ||||
| { | { | ||||
| @@ -117,19 +123,56 @@ inline PlcCommunicationResult validatePlcSerialConfiguration( | |||||
| class PlcCommunicationGateway | class PlcCommunicationGateway | ||||
| { | { | ||||
| public: | public: | ||||
| // 允许通过网关基类指针安全释放具体通信实现 | |||||
| virtual ~PlcCommunicationGateway() = default; | virtual ~PlcCommunicationGateway() = default; | ||||
| /** | |||||
| * @brief 校验串口配置并启动异步 PLC 连接 | |||||
| * @param configuration 本次连接使用的串口、Modbus 和轮询参数 | |||||
| * @return true 仅表示连接请求已受理,实际连接结果通过状态回调通知 | |||||
| */ | |||||
| virtual PlcCommunicationResult connectDevice( | virtual PlcCommunicationResult connectDevice( | ||||
| const PlcSerialConfiguration &configuration) = 0; | const PlcSerialConfiguration &configuration) = 0; | ||||
| /** | |||||
| * @brief 停止轮询并断开当前 PLC 连接 | |||||
| * | |||||
| * 断开后当前缓存视为无效,首读资格同时清除 | |||||
| */ | |||||
| virtual void disconnectDevice() = 0; | virtual void disconnectDevice() = 0; | ||||
| // 设置后续轮询的 M/D 地址集合;实现可以合并相邻地址块 | |||||
| /** | |||||
| * @brief 设置后续轮询的 M/D 地址集合 | |||||
| * | |||||
| * 实现可以校验、排序、去重并合并相邻地址块;已有读请求进行时, | |||||
| * 新集合可以延后到当前请求完成后生效 | |||||
| * @param addresses 需要周期性读回的 M/D 地址集合,空集合使用实现的默认探测地址 | |||||
| * @return true 表示集合已接受,false 表示地址或轮询资源校验失败 | |||||
| */ | |||||
| virtual PlcCommunicationResult setPollAddresses( | virtual PlcCommunicationResult setPollAddresses( | ||||
| const std::vector<RegisterAddress> &addresses) = 0; | const std::vector<RegisterAddress> &addresses) = 0; | ||||
| // 返回当前连接生命周期状态;Connected 不代表首读资格已经完成 | |||||
| virtual PlcConnectionState state() const = 0; | virtual PlcConnectionState state() const = 0; | ||||
| // 返回本次连接是否已完成全部轮询块的成功首读 | |||||
| virtual bool initialReadCompleted() const = 0; | virtual bool initialReadCompleted() const = 0; | ||||
| // 返回最近一次通信错误的分类;没有错误或错误已清除时返回 None | |||||
| virtual PlcCommunicationError lastErrorType() const = 0; | virtual PlcCommunicationError lastErrorType() const = 0; | ||||
| // 返回最近一次通信错误的 UTF-8 可读文本;没有错误时返回空字符串 | |||||
| virtual const std::string &lastError() const = 0; | virtual const std::string &lastError() const = 0; | ||||
| // 注册通信状态、首读资格、缓存更新和错误通知 | |||||
| /** | |||||
| * @brief 替换异步通信事件回调 | |||||
| * | |||||
| * 传入空 std::function 可取消对应通知;回调由实现在线程或事件循环中按事件发生时调用 | |||||
| * @param state_changed 连接状态发生变化时调用 | |||||
| * @param initial_read_changed 首读资格发生变化时调用,参数表示当前是否已完成首读 | |||||
| * @param cache_updated 任一轮询块成功更新 PLC 缓存后调用 | |||||
| * @param error_reported 发生通信错误时调用,参数为 UTF-8 可读错误文本 | |||||
| */ | |||||
| virtual void setCallbacks( | virtual void setCallbacks( | ||||
| std::function<void()> state_changed, | std::function<void()> state_changed, | ||||
| std::function<void(bool)> initial_read_changed, | std::function<void(bool)> initial_read_changed, | ||||
| @@ -11,53 +11,96 @@ | |||||
| // 自由监控列表编辑的失败分类 | // 自由监控列表编辑的失败分类 | ||||
| enum class RegisterMonitorError | enum class RegisterMonitorError | ||||
| { | { | ||||
| None, | |||||
| InvalidAddress, | |||||
| RangeOverflow, | |||||
| LimitExceeded, | |||||
| NoChange | |||||
| None, // 操作成功 | |||||
| InvalidAddress, // 起始地址格式或区域不支持 | |||||
| RangeOverflow, // 连续范围超出 M/D 地址上限 | |||||
| LimitExceeded, // 监控地址数量超过会话上限 | |||||
| NoChange // 操作没有产生任何列表变化 | |||||
| }; | }; | ||||
| // 监控列表编辑结果;affectedCount 表示本次受影响的地址数量 | // 监控列表编辑结果;affectedCount 表示本次受影响的地址数量 | ||||
| struct RegisterMonitorResult | struct RegisterMonitorResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| RegisterMonitorError error = RegisterMonitorError::None; | |||||
| std::string message; | |||||
| int affectedCount = 0; | |||||
| bool succeeded = false; // 列表编辑是否成功 | |||||
| RegisterMonitorError error = RegisterMonitorError::None; // 失败时的分类 | |||||
| std::string message; // 面向用户的 UTF-8 成功说明或失败原因 | |||||
| int affectedCount = 0; // 本次新增或删除的地址数量 | |||||
| }; | }; | ||||
| // 自由监控写入结果;底层 RegisterError 会原样保留 | // 自由监控写入结果;底层 RegisterError 会原样保留 | ||||
| struct RegisterMonitorWriteResult | struct RegisterMonitorWriteResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| RegisterError error = RegisterError::Unavailable; | |||||
| std::string message; | |||||
| bool succeeded = false; // 写入请求是否成功提交 | |||||
| RegisterError error = RegisterError::Unavailable; // 底层仓库错误分类 | |||||
| std::string message; // 面向用户的 UTF-8 错误说明 | |||||
| }; | }; | ||||
| // 编排监控模型和寄存器仓库,地址列表属于当前会话 | |||||
| /** | |||||
| * @brief 编排自由监控地址列表和寄存器仓库读写 | |||||
| * | |||||
| * 地址列表只属于当前运行会话,不写入工程文件;仓库由当前运行模式注入 | |||||
| */ | |||||
| class RegisterMonitorService | class RegisterMonitorService | ||||
| { | { | ||||
| public: | public: | ||||
| /** | |||||
| * @brief 创建自由监控服务 | |||||
| * @param repository 当前运行模式使用的寄存器仓库,不由服务拥有 | |||||
| */ | |||||
| explicit RegisterMonitorService(RegisterRepository &repository); | explicit RegisterMonitorService(RegisterRepository &repository); | ||||
| // 从起始地址连续添加 count 个同区域地址 | |||||
| /** | |||||
| * @brief 从起始地址连续添加同一区域的多个地址 | |||||
| * @param start_address 文本地址,例如 M0 或 D100 | |||||
| * @param count 连续地址数量,必须为正且不能越过 4000 | |||||
| * @return 去重后实际新增数量;格式、范围、容量或无变化时返回失败 | |||||
| */ | |||||
| RegisterMonitorResult addRange(const std::string &start_address, int count); | RegisterMonitorResult addRange(const std::string &start_address, int count); | ||||
| // 删除一批地址并通知 UI 刷新轮询集合 | |||||
| /** | |||||
| * @brief 删除一批监控地址 | |||||
| * @param addresses 待删除地址;不存在的地址会被忽略 | |||||
| * @return 实际删除数量,并在发生变化时通知轮询集合更新 | |||||
| */ | |||||
| RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses); | RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses); | ||||
| // 清空监控列表 | |||||
| /** | |||||
| * @brief 清空当前会话的监控地址列表 | |||||
| * @return 实际清除数量;列表本来为空时返回 NoChange | |||||
| */ | |||||
| RegisterMonitorResult clear(); | RegisterMonitorResult clear(); | ||||
| // 对单个 M 地址写位 | |||||
| /** | |||||
| * @brief 对单个 M 地址提交位写入 | |||||
| * @param address 必须是有效的 M 区地址 | |||||
| * @param value 要写入的位值 | |||||
| * @return 成功或包含底层 RegisterError 和用户可读文本的失败结果 | |||||
| */ | |||||
| RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value); | RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value); | ||||
| // 对单个 D 地址写字 | |||||
| /** | |||||
| * @brief 对单个 D 地址提交有符号 16 位字写入 | |||||
| * @param address 必须是有效的 D 区地址 | |||||
| * @param value 要写入的 D 字值 | |||||
| * @return 成功或包含底层 RegisterError 和用户可读文本的失败结果 | |||||
| */ | |||||
| RegisterMonitorWriteResult writeWord( | RegisterMonitorWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value); | const RegisterAddress &address, std::int16_t value); | ||||
| /** | |||||
| * @brief 返回当前会话的监控地址列表 | |||||
| * @return 按服务内部顺序保存的只读地址列表 | |||||
| */ | |||||
| const std::vector<RegisterAddress> &addresses() const; | const std::vector<RegisterAddress> &addresses() const; | ||||
| /** | |||||
| * @brief 读取监控列表的当前值并生成 UI 展示模型 | |||||
| * @param communication_fault 通信处于故障态时,将成功读回值标记为通信故障 | |||||
| * @return 每个监控地址对应一个值和可用性状态 | |||||
| */ | |||||
| std::vector<MonitorValue> values(bool communication_fault) const; | std::vector<MonitorValue> values(bool communication_fault) const; | ||||
| /** | |||||
| * @brief 设置监控地址变化通知回调 | |||||
| * @param callback 地址列表新增、删除或清空后调用;可传空函数取消通知 | |||||
| */ | |||||
| void setAddressesChangedCallback(std::function<void()> callback); | void setAddressesChangedCallback(std::function<void()> callback); | ||||
| private: | private: | ||||
| RegisterRepository &repository_; | |||||
| RegisterMonitorModel model_; | |||||
| std::function<void()> addresses_changed_callback_; | |||||
| RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有 | |||||
| RegisterMonitorModel model_; // 当前会话的去重监控地址列表 | |||||
| std::function<void()> addresses_changed_callback_; // 地址变化后的轮询刷新通知 | |||||
| }; | }; | ||||
| @@ -24,11 +24,21 @@ class RegisterRepository; | |||||
| class RuntimeModeService | class RuntimeModeService | ||||
| { | { | ||||
| public: | public: | ||||
| /** | |||||
| * @brief 创建运行模式服务 | |||||
| * @param project_service 只读工程服务,用于运行前校验和收集轮询地址 | |||||
| * @param offline_simulation_service 离线仿真服务,由应用层负责其生命周期 | |||||
| */ | |||||
| RuntimeModeService( | RuntimeModeService( | ||||
| const ProjectService &project_service, | const ProjectService &project_service, | ||||
| OfflineSimulationService &offline_simulation_service); | OfflineSimulationService &offline_simulation_service); | ||||
| /** | |||||
| * @brief 解除 PLC 网关回调绑定 | |||||
| */ | |||||
| ~RuntimeModeService(); | ~RuntimeModeService(); | ||||
| /** @brief 获取当前应用运行模式 */ | |||||
| ApplicationMode mode() const; | ApplicationMode mode() const; | ||||
| /** | /** | ||||
| * @brief 获取当前模式下允许的工程编辑和寄存器使用策略 | * @brief 获取当前模式下允许的工程编辑和寄存器使用策略 | ||||
| @@ -65,35 +75,71 @@ public: | |||||
| */ | */ | ||||
| bool initialPlcReadCompleted() const; | bool initialPlcReadCompleted() const; | ||||
| /** @brief 获取离线仿真当前状态 */ | |||||
| SimulationState simulationState() const; | SimulationState simulationState() const; | ||||
| /** @brief 获取离线仿真成功完成的扫描轮数 */ | |||||
| std::uint64_t successfulScanCount() const; | std::uint64_t successfulScanCount() const; | ||||
| /** @brief 获取离线仿真最近一次扫描错误 */ | |||||
| const LogicScanResult &simulationError() const; | const LogicScanResult &simulationError() const; | ||||
| /** @brief 返回服务持有的离线仿真服务引用 */ | |||||
| OfflineSimulationService &offlineSimulationService(); | OfflineSimulationService &offlineSimulationService(); | ||||
| // 注入 PLC 网关、活动仓库和两种实际数据源;应用生命周期内只调用一次 | |||||
| /** | |||||
| * @brief 注入 PLC 网关、活动仓库和两种实际数据源 | |||||
| * | |||||
| * 应用生命周期内只调用一次;真机模式使用 PLC 仓库,编辑/离线模式使用虚拟仓库 | |||||
| */ | |||||
| void configurePlc( | void configurePlc( | ||||
| PlcCommunicationGateway &gateway, | PlcCommunicationGateway &gateway, | ||||
| ActiveRegisterRepository &active_repository, | ActiveRegisterRepository &active_repository, | ||||
| RegisterRepository &virtual_repository, | RegisterRepository &virtual_repository, | ||||
| RegisterRepository &plc_repository); | RegisterRepository &plc_repository); | ||||
| // 连接 PLC 并开始异步通信,不阻塞 UI 等待首读 | |||||
| /** | |||||
| * @brief 连接 PLC 并开始异步通信 | |||||
| * @param configuration 串口、Modbus 和轮询配置 | |||||
| * @return 连接请求受理结果,不阻塞 UI 等待首读完成 | |||||
| */ | |||||
| PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration); | PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration); | ||||
| /** | |||||
| * @brief 设置自由监控引用的地址并刷新 PLC 轮询集合 | |||||
| * @param addresses 自由监控当前需要读取的 M/D 地址 | |||||
| */ | |||||
| void setMonitorAddresses(const std::vector<RegisterAddress> &addresses); | void setMonitorAddresses(const std::vector<RegisterAddress> &addresses); | ||||
| /** | |||||
| * @brief 汇总工程和自由监控引用并刷新 PLC 轮询地址 | |||||
| * @return 网关未配置或地址数量超限时返回失败结果 | |||||
| */ | |||||
| PlcCommunicationResult refreshPlcPollAddresses(); | PlcCommunicationResult refreshPlcPollAddresses(); | ||||
| /** | |||||
| * @brief 退出真机运行、断开 PLC 并清除首读资格 | |||||
| */ | |||||
| void disconnectPlc(); | void disconnectPlc(); | ||||
| /** @brief 获取当前 PLC 连接状态;网关未配置时返回 Disconnected */ | |||||
| PlcConnectionState plcConnectionState() const; | PlcConnectionState plcConnectionState() const; | ||||
| /** @brief 获取 PLC 最近一次通信错误文本;网关未配置时返回空字符串 */ | |||||
| const std::string &plcError() const; | const std::string &plcError() const; | ||||
| /** | |||||
| * @brief 设置 PLC 状态变化通知回调 | |||||
| * @param callback 状态、首读资格或通信错误变化时调用的函数;可传空函数取消通知 | |||||
| */ | |||||
| void setPlcStatusChangedCallback(std::function<void()> callback); | void setPlcStatusChangedCallback(std::function<void()> callback); | ||||
| private: | private: | ||||
| const ProjectService &project_service_; | |||||
| OfflineSimulationService &offline_simulation_service_; | |||||
| RuntimeState state_; | |||||
| // 表示 PLC 缓存是否已通过至少一次有效读取建立 | |||||
| const ProjectService &project_service_; // 不拥有的只读工程服务 | |||||
| OfflineSimulationService &offline_simulation_service_; // 不拥有的离线仿真服务 | |||||
| RuntimeState state_; // 编辑、离线和真机模式状态机 | |||||
| // PLC 缓存是否已通过有效首读建立,通信故障时清除 | |||||
| bool initial_plc_read_completed_ = false; | bool initial_plc_read_completed_ = false; | ||||
| PlcCommunicationGateway *plc_gateway_ = nullptr; | |||||
| ActiveRegisterRepository *active_repository_ = nullptr; | |||||
| RegisterRepository *virtual_repository_ = nullptr; | |||||
| RegisterRepository *plc_repository_ = nullptr; | |||||
| std::function<void()> plc_status_changed_callback_; | |||||
| std::vector<RegisterAddress> monitor_addresses_; | |||||
| PlcCommunicationGateway *plc_gateway_ = nullptr; // 不拥有的 PLC 通信网关 | |||||
| ActiveRegisterRepository *active_repository_ = nullptr; // 当前模式使用的仓库代理 | |||||
| RegisterRepository *virtual_repository_ = nullptr; // 离线/编辑模式的虚拟仓库 | |||||
| RegisterRepository *plc_repository_ = nullptr; // 真机模式的 PLC 缓存仓库 | |||||
| std::function<void()> plc_status_changed_callback_; // PLC 状态变化通知 | |||||
| std::vector<RegisterAddress> monitor_addresses_; // 自由监控额外引用的地址 | |||||
| }; | }; | ||||
| @@ -383,6 +383,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| bool *value) | bool *value) | ||||
| { | { | ||||
| // 根据条件节点的具体配置读取 M/D 或内部 T/C 状态,并计算触点结果 | |||||
| // 边沿触点使用 logic_id 和 node.id 组成运行时状态键,避免同名节点互相影响 | |||||
| if (value == nullptr) | if (value == nullptr) | ||||
| { | { | ||||
| return failure( | return failure( | ||||
| @@ -400,6 +402,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| using Config = std::decay_t<decltype(config)>; | using Config = std::decay_t<decltype(config)>; | ||||
| if constexpr (std::is_same_v<Config, ContactNodeConfig>) | if constexpr (std::is_same_v<Config, ContactNodeConfig>) | ||||
| { | { | ||||
| // 普通触点直接读取 M 位;常闭触点使用读回值的反值 | |||||
| const BitReadResult read = repository.readBit(config.address); | const BitReadResult read = repository.readBit(config.address); | ||||
| if (!read.succeeded) | if (!read.succeeded) | ||||
| { | { | ||||
| @@ -416,6 +419,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, EdgeContactNodeConfig>) | else if constexpr (std::is_same_v<Config, EdgeContactNodeConfig>) | ||||
| { | { | ||||
| // 边沿触点需要比较本轮与上一轮的 M 位值,只在目标边沿出现时导通 | |||||
| const BitReadResult read = repository.readBit(config.address); | const BitReadResult read = repository.readBit(config.address); | ||||
| if (!read.succeeded) | if (!read.succeeded) | ||||
| { | { | ||||
| @@ -435,18 +439,21 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>) | else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>) | ||||
| { | { | ||||
| // 定时器触点不读 PLC 地址,而是读取本地 TON 的完成状态 | |||||
| const bool done = ton_states_[config.address.index()].done; | const bool done = ton_states_[config.address.index()].done; | ||||
| *value = config.mode == ContactMode::NormallyOpen ? done : !done; | *value = config.mode == ContactMode::NormallyOpen ? done : !done; | ||||
| return success(); | return success(); | ||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | ||||
| { | { | ||||
| // 计数器触点读取本地 CTU/CTD 的完成状态 | |||||
| const bool done = counter_states_[config.address.index()].done; | const bool done = counter_states_[config.address.index()].done; | ||||
| *value = config.mode == ContactMode::NormallyOpen ? done : !done; | *value = config.mode == ContactMode::NormallyOpen ? done : !done; | ||||
| return success(); | return success(); | ||||
| } | } | ||||
| else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | ||||
| { | { | ||||
| // 字比较触点读取 D 字,再按配置的比较运算符与目标值比较 | |||||
| const WordReadResult read = repository.readWord(config.address); | const WordReadResult read = repository.readWord(config.address); | ||||
| if (!read.succeeded) | if (!read.succeeded) | ||||
| { | { | ||||
| @@ -462,6 +469,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| // 输出节点只能由 executeOutput 处理,不能作为条件触点计算 | |||||
| return failure( | return failure( | ||||
| LogicScanError::InvalidLogic, | LogicScanError::InvalidLogic, | ||||
| "条件节点不能包含输出节点", | "条件节点不能包含输出节点", | ||||
| @@ -11,75 +11,78 @@ | |||||
| #include <utility> | #include <utility> | ||||
| #include <vector> | #include <vector> | ||||
| // 一轮离线扫描可能返回的错误 | |||||
| // 一轮软件逻辑扫描可能返回的错误 | |||||
| enum class LogicScanError | enum class LogicScanError | ||||
| { | { | ||||
| None, | |||||
| InvalidLogic, | |||||
| ConflictingOutput, | |||||
| RegisterReadFailed, | |||||
| RegisterWriteFailed | |||||
| None, // 扫描成功 | |||||
| InvalidLogic, // 逻辑结构或运行配置无效 | |||||
| ConflictingOutput, // 多个输出以不兼容的线圈模式写入同一地址 | |||||
| RegisterReadFailed, // 扫描读取寄存器失败 | |||||
| RegisterWriteFailed // 扫描写入寄存器失败 | |||||
| }; | }; | ||||
| // 扫描结果;失败时携带出错的逻辑、网络和节点 ID | // 扫描结果;失败时携带出错的逻辑、网络和节点 ID | ||||
| struct LogicScanResult | struct LogicScanResult | ||||
| { | { | ||||
| bool succeeded = false; | |||||
| LogicScanError error = LogicScanError::None; | |||||
| std::string message; | |||||
| std::string logicId; | |||||
| std::string rungId; | |||||
| std::string nodeId; | |||||
| bool succeeded = false; // 本轮扫描是否完成 | |||||
| LogicScanError error = LogicScanError::None; // 失败时的分类 | |||||
| std::string message; // UTF-8 可读结果或失败原因 | |||||
| std::string logicId; // 失败关联的控制逻辑 ID | |||||
| std::string rungId; // 失败关联的网络 ID | |||||
| std::string nodeId; // 失败关联的节点 ID | |||||
| }; | }; | ||||
| // TON 节点在最近一轮扫描中的可视化状态 | // TON 节点在最近一轮扫描中的可视化状态 | ||||
| struct TonTraceValue | struct TonTraceValue | ||||
| { | { | ||||
| bool input = false; | |||||
| bool done = false; | |||||
| std::int64_t elapsedMs = 0; | |||||
| int presetMs = 0; | |||||
| bool input = false; // 本轮 TON 输入状态 | |||||
| bool done = false; // 当前是否已经到达预置时间 | |||||
| std::int64_t elapsedMs = 0; // 已累计时间,单位为毫秒 | |||||
| int presetMs = 0; // 预置时间,单位为毫秒 | |||||
| }; | }; | ||||
| // CTU/CTD 节点在最近一轮扫描中的可视化状态 | // CTU/CTD 节点在最近一轮扫描中的可视化状态 | ||||
| struct CounterTraceValue | struct CounterTraceValue | ||||
| { | { | ||||
| bool input = false; | |||||
| bool reset = false; | |||||
| bool done = false; | |||||
| std::int16_t value = 0; | |||||
| std::int16_t preset = 0; | |||||
| 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 | ||||
| { | { | ||||
| std::int16_t value = 0; | |||||
| bool overflow = false; | |||||
| std::int16_t value = 0; // MOVE/ADD/SUB 的结果值 | |||||
| bool overflow = false; // ADD/SUB 是否发生饱和溢出 | |||||
| }; | }; | ||||
| // 一条逻辑或一个网络的运行轨迹快照 | |||||
| // 一条控制逻辑的运行轨迹快照;各映射使用节点、表达式或网络 ID 作为键 | |||||
| struct LogicTraceValues | struct LogicTraceValues | ||||
| { | { | ||||
| std::unordered_map<std::string, bool> nodeValues; | |||||
| std::unordered_map<std::string, bool> nodePowerValues; | |||||
| std::unordered_map<std::string, bool> expressionValues; | |||||
| std::unordered_map<std::string, bool> expressionInputValues; | |||||
| std::unordered_map<std::string, bool> expressionPowerValues; | |||||
| std::unordered_map<std::string, bool> rungValues; | |||||
| std::unordered_map<std::string, TonTraceValue> tonValues; | |||||
| std::unordered_map<std::string, CounterTraceValue> counterValues; | |||||
| std::unordered_map<std::string, WordTraceValue> wordValues; | |||||
| std::unordered_map<std::string, bool> nodeValues; // 节点最终逻辑值 | |||||
| std::unordered_map<std::string, bool> nodePowerValues; // 节点输入电源状态 | |||||
| std::unordered_map<std::string, bool> expressionValues; // 条件表达式最终值 | |||||
| std::unordered_map<std::string, bool> expressionInputValues; // 表达式输入状态 | |||||
| std::unordered_map<std::string, bool> expressionPowerValues; // 表达式输出电源状态 | |||||
| std::unordered_map<std::string, bool> rungValues; // 网络最终逻辑值 | |||||
| std::unordered_map<std::string, TonTraceValue> tonValues; // TON 节点状态 | |||||
| std::unordered_map<std::string, CounterTraceValue> counterValues; // CTU/CTD 节点状态 | |||||
| std::unordered_map<std::string, WordTraceValue> wordValues; // MOVE/ADD/SUB 节点状态 | |||||
| // 清除当前逻辑或网络的全部轨迹 | |||||
| void clear(); | void clear(); | ||||
| }; | }; | ||||
| // 全工程轨迹;logicValues 按逻辑 ID 隔离,避免节点 ID 重复互相覆盖 | |||||
| // 全工程轨迹;logicValues 按逻辑 ID 隔离,避免不同逻辑中的节点 ID 互相覆盖 | |||||
| struct LogicTraceSnapshot : LogicTraceValues | struct LogicTraceSnapshot : LogicTraceValues | ||||
| { | { | ||||
| std::unordered_map<std::string, LogicTraceValues> logicValues; | |||||
| std::unordered_map<std::string, LogicTraceValues> logicValues; // 按逻辑 ID 保存的轨迹 | |||||
| // 清除全工程轨迹及按逻辑分组的轨迹 | |||||
| void clear(); | void clear(); | ||||
| // 返回指定逻辑的轨迹投影;不存在时返回空轨迹 | |||||
| LogicTraceSnapshot forLogic(const std::string &logic_id) const; | LogicTraceSnapshot forLogic(const std::string &logic_id) const; | ||||
| }; | }; | ||||
| @@ -90,16 +93,41 @@ public: | |||||
| using Clock = std::chrono::steady_clock; | using Clock = std::chrono::steady_clock; | ||||
| using TimePoint = Clock::time_point; | using TimePoint = Clock::time_point; | ||||
| // 在启动仿真前检查所有启用逻辑及 T/C 资源引用 | |||||
| /** | |||||
| * @brief 检查逻辑是否满足软件扫描的运行要求 | |||||
| * @param logics 待检查的控制逻辑集合 | |||||
| * @return 成功或包含逻辑、网络、节点定位信息的失败结果 | |||||
| * | |||||
| * 检查逻辑结构、启用逻辑的运行配置、共享输出冲突以及 T/C 资源引用 | |||||
| */ | |||||
| LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | ||||
| // 清除沿触发、定时器和计数器的跨扫描运行状态 | |||||
| /** | |||||
| * @brief 清除所有跨扫描运行状态 | |||||
| * | |||||
| * 重置边沿触发、TON 定时器和 CTU/CTD 计数器;不修改寄存器仓库和轨迹对象 | |||||
| */ | |||||
| void resetRuntime(); | void resetRuntime(); | ||||
| // 使用当前 steady_clock 执行一轮扫描 | |||||
| /** | |||||
| * @brief 使用当前 steady_clock 执行一轮完整扫描 | |||||
| * @param logics 按工程顺序扫描的控制逻辑集合 | |||||
| * @param repository 扫描读取和写入的寄存器仓库 | |||||
| * @param trace 可选的轨迹输出;非空时会先清空再写入本轮结果 | |||||
| * @return 扫描结果,失败时不会返回部分成功状态作为成功结果 | |||||
| */ | |||||
| LogicScanResult executeScan( | LogicScanResult executeScan( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceSnapshot *trace = nullptr); | LogicTraceSnapshot *trace = nullptr); | ||||
| // 使用指定时间执行扫描,便于稳定验证 TON 等时间逻辑 | |||||
| /** | |||||
| * @brief 使用指定时间执行一轮扫描 | |||||
| * @param logics 按工程顺序扫描的控制逻辑集合 | |||||
| * @param repository 扫描读取和写入的寄存器仓库 | |||||
| * @param now 本轮扫描使用的单调时钟时间点 | |||||
| * @param trace 可选的轨迹输出;适合测试 TON 等时间相关逻辑 | |||||
| * @return 扫描结果 | |||||
| */ | |||||
| LogicScanResult executeScanAt( | LogicScanResult executeScanAt( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| @@ -107,19 +135,38 @@ public: | |||||
| LogicTraceSnapshot *trace = nullptr); | LogicTraceSnapshot *trace = nullptr); | ||||
| private: | private: | ||||
| // 单个 TON 定时器跨扫描保存的计时状态 | |||||
| struct TonRuntimeState | struct TonRuntimeState | ||||
| { | { | ||||
| bool timing = false; | |||||
| bool done = false; | |||||
| TimePoint startedAt{}; | |||||
| std::chrono::milliseconds elapsed{0}; | |||||
| bool timing = false; // 是否正在累计输入有效时间 | |||||
| bool done = false; // 是否已经完成预置时间 | |||||
| TimePoint startedAt{}; // 本次计时开始时间 | |||||
| std::chrono::milliseconds elapsed{0}; // 最近一次计算的累计时间 | |||||
| }; | }; | ||||
| /** | |||||
| * @brief 计算单个条件节点的导通结果 | |||||
| * @param logic_id 所属控制逻辑 ID,用于隔离边沿触点的跨扫描状态 | |||||
| * @param node 待读取和计算的条件节点 | |||||
| * @param repository 提供 M/D 值的寄存器仓库;T/C 条件读取执行器内部状态 | |||||
| * @param value 输出节点导通结果,不能为空 | |||||
| * @return 成功结果,或包含节点 ID 的逻辑/寄存器读取失败结果 | |||||
| */ | |||||
| LogicScanResult evaluateCondition( | LogicScanResult evaluateCondition( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const LogicNode &node, | const LogicNode &node, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| bool *value); | bool *value); | ||||
| /** | |||||
| * @brief 递归计算串联、并联或叶子条件表达式 | |||||
| * @param logic_id 所属控制逻辑 ID,用于传递给叶子节点状态计算 | |||||
| * @param expression 待读取的条件表达式树 | |||||
| * @param repository 提供 M/D 值的寄存器仓库 | |||||
| * @param trace 可选的轨迹输出,用于记录表达式值、电源输入和电源输出 | |||||
| * @param input_power 进入当前表达式的电源状态 | |||||
| * @param value 输出表达式最终值,不能为空 | |||||
| * @return 成功结果,或包含失败节点 ID 的扫描错误 | |||||
| */ | |||||
| LogicScanResult evaluateExpression( | LogicScanResult evaluateExpression( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const ConditionExpression &expression, | const ConditionExpression &expression, | ||||
| @@ -127,6 +174,17 @@ private: | |||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool input_power, | bool input_power, | ||||
| bool *value); | bool *value); | ||||
| /** | |||||
| * @brief 在网络结果驱动下执行单个输出节点 | |||||
| * @param logic_id 所属控制逻辑 ID,用于隔离计数器等跨扫描状态 | |||||
| * @param node 待读取配置并执行的输出节点 | |||||
| * @param rung_value 当前网络的逻辑结果,决定输出是否动作 | |||||
| * @param repository 提供输出读写所需的 M/D 寄存器仓库 | |||||
| * @param now 本轮扫描的单调时钟时间点,TON 使用它计算累计时间 | |||||
| * @param trace 可选的轨迹输出,用于记录 TON、计数器和字操作结果 | |||||
| * @param output_value 输出节点最终逻辑值,不能为空 | |||||
| * @return 成功结果,或包含节点 ID 的逻辑/寄存器读写失败结果 | |||||
| */ | |||||
| LogicScanResult executeOutput( | LogicScanResult executeOutput( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const LogicNode &node, | const LogicNode &node, | ||||
| @@ -135,18 +193,30 @@ private: | |||||
| TimePoint now, | TimePoint now, | ||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool *output_value); | bool *output_value); | ||||
| /** | |||||
| * @brief 解析并读取一个字操作数 | |||||
| * @param operand 常量或 D 寄存器操作数配置 | |||||
| * @param repository 读取寄存器操作数时使用的仓库 | |||||
| * @param value 输出解析后的 int16 数值,不能为空 | |||||
| * @param node_id 关联节点 ID,用于构造可定位的失败结果 | |||||
| * @return 常量读取成功,或 D 寄存器读取/操作数类型校验失败 | |||||
| */ | |||||
| LogicScanResult readWordOperand( | LogicScanResult readWordOperand( | ||||
| const WordOperand &operand, | const WordOperand &operand, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| std::int16_t *value, | std::int16_t *value, | ||||
| const std::string &node_id) const; | const std::string &node_id) const; | ||||
| // 按逻辑 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::map<std::pair<std::string, std::string>, bool> previous_counter_inputs_; | ||||
| // 按定时器编号保存跨扫描状态 | |||||
| std::unordered_map<int, TonRuntimeState> ton_states_; | std::unordered_map<int, TonRuntimeState> ton_states_; | ||||
| // 单个计数器跨扫描保存的完成状态 | |||||
| struct CounterRuntimeState | struct CounterRuntimeState | ||||
| { | { | ||||
| bool done = false; | |||||
| bool done = false; // 是否已经达到预置值 | |||||
| }; | }; | ||||
| std::unordered_map<int, CounterRuntimeState> counter_states_; | |||||
| std::unordered_map<int, CounterRuntimeState> counter_states_; // 按计数器编号保存状态 | |||||
| }; | }; | ||||
| @@ -20,13 +20,15 @@ class AlarmConfigurationDialog; | |||||
| class AlarmEditorService; | class AlarmEditorService; | ||||
| struct AlarmDefinition; | struct AlarmDefinition; | ||||
| /** 报警定义编辑对话框;保存和删除都委托 AlarmEditorService */ | |||||
| /** 报警定义编辑对话框;保存、更新和删除都委托 AlarmEditorService */ | |||||
| class AlarmConfigurationDialog final : public QDialog | class AlarmConfigurationDialog final : public QDialog | ||||
| { | { | ||||
| public: | public: | ||||
| /** 创建报警配置对话框并绑定报警编辑服务 */ | |||||
| explicit AlarmConfigurationDialog( | explicit AlarmConfigurationDialog( | ||||
| AlarmEditorService &service, | AlarmEditorService &service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 释放 Designer 界面对象 */ | |||||
| ~AlarmConfigurationDialog() override; | ~AlarmConfigurationDialog() override; | ||||
| private: | private: | ||||
| @@ -27,9 +27,11 @@ class FreeMonitorWidget final : public QWidget | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| /** 创建自由监控控件并绑定监控服务 */ | |||||
| explicit FreeMonitorWidget( | explicit FreeMonitorWidget( | ||||
| RegisterMonitorService &service, | RegisterMonitorService &service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 释放 Designer 界面对象 */ | |||||
| ~FreeMonitorWidget() override; | ~FreeMonitorWidget() override; | ||||
| /** 真机未连接、通信故障或编辑态时应关闭写入入口 */ | /** 真机未连接、通信故障或编辑态时应关闭写入入口 */ | ||||
| @@ -27,6 +27,7 @@ class LogicEditorWidget final : public QGraphicsView | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| /** 梯形图中的条件、输出、横线和辅助选择图元类型 */ | |||||
| class NodeItem; | class NodeItem; | ||||
| class WireItem; | class WireItem; | ||||
| class WireCellItem; | class WireCellItem; | ||||
| @@ -34,11 +35,12 @@ public: | |||||
| class RungItem; | class RungItem; | ||||
| class EmptySlotItem; | class EmptySlotItem; | ||||
| /** 创建梯形图画布并绑定编辑服务 */ | |||||
| explicit LogicEditorWidget( | explicit LogicEditorWidget( | ||||
| LogicEditorService &editor_service, | LogicEditorService &editor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 切换当前显示的控制逻辑 */ | |||||
| /** 切换当前显示的控制逻辑;不存在时显示空场景 */ | |||||
| void setLogicId(const std::string &logic_id); | void setLogicId(const std::string &logic_id); | ||||
| /** 设置是否允许编辑梯形图 */ | /** 设置是否允许编辑梯形图 */ | ||||
| void setEditingEnabled(bool enabled); | void setEditingEnabled(bool enabled); | ||||
| @@ -27,9 +27,11 @@ class LogicInstructionDialog final : public QDialog | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| /** 使用已有节点配置初始化指令参数表单 */ | |||||
| explicit LogicInstructionDialog( | explicit LogicInstructionDialog( | ||||
| const LogicNodeConfig &config, | const LogicNodeConfig &config, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 释放 Designer 界面对象 */ | |||||
| ~LogicInstructionDialog() override; | ~LogicInstructionDialog() override; | ||||
| /** 返回当前表单配置,调用方仍需交给 LogicEditorService 校验 */ | /** 返回当前表单配置,调用方仍需交给 LogicEditorService 校验 */ | ||||
| @@ -1,7 +1,7 @@ | |||||
| /** | /** | ||||
| * @file main_window.h | * @file main_window.h | ||||
| * @brief 定义综合平台编程器主窗口及运行模式界面协调逻辑 | * @brief 定义综合平台编程器主窗口及运行模式界面协调逻辑 | ||||
| * @version 0.2.0 | |||||
| * @version 1.0.0 | |||||
| * @author suyu | * @author suyu | ||||
| * @date 2026-08-08 | * @date 2026-08-08 | ||||
| */ | */ | ||||
| @@ -76,6 +76,11 @@ public: | |||||
| RegisterCommentService ®ister_comment_service, | RegisterCommentService ®ister_comment_service, | ||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** | |||||
| * @brief 创建主窗口,并使用调用方提供的 HMI 页面导航服务 | |||||
| * | |||||
| * 两个构造函数的区别只在导航服务的所有权;其余服务均由应用入口管理 | |||||
| */ | |||||
| MainWindow( | MainWindow( | ||||
| RuntimeModeService &runtime_mode_service, | RuntimeModeService &runtime_mode_service, | ||||
| ProjectService &project_service, | ProjectService &project_service, | ||||
| @@ -89,6 +94,7 @@ public: | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 释放窗口拥有的控制器和可选导航服务 */ | |||||
| ~MainWindow() override; | ~MainWindow() override; | ||||
| /** 返回当前选中的 HMI 页面标识 */ | /** 返回当前选中的 HMI 页面标识 */ | ||||
| @@ -24,9 +24,11 @@ class PlcConnectionDialog final : public QDialog | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| /** 使用现有配置初始化 PLC 参数表单 */ | |||||
| explicit PlcConnectionDialog( | explicit PlcConnectionDialog( | ||||
| const PlcSerialConfiguration &configuration, | const PlcSerialConfiguration &configuration, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 释放 Designer 界面对象 */ | |||||
| ~PlcConnectionDialog() override; | ~PlcConnectionDialog() override; | ||||
| /** 返回表单中的串口配置,连接前由网关再次校验 */ | /** 返回表单中的串口配置,连接前由网关再次校验 */ | ||||
| @@ -48,6 +48,7 @@ public: | |||||
| /** 通知主窗口编辑状态已经发生变化 */ | /** 通知主窗口编辑状态已经发生变化 */ | ||||
| using EditStateChanged = std::function<void()>; | using EditStateChanged = std::function<void()>; | ||||
| /** 创建工程树控制器并绑定主窗口服务和编辑器 */ | |||||
| ProjectWorkspaceController( | ProjectWorkspaceController( | ||||
| QWidget &parent, | QWidget &parent, | ||||
| Ui::MainWindow &ui, | Ui::MainWindow &ui, | ||||
| @@ -97,9 +98,9 @@ private: | |||||
| /** 工程树项目的三种业务类型 */ | /** 工程树项目的三种业务类型 */ | ||||
| enum class ItemKind | enum class ItemKind | ||||
| { | { | ||||
| Root = 0, | |||||
| HmiPage = 1, | |||||
| ControlLogic = 2 | |||||
| Root = 0, // 工程树根节点 | |||||
| HmiPage = 1, // HMI 页面节点 | |||||
| ControlLogic = 2 // 控制逻辑节点 | |||||
| }; | }; | ||||
| /** 处理工程树选择变化并刷新当前编辑器 */ | /** 处理工程树选择变化并刷新当前编辑器 */ | ||||
| @@ -42,6 +42,7 @@ public: | |||||
| using StatusReporter = std::function<void( | using StatusReporter = std::function<void( | ||||
| const QString &message, int timeout_ms)>; | const QString &message, int timeout_ms)>; | ||||
| /** 创建属性面板控制器并注入编辑服务和选择状态 */ | |||||
| PropertyPanelController( | PropertyPanelController( | ||||
| QWidget &parent, | QWidget &parent, | ||||
| Ui::MainWindow &ui, | Ui::MainWindow &ui, | ||||
| @@ -25,9 +25,11 @@ class RegisterCommentService; | |||||
| class RegisterCommentDialog final : public QDialog | class RegisterCommentDialog final : public QDialog | ||||
| { | { | ||||
| public: | public: | ||||
| /** 创建寄存器注释对话框并绑定注释服务 */ | |||||
| explicit RegisterCommentDialog( | explicit RegisterCommentDialog( | ||||
| RegisterCommentService &service, | RegisterCommentService &service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 释放 Designer 界面对象 */ | |||||
| ~RegisterCommentDialog() override; | ~RegisterCommentDialog() override; | ||||
| private: | private: | ||||
| @@ -38,6 +38,7 @@ class RuntimeMonitorWidget final : public QWidget | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| /** 创建组合 HMI、梯形图和自由监控的运行投影 */ | |||||
| RuntimeMonitorWidget( | RuntimeMonitorWidget( | ||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| @@ -47,6 +48,7 @@ public: | |||||
| AlarmService &alarm_service, | AlarmService &alarm_service, | ||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** 释放组合的运行监控子控件 */ | |||||
| ~RuntimeMonitorWidget() override; | ~RuntimeMonitorWidget() override; | ||||
| /** 将当前页面和控制逻辑绑定到运行监控界面 */ | /** 将当前页面和控制逻辑绑定到运行监控界面 */ | ||||
| @@ -25,7 +25,9 @@ class RuntimeMonitorWindow final : public QMainWindow | |||||
| Q_OBJECT | Q_OBJECT | ||||
| public: | public: | ||||
| /** 创建运行监控顶层窗口 */ | |||||
| explicit RuntimeMonitorWindow(QWidget *parent = nullptr); | explicit RuntimeMonitorWindow(QWidget *parent = nullptr); | ||||
| /** 释放窗口和 Designer 界面对象 */ | |||||
| ~RuntimeMonitorWindow() override; | ~RuntimeMonitorWindow() override; | ||||
| /** 将运行监控控件放入窗口中央区域 */ | /** 将运行监控控件放入窗口中央区域 */ | ||||
| @@ -45,6 +45,7 @@ public: | |||||
| /** 请求上层退出当前运行态 */ | /** 请求上层退出当前运行态 */ | ||||
| using RuntimeExitRequester = std::function<void()>; | using RuntimeExitRequester = std::function<void()>; | ||||
| /** 创建运行面板控制器并注入运行模式、编辑器和监控窗口依赖 */ | |||||
| RuntimePanelController( | RuntimePanelController( | ||||
| QWidget &parent, | QWidget &parent, | ||||
| RuntimeModeService &runtime_mode_service, | RuntimeModeService &runtime_mode_service, | ||||
| @@ -64,6 +65,7 @@ public: | |||||
| StatusReporter status_reporter, | StatusReporter status_reporter, | ||||
| OutputReporter output_reporter, | OutputReporter output_reporter, | ||||
| RuntimeExitRequester runtime_exit_requester); | RuntimeExitRequester runtime_exit_requester); | ||||
| /** 停止刷新并释放控制器拥有的运行监控窗口 */ | |||||
| ~RuntimePanelController(); | ~RuntimePanelController(); | ||||
| /** 连接运行监控相关信号并完成初始配置 */ | /** 连接运行监控相关信号并完成初始配置 */ | ||||
| @@ -13,55 +13,55 @@ | |||||
| /** 工具栏中使用的语义图标类型 */ | /** 工具栏中使用的语义图标类型 */ | ||||
| enum class UiIcon | enum class UiIcon | ||||
| { | { | ||||
| Edit, | |||||
| RunOffline, | |||||
| RunOnline, | |||||
| NewDocument, | |||||
| Save, | |||||
| SaveAs, | |||||
| Open, | |||||
| Undo, | |||||
| Redo, | |||||
| Exit, | |||||
| PlcSettings, | |||||
| Disconnect, | |||||
| RegisterComments, | |||||
| HmiButton, | |||||
| Indicator, | |||||
| NumericDisplay, | |||||
| NumericInput, | |||||
| ProgressBar, | |||||
| Text, | |||||
| PageJump, | |||||
| AlarmList, | |||||
| AlarmSettings, | |||||
| Delete, | |||||
| AddRung, | |||||
| HorizontalWire, | |||||
| VerticalWire, | |||||
| ParallelBranch, | |||||
| NormallyOpenContact, | |||||
| NormallyClosedContact, | |||||
| RisingEdgeContact, | |||||
| FallingEdgeContact, | |||||
| TimerContact, | |||||
| CounterContact, | |||||
| Coil, | |||||
| SetCoil, | |||||
| ResetCoil, | |||||
| Ton, | |||||
| Ctu, | |||||
| Ctd, | |||||
| Move, | |||||
| Add, | |||||
| Subtract, | |||||
| Compare, | |||||
| Comment, | |||||
| More, | |||||
| HmiPage, | |||||
| Logic, | |||||
| Runtime, | |||||
| ClearList | |||||
| Edit, // 编辑模式 | |||||
| RunOffline, // 离线运行 | |||||
| RunOnline, // 真机运行 | |||||
| NewDocument, // 新建工程 | |||||
| Save, // 保存工程 | |||||
| SaveAs, // 另存工程 | |||||
| Open, // 打开工程 | |||||
| Undo, // 撤销 | |||||
| Redo, // 重做 | |||||
| Exit, // 退出程序 | |||||
| PlcSettings, // PLC 连接设置 | |||||
| Disconnect, // 断开 PLC | |||||
| RegisterComments, // 寄存器注释 | |||||
| HmiButton, // HMI 按钮 | |||||
| Indicator, // HMI 指示灯 | |||||
| NumericDisplay, // HMI 数值显示 | |||||
| NumericInput, // HMI 数值输入 | |||||
| ProgressBar, // HMI 进度条 | |||||
| Text, // HMI 文本 | |||||
| PageJump, // HMI 页面跳转 | |||||
| AlarmList, // HMI 报警列表 | |||||
| AlarmSettings, // 报警配置 | |||||
| Delete, // 删除 | |||||
| AddRung, // 新增网络 | |||||
| HorizontalWire, // 横线 | |||||
| VerticalWire, // 竖线 | |||||
| ParallelBranch, // 并联支路 | |||||
| NormallyOpenContact, // 常开触点 | |||||
| NormallyClosedContact, // 常闭触点 | |||||
| RisingEdgeContact, // 上升沿触点 | |||||
| FallingEdgeContact, // 下降沿触点 | |||||
| TimerContact, // 定时器触点 | |||||
| CounterContact, // 计数器触点 | |||||
| Coil, // 普通线圈 | |||||
| SetCoil, // 置位线圈 | |||||
| ResetCoil, // 复位线圈 | |||||
| Ton, // TON 定时器 | |||||
| Ctu, // CTU 加计数器 | |||||
| Ctd, // CTD 减计数器 | |||||
| Move, // MOVE 指令 | |||||
| Add, // ADD 指令 | |||||
| Subtract, // SUB 指令 | |||||
| Compare, // 比较指令 | |||||
| Comment, // 注释 | |||||
| More, // 更多操作 | |||||
| HmiPage, // HMI 页面 | |||||
| Logic, // 控制逻辑 | |||||
| Runtime, // 运行监控 | |||||
| ClearList // 清空列表 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -1,32 +0,0 @@ | |||||
| include(pri/test_defaults.pri) | |||||
| include(pri/test_layers.pri) | |||||
| TARGET = main_window_tests | |||||
| QT += widgets serialport testlib | |||||
| SOURCES += \ | |||||
| main_window_tests.cpp \ | |||||
| $$UI_MAIN_SOURCES \ | |||||
| $$DOMAIN_ALL_SOURCES \ | |||||
| $$SERVICE_PROJECT_SOURCES \ | |||||
| $$SERVICE_ALARM_SOURCES \ | |||||
| $$SERVICE_HMI_SOURCES \ | |||||
| $$SERVICE_LOGIC_SOURCES \ | |||||
| $$SERVICE_OFFLINE_SOURCES \ | |||||
| $$SERVICE_RUNTIME_SOURCES \ | |||||
| $$SERVICE_MONITOR_SOURCES | |||||
| HEADERS += \ | |||||
| $$UI_MAIN_HEADERS \ | |||||
| $$DOMAIN_ALL_HEADERS \ | |||||
| $$SERVICE_PROJECT_HEADERS \ | |||||
| $$SERVICE_ALARM_HEADERS \ | |||||
| $$SERVICE_HMI_HEADERS \ | |||||
| $$SERVICE_LOGIC_HEADERS \ | |||||
| $$SERVICE_OFFLINE_HEADERS \ | |||||
| $$SERVICE_RUNTIME_HEADERS \ | |||||
| $$SERVICE_MONITOR_HEADERS \ | |||||
| ../src/services/plc_communication_gateway.h \ | |||||
| $$TEST_SUPPORT_HEADERS | |||||
| FORMS += $$UI_MAIN_FORMS | |||||
| @@ -132,45 +132,3 @@ INFRASTRUCTURE_PLC_HEADERS = \ | |||||
| ../src/infrastructure/plc_communication_error_classifier.h \ | ../src/infrastructure/plc_communication_error_classifier.h \ | ||||
| ../src/infrastructure/plc_register_repository.h \ | ../src/infrastructure/plc_register_repository.h \ | ||||
| ../src/infrastructure/plc_communication_service.h | ../src/infrastructure/plc_communication_service.h | ||||
| UI_MAIN_SOURCES = \ | |||||
| ../src/ui/main_window.cpp \ | |||||
| ../src/ui/logic_instruction_dialog.cpp \ | |||||
| ../src/ui/alarm_configuration_dialog.cpp \ | |||||
| ../src/ui/register_comment_dialog.cpp \ | |||||
| ../src/ui/plc_connection_dialog.cpp \ | |||||
| ../src/ui/free_monitor_widget.cpp \ | |||||
| ../src/ui/runtime_monitor_window.cpp \ | |||||
| ../src/ui/runtime_monitor_widget.cpp \ | |||||
| ../src/ui/toolbar_icon_factory.cpp \ | |||||
| ../src/ui/project_workspace_controller.cpp \ | |||||
| ../src/ui/property_panel_controller.cpp \ | |||||
| ../src/ui/runtime_panel_controller.cpp \ | |||||
| ../src/ui/hmi_editor_widget.cpp \ | |||||
| ../src/ui/logic_editor_widget.cpp | |||||
| UI_MAIN_HEADERS = \ | |||||
| ../src/ui/main_window.h \ | |||||
| ../src/ui/logic_instruction_dialog.h \ | |||||
| ../src/ui/alarm_configuration_dialog.h \ | |||||
| ../src/ui/register_comment_dialog.h \ | |||||
| ../src/ui/plc_connection_dialog.h \ | |||||
| ../src/ui/free_monitor_widget.h \ | |||||
| ../src/ui/runtime_monitor_window.h \ | |||||
| ../src/ui/runtime_monitor_widget.h \ | |||||
| ../src/ui/toolbar_icon_factory.h \ | |||||
| ../src/ui/project_workspace_controller.h \ | |||||
| ../src/ui/property_panel_controller.h \ | |||||
| ../src/ui/runtime_panel_controller.h \ | |||||
| ../src/ui/hmi_editor_widget.h \ | |||||
| ../src/ui/logic_editor_widget.h | |||||
| UI_MAIN_FORMS = \ | |||||
| ../src/ui/main_window.ui \ | |||||
| ../src/ui/logic_instruction_dialog.ui \ | |||||
| ../src/ui/alarm_configuration_dialog.ui \ | |||||
| ../src/ui/register_comment_dialog.ui \ | |||||
| ../src/ui/plc_connection_dialog.ui \ | |||||
| ../src/ui/free_monitor_widget.ui \ | |||||
| ../src/ui/runtime_monitor_window.ui \ | |||||
| ../src/ui/runtime_monitor_widget.ui | |||||
| @@ -1,4 +1,4 @@ | |||||
| # 测试总入口:只负责编排目标,不把不同环境的测试揉成一个可执行文件 | |||||
| # 功能测试总入口:性能测试使用 performance_tests.pro 单独运行 | |||||
| TEMPLATE = subdirs | TEMPLATE = subdirs | ||||
| CONFIG += ordered | CONFIG += ordered | ||||
| @@ -11,9 +11,7 @@ SUBDIRS += \ | |||||
| project_management \ | project_management \ | ||||
| register_monitor \ | register_monitor \ | ||||
| runtime_mode \ | runtime_mode \ | ||||
| plc_runtime \ | |||||
| main_window \ | |||||
| performance | |||||
| plc_runtime | |||||
| domain.file = domain_tests.pro | domain.file = domain_tests.pro | ||||
| alarm_service.file = alarm_service_tests.pro | alarm_service.file = alarm_service_tests.pro | ||||
| @@ -24,5 +22,3 @@ project_management.file = project_management_tests.pro | |||||
| register_monitor.file = register_monitor_service_tests.pro | register_monitor.file = register_monitor_service_tests.pro | ||||
| runtime_mode.file = runtime_mode_service_tests.pro | runtime_mode.file = runtime_mode_service_tests.pro | ||||
| plc_runtime.file = plc_runtime_tests.pro | plc_runtime.file = plc_runtime_tests.pro | ||||
| main_window.file = main_window_tests.pro | |||||
| performance.file = performance_tests.pro | |||||
| @@ -2,7 +2,8 @@ | |||||
| param( | param( | ||||
| [ValidateSet('Debug', 'Release')] | [ValidateSet('Debug', 'Release')] | ||||
| [string]$Configuration = 'Debug', | [string]$Configuration = 'Debug', | ||||
| [switch]$SkipPerformance | |||||
| [ValidateSet('Functional', 'Performance', 'All')] | |||||
| [string]$Suite = 'Functional' | |||||
| ) | ) | ||||
| Set-StrictMode -Version Latest | Set-StrictMode -Version Latest | ||||
| @@ -23,7 +24,7 @@ foreach ($requiredPath in @($qmakePath, $makePath, $testSourceRoot)) { | |||||
| } | } | ||||
| } | } | ||||
| $targets = @( | |||||
| $functionalTargets = @( | |||||
| 'domain_tests', | 'domain_tests', | ||||
| 'alarm_service_tests', | 'alarm_service_tests', | ||||
| 'hmi_editor_service_tests', | 'hmi_editor_service_tests', | ||||
| @@ -32,17 +33,20 @@ $targets = @( | |||||
| 'project_management_tests', | 'project_management_tests', | ||||
| 'register_monitor_service_tests', | 'register_monitor_service_tests', | ||||
| 'runtime_mode_service_tests', | 'runtime_mode_service_tests', | ||||
| 'plc_runtime_tests', | |||||
| 'main_window_tests' | |||||
| 'plc_runtime_tests' | |||||
| ) | ) | ||||
| if (-not $SkipPerformance) { | |||||
| $targets += 'performance_tests' | |||||
| $performanceTargets = @('performance_tests') | |||||
| $targets = switch ($Suite) { | |||||
| 'Functional' { $functionalTargets } | |||||
| 'Performance' { $performanceTargets } | |||||
| 'All' { $functionalTargets + $performanceTargets } | |||||
| } | } | ||||
| $originalPath = $env:Path | $originalPath = $env:Path | ||||
| $originalPlatform = $env:QT_QPA_PLATFORM | |||||
| $env:Path = "$qtRoot\bin;$mingwBin;$env:Path" | $env:Path = "$qtRoot\bin;$mingwBin;$env:Path" | ||||
| $env:QT_QPA_PLATFORM = 'offscreen' | |||||
| Write-Host "测试类型:$Suite" | |||||
| try { | try { | ||||
| foreach ($target in $targets) { | foreach ($target in $targets) { | ||||
| @@ -68,10 +72,19 @@ try { | |||||
| } | } | ||||
| Write-Host "运行 $target" | Write-Host "运行 $target" | ||||
| & $executablePath | |||||
| if ($target -eq 'performance_tests') { | |||||
| $benchmarkResultPath = Join-Path $buildDirectory 'benchmark.csv' | |||||
| & $executablePath '-o' "$benchmarkResultPath,csv" | |||||
| } else { | |||||
| & $executablePath | |||||
| } | |||||
| if ($LASTEXITCODE -ne 0) { | if ($LASTEXITCODE -ne 0) { | ||||
| throw "测试失败:$target,退出码:$LASTEXITCODE" | throw "测试失败:$target,退出码:$LASTEXITCODE" | ||||
| } | } | ||||
| if ($target -eq 'performance_tests') { | |||||
| Write-Host "性能结果:$benchmarkResultPath" | |||||
| Get-Content -LiteralPath $benchmarkResultPath -Encoding utf8 | |||||
| } | |||||
| } | } | ||||
| finally { | finally { | ||||
| Pop-Location | Pop-Location | ||||
| @@ -80,9 +93,4 @@ try { | |||||
| } | } | ||||
| finally { | finally { | ||||
| $env:Path = $originalPath | $env:Path = $originalPath | ||||
| if ($null -eq $originalPlatform) { | |||||
| Remove-Item Env:QT_QPA_PLATFORM -ErrorAction SilentlyContinue | |||||
| } else { | |||||
| $env:QT_QPA_PLATFORM = $originalPlatform | |||||
| } | |||||
| } | } | ||||