| @@ -21,6 +21,7 @@ SOURCES += \ | |||||
| src/domain/register_address.cpp \ | src/domain/register_address.cpp \ | ||||
| src/domain/register_repository.cpp \ | src/domain/register_repository.cpp \ | ||||
| src/domain/active_register_repository.cpp \ | src/domain/active_register_repository.cpp \ | ||||
| src/domain/register_monitor_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 \ | ||||
| @@ -32,6 +33,7 @@ SOURCES += \ | |||||
| src/services/software_logic_executor.cpp \ | src/services/software_logic_executor.cpp \ | ||||
| src/services/offline_simulation_service.cpp \ | src/services/offline_simulation_service.cpp \ | ||||
| src/services/runtime_mode_service.cpp \ | src/services/runtime_mode_service.cpp \ | ||||
| src/services/register_monitor_service.cpp \ | |||||
| src/infrastructure/plc_register_repository.cpp \ | src/infrastructure/plc_register_repository.cpp \ | ||||
| src/infrastructure/plc_communication_service.cpp \ | src/infrastructure/plc_communication_service.cpp \ | ||||
| src/infrastructure/json_project_storage.cpp \ | src/infrastructure/json_project_storage.cpp \ | ||||
| @@ -44,6 +46,7 @@ HEADERS += \ | |||||
| src/domain/register_address.h \ | src/domain/register_address.h \ | ||||
| src/domain/register_repository.h \ | src/domain/register_repository.h \ | ||||
| src/domain/active_register_repository.h \ | src/domain/active_register_repository.h \ | ||||
| src/domain/register_monitor_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 \ | ||||
| @@ -57,6 +60,7 @@ HEADERS += \ | |||||
| src/services/offline_simulation_service.h \ | src/services/offline_simulation_service.h \ | ||||
| src/services/runtime_mode_service.h \ | src/services/runtime_mode_service.h \ | ||||
| src/services/plc_communication_gateway.h \ | src/services/plc_communication_gateway.h \ | ||||
| src/services/register_monitor_service.h \ | |||||
| src/infrastructure/plc_register_repository.h \ | src/infrastructure/plc_register_repository.h \ | ||||
| src/infrastructure/plc_communication_service.h \ | src/infrastructure/plc_communication_service.h \ | ||||
| src/infrastructure/json_project_storage.h \ | src/infrastructure/json_project_storage.h \ | ||||
| @@ -1,5 +1,9 @@ | |||||
| #include "register_address.h" | #include "register_address.h" | ||||
| #include <algorithm> | |||||
| #include <cctype> | |||||
| #include <limits> | |||||
| namespace { | namespace { | ||||
| bool isSupportedArea(RegisterArea area) | bool isSupportedArea(RegisterArea area) | ||||
| @@ -10,6 +14,60 @@ bool isSupportedArea(RegisterArea area) | |||||
| } // namespace | } // namespace | ||||
| RegisterAddressParseResult parseRegisterAddress(const std::string &text) | |||||
| { | |||||
| const auto first = std::find_if_not( | |||||
| text.begin(), text.end(), [](unsigned char value) { return std::isspace(value); }); | |||||
| const auto last = std::find_if_not( | |||||
| text.rbegin(), text.rend(), [](unsigned char value) { return std::isspace(value); }) | |||||
| .base(); | |||||
| if (first >= last) | |||||
| { | |||||
| return {false, {RegisterArea::M, 0}, RegisterAddressParseError::Empty}; | |||||
| } | |||||
| const char area_character = static_cast<char>( | |||||
| std::toupper(static_cast<unsigned char>(*first))); | |||||
| RegisterArea area = RegisterArea::M; | |||||
| if (area_character == 'M') | |||||
| { | |||||
| area = RegisterArea::M; | |||||
| } | |||||
| else if (area_character == 'D') | |||||
| { | |||||
| area = RegisterArea::D; | |||||
| } | |||||
| else | |||||
| { | |||||
| return {false, {RegisterArea::M, 0}, RegisterAddressParseError::UnsupportedArea}; | |||||
| } | |||||
| if (first + 1 == last) | |||||
| { | |||||
| return {false, {area, 0}, RegisterAddressParseError::InvalidFormat}; | |||||
| } | |||||
| int index = 0; | |||||
| for (auto current = first + 1; current != last; ++current) | |||||
| { | |||||
| const unsigned char character = static_cast<unsigned char>(*current); | |||||
| if (!std::isdigit(character)) | |||||
| { | |||||
| return {false, {area, 0}, RegisterAddressParseError::InvalidFormat}; | |||||
| } | |||||
| const int digit = *current - '0'; | |||||
| if (index > (std::numeric_limits<int>::max() - digit) / 10) | |||||
| { | |||||
| return {false, {area, 0}, RegisterAddressParseError::OutOfRange}; | |||||
| } | |||||
| index = index * 10 + digit; | |||||
| } | |||||
| RegisterAddress address{area, index}; | |||||
| return address.isValid() | |||||
| ? RegisterAddressParseResult{true, address, RegisterAddressParseError::None} | |||||
| : RegisterAddressParseResult{false, address, RegisterAddressParseError::OutOfRange}; | |||||
| } | |||||
| RegisterAddress::RegisterAddress(RegisterArea area, int index) | RegisterAddress::RegisterAddress(RegisterArea area, int index) | ||||
| : area_(area), | : area_(area), | ||||
| index_(index) | index_(index) | ||||
| @@ -2,6 +2,15 @@ | |||||
| #include <string> | #include <string> | ||||
| enum class RegisterAddressParseError | |||||
| { | |||||
| None, | |||||
| Empty, | |||||
| UnsupportedArea, | |||||
| InvalidFormat, | |||||
| OutOfRange | |||||
| }; | |||||
| enum class RegisterArea | enum class RegisterArea | ||||
| { | { | ||||
| M, | M, | ||||
| @@ -28,3 +37,12 @@ private: | |||||
| RegisterArea area_; | RegisterArea area_; | ||||
| int index_; | int index_; | ||||
| }; | }; | ||||
| struct RegisterAddressParseResult | |||||
| { | |||||
| bool succeeded = false; | |||||
| RegisterAddress address{RegisterArea::M, 0}; | |||||
| RegisterAddressParseError error = RegisterAddressParseError::InvalidFormat; | |||||
| }; | |||||
| RegisterAddressParseResult parseRegisterAddress(const std::string &text); | |||||
| @@ -0,0 +1,40 @@ | |||||
| #include "register_monitor_model.h" | |||||
| #include <algorithm> | |||||
| const std::vector<RegisterAddress> &RegisterMonitorModel::addresses() const | |||||
| { | |||||
| return addresses_; | |||||
| } | |||||
| bool RegisterMonitorModel::contains(const RegisterAddress &address) const | |||||
| { | |||||
| return std::find(addresses_.begin(), addresses_.end(), address) != addresses_.end(); | |||||
| } | |||||
| bool RegisterMonitorModel::add(const RegisterAddress &address) | |||||
| { | |||||
| if (!address.isValid() || contains(address) | |||||
| || addresses_.size() >= kMaximumItemCount) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| addresses_.push_back(address); | |||||
| return true; | |||||
| } | |||||
| bool RegisterMonitorModel::remove(const RegisterAddress &address) | |||||
| { | |||||
| const auto position = std::find(addresses_.begin(), addresses_.end(), address); | |||||
| if (position == addresses_.end()) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| addresses_.erase(position); | |||||
| return true; | |||||
| } | |||||
| void RegisterMonitorModel::clear() | |||||
| { | |||||
| addresses_.clear(); | |||||
| } | |||||
| @@ -0,0 +1,37 @@ | |||||
| #pragma once | |||||
| #include "register_address.h" | |||||
| #include <cstddef> | |||||
| #include <cstdint> | |||||
| #include <vector> | |||||
| enum class MonitorValueState | |||||
| { | |||||
| Valid, | |||||
| Unavailable, | |||||
| CommunicationFault | |||||
| }; | |||||
| struct MonitorValue | |||||
| { | |||||
| RegisterAddress address{RegisterArea::M, 0}; | |||||
| MonitorValueState state = MonitorValueState::Unavailable; | |||||
| bool bitValue = false; | |||||
| std::int16_t wordValue = 0; | |||||
| }; | |||||
| class RegisterMonitorModel | |||||
| { | |||||
| public: | |||||
| static constexpr std::size_t kMaximumItemCount = 64U; | |||||
| const std::vector<RegisterAddress> &addresses() const; | |||||
| bool contains(const RegisterAddress &address) const; | |||||
| bool add(const RegisterAddress &address); | |||||
| bool remove(const RegisterAddress &address); | |||||
| void clear(); | |||||
| private: | |||||
| std::vector<RegisterAddress> addresses_; | |||||
| }; | |||||
| @@ -0,0 +1,154 @@ | |||||
| #include "register_monitor_service.h" | |||||
| #include <algorithm> | |||||
| #include <utility> | |||||
| namespace { | |||||
| std::string parseErrorMessage(RegisterAddressParseError error) | |||||
| { | |||||
| switch (error) | |||||
| { | |||||
| case RegisterAddressParseError::Empty: | |||||
| return "请输入 M0~M4000 或 D0~D4000 地址"; | |||||
| case RegisterAddressParseError::UnsupportedArea: | |||||
| return "自由监控只支持 M 区和 D 区地址"; | |||||
| case RegisterAddressParseError::OutOfRange: | |||||
| return "监控地址必须位于 M0~M4000 或 D0~D4000"; | |||||
| case RegisterAddressParseError::InvalidFormat: | |||||
| case RegisterAddressParseError::None: | |||||
| default: | |||||
| return "监控地址格式无效,请输入例如 M0 或 D100"; | |||||
| } | |||||
| } | |||||
| } // namespace | |||||
| RegisterMonitorService::RegisterMonitorService(RegisterRepository &repository) | |||||
| : repository_(repository) | |||||
| { | |||||
| } | |||||
| RegisterMonitorResult RegisterMonitorService::addRange( | |||||
| const std::string &start_address, int count) | |||||
| { | |||||
| const RegisterAddressParseResult parsed = parseRegisterAddress(start_address); | |||||
| if (!parsed.succeeded) | |||||
| { | |||||
| return {false, RegisterMonitorError::InvalidAddress, | |||||
| parseErrorMessage(parsed.error), 0}; | |||||
| } | |||||
| if (count < 1 | |||||
| || parsed.address.index() > RegisterAddress::kMaximumIndex - (count - 1)) | |||||
| { | |||||
| return {false, RegisterMonitorError::RangeOverflow, | |||||
| "连续监控范围不能超过地址 4000", 0}; | |||||
| } | |||||
| std::vector<RegisterAddress> pending; | |||||
| pending.reserve(static_cast<std::size_t>(count)); | |||||
| for (int offset = 0; offset < count; ++offset) | |||||
| { | |||||
| RegisterAddress address{parsed.address.area(), parsed.address.index() + offset}; | |||||
| if (!model_.contains(address)) | |||||
| { | |||||
| pending.push_back(address); | |||||
| } | |||||
| } | |||||
| if (pending.empty()) | |||||
| { | |||||
| return {false, RegisterMonitorError::NoChange, "所选地址已经在监控表中", 0}; | |||||
| } | |||||
| if (model_.addresses().size() + pending.size() > RegisterMonitorModel::kMaximumItemCount) | |||||
| { | |||||
| return {false, RegisterMonitorError::LimitExceeded, | |||||
| "自由监控最多允许 64 个地址", 0}; | |||||
| } | |||||
| for (const RegisterAddress &address : pending) | |||||
| { | |||||
| model_.add(address); | |||||
| } | |||||
| if (addresses_changed_callback_) | |||||
| { | |||||
| addresses_changed_callback_(); | |||||
| } | |||||
| return {true, RegisterMonitorError::None, {}, static_cast<int>(pending.size())}; | |||||
| } | |||||
| RegisterMonitorResult RegisterMonitorService::remove( | |||||
| const std::vector<RegisterAddress> &addresses) | |||||
| { | |||||
| int removed_count = 0; | |||||
| for (const RegisterAddress &address : addresses) | |||||
| { | |||||
| if (model_.remove(address)) | |||||
| { | |||||
| ++removed_count; | |||||
| } | |||||
| } | |||||
| if (removed_count == 0) | |||||
| { | |||||
| return {false, RegisterMonitorError::NoChange, "请先选择要删除的监控地址", 0}; | |||||
| } | |||||
| if (addresses_changed_callback_) | |||||
| { | |||||
| addresses_changed_callback_(); | |||||
| } | |||||
| return {true, RegisterMonitorError::None, {}, removed_count}; | |||||
| } | |||||
| RegisterMonitorResult RegisterMonitorService::clear() | |||||
| { | |||||
| if (model_.addresses().empty()) | |||||
| { | |||||
| return {false, RegisterMonitorError::NoChange, "监控表已经为空", 0}; | |||||
| } | |||||
| const int removed_count = static_cast<int>(model_.addresses().size()); | |||||
| model_.clear(); | |||||
| if (addresses_changed_callback_) | |||||
| { | |||||
| addresses_changed_callback_(); | |||||
| } | |||||
| return {true, RegisterMonitorError::None, {}, removed_count}; | |||||
| } | |||||
| const std::vector<RegisterAddress> &RegisterMonitorService::addresses() const | |||||
| { | |||||
| return model_.addresses(); | |||||
| } | |||||
| std::vector<MonitorValue> RegisterMonitorService::values(bool communication_fault) const | |||||
| { | |||||
| std::vector<MonitorValue> result; | |||||
| result.reserve(model_.addresses().size()); | |||||
| for (const RegisterAddress &address : model_.addresses()) | |||||
| { | |||||
| MonitorValue value; | |||||
| value.address = address; | |||||
| if (address.area() == RegisterArea::M) | |||||
| { | |||||
| const BitReadResult read = repository_.readBit(address); | |||||
| value.bitValue = read.value; | |||||
| value.state = read.succeeded | |||||
| ? (communication_fault ? MonitorValueState::CommunicationFault | |||||
| : MonitorValueState::Valid) | |||||
| : MonitorValueState::Unavailable; | |||||
| } | |||||
| else | |||||
| { | |||||
| const WordReadResult read = repository_.readWord(address); | |||||
| value.wordValue = read.value; | |||||
| value.state = read.succeeded | |||||
| ? (communication_fault ? MonitorValueState::CommunicationFault | |||||
| : MonitorValueState::Valid) | |||||
| : MonitorValueState::Unavailable; | |||||
| } | |||||
| result.push_back(value); | |||||
| } | |||||
| return result; | |||||
| } | |||||
| void RegisterMonitorService::setAddressesChangedCallback(std::function<void()> callback) | |||||
| { | |||||
| addresses_changed_callback_ = std::move(callback); | |||||
| } | |||||
| @@ -0,0 +1,43 @@ | |||||
| #pragma once | |||||
| #include "domain/register_monitor_model.h" | |||||
| #include "domain/register_repository.h" | |||||
| #include <functional> | |||||
| #include <string> | |||||
| #include <vector> | |||||
| enum class RegisterMonitorError | |||||
| { | |||||
| None, | |||||
| InvalidAddress, | |||||
| RangeOverflow, | |||||
| LimitExceeded, | |||||
| NoChange | |||||
| }; | |||||
| struct RegisterMonitorResult | |||||
| { | |||||
| bool succeeded = false; | |||||
| RegisterMonitorError error = RegisterMonitorError::None; | |||||
| std::string message; | |||||
| int affectedCount = 0; | |||||
| }; | |||||
| class RegisterMonitorService | |||||
| { | |||||
| public: | |||||
| explicit RegisterMonitorService(RegisterRepository &repository); | |||||
| RegisterMonitorResult addRange(const std::string &start_address, int count); | |||||
| RegisterMonitorResult remove(const std::vector<RegisterAddress> &addresses); | |||||
| RegisterMonitorResult clear(); | |||||
| const std::vector<RegisterAddress> &addresses() const; | |||||
| std::vector<MonitorValue> values(bool communication_fault) const; | |||||
| void setAddressesChangedCallback(std::function<void()> callback); | |||||
| private: | |||||
| RegisterRepository &repository_; | |||||
| RegisterMonitorModel model_; | |||||
| std::function<void()> addresses_changed_callback_; | |||||
| }; | |||||
| @@ -1 +0,0 @@ | |||||
| @@ -36,6 +36,28 @@ void testRegisterAddressBoundaries() | |||||
| "unknown register area must be rejected"); | "unknown register area must be rejected"); | ||||
| } | } | ||||
| void testRegisterAddressParsing() | |||||
| { | |||||
| const RegisterAddressParseResult m0 = parseRegisterAddress(" m0 "); | |||||
| require(m0.succeeded && m0.address == RegisterAddress{RegisterArea::M, 0}, | |||||
| "register parser must trim and normalize lowercase M addresses"); | |||||
| const RegisterAddressParseResult d4000 = parseRegisterAddress("D4000"); | |||||
| require(d4000.succeeded | |||||
| && d4000.address == RegisterAddress{RegisterArea::D, 4000}, | |||||
| "register parser must accept the maximum D address"); | |||||
| require(parseRegisterAddress("").error == RegisterAddressParseError::Empty, | |||||
| "register parser must distinguish empty input"); | |||||
| require(parseRegisterAddress("X0").error | |||||
| == RegisterAddressParseError::UnsupportedArea, | |||||
| "register parser must reject unsupported areas"); | |||||
| require(parseRegisterAddress("M1.0").error | |||||
| == RegisterAddressParseError::InvalidFormat, | |||||
| "register parser must reject non-decimal indices"); | |||||
| require(parseRegisterAddress("D4001").error | |||||
| == RegisterAddressParseError::OutOfRange, | |||||
| "register parser must reject addresses above the project range"); | |||||
| } | |||||
| void testRegisterRepositorySeparatesAreas() | void testRegisterRepositorySeparatesAreas() | ||||
| { | { | ||||
| // 验证离线仓库不会把 M 位和 D 字交叉解释 | // 验证离线仓库不会把 M 位和 D 字交叉解释 | ||||
| @@ -275,6 +297,7 @@ int main() | |||||
| { | { | ||||
| // 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程 | // 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程 | ||||
| testRegisterAddressBoundaries(); | testRegisterAddressBoundaries(); | ||||
| testRegisterAddressParsing(); | |||||
| testRegisterRepositorySeparatesAreas(); | testRegisterRepositorySeparatesAreas(); | ||||
| testLogicNodeConfigurationBoundaries(); | testLogicNodeConfigurationBoundaries(); | ||||
| testLadderLogicBoundaries(); | testLadderLogicBoundaries(); | ||||
| @@ -0,0 +1,95 @@ | |||||
| #include "domain/active_register_repository.h" | |||||
| #include "domain/register_monitor_model.h" | |||||
| #include "domain/register_repository.h" | |||||
| #include "services/register_monitor_service.h" | |||||
| #include <iostream> | |||||
| #include <stdexcept> | |||||
| #include <string> | |||||
| namespace { | |||||
| void require(bool condition, const std::string &message) | |||||
| { | |||||
| if (!condition) | |||||
| { | |||||
| throw std::runtime_error(message); | |||||
| } | |||||
| } | |||||
| void testRangeManagement() | |||||
| { | |||||
| VirtualRegisterRepository repository; | |||||
| RegisterMonitorService service(repository); | |||||
| int change_count = 0; | |||||
| service.setAddressesChangedCallback([&change_count] { ++change_count; }); | |||||
| const RegisterMonitorResult added = service.addRange(" d10 ", 3); | |||||
| require(added.succeeded && added.affectedCount == 3 | |||||
| && service.addresses().size() == 3U, | |||||
| "monitor service must expand a normalized continuous address range"); | |||||
| require(service.addresses().front() == RegisterAddress{RegisterArea::D, 10} | |||||
| && service.addresses().back() == RegisterAddress{RegisterArea::D, 12}, | |||||
| "monitor service must preserve expanded address order"); | |||||
| const RegisterMonitorResult duplicate = service.addRange("D11", 2); | |||||
| require(!duplicate.succeeded && duplicate.error == RegisterMonitorError::NoChange, | |||||
| "monitor service must reject a fully duplicated range"); | |||||
| require(!service.addRange("M4000", 2).succeeded, | |||||
| "monitor service must reject a range crossing address 4000"); | |||||
| require(change_count == 1, | |||||
| "failed additions must not report a monitor address change"); | |||||
| require(service.remove({RegisterAddress{RegisterArea::D, 11}}).succeeded | |||||
| && service.addresses().size() == 2U, | |||||
| "monitor service must remove selected addresses"); | |||||
| require(service.clear().succeeded && service.addresses().empty(), | |||||
| "monitor service must clear the current monitor table"); | |||||
| require(change_count == 3, | |||||
| "successful add, remove and clear operations must notify address changes"); | |||||
| } | |||||
| void testSharedActiveRepositoryValues() | |||||
| { | |||||
| VirtualRegisterRepository virtual_repository; | |||||
| VirtualRegisterRepository plc_repository; | |||||
| ActiveRegisterRepository active_repository(virtual_repository); | |||||
| RegisterMonitorService service(active_repository); | |||||
| require(service.addRange("M0", 1).succeeded | |||||
| && service.addRange("D0", 1).succeeded, | |||||
| "monitor service must accept one M and one D address"); | |||||
| virtual_repository.writeBit({RegisterArea::M, 0}, true); | |||||
| virtual_repository.writeWord({RegisterArea::D, 0}, -123); | |||||
| std::vector<MonitorValue> values = service.values(false); | |||||
| require(values.size() == 2U && values[0].bitValue && values[1].wordValue == -123, | |||||
| "monitor values must reflect the active virtual repository"); | |||||
| plc_repository.writeBit({RegisterArea::M, 0}, false); | |||||
| plc_repository.writeWord({RegisterArea::D, 0}, 456); | |||||
| active_repository.use(plc_repository); | |||||
| values = service.values(false); | |||||
| require(!values[0].bitValue && values[1].wordValue == 456, | |||||
| "monitor values must follow the active repository switch"); | |||||
| values = service.values(true); | |||||
| require(values[0].state == MonitorValueState::CommunicationFault | |||||
| && values[1].state == MonitorValueState::CommunicationFault, | |||||
| "valid cached values must be marked stale during a communication fault"); | |||||
| } | |||||
| } // namespace | |||||
| int main() | |||||
| { | |||||
| try | |||||
| { | |||||
| testRangeManagement(); | |||||
| testSharedActiveRepositoryValues(); | |||||
| } | |||||
| catch (const std::exception &error) | |||||
| { | |||||
| std::cerr << "register monitor service tests failed: " << error.what() << '\n'; | |||||
| return 1; | |||||
| } | |||||
| std::cout << "register monitor service tests passed\n"; | |||||
| return 0; | |||||
| } | |||||
| @@ -0,0 +1,24 @@ | |||||
| QT -= gui | |||||
| TEMPLATE = app | |||||
| TARGET = register_monitor_service_tests | |||||
| CONFIG += console c++17 warn_on | |||||
| CONFIG -= app_bundle | |||||
| INCLUDEPATH += ../src | |||||
| SOURCES += \ | |||||
| register_monitor_service_tests.cpp \ | |||||
| ../src/domain/register_address.cpp \ | |||||
| ../src/domain/register_repository.cpp \ | |||||
| ../src/domain/active_register_repository.cpp \ | |||||
| ../src/domain/register_monitor_model.cpp \ | |||||
| ../src/services/register_monitor_service.cpp | |||||
| HEADERS += \ | |||||
| ../src/domain/register_address.h \ | |||||
| ../src/domain/register_repository.h \ | |||||
| ../src/domain/active_register_repository.h \ | |||||
| ../src/domain/register_monitor_model.h \ | |||||
| ../src/services/register_monitor_service.h | |||||