Просмотр исходного кода

feat: 增加运行监控工作台

main
suyu 1 месяц назад
Родитель
Сommit
ff7b0f3907
13 измененных файлов: 747 добавлений и 11 удалений
  1. +8
    -2
      app/integrated_platform.pro
  2. +4
    -1
      app/src/main.cpp
  3. +193
    -0
      app/src/ui/free_monitor_widget.cpp
  4. +41
    -0
      app/src/ui/free_monitor_widget.h
  5. +46
    -0
      app/src/ui/free_monitor_widget.ui
  6. +80
    -2
      app/src/ui/main_window.cpp
  7. +7
    -0
      app/src/ui/main_window.h
  8. +11
    -0
      app/src/ui/main_window.ui
  9. +89
    -0
      app/src/ui/runtime_monitor_widget.cpp
  10. +49
    -0
      app/src/ui/runtime_monitor_widget.h
  11. +54
    -0
      app/src/ui/runtime_monitor_widget.ui
  12. +152
    -3
      app/tests/main_window_tests.cpp
  13. +13
    -3
      app/tests/main_window_tests.pro

+ 8
- 2
app/integrated_platform.pro Просмотреть файл

@@ -18,6 +18,8 @@ SOURCES += \
src/main.cpp \
src/ui/main_window.cpp \
src/ui/plc_connection_dialog.cpp \
src/ui/free_monitor_widget.cpp \
src/ui/runtime_monitor_widget.cpp \
src/domain/register_address.cpp \
src/domain/register_repository.cpp \
src/domain/active_register_repository.cpp \
@@ -43,6 +45,8 @@ SOURCES += \
HEADERS += \
src/ui/main_window.h \
src/ui/plc_connection_dialog.h \
src/ui/free_monitor_widget.h \
src/ui/runtime_monitor_widget.h \
src/domain/register_address.h \
src/domain/register_repository.h \
src/domain/active_register_repository.h \
@@ -59,8 +63,8 @@ HEADERS += \
src/services/software_logic_executor.h \
src/services/offline_simulation_service.h \
src/services/runtime_mode_service.h \
src/services/plc_communication_gateway.h \
src/services/register_monitor_service.h \
src/services/plc_communication_gateway.h \
src/infrastructure/plc_register_repository.h \
src/infrastructure/plc_communication_service.h \
src/infrastructure/json_project_storage.h \
@@ -69,4 +73,6 @@ HEADERS += \

FORMS += \
src/ui/main_window.ui \
src/ui/plc_connection_dialog.ui
src/ui/plc_connection_dialog.ui \
src/ui/free_monitor_widget.ui \
src/ui/runtime_monitor_widget.ui

+ 4
- 1
app/src/main.cpp Просмотреть файл

@@ -18,6 +18,7 @@
#include "services/offline_simulation_service.h"
#include "services/project_service.h"
#include "services/runtime_mode_service.h"
#include "services/register_monitor_service.h"
#include "ui/main_window.h"

int main(int argc, char *argv[])
@@ -37,6 +38,7 @@ int main(int argc, char *argv[])
ActiveRegisterRepository active_register_repository(virtual_register_repository);
PlcCommunicationService plc_communication_service(plc_register_repository);
HmiRuntimeService hmi_runtime_service(active_register_repository);
RegisterMonitorService register_monitor_service(active_register_repository);
OfflineSimulationService offline_simulation_service(virtual_register_repository);
RuntimeModeService runtime_mode_service(
project_service, offline_simulation_service);
@@ -50,7 +52,8 @@ int main(int argc, char *argv[])
project_service,
hmi_editor_service,
logic_editor_service,
hmi_runtime_service);
hmi_runtime_service,
register_monitor_service);
main_window.show();

return application.exec();


+ 193
- 0
app/src/ui/free_monitor_widget.cpp Просмотреть файл

@@ -0,0 +1,193 @@
#include "free_monitor_widget.h"

#include "services/register_monitor_service.h"
#include "ui_free_monitor_widget.h"

#include <QHeaderView>
#include <QMessageBox>
#include <QStyle>
#include <QTableWidgetItem>

#include <set>

namespace {

QString fromUtf8(const std::string &value)
{
return QString::fromUtf8(value.data(), static_cast<int>(value.size()));
}

QString addressText(const RegisterAddress &address)
{
return QString::fromStdString(address.toString());
}

} // namespace

FreeMonitorWidget::FreeMonitorWidget(
RegisterMonitorService &service,
QWidget *parent)
: QWidget(parent),
ui_(std::make_unique<Ui::FreeMonitorWidget>()),
service_(service)
{
ui_->setupUi(this);
ui_->removeButton->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
ui_->clearButton->setIcon(style()->standardIcon(QStyle::SP_DialogResetButton));
ui_->monitorTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
ui_->monitorTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
ui_->monitorTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
ui_->monitorTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
ui_->monitorTable->verticalHeader()->setVisible(false);
connect(ui_->addButton, &QPushButton::clicked, this, &FreeMonitorWidget::addAddresses);
connect(ui_->addressEdit, &QLineEdit::returnPressed,
this, &FreeMonitorWidget::addAddresses);
connect(ui_->removeButton, &QToolButton::clicked,
this, &FreeMonitorWidget::removeSelectedAddresses);
connect(ui_->clearButton, &QToolButton::clicked,
this, &FreeMonitorWidget::clearAddresses);
reloadAddresses();
}

