Explorar el Código

feat: 实现PLC异步通信运行链路

main
suyu hace 1 mes
padre
commit
569d702d09
Se han modificado 13 ficheros con 1163 adiciones y 3 borrados
  1. +7
    -0
      app/integrated_platform.pro
  2. +33
    -0
      app/src/domain/active_register_repository.cpp
  3. +19
    -0
      app/src/domain/active_register_repository.h
  4. +391
    -0
      app/src/infrastructure/plc_communication_service.cpp
  5. +81
    -0
      app/src/infrastructure/plc_communication_service.h
  6. +113
    -0
      app/src/infrastructure/plc_register_repository.cpp
  7. +36
    -0
      app/src/infrastructure/plc_register_repository.h
  8. +54
    -0
      app/src/services/plc_communication_gateway.h
  9. +153
    -3
      app/src/services/runtime_mode_service.cpp
  10. +20
    -0
      app/src/services/runtime_mode_service.h
  11. +210
    -0
      app/tests/plc_runtime_tests.cpp
  12. +42
    -0
      app/tests/plc_runtime_tests.pro
  13. +4
    -0
      app/tests/runtime_mode_service_tests.pro

+ 7
- 0
app/integrated_platform.pro Ver fichero

@@ -19,6 +19,7 @@ SOURCES += \
src/ui/main_window.cpp \
src/domain/register_address.cpp \
src/domain/register_repository.cpp \
src/domain/active_register_repository.cpp \
src/domain/data_point_model.cpp \
src/domain/hmi_model.cpp \
src/domain/control_logic_model.cpp \
@@ -32,6 +33,8 @@ SOURCES += \
src/services/software_logic_executor.cpp \
src/services/offline_simulation_service.cpp \
src/services/runtime_mode_service.cpp \
src/infrastructure/plc_register_repository.cpp \
src/infrastructure/plc_communication_service.cpp \
src/infrastructure/json_project_storage.cpp \
src/ui/hmi_editor_widget.cpp \
src/ui/logic_editor_widget.cpp
@@ -39,6 +42,7 @@ SOURCES += \
HEADERS += \
src/ui/main_window.h \
src/domain/register_address.h \
src/domain/active_register_repository.h \
src/domain/register_repository.h \
src/domain/data_point_model.h \
src/domain/hmi_model.h \
@@ -53,6 +57,9 @@ HEADERS += \
src/services/hmi_runtime_service.h \
src/services/software_logic_executor.h \
src/services/offline_simulation_service.h \
src/services/plc_communication_gateway.h \
src/infrastructure/plc_register_repository.h \
src/infrastructure/plc_communication_service.h \
src/services/runtime_mode_service.h \
src/infrastructure/json_project_storage.h \
src/ui/hmi_editor_widget.h \


+ 33
- 0
app/src/domain/active_register_repository.cpp Ver fichero

@@ -0,0 +1,33 @@
#include "active_register_repository.h"

ActiveRegisterRepository::ActiveRegisterRepository(RegisterRepository &initial_repository)
: repository_(&initial_repository)
{
}

void ActiveRegisterRepository::use(RegisterRepository &repository)
{
repository_ = &repository;
}

BitReadResult ActiveRegisterRepository::readBit(const RegisterAddress &address) const
{
return repository_->readBit(address);
}

RegisterWriteResult ActiveRegisterRepository::writeBit(
const RegisterAddress &address, bool value)
{
return repository_->writeBit(address, value);
}

WordReadResult ActiveRegisterRepository::readWord(const RegisterAddress &address) const
{
return repository_->readWord(address);
}

RegisterWriteResult ActiveRegisterRepository::writeWord(
const RegisterAddress &address, std::int16_t value)
{
return repository_->writeWord(address, value);
}

+ 19
- 0
app/src/domain/active_register_repository.h Ver fichero

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

#include "register_repository.h"

class ActiveRegisterRepository final : public RegisterRepository
{
public:
explicit ActiveRegisterRepository(RegisterRepository &initial_repository);

void use(RegisterRepository &repository);
BitReadResult readBit(const RegisterAddress &address) const override;
RegisterWriteResult writeBit(const RegisterAddress &address, bool value) override;
WordReadResult readWord(const RegisterAddress &address) const override;
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;

private:
RegisterRepository *repository_ = nullptr;
};

+ 391
- 0
app/src/infrastructure/plc_communication_service.cpp Ver fichero

@@ -0,0 +1,391 @@
#include "plc_communication_service.h"

#include "plc_register_repository.h"

