Procházet zdrojové kódy

refactor: 收敛测试体系与运行服务边界

main
suyu před 3 týdny
rodič
revize
00b1035942
40 změnil soubory, kde provedl 1098 přidání a 3240 odebrání
  1. +3
    -0
      .gitignore
  2. +8
    -8
      app/src/domain/alarm_model.h
  3. +2
    -0
      app/src/domain/control_logic_model.cpp
  4. +117
    -90
      app/src/domain/control_logic_model.h
  5. +15
    -15
      app/src/domain/hmi_control_registry.h
  6. +50
    -50
      app/src/domain/project_limits.h
  7. +44
    -4
      app/src/infrastructure/plc_communication_service.cpp
  8. +49
    -27
      app/src/infrastructure/plc_communication_service.h
  9. +7
    -6
      app/src/infrastructure/plc_register_repository.h
  10. +27
    -2
      app/src/main.cpp
  11. +10
    -0
      app/src/services/alarm_service.cpp
  12. +43
    -13
      app/src/services/alarm_service.h
  13. +39
    -6
      app/src/services/editor_history.h
  14. +93
    -18
      app/src/services/hmi_editor_service.h
  15. +33
    -11
      app/src/services/hmi_runtime_service.h
  16. +99
    -22
      app/src/services/logic_editor_service.h
  17. +34
    -20
      app/src/services/offline_simulation_service.h
  18. +74
    -31
      app/src/services/plc_communication_gateway.h
  19. +64
    -21
      app/src/services/register_monitor_service.h
  20. +58
    -12
      app/src/services/runtime_mode_service.h
  21. +8
    -0
      app/src/services/software_logic_executor.cpp
  22. +116
    -46
      app/src/services/software_logic_executor.h
  23. +3
    -1
      app/src/ui/alarm_configuration_dialog.h
  24. +2
    -0
      app/src/ui/free_monitor_widget.h
  25. +3
    -1
      app/src/ui/logic_editor_widget.h
  26. +2
    -0
      app/src/ui/logic_instruction_dialog.h
  27. +7
    -1
      app/src/ui/main_window.h
  28. +2
    -0
      app/src/ui/plc_connection_dialog.h
  29. +4
    -3
      app/src/ui/project_workspace_controller.h
  30. +1
    -0
      app/src/ui/property_panel_controller.h
  31. +2
    -0
      app/src/ui/register_comment_dialog.h
  32. +2
    -0
      app/src/ui/runtime_monitor_widget.h
  33. +2
    -0
      app/src/ui/runtime_monitor_window.h
  34. +2
    -0
      app/src/ui/runtime_panel_controller.h
  35. +49
    -49
      app/src/ui/toolbar_icon_factory.h
  36. +0
    -2689
      app/tests/main_window_tests.cpp
  37. +0
    -32
      app/tests/main_window_tests.pro
  38. +0
    -42
      app/tests/pri/test_layers.pri
  39. +2
    -6
      app/tests/tests.pro
  40. +22
    -14
      scripts/run_qt_tests.ps1

+ 3
- 0
.gitignore Zobrazit soubor

@@ -23,6 +23,7 @@ ui_*.h
*.ilk
*.exp
*.manifest
*.autosave

# Qt Creator local user settings
*.pro.user
@@ -63,3 +64,5 @@ Desktop.ini
设计方案书.doc
/学习
/json
/代码
/other_version

+ 8
- 8
app/src/domain/alarm_model.h Zobrazit soubor

@@ -8,19 +8,19 @@
// 报警触发条件:M 位为 1,或 D 字高于/低于阈值
enum class AlarmCondition
{
MOn,
DHigh,
DLow
MOn, // M 位为 1 时触发
DHigh, // D 寄存器值高于阈值时触发
DLow // D 寄存器值低于阈值时触发
};

// 可保存的报警定义;运行时 AlarmService 根据此定义生成报警记录
struct AlarmDefinition
{
std::string id;
RegisterAddress address{RegisterArea::M, 0};
AlarmCondition condition = AlarmCondition::MOn;
std::int16_t threshold = 0;
std::string message;
std::string id; // 报警定义的唯一 ID
RegisterAddress address{RegisterArea::M, 0}; // 报警监控的 M 或 D 地址
AlarmCondition condition = AlarmCondition::MOn; // 报警触发条件
std::int16_t threshold = 0; // D 地址比较时使用的阈值
std::string message; // 报警触发后显示的提示文字

// 校验标识、地址区域、消息长度和条件之间的组合是否有效
bool validate(std::string *error = nullptr) const;


+ 2
- 0
app/src/domain/control_logic_model.cpp Zobrazit soubor

@@ -604,6 +604,7 @@ bool LogicNode::isConfigured() const
return configured;
}

// 判断当前节点是不是条件触点类节点
bool LogicNode::isCondition() const
{
return std::holds_alternative<ContactNodeConfig>(config)
@@ -613,6 +614,7 @@ bool LogicNode::isCondition() const
|| std::holds_alternative<CompareNodeConfig>(config);
}

// 判断当前节点是不是输出执行类节点
bool LogicNode::isOutput() const
{
return std::holds_alternative<CoilNodeConfig>(config)


+ 117
- 90
app/src/domain/control_logic_model.h Zobrazit soubor

@@ -12,103 +12,114 @@
// 普通触点的导通方式;常闭触点会对读取到的位值取反
enum class ContactMode
{
NormallyOpen,
NormallyClosed
NormallyOpen, // 常开:位值为 1 时触点导通
NormallyClosed // 常闭:位值为 0 时触点导通
};

// 线圈写入方式:普通写入、置位保持、复位清零
enum class CoilMode
{
Normal,
Set,
Reset
Normal, // 普通写入:直接写入当前逻辑结果
Set, // 置位:逻辑结果为 1 时保持为 1
Reset // 复位:逻辑结果为 1 时清零
};

// 上升沿/下降沿触点的边沿方向
enum class EdgeMode
{
Rising,
Falling
Rising, // 上升沿:信号从 0 变成 1
Falling // 下降沿:信号从 1 变成 0
};

// D 字比较节点支持的比较运算
// D 寄存器字比较,等于、不等于、大于、小于等 6 种比较
enum class ComparisonOperator
{
Equal,
NotEqual,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual
Equal, // 等于
NotEqual, // 不等于
LessThan, // 小于
LessThanOrEqual, // 小于或等于
GreaterThan, // 大于
GreaterThanOrEqual // 大于或等于
};

// 字操作数可以来自常量,也可以来自 D 寄存器
enum class WordOperandKind
{
Constant,
Register
Constant, // 使用固定数值
Register // 使用 D 寄存器中的数值
};

// MOVE、ADD/SUB、计数器预置值共用的字操作数
struct WordOperand
{
WordOperandKind kind = WordOperandKind::Constant;
RegisterAddress address{RegisterArea::D, 0};
std::int16_t constant = 0;
WordOperandKind kind = WordOperandKind::Constant; // 操作数来源
RegisterAddress address{RegisterArea::D, 0}; // kind 为 Register 时使用的 D 地址
std::int16_t constant = 0; // kind 为 Constant 时使用的数值

// 常量始终有效;寄存器操作数必须是有效 D 地址
bool validate(std::string *error = nullptr) const;
};

// 普通 M 寄存器触点,地址 + 常开常闭
struct ContactNodeConfig
{
RegisterAddress address{RegisterArea::M, 0};
ContactMode mode = ContactMode::NormallyOpen;
RegisterAddress address{RegisterArea::M, 0}; // 触点读取的 M 地址
ContactMode mode = ContactMode::NormallyOpen; // 常开或常闭
};

// 边沿触点,地址 + 上升 / 下降沿
struct EdgeContactNodeConfig
{
// 用于检测 M 寄存器的上升沿或下降沿
RegisterAddress address{RegisterArea::M, 0};
EdgeMode mode = EdgeMode::Rising;
};

// 线圈输出配置,决定如何写入 M 寄存器
struct CoilNodeConfig
{
// 线圈要写入的 M 寄存器地址
RegisterAddress address{RegisterArea::M, 0};
CoilMode mode = CoilMode::Normal;
};

// D 寄存器比较配置
struct CompareNodeConfig
{
// 要参与比较的 D 寄存器地址
RegisterAddress address{RegisterArea::D, 0};
// 比较运算符
ComparisonOperator comparison = ComparisonOperator::Equal;
// 与寄存器值比较的常量
std::int16_t value = 0;
};

// 离线执行器内部使用的 T 区地址,不会加入 PLC M/D 轮询
// 定时器 T 编号,内部使用,不映射 M/D 寄存器;限制编号范围,判断是否合法,可转字符串
class TimerAddress
{
public:
static constexpr int kMinimumIndex = 0;
static constexpr int kMaximumIndex = 4000;
static constexpr int kMinimumIndex = 0; // 允许使用的最小 T 编号
static constexpr int kMaximumIndex = 4000; // 允许使用的最大 T 编号

explicit TimerAddress(int index = 0);
explicit TimerAddress(int index = 0); // 使用编号创建 T 地址

int index() const;
bool isValid() const;
std::string toString() const;
int index() const; // 返回地址编号
bool isValid() const; // 判断地址编号是否在有效范围内
std::string toString() const; // 转成 T0 这样的显示文本

bool operator==(const TimerAddress &other) const;
bool operator!=(const TimerAddress &other) const;
bool operator==(const TimerAddress &other) const; // 判断两个 T 地址是否相同
bool operator!=(const TimerAddress &other) const; // 判断两个 T 地址是否不同

private:
int index_ = 0;
int index_ = 0; // 保存 T 地址编号
};

// 读取 TON 完成状态的条件触点
struct TimerContactNodeConfig
{
// 要读取完成状态的定时器地址
TimerAddress address;
// 按常开或常闭方式判断完成状态
ContactMode mode = ContactMode::NormallyOpen;
};

@@ -116,32 +127,34 @@ struct TimerContactNodeConfig
class CounterAddress
{
public:
static constexpr int kMinimumIndex = 0;
static constexpr int kMaximumIndex = 4000;
static constexpr int kMinimumIndex = 0; // 允许使用的最小 C 编号
static constexpr int kMaximumIndex = 4000; // 允许使用的最大 C 编号

explicit CounterAddress(int index = 0);
explicit CounterAddress(int index = 0); // 使用编号创建 C 地址

int index() const;
bool isValid() const;
std::string toString() const;
int index() const; // 返回地址编号
bool isValid() const; // 判断地址编号是否在有效范围内
std::string toString() const; // 转成 C0 这样的显示文本

bool operator==(const CounterAddress &other) const;
bool operator!=(const CounterAddress &other) const;
bool operator==(const CounterAddress &other) const; // 判断两个 C 地址是否相同
bool operator!=(const CounterAddress &other) const; // 判断两个 C 地址是否不同

private:
int index_ = 0;
int index_ = 0; // 保存 C 地址编号
};

enum class CounterMode
{
Up,
Down
Up, // 加计数
Down // 减计数
};

// 读取计数器完成状态的条件触点
struct CounterContactNodeConfig
{
// 要读取完成状态的计数器地址
CounterAddress address;
// 按常开或常闭方式判断完成状态
ContactMode mode = ContactMode::NormallyOpen;
};

@@ -151,54 +164,54 @@ struct TonNodeConfig
static constexpr int kMinimumPresetMs = 1;
static constexpr int kMaximumPresetMs = 86400000;

TimerAddress address;
int presetMs = 1000;
TimerAddress address; // 定时器地址
int presetMs = 1000; // 延时时间,单位为毫秒
};

// CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D
struct CounterNodeConfig
{
CounterAddress address;
CounterMode mode = CounterMode::Up;
RegisterAddress currentValueAddress{RegisterArea::D, 0};
WordOperand preset;
RegisterAddress resetAddress{RegisterArea::M, 0};
CounterAddress address; // 计数器地址
CounterMode mode = CounterMode::Up; // 加计数或减计数
RegisterAddress currentValueAddress{RegisterArea::D, 0}; // 当前值保存到的 D 地址
WordOperand preset; // 计数预置值
RegisterAddress resetAddress{RegisterArea::M, 0}; // 复位输入使用的 M 地址
};

// MOVE 输出:把一个字操作数写入目标 D 地址
struct MoveNodeConfig
{
WordOperand source;
RegisterAddress destination{RegisterArea::D, 0};
WordOperand source; // 要写入的源操作数
RegisterAddress destination{RegisterArea::D, 0}; // 接收数据的 D 地址
};

enum class ArithmeticOperation
{
Add,
Subtract
Add, // 加法
Subtract // 减法
};

// ADD/SUB 输出:计算两个字操作数并写入目标 D 地址
struct ArithmeticNodeConfig
{
ArithmeticOperation operation = ArithmeticOperation::Add;
WordOperand left;
WordOperand right;
RegisterAddress destination{RegisterArea::D, 0};
ArithmeticOperation operation = ArithmeticOperation::Add; // 加法或减法
WordOperand left; // 左操作数
WordOperand right; // 右操作数
RegisterAddress destination{RegisterArea::D, 0}; // 运算结果写入的 D 地址
};

// 所有梯形图指令的强类型配置联合,避免用无关字段拼装指令
using LogicNodeConfig = std::variant<
ContactNodeConfig,
EdgeContactNodeConfig,
TimerContactNodeConfig,
CounterContactNodeConfig,
CoilNodeConfig,
CompareNodeConfig,
TonNodeConfig,
CounterNodeConfig,
MoveNodeConfig,
ArithmeticNodeConfig>;
ContactNodeConfig, // 普通常开或常闭触点
EdgeContactNodeConfig, // 上升沿或下降沿触点
TimerContactNodeConfig, // 定时器完成触点
CounterContactNodeConfig, // 计数器完成触点
CoilNodeConfig, // 普通、置位或复位线圈
CompareNodeConfig, // D 寄存器比较节点
TonNodeConfig, // TON 定时器输出
CounterNodeConfig, // CTU 或 CTD 计数器输出
MoveNodeConfig, // MOVE 数据传送输出
ArithmeticNodeConfig>; // ADD 或 SUB 算术输出

