diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index a4328e8..e345033 100644 --- a/app/integrated_platform.pro +++ b/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 diff --git a/app/src/main.cpp b/app/src/main.cpp index 5fa5d07..02721b5 100644 --- a/app/src/main.cpp +++ b/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(); diff --git a/app/src/ui/free_monitor_widget.cpp b/app/src/ui/free_monitor_widget.cpp new file mode 100644 index 0000000..42d8a04 --- /dev/null +++ b/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 +#include +#include +#include + +#include + +namespace { + +QString fromUtf8(const std::string &value) +{ + return QString::fromUtf8(value.data(), static_cast(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()), + 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 values = values_available + ? service_.values(communication_fault) : std::vector{}; + 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(row) >= values.size()) + { + value_item->setText(QStringLiteral("--")); + state_item->setText(tr("未运行")); + continue; + } + const MonitorValue &value = values.at(static_cast(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 &addresses = service_.addresses(); + ui_->monitorTable->setRowCount(static_cast(addresses.size())); + for (std::size_t index = 0; index < addresses.size(); ++index) + { + const RegisterAddress &address = addresses[index]; + const int row = static_cast(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(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 rows; + for (const QModelIndex &index : ui_->monitorTable->selectionModel()->selectedRows()) + { + rows.insert(index.row()); + } + std::vector addresses; + const std::vector &all_addresses = service_.addresses(); + for (int row : rows) + { + if (row >= 0 && static_cast(row) < all_addresses.size()) + { + addresses.push_back(all_addresses.at(static_cast(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)); + } +} diff --git a/app/src/ui/free_monitor_widget.h b/app/src/ui/free_monitor_widget.h new file mode 100644 index 0000000..759f340 --- /dev/null +++ b/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 + +#include + +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_; + RegisterMonitorService &service_; +}; diff --git a/app/src/ui/free_monitor_widget.ui b/app/src/ui/free_monitor_widget.ui new file mode 100644 index 0000000..3957995 --- /dev/null +++ b/app/src/ui/free_monitor_widget.ui @@ -0,0 +1,46 @@ + + + FreeMonitorWidget + + + 420190 + + + 6 + 0 + 0 + 0 + 0 + + + 起始地址 + 90016016777215M0 或 D0true + 连续个数 + 1641 + 添加 + Qt::Horizontal4020 + 删除选中的监控地址删除 + 清空监控表清空 + 数据源:未启用Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + true + QAbstractItemView::ExtendedSelection + QAbstractItemView::SelectRows + QAbstractItemView::NoEditTriggers + true + 4 + 0 + 地址 + 类型 + 当前值 + 状态 + + + + + + + diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 234c61a..253bc12 100644 --- a/app/src/ui/main_window.cpp +++ b/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 @@ -159,6 +162,7 @@ MainWindow::MainWindow( HmiEditorService &hmi_editor_service, LogicEditorService &logic_editor_service, HmiRuntimeService &hmi_runtime_service, + RegisterMonitorService ®ister_monitor_service, QWidget *parent) : QMainWindow(parent), ui_(std::make_unique()), @@ -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); diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index de994af..4a39000 100644 --- a/app/src/ui/main_window.h +++ b/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 ®ister_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 ®ister_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; }; diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index 07cc731..5e6482e 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -207,6 +207,17 @@ + + + 运行监控 + + + 0 + 0 + 0 + 0 + + diff --git a/app/src/ui/runtime_monitor_widget.cpp b/app/src/ui/runtime_monitor_widget.cpp new file mode 100644 index 0000000..af1629e --- /dev/null +++ b/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 + +RuntimeMonitorWidget::RuntimeMonitorWidget( + HmiEditorService &hmi_editor_service, + HmiRuntimeService &hmi_runtime_service, + LogicEditorService &logic_editor_service, + RegisterMonitorService ®ister_monitor_service, + QWidget *parent) + : QWidget(parent), + ui_(std::make_unique()) +{ + 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_; +} diff --git a/app/src/ui/runtime_monitor_widget.h b/app/src/ui/runtime_monitor_widget.h new file mode 100644 index 0000000..5147833 --- /dev/null +++ b/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 + +#include +#include + +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 ®ister_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_; + HmiEditorWidget *hmi_view_ = nullptr; + LogicEditorWidget *logic_view_ = nullptr; + FreeMonitorWidget *free_monitor_widget_ = nullptr; +}; diff --git a/app/src/ui/runtime_monitor_widget.ui b/app/src/ui/runtime_monitor_widget.ui new file mode 100644 index 0000000..9f7f4b2 --- /dev/null +++ b/app/src/ui/runtime_monitor_widget.ui @@ -0,0 +1,54 @@ + + + RuntimeMonitorWidget + + + 6 + 8 + 8 + 8 + 8 + + + 运行监控font-weight: 600; + Qt::Horizontal4020 + 当前未运行Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Qt::Vertical + + Qt::Horizontal + + QFrame::StyledPanel + + 4444 + HMI 运行视图 + 0000 + + + + QFrame::StyledPanel + + 4444 + 梯形图运行状态 + 0000 + + + + + QFrame::StyledPanel + + 4444 + 自由监控 + 0000 + + + + + + + + + diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 4401430..17abd76 100644 --- a/app/tests/main_window_tests.cpp +++ b/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 #include #include +#include +#include #include #include @@ -100,6 +104,64 @@ private: std::function 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 &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 state_callback, + std::function initial_callback, + std::function cache_callback, + std::function error_callback) override + { + state_changed = std::move(state_callback); + initial_read_changed = std::move(initial_callback); + cache_updated = std::move(cache_callback); + 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 poll_addresses; + std::function state_changed; + std::function initial_read_changed; + std::function cache_updated; + std::function 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( window, "hmiEditorWidget"); QLabel *executor_status = requiredChild(window, "executorStatusLabel"); + QWidget *runtime_tab = requiredChild(window, "runtimeMonitorTab"); + QWidget *runtime_hmi = requiredChild(window, "runtimeHmiView"); + QWidget *runtime_logic = requiredChild(window, "runtimeLogicView"); + QWidget *free_monitor = requiredChild(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(window, "addressEdit"); + QPushButton *monitor_add = requiredChild(window, "addButton"); + QTableWidget *monitor_table = requiredChild(window, "monitorTable"); + monitor_address->setText(QStringLiteral("M0")); + monitor_add->click(); + repository.writeBit({RegisterArea::M, 0}, true); + auto *monitor_widget = requiredChild(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(QStringLiteral("serialPortComboBox")) == nullptr, "serial configuration controls must not be embedded in the main window"); QAction *configure_action = requiredChild(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(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(window, "onlineModeAction"); + online_action->trigger(); + QApplication::processEvents(); + + QWidget *runtime_tab = requiredChild(window, "runtimeMonitorTab"); + QWidget *runtime_hmi = requiredChild(window, "runtimeHmiView"); + QWidget *runtime_logic = requiredChild(window, "runtimeLogicView"); + QWidget *free_monitor = requiredChild(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(window, "addressEdit"); + QPushButton *monitor_add = requiredChild(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) { diff --git a/app/tests/main_window_tests.pro b/app/tests/main_window_tests.pro index 669ff36..5225e18 100644 --- a/app/tests/main_window_tests.pro +++ b/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