| @@ -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 \ | |||
| @@ -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 "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); | |||
| } | |||
| @@ -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_; | |||
| }; | |||
| @@ -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 \ | |||
| ../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 \ | |||