#include <QModbusDataUnit>
#include <QModbusDevice>
#include <QModbusReply>
#include <QModbusRtuSerialMaster>
#include <QSerialPort>
#include <QVariant>

#include <algorithm>
#include <utility>

namespace {

constexpr int kMaximumReadCount = 120;

std::string toUtf8(const QString &value)
{
const QByteArray bytes = value.toUtf8();
return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
}

QModbusDataUnit::RegisterType registerType(RegisterArea area)
{
return area == RegisterArea::M
? QModbusDataUnit::Coils : QModbusDataUnit::HoldingRegisters;
}

} // namespace

PlcCommunicationService::PlcCommunicationService(
PlcRegisterRepository &repository,
QObject *parent)
: QObject(parent),
repository_(repository),
master_(std::make_unique<QModbusRtuSerialMaster>())
{
repository_.setWriteHandlers(
[this](const RegisterAddress &address, bool value)
{
return sendBitWrite(address, value);
},
[this](const RegisterAddress &address, std::int16_t value)
{
return sendWordWrite(address, value);
});
connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock);
connect(master_.get(), &QModbusClient::stateChanged,
this,
[this](QModbusDevice::State device_state)
{
if (device_state == QModbusDevice::ConnectedState)
{
setState(PlcConnectionState::Connected);
poll_timer_.start(configuration_.pollIntervalMs);
pollNextBlock();
}
else if (device_state == QModbusDevice::ConnectingState)
{
setState(PlcConnectionState::Connecting);
}
else if (device_state == QModbusDevice::UnconnectedState
&& state_ != PlcConnectionState::Faulted)
{
setState(PlcConnectionState::Disconnected);
}
});
connect(master_.get(), &QModbusClient::errorOccurred,
this,
[this](QModbusDevice::Error error)
{
if (error != QModbusDevice::NoError)
{
setError(master_->errorString());
}
});
}

PlcCommunicationService::~PlcCommunicationService() = default;

PlcCommunicationResult PlcCommunicationService::connectDevice(
const PlcSerialConfiguration &configuration)
{
if (QString::fromStdString(configuration.portName).trimmed().isEmpty()
|| configuration.serverAddress < 1 || configuration.serverAddress > 247)
{
return {false, "serial port and Modbus server address are required"};
}
if (master_->state() != QModbusDevice::UnconnectedState)
{
return {false, "PLC connection is already active"};
}
configuration_ = configuration;
master_->setConnectionParameter(
QModbusDevice::SerialPortNameParameter,
QString::fromStdString(configuration.portName));
master_->setConnectionParameter(
QModbusDevice::SerialBaudRateParameter, configuration.baudRate);
master_->setConnectionParameter(
QModbusDevice::SerialDataBitsParameter,
static_cast<QSerialPort::DataBits>(configuration.dataBits));
master_->setConnectionParameter(
QModbusDevice::SerialParityParameter,
static_cast<QSerialPort::Parity>(configuration.parity));
master_->setConnectionParameter(
QModbusDevice::SerialStopBitsParameter,
static_cast<QSerialPort::StopBits>(configuration.stopBits));
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);
}
rebuildPollBlocks();
if (!master_->connectDevice())
{
setError(master_->errorString());
return {false, last_error_};
}
setState(PlcConnectionState::Connecting);
return {true, {}};
}

void PlcCommunicationService::disconnectDevice()
{
poll_timer_.stop();
master_->disconnectDevice();
repository_.invalidate();
initial_read_completed_ = false;
emit initialReadCompletedChanged(false);
if (initial_read_changed_callback_)
{
initial_read_changed_callback_(false);
}
setState(PlcConnectionState::Disconnected);
}

void PlcCommunicationService::setPollAddresses(
const std::vector<RegisterAddress> &addresses)
{
poll_addresses_ = addresses;
rebuildPollBlocks();
}

PlcConnectionState PlcCommunicationService::state() const
{
return state_;
}

bool PlcCommunicationService::initialReadCompleted() const
{
return initial_read_completed_;
}

const std::string &PlcCommunicationService::lastError() const
{
return last_error_;
}

const PlcSerialConfiguration &PlcCommunicationService::configuration() const
{
return configuration_;
}

void PlcCommunicationService::setCallbacks(
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)
{
state_changed_callback_ = std::move(state_changed);
initial_read_changed_callback_ = std::move(initial_read_changed);
cache_updated_callback_ = std::move(cache_updated);
error_reported_callback_ = std::move(error_reported);
}

