Sfoglia il codice sorgente

feat: 增加按钮启用条件

main
suyu 3 settimane fa
parent
commit
1a59ee8271
22 ha cambiato i file con 971 aggiunte e 34 eliminazioni
  1. +3
    -0
      app/integrated_platform.pro
  2. +85
    -0
      app/src/domain/hmi_model.cpp
  3. +43
    -0
      app/src/domain/hmi_model.h
  4. +1
    -1
      app/src/domain/project_model.h
  5. +189
    -1
      app/src/infrastructure/json_project_storage.cpp
  6. +1
    -1
      app/src/infrastructure/json_project_storage.h
  7. +1
    -0
      app/src/services/hmi_editor_service.cpp
  8. +103
    -0
      app/src/services/hmi_runtime_service.cpp
  9. +13
    -1
      app/src/services/hmi_runtime_service.h
  10. +1
    -1
      app/src/services/project_service.cpp
  11. +27
    -0
      app/src/services/runtime_mode_service.cpp
  12. +124
    -0
      app/src/ui/button_extension_dialog.cpp
  13. +30
    -0
      app/src/ui/button_extension_dialog.h
  14. +36
    -0
      app/src/ui/button_extension_dialog.ui
  15. +43
    -4
      app/src/ui/hmi_editor_widget.cpp
  16. +20
    -10
      app/src/ui/main_window.ui
  17. +78
    -0
      app/src/ui/property_panel_controller.cpp
  18. +2
    -0
      app/src/ui/property_panel_controller.h
  19. +57
    -1
      app/tests/domain_tests.cpp
  20. +39
    -0
      app/tests/hmi_editor_service_tests.cpp
  21. +59
    -12
      app/tests/project_management_tests.cpp
  22. +16
    -2
      app/tests/runtime_mode_service_tests.cpp

+ 3
- 0
app/integrated_platform.pro Vedi File

@@ -21,6 +21,7 @@ SOURCES += \
src/ui/alarm_configuration_dialog.cpp \ src/ui/alarm_configuration_dialog.cpp \
src/ui/register_comment_dialog.cpp \ src/ui/register_comment_dialog.cpp \
src/ui/status_text_dialog.cpp \ src/ui/status_text_dialog.cpp \
src/ui/button_extension_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_window.cpp \ src/ui/runtime_monitor_window.cpp \
@@ -71,6 +72,7 @@ HEADERS += \
src/ui/alarm_configuration_dialog.h \ src/ui/alarm_configuration_dialog.h \
src/ui/register_comment_dialog.h \ src/ui/register_comment_dialog.h \
src/ui/status_text_dialog.h \ src/ui/status_text_dialog.h \
src/ui/button_extension_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_window.h \ src/ui/runtime_monitor_window.h \
@@ -127,6 +129,7 @@ FORMS += \
src/ui/alarm_configuration_dialog.ui \ src/ui/alarm_configuration_dialog.ui \
src/ui/register_comment_dialog.ui \ src/ui/register_comment_dialog.ui \
src/ui/status_text_dialog.ui \ src/ui/status_text_dialog.ui \
src/ui/button_extension_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_window.ui \ src/ui/runtime_monitor_window.ui \


+ 85
- 0
app/src/domain/hmi_model.cpp Vedi File

@@ -7,6 +7,7 @@
#include <cctype> #include <cctype>
#include <charconv> #include <charconv>
#include <cmath> #include <cmath>
#include <limits>


namespace { namespace {


@@ -183,6 +184,77 @@ bool validateAppearanceProperty(
return true; return true;
} }


bool validateButtonEnableCondition(
const HmiButtonEnableCondition &condition, std::string *error)
{
if (const auto *bit = std::get_if<HmiButtonBitEnableCondition>(&condition))
{
if (!bit->address.isValid() || bit->address.area() != RegisterArea::M)
{
setError(error, "按钮 M 启用条件必须使用有效的 M 区地址");
return false;
}
return true;
}

const auto *word = std::get_if<HmiButtonWordEnableCondition>(&condition);
if (word == nullptr || !registerDataTypeIsSupported(word->dataType))
{
setError(error, "按钮 D 启用条件的数据类型无效");
return false;
}
switch (word->operation)
{
case HmiButtonConditionOperator::Equal:
case HmiButtonConditionOperator::NotEqual:
case HmiButtonConditionOperator::LessThan:
case HmiButtonConditionOperator::LessThanOrEqual:
case HmiButtonConditionOperator::GreaterThan:
case HmiButtonConditionOperator::GreaterThanOrEqual:
break;
default:
setError(error, "按钮 D 启用条件的比较方式无效");
return false;
}
if (!registerDataTypeAddressIsValid(word->dataType, word->address))
{
setError(error, "按钮 D 启用条件地址超出可用范围");
return false;
}
if (!std::isfinite(word->value))
{
setError(error, "按钮 D 启用条件比较值必须是有限数值");
return false;
}
if (word->dataType == RegisterDataType::Int16)
{
if (std::trunc(word->value) != word->value
|| word->value < std::numeric_limits<std::int16_t>::min()
|| word->value > std::numeric_limits<std::int16_t>::max())
{
setError(error, "Int16 按钮启用条件比较值必须是 -32768~32767 的整数");
return false;
}
}
else if (word->dataType == RegisterDataType::Int32)
{
if (std::trunc(word->value) != word->value
|| word->value < std::numeric_limits<std::int32_t>::min()
|| word->value > std::numeric_limits<std::int32_t>::max())
{
setError(error, "Int32 按钮启用条件比较值必须是有效的 32 位整数");
return false;
}
}
else if (word->dataType == RegisterDataType::Float32
&& !std::isfinite(static_cast<float>(word->value)))
{
setError(error, "Float32 按钮启用条件比较值超出范围");
return false;
}
return true;
}

} // namespace } // namespace


bool HmiControl::validate(std::string *error) const bool HmiControl::validate(std::string *error) const
@@ -359,6 +431,19 @@ bool HmiControl::validate(std::string *error) const
setError(error, "非页面跳转控件不能包含跳转配置"); setError(error, "非页面跳转控件不能包含跳转配置");
return false; return false;
} }
if (type == HmiControlType::Button)
{
if (buttonEnableCondition.has_value()
&& !validateButtonEnableCondition(*buttonEnableCondition, error))
{
return false;
}
}
else if (buttonEnableCondition.has_value())
{
setError(error, "非按钮控件不能包含按钮启用条件");
return false;
}
return true; return true;
} }




+ 43
- 0
app/src/domain/hmi_model.h Vedi File

@@ -72,6 +72,48 @@ enum class HmiButtonOperation
MomentaryOn // 按下时写入 1,释放时写入 0 MomentaryOn // 按下时写入 1,释放时写入 0
}; };


/** @brief HMI 按钮 D 数值启用条件的比较方式 */
enum class HmiButtonConditionOperator
{
Equal,
NotEqual,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual
};

struct HmiButtonBitEnableCondition
{
RegisterAddress address{RegisterArea::M, 0};
bool expected = false;

bool operator==(const HmiButtonBitEnableCondition &other) const
{
return address == other.address && expected == other.expected;
}
};

struct HmiButtonWordEnableCondition
{
RegisterAddress address{RegisterArea::D, 0};
RegisterDataType dataType = RegisterDataType::Int16;
HmiButtonConditionOperator operation = HmiButtonConditionOperator::Equal;
double value = 0.0;

bool operator==(const HmiButtonWordEnableCondition &other) const
{
return address == other.address
&& dataType == other.dataType
&& operation == other.operation
&& value == other.value;
}
};

using HmiButtonEnableCondition = std::variant<
HmiButtonBitEnableCondition,
HmiButtonWordEnableCondition>;

struct HmiPageJumpConfig struct HmiPageJumpConfig
{ {
std::string targetPageId; std::string targetPageId;
@@ -131,6 +173,7 @@ struct HmiControl
RegisterDataType dataType = RegisterDataType::Int16; RegisterDataType dataType = RegisterDataType::Int16;
std::map<std::string, std::string> properties; std::map<std::string, std::string> properties;
HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn;
std::optional<HmiButtonEnableCondition> buttonEnableCondition;
std::optional<HmiPageJumpConfig> pageJump; std::optional<HmiPageJumpConfig> pageJump;
std::optional<HmiStatusTextConfig> statusText; std::optional<HmiStatusTextConfig> statusText;




+ 1
- 1
app/src/domain/project_model.h Vedi File

@@ -26,7 +26,7 @@ struct ProjectMetadata
// 工程显示名称 // 工程显示名称
std::string name; std::string name;
// 工程文件格式版本 // 工程文件格式版本
std::string formatVersion = "3.0";
std::string formatVersion = "4.0";
}; };


