Przeglądaj źródła

feat: 支持D区Float32读写

main
suyu 3 tygodni temu
rodzic
commit
1034198aed
37 zmienionych plików z 1133 dodań i 75 usunięć
  1. +2
    -0
      app/integrated_platform.pro
  2. +13
    -0
      app/src/domain/active_register_repository.cpp
  3. +4
    -0
      app/src/domain/active_register_repository.h
  4. +14
    -0
      app/src/domain/hmi_model.cpp
  5. +2
    -0
      app/src/domain/hmi_model.h
  6. +102
    -0
      app/src/domain/project_model.cpp
  7. +52
    -9
      app/src/domain/register_monitor_model.cpp
  8. +17
    -3
      app/src/domain/register_monitor_model.h
  9. +37
    -0
      app/src/domain/register_repository.h
  10. +51
    -0
      app/src/domain/register_value_type.cpp
  11. +22
    -0
      app/src/domain/register_value_type.h
  12. +13
    -0
      app/src/domain/virtual_register_repository.cpp
  13. +4
    -0
      app/src/domain/virtual_register_repository.h
  14. +19
    -0
      app/src/infrastructure/json_project_storage.cpp
  15. +176
    -2
      app/src/infrastructure/plc_communication_service.cpp
  16. +9
    -0
      app/src/infrastructure/plc_communication_service.h
  17. +24
    -1
      app/src/infrastructure/plc_register_repository.cpp
  18. +9
    -1
      app/src/infrastructure/plc_register_repository.h
  19. +1
    -0
      app/src/services/hmi_editor_service.cpp
  20. +52
    -6
      app/src/services/hmi_runtime_service.cpp
  21. +2
    -1
      app/src/services/hmi_runtime_service.h
  22. +16
    -0
      app/src/services/plc_communication_gateway.h
  23. +140
    -11
      app/src/services/register_monitor_service.cpp
  24. +8
    -0
      app/src/services/register_monitor_service.h
  25. +40
    -1
      app/src/services/runtime_mode_service.cpp
  26. +4
    -0
      app/src/services/runtime_mode_service.h
  27. +60
    -18
      app/src/ui/free_monitor_widget.cpp
  28. +7
    -5
      app/src/ui/free_monitor_widget.ui
  29. +39
    -15
      app/src/ui/hmi_editor_widget.cpp
  30. +12
    -1
      app/src/ui/main_window.ui
  31. +17
    -0
      app/src/ui/property_panel_controller.cpp
  32. +2
    -1
      app/src/ui/runtime_panel_controller.cpp
  33. +84
    -0
      app/tests/domain_tests.cpp
  34. +13
    -0
      app/tests/hmi_editor_service_tests.cpp
  35. +2
    -0
      app/tests/pri/test_layers.pri
  36. +22
    -0
      app/tests/project_management_tests.cpp
  37. +42
    -0
      app/tests/register_monitor_service_tests.cpp

+ 2
- 0
app/integrated_platform.pro Wyświetl plik

@@ -29,6 +29,7 @@ SOURCES += \
src/ui/property_panel_controller.cpp \
src/ui/runtime_panel_controller.cpp \
src/domain/register_address.cpp \
src/domain/register_value_type.cpp \
src/domain/virtual_register_repository.cpp \
src/domain/active_register_repository.cpp \
src/domain/register_monitor_model.cpp \
@@ -75,6 +76,7 @@ HEADERS += \
src/ui/property_panel_controller.h \
src/ui/runtime_panel_controller.h \
src/domain/register_address.h \
src/domain/register_value_type.h \
src/domain/register_repository.h \
src/domain/virtual_register_repository.h \
src/domain/active_register_repository.h \


+ 13
- 0
app/src/domain/active_register_repository.cpp Wyświetl plik

@@ -32,3 +32,16 @@ RegisterWriteResult ActiveRegisterRepository::writeWord(
{
return repository_->writeWord(address, value);
}

WordPairReadResult ActiveRegisterRepository::readWordPair(
const RegisterAddress &address) const
{
return repository_->readWordPair(address);
}

RegisterWriteResult ActiveRegisterRepository::writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values)
{
return repository_->writeWordPair(address, values);
}

+ 4
- 0
app/src/domain/active_register_repository.h Wyświetl plik

@@ -15,6 +15,10 @@ public:
WordReadResult readWord(const RegisterAddress &address) const override;
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;
WordPairReadResult readWordPair(const RegisterAddress &address) const override;
RegisterWriteResult writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values) override;

private:
RegisterRepository *repository_ = nullptr;


+ 14
- 0
app/src/domain/hmi_model.cpp Wyświetl plik

@@ -171,6 +171,20 @@ bool HmiControl::validate(std::string *error) const
setError(error, "HMI 控件绑定了无效地址");
return false;
}
const bool numeric = type == HmiControlType::NumericDisplay
|| type == HmiControlType::NumericInput;
if (!numeric && dataType != RegisterDataType::Int16)
{
setError(error, "只有 HMI 数值控件可以选择 Float32");
return false;
}
if (numeric && dataType == RegisterDataType::Float32
&& binding.has_value()
&& binding->index() >= RegisterAddress::kMaximumIndex)
{
setError(error, "Float32 起始地址必须位于 D0~D3999");
return false;
}
if (type == HmiControlType::PageJump)
{
if (!pageJump.has_value())


+ 2
- 0
app/src/domain/hmi_model.h Wyświetl plik

@@ -8,6 +8,7 @@
#pragma once

#include "register_address.h"
#include "register_value_type.h"
#include "project_limits.h"

#include <map>
@@ -86,6 +87,7 @@ struct HmiControl
HmiRect bounds;
std::string text;
std::optional<RegisterAddress> binding;
RegisterDataType dataType = RegisterDataType::Int16;
std::map<std::string, std::string> properties;
HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn;
std::optional<HmiPageJumpConfig> pageJump;


+ 102
- 0
app/src/domain/project_model.cpp Wyświetl plik

@@ -4,6 +4,8 @@

#include <algorithm>
#include <cctype>
#include <variant>
#include <type_traits>

namespace {

@@ -70,6 +72,51 @@ bool containsLineBreak(const std::string &value)
|| value.find('\n') != std::string::npos;
}

struct HmiDataBinding
{
RegisterAddress address{RegisterArea::D, 0};
RegisterDataType type = RegisterDataType::Int16;
std::string controlId;
};

bool isNumericControl(const HmiControl &control)
{
return control.type == HmiControlType::NumericDisplay
|| control.type == HmiControlType::NumericInput;
}

bool rangesIntersect(const HmiDataBinding &left, const HmiDataBinding &right)
{
const int leftEnd = left.address.index()
+ registerDataTypeWordCount(left.type) - 1;
const int rightEnd = right.address.index()
+ registerDataTypeWordCount(right.type) - 1;
return left.address.area() == right.address.area()
&& left.address.index() <= rightEnd
&& right.address.index() <= leftEnd;
}

bool isFloatWriteDestination(const LogicNodeConfig &config, RegisterAddress *address)
{
if (address == nullptr)
{
return false;
}
return std::visit(
[address](const auto &value)
{
using Config = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<Config, MoveNodeConfig>
|| std::is_same_v<Config, ArithmeticNodeConfig>)
{
*address = value.destination;
return true;
}
return false;
},
config);
}

} // namespace

bool RegisterComment::validate(std::string *error) const
@@ -290,6 +337,34 @@ bool Project::validate(
}
}
}
std::vector<HmiDataBinding> hmi_bindings;
for (const HmiPage &page : hmiPages)
{
for (const HmiControl &control : page.controls)
{
if (isNumericControl(control) && control.binding.has_value()
&& control.binding->area() == RegisterArea::D)
{
hmi_bindings.push_back({*control.binding, control.dataType, control.id});
}
}
}
for (auto current = hmi_bindings.cbegin(); current != hmi_bindings.cend(); ++current)
{
for (auto other = current + 1; other != hmi_bindings.cend(); ++other)
{
const bool samePoint = current->address == other->address
&& current->type == other->type;
if (rangesIntersect(*current, *other) && !samePoint)
{
setError(
error,
"HMI 数值控件 " + current->controlId + " 与 "
+ other->controlId + " 的 D 占用范围重叠或类型冲突");
return false;
}
}
}
for (const ControlLogic &logic : controlLogics)
{
if (!logic.validate(limits, error))
@@ -307,6 +382,13 @@ bool Project::validate(
if (control.binding.has_value())
{
poll_addresses.push_back(*control.binding);
if (isNumericControl(control)
&& control.dataType == RegisterDataType::Float32
&& control.binding->area() == RegisterArea::D)
{
poll_addresses.push_back(RegisterAddress{
RegisterArea::D, control.binding->index() + 1});
}
}
}
}
@@ -330,6 +412,26 @@ bool Project::validate(
for (const LogicNode *node : nodes)
{
collectRegisterAddressesForLogicNode(node->config, &poll_addresses);
RegisterAddress destination{RegisterArea::D, 0};
if (node->configured
&& isFloatWriteDestination(node->config, &destination))
{
for (const HmiDataBinding &binding : hmi_bindings)
{
if (binding.type == RegisterDataType::Float32
&& binding.address.area() == RegisterArea::D
&& destination.index() >= binding.address.index()
&& destination.index()
< binding.address.index() + 2)
{
setError(
error,
"普通整数指令不能写入 Float32 占用的 D"
+ std::to_string(destination.index()));
return false;
}
}
}
}
}
}


+ 52
- 9
app/src/domain/register_monitor_model.cpp Wyświetl plik

@@ -4,37 +4,80 @@

const std::vector<RegisterAddress> &RegisterMonitorModel::addresses() const
{
addresses_.clear();
addresses_.reserve(points_.size());
for (const MonitorPoint &point : points_)
{
addresses_.push_back(point.address);
}
return addresses_;
}

