Ver código fonte

feat: 集成数据点与PLC主界面

main
suyu 1 mês atrás
pai
commit
ebe9dbd338
12 arquivos alterados com 976 adições e 400 exclusões
  1. +6
    -3
      app/integrated_platform.pro
  2. +15
    -1
      app/src/main.cpp
  3. +10
    -0
      app/src/services/hmi_runtime_service.cpp
  4. +2
    -0
      app/src/services/hmi_runtime_service.h
  5. +452
    -315
      app/src/ui/main_window.cpp
  6. +14
    -47
      app/src/ui/main_window.h
  7. +181
    -27
      app/src/ui/main_window.ui
  8. +88
    -0
      app/src/ui/plc_connection_dialog.cpp
  9. +27
    -0
      app/src/ui/plc_connection_dialog.h
  10. +85
    -0
      app/src/ui/plc_connection_dialog.ui
  11. +82
    -5
      app/tests/main_window_tests.cpp
  12. +14
    -2
      app/tests/main_window_tests.pro

+ 6
- 3
app/integrated_platform.pro Ver arquivo

@@ -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

+ 15
- 1
app/src/main.cpp Ver arquivo

@@ -9,7 +9,11 @@
#include <QApplication>

#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();


+ 10
- 0
app/src/services/hmi_runtime_service.cpp Ver arquivo

@@ -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 可理解的运行错误


+ 2
- 0
app/src/services/hmi_runtime_service.h Ver arquivo

@@ -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);


+ 452
- 315
app/src/ui/main_window.cpp
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 14
- 47
app/src/ui/main_window.h Ver arquivo

@@ -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 <QMainWindow>

@@ -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_;
};

+ 181
- 27
app/src/ui/main_window.ui Ver arquivo

