diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index fb73612..feb737e 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -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 \ diff --git a/app/src/domain/active_register_repository.cpp b/app/src/domain/active_register_repository.cpp new file mode 100644 index 0000000..0c1c5dd --- /dev/null +++ b/app/src/domain/active_register_repository.cpp @@ -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); +} diff --git a/app/src/domain/active_register_repository.h b/app/src/domain/active_register_repository.h new file mode 100644 index 0000000..d5857bb --- /dev/null +++ b/app/src/domain/active_register_repository.h @@ -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; +}; diff --git a/app/src/infrastructure/plc_communication_service.cpp b/app/src/infrastructure/plc_communication_service.cpp new file mode 100644 index 0000000..386ad0c --- /dev/null +++ b/app/src/infrastructure/plc_communication_service.cpp @@ -0,0 +1,391 @@ +#include "plc_communication_service.h" + +#include "plc_register_repository.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +constexpr int kMaximumReadCount = 120; + +std::string toUtf8(const QString &value) +{ + const QByteArray bytes = value.toUtf8(); + return std::string(bytes.constData(), static_cast(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()) +{ + 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(configuration.dataBits)); + master_->setConnectionParameter( + QModbusDevice::SerialParityParameter, + static_cast(configuration.parity)); + master_->setConnectionParameter( + QModbusDevice::SerialStopBitsParameter, + static_cast(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 &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 state_changed, + std::function initial_read_changed, + std::function cache_updated, + std::function 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 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(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(index); + if (block.area == RegisterArea::M) + { + repository_.updateBit(address, result.value(index) != 0U); + } + else + { + repository_.updateWord(address, static_cast(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(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_); + } +} diff --git a/app/src/infrastructure/plc_communication_service.h b/app/src/infrastructure/plc_communication_service.h new file mode 100644 index 0000000..4ccff23 --- /dev/null +++ b/app/src/infrastructure/plc_communication_service.h @@ -0,0 +1,81 @@ +#pragma once + +#include "services/plc_communication_gateway.h" +#include "domain/register_repository.h" + +#include +#include + +#include +#include +#include + +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 &addresses) override; + + PlcConnectionState state() const override; + bool initialReadCompleted() const override; + const std::string &lastError() const override; + const PlcSerialConfiguration &configuration() const; + void setCallbacks( + std::function state_changed, + std::function initial_read_changed, + std::function cache_updated, + std::function 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 master_; + QTimer poll_timer_; + PlcSerialConfiguration configuration_; + std::vector poll_addresses_; + std::vector poll_blocks_; + std::size_t next_poll_block_ = 0; + QModbusReply *pending_reply_ = nullptr; + PlcConnectionState state_ = PlcConnectionState::Disconnected; + bool initial_read_completed_ = false; + std::vector initial_blocks_read_; + std::string last_error_; + std::function state_changed_callback_; + std::function initial_read_changed_callback_; + std::function cache_updated_callback_; + std::function error_reported_callback_; +}; diff --git a/app/src/infrastructure/plc_register_repository.cpp b/app/src/infrastructure/plc_register_repository.cpp new file mode 100644 index 0000000..3705bb8 --- /dev/null +++ b/app/src/infrastructure/plc_register_repository.cpp @@ -0,0 +1,113 @@ +#include "plc_register_repository.h" + +#include + +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(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(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 bit_handler, + std::function 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(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(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; }); +} diff --git a/app/src/infrastructure/plc_register_repository.h b/app/src/infrastructure/plc_register_repository.h new file mode 100644 index 0000000..8d4b6a1 --- /dev/null +++ b/app/src/infrastructure/plc_register_repository.h @@ -0,0 +1,36 @@ +#pragma once + +#include "domain/register_repository.h" + +#include +#include + +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 bit_handler, + std::function 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(RegisterAddress::kMaximumIndex + 1); + std::array bits_{}; + std::array words_{}; + std::array valid_bits_{}; + std::array valid_words_{}; + std::function bit_handler_; + std::function word_handler_; +}; diff --git a/app/src/services/plc_communication_gateway.h b/app/src/services/plc_communication_gateway.h new file mode 100644 index 0000000..7b99aad --- /dev/null +++ b/app/src/services/plc_communication_gateway.h @@ -0,0 +1,54 @@ +#pragma once + +#include "domain/register_address.h" + +#include +#include +#include + +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 &addresses) = 0; + virtual PlcConnectionState state() const = 0; + virtual bool initialReadCompleted() const = 0; + virtual const std::string &lastError() const = 0; + virtual void setCallbacks( + std::function state_changed, + std::function initial_read_changed, + std::function cache_updated, + std::function error_reported) = 0; +}; diff --git a/app/src/services/runtime_mode_service.cpp b/app/src/services/runtime_mode_service.cpp index af69ead..168802d 100644 --- a/app/src/services/runtime_mode_service.cpp +++ b/app/src/services/runtime_mode_service.cpp @@ -9,6 +9,9 @@ #include "runtime_mode_service.h" #include "project_service.h" +#include "domain/active_register_repository.h" + +#include 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 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 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 callback) +{ + plc_status_changed_callback_ = std::move(callback); +} diff --git a/app/src/services/runtime_mode_service.h b/app/src/services/runtime_mode_service.h index 1fa64e2..392fbbf 100644 --- a/app/src/services/runtime_mode_service.h +++ b/app/src/services/runtime_mode_service.h @@ -10,10 +10,14 @@ #include "domain/runtime_state.h" #include "offline_simulation_service.h" +#include "plc_communication_gateway.h" #include +#include 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 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 plc_status_changed_callback_; }; diff --git a/app/tests/plc_runtime_tests.cpp b/app/tests/plc_runtime_tests.cpp new file mode 100644 index 0000000..56940e5 --- /dev/null +++ b/app/tests/plc_runtime_tests.cpp @@ -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 +#include +#include +#include + +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 &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 state_callback, + std::function initial_callback, + std::function cache_callback, + std::function 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 poll_addresses; + std::function state_changed; + std::function initial_read_changed; + std::function cache_updated; + std::function 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; +} diff --git a/app/tests/plc_runtime_tests.pro b/app/tests/plc_runtime_tests.pro new file mode 100644 index 0000000..067af3d --- /dev/null +++ b/app/tests/plc_runtime_tests.pro @@ -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 diff --git a/app/tests/runtime_mode_service_tests.pro b/app/tests/runtime_mode_service_tests.pro index 34de72a..ce46d4b 100644 --- a/app/tests/runtime_mode_service_tests.pro +++ b/app/tests/runtime_mode_service_tests.pro @@ -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 \