diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index e345033..8986fa5 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -37,6 +37,7 @@ SOURCES += \ src/services/runtime_mode_service.cpp \ src/services/register_monitor_service.cpp \ src/infrastructure/plc_register_repository.cpp \ + src/infrastructure/plc_communication_error_classifier.cpp \ src/infrastructure/plc_communication_service.cpp \ src/infrastructure/json_project_storage.cpp \ src/ui/hmi_editor_widget.cpp \ @@ -66,6 +67,7 @@ HEADERS += \ src/services/register_monitor_service.h \ src/services/plc_communication_gateway.h \ src/infrastructure/plc_register_repository.h \ + src/infrastructure/plc_communication_error_classifier.h \ src/infrastructure/plc_communication_service.h \ src/infrastructure/json_project_storage.h \ src/ui/hmi_editor_widget.h \ diff --git a/app/src/infrastructure/plc_communication_error_classifier.cpp b/app/src/infrastructure/plc_communication_error_classifier.cpp new file mode 100644 index 0000000..227d6d3 --- /dev/null +++ b/app/src/infrastructure/plc_communication_error_classifier.cpp @@ -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 通信失败")}; + } + } +} diff --git a/app/src/infrastructure/plc_communication_error_classifier.h b/app/src/infrastructure/plc_communication_error_classifier.h new file mode 100644 index 0000000..d99c634 --- /dev/null +++ b/app/src/infrastructure/plc_communication_error_classifier.h @@ -0,0 +1,25 @@ +#pragma once + +#include "services/plc_communication_gateway.h" + +#include +#include + +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); diff --git a/app/src/infrastructure/plc_communication_service.cpp b/app/src/infrastructure/plc_communication_service.cpp index 30c68a3..eb664fb 100644 --- a/app/src/infrastructure/plc_communication_service.cpp +++ b/app/src/infrastructure/plc_communication_service.cpp @@ -1,5 +1,6 @@ #include "plc_communication_service.h" +#include "plc_communication_error_classifier.h" #include "plc_register_repository.h" #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include @@ -28,32 +30,6 @@ QModbusDataUnit::RegisterType registerType(RegisterArea area) ? 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 PlcCommunicationService::PlcCommunicationService( @@ -79,18 +55,26 @@ PlcCommunicationService::PlcCommunicationService( { if (device_state == QModbusDevice::ConnectedState) { + serial_session_opened_ = true; setState(PlcConnectionState::Connected); poll_timer_.start(configuration_.pollIntervalMs); pollNextBlock(); } else if (device_state == QModbusDevice::ConnectingState) { + serial_session_opened_ = false; 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, @@ -99,7 +83,7 @@ PlcCommunicationService::PlcCommunicationService( { if (error != QModbusDevice::NoError) { - setError(modbusErrorText(error)); + handleModbusError(error); } }); } @@ -119,6 +103,11 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( return {false, "PLC 连接已经启动,请先断开当前连接"}; } configuration_ = configuration; + ++connection_generation_; + disconnecting_ = false; + serial_session_opened_ = false; + received_valid_response_ = false; + last_error_type_ = PlcCommunicationError::None; last_error_.clear(); master_->setConnectionParameter( QModbusDevice::SerialPortNameParameter, @@ -137,17 +126,14 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( master_->setTimeout(configuration.responseTimeoutMs); master_->setNumberOfRetries(configuration.retries); repository_.invalidate(); - initial_read_completed_ = false; - emit initialReadCompletedChanged(false); - if (initial_read_changed_callback_) - { - initial_read_changed_callback_(false); - } + updateInitialReadCompleted(false, true); rebuildPollBlocks(); setState(PlcConnectionState::Connecting); 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 {true, {}}; @@ -155,16 +141,19 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( void PlcCommunicationService::disconnectDevice() { + ++connection_generation_; + disconnecting_ = true; poll_timer_.stop(); master_->disconnectDevice(); + pending_reply_ = nullptr; + poll_update_pending_ = false; + pending_poll_addresses_.clear(); + serial_session_opened_ = false; + received_valid_response_ = false; 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); + disconnecting_ = false; } void PlcCommunicationService::setPollAddresses( @@ -189,6 +178,11 @@ bool PlcCommunicationService::initialReadCompleted() const return initial_read_completed_; } +PlcCommunicationError PlcCommunicationService::lastErrorType() const +{ + return last_error_type_; +} + const std::string &PlcCommunicationService::lastError() const { return last_error_; @@ -291,14 +285,20 @@ void PlcCommunicationService::pollNextBlock() QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress); if (reply == nullptr) { - setError(modbusErrorText(master_->error())); + handleModbusError(master_->error()); return; } pending_reply_ = reply; + const std::uint64_t generation = connection_generation_; connect(reply, &QModbusReply::finished, this, - [this, reply, block, block_index] + [this, reply, block, block_index, generation] { + if (generation != connection_generation_) + { + reply->deleteLater(); + return; + } handleReadFinished(reply, block); if (state_ == PlcConnectionState::Connected && reply->error() == QModbusDevice::NoError @@ -312,12 +312,7 @@ void PlcCommunicationService::pollNextBlock() [](bool read) { return read; }); 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) @@ -341,7 +336,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock } if (reply->error() != QModbusDevice::NoError) { - setError(modbusErrorText(reply->error())); + handleModbusError(reply->error()); return; } const QModbusDataUnit result = reply->result(); @@ -357,6 +352,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock repository_.updateWord(address, static_cast(result.value(index))); } } + received_valid_response_ = true; last_error_.clear(); emit cacheUpdated(); if (cache_updated_callback_) @@ -377,16 +373,21 @@ RegisterWriteResult PlcCommunicationService::sendBitWrite( QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); if (reply == nullptr) { - setError(modbusErrorText(master_->error())); + handleModbusError(master_->error()); return {false, RegisterError::WriteRejected}; } connect(reply, &QModbusReply::finished, this, - [this, reply] + [this, reply, generation = connection_generation_] { + if (generation != connection_generation_) + { + reply->deleteLater(); + return; + } if (reply->error() != QModbusDevice::NoError) { - setError(modbusErrorText(reply->error())); + handleModbusError(reply->error()); } reply->deleteLater(); }); @@ -405,22 +406,77 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite( QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress); if (reply == nullptr) { - setError(modbusErrorText(master_->error())); + handleModbusError(master_->error()); return {false, RegisterError::WriteRejected}; } connect(reply, &QModbusReply::finished, this, - [this, reply] + [this, reply, generation = connection_generation_] { + if (generation != connection_generation_) + { + reply->deleteLater(); + return; + } if (reply->error() != QModbusDevice::NoError) { - setError(modbusErrorText(reply->error())); + handleModbusError(reply->error()); } reply->deleteLater(); }); 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 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) { 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(); + updateInitialReadCompleted(false); setState(PlcConnectionState::Faulted); - emit communicationError(message); + emit communicationError(failure.message); if (error_reported_callback_) { error_reported_callback_(last_error_); diff --git a/app/src/infrastructure/plc_communication_service.h b/app/src/infrastructure/plc_communication_service.h index 0b85025..9dbb0b2 100644 --- a/app/src/infrastructure/plc_communication_service.h +++ b/app/src/infrastructure/plc_communication_service.h @@ -3,9 +3,11 @@ #include "services/plc_communication_gateway.h" #include "domain/register_repository.h" +#include #include #include +#include #include #include #include @@ -13,6 +15,7 @@ class PlcRegisterRepository; class QModbusReply; class QModbusRtuSerialMaster; +struct PlcCommunicationFailure; class PlcCommunicationService final : public QObject, public PlcCommunicationGateway { @@ -31,6 +34,7 @@ public: PlcConnectionState state() const override; bool initialReadCompleted() const override; + PlcCommunicationError lastErrorType() const override; const std::string &lastError() const override; const PlcSerialConfiguration &configuration() const; void setCallbacks( @@ -60,8 +64,11 @@ private: RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); RegisterWriteResult sendWordWrite( 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 setError(const QString &message); + void setError(const PlcCommunicationFailure &failure); PlcRegisterRepository &repository_; std::unique_ptr master_; @@ -73,9 +80,14 @@ private: std::size_t next_poll_block_ = 0; QModbusReply *pending_reply_ = nullptr; bool poll_update_pending_ = false; + bool disconnecting_ = false; + bool serial_session_opened_ = false; + bool received_valid_response_ = false; + std::uint64_t connection_generation_ = 0; PlcConnectionState state_ = PlcConnectionState::Disconnected; bool initial_read_completed_ = false; std::vector initial_blocks_read_; + PlcCommunicationError last_error_type_ = PlcCommunicationError::None; std::string last_error_; std::function state_changed_callback_; std::function initial_read_changed_callback_; diff --git a/app/src/services/plc_communication_gateway.h b/app/src/services/plc_communication_gateway.h index 7b99aad..db4682c 100644 --- a/app/src/services/plc_communication_gateway.h +++ b/app/src/services/plc_communication_gateway.h @@ -27,6 +27,22 @@ enum class PlcConnectionState Faulted }; +enum class PlcCommunicationError +{ + None, + SerialPortOpenFailed, + PlcNotResponding, + UsbSerialAdapterRemoved, + SerialConnectionLost, + CommunicationTimeout, + ProtocolError, + ReadFailed, + WriteFailed, + ConfigurationError, + RequestAborted, + Unknown +}; + struct PlcCommunicationResult { bool succeeded = false; @@ -45,6 +61,7 @@ public: const std::vector &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 state_changed, diff --git a/app/src/services/runtime_mode_service.cpp b/app/src/services/runtime_mode_service.cpp index 82a0446..6e9dcd4 100644 --- a/app/src/services/runtime_mode_service.cpp +++ b/app/src/services/runtime_mode_service.cpp @@ -87,7 +87,7 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() ModeTransitionResult RuntimeModeService::enterOnlineRunning() { const ModeTransitionResult result = state_.enterOnlineRunning( - initial_plc_read_completed_); + initialPlcReadCompleted()); if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr) { active_repository_->use(*plc_repository_); @@ -103,7 +103,10 @@ void RuntimeModeService::setInitialPlcReadCompleted(bool completed) 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 @@ -139,13 +142,17 @@ void RuntimeModeService::configurePlc( gateway.setCallbacks( [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_) @@ -178,6 +185,10 @@ PlcCommunicationResult RuntimeModeService::connectPlc( { return {false, "PLC 通信服务尚未配置"}; } + if (plc_gateway_->state() == PlcConnectionState::Faulted) + { + plc_gateway_->disconnectDevice(); + } refreshPlcPollAddresses(); setInitialPlcReadCompleted(false); return plc_gateway_->connectDevice(configuration); diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 497cd37..a365653 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -1205,8 +1205,16 @@ void MainWindow::updateModeUi(const QString &message) policy.usesVirtualRegisters ? tr("数据源:虚拟 M/D") : policy.usesPlcRegisters ? tr("数据源:PLC 缓存") : tr("数据源:未启用")); const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); + const bool plc_configuration_available = plc_state == PlcConnectionState::Disconnected + || plc_state == PlcConnectionState::Faulted; 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); if (plc_state == PlcConnectionState::Connecting) { @@ -1239,15 +1247,16 @@ void MainWindow::updateSimulationUi(bool report_fault) == ApplicationMode::OfflineRunning; const bool online = runtime_mode_service_.mode() == ApplicationMode::OnlineRunning; + const bool plc_connected = runtime_mode_service_.plcConnectionState() + == PlcConnectionState::Connected; const SimulationState state = runtime_mode_service_.simulationState(); hmi_editor_widget_->setRuntimeWriteEnabled( - online || (offline && state == SimulationState::Running)); + (online && plc_connected) + || (offline && state == SimulationState::Running)); if (runtime_monitor_widget_ != nullptr) { runtime_monitor_widget_->setHmiWriteEnabled( - (online - && runtime_mode_service_.plcConnectionState() - == PlcConnectionState::Connected) + (online && plc_connected) || (offline && state == SimulationState::Running)); runtime_monitor_widget_->refreshValues( runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index c33eae7..154051b 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -76,6 +76,10 @@ public: void setPollAddresses(const std::vector &) override {} PlcConnectionState state() const override { return connection_state; } bool initialReadCompleted() const override { return false; } + PlcCommunicationError lastErrorType() const override + { + return PlcCommunicationError::None; + } const std::string &lastError() const override { return last_error; } void setCallbacks( @@ -123,6 +127,15 @@ public: void disconnectDevice() override { 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 &addresses) override @@ -132,6 +145,7 @@ public: PlcConnectionState state() const override { return connection_state; } bool initialReadCompleted() const override { return initial_read; } + PlcCommunicationError lastErrorType() const override { return last_error_type; } const std::string &lastError() const override { return last_error; } 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; bool initial_read = false; + PlcCommunicationError last_error_type = PlcCommunicationError::None; std::string last_error; std::vector poll_addresses; std::function state_changed; @@ -644,6 +680,30 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() gateway.poll_addresses.end(), RegisterAddress{RegisterArea::D, 9}) != gateway.poll_addresses.end(), "online free monitor addresses must join the active PLC poll set immediately"); + + QAction *configure_action = requiredChild(window, "configurePlcAction"); + QAction *disconnect_action = requiredChild(window, "disconnectPlcAction"); + QListWidget *output = requiredChild(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 diff --git a/app/tests/plc_runtime_tests.cpp b/app/tests/plc_runtime_tests.cpp index e21d67e..4f9fcb6 100644 --- a/app/tests/plc_runtime_tests.cpp +++ b/app/tests/plc_runtime_tests.cpp @@ -1,5 +1,6 @@ #include "domain/active_register_repository.h" #include "domain/project_storage.h" +#include "infrastructure/plc_communication_error_classifier.h" #include "infrastructure/plc_register_repository.h" #include "services/offline_simulation_service.h" #include "services/plc_communication_gateway.h" @@ -34,6 +35,9 @@ public: const PlcSerialConfiguration &configuration) override { last_configuration = configuration; + last_error_type = PlcCommunicationError::None; + last_error.clear(); + initial_read = false; connection_state = PlcConnectionState::Connected; if (state_changed) { @@ -44,6 +48,7 @@ public: void disconnectDevice() override { + ++disconnect_count; connection_state = PlcConnectionState::Disconnected; initial_read = false; if (initial_read_changed) @@ -63,6 +68,7 @@ public: PlcConnectionState state() const override { return connection_state; } bool initialReadCompleted() const override { return initial_read; } + PlcCommunicationError lastErrorType() const override { return last_error_type; } const std::string &lastError() const override { return last_error; } 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; bool initial_read = false; + int disconnect_count = 0; + PlcCommunicationError last_error_type = PlcCommunicationError::None; std::string last_error; PlcSerialConfiguration last_configuration; std::vector poll_addresses; @@ -209,6 +238,82 @@ void testRuntimeRepositorySwitchingAndDisconnect() "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 int main() @@ -217,6 +322,8 @@ int main() { testPlcCacheAndWriteForwarding(); testRuntimeRepositorySwitchingAndDisconnect(); + testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect(); + testPlcCommunicationErrorClassification(); } catch (const std::exception &error) { diff --git a/app/tests/plc_runtime_tests.pro b/app/tests/plc_runtime_tests.pro index 111da01..4da19a7 100644 --- a/app/tests/plc_runtime_tests.pro +++ b/app/tests/plc_runtime_tests.pro @@ -1,4 +1,4 @@ -QT += core +QT += core serialbus TEMPLATE = app TARGET = plc_runtime_tests @@ -22,6 +22,7 @@ SOURCES += \ ../src/services/software_logic_executor.cpp \ ../src/services/offline_simulation_service.cpp \ ../src/services/runtime_mode_service.cpp \ + ../src/infrastructure/plc_communication_error_classifier.cpp \ ../src/infrastructure/plc_register_repository.cpp HEADERS += \ @@ -39,4 +40,5 @@ HEADERS += \ ../src/services/offline_simulation_service.h \ ../src/services/runtime_mode_service.h \ ../src/services/plc_communication_gateway.h \ + ../src/infrastructure/plc_communication_error_classifier.h \ ../src/infrastructure/plc_register_repository.h diff --git a/app/tests/runtime_mode_service_tests.cpp b/app/tests/runtime_mode_service_tests.cpp index 3be4689..1fe57df 100644 --- a/app/tests/runtime_mode_service_tests.cpp +++ b/app/tests/runtime_mode_service_tests.cpp @@ -1,12 +1,15 @@ #include "services/runtime_mode_service.h" #include "services/offline_simulation_service.h" #include "services/project_service.h" +#include "domain/active_register_repository.h" #include "domain/project_storage.h" #include "domain/register_repository.h" +#include #include #include #include +#include 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 &) 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 state_callback, + std::function initial_callback, + std::function cache_callback, + std::function 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 state_changed; + std::function initial_read_changed; + std::function cache_updated; + std::function error_reported; +}; + void require(bool condition, const std::string &message) { if (!condition) @@ -37,9 +99,14 @@ void testModeTransitions() // 验证服务将 PLC 首读状态与领域模式切换规则正确组合 TestProjectStorage 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); + service.configurePlc( + gateway, active_repository, virtual_repository, plc_repository); require(service.mode() == ApplicationMode::Editing, "service must start in editing mode"); @@ -64,7 +131,10 @@ void testModeTransitions() == ModeTransitionError::InitialPlcReadRequired, "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(), "service must retain the initial PLC read state"); require(service.enterOnlineRunning().succeeded,