| @@ -18,6 +18,8 @@ SOURCES += \ | |||||
| src/main.cpp \ | src/main.cpp \ | ||||
| src/ui/main_window.cpp \ | src/ui/main_window.cpp \ | ||||
| src/ui/plc_connection_dialog.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_address.cpp \ | ||||
| src/domain/register_repository.cpp \ | src/domain/register_repository.cpp \ | ||||
| src/domain/active_register_repository.cpp \ | src/domain/active_register_repository.cpp \ | ||||
| @@ -43,6 +45,8 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| src/ui/main_window.h \ | src/ui/main_window.h \ | ||||
| src/ui/plc_connection_dialog.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_address.h \ | ||||
| src/domain/register_repository.h \ | src/domain/register_repository.h \ | ||||
| src/domain/active_register_repository.h \ | src/domain/active_register_repository.h \ | ||||
| @@ -59,8 +63,8 @@ HEADERS += \ | |||||
| src/services/software_logic_executor.h \ | src/services/software_logic_executor.h \ | ||||
| src/services/offline_simulation_service.h \ | src/services/offline_simulation_service.h \ | ||||
| src/services/runtime_mode_service.h \ | src/services/runtime_mode_service.h \ | ||||
| src/services/plc_communication_gateway.h \ | |||||
| src/services/register_monitor_service.h \ | src/services/register_monitor_service.h \ | ||||
| src/services/plc_communication_gateway.h \ | |||||
| src/infrastructure/plc_register_repository.h \ | src/infrastructure/plc_register_repository.h \ | ||||
| src/infrastructure/plc_communication_service.h \ | src/infrastructure/plc_communication_service.h \ | ||||
| src/infrastructure/json_project_storage.h \ | src/infrastructure/json_project_storage.h \ | ||||
| @@ -69,4 +73,6 @@ HEADERS += \ | |||||
| FORMS += \ | FORMS += \ | ||||
| src/ui/main_window.ui \ | 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 | |||||
| @@ -18,6 +18,7 @@ | |||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "services/register_monitor_service.h" | |||||
| #include "ui/main_window.h" | #include "ui/main_window.h" | ||||
| int main(int argc, char *argv[]) | int main(int argc, char *argv[]) | ||||
| @@ -37,6 +38,7 @@ int main(int argc, char *argv[]) | |||||
| ActiveRegisterRepository active_register_repository(virtual_register_repository); | ActiveRegisterRepository active_register_repository(virtual_register_repository); | ||||
| PlcCommunicationService plc_communication_service(plc_register_repository); | PlcCommunicationService plc_communication_service(plc_register_repository); | ||||
| HmiRuntimeService hmi_runtime_service(active_register_repository); | HmiRuntimeService hmi_runtime_service(active_register_repository); | ||||
| RegisterMonitorService register_monitor_service(active_register_repository); | |||||
| OfflineSimulationService offline_simulation_service(virtual_register_repository); | OfflineSimulationService offline_simulation_service(virtual_register_repository); | ||||
| RuntimeModeService runtime_mode_service( | RuntimeModeService runtime_mode_service( | ||||
| project_service, offline_simulation_service); | project_service, offline_simulation_service); | ||||
| @@ -50,7 +52,8 @@ int main(int argc, char *argv[]) | |||||
| project_service, | project_service, | ||||
| hmi_editor_service, | hmi_editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| hmi_runtime_service); | |||||
| hmi_runtime_service, | |||||
| register_monitor_service); | |||||
| main_window.show(); | main_window.show(); | ||||
| return application.exec(); | return application.exec(); | ||||
| @@ -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)); | |||||
| } | |||||
| } | |||||
| @@ -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_; | |||||
| }; | |||||
| @@ -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> | |||||
| @@ -3,11 +3,14 @@ | |||||
| #include "hmi_editor_widget.h" | #include "hmi_editor_widget.h" | ||||
| #include "logic_editor_widget.h" | #include "logic_editor_widget.h" | ||||
| #include "plc_connection_dialog.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_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "services/register_monitor_service.h" | |||||
| #include "ui_main_window.h" | #include "ui_main_window.h" | ||||
| #include <QActionGroup> | #include <QActionGroup> | ||||
| @@ -159,6 +162,7 @@ MainWindow::MainWindow( | |||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| RegisterMonitorService ®ister_monitor_service, | |||||
| QWidget *parent) | QWidget *parent) | ||||
| : QMainWindow(parent), | : QMainWindow(parent), | ||||
| ui_(std::make_unique<Ui::MainWindow>()), | ui_(std::make_unique<Ui::MainWindow>()), | ||||
| @@ -166,7 +170,8 @@ MainWindow::MainWindow( | |||||
| project_service_(project_service), | project_service_(project_service), | ||||
| hmi_editor_service_(hmi_editor_service), | hmi_editor_service_(hmi_editor_service), | ||||
| logic_editor_service_(logic_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); | ui_->setupUi(this); | ||||
| configureAppearance(); | configureAppearance(); | ||||
| @@ -175,6 +180,7 @@ MainWindow::MainWindow( | |||||
| configurePlcConnection(); | configurePlcConnection(); | ||||
| configureHmiEditor(); | configureHmiEditor(); | ||||
| configureLogicEditor(); | configureLogicEditor(); | ||||
| configureRuntimeMonitor(); | |||||
| runtime_mode_service_.setPlcStatusChangedCallback( | runtime_mode_service_.setPlcStatusChangedCallback( | ||||
| [this] { schedulePlcStatusUpdate(); }); | [this] { schedulePlcStatusUpdate(); }); | ||||
| hmi_editor_service_.ensureDefaultPage(); | hmi_editor_service_.ensureDefaultPage(); | ||||
| @@ -326,10 +332,14 @@ void MainWindow::configureActions() | |||||
| { | { | ||||
| showControlProperties(selected_control_id_); | showControlProperties(selected_control_id_); | ||||
| } | } | ||||
| else | |||||
| else if (index == 1) | |||||
| { | { | ||||
| showLogicNodeProperties(selected_logic_node_id_); | showLogicNodeProperties(selected_logic_node_id_); | ||||
| } | } | ||||
| if (index == 0 || index == 1) | |||||
| { | |||||
| last_editing_tab_index_ = index; | |||||
| } | |||||
| }); | }); | ||||
| ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0); | ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0); | ||||
| ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1); | ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1); | ||||
| @@ -375,6 +385,7 @@ void MainWindow::configureAppearance() | |||||
| ui_->deleteLogicAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); | ui_->deleteLogicAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); | ||||
| ui_->editorTabWidget->setTabIcon(0, style()->standardIcon(QStyle::SP_DesktopIcon)); | ui_->editorTabWidget->setTabIcon(0, style()->standardIcon(QStyle::SP_DesktopIcon)); | ||||
| ui_->editorTabWidget->setTabIcon(1, style()->standardIcon(QStyle::SP_FileDialogListView)); | ui_->editorTabWidget->setTabIcon(1, style()->standardIcon(QStyle::SP_FileDialogListView)); | ||||
| ui_->editorTabWidget->setTabIcon(2, style()->standardIcon(QStyle::SP_ComputerIcon)); | |||||
| ui_->outputDock->setMaximumHeight(220); | ui_->outputDock->setMaximumHeight(220); | ||||
| mode_status_label_ = new QLabel(this); | mode_status_label_ = new QLabel(this); | ||||
| @@ -444,6 +455,9 @@ void MainWindow::configureHmiEditor() | |||||
| if (runtime_mode_service_.mode() != ApplicationMode::Editing) | if (runtime_mode_service_.mode() != ApplicationMode::Editing) | ||||
| { | { | ||||
| hmi_editor_widget_->refreshRuntimeValues(); | hmi_editor_widget_->refreshRuntimeValues(); | ||||
| runtime_monitor_widget_->refreshValues( | |||||
| runtime_mode_service_.mode(), | |||||
| runtime_mode_service_.plcConnectionState()); | |||||
| updateSimulationUi(false); | updateSimulationUi(false); | ||||
| } | } | ||||
| }); | }); | ||||
| @@ -460,6 +474,8 @@ void MainWindow::configureHmiEditor() | |||||
| { | { | ||||
| logic_editor_widget_->setRuntimeTrace( | logic_editor_widget_->setRuntimeTrace( | ||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot()); | runtime_mode_service_.offlineSimulationService().traceSnapshot()); | ||||
| runtime_monitor_widget_->setLogicTrace( | |||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot()); | |||||
| }, | }, | ||||
| Qt::QueuedConnection); | 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() | void MainWindow::configurePropertyEditor() | ||||
| { | { | ||||
| ui_->bindingAreaComboBox->setItemData(0, -1); | ui_->bindingAreaComboBox->setItemData(0, -1); | ||||
| @@ -525,6 +572,11 @@ void MainWindow::refreshProjectUi() | |||||
| { | { | ||||
| logic_editor_widget_->setLogicId(current_logic_id); | 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(); | ui_->projectTree->clear(); | ||||
| auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); | auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); | ||||
| if (page != nullptr) | if (page != nullptr) | ||||
| @@ -1067,6 +1119,19 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| const ApplicationMode mode = runtime_mode_service_.mode(); | const ApplicationMode mode = runtime_mode_service_.mode(); | ||||
| const ModePolicy policy = runtime_mode_service_.policy(); | const ModePolicy policy = runtime_mode_service_.policy(); | ||||
| restoreCurrentModeAction(); | 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_->projectDock->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->propertiesDock->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(); | const SimulationState state = runtime_mode_service_.simulationState(); | ||||
| hmi_editor_widget_->setRuntimeWriteEnabled( | hmi_editor_widget_->setRuntimeWriteEnabled( | ||||
| online || (offline && state == SimulationState::Running)); | 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) | switch (state) | ||||
| { | { | ||||
| @@ -1173,6 +1248,9 @@ void MainWindow::updateSimulationUi(bool report_fault) | |||||
| logic_editor_widget_->setRuntimeTrace( | logic_editor_widget_->setRuntimeTrace( | ||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot(), | runtime_mode_service_.offlineSimulationService().traceSnapshot(), | ||||
| error.nodeId); | error.nodeId); | ||||
| runtime_monitor_widget_->setLogicTrace( | |||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot(), | |||||
| error.nodeId); | |||||
| if (!error.nodeId.empty()) | if (!error.nodeId.empty()) | ||||
| { | { | ||||
| logic_editor_widget_->selectNode(error.nodeId); | logic_editor_widget_->selectNode(error.nodeId); | ||||
| @@ -34,6 +34,8 @@ class HmiRuntimeService; | |||||
| class HmiEditorWidget; | class HmiEditorWidget; | ||||
| class LogicEditorService; | class LogicEditorService; | ||||
| class LogicEditorWidget; | class LogicEditorWidget; | ||||
| class RegisterMonitorService; | |||||
| class RuntimeMonitorWidget; | |||||
| /** | /** | ||||
| * @brief 组织主界面区域并将用户操作转交给服务层 | * @brief 组织主界面区域并将用户操作转交给服务层 | ||||
| @@ -59,6 +61,7 @@ public: | |||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| RegisterMonitorService ®ister_monitor_service, | |||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| ~MainWindow() override; | ~MainWindow() override; | ||||
| @@ -72,6 +75,7 @@ private: | |||||
| void configureHmiEditor(); | void configureHmiEditor(); | ||||
| // 创建并连接控制逻辑编辑画布 | // 创建并连接控制逻辑编辑画布 | ||||
| void configureLogicEditor(); | void configureLogicEditor(); | ||||
| void configureRuntimeMonitor(); | |||||
| // 创建并连接控件属性编辑表单 | // 创建并连接控件属性编辑表单 | ||||
| void configurePropertyEditor(); | void configurePropertyEditor(); | ||||
| void configurePlcConnection(); | void configurePlcConnection(); | ||||
| @@ -147,9 +151,11 @@ private: | |||||
| HmiEditorService &hmi_editor_service_; | HmiEditorService &hmi_editor_service_; | ||||
| LogicEditorService &logic_editor_service_; | LogicEditorService &logic_editor_service_; | ||||
| HmiRuntimeService &hmi_runtime_service_; | HmiRuntimeService &hmi_runtime_service_; | ||||
| RegisterMonitorService ®ister_monitor_service_; | |||||
| QActionGroup *mode_action_group_ = nullptr; | QActionGroup *mode_action_group_ = nullptr; | ||||
| HmiEditorWidget *hmi_editor_widget_ = nullptr; | HmiEditorWidget *hmi_editor_widget_ = nullptr; | ||||
| LogicEditorWidget *logic_editor_widget_ = nullptr; | LogicEditorWidget *logic_editor_widget_ = nullptr; | ||||
| RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | |||||
| QTimer *runtime_refresh_timer_ = nullptr; | QTimer *runtime_refresh_timer_ = nullptr; | ||||
| QLabel *mode_status_label_ = nullptr; | QLabel *mode_status_label_ = nullptr; | ||||
| QLabel *register_status_label_ = nullptr; | QLabel *register_status_label_ = nullptr; | ||||
| @@ -158,4 +164,5 @@ private: | |||||
| std::string selected_control_id_; | std::string selected_control_id_; | ||||
| std::string selected_logic_node_id_; | std::string selected_logic_node_id_; | ||||
| bool plc_status_update_pending_ = false; | bool plc_status_update_pending_ = false; | ||||
| int last_editing_tab_index_ = 0; | |||||
| }; | }; | ||||
| @@ -207,6 +207,17 @@ | |||||
| </item> | </item> | ||||
| </layout> | </layout> | ||||
| </widget> | </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> | </widget> | ||||
| </item> | </item> | ||||
| </layout> | </layout> | ||||
| @@ -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 ®ister_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_; | |||||
| } | |||||
| @@ -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 ®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::RuntimeMonitorWidget> ui_; | |||||
| HmiEditorWidget *hmi_view_ = nullptr; | |||||
| LogicEditorWidget *logic_view_ = nullptr; | |||||
| FreeMonitorWidget *free_monitor_widget_ = nullptr; | |||||
| }; | |||||
| @@ -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> | |||||
| @@ -7,7 +7,9 @@ | |||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "services/register_monitor_service.h" | |||||
| #include "ui/hmi_editor_widget.h" | #include "ui/hmi_editor_widget.h" | ||||
| #include "ui/free_monitor_widget.h" | |||||
| #include "ui/logic_editor_widget.h" | #include "ui/logic_editor_widget.h" | ||||
| #include "ui/main_window.h" | #include "ui/main_window.h" | ||||
| #include "ui/plc_connection_dialog.h" | #include "ui/plc_connection_dialog.h" | ||||
| @@ -23,6 +25,8 @@ | |||||
| #include <QLineEdit> | #include <QLineEdit> | ||||
| #include <QListWidget> | #include <QListWidget> | ||||
| #include <QSpinBox> | #include <QSpinBox> | ||||
| #include <QPushButton> | |||||
| #include <QTableWidget> | |||||
| #include <QTimer> | #include <QTimer> | ||||
| #include <iostream> | #include <iostream> | ||||
| @@ -100,6 +104,64 @@ private: | |||||
| std::function<void(const std::string &)> error_reported; | 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) | void require(bool condition, const std::string &message) | ||||
| { | { | ||||
| if (!condition) | if (!condition) | ||||
| @@ -128,12 +190,14 @@ void testModeActionsControlEditingAvailability() | |||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| OfflineSimulationService simulation_service(repository); | OfflineSimulationService simulation_service(repository); | ||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| RegisterMonitorService monitor_service(repository); | |||||
| MainWindow window( | MainWindow window( | ||||
| mode_service, | mode_service, | ||||
| project_service, | project_service, | ||||
| editor_service, | editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| runtime_service); | |||||
| runtime_service, | |||||
| monitor_service); | |||||
| window.resize(1000, 640); | window.resize(1000, 640); | ||||
| window.show(); | window.show(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| @@ -162,6 +226,12 @@ void testModeActionsControlEditingAvailability() | |||||
| HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>( | HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>( | ||||
| window, "hmiEditorWidget"); | window, "hmiEditorWidget"); | ||||
| QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel"); | 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(); | const qreal compact_scale = hmi_editor->transform().m11(); | ||||
| window.resize(1600, 900); | window.resize(1600, 900); | ||||
| @@ -253,6 +323,21 @@ void testModeActionsControlEditingAvailability() | |||||
| "HMI add controls must be disabled while running"); | "HMI add controls must be disabled while running"); | ||||
| require(!add_normally_open_action->isEnabled(), | require(!add_normally_open_action->isEnabled(), | ||||
| "logic add nodes must be disabled while running"); | "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(); | online_action->trigger(); | ||||
| require(mode_service.mode() == ApplicationMode::OfflineRunning, | require(mode_service.mode() == ApplicationMode::OfflineRunning, | ||||
| @@ -275,6 +360,8 @@ void testModeActionsControlEditingAvailability() | |||||
| "HMI add controls must be restored after returning to editing"); | "HMI add controls must be restored after returning to editing"); | ||||
| require(add_normally_open_action->isEnabled(), | require(add_normally_open_action->isEnabled(), | ||||
| "logic add nodes must be restored after returning to editing"); | "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(); | online_action->trigger(); | ||||
| require(mode_service.mode() == ApplicationMode::Editing, | require(mode_service.mode() == ApplicationMode::Editing, | ||||
| @@ -318,12 +405,14 @@ void testPlcConfigurationUsesDialog() | |||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| OfflineSimulationService simulation_service(repository); | OfflineSimulationService simulation_service(repository); | ||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| RegisterMonitorService monitor_service(repository); | |||||
| MainWindow window( | MainWindow window( | ||||
| mode_service, | mode_service, | ||||
| project_service, | project_service, | ||||
| editor_service, | editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| runtime_service); | |||||
| runtime_service, | |||||
| monitor_service); | |||||
| require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr, | require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr, | ||||
| "serial configuration controls must not be embedded in the main window"); | "serial configuration controls must not be embedded in the main window"); | ||||
| QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction"); | QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction"); | ||||
| @@ -357,6 +446,7 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||||
| HmiRuntimeService runtime_service(active_repository); | HmiRuntimeService runtime_service(active_repository); | ||||
| OfflineSimulationService simulation_service(virtual_repository); | OfflineSimulationService simulation_service(virtual_repository); | ||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| RegisterMonitorService monitor_service(active_repository); | |||||
| RepeatedStatusGateway gateway; | RepeatedStatusGateway gateway; | ||||
| mode_service.configurePlc( | mode_service.configurePlc( | ||||
| gateway, active_repository, virtual_repository, plc_repository); | gateway, active_repository, virtual_repository, plc_repository); | ||||
| @@ -365,7 +455,8 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||||
| project_service, | project_service, | ||||
| editor_service, | editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| runtime_service); | |||||
| runtime_service, | |||||
| monitor_service); | |||||
| QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | ||||
| const int initial_count = output->count(); | const int initial_count = output->count(); | ||||
| @@ -387,6 +478,63 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||||
| "repeated PLC status callbacks must append one coalesced output message"); | "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 | } // namespace | ||||
| int main(int argc, char *argv[]) | int main(int argc, char *argv[]) | ||||
| @@ -400,6 +548,7 @@ int main(int argc, char *argv[]) | |||||
| testModeActionsControlEditingAvailability(); | testModeActionsControlEditingAvailability(); | ||||
| testPlcConfigurationUsesDialog(); | testPlcConfigurationUsesDialog(); | ||||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | testRepeatedPlcStatusNotificationsAreCoalesced(); | ||||
| testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||
| @@ -12,11 +12,14 @@ SOURCES += \ | |||||
| main_window_tests.cpp \ | main_window_tests.cpp \ | ||||
| ../src/ui/main_window.cpp \ | ../src/ui/main_window.cpp \ | ||||
| ../src/ui/plc_connection_dialog.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/hmi_editor_widget.cpp \ | ||||
| ../src/ui/logic_editor_widget.cpp \ | ../src/ui/logic_editor_widget.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/active_register_repository.cpp \ | ../src/domain/active_register_repository.cpp \ | ||||
| ../src/domain/register_monitor_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -27,16 +30,20 @@ SOURCES += \ | |||||
| ../src/services/hmi_runtime_service.cpp \ | ../src/services/hmi_runtime_service.cpp \ | ||||
| ../src/services/software_logic_executor.cpp \ | ../src/services/software_logic_executor.cpp \ | ||||
| ../src/services/offline_simulation_service.cpp \ | ../src/services/offline_simulation_service.cpp \ | ||||
| ../src/services/runtime_mode_service.cpp | |||||
| ../src/services/runtime_mode_service.cpp \ | |||||
| ../src/services/register_monitor_service.cpp | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/ui/main_window.h \ | ../src/ui/main_window.h \ | ||||
| ../src/ui/plc_connection_dialog.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/hmi_editor_widget.h \ | ||||
| ../src/ui/logic_editor_widget.h \ | ../src/ui/logic_editor_widget.h \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/active_register_repository.h \ | ../src/domain/active_register_repository.h \ | ||||
| ../src/domain/register_monitor_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| @@ -48,11 +55,14 @@ HEADERS += \ | |||||
| ../src/services/hmi_runtime_service.h \ | ../src/services/hmi_runtime_service.h \ | ||||
| ../src/services/software_logic_executor.h \ | ../src/services/software_logic_executor.h \ | ||||
| ../src/services/offline_simulation_service.h \ | ../src/services/offline_simulation_service.h \ | ||||
| ../src/services/runtime_mode_service.h | |||||
| ../src/services/runtime_mode_service.h \ | |||||
| ../src/services/register_monitor_service.h | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/services/plc_communication_gateway.h | ../src/services/plc_communication_gateway.h | ||||
| FORMS += \ | FORMS += \ | ||||
| ../src/ui/main_window.ui \ | ../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 | |||||