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
+2
View File
@@ -61,6 +61,7 @@ add_library(secsgem
src/gem/ept_state.cpp src/gem/ept_state.cpp
src/gem/cem_objects.cpp src/gem/cem_objects.cpp
src/gem/module_state.cpp src/gem/module_state.cpp
src/gem/e84_state.cpp
src/gem/host_handler.cpp src/gem/host_handler.cpp
src/config/loader.cpp src/config/loader.cpp
src/endpoint.cpp src/endpoint.cpp
@@ -113,6 +114,7 @@ add_executable(secsgem_tests
tests/test_modules.cpp tests/test_modules.cpp
tests/test_sml.cpp tests/test_sml.cpp
tests/test_s9_fallback.cpp tests/test_s9_fallback.cpp
tests/test_e84.cpp
) )
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest) target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
target_compile_definitions(secsgem_tests PRIVATE target_compile_definitions(secsgem_tests PRIVATE
+2
View File
@@ -4,6 +4,7 @@
#include "secsgem/gem/store/carriers.hpp" #include "secsgem/gem/store/carriers.hpp"
#include "secsgem/gem/store/cem_objects.hpp" #include "secsgem/gem/store/cem_objects.hpp"
#include "secsgem/gem/store/clock.hpp" #include "secsgem/gem/store/clock.hpp"
#include "secsgem/gem/e84_state.hpp"
#include "secsgem/gem/ept_state.hpp" #include "secsgem/gem/ept_state.hpp"
#include "secsgem/gem/store/control_jobs.hpp" #include "secsgem/gem/store/control_jobs.hpp"
#include "secsgem/gem/store/equipment_constants.hpp" #include "secsgem/gem/store/equipment_constants.hpp"
@@ -47,6 +48,7 @@ struct EquipmentDataModel {
EptStateMachine ept; EptStateMachine ept;
CemObjectStore cem; CemObjectStore cem;
ModuleStore modules; ModuleStore modules;
E84StateMachine e84;
// Convenience: VID -> value lookup spanning SVIDs and DVIDs. // Convenience: VID -> value lookup spanning SVIDs and DVIDs.
std::optional<s2::Item> vid_value(uint32_t vid) const { std::optional<s2::Item> vid_value(uint32_t vid) const {
+97
View File
@@ -0,0 +1,97 @@
#pragma once
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
// E84 §6 Parallel I/O — the digital handshake between AMHS (Automated
// Material Handling System) and equipment for carrier load/unload.
//
// E84 is signal-level, not SECS: ten parallel boolean wires between the
// AMHS robot and the equipment, sequenced in a strict handshake. This
// FSM models the signal bitmap and the handshake state, accepting
// signal-change events as input and exposing state transitions for
// observation. Real wiring uses opto-isolated 24V lines; we abstract
// it as bool getters/setters so the same FSM drives both real hardware
// and back-to-back testing.
namespace secsgem::gem {
enum class E84Signal : uint8_t {
CS_0 = 0, // AMHS -> equip: carrier stage select 0
CS_1 = 1, // AMHS -> equip: carrier stage select 1
VALID = 2, // AMHS -> equip: handshake start
TR_REQ = 3, // AMHS -> equip: transfer request
BUSY = 4, // AMHS -> equip: transfer in progress
COMPT = 5, // AMHS -> equip: transfer complete
L_REQ = 6, // equip -> AMHS: load request (port ready to receive)
U_REQ = 7, // equip -> AMHS: unload request (port ready to release)
READY = 8, // equip -> AMHS: ready
ES = 9, // either direction: emergency stop
};
const char* e84_signal_name(E84Signal s);
// 10-bit signal bitmap with bool get/set.
class E84SignalSet {
public:
bool get(E84Signal s) const {
return (bits_ & (uint16_t{1} << static_cast<uint8_t>(s))) != 0;
}
void set(E84Signal s, bool v) {
const uint16_t mask = uint16_t{1} << static_cast<uint8_t>(s);
if (v) bits_ |= mask;
else bits_ &= static_cast<uint16_t>(~mask);
}
uint16_t raw() const { return bits_; }
void clear() { bits_ = 0; }
private:
uint16_t bits_ = 0;
};
// E84 handoff handshake state (E84 §6.3). Names are short for log
// readability; semantics in comments.
enum class E84State : uint8_t {
Idle = 0, // no signals asserted (or carrier absent)
CarrierPresent = 1, // CS_0 or CS_1 asserted; no VALID yet
ValidAsserted = 2, // CS && VALID; equipment hasn't acknowledged
LoadReady = 3, // VALID && L_REQ; ready to receive carrier
UnloadReady = 4, // VALID && U_REQ; ready to release carrier
Transferring = 5, // BUSY asserted; transfer in progress
Complete = 6, // COMPT asserted; AMHS reports done
EmergencyStop = 7, // ES asserted
NoState = 255,
};
const char* e84_state_name(E84State s);
class E84StateMachine {
public:
using StateChangeHandler =
std::function<void(E84State from, E84State to, E84Signal trigger)>;
E84State state() const { return state_; }
const E84SignalSet& signals() const { return signals_; }
bool signal(E84Signal s) const { return signals_.get(s); }
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
// Apply a single signal change. Re-evaluates the handshake state
// and fires the change handler on transition. Order of signal
// changes matters for the AMHS-equipment handshake; the FSM accepts
// any order and just reports the resulting state.
void on_signal_change(E84Signal s, bool value);
// Convenience: clear all signals; resets state to Idle.
void reset();
private:
void reevaluate(E84Signal trigger);
E84SignalSet signals_;
E84State state_ = E84State::Idle;
StateChangeHandler on_change_;
};
} // namespace secsgem::gem
+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
+110
View File
@@ -0,0 +1,110 @@
#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);
}