| @@ -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 \ | |||
| @@ -0,0 +1,37 @@ | |||
| #include "data_point_model.h" | |||
| #include <algorithm> | |||
| #include <cctype> | |||
| 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; | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| #pragma once | |||
| #include "register_address.h" | |||
| #include <string> | |||
| struct DataPoint | |||
| { | |||
| RegisterAddress address; | |||
| std::string name; | |||
| std::string comment; | |||
| bool validate(std::string *error = nullptr) const; | |||
| }; | |||
| @@ -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) | |||
| { | |||
| // 工程聚合校验会向下委托页面和控件的完整规则 | |||
| @@ -1,6 +1,7 @@ | |||
| #pragma once | |||
| #include "control_logic_model.h" | |||
| #include "data_point_model.h" | |||
| #include "hmi_model.h" | |||
| #include <string> | |||
| @@ -26,6 +27,8 @@ struct Project | |||
| std::vector<HmiPage> hmiPages; | |||
| // 工程包含的控制逻辑集合 | |||
| std::vector<ControlLogic> controlLogics; | |||
| // M/D 地址的工程级名称和注释定义 | |||
| std::vector<DataPoint> dataPoints; | |||
| // 校验工程配置并通过 error 返回失败原因 | |||
| bool validate(std::string *error = nullptr) const; | |||
| @@ -0,0 +1,170 @@ | |||
| #include "data_point_service.h" | |||
| #include "project_service.h" | |||
| #include <algorithm> | |||
| 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<DataPoint> &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<DataPointReference> DataPointService::references( | |||
| const RegisterAddress &address) const | |||
| { | |||
| std::vector<DataPointReference> 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<const LogicNode *> 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, {}}; | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| #pragma once | |||
| #include "domain/data_point_model.h" | |||
| #include <string> | |||
| #include <vector> | |||
| 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<DataPoint> &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<DataPointReference> references(const RegisterAddress &address) const; | |||
| private: | |||
| DataPointResult validateUnique( | |||
| const DataPoint &data_point, | |||
| const RegisterAddress *ignored_address) const; | |||
| ProjectService &project_service_; | |||
| }; | |||
| @@ -0,0 +1,107 @@ | |||
| #include "domain/project_storage.h" | |||
| #include "services/data_point_service.h" | |||
| #include "services/project_service.h" | |||
| #include <iostream> | |||
| #include <stdexcept> | |||
| 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; | |||
| } | |||
| @@ -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 | |||
| @@ -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 \ | |||
| @@ -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 \ | |||
| @@ -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 \ | |||
| @@ -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 \ | |||