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:
@@ -61,6 +61,7 @@ add_library(secsgem
|
||||
src/gem/ept_state.cpp
|
||||
src/gem/cem_objects.cpp
|
||||
src/gem/module_state.cpp
|
||||
src/gem/e84_state.cpp
|
||||
src/gem/host_handler.cpp
|
||||
src/config/loader.cpp
|
||||
src/endpoint.cpp
|
||||
@@ -113,6 +114,7 @@ add_executable(secsgem_tests
|
||||
tests/test_modules.cpp
|
||||
tests/test_sml.cpp
|
||||
tests/test_s9_fallback.cpp
|
||||
tests/test_e84.cpp
|
||||
)
|
||||
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
|
||||
target_compile_definitions(secsgem_tests PRIVATE
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "secsgem/gem/store/carriers.hpp"
|
||||
#include "secsgem/gem/store/cem_objects.hpp"
|
||||
#include "secsgem/gem/store/clock.hpp"
|
||||
#include "secsgem/gem/e84_state.hpp"
|
||||
#include "secsgem/gem/ept_state.hpp"
|
||||
#include "secsgem/gem/store/control_jobs.hpp"
|
||||
#include "secsgem/gem/store/equipment_constants.hpp"
|
||||
@@ -47,6 +48,7 @@ struct EquipmentDataModel {
|
||||
EptStateMachine ept;
|
||||
CemObjectStore cem;
|
||||
ModuleStore modules;
|
||||
E84StateMachine e84;
|
||||
|
||||
// Convenience: VID -> value lookup spanning SVIDs and DVIDs.
|
||||
std::optional<s2::Item> vid_value(uint32_t vid) const {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user