diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 1d6c097..a4328e8 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -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 \ diff --git a/app/src/domain/register_address.cpp b/app/src/domain/register_address.cpp index 96c2098..57f52f1 100644 --- a/app/src/domain/register_address.cpp +++ b/app/src/domain/register_address.cpp @@ -1,5 +1,9 @@ #include "register_address.h" +#include +#include +#include + 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( + std::toupper(static_cast(*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(*current); + if (!std::isdigit(character)) + { + return {false, {area, 0}, RegisterAddressParseError::InvalidFormat}; + } + const int digit = *current - '0'; + if (index > (std::numeric_limits::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) diff --git a/app/src/domain/register_address.h b/app/src/domain/register_address.h index 9f7978c..a9f2ccf 100644 --- a/app/src/domain/register_address.h +++ b/app/src/domain/register_address.h @@ -2,6 +2,15 @@ #include +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); diff --git a/app/src/domain/register_monitor_model.cpp b/app/src/domain/register_monitor_model.cpp new file mode 100644 index 0000000..285beb2 --- /dev/null +++ b/app/src/domain/register_monitor_model.cpp @@ -0,0 +1,40 @@ +#include "register_monitor_model.h" + +#include + +const std::vector &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(); +} diff --git a/app/src/domain/register_monitor_model.h b/app/src/domain/register_monitor_model.h new file mode 100644 index 0000000..090fd90 --- /dev/null +++ b/app/src/domain/register_monitor_model.h @@ -0,0 +1,37 @@ +#pragma once + +#include "register_address.h" + +#include +#include +#include + +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 &addresses() const; + bool contains(const RegisterAddress &address) const; + bool add(const RegisterAddress &address); + bool remove(const RegisterAddress &address); + void clear(); + +private: + std::vector addresses_; +}; diff --git a/app/src/services/register_monitor_service.cpp b/app/src/services/register_monitor_service.cpp new file mode 100644 index 0000000..5596097 --- /dev/null +++ b/app/src/services/register_monitor_service.cpp @@ -0,0 +1,154 @@ +#include "register_monitor_service.h" + +#include +#include + +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 pending; + pending.reserve(static_cast(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(pending.size())}; +} + +RegisterMonitorResult RegisterMonitorService::remove( + const std::vector &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(model_.addresses().size()); + model_.clear(); + if (addresses_changed_callback_) + { + addresses_changed_callback_(); + } + return {true, RegisterMonitorError::None, {}, removed_count}; +} + +const std::vector &RegisterMonitorService::addresses() const +{ + return model_.addresses(); +} + +std::vector RegisterMonitorService::values(bool communication_fault) const +{ + std::vector 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 callback) +{ + addresses_changed_callback_ = std::move(callback); +} diff --git a/app/src/services/register_monitor_service.h b/app/src/services/register_monitor_service.h new file mode 100644 index 0000000..e394aec --- /dev/null +++ b/app/src/services/register_monitor_service.h @@ -0,0 +1,43 @@ +#pragma once + +#include "domain/register_monitor_model.h" +#include "domain/register_repository.h" + +#include +#include +#include + +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 &addresses); + RegisterMonitorResult clear(); + const std::vector &addresses() const; + std::vector values(bool communication_fault) const; + void setAddressesChangedCallback(std::function callback); + +private: + RegisterRepository &repository_; + RegisterMonitorModel model_; + std::function addresses_changed_callback_; +}; diff --git a/app/tests/.gitkeep b/app/tests/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/app/tests/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index 47cf10c..c8394b9 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -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(); diff --git a/app/tests/register_monitor_service_tests.cpp b/app/tests/register_monitor_service_tests.cpp new file mode 100644 index 0000000..7d8934f --- /dev/null +++ b/app/tests/register_monitor_service_tests.cpp @@ -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 +#include +#include + +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 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; +} diff --git a/app/tests/register_monitor_service_tests.pro b/app/tests/register_monitor_service_tests.pro new file mode 100644 index 0000000..a5d93bc --- /dev/null +++ b/app/tests/register_monitor_service_tests.pro @@ -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