0e832d6ff7
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>
111 lines
3.7 KiB
C++
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);
|
|
}
|