Просмотр исходного кода

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

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

+ 1
- 0
app/src/domain/active_register_repository.cpp Просмотреть файл

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

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



+ 2
- 0
app/src/domain/active_register_repository.h Просмотреть файл

@@ -2,11 +2,13 @@

#include "register_repository.h"

// 根据当前运行模式把寄存器访问转发给虚拟仓库或 PLC 缓存仓库
class ActiveRegisterRepository final : public RegisterRepository
{
public:
explicit ActiveRegisterRepository(RegisterRepository &initial_repository);

// 切换活动仓库;运行模式服务负责保证切换时机和生命周期
void use(RegisterRepository &repository);
BitReadResult readBit(const RegisterAddress &address) const override;
RegisterWriteResult writeBit(const RegisterAddress &address, bool value) override;


+ 3
- 0
app/src/domain/alarm_model.h Просмотреть файл

@@ -5,6 +5,7 @@
#include <cstdint>
#include <string>

// 报警触发条件:M 位为 1,或 D 字高于/低于阈值
enum class AlarmCondition
{
MOn,
@@ -12,6 +13,7 @@ enum class AlarmCondition
DLow
};

// 可保存的报警定义;运行时 AlarmService 根据此定义生成报警记录
struct AlarmDefinition
{
std::string id;
@@ -20,5 +22,6 @@ struct AlarmDefinition
std::int16_t threshold = 0;
std::string message;

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

+ 1
- 0
app/src/domain/control_logic_model.cpp Просмотреть файл

@@ -691,6 +691,7 @@ bool ConditionExpression::validateForRunning(std::string *error) const

void normalizeConditionExpression(std::optional<ConditionExpression> *expression)
{
// 删除节点后折叠单子项容器,并合并相邻同类容器,保持 JSON 结构稳定
if (expression == nullptr || !expression->has_value())
{
return;


+ 33
- 0
app/src/domain/control_logic_model.h Просмотреть файл

@@ -9,12 +9,14 @@
#include <variant>
#include <vector>

// 普通触点的导通方式;常闭触点会对读取到的位值取反
enum class ContactMode
{
NormallyOpen,
NormallyClosed
};

// 线圈写入方式:普通写入、置位保持、复位清零
enum class CoilMode
{
Normal,
@@ -22,12 +24,14 @@ enum class CoilMode
Reset
};

// 上升沿/下降沿触点的边沿方向
enum class EdgeMode
{
Rising,
Falling
};

// D 字比较节点支持的比较运算
enum class ComparisonOperator
{
Equal,
@@ -38,18 +42,21 @@ enum class ComparisonOperator
GreaterThanOrEqual
};

// 字操作数可以来自常量,也可以来自 D 寄存器
enum class WordOperandKind
{
Constant,
Register
};

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

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

@@ -78,6 +85,7 @@ struct CompareNodeConfig
std::int16_t value = 0;
};

// 离线执行器内部使用的 T 区地址,不会加入 PLC M/D 轮询
class TimerAddress
{
public:
@@ -97,12 +105,14 @@ private:
int index_ = 0;
};

// 读取 TON 完成状态的条件触点
struct TimerContactNodeConfig
{
TimerAddress address;
ContactMode mode = ContactMode::NormallyOpen;
};

// 离线执行器内部使用的 C 区地址,不会直接映射 PLC C 区
class CounterAddress
{
public:
@@ -128,12 +138,14 @@ enum class CounterMode
Down
};

// 读取计数器完成状态的条件触点
struct CounterContactNodeConfig
{
CounterAddress address;
ContactMode mode = ContactMode::NormallyOpen;
};

// TON 输出的定时器和预置时间,单位为毫秒
struct TonNodeConfig
{
static constexpr int kMinimumPresetMs = 1;
@@ -143,6 +155,7 @@ struct TonNodeConfig
int presetMs = 1000;
};

// CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D
struct CounterNodeConfig
{
CounterAddress address;
@@ -152,6 +165,7 @@ struct CounterNodeConfig
RegisterAddress resetAddress{RegisterArea::M, 0};
};

// MOVE 输出:把一个字操作数写入目标 D 地址
struct MoveNodeConfig
{
WordOperand source;
@@ -164,6 +178,7 @@ enum class ArithmeticOperation
Subtract
};

// ADD/SUB 输出:计算两个字操作数并写入目标 D 地址
struct ArithmeticNodeConfig
{
ArithmeticOperation operation = ArithmeticOperation::Add;
@@ -172,6 +187,7 @@ struct ArithmeticNodeConfig
RegisterAddress destination{RegisterArea::D, 0};
};

// 所有梯形图指令的强类型配置联合,避免用无关字段拼装指令
using LogicNodeConfig = std::variant<
ContactNodeConfig,
EdgeContactNodeConfig,
@@ -184,20 +200,25 @@ using LogicNodeConfig = std::variant<
MoveNodeConfig,
ArithmeticNodeConfig>;

// 返回节点最主要的 M/D 地址;复合输出请使用下面的收集函数
std::optional<RegisterAddress> registerAddressForLogicNode(
const LogicNodeConfig &config);
// 收集节点中所有显式 M/D 引用,用于 PLC 轮询集合构建
void collectRegisterAddressesForLogicNode(
const LogicNodeConfig &config,
std::vector<RegisterAddress> *addresses);

// 表达式中的一个指令节点;configured=false 表示编辑中的未完成草稿
struct LogicNode
{
std::string id;
LogicNodeConfig config;
bool configured = true;

// 校验节点 ID、配置类型和配置内容
bool validate(std::string *error = nullptr) const;
bool isConfigured() const;
// 判断节点能否放在条件区或固定输出槽
bool isCondition() const;
bool isOutput() const;
};
@@ -210,6 +231,7 @@ enum class ConditionExpressionKind
Parallel
};

// 条件区中的持久化横线;columnSpan 表示跨越的网格列数
struct WireSegment
{
static constexpr int kMinimumColumnSpan = 1;
@@ -232,7 +254,9 @@ struct ConditionExpression

static ConditionExpression fromNode(LogicNode node);
static ConditionExpression fromWire(std::string id, int column_span = 1);
// 编辑态校验允许空配置节点,但必须保持树结构合法
bool validate(std::string *error = nullptr) const;
// 运行态校验额外要求每个节点都已经配置完成
bool validateForRunning(std::string *error = nullptr) const;
};

@@ -260,11 +284,15 @@ struct LadderRung
std::optional<ConditionExpression> condition;
std::optional<LogicNode> output;

// 编辑态允许没有条件或输出,便于逐步搭建网络
bool validate(std::string *error = nullptr) const;
// 只检查表达式拓扑、列数和输出位置
bool validateStructure(std::string *error = nullptr) const;
// 运行态要求有输出且所有引用都已配置
bool validateForRunning(std::string *error = nullptr) const;
};

// 一组按顺序扫描的梯形图网络;enabled=false 时运行校验会跳过它
struct ControlLogic
{
std::string id;
@@ -272,17 +300,22 @@ struct ControlLogic
std::vector<LadderRung> rungs;
bool enabled = true;

// 校验可保存结构
bool validate(std::string *error = nullptr) const;
bool validateStructure(std::string *error = nullptr) const;
// 校验运行所需资源和网络配置
bool validateForRunning(std::string *error = nullptr) const;
};

// 检查同一 T 地址是否被多个逻辑节点以冲突方式使用
bool validateTimerReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);
// 检查同一 C 地址是否被多个计数器配置重复占用
bool validateCounterReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);
// 一次性执行 T/C 资源引用冲突检查
bool validateLogicResourceReferencesForRunning(
const std::vector<ControlLogic> &logics,
std::string *error = nullptr);

