| @@ -8,18 +8,6 @@ QString displayPortName(const QString &port_name) | |||||
| return trimmed.isEmpty() ? QStringLiteral("当前端口") : 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 | } // namespace | ||||
| PlcCommunicationFailure classifyPlcCommunicationError( | PlcCommunicationFailure classifyPlcCommunicationError( | ||||
| @@ -27,18 +15,6 @@ PlcCommunicationFailure classifyPlcCommunicationError( | |||||
| const PlcCommunicationErrorContext &context) | const PlcCommunicationErrorContext &context) | ||||
| { | { | ||||
| const QString port_name = displayPortName(context.portName); | 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) | switch (error) | ||||
| { | { | ||||
| @@ -54,7 +30,8 @@ PlcCommunicationFailure classifyPlcCommunicationError( | |||||
| } | } | ||||
| return { | return { | ||||
| PlcCommunicationError::SerialConnectionLost, | PlcCommunicationError::SerialConnectionLost, | ||||
| QStringLiteral("PLC 串口连接异常,本地端口 %1 仍存在;请重新连接") | |||||
| QStringLiteral( | |||||
| "PLC 串口 %1 连接已中断;请检查 USB 转串口是否被拔出或已经失效") | |||||
| .arg(port_name)}; | .arg(port_name)}; | ||||
| } | } | ||||
| case QModbusDevice::TimeoutError: | case QModbusDevice::TimeoutError: | ||||
| @@ -8,9 +8,7 @@ | |||||
| struct PlcCommunicationErrorContext | struct PlcCommunicationErrorContext | ||||
| { | { | ||||
| QString portName; | QString portName; | ||||
| QString nativeError; | |||||
| bool serialSessionOpened = false; | bool serialSessionOpened = false; | ||||
| bool portAvailable = false; | |||||
| bool receivedValidResponse = false; | bool receivedValidResponse = false; | ||||
| }; | }; | ||||
| @@ -8,7 +8,6 @@ | |||||
| #include <QModbusReply> | #include <QModbusReply> | ||||
| #include <QModbusRtuSerialMaster> | #include <QModbusRtuSerialMaster> | ||||
| #include <QSerialPort> | #include <QSerialPort> | ||||
| #include <QSerialPortInfo> | |||||
| #include <QVariant> | #include <QVariant> | ||||
| #include <algorithm> | #include <algorithm> | ||||
| @@ -71,7 +70,11 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| { | { | ||||
| serial_session_opened_ = false; | serial_session_opened_ = false; | ||||
| } | } | ||||
| if (!disconnecting_ && state_ != PlcConnectionState::Faulted) | |||||
| else if (serial_session_opened_) | |||||
| { | |||||
| handleUnexpectedDisconnect(); | |||||
| } | |||||
| else if (state_ != PlcConnectionState::Faulted) | |||||
| { | { | ||||
| setState(PlcConnectionState::Disconnected); | setState(PlcConnectionState::Disconnected); | ||||
| } | } | ||||
| @@ -152,6 +155,8 @@ void PlcCommunicationService::disconnectDevice() | |||||
| received_valid_response_ = false; | received_valid_response_ = false; | ||||
| repository_.invalidate(); | repository_.invalidate(); | ||||
| updateInitialReadCompleted(false, true); | updateInitialReadCompleted(false, true); | ||||
| last_error_type_ = PlcCommunicationError::None; | |||||
| last_error_.clear(); | |||||
| setState(PlcConnectionState::Disconnected); | setState(PlcConnectionState::Disconnected); | ||||
| disconnecting_ = false; | disconnecting_ = false; | ||||
| } | } | ||||
| @@ -353,6 +358,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock | |||||
| } | } | ||||
| } | } | ||||
| received_valid_response_ = true; | received_valid_response_ = true; | ||||
| last_error_type_ = PlcCommunicationError::None; | |||||
| last_error_.clear(); | last_error_.clear(); | ||||
| emit cacheUpdated(); | emit cacheUpdated(); | ||||
| if (cache_updated_callback_) | if (cache_updated_callback_) | ||||
| @@ -427,23 +433,6 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite( | |||||
| 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( | void PlcCommunicationService::updateInitialReadCompleted( | ||||
| bool completed, bool force_notification) | 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) | void PlcCommunicationService::handleModbusError(QModbusDevice::Error error) | ||||
| { | { | ||||
| if (disconnecting_ | if (disconnecting_ | ||||
| || (error == QModbusDevice::ReplyAbortedError | || (error == QModbusDevice::ReplyAbortedError | ||||
| && state_ == PlcConnectionState::Disconnected) | && state_ == PlcConnectionState::Disconnected) | ||||
| || (state_ == PlcConnectionState::Disconnected | |||||
| && last_error_type_ == PlcCommunicationError::SerialConnectionLost) | |||||
| || state_ == PlcConnectionState::Faulted) | || state_ == PlcConnectionState::Faulted) | ||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| const PlcCommunicationErrorContext context{ | const PlcCommunicationErrorContext context{ | ||||
| QString::fromStdString(configuration_.portName), | QString::fromStdString(configuration_.portName), | ||||
| master_->errorString(), | |||||
| serial_session_opened_, | serial_session_opened_, | ||||
| configuredPortAvailable(), | |||||
| received_valid_response_}; | received_valid_response_}; | ||||
| setError(classifyPlcCommunicationError(error, context)); | setError(classifyPlcCommunicationError(error, context)); | ||||
| } | } | ||||
| @@ -497,7 +497,15 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) | |||||
| last_error_ = toUtf8(failure.message); | last_error_ = toUtf8(failure.message); | ||||
| poll_timer_.stop(); | poll_timer_.stop(); | ||||
| updateInitialReadCompleted(false); | 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); | emit communicationError(failure.message); | ||||
| if (error_reported_callback_) | if (error_reported_callback_) | ||||
| { | { | ||||
| @@ -64,8 +64,8 @@ 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 updateInitialReadCompleted(bool completed, bool force_notification = false); | ||||
| void handleUnexpectedDisconnect(); | |||||
| void handleModbusError(QModbusDevice::Error error); | void handleModbusError(QModbusDevice::Error error); | ||||
| void setState(PlcConnectionState state); | void setState(PlcConnectionState state); | ||||
| void setError(const PlcCommunicationFailure &failure); | void setError(const PlcCommunicationFailure &failure); | ||||
| @@ -121,9 +121,17 @@ QString plcStatusText(const RuntimeModeService &service) | |||||
| : MainWindow::tr("PLC 通信故障:%1").arg(error); | : MainWindow::tr("PLC 通信故障:%1").arg(error); | ||||
| } | } | ||||
| case PlcConnectionState::Disconnected: | case PlcConnectionState::Disconnected: | ||||
| { | |||||
| const QString error = fromUtf8(service.plcError()); | |||||
| return error.isEmpty() | |||||
| ? MainWindow::tr("PLC 已断开") | |||||
| : MainWindow::tr("PLC 已断开:%1").arg(error); | |||||
| } | |||||
| default: | default: | ||||
| { | |||||
| return MainWindow::tr("PLC 已断开"); | return MainWindow::tr("PLC 已断开"); | ||||
| } | } | ||||
| } | |||||
| } | } | ||||
| QString controlTypeText(HmiControlType type) | 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; | initial_read = false; | ||||
| last_error_type = error_type; | |||||
| last_error_type = PlcCommunicationError::SerialConnectionLost; | |||||
| last_error = message; | last_error = message; | ||||
| connection_state = PlcConnectionState::Faulted; | |||||
| connection_state = PlcConnectionState::Disconnected; | |||||
| if (initial_read_changed) | if (initial_read_changed) | ||||
| { | { | ||||
| initial_read_changed(false); | initial_read_changed(false); | ||||
| @@ -684,21 +683,20 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction"); | QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction"); | ||||
| QAction *disconnect_action = requiredChild<QAction>(window, "disconnectPlcAction"); | QAction *disconnect_action = requiredChild<QAction>(window, "disconnectPlcAction"); | ||||
| QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | ||||
| gateway.failCommunication( | |||||
| PlcCommunicationError::CommunicationTimeout, | |||||
| "PLC 通信超时,本地串口 COM9 仍处于打开状态;请检查 PLC 供电和 RS-485 接线"); | |||||
| gateway.loseSerialConnection( | |||||
| "PLC 串口 COM9 连接已中断;请检查 USB 转串口是否被拔出或已经失效"); | |||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| require(mode_service.mode() == ApplicationMode::Editing, | require(mode_service.mode() == ApplicationMode::Editing, | ||||
| "a communication fault must return the UI from online running to editing"); | "a communication fault must return the UI from online running to editing"); | ||||
| require(configure_action->isEnabled() | 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 | require(output->count() > 0 | ||||
| && output->item(output->count() - 1)->text().contains( | && output->item(output->count() - 1)->text().contains( | ||||
| QStringLiteral("PLC 通信超时")), | |||||
| QStringLiteral("连接已中断")), | |||||
| "communication fault details must remain in the output log"); | "communication fault details must remain in the output log"); | ||||
| online_action->trigger(); | online_action->trigger(); | ||||
| @@ -289,7 +289,6 @@ void testPlcCommunicationErrorClassification() | |||||
| "connection failure before opening the port must be classified explicitly"); | "connection failure before opening the port must be classified explicitly"); | ||||
| context.serialSessionOpened = true; | context.serialSessionOpened = true; | ||||
| context.portAvailable = true; | |||||
| failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context); | failure = classifyPlcCommunicationError(QModbusDevice::TimeoutError, context); | ||||
| require(failure.type == PlcCommunicationError::PlcNotResponding | require(failure.type == PlcCommunicationError::PlcNotResponding | ||||
| && failure.message.contains(QStringLiteral("PLC 未响应")), | && failure.message.contains(QStringLiteral("PLC 未响应")), | ||||
| @@ -301,13 +300,11 @@ void testPlcCommunicationErrorClassification() | |||||
| && failure.message.contains(QStringLiteral("仍处于打开状态")), | && failure.message.contains(QStringLiteral("仍处于打开状态")), | ||||
| "timeout after valid traffic must report an open local serial session"); | "timeout after valid traffic must report an open local serial session"); | ||||
| context.portAvailable = false; | |||||
| failure = classifyPlcCommunicationError(QModbusDevice::ConnectionError, context); | 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); | failure = classifyPlcCommunicationError(QModbusDevice::ProtocolError, context); | ||||
| require(failure.type == PlcCommunicationError::ProtocolError | require(failure.type == PlcCommunicationError::ProtocolError | ||||
| && failure.message.contains(QStringLiteral("Modbus 协议异常")), | && failure.message.contains(QStringLiteral("Modbus 协议异常")), | ||||