| @@ -47,6 +47,7 @@ SOURCES += \ | |||||
| src/services/hmi_runtime_service.cpp \ | src/services/hmi_runtime_service.cpp \ | ||||
| src/services/software_logic_executor.cpp \ | 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 \ | |||||
| src/services/runtime_mode_service.cpp \ | src/services/runtime_mode_service.cpp \ | ||||
| src/services/plc_discovery_gateway.cpp \ | src/services/plc_discovery_gateway.cpp \ | ||||
| src/services/register_monitor_service.cpp \ | src/services/register_monitor_service.cpp \ | ||||
| @@ -96,6 +97,7 @@ HEADERS += \ | |||||
| src/services/hmi_runtime_service.h \ | src/services/hmi_runtime_service.h \ | ||||
| src/services/software_logic_executor.h \ | 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 \ | |||||
| src/services/runtime_mode_service.h \ | src/services/runtime_mode_service.h \ | ||||
| src/services/plc_discovery_gateway.h \ | src/services/plc_discovery_gateway.h \ | ||||
| src/services/register_monitor_service.h \ | src/services/register_monitor_service.h \ | ||||
| @@ -52,12 +52,12 @@ constexpr ModePolicy policyForMode(ApplicationMode mode) | |||||
| } | } | ||||
| case ApplicationMode::OnlineRunning: | case ApplicationMode::OnlineRunning: | ||||
| { | { | ||||
| // 真机运行态禁止改工程,使用 PLC 数据且禁止软件逻辑重复控制 PLC | |||||
| // 真机运行态使用 PLC 数据,并在本地只读推算梯形图轨迹 | |||||
| return { | return { | ||||
| false, // allowsProjectEditing 禁止编辑工程 | false, // allowsProjectEditing 禁止编辑工程 | ||||
| false, // usesVirtualRegisters 不使用虚拟寄存器 | false, // usesVirtualRegisters 不使用虚拟寄存器 | ||||
| true, // usesPlcRegisters 使用 PLC 寄存器 | true, // usesPlcRegisters 使用 PLC 寄存器 | ||||
| false, // runsLogicExecutor 不运行软件逻辑执行器 | |||||
| true, // runsLogicExecutor 运行只读本地轨迹执行器 | |||||
| true // requiresInitialPlcRead 必须先完成 PLC 初次读取 | true // requiresInitialPlcRead 必须先完成 PLC 初次读取 | ||||
| }; | }; | ||||
| } | } | ||||
| @@ -76,7 +76,7 @@ enum class ModeTransitionError | |||||
| MustReturnToEditing, // 必须先返回编辑态 | MustReturnToEditing, // 必须先返回编辑态 | ||||
| InitialPlcReadRequired, // 进入真机态前必须完成 PLC 初次读取 | InitialPlcReadRequired, // 进入真机态前必须完成 PLC 初次读取 | ||||
| ProjectNotReady, // 工程存在未配置或未完成的运行项 | ProjectNotReady, // 工程存在未配置或未完成的运行项 | ||||
| SimulationStartFailed // 离线执行器预检或启动失败 | |||||
| SimulationStartFailed // 当前模式的本地执行器预检或启动失败 | |||||
| }; | }; | ||||
| // 模式切换操作结果 | // 模式切换操作结果 | ||||
| @@ -312,12 +312,14 @@ void PlcCommunicationService::setCallbacks( | |||||
| std::function<void()> state_changed, | std::function<void()> state_changed, | ||||
| std::function<void(bool)> initial_read_changed, | std::function<void(bool)> initial_read_changed, | ||||
| std::function<void()> cache_updated, | std::function<void()> cache_updated, | ||||
| std::function<void()> poll_cycle_completed, | |||||
| std::function<void(const std::string &)> error_reported) | std::function<void(const std::string &)> error_reported) | ||||
| { | { | ||||
| // 保存外部通知函数,通信事件发生时再调用对应函数 | // 保存外部通知函数,通信事件发生时再调用对应函数 | ||||
| state_changed_callback_ = std::move(state_changed); // 通信状态变化时通知外部 | state_changed_callback_ = std::move(state_changed); // 通信状态变化时通知外部 | ||||
| initial_read_changed_callback_ = std::move(initial_read_changed); // 首读资格变化时通知外部 | initial_read_changed_callback_ = std::move(initial_read_changed); // 首读资格变化时通知外部 | ||||
| cache_updated_callback_ = std::move(cache_updated); // PLC 缓存更新后通知外部 | cache_updated_callback_ = std::move(cache_updated); // PLC 缓存更新后通知外部 | ||||
| poll_cycle_completed_callback_ = std::move(poll_cycle_completed); | |||||
| error_reported_callback_ = std::move(error_reported); // 发生通信错误时通知外部 | 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) | if (pending_reply_ == reply) | ||||
| { | { | ||||
| // 只有当前指针仍指向本次回复时才清空,避免误清新请求 | // 只有当前指针仍指向本次回复时才清空,避免误清新请求 | ||||
| @@ -455,6 +461,14 @@ void PlcCommunicationService::pollNextBlock() | |||||
| const std::vector<RegisterAddress> addresses = pending_poll_addresses_; | const std::vector<RegisterAddress> addresses = pending_poll_addresses_; | ||||
| applyPollAddresses(addresses); | applyPollAddresses(addresses); | ||||
| } | } | ||||
| if (poll_cycle_completed) | |||||
| { | |||||
| emit pollCycleCompleted(); | |||||
| if (poll_cycle_completed_callback_) | |||||
| { | |||||
| poll_cycle_completed_callback_(); | |||||
| } | |||||
| } | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -55,6 +55,7 @@ public: | |||||
| std::function<void()> state_changed, | std::function<void()> state_changed, | ||||
| std::function<void(bool)> initial_read_changed, | std::function<void(bool)> initial_read_changed, | ||||
| std::function<void()> cache_updated, | std::function<void()> cache_updated, | ||||
| std::function<void()> poll_cycle_completed, | |||||
| std::function<void(const std::string &)> error_reported) override; | std::function<void(const std::string &)> error_reported) override; | ||||
| signals: | signals: | ||||
| @@ -64,6 +65,8 @@ signals: | |||||
| void initialReadCompletedChanged(bool completed); | void initialReadCompletedChanged(bool completed); | ||||
| // PLC 缓存收到新的成功读数 | // PLC 缓存收到新的成功读数 | ||||
| void cacheUpdated(); | void cacheUpdated(); | ||||
| // 全部轮询块成功更新一轮 | |||||
| void pollCycleCompleted(); | |||||
| // 通信错误的可读提示文字 | // 通信错误的可读提示文字 | ||||
| void communicationError(const QString &message); | void communicationError(const QString &message); | ||||
| @@ -132,5 +135,6 @@ private: | |||||
| std::function<void()> state_changed_callback_; // 状态变化时调用 | std::function<void()> state_changed_callback_; // 状态变化时调用 | ||||
| std::function<void(bool)> initial_read_changed_callback_; // 首读资格变化时调用 | std::function<void(bool)> initial_read_changed_callback_; // 首读资格变化时调用 | ||||
| std::function<void()> cache_updated_callback_; // 缓存更新后调用 | std::function<void()> cache_updated_callback_; // 缓存更新后调用 | ||||
| std::function<void()> poll_cycle_completed_callback_; // 完整轮询一轮后调用 | |||||
| std::function<void(const std::string &)> error_reported_callback_; // 发生错误时调用 | std::function<void(const std::string &)> error_reported_callback_; // 发生错误时调用 | ||||
| }; | }; | ||||
| @@ -22,6 +22,7 @@ | |||||
| #include "services/hmi_navigation_service.h" | #include "services/hmi_navigation_service.h" | ||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| #include "services/online_logic_monitor_service.h" | |||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "services/register_monitor_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); | RegisterMonitorService register_monitor_service(active_register_repository); | ||||
| // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 | // 用虚拟寄存器执行本地梯形图,实现不连接 PLC 的离线仿真 | ||||
| OfflineSimulationService offline_simulation_service(virtual_register_repository); | OfflineSimulationService offline_simulation_service(virtual_register_repository); | ||||
| // 管理编辑、离线运行和真机运行三种模式,并控制离线仿真的启停 | |||||
| // 从 PLC 缓存推算本地梯形图轨迹,输出只写内部临时仓库 | |||||
| OnlineLogicMonitorService online_logic_monitor_service(plc_register_repository); | |||||
| // 管理三种运行模式,并控制离线仿真和真机只读轨迹的生命周期 | |||||
| RuntimeModeService runtime_mode_service( | RuntimeModeService runtime_mode_service( | ||||
| project_service, offline_simulation_service); | |||||
| project_service, offline_simulation_service, online_logic_monitor_service); | |||||
| // 把 PLC 通信和两套寄存器接入运行模式,切换模式时才能切换数据来源 | // 把 PLC 通信和两套寄存器接入运行模式,切换模式时才能切换数据来源 | ||||
| runtime_mode_service.configurePlc( | runtime_mode_service.configurePlc( | ||||
| plc_communication_service, | plc_communication_service, | ||||
| @@ -0,0 +1,209 @@ | |||||
| #include "online_logic_monitor_service.h" | |||||
| #include <algorithm> | |||||
| #include <utility> | |||||
| 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<ControlLogic> &logics) | |||||
| { | |||||
| if (state_ == OnlineLogicMonitorState::Running) | |||||
| { | |||||
| return {false, OnlineLogicMonitorStartError::AlreadyRunning, {}}; | |||||
| } | |||||
| std::vector<ControlLogic> 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<RegisterAddress> addresses; | |||||
| for (const ControlLogic &logic : snapshot) | |||||
| { | |||||
| if (!logic.enabled) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| for (const LadderRung &rung : logic.rungs) | |||||
| { | |||||
| std::vector<const LogicNode *> nodes; | |||||
| if (rung.condition.has_value()) | |||||
| { | |||||
| collectConditionNodes(*rung.condition, &nodes); | |||||
| } | |||||
| if (rung.output.has_value()) | |||||
| { | |||||
| nodes.push_back(&*rung.output); | |||||
| } | |||||
| for (const LogicNode *node : nodes) | |||||
| { | |||||
| 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(); | |||||
| } | |||||
| @@ -0,0 +1,70 @@ | |||||
| #pragma once | |||||
| #include "domain/virtual_register_repository.h" | |||||
| #include "software_logic_executor.h" | |||||
| #include <QObject> | |||||
| #include <cstdint> | |||||
| #include <vector> | |||||
| 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<ControlLogic> &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<ControlLogic> logic_snapshot_; | |||||
| std::vector<RegisterAddress> referenced_addresses_; | |||||
| OnlineLogicMonitorState state_ = OnlineLogicMonitorState::Stopped; | |||||
| std::uint64_t successful_scan_count_ = 0; | |||||
| LogicScanResult last_error_{true, LogicScanError::None, {}, {}, {}, {}}; | |||||
| LogicTraceSnapshot trace_snapshot_; | |||||
| }; | |||||
| @@ -171,11 +171,13 @@ public: | |||||
| * @param state_changed 连接状态发生变化时调用 | * @param state_changed 连接状态发生变化时调用 | ||||
| * @param initial_read_changed 首读资格发生变化时调用,参数表示当前是否已完成首读 | * @param initial_read_changed 首读资格发生变化时调用,参数表示当前是否已完成首读 | ||||
| * @param cache_updated 任一轮询块成功更新 PLC 缓存后调用 | * @param cache_updated 任一轮询块成功更新 PLC 缓存后调用 | ||||
| * @param poll_cycle_completed 全部轮询块成功更新一轮后调用 | |||||
| * @param error_reported 发生通信错误时调用,参数为 UTF-8 可读错误文本 | * @param error_reported 发生通信错误时调用,参数为 UTF-8 可读错误文本 | ||||
| */ | */ | ||||
| virtual void setCallbacks( | virtual void setCallbacks( | ||||
| std::function<void()> state_changed, | std::function<void()> state_changed, | ||||
| std::function<void(bool)> initial_read_changed, | std::function<void(bool)> initial_read_changed, | ||||
| std::function<void()> cache_updated, | std::function<void()> cache_updated, | ||||
| std::function<void()> poll_cycle_completed, | |||||
| std::function<void(const std::string &)> error_reported) = 0; | std::function<void(const std::string &)> error_reported) = 0; | ||||
| }; | }; | ||||
| @@ -16,9 +16,11 @@ | |||||
| RuntimeModeService::RuntimeModeService( | RuntimeModeService::RuntimeModeService( | ||||
| const ProjectService &project_service, | const ProjectService &project_service, | ||||
| OfflineSimulationService &offline_simulation_service) | |||||
| OfflineSimulationService &offline_simulation_service, | |||||
| OnlineLogicMonitorService &online_logic_monitor_service) | |||||
| : project_service_(project_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) | if (plc_gateway_ != nullptr) | ||||
| { | { | ||||
| plc_gateway_->setCallbacks({}, {}, {}, {}); | |||||
| plc_gateway_->setCallbacks({}, {}, {}, {}, {}); | |||||
| } | } | ||||
| } | } | ||||
| @@ -47,6 +49,10 @@ ModeTransitionResult RuntimeModeService::enterEditing() | |||||
| // 先停止扫描再开放编辑,防止运行快照继续写寄存器 | // 先停止扫描再开放编辑,防止运行快照继续写寄存器 | ||||
| offline_simulation_service_.stop(); | offline_simulation_service_.stop(); | ||||
| } | } | ||||
| else if (state_.mode() == ApplicationMode::OnlineRunning) | |||||
| { | |||||
| online_logic_monitor_service_.stop(); | |||||
| } | |||||
| const ModeTransitionResult result = state_.enterEditing(); | const ModeTransitionResult result = state_.enterEditing(); | ||||
| if (result.succeeded && active_repository_ != nullptr | if (result.succeeded && active_repository_ != nullptr | ||||
| && virtual_repository_ != nullptr) | && virtual_repository_ != nullptr) | ||||
| @@ -90,13 +96,30 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() | |||||
| ModeTransitionResult RuntimeModeService::enterOnlineRunning() | 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) | if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr) | ||||
| { | { | ||||
| active_repository_->use(*plc_repository_); | active_repository_->use(*plc_repository_); | ||||
| } | } | ||||
| else if (!result.succeeded) | |||||
| { | |||||
| online_logic_monitor_service_.stop(); | |||||
| } | |||||
| return result; | return result; | ||||
| } | } | ||||
| @@ -134,6 +157,11 @@ OfflineSimulationService &RuntimeModeService::offlineSimulationService() | |||||
| return offline_simulation_service_; | return offline_simulation_service_; | ||||
| } | } | ||||
| OnlineLogicMonitorService &RuntimeModeService::onlineLogicMonitorService() | |||||
| { | |||||
| return online_logic_monitor_service_; | |||||
| } | |||||
| void RuntimeModeService::configurePlc( | void RuntimeModeService::configurePlc( | ||||
| PlcCommunicationGateway &gateway, | PlcCommunicationGateway &gateway, | ||||
| ActiveRegisterRepository &active_repository, | 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 &) | [this](const std::string &) | ||||
| { | { | ||||
| if (plc_status_changed_callback_) | if (plc_status_changed_callback_) | ||||
| @@ -10,6 +10,7 @@ | |||||
| #include "domain/runtime_state.h" | #include "domain/runtime_state.h" | ||||
| #include "offline_simulation_service.h" | #include "offline_simulation_service.h" | ||||
| #include "online_logic_monitor_service.h" | |||||
| #include "plc_communication_gateway.h" | #include "plc_communication_gateway.h" | ||||
| #include <cstdint> | #include <cstdint> | ||||
| @@ -31,7 +32,8 @@ public: | |||||
| */ | */ | ||||
| RuntimeModeService( | RuntimeModeService( | ||||
| const ProjectService &project_service, | const ProjectService &project_service, | ||||
| OfflineSimulationService &offline_simulation_service); | |||||
| OfflineSimulationService &offline_simulation_service, | |||||
| OnlineLogicMonitorService &online_logic_monitor_service); | |||||
| /** | /** | ||||
| * @brief 解除 PLC 网关回调绑定 | * @brief 解除 PLC 网关回调绑定 | ||||
| @@ -83,6 +85,7 @@ public: | |||||
| const LogicScanResult &simulationError() const; | const LogicScanResult &simulationError() const; | ||||
| /** @brief 返回服务持有的离线仿真服务引用 */ | /** @brief 返回服务持有的离线仿真服务引用 */ | ||||
| OfflineSimulationService &offlineSimulationService(); | OfflineSimulationService &offlineSimulationService(); | ||||
| OnlineLogicMonitorService &onlineLogicMonitorService(); | |||||
| /** | /** | ||||
| * @brief 注入 PLC 网关、活动仓库和两种实际数据源 | * @brief 注入 PLC 网关、活动仓库和两种实际数据源 | ||||
| @@ -133,6 +136,7 @@ public: | |||||
| private: | private: | ||||
| const ProjectService &project_service_; // 不拥有的只读工程服务 | const ProjectService &project_service_; // 不拥有的只读工程服务 | ||||
| OfflineSimulationService &offline_simulation_service_; // 不拥有的离线仿真服务 | OfflineSimulationService &offline_simulation_service_; // 不拥有的离线仿真服务 | ||||
| OnlineLogicMonitorService &online_logic_monitor_service_; // 不拥有的真机只读轨迹服务 | |||||
| RuntimeState state_; // 编辑、离线和真机模式状态机 | RuntimeState state_; // 编辑、离线和真机模式状态机 | ||||
| // PLC 缓存是否已通过有效首读建立,通信故障时清除 | // PLC 缓存是否已通过有效首读建立,通信故障时清除 | ||||
| bool initial_plc_read_completed_ = false; | bool initial_plc_read_completed_ = false; | ||||
| @@ -171,11 +171,11 @@ QString transitionErrorText(ModeTransitionError error) | |||||
| } | } | ||||
| case ModeTransitionError::ProjectNotReady: | case ModeTransitionError::ProjectNotReady: | ||||
| { | { | ||||
| return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入离线运行态"); | |||||
| return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入运行态"); | |||||
| } | } | ||||
| case ModeTransitionError::SimulationStartFailed: | case ModeTransitionError::SimulationStartFailed: | ||||
| { | { | ||||
| return MainWindow::tr("离线逻辑执行器预检或启动失败"); | |||||
| return MainWindow::tr("本地逻辑执行器预检或启动失败"); | |||||
| } | } | ||||
| case ModeTransitionError::None: | case ModeTransitionError::None: | ||||
| default: | default: | ||||
| @@ -1731,7 +1731,10 @@ void MainWindow::requestMode(ApplicationMode requested_mode) | |||||
| } | } | ||||
| if (result.error == ModeTransitionError::SimulationStartFailed) | 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()) | if (!error.message.empty()) | ||||
| { | { | ||||
| message += QStringLiteral(": ") + fromUtf8(error.message); | message += QStringLiteral(": ") + fromUtf8(error.message); | ||||
| @@ -66,6 +66,7 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||||
| register_monitor_service, ui_->monitorContainer); | register_monitor_service, ui_->monitorContainer); | ||||
| free_monitor_widget_->setObjectName(QStringLiteral("freeMonitorWidget")); | free_monitor_widget_->setObjectName(QStringLiteral("freeMonitorWidget")); | ||||
| ui_->monitorContainerLayout->addWidget(free_monitor_widget_); | ui_->monitorContainerLayout->addWidget(free_monitor_widget_); | ||||
| ui_->logicNoticeLabel->setVisible(false); | |||||
| ui_->upperSplitter->setSizes({600, 600}); | ui_->upperSplitter->setSizes({600, 600}); | ||||
| ui_->verticalSplitter->setSizes({440, 260}); | ui_->verticalSplitter->setSizes({440, 260}); | ||||
| connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, | connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, | ||||
| @@ -132,10 +133,9 @@ void RuntimeMonitorWidget::setRuntimePage(const std::string &page_id) | |||||
| void RuntimeMonitorWidget::setMode( | void RuntimeMonitorWidget::setMode( | ||||
| ApplicationMode mode, PlcConnectionState plc_state) | ApplicationMode mode, PlcConnectionState plc_state) | ||||
| { | { | ||||
| // 真机时清空本地轨迹,防止把 PC 离线执行结果误认为 PLC 内部程序轨迹 | |||||
| const bool offline = mode == ApplicationMode::OfflineRunning; | const bool offline = mode == ApplicationMode::OfflineRunning; | ||||
| const bool online = mode == ApplicationMode::OnlineRunning; | const bool online = mode == ApplicationMode::OnlineRunning; | ||||
| ui_->logicFrame->setVisible(offline); | |||||
| ui_->logicFrame->setVisible(offline || online); | |||||
| hmi_view_->setRuntimeActive(offline || online); | hmi_view_->setRuntimeActive(offline || online); | ||||
| if (!offline && !online) | if (!offline && !online) | ||||
| { | { | ||||
| @@ -143,16 +143,20 @@ void RuntimeMonitorWidget::setMode( | |||||
| } | } | ||||
| setFreeMonitorWriteEnabled(offline | setFreeMonitorWriteEnabled(offline | ||||
| || (online && plc_state == PlcConnectionState::Connected)); | || (online && plc_state == PlcConnectionState::Connected)); | ||||
| if (!offline) | |||||
| if (!offline && !online) | |||||
| { | { | ||||
| logic_view_->clearRuntimeTrace(); | logic_view_->clearRuntimeTrace(); | ||||
| latest_trace_.clear(); | latest_trace_.clear(); | ||||
| latest_fault_node_id_.clear(); | latest_fault_node_id_.clear(); | ||||
| has_logic_trace_ = false; | has_logic_trace_ = false; | ||||
| } | } | ||||
| ui_->logicLabel->setText( | |||||
| online ? tr("本地推算轨迹") : tr("梯形图运行状态")); | |||||
| ui_->logicNoticeLabel->setVisible(online); | |||||
| ui_->modeLabel->setText( | ui_->modeLabel->setText( | ||||
| offline ? tr("离线仿真 · 虚拟 M/D") | offline ? tr("离线仿真 · 虚拟 M/D") | ||||
| : online ? tr("真机运行 · PLC 缓存") : tr("当前未运行")); | |||||
| : online ? tr("真机运行 · PLC 缓存 · 本地推算轨迹") | |||||
| : tr("当前未运行")); | |||||
| free_monitor_widget_->refreshValues(mode, plc_state); | free_monitor_widget_->refreshValues(mode, plc_state); | ||||
| } | } | ||||
| @@ -63,7 +63,7 @@ public: | |||||
| void setHmiWriteEnabled(bool enabled); | void setHmiWriteEnabled(bool enabled); | ||||
| /** 设置自由监控是否允许写入寄存器 */ | /** 设置自由监控是否允许写入寄存器 */ | ||||
| void setFreeMonitorWriteEnabled(bool enabled); | void setFreeMonitorWriteEnabled(bool enabled); | ||||
| /** 只接收离线执行器轨迹;真机时由上层清空 */ | |||||
| /** 接收离线轨迹或基于 PLC 缓存计算的真机本地推算轨迹 */ | |||||
| void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); | void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); | ||||
| /** 返回自由监控子控件,供上层刷新或连接信号 */ | /** 返回自由监控子控件,供上层刷新或连接信号 */ | ||||
| FreeMonitorWidget *freeMonitorWidget() const; | FreeMonitorWidget *freeMonitorWidget() const; | ||||
| @@ -96,9 +96,9 @@ private: | |||||
| FreeMonitorWidget *free_monitor_widget_ = nullptr; | FreeMonitorWidget *free_monitor_widget_ = nullptr; | ||||
| /** 当前选中的控制逻辑标识 */ | /** 当前选中的控制逻辑标识 */ | ||||
| std::string selected_logic_id_; | std::string selected_logic_id_; | ||||
| /** 最近一次收到的离线逻辑执行轨迹 */ | |||||
| /** 最近一次收到的本地逻辑执行轨迹 */ | |||||
| LogicTraceSnapshot latest_trace_; | LogicTraceSnapshot latest_trace_; | ||||
| /** 最近一次离线执行故障节点标识 */ | |||||
| /** 最近一次本地执行故障节点标识 */ | |||||
| std::string latest_fault_node_id_; | std::string latest_fault_node_id_; | ||||
| /** 是否已经收到过可显示的逻辑轨迹 */ | /** 是否已经收到过可显示的逻辑轨迹 */ | ||||
| bool has_logic_trace_ = false; | bool has_logic_trace_ = false; | ||||
| @@ -46,6 +46,7 @@ | |||||
| <item><widget class="QComboBox" name="runtimeLogicComboBox"><property name="minimumSize"><size><width>150</width><height>0</height></size></property></widget></item> | <item><widget class="QComboBox" name="runtimeLogicComboBox"><property name="minimumSize"><size><width>150</width><height>0</height></size></property></widget></item> | ||||
| </layout> | </layout> | ||||
| </item> | </item> | ||||
| <item><widget class="QLabel" name="logicNoticeLabel"><property name="styleSheet"><string notr="true">color: #8a5a00;</string></property><property name="text"><string>真机模式:本地梯形图只读推算 仅使用 PLC 缓存显示轨迹,本地输出不写入 PLC 程序或 M/D,不影响 PLC 内部逻辑;HMI 和自由监控仍可写 PLC。</string></property><property name="wordWrap"><bool>true</bool></property></widget></item> | |||||
| <item><widget class="QWidget" name="logicViewContainer" native="true"><layout class="QVBoxLayout" name="logicViewLayout"><property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property></layout></widget></item> | <item><widget class="QWidget" name="logicViewContainer" native="true"><layout class="QVBoxLayout" name="logicViewLayout"><property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property></layout></widget></item> | ||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| @@ -133,6 +133,13 @@ void RuntimePanelController::configure() | |||||
| &OfflineSimulationService::scanCompleted, | &OfflineSimulationService::scanCompleted, | ||||
| &parent_, [this] { handleScanCompleted(); }, | &parent_, [this] { handleScanCompleted(); }, | ||||
| Qt::QueuedConnection); | 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 | RuntimeMonitorWidget *RuntimePanelController::runtimeMonitorWidget() const | ||||
| @@ -193,6 +200,40 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) | |||||
| runtime_mode_service_.mode(), | runtime_mode_service_.mode(), | ||||
| runtime_mode_service_.plcConnectionState()); | 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) | switch (state) | ||||
| { | { | ||||
| case SimulationState::Running: | case SimulationState::Running: | ||||
| @@ -265,7 +306,7 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) | |||||
| void RuntimePanelController::handleRuntimeTimer() | void RuntimePanelController::handleRuntimeTimer() | ||||
| { | { | ||||
| // 定时器只负责刷新投影;离线逻辑扫描由 OfflineSimulationService 自己的定时器驱动 | |||||
| // 定时器只刷新投影;离线按定时器扫描,真机在完整 PLC 轮询后推算 | |||||
| if (runtime_mode_service_.mode() == ApplicationMode::Editing) | if (runtime_mode_service_.mode() == ApplicationMode::Editing) | ||||
| { | { | ||||
| return; | return; | ||||
| @@ -288,19 +329,24 @@ void RuntimePanelController::handleSimulationStateChanged() | |||||
| void RuntimePanelController::handleScanCompleted() | 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; | return; | ||||
| } | } | ||||
| const std::string logic_id = current_logic_id_(); | 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) | if (runtime_monitor_widget_ != nullptr) | ||||
| { | { | ||||
| runtime_monitor_widget_->setLogicTrace( | |||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot()); | |||||
| runtime_monitor_widget_->setLogicTrace(trace); | |||||
| } | } | ||||
| } | } | ||||
| @@ -33,7 +33,7 @@ class RuntimeMonitorWindow; | |||||
| class QTimer; | class QTimer; | ||||
| class QWidget; | class QWidget; | ||||
| /** 组织运行监控窗口、刷新定时器和离线执行器反馈 */ | |||||
| /** 组织运行监控窗口、刷新定时器和本地逻辑执行器反馈 */ | |||||
| class RuntimePanelController final | class RuntimePanelController final | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -80,7 +80,7 @@ public: | |||||
| void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state); | void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| /** 应用退出时关闭运行监控窗口并停止相关刷新 */ | /** 应用退出时关闭运行监控窗口并停止相关刷新 */ | ||||
| void closeForApplicationExit(); | void closeForApplicationExit(); | ||||
| /** 根据离线执行器状态刷新运行界面 */ | |||||
| /** 根据当前模式的本地执行器状态刷新运行界面 */ | |||||
| void updateSimulationUi(bool report_fault); | void updateSimulationUi(bool report_fault); | ||||
| /** 返回当前唯一的运行监控控件 */ | /** 返回当前唯一的运行监控控件 */ | ||||
| RuntimeMonitorWidget *runtimeMonitorWidget() const; | RuntimeMonitorWidget *runtimeMonitorWidget() const; | ||||
| @@ -88,7 +88,7 @@ public: | |||||
| private: | private: | ||||
| /** 处理运行刷新定时器到期事件 */ | /** 处理运行刷新定时器到期事件 */ | ||||
| void handleRuntimeTimer(); | void handleRuntimeTimer(); | ||||
| /** 处理离线执行器状态变化 */ | |||||
| /** 处理离线或真机只读轨迹执行器状态变化 */ | |||||
| void handleSimulationStateChanged(); | void handleSimulationStateChanged(); | ||||
| /** 处理一次离线扫描完成事件 */ | /** 处理一次离线扫描完成事件 */ | ||||
| void handleScanCompleted(); | void handleScanCompleted(); | ||||
| @@ -115,7 +115,7 @@ private: | |||||
| HmiEditorWidget &hmi_editor_widget_; | HmiEditorWidget &hmi_editor_widget_; | ||||
| /** 主窗口中的梯形图编辑器,不由控制器拥有 */ | /** 主窗口中的梯形图编辑器,不由控制器拥有 */ | ||||
| LogicEditorWidget &logic_editor_widget_; | LogicEditorWidget &logic_editor_widget_; | ||||
| /** 显示离线执行器状态的标签 */ | |||||
| /** 显示当前本地执行器状态的标签 */ | |||||
| QLabel &executor_status_label_; | QLabel &executor_status_label_; | ||||
| /** 获取当前控制逻辑标识的回调 */ | /** 获取当前控制逻辑标识的回调 */ | ||||
| std::function<std::string()> current_logic_id_; | std::function<std::string()> current_logic_id_; | ||||
| @@ -796,8 +796,8 @@ void testRuntimeStateBoundaries() | |||||
| "online mode must require an initial PLC read"); | "online mode must require an initial PLC read"); | ||||
| require(state.enterOnlineRunning(true).succeeded, | require(state.enterOnlineRunning(true).succeeded, | ||||
| "editing may enter online mode after initial PLC read"); | "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"); | require(state.policy().usesPlcRegisters, "online mode must use PLC registers"); | ||||
| } | } | ||||
| @@ -4,6 +4,7 @@ | |||||
| #include "domain/virtual_register_repository.h" | #include "domain/virtual_register_repository.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| #include "services/online_logic_monitor_service.h" | |||||
| #include "services/software_logic_executor.h" | #include "services/software_logic_executor.h" | ||||
| #include "support/test_support.h" | #include "support/test_support.h" | ||||
| @@ -688,6 +689,57 @@ void testRepositoryFailureEntersFaultState() | |||||
| "fault feedback must retain logic, rung and node context"); | "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 | } // namespace | ||||
| int main(int argc, char *argv[]) | int main(int argc, char *argv[]) | ||||
| @@ -709,6 +761,8 @@ int main(int argc, char *argv[]) | |||||
| testHmiSimulationClosedLoop(); | testHmiSimulationClosedLoop(); | ||||
| testSimulationLifecycleSnapshotAndFault(); | testSimulationLifecycleSnapshotAndFault(); | ||||
| testRepositoryFailureEntersFaultState(); | testRepositoryFailureEntersFaultState(); | ||||
| testOnlineMonitorUsesTemporaryRegistersWithoutWritingPlcSource(); | |||||
| testOnlineMonitorPreservesFaultAfterStopping(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||
| @@ -72,11 +72,13 @@ public: | |||||
| std::function<void()> state_callback, | std::function<void()> state_callback, | ||||
| std::function<void(bool)> initial_callback, | std::function<void(bool)> initial_callback, | ||||
| std::function<void()> cache_callback, | std::function<void()> cache_callback, | ||||
| std::function<void()> poll_cycle_callback, | |||||
| std::function<void(const std::string &)> error_callback) override | std::function<void(const std::string &)> error_callback) override | ||||
| { | { | ||||
| state_changed = std::move(state_callback); | state_changed = std::move(state_callback); | ||||
| initial_read_changed = std::move(initial_callback); | initial_read_changed = std::move(initial_callback); | ||||
| cache_updated = std::move(cache_callback); | cache_updated = std::move(cache_callback); | ||||
| poll_cycle_completed = std::move(poll_cycle_callback); | |||||
| error_reported = std::move(error_callback); | error_reported = std::move(error_callback); | ||||
| } | } | ||||
| @@ -120,6 +122,7 @@ public: | |||||
| std::function<void()> state_changed; | std::function<void()> state_changed; | ||||
| std::function<void(bool)> initial_read_changed; | std::function<void(bool)> initial_read_changed; | ||||
| std::function<void()> cache_updated; | std::function<void()> cache_updated; | ||||
| std::function<void()> poll_cycle_completed; | |||||
| std::function<void(const std::string &)> error_reported; | std::function<void(const std::string &)> error_reported; | ||||
| }; | }; | ||||
| @@ -189,8 +192,10 @@ void testRuntimeRepositorySwitchingAndDisconnect() | |||||
| PlcRegisterRepository plc_repository; | PlcRegisterRepository plc_repository; | ||||
| ActiveRegisterRepository active_repository(virtual_repository); | ActiveRegisterRepository active_repository(virtual_repository); | ||||
| OfflineSimulationService simulation_service(virtual_repository); | OfflineSimulationService simulation_service(virtual_repository); | ||||
| OnlineLogicMonitorService online_monitor_service(plc_repository); | |||||
| FakePlcGateway gateway; | FakePlcGateway gateway; | ||||
| RuntimeModeService service(project_service, simulation_service); | |||||
| RuntimeModeService service( | |||||
| project_service, simulation_service, online_monitor_service); | |||||
| service.configurePlc( | service.configurePlc( | ||||
| gateway, active_repository, virtual_repository, plc_repository); | gateway, active_repository, virtual_repository, plc_repository); | ||||
| @@ -222,6 +227,9 @@ void testRuntimeRepositorySwitchingAndDisconnect() | |||||
| gateway.disconnectDevice(); | gateway.disconnectDevice(); | ||||
| require(service.mode() == ApplicationMode::Editing, | require(service.mode() == ApplicationMode::Editing, | ||||
| "an online disconnect must return the application to editing mode"); | "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(), | require(!service.initialPlcReadCompleted(), | ||||
| "an online disconnect must clear the initial read flag"); | "an online disconnect must clear the initial read flag"); | ||||
| require(active_repository.readBit({RegisterArea::M, 5}).succeeded | require(active_repository.readBit({RegisterArea::M, 5}).succeeded | ||||
| @@ -237,8 +245,10 @@ void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect() | |||||
| PlcRegisterRepository plc_repository; | PlcRegisterRepository plc_repository; | ||||
| ActiveRegisterRepository active_repository(virtual_repository); | ActiveRegisterRepository active_repository(virtual_repository); | ||||
| OfflineSimulationService simulation_service(virtual_repository); | OfflineSimulationService simulation_service(virtual_repository); | ||||
| OnlineLogicMonitorService online_monitor_service(plc_repository); | |||||
| FakePlcGateway gateway; | FakePlcGateway gateway; | ||||
| RuntimeModeService service(project_service, simulation_service); | |||||
| RuntimeModeService service( | |||||
| project_service, simulation_service, online_monitor_service); | |||||
| service.configurePlc( | service.configurePlc( | ||||
| gateway, active_repository, virtual_repository, plc_repository); | gateway, active_repository, virtual_repository, plc_repository); | ||||
| @@ -254,6 +264,9 @@ void testRuntimeFaultRevokesOnlineReadinessAndAllowsReconnect() | |||||
| "PLC communication timed out while the serial port remained open"); | "PLC communication timed out while the serial port remained open"); | ||||
| require(service.mode() == ApplicationMode::Editing, | require(service.mode() == ApplicationMode::Editing, | ||||
| "a PLC communication fault must return online running to 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(), | require(!service.initialPlcReadCompleted(), | ||||
| "a PLC communication fault must revoke initial read readiness"); | "a PLC communication fault must revoke initial read readiness"); | ||||
| require(service.enterOnlineRunning().error | require(service.enterOnlineRunning().error | ||||
| @@ -97,11 +97,13 @@ SERVICE_LOGIC_HEADERS = \ | |||||
| SERVICE_OFFLINE_SOURCES = \ | SERVICE_OFFLINE_SOURCES = \ | ||||
| ../src/services/software_logic_executor.cpp \ | ../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 = \ | SERVICE_OFFLINE_HEADERS = \ | ||||
| ../src/services/software_logic_executor.h \ | ../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 = \ | SERVICE_RUNTIME_SOURCES = \ | ||||
| ../src/services/runtime_mode_service.cpp \ | ../src/services/runtime_mode_service.cpp \ | ||||
| @@ -54,11 +54,13 @@ public: | |||||
| std::function<void()> state_callback, | std::function<void()> state_callback, | ||||
| std::function<void(bool)> initial_callback, | std::function<void(bool)> initial_callback, | ||||
| std::function<void()> cache_callback, | std::function<void()> cache_callback, | ||||
| std::function<void()> poll_cycle_callback, | |||||
| std::function<void(const std::string &)> error_callback) override | std::function<void(const std::string &)> error_callback) override | ||||
| { | { | ||||
| state_changed = std::move(state_callback); | state_changed = std::move(state_callback); | ||||
| initial_read_changed = std::move(initial_callback); | initial_read_changed = std::move(initial_callback); | ||||
| cache_updated = std::move(cache_callback); | cache_updated = std::move(cache_callback); | ||||
| poll_cycle_completed = std::move(poll_cycle_callback); | |||||
| error_reported = std::move(error_callback); | error_reported = std::move(error_callback); | ||||
| } | } | ||||
| @@ -71,6 +73,14 @@ public: | |||||
| } | } | ||||
| } | } | ||||
| void completePollCycle() | |||||
| { | |||||
| if (poll_cycle_completed) | |||||
| { | |||||
| poll_cycle_completed(); | |||||
| } | |||||
| } | |||||
| const std::vector<RegisterAddress> &pollAddresses() const | const std::vector<RegisterAddress> &pollAddresses() const | ||||
| { | { | ||||
| return poll_addresses; | return poll_addresses; | ||||
| @@ -83,6 +93,7 @@ private: | |||||
| std::function<void()> state_changed; | std::function<void()> state_changed; | ||||
| std::function<void(bool)> initial_read_changed; | std::function<void(bool)> initial_read_changed; | ||||
| std::function<void()> cache_updated; | std::function<void()> cache_updated; | ||||
| std::function<void()> poll_cycle_completed; | |||||
| std::function<void(const std::string &)> error_reported; | std::function<void(const std::string &)> error_reported; | ||||
| std::vector<RegisterAddress> poll_addresses; | std::vector<RegisterAddress> poll_addresses; | ||||
| }; | }; | ||||
| @@ -111,8 +122,10 @@ void testModeTransitions() | |||||
| VirtualRegisterRepository plc_repository; | VirtualRegisterRepository plc_repository; | ||||
| ActiveRegisterRepository active_repository(virtual_repository); | ActiveRegisterRepository active_repository(virtual_repository); | ||||
| OfflineSimulationService simulation_service(virtual_repository); | OfflineSimulationService simulation_service(virtual_repository); | ||||
| OnlineLogicMonitorService online_monitor_service(plc_repository); | |||||
| ReadyPlcGateway gateway; | ReadyPlcGateway gateway; | ||||
| RuntimeModeService service(project_service, simulation_service); | |||||
| RuntimeModeService service( | |||||
| project_service, simulation_service, online_monitor_service); | |||||
| service.configurePlc( | service.configurePlc( | ||||
| gateway, active_repository, virtual_repository, plc_repository); | gateway, active_repository, virtual_repository, plc_repository); | ||||
| @@ -241,14 +254,26 @@ void testModeTransitions() | |||||
| gateway.completeInitialRead(); | gateway.completeInitialRead(); | ||||
| require(service.initialPlcReadCompleted(), | require(service.initialPlcReadCompleted(), | ||||
| "service must retain the initial PLC read state"); | "service must retain the initial PLC read state"); | ||||
| plc_repository.writeBit({RegisterArea::M, 20}, true); | |||||
| require(service.enterOnlineRunning().succeeded, | require(service.enterOnlineRunning().succeeded, | ||||
| "online running must start after an initial PLC read"); | "online running must start after an initial PLC read"); | ||||
| require(service.simulationState() == SimulationState::Stopped, | 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, | require(service.policy().usesPlcRegisters, | ||||
| "online running must use PLC registers"); | "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 | } // namespace | ||||
| @@ -11,6 +11,7 @@ | |||||
| #include "support/test_support.h" | #include "support/test_support.h" | ||||
| #include "ui/hmi_editor_widget.h" | #include "ui/hmi_editor_widget.h" | ||||
| #include "ui/logic_editor_widget.h" | #include "ui/logic_editor_widget.h" | ||||
| #include "ui/runtime_monitor_widget.h" | |||||
| #include "ui/runtime_panel_controller.h" | #include "ui/runtime_panel_controller.h" | ||||
| #include <QApplication> | #include <QApplication> | ||||
| @@ -54,7 +55,9 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() | |||||
| ProjectService project_service(storage); | ProjectService project_service(storage); | ||||
| VirtualRegisterRepository virtual_repository; | VirtualRegisterRepository virtual_repository; | ||||
| OfflineSimulationService simulation_service(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); | HmiEditorService hmi_editor_service(project_service); | ||||
| HmiRuntimeService hmi_runtime_service(virtual_repository); | HmiRuntimeService hmi_runtime_service(virtual_repository); | ||||
| LogicEditorService logic_editor_service(project_service); | LogicEditorService logic_editor_service(project_service); | ||||
| @@ -102,6 +105,24 @@ void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() | |||||
| controller.enterRuntime( | controller.enterRuntime( | ||||
| {}, logic.id, runtime_mode_service.mode(), | {}, logic.id, runtime_mode_service.mode(), | ||||
| runtime_mode_service.plcConnectionState()); | runtime_mode_service.plcConnectionState()); | ||||
| QLabel *logic_label = controller.runtimeMonitorWidget() | |||||
| ->findChild<QLabel *>(QStringLiteral("logicLabel")); | |||||
| QLabel *notice_label = controller.runtimeMonitorWidget() | |||||
| ->findChild<QLabel *>(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, | require(simulation_service.executeOnce().succeeded, | ||||
| "offline simulation must produce a trace before editing"); | "offline simulation must produce a trace before editing"); | ||||
| require(simulation_service.traceSnapshot() | require(simulation_service.traceSnapshot() | ||||
| @@ -26,6 +26,7 @@ foreach ($requiredPath in @($qmakePath, $makePath, $testSourceRoot)) { | |||||
| $functionalTargets = @( | $functionalTargets = @( | ||||
| 'domain_tests', | 'domain_tests', | ||||
| 'application_settings_tests', | |||||
| 'alarm_service_tests', | 'alarm_service_tests', | ||||
| 'hmi_editor_service_tests', | 'hmi_editor_service_tests', | ||||
| 'logic_editor_service_tests', | 'logic_editor_service_tests', | ||||