| @@ -21,6 +21,7 @@ SOURCES += \ | |||
| src/domain/register_address.cpp \ | |||
| src/domain/register_repository.cpp \ | |||
| src/domain/active_register_repository.cpp \ | |||
| src/domain/register_monitor_model.cpp \ | |||
| src/domain/hmi_model.cpp \ | |||
| src/domain/control_logic_model.cpp \ | |||
| src/domain/project_model.cpp \ | |||
| @@ -32,6 +33,7 @@ SOURCES += \ | |||
| src/services/software_logic_executor.cpp \ | |||
| src/services/offline_simulation_service.cpp \ | |||
| src/services/runtime_mode_service.cpp \ | |||
| src/services/register_monitor_service.cpp \ | |||
| src/infrastructure/plc_register_repository.cpp \ | |||
| src/infrastructure/plc_communication_service.cpp \ | |||
| src/infrastructure/json_project_storage.cpp \ | |||
| @@ -44,6 +46,7 @@ HEADERS += \ | |||
| src/domain/register_address.h \ | |||
| src/domain/register_repository.h \ | |||
| src/domain/active_register_repository.h \ | |||
| src/domain/register_monitor_model.h \ | |||
| src/domain/hmi_model.h \ | |||
| src/domain/control_logic_model.h \ | |||
| src/domain/project_model.h \ | |||
| @@ -57,6 +60,7 @@ HEADERS += \ | |||
| src/services/offline_simulation_service.h \ | |||
| src/services/runtime_mode_service.h \ | |||
| src/services/plc_communication_gateway.h \ | |||
| src/services/register_monitor_service.h \ | |||
| src/infrastructure/plc_register_repository.h \ | |||
| src/infrastructure/plc_communication_service.h \ | |||
| src/infrastructure/json_project_storage.h \ | |||
| @@ -1,5 +1,9 @@ | |||
| #include "register_address.h" | |||
| #include <algorithm> | |||
| #include <cctype> | |||
| #include <limits> | |||
| namespace { | |||
| bool isSupportedArea(RegisterArea area) | |||
| @@ -10,6 +14,60 @@ bool isSupportedArea(RegisterArea area) | |||
| } // 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) | |||
| : area_(area), | |||
| index_(index) | |||
| @@ -2,6 +2,15 @@ | |||
| #include <string> | |||
| enum class RegisterAddressParseError | |||
| { | |||
| None, | |||
| Empty, | |||
| UnsupportedArea, | |||
| InvalidFormat, | |||
| OutOfRange | |||
| }; | |||
| enum class RegisterArea | |||
| { | |||
| M, | |||
| @@ -28,3 +37,12 @@ private: | |||
| RegisterArea area_; | |||
| 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"); | |||
| } | |||
| 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() | |||
| { | |||
| // 验证离线仓库不会把 M 位和 D 字交叉解释 | |||
| @@ -275,6 +297,7 @@ int main() | |||
| { | |||
| // 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程 | |||
| testRegisterAddressBoundaries(); | |||
| testRegisterAddressParsing(); | |||
| testRegisterRepositorySeparatesAreas(); | |||
| testLogicNodeConfigurationBoundaries(); | |||
| 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 | |||