100%/D: Trace Data Collection (S2F23/F24 + S6F1/F2, E30 §6.12)
tests / build-and-test (push) Failing after 32s
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:
+3
-3
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user