#pragma once #include "domain/register_address.h" #include "domain/project_limits.h" #include #include #include #include #include // PLC 串口和轮询参数;parity 使用 Qt 约定值:0 无、2 偶、3 奇 struct PlcSerialConfiguration { 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, // 未建立 PLC 连接 Connecting, // 正在异步建立 PLC 连接 Connected, // 串口已连接并进行正常轮询,不代表首读已完成 Recovering, // 从可恢复通信故障中探测恢复并重新进行首读 Faulted // 当前通信发生故障,等待恢复探测或重新连接 }; // 通信故障分类,用于状态栏提示和真机运行资格撤销 enum class PlcCommunicationError { None, // 没有待报告的通信错误 SerialPortOpenFailed, // 串口打开失败 PlcNotResponding, // PLC 未响应请求 UsbSerialAdapterRemoved, // USB 转串口设备被移除 SerialConnectionLost, // 已建立的串口连接意外中断 CommunicationTimeout, // 请求等待响应超时 ProtocolError, // 收到的 Modbus 数据不符合协议 ReadFailed, // 读取请求失败 WriteFailed, // 写入请求失败 ConfigurationError, // 串口或通信参数无效 RequestAborted, // 请求在完成前被中止 Unknown // 未分类的通信错误 }; // 通信命令的受理结果;真正的读写完成由回调和缓存更新通知 struct PlcCommunicationResult { bool succeeded = false; // 命令是否被接受或参数校验是否通过 std::string message; // 失败原因或补充说明,成功时通常为空 }; /** * @brief 校验 PLC 串口、Modbus 和轮询参数 * * 此函数只检查配置,不打开串口、不启动通信,也不会修改传入对象 * @param configuration 待校验的连接配置 * @return 校验通过时返回 succeeded 为 true,否则返回 false 和 UTF-8 错误说明 */ inline PlcCommunicationResult validatePlcSerialConfiguration( const PlcSerialConfiguration &configuration) { const bool has_port_name = std::any_of( configuration.portName.cbegin(), configuration.portName.cend(), [](unsigned char character) { return std::isspace(character) == 0; }); if (!has_port_name) { return {false, "必须填写串口端口"}; } if (configuration.serverAddress < ProjectLimits::kMinimumPlcServerAddress || configuration.serverAddress > ProjectLimits::kMaximumPlcServerAddress) { return {false, "PLC 站号必须在 1~247 范围内"}; } if (configuration.baudRate != 9600 && configuration.baudRate != 19200 && configuration.baudRate != 38400 && configuration.baudRate != 57600 && configuration.baudRate != 115200) { return {false, "波特率只支持 9600、19200、38400、57600 或 115200"}; } if (configuration.dataBits != 7 && configuration.dataBits != 8) { return {false, "数据位只支持 7 或 8"}; } if (configuration.parity != 0 && configuration.parity != 2 && configuration.parity != 3) { return {false, "校验方式只支持无校验、偶校验或奇校验"}; } if (configuration.stopBits != 1 && configuration.stopBits != 2) { return {false, "停止位只支持 1 或 2"}; } if (configuration.responseTimeoutMs < ProjectLimits::kMinimumResponseTimeoutMs || configuration.responseTimeoutMs > ProjectLimits::kMaximumResponseTimeoutMs) { return {false, "PLC 响应超时必须在 100~30000 ms 范围内"}; } if (configuration.retries < ProjectLimits::kMinimumRetries || configuration.retries > ProjectLimits::kMaximumRetries) { return {false, "PLC 失败重试次数必须在 0~5 范围内"}; } if (configuration.pollIntervalMs < ProjectLimits::kMinimumPollIntervalMs || configuration.pollIntervalMs > ProjectLimits::kMaximumPollIntervalMs) { return {false, "PLC 轮询周期必须在 50~10000 ms 范围内"}; } return {true, {}}; } // UI/运行服务使用的异步 PLC 通信契约,具体实现位于 infrastructure 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; /** * @brief 设置后续轮询的 M/D 地址集合 * * 实现可以校验、排序、去重并合并相邻地址块;已有读请求进行时, * 新集合可以延后到当前请求完成后生效 * @param addresses 需要周期性读回的 M/D 地址集合,空集合使用实现的默认探测地址 * @return true 表示集合已接受,false 表示地址或轮询资源校验失败 */ virtual PlcCommunicationResult setPollAddresses( const std::vector &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 state_changed, std::function initial_read_changed, std::function cache_updated, std::function error_reported) = 0; };