e84: asio adapter for handshake timers + wall-clock test

The E84StateMachine timers landed last commit but stayed theoretical —
arming was delivered via abstract callbacks the application had to
glue to a real clock.  This commit ships the canonical glue:

- include/secsgem/gem/e84_asio_timers.hpp: header-only
  E84AsioTimers wraps three asio::steady_timers, wires set_timer_handlers
  on attach(), routes async_wait expiry back into fsm.on_timeout().
  detach() cancels everything cleanly.

- tests/test_e84_asio_timers.cpp: four scenarios exercised through a
  real asio::io_context with wall-clock timers — TA1 expiry,
  signal-driven cancel before TA1 fires, TA3 expiry from the
  Transferring state, and detach() halting further transitions.
  These cover the integration the synthetic unit tests in
  test_e84_timers.cpp can't reach.

- INTEGRATION.md §4.6: the vendor-side recipe — create the port,
  set timeouts, make_shared<E84AsioTimers>(...)::attach(), feed signals
  from your I/O bridge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 14:08:16 +02:00
parent 2ea3ab796a
commit 54dcf6c532
4 changed files with 267 additions and 1 deletions
+44 -1
View File
@@ -267,7 +267,50 @@ model->alarms.set(1, false); // emits S5F1(ALCD=0x04)
The dispatcher takes care of the wire frame — you just toggle.
### 4.6. Recoverable exceptions (E5 §9, S5F9F18)
### 4.6. E84 parallel I/O handoff (AMHS)
For each load port that talks to the AMHS robot:
```cpp
#include "secsgem/gem/e84_asio_timers.hpp"
auto* fsm = model->e84_ports.get(/*port_id=*/1);
if (!fsm) { model->e84_ports.create(1); fsm = model->e84_ports.get(1); }
// SEMI E84 §6 handshake timers. Defaults below are spec-typical; tune
// per port. TA1=AMHS waits for L_REQ/U_REQ after VALID; TA2=equipment
// waits for BUSY after port is ready; TA3=BUSY phase budget.
fsm->set_timeouts({std::chrono::seconds(2),
std::chrono::seconds(2),
std::chrono::seconds(60)});
// Wire arm/cancel into asio so the FSM polices the real wall clock.
auto driver = std::make_shared<gem::E84AsioTimers>(io.get_executor(), *fsm);
driver->attach();
// Keep `driver` alive for the lifetime of the FSM (e.g. as a member
// of your per-port object).
// Optional: log handoff faults.
fsm->set_fault_handler([port_id = 1](gem::E84Fault reason) {
log("E84 port " + std::to_string(port_id) + " fault: " +
gem::e84_fault_name(reason));
});
// Now feed signal changes from your I/O bridge. On a real AMHS the
// bridge polls or interrupts on the parallel-I/O lines:
model->e84_ports.on_signal_change(1, gem::E84Signal::CS_0, true);
model->e84_ports.on_signal_change(1, gem::E84Signal::VALID, true);
// equipment side asserts when port is physically ready:
model->e84_ports.on_signal_change(1, gem::E84Signal::L_REQ, true);
// ... AMHS continues with BUSY / COMPT.
```
If TA1, TA2, or TA3 expires the FSM transitions to `HandoffFault` and
the fault handler fires with the precise `E84Fault` reason. Your
application is then responsible for whatever the tool's fault policy is
(typically: assert your local ES line and raise an alarm).
### 4.7. Recoverable exceptions (E5 §9, S5F9F18)
For faults where you want a host/equipment recovery dialogue: