| @@ -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<RegisterAddress> binding; | |||
| std::map<std::string, std::string> properties; | |||
| HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; | |||
| /** | |||
| * @brief 校验控件的标识、尺寸、扩展属性和寄存器绑定 | |||
| @@ -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()) | |||
| @@ -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)); | |||
| @@ -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 地址 | |||
| @@ -30,12 +30,14 @@ public: | |||
| int page_width, | |||
| int page_height, | |||
| std::function<void(const std::string &, const QPointF &)> moved, | |||
| std::function<void(const std::string &)> activated) | |||
| std::function<void(const std::string &, HmiButtonEvent)> button_event, | |||
| std::function<void(const std::string &)> 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<void(const std::string &, const QPointF &)> moved_; | |||
| // 运行模式点击按钮 / 双击输入框回调,通知外层做寄存器读写 | |||
| std::function<void(const std::string &)> activated_; | |||
| std::function<void(const std::string &, HmiButtonEvent)> button_event_; | |||
| std::function<void(const std::string &)> 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<int>(control->text.size())), | |||
| initial, | |||
| std::numeric_limits<std::int16_t>::min(), | |||
| std::numeric_limits<std::int16_t>::max(), | |||
| 1, | |||
| &accepted); | |||
| if (!accepted) | |||
| { | |||
| return; | |||
| } | |||
| result = runtime_service_.writeNumericInput( | |||
| *control, static_cast<std::int16_t>(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<int>(control->text.size())), | |||
| initial, | |||
| std::numeric_limits<std::int16_t>::min(), | |||
| std::numeric_limits<std::int16_t>::max(), | |||
| 1, | |||
| &accepted); | |||
| if (!accepted) | |||
| { | |||
| return; | |||
| } | |||
| const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( | |||
| *control, static_cast<std::int16_t>(value)); | |||
| if (!result.succeeded) | |||
| { | |||
| emit editorError(tr("寄存器操作失败")); | |||
| @@ -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_; | |||
| @@ -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<int>(HmiButtonOperation::SetOn)); | |||
| ui_->buttonOperationComboBox->setItemData( | |||
| 1, static_cast<int>(HmiButtonOperation::SetOff)); | |||
| ui_->buttonOperationComboBox->setItemData( | |||
| 2, static_cast<int>(HmiButtonOperation::Toggle)); | |||
| ui_->buttonOperationComboBox->setItemData( | |||
| 3, static_cast<int>(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<QWidget *>(ui_->controlHeightSpinBox), | |||
| static_cast<QWidget *>(ui_->bindingAreaComboBox), | |||
| static_cast<QWidget *>(ui_->bindingIndexSpinBox), | |||
| static_cast<QWidget *>(ui_->buttonOperationComboBox), | |||
| static_cast<QWidget *>(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<int>(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<HmiButtonOperation>( | |||
| ui_->buttonOperationComboBox->currentData().toInt()); | |||
| } | |||
| const HmiEditorResult result = hmi_editor_service_.updateControl( | |||
| hmi_editor_service_.firstPageId(), selected_control_id_, control); | |||
| if (!result.succeeded) | |||
| @@ -575,8 +575,39 @@ | |||
| </property> | |||
| </widget> | |||
| </item> | |||
| <item row="8" column="0" colspan="2"> | |||
| <widget class="QPushButton" name="applyPropertiesButton"> | |||
| <item row="8" column="0"> | |||
| <widget class="QLabel" name="buttonOperationLabel"> | |||
| <property name="text"> | |||
| <string>按钮操作</string> | |||
| </property> | |||
| </widget> | |||
| </item> | |||
| <item row="8" column="1"> | |||
| <widget class="QComboBox" name="buttonOperationComboBox"> | |||
| <item> | |||
| <property name="text"> | |||
| <string>置 ON</string> | |||
| </property> | |||
| </item> | |||
| <item> | |||
| <property name="text"> | |||
| <string>置 OFF</string> | |||
| </property> | |||
| </item> | |||
| <item> | |||
| <property name="text"> | |||
| <string>取反</string> | |||
| </property> | |||
| </item> | |||
| <item> | |||
| <property name="text"> | |||
| <string>瞬时 ON</string> | |||
| </property> | |||
| </item> | |||
| </widget> | |||
| </item> | |||
| <item row="9" column="0" colspan="2"> | |||
| <widget class="QPushButton" name="applyPropertiesButton"> | |||
| <property name="text"> | |||
| <string>应用属性</string> | |||
| </property> | |||
| @@ -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"; | |||
| @@ -224,6 +224,10 @@ void testModeActionsControlEditingAvailability() | |||
| QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | |||
| QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | |||
| QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit"); | |||
| QComboBox *button_operation = requiredChild<QComboBox>( | |||
| window, "buttonOperationComboBox"); | |||
| QPushButton *apply_properties = requiredChild<QPushButton>( | |||
| window, "applyPropertiesButton"); | |||
| HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>( | |||
| window, "hmiEditorWidget"); | |||
| QLabel *executor_status = requiredChild<QLabel>(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<QGraphicsItem *> 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<int>(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( | |||
| @@ -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"); | |||
| @@ -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() | |||