void PlcCommunicationService::rebuildPollBlocks()
{
std::vector<RegisterAddress> addresses = poll_addresses_;
if (addresses.empty())
{
addresses = {
RegisterAddress{RegisterArea::M, 0},
RegisterAddress{RegisterArea::D, 0}};
}
std::sort(
addresses.begin(), addresses.end(),
[](const RegisterAddress &left, const RegisterAddress &right)
{
if (left.area() != right.area())
{
return left.area() == RegisterArea::M;
}
return left.index() < right.index();
});
addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end());
poll_blocks_.clear();
for (const RegisterAddress &address : addresses)
{
if (!address.isValid())
{
continue;
}
if (poll_blocks_.empty()
|| poll_blocks_.back().area != address.area()
|| address.index() > poll_blocks_.back().startAddress
+ poll_blocks_.back().count
|| poll_blocks_.back().count >= kMaximumReadCount)
{
poll_blocks_.push_back({address.area(), address.index(), 1});
}
else
{
poll_blocks_.back().count = address.index()
- poll_blocks_.back().startAddress + 1;
}
}
next_poll_block_ = 0;
initial_blocks_read_.assign(poll_blocks_.size(), false);
initial_read_completed_ = false;
}

void PlcCommunicationService::pollNextBlock()
{
if (state_ != PlcConnectionState::Connected
|| pending_reply_ != nullptr || poll_blocks_.empty())
{
return;
}
const std::size_t block_index = next_poll_block_;
const PollBlock block = poll_blocks_.at(block_index);
next_poll_block_ = (next_poll_block_ + 1U) % poll_blocks_.size();
QModbusDataUnit request(
registerType(block.area), block.startAddress, static_cast<quint16>(block.count));
QModbusReply *reply = master_->sendReadRequest(request, configuration_.serverAddress);
if (reply == nullptr)
{
setError(master_->errorString());
return;
}
pending_reply_ = reply;
connect(reply, &QModbusReply::finished,
this,
[this, reply, block, block_index]
{
handleReadFinished(reply, block);
if (state_ == PlcConnectionState::Connected
&& reply->error() == QModbusDevice::NoError
&& block_index < initial_blocks_read_.size())
{
initial_blocks_read_[block_index] = true;
const bool completed = std::all_of(
initial_blocks_read_.cbegin(),
initial_blocks_read_.cend(),
[](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);
}
}
}
if (pending_reply_ == reply)
{
pending_reply_ = nullptr;
}
reply->deleteLater();
});
}

void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock block)
{
if (state_ != PlcConnectionState::Connected)
{
return;
}
if (reply->error() != QModbusDevice::NoError)
{
setError(reply->errorString());
return;
}
const QModbusDataUnit result = reply->result();
for (uint index = 0; index < result.valueCount(); ++index)
{
const int address = block.startAddress + static_cast<int>(index);
if (block.area == RegisterArea::M)
{
repository_.updateBit(address, result.value(index) != 0U);
}
else
{
repository_.updateWord(address, static_cast<std::int16_t>(result.value(index)));
}
}
last_error_.clear();
emit cacheUpdated();
if (cache_updated_callback_)
{
cache_updated_callback_();
}
}

RegisterWriteResult PlcCommunicationService::sendBitWrite(
const RegisterAddress &address, bool value)
{
if (state_ != PlcConnectionState::Connected)
{
return {false, RegisterError::Unavailable};
}
QModbusDataUnit unit(QModbusDataUnit::Coils, address.index(), 1);
unit.setValue(0, value ? 1U : 0U);
QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
if (reply == nullptr)
{
setError(master_->errorString());
return {false, RegisterError::WriteRejected};
}
connect(reply, &QModbusReply::finished,
this,
[this, reply]
{
if (reply->error() != QModbusDevice::NoError)
{
setError(reply->errorString());
}
reply->deleteLater();
});
return {true, RegisterError::None};
}

RegisterWriteResult PlcCommunicationService::sendWordWrite(
const RegisterAddress &address, std::int16_t value)
{
if (state_ != PlcConnectionState::Connected)
{
return {false, RegisterError::Unavailable};
}
QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 1);
unit.setValue(0, static_cast<quint16>(value));
QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
if (reply == nullptr)
{
setError(master_->errorString());
return {false, RegisterError::WriteRejected};
}
connect(reply, &QModbusReply::finished,
this,
[this, reply]
{
if (reply->error() != QModbusDevice::NoError)
{
setError(reply->errorString());
}
reply->deleteLater();
});
return {true, RegisterError::None};
}

void PlcCommunicationService::setState(PlcConnectionState state)
{
if (state_ == state)
{
return;
}
state_ = state;
emit stateChanged();
if (state_changed_callback_)
{
state_changed_callback_();
}
}

