diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 32b4702..1f03ed9 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -47,6 +47,7 @@ SOURCES += \ src/services/hmi_runtime_service.cpp \ src/services/software_logic_executor.cpp \ src/services/offline_simulation_service.cpp \ + src/services/online_logic_monitor_service.cpp \ src/services/runtime_mode_service.cpp \ src/services/plc_discovery_gateway.cpp \ src/services/register_monitor_service.cpp \ @@ -96,6 +97,7 @@ HEADERS += \ src/services/hmi_runtime_service.h \ src/services/software_logic_executor.h \ src/services/offline_simulation_service.h \ + src/services/online_logic_monitor_service.h \ src/services/runtime_mode_service.h \ src/services/plc_discovery_gateway.h \ src/services/register_monitor_service.h \ diff --git a/app/src/domain/runtime_state.h b/app/src/domain/runtime_state.h index 92937aa..16e34d0 100644 --- a/app/src/domain/runtime_state.h +++ b/app/src/domain/runtime_state.h @@ -52,12 +52,12 @@ constexpr ModePolicy policyForMode(ApplicationMode mode) } case ApplicationMode::OnlineRunning: { - // 真机运行态禁止改工程,使用 PLC 数据且禁止软件逻辑重复控制 PLC + // 真机运行态使用 PLC 数据,并在本地只读推算梯形图轨迹 return { false, // allowsProjectEditing 禁止编辑工程 false, // usesVirtualRegisters 不使用虚拟寄存器 true, // usesPlcRegisters 使用 PLC 寄存器 - false, // runsLogicExecutor 不运行软件逻辑执行器 + true, // runsLogicExecutor 运行只读本地轨迹执行器 true // requiresInitialPlcRead 必须先完成 PLC 初次读取 }; } @@ -76,7 +76,7 @@ enum class ModeTransitionError MustReturnToEditing, // 必须先返回编辑态 InitialPlcReadRequired, // 进入真机态前必须完成 PLC 初次读取 ProjectNotReady, // 工程存在未配置或未完成的运行项 - SimulationStartFailed // 离线执行器预检或启动失败 + SimulationStartFailed // 当前模式的本地执行器预检或启动失败 }; // 模式切换操作结果 diff --git a/app/src/infrastructure/plc_communication_service.cpp b/app/src/infrastructure/plc_communication_service.cpp index c8a216f..ce7d9da 100644 --- a/app/src/infrastructure/plc_communication_service.cpp +++ b/app/src/infrastructure/plc_communication_service.cpp @@ -312,12 +312,14 @@ void PlcCommunicationService::setCallbacks( std::function state_changed, std::function initial_read_changed, std::function cache_updated, + std::function poll_cycle_completed, std::function 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); // PLC 缓存更新后通知外部 + poll_cycle_completed_callback_ = std::move(poll_cycle_completed); error_reported_callback_ = std::move(error_reported); // 发生通信错误时通知外部 } @@ -444,6 +446,10 @@ void PlcCommunicationService::pollNextBlock() } } } + const bool poll_cycle_completed = isReadingState(state_) + && reply->error() == QModbusDevice::NoError + && !poll_update_pending_ + && next_poll_block_ == 0U; if (pending_reply_ == reply) { // 只有当前指针仍指向本次回复时才清空,避免误清新请求 @@ -455,6 +461,14 @@ void PlcCommunicationService::pollNextBlock() const std::vector addresses = pending_poll_addresses_; applyPollAddresses(addresses); } + if (poll_cycle_completed) + { + emit pollCycleCompleted(); + if (poll_cycle_completed_callback_) + { + poll_cycle_completed_callback_(); + } + } }); } diff --git a/app/src/infrastructure/plc_communication_service.h b/app/src/infrastructure/plc_communication_service.h index ac8859e..3d20433 100644 --- a/app/src/infrastructure/plc_communication_service.h +++ b/app/src/infrastructure/plc_communication_service.h @@ -55,6 +55,7 @@ public: std::function state_changed, std::function initial_read_changed, std::function cache_updated, + std::function poll_cycle_completed, std::function error_reported) override; signals: @@ -64,6 +65,8 @@ signals: void initialReadCompletedChanged(bool completed); // PLC 缓存收到新的成功读数 void cacheUpdated(); + // 全部轮询块成功更新一轮 + void pollCycleCompleted(); // 通信错误的可读提示文字 void communicationError(const QString &message); @@ -132,5 +135,6 @@ private: std::function state_changed_callback_; // 状态变化时调用 std::function initial_read_changed_callback_; // 首读资格变化时调用 std::function cache_updated_callback_; // 缓存更新后调用 + std::function poll_cycle_completed_callback_; // 完整轮询一轮后调用 std::function error_reported_callback_; // 发生错误时调用 }; diff --git a/app/src/main.cpp b/app/src/main.cpp index fdf37c5..0565898 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -22,6 +22,7 @@ #include "services/hmi_navigation_service.h" #include "services/logic_editor_service.h" #include "services/offline_simulation_service.h" +#include "services/online_logic_monitor_service.h" #include "services/project_service.h" #include "services/runtime_mode_service.h" #include "services/register_monitor_service.h" @@ -80,9 +81,11 @@ int main(int argc, char *argv[]) RegisterMonitorService register_monitor_service(active_register_repository); // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 OfflineSimulationService offline_simulation_service(virtual_register_repository); - // 管理编辑、离线运行和真机运行三种模式,并控制离线仿真的启停 + // 从 PLC 缓存推算本地梯形图轨迹,输出只写内部临时仓库 + OnlineLogicMonitorService online_logic_monitor_service(plc_register_repository); + // 管理三种运行模式,并控制离线仿真和真机只读轨迹的生命周期 RuntimeModeService runtime_mode_service( - project_service, offline_simulation_service); + project_service, offline_simulation_service, online_logic_monitor_service); // 把 PLC 通信和两套寄存器接入运行模式,切换模式时才能切换数据来源 runtime_mode_service.configurePlc( plc_communication_service, diff --git a/app/src/services/online_logic_monitor_service.cpp b/app/src/services/online_logic_monitor_service.cpp new file mode 100644 index 0000000..f3bd659 --- /dev/null +++ b/app/src/services/online_logic_monitor_service.cpp @@ -0,0 +1,209 @@ +#include "online_logic_monitor_service.h" + +#include +#include + +namespace { + +LogicScanResult success() +{ + return {true, LogicScanError::None, {}, {}, {}, {}}; +} + +LogicScanResult snapshotFailure(const RegisterAddress &address) +{ + return { + false, + LogicScanError::RegisterReadFailed, + "读取 PLC 缓存失败:" + address.toString(), + {}, + {}, + {}}; +} + +} // namespace + +OnlineLogicMonitorService::OnlineLogicMonitorService( + RegisterRepository &plc_repository, + QObject *parent) + : QObject(parent), + plc_repository_(plc_repository) +{ +} + +OnlineLogicMonitorStartResult OnlineLogicMonitorService::start( + const std::vector &logics) +{ + if (state_ == OnlineLogicMonitorState::Running) + { + return {false, OnlineLogicMonitorStartError::AlreadyRunning, {}}; + } + + std::vector snapshot = logics; + const LogicScanResult validation = executor_.validate(snapshot); + if (!validation.succeeded) + { + last_error_ = validation; + state_ = OnlineLogicMonitorState::Faulted; + emit stateChanged(); + return {false, OnlineLogicMonitorStartError::InvalidLogic, validation}; + } + + std::vector addresses; + for (const ControlLogic &logic : snapshot) + { + if (!logic.enabled) + { + continue; + } + for (const LadderRung &rung : logic.rungs) + { + std::vector 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) + { + collectRegisterAddressesForLogicNode(node->config, &addresses); + } + } + } + std::sort( + addresses.begin(), addresses.end(), + [](const RegisterAddress &left, const RegisterAddress &right) + { + return left.area() == right.area() + ? left.index() < right.index() + : left.area() == RegisterArea::M; + }); + addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); + + executor_.resetRuntime(); + working_repository_.clear(); + logic_snapshot_ = std::move(snapshot); + referenced_addresses_ = std::move(addresses); + successful_scan_count_ = 0; + last_error_ = success(); + trace_snapshot_.clear(); + state_ = OnlineLogicMonitorState::Running; + emit stateChanged(); + + const LogicScanResult first_scan = executeOnce(); + if (!first_scan.succeeded) + { + return { + false, + OnlineLogicMonitorStartError::PlcSnapshotUnavailable, + first_scan}; + } + return {true, OnlineLogicMonitorStartError::None, success()}; +} + +void OnlineLogicMonitorService::stop() +{ + executor_.resetRuntime(); + working_repository_.clear(); + logic_snapshot_.clear(); + referenced_addresses_.clear(); + trace_snapshot_.clear(); + successful_scan_count_ = 0; + // 主动停止时保留最近一次故障,便于真机退出后继续查看诊断信息 + if (state_ == OnlineLogicMonitorState::Stopped) + { + return; + } + state_ = OnlineLogicMonitorState::Stopped; + emit stateChanged(); +} + +LogicScanResult OnlineLogicMonitorService::executeOnce() +{ + if (state_ != OnlineLogicMonitorState::Running) + { + return { + false, + LogicScanError::InvalidLogic, + "真机本地轨迹尚未运行", + {}, + {}, + {}}; + } + + const LogicScanResult copied = copyPlcSnapshot(); + if (!copied.succeeded) + { + enterFault(copied); + return copied; + } + const LogicScanResult result = executor_.executeScan( + logic_snapshot_, working_repository_, &trace_snapshot_); + if (!result.succeeded) + { + enterFault(result); + return result; + } + ++successful_scan_count_; + emit scanCompleted(); + return result; +} + +OnlineLogicMonitorState OnlineLogicMonitorService::state() const +{ + return state_; +} + +std::uint64_t OnlineLogicMonitorService::successfulScanCount() const +{ + return successful_scan_count_; +} + +const LogicScanResult &OnlineLogicMonitorService::lastError() const +{ + return last_error_; +} + +const LogicTraceSnapshot &OnlineLogicMonitorService::traceSnapshot() const +{ + return trace_snapshot_; +} + +LogicScanResult OnlineLogicMonitorService::copyPlcSnapshot() +{ + // 每轮从真实读回缓存重新开始,本地输出不会跨轮污染 PLC 数据 + working_repository_.clear(); + for (const RegisterAddress &address : referenced_addresses_) + { + if (address.area() == RegisterArea::M) + { + const BitReadResult read = plc_repository_.readBit(address); + if (!read.succeeded) + { + return snapshotFailure(address); + } + working_repository_.writeBit(address, read.value); + } + else + { + const WordReadResult read = plc_repository_.readWord(address); + if (!read.succeeded) + { + return snapshotFailure(address); + } + working_repository_.writeWord(address, read.value); + } + } + return success(); +} + +void OnlineLogicMonitorService::enterFault(const LogicScanResult &error) +{ + executor_.resetRuntime(); + last_error_ = error; + state_ = OnlineLogicMonitorState::Faulted; + emit stateChanged(); +} diff --git a/app/src/services/online_logic_monitor_service.h b/app/src/services/online_logic_monitor_service.h new file mode 100644 index 0000000..c1933ff --- /dev/null +++ b/app/src/services/online_logic_monitor_service.h @@ -0,0 +1,70 @@ +#pragma once + +#include "domain/virtual_register_repository.h" +#include "software_logic_executor.h" + +#include + +#include +#include + +enum class OnlineLogicMonitorState +{ + Stopped, + Running, + Faulted +}; + +enum class OnlineLogicMonitorStartError +{ + None, + AlreadyRunning, + InvalidLogic, + PlcSnapshotUnavailable +}; + +struct OnlineLogicMonitorStartResult +{ + bool succeeded = false; + OnlineLogicMonitorStartError error = OnlineLogicMonitorStartError::None; + LogicScanResult detail; +}; + +// 使用 PLC 读回缓存推算本地梯形图轨迹,所有输出只写临时仓库 +class OnlineLogicMonitorService final : public QObject +{ + Q_OBJECT + +public: + explicit OnlineLogicMonitorService( + RegisterRepository &plc_repository, + QObject *parent = nullptr); + + OnlineLogicMonitorStartResult start(const std::vector &logics); + void stop(); + LogicScanResult executeOnce(); + + OnlineLogicMonitorState state() const; + std::uint64_t successfulScanCount() const; + // 返回最近一次推算错误;主动停止不会清除故障详情,下一次 start 会重置 + const LogicScanResult &lastError() const; + const LogicTraceSnapshot &traceSnapshot() const; + +signals: + void stateChanged(); + void scanCompleted(); + +private: + LogicScanResult copyPlcSnapshot(); + void enterFault(const LogicScanResult &error); + + RegisterRepository &plc_repository_; + VirtualRegisterRepository working_repository_; + SoftwareLogicExecutor executor_; + std::vector logic_snapshot_; + std::vector referenced_addresses_; + OnlineLogicMonitorState state_ = OnlineLogicMonitorState::Stopped; + std::uint64_t successful_scan_count_ = 0; + LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}}; + LogicTraceSnapshot trace_snapshot_; +}; diff --git a/app/src/services/plc_communication_gateway.h b/app/src/services/plc_communication_gateway.h index f5376e3..390a7a0 100644 --- a/app/src/services/plc_communication_gateway.h +++ b/app/src/services/plc_communication_gateway.h @@ -171,11 +171,13 @@ public: * @param state_changed 连接状态发生变化时调用 * @param initial_read_changed 首读资格发生变化时调用,参数表示当前是否已完成首读 * @param cache_updated 任一轮询块成功更新 PLC 缓存后调用 + * @param poll_cycle_completed 全部轮询块成功更新一轮后调用 * @param error_reported 发生通信错误时调用,参数为 UTF-8 可读错误文本 */ virtual void setCallbacks( std::function state_changed, std::function initial_read_changed, std::function cache_updated, + std::function poll_cycle_completed, std::function error_reported) = 0; }; diff --git a/app/src/services/runtime_mode_service.cpp b/app/src/services/runtime_mode_service.cpp index d196f44..ad68734 100644 --- a/app/src/services/runtime_mode_service.cpp +++ b/app/src/services/runtime_mode_service.cpp @@ -16,9 +16,11 @@ RuntimeModeService::RuntimeModeService( const ProjectService &project_service, - OfflineSimulationService &offline_simulation_service) + OfflineSimulationService &offline_simulation_service, + OnlineLogicMonitorService &online_logic_monitor_service) : project_service_(project_service), - offline_simulation_service_(offline_simulation_service) + offline_simulation_service_(offline_simulation_service), + online_logic_monitor_service_(online_logic_monitor_service) { } @@ -26,7 +28,7 @@ RuntimeModeService::~RuntimeModeService() { if (plc_gateway_ != nullptr) { - plc_gateway_->setCallbacks({}, {}, {}, {}); + plc_gateway_->setCallbacks({}, {}, {}, {}, {}); } } @@ -47,6 +49,10 @@ ModeTransitionResult RuntimeModeService::enterEditing() // 先停止扫描再开放编辑,防止运行快照继续写寄存器 offline_simulation_service_.stop(); } + else if (state_.mode() == ApplicationMode::OnlineRunning) + { + online_logic_monitor_service_.stop(); + } const ModeTransitionResult result = state_.enterEditing(); if (result.succeeded && active_repository_ != nullptr && virtual_repository_ != nullptr) @@ -90,13 +96,30 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() ModeTransitionResult RuntimeModeService::enterOnlineRunning() { - // 真机模式只允许使用已完成首读的 PLC 缓存,不复制离线寄存器值 - const ModeTransitionResult result = state_.enterOnlineRunning( - initialPlcReadCompleted()); + if (state_.mode() != ApplicationMode::Editing) + { + return state_.enterOnlineRunning(initialPlcReadCompleted()); + } + if (!initialPlcReadCompleted()) + { + return state_.enterOnlineRunning(false); + } + const OnlineLogicMonitorStartResult start_result = + online_logic_monitor_service_.start( + project_service_.project().controlLogics); + if (!start_result.succeeded) + { + return {false, ModeTransitionError::SimulationStartFailed}; + } + const ModeTransitionResult result = state_.enterOnlineRunning(true); if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr) { active_repository_->use(*plc_repository_); } + else if (!result.succeeded) + { + online_logic_monitor_service_.stop(); + } return result; } @@ -134,6 +157,11 @@ OfflineSimulationService &RuntimeModeService::offlineSimulationService() return offline_simulation_service_; } +OnlineLogicMonitorService &RuntimeModeService::onlineLogicMonitorService() +{ + return online_logic_monitor_service_; +} + void RuntimeModeService::configurePlc( PlcCommunicationGateway &gateway, ActiveRegisterRepository &active_repository, @@ -175,6 +203,25 @@ void RuntimeModeService::configurePlc( } }, [] {}, + [this] + { + if (state_.mode() == ApplicationMode::OnlineRunning + && online_logic_monitor_service_.state() + == OnlineLogicMonitorState::Running) + { + const LogicScanResult result = + online_logic_monitor_service_.executeOnce(); + if (!result.succeeded + && state_.mode() == ApplicationMode::OnlineRunning) + { + enterEditing(); + if (plc_status_changed_callback_) + { + plc_status_changed_callback_(); + } + } + } + }, [this](const std::string &) { if (plc_status_changed_callback_) diff --git a/app/src/services/runtime_mode_service.h b/app/src/services/runtime_mode_service.h index 27dae7e..f39ea35 100644 --- a/app/src/services/runtime_mode_service.h +++ b/app/src/services/runtime_mode_service.h @@ -10,6 +10,7 @@ #include "domain/runtime_state.h" #include "offline_simulation_service.h" +#include "online_logic_monitor_service.h" #include "plc_communication_gateway.h" #include @@ -31,7 +32,8 @@ public: */ RuntimeModeService( const ProjectService &project_service, - OfflineSimulationService &offline_simulation_service); + OfflineSimulationService &offline_simulation_service, + OnlineLogicMonitorService &online_logic_monitor_service); /** * @brief 解除 PLC 网关回调绑定 @@ -83,6 +85,7 @@ public: const LogicScanResult &simulationError() const; /** @brief 返回服务持有的离线仿真服务引用 */ OfflineSimulationService &offlineSimulationService(); + OnlineLogicMonitorService &onlineLogicMonitorService(); /** * @brief 注入 PLC 网关、活动仓库和两种实际数据源 @@ -133,6 +136,7 @@ public: private: const ProjectService &project_service_; // 不拥有的只读工程服务 OfflineSimulationService &offline_simulation_service_; // 不拥有的离线仿真服务 + OnlineLogicMonitorService &online_logic_monitor_service_; // 不拥有的真机只读轨迹服务 RuntimeState state_; // 编辑、离线和真机模式状态机 // PLC 缓存是否已通过有效首读建立,通信故障时清除 bool initial_plc_read_completed_ = false; diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 153b254..5a9e43b 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -171,11 +171,11 @@ QString transitionErrorText(ModeTransitionError error) } case ModeTransitionError::ProjectNotReady: { - return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入离线运行态"); + return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入运行态"); } case ModeTransitionError::SimulationStartFailed: { - return MainWindow::tr("离线逻辑执行器预检或启动失败"); + return MainWindow::tr("本地逻辑执行器预检或启动失败"); } case ModeTransitionError::None: default: @@ -1731,7 +1731,10 @@ void MainWindow::requestMode(ApplicationMode requested_mode) } if (result.error == ModeTransitionError::SimulationStartFailed) { - const LogicScanResult &error = runtime_mode_service_.simulationError(); + const LogicScanResult &error = requested_mode + == ApplicationMode::OnlineRunning + ? runtime_mode_service_.onlineLogicMonitorService().lastError() + : runtime_mode_service_.simulationError(); if (!error.message.empty()) { message += QStringLiteral(": ") + fromUtf8(error.message); diff --git a/app/src/ui/runtime_monitor_widget.cpp b/app/src/ui/runtime_monitor_widget.cpp index 25d1689..de6c6b6 100644 --- a/app/src/ui/runtime_monitor_widget.cpp +++ b/app/src/ui/runtime_monitor_widget.cpp @@ -66,6 +66,7 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( register_monitor_service, ui_->monitorContainer); free_monitor_widget_->setObjectName(QStringLiteral("freeMonitorWidget")); ui_->monitorContainerLayout->addWidget(free_monitor_widget_); + ui_->logicNoticeLabel->setVisible(false); ui_->upperSplitter->setSizes({600, 600}); ui_->verticalSplitter->setSizes({440, 260}); connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, @@ -132,10 +133,9 @@ void RuntimeMonitorWidget::setRuntimePage(const std::string &page_id) void RuntimeMonitorWidget::setMode( ApplicationMode mode, PlcConnectionState plc_state) { - // 真机时清空本地轨迹,防止把 PC 离线执行结果误认为 PLC 内部程序轨迹 const bool offline = mode == ApplicationMode::OfflineRunning; const bool online = mode == ApplicationMode::OnlineRunning; - ui_->logicFrame->setVisible(offline); + ui_->logicFrame->setVisible(offline || online); hmi_view_->setRuntimeActive(offline || online); if (!offline && !online) { @@ -143,16 +143,20 @@ void RuntimeMonitorWidget::setMode( } setFreeMonitorWriteEnabled(offline || (online && plc_state == PlcConnectionState::Connected)); - if (!offline) + if (!offline && !online) { logic_view_->clearRuntimeTrace(); latest_trace_.clear(); latest_fault_node_id_.clear(); has_logic_trace_ = false; } + ui_->logicLabel->setText( + online ? tr("本地推算轨迹") : tr("梯形图运行状态")); + ui_->logicNoticeLabel->setVisible(online); ui_->modeLabel->setText( offline ? tr("离线仿真 · 虚拟 M/D") - : online ? tr("真机运行 · PLC 缓存") : tr("当前未运行")); + : online ? tr("真机运行 · PLC 缓存 · 本地推算轨迹") + : tr("当前未运行")); free_monitor_widget_->refreshValues(mode, plc_state); } diff --git a/app/src/ui/runtime_monitor_widget.h b/app/src/ui/runtime_monitor_widget.h index 9f4d374..52f4b75 100644 --- a/app/src/ui/runtime_monitor_widget.h +++ b/app/src/ui/runtime_monitor_widget.h @@ -63,7 +63,7 @@ public: void setHmiWriteEnabled(bool enabled); /** 设置自由监控是否允许写入寄存器 */ void setFreeMonitorWriteEnabled(bool enabled); - /** 只接收离线执行器轨迹;真机时由上层清空 */ + /** 接收离线轨迹或基于 PLC 缓存计算的真机本地推算轨迹 */ void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); /** 返回自由监控子控件,供上层刷新或连接信号 */ FreeMonitorWidget *freeMonitorWidget() const; @@ -96,9 +96,9 @@ private: FreeMonitorWidget *free_monitor_widget_ = nullptr; /** 当前选中的控制逻辑标识 */ std::string selected_logic_id_; - /** 最近一次收到的离线逻辑执行轨迹 */ + /** 最近一次收到的本地逻辑执行轨迹 */ LogicTraceSnapshot latest_trace_; - /** 最近一次离线执行故障节点标识 */ + /** 最近一次本地执行故障节点标识 */ std::string latest_fault_node_id_; /** 是否已经收到过可显示的逻辑轨迹 */ bool has_logic_trace_ = false; diff --git a/app/src/ui/runtime_monitor_widget.ui b/app/src/ui/runtime_monitor_widget.ui index 8302caa..9564e95 100644 --- a/app/src/ui/runtime_monitor_widget.ui +++ b/app/src/ui/runtime_monitor_widget.ui @@ -46,6 +46,7 @@ 1500 + color: #8a5a00;真机模式:本地梯形图只读推算 仅使用 PLC 缓存显示轨迹,本地输出不写入 PLC 程序或 M/D,不影响 PLC 内部逻辑;HMI 和自由监控仍可写 PLC。true 0000 diff --git a/app/src/ui/runtime_panel_controller.cpp b/app/src/ui/runtime_panel_controller.cpp index 31eba16..b5d1def 100644 --- a/app/src/ui/runtime_panel_controller.cpp +++ b/app/src/ui/runtime_panel_controller.cpp @@ -133,6 +133,13 @@ void RuntimePanelController::configure() &OfflineSimulationService::scanCompleted, &parent_, [this] { handleScanCompleted(); }, Qt::QueuedConnection); + QObject::connect(&runtime_mode_service_.onlineLogicMonitorService(), + &OnlineLogicMonitorService::stateChanged, + &parent_, [this] { handleSimulationStateChanged(); }); + QObject::connect(&runtime_mode_service_.onlineLogicMonitorService(), + &OnlineLogicMonitorService::scanCompleted, + &parent_, [this] { handleScanCompleted(); }, + Qt::QueuedConnection); } RuntimeMonitorWidget *RuntimePanelController::runtimeMonitorWidget() const @@ -193,6 +200,40 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); } + if (online) + { + switch (runtime_mode_service_.onlineLogicMonitorService().state()) + { + case OnlineLogicMonitorState::Running: + { + executor_status_label_.setText( + QObject::tr("本地推算轨迹:运行(%1 次)") + .arg(runtime_mode_service_.onlineLogicMonitorService() + .successfulScanCount())); + return; + } + case OnlineLogicMonitorState::Faulted: + { + executor_status_label_.setText(QObject::tr("本地推算轨迹:故障")); + if (report_fault) + { + const LogicScanResult &error = runtime_mode_service_ + .onlineLogicMonitorService().lastError(); + const QString message = QObject::tr("真机本地轨迹异常:%1") + .arg(fromUtf8(error.message)); + status_reporter_(message, 0); + output_reporter_(message); + } + return; + } + case OnlineLogicMonitorState::Stopped: + default: + { + executor_status_label_.setText(QObject::tr("本地推算轨迹:停止")); + return; + } + } + } switch (state) { case SimulationState::Running: @@ -265,7 +306,7 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) void RuntimePanelController::handleRuntimeTimer() { - // 定时器只负责刷新投影;离线逻辑扫描由 OfflineSimulationService 自己的定时器驱动 + // 定时器只刷新投影;离线按定时器扫描,真机在完整 PLC 轮询后推算 if (runtime_mode_service_.mode() == ApplicationMode::Editing) { return; @@ -288,19 +329,24 @@ void RuntimePanelController::handleSimulationStateChanged() void RuntimePanelController::handleScanCompleted() { - // 排队的扫描信号可能晚于退出操作到达,编辑态不得恢复旧运行轨迹 - if (runtime_mode_service_.mode() != ApplicationMode::OfflineRunning - || runtime_mode_service_.simulationState() != SimulationState::Running) + const ApplicationMode mode = runtime_mode_service_.mode(); + const bool offline_running = mode == ApplicationMode::OfflineRunning + && runtime_mode_service_.simulationState() == SimulationState::Running; + const bool online_running = mode == ApplicationMode::OnlineRunning + && runtime_mode_service_.onlineLogicMonitorService().state() + == OnlineLogicMonitorState::Running; + // 排队信号可能晚于退出操作到达,编辑态不得恢复旧运行轨迹 + if (!offline_running && !online_running) { return; } const std::string logic_id = current_logic_id_(); - logic_editor_widget_.setRuntimeTrace( - runtime_mode_service_.offlineSimulationService() - .traceSnapshot().forLogic(logic_id)); + const LogicTraceSnapshot &trace = offline_running + ? runtime_mode_service_.offlineSimulationService().traceSnapshot() + : runtime_mode_service_.onlineLogicMonitorService().traceSnapshot(); + logic_editor_widget_.setRuntimeTrace(trace.forLogic(logic_id)); if (runtime_monitor_widget_ != nullptr) { - runtime_monitor_widget_->setLogicTrace( - runtime_mode_service_.offlineSimulationService().traceSnapshot()); + runtime_monitor_widget_->setLogicTrace(trace); } } diff --git a/app/src/ui/runtime_panel_controller.h b/app/src/ui/runtime_panel_controller.h index bddff79..efc8b48 100644 --- a/app/src/ui/runtime_panel_controller.h +++ b/app/src/ui/runtime_panel_controller.h @@ -33,7 +33,7 @@ class RuntimeMonitorWindow; class QTimer; class QWidget; -/** 组织运行监控窗口、刷新定时器和离线执行器反馈 */ +/** 组织运行监控窗口、刷新定时器和本地逻辑执行器反馈 */ class RuntimePanelController final { public: @@ -80,7 +80,7 @@ public: void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state); /** 应用退出时关闭运行监控窗口并停止相关刷新 */ void closeForApplicationExit(); - /** 根据离线执行器状态刷新运行界面 */ + /** 根据当前模式的本地执行器状态刷新运行界面 */ void updateSimulationUi(bool report_fault); /** 返回当前唯一的运行监控控件 */ RuntimeMonitorWidget *runtimeMonitorWidget() const; @@ -88,7 +88,7 @@ public: private: /** 处理运行刷新定时器到期事件 */ void handleRuntimeTimer(); - /** 处理离线执行器状态变化 */ + /** 处理离线或真机只读轨迹执行器状态变化 */ void handleSimulationStateChanged(); /** 处理一次离线扫描完成事件 */ void handleScanCompleted(); @@ -115,7 +115,7 @@ private: HmiEditorWidget &hmi_editor_widget_; /** 主窗口中的梯形图编辑器,不由控制器拥有 */ LogicEditorWidget &logic_editor_widget_; - /** 显示离线执行器状态的标签 */ + /** 显示当前本地执行器状态的标签 */ QLabel &executor_status_label_; /** 获取当前控制逻辑标识的回调 */ std::function current_logic_id_; diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index f458b87..66ac201 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -796,8 +796,8 @@ void testRuntimeStateBoundaries() "online mode must require an initial PLC read"); require(state.enterOnlineRunning(true).succeeded, "editing may enter online mode after initial PLC read"); - require(!state.policy().runsLogicExecutor, - "online mode must keep the software logic executor stopped"); + require(state.policy().runsLogicExecutor, + "online mode must run the local read-only trace executor"); require(state.policy().usesPlcRegisters, "online mode must use PLC registers"); } diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index 2477564..925bea5 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -4,6 +4,7 @@ #include "domain/virtual_register_repository.h" #include "services/hmi_runtime_service.h" #include "services/offline_simulation_service.h" +#include "services/online_logic_monitor_service.h" #include "services/software_logic_executor.h" #include "support/test_support.h" @@ -688,6 +689,57 @@ void testRepositoryFailureEntersFaultState() "fault feedback must retain logic, rung and node context"); } +void testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource() +{ + VirtualRegisterRepository plc_source; + OnlineLogicMonitorService monitor(plc_source); + const ControlLogic program = logic({ + rung("online-rung-1", {{contact("online-input", 0)}}, + coil("online-output-1", 1)), + rung("online-rung-2", {{contact("online-feedback", 1)}}, + coil("online-output-2", 2))}); + + writeBit(plc_source, 0, true); + require(monitor.start({program}).succeeded, + "online monitor must start from the PLC source snapshot"); + const LogicTraceSnapshot &first_trace = monitor.traceSnapshot(); + require(first_trace.rungValues.at("online-rung-1") + && first_trace.rungValues.at("online-rung-2"), + "a local output must be visible to later rungs in the same temporary scan"); + require(!readBit(plc_source, 1) && !readBit(plc_source, 2), + "local trace outputs must never change the PLC source repository"); + + writeBit(plc_source, 0, false); + require(monitor.executeOnce().succeeded, + "a new PLC snapshot must support another local trace scan"); + const LogicTraceSnapshot &second_trace = monitor.traceSnapshot(); + require(!second_trace.rungValues.at("online-rung-1") + && !second_trace.rungValues.at("online-rung-2"), + "each local scan must restart from the latest PLC source values"); + require(!readBit(plc_source, 1) && !readBit(plc_source, 2), + "repeated local scans must remain read-only toward the PLC source"); +} + +void testOnlineMonitorPreservesFaultAfterStopping() +{ + FailingVirtualRegisterRepository plc_source; + OnlineLogicMonitorService monitor(plc_source); + const ControlLogic program = logic({ + rung("online-fault-rung", {{contact("online-fault-input", 0)}}, + coil("online-fault-output", 1))}); + + const OnlineLogicMonitorStartResult start = monitor.start({program}); + require(!start.succeeded + && start.detail.error == LogicScanError::RegisterReadFailed, + "an online snapshot failure must be reported at startup"); + monitor.stop(); + require(monitor.state() == OnlineLogicMonitorState::Stopped, + "stopping a failed online monitor must leave it stopped"); + require(!monitor.lastError().succeeded + && monitor.lastError().error == LogicScanError::RegisterReadFailed, + "stopping after a local trace fault must preserve its diagnostic"); +} + } // namespace int main(int argc, char *argv[]) @@ -709,6 +761,8 @@ int main(int argc, char *argv[]) testHmiSimulationClosedLoop(); testSimulationLifecycleSnapshotAndFault(); testRepositoryFailureEntersFaultState(); + testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource(); + testOnlineMonitorPreservesFaultAfterStopping(); } catch (const std::exception &error) { diff --git a/app/tests/plc_runtime_tests.cpp b/app/tests/plc_runtime_tests.cpp index dfe7586..5ce360e 100644 --- a/app/tests/plc_runtime_tests.cpp +++ b/app/tests/plc_runtime_tests.cpp @@ -72,11 +72,13 @@ public: std::function state_callback, std::function initial_callback, std::function cache_callback, + std::function poll_cycle_callback, std::function error_callback) override { state_changed = std::move(state_callback); initial_read_changed = std::move(initial_callback); cache_updated = std::move(cache_callback); + poll_cycle_completed = std::move(poll_cycle_callback); error_reported = std::move(error_callback); } @@ -120,6 +122,7 @@ public: std::function state_changed; std::function initial_read_changed; std::function cache_updated; + std::function poll_cycle_completed; std::function error_reported; }; @@ -189,8 +192,10 @@ void testRuntimeRepositorySwitchingAndDisconnect() PlcRegisterRepository plc_repository; ActiveRegisterRepository active_repository(virtual_repository); OfflineSimulationService simulation_service(virtual_repository); + OnlineLogicMonitorService online_monitor_service(plc_repository); FakePlcGateway gateway; - RuntimeModeService service(project_service, simulation_service); + RuntimeModeService service( + project_service, simulation_service, online_monitor_service); service.configurePlc( gateway, active_repository, virtual_repository, plc_repository); @@ -222,6 +227,9 @@ void testRuntimeRepositorySwitchingAndDisconnect() gateway.disconnectDevice(); require(service.mode() == ApplicationMode::Editing, "an online disconnect must return the application to editing mode"); + require(service.onlineLogicMonitorService().state() + == OnlineLogicMonitorState::Stopped, + "an online disconnect must stop the local read-only trace executor"); require(!service.initialPlcReadCompleted(), "an online disconnect must clear the initial read flag"); require(active_repository.readBit({RegisterArea::M, 5}).succeeded @@ -237,8 +245,10 @@ void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect() PlcRegisterRepository plc_repository; ActiveRegisterRepository active_repository(virtual_repository); OfflineSimulationService simulation_service(virtual_repository); + OnlineLogicMonitorService online_monitor_service(plc_repository); FakePlcGateway gateway; - RuntimeModeService service(project_service, simulation_service); + RuntimeModeService service( + project_service, simulation_service, online_monitor_service); service.configurePlc( gateway, active_repository, virtual_repository, plc_repository); @@ -254,6 +264,9 @@ void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect() "PLC communication timed out while the serial port remained open"); require(service.mode() == ApplicationMode::Editing, "a PLC communication fault must return online running to editing"); + require(service.onlineLogicMonitorService().state() + == OnlineLogicMonitorState::Stopped, + "a PLC communication fault must stop the local trace executor"); require(!service.initialPlcReadCompleted(), "a PLC communication fault must revoke initial read readiness"); require(service.enterOnlineRunning().error diff --git a/app/tests/pri/test_layers.pri b/app/tests/pri/test_layers.pri index bb864df..9b5d292 100644 --- a/app/tests/pri/test_layers.pri +++ b/app/tests/pri/test_layers.pri @@ -97,11 +97,13 @@ SERVICE_LOGIC_HEADERS = \ SERVICE_OFFLINE_SOURCES = \ ../src/services/software_logic_executor.cpp \ - ../src/services/offline_simulation_service.cpp + ../src/services/offline_simulation_service.cpp \ + ../src/services/online_logic_monitor_service.cpp SERVICE_OFFLINE_HEADERS = \ ../src/services/software_logic_executor.h \ - ../src/services/offline_simulation_service.h + ../src/services/offline_simulation_service.h \ + ../src/services/online_logic_monitor_service.h SERVICE_RUNTIME_SOURCES = \ ../src/services/runtime_mode_service.cpp \ diff --git a/app/tests/runtime_mode_service_tests.cpp b/app/tests/runtime_mode_service_tests.cpp index 9eec390..9f98f93 100644 --- a/app/tests/runtime_mode_service_tests.cpp +++ b/app/tests/runtime_mode_service_tests.cpp @@ -54,11 +54,13 @@ public: std::function state_callback, std::function initial_callback, std::function cache_callback, + std::function poll_cycle_callback, std::function error_callback) override { state_changed = std::move(state_callback); initial_read_changed = std::move(initial_callback); cache_updated = std::move(cache_callback); + poll_cycle_completed = std::move(poll_cycle_callback); error_reported = std::move(error_callback); } @@ -71,6 +73,14 @@ public: } } + void completePollCycle() + { + if (poll_cycle_completed) + { + poll_cycle_completed(); + } + } + const std::vector &pollAddresses() const { return poll_addresses; @@ -83,6 +93,7 @@ private: std::function state_changed; std::function initial_read_changed; std::function cache_updated; + std::function poll_cycle_completed; std::function error_reported; std::vector poll_addresses; }; @@ -111,8 +122,10 @@ void testModeTransitions() VirtualRegisterRepository plc_repository; ActiveRegisterRepository active_repository(virtual_repository); OfflineSimulationService simulation_service(virtual_repository); + OnlineLogicMonitorService online_monitor_service(plc_repository); ReadyPlcGateway gateway; - RuntimeModeService service(project_service, simulation_service); + RuntimeModeService service( + project_service, simulation_service, online_monitor_service); service.configurePlc( gateway, active_repository, virtual_repository, plc_repository); @@ -241,14 +254,26 @@ void testModeTransitions() gateway.completeInitialRead(); require(service.initialPlcReadCompleted(), "service must retain the initial PLC read state"); + plc_repository.writeBit({RegisterArea::M, 20}, true); require(service.enterOnlineRunning().succeeded, "online running must start after an initial PLC read"); require(service.simulationState() == SimulationState::Stopped, - "online running must never start the software executor"); + "online running must keep the offline executor stopped"); + require(service.onlineLogicMonitorService().state() + == OnlineLogicMonitorState::Running, + "online running must start the local read-only trace executor"); + require(!plc_repository.readBit({RegisterArea::M, 21}).value, + "the local trace output must not change the PLC source repository"); require(service.policy().usesPlcRegisters, "online running must use PLC registers"); - require(!service.policy().runsLogicExecutor, - "online running must keep the software executor stopped"); + require(service.policy().runsLogicExecutor, + "online running must advertise the local read-only trace executor"); + const std::uint64_t scan_count = service.onlineLogicMonitorService() + .successfulScanCount(); + gateway.completePollCycle(); + require(service.onlineLogicMonitorService().successfulScanCount() + == scan_count + 1U, + "a completed PLC poll cycle must trigger one new local trace scan"); } } // namespace diff --git a/app/tests/runtime_panel_controller_tests.cpp b/app/tests/runtime_panel_controller_tests.cpp index c62532a..ee36326 100644 --- a/app/tests/runtime_panel_controller_tests.cpp +++ b/app/tests/runtime_panel_controller_tests.cpp @@ -11,6 +11,7 @@ #include "support/test_support.h" #include "ui/hmi_editor_widget.h" #include "ui/logic_editor_widget.h" +#include "ui/runtime_monitor_widget.h" #include "ui/runtime_panel_controller.h" #include @@ -54,7 +55,9 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() ProjectService project_service(storage); VirtualRegisterRepository virtual_repository; OfflineSimulationService simulation_service(virtual_repository); - RuntimeModeService runtime_mode_service(project_service, simulation_service); + OnlineLogicMonitorService online_monitor_service(virtual_repository); + RuntimeModeService runtime_mode_service( + project_service, simulation_service, online_monitor_service); HmiEditorService hmi_editor_service(project_service); HmiRuntimeService hmi_runtime_service(virtual_repository); LogicEditorService logic_editor_service(project_service); @@ -102,6 +105,24 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() controller.enterRuntime( {}, logic.id, runtime_mode_service.mode(), runtime_mode_service.plcConnectionState()); + QLabel *logic_label = controller.runtimeMonitorWidget() + ->findChild(QStringLiteral("logicLabel")); + QLabel *notice_label = controller.runtimeMonitorWidget() + ->findChild(QStringLiteral("logicNoticeLabel")); + require(logic_label != nullptr && notice_label != nullptr, + "runtime monitor must expose its local trace labels"); + require(logic_label->text() == QStringLiteral("梯形图运行状态") + && !notice_label->isVisible(), + "offline runtime must keep the normal ladder trace heading"); + controller.runtimeMonitorWidget()->setMode( + ApplicationMode::OnlineRunning, PlcConnectionState::Connected); + require(logic_label->text() == QStringLiteral("本地推算轨迹") + && notice_label->isVisible() + && notice_label->text().contains(QStringLiteral("不写入 PLC 程序或 M/D")) + && notice_label->text().contains(QStringLiteral("HMI 和自由监控仍可写 PLC")), + "online runtime must explain the read-only local trace boundary"); + controller.runtimeMonitorWidget()->setMode( + ApplicationMode::OfflineRunning, PlcConnectionState::Disconnected); require(simulation_service.executeOnce().succeeded, "offline simulation must produce a trace before editing"); require(simulation_service.traceSnapshot() diff --git a/scripts/run_qt_tests.ps1 b/scripts/run_qt_tests.ps1 index 6f92dc8..e803f2f 100644 --- a/scripts/run_qt_tests.ps1 +++ b/scripts/run_qt_tests.ps1 @@ -26,6 +26,7 @@ foreach ($requiredPath in @($qmakePath, $makePath, $testSourceRoot)) { $functionalTargets = @( 'domain_tests', + 'application_settings_tests', 'alarm_service_tests', 'hmi_editor_service_tests', 'logic_editor_service_tests',