| @@ -37,6 +37,7 @@ SOURCES += \ | |||||
| src/services/runtime_mode_service.cpp \ | src/services/runtime_mode_service.cpp \ | ||||
| src/services/register_monitor_service.cpp \ | src/services/register_monitor_service.cpp \ | ||||
| src/infrastructure/plc_register_repository.cpp \ | src/infrastructure/plc_register_repository.cpp \ | ||||
| src/infrastructure/plc_communication_error_classifier.cpp \ | |||||
| src/infrastructure/plc_communication_service.cpp \ | src/infrastructure/plc_communication_service.cpp \ | ||||
| src/infrastructure/json_project_storage.cpp \ | src/infrastructure/json_project_storage.cpp \ | ||||
| src/ui/hmi_editor_widget.cpp \ | src/ui/hmi_editor_widget.cpp \ | ||||
| @@ -66,6 +67,7 @@ HEADERS += \ | |||||
| src/services/register_monitor_service.h \ | src/services/register_monitor_service.h \ | ||||
| src/services/plc_communication_gateway.h \ | src/services/plc_communication_gateway.h \ | ||||
| src/infrastructure/plc_register_repository.h \ | src/infrastructure/plc_register_repository.h \ | ||||
| src/infrastructure/plc_communication_error_classifier.h \ | |||||
| src/infrastructure/plc_communication_service.h \ | src/infrastructure/plc_communication_service.h \ | ||||
| src/infrastructure/json_project_storage.h \ | src/infrastructure/json_project_storage.h \ | ||||
| src/ui/hmi_editor_widget.h \ | src/ui/hmi_editor_widget.h \ | ||||
| @@ -0,0 +1,124 @@ | |||||
| #include "plc_communication_error_classifier.h" | |||||
| namespace { | |||||
| QString displayPortName(const QString &port_name) | |||||
| { | |||||
| const QString trimmed = port_name.trimmed(); | |||||
| return trimmed.isEmpty() ? QStringLiteral("当前端口") : trimmed; | |||||
| } | |||||
| bool nativeErrorReportsRemovedDevice(const QString &native_error) | |||||
| { | |||||
| const QString error = native_error.toLower(); | |||||
| return error.contains(QStringLiteral("device has been disconnected")) | |||||
| || error.contains(QStringLiteral("device is not connected")) | |||||
| || error.contains(QStringLiteral("device not found")) | |||||
| || error.contains(QStringLiteral("no such file")) | |||||
| || error.contains(QStringLiteral("设备已断开")) | |||||
| || error.contains(QStringLiteral("设备不存在")) | |||||
| || error.contains(QStringLiteral("找不到指定的文件")); | |||||
| } | |||||
| } // namespace | |||||
| PlcCommunicationFailure classifyPlcCommunicationError( | |||||
| QModbusDevice::Error error, | |||||
| const PlcCommunicationErrorContext &context) | |||||
| { | |||||
| const QString port_name = displayPortName(context.portName); | |||||
| const bool adapter_removed = context.serialSessionOpened | |||||
| && (!context.portAvailable | |||||
| || nativeErrorReportsRemovedDevice(context.nativeError)); | |||||
| if (adapter_removed | |||||
| && error != QModbusDevice::ConfigurationError | |||||
| && error != QModbusDevice::ProtocolError) | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::UsbSerialAdapterRemoved, | |||||
| QStringLiteral("USB 转串口 %1 已从电脑移除;本地串口会话已失效") | |||||
| .arg(port_name)}; | |||||
| } | |||||
| switch (error) | |||||
| { | |||||
| case QModbusDevice::ConnectionError: | |||||
| { | |||||
| if (!context.serialSessionOpened) | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::SerialPortOpenFailed, | |||||
| QStringLiteral( | |||||
| "PLC 连接失败:串口 %1 未能打开;请检查端口是否存在或是否被其他程序占用") | |||||
| .arg(port_name)}; | |||||
| } | |||||
| return { | |||||
| PlcCommunicationError::SerialConnectionLost, | |||||
| QStringLiteral("PLC 串口连接异常,本地端口 %1 仍存在;请重新连接") | |||||
| .arg(port_name)}; | |||||
| } | |||||
| case QModbusDevice::TimeoutError: | |||||
| { | |||||
| if (!context.receivedValidResponse) | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::PlcNotResponding, | |||||
| QStringLiteral( | |||||
| "串口 %1 已打开,但 PLC 未响应;请检查站号、串口参数和 RS-485 A/B 接线") | |||||
| .arg(port_name)}; | |||||
| } | |||||
| return { | |||||
| PlcCommunicationError::CommunicationTimeout, | |||||
| QStringLiteral( | |||||
| "PLC 通信超时,本地串口 %1 仍处于打开状态;请检查 PLC 供电和 RS-485 接线") | |||||
| .arg(port_name)}; | |||||
| } | |||||
| case QModbusDevice::ProtocolError: | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::ProtocolError, | |||||
| QStringLiteral( | |||||
| "PLC 返回 Modbus 协议异常响应;请检查站号、功能码和 PLC 通信配置")}; | |||||
| } | |||||
| case QModbusDevice::ReadError: | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::ReadFailed, | |||||
| QStringLiteral("读取 PLC 数据失败,本地串口 %1 仍处于打开状态") | |||||
| .arg(port_name)}; | |||||
| } | |||||
| case QModbusDevice::WriteError: | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::WriteFailed, | |||||
| QStringLiteral("写入 PLC 数据失败,本地串口 %1 仍处于打开状态") | |||||
| .arg(port_name)}; | |||||
| } | |||||
| case QModbusDevice::ConfigurationError: | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::ConfigurationError, | |||||
| QStringLiteral( | |||||
| "PLC 串口参数配置错误;请检查波特率、数据位、校验位和停止位")}; | |||||
| } | |||||
| case QModbusDevice::ReplyAbortedError: | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::RequestAborted, | |||||
| QStringLiteral("PLC 通信请求已取消")}; | |||||
| } | |||||
| case QModbusDevice::UnknownError: | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::Unknown, | |||||
| QStringLiteral("PLC 通信发生未知错误")}; | |||||
| } | |||||
| case QModbusDevice::NoError: | |||||
| default: | |||||
| { | |||||
| return { | |||||
| PlcCommunicationError::Unknown, | |||||
| QStringLiteral("PLC 通信失败")}; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,25 @@ | |||||
| #pragma once | |||||
| #include "services/plc_communication_gateway.h" | |||||
| #include <QModbusDevice> | |||||
| #include <QString> | |||||
| struct PlcCommunicationErrorContext | |||||
| { | |||||
| QString portName; | |||||
| QString nativeError; | |||||
| bool serialSessionOpened = false; | |||||
| bool portAvailable = false; | |||||
| bool receivedValidResponse = false; | |||||
| }; | |||||
| struct PlcCommunicationFailure | |||||
| { | |||||
| PlcCommunicationError type = PlcCommunicationError::Unknown; | |||||
| QString message; | |||||
| }; | |||||
| PlcCommunicationFailure classifyPlcCommunicationError( | |||||
| QModbusDevice::Error error, | |||||
| const PlcCommunicationErrorContext &context); | |||||
| @@ -1,5 +1,6 @@ | |||||
| #include "plc_communication_service.h" | #include "plc_communication_service.h" | ||||
| #include "plc_communication_error_classifier.h" | |||||
| #include "plc_register_repository.h" | #include "plc_register_repository.h" | ||||
| #include <QModbusDataUnit> | #include <QModbusDataUnit> | ||||
| @@ -7,6 +8,7 @@ | |||||
| #include <QModbusReply> | #include <QModbusReply> | ||||
| #include <QModbusRtuSerialMaster> | #include <QModbusRtuSerialMaster> | ||||
| #include <QSerialPort> | #include <QSerialPort> | ||||
| #include <QSerialPortInfo> | |||||
| #include <QVariant> | #include <QVariant> | ||||
| #include <algorithm> | #include <algorithm> | ||||
| @@ -28,32 +30,6 @@ QModbusDataUnit::RegisterType registerType(RegisterArea area) | |||||
| ? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters; | ? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters; | ||||
| } | } | ||||
| QString modbusErrorText(QModbusDevice::Error error) | |||||
| { | |||||
| switch (error) | |||||
| { | |||||
| case QModbusDevice::ReadError: | |||||
| return QStringLiteral("读取 PLC 数据失败"); | |||||
| case QModbusDevice::WriteError: | |||||
| return QStringLiteral("写入 PLC 数据失败"); | |||||
| case QModbusDevice::ConnectionError: | |||||
| return QStringLiteral("PLC 串口连接失败"); | |||||
| case QModbusDevice::ConfigurationError: | |||||
| return QStringLiteral("PLC 串口参数配置错误"); | |||||
| case QModbusDevice::TimeoutError: | |||||
| return QStringLiteral("PLC 通信超时,请检查站号、串口参数和 RS-485 接线"); | |||||
| case QModbusDevice::ProtocolError: | |||||
| return QStringLiteral("PLC 返回了无效或异常的 Modbus 响应"); | |||||
| case QModbusDevice::ReplyAbortedError: | |||||
| return QStringLiteral("PLC 通信请求已取消"); | |||||
| case QModbusDevice::UnknownError: | |||||
| return QStringLiteral("PLC 通信发生未知错误"); | |||||
| case QModbusDevice::NoError: | |||||
| default: | |||||
| return QStringLiteral("PLC 通信失败"); | |||||
| } | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| PlcCommunicationService::PlcCommunicationService( | PlcCommunicationService::PlcCommunicationService( | ||||
| @@ -79,18 +55,26 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| { | { | ||||
| if (device_state == QModbusDevice::ConnectedState) | if (device_state == QModbusDevice::ConnectedState) | ||||
| { | { | ||||
| serial_session_opened_ = true; | |||||
| setState(PlcConnectionState::Connected); | setState(PlcConnectionState::Connected); | ||||
| poll_timer_.start(configuration_.pollIntervalMs); | poll_timer_.start(configuration_.pollIntervalMs); | ||||
| pollNextBlock(); | pollNextBlock(); | ||||
| } | } | ||||
| else if (device_state == QModbusDevice::ConnectingState) | else if (device_state == QModbusDevice::ConnectingState) | ||||
| { | { | ||||
| serial_session_opened_ = false; | |||||
| setState(PlcConnectionState::Connecting); | setState(PlcConnectionState::Connecting); | ||||
| } | } | ||||
| else if (device_state == QModbusDevice::UnconnectedState | |||||
| && state_ != PlcConnectionState::Faulted) | |||||
| else if (device_state == QModbusDevice::UnconnectedState) | |||||
| { | { | ||||
| setState(PlcConnectionState::Disconnected); | |||||
| if (disconnecting_) | |||||
| { | |||||
| serial_session_opened_ = false; | |||||
| } | |||||
| if (!disconnecting_ && state_ != PlcConnectionState::Faulted) | |||||
| { | |||||
| setState(PlcConnectionState::Disconnected); | |||||
| } | |||||
| } | } | ||||
| }); | }); | ||||
| connect(master_.get(), &QModbusClient::errorOccurred, | connect(master_.get(), &QModbusClient::errorOccurred, | ||||
| @@ -99,7 +83,7 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| { | { | ||||
| if (error != QModbusDevice::NoError) | if (error != QModbusDevice::NoError) | ||||
| { | { | ||||
| setError(modbusErrorText(error)); | |||||
| handleModbusError(error); | |||||
| } | } | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -119,6 +103,11 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||||
| return {false, "PLC 连接已经启动,请先断开当前连接"}; | return {false, "PLC 连接已经启动,请先断开当前连接"}; | ||||
| } | } | ||||
| configuration_ = configuration; | configuration_ = configuration; | ||||
| ++connection_generation_; | |||||
| disconnecting_ = false; | |||||
| serial_session_opened_ = false; | |||||
| received_valid_response_ = false; | |||||
| last_error_type_ = PlcCommunicationError::None; | |||||
| last_error_.clear(); | last_error_.clear(); | ||||
| master_->setConnectionParameter( | master_->setConnectionParameter( | ||||
| QModbusDevice::SerialPortNameParameter, | QModbusDevice::SerialPortNameParameter, | ||||
| @@ -137,17 +126,14 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||||
| master_->setTimeout(configuration.responseTimeoutMs); | master_->setTimeout(configuration.responseTimeoutMs); | ||||
| master_->setNumberOfRetries(configuration.retries); | master_->setNumberOfRetries(configuration.retries); | ||||
| repository_.invalidate(); | repository_.invalidate(); | ||||
| initial_read_completed_ = false; | |||||
| emit initialReadCompletedChanged(false); | |||||
| if (initial_read_changed_callback_) | |||||
| { | |||||
| initial_read_changed_callback_(false); | |||||
| } | |||||
| updateInitialReadCompleted(false, true); | |||||
| rebuildPollBlocks(); | rebuildPollBlocks(); | ||||
| setState(PlcConnectionState::Connecting); | setState(PlcConnectionState::Connecting); | ||||
| if (!master_->connectDevice()) | if (!master_->connectDevice()) | ||||
| { | { | ||||
| setError(modbusErrorText(master_->error())); | |||||
| const QModbusDevice::Error error = master_->error() == QModbusDevice::NoError | |||||
| ? QModbusDevice::ConnectionError : master_->error(); | |||||
| handleModbusError(error); | |||||
| return {false, last_error_}; | return {false, last_error_}; | ||||
| } | } | ||||
| return {true, {}}; | return {true, {}}; | ||||
| @@ -155,16 +141,19 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( | |||||
| void PlcCommunicationService::disconnectDevice() | void PlcCommunicationService::disconnectDevice() | ||||
| { | { | ||||
| ++connection_generation_; | |||||
| disconnecting_ = true; | |||||
| poll_timer_.stop(); | poll_timer_.stop(); | ||||
| master_->disconnectDevice(); | master_->disconnectDevice(); | ||||
| pending_reply_ = nullptr; | |||||
| poll_update_pending_ = false; | |||||
| pending_poll_addresses_.clear(); | |||||
| serial_session_opened_ = false; | |||||
| received_valid_response_ = false; | |||||
| repository_.invalidate(); | repository_.invalidate(); | ||||
| initial_read_completed_ = false; | |||||
| emit initialReadCompletedChanged(false); | |||||
| if (initial_read_changed_callback_) | |||||
| { | |||||
| initial_read_changed_callback_(false); | |||||
| } | |||||
| updateInitialReadCompleted(false, true); | |||||
| setState(PlcConnectionState::Disconnected); | setState(PlcConnectionState::Disconnected); | ||||
| disconnecting_ = false; | |||||
| } | } | ||||
| void PlcCommunicationService::setPollAddresses( | void PlcCommunicationService::setPollAddresses( | ||||
| @@ -189,6 +178,11 @@ bool PlcCommunicationService::initialReadCompleted() const | |||||
| return initial_read_completed_; | return initial_read_completed_; | ||||
| } | } | ||||
| PlcCommunicationError PlcCommunicationService::lastErrorType() const | |||||
| { | |||||
| return last_error_type_; | |||||
| } | |||||
| const std::string &PlcCommunicationService::lastError() const | const std::string &PlcCommunicationService::lastError() const | ||||
| { | { | ||||
| return last_error_; | return last_error_; | ||||
| @@ -291,14 +285,20 @@ void PlcCommunicationService::pollNextBlock() | |||||
| QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress); | QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress); | ||||
| if (reply == nullptr) | if (reply == nullptr) | ||||
| { | { | ||||
| setError(modbusErrorText(master_->error())); | |||||
| handleModbusError(master_->error()); | |||||
| return; | return; | ||||
| } | } | ||||
| pending_reply_ = reply; | pending_reply_ = reply; | ||||
| const std::uint64_t generation = connection_generation_; | |||||
| connect(reply, &QModbusReply::finished, | connect(reply, &QModbusReply::finished, | ||||
| this, | this, | ||||
| [this, reply, block, block_index] | |||||
| [this, reply, block, block_index, generation] | |||||
| { | { | ||||
| if (generation != connection_generation_) | |||||
| { | |||||
| reply->deleteLater(); | |||||
| return; | |||||
| } | |||||
| handleReadFinished(reply, block); | handleReadFinished(reply, block); | ||||
| if (state_ == PlcConnectionState::Connected | if (state_ == PlcConnectionState::Connected | ||||
| && reply->error() == QModbusDevice::NoError | && reply->error() == QModbusDevice::NoError | ||||
| @@ -312,12 +312,7 @@ void PlcCommunicationService::pollNextBlock() | |||||
| [](bool read) { return read; }); | [](bool read) { return read; }); | ||||
| if (completed && !initial_read_completed_) | if (completed && !initial_read_completed_) | ||||
| { | { | ||||
| initial_read_completed_ = true; | |||||
| emit initialReadCompletedChanged(true); | |||||
| if (initial_read_changed_callback_) | |||||
| { | |||||
| initial_read_changed_callback_(true); | |||||
| } | |||||
| updateInitialReadCompleted(true); | |||||
| } | } | ||||
| } | } | ||||
| if (pending_reply_ == reply) | if (pending_reply_ == reply) | ||||
| @@ -341,7 +336,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock | |||||
| } | } | ||||
| if (reply->error() != QModbusDevice::NoError) | if (reply->error() != QModbusDevice::NoError) | ||||
| { | { | ||||
| setError(modbusErrorText(reply->error())); | |||||
| handleModbusError(reply->error()); | |||||
| return; | return; | ||||
| } | } | ||||
| const QModbusDataUnit result = reply->result(); | const QModbusDataUnit result = reply->result(); | ||||
| @@ -357,6 +352,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock | |||||
| repository_.updateWord(address, static_cast<std::int16_t>(result.value(index))); | repository_.updateWord(address, static_cast<std::int16_t>(result.value(index))); | ||||
| } | } | ||||
| } | } | ||||
| received_valid_response_ = true; | |||||
| last_error_.clear(); | last_error_.clear(); | ||||
| emit cacheUpdated(); | emit cacheUpdated(); | ||||
| if (cache_updated_callback_) | if (cache_updated_callback_) | ||||
| @@ -377,16 +373,21 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite( | |||||
| QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | ||||
| if (reply == nullptr) | if (reply == nullptr) | ||||
| { | { | ||||
| setError(modbusErrorText(master_->error())); | |||||
| handleModbusError(master_->error()); | |||||
| return {false, RegisterError::WriteRejected}; | return {false, RegisterError::WriteRejected}; | ||||
| } | } | ||||
| connect(reply, &QModbusReply::finished, | connect(reply, &QModbusReply::finished, | ||||
| this, | this, | ||||
| [this, reply] | |||||
| [this, reply, generation = connection_generation_] | |||||
| { | { | ||||
| if (generation != connection_generation_) | |||||
| { | |||||
| reply->deleteLater(); | |||||
| return; | |||||
| } | |||||
| if (reply->error() != QModbusDevice::NoError) | if (reply->error() != QModbusDevice::NoError) | ||||
| { | { | ||||
| setError(modbusErrorText(reply->error())); | |||||
| handleModbusError(reply->error()); | |||||
| } | } | ||||
| reply->deleteLater(); | reply->deleteLater(); | ||||
| }); | }); | ||||
| @@ -405,22 +406,77 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite( | |||||
| QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); | ||||
| if (reply == nullptr) | if (reply == nullptr) | ||||
| { | { | ||||
| setError(modbusErrorText(master_->error())); | |||||
| handleModbusError(master_->error()); | |||||
| return {false, RegisterError::WriteRejected}; | return {false, RegisterError::WriteRejected}; | ||||
| } | } | ||||
| connect(reply, &QModbusReply::finished, | connect(reply, &QModbusReply::finished, | ||||
| this, | this, | ||||
| [this, reply] | |||||
| [this, reply, generation = connection_generation_] | |||||
| { | { | ||||
| if (generation != connection_generation_) | |||||
| { | |||||
| reply->deleteLater(); | |||||
| return; | |||||
| } | |||||
| if (reply->error() != QModbusDevice::NoError) | if (reply->error() != QModbusDevice::NoError) | ||||
| { | { | ||||
| setError(modbusErrorText(reply->error())); | |||||
| handleModbusError(reply->error()); | |||||
| } | } | ||||
| reply->deleteLater(); | reply->deleteLater(); | ||||
| }); | }); | ||||
| return {true, RegisterError::None}; | return {true, RegisterError::None}; | ||||
| } | } | ||||
| bool PlcCommunicationService::configuredPortAvailable() const | |||||
| { | |||||
| const QString configured_port = QString::fromStdString( | |||||
| configuration_.portName).trimmed(); | |||||
| if (configured_port.isEmpty()) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| const QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts(); | |||||
| return std::any_of( | |||||
| ports.cbegin(), ports.cend(), | |||||
| [&configured_port](const QSerialPortInfo &port) | |||||
| { | |||||
| return port.portName().compare(configured_port, Qt::CaseInsensitive) == 0; | |||||
| }); | |||||
| } | |||||
| void PlcCommunicationService::updateInitialReadCompleted( | |||||
| bool completed, bool force_notification) | |||||
| { | |||||
| if (initial_read_completed_ == completed && !force_notification) | |||||
| { | |||||
| return; | |||||
| } | |||||
| initial_read_completed_ = completed; | |||||
| emit initialReadCompletedChanged(completed); | |||||
| if (initial_read_changed_callback_) | |||||
| { | |||||
| initial_read_changed_callback_(completed); | |||||
| } | |||||
| } | |||||
| void PlcCommunicationService::handleModbusError(QModbusDevice::Error error) | |||||
| { | |||||
| if (disconnecting_ | |||||
| || (error == QModbusDevice::ReplyAbortedError | |||||
| && state_ == PlcConnectionState::Disconnected) | |||||
| || state_ == PlcConnectionState::Faulted) | |||||
| { | |||||
| return; | |||||
| } | |||||
| const PlcCommunicationErrorContext context{ | |||||
| QString::fromStdString(configuration_.portName), | |||||
| master_->errorString(), | |||||
| serial_session_opened_, | |||||
| configuredPortAvailable(), | |||||
| received_valid_response_}; | |||||
| setError(classifyPlcCommunicationError(error, context)); | |||||
| } | |||||
| void PlcCommunicationService::setState(PlcConnectionState state) | void PlcCommunicationService::setState(PlcConnectionState state) | ||||
| { | { | ||||
| if (state_ == state) | if (state_ == state) | ||||
| @@ -435,12 +491,14 @@ void PlcCommunicationService::setState(PlcConnectionState state) | |||||
| } | } | ||||
| } | } | ||||
| void PlcCommunicationService::setError(const QString &message) | |||||
| void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | |||||
| { | { | ||||
| last_error_ = toUtf8(message); | |||||
| last_error_type_ = failure.type; | |||||
| last_error_ = toUtf8(failure.message); | |||||
| poll_timer_.stop(); | poll_timer_.stop(); | ||||
| updateInitialReadCompleted(false); | |||||
| setState(PlcConnectionState::Faulted); | setState(PlcConnectionState::Faulted); | ||||
| emit communicationError(message); | |||||
| emit communicationError(failure.message); | |||||
| if (error_reported_callback_) | if (error_reported_callback_) | ||||
| { | { | ||||
| error_reported_callback_(last_error_); | error_reported_callback_(last_error_); | ||||
| @@ -3,9 +3,11 @@ | |||||
| #include "services/plc_communication_gateway.h" | #include "services/plc_communication_gateway.h" | ||||
| #include "domain/register_repository.h" | #include "domain/register_repository.h" | ||||
| #include <QModbusDevice> | |||||
| #include <QObject> | #include <QObject> | ||||
| #include <QTimer> | #include <QTimer> | ||||
| #include <cstdint> | |||||
| #include <memory> | #include <memory> | ||||
| #include <string> | #include <string> | ||||
| #include <vector> | #include <vector> | ||||
| @@ -13,6 +15,7 @@ | |||||
| class PlcRegisterRepository; | class PlcRegisterRepository; | ||||
| class QModbusReply; | class QModbusReply; | ||||
| class QModbusRtuSerialMaster; | class QModbusRtuSerialMaster; | ||||
| struct PlcCommunicationFailure; | |||||
| class PlcCommunicationService final : public QObject, public PlcCommunicationGateway | class PlcCommunicationService final : public QObject, public PlcCommunicationGateway | ||||
| { | { | ||||
| @@ -31,6 +34,7 @@ public: | |||||
| PlcConnectionState state() const override; | PlcConnectionState state() const override; | ||||
| bool initialReadCompleted() const override; | bool initialReadCompleted() const override; | ||||
| PlcCommunicationError lastErrorType() const override; | |||||
| const std::string &lastError() const override; | const std::string &lastError() const override; | ||||
| const PlcSerialConfiguration &configuration() const; | const PlcSerialConfiguration &configuration() const; | ||||
| void setCallbacks( | void setCallbacks( | ||||
| @@ -60,8 +64,11 @@ private: | |||||
| RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); | RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); | ||||
| RegisterWriteResult sendWordWrite( | RegisterWriteResult sendWordWrite( | ||||
| const RegisterAddress &address, std::int16_t value); | const RegisterAddress &address, std::int16_t value); | ||||
| bool configuredPortAvailable() const; | |||||
| void updateInitialReadCompleted(bool completed, bool force_notification = false); | |||||
| void handleModbusError(QModbusDevice::Error error); | |||||
| void setState(PlcConnectionState state); | void setState(PlcConnectionState state); | ||||
| void setError(const QString &message); | |||||
| void setError(const PlcCommunicationFailure &failure); | |||||
| PlcRegisterRepository &repository_; | PlcRegisterRepository &repository_; | ||||
| std::unique_ptr<QModbusRtuSerialMaster> master_; | std::unique_ptr<QModbusRtuSerialMaster> master_; | ||||
| @@ -73,9 +80,14 @@ private: | |||||
| std::size_t next_poll_block_ = 0; | std::size_t next_poll_block_ = 0; | ||||
| QModbusReply *pending_reply_ = nullptr; | QModbusReply *pending_reply_ = nullptr; | ||||
| bool poll_update_pending_ = false; | 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; | PlcConnectionState state_ = PlcConnectionState::Disconnected; | ||||
| bool initial_read_completed_ = false; | bool initial_read_completed_ = false; | ||||
| std::vector<bool> initial_blocks_read_; | std::vector<bool> initial_blocks_read_; | ||||
| PlcCommunicationError last_error_type_ = PlcCommunicationError::None; | |||||
| std::string last_error_; | std::string last_error_; | ||||
| std::function<void()> state_changed_callback_; | std::function<void()> state_changed_callback_; | ||||
| std::function<void(bool)> initial_read_changed_callback_; | std::function<void(bool)> initial_read_changed_callback_; | ||||
| @@ -27,6 +27,22 @@ enum class PlcConnectionState | |||||
| Faulted | Faulted | ||||
| }; | }; | ||||
| enum class PlcCommunicationError | |||||
| { | |||||
| None, | |||||
| SerialPortOpenFailed, | |||||
| PlcNotResponding, | |||||
| UsbSerialAdapterRemoved, | |||||
| SerialConnectionLost, | |||||
| CommunicationTimeout, | |||||
| ProtocolError, | |||||
| ReadFailed, | |||||
| WriteFailed, | |||||
| ConfigurationError, | |||||
| RequestAborted, | |||||
| Unknown | |||||
| }; | |||||
| struct PlcCommunicationResult | struct PlcCommunicationResult | ||||
| { | { | ||||
| bool succeeded = false; | bool succeeded = false; | ||||
| @@ -45,6 +61,7 @@ public: | |||||
| const std::vector<RegisterAddress> &addresses) = 0; | const std::vector<RegisterAddress> &addresses) = 0; | ||||
| virtual PlcConnectionState state() const = 0; | virtual PlcConnectionState state() const = 0; | ||||
| virtual bool initialReadCompleted() const = 0; | virtual bool initialReadCompleted() const = 0; | ||||
| virtual PlcCommunicationError lastErrorType() const = 0; | |||||
| virtual const std::string &lastError() const = 0; | virtual const std::string &lastError() const = 0; | ||||
| virtual void setCallbacks( | virtual void setCallbacks( | ||||
| std::function<void()> state_changed, | std::function<void()> state_changed, | ||||
| @@ -87,7 +87,7 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() | |||||
| ModeTransitionResult RuntimeModeService::enterOnlineRunning() | ModeTransitionResult RuntimeModeService::enterOnlineRunning() | ||||
| { | { | ||||
| const ModeTransitionResult result = state_.enterOnlineRunning( | const ModeTransitionResult result = state_.enterOnlineRunning( | ||||
| initial_plc_read_completed_); | |||||
| initialPlcReadCompleted()); | |||||
| if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr) | if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr) | ||||
| { | { | ||||
| active_repository_->use(*plc_repository_); | active_repository_->use(*plc_repository_); | ||||
| @@ -103,7 +103,10 @@ void RuntimeModeService::setInitialPlcReadCompleted(bool completed) | |||||
| bool RuntimeModeService::initialPlcReadCompleted() const | bool RuntimeModeService::initialPlcReadCompleted() const | ||||
| { | { | ||||
| return initial_plc_read_completed_; | |||||
| return initial_plc_read_completed_ | |||||
| && plc_gateway_ != nullptr | |||||
| && plc_gateway_->state() == PlcConnectionState::Connected | |||||
| && plc_gateway_->initialReadCompleted(); | |||||
| } | } | ||||
| SimulationState RuntimeModeService::simulationState() const | SimulationState RuntimeModeService::simulationState() const | ||||
| @@ -139,13 +142,17 @@ void RuntimeModeService::configurePlc( | |||||
| gateway.setCallbacks( | gateway.setCallbacks( | ||||
| [this] | [this] | ||||
| { | { | ||||
| if (plc_gateway_ != nullptr | |||||
| && plc_gateway_->state() == PlcConnectionState::Disconnected) | |||||
| if (plc_gateway_ != nullptr) | |||||
| { | { | ||||
| setInitialPlcReadCompleted(false); | |||||
| if (state_.mode() == ApplicationMode::OnlineRunning) | |||||
| const PlcConnectionState plc_state = plc_gateway_->state(); | |||||
| if (plc_state == PlcConnectionState::Disconnected | |||||
| || plc_state == PlcConnectionState::Faulted) | |||||
| { | { | ||||
| enterEditing(); | |||||
| setInitialPlcReadCompleted(false); | |||||
| if (state_.mode() == ApplicationMode::OnlineRunning) | |||||
| { | |||||
| enterEditing(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| if (plc_status_changed_callback_) | if (plc_status_changed_callback_) | ||||
| @@ -178,6 +185,10 @@ PlcCommunicationResult RuntimeModeService::connectPlc( | |||||
| { | { | ||||
| return {false, "PLC 通信服务尚未配置"}; | return {false, "PLC 通信服务尚未配置"}; | ||||
| } | } | ||||
| if (plc_gateway_->state() == PlcConnectionState::Faulted) | |||||
| { | |||||
| plc_gateway_->disconnectDevice(); | |||||
| } | |||||
| refreshPlcPollAddresses(); | refreshPlcPollAddresses(); | ||||
| setInitialPlcReadCompleted(false); | setInitialPlcReadCompleted(false); | ||||
| return plc_gateway_->connectDevice(configuration); | return plc_gateway_->connectDevice(configuration); | ||||
| @@ -1205,8 +1205,16 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| policy.usesVirtualRegisters ? tr("数据源:虚拟 M/D") | policy.usesVirtualRegisters ? tr("数据源:虚拟 M/D") | ||||
| : policy.usesPlcRegisters ? tr("数据源:PLC 缓存") : tr("数据源:未启用")); | : policy.usesPlcRegisters ? tr("数据源:PLC 缓存") : tr("数据源:未启用")); | ||||
| const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); | const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); | ||||
| const bool plc_configuration_available = plc_state == PlcConnectionState::Disconnected | |||||
| || plc_state == PlcConnectionState::Faulted; | |||||
| ui_->configurePlcAction->setEnabled( | ui_->configurePlcAction->setEnabled( | ||||
| policy.allowsProjectEditing && plc_state == PlcConnectionState::Disconnected); | |||||
| policy.allowsProjectEditing && plc_configuration_available); | |||||
| ui_->configurePlcAction->setText( | |||||
| plc_state == PlcConnectionState::Faulted ? tr("PLC 重新配置") : tr("PLC 配置")); | |||||
| ui_->configurePlcAction->setToolTip( | |||||
| plc_state == PlcConnectionState::Faulted | |||||
| ? tr("清理故障会话后重新配置并连接真实 PLC") | |||||
| : tr("配置参数并连接真实 PLC")); | |||||
| ui_->disconnectPlcAction->setEnabled(plc_state != PlcConnectionState::Disconnected); | ui_->disconnectPlcAction->setEnabled(plc_state != PlcConnectionState::Disconnected); | ||||
| if (plc_state == PlcConnectionState::Connecting) | if (plc_state == PlcConnectionState::Connecting) | ||||
| { | { | ||||
| @@ -1239,15 +1247,16 @@ void MainWindow::updateSimulationUi(bool report_fault) | |||||
| == ApplicationMode::OfflineRunning; | == ApplicationMode::OfflineRunning; | ||||
| const bool online = runtime_mode_service_.mode() | const bool online = runtime_mode_service_.mode() | ||||
| == ApplicationMode::OnlineRunning; | == ApplicationMode::OnlineRunning; | ||||
| const bool plc_connected = runtime_mode_service_.plcConnectionState() | |||||
| == PlcConnectionState::Connected; | |||||
| const SimulationState state = runtime_mode_service_.simulationState(); | const SimulationState state = runtime_mode_service_.simulationState(); | ||||
| hmi_editor_widget_->setRuntimeWriteEnabled( | hmi_editor_widget_->setRuntimeWriteEnabled( | ||||
| online || (offline && state == SimulationState::Running)); | |||||
| (online && plc_connected) | |||||
| || (offline && state == SimulationState::Running)); | |||||
| if (runtime_monitor_widget_ != nullptr) | if (runtime_monitor_widget_ != nullptr) | ||||
| { | { | ||||
| runtime_monitor_widget_->setHmiWriteEnabled( | runtime_monitor_widget_->setHmiWriteEnabled( | ||||
| (online | |||||
| && runtime_mode_service_.plcConnectionState() | |||||
| == PlcConnectionState::Connected) | |||||
| (online && plc_connected) | |||||
| || (offline && state == SimulationState::Running)); | || (offline && state == SimulationState::Running)); | ||||
| runtime_monitor_widget_->refreshValues( | runtime_monitor_widget_->refreshValues( | ||||
| runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); | runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); | ||||
| @@ -76,6 +76,10 @@ public: | |||||
| void setPollAddresses(const std::vector<RegisterAddress> &) override {} | void setPollAddresses(const std::vector<RegisterAddress> &) override {} | ||||
| PlcConnectionState state() const override { return connection_state; } | PlcConnectionState state() const override { return connection_state; } | ||||
| bool initialReadCompleted() const override { return false; } | bool initialReadCompleted() const override { return false; } | ||||
| PlcCommunicationError lastErrorType() const override | |||||
| { | |||||
| return PlcCommunicationError::None; | |||||
| } | |||||
| const std::string &lastError() const override { return last_error; } | const std::string &lastError() const override { return last_error; } | ||||
| void setCallbacks( | void setCallbacks( | ||||
| @@ -123,6 +127,15 @@ public: | |||||
| void disconnectDevice() override | void disconnectDevice() override | ||||
| { | { | ||||
| connection_state = PlcConnectionState::Disconnected; | connection_state = PlcConnectionState::Disconnected; | ||||
| initial_read = false; | |||||
| if (initial_read_changed) | |||||
| { | |||||
| initial_read_changed(false); | |||||
| } | |||||
| if (state_changed) | |||||
| { | |||||
| state_changed(); | |||||
| } | |||||
| } | } | ||||
| void setPollAddresses(const std::vector<RegisterAddress> &addresses) override | void setPollAddresses(const std::vector<RegisterAddress> &addresses) override | ||||
| @@ -132,6 +145,7 @@ public: | |||||
| PlcConnectionState state() const override { return connection_state; } | PlcConnectionState state() const override { return connection_state; } | ||||
| bool initialReadCompleted() const override { return initial_read; } | bool initialReadCompleted() const override { return initial_read; } | ||||
| PlcCommunicationError lastErrorType() const override { return last_error_type; } | |||||
| const std::string &lastError() const override { return last_error; } | const std::string &lastError() const override { return last_error; } | ||||
| void setCallbacks( | void setCallbacks( | ||||
| @@ -155,8 +169,30 @@ public: | |||||
| } | } | ||||
| } | } | ||||
| void failCommunication( | |||||
| PlcCommunicationError error_type, const std::string &message) | |||||
| { | |||||
| initial_read = false; | |||||
| last_error_type = error_type; | |||||
| last_error = message; | |||||
| connection_state = PlcConnectionState::Faulted; | |||||
| if (initial_read_changed) | |||||
| { | |||||
| initial_read_changed(false); | |||||
| } | |||||
| if (state_changed) | |||||
| { | |||||
| state_changed(); | |||||
| } | |||||
| if (error_reported) | |||||
| { | |||||
| error_reported(last_error); | |||||
| } | |||||
| } | |||||
| PlcConnectionState connection_state = PlcConnectionState::Disconnected; | PlcConnectionState connection_state = PlcConnectionState::Disconnected; | ||||
| bool initial_read = false; | bool initial_read = false; | ||||
| PlcCommunicationError last_error_type = PlcCommunicationError::None; | |||||
| std::string last_error; | std::string last_error; | ||||
| std::vector<RegisterAddress> poll_addresses; | std::vector<RegisterAddress> poll_addresses; | ||||
| std::function<void()> state_changed; | std::function<void()> state_changed; | ||||
| @@ -644,6 +680,30 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| gateway.poll_addresses.end(), | gateway.poll_addresses.end(), | ||||
| RegisterAddress{RegisterArea::D, 9}) != gateway.poll_addresses.end(), | RegisterAddress{RegisterArea::D, 9}) != gateway.poll_addresses.end(), | ||||
| "online free monitor addresses must join the active PLC poll set immediately"); | "online free monitor addresses must join the active PLC poll set immediately"); | ||||
| QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction"); | |||||
| QAction *disconnect_action = requiredChild<QAction>(window, "disconnectPlcAction"); | |||||
| QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | |||||
| gateway.failCommunication( | |||||
| PlcCommunicationError::CommunicationTimeout, | |||||
| "PLC 通信超时,本地串口 COM9 仍处于打开状态;请检查 PLC 供电和 RS-485 接线"); | |||||
| QApplication::processEvents(); | |||||
| require(mode_service.mode() == ApplicationMode::Editing, | |||||
| "a communication fault must return the UI from online running to editing"); | |||||
| require(configure_action->isEnabled() | |||||
| && configure_action->text() == QStringLiteral("PLC 重新配置"), | |||||
| "faulted PLC state must expose direct reconfiguration"); | |||||
| require(disconnect_action->isEnabled(), | |||||
| "faulted PLC state must still allow explicit serial session cleanup"); | |||||
| require(output->count() > 0 | |||||
| && output->item(output->count() - 1)->text().contains( | |||||
| QStringLiteral("PLC 通信超时")), | |||||
| "communication fault details must remain in the output log"); | |||||
| online_action->trigger(); | |||||
| require(mode_service.mode() == ApplicationMode::Editing, | |||||
| "faulted PLC state must not re-enter online running without a fresh read"); | |||||
| } | } | ||||
| } // namespace | } // namespace | ||||
| @@ -1,5 +1,6 @@ | |||||
| #include "domain/active_register_repository.h" | #include "domain/active_register_repository.h" | ||||
| #include "domain/project_storage.h" | #include "domain/project_storage.h" | ||||
| #include "infrastructure/plc_communication_error_classifier.h" | |||||
| #include "infrastructure/plc_register_repository.h" | #include "infrastructure/plc_register_repository.h" | ||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| #include "services/plc_communication_gateway.h" | #include "services/plc_communication_gateway.h" | ||||
| @@ -34,6 +35,9 @@ public: | |||||
| const PlcSerialConfiguration &configuration) override | const PlcSerialConfiguration &configuration) override | ||||
| { | { | ||||
| last_configuration = configuration; | last_configuration = configuration; | ||||
| last_error_type = PlcCommunicationError::None; | |||||
| last_error.clear(); | |||||
| initial_read = false; | |||||
| connection_state = PlcConnectionState::Connected; | connection_state = PlcConnectionState::Connected; | ||||
| if (state_changed) | if (state_changed) | ||||
| { | { | ||||
| @@ -44,6 +48,7 @@ public: | |||||
| void disconnectDevice() override | void disconnectDevice() override | ||||
| { | { | ||||
| ++disconnect_count; | |||||
| connection_state = PlcConnectionState::Disconnected; | connection_state = PlcConnectionState::Disconnected; | ||||
| initial_read = false; | initial_read = false; | ||||
| if (initial_read_changed) | if (initial_read_changed) | ||||
| @@ -63,6 +68,7 @@ public: | |||||
| PlcConnectionState state() const override { return connection_state; } | PlcConnectionState state() const override { return connection_state; } | ||||
| bool initialReadCompleted() const override { return initial_read; } | bool initialReadCompleted() const override { return initial_read; } | ||||
| PlcCommunicationError lastErrorType() const override { return last_error_type; } | |||||
| const std::string &lastError() const override { return last_error; } | const std::string &lastError() const override { return last_error; } | ||||
| void setCallbacks( | void setCallbacks( | ||||
| @@ -86,8 +92,31 @@ public: | |||||
| } | } | ||||
| } | } | ||||
| void failCommunication( | |||||
| PlcCommunicationError error_type, const std::string &message) | |||||
| { | |||||
| initial_read = false; | |||||
| last_error_type = error_type; | |||||
| last_error = message; | |||||
| connection_state = PlcConnectionState::Faulted; | |||||
| if (initial_read_changed) | |||||
| { | |||||
| initial_read_changed(false); | |||||
| } | |||||
| if (state_changed) | |||||
| { | |||||
| state_changed(); | |||||
| } | |||||
| if (error_reported) | |||||
| { | |||||
| error_reported(last_error); | |||||
| } | |||||
| } | |||||
| PlcConnectionState connection_state = PlcConnectionState::Disconnected; | PlcConnectionState connection_state = PlcConnectionState::Disconnected; | ||||
| bool initial_read = false; | bool initial_read = false; | ||||
| int disconnect_count = 0; | |||||
| PlcCommunicationError last_error_type = PlcCommunicationError::None; | |||||
| std::string last_error; | std::string last_error; | ||||
| PlcSerialConfiguration last_configuration; | PlcSerialConfiguration last_configuration; | ||||
| std::vector<RegisterAddress> poll_addresses; | std::vector<RegisterAddress> poll_addresses; | ||||
| @@ -209,6 +238,82 @@ void testRuntimeRepositorySwitchingAndDisconnect() | |||||
| "editing after disconnect must switch back to the virtual repository"); | "editing after disconnect must switch back to the virtual repository"); | ||||
| } | } | ||||
| void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| VirtualRegisterRepository virtual_repository; | |||||
| PlcRegisterRepository plc_repository; | |||||
| ActiveRegisterRepository active_repository(virtual_repository); | |||||
| OfflineSimulationService simulation_service(virtual_repository); | |||||
| FakePlcGateway gateway; | |||||
| RuntimeModeService service(project_service, simulation_service); | |||||
| service.configurePlc( | |||||
| gateway, active_repository, virtual_repository, plc_repository); | |||||
| require(service.connectPlc( | |||||
| {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded, | |||||
| "PLC connection must start before testing a communication fault"); | |||||
| gateway.completeInitialRead(); | |||||
| require(service.enterOnlineRunning().succeeded, | |||||
| "completed initial read must allow online running before a fault"); | |||||
| gateway.failCommunication( | |||||
| PlcCommunicationError::CommunicationTimeout, | |||||
| "PLC communication timed out while the serial port remained open"); | |||||
| require(service.mode() == ApplicationMode::Editing, | |||||
| "a PLC communication fault must return online running to editing"); | |||||
| require(!service.initialPlcReadCompleted(), | |||||
| "a PLC communication fault must revoke initial read readiness"); | |||||
| require(service.enterOnlineRunning().error | |||||
| == ModeTransitionError::InitialPlcReadRequired, | |||||
| "faulted PLC state must not reuse readiness from the previous connection"); | |||||
| const int disconnect_count = gateway.disconnect_count; | |||||
| require(service.connectPlc( | |||||
| {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded, | |||||
| "faulted PLC state must support direct reconnect"); | |||||
| require(gateway.disconnect_count == disconnect_count + 1, | |||||
| "reconnecting from a fault must clean the previous serial session first"); | |||||
| } | |||||
| void testPlcCommunicationErrorClassification() | |||||
| { | |||||
| PlcCommunicationErrorContext context; | |||||
| context.portName = QStringLiteral("COM3"); | |||||
| PlcCommunicationFailure failure = classifyPlcCommunicationError( | |||||
| QModbusDevice::ConnectionError, context); | |||||
| require(failure.type == PlcCommunicationError::SerialPortOpenFailed | |||||
| && failure.message.contains(QStringLiteral("未能打开")), | |||||
| "connection failure before opening the port must be classified explicitly"); | |||||
| context.serialSessionOpened = true; | |||||
| context.portAvailable = true; | |||||
| failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context); | |||||
| require(failure.type == PlcCommunicationError::PlcNotResponding | |||||
| && failure.message.contains(QStringLiteral("PLC 未响应")), | |||||
| "initial timeout with an open serial port must report a non-responsive PLC"); | |||||
| context.receivedValidResponse = true; | |||||
| failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context); | |||||
| require(failure.type == PlcCommunicationError::CommunicationTimeout | |||||
| && failure.message.contains(QStringLiteral("仍处于打开状态")), | |||||
| "timeout after valid traffic must report an open local serial session"); | |||||
| context.portAvailable = false; | |||||
| failure = classifyPlcCommunicationError(QModbusDevice::ConnectionError, context); | |||||
| require(failure.type == PlcCommunicationError::UsbSerialAdapterRemoved | |||||
| && failure.message.contains(QStringLiteral("已从电脑移除")), | |||||
| "a missing port after connection must report USB serial adapter removal"); | |||||
| context.portAvailable = true; | |||||
| failure = classifyPlcCommunicationError(QModbusDevice::ProtocolError, context); | |||||
| require(failure.type == PlcCommunicationError::ProtocolError | |||||
| && failure.message.contains(QStringLiteral("Modbus 协议异常")), | |||||
| "protocol errors must remain distinct from timeouts and disconnections"); | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| int main() | int main() | ||||
| @@ -217,6 +322,8 @@ int main() | |||||
| { | { | ||||
| testPlcCacheAndWriteForwarding(); | testPlcCacheAndWriteForwarding(); | ||||
| testRuntimeRepositorySwitchingAndDisconnect(); | testRuntimeRepositorySwitchingAndDisconnect(); | ||||
| testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect(); | |||||
| testPlcCommunicationErrorClassification(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||
| @@ -1,4 +1,4 @@ | |||||
| QT += core | |||||
| QT += core serialbus | |||||
| TEMPLATE = app | TEMPLATE = app | ||||
| TARGET = plc_runtime_tests | TARGET = plc_runtime_tests | ||||
| @@ -22,6 +22,7 @@ SOURCES += \ | |||||
| ../src/services/software_logic_executor.cpp \ | ../src/services/software_logic_executor.cpp \ | ||||
| ../src/services/offline_simulation_service.cpp \ | ../src/services/offline_simulation_service.cpp \ | ||||
| ../src/services/runtime_mode_service.cpp \ | ../src/services/runtime_mode_service.cpp \ | ||||
| ../src/infrastructure/plc_communication_error_classifier.cpp \ | |||||
| ../src/infrastructure/plc_register_repository.cpp | ../src/infrastructure/plc_register_repository.cpp | ||||
| HEADERS += \ | HEADERS += \ | ||||
| @@ -39,4 +40,5 @@ HEADERS += \ | |||||
| ../src/services/offline_simulation_service.h \ | ../src/services/offline_simulation_service.h \ | ||||
| ../src/services/runtime_mode_service.h \ | ../src/services/runtime_mode_service.h \ | ||||
| ../src/services/plc_communication_gateway.h \ | ../src/services/plc_communication_gateway.h \ | ||||
| ../src/infrastructure/plc_communication_error_classifier.h \ | |||||
| ../src/infrastructure/plc_register_repository.h | ../src/infrastructure/plc_register_repository.h | ||||
| @@ -1,12 +1,15 @@ | |||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "domain/active_register_repository.h" | |||||
| #include "domain/project_storage.h" | #include "domain/project_storage.h" | ||||
| #include "domain/register_repository.h" | #include "domain/register_repository.h" | ||||
| #include <functional> | |||||
| #include <iostream> | #include <iostream> | ||||
| #include <stdexcept> | #include <stdexcept> | ||||
| #include <string> | #include <string> | ||||
| #include <utility> | |||||
| namespace { | namespace { | ||||
| @@ -24,6 +27,65 @@ public: | |||||
| } | } | ||||
| }; | }; | ||||
| class ReadyPlcGateway final : public PlcCommunicationGateway | |||||
| { | |||||
| public: | |||||
| PlcCommunicationResult connectDevice(const PlcSerialConfiguration &) override | |||||
| { | |||||
| connection_state = PlcConnectionState::Connected; | |||||
| if (state_changed) | |||||
| { | |||||
| state_changed(); | |||||
| } | |||||
| return {true, {}}; | |||||
| } | |||||
| void disconnectDevice() override | |||||
| { | |||||
| connection_state = PlcConnectionState::Disconnected; | |||||
| initial_read = false; | |||||
| } | |||||
| void setPollAddresses(const std::vector<RegisterAddress> &) override {} | |||||
| PlcConnectionState state() const override { return connection_state; } | |||||
| bool initialReadCompleted() const override { return initial_read; } | |||||
| PlcCommunicationError lastErrorType() const override | |||||
| { | |||||
| return PlcCommunicationError::None; | |||||
| } | |||||
| const std::string &lastError() const override { return last_error; } | |||||
| void setCallbacks( | |||||
| std::function<void()> state_callback, | |||||
| std::function<void(bool)> initial_callback, | |||||
| std::function<void()> cache_callback, | |||||
| std::function<void(const std::string &)> error_callback) override | |||||
| { | |||||
| state_changed = std::move(state_callback); | |||||
| initial_read_changed = std::move(initial_callback); | |||||
| cache_updated = std::move(cache_callback); | |||||
| error_reported = std::move(error_callback); | |||||
| } | |||||
| void completeInitialRead() | |||||
| { | |||||
| initial_read = true; | |||||
| if (initial_read_changed) | |||||
| { | |||||
| initial_read_changed(true); | |||||
| } | |||||
| } | |||||
| private: | |||||
| PlcConnectionState connection_state = PlcConnectionState::Disconnected; | |||||
| bool initial_read = false; | |||||
| std::string last_error; | |||||
| std::function<void()> state_changed; | |||||
| std::function<void(bool)> initial_read_changed; | |||||
| std::function<void()> cache_updated; | |||||
| std::function<void(const std::string &)> error_reported; | |||||
| }; | |||||
| void require(bool condition, const std::string &message) | void require(bool condition, const std::string &message) | ||||
| { | { | ||||
| if (!condition) | if (!condition) | ||||
| @@ -37,9 +99,14 @@ void testModeTransitions() | |||||
| // 验证服务将 PLC 首读状态与领域模式切换规则正确组合 | // 验证服务将 PLC 首读状态与领域模式切换规则正确组合 | ||||
| TestProjectStorage storage; | TestProjectStorage storage; | ||||
| ProjectService project_service(storage); | ProjectService project_service(storage); | ||||
| VirtualRegisterRepository repository; | |||||
| OfflineSimulationService simulation_service(repository); | |||||
| VirtualRegisterRepository virtual_repository; | |||||
| VirtualRegisterRepository plc_repository; | |||||
| ActiveRegisterRepository active_repository(virtual_repository); | |||||
| OfflineSimulationService simulation_service(virtual_repository); | |||||
| ReadyPlcGateway gateway; | |||||
| RuntimeModeService service(project_service, simulation_service); | RuntimeModeService service(project_service, simulation_service); | ||||
| service.configurePlc( | |||||
| gateway, active_repository, virtual_repository, plc_repository); | |||||
| require(service.mode() == ApplicationMode::Editing, | require(service.mode() == ApplicationMode::Editing, | ||||
| "service must start in editing mode"); | "service must start in editing mode"); | ||||
| @@ -64,7 +131,10 @@ void testModeTransitions() | |||||
| == ModeTransitionError::InitialPlcReadRequired, | == ModeTransitionError::InitialPlcReadRequired, | ||||
| "online running must require an initial PLC read"); | "online running must require an initial PLC read"); | ||||
| service.setInitialPlcReadCompleted(true); | |||||
| require(service.connectPlc( | |||||
| {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded, | |||||
| "PLC connection must be established before its initial read can complete"); | |||||
| gateway.completeInitialRead(); | |||||
| require(service.initialPlcReadCompleted(), | require(service.initialPlcReadCompleted(), | ||||
| "service must retain the initial PLC read state"); | "service must retain the initial PLC read state"); | ||||
| require(service.enterOnlineRunning().succeeded, | require(service.enterOnlineRunning().succeeded, | ||||