| @@ -19,6 +19,7 @@ SOURCES += \ | |||||
| src/ui/main_window.cpp \ | src/ui/main_window.cpp \ | ||||
| src/domain/register_address.cpp \ | src/domain/register_address.cpp \ | ||||
| src/domain/register_repository.cpp \ | src/domain/register_repository.cpp \ | ||||
| src/domain/active_register_repository.cpp \ | |||||
| src/domain/data_point_model.cpp \ | src/domain/data_point_model.cpp \ | ||||
| src/domain/hmi_model.cpp \ | src/domain/hmi_model.cpp \ | ||||
| src/domain/control_logic_model.cpp \ | src/domain/control_logic_model.cpp \ | ||||
| @@ -32,6 +33,8 @@ SOURCES += \ | |||||
| src/services/software_logic_executor.cpp \ | src/services/software_logic_executor.cpp \ | ||||
| src/services/offline_simulation_service.cpp \ | src/services/offline_simulation_service.cpp \ | ||||
| src/services/runtime_mode_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/infrastructure/json_project_storage.cpp \ | ||||
| src/ui/hmi_editor_widget.cpp \ | src/ui/hmi_editor_widget.cpp \ | ||||
| src/ui/logic_editor_widget.cpp | src/ui/logic_editor_widget.cpp | ||||
| @@ -39,6 +42,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| src/ui/main_window.h \ | src/ui/main_window.h \ | ||||
| src/domain/register_address.h \ | src/domain/register_address.h \ | ||||
| src/domain/active_register_repository.h \ | |||||
| src/domain/register_repository.h \ | src/domain/register_repository.h \ | ||||
| src/domain/data_point_model.h \ | src/domain/data_point_model.h \ | ||||
| src/domain/hmi_model.h \ | src/domain/hmi_model.h \ | ||||
| @@ -53,6 +57,9 @@ HEADERS += \ | |||||
| src/services/hmi_runtime_service.h \ | src/services/hmi_runtime_service.h \ | ||||
| src/services/software_logic_executor.h \ | src/services/software_logic_executor.h \ | ||||
| src/services/offline_simulation_service.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/services/runtime_mode_service.h \ | ||||
| src/infrastructure/json_project_storage.h \ | src/infrastructure/json_project_storage.h \ | ||||
| src/ui/hmi_editor_widget.h \ | src/ui/hmi_editor_widget.h \ | ||||
| @@ -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); | |||||
| } | |||||
| @@ -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; | |||||
| }; | |||||
| @@ -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_); | |||||
| } | |||||
| } | |||||
| @@ -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_; | |||||
| }; | |||||
| @@ -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; }); | |||||
| } | |||||
| @@ -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_; | |||||
| }; | |||||
| @@ -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; | |||||
| }; | |||||
| @@ -9,6 +9,9 @@ | |||||
| #include "runtime_mode_service.h" | #include "runtime_mode_service.h" | ||||
| #include "project_service.h" | #include "project_service.h" | ||||
| #include "domain/active_register_repository.h" | |||||
| #include <algorithm> | |||||
| RuntimeModeService::RuntimeModeService( | RuntimeModeService::RuntimeModeService( | ||||
| const ProjectService &project_service, | const ProjectService &project_service, | ||||
| @@ -18,6 +21,14 @@ RuntimeModeService::RuntimeModeService( | |||||
| { | { | ||||
| } | } | ||||
| RuntimeModeService::~RuntimeModeService() | |||||
| { | |||||
| if (plc_gateway_ != nullptr) | |||||
| { | |||||
| plc_gateway_->setCallbacks({}, {}, {}, {}); | |||||
| } | |||||
| } | |||||
| ApplicationMode RuntimeModeService::mode() const | ApplicationMode RuntimeModeService::mode() const | ||||
| { | { | ||||
| return state_.mode(); | return state_.mode(); | ||||
| @@ -35,7 +46,13 @@ ModeTransitionResult RuntimeModeService::enterEditing() | |||||
| // 先停止扫描再开放编辑,防止运行快照继续写寄存器 | // 先停止扫描再开放编辑,防止运行快照继续写寄存器 | ||||
| offline_simulation_service_.stop(); | 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() | ModeTransitionResult RuntimeModeService::enterOfflineRunning() | ||||
| @@ -49,6 +66,10 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() | |||||
| { | { | ||||
| return {false, ModeTransitionError::ProjectNotReady}; | return {false, ModeTransitionError::ProjectNotReady}; | ||||
| } | } | ||||
| if (active_repository_ != nullptr && virtual_repository_ != nullptr) | |||||
| { | |||||
| active_repository_->use(*virtual_repository_); | |||||
| } | |||||
| const SimulationStartResult start_result = offline_simulation_service_.start( | const SimulationStartResult start_result = offline_simulation_service_.start( | ||||
| project_service_.project().controlLogics); | project_service_.project().controlLogics); | ||||
| if (!start_result.succeeded) | if (!start_result.succeeded) | ||||
| @@ -65,8 +86,13 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() | |||||
| ModeTransitionResult RuntimeModeService::enterOnlineRunning() | 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) | void RuntimeModeService::setInitialPlcReadCompleted(bool completed) | ||||
| @@ -99,3 +125,127 @@ OfflineSimulationService &RuntimeModeService::offlineSimulationService() | |||||
| { | { | ||||
| return offline_simulation_service_; | 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); | |||||
| } | |||||
| @@ -10,10 +10,14 @@ | |||||
| #include "domain/runtime_state.h" | #include "domain/runtime_state.h" | ||||
| #include "offline_simulation_service.h" | #include "offline_simulation_service.h" | ||||
| #include "plc_communication_gateway.h" | |||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <functional> | |||||
| class ProjectService; | class ProjectService; | ||||
| class ActiveRegisterRepository; | |||||
| class RegisterRepository; | |||||
| // 隔离 UI 与领域状态机并编排进入真机运行态的前置条件 | // 隔离 UI 与领域状态机并编排进入真机运行态的前置条件 | ||||
| class RuntimeModeService | class RuntimeModeService | ||||
| @@ -22,6 +26,7 @@ public: | |||||
| RuntimeModeService( | RuntimeModeService( | ||||
| const ProjectService &project_service, | const ProjectService &project_service, | ||||
| OfflineSimulationService &offline_simulation_service); | OfflineSimulationService &offline_simulation_service); | ||||
| ~RuntimeModeService(); | |||||
| ApplicationMode mode() const; | ApplicationMode mode() const; | ||||
| /** | /** | ||||
| @@ -63,6 +68,16 @@ public: | |||||
| std::uint64_t successfulScanCount() const; | std::uint64_t successfulScanCount() const; | ||||
| const LogicScanResult &simulationError() const; | const LogicScanResult &simulationError() const; | ||||
| OfflineSimulationService &offlineSimulationService(); | 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: | private: | ||||
| const ProjectService &project_service_; | const ProjectService &project_service_; | ||||
| @@ -70,4 +85,9 @@ private: | |||||
| RuntimeState state_; | RuntimeState state_; | ||||
| // 表示 PLC 缓存是否已通过至少一次有效读取建立 | // 表示 PLC 缓存是否已通过至少一次有效读取建立 | ||||
| bool initial_plc_read_completed_ = false; | 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_; | |||||
| }; | }; | ||||
| @@ -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; | |||||
| } | |||||
| @@ -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 | |||||
| @@ -12,6 +12,8 @@ SOURCES += \ | |||||
| runtime_mode_service_tests.cpp \ | runtime_mode_service_tests.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.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/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -24,6 +26,8 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.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/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||