| @@ -23,6 +23,7 @@ ui_*.h | |||
| *.ilk | |||
| *.exp | |||
| *.manifest | |||
| *.autosave | |||
| # Qt Creator local user settings | |||
| *.pro.user | |||
| @@ -63,3 +64,5 @@ Desktop.ini | |||
| 设计方案书.doc | |||
| /学习 | |||
| /json | |||
| /代码 | |||
| /other_version | |||
| @@ -8,19 +8,19 @@ | |||
| // 报警触发条件:M 位为 1,或 D 字高于/低于阈值 | |||
| enum class AlarmCondition | |||
| { | |||
| MOn, | |||
| DHigh, | |||
| DLow | |||
| MOn, // M 位为 1 时触发 | |||
| DHigh, // D 寄存器值高于阈值时触发 | |||
| DLow // D 寄存器值低于阈值时触发 | |||
| }; | |||
| // 可保存的报警定义;运行时 AlarmService 根据此定义生成报警记录 | |||
| 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; | |||
| @@ -604,6 +604,7 @@ bool LogicNode::isConfigured() const | |||
| return configured; | |||
| } | |||
| // 判断当前节点是不是条件触点类节点 | |||
| bool LogicNode::isCondition() const | |||
| { | |||
| return std::holds_alternative<ContactNodeConfig>(config) | |||
| @@ -613,6 +614,7 @@ bool LogicNode::isCondition() const | |||
| || std::holds_alternative<CompareNodeConfig>(config); | |||
| } | |||
| // 判断当前节点是不是输出执行类节点 | |||
| bool LogicNode::isOutput() const | |||
| { | |||
| return std::holds_alternative<CoilNodeConfig>(config) | |||
| @@ -12,103 +12,114 @@ | |||
| // 普通触点的导通方式;常闭触点会对读取到的位值取反 | |||
| enum class ContactMode | |||
| { | |||
| NormallyOpen, | |||
| NormallyClosed | |||
| NormallyOpen, // 常开:位值为 1 时触点导通 | |||
| NormallyClosed // 常闭:位值为 0 时触点导通 | |||
| }; | |||
| // 线圈写入方式:普通写入、置位保持、复位清零 | |||
| enum class CoilMode | |||
| { | |||
| Normal, | |||
| Set, | |||
| Reset | |||
| Normal, // 普通写入:直接写入当前逻辑结果 | |||
| Set, // 置位:逻辑结果为 1 时保持为 1 | |||
| Reset // 复位:逻辑结果为 1 时清零 | |||
| }; | |||
| // 上升沿/下降沿触点的边沿方向 | |||
| enum class EdgeMode | |||
| { | |||
| Rising, | |||
| Falling | |||
| Rising, // 上升沿:信号从 0 变成 1 | |||
| Falling // 下降沿:信号从 1 变成 0 | |||
| }; | |||
| // D 字比较节点支持的比较运算 | |||
| // D 寄存器字比较,等于、不等于、大于、小于等 6 种比较 | |||
| enum class ComparisonOperator | |||
| { | |||
| Equal, | |||
| NotEqual, | |||
| LessThan, | |||
| LessThanOrEqual, | |||
| GreaterThan, | |||
| GreaterThanOrEqual | |||
| Equal, // 等于 | |||
| NotEqual, // 不等于 | |||
| LessThan, // 小于 | |||
| LessThanOrEqual, // 小于或等于 | |||
| GreaterThan, // 大于 | |||
| GreaterThanOrEqual // 大于或等于 | |||
| }; | |||
| // 字操作数可以来自常量,也可以来自 D 寄存器 | |||
| enum class WordOperandKind | |||
| { | |||
| Constant, | |||
| Register | |||
| Constant, // 使用固定数值 | |||
| Register // 使用 D 寄存器中的数值 | |||
| }; | |||
| // MOVE、ADD/SUB、计数器预置值共用的字操作数 | |||
| 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 地址 | |||
| bool validate(std::string *error = nullptr) const; | |||
| }; | |||
| // 普通 M 寄存器触点,地址 + 常开常闭 | |||
| struct ContactNodeConfig | |||
| { | |||
| RegisterAddress address{RegisterArea::M, 0}; | |||
| ContactMode mode = ContactMode::NormallyOpen; | |||
| RegisterAddress address{RegisterArea::M, 0}; // 触点读取的 M 地址 | |||
| ContactMode mode = ContactMode::NormallyOpen; // 常开或常闭 | |||
| }; | |||
| // 边沿触点,地址 + 上升 / 下降沿 | |||
| struct EdgeContactNodeConfig | |||
| { | |||
| // 用于检测 M 寄存器的上升沿或下降沿 | |||
| RegisterAddress address{RegisterArea::M, 0}; | |||
| EdgeMode mode = EdgeMode::Rising; | |||
| }; | |||
| // 线圈输出配置,决定如何写入 M 寄存器 | |||
| struct CoilNodeConfig | |||
| { | |||
| // 线圈要写入的 M 寄存器地址 | |||
| RegisterAddress address{RegisterArea::M, 0}; | |||
| CoilMode mode = CoilMode::Normal; | |||
| }; | |||
| // D 寄存器比较配置 | |||
| struct CompareNodeConfig | |||
| { | |||
| // 要参与比较的 D 寄存器地址 | |||
| RegisterAddress address{RegisterArea::D, 0}; | |||
| // 比较运算符 | |||
| ComparisonOperator comparison = ComparisonOperator::Equal; | |||
| // 与寄存器值比较的常量 | |||
| std::int16_t value = 0; | |||
| }; | |||
| // 离线执行器内部使用的 T 区地址,不会加入 PLC M/D 轮询 | |||
| // 定时器 T 编号,内部使用,不映射 M/D 寄存器;限制编号范围,判断是否合法,可转字符串 | |||
| class TimerAddress | |||
| { | |||
| 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: | |||
| int index_ = 0; | |||
| int index_ = 0; // 保存 T 地址编号 | |||
| }; | |||
| // 读取 TON 完成状态的条件触点 | |||
| struct TimerContactNodeConfig | |||
| { | |||
| // 要读取完成状态的定时器地址 | |||
| TimerAddress address; | |||
| // 按常开或常闭方式判断完成状态 | |||
| ContactMode mode = ContactMode::NormallyOpen; | |||
| }; | |||
| @@ -116,32 +127,34 @@ struct TimerContactNodeConfig | |||
| class CounterAddress | |||
| { | |||
| 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: | |||
| int index_ = 0; | |||
| int index_ = 0; // 保存 C 地址编号 | |||
| }; | |||
| enum class CounterMode | |||
| { | |||
| Up, | |||
| Down | |||
| Up, // 加计数 | |||
| Down // 减计数 | |||
| }; | |||
| // 读取计数器完成状态的条件触点 | |||
| struct CounterContactNodeConfig | |||
| { | |||
| // 要读取完成状态的计数器地址 | |||
| CounterAddress address; | |||
| // 按常开或常闭方式判断完成状态 | |||
| ContactMode mode = ContactMode::NormallyOpen; | |||
| }; | |||
| @@ -151,54 +164,54 @@ struct TonNodeConfig | |||
| static constexpr int kMinimumPresetMs = 1; | |||
| static constexpr int kMaximumPresetMs = 86400000; | |||
| TimerAddress address; | |||
| int presetMs = 1000; | |||
| TimerAddress address; // 定时器地址 | |||
| int presetMs = 1000; // 延时时间,单位为毫秒 | |||
| }; | |||
| // CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D | |||
| 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 地址 | |||
| struct MoveNodeConfig | |||
| { | |||
| WordOperand source; | |||
| RegisterAddress destination{RegisterArea::D, 0}; | |||
| WordOperand source; // 要写入的源操作数 | |||
| RegisterAddress destination{RegisterArea::D, 0}; // 接收数据的 D 地址 | |||
| }; | |||
| enum class ArithmeticOperation | |||
| { | |||
| Add, | |||
| Subtract | |||
| Add, // 加法 | |||
| Subtract // 减法 | |||
| }; | |||
| // ADD/SUB 输出:计算两个字操作数并写入目标 D 地址 | |||
| 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< | |||
| 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 地址;复合输出请使用下面的收集函数 | |||
| std::optional<RegisterAddress> registerAddressForLogicNode( | |||
| @@ -211,9 +224,9 @@ void collectRegisterAddressesForLogicNode( | |||
| // 表达式中的一个指令节点;configured=false 表示编辑中的未完成草稿 | |||
| struct LogicNode | |||
| { | |||
| std::string id; | |||
| LogicNodeConfig config; | |||
| bool configured = true; | |||
| std::string id; // 节点唯一 ID | |||
| LogicNodeConfig config; // 节点的具体指令配置 | |||
| bool configured = true; // 是否已完成配置 | |||
| // 校验节点 ID、配置类型和配置内容 | |||
| bool validate(std::string *error = nullptr) const; | |||
| @@ -225,10 +238,10 @@ struct LogicNode | |||
| enum class ConditionExpressionKind | |||
| { | |||
| Node, | |||
| Wire, | |||
| Series, | |||
| Parallel | |||
| Node, // 一个实际的条件节点 | |||
| Wire, // 一段横线 | |||
| Series, // 多个条件串联,必须全部满足 | |||
| Parallel // 多个条件并联,满足任意一条即可 | |||
| }; | |||
| // 条件区中的持久化横线;columnSpan 表示跨越的网格列数 | |||
| @@ -238,7 +251,7 @@ struct WireSegment | |||
| static constexpr int kMaximumColumnSpan = | |||
| ProjectLimits::kMaximumConditionColumns; | |||
| int columnSpan = 1; | |||
| int columnSpan = 1; // 横线占用的网格列数 | |||
| bool validate(std::string *error = nullptr) const; | |||
| }; | |||
| @@ -246,13 +259,15 @@ struct WireSegment | |||
| // 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接 | |||
| 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 fromWire(std::string id, int column_span = 1); | |||
| // 编辑态校验允许空配置节点,但必须保持树结构合法 | |||
| bool validate(std::string *error = nullptr) const; | |||
| @@ -260,29 +275,41 @@ struct ConditionExpression | |||
| bool validateForRunning(std::string *error = nullptr) const; | |||
| }; | |||
| // 删除后折叠单子项容器并合并相邻同类容器,保持表达式的规范形态 | |||
| // 删除节点后整理表达式结构 | |||
| void normalizeConditionExpression(std::optional<ConditionExpression> *expression); | |||
| // 按节点 ID 查找条件节点,只读版本 | |||
| const LogicNode *findConditionNode( | |||
| const ConditionExpression &expression, const std::string &node_id); | |||
| // 按节点 ID 查找条件节点,可修改版本 | |||
| LogicNode *findConditionNode( | |||
| ConditionExpression &expression, const std::string &node_id); | |||
| // 按表达式 ID 查找子表达式,只读版本 | |||
| const ConditionExpression *findConditionExpression( | |||
| const ConditionExpression &expression, const std::string &expression_id); | |||
| // 按表达式 ID 查找子表达式,可修改版本 | |||
| ConditionExpression *findConditionExpression( | |||
| ConditionExpression &expression, const std::string &expression_id); | |||
| // 收集表达式中的所有条件节点 | |||
| void collectConditionNodes( | |||
| const ConditionExpression &expression, std::vector<const LogicNode *> *nodes); | |||
| // 收集表达式和子表达式的所有 ID | |||
| void collectConditionExpressionIds( | |||
| const ConditionExpression &expression, std::vector<std::string> *ids); | |||
| // 条件表达式为空时表示恒真网络,输出指令固定在最右侧 | |||
| 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; | |||
| @@ -295,10 +322,10 @@ struct LadderRung | |||
| // 一组按顺序扫描的梯形图网络;enabled=false 时运行校验会跳过它 | |||
| 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; | |||
| @@ -17,9 +17,9 @@ | |||
| */ | |||
| enum class HmiBindingKind | |||
| { | |||
| None, | |||
| Bit, | |||
| Word | |||
| None, // 不支持寄存器绑定 | |||
| Bit, // 绑定 M 区位地址 | |||
| Word // 绑定 D 区字地址 | |||
| }; | |||
| /** | |||
| @@ -27,9 +27,9 @@ enum class HmiBindingKind | |||
| */ | |||
| enum class HmiRuntimeValueKind | |||
| { | |||
| None, | |||
| Bit, | |||
| Word | |||
| None, // 运行态不读取寄存器值 | |||
| Bit, // 运行态读取绑定的 M 位值 | |||
| Word // 运行态读取绑定的 D 字值 | |||
| }; | |||
| /** | |||
| @@ -37,15 +37,15 @@ enum class HmiRuntimeValueKind | |||
| */ | |||
| 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 各自写数字 | |||
| 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 { | |||
| // 故障恢复时,两次轻量探测之间等待的时间 | |||
| constexpr int kRecoveryProbeIntervalMs = 2000; | |||
| // Qt 使用 QString,项目内部错误信息统一使用 UTF-8 std::string | |||
| std::string toUtf8(const QString &value) | |||
| { | |||
| const QByteArray bytes = value.toUtf8(); | |||
| return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | |||
| } | |||
| // M 区在 Modbus 中按线圈读取,D 区按保持寄存器读取 | |||
| QModbusDataUnit::RegisterType registerType(RegisterArea area) | |||
| { | |||
| return area == RegisterArea::M | |||
| ? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters; | |||
| } | |||
| // 只有超时和 PLC 不响应属于可以自动探测恢复的故障 | |||
| bool isRecoverableTimeout(PlcCommunicationError error) | |||
| { | |||
| return error == PlcCommunicationError::PlcNotResponding | |||
| || error == PlcCommunicationError::CommunicationTimeout; | |||
| } | |||
| // Connected 正常轮询,Recovering 正在恢复后的重新首读 | |||
| bool isReadingState(PlcConnectionState state) | |||
| { | |||
| return state == PlcConnectionState::Connected | |||
| @@ -46,6 +51,7 @@ bool normalizePollAddresses( | |||
| std::vector<RegisterAddress> *normalized, | |||
| std::string *error) | |||
| { | |||
| // 先复制,后续排序和去重不能改变调用方传入的地址列表 | |||
| *normalized = addresses; | |||
| if (std::any_of( | |||
| normalized->cbegin(), normalized->cend(), | |||
| @@ -76,6 +82,7 @@ std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses) | |||
| { | |||
| if (addresses.empty()) | |||
| { | |||
| // 空集合也会保留 M0、D0 两个默认读块 | |||
| return 2U; | |||
| } | |||
| std::size_t blocks = 0U; | |||
| @@ -84,6 +91,7 @@ std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses) | |||
| int count = 0; | |||
| for (const RegisterAddress &address : addresses) | |||
| { | |||
| // 跨区域、出现地址间隔或达到 Modbus 单次上限时,必须开始新读块 | |||
| if (count == 0 | |||
| || address.area() != current_area | |||
| || address.index() > start_address + count | |||
| @@ -111,6 +119,7 @@ PlcCommunicationService::PlcCommunicationService( | |||
| repository_(repository), | |||
| master_(std::make_unique<QModbusRtuSerialMaster>()) | |||
| { | |||
| // 仓库只负责缓存;写入请求通过这里注入的回调转给通信服务 | |||
| repository_.setWriteHandlers( | |||
| [this](const RegisterAddress &address, bool value) | |||
| { | |||
| @@ -120,6 +129,7 @@ PlcCommunicationService::PlcCommunicationService( | |||
| { | |||
| return sendWordWrite(address, value); | |||
| }); | |||
| // 轮询定时器每次只推动一个读块,避免一次压入大量异步请求 | |||
| connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock); | |||
| recovery_timer_.setSingleShot(true); | |||
| connect( | |||
| @@ -127,12 +137,14 @@ PlcCommunicationService::PlcCommunicationService( | |||
| &QTimer::timeout, | |||
| this, | |||
| &PlcCommunicationService::probeRecovery); | |||
| // Qt 串口状态变化是异步通知,所有连接状态都从这里统一处理 | |||
| connect(master_.get(), &QModbusClient::stateChanged, | |||
| this, | |||
| [this](QModbusDevice::State device_state) | |||
| { | |||
| if (device_state == QModbusDevice::ConnectedState) | |||
| { | |||
| // 串口连接成功后立即开始轮询,并不代表首读已经完成 | |||
| serial_session_opened_ = true; | |||
| setState(PlcConnectionState::Connected); | |||
| poll_timer_.start(configuration_.pollIntervalMs); | |||
| @@ -145,6 +157,7 @@ PlcCommunicationService::PlcCommunicationService( | |||
| } | |||
| else if (device_state == QModbusDevice::UnconnectedState) | |||
| { | |||
| // 主动断开和意外断线要区分,后者需要报告故障 | |||
| if (disconnecting_) | |||
| { | |||
| serial_session_opened_ = false; | |||
| @@ -159,6 +172,7 @@ PlcCommunicationService::PlcCommunicationService( | |||
| } | |||
| } | |||
| }); | |||
| // Qt 报告底层错误时,转换为项目自己的错误和状态 | |||
| connect(master_.get(), &QModbusClient::errorOccurred, | |||
| this, | |||
| [this](QModbusDevice::Error error) | |||
| @@ -198,6 +212,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||
| received_valid_response_ = false; | |||
| last_error_type_ = PlcCommunicationError::None; | |||
| last_error_.clear(); | |||
| // 把项目配置转换成 Qt 串口参数 | |||
| master_->setConnectionParameter( | |||
| QModbusDevice::SerialPortNameParameter, | |||
| QString::fromStdString(configuration.portName)); | |||
| @@ -234,6 +249,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||
| void PlcCommunicationService::disconnectDevice() | |||
| { | |||
| // 关闭串口、清除缓存有效标记,防止断开后继续使用旧值 | |||
| closeSerialSession(); | |||
| repository_.invalidate(); | |||
| updateInitialReadCompleted(false, true); | |||
| @@ -258,6 +274,7 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( | |||
| // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复 | |||
| if (pending_reply_ != nullptr) | |||
| { | |||
| // 当前回复仍在解析旧集合,等它结束后再切换到新集合 | |||
| pending_poll_addresses_ = std::move(normalized); | |||
| poll_update_pending_ = true; | |||
| return {true, {}}; | |||
| @@ -297,10 +314,11 @@ void PlcCommunicationService::setCallbacks( | |||
| std::function<void()> cache_updated, | |||
| 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() | |||
| @@ -308,6 +326,7 @@ void PlcCommunicationService::rebuildPollBlocks() | |||
| std::vector<RegisterAddress> addresses = poll_addresses_; | |||
| if (addresses.empty()) | |||
| { | |||
| // 没有指定地址时保留 M0/D0,保证连接后仍能验证设备是否响应 | |||
| addresses = { | |||
| RegisterAddress{RegisterArea::M, 0}, | |||
| RegisterAddress{RegisterArea::D, 0}}; | |||
| @@ -349,6 +368,7 @@ void PlcCommunicationService::rebuildPollBlocks() | |||
| next_poll_block_ = 0; | |||
| if (!initial_read_completed_) | |||
| { | |||
| // 地址集合变化或重新连接后,首读资格从头统计 | |||
| initial_blocks_read_.assign(poll_blocks_.size(), false); | |||
| } | |||
| else | |||
| @@ -360,6 +380,7 @@ void PlcCommunicationService::rebuildPollBlocks() | |||
| void PlcCommunicationService::applyPollAddresses( | |||
| const std::vector<RegisterAddress> &addresses) | |||
| { | |||
| // 只有在没有读请求占用时,新的地址集合才会真正生效 | |||
| poll_addresses_ = addresses; | |||
| rebuildPollBlocks(); | |||
| poll_update_pending_ = false; | |||
| @@ -390,6 +411,7 @@ void PlcCommunicationService::pollNextBlock() | |||
| return; | |||
| } | |||
| pending_reply_ = reply; | |||
| // 把当前连接代次带进回调,防止断线重连后旧回复误更新新连接 | |||
| const std::uint64_t generation = connection_generation_; | |||
| connect(reply, &QModbusReply::finished, | |||
| this, | |||
| @@ -424,6 +446,7 @@ void PlcCommunicationService::pollNextBlock() | |||
| } | |||
| if (pending_reply_ == reply) | |||
| { | |||
| // 只有当前指针仍指向本次回复时才清空,避免误清新请求 | |||
| pending_reply_ = nullptr; | |||
| } | |||
| reply->deleteLater(); | |||
| @@ -437,6 +460,7 @@ void PlcCommunicationService::pollNextBlock() | |||
| void PlcCommunicationService::probeRecovery() | |||
| { | |||
| // 只在可恢复的超时故障中探测;串口拔出等故障不会反复探测 | |||
| if (state_ != PlcConnectionState::Faulted | |||
| || !isRecoverableTimeout(last_error_type_)) | |||
| { | |||
| @@ -448,6 +472,7 @@ void PlcCommunicationService::probeRecovery() | |||
| } | |||
| if (pending_reply_ != nullptr) | |||
| { | |||
| // 已有读请求在途,稍后再探测,保证同一时间只有一个读回复 | |||
| recovery_timer_.start(kRecoveryProbeIntervalMs); | |||
| return; | |||
| } | |||
| @@ -455,6 +480,7 @@ void PlcCommunicationService::probeRecovery() | |||
| { | |||
| return; | |||
| } | |||
| // 恢复探测只读取一个地址,确认链路恢复后再进行完整首读 | |||
| const PollBlock block = poll_blocks_.front(); | |||
| const QModbusDataUnit request( | |||
| registerType(block.area), block.startAddress, 1); | |||
| @@ -521,6 +547,7 @@ void PlcCommunicationService::handleRecoveryProbeFailure(QModbusDevice::Error er | |||
| void PlcCommunicationService::restoreCommunication() | |||
| { | |||
| // 探测成功只说明链路恢复,仍要重新读取全部读块才能恢复真机资格 | |||
| received_valid_response_ = true; | |||
| last_error_type_ = PlcCommunicationError::None; | |||
| last_error_.clear(); | |||
| @@ -568,6 +595,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock | |||
| RegisterWriteResult PlcCommunicationService::sendBitWrite( | |||
| const RegisterAddress &address, bool value) | |||
| { | |||
| // 写入只允许在正常 Connected 状态进行,Recovering/Faulted 都拒绝 | |||
| if (state_ != PlcConnectionState::Connected) | |||
| { | |||
| return {false, RegisterError::Unavailable}; | |||
| @@ -576,6 +604,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite( | |||
| { | |||
| return {false, RegisterError::WriteRejected}; | |||
| } | |||
| // M 区对应 Modbus Coils,单次只写一个地址 | |||
| QModbusDataUnit unit(QModbusDataUnit::Coils, address.index(), 1); | |||
| unit.setValue(0, value ? 1U : 0U); | |||
| QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | |||
| @@ -585,6 +614,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite( | |||
| return {false, RegisterError::WriteRejected}; | |||
| } | |||
| pending_write_reply_ = reply; | |||
| // 写回复完成后只处理成功或错误,不直接改缓存;后续轮询负责确认真实值 | |||
| connect(reply, &QModbusReply::finished, | |||
| this, | |||
| [this, reply, generation = connection_generation_] | |||
| @@ -610,6 +640,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite( | |||
| RegisterWriteResult PlcCommunicationService::sendWordWrite( | |||
| const RegisterAddress &address, std::int16_t value) | |||
| { | |||
| // D 区写入流程与 M 区相同,只是 Modbus 类型不同 | |||
| if (state_ != PlcConnectionState::Connected) | |||
| { | |||
| return {false, RegisterError::Unavailable}; | |||
| @@ -618,6 +649,7 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite( | |||
| { | |||
| return {false, RegisterError::WriteRejected}; | |||
| } | |||
| // D 区对应 Modbus HoldingRegisters,单次只写一个字 | |||
| QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 1); | |||
| unit.setValue(0, static_cast<quint16>(value)); | |||
| QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | |||
| @@ -656,6 +688,7 @@ void PlcCommunicationService::updateInitialReadCompleted( | |||
| { | |||
| return; | |||
| } | |||
| // 只有状态真的变化,或调用方明确要求通知时才发出信号 | |||
| initial_read_completed_ = completed; | |||
| emit initialReadCompletedChanged(completed); | |||
| if (initial_read_changed_callback_) | |||
| @@ -666,6 +699,7 @@ void PlcCommunicationService::updateInitialReadCompleted( | |||
| void PlcCommunicationService::handleUnexpectedDisconnect() | |||
| { | |||
| // 这里表示设备原本连上过,后来串口意外断开 | |||
| serial_session_opened_ = false; | |||
| const QString port_name = QString::fromStdString(configuration_.portName).trimmed(); | |||
| setError({ | |||
| @@ -677,6 +711,7 @@ void PlcCommunicationService::handleUnexpectedDisconnect() | |||
| void PlcCommunicationService::handleModbusError(QModbusDevice::Error error) | |||
| { | |||
| // 主动断开、已处理的故障和旧回复错误都不重复上报 | |||
| if (disconnecting_ | |||
| || (error == QModbusDevice::ReplyAbortedError | |||
| && state_ == PlcConnectionState::Disconnected) | |||
| @@ -695,6 +730,7 @@ void PlcCommunicationService::handleModbusError(QModbusDevice::Error error) | |||
| void PlcCommunicationService::closeSerialSession() | |||
| { | |||
| // 先递增代次并停止定时器,再断开串口;旧异步回调会因此失效 | |||
| ++connection_generation_; | |||
| disconnecting_ = true; | |||
| poll_timer_.stop(); | |||
| @@ -718,6 +754,7 @@ void PlcCommunicationService::setState(PlcConnectionState state) | |||
| { | |||
| return; | |||
| } | |||
| // 状态集中从这里修改,确保 Qt 信号和 std::function 回调同步触发 | |||
| state_ = state; | |||
| emit stateChanged(); | |||
| if (state_changed_callback_) | |||
| @@ -728,6 +765,7 @@ void PlcCommunicationService::setState(PlcConnectionState state) | |||
| void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | |||
| { | |||
| // 统一保存错误、停止正常轮询、撤销首读资格并通知 UI | |||
| last_error_type_ = failure.type; | |||
| last_error_ = toUtf8(failure.message); | |||
| poll_timer_.stop(); | |||
| @@ -754,3 +792,5 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | |||
| recovery_timer_.start(kRecoveryProbeIntervalMs); | |||
| } | |||
| } | |||
| // 按区域和地址排序,方便后面合并连续读块 | |||
| // 相同区域和编号的地址只保留一个 | |||
| @@ -24,22 +24,33 @@ class PlcCommunicationService final : public QObject, public PlcCommunicationGat | |||
| Q_OBJECT | |||
| public: | |||
| // 创建通信服务,并把 PLC 缓存仓库交给它管理 | |||
| explicit PlcCommunicationService( | |||
| PlcRegisterRepository &repository, | |||
| QObject *parent = nullptr); | |||
| // 释放 Modbus 主站和未完成的通信资源 | |||
| ~PlcCommunicationService() override; | |||
| // 按给定串口参数连接 PLC,并开始异步轮询 | |||
| PlcCommunicationResult connectDevice( | |||
| const PlcSerialConfiguration &configuration) override; | |||
| // 停止轮询并断开 PLC | |||
| void disconnectDevice() override; | |||
| // 设置需要周期性读取的 M/D 地址集合 | |||
| PlcCommunicationResult setPollAddresses( | |||
| const std::vector<RegisterAddress> &addresses) override; | |||
| // 返回当前连接状态 | |||
| PlcConnectionState state() const override; | |||
| // 判断本次连接是否已经完成所有轮询块的首次读取 | |||
| bool initialReadCompleted() const override; | |||
| // 返回最近一次通信错误的类型 | |||
| PlcCommunicationError lastErrorType() const override; | |||
| // 返回最近一次通信错误的文字 | |||
| const std::string &lastError() const override; | |||
| // 返回当前使用的串口和 Modbus 参数 | |||
| const PlcSerialConfiguration &configuration() const; | |||
| // 注册状态、首读、缓存更新和错误通知回调 | |||
| void setCallbacks( | |||
| std::function<void()> state_changed, | |||
| std::function<void(bool)> initial_read_changed, | |||
| @@ -47,18 +58,22 @@ public: | |||
| std::function<void(const std::string &)> error_reported) override; | |||
| signals: | |||
| // 连接状态发生变化 | |||
| void stateChanged(); | |||
| // 首次完整读取资格发生变化 | |||
| void initialReadCompletedChanged(bool completed); | |||
| // PLC 缓存收到新的成功读数 | |||
| void cacheUpdated(); | |||
| // 通信错误的可读提示文字 | |||
| void communicationError(const QString &message); | |||
| private: | |||
| struct PollBlock | |||
| { | |||
| // 同一区域的一段连续 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(); | |||
| // Faulted 状态下用轻量探测判断通信是否恢复 | |||
| void probeRecovery(); | |||
| // 处理恢复探测失败,并安排下一次探测 | |||
| void handleRecoveryProbeFailure(QModbusDevice::Error error); | |||
| // 探测成功后恢复轮询,但仍需重新完成首读 | |||
| void restoreCommunication(); | |||
| // 处理读回复并把值写入 PLC 缓存 | |||
| void handleReadFinished(QModbusReply *reply, PollBlock block); | |||
| @@ -79,36 +96,41 @@ private: | |||
| const RegisterAddress &address, std::int16_t value); | |||
| // 更新“所有轮询块均成功读取”的真机进入资格 | |||
| void updateInitialReadCompleted(bool completed, bool force_notification = false); | |||
| // 处理没有主动断开时发生的串口断线 | |||
| void handleUnexpectedDisconnect(); | |||
| // 把 Qt Modbus 错误转换成项目自己的通信错误 | |||
| void handleModbusError(QModbusDevice::Error error); | |||
| // 关闭串口并清理当前连接的请求和状态 | |||
| void closeSerialSession(); | |||
| // 修改连接状态并通知外部观察者 | |||
| void setState(PlcConnectionState state); | |||
| // 保存错误信息、更新状态并通知外部观察者 | |||
| 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 后才允许进入真机 | |||
| 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; | |||
| private: | |||
| // M/D 区共用的数组长度,覆盖 0 到最大地址 | |||
| static constexpr std::size_t kRegisterCount = | |||
| 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 "ui/main_window.h" | |||
| // 程序从这里开始,argc 和 argv 保存用户启动程序时附带的命令行参数 | |||
| int main(int argc, char *argv[]) | |||
| { | |||
| // 创建整个 Qt 应用,后面的窗口、按钮和消息循环都要依靠它 | |||
| QApplication application(argc, argv); | |||
| // 设置程序名称,Qt 的窗口信息和配置中会用到这个名字 | |||
| application.setApplicationName(QObject::tr("综合平台编程器")); | |||
| // 设置组织名称,用来区分这个程序保存的 Qt 配置 | |||
| application.setOrganizationName(QStringLiteral("QtProXinJe")); | |||
| // 组合根负责创建具体实现并将抽象依赖注入服务和 UI | |||
| // 负责把工程保存到 JSON 文件,也负责从 JSON 文件读取工程 | |||
| JsonProjectStorage project_storage; | |||
| // 统一管理新建、打开、保存和校验工程,实际读写文件交给上面的对象 | |||
| ProjectService project_service(project_storage); | |||
| // 处理 HMI 页面和控件的添加、修改、删除、撤销与重做 | |||
| HmiEditorService hmi_editor_service(project_service); | |||
| // 处理运行画面在不同 HMI 页面之间的跳转 | |||
| HmiNavigationService hmi_navigation_service(project_service); | |||
| // 处理梯形图网络和节点的编辑、校验、撤销与重做 | |||
| LogicEditorService logic_editor_service(project_service); | |||
| // 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现 | |||
| // 在内存中保存离线仿真使用的 M、D 寄存器值,不会写入真实 PLC | |||
| VirtualRegisterRepository virtual_register_repository; | |||
| // 保存从真实 PLC 最近一次读取到的 M、D 值,并转交写入请求 | |||
| PlcRegisterRepository plc_register_repository; | |||
| // 给界面提供统一的寄存器入口,启动时先使用上面的离线寄存器 | |||
| ActiveRegisterRepository active_register_repository(virtual_register_repository); | |||
| // 负责通过串口异步连接、读取和写入真实 PLC | |||
| PlcCommunicationService plc_communication_service(plc_register_repository); | |||
| // 处理运行画面读取和写入寄存器,数据来自当前选中的寄存器入口 | |||
| HmiRuntimeService hmi_runtime_service(active_register_repository); | |||
| // 处理报警规则的添加、修改和删除 | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| // 根据当前寄存器值判断报警是否触发,并处理报警确认 | |||
| AlarmService alarm_service(project_service, active_register_repository); | |||
| // 管理工程中 M、D 地址对应的文字备注 | |||
| RegisterCommentService register_comment_service(project_service); | |||
| // 处理自由监控列表中的寄存器读取和单点写入 | |||
| RegisterMonitorService register_monitor_service(active_register_repository); | |||
| // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 | |||
| OfflineSimulationService offline_simulation_service(virtual_register_repository); | |||
| // 管理编辑、离线运行和真机运行三种模式,并控制离线仿真的启停 | |||
| RuntimeModeService runtime_mode_service( | |||
| project_service, offline_simulation_service); | |||
| // 把 PLC 通信和两套寄存器接入运行模式,切换模式时才能切换数据来源 | |||
| runtime_mode_service.configurePlc( | |||
| plc_communication_service, | |||
| active_register_repository, | |||
| virtual_register_repository, | |||
| plc_register_repository); | |||
| // 创建主窗口,并把界面操作需要的各项服务交给它使用 | |||
| MainWindow main_window( | |||
| runtime_mode_service, | |||
| project_service, | |||
| @@ -67,7 +90,9 @@ int main(int argc, char *argv[]) | |||
| hmi_navigation_service, | |||
| register_comment_service, | |||
| register_monitor_service); | |||
| // 把主窗口显示到屏幕上 | |||
| main_window.show(); | |||
| // 启动 Qt 消息循环,持续响应鼠标、键盘、定时器和串口事件,窗口关闭后才返回 | |||
| return application.exec(); | |||
| } | |||
| @@ -21,11 +21,13 @@ void AlarmService::refresh() | |||
| for (const AlarmDefinition &definition | |||
| : project_service_.project().alarmDefinitions) | |||
| { | |||
| // 空值表示寄存器读取失败;保留原记录,等待下一轮成功读回 | |||
| const std::optional<bool> active = evaluate(definition); | |||
| if (!active.has_value()) | |||
| { | |||
| continue; | |||
| } | |||
| // 同一报警定义在当前会话中最多保留一条活动记录 | |||
| const auto current = std::find_if( | |||
| records_.begin(), records_.end(), | |||
| [&definition](const AlarmRecord &record) | |||
| @@ -34,12 +36,14 @@ void AlarmService::refresh() | |||
| }); | |||
| if (*active && current == records_.end()) | |||
| { | |||
| // 首次检测到触发时创建未确认记录,并置于列表头部 | |||
| records_.insert( | |||
| records_.begin(), | |||
| AlarmRecord{definition.id, definition.message, false, now}); | |||
| } | |||
| else if (!*active && current != records_.end()) | |||
| { | |||
| // 条件恢复后立即移除对应记录,下一次再次触发会生成新记录 | |||
| records_.erase(current); | |||
| } | |||
| } | |||
| @@ -76,21 +80,27 @@ std::optional<bool> AlarmService::evaluate( | |||
| { | |||
| if (definition.condition == AlarmCondition::MOn) | |||
| { | |||
| // MOn 报警直接读取 M 位;读取失败返回空值,交由 refresh 保留旧记录 | |||
| const BitReadResult value = repository_.readBit(definition.address); | |||
| return value.succeeded ? std::optional<bool>{value.value} : std::nullopt; | |||
| } | |||
| // DHigh 和 DLow 报警读取 D 字,比较方向由定义中的条件决定 | |||
| const WordReadResult value = repository_.readWord(definition.address); | |||
| if (!value.succeeded) | |||
| { | |||
| // 通信或仓库读取失败不等同于报警解除 | |||
| return std::nullopt; | |||
| } | |||
| if (definition.condition == AlarmCondition::DHigh) | |||
| { | |||
| // 达到阈值即触发高限报警 | |||
| return value.value >= definition.threshold; | |||
| } | |||
| if (definition.condition == AlarmCondition::DLow) | |||
| { | |||
| // 降到阈值即触发低限报警 | |||
| return value.value <= definition.threshold; | |||
| } | |||
| // 防御未来新增但尚未实现的报警条件 | |||
| return std::nullopt; | |||
| } | |||
| @@ -9,36 +9,66 @@ class ProjectService; | |||
| class RegisterRepository; | |||
| struct AlarmDefinition; | |||
| // 运行时产生的报警记录,不会写入工程 JSON | |||
| // 运行时产生的报警记录,只存在当前会话,不会写入工程 JSON | |||
| 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 | |||
| { | |||
| public: | |||
| /** | |||
| * @brief 创建报警服务 | |||
| * @param project_service 只读工程服务,提供报警定义集合 | |||
| * @param repository 当前运行模式使用的寄存器仓库 | |||
| */ | |||
| AlarmService( | |||
| const ProjectService &project_service, | |||
| RegisterRepository &repository); | |||
| // 重新评估全部定义;仍处于触发状态的记录会保留确认状态 | |||
| /** | |||
| * @brief 重新评估全部报警定义 | |||
| * | |||
| * 仍处于触发状态的记录会保留确认状态;读取失败时跳过该定义并保留原记录。 | |||
| * 新触发记录插入列表头部,解除触发的记录立即移除 | |||
| */ | |||
| void refresh(); | |||
| // 确认指定报警;不存在时返回 false | |||
| /** | |||
| * @brief 确认当前会话中的指定报警记录 | |||
| * @param definition_id 报警定义的稳定 ID | |||
| * @return 找到当前活动记录并完成确认时返回 true,否则返回 false | |||
| */ | |||
| bool acknowledge(const std::string &definition_id); | |||
| // 清空当前运行会话的报警记录 | |||
| /** | |||
| * @brief 清空当前运行会话的全部报警记录和确认状态 | |||
| */ | |||
| void reset(); | |||
| // 返回按触发时间保存的报警记录 | |||
| /** | |||
| * @brief 返回当前会话的报警记录 | |||
| * @return 按触发时间倒序排列的只读记录列表,列表由服务持有 | |||
| */ | |||
| const std::vector<AlarmRecord> &records() const; | |||
| private: | |||
| /** | |||
| * @brief 使用当前寄存器值评估单条报警定义 | |||
| * @param definition 待评估的报警定义 | |||
| * @return true 表示触发,false 表示未触发;读取失败时返回空值 | |||
| * | |||
| * MOn 比较 M 位是否为 1;DHigh 使用大于等于阈值,DLow 使用小于等于阈值 | |||
| */ | |||
| 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 保存有限数量的编辑前快照并提供撤销、重做 | |||
| * | |||
| * 历史只存在当前编辑会话内。新编辑会清空重做栈,超过容量时丢弃最早记录。 | |||
| * State 由具体编辑服务决定,可以是 HMI 页面集合或控制逻辑集合 | |||
| * 历史只存在当前编辑会话内,不写入工程文件 | |||
| * 新编辑会清空重做栈,超过容量时丢弃最早记录 | |||
| */ | |||
| template <typename State> | |||
| class EditorHistory final | |||
| { | |||
| public: | |||
| // 单个编辑服务最多保留 100 条撤销和重做历史 | |||
| static constexpr std::size_t kMaximumEntries = 100U; | |||
| template <typename Equal> | |||
| // 只有前后状态不同才记录;新编辑会清空重做栈 | |||
| /** | |||
| * @brief 记录一次编辑前状态 | |||
| * @param before 编辑前状态 | |||
| * @param after 编辑完成后的当前状态 | |||
| * @param equal 比较前后状态是否相同的可调用对象 | |||
| * | |||
| * 状态没有变化时不创建空历史 | |||
| * 新编辑成功记录后清空重做栈 | |||
| */ | |||
| void record(State before, const State &after, Equal equal) | |||
| { | |||
| if (equal(before, after)) | |||
| @@ -29,7 +40,13 @@ public: | |||
| redo_states_.clear(); | |||
| } | |||
| // 返回撤销后的目标状态,并把当前状态放入重做栈 | |||
| /** | |||
| * @brief 撤销最近一次编辑 | |||
| * @param current 当前状态 | |||
| * @return 撤销后的目标状态;没有可撤销历史时返回空值 | |||
| * | |||
| * 当前状态会先进入重做栈,之后取出撤销栈顶的编辑前快照 | |||
| */ | |||
| std::optional<State> undo(State current) | |||
| { | |||
| if (undo_states_.empty()) | |||
| @@ -43,7 +60,13 @@ public: | |||
| return target; | |||
| } | |||
| // 返回重做后的目标状态,并把当前状态放回撤销栈 | |||
| /** | |||
| * @brief 重做最近一次被撤销的编辑 | |||
| * @param current 当前状态 | |||
| * @return 重做后的目标状态;没有可重做历史时返回空值 | |||
| * | |||
| * 当前状态会先放回撤销栈,之后取出重做栈顶的目标状态 | |||
| */ | |||
| std::optional<State> redo(State current) | |||
| { | |||
| if (redo_states_.empty()) | |||
| @@ -57,23 +80,33 @@ public: | |||
| return target; | |||
| } | |||
| /** | |||
| * @brief 清空撤销栈和重做栈 | |||
| */ | |||
| void clear() | |||
| { | |||
| undo_states_.clear(); | |||
| redo_states_.clear(); | |||
| } | |||
| /** | |||
| * @brief 判断是否存在可撤销历史 | |||
| */ | |||
| bool canUndo() const | |||
| { | |||
| return !undo_states_.empty(); | |||
| } | |||
| /** | |||
| * @brief 判断是否存在可重做历史 | |||
| */ | |||
| bool canRedo() const | |||
| { | |||
| return !redo_states_.empty(); | |||
| } | |||
| private: | |||
| // 保持历史不超过上限,超出时从最早的记录开始丢弃 | |||
| static void trim(std::vector<State> *states) | |||
| { | |||
| 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 | |||
| { | |||
| 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 | |||
| { | |||
| 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 | |||
| { | |||
| public: | |||
| /** | |||
| * @brief 创建 HMI 编辑服务 | |||
| * @param project_service 用于读取和原子修改当前工程的项目服务 | |||
| */ | |||
| explicit HmiEditorService(ProjectService &project_service); | |||
| /** | |||
| @@ -82,13 +86,52 @@ public: | |||
| * @return 已有或新建页面的成功结果及其页面标识 | |||
| */ | |||
| HmiEditorResult ensureDefaultPage(); | |||
| /** | |||
| * @brief 添加一个 HMI 页面 | |||
| * @param name 页面显示名称,不能为空且必须在工程内唯一 | |||
| * @return 成功时返回新页面 ID | |||
| */ | |||
| HmiEditorResult addPage(const std::string &name); | |||
| /** | |||
| * @brief 调整页面尺寸 | |||
| * @param page_id 目标页面唯一标识 | |||
| * @param width 页面宽度,必须满足页面边界且不能使已有控件越界 | |||
| * @param height 页面高度,必须满足页面边界且不能使已有控件越界 | |||
| * @return 页面不存在或新尺寸无效时返回失败结果 | |||
| */ | |||
| HmiEditorResult resizePage( | |||
| const std::string &page_id, int width, int height); | |||
| /** | |||
| * @brief 修改页面显示名称 | |||
| * @param page_id 目标页面唯一标识 | |||
| * @param name 新页面名称,不能为空且必须在工程内唯一 | |||
| * @return 页面不存在或名称无效/重复时返回失败结果 | |||
| */ | |||
| HmiEditorResult renamePage( | |||
| const std::string &page_id, const std::string &name); | |||
| /** | |||
| * @brief 删除一个 HMI 页面 | |||
| * @param page_id 待删除页面唯一标识 | |||
| * @return 页面不存在、页面是唯一页面、仍为初始页面或仍被引用时返回失败结果 | |||
| */ | |||
| 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); | |||
| /** | |||
| * @brief 设置工程的初始 HMI 页面 | |||
| * @param page_id 要作为启动页面的页面唯一标识 | |||
| * @return 页面不存在时返回 PageNotFound | |||
| */ | |||
| HmiEditorResult setInitialPage(const std::string &page_id); | |||
| /** | |||
| * @brief 向指定页面添加待配置的基础控件 | |||
| @@ -108,6 +151,13 @@ public: | |||
| */ | |||
| HmiEditorResult removeControl( | |||
| const std::string &page_id, const std::string &control_id); | |||
| /** | |||
| * @brief 一次删除同一页面中的多个控件 | |||
| * @param page_id 所属页面唯一标识 | |||
| * @param control_ids 待删除控件 ID 列表,不能为空且不能包含重复 ID | |||
| * @return 页面、控件不存在或 ID 列表无效时返回失败结果 | |||
| */ | |||
| HmiEditorResult removeControls( | |||
| const std::string &page_id, | |||
| const std::vector<std::string> &control_ids); | |||
| @@ -136,26 +186,51 @@ public: | |||
| const std::string &control_id, | |||
| const HmiControl &control); | |||
| /** @brief 判断是否存在可撤销的 HMI 编辑操作 */ | |||
| bool canUndo() const; | |||
| /** @brief 判断是否存在可重做的 HMI 编辑操作 */ | |||
| bool canRedo() const; | |||
| /** | |||
| * @brief 撤销最近一次成功的编辑操作 | |||
| * @return 没有可撤销操作时返回 InvalidOperation | |||
| */ | |||
| HmiEditorResult undo(); | |||
| /** | |||
| * @brief 重做最近一次被撤销的编辑操作 | |||
| * @return 没有可重做操作时返回 InvalidOperation | |||
| */ | |||
| HmiEditorResult redo(); | |||
| /** | |||
| * @brief 清空撤销和重做历史 | |||
| * | |||
| * 不修改当前工程内容 | |||
| */ | |||
| void clearHistory(); | |||
| private: | |||
| // 撤销/重做需要保存的完整 HMI 编辑状态 | |||
| struct HistoryState | |||
| { | |||
| std::vector<HmiPage> pages; | |||
| std::string initial_page_id; | |||
| std::vector<HmiPage> pages; // 页面及其控件的深拷贝 | |||
| std::string initial_page_id; // 工程初始页面 ID | |||
| }; | |||
| // 捕获当前工程状态,作为一次编辑操作的前后快照 | |||
| HistoryState captureState() const; | |||
| // 比较编辑前后状态并记录实际发生的修改 | |||
| void recordHistory(HistoryState before); | |||
| // 比较两个历史快照是否完全相同 | |||
| static bool statesEqual( | |||
| const HistoryState &left, const HistoryState &right); | |||
| // 比较两个 HMI 控件的全部可保存属性 | |||
| static bool controlsEqual( | |||
| const HmiControl &left, const HmiControl &right); | |||
| // 比较两个 HMI 页面及其控件集合 | |||
| static bool pagesEqual(const HmiPage &left, const HmiPage &right); | |||
| // 生成统一的历史操作失败结果 | |||
| static HmiEditorResult historyFailure(const std::string &message); | |||
| /** | |||
| @@ -183,7 +258,7 @@ private: | |||
| /** | |||
| * @brief 按控件类型创建带默认属性的新控件 | |||
| * @param page 新控件所属页面,用于确定初始位置和唯一标识 | |||
| * @param type 新控件类型 | |||
| * @param descriptor 新控件类型对应的注册描述 | |||
| * @return 未绑定寄存器的默认控件配置 | |||
| */ | |||
| static HmiControl makeControl( | |||
| @@ -18,10 +18,10 @@ | |||
| enum class HmiRuntimeError | |||
| { | |||
| None, // 无错误 | |||
| UnsupportedControl, // 控件类型不支持寄存器读写 | |||
| UnsupportedControl, // 控件类型不支持寄存器读写或写入操作 | |||
| MissingBinding, // 控件未配置寄存器绑定 | |||
| InvalidBinding, // 控件绑定地址无效或地址区域不匹配 | |||
| RepositoryFailure // 寄存器仓库读取或写入失败 | |||
| RepositoryFailure // 寄存器仓库不可用或拒绝读写 | |||
| }; | |||
| /** | |||
| @@ -31,10 +31,10 @@ enum class HmiRuntimeError | |||
| */ | |||
| 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 | |||
| { | |||
| bool succeeded = false; | |||
| HmiRuntimeError error = HmiRuntimeError::None; | |||
| bool succeeded = false; // 写入请求是否被仓库接受 | |||
| HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类 | |||
| }; | |||
| /** | |||
| @@ -51,8 +51,8 @@ struct HmiRuntimeWriteResult | |||
| */ | |||
| enum class HmiButtonEvent | |||
| { | |||
| Pressed, | |||
| Released | |||
| Pressed, // 按钮按下事件 | |||
| Released // 按钮释放事件 | |||
| }; | |||
| /** | |||
| @@ -64,6 +64,10 @@ enum class HmiButtonEvent | |||
| class HmiRuntimeService | |||
| { | |||
| public: | |||
| /** | |||
| * @brief 创建 HMI 运行态读写服务 | |||
| * @param repository 当前运行模式提供的寄存器仓库,不由本服务拥有 | |||
| */ | |||
| explicit HmiRuntimeService(RegisterRepository &repository); | |||
| /** | |||
| @@ -79,6 +83,9 @@ public: | |||
| * @param control 按钮控件,必须绑定有效 M 地址 | |||
| * @param event 当前输入事件 | |||
| * @return 无需写入的事件返回成功,配置无效或仓库拒绝写入时返回失败 | |||
| * | |||
| * SetOn、SetOff 和 Toggle 只在按下时写入;MomentaryOn 在按下时写 1、 | |||
| * 释放时写 0。写入结果以仓库返回值为准,不直接假定缓存已经更新 | |||
| */ | |||
| HmiRuntimeWriteResult operateButton( | |||
| const HmiControl &control, HmiButtonEvent event); | |||
| @@ -87,14 +94,29 @@ public: | |||
| * @param control 数值输入控件,必须绑定有效 D 地址 | |||
| * @param value 要写入的 D 字值 | |||
| * @return 非数值输入、绑定无效或仓库拒绝写入时返回失败结果 | |||
| * | |||
| * 该服务只负责提交写入请求,实际读回值由后续仓库轮询确认 | |||
| */ | |||
| HmiRuntimeWriteResult writeNumericInput( | |||
| const HmiControl &control, std::int16_t value); | |||
| /** | |||
| * @brief 读取指定 M 位 | |||
| * @param address 必须是有效的 M 区地址 | |||
| * @return 仓库原始读取结果,不转换为 HMI 错误分类 | |||
| */ | |||
| BitReadResult readBit(const RegisterAddress &address) const; | |||
| /** | |||
| * @brief 读取指定 D 字 | |||
| * @param address 必须是有效的 D 区地址 | |||
| * @return 仓库原始读取结果,不转换为 HMI 错误分类 | |||
| */ | |||
| WordReadResult readWord(const RegisterAddress &address) const; | |||
| private: | |||
| // 将寄存器仓库错误统一转换为 HMI 运行态错误 | |||
| static HmiRuntimeError repositoryError(RegisterError error); | |||
| RegisterRepository &repository_; | |||
| RegisterRepository &repository_; // 当前运行模式使用的寄存器仓库,不由本服务拥有 | |||
| }; | |||
| @@ -12,164 +12,233 @@ class ProjectService; | |||
| // 梯形图编辑失败分类 | |||
| 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 | |||
| 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 编辑命令转换为结构化表达式树操作,并维护撤销/重做 | |||
| class LogicEditorService | |||
| { | |||
| public: | |||
| /** | |||
| * @brief 创建梯形图编辑服务 | |||
| * @param project_service 用于读取和修改当前工程的项目服务 | |||
| */ | |||
| explicit LogicEditorService(ProjectService &project_service); | |||
| // 以下查询接口只读工程模型,供编辑器投影和属性面板使用 | |||
| /** @brief 按 ID 查找控制逻辑,未找到时返回空指针 */ | |||
| const ControlLogic *findLogic(const std::string &logic_id) const; | |||
| /** @brief 在指定逻辑中按 ID 查找网络,未找到时返回空指针 */ | |||
| const LadderRung *findRung( | |||
| const std::string &logic_id, const std::string &rung_id) const; | |||
| /** @brief 在指定逻辑的所有网络中按 ID 查找节点,未找到时返回空指针 */ | |||
| const LogicNode *findNode( | |||
| const std::string &logic_id, const std::string &node_id) const; | |||
| /** @brief 在指定网络中按 ID 查找条件表达式,未找到时返回空指针 */ | |||
| const ConditionExpression *findExpression( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &expression_id) const; | |||
| /** @brief 返回工程中第一个控制逻辑 ID,没有逻辑时返回空字符串 */ | |||
| std::string firstLogicId() const; | |||
| /** @brief 返回指定逻辑中第一个网络 ID,没有网络或逻辑不存在时返回空字符串 */ | |||
| std::string firstRungId(const std::string &logic_id) const; | |||
| /** @brief 查找节点所属网络 ID,未找到时返回空字符串 */ | |||
| std::string rungIdForNode( | |||
| const std::string &logic_id, const std::string &node_id) const; | |||
| /** @brief 返回指定 M/D 地址的工程注释,没有注释时返回空字符串 */ | |||
| std::string registerCommentFor(const RegisterAddress &address) const; | |||
| // 确保新工程至少有一组可编辑逻辑;逻辑组初始允许没有网络 | |||
| /** | |||
| * @brief 确保工程至少有一组可编辑逻辑 | |||
| * @return 已有或新建逻辑的成功结果及其 ID | |||
| * | |||
| * 新建逻辑时允许暂时没有网络,第一次实际编辑网络内容时再创建网络 | |||
| */ | |||
| LogicEditorResult ensureDefaultLogic(); | |||
| // 逻辑组管理 | |||
| /** @brief 添加控制逻辑;名称不能为空且必须唯一 */ | |||
| LogicEditorResult addLogic(const std::string &name); | |||
| /** @brief 修改控制逻辑名称;名称不能为空且必须唯一 */ | |||
| LogicEditorResult renameLogic( | |||
| const std::string &logic_id, const std::string &name); | |||
| /** @brief 删除控制逻辑;工程至少保留一组逻辑 */ | |||
| LogicEditorResult removeLogic(const std::string &logic_id); | |||
| /** @brief 将逻辑在工程列表中上移或下移一位,offset 只能为 -1 或 1 */ | |||
| LogicEditorResult moveLogic(const std::string &logic_id, int offset); | |||
| /** @brief 启用或停用指定控制逻辑 */ | |||
| LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled); | |||
| // 网络管理 | |||
| /** @brief 在指定逻辑末尾添加空网络 */ | |||
| LogicEditorResult addRung(const std::string &logic_id); | |||
| /** @brief 删除指定网络 */ | |||
| LogicEditorResult removeRung( | |||
| const std::string &logic_id, const std::string &rung_id); | |||
| /** @brief 修改网络注释;注释长度和换行规则由领域校验约束 */ | |||
| LogicEditorResult updateRungComment( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &comment); | |||
| // 条件区编辑:串联、按列插入、横线和并联分支 | |||
| /** @brief 在网络条件末尾追加一个条件节点 */ | |||
| LogicEditorResult appendCondition( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const LogicNodeConfig &config); | |||
| /** | |||
| * @brief 按绝对条件列插入条件节点 | |||
| * @param column 从 0 开始的条件列号;插入位置必须位于允许的条件区 | |||
| */ | |||
| LogicEditorResult insertConditionAtColumn( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column, | |||
| const LogicNodeConfig &config); | |||
| /** | |||
| * @brief 在直属并联分支的指定视觉列插入条件节点 | |||
| * | |||
| * 目标列可以是分支已有横线或 UI 投影出的补线格,服务会原子补齐必要横线 | |||
| */ | |||
| LogicEditorResult insertConditionInBranchAtColumn( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &branch_expression_id, | |||
| int column, | |||
| const LogicNodeConfig &config); | |||
| /** @brief 在网络条件末尾追加指定列宽的横线 */ | |||
| LogicEditorResult appendWire( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column_span = 1); | |||
| /** @brief 在目标节点后串联插入条件节点 */ | |||
| LogicEditorResult insertConditionAfter( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &target_node_id, | |||
| const LogicNodeConfig &config); | |||
| /** @brief 在目标表达式后串联插入横线 */ | |||
| LogicEditorResult insertWireAfter( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &target_expression_id, | |||
| int column_span = 1); | |||
| /** @brief 用条件节点整体替换一条横线表达式 */ | |||
| LogicEditorResult replaceWireWithCondition( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &wire_expression_id, | |||
| const LogicNodeConfig &config); | |||
| /** @brief 只替换横线表达式中的一个指定列单元格 */ | |||
| LogicEditorResult replaceWireColumnWithCondition( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &wire_expression_id, | |||
| int column_offset, | |||
| const LogicNodeConfig &config); | |||
| /** | |||
| * @brief 将选中的条件节点建立为并联分支 | |||
| * @param selected_node_ids 同一网络中按视觉连续范围选择的条件节点 ID | |||
| */ | |||
| LogicEditorResult addParallelBranch( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::string> &selected_node_ids, | |||
| const LogicNodeConfig &config); | |||
| /** @brief 将选中的横线表达式建立为并联旁路 */ | |||
| LogicEditorResult addParallelWireBranch( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::string> &selected_expression_ids); | |||
| // 按选中的横线网格建立旁路;网格必须来自同一条横线且列号连续 | |||
| /** | |||
| * @brief 按选中的横线网格建立并联旁路 | |||
| * | |||
| * 所有网格必须来自同一条横线,且列号连续;操作失败时不保留部分修改 | |||
| */ | |||
| LogicEditorResult addParallelWireBranchAtCells( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::pair<std::string, int>> &selected_wire_cells); | |||
| // 输出槽编辑;每个网络最多一个输出节点 | |||
| /** | |||
| * @brief 设置网络唯一的输出节点 | |||
| * @param configured 是否将新输出标记为已完成配置 | |||
| * | |||
| * 输出节点固定位于网络输出槽,替换已有输出时保持一次原子编辑 | |||
| */ | |||
| LogicEditorResult setOutput( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const LogicNodeConfig &config, | |||
| bool configured = false); | |||
| // 节点属性和删除操作 | |||
| /** @brief 更新节点配置;条件节点和输出节点不能互相改型 */ | |||
| LogicEditorResult updateNodeConfig( | |||
| const std::string &logic_id, | |||
| const std::string &node_id, | |||
| const LogicNodeConfig &config); | |||
| /** @brief 删除指定节点并归一化受影响的表达式树 */ | |||
| LogicEditorResult removeNode( | |||
| const std::string &logic_id, const std::string &node_id); | |||
| /** | |||
| * @brief 批量删除节点 | |||
| * @param node_ids 节点 ID 列表,不能为空且不能包含重复或不存在的 ID | |||
| * @return 成功时作为一次编辑记录,失败时整体回滚 | |||
| */ | |||
| LogicEditorResult removeNodes( | |||
| const std::string &logic_id, | |||
| const std::vector<std::string> &node_ids); | |||
| /** @brief 删除指定条件表达式并归一化表达式树 */ | |||
| LogicEditorResult removeExpression( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &expression_id); | |||
| /** @brief 批量删除条件表达式,失败时整体回滚 */ | |||
| LogicEditorResult removeExpressions( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::vector<std::string> &expression_ids); | |||
| // 当前编辑会话的撤销/重做 | |||
| /** @brief 判断是否存在可撤销的梯形图编辑操作 */ | |||
| bool canUndo() const; | |||
| /** @brief 判断是否存在可重做的梯形图编辑操作 */ | |||
| bool canRedo() const; | |||
| /** @brief 撤销最近一次成功的梯形图编辑操作 */ | |||
| LogicEditorResult undo(); | |||
| /** @brief 重做最近一次被撤销的梯形图编辑操作 */ | |||
| LogicEditorResult redo(); | |||
| /** @brief 清空撤销/重做历史,不修改当前工程内容 */ | |||
| void clearHistory(); | |||
| private: | |||
| // 只保存逻辑集合快照,当前选择等 UI 会话状态不进入历史 | |||
| struct HistoryState | |||
| { | |||
| std::vector<ControlLogic> logics; | |||
| std::vector<ControlLogic> logics; // 编辑前或编辑后的完整逻辑集合 | |||
| }; | |||
| // 捕获当前工程中的全部控制逻辑 | |||
| HistoryState captureState() const; | |||
| // 比较编辑前后状态并记录实际发生的修改 | |||
| void recordHistory(HistoryState before); | |||
| // 失败时恢复工程内容和操作前脏标记,保证编辑原子性 | |||
| void rollbackEdit(HistoryState before, bool modified_before); | |||
| // 比较两个历史快照是否完全相同 | |||
| static bool statesEqual( | |||
| const HistoryState &left, const HistoryState &right); | |||
| static bool logicsEqual( | |||
| @@ -183,17 +252,25 @@ private: | |||
| const LogicNodeConfig &left, const LogicNodeConfig &right); | |||
| static LogicEditorResult historyFailure(const std::string &message); | |||
| // 判断节点配置是否属于条件节点 | |||
| static bool isConditionConfig(const LogicNodeConfig &config); | |||
| // 判断节点配置是否属于输出节点 | |||
| static bool isOutputConfig(const LogicNodeConfig &config); | |||
| // 返回节点配置对应的稳定 ID 前缀 | |||
| static std::string nodePrefix(const LogicNodeConfig &config); | |||
| // 在一个控制逻辑的全部网络中生成唯一节点 ID | |||
| static std::string makeUniqueNodeId( | |||
| const ControlLogic &logic, const std::string &prefix); | |||
| // 生成唯一横线表达式 ID | |||
| static std::string makeUniqueWireId(const ControlLogic &logic); | |||
| // 生成唯一容器表达式 ID | |||
| static std::string makeUniqueExpressionId(const ControlLogic &logic); | |||
| // 生成唯一网络 ID | |||
| static std::string makeUniqueRungId(const ControlLogic &logic); | |||
| // 创建统一的失败结果 | |||
| static LogicEditorResult failure( | |||
| 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 | |||
| { | |||
| Stopped, | |||
| Running, | |||
| Faulted | |||
| Stopped, // 未运行或已主动停止 | |||
| Running, // 定时器正在驱动离线扫描 | |||
| Faulted // 最近一轮扫描失败,定时器已停止 | |||
| }; | |||
| // 启动离线仿真失败的原因 | |||
| enum class SimulationStartError | |||
| { | |||
| None, | |||
| AlreadyRunning, | |||
| InvalidLogic | |||
| None, // 启动成功 | |||
| AlreadyRunning, // 当前已经处于运行态 | |||
| InvalidLogic // 启动前逻辑校验失败 | |||
| }; | |||
| // 离线仿真启动结果;detail 保存领域执行器的具体校验错误 | |||
| 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 | |||
| public: | |||
| // 默认离线扫描周期,单位为毫秒 | |||
| static constexpr int kDefaultScanIntervalMs = 50; | |||
| /** | |||
| * @brief 创建离线仿真服务 | |||
| * @param repository 离线模式使用的虚拟 M/D 寄存器仓库 | |||
| * @param parent Qt 对象父级 | |||
| */ | |||
| explicit OfflineSimulationService( | |||
| VirtualRegisterRepository &repository, | |||
| QObject *parent = nullptr); | |||
| // 复制工程逻辑作为本次会话快照并启动定时扫描 | |||
| // 复制工程逻辑作为本次会话快照并启动定时扫描;启动失败时不进入运行态 | |||
| SimulationStartResult start(const std::vector<ControlLogic> &logics); | |||
| // 停止扫描但保留最近一次轨迹供 UI 显示 | |||
| // 停止扫描并清除运行快照,但保留最近一次轨迹供 UI 显示 | |||
| void stop(); | |||
| // 立即执行一轮扫描,主要供定时器和调试调用 | |||
| // 立即执行一轮扫描;非 Running 状态调用会返回失败结果 | |||
| LogicScanResult executeOnce(); | |||
| // 返回当前离线仿真生命周期状态 | |||
| SimulationState state() const; | |||
| // 返回当前定时器扫描周期,单位为毫秒 | |||
| int scanIntervalMs() const; | |||
| // 返回本次会话成功完成的扫描轮数 | |||
| std::uint64_t successfulScanCount() const; | |||
| // 返回最近一次扫描错误;无错误时为成功结果 | |||
| const LogicScanResult &lastError() const; | |||
| // 返回最近一轮按逻辑 ID 隔离的运行轨迹 | |||
| const LogicTraceSnapshot &traceSnapshot() const; | |||
| signals: | |||
| // 仿真状态从 Stopped、Running 或 Faulted 之一切换时发出 | |||
| void stateChanged(); | |||
| // 一轮扫描成功完成并更新轨迹后发出 | |||
| void scanCompleted(); | |||
| private: | |||
| // 定时器到期时触发一轮扫描 | |||
| void handleTimeout(); | |||
| // 保存错误、停止定时器并切换到 Faulted | |||
| 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 奇 | |||
| 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 异步通信生命周期 | |||
| enum class PlcConnectionState | |||
| { | |||
| Disconnected, | |||
| Connecting, | |||
| Connected, | |||
| Recovering, | |||
| Faulted | |||
| Disconnected, // 未建立 PLC 连接 | |||
| Connecting, // 正在异步建立 PLC 连接 | |||
| Connected, // 串口已连接并进行正常轮询,不代表首读已完成 | |||
| Recovering, // 从可恢复通信故障中探测恢复并重新进行首读 | |||
| Faulted // 当前通信发生故障,等待恢复探测或重新连接 | |||
| }; | |||
| // 通信故障分类,用于状态栏提示和真机运行资格撤销 | |||
| 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 | |||
| { | |||
| 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( | |||
| const PlcSerialConfiguration &configuration) | |||
| { | |||
| @@ -117,19 +123,56 @@ inline PlcCommunicationResult validatePlcSerialConfiguration( | |||
| class PlcCommunicationGateway | |||
| { | |||
| public: | |||
| // 允许通过网关基类指针安全释放具体通信实现 | |||
| virtual ~PlcCommunicationGateway() = default; | |||
| /** | |||
| * @brief 校验串口配置并启动异步 PLC 连接 | |||
| * @param configuration 本次连接使用的串口、Modbus 和轮询参数 | |||
| * @return true 仅表示连接请求已受理,实际连接结果通过状态回调通知 | |||
| */ | |||
| virtual PlcCommunicationResult connectDevice( | |||
| const PlcSerialConfiguration &configuration) = 0; | |||
| /** | |||
| * @brief 停止轮询并断开当前 PLC 连接 | |||
| * | |||
| * 断开后当前缓存视为无效,首读资格同时清除 | |||
| */ | |||
| virtual void disconnectDevice() = 0; | |||
| // 设置后续轮询的 M/D 地址集合;实现可以合并相邻地址块 | |||
| /** | |||
| * @brief 设置后续轮询的 M/D 地址集合 | |||
| * | |||
| * 实现可以校验、排序、去重并合并相邻地址块;已有读请求进行时, | |||
| * 新集合可以延后到当前请求完成后生效 | |||
| * @param addresses 需要周期性读回的 M/D 地址集合,空集合使用实现的默认探测地址 | |||
| * @return true 表示集合已接受,false 表示地址或轮询资源校验失败 | |||
| */ | |||
| virtual PlcCommunicationResult setPollAddresses( | |||
| const std::vector<RegisterAddress> &addresses) = 0; | |||
| // 返回当前连接生命周期状态;Connected 不代表首读资格已经完成 | |||
| virtual PlcConnectionState state() const = 0; | |||
| // 返回本次连接是否已完成全部轮询块的成功首读 | |||
| virtual bool initialReadCompleted() const = 0; | |||
| // 返回最近一次通信错误的分类;没有错误或错误已清除时返回 None | |||
| virtual PlcCommunicationError lastErrorType() const = 0; | |||
| // 返回最近一次通信错误的 UTF-8 可读文本;没有错误时返回空字符串 | |||
| 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( | |||
| std::function<void()> state_changed, | |||
| std::function<void(bool)> initial_read_changed, | |||
| @@ -11,53 +11,96 @@ | |||
| // 自由监控列表编辑的失败分类 | |||
| enum class RegisterMonitorError | |||
| { | |||
| None, | |||
| InvalidAddress, | |||
| RangeOverflow, | |||
| LimitExceeded, | |||
| NoChange | |||
| None, // 操作成功 | |||
| InvalidAddress, // 起始地址格式或区域不支持 | |||
| RangeOverflow, // 连续范围超出 M/D 地址上限 | |||
| LimitExceeded, // 监控地址数量超过会话上限 | |||
| NoChange // 操作没有产生任何列表变化 | |||
| }; | |||
| // 监控列表编辑结果;affectedCount 表示本次受影响的地址数量 | |||
| 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 会原样保留 | |||
| 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 | |||
| { | |||
| public: | |||
| /** | |||
| * @brief 创建自由监控服务 | |||
| * @param 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); | |||
| // 删除一批地址并通知 UI 刷新轮询集合 | |||
| /** | |||
| * @brief 删除一批监控地址 | |||
| * @param addresses 待删除地址;不存在的地址会被忽略 | |||
| * @return 实际删除数量,并在发生变化时通知轮询集合更新 | |||
| */ | |||
| RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses); | |||
| // 清空监控列表 | |||
| /** | |||
| * @brief 清空当前会话的监控地址列表 | |||
| * @return 实际清除数量;列表本来为空时返回 NoChange | |||
| */ | |||
| RegisterMonitorResult clear(); | |||
| // 对单个 M 地址写位 | |||
| /** | |||
| * @brief 对单个 M 地址提交位写入 | |||
| * @param address 必须是有效的 M 区地址 | |||
| * @param value 要写入的位值 | |||
| * @return 成功或包含底层 RegisterError 和用户可读文本的失败结果 | |||
| */ | |||
| RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value); | |||
| // 对单个 D 地址写字 | |||
| /** | |||
| * @brief 对单个 D 地址提交有符号 16 位字写入 | |||
| * @param address 必须是有效的 D 区地址 | |||
| * @param value 要写入的 D 字值 | |||
| * @return 成功或包含底层 RegisterError 和用户可读文本的失败结果 | |||
| */ | |||
| RegisterMonitorWriteResult writeWord( | |||
| const RegisterAddress &address, std::int16_t value); | |||
| /** | |||
| * @brief 返回当前会话的监控地址列表 | |||
| * @return 按服务内部顺序保存的只读地址列表 | |||
| */ | |||
| const std::vector<RegisterAddress> &addresses() const; | |||
| /** | |||
| * @brief 读取监控列表的当前值并生成 UI 展示模型 | |||
| * @param communication_fault 通信处于故障态时,将成功读回值标记为通信故障 | |||
| * @return 每个监控地址对应一个值和可用性状态 | |||
| */ | |||
| std::vector<MonitorValue> values(bool communication_fault) const; | |||
| /** | |||
| * @brief 设置监控地址变化通知回调 | |||
| * @param callback 地址列表新增、删除或清空后调用;可传空函数取消通知 | |||
| */ | |||
| void setAddressesChangedCallback(std::function<void()> callback); | |||
| 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 | |||
| { | |||
| public: | |||
| /** | |||
| * @brief 创建运行模式服务 | |||
| * @param project_service 只读工程服务,用于运行前校验和收集轮询地址 | |||
| * @param offline_simulation_service 离线仿真服务,由应用层负责其生命周期 | |||
| */ | |||
| RuntimeModeService( | |||
| const ProjectService &project_service, | |||
| OfflineSimulationService &offline_simulation_service); | |||
| /** | |||
| * @brief 解除 PLC 网关回调绑定 | |||
| */ | |||
| ~RuntimeModeService(); | |||
| /** @brief 获取当前应用运行模式 */ | |||
| ApplicationMode mode() const; | |||
| /** | |||
| * @brief 获取当前模式下允许的工程编辑和寄存器使用策略 | |||
| @@ -65,35 +75,71 @@ public: | |||
| */ | |||
| bool initialPlcReadCompleted() const; | |||
| /** @brief 获取离线仿真当前状态 */ | |||
| SimulationState simulationState() const; | |||
| /** @brief 获取离线仿真成功完成的扫描轮数 */ | |||
| std::uint64_t successfulScanCount() const; | |||
| /** @brief 获取离线仿真最近一次扫描错误 */ | |||
| const LogicScanResult &simulationError() const; | |||
| /** @brief 返回服务持有的离线仿真服务引用 */ | |||
| OfflineSimulationService &offlineSimulationService(); | |||
| // 注入 PLC 网关、活动仓库和两种实际数据源;应用生命周期内只调用一次 | |||
| /** | |||
| * @brief 注入 PLC 网关、活动仓库和两种实际数据源 | |||
| * | |||
| * 应用生命周期内只调用一次;真机模式使用 PLC 仓库,编辑/离线模式使用虚拟仓库 | |||
| */ | |||
| void configurePlc( | |||
| PlcCommunicationGateway &gateway, | |||
| ActiveRegisterRepository &active_repository, | |||
| RegisterRepository &virtual_repository, | |||
| RegisterRepository &plc_repository); | |||
| // 连接 PLC 并开始异步通信,不阻塞 UI 等待首读 | |||
| /** | |||
| * @brief 连接 PLC 并开始异步通信 | |||
| * @param configuration 串口、Modbus 和轮询配置 | |||
| * @return 连接请求受理结果,不阻塞 UI 等待首读完成 | |||
| */ | |||
| PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration); | |||
| /** | |||
| * @brief 设置自由监控引用的地址并刷新 PLC 轮询集合 | |||
| * @param addresses 自由监控当前需要读取的 M/D 地址 | |||
| */ | |||
| void setMonitorAddresses(const std::vector<RegisterAddress> &addresses); | |||
| /** | |||
| * @brief 汇总工程和自由监控引用并刷新 PLC 轮询地址 | |||
| * @return 网关未配置或地址数量超限时返回失败结果 | |||
| */ | |||
| PlcCommunicationResult refreshPlcPollAddresses(); | |||
| /** | |||
| * @brief 退出真机运行、断开 PLC 并清除首读资格 | |||
| */ | |||
| void disconnectPlc(); | |||
| /** @brief 获取当前 PLC 连接状态;网关未配置时返回 Disconnected */ | |||
| PlcConnectionState plcConnectionState() const; | |||
| /** @brief 获取 PLC 最近一次通信错误文本;网关未配置时返回空字符串 */ | |||
| const std::string &plcError() const; | |||
| /** | |||
| * @brief 设置 PLC 状态变化通知回调 | |||
| * @param callback 状态、首读资格或通信错误变化时调用的函数;可传空函数取消通知 | |||
| */ | |||
| void setPlcStatusChangedCallback(std::function<void()> callback); | |||
| 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; | |||
| 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, | |||
| bool *value) | |||
| { | |||
| // 根据条件节点的具体配置读取 M/D 或内部 T/C 状态,并计算触点结果 | |||
| // 边沿触点使用 logic_id 和 node.id 组成运行时状态键,避免同名节点互相影响 | |||
| if (value == nullptr) | |||
| { | |||
| return failure( | |||
| @@ -400,6 +402,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||
| using Config = std::decay_t<decltype(config)>; | |||
| if constexpr (std::is_same_v<Config, ContactNodeConfig>) | |||
| { | |||
| // 普通触点直接读取 M 位;常闭触点使用读回值的反值 | |||
| const BitReadResult read = repository.readBit(config.address); | |||
| if (!read.succeeded) | |||
| { | |||
| @@ -416,6 +419,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||
| } | |||
| else if constexpr (std::is_same_v<Config, EdgeContactNodeConfig>) | |||
| { | |||
| // 边沿触点需要比较本轮与上一轮的 M 位值,只在目标边沿出现时导通 | |||
| const BitReadResult read = repository.readBit(config.address); | |||
| if (!read.succeeded) | |||
| { | |||
| @@ -435,18 +439,21 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||
| } | |||
| else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>) | |||
| { | |||
| // 定时器触点不读 PLC 地址,而是读取本地 TON 的完成状态 | |||
| const bool done = ton_states_[config.address.index()].done; | |||
| *value = config.mode == ContactMode::NormallyOpen ? done : !done; | |||
| return success(); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | |||
| { | |||
| // 计数器触点读取本地 CTU/CTD 的完成状态 | |||
| const bool done = counter_states_[config.address.index()].done; | |||
| *value = config.mode == ContactMode::NormallyOpen ? done : !done; | |||
| return success(); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | |||
| { | |||
| // 字比较触点读取 D 字,再按配置的比较运算符与目标值比较 | |||
| const WordReadResult read = repository.readWord(config.address); | |||
| if (!read.succeeded) | |||
| { | |||
| @@ -462,6 +469,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||
| } | |||
| else | |||
| { | |||
| // 输出节点只能由 executeOutput 处理,不能作为条件触点计算 | |||
| return failure( | |||
| LogicScanError::InvalidLogic, | |||
| "条件节点不能包含输出节点", | |||
| @@ -11,75 +11,78 @@ | |||
| #include <utility> | |||
| #include <vector> | |||
| // 一轮离线扫描可能返回的错误 | |||
| // 一轮软件逻辑扫描可能返回的错误 | |||
| enum class LogicScanError | |||
| { | |||
| None, | |||
| InvalidLogic, | |||
| ConflictingOutput, | |||
| RegisterReadFailed, | |||
| RegisterWriteFailed | |||
| None, // 扫描成功 | |||
| InvalidLogic, // 逻辑结构或运行配置无效 | |||
| ConflictingOutput, // 多个输出以不兼容的线圈模式写入同一地址 | |||
| RegisterReadFailed, // 扫描读取寄存器失败 | |||
| RegisterWriteFailed // 扫描写入寄存器失败 | |||
| }; | |||
| // 扫描结果;失败时携带出错的逻辑、网络和节点 ID | |||
| 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 节点在最近一轮扫描中的可视化状态 | |||
| 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 节点在最近一轮扫描中的可视化状态 | |||
| 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 节点的结果值和溢出标记 | |||
| 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 | |||
| { | |||
| 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(); | |||
| }; | |||
| // 全工程轨迹;logicValues 按逻辑 ID 隔离,避免节点 ID 重复互相覆盖 | |||
| // 全工程轨迹;logicValues 按逻辑 ID 隔离,避免不同逻辑中的节点 ID 互相覆盖 | |||
| struct LogicTraceSnapshot : LogicTraceValues | |||
| { | |||
| std::unordered_map<std::string, LogicTraceValues> logicValues; | |||
| std::unordered_map<std::string, LogicTraceValues> logicValues; // 按逻辑 ID 保存的轨迹 | |||
| // 清除全工程轨迹及按逻辑分组的轨迹 | |||
| void clear(); | |||
| // 返回指定逻辑的轨迹投影;不存在时返回空轨迹 | |||
| LogicTraceSnapshot forLogic(const std::string &logic_id) const; | |||
| }; | |||
| @@ -90,16 +93,41 @@ public: | |||
| using Clock = std::chrono::steady_clock; | |||
| using TimePoint = Clock::time_point; | |||
| // 在启动仿真前检查所有启用逻辑及 T/C 资源引用 | |||
| /** | |||
| * @brief 检查逻辑是否满足软件扫描的运行要求 | |||
| * @param logics 待检查的控制逻辑集合 | |||
| * @return 成功或包含逻辑、网络、节点定位信息的失败结果 | |||
| * | |||
| * 检查逻辑结构、启用逻辑的运行配置、共享输出冲突以及 T/C 资源引用 | |||
| */ | |||
| LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | |||
| // 清除沿触发、定时器和计数器的跨扫描运行状态 | |||
| /** | |||
| * @brief 清除所有跨扫描运行状态 | |||
| * | |||
| * 重置边沿触发、TON 定时器和 CTU/CTD 计数器;不修改寄存器仓库和轨迹对象 | |||
| */ | |||
| void resetRuntime(); | |||
| // 使用当前 steady_clock 执行一轮扫描 | |||
| /** | |||
| * @brief 使用当前 steady_clock 执行一轮完整扫描 | |||
| * @param logics 按工程顺序扫描的控制逻辑集合 | |||
| * @param repository 扫描读取和写入的寄存器仓库 | |||
| * @param trace 可选的轨迹输出;非空时会先清空再写入本轮结果 | |||
| * @return 扫描结果,失败时不会返回部分成功状态作为成功结果 | |||
| */ | |||
| LogicScanResult executeScan( | |||
| const std::vector<ControlLogic> &logics, | |||
| RegisterRepository &repository, | |||
| LogicTraceSnapshot *trace = nullptr); | |||
| // 使用指定时间执行扫描,便于稳定验证 TON 等时间逻辑 | |||
| /** | |||
| * @brief 使用指定时间执行一轮扫描 | |||
| * @param logics 按工程顺序扫描的控制逻辑集合 | |||
| * @param repository 扫描读取和写入的寄存器仓库 | |||
| * @param now 本轮扫描使用的单调时钟时间点 | |||
| * @param trace 可选的轨迹输出;适合测试 TON 等时间相关逻辑 | |||
| * @return 扫描结果 | |||
| */ | |||
| LogicScanResult executeScanAt( | |||
| const std::vector<ControlLogic> &logics, | |||
| RegisterRepository &repository, | |||
| @@ -107,19 +135,38 @@ public: | |||
| LogicTraceSnapshot *trace = nullptr); | |||
| private: | |||
| // 单个 TON 定时器跨扫描保存的计时状态 | |||
| 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( | |||
| const std::string &logic_id, | |||
| const LogicNode &node, | |||
| RegisterRepository &repository, | |||
| bool *value); | |||
| /** | |||
| * @brief 递归计算串联、并联或叶子条件表达式 | |||
| * @param logic_id 所属控制逻辑 ID,用于传递给叶子节点状态计算 | |||
| * @param expression 待读取的条件表达式树 | |||
| * @param repository 提供 M/D 值的寄存器仓库 | |||
| * @param trace 可选的轨迹输出,用于记录表达式值、电源输入和电源输出 | |||
| * @param input_power 进入当前表达式的电源状态 | |||
| * @param value 输出表达式最终值,不能为空 | |||
| * @return 成功结果,或包含失败节点 ID 的扫描错误 | |||
| */ | |||
| LogicScanResult evaluateExpression( | |||
| const std::string &logic_id, | |||
| const ConditionExpression &expression, | |||
| @@ -127,6 +174,17 @@ private: | |||
| LogicTraceValues *trace, | |||
| bool input_power, | |||
| 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( | |||
| const std::string &logic_id, | |||
| const LogicNode &node, | |||
| @@ -135,18 +193,30 @@ private: | |||
| TimePoint now, | |||
| LogicTraceValues *trace, | |||
| bool *output_value); | |||
| /** | |||
| * @brief 解析并读取一个字操作数 | |||
| * @param operand 常量或 D 寄存器操作数配置 | |||
| * @param repository 读取寄存器操作数时使用的仓库 | |||
| * @param value 输出解析后的 int16 数值,不能为空 | |||
| * @param node_id 关联节点 ID,用于构造可定位的失败结果 | |||
| * @return 常量读取成功,或 D 寄存器读取/操作数类型校验失败 | |||
| */ | |||
| LogicScanResult readWordOperand( | |||
| const WordOperand &operand, | |||
| RegisterRepository &repository, | |||
| std::int16_t *value, | |||
| const std::string &node_id) const; | |||
| // 按逻辑 ID 和节点 ID 保存上一轮边沿输入 | |||
| std::map<std::pair<std::string, std::string>, bool> previous_edge_inputs_; | |||
| // 按逻辑 ID 和节点 ID 保存上一轮计数输入 | |||
| std::map<std::pair<std::string, std::string>, bool> previous_counter_inputs_; | |||
| // 按定时器编号保存跨扫描状态 | |||
| std::unordered_map<int, TonRuntimeState> ton_states_; | |||
| // 单个计数器跨扫描保存的完成状态 | |||
| struct CounterRuntimeState | |||
| { | |||
| bool done = false; | |||
| 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; | |||
| struct AlarmDefinition; | |||
| /** 报警定义编辑对话框;保存和删除都委托 AlarmEditorService */ | |||
| /** 报警定义编辑对话框;保存、更新和删除都委托 AlarmEditorService */ | |||
| class AlarmConfigurationDialog final : public QDialog | |||
| { | |||
| public: | |||
| /** 创建报警配置对话框并绑定报警编辑服务 */ | |||
| explicit AlarmConfigurationDialog( | |||
| AlarmEditorService &service, | |||
| QWidget *parent = nullptr); | |||
| /** 释放 Designer 界面对象 */ | |||
| ~AlarmConfigurationDialog() override; | |||
| private: | |||
| @@ -27,9 +27,11 @@ class FreeMonitorWidget final : public QWidget | |||
| Q_OBJECT | |||
| public: | |||
| /** 创建自由监控控件并绑定监控服务 */ | |||
| explicit FreeMonitorWidget( | |||
| RegisterMonitorService &service, | |||
| QWidget *parent = nullptr); | |||
| /** 释放 Designer 界面对象 */ | |||
| ~FreeMonitorWidget() override; | |||
| /** 真机未连接、通信故障或编辑态时应关闭写入入口 */ | |||
| @@ -27,6 +27,7 @@ class LogicEditorWidget final : public QGraphicsView | |||
| Q_OBJECT | |||
| public: | |||
| /** 梯形图中的条件、输出、横线和辅助选择图元类型 */ | |||
| class NodeItem; | |||
| class WireItem; | |||
| class WireCellItem; | |||
| @@ -34,11 +35,12 @@ public: | |||
| class RungItem; | |||
| class EmptySlotItem; | |||
| /** 创建梯形图画布并绑定编辑服务 */ | |||
| explicit LogicEditorWidget( | |||
| LogicEditorService &editor_service, | |||
| QWidget *parent = nullptr); | |||
| /** 切换当前显示的控制逻辑 */ | |||
| /** 切换当前显示的控制逻辑;不存在时显示空场景 */ | |||
| void setLogicId(const std::string &logic_id); | |||
| /** 设置是否允许编辑梯形图 */ | |||
| void setEditingEnabled(bool enabled); | |||
| @@ -27,9 +27,11 @@ class LogicInstructionDialog final : public QDialog | |||
| Q_OBJECT | |||
| public: | |||
| /** 使用已有节点配置初始化指令参数表单 */ | |||
| explicit LogicInstructionDialog( | |||
| const LogicNodeConfig &config, | |||
| QWidget *parent = nullptr); | |||
| /** 释放 Designer 界面对象 */ | |||
| ~LogicInstructionDialog() override; | |||
| /** 返回当前表单配置,调用方仍需交给 LogicEditorService 校验 */ | |||
| @@ -1,7 +1,7 @@ | |||
| /** | |||
| * @file main_window.h | |||
| * @brief 定义综合平台编程器主窗口及运行模式界面协调逻辑 | |||
| * @version 0.2.0 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-08 | |||
| */ | |||
| @@ -76,6 +76,11 @@ public: | |||
| RegisterCommentService ®ister_comment_service, | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| QWidget *parent = nullptr); | |||
| /** | |||
| * @brief 创建主窗口,并使用调用方提供的 HMI 页面导航服务 | |||
| * | |||
| * 两个构造函数的区别只在导航服务的所有权;其余服务均由应用入口管理 | |||
| */ | |||
| MainWindow( | |||
| RuntimeModeService &runtime_mode_service, | |||
| ProjectService &project_service, | |||
| @@ -89,6 +94,7 @@ public: | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| QWidget *parent = nullptr); | |||
| /** 释放窗口拥有的控制器和可选导航服务 */ | |||
| ~MainWindow() override; | |||
| /** 返回当前选中的 HMI 页面标识 */ | |||
| @@ -24,9 +24,11 @@ class PlcConnectionDialog final : public QDialog | |||
| Q_OBJECT | |||
| public: | |||
| /** 使用现有配置初始化 PLC 参数表单 */ | |||
| explicit PlcConnectionDialog( | |||
| const PlcSerialConfiguration &configuration, | |||
| QWidget *parent = nullptr); | |||
| /** 释放 Designer 界面对象 */ | |||
| ~PlcConnectionDialog() override; | |||
| /** 返回表单中的串口配置,连接前由网关再次校验 */ | |||
| @@ -48,6 +48,7 @@ public: | |||
| /** 通知主窗口编辑状态已经发生变化 */ | |||
| using EditStateChanged = std::function<void()>; | |||
| /** 创建工程树控制器并绑定主窗口服务和编辑器 */ | |||
| ProjectWorkspaceController( | |||
| QWidget &parent, | |||
| Ui::MainWindow &ui, | |||
| @@ -97,9 +98,9 @@ private: | |||
| /** 工程树项目的三种业务类型 */ | |||
| 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( | |||
| const QString &message, int timeout_ms)>; | |||
| /** 创建属性面板控制器并注入编辑服务和选择状态 */ | |||
| PropertyPanelController( | |||
| QWidget &parent, | |||
| Ui::MainWindow &ui, | |||
| @@ -25,9 +25,11 @@ class RegisterCommentService; | |||
| class RegisterCommentDialog final : public QDialog | |||
| { | |||
| public: | |||
| /** 创建寄存器注释对话框并绑定注释服务 */ | |||
| explicit RegisterCommentDialog( | |||
| RegisterCommentService &service, | |||
| QWidget *parent = nullptr); | |||
| /** 释放 Designer 界面对象 */ | |||
| ~RegisterCommentDialog() override; | |||
| private: | |||
| @@ -38,6 +38,7 @@ class RuntimeMonitorWidget final : public QWidget | |||
| Q_OBJECT | |||
| public: | |||
| /** 创建组合 HMI、梯形图和自由监控的运行投影 */ | |||
| RuntimeMonitorWidget( | |||
| HmiEditorService &hmi_editor_service, | |||
| HmiRuntimeService &hmi_runtime_service, | |||
| @@ -47,6 +48,7 @@ public: | |||
| AlarmService &alarm_service, | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| QWidget *parent = nullptr); | |||
| /** 释放组合的运行监控子控件 */ | |||
| ~RuntimeMonitorWidget() override; | |||
| /** 将当前页面和控制逻辑绑定到运行监控界面 */ | |||
| @@ -25,7 +25,9 @@ class RuntimeMonitorWindow final : public QMainWindow | |||
| Q_OBJECT | |||
| public: | |||
| /** 创建运行监控顶层窗口 */ | |||
| explicit RuntimeMonitorWindow(QWidget *parent = nullptr); | |||
| /** 释放窗口和 Designer 界面对象 */ | |||
| ~RuntimeMonitorWindow() override; | |||
| /** 将运行监控控件放入窗口中央区域 */ | |||
| @@ -45,6 +45,7 @@ public: | |||
| /** 请求上层退出当前运行态 */ | |||
| using RuntimeExitRequester = std::function<void()>; | |||
| /** 创建运行面板控制器并注入运行模式、编辑器和监控窗口依赖 */ | |||
| RuntimePanelController( | |||
| QWidget &parent, | |||
| RuntimeModeService &runtime_mode_service, | |||
| @@ -64,6 +65,7 @@ public: | |||
| StatusReporter status_reporter, | |||
| OutputReporter output_reporter, | |||
| RuntimeExitRequester runtime_exit_requester); | |||
| /** 停止刷新并释放控制器拥有的运行监控窗口 */ | |||
| ~RuntimePanelController(); | |||
| /** 连接运行监控相关信号并完成初始配置 */ | |||
| @@ -13,55 +13,55 @@ | |||
| /** 工具栏中使用的语义图标类型 */ | |||
| 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_register_repository.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 | |||
| CONFIG += ordered | |||
| @@ -11,9 +11,7 @@ SUBDIRS += \ | |||
| project_management \ | |||
| register_monitor \ | |||
| runtime_mode \ | |||
| plc_runtime \ | |||
| main_window \ | |||
| performance | |||
| plc_runtime | |||
| domain.file = domain_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 | |||
| runtime_mode.file = runtime_mode_service_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( | |||
| [ValidateSet('Debug', 'Release')] | |||
| [string]$Configuration = 'Debug', | |||
| [switch]$SkipPerformance | |||
| [ValidateSet('Functional', 'Performance', 'All')] | |||
| [string]$Suite = 'Functional' | |||
| ) | |||
| Set-StrictMode -Version Latest | |||
| @@ -23,7 +24,7 @@ foreach ($requiredPath in @($qmakePath, $makePath, $testSourceRoot)) { | |||
| } | |||
| } | |||
| $targets = @( | |||
| $functionalTargets = @( | |||
| 'domain_tests', | |||
| 'alarm_service_tests', | |||
| 'hmi_editor_service_tests', | |||
| @@ -32,17 +33,20 @@ $targets = @( | |||
| 'project_management_tests', | |||
| 'register_monitor_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 | |||
| $originalPlatform = $env:QT_QPA_PLATFORM | |||
| $env:Path = "$qtRoot\bin;$mingwBin;$env:Path" | |||
| $env:QT_QPA_PLATFORM = 'offscreen' | |||
| Write-Host "测试类型:$Suite" | |||
| try { | |||
| foreach ($target in $targets) { | |||
| @@ -68,10 +72,19 @@ try { | |||
| } | |||
| 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) { | |||
| throw "测试失败:$target,退出码:$LASTEXITCODE" | |||
| } | |||
| if ($target -eq 'performance_tests') { | |||
| Write-Host "性能结果:$benchmarkResultPath" | |||
| Get-Content -LiteralPath $benchmarkResultPath -Encoding utf8 | |||
| } | |||
| } | |||
| finally { | |||
| Pop-Location | |||
| @@ -80,9 +93,4 @@ try { | |||
| } | |||
| finally { | |||
| $env:Path = $originalPath | |||
| if ($null -eq $originalPlatform) { | |||
| Remove-Item Env:QT_QPA_PLATFORM -ErrorAction SilentlyContinue | |||
| } else { | |||
| $env:QT_QPA_PLATFORM = $originalPlatform | |||
| } | |||
| } | |||