Переглянути джерело

feat: 完善运行监控与控制逻辑能力

main
suyu 1 місяць тому
джерело
коміт
875d9ba3c6
58 змінених файлів з 258 додано та 17 видалено
  1. +1
    -0
      app/src/domain/active_register_repository.cpp
  2. +2
    -0
      app/src/domain/active_register_repository.h
  3. +3
    -0
      app/src/domain/alarm_model.h
  4. +1
    -0
      app/src/domain/control_logic_model.cpp
  5. +33
    -0
      app/src/domain/control_logic_model.h
  6. +1
    -0
      app/src/domain/project_limits.h
  7. +3
    -0
      app/src/domain/project_model.cpp
  8. +6
    -2
      app/src/domain/project_model.h
  9. +10
    -0
      app/src/domain/register_address.h
  10. +8
    -0
      app/src/domain/register_monitor_model.h
  11. +8
    -7
      app/src/domain/register_repository.h
  12. +5
    -4
      app/src/domain/runtime_state.h
  13. +1
    -1
      app/src/infrastructure/json_project_storage.h
  14. +3
    -0
      app/src/infrastructure/plc_communication_error_classifier.h
  15. +9
    -0
      app/src/infrastructure/plc_communication_service.cpp
  16. +11
    -0
      app/src/infrastructure/plc_communication_service.h
  17. +6
    -0
      app/src/infrastructure/plc_register_repository.h
  18. +8
    -0
      app/src/services/alarm_editor_service.h
  19. +1
    -0
      app/src/services/alarm_service.cpp
  20. +6
    -0
      app/src/services/alarm_service.h
  21. +3
    -0
      app/src/services/editor_history.h
  22. +1
    -0
      app/src/services/hmi_editor_service.cpp
  23. +1
    -0
      app/src/services/hmi_navigation_service.cpp
  24. +7
    -0
      app/src/services/hmi_navigation_service.h
  25. +1
    -0
      app/src/services/hmi_runtime_service.h
  26. +4
    -0
      app/src/services/logic_editor_service.cpp
  27. +13
    -0
      app/src/services/logic_editor_service.h
  28. +4
    -0
      app/src/services/offline_simulation_service.cpp
  29. +7
    -0
      app/src/services/offline_simulation_service.h
  30. +8
    -0
      app/src/services/plc_communication_gateway.h
  31. +1
    -0
      app/src/services/project_service.cpp
  32. +3
    -1
      app/src/services/project_service.h
  33. +6
    -0
      app/src/services/register_comment_service.h
  34. +9
    -0
      app/src/services/register_monitor_service.h
  35. +6
    -0
      app/src/services/runtime_mode_service.cpp
  36. +3
    -1
      app/src/services/runtime_mode_service.h
  37. +3
    -0
      app/src/services/software_logic_executor.cpp
  38. +11
    -0
      app/src/services/software_logic_executor.h
  39. +2
    -0
      app/src/ui/alarm_configuration_dialog.h
  40. +2
    -0
      app/src/ui/free_monitor_widget.h
  41. +5
    -0
      app/src/ui/hmi_editor_widget.cpp
  42. +1
    -0
      app/src/ui/hmi_editor_widget.h
  43. +2
    -0
      app/src/ui/logic_editor_widget.cpp
  44. +3
    -0
      app/src/ui/logic_editor_widget.h
  45. +2
    -0
      app/src/ui/logic_instruction_dialog.h
  46. +2
    -0
      app/src/ui/main_window.cpp
  47. +1
    -0
      app/src/ui/main_window.h
  48. +2
    -0
      app/src/ui/plc_connection_dialog.h
  49. +1
    -0
      app/src/ui/project_workspace_controller.h
  50. +1
    -0
      app/src/ui/property_panel_controller.h
  51. +1
    -0
      app/src/ui/register_comment_dialog.h
  52. +2
    -0
      app/src/ui/runtime_monitor_widget.cpp
  53. +3
    -0
      app/src/ui/runtime_monitor_widget.h
  54. +1
    -0
      app/src/ui/runtime_monitor_window.cpp
  55. +3
    -0
      app/src/ui/runtime_monitor_window.h
  56. +3
    -1
      app/src/ui/runtime_panel_controller.cpp
  57. +2
    -0
      app/src/ui/runtime_panel_controller.h
  58. +2
    -0
      app/src/ui/toolbar_icon_factory.h

+ 1
- 0
app/src/domain/active_register_repository.cpp Переглянути файл