void PlcCommunicationService::setError(const QString &message)
{
last_error_ = toUtf8(message);
poll_timer_.stop();
setState(PlcConnectionState::Faulted);
emit communicationError(message);
if (error_reported_callback_)
{
error_reported_callback_(last_error_);
}
}

+ 81
- 0
app/src/infrastructure/plc_communication_service.h Ver fichero

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

#include "services/plc_communication_gateway.h"
#include "domain/register_repository.h"

#include <QObject>
#include <QTimer>

#include <memory>
#include <string>
#include <vector>

class PlcRegisterRepository;
class QModbusReply;
class QModbusRtuSerialMaster;

class PlcCommunicationService final : public QObject, public PlcCommunicationGateway
{
Q_OBJECT

public:
explicit PlcCommunicationService(
PlcRegisterRepository &repository,
QObject *parent = nullptr);
~PlcCommunicationService() override;

PlcCommunicationResult connectDevice(
const PlcSerialConfiguration &configuration) override;
void disconnectDevice() override;
void setPollAddresses(const std::vector<RegisterAddress> &addresses) override;

PlcConnectionState state() const override;
bool initialReadCompleted() const override;
const std::string &lastError() const override;
const PlcSerialConfiguration &configuration() const;
void setCallbacks(
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) override;

signals:
void stateChanged();
void initialReadCompletedChanged(bool completed);
void cacheUpdated();
void communicationError(const QString &message);

private:
struct PollBlock
{
RegisterArea area = RegisterArea::M;
int startAddress = 0;
int count = 1;
};

void rebuildPollBlocks();
void pollNextBlock();
void handleReadFinished(QModbusReply *reply, PollBlock block);
RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value);
RegisterWriteResult sendWordWrite(
const RegisterAddress &address, std::int16_t value);
void setState(PlcConnectionState state);
void setError(const QString &message);

PlcRegisterRepository &repository_;
std::unique_ptr<QModbusRtuSerialMaster> master_;
QTimer poll_timer_;
PlcSerialConfiguration configuration_;
std::vector<RegisterAddress> poll_addresses_;
std::vector<PollBlock> poll_blocks_;
std::size_t next_poll_block_ = 0;
QModbusReply *pending_reply_ = nullptr;
PlcConnectionState state_ = PlcConnectionState::Disconnected;
bool initial_read_completed_ = false;
std::vector<bool> initial_blocks_read_;
std::string last_error_;
std::function<void()> state_changed_callback_;
std::function<void(bool)> initial_read_changed_callback_;
std::function<void()> cache_updated_callback_;
std::function<void(const std::string &)> error_reported_callback_;
};

+ 113
- 0
app/src/infrastructure/plc_register_repository.cpp Ver fichero

@@ -0,0 +1,113 @@
#include "plc_register_repository.h"

#include <algorithm>

namespace {

bool validAddress(const RegisterAddress &address, RegisterArea area)
{
return address.isValid() && address.area() == area;
}

} // namespace

PlcRegisterRepository::PlcRegisterRepository() = default;

BitReadResult PlcRegisterRepository::readBit(const RegisterAddress &address) const
{
if (!address.isValid())
{
return {false, false, RegisterError::InvalidAddress};
}
if (address.area() != RegisterArea::M)
{
return {false, false, RegisterError::AreaMismatch};
}
const auto index = static_cast<std::size_t>(address.index());
return valid_bits_[index]
? BitReadResult{true, bits_[index], RegisterError::None}
: BitReadResult{false, false, RegisterError::Unavailable};
}

RegisterWriteResult PlcRegisterRepository::writeBit(
const RegisterAddress &address, bool value)
{
if (!validAddress(address, RegisterArea::M))
{
return {false, address.isValid()
? RegisterError::AreaMismatch : RegisterError::InvalidAddress};
}
return bit_handler_ ? bit_handler_(address, value)
: RegisterWriteResult{false, RegisterError::Unavailable};
}

WordReadResult PlcRegisterRepository::readWord(const RegisterAddress &address) const
{
if (!address.isValid())
{
return {false, 0, RegisterError::InvalidAddress};
}
if (address.area() != RegisterArea::D)
{
return {false, 0, RegisterError::AreaMismatch};
}
const auto index = static_cast<std::size_t>(address.index());
return valid_words_[index]
? WordReadResult{true, words_[index], RegisterError::None}
: WordReadResult{false, 0, RegisterError::Unavailable};
}

RegisterWriteResult PlcRegisterRepository::writeWord(
const RegisterAddress &address, std::int16_t value)
{
if (!validAddress(address, RegisterArea::D))
{
return {false, address.isValid()
? RegisterError::AreaMismatch : RegisterError::InvalidAddress};
}
return word_handler_ ? word_handler_(address, value)
: RegisterWriteResult{false, RegisterError::Unavailable};
}