bool RegisterMonitorModel::contains(const RegisterAddress &address) const
const std::vector<MonitorPoint> &RegisterMonitorModel::points() const
{
return std::find(addresses_.begin(), addresses_.end(), address) != addresses_.end();
return points_;
}

bool RegisterMonitorModel::add(const RegisterAddress &address)
bool RegisterMonitorModel::contains(const MonitorPoint &point) const
{
if (!address.isValid() || contains(address)
|| addresses_.size() >= kMaximumItemCount)
return std::find_if(
points_.cbegin(), points_.cend(), [&point](const MonitorPoint &candidate)
{
return candidate.address == point.address
&& candidate.dataType == point.dataType;
}) != points_.cend();
}

bool RegisterMonitorModel::add(const MonitorPoint &point)
{
const int end = point.address.index()
+ registerDataTypeWordCount(point.dataType) - 1;
if (!point.address.isValid()
|| (point.dataType == RegisterDataType::Float32
&& point.address.area() != RegisterArea::D)
|| end > RegisterAddress::kMaximumIndex
|| contains(point)
|| points_.size() >= kMaximumItemCount)
{
return false;
}
addresses_.push_back(address);
points_.push_back(point);
return true;
}

bool RegisterMonitorModel::remove(const RegisterAddress &address)
{
const auto position = std::find(addresses_.begin(), addresses_.end(), address);
if (position == addresses_.end())
const auto position = std::find_if(
points_.begin(), points_.end(), [&address](const MonitorPoint &point)
{
return point.address == address;
});
if (position == points_.end())
{
return false;
}
points_.erase(position);
return true;
}

bool RegisterMonitorModel::remove(const MonitorPoint &point)
{
const auto position = std::find_if(
points_.begin(), points_.end(), [&point](const MonitorPoint &candidate)
{
return candidate.address == point.address
&& candidate.dataType == point.dataType;
});
if (position == points_.end())
{
return false;
}
addresses_.erase(position);
points_.erase(position);
return true;
}

void RegisterMonitorModel::clear()
{
addresses_.clear();
points_.clear();
}

+ 17
- 3
app/src/domain/register_monitor_model.h Wyświetl plik

@@ -1,6 +1,7 @@
#pragma once

#include "register_address.h"
#include "register_value_type.h"

#include <cstddef>
#include <cstdint>
@@ -18,9 +19,18 @@ enum class MonitorValueState
struct MonitorValue
{
RegisterAddress address{RegisterArea::M, 0};
RegisterDataType dataType = RegisterDataType::Int16;
int endAddress = 0;
MonitorValueState state = MonitorValueState::Unavailable;
bool bitValue = false;
std::int16_t wordValue = 0;
float floatValue = 0.0f;
};

struct MonitorPoint
{
RegisterAddress address{RegisterArea::M, 0};
RegisterDataType dataType = RegisterDataType::Int16;
};

// 运行期间由用户临时维护的监控地址列表,不写入工程文件
@@ -31,15 +41,19 @@ public:

// 返回当前按添加顺序保存的地址
const std::vector<RegisterAddress> &addresses() const;
const std::vector<MonitorPoint> &points() const;
// 判断地址是否已经在监控列表中
bool contains(const RegisterAddress &address) const;
bool contains(const MonitorPoint &point) const;
// 添加一个地址;重复或超过上限时返回 false
bool add(const RegisterAddress &address);
bool add(const MonitorPoint &point);
// 删除一个地址;地址不存在时返回 false
bool remove(const RegisterAddress &address);
// 删除一个精确的监控点;地址相同但类型不同的点互不影响
bool remove(const MonitorPoint &point);
// 清空当前会话的监控列表
void clear();

private:
std::vector<RegisterAddress> addresses_;
mutable std::vector<RegisterAddress> addresses_;
std::vector<MonitorPoint> points_;
};

+ 37
- 0
app/src/domain/register_repository.h Wyświetl plik

@@ -3,6 +3,7 @@
#include "register_address.h"

#include <cstdint>
#include <array>

// 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息
enum class RegisterError
@@ -37,6 +38,13 @@ struct RegisterWriteResult
RegisterError error = RegisterError::Unavailable; // 写入失败原因
};

struct WordPairReadResult
{
bool succeeded = false;
std::array<std::int16_t, 2> values{};
RegisterError error = RegisterError::Unavailable;
};

// M/D 寄存器访问边界
// 所有运行态读写都必须经过此接口,UI 不直接接触串口或缓存实现
class RegisterRepository
@@ -53,4 +61,33 @@ public:
// 写入 D 区 16 位字寄存器
virtual RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) = 0;
virtual WordPairReadResult readWordPair(const RegisterAddress &address) const
{
if (!address.isValid() || address.area() != RegisterArea::D
|| address.index() >= RegisterAddress::kMaximumIndex)
{
return {false, {}, address.isValid() && address.area() != RegisterArea::D
? RegisterError::AreaMismatch : RegisterError::InvalidAddress};
}
const WordReadResult low = readWord(address);
const WordReadResult high = readWord(
RegisterAddress{RegisterArea::D, address.index() + 1});
if (!low.succeeded) return {false, {}, low.error};
if (!high.succeeded) return {false, {}, high.error};
return {true, {low.value, high.value}, RegisterError::None};
}
virtual RegisterWriteResult writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values)
{
if (!address.isValid() || address.area() != RegisterArea::D
|| address.index() >= RegisterAddress::kMaximumIndex)
{
return {false, address.isValid() && address.area() != RegisterArea::D
? RegisterError::AreaMismatch : RegisterError::InvalidAddress};
}
const RegisterWriteResult low = writeWord(address, values[0]);
if (!low.succeeded) return low;
return writeWord(RegisterAddress{RegisterArea::D, address.index() + 1}, values[1]);
}
};

+ 51
- 0
app/src/domain/register_value_type.cpp Wyświetl plik

@@ -0,0 +1,51 @@
#include "register_value_type.h"

#include <cmath>
#include <cstring>

const char *registerDataTypeName(RegisterDataType type)
{
return type == RegisterDataType::Float32 ? "float32" : "int16";
}

bool parseRegisterDataType(const std::string &text, RegisterDataType *type)
{
if (type == nullptr)
{
return false;
}
if (text == "int16")
{
*type = RegisterDataType::Int16;
return true;
}
if (text == "float32")
{
*type = RegisterDataType::Float32;
return true;
}
return false;
}

int registerDataTypeWordCount(RegisterDataType type)
{
return type == RegisterDataType::Float32 ? 2 : 1;
}

std::array<std::int16_t, 2> Float32Codec::encode(float value)
{
std::uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return {static_cast<std::int16_t>(bits & 0xffffU),
static_cast<std::int16_t>(bits >> 16U)};
}

std::optional<float> Float32Codec::decode(
std::int16_t low_word, std::int16_t high_word)
{
const std::uint32_t bits = static_cast<std::uint16_t>(low_word)
| (static_cast<std::uint32_t>(static_cast<std::uint16_t>(high_word)) << 16U);
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return std::isfinite(value) ? std::optional<float>(value) : std::nullopt;
}

+ 22
- 0
app/src/domain/register_value_type.h Wyświetl plik

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

#include <array>
#include <cstdint>
#include <optional>
#include <string>

enum class RegisterDataType
{
Int16,
Float32
};

const char *registerDataTypeName(RegisterDataType type);
bool parseRegisterDataType(const std::string &text, RegisterDataType *type);
int registerDataTypeWordCount(RegisterDataType type);

struct Float32Codec
{
static std::array<std::int16_t, 2> encode(float value);
static std::optional<float> decode(std::int16_t low_word, std::int16_t high_word);
};

+ 13
- 0
app/src/domain/virtual_register_repository.cpp Wyświetl plik

@@ -66,6 +66,19 @@ RegisterWriteResult VirtualRegisterRepository::writeWord(
return {true, RegisterError::None};
}

WordPairReadResult VirtualRegisterRepository::readWordPair(
const RegisterAddress &address) const
{
return RegisterRepository::readWordPair(address);
}

RegisterWriteResult VirtualRegisterRepository::writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values)
{
return RegisterRepository::writeWordPair(address, values);
}

void VirtualRegisterRepository::clear()
{
// 离线运行复位时同时清空 M 区和 D 区


+ 4
- 0
app/src/domain/virtual_register_repository.h Wyświetl plik

@@ -20,6 +20,10 @@ public:
// 更新内存中的 D 区字值
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;
WordPairReadResult readWordPair(const RegisterAddress &address) const override;
RegisterWriteResult writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values) override;

// 将所有虚拟寄存器恢复为默认值,开始新的离线会话
void clear();


+ 19
- 0
app/src/infrastructure/json_project_storage.cpp Wyświetl plik

