Przeglądaj źródła

fix: 完善PLC通信故障恢复

main
suyu 1 miesiąc temu
rodzic
commit
0a640c389f
12 zmienionych plików z 575 dodań i 78 usunięć
  1. +2
    -0
      app/integrated_platform.pro
  2. +124
    -0
      app/src/infrastructure/plc_communication_error_classifier.cpp
  3. +25
    -0
      app/src/infrastructure/plc_communication_error_classifier.h
  4. +119
    -61
      app/src/infrastructure/plc_communication_service.cpp
  5. +13
    -1
      app/src/infrastructure/plc_communication_service.h
  6. +17
    -0
      app/src/services/plc_communication_gateway.h
  7. +18
    -7
      app/src/services/runtime_mode_service.cpp
  8. +14
    -5
      app/src/ui/main_window.cpp
  9. +60
    -0
      app/tests/main_window_tests.cpp
  10. +107
    -0
      app/tests/plc_runtime_tests.cpp
  11. +3
    -1
      app/tests/plc_runtime_tests.pro
  12. +73
    -3
      app/tests/runtime_mode_service_tests.cpp

+ 2
- 0
app/integrated_platform.pro Wyświetl plik

@@ -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 \


+ 124
- 0
app/src/infrastructure/plc_communication_error_classifier.cpp Wyświetl plik

@@ -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 通信失败")};
}
}
}

+ 25
- 0
app/src/infrastructure/plc_communication_error_classifier.h Wyświetl plik

@@ -0,0 +1,25 @@
#pragma once

#include "services/plc_communication_gateway.h"

#include <QModbusDevice>
#include <QString>

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);

+ 119
- 61
app/src/infrastructure/plc_communication_service.cpp Wyświetl plik

@@ -1,5 +1,6 @@
#include "plc_communication_service.h"

#include "plc_communication_error_classifier.h"
#include "plc_register_repository.h"

#include <QModbusDataUnit>
@@ -7,6 +8,7 @@
#include <QModbusReply>
#include <QModbusRtuSerialMaster>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QVariant>

#include <algorithm>
@@ -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<std::int16_t>(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<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)
{
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_);


+ 13
- 1
app/src/infrastructure/plc_communication_service.h Wyświetl plik

@@ -3,9 +3,11 @@
#include "services/plc_communication_gateway.h"
#include "domain/register_repository.h"

#include <QModbusDevice>
#include <QObject>
#include <QTimer>

#include <cstdint>
#include <memory>
#include <string>
#include <vector>
@@ -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<QModbusRtuSerialMaster> 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<bool> initial_blocks_read_;
PlcCommunicationError last_error_type_ = PlcCommunicationError::None;
std::string last_error_;
std::function<void()> state_changed_callback_;
std::function<void(bool)> initial_read_changed_callback_;


+ 17
- 0
app/src/services/plc_communication_gateway.h Wyświetl plik

@@ -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<RegisterAddress> &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<void()> state_changed,


+ 18
- 7
app/src/services/runtime_mode_service.cpp Wyświetl plik

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


+ 14
- 5
app/src/ui/main_window.cpp Wyświetl plik

@@ -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());


+ 60
- 0
app/tests/main_window_tests.cpp Wyświetl plik

@@ -76,6 +76,10 @@ public:
void setPollAddresses(const std::vector<RegisterAddress> &) 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<RegisterAddress> &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<RegisterAddress> poll_addresses;
std::function<void()> 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<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 接线");
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


+ 107
- 0
app/tests/plc_runtime_tests.cpp Wyświetl plik

@@ -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<RegisterAddress> 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)
{


+ 3
- 1
app/tests/plc_runtime_tests.pro Wyświetl plik

@@ -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

+ 73
- 3
app/tests/runtime_mode_service_tests.cpp Wyświetl plik

@@ -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 <functional>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>

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<RegisterAddress> &) 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<void()> state_callback,
std::function<void(bool)> initial_callback,
std::function<void()> cache_callback,
std::function<void(const std::string &)> 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<void()> state_changed;
std::function<void(bool)> initial_read_changed;
std::function<void()> cache_updated;
std::function<void(const std::string &)> 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,


Ładowanie…
Anuluj
Zapisz