Przeglądaj źródła

feat: 增加自由寄存器监控核心

main
suyu 1 miesiąc temu
rodzic
commit
8a6dff85c0
11 zmienionych plików z 496 dodań i 1 usunięć
  1. +4
    -0
      app/integrated_platform.pro
  2. +58
    -0
      app/src/domain/register_address.cpp
  3. +18
    -0
      app/src/domain/register_address.h
  4. +40
    -0
      app/src/domain/register_monitor_model.cpp
  5. +37
    -0
      app/src/domain/register_monitor_model.h
  6. +154
    -0
      app/src/services/register_monitor_service.cpp
  7. +43
    -0
      app/src/services/register_monitor_service.h
  8. +0
    -1
      app/tests/.gitkeep
  9. +23
    -0
      app/tests/domain_tests.cpp
  10. +95
    -0
      app/tests/register_monitor_service_tests.cpp
  11. +24
    -0
      app/tests/register_monitor_service_tests.pro

+ 4
- 0
app/integrated_platform.pro Wyświetl plik

@@ -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 \


+ 58
- 0
app/src/domain/register_address.cpp Wyświetl plik

@@ -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)


+ 18
- 0
app/src/domain/register_address.h Wyświetl plik

@@ -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);

+ 40
- 0
app/src/domain/register_monitor_model.cpp Wyświetl plik

@@ -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();
}

+ 37
- 0
app/src/domain/register_monitor_model.h Wyświetl plik

@@ -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_;
};

+ 154
- 0
app/src/services/register_monitor_service.cpp Wyświetl plik

@@ -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);
}

+ 43
- 0
app/src/services/register_monitor_service.h Wyświetl plik

@@ -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_;
};

+ 0
- 1
app/tests/.gitkeep Wyświetl plik

@@ -1 +0,0 @@


+ 23
- 0
app/tests/domain_tests.cpp Wyświetl plik

@@ -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();


+ 95
- 0
app/tests/register_monitor_service_tests.cpp Wyświetl plik

@@ -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;
}

+ 24
- 0
app/tests/register_monitor_service_tests.pro Wyświetl plik

@@ -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

Ładowanie…
Anuluj
Zapisz