Files
secs-gem/tests/test_e84.cpp
T
raphael 0e832d6ff7 P: E84 Parallel I/O handoff signaling
The biggest single gap I called out in the GEM300 audit — closed.
E84 is the digital handshake between AMHS (Automated Material
Handling System) and the equipment for carrier load/unload.  Unlike
the rest of GEM300, this isn't SECS messaging; it's a fixed set of
ten parallel boolean wires that follow a strict sequencing protocol
(E84-0710 §6.3).

Adds:
  E84Signal enum     CS_0/CS_1/VALID/TR_REQ/BUSY/COMPT/L_REQ/U_REQ/
                     READY/ES
  E84SignalSet       10-bit bitmap with bool get/set
  E84State           Idle / CarrierPresent / ValidAsserted /
                     LoadReady / UnloadReady / Transferring /
                     Complete / EmergencyStop
  E84StateMachine    re-evaluates state on every signal change,
                     observable via set_state_change_handler

Joins EquipmentDataModel as `e84` (top-level — there's one per tool,
not per port).  ES (emergency stop) dominates regardless of other
signals; COMPT and BUSY override the VALID-handshake states.  Same
FSM drives real opto-isolated I/O lines (when wired through an
asio digital input adapter) and the back-to-back test simulation.

Six test cases cover the full load handshake trace (six transitions,
including the transient LoadReady-after-BUSY-drops state), the
unload variant via U_REQ, ES dominance + recovery, reset(), and
no-op suppression for idempotent signal writes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 09:17:17 +02:00

111 lines
3.7 KiB
C++

#include <doctest/doctest.h>
#include <vector>
#include "secsgem/gem/e84_state.hpp"
using namespace secsgem::gem;
TEST_CASE("E84: initial state is Idle with all signals low") {
E84StateMachine fsm;
CHECK(fsm.state() == E84State::Idle);
for (auto s : {E84Signal::CS_0, E84Signal::CS_1, E84Signal::VALID,
E84Signal::TR_REQ, E84Signal::BUSY, E84Signal::COMPT,
E84Signal::L_REQ, E84Signal::U_REQ, E84Signal::READY,
E84Signal::ES}) {
CHECK_FALSE(fsm.signal(s));
}
}
TEST_CASE("E84: load handshake sequence") {
E84StateMachine fsm;
std::vector<E84State> trace;
fsm.set_state_change_handler(
[&](E84State, E84State to, E84Signal) { trace.push_back(to); });
// AMHS asserts CS_0 (port 0 selected).
fsm.on_signal_change(E84Signal::CS_0, true);
CHECK(fsm.state() == E84State::CarrierPresent);
// AMHS asserts VALID.
fsm.on_signal_change(E84Signal::VALID, true);
CHECK(fsm.state() == E84State::ValidAsserted);
// Equipment asserts L_REQ (port is ready to receive).
fsm.on_signal_change(E84Signal::L_REQ, true);
CHECK(fsm.state() == E84State::LoadReady);
// AMHS begins transfer.
fsm.on_signal_change(E84Signal::BUSY, true);
CHECK(fsm.state() == E84State::Transferring);
// AMHS completes. Dropping BUSY transiently returns to LoadReady
// (VALID + L_REQ still held), then COMPT pushes Complete.
fsm.on_signal_change(E84Signal::BUSY, false);
CHECK(fsm.state() == E84State::LoadReady);
fsm.on_signal_change(E84Signal::COMPT, true);
CHECK(fsm.state() == E84State::Complete);
// Six transitions: Idle->CarrierPresent->ValidAsserted->LoadReady->
// Transferring->LoadReady->Complete.
CHECK(trace.size() == 6);
CHECK(trace.back() == E84State::Complete);
}
TEST_CASE("E84: unload handshake distinguishes U_REQ vs L_REQ") {
E84StateMachine fsm;
fsm.on_signal_change(E84Signal::CS_1, true);
fsm.on_signal_change(E84Signal::VALID, true);
fsm.on_signal_change(E84Signal::U_REQ, true);
CHECK(fsm.state() == E84State::UnloadReady);
}
TEST_CASE("E84: ES dominates regardless of other signals") {
E84StateMachine fsm;
fsm.on_signal_change(E84Signal::CS_0, true);
fsm.on_signal_change(E84Signal::VALID, true);
fsm.on_signal_change(E84Signal::L_REQ, true);
REQUIRE(fsm.state() == E84State::LoadReady);
fsm.on_signal_change(E84Signal::ES, true);
CHECK(fsm.state() == E84State::EmergencyStop);
// ES persists even as other signals drop.
fsm.on_signal_change(E84Signal::VALID, false);
fsm.on_signal_change(E84Signal::CS_0, false);
CHECK(fsm.state() == E84State::EmergencyStop);
// Clearing ES returns to whatever the remaining signals indicate.
// VALID and CS_0 are gone, so even though L_REQ is still held, the
// handshake collapses to Idle (no carrier present + no VALID).
fsm.on_signal_change(E84Signal::ES, false);
CHECK(fsm.state() == E84State::Idle);
}
TEST_CASE("E84: reset() drops all signals and returns to Idle") {
E84StateMachine fsm;
fsm.on_signal_change(E84Signal::CS_0, true);
fsm.on_signal_change(E84Signal::VALID, true);
REQUIRE(fsm.state() != E84State::Idle);
fsm.reset();
CHECK(fsm.state() == E84State::Idle);
CHECK_FALSE(fsm.signal(E84Signal::CS_0));
CHECK_FALSE(fsm.signal(E84Signal::VALID));
}
TEST_CASE("E84: handler suppresses no-op signal-change events") {
E84StateMachine fsm;
int calls = 0;
fsm.set_state_change_handler(
[&](E84State, E84State, E84Signal) { ++calls; });
// Setting CS_0 false (already false) doesn't change state.
fsm.on_signal_change(E84Signal::CS_0, false);
CHECK(calls == 0);
// Real change.
fsm.on_signal_change(E84Signal::CS_0, true);
CHECK(calls == 1);
// Setting it true again (idempotent).
fsm.on_signal_change(E84Signal::CS_0, true);
CHECK(calls == 1);
}