| @@ -29,21 +29,19 @@ void AlarmService::refresh() | |||||
| records_.begin(), records_.end(), | records_.begin(), records_.end(), | ||||
| [&definition](const AlarmRecord &record) | [&definition](const AlarmRecord &record) | ||||
| { | { | ||||
| return record.definitionId == definition.id && record.active; | |||||
| return record.definitionId == definition.id; | |||||
| }); | }); | ||||
| if (*active && current == records_.end()) | if (*active && current == records_.end()) | ||||
| { | { | ||||
| records_.insert( | records_.insert( | ||||
| records_.begin(), | records_.begin(), | ||||
| AlarmRecord{definition.id, definition.message, true, false, now, {}}); | |||||
| AlarmRecord{definition.id, definition.message, false, now}); | |||||
| } | } | ||||
| else if (!*active && current != records_.end()) | else if (!*active && current != records_.end()) | ||||
| { | { | ||||
| current->active = false; | |||||
| current->clearedAt = now; | |||||
| records_.erase(current); | |||||
| } | } | ||||
| } | } | ||||
| trimHistory(); | |||||
| } | } | ||||
| bool AlarmService::acknowledge(const std::string &definition_id) | bool AlarmService::acknowledge(const std::string &definition_id) | ||||
| @@ -52,7 +50,7 @@ bool AlarmService::acknowledge(const std::string &definition_id) | |||||
| records_.begin(), records_.end(), | records_.begin(), records_.end(), | ||||
| [&definition_id](const AlarmRecord &candidate) | [&definition_id](const AlarmRecord &candidate) | ||||
| { | { | ||||
| return candidate.definitionId == definition_id && candidate.active; | |||||
| return candidate.definitionId == definition_id; | |||||
| }); | }); | ||||
| if (record == records_.end()) | if (record == records_.end()) | ||||
| { | { | ||||
| @@ -95,20 +93,3 @@ std::optional<bool> AlarmService::evaluate( | |||||
| } | } | ||||
| return std::nullopt; | return std::nullopt; | ||||
| } | } | ||||
| void AlarmService::trimHistory() | |||||
| { | |||||
| std::size_t history_count = static_cast<std::size_t>(std::count_if( | |||||
| records_.cbegin(), records_.cend(), | |||||
| [](const AlarmRecord &record) { return !record.active; })); | |||||
| for (auto record = records_.end(); | |||||
| record != records_.begin() && history_count > kHistoryLimit;) | |||||
| { | |||||
| --record; | |||||
| if (!record->active) | |||||
| { | |||||
| record = records_.erase(record); | |||||
| --history_count; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,7 +1,6 @@ | |||||
| #pragma once | #pragma once | ||||
| #include <chrono> | #include <chrono> | ||||
| #include <cstddef> | |||||
| #include <optional> | #include <optional> | ||||
| #include <string> | #include <string> | ||||
| #include <vector> | #include <vector> | ||||
| @@ -14,10 +13,8 @@ struct AlarmRecord | |||||
| { | { | ||||
| std::string definitionId; | std::string definitionId; | ||||
| std::string message; | std::string message; | ||||
| bool active = true; | |||||
| bool acknowledged = false; | bool acknowledged = false; | ||||
| std::chrono::system_clock::time_point occurredAt; | std::chrono::system_clock::time_point occurredAt; | ||||
| std::optional<std::chrono::system_clock::time_point> clearedAt; | |||||
| }; | }; | ||||
| class AlarmService | class AlarmService | ||||
| @@ -34,9 +31,6 @@ public: | |||||
| private: | private: | ||||
| std::optional<bool> evaluate(const AlarmDefinition &definition) const; | std::optional<bool> evaluate(const AlarmDefinition &definition) const; | ||||
| void trimHistory(); | |||||
| static constexpr std::size_t kHistoryLimit = 20; | |||||
| const ProjectService &project_service_; | const ProjectService &project_service_; | ||||
| RegisterRepository &repository_; | RegisterRepository &repository_; | ||||
| @@ -224,7 +224,7 @@ public: | |||||
| rect.height() - kAlarmHeaderHeight), | rect.height() - kAlarmHeaderHeight), | ||||
| Qt::AlignCenter, | Qt::AlignCenter, | ||||
| runtime_active_ ? QObject::tr("暂无报警") | runtime_active_ ? QObject::tr("暂无报警") | ||||
| : QObject::tr("运行时显示报警记录")); | |||||
| : QObject::tr("运行时显示当前报警")); | |||||
| break; | break; | ||||
| } | } | ||||
| for (int row = 0; row < visible_rows; ++row) | for (int row = 0; row < visible_rows; ++row) | ||||
| @@ -237,18 +237,12 @@ public: | |||||
| kAlarmRowHeight); | kAlarmRowHeight); | ||||
| painter->fillRect( | painter->fillRect( | ||||
| row_rect, | row_rect, | ||||
| record.active | |||||
| ? QColor(QStringLiteral("#fde8e8")) | |||||
| : QColor(QStringLiteral("#f0f2f3"))); | |||||
| QColor(QStringLiteral("#fde8e8"))); | |||||
| painter->setPen(QColor(QStringLiteral("#d3d8dc"))); | painter->setPen(QColor(QStringLiteral("#d3d8dc"))); | ||||
| painter->drawLine(row_rect.bottomLeft(), row_rect.bottomRight()); | painter->drawLine(row_rect.bottomLeft(), row_rect.bottomRight()); | ||||
| painter->setPen(record.active | |||||
| ? QColor(QStringLiteral("#8d1f1f")) | |||||
| : QColor(QStringLiteral("#667078"))); | |||||
| const QString state = record.active | |||||
| ? (record.acknowledged ? QObject::tr("已确认") | |||||
| : QObject::tr("未确认")) | |||||
| : QObject::tr("已清除"); | |||||
| painter->setPen(QColor(QStringLiteral("#8d1f1f"))); | |||||
| const QString state = record.acknowledged | |||||
| ? QObject::tr("已确认") : QObject::tr("未确认"); | |||||
| painter->drawText( | painter->drawText( | ||||
| QRectF(row_rect.left() + 5, row_rect.top(), 58, row_rect.height()), | QRectF(row_rect.left() + 5, row_rect.top(), 58, row_rect.height()), | ||||
| Qt::AlignVCenter | Qt::AlignLeft, | Qt::AlignVCenter | Qt::AlignLeft, | ||||
| @@ -341,6 +335,7 @@ public: | |||||
| page_hovered_ = false; | page_hovered_ = false; | ||||
| button_pressed_ = false; | button_pressed_ = false; | ||||
| } | } | ||||
| updateAlarmVisibility(); | |||||
| update(); | update(); | ||||
| } | } | ||||
| @@ -356,6 +351,7 @@ public: | |||||
| void setAlarmRecords(const std::vector<AlarmRecord> &records) | void setAlarmRecords(const std::vector<AlarmRecord> &records) | ||||
| { | { | ||||
| alarm_records_ = records; | alarm_records_ = records; | ||||
| updateAlarmVisibility(); | |||||
| update(); | update(); | ||||
| } | } | ||||
| @@ -394,7 +390,7 @@ protected: | |||||
| if (row >= 0 && row < static_cast<int>(alarm_records_.size())) | if (row >= 0 && row < static_cast<int>(alarm_records_.size())) | ||||
| { | { | ||||
| const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | ||||
| if (record.active && !record.acknowledged && alarm_acknowledge_) | |||||
| if (!record.acknowledged && alarm_acknowledge_) | |||||
| { | { | ||||
| alarm_acknowledge_(record.definitionId); | alarm_acknowledge_(record.definitionId); | ||||
| } | } | ||||
| @@ -501,6 +497,17 @@ protected: | |||||
| } | } | ||||
| private: | private: | ||||
| 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 | ||||
| { | { | ||||
| @@ -631,6 +638,7 @@ void HmiEditorWidget::setRuntimeActive(bool active) | |||||
| if (control_item != nullptr) | if (control_item != nullptr) | ||||
| { | { | ||||
| control_item->setRuntimeValue(false, 0, false); | control_item->setRuntimeValue(false, 0, false); | ||||
| control_item->setAlarmRecords({}); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -34,7 +34,7 @@ void require(bool condition, const std::string &message) | |||||
| } | } | ||||
| } | } | ||||
| const AlarmRecord *findActiveRecord( | |||||
| const AlarmRecord *findRecord( | |||||
| const AlarmService &service, const std::string &definition_id) | const AlarmService &service, const std::string &definition_id) | ||||
| { | { | ||||
| const auto &records = service.records(); | const auto &records = service.records(); | ||||
| @@ -42,7 +42,7 @@ const AlarmRecord *findActiveRecord( | |||||
| records.cbegin(), records.cend(), | records.cbegin(), records.cend(), | ||||
| [&definition_id](const AlarmRecord &candidate) | [&definition_id](const AlarmRecord &candidate) | ||||
| { | { | ||||
| return candidate.definitionId == definition_id && candidate.active; | |||||
| return candidate.definitionId == definition_id; | |||||
| }); | }); | ||||
| return record == records.cend() ? nullptr : &*record; | return record == records.cend() ? nullptr : &*record; | ||||
| } | } | ||||
| @@ -92,29 +92,27 @@ void testAlarmDefinitionsAndRuntimeLifecycle() | |||||
| 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(findActiveRecord(alarm_service, m_result.id) != nullptr | |||||
| && findActiveRecord(alarm_service, high_result.id) != nullptr | |||||
| && findActiveRecord(alarm_service, low_result.id) != nullptr, | |||||
| require(findRecord(alarm_service, m_result.id) != nullptr | |||||
| && findRecord(alarm_service, high_result.id) != nullptr | |||||
| && findRecord(alarm_service, low_result.id) != nullptr, | |||||
| "all three alarm conditions must create active records"); | "all three alarm conditions must create active records"); | ||||
| require(alarm_service.acknowledge(m_result.id) | require(alarm_service.acknowledge(m_result.id) | ||||
| && findActiveRecord(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.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(); | ||||
| require(std::none_of( | |||||
| alarm_service.records().cbegin(), | |||||
| alarm_service.records().cend(), | |||||
| [](const AlarmRecord &record) { return record.active; }), | |||||
| "cleared conditions must move active alarms into history"); | |||||
| require(alarm_service.records().empty(), | |||||
| "cleared conditions must immediately remove active alarms"); | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | ||||
| alarm_service.refresh(); | alarm_service.refresh(); | ||||
| require(findActiveRecord(alarm_service, m_result.id) != nullptr | |||||
| && alarm_service.records().size() == 4, | |||||
| "a repeated alarm must create a new occurrence and keep history"); | |||||
| require(findRecord(alarm_service, m_result.id) != nullptr | |||||
| && alarm_service.records().size() == 1 | |||||
| && !alarm_service.records().front().acknowledged, | |||||
| "a repeated alarm must create a new unacknowledged occurrence"); | |||||
| alarm_service.reset(); | alarm_service.reset(); | ||||
| require(alarm_service.records().empty(), | require(alarm_service.records().empty(), | ||||
| @@ -428,14 +428,20 @@ void testRuntimeAlarmListInteraction() | |||||
| require(alarm_result.succeeded, | require(alarm_result.succeeded, | ||||
| "runtime alarm test must create an M alarm definition"); | "runtime alarm test must create an M alarm definition"); | ||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | |||||
| alarm_service.refresh(); | |||||
| require(alarm_service.records().size() == 1 | |||||
| && alarm_service.records().front().active, | |||||
| "an active M alarm must be available to AlarmList"); | |||||
| HmiEditorWidget view(editor_service, runtime_service, alarm_service); | HmiEditorWidget view(editor_service, runtime_service, alarm_service); | ||||
| view.setPageId(page_id); | view.setPageId(page_id); | ||||
| QGraphicsItem *alarm_item = nullptr; | |||||
| for (QGraphicsItem *item : view.scene()->items()) | |||||
| { | |||||
| if (item->zValue() >= 0.0) | |||||
| { | |||||
| alarm_item = item; | |||||
| break; | |||||
| } | |||||
| } | |||||
| require(alarm_item != nullptr && alarm_item->isVisible(), | |||||
| "AlarmList must remain visible while editing"); | |||||
| view.setEditingEnabled(false); | view.setEditingEnabled(false); | ||||
| view.setRuntimeActive(true); | view.setRuntimeActive(true); | ||||
| view.setRuntimeWriteEnabled(false); | view.setRuntimeWriteEnabled(false); | ||||
| @@ -443,6 +449,17 @@ void testRuntimeAlarmListInteraction() | |||||
| view.resize(900, 560); | view.resize(900, 560); | ||||
| view.show(); | view.show(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| require(!alarm_item->isVisible(), | |||||
| "AlarmList must be hidden when runtime has no active alarm"); | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | |||||
| alarm_service.refresh(); | |||||
| view.refreshRuntimeValues(); | |||||
| QApplication::processEvents(); | |||||
| require(alarm_service.records().size() == 1, | |||||
| "an active M alarm must be available to AlarmList"); | |||||
| require(alarm_item->isVisible() && alarm_item->zValue() > 0.0, | |||||
| "AlarmList must appear above the page for an active alarm"); | |||||
| const QPoint header_position = view.mapFromScene(QPointF( | const QPoint header_position = view.mapFromScene(QPointF( | ||||
| alarm_list->bounds.x + 20.0, alarm_list->bounds.y + 10.0)); | alarm_list->bounds.x + 20.0, alarm_list->bounds.y + 10.0)); | ||||
| @@ -461,9 +478,16 @@ void testRuntimeAlarmListInteraction() | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | ||||
| alarm_service.refresh(); | alarm_service.refresh(); | ||||
| view.refreshRuntimeValues(); | view.refreshRuntimeValues(); | ||||
| require(!alarm_service.records().front().active | |||||
| && alarm_service.records().front().clearedAt.has_value(), | |||||
| "clearing the M condition must retain a cleared history record"); | |||||
| QApplication::processEvents(); | |||||
| require(alarm_service.records().empty(), | |||||
| "clearing the M condition must remove the alarm immediately"); | |||||
| require(!alarm_item->isVisible(), | |||||
| "AlarmList must hide after the final alarm recovers"); | |||||
| view.setRuntimeActive(false); | |||||
| view.setEditingEnabled(true); | |||||
| require(alarm_item->isVisible(), | |||||
| "AlarmList must become visible again after returning to editing"); | |||||
| } | } | ||||
| void testModeActionsControlEditingAvailability() | void testModeActionsControlEditingAvailability() | ||||