void PlcRegisterRepository::setWriteHandlers(
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler,
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler)
{
bit_handler_ = std::move(bit_handler);
word_handler_ = std::move(word_handler);
}

void PlcRegisterRepository::updateBit(int address, bool value)
{
if (address < 0 || address > RegisterAddress::kMaximumIndex)
{
return;
}
const auto index = static_cast<std::size_t>(address);
bits_[index] = value;
valid_bits_[index] = true;
}

void PlcRegisterRepository::updateWord(int address, std::int16_t value)
{
if (address < 0 || address > RegisterAddress::kMaximumIndex)
{
return;
}
const auto index = static_cast<std::size_t>(address);
words_[index] = value;
valid_words_[index] = true;
}

void PlcRegisterRepository::invalidate()
{
valid_bits_.fill(false);
valid_words_.fill(false);
}

bool PlcRegisterRepository::hasAnyValidValue() const
{
return std::any_of(valid_bits_.cbegin(), valid_bits_.cend(), [](bool value) { return value; })
|| std::any_of(
valid_words_.cbegin(), valid_words_.cend(), [](bool value) { return value; });
}

+ 36
- 0
app/src/infrastructure/plc_register_repository.h Ver fichero

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

#include "domain/register_repository.h"

#include <array>
#include <functional>

class PlcRegisterRepository final : public RegisterRepository
{
public:
PlcRegisterRepository();

BitReadResult readBit(const RegisterAddress &address) const override;
RegisterWriteResult writeBit(const RegisterAddress &address, bool value) override;
WordReadResult readWord(const RegisterAddress &address) const override;
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;

void setWriteHandlers(
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler,
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler);
void updateBit(int address, bool value);
void updateWord(int address, std::int16_t value);
void invalidate();
bool hasAnyValidValue() const;

private:
static constexpr std::size_t kRegisterCount =
static_cast<std::size_t>(RegisterAddress::kMaximumIndex + 1);
std::array<bool, kRegisterCount> bits_{};
std::array<std::int16_t, kRegisterCount> words_{};
std::array<bool, kRegisterCount> valid_bits_{};
std::array<bool, kRegisterCount> valid_words_{};
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler_;
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler_;
};

+ 54
- 0
app/src/services/plc_communication_gateway.h Ver fichero

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

#include "domain/register_address.h"

#include <functional>
#include <string>
#include <vector>

struct PlcSerialConfiguration
{
std::string portName;
int serverAddress = 1;
int baudRate = 9600;
int dataBits = 8;
int parity = 2;
int stopBits = 1;
int responseTimeoutMs = 1000;
int retries = 2;
int pollIntervalMs = 200;
};

enum class PlcConnectionState
{
Disconnected,
Connecting,
Connected,
Faulted
};

struct PlcCommunicationResult
{
bool succeeded = false;
std::string message;
};

class PlcCommunicationGateway
{
public:
virtual ~PlcCommunicationGateway() = default;

virtual PlcCommunicationResult connectDevice(
const PlcSerialConfiguration &configuration) = 0;
virtual void disconnectDevice() = 0;
virtual void setPollAddresses(
const std::vector<RegisterAddress> &addresses) = 0;
virtual PlcConnectionState state() const = 0;
virtual bool initialReadCompleted() const = 0;
virtual const std::string &lastError() const = 0;
virtual void setCallbacks(
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) = 0;
};

+ 153
- 3
app/src/services/runtime_mode_service.cpp Ver fichero

@@ -9,6 +9,9 @@
#include "runtime_mode_service.h"

#include "project_service.h"
#include "domain/active_register_repository.h"

#include <algorithm>

