| @@ -44,6 +44,17 @@ enum class HmiControlType | |||||
| Label // 标签 | Label // 标签 | ||||
| }; | }; | ||||
| /** | |||||
| * @brief 按钮对绑定 M 位执行的操作 | |||||
| */ | |||||
| enum class HmiButtonOperation | |||||
| { | |||||
| SetOn, // 按下时写入 1 | |||||
| SetOff, // 按下时写入 0 | |||||
| Toggle, // 按下时写入当前读回值的反值 | |||||
| MomentaryOn // 按下时写入 1,释放时写入 0 | |||||
| }; | |||||
| /** | /** | ||||
| * @brief 描述一个可保存的 HMI 控件及其显示和寄存器配置 | * @brief 描述一个可保存的 HMI 控件及其显示和寄存器配置 | ||||
| * | * | ||||
| @@ -57,6 +68,7 @@ struct HmiControl | |||||
| std::string text; | std::string text; | ||||
| std::optional<RegisterAddress> binding; | std::optional<RegisterAddress> binding; | ||||
| std::map<std::string, std::string> properties; | std::map<std::string, std::string> properties; | ||||
| HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; | |||||
| /** | /** | ||||
| * @brief 校验控件的标识、尺寸、扩展属性和寄存器绑定 | * @brief 校验控件的标识、尺寸、扩展属性和寄存器绑定 | ||||
| @@ -318,6 +318,58 @@ bool parseHmiControlType( | |||||
| return true; | 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 对象 | // 将 HMI 控件矩形区域序列化为 JSON 对象 | ||||
| QJsonObject serializeBounds(const HmiRect &bounds) | QJsonObject serializeBounds(const HmiRect &bounds) | ||||
| { | { | ||||
| @@ -418,6 +470,12 @@ QJsonObject serializeHmiControl(const HmiControl &control) | |||||
| object.insert(QStringLiteral("binding"), QJsonValue::Null); | object.insert(QStringLiteral("binding"), QJsonValue::Null); | ||||
| } | } | ||||
| object.insert(QStringLiteral("properties"), serializeProperties(control.properties)); | object.insert(QStringLiteral("properties"), serializeProperties(control.properties)); | ||||
| if (control.type == HmiControlType::Button) | |||||
| { | |||||
| object.insert( | |||||
| QStringLiteral("buttonOperation"), | |||||
| hmiButtonOperationName(control.buttonOperation)); | |||||
| } | |||||
| return object; | return object; | ||||
| } | } | ||||
| @@ -447,6 +505,21 @@ bool parseHmiControl( | |||||
| { | { | ||||
| return false; | 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,其余非空值必须是合法的寄存器地址对象 | // binding 允许为 null,其余非空值必须是合法的寄存器地址对象 | ||||
| if (binding.isNull()) | if (binding.isNull()) | ||||
| @@ -67,20 +67,66 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c | |||||
| return readFailure(HmiRuntimeError::UnsupportedControl); | return readFailure(HmiRuntimeError::UnsupportedControl); | ||||
| } | } | ||||
| HmiRuntimeWriteResult HmiRuntimeService::toggleButton(const HmiControl &control) | |||||
| HmiRuntimeWriteResult HmiRuntimeService::operateButton( | |||||
| const HmiControl &control, HmiButtonEvent event) | |||||
| { | { | ||||
| if (control.type != HmiControlType::Button) | if (control.type != HmiControlType::Button) | ||||
| { | { | ||||
| return writeFailure(HmiRuntimeError::UnsupportedControl); | 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 | return result.succeeded | ||||
| ? HmiRuntimeWriteResult{true, HmiRuntimeError::None} | ? HmiRuntimeWriteResult{true, HmiRuntimeError::None} | ||||
| : writeFailure(repositoryError(result.error)); | : writeFailure(repositoryError(result.error)); | ||||
| @@ -46,6 +46,15 @@ struct HmiRuntimeWriteResult | |||||
| HmiRuntimeError error = HmiRuntimeError::None; | HmiRuntimeError error = HmiRuntimeError::None; | ||||
| }; | }; | ||||
| /** | |||||
| * @brief HMI 按钮在运行态产生的输入事件 | |||||
| */ | |||||
| enum class HmiButtonEvent | |||||
| { | |||||
| Pressed, | |||||
| Released | |||||
| }; | |||||
| /** | /** | ||||
| * @brief 仅通过统一寄存器仓库执行 HMI 运行态读写 | * @brief 仅通过统一寄存器仓库执行 HMI 运行态读写 | ||||
| * | * | ||||
| @@ -65,11 +74,13 @@ public: | |||||
| */ | */ | ||||
| HmiRuntimeReadResult readControl(const HmiControl &control) const; | HmiRuntimeReadResult readControl(const HmiControl &control) const; | ||||
| /** | /** | ||||
| * @brief 切换按钮当前绑定 M 位的值 | |||||
| * @brief 根据按钮配置处理按下或释放事件 | |||||
| * @param control 按钮控件,必须绑定有效 M 地址 | * @param control 按钮控件,必须绑定有效 M 地址 | ||||
| * @return 非按钮、绑定无效或仓库拒绝写入时返回失败结果 | |||||
| * @param event 当前输入事件 | |||||
| * @return 无需写入的事件返回成功,配置无效或仓库拒绝写入时返回失败 | |||||
| */ | */ | ||||
| HmiRuntimeWriteResult toggleButton(const HmiControl &control); | |||||
| HmiRuntimeWriteResult operateButton( | |||||
| const HmiControl &control, HmiButtonEvent event); | |||||
| /** | /** | ||||
| * @brief 将带符号 16 位数值写入数值输入控件绑定的 D 地址 | * @brief 将带符号 16 位数值写入数值输入控件绑定的 D 地址 | ||||
| * @param control 数值输入控件,必须绑定有效 D 地址 | * @param control 数值输入控件,必须绑定有效 D 地址 | ||||
| @@ -30,12 +30,14 @@ public: | |||||
| int page_width, | int page_width, | ||||
| int page_height, | int page_height, | ||||
| std::function<void(const std::string &, const QPointF &)> moved, | 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), | : control_(control), | ||||
| page_width_(page_width), | page_width_(page_width), | ||||
| page_height_(page_height), | page_height_(page_height), | ||||
| moved_(std::move(moved)), | 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); | setPos(control_.bounds.x, control_.bounds.y); | ||||
| @@ -194,16 +196,15 @@ protected: | |||||
| return QGraphicsItem::itemChange(change, value); | return QGraphicsItem::itemChange(change, value); | ||||
| } | } | ||||
| // 运行态点击按钮时通知外层执行 M 位切换 | |||||
| // 运行态按钮按下时通知外层执行配置的 M 位操作 | |||||
| void mousePressEvent(QGraphicsSceneMouseEvent *event) override | void mousePressEvent(QGraphicsSceneMouseEvent *event) override | ||||
| { | { | ||||
| // 条件:!ItemIsMovable 【也就是运行模式】 + 是按钮 + 有回调 | |||||
| if (!flags().testFlag(ItemIsMovable) | if (!flags().testFlag(ItemIsMovable) | ||||
| && control_.type == HmiControlType::Button && activated_) | |||||
| && control_.type == HmiControlType::Button && button_event_) | |||||
| { | { | ||||
| // 运行态单击按钮才触发 M 位切换,编辑态只用于选择和拖动 | |||||
| setSelected(true); | setSelected(true); | ||||
| activated_(control_.id); | |||||
| button_pressed_ = true; | |||||
| button_event_(control_.id, HmiButtonEvent::Pressed); | |||||
| event->accept(); | event->accept(); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -215,11 +216,12 @@ protected: | |||||
| void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override | void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override | ||||
| { | { | ||||
| if (!flags().testFlag(ItemIsMovable) | if (!flags().testFlag(ItemIsMovable) | ||||
| && control_.type == HmiControlType::NumericInput && activated_) | |||||
| && control_.type == HmiControlType::NumericInput | |||||
| && numeric_input_activated_) | |||||
| { | { | ||||
| // 数值输入使用双击,避免普通选择操作意外写入 D 字 | // 数值输入使用双击,避免普通选择操作意外写入 D 字 | ||||
| setSelected(true); | setSelected(true); | ||||
| activated_(control_.id); | |||||
| numeric_input_activated_(control_.id); | |||||
| event->accept(); | event->accept(); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -229,6 +231,14 @@ protected: | |||||
| // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 | // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 | ||||
| void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override | 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); | QGraphicsItem::mouseReleaseEvent(event); | ||||
| // 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层 | // 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层 | ||||
| if (flags().testFlag(ItemIsMovable) && moved_) | if (flags().testFlag(ItemIsMovable) && moved_) | ||||
| @@ -290,8 +300,9 @@ private: | |||||
| int page_height_ = 0; | int page_height_ = 0; | ||||
| // 拖拽完成后回调 HmiEditorWidget 提交控件新位置 | // 拖拽完成后回调 HmiEditorWidget 提交控件新位置 | ||||
| std::function<void(const std::string &, const QPointF &)> moved_; | 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 位值 | // 指示灯读取到的 M 位值 | ||||
| bool bit_value_ = false; | bool bit_value_ = false; | ||||
| // 数值控件读取到的 D 字值 | // 数值控件读取到的 D 字值 | ||||
| @@ -395,9 +406,13 @@ void HmiEditorWidget::reloadPage() | |||||
| { | { | ||||
| handleControlMoved(control_id, position); | handleControlMoved(control_id, position); | ||||
| }, | }, | ||||
| [this](const std::string &control_id, HmiButtonEvent event) | |||||
| { | |||||
| handleButtonEvent(control_id, event); | |||||
| }, | |||||
| [this](const std::string &control_id) | [this](const std::string &control_id) | ||||
| { | { | ||||
| handleControlActivated(control_id); | |||||
| handleNumericInputActivated(control_id); | |||||
| }); | }); | ||||
| scene_->addItem(item); | scene_->addItem(item); | ||||
| item->setInteractionEnabled(editing_enabled_); | item->setInteractionEnabled(editing_enabled_); | ||||
| @@ -518,7 +533,8 @@ void HmiEditorWidget::handleControlMoved( | |||||
| emit controlChanged(QString::fromStdString(control_id)); | 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_) | if (!runtime_active_ || !runtime_write_enabled_) | ||||
| { | { | ||||
| @@ -529,37 +545,48 @@ void HmiEditorWidget::handleControlActivated(const std::string &control_id) | |||||
| { | { | ||||
| return; | 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; | return; | ||||
| } | } | ||||
| const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( | |||||
| *control, static_cast<std::int16_t>(value)); | |||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| emit editorError(tr("寄存器操作失败")); | emit editorError(tr("寄存器操作失败")); | ||||
| @@ -15,6 +15,7 @@ class HmiEditorService; | |||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class QGraphicsScene; | class QGraphicsScene; | ||||
| class QResizeEvent; | class QResizeEvent; | ||||
| enum class HmiButtonEvent; | |||||
| /** | /** | ||||
| * @brief 将 HMI 页面模型投影为可选择和拖动的图形编辑画布 | * @brief 将 HMI 页面模型投影为可选择和拖动的图形编辑画布 | ||||
| @@ -102,8 +103,11 @@ private: | |||||
| void handleSelectionChanged(); | void handleSelectionChanged(); | ||||
| // 将图元拖动后的坐标提交给 HMI 编辑服务 | // 将图元拖动后的坐标提交给 HMI 编辑服务 | ||||
| void handleControlMoved(const std::string &control_id, const QPointF &position); | 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 图元数据 | // 提供控件查找、移动和属性更新能力,不直接操作 Qt 图元数据 | ||||
| HmiEditorService &editor_service_; | HmiEditorService &editor_service_; | ||||
| @@ -541,6 +541,14 @@ void MainWindow::configurePropertyEditor() | |||||
| ui_->bindingAreaComboBox->setItemData(0, -1); | ui_->bindingAreaComboBox->setItemData(0, -1); | ||||
| ui_->bindingAreaComboBox->setItemData(1, 0); | ui_->bindingAreaComboBox->setItemData(1, 0); | ||||
| ui_->bindingAreaComboBox->setItemData(2, 1); | 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) | for (int index = 0; index < ui_->logicComparisonComboBox->count(); ++index) | ||||
| { | { | ||||
| ui_->logicComparisonComboBox->setItemData(index, 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_->controlHeightSpinBox), | ||||
| static_cast<QWidget *>(ui_->bindingAreaComboBox), | static_cast<QWidget *>(ui_->bindingAreaComboBox), | ||||
| static_cast<QWidget *>(ui_->bindingIndexSpinBox), | static_cast<QWidget *>(ui_->bindingIndexSpinBox), | ||||
| static_cast<QWidget *>(ui_->buttonOperationComboBox), | |||||
| static_cast<QWidget *>(ui_->applyPropertiesButton)}) | static_cast<QWidget *>(ui_->applyPropertiesButton)}) | ||||
| { | { | ||||
| widget->setEnabled(has_control); | widget->setEnabled(has_control); | ||||
| @@ -682,6 +691,8 @@ void MainWindow::showControlProperties(const std::string &control_id) | |||||
| // 未选中有效控件,无需填充属性,直接退出 | // 未选中有效控件,无需填充属性,直接退出 | ||||
| if (!has_control) | if (!has_control) | ||||
| { | { | ||||
| ui_->buttonOperationLabel->setVisible(false); | |||||
| ui_->buttonOperationComboBox->setVisible(false); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -701,6 +712,13 @@ void MainWindow::showControlProperties(const std::string &control_id) | |||||
| : control->binding->area() == RegisterArea::M ? 1 : 2); | : control->binding->area() == RegisterArea::M ? 1 : 2); | ||||
| ui_->bindingIndexSpinBox->setValue( | ui_->bindingIndexSpinBox->setValue( | ||||
| control->binding.has_value() ? control->binding->index() : 0); | 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) | 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; | const RegisterArea area = binding_area == 0 ? RegisterArea::M : RegisterArea::D; | ||||
| control.binding = RegisterAddress{area, ui_->bindingIndexSpinBox->value()}; | 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( | const HmiEditorResult result = hmi_editor_service_.updateControl( | ||||
| hmi_editor_service_.firstPageId(), selected_control_id_, control); | hmi_editor_service_.firstPageId(), selected_control_id_, control); | ||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| @@ -575,8 +575,39 @@ | |||||
| </property> | </property> | ||||
| </widget> | </widget> | ||||
| </item> | </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"> | <property name="text"> | ||||
| <string>应用属性</string> | <string>应用属性</string> | ||||
| </property> | </property> | ||||
| @@ -91,10 +91,38 @@ void testRuntimeUsesRegisterRepository() | |||||
| button.id = "start"; | button.id = "start"; | ||||
| button.type = HmiControlType::Button; | button.type = HmiControlType::Button; | ||||
| button.binding = RegisterAddress{RegisterArea::M, 12}; | 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, | 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; | HmiControl indicator; | ||||
| indicator.id = "running"; | indicator.id = "running"; | ||||
| @@ -224,6 +224,10 @@ void testModeActionsControlEditingAvailability() | |||||
| QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | ||||
| QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | ||||
| QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit"); | 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>( | HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>( | ||||
| window, "hmiEditorWidget"); | window, "hmiEditorWidget"); | ||||
| QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel"); | QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel"); | ||||
| @@ -252,10 +256,19 @@ void testModeActionsControlEditingAvailability() | |||||
| "an unbound added control must be marked in the property panel"); | "an unbound added control must be marked in the property panel"); | ||||
| require(text_edit->text() == QStringLiteral("按钮"), | require(text_edit->text() == QStringLiteral("按钮"), | ||||
| "property panel must show the control text"); | "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(); | const QList<QGraphicsItem *> unbound_items = hmi_editor->scene()->selectedItems(); | ||||
| require(unbound_items.size() == 1 && unbound_items.front()->boundingRect().top() == 0, | require(unbound_items.size() == 1 && unbound_items.front()->boundingRect().top() == 0, | ||||
| "an unbound HMI control must not reserve an address label area"); | "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"); | HmiControl bound_button = *editor_service.findControl(page_id, "button-1"); | ||||
| bound_button.binding = RegisterAddress{RegisterArea::M, 0}; | bound_button.binding = RegisterAddress{RegisterArea::M, 0}; | ||||
| require(editor_service.updateControl(page_id, bound_button.id, bound_button).succeeded, | require(editor_service.updateControl(page_id, bound_button.id, bound_button).succeeded, | ||||
| @@ -458,9 +471,9 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||||
| ActiveRegisterRepository active_repository(virtual_repository); | ActiveRegisterRepository active_repository(virtual_repository); | ||||
| HmiRuntimeService runtime_service(active_repository); | HmiRuntimeService runtime_service(active_repository); | ||||
| OfflineSimulationService simulation_service(virtual_repository); | OfflineSimulationService simulation_service(virtual_repository); | ||||
| RepeatedStatusGateway gateway; | |||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| RegisterMonitorService monitor_service(active_repository); | RegisterMonitorService monitor_service(active_repository); | ||||
| RepeatedStatusGateway gateway; | |||||
| mode_service.configurePlc( | mode_service.configurePlc( | ||||
| gateway, active_repository, virtual_repository, plc_repository); | gateway, active_repository, virtual_repository, plc_repository); | ||||
| MainWindow window( | MainWindow window( | ||||
| @@ -502,9 +515,9 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| ActiveRegisterRepository active_repository(virtual_repository); | ActiveRegisterRepository active_repository(virtual_repository); | ||||
| HmiRuntimeService runtime_service(active_repository); | HmiRuntimeService runtime_service(active_repository); | ||||
| OfflineSimulationService simulation_service(virtual_repository); | OfflineSimulationService simulation_service(virtual_repository); | ||||
| OnlineReadyGateway gateway; | |||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| RegisterMonitorService monitor_service(active_repository); | RegisterMonitorService monitor_service(active_repository); | ||||
| OnlineReadyGateway gateway; | |||||
| mode_service.configurePlc( | mode_service.configurePlc( | ||||
| gateway, active_repository, virtual_repository, plc_repository); | gateway, active_repository, virtual_repository, plc_repository); | ||||
| MainWindow window( | MainWindow window( | ||||
| @@ -255,6 +255,7 @@ void testHmiSimulationClosedLoop() | |||||
| start.bounds = {0, 0, 80, 30}; | start.bounds = {0, 0, 80, 30}; | ||||
| start.text = "start"; | start.text = "start"; | ||||
| start.binding = RegisterAddress{RegisterArea::M, 0}; | start.binding = RegisterAddress{RegisterArea::M, 0}; | ||||
| start.buttonOperation = HmiButtonOperation::MomentaryOn; | |||||
| HmiControl indicator; | HmiControl indicator; | ||||
| indicator.id = "run-indicator"; | indicator.id = "run-indicator"; | ||||
| indicator.type = HmiControlType::Indicator; | indicator.type = HmiControlType::Indicator; | ||||
| @@ -267,12 +268,14 @@ void testHmiSimulationClosedLoop() | |||||
| {contact("start", 0), contact("feedback", 2)}}, | {contact("start", 0), contact("feedback", 2)}}, | ||||
| coil("run", 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, | require(executor.executeScan({program}, repository).succeeded, | ||||
| "closed-loop scan must succeed"); | "closed-loop scan must succeed"); | ||||
| require(hmi.readControl(indicator).bit_value, | require(hmi.readControl(indicator).bit_value, | ||||
| "HMI indicator must observe the logic output"); | "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(executor.executeScan({program}, repository).succeeded, "holding scan must succeed"); | ||||
| require(hmi.readControl(indicator).bit_value, | require(hmi.readControl(indicator).bit_value, | ||||
| "feedback contact must hold the output after start turns off"); | "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.bounds = {10, 20, 120, 48}; | ||||
| start_button.text = "Start"; | start_button.text = "Start"; | ||||
| start_button.binding = RegisterAddress{RegisterArea::M, 0}; | start_button.binding = RegisterAddress{RegisterArea::M, 0}; | ||||
| start_button.buttonOperation = HmiButtonOperation::SetOn; | |||||
| start_button.properties.emplace("color", "green"); | start_button.properties.emplace("color", "green"); | ||||
| HmiControl running_indicator; | HmiControl running_indicator; | ||||
| @@ -170,6 +171,7 @@ void testExampleProjectRoundTrip() | |||||
| const QString first_path = directory.filePath("example.json"); | const QString first_path = directory.filePath("example.json"); | ||||
| const QString second_path = directory.filePath("example-copy.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, | require(service.saveAs(first_path.toStdString()).succeeded, | ||||
| "example project save must succeed"); | "example project save must succeed"); | ||||
| const QByteArray saved_json = readBytes(first_path); | const QByteArray saved_json = readBytes(first_path); | ||||
| @@ -180,6 +182,9 @@ void testExampleProjectRoundTrip() | |||||
| "saved project must use structured ladder expressions"); | "saved project must use structured ladder expressions"); | ||||
| require(!saved_json.contains("\"dataPoints\""), | require(!saved_json.contains("\"dataPoints\""), | ||||
| "saved project must not contain the removed data point model"); | "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\"") | require(!saved_json.contains("\"stages\"") | ||||
| && !saved_json.contains("\"branches\""), | && !saved_json.contains("\"branches\""), | ||||
| "current project format must not contain the removed stage model"); | "current project format must not contain the removed stage model"); | ||||
| @@ -197,6 +202,9 @@ void testExampleProjectRoundTrip() | |||||
| require(project.hmiPages.front().controls.front().binding->area() | require(project.hmiPages.front().controls.front().binding->area() | ||||
| == RegisterArea::M, | == RegisterArea::M, | ||||
| "HMI M binding must survive round trip"); | "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", | require(project.hmiPages.front().controls.front().properties.at("color") == "green", | ||||
| "HMI properties must survive round trip"); | "HMI properties must survive round trip"); | ||||
| require(project.hmiPages.front().controls.at(1).type == HmiControlType::Indicator, | require(project.hmiPages.front().controls.at(1).type == HmiControlType::Indicator, | ||||
| @@ -228,6 +236,14 @@ void testExampleProjectRoundTrip() | |||||
| "save as must succeed after load"); | "save as must succeed after load"); | ||||
| require(readBytes(first_path) == readBytes(second_path), | require(readBytes(first_path) == readBytes(second_path), | ||||
| "save and save as must produce stable JSON"); | "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() | void testInvalidFiles() | ||||