#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:
@@ -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
|
||||
Reference in New Issue
Block a user