From 711ee1b40f7859e4f984383d4220dacb8bfa1a41 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Tue, 2 Jun 2026 09:51:54 +0200 Subject: [PATCH] #4 Split EquipmentDataModel into focused stores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 -> std::vector. 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 --- CMakeLists.txt | 1 - apps/secs_client.cpp | 2 +- apps/secs_server.cpp | 52 +-- include/secsgem/gem/data_model.hpp | 316 +++------------- include/secsgem/gem/messages_helpers.hpp | 13 + include/secsgem/gem/store/alarms.hpp | 74 ++++ include/secsgem/gem/store/clock.hpp | 78 ++++ .../secsgem/gem/store/equipment_constants.hpp | 103 ++++++ include/secsgem/gem/store/event_reports.hpp | 178 +++++++++ include/secsgem/gem/store/host_commands.hpp | 64 ++++ include/secsgem/gem/store/recipes.hpp | 49 +++ .../secsgem/gem/store/status_variables.hpp | 94 +++++ src/config/loader.cpp | 14 +- src/gem/data_model.cpp | 339 ------------------ tests/test_data_model.cpp | 238 ++++++------ tests/test_loader.cpp | 32 +- 16 files changed, 877 insertions(+), 770 deletions(-) create mode 100644 include/secsgem/gem/store/alarms.hpp create mode 100644 include/secsgem/gem/store/clock.hpp create mode 100644 include/secsgem/gem/store/equipment_constants.hpp create mode 100644 include/secsgem/gem/store/event_reports.hpp create mode 100644 include/secsgem/gem/store/host_commands.hpp create mode 100644 include/secsgem/gem/store/recipes.hpp create mode 100644 include/secsgem/gem/store/status_variables.hpp delete mode 100644 src/gem/data_model.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c5a1590..40d15ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,6 @@ add_library(secsgem src/hsms/header.cpp src/hsms/connection.cpp src/gem/control_state.cpp - src/gem/data_model.cpp src/config/loader.cpp src/endpoint.cpp ) diff --git a/apps/secs_client.cpp b/apps/secs_client.cpp index f88616f..46ba579 100644 --- a/apps/secs_client.cpp +++ b/apps/secs_client.cpp @@ -222,7 +222,7 @@ int main(int argc, char** argv) { // 9. Host command START -> equipment fires CEID kCeidProcessStarted. seq->steps.push_back([conn, logfn, fail, pause_then](auto next) { - std::vector params = { + std::vector params = { {"LOTID", s2::Item::ascii("LOT-42")}, {"PPID", s2::Item::ascii("RECIPE-A")}, }; diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index b728bbf..8f6c4c6 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -36,8 +36,8 @@ constexpr uint32_t kSvidControlState = 1; constexpr uint32_t kSvidClock = 2; void refresh(gem::EquipmentDataModel& m, const gem::ControlStateMachine& sm) { - m.set_status_value(kSvidControlState, s2::Item::ascii(gem::control_state_name(sm.state()))); - m.set_status_value(kSvidClock, s2::Item::ascii(m.current_time_string())); + m.svids.set_value(kSvidControlState, s2::Item::ascii(gem::control_state_name(sm.state()))); + m.svids.set_value(kSvidClock, s2::Item::ascii(m.clock.current_time_string())); } } // namespace @@ -59,10 +59,10 @@ int main(int argc, char** argv) { std::cerr << "[equip] config error: " << e.what() << std::endl; return 1; } - logfn("loaded " + std::to_string(model->all_status_variables().size()) + " SVIDs, " + - std::to_string(model->all_equipment_constants().size()) + " ECIDs, " + - std::to_string(model->all_events().size()) + " CEIDs, " + - std::to_string(model->all_alarms().size()) + " alarms"); + logfn("loaded " + std::to_string(model->svids.size()) + " SVIDs, " + + std::to_string(model->ecids.all().size()) + " ECIDs, " + + std::to_string(model->events.all_events().size()) + " CEIDs, " + + std::to_string(model->alarms.all().size()) + " alarms"); auto sm = std::make_shared(sm_cfg.table, sm_cfg.initial); @@ -93,11 +93,11 @@ int main(int argc, char** argv) { asio::post(io, [active_conn, model, logfn, emit_event, alid]() { auto conn = active_conn->lock(); if (!conn) return; - auto alarm = model->alarm(alid); + auto alarm = model->alarms.get(alid); if (!alarm) return; - auto alcd = model->alarm_set(alid); + auto alcd = model->alarms.set_active(alid); if (!alcd) return; - if (model->alarm_enabled(alid)) { + if (model->alarms.enabled(alid)) { logfn("emit S5F1 alarm set ALID=" + std::to_string(alid)); conn->send_request(gem::s5f1_alarm_report(*alcd, alid, alarm->text), [](std::error_code, const s2::Message&) {}); @@ -128,10 +128,10 @@ int main(int argc, char** argv) { if (!svids) return s2::Message(1, 0, false); std::vector> values; if (svids->empty()) { - for (const auto& sv : model->all_status_variables()) values.push_back(sv.value); + for (const auto& sv : model->svids.all()) values.push_back(sv.value); } else { for (auto id : *svids) { - auto sv = model->status_variable(id); + auto sv = model->svids.get(id); values.push_back(sv ? std::optional(sv->value) : std::nullopt); } } @@ -140,7 +140,7 @@ int main(int argc, char** argv) { }); router.on(1, 11, [model, logfn](const s2::Message&) { std::vector rows; - for (const auto& sv : model->all_status_variables()) + for (const auto& sv : model->svids.all()) rows.push_back({sv.id, sv.name, sv.units}); logfn("S1F11 -> S1F12 (namelist, " + std::to_string(rows.size()) + ")"); return gem::s1f12_status_namelist_data(rows); @@ -166,7 +166,7 @@ int main(int argc, char** argv) { if (!ids) return s2::Message(2, 0, false); std::vector values; for (auto id : *ids) { - auto ec = model->equipment_constant(id); + auto ec = model->ecids.get(id); values.push_back(ec ? ec->value : s2::Item::list({})); } logfn("S2F13 -> S2F14 (" + std::to_string(values.size()) + " values)"); @@ -178,7 +178,7 @@ int main(int argc, char** argv) { if (!sets) eac = gem::EquipmentAck::Denied_OutOfRange; else for (const auto& s : *sets) { - auto r = model->set_equipment_constant_value(s.ecid, s.value); + auto r = model->ecids.set_value(s.ecid, s.value); if (r != gem::EquipmentAck::Accept) eac = r; } logfn("S2F15 -> S2F16 EAC=" + std::to_string(static_cast(eac))); @@ -186,15 +186,15 @@ int main(int argc, char** argv) { }); router.on(2, 17, [model, logfn](const s2::Message&) { logfn("S2F17 -> S2F18 (clock)"); - return gem::s2f18_date_time_data(model->current_time_string()); + return gem::s2f18_date_time_data(model->clock.current_time_string()); }); router.on(2, 29, [model, logfn](const s2::Message& msg) { auto ids = gem::parse_u4_list_body(msg); std::vector ecs; - if (ids && ids->empty()) ecs = model->all_equipment_constants(); + if (ids && ids->empty()) ecs = model->ecids.all(); else if (ids) for (auto id : *ids) { - auto ec = model->equipment_constant(id); + auto ec = model->ecids.get(id); if (ec) ecs.push_back(*ec); } std::vector rows; @@ -205,7 +205,7 @@ int main(int argc, char** argv) { }); router.on(2, 31, [model, logfn](const s2::Message& msg) { auto t = gem::parse_s2f31(msg); - auto ack = t ? model->set_time_string(*t) : gem::TimeAck::Error; + auto ack = t ? model->clock.set_time_string(*t) : gem::TimeAck::Error; logfn("S2F31 -> S2F32 TIACK=" + std::to_string(static_cast(ack))); return gem::s2f32_date_time_ack(ack); }); @@ -244,7 +244,7 @@ int main(int argc, char** argv) { router.on(2, 41, [model, logfn, emit_event, emit_alarm_set](const s2::Message& msg) { auto cmd = gem::parse_s2f41(msg); if (!cmd) return gem::s2f42_host_command_ack(gem::HostCmdAck::ParameterInvalid, {}); - auto result = model->dispatch_command(cmd->rcmd, cmd->params); + auto result = model->commands.dispatch(cmd->rcmd, cmd->params); logfn("S2F41 RCMD=" + cmd->rcmd + " -> S2F42 HCACK=" + std::to_string(static_cast(result.ack))); if (result.ack == gem::HostCmdAck::Accept) { @@ -256,7 +256,7 @@ int main(int argc, char** argv) { router.on(5, 3, [model, logfn](const s2::Message& msg) { auto req = gem::parse_s5f3(msg); - auto ack = req ? model->set_alarm_enabled(req->alid, (req->aled & 0x80) != 0) + auto ack = req ? model->alarms.set_enabled(req->alid, (req->aled & 0x80) != 0) : gem::AlarmAck::Error; logfn(std::string("S5F3 -> S5F4 ACKC5=") + std::to_string(static_cast(ack))); return gem::s5f4_enable_alarm_ack(ack); @@ -264,33 +264,33 @@ int main(int argc, char** argv) { router.on(5, 5, [model, logfn](const s2::Message& msg) { auto ids = gem::parse_u4_list_body(msg); std::vector alarms; - if (ids && ids->empty()) alarms = model->all_alarms(); + if (ids && ids->empty()) alarms = model->alarms.all(); else if (ids) for (auto id : *ids) { - auto a = model->alarm(id); + auto a = model->alarms.get(id); if (a) alarms.push_back(*a); } logfn("S5F5 -> S5F6 (" + std::to_string(alarms.size()) + " alarms)"); return gem::s5f6_list_alarms_data( - alarms, [model](uint32_t id) { return model->alarm_active(id); }); + alarms, [model](uint32_t id) { return model->alarms.active(id); }); }); router.on(7, 3, [model, logfn](const s2::Message& msg) { auto pp = gem::parse_s7f3(msg); if (!pp) return gem::s7f4_process_program_ack(gem::ProcessProgramAck::LengthError); - model->add_process_program(pp->ppid, pp->ppbody); + model->recipes.add(pp->ppid, pp->ppbody); logfn("S7F3 PPID=" + pp->ppid + " -> S7F4 (Accept)"); return gem::s7f4_process_program_ack(gem::ProcessProgramAck::Accept); }); router.on(7, 5, [model, logfn](const s2::Message& msg) { auto ppid = gem::parse_s7f5(msg); if (!ppid) return gem::s7f6_process_program_data("", ""); - auto body = model->process_program(*ppid); + auto body = model->recipes.get(*ppid); logfn("S7F5 PPID=" + *ppid + " -> S7F6"); return gem::s7f6_process_program_data(*ppid, body ? *body : ""); }); router.on(7, 19, [model, logfn](const s2::Message&) { - auto list = model->process_program_list(); + auto list = model->recipes.list(); logfn("S7F19 -> S7F20 (" + std::to_string(list.size()) + " PPIDs)"); return gem::s7f20_current_eppd_data(list); }); diff --git a/include/secsgem/gem/data_model.hpp b/include/secsgem/gem/data_model.hpp index 23d8159..7c741ed 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -1,283 +1,57 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "secsgem/secs2/item.hpp" +#include "secsgem/gem/store/alarms.hpp" +#include "secsgem/gem/store/clock.hpp" +#include "secsgem/gem/store/equipment_constants.hpp" +#include "secsgem/gem/store/event_reports.hpp" +#include "secsgem/gem/store/host_commands.hpp" +#include "secsgem/gem/store/recipes.hpp" +#include "secsgem/gem/store/status_variables.hpp" namespace secsgem::gem { -namespace s2 = secsgem::secs2; +// Composite over the seven focused stores. Each store is independently +// testable and independently usable — the application can keep a reference +// to just `alarms` and ignore the rest if that's all it needs. Variable +// lookups span SVIDs and DVIDs through this composite (the host's view +// per E30 §6.11). +struct EquipmentDataModel { + StatusVariableStore svids; + DataVariableStore dvids; + EquipmentConstantStore ecids; + EventReportSubscriptions events; + AlarmRegistry alarms; + RecipeStore recipes; + Clock clock; + HostCommandRegistry commands; -// ---- Status / data / equipment-constant variables ------------------------ + // Convenience: VID -> value lookup spanning SVIDs and DVIDs. + std::optional vid_value(uint32_t vid) const { + if (auto v = svids.value(vid)) return v; + if (auto v = dvids.value(vid)) return v; + return std::nullopt; + } + bool vid_exists(uint32_t vid) const { + return svids.has(vid) || dvids.has(vid); + } -struct StatusVariable { - uint32_t id; - std::string name; - std::string units; - s2::Item value; -}; - -struct DataVariable { - uint32_t id; - std::string name; - std::string units; - s2::Item value; -}; - -struct EquipmentConstant { - uint32_t id; - std::string name; - std::string units; - s2::Item value; - s2::Item def_value; - std::string min_str; // optional, ASCII for S2F30 ECMIN/ECMAX - std::string max_str; -}; - -// ---- Event reports ------------------------------------------------------- - -struct CollectionEvent { - uint32_t id; - std::string name; -}; - -// One entry on S2F41. Defined here (not in the generated -// messages.hpp) so the data model can name it in its public API; the codegen -// references it as an external struct. -struct CommandParameter { - std::string name; - s2::Item value; -}; - -struct Report { - uint32_t id; - std::vector vids; -}; - -// One report's worth of data at emission time: the RPTID and the values for -// each VID, in declaration order. -struct ReportData { - uint32_t rptid; - std::vector values; -}; - -// ---- Alarms -------------------------------------------------------------- - -struct Alarm { - uint32_t id; - std::string text; - // Lower 7 bits of ALCD: alarm severity category (1=personal safety, - // 2=equipment safety, 4=parameter control error, ...). Bit 7 is the - // set/cleared flag, applied at emit time. - uint8_t severity_category; -}; - -// ---- Ack codes for the new SxFy ------------------------------------------ - -// S2F30 carries no ack; S2F34 / S2F36 / S2F38 do. -enum class DefineReportAck : uint8_t { - Accept = 0, - InsufficientSpace = 1, - InvalidFormat = 2, - RptidAlreadyDefined = 3, - InvalidVid = 5, -}; - -enum class LinkEventAck : uint8_t { - Accept = 0, - InsufficientSpace = 1, - InvalidFormat = 2, - UnknownCeid = 3, - UnknownRptid = 4, - CeidAlreadyLinked = 5, -}; - -enum class EnableEventAck : uint8_t { - Accept = 0, - UnknownCeid = 1, -}; - -// S5F2 / S5F4 alarm ack. -enum class AlarmAck : uint8_t { - Accept = 0, - Error = 1, -}; - -// S6F12 event-report ack. -enum class EventReportAck : uint8_t { - Accept = 0, - Denied = 1, -}; - -// S7F4 process-program ack. -enum class ProcessProgramAck : uint8_t { - Accept = 0, - PermissionNotGranted = 1, - LengthError = 2, - MatrixOverflow = 3, - PpidNotFound = 4, - ModeUnsupported = 5, - PerformanceError = 6, -}; - -// ---- Existing ack enums from earlier batches ----------------------------- - -enum class EquipmentAck : uint8_t { - Accept = 0, - Denied_UnknownEcid = 1, - Denied_Busy = 3, - Denied_OutOfRange = 4, -}; - -enum class TimeAck : uint8_t { - Accept = 0, - Error = 1, - NotDoneNotEmpty = 2, -}; - -enum class HostCmdAck : uint8_t { - Accept = 0, - InvalidCommand = 1, - CannotDoNow = 2, - ParameterInvalid = 3, - AcceptedWillFinishLater = 4, - Rejected = 5, - InvalidObject = 6, -}; - -enum class TerminalAck : uint8_t { - Accepted = 0, - WillNotDisplay = 1, - TerminalNotAvailable = 2, -}; - -// The in-memory equipment data dictionary. Owns SVIDs, DVIDs, ECIDs, the -// dynamic event-report subscription state (reports + links + enabled set), -// the alarm table, and a small process-program registry. Single-threaded; -// all access happens on the Asio executor. -class EquipmentDataModel { - public: - using CommandParam = CommandParameter; // back-compat alias - - // Declarative host-command effect, loaded from YAML. The server - // dispatches a command by looking up the spec and (optionally) firing - // a CEID emit / setting an alarm after the S2F42 reply is sent. - struct CommandSpec { - HostCmdAck ack = HostCmdAck::Accept; - std::optional emit_ceid; - std::optional set_alarm; - }; - - struct CommandResult { - HostCmdAck ack = HostCmdAck::InvalidCommand; - std::optional emit_ceid; - std::optional set_alarm; - }; - - // --- SVID --------------------------------------------------------------- - void add_status_variable(StatusVariable sv); - std::optional status_variable(uint32_t id) const; - std::vector all_status_variables() const; - void set_status_value(uint32_t id, s2::Item value); - - // --- DVID --------------------------------------------------------------- - void add_data_variable(DataVariable dv); - std::optional data_variable(uint32_t id) const; - std::vector all_data_variables() const; - void set_data_value(uint32_t id, s2::Item value); - - // VID lookup that searches SVIDs then DVIDs (E30 §6.5 — VIDs share a - // namespace from the host's point of view). - std::optional vid_value(uint32_t vid) const; - bool vid_exists(uint32_t vid) const; - - // --- ECID --------------------------------------------------------------- - void add_equipment_constant(EquipmentConstant ec); - std::optional equipment_constant(uint32_t id) const; - std::vector all_equipment_constants() const; - EquipmentAck set_equipment_constant_value(uint32_t id, s2::Item value); - - // --- Clock -------------------------------------------------------------- - std::string current_time_string() const; - TimeAck set_time_string(const std::string& time_str); - - // --- Host commands ------------------------------------------------------ - void register_command(const std::string& rcmd, CommandSpec spec); - CommandResult dispatch_command(const std::string& rcmd, - const std::vector& params) const; - bool has_command(const std::string& rcmd) const; - - // --- Collection events -------------------------------------------------- - void register_event(CollectionEvent ce); - bool has_event(uint32_t ceid) const; - std::vector all_events() const; - - // S2F33: define reports. `reports` maps RPTID -> VID list. An empty - // vector deletes all reports (and clears all links). An empty VID list - // for a given RPTID deletes that report. + // Sugar that adapts the EventReportSubscriptions API to the host-supplied + // VID list shape (matches what parse_s2f33 / parse_s2f35 yield). DefineReportAck define_reports( - const std::vector>>& reports); - - // S2F35: link CEIDs to RPTID lists. `links` maps CEID -> RPTID list. - // An empty RPTID list for a CEID clears its links. An empty outer list - // clears all links. + const std::vector>>& rows) { + return events.define_reports(rows, [this](uint32_t vid) { return vid_exists(vid); }); + } LinkEventAck link_event_reports( - const std::vector>>& links); - - // S2F37: enable/disable events. Empty CEID list applies to *all* - // registered events. - EnableEventAck enable_events(bool enable, const std::vector& ceids); - - bool is_event_enabled(uint32_t ceid) const; - - // Compose the report data for a CEID emission (S6F11 body's report list). - std::vector compose_reports_for(uint32_t ceid) const; - - std::vector all_reports() const; - - // --- Alarms ------------------------------------------------------------- - void add_alarm(Alarm a); - std::optional alarm(uint32_t alid) const; - std::vector all_alarms() const; - AlarmAck set_alarm_enabled(uint32_t alid, bool enable); - bool alarm_enabled(uint32_t alid) const; - // Returns the ALCD byte to put on the wire (bit 7 indicates set/cleared). - // Returns nullopt for unknown alarms. - std::optional alarm_set(uint32_t alid); - std::optional alarm_clear(uint32_t alid); - bool alarm_active(uint32_t alid) const; - - // --- Process programs --------------------------------------------------- - void add_process_program(std::string ppid, std::string ppbody); - std::optional process_program(const std::string& ppid) const; - std::vector process_program_list() const; - ProcessProgramAck delete_process_program(const std::string& ppid); - - private: - std::map svids_; - std::map dvids_; - std::map ecids_; - std::int64_t time_offset_seconds_ = 0; - std::map commands_; - - std::map ceids_; - std::map reports_; - std::map> ce_links_; // CEID -> RPTID list - std::set events_enabled_; - - std::map alarms_; - std::set alarms_enabled_; - std::set alarms_active_; - - std::map process_programs_; + const std::vector>>& rows) { + return events.link_event_reports(rows); + } + EnableEventAck enable_events(bool enable, const std::vector& ceids) { + return events.enable_events(enable, ceids); + } + bool is_event_enabled(uint32_t ceid) const { return events.is_enabled(ceid); } + std::vector compose_reports_for(uint32_t ceid) const { + return events.compose_for(ceid, [this](uint32_t vid) { return vid_value(vid); }); + } }; } // namespace secsgem::gem diff --git a/include/secsgem/gem/messages_helpers.hpp b/include/secsgem/gem/messages_helpers.hpp index e05a0ae..86473c4 100644 --- a/include/secsgem/gem/messages_helpers.hpp +++ b/include/secsgem/gem/messages_helpers.hpp @@ -127,4 +127,17 @@ inline s2::Message s5f6_list_alarms_data(const std::vector& alarms, inline constexpr uint8_t kAlarmEnableByte = 0x80; inline constexpr uint8_t kAlarmDisableByte = 0x00; +// ---- Ack enums that aren't tied to a specific store ------------------- + +enum class EventReportAck : uint8_t { // S6F12 + Accept = 0, + Denied = 1, +}; + +enum class TerminalAck : uint8_t { // S10F2, S10F4 + Accepted = 0, + WillNotDisplay = 1, + TerminalNotAvailable = 2, +}; + } // namespace secsgem::gem diff --git a/include/secsgem/gem/store/alarms.hpp b/include/secsgem/gem/store/alarms.hpp new file mode 100644 index 0000000..60755eb --- /dev/null +++ b/include/secsgem/gem/store/alarms.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace secsgem::gem { + +struct Alarm { + uint32_t id; + std::string text; + // Lower 7 bits of ALCD: severity category (1=personal safety, + // 2=equipment safety, 4=parameter control error, ...). Bit 7 marks + // set vs cleared and is applied at emit time. + uint8_t severity_category; +}; + +enum class AlarmAck : uint8_t { + Accept = 0, + Error = 1, +}; + +class AlarmRegistry { + public: + void add(Alarm a) { by_id_.insert_or_assign(a.id, std::move(a)); } + std::optional get(uint32_t id) const { + auto it = by_id_.find(id); + if (it == by_id_.end()) return std::nullopt; + return it->second; + } + std::vector all() const { + std::vector out; + out.reserve(by_id_.size()); + for (const auto& [_, a] : by_id_) out.push_back(a); + return out; + } + bool has(uint32_t id) const { return by_id_.count(id) > 0; } + + // S5F3 enable / disable. + AlarmAck set_enabled(uint32_t id, bool enable) { + if (!by_id_.count(id)) return AlarmAck::Error; + if (enable) enabled_.insert(id); + else enabled_.erase(id); + return AlarmAck::Accept; + } + bool enabled(uint32_t id) const { return enabled_.count(id) > 0; } + + // Trigger set / clear; returns the ALCD byte to put on the wire (bit 7 + // is the set flag, lower 7 carry the category). std::nullopt on unknown. + std::optional set_active(uint32_t id) { + auto it = by_id_.find(id); + if (it == by_id_.end()) return std::nullopt; + active_.insert(id); + return static_cast((it->second.severity_category & 0x7F) | 0x80); + } + std::optional clear_active(uint32_t id) { + auto it = by_id_.find(id); + if (it == by_id_.end()) return std::nullopt; + active_.erase(id); + return static_cast(it->second.severity_category & 0x7F); + } + bool active(uint32_t id) const { return active_.count(id) > 0; } + + private: + std::map by_id_; + std::set enabled_; + std::set active_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/store/clock.hpp b/include/secsgem/gem/store/clock.hpp new file mode 100644 index 0000000..7e97353 --- /dev/null +++ b/include/secsgem/gem/store/clock.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace secsgem::gem { + +enum class TimeAck : uint8_t { + Accept = 0, + Error = 1, + NotDoneNotEmpty = 2, +}; + +// The equipment clock. current_time_string() returns the 16-char SECS-II +// TIME format ("YYYYMMDDhhmmsscc"), with an offset applied if the host has +// previously set the time via S2F31. +class Clock { + public: + std::string current_time_string() const { + using namespace std::chrono; + const auto now = system_clock::now() + seconds(offset_seconds_); + const auto t = system_clock::to_time_t(now); + const auto ms = duration_cast(now.time_since_epoch()) % 1000; + + std::tm tm{}; + gmtime_r(&t, &tm); + + std::array 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(ms.count() / 10)); + return std::string(buf.data()); + } + + TimeAck 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_digits(s.data() + 0, 4, y) || + !parse_digits(s.data() + 4, 2, mo) || + !parse_digits(s.data() + 6, 2, d) || + !parse_digits(s.data() + 8, 2, h) || + !parse_digits(s.data() + 10, 2, mi) || + !parse_digits(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(-1)) return TimeAck::Error; + offset_seconds_ = static_cast(target - std::time(nullptr)); + return TimeAck::Accept; + } + + private: + static bool parse_digits(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; + } + + std::int64_t offset_seconds_ = 0; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/store/equipment_constants.hpp b/include/secsgem/gem/store/equipment_constants.hpp new file mode 100644 index 0000000..1e84b1e --- /dev/null +++ b/include/secsgem/gem/store/equipment_constants.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "secsgem/secs2/item.hpp" + +namespace secsgem::gem { + +namespace s2 = secsgem::secs2; + +struct EquipmentConstant { + uint32_t id; + std::string name; + std::string units; + s2::Item value; + s2::Item def_value; + std::string min_str; // bounds for S2F30 + EAC validation; "" disables check + std::string max_str; +}; + +enum class EquipmentAck : uint8_t { + Accept = 0, + Denied_UnknownEcid = 1, + Denied_Busy = 3, + Denied_OutOfRange = 4, +}; + +class EquipmentConstantStore { + public: + void add(EquipmentConstant ec) { by_id_.insert_or_assign(ec.id, std::move(ec)); } + std::optional get(uint32_t id) const { + auto it = by_id_.find(id); + if (it == by_id_.end()) return std::nullopt; + return it->second; + } + std::vector all() const { + std::vector out; + out.reserve(by_id_.size()); + for (const auto& [_, ec] : by_id_) out.push_back(ec); + return out; + } + bool has(uint32_t id) const { return by_id_.count(id) > 0; } + + // Validates against min/max for numeric formats (only when min_str and + // max_str parse cleanly). Returns Denied_UnknownEcid / Denied_OutOfRange + // per S2F16 EAC. This is the rule that closes the COMPLIANCE.md gap + // about "EC range validation against min/max". + EquipmentAck set_value(uint32_t id, s2::Item value) { + auto it = by_id_.find(id); + if (it == by_id_.end()) return EquipmentAck::Denied_UnknownEcid; + if (!in_range(it->second, value)) return EquipmentAck::Denied_OutOfRange; + it->second.value = std::move(value); + return EquipmentAck::Accept; + } + + private: + // For numeric formats, parse min_str / max_str as integers / doubles and + // compare against the first value of the array (we don't support setting + // multi-element ECs in this implementation). + static bool in_range(const EquipmentConstant& ec, const s2::Item& value) { + if (ec.min_str.empty() && ec.max_str.empty()) return true; + + auto parse_d = [](const std::string& s, double& out) { + if (s.empty()) return false; + try { out = std::stod(s); return true; } catch (...) { return false; } + }; + + double v; + if (!extract_number(value, v)) return true; // unknown format: skip + + double lo = -1e300, hi = 1e300; + parse_d(ec.min_str, lo); + parse_d(ec.max_str, hi); + return v >= lo && v <= hi; + } + + static bool extract_number(const s2::Item& item, double& out) { + switch (item.format()) { + case s2::Format::U1: out = std::get>(item.storage()).front(); return true; + case s2::Format::U2: out = std::get>(item.storage()).front(); return true; + case s2::Format::U4: out = std::get>(item.storage()).front(); return true; + case s2::Format::U8: out = static_cast(std::get>(item.storage()).front()); return true; + case s2::Format::I1: out = std::get>(item.storage()).front(); return true; + case s2::Format::I2: out = std::get>(item.storage()).front(); return true; + case s2::Format::I4: out = std::get>(item.storage()).front(); return true; + case s2::Format::I8: out = static_cast(std::get>(item.storage()).front()); return true; + case s2::Format::F4: out = std::get>(item.storage()).front(); return true; + case s2::Format::F8: out = std::get>(item.storage()).front(); return true; + default: return false; + } + } + + std::map by_id_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/store/event_reports.hpp b/include/secsgem/gem/store/event_reports.hpp new file mode 100644 index 0000000..6bbff89 --- /dev/null +++ b/include/secsgem/gem/store/event_reports.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "secsgem/secs2/item.hpp" + +namespace secsgem::gem { + +namespace s2 = secsgem::secs2; + +struct CollectionEvent { + uint32_t id; + std::string name; +}; + +struct Report { + uint32_t id; + std::vector vids; +}; + +struct ReportData { + uint32_t rptid; + std::vector values; +}; + +enum class DefineReportAck : uint8_t { + Accept = 0, + InsufficientSpace = 1, + InvalidFormat = 2, + RptidAlreadyDefined = 3, + InvalidVid = 5, +}; + +enum class LinkEventAck : uint8_t { + Accept = 0, + InsufficientSpace = 1, + InvalidFormat = 2, + UnknownCeid = 3, + UnknownRptid = 4, + CeidAlreadyLinked = 5, +}; + +enum class EnableEventAck : uint8_t { + Accept = 0, + UnknownCeid = 1, +}; + +// VID resolver: a callable that maps a VID (which may be an SVID or DVID) to +// its current value, or std::nullopt if unknown. Injected at S2F33 validation +// and again at compose_reports_for emission time; that way the subscription +// service stays pure data (no back-reference to the SVID / DVID stores). +using VidLookup = std::function(uint32_t)>; +using VidExists = std::function; + +// E30 §6.6 dynamic event reporting state: +// * the catalog of registered CEIDs (defined by the equipment), +// * the host-defined reports (RPTID -> VID list), +// * the CEID -> RPTID links, +// * and the set of CEIDs the host has enabled. +// +// Pure data; no IO. +class EventReportSubscriptions { + public: + // --- CEID catalog ----------------------------------------------------- + void register_event(CollectionEvent ce) { + by_ceid_.insert_or_assign(ce.id, std::move(ce)); + } + bool has_event(uint32_t ceid) const { return by_ceid_.count(ceid) > 0; } + std::vector all_events() const { + std::vector out; + out.reserve(by_ceid_.size()); + for (const auto& [_, e] : by_ceid_) out.push_back(e); + return out; + } + + // --- S2F33 define reports -------------------------------------------- + DefineReportAck define_reports( + const std::vector>>& rows, + const VidExists& vid_exists) { + if (rows.empty()) { + reports_.clear(); + links_.clear(); + return DefineReportAck::Accept; + } + for (const auto& [rptid, vids] : rows) { + if (vids.empty()) continue; + for (auto v : vids) + if (!vid_exists(v)) return DefineReportAck::InvalidVid; + } + for (const auto& [rptid, vids] : rows) { + if (vids.empty()) { + reports_.erase(rptid); + for (auto& [_, rpts] : links_) + rpts.erase(std::remove(rpts.begin(), rpts.end(), rptid), rpts.end()); + } else { + reports_.insert_or_assign(rptid, Report{rptid, vids}); + } + } + return DefineReportAck::Accept; + } + + std::vector all_reports() const { + std::vector out; + out.reserve(reports_.size()); + for (const auto& [_, r] : reports_) out.push_back(r); + return out; + } + + // --- S2F35 link event report ----------------------------------------- + LinkEventAck link_event_reports( + const std::vector>>& rows) { + if (rows.empty()) { + links_.clear(); + return LinkEventAck::Accept; + } + for (const auto& [ceid, rpts] : rows) { + if (!has_event(ceid)) return LinkEventAck::UnknownCeid; + for (auto r : rpts) + if (!reports_.count(r)) return LinkEventAck::UnknownRptid; + } + for (const auto& [ceid, rpts] : rows) { + if (rpts.empty()) links_.erase(ceid); + else links_[ceid] = rpts; + } + return LinkEventAck::Accept; + } + + // --- S2F37 enable / disable event ----------------------------------- + EnableEventAck enable_events(bool enable, const std::vector& ceids) { + if (ceids.empty()) { + if (enable) for (const auto& [id, _] : by_ceid_) enabled_.insert(id); + else enabled_.clear(); + return EnableEventAck::Accept; + } + for (auto id : ceids) + if (!has_event(id)) return EnableEventAck::UnknownCeid; + for (auto id : ceids) { + if (enable) enabled_.insert(id); + else enabled_.erase(id); + } + return EnableEventAck::Accept; + } + + bool is_enabled(uint32_t ceid) const { return enabled_.count(ceid) > 0; } + + // --- S6F11 emission -------------------------------------------------- + std::vector compose_for(uint32_t ceid, const VidLookup& lookup) const { + std::vector out; + auto it = links_.find(ceid); + if (it == 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 = lookup(vid); + rd.values.push_back(v ? *v : s2::Item::list({})); + } + out.push_back(std::move(rd)); + } + return out; + } + + private: + std::map by_ceid_; + std::map reports_; + std::map> links_; + std::set enabled_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/store/host_commands.hpp b/include/secsgem/gem/store/host_commands.hpp new file mode 100644 index 0000000..94073e8 --- /dev/null +++ b/include/secsgem/gem/store/host_commands.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "secsgem/secs2/item.hpp" + +namespace secsgem::gem { + +namespace s2 = secsgem::secs2; + +enum class HostCmdAck : uint8_t { + Accept = 0, + InvalidCommand = 1, + CannotDoNow = 2, + ParameterInvalid = 3, + AcceptedWillFinishLater = 4, + Rejected = 5, + InvalidObject = 6, +}; + +// One entry on S2F41. The messages catalog declares this +// struct external_struct: true so the codegen references it rather than +// redefining it. +struct CommandParameter { + std::string name; + s2::Item value; +}; + +class HostCommandRegistry { + public: + // Declarative effect, loaded from YAML. + struct Spec { + HostCmdAck ack = HostCmdAck::Accept; + std::optional emit_ceid; + std::optional set_alarm; + }; + + struct Result { + HostCmdAck ack = HostCmdAck::InvalidCommand; + std::optional emit_ceid; + std::optional set_alarm; + }; + + void register_command(std::string rcmd, Spec spec) { + by_rcmd_.insert_or_assign(std::move(rcmd), std::move(spec)); + } + bool has(const std::string& rcmd) const { return by_rcmd_.count(rcmd) > 0; } + Result dispatch(const std::string& rcmd, + const std::vector& /*params*/) const { + auto it = by_rcmd_.find(rcmd); + if (it == by_rcmd_.end()) return {HostCmdAck::InvalidCommand, std::nullopt, std::nullopt}; + return {it->second.ack, it->second.emit_ceid, it->second.set_alarm}; + } + + private: + std::map by_rcmd_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/store/recipes.hpp b/include/secsgem/gem/store/recipes.hpp new file mode 100644 index 0000000..79f1db5 --- /dev/null +++ b/include/secsgem/gem/store/recipes.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace secsgem::gem { + +enum class ProcessProgramAck : uint8_t { + Accept = 0, + PermissionNotGranted = 1, + LengthError = 2, + MatrixOverflow = 3, + PpidNotFound = 4, + ModeUnsupported = 5, + PerformanceError = 6, +}; + +class RecipeStore { + public: + void add(std::string ppid, std::string body) { + by_ppid_.insert_or_assign(std::move(ppid), std::move(body)); + } + std::optional get(const std::string& ppid) const { + auto it = by_ppid_.find(ppid); + if (it == by_ppid_.end()) return std::nullopt; + return it->second; + } + std::vector list() const { + std::vector out; + out.reserve(by_ppid_.size()); + for (const auto& [k, _] : by_ppid_) out.push_back(k); + return out; + } + ProcessProgramAck remove(const std::string& ppid) { + if (!by_ppid_.count(ppid)) return ProcessProgramAck::PpidNotFound; + by_ppid_.erase(ppid); + return ProcessProgramAck::Accept; + } + std::size_t size() const { return by_ppid_.size(); } + + private: + std::map by_ppid_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/store/status_variables.hpp b/include/secsgem/gem/store/status_variables.hpp new file mode 100644 index 0000000..f6e4abb --- /dev/null +++ b/include/secsgem/gem/store/status_variables.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "secsgem/secs2/item.hpp" + +namespace secsgem::gem { + +namespace s2 = secsgem::secs2; + +struct StatusVariable { + uint32_t id; + std::string name; + std::string units; + s2::Item value; +}; + +class StatusVariableStore { + public: + void add(StatusVariable sv) { + const uint32_t id = sv.id; + by_id_.insert_or_assign(id, std::move(sv)); + } + bool has(uint32_t id) const { return by_id_.count(id) > 0; } + std::optional get(uint32_t id) const { + auto it = by_id_.find(id); + if (it == by_id_.end()) return std::nullopt; + return it->second; + } + std::vector all() const { + std::vector out; + out.reserve(by_id_.size()); + for (const auto& [_, sv] : by_id_) out.push_back(sv); + return out; + } + void set_value(uint32_t id, s2::Item value) { + auto it = by_id_.find(id); + if (it != by_id_.end()) it->second.value = std::move(value); + } + std::optional value(uint32_t id) const { + auto it = by_id_.find(id); + if (it == by_id_.end()) return std::nullopt; + return it->second.value; + } + std::size_t size() const { return by_id_.size(); } + + private: + std::map by_id_; +}; + +// DVIDs (data variables) share storage shape with SVIDs; reuse the same class +// under a clearer alias so call sites read correctly. +struct DataVariable { + uint32_t id; + std::string name; + std::string units; + s2::Item value; +}; + +class DataVariableStore { + public: + void add(DataVariable dv) { by_id_.insert_or_assign(dv.id, std::move(dv)); } + std::optional get(uint32_t id) const { + auto it = by_id_.find(id); + if (it == by_id_.end()) return std::nullopt; + return it->second; + } + std::vector all() const { + std::vector out; + out.reserve(by_id_.size()); + for (const auto& [_, dv] : by_id_) out.push_back(dv); + return out; + } + void set_value(uint32_t id, s2::Item value) { + auto it = by_id_.find(id); + if (it != by_id_.end()) it->second.value = std::move(value); + } + std::optional value(uint32_t id) const { + auto it = by_id_.find(id); + if (it == by_id_.end()) return std::nullopt; + return it->second.value; + } + bool has(uint32_t id) const { return by_id_.count(id) > 0; } + + private: + std::map by_id_; +}; + +} // namespace secsgem::gem diff --git a/src/config/loader.cpp b/src/config/loader.cpp index 65e63cf..315b6d3 100644 --- a/src/config/loader.cpp +++ b/src/config/loader.cpp @@ -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(sv["id"].as()), sv["name"].as(), sv["units"] ? sv["units"].as() : "", @@ -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(ec["id"].as()), ec["name"].as(), ec["units"] ? ec["units"].as() : "", @@ -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(ce["id"].as()), ce["name"].as(), }); @@ -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(a["id"].as()), a["text"].as(), static_cast(a["category"].as()), @@ -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(), r["body"].as()); + model.recipes.add(r["id"].as(), r["body"].as()); } } 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(), path + " host_commands"); if (auto e = c["emit_ceid"]) spec.emit_ceid = static_cast(e.as()); if (auto a = c["set_alarm"]) spec.set_alarm = static_cast(a.as()); - model.register_command(c["name"].as(), spec); + model.commands.register_command(c["name"].as(), spec); } } diff --git a/src/gem/data_model.cpp b/src/gem/data_model.cpp deleted file mode 100644 index b9cd54f..0000000 --- a/src/gem/data_model.cpp +++ /dev/null @@ -1,339 +0,0 @@ -#include "secsgem/gem/data_model.hpp" - -#include -#include -#include -#include - -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 EquipmentDataModel::status_variable(uint32_t id) const { - auto it = svids_.find(id); - if (it == svids_.end()) return std::nullopt; - return it->second; -} -std::vector EquipmentDataModel::all_status_variables() const { - std::vector 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 EquipmentDataModel::data_variable(uint32_t id) const { - auto it = dvids_.find(id); - if (it == dvids_.end()) return std::nullopt; - return it->second; -} -std::vector EquipmentDataModel::all_data_variables() const { - std::vector 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 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 EquipmentDataModel::equipment_constant(uint32_t id) const { - auto it = ecids_.find(id); - if (it == ecids_.end()) return std::nullopt; - return it->second; -} -std::vector EquipmentDataModel::all_equipment_constants() const { - std::vector 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(now.time_since_epoch()) % 1000; - - std::tm tm{}; - gmtime_r(&t, &tm); - - std::array 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(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(-1)) return TimeAck::Error; - const std::time_t now = std::time(nullptr); - time_offset_seconds_ = static_cast(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& /*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 EquipmentDataModel::all_events() const { - std::vector out; - out.reserve(ceids_.size()); - for (const auto& [_, e] : ceids_) out.push_back(e); - return out; -} - -DefineReportAck EquipmentDataModel::define_reports( - const std::vector>>& 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>>& 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& 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 EquipmentDataModel::compose_reports_for(uint32_t ceid) const { - std::vector 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 EquipmentDataModel::all_reports() const { - std::vector 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 EquipmentDataModel::alarm(uint32_t alid) const { - auto it = alarms_.find(alid); - if (it == alarms_.end()) return std::nullopt; - return it->second; -} -std::vector EquipmentDataModel::all_alarms() const { - std::vector 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 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((it->second.severity_category & 0x7F) | 0x80); -} -std::optional 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(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 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 EquipmentDataModel::process_program_list() const { - std::vector 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 diff --git a/tests/test_data_model.cpp b/tests/test_data_model.cpp index 411a90f..113b3bf 100644 --- a/tests/test_data_model.cpp +++ b/tests/test_data_model.cpp @@ -4,96 +4,117 @@ using namespace secsgem::gem; +// ---- Status variables ---------------------------------------------------- + TEST_CASE("SVID add / get / list / set value") { - EquipmentDataModel m; - m.add_status_variable({1, "Clock", "", s2::Item::ascii("19700101000000")}); - m.add_status_variable({2, "EventsEnabled", "", s2::Item::boolean(true)}); + StatusVariableStore svids; + svids.add({1, "Clock", "", s2::Item::ascii("19700101000000")}); + svids.add({2, "EventsEnabled", "", s2::Item::boolean(true)}); - CHECK(m.status_variable(1).has_value()); - CHECK(m.status_variable(99).has_value() == false); - CHECK(m.all_status_variables().size() == 2); + CHECK(svids.get(1).has_value()); + CHECK_FALSE(svids.get(99).has_value()); + CHECK(svids.all().size() == 2); - m.set_status_value(2, s2::Item::boolean(false)); - CHECK(m.status_variable(2)->value == s2::Item::boolean(false)); + svids.set_value(2, s2::Item::boolean(false)); + CHECK(svids.get(2)->value == s2::Item::boolean(false)); } -TEST_CASE("ECID set rejects unknown id") { - EquipmentDataModel m; - m.add_equipment_constant({10, "MaxSpool", "msgs", s2::Item::u4(uint32_t{100}), - s2::Item::u4(uint32_t{100}), "0", "1000"}); - CHECK(m.set_equipment_constant_value(10, s2::Item::u4(uint32_t{50})) == - EquipmentAck::Accept); - CHECK(m.equipment_constant(10)->value == s2::Item::u4(uint32_t{50})); - CHECK(m.set_equipment_constant_value(999, s2::Item::u4(uint32_t{1})) == - EquipmentAck::Denied_UnknownEcid); +// ---- Equipment constants ------------------------------------------------- + +TEST_CASE("ECID set rejects unknown id and out-of-range values") { + EquipmentConstantStore ecids; + ecids.add({10, "MaxSpool", "msgs", s2::Item::u4(uint32_t{100}), + s2::Item::u4(uint32_t{100}), "0", "1000"}); + CHECK(ecids.set_value(10, s2::Item::u4(uint32_t{50})) == EquipmentAck::Accept); + CHECK(ecids.get(10)->value == s2::Item::u4(uint32_t{50})); + CHECK(ecids.set_value(999, s2::Item::u4(uint32_t{1})) == EquipmentAck::Denied_UnknownEcid); + + // Out-of-range rejected (closes COMPLIANCE.md gap). + CHECK(ecids.set_value(10, s2::Item::u4(uint32_t{5000})) == EquipmentAck::Denied_OutOfRange); + CHECK(ecids.get(10)->value == s2::Item::u4(uint32_t{50})); // unchanged } -TEST_CASE("clock string has 16 digit characters") { - EquipmentDataModel m; - auto s = m.current_time_string(); +TEST_CASE("ECID with no min/max accepts any value") { + EquipmentConstantStore ecids; + ecids.add({1, "n", "", s2::Item::u4(uint32_t{0}), s2::Item::u4(uint32_t{0}), "", ""}); + CHECK(ecids.set_value(1, s2::Item::u4(uint32_t{99999999})) == EquipmentAck::Accept); +} + +TEST_CASE("ECID range validation skipped for non-numeric formats") { + EquipmentConstantStore ecids; + ecids.add({1, "label", "", s2::Item::ascii("a"), s2::Item::ascii("a"), "0", "10"}); + CHECK(ecids.set_value(1, s2::Item::ascii("foo")) == EquipmentAck::Accept); +} + +// ---- Clock --------------------------------------------------------------- + +TEST_CASE("clock string has 16 characters") { + Clock c; + auto s = c.current_time_string(); REQUIRE(s.size() == 16); - for (char c : s) CHECK(c >= '0'); } TEST_CASE("set_time_string accepts well-formed and rejects malformed") { - EquipmentDataModel m; - CHECK(m.set_time_string("20260101000000") == TimeAck::Accept); - CHECK(m.set_time_string("2026010100000000") == TimeAck::Accept); - CHECK(m.set_time_string("not-a-date-12345") == TimeAck::Error); - CHECK(m.set_time_string("short") == TimeAck::Error); + Clock c; + CHECK(c.set_time_string("20260101000000") == TimeAck::Accept); + CHECK(c.set_time_string("2026010100000000") == TimeAck::Accept); + CHECK(c.set_time_string("not-a-date-12345") == TimeAck::Error); + CHECK(c.set_time_string("short") == TimeAck::Error); } -TEST_CASE("host command registry") { - EquipmentDataModel m; - m.register_command("START", {HostCmdAck::Accept, 300, std::nullopt}); - m.register_command("STOP", {HostCmdAck::CannotDoNow, std::nullopt, std::nullopt}); - m.register_command("FAULT", {HostCmdAck::Accept, std::nullopt, 1}); +// ---- Host command registry ---------------------------------------------- - CHECK(m.has_command("START")); - CHECK_FALSE(m.has_command("PAUSE")); +TEST_CASE("host command registry returns spec + result") { + HostCommandRegistry r; + r.register_command("START", {HostCmdAck::Accept, 300, std::nullopt}); + r.register_command("STOP", {HostCmdAck::CannotDoNow, std::nullopt, std::nullopt}); + r.register_command("FAULT", {HostCmdAck::Accept, std::nullopt, 1}); - auto start = m.dispatch_command("START", {}); + CHECK(r.has("START")); + CHECK_FALSE(r.has("PAUSE")); + + auto start = r.dispatch("START", {}); CHECK(start.ack == HostCmdAck::Accept); CHECK(start.emit_ceid.value_or(0) == 300); - CHECK_FALSE(start.set_alarm.has_value()); - auto stop = m.dispatch_command("STOP", {}); - CHECK(stop.ack == HostCmdAck::CannotDoNow); - - auto fault = m.dispatch_command("FAULT", {}); - CHECK(fault.ack == HostCmdAck::Accept); + auto fault = r.dispatch("FAULT", {}); CHECK(fault.set_alarm.value_or(0) == 1); - CHECK(m.dispatch_command("UNKNOWN", {}).ack == HostCmdAck::InvalidCommand); + CHECK(r.dispatch("UNKNOWN", {}).ack == HostCmdAck::InvalidCommand); } // ---- Event reports ------------------------------------------------------- -TEST_CASE("define report rejects unknown VID, accepts known VIDs") { - EquipmentDataModel m; - m.add_status_variable({1, "A", "", s2::Item::u4(uint32_t{10})}); - m.add_status_variable({2, "B", "", s2::Item::u4(uint32_t{20})}); +TEST_CASE("define reports rejects unknown VID") { + EventReportSubscriptions ev; + ev.register_event({100, "x"}); + auto exists = [](uint32_t id) { return id == 1 || id == 2; }; - CHECK(m.define_reports({{100, {1, 2}}}) == DefineReportAck::Accept); - CHECK(m.define_reports({{101, {1, 999}}}) == DefineReportAck::InvalidVid); + CHECK(ev.define_reports({{1000, {1, 2}}}, exists) == DefineReportAck::Accept); + CHECK(ev.define_reports({{1001, {1, 999}}}, exists) == DefineReportAck::InvalidVid); } -TEST_CASE("link / enable / compose: full event-report pipeline") { - EquipmentDataModel m; - m.add_status_variable({1, "ControlState", "", s2::Item::ascii("OnlineRemote")}); - m.add_status_variable({2, "Clock", "", s2::Item::ascii("19700101000000")}); - m.register_event({100, "ControlStateChanged"}); - m.register_event({200, "AlarmSetEvent"}); +TEST_CASE("full event-report pipeline") { + StatusVariableStore svids; + svids.add({1, "ControlState", "", s2::Item::ascii("OnlineRemote")}); + svids.add({2, "Clock", "", s2::Item::ascii("19700101000000")}); - REQUIRE(m.define_reports({{1000, {1}}, {1001, {1, 2}}}) == DefineReportAck::Accept); - REQUIRE(m.link_event_reports({{100, {1000, 1001}}, {200, {1001}}}) == LinkEventAck::Accept); - CHECK_FALSE(m.is_event_enabled(100)); + EventReportSubscriptions ev; + ev.register_event({100, "ControlStateChanged"}); + ev.register_event({200, "AlarmSetEvent"}); - REQUIRE(m.enable_events(true, {100}) == EnableEventAck::Accept); - CHECK(m.is_event_enabled(100)); - CHECK_FALSE(m.is_event_enabled(200)); + auto exists = [&](uint32_t id) { return svids.has(id); }; + auto value = [&](uint32_t id) { return svids.value(id); }; - auto reports = m.compose_reports_for(100); + REQUIRE(ev.define_reports({{1000, {1}}, {1001, {1, 2}}}, exists) == DefineReportAck::Accept); + REQUIRE(ev.link_event_reports({{100, {1000, 1001}}, {200, {1001}}}) == LinkEventAck::Accept); + CHECK_FALSE(ev.is_enabled(100)); + + REQUIRE(ev.enable_events(true, {100}) == EnableEventAck::Accept); + CHECK(ev.is_enabled(100)); + CHECK_FALSE(ev.is_enabled(200)); + + auto reports = ev.compose_for(100, value); REQUIRE(reports.size() == 2); CHECK(reports[0].rptid == 1000); REQUIRE(reports[0].values.size() == 1); @@ -103,75 +124,74 @@ TEST_CASE("link / enable / compose: full event-report pipeline") { } TEST_CASE("enable_events with empty CEID list enables all registered events") { - EquipmentDataModel m; - m.register_event({1, "a"}); - m.register_event({2, "b"}); - CHECK(m.enable_events(true, {}) == EnableEventAck::Accept); - CHECK(m.is_event_enabled(1)); - CHECK(m.is_event_enabled(2)); - CHECK(m.enable_events(false, {}) == EnableEventAck::Accept); - CHECK_FALSE(m.is_event_enabled(1)); + EventReportSubscriptions ev; + ev.register_event({1, "a"}); + ev.register_event({2, "b"}); + CHECK(ev.enable_events(true, {}) == EnableEventAck::Accept); + CHECK(ev.is_enabled(1)); + CHECK(ev.is_enabled(2)); + CHECK(ev.enable_events(false, {}) == EnableEventAck::Accept); + CHECK_FALSE(ev.is_enabled(1)); } TEST_CASE("link_event_reports rejects unknown CEID or RPTID") { - EquipmentDataModel m; - m.add_status_variable({1, "A", "", s2::Item::u4(uint32_t{0})}); - m.register_event({100, "x"}); - m.define_reports({{500, {1}}}); + EventReportSubscriptions ev; + ev.register_event({100, "x"}); + ev.define_reports({{500, {1}}}, [](uint32_t) { return true; }); - CHECK(m.link_event_reports({{999, {500}}}) == LinkEventAck::UnknownCeid); - CHECK(m.link_event_reports({{100, {999}}}) == LinkEventAck::UnknownRptid); - CHECK(m.link_event_reports({{100, {500}}}) == LinkEventAck::Accept); -} - -TEST_CASE("define_reports empty list clears reports and links") { - EquipmentDataModel m; - m.add_status_variable({1, "A", "", s2::Item::u4(uint32_t{0})}); - m.register_event({100, "x"}); - m.define_reports({{500, {1}}}); - m.link_event_reports({{100, {500}}}); - - CHECK(m.define_reports({}) == DefineReportAck::Accept); - CHECK(m.all_reports().empty()); - CHECK(m.compose_reports_for(100).empty()); + CHECK(ev.link_event_reports({{999, {500}}}) == LinkEventAck::UnknownCeid); + CHECK(ev.link_event_reports({{100, {999}}}) == LinkEventAck::UnknownRptid); + CHECK(ev.link_event_reports({{100, {500}}}) == LinkEventAck::Accept); } // ---- Alarms -------------------------------------------------------------- TEST_CASE("alarm enable / set / clear / list") { - EquipmentDataModel m; - m.add_alarm({1, "Chiller Temp High", 4}); - m.add_alarm({2, "Door Open", 1}); + AlarmRegistry r; + r.add({1, "Chiller Temp High", 4}); + r.add({2, "Door Open", 1}); - CHECK(m.set_alarm_enabled(1, true) == AlarmAck::Accept); - CHECK(m.alarm_enabled(1)); - CHECK(m.set_alarm_enabled(999, true) == AlarmAck::Error); + CHECK(r.set_enabled(1, true) == AlarmAck::Accept); + CHECK(r.enabled(1)); + CHECK(r.set_enabled(999, true) == AlarmAck::Error); - auto alcd_set = m.alarm_set(1); + auto alcd_set = r.set_active(1); REQUIRE(alcd_set.has_value()); CHECK((*alcd_set & 0x80) != 0); CHECK((*alcd_set & 0x7F) == 4); - CHECK(m.alarm_active(1)); + CHECK(r.active(1)); - auto alcd_clr = m.alarm_clear(1); + auto alcd_clr = r.clear_active(1); REQUIRE(alcd_clr.has_value()); CHECK((*alcd_clr & 0x80) == 0); - CHECK_FALSE(m.alarm_active(1)); + CHECK_FALSE(r.active(1)); - CHECK(m.all_alarms().size() == 2); + CHECK(r.all().size() == 2); } -// ---- Process programs --------------------------------------------------- +// ---- Process programs ---------------------------------------------------- -TEST_CASE("process program CRUD") { +TEST_CASE("recipe CRUD") { + RecipeStore r; + r.add("RECIPE-A", "step1\nstep2\n"); + r.add("RECIPE-B", "alt body"); + CHECK(r.list().size() == 2); + CHECK(r.get("RECIPE-A").value() == "step1\nstep2\n"); + CHECK_FALSE(r.get("UNKNOWN").has_value()); + + CHECK(r.remove("RECIPE-A") == ProcessProgramAck::Accept); + CHECK_FALSE(r.get("RECIPE-A").has_value()); + CHECK(r.remove("RECIPE-A") == ProcessProgramAck::PpidNotFound); +} + +// ---- Composite EquipmentDataModel --------------------------------------- + +TEST_CASE("EquipmentDataModel composes the seven stores") { EquipmentDataModel m; - m.add_process_program("RECIPE-A", "step1\nstep2\n"); - m.add_process_program("RECIPE-B", "alt body"); - CHECK(m.process_program_list().size() == 2); - CHECK(m.process_program("RECIPE-A").value() == "step1\nstep2\n"); - CHECK_FALSE(m.process_program("UNKNOWN").has_value()); - - CHECK(m.delete_process_program("RECIPE-A") == ProcessProgramAck::Accept); - CHECK_FALSE(m.process_program("RECIPE-A").has_value()); - CHECK(m.delete_process_program("RECIPE-A") == ProcessProgramAck::PpidNotFound); + m.svids.add({1, "Clock", "", s2::Item::ascii("")}); + m.dvids.add({2, "Temp", "C", s2::Item::u4(uint32_t{25})}); + CHECK(m.vid_exists(1)); + CHECK(m.vid_exists(2)); + CHECK_FALSE(m.vid_exists(3)); + CHECK(m.vid_value(2) == s2::Item::u4(uint32_t{25})); } diff --git a/tests/test_loader.cpp b/tests/test_loader.cpp index 0e13450..27934f8 100644 --- a/tests/test_loader.cpp +++ b/tests/test_loader.cpp @@ -40,29 +40,29 @@ TEST_CASE("equipment.yaml populates SVIDs, ECIDs, CEIDs, alarms, recipes, comman REQUIRE(desc.emit_on_control_change.has_value()); CHECK(*desc.emit_on_control_change == 100); - CHECK(m.all_status_variables().size() == 3); - CHECK(m.status_variable(1)->name == "ControlState"); - CHECK(m.status_variable(3)->value == s2::Item::boolean(true)); + CHECK(m.svids.all().size() == 3); + CHECK(m.svids.get(1)->name == "ControlState"); + CHECK(m.svids.get(3)->value == s2::Item::boolean(true)); - CHECK(m.all_equipment_constants().size() == 2); - CHECK(m.equipment_constant(10)->value == s2::Item::u4(uint32_t{1})); + CHECK(m.ecids.all().size() == 2); + CHECK(m.ecids.get(10)->value == s2::Item::u4(uint32_t{1})); - CHECK(m.all_events().size() == 3); - CHECK(m.has_event(100)); - CHECK(m.has_event(300)); + CHECK(m.events.all_events().size() == 3); + CHECK(m.events.has_event(100)); + CHECK(m.events.has_event(300)); - CHECK(m.all_alarms().size() == 2); - CHECK(m.alarm(1)->text == "Chiller Temp High"); - CHECK(m.alarm(1)->severity_category == 4); + CHECK(m.alarms.all().size() == 2); + CHECK(m.alarms.get(1)->text == "Chiller Temp High"); + CHECK(m.alarms.get(1)->severity_category == 4); - CHECK(m.process_program_list().size() == 2); - CHECK(m.process_program("RECIPE-A").value().find("CHAMBER ARGON") != std::string::npos); + CHECK(m.recipes.list().size() == 2); + CHECK(m.recipes.get("RECIPE-A").value().find("CHAMBER ARGON") != std::string::npos); - CHECK(m.has_command("START")); - auto start = m.dispatch_command("START", {}); + CHECK(m.commands.has("START")); + auto start = m.commands.dispatch("START", {}); CHECK(start.ack == gem::HostCmdAck::Accept); CHECK(start.emit_ceid.value_or(0) == 300); - auto fault = m.dispatch_command("FAULT", {}); + auto fault = m.commands.dispatch("FAULT", {}); CHECK(fault.set_alarm.value_or(0) == 1); }