// 聚合工程中的 HMI 页面、报警、寄存器注释和控制逻辑 // 聚合工程中的 HMI 页面、报警、寄存器注释和控制逻辑


+ 189
- 1
app/src/infrastructure/json_project_storage.cpp Vedi File

@@ -18,7 +18,7 @@
namespace { namespace {


// 当前读写实现支持的工程文件格式版本 // 当前读写实现支持的工程文件格式版本
constexpr const char *kCurrentFormatVersion = "3.0";
constexpr const char *kCurrentFormatVersion = "4.0";


// 保存解析过程中遇到的第一个错误,避免后续失败覆盖根因 // 保存解析过程中遇到的第一个错误,避免后续失败覆盖根因
struct ParseState struct ParseState
@@ -426,6 +426,156 @@ bool parseHmiButtonOperation(
return true; return true;
} }


QString hmiButtonConditionOperatorName(HmiButtonConditionOperator operation)
{
switch (operation)
{
case HmiButtonConditionOperator::Equal:
return QStringLiteral("equal");
case HmiButtonConditionOperator::NotEqual:
return QStringLiteral("notEqual");
case HmiButtonConditionOperator::LessThan:
return QStringLiteral("lessThan");
case HmiButtonConditionOperator::LessThanOrEqual:
return QStringLiteral("lessThanOrEqual");
case HmiButtonConditionOperator::GreaterThan:
return QStringLiteral("greaterThan");
case HmiButtonConditionOperator::GreaterThanOrEqual:
return QStringLiteral("greaterThanOrEqual");
default:
return {};
}
}

bool parseHmiButtonConditionOperator(
const std::string &value,
HmiButtonConditionOperator *operation,
ParseState *state)
{
if (value == "equal")
{
*operation = HmiButtonConditionOperator::Equal;
}
else if (value == "notEqual")
{
*operation = HmiButtonConditionOperator::NotEqual;
}
else if (value == "lessThan")
{
*operation = HmiButtonConditionOperator::LessThan;
}
else if (value == "lessThanOrEqual")
{
*operation = HmiButtonConditionOperator::LessThanOrEqual;
}
else if (value == "greaterThan")
{
*operation = HmiButtonConditionOperator::GreaterThan;
}
else if (value == "greaterThanOrEqual")
{
*operation = HmiButtonConditionOperator::GreaterThanOrEqual;
}
else
{
return state->fail(
ProjectStorageError::InvalidField,
"不支持的按钮 D 启用条件比较方式:" + value);
}
return true;
}

QJsonObject serializeHmiButtonEnableCondition(
const HmiButtonEnableCondition &condition)
{
QJsonObject object;
if (const auto *bit = std::get_if<HmiButtonBitEnableCondition>(&condition))
{
object.insert(QStringLiteral("type"), QStringLiteral("mBit"));
object.insert(QStringLiteral("address"), serializeAddress(bit->address));
object.insert(QStringLiteral("expected"), bit->expected);
return object;
}

const auto &word = std::get<HmiButtonWordEnableCondition>(condition);
object.insert(QStringLiteral("type"), QStringLiteral("dValue"));
object.insert(QStringLiteral("address"), serializeAddress(word.address));
object.insert(
QStringLiteral("dataType"),
QString::fromLatin1(registerDataTypeName(word.dataType)));
object.insert(
QStringLiteral("operation"),
hmiButtonConditionOperatorName(word.operation));
object.insert(QStringLiteral("value"), word.value);
return object;
}

bool parseHmiButtonEnableCondition(
const QJsonObject &object,
const std::string &context,
HmiButtonEnableCondition *condition,
ParseState *state)
{
std::string type;
QJsonObject address_object;
if (!readString(object, "type", context, &type, state)
|| !readObject(object, "address", context, &address_object, state))
{
return false;
}
RegisterAddress address{RegisterArea::M, 0};
if (!parseAddress(address_object, context + ".address", &address, state))
{
return false;
}
if (type == "mBit")
{
bool expected = false;
if (!readBool(object, "expected", context, &expected, state))
{
return false;
}
*condition = HmiButtonBitEnableCondition{address, expected};
return true;
}
if (type != "dValue")
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".type 必须是 mBit 或 dValue");
}
std::string data_type_text;
std::string operation_text;
QJsonValue value_json;
if (!readString(object, "dataType", context, &data_type_text, state)
|| !readString(object, "operation", context, &operation_text, state)
|| !readValue(object, "value", context, &value_json, state))
{
return false;
}
RegisterDataType data_type = RegisterDataType::Int16;
if (!parseRegisterDataType(data_type_text, &data_type))
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".dataType 必须是 int16、int32、float32 或 float64");
}
if (!value_json.isDouble() || !std::isfinite(value_json.toDouble()))
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".value 必须是有限数值");
}
HmiButtonConditionOperator operation = HmiButtonConditionOperator::Equal;
if (!parseHmiButtonConditionOperator(operation_text, &operation, state))
{
return false;
}
*condition = HmiButtonWordEnableCondition{
address, data_type, operation, value_json.toDouble()};
return true;
}

