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>
This commit is contained in:
2026-06-08 09:17:17 +02:00
parent 564bd47132
commit 0e832d6ff7
5 changed files with 288 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
#include "secsgem/gem/e84_state.hpp"
namespace secsgem::gem {
const char* e84_signal_name(E84Signal s) {
switch (s) {
case E84Signal::CS_0: return "CS_0";
case E84Signal::CS_1: return "CS_1";
case E84Signal::VALID: return "VALID";
case E84Signal::TR_REQ: return "TR_REQ";
case E84Signal::BUSY: return "BUSY";
case E84Signal::COMPT: return "COMPT";
case E84Signal::L_REQ: return "L_REQ";
case E84Signal::U_REQ: return "U_REQ";
case E84Signal::READY: return "READY";
case E84Signal::ES: return "ES";
}
return "?";
}
const char* e84_state_name(E84State s) {
switch (s) {
case E84State::Idle: return "Idle";
case E84State::CarrierPresent: return "CarrierPresent";
case E84State::ValidAsserted: return "ValidAsserted";
case E84State::LoadReady: return "LoadReady";
case E84State::UnloadReady: return "UnloadReady";
case E84State::Transferring: return "Transferring";
case E84State::Complete: return "Complete";
case E84State::EmergencyStop: return "EmergencyStop";
case E84State::NoState: return "NoState";
}
return "?";
}
void E84StateMachine::on_signal_change(E84Signal s, bool value) {
signals_.set(s, value);
reevaluate(s);
}
void E84StateMachine::reset() {
signals_.clear();
if (state_ != E84State::Idle) {
const auto prev = state_;
state_ = E84State::Idle;
if (on_change_) on_change_(prev, state_, E84Signal::ES); // trigger is arbitrary
}
}
void E84StateMachine::reevaluate(E84Signal trigger) {
E84State next = state_;
// Emergency stop dominates.
if (signals_.get(E84Signal::ES)) {
next = E84State::EmergencyStop;
} else if (signals_.get(E84Signal::COMPT)) {
next = E84State::Complete;
} else if (signals_.get(E84Signal::BUSY)) {
next = E84State::Transferring;
} else if (signals_.get(E84Signal::VALID)) {
if (signals_.get(E84Signal::L_REQ)) next = E84State::LoadReady;
else if (signals_.get(E84Signal::U_REQ)) next = E84State::UnloadReady;
else next = E84State::ValidAsserted;
} else if (signals_.get(E84Signal::CS_0) || signals_.get(E84Signal::CS_1)) {
next = E84State::CarrierPresent;
} else {
next = E84State::Idle;
}
if (next != state_) {
const auto prev = state_;
state_ = next;
if (on_change_) on_change_(prev, state_, trigger);
}
}
} // namespace secsgem::gem