Sfoglia il codice sorgente

feat: 支持PLC通信自动恢复

main
suyu 1 mese fa
parent
commit
a0dfb4be0a
5 ha cambiato i file con 212 aggiunte e 4 eliminazioni
  1. +129
    -4
      app/src/infrastructure/plc_communication_service.cpp
  2. +4
    -0
      app/src/infrastructure/plc_communication_service.h
  3. +1
    -0
      app/src/services/plc_communication_gateway.h
  4. +6
    -0
      app/src/ui/main_window.cpp
  5. +72
    -0
      app/tests/main_window_tests.cpp

+ 129
- 4
app/src/infrastructure/plc_communication_service.cpp Vedi File

@@ -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<RegisterAddress> 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);
}
}

+ 4
- 0
app/src/infrastructure/plc_communication_service.h Vedi File

@@ -60,6 +60,9 @@ private:
void rebuildPollBlocks();
void applyPollAddresses(const std::vector<RegisterAddress> &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<QModbusRtuSerialMaster> master_;
QTimer poll_timer_;
QTimer recovery_timer_;
PlcSerialConfiguration configuration_;
std::vector<RegisterAddress> poll_addresses_;
std::vector<RegisterAddress> pending_poll_addresses_;


+ 1
- 0
app/src/services/plc_communication_gateway.h Vedi File

@@ -24,6 +24,7 @@ enum class PlcConnectionState
Disconnected,
Connecting,
Connected,
Recovering,
Faulted
};



+ 6
- 0
app/src/ui/main_window.cpp Vedi File

@@ -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:通信故障"));


+ 72
- 0
app/tests/main_window_tests.cpp Vedi File

@@ -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<QAction>(window, "configurePlcAction");
QAction *disconnect_action = requiredChild<QAction>(window, "disconnectPlcAction");
QListWidget *output = requiredChild<QListWidget>(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();


Caricamento…
Annulla
Salva