FreeMonitorWidget::~FreeMonitorWidget() = default;

void FreeMonitorWidget::refreshValues(
ApplicationMode mode, PlcConnectionState plc_state)
{
QString source = tr("数据源:未启用");
bool values_available = false;
bool communication_fault = false;
if (mode == ApplicationMode::OfflineRunning)
{
source = tr("数据源:虚拟 M/D");
values_available = true;
}
else if (mode == ApplicationMode::OnlineRunning)
{
source = tr("数据源:PLC 缓存");
values_available = true;
communication_fault = plc_state == PlcConnectionState::Faulted;
}
ui_->sourceLabel->setText(source);

const std::vector<MonitorValue> values = values_available
? service_.values(communication_fault) : std::vector<MonitorValue>{};
for (int row = 0; row < ui_->monitorTable->rowCount(); ++row)
{
QTableWidgetItem *value_item = ui_->monitorTable->item(row, 2);
QTableWidgetItem *state_item = ui_->monitorTable->item(row, 3);
if (!values_available || static_cast<std::size_t>(row) >= values.size())
{
value_item->setText(QStringLiteral("--"));
state_item->setText(tr("未运行"));
continue;
}
const MonitorValue &value = values.at(static_cast<std::size_t>(row));
if (value.state == MonitorValueState::Unavailable)
{
value_item->setText(QStringLiteral("--"));
state_item->setText(mode == ApplicationMode::OnlineRunning
? tr("等待读取") : tr("不可用"));
continue;
}
value_item->setText(value.address.area() == RegisterArea::M
? (value.bitValue ? QStringLiteral("ON")
: QStringLiteral("OFF"))
: QString::number(value.wordValue));
state_item->setText(value.state == MonitorValueState::CommunicationFault
? tr("通信故障,最后有效值") : tr("有效"));
}
}

void FreeMonitorWidget::reloadAddresses()
{
const std::vector<RegisterAddress> &addresses = service_.addresses();
ui_->monitorTable->setRowCount(static_cast<int>(addresses.size()));
for (std::size_t index = 0; index < addresses.size(); ++index)
{
const RegisterAddress &address = addresses[index];
const int row = static_cast<int>(index);
ui_->monitorTable->setItem(row, 0, new QTableWidgetItem(addressText(address)));
ui_->monitorTable->setItem(
row, 1, new QTableWidgetItem(address.area() == RegisterArea::M
? tr("位") : tr("16 位整数")));
ui_->monitorTable->setItem(row, 2, new QTableWidgetItem(QStringLiteral("--")));
ui_->monitorTable->setItem(row, 3, new QTableWidgetItem(tr("未运行")));
}
}

void FreeMonitorWidget::addAddresses()
{
const QByteArray address = ui_->addressEdit->text().toUtf8();
const RegisterMonitorResult result = service_.addRange(
std::string(address.constData(), static_cast<std::size_t>(address.size())),
ui_->countSpinBox->value());
if (result.succeeded)
{
reloadAddresses();
ui_->addressEdit->selectAll();
emit monitorAddressesChanged();
emit operationMessage(tr("已添加 %1 个监控地址").arg(result.affectedCount));
return;
}
handleResult(tr("添加监控地址"), false, result.message);
}

void FreeMonitorWidget::removeSelectedAddresses()
{
std::set<int> rows;
for (const QModelIndex &index : ui_->monitorTable->selectionModel()->selectedRows())
{
rows.insert(index.row());
}
std::vector<RegisterAddress> addresses;
const std::vector<RegisterAddress> &all_addresses = service_.addresses();
for (int row : rows)
{
if (row >= 0 && static_cast<std::size_t>(row) < all_addresses.size())
{
addresses.push_back(all_addresses.at(static_cast<std::size_t>(row)));
}
}
const RegisterMonitorResult result = service_.remove(addresses);
if (result.succeeded)
{
reloadAddresses();
emit monitorAddressesChanged();
emit operationMessage(tr("已删除 %1 个监控地址").arg(result.affectedCount));
return;
}
handleResult(tr("删除监控地址"), false, result.message);
}

void FreeMonitorWidget::clearAddresses()
{
if (service_.addresses().empty())
{
return;
}
if (QMessageBox::question(
this, tr("清空自由监控"), tr("确定清空当前监控表吗?"))
!= QMessageBox::Yes)
{
return;
}
const RegisterMonitorResult result = service_.clear();
if (result.succeeded)
{
reloadAddresses();
emit monitorAddressesChanged();
emit operationMessage(tr("自由监控已清空"));
return;
}
handleResult(tr("清空自由监控"), false, result.message);
}

void FreeMonitorWidget::handleResult(
const QString &action, bool succeeded, const std::string &message)
{
if (!succeeded)
{
emit operationMessage(action + QStringLiteral(":") + fromUtf8(message));
}
}

