diff --git a/docs/13_e30_gem.md b/docs/13_e30_gem.md index dedb497..43fa748 100644 --- a/docs/13_e30_gem.md +++ b/docs/13_e30_gem.md @@ -146,7 +146,7 @@ these isn't GEM-compliant, end of story. | State models | — | `ControlStateMachine`, `CommunicationStateMachine` | | Equipment Processing States | — | `ControlTransitionTable` (vendor supplies concrete states) | | Host-Initiated S1F13/F14 | S1F13 / S1F14 | `gem::CommunicationStateMachine` | -| Event Notification | S6F11 / S6F12 | `EventStore` + `EquipmentDataModel::compose_reports_for` | +| Event Notification | S6F11 / S6F12 | `EventReportSubscriptions` + `EquipmentDataModel::compose_reports_for` | | On-Line Identification | S1F1 / S1F2 | Router handler in `apps/secs_server.cpp` | | Error Messages | S9F1/F3/F5/F7/F9/F11 | `Connection::emit_s9` + `Router::dispatch_with_s9` | | Documentation | S1F19/F20, S1F21/F22, S1F23/F24 | `gem::compliance` / namelist handlers | @@ -166,17 +166,17 @@ the SEMI spec, but mandatory for procurement." | Additional | Messages | Code | |---------------------------------------|---------------------------------------------------------------------------|-----------------------------------------------| | Establish Communications | S1F13/F14 | `CommunicationStateMachine` (also in Fundamentals) | -| Dynamic Event Report Configuration | S2F33/F34, S2F35/F36, S2F37/F38 | `ReportStore`, `EventStore` | +| Dynamic Event Report Configuration | S2F33/F34, S2F35/F36, S2F37/F38 | `EventReportSubscriptions` | | Variable Data Collection | S1F21/F22 + DVID values via `vid_value` | `DataVariableStore` | | Trace Data Collection | S2F23/F24, S6F1/F2 | `TraceStore` | -| Status Data Collection | S1F3/F4, S1F11/F12 | `SvidStore` | -| Alarm Management | S5F1/F2, S5F3/F4, S5F5/F6, S5F7/F8 | `AlarmStore`, `AlarmDispatcher` | +| Status Data Collection | S1F3/F4, S1F11/F12 | `StatusVariableStore` | +| Alarm Management | S5F1/F2, S5F3/F4, S5F5/F6, S5F7/F8 | `AlarmRegistry` | | Remote Control | S2F41/F42, S2F49/F50, S2F21/F22 | `HostCommandRegistry` | | Equipment Constants | S2F13/F14, S2F15/F16, S2F29/F30 | `EquipmentConstantStore` | | Process Program Management | S7F1–F6, S7F17–F20, S7F23–F26 | `RecipeStore` | | Material Movement | (handled by E40 + E94 + E87 + E90 + E157) | see chapters 14–16 | -| Equipment Terminal Services | S10F1/F2, S10F3/F4, S10F5/F6 | `TerminalServiceStore` | -| Clock | S2F17/F18, S2F31/F32 | `ClockStore` (+ E148 in chapter 19) | +| Equipment Terminal Services | S10F1/F2, S10F3/F4, S10F5/F6 | S10F1–F6 Router handlers (no dedicated store) | +| Clock | S2F17/F18, S2F31/F32 | `Clock` (+ E148 in chapter 19) | | Limits Monitoring | S2F45/F46, S2F47/F48 | `LimitMonitorStore` | | Spooling | S2F43/F44, S6F23/F24, S6F25/F26 | `SpoolStore` (persistent file-backed journal) | @@ -193,13 +193,13 @@ Pick **Event Notification** — the canonical GEM scenario: ``` 1. Host sends S2F33 (DefineReport): "RPTID 100 = [SVID 1, SVID 5]" -2. Equipment stores the definition in ReportStore; replies S2F34(DRACK=0). +2. Equipment stores the definition in EventReportSubscriptions; replies S2F34(DRACK=0). 3. Host sends S2F35 (LinkEvent): "CEID 300 → RPTID 100" -4. Equipment stores the link in EventStore; replies S2F36(LRACK=0). +4. Equipment stores the link in EventReportSubscriptions; replies S2F36(LRACK=0). 5. Host sends S2F37 (EnableEvent CEED=true, CEID=[300]) -6. Equipment marks CEID 300 enabled in EventStore; replies S2F38(ERACK=0). +6. Equipment marks CEID 300 enabled in EventReportSubscriptions; replies S2F38(ERACK=0). 7. Later: some FSM transition decides to fire CEID 300. - compose_reports_for(300) walks EventStore → ReportStore → SvidStore + compose_reports_for(300) walks EventReportSubscriptions → StatusVariableStore and assembles {RPTID=100, V=[svid1_val, svid5_val]}. 8. Equipment emits S6F11 with the assembled body. 9. Host replies S6F12(ACKC6=0). diff --git a/docs/17_e116_e120_e39_objects.md b/docs/17_e116_e120_e39_objects.md index 24821b4..5425223 100644 --- a/docs/17_e116_e120_e39_objects.md +++ b/docs/17_e116_e120_e39_objects.md @@ -61,9 +61,9 @@ tracks: **how long was the equipment in each state**. The store accumulates time-buckets: ```cpp -class EptStore { +class EptStateMachine { // For each EptState, accumulated wall-clock duration. - std::array bucket_; + std::array buckets_{}; // Current state + when it was entered (so the dwell so far is // counted as part of the current bucket on read). diff --git a/docs/35_state_machines_and_dispatch.md b/docs/35_state_machines_and_dispatch.md index a70e03d..4ce6dec 100644 --- a/docs/35_state_machines_and_dispatch.md +++ b/docs/35_state_machines_and_dispatch.md @@ -147,14 +147,16 @@ inline secs2::Message s2f42(uint8_t hcack, /* per-param acks */) { ```cpp // apps/secs_server.cpp -router->on(2, 41, [model](const secs2::Message& m) { - auto cmd = messages::parse_s2f41(m.body()); - if (!cmd) { - // Body didn't parse — reply S2F42 with HCACK = 1 (invalid). - return messages::s2f42(1, {}); +router.on(2, 41, [model, emit_event, emit_alarm_set](const s2::Message& msg) { + auto cmd = gem::parse_s2f41(msg); + if (!cmd) // body didn't parse + return gem::s2f42_host_command_ack(gem::HostCmdAck::ParameterInvalid, {}); + auto result = model->commands.dispatch(cmd->rcmd, cmd->params); + if (result.ack == gem::HostCmdAck::Accept) { // apply declared side effects + if (result.emit_ceid) emit_event(*result.emit_ceid); + if (result.set_alarm) emit_alarm_set(*result.set_alarm); } - auto outcome = model->commands.dispatch(cmd->rcmd, cmd->params); - return messages::s2f42(static_cast(outcome.ack), outcome.cpacks); + return gem::s2f42_host_command_ack(result.ack, {}); }); ``` @@ -164,33 +166,62 @@ router->on(2, 41, [model](const secs2::Message& m) { // include/secsgem/gem/store/host_commands.hpp class HostCommandRegistry { public: - CommandOutcome dispatch(const std::string& rcmd, const ParamList& params) { - auto it = commands_.find(rcmd); - if (it == commands_.end()) return {HostCmdAck::InvalidCommand, ...}; - const auto& cmd = it->second; - // Apply configured side effects: emit_ceid, set_alarm, … - for (auto ceid : cmd.emit_ceids) on_emit_ceid_(ceid); - for (auto alid : cmd.set_alarms) alarm_registry_->set(alid); - return {cmd.default_ack, ...}; + // Declarative default + optional side effects, loaded from equipment.yaml. + struct Spec { HostCmdAck ack; std::optional emit_ceid, set_alarm; /* … */ }; + struct Result { HostCmdAck ack; std::optional emit_ceid, set_alarm; /* … */ }; + // Application behaviour: runs real work and decides the ack (see §4). + using Handler = std::function&)>; + + void register_command(std::string rcmd, Spec spec); // wired from YAML + void set_handler(std::string rcmd, Handler h); // wired from application code + + Result dispatch(const std::string& rcmd, + const std::vector& params) const { + auto it = by_rcmd_.find(rcmd); + if (it == by_rcmd_.end()) + return {HostCmdAck::InvalidCommand, {}, {}}; + HostCmdAck ack = it->second.ack; // declarative default + if (auto h = handlers_.find(rcmd); h != handlers_.end() && h->second) + ack = h->second(rcmd, params); // application code overrides it + return {ack, it->second.emit_ceid, it->second.set_alarm}; } }; ``` -### 4. The side-effect dispatcher +### 4. Behaviour: declarative default vs. application code -Steps in `dispatch` like `on_emit_ceid_(ceid)` call back into -the EAP: +`dispatch` settles two things — *what ack the host gets* and *what side +effects fire*. They come from two layers: + +**Declarative (YAML).** A row in `equipment.yaml` gives a command a static +`ack` plus optional `emit_ceid` / `set_alarm`. Those ride back on the +`Result`, and the Router handler (§2) applies them by calling the +`emit_event` / `emit_alarm_set` lambdas — which `asio::post` onto the +io_context and then build the S6F11 / S5F1. Fine for a fixed mapping +("FAULT always raises alarm 1"). + +**Application behaviour (the hook).** A static ack can't *do* anything — +start a recipe, read the command's parameters, decide based on tool +state. For that, register a handler. Its return value becomes the ack: ```cpp -// Set up at startup: -model->commands.set_emit_ceid_handler([conn, model](uint32_t ceid) { - if (!model->is_event_enabled(ceid)) return; - auto reports = model->compose_reports_for(ceid); - auto msg = build_s6f11(ceid, reports); - conn->send_data(std::move(msg)); -}); +// Set up at startup, alongside register_command: +model->commands.set_handler("START", + [&](const std::string&, const std::vector& params) { + if (tool.busy()) return gem::HostCmdAck::CannotDoNow; // reject + tool.run_recipe(find_param(params, "PPID")); // real work + return gem::HostCmdAck::Accept; + }); ``` +The same hook covers `S2F41`, `S2F21`, and `S2F49`, since all three call +`dispatch`. Because the Router applies declared side effects only on +`Accept`, a rejecting handler suppresses them for free; with no handler +the command stays purely declarative. This is the seam application code +— and the planned Python binding's `@on("START")` — uses to put real +behaviour behind a host command. + ### 5. The wire `conn->send_data(s6f11)` walks through `secs2::encode` → diff --git a/docs/51_extending_the_codebase.md b/docs/51_extending_the_codebase.md index 627e711..30452e7 100644 --- a/docs/51_extending_the_codebase.md +++ b/docs/51_extending_the_codebase.md @@ -118,14 +118,14 @@ set-alarm, register a custom handler: ```cpp // At startup: -model->commands.register_handler("VENT", - [model](const ParamList& params) -> CommandOutcome { +model->commands.set_handler("VENT", + [model](const std::string& rcmd, + const std::vector& params) { // Actually vent the chamber here. - if (!vacuum_safe_to_vent()) { - return {HostCmdAck::CannotPerformNow, {}}; - } + if (!vacuum_safe_to_vent()) + return gem::HostCmdAck::CannotDoNow; hardware_vent_chamber(); - return {HostCmdAck::Accept, {}}; + return gem::HostCmdAck::Accept; }); ```