@@ -221,7 +221,12 @@
<property name="title">
<string>文件(&amp;F)</string>
</property>
<addaction name="exitAction"/>
<addaction name="newProjectAction"/>
<addaction name="saveProjectAction"/>
<addaction name="saveAsProjectAction"/>
<addaction name="loadProjectAction"/>
<addaction name="separator"/>
<addaction name="exitAction"/>
</widget>
<widget class="QMenu" name="runMenu">
<property name="title">
@@ -229,7 +234,10 @@
</property>
<addaction name="editingModeAction"/>
<addaction name="offlineModeAction"/>
<addaction name="onlineModeAction"/>
<addaction name="onlineModeAction"/>
<addaction name="separator"/>
<addaction name="configurePlcAction"/>
<addaction name="disconnectPlcAction"/>
</widget>
<widget class="QMenu" name="viewMenu">
<property name="title">
@@ -260,7 +268,39 @@
<addaction name="editingModeAction"/>
<addaction name="offlineModeAction"/>
<addaction name="onlineModeAction"/>
</widget>
<addaction name="configurePlcAction"/>
<addaction name="disconnectPlcAction"/>
</widget>
<widget class="QToolBar" name="hmiToolBar">
<property name="windowTitle"><string>HMI 控件</string></property>
<property name="toolButtonStyle"><enum>Qt::ToolButtonTextBesideIcon</enum></property>
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute>
<attribute name="toolBarBreak"><bool>false</bool></attribute>
<addaction name="addButtonAction"/>
<addaction name="addIndicatorAction"/>
<addaction name="addNumericDisplayAction"/>
<addaction name="addNumericInputAction"/>
<addaction name="deleteControlAction"/>
</widget>
<widget class="QToolBar" name="logicToolBar">
<property name="windowTitle"><string>控制逻辑节点</string></property>
<property name="toolButtonStyle"><enum>Qt::ToolButtonTextBesideIcon</enum></property>
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute>
<attribute name="toolBarBreak"><bool>false</bool></attribute>
<addaction name="addRungAction"/>
<addaction name="appendInsertAction"/>
<addaction name="beforeInsertAction"/>
<addaction name="afterInsertAction"/>
<addaction name="parallelInsertAction"/>
<addaction name="separator"/>
<addaction name="addNormallyOpenAction"/>
<addaction name="addNormallyClosedAction"/>
<addaction name="addNormalCoilAction"/>
<addaction name="addSetCoilAction"/>
<addaction name="addResetCoilAction"/>
<addaction name="addCompareAction"/>
<addaction name="deleteLogicAction"/>
</widget>
<widget class="QDockWidget" name="projectDock">
<property name="minimumSize">
<size>
@@ -360,32 +400,82 @@
<height>164</height>
</rect>
</property>
<layout class="QFormLayout" name="propertiesForm">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="selectionCaptionLabel">
<property name="text">
<string>当前对象</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="selectionValueLabel">
<property name="text">
<string>未选择</string>
</property>
</widget>
</item>
</layout>
<layout class="QVBoxLayout" name="propertiesPageLayout">
<item>
<layout class="QFormLayout" name="selectionForm">
<item row="0" column="0"><widget class="QLabel" name="selectionCaptionLabel"><property name="text"><string>当前对象</string></property></widget></item>
<item row="0" column="1"><widget class="QLabel" name="selectionValueLabel"><property name="text"><string>未选择</string></property></widget></item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="propertyStack">
<widget class="QWidget" name="hmiPropertiesPage">
<layout class="QFormLayout" name="hmiPropertiesForm">
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
<item row="0" column="0"><widget class="QLabel" name="controlIdLabel"><property name="text"><string>控件 ID</string></property></widget></item>
<item row="0" column="1"><widget class="QLineEdit" name="controlIdEdit"/></item>
<item row="1" column="0"><widget class="QLabel" name="controlTextLabel"><property name="text"><string>显示文本</string></property></widget></item>
<item row="1" column="1"><widget class="QLineEdit" name="controlTextEdit"/></item>
<item row="2" column="0"><widget class="QLabel" name="controlXLabel"><property name="text"><string>X</string></property></widget></item>
<item row="2" column="1"><widget class="QSpinBox" name="controlXSpinBox"><property name="maximum"><number>4000</number></property></widget></item>
<item row="3" column="0"><widget class="QLabel" name="controlYLabel"><property name="text"><string>Y</string></property></widget></item>
<item row="3" column="1"><widget class="QSpinBox" name="controlYSpinBox"><property name="maximum"><number>4000</number></property></widget></item>
<item row="4" column="0"><widget class="QLabel" name="controlWidthLabel"><property name="text"><string>宽度</string></property></widget></item>
<item row="4" column="1"><widget class="QSpinBox" name="controlWidthSpinBox"><property name="minimum"><number>1</number></property><property name="maximum"><number>4000</number></property></widget></item>
<item row="5" column="0"><widget class="QLabel" name="controlHeightLabel"><property name="text"><string>高度</string></property></widget></item>
<item row="5" column="1"><widget class="QSpinBox" name="controlHeightSpinBox"><property name="minimum"><number>1</number></property><property name="maximum"><number>4000</number></property></widget></item>
<item row="6" column="0"><widget class="QLabel" name="bindingAreaLabel"><property name="text"><string>绑定区域</string></property></widget></item>
<item row="6" column="1"><widget class="QComboBox" name="bindingAreaComboBox">
<item><property name="text"><string>未绑定</string></property><property name="userData"><string notr="true">-1</string></property></item>
<item><property name="text"><string>M</string></property><property name="userData"><string notr="true">0</string></property></item>
<item><property name="text"><string>D</string></property><property name="userData"><string notr="true">1</string></property></item>
</widget></item>
<item row="7" column="0"><widget class="QLabel" name="bindingIndexLabel"><property name="text"><string>绑定地址</string></property></widget></item>
<item row="7" column="1"><widget class="QSpinBox" name="bindingIndexSpinBox"><property name="maximum"><number>4000</number></property></widget></item>
<item row="8" column="0"><widget class="QLabel" name="hmiDataPointLabel"><property name="text"><string>选择数据点</string></property></widget></item>
<item row="8" column="1"><widget class="QComboBox" name="hmiDataPointComboBox"/></item>
<item row="9" column="0" colspan="2"><widget class="QPushButton" name="applyPropertiesButton"><property name="text"><string>应用属性</string></property></widget></item>
</layout>
</widget>
<widget class="QWidget" name="logicPropertiesPage">
<layout class="QFormLayout" name="logicPropertiesForm">
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
<item row="0" column="0"><widget class="QLabel" name="logicNodeIdLabel"><property name="text"><string>节点 ID</string></property></widget></item>
<item row="0" column="1"><widget class="QLineEdit" name="logicNodeIdEdit"><property name="readOnly"><bool>true</bool></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="logicNodeTypeCaption"><property name="text"><string>节点类型</string></property></widget></item>
<item row="1" column="1"><widget class="QLabel" name="logicNodeTypeLabel"><property name="text"><string/></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="logicAddressAreaCaption"><property name="text"><string>地址区域</string></property></widget></item>
<item row="2" column="1"><widget class="QLabel" name="logicAddressAreaLabel"><property name="text"><string/></property></widget></item>
<item row="3" column="0"><widget class="QLabel" name="logicAddressLabel"><property name="text"><string>地址</string></property></widget></item>
<item row="3" column="1"><widget class="QSpinBox" name="logicAddressSpinBox"><property name="maximum"><number>4000</number></property></widget></item>
<item row="4" column="0"><widget class="QLabel" name="logicDataPointLabel"><property name="text"><string>选择数据点</string></property></widget></item>
<item row="4" column="1"><widget class="QComboBox" name="logicDataPointComboBox"/></item>
<item row="5" column="0"><widget class="QLabel" name="logicModeLabel"><property name="text"><string>工作方式</string></property></widget></item>
<item row="5" column="1"><widget class="QComboBox" name="logicModeComboBox"/></item>
<item row="6" column="0"><widget class="QLabel" name="logicComparisonLabel"><property name="text"><string>比较运算</string></property></widget></item>
<item row="6" column="1"><widget class="QComboBox" name="logicComparisonComboBox">
<item><property name="text"><string>等于</string></property><property name="userData"><string notr="true">0</string></property></item>
<item><property name="text"><string>不等于</string></property><property name="userData"><string notr="true">1</string></property></item>
<item><property name="text"><string>小于</string></property><property name="userData"><string notr="true">2</string></property></item>
<item><property name="text"><string>小于等于</string></property><property name="userData"><string notr="true">3</string></property></item>
<item><property name="text"><string>大于</string></property><property name="userData"><string notr="true">4</string></property></item>
<item><property name="text"><string>大于等于</string></property><property name="userData"><string notr="true">5</string></property></item>
</widget></item>
<item row="7" column="0"><widget class="QLabel" name="logicValueLabel"><property name="text"><string>比较常量</string></property></widget></item>
<item row="7" column="1"><widget class="QSpinBox" name="logicValueSpinBox"><property name="minimum"><number>-32768</number></property><property name="maximum"><number>32767</number></property></widget></item>
<item row="8" column="0" colspan="2"><widget class="QPushButton" name="applyLogicPropertiesButton"><property name="text"><string>应用属性</string></property></widget></item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="outputDock">
<widget class="QDockWidget" name="outputDock">
<property name="minimumSize">
<size>
<width>300</width>
@@ -422,13 +512,77 @@
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
<action name="exitAction">
<widget class="QDockWidget" name="dataPointDock">
<property name="minimumSize"><size><width>420</width><height>170</height></size></property>
<property name="windowTitle"><string>数据点</string></property>
<attribute name="dockWidgetArea"><number>8</number></attribute>
<widget class="QWidget" name="dataPointDockContents">
<layout class="QVBoxLayout" name="dataPointDockLayout">
<property name="leftMargin"><number>6</number></property>
<property name="topMargin"><number>6</number></property>
<property name="rightMargin"><number>6</number></property>
<property name="bottomMargin"><number>6</number></property>
<item>
<widget class="QTableWidget" name="dataPointTable">
<property name="selectionMode"><enum>QAbstractItemView::SingleSelection</enum></property>
<property name="selectionBehavior"><enum>QAbstractItemView::SelectRows</enum></property>
<property name="editTriggers"><set>QAbstractItemView::NoEditTriggers</set></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>
<column><property name="text"><string>当前值</string></property></column>
<column><property name="text"><string>引用位置</string></property></column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="dataPointEditorLayout">
<item><widget class="QLabel" name="dataPointAreaLabel"><property name="text"><string>区域</string></property></widget></item>
<item><widget class="QComboBox" name="dataPointAreaComboBox"><item><property name="text"><string>M</string></property><property name="userData"><string notr="true">0</string></property></item><item><property name="text"><string>D</string></property><property name="userData"><string notr="true">1</string></property></item></widget></item>
<item><widget class="QLabel" name="dataPointAddressLabel"><property name="text"><string>地址</string></property></widget></item>
<item><widget class="QSpinBox" name="dataPointAddressSpinBox"><property name="maximum"><number>4000</number></property></widget></item>
<item><widget class="QLabel" name="dataPointNameLabel"><property name="text"><string>名称</string></property></widget></item>
<item><widget class="QLineEdit" name="dataPointNameEdit"/></item>
<item><widget class="QLabel" name="dataPointCommentLabel"><property name="text"><string>注释</string></property></widget></item>
<item><widget class="QLineEdit" name="dataPointCommentEdit"/></item>
<item><widget class="QPushButton" name="addDataPointButton"><property name="text"><string>新增</string></property></widget></item>
<item><widget class="QPushButton" name="updateDataPointButton"><property name="text"><string>更新</string></property></widget></item>
<item><widget class="QPushButton" name="deleteDataPointButton"><property name="text"><string>删除</string></property></widget></item>
</layout>
</item>
</layout>
</widget>
</widget>
<action name="exitAction">
<property name="text">
<string>退出(&amp;X)</string>
</property>
</action>
</action>
<action name="newProjectAction"><property name="text"><string>新建工程</string></property></action>
<action name="saveProjectAction"><property name="text"><string>保存工程</string></property></action>
<action name="saveAsProjectAction"><property name="text"><string>工程另存为</string></property></action>
<action name="loadProjectAction"><property name="text"><string>加载工程</string></property></action>
<action name="configurePlcAction"><property name="text"><string>PLC 配置</string></property><property name="toolTip"><string>配置参数并连接真实 PLC</string></property></action>
<action name="disconnectPlcAction"><property name="text"><string>断开 PLC</string></property><property name="toolTip"><string>断开当前 PLC 连接</string></property></action>
<action name="addButtonAction"><property name="text"><string>按钮</string></property><property name="toolTip"><string>添加按钮控件</string></property></action>
<action name="addIndicatorAction"><property name="text"><string>指示灯</string></property><property name="toolTip"><string>添加指示灯控件</string></property></action>
<action name="addNumericDisplayAction"><property name="text"><string>数值显示</string></property><property name="toolTip"><string>添加数值显示控件</string></property></action>
<action name="addNumericInputAction"><property name="text"><string>数值输入</string></property><property name="toolTip"><string>添加数值输入控件</string></property></action>
<action name="deleteControlAction"><property name="text"><string>删除</string></property><property name="toolTip"><string>删除当前控件</string></property></action>
<action name="addRungAction"><property name="text"><string>新建网络</string></property><property name="toolTip"><string>在梯形图末尾新增网络</string></property></action>
<action name="appendInsertAction"><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property><property name="text"><string>末尾追加</string></property><property name="toolTip"><string>将下一个条件追加到当前网络末尾</string></property></action>
<action name="beforeInsertAction"><property name="checkable"><bool>true</bool></property><property name="text"><string>前插</string></property><property name="toolTip"><string>将下一个条件串联到选中节点或支路之前</string></property></action>
<action name="afterInsertAction"><property name="checkable"><bool>true</bool></property><property name="text"><string>后插</string></property><property name="toolTip"><string>将下一个条件串联到选中节点或支路之后</string></property></action>
<action name="parallelInsertAction"><property name="checkable"><bool>true</bool></property><property name="text"><string>并联插入</string></property><property name="toolTip"><string>将下一个条件并联到选中节点或整条支路</string></property></action>
<action name="addNormallyOpenAction"><property name="text"><string>常开</string></property><property name="toolTip"><string>向当前网络添加常开触点</string></property></action>
<action name="addNormallyClosedAction"><property name="text"><string>常闭</string></property><property name="toolTip"><string>向当前网络添加常闭触点</string></property></action>
<action name="addNormalCoilAction"><property name="text"><string>线圈</string></property><property name="toolTip"><string>设置当前网络的普通线圈</string></property></action>
<action name="addSetCoilAction"><property name="text"><string>置位</string></property><property name="toolTip"><string>设置当前网络的置位线圈</string></property></action>
<action name="addResetCoilAction"><property name="text"><string>复位</string></property><property name="toolTip"><string>设置当前网络的复位线圈</string></property></action>
<action name="addCompareAction"><property name="text"><string>D 比较</string></property><property name="toolTip"><string>向当前网络添加 D 值与常量比较</string></property></action>
<action name="deleteLogicAction"><property name="text"><string>删除</string></property><property name="toolTip"><string>删除选中的逻辑节点或网络</string></property></action>
<action name="editingModeAction">
<property name="checkable">
<bool>true</bool>
@@ -468,4 +622,4 @@
</widget>
<resources/>
<connections/>
</ui>
</ui>

