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