From bf1961ea1c45f0ebcb092ef9923b26aec365dc8a Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Tue, 25 Aug 2026 20:21:44 +0800 Subject: [PATCH] feat: add editable register monitoring --- .../domain/virtual_register_repository.cpp | 6 ++ app/src/domain/virtual_register_repository.h | 2 + app/src/main.cpp | 7 +- .../services/offline_simulation_service.cpp | 21 ++++- app/src/services/offline_simulation_service.h | 5 ++ app/src/services/register_monitor_service.cpp | 78 +++++++++++++++---- app/src/services/register_monitor_service.h | 21 ++++- app/src/ui/free_monitor_widget.cpp | 7 +- app/src/ui/free_monitor_widget.h | 2 +- app/src/ui/main_window.cpp | 78 +++++++++++++++++++ app/src/ui/main_window.h | 11 +++ app/src/ui/main_window.ui | 22 ++++++ app/src/ui/runtime_panel_controller.cpp | 5 +- .../offline_simulation_service_tests.cpp | 32 ++++++++ app/tests/register_monitor_service_tests.cpp | 28 +++++++ 15 files changed, 303 insertions(+), 22 deletions(-) diff --git a/app/src/domain/virtual_register_repository.cpp b/app/src/domain/virtual_register_repository.cpp index 6503515..091932c 100644 --- a/app/src/domain/virtual_register_repository.cpp +++ b/app/src/domain/virtual_register_repository.cpp @@ -85,3 +85,9 @@ void VirtualRegisterRepository::clear() bits_.fill(false); words_.fill(0); } + +void VirtualRegisterRepository::copyFrom(const VirtualRegisterRepository &source) +{ + bits_ = source.bits_; + words_ = source.words_; +} diff --git a/app/src/domain/virtual_register_repository.h b/app/src/domain/virtual_register_repository.h index 498097d..cd7a587 100644 --- a/app/src/domain/virtual_register_repository.h +++ b/app/src/domain/virtual_register_repository.h @@ -27,6 +27,8 @@ public: // 将所有虚拟寄存器恢复为默认值,开始新的离线会话 void clear(); + // 从另一份虚拟寄存器复制完整的 M/D 快照 + void copyFrom(const VirtualRegisterRepository &source); private: static constexpr std::size_t kRegisterCount = diff --git a/app/src/main.cpp b/app/src/main.cpp index 0565898..eaea52a 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -60,6 +60,8 @@ int main(int argc, char *argv[]) // 在内存中保存离线仿真使用的 M、D 寄存器值,不会写入真实 PLC VirtualRegisterRepository virtual_register_repository; + // 保存编辑态确认的离线初始值,不参与仿真扫描和工程持久化 + VirtualRegisterRepository offline_initial_repository; // 保存从真实 PLC 最近一次读取到的 M、D 值,并转交写入请求 PlcRegisterRepository plc_register_repository; // 给界面提供统一的寄存器入口,启动时先使用上面的离线寄存器 @@ -79,8 +81,11 @@ int main(int argc, char *argv[]) RegisterCommentService register_comment_service(project_service); // 处理自由监控列表中的寄存器读取和单点写入 RegisterMonitorService register_monitor_service(active_register_repository); + register_monitor_service.setOfflineInitialRepository(offline_initial_repository); + register_monitor_service.setOfflineInitialCaptureEnabled(true); // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 - OfflineSimulationService offline_simulation_service(virtual_register_repository); + OfflineSimulationService offline_simulation_service( + virtual_register_repository, &offline_initial_repository); // 从 PLC 缓存推算本地梯形图轨迹,输出只写内部临时仓库 OnlineLogicMonitorService online_logic_monitor_service(plc_register_repository); // 管理三种运行模式,并控制离线仿真和真机只读轨迹的生命周期 diff --git a/app/src/services/offline_simulation_service.cpp b/app/src/services/offline_simulation_service.cpp index d3c0371..857559f 100644 --- a/app/src/services/offline_simulation_service.cpp +++ b/app/src/services/offline_simulation_service.cpp @@ -4,9 +4,11 @@ OfflineSimulationService::OfflineSimulationService( VirtualRegisterRepository &repository, + VirtualRegisterRepository *initial_repository, QObject *parent) : QObject(parent), - repository_(repository) + repository_(repository), + initial_repository_(initial_repository) { timer_.setInterval(kDefaultScanIntervalMs); timer_.setTimerType(Qt::PreciseTimer); @@ -31,10 +33,14 @@ SimulationStartResult OfflineSimulationService::start( return {false, SimulationStartError::InvalidLogic, validation}; } - // 重启会话时清空沿触发状态和虚拟 M/D,避免继承上次运行现场 + // 重启会话时清空沿触发状态和运行值,再装载编辑态确认的初始值 timer_.stop(); executor_.resetRuntime(); repository_.clear(); + if (initial_repository_ != nullptr) + { + repository_.copyFrom(*initial_repository_); + } logic_snapshot_ = std::move(snapshot); successful_scan_count_ = 0; last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; @@ -45,6 +51,15 @@ SimulationStartResult OfflineSimulationService::start( return {true, SimulationStartError::None, {true, LogicScanError::None, {}, {}, {}, {}}}; } +void OfflineSimulationService::restoreInitialValues() +{ + repository_.clear(); + if (initial_repository_ != nullptr) + { + repository_.copyFrom(*initial_repository_); + } +} + void OfflineSimulationService::stop() { timer_.stop(); @@ -54,6 +69,8 @@ void OfflineSimulationService::stop() { return; } + // 运行结束后丢弃本轮输出,编辑态继续看到下一轮要使用的初始值 + restoreInitialValues(); state_ = SimulationState::Stopped; last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; emit stateChanged(); diff --git a/app/src/services/offline_simulation_service.h b/app/src/services/offline_simulation_service.h index 9ce46f8..19b5a43 100644 --- a/app/src/services/offline_simulation_service.h +++ b/app/src/services/offline_simulation_service.h @@ -45,10 +45,12 @@ public: /** * @brief 创建离线仿真服务 * @param repository 离线模式使用的虚拟 M/D 寄存器仓库 + * @param initial_repository 编辑态确认的离线初始值仓库,可为空以保持旧的全清零行为 * @param parent Qt 对象父级 */ explicit OfflineSimulationService( VirtualRegisterRepository &repository, + VirtualRegisterRepository *initial_repository = nullptr, QObject *parent = nullptr); // 复制工程逻辑作为本次会话快照并启动定时扫描;启动失败时不进入运行态 @@ -57,6 +59,8 @@ public: void stop(); // 立即执行一轮扫描;非 Running 状态调用会返回失败结果 LogicScanResult executeOnce(); + // 将当前会话的离线初始值恢复到运行仓库 + void restoreInitialValues(); // 返回当前离线仿真生命周期状态 SimulationState state() const; @@ -82,6 +86,7 @@ private: void enterFault(const LogicScanResult &error); VirtualRegisterRepository &repository_; // 不拥有的虚拟寄存器仓库 + VirtualRegisterRepository *initial_repository_ = nullptr; // 不拥有的离线初始值仓库 SoftwareLogicExecutor executor_; // 执行逻辑校验和扫描的领域执行器 QTimer timer_; // 按固定周期触发离线扫描 std::vector logic_snapshot_; // 本次仿真使用的逻辑快照 diff --git a/app/src/services/register_monitor_service.cpp b/app/src/services/register_monitor_service.cpp index d135750..978b0c8 100644 --- a/app/src/services/register_monitor_service.cpp +++ b/app/src/services/register_monitor_service.cpp @@ -1,5 +1,7 @@ #include "register_monitor_service.h" +#include "domain/virtual_register_repository.h" + #include #include #include @@ -31,6 +33,17 @@ RegisterMonitorService::RegisterMonitorService(RegisterRepository &repository) { } +void RegisterMonitorService::setOfflineInitialRepository( + VirtualRegisterRepository &repository) +{ + offline_initial_repository_ = &repository; +} + +void RegisterMonitorService::setOfflineInitialCaptureEnabled(bool enabled) +{ + capture_offline_initial_values_ = enabled; +} + RegisterMonitorResult RegisterMonitorService::addRange( const std::string &start_address, int count) { @@ -175,34 +188,32 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit( const RegisterAddress &address, bool value) { const RegisterWriteResult result = repository_.writeBit(address, value); - if (result.succeeded) - { - return {true, RegisterError::None, {}}; - } - return {false, result.error, result.error == RegisterError::AreaMismatch + const RegisterMonitorWriteResult active_result = result.succeeded + ? RegisterMonitorWriteResult{true, RegisterError::None, {}} + : RegisterMonitorWriteResult{false, result.error, result.error == RegisterError::AreaMismatch ? "位写入只允许访问 M 区" : result.error == RegisterError::InvalidAddress ? "写入地址无效" : result.error == RegisterError::Unavailable ? "当前寄存器源不可用" : "寄存器写入请求被拒绝"}; + return captureInitialBit(address, value, active_result); } RegisterMonitorWriteResult RegisterMonitorService::writeWord( const RegisterAddress &address, std::int16_t value) { const RegisterWriteResult result = repository_.writeWord(address, value); - if (result.succeeded) - { - return {true, RegisterError::None, {}}; - } - return {false, result.error, result.error == RegisterError::AreaMismatch + const RegisterMonitorWriteResult active_result = result.succeeded + ? RegisterMonitorWriteResult{true, RegisterError::None, {}} + : RegisterMonitorWriteResult{false, result.error, result.error == RegisterError::AreaMismatch ? "字写入只允许访问 D 区" : result.error == RegisterError::InvalidAddress ? "写入地址无效" : result.error == RegisterError::Unavailable ? "当前寄存器源不可用" : "寄存器写入请求被拒绝"}; + return captureInitialWord(address, value, active_result); } RegisterMonitorWriteResult RegisterMonitorService::writeFloat( @@ -216,12 +227,51 @@ RegisterMonitorWriteResult RegisterMonitorService::writeFloat( } const RegisterWriteResult result = repository_.writeWordPair( address, Float32Codec::encode(value)); - if (result.succeeded) + const RegisterMonitorWriteResult active_result = result.succeeded + ? RegisterMonitorWriteResult{true, RegisterError::None, {}} + : RegisterMonitorWriteResult{false, result.error, result.error == RegisterError::Unavailable + ? "当前寄存器源不可用" : "寄存器写入请求被拒绝"}; + return captureInitialFloat(address, value, active_result); +} + +RegisterMonitorWriteResult RegisterMonitorService::captureInitialBit( + const RegisterAddress &address, bool value, + const RegisterMonitorWriteResult &active_result) +{ + if (!active_result.succeeded || !capture_offline_initial_values_ + || offline_initial_repository_ == nullptr) + { + return active_result; + } + offline_initial_repository_->writeBit(address, value); + return active_result; +} + +RegisterMonitorWriteResult RegisterMonitorService::captureInitialWord( + const RegisterAddress &address, std::int16_t value, + const RegisterMonitorWriteResult &active_result) +{ + if (!active_result.succeeded || !capture_offline_initial_values_ + || offline_initial_repository_ == nullptr) { - return {true, RegisterError::None, {}}; + return active_result; } - return {false, result.error, result.error == RegisterError::Unavailable - ? "当前寄存器源不可用" : "寄存器写入请求被拒绝"}; + offline_initial_repository_->writeWord(address, value); + return active_result; +} + +RegisterMonitorWriteResult RegisterMonitorService::captureInitialFloat( + const RegisterAddress &address, float value, + const RegisterMonitorWriteResult &active_result) +{ + if (!active_result.succeeded || !capture_offline_initial_values_ + || offline_initial_repository_ == nullptr) + { + return active_result; + } + offline_initial_repository_->writeWordPair( + address, Float32Codec::encode(value)); + return active_result; } const std::vector &RegisterMonitorService::addresses() const diff --git a/app/src/services/register_monitor_service.h b/app/src/services/register_monitor_service.h index bbc3642..fbe54b0 100644 --- a/app/src/services/register_monitor_service.h +++ b/app/src/services/register_monitor_service.h @@ -8,6 +8,8 @@ #include #include +class VirtualRegisterRepository; + // 自由监控列表编辑的失败分类 enum class RegisterMonitorError { @@ -38,7 +40,7 @@ struct RegisterMonitorWriteResult /** * @brief 编排自由监控地址列表和寄存器仓库读写 * - * 地址列表只属于当前运行会话,不写入工程文件;仓库由当前运行模式注入 + * 地址列表和离线初始值只属于当前运行会话,不写入工程文件;仓库由当前运行模式注入 */ class RegisterMonitorService { @@ -49,6 +51,11 @@ public: */ explicit RegisterMonitorService(RegisterRepository &repository); + // 配置编辑态离线初始值仓库;该仓库只保存在当前进程内 + void setOfflineInitialRepository(VirtualRegisterRepository &repository); + // 编辑态开启时,监控写入同时更新离线初始值 + void setOfflineInitialCaptureEnabled(bool enabled); + /** * @brief 从起始地址连续添加同一区域的多个地址 * @param start_address 文本地址,例如 M0 或 D100 @@ -108,7 +115,19 @@ public: void setAddressesChangedCallback(std::function callback); private: + RegisterMonitorWriteResult captureInitialBit( + const RegisterAddress &address, bool value, + const RegisterMonitorWriteResult &active_result); + RegisterMonitorWriteResult captureInitialWord( + const RegisterAddress &address, std::int16_t value, + const RegisterMonitorWriteResult &active_result); + RegisterMonitorWriteResult captureInitialFloat( + const RegisterAddress &address, float value, + const RegisterMonitorWriteResult &active_result); + RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有 + VirtualRegisterRepository *offline_initial_repository_ = nullptr; + bool capture_offline_initial_values_ = false; RegisterMonitorModel model_; // 当前会话的去重监控地址列表 std::function addresses_changed_callback_; // 地址变化后的轮询刷新通知 }; diff --git a/app/src/ui/free_monitor_widget.cpp b/app/src/ui/free_monitor_widget.cpp index e703c2f..895eb30 100644 --- a/app/src/ui/free_monitor_widget.cpp +++ b/app/src/ui/free_monitor_widget.cpp @@ -73,7 +73,12 @@ void FreeMonitorWidget::refreshValues( QString source = tr("数据源:未启用"); bool values_available = false; bool communication_fault = false; - if (mode == ApplicationMode::OfflineRunning) + if (mode == ApplicationMode::Editing) + { + source = tr("数据源:离线初始值 · 虚拟 M/D"); + values_available = true; + } + else if (mode == ApplicationMode::OfflineRunning) { source = tr("数据源:虚拟 M/D"); values_available = true; diff --git a/app/src/ui/free_monitor_widget.h b/app/src/ui/free_monitor_widget.h index 5ecd095..fde2eb9 100644 --- a/app/src/ui/free_monitor_widget.h +++ b/app/src/ui/free_monitor_widget.h @@ -34,7 +34,7 @@ public: /** 释放 Designer 界面对象 */ ~FreeMonitorWidget() override; - /** 真机未连接、通信故障或编辑态时应关闭写入入口 */ + /** 根据当前模式和通信状态开启或关闭写入入口 */ void setWriteEnabled(bool enabled); /** 根据运行模式和 PLC 状态刷新所有监控值 */ void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 5a9e43b..fd5d8e5 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -6,6 +6,7 @@ #include "logic_editor_widget.h" #include "logic_instruction_dialog.h" #include "plc_connection_dialog.h" +#include "free_monitor_widget.h" #include "runtime_monitor_widget.h" #include "toolbar_icon_factory.h" #include "project_workspace_controller.h" @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -352,6 +354,7 @@ void MainWindow::initializeUi() configureHmiEditor(); configureLogicEditor(); configureRuntimeMonitor(); + configureDataMonitor(); configureProjectTree(); runtime_mode_service_.setPlcStatusChangedCallback( [this] { schedulePlcStatusUpdate(); }); @@ -384,6 +387,7 @@ void MainWindow::initializeUi() MainWindow::~MainWindow() { qApp->removeEventFilter(this); + register_monitor_service_.setAddressesChangedCallback({}); runtime_mode_service_.setPlcStatusChangedCallback({}); } @@ -871,6 +875,77 @@ void MainWindow::configureRuntimeMonitor() runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); } +void MainWindow::configureDataMonitor() +{ + data_monitor_dock_ = ui_->dataMonitorDock; + data_monitor_widget_ = new FreeMonitorWidget( + register_monitor_service_, data_monitor_dock_); + data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget")); + ui_->dataMonitorDockLayout->addWidget(data_monitor_widget_); + ui_->viewMenu->addAction(data_monitor_dock_->toggleViewAction()); + + connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged, + this, + [this] + { + runtime_mode_service_.setMonitorAddresses( + register_monitor_service_.pollAddresses(), + register_monitor_service_.float32Starts()); + refreshDataMonitorUi(); + }); + connect(data_monitor_widget_, &FreeMonitorWidget::operationMessage, + this, + [this](const QString &message) + { + statusBar()->showMessage(message, 4000); + appendOutputMessage(message); + }); + register_monitor_service_.setAddressesChangedCallback( + [this] + { + if (data_monitor_widget_ != nullptr) + { + data_monitor_widget_->reloadAddresses(); + } + if (runtime_monitor_widget_ != nullptr + && runtime_monitor_widget_->freeMonitorWidget() != nullptr) + { + runtime_monitor_widget_->freeMonitorWidget()->reloadAddresses(); + } + runtime_mode_service_.setMonitorAddresses( + register_monitor_service_.pollAddresses(), + register_monitor_service_.float32Starts()); + }); + data_monitor_refresh_timer_ = new QTimer(this); + data_monitor_refresh_timer_->setInterval(150); + connect(data_monitor_refresh_timer_, &QTimer::timeout, + this, &MainWindow::refreshDataMonitorUi); + data_monitor_refresh_timer_->start(); + refreshDataMonitorUi(); +} + +void MainWindow::refreshDataMonitorUi() +{ + if (data_monitor_widget_ == nullptr) + { + return; + } + const ApplicationMode mode = runtime_mode_service_.mode(); + const bool online_connected = mode == ApplicationMode::OnlineRunning + && runtime_mode_service_.plcConnectionState() + == PlcConnectionState::Connected; + data_monitor_widget_->setWriteEnabled( + mode == ApplicationMode::Editing + || mode == ApplicationMode::OfflineRunning + || online_connected); + data_monitor_widget_->refreshValues( + mode, runtime_mode_service_.plcConnectionState()); + if (data_monitor_dock_ != nullptr) + { + data_monitor_dock_->setVisible(mode == ApplicationMode::Editing); + } +} + void MainWindow::configureProjectTree() { project_workspace_controller_ = std::make_unique( @@ -1751,6 +1826,8 @@ void MainWindow::updateModeUi(const QString &message) // 按服务层策略统一启用/禁用编辑入口,避免单个按钮遗漏状态同步 const ApplicationMode mode = runtime_mode_service_.mode(); const ModePolicy policy = runtime_mode_service_.policy(); + register_monitor_service_.setOfflineInitialCaptureEnabled( + mode == ApplicationMode::Editing); restoreCurrentModeAction(); const bool running = mode != ApplicationMode::Editing; if (running) @@ -1869,6 +1946,7 @@ void MainWindow::updateModeUi(const QString &message) register_status_label_->setText(tr("PLC:通信故障")); } updateSimulationUi(false); + refreshDataMonitorUi(); statusBar()->showMessage(message, 4000); const bool duplicate = ui_->outputList->count() > 0 && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message; diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index e59fab1..d38c4c2 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -26,6 +26,8 @@ class QActionGroup; class QCloseEvent; class QEvent; class QLabel; +class QDockWidget; +class QTimer; namespace Ui { class MainWindow; @@ -46,6 +48,7 @@ class LogicEditorWidget; class RegisterMonitorService; class PlcDiscoveryGateway; class RuntimeMonitorWidget; +class FreeMonitorWidget; class ProjectWorkspaceController; class PropertyPanelController; class RuntimePanelController; @@ -127,6 +130,8 @@ private: void configureLogicEditor(); /** 创建并连接运行监控面板 */ void configureRuntimeMonitor(); + /** 创建并连接编辑态数据监控面板 */ + void configureDataMonitor(); /** 创建并连接工程树 */ void configureProjectTree(); /** 创建并连接控件属性编辑表单 */ @@ -232,6 +237,8 @@ private: void restoreCurrentModeAction(); /** 完成主窗口初始化和首次界面刷新 */ void initializeUi(); + /** 刷新编辑态数据监控的模式、权限和当前值 */ + void refreshDataMonitorUi(); /** Qt Designer 生成的主窗口界面对象 */ std::unique_ptr ui_; @@ -279,6 +286,10 @@ private: LogicEditorWidget *logic_editor_widget_ = nullptr; /** 唯一的运行监控控件 */ RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; + /** 主窗口中的数据监控面板 */ + QDockWidget *data_monitor_dock_ = nullptr; + FreeMonitorWidget *data_monitor_widget_ = nullptr; + QTimer *data_monitor_refresh_timer_ = nullptr; /** 显示当前运行模式的状态标签 */ QLabel *mode_status_label_ = nullptr; /** 显示 PLC 连接状态的标签 */ diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index a2c04c9..6ce7a6f 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -905,6 +905,28 @@ + + + + 640 + 220 + + + + 数据监控 + + + 2 + + + + 6 + 6 + 6 + 6 + + + diff --git a/app/src/ui/runtime_panel_controller.cpp b/app/src/ui/runtime_panel_controller.cpp index 2438877..06b6936 100644 --- a/app/src/ui/runtime_panel_controller.cpp +++ b/app/src/ui/runtime_panel_controller.cpp @@ -157,9 +157,10 @@ void RuntimePanelController::enterRuntime( if (!runtime_session_active_) { runtime_monitor_widget_->setProjectObjects(page_id, logic_id); - // 运行窗口只创建一次,后续进入运行态复用同一个监控投影 - runtime_session_active_ = true; + // 运行窗口只创建一次,后续进入运行态复用同一个监控投影 + runtime_session_active_ = true; } + runtime_monitor_widget_->freeMonitorWidget()->reloadAddresses(); runtime_monitor_widget_->setMode(mode, plc_state); runtime_monitor_window_->showForRuntime(); } diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index 925bea5..db7c68b 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -660,6 +660,37 @@ void testSimulationLifecycleSnapshotAndFault() simulation.stop(); } +void testSimulationUsesInitialValuesAndDiscardsRuntimeOutputs() +{ + VirtualRegisterRepository repository; + VirtualRegisterRepository initial_repository; + OfflineSimulationService simulation(repository, &initial_repository); + const ControlLogic program = logic({ + rung("initial-rung", {{contact("initial-input", 0)}}, coil("runtime-output", 1))}); + + writeBit(initial_repository, 0, true); + writeWord(initial_repository, 10, 321); + require(simulation.start({program}).succeeded, + "simulation must start from the explicit initial repository"); + require(readBit(repository, 0) + && readWord(repository, 10) == 321, + "simulation startup must copy initial M/D values into its runtime repository"); + require(simulation.executeOnce().succeeded && readBit(repository, 1), + "simulation must execute against the copied initial values"); + simulation.stop(); + require(!readBit(repository, 1) + && readBit(repository, 0) + && readWord(repository, 10) == 321, + "stopping simulation must restore initial values instead of runtime outputs"); + + writeBit(initial_repository, 0, false); + require(simulation.start({program}).succeeded, + "simulation must support restart with changed initial values"); + require(!readBit(repository, 0) && !readBit(repository, 1), + "restart must use the latest initial values and discard the previous output"); + simulation.stop(); +} + class FailingVirtualRegisterRepository final : public VirtualRegisterRepository { public: @@ -760,6 +791,7 @@ int main(int argc, char *argv[]) testConflictingCoilsAreRejected(); testHmiSimulationClosedLoop(); testSimulationLifecycleSnapshotAndFault(); + testSimulationUsesInitialValuesAndDiscardsRuntimeOutputs(); testRepositoryFailureEntersFaultState(); testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource(); testOnlineMonitorPreservesFaultAfterStopping(); diff --git a/app/tests/register_monitor_service_tests.cpp b/app/tests/register_monitor_service_tests.cpp index 6af9c22..e721e7c 100644 --- a/app/tests/register_monitor_service_tests.cpp +++ b/app/tests/register_monitor_service_tests.cpp @@ -103,6 +103,33 @@ void testRegisterWrites() "monitor word writes must preserve signed 16-bit boundary values"); } +void testOfflineInitialValueCapture() +{ + VirtualRegisterRepository virtual_repository; + VirtualRegisterRepository initial_repository; + ActiveRegisterRepository active_repository(virtual_repository); + RegisterMonitorService service(active_repository); + service.setOfflineInitialRepository(initial_repository); + service.setOfflineInitialCaptureEnabled(true); + + require(service.writeBit({RegisterArea::M, 3}, true).succeeded + && service.writeWord({RegisterArea::D, 10}, -456).succeeded + && service.writeFloat({RegisterArea::D, 20}, 1.5f).succeeded, + "editing monitor writes must succeed against the virtual repository"); + require(initial_repository.readBit({RegisterArea::M, 3}).value + && initial_repository.readWord({RegisterArea::D, 10}).value == -456 + && std::fabs(Float32Codec::decode( + initial_repository.readWordPair({RegisterArea::D, 20}).values[0], + initial_repository.readWordPair({RegisterArea::D, 20}).values[1]) + .value_or(0.0f) - 1.5f) < 0.000001f, + "editing monitor writes must be captured as offline initial values"); + + service.setOfflineInitialCaptureEnabled(false); + require(service.writeBit({RegisterArea::M, 3}, false).succeeded + && initial_repository.readBit({RegisterArea::M, 3}).value, + "runtime writes must not overwrite the captured initial values"); +} + void testFloat32Monitoring() { VirtualRegisterRepository repository; @@ -151,6 +178,7 @@ int main() testRangeManagement(); testSharedActiveRepositoryValues(); testRegisterWrites(); + testOfflineInitialValueCapture(); testFloat32Monitoring(); } catch (const std::exception &error)