#pragma once #include #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. // // The handshake is policed by SEMI E84 §6 timers (TA1/TA2/TA3) that // bound how long each leg of the dance may take. The FSM stays I/O-free // — it asks the application to arm / cancel a timer via callbacks and // the application drives the real clock (asio::steady_timer in the // reference server). On expiry the application calls `on_timeout()` // and the FSM transitions to `HandoffFault`. 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 HandoffFault = 8, // a handshake timer (TA1/TA2/TA3) expired NoState = 255, }; const char* e84_state_name(E84State s); // SEMI E84 §6 handshake timers. // // TA1 — AMHS bounds how long equipment may take to assert L_REQ / // U_REQ after VALID. Armed on entering ValidAsserted; // cancelled on entering Load/UnloadReady. // TA2 — Equipment bounds how long AMHS may take to start the actual // transfer (assert BUSY) once the port is ready. Armed on // entering Load/UnloadReady; cancelled on entering Transferring. // TA3 — Equipment bounds how long the BUSY phase may last. Armed on // entering Transferring; cancelled on entering Complete. // // SEMI's default values are 2 s / 2 s / 60 s respectively but a tool // builder typically tunes them per port. A timeout of 0 disables the // timer (used in tests and for back-to-back simulation that doesn't // model wall-clock pacing). enum class E84TimerId : uint8_t { TA1 = 1, TA2 = 2, TA3 = 3, }; const char* e84_timer_name(E84TimerId t); struct E84Timeouts { std::chrono::milliseconds ta1{0}; std::chrono::milliseconds ta2{0}; std::chrono::milliseconds ta3{0}; }; // Fault reason recorded on a HandoffFault transition. enum class E84Fault : uint8_t { None = 0, TA1Expired = 1, TA2Expired = 2, TA3Expired = 3, }; const char* e84_fault_name(E84Fault f); class E84StateMachine { public: using StateChangeHandler = std::function; using TimerArmHandler = std::function; using TimerCancelHandler = std::function; using FaultHandler = 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); } // Configure SEMI E84 §6 handshake timeouts. Default is all-zero, // which disables timer enforcement entirely — the back-to-back tests // (and the legacy CarrierPresent → LoadReady → Transferring → Complete // happy path) work unchanged. Production code should pass spec-derived // values (typically TA1=2s, TA2=2s, TA3=60s). void set_timeouts(const E84Timeouts& t) { timeouts_ = t; } const E84Timeouts& timeouts() const { return timeouts_; } // Wire the FSM's timer notifications to the application's clock. The // FSM calls `arm` when entering a state that starts a timer and // `cancel` when leaving it. Real applications back this with // asio::steady_timer; tests can record the calls directly. void set_timer_handlers(TimerArmHandler arm, TimerCancelHandler cancel) { on_arm_ = std::move(arm); on_cancel_ = std::move(cancel); } void set_fault_handler(FaultHandler h) { on_fault_ = 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); // Called by the application when its real-clock timer for `id` fires. // If the timer is still armed in the FSM (i.e. not raced by a // cancellation that hasn't landed yet on the application side), the // FSM transitions to HandoffFault and records the reason. Stale // expiries are silently ignored. void on_timeout(E84TimerId id); bool timer_armed(E84TimerId id) const; E84Fault fault() const { return fault_; } // Convenience: clear all signals; resets state to Idle, cancels every // armed timer, and clears any fault. void reset(); private: void reevaluate(E84Signal trigger); void enter_state_(E84State next, E84Signal trigger); void update_timers_for_state_(E84State target); void arm_timer_(E84TimerId id); void cancel_timer_(E84TimerId id); void cancel_all_timers_(); std::chrono::milliseconds timeout_for_(E84TimerId id) const; E84SignalSet signals_; E84State state_ = E84State::Idle; StateChangeHandler on_change_; TimerArmHandler on_arm_; TimerCancelHandler on_cancel_; FaultHandler on_fault_; E84Timeouts timeouts_; E84Fault fault_ = E84Fault::None; bool armed_[3] = {false, false, false}; // index = (id - 1) }; } // namespace secsgem::gem