diff --git a/COMPLIANCE.md b/COMPLIANCE.md index 13c387f..93f65f6 100644 --- a/COMPLIANCE.md +++ b/COMPLIANCE.md @@ -84,7 +84,7 @@ Legend: | 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. | | Status Data Collection | ✅ | E30 §6.13| S1F3/F4, S1F11/F12 | | -| Alarm Management | 🟡 | E30 §6.14| S5F1/F2, S5F3/F4, S5F5/F6 | F1–F6 implemented; **S5F7/F8 list-enabled-alarms not implemented**. ALCD bit-7 set/cleared, lower-7 category supported. | +| 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. | | Equipment Constants | ✅ | E30 §6.16| S2F13/F14, S2F15/F16, S2F29/F30 | **EAC range validation against min/max is NOT performed** — set always accepts a known ECID; this would reject out-of-range with EAC=4 in a conformant equipment. | | Process Program Management | 🟡 | E30 §6.17| S7F3/F4, S7F5/F6, S7F19/F20 | Unformatted PP send/request/list. **No E42 enhanced PP**, **no S7F23/F24/F25/F26 PP verify**, **no PPID validation**. PPBODY treated as raw bytes (Binary item). | @@ -123,7 +123,7 @@ Legend: | S5F1 / S5F2 | E→H | ✅ | `messages.hpp` | ✅ round-trip | | S5F3 / S5F4 | H→E | ✅ | `messages.hpp` | ✅ round-trip | | S5F5 / S5F6 | H→E | ✅ | `messages.hpp` | ✅ in demo | -| S5F7 / S5F8 | H→E | ⬜ | — | — | +| S5F7 / S5F8 | H→E | ✅ | catalog | ✅ round-trip | | S6F1 / S6F2 | E→H | ⬜ | — | — | | S6F5 / S6F6 | H↔E | ⬜ | — | multi-block | | S6F7 / S6F8 | H↔E | ⬜ | — | multi-block | diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index 736c523..7beb178 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -335,6 +335,18 @@ int main(int argc, char** argv) { logfn(std::string("S5F3 -> S5F4 ACKC5=") + std::to_string(static_cast(ack))); return gem::s5f4_enable_alarm_ack(ack); }); + router.on(5, 7, [model, logfn](const s2::Message&) { + std::vector rows; + for (const auto& a : model->alarms.all()) { + if (!model->alarms.enabled(a.id)) continue; + const uint8_t alcd = (a.severity_category & 0x7F) | + static_cast(model->alarms.active(a.id) ? 0x80 : 0x00); + rows.push_back({alcd, a.id, a.text}); + } + logfn("S5F7 -> S5F8 (" + std::to_string(rows.size()) + " enabled)"); + return gem::s5f8_list_enabled_alarms_data(rows); + }); + router.on(5, 5, [model, logfn](const s2::Message& msg) { auto ids = gem::parse_u4_list_body(msg); std::vector alarms; diff --git a/data/messages.yaml b/data/messages.yaml index c9f5c24..c781c2f 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -503,6 +503,30 @@ messages: # S5F6 is hand-written (per-row ALCD requires an active-callback API). + # S5F7 / S5F8 — List Enabled Alarms. Same wire shape as S5F6 but only + # alarms with bit-7 of their stored "enabled" flag are returned. + - id: S5F7 + stream: 5 + function: 7 + w: true + builder: s5f7_list_enabled_alarms_request + + - id: S5F8 + stream: 5 + function: 8 + builder: s5f8_list_enabled_alarms_data + parser: parse_s5f8 + body: + kind: list_of + name: items + element: + kind: list + struct_name: AlarmListing + fields: + - {name: alcd, shape: {kind: scalar, item_type: BINARY_BYTE}} + - {name: alid, shape: {kind: scalar, item_type: U4}} + - {name: altx, shape: {kind: scalar, item_type: ASCII}} + # ===================================================================== # S6 — Data collection # ===================================================================== diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index 90e3c7a..35d48f9 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -151,6 +151,27 @@ TEST_CASE("S9F13 conversation timeout carries MEXP + EDID") { CHECK(parsed->edid == "EQUIP-01"); } +// ---- S5F7 / S5F8 List Enabled Alarms ----------------------------------- + +TEST_CASE("S5F7 header-only; S5F8 round-trips alarm listing") { + auto req = s5f7_list_enabled_alarms_request(); + CHECK(req.stream == 5); + CHECK(req.function == 7); + CHECK(req.reply_expected); + CHECK_FALSE(req.body.has_value()); + + std::vector rows = {{0x84, 1, "Chiller Temp High"}, + {0x01, 2, "Door Open"}}; + auto m = s5f8_list_enabled_alarms_data(rows); + auto parsed = parse_s5f8(m); + REQUIRE(parsed.has_value()); + REQUIRE(parsed->size() == 2); + CHECK((*parsed)[0].alcd == 0x84); + CHECK((*parsed)[0].alid == 1); + CHECK((*parsed)[1].alcd == 0x01); + CHECK((*parsed)[1].altx == "Door Open"); +} + // ---- S1F19 / S1F20 Get GEM Compliance ---------------------------------- TEST_CASE("S1F19 is header-only and S1F20 round-trips capability list") {