// 返回节点最主要的 M/D 地址;复合输出请使用下面的收集函数
std::optional<RegisterAddress> registerAddressForLogicNode(
@@ -211,9 +224,9 @@ void collectRegisterAddressesForLogicNode(
// 表达式中的一个指令节点;configured=false 表示编辑中的未完成草稿
struct LogicNode
{
std::string id;
LogicNodeConfig config;
bool configured = true;
std::string id; // 节点唯一 ID
LogicNodeConfig config; // 节点的具体指令配置
bool configured = true; // 是否已完成配置

// 校验节点 ID、配置类型和配置内容
bool validate(std::string *error = nullptr) const;
@@ -225,10 +238,10 @@ struct LogicNode

enum class ConditionExpressionKind
{
Node,
Wire,
Series,
Parallel
Node, // 一个实际的条件节点
Wire, // 一段横线
Series, // 多个条件串联,必须全部满足
Parallel // 多个条件并联,满足任意一条即可
};

// 条件区中的持久化横线;columnSpan 表示跨越的网格列数
@@ -238,7 +251,7 @@ struct WireSegment
static constexpr int kMaximumColumnSpan =
ProjectLimits::kMaximumConditionColumns;

int columnSpan = 1;
int columnSpan = 1; // 横线占用的网格列数

bool validate(std::string *error = nullptr) const;
};
@@ -246,13 +259,15 @@ struct WireSegment
// 结构化表达式只允许合法的串并联拓扑,不保存可产生悬空线或环路的像素连接
struct ConditionExpression
{
std::string id;
ConditionExpressionKind kind = ConditionExpressionKind::Node;
std::optional<LogicNode> node;
std::optional<WireSegment> wire;
std::vector<ConditionExpression> children;
std::string id; // 表达式唯一 ID
ConditionExpressionKind kind = ConditionExpressionKind::Node; // 表达式类型
std::optional<LogicNode> node; // kind 为 Node 时保存的节点
std::optional<WireSegment> wire; // kind 为 Wire 时保存的横线
std::vector<ConditionExpression> children; // 串联或并联的子表达式

// 把一个逻辑节点包装成条件表达式
static ConditionExpression fromNode(LogicNode node);
// 创建一段指定列数的横线表达式
static ConditionExpression fromWire(std::string id, int column_span = 1);
// 编辑态校验允许空配置节点,但必须保持树结构合法
bool validate(std::string *error = nullptr) const;
@@ -260,29 +275,41 @@ struct ConditionExpression
bool validateForRunning(std::string *error = nullptr) const;
};

// 删除后折叠单子项容器并合并相邻同类容器,保持表达式的规范形态
// 删除节点后整理表达式结构
void normalizeConditionExpression(std::optional<ConditionExpression> *expression);

// 按节点 ID 查找条件节点,只读版本
const LogicNode *findConditionNode(
const ConditionExpression &expression, const std::string &node_id);

// 按节点 ID 查找条件节点,可修改版本
LogicNode *findConditionNode(
ConditionExpression &expression, const std::string &node_id);

// 按表达式 ID 查找子表达式,只读版本
const ConditionExpression *findConditionExpression(
const ConditionExpression &expression, const std::string &expression_id);

// 按表达式 ID 查找子表达式,可修改版本
ConditionExpression *findConditionExpression(
ConditionExpression &expression, const std::string &expression_id);

// 收集表达式中的所有条件节点
void collectConditionNodes(
const ConditionExpression &expression, std::vector<const LogicNode *> *nodes);

// 收集表达式和子表达式的所有 ID
void collectConditionExpressionIds(
const ConditionExpression &expression, std::vector<std::string> *ids);

// 条件表达式为空时表示恒真网络,输出指令固定在最右侧
struct LadderRung
{
std::string id;
std::string name;
std::string comment;
std::optional<ConditionExpression> condition;
std::optional<LogicNode> output;
std::string id; // 网络唯一 ID
std::string name; // 网络名称
std::string comment; // 网络注释
std::optional<ConditionExpression> condition; // 条件表达式,没有时表示恒真
std::optional<LogicNode> output; // 网络右侧的输出指令

// 编辑态允许没有条件或输出,便于逐步搭建网络
bool validate(std::string *error = nullptr) const;
@@ -295,10 +322,10 @@ struct LadderRung
// 一组按顺序扫描的梯形图网络;enabled=false 时运行校验会跳过它
struct ControlLogic
{
std::string id;
std::string name;
std::vector<LadderRung> rungs;
bool enabled = true;
std::string id; // 控制逻辑唯一 ID
std::string name; // 控制逻辑名称
std::vector<LadderRung> rungs; // 很多条 LadderRung(梯形图网络/梯级)
bool enabled = true; // 是否参与运行扫描

// 校验可保存结构
bool validate(std::string *error = nullptr) const;


+ 15
- 15
app/src/domain/hmi_control_registry.h Zobrazit soubor

@@ -17,9 +17,9 @@
*/
enum class HmiBindingKind
{
None,
Bit,
Word
None, // 不支持寄存器绑定
Bit, // 绑定 M 区位地址
Word // 绑定 D 区字地址
};

/**
@@ -27,9 +27,9 @@ enum class HmiBindingKind
*/
enum class HmiRuntimeValueKind
{
None,
Bit,
Word
None, // 运行态不读取寄存器值
Bit, // 运行态读取绑定的 M 位值
Word // 运行态读取绑定的 D 字值
};

/**
@@ -37,15 +37,15 @@ enum class HmiRuntimeValueKind
*/
struct HmiControlDescriptor
{
HmiControlType type;
const char *storageName;
const char *displayName;
const char *idPrefix;
const char *defaultText;
HmiRect defaultBounds;
HmiBindingKind bindingKind;
HmiRuntimeValueKind runtimeValueKind;
bool requiresBindingForRunning;
HmiControlType type; // 控件的领域类型
const char *storageName; // 工程 JSON 中持久化的稳定类型名
const char *displayName; // 编辑器和属性面板使用的显示名称
const char *idPrefix; // 自动生成控件 ID 时使用的前缀
const char *defaultText; // 新建控件时使用的默认显示文本
HmiRect defaultBounds; // 新建控件时使用的默认位置和尺寸
HmiBindingKind bindingKind; // 编辑态允许的寄存器绑定类型
HmiRuntimeValueKind runtimeValueKind; // 运行态读取的寄存器值类型
bool requiresBindingForRunning; // 运行前是否必须完成有效寄存器绑定
};

/**


+ 50
- 50
app/src/domain/project_limits.h Zobrazit soubor

@@ -5,62 +5,62 @@
// 所有跨层共享的数量和范围边界集中在这里,避免 UI、服务和 JSON 各自写数字
namespace ProjectLimits {

constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U;
constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U; // 一个工程文件最大 16 MiB,防止异常大文件占满内存

constexpr std::size_t kMaximumHmiPages = 128U;
constexpr std::size_t kMaximumHmiControlsPerPage = 512U;
constexpr std::size_t kMaximumAlarmDefinitions = 512U;
constexpr std::size_t kMaximumRegisterComments = 8002U;
constexpr std::size_t kMaximumControlLogics = 128U;
constexpr std::size_t kMaximumRungsPerLogic = 1024U;
constexpr std::size_t kMaximumHmiPages = 128U; // 一个工程最多放 128 个 HMI 页面
constexpr std::size_t kMaximumHmiControlsPerPage = 512U; // 一个 HMI 页面最多放 512 个控件
constexpr std::size_t kMaximumAlarmDefinitions = 512U; // 一个工程最多配置 512 条报警
constexpr std::size_t kMaximumRegisterComments = 8002U; // M0~M4000 和 D0~D4000 最多各写一条注释
constexpr std::size_t kMaximumControlLogics = 128U; // 一个工程最多放 128 组梯形图控制逻辑
constexpr std::size_t kMaximumRungsPerLogic = 1024U; // 一组控制逻辑最多放 1024 个网络

constexpr std::size_t kMaximumExpressionNodesPerRung = 4096U;
constexpr std::size_t kMaximumExpressionDepth = 20U;
constexpr std::size_t kMaximumExpressionChildren = 256U;
constexpr int kMaximumConditionColumns = 10;
constexpr int kMaximumLadderColumns = 11;
constexpr int kMaximumLogicRows = 256;
constexpr std::size_t kMaximumExpressionNodesPerRung = 4096U; // 一个网络里的触点、横线和内部结构最多共 4096 个
constexpr std::size_t kMaximumExpressionDepth = 20U; // 并联套并联时最多套 20 层,防止结构无限变复杂
constexpr std::size_t kMaximumExpressionChildren = 256U; // 一个串联组或并联组最多放 256 个子项
constexpr int kMaximumConditionColumns = 10; // 一个网络前面的条件区最多 10 列
constexpr int kMaximumLadderColumns = 11; // 一个网络总共 11 列,最后 1 列专门放输出
constexpr int kMaximumLogicRows = 256; // 一个网络上下最多 256 行并联支路

static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1);
static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1); // 确保总列数始终等于条件列加输出列

constexpr std::size_t kMaximumHmiProperties = 64U;
constexpr std::size_t kMaximumIdBytes = 128U;
constexpr std::size_t kMaximumTextBytes = 4096U;
constexpr std::size_t kMaximumHmiControlTextCharacters = 12U;
constexpr std::size_t kMaximumAlarmTitleCharacters = 12U;
constexpr std::size_t kMaximumAlarmMessageCharacters = 20U;
constexpr std::size_t kMaximumPropertyKeyBytes = 128U;
constexpr std::size_t kMaximumPropertyValueBytes = 4096U;
constexpr std::size_t kMaximumRegisterCommentBytes = 64U;
constexpr std::size_t kMaximumRungCommentBytes = 128U;
constexpr std::size_t kMaximumHmiProperties = 64U; // 一个 HMI 控件最多保存 64 对扩展属性
constexpr std::size_t kMaximumIdBytes = 128U; // ID 最多 128 个 UTF-8 字节,够用来做稳定标识
constexpr std::size_t kMaximumTextBytes = 4096U; // 工程名称等普通文本最多 4096 个 UTF-8 字节
constexpr std::size_t kMaximumHmiControlTextCharacters = 12U; // HMI 控件显示文字最多输入 12 个字符
constexpr std::size_t kMaximumAlarmTitleCharacters = 12U; // 报警列表标题最多输入 12 个字符
constexpr std::size_t kMaximumAlarmMessageCharacters = 20U; // 一条报警内容最多输入 20 个字符
constexpr std::size_t kMaximumPropertyKeyBytes = 128U; // HMI 属性名最多 128 个 UTF-8 字节
constexpr std::size_t kMaximumPropertyValueBytes = 4096U; // HMI 属性值最多 4096 个 UTF-8 字节
constexpr std::size_t kMaximumRegisterCommentBytes = 64U; // M/D 注释最多 64 个 UTF-8 字节,保证节点下方放得下
constexpr std::size_t kMaximumRungCommentBytes = 128U; // 网络注释最多 128 个 UTF-8 字节,并且保持单行显示

constexpr int kMinimumHmiPageWidth = 320;
constexpr int kMaximumHmiPageWidth = 1600;
constexpr int kMinimumHmiPageHeight = 200;
constexpr int kMaximumHmiPageHeight = 800;
constexpr int kDefaultHmiPageWidth = 800;
constexpr int kDefaultHmiPageHeight = 400;
constexpr int kMaximumHmiControlWidth = 8192;
constexpr int kMaximumHmiControlHeight = 8192;
constexpr int kMinimumHmiFontPointSize = 6;
constexpr int kMaximumHmiFontPointSize = 72;
constexpr int kAlarmDefaultFontPointReduction = 2;
constexpr int kMaximumVisibleAlarmRows = 5;
constexpr int kMinimumHmiPageWidth = 320; // HMI 页面最窄 320 像素,太窄会不好编辑
constexpr int kMaximumHmiPageWidth = 1600; // HMI 页面最宽 1600 像素,避免画布无限变大
constexpr int kMinimumHmiPageHeight = 200; // HMI 页面最矮 200 像素,保证基本可用空间
constexpr int kMaximumHmiPageHeight = 800; // HMI 页面最高 800 像素,适合当前桌面编辑器
constexpr int kDefaultHmiPageWidth = 800; // 新建 HMI 页面默认宽度 800 像素
constexpr int kDefaultHmiPageHeight = 400; // 新建 HMI 页面默认高度 400 像素
constexpr int kMaximumHmiControlWidth = 8192; // 单个 HMI 控件最大宽度,防止异常尺寸撑坏画布
constexpr int kMaximumHmiControlHeight = 8192; // 单个 HMI 控件最大高度,防止异常尺寸撑坏画布
constexpr int kMinimumHmiFontPointSize = 6; // HMI 字体最小 6 磅,太小基本看不清
constexpr int kMaximumHmiFontPointSize = 72; // HMI 字体最大 72 磅,防止文字撑坏布局
constexpr int kAlarmDefaultFontPointReduction = 2; // 报警列表默认比普通文字小 2 磅,给表头留空间
constexpr int kMaximumVisibleAlarmRows = 5; // 报警列表默认最多同时显示 5 行,更多内容通过翻页查看

constexpr std::size_t kMaximumPollAddresses = 1024U;
constexpr std::size_t kMaximumPollBlocks = 64U;
constexpr int kMaximumModbusReadCount = 120;
constexpr std::size_t kMaximumPollAddresses = 1024U; // PLC 一轮轮询最多读取 1024 个去重后的地址
constexpr std::size_t kMaximumPollBlocks = 64U; // PLC 一轮轮询最多拆成 64 个读块
constexpr int kMaximumModbusReadCount = 120; // 一条 Modbus 读指令最多读 120 个数据项

constexpr int kMinimumPlcServerAddress = 1;
constexpr int kMaximumPlcServerAddress = 247;
constexpr int kMinimumResponseTimeoutMs = 100;
constexpr int kMaximumResponseTimeoutMs = 30000;
constexpr int kMinimumRetries = 0;
constexpr int kMaximumRetries = 5;
constexpr int kMinimumPollIntervalMs = 50;
constexpr int kMaximumPollIntervalMs = 10000;
constexpr int kMinimumPlcServerAddress = 1; // Modbus 站号从 1 开始
constexpr int kMaximumPlcServerAddress = 247; // 站号最大 247,避开 Modbus 保留站号
constexpr int kMinimumResponseTimeoutMs = 100; // PLC 最短等待回复 100 毫秒,避免过早判定超时
constexpr int kMaximumResponseTimeoutMs = 30000; // PLC 最长等待回复 30 秒,避免故障时一直卡住
constexpr int kMinimumRetries = 0; // 最少可以不重试,收到失败就直接返回
constexpr int kMaximumRetries = 5; // PLC 通信最多自动重试 5 次
constexpr int kMinimumPollIntervalMs = 50; // PLC 最快每 50 毫秒轮询一次,避免压满串口
constexpr int kMaximumPollIntervalMs = 10000; // PLC 最慢每 10 秒轮询一次,避免画面长时间不刷新

constexpr std::size_t kMaximumPendingWrites = 1U;
constexpr int kMaximumOutputMessages = 1000;
constexpr std::size_t kMaximumPendingWrites = 1U; // 同时只允许等待 1 个 PLC 写入,避免写入顺序混乱
constexpr int kMaximumOutputMessages = 1000; // 输出日志最多保留 1000 条,超过后删除最旧的一条

} // namespace ProjectLimits
}

+ 44
- 4
app/src/infrastructure/plc_communication_service.cpp Zobrazit soubor

@@ -15,26 +15,31 @@

namespace {

// 故障恢复时,两次轻量探测之间等待的时间
constexpr int kRecoveryProbeIntervalMs = 2000;

// Qt 使用 QString,项目内部错误信息统一使用 UTF-8 std::string
std::string toUtf8(const QString &value)
{
const QByteArray bytes = value.toUtf8();
return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
}

// M 区在 Modbus 中按线圈读取,D 区按保持寄存器读取
QModbusDataUnit::RegisterType registerType(RegisterArea area)
{
return area == RegisterArea::M
? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters;
}

// 只有超时和 PLC 不响应属于可以自动探测恢复的故障
bool isRecoverableTimeout(PlcCommunicationError error)
{
return error == PlcCommunicationError::PlcNotResponding
|| error == PlcCommunicationError::CommunicationTimeout;
}

// Connected 正常轮询,Recovering 正在恢复后的重新首读
bool isReadingState(PlcConnectionState state)
{
return state == PlcConnectionState::Connected
@@ -46,6 +51,7 @@ bool normalizePollAddresses(
std::vector<RegisterAddress> *normalized,
std::string *error)
{
// 先复制,后续排序和去重不能改变调用方传入的地址列表
*normalized = addresses;
if (std::any_of(
normalized->cbegin(), normalized->cend(),
@@ -76,6 +82,7 @@ std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses)
{
if (addresses.empty())
{
// 空集合也会保留 M0、D0 两个默认读块
return 2U;
}
std::size_t blocks = 0U;
@@ -84,6 +91,7 @@ std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses)
int count = 0;
for (const RegisterAddress &address : addresses)
{
// 跨区域、出现地址间隔或达到 Modbus 单次上限时,必须开始新读块
if (count == 0
|| address.area() != current_area
|| address.index() > start_address + count
@@ -111,6 +119,7 @@ PlcCommunicationService::PlcCommunicationService(
repository_(repository),
master_(std::make_unique<QModbusRtuSerialMaster>())
{
// 仓库只负责缓存;写入请求通过这里注入的回调转给通信服务
repository_.setWriteHandlers(
[this](const RegisterAddress &address, bool value)
{
@@ -120,6 +129,7 @@ PlcCommunicationService::PlcCommunicationService(
{
return sendWordWrite(address, value);
});
// 轮询定时器每次只推动一个读块,避免一次压入大量异步请求
connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock);
recovery_timer_.setSingleShot(true);
connect(
@@ -127,12 +137,14 @@ PlcCommunicationService::PlcCommunicationService(
&QTimer::timeout,
this,
&PlcCommunicationService::probeRecovery);
// Qt 串口状态变化是异步通知,所有连接状态都从这里统一处理
connect(master_.get(), &QModbusClient::stateChanged,
this,
[this](QModbusDevice::State device_state)
{
if (device_state == QModbusDevice::ConnectedState)
{
// 串口连接成功后立即开始轮询,并不代表首读已经完成
serial_session_opened_ = true;
setState(PlcConnectionState::Connected);
poll_timer_.start(configuration_.pollIntervalMs);
@@ -145,6 +157,7 @@ PlcCommunicationService::PlcCommunicationService(
}
else if (device_state == QModbusDevice::UnconnectedState)
{
// 主动断开和意外断线要区分,后者需要报告故障
if (disconnecting_)
{
serial_session_opened_ = false;
@@ -159,6 +172,7 @@ PlcCommunicationService::PlcCommunicationService(
}
}
});
// Qt 报告底层错误时,转换为项目自己的错误和状态
connect(master_.get(), &QModbusClient::errorOccurred,
this,
[this](QModbusDevice::Error error)
@@ -198,6 +212,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice(
received_valid_response_ = false;
last_error_type_ = PlcCommunicationError::None;
last_error_.clear();
// 把项目配置转换成 Qt 串口参数
master_->setConnectionParameter(
QModbusDevice::SerialPortNameParameter,
QString::fromStdString(configuration.portName));
@@ -234,6 +249,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice(

void PlcCommunicationService::disconnectDevice()
{
// 关闭串口、清除缓存有效标记,防止断开后继续使用旧值
closeSerialSession();
repository_.invalidate();
updateInitialReadCompleted(false, true);
@@ -258,6 +274,7 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses(
// 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复
if (pending_reply_ != nullptr)
{
// 当前回复仍在解析旧集合,等它结束后再切换到新集合
pending_poll_addresses_ = std::move(normalized);
poll_update_pending_ = true;
return {true, {}};
@@ -297,10 +314,11 @@ void PlcCommunicationService::setCallbacks(
std::function<void()> cache_updated,
std::function<void(const std::string &)> error_reported)
{
state_changed_callback_ = std::move(state_changed);
initial_read_changed_callback_ = std::move(initial_read_changed);
cache_updated_callback_ = std::move(cache_updated);
error_reported_callback_ = std::move(error_reported);
// 保存外部通知函数,通信事件发生时再调用对应函数
state_changed_callback_ = std::move(state_changed); // 通信状态变化时通知外部
initial_read_changed_callback_ = std::move(initial_read_changed); // 首读资格变化时通知外部
cache_updated_callback_ = std::move(cache_updated); // PLC 缓存更新后通知外部
error_reported_callback_ = std::move(error_reported); // 发生通信错误时通知外部
}

void PlcCommunicationService::rebuildPollBlocks()
@@ -308,6 +326,7 @@ void PlcCommunicationService::rebuildPollBlocks()
std::vector<RegisterAddress> addresses = poll_addresses_;
if (addresses.empty())
{
// 没有指定地址时保留 M0/D0,保证连接后仍能验证设备是否响应
addresses = {
RegisterAddress{RegisterArea::M, 0},
RegisterAddress{RegisterArea::D, 0}};
@@ -349,6 +368,7 @@ void PlcCommunicationService::rebuildPollBlocks()
next_poll_block_ = 0;
if (!initial_read_completed_)
{
// 地址集合变化或重新连接后,首读资格从头统计
initial_blocks_read_.assign(poll_blocks_.size(), false);
}
else
@@ -360,6 +380,7 @@ void PlcCommunicationService::rebuildPollBlocks()
void PlcCommunicationService::applyPollAddresses(
const std::vector<RegisterAddress> &addresses)
{
// 只有在没有读请求占用时,新的地址集合才会真正生效
poll_addresses_ = addresses;
rebuildPollBlocks();
poll_update_pending_ = false;
@@ -390,6 +411,7 @@ void PlcCommunicationService::pollNextBlock()
return;
}
pending_reply_ = reply;
// 把当前连接代次带进回调,防止断线重连后旧回复误更新新连接
const std::uint64_t generation = connection_generation_;
connect(reply, &QModbusReply::finished,
this,
@@ -424,6 +446,7 @@ void PlcCommunicationService::pollNextBlock()
}
if (pending_reply_ == reply)
{
// 只有当前指针仍指向本次回复时才清空,避免误清新请求
pending_reply_ = nullptr;
}
reply->deleteLater();
@@ -437,6 +460,7 @@ void PlcCommunicationService::pollNextBlock()

void PlcCommunicationService::probeRecovery()
{
// 只在可恢复的超时故障中探测;串口拔出等故障不会反复探测
if (state_ != PlcConnectionState::Faulted
|| !isRecoverableTimeout(last_error_type_))
{
@@ -448,6 +472,7 @@ void PlcCommunicationService::probeRecovery()
}
if (pending_reply_ != nullptr)
{
// 已有读请求在途,稍后再探测,保证同一时间只有一个读回复
recovery_timer_.start(kRecoveryProbeIntervalMs);
return;
}
@@ -455,6 +480,7 @@ void PlcCommunicationService::probeRecovery()
{
return;
}
// 恢复探测只读取一个地址,确认链路恢复后再进行完整首读
const PollBlock block = poll_blocks_.front();
const QModbusDataUnit request(
registerType(block.area), block.startAddress, 1);
@@ -521,6 +547,7 @@ void PlcCommunicationService::handleRecoveryProbeFailure(QModbusDevice::Error er

void PlcCommunicationService::restoreCommunication()
{
// 探测成功只说明链路恢复,仍要重新读取全部读块才能恢复真机资格
received_valid_response_ = true;
last_error_type_ = PlcCommunicationError::None;
last_error_.clear();
@@ -568,6 +595,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock
RegisterWriteResult PlcCommunicationService::sendBitWrite(
const RegisterAddress &address, bool value)
{
// 写入只允许在正常 Connected 状态进行,Recovering/Faulted 都拒绝
if (state_ != PlcConnectionState::Connected)
{
return {false, RegisterError::Unavailable};
@@ -576,6 +604,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite(
{
return {false, RegisterError::WriteRejected};
}
// M 区对应 Modbus Coils,单次只写一个地址
QModbusDataUnit unit(QModbusDataUnit::Coils, address.index(), 1);
unit.setValue(0, value ? 1U : 0U);
QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
@@ -585,6 +614,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite(
return {false, RegisterError::WriteRejected};
}
pending_write_reply_ = reply;
// 写回复完成后只处理成功或错误,不直接改缓存;后续轮询负责确认真实值
connect(reply, &QModbusReply::finished,
this,
[this, reply, generation = connection_generation_]
@@ -610,6 +640,7 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite(
RegisterWriteResult PlcCommunicationService::sendWordWrite(
const RegisterAddress &address, std::int16_t value)
{
// D 区写入流程与 M 区相同,只是 Modbus 类型不同
if (state_ != PlcConnectionState::Connected)
{
return {false, RegisterError::Unavailable};
@@ -618,6 +649,7 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite(
{
return {false, RegisterError::WriteRejected};
}
// D 区对应 Modbus HoldingRegisters,单次只写一个字
QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 1);
unit.setValue(0, static_cast<quint16>(value));
QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
@@ -656,6 +688,7 @@ void PlcCommunicationService::updateInitialReadCompleted(
{
return;
}
// 只有状态真的变化,或调用方明确要求通知时才发出信号
initial_read_completed_ = completed;
emit initialReadCompletedChanged(completed);
if (initial_read_changed_callback_)
@@ -666,6 +699,7 @@ void PlcCommunicationService::updateInitialReadCompleted(

void PlcCommunicationService::handleUnexpectedDisconnect()
{
// 这里表示设备原本连上过,后来串口意外断开
serial_session_opened_ = false;
const QString port_name = QString::fromStdString(configuration_.portName).trimmed();
setError({
@@ -677,6 +711,7 @@ void PlcCommunicationService::handleUnexpectedDisconnect()

void PlcCommunicationService::handleModbusError(QModbusDevice::Error error)
{
// 主动断开、已处理的故障和旧回复错误都不重复上报
if (disconnecting_
|| (error == QModbusDevice::ReplyAbortedError
&& state_ == PlcConnectionState::Disconnected)
@@ -695,6 +730,7 @@ void PlcCommunicationService::handleModbusError(QModbusDevice::Error error)

void PlcCommunicationService::closeSerialSession()
{
// 先递增代次并停止定时器,再断开串口;旧异步回调会因此失效
++connection_generation_;
disconnecting_ = true;
poll_timer_.stop();
@@ -718,6 +754,7 @@ void PlcCommunicationService::setState(PlcConnectionState state)
{
return;
}
// 状态集中从这里修改,确保 Qt 信号和 std::function 回调同步触发
state_ = state;
emit stateChanged();
if (state_changed_callback_)
@@ -728,6 +765,7 @@ void PlcCommunicationService::setState(PlcConnectionState state)

void PlcCommunicationService::setError(const PlcCommunicationFailure &failure)
{
// 统一保存错误、停止正常轮询、撤销首读资格并通知 UI
last_error_type_ = failure.type;
last_error_ = toUtf8(failure.message);
poll_timer_.stop();
@@ -754,3 +792,5 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure)
recovery_timer_.start(kRecoveryProbeIntervalMs);
}
}
// 按区域和地址排序,方便后面合并连续读块
// 相同区域和编号的地址只保留一个

+ 49
- 27
app/src/infrastructure/plc_communication_service.h Zobrazit soubor

@@ -24,22 +24,33 @@ class PlcCommunicationService final : public QObject, public PlcCommunicationGat
Q_OBJECT

public:
// 创建通信服务,并把 PLC 缓存仓库交给它管理
explicit PlcCommunicationService(
PlcRegisterRepository &repository,
QObject *parent = nullptr);
// 释放 Modbus 主站和未完成的通信资源
~PlcCommunicationService() override;

// 按给定串口参数连接 PLC,并开始异步轮询
PlcCommunicationResult connectDevice(
const PlcSerialConfiguration &configuration) override;
// 停止轮询并断开 PLC
void disconnectDevice() override;
// 设置需要周期性读取的 M/D 地址集合
PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) override;

// 返回当前连接状态
PlcConnectionState state() const override;
// 判断本次连接是否已经完成所有轮询块的首次读取
bool initialReadCompleted() const override;
// 返回最近一次通信错误的类型
PlcCommunicationError lastErrorType() const override;
// 返回最近一次通信错误的文字
const std::string &lastError() const override;
// 返回当前使用的串口和 Modbus 参数
const PlcSerialConfiguration &configuration() const;
// 注册状态、首读、缓存更新和错误通知回调
void setCallbacks(
std::function<void()> state_changed,
std::function<void(bool)> initial_read_changed,
@@ -47,18 +58,22 @@ public:
std::function<void(const std::string &)> error_reported) override;

signals:
// 连接状态发生变化
void stateChanged();
// 首次完整读取资格发生变化
void initialReadCompletedChanged(bool completed);
// PLC 缓存收到新的成功读数
void cacheUpdated();
// 通信错误的可读提示文字
void communicationError(const QString &message);

private:
struct PollBlock
{
// 同一区域的一段连续 Modbus 原始地址
RegisterArea area = RegisterArea::M;
int startAddress = 0;
int count = 1;
RegisterArea area = RegisterArea::M; // M 区或 D 区
int startAddress = 0; // 读块的第一个原始地址
int count = 1; // 从起始地址连续读取的数量
};

// 把去重后的地址集合压缩为有限数量的连续读块
@@ -69,7 +84,9 @@ private:
void pollNextBlock();
// Faulted 状态下用轻量探测判断通信是否恢复
void probeRecovery();
// 处理恢复探测失败,并安排下一次探测
void handleRecoveryProbeFailure(QModbusDevice::Error error);
// 探测成功后恢复轮询,但仍需重新完成首读
void restoreCommunication();
// 处理读回复并把值写入 PLC 缓存
void handleReadFinished(QModbusReply *reply, PollBlock block);
@@ -79,36 +96,41 @@ private:
const RegisterAddress &address, std::int16_t value);
// 更新“所有轮询块均成功读取”的真机进入资格
void updateInitialReadCompleted(bool completed, bool force_notification = false);
// 处理没有主动断开时发生的串口断线
void handleUnexpectedDisconnect();
// 把 Qt Modbus 错误转换成项目自己的通信错误
void handleModbusError(QModbusDevice::Error error);
// 关闭串口并清理当前连接的请求和状态
void closeSerialSession();
// 修改连接状态并通知外部观察者
void setState(PlcConnectionState state);
// 保存错误信息、更新状态并通知外部观察者
void setError(const PlcCommunicationFailure &failure);

PlcRegisterRepository &repository_;
std::unique_ptr<QModbusRtuSerialMaster> master_;
QTimer poll_timer_;
QTimer recovery_timer_;
PlcSerialConfiguration configuration_;
std::vector<RegisterAddress> poll_addresses_;
std::vector<RegisterAddress> pending_poll_addresses_;
std::vector<PollBlock> poll_blocks_;
std::size_t next_poll_block_ = 0;
QModbusReply *pending_reply_ = nullptr;
QModbusReply *pending_write_reply_ = nullptr;
bool poll_update_pending_ = false;
bool disconnecting_ = false;
bool serial_session_opened_ = false;
bool received_valid_response_ = false;
std::uint64_t connection_generation_ = 0;
PlcConnectionState state_ = PlcConnectionState::Disconnected;
bool initial_read_completed_ = false;
PlcRegisterRepository &repository_; // 用于保存 PLC 最近一次成功读回的 M/D 值
std::unique_ptr<QModbusRtuSerialMaster> master_; // Qt Modbus RTU 主站对象
QTimer poll_timer_; // 周期性触发下一轮轮询
QTimer recovery_timer_; // Faulted 状态下定时发起恢复探测
PlcSerialConfiguration configuration_; // 当前串口和站号配置
std::vector<RegisterAddress> poll_addresses_; // 当前生效的轮询地址
std::vector<RegisterAddress> pending_poll_addresses_; // 请求在途时暂存的新地址
std::vector<PollBlock> poll_blocks_; // 根据地址合并出的连续读块
std::size_t next_poll_block_ = 0; // 下一次要读取的读块下标
QModbusReply *pending_reply_ = nullptr; // 当前未完成的读请求或恢复探测
QModbusReply *pending_write_reply_ = nullptr; // 当前未完成的单点写请求
bool poll_update_pending_ = false; // 是否有等待请求完成后应用的新轮询集合
bool disconnecting_ = false; // 是否正在执行主动断开
bool serial_session_opened_ = false; // 本次串口会话是否曾经成功打开
bool received_valid_response_ = false; // 本次连接是否收到过有效 PLC 回复
std::uint64_t connection_generation_ = 0; // 连接代次,用于丢弃旧连接的异步回复
PlcConnectionState state_ = PlcConnectionState::Disconnected; // 当前通信状态
bool initial_read_completed_ = false; // 是否已完成首读,可作为真机运行门槛
// 首读阶段每个读块的完成标记,全部为 true 后才允许进入真机
std::vector<bool> initial_blocks_read_;
PlcCommunicationError last_error_type_ = PlcCommunicationError::None;
std::string last_error_;
std::function<void()> state_changed_callback_;
std::function<void(bool)> initial_read_changed_callback_;
std::function<void()> cache_updated_callback_;
std::function<void(const std::string &)> error_reported_callback_;
PlcCommunicationError last_error_type_ = PlcCommunicationError::None; // 最近一次错误类型
std::string last_error_; // 最近一次错误的可读文字
std::function<void()> state_changed_callback_; // 状态变化时调用
std::function<void(bool)> initial_read_changed_callback_; // 首读资格变化时调用
std::function<void()> cache_updated_callback_; // 缓存更新后调用
std::function<void(const std::string &)> error_reported_callback_; // 发生错误时调用
};

+ 7
- 6
app/src/infrastructure/plc_register_repository.h Zobrazit soubor

@@ -31,12 +31,13 @@ public:
bool hasAnyValidValue() const;

private:
// M/D 区共用的数组长度,覆盖 0 到最大地址
static constexpr std::size_t kRegisterCount =
static_cast<std::size_t>(RegisterAddress::kMaximumIndex + 1);
std::array<bool, kRegisterCount> bits_{};
std::array<std::int16_t, kRegisterCount> words_{};
std::array<bool, kRegisterCount> valid_bits_{};
std::array<bool, kRegisterCount> valid_words_{};
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler_;
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler_;
std::array<bool, kRegisterCount> bits_{}; // 最近一次读到的 M 区位值
std::array<std::int16_t, kRegisterCount> words_{}; // 最近一次读到的 D 区字值
std::array<bool, kRegisterCount> valid_bits_{}; // 对应 M 地址是否读到过有效值
std::array<bool, kRegisterCount> valid_words_{}; // 对应 D 地址是否读到过有效值
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler_; // M 区异步写入回调
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler_; // D 区异步写入回调
};

+ 27
- 2
app/src/main.cpp Zobrazit soubor

@@ -26,36 +26,59 @@
#include "services/register_comment_service.h"
#include "ui/main_window.h"

// 程序从这里开始,argc 和 argv 保存用户启动程序时附带的命令行参数
int main(int argc, char *argv[])
{
// 创建整个 Qt 应用,后面的窗口、按钮和消息循环都要依靠它
QApplication application(argc, argv);
// 设置程序名称,Qt 的窗口信息和配置中会用到这个名字
application.setApplicationName(QObject::tr("综合平台编程器"));
// 设置组织名称,用来区分这个程序保存的 Qt 配置
application.setOrganizationName(QStringLiteral("QtProXinJe"));

// 组合根负责创建具体实现并将抽象依赖注入服务和 UI
// 负责把工程保存到 JSON 文件,也负责从 JSON 文件读取工程
JsonProjectStorage project_storage;
// 统一管理新建、打开、保存和校验工程,实际读写文件交给上面的对象
ProjectService project_service(project_storage);
// 处理 HMI 页面和控件的添加、修改、删除、撤销与重做
HmiEditorService hmi_editor_service(project_service);
// 处理运行画面在不同 HMI 页面之间的跳转
HmiNavigationService hmi_navigation_service(project_service);
// 处理梯形图网络和节点的编辑、校验、撤销与重做
LogicEditorService logic_editor_service(project_service);
// 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现

// 在内存中保存离线仿真使用的 M、D 寄存器值,不会写入真实 PLC
VirtualRegisterRepository virtual_register_repository;
// 保存从真实 PLC 最近一次读取到的 M、D 值,并转交写入请求
PlcRegisterRepository plc_register_repository;
// 给界面提供统一的寄存器入口,启动时先使用上面的离线寄存器
ActiveRegisterRepository active_register_repository(virtual_register_repository);
// 负责通过串口异步连接、读取和写入真实 PLC
PlcCommunicationService plc_communication_service(plc_register_repository);
// 处理运行画面读取和写入寄存器,数据来自当前选中的寄存器入口
HmiRuntimeService hmi_runtime_service(active_register_repository);

// 处理报警规则的添加、修改和删除
AlarmEditorService alarm_editor_service(project_service);
// 根据当前寄存器值判断报警是否触发,并处理报警确认
AlarmService alarm_service(project_service, active_register_repository);
// 管理工程中 M、D 地址对应的文字备注
RegisterCommentService register_comment_service(project_service);
// 处理自由监控列表中的寄存器读取和单点写入
RegisterMonitorService register_monitor_service(active_register_repository);
// 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真
OfflineSimulationService offline_simulation_service(virtual_register_repository);
// 管理编辑、离线运行和真机运行三种模式,并控制离线仿真的启停
RuntimeModeService runtime_mode_service(
project_service, offline_simulation_service);
// 把 PLC 通信和两套寄存器接入运行模式,切换模式时才能切换数据来源
runtime_mode_service.configurePlc(
plc_communication_service,
active_register_repository,
virtual_register_repository,
plc_register_repository);

// 创建主窗口,并把界面操作需要的各项服务交给它使用
MainWindow main_window(
runtime_mode_service,
project_service,
@@ -67,7 +90,9 @@ int main(int argc, char *argv[])
hmi_navigation_service,
register_comment_service,
register_monitor_service);
// 把主窗口显示到屏幕上
main_window.show();

// 启动 Qt 消息循环,持续响应鼠标、键盘、定时器和串口事件,窗口关闭后才返回
return application.exec();
}

+ 10
- 0
app/src/services/alarm_service.cpp Zobrazit soubor

@@ -21,11 +21,13 @@ void AlarmService::refresh()
for (const AlarmDefinition &definition
: project_service_.project().alarmDefinitions)
{
// 空值表示寄存器读取失败;保留原记录,等待下一轮成功读回
const std::optional<bool> active = evaluate(definition);
if (!active.has_value())
{
continue;
}
// 同一报警定义在当前会话中最多保留一条活动记录
const auto current = std::find_if(
records_.begin(), records_.end(),
[&definition](const AlarmRecord &record)
@@ -34,12 +36,14 @@ void AlarmService::refresh()
});
if (*active && current == records_.end())
{
// 首次检测到触发时创建未确认记录,并置于列表头部
records_.insert(
records_.begin(),
AlarmRecord{definition.id, definition.message, false, now});
}
else if (!*active && current != records_.end())
{
// 条件恢复后立即移除对应记录,下一次再次触发会生成新记录
records_.erase(current);
}
}
@@ -76,21 +80,27 @@ std::optional<bool> AlarmService::evaluate(
{
if (definition.condition == AlarmCondition::MOn)
{
// MOn 报警直接读取 M 位;读取失败返回空值,交由 refresh 保留旧记录
const BitReadResult value = repository_.readBit(definition.address);
return value.succeeded ? std::optional<bool>{value.value} : std::nullopt;
}
// DHigh 和 DLow 报警读取 D 字,比较方向由定义中的条件决定
const WordReadResult value = repository_.readWord(definition.address);
if (!value.succeeded)
{
// 通信或仓库读取失败不等同于报警解除
return std::nullopt;
}
if (definition.condition == AlarmCondition::DHigh)
{
// 达到阈值即触发高限报警
return value.value >= definition.threshold;
}
if (definition.condition == AlarmCondition::DLow)
{
// 降到阈值即触发低限报警
return value.value <= definition.threshold;
}
// 防御未来新增但尚未实现的报警条件
return std::nullopt;
}

+ 43
- 13
app/src/services/alarm_service.h Zobrazit soubor

@@ -9,36 +9,66 @@ class ProjectService;
class RegisterRepository;
struct AlarmDefinition;

// 运行时产生的报警记录,不会写入工程 JSON
// 运行时产生的报警记录,只存在当前会话,不会写入工程 JSON
struct AlarmRecord
{
std::string definitionId;
std::string message;
bool acknowledged = false;
std::chrono::system_clock::time_point occurredAt;
std::string definitionId; // 对应 AlarmDefinition 的稳定 ID
std::string message; // 触发时复制的报警提示文本
bool acknowledged = false; // 当前记录是否已被操作员确认
std::chrono::system_clock::time_point occurredAt; // 本次触发时间
};

// 根据当前活动寄存器仓库评估报警,并维护确认状态
/**
* @brief 根据当前活动寄存器仓库评估报警并维护会话记录
*
* 服务只读取仓库,不修改工程中的报警定义;仓库可以由运行模式注入虚拟或 PLC 数据源
*/
class AlarmService
{
public:
/**
* @brief 创建报警服务
* @param project_service 只读工程服务,提供报警定义集合
* @param repository 当前运行模式使用的寄存器仓库
*/
AlarmService(
const ProjectService &project_service,
RegisterRepository &repository);

// 重新评估全部定义;仍处于触发状态的记录会保留确认状态
/**
* @brief 重新评估全部报警定义
*
* 仍处于触发状态的记录会保留确认状态;读取失败时跳过该定义并保留原记录。
* 新触发记录插入列表头部,解除触发的记录立即移除
*/
void refresh();
// 确认指定报警;不存在时返回 false
/**
* @brief 确认当前会话中的指定报警记录
* @param definition_id 报警定义的稳定 ID
* @return 找到当前活动记录并完成确认时返回 true,否则返回 false
*/
bool acknowledge(const std::string &definition_id);
// 清空当前运行会话的报警记录
/**
* @brief 清空当前运行会话的全部报警记录和确认状态
*/
void reset();
// 返回按触发时间保存的报警记录
/**
* @brief 返回当前会话的报警记录
* @return 按触发时间倒序排列的只读记录列表,列表由服务持有
*/
const std::vector<AlarmRecord> &records() const;

private:
/**
* @brief 使用当前寄存器值评估单条报警定义
* @param definition 待评估的报警定义
* @return true 表示触发,false 表示未触发;读取失败时返回空值
*
* MOn 比较 M 位是否为 1;DHigh 使用大于等于阈值,DLow 使用小于等于阈值
*/
std::optional<bool> evaluate(const AlarmDefinition &definition) const;

const ProjectService &project_service_;
RegisterRepository &repository_;
std::vector<AlarmRecord> records_;
const ProjectService &project_service_; // 不拥有的只读工程服务
RegisterRepository &repository_; // 当前运行模式的活动寄存器仓库
std::vector<AlarmRecord> records_; // 当前会话记录,最新触发项位于列表头部
};

+ 39
- 6
app/src/services/editor_history.h Zobrazit soubor

@@ -8,16 +8,27 @@
/**
* @brief 保存有限数量的编辑前快照并提供撤销、重做
*
* 历史只存在当前编辑会话内。新编辑会清空重做栈,超过容量时丢弃最早记录。
* State 由具体编辑服务决定,可以是 HMI 页面集合或控制逻辑集合
* 历史只存在当前编辑会话内,不写入工程文件
* 新编辑会清空重做栈,超过容量时丢弃最早记录
*/
template <typename State>
class EditorHistory final
{
public:
// 单个编辑服务最多保留 100 条撤销和重做历史
static constexpr std::size_t kMaximumEntries = 100U;

template <typename Equal>
// 只有前后状态不同才记录;新编辑会清空重做栈
/**
* @brief 记录一次编辑前状态
* @param before 编辑前状态
* @param after 编辑完成后的当前状态
* @param equal 比较前后状态是否相同的可调用对象
*
* 状态没有变化时不创建空历史
* 新编辑成功记录后清空重做栈
*/
void record(State before, const State &after, Equal equal)
{
if (equal(before, after))
@@ -29,7 +40,13 @@ public:
redo_states_.clear();
}

// 返回撤销后的目标状态,并把当前状态放入重做栈
/**
* @brief 撤销最近一次编辑
* @param current 当前状态
* @return 撤销后的目标状态;没有可撤销历史时返回空值
*
* 当前状态会先进入重做栈,之后取出撤销栈顶的编辑前快照
*/
std::optional<State> undo(State current)
{
if (undo_states_.empty())
@@ -43,7 +60,13 @@ public:
return target;
}

// 返回重做后的目标状态,并把当前状态放回撤销栈
/**
* @brief 重做最近一次被撤销的编辑
* @param current 当前状态
* @return 重做后的目标状态;没有可重做历史时返回空值
*
* 当前状态会先放回撤销栈,之后取出重做栈顶的目标状态
*/
std::optional<State> redo(State current)
{
if (redo_states_.empty())
@@ -57,23 +80,33 @@ public:
return target;
}

/**
* @brief 清空撤销栈和重做栈
*/
void clear()
{
undo_states_.clear();
redo_states_.clear();
}

/**
* @brief 判断是否存在可撤销历史
*/
bool canUndo() const
{
return !undo_states_.empty();
}

/**
* @brief 判断是否存在可重做历史
*/
bool canRedo() const
{
return !redo_states_.empty();
}

private:
// 保持历史不超过上限,超出时从最早的记录开始丢弃
static void trim(std::vector<State> *states)
{
while (states->size() > kMaximumEntries)
@@ -82,6 +115,6 @@ private:
}
}

std::vector<State> undo_states_;
std::vector<State> redo_states_;
std::vector<State> undo_states_; // 编辑前快照栈,末尾是下一次撤销要恢复的状态
std::vector<State> redo_states_; // 撤销后状态栈,末尾是下一次重做要恢复的状态
};

+ 93
- 18
app/src/services/hmi_editor_service.h Zobrazit soubor

@@ -21,17 +21,17 @@ struct HmiControlDescriptor;
*/
enum class HmiEditorError
{
None,
PageNotFound,
ControlNotFound,
DuplicateId,
DuplicateName,
InvalidPage,
InvalidControl,
LastPageRequired,
InitialPageCannotBeRemoved,
PageReferenced,
InvalidOperation
None, // 操作成功或没有错误
PageNotFound, // 目标页面不存在
ControlNotFound, // 目标控件不存在
DuplicateId, // 控件 ID 与同页其他控件重复
DuplicateName, // 页面名称与其他页面重复
InvalidPage, // 页面名称、尺寸或页面配置无效
InvalidControl, // 控件属性、绑定或页面边界无效
LastPageRequired, // 删除后不能少于一个 HMI 页面
InitialPageCannotBeRemoved, // 初始页面仍未切换,不能删除
PageReferenced, // 页面仍被页面跳转控件引用
InvalidOperation // 操作参数或撤销/重做状态不允许
};

/**
@@ -41,10 +41,10 @@ enum class HmiEditorError
*/
struct HmiEditorResult
{
bool succeeded = false;
HmiEditorError error = HmiEditorError::None;
std::string message;
std::string id;
bool succeeded = false; // 操作是否成功
HmiEditorError error = HmiEditorError::None; // 失败时的分类
std::string message; // 面向用户的 UTF-8 成功说明或失败原因
std::string id; // 成功时返回新建或更新后的页面/控件 ID
};

/**
@@ -55,6 +55,10 @@ struct HmiEditorResult
class HmiEditorService
{
public:
/**
* @brief 创建 HMI 编辑服务
* @param project_service 用于读取和原子修改当前工程的项目服务
*/
explicit HmiEditorService(ProjectService &project_service);

/**
@@ -82,13 +86,52 @@ public:
* @return 已有或新建页面的成功结果及其页面标识
*/
HmiEditorResult ensureDefaultPage();
/**
* @brief 添加一个 HMI 页面
* @param name 页面显示名称,不能为空且必须在工程内唯一
* @return 成功时返回新页面 ID
*/
HmiEditorResult addPage(const std::string &name);

/**
* @brief 调整页面尺寸
* @param page_id 目标页面唯一标识
* @param width 页面宽度,必须满足页面边界且不能使已有控件越界
* @param height 页面高度,必须满足页面边界且不能使已有控件越界
* @return 页面不存在或新尺寸无效时返回失败结果
*/
HmiEditorResult resizePage(
const std::string &page_id, int width, int height);

/**
* @brief 修改页面显示名称
* @param page_id 目标页面唯一标识
* @param name 新页面名称,不能为空且必须在工程内唯一
* @return 页面不存在或名称无效/重复时返回失败结果
*/
HmiEditorResult renamePage(
const std::string &page_id, const std::string &name);

/**
* @brief 删除一个 HMI 页面
* @param page_id 待删除页面唯一标识
* @return 页面不存在、页面是唯一页面、仍为初始页面或仍被引用时返回失败结果
*/
HmiEditorResult removePage(const std::string &page_id);

/**
* @brief 将页面在工程页面列表中上移或下移一位
* @param page_id 目标页面唯一标识
* @param offset 只能为 -1(上移)或 1(下移)
* @return 页面不存在或已经位于目标边界时返回失败结果
*/
HmiEditorResult movePage(const std::string &page_id, int offset);

/**
* @brief 设置工程的初始 HMI 页面
* @param page_id 要作为启动页面的页面唯一标识
* @return 页面不存在时返回 PageNotFound
*/
HmiEditorResult setInitialPage(const std::string &page_id);
/**
* @brief 向指定页面添加待配置的基础控件
@@ -108,6 +151,13 @@ public:
*/
HmiEditorResult removeControl(
const std::string &page_id, const std::string &control_id);

/**
* @brief 一次删除同一页面中的多个控件
* @param page_id 所属页面唯一标识
* @param control_ids 待删除控件 ID 列表,不能为空且不能包含重复 ID
* @return 页面、控件不存在或 ID 列表无效时返回失败结果
*/
HmiEditorResult removeControls(
const std::string &page_id,
const std::vector<std::string> &control_ids);
@@ -136,26 +186,51 @@ public:
const std::string &control_id,
const HmiControl &control);

/** @brief 判断是否存在可撤销的 HMI 编辑操作 */
bool canUndo() const;
/** @brief 判断是否存在可重做的 HMI 编辑操作 */
bool canRedo() const;

/**
* @brief 撤销最近一次成功的编辑操作
* @return 没有可撤销操作时返回 InvalidOperation
*/
HmiEditorResult undo();

/**
* @brief 重做最近一次被撤销的编辑操作
* @return 没有可重做操作时返回 InvalidOperation
*/
HmiEditorResult redo();

/**
* @brief 清空撤销和重做历史
*
* 不修改当前工程内容
*/
void clearHistory();

private:
// 撤销/重做需要保存的完整 HMI 编辑状态
struct HistoryState
{
std::vector<HmiPage> pages;
std::string initial_page_id;
std::vector<HmiPage> pages; // 页面及其控件的深拷贝
std::string initial_page_id; // 工程初始页面 ID
};

// 捕获当前工程状态,作为一次编辑操作的前后快照
HistoryState captureState() const;
// 比较编辑前后状态并记录实际发生的修改
void recordHistory(HistoryState before);
// 比较两个历史快照是否完全相同
static bool statesEqual(
const HistoryState &left, const HistoryState &right);
// 比较两个 HMI 控件的全部可保存属性
static bool controlsEqual(
const HmiControl &left, const HmiControl &right);
// 比较两个 HMI 页面及其控件集合
static bool pagesEqual(const HmiPage &left, const HmiPage &right);
// 生成统一的历史操作失败结果
static HmiEditorResult historyFailure(const std::string &message);

/**
@@ -183,7 +258,7 @@ private:
/**
* @brief 按控件类型创建带默认属性的新控件
* @param page 新控件所属页面,用于确定初始位置和唯一标识
* @param type 新控件类型
* @param descriptor 新控件类型对应的注册描述
* @return 未绑定寄存器的默认控件配置
*/
static HmiControl makeControl(


+ 33
- 11
app/src/services/hmi_runtime_service.h Zobrazit soubor

@@ -18,10 +18,10 @@
enum class HmiRuntimeError
{
None, // 无错误
UnsupportedControl, // 控件类型不支持寄存器读写
UnsupportedControl, // 控件类型不支持寄存器读写或写入操作
MissingBinding, // 控件未配置寄存器绑定
InvalidBinding, // 控件绑定地址无效或地址区域不匹配
RepositoryFailure // 寄存器仓库读取或入失败
RepositoryFailure // 寄存器仓库不可用或拒绝读写
};

/**
@@ -31,10 +31,10 @@ enum class HmiRuntimeError
*/
struct HmiRuntimeReadResult
{
bool succeeded = false;
HmiRuntimeError error = HmiRuntimeError::None;
bool bit_value = false;
std::int16_t word_value = 0;
bool succeeded = false; // 是否成功读到控件绑定值
HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类
bool bit_value = false; // 位控件的读回值;字控件读取成功时无效
std::int16_t word_value = 0; // 字控件的读回值;位控件读取成功时无效
};

/**
@@ -42,8 +42,8 @@ struct HmiRuntimeReadResult
*/
struct HmiRuntimeWriteResult
{
bool succeeded = false;
HmiRuntimeError error = HmiRuntimeError::None;
bool succeeded = false; // 写入请求是否被仓库接受
HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类
};

/**
@@ -51,8 +51,8 @@ struct HmiRuntimeWriteResult
*/
enum class HmiButtonEvent
{
Pressed,
Released
Pressed, // 按钮按下事件
Released // 按钮释放事件
};

/**
@@ -64,6 +64,10 @@ enum class HmiButtonEvent
class HmiRuntimeService
{
public:
/**
* @brief 创建 HMI 运行态读写服务
* @param repository 当前运行模式提供的寄存器仓库,不由本服务拥有
*/
explicit HmiRuntimeService(RegisterRepository &repository);

/**
@@ -79,6 +83,9 @@ public:
* @param control 按钮控件,必须绑定有效 M 地址
* @param event 当前输入事件
* @return 无需写入的事件返回成功,配置无效或仓库拒绝写入时返回失败
*
* SetOn、SetOff 和 Toggle 只在按下时写入;MomentaryOn 在按下时写 1、
* 释放时写 0。写入结果以仓库返回值为准,不直接假定缓存已经更新
*/
HmiRuntimeWriteResult operateButton(
const HmiControl &control, HmiButtonEvent event);
@@ -87,14 +94,29 @@ public:
* @param control 数值输入控件,必须绑定有效 D 地址
* @param value 要写入的 D 字值
* @return 非数值输入、绑定无效或仓库拒绝写入时返回失败结果
*
* 该服务只负责提交写入请求,实际读回值由后续仓库轮询确认
*/
HmiRuntimeWriteResult writeNumericInput(
const HmiControl &control, std::int16_t value);

/**
* @brief 读取指定 M 位
* @param address 必须是有效的 M 区地址
* @return 仓库原始读取结果,不转换为 HMI 错误分类
*/
BitReadResult readBit(const RegisterAddress &address) const;

/**
* @brief 读取指定 D 字
* @param address 必须是有效的 D 区地址
* @return 仓库原始读取结果,不转换为 HMI 错误分类
*/
WordReadResult readWord(const RegisterAddress &address) const;

private:
// 将寄存器仓库错误统一转换为 HMI 运行态错误
static HmiRuntimeError repositoryError(RegisterError error);

RegisterRepository &repository_;
RegisterRepository &repository_; // 当前运行模式使用的寄存器仓库,不由本服务拥有
};

+ 99
- 22
app/src/services/logic_editor_service.h Zobrazit soubor

@@ -12,164 +12,233 @@ class ProjectService;
// 梯形图编辑失败分类
enum class LogicEditorError
{
None,
LogicNotFound,
RungNotFound,
ExpressionNotFound,
NodeNotFound,
InvalidNode,
InvalidOperation,
UnsupportedNodeChange,
DuplicateName,
LastLogicRequired
None, // 操作成功或没有错误
LogicNotFound, // 控制逻辑不存在
RungNotFound, // 网络不存在
ExpressionNotFound, // 条件表达式不存在
NodeNotFound, // 节点不存在
InvalidNode, // 节点配置或节点位置无效
InvalidOperation, // 操作参数或当前结构不允许该操作
UnsupportedNodeChange, // 不允许把条件节点改成输出节点,或反向修改
DuplicateName, // 控制逻辑名称与其他逻辑重复
LastLogicRequired // 删除后不能少于一个控制逻辑
};

// 梯形图编辑结果;成功时 id 通常是新建或更新对象的稳定 ID
struct LogicEditorResult
{
bool succeeded = false;
LogicEditorError error = LogicEditorError::None;
std::string message;
std::string id;
bool succeeded = false; // 操作是否成功
LogicEditorError error = LogicEditorError::None; // 失败时的分类
std::string message; // 面向用户的 UTF-8 成功说明或失败原因
std::string id; // 成功时返回新建或更新对象的稳定 ID
};

// 负责把 UI 编辑命令转换为结构化表达式树操作,并维护撤销/重做
class LogicEditorService
{
public:
/**
* @brief 创建梯形图编辑服务
* @param project_service 用于读取和修改当前工程的项目服务
*/
explicit LogicEditorService(ProjectService &project_service);

// 以下查询接口只读工程模型,供编辑器投影和属性面板使用
/** @brief 按 ID 查找控制逻辑,未找到时返回空指针 */
const ControlLogic *findLogic(const std::string &logic_id) const;
/** @brief 在指定逻辑中按 ID 查找网络,未找到时返回空指针 */
const LadderRung *findRung(
const std::string &logic_id, const std::string &rung_id) const;
/** @brief 在指定逻辑的所有网络中按 ID 查找节点,未找到时返回空指针 */
const LogicNode *findNode(
const std::string &logic_id, const std::string &node_id) const;
/** @brief 在指定网络中按 ID 查找条件表达式,未找到时返回空指针 */
const ConditionExpression *findExpression(
const std::string &logic_id,
const std::string &rung_id,
const std::string &expression_id) const;
/** @brief 返回工程中第一个控制逻辑 ID,没有逻辑时返回空字符串 */
std::string firstLogicId() const;
/** @brief 返回指定逻辑中第一个网络 ID,没有网络或逻辑不存在时返回空字符串 */
std::string firstRungId(const std::string &logic_id) const;
/** @brief 查找节点所属网络 ID,未找到时返回空字符串 */
std::string rungIdForNode(
const std::string &logic_id, const std::string &node_id) const;
/** @brief 返回指定 M/D 地址的工程注释,没有注释时返回空字符串 */
std::string registerCommentFor(const RegisterAddress &address) const;

// 确保新工程至少有一组可编辑逻辑;逻辑组初始允许没有网络
/**
* @brief 确保工程至少有一组可编辑逻辑
* @return 已有或新建逻辑的成功结果及其 ID
*
* 新建逻辑时允许暂时没有网络,第一次实际编辑网络内容时再创建网络
*/
LogicEditorResult ensureDefaultLogic();
// 逻辑组管理
/** @brief 添加控制逻辑;名称不能为空且必须唯一 */
LogicEditorResult addLogic(const std::string &name);
/** @brief 修改控制逻辑名称;名称不能为空且必须唯一 */
LogicEditorResult renameLogic(
const std::string &logic_id, const std::string &name);
/** @brief 删除控制逻辑;工程至少保留一组逻辑 */
LogicEditorResult removeLogic(const std::string &logic_id);
/** @brief 将逻辑在工程列表中上移或下移一位,offset 只能为 -1 或 1 */
LogicEditorResult moveLogic(const std::string &logic_id, int offset);
/** @brief 启用或停用指定控制逻辑 */
LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled);
// 网络管理
/** @brief 在指定逻辑末尾添加空网络 */
LogicEditorResult addRung(const std::string &logic_id);
/** @brief 删除指定网络 */
LogicEditorResult removeRung(
const std::string &logic_id, const std::string &rung_id);
/** @brief 修改网络注释;注释长度和换行规则由领域校验约束 */
LogicEditorResult updateRungComment(
const std::string &logic_id,
const std::string &rung_id,
const std::string &comment);
// 条件区编辑:串联、按列插入、横线和并联分支
/** @brief 在网络条件末尾追加一个条件节点 */
LogicEditorResult appendCondition(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config);
/**
* @brief 按绝对条件列插入条件节点
* @param column 从 0 开始的条件列号;插入位置必须位于允许的条件区
*/
LogicEditorResult insertConditionAtColumn(
const std::string &logic_id,
const std::string &rung_id,
int column,
const LogicNodeConfig &config);
/**
* @brief 在直属并联分支的指定视觉列插入条件节点
*
* 目标列可以是分支已有横线或 UI 投影出的补线格,服务会原子补齐必要横线
*/
LogicEditorResult insertConditionInBranchAtColumn(
const std::string &logic_id,
const std::string &rung_id,
const std::string &branch_expression_id,
int column,
const LogicNodeConfig &config);
/** @brief 在网络条件末尾追加指定列宽的横线 */
LogicEditorResult appendWire(
const std::string &logic_id,
const std::string &rung_id,
int column_span = 1);
/** @brief 在目标节点后串联插入条件节点 */
LogicEditorResult insertConditionAfter(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_node_id,
const LogicNodeConfig &config);
/** @brief 在目标表达式后串联插入横线 */
LogicEditorResult insertWireAfter(
const std::string &logic_id,
const std::string &rung_id,
const std::string &target_expression_id,
int column_span = 1);
/** @brief 用条件节点整体替换一条横线表达式 */
LogicEditorResult replaceWireWithCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &wire_expression_id,
const LogicNodeConfig &config);
/** @brief 只替换横线表达式中的一个指定列单元格 */
LogicEditorResult replaceWireColumnWithCondition(
const std::string &logic_id,
const std::string &rung_id,
const std::string &wire_expression_id,
int column_offset,
const LogicNodeConfig &config);
/**
* @brief 将选中的条件节点建立为并联分支
* @param selected_node_ids 同一网络中按视觉连续范围选择的条件节点 ID
*/
LogicEditorResult addParallelBranch(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_node_ids,
const LogicNodeConfig &config);
/** @brief 将选中的横线表达式建立为并联旁路 */
LogicEditorResult addParallelWireBranch(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_expression_ids);
// 按选中的横线网格建立旁路;网格必须来自同一条横线且列号连续
/**
* @brief 按选中的横线网格建立并联旁路
*
* 所有网格必须来自同一条横线,且列号连续;操作失败时不保留部分修改
*/
LogicEditorResult addParallelWireBranchAtCells(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::pair<std::string, int>> &selected_wire_cells);
// 输出槽编辑;每个网络最多一个输出节点
/**
* @brief 设置网络唯一的输出节点
* @param configured 是否将新输出标记为已完成配置
*
* 输出节点固定位于网络输出槽,替换已有输出时保持一次原子编辑
*/
LogicEditorResult setOutput(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config,
bool configured = false);
// 节点属性和删除操作
/** @brief 更新节点配置;条件节点和输出节点不能互相改型 */
LogicEditorResult updateNodeConfig(
const std::string &logic_id,
const std::string &node_id,
const LogicNodeConfig &config);
/** @brief 删除指定节点并归一化受影响的表达式树 */
LogicEditorResult removeNode(
const std::string &logic_id, const std::string &node_id);
/**
* @brief 批量删除节点
* @param node_ids 节点 ID 列表,不能为空且不能包含重复或不存在的 ID
* @return 成功时作为一次编辑记录,失败时整体回滚
*/
LogicEditorResult removeNodes(
const std::string &logic_id,
const std::vector<std::string> &node_ids);
/** @brief 删除指定条件表达式并归一化表达式树 */
LogicEditorResult removeExpression(
const std::string &logic_id,
const std::string &rung_id,
const std::string &expression_id);
/** @brief 批量删除条件表达式,失败时整体回滚 */
LogicEditorResult removeExpressions(
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &expression_ids);

// 当前编辑会话的撤销/重做
/** @brief 判断是否存在可撤销的梯形图编辑操作 */
bool canUndo() const;
/** @brief 判断是否存在可重做的梯形图编辑操作 */
bool canRedo() const;
/** @brief 撤销最近一次成功的梯形图编辑操作 */
LogicEditorResult undo();
/** @brief 重做最近一次被撤销的梯形图编辑操作 */
LogicEditorResult redo();
/** @brief 清空撤销/重做历史,不修改当前工程内容 */
void clearHistory();

private:
// 只保存逻辑集合快照,当前选择等 UI 会话状态不进入历史
struct HistoryState
{
std::vector<ControlLogic> logics;
std::vector<ControlLogic> logics; // 编辑前或编辑后的完整逻辑集合
};

// 捕获当前工程中的全部控制逻辑
HistoryState captureState() const;
// 比较编辑前后状态并记录实际发生的修改
void recordHistory(HistoryState before);
// 失败时恢复工程内容和操作前脏标记,保证编辑原子性
void rollbackEdit(HistoryState before, bool modified_before);
// 比较两个历史快照是否完全相同
static bool statesEqual(
const HistoryState &left, const HistoryState &right);
static bool logicsEqual(
@@ -183,17 +252,25 @@ private:
const LogicNodeConfig &left, const LogicNodeConfig &right);
static LogicEditorResult historyFailure(const std::string &message);

// 判断节点配置是否属于条件节点
static bool isConditionConfig(const LogicNodeConfig &config);
// 判断节点配置是否属于输出节点
static bool isOutputConfig(const LogicNodeConfig &config);
// 返回节点配置对应的稳定 ID 前缀
static std::string nodePrefix(const LogicNodeConfig &config);
// 在一个控制逻辑的全部网络中生成唯一节点 ID
static std::string makeUniqueNodeId(
const ControlLogic &logic, const std::string &prefix);
// 生成唯一横线表达式 ID
static std::string makeUniqueWireId(const ControlLogic &logic);
// 生成唯一容器表达式 ID
static std::string makeUniqueExpressionId(const ControlLogic &logic);
// 生成唯一网络 ID
static std::string makeUniqueRungId(const ControlLogic &logic);
// 创建统一的失败结果
static LogicEditorResult failure(
LogicEditorError error, const std::string &message);

ProjectService &project_service_;
EditorHistory<HistoryState> history_;
ProjectService &project_service_; // 不拥有的工程服务依赖
EditorHistory<HistoryState> history_; // 当前编辑会话的撤销/重做历史
};

