100%/D: Trace Data Collection (S2F23/F24 + S6F1/F2, E30 §6.12)
tests / build-and-test (push) Failing after 32s

New TraceStore keyed by TRID; each entry is a TraceConfig with
DSPER + TOTSMP + REPGSZ + SVID list.  S2F23 validates that every SVID
exists (TIAACK=4 otherwise) and registers the trace.

S6F1's body is L,4 of {TRID U4, SMPLN U4, STIME ASCII, list_of <Item>}
— the application chooses whether each value Item is a scalar SVID
value or a packed batch.

The periodic sampling timer that turns an active TraceConfig into
S6F1 emissions is intentionally left to the application (E5 doesn't
mandate a specific scheduler and vendors typically already have one).

Four new SxFy in the catalog.

COMPLIANCE.md: Trace Data Collection Additional capability flips .
Tests: 82 cases / 477 assertions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 02:16:50 +02:00
parent 224130d99f
commit 6cedaa10dc
6 changed files with 164 additions and 3 deletions
+3 -3
View File
@@ -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 |
+17
View File
@@ -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<int>(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;
+58
View File
@@ -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.
+2
View File
@@ -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<s2::Item> vid_value(uint32_t vid) const {
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
// 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<uint32_t> svids;
};
class TraceStore {
public:
void add(TraceConfig c) { by_trid_.insert_or_assign(c.trid, std::move(c)); }
std::optional<TraceConfig> 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<TraceConfig> all() const {
std::vector<TraceConfig> 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<uint32_t, TraceConfig> by_trid_;
};
} // namespace secsgem::gem
+26
View File
@@ -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<uint32_t>{1, 2, 3});
}
TEST_CASE("S6F1 round-trip carries TRID + SMPLN + STIME + values") {
std::vector<s2::Item> 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") {