| @@ -41,11 +41,12 @@ bool AlarmDefinition::validate(std::string *error) const | |||||
| setError(error, "报警定义使用了无效地址"); | setError(error, "报警定义使用了无效地址"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (condition == AlarmCondition::MOn) | |||||
| if (condition == AlarmCondition::MOn | |||||
| || condition == AlarmCondition::MOff) | |||||
| { | { | ||||
| if (address.area() != RegisterArea::M) | if (address.area() != RegisterArea::M) | ||||
| { | { | ||||
| setError(error, "M ON 报警必须使用 M 地址"); | |||||
| setError(error, "M ON/OFF 报警必须使用 M 地址"); | |||||
| return false; | return false; | ||||
| } | } | ||||
| return true; | return true; | ||||
| @@ -5,10 +5,11 @@ | |||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <string> | #include <string> | ||||
| // 报警触发条件:M 位为 1,或 D 字高于/低于阈值 | |||||
| // 报警触发条件:M 位为 1/0,或 D 字高于/低于阈值 | |||||
| enum class AlarmCondition | enum class AlarmCondition | ||||
| { | { | ||||
| MOn, // M 位为 1 时触发 | MOn, // M 位为 1 时触发 | ||||
| MOff, // M 位为 0 时触发 | |||||
| DHigh, // D 寄存器值高于阈值时触发 | DHigh, // D 寄存器值高于阈值时触发 | ||||
| DLow // D 寄存器值低于阈值时触发 | DLow // D 寄存器值低于阈值时触发 | ||||
| }; | }; | ||||
| @@ -434,6 +434,10 @@ QString alarmConditionName(AlarmCondition condition) | |||||
| { | { | ||||
| return QStringLiteral("mOn"); | return QStringLiteral("mOn"); | ||||
| } | } | ||||
| case AlarmCondition::MOff: | |||||
| { | |||||
| return QStringLiteral("mOff"); | |||||
| } | |||||
| case AlarmCondition::DHigh: | case AlarmCondition::DHigh: | ||||
| { | { | ||||
| return QStringLiteral("dHigh"); | return QStringLiteral("dHigh"); | ||||
| @@ -456,6 +460,10 @@ bool parseAlarmCondition( | |||||
| { | { | ||||
| *condition = AlarmCondition::MOn; | *condition = AlarmCondition::MOn; | ||||
| } | } | ||||
| else if (value == "mOff") | |||||
| { | |||||
| *condition = AlarmCondition::MOff; | |||||
| } | |||||
| else if (value == "dHigh") | else if (value == "dHigh") | ||||
| { | { | ||||
| *condition = AlarmCondition::DHigh; | *condition = AlarmCondition::DHigh; | ||||
| @@ -78,11 +78,17 @@ const std::vector<AlarmRecord> &AlarmService::records() const | |||||
| std::optional<bool> AlarmService::evaluate( | std::optional<bool> AlarmService::evaluate( | ||||
| const AlarmDefinition &definition) const | const AlarmDefinition &definition) const | ||||
| { | { | ||||
| if (definition.condition == AlarmCondition::MOn) | |||||
| if (definition.condition == AlarmCondition::MOn | |||||
| || definition.condition == AlarmCondition::MOff) | |||||
| { | { | ||||
| // MOn 报警直接读取 M 位;读取失败返回空值,交由 refresh 保留旧记录 | |||||
| // M ON/OFF 共用一次位读取;读取失败时保留旧记录 | |||||
| const BitReadResult value = repository_.readBit(definition.address); | const BitReadResult value = repository_.readBit(definition.address); | ||||
| return value.succeeded ? std::optional<bool>{value.value} : std::nullopt; | |||||
| if (!value.succeeded) | |||||
| { | |||||
| return std::nullopt; | |||||
| } | |||||
| return definition.condition == AlarmCondition::MOn | |||||
| ? value.value : !value.value; | |||||
| } | } | ||||
| // DHigh 和 DLow 报警读取 D 字,比较方向由定义中的条件决定 | // DHigh 和 DLow 报警读取 D 字,比较方向由定义中的条件决定 | ||||
| const WordReadResult value = repository_.readWord(definition.address); | const WordReadResult value = repository_.readWord(definition.address); | ||||
| @@ -64,7 +64,7 @@ private: | |||||
| * @param definition 待评估的报警定义 | * @param definition 待评估的报警定义 | ||||
| * @return true 表示触发,false 表示未触发;读取失败时返回空值 | * @return true 表示触发,false 表示未触发;读取失败时返回空值 | ||||
| * | * | ||||
| * MOn 比较 M 位是否为 1;DHigh 使用大于等于阈值,DLow 使用小于等于阈值 | |||||
| * MOn/MOff 比较 M 位是否为 1/0;DHigh 使用大于等于阈值,DLow 使用小于等于阈值 | |||||
| */ | */ | ||||
| std::optional<bool> evaluate(const AlarmDefinition &definition) const; | std::optional<bool> evaluate(const AlarmDefinition &definition) const; | ||||
| @@ -29,6 +29,10 @@ QString conditionText(AlarmCondition condition) | |||||
| { | { | ||||
| return AlarmConfigurationDialog::tr("M 为 ON"); | return AlarmConfigurationDialog::tr("M 为 ON"); | ||||
| } | } | ||||
| case AlarmCondition::MOff: | |||||
| { | |||||
| return AlarmConfigurationDialog::tr("M 为 OFF"); | |||||
| } | |||||
| case AlarmCondition::DHigh: | case AlarmCondition::DHigh: | ||||
| { | { | ||||
| return AlarmConfigurationDialog::tr("D 大于等于阈值"); | return AlarmConfigurationDialog::tr("D 大于等于阈值"); | ||||
| @@ -109,7 +113,7 @@ void AlarmConfigurationDialog::reloadDefinitions(const std::string &selected_id) | |||||
| row, | row, | ||||
| 3, | 3, | ||||
| new QTableWidgetItem( | new QTableWidgetItem( | ||||
| definition.condition == AlarmCondition::MOn | |||||
| definition.address.area() == RegisterArea::M | |||||
| ? QStringLiteral("-") | ? QStringLiteral("-") | ||||
| : QString::number(definition.threshold))); | : QString::number(definition.threshold))); | ||||
| ui_->alarmTable->setItem( | ui_->alarmTable->setItem( | ||||
| @@ -162,6 +166,9 @@ void AlarmConfigurationDialog::updateConditionOptions() | |||||
| ui_->conditionComboBox->addItem( | ui_->conditionComboBox->addItem( | ||||
| conditionText(AlarmCondition::MOn), | conditionText(AlarmCondition::MOn), | ||||
| static_cast<int>(AlarmCondition::MOn)); | static_cast<int>(AlarmCondition::MOn)); | ||||
| ui_->conditionComboBox->addItem( | |||||
| conditionText(AlarmCondition::MOff), | |||||
| static_cast<int>(AlarmCondition::MOff)); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -83,9 +83,9 @@ public: | |||||
| { | { | ||||
| if (!control_.binding.has_value()) | if (!control_.binding.has_value()) | ||||
| { | { | ||||
| return visibleControlRect(); | |||||
| return controlRect(); | |||||
| } | } | ||||
| return visibleControlRect().united(addressRect()); | |||||
| return controlRect().united(addressRect()); | |||||
| } | } | ||||
| void paint( | void paint( | ||||
| @@ -94,7 +94,7 @@ public: | |||||
| QWidget *) override | QWidget *) override | ||||
| { | { | ||||
| // 留出一个像素边距,避免描边被图元边界裁剪 | // 留出一个像素边距,避免描边被图元边界裁剪 | ||||
| const QRectF rect = visibleControlRect().adjusted(1, 1, -1, -1); | |||||
| const QRectF rect = controlRect().adjusted(1, 1, -1, -1); | |||||
| painter->setRenderHint(QPainter::Antialiasing, true); | painter->setRenderHint(QPainter::Antialiasing, true); | ||||
| painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); | painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); | ||||
| @@ -363,13 +363,9 @@ public: | |||||
| void setInteractionState( | void setInteractionState( | ||||
| bool editable, bool runtime_active, bool runtime_write_enabled) | bool editable, bool runtime_active, bool runtime_write_enabled) | ||||
| { | { | ||||
| if (control_.type == HmiControlType::AlarmList) | |||||
| if (control_.type == HmiControlType::AlarmList && !runtime_active) | |||||
| { | { | ||||
| prepareGeometryChange(); | |||||
| if (!runtime_active) | |||||
| { | |||||
| alarm_page_ = 0U; | |||||
| } | |||||
| alarm_page_ = 0U; | |||||
| } | } | ||||
| editing_enabled_ = editable; | editing_enabled_ = editable; | ||||
| runtime_active_ = runtime_active; | runtime_active_ = runtime_active; | ||||
| @@ -412,7 +408,6 @@ public: | |||||
| page_hovered_ = false; | page_hovered_ = false; | ||||
| button_pressed_ = false; | button_pressed_ = false; | ||||
| } | } | ||||
| updateAlarmVisibility(); | |||||
| update(); | update(); | ||||
| } | } | ||||
| @@ -429,15 +424,10 @@ public: | |||||
| void setAlarmRecords(const std::vector<AlarmRecord> &records) | void setAlarmRecords(const std::vector<AlarmRecord> &records) | ||||
| { | { | ||||
| if (control_.type == HmiControlType::AlarmList) | |||||
| { | |||||
| prepareGeometryChange(); | |||||
| } | |||||
| alarm_records_ = records; | alarm_records_ = records; | ||||
| const std::size_t page_count = alarmPageCount(); | const std::size_t page_count = alarmPageCount(); | ||||
| alarm_page_ = page_count == 0U | alarm_page_ = page_count == 0U | ||||
| ? 0U : std::min(alarm_page_, page_count - 1U); | ? 0U : std::min(alarm_page_, page_count - 1U); | ||||
| updateAlarmVisibility(); | |||||
| update(); | update(); | ||||
| } | } | ||||
| @@ -469,7 +459,7 @@ protected: | |||||
| if (event->pos().y() < kAlarmHeaderHeight) | if (event->pos().y() < kAlarmHeaderHeight) | ||||
| { | { | ||||
| const QRectF content_rect = | const QRectF content_rect = | ||||
| visibleControlRect().adjusted(1, 1, -1, -1); | |||||
| controlRect().adjusted(1, 1, -1, -1); | |||||
| const QRectF header( | const QRectF header( | ||||
| content_rect.left(), | content_rect.left(), | ||||
| content_rect.top(), | content_rect.top(), | ||||
| @@ -644,22 +634,6 @@ private: | |||||
| alarmPageSize(), alarm_records_.size() - first_record); | alarmPageSize(), alarm_records_.size() - first_record); | ||||
| } | } | ||||
| QRectF visibleControlRect() const | |||||
| { | |||||
| const QRectF full_rect = controlRect(); | |||||
| if (control_.type != HmiControlType::AlarmList || !runtime_active_) | |||||
| { | |||||
| return full_rect; | |||||
| } | |||||
| const qreal content_height = kAlarmHeaderHeight | |||||
| + static_cast<qreal>(visibleAlarmRecordCount()) * kAlarmRowHeight | |||||
| + 2.0; | |||||
| return {full_rect.left(), | |||||
| full_rect.top(), | |||||
| full_rect.width(), | |||||
| std::min(full_rect.height(), content_height)}; | |||||
| } | |||||
| QRectF alarmNextPageRect(const QRectF &header) const | QRectF alarmNextPageRect(const QRectF &header) const | ||||
| { | { | ||||
| return { | return { | ||||
| @@ -689,17 +663,6 @@ private: | |||||
| kAlarmPageButtonSize}; | kAlarmPageButtonSize}; | ||||
| } | } | ||||
| void updateAlarmVisibility() | |||||
| { | |||||
| if (control_.type != HmiControlType::AlarmList) | |||||
| { | |||||
| return; | |||||
| } | |||||
| const bool has_active_alarm = !alarm_records_.empty(); | |||||
| setVisible(editing_enabled_ || !runtime_active_ || has_active_alarm); | |||||
| setZValue(runtime_active_ && has_active_alarm ? 1000.0 : 0.0); | |||||
| } | |||||
| // 控件本体范围保持领域坐标和尺寸语义不变 | // 控件本体范围保持领域坐标和尺寸语义不变 | ||||
| QRectF controlRect() const | QRectF controlRect() const | ||||
| { | { | ||||
| @@ -44,6 +44,13 @@ void testAlarmDefinitionsAndRuntimeLifecycle() | |||||
| m_alarm.message = "Emergency stop"; | m_alarm.message = "Emergency stop"; | ||||
| const AlarmEditorResult m_result = editor_service.addDefinition(m_alarm); | const AlarmEditorResult m_result = editor_service.addDefinition(m_alarm); | ||||
| AlarmDefinition m_off_alarm; | |||||
| m_off_alarm.address = RegisterAddress{RegisterArea::M, 1}; | |||||
| m_off_alarm.condition = AlarmCondition::MOff; | |||||
| m_off_alarm.message = "Safety circuit open"; | |||||
| const AlarmEditorResult m_off_result = editor_service.addDefinition( | |||||
| m_off_alarm); | |||||
| AlarmDefinition high_alarm; | AlarmDefinition high_alarm; | ||||
| high_alarm.address = RegisterAddress{RegisterArea::D, 0}; | high_alarm.address = RegisterAddress{RegisterArea::D, 0}; | ||||
| high_alarm.condition = AlarmCondition::DHigh; | high_alarm.condition = AlarmCondition::DHigh; | ||||
| @@ -57,8 +64,9 @@ void testAlarmDefinitionsAndRuntimeLifecycle() | |||||
| low_alarm.threshold = 10; | low_alarm.threshold = 10; | ||||
| low_alarm.message = "Pressure low"; | low_alarm.message = "Pressure low"; | ||||
| const AlarmEditorResult low_result = editor_service.addDefinition(low_alarm); | const AlarmEditorResult low_result = editor_service.addDefinition(low_alarm); | ||||
| require(m_result.succeeded && high_result.succeeded && low_result.succeeded, | |||||
| "M, D high and D low alarm definitions must be accepted"); | |||||
| require(m_result.succeeded && m_off_result.succeeded | |||||
| && high_result.succeeded && low_result.succeeded, | |||||
| "M ON, M OFF, D high and D low alarms must be accepted"); | |||||
| AlarmDefinition invalid = high_alarm; | AlarmDefinition invalid = high_alarm; | ||||
| invalid.address = RegisterAddress{RegisterArea::M, 2}; | invalid.address = RegisterAddress{RegisterArea::M, 2}; | ||||
| @@ -66,24 +74,34 @@ void testAlarmDefinitionsAndRuntimeLifecycle() | |||||
| == AlarmEditorError::InvalidDefinition, | == AlarmEditorError::InvalidDefinition, | ||||
| "D alarm conditions must reject M addresses"); | "D alarm conditions must reject M addresses"); | ||||
| invalid = m_off_alarm; | |||||
| invalid.address = RegisterAddress{RegisterArea::D, 2}; | |||||
| require(editor_service.addDefinition(invalid).error | |||||
| == AlarmEditorError::InvalidDefinition, | |||||
| "M OFF alarm conditions must reject D addresses"); | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 1}, true); | |||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 20); | repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 20); | ||||
| alarm_service.refresh(); | alarm_service.refresh(); | ||||
| require(alarm_service.records().empty(), | require(alarm_service.records().empty(), | ||||
| "inactive alarm conditions must not create records"); | "inactive alarm conditions must not create records"); | ||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | ||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 1}, false); | |||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 0}, 80); | repository.writeWord(RegisterAddress{RegisterArea::D, 0}, 80); | ||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 10); | repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 10); | ||||
| alarm_service.refresh(); | alarm_service.refresh(); | ||||
| require(findRecord(alarm_service, m_result.id) != nullptr | require(findRecord(alarm_service, m_result.id) != nullptr | ||||
| && findRecord(alarm_service, m_off_result.id) != nullptr | |||||
| && findRecord(alarm_service, high_result.id) != nullptr | && findRecord(alarm_service, high_result.id) != nullptr | ||||
| && findRecord(alarm_service, low_result.id) != nullptr, | && findRecord(alarm_service, low_result.id) != nullptr, | ||||
| "all three alarm conditions must create active records"); | |||||
| "all four alarm conditions must create active records"); | |||||
| require(alarm_service.acknowledge(m_result.id) | require(alarm_service.acknowledge(m_result.id) | ||||
| && findRecord(alarm_service, m_result.id)->acknowledged, | && findRecord(alarm_service, m_result.id)->acknowledged, | ||||
| "an active alarm must be locally acknowledged"); | "an active alarm must be locally acknowledged"); | ||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | ||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 1}, true); | |||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 0}, 79); | repository.writeWord(RegisterAddress{RegisterArea::D, 0}, 79); | ||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 11); | repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 11); | ||||
| alarm_service.refresh(); | alarm_service.refresh(); | ||||
| @@ -221,6 +221,35 @@ void testGridProjectRoundTrip() | |||||
| "typed ladder instructions must survive grid round trip"); | "typed ladder instructions must survive grid round trip"); | ||||
| } | } | ||||
| void testMOffAlarmRoundTrip() | |||||
| { | |||||
| QTemporaryDir directory; | |||||
| require(directory.isValid(), "temporary directory must be valid"); | |||||
| JsonProjectStorage storage; | |||||
| const QString path = directory.filePath("m-off-alarm.json"); | |||||
| Project project = makeExampleProject(); | |||||
| project.alarmDefinitions.push_back({ | |||||
| "alarm-m-off", | |||||
| RegisterAddress{RegisterArea::M, 5}, | |||||
| AlarmCondition::MOff, | |||||
| 0, | |||||
| "Safety circuit open"}); | |||||
| require(storage.save(project, path.toStdString()).succeeded, | |||||
| "an M OFF alarm project must save"); | |||||
| require(readBytes(path).contains("\"condition\": \"mOff\""), | |||||
| "M OFF alarms must serialize with the explicit mOff condition"); | |||||
| const ProjectLoadResult loaded = storage.load(path.toStdString()); | |||||
| require(loaded.succeeded | |||||
| && loaded.project.alarmDefinitions.size() == 1U | |||||
| && loaded.project.alarmDefinitions.front().condition | |||||
| == AlarmCondition::MOff | |||||
| && loaded.project.alarmDefinitions.front().address | |||||
| == RegisterAddress{RegisterArea::M, 5}, | |||||
| "the M OFF condition and address must survive JSON round trip"); | |||||
| } | |||||
| void testStrictVersionAndRequiredFields() | void testStrictVersionAndRequiredFields() | ||||
| { | { | ||||
| QTemporaryDir directory; | QTemporaryDir directory; | ||||
| @@ -390,6 +419,7 @@ int main() | |||||
| { | { | ||||
| testEmptyProjectRoundTrip(); | testEmptyProjectRoundTrip(); | ||||
| testGridProjectRoundTrip(); | testGridProjectRoundTrip(); | ||||
| testMOffAlarmRoundTrip(); | |||||
| testStrictVersionAndRequiredFields(); | testStrictVersionAndRequiredFields(); | ||||
| testInvalidGridAndConnectionsAreRejected(); | testInvalidGridAndConnectionsAreRejected(); | ||||
| testProjectServiceStateAndConfiguredLimits(); | testProjectServiceStateAndConfiguredLimits(); | ||||
| @@ -1,4 +1,5 @@ | |||||
| #include "domain/virtual_register_repository.h" | #include "domain/virtual_register_repository.h" | ||||
| #include "services/alarm_editor_service.h" | |||||
| #include "services/alarm_service.h" | #include "services/alarm_service.h" | ||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_navigation_service.h" | #include "services/hmi_navigation_service.h" | ||||
| @@ -9,6 +10,7 @@ | |||||
| #include "services/register_monitor_service.h" | #include "services/register_monitor_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "support/test_support.h" | #include "support/test_support.h" | ||||
| #include "ui/alarm_configuration_dialog.h" | |||||
| #include "ui/hmi_editor_widget.h" | #include "ui/hmi_editor_widget.h" | ||||
| #include "ui/logic_editor_widget.h" | #include "ui/logic_editor_widget.h" | ||||
| #include "ui/runtime_monitor_widget.h" | #include "ui/runtime_monitor_widget.h" | ||||
| @@ -18,12 +20,14 @@ | |||||
| #include <QCoreApplication> | #include <QCoreApplication> | ||||
| #include <QEventLoop> | #include <QEventLoop> | ||||
| #include <QGraphicsLineItem> | #include <QGraphicsLineItem> | ||||
| #include <QGraphicsRectItem> | |||||
| #include <QGraphicsScene> | #include <QGraphicsScene> | ||||
| #include <QGraphicsSimpleTextItem> | #include <QGraphicsSimpleTextItem> | ||||
| #include <QImage> | #include <QImage> | ||||
| #include <QKeyEvent> | #include <QKeyEvent> | ||||
| #include <QLabel> | #include <QLabel> | ||||
| #include <QLineEdit> | #include <QLineEdit> | ||||
| #include <QComboBox> | |||||
| #include <QMouseEvent> | #include <QMouseEvent> | ||||
| #include <QPainter> | #include <QPainter> | ||||
| #include <QWidget> | #include <QWidget> | ||||
| @@ -66,6 +70,103 @@ ControlLogic makeAlwaysOnLogic() | |||||
| return logic; | return logic; | ||||
| } | } | ||||
| void testAlarmConfigurationOffersMOnAndMOff() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| AlarmEditorService alarm_editor_service(project_service); | |||||
| AlarmConfigurationDialog dialog(alarm_editor_service); | |||||
| QComboBox *area_combo = dialog.findChild<QComboBox *>( | |||||
| QStringLiteral("areaComboBox")); | |||||
| QComboBox *condition_combo = dialog.findChild<QComboBox *>( | |||||
| QStringLiteral("conditionComboBox")); | |||||
| require(area_combo != nullptr && condition_combo != nullptr, | |||||
| "alarm configuration must expose its area and condition inputs"); | |||||
| require(area_combo->currentData().toInt() | |||||
| == static_cast<int>(RegisterArea::M) | |||||
| && condition_combo->count() == 2 | |||||
| && condition_combo->findData( | |||||
| static_cast<int>(AlarmCondition::MOn)) >= 0 | |||||
| && condition_combo->findData( | |||||
| static_cast<int>(AlarmCondition::MOff)) >= 0 | |||||
| && condition_combo->findText(QStringLiteral("M 为 ON")) >= 0 | |||||
| && condition_combo->findText(QStringLiteral("M 为 OFF")) >= 0, | |||||
| "M alarm configuration must offer both ON and OFF conditions"); | |||||
| } | |||||
| void testAlarmListKeepsFixedGeometryWhileRecordsChange() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| VirtualRegisterRepository virtual_repository; | |||||
| HmiEditorService hmi_editor_service(project_service); | |||||
| HmiRuntimeService hmi_runtime_service(virtual_repository); | |||||
| AlarmService alarm_service(project_service, virtual_repository); | |||||
| const HmiEditorResult page = hmi_editor_service.ensureDefaultPage(); | |||||
| const HmiEditorResult alarm_list = hmi_editor_service.addControl( | |||||
| page.id, HmiControlType::AlarmList); | |||||
| require(page.succeeded && alarm_list.succeeded, | |||||
| "alarm-list fixture must create a page and control"); | |||||
| project_service.editProject().alarmDefinitions.push_back({ | |||||
| "fixed-alarm", | |||||
| RegisterAddress{RegisterArea::M, 0}, | |||||
| AlarmCondition::MOn, | |||||
| 0, | |||||
| "Fixed alarm"}); | |||||
| HmiEditorWidget widget( | |||||
| hmi_editor_service, hmi_runtime_service, alarm_service); | |||||
| widget.setPageId(page.id); | |||||
| widget.setEditingEnabled(false); | |||||
| widget.setRuntimeActive(true); | |||||
| widget.refreshRuntimeValues(); | |||||
| QGraphicsItem *alarm_item = nullptr; | |||||
| for (QGraphicsItem *item : widget.scene()->items()) | |||||
| { | |||||
| if (dynamic_cast<QGraphicsRectItem *>(item) == nullptr) | |||||
| { | |||||
| alarm_item = item; | |||||
| break; | |||||
| } | |||||
| } | |||||
| const HmiControl *control = hmi_editor_service.findControl( | |||||
| page.id, alarm_list.id); | |||||
| require(alarm_item != nullptr && control != nullptr, | |||||
| "runtime HMI scene must contain the alarm-list item"); | |||||
| const QRectF fixed_bounds = alarm_item->boundingRect(); | |||||
| require(alarm_item->isVisible() | |||||
| && qFuzzyCompare( | |||||
| fixed_bounds.width() + 1.0, | |||||
| static_cast<qreal>(control->bounds.width) + 1.0) | |||||
| && qFuzzyCompare( | |||||
| fixed_bounds.height() + 1.0, | |||||
| static_cast<qreal>(control->bounds.height) + 1.0), | |||||
| "an empty runtime alarm list must remain visible at its configured size"); | |||||
| require(virtual_repository.writeBit( | |||||
| RegisterAddress{RegisterArea::M, 0}, true).succeeded, | |||||
| "alarm-list fixture must activate M0"); | |||||
| alarm_service.refresh(); | |||||
| widget.refreshRuntimeValues(); | |||||
| require(alarm_service.records().size() == 1U | |||||
| && alarm_item->isVisible() | |||||
| && alarm_item->boundingRect() == fixed_bounds, | |||||
| "adding an alarm row must not resize or hide the alarm control"); | |||||
| require(virtual_repository.writeBit( | |||||
| RegisterAddress{RegisterArea::M, 0}, false).succeeded, | |||||
| "alarm-list fixture must restore M0"); | |||||
| alarm_service.refresh(); | |||||
| widget.refreshRuntimeValues(); | |||||
| require(alarm_service.records().empty() | |||||
| && alarm_item->isVisible() | |||||
| && alarm_item->boundingRect() == fixed_bounds, | |||||
| "removing the last alarm row must keep the fixed alarm control visible"); | |||||
| } | |||||
| void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() | void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() | ||||
| { | { | ||||
| TestProjectStorage storage; | TestProjectStorage storage; | ||||
| @@ -856,6 +957,8 @@ int main(int argc, char *argv[]) | |||||
| QApplication application(argc, argv); | QApplication application(argc, argv); | ||||
| try | try | ||||
| { | { | ||||
| testAlarmConfigurationOffersMOnAndMOff(); | |||||
| testAlarmListKeepsFixedGeometryWhileRecordsChange(); | |||||
| testQueuedOfflineTraceIsIgnoredAfterReturningToEditing(); | testQueuedOfflineTraceIsIgnoredAfterReturningToEditing(); | ||||
| testLogicEditorGridSelectionAndDeletion(); | testLogicEditorGridSelectionAndDeletion(); | ||||
| testLadderLayoutAndDragDeletion(); | testLadderLayoutAndDragDeletion(); | ||||
| @@ -15,6 +15,7 @@ SOURCES += \ | |||||
| $$SERVICE_OFFLINE_SOURCES \ | $$SERVICE_OFFLINE_SOURCES \ | ||||
| $$SERVICE_RUNTIME_SOURCES \ | $$SERVICE_RUNTIME_SOURCES \ | ||||
| $$SERVICE_MONITOR_SOURCES \ | $$SERVICE_MONITOR_SOURCES \ | ||||
| ../src/ui/alarm_configuration_dialog.cpp \ | |||||
| ../src/ui/hmi_editor_widget.cpp \ | ../src/ui/hmi_editor_widget.cpp \ | ||||
| ../src/ui/logic_editor_widget.cpp \ | ../src/ui/logic_editor_widget.cpp \ | ||||
| ../src/ui/free_monitor_widget.cpp \ | ../src/ui/free_monitor_widget.cpp \ | ||||
| @@ -33,6 +34,7 @@ HEADERS += \ | |||||
| $$SERVICE_RUNTIME_HEADERS \ | $$SERVICE_RUNTIME_HEADERS \ | ||||
| $$SERVICE_MONITOR_HEADERS \ | $$SERVICE_MONITOR_HEADERS \ | ||||
| ../src/services/plc_communication_gateway.h \ | ../src/services/plc_communication_gateway.h \ | ||||
| ../src/ui/alarm_configuration_dialog.h \ | |||||
| ../src/ui/hmi_editor_widget.h \ | ../src/ui/hmi_editor_widget.h \ | ||||
| ../src/ui/logic_editor_widget.h \ | ../src/ui/logic_editor_widget.h \ | ||||
| ../src/ui/free_monitor_widget.h \ | ../src/ui/free_monitor_widget.h \ | ||||
| @@ -43,6 +45,7 @@ HEADERS += \ | |||||
| $$TEST_SUPPORT_HEADERS | $$TEST_SUPPORT_HEADERS | ||||
| FORMS += \ | FORMS += \ | ||||
| ../src/ui/alarm_configuration_dialog.ui \ | |||||
| ../src/ui/free_monitor_widget.ui \ | ../src/ui/free_monitor_widget.ui \ | ||||
| ../src/ui/runtime_monitor_window.ui \ | ../src/ui/runtime_monitor_window.ui \ | ||||
| ../src/ui/runtime_monitor_widget.ui | ../src/ui/runtime_monitor_widget.ui | ||||