+ 34
- 20
app/src/services/offline_simulation_service.h Zobrazit soubor

@@ -12,25 +12,25 @@
// 离线扫描器的生命周期状态
enum class SimulationState
{
Stopped,
Running,
Faulted
Stopped, // 未运行或已主动停止
Running, // 定时器正在驱动离线扫描
Faulted // 最近一轮扫描失败,定时器已停止
};

// 启动离线仿真失败的原因
enum class SimulationStartError
{
None,
AlreadyRunning,
InvalidLogic
None, // 启动成功
AlreadyRunning, // 当前已经处于运行态
InvalidLogic // 启动前逻辑校验失败
};

// 离线仿真启动结果;detail 保存领域执行器的具体校验错误
struct SimulationStartResult
{
bool succeeded = false;
SimulationStartError error = SimulationStartError::None;
LogicScanResult detail;
bool succeeded = false; // 是否成功启动仿真
SimulationStartError error = SimulationStartError::None; // 启动失败分类
LogicScanResult detail; // 领域执行器的详细校验或执行结果
};

// 管理离线逻辑快照、固定周期扫描和实际执行状态
@@ -39,40 +39,54 @@ class OfflineSimulationService final : public QObject
Q_OBJECT

public:
// 默认离线扫描周期,单位为毫秒
static constexpr int kDefaultScanIntervalMs = 50;