RuntimeModeService::RuntimeModeService(
const ProjectService &project_service,
@@ -18,6 +21,14 @@ RuntimeModeService::RuntimeModeService(
{
}

RuntimeModeService::~RuntimeModeService()
{
if (plc_gateway_ != nullptr)
{
plc_gateway_->setCallbacks({}, {}, {}, {});
}
}

ApplicationMode RuntimeModeService::mode() const
{
return state_.mode();
@@ -35,7 +46,13 @@ ModeTransitionResult RuntimeModeService::enterEditing()
// 先停止扫描再开放编辑,防止运行快照继续写寄存器
offline_simulation_service_.stop();
}
return state_.enterEditing();
const ModeTransitionResult result = state_.enterEditing();
if (result.succeeded && active_repository_ != nullptr
&& virtual_repository_ != nullptr)
{
active_repository_->use(*virtual_repository_);
}
return result;
}

ModeTransitionResult RuntimeModeService::enterOfflineRunning()
@@ -49,6 +66,10 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning()
{
return {false, ModeTransitionError::ProjectNotReady};
}
if (active_repository_ != nullptr && virtual_repository_ != nullptr)
{
active_repository_->use(*virtual_repository_);
}
const SimulationStartResult start_result = offline_simulation_service_.start(
project_service_.project().controlLogics);
if (!start_result.succeeded)
@@ -65,8 +86,13 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning()

ModeTransitionResult RuntimeModeService::enterOnlineRunning()
{
// 首次 PLC 读取状态由服务维护,再交给领域状态机统一裁决
return state_.enterOnlineRunning(initial_plc_read_completed_);
const ModeTransitionResult result = state_.enterOnlineRunning(
initial_plc_read_completed_);
if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr)
{
active_repository_->use(*plc_repository_);
}
return result;
}

void RuntimeModeService::setInitialPlcReadCompleted(bool completed)
@@ -99,3 +125,127 @@ OfflineSimulationService &RuntimeModeService::offlineSimulationService()
{
return offline_simulation_service_;
}

void RuntimeModeService::configurePlc(
PlcCommunicationGateway &gateway,
ActiveRegisterRepository &active_repository,
RegisterRepository &virtual_repository,
RegisterRepository &plc_repository)
{
plc_gateway_ = &gateway;
active_repository_ = &active_repository;
virtual_repository_ = &virtual_repository;
plc_repository_ = &plc_repository;
gateway.setCallbacks(
[this]
{
if (plc_gateway_ != nullptr
&& plc_gateway_->state() == PlcConnectionState::Disconnected)
{
setInitialPlcReadCompleted(false);
if (state_.mode() == ApplicationMode::OnlineRunning)
{
enterEditing();
}
}
if (plc_status_changed_callback_)
{
plc_status_changed_callback_();
}
},
[this](bool completed)
{
setInitialPlcReadCompleted(completed);
if (plc_status_changed_callback_)
{
plc_status_changed_callback_();
}
},
[] {},
[this](const std::string &)
{
if (plc_status_changed_callback_)
{
plc_status_changed_callback_();
}
});
}

PlcCommunicationResult RuntimeModeService::connectPlc(
const PlcSerialConfiguration &configuration)
{
if (plc_gateway_ == nullptr)
{
return {false, "PLC communication is not configured"};
}
std::vector<RegisterAddress> addresses;
const Project &project = project_service_.project();
for (const DataPoint &point : project.dataPoints)
{
addresses.push_back(point.address);
}
for (const HmiPage &page : project.hmiPages)
{
for (const HmiControl &control : page.controls)
{
if (control.binding.has_value())
{
addresses.push_back(*control.binding);
}
}
}
for (const ControlLogic &logic : project.controlLogics)
{
for (const LadderRung &rung : logic.rungs)
{
std::vector<const LogicNode *> nodes;
if (rung.condition.has_value())
{
collectConditionNodes(*rung.condition, &nodes);
}
if (rung.output.has_value())
{
nodes.push_back(&*rung.output);
}
for (const LogicNode *node : nodes)
{
std::visit(
[&addresses](const auto &config) { addresses.push_back(config.address); },
node->config);
}
}
}
plc_gateway_->setPollAddresses(addresses);
setInitialPlcReadCompleted(false);
return plc_gateway_->connectDevice(configuration);
}

void RuntimeModeService::disconnectPlc()
{
if (state_.mode() == ApplicationMode::OnlineRunning)
{
enterEditing();
}
if (plc_gateway_ != nullptr)
{
plc_gateway_->disconnectDevice();
}
setInitialPlcReadCompleted(false);
}

PlcConnectionState RuntimeModeService::plcConnectionState() const
{
return plc_gateway_ == nullptr
? PlcConnectionState::Disconnected : plc_gateway_->state();
}

const std::string &RuntimeModeService::plcError() const
{
static const std::string empty;
return plc_gateway_ == nullptr ? empty : plc_gateway_->lastError();
}

void RuntimeModeService::setPlcStatusChangedCallback(std::function<void()> callback)
{
plc_status_changed_callback_ = std::move(callback);
}

+ 20
- 0
app/src/services/runtime_mode_service.h Ver fichero

@@ -10,10 +10,14 @@

#include "domain/runtime_state.h"
#include "offline_simulation_service.h"
#include "plc_communication_gateway.h"

#include <cstdint>
#include <functional>