QString alarmConditionName(AlarmCondition condition) QString alarmConditionName(AlarmCondition condition)
{ {
switch (condition) switch (condition)
@@ -810,6 +960,11 @@ QJsonObject serializeHmiControl(const HmiControl &control)
object.insert( object.insert(
QStringLiteral("buttonOperation"), QStringLiteral("buttonOperation"),
hmiButtonOperationName(control.buttonOperation)); hmiButtonOperationName(control.buttonOperation));
object.insert(
QStringLiteral("buttonEnableCondition"),
control.buttonEnableCondition.has_value()
? serializeHmiButtonEnableCondition(*control.buttonEnableCondition)
: QJsonValue(QJsonValue::Null));
} }
if (control.type == HmiControlType::PageJump) if (control.type == HmiControlType::PageJump)
{ {
@@ -889,6 +1044,7 @@ bool parseHmiControl(
if (control->type == HmiControlType::Button) if (control->type == HmiControlType::Button)
{ {
std::string operation_text; std::string operation_text;
QJsonValue enable_condition_value;
if (!readString( if (!readString(
object, object,
"buttonOperation", "buttonOperation",
@@ -900,6 +1056,38 @@ bool parseHmiControl(
{ {
return false; return false;
} }
if (!readValue(
object,
"buttonEnableCondition",
context,
&enable_condition_value,
state))
{
return false;
}
if (enable_condition_value.isNull())
{
control->buttonEnableCondition.reset();
}
else if (!enable_condition_value.isObject())
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".buttonEnableCondition 必须是对象或 null");
}
else
{
HmiButtonEnableCondition condition;
if (!parseHmiButtonEnableCondition(
enable_condition_value.toObject(),
context + ".buttonEnableCondition",
&condition,
state))
{
return false;
}
control->buttonEnableCondition = std::move(condition);
}
} }
if (control->type == HmiControlType::PageJump) if (control->type == HmiControlType::PageJump)
{ {


+ 1
- 1
app/src/infrastructure/json_project_storage.h Vedi File

@@ -2,7 +2,7 @@


#include "domain/project_storage.h" #include "domain/project_storage.h"


// 严格读写当前 3.0 JSON 工程格式
// 严格读写当前 4.0 JSON 工程格式
class JsonProjectStorage final : public ProjectStorage class JsonProjectStorage final : public ProjectStorage
{ {
public: public:


+ 1
- 0
app/src/services/hmi_editor_service.cpp Vedi File

@@ -137,6 +137,7 @@ bool HmiEditorService::controlsEqual(
&& left.dataType == right.dataType && left.dataType == right.dataType
&& left.properties == right.properties && left.properties == right.properties
&& left.buttonOperation == right.buttonOperation && left.buttonOperation == right.buttonOperation
&& left.buttonEnableCondition == right.buttonEnableCondition
&& left.statusText == right.statusText && left.statusText == right.statusText
&& page_jump_equal; && page_jump_equal;
} }


+ 103
- 0
app/src/services/hmi_runtime_service.cpp Vedi File

@@ -29,6 +29,28 @@ double numericValueAsDouble(const RegisterNumericValue &value)
value); value);
} }


bool compareButtonValue(
double actual, HmiButtonConditionOperator operation, double expected)
{
switch (operation)
{
case HmiButtonConditionOperator::Equal:
return actual == expected;
case HmiButtonConditionOperator::NotEqual:
return actual != expected;
case HmiButtonConditionOperator::LessThan:
return actual < expected;
case HmiButtonConditionOperator::LessThanOrEqual:
return actual <= expected;
case HmiButtonConditionOperator::GreaterThan:
return actual > expected;
case HmiButtonConditionOperator::GreaterThanOrEqual:
return actual >= expected;
default:
return false;
}
}

} // namespace } // namespace


HmiRuntimeService::HmiRuntimeService(RegisterRepository &repository) HmiRuntimeService::HmiRuntimeService(RegisterRepository &repository)
@@ -177,6 +199,74 @@ HmiStatusTextReadResult HmiRuntimeService::readStatusText(
true, HmiRuntimeError::None, match->text}; true, HmiRuntimeError::None, match->text};
} }


HmiButtonEnabledResult HmiRuntimeService::evaluateButtonEnabled(
const HmiControl &control) const
{
if (control.type != HmiControlType::Button)
{
return {false, false, HmiRuntimeError::UnsupportedControl};
}
if (!control.buttonEnableCondition.has_value())
{
return {true, true, HmiRuntimeError::None};
}
if (const auto *bit = std::get_if<HmiButtonBitEnableCondition>(
&*control.buttonEnableCondition))
{
if (!bit->address.isValid() || bit->address.area() != RegisterArea::M)
{
return {false, false, HmiRuntimeError::ConditionUnavailable};
}
const BitReadResult result = repository_.readBit(bit->address);
if (!result.succeeded)
{
return {false, false, HmiRuntimeError::ConditionUnavailable};
}
return {true,
result.value == bit->expected,
result.value == bit->expected
? HmiRuntimeError::None : HmiRuntimeError::ConditionNotMet};
}

const auto *word = std::get_if<HmiButtonWordEnableCondition>(
&*control.buttonEnableCondition);
if (word == nullptr
|| !registerDataTypeAddressIsValid(word->dataType, word->address)
|| !std::isfinite(word->value))
{
return {false, false, HmiRuntimeError::ConditionUnavailable};
}
switch (word->operation)
{
case HmiButtonConditionOperator::Equal:
case HmiButtonConditionOperator::NotEqual:
case HmiButtonConditionOperator::LessThan:
case HmiButtonConditionOperator::LessThanOrEqual:
case HmiButtonConditionOperator::GreaterThan:
case HmiButtonConditionOperator::GreaterThanOrEqual:
break;
default:
return {false, false, HmiRuntimeError::ConditionUnavailable};
}
const WordsReadResult words = repository_.readWords(
word->address, registerDataTypeWordCount(word->dataType));
if (!words.succeeded)
{
return {false, false, HmiRuntimeError::ConditionUnavailable};
}
const std::optional<RegisterNumericValue> value =
decodeRegisterNumericValue(word->dataType, words.values);
if (!value.has_value())
{
return {false, false, HmiRuntimeError::ConditionUnavailable};
}
const bool enabled = compareButtonValue(
numericValueAsDouble(*value), word->operation, word->value);
return {true,
enabled,
enabled ? HmiRuntimeError::None : HmiRuntimeError::ConditionNotMet};
}

HmiRuntimeWriteResult HmiRuntimeService::operateButton( HmiRuntimeWriteResult HmiRuntimeService::operateButton(
const HmiControl &control, HmiButtonEvent event) const HmiControl &control, HmiButtonEvent event)
{ {
@@ -193,6 +283,19 @@ HmiRuntimeWriteResult HmiRuntimeService::operateButton(
return writeFailure(HmiRuntimeError::InvalidBinding); return writeFailure(HmiRuntimeError::InvalidBinding);
} }


if (event == HmiButtonEvent::Pressed)
{
const HmiButtonEnabledResult enabled = evaluateButtonEnabled(control);
if (!enabled.succeeded)
{
return writeFailure(enabled.error);
}
if (!enabled.enabled)
{
return writeFailure(HmiRuntimeError::ConditionNotMet);
}
}

bool value = false; bool value = false;
switch (control.buttonOperation) switch (control.buttonOperation)
{ {


+ 13
- 1
app/src/services/hmi_runtime_service.h Vedi File

@@ -21,7 +21,9 @@ enum class HmiRuntimeError
UnsupportedControl, // 控件类型不支持寄存器读写或写入操作 UnsupportedControl, // 控件类型不支持寄存器读写或写入操作
MissingBinding, // 控件未配置寄存器绑定 MissingBinding, // 控件未配置寄存器绑定
InvalidBinding, // 控件绑定地址无效或地址区域不匹配 InvalidBinding, // 控件绑定地址无效或地址区域不匹配
RepositoryFailure // 寄存器仓库不可用或拒绝读写
RepositoryFailure, // 寄存器仓库不可用或拒绝读写
ConditionNotMet, // 按钮启用条件不满足
ConditionUnavailable // 按钮启用条件无法读取或配置无效
}; };


/** /**
@@ -46,6 +48,13 @@ struct HmiRuntimeWriteResult
HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类 HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类
}; };


struct HmiButtonEnabledResult
{
bool succeeded = false;
bool enabled = false;
HmiRuntimeError error = HmiRuntimeError::ConditionUnavailable;
};

struct HmiStatusTextReadResult struct HmiStatusTextReadResult
{ {
bool succeeded = false; bool succeeded = false;
@@ -86,6 +95,9 @@ public:
*/ */
HmiRuntimeReadResult readControl(const HmiControl &control) const; HmiRuntimeReadResult readControl(const HmiControl &control) const;
HmiStatusTextReadResult readStatusText(const HmiControl &control) const; HmiStatusTextReadResult readStatusText(const HmiControl &control) const;
/** @brief 读取并评估按钮启用条件;无条件按钮直接返回可用 */
HmiButtonEnabledResult evaluateButtonEnabled(
const HmiControl &control) const;
/** /**
* @brief 根据按钮配置处理按下或释放事件 * @brief 根据按钮配置处理按下或释放事件
* @param control 按钮控件,必须绑定有效 M 地址 * @param control 按钮控件,必须绑定有效 M 地址


+ 1
- 1
app/src/services/project_service.cpp Vedi File

@@ -192,7 +192,7 @@ Project ProjectService::makeNewProject(const std::string &name)
project.metadata.id = generateProjectId(); project.metadata.id = generateProjectId();
project.metadata.name = name; project.metadata.name = name;
// 新工程固定使用当前存储格式版本 // 新工程固定使用当前存储格式版本
project.metadata.formatVersion = "3.0";
project.metadata.formatVersion = "4.0";
return project; return project;
} }




+ 27
- 0
app/src/services/runtime_mode_service.cpp Vedi File

@@ -339,6 +339,33 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses()
} }
} }
} }
if (control.type == HmiControlType::Button
&& control.buttonEnableCondition.has_value())
{
if (const auto *bit = std::get_if<
HmiButtonBitEnableCondition>(
&*control.buttonEnableCondition))
{
addresses.push_back(bit->address);
}
else if (const auto *word = std::get_if<
HmiButtonWordEnableCondition>(
&*control.buttonEnableCondition))
{
addresses.push_back(word->address);
const int word_count = registerDataTypeWordCount(
word->dataType);
for (int offset = 1; offset < word_count; ++offset)
{
addresses.push_back(RegisterAddress{
RegisterArea::D, word->address.index() + offset});
}
if (word_count > 1)
{
multi_word_ranges.push_back({word->address, word_count});
}
}
}
} }
} }
for (const AlarmDefinition &definition : project.alarmDefinitions) for (const AlarmDefinition &definition : project.alarmDefinitions)


+ 124
- 0
app/src/ui/button_extension_dialog.cpp Vedi File

@@ -0,0 +1,124 @@
#include "button_extension_dialog.h"

#include "domain/project_limits.h"
#include "ui_button_extension_dialog.h"

#include <QComboBox>
#include <QLineEdit>
#include <QLocale>
#include <QMessageBox>

namespace {

QString fromUtf8(const std::string &value)
{
return QString::fromUtf8(value.data(), static_cast<int>(value.size()));
}

} // namespace

ButtonExtensionDialog::ButtonExtensionDialog(
const HmiControl &control, QWidget *parent)
: QDialog(parent),
ui_(std::make_unique<Ui::ButtonExtensionDialog>()),
control_(control)
{
ui_->setupUi(this);
ui_->conditionTypeComboBox->setItemData(0, 0);
ui_->conditionTypeComboBox->setItemData(1, 1);
ui_->conditionTypeComboBox->setItemData(2, 2);
ui_->dataTypeComboBox->setItemData(0, static_cast<int>(RegisterDataType::Int16));
ui_->dataTypeComboBox->setItemData(1, static_cast<int>(RegisterDataType::Int32));
ui_->dataTypeComboBox->setItemData(2, static_cast<int>(RegisterDataType::Float32));
ui_->dataTypeComboBox->setItemData(3, static_cast<int>(RegisterDataType::Float64));
ui_->operatorComboBox->setItemData(0, static_cast<int>(HmiButtonConditionOperator::Equal));
ui_->operatorComboBox->setItemData(1, static_cast<int>(HmiButtonConditionOperator::NotEqual));
ui_->operatorComboBox->setItemData(2, static_cast<int>(HmiButtonConditionOperator::LessThan));
ui_->operatorComboBox->setItemData(3, static_cast<int>(HmiButtonConditionOperator::LessThanOrEqual));
ui_->operatorComboBox->setItemData(4, static_cast<int>(HmiButtonConditionOperator::GreaterThan));
ui_->operatorComboBox->setItemData(5, static_cast<int>(HmiButtonConditionOperator::GreaterThanOrEqual));
ui_->addressSpinBox->setRange(0, RegisterAddress::kMaximumIndex);
ui_->dAddressSpinBox->setRange(0, RegisterAddress::kMaximumIndex);

if (control.buttonEnableCondition.has_value())
{
if (const auto *bit = std::get_if<HmiButtonBitEnableCondition>(
&*control.buttonEnableCondition))
{
ui_->conditionTypeComboBox->setCurrentIndex(1);
ui_->addressSpinBox->setValue(bit->address.index());
ui_->expectedComboBox->setCurrentIndex(bit->expected ? 1 : 0);
}
else if (const auto *word = std::get_if<HmiButtonWordEnableCondition>(
&*control.buttonEnableCondition))
{
ui_->conditionTypeComboBox->setCurrentIndex(2);
ui_->dAddressSpinBox->setValue(word->address.index());
ui_->dataTypeComboBox->setCurrentIndex(
ui_->dataTypeComboBox->findData(static_cast<int>(word->dataType)));
ui_->operatorComboBox->setCurrentIndex(
ui_->operatorComboBox->findData(static_cast<int>(word->operation)));
ui_->valueEdit->setText(QLocale::c().toString(word->value, 'g', 17));
}
}
connect(ui_->conditionTypeComboBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ButtonExtensionDialog::updateConditionPage);
updateConditionPage();
}

ButtonExtensionDialog::~ButtonExtensionDialog() = default;

const HmiControl &ButtonExtensionDialog::control() const
{
return control_;
}

void ButtonExtensionDialog::updateConditionPage()
{
const int type = ui_->conditionTypeComboBox->currentData().toInt();
ui_->configurationStack->setCurrentIndex(type == 2 ? 1 : 0);
ui_->bitPage->setEnabled(type == 1);
ui_->wordPage->setEnabled(type == 2);
}

void ButtonExtensionDialog::accept()
{
const int type = ui_->conditionTypeComboBox->currentData().toInt();
if (type == 0)
{
control_.buttonEnableCondition.reset();
}
else if (type == 1)
{
control_.buttonEnableCondition = HmiButtonBitEnableCondition{
RegisterAddress{RegisterArea::M, ui_->addressSpinBox->value()},
ui_->expectedComboBox->currentData().toInt() != 0};
}
else
{
bool converted = false;
const double value = QLocale::c().toDouble(
ui_->valueEdit->text().trimmed(), &converted);
if (!converted)
{
QMessageBox::warning(this, tr("按钮扩展配置"),
tr("D 条件比较值必须是有限数值"));
return;
}
control_.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, ui_->dAddressSpinBox->value()},
static_cast<RegisterDataType>(ui_->dataTypeComboBox->currentData().toInt()),
static_cast<HmiButtonConditionOperator>(
ui_->operatorComboBox->currentData().toInt()),
value};
}

std::string error;
if (!control_.validate(&error))
{
QMessageBox::warning(this, tr("按钮扩展配置"), fromUtf8(error));
return;
}
QDialog::accept();
}

+ 30
- 0
app/src/ui/button_extension_dialog.h Vedi File

@@ -0,0 +1,30 @@
#pragma once

#include "domain/hmi_model.h"

#include <QDialog>

#include <memory>

namespace Ui {
class ButtonExtensionDialog;
}

class ButtonExtensionDialog final : public QDialog
{
Q_OBJECT

public:
explicit ButtonExtensionDialog(const HmiControl &control, QWidget *parent = nullptr);
~ButtonExtensionDialog() override;

const HmiControl &control() const;

private slots:
void updateConditionPage();
void accept() override;

private:
std::unique_ptr<Ui::ButtonExtensionDialog> ui_;
HmiControl control_;
};

+ 36
- 0
app/src/ui/button_extension_dialog.ui Vedi File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ButtonExtensionDialog</class>
<widget class="QDialog" name="ButtonExtensionDialog">
<property name="windowTitle"><string>按钮扩展配置</string></property>
<property name="minimumSize"><size><width>460</width><height>260</height></size></property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="conditionForm">
<item row="0" column="0"><widget class="QLabel" name="conditionTypeLabel"><property name="text"><string>启用条件</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="conditionTypeComboBox"><item><property name="text"><string>未设置</string></property></item><item><property name="text"><string>M 位条件</string></property></item><item><property name="text"><string>D 数值条件</string></property></item></widget></item>
</layout>
</item>
<item><widget class="QStackedWidget" name="configurationStack">
<widget class="QWidget" name="bitPage"><layout class="QFormLayout" name="bitForm">
<item row="0" column="0"><widget class="QLabel" name="addressLabel"><property name="text"><string>M 地址</string></property></widget></item>
<item row="0" column="1"><widget class="QSpinBox" name="addressSpinBox"><property name="maximum"><number>4000</number></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="expectedLabel"><property name="text"><string>要求状态</string></property></widget></item>
<item row="1" column="1"><widget class="QComboBox" name="expectedComboBox"><item><property name="text"><string>OFF</string></property><property name="userData"><bool>false</bool></property></item><item><property name="text"><string>ON</string></property><property name="userData"><bool>true</bool></property></item></widget></item>
</layout></widget>
<widget class="QWidget" name="wordPage"><layout class="QFormLayout" name="wordForm">
<item row="0" column="0"><widget class="QLabel" name="dAddressLabel"><property name="text"><string>D 地址</string></property></widget></item>
<item row="0" column="1"><widget class="QSpinBox" name="dAddressSpinBox"><property name="maximum"><number>4000</number></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="dataTypeLabel"><property name="text"><string>数值类型</string></property></widget></item>
<item row="1" column="1"><widget class="QComboBox" name="dataTypeComboBox"><item><property name="text"><string>Int16</string></property></item><item><property name="text"><string>Int32</string></property></item><item><property name="text"><string>Float32</string></property></item><item><property name="text"><string>Double (Float64)</string></property></item></widget></item>
<item row="2" column="0"><widget class="QLabel" name="operatorLabel"><property name="text"><string>比较方式</string></property></widget></item>
<item row="2" column="1"><widget class="QComboBox" name="operatorComboBox"><item><property name="text"><string>等于</string></property></item><item><property name="text"><string>不等于</string></property></item><item><property name="text"><string>小于</string></property></item><item><property name="text"><string>小于等于</string></property></item><item><property name="text"><string>大于</string></property></item><item><property name="text"><string>大于等于</string></property></item></widget></item>
<item row="3" column="0"><widget class="QLabel" name="valueLabel"><property name="text"><string>比较值</string></property></widget></item>
<item row="3" column="1"><widget class="QLineEdit" name="valueEdit"><property name="placeholderText"><string>请输入数值</string></property></widget></item>
</layout></widget>
</widget></item>
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set></property></widget></item>
</layout>
</widget>
<connections><connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>ButtonExtensionDialog</receiver><slot>accept()</slot></connection><connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>ButtonExtensionDialog</receiver><slot>reject()</slot></connection></connections>
</ui>

+ 43
- 4
app/src/ui/hmi_editor_widget.cpp Vedi File

@@ -199,7 +199,7 @@ public:
case HmiControlType::Button: case HmiControlType::Button:
{ {
const bool disabled = !editing_enabled_ && runtime_active_ const bool disabled = !editing_enabled_ && runtime_active_
&& !runtime_write_enabled_;
&& (!runtime_write_enabled_ || !button_condition_enabled_);
const QColor fill = disabled const QColor fill = disabled
? QColor(QStringLiteral("#e2e6e4")) ? QColor(QStringLiteral("#e2e6e4"))
: button_pressed_ ? QColor(QStringLiteral("#a9cbb6")) : button_pressed_ ? QColor(QStringLiteral("#a9cbb6"))
@@ -454,6 +454,7 @@ public:
void setInteractionState( void setInteractionState(
bool editable, bool runtime_active, bool runtime_write_enabled) bool editable, bool runtime_active, bool runtime_write_enabled)
{ {
const bool runtime_was_active = runtime_active_;
if (control_.type == HmiControlType::AlarmList && !runtime_active) if (control_.type == HmiControlType::AlarmList && !runtime_active)
{ {
alarm_page_ = 0U; alarm_page_ = 0U;
@@ -461,6 +462,14 @@ public:
editing_enabled_ = editable; editing_enabled_ = editable;
runtime_active_ = runtime_active; runtime_active_ = runtime_active;
runtime_write_enabled_ = runtime_active && runtime_write_enabled; runtime_write_enabled_ = runtime_active && runtime_write_enabled;
if (runtime_active_ && !runtime_was_active)
{
button_condition_enabled_ = false;
}
else if (!runtime_active_)
{
button_condition_enabled_ = false;
}
setFlag(ItemIsMovable, editable); setFlag(ItemIsMovable, editable);
setFlag(ItemIsSelectable, editable); setFlag(ItemIsSelectable, editable);
if (!editable) if (!editable)
@@ -473,15 +482,20 @@ public:
|| control_.type == HmiControlType::NumericInput || control_.type == HmiControlType::NumericInput
|| control_.type == HmiControlType::PageJump || control_.type == HmiControlType::PageJump
|| control_.type == HmiControlType::AlarmList); || control_.type == HmiControlType::AlarmList);
const bool runtime_write_input_enabled = control_.type
== HmiControlType::Button
? runtime_write_enabled_ && button_condition_enabled_
: runtime_write_enabled_;
const bool runtime_input_enabled = runtime_input const bool runtime_input_enabled = runtime_input
&& (control_.type == HmiControlType::PageJump && (control_.type == HmiControlType::PageJump
|| control_.type == HmiControlType::AlarmList || control_.type == HmiControlType::AlarmList
|| runtime_write_enabled_);
|| runtime_write_input_enabled);
setAcceptedMouseButtons( setAcceptedMouseButtons(
editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton);


const bool runtime_button_enabled = runtime_active_ const bool runtime_button_enabled = runtime_active_
&& control_.type == HmiControlType::Button && runtime_write_enabled_;
&& control_.type == HmiControlType::Button && runtime_write_enabled_
&& button_condition_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_ const bool runtime_alarm_enabled = runtime_active_
@@ -497,7 +511,10 @@ public:
unsetCursor(); unsetCursor();
button_hovered_ = false; button_hovered_ = false;
page_hovered_ = false; page_hovered_ = false;
button_pressed_ = false;
if (!runtime_active_ || !runtime_write_enabled_)
{
button_pressed_ = false;
}
} }
update(); update();
} }
@@ -512,6 +529,13 @@ public:
update(); update();
} }


void setButtonConditionEnabled(bool enabled)
{
button_condition_enabled_ = enabled;
setInteractionState(
editing_enabled_, runtime_active_, runtime_write_enabled_);
}

void setStatusText(const std::string &text, bool available) void setStatusText(const std::string &text, bool available)
{ {
status_text_ = QString::fromUtf8( status_text_ = QString::fromUtf8(
@@ -606,6 +630,7 @@ protected:
return; return;
} }
if (runtime_active_ && runtime_write_enabled_ if (runtime_active_ && runtime_write_enabled_
&& button_condition_enabled_
&& control_.type == HmiControlType::Button && button_event_) && control_.type == HmiControlType::Button && button_event_)
{ {
button_pressed_ = true; button_pressed_ = true;
@@ -887,6 +912,7 @@ private:
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;
bool button_condition_enabled_ = true;
bool button_hovered_ = false; bool button_hovered_ = false;
bool button_pressed_ = false; bool button_pressed_ = false;
bool page_hovered_ = false; bool page_hovered_ = false;
@@ -1135,6 +1161,13 @@ void HmiEditorWidget::refreshRuntimeValues()
control_item->setAlarmRecords(alarm_service_.records()); control_item->setAlarmRecords(alarm_service_.records());
continue; continue;
} }
if (control->type == HmiControlType::Button)
{
const HmiButtonEnabledResult enabled =
runtime_service_.evaluateButtonEnabled(*control);
control_item->setButtonConditionEnabled(
enabled.succeeded && enabled.enabled);
}
if (control->type == HmiControlType::StatusText) if (control->type == HmiControlType::StatusText)
{ {
const HmiStatusTextReadResult value = const HmiStatusTextReadResult value =
@@ -1229,6 +1262,12 @@ void HmiEditorWidget::handleButtonEvent(
const HmiRuntimeWriteResult result = runtime_service_.operateButton(*control, event); const HmiRuntimeWriteResult result = runtime_service_.operateButton(*control, event);
if (!result.succeeded) if (!result.succeeded)
{ {
if (result.error == HmiRuntimeError::ConditionNotMet
|| result.error == HmiRuntimeError::ConditionUnavailable)
{
refreshRuntimeValues();
return;
}
emit editorError(tr("寄存器操作失败")); emit editorError(tr("寄存器操作失败"));
return; return;
} }


+ 20
- 10
app/src/ui/main_window.ui Vedi File

@@ -706,13 +706,23 @@
</widget> </widget>
</item> </item>
<item row="10" column="0"> <item row="10" column="0">
<widget class="QLabel" name="buttonEnableConditionLabel">
<property name="text"><string>启用条件</string></property>
</widget>
</item>
<item row="10" column="1">
<widget class="QPushButton" name="configureButtonExtensionButton">
<property name="text"><string>未设置</string></property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="textColorLabel"> <widget class="QLabel" name="textColorLabel">
<property name="text"> <property name="text">
<string>字体颜色</string> <string>字体颜色</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="1">
<item row="11" column="1">
<widget class="QWidget" name="textColorContainer"> <widget class="QWidget" name="textColorContainer">
<layout class="QHBoxLayout" name="textColorLayout"> <layout class="QHBoxLayout" name="textColorLayout">
<property name="leftMargin"><number>0</number></property> <property name="leftMargin"><number>0</number></property>
@@ -736,24 +746,24 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="11" column="0">
<item row="12" column="0">
<widget class="QLabel" name="fontSizeLabel"> <widget class="QLabel" name="fontSizeLabel">
<property name="text"> <property name="text">
<string>字号</string> <string>字号</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="1">
<item row="12" column="1">
<widget class="QSpinBox" name="fontSizeSpinBox"/> <widget class="QSpinBox" name="fontSizeSpinBox"/>
</item> </item>
<item row="12" column="0">
<item row="13" column="0">
<widget class="QLabel" name="fontBoldLabel"> <widget class="QLabel" name="fontBoldLabel">
<property name="text"> <property name="text">
<string>字体样式</string> <string>字体样式</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="12" column="1">
<item row="13" column="1">
<widget class="QWidget" name="fontStyleContainer"> <widget class="QWidget" name="fontStyleContainer">
<layout class="QHBoxLayout" name="fontStyleLayout"> <layout class="QHBoxLayout" name="fontStyleLayout">
<property name="leftMargin"><number>0</number></property> <property name="leftMargin"><number>0</number></property>
@@ -773,12 +783,12 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="13" column="0">
<item row="14" column="0">
<widget class="QLabel" name="dataTypeLabel"> <widget class="QLabel" name="dataTypeLabel">
<property name="text"><string>数值类型</string></property> <property name="text"><string>数值类型</string></property>
</widget> </widget>
</item> </item>
<item row="13" column="1">
<item row="14" column="1">
<widget class="QComboBox" name="dataTypeComboBox"> <widget class="QComboBox" name="dataTypeComboBox">
<item><property name="text"><string>Int16</string></property></item> <item><property name="text"><string>Int16</string></property></item>
<item><property name="text"><string>Int32</string></property></item> <item><property name="text"><string>Int32</string></property></item>
@@ -786,17 +796,17 @@
<item><property name="text"><string>Double (Float64)</string></property></item> <item><property name="text"><string>Double (Float64)</string></property></item>
</widget> </widget>
</item> </item>
<item row="14" column="0">
<item row="15" column="0">
<widget class="QLabel" name="statusTextConfigLabel"> <widget class="QLabel" name="statusTextConfigLabel">
<property name="text"><string>状态映射</string></property> <property name="text"><string>状态映射</string></property>
</widget> </widget>
</item> </item>
<item row="14" column="1">
<item row="15" column="1">
<widget class="QPushButton" name="configureStatusTextButton"> <widget class="QPushButton" name="configureStatusTextButton">
<property name="text"><string>配置</string></property> <property name="text"><string>配置</string></property>
</widget> </widget>
</item> </item>
<item row="15" column="0" colspan="2">
<item row="16" column="0" colspan="2">
<widget class="QPushButton" name="applyPropertiesButton"> <widget class="QPushButton" name="applyPropertiesButton">
<property name="text"> <property name="text">
<string>应用属性</string> <string>应用属性</string>


+ 78
- 0
app/src/ui/property_panel_controller.cpp Vedi File

@@ -6,6 +6,7 @@
#include "logic_editor_widget.h" #include "logic_editor_widget.h"
#include "logic_instruction_dialog.h" #include "logic_instruction_dialog.h"
#include "status_text_dialog.h" #include "status_text_dialog.h"
#include "button_extension_dialog.h"
#include "services/hmi_editor_service.h" #include "services/hmi_editor_service.h"
#include "services/logic_editor_service.h" #include "services/logic_editor_service.h"
#include "services/project_service.h" #include "services/project_service.h"
@@ -131,6 +132,8 @@ void PropertyPanelController::configure()
&parent_, [this] { chooseTextColor(); }); &parent_, [this] { chooseTextColor(); });
QObject::connect(ui_.configureStatusTextButton, &QPushButton::clicked, QObject::connect(ui_.configureStatusTextButton, &QPushButton::clicked,
&parent_, [this] { configureStatusText(); }); &parent_, [this] { configureStatusText(); });
QObject::connect(ui_.configureButtonExtensionButton, &QPushButton::clicked,
&parent_, [this] { configureButtonExtension(); });
ui_.targetPageLabel->setVisible(false); ui_.targetPageLabel->setVisible(false);
ui_.targetPageComboBox->setVisible(false); ui_.targetPageComboBox->setVisible(false);
showControlProperties({}); showControlProperties({});
@@ -206,6 +209,7 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
static_cast<QWidget *>(ui_.bindingIndexSpinBox), static_cast<QWidget *>(ui_.bindingIndexSpinBox),
static_cast<QWidget *>(ui_.targetPageComboBox), static_cast<QWidget *>(ui_.targetPageComboBox),
static_cast<QWidget *>(ui_.buttonOperationComboBox), static_cast<QWidget *>(ui_.buttonOperationComboBox),
static_cast<QWidget *>(ui_.configureButtonExtensionButton),
static_cast<QWidget *>(ui_.dataTypeComboBox), static_cast<QWidget *>(ui_.dataTypeComboBox),
static_cast<QWidget *>(ui_.configureStatusTextButton), static_cast<QWidget *>(ui_.configureStatusTextButton),
static_cast<QWidget *>(ui_.textColorEdit), static_cast<QWidget *>(ui_.textColorEdit),
@@ -230,6 +234,8 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.targetPageComboBox->setVisible(false); ui_.targetPageComboBox->setVisible(false);
ui_.buttonOperationLabel->setVisible(false); ui_.buttonOperationLabel->setVisible(false);
ui_.buttonOperationComboBox->setVisible(false); ui_.buttonOperationComboBox->setVisible(false);
ui_.buttonEnableConditionLabel->setVisible(false);
ui_.configureButtonExtensionButton->setVisible(false);
ui_.dataTypeLabel->setVisible(false); ui_.dataTypeLabel->setVisible(false);
ui_.dataTypeComboBox->setVisible(false); ui_.dataTypeComboBox->setVisible(false);
ui_.statusTextConfigLabel->setVisible(false); ui_.statusTextConfigLabel->setVisible(false);
@@ -276,6 +282,49 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.buttonOperationComboBox->setCurrentIndex( ui_.buttonOperationComboBox->setCurrentIndex(
ui_.buttonOperationComboBox->findData( ui_.buttonOperationComboBox->findData(
static_cast<int>(control->buttonOperation))); static_cast<int>(control->buttonOperation)));
ui_.buttonEnableConditionLabel->setVisible(is_button);
ui_.configureButtonExtensionButton->setVisible(is_button);
ui_.configureButtonExtensionButton->setEnabled(is_button);
if (is_button)
{
if (!control->buttonEnableCondition.has_value())
{
ui_.configureButtonExtensionButton->setText(QObject::tr("未设置"));
}
else if (std::holds_alternative<HmiButtonBitEnableCondition>(
*control->buttonEnableCondition))
{
const auto &condition = std::get<HmiButtonBitEnableCondition>(
*control->buttonEnableCondition);
ui_.configureButtonExtensionButton->setText(
QObject::tr("M%1 = %2")
.arg(condition.address.index())
.arg(condition.expected ? QObject::tr("ON") : QObject::tr("OFF")));
}
else
{
const auto &condition = std::get<HmiButtonWordEnableCondition>(
*control->buttonEnableCondition);
const QString operation = [&condition]
{
switch (condition.operation)
{
case HmiButtonConditionOperator::Equal: return QObject::tr("=");
case HmiButtonConditionOperator::NotEqual: return QObject::tr("!=");
case HmiButtonConditionOperator::LessThan: return QObject::tr("<");
case HmiButtonConditionOperator::LessThanOrEqual: return QObject::tr("<=");
case HmiButtonConditionOperator::GreaterThan: return QObject::tr(">");
case HmiButtonConditionOperator::GreaterThanOrEqual: return QObject::tr(">=");
default: return QObject::tr("?");
}
}();
ui_.configureButtonExtensionButton->setText(
QObject::tr("D%1 %2 %3")
.arg(condition.address.index())
.arg(operation)
.arg(QString::number(condition.value, 'g', 8)));
}
}


const bool is_numeric = control->type == HmiControlType::NumericDisplay const bool is_numeric = control->type == HmiControlType::NumericDisplay
|| control->type == HmiControlType::NumericInput; || control->type == HmiControlType::NumericInput;
@@ -700,6 +749,35 @@ void PropertyPanelController::configureStatusText()
status_reporter_(QObject::tr("状态文本配置已更新"), 3000); status_reporter_(QObject::tr("状态文本配置已更新"), 3000);
} }


void PropertyPanelController::configureButtonExtension()
{
const HmiControl *old_control = hmi_editor_service_.findControl(
current_page_id_(), selected_control_id_);
if (old_control == nullptr || old_control->type != HmiControlType::Button)
{
return;
}
ButtonExtensionDialog dialog(*old_control, &parent_);
if (dialog.exec() != QDialog::Accepted)
{
return;
}
const HmiEditorResult result = hmi_editor_service_.updateControl(
current_page_id_(), selected_control_id_, dialog.control());
if (!result.succeeded)
{
reportFailure(QObject::tr("按钮扩展配置"), result.message);
return;
}
const std::string updated_control_id = result.id;
hmi_editor_widget_->reloadPage();
selected_control_id_ = updated_control_id;
hmi_editor_widget_->selectControl(updated_control_id);
showControlProperties(updated_control_id);
refresh_project_ui_();
status_reporter_(QObject::tr("按钮扩展配置已更新"), 3000);
}

void PropertyPanelController::applySelectedLogicNodeProperties() void PropertyPanelController::applySelectedLogicNodeProperties()
{ {
const LogicNode *node = logic_editor_service_.findNode( const LogicNode *node = logic_editor_service_.findNode(


+ 2
- 0
app/src/ui/property_panel_controller.h Vedi File

@@ -91,6 +91,8 @@ private:
void chooseTextColor(); void chooseTextColor();
/** 打开状态文本专用配置并原子更新控件 */ /** 打开状态文本专用配置并原子更新控件 */
void configureStatusText(); void configureStatusText();
/** 打开按钮启用条件专用配置并原子更新控件 */
void configureButtonExtension();
/** 统一报告属性操作失败 */ /** 统一报告属性操作失败 */
void reportFailure( void reportFailure(
const QString &action, const std::string &message) const; const QString &action, const std::string &message) const;


+ 57
- 1
app/tests/domain_tests.cpp Vedi File

@@ -502,6 +502,61 @@ void testHmiAppearancePropertyBoundaries()
require(project.validate(defaultProjectLimitSettings()), "unknown HMI extension properties must remain supported"); require(project.validate(defaultProjectLimitSettings()), "unknown HMI extension properties must remain supported");
} }


