瀏覽代碼

feat: 增加真机本地梯形图只读推算

main
suyu 3 週之前
父節點
當前提交
19398b2641
共有 23 個文件被更改,包括 571 次插入46 次删除
  1. +2
    -0
      app/integrated_platform.pro
  2. +3
    -3
      app/src/domain/runtime_state.h
  3. +14
    -0
      app/src/infrastructure/plc_communication_service.cpp
  4. +4
    -0
      app/src/infrastructure/plc_communication_service.h
  5. +5
    -2
      app/src/main.cpp
  6. +209
    -0
      app/src/services/online_logic_monitor_service.cpp
  7. +70
    -0
      app/src/services/online_logic_monitor_service.h
  8. +2
    -0
      app/src/services/plc_communication_gateway.h
  9. +53
    -6
      app/src/services/runtime_mode_service.cpp
  10. +5
    -1
      app/src/services/runtime_mode_service.h
  11. +6
    -3
      app/src/ui/main_window.cpp
  12. +8
    -4
      app/src/ui/runtime_monitor_widget.cpp
  13. +3
    -3
      app/src/ui/runtime_monitor_widget.h
  14. +1
    -0
      app/src/ui/runtime_monitor_widget.ui
  15. +55
    -9
      app/src/ui/runtime_panel_controller.cpp
  16. +4
    -4
      app/src/ui/runtime_panel_controller.h
  17. +2
    -2
      app/tests/domain_tests.cpp
  18. +54
    -0
      app/tests/offline_simulation_service_tests.cpp
  19. +15
    -2
      app/tests/plc_runtime_tests.cpp
  20. +4
    -2
      app/tests/pri/test_layers.pri
  21. +29
    -4
      app/tests/runtime_mode_service_tests.cpp
  22. +22
    -1
      app/tests/runtime_panel_controller_tests.cpp
  23. +1
    -0
      scripts/run_qt_tests.ps1

+ 2
- 0
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 \


+ 3
- 3
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 // 当前模式的本地执行器预检或启动失败
};

// 模式切换操作结果


+ 14
- 0
app/src/infrastructure/plc_communication_service.cpp 查看文件

@@ -312,12 +312,14 @@ void PlcCommunicationService::setCallbacks(
std::function<void()> state_changed,
std::function<void(bool)> initial_read_changed,
std::function<void()> cache_updated,
std::function<void()> poll_cycle_completed,
std::function<void(const std::string &)> 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<RegisterAddress> addresses = pending_poll_addresses_;
applyPollAddresses(addresses);
}
if (poll_cycle_completed)
{
emit pollCycleCompleted();
if (poll_cycle_completed_callback_)
{
poll_cycle_completed_callback_();
}
}
});
}



+ 4
- 0
app/src/infrastructure/plc_communication_service.h 查看文件

@@ -55,6 +55,7 @@ public:
std::function<void()> state_changed,
std::function<void(bool)> initial_read_changed,
std::function<void()> cache_updated,
std::function<void()> poll_cycle_completed,
std::function<void(const std::string &)> 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<void()> state_changed_callback_; // 状态变化时调用
std::function<void(bool)> initial_read_changed_callback_; // 首读资格变化时调用
std::function<void()> cache_updated_callback_; // 缓存更新后调用
std::function<void()> poll_cycle_completed_callback_; // 完整轮询一轮后调用
std::function<void(const std::string &)> error_reported_callback_; // 发生错误时调用
};

+ 5
- 2
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,


+ 209
- 0
app/src/services/online_logic_monitor_service.cpp 查看文件

@@ -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();
}

+ 70
- 0
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 <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_;
};

+ 2
- 0
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<void()> state_changed,
std::function<void(bool)> initial_read_changed,
std::function<void()> cache_updated,
std::function<void()> poll_cycle_completed,
std::function<void(const std::string &)> error_reported) = 0;
};

+ 53
- 6
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_)


+ 5
- 1
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 <cstdint>
@@ -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;


+ 6
- 3
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);


+ 8
- 4
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);
}



+ 3
- 3
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;


+ 1
- 0
app/src/ui/runtime_monitor_widget.ui 查看文件

@@ -46,6 +46,7 @@
<item><widget class="QComboBox" name="runtimeLogicComboBox"><property name="minimumSize"><size><width>150</width><height>0</height></size></property></widget></item>
</layout>
</item>
<item><widget class="QLabel" name="logicNoticeLabel"><property name="styleSheet"><string notr="true">color: #8a5a00;</string></property><property name="text"><string>真机模式:本地梯形图只读推算&#10;仅使用 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>
</layout>
</widget>


+ 55
- 9
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);
}
}

+ 4
- 4
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<std::string()> current_logic_id_;


+ 2
- 2
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");
}



+ 54
- 0
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)
{


+ 15
- 2
app/tests/plc_runtime_tests.cpp 查看文件

@@ -72,11 +72,13 @@ public:
std::function<void()> state_callback,
std::function<void(bool)> initial_callback,
std::function<void()> cache_callback,
std::function<void()> poll_cycle_callback,
std::function<void(const std::string &)> 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<void()> state_changed;
std::function<void(bool)> initial_read_changed;
std::function<void()> cache_updated;
std::function<void()> poll_cycle_completed;
std::function<void(const std::string &)> 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


+ 4
- 2
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 \


+ 29
- 4
app/tests/runtime_mode_service_tests.cpp 查看文件

@@ -54,11 +54,13 @@ public:
std::function<void()> state_callback,
std::function<void(bool)> initial_callback,
std::function<void()> cache_callback,
std::function<void()> poll_cycle_callback,
std::function<void(const std::string &)> 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<RegisterAddress> &pollAddresses() const
{
return poll_addresses;
@@ -83,6 +93,7 @@ private:
std::function<void()> state_changed;
std::function<void(bool)> initial_read_changed;
std::function<void()> cache_updated;
std::function<void()> poll_cycle_completed;
std::function<void(const std::string &)> error_reported;
std::vector<RegisterAddress> 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


+ 22
- 1
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 <QApplication>
@@ -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<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,
"offline simulation must produce a trace before editing");
require(simulation_service.traceSnapshot()


+ 1
- 0
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',


Loading…
取消
儲存