#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:
@@ -1,283 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<s2::Item> 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 <CPNAME, CPVAL> 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<uint32_t> 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<s2::Item> 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<uint32_t> emit_ceid;
|
||||
std::optional<uint32_t> set_alarm;
|
||||
};
|
||||
|
||||
struct CommandResult {
|
||||
HostCmdAck ack = HostCmdAck::InvalidCommand;
|
||||
std::optional<uint32_t> emit_ceid;
|
||||
std::optional<uint32_t> set_alarm;
|
||||
};
|
||||
|
||||
// --- SVID ---------------------------------------------------------------
|
||||
void add_status_variable(StatusVariable sv);
|
||||
std::optional<StatusVariable> status_variable(uint32_t id) const;
|
||||
std::vector<StatusVariable> all_status_variables() const;
|
||||
void set_status_value(uint32_t id, s2::Item value);
|
||||
|
||||
// --- DVID ---------------------------------------------------------------
|
||||
void add_data_variable(DataVariable dv);
|
||||
std::optional<DataVariable> data_variable(uint32_t id) const;
|
||||
std::vector<DataVariable> 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<s2::Item> vid_value(uint32_t vid) const;
|
||||
bool vid_exists(uint32_t vid) const;
|
||||
|
||||
// --- ECID ---------------------------------------------------------------
|
||||
void add_equipment_constant(EquipmentConstant ec);
|
||||
std::optional<EquipmentConstant> equipment_constant(uint32_t id) const;
|
||||
std::vector<EquipmentConstant> 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<CommandParam>& 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<CollectionEvent> 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<std::pair<uint32_t, std::vector<uint32_t>>>& 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<std::pair<uint32_t, std::vector<uint32_t>>>& rows) {
|
||||
return events.define_reports(rows, [this](uint32_t vid) { return vid_exists(vid); });
|
||||
}
|
||||
LinkEventAck link_event_reports(
|
||||
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& links);
|
||||
|
||||
// S2F37: enable/disable events. Empty CEID list applies to *all*
|
||||
// registered events.
|
||||
EnableEventAck enable_events(bool enable, const std::vector<uint32_t>& ceids);
|
||||
|
||||
bool is_event_enabled(uint32_t ceid) const;
|
||||
|
||||
// Compose the report data for a CEID emission (S6F11 body's report list).
|
||||
std::vector<ReportData> compose_reports_for(uint32_t ceid) const;
|
||||
|
||||
std::vector<Report> all_reports() const;
|
||||
|
||||
// --- Alarms -------------------------------------------------------------
|
||||
void add_alarm(Alarm a);
|
||||
std::optional<Alarm> alarm(uint32_t alid) const;
|
||||
std::vector<Alarm> 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<uint8_t> alarm_set(uint32_t alid);
|
||||
std::optional<uint8_t> 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<std::string> process_program(const std::string& ppid) const;
|
||||
std::vector<std::string> process_program_list() const;
|
||||
ProcessProgramAck delete_process_program(const std::string& ppid);
|
||||
|
||||
private:
|
||||
std::map<uint32_t, StatusVariable> svids_;
|
||||
std::map<uint32_t, DataVariable> dvids_;
|
||||
std::map<uint32_t, EquipmentConstant> ecids_;
|
||||
std::int64_t time_offset_seconds_ = 0;
|
||||
std::map<std::string, CommandSpec> commands_;
|
||||
|
||||
std::map<uint32_t, CollectionEvent> ceids_;
|
||||
std::map<uint32_t, Report> reports_;
|
||||
std::map<uint32_t, std::vector<uint32_t>> ce_links_; // CEID -> RPTID list
|
||||
std::set<uint32_t> events_enabled_;
|
||||
|
||||
std::map<uint32_t, Alarm> alarms_;
|
||||
std::set<uint32_t> alarms_enabled_;
|
||||
std::set<uint32_t> alarms_active_;
|
||||
|
||||
std::map<std::string, std::string> process_programs_;
|
||||
const std::vector<std::pair<uint32_t, std::vector<uint32_t>>>& rows) {
|
||||
return events.link_event_reports(rows);
|
||||
}
|
||||
EnableEventAck enable_events(bool enable, const std::vector<uint32_t>& ceids) {
|
||||
return events.enable_events(enable, ceids);
|
||||
}
|
||||
bool is_event_enabled(uint32_t ceid) const { return events.is_enabled(ceid); }
|
||||
std::vector<ReportData> compose_reports_for(uint32_t ceid) const {
|
||||
return events.compose_for(ceid, [this](uint32_t vid) { return vid_value(vid); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
|
||||
@@ -127,4 +127,17 @@ inline s2::Message s5f6_list_alarms_data(const std::vector<Alarm>& 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
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<Alarm> get(uint32_t id) const {
|
||||
auto it = by_id_.find(id);
|
||||
if (it == by_id_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<Alarm> all() const {
|
||||
std::vector<Alarm> 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<uint8_t> 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<uint8_t>((it->second.severity_category & 0x7F) | 0x80);
|
||||
}
|
||||
std::optional<uint8_t> 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<uint8_t>(it->second.severity_category & 0x7F);
|
||||
}
|
||||
bool active(uint32_t id) const { return active_.count(id) > 0; }
|
||||
|
||||
private:
|
||||
std::map<uint32_t, Alarm> by_id_;
|
||||
std::set<uint32_t> enabled_;
|
||||
std::set<uint32_t> active_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
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<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());
|
||||
}
|
||||
|
||||
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<std::time_t>(-1)) return TimeAck::Error;
|
||||
offset_seconds_ = static_cast<std::int64_t>(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
|
||||
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#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<EquipmentConstant> get(uint32_t id) const {
|
||||
auto it = by_id_.find(id);
|
||||
if (it == by_id_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<EquipmentConstant> all() const {
|
||||
std::vector<EquipmentConstant> 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<std::vector<uint8_t>>(item.storage()).front(); return true;
|
||||
case s2::Format::U2: out = std::get<std::vector<uint16_t>>(item.storage()).front(); return true;
|
||||
case s2::Format::U4: out = std::get<std::vector<uint32_t>>(item.storage()).front(); return true;
|
||||
case s2::Format::U8: out = static_cast<double>(std::get<std::vector<uint64_t>>(item.storage()).front()); return true;
|
||||
case s2::Format::I1: out = std::get<std::vector<int8_t>>(item.storage()).front(); return true;
|
||||
case s2::Format::I2: out = std::get<std::vector<int16_t>>(item.storage()).front(); return true;
|
||||
case s2::Format::I4: out = std::get<std::vector<int32_t>>(item.storage()).front(); return true;
|
||||
case s2::Format::I8: out = static_cast<double>(std::get<std::vector<int64_t>>(item.storage()).front()); return true;
|
||||
case s2::Format::F4: out = std::get<std::vector<float>>(item.storage()).front(); return true;
|
||||
case s2::Format::F8: out = std::get<std::vector<double>>(item.storage()).front(); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<uint32_t, EquipmentConstant> by_id_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<uint32_t> vids;
|
||||
};
|
||||
|
||||
struct ReportData {
|
||||
uint32_t rptid;
|
||||
std::vector<s2::Item> 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<std::optional<s2::Item>(uint32_t)>;
|
||||
using VidExists = std::function<bool(uint32_t)>;
|
||||
|
||||
// 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<CollectionEvent> all_events() const {
|
||||
std::vector<CollectionEvent> 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<std::pair<uint32_t, std::vector<uint32_t>>>& 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<Report> all_reports() const {
|
||||
std::vector<Report> 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<std::pair<uint32_t, std::vector<uint32_t>>>& 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<uint32_t>& 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<ReportData> compose_for(uint32_t ceid, const VidLookup& lookup) const {
|
||||
std::vector<ReportData> 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<uint32_t, CollectionEvent> by_ceid_;
|
||||
std::map<uint32_t, Report> reports_;
|
||||
std::map<uint32_t, std::vector<uint32_t>> links_;
|
||||
std::set<uint32_t> enabled_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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 <CPNAME, CPVAL> 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<uint32_t> emit_ceid;
|
||||
std::optional<uint32_t> set_alarm;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
HostCmdAck ack = HostCmdAck::InvalidCommand;
|
||||
std::optional<uint32_t> emit_ceid;
|
||||
std::optional<uint32_t> 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<CommandParameter>& /*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<std::string, Spec> by_rcmd_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> 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<std::string> list() const {
|
||||
std::vector<std::string> 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<std::string, std::string> by_ppid_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<StatusVariable> get(uint32_t id) const {
|
||||
auto it = by_id_.find(id);
|
||||
if (it == by_id_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<StatusVariable> all() const {
|
||||
std::vector<StatusVariable> 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<s2::Item> 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<uint32_t, StatusVariable> 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<DataVariable> get(uint32_t id) const {
|
||||
auto it = by_id_.find(id);
|
||||
if (it == by_id_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
std::vector<DataVariable> all() const {
|
||||
std::vector<DataVariable> 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<s2::Item> 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<uint32_t, DataVariable> by_id_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
Reference in New Issue
Block a user