void testButtonEnableConditionBoundaries()
{
Project project = makeValidProject();
HmiControl &button = project.hmiPages.front().controls.front();
button.buttonEnableCondition = HmiButtonBitEnableCondition{
RegisterAddress{RegisterArea::M, 5}, false};
require(project.validate(defaultProjectLimitSettings()),
"valid M button enable conditions must pass validation");

button.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, 20},
RegisterDataType::Float64,
HmiButtonConditionOperator::LessThan,
12.5};
require(project.validate(defaultProjectLimitSettings()),
"valid D button enable conditions must pass validation");

button.buttonEnableCondition = HmiButtonBitEnableCondition{
RegisterAddress{RegisterArea::D, 5}, true};
require(!project.validate(defaultProjectLimitSettings()),
"M button conditions must reject D addresses");

project = makeValidProject();
HmiControl &d_button = project.hmiPages.front().controls.front();
d_button.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, 20},
RegisterDataType::Int16,
HmiButtonConditionOperator::Equal,
1.5};
require(!project.validate(defaultProjectLimitSettings()),
"Int16 button conditions must reject fractional comparison values");

d_button.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, 3997},
RegisterDataType::Float64,
HmiButtonConditionOperator::Equal,
1.0};
require(!project.validate(defaultProjectLimitSettings()),
"Float64 button conditions must reject overflowing start addresses");

