From c9ae5334404d21e619025104f03662ee59092f68 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Thu, 13 Aug 2026 09:46:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=82=B9=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/integrated_platform.pro | 4 + app/src/domain/data_point_model.cpp | 37 +++++ app/src/domain/data_point_model.h | 14 ++ app/src/domain/project_model.cpp | 23 +++ app/src/domain/project_model.h | 3 + app/src/services/data_point_service.cpp | 170 +++++++++++++++++++++++ app/src/services/data_point_service.h | 51 +++++++ app/tests/data_point_service_tests.cpp | 107 ++++++++++++++ app/tests/data_point_service_tests.pro | 29 ++++ app/tests/domain_tests.pro | 2 + app/tests/hmi_editor_service_tests.pro | 2 + app/tests/logic_editor_service_tests.pro | 2 + app/tests/project_management_tests.pro | 2 + 13 files changed, 446 insertions(+) create mode 100644 app/src/domain/data_point_model.cpp create mode 100644 app/src/domain/data_point_model.h create mode 100644 app/src/services/data_point_service.cpp create mode 100644 app/src/services/data_point_service.h create mode 100644 app/tests/data_point_service_tests.cpp create mode 100644 app/tests/data_point_service_tests.pro diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index bce6eaa..fb73612 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -19,11 +19,13 @@ SOURCES += \ src/ui/main_window.cpp \ src/domain/register_address.cpp \ src/domain/register_repository.cpp \ + src/domain/data_point_model.cpp \ src/domain/hmi_model.cpp \ src/domain/control_logic_model.cpp \ src/domain/project_model.cpp \ src/domain/runtime_state.cpp \ src/services/project_service.cpp \ + src/services/data_point_service.cpp \ src/services/hmi_editor_service.cpp \ src/services/logic_editor_service.cpp \ src/services/hmi_runtime_service.cpp \ @@ -38,12 +40,14 @@ HEADERS += \ src/ui/main_window.h \ src/domain/register_address.h \ src/domain/register_repository.h \ + src/domain/data_point_model.h \ src/domain/hmi_model.h \ src/domain/control_logic_model.h \ src/domain/project_model.h \ src/domain/runtime_state.h \ src/domain/project_storage.h \ src/services/project_service.h \ + src/services/data_point_service.h \ src/services/hmi_editor_service.h \ src/services/logic_editor_service.h \ src/services/hmi_runtime_service.h \ diff --git a/app/src/domain/data_point_model.cpp b/app/src/domain/data_point_model.cpp new file mode 100644 index 0000000..207efdd --- /dev/null +++ b/app/src/domain/data_point_model.cpp @@ -0,0 +1,37 @@ +#include "data_point_model.h" + +#include +#include + +namespace { + +bool isBlank(const std::string &value) +{ + return value.empty() + || std::all_of( + value.cbegin(), value.cend(), + [](unsigned char character) { return std::isspace(character) != 0; }); +} + +} // namespace + +bool DataPoint::validate(std::string *error) const +{ + if (!address.isValid()) + { + if (error != nullptr) + { + *error = "data point requires a valid M or D address"; + } + return false; + } + if (isBlank(name)) + { + if (error != nullptr) + { + *error = "data point name must not be empty"; + } + return false; + } + return true; +} diff --git a/app/src/domain/data_point_model.h b/app/src/domain/data_point_model.h new file mode 100644 index 0000000..db8f7ec --- /dev/null +++ b/app/src/domain/data_point_model.h @@ -0,0 +1,14 @@ +#pragma once + +#include "register_address.h" + +#include + +struct DataPoint +{ + RegisterAddress address; + std::string name; + std::string comment; + + bool validate(std::string *error = nullptr) const; +}; diff --git a/app/src/domain/project_model.cpp b/app/src/domain/project_model.cpp index 4d6d118..4a15e7b 100644 --- a/app/src/domain/project_model.cpp +++ b/app/src/domain/project_model.cpp @@ -52,6 +52,29 @@ bool Project::validate(std::string *error) const setError(error, "control logic ids must be unique within a project"); return false; } + for (auto current = dataPoints.cbegin(); current != dataPoints.cend(); ++current) + { + if (!current->validate(error)) + { + return false; + } + const auto duplicate = std::find_if( + current + 1, + dataPoints.cend(), + [¤t](const DataPoint &candidate) + { + return candidate.address == current->address + || candidate.name == current->name; + }); + if (duplicate != dataPoints.cend()) + { + if (error != nullptr) + { + *error = "data point addresses and names must be unique"; + } + return false; + } + } for (const HmiPage &page : hmiPages) { // 工程聚合校验会向下委托页面和控件的完整规则 diff --git a/app/src/domain/project_model.h b/app/src/domain/project_model.h index c6aeb39..2184712 100644 --- a/app/src/domain/project_model.h +++ b/app/src/domain/project_model.h @@ -1,6 +1,7 @@ #pragma once #include "control_logic_model.h" +#include "data_point_model.h" #include "hmi_model.h" #include @@ -26,6 +27,8 @@ struct Project std::vector hmiPages; // 工程包含的控制逻辑集合 std::vector controlLogics; + // M/D 地址的工程级名称和注释定义 + std::vector dataPoints; // 校验工程配置并通过 error 返回失败原因 bool validate(std::string *error = nullptr) const; diff --git a/app/src/services/data_point_service.cpp b/app/src/services/data_point_service.cpp new file mode 100644 index 0000000..7b11bba --- /dev/null +++ b/app/src/services/data_point_service.cpp @@ -0,0 +1,170 @@ +#include "data_point_service.h" + +#include "project_service.h" + +#include + +namespace { + +bool nodeUsesAddress(const LogicNode &node, const RegisterAddress &address) +{ + return std::visit( + [&address](const auto &config) { return config.address == address; }, + node.config); +} + +} // namespace + +DataPointService::DataPointService(ProjectService &project_service) + : project_service_(project_service) +{ +} + +const std::vector &DataPointService::dataPoints() const +{ + return project_service_.project().dataPoints; +} + +const DataPoint *DataPointService::find(const RegisterAddress &address) const +{ + const auto &points = dataPoints(); + const auto found = std::find_if( + points.cbegin(), points.cend(), + [&address](const DataPoint &point) { return point.address == address; }); + return found == points.cend() ? nullptr : &*found; +} + +DataPointResult DataPointService::add(const DataPoint &data_point) +{ + const DataPointResult validation = validateUnique(data_point, nullptr); + if (!validation.succeeded) + { + return validation; + } + project_service_.editProject().dataPoints.push_back(data_point); + return {true, DataPointError::None, {}}; +} + +DataPointResult DataPointService::update( + const RegisterAddress &old_address, const DataPoint &data_point) +{ + const auto existing = std::find_if( + dataPoints().cbegin(), dataPoints().cend(), + [&old_address](const DataPoint &candidate) + { + return candidate.address == old_address; + }); + if (existing == dataPoints().cend()) + { + return {false, DataPointError::NotFound, "data point was not found"}; + } + const DataPointResult validation = validateUnique(data_point, &old_address); + if (!validation.succeeded) + { + return validation; + } + if (data_point.address != old_address && !references(old_address).empty()) + { + return {false, DataPointError::AddressInUse, + "referenced data point address cannot be changed"}; + } + Project &project = project_service_.editProject(); + const auto point = std::find_if( + project.dataPoints.begin(), project.dataPoints.end(), + [&old_address](const DataPoint &candidate) + { + return candidate.address == old_address; + }); + *point = data_point; + return {true, DataPointError::None, {}}; +} + +DataPointResult DataPointService::remove(const RegisterAddress &address) +{ + if (find(address) == nullptr) + { + return {false, DataPointError::NotFound, "data point was not found"}; + } + if (!references(address).empty()) + { + return {false, DataPointError::AddressInUse, + "data point is still referenced by HMI or ladder logic"}; + } + Project &project = project_service_.editProject(); + project.dataPoints.erase( + std::remove_if( + project.dataPoints.begin(), project.dataPoints.end(), + [&address](const DataPoint &point) { return point.address == address; }), + project.dataPoints.end()); + return {true, DataPointError::None, {}}; +} + +std::vector DataPointService::references( + const RegisterAddress &address) const +{ + std::vector result; + const Project &project = project_service_.project(); + for (const HmiPage &page : project.hmiPages) + { + for (const HmiControl &control : page.controls) + { + if (control.binding.has_value() && *control.binding == address) + { + result.push_back({"HMI/" + page.name + '/' + control.id}); + } + } + } + for (const ControlLogic &logic : project.controlLogics) + { + for (const LadderRung &rung : logic.rungs) + { + if (rung.condition.has_value()) + { + std::vector nodes; + collectConditionNodes(*rung.condition, &nodes); + for (const LogicNode *node : nodes) + { + if (nodeUsesAddress(*node, address)) + { + result.push_back({"LAD/" + logic.name + '/' + rung.name + '/' + node->id}); + } + } + } + if (rung.output.has_value() && nodeUsesAddress(*rung.output, address)) + { + result.push_back({"LAD/" + logic.name + '/' + rung.name + '/' + + rung.output->id}); + } + } + } + return result; +} + +DataPointResult DataPointService::validateUnique( + const DataPoint &data_point, + const RegisterAddress *ignored_address) const +{ + std::string error; + if (!data_point.validate(&error)) + { + return {false, DataPointError::InvalidDataPoint, error}; + } + for (const DataPoint &existing : dataPoints()) + { + if (ignored_address != nullptr && existing.address == *ignored_address) + { + continue; + } + if (existing.address == data_point.address) + { + return {false, DataPointError::DuplicateAddress, + "data point address already exists"}; + } + if (existing.name == data_point.name) + { + return {false, DataPointError::DuplicateName, + "data point name already exists"}; + } + } + return {true, DataPointError::None, {}}; +} diff --git a/app/src/services/data_point_service.h b/app/src/services/data_point_service.h new file mode 100644 index 0000000..88fe087 --- /dev/null +++ b/app/src/services/data_point_service.h @@ -0,0 +1,51 @@ +#pragma once + +#include "domain/data_point_model.h" + +#include +#include + +class ProjectService; + +enum class DataPointError +{ + None, + InvalidDataPoint, + DuplicateAddress, + DuplicateName, + NotFound, + AddressInUse +}; + +struct DataPointResult +{ + bool succeeded = false; + DataPointError error = DataPointError::None; + std::string message; +}; + +struct DataPointReference +{ + std::string location; +}; + +class DataPointService +{ +public: + explicit DataPointService(ProjectService &project_service); + + const std::vector &dataPoints() const; + const DataPoint *find(const RegisterAddress &address) const; + DataPointResult add(const DataPoint &data_point); + DataPointResult update( + const RegisterAddress &old_address, const DataPoint &data_point); + DataPointResult remove(const RegisterAddress &address); + std::vector references(const RegisterAddress &address) const; + +private: + DataPointResult validateUnique( + const DataPoint &data_point, + const RegisterAddress *ignored_address) const; + + ProjectService &project_service_; +}; diff --git a/app/tests/data_point_service_tests.cpp b/app/tests/data_point_service_tests.cpp new file mode 100644 index 0000000..a154dd7 --- /dev/null +++ b/app/tests/data_point_service_tests.cpp @@ -0,0 +1,107 @@ +#include "domain/project_storage.h" +#include "services/data_point_service.h" +#include "services/project_service.h" + +#include +#include + +namespace { + +class TestProjectStorage final : public ProjectStorage +{ +public: + ProjectSaveResult save(const Project &, const std::string &) override + { + return {true, ProjectStorageError::None, {}}; + } + + ProjectLoadResult load(const std::string &) override + { + return {false, {}, ProjectStorageError::FileReadFailed, {}}; + } +}; + +void require(bool condition, const std::string &message) +{ + if (!condition) + { + throw std::runtime_error(message); + } +} + +void testUniqueDataPointsAndReferences() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + DataPointService service(project_service); + const RegisterAddress m0{RegisterArea::M, 0}; + const RegisterAddress m1{RegisterArea::M, 1}; + const RegisterAddress d0{RegisterArea::D, 0}; + + require(service.add({m0, "StartCommand", "启动命令"}).succeeded, + "a valid data point must be added"); + require(service.add({d0, "Temperature", "温度"}).succeeded, + "M and D data points must coexist"); + require(service.add({m0, "DuplicateAddress", {}}).error + == DataPointError::DuplicateAddress, + "duplicate data point addresses must be rejected"); + require(service.add({m1, "StartCommand", {}}).error + == DataPointError::DuplicateName, + "duplicate data point names must be rejected"); + + HmiControl button; + button.id = "start-button"; + button.type = HmiControlType::Button; + button.bounds = {0, 0, 100, 40}; + button.text = "启动"; + button.binding = m0; + HmiPage page; + page.id = "main-page"; + page.name = "主画面"; + page.controls.push_back(button); + project_service.editProject().hmiPages.push_back(page); + + LogicNode contact; + contact.id = "start-contact"; + contact.config = ContactNodeConfig{m0, ContactMode::NormallyOpen}; + LadderRung rung; + rung.id = "rung-1"; + rung.name = "网络 1"; + rung.condition = ConditionExpression::fromNode(contact); + ControlLogic logic; + logic.id = "logic-1"; + logic.name = "控制逻辑 1"; + logic.rungs.push_back(rung); + project_service.editProject().controlLogics.push_back(logic); + + const auto references = service.references(m0); + require(references.size() == 2U, + "data point references must include HMI and ladder locations"); + require(service.remove(m0).error == DataPointError::AddressInUse, + "referenced data points must not be deleted"); + require(service.update(m0, {m1, "StartCommand", "启动命令"}).error + == DataPointError::AddressInUse, + "referenced data point addresses must not change"); + require(service.update(m0, {m0, "Start", "启动命令"}).succeeded, + "referenced data point names and comments may change"); + require(service.remove(RegisterAddress{RegisterArea::D, 99}).error + == DataPointError::NotFound, + "removing an unknown data point must report not found"); +} + +} // namespace + +int main() +{ + try + { + testUniqueDataPointsAndReferences(); + } + catch (const std::exception &error) + { + std::cerr << "data point service tests failed: " << error.what() << '\n'; + return 1; + } + std::cout << "data point service tests passed\n"; + return 0; +} diff --git a/app/tests/data_point_service_tests.pro b/app/tests/data_point_service_tests.pro new file mode 100644 index 0000000..3713778 --- /dev/null +++ b/app/tests/data_point_service_tests.pro @@ -0,0 +1,29 @@ +TEMPLATE = app +TARGET = data_point_service_tests + +CONFIG += console c++17 testcase warn_on +CONFIG -= app_bundle qt + +INCLUDEPATH += ../src + +SOURCES += \ + data_point_service_tests.cpp \ + ../src/domain/register_address.cpp \ + ../src/domain/register_repository.cpp \ + ../src/domain/data_point_model.cpp \ + ../src/domain/hmi_model.cpp \ + ../src/domain/control_logic_model.cpp \ + ../src/domain/project_model.cpp \ + ../src/services/project_service.cpp \ + ../src/services/data_point_service.cpp + +HEADERS += \ + ../src/domain/register_address.h \ + ../src/domain/register_repository.h \ + ../src/domain/data_point_model.h \ + ../src/domain/hmi_model.h \ + ../src/domain/control_logic_model.h \ + ../src/domain/project_model.h \ + ../src/domain/project_storage.h \ + ../src/services/project_service.h \ + ../src/services/data_point_service.h diff --git a/app/tests/domain_tests.pro b/app/tests/domain_tests.pro index a7fca1b..b89b2df 100644 --- a/app/tests/domain_tests.pro +++ b/app/tests/domain_tests.pro @@ -10,6 +10,7 @@ SOURCES += \ domain_tests.cpp \ ../src/domain/register_address.cpp \ ../src/domain/register_repository.cpp \ + ../src/domain/data_point_model.cpp \ ../src/domain/hmi_model.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ @@ -18,6 +19,7 @@ SOURCES += \ HEADERS += \ ../src/domain/register_address.h \ ../src/domain/register_repository.h \ + ../src/domain/data_point_model.h \ ../src/domain/hmi_model.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ diff --git a/app/tests/hmi_editor_service_tests.pro b/app/tests/hmi_editor_service_tests.pro index 814ae7a..cab5690 100644 --- a/app/tests/hmi_editor_service_tests.pro +++ b/app/tests/hmi_editor_service_tests.pro @@ -10,6 +10,7 @@ SOURCES += \ hmi_editor_service_tests.cpp \ ../src/domain/register_address.cpp \ ../src/domain/register_repository.cpp \ + ../src/domain/data_point_model.cpp \ ../src/domain/hmi_model.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ @@ -20,6 +21,7 @@ SOURCES += \ HEADERS += \ ../src/domain/register_address.h \ ../src/domain/register_repository.h \ + ../src/domain/data_point_model.h \ ../src/domain/hmi_model.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ diff --git a/app/tests/logic_editor_service_tests.pro b/app/tests/logic_editor_service_tests.pro index a37ee79..cb617de 100644 --- a/app/tests/logic_editor_service_tests.pro +++ b/app/tests/logic_editor_service_tests.pro @@ -9,6 +9,7 @@ INCLUDEPATH += ../src SOURCES += \ logic_editor_service_tests.cpp \ ../src/domain/register_address.cpp \ + ../src/domain/data_point_model.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/domain/hmi_model.cpp \ @@ -18,6 +19,7 @@ SOURCES += \ HEADERS += \ ../src/domain/register_address.h \ + ../src/domain/data_point_model.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/hmi_model.h \ diff --git a/app/tests/project_management_tests.pro b/app/tests/project_management_tests.pro index dccd1c9..5225ce8 100644 --- a/app/tests/project_management_tests.pro +++ b/app/tests/project_management_tests.pro @@ -12,6 +12,7 @@ SOURCES += \ project_management_tests.cpp \ ../src/domain/register_address.cpp \ ../src/domain/register_repository.cpp \ + ../src/domain/data_point_model.cpp \ ../src/domain/hmi_model.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ @@ -22,6 +23,7 @@ SOURCES += \ HEADERS += \ ../src/domain/register_address.h \ ../src/domain/register_repository.h \ + ../src/domain/data_point_model.h \ ../src/domain/hmi_model.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \