| @@ -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 | |||
| @@ -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(); | |||
| @@ -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 "logic_editor_widget.h" | |||
| #include "plc_connection_dialog.h" | |||
| #include "runtime_monitor_widget.h" | |||
| #include "free_monitor_widget.h" | |||
| #include "services/hmi_editor_service.h" | |||
| #include "services/hmi_runtime_service.h" | |||
| #include "services/logic_editor_service.h" | |||
| #include "services/project_service.h" | |||
| #include "services/runtime_mode_service.h" | |||
| #include "services/register_monitor_service.h" | |||
| #include "ui_main_window.h" | |||
| #include <QActionGroup> | |||
| @@ -159,6 +162,7 @@ MainWindow::MainWindow( | |||
| HmiEditorService &hmi_editor_service, | |||
| LogicEditorService &logic_editor_service, | |||
| HmiRuntimeService &hmi_runtime_service, | |||
| RegisterMonitorService ®ister_monitor_service, | |||
| QWidget *parent) | |||
| : QMainWindow(parent), | |||
| ui_(std::make_unique<Ui::MainWindow>()), | |||
| @@ -166,7 +170,8 @@ MainWindow::MainWindow( | |||
| project_service_(project_service), | |||
| hmi_editor_service_(hmi_editor_service), | |||
| logic_editor_service_(logic_editor_service), | |||
| hmi_runtime_service_(hmi_runtime_service) | |||
| hmi_runtime_service_(hmi_runtime_service), | |||
| register_monitor_service_(register_monitor_service) | |||
| { | |||
| ui_->setupUi(this); | |||
| configureAppearance(); | |||
| @@ -175,6 +180,7 @@ MainWindow::MainWindow( | |||
| configurePlcConnection(); | |||
| configureHmiEditor(); | |||
| configureLogicEditor(); | |||
| configureRuntimeMonitor(); | |||
| runtime_mode_service_.setPlcStatusChangedCallback( | |||
| [this] { schedulePlcStatusUpdate(); }); | |||
| hmi_editor_service_.ensureDefaultPage(); | |||
| @@ -326,10 +332,14 @@ void MainWindow::configureActions() | |||
| { | |||
| showControlProperties(selected_control_id_); | |||
| } | |||
| else | |||
| else if (index == 1) | |||
| { | |||
| showLogicNodeProperties(selected_logic_node_id_); | |||
| } | |||
| if (index == 0 || index == 1) | |||
| { | |||
| last_editing_tab_index_ = index; | |||
| } | |||
| }); | |||
| ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0); | |||
| ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1); | |||
| @@ -375,6 +385,7 @@ void MainWindow::configureAppearance() | |||
| ui_->deleteLogicAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); | |||
| ui_->editorTabWidget->setTabIcon(0, style()->standardIcon(QStyle::SP_DesktopIcon)); | |||
| ui_->editorTabWidget->setTabIcon(1, style()->standardIcon(QStyle::SP_FileDialogListView)); | |||
| ui_->editorTabWidget->setTabIcon(2, style()->standardIcon(QStyle::SP_ComputerIcon)); | |||
| ui_->outputDock->setMaximumHeight(220); | |||
| mode_status_label_ = new QLabel(this); | |||
| @@ -444,6 +455,9 @@ void MainWindow::configureHmiEditor() | |||
| if (runtime_mode_service_.mode() != ApplicationMode::Editing) | |||
| { | |||
| hmi_editor_widget_->refreshRuntimeValues(); | |||
| runtime_monitor_widget_->refreshValues( | |||
| runtime_mode_service_.mode(), | |||
| runtime_mode_service_.plcConnectionState()); | |||
| updateSimulationUi(false); | |||
| } | |||
| }); | |||
| @@ -460,6 +474,8 @@ void MainWindow::configureHmiEditor() | |||
| { | |||
| logic_editor_widget_->setRuntimeTrace( | |||
| runtime_mode_service_.offlineSimulationService().traceSnapshot()); | |||
| runtime_monitor_widget_->setLogicTrace( | |||
| runtime_mode_service_.offlineSimulationService().traceSnapshot()); | |||
| }, | |||
| Qt::QueuedConnection); | |||
| } | |||
| @@ -489,6 +505,37 @@ void MainWindow::configureLogicEditor() | |||
| }); | |||
| } | |||
| void MainWindow::configureRuntimeMonitor() | |||
| { | |||
| runtime_monitor_widget_ = new RuntimeMonitorWidget( | |||
| hmi_editor_service_, | |||
| hmi_runtime_service_, | |||
| logic_editor_service_, | |||
| register_monitor_service_, | |||
| ui_->runtimeMonitorTab); | |||
| runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | |||
| ui_->runtimeMonitorLayout->addWidget(runtime_monitor_widget_); | |||
| connect(runtime_monitor_widget_->freeMonitorWidget(), | |||
| &FreeMonitorWidget::monitorAddressesChanged, | |||
| this, | |||
| [this] | |||
| { | |||
| runtime_mode_service_.setMonitorAddresses( | |||
| register_monitor_service_.addresses()); | |||
| runtime_monitor_widget_->refreshValues( | |||
| runtime_mode_service_.mode(), | |||
| runtime_mode_service_.plcConnectionState()); | |||
| }); | |||
| connect(runtime_monitor_widget_->freeMonitorWidget(), | |||
| &FreeMonitorWidget::operationMessage, | |||
| this, | |||
| [this](const QString &message) | |||
| { | |||
| statusBar()->showMessage(message, 4000); | |||
| }); | |||
| ui_->editorTabWidget->setTabVisible(2, false); | |||
| } | |||
| void MainWindow::configurePropertyEditor() | |||
| { | |||
| ui_->bindingAreaComboBox->setItemData(0, -1); | |||
| @@ -525,6 +572,11 @@ void MainWindow::refreshProjectUi() | |||
| { | |||
| logic_editor_widget_->setLogicId(current_logic_id); | |||
| } | |||
| if (runtime_monitor_widget_ != nullptr) | |||
| { | |||
| runtime_monitor_widget_->setProjectObjects(current_page_id, current_logic_id); | |||
| } | |||
| runtime_mode_service_.refreshPlcPollAddresses(); | |||
| ui_->projectTree->clear(); | |||
| auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); | |||
| if (page != nullptr) | |||
| @@ -1067,6 +1119,19 @@ void MainWindow::updateModeUi(const QString &message) | |||
| const ApplicationMode mode = runtime_mode_service_.mode(); | |||
| const ModePolicy policy = runtime_mode_service_.policy(); | |||
| restoreCurrentModeAction(); | |||
| const bool running = mode != ApplicationMode::Editing; | |||
| ui_->editorTabWidget->setTabVisible(2, running); | |||
| if (running) | |||
| { | |||
| runtime_monitor_widget_->setProjectObjects( | |||
| hmi_editor_service_.firstPageId(), logic_editor_service_.firstLogicId()); | |||
| runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState()); | |||
| ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); | |||
| } | |||
| else if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) | |||
| { | |||
| ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_); | |||
| } | |||
| // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作 | |||
| ui_->projectDock->setEnabled(policy.allowsProjectEditing); | |||
| ui_->propertiesDock->setEnabled(policy.allowsProjectEditing); | |||
| @@ -1154,6 +1219,16 @@ void MainWindow::updateSimulationUi(bool report_fault) | |||
| const SimulationState state = runtime_mode_service_.simulationState(); | |||
| hmi_editor_widget_->setRuntimeWriteEnabled( | |||
| online || (offline && state == SimulationState::Running)); | |||
| if (runtime_monitor_widget_ != nullptr) | |||
| { | |||
| runtime_monitor_widget_->setHmiWriteEnabled( | |||
| (online | |||
| && runtime_mode_service_.plcConnectionState() | |||
| == PlcConnectionState::Connected) | |||
| || (offline && state == SimulationState::Running)); | |||
| runtime_monitor_widget_->refreshValues( | |||
| runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); | |||
| } | |||
| switch (state) | |||
| { | |||
| @@ -1173,6 +1248,9 @@ void MainWindow::updateSimulationUi(bool report_fault) | |||
| logic_editor_widget_->setRuntimeTrace( | |||
| runtime_mode_service_.offlineSimulationService().traceSnapshot(), | |||
| error.nodeId); | |||
| runtime_monitor_widget_->setLogicTrace( | |||
| runtime_mode_service_.offlineSimulationService().traceSnapshot(), | |||
| error.nodeId); | |||
| if (!error.nodeId.empty()) | |||
| { | |||
| logic_editor_widget_->selectNode(error.nodeId); | |||
| @@ -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; | |||
| }; | |||
| @@ -207,6 +207,17 @@ | |||
| </item> | |||
| </layout> | |||
| </widget> | |||
| <widget class="QWidget" name="runtimeMonitorTab"> | |||
| <attribute name="title"> | |||
| <string>运行监控</string> | |||
| </attribute> | |||
| <layout class="QVBoxLayout" name="runtimeMonitorLayout"> | |||
| <property name="leftMargin"><number>0</number></property> | |||
| <property name="topMargin"><number>0</number></property> | |||
| <property name="rightMargin"><number>0</number></property> | |||
| <property name="bottomMargin"><number>0</number></property> | |||
| </layout> | |||
| </widget> | |||
| </widget> | |||
| </item> | |||
| </layout> | |||
| @@ -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/project_service.h" | |||
| #include "services/runtime_mode_service.h" | |||
| #include "services/register_monitor_service.h" | |||
| #include "ui/hmi_editor_widget.h" | |||
| #include "ui/free_monitor_widget.h" | |||
| #include "ui/logic_editor_widget.h" | |||
| #include "ui/main_window.h" | |||
| #include "ui/plc_connection_dialog.h" | |||
| @@ -23,6 +25,8 @@ | |||
| #include <QLineEdit> | |||
| #include <QListWidget> | |||
| #include <QSpinBox> | |||
| #include <QPushButton> | |||
| #include <QTableWidget> | |||
| #include <QTimer> | |||
| #include <iostream> | |||
| @@ -100,6 +104,64 @@ private: | |||
| std::function<void(const std::string &)> error_reported; | |||
| }; | |||
| class OnlineReadyGateway final : public PlcCommunicationGateway | |||
| { | |||
| public: | |||
| PlcCommunicationResult connectDevice(const PlcSerialConfiguration &) override | |||
| { | |||
| connection_state = PlcConnectionState::Connected; | |||
| if (state_changed) | |||
| { | |||
| state_changed(); | |||
| } | |||
| return {true, {}}; | |||
| } | |||
| void disconnectDevice() override | |||
| { | |||
| connection_state = PlcConnectionState::Disconnected; | |||
| } | |||
| void setPollAddresses(const std::vector<RegisterAddress> &addresses) override | |||
| { | |||
| poll_addresses = addresses; | |||
| } | |||
| PlcConnectionState state() const override { return connection_state; } | |||
| bool initialReadCompleted() const override { return initial_read; } | |||
| const std::string &lastError() const override { return last_error; } | |||
| void setCallbacks( | |||
| std::function<void()> state_callback, | |||
| std::function<void(bool)> initial_callback, | |||
| std::function<void()> cache_callback, | |||
| std::function<void(const std::string &)> error_callback) override | |||
| { | |||
| state_changed = std::move(state_callback); | |||
| initial_read_changed = std::move(initial_callback); | |||
| cache_updated = std::move(cache_callback); | |||
| error_reported = std::move(error_callback); | |||
| } | |||
| void completeInitialRead() | |||
| { | |||
| initial_read = true; | |||
| if (initial_read_changed) | |||
| { | |||
| initial_read_changed(true); | |||
| } | |||
| } | |||
| PlcConnectionState connection_state = PlcConnectionState::Disconnected; | |||
| bool initial_read = false; | |||
| std::string last_error; | |||
| std::vector<RegisterAddress> poll_addresses; | |||
| std::function<void()> state_changed; | |||
| std::function<void(bool)> initial_read_changed; | |||
| std::function<void()> cache_updated; | |||
| std::function<void(const std::string &)> error_reported; | |||
| }; | |||
| void require(bool condition, const std::string &message) | |||
| { | |||
| if (!condition) | |||
| @@ -128,12 +190,14 @@ void testModeActionsControlEditingAvailability() | |||
| HmiRuntimeService runtime_service(repository); | |||
| OfflineSimulationService simulation_service(repository); | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| RegisterMonitorService monitor_service(repository); | |||
| MainWindow window( | |||
| mode_service, | |||
| project_service, | |||
| editor_service, | |||
| logic_editor_service, | |||
| runtime_service); | |||
| runtime_service, | |||
| monitor_service); | |||
| window.resize(1000, 640); | |||
| window.show(); | |||
| QApplication::processEvents(); | |||
| @@ -162,6 +226,12 @@ void testModeActionsControlEditingAvailability() | |||
| HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>( | |||
| window, "hmiEditorWidget"); | |||
| QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel"); | |||
| QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab"); | |||
| QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView"); | |||
| QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView"); | |||
| QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget"); | |||
| require(!runtime_tab->isVisible(), | |||
| "runtime monitor workspace must be hidden while editing"); | |||
| const qreal compact_scale = hmi_editor->transform().m11(); | |||
| window.resize(1600, 900); | |||
| @@ -253,6 +323,21 @@ void testModeActionsControlEditingAvailability() | |||
| "HMI add controls must be disabled while running"); | |||
| require(!add_normally_open_action->isEnabled(), | |||
| "logic add nodes must be disabled while running"); | |||
| require(runtime_tab->isVisible() && runtime_hmi->isVisible() | |||
| && runtime_logic->isVisible() && free_monitor->isVisible(), | |||
| "offline running must show HMI, ladder trace and free monitor together"); | |||
| QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit"); | |||
| QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton"); | |||
| QTableWidget *monitor_table = requiredChild<QTableWidget>(window, "monitorTable"); | |||
| monitor_address->setText(QStringLiteral("M0")); | |||
| monitor_add->click(); | |||
| repository.writeBit({RegisterArea::M, 0}, true); | |||
| auto *monitor_widget = requiredChild<FreeMonitorWidget>(window, "freeMonitorWidget"); | |||
| monitor_widget->refreshValues( | |||
| ApplicationMode::OfflineRunning, PlcConnectionState::Disconnected); | |||
| require(monitor_table->rowCount() == 1 | |||
| && monitor_table->item(0, 2)->text() == QStringLiteral("ON"), | |||
| "offline free monitor must read the same virtual M/D repository as HMI"); | |||
| online_action->trigger(); | |||
| require(mode_service.mode() == ApplicationMode::OfflineRunning, | |||
| @@ -275,6 +360,8 @@ void testModeActionsControlEditingAvailability() | |||
| "HMI add controls must be restored after returning to editing"); | |||
| require(add_normally_open_action->isEnabled(), | |||
| "logic add nodes must be restored after returning to editing"); | |||
| require(!runtime_tab->isVisible(), | |||
| "returning to editing must hide the runtime monitor workspace"); | |||
| online_action->trigger(); | |||
| require(mode_service.mode() == ApplicationMode::Editing, | |||
| @@ -318,12 +405,14 @@ void testPlcConfigurationUsesDialog() | |||
| HmiRuntimeService runtime_service(repository); | |||
| OfflineSimulationService simulation_service(repository); | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| RegisterMonitorService monitor_service(repository); | |||
| MainWindow window( | |||
| mode_service, | |||
| project_service, | |||
| editor_service, | |||
| logic_editor_service, | |||
| runtime_service); | |||
| runtime_service, | |||
| monitor_service); | |||
| require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr, | |||
| "serial configuration controls must not be embedded in the main window"); | |||
| QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction"); | |||
| @@ -357,6 +446,7 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||
| HmiRuntimeService runtime_service(active_repository); | |||
| OfflineSimulationService simulation_service(virtual_repository); | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| RegisterMonitorService monitor_service(active_repository); | |||
| RepeatedStatusGateway gateway; | |||
| mode_service.configurePlc( | |||
| gateway, active_repository, virtual_repository, plc_repository); | |||
| @@ -365,7 +455,8 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||
| project_service, | |||
| editor_service, | |||
| logic_editor_service, | |||
| runtime_service); | |||
| runtime_service, | |||
| monitor_service); | |||
| QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | |||
| const int initial_count = output->count(); | |||
| @@ -387,6 +478,63 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||
| "repeated PLC status callbacks must append one coalesced output message"); | |||
| } | |||
| void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService editor_service(project_service); | |||
| LogicEditorService logic_editor_service(project_service); | |||
| VirtualRegisterRepository virtual_repository; | |||
| VirtualRegisterRepository plc_repository; | |||
| ActiveRegisterRepository active_repository(virtual_repository); | |||
| HmiRuntimeService runtime_service(active_repository); | |||
| OfflineSimulationService simulation_service(virtual_repository); | |||
| RuntimeModeService mode_service(project_service, simulation_service); | |||
| RegisterMonitorService monitor_service(active_repository); | |||
| OnlineReadyGateway gateway; | |||
| mode_service.configurePlc( | |||
| gateway, active_repository, virtual_repository, plc_repository); | |||
| MainWindow window( | |||
| mode_service, | |||
| project_service, | |||
| editor_service, | |||
| logic_editor_service, | |||
| runtime_service, | |||
| monitor_service); | |||
| window.resize(1200, 760); | |||
| window.show(); | |||
| QApplication::processEvents(); | |||
| require(mode_service.connectPlc( | |||
| {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded, | |||
| "online workspace test must connect the fake PLC gateway"); | |||
| gateway.completeInitialRead(); | |||
| QAction *online_action = requiredChild<QAction>(window, "onlineModeAction"); | |||
| online_action->trigger(); | |||
| QApplication::processEvents(); | |||
| QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab"); | |||
| QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView"); | |||
| QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView"); | |||
| QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget"); | |||
| require(mode_service.mode() == ApplicationMode::OnlineRunning, | |||
| "completed PLC initial read must allow online running"); | |||
| require(runtime_tab->isVisible() && runtime_hmi->isVisible() | |||
| && free_monitor->isVisible(), | |||
| "online running must show HMI and free monitor together"); | |||
| require(!runtime_logic->isVisible(), | |||
| "online running must not show a misleading local ladder runtime trace"); | |||
| QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit"); | |||
| QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton"); | |||
| monitor_address->setText(QStringLiteral("D9")); | |||
| monitor_add->click(); | |||
| require(std::find( | |||
| gateway.poll_addresses.begin(), | |||
| gateway.poll_addresses.end(), | |||
| RegisterAddress{RegisterArea::D, 9}) != gateway.poll_addresses.end(), | |||
| "online free monitor addresses must join the active PLC poll set immediately"); | |||
| } | |||
| } // namespace | |||
| int main(int argc, char *argv[]) | |||
| @@ -400,6 +548,7 @@ int main(int argc, char *argv[]) | |||
| testModeActionsControlEditingAvailability(); | |||
| testPlcConfigurationUsesDialog(); | |||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | |||
| testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); | |||
| } | |||
| catch (const std::exception &error) | |||
| { | |||
| @@ -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 | |||