diff --git a/COMPLIANCE.md b/COMPLIANCE.md index ddecaed..3284709 100644 --- a/COMPLIANCE.md +++ b/COMPLIANCE.md @@ -92,7 +92,7 @@ Legend: | Equipment Terminal Services | 🟡 | E30 §6.19| S10F1/F2, S10F3/F4 | Single-line only. **S10F5/F6 multi-block, S10F7 broadcast not implemented.** | | Clock | ✅ | E30 §6.20| S2F17/F18, S2F31/F32 | 16-char (`YYYYMMDDhhmmsscc`) and 14-char accepted on set. | | Limits Monitoring | ✅ | E30 §6.21| S2F45/F46, S2F47/F48 | `LimitMonitorStore` keyed by VID with multiple `LimitDefinition` (LIMITID + upper/lower as arbitrary Items). Server S2F45 validates each VID exists (returns VLAACK=4 otherwise) and stores definitions; S2F47 returns them. Crossing-detection + CEID emission is intentionally left to the application's set_value path (E30 §6.21 is permissive about *how* the equipment detects crossings). | -| Spooling | 🟡 | E30 §6.22| S2F43/F44, S6F23/F24 | Spoolable-streams config (S2F43/F44) + spool request transmit/purge (S6F23/F24) implemented; SpoolStore queues equipment-initiated primaries when the host is unreachable or force-spool is on, drains FIFO on S6F23 transmit, purges on S6F23 purge. **Not implemented**: S6F25/F26 spooled-data-ready notification, automatic spool activation tied to HSMS NOT-SELECTED, persistent (on-disk) spool. | +| Spooling | ✅ | E30 §6.22| S2F43/F44, S6F23/F24, S6F25/F26 | `SpoolStore` queues equipment-initiated primaries when the host is unreachable (or `force_spool` is on in the demo), drains FIFO on S6F23 transmit, purges on S6F23 purge. Server auto-emits **S6F25** on entering SELECTED whenever the spool is non-empty (the canonical re-SELECT notification path); host acks via S6F26. Demo doesn't drop TCP so re-SELECT doesn't fire there, but the wiring is correct. **Not implemented**: persistent (on-disk) spool — equipment restart still loses queued events. | | Control | ✅ | E30 §6.2 | — | See Fundamental. | --- @@ -132,7 +132,7 @@ Legend: | S6F15 / S6F16 | H→E | ⬜ | — | event report request | | S2F43 / S2F44 | H→E | ✅ | catalog | ✅ round-trip + demo | | S6F23 / S6F24 | H→E | ✅ | catalog | ✅ round-trip + demo | -| S6F25 / S6F26 | spool | ⬜ | — | spool-data-ready notification | +| S6F25 / S6F26 | E→H | ✅ | catalog + server | ✅ round-trip + auto-emitted on re-SELECT | | S7F3 / S7F4 | H→E | ✅ | `messages.hpp` | ✅ round-trip | | S7F5 / S7F6 | H→E | ✅ | `messages.hpp` | ✅ in demo | | S7F19 / S7F20 | H→E | ✅ | `messages.hpp` | ✅ round-trip + demo | @@ -189,11 +189,9 @@ Unit tests: **63 cases / 278 assertions pass** (`docker compose run --rm tests`) The honest list, in priority order: -1. **Finish spooling**: S6F25/F26 spooled-data-ready notification, plus - automatic activation when HSMS goes NOT-SELECTED (and automatic - notification on re-SELECT) so the host doesn't have to manually flip - the test-only `force_spool` flag. Optional: persistent on-disk spool - so equipment restarts don't lose queued events. +1. *(done in this revision)*: S6F25/F26 + auto-trigger on re-SELECT. + Optional remaining: persistent on-disk spool so equipment restarts + don't lose queued events. 2. *(done in this revision)*: S9F3/F5/F11 — all S9 conditions now auto-emit. 3. *(done in this revision)*: S1F19/F20 + S1F21/F22 — Documentation. 4. **Implement EC range validation** in `set_equipment_constant_value` diff --git a/apps/secs_client.cpp b/apps/secs_client.cpp index 89cf211..c0be4ec 100644 --- a/apps/secs_client.cpp +++ b/apps/secs_client.cpp @@ -96,6 +96,12 @@ int main(int argc, char** argv) { } return gem::s6f12_event_report_ack(gem::EventReportAck::Accept); } + // S6F25: spool-data-ready notification (E30 §6.22). + if (msg.stream == 6 && msg.function == 25) { + auto n = gem::parse_s6f25(msg); + logfn("SPOOL READY: " + std::to_string(n.value_or(0)) + " queued messages"); + return gem::s6f26_spool_data_ready_ack(gem::EventReportAck::Accept); + } // S5F1: alarm send from equipment. if (msg.stream == 5 && msg.function == 1) { auto a = gem::parse_s5f1(msg); diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index f729e88..4f449b8 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -435,13 +435,26 @@ int main(int argc, char** argv) { logfn("registered " + std::to_string(router.size()) + " (stream,function) handlers"); // ---- Wire the router into accepted connections ----------------------- - server.on_connection([sm, model, logfn, active_conn, &router, desc]( + server.on_connection([&io, sm, model, logfn, active_conn, &router, desc]( std::shared_ptr conn) { *active_conn = conn; conn->set_closed_handler([active_conn](const std::string&) { active_conn->reset(); }); - conn->set_selected_handler([logfn, sm]() { + // E30 §6.22: on entering SELECTED, if there's spooled data, notify + // the host via S6F25 so it can decide (S6F23 Transmit vs Purge). + // Our happy-path demo never drops the link so this branch doesn't + // fire there — but the wiring is the canonical re-SELECT trigger. + conn->set_selected_handler([logfn, sm, model, &io, active_conn]() { logfn(std::string("host is online; control=") + gem::control_state_name(sm->state())); + if (model->spool.size() == 0) return; + asio::post(io, [active_conn, model, logfn]() { + auto c = active_conn->lock(); + if (!c) return; + const uint32_t n = static_cast(model->spool.size()); + logfn("spool: notifying host of " + std::to_string(n) + " queued messages"); + c->send_request(gem::s6f25_spool_data_ready(n), + [](std::error_code, const s2::Message&) {}); + }); }); // Wrap router.dispatch so we can emit S9F3 / S9F5 when an inbound diff --git a/data/messages.yaml b/data/messages.yaml index 54f2d77..333589d 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -709,6 +709,23 @@ messages: builder: s6f24_request_spool_data_ack body: {kind: scalar, item_type: BINARY_BYTE, enum: SpoolRequestAck, param: rsda} + # S6F25 / S6F26 — Spool data ready notification (equipment-initiated; + # fires on re-SELECT when there's queued data). Host acks with S6F26; + # then host decides whether to issue S6F23(Transmit) or S6F23(Purge). + - id: S6F25 + stream: 6 + function: 25 + w: true + builder: s6f25_spool_data_ready + parser: parse_s6f25 + body: {kind: scalar, item_type: U4, param: num_msg} + + - id: S6F26 + stream: 6 + function: 26 + builder: s6f26_spool_data_ready_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: EventReportAck, param: ack} + # ===================================================================== # S7 — Process programs # ===================================================================== diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index c0e14c4..e77037c 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -112,6 +112,18 @@ TEST_CASE("S10F3 terminal display round-trip") { CHECK(parsed->text == "ALARM: chiller temperature high"); } +// ---- Spool data ready (S6F25/F26) -------------------------------------- + +TEST_CASE("S6F25 carries NUM-MSG; S6F26 ack round-trip") { + auto m = s6f25_spool_data_ready(42); + auto n = parse_s6f25(m); + REQUIRE(n.has_value()); + CHECK(*n == 42); + CHECK(m.reply_expected); + + CHECK(*ack_byte(s6f26_spool_data_ready_ack(EventReportAck::Accept)) == 0); +} + // ---- Trace Data Collection (S2F23/F24 + S6F1/F2) ----------------------- TEST_CASE("S2F23 round-trip") {