+ 1
- 0
app/src/domain/project_limits.h Просмотреть файл

@@ -2,6 +2,7 @@

#include <cstddef>

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

constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U;


+ 3
- 0
app/src/domain/project_model.cpp Просмотреть файл

@@ -111,6 +111,7 @@ const RegisterComment *Project::findRegisterComment(

bool Project::validate(std::string *error) const
{
// 这里是工程级总闸门:先检查数量和唯一性,再向下委托页面、报警和逻辑
if (metadata.id.empty() || metadata.name.empty() || metadata.formatVersion.empty())
{
setError(error, "工程 ID、名称和格式版本不能为空");
@@ -258,6 +259,7 @@ bool Project::validate(std::string *error) const
}
}

// 同时收集所有显式 M/D 引用,提前检查 PLC 轮询集合不会超过上限
std::vector<RegisterAddress> poll_addresses;
for (const HmiPage &page : hmiPages)
{
@@ -313,6 +315,7 @@ bool Project::validate(std::string *error) const

bool Project::validateForRunning(std::string *error) const
{
// 运行校验建立在可保存校验之上,再额外拒绝未配置的运行对象
if (!validate(error))
{
return false;


+ 6
- 2
app/src/domain/project_model.h Просмотреть файл

@@ -7,11 +7,13 @@
#include <string>
#include <vector>

// 工程级 M/D 注释,只描述地址含义,不保存实时值
struct RegisterComment
{
RegisterAddress address{RegisterArea::M, 0};
std::string text;

// 校验地址有效性和 UTF-8 文本长度
bool validate(std::string *error = nullptr) const;
};

@@ -26,7 +28,7 @@ struct ProjectMetadata
std::string formatVersion = "1.0";
};

// 聚合工程中的 HMI 页面和控制逻辑
// 聚合工程中的 HMI 页面、报警、寄存器注释和控制逻辑
struct Project
{
// 工程基本信息
@@ -42,8 +44,10 @@ struct Project
// 工程包含的控制逻辑集合
std::vector<ControlLogic> controlLogics;

// 按地址查找工程注释,找不到时返回 nullptr
const RegisterComment *findRegisterComment(const RegisterAddress &address) const;
// 校验工程配置并通过 error 返回失败原因
// 编辑态校验:允许尚未绑定完成的草稿
bool validate(std::string *error = nullptr) const;
// 运行态校验:额外要求所有运行所需对象都已配置
bool validateForRunning(std::string *error = nullptr) const;
};

+ 10
- 0
app/src/domain/register_address.h Просмотреть файл

@@ -2,6 +2,7 @@

#include <string>

// 解析用户输入的 M/D 地址时可能出现的失败原因
enum class RegisterAddressParseError
{
None,
@@ -11,23 +12,30 @@ enum class RegisterAddressParseError
OutOfRange
};

// PLC 中支持的寄存器区域:M 是位,D 是 16 位有符号字
enum class RegisterArea
{
M,
D
};

// 表示一个已经拆分为“区域 + 下标”的 M/D 地址
// 下标统一使用 0~4000,和 Modbus 原始地址保持一致
class RegisterAddress
{
public:
static constexpr int kMinimumIndex = 0;
static constexpr int kMaximumIndex = 4000;

// 构造地址对象;非法下标不会在构造时抛异常,应通过 isValid() 检查
RegisterAddress(RegisterArea area, int index);

// 返回地址区域和数字下标
RegisterArea area() const;
int index() const;
// 判断区域和下标是否满足项目边界
bool isValid() const;
// 转成界面和日志中使用的形式,例如 M10 或 D4000
std::string toString() const;

bool operator==(const RegisterAddress &other) const;
@@ -38,6 +46,7 @@ private:
int index_;
};

// 文本地址解析结果;失败时 address 保持默认值,具体原因见 error
struct RegisterAddressParseResult
{
bool succeeded = false;
@@ -45,4 +54,5 @@ struct RegisterAddressParseResult
RegisterAddressParseError error = RegisterAddressParseError::InvalidFormat;
};

// 解析 M0、D4000 这类地址文本,不接受带偏移的 Modbus 地址
RegisterAddressParseResult parseRegisterAddress(const std::string &text);

+ 8
- 0
app/src/domain/register_monitor_model.h Просмотреть файл

@@ -6,6 +6,7 @@
#include <cstdint>
#include <vector>

// 监控读数的可用状态
enum class MonitorValueState
{
Valid,
@@ -13,6 +14,7 @@ enum class MonitorValueState
CommunicationFault
};

// 一个监控行的地址、可用性和当前值
struct MonitorValue
{
RegisterAddress address{RegisterArea::M, 0};
@@ -21,15 +23,21 @@ struct MonitorValue
std::int16_t wordValue = 0;
};

// 运行期间由用户临时维护的监控地址列表,不写入工程文件
class RegisterMonitorModel
{
public:
static constexpr std::size_t kMaximumItemCount = 64U;

// 返回当前按添加顺序保存的地址
const std::vector<RegisterAddress> &addresses() const;
// 判断地址是否已经在监控列表中
bool contains(const RegisterAddress &address) const;
// 添加一个地址;重复或超过上限时返回 false
bool add(const RegisterAddress &address);
// 删除一个地址;地址不存在时返回 false
bool remove(const RegisterAddress &address);
// 清空当前会话的监控列表
void clear();

private:


+ 8
- 7
app/src/domain/register_repository.h Просмотреть файл

@@ -6,7 +6,7 @@
#include <cstddef>
#include <cstdint>

// 寄存器仓库操作结果
// 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息
enum class RegisterError
{
None, // 操作成功 无错误
@@ -40,23 +40,24 @@ struct RegisterWriteResult
};

// M/D 寄存器访问边界
// 所有运行态读写都必须经过此接口,UI 不直接接触串口或缓存实现
class RegisterRepository
{
public:
virtual ~RegisterRepository() = default;

// 读取 M 区位寄存器
// 读取 M 区位寄存器;传入 D 区地址会返回 AreaMismatch
virtual BitReadResult readBit(const RegisterAddress &address) const = 0;
// 写入 M 区位寄存器
// 写入 M 区位寄存器;真机实现不会在收到 PLC 确认前乐观修改缓存
virtual RegisterWriteResult writeBit(const RegisterAddress &address, bool value) = 0;
// 读取 D 区字寄存器
// 读取 D 区 16 位字寄存器
virtual WordReadResult readWord(const RegisterAddress &address) const = 0;
// 写入 D 区字寄存器
// 写入 D 区 16 位字寄存器
virtual RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) = 0;
};

// 基于内存的离线 M/D 寄存器仓库
// 基于内存的离线 M/D 寄存器仓库;地址范围与真实 PLC 保持一致
class VirtualRegisterRepository : public RegisterRepository
{
public:
@@ -72,7 +73,7 @@ public:
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;

// 将所有虚拟寄存器恢复为默认值
// 将所有虚拟寄存器恢复为默认值,开始新的离线会话
void clear();

private:


+ 5
- 4
app/src/domain/runtime_state.h Просмотреть файл

@@ -1,6 +1,6 @@
#pragma once

// 应用当前运行模式
// 应用当前运行模式;三个状态之间不能直接从离线切到真机
enum class ApplicationMode
{
Editing, // 编辑工程状态
@@ -8,7 +8,7 @@ enum class ApplicationMode
OnlineRunning // 真机联机运行状态
};

// 当前运行模式对应的能力策略
// 当前运行模式对应的能力策略,UI 用它决定哪些入口可用
struct ModePolicy
{
// 是否允许编辑工程
@@ -23,7 +23,7 @@ struct ModePolicy
bool requiresInitialPlcRead = false;
};

// 根据运行模式返回对应的能力策略
// 根据运行模式返回对应的能力策略;default 用于防御非法枚举值
constexpr ModePolicy policyForMode(ApplicationMode mode)
{
switch (mode)
@@ -68,7 +68,7 @@ constexpr ModePolicy policyForMode(ApplicationMode mode)
}
}

// 模式切换失败原因
// 模式切换失败原因,服务层会进一步补充可读消息
enum class ModeTransitionError
{
None, // 切换成功
@@ -88,6 +88,7 @@ struct ModeTransitionResult
ModeTransitionError error = ModeTransitionError::None;
};

// 保存当前模式并执行最基本的状态机约束
class RuntimeState
{
public:


+ 1
- 1
app/src/infrastructure/json_project_storage.h Просмотреть файл

@@ -2,7 +2,7 @@

#include "domain/project_storage.h"

// 使用版本化 JSON 文件保存和加载工程
// 使用严格 1.0 JSON 文件保存和加载工程;加载成功后才替换调用方工程
class JsonProjectStorage final : public ProjectStorage
{
public:


+ 3
- 0
app/src/infrastructure/plc_communication_error_classifier.h Просмотреть файл

@@ -5,6 +5,7 @@
#include <QModbusDevice>
#include <QString>

// Qt 错误码之外的通信现场信息,用于区分“未打开串口”和“已断线”
struct PlcCommunicationErrorContext
{
QString portName;
@@ -12,12 +13,14 @@ struct PlcCommunicationErrorContext
bool receivedValidResponse = false;
};

// 分类后的用户可读通信故障
struct PlcCommunicationFailure
{
PlcCommunicationError type = PlcCommunicationError::Unknown;
QString message;
};

// 将 Qt Modbus 错误和上下文转换为项目统一错误类型
PlcCommunicationFailure classifyPlcCommunicationError(
QModbusDevice::Error error,
const PlcCommunicationErrorContext &context);

+ 9
- 0
app/src/infrastructure/plc_communication_service.cpp Просмотреть файл

@@ -190,6 +190,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice(
{
return {false, "PLC 连接已经启动,请先断开当前连接"};
}
// 每次连接都递增代次;旧连接的异步回复即使晚到也不能污染新缓存
configuration_ = configuration;
++connection_generation_;
disconnecting_ = false;
@@ -213,6 +214,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice(
static_cast<QSerialPort::StopBits>(configuration.stopBits));
master_->setTimeout(configuration.responseTimeoutMs);
master_->setNumberOfRetries(configuration.retries);
// 新连接必须重新完成完整首读,不能沿用上一条连接的运行资格
repository_.invalidate();
updateInitialReadCompleted(false, true);
rebuildPollBlocks();
@@ -253,6 +255,7 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses(
{
return {false, "PLC 轮询地址拆分后最多允许 64 个读块"};
}
// 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复
if (pending_reply_ != nullptr)
{
pending_poll_addresses_ = std::move(normalized);
@@ -320,6 +323,7 @@ void PlcCommunicationService::rebuildPollBlocks()
return left.index() < right.index();
});
addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end());
// 将相邻地址合并为读块,同时遵守 Modbus 单次读取上限
poll_blocks_.clear();
for (const RegisterAddress &address : addresses)
{
@@ -373,6 +377,7 @@ void PlcCommunicationService::pollNextBlock()
{
return;
}
// 每次只发一个异步请求,完成回调中再推进到下一个块
const std::size_t block_index = next_poll_block_;
const PollBlock block = poll_blocks_.at(block_index);
next_poll_block_ = (next_poll_block_ + 1U) % poll_blocks_.size();
@@ -390,6 +395,7 @@ void PlcCommunicationService::pollNextBlock()
this,
[this, reply, block, block_index, generation]
{
// 连接已经重建时,直接丢弃旧回复
if (generation != connection_generation_)
{
reply->deleteLater();
@@ -406,6 +412,7 @@ void PlcCommunicationService::pollNextBlock()
initial_blocks_read_.cbegin(),
initial_blocks_read_.cend(),
[](bool read) { return read; });
// 所有轮询块都成功读过,才授予真机运行资格
if (completed && !initial_read_completed_)
{
updateInitialReadCompleted(true);
@@ -534,6 +541,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock
handleModbusError(reply->error());
return;
}
// 只有成功回复才能更新 PLC 缓存;写请求不会直接改缓存
const QModbusDataUnit result = reply->result();
for (uint index = 0; index < result.valueCount(); ++index)
{
@@ -724,6 +732,7 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure)
last_error_ = toUtf8(failure.message);
poll_timer_.stop();
recovery_timer_.stop();
// 任意通信故障都会撤销首读资格;恢复后必须重新完整读取
updateInitialReadCompleted(false);
const bool disconnected = failure.type == PlcCommunicationError::SerialPortOpenFailed
|| failure.type == PlcCommunicationError::SerialConnectionLost


+ 11
- 0
app/src/infrastructure/plc_communication_service.h Просмотреть файл

@@ -17,6 +17,8 @@ class QModbusReply;
class QModbusRtuSerialMaster;
struct PlcCommunicationFailure;

// 基于 Qt Modbus RTU 的异步 PLC 通信实现
// 负责连接、轮询、单点写入、故障恢复和首读资格,不阻塞 UI 线程
class PlcCommunicationService final : public QObject, public PlcCommunicationGateway
{
Q_OBJECT
@@ -53,21 +55,29 @@ signals:
private:
struct PollBlock
{
// 同一区域的一段连续 Modbus 原始地址
RegisterArea area = RegisterArea::M;
int startAddress = 0;
int count = 1;
};

// 把去重后的地址集合压缩为有限数量的连续读块
void rebuildPollBlocks();
// 应用新的轮询集合;有请求在途时由 pending_* 延后应用
void applyPollAddresses(const std::vector<RegisterAddress> &addresses);
// 发送下一段异步读取请求
void pollNextBlock();
// Faulted 状态下用轻量探测判断通信是否恢复
void probeRecovery();
void handleRecoveryProbeFailure(QModbusDevice::Error error);
void restoreCommunication();
// 处理读回复并把值写入 PLC 缓存
void handleReadFinished(QModbusReply *reply, PollBlock block);
// 发送单点 M/D 写请求;缓存等待后续轮询确认
RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value);
RegisterWriteResult sendWordWrite(
const RegisterAddress &address, std::int16_t value);
// 更新“所有轮询块均成功读取”的真机进入资格
void updateInitialReadCompleted(bool completed, bool force_notification = false);
void handleUnexpectedDisconnect();
void handleModbusError(QModbusDevice::Error error);
@@ -93,6 +103,7 @@ private:
std::uint64_t connection_generation_ = 0;
PlcConnectionState state_ = PlcConnectionState::Disconnected;
bool initial_read_completed_ = false;
// 首读阶段每个读块的完成标记,全部为 true 后才允许进入真机
std::vector<bool> initial_blocks_read_;
PlcCommunicationError last_error_type_ = PlcCommunicationError::None;
std::string last_error_;


+ 6
- 0
app/src/infrastructure/plc_register_repository.h Просмотреть файл

@@ -5,6 +5,8 @@
#include <array>
#include <functional>

// 保存 PLC 最近一次成功读回的 M/D 缓存,并把写请求转交给通信服务
// 未读到过的地址保持 Unavailable,避免界面显示伪造的初始值
class PlcRegisterRepository final : public RegisterRepository
{
public:
@@ -16,12 +18,16 @@ public:
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;

// 注入异步写入回调;回调成功不代表缓存已经更新
void setWriteHandlers(
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler,
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler);
// 由通信服务在读回成功后更新缓存并标记地址有效
void updateBit(int address, bool value);
void updateWord(int address, std::int16_t value);
// 通信会话失效时清除“已读”标记,但保留数组内的旧值用于诊断
void invalidate();
// 判断是否至少收到过一个有效 M/D 值
bool hasAnyValidValue() const;

private:


+ 8
- 0
app/src/services/alarm_editor_service.h Просмотреть файл

@@ -7,6 +7,7 @@

class ProjectService;

// 报警编辑操作的失败分类
enum class AlarmEditorError
{
None,
@@ -15,6 +16,7 @@ enum class AlarmEditorError
InvalidDefinition
};

// 报警编辑结果;成功时 id 是新增或更新后的定义 ID
struct AlarmEditorResult
{
bool succeeded = false;
@@ -23,16 +25,22 @@ struct AlarmEditorResult
std::string id;
};

// 负责报警定义的增删改,不负责读取实时寄存器
class AlarmEditorService
{
public:
explicit AlarmEditorService(ProjectService &project_service);

// 返回当前工程中的报警定义
const std::vector<AlarmDefinition> &definitions() const;
// 按稳定 ID 查找报警定义
const AlarmDefinition *findDefinition(const std::string &id) const;
// 添加定义;失败时工程保持不变
AlarmEditorResult addDefinition(const AlarmDefinition &definition);
// 更新指定 ID;definition.id 会被服务统一为传入的 id
AlarmEditorResult updateDefinition(
const std::string &id, const AlarmDefinition &definition);
// 删除指定报警定义
AlarmEditorResult removeDefinition(const std::string &id);

private:


+ 1
- 0
app/src/services/alarm_service.cpp Просмотреть файл

@@ -16,6 +16,7 @@ AlarmService::AlarmService(

void AlarmService::refresh()
{
// 每次刷新只根据当前读回值增删记录,不会把通信失败误判为报警解除
const auto now = std::chrono::system_clock::now();
for (const AlarmDefinition &definition
: project_service_.project().alarmDefinitions)


+ 6
- 0
app/src/services/alarm_service.h Просмотреть файл

@@ -9,6 +9,7 @@ class ProjectService;
class RegisterRepository;
struct AlarmDefinition;

// 运行时产生的报警记录,不会写入工程 JSON
struct AlarmRecord
{
std::string definitionId;
@@ -17,6 +18,7 @@ struct AlarmRecord
std::chrono::system_clock::time_point occurredAt;
};

// 根据当前活动寄存器仓库评估报警,并维护确认状态
class AlarmService
{
public:
@@ -24,9 +26,13 @@ public:
const ProjectService &project_service,
RegisterRepository &repository);

// 重新评估全部定义;仍处于触发状态的记录会保留确认状态
void refresh();
// 确认指定报警;不存在时返回 false
bool acknowledge(const std::string &definition_id);
// 清空当前运行会话的报警记录
void reset();
// 返回按触发时间保存的报警记录
const std::vector<AlarmRecord> &records() const;

private:


+ 3
- 0
app/src/services/editor_history.h Просмотреть файл

@@ -17,6 +17,7 @@ public:
static constexpr std::size_t kMaximumEntries = 100U;

template <typename Equal>
// 只有前后状态不同才记录;新编辑会清空重做栈
void record(State before, const State &after, Equal equal)
{
if (equal(before, after))
@@ -28,6 +29,7 @@ public:
redo_states_.clear();
}

// 返回撤销后的目标状态,并把当前状态放入重做栈
std::optional<State> undo(State current)
{
if (undo_states_.empty())
@@ -41,6 +43,7 @@ public:
return target;
}

// 返回重做后的目标状态,并把当前状态放回撤销栈
std::optional<State> redo(State current)
{
if (redo_states_.empty())


+ 1
- 0
app/src/services/hmi_editor_service.cpp Просмотреть файл

@@ -526,6 +526,7 @@ HmiEditorResult HmiEditorService::moveControl(
HmiControl candidate = *control;
candidate.bounds = bounds;
// 更新前保留原控件,只有全部编辑规则通过才覆盖原值
// 属性更新是整体替换:类型不变,但绑定、外观和扩展属性必须一起通过校验
std::string error;
if (!validateEditableControl(*page, candidate, &error))
{


+ 1
- 0
app/src/services/hmi_navigation_service.cpp Просмотреть файл

@@ -11,6 +11,7 @@ HmiNavigationService::HmiNavigationService(ProjectService &project_service)

HmiNavigationResult HmiNavigationService::start()
{
// 运行开始始终从工程声明的初始页面进入,而不是沿用上次会话页面
const Project &project = project_service_.project();
if (project.hmiPages.empty())
{


+ 7
- 0
app/src/services/hmi_navigation_service.h Просмотреть файл

@@ -4,6 +4,7 @@

class ProjectService;

// HMI 页面跳转失败分类
enum class HmiNavigationError
{
None,
@@ -11,6 +12,7 @@ enum class HmiNavigationError
PageNotFound
};

// 页面跳转结果;成功时 pageId 是当前页 ID
struct HmiNavigationResult
{
bool succeeded = false;
@@ -19,14 +21,19 @@ struct HmiNavigationResult
std::string pageId;
};

// 管理运行态当前页面,不直接操作 Qt 页面控件
class HmiNavigationService
{
public:
explicit HmiNavigationService(ProjectService &project_service);

// 进入工程初始页面
HmiNavigationResult start();
// 跳转到指定页面;目标不存在时保留原页面
HmiNavigationResult navigateTo(const std::string &page_id);
// 结束运行会话并清空当前页
void stop();
// 返回当前运行页面 ID,未启动时为空
const std::string &currentPageId() const;

private:


+ 1
- 0
app/src/services/hmi_runtime_service.h Просмотреть файл

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


+ 4
- 0
app/src/services/logic_editor_service.cpp Просмотреть файл

@@ -918,6 +918,7 @@ LogicEditorResult LogicEditorService::appendCondition(
{
return failure(LogicEditorError::InvalidNode, "梯形图条件不能使用线圈节点");
}
// 先生成稳定节点 ID,再把新节点接到已有表达式的串联末尾
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
HistoryState before = captureState();
@@ -941,6 +942,7 @@ LogicEditorResult LogicEditorService::appendCondition(
std::move(leaf));
}
std::string validation_error;
// 编辑服务修改后立即做整条网络校验;失败就恢复包括脏标记在内的完整快照
if (!rung->validate(&validation_error))
{
rollbackEdit(std::move(before), modified_before);
@@ -981,6 +983,7 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn(
return failure(LogicEditorError::InvalidOperation, "所选网格已被条件或横线占用");
}

// 点击远端空网格时,用持久化横线填充前置空档,再放入真实条件节点
const int leading_wire_columns = column - occupied_columns;
const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config));
ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config));
@@ -1035,6 +1038,7 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn(
}

std::string validation_error;
// 原子提交要求候选表达式整体通过校验,否则撤销本次所有局部拼接
if (!rung->validate(&validation_error))
{
rollbackEdit(std::move(before), modified_before);


+ 13
- 0
app/src/services/logic_editor_service.h Просмотреть файл

@@ -8,6 +8,7 @@

class ProjectService;

// 梯形图编辑失败分类
enum class LogicEditorError
{
None,
@@ -22,6 +23,7 @@ enum class LogicEditorError
LastLogicRequired
};

// 梯形图编辑结果;成功时 id 通常是新建或更新对象的稳定 ID
struct LogicEditorResult
{
bool succeeded = false;
@@ -30,11 +32,13 @@ struct LogicEditorResult
std::string id;
};

// 负责把 UI 编辑命令转换为结构化表达式树操作,并维护撤销/重做
class LogicEditorService
{
public:
explicit LogicEditorService(ProjectService &project_service);

// 以下查询接口只读工程模型,供编辑器投影和属性面板使用
const ControlLogic *findLogic(const std::string &logic_id) const;
const LadderRung *findRung(
const std::string &logic_id, const std::string &rung_id) const;
@@ -50,13 +54,16 @@ public:
const std::string &logic_id, const std::string &node_id) const;
std::string registerCommentFor(const RegisterAddress &address) const;

// 确保新工程至少有一组可编辑逻辑
LogicEditorResult ensureDefaultLogic();
// 逻辑组管理
LogicEditorResult addLogic(const std::string &name);
LogicEditorResult renameLogic(
const std::string &logic_id, const std::string &name);
LogicEditorResult removeLogic(const std::string &logic_id);
LogicEditorResult moveLogic(const std::string &logic_id, int offset);
LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled);
// 网络管理
LogicEditorResult addRung(const std::string &logic_id);
LogicEditorResult removeRung(
const std::string &logic_id, const std::string &rung_id);
@@ -64,6 +71,7 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const std::string &comment);
// 条件区编辑:串联、按列插入、横线和并联分支
LogicEditorResult appendCondition(
const std::string &logic_id,
const std::string &rung_id,
@@ -107,11 +115,13 @@ public:
const std::string &logic_id,
const std::string &rung_id,
const std::vector<std::string> &selected_expression_ids);
// 输出槽编辑;每个网络最多一个输出节点
LogicEditorResult setOutput(
const std::string &logic_id,
const std::string &rung_id,
const LogicNodeConfig &config,
bool configured = false);
// 节点属性和删除操作
LogicEditorResult updateNodeConfig(
const std::string &logic_id,
const std::string &node_id,
@@ -130,6 +140,7 @@ public:
const std::string &rung_id,
const std::vector<std::string> &expression_ids);

// 当前编辑会话的撤销/重做
bool canUndo() const;
bool canRedo() const;
LogicEditorResult undo();
@@ -137,6 +148,7 @@ public:
void clearHistory();

private:
// 只保存逻辑集合快照,当前选择等 UI 会话状态不进入历史
struct HistoryState
{
std::vector<ControlLogic> logics;
@@ -144,6 +156,7 @@ private:

HistoryState captureState() const;
void recordHistory(HistoryState before);
// 失败时恢复工程内容和操作前脏标记,保证编辑原子性
void rollbackEdit(HistoryState before, bool modified_before);
static bool statesEqual(
const HistoryState &left, const HistoryState &right);


+ 4
- 0
app/src/services/offline_simulation_service.cpp Просмотреть файл

@@ -22,6 +22,7 @@ SimulationStartResult OfflineSimulationService::start(
return {false, SimulationStartError::AlreadyRunning, {}};
}

// 仿真期间使用启动时快照,编辑器后续修改不会改变正在扫描的逻辑
std::vector<ControlLogic> snapshot = logics;
const LogicScanResult validation = executor_.validate(snapshot);
if (!validation.succeeded)
@@ -30,6 +31,7 @@ SimulationStartResult OfflineSimulationService::start(
return {false, SimulationStartError::InvalidLogic, validation};
}

// 重启会话时清空 T/C/沿触发状态和虚拟 M/D,避免继承上次运行现场
timer_.stop();
executor_.resetRuntime();
repository_.clear();
@@ -68,6 +70,7 @@ LogicScanResult OfflineSimulationService::executeOnce()
{},
{}};
}
// 执行器负责一轮完整扫描;任何读写错误都会让服务进入 Faulted
const LogicScanResult result = executor_.executeScan(
logic_snapshot_, repository_, &trace_snapshot_);
if (!result.succeeded)
@@ -112,6 +115,7 @@ void OfflineSimulationService::handleTimeout()

void OfflineSimulationService::enterFault(const LogicScanResult &error)
{
// 故障后停止定时器和运行时状态,保留错误及最后轨迹供 UI 诊断
timer_.stop();
executor_.resetRuntime();
last_error_ = error;


+ 7
- 0
app/src/services/offline_simulation_service.h Просмотреть файл

@@ -8,6 +8,7 @@
#include <cstdint>
#include <vector>

// 离线扫描器的生命周期状态
enum class SimulationState
{
Stopped,
@@ -15,6 +16,7 @@ enum class SimulationState
Faulted
};

// 启动离线仿真失败的原因
enum class SimulationStartError
{
None,
@@ -22,6 +24,7 @@ enum class SimulationStartError
InvalidLogic
};

// 离线仿真启动结果;detail 保存领域执行器的具体校验错误
struct SimulationStartResult
{
bool succeeded = false;
@@ -41,14 +44,18 @@ public:
VirtualRegisterRepository &repository,
QObject *parent = nullptr);

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

SimulationState state() const;
int scanIntervalMs() const;
std::uint64_t successfulScanCount() const;
const LogicScanResult &lastError() const;
// 返回最近一轮按逻辑 ID 隔离的运行轨迹
const LogicTraceSnapshot &traceSnapshot() const;

signals:


+ 8
- 0
app/src/services/plc_communication_gateway.h Просмотреть файл

@@ -9,6 +9,7 @@
#include <string>
#include <vector>

// PLC 串口和轮询参数;parity 使用 Qt 约定值:0 无、2 偶、3 奇
struct PlcSerialConfiguration
{
std::string portName;
@@ -22,6 +23,7 @@ struct PlcSerialConfiguration
int pollIntervalMs = 200;
};

// PLC 异步通信生命周期
enum class PlcConnectionState
{
Disconnected,
@@ -31,6 +33,7 @@ enum class PlcConnectionState
Faulted
};

// 通信故障分类,用于状态栏提示和真机运行资格撤销
enum class PlcCommunicationError
{
None,
@@ -47,12 +50,14 @@ enum class PlcCommunicationError
Unknown
};

// 通信命令的受理结果;真正的读写完成由回调和缓存更新通知
struct PlcCommunicationResult
{
bool succeeded = false;
std::string message;
};

// 校验串口配置,不打开串口也不产生副作用
inline PlcCommunicationResult validatePlcSerialConfiguration(
const PlcSerialConfiguration &configuration)
{
@@ -108,6 +113,7 @@ inline PlcCommunicationResult validatePlcSerialConfiguration(
return {true, {}};
}

// UI/运行服务使用的异步 PLC 通信契约,具体实现位于 infrastructure
class PlcCommunicationGateway
{
public:
@@ -116,12 +122,14 @@ public:
virtual PlcCommunicationResult connectDevice(
const PlcSerialConfiguration &configuration) = 0;
virtual void disconnectDevice() = 0;
// 设置后续轮询的 M/D 地址集合;实现可以合并相邻地址块
virtual PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) = 0;
virtual PlcConnectionState state() const = 0;
virtual bool initialReadCompleted() const = 0;
virtual PlcCommunicationError lastErrorType() const = 0;
virtual const std::string &lastError() const = 0;
// 注册通信状态、首读资格、缓存更新和错误通知
virtual void setCallbacks(
std::function<void()> state_changed,
std::function<void(bool)> initial_read_changed,


+ 1
- 0
app/src/services/project_service.cpp Просмотреть файл

@@ -134,6 +134,7 @@ ProjectOperationResult ProjectService::load(const std::string &file_path)
"必须指定工程文件路径"};
}

// 存储层先在临时结果中解析,避免半个工程替换当前工程
ProjectLoadResult result = storage_.load(file_path);
if (!result.succeeded)
{


+ 3
- 1
app/src/services/project_service.h Просмотреть файл

@@ -14,7 +14,7 @@ enum class ProjectServiceError
StorageFailure // 工程文件存储操作失败
};

// 工程管理操作结果
// 工程管理操作结果;失败时当前工程和文件路径保持原状
struct ProjectOperationResult
{
bool succeeded = false; // 操作是否成功
@@ -30,7 +30,9 @@ public:
// 使用外部提供的工程存储实现创建服务
explicit ProjectService(ProjectStorage &storage);

// 只读访问当前工程;UI 和其他服务不应绕过 editProject 修改它
const Project &project() const;
// 获取服务层授权的可编辑引用,调用方必须随后完成校验
Project &editProject();
const std::string &currentFilePath() const;
bool hasCurrentFile() const;


+ 6
- 0
app/src/services/register_comment_service.h Просмотреть файл

@@ -7,6 +7,7 @@

class ProjectService;

// 工程级寄存器注释操作的失败分类
enum class RegisterCommentError
{
None,
@@ -14,6 +15,7 @@ enum class RegisterCommentError
NotFound
};

// 注释编辑结果
struct RegisterCommentResult
{
bool succeeded = false;
@@ -21,15 +23,19 @@ struct RegisterCommentResult
std::string message;
};

// 维护工程里的 M/D 注释,不读取也不修改寄存器实时值
class RegisterCommentService
{
public:
explicit RegisterCommentService(ProjectService &project_service);

const std::vector<RegisterComment> &comments() const;
// 按地址查找注释
const RegisterComment *findComment(const RegisterAddress &address) const;
// 新建或更新注释;空文本通常表示删除该注释
RegisterCommentResult setComment(
const RegisterAddress &address, const std::string &text);
// 删除指定地址注释
RegisterCommentResult removeComment(const RegisterAddress &address);

private:


+ 9
- 0
app/src/services/register_monitor_service.h Просмотреть файл

@@ -8,6 +8,7 @@
#include <string>
#include <vector>

// 自由监控列表编辑的失败分类
enum class RegisterMonitorError
{
None,
@@ -17,6 +18,7 @@ enum class RegisterMonitorError
NoChange
};

// 监控列表编辑结果;affectedCount 表示本次受影响的地址数量
struct RegisterMonitorResult
{
bool succeeded = false;
@@ -25,6 +27,7 @@ struct RegisterMonitorResult
int affectedCount = 0;
};

// 自由监控写入结果;底层 RegisterError 会原样保留
struct RegisterMonitorWriteResult
{
bool succeeded = false;
@@ -32,15 +35,21 @@ struct RegisterMonitorWriteResult
std::string message;
};

// 编排监控模型和寄存器仓库,地址列表属于当前会话
class RegisterMonitorService
{
public:
explicit RegisterMonitorService(RegisterRepository &repository);

// 从起始地址连续添加 count 个同区域地址
RegisterMonitorResult addRange(const std::string &start_address, int count);
// 删除一批地址并通知 UI 刷新轮询集合
RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses);
// 清空监控列表
RegisterMonitorResult clear();
// 对单个 M 地址写位
RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value);
// 对单个 D 地址写字
RegisterMonitorWriteResult writeWord(
const RegisterAddress &address, std::int16_t value);
const std::vector<RegisterAddress> &addresses() const;


+ 6
- 0
app/src/services/runtime_mode_service.cpp Просмотреть файл

@@ -67,10 +67,12 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning()
{
return {false, ModeTransitionError::ProjectNotReady};
}
// 离线仿真必须先切到虚拟仓库,避免扫描结果写入 PLC 缓存
if (active_repository_ != nullptr && virtual_repository_ != nullptr)
{
active_repository_->use(*virtual_repository_);
}
// start() 会复制逻辑快照并做运行前校验;失败时不进入运行态
const SimulationStartResult start_result = offline_simulation_service_.start(
project_service_.project().controlLogics);
if (!start_result.succeeded)
@@ -87,6 +89,7 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning()

ModeTransitionResult RuntimeModeService::enterOnlineRunning()
{
// 真机模式只允许使用已完成首读的 PLC 缓存,不复制离线寄存器值
const ModeTransitionResult result = state_.enterOnlineRunning(
initialPlcReadCompleted());
if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr)
@@ -140,6 +143,7 @@ void RuntimeModeService::configurePlc(
active_repository_ = &active_repository;
virtual_repository_ = &virtual_repository;
plc_repository_ = &plc_repository;
// 通信故障撤销首读资格;若正在真机运行,立即回到编辑态
gateway.setCallbacks(
[this]
{
@@ -212,6 +216,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses()
{
return {false, "PLC 通信服务尚未配置"};
}
// 轮询集合来自自由监控、HMI、报警和梯形图引用的并集
std::vector<RegisterAddress> addresses = monitor_addresses_;
const Project &project = project_service_.project();
for (const HmiPage &page : project.hmiPages)
@@ -262,6 +267,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses()
}
return left.index() < right.index();
});
// 排序后去重,保证同一个地址只占一份轮询配额
addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end());
if (addresses.size() > ProjectLimits::kMaximumPollAddresses)
{


+ 3
- 1
app/src/services/runtime_mode_service.h Просмотреть файл

@@ -20,7 +20,7 @@ class ProjectService;
class ActiveRegisterRepository;
class RegisterRepository;

// 隔离 UI 与领域状态机并编排进入真机运行态的前置条件
// 隔离 UI 与领域状态机并编排寄存器仓库切换和进入真机的前置条件
class RuntimeModeService
{
public:
@@ -69,11 +69,13 @@ public:
std::uint64_t successfulScanCount() const;
const LogicScanResult &simulationError() const;
OfflineSimulationService &offlineSimulationService();
// 注入 PLC 网关、活动仓库和两种实际数据源;应用生命周期内只调用一次
void configurePlc(
PlcCommunicationGateway &gateway,
ActiveRegisterRepository &active_repository,
RegisterRepository &virtual_repository,
RegisterRepository &plc_repository);
// 连接 PLC 并开始异步通信,不阻塞 UI 等待首读
PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration);
void setMonitorAddresses(const std::vector<RegisterAddress> &addresses);
PlcCommunicationResult refreshPlcPollAddresses();


+ 3
- 0
app/src/services/software_logic_executor.cpp Просмотреть файл

@@ -113,6 +113,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic(
LogicScanResult SoftwareLogicExecutor::validate(
const std::vector<ControlLogic> &logics) const
{
// 先检查每个逻辑自身,再检查跨网络共享线圈和 T/C 资源的冲突
std::map<int, CoilMode> output_modes;
for (const ControlLogic &logic : logics)
{
@@ -209,10 +210,12 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt(
return validation;
}

// 每轮从空轨迹开始,防止已删除或未执行的节点残留旧状态
if (trace != nullptr)
{
trace->clear();
}
// 按工程顺序扫描;前面网络写入的 M/D 值对后面网络立即可见
for (const ControlLogic &logic : logics)
{
if (!logic.enabled)


+ 11
- 0
app/src/services/software_logic_executor.h Просмотреть файл

@@ -11,6 +11,7 @@
#include <utility>
#include <vector>

// 一轮离线扫描可能返回的错误
enum class LogicScanError
{
None,
@@ -20,6 +21,7 @@ enum class LogicScanError
RegisterWriteFailed
};

// 扫描结果;失败时携带出错的逻辑、网络和节点 ID
struct LogicScanResult
{
bool succeeded = false;
@@ -30,6 +32,7 @@ struct LogicScanResult
std::string nodeId;
};

// TON 节点在最近一轮扫描中的可视化状态
struct TonTraceValue
{
bool input = false;
@@ -38,6 +41,7 @@ struct TonTraceValue
int presetMs = 0;
};

// CTU/CTD 节点在最近一轮扫描中的可视化状态
struct CounterTraceValue
{
bool input = false;
@@ -47,12 +51,14 @@ struct CounterTraceValue
std::int16_t preset = 0;
};

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

// 一条逻辑或一个网络的运行轨迹快照
struct LogicTraceValues
{
std::unordered_map<std::string, bool> nodeValues;
@@ -68,6 +74,7 @@ struct LogicTraceValues
void clear();
};

// 全工程轨迹;logicValues 按逻辑 ID 隔离,避免节点 ID 重复互相覆盖
struct LogicTraceSnapshot : LogicTraceValues
{
std::unordered_map<std::string, LogicTraceValues> logicValues;
@@ -83,12 +90,16 @@ public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;

// 在启动仿真前检查所有启用逻辑及 T/C 资源引用
LogicScanResult validate(const std::vector<ControlLogic> &logics) const;
// 清除沿触发、定时器和计数器的跨扫描运行状态
void resetRuntime();
// 使用当前 steady_clock 执行一轮扫描
LogicScanResult executeScan(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,
LogicTraceSnapshot *trace = nullptr);
// 使用指定时间执行扫描,便于稳定验证 TON 等时间逻辑
LogicScanResult executeScanAt(
const std::vector<ControlLogic> &logics,
RegisterRepository &repository,


+ 2
- 0
app/src/ui/alarm_configuration_dialog.h Просмотреть файл

@@ -12,6 +12,7 @@ class AlarmConfigurationDialog;
class AlarmEditorService;
struct AlarmDefinition;

// 报警定义编辑对话框;保存和删除都委托 AlarmEditorService
class AlarmConfigurationDialog final : public QDialog
{
public:
@@ -21,6 +22,7 @@ public:
~AlarmConfigurationDialog() override;

private:
// 刷新左侧列表并尽量恢复原选中项
void reloadDefinitions(const std::string &selected_id = {});
void loadSelectedDefinition();
void updateConditionOptions();


+ 2
- 0
app/src/ui/free_monitor_widget.h Просмотреть файл

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

class RegisterMonitorService;

// 自由监控 UI:地址列表属于当前会话,读写统一委托 RegisterMonitorService
class FreeMonitorWidget final : public QWidget
{
Q_OBJECT
@@ -23,6 +24,7 @@ public:
QWidget *parent = nullptr);
~FreeMonitorWidget() override;

// 真机未连接、通信故障或编辑态时应关闭写入入口
void setWriteEnabled(bool enabled);
void refreshValues(ApplicationMode mode, PlcConnectionState plc_state);
void reloadAddresses();


+ 5
- 0
app/src/ui/hmi_editor_widget.cpp Просмотреть файл

@@ -902,6 +902,7 @@ HmiEditorWidget::HmiEditorWidget(
// 切换显示页面
void HmiEditorWidget::setPageId(const std::string &page_id)
{
// 切换页面只改变投影上下文,真正的页面数据仍由 ProjectService 持有
if (page_id_ == page_id)
{
return;
@@ -945,6 +946,7 @@ void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled)
// 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形
void HmiEditorWidget::reloadPage()
{
// 模型发生变化后完全重建图元,避免增量刷新遗漏属性或选择状态
// 画布始终从当前领域页面重建,避免保留已删除控件的图元
scene_->clear();
const HmiPage *page = editor_service_.findPage(page_id_);
@@ -1052,6 +1054,7 @@ std::vector<std::string> HmiEditorWidget::selectedControlIds() const

void HmiEditorWidget::refreshRuntimeValues()
{
// 运行态才读取寄存器;编辑态画布只显示工程配置,不显示缓存值
if (!runtime_active_)
{
return;
@@ -1119,6 +1122,7 @@ void HmiEditorWidget::handleSelectionChanged()
void HmiEditorWidget::handleControlMoved(
const std::string &control_id, const QPointF &position)
{
// 拖动结束后把坐标交给服务层,由服务层负责页面边界校验和历史记录
const HmiControl *control = editor_service_.findControl(page_id_, control_id);
if (control == nullptr)
{
@@ -1146,6 +1150,7 @@ void HmiEditorWidget::handleButtonEvent(
{
return;
}
// 运行态按钮事件只通过 HmiRuntimeService 写寄存器,不直接访问仓库
const HmiControl *control = editor_service_.findControl(page_id_, control_id);
if (control == nullptr)
{


+ 1
- 0
app/src/ui/hmi_editor_widget.h Просмотреть файл

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


+ 2
- 0
app/src/ui/logic_editor_widget.cpp Просмотреть файл

@@ -1048,6 +1048,7 @@ void LogicEditorWidget::setEditingEnabled(bool enabled)
void LogicEditorWidget::setRuntimeTrace(
const LogicTraceSnapshot &trace, const std::string &fault_node_id)
{
// 轨迹只是模型的只读投影;编辑器不因显示轨迹而修改工程表达式
trace_ = trace;
fault_node_id_ = fault_node_id;
runtime_trace_enabled_ = true;
@@ -1064,6 +1065,7 @@ void LogicEditorWidget::clearRuntimeTrace()

void LogicEditorWidget::reloadLogic()
{
// 逻辑或选择变化后重新计算网格布局和可点击插入目标
scene_->clear();
const ControlLogic *logic = editor_service_.findLogic(logic_id_);
if (logic == nullptr)


+ 3
- 0
app/src/ui/logic_editor_widget.h Просмотреть файл

@@ -13,6 +13,7 @@
class QGraphicsScene;
class QResizeEvent;

// 将结构化梯形图表达式投影成网格图元;不保存自由线段,编辑通过服务提交
class LogicEditorWidget final : public QGraphicsView
{
Q_OBJECT
@@ -31,6 +32,7 @@ public:

void setLogicId(const std::string &logic_id);
void setEditingEnabled(bool enabled);
// 设置离线运行轨迹;真机模式应传入空轨迹,避免显示本地伪轨迹
void setRuntimeTrace(
const LogicTraceSnapshot &trace,
const std::string &fault_node_id = {});
@@ -42,6 +44,7 @@ public:
std::string selectedRungId() const;

LogicEditorResult addRung();
// 下列编辑命令只负责把当前选择翻译成服务调用
LogicEditorResult addCondition(const LogicNodeConfig &config);
LogicEditorResult addParallelBranch(const LogicNodeConfig &config);
LogicEditorResult addHorizontalWire();


+ 2
- 0
app/src/ui/logic_instruction_dialog.h Просмотреть файл

@@ -13,6 +13,7 @@ namespace Ui {
class LogicInstructionDialog;
}

// 梯形图节点配置对话框;只负责把表单转换为强类型 LogicNodeConfig
class LogicInstructionDialog final : public QDialog
{
Q_OBJECT
@@ -23,6 +24,7 @@ public:
QWidget *parent = nullptr);
~LogicInstructionDialog() override;

// 返回当前表单配置,调用方仍需交给 LogicEditorService 校验
LogicNodeConfig config() const;

private:


+ 2
- 0
app/src/ui/main_window.cpp Просмотреть файл

@@ -1494,6 +1494,7 @@ void MainWindow::appendOutputMessage(const QString &message)

void MainWindow::requestMode(ApplicationMode requested_mode)
{
// UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定
// UI 仅转发模式意图,合法性由服务层和领域状态机决定
ModeTransitionResult result;
switch (requested_mode)
@@ -1544,6 +1545,7 @@ void MainWindow::requestMode(ApplicationMode requested_mode)

void MainWindow::updateModeUi(const QString &message)
{
// 按服务层策略统一启用/禁用编辑入口,避免单个按钮遗漏状态同步
const ApplicationMode mode = runtime_mode_service_.mode();
const ModePolicy policy = runtime_mode_service_.policy();
restoreCurrentModeAction();


+ 1
- 0
app/src/ui/main_window.h Просмотреть файл

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


+ 2
- 0
app/src/ui/plc_connection_dialog.h Просмотреть файл

@@ -10,6 +10,7 @@ namespace Ui {
class PlcConnectionDialog;
}

// PLC 串口参数对话框;只编辑配置,不负责打开串口
class PlcConnectionDialog final : public QDialog
{
Q_OBJECT
@@ -20,6 +21,7 @@ public:
QWidget *parent = nullptr);
~PlcConnectionDialog() override;

// 返回表单中的串口配置,连接前由网关再次校验
PlcSerialConfiguration configuration() const;

private:


+ 1
- 0
app/src/ui/project_workspace_controller.h Просмотреть файл

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

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


+ 1
- 0
app/src/ui/property_panel_controller.h Просмотреть файл

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

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


+ 1
- 0
app/src/ui/register_comment_dialog.h Просмотреть файл

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

class RegisterCommentService;

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


+ 2
- 0
app/src/ui/runtime_monitor_widget.cpp Просмотреть файл

@@ -132,6 +132,7 @@ void RuntimeMonitorWidget::setRuntimePage(const std::string &page_id)
void RuntimeMonitorWidget::setMode(
ApplicationMode mode, PlcConnectionState plc_state)
{
// 真机时清空本地轨迹,防止把 PC 离线执行结果误认为 PLC 内部程序轨迹
const bool offline = mode == ApplicationMode::OfflineRunning;
const bool online = mode == ApplicationMode::OnlineRunning;
ui_->logicFrame->setVisible(offline);
@@ -168,6 +169,7 @@ void RuntimeMonitorWidget::setFreeMonitorWriteEnabled(bool enabled)
void RuntimeMonitorWidget::refreshValues(
ApplicationMode mode, PlcConnectionState plc_state)
{
// HMI、报警和自由监控共享同一个活动寄存器仓库
hmi_view_->refreshRuntimeValues();
free_monitor_widget_->refreshValues(mode, plc_state);
}


+ 3
- 0
app/src/ui/runtime_monitor_widget.h Просмотреть файл

@@ -24,6 +24,7 @@ class LogicEditorWidget;
class RegisterMonitorService;
class ProjectService;

// 唯一的工程师运行监控投影,组合 HMI、梯形图轨迹和自由监控
class RuntimeMonitorWidget final : public QWidget
{
Q_OBJECT
@@ -42,10 +43,12 @@ public:

void setProjectObjects(const std::string &page_id, const std::string &logic_id);
void setRuntimePage(const std::string &page_id);
// 根据模式决定是否显示本地逻辑轨迹以及是否允许写入
void setMode(ApplicationMode mode, PlcConnectionState plc_state);
void refreshValues(ApplicationMode mode, PlcConnectionState plc_state);
void setHmiWriteEnabled(bool enabled);
void setFreeMonitorWriteEnabled(bool enabled);
// 只接收离线执行器轨迹;真机时由上层清空
void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {});
FreeMonitorWidget *freeMonitorWidget() const;
std::string selectedLogicId() const;


+ 1
- 0
app/src/ui/runtime_monitor_window.cpp Просмотреть файл

@@ -44,6 +44,7 @@ void RuntimeMonitorWindow::closeForApplicationExit()

void RuntimeMonitorWindow::closeEvent(QCloseEvent *event)
{
// 运行期间关闭按钮等价于请求返回编辑态,应用退出时才真正放行关闭
if (runtime_active_ && !application_exit_)
{
emit exitRequested();


+ 3
- 0
app/src/ui/runtime_monitor_window.h Просмотреть файл

@@ -21,8 +21,11 @@ public:
~RuntimeMonitorWindow() override;

void setMonitorWidget(QWidget &widget);
// 进入运行态时显示并最大化窗口
void showForRuntime();
// 返回编辑态时隐藏窗口但保留唯一监控控件
void hideForEditing();
// 应用退出时允许真正关闭窗口
void closeForApplicationExit();

signals:


+ 3
- 1
app/src/ui/runtime_panel_controller.cpp Просмотреть файл

@@ -158,7 +158,8 @@ void RuntimePanelController::enterRuntime(
if (!runtime_session_active_)
{
runtime_monitor_widget_->setProjectObjects(page_id, logic_id);
runtime_session_active_ = true;
// 运行窗口只创建一次,后续进入运行态复用同一个监控投影
runtime_session_active_ = true;
}
runtime_monitor_widget_->setMode(mode, plc_state);
runtime_monitor_window_->showForRuntime();
@@ -273,6 +274,7 @@ void RuntimePanelController::updateSimulationUi(bool report_fault)

void RuntimePanelController::handleRuntimeTimer()
{
// 定时器只负责刷新投影;离线逻辑扫描由 OfflineSimulationService 自己的定时器驱动
if (runtime_mode_service_.mode() == ApplicationMode::Editing)
{
return;


+ 2
- 0
app/src/ui/runtime_panel_controller.h Просмотреть файл

@@ -56,11 +56,13 @@ public:
~RuntimePanelController();

void configure();
// 创建/复用唯一监控窗口,并把当前工程对象投影到运行态
void enterRuntime(
const std::string &page_id,
const std::string &logic_id,
ApplicationMode mode,
PlcConnectionState plc_state);
// 停止刷新并隐藏监控窗口,离线服务的停止由运行模式服务负责
void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state);
void closeForApplicationExit();
void updateSimulationUi(bool report_fault);


+ 2
- 0
app/src/ui/toolbar_icon_factory.h Просмотреть файл

@@ -2,6 +2,7 @@

#include <QIcon>

// 工具栏语义图标;具体 QIcon 绘制集中在 toolbar_icon_factory.cpp
enum class UiIcon
{
Edit,
@@ -55,4 +56,5 @@ enum class UiIcon
ClearList
};

// 根据语义图标类型创建 Qt 图标
QIcon makeUiIcon(UiIcon icon);

Загрузка…
Отмена
Сохранить