| @@ -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_; | |||
| } | |||
| @@ -27,6 +27,8 @@ public: | |||
| // 将所有虚拟寄存器恢复为默认值,开始新的离线会话 | |||
| void clear(); | |||
| // 从另一份虚拟寄存器复制完整的 M/D 快照 | |||
| void copyFrom(const VirtualRegisterRepository &source); | |||
| private: | |||
| static constexpr std::size_t kRegisterCount = | |||
| @@ -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); | |||
| // 管理三种运行模式,并控制离线仿真和真机只读轨迹的生命周期 | |||
| @@ -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(); | |||
| @@ -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<ControlLogic> logic_snapshot_; // 本次仿真使用的逻辑快照 | |||
| @@ -1,5 +1,7 @@ | |||
| #include "register_monitor_service.h" | |||
| #include "domain/virtual_register_repository.h" | |||
| #include <algorithm> | |||
| #include <cmath> | |||
| #include <optional> | |||
| @@ -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<RegisterAddress> &RegisterMonitorService::addresses() const | |||
| @@ -8,6 +8,8 @@ | |||
| #include <string> | |||
| #include <vector> | |||
| 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<void()> 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<void()> addresses_changed_callback_; // 地址变化后的轮询刷新通知 | |||
| }; | |||
| @@ -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; | |||
| @@ -34,7 +34,7 @@ public: | |||
| /** 释放 Designer 界面对象 */ | |||
| ~FreeMonitorWidget() override; | |||
| /** 真机未连接、通信故障或编辑态时应关闭写入入口 */ | |||
| /** 根据当前模式和通信状态开启或关闭写入入口 */ | |||
| void setWriteEnabled(bool enabled); | |||
| /** 根据运行模式和 PLC 状态刷新所有监控值 */ | |||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | |||
| @@ -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 <QApplication> | |||
| #include <QCloseEvent> | |||
| #include <QDialogButtonBox> | |||
| #include <QDockWidget> | |||
| #include <QEvent> | |||
| #include <QFileDialog> | |||
| #include <QGraphicsScene> | |||
| @@ -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<ProjectWorkspaceController>( | |||
| @@ -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; | |||
| @@ -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::MainWindow> 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 连接状态的标签 */ | |||
| @@ -905,6 +905,28 @@ | |||
| </layout> | |||
| </widget> | |||
| </widget> | |||
| <widget class="QDockWidget" name="dataMonitorDock"> | |||
| <property name="minimumSize"> | |||
| <size> | |||
| <width>640</width> | |||
| <height>220</height> | |||
| </size> | |||
| </property> | |||
| <property name="windowTitle"> | |||
| <string>数据监控</string> | |||
| </property> | |||
| <attribute name="dockWidgetArea"> | |||
| <number>2</number> | |||
| </attribute> | |||
| <widget class="QWidget" name="dataMonitorDockContents"> | |||
| <layout class="QVBoxLayout" name="dataMonitorDockLayout"> | |||
| <property name="leftMargin"><number>6</number></property> | |||
| <property name="topMargin"><number>6</number></property> | |||
| <property name="rightMargin"><number>6</number></property> | |||
| <property name="bottomMargin"><number>6</number></property> | |||
| </layout> | |||
| </widget> | |||
| </widget> | |||
| <widget class="QDockWidget" name="outputDock"> | |||
| <property name="minimumSize"> | |||
| <size> | |||
| @@ -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(); | |||
| } | |||
| @@ -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(); | |||
| @@ -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) | |||