+ 41
- 0
app/src/ui/free_monitor_widget.h Просмотреть файл

@@ -0,0 +1,41 @@
#pragma once

#include "domain/runtime_state.h"
#include "services/plc_communication_gateway.h"

#include <QWidget>

#include <memory>

namespace Ui {
class FreeMonitorWidget;
}

class RegisterMonitorService;

class FreeMonitorWidget final : public QWidget
{
Q_OBJECT

public:
explicit FreeMonitorWidget(
RegisterMonitorService &service,
QWidget *parent = nullptr);
~FreeMonitorWidget() override;

void refreshValues(ApplicationMode mode, PlcConnectionState plc_state);
void reloadAddresses();

signals:
void monitorAddressesChanged();
void operationMessage(const QString &message);

private:
void addAddresses();
void removeSelectedAddresses();
void clearAddresses();
void handleResult(const QString &action, bool succeeded, const std::string &message);

std::unique_ptr<Ui::FreeMonitorWidget> ui_;
RegisterMonitorService &service_;
};

+ 46
- 0
app/src/ui/free_monitor_widget.ui Просмотреть файл

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FreeMonitorWidget</class>
<widget class="QWidget" name="FreeMonitorWidget">
<property name="minimumSize">
<size><width>420</width><height>190</height></size>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing"><number>6</number></property>
<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>
<item>
<layout class="QGridLayout" name="commandLayout">
<item row="0" column="0"><widget class="QLabel" name="addressLabel"><property name="text"><string>起始地址</string></property></widget></item>
<item row="0" column="1"><widget class="QLineEdit" name="addressEdit"><property name="minimumSize"><size><width>90</width><height>0</height></size></property><property name="maximumSize"><size><width>160</width><height>16777215</height></size></property><property name="placeholderText"><string>M0 或 D0</string></property><property name="clearButtonEnabled"><bool>true</bool></property></widget></item>
<item row="0" column="2"><widget class="QLabel" name="countLabel"><property name="text"><string>连续个数</string></property></widget></item>
<item row="0" column="3"><widget class="QSpinBox" name="countSpinBox"><property name="minimum"><number>1</number></property><property name="maximum"><number>64</number></property><property name="value"><number>1</number></property></widget></item>
<item row="0" column="4"><widget class="QPushButton" name="addButton"><property name="text"><string>添加</string></property></widget></item>
<item row="0" column="5"><spacer name="firstRowSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
<item row="1" column="0"><widget class="QToolButton" name="removeButton"><property name="toolTip"><string>删除选中的监控地址</string></property><property name="text"><string>删除</string></property></widget></item>
<item row="1" column="1"><widget class="QToolButton" name="clearButton"><property name="toolTip"><string>清空监控表</string></property><property name="text"><string>清空</string></property></widget></item>
<item row="1" column="2" colspan="4"><widget class="QLabel" name="sourceLabel"><property name="text"><string>数据源:未启用</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="monitorTable">
<property name="alternatingRowColors"><bool>true</bool></property>
<property name="selectionMode"><enum>QAbstractItemView::ExtendedSelection</enum></property>
<property name="selectionBehavior"><enum>QAbstractItemView::SelectRows</enum></property>
<property name="editTriggers"><set>QAbstractItemView::NoEditTriggers</set></property>
<property name="showGrid"><bool>true</bool></property>
<property name="columnCount"><number>4</number></property>
<property name="rowCount"><number>0</number></property>
<column><property name="text"><string>地址</string></property></column>
<column><property name="text"><string>类型</string></property></column>
<column><property name="text"><string>当前值</string></property></column>
<column><property name="text"><string>状态</string></property></column>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

+ 80
- 2
app/src/ui/main_window.cpp Просмотреть файл

@@ -3,11 +3,14 @@
#include "hmi_editor_widget.h"
#include "logic_editor_widget.h"
#include "plc_connection_dialog.h"
#include "runtime_monitor_widget.h"
#include "free_monitor_widget.h"
#include "services/hmi_editor_service.h"
#include "services/hmi_runtime_service.h"
#include "services/logic_editor_service.h"
#include "services/project_service.h"
#include "services/runtime_mode_service.h"
#include "services/register_monitor_service.h"
#include "ui_main_window.h"