@@ -658,6 +658,13 @@ QJsonObject serializeHmiControl(const HmiControl &control)
object.insert(QStringLiteral("binding"), QJsonValue::Null);
}
object.insert(QStringLiteral("properties"), serializeProperties(control.properties));
if (control.type == HmiControlType::NumericDisplay
|| control.type == HmiControlType::NumericInput)
{
object.insert(
QStringLiteral("dataType"),
QString::fromLatin1(registerDataTypeName(control.dataType)));
}
if (control.type == HmiControlType::Button)
{
object.insert(
@@ -703,6 +710,18 @@ bool parseHmiControl(
{
return false;
}
if (control->type == HmiControlType::NumericDisplay
|| control->type == HmiControlType::NumericInput)
{
std::string data_type_text;
if (!readString(object, "dataType", context, &data_type_text, state)
|| !parseRegisterDataType(data_type_text, &control->dataType))
{
return state->fail(
ProjectStorageError::InvalidField,
context + ".dataType 必须是 int16 或 float32");
}
}
if (control->type == HmiControlType::Button)
{
std::string operation_text;


+ 176
- 2
app/src/infrastructure/plc_communication_service.cpp Wyświetl plik

@@ -78,7 +78,25 @@ bool normalizePollAddresses(
return true;
}

std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses)
bool isFloat32Start(
const std::vector<RegisterAddress> &float32_starts,
RegisterArea area,
int index)
{
return std::binary_search(
float32_starts.cbegin(), float32_starts.cend(),
RegisterAddress{area, index},
[](const RegisterAddress &left, const RegisterAddress &right)
{
return left.area() == right.area()
? left.index() < right.index()
: left.area() == RegisterArea::M;
});
}

std::size_t pollBlockCount(
const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterAddress> &float32_starts)
{
if (addresses.empty())
{
@@ -91,6 +109,19 @@ std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses)
int count = 0;
for (const RegisterAddress &address : addresses)
{
const bool must_keep_pair = count >= ProjectLimits::kMaximumModbusReadCount
&& address.area() == current_area
&& address.index() == start_address + count
&& isFloat32Start(float32_starts, address.area(), address.index() - 1)
&& start_address + count - 1 == address.index() - 1;
if (must_keep_pair)
{
++blocks;
count = 2;
start_address = address.index() - 1;
current_area = address.area();
continue;
}
// 跨区域、出现地址间隔或达到 Modbus 单次上限时,必须开始新读块
if (count == 0
|| address.area() != current_area
@@ -128,6 +159,11 @@ PlcCommunicationService::PlcCommunicationService(
[this](const RegisterAddress &address, std::int16_t value)
{
return sendWordWrite(address, value);
},
[this](const RegisterAddress &address,
const std::array<std::int16_t, 2> &values)
{
return sendWordPairWrite(address, values);
});
// 轮询定时器每次只推动一个读块,避免一次压入大量异步请求
connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock);
@@ -260,6 +296,13 @@ void PlcCommunicationService::disconnectDevice()

PlcCommunicationResult PlcCommunicationService::setPollAddresses(
const std::vector<RegisterAddress> &addresses)
{
return setPollAddresses(addresses, {});
}

PlcCommunicationResult PlcCommunicationService::setPollAddresses(
const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterAddress> &float32_starts)
{
std::vector<RegisterAddress> normalized;
std::string error;
@@ -267,7 +310,60 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses(
{
return {false, error};
}
if (pollBlockCount(normalized) > ProjectLimits::kMaximumPollBlocks)
std::vector<RegisterAddress> normalized_float32_starts = float32_starts;
std::sort(
normalized_float32_starts.begin(), normalized_float32_starts.end(),
[](const RegisterAddress &left, const RegisterAddress &right)
{
return left.area() == right.area()
? left.index() < right.index()
: left.area() == RegisterArea::M;
});
normalized_float32_starts.erase(
std::unique(
normalized_float32_starts.begin(), normalized_float32_starts.end()),
normalized_float32_starts.end());
if (std::any_of(
normalized_float32_starts.cbegin(), normalized_float32_starts.cend(),
[](const RegisterAddress &address)
{
return !address.isValid()
|| address.area() != RegisterArea::D
|| address.index() >= RegisterAddress::kMaximumIndex;
}))
{
return {false, "Float32 轮询起始地址必须位于 D0~D3999"};
}
for (const RegisterAddress &start : normalized_float32_starts)
{
const RegisterAddress high{
RegisterArea::D, start.index() + 1};
if (std::find(normalized.cbegin(), normalized.cend(), start)
== normalized.cend())
{
normalized.push_back(start);
}
if (std::find(normalized.cbegin(), normalized.cend(), high)
== normalized.cend())
{
normalized.push_back(high);
}
}
std::sort(
normalized.begin(), normalized.end(),
[](const RegisterAddress &left, const RegisterAddress &right)
{
return left.area() == right.area()
? left.index() < right.index()
: left.area() == RegisterArea::M;
});
normalized.erase(std::unique(normalized.begin(), normalized.end()), normalized.end());
if (normalized.size() > ProjectLimits::kMaximumPollAddresses)
{
return {false, "PLC 轮询的去重 M/D 地址最多为 256 个"};
}
if (pollBlockCount(normalized, normalized_float32_starts)
> ProjectLimits::kMaximumPollBlocks)
{
return {false, "PLC 轮询地址拆分后最多允许 8 个读块"};
}
@@ -276,9 +372,11 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses(
{
// 当前回复仍在解析旧集合,等它结束后再切换到新集合
pending_poll_addresses_ = std::move(normalized);
pending_poll_float32_starts_ = std::move(normalized_float32_starts);
poll_update_pending_ = true;
return {true, {}};
}
poll_float32_starts_ = std::move(normalized_float32_starts);
applyPollAddresses(normalized);
return {true, {}};
}
@@ -344,6 +442,17 @@ void PlcCommunicationService::rebuildPollBlocks()
return left.index() < right.index();
});
addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end());
std::sort(
poll_float32_starts_.begin(), poll_float32_starts_.end(),
[](const RegisterAddress &left, const RegisterAddress &right)
{
return left.area() == right.area()
? left.index() < right.index()
: left.area() == RegisterArea::M;
});
poll_float32_starts_.erase(
std::unique(poll_float32_starts_.begin(), poll_float32_starts_.end()),
poll_float32_starts_.end());
// 将相邻地址合并为读块,同时遵守 Modbus 单次读取上限
poll_blocks_.clear();
for (const RegisterAddress &address : addresses)
@@ -352,6 +461,21 @@ void PlcCommunicationService::rebuildPollBlocks()
{
continue;
}
if (!poll_blocks_.empty()
&& poll_blocks_.back().area == address.area()
&& address.index() == poll_blocks_.back().startAddress
+ poll_blocks_.back().count
&& poll_blocks_.back().count >= ProjectLimits::kMaximumModbusReadCount
&& isFloat32Start(
poll_float32_starts_, address.area(), address.index() - 1)
&& poll_blocks_.back().startAddress
+ poll_blocks_.back().count - 1 == address.index() - 1)
{
// 把 Float32 低字从已满读块移到新块,保证高字不会单独读取
--poll_blocks_.back().count;
poll_blocks_.push_back({address.area(), address.index() - 1, 2});
continue;
}
if (poll_blocks_.empty()
|| poll_blocks_.back().area != address.area()
|| address.index() > poll_blocks_.back().startAddress
@@ -387,6 +511,7 @@ void PlcCommunicationService::applyPollAddresses(
rebuildPollBlocks();
poll_update_pending_ = false;
pending_poll_addresses_.clear();
pending_poll_float32_starts_.clear();
if (isReadingState(state_) && pending_reply_ == nullptr)
{
pollNextBlock();
@@ -459,6 +584,7 @@ void PlcCommunicationService::pollNextBlock()
if (poll_update_pending_)
{
const std::vector<RegisterAddress> addresses = pending_poll_addresses_;
poll_float32_starts_ = pending_poll_float32_starts_;
applyPollAddresses(addresses);
}
if (poll_cycle_completed)
@@ -524,6 +650,7 @@ void PlcCommunicationService::probeRecovery()
if (poll_update_pending_)
{
const std::vector<RegisterAddress> addresses = pending_poll_addresses_;
poll_float32_starts_ = pending_poll_float32_starts_;
applyPollAddresses(addresses);
}
if (state_ != PlcConnectionState::Faulted
@@ -695,6 +822,53 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite(
return {true, RegisterError::None};
}

RegisterWriteResult PlcCommunicationService::sendWordPairWrite(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values)
{
if (state_ != PlcConnectionState::Connected)
{
return {false, RegisterError::Unavailable};
}
if (pending_write_reply_ != nullptr
|| !address.isValid() || address.area() != RegisterArea::D
|| address.index() >= RegisterAddress::kMaximumIndex)
{
return {false, pending_write_reply_ != nullptr
? RegisterError::WriteRejected : RegisterError::InvalidAddress};
}
QModbusDataUnit unit(QModbusDataUnit::HoldingRegisters, address.index(), 2);
unit.setValue(0, static_cast<quint16>(values[0]));
unit.setValue(1, static_cast<quint16>(values[1]));
QModbusReply *reply = master_->sendWriteRequest(unit, configuration_.serverAddress);
if (reply == nullptr)
{
handleModbusError(master_->error());
return {false, RegisterError::WriteRejected};
}
pending_write_reply_ = reply;
connect(reply, &QModbusReply::finished,
this,
[this, reply, generation = connection_generation_]
{
if (pending_write_reply_ == reply)
{
pending_write_reply_ = nullptr;
}
if (generation != connection_generation_)
{
reply->deleteLater();
return;
}
if (reply->error() != QModbusDevice::NoError)
{
handleModbusError(reply->error());
}
reply->deleteLater();
});
return {true, RegisterError::None};
}

void PlcCommunicationService::updateInitialReadCompleted(
bool completed, bool force_notification)
{


+ 9
- 0
app/src/infrastructure/plc_communication_service.h Wyświetl plik

@@ -8,6 +8,7 @@
#include <QTimer>

#include <cstdint>
#include <array>
#include <memory>
#include <string>
#include <vector>
@@ -39,6 +40,9 @@ public:
// 设置需要周期性读取的 M/D 地址集合
PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) override;
PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterAddress> &float32_starts) override;

// 返回当前连接状态
PlcConnectionState state() const override;
@@ -97,6 +101,9 @@ private:
RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value);
RegisterWriteResult sendWordWrite(
const RegisterAddress &address, std::int16_t value);
RegisterWriteResult sendWordPairWrite(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values);
// 更新“所有轮询块均成功读取”的真机进入资格
void updateInitialReadCompleted(bool completed, bool force_notification = false);
// 处理没有主动断开时发生的串口断线
@@ -117,6 +124,8 @@ private:
PlcSerialConfiguration configuration_; // 当前串口和站号配置
std::vector<RegisterAddress> poll_addresses_; // 当前生效的轮询地址
std::vector<RegisterAddress> pending_poll_addresses_; // 请求在途时暂存的新地址
std::vector<RegisterAddress> poll_float32_starts_; // 当前轮询集合中的 Float32 起始地址
std::vector<RegisterAddress> pending_poll_float32_starts_; // 请求在途时暂存的 Float32 起始地址
std::vector<PollBlock> poll_blocks_; // 根据地址合并出的连续读块
std::size_t next_poll_block_ = 0; // 下一次要读取的读块下标
QModbusReply *pending_reply_ = nullptr; // 当前未完成的读请求或恢复探测


+ 24
- 1
app/src/infrastructure/plc_register_repository.cpp Wyświetl plik

@@ -69,12 +69,35 @@ RegisterWriteResult PlcRegisterRepository::writeWord(
: RegisterWriteResult{false, RegisterError::Unavailable};
}

WordPairReadResult PlcRegisterRepository::readWordPair(
const RegisterAddress &address) const
{
return RegisterRepository::readWordPair(address);
}

RegisterWriteResult PlcRegisterRepository::writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values)
{
if (!address.isValid() || address.area() != RegisterArea::D
|| address.index() >= RegisterAddress::kMaximumIndex)
{
return {false, address.isValid() && address.area() != RegisterArea::D
? RegisterError::AreaMismatch : RegisterError::InvalidAddress};
}
return word_pair_handler_ ? word_pair_handler_(address, values)
: RegisterWriteResult{false, RegisterError::Unavailable};
}

void PlcRegisterRepository::setWriteHandlers(
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler,
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler)
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler,
std::function<RegisterWriteResult(
const RegisterAddress &, const std::array<std::int16_t, 2> &)> word_pair_handler)
{
bit_handler_ = std::move(bit_handler);
word_handler_ = std::move(word_handler);
word_pair_handler_ = std::move(word_pair_handler);
}

void PlcRegisterRepository::updateBit(int address, bool value)


+ 9
- 1
app/src/infrastructure/plc_register_repository.h Wyświetl plik

@@ -17,11 +17,17 @@ public:
WordReadResult readWord(const RegisterAddress &address) const override;
RegisterWriteResult writeWord(
const RegisterAddress &address, std::int16_t value) override;
WordPairReadResult readWordPair(const RegisterAddress &address) const override;
RegisterWriteResult writeWordPair(
const RegisterAddress &address,
const std::array<std::int16_t, 2> &values) override;

// 注入异步写入回调;回调成功不代表缓存已经更新
void setWriteHandlers(
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler,
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler);
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler,
std::function<RegisterWriteResult(
const RegisterAddress &, const std::array<std::int16_t, 2> &)> word_pair_handler = {});
// 由通信服务在读回成功后更新缓存并标记地址有效
void updateBit(int address, bool value);
void updateWord(int address, std::int16_t value);
@@ -40,4 +46,6 @@ private:
std::array<bool, kRegisterCount> valid_words_{}; // 对应 D 地址是否读到过有效值
std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler_; // M 区异步写入回调
std::function<RegisterWriteResult(const RegisterAddress &, std::int16_t)> word_handler_; // D 区异步写入回调
std::function<RegisterWriteResult(
const RegisterAddress &, const std::array<std::int16_t, 2> &)> word_pair_handler_;
};

+ 1
- 0
app/src/services/hmi_editor_service.cpp Wyświetl plik

@@ -134,6 +134,7 @@ bool HmiEditorService::controlsEqual(
&& left.bounds.height == right.bounds.height
&& left.text == right.text
&& left.binding == right.binding
&& left.dataType == right.dataType
&& left.properties == right.properties
&& left.buttonOperation == right.buttonOperation
&& page_jump_equal;


+ 52
- 6
app/src/services/hmi_runtime_service.cpp Wyświetl plik

@@ -2,11 +2,14 @@

#include "domain/hmi_control_registry.h"

#include <cmath>
#include <limits>

namespace {

HmiRuntimeReadResult readFailure(HmiRuntimeError error)
{
return {false, error, false, 0};
return {false, error, false, 0, 0.0f};
}

HmiRuntimeWriteResult writeFailure(HmiRuntimeError error)
@@ -55,16 +58,30 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c
{
return readFailure(repositoryError(result.error));
}
return {true, HmiRuntimeError::None, result.value, 0};
return {true, HmiRuntimeError::None, result.value, 0, 0.0f};
}
case HmiRuntimeValueKind::Word:
{
if (control.dataType == RegisterDataType::Float32)
{
const WordPairReadResult result = repository_.readWordPair(*control.binding);
if (!result.succeeded)
{
return readFailure(repositoryError(result.error));
}
const std::optional<float> value = Float32Codec::decode(
result.values[0], result.values[1]);
return value.has_value()
? HmiRuntimeReadResult{
true, HmiRuntimeError::None, false, 0, *value}
: readFailure(HmiRuntimeError::RepositoryFailure);
}
const WordReadResult result = repository_.readWord(*control.binding);
if (!result.succeeded)
{
return readFailure(repositoryError(result.error));
}
return {true, HmiRuntimeError::None, false, result.value};
return {true, HmiRuntimeError::None, false, result.value, 0.0f};
}
case HmiRuntimeValueKind::None:
default:
@@ -140,7 +157,7 @@ HmiRuntimeWriteResult HmiRuntimeService::operateButton(
}

HmiRuntimeWriteResult HmiRuntimeService::writeNumericInput(
const HmiControl &control, std::int16_t value)
const HmiControl &control, double value)
{
if (control.type != HmiControlType::NumericInput)
{
@@ -150,11 +167,40 @@ HmiRuntimeWriteResult HmiRuntimeService::writeNumericInput(
{
return writeFailure(HmiRuntimeError::MissingBinding);
}
if (!control.binding->isValid())
if (!control.binding->isValid() || control.binding->area() != RegisterArea::D)
{
return writeFailure(HmiRuntimeError::InvalidBinding);
}
const RegisterWriteResult result = repository_.writeWord(*control.binding, value);
RegisterWriteResult result;
if (control.dataType == RegisterDataType::Float32)
{
if (control.binding->index() >= RegisterAddress::kMaximumIndex
|| !std::isfinite(value)
|| value > static_cast<double>(std::numeric_limits<float>::max())
|| value < -static_cast<double>(std::numeric_limits<float>::max()))
{
return writeFailure(HmiRuntimeError::InvalidBinding);
}
const float float_value = static_cast<float>(value);
if (!std::isfinite(float_value))
{
return writeFailure(HmiRuntimeError::InvalidBinding);
}
result = repository_.writeWordPair(
*control.binding, Float32Codec::encode(float_value));
}
else
{
if (!std::isfinite(value)
|| value < std::numeric_limits<std::int16_t>::min()
|| value > std::numeric_limits<std::int16_t>::max()
|| std::trunc(value) != value)
{
return writeFailure(HmiRuntimeError::InvalidBinding);
}
result = repository_.writeWord(
*control.binding, static_cast<std::int16_t>(value));
}
return result.succeeded
? HmiRuntimeWriteResult{true, HmiRuntimeError::None}
: writeFailure(repositoryError(result.error));


+ 2
- 1
app/src/services/hmi_runtime_service.h Wyświetl plik

@@ -35,6 +35,7 @@ struct HmiRuntimeReadResult
HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类
bool bit_value = false; // 位控件的读回值;字控件读取成功时无效
std::int16_t word_value = 0; // 字控件的读回值;位控件读取成功时无效
float float_value = 0.0f; // Float32 数值控件的读回值
};

/**
@@ -98,7 +99,7 @@ public:
* 该服务只负责提交写入请求,实际读回值由后续仓库轮询确认
*/
HmiRuntimeWriteResult writeNumericInput(
const HmiControl &control, std::int16_t value);
const HmiControl &control, double value);

/**
* @brief 读取指定 M 位


+ 16
- 0
app/src/services/plc_communication_gateway.h Wyświetl plik

@@ -152,6 +152,22 @@ public:
virtual PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses) = 0;

/**
* @brief 设置轮询地址并标记必须成对读取的 Float32 起始地址
* @param addresses 需要周期性读回的 M/D 地址集合
* @param float32_starts D 区 Float32 起始地址;每个地址必须和下一个 D 一起读取
* @return true 表示集合已接受
*
* 默认实现兼容只关心地址集合的测试网关和旧调用方;真实 Modbus 实现会使用成对信息避免拆分 Float32
*/
virtual PlcCommunicationResult setPollAddresses(
const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterAddress> &float32_starts)
{
(void)float32_starts;
return setPollAddresses(addresses);
}

// 返回当前连接生命周期状态;Connected 不代表首读资格已经完成
virtual PlcConnectionState state() const = 0;



+ 140
- 11
app/src/services/register_monitor_service.cpp Wyświetl plik

@@ -1,6 +1,8 @@
#include "register_monitor_service.h"

#include <algorithm>
#include <cmath>
#include <optional>
#include <utility>

namespace {
@@ -31,6 +33,12 @@ RegisterMonitorService::RegisterMonitorService(RegisterRepository &repository)

RegisterMonitorResult RegisterMonitorService::addRange(
const std::string &start_address, int count)
{
return addRange(start_address, count, RegisterDataType::Int16);
}

RegisterMonitorResult RegisterMonitorService::addRange(
const std::string &start_address, int count, RegisterDataType data_type)
{
const RegisterAddressParseResult parsed = parseRegisterAddress(start_address);
if (!parsed.succeeded)
@@ -38,8 +46,15 @@ RegisterMonitorResult RegisterMonitorService::addRange(
return {false, RegisterMonitorError::InvalidAddress,
parseErrorMessage(parsed.error), 0};
}
if (parsed.address.area() == RegisterArea::M
&& data_type == RegisterDataType::Float32)
{
return {false, RegisterMonitorError::InvalidAddress,
"Float32 只允许使用 D 区地址", 0};
}
const int step = registerDataTypeWordCount(data_type);
if (count < 1
|| parsed.address.index() > RegisterAddress::kMaximumIndex - (count - 1))
|| parsed.address.index() > RegisterAddress::kMaximumIndex - step * count + 1)
{
return {false, RegisterMonitorError::RangeOverflow,
"连续监控范围不能超过地址 4000", 0};
@@ -49,10 +64,13 @@ RegisterMonitorResult RegisterMonitorService::addRange(
pending.reserve(static_cast<std::size_t>(count));
for (int offset = 0; offset < count; ++offset)
{
RegisterAddress address{parsed.address.area(), parsed.address.index() + offset};
if (!model_.contains(address))
MonitorPoint point{
RegisterAddress{
parsed.address.area(), parsed.address.index() + offset * step},
data_type};
if (!model_.contains(point))
{
pending.push_back(address);
pending.push_back(point.address);
}
}
if (pending.empty())
@@ -66,13 +84,32 @@ RegisterMonitorResult RegisterMonitorService::addRange(
}
for (const RegisterAddress &address : pending)
{
model_.add(address);
model_.add(MonitorPoint{address, data_type});
}
if (addresses_changed_callback_)
{
addresses_changed_callback_();
}
return {true, RegisterMonitorError::None, {}, static_cast<int>(pending.size())};
bool overlaps = false;
for (const MonitorPoint &existing : model_.points())
{
const int existing_end = existing.address.index()
+ registerDataTypeWordCount(existing.dataType) - 1;
for (const RegisterAddress &address : pending)
{
const int pending_end = address.index() + step - 1;
if (existing.address.area() == address.area()
&& existing.address.index() <= pending_end
&& address.index() <= existing_end
&& !(existing.address == address && existing.dataType == data_type))
{
overlaps = true;
}
}
}
return {true, RegisterMonitorError::None,
overlaps ? "已添加,但与已有监控点的 D 地址范围重叠" : std::string{},
static_cast<int>(pending.size())};
}

RegisterMonitorResult RegisterMonitorService::remove(
@@ -97,6 +134,28 @@ RegisterMonitorResult RegisterMonitorService::remove(
return {true, RegisterMonitorError::None, {}, removed_count};
}

RegisterMonitorResult RegisterMonitorService::remove(
const std::vector<MonitorPoint> &points)
{
int removed_count = 0;
for (const MonitorPoint &point : points)
{
if (model_.remove(point))
{
++removed_count;
}
}
if (removed_count == 0)
{
return {false, RegisterMonitorError::NoChange, "请先选择要删除的监控点", 0};
}
if (addresses_changed_callback_)
{
addresses_changed_callback_();
}
return {true, RegisterMonitorError::None, {}, removed_count};
}

RegisterMonitorResult RegisterMonitorService::clear()
{
if (model_.addresses().empty())
@@ -146,6 +205,25 @@ RegisterMonitorWriteResult RegisterMonitorService::writeWord(
: "寄存器写入请求被拒绝"};
}

RegisterMonitorWriteResult RegisterMonitorService::writeFloat(
const RegisterAddress &address, float value)
{
if (!std::isfinite(value) || !address.isValid()
|| address.area() != RegisterArea::D
|| address.index() >= RegisterAddress::kMaximumIndex)
{
return {false, RegisterError::InvalidAddress, "Float32 写入值或地址无效"};
}
const RegisterWriteResult result = repository_.writeWordPair(
address, Float32Codec::encode(value));
if (result.succeeded)
{
return {true, RegisterError::None, {}};
}
return {false, result.error, result.error == RegisterError::Unavailable
? "当前寄存器源不可用" : "寄存器写入请求被拒绝"};
}

const std::vector<RegisterAddress> &RegisterMonitorService::addresses() const
{
return model_.addresses();
@@ -154,11 +232,15 @@ const std::vector<RegisterAddress> &RegisterMonitorService::addresses() const
std::vector<MonitorValue> RegisterMonitorService::values(bool communication_fault) const
{
std::vector<MonitorValue> result;
result.reserve(model_.addresses().size());
for (const RegisterAddress &address : model_.addresses())
result.reserve(model_.points().size());
for (const MonitorPoint &point : model_.points())
{
const RegisterAddress &address = point.address;
MonitorValue value;
value.address = address;
value.dataType = point.dataType;
value.endAddress = address.index()
+ registerDataTypeWordCount(point.dataType) - 1;
if (address.area() == RegisterArea::M)
{
const BitReadResult read = repository_.readBit(address);
@@ -170,18 +252,65 @@ std::vector<MonitorValue> RegisterMonitorService::values(bool communication_faul
}
else
{
const WordReadResult read = repository_.readWord(address);
value.wordValue = read.value;
value.state = read.succeeded
if (point.dataType == RegisterDataType::Float32)
{
const WordPairReadResult read = repository_.readWordPair(address);
const std::optional<float> decoded = read.succeeded
? Float32Codec::decode(read.values[0], read.values[1])
: std::nullopt;
value.floatValue = decoded.value_or(0.0f);
value.state = decoded.has_value()
? (communication_fault ? MonitorValueState::CommunicationFault
: MonitorValueState::Valid)
: MonitorValueState::Unavailable;
}
else
{
const WordReadResult read = repository_.readWord(address);
value.wordValue = read.value;
value.state = read.succeeded
? (communication_fault ? MonitorValueState::CommunicationFault
: MonitorValueState::Valid)
: MonitorValueState::Unavailable;
}
}
result.push_back(value);
}
return result;
}

const std::vector<MonitorPoint> &RegisterMonitorService::points() const
{
return model_.points();
}

std::vector<RegisterAddress> RegisterMonitorService::pollAddresses() const
{
std::vector<RegisterAddress> result;
for (const MonitorPoint &point : model_.points())
{
for (int offset = 0; offset < registerDataTypeWordCount(point.dataType); ++offset)
{
result.push_back(RegisterAddress{
point.address.area(), point.address.index() + offset});
}
}
return result;
}

std::vector<RegisterAddress> RegisterMonitorService::float32Starts() const
{
std::vector<RegisterAddress> result;
for (const MonitorPoint &point : model_.points())
{
if (point.dataType == RegisterDataType::Float32)
{
result.push_back(point.address);
}
}
return result;
}

void RegisterMonitorService::setAddressesChangedCallback(std::function<void()> callback)
{
addresses_changed_callback_ = std::move(callback);


+ 8
- 0
app/src/services/register_monitor_service.h Wyświetl plik

@@ -56,12 +56,15 @@ public:
* @return 去重后实际新增数量;格式、范围、容量或无变化时返回失败
*/
RegisterMonitorResult addRange(const std::string &start_address, int count);
RegisterMonitorResult addRange(
const std::string &start_address, int count, RegisterDataType data_type);
/**
* @brief 删除一批监控地址
* @param addresses 待删除地址;不存在的地址会被忽略
* @return 实际删除数量,并在发生变化时通知轮询集合更新
*/
RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses);
RegisterMonitorResult remove(const std::vector<MonitorPoint> &points);
/**
* @brief 清空当前会话的监控地址列表
* @return 实际清除数量;列表本来为空时返回 NoChange
@@ -82,11 +85,16 @@ public:
*/
RegisterMonitorWriteResult writeWord(
const RegisterAddress &address, std::int16_t value);
RegisterMonitorWriteResult writeFloat(
const RegisterAddress &address, float value);
/**
* @brief 返回当前会话的监控地址列表
* @return 按服务内部顺序保存的只读地址列表
*/
const std::vector<RegisterAddress> &addresses() const;
const std::vector<MonitorPoint> &points() const;
std::vector<RegisterAddress> pollAddresses() const;
std::vector<RegisterAddress> float32Starts() const;
/**
* @brief 读取监控列表的当前值并生成 UI 展示模型
* @param communication_fault 通信处于故障态时,将成功读回值标记为通信故障


+ 40
- 1
app/src/services/runtime_mode_service.cpp Wyświetl plik

@@ -253,8 +253,16 @@ PlcCommunicationResult RuntimeModeService::connectPlc(

void RuntimeModeService::setMonitorAddresses(
const std::vector<RegisterAddress> &addresses)
{
setMonitorAddresses(addresses, {});
}

void RuntimeModeService::setMonitorAddresses(
const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterAddress> &float32_starts)
{
monitor_addresses_ = addresses;
monitor_float32_starts_ = float32_starts;
refreshPlcPollAddresses();
}

@@ -266,6 +274,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses()
}
// 轮询集合来自自由监控、HMI、报警和梯形图引用的并集
std::vector<RegisterAddress> addresses = monitor_addresses_;
std::vector<RegisterAddress> float32_starts = monitor_float32_starts_;
const Project &project = project_service_.project();
for (const HmiPage &page : project.hmiPages)
{
@@ -274,6 +283,15 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses()
if (control.binding.has_value())
{
addresses.push_back(*control.binding);
if ((control.type == HmiControlType::NumericDisplay
|| control.type == HmiControlType::NumericInput)
&& control.dataType == RegisterDataType::Float32
&& control.binding->area() == RegisterArea::D)
{
addresses.push_back(RegisterAddress{
RegisterArea::D, control.binding->index() + 1});
float32_starts.push_back(*control.binding);
}
}
}
}
@@ -321,7 +339,28 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses()
{
return {false, "PLC 轮询的去重 M/D 地址最多为 256 个"};
}
return plc_gateway_->setPollAddresses(addresses);
std::sort(
float32_starts.begin(), float32_starts.end(),
[](const RegisterAddress &left, const RegisterAddress &right)
{
return left.area() == right.area()
? left.index() < right.index()
: left.area() == RegisterArea::M;
});
float32_starts.erase(
std::unique(float32_starts.begin(), float32_starts.end()),
float32_starts.end());
float32_starts.erase(
std::remove_if(
float32_starts.begin(), float32_starts.end(),
[](const RegisterAddress &address)
{
return !address.isValid()
|| address.area() != RegisterArea::D
|| address.index() >= RegisterAddress::kMaximumIndex;
}),
float32_starts.end());
return plc_gateway_->setPollAddresses(addresses, float32_starts);
}

void RuntimeModeService::disconnectPlc()


+ 4
- 0
app/src/services/runtime_mode_service.h Wyświetl plik

@@ -109,6 +109,9 @@ public:
* @param addresses 自由监控当前需要读取的 M/D 地址
*/
void setMonitorAddresses(const std::vector<RegisterAddress> &addresses);
void setMonitorAddresses(
const std::vector<RegisterAddress> &addresses,
const std::vector<RegisterAddress> &float32_starts);

/**
* @brief 汇总工程和自由监控引用并刷新 PLC 轮询地址
@@ -146,4 +149,5 @@ private:
RegisterRepository *plc_repository_ = nullptr; // 真机模式的 PLC 缓存仓库
std::function<void()> plc_status_changed_callback_; // PLC 状态变化通知
std::vector<RegisterAddress> monitor_addresses_; // 自由监控额外引用的地址
std::vector<RegisterAddress> monitor_float32_starts_; // 自由监控中的 Float32 起始地址
};

+ 60
- 18
app/src/ui/free_monitor_widget.cpp Wyświetl plik

@@ -13,6 +13,7 @@
#include <QTableWidgetItem>

#include <cstdint>
#include <cmath>
#include <set>

namespace {
@@ -45,6 +46,8 @@ FreeMonitorWidget::FreeMonitorWidget(
ui_->monitorTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
ui_->monitorTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
ui_->monitorTable->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch);
ui_->dataTypeComboBox->setItemData(0, static_cast<int>(RegisterDataType::Int16));
ui_->dataTypeComboBox->setItemData(1, static_cast<int>(RegisterDataType::Float32));
ui_->monitorTable->verticalHeader()->setVisible(false);
connect(ui_->addButton, &QPushButton::clicked, this, &FreeMonitorWidget::addAddresses);
connect(ui_->addressEdit, &QLineEdit::returnPressed,
@@ -103,10 +106,20 @@ void FreeMonitorWidget::refreshValues(
? tr("等待读取") : tr("不可用"));
continue;
}
value_item->setText(value.address.area() == RegisterArea::M
? (value.bitValue ? QStringLiteral("ON")
: QStringLiteral("OFF"))
: QString::number(value.wordValue));
if (value.address.area() == RegisterArea::M)
{
value_item->setText(value.bitValue ? QStringLiteral("ON")
: QStringLiteral("OFF"));
}
else if (value.dataType == RegisterDataType::Float32)
{
value_item->setText(QString::number(
static_cast<double>(value.floatValue), 'g', 7));
}
else
{
value_item->setText(QString::number(value.wordValue));
}
state_item->setText(value.state == MonitorValueState::CommunicationFault
? tr("通信故障,最后有效值") : tr("有效"));
}
@@ -122,9 +135,17 @@ void FreeMonitorWidget::reloadAddresses()
const RegisterAddress &address = addresses[index];
const int row = static_cast<int>(index);
ui_->monitorTable->setItem(row, 0, new QTableWidgetItem(addressText(address)));
const MonitorPoint &point = service_.points().at(index);
ui_->monitorTable->setItem(
row, 1, new QTableWidgetItem(address.area() == RegisterArea::M
? tr("位") : tr("16 位整数")));
? tr("位")
: point.dataType == RegisterDataType::Float32
? tr("Float32 %1 (%1~%2)")
.arg(addressText(address),
addressText(RegisterAddress{
RegisterArea::D,
point.address.index() + 1}))
: tr("Int16")));
ui_->monitorTable->setItem(row, 2, new QTableWidgetItem(QStringLiteral("--")));
if (address.area() == RegisterArea::M)
{
@@ -136,7 +157,14 @@ void FreeMonitorWidget::reloadAddresses()
else
{
auto *target = new QLineEdit(ui_->monitorTable);
target->setValidator(new QIntValidator(-32768, 32767, target));
if (point.dataType == RegisterDataType::Float32)
{
target->setPlaceholderText(tr("小数或科学计数法"));
}
else
{
target->setValidator(new QIntValidator(-32768, 32767, target));
}
target->setText(QStringLiteral("0"));
target->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
ui_->monitorTable->setCellWidget(row, 3, target);
@@ -162,7 +190,8 @@ void FreeMonitorWidget::writeRow(int row)
return;
}

const RegisterAddress address = service_.addresses().at(static_cast<std::size_t>(row));
const MonitorPoint point = service_.points().at(static_cast<std::size_t>(row));
const RegisterAddress address = point.address;
RegisterMonitorWriteResult result;
if (address.area() == RegisterArea::M)
{
@@ -181,14 +210,20 @@ void FreeMonitorWidget::writeRow(int row)
return;
}
bool converted = false;
const int value = target->text().toInt(&converted);
if (!converted || value < -32768 || value > 32767)
const double value = target->text().toDouble(&converted);
if (!converted || !std::isfinite(value)
|| (point.dataType == RegisterDataType::Int16
&& (value < -32768 || value > 32767 || std::trunc(value) != value)))
{
ui_->monitorTable->item(row, 5)->setText(tr("目标值无效"));
emit operationMessage(tr("写入 D 区地址失败:请输入 -32768~32767 的整数"));
emit operationMessage(point.dataType == RegisterDataType::Float32
? tr("写入 D 区地址失败:请输入有限 Float32 数值")
: tr("写入 D 区地址失败:请输入 -32768~32767 的整数"));
return;
}
result = service_.writeWord(address, static_cast<std::int16_t>(value));
result = point.dataType == RegisterDataType::Float32
? service_.writeFloat(address, static_cast<float>(value))
: service_.writeWord(address, static_cast<std::int16_t>(value));
}

QTableWidgetItem *state_item = ui_->monitorTable->item(row, 5);
@@ -225,13 +260,20 @@ void FreeMonitorWidget::addAddresses()
const QByteArray address = ui_->addressEdit->text().toUtf8();
const RegisterMonitorResult result = service_.addRange(
std::string(address.constData(), static_cast<std::size_t>(address.size())),
ui_->countSpinBox->value());
ui_->countSpinBox->value(),
static_cast<RegisterDataType>(ui_->dataTypeComboBox->currentData().toInt()));
if (result.succeeded)
{
reloadAddresses();
ui_->addressEdit->selectAll();
emit monitorAddressesChanged();
emit operationMessage(tr("已添加 %1 个监控地址").arg(result.affectedCount));
const QString message = fromUtf8(result.message);
emit operationMessage(
message.isEmpty()
? tr("已添加 %1 个监控点").arg(result.affectedCount)
: tr("已添加 %1 个监控点;%2")
.arg(result.affectedCount)
.arg(message));
return;
}
handleResult(tr("添加监控地址"), false, result.message);
@@ -244,16 +286,16 @@ void FreeMonitorWidget::removeSelectedAddresses()
{
rows.insert(index.row());
}
std::vector<RegisterAddress> addresses;
const std::vector<RegisterAddress> &all_addresses = service_.addresses();
std::vector<MonitorPoint> points;
const std::vector<MonitorPoint> &all_points = service_.points();
for (int row : rows)
{
if (row >= 0 && static_cast<std::size_t>(row) < all_addresses.size())
if (row >= 0 && static_cast<std::size_t>(row) < all_points.size())
{
addresses.push_back(all_addresses.at(static_cast<std::size_t>(row)));
points.push_back(all_points.at(static_cast<std::size_t>(row)));
}
}
const RegisterMonitorResult result = service_.remove(addresses);
const RegisterMonitorResult result = service_.remove(points);
if (result.succeeded)
{
reloadAddresses();


+ 7
- 5
app/src/ui/free_monitor_widget.ui Wyświetl plik

@@ -15,13 +15,15 @@
<layout class="QGridLayout" name="commandLayout">
<item row="0" column="0"><widget class="QLabel" name="addressLabel"><property name="text"><string>起始地址</string></property></widget></item>
<item row="0" column="1"><widget class="QLineEdit" name="addressEdit"><property name="minimumSize"><size><width>90</width><height>0</height></size></property><property name="maximumSize"><size><width>160</width><height>16777215</height></size></property><property name="placeholderText"><string>M0 或 D0</string></property><property name="clearButtonEnabled"><bool>true</bool></property></widget></item>
<item row="0" column="2"><widget class="QLabel" name="countLabel"><property name="text"><string>连续个数</string></property></widget></item>
<item row="0" column="3"><widget class="QSpinBox" name="countSpinBox"><property name="minimum"><number>1</number></property><property name="maximum"><number>64</number></property><property name="value"><number>1</number></property></widget></item>
<item row="0" column="4"><widget class="QPushButton" name="addButton"><property name="text"><string>添加</string></property></widget></item>
<item row="0" column="5"><spacer name="firstRowSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
<item row="0" column="2"><widget class="QLabel" name="dataTypeLabel"><property name="text"><string>类型</string></property></widget></item>
<item row="0" column="3"><widget class="QComboBox" name="dataTypeComboBox"><item><property name="text"><string>Int16</string></property></item><item><property name="text"><string>Float32</string></property></item></widget></item>
<item row="0" column="4"><widget class="QLabel" name="countLabel"><property name="text"><string>连续个数</string></property></widget></item>
<item row="0" column="5"><widget class="QSpinBox" name="countSpinBox"><property name="minimum"><number>1</number></property><property name="maximum"><number>64</number></property><property name="value"><number>1</number></property></widget></item>
<item row="0" column="6"><widget class="QPushButton" name="addButton"><property name="text"><string>添加</string></property></widget></item>
<item row="0" column="7"><spacer name="firstRowSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
<item row="1" column="0"><widget class="QToolButton" name="removeButton"><property name="toolTip"><string>删除选中的监控地址</string></property><property name="text"><string>删除</string></property></widget></item>
<item row="1" column="1"><widget class="QToolButton" name="clearButton"><property name="toolTip"><string>清空监控表</string></property><property name="text"><string>清空</string></property></widget></item>
<item row="1" column="2" colspan="4"><widget class="QLabel" name="sourceLabel"><property name="text"><string>数据源:未启用</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item>
<item row="1" column="2" colspan="6"><widget class="QLabel" name="sourceLabel"><property name="text"><string>数据源:未启用</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item>
</layout>
</item>
<item>


+ 39
- 15
app/src/ui/hmi_editor_widget.cpp Wyświetl plik

@@ -417,10 +417,12 @@ public:
}

// 缓存运行服务读出的值并触发 Qt 重绘
void setRuntimeValue(bool bit_value, std::int16_t word_value, bool available)
void setRuntimeValue(
bool bit_value, std::int16_t word_value, float float_value, bool available)
{
bit_value_ = bit_value;
word_value_ = word_value;
float_value_ = float_value;
has_runtime_value_ = available;
update();
}
@@ -795,7 +797,10 @@ private:
if (control_.type == HmiControlType::NumericDisplay
|| control_.type == HmiControlType::NumericInput)
{
return text + QStringLiteral(": ") + QString::number(word_value_);
const QString value = control_.dataType == RegisterDataType::Float32
? QString::number(float_value_, 'g', 7)
: QString::number(word_value_);
return text + QStringLiteral(": ") + value;
}
return text;
}
@@ -825,6 +830,7 @@ private:
bool bit_value_ = false;
// 数值控件读取到的 D 字值
std::int16_t word_value_ = 0;
float float_value_ = 0.0f;
// 标记当前缓存值是否来自一次成功的运行时读取
bool has_runtime_value_ = false;
};
@@ -887,7 +893,7 @@ void HmiEditorWidget::setRuntimeActive(bool active)
HmiGraphicsItem *control_item = asHmiItem(item);
if (control_item != nullptr)
{
control_item->setRuntimeValue(false, 0, false);
control_item->setRuntimeValue(false, 0, 0.0f, false);
control_item->setAlarmRecords({});
}
}
@@ -1037,7 +1043,8 @@ void HmiEditorWidget::refreshRuntimeValues()
}
// 运行值通过服务读取,图元不直接接触寄存器仓库
const HmiRuntimeReadResult value = runtime_service_.readControl(*control);
control_item->setRuntimeValue(value.bit_value, value.word_value, value.succeeded);
control_item->setRuntimeValue(
value.bit_value, value.word_value, value.float_value, value.succeeded);
}
}

@@ -1140,22 +1147,39 @@ void HmiEditorWidget::handleNumericInputActivated(const std::string &control_id)
}
bool accepted = false;
const HmiRuntimeReadResult current = runtime_service_.readControl(*control);
const int initial = current.succeeded ? current.word_value : 0;
const int value = QInputDialog::getInt(
this,
tr("输入数值"),
QString::fromUtf8(control->text.data(), static_cast<int>(control->text.size())),
initial,
std::numeric_limits<std::int16_t>::min(),
std::numeric_limits<std::int16_t>::max(),
1,
&accepted);
double value = 0.0;
if (control->dataType == RegisterDataType::Float32)
{
value = QInputDialog::getDouble(
this,
tr("输入 Float32"),
QString::fromUtf8(
control->text.data(), static_cast<int>(control->text.size())),
current.succeeded ? static_cast<double>(current.float_value) : 0.0,
-static_cast<double>(std::numeric_limits<float>::max()),
static_cast<double>(std::numeric_limits<float>::max()),
7,
&accepted);
}
else
{
value = QInputDialog::getInt(
this,
tr("输入 Int16"),
QString::fromUtf8(
control->text.data(), static_cast<int>(control->text.size())),
current.succeeded ? current.word_value : 0,
std::numeric_limits<std::int16_t>::min(),
std::numeric_limits<std::int16_t>::max(),
1,
&accepted);
}
if (!accepted)
{
return;
}
const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput(
*control, static_cast<std::int16_t>(value));
*control, value);
if (!result.succeeded)
{
emit editorError(tr("寄存器操作失败"));


+ 12
- 1
app/src/ui/main_window.ui Wyświetl plik

@@ -738,7 +738,18 @@
</layout>
</widget>
</item>
<item row="13" column="0" colspan="2">
<item row="13" column="0">
<widget class="QLabel" name="dataTypeLabel">
<property name="text"><string>数值类型</string></property>
</widget>
</item>
<item row="13" column="1">
<widget class="QComboBox" name="dataTypeComboBox">
<item><property name="text"><string>Int16</string></property></item>
<item><property name="text"><string>Float32</string></property></item>
</widget>
</item>
<item row="14" column="0" colspan="2">
<widget class="QPushButton" name="applyPropertiesButton">
<property name="text">
<string>应用属性</string>


+ 17
- 0
app/src/ui/property_panel_controller.cpp Wyświetl plik

@@ -108,6 +108,10 @@ void PropertyPanelController::configure()
2, static_cast<int>(HmiButtonOperation::Toggle));
ui_.buttonOperationComboBox->setItemData(
3, static_cast<int>(HmiButtonOperation::MomentaryOn));
ui_.dataTypeComboBox->setItemData(
0, static_cast<int>(RegisterDataType::Int16));
ui_.dataTypeComboBox->setItemData(
1, static_cast<int>(RegisterDataType::Float32));
for (int index = 0; index < ui_.logicComparisonComboBox->count(); ++index)
{
ui_.logicComparisonComboBox->setItemData(index, index);
@@ -195,6 +199,7 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
static_cast<QWidget *>(ui_.bindingIndexSpinBox),
static_cast<QWidget *>(ui_.targetPageComboBox),
static_cast<QWidget *>(ui_.buttonOperationComboBox),
static_cast<QWidget *>(ui_.dataTypeComboBox),
static_cast<QWidget *>(ui_.textColorEdit),
static_cast<QWidget *>(ui_.textColorButton),
static_cast<QWidget *>(ui_.fontSizeSpinBox),
@@ -217,6 +222,8 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.targetPageComboBox->setVisible(false);
ui_.buttonOperationLabel->setVisible(false);
ui_.buttonOperationComboBox->setVisible(false);
ui_.dataTypeLabel->setVisible(false);
ui_.dataTypeComboBox->setVisible(false);
ui_.textColorEdit->clear();
ui_.textColorButton->setStyleSheet(QString{});
ui_.fontSizeSpinBox->setValue(
@@ -256,6 +263,14 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.buttonOperationComboBox->findData(
static_cast<int>(control->buttonOperation)));

const bool is_numeric = control->type == HmiControlType::NumericDisplay
|| control->type == HmiControlType::NumericInput;
ui_.dataTypeLabel->setVisible(is_numeric);
ui_.dataTypeComboBox->setVisible(is_numeric);
ui_.dataTypeComboBox->setEnabled(is_numeric);
ui_.dataTypeComboBox->setCurrentIndex(
ui_.dataTypeComboBox->findData(static_cast<int>(control->dataType)));

const bool is_page_jump = control->type == HmiControlType::PageJump;
ui_.targetPageLabel->setVisible(is_page_jump);
ui_.targetPageComboBox->setVisible(is_page_jump);
@@ -517,6 +532,8 @@ void PropertyPanelController::applySelectedControlProperties()
control.bounds.y = ui_.controlYSpinBox->value();
control.bounds.width = ui_.controlWidthSpinBox->value();
control.bounds.height = ui_.controlHeightSpinBox->value();
control.dataType = static_cast<RegisterDataType>(
ui_.dataTypeComboBox->currentData().toInt());
const HmiControlDescriptor *descriptor =
findHmiControlDescriptor(control.type);
if (descriptor == nullptr


+ 2
- 1
app/src/ui/runtime_panel_controller.cpp Wyświetl plik

@@ -103,7 +103,8 @@ void RuntimePanelController::configure()
&parent_, [this]
{
runtime_mode_service_.setMonitorAddresses(
register_monitor_service_.addresses());
register_monitor_service_.pollAddresses(),
register_monitor_service_.float32Starts());
runtime_monitor_widget_->refreshValues(
runtime_mode_service_.mode(),
runtime_mode_service_.plcConnectionState());


+ 84
- 0
app/tests/domain_tests.cpp Wyświetl plik

@@ -10,10 +10,12 @@
#include "support/test_support.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <exception>
#include <functional>
#include <iostream>
#include <limits>
#include <set>
#include <stdexcept>
#include <string>
@@ -101,6 +103,37 @@ void testRegisterRepositorySeparatesAreas()
"M address must not be read as a word");
}

void testFloat32CodecAndPairAccess()
{
const std::array<float, 4> values = {1.0f, -2.5f, 0.1f, 0.0f};
for (const float value : values)
{
const std::array<std::int16_t, 2> words = Float32Codec::encode(value);
const std::optional<float> decoded = Float32Codec::decode(words[0], words[1]);
require(decoded.has_value() && *decoded == value,
"Float32 codec must preserve representative finite values");
}
require(!Float32Codec::decode(
static_cast<std::int16_t>(0),
static_cast<std::int16_t>(0x7f80)).has_value(),
"Float32 codec must reject positive infinity");
require(!Float32Codec::decode(
static_cast<std::int16_t>(0),
static_cast<std::int16_t>(0x7fc0)).has_value(),
"Float32 codec must reject NaN");

VirtualRegisterRepository repository;
const RegisterAddress d10{RegisterArea::D, 10};
require(repository.writeWordPair(d10, Float32Codec::encode(-2.5f)).succeeded,
"virtual repository must write Float32 low and high words");
const WordPairReadResult pair = repository.readWordPair(d10);
require(pair.succeeded
&& Float32Codec::decode(pair.values[0], pair.values[1]).value() == -2.5f,
"virtual repository must read Float32 from two consecutive words");
require(!repository.writeWordPair({RegisterArea::D, 4000}, {}).succeeded,
"Float32 write at D4000 must be rejected");
}

void testHmiControlRegistryCompleteness()
{
std::set<const HmiControlDescriptor *> descriptors;
@@ -189,6 +222,55 @@ void testHmiControlRegistryCompleteness()

Project makeValidProject();

void testFloat32HmiBoundaries()
{
Project project = makeValidProject();
HmiControl display;
display.id = "float-display";
display.type = HmiControlType::NumericDisplay;
display.text = "Value";
display.binding = RegisterAddress{RegisterArea::D, 3999};
display.dataType = RegisterDataType::Float32;
project.hmiPages.front().controls.push_back(display);
require(project.validate(),
"Float32 D3999 must be valid and occupy D3999~D4000");

project.hmiPages.front().controls.back().binding =
RegisterAddress{RegisterArea::D, 4000};
require(!project.validate(), "Float32 D4000 must be rejected");

project = makeValidProject();
HmiControl first = display;
first.binding = RegisterAddress{RegisterArea::D, 10};
HmiControl second = first;
second.id = "float-display-duplicate";
project.hmiPages.front().controls.push_back(first);
project.hmiPages.front().controls.push_back(second);
require(project.validate(),
"same Float32 start address and type may be bound more than once");

second.dataType = RegisterDataType::Int16;
project.hmiPages.front().controls.back() = second;
require(!project.validate(),
"different HMI data types may not partially overlap");

project = makeValidProject();
HmiControl configured_float = display;
configured_float.binding = RegisterAddress{RegisterArea::D, 10};
project.hmiPages.front().controls.push_back(configured_float);
project.controlLogics.front().rungs.front().output = LogicNode{
"move-output",
MoveNodeConfig{
WordOperand{
WordOperandKind::Constant,
RegisterAddress{RegisterArea::D, 0},
1},
RegisterAddress{RegisterArea::D, 11}},
true};
require(!project.validate(),
"ordinary integer instructions must not write inside Float32 range");
}

void testHmiAppearancePropertyBoundaries()
{
// 外观属性必须在领域层拒绝格式错误,但不能影响未知扩展属性
@@ -860,7 +942,9 @@ int main()
testRegisterAddressBoundaries();
testRegisterAddressParsing();
testRegisterRepositorySeparatesAreas();
testFloat32CodecAndPairAccess();
testHmiControlRegistryCompleteness();
testFloat32HmiBoundaries();
testHmiAppearancePropertyBoundaries();
testLogicNodeConfigurationBoundaries();
testEdgeAndCommentBoundaries();


+ 13
- 0
app/tests/hmi_editor_service_tests.cpp Wyświetl plik

@@ -10,6 +10,7 @@

#include <exception>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>

@@ -132,6 +133,18 @@ void testRuntimeUsesRegisterRepository()
require(numeric_value.succeeded && numeric_value.word_value == -18,
"numeric display must read D values through the repository");

HmiControl float_input = numeric_input;
float_input.dataType = RegisterDataType::Float32;
float_input.binding = RegisterAddress{RegisterArea::D, 20};
require(runtime_service.writeNumericInput(float_input, -2.5).succeeded,
"Float32 numeric input must write two consecutive D words");
const HmiRuntimeReadResult float_value = runtime_service.readControl(float_input);
require(float_value.succeeded && float_value.float_value == -2.5f,
"Float32 numeric control must decode two consecutive D words");
require(!runtime_service.writeNumericInput(
float_input, std::numeric_limits<double>::infinity()).succeeded,
"Float32 numeric input must reject infinity");

}

void testHistoryAndAtomicBatchDelete()


+ 2
- 0
app/tests/pri/test_layers.pri Wyświetl plik

@@ -2,10 +2,12 @@

DOMAIN_REGISTER_SOURCES = \
../src/domain/register_address.cpp \
../src/domain/register_value_type.cpp \
../src/domain/virtual_register_repository.cpp

DOMAIN_REGISTER_HEADERS = \
../src/domain/register_address.h \
../src/domain/register_value_type.h \
../src/domain/register_repository.h \
../src/domain/virtual_register_repository.h



+ 22
- 0
app/tests/project_management_tests.cpp Wyświetl plik

@@ -429,6 +429,7 @@ void testExampleProjectRoundTrip()
const QString unsupported_hmi_type_path = directory.filePath(
"unsupported-hmi-type.json");
const QString missing_initial_path = directory.filePath("missing-initial.json");
const QString missing_data_type_path = directory.filePath("missing-data-type.json");
const QString missing_target_path = directory.filePath("missing-target.json");
const QString missing_alarms_path = directory.filePath("missing-alarms.json");
const QString missing_register_comments_path = directory.filePath(
@@ -450,6 +451,7 @@ void testExampleProjectRoundTrip()
require(!saved_json.contains("\"dataPoints\""),
"saved project must not contain the removed data point model");
require(saved_json.contains("\"formatVersion\": \"1.0\"")
&& saved_json.contains("\"dataType\": \"int16\"")
&& saved_json.contains("\"buttonOperation\": \"setOn\"")
&& saved_json.contains("\"initialHmiPageId\": \"main-page\"")
&& saved_json.contains("\"targetPageId\": \"settings-page\"")
@@ -604,6 +606,26 @@ void testExampleProjectRoundTrip()
&& missing_result.storageError == ProjectStorageError::MissingField,
"the 1.0 schema must require initialHmiPageId without migration defaults");

QJsonObject missing_data_type = QJsonDocument::fromJson(saved_json).object();
QJsonArray data_type_pages = missing_data_type.value(
QStringLiteral("hmiPages")).toArray();
QJsonObject data_type_page = data_type_pages.at(0).toObject();
QJsonArray data_type_controls = data_type_page.value(
QStringLiteral("controls")).toArray();
QJsonObject numeric_display = data_type_controls.at(2).toObject();
numeric_display.remove(QStringLiteral("dataType"));
data_type_controls.replace(2, numeric_display);
data_type_page.insert(QStringLiteral("controls"), data_type_controls);
data_type_pages.replace(0, data_type_page);
missing_data_type.insert(QStringLiteral("hmiPages"), data_type_pages);
writeText(
missing_data_type_path,
QJsonDocument(missing_data_type).toJson(QJsonDocument::Compact));
missing_result = service.load(missing_data_type_path.toStdString());
require(!missing_result.succeeded
&& missing_result.storageError == ProjectStorageError::MissingField,
"numeric controls must require dataType in strict 1.0 JSON");

QJsonObject missing_alarms = QJsonDocument::fromJson(saved_json).object();
missing_alarms.remove(QStringLiteral("alarmDefinitions"));
writeText(


+ 42
- 0
app/tests/register_monitor_service_tests.cpp Wyświetl plik

@@ -6,6 +6,8 @@
#include "support/test_support.h"

#include <iostream>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <string>

@@ -101,6 +103,45 @@ void testRegisterWrites()
"monitor word writes must preserve signed 16-bit boundary values");
}

void testFloat32Monitoring()
{
VirtualRegisterRepository repository;
RegisterMonitorService service(repository);
const RegisterMonitorResult added = service.addRange(
"D1", 3, RegisterDataType::Float32);
require(added.succeeded && added.affectedCount == 3
&& service.points().size() == 3U
&& service.points()[0].address == RegisterAddress{RegisterArea::D, 1}
&& service.points()[1].address == RegisterAddress{RegisterArea::D, 3}
&& service.points()[2].address == RegisterAddress{RegisterArea::D, 5},
"Float32 monitor batches must advance by two D words");
require(service.float32Starts().size() == 3U
&& service.pollAddresses().size() == 6U,
"Float32 monitor points must expose both words for polling");

const RegisterMonitorResult duplicate = service.addRange(
"D1", 1, RegisterDataType::Float32);
require(!duplicate.succeeded && duplicate.error == RegisterMonitorError::NoChange,
"identical Float32 monitor points must be rejected");
const RegisterMonitorResult overlap = service.addRange(
"D2", 1, RegisterDataType::Int16);
require(overlap.succeeded && !overlap.message.empty(),
"partially overlapping monitor points must be added with a warning");
require(service.remove(std::vector<MonitorPoint>{
MonitorPoint{{RegisterArea::D, 2}, RegisterDataType::Int16}}).succeeded
&& service.points().size() == 3U,
"overlapping monitor removal must identify the point type");

require(service.writeFloat({RegisterArea::D, 1}, -2.5f).succeeded,
"Float32 monitor writes must use the pair write path");
const std::vector<MonitorValue> values = service.values(false);
require(values.front().state == MonitorValueState::Valid
&& std::fabs(values.front().floatValue - (-2.5f)) < 0.000001f,
"Float32 monitor values must decode the pair correctly");
require(!service.writeFloat({RegisterArea::D, 4000}, 1.0f).succeeded,
"Float32 monitor writes at D4000 must be rejected");
}

} // namespace

int main()
@@ -110,6 +151,7 @@ int main()
testRangeManagement();
testSharedActiveRepositoryValues();
testRegisterWrites();
testFloat32Monitoring();
}
catch (const std::exception &error)
{


Ładowanie…
Anuluj
Zapisz