/**
* @brief 创建离线仿真服务
* @param repository 离线模式使用的虚拟 M/D 寄存器仓库
* @param parent Qt 对象父级
*/
explicit OfflineSimulationService(
VirtualRegisterRepository &repository,
QObject *parent = nullptr);

// 复制工程逻辑作为本次会话快照并启动定时扫描
// 复制工程逻辑作为本次会话快照并启动定时扫描;启动失败时不进入运行态
SimulationStartResult start(const std::vector<ControlLogic> &logics);
// 停止扫描但保留最近一次轨迹供 UI 显示
// 停止扫描并清除运行快照,但保留最近一次轨迹供 UI 显示
void stop();
// 立即执行一轮扫描,主要供定时器和调试调用
// 立即执行一轮扫描;非 Running 状态调用会返回失败结果
LogicScanResult executeOnce();

// 返回当前离线仿真生命周期状态
SimulationState state() const;
// 返回当前定时器扫描周期,单位为毫秒
int scanIntervalMs() const;
// 返回本次会话成功完成的扫描轮数
std::uint64_t successfulScanCount() const;
// 返回最近一次扫描错误;无错误时为成功结果
const LogicScanResult &lastError() const;
// 返回最近一轮按逻辑 ID 隔离的运行轨迹
const LogicTraceSnapshot &traceSnapshot() const;