#include <QActionGroup>
@@ -159,6 +162,7 @@ MainWindow::MainWindow(
HmiEditorService &hmi_editor_service,
LogicEditorService &logic_editor_service,
HmiRuntimeService &hmi_runtime_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent)
: QMainWindow(parent),
ui_(std::make_unique<Ui::MainWindow>()),
@@ -166,7 +170,8 @@ MainWindow::MainWindow(
project_service_(project_service),
hmi_editor_service_(hmi_editor_service),
logic_editor_service_(logic_editor_service),
hmi_runtime_service_(hmi_runtime_service)
hmi_runtime_service_(hmi_runtime_service),
register_monitor_service_(register_monitor_service)
{
ui_->setupUi(this);
configureAppearance();
@@ -175,6 +180,7 @@ MainWindow::MainWindow(
configurePlcConnection();
configureHmiEditor();
configureLogicEditor();
configureRuntimeMonitor();
runtime_mode_service_.setPlcStatusChangedCallback(
[this] { schedulePlcStatusUpdate(); });
hmi_editor_service_.ensureDefaultPage();
@@ -326,10 +332,14 @@ void MainWindow::configureActions()
{
showControlProperties(selected_control_id_);
}
else
else if (index == 1)
{
showLogicNodeProperties(selected_logic_node_id_);
}
if (index == 0 || index == 1)
{
last_editing_tab_index_ = index;
}
});
ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0);
ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1);
@@ -375,6 +385,7 @@ void MainWindow::configureAppearance()
ui_->deleteLogicAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
ui_->editorTabWidget->setTabIcon(0, style()->standardIcon(QStyle::SP_DesktopIcon));
ui_->editorTabWidget->setTabIcon(1, style()->standardIcon(QStyle::SP_FileDialogListView));
ui_->editorTabWidget->setTabIcon(2, style()->standardIcon(QStyle::SP_ComputerIcon));
ui_->outputDock->setMaximumHeight(220);

mode_status_label_ = new QLabel(this);
@@ -444,6 +455,9 @@ void MainWindow::configureHmiEditor()
if (runtime_mode_service_.mode() != ApplicationMode::Editing)
{
hmi_editor_widget_->refreshRuntimeValues();
runtime_monitor_widget_->refreshValues(
runtime_mode_service_.mode(),
runtime_mode_service_.plcConnectionState());
updateSimulationUi(false);
}
});
@@ -460,6 +474,8 @@ void MainWindow::configureHmiEditor()
{
logic_editor_widget_->setRuntimeTrace(
runtime_mode_service_.offlineSimulationService().traceSnapshot());
runtime_monitor_widget_->setLogicTrace(
runtime_mode_service_.offlineSimulationService().traceSnapshot());
},
Qt::QueuedConnection);
}
@@ -489,6 +505,37 @@ void MainWindow::configureLogicEditor()
});
}

void MainWindow::configureRuntimeMonitor()
{
runtime_monitor_widget_ = new RuntimeMonitorWidget(
hmi_editor_service_,
hmi_runtime_service_,
logic_editor_service_,
register_monitor_service_,
ui_->runtimeMonitorTab);
runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget"));
ui_->runtimeMonitorLayout->addWidget(runtime_monitor_widget_);
connect(runtime_monitor_widget_->freeMonitorWidget(),
&FreeMonitorWidget::monitorAddressesChanged,
this,
[this]
{
runtime_mode_service_.setMonitorAddresses(
register_monitor_service_.addresses());
runtime_monitor_widget_->refreshValues(
runtime_mode_service_.mode(),
runtime_mode_service_.plcConnectionState());
});
connect(runtime_monitor_widget_->freeMonitorWidget(),
&FreeMonitorWidget::operationMessage,
this,
[this](const QString &message)
{
statusBar()->showMessage(message, 4000);
});
ui_->editorTabWidget->setTabVisible(2, false);
}

void MainWindow::configurePropertyEditor()
{
ui_->bindingAreaComboBox->setItemData(0, -1);
@@ -525,6 +572,11 @@ void MainWindow::refreshProjectUi()
{
logic_editor_widget_->setLogicId(current_logic_id);
}
if (runtime_monitor_widget_ != nullptr)
{
runtime_monitor_widget_->setProjectObjects(current_page_id, current_logic_id);
}
runtime_mode_service_.refreshPlcPollAddresses();
ui_->projectTree->clear();
auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")});
if (page != nullptr)
@@ -1067,6 +1119,19 @@ void MainWindow::updateModeUi(const QString &message)
const ApplicationMode mode = runtime_mode_service_.mode();
const ModePolicy policy = runtime_mode_service_.policy();
restoreCurrentModeAction();
const bool running = mode != ApplicationMode::Editing;
ui_->editorTabWidget->setTabVisible(2, running);
if (running)
{
runtime_monitor_widget_->setProjectObjects(
hmi_editor_service_.firstPageId(), logic_editor_service_.firstLogicId());
runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState());
ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab);
}
else if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab)
{
ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_);
}
// 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作
ui_->projectDock->setEnabled(policy.allowsProjectEditing);
ui_->propertiesDock->setEnabled(policy.allowsProjectEditing);
@@ -1154,6 +1219,16 @@ void MainWindow::updateSimulationUi(bool report_fault)
const SimulationState state = runtime_mode_service_.simulationState();
hmi_editor_widget_->setRuntimeWriteEnabled(
online || (offline && state == SimulationState::Running));
if (runtime_monitor_widget_ != nullptr)
{
runtime_monitor_widget_->setHmiWriteEnabled(
(online
&& runtime_mode_service_.plcConnectionState()
== PlcConnectionState::Connected)
|| (offline && state == SimulationState::Running));
runtime_monitor_widget_->refreshValues(
runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState());
}

