diff --git a/CMakeLists.txt b/CMakeLists.txt index c68ffff..48968af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/include/secsgem/gem/data_model.hpp b/include/secsgem/gem/data_model.hpp index ffe7690..8b01f64 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -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 vid_value(uint32_t vid) const { diff --git a/include/secsgem/gem/e84_state.hpp b/include/secsgem/gem/e84_state.hpp new file mode 100644 index 0000000..ac67c3a --- /dev/null +++ b/include/secsgem/gem/e84_state.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include + +// 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(s))) != 0; + } + void set(E84Signal s, bool v) { + const uint16_t mask = uint16_t{1} << static_cast(s); + if (v) bits_ |= mask; + else bits_ &= static_cast(~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; + + 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 diff --git a/src/gem/e84_state.cpp b/src/gem/e84_state.cpp new file mode 100644 index 0000000..2cd6902 --- /dev/null +++ b/src/gem/e84_state.cpp @@ -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 diff --git a/tests/test_e84.cpp b/tests/test_e84.cpp new file mode 100644 index 0000000..5156d46 --- /dev/null +++ b/tests/test_e84.cpp @@ -0,0 +1,110 @@ +#include + +#include + +#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 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); +}