signals:
// 仿真状态从 Stopped、Running 或 Faulted 之一切换时发出
void stateChanged();
// 一轮扫描成功完成并更新轨迹后发出
void scanCompleted();

private:
// 定时器到期时触发一轮扫描
void handleTimeout();
// 保存错误、停止定时器并切换到 Faulted
void enterFault(const LogicScanResult &error);

VirtualRegisterRepository &repository_;
SoftwareLogicExecutor executor_;
QTimer timer_;
std::vector<ControlLogic> logic_snapshot_;
SimulationState state_ = SimulationState::Stopped;
std::uint64_t successful_scan_count_ = 0;
LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}};
LogicTraceSnapshot trace_snapshot_;
VirtualRegisterRepository &repository_; // 不拥有的虚拟寄存器仓库
SoftwareLogicExecutor executor_; // 执行逻辑校验和扫描的领域执行器
QTimer timer_; // 按固定周期触发离线扫描
std::vector<ControlLogic> logic_snapshot_; // 本次仿真使用的逻辑快照
SimulationState state_ = SimulationState::Stopped; // 当前仿真状态
std::uint64_t successful_scan_count_ = 0; // 当前会话成功扫描次数
LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}}; // 最近一次错误
LogicTraceSnapshot trace_snapshot_; // 最近一次扫描的运行轨迹
};

