| @@ -29,6 +29,7 @@ SOURCES += \ | |||||
| src/ui/property_panel_controller.cpp \ | src/ui/property_panel_controller.cpp \ | ||||
| src/ui/runtime_panel_controller.cpp \ | src/ui/runtime_panel_controller.cpp \ | ||||
| src/domain/register_address.cpp \ | src/domain/register_address.cpp \ | ||||
| src/domain/register_value_type.cpp \ | |||||
| src/domain/virtual_register_repository.cpp \ | src/domain/virtual_register_repository.cpp \ | ||||
| src/domain/active_register_repository.cpp \ | src/domain/active_register_repository.cpp \ | ||||
| src/domain/register_monitor_model.cpp \ | src/domain/register_monitor_model.cpp \ | ||||
| @@ -75,6 +76,7 @@ HEADERS += \ | |||||
| src/ui/property_panel_controller.h \ | src/ui/property_panel_controller.h \ | ||||
| src/ui/runtime_panel_controller.h \ | src/ui/runtime_panel_controller.h \ | ||||
| src/domain/register_address.h \ | src/domain/register_address.h \ | ||||
| src/domain/register_value_type.h \ | |||||
| src/domain/register_repository.h \ | src/domain/register_repository.h \ | ||||
| src/domain/virtual_register_repository.h \ | src/domain/virtual_register_repository.h \ | ||||
| src/domain/active_register_repository.h \ | src/domain/active_register_repository.h \ | ||||
| @@ -32,3 +32,16 @@ RegisterWriteResult ActiveRegisterRepository::writeWord( | |||||
| { | { | ||||
| return repository_->writeWord(address, value); | 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); | |||||
| } | |||||
| @@ -15,6 +15,10 @@ public: | |||||
| WordReadResult readWord(const RegisterAddress &address) const override; | WordReadResult readWord(const RegisterAddress &address) const override; | ||||
| RegisterWriteResult writeWord( | RegisterWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value) override; | 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: | private: | ||||
| RegisterRepository *repository_ = nullptr; | RegisterRepository *repository_ = nullptr; | ||||
| @@ -171,6 +171,20 @@ bool HmiControl::validate(std::string *error) const | |||||
| setError(error, "HMI 控件绑定了无效地址"); | setError(error, "HMI 控件绑定了无效地址"); | ||||
| return false; | 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 (type == HmiControlType::PageJump) | ||||
| { | { | ||||
| if (!pageJump.has_value()) | if (!pageJump.has_value()) | ||||
| @@ -8,6 +8,7 @@ | |||||
| #pragma once | #pragma once | ||||
| #include "register_address.h" | #include "register_address.h" | ||||
| #include "register_value_type.h" | |||||
| #include "project_limits.h" | #include "project_limits.h" | ||||
| #include <map> | #include <map> | ||||
| @@ -86,6 +87,7 @@ struct HmiControl | |||||
| HmiRect bounds; | HmiRect bounds; | ||||
| std::string text; | std::string text; | ||||
| std::optional<RegisterAddress> binding; | std::optional<RegisterAddress> binding; | ||||
| RegisterDataType dataType = RegisterDataType::Int16; | |||||
| std::map<std::string, std::string> properties; | std::map<std::string, std::string> properties; | ||||
| HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; | HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; | ||||
| std::optional<HmiPageJumpConfig> pageJump; | std::optional<HmiPageJumpConfig> pageJump; | ||||
| @@ -4,6 +4,8 @@ | |||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <cctype> | #include <cctype> | ||||
| #include <variant> | |||||
| #include <type_traits> | |||||
| namespace { | namespace { | ||||
| @@ -70,6 +72,51 @@ bool containsLineBreak(const std::string &value) | |||||
| || value.find('\n') != std::string::npos; | || 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 | } // namespace | ||||
| bool RegisterComment::validate(std::string *error) const | 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) | for (const ControlLogic &logic : controlLogics) | ||||
| { | { | ||||
| if (!logic.validate(limits, error)) | if (!logic.validate(limits, error)) | ||||
| @@ -307,6 +382,13 @@ bool Project::validate( | |||||
| if (control.binding.has_value()) | if (control.binding.has_value()) | ||||
| { | { | ||||
| poll_addresses.push_back(*control.binding); | 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) | for (const LogicNode *node : nodes) | ||||
| { | { | ||||
| collectRegisterAddressesForLogicNode(node->config, &poll_addresses); | 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -4,37 +4,80 @@ | |||||
| const std::vector<RegisterAddress> &RegisterMonitorModel::addresses() const | 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_; | 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; | return false; | ||||
| } | } | ||||
| addresses_.push_back(address); | |||||
| points_.push_back(point); | |||||
| return true; | return true; | ||||
| } | } | ||||
| bool RegisterMonitorModel::remove(const RegisterAddress &address) | 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; | return false; | ||||
| } | } | ||||
| addresses_.erase(position); | |||||
| points_.erase(position); | |||||
| return true; | return true; | ||||
| } | } | ||||
| void RegisterMonitorModel::clear() | void RegisterMonitorModel::clear() | ||||
| { | { | ||||
| addresses_.clear(); | addresses_.clear(); | ||||
| points_.clear(); | |||||
| } | } | ||||
| @@ -1,6 +1,7 @@ | |||||
| #pragma once | #pragma once | ||||
| #include "register_address.h" | #include "register_address.h" | ||||
| #include "register_value_type.h" | |||||
| #include <cstddef> | #include <cstddef> | ||||
| #include <cstdint> | #include <cstdint> | ||||
| @@ -18,9 +19,18 @@ enum class MonitorValueState | |||||
| struct MonitorValue | struct MonitorValue | ||||
| { | { | ||||
| RegisterAddress address{RegisterArea::M, 0}; | RegisterAddress address{RegisterArea::M, 0}; | ||||
| RegisterDataType dataType = RegisterDataType::Int16; | |||||
| int endAddress = 0; | |||||
| MonitorValueState state = MonitorValueState::Unavailable; | MonitorValueState state = MonitorValueState::Unavailable; | ||||
| bool bitValue = false; | bool bitValue = false; | ||||
| std::int16_t wordValue = 0; | 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<RegisterAddress> &addresses() const; | ||||
| const std::vector<MonitorPoint> &points() const; | |||||
| // 判断地址是否已经在监控列表中 | // 判断地址是否已经在监控列表中 | ||||
| bool contains(const RegisterAddress &address) const; | |||||
| bool contains(const MonitorPoint &point) const; | |||||
| // 添加一个地址;重复或超过上限时返回 false | // 添加一个地址;重复或超过上限时返回 false | ||||
| bool add(const RegisterAddress &address); | |||||
| bool add(const MonitorPoint &point); | |||||
| // 删除一个地址;地址不存在时返回 false | // 删除一个地址;地址不存在时返回 false | ||||
| bool remove(const RegisterAddress &address); | bool remove(const RegisterAddress &address); | ||||
| // 删除一个精确的监控点;地址相同但类型不同的点互不影响 | |||||
| bool remove(const MonitorPoint &point); | |||||
| // 清空当前会话的监控列表 | // 清空当前会话的监控列表 | ||||
| void clear(); | void clear(); | ||||
| private: | private: | ||||
| std::vector<RegisterAddress> addresses_; | |||||
| mutable std::vector<RegisterAddress> addresses_; | |||||
| std::vector<MonitorPoint> points_; | |||||
| }; | }; | ||||
| @@ -3,6 +3,7 @@ | |||||
| #include "register_address.h" | #include "register_address.h" | ||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <array> | |||||
| // 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息 | // 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息 | ||||
| enum class RegisterError | enum class RegisterError | ||||
| @@ -37,6 +38,13 @@ struct RegisterWriteResult | |||||
| RegisterError error = RegisterError::Unavailable; // 写入失败原因 | RegisterError error = RegisterError::Unavailable; // 写入失败原因 | ||||
| }; | }; | ||||
| struct WordPairReadResult | |||||
| { | |||||
| bool succeeded = false; | |||||
| std::array<std::int16_t, 2> values{}; | |||||
| RegisterError error = RegisterError::Unavailable; | |||||
| }; | |||||
| // M/D 寄存器访问边界 | // M/D 寄存器访问边界 | ||||
| // 所有运行态读写都必须经过此接口,UI 不直接接触串口或缓存实现 | // 所有运行态读写都必须经过此接口,UI 不直接接触串口或缓存实现 | ||||
| class RegisterRepository | class RegisterRepository | ||||
| @@ -53,4 +61,33 @@ public: | |||||
| // 写入 D 区 16 位字寄存器 | // 写入 D 区 16 位字寄存器 | ||||
| virtual RegisterWriteResult writeWord( | virtual RegisterWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value) = 0; | 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]); | |||||
| } | |||||
| }; | }; | ||||
| @@ -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; | |||||
| } | |||||
| @@ -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); | |||||
| }; | |||||
| @@ -66,6 +66,19 @@ RegisterWriteResult VirtualRegisterRepository::writeWord( | |||||
| return {true, RegisterError::None}; | 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() | void VirtualRegisterRepository::clear() | ||||
| { | { | ||||
| // 离线运行复位时同时清空 M 区和 D 区 | // 离线运行复位时同时清空 M 区和 D 区 | ||||
| @@ -20,6 +20,10 @@ public: | |||||
| // 更新内存中的 D 区字值 | // 更新内存中的 D 区字值 | ||||
| RegisterWriteResult writeWord( | RegisterWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value) override; | 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(); | void clear(); | ||||
| @@ -658,6 +658,13 @@ QJsonObject serializeHmiControl(const HmiControl &control) | |||||
| object.insert(QStringLiteral("binding"), QJsonValue::Null); | object.insert(QStringLiteral("binding"), QJsonValue::Null); | ||||
| } | } | ||||
| object.insert(QStringLiteral("properties"), serializeProperties(control.properties)); | 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) | if (control.type == HmiControlType::Button) | ||||
| { | { | ||||
| object.insert( | object.insert( | ||||
| @@ -703,6 +710,18 @@ bool parseHmiControl( | |||||
| { | { | ||||
| return false; | 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) | if (control->type == HmiControlType::Button) | ||||
| { | { | ||||
| std::string operation_text; | std::string operation_text; | ||||
| @@ -78,7 +78,25 @@ bool normalizePollAddresses( | |||||
| return true; | 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()) | if (addresses.empty()) | ||||
| { | { | ||||
| @@ -91,6 +109,19 @@ std::size_t pollBlockCount(const std::vector<RegisterAddress> &addresses) | |||||
| int count = 0; | int count = 0; | ||||
| for (const RegisterAddress &address : addresses) | 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 单次上限时,必须开始新读块 | // 跨区域、出现地址间隔或达到 Modbus 单次上限时,必须开始新读块 | ||||
| if (count == 0 | if (count == 0 | ||||
| || address.area() != current_area | || address.area() != current_area | ||||
| @@ -128,6 +159,11 @@ PlcCommunicationService::PlcCommunicationService( | |||||
| [this](const RegisterAddress &address, std::int16_t value) | [this](const RegisterAddress &address, std::int16_t value) | ||||
| { | { | ||||
| return sendWordWrite(address, 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); | connect(&poll_timer_, &QTimer::timeout, this, &PlcCommunicationService::pollNextBlock); | ||||
| @@ -260,6 +296,13 @@ void PlcCommunicationService::disconnectDevice() | |||||
| PlcCommunicationResult PlcCommunicationService::setPollAddresses( | PlcCommunicationResult PlcCommunicationService::setPollAddresses( | ||||
| const std::vector<RegisterAddress> &addresses) | 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::vector<RegisterAddress> normalized; | ||||
| std::string error; | std::string error; | ||||
| @@ -267,7 +310,60 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( | |||||
| { | { | ||||
| return {false, error}; | 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 个读块"}; | return {false, "PLC 轮询地址拆分后最多允许 8 个读块"}; | ||||
| } | } | ||||
| @@ -276,9 +372,11 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( | |||||
| { | { | ||||
| // 当前回复仍在解析旧集合,等它结束后再切换到新集合 | // 当前回复仍在解析旧集合,等它结束后再切换到新集合 | ||||
| pending_poll_addresses_ = std::move(normalized); | pending_poll_addresses_ = std::move(normalized); | ||||
| pending_poll_float32_starts_ = std::move(normalized_float32_starts); | |||||
| poll_update_pending_ = true; | poll_update_pending_ = true; | ||||
| return {true, {}}; | return {true, {}}; | ||||
| } | } | ||||
| poll_float32_starts_ = std::move(normalized_float32_starts); | |||||
| applyPollAddresses(normalized); | applyPollAddresses(normalized); | ||||
| return {true, {}}; | return {true, {}}; | ||||
| } | } | ||||
| @@ -344,6 +442,17 @@ void PlcCommunicationService::rebuildPollBlocks() | |||||
| return left.index() < right.index(); | return left.index() < right.index(); | ||||
| }); | }); | ||||
| addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); | 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 单次读取上限 | // 将相邻地址合并为读块,同时遵守 Modbus 单次读取上限 | ||||
| poll_blocks_.clear(); | poll_blocks_.clear(); | ||||
| for (const RegisterAddress &address : addresses) | for (const RegisterAddress &address : addresses) | ||||
| @@ -352,6 +461,21 @@ void PlcCommunicationService::rebuildPollBlocks() | |||||
| { | { | ||||
| continue; | 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() | if (poll_blocks_.empty() | ||||
| || poll_blocks_.back().area != address.area() | || poll_blocks_.back().area != address.area() | ||||
| || address.index() > poll_blocks_.back().startAddress | || address.index() > poll_blocks_.back().startAddress | ||||
| @@ -387,6 +511,7 @@ void PlcCommunicationService::applyPollAddresses( | |||||
| rebuildPollBlocks(); | rebuildPollBlocks(); | ||||
| poll_update_pending_ = false; | poll_update_pending_ = false; | ||||
| pending_poll_addresses_.clear(); | pending_poll_addresses_.clear(); | ||||
| pending_poll_float32_starts_.clear(); | |||||
| if (isReadingState(state_) && pending_reply_ == nullptr) | if (isReadingState(state_) && pending_reply_ == nullptr) | ||||
| { | { | ||||
| pollNextBlock(); | pollNextBlock(); | ||||
| @@ -459,6 +584,7 @@ void PlcCommunicationService::pollNextBlock() | |||||
| if (poll_update_pending_) | if (poll_update_pending_) | ||||
| { | { | ||||
| const std::vector<RegisterAddress> addresses = pending_poll_addresses_; | const std::vector<RegisterAddress> addresses = pending_poll_addresses_; | ||||
| poll_float32_starts_ = pending_poll_float32_starts_; | |||||
| applyPollAddresses(addresses); | applyPollAddresses(addresses); | ||||
| } | } | ||||
| if (poll_cycle_completed) | if (poll_cycle_completed) | ||||
| @@ -524,6 +650,7 @@ void PlcCommunicationService::probeRecovery() | |||||
| if (poll_update_pending_) | if (poll_update_pending_) | ||||
| { | { | ||||
| const std::vector<RegisterAddress> addresses = pending_poll_addresses_; | const std::vector<RegisterAddress> addresses = pending_poll_addresses_; | ||||
| poll_float32_starts_ = pending_poll_float32_starts_; | |||||
| applyPollAddresses(addresses); | applyPollAddresses(addresses); | ||||
| } | } | ||||
| if (state_ != PlcConnectionState::Faulted | if (state_ != PlcConnectionState::Faulted | ||||
| @@ -695,6 +822,53 @@ RegisterWriteResult PlcCommunicationService::sendWordWrite( | |||||
| return {true, RegisterError::None}; | 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( | void PlcCommunicationService::updateInitialReadCompleted( | ||||
| bool completed, bool force_notification) | bool completed, bool force_notification) | ||||
| { | { | ||||
| @@ -8,6 +8,7 @@ | |||||
| #include <QTimer> | #include <QTimer> | ||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <array> | |||||
| #include <memory> | #include <memory> | ||||
| #include <string> | #include <string> | ||||
| #include <vector> | #include <vector> | ||||
| @@ -39,6 +40,9 @@ public: | |||||
| // 设置需要周期性读取的 M/D 地址集合 | // 设置需要周期性读取的 M/D 地址集合 | ||||
| PlcCommunicationResult setPollAddresses( | PlcCommunicationResult setPollAddresses( | ||||
| const std::vector<RegisterAddress> &addresses) override; | const std::vector<RegisterAddress> &addresses) override; | ||||
| PlcCommunicationResult setPollAddresses( | |||||
| const std::vector<RegisterAddress> &addresses, | |||||
| const std::vector<RegisterAddress> &float32_starts) override; | |||||
| // 返回当前连接状态 | // 返回当前连接状态 | ||||
| PlcConnectionState state() const override; | PlcConnectionState state() const override; | ||||
| @@ -97,6 +101,9 @@ private: | |||||
| RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); | RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); | ||||
| RegisterWriteResult sendWordWrite( | RegisterWriteResult sendWordWrite( | ||||
| const RegisterAddress &address, std::int16_t value); | 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); | void updateInitialReadCompleted(bool completed, bool force_notification = false); | ||||
| // 处理没有主动断开时发生的串口断线 | // 处理没有主动断开时发生的串口断线 | ||||
| @@ -117,6 +124,8 @@ private: | |||||
| PlcSerialConfiguration configuration_; // 当前串口和站号配置 | PlcSerialConfiguration configuration_; // 当前串口和站号配置 | ||||
| std::vector<RegisterAddress> poll_addresses_; // 当前生效的轮询地址 | std::vector<RegisterAddress> poll_addresses_; // 当前生效的轮询地址 | ||||
| std::vector<RegisterAddress> pending_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::vector<PollBlock> poll_blocks_; // 根据地址合并出的连续读块 | ||||
| std::size_t next_poll_block_ = 0; // 下一次要读取的读块下标 | std::size_t next_poll_block_ = 0; // 下一次要读取的读块下标 | ||||
| QModbusReply *pending_reply_ = nullptr; // 当前未完成的读请求或恢复探测 | QModbusReply *pending_reply_ = nullptr; // 当前未完成的读请求或恢复探测 | ||||
| @@ -69,12 +69,35 @@ RegisterWriteResult PlcRegisterRepository::writeWord( | |||||
| : RegisterWriteResult{false, RegisterError::Unavailable}; | : 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( | void PlcRegisterRepository::setWriteHandlers( | ||||
| std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler, | 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); | bit_handler_ = std::move(bit_handler); | ||||
| word_handler_ = std::move(word_handler); | word_handler_ = std::move(word_handler); | ||||
| word_pair_handler_ = std::move(word_pair_handler); | |||||
| } | } | ||||
| void PlcRegisterRepository::updateBit(int address, bool value) | void PlcRegisterRepository::updateBit(int address, bool value) | ||||
| @@ -17,11 +17,17 @@ public: | |||||
| WordReadResult readWord(const RegisterAddress &address) const override; | WordReadResult readWord(const RegisterAddress &address) const override; | ||||
| RegisterWriteResult writeWord( | RegisterWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value) override; | 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( | void setWriteHandlers( | ||||
| std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler, | 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 updateBit(int address, bool value); | ||||
| void updateWord(int address, std::int16_t value); | void updateWord(int address, std::int16_t value); | ||||
| @@ -40,4 +46,6 @@ private: | |||||
| std::array<bool, kRegisterCount> valid_words_{}; // 对应 D 地址是否读到过有效值 | std::array<bool, kRegisterCount> valid_words_{}; // 对应 D 地址是否读到过有效值 | ||||
| std::function<RegisterWriteResult(const RegisterAddress &, bool)> bit_handler_; // M 区异步写入回调 | 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 &, std::int16_t)> word_handler_; // D 区异步写入回调 | ||||
| std::function<RegisterWriteResult( | |||||
| const RegisterAddress &, const std::array<std::int16_t, 2> &)> word_pair_handler_; | |||||
| }; | }; | ||||
| @@ -134,6 +134,7 @@ bool HmiEditorService::controlsEqual( | |||||
| && left.bounds.height == right.bounds.height | && left.bounds.height == right.bounds.height | ||||
| && left.text == right.text | && left.text == right.text | ||||
| && left.binding == right.binding | && left.binding == right.binding | ||||
| && left.dataType == right.dataType | |||||
| && left.properties == right.properties | && left.properties == right.properties | ||||
| && left.buttonOperation == right.buttonOperation | && left.buttonOperation == right.buttonOperation | ||||
| && page_jump_equal; | && page_jump_equal; | ||||
| @@ -2,11 +2,14 @@ | |||||
| #include "domain/hmi_control_registry.h" | #include "domain/hmi_control_registry.h" | ||||
| #include <cmath> | |||||
| #include <limits> | |||||
| namespace { | namespace { | ||||
| HmiRuntimeReadResult readFailure(HmiRuntimeError error) | HmiRuntimeReadResult readFailure(HmiRuntimeError error) | ||||
| { | { | ||||
| return {false, error, false, 0}; | |||||
| return {false, error, false, 0, 0.0f}; | |||||
| } | } | ||||
| HmiRuntimeWriteResult writeFailure(HmiRuntimeError error) | HmiRuntimeWriteResult writeFailure(HmiRuntimeError error) | ||||
| @@ -55,16 +58,30 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c | |||||
| { | { | ||||
| return readFailure(repositoryError(result.error)); | return readFailure(repositoryError(result.error)); | ||||
| } | } | ||||
| return {true, HmiRuntimeError::None, result.value, 0}; | |||||
| return {true, HmiRuntimeError::None, result.value, 0, 0.0f}; | |||||
| } | } | ||||
| case HmiRuntimeValueKind::Word: | 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); | const WordReadResult result = repository_.readWord(*control.binding); | ||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| return readFailure(repositoryError(result.error)); | return readFailure(repositoryError(result.error)); | ||||
| } | } | ||||
| return {true, HmiRuntimeError::None, false, result.value}; | |||||
| return {true, HmiRuntimeError::None, false, result.value, 0.0f}; | |||||
| } | } | ||||
| case HmiRuntimeValueKind::None: | case HmiRuntimeValueKind::None: | ||||
| default: | default: | ||||
| @@ -140,7 +157,7 @@ HmiRuntimeWriteResult HmiRuntimeService::operateButton( | |||||
| } | } | ||||
| HmiRuntimeWriteResult HmiRuntimeService::writeNumericInput( | HmiRuntimeWriteResult HmiRuntimeService::writeNumericInput( | ||||
| const HmiControl &control, std::int16_t value) | |||||
| const HmiControl &control, double value) | |||||
| { | { | ||||
| if (control.type != HmiControlType::NumericInput) | if (control.type != HmiControlType::NumericInput) | ||||
| { | { | ||||
| @@ -150,11 +167,40 @@ HmiRuntimeWriteResult HmiRuntimeService::writeNumericInput( | |||||
| { | { | ||||
| return writeFailure(HmiRuntimeError::MissingBinding); | return writeFailure(HmiRuntimeError::MissingBinding); | ||||
| } | } | ||||
| if (!control.binding->isValid()) | |||||
| if (!control.binding->isValid() || control.binding->area() != RegisterArea::D) | |||||
| { | { | ||||
| return writeFailure(HmiRuntimeError::InvalidBinding); | 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 | return result.succeeded | ||||
| ? HmiRuntimeWriteResult{true, HmiRuntimeError::None} | ? HmiRuntimeWriteResult{true, HmiRuntimeError::None} | ||||
| : writeFailure(repositoryError(result.error)); | : writeFailure(repositoryError(result.error)); | ||||
| @@ -35,6 +35,7 @@ struct HmiRuntimeReadResult | |||||
| HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类 | HmiRuntimeError error = HmiRuntimeError::None; // 失败时的 HMI 错误分类 | ||||
| bool bit_value = false; // 位控件的读回值;字控件读取成功时无效 | bool bit_value = false; // 位控件的读回值;字控件读取成功时无效 | ||||
| std::int16_t word_value = 0; // 字控件的读回值;位控件读取成功时无效 | std::int16_t word_value = 0; // 字控件的读回值;位控件读取成功时无效 | ||||
| float float_value = 0.0f; // Float32 数值控件的读回值 | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -98,7 +99,7 @@ public: | |||||
| * 该服务只负责提交写入请求,实际读回值由后续仓库轮询确认 | * 该服务只负责提交写入请求,实际读回值由后续仓库轮询确认 | ||||
| */ | */ | ||||
| HmiRuntimeWriteResult writeNumericInput( | HmiRuntimeWriteResult writeNumericInput( | ||||
| const HmiControl &control, std::int16_t value); | |||||
| const HmiControl &control, double value); | |||||
| /** | /** | ||||
| * @brief 读取指定 M 位 | * @brief 读取指定 M 位 | ||||
| @@ -152,6 +152,22 @@ public: | |||||
| virtual PlcCommunicationResult setPollAddresses( | virtual PlcCommunicationResult setPollAddresses( | ||||
| const std::vector<RegisterAddress> &addresses) = 0; | 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 不代表首读资格已经完成 | // 返回当前连接生命周期状态;Connected 不代表首读资格已经完成 | ||||
| virtual PlcConnectionState state() const = 0; | virtual PlcConnectionState state() const = 0; | ||||
| @@ -1,6 +1,8 @@ | |||||
| #include "register_monitor_service.h" | #include "register_monitor_service.h" | ||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <cmath> | |||||
| #include <optional> | |||||
| #include <utility> | #include <utility> | ||||
| namespace { | namespace { | ||||
| @@ -31,6 +33,12 @@ RegisterMonitorService::RegisterMonitorService(RegisterRepository &repository) | |||||
| RegisterMonitorResult RegisterMonitorService::addRange( | RegisterMonitorResult RegisterMonitorService::addRange( | ||||
| const std::string &start_address, int count) | 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); | const RegisterAddressParseResult parsed = parseRegisterAddress(start_address); | ||||
| if (!parsed.succeeded) | if (!parsed.succeeded) | ||||
| @@ -38,8 +46,15 @@ RegisterMonitorResult RegisterMonitorService::addRange( | |||||
| return {false, RegisterMonitorError::InvalidAddress, | return {false, RegisterMonitorError::InvalidAddress, | ||||
| parseErrorMessage(parsed.error), 0}; | 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 | if (count < 1 | ||||
| || parsed.address.index() > RegisterAddress::kMaximumIndex - (count - 1)) | |||||
| || parsed.address.index() > RegisterAddress::kMaximumIndex - step * count + 1) | |||||
| { | { | ||||
| return {false, RegisterMonitorError::RangeOverflow, | return {false, RegisterMonitorError::RangeOverflow, | ||||
| "连续监控范围不能超过地址 4000", 0}; | "连续监控范围不能超过地址 4000", 0}; | ||||
| @@ -49,10 +64,13 @@ RegisterMonitorResult RegisterMonitorService::addRange( | |||||
| pending.reserve(static_cast<std::size_t>(count)); | pending.reserve(static_cast<std::size_t>(count)); | ||||
| for (int offset = 0; offset < count; ++offset) | 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()) | if (pending.empty()) | ||||
| @@ -66,13 +84,32 @@ RegisterMonitorResult RegisterMonitorService::addRange( | |||||
| } | } | ||||
| for (const RegisterAddress &address : pending) | for (const RegisterAddress &address : pending) | ||||
| { | { | ||||
| model_.add(address); | |||||
| model_.add(MonitorPoint{address, data_type}); | |||||
| } | } | ||||
| if (addresses_changed_callback_) | if (addresses_changed_callback_) | ||||
| { | { | ||||
| 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( | RegisterMonitorResult RegisterMonitorService::remove( | ||||
| @@ -97,6 +134,28 @@ RegisterMonitorResult RegisterMonitorService::remove( | |||||
| return {true, RegisterMonitorError::None, {}, removed_count}; | 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() | RegisterMonitorResult RegisterMonitorService::clear() | ||||
| { | { | ||||
| if (model_.addresses().empty()) | 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 | const std::vector<RegisterAddress> &RegisterMonitorService::addresses() const | ||||
| { | { | ||||
| return model_.addresses(); | 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> RegisterMonitorService::values(bool communication_fault) const | ||||
| { | { | ||||
| std::vector<MonitorValue> result; | 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; | MonitorValue value; | ||||
| value.address = address; | value.address = address; | ||||
| value.dataType = point.dataType; | |||||
| value.endAddress = address.index() | |||||
| + registerDataTypeWordCount(point.dataType) - 1; | |||||
| if (address.area() == RegisterArea::M) | if (address.area() == RegisterArea::M) | ||||
| { | { | ||||
| const BitReadResult read = repository_.readBit(address); | const BitReadResult read = repository_.readBit(address); | ||||
| @@ -170,18 +252,65 @@ std::vector<MonitorValue> RegisterMonitorService::values(bool communication_faul | |||||
| } | } | ||||
| else | 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 | ? (communication_fault ? MonitorValueState::CommunicationFault | ||||
| : MonitorValueState::Valid) | : MonitorValueState::Valid) | ||||
| : MonitorValueState::Unavailable; | : MonitorValueState::Unavailable; | ||||
| } | |||||
| } | } | ||||
| result.push_back(value); | result.push_back(value); | ||||
| } | } | ||||
| return result; | 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) | void RegisterMonitorService::setAddressesChangedCallback(std::function<void()> callback) | ||||
| { | { | ||||
| addresses_changed_callback_ = std::move(callback); | addresses_changed_callback_ = std::move(callback); | ||||
| @@ -56,12 +56,15 @@ public: | |||||
| * @return 去重后实际新增数量;格式、范围、容量或无变化时返回失败 | * @return 去重后实际新增数量;格式、范围、容量或无变化时返回失败 | ||||
| */ | */ | ||||
| RegisterMonitorResult addRange(const std::string &start_address, int count); | RegisterMonitorResult addRange(const std::string &start_address, int count); | ||||
| RegisterMonitorResult addRange( | |||||
| const std::string &start_address, int count, RegisterDataType data_type); | |||||
| /** | /** | ||||
| * @brief 删除一批监控地址 | * @brief 删除一批监控地址 | ||||
| * @param addresses 待删除地址;不存在的地址会被忽略 | * @param addresses 待删除地址;不存在的地址会被忽略 | ||||
| * @return 实际删除数量,并在发生变化时通知轮询集合更新 | * @return 实际删除数量,并在发生变化时通知轮询集合更新 | ||||
| */ | */ | ||||
| RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses); | RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses); | ||||
| RegisterMonitorResult remove(const std::vector<MonitorPoint> &points); | |||||
| /** | /** | ||||
| * @brief 清空当前会话的监控地址列表 | * @brief 清空当前会话的监控地址列表 | ||||
| * @return 实际清除数量;列表本来为空时返回 NoChange | * @return 实际清除数量;列表本来为空时返回 NoChange | ||||
| @@ -82,11 +85,16 @@ public: | |||||
| */ | */ | ||||
| RegisterMonitorWriteResult writeWord( | RegisterMonitorWriteResult writeWord( | ||||
| const RegisterAddress &address, std::int16_t value); | const RegisterAddress &address, std::int16_t value); | ||||
| RegisterMonitorWriteResult writeFloat( | |||||
| const RegisterAddress &address, float value); | |||||
| /** | /** | ||||
| * @brief 返回当前会话的监控地址列表 | * @brief 返回当前会话的监控地址列表 | ||||
| * @return 按服务内部顺序保存的只读地址列表 | * @return 按服务内部顺序保存的只读地址列表 | ||||
| */ | */ | ||||
| const std::vector<RegisterAddress> &addresses() const; | const std::vector<RegisterAddress> &addresses() const; | ||||
| const std::vector<MonitorPoint> &points() const; | |||||
| std::vector<RegisterAddress> pollAddresses() const; | |||||
| std::vector<RegisterAddress> float32Starts() const; | |||||
| /** | /** | ||||
| * @brief 读取监控列表的当前值并生成 UI 展示模型 | * @brief 读取监控列表的当前值并生成 UI 展示模型 | ||||
| * @param communication_fault 通信处于故障态时,将成功读回值标记为通信故障 | * @param communication_fault 通信处于故障态时,将成功读回值标记为通信故障 | ||||
| @@ -253,8 +253,16 @@ PlcCommunicationResult RuntimeModeService::connectPlc( | |||||
| void RuntimeModeService::setMonitorAddresses( | void RuntimeModeService::setMonitorAddresses( | ||||
| const std::vector<RegisterAddress> &addresses) | 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_addresses_ = addresses; | ||||
| monitor_float32_starts_ = float32_starts; | |||||
| refreshPlcPollAddresses(); | refreshPlcPollAddresses(); | ||||
| } | } | ||||
| @@ -266,6 +274,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() | |||||
| } | } | ||||
| // 轮询集合来自自由监控、HMI、报警和梯形图引用的并集 | // 轮询集合来自自由监控、HMI、报警和梯形图引用的并集 | ||||
| std::vector<RegisterAddress> addresses = monitor_addresses_; | std::vector<RegisterAddress> addresses = monitor_addresses_; | ||||
| std::vector<RegisterAddress> float32_starts = monitor_float32_starts_; | |||||
| const Project &project = project_service_.project(); | const Project &project = project_service_.project(); | ||||
| for (const HmiPage &page : project.hmiPages) | for (const HmiPage &page : project.hmiPages) | ||||
| { | { | ||||
| @@ -274,6 +283,15 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() | |||||
| if (control.binding.has_value()) | if (control.binding.has_value()) | ||||
| { | { | ||||
| addresses.push_back(*control.binding); | 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 {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() | void RuntimeModeService::disconnectPlc() | ||||
| @@ -109,6 +109,9 @@ public: | |||||
| * @param addresses 自由监控当前需要读取的 M/D 地址 | * @param addresses 自由监控当前需要读取的 M/D 地址 | ||||
| */ | */ | ||||
| void setMonitorAddresses(const std::vector<RegisterAddress> &addresses); | void setMonitorAddresses(const std::vector<RegisterAddress> &addresses); | ||||
| void setMonitorAddresses( | |||||
| const std::vector<RegisterAddress> &addresses, | |||||
| const std::vector<RegisterAddress> &float32_starts); | |||||
| /** | /** | ||||
| * @brief 汇总工程和自由监控引用并刷新 PLC 轮询地址 | * @brief 汇总工程和自由监控引用并刷新 PLC 轮询地址 | ||||
| @@ -146,4 +149,5 @@ private: | |||||
| RegisterRepository *plc_repository_ = nullptr; // 真机模式的 PLC 缓存仓库 | RegisterRepository *plc_repository_ = nullptr; // 真机模式的 PLC 缓存仓库 | ||||
| std::function<void()> plc_status_changed_callback_; // PLC 状态变化通知 | std::function<void()> plc_status_changed_callback_; // PLC 状态变化通知 | ||||
| std::vector<RegisterAddress> monitor_addresses_; // 自由监控额外引用的地址 | std::vector<RegisterAddress> monitor_addresses_; // 自由监控额外引用的地址 | ||||
| std::vector<RegisterAddress> monitor_float32_starts_; // 自由监控中的 Float32 起始地址 | |||||
| }; | }; | ||||
| @@ -13,6 +13,7 @@ | |||||
| #include <QTableWidgetItem> | #include <QTableWidgetItem> | ||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <cmath> | |||||
| #include <set> | #include <set> | ||||
| namespace { | namespace { | ||||
| @@ -45,6 +46,8 @@ FreeMonitorWidget::FreeMonitorWidget( | |||||
| ui_->monitorTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); | ui_->monitorTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); | ||||
| ui_->monitorTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); | ui_->monitorTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); | ||||
| ui_->monitorTable->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch); | 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); | ui_->monitorTable->verticalHeader()->setVisible(false); | ||||
| connect(ui_->addButton, &QPushButton::clicked, this, &FreeMonitorWidget::addAddresses); | connect(ui_->addButton, &QPushButton::clicked, this, &FreeMonitorWidget::addAddresses); | ||||
| connect(ui_->addressEdit, &QLineEdit::returnPressed, | connect(ui_->addressEdit, &QLineEdit::returnPressed, | ||||
| @@ -103,10 +106,20 @@ void FreeMonitorWidget::refreshValues( | |||||
| ? tr("等待读取") : tr("不可用")); | ? tr("等待读取") : tr("不可用")); | ||||
| continue; | 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 | state_item->setText(value.state == MonitorValueState::CommunicationFault | ||||
| ? tr("通信故障,最后有效值") : tr("有效")); | ? tr("通信故障,最后有效值") : tr("有效")); | ||||
| } | } | ||||
| @@ -122,9 +135,17 @@ void FreeMonitorWidget::reloadAddresses() | |||||
| const RegisterAddress &address = addresses[index]; | const RegisterAddress &address = addresses[index]; | ||||
| const int row = static_cast<int>(index); | const int row = static_cast<int>(index); | ||||
| ui_->monitorTable->setItem(row, 0, new QTableWidgetItem(addressText(address))); | ui_->monitorTable->setItem(row, 0, new QTableWidgetItem(addressText(address))); | ||||
| const MonitorPoint &point = service_.points().at(index); | |||||
| ui_->monitorTable->setItem( | ui_->monitorTable->setItem( | ||||
| row, 1, new QTableWidgetItem(address.area() == RegisterArea::M | 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("--"))); | ui_->monitorTable->setItem(row, 2, new QTableWidgetItem(QStringLiteral("--"))); | ||||
| if (address.area() == RegisterArea::M) | if (address.area() == RegisterArea::M) | ||||
| { | { | ||||
| @@ -136,7 +157,14 @@ void FreeMonitorWidget::reloadAddresses() | |||||
| else | else | ||||
| { | { | ||||
| auto *target = new QLineEdit(ui_->monitorTable); | 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->setText(QStringLiteral("0")); | ||||
| target->setAlignment(Qt::AlignRight | Qt::AlignVCenter); | target->setAlignment(Qt::AlignRight | Qt::AlignVCenter); | ||||
| ui_->monitorTable->setCellWidget(row, 3, target); | ui_->monitorTable->setCellWidget(row, 3, target); | ||||
| @@ -162,7 +190,8 @@ void FreeMonitorWidget::writeRow(int row) | |||||
| return; | 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; | RegisterMonitorWriteResult result; | ||||
| if (address.area() == RegisterArea::M) | if (address.area() == RegisterArea::M) | ||||
| { | { | ||||
| @@ -181,14 +210,20 @@ void FreeMonitorWidget::writeRow(int row) | |||||
| return; | return; | ||||
| } | } | ||||
| bool converted = false; | 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("目标值无效")); | 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; | 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); | QTableWidgetItem *state_item = ui_->monitorTable->item(row, 5); | ||||
| @@ -225,13 +260,20 @@ void FreeMonitorWidget::addAddresses() | |||||
| const QByteArray address = ui_->addressEdit->text().toUtf8(); | const QByteArray address = ui_->addressEdit->text().toUtf8(); | ||||
| const RegisterMonitorResult result = service_.addRange( | const RegisterMonitorResult result = service_.addRange( | ||||
| std::string(address.constData(), static_cast<std::size_t>(address.size())), | 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) | if (result.succeeded) | ||||
| { | { | ||||
| reloadAddresses(); | reloadAddresses(); | ||||
| ui_->addressEdit->selectAll(); | ui_->addressEdit->selectAll(); | ||||
| emit monitorAddressesChanged(); | 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; | return; | ||||
| } | } | ||||
| handleResult(tr("添加监控地址"), false, result.message); | handleResult(tr("添加监控地址"), false, result.message); | ||||
| @@ -244,16 +286,16 @@ void FreeMonitorWidget::removeSelectedAddresses() | |||||
| { | { | ||||
| rows.insert(index.row()); | 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) | 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) | if (result.succeeded) | ||||
| { | { | ||||
| reloadAddresses(); | reloadAddresses(); | ||||
| @@ -15,13 +15,15 @@ | |||||
| <layout class="QGridLayout" name="commandLayout"> | <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="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="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="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="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> | </layout> | ||||
| </item> | </item> | ||||
| <item> | <item> | ||||
| @@ -417,10 +417,12 @@ public: | |||||
| } | } | ||||
| // 缓存运行服务读出的值并触发 Qt 重绘 | // 缓存运行服务读出的值并触发 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; | bit_value_ = bit_value; | ||||
| word_value_ = word_value; | word_value_ = word_value; | ||||
| float_value_ = float_value; | |||||
| has_runtime_value_ = available; | has_runtime_value_ = available; | ||||
| update(); | update(); | ||||
| } | } | ||||
| @@ -795,7 +797,10 @@ private: | |||||
| if (control_.type == HmiControlType::NumericDisplay | if (control_.type == HmiControlType::NumericDisplay | ||||
| || control_.type == HmiControlType::NumericInput) | || 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; | return text; | ||||
| } | } | ||||
| @@ -825,6 +830,7 @@ private: | |||||
| bool bit_value_ = false; | bool bit_value_ = false; | ||||
| // 数值控件读取到的 D 字值 | // 数值控件读取到的 D 字值 | ||||
| std::int16_t word_value_ = 0; | std::int16_t word_value_ = 0; | ||||
| float float_value_ = 0.0f; | |||||
| // 标记当前缓存值是否来自一次成功的运行时读取 | // 标记当前缓存值是否来自一次成功的运行时读取 | ||||
| bool has_runtime_value_ = false; | bool has_runtime_value_ = false; | ||||
| }; | }; | ||||
| @@ -887,7 +893,7 @@ void HmiEditorWidget::setRuntimeActive(bool active) | |||||
| HmiGraphicsItem *control_item = asHmiItem(item); | HmiGraphicsItem *control_item = asHmiItem(item); | ||||
| if (control_item != nullptr) | if (control_item != nullptr) | ||||
| { | { | ||||
| control_item->setRuntimeValue(false, 0, false); | |||||
| control_item->setRuntimeValue(false, 0, 0.0f, false); | |||||
| control_item->setAlarmRecords({}); | control_item->setAlarmRecords({}); | ||||
| } | } | ||||
| } | } | ||||
| @@ -1037,7 +1043,8 @@ void HmiEditorWidget::refreshRuntimeValues() | |||||
| } | } | ||||
| // 运行值通过服务读取,图元不直接接触寄存器仓库 | // 运行值通过服务读取,图元不直接接触寄存器仓库 | ||||
| const HmiRuntimeReadResult value = runtime_service_.readControl(*control); | 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; | bool accepted = false; | ||||
| const HmiRuntimeReadResult current = runtime_service_.readControl(*control); | 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) | if (!accepted) | ||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( | const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( | ||||
| *control, static_cast<std::int16_t>(value)); | |||||
| *control, value); | |||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| emit editorError(tr("寄存器操作失败")); | emit editorError(tr("寄存器操作失败")); | ||||
| @@ -738,7 +738,18 @@ | |||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| </item> | </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"> | <widget class="QPushButton" name="applyPropertiesButton"> | ||||
| <property name="text"> | <property name="text"> | ||||
| <string>应用属性</string> | <string>应用属性</string> | ||||
| @@ -108,6 +108,10 @@ void PropertyPanelController::configure() | |||||
| 2, static_cast<int>(HmiButtonOperation::Toggle)); | 2, static_cast<int>(HmiButtonOperation::Toggle)); | ||||
| ui_.buttonOperationComboBox->setItemData( | ui_.buttonOperationComboBox->setItemData( | ||||
| 3, static_cast<int>(HmiButtonOperation::MomentaryOn)); | 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) | for (int index = 0; index < ui_.logicComparisonComboBox->count(); ++index) | ||||
| { | { | ||||
| ui_.logicComparisonComboBox->setItemData(index, 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_.bindingIndexSpinBox), | ||||
| static_cast<QWidget *>(ui_.targetPageComboBox), | static_cast<QWidget *>(ui_.targetPageComboBox), | ||||
| static_cast<QWidget *>(ui_.buttonOperationComboBox), | static_cast<QWidget *>(ui_.buttonOperationComboBox), | ||||
| static_cast<QWidget *>(ui_.dataTypeComboBox), | |||||
| static_cast<QWidget *>(ui_.textColorEdit), | static_cast<QWidget *>(ui_.textColorEdit), | ||||
| static_cast<QWidget *>(ui_.textColorButton), | static_cast<QWidget *>(ui_.textColorButton), | ||||
| static_cast<QWidget *>(ui_.fontSizeSpinBox), | static_cast<QWidget *>(ui_.fontSizeSpinBox), | ||||
| @@ -217,6 +222,8 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||||
| ui_.targetPageComboBox->setVisible(false); | ui_.targetPageComboBox->setVisible(false); | ||||
| ui_.buttonOperationLabel->setVisible(false); | ui_.buttonOperationLabel->setVisible(false); | ||||
| ui_.buttonOperationComboBox->setVisible(false); | ui_.buttonOperationComboBox->setVisible(false); | ||||
| ui_.dataTypeLabel->setVisible(false); | |||||
| ui_.dataTypeComboBox->setVisible(false); | |||||
| ui_.textColorEdit->clear(); | ui_.textColorEdit->clear(); | ||||
| ui_.textColorButton->setStyleSheet(QString{}); | ui_.textColorButton->setStyleSheet(QString{}); | ||||
| ui_.fontSizeSpinBox->setValue( | ui_.fontSizeSpinBox->setValue( | ||||
| @@ -256,6 +263,14 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||||
| ui_.buttonOperationComboBox->findData( | ui_.buttonOperationComboBox->findData( | ||||
| static_cast<int>(control->buttonOperation))); | 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; | const bool is_page_jump = control->type == HmiControlType::PageJump; | ||||
| ui_.targetPageLabel->setVisible(is_page_jump); | ui_.targetPageLabel->setVisible(is_page_jump); | ||||
| ui_.targetPageComboBox->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.y = ui_.controlYSpinBox->value(); | ||||
| control.bounds.width = ui_.controlWidthSpinBox->value(); | control.bounds.width = ui_.controlWidthSpinBox->value(); | ||||
| control.bounds.height = ui_.controlHeightSpinBox->value(); | control.bounds.height = ui_.controlHeightSpinBox->value(); | ||||
| control.dataType = static_cast<RegisterDataType>( | |||||
| ui_.dataTypeComboBox->currentData().toInt()); | |||||
| const HmiControlDescriptor *descriptor = | const HmiControlDescriptor *descriptor = | ||||
| findHmiControlDescriptor(control.type); | findHmiControlDescriptor(control.type); | ||||
| if (descriptor == nullptr | if (descriptor == nullptr | ||||
| @@ -103,7 +103,8 @@ void RuntimePanelController::configure() | |||||
| &parent_, [this] | &parent_, [this] | ||||
| { | { | ||||
| runtime_mode_service_.setMonitorAddresses( | runtime_mode_service_.setMonitorAddresses( | ||||
| register_monitor_service_.addresses()); | |||||
| register_monitor_service_.pollAddresses(), | |||||
| register_monitor_service_.float32Starts()); | |||||
| runtime_monitor_widget_->refreshValues( | runtime_monitor_widget_->refreshValues( | ||||
| runtime_mode_service_.mode(), | runtime_mode_service_.mode(), | ||||
| runtime_mode_service_.plcConnectionState()); | runtime_mode_service_.plcConnectionState()); | ||||
| @@ -10,10 +10,12 @@ | |||||
| #include "support/test_support.h" | #include "support/test_support.h" | ||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <cmath> | |||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <exception> | #include <exception> | ||||
| #include <functional> | #include <functional> | ||||
| #include <iostream> | #include <iostream> | ||||
| #include <limits> | |||||
| #include <set> | #include <set> | ||||
| #include <stdexcept> | #include <stdexcept> | ||||
| #include <string> | #include <string> | ||||
| @@ -101,6 +103,37 @@ void testRegisterRepositorySeparatesAreas() | |||||
| "M address must not be read as a word"); | "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() | void testHmiControlRegistryCompleteness() | ||||
| { | { | ||||
| std::set<const HmiControlDescriptor *> descriptors; | std::set<const HmiControlDescriptor *> descriptors; | ||||
| @@ -189,6 +222,55 @@ void testHmiControlRegistryCompleteness() | |||||
| Project makeValidProject(); | 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() | void testHmiAppearancePropertyBoundaries() | ||||
| { | { | ||||
| // 外观属性必须在领域层拒绝格式错误,但不能影响未知扩展属性 | // 外观属性必须在领域层拒绝格式错误,但不能影响未知扩展属性 | ||||
| @@ -860,7 +942,9 @@ int main() | |||||
| testRegisterAddressBoundaries(); | testRegisterAddressBoundaries(); | ||||
| testRegisterAddressParsing(); | testRegisterAddressParsing(); | ||||
| testRegisterRepositorySeparatesAreas(); | testRegisterRepositorySeparatesAreas(); | ||||
| testFloat32CodecAndPairAccess(); | |||||
| testHmiControlRegistryCompleteness(); | testHmiControlRegistryCompleteness(); | ||||
| testFloat32HmiBoundaries(); | |||||
| testHmiAppearancePropertyBoundaries(); | testHmiAppearancePropertyBoundaries(); | ||||
| testLogicNodeConfigurationBoundaries(); | testLogicNodeConfigurationBoundaries(); | ||||
| testEdgeAndCommentBoundaries(); | testEdgeAndCommentBoundaries(); | ||||
| @@ -10,6 +10,7 @@ | |||||
| #include <exception> | #include <exception> | ||||
| #include <iostream> | #include <iostream> | ||||
| #include <limits> | |||||
| #include <stdexcept> | #include <stdexcept> | ||||
| #include <string> | #include <string> | ||||
| @@ -132,6 +133,18 @@ void testRuntimeUsesRegisterRepository() | |||||
| require(numeric_value.succeeded && numeric_value.word_value == -18, | require(numeric_value.succeeded && numeric_value.word_value == -18, | ||||
| "numeric display must read D values through the repository"); | "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() | void testHistoryAndAtomicBatchDelete() | ||||
| @@ -2,10 +2,12 @@ | |||||
| DOMAIN_REGISTER_SOURCES = \ | DOMAIN_REGISTER_SOURCES = \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_value_type.cpp \ | |||||
| ../src/domain/virtual_register_repository.cpp | ../src/domain/virtual_register_repository.cpp | ||||
| DOMAIN_REGISTER_HEADERS = \ | DOMAIN_REGISTER_HEADERS = \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_value_type.h \ | |||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/virtual_register_repository.h | ../src/domain/virtual_register_repository.h | ||||
| @@ -429,6 +429,7 @@ void testExampleProjectRoundTrip() | |||||
| const QString unsupported_hmi_type_path = directory.filePath( | const QString unsupported_hmi_type_path = directory.filePath( | ||||
| "unsupported-hmi-type.json"); | "unsupported-hmi-type.json"); | ||||
| const QString missing_initial_path = directory.filePath("missing-initial.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_target_path = directory.filePath("missing-target.json"); | ||||
| const QString missing_alarms_path = directory.filePath("missing-alarms.json"); | const QString missing_alarms_path = directory.filePath("missing-alarms.json"); | ||||
| const QString missing_register_comments_path = directory.filePath( | const QString missing_register_comments_path = directory.filePath( | ||||
| @@ -450,6 +451,7 @@ void testExampleProjectRoundTrip() | |||||
| require(!saved_json.contains("\"dataPoints\""), | require(!saved_json.contains("\"dataPoints\""), | ||||
| "saved project must not contain the removed data point model"); | "saved project must not contain the removed data point model"); | ||||
| require(saved_json.contains("\"formatVersion\": \"1.0\"") | require(saved_json.contains("\"formatVersion\": \"1.0\"") | ||||
| && saved_json.contains("\"dataType\": \"int16\"") | |||||
| && saved_json.contains("\"buttonOperation\": \"setOn\"") | && saved_json.contains("\"buttonOperation\": \"setOn\"") | ||||
| && saved_json.contains("\"initialHmiPageId\": \"main-page\"") | && saved_json.contains("\"initialHmiPageId\": \"main-page\"") | ||||
| && saved_json.contains("\"targetPageId\": \"settings-page\"") | && saved_json.contains("\"targetPageId\": \"settings-page\"") | ||||
| @@ -604,6 +606,26 @@ void testExampleProjectRoundTrip() | |||||
| && missing_result.storageError == ProjectStorageError::MissingField, | && missing_result.storageError == ProjectStorageError::MissingField, | ||||
| "the 1.0 schema must require initialHmiPageId without migration defaults"); | "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(); | QJsonObject missing_alarms = QJsonDocument::fromJson(saved_json).object(); | ||||
| missing_alarms.remove(QStringLiteral("alarmDefinitions")); | missing_alarms.remove(QStringLiteral("alarmDefinitions")); | ||||
| writeText( | writeText( | ||||
| @@ -6,6 +6,8 @@ | |||||
| #include "support/test_support.h" | #include "support/test_support.h" | ||||
| #include <iostream> | #include <iostream> | ||||
| #include <cmath> | |||||
| #include <limits> | |||||
| #include <stdexcept> | #include <stdexcept> | ||||
| #include <string> | #include <string> | ||||
| @@ -101,6 +103,45 @@ void testRegisterWrites() | |||||
| "monitor word writes must preserve signed 16-bit boundary values"); | "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 | } // namespace | ||||
| int main() | int main() | ||||
| @@ -110,6 +151,7 @@ int main() | |||||
| testRangeManagement(); | testRangeManagement(); | ||||
| testSharedActiveRepositoryValues(); | testSharedActiveRepositoryValues(); | ||||
| testRegisterWrites(); | testRegisterWrites(); | ||||
| testFloat32Monitoring(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||