d_button.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, 20},
RegisterDataType::Int16,
static_cast<HmiButtonConditionOperator>(99),
1.0};
require(!project.validate(defaultProjectLimitSettings()),
"button conditions must reject unknown comparison operators");

HmiControl label = d_button;
label.type = HmiControlType::Label;
label.binding.reset();
require(!label.validate(),
"non-button controls must reject button enable conditions");
}

Project makeValidProject() Project makeValidProject()
{ {
// 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线 // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线
@@ -547,7 +602,7 @@ Project makeValidProject()
logic.rungs.push_back(rung); logic.rungs.push_back(rung);


Project project; Project project;
project.metadata = {"sample-project", "Sample project", "3.0"};
project.metadata = {"sample-project", "Sample project", "4.0"};
project.hmiPages.push_back(page); project.hmiPages.push_back(page);
project.initialHmiPageId = page.id; project.initialHmiPageId = page.id;
project.controlLogics.push_back(logic); project.controlLogics.push_back(logic);
@@ -1076,6 +1131,7 @@ int main()
testStatusTextDomainRules(); testStatusTextDomainRules();
testMultiWordHmiBoundaries(); testMultiWordHmiBoundaries();
testHmiAppearancePropertyBoundaries(); testHmiAppearancePropertyBoundaries();
testButtonEnableConditionBoundaries();
testLogicNodeConfigurationBoundaries(); testLogicNodeConfigurationBoundaries();
testEdgeAndCommentBoundaries(); testEdgeAndCommentBoundaries();
testDataInstructionBoundaries(); testDataInstructionBoundaries();


+ 39
- 0
app/tests/hmi_editor_service_tests.cpp Vedi File

@@ -235,6 +235,45 @@ void testRuntimeUsesRegisterRepository()
require(repository.readBit(*button.binding).value, require(repository.readBit(*button.binding).value,
"toggle button press must invert the current value"); "toggle button press must invert the current value");


button.buttonOperation = HmiButtonOperation::MomentaryOn;
button.buttonEnableCondition = HmiButtonBitEnableCondition{
RegisterAddress{RegisterArea::M, 13}, true};
repository.writeBit({RegisterArea::M, 13}, false);
HmiButtonEnabledResult condition = runtime_service.evaluateButtonEnabled(button);
require(condition.succeeded && !condition.enabled
&& condition.error == HmiRuntimeError::ConditionNotMet,
"an M button condition must disable the button when it is false");
require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).error
== HmiRuntimeError::ConditionNotMet,
"a false M button condition must reject the write at the service boundary");
repository.writeBit({RegisterArea::M, 13}, true);
condition = runtime_service.evaluateButtonEnabled(button);
require(condition.succeeded && condition.enabled,
"an M button condition must enable the button when it is true");
require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded
&& repository.readBit(*button.binding).value,
"a satisfied M button condition must allow the configured operation");
repository.writeBit({RegisterArea::M, 13}, false);
require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded
&& !repository.readBit(*button.binding).value,
"a momentary button release must reset safely after its condition changes");
repository.writeBit({RegisterArea::M, 12}, true);

