diff --git a/app/src/infrastructure/plc_communication_service.cpp b/app/src/infrastructure/plc_communication_service.cpp index dd300f1..c84743f 100644 --- a/app/src/infrastructure/plc_communication_service.cpp +++ b/app/src/infrastructure/plc_communication_service.cpp @@ -16,6 +16,7 @@ namespace { constexpr int kMaximumReadCount = 120; +constexpr int kRecoveryProbeIntervalMs = 2000; std::string toUtf8(const QString &value) { @@ -29,6 +30,18 @@ QModbusDataUnit::RegisterType registerType(RegisterArea area) ? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters; } +bool isRecoverableTimeout(PlcCommunicationError error) +{ + return error == PlcCommunicationError::PlcNotResponding + || error == PlcCommunicationError::CommunicationTimeout; +} + +bool isReadingState(PlcConnectionState state) +{ + return state == PlcConnectionState::Connected + || state == PlcConnectionState::Recovering; +} + } // namespace PlcCommunicationService::PlcCommunicationService( @@ -48,6 +61,12 @@ PlcCommunicationService::PlcCommunicationService( return sendWordWrite(address, value); }); connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock); + recovery_timer_.setSingleShot(true); + connect( + &recovery_timer_, + &QTimer::timeout, + this, + &PlcCommunicationService::probeRecovery); connect(master_.get(), &QModbusClient::stateChanged, this, [this](QModbusDevice::State device_state) @@ -268,7 +287,7 @@ void PlcCommunicationService::applyPollAddresses( rebuildPollBlocks(); poll_update_pending_ = false; pending_poll_addresses_.clear(); - if (state_ == PlcConnectionState::Connected && pending_reply_ == nullptr) + if (isReadingState(state_) && pending_reply_ == nullptr) { pollNextBlock(); } @@ -276,7 +295,7 @@ void PlcCommunicationService::applyPollAddresses( void PlcCommunicationService::pollNextBlock() { - if (state_ != PlcConnectionState::Connected + if (!isReadingState(state_) || pending_reply_ != nullptr || poll_blocks_.empty()) { return; @@ -304,7 +323,7 @@ void PlcCommunicationService::pollNextBlock() return; } handleReadFinished(reply, block); - if (state_ == PlcConnectionState::Connected + if (isReadingState(state_) && reply->error() == QModbusDevice::NoError && !poll_update_pending_ && block_index < initial_blocks_read_.size()) @@ -317,6 +336,10 @@ void PlcCommunicationService::pollNextBlock() if (completed && !initial_read_completed_) { updateInitialReadCompleted(true); + if (state_ == PlcConnectionState::Recovering) + { + setState(PlcConnectionState::Connected); + } } } if (pending_reply_ == reply) @@ -332,9 +355,104 @@ void PlcCommunicationService::pollNextBlock() }); } +void PlcCommunicationService::probeRecovery() +{ + if (state_ != PlcConnectionState::Faulted + || !isRecoverableTimeout(last_error_type_)) + { + return; + } + if (master_->state() != QModbusDevice::ConnectedState) + { + return; + } + if (pending_reply_ != nullptr) + { + recovery_timer_.start(kRecoveryProbeIntervalMs); + return; + } + if (poll_blocks_.empty()) + { + return; + } + const PollBlock block = poll_blocks_.front(); + const QModbusDataUnit request( + registerType(block.area), block.startAddress, 1); + QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress); + if (reply == nullptr) + { + handleRecoveryProbeFailure(master_->error()); + return; + } + pending_reply_ = reply; + const std::uint64_t generation = connection_generation_; + connect(reply, &QModbusReply::finished, + this, + [this, reply, generation] + { + if (generation != connection_generation_) + { + reply->deleteLater(); + return; + } + if (pending_reply_ == reply) + { + pending_reply_ = nullptr; + } + const QModbusDevice::Error error = reply->error(); + reply->deleteLater(); + if (poll_update_pending_) + { + const std::vector addresses = pending_poll_addresses_; + applyPollAddresses(addresses); + } + if (state_ != PlcConnectionState::Faulted + || !isRecoverableTimeout(last_error_type_)) + { + return; + } + if (error != QModbusDevice::NoError) + { + handleRecoveryProbeFailure(error); + return; + } + restoreCommunication(); + }); +} + +void PlcCommunicationService::handleRecoveryProbeFailure(QModbusDevice::Error error) +{ + if (state_ != PlcConnectionState::Faulted + || !isRecoverableTimeout(last_error_type_)) + { + return; + } + if (error == QModbusDevice::ConnectionError) + { + const PlcCommunicationErrorContext context{ + QString::fromStdString(configuration_.portName), + serial_session_opened_, + received_valid_response_}; + setError(classifyPlcCommunicationError(error, context)); + return; + } + recovery_timer_.start(kRecoveryProbeIntervalMs); +} + +void PlcCommunicationService::restoreCommunication() +{ + received_valid_response_ = true; + last_error_type_ = PlcCommunicationError::None; + last_error_.clear(); + rebuildPollBlocks(); + setState(PlcConnectionState::Recovering); + poll_timer_.start(configuration_.pollIntervalMs); + pollNextBlock(); +} + void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock block) { - if (state_ != PlcConnectionState::Connected) + if (!isReadingState(state_)) { return; } @@ -481,6 +599,7 @@ void PlcCommunicationService::closeSerialSession() ++connection_generation_; disconnecting_ = true; poll_timer_.stop(); + recovery_timer_.stop(); pending_reply_ = nullptr; poll_update_pending_ = false; pending_poll_addresses_.clear(); @@ -512,6 +631,7 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) last_error_type_ = failure.type; last_error_ = toUtf8(failure.message); poll_timer_.stop(); + recovery_timer_.stop(); updateInitialReadCompleted(false); const bool disconnected = failure.type == PlcCommunicationError::SerialPortOpenFailed || failure.type == PlcCommunicationError::SerialConnectionLost @@ -527,4 +647,9 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) { error_reported_callback_(last_error_); } + if (isRecoverableTimeout(failure.type) + && master_->state() == QModbusDevice::ConnectedState) + { + recovery_timer_.start(kRecoveryProbeIntervalMs); + } } diff --git a/app/src/infrastructure/plc_communication_service.h b/app/src/infrastructure/plc_communication_service.h index 7432cac..1afa37d 100644 --- a/app/src/infrastructure/plc_communication_service.h +++ b/app/src/infrastructure/plc_communication_service.h @@ -60,6 +60,9 @@ private: void rebuildPollBlocks(); void applyPollAddresses(const std::vector &addresses); void pollNextBlock(); + void probeRecovery(); + void handleRecoveryProbeFailure(QModbusDevice::Error error); + void restoreCommunication(); void handleReadFinished(QModbusReply *reply, PollBlock block); RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); RegisterWriteResult sendWordWrite( @@ -74,6 +77,7 @@ private: PlcRegisterRepository &repository_; std::unique_ptr master_; QTimer poll_timer_; + QTimer recovery_timer_; PlcSerialConfiguration configuration_; std::vector poll_addresses_; std::vector pending_poll_addresses_; diff --git a/app/src/services/plc_communication_gateway.h b/app/src/services/plc_communication_gateway.h index db4682c..37c0d45 100644 --- a/app/src/services/plc_communication_gateway.h +++ b/app/src/services/plc_communication_gateway.h @@ -24,6 +24,7 @@ enum class PlcConnectionState Disconnected, Connecting, Connected, + Recovering, Faulted }; diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 1ea9fdc..9988d00 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -113,6 +113,8 @@ QString plcStatusText(const RuntimeModeService &service) return service.initialPlcReadCompleted() ? MainWindow::tr("PLC 已连接,首次读取完成") : MainWindow::tr("PLC 已连接,正在读取工程使用的 M/D 地址"); + case PlcConnectionState::Recovering: + return MainWindow::tr("PLC 通信已恢复,正在重新读取工程使用的 M/D 地址"); case PlcConnectionState::Faulted: { const QString error = fromUtf8(service.plcError()); @@ -1243,6 +1245,10 @@ void MainWindow::updateModeUi(const QString &message) runtime_mode_service_.initialPlcReadCompleted() ? tr("PLC:已连接,首读完成") : tr("PLC:已连接,正在首读")); } + else if (plc_state == PlcConnectionState::Recovering) + { + register_status_label_->setText(tr("PLC:通信已恢复,正在重新首读")); + } else if (plc_state == PlcConnectionState::Faulted) { register_status_label_->setText(tr("PLC:通信故障")); diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 796b937..443758c 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -163,12 +163,54 @@ public: void completeInitialRead() { initial_read = true; + last_error_type = PlcCommunicationError::None; + last_error.clear(); + if (connection_state == PlcConnectionState::Recovering) + { + connection_state = PlcConnectionState::Connected; + if (state_changed) + { + state_changed(); + } + } if (initial_read_changed) { initial_read_changed(true); } } + void timeoutCommunication(const std::string &message) + { + initial_read = false; + last_error_type = PlcCommunicationError::CommunicationTimeout; + 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); + } + } + + void recoverCommunication() + { + initial_read = false; + last_error_type = PlcCommunicationError::None; + last_error.clear(); + connection_state = PlcConnectionState::Recovering; + if (state_changed) + { + state_changed(); + } + } + void loseSerialConnection(const std::string &message) { initial_read = false; @@ -683,6 +725,36 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() QAction *configure_action = requiredChild(window, "configurePlcAction"); QAction *disconnect_action = requiredChild(window, "disconnectPlcAction"); QListWidget *output = requiredChild(window, "outputList"); + gateway.timeoutCommunication( + "PLC 通信超时,本地串口 COM9 仍处于打开状态;请检查 PLC 供电和 RS-485 接线"); + QApplication::processEvents(); + + require(mode_service.mode() == ApplicationMode::Editing, + "a PLC timeout must return online running to editing"); + require(disconnect_action->isEnabled(), + "a timeout must keep disconnect available while the local port remains open"); + require(configure_action->isEnabled() + && configure_action->text() == QStringLiteral("PLC 重新配置"), + "a timeout must keep PLC reconfiguration available"); + + gateway.recoverCommunication(); + QApplication::processEvents(); + require(mode_service.mode() == ApplicationMode::Editing, + "automatic communication recovery must remain in editing mode"); + require(output->count() > 0 + && output->item(output->count() - 1)->text().contains( + QStringLiteral("通信已恢复")), + "automatic recovery must be retained in the output log"); + online_action->trigger(); + require(mode_service.mode() == ApplicationMode::Editing, + "recovered communication must complete a fresh initial read before online mode"); + + gateway.completeInitialRead(); + QApplication::processEvents(); + online_action->trigger(); + require(mode_service.mode() == ApplicationMode::OnlineRunning, + "users must be able to re-enter online mode after the recovered initial read"); + gateway.loseSerialConnection( "PLC 串口 COM9 连接已中断;请检查 USB 转串口是否被拔出或已经失效"); QApplication::processEvents();