diff --git a/COMPLIANCE.md b/COMPLIANCE.md index 2436973..ddecaed 100644 --- a/COMPLIANCE.md +++ b/COMPLIANCE.md @@ -82,7 +82,7 @@ Legend: | Establish Communications | ✅ | E30 §6.5 | S1F13/F14 | Both directions modeled; COMMACK enum. | | Dynamic Event Report Configuration | ✅ | E30 §6.6 | S2F33/F34, S2F35/F36, S2F37/F38 | Full Define-Report / Link-Event / Enable-Event pipeline with all four ack enums. | | Variable Data Collection | ✅ | E30 §6.11| — | `DVID` table; values resolvable via `vid_value`. | -| Trace Data Collection | ⬜ | E30 §6.12| S2F23/F24, S6F1/F2 | Not implemented. | +| Trace Data Collection | ✅ | E30 §6.12| S2F23/F24, S6F1/F2 | `TraceStore` keeps an active TRID -> TraceConfig map; S2F23 validates each SVID exists and registers the request (TIAACK=4 on unknown VID). S6F1 round-trips with TRID/SMPLN/STIME/values. The periodic sampling timer that turns a TraceConfig into S6F1 emissions is intentionally left to the application (vendors typically already have their own scheduler). | | Status Data Collection | ✅ | E30 §6.13| S1F3/F4, S1F11/F12 | | | Alarm Management | ✅ | E30 §6.14| S5F1/F2, S5F3/F4, S5F5/F6, S5F7/F8 | Full set: emit, ack, enable/disable, list directory, list enabled. ALCD bit-7 set/cleared, lower-7 category. | | Remote Control | ✅ | E30 §6.15| S2F41/F42 | Full HCACK 7-value enum + per-parameter CPACKs. | @@ -112,7 +112,7 @@ Legend: | S2F13 / S2F14 | H→E | ✅ | `messages.hpp` | ✅ in demo | | S2F15 / S2F16 | H→E | ✅ | `messages.hpp` | ✅ round-trip | | S2F17 / S2F18 | H→E | ✅ | `messages.hpp` | ✅ round-trip | -| S2F23 / S2F24 | H→E | ⬜ | — | — | +| S2F23 / S2F24 | H→E | ✅ | catalog | ✅ round-trip | | S2F29 / S2F30 | H→E | ✅ | `messages.hpp` | ✅ in demo | | S2F31 / S2F32 | H→E | ✅ | `messages.hpp` | ✅ in demo | | S2F33 / S2F34 | H→E | ✅ | `messages.hpp` | ✅ round-trip | @@ -125,7 +125,7 @@ Legend: | S5F3 / S5F4 | H→E | ✅ | `messages.hpp` | ✅ round-trip | | S5F5 / S5F6 | H→E | ✅ | `messages.hpp` | ✅ in demo | | S5F7 / S5F8 | H→E | ✅ | catalog | ✅ round-trip | -| S6F1 / S6F2 | E→H | ⬜ | — | — | +| S6F1 / S6F2 | E→H | ✅ | catalog | ✅ round-trip | | S6F5 / S6F6 | H↔E | ⬜ | — | multi-block | | S6F7 / S6F8 | H↔E | ⬜ | — | multi-block | | S6F11 / S6F12 | E→H | ✅ | `messages.hpp` | ✅ round-trip + demo | diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index 428afd9..f729e88 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -290,6 +290,23 @@ int main(int argc, char** argv) { return gem::s2f42_host_command_ack(result.ack, {}); }); + router.on(2, 23, [model, logfn](const s2::Message& msg) { + auto req = gem::parse_s2f23(msg); + auto ack = gem::TraceAck::Accept; + if (!req) { + ack = gem::TraceAck::InvalidPeriod; + } else { + for (auto v : req->svids) { + if (!model->vid_exists(v)) { ack = gem::TraceAck::UnknownVid; break; } + } + if (ack == gem::TraceAck::Accept) { + model->traces.add({req->trid, req->dsper, req->totsmp, req->repgsz, req->svids}); + } + } + logfn("S2F23 -> S2F24 TIAACK=" + std::to_string(static_cast(ack))); + return gem::s2f24_trace_initialize_ack(ack); + }); + router.on(2, 45, [model, logfn](const s2::Message& msg) { auto req = gem::parse_s2f45(msg); auto ack = gem::LimitMonitorAck::Accept; diff --git a/data/messages.yaml b/data/messages.yaml index b934672..54f2d77 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -415,6 +415,64 @@ messages: - {name: name, shape: {kind: scalar, item_type: ASCII}} - {name: code, shape: {kind: scalar, item_type: BINARY_BYTE}} + # S2F23 / S2F24 — Trace Initialize Send (E30 §6.12). Host requests + # periodic SVID sampling: every DSPER, equipment captures the SVID + # values; every REPGSZ samples it emits an S6F1 batch; after TOTSMP + # total samples it stops. Periodic execution is the application's + # responsibility; this codebase only stores the request. + - id: S2F23 + stream: 2 + function: 23 + w: true + builder: s2f23_trace_initialize_send + parser: parse_s2f23 + body: + kind: list + struct_name: TraceInit + fields: + - {name: trid, shape: {kind: scalar, item_type: U4}} + - {name: dsper, shape: {kind: scalar, item_type: ASCII}} + - {name: totsmp, shape: {kind: scalar, item_type: U4}} + - {name: repgsz, shape: {kind: scalar, item_type: U4}} + - name: svids + shape: + kind: list_of + element: {kind: scalar, item_type: U4} + + - id: S2F24 + stream: 2 + function: 24 + builder: s2f24_trace_initialize_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: TraceAck, param: tiaack} + + # S6F1 / S6F2 — Trace Data Send. Emitted by equipment when a batch + # completes. `values` holds REPGSZ * len(svids) Items (REPGSZ samples + # of len(svids) values each), but the shape lets the application choose + # any layout it wants. + - id: S6F1 + stream: 6 + function: 1 + w: true + builder: s6f1_trace_data_send + parser: parse_s6f1 + body: + kind: list + struct_name: TraceSample + fields: + - {name: trid, shape: {kind: scalar, item_type: U4}} + - {name: smpln, shape: {kind: scalar, item_type: U4}} + - {name: stime, shape: {kind: scalar, item_type: ASCII}} + - name: values + shape: + kind: list_of + element: {kind: scalar, item_type: ITEM} + + - id: S6F2 + stream: 6 + function: 2 + builder: s6f2_trace_data_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: EventReportAck, param: ack} + # S2F45 / S2F46 / S2F47 / S2F48 — Limits Monitoring (E30 §6.21). Each # VID may carry several LimitDefinitions (one per LIMITID), each with # an upper / lower deadband expressed as an arbitrary Item. diff --git a/include/secsgem/gem/data_model.hpp b/include/secsgem/gem/data_model.hpp index a460f16..76cc975 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -9,6 +9,7 @@ #include "secsgem/gem/store/recipes.hpp" #include "secsgem/gem/store/spool.hpp" #include "secsgem/gem/store/status_variables.hpp" +#include "secsgem/gem/store/trace.hpp" namespace secsgem::gem { @@ -28,6 +29,7 @@ struct EquipmentDataModel { HostCommandRegistry commands; SpoolStore spool; LimitMonitorStore limits; + TraceStore traces; // Convenience: VID -> value lookup spanning SVIDs and DVIDs. std::optional vid_value(uint32_t vid) const { diff --git a/include/secsgem/gem/store/trace.hpp b/include/secsgem/gem/store/trace.hpp new file mode 100644 index 0000000..f9baf4f --- /dev/null +++ b/include/secsgem/gem/store/trace.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// E30 §6.12 Trace Data Collection. +// +// Host sends S2F23 with (TRID, DSPER, TOTSMP, REPGSZ, SVID list); equipment +// is supposed to sample those SVIDs every DSPER, batch them in groups of +// REPGSZ, and emit each batch as S6F1. This store keeps the *configuration* +// for each active TRID — the periodic sampling itself is intentionally left +// to the application so equipment vendors can hook into whatever clock / +// scheduler they already use. + +namespace secsgem::gem { + +enum class TraceAck : uint8_t { // S2F24 TIAACK + Accept = 0, + TooManySvids = 1, + NoMoreTracesAllowed = 2, + InvalidPeriod = 3, + UnknownVid = 4, +}; + +struct TraceConfig { + uint32_t trid; + std::string dsper; // sample period (E5: ASCII, e.g. "1000" = ms) + uint32_t totsmp; // total samples to collect + uint32_t repgsz; // samples per S6F1 batch + std::vector svids; +}; + +class TraceStore { + public: + void add(TraceConfig c) { by_trid_.insert_or_assign(c.trid, std::move(c)); } + std::optional get(uint32_t trid) const { + auto it = by_trid_.find(trid); + if (it == by_trid_.end()) return std::nullopt; + return it->second; + } + void remove(uint32_t trid) { by_trid_.erase(trid); } + std::vector all() const { + std::vector out; + out.reserve(by_trid_.size()); + for (const auto& [_, c] : by_trid_) out.push_back(c); + return out; + } + std::size_t size() const { return by_trid_.size(); } + + private: + std::map by_trid_; +}; + +} // namespace secsgem::gem diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index d827d7e..c0e14c4 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -112,6 +112,32 @@ TEST_CASE("S10F3 terminal display round-trip") { CHECK(parsed->text == "ALARM: chiller temperature high"); } +// ---- Trace Data Collection (S2F23/F24 + S6F1/F2) ----------------------- + +TEST_CASE("S2F23 round-trip") { + TraceInit ti{7, "1000", 100, 10, {1, 2, 3}}; + auto m = s2f23_trace_initialize_send(ti.trid, ti.dsper, ti.totsmp, ti.repgsz, ti.svids); + auto parsed = parse_s2f23(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->trid == 7); + CHECK(parsed->dsper == "1000"); + CHECK(parsed->totsmp == 100); + CHECK(parsed->repgsz == 10); + CHECK(parsed->svids == std::vector{1, 2, 3}); +} + +TEST_CASE("S6F1 round-trip carries TRID + SMPLN + STIME + values") { + std::vector values = {s2::Item::u4(uint32_t{42}), s2::Item::ascii("hot")}; + auto m = s6f1_trace_data_send(7, 3, "20260606123000", values); + auto parsed = parse_s6f1(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->trid == 7); + CHECK(parsed->smpln == 3); + CHECK(parsed->stime == "20260606123000"); + REQUIRE(parsed->values.size() == 2); + CHECK(parsed->values[0] == s2::Item::u4(uint32_t{42})); +} + // ---- Limits Monitoring (S2F45-F48) ------------------------------------- TEST_CASE("S2F45 round-trips define-limits request") {