| @@ -8,18 +8,6 @@ QString displayPortName(const QString &port_name) | |||
| 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( | |||
| @@ -27,18 +15,6 @@ PlcCommunicationFailure classifyPlcCommunicationError( | |||
| 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) | |||
| { | |||
| @@ -54,7 +30,8 @@ PlcCommunicationFailure classifyPlcCommunicationError( | |||
| } | |||
| return { | |||
| PlcCommunicationError::SerialConnectionLost, | |||
| QStringLiteral("PLC 串口连接异常,本地端口 %1 仍存在;请重新连接") | |||
| QStringLiteral( | |||
| "PLC 串口 %1 连接已中断;请检查 USB 转串口是否被拔出或已经失效") | |||
| .arg(port_name)}; | |||
| } | |||
| case QModbusDevice::TimeoutError: | |||
| @@ -8,9 +8,7 @@ | |||
| struct PlcCommunicationErrorContext | |||
| { | |||
| QString portName; | |||
| QString nativeError; | |||
| bool serialSessionOpened = false; | |||
| bool portAvailable = false; | |||
| bool receivedValidResponse = false; | |||
| }; | |||
| @@ -8,7 +8,6 @@ | |||
| #include <QModbusReply> | |||
| #include <QModbusRtuSerialMaster> | |||
| #include <QSerialPort> | |||
| #include <QSerialPortInfo> | |||
| #include <QVariant> | |||
| #include <algorithm> | |||
| @@ -71,7 +70,11 @@ PlcCommunicationService::PlcCommunicationService( | |||
| { | |||
| serial_session_opened_ = false; | |||
| } | |||
| if (!disconnecting_ && state_ != PlcConnectionState::Faulted) | |||
| else if (serial_session_opened_) | |||
| { | |||
| handleUnexpectedDisconnect(); | |||
| } | |||
| else if (state_ != PlcConnectionState::Faulted) | |||
| { | |||
| setState(PlcConnectionState::Disconnected); | |||
| } | |||
| @@ -152,6 +155,8 @@ void PlcCommunicationService::disconnectDevice() | |||
| received_valid_response_ = false; | |||
| repository_.invalidate(); | |||
| updateInitialReadCompleted(false, true); | |||
| last_error_type_ = PlcCommunicationError::None; | |||
| last_error_.clear(); | |||
| setState(PlcConnectionState::Disconnected); | |||
| disconnecting_ = false; | |||
| } | |||
| @@ -353,6 +358,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock | |||
| } | |||
| } | |||
| received_valid_response_ = true; | |||
| last_error_type_ = PlcCommunicationError::None; | |||
| last_error_.clear(); | |||
| emit cacheUpdated(); | |||
| if (cache_updated_callback_) | |||
| @@ -427,23 +433,6 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite( | |||
| 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) | |||
| { | |||
| @@ -459,20 +448,31 @@ void PlcCommunicationService::updateInitialReadCompleted( | |||
| } | |||
| } | |||
| void PlcCommunicationService::handleUnexpectedDisconnect() | |||
| { | |||
| serial_session_opened_ = false; | |||
| const QString port_name = QString::fromStdString(configuration_.portName).trimmed(); | |||
| setError({ | |||
| PlcCommunicationError::SerialConnectionLost, | |||
| QStringLiteral( | |||
| "PLC 串口 %1 连接已中断;请检查 USB 转串口是否被拔出或已经失效") | |||
| .arg(port_name)}); | |||
| } | |||
| void PlcCommunicationService::handleModbusError(QModbusDevice::Error error) | |||
| { | |||
| if (disconnecting_ | |||
| || (error == QModbusDevice::ReplyAbortedError | |||
| && state_ == PlcConnectionState::Disconnected) | |||
| || (state_ == PlcConnectionState::Disconnected | |||
| && last_error_type_ == PlcCommunicationError::SerialConnectionLost) | |||
| || state_ == PlcConnectionState::Faulted) | |||
| { | |||
| return; | |||
| } | |||
| const PlcCommunicationErrorContext context{ | |||
| QString::fromStdString(configuration_.portName), | |||
| master_->errorString(), | |||
| serial_session_opened_, | |||
| configuredPortAvailable(), | |||
| received_valid_response_}; | |||
| setError(classifyPlcCommunicationError(error, context)); | |||
| } | |||
| @@ -497,7 +497,15 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | |||
| last_error_ = toUtf8(failure.message); | |||
| poll_timer_.stop(); | |||
| updateInitialReadCompleted(false); | |||
| setState(PlcConnectionState::Faulted); | |||
| const bool disconnected = failure.type == PlcCommunicationError::SerialPortOpenFailed | |||
| || failure.type == PlcCommunicationError::SerialConnectionLost | |||
| || failure.type == PlcCommunicationError::UsbSerialAdapterRemoved; | |||
| if (disconnected) | |||
| { | |||
| serial_session_opened_ = false; | |||
| } | |||
| setState(disconnected | |||
| ? PlcConnectionState::Disconnected : PlcConnectionState::Faulted); | |||
| emit communicationError(failure.message); | |||
| if (error_reported_callback_) | |||
| { | |||
| @@ -64,8 +64,8 @@ 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 handleUnexpectedDisconnect(); | |||
| void handleModbusError(QModbusDevice::Error error); | |||
| void setState(PlcConnectionState state); | |||
| void setError(const PlcCommunicationFailure &failure); | |||
| @@ -121,9 +121,17 @@ QString plcStatusText(const RuntimeModeService &service) | |||
| : MainWindow::tr("PLC 通信故障:%1").arg(error); | |||
| } | |||
| case PlcConnectionState::Disconnected: | |||
| { | |||
| const QString error = fromUtf8(service.plcError()); | |||
| return error.isEmpty() | |||
| ? MainWindow::tr("PLC 已断开") | |||
| : MainWindow::tr("PLC 已断开:%1").arg(error); | |||
| } | |||
| default: | |||
| { | |||
| return MainWindow::tr("PLC 已断开"); | |||
| } | |||
| } | |||
| } | |||
| QString controlTypeText(HmiControlType type) | |||
| @@ -169,13 +169,12 @@ public: | |||
| } | |||
| } | |||
| void failCommunication( | |||
| PlcCommunicationError error_type, const std::string &message) | |||
| void loseSerialConnection(const std::string &message) | |||
| { | |||
| initial_read = false; | |||
| last_error_type = error_type; | |||
| last_error_type = PlcCommunicationError::SerialConnectionLost; | |||
| last_error = message; | |||
| connection_state = PlcConnectionState::Faulted; | |||
| connection_state = PlcConnectionState::Disconnected; | |||
| if (initial_read_changed) | |||
| { | |||
| initial_read_changed(false); | |||
| @@ -684,21 +683,20 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||
| 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 接线"); | |||
| gateway.loseSerialConnection( | |||
| "PLC 串口 COM9 连接已中断;请检查 USB 转串口是否被拔出或已经失效"); | |||
| 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"); | |||
| && configure_action->text() == QStringLiteral("PLC 配置"), | |||
| "a lost serial connection must expose direct configuration"); | |||
| require(!disconnect_action->isEnabled(), | |||
| "a lost serial connection must disable redundant disconnect actions"); | |||
| require(output->count() > 0 | |||
| && output->item(output->count() - 1)->text().contains( | |||
| QStringLiteral("PLC 通信超时")), | |||
| QStringLiteral("连接已中断")), | |||
| "communication fault details must remain in the output log"); | |||
| online_action->trigger(); | |||
| @@ -289,7 +289,6 @@ void testPlcCommunicationErrorClassification() | |||
| "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 未响应")), | |||
| @@ -301,13 +300,11 @@ void testPlcCommunicationErrorClassification() | |||
| && 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"); | |||
| require(failure.type == PlcCommunicationError::SerialConnectionLost | |||
| && failure.message.contains(QStringLiteral("连接已中断")), | |||
| "connection errors after opening the port must report a lost serial link"); | |||
| context.portAvailable = true; | |||
| failure = classifyPlcCommunicationError(QModbusDevice::ProtocolError, context); | |||
| require(failure.type == PlcCommunicationError::ProtocolError | |||
| && failure.message.contains(QStringLiteral("Modbus 协议异常")), | |||