docs: correct drifted and fabricated APIs in chapters 13/17/35/51

An audit of doc code blocks against the real headers found APIs that do
not exist in the codebase, presented as authoritative walkthroughs:

- ch35 (dispatch): an entirely fabricated callback architecture —
  HostCommandRegistry::set_emit_ceid_handler, CommandOutcome, emit_ceids.
  Rewritten to the real Spec/Result/dispatch + the new set_handler hook.
- ch13 (E30): wrong store names — EventStore/ReportStore -> EventReportSubscriptions,
  SvidStore -> StatusVariableStore, AlarmStore/AlarmDispatcher -> AlarmRegistry,
  ClockStore -> Clock, TerminalServiceStore -> (no store), in both the
  capability tables and the worked S2F33 example.
- ch17 (E116): EptStore/seconds/bucket_ -> EptStateMachine/milliseconds/buckets_.
- ch51 (extending): stale host-command handler -> the real set_handler signature.

Verified clean by grep: no fabricated symbols remain in docs/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 18:00:58 +02:00
parent 0090791968
commit 4b4b2ac690
4 changed files with 74 additions and 43 deletions
+56 -25
View File
@@ -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<uint8_t>(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<uint32_t> emit_ceid, set_alarm; /* … */ };
struct Result { HostCmdAck ack; std::optional<uint32_t> emit_ceid, set_alarm; /* … */ };
// Application behaviour: runs real work and decides the ack (see §4).
using Handler = std::function<HostCmdAck(const std::string& rcmd,
const std::vector<CommandParameter>&)>;
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<CommandParameter>& 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<gem::CommandParameter>& 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`