@@ -7,6 +7,7 @@ ActiveRegisterRepository::ActiveRegisterRepository(RegisterRepository &initial_r


void ActiveRegisterRepository::use(RegisterRepository &repository) void ActiveRegisterRepository::use(RegisterRepository &repository)
{ {
// 只替换转发目标,不复制寄存器值;切换模式必须使用各自数据源
repository_ = &repository; repository_ = &repository;
} }




+ 2
- 0
app/src/domain/active_register_repository.h Переглянути файл

@@ -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;


+ 3
- 0
app/src/domain/alarm_model.h Переглянути файл

@@ -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;
}; };

+ 1
- 0
app/src/domain/control_logic_model.cpp Переглянути файл

@@ -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;


+ 33
- 0
app/src/domain/control_logic_model.h Переглянути файл

@@ -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);

+ 1
- 0
app/src/domain/project_limits.h Переглянути файл

@@ -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;


+ 3
- 0
app/src/domain/project_model.cpp Переглянути файл

@@ -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;


+ 6
- 2
app/src/domain/project_model.h Переглянути файл

@@ -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;
}; };

+ 10
- 0
app/src/domain/register_address.h Переглянути файл

@@ -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);

+ 8
- 0
app/src/domain/register_monitor_model.h Переглянути файл

@@ -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:


+ 8
- 7
app/src/domain/register_repository.h Переглянути файл

@@ -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:


+ 5
- 4
app/src/domain/runtime_state.h Переглянути файл

@@ -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:


+ 1
- 1
app/src/infrastructure/json_project_storage.h Переглянути файл

@@ -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:


+ 3
- 0
app/src/infrastructure/plc_communication_error_classifier.h Переглянути файл

@@ -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);

+ 9
- 0
app/src/infrastructure/plc_communication_service.cpp Переглянути файл

@@ -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


+ 11
- 0
app/src/infrastructure/plc_communication_service.h Переглянути файл

@@ -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_;


+ 6
- 0
app/src/infrastructure/plc_register_repository.h Переглянути файл

@@ -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:


+ 8
- 0
app/src/services/alarm_editor_service.h Переглянути файл

@@ -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:


+ 1
- 0
app/src/services/alarm_service.cpp Переглянути файл

@@ -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)


+ 6
- 0
app/src/services/alarm_service.h Переглянути файл

@@ -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:


+ 3
- 0
app/src/services/editor_history.h Переглянути файл

@@ -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())


+ 1
- 0
app/src/services/hmi_editor_service.cpp Переглянути файл

