diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index feb737e..430dd32 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -17,6 +17,7 @@ INCLUDEPATH += src SOURCES += \ src/main.cpp \ src/ui/main_window.cpp \ + src/ui/plc_connection_dialog.cpp \ src/domain/register_address.cpp \ src/domain/register_repository.cpp \ src/domain/active_register_repository.cpp \ @@ -41,9 +42,10 @@ SOURCES += \ HEADERS += \ src/ui/main_window.h \ + src/ui/plc_connection_dialog.h \ src/domain/register_address.h \ - src/domain/active_register_repository.h \ src/domain/register_repository.h \ + src/domain/active_register_repository.h \ src/domain/data_point_model.h \ src/domain/hmi_model.h \ src/domain/control_logic_model.h \ @@ -57,13 +59,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/plc_communication_gateway.h \ src/infrastructure/plc_register_repository.h \ src/infrastructure/plc_communication_service.h \ - src/services/runtime_mode_service.h \ src/infrastructure/json_project_storage.h \ src/ui/hmi_editor_widget.h \ src/ui/logic_editor_widget.h FORMS += \ - src/ui/main_window.ui + src/ui/main_window.ui \ + src/ui/plc_connection_dialog.ui diff --git a/app/src/main.cpp b/app/src/main.cpp index 1b2abbf..f28577c 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -9,7 +9,11 @@ #include #include "infrastructure/json_project_storage.h" +#include "infrastructure/plc_communication_service.h" +#include "infrastructure/plc_register_repository.h" +#include "domain/active_register_repository.h" #include "services/hmi_editor_service.h" +#include "services/data_point_service.h" #include "services/hmi_runtime_service.h" #include "services/logic_editor_service.h" #include "services/offline_simulation_service.h" @@ -27,17 +31,27 @@ int main(int argc, char *argv[]) JsonProjectStorage project_storage; ProjectService project_service(project_storage); HmiEditorService hmi_editor_service(project_service); + DataPointService data_point_service(project_service); LogicEditorService logic_editor_service(project_service); // 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现 VirtualRegisterRepository virtual_register_repository; - HmiRuntimeService hmi_runtime_service(virtual_register_repository); + PlcRegisterRepository plc_register_repository; + ActiveRegisterRepository active_register_repository(virtual_register_repository); + PlcCommunicationService plc_communication_service(plc_register_repository); + HmiRuntimeService hmi_runtime_service(active_register_repository); OfflineSimulationService offline_simulation_service(virtual_register_repository); RuntimeModeService runtime_mode_service( project_service, offline_simulation_service); + runtime_mode_service.configurePlc( + plc_communication_service, + active_register_repository, + virtual_register_repository, + plc_register_repository); MainWindow main_window( runtime_mode_service, project_service, hmi_editor_service, + data_point_service, logic_editor_service, hmi_runtime_service); main_window.show(); diff --git a/app/src/services/hmi_runtime_service.cpp b/app/src/services/hmi_runtime_service.cpp index a527c08..fd726f7 100644 --- a/app/src/services/hmi_runtime_service.cpp +++ b/app/src/services/hmi_runtime_service.cpp @@ -107,6 +107,16 @@ HmiRuntimeWriteResult HmiRuntimeService::writeNumericInput( : writeFailure(repositoryError(result.error)); } +BitReadResult HmiRuntimeService::readBit(const RegisterAddress &address) const +{ + return repository_.readBit(address); +} + +WordReadResult HmiRuntimeService::readWord(const RegisterAddress &address) const +{ + return repository_.readWord(address); +} + HmiRuntimeError HmiRuntimeService::repositoryError(RegisterError error) { // 领域仓库错误被收敛为 HMI 可理解的运行错误 diff --git a/app/src/services/hmi_runtime_service.h b/app/src/services/hmi_runtime_service.h index 0b12814..139a640 100644 --- a/app/src/services/hmi_runtime_service.h +++ b/app/src/services/hmi_runtime_service.h @@ -78,6 +78,8 @@ public: */ HmiRuntimeWriteResult writeNumericInput( const HmiControl &control, std::int16_t value); + BitReadResult readBit(const RegisterAddress &address) const; + WordReadResult readWord(const RegisterAddress &address) const; private: static HmiRuntimeError repositoryError(RegisterError error); diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 48c454a..6936d05 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -2,7 +2,9 @@ #include "hmi_editor_widget.h" #include "logic_editor_widget.h" +#include "plc_connection_dialog.h" #include "services/hmi_editor_service.h" +#include "services/data_point_service.h" #include "services/hmi_runtime_service.h" #include "services/logic_editor_service.h" #include "services/project_service.h" @@ -13,24 +15,22 @@ #include #include #include -#include +#include #include #include #include #include #include #include -#include #include #include #include -#include #include +#include #include #include #include -#include #include namespace { @@ -135,6 +135,7 @@ MainWindow::MainWindow( RuntimeModeService &runtime_mode_service, ProjectService &project_service, HmiEditorService &hmi_editor_service, + DataPointService &data_point_service, LogicEditorService &logic_editor_service, HmiRuntimeService &hmi_runtime_service, QWidget *parent) @@ -143,22 +144,36 @@ MainWindow::MainWindow( runtime_mode_service_(runtime_mode_service), project_service_(project_service), hmi_editor_service_(hmi_editor_service), + data_point_service_(data_point_service), logic_editor_service_(logic_editor_service), hmi_runtime_service_(hmi_runtime_service) { ui_->setupUi(this); - configureAppearance(); // 窗口样式、布局、状态栏标签初始化,纯视觉代码,无业务逻辑 - configureActions(); // 创建所有菜单 Action、工具栏按钮,绑定点击槽函数 - configurePropertyEditor(); // 动态构建属性表单:ID、文本、坐标宽高、寄存器区域 (M/D)、地址索引 + 应用按钮 - configureHmiEditor(); // 创建画布控件HmiEditorWidget嵌入主窗口,绑定画布信号:选中控件、控件拖动修改、编辑器报错;同时启动运行刷新定时器 + configureAppearance(); + configureActions(); + configurePropertyEditor(); + configureDataPointEditor(); + configurePlcConnection(); + configureHmiEditor(); configureLogicEditor(); + runtime_mode_service_.setPlcStatusChangedCallback( + [this] + { + QMetaObject::invokeMethod( + this, + [this] { updateModeUi(tr("PLC 通信状态已更新")); }, + Qt::QueuedConnection); + }); hmi_editor_service_.ensureDefaultPage(); logic_editor_service_.ensureDefaultLogic(); refreshProjectUi(); updateModeUi(tr("系统已进入编辑态")); } -MainWindow::~MainWindow() = default; +MainWindow::~MainWindow() +{ + runtime_mode_service_.setPlcStatusChangedCallback({}); +} void MainWindow::configureActions() { @@ -175,106 +190,31 @@ void MainWindow::configureActions() connect(ui_->onlineModeAction, &QAction::triggered, this, [this] { requestMode(ApplicationMode::OnlineRunning); }); connect(ui_->exitAction, &QAction::triggered, qApp, &QApplication::closeAllWindows); + connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject); + connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); + connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); + connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject); - new_project_action_ = new QAction(tr("新建工程"), this); - save_project_action_ = new QAction(tr("保存工程"), this); - save_as_project_action_ = new QAction(tr("工程另存为"), this); - load_project_action_ = new QAction(tr("加载工程"), this); - new_project_action_->setObjectName(QStringLiteral("newProjectAction")); - save_project_action_->setObjectName(QStringLiteral("saveProjectAction")); - save_as_project_action_->setObjectName(QStringLiteral("saveAsProjectAction")); - load_project_action_->setObjectName(QStringLiteral("loadProjectAction")); - new_project_action_->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); - save_project_action_->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton)); - load_project_action_->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton)); - ui_->fileMenu->insertAction(ui_->exitAction, new_project_action_); - ui_->fileMenu->insertAction(ui_->exitAction, save_project_action_); - ui_->fileMenu->insertAction(ui_->exitAction, save_as_project_action_); - ui_->fileMenu->insertAction(ui_->exitAction, load_project_action_); - ui_->fileMenu->insertSeparator(ui_->exitAction); - connect(new_project_action_, &QAction::triggered, this, &MainWindow::createNewProject); - connect(save_project_action_, &QAction::triggered, this, &MainWindow::saveProject); - connect(save_as_project_action_, &QAction::triggered, this, &MainWindow::saveProjectAs); - connect(load_project_action_, &QAction::triggered, this, &MainWindow::loadProject); - - hmi_tool_bar_ = addToolBar(tr("HMI 控件")); - hmi_tool_bar_->setObjectName(QStringLiteral("hmiToolBar")); - hmi_tool_bar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - add_button_action_ = hmi_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_DialogApplyButton), tr("按钮")); - add_indicator_action_ = hmi_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_DialogYesButton), tr("指示灯")); - add_numeric_display_action_ = hmi_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_FileDialogInfoView), tr("数值显示")); - add_numeric_input_action_ = hmi_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("数值输入")); - delete_control_action_ = hmi_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_TrashIcon), tr("删除")); - add_button_action_->setObjectName(QStringLiteral("addButtonAction")); - add_indicator_action_->setObjectName(QStringLiteral("addIndicatorAction")); - add_numeric_display_action_->setObjectName(QStringLiteral("addNumericDisplayAction")); - add_numeric_input_action_->setObjectName(QStringLiteral("addNumericInputAction")); - delete_control_action_->setObjectName(QStringLiteral("deleteControlAction")); - add_button_action_->setToolTip(tr("添加按钮控件")); - add_indicator_action_->setToolTip(tr("添加指示灯控件")); - add_numeric_display_action_->setToolTip(tr("添加数值显示控件")); - add_numeric_input_action_->setToolTip(tr("添加数值输入控件")); - delete_control_action_->setToolTip(tr("删除当前控件")); - connect(add_button_action_, &QAction::triggered, this, + connect(ui_->addButtonAction, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::Button); }); - connect(add_indicator_action_, &QAction::triggered, this, + connect(ui_->addIndicatorAction, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::Indicator); }); - connect(add_numeric_display_action_, &QAction::triggered, this, + connect(ui_->addNumericDisplayAction, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::NumericDisplay); }); - connect(add_numeric_input_action_, &QAction::triggered, this, + connect(ui_->addNumericInputAction, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::NumericInput); }); - connect(delete_control_action_, &QAction::triggered, + connect(ui_->deleteControlAction, &QAction::triggered, this, &MainWindow::deleteSelectedControl); - logic_tool_bar_ = addToolBar(tr("控制逻辑节点")); - logic_tool_bar_->setObjectName(QStringLiteral("logicToolBar")); - logic_tool_bar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - add_rung_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_FileIcon), tr("新建网络")); - parallel_insert_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_ArrowDown), tr("并联插入")); - parallel_insert_action_->setCheckable(true); - logic_tool_bar_->addSeparator(); - add_normally_open_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_ArrowRight), tr("常开")); - add_normally_closed_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_ArrowRight), tr("常闭")); - add_normal_coil_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_DialogApplyButton), tr("线圈")); - add_set_coil_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_DialogYesButton), tr("置位")); - add_reset_coil_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_DialogNoButton), tr("复位")); - add_compare_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_FileDialogInfoView), tr("D 比较")); - delete_logic_action_ = logic_tool_bar_->addAction( - style()->standardIcon(QStyle::SP_TrashIcon), tr("删除")); - add_rung_action_->setObjectName(QStringLiteral("addRungAction")); - parallel_insert_action_->setObjectName(QStringLiteral("parallelInsertAction")); - add_normally_open_action_->setObjectName(QStringLiteral("addNormallyOpenAction")); - add_normally_closed_action_->setObjectName(QStringLiteral("addNormallyClosedAction")); - add_normal_coil_action_->setObjectName(QStringLiteral("addNormalCoilAction")); - add_set_coil_action_->setObjectName(QStringLiteral("addSetCoilAction")); - add_reset_coil_action_->setObjectName(QStringLiteral("addResetCoilAction")); - add_compare_action_->setObjectName(QStringLiteral("addCompareAction")); - delete_logic_action_->setObjectName(QStringLiteral("deleteLogicAction")); - add_rung_action_->setToolTip(tr("在梯形图末尾新增网络")); - parallel_insert_action_->setToolTip(tr("将下一个条件并联到当前选中的条件")); - add_normally_open_action_->setToolTip(tr("向当前网络添加常开触点")); - add_normally_closed_action_->setToolTip(tr("向当前网络添加常闭触点")); - add_normal_coil_action_->setToolTip(tr("设置当前网络的普通线圈")); - add_set_coil_action_->setToolTip(tr("设置当前网络的置位线圈")); - add_reset_coil_action_->setToolTip(tr("设置当前网络的复位线圈")); - add_compare_action_->setToolTip(tr("向当前网络添加 D 值与常量比较")); - delete_logic_action_->setToolTip(tr("删除选中的逻辑节点或网络")); - connect(add_rung_action_, &QAction::triggered, + logic_insert_action_group_ = new QActionGroup(this); + logic_insert_action_group_->setExclusive(true); + logic_insert_action_group_->addAction(ui_->appendInsertAction); + logic_insert_action_group_->addAction(ui_->beforeInsertAction); + logic_insert_action_group_->addAction(ui_->afterInsertAction); + logic_insert_action_group_->addAction(ui_->parallelInsertAction); + connect(ui_->addRungAction, &QAction::triggered, this, &MainWindow::addLogicRung); - connect(add_normally_open_action_, &QAction::triggered, + connect(ui_->addNormallyOpenAction, &QAction::triggered, this, [this] { @@ -282,7 +222,7 @@ void MainWindow::configureActions() RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen}); }); - connect(add_normally_closed_action_, &QAction::triggered, + connect(ui_->addNormallyClosedAction, &QAction::triggered, this, [this] { @@ -290,7 +230,7 @@ void MainWindow::configureActions() RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyClosed}); }); - connect(add_normal_coil_action_, &QAction::triggered, + connect(ui_->addNormalCoilAction, &QAction::triggered, this, [this] { @@ -298,7 +238,7 @@ void MainWindow::configureActions() RegisterAddress{RegisterArea::M, 0}, CoilMode::Normal}); }); - connect(add_set_coil_action_, &QAction::triggered, + connect(ui_->addSetCoilAction, &QAction::triggered, this, [this] { @@ -306,7 +246,7 @@ void MainWindow::configureActions() RegisterAddress{RegisterArea::M, 0}, CoilMode::Set}); }); - connect(add_reset_coil_action_, &QAction::triggered, + connect(ui_->addResetCoilAction, &QAction::triggered, this, [this] { @@ -314,7 +254,7 @@ void MainWindow::configureActions() RegisterAddress{RegisterArea::M, 0}, CoilMode::Reset}); }); - connect(add_compare_action_, &QAction::triggered, + connect(ui_->addCompareAction, &QAction::triggered, this, [this] { @@ -323,15 +263,15 @@ void MainWindow::configureActions() ComparisonOperator::Equal, 0}); }); - connect(delete_logic_action_, &QAction::triggered, + connect(ui_->deleteLogicAction, &QAction::triggered, this, &MainWindow::deleteSelectedLogicObject); connect(ui_->editorTabWidget, &QTabWidget::currentChanged, this, [this](int index) { - hmi_tool_bar_->setVisible(index == 0); - logic_tool_bar_->setVisible(index == 1); + ui_->hmiToolBar->setVisible(index == 0); + ui_->logicToolBar->setVisible(index == 1); if (index == 0) { showControlProperties(selected_control_id_); @@ -341,14 +281,14 @@ void MainWindow::configureActions() showLogicNodeProperties(selected_logic_node_id_); } }); - hmi_tool_bar_->setVisible(ui_->editorTabWidget->currentIndex() == 0); - logic_tool_bar_->setVisible(ui_->editorTabWidget->currentIndex() == 1); + ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0); + ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1); ui_->viewMenu->addAction(ui_->projectDock->toggleViewAction()); ui_->viewMenu->addAction(ui_->propertiesDock->toggleViewAction()); ui_->viewMenu->addAction(ui_->outputDock->toggleViewAction()); - ui_->viewMenu->addAction(hmi_tool_bar_->toggleViewAction()); - ui_->viewMenu->addAction(logic_tool_bar_->toggleViewAction()); + ui_->viewMenu->addAction(ui_->hmiToolBar->toggleViewAction()); + ui_->viewMenu->addAction(ui_->logicToolBar->toggleViewAction()); ui_->viewMenu->addSeparator(); ui_->viewMenu->addAction(ui_->modeToolBar->toggleViewAction()); } @@ -362,6 +302,30 @@ void MainWindow::configureAppearance() ui_->editingModeAction->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView)); ui_->offlineModeAction->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); ui_->onlineModeAction->setIcon(style()->standardIcon(QStyle::SP_DriveNetIcon)); + ui_->newProjectAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); + ui_->saveProjectAction->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton)); + ui_->loadProjectAction->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton)); + ui_->configurePlcAction->setIcon(style()->standardIcon(QStyle::SP_ComputerIcon)); + ui_->disconnectPlcAction->setIcon(style()->standardIcon(QStyle::SP_DialogCloseButton)); + ui_->addButtonAction->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton)); + ui_->addIndicatorAction->setIcon(style()->standardIcon(QStyle::SP_DialogYesButton)); + ui_->addNumericDisplayAction->setIcon( + style()->standardIcon(QStyle::SP_FileDialogInfoView)); + ui_->addNumericInputAction->setIcon( + style()->standardIcon(QStyle::SP_FileDialogContentsView)); + ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); + ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); + ui_->appendInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); + ui_->beforeInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowLeft)); + ui_->afterInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); + ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); + ui_->addNormallyOpenAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); + ui_->addNormallyClosedAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); + ui_->addNormalCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton)); + ui_->addSetCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogYesButton)); + ui_->addResetCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogNoButton)); + ui_->addCompareAction->setIcon(style()->standardIcon(QStyle::SP_FileDialogInfoView)); + 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_->outputDock->setMaximumHeight(220); @@ -432,6 +396,7 @@ void MainWindow::configureHmiEditor() if (runtime_mode_service_.mode() != ApplicationMode::Editing) { hmi_editor_widget_->refreshRuntimeValues(); + refreshDataPointUi(); updateSimulationUi(false); } }); @@ -441,6 +406,15 @@ void MainWindow::configureHmiEditor() this, [this] { updateSimulationUi(true); }, Qt::QueuedConnection); + connect(&runtime_mode_service_.offlineSimulationService(), + &OfflineSimulationService::scanCompleted, + this, + [this] + { + logic_editor_widget_->setRuntimeTrace( + runtime_mode_service_.offlineSimulationService().traceSnapshot()); + }, + Qt::QueuedConnection); } void MainWindow::configureLogicEditor() @@ -470,108 +444,79 @@ void MainWindow::configureLogicEditor() void MainWindow::configurePropertyEditor() { - control_id_edit_ = new QLineEdit(ui_->propertiesPage); - control_id_edit_->setObjectName(QStringLiteral("controlIdEdit")); - control_text_edit_ = new QLineEdit(ui_->propertiesPage); - control_text_edit_->setObjectName(QStringLiteral("controlTextEdit")); - control_x_spin_box_ = new QSpinBox(ui_->propertiesPage); - control_y_spin_box_ = new QSpinBox(ui_->propertiesPage); - control_width_spin_box_ = new QSpinBox(ui_->propertiesPage); - control_height_spin_box_ = new QSpinBox(ui_->propertiesPage); - control_x_spin_box_->setObjectName(QStringLiteral("controlXSpinBox")); - control_y_spin_box_->setObjectName(QStringLiteral("controlYSpinBox")); - control_width_spin_box_->setObjectName(QStringLiteral("controlWidthSpinBox")); - control_height_spin_box_->setObjectName(QStringLiteral("controlHeightSpinBox")); - for (QSpinBox *box : {control_x_spin_box_, control_y_spin_box_}) - { - box->setRange(0, 4000); - } - for (QSpinBox *box : {control_width_spin_box_, control_height_spin_box_}) - { - box->setRange(1, 4000); - } - binding_area_combo_box_ = new QComboBox(ui_->propertiesPage); - binding_area_combo_box_->setObjectName(QStringLiteral("bindingAreaComboBox")); - binding_area_combo_box_->addItem(tr("未绑定"), QVariant::fromValue(-1)); - binding_area_combo_box_->addItem(QStringLiteral("M"), QVariant::fromValue(0)); - binding_area_combo_box_->addItem(QStringLiteral("D"), QVariant::fromValue(1)); - binding_index_spin_box_ = new QSpinBox(ui_->propertiesPage); - binding_index_spin_box_->setObjectName(QStringLiteral("bindingIndexSpinBox")); - binding_index_spin_box_->setRange(0, RegisterAddress::kMaximumIndex); - apply_properties_button_ = new QPushButton(tr("应用属性"), ui_->propertiesPage); - apply_properties_button_->setObjectName(QStringLiteral("applyPropertiesButton")); - property_stack_ = new QStackedWidget(ui_->propertiesPage); - property_stack_->setObjectName(QStringLiteral("propertyStack")); - hmi_properties_page_ = new QWidget(property_stack_); - auto *hmi_form = new QFormLayout(hmi_properties_page_); - hmi_form->setContentsMargins(0, 0, 0, 0); - hmi_form->addRow(tr("控件 ID"), control_id_edit_); - hmi_form->addRow(tr("显示文本"), control_text_edit_); - hmi_form->addRow(tr("X"), control_x_spin_box_); - hmi_form->addRow(tr("Y"), control_y_spin_box_); - hmi_form->addRow(tr("宽度"), control_width_spin_box_); - hmi_form->addRow(tr("高度"), control_height_spin_box_); - hmi_form->addRow(tr("绑定区域"), binding_area_combo_box_); - hmi_form->addRow(tr("绑定地址"), binding_index_spin_box_); - hmi_form->addRow(apply_properties_button_); - - logic_properties_page_ = new QWidget(property_stack_); - auto *logic_form = new QFormLayout(logic_properties_page_); - logic_form->setContentsMargins(0, 0, 0, 0); - logic_node_id_edit_ = new QLineEdit(logic_properties_page_); - logic_node_id_edit_->setObjectName(QStringLiteral("logicNodeIdEdit")); - logic_node_id_edit_->setReadOnly(true); - logic_node_type_label_ = new QLabel(logic_properties_page_); - logic_node_type_label_->setObjectName(QStringLiteral("logicNodeTypeLabel")); - logic_address_area_label_ = new QLabel(logic_properties_page_); - logic_address_area_label_->setObjectName(QStringLiteral("logicAddressAreaLabel")); - logic_address_spin_box_ = new QSpinBox(logic_properties_page_); - logic_address_spin_box_->setObjectName(QStringLiteral("logicAddressSpinBox")); - logic_address_spin_box_->setRange(0, RegisterAddress::kMaximumIndex); - logic_mode_combo_box_ = new QComboBox(logic_properties_page_); - logic_mode_combo_box_->setObjectName(QStringLiteral("logicModeComboBox")); - logic_comparison_combo_box_ = new QComboBox(logic_properties_page_); - logic_comparison_combo_box_->setObjectName(QStringLiteral("logicComparisonComboBox")); - logic_comparison_combo_box_->addItem( - tr("等于"), static_cast(ComparisonOperator::Equal)); - logic_comparison_combo_box_->addItem( - tr("不等于"), static_cast(ComparisonOperator::NotEqual)); - logic_comparison_combo_box_->addItem( - tr("小于"), static_cast(ComparisonOperator::LessThan)); - logic_comparison_combo_box_->addItem( - tr("小于等于"), static_cast(ComparisonOperator::LessThanOrEqual)); - logic_comparison_combo_box_->addItem( - tr("大于"), static_cast(ComparisonOperator::GreaterThan)); - logic_comparison_combo_box_->addItem( - tr("大于等于"), static_cast(ComparisonOperator::GreaterThanOrEqual)); - logic_value_spin_box_ = new QSpinBox(logic_properties_page_); - logic_value_spin_box_->setObjectName(QStringLiteral("logicValueSpinBox")); - logic_value_spin_box_->setRange( - std::numeric_limits::min(), - std::numeric_limits::max()); - apply_logic_properties_button_ = new QPushButton( - tr("应用属性"), logic_properties_page_); - apply_logic_properties_button_->setObjectName( - QStringLiteral("applyLogicPropertiesButton")); - logic_form->addRow(tr("节点 ID"), logic_node_id_edit_); - logic_form->addRow(tr("节点类型"), logic_node_type_label_); - logic_form->addRow(tr("地址区域"), logic_address_area_label_); - logic_form->addRow(tr("地址"), logic_address_spin_box_); - logic_form->addRow(tr("工作方式"), logic_mode_combo_box_); - logic_form->addRow(tr("比较运算"), logic_comparison_combo_box_); - logic_form->addRow(tr("比较常量"), logic_value_spin_box_); - logic_form->addRow(apply_logic_properties_button_); - - property_stack_->addWidget(hmi_properties_page_); - property_stack_->addWidget(logic_properties_page_); - ui_->propertiesForm->addRow(property_stack_); - connect(apply_properties_button_, &QPushButton::clicked, + ui_->bindingAreaComboBox->setItemData(0, -1); + ui_->bindingAreaComboBox->setItemData(1, 0); + ui_->bindingAreaComboBox->setItemData(2, 1); + for (int index = 0; index < ui_->logicComparisonComboBox->count(); ++index) + { + ui_->logicComparisonComboBox->setItemData(index, index); + } + connect(ui_->applyPropertiesButton, &QPushButton::clicked, this, &MainWindow::applySelectedControlProperties); - connect(apply_logic_properties_button_, &QPushButton::clicked, + connect(ui_->applyLogicPropertiesButton, &QPushButton::clicked, this, &MainWindow::applySelectedLogicNodeProperties); + connect(ui_->hmiDataPointComboBox, qOverload(&QComboBox::currentIndexChanged), + this, + [this](int index) + { + if (index <= 0) + { + return; + } + const QString address = ui_->hmiDataPointComboBox->currentData().toString(); + ui_->bindingAreaComboBox->setCurrentIndex(address.startsWith('M') ? 1 : 2); + ui_->bindingIndexSpinBox->setValue(address.mid(1).toInt()); + }); + connect(ui_->logicDataPointComboBox, qOverload(&QComboBox::currentIndexChanged), + this, + [this](int index) + { + if (index <= 0) + { + return; + } + ui_->logicAddressSpinBox->setValue( + ui_->logicDataPointComboBox->currentData().toString().mid(1).toInt()); + }); showControlProperties({}); } +void MainWindow::configureDataPointEditor() +{ + ui_->dataPointTable->horizontalHeader()->setSectionResizeMode( + QHeaderView::ResizeToContents); + ui_->dataPointTable->horizontalHeader()->setStretchLastSection(true); + tabifyDockWidget(ui_->outputDock, ui_->dataPointDock); + ui_->viewMenu->addAction(ui_->dataPointDock->toggleViewAction()); + + connect(ui_->addDataPointButton, &QPushButton::clicked, this, &MainWindow::addDataPoint); + connect(ui_->updateDataPointButton, &QPushButton::clicked, + this, &MainWindow::updateSelectedDataPoint); + connect(ui_->deleteDataPointButton, &QPushButton::clicked, + this, &MainWindow::deleteSelectedDataPoint); + connect(ui_->dataPointTable, &QTableWidget::itemSelectionChanged, + this, + [this] + { + const int row = ui_->dataPointTable->currentRow(); + if (row < 0) + { + return; + } + const QString address = ui_->dataPointTable->item(row, 0)->text(); + ui_->dataPointAreaComboBox->setCurrentIndex(address.startsWith('M') ? 0 : 1); + ui_->dataPointAddressSpinBox->setValue(address.mid(1).toInt()); + ui_->dataPointNameEdit->setText(ui_->dataPointTable->item(row, 2)->text()); + ui_->dataPointCommentEdit->setText(ui_->dataPointTable->item(row, 3)->text()); + }); +} + +void MainWindow::configurePlcConnection() +{ + connect(ui_->configurePlcAction, &QAction::triggered, this, &MainWindow::connectPlc); + connect(ui_->disconnectPlcAction, &QAction::triggered, this, &MainWindow::disconnectPlc); +} + void MainWindow::refreshProjectUi() { // 工程树只展示模型摘要,画布内容由 HmiEditorWidget 单独投影 @@ -608,12 +553,162 @@ void MainWindow::refreshProjectUi() logic_root, {fromUtf8(logic.name) + suffix})); } ui_->projectTree->expandAll(); + refreshDataPointUi(); +} + +void MainWindow::refreshDataPointUi() +{ + const auto &points = data_point_service_.dataPoints(); + ui_->dataPointTable->setRowCount(static_cast(points.size())); + ui_->hmiDataPointComboBox->blockSignals(true); + ui_->logicDataPointComboBox->blockSignals(true); + ui_->hmiDataPointComboBox->clear(); + ui_->logicDataPointComboBox->clear(); + ui_->hmiDataPointComboBox->addItem(tr("手动输入")); + ui_->logicDataPointComboBox->addItem(tr("手动输入")); + for (int row = 0; row < static_cast(points.size()); ++row) + { + const DataPoint &point = points.at(static_cast(row)); + const QString address = fromUtf8(point.address.toString()); + const QString display = tr("%1 - %2").arg(address, fromUtf8(point.name)); + ui_->hmiDataPointComboBox->addItem(display, address); + ui_->logicDataPointComboBox->addItem(display, address); + QString value = QStringLiteral("-"); + if (runtime_mode_service_.mode() != ApplicationMode::Editing) + { + if (point.address.area() == RegisterArea::M) + { + const BitReadResult read = hmi_runtime_service_.readBit(point.address); + value = read.succeeded ? (read.value ? QStringLiteral("ON") : QStringLiteral("OFF")) + : tr("不可用"); + } + else + { + const WordReadResult read = hmi_runtime_service_.readWord(point.address); + value = read.succeeded ? QString::number(read.value) : tr("不可用"); + } + } + QStringList references; + for (const DataPointReference &reference : data_point_service_.references(point.address)) + { + references.push_back(fromUtf8(reference.location)); + } + const QStringList columns{ + address, + point.address.area() == RegisterArea::M ? tr("位") : tr("16 位字"), + fromUtf8(point.name), + fromUtf8(point.comment), + value, + references.join(QStringLiteral("; "))}; + for (int column = 0; column < columns.size(); ++column) + { + ui_->dataPointTable->setItem( + row, column, new QTableWidgetItem(columns.at(column))); + } + } + ui_->hmiDataPointComboBox->blockSignals(false); + ui_->logicDataPointComboBox->blockSignals(false); +} + +void MainWindow::addDataPoint() +{ + const RegisterArea area = ui_->dataPointAreaComboBox->currentIndex() == 0 + ? RegisterArea::M : RegisterArea::D; + const DataPointResult result = data_point_service_.add({ + RegisterAddress{area, ui_->dataPointAddressSpinBox->value()}, + toUtf8(ui_->dataPointNameEdit->text()), + toUtf8(ui_->dataPointCommentEdit->text())}); + if (!result.succeeded) + { + showProjectResult(tr("新增数据点"), fromUtf8(result.message), false); + return; + } + refreshProjectUi(); + statusBar()->showMessage(tr("数据点已新增"), 3000); +} + +void MainWindow::updateSelectedDataPoint() +{ + const int row = ui_->dataPointTable->currentRow(); + if (row < 0) + { + statusBar()->showMessage(tr("请先选择要更新的数据点"), 3000); + return; + } + const QString old_text = ui_->dataPointTable->item(row, 0)->text(); + const RegisterAddress old_address{ + old_text.startsWith('M') ? RegisterArea::M : RegisterArea::D, + old_text.mid(1).toInt()}; + const RegisterArea area = ui_->dataPointAreaComboBox->currentIndex() == 0 + ? RegisterArea::M : RegisterArea::D; + const DataPointResult result = data_point_service_.update( + old_address, + {RegisterAddress{area, ui_->dataPointAddressSpinBox->value()}, + toUtf8(ui_->dataPointNameEdit->text()), + toUtf8(ui_->dataPointCommentEdit->text())}); + if (!result.succeeded) + { + showProjectResult(tr("更新数据点"), fromUtf8(result.message), false); + return; + } + refreshProjectUi(); + statusBar()->showMessage(tr("数据点已更新"), 3000); +} + +void MainWindow::deleteSelectedDataPoint() +{ + const int row = ui_->dataPointTable->currentRow(); + if (row < 0) + { + statusBar()->showMessage(tr("请先选择要删除的数据点"), 3000); + return; + } + const QString address_text = ui_->dataPointTable->item(row, 0)->text(); + const DataPointResult result = data_point_service_.remove({ + address_text.startsWith('M') ? RegisterArea::M : RegisterArea::D, + address_text.mid(1).toInt()}); + if (!result.succeeded) + { + showProjectResult(tr("删除数据点"), fromUtf8(result.message), false); + return; + } + refreshProjectUi(); + statusBar()->showMessage(tr("数据点已删除"), 3000); +} + +void MainWindow::connectPlc() +{ + PlcConnectionDialog dialog(plc_configuration_, this); + if (dialog.exec() != QDialog::Accepted) + { + return; + } + plc_configuration_ = dialog.configuration(); + if (plc_configuration_.portName.empty()) + { + showProjectResult(tr("连接 PLC"), tr("请选择或输入串口端口"), false); + return; + } + const PlcCommunicationResult result = runtime_mode_service_.connectPlc( + plc_configuration_); + if (!result.succeeded) + { + showProjectResult(tr("连接 PLC"), fromUtf8(result.message), false); + return; + } + statusBar()->showMessage(tr("PLC 正在连接,等待首次 M/D 读取"), 5000); +} + +void MainWindow::disconnectPlc() +{ + runtime_mode_service_.disconnectPlc(); + updateModeUi(tr("PLC 已断开")); } // 根据控件ID加载控件属性到右侧属性面板 void MainWindow::showControlProperties(const std::string &control_id) { - property_stack_->setCurrentWidget(hmi_properties_page_); + ui_->propertyStack->setCurrentWidget(ui_->hmiPropertiesPage); // 更新窗口全局状态:记录当前选中控件ID selected_control_id_ = control_id; @@ -630,15 +725,15 @@ void MainWindow::showControlProperties(const std::string &control_id) : tr("未选择")); // 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑 - for (QWidget *widget : {static_cast(control_id_edit_), - static_cast(control_text_edit_), - static_cast(control_x_spin_box_), - static_cast(control_y_spin_box_), - static_cast(control_width_spin_box_), - static_cast(control_height_spin_box_), - static_cast(binding_area_combo_box_), - static_cast(binding_index_spin_box_), - static_cast(apply_properties_button_)}) + for (QWidget *widget : {static_cast(ui_->controlIdEdit), + static_cast(ui_->controlTextEdit), + static_cast(ui_->controlXSpinBox), + static_cast(ui_->controlYSpinBox), + static_cast(ui_->controlWidthSpinBox), + static_cast(ui_->controlHeightSpinBox), + static_cast(ui_->bindingAreaComboBox), + static_cast(ui_->bindingIndexSpinBox), + static_cast(ui_->applyPropertiesButton)}) { widget->setEnabled(has_control); } @@ -650,105 +745,105 @@ void MainWindow::showControlProperties(const std::string &control_id) } // 将控件基础信息填充至UI输入框 - control_id_edit_->setText(fromUtf8(control->id)); - control_text_edit_->setText(fromUtf8(control->text)); + ui_->controlIdEdit->setText(fromUtf8(control->id)); + ui_->controlTextEdit->setText(fromUtf8(control->text)); // 控件位置坐标 - control_x_spin_box_->setValue(control->bounds.x); - control_y_spin_box_->setValue(control->bounds.y); + ui_->controlXSpinBox->setValue(control->bounds.x); + ui_->controlYSpinBox->setValue(control->bounds.y); // 控件宽高尺寸 - control_width_spin_box_->setValue(control->bounds.width); - control_height_spin_box_->setValue(control->bounds.height); + ui_->controlWidthSpinBox->setValue(control->bounds.width); + ui_->controlHeightSpinBox->setValue(control->bounds.height); // 根据控件类型获取默认寄存器区域:按钮/指示灯默认M区;数值类控件默认D区 // 未绑定必须保留明确状态,避免属性编辑把显示默认值误提交为 M0/D0 - binding_area_combo_box_->setCurrentIndex(!control->binding.has_value() ? 0 + ui_->bindingAreaComboBox->setCurrentIndex(!control->binding.has_value() ? 0 : control->binding->area() == RegisterArea::M ? 1 : 2); - binding_index_spin_box_->setValue( + ui_->bindingIndexSpinBox->setValue( control->binding.has_value() ? control->binding->index() : 0); } void MainWindow::showLogicNodeProperties(const std::string &node_id) { - property_stack_->setCurrentWidget(logic_properties_page_); + ui_->propertyStack->setCurrentWidget(ui_->logicPropertiesPage); selected_logic_node_id_ = node_id; const LogicNode *node = logic_editor_service_.findNode( logic_editor_service_.firstLogicId(), node_id); const bool has_node = node != nullptr; ui_->selectionValueLabel->setText( has_node ? fromUtf8(node->id) : tr("未选择")); - for (QWidget *widget : {static_cast(logic_node_id_edit_), - static_cast(logic_address_spin_box_), - static_cast(logic_mode_combo_box_), - static_cast(logic_comparison_combo_box_), - static_cast(logic_value_spin_box_), - static_cast(apply_logic_properties_button_)}) + for (QWidget *widget : {static_cast(ui_->logicNodeIdEdit), + static_cast(ui_->logicAddressSpinBox), + static_cast(ui_->logicModeComboBox), + static_cast(ui_->logicComparisonComboBox), + static_cast(ui_->logicValueSpinBox), + static_cast(ui_->applyLogicPropertiesButton)}) { widget->setEnabled(has_node); } - logic_node_type_label_->setEnabled(has_node); - logic_address_area_label_->setEnabled(has_node); + ui_->logicNodeTypeLabel->setEnabled(has_node); + ui_->logicAddressAreaLabel->setEnabled(has_node); if (!has_node) { - logic_node_id_edit_->clear(); - logic_node_type_label_->clear(); - logic_address_area_label_->clear(); - logic_mode_combo_box_->clear(); + ui_->logicNodeIdEdit->clear(); + ui_->logicNodeTypeLabel->clear(); + ui_->logicAddressAreaLabel->clear(); + ui_->logicModeComboBox->clear(); return; } - logic_node_id_edit_->setText(fromUtf8(node->id)); - logic_mode_combo_box_->clear(); + ui_->logicNodeIdEdit->setText(fromUtf8(node->id)); + ui_->logicModeComboBox->clear(); if (const auto *contact = std::get_if(&node->config)) { - logic_node_type_label_->setText(tr("触点")); - logic_address_area_label_->setText(QStringLiteral("M")); - logic_address_spin_box_->setValue(contact->address.index()); - logic_mode_combo_box_->addItem( + ui_->logicNodeTypeLabel->setText(tr("触点")); + ui_->logicAddressAreaLabel->setText(QStringLiteral("M")); + ui_->logicAddressSpinBox->setValue(contact->address.index()); + ui_->logicModeComboBox->addItem( tr("常开"), static_cast(ContactMode::NormallyOpen)); - logic_mode_combo_box_->addItem( + ui_->logicModeComboBox->addItem( tr("常闭"), static_cast(ContactMode::NormallyClosed)); - logic_mode_combo_box_->setCurrentIndex( - logic_mode_combo_box_->findData(static_cast(contact->mode))); - logic_mode_combo_box_->setEnabled(true); - logic_comparison_combo_box_->setEnabled(false); - logic_value_spin_box_->setEnabled(false); + ui_->logicModeComboBox->setCurrentIndex( + ui_->logicModeComboBox->findData(static_cast(contact->mode))); + ui_->logicModeComboBox->setEnabled(true); + ui_->logicComparisonComboBox->setEnabled(false); + ui_->logicValueSpinBox->setEnabled(false); } else if (const auto *coil = std::get_if(&node->config)) { - logic_node_type_label_->setText(tr("线圈")); - logic_address_area_label_->setText(QStringLiteral("M")); - logic_address_spin_box_->setValue(coil->address.index()); - logic_mode_combo_box_->addItem( + ui_->logicNodeTypeLabel->setText(tr("线圈")); + ui_->logicAddressAreaLabel->setText(QStringLiteral("M")); + ui_->logicAddressSpinBox->setValue(coil->address.index()); + ui_->logicModeComboBox->addItem( tr("普通"), static_cast(CoilMode::Normal)); - logic_mode_combo_box_->addItem( + ui_->logicModeComboBox->addItem( tr("置位"), static_cast(CoilMode::Set)); - logic_mode_combo_box_->addItem( + ui_->logicModeComboBox->addItem( tr("复位"), static_cast(CoilMode::Reset)); - logic_mode_combo_box_->setCurrentIndex( - logic_mode_combo_box_->findData(static_cast(coil->mode))); - logic_mode_combo_box_->setEnabled(true); - logic_comparison_combo_box_->setEnabled(false); - logic_value_spin_box_->setEnabled(false); + ui_->logicModeComboBox->setCurrentIndex( + ui_->logicModeComboBox->findData(static_cast(coil->mode))); + ui_->logicModeComboBox->setEnabled(true); + ui_->logicComparisonComboBox->setEnabled(false); + ui_->logicValueSpinBox->setEnabled(false); } else { const auto &compare = std::get(node->config); - logic_node_type_label_->setText(tr("D 值比较")); - logic_address_area_label_->setText(QStringLiteral("D")); - logic_address_spin_box_->setValue(compare.address.index()); - logic_mode_combo_box_->addItem(tr("不适用")); - logic_mode_combo_box_->setEnabled(false); - logic_comparison_combo_box_->setEnabled(true); - logic_comparison_combo_box_->setCurrentIndex( - logic_comparison_combo_box_->findData( + ui_->logicNodeTypeLabel->setText(tr("D 值比较")); + ui_->logicAddressAreaLabel->setText(QStringLiteral("D")); + ui_->logicAddressSpinBox->setValue(compare.address.index()); + ui_->logicModeComboBox->addItem(tr("不适用")); + ui_->logicModeComboBox->setEnabled(false); + ui_->logicComparisonComboBox->setEnabled(true); + ui_->logicComparisonComboBox->setCurrentIndex( + ui_->logicComparisonComboBox->findData( static_cast(compare.comparison))); - logic_value_spin_box_->setEnabled(true); - logic_value_spin_box_->setValue(compare.value); + ui_->logicValueSpinBox->setEnabled(true); + ui_->logicValueSpinBox->setValue(compare.value); } if (!node->configured) { - logic_node_type_label_->setText(tr("待配置:%1").arg( - logic_node_type_label_->text())); + ui_->logicNodeTypeLabel->setText(tr("待配置:%1").arg( + ui_->logicNodeTypeLabel->text())); } } @@ -797,13 +892,13 @@ void MainWindow::applySelectedControlProperties() } // 从现有控件复制不可见属性,避免属性面板覆盖扩展配置 HmiControl control = *old_control; - control.id = toUtf8(control_id_edit_->text()); - control.text = toUtf8(control_text_edit_->text()); - control.bounds.x = control_x_spin_box_->value(); - control.bounds.y = control_y_spin_box_->value(); - control.bounds.width = control_width_spin_box_->value(); - control.bounds.height = control_height_spin_box_->value(); - const int binding_area = binding_area_combo_box_->currentData().toInt(); + control.id = toUtf8(ui_->controlIdEdit->text()); + control.text = toUtf8(ui_->controlTextEdit->text()); + control.bounds.x = ui_->controlXSpinBox->value(); + control.bounds.y = ui_->controlYSpinBox->value(); + control.bounds.width = ui_->controlWidthSpinBox->value(); + control.bounds.height = ui_->controlHeightSpinBox->value(); + const int binding_area = ui_->bindingAreaComboBox->currentData().toInt(); if (binding_area < 0) { control.binding.reset(); @@ -811,7 +906,7 @@ void MainWindow::applySelectedControlProperties() else { const RegisterArea area = binding_area == 0 ? RegisterArea::M : RegisterArea::D; - control.binding = RegisterAddress{area, binding_index_spin_box_->value()}; + control.binding = RegisterAddress{area, ui_->bindingIndexSpinBox->value()}; } const HmiEditorResult result = hmi_editor_service_.updateControl( hmi_editor_service_.firstPageId(), selected_control_id_, control); @@ -829,15 +924,31 @@ void MainWindow::applySelectedControlProperties() void MainWindow::addLogicCondition(const LogicNodeConfig &config) { - const LogicEditorResult result = parallel_insert_action_->isChecked() - ? logic_editor_widget_->addParallelCondition(config) - : logic_editor_widget_->appendCondition(config); + LogicEditorResult result; + if (ui_->beforeInsertAction->isChecked()) + { + result = logic_editor_widget_->insertCondition( + config, SeriesInsertPosition::Before); + } + else if (ui_->afterInsertAction->isChecked()) + { + result = logic_editor_widget_->insertCondition( + config, SeriesInsertPosition::After); + } + else if (ui_->parallelInsertAction->isChecked()) + { + result = logic_editor_widget_->addParallelCondition(config); + } + else + { + result = logic_editor_widget_->appendCondition(config); + } if (!result.succeeded) { showProjectResult(tr("添加逻辑条件"), fromUtf8(result.message), false); return; } - parallel_insert_action_->setChecked(false); + ui_->appendInsertAction->setChecked(true); selected_logic_node_id_ = result.id; showLogicNodeProperties(result.id); refreshProjectUi(); @@ -898,22 +1009,22 @@ void MainWindow::applySelectedLogicNodeProperties() if (std::holds_alternative(config)) { config = ContactNodeConfig{ - RegisterAddress{RegisterArea::M, logic_address_spin_box_->value()}, - static_cast(logic_mode_combo_box_->currentData().toInt())}; + RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()}, + static_cast(ui_->logicModeComboBox->currentData().toInt())}; } else if (std::holds_alternative(config)) { config = CoilNodeConfig{ - RegisterAddress{RegisterArea::M, logic_address_spin_box_->value()}, - static_cast(logic_mode_combo_box_->currentData().toInt())}; + RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()}, + static_cast(ui_->logicModeComboBox->currentData().toInt())}; } else { config = CompareNodeConfig{ - RegisterAddress{RegisterArea::D, logic_address_spin_box_->value()}, + RegisterAddress{RegisterArea::D, ui_->logicAddressSpinBox->value()}, static_cast( - logic_comparison_combo_box_->currentData().toInt()), - static_cast(logic_value_spin_box_->value())}; + ui_->logicComparisonComboBox->currentData().toInt()), + static_cast(ui_->logicValueSpinBox->value())}; } const LogicEditorResult result = logic_editor_service_.updateNodeConfig( @@ -1072,26 +1183,32 @@ void MainWindow::updateModeUi(const QString &message) ui_->projectDock->setEnabled(policy.allowsProjectEditing); ui_->propertiesDock->setEnabled(policy.allowsProjectEditing); hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); - hmi_editor_widget_->setRuntimeActive(policy.usesVirtualRegisters); + hmi_editor_widget_->setRuntimeActive( + policy.usesVirtualRegisters || policy.usesPlcRegisters); logic_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); - hmi_tool_bar_->setEnabled(policy.allowsProjectEditing); - logic_tool_bar_->setEnabled(policy.allowsProjectEditing); - add_button_action_->setEnabled(policy.allowsProjectEditing); - add_indicator_action_->setEnabled(policy.allowsProjectEditing); - add_numeric_display_action_->setEnabled(policy.allowsProjectEditing); - add_numeric_input_action_->setEnabled(policy.allowsProjectEditing); - delete_control_action_->setEnabled(policy.allowsProjectEditing); - add_normally_open_action_->setEnabled(policy.allowsProjectEditing); - add_normally_closed_action_->setEnabled(policy.allowsProjectEditing); - add_normal_coil_action_->setEnabled(policy.allowsProjectEditing); - add_set_coil_action_->setEnabled(policy.allowsProjectEditing); - add_reset_coil_action_->setEnabled(policy.allowsProjectEditing); - add_compare_action_->setEnabled(policy.allowsProjectEditing); - delete_logic_action_->setEnabled(policy.allowsProjectEditing); - new_project_action_->setEnabled(policy.allowsProjectEditing); - save_project_action_->setEnabled(policy.allowsProjectEditing); - save_as_project_action_->setEnabled(policy.allowsProjectEditing); - load_project_action_->setEnabled(policy.allowsProjectEditing); + if (mode == ApplicationMode::Editing) + { + logic_editor_widget_->clearRuntimeTrace(); + } + ui_->hmiToolBar->setEnabled(policy.allowsProjectEditing); + ui_->logicToolBar->setEnabled(policy.allowsProjectEditing); + ui_->dataPointDock->setEnabled(policy.allowsProjectEditing); + ui_->addButtonAction->setEnabled(policy.allowsProjectEditing); + ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing); + ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing); + ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing); + ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); + ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); + ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); + ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing); + ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing); + ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing); + ui_->addCompareAction->setEnabled(policy.allowsProjectEditing); + ui_->deleteLogicAction->setEnabled(policy.allowsProjectEditing); + ui_->newProjectAction->setEnabled(policy.allowsProjectEditing); + ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); + ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing); + ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing); mode_status_label_->setText(modeText(mode)); if (mode == ApplicationMode::Editing) @@ -1112,6 +1229,16 @@ void MainWindow::updateModeUi(const QString &message) register_status_label_->setText( policy.usesVirtualRegisters ? tr("数据源:虚拟 M/D") : policy.usesPlcRegisters ? tr("数据源:PLC 缓存") : tr("数据源:未启用")); + const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); + ui_->configurePlcAction->setEnabled( + policy.allowsProjectEditing && plc_state == PlcConnectionState::Disconnected); + ui_->disconnectPlcAction->setEnabled(plc_state != PlcConnectionState::Disconnected); + if (plc_state == PlcConnectionState::Connected) + { + register_status_label_->setText( + runtime_mode_service_.initialPlcReadCompleted() + ? tr("PLC:已连接,首读完成") : tr("PLC:已连接,正在首读")); + } updateSimulationUi(false); statusBar()->showMessage(message, 4000); ui_->outputList->addItem(message); @@ -1122,9 +1249,11 @@ void MainWindow::updateSimulationUi(bool report_fault) { const bool offline = runtime_mode_service_.mode() == ApplicationMode::OfflineRunning; + const bool online = runtime_mode_service_.mode() + == ApplicationMode::OnlineRunning; const SimulationState state = runtime_mode_service_.simulationState(); hmi_editor_widget_->setRuntimeWriteEnabled( - offline && state == SimulationState::Running); + online || (offline && state == SimulationState::Running)); switch (state) { @@ -1141,6 +1270,14 @@ void MainWindow::updateSimulationUi(bool report_fault) if (report_fault) { const LogicScanResult &error = runtime_mode_service_.simulationError(); + logic_editor_widget_->setRuntimeTrace( + runtime_mode_service_.offlineSimulationService().traceSnapshot(), + error.nodeId); + if (!error.nodeId.empty()) + { + logic_editor_widget_->selectNode(error.nodeId); + showLogicNodeProperties(error.nodeId); + } QString message = tr("离线仿真异常:%1").arg(fromUtf8(error.message)); if (!error.logicId.empty()) { diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index c5ac1f3..d1a4992 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -11,6 +11,7 @@ #include "domain/hmi_model.h" #include "domain/control_logic_model.h" #include "domain/runtime_state.h" +#include "services/plc_communication_gateway.h" #include @@ -18,15 +19,8 @@ QT_BEGIN_NAMESPACE class QActionGroup; -class QAction; -class QComboBox; class QLabel; -class QLineEdit; -class QPushButton; -class QSpinBox; -class QStackedWidget; class QTimer; -class QToolBar; namespace Ui { class MainWindow; @@ -37,6 +31,7 @@ class RuntimeModeService; class ProjectService; class HmiEditorService; class HmiRuntimeService; +class DataPointService; class HmiEditorWidget; class LogicEditorService; class LogicEditorWidget; @@ -63,6 +58,7 @@ public: RuntimeModeService &runtime_mode_service, ProjectService &project_service, HmiEditorService &hmi_editor_service, + DataPointService &data_point_service, LogicEditorService &logic_editor_service, HmiRuntimeService &hmi_runtime_service, QWidget *parent = nullptr); @@ -80,6 +76,8 @@ private: void configureLogicEditor(); // 创建并连接控件属性编辑表单 void configurePropertyEditor(); + void configureDataPointEditor(); + void configurePlcConnection(); // 刷新工程树和当前 HMI 页面信息 void refreshProjectUi(); // 显示指定 HMI 控件的可编辑属性 @@ -102,6 +100,12 @@ private: void deleteSelectedLogicObject(); // 将逻辑属性表单内容应用到当前选中节点 void applySelectedLogicNodeProperties(); + void refreshDataPointUi(); + void addDataPoint(); + void updateSelectedDataPoint(); + void deleteSelectedDataPoint(); + void connectPlc(); + void disconnectPlc(); // 创建新的工程并刷新编辑界面 void createNewProject(); // 保存当前工程到已关联的路径 @@ -144,55 +148,18 @@ private: */ ProjectService &project_service_; HmiEditorService &hmi_editor_service_; + DataPointService &data_point_service_; LogicEditorService &logic_editor_service_; HmiRuntimeService &hmi_runtime_service_; QActionGroup *mode_action_group_ = nullptr; - QAction *new_project_action_ = nullptr; - QAction *save_project_action_ = nullptr; - QAction *save_as_project_action_ = nullptr; - QAction *load_project_action_ = nullptr; - QAction *add_button_action_ = nullptr; - QAction *add_indicator_action_ = nullptr; - QAction *add_numeric_display_action_ = nullptr; - QAction *add_numeric_input_action_ = nullptr; - QAction *delete_control_action_ = nullptr; - QAction *add_rung_action_ = nullptr; - QAction *parallel_insert_action_ = nullptr; - QAction *add_normally_open_action_ = nullptr; - QAction *add_normally_closed_action_ = nullptr; - QAction *add_normal_coil_action_ = nullptr; - QAction *add_set_coil_action_ = nullptr; - QAction *add_reset_coil_action_ = nullptr; - QAction *add_compare_action_ = nullptr; - QAction *delete_logic_action_ = nullptr; - QToolBar *hmi_tool_bar_ = nullptr; - QToolBar *logic_tool_bar_ = nullptr; + QActionGroup *logic_insert_action_group_ = nullptr; HmiEditorWidget *hmi_editor_widget_ = nullptr; LogicEditorWidget *logic_editor_widget_ = nullptr; QTimer *runtime_refresh_timer_ = nullptr; QLabel *mode_status_label_ = nullptr; QLabel *register_status_label_ = nullptr; QLabel *executor_status_label_ = nullptr; - QLineEdit *control_id_edit_ = nullptr; - QLineEdit *control_text_edit_ = nullptr; - QSpinBox *control_x_spin_box_ = nullptr; - QSpinBox *control_y_spin_box_ = nullptr; - QSpinBox *control_width_spin_box_ = nullptr; - QSpinBox *control_height_spin_box_ = nullptr; - QComboBox *binding_area_combo_box_ = nullptr; - QSpinBox *binding_index_spin_box_ = nullptr; - QPushButton *apply_properties_button_ = nullptr; - QStackedWidget *property_stack_ = nullptr; - QWidget *hmi_properties_page_ = nullptr; - QWidget *logic_properties_page_ = nullptr; - QLineEdit *logic_node_id_edit_ = nullptr; - QLabel *logic_node_type_label_ = nullptr; - QLabel *logic_address_area_label_ = nullptr; - QSpinBox *logic_address_spin_box_ = nullptr; - QComboBox *logic_mode_combo_box_ = nullptr; - QComboBox *logic_comparison_combo_box_ = nullptr; - QSpinBox *logic_value_spin_box_ = nullptr; - QPushButton *apply_logic_properties_button_ = nullptr; + PlcSerialConfiguration plc_configuration_; std::string selected_control_id_; std::string selected_logic_node_id_; }; diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index 95836f2..da52ee7 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -221,7 +221,12 @@ 文件(&F) - + + + + + + @@ -229,7 +234,10 @@ - + + + + @@ -260,7 +268,39 @@ - + + + + + HMI 控件 + Qt::ToolButtonTextBesideIcon + TopToolBarArea + false + + + + + + + + 控制逻辑节点 + Qt::ToolButtonTextBesideIcon + TopToolBarArea + false + + + + + + + + + + + + + + @@ -360,32 +400,82 @@ 164 - - - QFormLayout::AllNonFixedFieldsGrow - - - - - 当前对象 - - - - - - - 未选择 - - - - + + + + 当前对象 + 未选择 + + + + + + + QFormLayout::AllNonFixedFieldsGrow + 控件 ID + + 显示文本 + + X + 4000 + Y + 4000 + 宽度 + 14000 + 高度 + 14000 + 绑定区域 + + 未绑定-1 + M0 + D1 + + 绑定地址 + 4000 + 选择数据点 + + 应用属性 + + + + + QFormLayout::AllNonFixedFieldsGrow + 节点 ID + true + 节点类型 + + 地址区域 + + 地址 + 4000 + 选择数据点 + + 工作方式 + + 比较运算 + + 等于0 + 不等于1 + 小于2 + 小于等于3 + 大于4 + 大于等于5 + + 比较常量 + -3276832767 + 应用属性 + + + + + - + 300 @@ -422,13 +512,77 @@ + - - + + 420170 + 数据点 + 8 + + + 6 + 6 + 6 + 6 + + + QAbstractItemView::SingleSelection + QAbstractItemView::SelectRows + QAbstractItemView::NoEditTriggers + 地址 + 类型 + 名称 + 注释 + 当前值 + 引用位置 + + + + + 区域 + M0D1 + 地址 + 4000 + 名称 + + 注释 + + 新增 + 更新 + 删除 + + + + + + 退出(&X) - + + 新建工程 + 保存工程 + 工程另存为 + 加载工程 + PLC 配置配置参数并连接真实 PLC + 断开 PLC断开当前 PLC 连接 + 按钮添加按钮控件 + 指示灯添加指示灯控件 + 数值显示添加数值显示控件 + 数值输入添加数值输入控件 + 删除删除当前控件 + 新建网络在梯形图末尾新增网络 + truetrue末尾追加将下一个条件追加到当前网络末尾 + true前插将下一个条件串联到选中节点或支路之前 + true后插将下一个条件串联到选中节点或支路之后 + true并联插入将下一个条件并联到选中节点或整条支路 + 常开向当前网络添加常开触点 + 常闭向当前网络添加常闭触点 + 线圈设置当前网络的普通线圈 + 置位设置当前网络的置位线圈 + 复位设置当前网络的复位线圈 + D 比较向当前网络添加 D 值与常量比较 + 删除删除选中的逻辑节点或网络 true @@ -468,4 +622,4 @@ - \ No newline at end of file + diff --git a/app/src/ui/plc_connection_dialog.cpp b/app/src/ui/plc_connection_dialog.cpp new file mode 100644 index 0000000..e974954 --- /dev/null +++ b/app/src/ui/plc_connection_dialog.cpp @@ -0,0 +1,88 @@ +#include "plc_connection_dialog.h" + +#include "ui_plc_connection_dialog.h" + +#include +#include + +#include + +namespace { + +void setItemData(QComboBox *combo_box, const std::initializer_list &values) +{ + int index = 0; + for (int value : values) + { + combo_box->setItemData(index, value); + ++index; + } +} + +void selectData(QComboBox *combo_box, int value) +{ + const int index = combo_box->findData(value); + if (index >= 0) + { + combo_box->setCurrentIndex(index); + } +} + +} // namespace + +PlcConnectionDialog::PlcConnectionDialog( + const PlcSerialConfiguration &configuration, + QWidget *parent) + : QDialog(parent), + ui_(std::make_unique()) +{ + ui_->setupUi(this); + setItemData(ui_->baudRateComboBox, {9600, 19200, 38400, 57600, 115200}); + setItemData(ui_->dataBitsComboBox, {7, 8}); + setItemData(ui_->parityComboBox, {0, 3, 2}); + setItemData(ui_->stopBitsComboBox, {1, 2}); + + for (const QSerialPortInfo &port : QSerialPortInfo::availablePorts()) + { + ui_->serialPortComboBox->addItem( + port.portName(), + port.description().isEmpty() ? port.portName() : port.description()); + } + const QString port_name = QString::fromUtf8(configuration.portName.c_str()); + if (!port_name.isEmpty()) + { + const int port_index = ui_->serialPortComboBox->findText(port_name); + if (port_index < 0) + { + ui_->serialPortComboBox->addItem(port_name); + } + ui_->serialPortComboBox->setCurrentText(port_name); + } + + ui_->serverAddressSpinBox->setValue(configuration.serverAddress); + selectData(ui_->baudRateComboBox, configuration.baudRate); + selectData(ui_->dataBitsComboBox, configuration.dataBits); + selectData(ui_->parityComboBox, configuration.parity); + selectData(ui_->stopBitsComboBox, configuration.stopBits); + ui_->responseTimeoutSpinBox->setValue(configuration.responseTimeoutMs); + ui_->retriesSpinBox->setValue(configuration.retries); + ui_->pollIntervalSpinBox->setValue(configuration.pollIntervalMs); +} + +PlcConnectionDialog::~PlcConnectionDialog() = default; + +PlcSerialConfiguration PlcConnectionDialog::configuration() const +{ + PlcSerialConfiguration result; + const QByteArray port_name = ui_->serialPortComboBox->currentText().trimmed().toUtf8(); + result.portName.assign(port_name.constData(), static_cast(port_name.size())); + result.serverAddress = ui_->serverAddressSpinBox->value(); + result.baudRate = ui_->baudRateComboBox->currentData().toInt(); + result.dataBits = ui_->dataBitsComboBox->currentData().toInt(); + result.parity = ui_->parityComboBox->currentData().toInt(); + result.stopBits = ui_->stopBitsComboBox->currentData().toInt(); + result.responseTimeoutMs = ui_->responseTimeoutSpinBox->value(); + result.retries = ui_->retriesSpinBox->value(); + result.pollIntervalMs = ui_->pollIntervalSpinBox->value(); + return result; +} diff --git a/app/src/ui/plc_connection_dialog.h b/app/src/ui/plc_connection_dialog.h new file mode 100644 index 0000000..8ef96d4 --- /dev/null +++ b/app/src/ui/plc_connection_dialog.h @@ -0,0 +1,27 @@ +#pragma once + +#include "services/plc_communication_gateway.h" + +#include + +#include + +namespace Ui { +class PlcConnectionDialog; +} + +class PlcConnectionDialog final : public QDialog +{ + Q_OBJECT + +public: + explicit PlcConnectionDialog( + const PlcSerialConfiguration &configuration, + QWidget *parent = nullptr); + ~PlcConnectionDialog() override; + + PlcSerialConfiguration configuration() const; + +private: + std::unique_ptr ui_; +}; diff --git a/app/src/ui/plc_connection_dialog.ui b/app/src/ui/plc_connection_dialog.ui new file mode 100644 index 0000000..0396983 --- /dev/null +++ b/app/src/ui/plc_connection_dialog.ui @@ -0,0 +1,85 @@ + + + PlcConnectionDialog + + + PLC 通信配置 + + + 4200 + + + + + Modbus RTU 串口 + + QFormLayout::AllNonFixedFieldsGrow + 端口 + + + true + 选择或输入 COM 端口 + + + PLC 站号 + 1247 + 波特率 + + + 96009600 + 1920019200 + 3840038400 + 5760057600 + 115200115200 + + + 数据位 + + + 77 + 88 + + + 校验方式 + + + 无校验0 + 奇校验3 + 偶校验2 + + + 停止位 + + + 11 + 22 + + + + + + + + 通信时序 + + 响应超时 + ms100300001001000 + 失败重试 + 102 + 轮询周期 + ms501000050200 + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + buttonBoxaccepted()PlcConnectionDialogaccept()320360210180 + buttonBoxrejected()PlcConnectionDialogreject()320360210180 + + diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 3433a25..1baf611 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -2,6 +2,7 @@ #include "domain/register_repository.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" +#include "services/data_point_service.h" #include "services/logic_editor_service.h" #include "services/offline_simulation_service.h" #include "services/project_service.h" @@ -9,12 +10,17 @@ #include "ui/hmi_editor_widget.h" #include "ui/logic_editor_widget.h" #include "ui/main_window.h" +#include "ui/plc_connection_dialog.h" #include #include +#include +#include #include #include #include +#include +#include #include #include @@ -60,6 +66,7 @@ void testModeActionsControlEditingAvailability() TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService editor_service(project_service); + DataPointService data_point_service(project_service); LogicEditorService logic_editor_service(project_service); VirtualRegisterRepository repository; HmiRuntimeService runtime_service(repository); @@ -69,6 +76,7 @@ void testModeActionsControlEditingAvailability() mode_service, project_service, editor_service, + data_point_service, logic_editor_service, runtime_service); window.resize(1000, 640); @@ -124,9 +132,11 @@ void testModeActionsControlEditingAvailability() logic_editor_service.firstLogicId()); require(logic != nullptr && logic->rungs.size() == 1, "logic actions must edit the default ladder rung"); - require(logic->rungs.front().stages.size() == 1 - && logic->rungs.front().stages.front().branches.size() == 2, - "parallel insert mode must add a branch to the selected stage"); + require(logic->rungs.front().condition.has_value() + && logic->rungs.front().condition->kind + == ConditionExpressionKind::Parallel + && logic->rungs.front().condition->children.size() == 2U, + "parallel insert mode must create a parallel expression"); require(logic->rungs.front().output.has_value(), "coil action must set the fixed ladder output"); @@ -134,11 +144,13 @@ void testModeActionsControlEditingAvailability() require(mode_service.mode() == ApplicationMode::Editing, "unconfigured ladder nodes must block offline running"); const LadderRung &rung = logic->rungs.front(); - for (const LogicNode &node : rung.stages.front().branches) + std::vector condition_nodes; + collectConditionNodes(*rung.condition, &condition_nodes); + for (const LogicNode *node : condition_nodes) { require(logic_editor_service.updateNodeConfig( logic_editor_service.firstLogicId(), - node.id, + node->id, ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen}) @@ -198,6 +210,70 @@ void testModeActionsControlEditingAvailability() "rejected online running must restore the editing action"); } +void testPlcConfigurationUsesDialog() +{ + PlcSerialConfiguration initial; + initial.portName = "COM17"; + initial.serverAddress = 12; + initial.baudRate = 38400; + initial.dataBits = 7; + initial.parity = 3; + initial.stopBits = 2; + initial.responseTimeoutMs = 2500; + initial.retries = 4; + initial.pollIntervalMs = 350; + PlcConnectionDialog configuration_dialog(initial); + const PlcSerialConfiguration actual = configuration_dialog.configuration(); + require(actual.portName == initial.portName, + "PLC dialog must preserve a manually entered serial port"); + require(actual.serverAddress == initial.serverAddress + && actual.baudRate == initial.baudRate + && actual.dataBits == initial.dataBits + && actual.parity == initial.parity + && actual.stopBits == initial.stopBits, + "PLC dialog must preserve Modbus RTU serial parameters"); + require(actual.responseTimeoutMs == initial.responseTimeoutMs + && actual.retries == initial.retries + && actual.pollIntervalMs == initial.pollIntervalMs, + "PLC dialog must preserve communication timing parameters"); + + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService editor_service(project_service); + DataPointService data_point_service(project_service); + LogicEditorService logic_editor_service(project_service); + VirtualRegisterRepository repository; + HmiRuntimeService runtime_service(repository); + OfflineSimulationService simulation_service(repository); + RuntimeModeService mode_service(project_service, simulation_service); + MainWindow window( + mode_service, + project_service, + editor_service, + data_point_service, + logic_editor_service, + runtime_service); + require(window.findChild(QStringLiteral("serialPortComboBox")) == nullptr, + "serial configuration controls must not be embedded in the main window"); + QAction *configure_action = requiredChild(window, "configurePlcAction"); + bool dialog_opened = false; + QTimer::singleShot( + 0, + [&dialog_opened] + { + auto *dialog = qobject_cast( + QApplication::activeModalWidget()); + dialog_opened = dialog != nullptr; + if (dialog != nullptr) + { + dialog->reject(); + } + }); + configure_action->trigger(); + require(dialog_opened, + "PLC configuration action must open the dedicated configuration dialog"); +} + } // namespace int main(int argc, char *argv[]) @@ -209,6 +285,7 @@ int main(int argc, char *argv[]) try { testModeActionsControlEditingAvailability(); + testPlcConfigurationUsesDialog(); } catch (const std::exception &error) { diff --git a/app/tests/main_window_tests.pro b/app/tests/main_window_tests.pro index 33f190b..33e3d9d 100644 --- a/app/tests/main_window_tests.pro +++ b/app/tests/main_window_tests.pro @@ -1,4 +1,4 @@ -QT += widgets +QT += widgets serialport TEMPLATE = app TARGET = main_window_tests @@ -11,15 +11,19 @@ INCLUDEPATH += ../src SOURCES += \ main_window_tests.cpp \ ../src/ui/main_window.cpp \ + ../src/ui/plc_connection_dialog.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/data_point_model.cpp \ ../src/domain/hmi_model.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/domain/runtime_state.cpp \ ../src/services/project_service.cpp \ + ../src/services/data_point_service.cpp \ ../src/services/hmi_editor_service.cpp \ ../src/services/logic_editor_service.cpp \ ../src/services/hmi_runtime_service.cpp \ @@ -29,16 +33,20 @@ SOURCES += \ HEADERS += \ ../src/ui/main_window.h \ + ../src/ui/plc_connection_dialog.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/data_point_model.h \ ../src/domain/hmi_model.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/project_storage.h \ ../src/domain/runtime_state.h \ ../src/services/project_service.h \ + ../src/services/data_point_service.h \ ../src/services/hmi_editor_service.h \ ../src/services/logic_editor_service.h \ ../src/services/hmi_runtime_service.h \ @@ -46,5 +54,9 @@ HEADERS += \ ../src/services/offline_simulation_service.h \ ../src/services/runtime_mode_service.h +HEADERS += \ + ../src/services/plc_communication_gateway.h + FORMS += \ - ../src/ui/main_window.ui + ../src/ui/main_window.ui \ + ../src/ui/plc_connection_dialog.ui