+ 74
- 31
app/src/services/plc_communication_gateway.h Zobrazit soubor

@@ -12,52 +12,58 @@
// PLC 串口和轮询参数;parity 使用 Qt 约定值:0 无、2 偶、3 奇
struct PlcSerialConfiguration
{
std::string portName;
int serverAddress = 1;
int baudRate = 9600;
int dataBits = 8;
int parity = 2;
int stopBits = 1;
int responseTimeoutMs = 1000;
int retries = 2;
int pollIntervalMs = 200;
std::string portName; // 串口名称,例如 COM3
int serverAddress = 1; // Modbus 从站地址,范围为 1~247
int baudRate = 9600; // 串口波特率,支持 9600、19200、38400、57600 和 115200
int dataBits = 8; // 数据位,支持 7 或 8
int parity = 2; // 校验方式,0 为无校验、2 为偶校验、3 为奇校验
int stopBits = 1; // 停止位,支持 1 或 2
int responseTimeoutMs = 1000; // 单次 Modbus 请求的响应超时时间,单位为毫秒
int retries = 2; // 请求失败后的重试次数,范围为 0~5
int pollIntervalMs = 200; // 轮询周期,单位为毫秒,范围为 50~10000
};

// PLC 异步通信生命周期
enum class PlcConnectionState
{
Disconnected,
Connecting,
Connected,
Recovering,
Faulted
Disconnected, // 未建立 PLC 连接
Connecting, // 正在异步建立 PLC 连接
Connected, // 串口已连接并进行正常轮询,不代表首读已完成
Recovering, // 从可恢复通信故障中探测恢复并重新进行首读
Faulted // 当前通信发生故障,等待恢复探测或重新连接
};

// 通信故障分类,用于状态栏提示和真机运行资格撤销
enum class PlcCommunicationError
{
None,
SerialPortOpenFailed,
PlcNotResponding,
UsbSerialAdapterRemoved,
SerialConnectionLost,
CommunicationTimeout,
ProtocolError,
ReadFailed,
WriteFailed,
ConfigurationError,
RequestAborted,
Unknown
None, // 没有待报告的通信错误
SerialPortOpenFailed, // 串口打开失败
PlcNotResponding, // PLC 未响应请求
UsbSerialAdapterRemoved, // USB 转串口设备被移除
SerialConnectionLost, // 已建立的串口连接意外中断
CommunicationTimeout, // 请求等待响应超时
ProtocolError, // 收到的 Modbus 数据不符合协议
ReadFailed, // 读取请求失败
WriteFailed, // 写入请求失败
ConfigurationError, // 串口或通信参数无效
RequestAborted, // 请求在完成前被中止
Unknown // 未分类的通信错误
};

// 通信命令的受理结果;真正的读写完成由回调和缓存更新通知
struct PlcCommunicationResult
{
bool succeeded = false;
std::string message;
bool succeeded = false; // 命令是否被接受或参数校验是否通过
std::string message; // 失败原因或补充说明,成功时通常为空
};

// 校验串口配置,不打开串口也不产生副作用
/**
* @brief 校验 PLC 串口、Modbus 和轮询参数
*
* 此函数只检查配置,不打开串口、不启动通信,也不会修改传入对象
* @param configuration 待校验的连接配置
* @return 校验通过时返回 succeeded 为 true,否则返回 false 和 UTF-8 错误说明
*/
inline PlcCommunicationResult validatePlcSerialConfiguration(
const PlcSerialConfiguration &configuration)
{
@@ -117,19 +123,56 @@ inline PlcCommunicationResult validatePlcSerialConfiguration(
class PlcCommunicationGateway
{
public:
// 允许通过网关基类指针安全释放具体通信实现
virtual ~PlcCommunicationGateway() = default;

/**
* @brief 校验串口配置并启动异步 PLC 连接
* @param configuration 本次连接使用的串口、Modbus 和轮询参数
* @return true 仅表示连接请求已受理,实际连接结果通过状态回调通知
*/
virtual PlcCommunicationResult connectDevice(
const PlcSerialConfiguration &configuration) = 0;

/**
* @brief 停止轮询并断开当前 PLC 连接
*
* 断开后当前缓存视为无效,首读资格同时清除
*/
virtual void disconnectDevice() = 0;
// 设置后续轮询的 M/D 地址集合;实现可以合并相邻地址块

/**
* @brief 设置后续轮询的 M/D 地址集合
*
* 实现可以校验、排序、去重并合并相邻地址块;已有读请求进行时,
* 新集合可以延后到当前请求完成后生效
* @param addresses 需要周期性读回的 M/D 地址集合,空集合使用实现的默认探测地址
* @return true 表示集合已接受,false 表示地址或轮询资源校验失败
*/
virtual PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) = 0;

// 返回当前连接生命周期状态;Connected 不代表首读资格已经完成
virtual PlcConnectionState state() const = 0;

// 返回本次连接是否已完成全部轮询块的成功首读
virtual bool initialReadCompleted() const = 0;

// 返回最近一次通信错误的分类;没有错误或错误已清除时返回 None
virtual PlcCommunicationError lastErrorType() const = 0;

// 返回最近一次通信错误的 UTF-8 可读文本;没有错误时返回空字符串
virtual const std::string &lastError() const = 0;
// 注册通信状态、首读资格、缓存更新和错误通知

/**
* @brief 替换异步通信事件回调
*
* 传入空 std::function 可取消对应通知;回调由实现在线程或事件循环中按事件发生时调用
* @param state_changed 连接状态发生变化时调用
* @param initial_read_changed 首读资格发生变化时调用,参数表示当前是否已完成首读
* @param cache_updated 任一轮询块成功更新 PLC 缓存后调用
* @param error_reported 发生通信错误时调用,参数为 UTF-8 可读错误文本
*/
virtual void setCallbacks(
std::function<void()> state_changed,
std::function<void(bool)> initial_read_changed,


+ 64
- 21
app/src/services/register_monitor_service.h Zobrazit soubor

@@ -11,53 +11,96 @@
// 自由监控列表编辑的失败分类
enum class RegisterMonitorError
{
None,
InvalidAddress,
RangeOverflow,
LimitExceeded,
NoChange
None, // 操作成功
InvalidAddress, // 起始地址格式或区域不支持
RangeOverflow, // 连续范围超出 M/D 地址上限
LimitExceeded, // 监控地址数量超过会话上限
NoChange // 操作没有产生任何列表变化
};

// 监控列表编辑结果;affectedCount 表示本次受影响的地址数量
struct RegisterMonitorResult
{
bool succeeded = false;
RegisterMonitorError error = RegisterMonitorError::None;
std::string message;
int affectedCount = 0;
bool succeeded = false; // 列表编辑是否成功
RegisterMonitorError error = RegisterMonitorError::None; // 失败时的分类
std::string message; // 面向用户的 UTF-8 成功说明或失败原因
int affectedCount = 0; // 本次新增或删除的地址数量
};

// 自由监控写入结果;底层 RegisterError 会原样保留
struct RegisterMonitorWriteResult
{
bool succeeded = false;
RegisterError error = RegisterError::Unavailable;
std::string message;
bool succeeded = false; // 写入请求是否成功提交
RegisterError error = RegisterError::Unavailable; // 底层仓库错误分类
std::string message; // 面向用户的 UTF-8 错误说明
};

// 编排监控模型和寄存器仓库,地址列表属于当前会话
/**
* @brief 编排自由监控地址列表和寄存器仓库读写
*
* 地址列表只属于当前运行会话,不写入工程文件;仓库由当前运行模式注入
*/
class RegisterMonitorService
{
public:
/**
* @brief 创建自由监控服务
* @param repository 当前运行模式使用的寄存器仓库,不由服务拥有
*/
explicit RegisterMonitorService(RegisterRepository &repository);

// 从起始地址连续添加 count 个同区域地址
/**
* @brief 从起始地址连续添加同一区域的多个地址
* @param start_address 文本地址,例如 M0 或 D100
* @param count 连续地址数量,必须为正且不能越过 4000
* @return 去重后实际新增数量;格式、范围、容量或无变化时返回失败
*/
RegisterMonitorResult addRange(const std::string &start_address, int count);
// 删除一批地址并通知 UI 刷新轮询集合
/**
* @brief 删除一批监控地址
* @param addresses 待删除地址;不存在的地址会被忽略
* @return 实际删除数量,并在发生变化时通知轮询集合更新
*/
RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses);
// 清空监控列表
/**
* @brief 清空当前会话的监控地址列表
* @return 实际清除数量;列表本来为空时返回 NoChange
*/
RegisterMonitorResult clear();
// 对单个 M 地址写位
/**
* @brief 对单个 M 地址提交位写入
* @param address 必须是有效的 M 区地址
* @param value 要写入的位值
* @return 成功或包含底层 RegisterError 和用户可读文本的失败结果
*/
RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value);
// 对单个 D 地址写字
/**
* @brief 对单个 D 地址提交有符号 16 位字写入
* @param address 必须是有效的 D 区地址
* @param value 要写入的 D 字值
* @return 成功或包含底层 RegisterError 和用户可读文本的失败结果
*/
RegisterMonitorWriteResult writeWord(
const RegisterAddress &address, std::int16_t value);
/**
* @brief 返回当前会话的监控地址列表
* @return 按服务内部顺序保存的只读地址列表
*/
const std::vector<RegisterAddress> &addresses() const;
/**
* @brief 读取监控列表的当前值并生成 UI 展示模型
* @param communication_fault 通信处于故障态时,将成功读回值标记为通信故障
* @return 每个监控地址对应一个值和可用性状态
*/
std::vector<MonitorValue> values(bool communication_fault) const;
/**
* @brief 设置监控地址变化通知回调
* @param callback 地址列表新增、删除或清空后调用;可传空函数取消通知
*/
void setAddressesChangedCallback(std::function<void()> callback);

private:
RegisterRepository &repository_;
RegisterMonitorModel model_;
std::function<void()> addresses_changed_callback_;
RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有
RegisterMonitorModel model_; // 当前会话的去重监控地址列表
std::function<void()> addresses_changed_callback_; // 地址变化后的轮询刷新通知
};

+ 58
- 12
app/src/services/runtime_mode_service.h Zobrazit soubor

@@ -24,11 +24,21 @@ class RegisterRepository;
class RuntimeModeService
{
public:
/**
* @brief 创建运行模式服务
* @param project_service 只读工程服务,用于运行前校验和收集轮询地址
* @param offline_simulation_service 离线仿真服务,由应用层负责其生命周期
*/
RuntimeModeService(
const ProjectService &project_service,
OfflineSimulationService &offline_simulation_service);

/**
* @brief 解除 PLC 网关回调绑定
*/
~RuntimeModeService();

/** @brief 获取当前应用运行模式 */
ApplicationMode mode() const;
/**
* @brief 获取当前模式下允许的工程编辑和寄存器使用策略
@@ -65,35 +75,71 @@ public:
*/
bool initialPlcReadCompleted() const;

/** @brief 获取离线仿真当前状态 */
SimulationState simulationState() const;
/** @brief 获取离线仿真成功完成的扫描轮数 */
std::uint64_t successfulScanCount() const;
/** @brief 获取离线仿真最近一次扫描错误 */
const LogicScanResult &simulationError() const;
/** @brief 返回服务持有的离线仿真服务引用 */
OfflineSimulationService &offlineSimulationService();
// 注入 PLC 网关、活动仓库和两种实际数据源;应用生命周期内只调用一次

/**
* @brief 注入 PLC 网关、活动仓库和两种实际数据源
*
* 应用生命周期内只调用一次;真机模式使用 PLC 仓库,编辑/离线模式使用虚拟仓库
*/
void configurePlc(
PlcCommunicationGateway &gateway,
ActiveRegisterRepository &active_repository,
RegisterRepository &virtual_repository,
RegisterRepository &plc_repository);
// 连接 PLC 并开始异步通信,不阻塞 UI 等待首读
/**
* @brief 连接 PLC 并开始异步通信
* @param configuration 串口、Modbus 和轮询配置
* @return 连接请求受理结果,不阻塞 UI 等待首读完成
*/
PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration);

/**
* @brief 设置自由监控引用的地址并刷新 PLC 轮询集合
* @param addresses 自由监控当前需要读取的 M/D 地址
*/
void setMonitorAddresses(const std::vector<RegisterAddress> &addresses);

/**
* @brief 汇总工程和自由监控引用并刷新 PLC 轮询地址
* @return 网关未配置或地址数量超限时返回失败结果
*/
PlcCommunicationResult refreshPlcPollAddresses();

/**
* @brief 退出真机运行、断开 PLC 并清除首读资格
*/
void disconnectPlc();

/** @brief 获取当前 PLC 连接状态;网关未配置时返回 Disconnected */
PlcConnectionState plcConnectionState() const;

/** @brief 获取 PLC 最近一次通信错误文本;网关未配置时返回空字符串 */
const std::string &plcError() const;

/**
* @brief 设置 PLC 状态变化通知回调
* @param callback 状态、首读资格或通信错误变化时调用的函数;可传空函数取消通知
*/
void setPlcStatusChangedCallback(std::function<void()> callback);

private:
const ProjectService &project_service_;
OfflineSimulationService &offline_simulation_service_;
RuntimeState state_;
// 表示 PLC 缓存是否已通过至少一次有效读建立
const ProjectService &project_service_; // 不拥有的只读工程服务
OfflineSimulationService &offline_simulation_service_; // 不拥有的离线仿真服务
RuntimeState state_; // 编辑、离线和真机模式状态机
// PLC 缓存是否已通过有效读建立,通信故障时清除
bool initial_plc_read_completed_ = false;
PlcCommunicationGateway *plc_gateway_ = nullptr;
ActiveRegisterRepository *active_repository_ = nullptr;
RegisterRepository *virtual_repository_ = nullptr;
RegisterRepository *plc_repository_ = nullptr;
std::function<void()> plc_status_changed_callback_;
std::vector<RegisterAddress> monitor_addresses_;
PlcCommunicationGateway *plc_gateway_ = nullptr; // 不拥有的 PLC 通信网关
ActiveRegisterRepository *active_repository_ = nullptr; // 当前模式使用的仓库代理
RegisterRepository *virtual_repository_ = nullptr; // 离线/编辑模式的虚拟仓库
RegisterRepository *plc_repository_ = nullptr; // 真机模式的 PLC 缓存仓库
std::function<void()> plc_status_changed_callback_; // PLC 状态变化通知
std::vector<RegisterAddress> monitor_addresses_; // 自由监控额外引用的地址
};