@@ -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))
{ {


+ 1
- 0
app/src/services/hmi_navigation_service.cpp Переглянути файл

@@ -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())
{ {


+ 7
- 0
app/src/services/hmi_navigation_service.h Переглянути файл

@@ -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 &currentPageId() const; const std::string &currentPageId() const;


private: private:


+ 1
- 0
app/src/services/hmi_runtime_service.h Переглянути файл

@@ -60,6 +60,7 @@ enum class HmiButtonEvent
* *
* 服务不依赖虚拟寄存器实现、串口或 Modbus,可由运行模式注入不同仓库 * 服务不依赖虚拟寄存器实现、串口或 Modbus,可由运行模式注入不同仓库
*/ */
// 仅通过统一寄存器仓库执行 HMI 运行态读写
class HmiRuntimeService class HmiRuntimeService
{ {
public: public:


+ 4
- 0
app/src/services/logic_editor_service.cpp Переглянути файл

@@ -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);


+ 13
- 0
app/src/services/logic_editor_service.h Переглянути файл

@@ -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);


+ 4
- 0
app/src/services/offline_simulation_service.cpp Переглянути файл

@@ -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;


+ 7
- 0
app/src/services/offline_simulation_service.h Переглянути файл

@@ -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:


+ 8
- 0
app/src/services/plc_communication_gateway.h Переглянути файл

@@ -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,


+ 1
- 0
app/src/services/project_service.cpp Переглянути файл

@@ -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)
{ {


+ 3
- 1
app/src/services/project_service.h Переглянути файл

@@ -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 &currentFilePath() const; const std::string &currentFilePath() const;
bool hasCurrentFile() const; bool hasCurrentFile() const;


+ 6
- 0
app/src/services/register_comment_service.h Переглянути файл

@@ -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:


+ 9
- 0
app/src/services/register_monitor_service.h Переглянути файл

@@ -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;


+ 6
- 0
app/src/services/runtime_mode_service.cpp Переглянути файл

@@ -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)
{ {


+ 3
- 1
app/src/services/runtime_mode_service.h Переглянути файл

@@ -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();


+ 3
- 0
app/src/services/software_logic_executor.cpp Переглянути файл

@@ -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
- 0
app/src/services/software_logic_executor.h Переглянути файл

@@ -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,


+ 2
- 0
app/src/ui/alarm_configuration_dialog.h Переглянути файл

@@ -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();


+ 2
- 0
app/src/ui/free_monitor_widget.h Переглянути файл

@@ -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();


+ 5
- 0
app/src/ui/hmi_editor_widget.cpp Переглянути файл

@@ -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)
{ {


+ 1
- 0
app/src/ui/hmi_editor_widget.h Переглянути файл

@@ -57,6 +57,7 @@ public:
*/ */
void setRuntimeActive(bool active); void setRuntimeActive(bool active);
// 故障态保持运行值可见,但禁止按钮和数值输入继续写入 // 故障态保持运行值可见,但禁止按钮和数值输入继续写入
// 故障态保持运行值可见,但禁止按钮和数值输入继续写入
void setRuntimeWriteEnabled(bool enabled); void setRuntimeWriteEnabled(bool enabled);
/** /**
* @brief 使用当前页面模型完全重建场景 * @brief 使用当前页面模型完全重建场景


+ 2
- 0
app/src/ui/logic_editor_widget.cpp Переглянути файл

@@ -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)


+ 3
- 0
app/src/ui/logic_editor_widget.h Переглянути файл

@@ -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();


+ 2
- 0
app/src/ui/logic_instruction_dialog.h Переглянути файл

@@ -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:


+ 2
- 0
app/src/ui/main_window.cpp Переглянути файл

@@ -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();


+ 1
- 0
app/src/ui/main_window.h Переглянути файл

@@ -51,6 +51,7 @@ class RuntimePanelController;
* *
* 主窗口不直接访问寄存器、工程文件或 PLC 通信实现 * 主窗口不直接访问寄存器、工程文件或 PLC 通信实现
*/ */
// 编程器主窗口:负责组装 UI 控制器、窗口动作和模式切换,不承载领域规则
class MainWindow final : public QMainWindow class MainWindow final : public QMainWindow
{ {
Q_OBJECT Q_OBJECT


+ 2
- 0
app/src/ui/plc_connection_dialog.h Переглянути файл

@@ -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:


+ 1
- 0
app/src/ui/project_workspace_controller.h Переглянути файл

@@ -24,6 +24,7 @@ class MainWindow;
} }


// 组织工程树、当前页面/逻辑选择和工程对象的界面刷新 // 组织工程树、当前页面/逻辑选择和工程对象的界面刷新
// 它不直接改 Project,所有增删改都转发给对应服务
class ProjectWorkspaceController final class ProjectWorkspaceController final
{ {
public: public:


+ 1
- 0
app/src/ui/property_panel_controller.h Переглянути файл

@@ -20,6 +20,7 @@ class MainWindow;
} }


// 组织 HMI/梯形图选择状态、属性面板和编辑器信号 // 组织 HMI/梯形图选择状态、属性面板和编辑器信号
// 属性提交统一经过编辑服务,以便校验、原子回滚和撤销/重做
class PropertyPanelController final class PropertyPanelController final
{ {
public: public:


+ 1
- 0
app/src/ui/register_comment_dialog.h Переглянути файл

@@ -13,6 +13,7 @@ class RegisterCommentDialog;


class RegisterCommentService; class RegisterCommentService;


// 工程级 M/D 注释编辑对话框;注释不包含实时寄存器值
class RegisterCommentDialog final : public QDialog class RegisterCommentDialog final : public QDialog
{ {
public: public:


+ 2
- 0
app/src/ui/runtime_monitor_widget.cpp Переглянути файл

@@ -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);
} }


+ 3
- 0
app/src/ui/runtime_monitor_widget.h Переглянути файл

@@ -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;


+ 1
- 0
app/src/ui/runtime_monitor_window.cpp Переглянути файл

@@ -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();


+ 3
- 0
app/src/ui/runtime_monitor_window.h Переглянути файл

@@ -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:


+ 3
- 1
app/src/ui/runtime_panel_controller.cpp Переглянути файл

@@ -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;


+ 2
- 0
app/src/ui/runtime_panel_controller.h Переглянути файл

@@ -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
- 0
app/src/ui/toolbar_icon_factory.h Переглянути файл

@@ -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);

Завантаження…
Відмінити
Зберегти