| @@ -19,11 +19,13 @@ SOURCES += \ | |||||
| src/ui/main_window.cpp \ | src/ui/main_window.cpp \ | ||||
| src/domain/register_address.cpp \ | src/domain/register_address.cpp \ | ||||
| src/domain/register_repository.cpp \ | src/domain/register_repository.cpp \ | ||||
| src/domain/data_point_model.cpp \ | |||||
| src/domain/hmi_model.cpp \ | src/domain/hmi_model.cpp \ | ||||
| src/domain/control_logic_model.cpp \ | src/domain/control_logic_model.cpp \ | ||||
| src/domain/project_model.cpp \ | src/domain/project_model.cpp \ | ||||
| src/domain/runtime_state.cpp \ | src/domain/runtime_state.cpp \ | ||||
| src/services/project_service.cpp \ | src/services/project_service.cpp \ | ||||
| src/services/data_point_service.cpp \ | |||||
| src/services/hmi_editor_service.cpp \ | src/services/hmi_editor_service.cpp \ | ||||
| src/services/logic_editor_service.cpp \ | src/services/logic_editor_service.cpp \ | ||||
| src/services/hmi_runtime_service.cpp \ | src/services/hmi_runtime_service.cpp \ | ||||
| @@ -38,12 +40,14 @@ HEADERS += \ | |||||
| src/ui/main_window.h \ | src/ui/main_window.h \ | ||||
| src/domain/register_address.h \ | src/domain/register_address.h \ | ||||
| src/domain/register_repository.h \ | src/domain/register_repository.h \ | ||||
| src/domain/data_point_model.h \ | |||||
| src/domain/hmi_model.h \ | src/domain/hmi_model.h \ | ||||
| src/domain/control_logic_model.h \ | src/domain/control_logic_model.h \ | ||||
| src/domain/project_model.h \ | src/domain/project_model.h \ | ||||
| src/domain/runtime_state.h \ | src/domain/runtime_state.h \ | ||||
| src/domain/project_storage.h \ | src/domain/project_storage.h \ | ||||
| src/services/project_service.h \ | src/services/project_service.h \ | ||||
| src/services/data_point_service.h \ | |||||
| src/services/hmi_editor_service.h \ | src/services/hmi_editor_service.h \ | ||||
| src/services/logic_editor_service.h \ | src/services/logic_editor_service.h \ | ||||
| src/services/hmi_runtime_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"); | setError(error, "control logic ids must be unique within a project"); | ||||
| return false; | 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) | for (const HmiPage &page : hmiPages) | ||||
| { | { | ||||
| // 工程聚合校验会向下委托页面和控件的完整规则 | // 工程聚合校验会向下委托页面和控件的完整规则 | ||||
| @@ -1,6 +1,7 @@ | |||||
| #pragma once | #pragma once | ||||
| #include "control_logic_model.h" | #include "control_logic_model.h" | ||||
| #include "data_point_model.h" | |||||
| #include "hmi_model.h" | #include "hmi_model.h" | ||||
| #include <string> | #include <string> | ||||
| @@ -26,6 +27,8 @@ struct Project | |||||
| std::vector<HmiPage> hmiPages; | std::vector<HmiPage> hmiPages; | ||||
| // 工程包含的控制逻辑集合 | // 工程包含的控制逻辑集合 | ||||
| std::vector<ControlLogic> controlLogics; | std::vector<ControlLogic> controlLogics; | ||||
| // M/D 地址的工程级名称和注释定义 | |||||
| std::vector<DataPoint> dataPoints; | |||||
| // 校验工程配置并通过 error 返回失败原因 | // 校验工程配置并通过 error 返回失败原因 | ||||
| bool validate(std::string *error = nullptr) const; | 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 \ | domain_tests.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/data_point_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -18,6 +19,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/data_point_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| @@ -10,6 +10,7 @@ SOURCES += \ | |||||
| hmi_editor_service_tests.cpp \ | hmi_editor_service_tests.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/data_point_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -20,6 +21,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/data_point_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| @@ -9,6 +9,7 @@ INCLUDEPATH += ../src | |||||
| SOURCES += \ | SOURCES += \ | ||||
| logic_editor_service_tests.cpp \ | logic_editor_service_tests.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/data_point_model.cpp \ | |||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| @@ -18,6 +19,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/data_point_model.h \ | |||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| @@ -12,6 +12,7 @@ SOURCES += \ | |||||
| project_management_tests.cpp \ | project_management_tests.cpp \ | ||||
| ../src/domain/register_address.cpp \ | ../src/domain/register_address.cpp \ | ||||
| ../src/domain/register_repository.cpp \ | ../src/domain/register_repository.cpp \ | ||||
| ../src/domain/data_point_model.cpp \ | |||||
| ../src/domain/hmi_model.cpp \ | ../src/domain/hmi_model.cpp \ | ||||
| ../src/domain/control_logic_model.cpp \ | ../src/domain/control_logic_model.cpp \ | ||||
| ../src/domain/project_model.cpp \ | ../src/domain/project_model.cpp \ | ||||
| @@ -22,6 +23,7 @@ SOURCES += \ | |||||
| HEADERS += \ | HEADERS += \ | ||||
| ../src/domain/register_address.h \ | ../src/domain/register_address.h \ | ||||
| ../src/domain/register_repository.h \ | ../src/domain/register_repository.h \ | ||||
| ../src/domain/data_point_model.h \ | |||||
| ../src/domain/hmi_model.h \ | ../src/domain/hmi_model.h \ | ||||
| ../src/domain/control_logic_model.h \ | ../src/domain/control_logic_model.h \ | ||||
| ../src/domain/project_model.h \ | ../src/domain/project_model.h \ | ||||