+ 88
- 0
app/src/ui/plc_connection_dialog.cpp Ver arquivo

@@ -0,0 +1,88 @@
#include "plc_connection_dialog.h"

#include "ui_plc_connection_dialog.h"

#include <QComboBox>
#include <QSerialPortInfo>

#include <initializer_list>

namespace {

void setItemData(QComboBox *combo_box, const std::initializer_list<int> &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::PlcConnectionDialog>())
{
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<std::size_t>(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;
}

+ 27
- 0
app/src/ui/plc_connection_dialog.h Ver arquivo

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

#include "services/plc_communication_gateway.h"

#include <QDialog>

#include <memory>

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::PlcConnectionDialog> ui_;
};

+ 85
- 0
app/src/ui/plc_connection_dialog.ui Ver arquivo

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlcConnectionDialog</class>
<widget class="QDialog" name="PlcConnectionDialog">
<property name="windowTitle">
<string>PLC 通信配置</string>
</property>
<property name="minimumSize">
<size><width>420</width><height>0</height></size>
</property>
<layout class="QVBoxLayout" name="dialogLayout">
<item>
<widget class="QGroupBox" name="serialGroupBox">
<property name="title"><string>Modbus RTU 串口</string></property>
<layout class="QFormLayout" name="serialFormLayout">
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
<item row="0" column="0"><widget class="QLabel" name="serialPortLabel"><property name="text"><string>端口</string></property></widget></item>
<item row="0" column="1">
<widget class="QComboBox" name="serialPortComboBox">
<property name="editable"><bool>true</bool></property>
<property name="placeholderText"><string>选择或输入 COM 端口</string></property>
</widget>
</item>
<item row="1" column="0"><widget class="QLabel" name="serverAddressLabel"><property name="text"><string>PLC 站号</string></property></widget></item>
<item row="1" column="1"><widget class="QSpinBox" name="serverAddressSpinBox"><property name="minimum"><number>1</number></property><property name="maximum"><number>247</number></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="baudRateLabel"><property name="text"><string>波特率</string></property></widget></item>
<item row="2" column="1">
<widget class="QComboBox" name="baudRateComboBox">
<item><property name="text"><string>9600</string></property><property name="userData"><string notr="true">9600</string></property></item>
<item><property name="text"><string>19200</string></property><property name="userData"><string notr="true">19200</string></property></item>
<item><property name="text"><string>38400</string></property><property name="userData"><string notr="true">38400</string></property></item>
<item><property name="text"><string>57600</string></property><property name="userData"><string notr="true">57600</string></property></item>
<item><property name="text"><string>115200</string></property><property name="userData"><string notr="true">115200</string></property></item>
</widget>
</item>
<item row="3" column="0"><widget class="QLabel" name="dataBitsLabel"><property name="text"><string>数据位</string></property></widget></item>
<item row="3" column="1">
<widget class="QComboBox" name="dataBitsComboBox">
<item><property name="text"><string>7</string></property><property name="userData"><string notr="true">7</string></property></item>
<item><property name="text"><string>8</string></property><property name="userData"><string notr="true">8</string></property></item>
</widget>
</item>
<item row="4" column="0"><widget class="QLabel" name="parityLabel"><property name="text"><string>校验方式</string></property></widget></item>
<item row="4" column="1">
<widget class="QComboBox" name="parityComboBox">
<item><property name="text"><string>无校验</string></property><property name="userData"><string notr="true">0</string></property></item>
<item><property name="text"><string>奇校验</string></property><property name="userData"><string notr="true">3</string></property></item>
<item><property name="text"><string>偶校验</string></property><property name="userData"><string notr="true">2</string></property></item>
</widget>
</item>
<item row="5" column="0"><widget class="QLabel" name="stopBitsLabel"><property name="text"><string>停止位</string></property></widget></item>
<item row="5" column="1">
<widget class="QComboBox" name="stopBitsComboBox">
<item><property name="text"><string>1</string></property><property name="userData"><string notr="true">1</string></property></item>
<item><property name="text"><string>2</string></property><property name="userData"><string notr="true">2</string></property></item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="timingGroupBox">
<property name="title"><string>通信时序</string></property>
<layout class="QFormLayout" name="timingFormLayout">
<item row="0" column="0"><widget class="QLabel" name="responseTimeoutLabel"><property name="text"><string>响应超时</string></property></widget></item>
<item row="0" column="1"><widget class="QSpinBox" name="responseTimeoutSpinBox"><property name="suffix"><string> ms</string></property><property name="minimum"><number>100</number></property><property name="maximum"><number>30000</number></property><property name="singleStep"><number>100</number></property><property name="value"><number>1000</number></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="retriesLabel"><property name="text"><string>失败重试</string></property></widget></item>
<item row="1" column="1"><widget class="QSpinBox" name="retriesSpinBox"><property name="suffix"><string> 次</string></property><property name="maximum"><number>10</number></property><property name="value"><number>2</number></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="pollIntervalLabel"><property name="text"><string>轮询周期</string></property></widget></item>
<item row="2" column="1"><widget class="QSpinBox" name="pollIntervalSpinBox"><property name="suffix"><string> ms</string></property><property name="minimum"><number>50</number></property><property name="maximum"><number>10000</number></property><property name="singleStep"><number>50</number></property><property name="value"><number>200</number></property></widget></item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons"><set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set></property>
</widget>
</item>
</layout>
</widget>
<connections>
<connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>PlcConnectionDialog</receiver><slot>accept()</slot><hints><hint type="sourcelabel"><x>320</x><y>360</y></hint><hint type="destinationlabel"><x>210</x><y>180</y></hint></hints></connection>
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>PlcConnectionDialog</receiver><slot>reject()</slot><hints><hint type="sourcelabel"><x>320</x><y>360</y></hint><hint type="destinationlabel"><x>210</x><y>180</y></hint></hints></connection>
</connections>
</ui>

+ 82
- 5
app/tests/main_window_tests.cpp Ver arquivo

@@ -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 <QAction>
#include <QApplication>
#include <QComboBox>
#include <QDialog>
#include <QDockWidget>
#include <QLabel>
#include <QLineEdit>
#include <QSpinBox>
#include <QTimer>

#include <iostream>
#include <stdexcept>
@@ -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<const LogicNode *> 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<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr,
"serial configuration controls must not be embedded in the main window");
QAction *configure_action = requiredChild<QAction>(window, "configurePlcAction");
bool dialog_opened = false;
QTimer::singleShot(
0,
[&dialog_opened]
{
auto *dialog = qobject_cast<PlcConnectionDialog *>(
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)
{


+ 14
- 2
app/tests/main_window_tests.pro Ver arquivo

@@ -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

Carregando…
Cancelar
Salvar