Kaynağa Gözat

refactor: 统一HMI控件注册与运行能力

main
suyu 1 ay önce
ebeveyn
işleme
3b862e7c3c
20 değiştirilmiş dosya ile 401 ekleme ve 347 silme
  1. +2
    -0
      app/integrated_platform.pro
  2. +125
    -0
      app/src/domain/hmi_control_registry.cpp
  3. +68
    -0
      app/src/domain/hmi_control_registry.h
  4. +29
    -54
      app/src/domain/hmi_model.cpp
  5. +2
    -1
      app/src/domain/hmi_model.h
  6. +9
    -64
      app/src/infrastructure/json_project_storage.cpp
  7. +20
    -165
      app/src/services/hmi_editor_service.cpp
  8. +2
    -1
      app/src/services/hmi_editor_service.h
  9. +23
    -16
      app/src/services/hmi_runtime_service.cpp
  10. +14
    -46
      app/src/ui/property_panel_controller.cpp
  11. +2
    -0
      app/tests/alarm_service_tests.pro
  12. +89
    -0
      app/tests/domain_tests.cpp
  13. +2
    -0
      app/tests/domain_tests.pro
  14. +2
    -0
      app/tests/hmi_editor_service_tests.pro
  15. +2
    -0
      app/tests/logic_editor_service_tests.pro
  16. +2
    -0
      app/tests/main_window_tests.pro
  17. +2
    -0
      app/tests/offline_simulation_service_tests.pro
  18. +2
    -0
      app/tests/plc_runtime_tests.pro
  19. +2
    -0
      app/tests/project_management_tests.pro
  20. +2
    -0
      app/tests/runtime_mode_service_tests.pro

+ 2
- 0
app/integrated_platform.pro Dosyayı Görüntüle

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


+ 125
- 0
app/src/domain/hmi_control_registry.cpp Dosyayı Görüntüle

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

+ 68
- 0
app/src/domain/hmi_control_registry.h Dosyayı Görüntüle

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

+ 29
- 54
app/src/domain/hmi_model.cpp Dosyayı Görüntüle

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


+ 2
- 1
app/src/domain/hmi_model.h Dosyayı Görüntüle

@@ -43,7 +43,8 @@ enum class HmiControlType
NumericInput, // 数值输入
Label, // 标签
PageJump, // 页面跳转
AlarmList // 报警列表
AlarmList, // 报警列表
Count // 已注册控件类型数量,不作为实际控件使用
};

/**


+ 9
- 64
app/src/infrastructure/json_project_storage.cpp Dosyayı Görüntüle

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



+ 20
- 165
app/src/services/hmi_editor_service.cpp Dosyayı Görüntüle

@@ -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{};
}


+ 2
- 1
app/src/services/hmi_editor_service.h Dosyayı Görüntüle

@@ -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 待检查页面


+ 23
- 16
app/src/services/hmi_runtime_service.cpp Dosyayı Görüntüle

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


+ 14
- 46
app/src/ui/property_panel_controller.cpp Dosyayı Görüntüle

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


+ 2
- 0
app/tests/alarm_service_tests.pro Dosyayı Görüntüle

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


+ 89
- 0
app/tests/domain_tests.cpp Dosyayı Görüntüle

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


+ 2
- 0
app/tests/domain_tests.pro Dosyayı Görüntüle

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

+ 2
- 0
app/tests/hmi_editor_service_tests.pro Dosyayı Görüntüle

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


+ 2
- 0
app/tests/logic_editor_service_tests.pro Dosyayı Görüntüle

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


+ 2
- 0
app/tests/main_window_tests.pro Dosyayı Görüntüle

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


+ 2
- 0
app/tests/offline_simulation_service_tests.pro Dosyayı Görüntüle

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


+ 2
- 0
app/tests/plc_runtime_tests.pro Dosyayı Görüntüle

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


+ 2
- 0
app/tests/project_management_tests.pro Dosyayı Görüntüle

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


+ 2
- 0
app/tests/runtime_mode_service_tests.pro Dosyayı Görüntüle

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


Yükleniyor…
İptal
Kaydet