| @@ -7,6 +7,7 @@ ActiveRegisterRepository::ActiveRegisterRepository(RegisterRepository &initial_r | |||||
| void ActiveRegisterRepository::use(RegisterRepository &repository) | void ActiveRegisterRepository::use(RegisterRepository &repository) | ||||
| { | { | ||||
| // 只替换转发目标,不复制寄存器值;切换模式必须使用各自数据源 | |||||
| repository_ = &repository; | repository_ = &repository; | ||||
| } | } | ||||
| @@ -2,11 +2,13 @@ | |||||
| #include "register_repository.h" | #include "register_repository.h" | ||||
| // 根据当前运行模式把寄存器访问转发给虚拟仓库或 PLC 缓存仓库 | |||||
| class ActiveRegisterRepository final : public RegisterRepository | class ActiveRegisterRepository final : public RegisterRepository | ||||
| { | { | ||||
| public: | public: | ||||
| explicit ActiveRegisterRepository(RegisterRepository &initial_repository); | explicit ActiveRegisterRepository(RegisterRepository &initial_repository); | ||||
| // 切换活动仓库;运行模式服务负责保证切换时机和生命周期 | |||||
| void use(RegisterRepository &repository); | void use(RegisterRepository &repository); | ||||
| BitReadResult readBit(const RegisterAddress &address) const override; | BitReadResult readBit(const RegisterAddress &address) const override; | ||||
| RegisterWriteResult writeBit(const RegisterAddress &address, bool value) override; | RegisterWriteResult writeBit(const RegisterAddress &address, bool value) override; | ||||
| @@ -5,6 +5,7 @@ | |||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <string> | #include <string> | ||||
| // 报警触发条件:M 位为 1,或 D 字高于/低于阈值 | |||||
| enum class AlarmCondition | enum class AlarmCondition | ||||
| { | { | ||||
| MOn, | MOn, | ||||
| @@ -12,6 +13,7 @@ enum class AlarmCondition | |||||
| DLow | DLow | ||||
| }; | }; | ||||
| // 可保存的报警定义;运行时 AlarmService 根据此定义生成报警记录 | |||||
| struct AlarmDefinition | struct AlarmDefinition | ||||
| { | { | ||||
| std::string id; | std::string id; | ||||
| @@ -20,5 +22,6 @@ struct AlarmDefinition | |||||
| std::int16_t threshold = 0; | std::int16_t threshold = 0; | ||||
| std::string message; | std::string message; | ||||
| // 校验标识、地址区域、消息长度和条件之间的组合是否有效 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| @@ -691,6 +691,7 @@ bool ConditionExpression::validateForRunning(std::string *error) const | |||||
| void normalizeConditionExpression(std::optional<ConditionExpression> *expression) | void normalizeConditionExpression(std::optional<ConditionExpression> *expression) | ||||
| { | { | ||||
| // 删除节点后折叠单子项容器,并合并相邻同类容器,保持 JSON 结构稳定 | |||||
| if (expression == nullptr || !expression->has_value()) | if (expression == nullptr || !expression->has_value()) | ||||
| { | { | ||||
| return; | return; | ||||
| @@ -9,12 +9,14 @@ | |||||
| #include <variant> | #include <variant> | ||||
| #include <vector> | #include <vector> | ||||
| // 普通触点的导通方式;常闭触点会对读取到的位值取反 | |||||
| enum class ContactMode | enum class ContactMode | ||||
| { | { | ||||
| NormallyOpen, | NormallyOpen, | ||||
| NormallyClosed | NormallyClosed | ||||
| }; | }; | ||||
| // 线圈写入方式:普通写入、置位保持、复位清零 | |||||
| enum class CoilMode | enum class CoilMode | ||||
| { | { | ||||
| Normal, | Normal, | ||||
| @@ -22,12 +24,14 @@ enum class CoilMode | |||||
| Reset | Reset | ||||
| }; | }; | ||||
| // 上升沿/下降沿触点的边沿方向 | |||||
| enum class EdgeMode | enum class EdgeMode | ||||
| { | { | ||||
| Rising, | Rising, | ||||
| Falling | Falling | ||||
| }; | }; | ||||
| // D 字比较节点支持的比较运算 | |||||
| enum class ComparisonOperator | enum class ComparisonOperator | ||||
| { | { | ||||
| Equal, | Equal, | ||||
| @@ -38,18 +42,21 @@ enum class ComparisonOperator | |||||
| GreaterThanOrEqual | GreaterThanOrEqual | ||||
| }; | }; | ||||
| // 字操作数可以来自常量,也可以来自 D 寄存器 | |||||
| enum class WordOperandKind | enum class WordOperandKind | ||||
| { | { | ||||
| Constant, | Constant, | ||||
| Register | Register | ||||
| }; | }; | ||||
| // MOVE、ADD/SUB、计数器预置值共用的字操作数 | |||||
| struct WordOperand | struct WordOperand | ||||
| { | { | ||||
| WordOperandKind kind = WordOperandKind::Constant; | WordOperandKind kind = WordOperandKind::Constant; | ||||
| RegisterAddress address{RegisterArea::D, 0}; | RegisterAddress address{RegisterArea::D, 0}; | ||||
| std::int16_t constant = 0; | std::int16_t constant = 0; | ||||
| // 常量始终有效;寄存器操作数必须是有效 D 地址 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| @@ -78,6 +85,7 @@ struct CompareNodeConfig | |||||
| std::int16_t value = 0; | std::int16_t value = 0; | ||||
| }; | }; | ||||
| // 离线执行器内部使用的 T 区地址,不会加入 PLC M/D 轮询 | |||||
| class TimerAddress | class TimerAddress | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -97,12 +105,14 @@ private: | |||||
| int index_ = 0; | int index_ = 0; | ||||
| }; | }; | ||||
| // 读取 TON 完成状态的条件触点 | |||||
| struct TimerContactNodeConfig | struct TimerContactNodeConfig | ||||
| { | { | ||||
| TimerAddress address; | TimerAddress address; | ||||
| ContactMode mode = ContactMode::NormallyOpen; | ContactMode mode = ContactMode::NormallyOpen; | ||||
| }; | }; | ||||
| // 离线执行器内部使用的 C 区地址,不会直接映射 PLC C 区 | |||||
| class CounterAddress | class CounterAddress | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -128,12 +138,14 @@ enum class CounterMode | |||||
| Down | Down | ||||
| }; | }; | ||||
| // 读取计数器完成状态的条件触点 | |||||
| struct CounterContactNodeConfig | struct CounterContactNodeConfig | ||||
| { | { | ||||
| CounterAddress address; | CounterAddress address; | ||||
| ContactMode mode = ContactMode::NormallyOpen; | ContactMode mode = ContactMode::NormallyOpen; | ||||
| }; | }; | ||||
| // TON 输出的定时器和预置时间,单位为毫秒 | |||||
| struct TonNodeConfig | struct TonNodeConfig | ||||
| { | { | ||||
| static constexpr int kMinimumPresetMs = 1; | static constexpr int kMinimumPresetMs = 1; | ||||
| @@ -143,6 +155,7 @@ struct TonNodeConfig | |||||
| int presetMs = 1000; | int presetMs = 1000; | ||||
| }; | }; | ||||
| // CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D | |||||
| struct CounterNodeConfig | struct CounterNodeConfig | ||||
| { | { | ||||
| CounterAddress address; | CounterAddress address; | ||||
| @@ -152,6 +165,7 @@ struct CounterNodeConfig | |||||
| RegisterAddress resetAddress{RegisterArea::M, 0}; | RegisterAddress resetAddress{RegisterArea::M, 0}; | ||||
| }; | }; | ||||
| // MOVE 输出:把一个字操作数写入目标 D 地址 | |||||
| struct MoveNodeConfig | struct MoveNodeConfig | ||||
| { | { | ||||
| WordOperand source; | WordOperand source; | ||||
| @@ -164,6 +178,7 @@ enum class ArithmeticOperation | |||||
| Subtract | Subtract | ||||
| }; | }; | ||||
| // ADD/SUB 输出:计算两个字操作数并写入目标 D 地址 | |||||
| struct ArithmeticNodeConfig | struct ArithmeticNodeConfig | ||||
| { | { | ||||
| ArithmeticOperation operation = ArithmeticOperation::Add; | ArithmeticOperation operation = ArithmeticOperation::Add; | ||||
| @@ -172,6 +187,7 @@ struct ArithmeticNodeConfig | |||||
| RegisterAddress destination{RegisterArea::D, 0}; | RegisterAddress destination{RegisterArea::D, 0}; | ||||
| }; | }; | ||||
| // 所有梯形图指令的强类型配置联合,避免用无关字段拼装指令 | |||||
| using LogicNodeConfig = std::variant< | using LogicNodeConfig = std::variant< | ||||
| ContactNodeConfig, | ContactNodeConfig, | ||||
| EdgeContactNodeConfig, | EdgeContactNodeConfig, | ||||
| @@ -184,20 +200,25 @@ using LogicNodeConfig = std::variant< | |||||
| MoveNodeConfig, | MoveNodeConfig, | ||||
| ArithmeticNodeConfig>; | ArithmeticNodeConfig>; | ||||
| // 返回节点最主要的 M/D 地址;复合输出请使用下面的收集函数 | |||||
| std::optional<RegisterAddress> registerAddressForLogicNode( | std::optional<RegisterAddress> registerAddressForLogicNode( | ||||
| const LogicNodeConfig &config); | const LogicNodeConfig &config); | ||||
| // 收集节点中所有显式 M/D 引用,用于 PLC 轮询集合构建 | |||||
| void collectRegisterAddressesForLogicNode( | void collectRegisterAddressesForLogicNode( | ||||
| const LogicNodeConfig &config, | const LogicNodeConfig &config, | ||||
| std::vector<RegisterAddress> *addresses); | std::vector<RegisterAddress> *addresses); | ||||
| // 表达式中的一个指令节点;configured=false 表示编辑中的未完成草稿 | |||||
| struct LogicNode | struct LogicNode | ||||
| { | { | ||||
| std::string id; | std::string id; | ||||
| LogicNodeConfig config; | LogicNodeConfig config; | ||||
| bool configured = true; | bool configured = true; | ||||
| // 校验节点 ID、配置类型和配置内容 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| bool isConfigured() const; | bool isConfigured() const; | ||||
| // 判断节点能否放在条件区或固定输出槽 | |||||
| bool isCondition() const; | bool isCondition() const; | ||||
| bool isOutput() const; | bool isOutput() const; | ||||
| }; | }; | ||||
| @@ -210,6 +231,7 @@ enum class ConditionExpressionKind | |||||
| Parallel | Parallel | ||||
| }; | }; | ||||
| // 条件区中的持久化横线;columnSpan 表示跨越的网格列数 | |||||
| struct WireSegment | struct WireSegment | ||||
| { | { | ||||
| static constexpr int kMinimumColumnSpan = 1; | static constexpr int kMinimumColumnSpan = 1; | ||||
| @@ -232,7 +254,9 @@ struct ConditionExpression | |||||
| static ConditionExpression fromNode(LogicNode node); | static ConditionExpression fromNode(LogicNode node); | ||||
| static ConditionExpression fromWire(std::string id, int column_span = 1); | static ConditionExpression fromWire(std::string id, int column_span = 1); | ||||
| // 编辑态校验允许空配置节点,但必须保持树结构合法 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| // 运行态校验额外要求每个节点都已经配置完成 | |||||
| bool validateForRunning(std::string *error = nullptr) const; | bool validateForRunning(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| @@ -260,11 +284,15 @@ struct LadderRung | |||||
| std::optional<ConditionExpression> condition; | std::optional<ConditionExpression> condition; | ||||
| std::optional<LogicNode> output; | std::optional<LogicNode> output; | ||||
| // 编辑态允许没有条件或输出,便于逐步搭建网络 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| // 只检查表达式拓扑、列数和输出位置 | |||||
| bool validateStructure(std::string *error = nullptr) const; | bool validateStructure(std::string *error = nullptr) const; | ||||
| // 运行态要求有输出且所有引用都已配置 | |||||
| bool validateForRunning(std::string *error = nullptr) const; | bool validateForRunning(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| // 一组按顺序扫描的梯形图网络;enabled=false 时运行校验会跳过它 | |||||
| struct ControlLogic | struct ControlLogic | ||||
| { | { | ||||
| std::string id; | std::string id; | ||||
| @@ -272,17 +300,22 @@ struct ControlLogic | |||||
| std::vector<LadderRung> rungs; | std::vector<LadderRung> rungs; | ||||
| bool enabled = true; | bool enabled = true; | ||||
| // 校验可保存结构 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| bool validateStructure(std::string *error = nullptr) const; | bool validateStructure(std::string *error = nullptr) const; | ||||
| // 校验运行所需资源和网络配置 | |||||
| bool validateForRunning(std::string *error = nullptr) const; | bool validateForRunning(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| // 检查同一 T 地址是否被多个逻辑节点以冲突方式使用 | |||||
| bool validateTimerReferencesForRunning( | bool validateTimerReferencesForRunning( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| std::string *error = nullptr); | std::string *error = nullptr); | ||||
| // 检查同一 C 地址是否被多个计数器配置重复占用 | |||||
| bool validateCounterReferencesForRunning( | bool validateCounterReferencesForRunning( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| std::string *error = nullptr); | std::string *error = nullptr); | ||||
| // 一次性执行 T/C 资源引用冲突检查 | |||||
| bool validateLogicResourceReferencesForRunning( | bool validateLogicResourceReferencesForRunning( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| std::string *error = nullptr); | std::string *error = nullptr); | ||||
| @@ -2,6 +2,7 @@ | |||||
| #include <cstddef> | #include <cstddef> | ||||
| // 所有跨层共享的数量和范围边界集中在这里,避免 UI、服务和 JSON 各自写数字 | |||||
| namespace ProjectLimits { | namespace ProjectLimits { | ||||
| constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U; | constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U; | ||||
| @@ -111,6 +111,7 @@ const RegisterComment *Project::findRegisterComment( | |||||
| bool Project::validate(std::string *error) const | bool Project::validate(std::string *error) const | ||||
| { | { | ||||
| // 这里是工程级总闸门:先检查数量和唯一性,再向下委托页面、报警和逻辑 | |||||
| if (metadata.id.empty() || metadata.name.empty() || metadata.formatVersion.empty()) | if (metadata.id.empty() || metadata.name.empty() || metadata.formatVersion.empty()) | ||||
| { | { | ||||
| setError(error, "工程 ID、名称和格式版本不能为空"); | setError(error, "工程 ID、名称和格式版本不能为空"); | ||||
| @@ -258,6 +259,7 @@ bool Project::validate(std::string *error) const | |||||
| } | } | ||||
| } | } | ||||
| // 同时收集所有显式 M/D 引用,提前检查 PLC 轮询集合不会超过上限 | |||||
| std::vector<RegisterAddress> poll_addresses; | std::vector<RegisterAddress> poll_addresses; | ||||
| for (const HmiPage &page : hmiPages) | for (const HmiPage &page : hmiPages) | ||||
| { | { | ||||
| @@ -313,6 +315,7 @@ bool Project::validate(std::string *error) const | |||||
| bool Project::validateForRunning(std::string *error) const | bool Project::validateForRunning(std::string *error) const | ||||
| { | { | ||||
| // 运行校验建立在可保存校验之上,再额外拒绝未配置的运行对象 | |||||
| if (!validate(error)) | if (!validate(error)) | ||||
| { | { | ||||
| return false; | return false; | ||||
| @@ -7,11 +7,13 @@ | |||||
| #include <string> | #include <string> | ||||
| #include <vector> | #include <vector> | ||||
| // 工程级 M/D 注释,只描述地址含义,不保存实时值 | |||||
| struct RegisterComment | struct RegisterComment | ||||
| { | { | ||||
| RegisterAddress address{RegisterArea::M, 0}; | RegisterAddress address{RegisterArea::M, 0}; | ||||
| std::string text; | std::string text; | ||||
| // 校验地址有效性和 UTF-8 文本长度 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| @@ -26,7 +28,7 @@ struct ProjectMetadata | |||||
| std::string formatVersion = "1.0"; | std::string formatVersion = "1.0"; | ||||
| }; | }; | ||||
| // 聚合工程中的 HMI 页面和控制逻辑 | |||||
| // 聚合工程中的 HMI 页面、报警、寄存器注释和控制逻辑 | |||||
| struct Project | struct Project | ||||
| { | { | ||||
| // 工程基本信息 | // 工程基本信息 | ||||
| @@ -42,8 +44,10 @@ struct Project | |||||
| // 工程包含的控制逻辑集合 | // 工程包含的控制逻辑集合 | ||||
| std::vector<ControlLogic> controlLogics; | std::vector<ControlLogic> controlLogics; | ||||
| // 按地址查找工程注释,找不到时返回 nullptr | |||||
| const RegisterComment *findRegisterComment(const RegisterAddress &address) const; | const RegisterComment *findRegisterComment(const RegisterAddress &address) const; | ||||
| // 校验工程配置并通过 error 返回失败原因 | |||||
| // 编辑态校验:允许尚未绑定完成的草稿 | |||||
| bool validate(std::string *error = nullptr) const; | bool validate(std::string *error = nullptr) const; | ||||
| // 运行态校验:额外要求所有运行所需对象都已配置 | |||||
| bool validateForRunning(std::string *error = nullptr) const; | bool validateForRunning(std::string *error = nullptr) const; | ||||
| }; | }; | ||||
| @@ -2,6 +2,7 @@ | |||||
| #include <string> | #include <string> | ||||
| // 解析用户输入的 M/D 地址时可能出现的失败原因 | |||||
| enum class RegisterAddressParseError | enum class RegisterAddressParseError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -11,23 +12,30 @@ enum class RegisterAddressParseError | |||||
| OutOfRange | OutOfRange | ||||
| }; | }; | ||||
| // PLC 中支持的寄存器区域:M 是位,D 是 16 位有符号字 | |||||
| enum class RegisterArea | enum class RegisterArea | ||||
| { | { | ||||
| M, | M, | ||||
| D | D | ||||
| }; | }; | ||||
| // 表示一个已经拆分为“区域 + 下标”的 M/D 地址 | |||||
| // 下标统一使用 0~4000,和 Modbus 原始地址保持一致 | |||||
| class RegisterAddress | class RegisterAddress | ||||
| { | { | ||||
| public: | public: | ||||
| static constexpr int kMinimumIndex = 0; | static constexpr int kMinimumIndex = 0; | ||||
| static constexpr int kMaximumIndex = 4000; | static constexpr int kMaximumIndex = 4000; | ||||
| // 构造地址对象;非法下标不会在构造时抛异常,应通过 isValid() 检查 | |||||
| RegisterAddress(RegisterArea area, int index); | RegisterAddress(RegisterArea area, int index); | ||||
| // 返回地址区域和数字下标 | |||||
| RegisterArea area() const; | RegisterArea area() const; | ||||
| int index() const; | int index() const; | ||||
| // 判断区域和下标是否满足项目边界 | |||||
| bool isValid() const; | bool isValid() const; | ||||
| // 转成界面和日志中使用的形式,例如 M10 或 D4000 | |||||
| std::string toString() const; | std::string toString() const; | ||||
| bool operator==(const RegisterAddress &other) const; | bool operator==(const RegisterAddress &other) const; | ||||
| @@ -38,6 +46,7 @@ private: | |||||
| int index_; | int index_; | ||||
| }; | }; | ||||
| // 文本地址解析结果;失败时 address 保持默认值,具体原因见 error | |||||
| struct RegisterAddressParseResult | struct RegisterAddressParseResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -45,4 +54,5 @@ struct RegisterAddressParseResult | |||||
| RegisterAddressParseError error = RegisterAddressParseError::InvalidFormat; | RegisterAddressParseError error = RegisterAddressParseError::InvalidFormat; | ||||
| }; | }; | ||||
| // 解析 M0、D4000 这类地址文本,不接受带偏移的 Modbus 地址 | |||||
| RegisterAddressParseResult parseRegisterAddress(const std::string &text); | RegisterAddressParseResult parseRegisterAddress(const std::string &text); | ||||
| @@ -6,6 +6,7 @@ | |||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <vector> | #include <vector> | ||||
| // 监控读数的可用状态 | |||||
| enum class MonitorValueState | enum class MonitorValueState | ||||
| { | { | ||||
| Valid, | Valid, | ||||
| @@ -13,6 +14,7 @@ enum class MonitorValueState | |||||
| CommunicationFault | CommunicationFault | ||||
| }; | }; | ||||
| // 一个监控行的地址、可用性和当前值 | |||||
| struct MonitorValue | struct MonitorValue | ||||
| { | { | ||||
| RegisterAddress address{RegisterArea::M, 0}; | RegisterAddress address{RegisterArea::M, 0}; | ||||
| @@ -21,15 +23,21 @@ struct MonitorValue | |||||
| std::int16_t wordValue = 0; | std::int16_t wordValue = 0; | ||||
| }; | }; | ||||
| // 运行期间由用户临时维护的监控地址列表,不写入工程文件 | |||||
| class RegisterMonitorModel | class RegisterMonitorModel | ||||
| { | { | ||||
| public: | public: | ||||
| static constexpr std::size_t kMaximumItemCount = 64U; | static constexpr std::size_t kMaximumItemCount = 64U; | ||||
| // 返回当前按添加顺序保存的地址 | |||||
| const std::vector<RegisterAddress> &addresses() const; | const std::vector<RegisterAddress> &addresses() const; | ||||
| // 判断地址是否已经在监控列表中 | |||||
| bool contains(const RegisterAddress &address) const; | bool contains(const RegisterAddress &address) const; | ||||
| // 添加一个地址;重复或超过上限时返回 false | |||||
| bool add(const RegisterAddress &address); | bool add(const RegisterAddress &address); | ||||
| // 删除一个地址;地址不存在时返回 false | |||||
| bool remove(const RegisterAddress &address); | bool remove(const RegisterAddress &address); | ||||
| // 清空当前会话的监控列表 | |||||
| void clear(); | void clear(); | ||||
| private: | private: | ||||
| @@ -6,7 +6,7 @@ | |||||
| #include <cstddef> | #include <cstddef> | ||||
| #include <cstdint> | #include <cstdint> | ||||
| // 寄存器仓库操作结果 | |||||
| // 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息 | |||||
| enum class RegisterError | enum class RegisterError | ||||
| { | { | ||||
| None, // 操作成功 无错误 | None, // 操作成功 无错误 | ||||
| @@ -40,23 +40,24 @@ struct RegisterWriteResult | |||||
| }; | }; | ||||
| // M/D 寄存器访问边界 | // M/D 寄存器访问边界 | ||||
| // 所有运行态读写都必须经过此接口,UI 不直接接触串口或缓存实现 | |||||
| class RegisterRepository | class RegisterRepository | ||||
| { | { | ||||
| public: | public: | ||||
| virtual ~RegisterRepository() = default; | virtual ~RegisterRepository() = default; | ||||
| // 读取 M 区位寄存器 | |||||
| // 读取 M 区位寄存器;传入 D 区地址会返回 AreaMismatch | |||||
| virtual BitReadResult readBit(const RegisterAddress &address) const = 0; | virtual BitReadResult readBit(const RegisterAddress &address) const = 0; | ||||
| // 写入 M 区位寄存器 | |||||
| // 写入 M 区位寄存器;真机实现不会在收到 PLC 确认前乐观修改缓存 | |||||
| virtual RegisterWriteResult writeBit(const RegisterAddress &address, bool value) = 0; | virtual RegisterWriteResult writeBit(const RegisterAddress &address, bool value) = 0; | ||||
| // 读取 D 区字寄存器 | |||||
| // 读取 D 区 16 位字寄存器 | |||||
| virtual WordReadResult readWord(const RegisterAddress &address) const = 0; | virtual WordReadResult readWord(const RegisterAddress &address) const = 0; | ||||
| // 写入 D 区字寄存器 | |||||
| // 写入 D 区 16 位字寄存器 | |||||
| virtual RegisterWriteResult writeWord( | virtual RegisterWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value) = 0; | const RegisterAddress &address, std::int16_t value) = 0; | ||||
| }; | }; | ||||
| // 基于内存的离线 M/D 寄存器仓库 | |||||
| // 基于内存的离线 M/D 寄存器仓库;地址范围与真实 PLC 保持一致 | |||||
| class VirtualRegisterRepository : public RegisterRepository | class VirtualRegisterRepository : public RegisterRepository | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -72,7 +73,7 @@ public: | |||||
| RegisterWriteResult writeWord( | RegisterWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value) override; | const RegisterAddress &address, std::int16_t value) override; | ||||
| // 将所有虚拟寄存器恢复为默认值 | |||||
| // 将所有虚拟寄存器恢复为默认值,开始新的离线会话 | |||||
| void clear(); | void clear(); | ||||
| private: | private: | ||||
| @@ -1,6 +1,6 @@ | |||||
| #pragma once | #pragma once | ||||
| // 应用当前运行模式 | |||||
| // 应用当前运行模式;三个状态之间不能直接从离线切到真机 | |||||
| enum class ApplicationMode | enum class ApplicationMode | ||||
| { | { | ||||
| Editing, // 编辑工程状态 | Editing, // 编辑工程状态 | ||||
| @@ -8,7 +8,7 @@ enum class ApplicationMode | |||||
| OnlineRunning // 真机联机运行状态 | OnlineRunning // 真机联机运行状态 | ||||
| }; | }; | ||||
| // 当前运行模式对应的能力策略 | |||||
| // 当前运行模式对应的能力策略,UI 用它决定哪些入口可用 | |||||
| struct ModePolicy | struct ModePolicy | ||||
| { | { | ||||
| // 是否允许编辑工程 | // 是否允许编辑工程 | ||||
| @@ -23,7 +23,7 @@ struct ModePolicy | |||||
| bool requiresInitialPlcRead = false; | bool requiresInitialPlcRead = false; | ||||
| }; | }; | ||||
| // 根据运行模式返回对应的能力策略 | |||||
| // 根据运行模式返回对应的能力策略;default 用于防御非法枚举值 | |||||
| constexpr ModePolicy policyForMode(ApplicationMode mode) | constexpr ModePolicy policyForMode(ApplicationMode mode) | ||||
| { | { | ||||
| switch (mode) | switch (mode) | ||||
| @@ -68,7 +68,7 @@ constexpr ModePolicy policyForMode(ApplicationMode mode) | |||||
| } | } | ||||
| } | } | ||||
| // 模式切换失败原因 | |||||
| // 模式切换失败原因,服务层会进一步补充可读消息 | |||||
| enum class ModeTransitionError | enum class ModeTransitionError | ||||
| { | { | ||||
| None, // 切换成功 | None, // 切换成功 | ||||
| @@ -88,6 +88,7 @@ struct ModeTransitionResult | |||||
| ModeTransitionError error = ModeTransitionError::None; | ModeTransitionError error = ModeTransitionError::None; | ||||
| }; | }; | ||||
| // 保存当前模式并执行最基本的状态机约束 | |||||
| class RuntimeState | class RuntimeState | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -2,7 +2,7 @@ | |||||
| #include "domain/project_storage.h" | #include "domain/project_storage.h" | ||||
| // 使用版本化 JSON 文件保存和加载工程 | |||||
| // 使用严格 1.0 JSON 文件保存和加载工程;加载成功后才替换调用方工程 | |||||
| class JsonProjectStorage final : public ProjectStorage | class JsonProjectStorage final : public ProjectStorage | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -5,6 +5,7 @@ | |||||
| #include <QModbusDevice> | #include <QModbusDevice> | ||||
| #include <QString> | #include <QString> | ||||
| // Qt 错误码之外的通信现场信息,用于区分“未打开串口”和“已断线” | |||||
| struct PlcCommunicationErrorContext | struct PlcCommunicationErrorContext | ||||
| { | { | ||||
| QString portName; | QString portName; | ||||
| @@ -12,12 +13,14 @@ struct PlcCommunicationErrorContext | |||||
| bool receivedValidResponse = false; | bool receivedValidResponse = false; | ||||
| }; | }; | ||||
| // 分类后的用户可读通信故障 | |||||
| struct PlcCommunicationFailure | struct PlcCommunicationFailure | ||||
| { | { | ||||
| PlcCommunicationError type = PlcCommunicationError::Unknown; | PlcCommunicationError type = PlcCommunicationError::Unknown; | ||||
| QString message; | QString message; | ||||
| }; | }; | ||||
| // 将 Qt Modbus 错误和上下文转换为项目统一错误类型 | |||||
| PlcCommunicationFailure classifyPlcCommunicationError( | PlcCommunicationFailure classifyPlcCommunicationError( | ||||
| QModbusDevice::Error error, | QModbusDevice::Error error, | ||||
| const PlcCommunicationErrorContext &context); | const PlcCommunicationErrorContext &context); | ||||
| @@ -190,6 +190,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||||
| { | { | ||||
| return {false, "PLC 连接已经启动,请先断开当前连接"}; | return {false, "PLC 连接已经启动,请先断开当前连接"}; | ||||
| } | } | ||||
| // 每次连接都递增代次;旧连接的异步回复即使晚到也不能污染新缓存 | |||||
| configuration_ = configuration; | configuration_ = configuration; | ||||
| ++connection_generation_; | ++connection_generation_; | ||||
| disconnecting_ = false; | disconnecting_ = false; | ||||
| @@ -213,6 +214,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||||
| static_cast<QSerialPort::StopBits>(configuration.stopBits)); | static_cast<QSerialPort::StopBits>(configuration.stopBits)); | ||||
| master_->setTimeout(configuration.responseTimeoutMs); | master_->setTimeout(configuration.responseTimeoutMs); | ||||
| master_->setNumberOfRetries(configuration.retries); | master_->setNumberOfRetries(configuration.retries); | ||||
| // 新连接必须重新完成完整首读,不能沿用上一条连接的运行资格 | |||||
| repository_.invalidate(); | repository_.invalidate(); | ||||
| updateInitialReadCompleted(false, true); | updateInitialReadCompleted(false, true); | ||||
| rebuildPollBlocks(); | rebuildPollBlocks(); | ||||
| @@ -253,6 +255,7 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( | |||||
| { | { | ||||
| return {false, "PLC 轮询地址拆分后最多允许 64 个读块"}; | return {false, "PLC 轮询地址拆分后最多允许 64 个读块"}; | ||||
| } | } | ||||
| // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复 | |||||
| if (pending_reply_ != nullptr) | if (pending_reply_ != nullptr) | ||||
| { | { | ||||
| pending_poll_addresses_ = std::move(normalized); | pending_poll_addresses_ = std::move(normalized); | ||||
| @@ -320,6 +323,7 @@ void PlcCommunicationService::rebuildPollBlocks() | |||||
| return left.index() < right.index(); | return left.index() < right.index(); | ||||
| }); | }); | ||||
| addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); | addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); | ||||
| // 将相邻地址合并为读块,同时遵守 Modbus 单次读取上限 | |||||
| poll_blocks_.clear(); | poll_blocks_.clear(); | ||||
| for (const RegisterAddress &address : addresses) | for (const RegisterAddress &address : addresses) | ||||
| { | { | ||||
| @@ -373,6 +377,7 @@ void PlcCommunicationService::pollNextBlock() | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| // 每次只发一个异步请求,完成回调中再推进到下一个块 | |||||
| const std::size_t block_index = next_poll_block_; | const std::size_t block_index = next_poll_block_; | ||||
| const PollBlock block = poll_blocks_.at(block_index); | const PollBlock block = poll_blocks_.at(block_index); | ||||
| next_poll_block_ = (next_poll_block_ + 1U) % poll_blocks_.size(); | next_poll_block_ = (next_poll_block_ + 1U) % poll_blocks_.size(); | ||||
| @@ -390,6 +395,7 @@ void PlcCommunicationService::pollNextBlock() | |||||
| this, | this, | ||||
| [this, reply, block, block_index, generation] | [this, reply, block, block_index, generation] | ||||
| { | { | ||||
| // 连接已经重建时,直接丢弃旧回复 | |||||
| if (generation != connection_generation_) | if (generation != connection_generation_) | ||||
| { | { | ||||
| reply->deleteLater(); | reply->deleteLater(); | ||||
| @@ -406,6 +412,7 @@ void PlcCommunicationService::pollNextBlock() | |||||
| initial_blocks_read_.cbegin(), | initial_blocks_read_.cbegin(), | ||||
| initial_blocks_read_.cend(), | initial_blocks_read_.cend(), | ||||
| [](bool read) { return read; }); | [](bool read) { return read; }); | ||||
| // 所有轮询块都成功读过,才授予真机运行资格 | |||||
| if (completed && !initial_read_completed_) | if (completed && !initial_read_completed_) | ||||
| { | { | ||||
| updateInitialReadCompleted(true); | updateInitialReadCompleted(true); | ||||
| @@ -534,6 +541,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock | |||||
| handleModbusError(reply->error()); | handleModbusError(reply->error()); | ||||
| return; | return; | ||||
| } | } | ||||
| // 只有成功回复才能更新 PLC 缓存;写请求不会直接改缓存 | |||||
| const QModbusDataUnit result = reply->result(); | const QModbusDataUnit result = reply->result(); | ||||
| for (uint index = 0; index < result.valueCount(); ++index) | for (uint index = 0; index < result.valueCount(); ++index) | ||||
| { | { | ||||
| @@ -724,6 +732,7 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | |||||
| last_error_ = toUtf8(failure.message); | last_error_ = toUtf8(failure.message); | ||||
| poll_timer_.stop(); | poll_timer_.stop(); | ||||
| recovery_timer_.stop(); | recovery_timer_.stop(); | ||||
| // 任意通信故障都会撤销首读资格;恢复后必须重新完整读取 | |||||
| updateInitialReadCompleted(false); | updateInitialReadCompleted(false); | ||||
| const bool disconnected = failure.type == PlcCommunicationError::SerialPortOpenFailed | const bool disconnected = failure.type == PlcCommunicationError::SerialPortOpenFailed | ||||
| || failure.type == PlcCommunicationError::SerialConnectionLost | || failure.type == PlcCommunicationError::SerialConnectionLost | ||||
| @@ -17,6 +17,8 @@ class QModbusReply; | |||||
| class QModbusRtuSerialMaster; | class QModbusRtuSerialMaster; | ||||
| struct PlcCommunicationFailure; | struct PlcCommunicationFailure; | ||||
| // 基于 Qt Modbus RTU 的异步 PLC 通信实现 | |||||
| // 负责连接、轮询、单点写入、故障恢复和首读资格,不阻塞 UI 线程 | |||||
| class PlcCommunicationService final : public QObject, public PlcCommunicationGateway | class PlcCommunicationService final : public QObject, public PlcCommunicationGateway | ||||
| { | { | ||||
| Q_OBJECT | Q_OBJECT | ||||
| @@ -53,21 +55,29 @@ signals: | |||||
| private: | private: | ||||
| struct PollBlock | struct PollBlock | ||||
| { | { | ||||
| // 同一区域的一段连续 Modbus 原始地址 | |||||
| RegisterArea area = RegisterArea::M; | RegisterArea area = RegisterArea::M; | ||||
| int startAddress = 0; | int startAddress = 0; | ||||
| int count = 1; | int count = 1; | ||||
| }; | }; | ||||
| // 把去重后的地址集合压缩为有限数量的连续读块 | |||||
| void rebuildPollBlocks(); | void rebuildPollBlocks(); | ||||
| // 应用新的轮询集合;有请求在途时由 pending_* 延后应用 | |||||
| void applyPollAddresses(const std::vector<RegisterAddress> &addresses); | void applyPollAddresses(const std::vector<RegisterAddress> &addresses); | ||||
| // 发送下一段异步读取请求 | |||||
| void pollNextBlock(); | void pollNextBlock(); | ||||
| // Faulted 状态下用轻量探测判断通信是否恢复 | |||||
| void probeRecovery(); | void probeRecovery(); | ||||
| void handleRecoveryProbeFailure(QModbusDevice::Error error); | void handleRecoveryProbeFailure(QModbusDevice::Error error); | ||||
| void restoreCommunication(); | void restoreCommunication(); | ||||
| // 处理读回复并把值写入 PLC 缓存 | |||||
| void handleReadFinished(QModbusReply *reply, PollBlock block); | void handleReadFinished(QModbusReply *reply, PollBlock block); | ||||
| // 发送单点 M/D 写请求;缓存等待后续轮询确认 | |||||
| RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); | RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); | ||||
| RegisterWriteResult sendWordWrite( | RegisterWriteResult sendWordWrite( | ||||
| const RegisterAddress &address, std::int16_t value); | const RegisterAddress &address, std::int16_t value); | ||||
| // 更新“所有轮询块均成功读取”的真机进入资格 | |||||
| void updateInitialReadCompleted(bool completed, bool force_notification = false); | void updateInitialReadCompleted(bool completed, bool force_notification = false); | ||||
| void handleUnexpectedDisconnect(); | void handleUnexpectedDisconnect(); | ||||
| void handleModbusError(QModbusDevice::Error error); | void handleModbusError(QModbusDevice::Error error); | ||||
| @@ -93,6 +103,7 @@ private: | |||||
| std::uint64_t connection_generation_ = 0; | std::uint64_t connection_generation_ = 0; | ||||
| PlcConnectionState state_ = PlcConnectionState::Disconnected; | PlcConnectionState state_ = PlcConnectionState::Disconnected; | ||||
| bool initial_read_completed_ = false; | bool initial_read_completed_ = false; | ||||
| // 首读阶段每个读块的完成标记,全部为 true 后才允许进入真机 | |||||
| std::vector<bool> initial_blocks_read_; | std::vector<bool> initial_blocks_read_; | ||||
| PlcCommunicationError last_error_type_ = PlcCommunicationError::None; | PlcCommunicationError last_error_type_ = PlcCommunicationError::None; | ||||
| std::string last_error_; | std::string last_error_; | ||||
| @@ -5,6 +5,8 @@ | |||||
| #include <array> | #include <array> | ||||
| #include <functional> | #include <functional> | ||||
| // 保存 PLC 最近一次成功读回的 M/D 缓存,并把写请求转交给通信服务 | |||||
| // 未读到过的地址保持 Unavailable,避免界面显示伪造的初始值 | |||||
| class PlcRegisterRepository final : public RegisterRepository | class PlcRegisterRepository final : public RegisterRepository | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -16,12 +18,16 @@ public: | |||||
| RegisterWriteResult writeWord( | RegisterWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value) override; | const RegisterAddress &address, std::int16_t value) override; | ||||
| // 注入异步写入回调;回调成功不代表缓存已经更新 | |||||
| void setWriteHandlers( | void setWriteHandlers( | ||||
| std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler, | std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler, | ||||
| std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler); | std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler); | ||||
| // 由通信服务在读回成功后更新缓存并标记地址有效 | |||||
| void updateBit(int address, bool value); | void updateBit(int address, bool value); | ||||
| void updateWord(int address, std::int16_t value); | void updateWord(int address, std::int16_t value); | ||||
| // 通信会话失效时清除“已读”标记,但保留数组内的旧值用于诊断 | |||||
| void invalidate(); | void invalidate(); | ||||
| // 判断是否至少收到过一个有效 M/D 值 | |||||
| bool hasAnyValidValue() const; | bool hasAnyValidValue() const; | ||||
| private: | private: | ||||
| @@ -7,6 +7,7 @@ | |||||
| class ProjectService; | class ProjectService; | ||||
| // 报警编辑操作的失败分类 | |||||
| enum class AlarmEditorError | enum class AlarmEditorError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -15,6 +16,7 @@ enum class AlarmEditorError | |||||
| InvalidDefinition | InvalidDefinition | ||||
| }; | }; | ||||
| // 报警编辑结果;成功时 id 是新增或更新后的定义 ID | |||||
| struct AlarmEditorResult | struct AlarmEditorResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -23,16 +25,22 @@ struct AlarmEditorResult | |||||
| std::string id; | std::string id; | ||||
| }; | }; | ||||
| // 负责报警定义的增删改,不负责读取实时寄存器 | |||||
| class AlarmEditorService | class AlarmEditorService | ||||
| { | { | ||||
| public: | public: | ||||
| explicit AlarmEditorService(ProjectService &project_service); | explicit AlarmEditorService(ProjectService &project_service); | ||||
| // 返回当前工程中的报警定义 | |||||
| const std::vector<AlarmDefinition> &definitions() const; | const std::vector<AlarmDefinition> &definitions() const; | ||||
| // 按稳定 ID 查找报警定义 | |||||
| const AlarmDefinition *findDefinition(const std::string &id) const; | const AlarmDefinition *findDefinition(const std::string &id) const; | ||||
| // 添加定义;失败时工程保持不变 | |||||
| AlarmEditorResult addDefinition(const AlarmDefinition &definition); | AlarmEditorResult addDefinition(const AlarmDefinition &definition); | ||||
| // 更新指定 ID;definition.id 会被服务统一为传入的 id | |||||
| AlarmEditorResult updateDefinition( | AlarmEditorResult updateDefinition( | ||||
| const std::string &id, const AlarmDefinition &definition); | const std::string &id, const AlarmDefinition &definition); | ||||
| // 删除指定报警定义 | |||||
| AlarmEditorResult removeDefinition(const std::string &id); | AlarmEditorResult removeDefinition(const std::string &id); | ||||
| private: | private: | ||||
| @@ -16,6 +16,7 @@ AlarmService::AlarmService( | |||||
| void AlarmService::refresh() | void AlarmService::refresh() | ||||
| { | { | ||||
| // 每次刷新只根据当前读回值增删记录,不会把通信失败误判为报警解除 | |||||
| const auto now = std::chrono::system_clock::now(); | const auto now = std::chrono::system_clock::now(); | ||||
| for (const AlarmDefinition &definition | for (const AlarmDefinition &definition | ||||
| : project_service_.project().alarmDefinitions) | : project_service_.project().alarmDefinitions) | ||||
| @@ -9,6 +9,7 @@ class ProjectService; | |||||
| class RegisterRepository; | class RegisterRepository; | ||||
| struct AlarmDefinition; | struct AlarmDefinition; | ||||
| // 运行时产生的报警记录,不会写入工程 JSON | |||||
| struct AlarmRecord | struct AlarmRecord | ||||
| { | { | ||||
| std::string definitionId; | std::string definitionId; | ||||
| @@ -17,6 +18,7 @@ struct AlarmRecord | |||||
| std::chrono::system_clock::time_point occurredAt; | std::chrono::system_clock::time_point occurredAt; | ||||
| }; | }; | ||||
| // 根据当前活动寄存器仓库评估报警,并维护确认状态 | |||||
| class AlarmService | class AlarmService | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -24,9 +26,13 @@ public: | |||||
| const ProjectService &project_service, | const ProjectService &project_service, | ||||
| RegisterRepository &repository); | RegisterRepository &repository); | ||||
| // 重新评估全部定义;仍处于触发状态的记录会保留确认状态 | |||||
| void refresh(); | void refresh(); | ||||
| // 确认指定报警;不存在时返回 false | |||||
| bool acknowledge(const std::string &definition_id); | bool acknowledge(const std::string &definition_id); | ||||
| // 清空当前运行会话的报警记录 | |||||
| void reset(); | void reset(); | ||||
| // 返回按触发时间保存的报警记录 | |||||
| const std::vector<AlarmRecord> &records() const; | const std::vector<AlarmRecord> &records() const; | ||||
| private: | private: | ||||
| @@ -17,6 +17,7 @@ public: | |||||
| static constexpr std::size_t kMaximumEntries = 100U; | static constexpr std::size_t kMaximumEntries = 100U; | ||||
| template <typename Equal> | template <typename Equal> | ||||
| // 只有前后状态不同才记录;新编辑会清空重做栈 | |||||
| void record(State before, const State &after, Equal equal) | void record(State before, const State &after, Equal equal) | ||||
| { | { | ||||
| if (equal(before, after)) | if (equal(before, after)) | ||||
| @@ -28,6 +29,7 @@ public: | |||||
| redo_states_.clear(); | redo_states_.clear(); | ||||
| } | } | ||||
| // 返回撤销后的目标状态,并把当前状态放入重做栈 | |||||
| std::optional<State> undo(State current) | std::optional<State> undo(State current) | ||||
| { | { | ||||
| if (undo_states_.empty()) | if (undo_states_.empty()) | ||||
| @@ -41,6 +43,7 @@ public: | |||||
| return target; | return target; | ||||
| } | } | ||||
| // 返回重做后的目标状态,并把当前状态放回撤销栈 | |||||
| std::optional<State> redo(State current) | std::optional<State> redo(State current) | ||||
| { | { | ||||
| if (redo_states_.empty()) | if (redo_states_.empty()) | ||||
| @@ -526,6 +526,7 @@ HmiEditorResult HmiEditorService::moveControl( | |||||
| HmiControl candidate = *control; | HmiControl candidate = *control; | ||||
| candidate.bounds = bounds; | candidate.bounds = bounds; | ||||
| // 更新前保留原控件,只有全部编辑规则通过才覆盖原值 | // 更新前保留原控件,只有全部编辑规则通过才覆盖原值 | ||||
| // 属性更新是整体替换:类型不变,但绑定、外观和扩展属性必须一起通过校验 | |||||
| std::string error; | std::string error; | ||||
| if (!validateEditableControl(*page, candidate, &error)) | if (!validateEditableControl(*page, candidate, &error)) | ||||
| { | { | ||||
| @@ -11,6 +11,7 @@ HmiNavigationService::HmiNavigationService(ProjectService &project_service) | |||||
| HmiNavigationResult HmiNavigationService::start() | HmiNavigationResult HmiNavigationService::start() | ||||
| { | { | ||||
| // 运行开始始终从工程声明的初始页面进入,而不是沿用上次会话页面 | |||||
| const Project &project = project_service_.project(); | const Project &project = project_service_.project(); | ||||
| if (project.hmiPages.empty()) | if (project.hmiPages.empty()) | ||||
| { | { | ||||
| @@ -4,6 +4,7 @@ | |||||
| class ProjectService; | class ProjectService; | ||||
| // HMI 页面跳转失败分类 | |||||
| enum class HmiNavigationError | enum class HmiNavigationError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -11,6 +12,7 @@ enum class HmiNavigationError | |||||
| PageNotFound | PageNotFound | ||||
| }; | }; | ||||
| // 页面跳转结果;成功时 pageId 是当前页 ID | |||||
| struct HmiNavigationResult | struct HmiNavigationResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -19,14 +21,19 @@ struct HmiNavigationResult | |||||
| std::string pageId; | std::string pageId; | ||||
| }; | }; | ||||
| // 管理运行态当前页面,不直接操作 Qt 页面控件 | |||||
| class HmiNavigationService | class HmiNavigationService | ||||
| { | { | ||||
| public: | public: | ||||
| explicit HmiNavigationService(ProjectService &project_service); | explicit HmiNavigationService(ProjectService &project_service); | ||||
| // 进入工程初始页面 | |||||
| HmiNavigationResult start(); | HmiNavigationResult start(); | ||||
| // 跳转到指定页面;目标不存在时保留原页面 | |||||
| HmiNavigationResult navigateTo(const std::string &page_id); | HmiNavigationResult navigateTo(const std::string &page_id); | ||||
| // 结束运行会话并清空当前页 | |||||
| void stop(); | void stop(); | ||||
| // 返回当前运行页面 ID,未启动时为空 | |||||
| const std::string ¤tPageId() const; | const std::string ¤tPageId() const; | ||||
| private: | private: | ||||
| @@ -60,6 +60,7 @@ enum class HmiButtonEvent | |||||
| * | * | ||||
| * 服务不依赖虚拟寄存器实现、串口或 Modbus,可由运行模式注入不同仓库 | * 服务不依赖虚拟寄存器实现、串口或 Modbus,可由运行模式注入不同仓库 | ||||
| */ | */ | ||||
| // 仅通过统一寄存器仓库执行 HMI 运行态读写 | |||||
| class HmiRuntimeService | class HmiRuntimeService | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -918,6 +918,7 @@ LogicEditorResult LogicEditorService::appendCondition( | |||||
| { | { | ||||
| return failure(LogicEditorError::InvalidNode, "梯形图条件不能使用线圈节点"); | return failure(LogicEditorError::InvalidNode, "梯形图条件不能使用线圈节点"); | ||||
| } | } | ||||
| // 先生成稳定节点 ID,再把新节点接到已有表达式的串联末尾 | |||||
| const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | ||||
| ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); | ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); | ||||
| HistoryState before = captureState(); | HistoryState before = captureState(); | ||||
| @@ -941,6 +942,7 @@ LogicEditorResult LogicEditorService::appendCondition( | |||||
| std::move(leaf)); | std::move(leaf)); | ||||
| } | } | ||||
| std::string validation_error; | std::string validation_error; | ||||
| // 编辑服务修改后立即做整条网络校验;失败就恢复包括脏标记在内的完整快照 | |||||
| if (!rung->validate(&validation_error)) | if (!rung->validate(&validation_error)) | ||||
| { | { | ||||
| rollbackEdit(std::move(before), modified_before); | rollbackEdit(std::move(before), modified_before); | ||||
| @@ -981,6 +983,7 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn( | |||||
| return failure(LogicEditorError::InvalidOperation, "所选网格已被条件或横线占用"); | return failure(LogicEditorError::InvalidOperation, "所选网格已被条件或横线占用"); | ||||
| } | } | ||||
| // 点击远端空网格时,用持久化横线填充前置空档,再放入真实条件节点 | |||||
| const int leading_wire_columns = column - occupied_columns; | const int leading_wire_columns = column - occupied_columns; | ||||
| const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | ||||
| ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); | ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); | ||||
| @@ -1035,6 +1038,7 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn( | |||||
| } | } | ||||
| std::string validation_error; | std::string validation_error; | ||||
| // 原子提交要求候选表达式整体通过校验,否则撤销本次所有局部拼接 | |||||
| if (!rung->validate(&validation_error)) | if (!rung->validate(&validation_error)) | ||||
| { | { | ||||
| rollbackEdit(std::move(before), modified_before); | rollbackEdit(std::move(before), modified_before); | ||||
| @@ -8,6 +8,7 @@ | |||||
| class ProjectService; | class ProjectService; | ||||
| // 梯形图编辑失败分类 | |||||
| enum class LogicEditorError | enum class LogicEditorError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -22,6 +23,7 @@ enum class LogicEditorError | |||||
| LastLogicRequired | LastLogicRequired | ||||
| }; | }; | ||||
| // 梯形图编辑结果;成功时 id 通常是新建或更新对象的稳定 ID | |||||
| struct LogicEditorResult | struct LogicEditorResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -30,11 +32,13 @@ struct LogicEditorResult | |||||
| std::string id; | std::string id; | ||||
| }; | }; | ||||
| // 负责把 UI 编辑命令转换为结构化表达式树操作,并维护撤销/重做 | |||||
| class LogicEditorService | class LogicEditorService | ||||
| { | { | ||||
| public: | public: | ||||
| explicit LogicEditorService(ProjectService &project_service); | explicit LogicEditorService(ProjectService &project_service); | ||||
| // 以下查询接口只读工程模型,供编辑器投影和属性面板使用 | |||||
| const ControlLogic *findLogic(const std::string &logic_id) const; | const ControlLogic *findLogic(const std::string &logic_id) const; | ||||
| const LadderRung *findRung( | const LadderRung *findRung( | ||||
| const std::string &logic_id, const std::string &rung_id) const; | const std::string &logic_id, const std::string &rung_id) const; | ||||
| @@ -50,13 +54,16 @@ public: | |||||
| const std::string &logic_id, const std::string &node_id) const; | const std::string &logic_id, const std::string &node_id) const; | ||||
| std::string registerCommentFor(const RegisterAddress &address) const; | std::string registerCommentFor(const RegisterAddress &address) const; | ||||
| // 确保新工程至少有一组可编辑逻辑 | |||||
| LogicEditorResult ensureDefaultLogic(); | LogicEditorResult ensureDefaultLogic(); | ||||
| // 逻辑组管理 | |||||
| LogicEditorResult addLogic(const std::string &name); | LogicEditorResult addLogic(const std::string &name); | ||||
| LogicEditorResult renameLogic( | LogicEditorResult renameLogic( | ||||
| const std::string &logic_id, const std::string &name); | const std::string &logic_id, const std::string &name); | ||||
| LogicEditorResult removeLogic(const std::string &logic_id); | LogicEditorResult removeLogic(const std::string &logic_id); | ||||
| LogicEditorResult moveLogic(const std::string &logic_id, int offset); | LogicEditorResult moveLogic(const std::string &logic_id, int offset); | ||||
| LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled); | LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled); | ||||
| // 网络管理 | |||||
| LogicEditorResult addRung(const std::string &logic_id); | LogicEditorResult addRung(const std::string &logic_id); | ||||
| LogicEditorResult removeRung( | LogicEditorResult removeRung( | ||||
| const std::string &logic_id, const std::string &rung_id); | const std::string &logic_id, const std::string &rung_id); | ||||
| @@ -64,6 +71,7 @@ public: | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &comment); | const std::string &comment); | ||||
| // 条件区编辑:串联、按列插入、横线和并联分支 | |||||
| LogicEditorResult appendCondition( | LogicEditorResult appendCondition( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -107,11 +115,13 @@ public: | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::vector<std::string> &selected_expression_ids); | const std::vector<std::string> &selected_expression_ids); | ||||
| // 输出槽编辑;每个网络最多一个输出节点 | |||||
| LogicEditorResult setOutput( | LogicEditorResult setOutput( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const LogicNodeConfig &config, | const LogicNodeConfig &config, | ||||
| bool configured = false); | bool configured = false); | ||||
| // 节点属性和删除操作 | |||||
| LogicEditorResult updateNodeConfig( | LogicEditorResult updateNodeConfig( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &node_id, | const std::string &node_id, | ||||
| @@ -130,6 +140,7 @@ public: | |||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::vector<std::string> &expression_ids); | const std::vector<std::string> &expression_ids); | ||||
| // 当前编辑会话的撤销/重做 | |||||
| bool canUndo() const; | bool canUndo() const; | ||||
| bool canRedo() const; | bool canRedo() const; | ||||
| LogicEditorResult undo(); | LogicEditorResult undo(); | ||||
| @@ -137,6 +148,7 @@ public: | |||||
| void clearHistory(); | void clearHistory(); | ||||
| private: | private: | ||||
| // 只保存逻辑集合快照,当前选择等 UI 会话状态不进入历史 | |||||
| struct HistoryState | struct HistoryState | ||||
| { | { | ||||
| std::vector<ControlLogic> logics; | std::vector<ControlLogic> logics; | ||||
| @@ -144,6 +156,7 @@ private: | |||||
| HistoryState captureState() const; | HistoryState captureState() const; | ||||
| void recordHistory(HistoryState before); | void recordHistory(HistoryState before); | ||||
| // 失败时恢复工程内容和操作前脏标记,保证编辑原子性 | |||||
| void rollbackEdit(HistoryState before, bool modified_before); | void rollbackEdit(HistoryState before, bool modified_before); | ||||
| static bool statesEqual( | static bool statesEqual( | ||||
| const HistoryState &left, const HistoryState &right); | const HistoryState &left, const HistoryState &right); | ||||
| @@ -22,6 +22,7 @@ SimulationStartResult OfflineSimulationService::start( | |||||
| return {false, SimulationStartError::AlreadyRunning, {}}; | return {false, SimulationStartError::AlreadyRunning, {}}; | ||||
| } | } | ||||
| // 仿真期间使用启动时快照,编辑器后续修改不会改变正在扫描的逻辑 | |||||
| std::vector<ControlLogic> snapshot = logics; | std::vector<ControlLogic> snapshot = logics; | ||||
| const LogicScanResult validation = executor_.validate(snapshot); | const LogicScanResult validation = executor_.validate(snapshot); | ||||
| if (!validation.succeeded) | if (!validation.succeeded) | ||||
| @@ -30,6 +31,7 @@ SimulationStartResult OfflineSimulationService::start( | |||||
| return {false, SimulationStartError::InvalidLogic, validation}; | return {false, SimulationStartError::InvalidLogic, validation}; | ||||
| } | } | ||||
| // 重启会话时清空 T/C/沿触发状态和虚拟 M/D,避免继承上次运行现场 | |||||
| timer_.stop(); | timer_.stop(); | ||||
| executor_.resetRuntime(); | executor_.resetRuntime(); | ||||
| repository_.clear(); | repository_.clear(); | ||||
| @@ -68,6 +70,7 @@ LogicScanResult OfflineSimulationService::executeOnce() | |||||
| {}, | {}, | ||||
| {}}; | {}}; | ||||
| } | } | ||||
| // 执行器负责一轮完整扫描;任何读写错误都会让服务进入 Faulted | |||||
| const LogicScanResult result = executor_.executeScan( | const LogicScanResult result = executor_.executeScan( | ||||
| logic_snapshot_, repository_, &trace_snapshot_); | logic_snapshot_, repository_, &trace_snapshot_); | ||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| @@ -112,6 +115,7 @@ void OfflineSimulationService::handleTimeout() | |||||
| void OfflineSimulationService::enterFault(const LogicScanResult &error) | void OfflineSimulationService::enterFault(const LogicScanResult &error) | ||||
| { | { | ||||
| // 故障后停止定时器和运行时状态,保留错误及最后轨迹供 UI 诊断 | |||||
| timer_.stop(); | timer_.stop(); | ||||
| executor_.resetRuntime(); | executor_.resetRuntime(); | ||||
| last_error_ = error; | last_error_ = error; | ||||
| @@ -8,6 +8,7 @@ | |||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <vector> | #include <vector> | ||||
| // 离线扫描器的生命周期状态 | |||||
| enum class SimulationState | enum class SimulationState | ||||
| { | { | ||||
| Stopped, | Stopped, | ||||
| @@ -15,6 +16,7 @@ enum class SimulationState | |||||
| Faulted | Faulted | ||||
| }; | }; | ||||
| // 启动离线仿真失败的原因 | |||||
| enum class SimulationStartError | enum class SimulationStartError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -22,6 +24,7 @@ enum class SimulationStartError | |||||
| InvalidLogic | InvalidLogic | ||||
| }; | }; | ||||
| // 离线仿真启动结果;detail 保存领域执行器的具体校验错误 | |||||
| struct SimulationStartResult | struct SimulationStartResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -41,14 +44,18 @@ public: | |||||
| VirtualRegisterRepository &repository, | VirtualRegisterRepository &repository, | ||||
| QObject *parent = nullptr); | QObject *parent = nullptr); | ||||
| // 复制工程逻辑作为本次会话快照并启动定时扫描 | |||||
| SimulationStartResult start(const std::vector<ControlLogic> &logics); | SimulationStartResult start(const std::vector<ControlLogic> &logics); | ||||
| // 停止扫描但保留最近一次轨迹供 UI 显示 | |||||
| void stop(); | void stop(); | ||||
| // 立即执行一轮扫描,主要供定时器和调试调用 | |||||
| LogicScanResult executeOnce(); | LogicScanResult executeOnce(); | ||||
| SimulationState state() const; | SimulationState state() const; | ||||
| int scanIntervalMs() const; | int scanIntervalMs() const; | ||||
| std::uint64_t successfulScanCount() const; | std::uint64_t successfulScanCount() const; | ||||
| const LogicScanResult &lastError() const; | const LogicScanResult &lastError() const; | ||||
| // 返回最近一轮按逻辑 ID 隔离的运行轨迹 | |||||
| const LogicTraceSnapshot &traceSnapshot() const; | const LogicTraceSnapshot &traceSnapshot() const; | ||||
| signals: | signals: | ||||
| @@ -9,6 +9,7 @@ | |||||
| #include <string> | #include <string> | ||||
| #include <vector> | #include <vector> | ||||
| // PLC 串口和轮询参数;parity 使用 Qt 约定值:0 无、2 偶、3 奇 | |||||
| struct PlcSerialConfiguration | struct PlcSerialConfiguration | ||||
| { | { | ||||
| std::string portName; | std::string portName; | ||||
| @@ -22,6 +23,7 @@ struct PlcSerialConfiguration | |||||
| int pollIntervalMs = 200; | int pollIntervalMs = 200; | ||||
| }; | }; | ||||
| // PLC 异步通信生命周期 | |||||
| enum class PlcConnectionState | enum class PlcConnectionState | ||||
| { | { | ||||
| Disconnected, | Disconnected, | ||||
| @@ -31,6 +33,7 @@ enum class PlcConnectionState | |||||
| Faulted | Faulted | ||||
| }; | }; | ||||
| // 通信故障分类,用于状态栏提示和真机运行资格撤销 | |||||
| enum class PlcCommunicationError | enum class PlcCommunicationError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -47,12 +50,14 @@ enum class PlcCommunicationError | |||||
| Unknown | Unknown | ||||
| }; | }; | ||||
| // 通信命令的受理结果;真正的读写完成由回调和缓存更新通知 | |||||
| struct PlcCommunicationResult | struct PlcCommunicationResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| std::string message; | std::string message; | ||||
| }; | }; | ||||
| // 校验串口配置,不打开串口也不产生副作用 | |||||
| inline PlcCommunicationResult validatePlcSerialConfiguration( | inline PlcCommunicationResult validatePlcSerialConfiguration( | ||||
| const PlcSerialConfiguration &configuration) | const PlcSerialConfiguration &configuration) | ||||
| { | { | ||||
| @@ -108,6 +113,7 @@ inline PlcCommunicationResult validatePlcSerialConfiguration( | |||||
| return {true, {}}; | return {true, {}}; | ||||
| } | } | ||||
| // UI/运行服务使用的异步 PLC 通信契约,具体实现位于 infrastructure | |||||
| class PlcCommunicationGateway | class PlcCommunicationGateway | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -116,12 +122,14 @@ public: | |||||
| virtual PlcCommunicationResult connectDevice( | virtual PlcCommunicationResult connectDevice( | ||||
| const PlcSerialConfiguration &configuration) = 0; | const PlcSerialConfiguration &configuration) = 0; | ||||
| virtual void disconnectDevice() = 0; | virtual void disconnectDevice() = 0; | ||||
| // 设置后续轮询的 M/D 地址集合;实现可以合并相邻地址块 | |||||
| virtual PlcCommunicationResult setPollAddresses( | virtual PlcCommunicationResult setPollAddresses( | ||||
| const std::vector<RegisterAddress> &addresses) = 0; | const std::vector<RegisterAddress> &addresses) = 0; | ||||
| virtual PlcConnectionState state() const = 0; | virtual PlcConnectionState state() const = 0; | ||||
| virtual bool initialReadCompleted() const = 0; | virtual bool initialReadCompleted() const = 0; | ||||
| virtual PlcCommunicationError lastErrorType() const = 0; | virtual PlcCommunicationError lastErrorType() const = 0; | ||||
| virtual const std::string &lastError() const = 0; | virtual const std::string &lastError() const = 0; | ||||
| // 注册通信状态、首读资格、缓存更新和错误通知 | |||||
| virtual void setCallbacks( | virtual void setCallbacks( | ||||
| std::function<void()> state_changed, | std::function<void()> state_changed, | ||||
| std::function<void(bool)> initial_read_changed, | std::function<void(bool)> initial_read_changed, | ||||
| @@ -134,6 +134,7 @@ ProjectOperationResult ProjectService::load(const std::string &file_path) | |||||
| "必须指定工程文件路径"}; | "必须指定工程文件路径"}; | ||||
| } | } | ||||
| // 存储层先在临时结果中解析,避免半个工程替换当前工程 | |||||
| ProjectLoadResult result = storage_.load(file_path); | ProjectLoadResult result = storage_.load(file_path); | ||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| @@ -14,7 +14,7 @@ enum class ProjectServiceError | |||||
| StorageFailure // 工程文件存储操作失败 | StorageFailure // 工程文件存储操作失败 | ||||
| }; | }; | ||||
| // 工程管理操作结果 | |||||
| // 工程管理操作结果;失败时当前工程和文件路径保持原状 | |||||
| struct ProjectOperationResult | struct ProjectOperationResult | ||||
| { | { | ||||
| bool succeeded = false; // 操作是否成功 | bool succeeded = false; // 操作是否成功 | ||||
| @@ -30,7 +30,9 @@ public: | |||||
| // 使用外部提供的工程存储实现创建服务 | // 使用外部提供的工程存储实现创建服务 | ||||
| explicit ProjectService(ProjectStorage &storage); | explicit ProjectService(ProjectStorage &storage); | ||||
| // 只读访问当前工程;UI 和其他服务不应绕过 editProject 修改它 | |||||
| const Project &project() const; | const Project &project() const; | ||||
| // 获取服务层授权的可编辑引用,调用方必须随后完成校验 | |||||
| Project &editProject(); | Project &editProject(); | ||||
| const std::string ¤tFilePath() const; | const std::string ¤tFilePath() const; | ||||
| bool hasCurrentFile() const; | bool hasCurrentFile() const; | ||||
| @@ -7,6 +7,7 @@ | |||||
| class ProjectService; | class ProjectService; | ||||
| // 工程级寄存器注释操作的失败分类 | |||||
| enum class RegisterCommentError | enum class RegisterCommentError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -14,6 +15,7 @@ enum class RegisterCommentError | |||||
| NotFound | NotFound | ||||
| }; | }; | ||||
| // 注释编辑结果 | |||||
| struct RegisterCommentResult | struct RegisterCommentResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -21,15 +23,19 @@ struct RegisterCommentResult | |||||
| std::string message; | std::string message; | ||||
| }; | }; | ||||
| // 维护工程里的 M/D 注释,不读取也不修改寄存器实时值 | |||||
| class RegisterCommentService | class RegisterCommentService | ||||
| { | { | ||||
| public: | public: | ||||
| explicit RegisterCommentService(ProjectService &project_service); | explicit RegisterCommentService(ProjectService &project_service); | ||||
| const std::vector<RegisterComment> &comments() const; | const std::vector<RegisterComment> &comments() const; | ||||
| // 按地址查找注释 | |||||
| const RegisterComment *findComment(const RegisterAddress &address) const; | const RegisterComment *findComment(const RegisterAddress &address) const; | ||||
| // 新建或更新注释;空文本通常表示删除该注释 | |||||
| RegisterCommentResult setComment( | RegisterCommentResult setComment( | ||||
| const RegisterAddress &address, const std::string &text); | const RegisterAddress &address, const std::string &text); | ||||
| // 删除指定地址注释 | |||||
| RegisterCommentResult removeComment(const RegisterAddress &address); | RegisterCommentResult removeComment(const RegisterAddress &address); | ||||
| private: | private: | ||||
| @@ -8,6 +8,7 @@ | |||||
| #include <string> | #include <string> | ||||
| #include <vector> | #include <vector> | ||||
| // 自由监控列表编辑的失败分类 | |||||
| enum class RegisterMonitorError | enum class RegisterMonitorError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -17,6 +18,7 @@ enum class RegisterMonitorError | |||||
| NoChange | NoChange | ||||
| }; | }; | ||||
| // 监控列表编辑结果;affectedCount 表示本次受影响的地址数量 | |||||
| struct RegisterMonitorResult | struct RegisterMonitorResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -25,6 +27,7 @@ struct RegisterMonitorResult | |||||
| int affectedCount = 0; | int affectedCount = 0; | ||||
| }; | }; | ||||
| // 自由监控写入结果;底层 RegisterError 会原样保留 | |||||
| struct RegisterMonitorWriteResult | struct RegisterMonitorWriteResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -32,15 +35,21 @@ struct RegisterMonitorWriteResult | |||||
| std::string message; | std::string message; | ||||
| }; | }; | ||||
| // 编排监控模型和寄存器仓库,地址列表属于当前会话 | |||||
| class RegisterMonitorService | class RegisterMonitorService | ||||
| { | { | ||||
| public: | public: | ||||
| explicit RegisterMonitorService(RegisterRepository &repository); | explicit RegisterMonitorService(RegisterRepository &repository); | ||||
| // 从起始地址连续添加 count 个同区域地址 | |||||
| RegisterMonitorResult addRange(const std::string &start_address, int count); | RegisterMonitorResult addRange(const std::string &start_address, int count); | ||||
| // 删除一批地址并通知 UI 刷新轮询集合 | |||||
| RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses); | RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses); | ||||
| // 清空监控列表 | |||||
| RegisterMonitorResult clear(); | RegisterMonitorResult clear(); | ||||
| // 对单个 M 地址写位 | |||||
| RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value); | RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value); | ||||
| // 对单个 D 地址写字 | |||||
| RegisterMonitorWriteResult writeWord( | RegisterMonitorWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value); | const RegisterAddress &address, std::int16_t value); | ||||
| const std::vector<RegisterAddress> &addresses() const; | const std::vector<RegisterAddress> &addresses() const; | ||||
| @@ -67,10 +67,12 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() | |||||
| { | { | ||||
| return {false, ModeTransitionError::ProjectNotReady}; | return {false, ModeTransitionError::ProjectNotReady}; | ||||
| } | } | ||||
| // 离线仿真必须先切到虚拟仓库,避免扫描结果写入 PLC 缓存 | |||||
| if (active_repository_ != nullptr && virtual_repository_ != nullptr) | if (active_repository_ != nullptr && virtual_repository_ != nullptr) | ||||
| { | { | ||||
| active_repository_->use(*virtual_repository_); | active_repository_->use(*virtual_repository_); | ||||
| } | } | ||||
| // start() 会复制逻辑快照并做运行前校验;失败时不进入运行态 | |||||
| const SimulationStartResult start_result = offline_simulation_service_.start( | const SimulationStartResult start_result = offline_simulation_service_.start( | ||||
| project_service_.project().controlLogics); | project_service_.project().controlLogics); | ||||
| if (!start_result.succeeded) | if (!start_result.succeeded) | ||||
| @@ -87,6 +89,7 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() | |||||
| ModeTransitionResult RuntimeModeService::enterOnlineRunning() | ModeTransitionResult RuntimeModeService::enterOnlineRunning() | ||||
| { | { | ||||
| // 真机模式只允许使用已完成首读的 PLC 缓存,不复制离线寄存器值 | |||||
| const ModeTransitionResult result = state_.enterOnlineRunning( | const ModeTransitionResult result = state_.enterOnlineRunning( | ||||
| initialPlcReadCompleted()); | initialPlcReadCompleted()); | ||||
| if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr) | if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr) | ||||
| @@ -140,6 +143,7 @@ void RuntimeModeService::configurePlc( | |||||
| active_repository_ = &active_repository; | active_repository_ = &active_repository; | ||||
| virtual_repository_ = &virtual_repository; | virtual_repository_ = &virtual_repository; | ||||
| plc_repository_ = &plc_repository; | plc_repository_ = &plc_repository; | ||||
| // 通信故障撤销首读资格;若正在真机运行,立即回到编辑态 | |||||
| gateway.setCallbacks( | gateway.setCallbacks( | ||||
| [this] | [this] | ||||
| { | { | ||||
| @@ -212,6 +216,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() | |||||
| { | { | ||||
| return {false, "PLC 通信服务尚未配置"}; | return {false, "PLC 通信服务尚未配置"}; | ||||
| } | } | ||||
| // 轮询集合来自自由监控、HMI、报警和梯形图引用的并集 | |||||
| std::vector<RegisterAddress> addresses = monitor_addresses_; | std::vector<RegisterAddress> addresses = monitor_addresses_; | ||||
| const Project &project = project_service_.project(); | const Project &project = project_service_.project(); | ||||
| for (const HmiPage &page : project.hmiPages) | for (const HmiPage &page : project.hmiPages) | ||||
| @@ -262,6 +267,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() | |||||
| } | } | ||||
| return left.index() < right.index(); | return left.index() < right.index(); | ||||
| }); | }); | ||||
| // 排序后去重,保证同一个地址只占一份轮询配额 | |||||
| addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); | addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); | ||||
| if (addresses.size() > ProjectLimits::kMaximumPollAddresses) | if (addresses.size() > ProjectLimits::kMaximumPollAddresses) | ||||
| { | { | ||||
| @@ -20,7 +20,7 @@ class ProjectService; | |||||
| class ActiveRegisterRepository; | class ActiveRegisterRepository; | ||||
| class RegisterRepository; | class RegisterRepository; | ||||
| // 隔离 UI 与领域状态机并编排进入真机运行态的前置条件 | |||||
| // 隔离 UI 与领域状态机,并编排寄存器仓库切换和进入真机的前置条件 | |||||
| class RuntimeModeService | class RuntimeModeService | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -69,11 +69,13 @@ public: | |||||
| std::uint64_t successfulScanCount() const; | std::uint64_t successfulScanCount() const; | ||||
| const LogicScanResult &simulationError() const; | const LogicScanResult &simulationError() const; | ||||
| OfflineSimulationService &offlineSimulationService(); | OfflineSimulationService &offlineSimulationService(); | ||||
| // 注入 PLC 网关、活动仓库和两种实际数据源;应用生命周期内只调用一次 | |||||
| void configurePlc( | void configurePlc( | ||||
| PlcCommunicationGateway &gateway, | PlcCommunicationGateway &gateway, | ||||
| ActiveRegisterRepository &active_repository, | ActiveRegisterRepository &active_repository, | ||||
| RegisterRepository &virtual_repository, | RegisterRepository &virtual_repository, | ||||
| RegisterRepository &plc_repository); | RegisterRepository &plc_repository); | ||||
| // 连接 PLC 并开始异步通信,不阻塞 UI 等待首读 | |||||
| PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration); | PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration); | ||||
| void setMonitorAddresses(const std::vector<RegisterAddress> &addresses); | void setMonitorAddresses(const std::vector<RegisterAddress> &addresses); | ||||
| PlcCommunicationResult refreshPlcPollAddresses(); | PlcCommunicationResult refreshPlcPollAddresses(); | ||||
| @@ -113,6 +113,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||||
| LogicScanResult SoftwareLogicExecutor::validate( | LogicScanResult SoftwareLogicExecutor::validate( | ||||
| const std::vector<ControlLogic> &logics) const | const std::vector<ControlLogic> &logics) const | ||||
| { | { | ||||
| // 先检查每个逻辑自身,再检查跨网络共享线圈和 T/C 资源的冲突 | |||||
| std::map<int, CoilMode> output_modes; | std::map<int, CoilMode> output_modes; | ||||
| for (const ControlLogic &logic : logics) | for (const ControlLogic &logic : logics) | ||||
| { | { | ||||
| @@ -209,10 +210,12 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| return validation; | return validation; | ||||
| } | } | ||||
| // 每轮从空轨迹开始,防止已删除或未执行的节点残留旧状态 | |||||
| if (trace != nullptr) | if (trace != nullptr) | ||||
| { | { | ||||
| trace->clear(); | trace->clear(); | ||||
| } | } | ||||
| // 按工程顺序扫描;前面网络写入的 M/D 值对后面网络立即可见 | |||||
| for (const ControlLogic &logic : logics) | for (const ControlLogic &logic : logics) | ||||
| { | { | ||||
| if (!logic.enabled) | if (!logic.enabled) | ||||
| @@ -11,6 +11,7 @@ | |||||
| #include <utility> | #include <utility> | ||||
| #include <vector> | #include <vector> | ||||
| // 一轮离线扫描可能返回的错误 | |||||
| enum class LogicScanError | enum class LogicScanError | ||||
| { | { | ||||
| None, | None, | ||||
| @@ -20,6 +21,7 @@ enum class LogicScanError | |||||
| RegisterWriteFailed | RegisterWriteFailed | ||||
| }; | }; | ||||
| // 扫描结果;失败时携带出错的逻辑、网络和节点 ID | |||||
| struct LogicScanResult | struct LogicScanResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -30,6 +32,7 @@ struct LogicScanResult | |||||
| std::string nodeId; | std::string nodeId; | ||||
| }; | }; | ||||
| // TON 节点在最近一轮扫描中的可视化状态 | |||||
| struct TonTraceValue | struct TonTraceValue | ||||
| { | { | ||||
| bool input = false; | bool input = false; | ||||
| @@ -38,6 +41,7 @@ struct TonTraceValue | |||||
| int presetMs = 0; | int presetMs = 0; | ||||
| }; | }; | ||||
| // CTU/CTD 节点在最近一轮扫描中的可视化状态 | |||||
| struct CounterTraceValue | struct CounterTraceValue | ||||
| { | { | ||||
| bool input = false; | bool input = false; | ||||
| @@ -47,12 +51,14 @@ struct CounterTraceValue | |||||
| std::int16_t preset = 0; | std::int16_t preset = 0; | ||||
| }; | }; | ||||
| // MOVE/ADD/SUB 节点的结果值和溢出标记 | |||||
| struct WordTraceValue | struct WordTraceValue | ||||
| { | { | ||||
| std::int16_t value = 0; | std::int16_t value = 0; | ||||
| bool overflow = false; | bool overflow = false; | ||||
| }; | }; | ||||
| // 一条逻辑或一个网络的运行轨迹快照 | |||||
| struct LogicTraceValues | struct LogicTraceValues | ||||
| { | { | ||||
| std::unordered_map<std::string, bool> nodeValues; | std::unordered_map<std::string, bool> nodeValues; | ||||
| @@ -68,6 +74,7 @@ struct LogicTraceValues | |||||
| void clear(); | void clear(); | ||||
| }; | }; | ||||
| // 全工程轨迹;logicValues 按逻辑 ID 隔离,避免节点 ID 重复互相覆盖 | |||||
| struct LogicTraceSnapshot : LogicTraceValues | struct LogicTraceSnapshot : LogicTraceValues | ||||
| { | { | ||||
| std::unordered_map<std::string, LogicTraceValues> logicValues; | std::unordered_map<std::string, LogicTraceValues> logicValues; | ||||
| @@ -83,12 +90,16 @@ public: | |||||
| using Clock = std::chrono::steady_clock; | using Clock = std::chrono::steady_clock; | ||||
| using TimePoint = Clock::time_point; | using TimePoint = Clock::time_point; | ||||
| // 在启动仿真前检查所有启用逻辑及 T/C 资源引用 | |||||
| LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | LogicScanResult validate(const std::vector<ControlLogic> &logics) const; | ||||
| // 清除沿触发、定时器和计数器的跨扫描运行状态 | |||||
| void resetRuntime(); | void resetRuntime(); | ||||
| // 使用当前 steady_clock 执行一轮扫描 | |||||
| LogicScanResult executeScan( | LogicScanResult executeScan( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| LogicTraceSnapshot *trace = nullptr); | LogicTraceSnapshot *trace = nullptr); | ||||
| // 使用指定时间执行扫描,便于稳定验证 TON 等时间逻辑 | |||||
| LogicScanResult executeScanAt( | LogicScanResult executeScanAt( | ||||
| const std::vector<ControlLogic> &logics, | const std::vector<ControlLogic> &logics, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| @@ -12,6 +12,7 @@ class AlarmConfigurationDialog; | |||||
| class AlarmEditorService; | class AlarmEditorService; | ||||
| struct AlarmDefinition; | struct AlarmDefinition; | ||||
| // 报警定义编辑对话框;保存和删除都委托 AlarmEditorService | |||||
| class AlarmConfigurationDialog final : public QDialog | class AlarmConfigurationDialog final : public QDialog | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -21,6 +22,7 @@ public: | |||||
| ~AlarmConfigurationDialog() override; | ~AlarmConfigurationDialog() override; | ||||
| private: | private: | ||||
| // 刷新左侧列表并尽量恢复原选中项 | |||||
| void reloadDefinitions(const std::string &selected_id = {}); | void reloadDefinitions(const std::string &selected_id = {}); | ||||
| void loadSelectedDefinition(); | void loadSelectedDefinition(); | ||||
| void updateConditionOptions(); | void updateConditionOptions(); | ||||
| @@ -13,6 +13,7 @@ class FreeMonitorWidget; | |||||
| class RegisterMonitorService; | class RegisterMonitorService; | ||||
| // 自由监控 UI:地址列表属于当前会话,读写统一委托 RegisterMonitorService | |||||
| class FreeMonitorWidget final : public QWidget | class FreeMonitorWidget final : public QWidget | ||||
| { | { | ||||
| Q_OBJECT | Q_OBJECT | ||||
| @@ -23,6 +24,7 @@ public: | |||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| ~FreeMonitorWidget() override; | ~FreeMonitorWidget() override; | ||||
| // 真机未连接、通信故障或编辑态时应关闭写入入口 | |||||
| void setWriteEnabled(bool enabled); | void setWriteEnabled(bool enabled); | ||||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| void reloadAddresses(); | void reloadAddresses(); | ||||
| @@ -902,6 +902,7 @@ HmiEditorWidget::HmiEditorWidget( | |||||
| // 切换显示页面 | // 切换显示页面 | ||||
| void HmiEditorWidget::setPageId(const std::string &page_id) | void HmiEditorWidget::setPageId(const std::string &page_id) | ||||
| { | { | ||||
| // 切换页面只改变投影上下文,真正的页面数据仍由 ProjectService 持有 | |||||
| if (page_id_ == page_id) | if (page_id_ == page_id) | ||||
| { | { | ||||
| return; | return; | ||||
| @@ -945,6 +946,7 @@ void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled) | |||||
| // 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形 | // 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形 | ||||
| void HmiEditorWidget::reloadPage() | void HmiEditorWidget::reloadPage() | ||||
| { | { | ||||
| // 模型发生变化后完全重建图元,避免增量刷新遗漏属性或选择状态 | |||||
| // 画布始终从当前领域页面重建,避免保留已删除控件的图元 | // 画布始终从当前领域页面重建,避免保留已删除控件的图元 | ||||
| scene_->clear(); | scene_->clear(); | ||||
| const HmiPage *page = editor_service_.findPage(page_id_); | const HmiPage *page = editor_service_.findPage(page_id_); | ||||
| @@ -1052,6 +1054,7 @@ std::vector<std::string> HmiEditorWidget::selectedControlIds() const | |||||
| void HmiEditorWidget::refreshRuntimeValues() | void HmiEditorWidget::refreshRuntimeValues() | ||||
| { | { | ||||
| // 运行态才读取寄存器;编辑态画布只显示工程配置,不显示缓存值 | |||||
| if (!runtime_active_) | if (!runtime_active_) | ||||
| { | { | ||||
| return; | return; | ||||
| @@ -1119,6 +1122,7 @@ void HmiEditorWidget::handleSelectionChanged() | |||||
| void HmiEditorWidget::handleControlMoved( | void HmiEditorWidget::handleControlMoved( | ||||
| const std::string &control_id, const QPointF &position) | const std::string &control_id, const QPointF &position) | ||||
| { | { | ||||
| // 拖动结束后把坐标交给服务层,由服务层负责页面边界校验和历史记录 | |||||
| const HmiControl *control = editor_service_.findControl(page_id_, control_id); | const HmiControl *control = editor_service_.findControl(page_id_, control_id); | ||||
| if (control == nullptr) | if (control == nullptr) | ||||
| { | { | ||||
| @@ -1146,6 +1150,7 @@ void HmiEditorWidget::handleButtonEvent( | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| // 运行态按钮事件只通过 HmiRuntimeService 写寄存器,不直接访问仓库 | |||||
| const HmiControl *control = editor_service_.findControl(page_id_, control_id); | const HmiControl *control = editor_service_.findControl(page_id_, control_id); | ||||
| if (control == nullptr) | if (control == nullptr) | ||||
| { | { | ||||
| @@ -57,6 +57,7 @@ public: | |||||
| */ | */ | ||||
| void setRuntimeActive(bool active); | void setRuntimeActive(bool active); | ||||
| // 故障态保持运行值可见,但禁止按钮和数值输入继续写入 | // 故障态保持运行值可见,但禁止按钮和数值输入继续写入 | ||||
| // 故障态保持运行值可见,但禁止按钮和数值输入继续写入 | |||||
| void setRuntimeWriteEnabled(bool enabled); | void setRuntimeWriteEnabled(bool enabled); | ||||
| /** | /** | ||||
| * @brief 使用当前页面模型完全重建场景 | * @brief 使用当前页面模型完全重建场景 | ||||
| @@ -1048,6 +1048,7 @@ void LogicEditorWidget::setEditingEnabled(bool enabled) | |||||
| void LogicEditorWidget::setRuntimeTrace( | void LogicEditorWidget::setRuntimeTrace( | ||||
| const LogicTraceSnapshot &trace, const std::string &fault_node_id) | const LogicTraceSnapshot &trace, const std::string &fault_node_id) | ||||
| { | { | ||||
| // 轨迹只是模型的只读投影;编辑器不因显示轨迹而修改工程表达式 | |||||
| trace_ = trace; | trace_ = trace; | ||||
| fault_node_id_ = fault_node_id; | fault_node_id_ = fault_node_id; | ||||
| runtime_trace_enabled_ = true; | runtime_trace_enabled_ = true; | ||||
| @@ -1064,6 +1065,7 @@ void LogicEditorWidget::clearRuntimeTrace() | |||||
| void LogicEditorWidget::reloadLogic() | void LogicEditorWidget::reloadLogic() | ||||
| { | { | ||||
| // 逻辑或选择变化后重新计算网格布局和可点击插入目标 | |||||
| scene_->clear(); | scene_->clear(); | ||||
| const ControlLogic *logic = editor_service_.findLogic(logic_id_); | const ControlLogic *logic = editor_service_.findLogic(logic_id_); | ||||
| if (logic == nullptr) | if (logic == nullptr) | ||||
| @@ -13,6 +13,7 @@ | |||||
| class QGraphicsScene; | class QGraphicsScene; | ||||
| class QResizeEvent; | class QResizeEvent; | ||||
| // 将结构化梯形图表达式投影成网格图元;不保存自由线段,编辑通过服务提交 | |||||
| class LogicEditorWidget final : public QGraphicsView | class LogicEditorWidget final : public QGraphicsView | ||||
| { | { | ||||
| Q_OBJECT | Q_OBJECT | ||||
| @@ -31,6 +32,7 @@ public: | |||||
| void setLogicId(const std::string &logic_id); | void setLogicId(const std::string &logic_id); | ||||
| void setEditingEnabled(bool enabled); | void setEditingEnabled(bool enabled); | ||||
| // 设置离线运行轨迹;真机模式应传入空轨迹,避免显示本地伪轨迹 | |||||
| void setRuntimeTrace( | void setRuntimeTrace( | ||||
| const LogicTraceSnapshot &trace, | const LogicTraceSnapshot &trace, | ||||
| const std::string &fault_node_id = {}); | const std::string &fault_node_id = {}); | ||||
| @@ -42,6 +44,7 @@ public: | |||||
| std::string selectedRungId() const; | std::string selectedRungId() const; | ||||
| LogicEditorResult addRung(); | LogicEditorResult addRung(); | ||||
| // 下列编辑命令只负责把当前选择翻译成服务调用 | |||||
| LogicEditorResult addCondition(const LogicNodeConfig &config); | LogicEditorResult addCondition(const LogicNodeConfig &config); | ||||
| LogicEditorResult addParallelBranch(const LogicNodeConfig &config); | LogicEditorResult addParallelBranch(const LogicNodeConfig &config); | ||||
| LogicEditorResult addHorizontalWire(); | LogicEditorResult addHorizontalWire(); | ||||
| @@ -13,6 +13,7 @@ namespace Ui { | |||||
| class LogicInstructionDialog; | class LogicInstructionDialog; | ||||
| } | } | ||||
| // 梯形图节点配置对话框;只负责把表单转换为强类型 LogicNodeConfig | |||||
| class LogicInstructionDialog final : public QDialog | class LogicInstructionDialog final : public QDialog | ||||
| { | { | ||||
| Q_OBJECT | Q_OBJECT | ||||
| @@ -23,6 +24,7 @@ public: | |||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| ~LogicInstructionDialog() override; | ~LogicInstructionDialog() override; | ||||
| // 返回当前表单配置,调用方仍需交给 LogicEditorService 校验 | |||||
| LogicNodeConfig config() const; | LogicNodeConfig config() const; | ||||
| private: | private: | ||||
| @@ -1494,6 +1494,7 @@ void MainWindow::appendOutputMessage(const QString &message) | |||||
| void MainWindow::requestMode(ApplicationMode requested_mode) | void MainWindow::requestMode(ApplicationMode requested_mode) | ||||
| { | { | ||||
| // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定 | |||||
| // UI 仅转发模式意图,合法性由服务层和领域状态机决定 | // UI 仅转发模式意图,合法性由服务层和领域状态机决定 | ||||
| ModeTransitionResult result; | ModeTransitionResult result; | ||||
| switch (requested_mode) | switch (requested_mode) | ||||
| @@ -1544,6 +1545,7 @@ void MainWindow::requestMode(ApplicationMode requested_mode) | |||||
| void MainWindow::updateModeUi(const QString &message) | void MainWindow::updateModeUi(const QString &message) | ||||
| { | { | ||||
| // 按服务层策略统一启用/禁用编辑入口,避免单个按钮遗漏状态同步 | |||||
| const ApplicationMode mode = runtime_mode_service_.mode(); | const ApplicationMode mode = runtime_mode_service_.mode(); | ||||
| const ModePolicy policy = runtime_mode_service_.policy(); | const ModePolicy policy = runtime_mode_service_.policy(); | ||||
| restoreCurrentModeAction(); | restoreCurrentModeAction(); | ||||
| @@ -51,6 +51,7 @@ class RuntimePanelController; | |||||
| * | * | ||||
| * 主窗口不直接访问寄存器、工程文件或 PLC 通信实现 | * 主窗口不直接访问寄存器、工程文件或 PLC 通信实现 | ||||
| */ | */ | ||||
| // 编程器主窗口:负责组装 UI 控制器、窗口动作和模式切换,不承载领域规则 | |||||
| class MainWindow final : public QMainWindow | class MainWindow final : public QMainWindow | ||||
| { | { | ||||
| Q_OBJECT | Q_OBJECT | ||||
| @@ -10,6 +10,7 @@ namespace Ui { | |||||
| class PlcConnectionDialog; | class PlcConnectionDialog; | ||||
| } | } | ||||
| // PLC 串口参数对话框;只编辑配置,不负责打开串口 | |||||
| class PlcConnectionDialog final : public QDialog | class PlcConnectionDialog final : public QDialog | ||||
| { | { | ||||
| Q_OBJECT | Q_OBJECT | ||||
| @@ -20,6 +21,7 @@ public: | |||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| ~PlcConnectionDialog() override; | ~PlcConnectionDialog() override; | ||||
| // 返回表单中的串口配置,连接前由网关再次校验 | |||||
| PlcSerialConfiguration configuration() const; | PlcSerialConfiguration configuration() const; | ||||
| private: | private: | ||||
| @@ -24,6 +24,7 @@ class MainWindow; | |||||
| } | } | ||||
| // 组织工程树、当前页面/逻辑选择和工程对象的界面刷新 | // 组织工程树、当前页面/逻辑选择和工程对象的界面刷新 | ||||
| // 它不直接改 Project,所有增删改都转发给对应服务 | |||||
| class ProjectWorkspaceController final | class ProjectWorkspaceController final | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -20,6 +20,7 @@ class MainWindow; | |||||
| } | } | ||||
| // 组织 HMI/梯形图选择状态、属性面板和编辑器信号 | // 组织 HMI/梯形图选择状态、属性面板和编辑器信号 | ||||
| // 属性提交统一经过编辑服务,以便校验、原子回滚和撤销/重做 | |||||
| class PropertyPanelController final | class PropertyPanelController final | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -13,6 +13,7 @@ class RegisterCommentDialog; | |||||
| class RegisterCommentService; | class RegisterCommentService; | ||||
| // 工程级 M/D 注释编辑对话框;注释不包含实时寄存器值 | |||||
| class RegisterCommentDialog final : public QDialog | class RegisterCommentDialog final : public QDialog | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -132,6 +132,7 @@ void RuntimeMonitorWidget::setRuntimePage(const std::string &page_id) | |||||
| void RuntimeMonitorWidget::setMode( | void RuntimeMonitorWidget::setMode( | ||||
| ApplicationMode mode, PlcConnectionState plc_state) | ApplicationMode mode, PlcConnectionState plc_state) | ||||
| { | { | ||||
| // 真机时清空本地轨迹,防止把 PC 离线执行结果误认为 PLC 内部程序轨迹 | |||||
| const bool offline = mode == ApplicationMode::OfflineRunning; | const bool offline = mode == ApplicationMode::OfflineRunning; | ||||
| const bool online = mode == ApplicationMode::OnlineRunning; | const bool online = mode == ApplicationMode::OnlineRunning; | ||||
| ui_->logicFrame->setVisible(offline); | ui_->logicFrame->setVisible(offline); | ||||
| @@ -168,6 +169,7 @@ void RuntimeMonitorWidget::setFreeMonitorWriteEnabled(bool enabled) | |||||
| void RuntimeMonitorWidget::refreshValues( | void RuntimeMonitorWidget::refreshValues( | ||||
| ApplicationMode mode, PlcConnectionState plc_state) | ApplicationMode mode, PlcConnectionState plc_state) | ||||
| { | { | ||||
| // HMI、报警和自由监控共享同一个活动寄存器仓库 | |||||
| hmi_view_->refreshRuntimeValues(); | hmi_view_->refreshRuntimeValues(); | ||||
| free_monitor_widget_->refreshValues(mode, plc_state); | free_monitor_widget_->refreshValues(mode, plc_state); | ||||
| } | } | ||||
| @@ -24,6 +24,7 @@ class LogicEditorWidget; | |||||
| class RegisterMonitorService; | class RegisterMonitorService; | ||||
| class ProjectService; | class ProjectService; | ||||
| // 唯一的工程师运行监控投影,组合 HMI、梯形图轨迹和自由监控 | |||||
| class RuntimeMonitorWidget final : public QWidget | class RuntimeMonitorWidget final : public QWidget | ||||
| { | { | ||||
| Q_OBJECT | Q_OBJECT | ||||
| @@ -42,10 +43,12 @@ public: | |||||
| void setProjectObjects(const std::string &page_id, const std::string &logic_id); | void setProjectObjects(const std::string &page_id, const std::string &logic_id); | ||||
| void setRuntimePage(const std::string &page_id); | void setRuntimePage(const std::string &page_id); | ||||
| // 根据模式决定是否显示本地逻辑轨迹以及是否允许写入 | |||||
| void setMode(ApplicationMode mode, PlcConnectionState plc_state); | void setMode(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| void setHmiWriteEnabled(bool enabled); | void setHmiWriteEnabled(bool enabled); | ||||
| void setFreeMonitorWriteEnabled(bool enabled); | void setFreeMonitorWriteEnabled(bool enabled); | ||||
| // 只接收离线执行器轨迹;真机时由上层清空 | |||||
| void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); | void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); | ||||
| FreeMonitorWidget *freeMonitorWidget() const; | FreeMonitorWidget *freeMonitorWidget() const; | ||||
| std::string selectedLogicId() const; | std::string selectedLogicId() const; | ||||
| @@ -44,6 +44,7 @@ void RuntimeMonitorWindow::closeForApplicationExit() | |||||
| void RuntimeMonitorWindow::closeEvent(QCloseEvent *event) | void RuntimeMonitorWindow::closeEvent(QCloseEvent *event) | ||||
| { | { | ||||
| // 运行期间关闭按钮等价于请求返回编辑态,应用退出时才真正放行关闭 | |||||
| if (runtime_active_ && !application_exit_) | if (runtime_active_ && !application_exit_) | ||||
| { | { | ||||
| emit exitRequested(); | emit exitRequested(); | ||||
| @@ -21,8 +21,11 @@ public: | |||||
| ~RuntimeMonitorWindow() override; | ~RuntimeMonitorWindow() override; | ||||
| void setMonitorWidget(QWidget &widget); | void setMonitorWidget(QWidget &widget); | ||||
| // 进入运行态时显示并最大化窗口 | |||||
| void showForRuntime(); | void showForRuntime(); | ||||
| // 返回编辑态时隐藏窗口但保留唯一监控控件 | |||||
| void hideForEditing(); | void hideForEditing(); | ||||
| // 应用退出时允许真正关闭窗口 | |||||
| void closeForApplicationExit(); | void closeForApplicationExit(); | ||||
| signals: | signals: | ||||
| @@ -158,7 +158,8 @@ void RuntimePanelController::enterRuntime( | |||||
| if (!runtime_session_active_) | if (!runtime_session_active_) | ||||
| { | { | ||||
| runtime_monitor_widget_->setProjectObjects(page_id, logic_id); | runtime_monitor_widget_->setProjectObjects(page_id, logic_id); | ||||
| runtime_session_active_ = true; | |||||
| // 运行窗口只创建一次,后续进入运行态复用同一个监控投影 | |||||
| runtime_session_active_ = true; | |||||
| } | } | ||||
| runtime_monitor_widget_->setMode(mode, plc_state); | runtime_monitor_widget_->setMode(mode, plc_state); | ||||
| runtime_monitor_window_->showForRuntime(); | runtime_monitor_window_->showForRuntime(); | ||||
| @@ -273,6 +274,7 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) | |||||
| void RuntimePanelController::handleRuntimeTimer() | void RuntimePanelController::handleRuntimeTimer() | ||||
| { | { | ||||
| // 定时器只负责刷新投影;离线逻辑扫描由 OfflineSimulationService 自己的定时器驱动 | |||||
| if (runtime_mode_service_.mode() == ApplicationMode::Editing) | if (runtime_mode_service_.mode() == ApplicationMode::Editing) | ||||
| { | { | ||||
| return; | return; | ||||
| @@ -56,11 +56,13 @@ public: | |||||
| ~RuntimePanelController(); | ~RuntimePanelController(); | ||||
| void configure(); | void configure(); | ||||
| // 创建/复用唯一监控窗口,并把当前工程对象投影到运行态 | |||||
| void enterRuntime( | void enterRuntime( | ||||
| const std::string &page_id, | const std::string &page_id, | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| ApplicationMode mode, | ApplicationMode mode, | ||||
| PlcConnectionState plc_state); | PlcConnectionState plc_state); | ||||
| // 停止刷新并隐藏监控窗口,离线服务的停止由运行模式服务负责 | |||||
| void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state); | void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| void closeForApplicationExit(); | void closeForApplicationExit(); | ||||
| void updateSimulationUi(bool report_fault); | void updateSimulationUi(bool report_fault); | ||||
| @@ -2,6 +2,7 @@ | |||||
| #include <QIcon> | #include <QIcon> | ||||
| // 工具栏语义图标;具体 QIcon 绘制集中在 toolbar_icon_factory.cpp | |||||
| enum class UiIcon | enum class UiIcon | ||||
| { | { | ||||
| Edit, | Edit, | ||||
| @@ -55,4 +56,5 @@ enum class UiIcon | |||||
| ClearList | ClearList | ||||
| }; | }; | ||||
| // 根据语义图标类型创建 Qt 图标 | |||||
| QIcon makeUiIcon(UiIcon icon); | QIcon makeUiIcon(UiIcon icon); | ||||