| @@ -32,6 +32,7 @@ SOURCES += \ | |||
| src/domain/register_monitor_model.cpp \ | |||
| src/domain/alarm_model.cpp \ | |||
| src/domain/hmi_model.cpp \ | |||
| src/domain/hmi_control_registry.cpp \ | |||
| src/domain/control_logic_model.cpp \ | |||
| src/domain/project_model.cpp \ | |||
| src/domain/runtime_state.cpp \ | |||
| @@ -71,6 +72,7 @@ HEADERS += \ | |||
| src/domain/register_monitor_model.h \ | |||
| src/domain/alarm_model.h \ | |||
| src/domain/hmi_model.h \ | |||
| src/domain/hmi_control_registry.h \ | |||
| src/domain/control_logic_model.h \ | |||
| src/domain/project_model.h \ | |||
| src/domain/runtime_state.h \ | |||
| @@ -0,0 +1,125 @@ | |||
| #include "hmi_control_registry.h" | |||
| #include <algorithm> | |||
| #include <array> | |||
| #include <cstddef> | |||
| namespace { | |||
| constexpr std::array kControlDescriptors = { | |||
| HmiControlDescriptor{HmiControlType::Button, | |||
| "button", | |||
| "按钮", | |||
| "button", | |||
| "按钮", | |||
| {0, 0, 120, 40}, | |||
| HmiBindingKind::Bit, | |||
| HmiRuntimeValueKind::Bit, | |||
| true}, | |||
| HmiControlDescriptor{HmiControlType::Indicator, | |||
| "indicator", | |||
| "指示灯", | |||
| "indicator", | |||
| "指示灯", | |||
| {0, 0, 64, 64}, | |||
| HmiBindingKind::Bit, | |||
| HmiRuntimeValueKind::Bit, | |||
| true}, | |||
| HmiControlDescriptor{HmiControlType::NumericDisplay, | |||
| "numericDisplay", | |||
| "数值显示", | |||
| "numeric-display", | |||
| "数值显示", | |||
| {0, 0, 120, 40}, | |||
| HmiBindingKind::Word, | |||
| HmiRuntimeValueKind::Word, | |||
| true}, | |||
| HmiControlDescriptor{HmiControlType::NumericInput, | |||
| "numericInput", | |||
| "数值输入", | |||
| "numeric-input", | |||
| "数值输入", | |||
| {0, 0, 120, 40}, | |||
| HmiBindingKind::Word, | |||
| HmiRuntimeValueKind::Word, | |||
| true}, | |||
| HmiControlDescriptor{HmiControlType::Label, | |||
| "label", | |||
| "文本", | |||
| "label", | |||
| "文本", | |||
| {0, 0, 120, 40}, | |||
| HmiBindingKind::None, | |||
| HmiRuntimeValueKind::None, | |||
| false}, | |||
| HmiControlDescriptor{HmiControlType::PageJump, | |||
| "pageJump", | |||
| "页面跳转", | |||
| "page-jump", | |||
| "页面跳转", | |||
| {0, 0, 120, 40}, | |||
| HmiBindingKind::None, | |||
| HmiRuntimeValueKind::None, | |||
| false}, | |||
| HmiControlDescriptor{HmiControlType::AlarmList, | |||
| "alarmList", | |||
| "报警列表", | |||
| "alarm-list", | |||
| "报警信息", | |||
| {0, 0, 360, 180}, | |||
| HmiBindingKind::None, | |||
| HmiRuntimeValueKind::None, | |||
| false} | |||
| }; | |||
| static_assert( | |||
| kControlDescriptors.size() | |||
| == static_cast<std::size_t>(HmiControlType::Count), | |||
| "每个 HMI 控件类型都必须注册描述"); | |||
| } // namespace | |||
| const HmiControlDescriptor *findHmiControlDescriptor(HmiControlType type) | |||
| { | |||
| const auto descriptor = std::find_if( | |||
| kControlDescriptors.cbegin(), | |||
| kControlDescriptors.cend(), | |||
| [type](const HmiControlDescriptor &candidate) | |||
| { | |||
| return candidate.type == type; | |||
| }); | |||
| return descriptor == kControlDescriptors.cend() ? nullptr : &*descriptor; | |||
| } | |||
| const HmiControlDescriptor *findHmiControlDescriptor( | |||
| std::string_view storage_name) | |||
| { | |||
| const auto descriptor = std::find_if( | |||
| kControlDescriptors.cbegin(), | |||
| kControlDescriptors.cend(), | |||
| [storage_name](const HmiControlDescriptor &candidate) | |||
| { | |||
| return storage_name == candidate.storageName; | |||
| }); | |||
| return descriptor == kControlDescriptors.cend() ? nullptr : &*descriptor; | |||
| } | |||
| std::optional<RegisterArea> hmiBindingArea(HmiBindingKind binding_kind) | |||
| { | |||
| switch (binding_kind) | |||
| { | |||
| case HmiBindingKind::Bit: | |||
| { | |||
| return RegisterArea::M; | |||
| } | |||
| case HmiBindingKind::Word: | |||
| { | |||
| return RegisterArea::D; | |||
| } | |||
| case HmiBindingKind::None: | |||
| default: | |||
| { | |||
| return std::nullopt; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,68 @@ | |||
| /** | |||
| * @file hmi_control_registry.h | |||
| * @brief 定义 HMI 控件公共描述及查询接口 | |||
| * @author suyu | |||
| * @date 2026-08-17 | |||
| */ | |||
| #pragma once | |||
| #include "hmi_model.h" | |||
| #include <optional> | |||
| #include <string_view> | |||
| /** | |||
| * @brief 控件允许绑定的寄存器数据类型 | |||
| */ | |||
| enum class HmiBindingKind | |||
| { | |||
| None, | |||
| Bit, | |||
| Word | |||
| }; | |||
| /** | |||
| * @brief 控件运行态读取的值类型 | |||
| */ | |||
| enum class HmiRuntimeValueKind | |||
| { | |||
| None, | |||
| Bit, | |||
| Word | |||
| }; | |||
| /** | |||
| * @brief 保存一种 HMI 控件的稳定名称、默认值和寄存器能力 | |||
| */ | |||
| struct HmiControlDescriptor | |||
| { | |||
| HmiControlType type; | |||
| const char *storageName; | |||
| const char *displayName; | |||
| const char *idPrefix; | |||
| const char *defaultText; | |||
| HmiRect defaultBounds; | |||
| HmiBindingKind bindingKind; | |||
| HmiRuntimeValueKind runtimeValueKind; | |||
| bool requiresBindingForRunning; | |||
| }; | |||
| /** | |||
| * @brief 按领域类型查找控件描述 | |||
| * @return 找到时返回描述指针,不支持的类型返回 nullptr | |||
| */ | |||
| const HmiControlDescriptor *findHmiControlDescriptor(HmiControlType type); | |||
| /** | |||
| * @brief 按工程文件中的稳定类型名查找控件描述 | |||
| * @return 找到时返回描述指针,未知名称返回 nullptr | |||
| */ | |||
| const HmiControlDescriptor *findHmiControlDescriptor( | |||
| std::string_view storage_name); | |||
| /** | |||
| * @brief 返回绑定类型要求的寄存器区域 | |||
| * @return 不支持寄存器绑定时返回空值 | |||
| */ | |||
| std::optional<RegisterArea> hmiBindingArea(HmiBindingKind binding_kind); | |||
| @@ -1,5 +1,7 @@ | |||
| #include "hmi_model.h" | |||
| #include "hmi_control_registry.h" | |||
| #include <algorithm> | |||
| namespace { | |||
| @@ -13,44 +15,12 @@ void setError(std::string *error, const std::string &message) | |||
| } | |||
| } | |||
| // 判断控件是否必须绑定 M 区位地址 | |||
| bool requiresBitBinding(HmiControlType type) | |||
| { | |||
| return type == HmiControlType::Button || type == HmiControlType::Indicator; | |||
| } | |||
| // 判断控件是否必须绑定 D 区字地址 | |||
| bool requiresWordBinding(HmiControlType type) | |||
| { | |||
| return type == HmiControlType::NumericDisplay || type == HmiControlType::NumericInput; | |||
| } | |||
| bool supportsRegisterBinding(HmiControlType type) | |||
| { | |||
| return requiresBitBinding(type) || requiresWordBinding(type); | |||
| } | |||
| bool isSupportedControlType(HmiControlType type) | |||
| { | |||
| switch (type) | |||
| { | |||
| case HmiControlType::Button: | |||
| case HmiControlType::Indicator: | |||
| case HmiControlType::NumericDisplay: | |||
| case HmiControlType::NumericInput: | |||
| case HmiControlType::Label: | |||
| case HmiControlType::PageJump: | |||
| case HmiControlType::AlarmList: | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| } // namespace | |||
| bool HmiControl::validate(std::string *error) const | |||
| { | |||
| if (!isSupportedControlType(type)) | |||
| const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); | |||
| if (descriptor == nullptr) | |||
| { | |||
| setError(error, "不支持的 HMI 控件类型"); | |||
| return false; | |||
| @@ -73,32 +43,31 @@ bool HmiControl::validate(std::string *error) const | |||
| return false; | |||
| } | |||
| } | |||
| if (requiresBitBinding(type)) | |||
| const std::optional<RegisterArea> binding_area = | |||
| hmiBindingArea(descriptor->bindingKind); | |||
| if (binding.has_value() && !binding_area.has_value()) | |||
| { | |||
| if (binding.has_value() && binding->area() != RegisterArea::M) | |||
| setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器"); | |||
| return false; | |||
| } | |||
| if (binding.has_value() && binding_area.has_value() | |||
| && binding->area() != *binding_area) | |||
| { | |||
| if (descriptor->bindingKind == HmiBindingKind::Bit) | |||
| { | |||
| setError(error, "按钮和指示灯必须绑定 M 区地址"); | |||
| return false; | |||
| } | |||
| } | |||
| if (requiresWordBinding(type)) | |||
| { | |||
| if (binding.has_value() && binding->area() != RegisterArea::D) | |||
| else | |||
| { | |||
| setError(error, "数值控件必须绑定 D 区地址"); | |||
| return false; | |||
| } | |||
| return false; | |||
| } | |||
| if (binding.has_value() && !binding->isValid()) | |||
| { | |||
| setError(error, "HMI 控件绑定了无效地址"); | |||
| return false; | |||
| } | |||
| if (!supportsRegisterBinding(type) && binding.has_value()) | |||
| { | |||
| setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器"); | |||
| return false; | |||
| } | |||
| if (type == HmiControlType::PageJump) | |||
| { | |||
| if (!pageJump.has_value()) | |||
| @@ -117,7 +86,13 @@ bool HmiControl::validate(std::string *error) const | |||
| bool HmiControl::isConfigured() const | |||
| { | |||
| if (type == HmiControlType::Label || type == HmiControlType::AlarmList) | |||
| const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); | |||
| if (descriptor == nullptr) | |||
| { | |||
| return false; | |||
| } | |||
| if (!descriptor->requiresBindingForRunning | |||
| && type != HmiControlType::PageJump) | |||
| { | |||
| return true; | |||
| } | |||
| @@ -125,15 +100,15 @@ bool HmiControl::isConfigured() const | |||
| { | |||
| return pageJump.has_value() && !pageJump->targetPageId.empty(); | |||
| } | |||
| if (!binding.has_value() || !binding->isValid()) | |||
| if (!descriptor->requiresBindingForRunning | |||
| || !binding.has_value() | |||
| || !binding->isValid()) | |||
| { | |||
| return false; | |||
| } | |||
| if (requiresBitBinding(type)) | |||
| { | |||
| return binding->area() == RegisterArea::M; | |||
| } | |||
| return !requiresWordBinding(type) || binding->area() == RegisterArea::D; | |||
| const std::optional<RegisterArea> binding_area = | |||
| hmiBindingArea(descriptor->bindingKind); | |||
| return binding_area.has_value() && binding->area() == *binding_area; | |||
| } | |||
| bool HmiPage::validate(std::string *error) const | |||
| @@ -43,7 +43,8 @@ enum class HmiControlType | |||
| NumericInput, // 数值输入 | |||
| Label, // 标签 | |||
| PageJump, // 页面跳转 | |||
| AlarmList // 报警列表 | |||
| AlarmList, // 报警列表 | |||
| Count // 已注册控件类型数量,不作为实际控件使用 | |||
| }; | |||
| /** | |||
| @@ -1,5 +1,7 @@ | |||
| #include "json_project_storage.h" | |||
| #include "domain/hmi_control_registry.h" | |||
| #include <QFile> | |||
| #include <QJsonArray> | |||
| #include <QJsonDocument> | |||
| @@ -392,81 +394,24 @@ bool parseWordOperand( | |||
| // 将 HMI 控件类型枚举转换为工程文件中的稳定字符串 | |||
| QString hmiControlTypeName(HmiControlType type) | |||
| { | |||
| switch (type) | |||
| { | |||
| case HmiControlType::Button: | |||
| { | |||
| return QStringLiteral("button"); | |||
| } | |||
| case HmiControlType::Indicator: | |||
| { | |||
| return QStringLiteral("indicator"); | |||
| } | |||
| case HmiControlType::NumericDisplay: | |||
| { | |||
| return QStringLiteral("numericDisplay"); | |||
| } | |||
| case HmiControlType::NumericInput: | |||
| { | |||
| return QStringLiteral("numericInput"); | |||
| } | |||
| case HmiControlType::Label: | |||
| { | |||
| return QStringLiteral("label"); | |||
| } | |||
| case HmiControlType::PageJump: | |||
| { | |||
| return QStringLiteral("pageJump"); | |||
| } | |||
| case HmiControlType::AlarmList: | |||
| { | |||
| return QStringLiteral("alarmList"); | |||
| } | |||
| default: | |||
| { | |||
| return {}; | |||
| } | |||
| } | |||
| const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); | |||
| return descriptor == nullptr | |||
| ? QString{} | |||
| : QString::fromLatin1(descriptor->storageName); | |||
| } | |||
| // 将工程文件中的控件类型字符串转换为 HMI 控件类型枚举 | |||
| bool parseHmiControlType( | |||
| const std::string &value, HmiControlType *type, ParseState *state) | |||
| { | |||
| if (value == "button") | |||
| { | |||
| *type = HmiControlType::Button; | |||
| } | |||
| else if (value == "indicator") | |||
| { | |||
| *type = HmiControlType::Indicator; | |||
| } | |||
| else if (value == "numericDisplay") | |||
| { | |||
| *type = HmiControlType::NumericDisplay; | |||
| } | |||
| else if (value == "numericInput") | |||
| { | |||
| *type = HmiControlType::NumericInput; | |||
| } | |||
| else if (value == "label") | |||
| { | |||
| *type = HmiControlType::Label; | |||
| } | |||
| else if (value == "pageJump") | |||
| { | |||
| *type = HmiControlType::PageJump; | |||
| } | |||
| else if (value == "alarmList") | |||
| { | |||
| *type = HmiControlType::AlarmList; | |||
| } | |||
| else | |||
| const HmiControlDescriptor *descriptor = findHmiControlDescriptor(value); | |||
| if (descriptor == nullptr) | |||
| { | |||
| return state->fail( | |||
| ProjectStorageError::InvalidField, | |||
| "不支持的 HMI 控件类型:" + value); | |||
| } | |||
| *type = descriptor->type; | |||
| return true; | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| #include "hmi_editor_service.h" | |||
| #include "domain/hmi_control_registry.h" | |||
| #include "project_service.h" | |||
| #include <algorithm> | |||
| @@ -17,84 +18,6 @@ void setError(std::string *error, const std::string &message) | |||
| } | |||
| } | |||
| std::string controlPrefix(HmiControlType type) | |||
| { | |||
| switch (type) | |||
| { | |||
| case HmiControlType::Button: | |||
| { | |||
| return "button"; | |||
| } | |||
| case HmiControlType::Indicator: | |||
| { | |||
| return "indicator"; | |||
| } | |||
| case HmiControlType::NumericDisplay: | |||
| { | |||
| return "numeric-display"; | |||
| } | |||
| case HmiControlType::NumericInput: | |||
| { | |||
| return "numeric-input"; | |||
| } | |||
| case HmiControlType::Label: | |||
| { | |||
| return "label"; | |||
| } | |||
| case HmiControlType::PageJump: | |||
| { | |||
| return "page-jump"; | |||
| } | |||
| case HmiControlType::AlarmList: | |||
| { | |||
| return "alarm-list"; | |||
| } | |||
| default: | |||
| { | |||
| return "control"; | |||
| } | |||
| } | |||
| } | |||
| std::string defaultText(HmiControlType type) | |||
| { | |||
| switch (type) | |||
| { | |||
| case HmiControlType::Button: | |||
| { | |||
| return "按钮"; | |||
| } | |||
| case HmiControlType::Indicator: | |||
| { | |||
| return "指示灯"; | |||
| } | |||
| case HmiControlType::NumericDisplay: | |||
| { | |||
| return "数值显示"; | |||
| } | |||
| case HmiControlType::NumericInput: | |||
| { | |||
| return "数值输入"; | |||
| } | |||
| case HmiControlType::Label: | |||
| { | |||
| return "文本"; | |||
| } | |||
| case HmiControlType::PageJump: | |||
| { | |||
| return "页面跳转"; | |||
| } | |||
| case HmiControlType::AlarmList: | |||
| { | |||
| return "报警信息"; | |||
| } | |||
| default: | |||
| { | |||
| return "控件"; | |||
| } | |||
| } | |||
| } | |||
| HmiEditorResult failure(HmiEditorError error, const std::string &message) | |||
| { | |||
| return {false, error, message, {}}; | |||
| @@ -351,9 +274,14 @@ HmiEditorResult HmiEditorService::addControl( | |||
| { | |||
| return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); | |||
| } | |||
| const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); | |||
| if (descriptor == nullptr) | |||
| { | |||
| return failure(HmiEditorError::InvalidControl, "不支持的 HMI 控件类型"); | |||
| } | |||
| // 新控件初始不绑定寄存器,避免自动分配地址造成误写风险 | |||
| HmiControl control = makeControl(*page, type); | |||
| HmiControl control = makeControl(*page, *descriptor); | |||
| Project &project = project_service_.editProject(); | |||
| auto target_page = std::find_if( | |||
| project.hmiPages.begin(), | |||
| @@ -484,27 +412,11 @@ HmiEditorResult HmiEditorService::updateControl( | |||
| bool HmiEditorService::validateEditableControl( | |||
| const HmiPage &page, const HmiControl &control, std::string *error) const | |||
| { | |||
| switch (control.type) | |||
| { | |||
| case HmiControlType::Button: | |||
| case HmiControlType::Indicator: | |||
| case HmiControlType::NumericDisplay: | |||
| case HmiControlType::NumericInput: | |||
| case HmiControlType::Label: | |||
| case HmiControlType::PageJump: | |||
| case HmiControlType::AlarmList: | |||
| break; | |||
| default: | |||
| setError(error, "不支持的 HMI 控件类型"); | |||
| return false; | |||
| } | |||
| if (control.id.empty()) | |||
| if (!control.validate(error)) | |||
| { | |||
| setError(error, "HMI 控件 ID 不能为空"); | |||
| return false; | |||
| } | |||
| if (control.bounds.width <= 0 || control.bounds.height <= 0 | |||
| || control.bounds.x < 0 || control.bounds.y < 0 | |||
| if (control.bounds.x < 0 || control.bounds.y < 0 | |||
| || control.bounds.width > page.width || control.bounds.height > page.height | |||
| || control.bounds.x > page.width - control.bounds.width | |||
| || control.bounds.y > page.height - control.bounds.height) | |||
| @@ -512,60 +424,11 @@ bool HmiEditorService::validateEditableControl( | |||
| setError(error, "HMI 控件不能超出页面范围"); | |||
| return false; | |||
| } | |||
| for (const auto &property : control.properties) | |||
| { | |||
| if (property.first.empty()) | |||
| { | |||
| setError(error, "HMI 控件属性名称不能为空"); | |||
| return false; | |||
| } | |||
| } | |||
| if (control.binding.has_value() && !control.binding->isValid()) | |||
| { | |||
| setError(error, "HMI 控件绑定了无效地址"); | |||
| return false; | |||
| } | |||
| if (control.binding.has_value() | |||
| && (control.type == HmiControlType::Button | |||
| || control.type == HmiControlType::Indicator) | |||
| && control.binding->area() != RegisterArea::M) | |||
| { | |||
| setError(error, "按钮和指示灯必须绑定 M 区地址"); | |||
| return false; | |||
| } | |||
| if (control.binding.has_value() | |||
| && (control.type == HmiControlType::NumericDisplay | |||
| || control.type == HmiControlType::NumericInput) | |||
| && control.binding->area() != RegisterArea::D) | |||
| { | |||
| setError(error, "数值控件必须绑定 D 区地址"); | |||
| return false; | |||
| } | |||
| if ((control.type == HmiControlType::Label | |||
| || control.type == HmiControlType::PageJump | |||
| || control.type == HmiControlType::AlarmList) | |||
| && control.binding.has_value()) | |||
| { | |||
| setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器"); | |||
| return false; | |||
| } | |||
| if (control.type == HmiControlType::PageJump) | |||
| { | |||
| if (!control.pageJump.has_value()) | |||
| { | |||
| setError(error, "页面跳转控件缺少跳转配置"); | |||
| return false; | |||
| } | |||
| if (!control.pageJump->targetPageId.empty() | |||
| && findPage(control.pageJump->targetPageId) == nullptr) | |||
| { | |||
| setError(error, "页面跳转控件的目标页面不存在"); | |||
| return false; | |||
| } | |||
| } | |||
| else if (control.pageJump.has_value()) | |||
| if (control.type == HmiControlType::PageJump | |||
| && !control.pageJump->targetPageId.empty() | |||
| && findPage(control.pageJump->targetPageId) == nullptr) | |||
| { | |||
| setError(error, "非页面跳转控件不能包含跳转配置"); | |||
| setError(error, "页面跳转控件的目标页面不存在"); | |||
| return false; | |||
| } | |||
| return true; | |||
| @@ -586,23 +449,15 @@ bool HmiEditorService::hasDuplicateControlId( | |||
| // 根据控件类型生成默认样式、文字、大小、初始坐标 | |||
| HmiControl HmiEditorService::makeControl( | |||
| const HmiPage &page, HmiControlType type) | |||
| const HmiPage &page, const HmiControlDescriptor &descriptor) | |||
| { | |||
| HmiControl control; | |||
| control.id = makeUniqueId(page, controlPrefix(type)); | |||
| control.type = type; | |||
| control.text = defaultText(type); | |||
| 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) | |||
| control.id = makeUniqueId(page, descriptor.idPrefix); | |||
| control.type = descriptor.type; | |||
| control.text = descriptor.defaultText; | |||
| control.bounds.width = std::min(descriptor.defaultBounds.width, page.width); | |||
| control.bounds.height = std::min(descriptor.defaultBounds.height, page.height); | |||
| if (descriptor.type == HmiControlType::PageJump) | |||
| { | |||
| control.pageJump = HmiPageJumpConfig{}; | |||
| } | |||
| @@ -12,6 +12,7 @@ | |||
| #include <string> | |||
| class ProjectService; | |||
| struct HmiControlDescriptor; | |||
| /** | |||
| * @brief HMI 编辑操作的失败分类 | |||
| @@ -158,7 +159,7 @@ private: | |||
| * @return 未绑定寄存器的默认控件配置 | |||
| */ | |||
| static HmiControl makeControl( | |||
| const HmiPage &page, HmiControlType type); | |||
| const HmiPage &page, const HmiControlDescriptor &descriptor); | |||
| /** | |||
| * @brief 在页面内生成带指定前缀的唯一控件标识 | |||
| * @param page 待检查页面 | |||
| @@ -1,5 +1,7 @@ | |||
| #include "hmi_runtime_service.h" | |||
| #include "domain/hmi_control_registry.h" | |||
| namespace { | |||
| HmiRuntimeReadResult readFailure(HmiRuntimeError error) | |||
| @@ -12,17 +14,6 @@ HmiRuntimeWriteResult writeFailure(HmiRuntimeError error) | |||
| return {false, error}; | |||
| } | |||
| bool isBitControl(HmiControlType type) | |||
| { | |||
| return type == HmiControlType::Button || type == HmiControlType::Indicator; | |||
| } | |||
| bool isWordControl(HmiControlType type) | |||
| { | |||
| return type == HmiControlType::NumericDisplay | |||
| || type == HmiControlType::NumericInput; | |||
| } | |||
| } // namespace | |||
| HmiRuntimeService::HmiRuntimeService(RegisterRepository &repository) | |||
| @@ -32,8 +23,10 @@ HmiRuntimeService::HmiRuntimeService(RegisterRepository &repository) | |||
| HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) const | |||
| { | |||
| // 标签等静态控件不参与寄存器读写 | |||
| if (!isBitControl(control.type) && !isWordControl(control.type)) | |||
| const HmiControlDescriptor *descriptor = | |||
| findHmiControlDescriptor(control.type); | |||
| if (descriptor == nullptr | |||
| || descriptor->runtimeValueKind == HmiRuntimeValueKind::None) | |||
| { | |||
| return readFailure(HmiRuntimeError::UnsupportedControl); | |||
| } | |||
| @@ -45,7 +38,16 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c | |||
| { | |||
| return readFailure(HmiRuntimeError::InvalidBinding); | |||
| } | |||
| if (isBitControl(control.type)) | |||
| const std::optional<RegisterArea> binding_area = | |||
| hmiBindingArea(descriptor->bindingKind); | |||
| if (!binding_area.has_value() || control.binding->area() != *binding_area) | |||
| { | |||
| return readFailure(HmiRuntimeError::InvalidBinding); | |||
| } | |||
| switch (descriptor->runtimeValueKind) | |||
| { | |||
| case HmiRuntimeValueKind::Bit: | |||
| { | |||
| // 按控件类型选择仓库的 M 位读取契约 | |||
| const BitReadResult result = repository_.readBit(*control.binding); | |||
| @@ -55,7 +57,7 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c | |||
| } | |||
| return {true, HmiRuntimeError::None, result.value, 0}; | |||
| } | |||
| if (isWordControl(control.type)) | |||
| case HmiRuntimeValueKind::Word: | |||
| { | |||
| const WordReadResult result = repository_.readWord(*control.binding); | |||
| if (!result.succeeded) | |||
| @@ -64,7 +66,12 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c | |||
| } | |||
| return {true, HmiRuntimeError::None, false, result.value}; | |||
| } | |||
| return readFailure(HmiRuntimeError::UnsupportedControl); | |||
| case HmiRuntimeValueKind::None: | |||
| default: | |||
| { | |||
| return readFailure(HmiRuntimeError::UnsupportedControl); | |||
| } | |||
| } | |||
| } | |||
| HmiRuntimeWriteResult HmiRuntimeService::operateButton( | |||
| @@ -1,5 +1,6 @@ | |||
| #include "property_panel_controller.h" | |||
| #include "domain/hmi_control_registry.h" | |||
| #include "hmi_editor_widget.h" | |||
| #include "logic_editor_widget.h" | |||
| #include "logic_instruction_dialog.h" | |||
| @@ -33,45 +34,6 @@ std::string toUtf8(const QString &value) | |||
| return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | |||
| } | |||
| QString controlTypeText(HmiControlType type) | |||
| { | |||
| switch (type) | |||
| { | |||
| case HmiControlType::Button: | |||
| { | |||
| return QObject::tr("按钮"); | |||
| } | |||
| case HmiControlType::Indicator: | |||
| { | |||
| return QObject::tr("指示灯"); | |||
| } | |||
| case HmiControlType::NumericDisplay: | |||
| { | |||
| return QObject::tr("数值显示"); | |||
| } | |||
| case HmiControlType::NumericInput: | |||
| { | |||
| return QObject::tr("数值输入"); | |||
| } | |||
| case HmiControlType::Label: | |||
| { | |||
| return QObject::tr("文本"); | |||
| } | |||
| case HmiControlType::PageJump: | |||
| { | |||
| return QObject::tr("页面跳转"); | |||
| } | |||
| case HmiControlType::AlarmList: | |||
| { | |||
| return QObject::tr("报警列表"); | |||
| } | |||
| default: | |||
| { | |||
| return QObject::tr("未知控件"); | |||
| } | |||
| } | |||
| } | |||
| } // namespace | |||
| PropertyPanelController::PropertyPanelController( | |||
| @@ -174,6 +136,8 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||
| const HmiControl *control = hmi_editor_service_.findControl( | |||
| current_page_id_(), control_id); | |||
| const bool has_control = control != nullptr; | |||
| const HmiControlDescriptor *descriptor = has_control | |||
| ? findHmiControlDescriptor(control->type) : nullptr; | |||
| ui_.selectionValueLabel->setText( | |||
| has_control ? fromUtf8(control->id) | |||
| + (control->isConfigured() ? QString{} | |||
| @@ -219,9 +183,8 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||
| : control->binding->area() == RegisterArea::M ? 1 : 2); | |||
| ui_.bindingIndexSpinBox->setValue( | |||
| control->binding.has_value() ? control->binding->index() : 0); | |||
| const bool has_binding = control->type != HmiControlType::Label | |||
| && control->type != HmiControlType::PageJump | |||
| && control->type != HmiControlType::AlarmList; | |||
| const bool has_binding = descriptor != nullptr | |||
| && descriptor->bindingKind != HmiBindingKind::None; | |||
| ui_.bindingAreaLabel->setVisible(has_binding); | |||
| ui_.bindingAreaComboBox->setVisible(has_binding); | |||
| ui_.bindingIndexLabel->setVisible(has_binding); | |||
| @@ -434,7 +397,11 @@ void PropertyPanelController::addHmiControl(HmiControlType type) | |||
| hmi_editor_widget_->selectControl(result.id); | |||
| showControlProperties(result.id); | |||
| refresh_project_ui_(); | |||
| status_reporter_(QObject::tr("已添加%1").arg(controlTypeText(type)), 3000); | |||
| const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); | |||
| const QString display_name = descriptor == nullptr | |||
| ? QObject::tr("未知控件") | |||
| : QString::fromUtf8(descriptor->displayName); | |||
| status_reporter_(QObject::tr("已添加%1").arg(display_name), 3000); | |||
| } | |||
| void PropertyPanelController::deleteSelectedControl() | |||
| @@ -471,9 +438,10 @@ void PropertyPanelController::applySelectedControlProperties() | |||
| control.bounds.y = ui_.controlYSpinBox->value(); | |||
| control.bounds.width = ui_.controlWidthSpinBox->value(); | |||
| control.bounds.height = ui_.controlHeightSpinBox->value(); | |||
| if (control.type == HmiControlType::Label | |||
| || control.type == HmiControlType::PageJump | |||
| || control.type == HmiControlType::AlarmList) | |||
| const HmiControlDescriptor *descriptor = | |||
| findHmiControlDescriptor(control.type); | |||
| if (descriptor == nullptr | |||
| || descriptor->bindingKind == HmiBindingKind::None) | |||
| { | |||
| control.binding.reset(); | |||
| } | |||
| @@ -12,6 +12,7 @@ SOURCES += \ | |||
| ../src/domain/register_repository.cpp \ | |||
| ../src/domain/alarm_model.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/services/project_service.cpp \ | |||
| @@ -23,6 +24,7 @@ HEADERS += \ | |||
| ../src/domain/register_repository.h \ | |||
| ../src/domain/alarm_model.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/project_storage.h \ | |||
| @@ -1,4 +1,5 @@ | |||
| #include "domain/control_logic_model.h" | |||
| #include "domain/hmi_control_registry.h" | |||
| #include "domain/hmi_model.h" | |||
| #include "domain/project_model.h" | |||
| #include "domain/register_address.h" | |||
| @@ -8,6 +9,7 @@ | |||
| #include <cstdint> | |||
| #include <exception> | |||
| #include <iostream> | |||
| #include <set> | |||
| #include <stdexcept> | |||
| #include <string> | |||
| @@ -76,6 +78,92 @@ void testRegisterRepositorySeparatesAreas() | |||
| "M address must not be read as a word"); | |||
| } | |||
| void testHmiControlRegistryCompleteness() | |||
| { | |||
| std::set<const HmiControlDescriptor *> descriptors; | |||
| std::set<std::string> storage_names; | |||
| std::set<std::string> id_prefixes; | |||
| const std::size_t control_type_count = | |||
| static_cast<std::size_t>(HmiControlType::Count); | |||
| for (std::size_t index = 0; index < control_type_count; ++index) | |||
| { | |||
| const HmiControlType type = static_cast<HmiControlType>(index); | |||
| const HmiControlDescriptor *descriptor = | |||
| findHmiControlDescriptor(type); | |||
| require(descriptor != nullptr, | |||
| "every HMI control type must have one descriptor"); | |||
| require(descriptor->type == type, | |||
| "HMI type lookup must return the requested descriptor"); | |||
| require(descriptors.emplace(descriptor).second, | |||
| "each HMI control type must resolve to a different descriptor"); | |||
| require(descriptor->storageName != nullptr | |||
| && descriptor->storageName[0] != '\0', | |||
| "HMI storage names must not be empty"); | |||
| require(descriptor->displayName != nullptr | |||
| && descriptor->displayName[0] != '\0', | |||
| "HMI display names must not be empty"); | |||
| require(descriptor->idPrefix != nullptr | |||
| && descriptor->idPrefix[0] != '\0', | |||
| "HMI id prefixes must not be empty"); | |||
| require(descriptor->defaultText != nullptr, | |||
| "HMI default text must not be null"); | |||
| require(descriptor->defaultBounds.width > 0 | |||
| && descriptor->defaultBounds.height > 0, | |||
| "HMI default bounds must have a positive size"); | |||
| require(findHmiControlDescriptor(descriptor->storageName) == descriptor, | |||
| "HMI storage name lookup must return its registered descriptor"); | |||
| require(storage_names.emplace(descriptor->storageName).second, | |||
| "HMI storage names must be unique"); | |||
| require(id_prefixes.emplace(descriptor->idPrefix).second, | |||
| "HMI id prefixes must be unique"); | |||
| const std::optional<RegisterArea> binding_area = | |||
| hmiBindingArea(descriptor->bindingKind); | |||
| switch (descriptor->runtimeValueKind) | |||
| { | |||
| case HmiRuntimeValueKind::Bit: | |||
| { | |||
| require(descriptor->bindingKind == HmiBindingKind::Bit | |||
| && binding_area == RegisterArea::M, | |||
| "bit runtime controls must bind to the M area"); | |||
| break; | |||
| } | |||
| case HmiRuntimeValueKind::Word: | |||
| { | |||
| require(descriptor->bindingKind == HmiBindingKind::Word | |||
| && binding_area == RegisterArea::D, | |||
| "word runtime controls must bind to the D area"); | |||
| break; | |||
| } | |||
| case HmiRuntimeValueKind::None: | |||
| { | |||
| require(descriptor->bindingKind == HmiBindingKind::None | |||
| && !binding_area.has_value(), | |||
| "static controls must not declare a register binding"); | |||
| break; | |||
| } | |||
| } | |||
| require(descriptor->requiresBindingForRunning | |||
| == (descriptor->runtimeValueKind | |||
| != HmiRuntimeValueKind::None), | |||
| "runtime value controls must require a configured binding"); | |||
| } | |||
| require(findHmiControlDescriptor(HmiControlType::Count) == nullptr, | |||
| "the HMI control count marker must not be registered"); | |||
| require(findHmiControlDescriptor(static_cast<HmiControlType>(99)) == nullptr, | |||
| "unknown HMI control types must not resolve"); | |||
| require(findHmiControlDescriptor("unknown") == nullptr, | |||
| "unknown HMI storage names must not resolve"); | |||
| require(hmiBindingArea(HmiBindingKind::Bit) == RegisterArea::M, | |||
| "bit bindings must use the M area"); | |||
| require(hmiBindingArea(HmiBindingKind::Word) == RegisterArea::D, | |||
| "word bindings must use the D area"); | |||
| require(!hmiBindingArea(HmiBindingKind::None).has_value(), | |||
| "controls without bindings must not resolve a register area"); | |||
| } | |||
| Project makeValidProject() | |||
| { | |||
| // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线 | |||
| @@ -584,6 +672,7 @@ int main() | |||
| testRegisterAddressBoundaries(); | |||
| testRegisterAddressParsing(); | |||
| testRegisterRepositorySeparatesAreas(); | |||
| testHmiControlRegistryCompleteness(); | |||
| testLogicNodeConfigurationBoundaries(); | |||
| testTimerAndCommentBoundaries(); | |||
| testTimerReferencesForRunning(); | |||
| @@ -12,6 +12,7 @@ SOURCES += \ | |||
| ../src/domain/register_repository.cpp \ | |||
| ../src/domain/alarm_model.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/domain/runtime_state.cpp | |||
| @@ -21,6 +22,7 @@ HEADERS += \ | |||
| ../src/domain/register_repository.h \ | |||
| ../src/domain/alarm_model.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/runtime_state.h | |||
| @@ -12,6 +12,7 @@ SOURCES += \ | |||
| ../src/domain/register_repository.cpp \ | |||
| ../src/domain/alarm_model.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/services/project_service.cpp \ | |||
| @@ -24,6 +25,7 @@ HEADERS += \ | |||
| ../src/domain/register_repository.h \ | |||
| ../src/domain/alarm_model.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/project_storage.h \ | |||
| @@ -12,6 +12,7 @@ SOURCES += \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/register_repository.cpp \ | |||
| ../src/domain/alarm_model.cpp \ | |||
| ../src/services/project_service.cpp \ | |||
| @@ -22,6 +23,7 @@ HEADERS += \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/register_repository.h \ | |||
| ../src/domain/alarm_model.h \ | |||
| ../src/domain/project_storage.h \ | |||
| @@ -28,6 +28,7 @@ SOURCES += \ | |||
| ../src/domain/register_monitor_model.cpp \ | |||
| ../src/domain/alarm_model.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/domain/runtime_state.cpp \ | |||
| @@ -63,6 +64,7 @@ HEADERS += \ | |||
| ../src/domain/register_monitor_model.h \ | |||
| ../src/domain/alarm_model.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/project_storage.h \ | |||
| @@ -14,6 +14,7 @@ SOURCES += \ | |||
| ../src/domain/register_address.cpp \ | |||
| ../src/domain/register_repository.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/services/hmi_runtime_service.cpp \ | |||
| ../src/services/software_logic_executor.cpp \ | |||
| @@ -23,6 +24,7 @@ HEADERS += \ | |||
| ../src/domain/register_address.h \ | |||
| ../src/domain/register_repository.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/services/hmi_runtime_service.h \ | |||
| ../src/services/software_logic_executor.h \ | |||
| @@ -16,6 +16,7 @@ SOURCES += \ | |||
| ../src/domain/register_monitor_model.cpp \ | |||
| ../src/domain/alarm_model.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/domain/runtime_state.cpp \ | |||
| @@ -33,6 +34,7 @@ HEADERS += \ | |||
| ../src/domain/register_monitor_model.h \ | |||
| ../src/domain/alarm_model.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/project_storage.h \ | |||
| @@ -14,6 +14,7 @@ SOURCES += \ | |||
| ../src/domain/register_repository.cpp \ | |||
| ../src/domain/alarm_model.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/domain/runtime_state.cpp \ | |||
| @@ -26,6 +27,7 @@ HEADERS += \ | |||
| ../src/domain/register_repository.h \ | |||
| ../src/domain/alarm_model.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/runtime_state.h \ | |||
| @@ -16,6 +16,7 @@ SOURCES += \ | |||
| ../src/domain/register_monitor_model.cpp \ | |||
| ../src/domain/alarm_model.cpp \ | |||
| ../src/domain/hmi_model.cpp \ | |||
| ../src/domain/hmi_control_registry.cpp \ | |||
| ../src/domain/control_logic_model.cpp \ | |||
| ../src/domain/project_model.cpp \ | |||
| ../src/domain/runtime_state.cpp \ | |||
| @@ -31,6 +32,7 @@ HEADERS += \ | |||
| ../src/domain/register_monitor_model.h \ | |||
| ../src/domain/alarm_model.h \ | |||
| ../src/domain/hmi_model.h \ | |||
| ../src/domain/hmi_control_registry.h \ | |||
| ../src/domain/control_logic_model.h \ | |||
| ../src/domain/project_model.h \ | |||
| ../src/domain/project_storage.h \ | |||