+ 8
- 0
app/src/services/software_logic_executor.cpp Zobrazit soubor

@@ -383,6 +383,8 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
RegisterRepository &repository,
bool *value)
{
// 根据条件节点的具体配置读取 M/D 或内部 T/C 状态,并计算触点结果
// 边沿触点使用 logic_id 和 node.id 组成运行时状态键,避免同名节点互相影响
if (value == nullptr)
{
return failure(
@@ -400,6 +402,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
using Config = std::decay_t<decltype(config)>;
if constexpr (std::is_same_v<Config, ContactNodeConfig>)
{
// 普通触点直接读取 M 位;常闭触点使用读回值的反值
const BitReadResult read = repository.readBit(config.address);
if (!read.succeeded)
{
@@ -416,6 +419,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
}
else if constexpr (std::is_same_v<Config, EdgeContactNodeConfig>)
{
// 边沿触点需要比较本轮与上一轮的 M 位值,只在目标边沿出现时导通
const BitReadResult read = repository.readBit(config.address);
if (!read.succeeded)
{
@@ -435,18 +439,21 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
}
else if constexpr (std::is_same_v<Config, TimerContactNodeConfig>)
{
// 定时器触点不读 PLC 地址,而是读取本地 TON 的完成状态
const bool done = ton_states_[config.address.index()].done;
*value = config.mode == ContactMode::NormallyOpen ? done : !done;
return success();
}
else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>)
{
// 计数器触点读取本地 CTU/CTD 的完成状态
const bool done = counter_states_[config.address.index()].done;
*value = config.mode == ContactMode::NormallyOpen ? done : !done;
return success();
}
else if constexpr (std::is_same_v<Config, CompareNodeConfig>)
{
// 字比较触点读取 D 字,再按配置的比较运算符与目标值比较
const WordReadResult read = repository.readWord(config.address);
if (!read.succeeded)
{
@@ -462,6 +469,7 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition(
}
else
{
// 输出节点只能由 executeOutput 处理,不能作为条件触点计算
return failure(
LogicScanError::InvalidLogic,
"条件节点不能包含输出节点",


+ 116
- 46
app/src/services/software_logic_executor.h Zobrazit soubor

@@ -11,75 +11,78 @@
#include <utility>
#include <vector>

// 一轮离线扫描可能返回的错误
// 一轮软件逻辑扫描可能返回的错误
enum class LogicScanError
{
None,
InvalidLogic,
ConflictingOutput,
RegisterReadFailed,
RegisterWriteFailed
None, // 扫描成功
InvalidLogic, // 逻辑结构或运行配置无效
ConflictingOutput, // 多个输出以不兼容的线圈模式写入同一地址
RegisterReadFailed, // 扫描读取寄存器失败
RegisterWriteFailed // 扫描写入寄存器失败
};

// 扫描结果;失败时携带出错的逻辑、网络和节点 ID
struct LogicScanResult
{
bool succeeded = false;
LogicScanError error = LogicScanError::None;
std::string message;
std::string logicId;
std::string rungId;
std::string nodeId;
bool succeeded = false; // 本轮扫描是否完成
LogicScanError error = LogicScanError::None; // 失败时的分类
std::string message; // UTF-8 可读结果或失败原因
std::string logicId; // 失败关联的控制逻辑 ID
std::string rungId; // 失败关联的网络 ID
std::string nodeId; // 失败关联的节点 ID
};

// TON 节点在最近一轮扫描中的可视化状态
struct TonTraceValue
{
bool input = false;
bool done = false;
std::int64_t elapsedMs = 0;
int presetMs = 0;
bool input = false; // 本轮 TON 输入状态
bool done = false; // 当前是否已经到达预置时间
std::int64_t elapsedMs = 0; // 已累计时间,单位为毫秒
int presetMs = 0; // 预置时间,单位为毫秒
};

// CTU/CTD 节点在最近一轮扫描中的可视化状态
struct CounterTraceValue
{
bool input = false;
bool reset = false;
bool done = false;
std::int16_t value = 0;
std::int16_t preset = 0;
bool input = false; // 本轮计数输入状态
bool reset = false; // 本轮复位输入状态
bool done = false; // 当前计数值是否达到预置值
std::int16_t value = 0; // 当前计数值
std::int16_t preset = 0; // 计数预置值
};

// MOVE/ADD/SUB 节点的结果值和溢出标记
struct WordTraceValue
{
std::int16_t value = 0;
bool overflow = false;
std::int16_t value = 0; // MOVE/ADD/SUB 的结果值
bool overflow = false; // ADD/SUB 是否发生饱和溢出
};

// 一条逻辑或一个网络的运行轨迹快照
// 一条控制逻辑的运行轨迹快照;各映射使用节点、表达式或网络 ID 作为键
struct LogicTraceValues
{
std::unordered_map<std::string, bool> nodeValues;
std::unordered_map<std::string, bool> nodePowerValues;
std::unordered_map<std::string, bool> expressionValues;
std::unordered_map<std::string, bool> expressionInputValues;
std::unordered_map<std::string, bool> expressionPowerValues;
std::unordered_map<std::string, bool> rungValues;
std::unordered_map<std::string, TonTraceValue> tonValues;
std::unordered_map<std::string, CounterTraceValue> counterValues;
std::unordered_map<std::string, WordTraceValue> wordValues;

std::unordered_map<std::string, bool> nodeValues; // 节点最终逻辑值
std::unordered_map<std::string, bool> nodePowerValues; // 节点输入电源状态
std::unordered_map<std::string, bool> expressionValues; // 条件表达式最终值
std::unordered_map<std::string, bool> expressionInputValues; // 表达式输入状态
std::unordered_map<std::string, bool> expressionPowerValues; // 表达式输出电源状态
std::unordered_map<std::string, bool> rungValues; // 网络最终逻辑值
std::unordered_map<std::string, TonTraceValue> tonValues; // TON 节点状态
std::unordered_map<std::string, CounterTraceValue> counterValues; // CTU/CTD 节点状态
std::unordered_map<std::string, WordTraceValue> wordValues; // MOVE/ADD/SUB 节点状态

// 清除当前逻辑或网络的全部轨迹
void clear();
};

// 全工程轨迹;logicValues 按逻辑 ID 隔离,避免节点 ID 重复互相覆盖
// 全工程轨迹;logicValues 按逻辑 ID 隔离,避免不同逻辑中的节点 ID 互相覆盖
struct LogicTraceSnapshot : LogicTraceValues
{
std::unordered_map<std::string, LogicTraceValues> logicValues;
std::unordered_map<std::string, LogicTraceValues> logicValues; // 按逻辑 ID 保存的轨迹

// 清除全工程轨迹及按逻辑分组的轨迹
void clear();
// 返回指定逻辑的轨迹投影;不存在时返回空轨迹
LogicTraceSnapshot forLogic(const std::string &logic_id) const;
};

@@ -90,16 +93,41 @@ public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;

// 在启动仿真前检查所有启用逻辑及 T/C 资源引用
/**
* @brief 检查逻辑是否满足软件扫描的运行要求
* @param logics 待检查的控制逻辑集合
* @return 成功或包含逻辑、网络、节点定位信息的失败结果
*
* 检查逻辑结构、启用逻辑的运行配置、共享输出冲突以及 T/C 资源引用
*/
LogicScanResult validate(const std::vector<ControlLogic> &logics) const;
// 清除沿触发、定时器和计数器的跨扫描运行状态

/**
* @brief 清除所有跨扫描运行状态
*
* 重置边沿触发、TON 定时器和 CTU/CTD 计数器;不修改寄存器仓库和轨迹对象
*/
void resetRuntime();
// 使用当前 steady_clock 执行一轮扫描

/**
* @brief 使用当前 steady_clock 执行一轮完整扫描
* @param logics 按工程顺序扫描的控制逻辑集合
* @param repository 扫描读取和写入的寄存器仓库
* @param trace 可选的轨迹输出;非空时会先清空再写入本轮结果
* @return 扫描结果,失败时不会返回部分成功状态作为成功结果
*/
LogicScanResult executeScan(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
LogicTraceSnapshot *trace = nullptr);
// 使用指定时间执行扫描,便于稳定验证 TON 等时间逻辑
/**
* @brief 使用指定时间执行一轮扫描
* @param logics 按工程顺序扫描的控制逻辑集合
* @param repository 扫描读取和写入的寄存器仓库
* @param now 本轮扫描使用的单调时钟时间点
* @param trace 可选的轨迹输出;适合测试 TON 等时间相关逻辑
* @return 扫描结果
*/
LogicScanResult executeScanAt(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
@@ -107,19 +135,38 @@ public:
LogicTraceSnapshot *trace = nullptr);

private:
// 单个 TON 定时器跨扫描保存的计时状态
struct TonRuntimeState
{
bool timing = false;
bool done = false;
TimePoint startedAt{};
std::chrono::milliseconds elapsed{0};
bool timing = false; // 是否正在累计输入有效时间
bool done = false; // 是否已经完成预置时间
TimePoint startedAt{}; // 本次计时开始时间
std::chrono::milliseconds elapsed{0}; // 最近一次计算的累计时间
};

/**
* @brief 计算单个条件节点的导通结果
* @param logic_id 所属控制逻辑 ID,用于隔离边沿触点的跨扫描状态
* @param node 待读取和计算的条件节点
* @param repository 提供 M/D 值的寄存器仓库;T/C 条件读取执行器内部状态
* @param value 输出节点导通结果,不能为空
* @return 成功结果,或包含节点 ID 的逻辑/寄存器读取失败结果
*/
LogicScanResult evaluateCondition(
const std::string &logic_id,
const LogicNode &node,
RegisterRepository &repository,
bool *value);
/**
* @brief 递归计算串联、并联或叶子条件表达式
* @param logic_id 所属控制逻辑 ID,用于传递给叶子节点状态计算
* @param expression 待读取的条件表达式树
* @param repository 提供 M/D 值的寄存器仓库
* @param trace 可选的轨迹输出,用于记录表达式值、电源输入和电源输出
* @param input_power 进入当前表达式的电源状态
* @param value 输出表达式最终值,不能为空
* @return 成功结果,或包含失败节点 ID 的扫描错误
*/
LogicScanResult evaluateExpression(
const std::string &logic_id,
const ConditionExpression &expression,
@@ -127,6 +174,17 @@ private:
LogicTraceValues *trace,
bool input_power,
bool *value);
/**
* @brief 在网络结果驱动下执行单个输出节点
* @param logic_id 所属控制逻辑 ID,用于隔离计数器等跨扫描状态
* @param node 待读取配置并执行的输出节点
* @param rung_value 当前网络的逻辑结果,决定输出是否动作
* @param repository 提供输出读写所需的 M/D 寄存器仓库
* @param now 本轮扫描的单调时钟时间点,TON 使用它计算累计时间
* @param trace 可选的轨迹输出,用于记录 TON、计数器和字操作结果
* @param output_value 输出节点最终逻辑值,不能为空
* @return 成功结果,或包含节点 ID 的逻辑/寄存器读写失败结果
*/
LogicScanResult executeOutput(
const std::string &logic_id,
const LogicNode &node,
@@ -135,18 +193,30 @@ private:
TimePoint now,
LogicTraceValues *trace,
bool *output_value);
/**
* @brief 解析并读取一个字操作数
* @param operand 常量或 D 寄存器操作数配置
* @param repository 读取寄存器操作数时使用的仓库
* @param value 输出解析后的 int16 数值,不能为空
* @param node_id 关联节点 ID,用于构造可定位的失败结果
* @return 常量读取成功,或 D 寄存器读取/操作数类型校验失败
*/
LogicScanResult readWordOperand(
const WordOperand &operand,
RegisterRepository &repository,
std::int16_t *value,
const std::string &node_id) const;

// 按逻辑 ID 和节点 ID 保存上一轮边沿输入
std::map<std::pair<std::string, std::string>, bool> previous_edge_inputs_;
// 按逻辑 ID 和节点 ID 保存上一轮计数输入
std::map<std::pair<std::string, std::string>, bool> previous_counter_inputs_;
// 按定时器编号保存跨扫描状态
std::unordered_map<int, TonRuntimeState> ton_states_;
// 单个计数器跨扫描保存的完成状态
struct CounterRuntimeState
{
bool done = false;
bool done = false; // 是否已经达到预置值
};
std::unordered_map<int, CounterRuntimeState> counter_states_;
std::unordered_map<int, CounterRuntimeState> counter_states_; // 按计数器编号保存状态
};

+ 3
- 1
app/src/ui/alarm_configuration_dialog.h Zobrazit soubor

@@ -20,13 +20,15 @@ class AlarmConfigurationDialog;
class AlarmEditorService;
struct AlarmDefinition;

/** 报警定义编辑对话框;保存和删除都委托 AlarmEditorService */
/** 报警定义编辑对话框;保存、更新和删除都委托 AlarmEditorService */
class AlarmConfigurationDialog final : public QDialog
{
public:
/** 创建报警配置对话框并绑定报警编辑服务 */
explicit AlarmConfigurationDialog(
AlarmEditorService &service,
QWidget *parent = nullptr);
/** 释放 Designer 界面对象 */
~AlarmConfigurationDialog() override;

private:


+ 2
- 0
app/src/ui/free_monitor_widget.h Zobrazit soubor

@@ -27,9 +27,11 @@ class FreeMonitorWidget final : public QWidget
Q_OBJECT

public:
/** 创建自由监控控件并绑定监控服务 */
explicit FreeMonitorWidget(
RegisterMonitorService &service,
QWidget *parent = nullptr);
/** 释放 Designer 界面对象 */
~FreeMonitorWidget() override;

/** 真机未连接、通信故障或编辑态时应关闭写入入口 */


+ 3
- 1
app/src/ui/logic_editor_widget.h Zobrazit soubor

@@ -27,6 +27,7 @@ class LogicEditorWidget final : public QGraphicsView
Q_OBJECT

public:
/** 梯形图中的条件、输出、横线和辅助选择图元类型 */
class NodeItem;
class WireItem;
class WireCellItem;
@@ -34,11 +35,12 @@ public:
class RungItem;
class EmptySlotItem;

/** 创建梯形图画布并绑定编辑服务 */
explicit LogicEditorWidget(
LogicEditorService &editor_service,
QWidget *parent = nullptr);

/** 切换当前显示的控制逻辑 */
/** 切换当前显示的控制逻辑;不存在时显示空场景 */
void setLogicId(const std::string &logic_id);
/** 设置是否允许编辑梯形图 */
void setEditingEnabled(bool enabled);


+ 2
- 0
app/src/ui/logic_instruction_dialog.h Zobrazit soubor

@@ -27,9 +27,11 @@ class LogicInstructionDialog final : public QDialog
Q_OBJECT

public:
/** 使用已有节点配置初始化指令参数表单 */
explicit LogicInstructionDialog(
const LogicNodeConfig &config,
QWidget *parent = nullptr);
/** 释放 Designer 界面对象 */
~LogicInstructionDialog() override;

/** 返回当前表单配置,调用方仍需交给 LogicEditorService 校验 */


+ 7
- 1
app/src/ui/main_window.h Zobrazit soubor

@@ -1,7 +1,7 @@
/**
* @file main_window.h
* @brief 定义综合平台编程器主窗口及运行模式界面协调逻辑
* @version 0.2.0
* @version 1.0.0
* @author suyu
* @date 2026-08-08
*/
@@ -76,6 +76,11 @@ public:
RegisterCommentService &register_comment_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent = nullptr);
/**
* @brief 创建主窗口,并使用调用方提供的 HMI 页面导航服务
*
* 两个构造函数的区别只在导航服务的所有权;其余服务均由应用入口管理
*/
MainWindow(
RuntimeModeService &runtime_mode_service,
ProjectService &project_service,
@@ -89,6 +94,7 @@ public:
RegisterMonitorService &register_monitor_service,
QWidget *parent = nullptr);

/** 释放窗口拥有的控制器和可选导航服务 */
~MainWindow() override;

/** 返回当前选中的 HMI 页面标识 */


+ 2
- 0
app/src/ui/plc_connection_dialog.h Zobrazit soubor

@@ -24,9 +24,11 @@ class PlcConnectionDialog final : public QDialog
Q_OBJECT

public:
/** 使用现有配置初始化 PLC 参数表单 */
explicit PlcConnectionDialog(
const PlcSerialConfiguration &configuration,
QWidget *parent = nullptr);
/** 释放 Designer 界面对象 */
~PlcConnectionDialog() override;

/** 返回表单中的串口配置,连接前由网关再次校验 */


+ 4
- 3
app/src/ui/project_workspace_controller.h Zobrazit soubor

@@ -48,6 +48,7 @@ public:
/** 通知主窗口编辑状态已经发生变化 */
using EditStateChanged = std::function<void()>;

/** 创建工程树控制器并绑定主窗口服务和编辑器 */
ProjectWorkspaceController(
QWidget &parent,
Ui::MainWindow &ui,
@@ -97,9 +98,9 @@ private:
/** 工程树项目的三种业务类型 */
enum class ItemKind
{
Root = 0,
HmiPage = 1,
ControlLogic = 2
Root = 0, // 工程树根节点
HmiPage = 1, // HMI 页面节点
ControlLogic = 2 // 控制逻辑节点
};

/** 处理工程树选择变化并刷新当前编辑器 */


+ 1
- 0
app/src/ui/property_panel_controller.h Zobrazit soubor

@@ -42,6 +42,7 @@ public:
using StatusReporter = std::function<void(
const QString &message, int timeout_ms)>;

/** 创建属性面板控制器并注入编辑服务和选择状态 */
PropertyPanelController(
QWidget &parent,
Ui::MainWindow &ui,


+ 2
- 0
app/src/ui/register_comment_dialog.h Zobrazit soubor

@@ -25,9 +25,11 @@ class RegisterCommentService;
class RegisterCommentDialog final : public QDialog
{
public:
/** 创建寄存器注释对话框并绑定注释服务 */
explicit RegisterCommentDialog(
RegisterCommentService &service,
QWidget *parent = nullptr);
/** 释放 Designer 界面对象 */
~RegisterCommentDialog() override;

private:


+ 2
- 0
app/src/ui/runtime_monitor_widget.h Zobrazit soubor

@@ -38,6 +38,7 @@ class RuntimeMonitorWidget final : public QWidget
Q_OBJECT

public:
/** 创建组合 HMI、梯形图和自由监控的运行投影 */
RuntimeMonitorWidget(
HmiEditorService &hmi_editor_service,
HmiRuntimeService &hmi_runtime_service,
@@ -47,6 +48,7 @@ public:
AlarmService &alarm_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent = nullptr);
/** 释放组合的运行监控子控件 */
~RuntimeMonitorWidget() override;

/** 将当前页面和控制逻辑绑定到运行监控界面 */


+ 2
- 0
app/src/ui/runtime_monitor_window.h Zobrazit soubor

@@ -25,7 +25,9 @@ class RuntimeMonitorWindow final : public QMainWindow
Q_OBJECT

public:
/** 创建运行监控顶层窗口 */
explicit RuntimeMonitorWindow(QWidget *parent = nullptr);
/** 释放窗口和 Designer 界面对象 */
~RuntimeMonitorWindow() override;

/** 将运行监控控件放入窗口中央区域 */


+ 2
- 0
app/src/ui/runtime_panel_controller.h Zobrazit soubor

@@ -45,6 +45,7 @@ public:
/** 请求上层退出当前运行态 */
using RuntimeExitRequester = std::function<void()>;

/** 创建运行面板控制器并注入运行模式、编辑器和监控窗口依赖 */
RuntimePanelController(
QWidget &parent,
RuntimeModeService &runtime_mode_service,
@@ -64,6 +65,7 @@ public:
StatusReporter status_reporter,
OutputReporter output_reporter,
RuntimeExitRequester runtime_exit_requester);
/** 停止刷新并释放控制器拥有的运行监控窗口 */
~RuntimePanelController();

/** 连接运行监控相关信号并完成初始配置 */


+ 49
- 49
app/src/ui/toolbar_icon_factory.h Zobrazit soubor

@@ -13,55 +13,55 @@
/** 工具栏中使用的语义图标类型 */
enum class UiIcon
{
Edit,
RunOffline,
RunOnline,
NewDocument,
Save,
SaveAs,
Open,
Undo,
Redo,
Exit,
PlcSettings,
Disconnect,
RegisterComments,
HmiButton,
Indicator,
NumericDisplay,
NumericInput,
ProgressBar,
Text,
PageJump,
AlarmList,
AlarmSettings,
Delete,
AddRung,
HorizontalWire,
VerticalWire,
ParallelBranch,
NormallyOpenContact,
NormallyClosedContact,
RisingEdgeContact,
FallingEdgeContact,
TimerContact,
CounterContact,
Coil,
SetCoil,
ResetCoil,
Ton,
Ctu,
Ctd,
Move,
Add,
Subtract,
Compare,
Comment,
More,
HmiPage,
Logic,
Runtime,
ClearList
Edit, // 编辑模式
RunOffline, // 离线运行
RunOnline, // 真机运行
NewDocument, // 新建工程
Save, // 保存工程
SaveAs, // 另存工程
Open, // 打开工程
Undo, // 撤销
Redo, // 重做
Exit, // 退出程序
PlcSettings, // PLC 连接设置
Disconnect, // 断开 PLC
RegisterComments, // 寄存器注释
HmiButton, // HMI 按钮
Indicator, // HMI 指示灯
NumericDisplay, // HMI 数值显示
NumericInput, // HMI 数值输入
ProgressBar, // HMI 进度条
Text, // HMI 文本
PageJump, // HMI 页面跳转
AlarmList, // HMI 报警列表
AlarmSettings, // 报警配置
Delete, // 删除
AddRung, // 新增网络
HorizontalWire, // 横线
VerticalWire, // 竖线
ParallelBranch, // 并联支路
NormallyOpenContact, // 常开触点
NormallyClosedContact, // 常闭触点
RisingEdgeContact, // 上升沿触点
FallingEdgeContact, // 下降沿触点
TimerContact, // 定时器触点
CounterContact, // 计数器触点
Coil, // 普通线圈
SetCoil, // 置位线圈
ResetCoil, // 复位线圈
Ton, // TON 定时器
Ctu, // CTU 加计数器
Ctd, // CTD 减计数器
Move, // MOVE 指令
Add, // ADD 指令
Subtract, // SUB 指令
Compare, // 比较指令
Comment, // 注释
More, // 更多操作
HmiPage, // HMI 页面
Logic, // 控制逻辑
Runtime, // 运行监控
ClearList // 清空列表
};

/**


+ 0
- 2689
app/tests/main_window_tests.cpp
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 0
- 32
app/tests/main_window_tests.pro Zobrazit soubor

@@ -1,32 +0,0 @@
include(pri/test_defaults.pri)
include(pri/test_layers.pri)

TARGET = main_window_tests
QT += widgets serialport testlib

SOURCES += \
main_window_tests.cpp \
$$UI_MAIN_SOURCES \
$$DOMAIN_ALL_SOURCES \
$$SERVICE_PROJECT_SOURCES \
$$SERVICE_ALARM_SOURCES \
$$SERVICE_HMI_SOURCES \
$$SERVICE_LOGIC_SOURCES \
$$SERVICE_OFFLINE_SOURCES \
$$SERVICE_RUNTIME_SOURCES \
$$SERVICE_MONITOR_SOURCES

HEADERS += \
$$UI_MAIN_HEADERS \
$$DOMAIN_ALL_HEADERS \
$$SERVICE_PROJECT_HEADERS \
$$SERVICE_ALARM_HEADERS \
$$SERVICE_HMI_HEADERS \
$$SERVICE_LOGIC_HEADERS \
$$SERVICE_OFFLINE_HEADERS \
$$SERVICE_RUNTIME_HEADERS \
$$SERVICE_MONITOR_HEADERS \
../src/services/plc_communication_gateway.h \
$$TEST_SUPPORT_HEADERS

FORMS += $$UI_MAIN_FORMS

+ 0
- 42
app/tests/pri/test_layers.pri Zobrazit soubor

@@ -132,45 +132,3 @@ INFRASTRUCTURE_PLC_HEADERS = \
../src/infrastructure/plc_communication_error_classifier.h \
../src/infrastructure/plc_register_repository.h \
../src/infrastructure/plc_communication_service.h

UI_MAIN_SOURCES = \
../src/ui/main_window.cpp \
../src/ui/logic_instruction_dialog.cpp \
../src/ui/alarm_configuration_dialog.cpp \
../src/ui/register_comment_dialog.cpp \
../src/ui/plc_connection_dialog.cpp \
../src/ui/free_monitor_widget.cpp \
../src/ui/runtime_monitor_window.cpp \
../src/ui/runtime_monitor_widget.cpp \
../src/ui/toolbar_icon_factory.cpp \
../src/ui/project_workspace_controller.cpp \
../src/ui/property_panel_controller.cpp \
../src/ui/runtime_panel_controller.cpp \
../src/ui/hmi_editor_widget.cpp \
../src/ui/logic_editor_widget.cpp

UI_MAIN_HEADERS = \
../src/ui/main_window.h \
../src/ui/logic_instruction_dialog.h \
../src/ui/alarm_configuration_dialog.h \
../src/ui/register_comment_dialog.h \
../src/ui/plc_connection_dialog.h \
../src/ui/free_monitor_widget.h \
../src/ui/runtime_monitor_window.h \
../src/ui/runtime_monitor_widget.h \
../src/ui/toolbar_icon_factory.h \
../src/ui/project_workspace_controller.h \
../src/ui/property_panel_controller.h \
../src/ui/runtime_panel_controller.h \
../src/ui/hmi_editor_widget.h \
../src/ui/logic_editor_widget.h

UI_MAIN_FORMS = \
../src/ui/main_window.ui \
../src/ui/logic_instruction_dialog.ui \
../src/ui/alarm_configuration_dialog.ui \
../src/ui/register_comment_dialog.ui \
../src/ui/plc_connection_dialog.ui \
../src/ui/free_monitor_widget.ui \
../src/ui/runtime_monitor_window.ui \
../src/ui/runtime_monitor_widget.ui

+ 2
- 6
app/tests/tests.pro Zobrazit soubor

@@ -1,4 +1,4 @@
# 测试总入口:只负责编排目标,不把不同环境的测试揉成一个可执行文件
# 功能测试总入口:性能测试使用 performance_tests.pro 单独运行
TEMPLATE = subdirs
CONFIG += ordered

@@ -11,9 +11,7 @@ SUBDIRS += \
project_management \
register_monitor \
runtime_mode \
plc_runtime \
main_window \
performance
plc_runtime

domain.file = domain_tests.pro
alarm_service.file = alarm_service_tests.pro
@@ -24,5 +22,3 @@ project_management.file = project_management_tests.pro
register_monitor.file = register_monitor_service_tests.pro
runtime_mode.file = runtime_mode_service_tests.pro
plc_runtime.file = plc_runtime_tests.pro
main_window.file = main_window_tests.pro
performance.file = performance_tests.pro

+ 22
- 14
scripts/run_qt_tests.ps1 Zobrazit soubor

@@ -2,7 +2,8 @@
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Debug',
[switch]$SkipPerformance
[ValidateSet('Functional', 'Performance', 'All')]
[string]$Suite = 'Functional'
)

Set-StrictMode -Version Latest
@@ -23,7 +24,7 @@ foreach ($requiredPath in @($qmakePath, $makePath, $testSourceRoot)) {
}
}

$targets = @(
$functionalTargets = @(
'domain_tests',
'alarm_service_tests',
'hmi_editor_service_tests',
@@ -32,17 +33,20 @@ $targets = @(
'project_management_tests',
'register_monitor_service_tests',
'runtime_mode_service_tests',
'plc_runtime_tests',
'main_window_tests'
'plc_runtime_tests'
)
if (-not $SkipPerformance) {
$targets += 'performance_tests'
$performanceTargets = @('performance_tests')

$targets = switch ($Suite) {
'Functional' { $functionalTargets }
'Performance' { $performanceTargets }
'All' { $functionalTargets + $performanceTargets }
}

$originalPath = $env:Path
$originalPlatform = $env:QT_QPA_PLATFORM
$env:Path = "$qtRoot\bin;$mingwBin;$env:Path"
$env:QT_QPA_PLATFORM = 'offscreen'

Write-Host "测试类型:$Suite"

try {
foreach ($target in $targets) {
@@ -68,10 +72,19 @@ try {
}

Write-Host "运行 $target"
& $executablePath
if ($target -eq 'performance_tests') {
$benchmarkResultPath = Join-Path $buildDirectory 'benchmark.csv'
& $executablePath '-o' "$benchmarkResultPath,csv"
} else {
& $executablePath
}
if ($LASTEXITCODE -ne 0) {
throw "测试失败:$target,退出码:$LASTEXITCODE"
}
if ($target -eq 'performance_tests') {
Write-Host "性能结果:$benchmarkResultPath"
Get-Content -LiteralPath $benchmarkResultPath -Encoding utf8
}
}
finally {
Pop-Location
@@ -80,9 +93,4 @@ try {
}
finally {
$env:Path = $originalPath
if ($null -eq $originalPlatform) {
Remove-Item Env:QT_QPA_PLATFORM -ErrorAction SilentlyContinue
} else {
$env:QT_QPA_PLATFORM = $originalPlatform
}
}

Načítá se…
Zrušit
Uložit