| @@ -17,6 +17,7 @@ INCLUDEPATH += src | |||||
| SOURCES += \ | SOURCES += \ | ||||
| src/main.cpp \ | src/main.cpp \ | ||||
| src/ui/main_window.cpp \ | src/ui/main_window.cpp \ | ||||
| src/ui/alarm_configuration_dialog.cpp \ | |||||
| src/ui/plc_connection_dialog.cpp \ | src/ui/plc_connection_dialog.cpp \ | ||||
| src/ui/free_monitor_widget.cpp \ | src/ui/free_monitor_widget.cpp \ | ||||
| src/ui/runtime_monitor_widget.cpp \ | src/ui/runtime_monitor_widget.cpp \ | ||||
| @@ -24,11 +25,14 @@ SOURCES += \ | |||||
| src/domain/register_repository.cpp \ | src/domain/register_repository.cpp \ | ||||
| src/domain/active_register_repository.cpp \ | src/domain/active_register_repository.cpp \ | ||||
| src/domain/register_monitor_model.cpp \ | src/domain/register_monitor_model.cpp \ | ||||
| src/domain/alarm_model.cpp \ | |||||
| src/domain/hmi_model.cpp \ | src/domain/hmi_model.cpp \ | ||||
| src/domain/control_logic_model.cpp \ | src/domain/control_logic_model.cpp \ | ||||
| src/domain/project_model.cpp \ | src/domain/project_model.cpp \ | ||||
| src/domain/runtime_state.cpp \ | src/domain/runtime_state.cpp \ | ||||
| src/services/project_service.cpp \ | src/services/project_service.cpp \ | ||||
| src/services/alarm_editor_service.cpp \ | |||||
| src/services/alarm_service.cpp \ | |||||
| src/services/hmi_editor_service.cpp \ | src/services/hmi_editor_service.cpp \ | ||||
| src/services/hmi_navigation_service.cpp \ | src/services/hmi_navigation_service.cpp \ | ||||
| src/services/logic_editor_service.cpp \ | src/services/logic_editor_service.cpp \ | ||||
| @@ -46,6 +50,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| src/ui/main_window.h \ | src/ui/main_window.h \ | ||||
| src/ui/alarm_configuration_dialog.h \ | |||||
| src/ui/plc_connection_dialog.h \ | src/ui/plc_connection_dialog.h \ | ||||
| src/ui/free_monitor_widget.h \ | src/ui/free_monitor_widget.h \ | ||||
| src/ui/runtime_monitor_widget.h \ | src/ui/runtime_monitor_widget.h \ | ||||
| @@ -53,12 +58,15 @@ HEADERS += \ | |||||
| src/domain/register_repository.h \ | src/domain/register_repository.h \ | ||||
| src/domain/active_register_repository.h \ | src/domain/active_register_repository.h \ | ||||
| src/domain/register_monitor_model.h \ | src/domain/register_monitor_model.h \ | ||||
| src/domain/alarm_model.h \ | |||||
| src/domain/hmi_model.h \ | src/domain/hmi_model.h \ | ||||
| src/domain/control_logic_model.h \ | src/domain/control_logic_model.h \ | ||||
| src/domain/project_model.h \ | src/domain/project_model.h \ | ||||
| src/domain/runtime_state.h \ | src/domain/runtime_state.h \ | ||||
| src/domain/project_storage.h \ | src/domain/project_storage.h \ | ||||
| src/services/project_service.h \ | src/services/project_service.h \ | ||||
| src/services/alarm_editor_service.h \ | |||||
| src/services/alarm_service.h \ | |||||
| src/services/hmi_editor_service.h \ | src/services/hmi_editor_service.h \ | ||||
| src/services/hmi_navigation_service.h \ | src/services/hmi_navigation_service.h \ | ||||
| src/services/logic_editor_service.h \ | src/services/logic_editor_service.h \ | ||||
| @@ -77,6 +85,7 @@ HEADERS += \ | |||||
| FORMS += \ | FORMS += \ | ||||
| src/ui/main_window.ui \ | src/ui/main_window.ui \ | ||||
| src/ui/alarm_configuration_dialog.ui \ | |||||
| src/ui/plc_connection_dialog.ui \ | src/ui/plc_connection_dialog.ui \ | ||||
| src/ui/free_monitor_widget.ui \ | src/ui/free_monitor_widget.ui \ | ||||
| src/ui/runtime_monitor_widget.ui | src/ui/runtime_monitor_widget.ui | ||||
| @@ -0,0 +1,53 @@ | |||||
| #include "alarm_model.h" | |||||
| namespace { | |||||
| void setError(std::string *error, const std::string &message) | |||||
| { | |||||
| if (error != nullptr) | |||||
| { | |||||
| *error = message; | |||||
| } | |||||
| } | |||||
| } // namespace | |||||
| bool AlarmDefinition::validate(std::string *error) const | |||||
| { | |||||
| if (id.empty()) | |||||
| { | |||||
| setError(error, "报警定义 ID 不能为空"); | |||||
| return false; | |||||
| } | |||||
| if (message.empty()) | |||||
| { | |||||
| setError(error, "报警文本不能为空"); | |||||
| return false; | |||||
| } | |||||
| if (!address.isValid()) | |||||
| { | |||||
| setError(error, "报警定义使用了无效地址"); | |||||
| return false; | |||||
| } | |||||
| if (condition == AlarmCondition::MOn) | |||||
| { | |||||
| if (address.area() != RegisterArea::M) | |||||
| { | |||||
| setError(error, "M ON 报警必须使用 M 地址"); | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| if (condition == AlarmCondition::DHigh | |||||
| || condition == AlarmCondition::DLow) | |||||
| { | |||||
| if (address.area() != RegisterArea::D) | |||||
| { | |||||
| setError(error, "D 高限和低限报警必须使用 D 地址"); | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| setError(error, "不支持的报警触发条件"); | |||||
| return false; | |||||
| } | |||||
| @@ -0,0 +1,24 @@ | |||||
| #pragma once | |||||
| #include "register_address.h" | |||||
| #include <cstdint> | |||||
| #include <string> | |||||
| enum class AlarmCondition | |||||
| { | |||||
| MOn, | |||||
| DHigh, | |||||
| DLow | |||||
| }; | |||||
| struct AlarmDefinition | |||||
| { | |||||
| std::string id; | |||||
| RegisterAddress address{RegisterArea::M, 0}; | |||||
| AlarmCondition condition = AlarmCondition::MOn; | |||||
| std::int16_t threshold = 0; | |||||
| std::string message; | |||||
| bool validate(std::string *error = nullptr) const; | |||||
| }; | |||||
| @@ -40,6 +40,7 @@ bool isSupportedControlType(HmiControlType type) | |||||
| case HmiControlType::NumericInput: | case HmiControlType::NumericInput: | ||||
| case HmiControlType::Label: | case HmiControlType::Label: | ||||
| case HmiControlType::PageJump: | case HmiControlType::PageJump: | ||||
| case HmiControlType::AlarmList: | |||||
| return true; | return true; | ||||
| } | } | ||||
| return false; | return false; | ||||
| @@ -95,7 +96,7 @@ bool HmiControl::validate(std::string *error) const | |||||
| } | } | ||||
| if (!supportsRegisterBinding(type) && binding.has_value()) | if (!supportsRegisterBinding(type) && binding.has_value()) | ||||
| { | { | ||||
| setError(error, "文本和页面跳转控件不能绑定寄存器"); | |||||
| setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器"); | |||||
| return false; | return false; | ||||
| } | } | ||||
| if (type == HmiControlType::PageJump) | if (type == HmiControlType::PageJump) | ||||
| @@ -116,7 +117,7 @@ bool HmiControl::validate(std::string *error) const | |||||
| bool HmiControl::isConfigured() const | bool HmiControl::isConfigured() const | ||||
| { | { | ||||
| if (type == HmiControlType::Label) | |||||
| if (type == HmiControlType::Label || type == HmiControlType::AlarmList) | |||||
| { | { | ||||
| return true; | return true; | ||||
| } | } | ||||
| @@ -42,7 +42,8 @@ enum class HmiControlType | |||||
| NumericDisplay, // 数值显示 | NumericDisplay, // 数值显示 | ||||
| NumericInput, // 数值输入 | NumericInput, // 数值输入 | ||||
| Label, // 标签 | Label, // 标签 | ||||
| PageJump // 页面跳转 | |||||
| PageJump, // 页面跳转 | |||||
| AlarmList // 报警列表 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -101,6 +101,18 @@ bool Project::validate(std::string *error) const | |||||
| setError(error, "工程内的控制逻辑名称必须唯一"); | setError(error, "工程内的控制逻辑名称必须唯一"); | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (containsDuplicateId(alarmDefinitions)) | |||||
| { | |||||
| setError(error, "工程内的报警定义 ID 必须唯一"); | |||||
| return false; | |||||
| } | |||||
| for (const AlarmDefinition &definition : alarmDefinitions) | |||||
| { | |||||
| if (!definition.validate(error)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| for (const HmiPage &page : hmiPages) | for (const HmiPage &page : hmiPages) | ||||
| { | { | ||||
| // 工程聚合校验会向下委托页面和控件的完整规则 | // 工程聚合校验会向下委托页面和控件的完整规则 | ||||
| @@ -1,5 +1,6 @@ | |||||
| #pragma once | #pragma once | ||||
| #include "alarm_model.h" | |||||
| #include "control_logic_model.h" | #include "control_logic_model.h" | ||||
| #include "hmi_model.h" | #include "hmi_model.h" | ||||
| @@ -26,6 +27,8 @@ struct Project | |||||
| std::vector<HmiPage> hmiPages; | std::vector<HmiPage> hmiPages; | ||||
| // 进入运行态时显示的初始 HMI 页面 | // 进入运行态时显示的初始 HMI 页面 | ||||
| std::string initialHmiPageId; | std::string initialHmiPageId; | ||||
| // 工程统一管理的报警定义 | |||||
| std::vector<AlarmDefinition> alarmDefinitions; | |||||
| // 工程包含的控制逻辑集合 | // 工程包含的控制逻辑集合 | ||||
| std::vector<ControlLogic> controlLogics; | std::vector<ControlLogic> controlLogics; | ||||
| @@ -282,6 +282,10 @@ QString hmiControlTypeName(HmiControlType type) | |||||
| { | { | ||||
| return QStringLiteral("pageJump"); | return QStringLiteral("pageJump"); | ||||
| } | } | ||||
| case HmiControlType::AlarmList: | |||||
| { | |||||
| return QStringLiteral("alarmList"); | |||||
| } | |||||
| default: | default: | ||||
| { | { | ||||
| return {}; | return {}; | ||||
| @@ -317,6 +321,10 @@ bool parseHmiControlType( | |||||
| { | { | ||||
| *type = HmiControlType::PageJump; | *type = HmiControlType::PageJump; | ||||
| } | } | ||||
| else if (value == "alarmList") | |||||
| { | |||||
| *type = HmiControlType::AlarmList; | |||||
| } | |||||
| else | else | ||||
| { | { | ||||
| return state->fail( | return state->fail( | ||||
| @@ -378,6 +386,98 @@ bool parseHmiButtonOperation( | |||||
| return true; | return true; | ||||
| } | } | ||||
| QString alarmConditionName(AlarmCondition condition) | |||||
| { | |||||
| switch (condition) | |||||
| { | |||||
| case AlarmCondition::MOn: | |||||
| { | |||||
| return QStringLiteral("mOn"); | |||||
| } | |||||
| case AlarmCondition::DHigh: | |||||
| { | |||||
| return QStringLiteral("dHigh"); | |||||
| } | |||||
| case AlarmCondition::DLow: | |||||
| { | |||||
| return QStringLiteral("dLow"); | |||||
| } | |||||
| default: | |||||
| { | |||||
| return {}; | |||||
| } | |||||
| } | |||||
| } | |||||
| bool parseAlarmCondition( | |||||
| const std::string &value, AlarmCondition *condition, ParseState *state) | |||||
| { | |||||
| if (value == "mOn") | |||||
| { | |||||
| *condition = AlarmCondition::MOn; | |||||
| } | |||||
| else if (value == "dHigh") | |||||
| { | |||||
| *condition = AlarmCondition::DHigh; | |||||
| } | |||||
| else if (value == "dLow") | |||||
| { | |||||
| *condition = AlarmCondition::DLow; | |||||
| } | |||||
| else | |||||
| { | |||||
| return state->fail( | |||||
| ProjectStorageError::InvalidField, | |||||
| "不支持的报警触发条件:" + value); | |||||
| } | |||||
| return true; | |||||
| } | |||||
| QJsonObject serializeAlarmDefinition(const AlarmDefinition &definition) | |||||
| { | |||||
| QJsonObject object; | |||||
| object.insert(QStringLiteral("id"), fromUtf8(definition.id)); | |||||
| object.insert(QStringLiteral("address"), serializeAddress(definition.address)); | |||||
| object.insert( | |||||
| QStringLiteral("condition"), alarmConditionName(definition.condition)); | |||||
| object.insert(QStringLiteral("threshold"), definition.threshold); | |||||
| object.insert(QStringLiteral("message"), fromUtf8(definition.message)); | |||||
| return object; | |||||
| } | |||||
| bool parseAlarmDefinition( | |||||
| const QJsonObject &object, | |||||
| const std::string &context, | |||||
| AlarmDefinition *definition, | |||||
| ParseState *state) | |||||
| { | |||||
| QJsonObject address; | |||||
| std::string condition; | |||||
| int threshold = 0; | |||||
| if (!readString(object, "id", context, &definition->id, state) | |||||
| || !readObject(object, "address", context, &address, state) | |||||
| || !readString(object, "condition", context, &condition, state) | |||||
| || !readInt( | |||||
| object, | |||||
| "threshold", | |||||
| context, | |||||
| std::numeric_limits<std::int16_t>::min(), | |||||
| std::numeric_limits<std::int16_t>::max(), | |||||
| &threshold, | |||||
| state) | |||||
| || !readString(object, "message", context, &definition->message, state)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| if (!parseAddress(address, context + ".address", &definition->address, state) | |||||
| || !parseAlarmCondition(condition, &definition->condition, state)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| definition->threshold = static_cast<std::int16_t>(threshold); | |||||
| return true; | |||||
| } | |||||
| // 将 HMI 控件矩形区域序列化为 JSON 对象 | // 将 HMI 控件矩形区域序列化为 JSON 对象 | ||||
| QJsonObject serializeBounds(const HmiRect &bounds) | QJsonObject serializeBounds(const HmiRect &bounds) | ||||
| { | { | ||||
| @@ -1164,6 +1264,12 @@ QJsonObject serializeProject(const Project &project) | |||||
| logics.append(serializeControlLogic(logic)); | logics.append(serializeControlLogic(logic)); | ||||
| } | } | ||||
| QJsonArray alarms; | |||||
| for (const AlarmDefinition &definition : project.alarmDefinitions) | |||||
| { | |||||
| alarms.append(serializeAlarmDefinition(definition)); | |||||
| } | |||||
| QJsonObject object; | QJsonObject object; | ||||
| object.insert(QStringLiteral("formatVersion"), fromUtf8(project.metadata.formatVersion)); | object.insert(QStringLiteral("formatVersion"), fromUtf8(project.metadata.formatVersion)); | ||||
| object.insert(QStringLiteral("id"), fromUtf8(project.metadata.id)); | object.insert(QStringLiteral("id"), fromUtf8(project.metadata.id)); | ||||
| @@ -1171,6 +1277,7 @@ QJsonObject serializeProject(const Project &project) | |||||
| object.insert(QStringLiteral("hmiPages"), pages); | object.insert(QStringLiteral("hmiPages"), pages); | ||||
| object.insert(QStringLiteral("initialHmiPageId"), | object.insert(QStringLiteral("initialHmiPageId"), | ||||
| fromUtf8(project.initialHmiPageId)); | fromUtf8(project.initialHmiPageId)); | ||||
| object.insert(QStringLiteral("alarmDefinitions"), alarms); | |||||
| object.insert(QStringLiteral("controlLogics"), logics); | object.insert(QStringLiteral("controlLogics"), logics); | ||||
| return object; | return object; | ||||
| } | } | ||||
| @@ -1180,6 +1287,7 @@ bool parseProject( | |||||
| const QJsonObject &object, Project *project, ParseState *state) | const QJsonObject &object, Project *project, ParseState *state) | ||||
| { | { | ||||
| QJsonArray pages; | QJsonArray pages; | ||||
| QJsonArray alarms; | |||||
| QJsonArray logics; | QJsonArray logics; | ||||
| if (!readString( | if (!readString( | ||||
| object, | object, | ||||
| @@ -1203,6 +1311,7 @@ bool parseProject( | |||||
| || !readArray(object, "hmiPages", "project", &pages, state) | || !readArray(object, "hmiPages", "project", &pages, state) | ||||
| || !readString(object, "initialHmiPageId", "project", | || !readString(object, "initialHmiPageId", "project", | ||||
| &project->initialHmiPageId, state) | &project->initialHmiPageId, state) | ||||
| || !readArray(object, "alarmDefinitions", "project", &alarms, state) | |||||
| || !readArray(object, "controlLogics", "project", &logics, state)) | || !readArray(object, "controlLogics", "project", &logics, state)) | ||||
| { | { | ||||
| return false; | return false; | ||||
| @@ -1230,6 +1339,27 @@ bool parseProject( | |||||
| project->hmiPages.push_back(std::move(page)); | project->hmiPages.push_back(std::move(page)); | ||||
| } | } | ||||
| project->alarmDefinitions.reserve(static_cast<std::size_t>(alarms.size())); | |||||
| for (int index = 0; index < alarms.size(); ++index) | |||||
| { | |||||
| if (!alarms.at(index).isObject()) | |||||
| { | |||||
| return state->fail( | |||||
| ProjectStorageError::InvalidField, | |||||
| "project.alarmDefinitions 的元素必须是对象"); | |||||
| } | |||||
| AlarmDefinition definition; | |||||
| if (!parseAlarmDefinition( | |||||
| alarms.at(index).toObject(), | |||||
| "project.alarmDefinitions[" + std::to_string(index) + ']', | |||||
| &definition, | |||||
| state)) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| project->alarmDefinitions.push_back(std::move(definition)); | |||||
| } | |||||
| project->controlLogics.reserve(static_cast<std::size_t>(logics.size())); | project->controlLogics.reserve(static_cast<std::size_t>(logics.size())); | ||||
| for (int index = 0; index < logics.size(); ++index) | for (int index = 0; index < logics.size(); ++index) | ||||
| { | { | ||||
| @@ -14,6 +14,8 @@ | |||||
| #include "domain/active_register_repository.h" | #include "domain/active_register_repository.h" | ||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/alarm_editor_service.h" | |||||
| #include "services/alarm_service.h" | |||||
| #include "services/hmi_navigation_service.h" | #include "services/hmi_navigation_service.h" | ||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| @@ -40,6 +42,8 @@ int main(int argc, char *argv[]) | |||||
| ActiveRegisterRepository active_register_repository(virtual_register_repository); | ActiveRegisterRepository active_register_repository(virtual_register_repository); | ||||
| PlcCommunicationService plc_communication_service(plc_register_repository); | PlcCommunicationService plc_communication_service(plc_register_repository); | ||||
| HmiRuntimeService hmi_runtime_service(active_register_repository); | HmiRuntimeService hmi_runtime_service(active_register_repository); | ||||
| AlarmEditorService alarm_editor_service(project_service); | |||||
| AlarmService alarm_service(project_service, active_register_repository); | |||||
| RegisterMonitorService register_monitor_service(active_register_repository); | RegisterMonitorService register_monitor_service(active_register_repository); | ||||
| OfflineSimulationService offline_simulation_service(virtual_register_repository); | OfflineSimulationService offline_simulation_service(virtual_register_repository); | ||||
| RuntimeModeService runtime_mode_service( | RuntimeModeService runtime_mode_service( | ||||
| @@ -55,6 +59,8 @@ int main(int argc, char *argv[]) | |||||
| hmi_editor_service, | hmi_editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| hmi_runtime_service, | hmi_runtime_service, | ||||
| alarm_editor_service, | |||||
| alarm_service, | |||||
| hmi_navigation_service, | hmi_navigation_service, | ||||
| register_monitor_service); | register_monitor_service); | ||||
| main_window.show(); | main_window.show(); | ||||
| @@ -0,0 +1,103 @@ | |||||
| #include "alarm_editor_service.h" | |||||
| #include "project_service.h" | |||||
| #include <algorithm> | |||||
| namespace { | |||||
| AlarmEditorResult failure(AlarmEditorError error, const std::string &message) | |||||
| { | |||||
| return {false, error, message, {}}; | |||||
| } | |||||
| } // namespace | |||||
| AlarmEditorService::AlarmEditorService(ProjectService &project_service) | |||||
| : project_service_(project_service) | |||||
| { | |||||
| } | |||||
| const std::vector<AlarmDefinition> &AlarmEditorService::definitions() const | |||||
| { | |||||
| return project_service_.project().alarmDefinitions; | |||||
| } | |||||
| const AlarmDefinition *AlarmEditorService::findDefinition( | |||||
| const std::string &id) const | |||||
| { | |||||
| const auto &definitions = project_service_.project().alarmDefinitions; | |||||
| const auto definition = std::find_if( | |||||
| definitions.cbegin(), definitions.cend(), | |||||
| [&id](const AlarmDefinition &candidate) { return candidate.id == id; }); | |||||
| return definition == definitions.cend() ? nullptr : &*definition; | |||||
| } | |||||
| AlarmEditorResult AlarmEditorService::addDefinition( | |||||
| const AlarmDefinition &definition) | |||||
| { | |||||
| AlarmDefinition candidate = definition; | |||||
| candidate.id = makeUniqueId(); | |||||
| std::string error; | |||||
| if (!candidate.validate(&error)) | |||||
| { | |||||
| return failure(AlarmEditorError::InvalidDefinition, error); | |||||
| } | |||||
| project_service_.editProject().alarmDefinitions.push_back(candidate); | |||||
| return {true, AlarmEditorError::None, {}, candidate.id}; | |||||
| } | |||||
| AlarmEditorResult AlarmEditorService::updateDefinition( | |||||
| const std::string &id, const AlarmDefinition &definition) | |||||
| { | |||||
| if (findDefinition(id) == nullptr) | |||||
| { | |||||
| return failure(AlarmEditorError::NotFound, "未找到报警定义"); | |||||
| } | |||||
| AlarmDefinition candidate = definition; | |||||
| candidate.id = id; | |||||
| std::string error; | |||||
| if (!candidate.validate(&error)) | |||||
| { | |||||
| return failure(AlarmEditorError::InvalidDefinition, error); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| auto editable = std::find_if( | |||||
| project.alarmDefinitions.begin(), project.alarmDefinitions.end(), | |||||
| [&id](const AlarmDefinition &item) { return item.id == id; }); | |||||
| *editable = std::move(candidate); | |||||
| return {true, AlarmEditorError::None, {}, id}; | |||||
| } | |||||
| AlarmEditorResult AlarmEditorService::removeDefinition(const std::string &id) | |||||
| { | |||||
| if (findDefinition(id) == nullptr) | |||||
| { | |||||
| return failure(AlarmEditorError::NotFound, "未找到报警定义"); | |||||
| } | |||||
| Project &project = project_service_.editProject(); | |||||
| project.alarmDefinitions.erase( | |||||
| std::remove_if( | |||||
| project.alarmDefinitions.begin(), project.alarmDefinitions.end(), | |||||
| [&id](const AlarmDefinition &definition) | |||||
| { | |||||
| return definition.id == id; | |||||
| }), | |||||
| project.alarmDefinitions.end()); | |||||
| return {true, AlarmEditorError::None, {}, id}; | |||||
| } | |||||
| std::string AlarmEditorService::makeUniqueId() const | |||||
| { | |||||
| int suffix = 1; | |||||
| while (true) | |||||
| { | |||||
| const std::string candidate = "alarm-" + std::to_string(suffix); | |||||
| if (findDefinition(candidate) == nullptr) | |||||
| { | |||||
| return candidate; | |||||
| } | |||||
| ++suffix; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,42 @@ | |||||
| #pragma once | |||||
| #include "domain/alarm_model.h" | |||||
| #include <string> | |||||
| #include <vector> | |||||
| class ProjectService; | |||||
| enum class AlarmEditorError | |||||
| { | |||||
| None, | |||||
| NotFound, | |||||
| DuplicateId, | |||||
| InvalidDefinition | |||||
| }; | |||||
| struct AlarmEditorResult | |||||
| { | |||||
| bool succeeded = false; | |||||
| AlarmEditorError error = AlarmEditorError::None; | |||||
| std::string message; | |||||
| std::string id; | |||||
| }; | |||||
| class AlarmEditorService | |||||
| { | |||||
| public: | |||||
| explicit AlarmEditorService(ProjectService &project_service); | |||||
| const std::vector<AlarmDefinition> &definitions() const; | |||||
| const AlarmDefinition *findDefinition(const std::string &id) const; | |||||
| AlarmEditorResult addDefinition(const AlarmDefinition &definition); | |||||
| AlarmEditorResult updateDefinition( | |||||
| const std::string &id, const AlarmDefinition &definition); | |||||
| AlarmEditorResult removeDefinition(const std::string &id); | |||||
| private: | |||||
| std::string makeUniqueId() const; | |||||
| ProjectService &project_service_; | |||||
| }; | |||||
| @@ -0,0 +1,114 @@ | |||||
| #include "alarm_service.h" | |||||
| #include "domain/alarm_model.h" | |||||
| #include "domain/register_repository.h" | |||||
| #include "project_service.h" | |||||
| #include <algorithm> | |||||
| AlarmService::AlarmService( | |||||
| const ProjectService &project_service, | |||||
| RegisterRepository &repository) | |||||
| : project_service_(project_service), | |||||
| repository_(repository) | |||||
| { | |||||
| } | |||||
| void AlarmService::refresh() | |||||
| { | |||||
| const auto now = std::chrono::system_clock::now(); | |||||
| for (const AlarmDefinition &definition | |||||
| : project_service_.project().alarmDefinitions) | |||||
| { | |||||
| const std::optional<bool> active = evaluate(definition); | |||||
| if (!active.has_value()) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| const auto current = std::find_if( | |||||
| records_.begin(), records_.end(), | |||||
| [&definition](const AlarmRecord &record) | |||||
| { | |||||
| return record.definitionId == definition.id && record.active; | |||||
| }); | |||||
| if (*active && current == records_.end()) | |||||
| { | |||||
| records_.insert( | |||||
| records_.begin(), | |||||
| AlarmRecord{definition.id, definition.message, true, false, now, {}}); | |||||
| } | |||||
| else if (!*active && current != records_.end()) | |||||
| { | |||||
| current->active = false; | |||||
| current->clearedAt = now; | |||||
| } | |||||
| } | |||||
| trimHistory(); | |||||
| } | |||||
| bool AlarmService::acknowledge(const std::string &definition_id) | |||||
| { | |||||
| const auto record = std::find_if( | |||||
| records_.begin(), records_.end(), | |||||
| [&definition_id](const AlarmRecord &candidate) | |||||
| { | |||||
| return candidate.definitionId == definition_id && candidate.active; | |||||
| }); | |||||
| if (record == records_.end()) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| record->acknowledged = true; | |||||
| return true; | |||||
| } | |||||
| void AlarmService::reset() | |||||
| { | |||||
| records_.clear(); | |||||
| } | |||||
| const std::vector<AlarmRecord> &AlarmService::records() const | |||||
| { | |||||
| return records_; | |||||
| } | |||||
| std::optional<bool> AlarmService::evaluate( | |||||
| const AlarmDefinition &definition) const | |||||
| { | |||||
| if (definition.condition == AlarmCondition::MOn) | |||||
| { | |||||
| const BitReadResult value = repository_.readBit(definition.address); | |||||
| return value.succeeded ? std::optional<bool>{value.value} : std::nullopt; | |||||
| } | |||||
| const WordReadResult value = repository_.readWord(definition.address); | |||||
| if (!value.succeeded) | |||||
| { | |||||
| return std::nullopt; | |||||
| } | |||||
| if (definition.condition == AlarmCondition::DHigh) | |||||
| { | |||||
| return value.value >= definition.threshold; | |||||
| } | |||||
| if (definition.condition == AlarmCondition::DLow) | |||||
| { | |||||
| return value.value <= definition.threshold; | |||||
| } | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| #pragma once | |||||
| #include <chrono> | |||||
| #include <cstddef> | |||||
| #include <optional> | |||||
| #include <string> | |||||
| #include <vector> | |||||
| class ProjectService; | |||||
| class RegisterRepository; | |||||
| struct AlarmDefinition; | |||||
| struct AlarmRecord | |||||
| { | |||||
| std::string definitionId; | |||||
| std::string message; | |||||
| bool active = true; | |||||
| bool acknowledged = false; | |||||
| std::chrono::system_clock::time_point occurredAt; | |||||
| std::optional<std::chrono::system_clock::time_point> clearedAt; | |||||
| }; | |||||
| class AlarmService | |||||
| { | |||||
| public: | |||||
| AlarmService( | |||||
| const ProjectService &project_service, | |||||
| RegisterRepository &repository); | |||||
| void refresh(); | |||||
| bool acknowledge(const std::string &definition_id); | |||||
| void reset(); | |||||
| const std::vector<AlarmRecord> &records() const; | |||||
| private: | |||||
| std::optional<bool> evaluate(const AlarmDefinition &definition) const; | |||||
| void trimHistory(); | |||||
| static constexpr std::size_t kHistoryLimit = 20; | |||||
| const ProjectService &project_service_; | |||||
| RegisterRepository &repository_; | |||||
| std::vector<AlarmRecord> records_; | |||||
| }; | |||||
| @@ -45,6 +45,10 @@ std::string controlPrefix(HmiControlType type) | |||||
| { | { | ||||
| return "page-jump"; | return "page-jump"; | ||||
| } | } | ||||
| case HmiControlType::AlarmList: | |||||
| { | |||||
| return "alarm-list"; | |||||
| } | |||||
| default: | default: | ||||
| { | { | ||||
| return "control"; | return "control"; | ||||
| @@ -80,6 +84,10 @@ std::string defaultText(HmiControlType type) | |||||
| { | { | ||||
| return "页面跳转"; | return "页面跳转"; | ||||
| } | } | ||||
| case HmiControlType::AlarmList: | |||||
| { | |||||
| return "报警信息"; | |||||
| } | |||||
| default: | default: | ||||
| { | { | ||||
| return "控件"; | return "控件"; | ||||
| @@ -484,6 +492,7 @@ bool HmiEditorService::validateEditableControl( | |||||
| case HmiControlType::NumericInput: | case HmiControlType::NumericInput: | ||||
| case HmiControlType::Label: | case HmiControlType::Label: | ||||
| case HmiControlType::PageJump: | case HmiControlType::PageJump: | ||||
| case HmiControlType::AlarmList: | |||||
| break; | break; | ||||
| default: | default: | ||||
| setError(error, "不支持的 HMI 控件类型"); | setError(error, "不支持的 HMI 控件类型"); | ||||
| @@ -533,10 +542,11 @@ bool HmiEditorService::validateEditableControl( | |||||
| return false; | return false; | ||||
| } | } | ||||
| if ((control.type == HmiControlType::Label | if ((control.type == HmiControlType::Label | ||||
| || control.type == HmiControlType::PageJump) | |||||
| || control.type == HmiControlType::PageJump | |||||
| || control.type == HmiControlType::AlarmList) | |||||
| && control.binding.has_value()) | && control.binding.has_value()) | ||||
| { | { | ||||
| setError(error, "文本和页面跳转控件不能绑定寄存器"); | |||||
| setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器"); | |||||
| return false; | return false; | ||||
| } | } | ||||
| if (control.type == HmiControlType::PageJump) | if (control.type == HmiControlType::PageJump) | ||||
| @@ -582,8 +592,16 @@ HmiControl HmiEditorService::makeControl( | |||||
| control.id = makeUniqueId(page, controlPrefix(type)); | control.id = makeUniqueId(page, controlPrefix(type)); | ||||
| control.type = type; | control.type = type; | ||||
| control.text = defaultText(type); | control.text = defaultText(type); | ||||
| control.bounds.width = type == HmiControlType::Indicator ? 64 : 120; | |||||
| control.bounds.height = type == HmiControlType::Indicator ? 64 : 40; | |||||
| if (type == HmiControlType::AlarmList) | |||||
| { | |||||
| control.bounds.width = std::min(360, page.width); | |||||
| control.bounds.height = std::min(180, page.height); | |||||
| } | |||||
| else | |||||
| { | |||||
| control.bounds.width = type == HmiControlType::Indicator ? 64 : 120; | |||||
| control.bounds.height = type == HmiControlType::Indicator ? 64 : 40; | |||||
| } | |||||
| if (type == HmiControlType::PageJump) | if (type == HmiControlType::PageJump) | ||||
| { | { | ||||
| control.pageJump = HmiPageJumpConfig{}; | control.pageJump = HmiPageJumpConfig{}; | ||||
| @@ -219,6 +219,10 @@ void RuntimeModeService::refreshPlcPollAddresses() | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| for (const AlarmDefinition &definition : project.alarmDefinitions) | |||||
| { | |||||
| addresses.push_back(definition.address); | |||||
| } | |||||
| for (const ControlLogic &logic : project.controlLogics) | for (const ControlLogic &logic : project.controlLogics) | ||||
| { | { | ||||
| for (const LadderRung &rung : logic.rungs) | for (const LadderRung &rung : logic.rungs) | ||||
| @@ -0,0 +1,245 @@ | |||||
| #include "alarm_configuration_dialog.h" | |||||
| #include "services/alarm_editor_service.h" | |||||
| #include "ui_alarm_configuration_dialog.h" | |||||
| #include <QHeaderView> | |||||
| #include <QMessageBox> | |||||
| #include <QTableWidgetItem> | |||||
| namespace { | |||||
| QString fromUtf8(const std::string &value) | |||||
| { | |||||
| return QString::fromUtf8(value.data(), static_cast<int>(value.size())); | |||||
| } | |||||
| std::string toUtf8(const QString &value) | |||||
| { | |||||
| const QByteArray bytes = value.toUtf8(); | |||||
| return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | |||||
| } | |||||
| QString conditionText(AlarmCondition condition) | |||||
| { | |||||
| switch (condition) | |||||
| { | |||||
| case AlarmCondition::MOn: | |||||
| { | |||||
| return AlarmConfigurationDialog::tr("M 为 ON"); | |||||
| } | |||||
| case AlarmCondition::DHigh: | |||||
| { | |||||
| return AlarmConfigurationDialog::tr("D 大于等于阈值"); | |||||
| } | |||||
| case AlarmCondition::DLow: | |||||
| { | |||||
| return AlarmConfigurationDialog::tr("D 小于等于阈值"); | |||||
| } | |||||
| default: | |||||
| { | |||||
| return {}; | |||||
| } | |||||
| } | |||||
| } | |||||
| } // namespace | |||||
| AlarmConfigurationDialog::AlarmConfigurationDialog( | |||||
| AlarmEditorService &service, | |||||
| QWidget *parent) | |||||
| : QDialog(parent), | |||||
| ui_(std::make_unique<Ui::AlarmConfigurationDialog>()), | |||||
| service_(service) | |||||
| { | |||||
| ui_->setupUi(this); | |||||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | |||||
| 0, QHeaderView::ResizeToContents); | |||||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | |||||
| 1, QHeaderView::ResizeToContents); | |||||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | |||||
| 2, QHeaderView::ResizeToContents); | |||||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | |||||
| 3, QHeaderView::ResizeToContents); | |||||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | |||||
| 4, QHeaderView::Stretch); | |||||
| ui_->areaComboBox->setItemData(0, static_cast<int>(RegisterArea::M)); | |||||
| ui_->areaComboBox->setItemData(1, static_cast<int>(RegisterArea::D)); | |||||
| connect(ui_->areaComboBox, | |||||
| QOverload<int>::of(&QComboBox::currentIndexChanged), | |||||
| this, | |||||
| [this] { updateConditionOptions(); }); | |||||
| connect(ui_->alarmTable, &QTableWidget::itemSelectionChanged, | |||||
| this, [this] { loadSelectedDefinition(); }); | |||||
| connect(ui_->addButton, &QPushButton::clicked, | |||||
| this, [this] { addDefinition(); }); | |||||
| connect(ui_->updateButton, &QPushButton::clicked, | |||||
| this, [this] { updateDefinition(); }); | |||||
| connect(ui_->removeButton, &QPushButton::clicked, | |||||
| this, [this] { removeDefinition(); }); | |||||
| connect(ui_->buttonBox, &QDialogButtonBox::rejected, | |||||
| this, &QDialog::reject); | |||||
| updateConditionOptions(); | |||||
| reloadDefinitions(); | |||||
| } | |||||
| AlarmConfigurationDialog::~AlarmConfigurationDialog() = default; | |||||
| void AlarmConfigurationDialog::reloadDefinitions(const std::string &selected_id) | |||||
| { | |||||
| const auto &definitions = service_.definitions(); | |||||
| ui_->alarmTable->setRowCount(static_cast<int>(definitions.size())); | |||||
| int selected_row = -1; | |||||
| for (std::size_t index = 0; index < definitions.size(); ++index) | |||||
| { | |||||
| const AlarmDefinition &definition = definitions[index]; | |||||
| const int row = static_cast<int>(index); | |||||
| auto *id_item = new QTableWidgetItem(fromUtf8(definition.id)); | |||||
| id_item->setData(Qt::UserRole, fromUtf8(definition.id)); | |||||
| ui_->alarmTable->setItem(row, 0, id_item); | |||||
| ui_->alarmTable->setItem( | |||||
| row, 1, new QTableWidgetItem(QString::fromStdString( | |||||
| definition.address.toString()))); | |||||
| ui_->alarmTable->setItem( | |||||
| row, 2, new QTableWidgetItem(conditionText(definition.condition))); | |||||
| ui_->alarmTable->setItem( | |||||
| row, | |||||
| 3, | |||||
| new QTableWidgetItem( | |||||
| definition.condition == AlarmCondition::MOn | |||||
| ? QStringLiteral("-") | |||||
| : QString::number(definition.threshold))); | |||||
| ui_->alarmTable->setItem( | |||||
| row, 4, new QTableWidgetItem(fromUtf8(definition.message))); | |||||
| if (definition.id == selected_id) | |||||
| { | |||||
| selected_row = row; | |||||
| } | |||||
| } | |||||
| if (selected_row >= 0) | |||||
| { | |||||
| ui_->alarmTable->selectRow(selected_row); | |||||
| } | |||||
| else | |||||
| { | |||||
| ui_->alarmTable->clearSelection(); | |||||
| ui_->updateButton->setEnabled(false); | |||||
| ui_->removeButton->setEnabled(false); | |||||
| } | |||||
| } | |||||
| void AlarmConfigurationDialog::loadSelectedDefinition() | |||||
| { | |||||
| const AlarmDefinition *definition = service_.findDefinition(selectedId()); | |||||
| const bool selected = definition != nullptr; | |||||
| ui_->updateButton->setEnabled(selected); | |||||
| ui_->removeButton->setEnabled(selected); | |||||
| if (!selected) | |||||
| { | |||||
| return; | |||||
| } | |||||
| ui_->areaComboBox->setCurrentIndex( | |||||
| definition->address.area() == RegisterArea::M ? 0 : 1); | |||||
| updateConditionOptions(); | |||||
| ui_->conditionComboBox->setCurrentIndex( | |||||
| ui_->conditionComboBox->findData( | |||||
| static_cast<int>(definition->condition))); | |||||
| ui_->indexSpinBox->setValue(definition->address.index()); | |||||
| ui_->thresholdSpinBox->setValue(definition->threshold); | |||||
| ui_->messageEdit->setText(fromUtf8(definition->message)); | |||||
| } | |||||
| void AlarmConfigurationDialog::updateConditionOptions() | |||||
| { | |||||
| const RegisterArea area = static_cast<RegisterArea>( | |||||
| ui_->areaComboBox->currentData().toInt()); | |||||
| ui_->conditionComboBox->clear(); | |||||
| if (area == RegisterArea::M) | |||||
| { | |||||
| ui_->conditionComboBox->addItem( | |||||
| conditionText(AlarmCondition::MOn), | |||||
| static_cast<int>(AlarmCondition::MOn)); | |||||
| } | |||||
| else | |||||
| { | |||||
| ui_->conditionComboBox->addItem( | |||||
| conditionText(AlarmCondition::DHigh), | |||||
| static_cast<int>(AlarmCondition::DHigh)); | |||||
| ui_->conditionComboBox->addItem( | |||||
| conditionText(AlarmCondition::DLow), | |||||
| static_cast<int>(AlarmCondition::DLow)); | |||||
| } | |||||
| ui_->thresholdLabel->setEnabled(area == RegisterArea::D); | |||||
| ui_->thresholdSpinBox->setEnabled(area == RegisterArea::D); | |||||
| } | |||||
| void AlarmConfigurationDialog::addDefinition() | |||||
| { | |||||
| const AlarmEditorResult result = service_.addDefinition(definitionFromInputs()); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| QMessageBox::warning(this, tr("新增报警"), fromUtf8(result.message)); | |||||
| return; | |||||
| } | |||||
| reloadDefinitions(result.id); | |||||
| } | |||||
| void AlarmConfigurationDialog::updateDefinition() | |||||
| { | |||||
| const std::string id = selectedId(); | |||||
| if (id.empty()) | |||||
| { | |||||
| return; | |||||
| } | |||||
| const AlarmEditorResult result = service_.updateDefinition( | |||||
| id, definitionFromInputs()); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| QMessageBox::warning(this, tr("更新报警"), fromUtf8(result.message)); | |||||
| return; | |||||
| } | |||||
| reloadDefinitions(result.id); | |||||
| } | |||||
| void AlarmConfigurationDialog::removeDefinition() | |||||
| { | |||||
| const std::string id = selectedId(); | |||||
| if (id.empty()) | |||||
| { | |||||
| return; | |||||
| } | |||||
| if (QMessageBox::question( | |||||
| this, tr("删除报警"), tr("确定删除当前报警定义吗?")) | |||||
| != QMessageBox::Yes) | |||||
| { | |||||
| return; | |||||
| } | |||||
| const AlarmEditorResult result = service_.removeDefinition(id); | |||||
| if (!result.succeeded) | |||||
| { | |||||
| QMessageBox::warning(this, tr("删除报警"), fromUtf8(result.message)); | |||||
| return; | |||||
| } | |||||
| reloadDefinitions(); | |||||
| } | |||||
| AlarmDefinition AlarmConfigurationDialog::definitionFromInputs() const | |||||
| { | |||||
| AlarmDefinition definition; | |||||
| const RegisterArea area = static_cast<RegisterArea>( | |||||
| ui_->areaComboBox->currentData().toInt()); | |||||
| definition.address = RegisterAddress{area, ui_->indexSpinBox->value()}; | |||||
| definition.condition = static_cast<AlarmCondition>( | |||||
| ui_->conditionComboBox->currentData().toInt()); | |||||
| definition.threshold = static_cast<std::int16_t>( | |||||
| ui_->thresholdSpinBox->value()); | |||||
| definition.message = toUtf8(ui_->messageEdit->text().trimmed()); | |||||
| return definition; | |||||
| } | |||||
| std::string AlarmConfigurationDialog::selectedId() const | |||||
| { | |||||
| const int row = ui_->alarmTable->currentRow(); | |||||
| const QTableWidgetItem *item = row >= 0 ? ui_->alarmTable->item(row, 0) : nullptr; | |||||
| return item == nullptr ? std::string{} : toUtf8(item->data(Qt::UserRole).toString()); | |||||
| } | |||||
| @@ -0,0 +1,35 @@ | |||||
| #pragma once | |||||
| #include <QDialog> | |||||
| #include <memory> | |||||
| #include <string> | |||||
| namespace Ui { | |||||
| class AlarmConfigurationDialog; | |||||
| } | |||||
| class AlarmEditorService; | |||||
| struct AlarmDefinition; | |||||
| class AlarmConfigurationDialog final : public QDialog | |||||
| { | |||||
| public: | |||||
| explicit AlarmConfigurationDialog( | |||||
| AlarmEditorService &service, | |||||
| QWidget *parent = nullptr); | |||||
| ~AlarmConfigurationDialog() override; | |||||
| private: | |||||
| void reloadDefinitions(const std::string &selected_id = {}); | |||||
| void loadSelectedDefinition(); | |||||
| void updateConditionOptions(); | |||||
| void addDefinition(); | |||||
| void updateDefinition(); | |||||
| void removeDefinition(); | |||||
| AlarmDefinition definitionFromInputs() const; | |||||
| std::string selectedId() const; | |||||
| std::unique_ptr<Ui::AlarmConfigurationDialog> ui_; | |||||
| AlarmEditorService &service_; | |||||
| }; | |||||
| @@ -0,0 +1,49 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <ui version="4.0"> | |||||
| <class>AlarmConfigurationDialog</class> | |||||
| <widget class="QDialog" name="AlarmConfigurationDialog"> | |||||
| <property name="windowTitle"><string>报警配置</string></property> | |||||
| <property name="minimumSize"><size><width>720</width><height>430</height></size></property> | |||||
| <layout class="QVBoxLayout" name="verticalLayout"> | |||||
| <item> | |||||
| <widget class="QTableWidget" name="alarmTable"> | |||||
| <property name="selectionBehavior"><enum>QAbstractItemView::SelectRows</enum></property> | |||||
| <property name="selectionMode"><enum>QAbstractItemView::SingleSelection</enum></property> | |||||
| <property name="editTriggers"><set>QAbstractItemView::NoEditTriggers</set></property> | |||||
| <property name="columnCount"><number>5</number></property> | |||||
| <column><property name="text"><string>ID</string></property></column> | |||||
| <column><property name="text"><string>地址</string></property></column> | |||||
| <column><property name="text"><string>条件</string></property></column> | |||||
| <column><property name="text"><string>阈值</string></property></column> | |||||
| <column><property name="text"><string>报警文本</string></property></column> | |||||
| </widget> | |||||
| </item> | |||||
| <item> | |||||
| <layout class="QFormLayout" name="alarmForm"> | |||||
| <property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property> | |||||
| <item row="0" column="0"><widget class="QLabel" name="areaLabel"><property name="text"><string>地址区域</string></property></widget></item> | |||||
| <item row="0" column="1"><widget class="QComboBox" name="areaComboBox"><item><property name="text"><string>M</string></property></item><item><property name="text"><string>D</string></property></item></widget></item> | |||||
| <item row="1" column="0"><widget class="QLabel" name="indexLabel"><property name="text"><string>地址</string></property></widget></item> | |||||
| <item row="1" column="1"><widget class="QSpinBox" name="indexSpinBox"><property name="maximum"><number>4000</number></property></widget></item> | |||||
| <item row="2" column="0"><widget class="QLabel" name="conditionLabel"><property name="text"><string>触发条件</string></property></widget></item> | |||||
| <item row="2" column="1"><widget class="QComboBox" name="conditionComboBox"/></item> | |||||
| <item row="3" column="0"><widget class="QLabel" name="thresholdLabel"><property name="text"><string>阈值</string></property></widget></item> | |||||
| <item row="3" column="1"><widget class="QSpinBox" name="thresholdSpinBox"><property name="minimum"><number>-32768</number></property><property name="maximum"><number>32767</number></property></widget></item> | |||||
| <item row="4" column="0"><widget class="QLabel" name="messageLabel"><property name="text"><string>报警文本</string></property></widget></item> | |||||
| <item row="4" column="1"><widget class="QLineEdit" name="messageEdit"><property name="placeholderText"><string>例如:温度过高</string></property></widget></item> | |||||
| </layout> | |||||
| </item> | |||||
| <item> | |||||
| <layout class="QHBoxLayout" name="operationLayout"> | |||||
| <item><widget class="QPushButton" name="addButton"><property name="text"><string>新增</string></property></widget></item> | |||||
| <item><widget class="QPushButton" name="updateButton"><property name="text"><string>更新</string></property></widget></item> | |||||
| <item><widget class="QPushButton" name="removeButton"><property name="text"><string>删除</string></property></widget></item> | |||||
| <item><spacer name="operationSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item> | |||||
| <item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::Close</set></property></widget></item> | |||||
| </layout> | |||||
| </item> | |||||
| </layout> | |||||
| </widget> | |||||
| <resources/> | |||||
| <connections/> | |||||
| </ui> | |||||
| @@ -2,7 +2,9 @@ | |||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/alarm_service.h" | |||||
| #include <QDateTime> | |||||
| #include <QGraphicsItem> | #include <QGraphicsItem> | ||||
| #include <QGraphicsRectItem> | #include <QGraphicsRectItem> | ||||
| #include <QGraphicsScene> | #include <QGraphicsScene> | ||||
| @@ -21,6 +23,16 @@ | |||||
| namespace { | namespace { | ||||
| constexpr qreal kAlarmHeaderHeight = 24.0; | |||||
| constexpr qreal kAlarmRowHeight = 22.0; | |||||
| QString alarmTimeText(const std::chrono::system_clock::time_point &time) | |||||
| { | |||||
| const auto seconds = std::chrono::duration_cast<std::chrono::seconds>( | |||||
| time.time_since_epoch()).count(); | |||||
| return QDateTime::fromSecsSinceEpoch(seconds).toString(QStringLiteral("HH:mm:ss")); | |||||
| } | |||||
| // 将领域层 HMI 控件投影为可绘制、可选择和可交互的场景图元 | // 将领域层 HMI 控件投影为可绘制、可选择和可交互的场景图元 | ||||
| class HmiGraphicsItem final : public QGraphicsItem | class HmiGraphicsItem final : public QGraphicsItem | ||||
| { | { | ||||
| @@ -33,14 +45,16 @@ public: | |||||
| std::function<void(const std::string &, const QPointF &)> moved, | std::function<void(const std::string &, const QPointF &)> moved, | ||||
| std::function<void(const std::string &, HmiButtonEvent)> button_event, | std::function<void(const std::string &, HmiButtonEvent)> button_event, | ||||
| std::function<void(const std::string &)> numeric_input_activated, | std::function<void(const std::string &)> numeric_input_activated, | ||||
| std::function<void(const std::string &)> page_navigation) | |||||
| std::function<void(const std::string &)> page_navigation, | |||||
| std::function<void(const std::string &)> alarm_acknowledge) | |||||
| : 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)), | ||||
| button_event_(std::move(button_event)), | button_event_(std::move(button_event)), | ||||
| numeric_input_activated_(std::move(numeric_input_activated)), | numeric_input_activated_(std::move(numeric_input_activated)), | ||||
| page_navigation_(std::move(page_navigation)) | |||||
| page_navigation_(std::move(page_navigation)), | |||||
| alarm_acknowledge_(std::move(alarm_acknowledge)) | |||||
| { | { | ||||
| // 领域坐标直接作为图元在场景中的初始位置 | // 领域坐标直接作为图元在场景中的初始位置 | ||||
| setPos(control_.bounds.x, control_.bounds.y); | setPos(control_.bounds.x, control_.bounds.y); | ||||
| @@ -178,6 +192,83 @@ public: | |||||
| painter->drawText(rect, Qt::AlignCenter, textWithValue()); | painter->drawText(rect, Qt::AlignCenter, textWithValue()); | ||||
| break; | break; | ||||
| } | } | ||||
| case HmiControlType::AlarmList: | |||||
| { | |||||
| painter->setPen(QPen(QColor(QStringLiteral("#9b3a3a")), 1)); | |||||
| painter->setBrush(QColor(QStringLiteral("#ffffff"))); | |||||
| painter->drawRect(rect); | |||||
| const QRectF header( | |||||
| rect.left(), rect.top(), rect.width(), kAlarmHeaderHeight); | |||||
| painter->fillRect(header, QColor(QStringLiteral("#a63f3f"))); | |||||
| painter->setPen(Qt::white); | |||||
| painter->drawText( | |||||
| header.adjusted(7, 0, -7, 0), | |||||
| Qt::AlignVCenter | Qt::AlignLeft, | |||||
| QString::fromUtf8( | |||||
| control_.text.data(), static_cast<int>(control_.text.size()))); | |||||
| const int maximum_rows = std::max( | |||||
| 0, | |||||
| static_cast<int>((rect.height() - kAlarmHeaderHeight) | |||||
| / kAlarmRowHeight)); | |||||
| const int visible_rows = std::min( | |||||
| maximum_rows, static_cast<int>(alarm_records_.size())); | |||||
| if (visible_rows == 0) | |||||
| { | |||||
| painter->setPen(QColor(QStringLiteral("#6f7a82"))); | |||||
| painter->drawText( | |||||
| QRectF( | |||||
| rect.left(), | |||||
| header.bottom(), | |||||
| rect.width(), | |||||
| rect.height() - kAlarmHeaderHeight), | |||||
| Qt::AlignCenter, | |||||
| runtime_active_ ? QObject::tr("暂无报警") | |||||
| : QObject::tr("运行时显示报警记录")); | |||||
| break; | |||||
| } | |||||
| for (int row = 0; row < visible_rows; ++row) | |||||
| { | |||||
| const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | |||||
| const QRectF row_rect( | |||||
| rect.left(), | |||||
| header.bottom() + row * kAlarmRowHeight, | |||||
| rect.width(), | |||||
| kAlarmRowHeight); | |||||
| painter->fillRect( | |||||
| row_rect, | |||||
| record.active | |||||
| ? QColor(QStringLiteral("#fde8e8")) | |||||
| : QColor(QStringLiteral("#f0f2f3"))); | |||||
| painter->setPen(QColor(QStringLiteral("#d3d8dc"))); | |||||
| 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->drawText( | |||||
| QRectF(row_rect.left() + 5, row_rect.top(), 58, row_rect.height()), | |||||
| Qt::AlignVCenter | Qt::AlignLeft, | |||||
| alarmTimeText(record.occurredAt)); | |||||
| painter->drawText( | |||||
| QRectF(row_rect.left() + 66, row_rect.top(), 52, row_rect.height()), | |||||
| Qt::AlignVCenter | Qt::AlignLeft, | |||||
| state); | |||||
| painter->drawText( | |||||
| QRectF( | |||||
| row_rect.left() + 120, | |||||
| row_rect.top(), | |||||
| std::max(0.0, row_rect.width() - 125), | |||||
| row_rect.height()), | |||||
| Qt::AlignVCenter | Qt::AlignLeft, | |||||
| QString::fromUtf8( | |||||
| record.message.data(), static_cast<int>(record.message.size()))); | |||||
| } | |||||
| break; | |||||
| } | |||||
| case HmiControlType::Label: | case HmiControlType::Label: | ||||
| default: | default: | ||||
| { | { | ||||
| @@ -222,9 +313,12 @@ public: | |||||
| const bool runtime_input = runtime_active_ | const bool runtime_input = runtime_active_ | ||||
| && (control_.type == HmiControlType::Button | && (control_.type == HmiControlType::Button | ||||
| || control_.type == HmiControlType::NumericInput | || control_.type == HmiControlType::NumericInput | ||||
| || control_.type == HmiControlType::PageJump); | |||||
| || control_.type == HmiControlType::PageJump | |||||
| || control_.type == HmiControlType::AlarmList); | |||||
| const bool runtime_input_enabled = runtime_input | const bool runtime_input_enabled = runtime_input | ||||
| && (control_.type == HmiControlType::PageJump || runtime_write_enabled_); | |||||
| && (control_.type == HmiControlType::PageJump | |||||
| || control_.type == HmiControlType::AlarmList | |||||
| || runtime_write_enabled_); | |||||
| setAcceptedMouseButtons( | setAcceptedMouseButtons( | ||||
| editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); | editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); | ||||
| @@ -232,8 +326,11 @@ public: | |||||
| && control_.type == HmiControlType::Button && runtime_write_enabled_; | && control_.type == HmiControlType::Button && runtime_write_enabled_; | ||||
| const bool runtime_page_jump_enabled = runtime_active_ | const bool runtime_page_jump_enabled = runtime_active_ | ||||
| && control_.type == HmiControlType::PageJump; | && control_.type == HmiControlType::PageJump; | ||||
| const bool runtime_alarm_enabled = runtime_active_ | |||||
| && control_.type == HmiControlType::AlarmList; | |||||
| setAcceptHoverEvents(runtime_button_enabled || runtime_page_jump_enabled); | setAcceptHoverEvents(runtime_button_enabled || runtime_page_jump_enabled); | ||||
| if (runtime_button_enabled || runtime_page_jump_enabled) | |||||
| if (runtime_button_enabled || runtime_page_jump_enabled | |||||
| || runtime_alarm_enabled) | |||||
| { | { | ||||
| setCursor(Qt::PointingHandCursor); | setCursor(Qt::PointingHandCursor); | ||||
| } | } | ||||
| @@ -256,6 +353,12 @@ public: | |||||
| update(); | update(); | ||||
| } | } | ||||
| void setAlarmRecords(const std::vector<AlarmRecord> &records) | |||||
| { | |||||
| alarm_records_ = records; | |||||
| update(); | |||||
| } | |||||
| protected: | protected: | ||||
| // 拖拽过程中将新位置限制在页面可见边界内 | // 拖拽过程中将新位置限制在页面可见边界内 | ||||
| QVariant itemChange(GraphicsItemChange change, const QVariant &value) override | QVariant itemChange(GraphicsItemChange change, const QVariant &value) override | ||||
| @@ -279,6 +382,26 @@ protected: | |||||
| // 运行态按钮按下时通知外层执行配置的 M 位操作 | // 运行态按钮按下时通知外层执行配置的 M 位操作 | ||||
| void mousePressEvent(QGraphicsSceneMouseEvent *event) override | void mousePressEvent(QGraphicsSceneMouseEvent *event) override | ||||
| { | { | ||||
| if (runtime_active_ && control_.type == HmiControlType::AlarmList) | |||||
| { | |||||
| if (event->pos().y() < kAlarmHeaderHeight) | |||||
| { | |||||
| event->accept(); | |||||
| return; | |||||
| } | |||||
| const int row = static_cast<int>( | |||||
| (event->pos().y() - kAlarmHeaderHeight) / kAlarmRowHeight); | |||||
| if (row >= 0 && row < static_cast<int>(alarm_records_.size())) | |||||
| { | |||||
| const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | |||||
| if (record.active && !record.acknowledged && alarm_acknowledge_) | |||||
| { | |||||
| alarm_acknowledge_(record.definitionId); | |||||
| } | |||||
| } | |||||
| event->accept(); | |||||
| return; | |||||
| } | |||||
| if (runtime_active_ && control_.type == HmiControlType::PageJump) | if (runtime_active_ && control_.type == HmiControlType::PageJump) | ||||
| { | { | ||||
| page_pressed_ = true; | page_pressed_ = true; | ||||
| @@ -433,6 +556,8 @@ private: | |||||
| std::function<void(const std::string &, HmiButtonEvent)> button_event_; | std::function<void(const std::string &, HmiButtonEvent)> button_event_; | ||||
| std::function<void(const std::string &)> numeric_input_activated_; | std::function<void(const std::string &)> numeric_input_activated_; | ||||
| std::function<void(const std::string &)> page_navigation_; | std::function<void(const std::string &)> page_navigation_; | ||||
| std::function<void(const std::string &)> alarm_acknowledge_; | |||||
| std::vector<AlarmRecord> alarm_records_; | |||||
| bool editing_enabled_ = true; | bool editing_enabled_ = true; | ||||
| bool runtime_active_ = false; | bool runtime_active_ = false; | ||||
| bool runtime_write_enabled_ = false; | bool runtime_write_enabled_ = false; | ||||
| @@ -459,10 +584,12 @@ HmiGraphicsItem *asHmiItem(QGraphicsItem *item) | |||||
| HmiEditorWidget::HmiEditorWidget( | HmiEditorWidget::HmiEditorWidget( | ||||
| HmiEditorService &editor_service, | HmiEditorService &editor_service, | ||||
| HmiRuntimeService &runtime_service, | HmiRuntimeService &runtime_service, | ||||
| AlarmService &alarm_service, | |||||
| QWidget *parent) | QWidget *parent) | ||||
| : QGraphicsView(parent), | : QGraphicsView(parent), | ||||
| editor_service_(editor_service), | editor_service_(editor_service), | ||||
| runtime_service_(runtime_service), | runtime_service_(runtime_service), | ||||
| alarm_service_(alarm_service), | |||||
| scene_(new QGraphicsScene(this)) | scene_(new QGraphicsScene(this)) | ||||
| { | { | ||||
| setObjectName(QStringLiteral("hmiEditorWidget")); | setObjectName(QStringLiteral("hmiEditorWidget")); | ||||
| @@ -557,6 +684,11 @@ void HmiEditorWidget::reloadPage() | |||||
| { | { | ||||
| emit pageNavigationRequested( | emit pageNavigationRequested( | ||||
| QString::fromStdString(target_page_id)); | QString::fromStdString(target_page_id)); | ||||
| }, | |||||
| [this](const std::string &definition_id) | |||||
| { | |||||
| alarm_service_.acknowledge(definition_id); | |||||
| refreshRuntimeValues(); | |||||
| }); | }); | ||||
| scene_->addItem(item); | scene_->addItem(item); | ||||
| item->setInteractionState( | item->setInteractionState( | ||||
| @@ -614,6 +746,11 @@ void HmiEditorWidget::refreshRuntimeValues() | |||||
| { | { | ||||
| continue; | continue; | ||||
| } | } | ||||
| if (control->type == HmiControlType::AlarmList) | |||||
| { | |||||
| control_item->setAlarmRecords(alarm_service_.records()); | |||||
| continue; | |||||
| } | |||||
| // 运行值通过服务读取,图元不直接接触寄存器仓库 | // 运行值通过服务读取,图元不直接接触寄存器仓库 | ||||
| const HmiRuntimeReadResult value = runtime_service_.readControl(*control); | const HmiRuntimeReadResult value = runtime_service_.readControl(*control); | ||||
| control_item->setRuntimeValue(value.bit_value, value.word_value, value.succeeded); | control_item->setRuntimeValue(value.bit_value, value.word_value, value.succeeded); | ||||
| @@ -13,6 +13,7 @@ | |||||
| class HmiEditorService; | class HmiEditorService; | ||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class AlarmService; | |||||
| class QGraphicsScene; | class QGraphicsScene; | ||||
| class QResizeEvent; | class QResizeEvent; | ||||
| enum class HmiButtonEvent; | enum class HmiButtonEvent; | ||||
| @@ -36,6 +37,7 @@ public: | |||||
| explicit HmiEditorWidget( | explicit HmiEditorWidget( | ||||
| HmiEditorService &editor_service, | HmiEditorService &editor_service, | ||||
| HmiRuntimeService &runtime_service, | HmiRuntimeService &runtime_service, | ||||
| AlarmService &alarm_service, | |||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| /** | /** | ||||
| @@ -114,6 +116,7 @@ private: | |||||
| HmiEditorService &editor_service_; | HmiEditorService &editor_service_; | ||||
| // 提供运行态寄存器读写能力,画布不直接访问寄存器仓库 | // 提供运行态寄存器读写能力,画布不直接访问寄存器仓库 | ||||
| HmiRuntimeService &runtime_service_; | HmiRuntimeService &runtime_service_; | ||||
| AlarmService &alarm_service_; | |||||
| // 持有所有页面图元和页面边框 | // 持有所有页面图元和页面边框 | ||||
| QGraphicsScene *scene_ = nullptr; | QGraphicsScene *scene_ = nullptr; | ||||
| // 当前画布投影的页面标识 | // 当前画布投影的页面标识 | ||||
| @@ -1,6 +1,7 @@ | |||||
| #include "main_window.h" | #include "main_window.h" | ||||
| #include "hmi_editor_widget.h" | #include "hmi_editor_widget.h" | ||||
| #include "alarm_configuration_dialog.h" | |||||
| #include "logic_editor_widget.h" | #include "logic_editor_widget.h" | ||||
| #include "plc_connection_dialog.h" | #include "plc_connection_dialog.h" | ||||
| #include "runtime_monitor_widget.h" | #include "runtime_monitor_widget.h" | ||||
| @@ -8,6 +9,8 @@ | |||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_navigation_service.h" | #include "services/hmi_navigation_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/alarm_editor_service.h" | |||||
| #include "services/alarm_service.h" | |||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| @@ -176,6 +179,10 @@ QString controlTypeText(HmiControlType type) | |||||
| { | { | ||||
| return MainWindow::tr("页面跳转"); | return MainWindow::tr("页面跳转"); | ||||
| } | } | ||||
| case HmiControlType::AlarmList: | |||||
| { | |||||
| return MainWindow::tr("报警列表"); | |||||
| } | |||||
| default: | default: | ||||
| { | { | ||||
| return MainWindow::tr("未知控件"); | return MainWindow::tr("未知控件"); | ||||
| @@ -191,6 +198,8 @@ MainWindow::MainWindow( | |||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| AlarmEditorService &alarm_editor_service, | |||||
| AlarmService &alarm_service, | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent) | QWidget *parent) | ||||
| : QMainWindow(parent), | : QMainWindow(parent), | ||||
| @@ -200,6 +209,8 @@ MainWindow::MainWindow( | |||||
| hmi_editor_service_(hmi_editor_service), | hmi_editor_service_(hmi_editor_service), | ||||
| logic_editor_service_(logic_editor_service), | logic_editor_service_(logic_editor_service), | ||||
| hmi_runtime_service_(hmi_runtime_service), | hmi_runtime_service_(hmi_runtime_service), | ||||
| alarm_editor_service_(alarm_editor_service), | |||||
| alarm_service_(alarm_service), | |||||
| register_monitor_service_(register_monitor_service), | register_monitor_service_(register_monitor_service), | ||||
| owned_hmi_navigation_service_( | owned_hmi_navigation_service_( | ||||
| std::make_unique<HmiNavigationService>(project_service)), | std::make_unique<HmiNavigationService>(project_service)), | ||||
| @@ -214,6 +225,8 @@ MainWindow::MainWindow( | |||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| AlarmEditorService &alarm_editor_service, | |||||
| AlarmService &alarm_service, | |||||
| HmiNavigationService &hmi_navigation_service, | HmiNavigationService &hmi_navigation_service, | ||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent) | QWidget *parent) | ||||
| @@ -224,6 +237,8 @@ MainWindow::MainWindow( | |||||
| hmi_editor_service_(hmi_editor_service), | hmi_editor_service_(hmi_editor_service), | ||||
| logic_editor_service_(logic_editor_service), | logic_editor_service_(logic_editor_service), | ||||
| hmi_runtime_service_(hmi_runtime_service), | hmi_runtime_service_(hmi_runtime_service), | ||||
| alarm_editor_service_(alarm_editor_service), | |||||
| alarm_service_(alarm_service), | |||||
| register_monitor_service_(register_monitor_service), | register_monitor_service_(register_monitor_service), | ||||
| hmi_navigation_service_(&hmi_navigation_service) | hmi_navigation_service_(&hmi_navigation_service) | ||||
| { | { | ||||
| @@ -237,6 +252,7 @@ void MainWindow::initializeUi() | |||||
| configureActions(); | configureActions(); | ||||
| configurePropertyEditor(); | configurePropertyEditor(); | ||||
| configurePlcConnection(); | configurePlcConnection(); | ||||
| configureAlarms(); | |||||
| configureHmiEditor(); | configureHmiEditor(); | ||||
| configureLogicEditor(); | configureLogicEditor(); | ||||
| configureRuntimeMonitor(); | configureRuntimeMonitor(); | ||||
| @@ -298,6 +314,8 @@ void MainWindow::configureActions() | |||||
| [this] { addHmiControl(HmiControlType::Label); }); | [this] { addHmiControl(HmiControlType::Label); }); | ||||
| connect(ui_->addPageJumpAction, &QAction::triggered, this, | connect(ui_->addPageJumpAction, &QAction::triggered, this, | ||||
| [this] { addHmiControl(HmiControlType::PageJump); }); | [this] { addHmiControl(HmiControlType::PageJump); }); | ||||
| connect(ui_->addAlarmListAction, &QAction::triggered, this, | |||||
| [this] { addHmiControl(HmiControlType::AlarmList); }); | |||||
| connect(ui_->deleteControlAction, &QAction::triggered, | connect(ui_->deleteControlAction, &QAction::triggered, | ||||
| this, &MainWindow::deleteSelectedControl); | this, &MainWindow::deleteSelectedControl); | ||||
| @@ -454,6 +472,10 @@ void MainWindow::configureAppearance() | |||||
| style()->standardIcon(QStyle::SP_FileDialogInfoView)); | style()->standardIcon(QStyle::SP_FileDialogInfoView)); | ||||
| ui_->addPageJumpAction->setIcon( | ui_->addPageJumpAction->setIcon( | ||||
| style()->standardIcon(QStyle::SP_ArrowForward)); | style()->standardIcon(QStyle::SP_ArrowForward)); | ||||
| ui_->addAlarmListAction->setIcon( | |||||
| style()->standardIcon(QStyle::SP_MessageBoxWarning)); | |||||
| ui_->configureAlarmsAction->setIcon( | |||||
| style()->standardIcon(QStyle::SP_FileDialogDetailedView)); | |||||
| ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); | ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); | ||||
| ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); | ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); | ||||
| ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); | ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); | ||||
| @@ -509,7 +531,10 @@ void MainWindow::configureHmiEditor() | |||||
| QLayout *layout = ui_->hmiCanvasPlaceholder->layout(); | QLayout *layout = ui_->hmiCanvasPlaceholder->layout(); | ||||
| delete ui_->hmiEmptyLabel; | delete ui_->hmiEmptyLabel; | ||||
| hmi_editor_widget_ = new HmiEditorWidget( | hmi_editor_widget_ = new HmiEditorWidget( | ||||
| hmi_editor_service_, hmi_runtime_service_, ui_->hmiCanvasPlaceholder); | |||||
| hmi_editor_service_, | |||||
| hmi_runtime_service_, | |||||
| alarm_service_, | |||||
| ui_->hmiCanvasPlaceholder); | |||||
| hmi_editor_widget_->setMinimumHeight(320); | hmi_editor_widget_->setMinimumHeight(320); | ||||
| layout->addWidget(hmi_editor_widget_); | layout->addWidget(hmi_editor_widget_); | ||||
| connect(hmi_editor_widget_, &HmiEditorWidget::controlSelected, | connect(hmi_editor_widget_, &HmiEditorWidget::controlSelected, | ||||
| @@ -535,6 +560,7 @@ void MainWindow::configureHmiEditor() | |||||
| // 离线 / 联机运行模式 | // 离线 / 联机运行模式 | ||||
| if (runtime_mode_service_.mode() != ApplicationMode::Editing) | if (runtime_mode_service_.mode() != ApplicationMode::Editing) | ||||
| { | { | ||||
| alarm_service_.refresh(); | |||||
| hmi_editor_widget_->refreshRuntimeValues(); | hmi_editor_widget_->refreshRuntimeValues(); | ||||
| runtime_monitor_widget_->refreshValues( | runtime_monitor_widget_->refreshValues( | ||||
| runtime_mode_service_.mode(), | runtime_mode_service_.mode(), | ||||
| @@ -595,6 +621,7 @@ void MainWindow::configureRuntimeMonitor() | |||||
| logic_editor_service_, | logic_editor_service_, | ||||
| project_service_, | project_service_, | ||||
| *hmi_navigation_service_, | *hmi_navigation_service_, | ||||
| alarm_service_, | |||||
| register_monitor_service_, | register_monitor_service_, | ||||
| ui_->runtimeMonitorTab); | ui_->runtimeMonitorTab); | ||||
| runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | ||||
| @@ -719,6 +746,18 @@ void MainWindow::configurePlcConnection() | |||||
| connect(ui_->disconnectPlcAction, &QAction::triggered, this, &MainWindow::disconnectPlc); | connect(ui_->disconnectPlcAction, &QAction::triggered, this, &MainWindow::disconnectPlc); | ||||
| } | } | ||||
| void MainWindow::configureAlarms() | |||||
| { | |||||
| connect(ui_->configureAlarmsAction, &QAction::triggered, | |||||
| this, | |||||
| [this] | |||||
| { | |||||
| AlarmConfigurationDialog dialog(alarm_editor_service_, this); | |||||
| dialog.exec(); | |||||
| refreshProjectUi(); | |||||
| }); | |||||
| } | |||||
| void MainWindow::refreshProjectUi() | void MainWindow::refreshProjectUi() | ||||
| { | { | ||||
| // 仅在当前对象消失时回退,普通刷新必须保留用户的会话选择 | // 仅在当前对象消失时回退,普通刷新必须保留用户的会话选择 | ||||
| @@ -1215,7 +1254,8 @@ void MainWindow::showControlProperties(const std::string &control_id) | |||||
| ui_->bindingIndexSpinBox->setValue( | ui_->bindingIndexSpinBox->setValue( | ||||
| control->binding.has_value() ? control->binding->index() : 0); | control->binding.has_value() ? control->binding->index() : 0); | ||||
| const bool has_binding = control->type != HmiControlType::Label | const bool has_binding = control->type != HmiControlType::Label | ||||
| && control->type != HmiControlType::PageJump; | |||||
| && control->type != HmiControlType::PageJump | |||||
| && control->type != HmiControlType::AlarmList; | |||||
| ui_->bindingAreaLabel->setVisible(has_binding); | ui_->bindingAreaLabel->setVisible(has_binding); | ||||
| ui_->bindingAreaComboBox->setVisible(has_binding); | ui_->bindingAreaComboBox->setVisible(has_binding); | ||||
| ui_->bindingIndexLabel->setVisible(has_binding); | ui_->bindingIndexLabel->setVisible(has_binding); | ||||
| @@ -1383,7 +1423,8 @@ void MainWindow::applySelectedControlProperties() | |||||
| control.bounds.width = ui_->controlWidthSpinBox->value(); | control.bounds.width = ui_->controlWidthSpinBox->value(); | ||||
| control.bounds.height = ui_->controlHeightSpinBox->value(); | control.bounds.height = ui_->controlHeightSpinBox->value(); | ||||
| if (control.type == HmiControlType::Label | if (control.type == HmiControlType::Label | ||||
| || control.type == HmiControlType::PageJump) | |||||
| || control.type == HmiControlType::PageJump | |||||
| || control.type == HmiControlType::AlarmList) | |||||
| { | { | ||||
| control.binding.reset(); | control.binding.reset(); | ||||
| } | } | ||||
| @@ -1721,6 +1762,7 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| logic_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); | logic_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); | ||||
| if (mode == ApplicationMode::Editing) | if (mode == ApplicationMode::Editing) | ||||
| { | { | ||||
| alarm_service_.reset(); | |||||
| logic_editor_widget_->clearRuntimeTrace(); | logic_editor_widget_->clearRuntimeTrace(); | ||||
| } | } | ||||
| ui_->hmiToolBar->setEnabled(policy.allowsProjectEditing); | ui_->hmiToolBar->setEnabled(policy.allowsProjectEditing); | ||||
| @@ -1731,6 +1773,8 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing); | ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addLabelAction->setEnabled(policy.allowsProjectEditing); | ui_->addLabelAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing); | ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addAlarmListAction->setEnabled(policy.allowsProjectEditing); | |||||
| ui_->configureAlarmsAction->setEnabled(policy.allowsProjectEditing); | |||||
| ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); | ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); | ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); | ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); | ||||
| @@ -32,6 +32,8 @@ class RuntimeModeService; | |||||
| class ProjectService; | class ProjectService; | ||||
| class HmiEditorService; | class HmiEditorService; | ||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class AlarmEditorService; | |||||
| class AlarmService; | |||||
| class HmiNavigationService; | class HmiNavigationService; | ||||
| class HmiEditorWidget; | class HmiEditorWidget; | ||||
| class LogicEditorService; | class LogicEditorService; | ||||
| @@ -63,6 +65,8 @@ public: | |||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| AlarmEditorService &alarm_editor_service, | |||||
| AlarmService &alarm_service, | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| MainWindow( | MainWindow( | ||||
| @@ -71,6 +75,8 @@ public: | |||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| AlarmEditorService &alarm_editor_service, | |||||
| AlarmService &alarm_service, | |||||
| HmiNavigationService &hmi_navigation_service, | HmiNavigationService &hmi_navigation_service, | ||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| @@ -94,6 +100,7 @@ private: | |||||
| // 创建并连接控件属性编辑表单 | // 创建并连接控件属性编辑表单 | ||||
| void configurePropertyEditor(); | void configurePropertyEditor(); | ||||
| void configurePlcConnection(); | void configurePlcConnection(); | ||||
| void configureAlarms(); | |||||
| // 刷新工程树和当前 HMI 页面信息 | // 刷新工程树和当前 HMI 页面信息 | ||||
| void refreshProjectUi(); | void refreshProjectUi(); | ||||
| void handleProjectTreeSelection(); | void handleProjectTreeSelection(); | ||||
| @@ -176,6 +183,8 @@ private: | |||||
| HmiEditorService &hmi_editor_service_; | HmiEditorService &hmi_editor_service_; | ||||
| LogicEditorService &logic_editor_service_; | LogicEditorService &logic_editor_service_; | ||||
| HmiRuntimeService &hmi_runtime_service_; | HmiRuntimeService &hmi_runtime_service_; | ||||
| AlarmEditorService &alarm_editor_service_; | |||||
| AlarmService &alarm_service_; | |||||
| RegisterMonitorService ®ister_monitor_service_; | RegisterMonitorService ®ister_monitor_service_; | ||||
| std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_; | std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_; | ||||
| HmiNavigationService *hmi_navigation_service_ = nullptr; | HmiNavigationService *hmi_navigation_service_ = nullptr; | ||||
| @@ -304,6 +304,8 @@ | |||||
| <addaction name="addNumericInputAction"/> | <addaction name="addNumericInputAction"/> | ||||
| <addaction name="addLabelAction"/> | <addaction name="addLabelAction"/> | ||||
| <addaction name="addPageJumpAction"/> | <addaction name="addPageJumpAction"/> | ||||
| <addaction name="addAlarmListAction"/> | |||||
| <addaction name="configureAlarmsAction"/> | |||||
| <addaction name="deleteControlAction"/> | <addaction name="deleteControlAction"/> | ||||
| </widget> | </widget> | ||||
| <widget class="QToolBar" name="logicToolBar"> | <widget class="QToolBar" name="logicToolBar"> | ||||
| @@ -902,6 +904,22 @@ | |||||
| <string>添加页面跳转控件</string> | <string>添加页面跳转控件</string> | ||||
| </property> | </property> | ||||
| </action> | </action> | ||||
| <action name="addAlarmListAction"> | |||||
| <property name="text"> | |||||
| <string>报警列表</string> | |||||
| </property> | |||||
| <property name="toolTip"> | |||||
| <string>添加报警列表控件</string> | |||||
| </property> | |||||
| </action> | |||||
| <action name="configureAlarmsAction"> | |||||
| <property name="text"> | |||||
| <string>报警配置</string> | |||||
| </property> | |||||
| <property name="toolTip"> | |||||
| <string>配置 M 和 D 报警条件</string> | |||||
| </property> | |||||
| </action> | |||||
| <action name="deleteControlAction"> | <action name="deleteControlAction"> | ||||
| <property name="text"> | <property name="text"> | ||||
| <string>删除</string> | <string>删除</string> | ||||
| @@ -4,6 +4,7 @@ | |||||
| #include "hmi_editor_widget.h" | #include "hmi_editor_widget.h" | ||||
| #include "logic_editor_widget.h" | #include "logic_editor_widget.h" | ||||
| #include "services/hmi_navigation_service.h" | #include "services/hmi_navigation_service.h" | ||||
| #include "services/alarm_service.h" | |||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "ui_runtime_monitor_widget.h" | #include "ui_runtime_monitor_widget.h" | ||||
| @@ -34,6 +35,7 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| ProjectService &project_service, | ProjectService &project_service, | ||||
| HmiNavigationService &hmi_navigation_service, | HmiNavigationService &hmi_navigation_service, | ||||
| AlarmService &alarm_service, | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent) | QWidget *parent) | ||||
| : QWidget(parent), | : QWidget(parent), | ||||
| @@ -43,7 +45,10 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||||
| { | { | ||||
| ui_->setupUi(this); | ui_->setupUi(this); | ||||
| hmi_view_ = new HmiEditorWidget( | hmi_view_ = new HmiEditorWidget( | ||||
| hmi_editor_service, hmi_runtime_service, ui_->hmiViewContainer); | |||||
| hmi_editor_service, | |||||
| hmi_runtime_service, | |||||
| alarm_service, | |||||
| ui_->hmiViewContainer); | |||||
| hmi_view_->setObjectName(QStringLiteral("runtimeHmiView")); | hmi_view_->setObjectName(QStringLiteral("runtimeHmiView")); | ||||
| hmi_view_->setEditingEnabled(false); | hmi_view_->setEditingEnabled(false); | ||||
| hmi_view_->setRuntimeActive(true); | hmi_view_->setRuntimeActive(true); | ||||
| @@ -17,6 +17,7 @@ class FreeMonitorWidget; | |||||
| class HmiEditorService; | class HmiEditorService; | ||||
| class HmiEditorWidget; | class HmiEditorWidget; | ||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class AlarmService; | |||||
| class HmiNavigationService; | class HmiNavigationService; | ||||
| class LogicEditorService; | class LogicEditorService; | ||||
| class LogicEditorWidget; | class LogicEditorWidget; | ||||
| @@ -34,6 +35,7 @@ public: | |||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| ProjectService &project_service, | ProjectService &project_service, | ||||
| HmiNavigationService &hmi_navigation_service, | HmiNavigationService &hmi_navigation_service, | ||||
| AlarmService &alarm_service, | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| ~RuntimeMonitorWidget() override; | ~RuntimeMonitorWidget() override; | ||||
| @@ -0,0 +1,140 @@ | |||||
| #include "domain/project_storage.h" | |||||
| #include "domain/register_repository.h" | |||||
| #include "services/alarm_editor_service.h" | |||||
| #include "services/alarm_service.h" | |||||
| #include "services/project_service.h" | |||||
| #include <algorithm> | |||||
| #include <exception> | |||||
| #include <iostream> | |||||
| #include <stdexcept> | |||||
| #include <string> | |||||
| namespace { | |||||
| class TestProjectStorage final : public ProjectStorage | |||||
| { | |||||
| public: | |||||
| ProjectSaveResult save(const Project &, const std::string &) override | |||||
| { | |||||
| return {true, ProjectStorageError::None, {}}; | |||||
| } | |||||
| ProjectLoadResult load(const std::string &) override | |||||
| { | |||||
| return {false, {}, ProjectStorageError::FileReadFailed, {}}; | |||||
| } | |||||
| }; | |||||
| void require(bool condition, const std::string &message) | |||||
| { | |||||
| if (!condition) | |||||
| { | |||||
| throw std::runtime_error(message); | |||||
| } | |||||
| } | |||||
| const AlarmRecord *findActiveRecord( | |||||
| const AlarmService &service, const std::string &definition_id) | |||||
| { | |||||
| const auto &records = service.records(); | |||||
| const auto record = std::find_if( | |||||
| records.cbegin(), records.cend(), | |||||
| [&definition_id](const AlarmRecord &candidate) | |||||
| { | |||||
| return candidate.definitionId == definition_id && candidate.active; | |||||
| }); | |||||
| return record == records.cend() ? nullptr : &*record; | |||||
| } | |||||
| void testAlarmDefinitionsAndRuntimeLifecycle() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| AlarmEditorService editor_service(project_service); | |||||
| VirtualRegisterRepository repository; | |||||
| AlarmService alarm_service(project_service, repository); | |||||
| AlarmDefinition m_alarm; | |||||
| m_alarm.address = RegisterAddress{RegisterArea::M, 0}; | |||||
| m_alarm.condition = AlarmCondition::MOn; | |||||
| m_alarm.message = "Emergency stop"; | |||||
| const AlarmEditorResult m_result = editor_service.addDefinition(m_alarm); | |||||
| AlarmDefinition high_alarm; | |||||
| high_alarm.address = RegisterAddress{RegisterArea::D, 0}; | |||||
| high_alarm.condition = AlarmCondition::DHigh; | |||||
| high_alarm.threshold = 80; | |||||
| high_alarm.message = "Temperature high"; | |||||
| const AlarmEditorResult high_result = editor_service.addDefinition(high_alarm); | |||||
| AlarmDefinition low_alarm; | |||||
| low_alarm.address = RegisterAddress{RegisterArea::D, 1}; | |||||
| low_alarm.condition = AlarmCondition::DLow; | |||||
| low_alarm.threshold = 10; | |||||
| low_alarm.message = "Pressure low"; | |||||
| 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"); | |||||
| AlarmDefinition invalid = high_alarm; | |||||
| invalid.address = RegisterAddress{RegisterArea::M, 2}; | |||||
| require(editor_service.addDefinition(invalid).error | |||||
| == AlarmEditorError::InvalidDefinition, | |||||
| "D alarm conditions must reject M addresses"); | |||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 20); | |||||
| alarm_service.refresh(); | |||||
| require(alarm_service.records().empty(), | |||||
| "inactive alarm conditions must not create records"); | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | |||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 0}, 80); | |||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 10); | |||||
| 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, | |||||
| "all three alarm conditions must create active records"); | |||||
| require(alarm_service.acknowledge(m_result.id) | |||||
| && findActiveRecord(alarm_service, m_result.id)->acknowledged, | |||||
| "an active alarm must be locally acknowledged"); | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | |||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 0}, 79); | |||||
| repository.writeWord(RegisterAddress{RegisterArea::D, 1}, 11); | |||||
| 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"); | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, true); | |||||
| 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"); | |||||
| alarm_service.reset(); | |||||
| require(alarm_service.records().empty(), | |||||
| "leaving runtime must clear session alarm records"); | |||||
| } | |||||
| } // namespace | |||||
| int main() | |||||
| { | |||||
| try | |||||
| { | |||||
| testAlarmDefinitionsAndRuntimeLifecycle(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "alarm service tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "alarm service tests passed\n"; | |||||
| return 0; | |||||
| } | |||||
| @@ -0,0 +1,33 @@ | |||||
| TEMPLATE = app | |||||
| TARGET = alarm_service_tests | |||||
| CONFIG += console c++17 testcase warn_on | |||||
| CONFIG -= app_bundle qt | |||||
| INCLUDEPATH += ../src | |||||
| SOURCES += \ | |||||
| alarm_service_tests.cpp \ | |||||
| ../src/domain/register_address.cpp \ | |||||
| ../src/domain/register_repository.cpp \ | |||||
| ../src/domain/alarm_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | |||||
| ../src/domain/control_logic_model.cpp \ | |||||
| ../src/domain/project_model.cpp \ | |||||
| ../src/services/project_service.cpp \ | |||||
| ../src/services/alarm_editor_service.cpp \ | |||||
| ../src/services/alarm_service.cpp | |||||
| HEADERS += \ | |||||
| ../src/domain/register_address.h \ | |||||
| ../src/domain/register_repository.h \ | |||||
| ../src/domain/alarm_model.h \ | |||||
| ../src/domain/hmi_model.h \ | |||||
| ../src/domain/control_logic_model.h \ | |||||
| ../src/domain/project_model.h \ | |||||
| ../src/domain/project_storage.h \ | |||||
| ../src/services/project_service.h \ | |||||
| ../src/services/alarm_editor_service.h \ | |||||
| ../src/services/alarm_service.h | |||||
| QT -= gui | |||||
| @@ -10,6 +10,7 @@ SOURCES += \ | |||||
| domain_tests.cpp \ | domain_tests.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/alarm_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -18,6 +19,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/alarm_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| @@ -175,6 +175,17 @@ void testPageLifecycleAndNavigation() | |||||
| require(label.succeeded | require(label.succeeded | ||||
| && service.findControl(main_id, label.id)->isConfigured(), | && service.findControl(main_id, label.id)->isConfigured(), | ||||
| "the editor service must expose a configured static Label control"); | "the editor service must expose a configured static Label control"); | ||||
| const HmiEditorResult alarm_list = service.addControl( | |||||
| main_id, HmiControlType::AlarmList); | |||||
| require(alarm_list.succeeded | |||||
| && service.findControl(main_id, alarm_list.id)->isConfigured() | |||||
| && !service.findControl(main_id, alarm_list.id)->binding.has_value(), | |||||
| "AlarmList must be configured without a register binding"); | |||||
| HmiControl bound_alarm_list = *service.findControl(main_id, alarm_list.id); | |||||
| bound_alarm_list.binding = RegisterAddress{RegisterArea::M, 20}; | |||||
| require(!service.updateControl( | |||||
| main_id, alarm_list.id, bound_alarm_list).succeeded, | |||||
| "AlarmList must reject direct register bindings"); | |||||
| const HmiEditorResult jump = service.addControl(main_id, HmiControlType::PageJump); | const HmiEditorResult jump = service.addControl(main_id, HmiControlType::PageJump); | ||||
| require(jump.succeeded, "the editor service must expose PageJump creation"); | require(jump.succeeded, "the editor service must expose PageJump creation"); | ||||
| HmiControl jump_control = *service.findControl(main_id, jump.id); | HmiControl jump_control = *service.findControl(main_id, jump.id); | ||||
| @@ -10,6 +10,7 @@ SOURCES += \ | |||||
| hmi_editor_service_tests.cpp \ | hmi_editor_service_tests.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/alarm_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -21,6 +22,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/alarm_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| @@ -13,6 +13,7 @@ SOURCES += \ | |||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/alarm_model.cpp \ | |||||
| ../src/services/project_service.cpp \ | ../src/services/project_service.cpp \ | ||||
| ../src/services/logic_editor_service.cpp | ../src/services/logic_editor_service.cpp | ||||
| @@ -22,6 +23,7 @@ HEADERS += \ | |||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/alarm_model.h \ | |||||
| ../src/domain/project_storage.h \ | ../src/domain/project_storage.h \ | ||||
| ../src/services/project_service.h \ | ../src/services/project_service.h \ | ||||
| ../src/services/logic_editor_service.h | ../src/services/logic_editor_service.h | ||||
| @@ -1,6 +1,8 @@ | |||||
| #include "domain/active_register_repository.h" | #include "domain/active_register_repository.h" | ||||
| #include "domain/project_storage.h" | #include "domain/project_storage.h" | ||||
| #include "domain/register_repository.h" | #include "domain/register_repository.h" | ||||
| #include "services/alarm_editor_service.h" | |||||
| #include "services/alarm_service.h" | |||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/hmi_navigation_service.h" | #include "services/hmi_navigation_service.h" | ||||
| @@ -278,6 +280,7 @@ void testRuntimeButtonMouseInteraction() | |||||
| HmiEditorService editor_service(project_service); | HmiEditorService editor_service(project_service); | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| AlarmService alarm_service(project_service, repository); | |||||
| const HmiEditorResult page_result = editor_service.ensureDefaultPage(); | const HmiEditorResult page_result = editor_service.ensureDefaultPage(); | ||||
| require(page_result.succeeded, "runtime button test must create an HMI page"); | require(page_result.succeeded, "runtime button test must create an HMI page"); | ||||
| const HmiEditorResult button_result = editor_service.addControl( | const HmiEditorResult button_result = editor_service.addControl( | ||||
| @@ -290,7 +293,7 @@ void testRuntimeButtonMouseInteraction() | |||||
| require(editor_service.updateControl(page_result.id, button.id, button).succeeded, | require(editor_service.updateControl(page_result.id, button.id, button).succeeded, | ||||
| "runtime button test must bind the button to M0"); | "runtime button test must bind the button to M0"); | ||||
| HmiEditorWidget view(editor_service, runtime_service); | |||||
| HmiEditorWidget view(editor_service, runtime_service, alarm_service); | |||||
| view.resize(900, 600); | view.resize(900, 600); | ||||
| view.setPageId(page_result.id); | view.setPageId(page_result.id); | ||||
| view.setEditingEnabled(false); | view.setEditingEnabled(false); | ||||
| @@ -361,11 +364,13 @@ void testRuntimePageJumpDoesNotRequireRegisterWritePermission() | |||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| HmiEditorWidget view(editor_service, runtime_service); | |||||
| AlarmService alarm_service(project_service, repository); | |||||
| HmiEditorWidget view(editor_service, runtime_service, alarm_service); | |||||
| view.setPageId(source_page); | view.setPageId(source_page); | ||||
| view.setEditingEnabled(false); | view.setEditingEnabled(false); | ||||
| view.setRuntimeActive(true); | view.setRuntimeActive(true); | ||||
| view.setRuntimeWriteEnabled(false); | view.setRuntimeWriteEnabled(false); | ||||
| view.refreshRuntimeValues(); | |||||
| view.resize(900, 560); | view.resize(900, 560); | ||||
| view.show(); | view.show(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| @@ -394,6 +399,73 @@ void testRuntimePageJumpDoesNotRequireRegisterWritePermission() | |||||
| "PageJump must remain active when register writes are disabled"); | "PageJump must remain active when register writes are disabled"); | ||||
| } | } | ||||
| void testRuntimeAlarmListInteraction() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| HmiEditorService editor_service(project_service); | |||||
| AlarmEditorService alarm_editor_service(project_service); | |||||
| VirtualRegisterRepository repository; | |||||
| HmiRuntimeService runtime_service(repository); | |||||
| AlarmService alarm_service(project_service, repository); | |||||
| const std::string page_id = editor_service.ensureDefaultPage().id; | |||||
| const HmiEditorResult list_result = editor_service.addControl( | |||||
| page_id, HmiControlType::AlarmList); | |||||
| require(list_result.succeeded, | |||||
| "runtime alarm test must create an AlarmList control"); | |||||
| const HmiControl *alarm_list = editor_service.findControl( | |||||
| page_id, list_result.id); | |||||
| require(alarm_list != nullptr && !alarm_list->binding.has_value(), | |||||
| "AlarmList must not require a register binding"); | |||||
| AlarmDefinition definition; | |||||
| definition.address = RegisterAddress{RegisterArea::M, 0}; | |||||
| definition.condition = AlarmCondition::MOn; | |||||
| definition.message = "Emergency stop"; | |||||
| const AlarmEditorResult alarm_result = | |||||
| alarm_editor_service.addDefinition(definition); | |||||
| require(alarm_result.succeeded, | |||||
| "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); | |||||
| view.setPageId(page_id); | |||||
| view.setEditingEnabled(false); | |||||
| view.setRuntimeActive(true); | |||||
| view.setRuntimeWriteEnabled(false); | |||||
| view.refreshRuntimeValues(); | |||||
| view.resize(900, 560); | |||||
| view.show(); | |||||
| QApplication::processEvents(); | |||||
| const QPoint header_position = view.mapFromScene(QPointF( | |||||
| alarm_list->bounds.x + 20.0, alarm_list->bounds.y + 10.0)); | |||||
| QTest::mouseClick( | |||||
| view.viewport(), Qt::LeftButton, Qt::NoModifier, header_position); | |||||
| require(!alarm_service.records().front().acknowledged, | |||||
| "clicking the AlarmList header must not acknowledge an alarm"); | |||||
| const QPoint first_row_position = view.mapFromScene(QPointF( | |||||
| alarm_list->bounds.x + 20.0, alarm_list->bounds.y + 34.0)); | |||||
| QTest::mouseClick( | |||||
| view.viewport(), Qt::LeftButton, Qt::NoModifier, first_row_position); | |||||
| require(alarm_service.records().front().acknowledged, | |||||
| "clicking an active AlarmList row must acknowledge the alarm"); | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | |||||
| alarm_service.refresh(); | |||||
| 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"); | |||||
| } | |||||
| void testModeActionsControlEditingAvailability() | void testModeActionsControlEditingAvailability() | ||||
| { | { | ||||
| // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 | // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 | ||||
| @@ -403,6 +475,8 @@ void testModeActionsControlEditingAvailability() | |||||
| LogicEditorService logic_editor_service(project_service); | LogicEditorService logic_editor_service(project_service); | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| AlarmEditorService alarm_editor_service(project_service); | |||||
| AlarmService alarm_service(project_service, repository); | |||||
| OfflineSimulationService simulation_service(repository); | OfflineSimulationService simulation_service(repository); | ||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| RegisterMonitorService monitor_service(repository); | RegisterMonitorService monitor_service(repository); | ||||
| @@ -412,6 +486,8 @@ void testModeActionsControlEditingAvailability() | |||||
| editor_service, | editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| runtime_service, | runtime_service, | ||||
| alarm_editor_service, | |||||
| alarm_service, | |||||
| monitor_service); | monitor_service); | ||||
| window.resize(1000, 640); | window.resize(1000, 640); | ||||
| window.show(); | window.show(); | ||||
| @@ -644,6 +720,8 @@ void testPlcConfigurationUsesDialog() | |||||
| LogicEditorService logic_editor_service(project_service); | LogicEditorService logic_editor_service(project_service); | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| AlarmEditorService alarm_editor_service(project_service); | |||||
| AlarmService alarm_service(project_service, repository); | |||||
| OfflineSimulationService simulation_service(repository); | OfflineSimulationService simulation_service(repository); | ||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| RegisterMonitorService monitor_service(repository); | RegisterMonitorService monitor_service(repository); | ||||
| @@ -653,6 +731,8 @@ void testPlcConfigurationUsesDialog() | |||||
| editor_service, | editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| runtime_service, | runtime_service, | ||||
| alarm_editor_service, | |||||
| alarm_service, | |||||
| monitor_service); | monitor_service); | ||||
| require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr, | require(window.findChild<QComboBox *>(QStringLiteral("serialPortComboBox")) == nullptr, | ||||
| "serial configuration controls must not be embedded in the main window"); | "serial configuration controls must not be embedded in the main window"); | ||||
| @@ -685,6 +765,8 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||||
| VirtualRegisterRepository plc_repository; | VirtualRegisterRepository plc_repository; | ||||
| ActiveRegisterRepository active_repository(virtual_repository); | ActiveRegisterRepository active_repository(virtual_repository); | ||||
| HmiRuntimeService runtime_service(active_repository); | HmiRuntimeService runtime_service(active_repository); | ||||
| AlarmEditorService alarm_editor_service(project_service); | |||||
| AlarmService alarm_service(project_service, active_repository); | |||||
| OfflineSimulationService simulation_service(virtual_repository); | OfflineSimulationService simulation_service(virtual_repository); | ||||
| RepeatedStatusGateway gateway; | RepeatedStatusGateway gateway; | ||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| @@ -697,6 +779,8 @@ void testRepeatedPlcStatusNotificationsAreCoalesced() | |||||
| editor_service, | editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| runtime_service, | runtime_service, | ||||
| alarm_editor_service, | |||||
| alarm_service, | |||||
| monitor_service); | monitor_service); | ||||
| QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | QListWidget *output = requiredChild<QListWidget>(window, "outputList"); | ||||
| const int initial_count = output->count(); | const int initial_count = output->count(); | ||||
| @@ -729,6 +813,8 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| VirtualRegisterRepository plc_repository; | VirtualRegisterRepository plc_repository; | ||||
| ActiveRegisterRepository active_repository(virtual_repository); | ActiveRegisterRepository active_repository(virtual_repository); | ||||
| HmiRuntimeService runtime_service(active_repository); | HmiRuntimeService runtime_service(active_repository); | ||||
| AlarmEditorService alarm_editor_service(project_service); | |||||
| AlarmService alarm_service(project_service, active_repository); | |||||
| OfflineSimulationService simulation_service(virtual_repository); | OfflineSimulationService simulation_service(virtual_repository); | ||||
| OnlineReadyGateway gateway; | OnlineReadyGateway gateway; | ||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| @@ -741,6 +827,8 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| editor_service, | editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| runtime_service, | runtime_service, | ||||
| alarm_editor_service, | |||||
| alarm_service, | |||||
| monitor_service); | monitor_service); | ||||
| window.resize(1200, 760); | window.resize(1200, 760); | ||||
| window.show(); | window.show(); | ||||
| @@ -851,6 +939,8 @@ void testMultiPageAndLogicMainWindowIntegration() | |||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| AlarmEditorService alarm_editor_service(project_service); | |||||
| AlarmService alarm_service(project_service, repository); | |||||
| OfflineSimulationService simulation_service(repository); | OfflineSimulationService simulation_service(repository); | ||||
| RuntimeModeService mode_service(project_service, simulation_service); | RuntimeModeService mode_service(project_service, simulation_service); | ||||
| RegisterMonitorService monitor_service(repository); | RegisterMonitorService monitor_service(repository); | ||||
| @@ -861,6 +951,8 @@ void testMultiPageAndLogicMainWindowIntegration() | |||||
| editor_service, | editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| runtime_service, | runtime_service, | ||||
| alarm_editor_service, | |||||
| alarm_service, | |||||
| navigation_service, | navigation_service, | ||||
| monitor_service); | monitor_service); | ||||
| window.resize(1400, 820); | window.resize(1400, 820); | ||||
| @@ -960,6 +1052,7 @@ int main(int argc, char *argv[]) | |||||
| { | { | ||||
| testRuntimeButtonMouseInteraction(); | testRuntimeButtonMouseInteraction(); | ||||
| testRuntimePageJumpDoesNotRequireRegisterWritePermission(); | testRuntimePageJumpDoesNotRequireRegisterWritePermission(); | ||||
| testRuntimeAlarmListInteraction(); | |||||
| testModeActionsControlEditingAvailability(); | testModeActionsControlEditingAvailability(); | ||||
| testPlcConfigurationUsesDialog(); | testPlcConfigurationUsesDialog(); | ||||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | testRepeatedPlcStatusNotificationsAreCoalesced(); | ||||
| @@ -11,6 +11,7 @@ INCLUDEPATH += ../src | |||||
| SOURCES += \ | SOURCES += \ | ||||
| main_window_tests.cpp \ | main_window_tests.cpp \ | ||||
| ../src/ui/main_window.cpp \ | ../src/ui/main_window.cpp \ | ||||
| ../src/ui/alarm_configuration_dialog.cpp \ | |||||
| ../src/ui/plc_connection_dialog.cpp \ | ../src/ui/plc_connection_dialog.cpp \ | ||||
| ../src/ui/free_monitor_widget.cpp \ | ../src/ui/free_monitor_widget.cpp \ | ||||
| ../src/ui/runtime_monitor_widget.cpp \ | ../src/ui/runtime_monitor_widget.cpp \ | ||||
| @@ -20,11 +21,14 @@ SOURCES += \ | |||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/active_register_repository.cpp \ | ../src/domain/active_register_repository.cpp \ | ||||
| ../src/domain/register_monitor_model.cpp \ | ../src/domain/register_monitor_model.cpp \ | ||||
| ../src/domain/alarm_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| ../src/domain/runtime_state.cpp \ | ../src/domain/runtime_state.cpp \ | ||||
| ../src/services/project_service.cpp \ | ../src/services/project_service.cpp \ | ||||
| ../src/services/alarm_editor_service.cpp \ | |||||
| ../src/services/alarm_service.cpp \ | |||||
| ../src/services/hmi_editor_service.cpp \ | ../src/services/hmi_editor_service.cpp \ | ||||
| ../src/services/hmi_navigation_service.cpp \ | ../src/services/hmi_navigation_service.cpp \ | ||||
| ../src/services/logic_editor_service.cpp \ | ../src/services/logic_editor_service.cpp \ | ||||
| @@ -36,6 +40,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/ui/main_window.h \ | ../src/ui/main_window.h \ | ||||
| ../src/ui/alarm_configuration_dialog.h \ | |||||
| ../src/ui/plc_connection_dialog.h \ | ../src/ui/plc_connection_dialog.h \ | ||||
| ../src/ui/free_monitor_widget.h \ | ../src/ui/free_monitor_widget.h \ | ||||
| ../src/ui/runtime_monitor_widget.h \ | ../src/ui/runtime_monitor_widget.h \ | ||||
| @@ -45,12 +50,15 @@ HEADERS += \ | |||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/active_register_repository.h \ | ../src/domain/active_register_repository.h \ | ||||
| ../src/domain/register_monitor_model.h \ | ../src/domain/register_monitor_model.h \ | ||||
| ../src/domain/alarm_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| ../src/domain/project_storage.h \ | ../src/domain/project_storage.h \ | ||||
| ../src/domain/runtime_state.h \ | ../src/domain/runtime_state.h \ | ||||
| ../src/services/project_service.h \ | ../src/services/project_service.h \ | ||||
| ../src/services/alarm_editor_service.h \ | |||||
| ../src/services/alarm_service.h \ | |||||
| ../src/services/hmi_editor_service.h \ | ../src/services/hmi_editor_service.h \ | ||||
| ../src/services/hmi_navigation_service.h \ | ../src/services/hmi_navigation_service.h \ | ||||
| ../src/services/logic_editor_service.h \ | ../src/services/logic_editor_service.h \ | ||||
| @@ -65,6 +73,7 @@ HEADERS += \ | |||||
| FORMS += \ | FORMS += \ | ||||
| ../src/ui/main_window.ui \ | ../src/ui/main_window.ui \ | ||||
| ../src/ui/alarm_configuration_dialog.ui \ | |||||
| ../src/ui/plc_connection_dialog.ui \ | ../src/ui/plc_connection_dialog.ui \ | ||||
| ../src/ui/free_monitor_widget.ui \ | ../src/ui/free_monitor_widget.ui \ | ||||
| ../src/ui/runtime_monitor_widget.ui | ../src/ui/runtime_monitor_widget.ui | ||||
| @@ -14,6 +14,7 @@ SOURCES += \ | |||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/active_register_repository.cpp \ | ../src/domain/active_register_repository.cpp \ | ||||
| ../src/domain/register_monitor_model.cpp \ | ../src/domain/register_monitor_model.cpp \ | ||||
| ../src/domain/alarm_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -30,6 +31,7 @@ HEADERS += \ | |||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/active_register_repository.h \ | ../src/domain/active_register_repository.h \ | ||||
| ../src/domain/register_monitor_model.h \ | ../src/domain/register_monitor_model.h \ | ||||
| ../src/domain/alarm_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| @@ -72,6 +72,12 @@ Project makeExampleProject() | |||||
| settings_jump.text = "Settings"; | settings_jump.text = "Settings"; | ||||
| settings_jump.pageJump = HmiPageJumpConfig{"settings-page"}; | settings_jump.pageJump = HmiPageJumpConfig{"settings-page"}; | ||||
| HmiControl alarm_list; | |||||
| alarm_list.id = "alarm-list"; | |||||
| alarm_list.type = HmiControlType::AlarmList; | |||||
| alarm_list.bounds = {10, 200, 360, 180}; | |||||
| alarm_list.text = "Alarms"; | |||||
| HmiPage page; | HmiPage page; | ||||
| page.id = "main-page"; | page.id = "main-page"; | ||||
| page.name = "Main"; | page.name = "Main"; | ||||
| @@ -81,6 +87,7 @@ Project makeExampleProject() | |||||
| page.controls.push_back(target_input); | page.controls.push_back(target_input); | ||||
| page.controls.push_back(title_label); | page.controls.push_back(title_label); | ||||
| page.controls.push_back(settings_jump); | page.controls.push_back(settings_jump); | ||||
| page.controls.push_back(alarm_list); | |||||
| HmiPage settings_page; | HmiPage settings_page; | ||||
| settings_page.id = "settings-page"; | settings_page.id = "settings-page"; | ||||
| @@ -139,6 +146,18 @@ Project makeExampleProject() | |||||
| project.hmiPages.push_back(page); | project.hmiPages.push_back(page); | ||||
| project.hmiPages.push_back(settings_page); | project.hmiPages.push_back(settings_page); | ||||
| project.initialHmiPageId = page.id; | project.initialHmiPageId = page.id; | ||||
| project.alarmDefinitions.push_back( | |||||
| {"alarm-emergency", | |||||
| RegisterAddress{RegisterArea::M, 10}, | |||||
| AlarmCondition::MOn, | |||||
| 0, | |||||
| "Emergency stop"}); | |||||
| project.alarmDefinitions.push_back( | |||||
| {"alarm-temperature", | |||||
| RegisterAddress{RegisterArea::D, 2}, | |||||
| AlarmCondition::DHigh, | |||||
| 80, | |||||
| "Temperature high"}); | |||||
| project.controlLogics.push_back(logic); | project.controlLogics.push_back(logic); | ||||
| ControlLogic draft_logic; | ControlLogic draft_logic; | ||||
| draft_logic.id = "draft-logic"; | draft_logic.id = "draft-logic"; | ||||
| @@ -188,6 +207,8 @@ void testEmptyProjectRoundTrip() | |||||
| require(service.project().hmiPages.empty(), "empty project must have no HMI pages"); | require(service.project().hmiPages.empty(), "empty project must have no HMI pages"); | ||||
| require(service.project().controlLogics.empty(), | require(service.project().controlLogics.empty(), | ||||
| "empty project must have no control logics"); | "empty project must have no control logics"); | ||||
| require(service.project().alarmDefinitions.empty(), | |||||
| "empty project must have no alarm definitions"); | |||||
| } | } | ||||
| void testExampleProjectRoundTrip() | void testExampleProjectRoundTrip() | ||||
| @@ -205,6 +226,7 @@ void testExampleProjectRoundTrip() | |||||
| const QString invalid_operation_path = directory.filePath("invalid-operation.json"); | const QString invalid_operation_path = directory.filePath("invalid-operation.json"); | ||||
| const QString missing_initial_path = directory.filePath("missing-initial.json"); | const QString missing_initial_path = directory.filePath("missing-initial.json"); | ||||
| const QString missing_target_path = directory.filePath("missing-target.json"); | const QString missing_target_path = directory.filePath("missing-target.json"); | ||||
| const QString missing_alarms_path = directory.filePath("missing-alarms.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); | ||||
| @@ -218,8 +240,10 @@ void testExampleProjectRoundTrip() | |||||
| require(saved_json.contains("\"formatVersion\": \"1.0\"") | require(saved_json.contains("\"formatVersion\": \"1.0\"") | ||||
| && saved_json.contains("\"buttonOperation\": \"setOn\"") | && saved_json.contains("\"buttonOperation\": \"setOn\"") | ||||
| && saved_json.contains("\"initialHmiPageId\": \"main-page\"") | && saved_json.contains("\"initialHmiPageId\": \"main-page\"") | ||||
| && saved_json.contains("\"targetPageId\": \"settings-page\""), | |||||
| "version 1.0 projects must persist the current multi-page schema"); | |||||
| && saved_json.contains("\"targetPageId\": \"settings-page\"") | |||||
| && saved_json.contains("\"alarmDefinitions\"") | |||||
| && saved_json.contains("\"type\": \"alarmList\""), | |||||
| "version 1.0 projects must persist pages and alarm definitions"); | |||||
| 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"); | ||||
| @@ -234,8 +258,8 @@ void testExampleProjectRoundTrip() | |||||
| require(project.hmiPages.size() == 2, "HMI page count must survive round trip"); | require(project.hmiPages.size() == 2, "HMI page count must survive round trip"); | ||||
| require(project.initialHmiPageId == "main-page", | require(project.initialHmiPageId == "main-page", | ||||
| "the initial HMI page id must survive round trip"); | "the initial HMI page id must survive round trip"); | ||||
| require(project.hmiPages.front().controls.size() == 6, | |||||
| "register, Label and PageJump controls must survive round trip"); | |||||
| require(project.hmiPages.front().controls.size() == 7, | |||||
| "register, navigation and AlarmList controls must survive round trip"); | |||||
| 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"); | ||||
| @@ -254,6 +278,14 @@ void testExampleProjectRoundTrip() | |||||
| && project.hmiPages.front().controls.at(5).pageJump->targetPageId | && project.hmiPages.front().controls.at(5).pageJump->targetPageId | ||||
| == "settings-page", | == "settings-page", | ||||
| "Label and PageJump typed data must survive round trip"); | "Label and PageJump typed data must survive round trip"); | ||||
| require(project.hmiPages.front().controls.at(6).type | |||||
| == HmiControlType::AlarmList, | |||||
| "AlarmList control type must survive round trip"); | |||||
| require(project.alarmDefinitions.size() == 2 | |||||
| && project.alarmDefinitions.front().condition | |||||
| == AlarmCondition::MOn | |||||
| && project.alarmDefinitions.at(1).threshold == 80, | |||||
| "M and D alarm definitions must survive round trip"); | |||||
| require(project.controlLogics.size() == 2, | require(project.controlLogics.size() == 2, | ||||
| "control logic count must survive round trip"); | "control logic count must survive round trip"); | ||||
| require(!project.controlLogics.front().enabled, | require(!project.controlLogics.front().enabled, | ||||
| @@ -297,6 +329,16 @@ void testExampleProjectRoundTrip() | |||||
| && missing_result.storageError == ProjectStorageError::MissingField, | && missing_result.storageError == ProjectStorageError::MissingField, | ||||
| "the 1.0 schema must require initialHmiPageId without migration defaults"); | "the 1.0 schema must require initialHmiPageId without migration defaults"); | ||||
| QJsonObject missing_alarms = QJsonDocument::fromJson(saved_json).object(); | |||||
| missing_alarms.remove(QStringLiteral("alarmDefinitions")); | |||||
| writeText( | |||||
| missing_alarms_path, | |||||
| QJsonDocument(missing_alarms).toJson(QJsonDocument::Compact)); | |||||
| missing_result = service.load(missing_alarms_path.toStdString()); | |||||
| require(!missing_result.succeeded | |||||
| && missing_result.storageError == ProjectStorageError::MissingField, | |||||
| "the 1.0 schema must require alarmDefinitions without migration defaults"); | |||||
| QJsonObject missing_target = QJsonDocument::fromJson(saved_json).object(); | QJsonObject missing_target = QJsonDocument::fromJson(saved_json).object(); | ||||
| QJsonArray pages = missing_target.value(QStringLiteral("hmiPages")).toArray(); | QJsonArray pages = missing_target.value(QStringLiteral("hmiPages")).toArray(); | ||||
| QJsonObject main_page = pages.at(0).toObject(); | QJsonObject main_page = pages.at(0).toObject(); | ||||
| @@ -12,6 +12,7 @@ SOURCES += \ | |||||
| project_management_tests.cpp \ | project_management_tests.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/alarm_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -22,6 +23,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/alarm_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| @@ -46,7 +46,11 @@ public: | |||||
| initial_read = false; | initial_read = false; | ||||
| } | } | ||||
| void setPollAddresses(const std::vector<RegisterAddress> &) override {} | |||||
| void setPollAddresses( | |||||
| const std::vector<RegisterAddress> &addresses) override | |||||
| { | |||||
| poll_addresses = addresses; | |||||
| } | |||||
| PlcConnectionState state() const override { return connection_state; } | PlcConnectionState state() const override { return connection_state; } | ||||
| bool initialReadCompleted() const override { return initial_read; } | bool initialReadCompleted() const override { return initial_read; } | ||||
| PlcCommunicationError lastErrorType() const override | PlcCommunicationError lastErrorType() const override | ||||
| @@ -76,6 +80,11 @@ public: | |||||
| } | } | ||||
| } | } | ||||
| const std::vector<RegisterAddress> &pollAddresses() const | |||||
| { | |||||
| return poll_addresses; | |||||
| } | |||||
| private: | private: | ||||
| PlcConnectionState connection_state = PlcConnectionState::Disconnected; | PlcConnectionState connection_state = PlcConnectionState::Disconnected; | ||||
| bool initial_read = false; | bool initial_read = false; | ||||
| @@ -84,6 +93,7 @@ private: | |||||
| std::function<void(bool)> initial_read_changed; | std::function<void(bool)> initial_read_changed; | ||||
| std::function<void()> cache_updated; | std::function<void()> cache_updated; | ||||
| std::function<void(const std::string &)> error_reported; | std::function<void(const std::string &)> error_reported; | ||||
| std::vector<RegisterAddress> poll_addresses; | |||||
| }; | }; | ||||
| void require(bool condition, const std::string &message) | void require(bool condition, const std::string &message) | ||||
| @@ -108,6 +118,12 @@ void testModeTransitions() | |||||
| service.configurePlc( | service.configurePlc( | ||||
| gateway, active_repository, virtual_repository, plc_repository); | gateway, active_repository, virtual_repository, plc_repository); | ||||
| Project &project = project_service.editProject(); | |||||
| project.alarmDefinitions.push_back( | |||||
| {"alarm-m", {RegisterArea::M, 12}, AlarmCondition::MOn, 0, "M alarm"}); | |||||
| project.alarmDefinitions.push_back( | |||||
| {"alarm-d", {RegisterArea::D, 34}, AlarmCondition::DHigh, 100, "D alarm"}); | |||||
| require(service.mode() == ApplicationMode::Editing, | require(service.mode() == ApplicationMode::Editing, | ||||
| "service must start in editing mode"); | "service must start in editing mode"); | ||||
| require(service.policy().allowsProjectEditing, | require(service.policy().allowsProjectEditing, | ||||
| @@ -134,6 +150,10 @@ void testModeTransitions() | |||||
| require(service.connectPlc( | require(service.connectPlc( | ||||
| {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded, | {"COM9", 1, 9600, 8, 2, 1, 1000, 2, 200}).succeeded, | ||||
| "PLC connection must be established before its initial read can complete"); | "PLC connection must be established before its initial read can complete"); | ||||
| require(gateway.pollAddresses() | |||||
| == std::vector<RegisterAddress>({ | |||||
| {RegisterArea::M, 12}, {RegisterArea::D, 34}}), | |||||
| "M and D alarm addresses must be included in PLC polling"); | |||||
| gateway.completeInitialRead(); | gateway.completeInitialRead(); | ||||
| require(service.initialPlcReadCompleted(), | require(service.initialPlcReadCompleted(), | ||||
| "service must retain the initial PLC read state"); | "service must retain the initial PLC read state"); | ||||
| @@ -14,6 +14,7 @@ SOURCES += \ | |||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/active_register_repository.cpp \ | ../src/domain/active_register_repository.cpp \ | ||||
| ../src/domain/register_monitor_model.cpp \ | ../src/domain/register_monitor_model.cpp \ | ||||
| ../src/domain/alarm_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -28,6 +29,7 @@ HEADERS += \ | |||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/active_register_repository.h \ | ../src/domain/active_register_repository.h \ | ||||
| ../src/domain/register_monitor_model.h \ | ../src/domain/register_monitor_model.h \ | ||||
| ../src/domain/alarm_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||