From 1034198aedd3ad3c75a237360e66c679c309eb03 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Tue, 25 Aug 2026 16:14:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81D=E5=8C=BAFloat32?= =?UTF-8?q?=E8=AF=BB=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/integrated_platform.pro | 2 + app/src/domain/active_register_repository.cpp | 13 ++ app/src/domain/active_register_repository.h | 4 + app/src/domain/hmi_model.cpp | 14 ++ app/src/domain/hmi_model.h | 2 + app/src/domain/project_model.cpp | 102 ++++++++++ app/src/domain/register_monitor_model.cpp | 61 +++++- app/src/domain/register_monitor_model.h | 20 +- app/src/domain/register_repository.h | 37 ++++ app/src/domain/register_value_type.cpp | 51 +++++ app/src/domain/register_value_type.h | 22 +++ .../domain/virtual_register_repository.cpp | 13 ++ app/src/domain/virtual_register_repository.h | 4 + .../infrastructure/json_project_storage.cpp | 19 ++ .../plc_communication_service.cpp | 178 +++++++++++++++++- .../plc_communication_service.h | 9 + .../plc_register_repository.cpp | 25 ++- .../infrastructure/plc_register_repository.h | 10 +- app/src/services/hmi_editor_service.cpp | 1 + app/src/services/hmi_runtime_service.cpp | 58 +++++- app/src/services/hmi_runtime_service.h | 3 +- app/src/services/plc_communication_gateway.h | 16 ++ app/src/services/register_monitor_service.cpp | 151 +++++++++++++-- app/src/services/register_monitor_service.h | 8 + app/src/services/runtime_mode_service.cpp | 41 +++- app/src/services/runtime_mode_service.h | 4 + app/src/ui/free_monitor_widget.cpp | 78 ++++++-- app/src/ui/free_monitor_widget.ui | 12 +- app/src/ui/hmi_editor_widget.cpp | 54 ++++-- app/src/ui/main_window.ui | 13 +- app/src/ui/property_panel_controller.cpp | 17 ++ app/src/ui/runtime_panel_controller.cpp | 3 +- app/tests/domain_tests.cpp | 84 +++++++++ app/tests/hmi_editor_service_tests.cpp | 13 ++ app/tests/pri/test_layers.pri | 2 + app/tests/project_management_tests.cpp | 22 +++ app/tests/register_monitor_service_tests.cpp | 42 +++++ 37 files changed, 1133 insertions(+), 75 deletions(-) create mode 100644 app/src/domain/register_value_type.cpp create mode 100644 app/src/domain/register_value_type.h diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 1f03ed9..f921940 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -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 \ diff --git a/app/src/domain/active_register_repository.cpp b/app/src/domain/active_register_repository.cpp index a91c5e8..b43b132 100644 --- a/app/src/domain/active_register_repository.cpp +++ b/app/src/domain/active_register_repository.cpp @@ -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 &values) +{ + return repository_->writeWordPair(address, values); +} diff --git a/app/src/domain/active_register_repository.h b/app/src/domain/active_register_repository.h index cb11e0b..0e0b4ff 100644 --- a/app/src/domain/active_register_repository.h +++ b/app/src/domain/active_register_repository.h @@ -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 &values) override; private: RegisterRepository *repository_ = nullptr; diff --git a/app/src/domain/hmi_model.cpp b/app/src/domain/hmi_model.cpp index 96af83b..0907a79 100644 --- a/app/src/domain/hmi_model.cpp +++ b/app/src/domain/hmi_model.cpp @@ -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()) diff --git a/app/src/domain/hmi_model.h b/app/src/domain/hmi_model.h index 4f4a7a0..f57ca52 100644 --- a/app/src/domain/hmi_model.h +++ b/app/src/domain/hmi_model.h @@ -8,6 +8,7 @@ #pragma once #include "register_address.h" +#include "register_value_type.h" #include "project_limits.h" #include @@ -86,6 +87,7 @@ struct HmiControl HmiRect bounds; std::string text; std::optional binding; + RegisterDataType dataType = RegisterDataType::Int16; std::map properties; HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; std::optional pageJump; diff --git a/app/src/domain/project_model.cpp b/app/src/domain/project_model.cpp index 4dccc6e..3651df9 100644 --- a/app/src/domain/project_model.cpp +++ b/app/src/domain/project_model.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include 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; + if constexpr (std::is_same_v + || std::is_same_v) + { + *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 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; + } + } + } } } } diff --git a/app/src/domain/register_monitor_model.cpp b/app/src/domain/register_monitor_model.cpp index 285beb2..f8afd34 100644 --- a/app/src/domain/register_monitor_model.cpp +++ b/app/src/domain/register_monitor_model.cpp @@ -4,37 +4,80 @@ const std::vector &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 &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(); } diff --git a/app/src/domain/register_monitor_model.h b/app/src/domain/register_monitor_model.h index e9a11ca..5a7cad9 100644 --- a/app/src/domain/register_monitor_model.h +++ b/app/src/domain/register_monitor_model.h @@ -1,6 +1,7 @@ #pragma once #include "register_address.h" +#include "register_value_type.h" #include #include @@ -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 &addresses() const; + const std::vector &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 addresses_; + mutable std::vector addresses_; + std::vector points_; }; diff --git a/app/src/domain/register_repository.h b/app/src/domain/register_repository.h index 6514a7c..c7a818c 100644 --- a/app/src/domain/register_repository.h +++ b/app/src/domain/register_repository.h @@ -3,6 +3,7 @@ #include "register_address.h" #include +#include // 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息 enum class RegisterError @@ -37,6 +38,13 @@ struct RegisterWriteResult RegisterError error = RegisterError::Unavailable; // 写入失败原因 }; +struct WordPairReadResult +{ + bool succeeded = false; + std::array 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 &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]); + } }; diff --git a/app/src/domain/register_value_type.cpp b/app/src/domain/register_value_type.cpp new file mode 100644 index 0000000..35f6794 --- /dev/null +++ b/app/src/domain/register_value_type.cpp @@ -0,0 +1,51 @@ +#include "register_value_type.h" + +#include +#include + +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 Float32Codec::encode(float value) +{ + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return {static_cast(bits & 0xffffU), + static_cast(bits >> 16U)}; +} + +std::optional Float32Codec::decode( + std::int16_t low_word, std::int16_t high_word) +{ + const std::uint32_t bits = static_cast(low_word) + | (static_cast(static_cast(high_word)) << 16U); + float value = 0.0f; + std::memcpy(&value, &bits, sizeof(value)); + return std::isfinite(value) ? std::optional(value) : std::nullopt; +} diff --git a/app/src/domain/register_value_type.h b/app/src/domain/register_value_type.h new file mode 100644 index 0000000..2605f64 --- /dev/null +++ b/app/src/domain/register_value_type.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include + +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 encode(float value); + static std::optional decode(std::int16_t low_word, std::int16_t high_word); +}; diff --git a/app/src/domain/virtual_register_repository.cpp b/app/src/domain/virtual_register_repository.cpp index 45b6e26..6503515 100644 --- a/app/src/domain/virtual_register_repository.cpp +++ b/app/src/domain/virtual_register_repository.cpp @@ -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 &values) +{ + return RegisterRepository::writeWordPair(address, values); +} + void VirtualRegisterRepository::clear() { // 离线运行复位时同时清空 M 区和 D 区 diff --git a/app/src/domain/virtual_register_repository.h b/app/src/domain/virtual_register_repository.h index 400de18..498097d 100644 --- a/app/src/domain/virtual_register_repository.h +++ b/app/src/domain/virtual_register_repository.h @@ -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 &values) override; // 将所有虚拟寄存器恢复为默认值,开始新的离线会话 void clear(); diff --git a/app/src/infrastructure/json_project_storage.cpp b/app/src/infrastructure/json_project_storage.cpp index 991cdda..7b192bf 100644 --- a/app/src/infrastructure/json_project_storage.cpp +++ b/app/src/infrastructure/json_project_storage.cpp @@ -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; diff --git a/app/src/infrastructure/plc_communication_service.cpp b/app/src/infrastructure/plc_communication_service.cpp index ce7d9da..eedbce3 100644 --- a/app/src/infrastructure/plc_communication_service.cpp +++ b/app/src/infrastructure/plc_communication_service.cpp @@ -78,7 +78,25 @@ bool normalizePollAddresses( return true; } -std::size_t pollBlockCount(const std::vector &addresses) +bool isFloat32Start( + const std::vector &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 &addresses, + const std::vector &float32_starts) { if (addresses.empty()) { @@ -91,6 +109,19 @@ std::size_t pollBlockCount(const std::vector &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 &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 &addresses) +{ + return setPollAddresses(addresses, {}); +} + +PlcCommunicationResult PlcCommunicationService::setPollAddresses( + const std::vector &addresses, + const std::vector &float32_starts) { std::vector normalized; std::string error; @@ -267,7 +310,60 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( { return {false, error}; } - if (pollBlockCount(normalized) > ProjectLimits::kMaximumPollBlocks) + std::vector 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 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 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 &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(values[0])); + unit.setValue(1, static_cast(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) { diff --git a/app/src/infrastructure/plc_communication_service.h b/app/src/infrastructure/plc_communication_service.h index 3d20433..d26381d 100644 --- a/app/src/infrastructure/plc_communication_service.h +++ b/app/src/infrastructure/plc_communication_service.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -39,6 +40,9 @@ public: // 设置需要周期性读取的 M/D 地址集合 PlcCommunicationResult setPollAddresses( const std::vector &addresses) override; + PlcCommunicationResult setPollAddresses( + const std::vector &addresses, + const std::vector &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 &values); // 更新“所有轮询块均成功读取”的真机进入资格 void updateInitialReadCompleted(bool completed, bool force_notification = false); // 处理没有主动断开时发生的串口断线 @@ -117,6 +124,8 @@ private: PlcSerialConfiguration configuration_; // 当前串口和站号配置 std::vector poll_addresses_; // 当前生效的轮询地址 std::vector pending_poll_addresses_; // 请求在途时暂存的新地址 + std::vector poll_float32_starts_; // 当前轮询集合中的 Float32 起始地址 + std::vector pending_poll_float32_starts_; // 请求在途时暂存的 Float32 起始地址 std::vector poll_blocks_; // 根据地址合并出的连续读块 std::size_t next_poll_block_ = 0; // 下一次要读取的读块下标 QModbusReply *pending_reply_ = nullptr; // 当前未完成的读请求或恢复探测 diff --git a/app/src/infrastructure/plc_register_repository.cpp b/app/src/infrastructure/plc_register_repository.cpp index 3705bb8..c90c8b3 100644 --- a/app/src/infrastructure/plc_register_repository.cpp +++ b/app/src/infrastructure/plc_register_repository.cpp @@ -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 &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 bit_handler, - std::function word_handler) + std::function word_handler, + std::function &)> 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) diff --git a/app/src/infrastructure/plc_register_repository.h b/app/src/infrastructure/plc_register_repository.h index 9ea5f42..01e051d 100644 --- a/app/src/infrastructure/plc_register_repository.h +++ b/app/src/infrastructure/plc_register_repository.h @@ -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 &values) override; // 注入异步写入回调;回调成功不代表缓存已经更新 void setWriteHandlers( std::function bit_handler, - std::function word_handler); + std::function word_handler, + std::function &)> word_pair_handler = {}); // 由通信服务在读回成功后更新缓存并标记地址有效 void updateBit(int address, bool value); void updateWord(int address, std::int16_t value); @@ -40,4 +46,6 @@ private: std::array valid_words_{}; // 对应 D 地址是否读到过有效值 std::function bit_handler_; // M 区异步写入回调 std::function word_handler_; // D 区异步写入回调 + std::function &)> word_pair_handler_; }; diff --git a/app/src/services/hmi_editor_service.cpp b/app/src/services/hmi_editor_service.cpp index d3aba9f..467d41c 100644 --- a/app/src/services/hmi_editor_service.cpp +++ b/app/src/services/hmi_editor_service.cpp @@ -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; diff --git a/app/src/services/hmi_runtime_service.cpp b/app/src/services/hmi_runtime_service.cpp index 198193f..30d879a 100644 --- a/app/src/services/hmi_runtime_service.cpp +++ b/app/src/services/hmi_runtime_service.cpp @@ -2,11 +2,14 @@ #include "domain/hmi_control_registry.h" +#include +#include + 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 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(std::numeric_limits::max()) + || value < -static_cast(std::numeric_limits::max())) + { + return writeFailure(HmiRuntimeError::InvalidBinding); + } + const float float_value = static_cast(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::min() + || value > std::numeric_limits::max() + || std::trunc(value) != value) + { + return writeFailure(HmiRuntimeError::InvalidBinding); + } + result = repository_.writeWord( + *control.binding, static_cast(value)); + } return result.succeeded ? HmiRuntimeWriteResult{true, HmiRuntimeError::None} : writeFailure(repositoryError(result.error)); diff --git a/app/src/services/hmi_runtime_service.h b/app/src/services/hmi_runtime_service.h index bea4040..827112a 100644 --- a/app/src/services/hmi_runtime_service.h +++ b/app/src/services/hmi_runtime_service.h @@ -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 位 diff --git a/app/src/services/plc_communication_gateway.h b/app/src/services/plc_communication_gateway.h index 390a7a0..1ea483f 100644 --- a/app/src/services/plc_communication_gateway.h +++ b/app/src/services/plc_communication_gateway.h @@ -152,6 +152,22 @@ public: virtual PlcCommunicationResult setPollAddresses( const std::vector &addresses) = 0; + /** + * @brief 设置轮询地址并标记必须成对读取的 Float32 起始地址 + * @param addresses 需要周期性读回的 M/D 地址集合 + * @param float32_starts D 区 Float32 起始地址;每个地址必须和下一个 D 一起读取 + * @return true 表示集合已接受 + * + * 默认实现兼容只关心地址集合的测试网关和旧调用方;真实 Modbus 实现会使用成对信息避免拆分 Float32 + */ + virtual PlcCommunicationResult setPollAddresses( + const std::vector &addresses, + const std::vector &float32_starts) + { + (void)float32_starts; + return setPollAddresses(addresses); + } + // 返回当前连接生命周期状态;Connected 不代表首读资格已经完成 virtual PlcConnectionState state() const = 0; diff --git a/app/src/services/register_monitor_service.cpp b/app/src/services/register_monitor_service.cpp index c7678bc..d135750 100644 --- a/app/src/services/register_monitor_service.cpp +++ b/app/src/services/register_monitor_service.cpp @@ -1,6 +1,8 @@ #include "register_monitor_service.h" #include +#include +#include #include 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(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(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(pending.size())}; } RegisterMonitorResult RegisterMonitorService::remove( @@ -97,6 +134,28 @@ RegisterMonitorResult RegisterMonitorService::remove( return {true, RegisterMonitorError::None, {}, removed_count}; } +RegisterMonitorResult RegisterMonitorService::remove( + const std::vector &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 &RegisterMonitorService::addresses() const { return model_.addresses(); @@ -154,11 +232,15 @@ const std::vector &RegisterMonitorService::addresses() const std::vector RegisterMonitorService::values(bool communication_fault) const { std::vector 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 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 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 &RegisterMonitorService::points() const +{ + return model_.points(); +} + +std::vector RegisterMonitorService::pollAddresses() const +{ + std::vector 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 RegisterMonitorService::float32Starts() const +{ + std::vector result; + for (const MonitorPoint &point : model_.points()) + { + if (point.dataType == RegisterDataType::Float32) + { + result.push_back(point.address); + } + } + return result; +} + void RegisterMonitorService::setAddressesChangedCallback(std::function callback) { addresses_changed_callback_ = std::move(callback); diff --git a/app/src/services/register_monitor_service.h b/app/src/services/register_monitor_service.h index 90d86e4..bbc3642 100644 --- a/app/src/services/register_monitor_service.h +++ b/app/src/services/register_monitor_service.h @@ -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 &addresses); + RegisterMonitorResult remove(const std::vector &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 &addresses() const; + const std::vector &points() const; + std::vector pollAddresses() const; + std::vector float32Starts() const; /** * @brief 读取监控列表的当前值并生成 UI 展示模型 * @param communication_fault 通信处于故障态时,将成功读回值标记为通信故障 diff --git a/app/src/services/runtime_mode_service.cpp b/app/src/services/runtime_mode_service.cpp index ad68734..9df711c 100644 --- a/app/src/services/runtime_mode_service.cpp +++ b/app/src/services/runtime_mode_service.cpp @@ -253,8 +253,16 @@ PlcCommunicationResult RuntimeModeService::connectPlc( void RuntimeModeService::setMonitorAddresses( const std::vector &addresses) +{ + setMonitorAddresses(addresses, {}); +} + +void RuntimeModeService::setMonitorAddresses( + const std::vector &addresses, + const std::vector &float32_starts) { monitor_addresses_ = addresses; + monitor_float32_starts_ = float32_starts; refreshPlcPollAddresses(); } @@ -266,6 +274,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() } // 轮询集合来自自由监控、HMI、报警和梯形图引用的并集 std::vector addresses = monitor_addresses_; + std::vector 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() diff --git a/app/src/services/runtime_mode_service.h b/app/src/services/runtime_mode_service.h index f39ea35..aaa1ebd 100644 --- a/app/src/services/runtime_mode_service.h +++ b/app/src/services/runtime_mode_service.h @@ -109,6 +109,9 @@ public: * @param addresses 自由监控当前需要读取的 M/D 地址 */ void setMonitorAddresses(const std::vector &addresses); + void setMonitorAddresses( + const std::vector &addresses, + const std::vector &float32_starts); /** * @brief 汇总工程和自由监控引用并刷新 PLC 轮询地址 @@ -146,4 +149,5 @@ private: RegisterRepository *plc_repository_ = nullptr; // 真机模式的 PLC 缓存仓库 std::function plc_status_changed_callback_; // PLC 状态变化通知 std::vector monitor_addresses_; // 自由监控额外引用的地址 + std::vector monitor_float32_starts_; // 自由监控中的 Float32 起始地址 }; diff --git a/app/src/ui/free_monitor_widget.cpp b/app/src/ui/free_monitor_widget.cpp index e3de074..e703c2f 100644 --- a/app/src/ui/free_monitor_widget.cpp +++ b/app/src/ui/free_monitor_widget.cpp @@ -13,6 +13,7 @@ #include #include +#include #include 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(RegisterDataType::Int16)); + ui_->dataTypeComboBox->setItemData(1, static_cast(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(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(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(row)); + const MonitorPoint point = service_.points().at(static_cast(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(value)); + result = point.dataType == RegisterDataType::Float32 + ? service_.writeFloat(address, static_cast(value)) + : service_.writeWord(address, static_cast(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(address.size())), - ui_->countSpinBox->value()); + ui_->countSpinBox->value(), + static_cast(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 addresses; - const std::vector &all_addresses = service_.addresses(); + std::vector points; + const std::vector &all_points = service_.points(); for (int row : rows) { - if (row >= 0 && static_cast(row) < all_addresses.size()) + if (row >= 0 && static_cast(row) < all_points.size()) { - addresses.push_back(all_addresses.at(static_cast(row))); + points.push_back(all_points.at(static_cast(row))); } } - const RegisterMonitorResult result = service_.remove(addresses); + const RegisterMonitorResult result = service_.remove(points); if (result.succeeded) { reloadAddresses(); diff --git a/app/src/ui/free_monitor_widget.ui b/app/src/ui/free_monitor_widget.ui index 38e2953..ff57067 100644 --- a/app/src/ui/free_monitor_widget.ui +++ b/app/src/ui/free_monitor_widget.ui @@ -15,13 +15,15 @@ 起始地址 90016016777215M0 或 D0true - 连续个数 - 1641 - 添加 - Qt::Horizontal4020 + 类型 + Int16Float32 + 连续个数 + 1641 + 添加 + Qt::Horizontal4020 删除选中的监控地址删除 清空监控表清空 - 数据源:未启用Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 数据源:未启用Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter diff --git a/app/src/ui/hmi_editor_widget.cpp b/app/src/ui/hmi_editor_widget.cpp index 5543b65..dd154bb 100644 --- a/app/src/ui/hmi_editor_widget.cpp +++ b/app/src/ui/hmi_editor_widget.cpp @@ -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(control->text.size())), - initial, - std::numeric_limits::min(), - std::numeric_limits::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(control->text.size())), + current.succeeded ? static_cast(current.float_value) : 0.0, + -static_cast(std::numeric_limits::max()), + static_cast(std::numeric_limits::max()), + 7, + &accepted); + } + else + { + value = QInputDialog::getInt( + this, + tr("输入 Int16"), + QString::fromUtf8( + control->text.data(), static_cast(control->text.size())), + current.succeeded ? current.word_value : 0, + std::numeric_limits::min(), + std::numeric_limits::max(), + 1, + &accepted); + } if (!accepted) { return; } const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( - *control, static_cast(value)); + *control, value); if (!result.succeeded) { emit editorError(tr("寄存器操作失败")); diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index 79043cc..a2c04c9 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -738,7 +738,18 @@ - + + + 数值类型 + + + + + Int16 + Float32 + + + 应用属性 diff --git a/app/src/ui/property_panel_controller.cpp b/app/src/ui/property_panel_controller.cpp index 1d240bf..4531a2e 100644 --- a/app/src/ui/property_panel_controller.cpp +++ b/app/src/ui/property_panel_controller.cpp @@ -108,6 +108,10 @@ void PropertyPanelController::configure() 2, static_cast(HmiButtonOperation::Toggle)); ui_.buttonOperationComboBox->setItemData( 3, static_cast(HmiButtonOperation::MomentaryOn)); + ui_.dataTypeComboBox->setItemData( + 0, static_cast(RegisterDataType::Int16)); + ui_.dataTypeComboBox->setItemData( + 1, static_cast(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(ui_.bindingIndexSpinBox), static_cast(ui_.targetPageComboBox), static_cast(ui_.buttonOperationComboBox), + static_cast(ui_.dataTypeComboBox), static_cast(ui_.textColorEdit), static_cast(ui_.textColorButton), static_cast(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(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(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( + ui_.dataTypeComboBox->currentData().toInt()); const HmiControlDescriptor *descriptor = findHmiControlDescriptor(control.type); if (descriptor == nullptr diff --git a/app/src/ui/runtime_panel_controller.cpp b/app/src/ui/runtime_panel_controller.cpp index b5d1def..2438877 100644 --- a/app/src/ui/runtime_panel_controller.cpp +++ b/app/src/ui/runtime_panel_controller.cpp @@ -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()); diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index 66ac201..d2de684 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -10,10 +10,12 @@ #include "support/test_support.h" #include +#include #include #include #include #include +#include #include #include #include @@ -101,6 +103,37 @@ void testRegisterRepositorySeparatesAreas() "M address must not be read as a word"); } +void testFloat32CodecAndPairAccess() +{ + const std::array values = {1.0f, -2.5f, 0.1f, 0.0f}; + for (const float value : values) + { + const std::array words = Float32Codec::encode(value); + const std::optional 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(0), + static_cast(0x7f80)).has_value(), + "Float32 codec must reject positive infinity"); + require(!Float32Codec::decode( + static_cast(0), + static_cast(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 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(); diff --git a/app/tests/hmi_editor_service_tests.cpp b/app/tests/hmi_editor_service_tests.cpp index f598cf4..f1348b2 100644 --- a/app/tests/hmi_editor_service_tests.cpp +++ b/app/tests/hmi_editor_service_tests.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -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::infinity()).succeeded, + "Float32 numeric input must reject infinity"); + } void testHistoryAndAtomicBatchDelete() diff --git a/app/tests/pri/test_layers.pri b/app/tests/pri/test_layers.pri index 9b5d292..d48a5a7 100644 --- a/app/tests/pri/test_layers.pri +++ b/app/tests/pri/test_layers.pri @@ -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 diff --git a/app/tests/project_management_tests.cpp b/app/tests/project_management_tests.cpp index 04658d4..3deb7c2 100644 --- a/app/tests/project_management_tests.cpp +++ b/app/tests/project_management_tests.cpp @@ -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( diff --git a/app/tests/register_monitor_service_tests.cpp b/app/tests/register_monitor_service_tests.cpp index 411ccf8..6af9c22 100644 --- a/app/tests/register_monitor_service_tests.cpp +++ b/app/tests/register_monitor_service_tests.cpp @@ -6,6 +6,8 @@ #include "support/test_support.h" #include +#include +#include #include #include @@ -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{{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 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) {