HmiControl d_button = button;
d_button.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, 100},
RegisterDataType::Int16,
HmiButtonConditionOperator::GreaterThanOrEqual,
50.0};
repository.writeWord({RegisterArea::D, 100}, 50);
condition = runtime_service.evaluateButtonEnabled(d_button);
require(condition.succeeded && condition.enabled,
"a satisfied D button condition must enable the button");
repository.writeWord({RegisterArea::D, 100}, 49);
condition = runtime_service.evaluateButtonEnabled(d_button);
require(condition.succeeded && !condition.enabled,
"a D button condition must compare the current repository value");

HmiControl indicator; HmiControl indicator;
indicator.id = "running"; indicator.id = "running";
indicator.type = HmiControlType::Indicator; indicator.type = HmiControlType::Indicator;


+ 59
- 12
app/tests/project_management_tests.cpp Vedi File

@@ -75,7 +75,7 @@ LadderRung makeRung(
Project makeExampleProject() Project makeExampleProject()
{ {
Project project; Project project;
project.metadata = {"example-project", "Example project", "3.0"};
project.metadata = {"example-project", "Example project", "4.0"};


HmiPage page; HmiPage page;
page.id = "main-page"; page.id = "main-page";
@@ -87,6 +87,11 @@ Project makeExampleProject()
button.text = "Start"; button.text = "Start";
button.binding = RegisterAddress{RegisterArea::M, 0}; button.binding = RegisterAddress{RegisterArea::M, 0};
button.buttonOperation = HmiButtonOperation::SetOn; button.buttonOperation = HmiButtonOperation::SetOn;
button.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, 100},
RegisterDataType::Int16,
HmiButtonConditionOperator::GreaterThanOrEqual,
10.0};
page.controls.push_back(button); page.controls.push_back(button);
project.hmiPages.push_back(page); project.hmiPages.push_back(page);
project.initialHmiPageId = page.id; project.initialHmiPageId = page.id;
@@ -168,7 +173,7 @@ void testEmptyProjectRoundTrip()
ProjectService service(storage, defaultProjectLimitSettings()); ProjectService service(storage, defaultProjectLimitSettings());
require(service.createNewProject("Empty project").succeeded, require(service.createNewProject("Empty project").succeeded,
"empty project creation must succeed"); "empty project creation must succeed");
require(service.project().metadata.formatVersion == "3.0",
require(service.project().metadata.formatVersion == "4.0",
"new projects must use the strict grid format"); "new projects must use the strict grid format");


const QString path = directory.filePath("empty.json"); const QString path = directory.filePath("empty.json");
@@ -193,15 +198,15 @@ void testGridProjectRoundTrip()
"the grid project must save"); "the grid project must save");


