Bladeren bron

feat: 增加HMI按钮位操作模式

main
suyu 1 maand geleden
bovenliggende
commit
e78eed84f7
12 gewijzigde bestanden met toevoegingen van 348 en 61 verwijderingen
  1. +12
    -0
      app/src/domain/hmi_model.h
  2. +73
    -0
      app/src/infrastructure/json_project_storage.cpp
  3. +53
    -7
      app/src/services/hmi_runtime_service.cpp
  4. +14
    -3
      app/src/services/hmi_runtime_service.h
  5. +67
    -40
      app/src/ui/hmi_editor_widget.cpp
  6. +6
    -2
      app/src/ui/hmi_editor_widget.h
  7. +23
    -0
      app/src/ui/main_window.cpp
  8. +33
    -2
      app/src/ui/main_window.ui
  9. +31
    -3
      app/tests/hmi_editor_service_tests.cpp
  10. +15
    -2
      app/tests/main_window_tests.cpp
  11. +5
    -2
      app/tests/offline_simulation_service_tests.cpp
  12. +16
    -0
      app/tests/project_management_tests.cpp

+ 12
- 0
app/src/domain/hmi_model.h Bestand weergeven

@@ -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 校验控件的标识、尺寸、扩展属性和寄存器绑定


+ 73
- 0
app/src/infrastructure/json_project_storage.cpp Bestand weergeven

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


+ 53
- 7
app/src/services/hmi_runtime_service.cpp Bestand weergeven

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


+ 14
- 3
app/src/services/hmi_runtime_service.h Bestand weergeven

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


+ 67
- 40
app/src/ui/hmi_editor_widget.cpp Bestand weergeven

@@ -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("寄存器操作失败"));


+ 6
- 2
app/src/ui/hmi_editor_widget.h Bestand weergeven

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


+ 23
- 0
app/src/ui/main_window.cpp Bestand weergeven

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


+ 33
- 2
app/src/ui/main_window.ui Bestand weergeven

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


+ 31
- 3
app/tests/hmi_editor_service_tests.cpp Bestand weergeven

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


+ 15
- 2
app/tests/main_window_tests.cpp Bestand weergeven

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


+ 5
- 2
app/tests/offline_simulation_service_tests.cpp Bestand weergeven

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


+ 16
- 0
app/tests/project_management_tests.cpp Bestand weergeven

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


Laden…
Annuleren
Opslaan