class ProjectService;
class ActiveRegisterRepository;
class RegisterRepository;

// 隔离 UI 与领域状态机并编排进入真机运行态的前置条件
class RuntimeModeService
@@ -22,6 +26,7 @@ public:
RuntimeModeService(
const ProjectService &project_service,
OfflineSimulationService &offline_simulation_service);
~RuntimeModeService();

ApplicationMode mode() const;
/**
@@ -63,6 +68,16 @@ public:
std::uint64_t successfulScanCount() const;
const LogicScanResult &simulationError() const;
OfflineSimulationService &offlineSimulationService();
void configurePlc(
PlcCommunicationGateway &gateway,
ActiveRegisterRepository &active_repository,
RegisterRepository &virtual_repository,
RegisterRepository &plc_repository);
PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration);
void disconnectPlc();
PlcConnectionState plcConnectionState() const;
const std::string &plcError() const;
void setPlcStatusChangedCallback(std::function<void()> callback);

private:
const ProjectService &project_service_;
@@ -70,4 +85,9 @@ private:
RuntimeState state_;
// 表示 PLC 缓存是否已通过至少一次有效读取建立
bool initial_plc_read_completed_ = false;
PlcCommunicationGateway *plc_gateway_ = nullptr;
ActiveRegisterRepository *active_repository_ = nullptr;
RegisterRepository *virtual_repository_ = nullptr;
RegisterRepository *plc_repository_ = nullptr;
std::function<void()> plc_status_changed_callback_;
};

+ 210
- 0
app/tests/plc_runtime_tests.cpp Ver fichero

@@ -0,0 +1,210 @@
#include "domain/active_register_repository.h"
#include "domain/project_storage.h"
#include "infrastructure/plc_register_repository.h"
#include "services/offline_simulation_service.h"
#include "services/plc_communication_gateway.h"
#include "services/project_service.h"
#include "services/runtime_mode_service.h"

#include <functional>
#include <iostream>
#include <stdexcept>
#include <utility>

namespace {

class TestProjectStorage final : public ProjectStorage
{
public:
ProjectSaveResult save(const Project &, const std::string &) override
{
return {true, ProjectStorageError::None, {}};
}

ProjectLoadResult load(const std::string &) override
{
return {false, {}, ProjectStorageError::FileReadFailed, {}};
}
};

class FakePlcGateway final : public PlcCommunicationGateway
{
public:
PlcCommunicationResult connectDevice(
const PlcSerialConfiguration &configuration) override
{
last_configuration = configuration;
connection_state = PlcConnectionState::Connected;
if (state_changed)
{
state_changed();
}
return {true, {}};
}

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
{
poll_addresses = addresses;
}

PlcConnectionState state() const override { return connection_state; }
bool initialReadCompleted() const override { return initial_read; }
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);
}
}

PlcConnectionState connection_state = PlcConnectionState::Disconnected;
bool initial_read = false;
std::string last_error;
PlcSerialConfiguration last_configuration;
std::vector<RegisterAddress> poll_addresses;
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)
{
throw std::runtime_error(message);
}
}

void testPlcCacheAndWriteForwarding()
{
PlcRegisterRepository repository;
const RegisterAddress m0{RegisterArea::M, 0};
const RegisterAddress d0{RegisterArea::D, 0};
require(repository.readBit(m0).error == RegisterError::Unavailable,
"uninitialized PLC bit cache must be unavailable");
require(repository.readWord(d0).error == RegisterError::Unavailable,
"uninitialized PLC word cache must be unavailable");

repository.updateBit(0, true);
repository.updateWord(0, -123);
require(repository.readBit(m0).succeeded && repository.readBit(m0).value,
"valid PLC bit cache must be readable");
require(repository.readWord(d0).succeeded
&& repository.readWord(d0).value == -123,
"valid PLC word cache must preserve signed values");

RegisterAddress written_address{RegisterArea::M, 1};
bool written_bit = false;
std::int16_t written_word = 0;
repository.setWriteHandlers(
[&](const RegisterAddress &address, bool value)
{
written_address = address;
written_bit = value;
return RegisterWriteResult{true, RegisterError::None};
},
[&](const RegisterAddress &address, std::int16_t value)
{
written_address = address;
written_word = value;
return RegisterWriteResult{true, RegisterError::None};
});
require(repository.writeBit(m0, false).succeeded
&& written_address == m0 && !written_bit,
"PLC bit writes must be forwarded without changing the cache");
require(repository.writeWord(d0, 456).succeeded
&& written_address == d0 && written_word == 456,
"PLC word writes must be forwarded without changing the cache");
repository.invalidate();
require(!repository.hasAnyValidValue(),
"disconnecting must invalidate all PLC cache validity flags");
}

void testRuntimeRepositorySwitchingAndDisconnect()
{
TestProjectStorage storage;
ProjectService project_service(storage);
project_service.editProject().dataPoints.push_back(
{{RegisterArea::M, 5}, "RunState", "运行状态"});
VirtualRegisterRepository virtual_repository;
PlcRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository);
OfflineSimulationService simulation_service(virtual_repository);
RuntimeModeService service(project_service, simulation_service);
FakePlcGateway gateway;
service.configurePlc(
gateway, active_repository, virtual_repository, plc_repository);

const PlcCommunicationResult connected = service.connectPlc(
{"COM9", 2, 19200, 8, 2, 1, 1000, 2, 200});
require(connected.succeeded && gateway.poll_addresses.size() == 1U,
"PLC connection must receive the project address poll set");
require(service.enterOnlineRunning().error
== ModeTransitionError::InitialPlcReadRequired,
"online mode must wait for the first valid PLC read");

plc_repository.updateBit(5, true);
gateway.completeInitialRead();
require(service.enterOnlineRunning().succeeded,
"online mode must start after the first valid PLC read");
require(active_repository.readBit({RegisterArea::M, 5}).succeeded
&& active_repository.readBit({RegisterArea::M, 5}).value,
"online mode must expose the PLC cache through the active repository");

gateway.disconnectDevice();
require(service.mode() == ApplicationMode::Editing,
"an online disconnect must return the application to editing mode");
require(!service.initialPlcReadCompleted(),
"an online disconnect must clear the initial read flag");
require(active_repository.readBit({RegisterArea::M, 5}).succeeded
&& !active_repository.readBit({RegisterArea::M, 5}).value,
"editing after disconnect must switch back to the virtual repository");
}

} // namespace

int main()
{
try
{
testPlcCacheAndWriteForwarding();
testRuntimeRepositorySwitchingAndDisconnect();
}
catch (const std::exception &error)
{
std::cerr << "PLC runtime tests failed: " << error.what() << '\n';
return 1;
}
std::cout << "PLC runtime tests passed\n";
return 0;
}

+ 42
- 0
app/tests/plc_runtime_tests.pro Ver fichero

@@ -0,0 +1,42 @@
QT += core

TEMPLATE = app
TARGET = plc_runtime_tests

CONFIG += console c++17 testcase warn_on
CONFIG -= app_bundle

INCLUDEPATH += ../src

SOURCES += \
plc_runtime_tests.cpp \
../src/domain/register_address.cpp \
../src/domain/register_repository.cpp \
../src/domain/active_register_repository.cpp \
../src/domain/data_point_model.cpp \
../src/domain/hmi_model.cpp \
../src/domain/control_logic_model.cpp \
../src/domain/project_model.cpp \
../src/domain/runtime_state.cpp \
../src/services/project_service.cpp \
../src/services/software_logic_executor.cpp \
../src/services/offline_simulation_service.cpp \
../src/services/runtime_mode_service.cpp \
../src/infrastructure/plc_register_repository.cpp

HEADERS += \
../src/domain/register_address.h \
../src/domain/register_repository.h \
../src/domain/active_register_repository.h \
../src/domain/data_point_model.h \
../src/domain/hmi_model.h \
../src/domain/control_logic_model.h \
../src/domain/project_model.h \
../src/domain/project_storage.h \
../src/domain/runtime_state.h \
../src/services/project_service.h \
../src/services/software_logic_executor.h \
../src/services/offline_simulation_service.h \
../src/services/runtime_mode_service.h \
../src/services/plc_communication_gateway.h \
../src/infrastructure/plc_register_repository.h

+ 4
- 0
app/tests/runtime_mode_service_tests.pro Ver fichero

@@ -12,6 +12,8 @@ SOURCES += \
runtime_mode_service_tests.cpp \
../src/domain/register_address.cpp \
../src/domain/register_repository.cpp \
../src/domain/active_register_repository.cpp \
../src/domain/data_point_model.cpp \
../src/domain/hmi_model.cpp \
../src/domain/control_logic_model.cpp \
../src/domain/project_model.cpp \
@@ -24,6 +26,8 @@ SOURCES += \
HEADERS += \
../src/domain/register_address.h \
../src/domain/register_repository.h \
../src/domain/active_register_repository.h \
../src/domain/data_point_model.h \
../src/domain/hmi_model.h \
../src/domain/control_logic_model.h \
../src/domain/project_model.h \


Cargando…
Cancelar
Guardar