const QByteArray json = readBytes(path); const QByteArray json = readBytes(path);
require(json.contains("\"formatVersion\": \"3.0\"")
require(json.contains("\"formatVersion\": \"4.0\"")
&& json.contains("\"cells\"") && json.contains("\"cells\"")
&& json.contains("\"verticalConnections\"") && json.contains("\"verticalConnections\"")
&& json.contains("\"kind\": \"node\"") && json.contains("\"kind\": \"node\"")
&& json.contains("\"kind\": \"wire\""), && json.contains("\"kind\": \"wire\""),
"3.0 JSON must persist cells and independent vertical edges");
"4.0 JSON must persist cells and independent vertical edges");
require(!json.contains("\"condition\"") require(!json.contains("\"condition\"")
&& !json.contains("\"children\""), && !json.contains("\"children\""),
"3.0 JSON must not serialize the removed expression tree");
"4.0 JSON must not serialize the removed expression tree");


const ProjectLoadResult loaded = storage.load(path.toStdString()); const ProjectLoadResult loaded = storage.load(path.toStdString());
require(loaded.succeeded, "the grid project must load"); require(loaded.succeeded, "the grid project must load");
@@ -278,12 +283,12 @@ void testMultiWordHmiDataTypeRoundTrip()
project.hmiPages.front().controls.push_back(double_input); project.hmiPages.front().controls.push_back(double_input);


require(storage.save(project, path.toStdString()).succeeded, require(storage.save(project, path.toStdString()).succeeded,
"Int32 and Double HMI controls must save in a 3.0 project");
"Int32 and Double HMI controls must save in a 4.0 project");
const QByteArray json = readBytes(path); const QByteArray json = readBytes(path);
require(json.contains("\"dataType\": \"int32\"") require(json.contains("\"dataType\": \"int32\"")
&& json.contains("\"dataType\": \"float64\"") && json.contains("\"dataType\": \"float64\"")
&& json.contains("\"formatVersion\": \"3.0\""),
"multi-word HMI types must use stable JSON names in 3.0");
&& json.contains("\"formatVersion\": \"4.0\""),
"multi-word HMI types must use stable JSON names in 4.0");


const ProjectLoadResult loaded = storage.load(path.toStdString()); const ProjectLoadResult loaded = storage.load(path.toStdString());
require(loaded.succeeded require(loaded.succeeded
@@ -314,6 +319,40 @@ void testMultiWordHmiDataTypeRoundTrip()
"unknown HMI data types must be rejected by strict JSON loading"); "unknown HMI data types must be rejected by strict JSON loading");
} }


void testButtonEnableConditionRoundTrip()
{
QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid");
JsonProjectStorage storage(defaultProjectLimitSettings());
const QString path = directory.filePath("button-condition.json");
Project project = makeExampleProject();
HmiControl &button = project.hmiPages.front().controls.front();
button.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, 120},
RegisterDataType::Float32,
HmiButtonConditionOperator::LessThanOrEqual,
12.5};
require(storage.save(project, path.toStdString()).succeeded,
"button enable condition project must save");
const QByteArray json = readBytes(path);
require(json.contains("\"buttonEnableCondition\"")
&& json.contains("\"type\": \"dValue\"")
&& json.contains("\"operation\": \"lessThanOrEqual\"")
&& json.contains("\"dataType\": \"float32\""),
"button enable conditions must use explicit 4.0 JSON fields");
const ProjectLoadResult loaded = storage.load(path.toStdString());
require(loaded.succeeded,
"button enable condition project must load after a JSON round trip");
const HmiControl &loaded_button = loaded.project.hmiPages.front().controls.front();
require(loaded_button.buttonEnableCondition.has_value()
&& std::get<HmiButtonWordEnableCondition>(
*loaded_button.buttonEnableCondition).address
== RegisterAddress{RegisterArea::D, 120}
&& std::get<HmiButtonWordEnableCondition>(
*loaded_button.buttonEnableCondition).value == 12.5,
"button enable condition type, address and value must survive round trip");
}