switch (state)
{
@@ -1173,6 +1248,9 @@ void MainWindow::updateSimulationUi(bool report_fault)
logic_editor_widget_->setRuntimeTrace(
runtime_mode_service_.offlineSimulationService().traceSnapshot(),
error.nodeId);
runtime_monitor_widget_->setLogicTrace(
runtime_mode_service_.offlineSimulationService().traceSnapshot(),
error.nodeId);
if (!error.nodeId.empty())
{
logic_editor_widget_->selectNode(error.nodeId);


+ 7
- 0
app/src/ui/main_window.h Просмотреть файл

@@ -34,6 +34,8 @@ class HmiRuntimeService;
class HmiEditorWidget;
class LogicEditorService;
class LogicEditorWidget;
class RegisterMonitorService;
class RuntimeMonitorWidget;

/**
* @brief 组织主界面区域并将用户操作转交给服务层
@@ -59,6 +61,7 @@ public:
HmiEditorService &hmi_editor_service,
LogicEditorService &logic_editor_service,
HmiRuntimeService &hmi_runtime_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent = nullptr);

~MainWindow() override;
@@ -72,6 +75,7 @@ private:
void configureHmiEditor();
// 创建并连接控制逻辑编辑画布
void configureLogicEditor();
void configureRuntimeMonitor();
// 创建并连接控件属性编辑表单
void configurePropertyEditor();
void configurePlcConnection();
@@ -147,9 +151,11 @@ private:
HmiEditorService &hmi_editor_service_;
LogicEditorService &logic_editor_service_;
HmiRuntimeService &hmi_runtime_service_;
RegisterMonitorService &register_monitor_service_;
QActionGroup *mode_action_group_ = nullptr;
HmiEditorWidget *hmi_editor_widget_ = nullptr;
LogicEditorWidget *logic_editor_widget_ = nullptr;
RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr;
QTimer *runtime_refresh_timer_ = nullptr;
QLabel *mode_status_label_ = nullptr;
QLabel *register_status_label_ = nullptr;
@@ -158,4 +164,5 @@ private:
std::string selected_control_id_;
std::string selected_logic_node_id_;
bool plc_status_update_pending_ = false;
int last_editing_tab_index_ = 0;
};

+ 11
- 0
app/src/ui/main_window.ui Просмотреть файл

@@ -207,6 +207,17 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="runtimeMonitorTab">
<attribute name="title">
<string>运行监控</string>
</attribute>
<layout class="QVBoxLayout" name="runtimeMonitorLayout">
<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>
</widget>
</item>
</layout>


+ 89
- 0
app/src/ui/runtime_monitor_widget.cpp Просмотреть файл

@@ -0,0 +1,89 @@
#include "runtime_monitor_widget.h"

#include "free_monitor_widget.h"
#include "hmi_editor_widget.h"
#include "logic_editor_widget.h"
#include "ui_runtime_monitor_widget.h"

#include <QSplitter>

RuntimeMonitorWidget::RuntimeMonitorWidget(
HmiEditorService &hmi_editor_service,
HmiRuntimeService &hmi_runtime_service,
LogicEditorService &logic_editor_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent)
: QWidget(parent),
ui_(std::make_unique<Ui::RuntimeMonitorWidget>())
{
ui_->setupUi(this);
hmi_view_ = new HmiEditorWidget(
hmi_editor_service, hmi_runtime_service, ui_->hmiViewContainer);
hmi_view_->setObjectName(QStringLiteral("runtimeHmiView"));
hmi_view_->setEditingEnabled(false);
hmi_view_->setRuntimeActive(true);
ui_->hmiViewLayout->addWidget(hmi_view_);

logic_view_ = new LogicEditorWidget(logic_editor_service, ui_->logicViewContainer);
logic_view_->setObjectName(QStringLiteral("runtimeLogicView"));
logic_view_->setEditingEnabled(false);
ui_->logicViewLayout->addWidget(logic_view_);

free_monitor_widget_ = new FreeMonitorWidget(
register_monitor_service, ui_->monitorContainer);
free_monitor_widget_->setObjectName(QStringLiteral("freeMonitorWidget"));
ui_->monitorContainerLayout->addWidget(free_monitor_widget_);
ui_->upperSplitter->setSizes({600, 600});
ui_->verticalSplitter->setSizes({440, 260});
}

RuntimeMonitorWidget::~RuntimeMonitorWidget() = default;

void RuntimeMonitorWidget::setProjectObjects(
const std::string &page_id, const std::string &logic_id)
{
hmi_view_->setPageId(page_id);
logic_view_->setLogicId(logic_id);
}

void RuntimeMonitorWidget::setMode(
ApplicationMode mode, PlcConnectionState plc_state)
{
const bool offline = mode == ApplicationMode::OfflineRunning;
const bool online = mode == ApplicationMode::OnlineRunning;
ui_->logicFrame->setVisible(offline);
hmi_view_->setRuntimeActive(offline || online);
hmi_view_->setRuntimeWriteEnabled(
offline || (online && plc_state == PlcConnectionState::Connected));
if (!offline)
{
logic_view_->clearRuntimeTrace();
}
ui_->modeLabel->setText(
offline ? tr("离线仿真 · 虚拟 M/D")
: online ? tr("真机运行 · PLC 缓存") : tr("当前未运行"));
free_monitor_widget_->refreshValues(mode, plc_state);
}

void RuntimeMonitorWidget::setHmiWriteEnabled(bool enabled)
{
hmi_view_->setRuntimeWriteEnabled(enabled);
}

void RuntimeMonitorWidget::refreshValues(
ApplicationMode mode, PlcConnectionState plc_state)
{
hmi_view_->refreshRuntimeValues();
free_monitor_widget_->refreshValues(mode, plc_state);
}

void RuntimeMonitorWidget::setLogicTrace(
const LogicTraceSnapshot &trace, const std::string &fault_node_id)
{
logic_view_->setRuntimeTrace(trace, fault_node_id);
}

FreeMonitorWidget *RuntimeMonitorWidget::freeMonitorWidget() const
{
return free_monitor_widget_;
}

+ 49
- 0
app/src/ui/runtime_monitor_widget.h Просмотреть файл

@@ -0,0 +1,49 @@
#pragma once

#include "domain/runtime_state.h"
#include "services/plc_communication_gateway.h"

#include <QWidget>

#include <memory>
#include <string>

namespace Ui {
class RuntimeMonitorWidget;
}

class FreeMonitorWidget;
class HmiEditorService;
class HmiEditorWidget;
class HmiRuntimeService;
class LogicEditorService;
class LogicEditorWidget;
class RegisterMonitorService;
struct LogicTraceSnapshot;

class RuntimeMonitorWidget final : public QWidget
{
Q_OBJECT

public:
RuntimeMonitorWidget(
HmiEditorService &hmi_editor_service,
HmiRuntimeService &hmi_runtime_service,
LogicEditorService &logic_editor_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent = nullptr);
~RuntimeMonitorWidget() override;

void setProjectObjects(const std::string &page_id, const std::string &logic_id);
void setMode(ApplicationMode mode, PlcConnectionState plc_state);
void refreshValues(ApplicationMode mode, PlcConnectionState plc_state);
void setHmiWriteEnabled(bool enabled);
void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {});
FreeMonitorWidget *freeMonitorWidget() const;

private:
std::unique_ptr<Ui::RuntimeMonitorWidget> ui_;
HmiEditorWidget *hmi_view_ = nullptr;
LogicEditorWidget *logic_view_ = nullptr;
FreeMonitorWidget *free_monitor_widget_ = nullptr;
};

+ 54
- 0
app/src/ui/runtime_monitor_widget.ui Просмотреть файл

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RuntimeMonitorWidget</class>
<widget class="QWidget" name="RuntimeMonitorWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing"><number>6</number></property>
<property name="leftMargin"><number>8</number></property>
<property name="topMargin"><number>8</number></property>
<property name="rightMargin"><number>8</number></property>
<property name="bottomMargin"><number>8</number></property>
<item>
<layout class="QHBoxLayout" name="headerLayout">
<item><widget class="QLabel" name="titleLabel"><property name="text"><string>运行监控</string></property><property name="styleSheet"><string notr="true">font-weight: 600;</string></property></widget></item>
<item><spacer name="headerSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
<item><widget class="QLabel" name="modeLabel"><property name="text"><string>当前未运行</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item>
</layout>
</item>
<item>
<widget class="QSplitter" name="verticalSplitter">
<property name="orientation"><enum>Qt::Vertical</enum></property>
<widget class="QSplitter" name="upperSplitter">
<property name="orientation"><enum>Qt::Horizontal</enum></property>
<widget class="QFrame" name="hmiFrame">
<property name="frameShape"><enum>QFrame::StyledPanel</enum></property>
<layout class="QVBoxLayout" name="hmiLayout">
<property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property>
<item><widget class="QLabel" name="hmiLabel"><property name="text"><string>HMI 运行视图</string></property></widget></item>
<item><widget class="QWidget" name="hmiViewContainer" native="true"><layout class="QVBoxLayout" name="hmiViewLayout"><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>
<widget class="QFrame" name="logicFrame">
<property name="frameShape"><enum>QFrame::StyledPanel</enum></property>
<layout class="QVBoxLayout" name="logicLayout">
<property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property>
<item><widget class="QLabel" name="logicLabel"><property name="text"><string>梯形图运行状态</string></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>
</widget>
<widget class="QFrame" name="monitorFrame">
<property name="frameShape"><enum>QFrame::StyledPanel</enum></property>
<layout class="QVBoxLayout" name="monitorLayout">
<property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property>
<item><widget class="QLabel" name="monitorLabel"><property name="text"><string>自由监控</string></property></widget></item>
<item><widget class="QWidget" name="monitorContainer" native="true"><layout class="QVBoxLayout" name="monitorContainerLayout"><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>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

+ 152
- 3
app/tests/main_window_tests.cpp Просмотреть файл

@@ -7,7 +7,9 @@
#include "services/offline_simulation_service.h"
#include "services/project_service.h"
#include "services/runtime_mode_service.h"
#include "services/register_monitor_service.h"
#include "ui/hmi_editor_widget.h"
#include "ui/free_monitor_widget.h"
#include "ui/logic_editor_widget.h"
#include "ui/main_window.h"
#include "ui/plc_connection_dialog.h"
@@ -23,6 +25,8 @@
#include <QLineEdit>
#include <QListWidget>
#include <QSpinBox>
#include <QPushButton>
#include <QTableWidget>
#include <QTimer>

#include <iostream>
@@ -100,6 +104,64 @@ private:
std::function<void(const std::string &)> error_reported;
};

class OnlineReadyGateway final : public PlcCommunicationGateway
{
public:
PlcCommunicationResult connectDevice(const PlcSerialConfiguration &) override
{
connection_state = PlcConnectionState::Connected;
if (state_changed)
{
state_changed();
}
return {true, {}};
}

void disconnectDevice() override
{
connection_state = PlcConnectionState::Disconnected;
}

void setPollAddresses(const std::vector<RegisterAddress> &addresses) override
{
poll_addresses = addresses;
}

PlcConnectionState state() const override { return connection_state; }
bool initialReadCompleted() const override { return initial_read; }
const std::string &lastError() const override { return last_error; }

void setCallbacks(
std::function<void()> state_callback,
std::function<void(bool)> initial_callback,
std::function<void()> cache_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);
error_reported = std::move(error_callback);
}

void completeInitialRead()
{
initial_read = true;
if (initial_read_changed)
{
initial_read_changed(true);
}
}

PlcConnectionState connection_state = PlcConnectionState::Disconnected;
bool initial_read = false;
std::string last_error;
std::vector<RegisterAddress> poll_addresses;
std::function<void()> state_changed;
std::function<void(bool)> initial_read_changed;
std::function<void()> cache_updated;
std::function<void(const std::string &)> error_reported;
};

void require(bool condition, const std::string &message)
{
if (!condition)
@@ -128,12 +190,14 @@ void testModeActionsControlEditingAvailability()
HmiRuntimeService runtime_service(repository);
OfflineSimulationService simulation_service(repository);
RuntimeModeService mode_service(project_service, simulation_service);
RegisterMonitorService monitor_service(repository);
MainWindow window(
mode_service,
project_service,
editor_service,
logic_editor_service,
runtime_service);
runtime_service,
monitor_service);
window.resize(1000, 640);
window.show();
QApplication::processEvents();
@@ -162,6 +226,12 @@ void testModeActionsControlEditingAvailability()
HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>(
window, "hmiEditorWidget");
QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel");
QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab");
QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView");
QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView");
QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget");
require(!runtime_tab->isVisible(),
"runtime monitor workspace must be hidden while editing");

const qreal compact_scale = hmi_editor->transform().m11();
window.resize(1600, 900);
@@ -253,6 +323,21 @@ void testModeActionsControlEditingAvailability()
"HMI add controls must be disabled while running");
require(!add_normally_open_action->isEnabled(),
"logic add nodes must be disabled while running");
require(runtime_tab->isVisible() && runtime_hmi->isVisible()
&& runtime_logic->isVisible() && free_monitor->isVisible(),
"offline running must show HMI, ladder trace and free monitor together");
QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit");
QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton");
QTableWidget *monitor_table = requiredChild<QTableWidget>(window, "monitorTable");
monitor_address->setText(QStringLiteral("M0"));
monitor_add->click();
repository.writeBit({RegisterArea::M, 0}, true);
auto *monitor_widget = requiredChild<FreeMonitorWidget>(window, "freeMonitorWidget");
monitor_widget->refreshValues(
ApplicationMode::OfflineRunning, PlcConnectionState::Disconnected);
require(monitor_table->rowCount() == 1
&& monitor_table->item(0, 2)->text() == QStringLiteral("ON"),
"offline free monitor must read the same virtual M/D repository as HMI");

online_action->trigger();
require(mode_service.mode() == ApplicationMode::OfflineRunning,
@@ -275,6 +360,8 @@ void testModeActionsControlEditingAvailability()
"HMI add controls must be restored after returning to editing");
require(add_normally_open_action->isEnabled(),
"logic add nodes must be restored after returning to editing");
require(!runtime_tab->isVisible(),
"returning to editing must hide the runtime monitor workspace");

online_action->trigger();
require(mode_service.mode() == ApplicationMode::Editing,
@@ -318,12 +405,14 @@ void testPlcConfigurationUsesDialog()
HmiRuntimeService runtime_service(repository);
OfflineSimulationService simulation_service(repository);
RuntimeModeService mode_service(project_service, simulation_service);
RegisterMonitorService monitor_service(repository);
MainWindow window(
mode_service,
project_service,
editor_service,
logic_editor_service,
runtime_service);
runtime_service,
monitor_service);
require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr,
"serial configuration controls must not be embedded in the main window");
QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction");
@@ -357,6 +446,7 @@ void testRepeatedPlcStatusNotificationsAreCoalesced()
HmiRuntimeService runtime_service(active_repository);
OfflineSimulationService simulation_service(virtual_repository);
RuntimeModeService mode_service(project_service, simulation_service);
RegisterMonitorService monitor_service(active_repository);
RepeatedStatusGateway gateway;
mode_service.configurePlc(
gateway, active_repository, virtual_repository, plc_repository);
@@ -365,7 +455,8 @@ void testRepeatedPlcStatusNotificationsAreCoalesced()
project_service,
editor_service,
logic_editor_service,
runtime_service);
runtime_service,
monitor_service);
QListWidget *output = requiredChild<QListWidget>(window, "outputList");
const int initial_count = output->count();

@@ -387,6 +478,63 @@ void testRepeatedPlcStatusNotificationsAreCoalesced()
"repeated PLC status callbacks must append one coalesced output message");
}

void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService editor_service(project_service);
LogicEditorService logic_editor_service(project_service);
VirtualRegisterRepository virtual_repository;
VirtualRegisterRepository plc_repository;
ActiveRegisterRepository active_repository(virtual_repository);
HmiRuntimeService runtime_service(active_repository);
OfflineSimulationService simulation_service(virtual_repository);
RuntimeModeService mode_service(project_service, simulation_service);
RegisterMonitorService monitor_service(active_repository);
OnlineReadyGateway gateway;
mode_service.configurePlc(
gateway, active_repository, virtual_repository, plc_repository);
MainWindow window(
mode_service,
project_service,
editor_service,
logic_editor_service,
runtime_service,
monitor_service);
window.resize(1200, 760);
window.show();
QApplication::processEvents();

require(mode_service.connectPlc(
{"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded,
"online workspace test must connect the fake PLC gateway");
gateway.completeInitialRead();
QAction *online_action = requiredChild<QAction>(window, "onlineModeAction");
online_action->trigger();
QApplication::processEvents();

QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab");
QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView");
QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView");
QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget");
require(mode_service.mode() == ApplicationMode::OnlineRunning,
"completed PLC initial read must allow online running");
require(runtime_tab->isVisible() && runtime_hmi->isVisible()
&& free_monitor->isVisible(),
"online running must show HMI and free monitor together");
require(!runtime_logic->isVisible(),
"online running must not show a misleading local ladder runtime trace");
QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit");
QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton");
monitor_address->setText(QStringLiteral("D9"));
monitor_add->click();
require(std::find(
gateway.poll_addresses.begin(),
gateway.poll_addresses.end(),
RegisterAddress{RegisterArea::D, 9}) != gateway.poll_addresses.end(),
"online free monitor addresses must join the active PLC poll set immediately");
}

} // namespace

int main(int argc, char *argv[])
@@ -400,6 +548,7 @@ int main(int argc, char *argv[])
testModeActionsControlEditingAvailability();
testPlcConfigurationUsesDialog();
testRepeatedPlcStatusNotificationsAreCoalesced();
testOnlineWorkspaceShowsHmiAndFreeMonitorOnly();
}
catch (const std::exception &error)
{


+ 13
- 3
app/tests/main_window_tests.pro Просмотреть файл

@@ -12,11 +12,14 @@ SOURCES += \
main_window_tests.cpp \
../src/ui/main_window.cpp \
../src/ui/plc_connection_dialog.cpp \
../src/ui/free_monitor_widget.cpp \
../src/ui/runtime_monitor_widget.cpp \
../src/ui/hmi_editor_widget.cpp \
../src/ui/logic_editor_widget.cpp \
../src/domain/register_address.cpp \
../src/domain/register_repository.cpp \
../src/domain/active_register_repository.cpp \
../src/domain/register_monitor_model.cpp \
../src/domain/hmi_model.cpp \
../src/domain/control_logic_model.cpp \
../src/domain/project_model.cpp \
@@ -27,16 +30,20 @@ SOURCES += \
../src/services/hmi_runtime_service.cpp \
../src/services/software_logic_executor.cpp \
../src/services/offline_simulation_service.cpp \
../src/services/runtime_mode_service.cpp
../src/services/runtime_mode_service.cpp \
../src/services/register_monitor_service.cpp

HEADERS += \
../src/ui/main_window.h \
../src/ui/plc_connection_dialog.h \
../src/ui/free_monitor_widget.h \
../src/ui/runtime_monitor_widget.h \
../src/ui/hmi_editor_widget.h \
../src/ui/logic_editor_widget.h \
../src/domain/register_address.h \
../src/domain/register_repository.h \
../src/domain/active_register_repository.h \
../src/domain/register_monitor_model.h \
../src/domain/hmi_model.h \
../src/domain/control_logic_model.h \
../src/domain/project_model.h \
@@ -48,11 +55,14 @@ HEADERS += \
../src/services/hmi_runtime_service.h \
../src/services/software_logic_executor.h \
../src/services/offline_simulation_service.h \
../src/services/runtime_mode_service.h
../src/services/runtime_mode_service.h \
../src/services/register_monitor_service.h

HEADERS += \
../src/services/plc_communication_gateway.h

FORMS += \
../src/ui/main_window.ui \
../src/ui/plc_connection_dialog.ui
../src/ui/plc_connection_dialog.ui \
../src/ui/free_monitor_widget.ui \
../src/ui/runtime_monitor_widget.ui

Загрузка…
Отмена
Сохранить