diff --git a/app/src/domain/hmi_model.h b/app/src/domain/hmi_model.h index 54feff3..46fb923 100644 --- a/app/src/domain/hmi_model.h +++ b/app/src/domain/hmi_model.h @@ -44,6 +44,17 @@ enum class HmiControlType Label // 标签 }; +/** + * @brief 按钮对绑定 M 位执行的操作 + */ +enum class HmiButtonOperation +{ + SetOn, // 按下时写入 1 + SetOff, // 按下时写入 0 + Toggle, // 按下时写入当前读回值的反值 + MomentaryOn // 按下时写入 1,释放时写入 0 +}; + /** * @brief 描述一个可保存的 HMI 控件及其显示和寄存器配置 * @@ -57,6 +68,7 @@ struct HmiControl std::string text; std::optional binding; std::map properties; + HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; /** * @brief 校验控件的标识、尺寸、扩展属性和寄存器绑定 diff --git a/app/src/infrastructure/json_project_storage.cpp b/app/src/infrastructure/json_project_storage.cpp index f3460a1..ce3447c 100644 --- a/app/src/infrastructure/json_project_storage.cpp +++ b/app/src/infrastructure/json_project_storage.cpp @@ -318,6 +318,58 @@ bool parseHmiControlType( return true; } +QString hmiButtonOperationName(HmiButtonOperation operation) +{ + switch (operation) + { + case HmiButtonOperation::SetOn: + { + return QStringLiteral("setOn"); + } + case HmiButtonOperation::SetOff: + { + return QStringLiteral("setOff"); + } + case HmiButtonOperation::Toggle: + { + return QStringLiteral("toggle"); + } + case HmiButtonOperation::MomentaryOn: + default: + { + return QStringLiteral("momentaryOn"); + } + } +} + +bool parseHmiButtonOperation( + const std::string &value, HmiButtonOperation *operation, ParseState *state) +{ + if (value == "setOn") + { + *operation = HmiButtonOperation::SetOn; + } + else if (value == "setOff") + { + *operation = HmiButtonOperation::SetOff; + } + else if (value == "toggle") + { + *operation = HmiButtonOperation::Toggle; + } + else if (value == "momentaryOn") + { + *operation = HmiButtonOperation::MomentaryOn; + } + else + { + return state->fail( + ProjectStorageError::InvalidField, + "不支持的 HMI 按钮操作:" + value); + } + return true; +} + // 将 HMI 控件矩形区域序列化为 JSON 对象 QJsonObject serializeBounds(const HmiRect &bounds) { @@ -418,6 +470,12 @@ QJsonObject serializeHmiControl(const HmiControl &control) object.insert(QStringLiteral("binding"), QJsonValue::Null); } object.insert(QStringLiteral("properties"), serializeProperties(control.properties)); + if (control.type == HmiControlType::Button) + { + object.insert( + QStringLiteral("buttonOperation"), + hmiButtonOperationName(control.buttonOperation)); + } return object; } @@ -447,6 +505,21 @@ bool parseHmiControl( { return false; } + if (control->type == HmiControlType::Button) + { + std::string operation_text; + if (!readString( + object, + "buttonOperation", + context, + &operation_text, + state) + || !parseHmiButtonOperation( + operation_text, &control->buttonOperation, state)) + { + return false; + } + } // binding 允许为 null,其余非空值必须是合法的寄存器地址对象 if (binding.isNull()) diff --git a/app/src/services/hmi_runtime_service.cpp b/app/src/services/hmi_runtime_service.cpp index fd726f7..291b3ab 100644 --- a/app/src/services/hmi_runtime_service.cpp +++ b/app/src/services/hmi_runtime_service.cpp @@ -67,20 +67,66 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c return readFailure(HmiRuntimeError::UnsupportedControl); } -HmiRuntimeWriteResult HmiRuntimeService::toggleButton(const HmiControl &control) +HmiRuntimeWriteResult HmiRuntimeService::operateButton( + const HmiControl &control, HmiButtonEvent event) { if (control.type != HmiControlType::Button) { return writeFailure(HmiRuntimeError::UnsupportedControl); } - const HmiRuntimeReadResult current = readControl(control); - if (!current.succeeded) + if (!control.binding.has_value()) + { + return writeFailure(HmiRuntimeError::MissingBinding); + } + if (!control.binding->isValid() || control.binding->area() != RegisterArea::M) + { + return writeFailure(HmiRuntimeError::InvalidBinding); + } + + bool value = false; + switch (control.buttonOperation) + { + case HmiButtonOperation::SetOn: + { + if (event == HmiButtonEvent::Released) + { + return {true, HmiRuntimeError::None}; + } + value = true; + break; + } + case HmiButtonOperation::SetOff: { - return writeFailure(current.error); + if (event == HmiButtonEvent::Released) + { + return {true, HmiRuntimeError::None}; + } + value = false; + break; } - // 先读后取反实现按钮点击切换 M 位语义 - const RegisterWriteResult result = repository_.writeBit( - *control.binding, !current.bit_value); + case HmiButtonOperation::Toggle: + { + if (event == HmiButtonEvent::Released) + { + return {true, HmiRuntimeError::None}; + } + const HmiRuntimeReadResult current = readControl(control); + if (!current.succeeded) + { + return writeFailure(current.error); + } + value = !current.bit_value; + break; + } + case HmiButtonOperation::MomentaryOn: + default: + { + value = event == HmiButtonEvent::Pressed; + break; + } + } + + const RegisterWriteResult result = repository_.writeBit(*control.binding, value); return result.succeeded ? HmiRuntimeWriteResult{true, HmiRuntimeError::None} : writeFailure(repositoryError(result.error)); diff --git a/app/src/services/hmi_runtime_service.h b/app/src/services/hmi_runtime_service.h index 139a640..e6c95e6 100644 --- a/app/src/services/hmi_runtime_service.h +++ b/app/src/services/hmi_runtime_service.h @@ -46,6 +46,15 @@ struct HmiRuntimeWriteResult HmiRuntimeError error = HmiRuntimeError::None; }; +/** + * @brief HMI 按钮在运行态产生的输入事件 + */ +enum class HmiButtonEvent +{ + Pressed, + Released +}; + /** * @brief 仅通过统一寄存器仓库执行 HMI 运行态读写 * @@ -65,11 +74,13 @@ public: */ HmiRuntimeReadResult readControl(const HmiControl &control) const; /** - * @brief 切换按钮当前绑定 M 位的值 + * @brief 根据按钮配置处理按下或释放事件 * @param control 按钮控件,必须绑定有效 M 地址 - * @return 非按钮、绑定无效或仓库拒绝写入时返回失败结果 + * @param event 当前输入事件 + * @return 无需写入的事件返回成功,配置无效或仓库拒绝写入时返回失败 */ - HmiRuntimeWriteResult toggleButton(const HmiControl &control); + HmiRuntimeWriteResult operateButton( + const HmiControl &control, HmiButtonEvent event); /** * @brief 将带符号 16 位数值写入数值输入控件绑定的 D 地址 * @param control 数值输入控件,必须绑定有效 D 地址 diff --git a/app/src/ui/hmi_editor_widget.cpp b/app/src/ui/hmi_editor_widget.cpp index 728b98f..3011dbb 100644 --- a/app/src/ui/hmi_editor_widget.cpp +++ b/app/src/ui/hmi_editor_widget.cpp @@ -30,12 +30,14 @@ public: int page_width, int page_height, std::function moved, - std::function activated) + std::function button_event, + std::function numeric_input_activated) : control_(control), page_width_(page_width), page_height_(page_height), moved_(std::move(moved)), - activated_(std::move(activated)) + button_event_(std::move(button_event)), + numeric_input_activated_(std::move(numeric_input_activated)) { // 领域坐标直接作为图元在场景中的初始位置 setPos(control_.bounds.x, control_.bounds.y); @@ -194,16 +196,15 @@ protected: return QGraphicsItem::itemChange(change, value); } - // 运行态点击按钮时通知外层执行 M 位切换 + // 运行态按钮按下时通知外层执行配置的 M 位操作 void mousePressEvent(QGraphicsSceneMouseEvent *event) override { - // 条件:!ItemIsMovable 【也就是运行模式】 + 是按钮 + 有回调 if (!flags().testFlag(ItemIsMovable) - && control_.type == HmiControlType::Button && activated_) + && control_.type == HmiControlType::Button && button_event_) { - // 运行态单击按钮才触发 M 位切换,编辑态只用于选择和拖动 setSelected(true); - activated_(control_.id); + button_pressed_ = true; + button_event_(control_.id, HmiButtonEvent::Pressed); event->accept(); return; } @@ -215,11 +216,12 @@ protected: void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override { if (!flags().testFlag(ItemIsMovable) - && control_.type == HmiControlType::NumericInput && activated_) + && control_.type == HmiControlType::NumericInput + && numeric_input_activated_) { // 数值输入使用双击,避免普通选择操作意外写入 D 字 setSelected(true); - activated_(control_.id); + numeric_input_activated_(control_.id); event->accept(); return; } @@ -229,6 +231,14 @@ protected: // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override { + if (!flags().testFlag(ItemIsMovable) + && control_.type == HmiControlType::Button && button_pressed_) + { + button_pressed_ = false; + button_event_(control_.id, HmiButtonEvent::Released); + event->accept(); + return; + } QGraphicsItem::mouseReleaseEvent(event); // 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层 if (flags().testFlag(ItemIsMovable) && moved_) @@ -290,8 +300,9 @@ private: int page_height_ = 0; // 拖拽完成后回调 HmiEditorWidget 提交控件新位置 std::function moved_; - // 运行模式点击按钮 / 双击输入框回调,通知外层做寄存器读写 - std::function activated_; + std::function button_event_; + std::function numeric_input_activated_; + bool button_pressed_ = false; // 指示灯读取到的 M 位值 bool bit_value_ = false; // 数值控件读取到的 D 字值 @@ -395,9 +406,13 @@ void HmiEditorWidget::reloadPage() { handleControlMoved(control_id, position); }, + [this](const std::string &control_id, HmiButtonEvent event) + { + handleButtonEvent(control_id, event); + }, [this](const std::string &control_id) { - handleControlActivated(control_id); + handleNumericInputActivated(control_id); }); scene_->addItem(item); item->setInteractionEnabled(editing_enabled_); @@ -518,7 +533,8 @@ void HmiEditorWidget::handleControlMoved( emit controlChanged(QString::fromStdString(control_id)); } -void HmiEditorWidget::handleControlActivated(const std::string &control_id) +void HmiEditorWidget::handleButtonEvent( + const std::string &control_id, HmiButtonEvent event) { if (!runtime_active_ || !runtime_write_enabled_) { @@ -529,37 +545,48 @@ void HmiEditorWidget::handleControlActivated(const std::string &control_id) { return; } - HmiRuntimeWriteResult result; - // 只有可写控件允许激活,其余控件只展示最新运行值 - if (control->type == HmiControlType::Button) - { - result = runtime_service_.toggleButton(*control); - } - else if (control->type == HmiControlType::NumericInput) - { - bool accepted = false; - const HmiRuntimeReadResult current = runtime_service_.readControl(*control); - const int initial = current.succeeded ? current.word_value : 0; - const int value = QInputDialog::getInt( - this, - tr("输入数值"), - QString::fromUtf8(control->text.data(), static_cast(control->text.size())), - initial, - std::numeric_limits::min(), - std::numeric_limits::max(), - 1, - &accepted); - if (!accepted) - { - return; - } - result = runtime_service_.writeNumericInput( - *control, static_cast(value)); + if (control->type != HmiControlType::Button) + { + return; + } + const HmiRuntimeWriteResult result = runtime_service_.operateButton(*control, event); + if (!result.succeeded) + { + emit editorError(tr("寄存器操作失败")); + return; + } + refreshRuntimeValues(); +} + +void HmiEditorWidget::handleNumericInputActivated(const std::string &control_id) +{ + if (!runtime_active_ || !runtime_write_enabled_) + { + return; + } + const HmiControl *control = editor_service_.findControl(page_id_, control_id); + if (control == nullptr || control->type != HmiControlType::NumericInput) + { + return; } - else + bool accepted = false; + const HmiRuntimeReadResult current = runtime_service_.readControl(*control); + const int initial = current.succeeded ? current.word_value : 0; + const int value = QInputDialog::getInt( + this, + tr("输入数值"), + QString::fromUtf8(control->text.data(), static_cast(control->text.size())), + initial, + std::numeric_limits::min(), + std::numeric_limits::max(), + 1, + &accepted); + if (!accepted) { return; } + const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( + *control, static_cast(value)); if (!result.succeeded) { emit editorError(tr("寄存器操作失败")); diff --git a/app/src/ui/hmi_editor_widget.h b/app/src/ui/hmi_editor_widget.h index 1ded0c1..d123c61 100644 --- a/app/src/ui/hmi_editor_widget.h +++ b/app/src/ui/hmi_editor_widget.h @@ -15,6 +15,7 @@ class HmiEditorService; class HmiRuntimeService; class QGraphicsScene; class QResizeEvent; +enum class HmiButtonEvent; /** * @brief 将 HMI 页面模型投影为可选择和拖动的图形编辑画布 @@ -102,8 +103,11 @@ private: void handleSelectionChanged(); // 将图元拖动后的坐标提交给 HMI 编辑服务 void handleControlMoved(const std::string &control_id, const QPointF &position); - // 在运行态处理按钮点击或数值输入激活 - void handleControlActivated(const std::string &control_id); + // 在运行态处理按钮按下和释放 + void handleButtonEvent( + const std::string &control_id, HmiButtonEvent event); + // 在运行态处理数值输入激活 + void handleNumericInputActivated(const std::string &control_id); // 提供控件查找、移动和属性更新能力,不直接操作 Qt 图元数据 HmiEditorService &editor_service_; diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 253bc12..497cd37 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -541,6 +541,14 @@ void MainWindow::configurePropertyEditor() ui_->bindingAreaComboBox->setItemData(0, -1); ui_->bindingAreaComboBox->setItemData(1, 0); ui_->bindingAreaComboBox->setItemData(2, 1); + ui_->buttonOperationComboBox->setItemData( + 0, static_cast(HmiButtonOperation::SetOn)); + ui_->buttonOperationComboBox->setItemData( + 1, static_cast(HmiButtonOperation::SetOff)); + ui_->buttonOperationComboBox->setItemData( + 2, static_cast(HmiButtonOperation::Toggle)); + ui_->buttonOperationComboBox->setItemData( + 3, static_cast(HmiButtonOperation::MomentaryOn)); for (int index = 0; index < ui_->logicComparisonComboBox->count(); ++index) { ui_->logicComparisonComboBox->setItemData(index, index); @@ -674,6 +682,7 @@ void MainWindow::showControlProperties(const std::string &control_id) static_cast(ui_->controlHeightSpinBox), static_cast(ui_->bindingAreaComboBox), static_cast(ui_->bindingIndexSpinBox), + static_cast(ui_->buttonOperationComboBox), static_cast(ui_->applyPropertiesButton)}) { widget->setEnabled(has_control); @@ -682,6 +691,8 @@ void MainWindow::showControlProperties(const std::string &control_id) // 未选中有效控件,无需填充属性,直接退出 if (!has_control) { + ui_->buttonOperationLabel->setVisible(false); + ui_->buttonOperationComboBox->setVisible(false); return; } @@ -701,6 +712,13 @@ void MainWindow::showControlProperties(const std::string &control_id) : control->binding->area() == RegisterArea::M ? 1 : 2); ui_->bindingIndexSpinBox->setValue( control->binding.has_value() ? control->binding->index() : 0); + const bool is_button = control->type == HmiControlType::Button; + ui_->buttonOperationLabel->setVisible(is_button); + ui_->buttonOperationComboBox->setVisible(is_button); + ui_->buttonOperationComboBox->setEnabled(is_button); + ui_->buttonOperationComboBox->setCurrentIndex( + ui_->buttonOperationComboBox->findData( + static_cast(control->buttonOperation))); } void MainWindow::showLogicNodeProperties(const std::string &node_id) @@ -849,6 +867,11 @@ void MainWindow::applySelectedControlProperties() const RegisterArea area = binding_area == 0 ? RegisterArea::M : RegisterArea::D; control.binding = RegisterAddress{area, ui_->bindingIndexSpinBox->value()}; } + if (control.type == HmiControlType::Button) + { + control.buttonOperation = static_cast( + ui_->buttonOperationComboBox->currentData().toInt()); + } const HmiEditorResult result = hmi_editor_service_.updateControl( hmi_editor_service_.firstPageId(), selected_control_id_, control); if (!result.succeeded) diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index 5e6482e..dc396e1 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -575,8 +575,39 @@ - - + + + + 按钮操作 + + + + + + + + 置 ON + + + + + 置 OFF + + + + + 取反 + + + + + 瞬时 ON + + + + + + 应用属性 diff --git a/app/tests/hmi_editor_service_tests.cpp b/app/tests/hmi_editor_service_tests.cpp index ea971b4..af84044 100644 --- a/app/tests/hmi_editor_service_tests.cpp +++ b/app/tests/hmi_editor_service_tests.cpp @@ -91,10 +91,38 @@ void testRuntimeUsesRegisterRepository() button.id = "start"; button.type = HmiControlType::Button; button.binding = RegisterAddress{RegisterArea::M, 12}; - require(runtime_service.toggleButton(button).succeeded, - "button runtime writes must be accepted by the repository"); + require(button.buttonOperation == HmiButtonOperation::MomentaryOn, + "new buttons must default to momentary ON"); + require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, + "momentary button press must be accepted by the repository"); require(repository.readBit(*button.binding).value, - "button runtime writes must reach the M repository value"); + "momentary button press must write ON"); + require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded, + "momentary button release must be accepted by the repository"); + require(!repository.readBit(*button.binding).value, + "momentary button release must write OFF"); + + button.buttonOperation = HmiButtonOperation::SetOn; + require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, + "set ON button press must succeed"); + require(repository.readBit(*button.binding).value, + "set ON button press must write ON"); + require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded, + "set ON button release must be ignored successfully"); + require(repository.readBit(*button.binding).value, + "set ON button release must keep the bit ON"); + + button.buttonOperation = HmiButtonOperation::SetOff; + require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, + "set OFF button press must succeed"); + require(!repository.readBit(*button.binding).value, + "set OFF button press must write OFF"); + + button.buttonOperation = HmiButtonOperation::Toggle; + require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, + "toggle button press must succeed"); + require(repository.readBit(*button.binding).value, + "toggle button press must invert the current value"); HmiControl indicator; indicator.id = "running"; diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index abeec82..c72c389 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -224,6 +224,10 @@ void testModeActionsControlEditingAvailability() QDockWidget *properties_dock = requiredChild(window, "propertiesDock"); QLabel *selection = requiredChild(window, "selectionValueLabel"); QLineEdit *text_edit = requiredChild(window, "controlTextEdit"); + QComboBox *button_operation = requiredChild( + window, "buttonOperationComboBox"); + QPushButton *apply_properties = requiredChild( + window, "applyPropertiesButton"); HmiEditorWidget *hmi_editor = requiredChild( window, "hmiEditorWidget"); QLabel *executor_status = requiredChild(window, "executorStatusLabel"); @@ -252,10 +256,19 @@ void testModeActionsControlEditingAvailability() "an unbound added control must be marked in the property panel"); require(text_edit->text() == QStringLiteral("按钮"), "property panel must show the control text"); + require(button_operation->currentText() == QStringLiteral("瞬时 ON"), + "new HMI buttons must default to momentary ON"); const QList unbound_items = hmi_editor->scene()->selectedItems(); require(unbound_items.size() == 1 && unbound_items.front()->boundingRect().top() == 0, "an unbound HMI control must not reserve an address label area"); + button_operation->setCurrentIndex( + button_operation->findData(static_cast(HmiButtonOperation::Toggle))); + apply_properties->click(); + require(editor_service.findControl(page_id, "button-1")->buttonOperation + == HmiButtonOperation::Toggle, + "the property panel must update the HMI button operation"); + HmiControl bound_button = *editor_service.findControl(page_id, "button-1"); bound_button.binding = RegisterAddress{RegisterArea::M, 0}; require(editor_service.updateControl(page_id, bound_button.id, bound_button).succeeded, @@ -458,9 +471,9 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() ActiveRegisterRepository active_repository(virtual_repository); HmiRuntimeService runtime_service(active_repository); OfflineSimulationService simulation_service(virtual_repository); + RepeatedStatusGateway gateway; RuntimeModeService mode_service(project_service, simulation_service); RegisterMonitorService monitor_service(active_repository); - RepeatedStatusGateway gateway; mode_service.configurePlc( gateway, active_repository, virtual_repository, plc_repository); MainWindow window( @@ -502,9 +515,9 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() ActiveRegisterRepository active_repository(virtual_repository); HmiRuntimeService runtime_service(active_repository); OfflineSimulationService simulation_service(virtual_repository); + OnlineReadyGateway gateway; RuntimeModeService mode_service(project_service, simulation_service); RegisterMonitorService monitor_service(active_repository); - OnlineReadyGateway gateway; mode_service.configurePlc( gateway, active_repository, virtual_repository, plc_repository); MainWindow window( diff --git a/app/tests/offline_simulation_service_tests.cpp b/app/tests/offline_simulation_service_tests.cpp index 930d9e0..39fe924 100644 --- a/app/tests/offline_simulation_service_tests.cpp +++ b/app/tests/offline_simulation_service_tests.cpp @@ -255,6 +255,7 @@ void testHmiSimulationClosedLoop() start.bounds = {0, 0, 80, 30}; start.text = "start"; start.binding = RegisterAddress{RegisterArea::M, 0}; + start.buttonOperation = HmiButtonOperation::MomentaryOn; HmiControl indicator; indicator.id = "run-indicator"; indicator.type = HmiControlType::Indicator; @@ -267,12 +268,14 @@ void testHmiSimulationClosedLoop() {contact("start", 0), contact("feedback", 2)}}, coil("run", 2))}); - require(hmi.toggleButton(start).succeeded, "HMI start button must write virtual M"); + require(hmi.operateButton(start, HmiButtonEvent::Pressed).succeeded, + "HMI start button press must write virtual M"); require(executor.executeScan({program}, repository).succeeded, "closed-loop scan must succeed"); require(hmi.readControl(indicator).bit_value, "HMI indicator must observe the logic output"); - require(hmi.toggleButton(start).succeeded, "HMI start button must toggle off"); + require(hmi.operateButton(start, HmiButtonEvent::Released).succeeded, + "HMI start button release must write virtual M"); require(executor.executeScan({program}, repository).succeeded, "holding scan must succeed"); require(hmi.readControl(indicator).bit_value, "feedback contact must hold the output after start turns off"); diff --git a/app/tests/project_management_tests.cpp b/app/tests/project_management_tests.cpp index ba39d8e..68568f8 100644 --- a/app/tests/project_management_tests.cpp +++ b/app/tests/project_management_tests.cpp @@ -29,6 +29,7 @@ Project makeExampleProject() start_button.bounds = {10, 20, 120, 48}; start_button.text = "Start"; start_button.binding = RegisterAddress{RegisterArea::M, 0}; + start_button.buttonOperation = HmiButtonOperation::SetOn; start_button.properties.emplace("color", "green"); HmiControl running_indicator; @@ -170,6 +171,7 @@ void testExampleProjectRoundTrip() const QString first_path = directory.filePath("example.json"); const QString second_path = directory.filePath("example-copy.json"); + const QString invalid_operation_path = directory.filePath("invalid-operation.json"); require(service.saveAs(first_path.toStdString()).succeeded, "example project save must succeed"); const QByteArray saved_json = readBytes(first_path); @@ -180,6 +182,9 @@ void testExampleProjectRoundTrip() "saved project must use structured ladder expressions"); require(!saved_json.contains("\"dataPoints\""), "saved project must not contain the removed data point model"); + require(saved_json.contains("\"formatVersion\": \"1.0\"") + && saved_json.contains("\"buttonOperation\": \"setOn\""), + "version 1.0 projects must persist the button operation"); require(!saved_json.contains("\"stages\"") && !saved_json.contains("\"branches\""), "current project format must not contain the removed stage model"); @@ -197,6 +202,9 @@ void testExampleProjectRoundTrip() require(project.hmiPages.front().controls.front().binding->area() == RegisterArea::M, "HMI M binding must survive round trip"); + require(project.hmiPages.front().controls.front().buttonOperation + == HmiButtonOperation::SetOn, + "HMI button operation must survive round trip"); require(project.hmiPages.front().controls.front().properties.at("color") == "green", "HMI properties must survive round trip"); require(project.hmiPages.front().controls.at(1).type == HmiControlType::Indicator, @@ -228,6 +236,14 @@ void testExampleProjectRoundTrip() "save as must succeed after load"); require(readBytes(first_path) == readBytes(second_path), "save and save as must produce stable JSON"); + + QByteArray invalid_operation = saved_json; + invalid_operation.replace( + "\"buttonOperation\": \"setOn\"", + "\"buttonOperation\": \"unsupported\""); + writeText(invalid_operation_path, invalid_operation); + require(!service.load(invalid_operation_path.toStdString()).succeeded, + "unsupported HMI button operations must be rejected"); } void testInvalidFiles()