void testStatusTextRoundTrip() void testStatusTextRoundTrip()
{ {
QTemporaryDir directory; QTemporaryDir directory;
@@ -343,7 +382,7 @@ void testStatusTextRoundTrip()
project.hmiPages.front().controls.push_back(word_status); project.hmiPages.front().controls.push_back(word_status);


require(storage.save(project, path.toStdString()).succeeded, require(storage.save(project, path.toStdString()).succeeded,
"M and D status text controls must save in 3.0");
"M and D status text controls must save in 4.0");
const QByteArray json = readBytes(path); const QByteArray json = readBytes(path);
require(json.contains("\"type\": \"statusText\"") require(json.contains("\"type\": \"statusText\"")
&& json.contains("\"source\": \"m\"") && json.contains("\"source\": \"m\"")
@@ -351,7 +390,7 @@ void testStatusTextRoundTrip()
&& json.contains("\"lower\": null") && json.contains("\"lower\": null")
&& json.contains("\"upper\": null") && json.contains("\"upper\": null")
&& json.contains("\"dataType\": \"float64\""), && json.contains("\"dataType\": \"float64\""),
"3.0 JSON must store explicit status text modes and range bounds");
"4.0 JSON must store explicit status text modes and range bounds");


const ProjectLoadResult loaded = storage.load(path.toStdString()); const ProjectLoadResult loaded = storage.load(path.toStdString());
require(loaded.succeeded require(loaded.succeeded
@@ -383,6 +422,13 @@ void testStrictVersionAndRequiredFields()
== ProjectStorageError::UnsupportedVersion, == ProjectStorageError::UnsupportedVersion,
"old 2.0 files must be rejected without migration code"); "old 2.0 files must be rejected without migration code");


root.insert(QStringLiteral("formatVersion"), QStringLiteral("3.0"));
const QString old_grid_path = directory.filePath("old-grid.json");
writeBytes(old_grid_path, QJsonDocument(root).toJson());
require(storage.load(old_grid_path.toStdString()).error
== ProjectStorageError::UnsupportedVersion,
"old 3.0 files must be rejected without migration code");

root = QJsonDocument::fromJson(readBytes(fixture_path)).object(); root = QJsonDocument::fromJson(readBytes(fixture_path)).object();
QJsonArray logics = root.value(QStringLiteral("controlLogics")).toArray(); QJsonArray logics = root.value(QStringLiteral("controlLogics")).toArray();
QJsonObject logic = logics.at(0).toObject(); QJsonObject logic = logics.at(0).toObject();
@@ -393,7 +439,7 @@ void testStrictVersionAndRequiredFields()
writeBytes(missing_path, QJsonDocument(root).toJson()); writeBytes(missing_path, QJsonDocument(root).toJson());
require(storage.load(missing_path.toStdString()).error require(storage.load(missing_path.toStdString()).error
== ProjectStorageError::MissingField, == ProjectStorageError::MissingField,
"3.0 must require the vertical connection array explicitly");
"4.0 must require the vertical connection array explicitly");
} }


void testInvalidGridAndConnectionsAreRejected() void testInvalidGridAndConnectionsAreRejected()
@@ -463,7 +509,7 @@ void testInvalidGridAndConnectionsAreRejected()
writeBytes(hidden_comment_path, QJsonDocument(hidden_comment).toJson()); writeBytes(hidden_comment_path, QJsonDocument(hidden_comment).toJson());
require(storage.load(hidden_comment_path.toStdString()).error require(storage.load(hidden_comment_path.toStdString()).error
== ProjectStorageError::InvalidProject, == ProjectStorageError::InvalidProject,
"a 3.0 project must reject comments stored on connected branch rows");
"a 4.0 project must reject comments stored on connected branch rows");


QJsonObject non_adjacent = original; QJsonObject non_adjacent = original;
logic = firstLogic(&non_adjacent); logic = firstLogic(&non_adjacent);
@@ -554,6 +600,7 @@ int main()
testMOffAlarmRoundTrip(); testMOffAlarmRoundTrip();
testMultiWordHmiDataTypeRoundTrip(); testMultiWordHmiDataTypeRoundTrip();
testStatusTextRoundTrip(); testStatusTextRoundTrip();
testButtonEnableConditionRoundTrip();
testStrictVersionAndRequiredFields(); testStrictVersionAndRequiredFields();
testInvalidGridAndConnectionsAreRejected(); testInvalidGridAndConnectionsAreRejected();
testProjectServiceStateAndConfiguredLimits(); testProjectServiceStateAndConfiguredLimits();


+ 16
- 2
app/tests/runtime_mode_service_tests.cpp Vedi File

@@ -181,6 +181,17 @@ void testModeTransitions()
{std::nullopt, 0.0, "Low"}, {std::nullopt, 0.0, "Low"},
{0.0, std::nullopt, "High"}}}; {0.0, std::nullopt, "High"}}};
page.controls.push_back(double_status); page.controls.push_back(double_status);
HmiControl gated_button;
gated_button.id = "runtime-gated-button";
gated_button.type = HmiControlType::Button;
gated_button.bounds = {400, 0, 120, 40};
gated_button.binding = RegisterAddress{RegisterArea::M, 30};
gated_button.buttonEnableCondition = HmiButtonWordEnableCondition{
RegisterAddress{RegisterArea::D, 80},
RegisterDataType::Int32,
HmiButtonConditionOperator::GreaterThanOrEqual,
1.0};
page.controls.push_back(gated_button);
project.initialHmiPageId = page.id; project.initialHmiPageId = page.id;
project.hmiPages.push_back(page); project.hmiPages.push_back(page);
project.alarmDefinitions.push_back( project.alarmDefinitions.push_back(
@@ -296,6 +307,7 @@ void testModeTransitions()
{RegisterArea::M, 12}, {RegisterArea::M, 20}, {RegisterArea::M, 12}, {RegisterArea::M, 20},
{RegisterArea::M, 21}, {RegisterArea::M, 22}, {RegisterArea::M, 21}, {RegisterArea::M, 22},
{RegisterArea::M, 27}, {RegisterArea::M, 28}, {RegisterArea::M, 27}, {RegisterArea::M, 28},
{RegisterArea::M, 30},
{RegisterArea::D, 34}, {RegisterArea::D, 35}, {RegisterArea::D, 34}, {RegisterArea::D, 35},
{RegisterArea::D, 38}, {RegisterArea::D, 39}, {RegisterArea::D, 38}, {RegisterArea::D, 39},
{RegisterArea::D, 40}, {RegisterArea::D, 41}, {RegisterArea::D, 40}, {RegisterArea::D, 41},
@@ -303,12 +315,14 @@ void testModeTransitions()
{RegisterArea::D, 60}, {RegisterArea::D, 61}, {RegisterArea::D, 60}, {RegisterArea::D, 61},
{RegisterArea::D, 62}, {RegisterArea::D, 63}, {RegisterArea::D, 62}, {RegisterArea::D, 63},
{RegisterArea::D, 70}, {RegisterArea::D, 71}, {RegisterArea::D, 70}, {RegisterArea::D, 71},
{RegisterArea::D, 72}, {RegisterArea::D, 73}})
{RegisterArea::D, 72}, {RegisterArea::D, 73},
{RegisterArea::D, 80}, {RegisterArea::D, 81}})
&& gateway.pollRanges() && gateway.pollRanges()
== std::vector<RegisterWordRange>({ == std::vector<RegisterWordRange>({
{{RegisterArea::D, 50}, 2}, {{RegisterArea::D, 50}, 2},
{{RegisterArea::D, 60}, 4}, {{RegisterArea::D, 60}, 4},
{{RegisterArea::D, 70}, 4}}),
{{RegisterArea::D, 70}, 4},
{{RegisterArea::D, 80}, 2}}),
"instructions and multi-word status text must be fully polled while comments stay metadata-only"); "instructions and multi-word status text must be fully polled while comments stay metadata-only");
gateway.completeInitialRead(); gateway.completeInitialRead();
require(service.initialPlcReadCompleted(), require(service.initialPlcReadCompleted(),


Caricamento…
Annulla
Salva