| @@ -85,3 +85,9 @@ void VirtualRegisterRepository::clear() | |||||
| bits_.fill(false); | bits_.fill(false); | ||||
| words_.fill(0); | words_.fill(0); | ||||
| } | } | ||||
| void VirtualRegisterRepository::copyFrom(const VirtualRegisterRepository &source) | |||||
| { | |||||
| bits_ = source.bits_; | |||||
| words_ = source.words_; | |||||
| } | |||||
| @@ -27,6 +27,8 @@ public: | |||||
| // 将所有虚拟寄存器恢复为默认值,开始新的离线会话 | // 将所有虚拟寄存器恢复为默认值,开始新的离线会话 | ||||
| void clear(); | void clear(); | ||||
| // 从另一份虚拟寄存器复制完整的 M/D 快照 | |||||
| void copyFrom(const VirtualRegisterRepository &source); | |||||
| private: | private: | ||||
| static constexpr std::size_t kRegisterCount = | static constexpr std::size_t kRegisterCount = | ||||
| @@ -60,6 +60,8 @@ int main(int argc, char *argv[]) | |||||
| // 在内存中保存离线仿真使用的 M、D 寄存器值,不会写入真实 PLC | // 在内存中保存离线仿真使用的 M、D 寄存器值,不会写入真实 PLC | ||||
| VirtualRegisterRepository virtual_register_repository; | VirtualRegisterRepository virtual_register_repository; | ||||
| // 保存编辑态确认的离线初始值,不参与仿真扫描和工程持久化 | |||||
| VirtualRegisterRepository offline_initial_repository; | |||||
| // 保存从真实 PLC 最近一次读取到的 M、D 值,并转交写入请求 | // 保存从真实 PLC 最近一次读取到的 M、D 值,并转交写入请求 | ||||
| PlcRegisterRepository plc_register_repository; | PlcRegisterRepository plc_register_repository; | ||||
| // 给界面提供统一的寄存器入口,启动时先使用上面的离线寄存器 | // 给界面提供统一的寄存器入口,启动时先使用上面的离线寄存器 | ||||
| @@ -79,8 +81,11 @@ int main(int argc, char *argv[]) | |||||
| RegisterCommentService register_comment_service(project_service); | RegisterCommentService register_comment_service(project_service); | ||||
| // 处理自由监控列表中的寄存器读取和单点写入 | // 处理自由监控列表中的寄存器读取和单点写入 | ||||
| RegisterMonitorService register_monitor_service(active_register_repository); | RegisterMonitorService register_monitor_service(active_register_repository); | ||||
| register_monitor_service.setOfflineInitialRepository(offline_initial_repository); | |||||
| register_monitor_service.setOfflineInitialCaptureEnabled(true); | |||||
| // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 | // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 | ||||
| OfflineSimulationService offline_simulation_service(virtual_register_repository); | |||||
| OfflineSimulationService offline_simulation_service( | |||||
| virtual_register_repository, &offline_initial_repository); | |||||
| // 从 PLC 缓存推算本地梯形图轨迹,输出只写内部临时仓库 | // 从 PLC 缓存推算本地梯形图轨迹,输出只写内部临时仓库 | ||||
| OnlineLogicMonitorService online_logic_monitor_service(plc_register_repository); | OnlineLogicMonitorService online_logic_monitor_service(plc_register_repository); | ||||
| // 管理三种运行模式,并控制离线仿真和真机只读轨迹的生命周期 | // 管理三种运行模式,并控制离线仿真和真机只读轨迹的生命周期 | ||||
| @@ -4,9 +4,11 @@ | |||||
| OfflineSimulationService::OfflineSimulationService( | OfflineSimulationService::OfflineSimulationService( | ||||
| VirtualRegisterRepository &repository, | VirtualRegisterRepository &repository, | ||||
| VirtualRegisterRepository *initial_repository, | |||||
| QObject *parent) | QObject *parent) | ||||
| : QObject(parent), | : QObject(parent), | ||||
| repository_(repository) | |||||
| repository_(repository), | |||||
| initial_repository_(initial_repository) | |||||
| { | { | ||||
| timer_.setInterval(kDefaultScanIntervalMs); | timer_.setInterval(kDefaultScanIntervalMs); | ||||
| timer_.setTimerType(Qt::PreciseTimer); | timer_.setTimerType(Qt::PreciseTimer); | ||||
| @@ -31,10 +33,14 @@ SimulationStartResult OfflineSimulationService::start( | |||||
| return {false, SimulationStartError::InvalidLogic, validation}; | return {false, SimulationStartError::InvalidLogic, validation}; | ||||
| } | } | ||||
| // 重启会话时清空沿触发状态和虚拟 M/D,避免继承上次运行现场 | |||||
| // 重启会话时清空沿触发状态和运行值,再装载编辑态确认的初始值 | |||||
| timer_.stop(); | timer_.stop(); | ||||
| executor_.resetRuntime(); | executor_.resetRuntime(); | ||||
| repository_.clear(); | repository_.clear(); | ||||
| if (initial_repository_ != nullptr) | |||||
| { | |||||
| repository_.copyFrom(*initial_repository_); | |||||
| } | |||||
| logic_snapshot_ = std::move(snapshot); | logic_snapshot_ = std::move(snapshot); | ||||
| successful_scan_count_ = 0; | successful_scan_count_ = 0; | ||||
| last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; | last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; | ||||
| @@ -45,6 +51,15 @@ SimulationStartResult OfflineSimulationService::start( | |||||
| return {true, SimulationStartError::None, {true, LogicScanError::None, {}, {}, {}, {}}}; | return {true, SimulationStartError::None, {true, LogicScanError::None, {}, {}, {}, {}}}; | ||||
| } | } | ||||
| void OfflineSimulationService::restoreInitialValues() | |||||
| { | |||||
| repository_.clear(); | |||||
| if (initial_repository_ != nullptr) | |||||
| { | |||||
| repository_.copyFrom(*initial_repository_); | |||||
| } | |||||
| } | |||||
| void OfflineSimulationService::stop() | void OfflineSimulationService::stop() | ||||
| { | { | ||||
| timer_.stop(); | timer_.stop(); | ||||
| @@ -54,6 +69,8 @@ void OfflineSimulationService::stop() | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| // 运行结束后丢弃本轮输出,编辑态继续看到下一轮要使用的初始值 | |||||
| restoreInitialValues(); | |||||
| state_ = SimulationState::Stopped; | state_ = SimulationState::Stopped; | ||||
| last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; | last_error_ = {true, LogicScanError::None, {}, {}, {}, {}}; | ||||
| emit stateChanged(); | emit stateChanged(); | ||||
| @@ -45,10 +45,12 @@ public: | |||||
| /** | /** | ||||
| * @brief 创建离线仿真服务 | * @brief 创建离线仿真服务 | ||||
| * @param repository 离线模式使用的虚拟 M/D 寄存器仓库 | * @param repository 离线模式使用的虚拟 M/D 寄存器仓库 | ||||
| * @param initial_repository 编辑态确认的离线初始值仓库,可为空以保持旧的全清零行为 | |||||
| * @param parent Qt 对象父级 | * @param parent Qt 对象父级 | ||||
| */ | */ | ||||
| explicit OfflineSimulationService( | explicit OfflineSimulationService( | ||||
| VirtualRegisterRepository &repository, | VirtualRegisterRepository &repository, | ||||
| VirtualRegisterRepository *initial_repository = nullptr, | |||||
| QObject *parent = nullptr); | QObject *parent = nullptr); | ||||
| // 复制工程逻辑作为本次会话快照并启动定时扫描;启动失败时不进入运行态 | // 复制工程逻辑作为本次会话快照并启动定时扫描;启动失败时不进入运行态 | ||||
| @@ -57,6 +59,8 @@ public: | |||||
| void stop(); | void stop(); | ||||
| // 立即执行一轮扫描;非 Running 状态调用会返回失败结果 | // 立即执行一轮扫描;非 Running 状态调用会返回失败结果 | ||||
| LogicScanResult executeOnce(); | LogicScanResult executeOnce(); | ||||
| // 将当前会话的离线初始值恢复到运行仓库 | |||||
| void restoreInitialValues(); | |||||
| // 返回当前离线仿真生命周期状态 | // 返回当前离线仿真生命周期状态 | ||||
| SimulationState state() const; | SimulationState state() const; | ||||
| @@ -82,6 +86,7 @@ private: | |||||
| void enterFault(const LogicScanResult &error); | void enterFault(const LogicScanResult &error); | ||||
| VirtualRegisterRepository &repository_; // 不拥有的虚拟寄存器仓库 | VirtualRegisterRepository &repository_; // 不拥有的虚拟寄存器仓库 | ||||
| VirtualRegisterRepository *initial_repository_ = nullptr; // 不拥有的离线初始值仓库 | |||||
| SoftwareLogicExecutor executor_; // 执行逻辑校验和扫描的领域执行器 | SoftwareLogicExecutor executor_; // 执行逻辑校验和扫描的领域执行器 | ||||
| QTimer timer_; // 按固定周期触发离线扫描 | QTimer timer_; // 按固定周期触发离线扫描 | ||||
| std::vector<ControlLogic> logic_snapshot_; // 本次仿真使用的逻辑快照 | std::vector<ControlLogic> logic_snapshot_; // 本次仿真使用的逻辑快照 | ||||
| @@ -1,5 +1,7 @@ | |||||
| #include "register_monitor_service.h" | #include "register_monitor_service.h" | ||||
| #include "domain/virtual_register_repository.h" | |||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <cmath> | #include <cmath> | ||||
| #include <optional> | #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( | RegisterMonitorResult RegisterMonitorService::addRange( | ||||
| const std::string &start_address, int count) | const std::string &start_address, int count) | ||||
| { | { | ||||
| @@ -175,34 +188,32 @@ RegisterMonitorWriteResult RegisterMonitorService::writeBit( | |||||
| const RegisterAddress &address, bool value) | const RegisterAddress &address, bool value) | ||||
| { | { | ||||
| const RegisterWriteResult result = repository_.writeBit(address, 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 区" | ? "位写入只允许访问 M 区" | ||||
| : result.error == RegisterError::InvalidAddress | : result.error == RegisterError::InvalidAddress | ||||
| ? "写入地址无效" | ? "写入地址无效" | ||||
| : result.error == RegisterError::Unavailable | : result.error == RegisterError::Unavailable | ||||
| ? "当前寄存器源不可用" | ? "当前寄存器源不可用" | ||||
| : "寄存器写入请求被拒绝"}; | : "寄存器写入请求被拒绝"}; | ||||
| return captureInitialBit(address, value, active_result); | |||||
| } | } | ||||
| RegisterMonitorWriteResult RegisterMonitorService::writeWord( | RegisterMonitorWriteResult RegisterMonitorService::writeWord( | ||||
| const RegisterAddress &address, std::int16_t value) | const RegisterAddress &address, std::int16_t value) | ||||
| { | { | ||||
| const RegisterWriteResult result = repository_.writeWord(address, 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 区" | ? "字写入只允许访问 D 区" | ||||
| : result.error == RegisterError::InvalidAddress | : result.error == RegisterError::InvalidAddress | ||||
| ? "写入地址无效" | ? "写入地址无效" | ||||
| : result.error == RegisterError::Unavailable | : result.error == RegisterError::Unavailable | ||||
| ? "当前寄存器源不可用" | ? "当前寄存器源不可用" | ||||
| : "寄存器写入请求被拒绝"}; | : "寄存器写入请求被拒绝"}; | ||||
| return captureInitialWord(address, value, active_result); | |||||
| } | } | ||||
| RegisterMonitorWriteResult RegisterMonitorService::writeFloat( | RegisterMonitorWriteResult RegisterMonitorService::writeFloat( | ||||
| @@ -216,12 +227,51 @@ RegisterMonitorWriteResult RegisterMonitorService::writeFloat( | |||||
| } | } | ||||
| const RegisterWriteResult result = repository_.writeWordPair( | const RegisterWriteResult result = repository_.writeWordPair( | ||||
| address, Float32Codec::encode(value)); | 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 | const std::vector<RegisterAddress> &RegisterMonitorService::addresses() const | ||||
| @@ -8,6 +8,8 @@ | |||||
| #include <string> | #include <string> | ||||
| #include <vector> | #include <vector> | ||||
| class VirtualRegisterRepository; | |||||
| // 自由监控列表编辑的失败分类 | // 自由监控列表编辑的失败分类 | ||||
| enum class RegisterMonitorError | enum class RegisterMonitorError | ||||
| { | { | ||||
| @@ -38,7 +40,7 @@ struct RegisterMonitorWriteResult | |||||
| /** | /** | ||||
| * @brief 编排自由监控地址列表和寄存器仓库读写 | * @brief 编排自由监控地址列表和寄存器仓库读写 | ||||
| * | * | ||||
| * 地址列表只属于当前运行会话,不写入工程文件;仓库由当前运行模式注入 | |||||
| * 地址列表和离线初始值只属于当前运行会话,不写入工程文件;仓库由当前运行模式注入 | |||||
| */ | */ | ||||
| class RegisterMonitorService | class RegisterMonitorService | ||||
| { | { | ||||
| @@ -49,6 +51,11 @@ public: | |||||
| */ | */ | ||||
| explicit RegisterMonitorService(RegisterRepository &repository); | explicit RegisterMonitorService(RegisterRepository &repository); | ||||
| // 配置编辑态离线初始值仓库;该仓库只保存在当前进程内 | |||||
| void setOfflineInitialRepository(VirtualRegisterRepository &repository); | |||||
| // 编辑态开启时,监控写入同时更新离线初始值 | |||||
| void setOfflineInitialCaptureEnabled(bool enabled); | |||||
| /** | /** | ||||
| * @brief 从起始地址连续添加同一区域的多个地址 | * @brief 从起始地址连续添加同一区域的多个地址 | ||||
| * @param start_address 文本地址,例如 M0 或 D100 | * @param start_address 文本地址,例如 M0 或 D100 | ||||
| @@ -108,7 +115,19 @@ public: | |||||
| void setAddressesChangedCallback(std::function<void()> callback); | void setAddressesChangedCallback(std::function<void()> callback); | ||||
| private: | 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_; // 当前运行模式的寄存器仓库,不由服务拥有 | RegisterRepository &repository_; // 当前运行模式的寄存器仓库,不由服务拥有 | ||||
| VirtualRegisterRepository *offline_initial_repository_ = nullptr; | |||||
| bool capture_offline_initial_values_ = false; | |||||
| RegisterMonitorModel model_; // 当前会话的去重监控地址列表 | RegisterMonitorModel model_; // 当前会话的去重监控地址列表 | ||||
| std::function<void()> addresses_changed_callback_; // 地址变化后的轮询刷新通知 | std::function<void()> addresses_changed_callback_; // 地址变化后的轮询刷新通知 | ||||
| }; | }; | ||||
| @@ -73,7 +73,12 @@ void FreeMonitorWidget::refreshValues( | |||||
| QString source = tr("数据源:未启用"); | QString source = tr("数据源:未启用"); | ||||
| bool values_available = false; | bool values_available = false; | ||||
| bool communication_fault = 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"); | source = tr("数据源:虚拟 M/D"); | ||||
| values_available = true; | values_available = true; | ||||
| @@ -34,7 +34,7 @@ public: | |||||
| /** 释放 Designer 界面对象 */ | /** 释放 Designer 界面对象 */ | ||||
| ~FreeMonitorWidget() override; | ~FreeMonitorWidget() override; | ||||
| /** 真机未连接、通信故障或编辑态时应关闭写入入口 */ | |||||
| /** 根据当前模式和通信状态开启或关闭写入入口 */ | |||||
| void setWriteEnabled(bool enabled); | void setWriteEnabled(bool enabled); | ||||
| /** 根据运行模式和 PLC 状态刷新所有监控值 */ | /** 根据运行模式和 PLC 状态刷新所有监控值 */ | ||||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| @@ -6,6 +6,7 @@ | |||||
| #include "logic_editor_widget.h" | #include "logic_editor_widget.h" | ||||
| #include "logic_instruction_dialog.h" | #include "logic_instruction_dialog.h" | ||||
| #include "plc_connection_dialog.h" | #include "plc_connection_dialog.h" | ||||
| #include "free_monitor_widget.h" | |||||
| #include "runtime_monitor_widget.h" | #include "runtime_monitor_widget.h" | ||||
| #include "toolbar_icon_factory.h" | #include "toolbar_icon_factory.h" | ||||
| #include "project_workspace_controller.h" | #include "project_workspace_controller.h" | ||||
| @@ -30,6 +31,7 @@ | |||||
| #include <QApplication> | #include <QApplication> | ||||
| #include <QCloseEvent> | #include <QCloseEvent> | ||||
| #include <QDialogButtonBox> | #include <QDialogButtonBox> | ||||
| #include <QDockWidget> | |||||
| #include <QEvent> | #include <QEvent> | ||||
| #include <QFileDialog> | #include <QFileDialog> | ||||
| #include <QGraphicsScene> | #include <QGraphicsScene> | ||||
| @@ -352,6 +354,7 @@ void MainWindow::initializeUi() | |||||
| configureHmiEditor(); | configureHmiEditor(); | ||||
| configureLogicEditor(); | configureLogicEditor(); | ||||
| configureRuntimeMonitor(); | configureRuntimeMonitor(); | ||||
| configureDataMonitor(); | |||||
| configureProjectTree(); | configureProjectTree(); | ||||
| runtime_mode_service_.setPlcStatusChangedCallback( | runtime_mode_service_.setPlcStatusChangedCallback( | ||||
| [this] { schedulePlcStatusUpdate(); }); | [this] { schedulePlcStatusUpdate(); }); | ||||
| @@ -384,6 +387,7 @@ void MainWindow::initializeUi() | |||||
| MainWindow::~MainWindow() | MainWindow::~MainWindow() | ||||
| { | { | ||||
| qApp->removeEventFilter(this); | qApp->removeEventFilter(this); | ||||
| register_monitor_service_.setAddressesChangedCallback({}); | |||||
| runtime_mode_service_.setPlcStatusChangedCallback({}); | runtime_mode_service_.setPlcStatusChangedCallback({}); | ||||
| } | } | ||||
| @@ -871,6 +875,77 @@ void MainWindow::configureRuntimeMonitor() | |||||
| runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); | 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() | void MainWindow::configureProjectTree() | ||||
| { | { | ||||
| project_workspace_controller_ = std::make_unique<ProjectWorkspaceController>( | 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 ApplicationMode mode = runtime_mode_service_.mode(); | ||||
| const ModePolicy policy = runtime_mode_service_.policy(); | const ModePolicy policy = runtime_mode_service_.policy(); | ||||
| register_monitor_service_.setOfflineInitialCaptureEnabled( | |||||
| mode == ApplicationMode::Editing); | |||||
| restoreCurrentModeAction(); | restoreCurrentModeAction(); | ||||
| const bool running = mode != ApplicationMode::Editing; | const bool running = mode != ApplicationMode::Editing; | ||||
| if (running) | if (running) | ||||
| @@ -1869,6 +1946,7 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| register_status_label_->setText(tr("PLC:通信故障")); | register_status_label_->setText(tr("PLC:通信故障")); | ||||
| } | } | ||||
| updateSimulationUi(false); | updateSimulationUi(false); | ||||
| refreshDataMonitorUi(); | |||||
| statusBar()->showMessage(message, 4000); | statusBar()->showMessage(message, 4000); | ||||
| const bool duplicate = ui_->outputList->count() > 0 | const bool duplicate = ui_->outputList->count() > 0 | ||||
| && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message; | && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message; | ||||
| @@ -26,6 +26,8 @@ class QActionGroup; | |||||
| class QCloseEvent; | class QCloseEvent; | ||||
| class QEvent; | class QEvent; | ||||
| class QLabel; | class QLabel; | ||||
| class QDockWidget; | |||||
| class QTimer; | |||||
| namespace Ui { | namespace Ui { | ||||
| class MainWindow; | class MainWindow; | ||||
| @@ -46,6 +48,7 @@ class LogicEditorWidget; | |||||
| class RegisterMonitorService; | class RegisterMonitorService; | ||||
| class PlcDiscoveryGateway; | class PlcDiscoveryGateway; | ||||
| class RuntimeMonitorWidget; | class RuntimeMonitorWidget; | ||||
| class FreeMonitorWidget; | |||||
| class ProjectWorkspaceController; | class ProjectWorkspaceController; | ||||
| class PropertyPanelController; | class PropertyPanelController; | ||||
| class RuntimePanelController; | class RuntimePanelController; | ||||
| @@ -127,6 +130,8 @@ private: | |||||
| void configureLogicEditor(); | void configureLogicEditor(); | ||||
| /** 创建并连接运行监控面板 */ | /** 创建并连接运行监控面板 */ | ||||
| void configureRuntimeMonitor(); | void configureRuntimeMonitor(); | ||||
| /** 创建并连接编辑态数据监控面板 */ | |||||
| void configureDataMonitor(); | |||||
| /** 创建并连接工程树 */ | /** 创建并连接工程树 */ | ||||
| void configureProjectTree(); | void configureProjectTree(); | ||||
| /** 创建并连接控件属性编辑表单 */ | /** 创建并连接控件属性编辑表单 */ | ||||
| @@ -232,6 +237,8 @@ private: | |||||
| void restoreCurrentModeAction(); | void restoreCurrentModeAction(); | ||||
| /** 完成主窗口初始化和首次界面刷新 */ | /** 完成主窗口初始化和首次界面刷新 */ | ||||
| void initializeUi(); | void initializeUi(); | ||||
| /** 刷新编辑态数据监控的模式、权限和当前值 */ | |||||
| void refreshDataMonitorUi(); | |||||
| /** Qt Designer 生成的主窗口界面对象 */ | /** Qt Designer 生成的主窗口界面对象 */ | ||||
| std::unique_ptr<Ui::MainWindow> ui_; | std::unique_ptr<Ui::MainWindow> ui_; | ||||
| @@ -279,6 +286,10 @@ private: | |||||
| LogicEditorWidget *logic_editor_widget_ = nullptr; | LogicEditorWidget *logic_editor_widget_ = nullptr; | ||||
| /** 唯一的运行监控控件 */ | /** 唯一的运行监控控件 */ | ||||
| RuntimeMonitorWidget *runtime_monitor_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; | QLabel *mode_status_label_ = nullptr; | ||||
| /** 显示 PLC 连接状态的标签 */ | /** 显示 PLC 连接状态的标签 */ | ||||
| @@ -905,6 +905,28 @@ | |||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| </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"> | <widget class="QDockWidget" name="outputDock"> | ||||
| <property name="minimumSize"> | <property name="minimumSize"> | ||||
| <size> | <size> | ||||
| @@ -157,9 +157,10 @@ void RuntimePanelController::enterRuntime( | |||||
| if (!runtime_session_active_) | if (!runtime_session_active_) | ||||
| { | { | ||||
| runtime_monitor_widget_->setProjectObjects(page_id, logic_id); | 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_widget_->setMode(mode, plc_state); | ||||
| runtime_monitor_window_->showForRuntime(); | runtime_monitor_window_->showForRuntime(); | ||||
| } | } | ||||
| @@ -660,6 +660,37 @@ void testSimulationLifecycleSnapshotAndFault() | |||||
| simulation.stop(); | 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 | class FailingVirtualRegisterRepository final : public VirtualRegisterRepository | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -760,6 +791,7 @@ int main(int argc, char *argv[]) | |||||
| testConflictingCoilsAreRejected(); | testConflictingCoilsAreRejected(); | ||||
| testHmiSimulationClosedLoop(); | testHmiSimulationClosedLoop(); | ||||
| testSimulationLifecycleSnapshotAndFault(); | testSimulationLifecycleSnapshotAndFault(); | ||||
| testSimulationUsesInitialValuesAndDiscardsRuntimeOutputs(); | |||||
| testRepositoryFailureEntersFaultState(); | testRepositoryFailureEntersFaultState(); | ||||
| testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource(); | testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource(); | ||||
| testOnlineMonitorPreservesFaultAfterStopping(); | testOnlineMonitorPreservesFaultAfterStopping(); | ||||
| @@ -103,6 +103,33 @@ void testRegisterWrites() | |||||
| "monitor word writes must preserve signed 16-bit boundary values"); | "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() | void testFloat32Monitoring() | ||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| @@ -151,6 +178,7 @@ int main() | |||||
| testRangeManagement(); | testRangeManagement(); | ||||
| testSharedActiveRepositoryValues(); | testSharedActiveRepositoryValues(); | ||||
| testRegisterWrites(); | testRegisterWrites(); | ||||
| testOfflineInitialValueCapture(); | |||||
| testFloat32Monitoring(); | testFloat32Monitoring(); | ||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||