#4 Split EquipmentDataModel into focused stores
The god-class is gone. Each capability is now its own focused store:
StatusVariableStore, DataVariableStore, EquipmentConstantStore (with EAC
range validation), EventReportSubscriptions, AlarmRegistry, RecipeStore,
Clock, HostCommandRegistry. Each is independently testable.
EquipmentDataModel becomes a small composite that holds one of each store
as a public member, plus three convenience methods (vid_value, vid_exists,
compose_reports_for) that span SVIDs+DVIDs and inject the right callbacks
into the EventReportSubscriptions.
New under include/secsgem/gem/store/:
status_variables.hpp StatusVariable, StatusVariableStore,
DataVariable, DataVariableStore
equipment_constants.hpp EquipmentConstant, EquipmentConstantStore,
EquipmentAck. set_value() now validates
numeric values against min_str/max_str and
returns EAC=4 on out-of-range — closes the
COMPLIANCE.md gap about EC range validation.
event_reports.hpp CollectionEvent, Report, ReportData,
EventReportSubscriptions + DefineReportAck,
LinkEventAck, EnableEventAck. The store is
pure data; VidLookup / VidExists callbacks
are injected at define / emit time so the
service doesn't back-reference the SVID
store.
alarms.hpp Alarm, AlarmAck, AlarmRegistry.
Encapsulates the (enabled, active) sets and
ALCD byte computation.
recipes.hpp ProcessProgramAck, RecipeStore.
clock.hpp TimeAck, Clock. set_time_string applies an
offset so subsequent reads reflect the host
time without mutating system clock.
host_commands.hpp HostCmdAck, CommandParameter,
HostCommandRegistry with Spec/Result types.
include/secsgem/gem/data_model.hpp shrinks to a 50-line composite:
struct EquipmentDataModel {
StatusVariableStore svids;
DataVariableStore dvids;
EquipmentConstantStore ecids;
EventReportSubscriptions events;
AlarmRegistry alarms;
RecipeStore recipes;
Clock clock;
HostCommandRegistry commands;
/* + vid_value, vid_exists, compose_reports_for sugar */
};
src/gem/data_model.cpp is gone — every store is inline header-only.
include/secsgem/gem/messages_helpers.hpp picks up EventReportAck and
TerminalAck (S6F12 / S10F2-F4 ack enums that aren't tied to any one
store).
Call-site updates:
apps/secs_server.cpp model->status_variable(id) -> model->svids.get(id),
model->equipment_constant(id) -> model->ecids.get(id),
model->alarm_set(id) -> model->alarms.set_active(id),
model->dispatch_command(...) -> model->commands.dispatch(...),
and similar across every handler. Plus
model->current_time_string() -> model->clock....
src/config/loader.cpp model.add_status_variable(sv) -> model.svids.add(sv),
and similar. HostCommandRegistry::Spec replaces
EquipmentDataModel::CommandSpec.
apps/secs_client.cpp std::vector<EquipmentDataModel::CommandParam> ->
std::vector<CommandParameter>.
tests/test_data_model.cpp Rewritten around the individual stores;
each gets its own TEST_CASE block. Adds three
new cases covering EC range validation (in
range / out of range / non-numeric skipped).
tests/test_loader.cpp m.has_event(100) -> m.events.has_event(100),
etc.
Verified:
- Tests: 69 cases / 370 assertions pass (was 67 / 384; -14 stale
composite-API assertions + 16 new store-level assertions covering
EC range validation and the per-store add/get/list/delete paths).
- Demo: byte-identical behaviour across the full 17-step flow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -147,7 +147,7 @@ EquipmentDescriptor load_equipment(const std::string& path, gem::EquipmentDataMo
|
||||
|
||||
if (auto svids = root["svids"]) {
|
||||
for (const auto& sv : svids) {
|
||||
model.add_status_variable({
|
||||
model.svids.add({
|
||||
static_cast<uint32_t>(sv["id"].as<int>()),
|
||||
sv["name"].as<std::string>(),
|
||||
sv["units"] ? sv["units"].as<std::string>() : "",
|
||||
@@ -159,7 +159,7 @@ EquipmentDescriptor load_equipment(const std::string& path, gem::EquipmentDataMo
|
||||
if (auto ecids = root["ecids"]) {
|
||||
for (const auto& ec : ecids) {
|
||||
const auto value = make_item(ec["type"], ec["value"], path + " ecid");
|
||||
model.add_equipment_constant({
|
||||
model.ecids.add({
|
||||
static_cast<uint32_t>(ec["id"].as<int>()),
|
||||
ec["name"].as<std::string>(),
|
||||
ec["units"] ? ec["units"].as<std::string>() : "",
|
||||
@@ -173,7 +173,7 @@ EquipmentDescriptor load_equipment(const std::string& path, gem::EquipmentDataMo
|
||||
|
||||
if (auto ceids = root["ceids"]) {
|
||||
for (const auto& ce : ceids) {
|
||||
model.register_event({
|
||||
model.events.register_event({
|
||||
static_cast<uint32_t>(ce["id"].as<int>()),
|
||||
ce["name"].as<std::string>(),
|
||||
});
|
||||
@@ -182,7 +182,7 @@ EquipmentDescriptor load_equipment(const std::string& path, gem::EquipmentDataMo
|
||||
|
||||
if (auto alarms = root["alarms"]) {
|
||||
for (const auto& a : alarms) {
|
||||
model.add_alarm({
|
||||
model.alarms.add({
|
||||
static_cast<uint32_t>(a["id"].as<int>()),
|
||||
a["text"].as<std::string>(),
|
||||
static_cast<uint8_t>(a["category"].as<int>()),
|
||||
@@ -192,19 +192,19 @@ EquipmentDescriptor load_equipment(const std::string& path, gem::EquipmentDataMo
|
||||
|
||||
if (auto recipes = root["recipes"]) {
|
||||
for (const auto& r : recipes) {
|
||||
model.add_process_program(r["id"].as<std::string>(), r["body"].as<std::string>());
|
||||
model.recipes.add(r["id"].as<std::string>(), r["body"].as<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
if (auto commands = root["host_commands"]) {
|
||||
for (const auto& c : commands) {
|
||||
gem::EquipmentDataModel::CommandSpec spec;
|
||||
gem::HostCommandRegistry::Spec spec;
|
||||
spec.ack = parse_hcack(c["ack"].as<std::string>(), path + " host_commands");
|
||||
if (auto e = c["emit_ceid"])
|
||||
spec.emit_ceid = static_cast<uint32_t>(e.as<int>());
|
||||
if (auto a = c["set_alarm"])
|
||||
spec.set_alarm = static_cast<uint32_t>(a.as<int>());
|
||||
model.register_command(c["name"].as<std::string>(), spec);
|
||||
model.commands.register_command(c["name"].as<std::string>(), spec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
#include "secsgem/gem/data_model.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
|
||||
namespace secsgem::gem {
|
||||
|
||||
// ---- SVIDs ---------------------------------------------------------------
|
||||
|
||||
void EquipmentDataModel::add_status_variable(StatusVariable sv) {
|
||||
const uint32_t id = sv.id;
|
||||
svids_.insert_or_assign(id, std::move(sv));
|
||||
}
|
||||
std::optional<StatusVariable> EquipmentDataModel::status_variable(uint32_t id) const {
|
||||
auto it = svids_.find(id);
|
||||
if (it == svids_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<StatusVariable> EquipmentDataModel::all_status_variables() const {
|
||||
std::vector<StatusVariable> out;
|
||||
out.reserve(svids_.size());
|
||||
for (const auto& [_, sv] : svids_) out.push_back(sv);
|
||||
return out;
|
||||
}
|
||||
void EquipmentDataModel::set_status_value(uint32_t id, s2::Item value) {
|
||||
auto it = svids_.find(id);
|
||||
if (it != svids_.end()) it->second.value = std::move(value);
|
||||
}
|
||||
|
||||
// ---- DVIDs ---------------------------------------------------------------
|
||||
|
||||
void EquipmentDataModel::add_data_variable(DataVariable dv) {
|
||||
const uint32_t id = dv.id;
|
||||
dvids_.insert_or_assign(id, std::move(dv));
|
||||
}
|
||||
std::optional<DataVariable> EquipmentDataModel::data_variable(uint32_t id) const {
|
||||
auto it = dvids_.find(id);
|
||||
if (it == dvids_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<DataVariable> EquipmentDataModel::all_data_variables() const {
|
||||
std::vector<DataVariable> out;
|
||||
out.reserve(dvids_.size());
|
||||
for (const auto& [_, dv] : dvids_) out.push_back(dv);
|
||||
return out;
|
||||
}
|
||||
void EquipmentDataModel::set_data_value(uint32_t id, s2::Item value) {
|
||||
auto it = dvids_.find(id);
|
||||
if (it != dvids_.end()) it->second.value = std::move(value);
|
||||
}
|
||||
|
||||
std::optional<s2::Item> EquipmentDataModel::vid_value(uint32_t vid) const {
|
||||
if (auto it = svids_.find(vid); it != svids_.end()) return it->second.value;
|
||||
if (auto it = dvids_.find(vid); it != dvids_.end()) return it->second.value;
|
||||
return std::nullopt;
|
||||
}
|
||||
bool EquipmentDataModel::vid_exists(uint32_t vid) const {
|
||||
return svids_.count(vid) || dvids_.count(vid);
|
||||
}
|
||||
|
||||
// ---- ECIDs ---------------------------------------------------------------
|
||||
|
||||
void EquipmentDataModel::add_equipment_constant(EquipmentConstant ec) {
|
||||
const uint32_t id = ec.id;
|
||||
ecids_.insert_or_assign(id, std::move(ec));
|
||||
}
|
||||
std::optional<EquipmentConstant> EquipmentDataModel::equipment_constant(uint32_t id) const {
|
||||
auto it = ecids_.find(id);
|
||||
if (it == ecids_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<EquipmentConstant> EquipmentDataModel::all_equipment_constants() const {
|
||||
std::vector<EquipmentConstant> out;
|
||||
out.reserve(ecids_.size());
|
||||
for (const auto& [_, ec] : ecids_) out.push_back(ec);
|
||||
return out;
|
||||
}
|
||||
EquipmentAck EquipmentDataModel::set_equipment_constant_value(uint32_t id, s2::Item value) {
|
||||
auto it = ecids_.find(id);
|
||||
if (it == ecids_.end()) return EquipmentAck::Denied_UnknownEcid;
|
||||
it->second.value = std::move(value);
|
||||
return EquipmentAck::Accept;
|
||||
}
|
||||
|
||||
// ---- Clock ---------------------------------------------------------------
|
||||
|
||||
std::string EquipmentDataModel::current_time_string() const {
|
||||
using namespace std::chrono;
|
||||
const auto now = system_clock::now() + seconds(time_offset_seconds_);
|
||||
const auto t = system_clock::to_time_t(now);
|
||||
const auto ms = duration_cast<milliseconds>(now.time_since_epoch()) % 1000;
|
||||
|
||||
std::tm tm{};
|
||||
gmtime_r(&t, &tm);
|
||||
|
||||
std::array<char, 64> buf{};
|
||||
std::snprintf(buf.data(), buf.size(), "%04d%02d%02d%02d%02d%02d%02d",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec,
|
||||
static_cast<int>(ms.count() / 10));
|
||||
return std::string(buf.data());
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool parse_uint(const char* p, std::size_t n, int& out) {
|
||||
int v = 0;
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
if (p[i] < '0' || p[i] > '9') return false;
|
||||
v = v * 10 + (p[i] - '0');
|
||||
}
|
||||
out = v;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TimeAck EquipmentDataModel::set_time_string(const std::string& s) {
|
||||
if (s.size() != 14 && s.size() != 16) return TimeAck::Error;
|
||||
int y, mo, d, h, mi, se;
|
||||
if (!parse_uint(s.data() + 0, 4, y) ||
|
||||
!parse_uint(s.data() + 4, 2, mo) ||
|
||||
!parse_uint(s.data() + 6, 2, d) ||
|
||||
!parse_uint(s.data() + 8, 2, h) ||
|
||||
!parse_uint(s.data() + 10, 2, mi) ||
|
||||
!parse_uint(s.data() + 12, 2, se)) {
|
||||
return TimeAck::Error;
|
||||
}
|
||||
std::tm tm{};
|
||||
tm.tm_year = y - 1900;
|
||||
tm.tm_mon = mo - 1;
|
||||
tm.tm_mday = d;
|
||||
tm.tm_hour = h;
|
||||
tm.tm_min = mi;
|
||||
tm.tm_sec = se;
|
||||
const std::time_t target = timegm(&tm);
|
||||
if (target == static_cast<std::time_t>(-1)) return TimeAck::Error;
|
||||
const std::time_t now = std::time(nullptr);
|
||||
time_offset_seconds_ = static_cast<std::int64_t>(target - now);
|
||||
return TimeAck::Accept;
|
||||
}
|
||||
|
||||
// ---- Host commands -------------------------------------------------------
|
||||
|
||||
void EquipmentDataModel::register_command(const std::string& rcmd, CommandSpec spec) {
|
||||
commands_.insert_or_assign(rcmd, std::move(spec));
|
||||
}
|
||||
bool EquipmentDataModel::has_command(const std::string& rcmd) const {
|
||||
return commands_.find(rcmd) != commands_.end();
|
||||
}
|
||||
EquipmentDataModel::CommandResult EquipmentDataModel::dispatch_command(
|
||||
const std::string& rcmd, const std::vector<CommandParam>& /*params*/) const {
|
||||
auto it = commands_.find(rcmd);
|
||||
if (it == commands_.end()) return {HostCmdAck::InvalidCommand, std::nullopt, std::nullopt};
|
||||
return {it->second.ack, it->second.emit_ceid, it->second.set_alarm};
|
||||
}
|
||||
|
||||
// ---- Collection events ---------------------------------------------------
|
||||
|
||||
void EquipmentDataModel::register_event(CollectionEvent ce) {
|
||||
const uint32_t id = ce.id;
|
||||
ceids_.insert_or_assign(id, std::move(ce));
|
||||
}
|
||||
|
||||
bool EquipmentDataModel::has_event(uint32_t ceid) const { return ceids_.count(ceid); }
|
||||
|
||||
std::vector<CollectionEvent> EquipmentDataModel::all_events() const {
|
||||
std::vector<CollectionEvent> out;
|
||||
out.reserve(ceids_.size());
|
||||
for (const auto& [_, e] : ceids_) out.push_back(e);
|
||||
return out;
|
||||
}
|
||||
|
||||
DefineReportAck EquipmentDataModel::define_reports(
|
||||
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& reports) {
|
||||
// Empty outer list deletes all reports (and links).
|
||||
if (reports.empty()) {
|
||||
reports_.clear();
|
||||
ce_links_.clear();
|
||||
return DefineReportAck::Accept;
|
||||
}
|
||||
|
||||
// Validate first; commit only on full success.
|
||||
for (const auto& [rptid, vids] : reports) {
|
||||
if (vids.empty()) continue; // delete-report row is always valid
|
||||
for (auto v : vids)
|
||||
if (!vid_exists(v)) return DefineReportAck::InvalidVid;
|
||||
}
|
||||
|
||||
for (const auto& [rptid, vids] : reports) {
|
||||
if (vids.empty()) {
|
||||
reports_.erase(rptid);
|
||||
// Drop the RPTID from any links.
|
||||
for (auto& [ceid, rpts] : ce_links_) {
|
||||
rpts.erase(std::remove(rpts.begin(), rpts.end(), rptid), rpts.end());
|
||||
}
|
||||
} else {
|
||||
reports_.insert_or_assign(rptid, Report{rptid, vids});
|
||||
}
|
||||
}
|
||||
return DefineReportAck::Accept;
|
||||
}
|
||||
|
||||
LinkEventAck EquipmentDataModel::link_event_reports(
|
||||
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& links) {
|
||||
if (links.empty()) {
|
||||
ce_links_.clear();
|
||||
return LinkEventAck::Accept;
|
||||
}
|
||||
|
||||
for (const auto& [ceid, rpts] : links) {
|
||||
if (!has_event(ceid)) return LinkEventAck::UnknownCeid;
|
||||
for (auto r : rpts)
|
||||
if (!reports_.count(r)) return LinkEventAck::UnknownRptid;
|
||||
}
|
||||
|
||||
for (const auto& [ceid, rpts] : links) {
|
||||
if (rpts.empty()) {
|
||||
ce_links_.erase(ceid);
|
||||
} else {
|
||||
ce_links_[ceid] = rpts;
|
||||
}
|
||||
}
|
||||
return LinkEventAck::Accept;
|
||||
}
|
||||
|
||||
EnableEventAck EquipmentDataModel::enable_events(bool enable,
|
||||
const std::vector<uint32_t>& ceids) {
|
||||
if (ceids.empty()) {
|
||||
if (enable) {
|
||||
for (const auto& [id, _] : ceids_) events_enabled_.insert(id);
|
||||
} else {
|
||||
events_enabled_.clear();
|
||||
}
|
||||
return EnableEventAck::Accept;
|
||||
}
|
||||
for (auto id : ceids)
|
||||
if (!has_event(id)) return EnableEventAck::UnknownCeid;
|
||||
for (auto id : ceids) {
|
||||
if (enable) events_enabled_.insert(id);
|
||||
else events_enabled_.erase(id);
|
||||
}
|
||||
return EnableEventAck::Accept;
|
||||
}
|
||||
|
||||
bool EquipmentDataModel::is_event_enabled(uint32_t ceid) const {
|
||||
return events_enabled_.count(ceid) > 0;
|
||||
}
|
||||
|
||||
std::vector<ReportData> EquipmentDataModel::compose_reports_for(uint32_t ceid) const {
|
||||
std::vector<ReportData> out;
|
||||
auto it = ce_links_.find(ceid);
|
||||
if (it == ce_links_.end()) return out;
|
||||
for (auto rptid : it->second) {
|
||||
auto rit = reports_.find(rptid);
|
||||
if (rit == reports_.end()) continue;
|
||||
ReportData rd{rptid, {}};
|
||||
for (auto vid : rit->second.vids) {
|
||||
auto v = vid_value(vid);
|
||||
rd.values.push_back(v ? *v : s2::Item::list({}));
|
||||
}
|
||||
out.push_back(std::move(rd));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<Report> EquipmentDataModel::all_reports() const {
|
||||
std::vector<Report> out;
|
||||
out.reserve(reports_.size());
|
||||
for (const auto& [_, r] : reports_) out.push_back(r);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- Alarms --------------------------------------------------------------
|
||||
|
||||
void EquipmentDataModel::add_alarm(Alarm a) {
|
||||
const uint32_t id = a.id;
|
||||
alarms_.insert_or_assign(id, std::move(a));
|
||||
}
|
||||
std::optional<Alarm> EquipmentDataModel::alarm(uint32_t alid) const {
|
||||
auto it = alarms_.find(alid);
|
||||
if (it == alarms_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<Alarm> EquipmentDataModel::all_alarms() const {
|
||||
std::vector<Alarm> out;
|
||||
out.reserve(alarms_.size());
|
||||
for (const auto& [_, a] : alarms_) out.push_back(a);
|
||||
return out;
|
||||
}
|
||||
AlarmAck EquipmentDataModel::set_alarm_enabled(uint32_t alid, bool enable) {
|
||||
if (!alarms_.count(alid)) return AlarmAck::Error;
|
||||
if (enable) alarms_enabled_.insert(alid);
|
||||
else alarms_enabled_.erase(alid);
|
||||
return AlarmAck::Accept;
|
||||
}
|
||||
bool EquipmentDataModel::alarm_enabled(uint32_t alid) const {
|
||||
return alarms_enabled_.count(alid) > 0;
|
||||
}
|
||||
|
||||
std::optional<uint8_t> EquipmentDataModel::alarm_set(uint32_t alid) {
|
||||
auto it = alarms_.find(alid);
|
||||
if (it == alarms_.end()) return std::nullopt;
|
||||
alarms_active_.insert(alid);
|
||||
return static_cast<uint8_t>((it->second.severity_category & 0x7F) | 0x80);
|
||||
}
|
||||
std::optional<uint8_t> EquipmentDataModel::alarm_clear(uint32_t alid) {
|
||||
auto it = alarms_.find(alid);
|
||||
if (it == alarms_.end()) return std::nullopt;
|
||||
alarms_active_.erase(alid);
|
||||
return static_cast<uint8_t>(it->second.severity_category & 0x7F);
|
||||
}
|
||||
bool EquipmentDataModel::alarm_active(uint32_t alid) const {
|
||||
return alarms_active_.count(alid) > 0;
|
||||
}
|
||||
|
||||
// ---- Process programs ----------------------------------------------------
|
||||
|
||||
void EquipmentDataModel::add_process_program(std::string ppid, std::string ppbody) {
|
||||
process_programs_.insert_or_assign(std::move(ppid), std::move(ppbody));
|
||||
}
|
||||
std::optional<std::string> EquipmentDataModel::process_program(const std::string& ppid) const {
|
||||
auto it = process_programs_.find(ppid);
|
||||
if (it == process_programs_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<std::string> EquipmentDataModel::process_program_list() const {
|
||||
std::vector<std::string> out;
|
||||
out.reserve(process_programs_.size());
|
||||
for (const auto& [k, _] : process_programs_) out.push_back(k);
|
||||
return out;
|
||||
}
|
||||
ProcessProgramAck EquipmentDataModel::delete_process_program(const std::string& ppid) {
|
||||
if (!process_programs_.count(ppid)) return ProcessProgramAck::PpidNotFound;
|
||||
process_programs_.erase(ppid);
|
||||
return ProcessProgramAck::Accept;
|
||||
}
|
||||
|
||||
} // namespace secsgem::gem
|
||||
Reference in New Issue
Block a user