#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::HandoffFault: return "HandoffFault"; case E84State::NoState: return "NoState"; } return "?"; } const char* e84_timer_name(E84TimerId t) { switch (t) { case E84TimerId::TA1: return "TA1"; case E84TimerId::TA2: return "TA2"; case E84TimerId::TA3: return "TA3"; } return "?"; } const char* e84_fault_name(E84Fault f) { switch (f) { case E84Fault::None: return "None"; case E84Fault::TA1Expired: return "TA1Expired"; case E84Fault::TA2Expired: return "TA2Expired"; case E84Fault::TA3Expired: return "TA3Expired"; } return "?"; } void E84StateMachine::on_signal_change(E84Signal s, bool value) { signals_.set(s, value); reevaluate(s); } void E84StateMachine::on_timeout(E84TimerId id) { const auto idx = static_cast(id) - 1; if (!armed_[idx]) return; // stale expiry, FSM already moved on armed_[idx] = false; // consume the arming // Record the fault and transition to HandoffFault. Other armed // timers are cancelled by enter_state_'s update_timers_for_state_. switch (id) { case E84TimerId::TA1: fault_ = E84Fault::TA1Expired; break; case E84TimerId::TA2: fault_ = E84Fault::TA2Expired; break; case E84TimerId::TA3: fault_ = E84Fault::TA3Expired; break; } if (on_fault_) on_fault_(fault_); // ES as trigger is the closest synthetic signal for "the handshake // collapsed out-of-band." The fault_handler delivers the precise // reason; observers that just track state changes still get notified. enter_state_(E84State::HandoffFault, E84Signal::ES); } bool E84StateMachine::timer_armed(E84TimerId id) const { return armed_[static_cast(id) - 1]; } void E84StateMachine::reset() { signals_.clear(); cancel_all_timers_(); fault_ = E84Fault::None; 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) { // A latched HandoffFault sticks until reset() — signal jitter on the // wires shouldn't silently clear a recorded handshake timeout. if (state_ == E84State::HandoffFault) return; 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_) enter_state_(next, trigger); } void E84StateMachine::enter_state_(E84State next, E84Signal trigger) { const auto prev = state_; state_ = next; update_timers_for_state_(next); if (on_change_) on_change_(prev, state_, trigger); } void E84StateMachine::update_timers_for_state_(E84State target) { // Arm exactly the timer that should be running in `target`; cancel // every other still-armed timer. This makes the arm/cancel pair on // the application side idempotent regardless of which state we came // from — the FSM is the source of truth. std::optional to_arm; switch (target) { case E84State::ValidAsserted: to_arm = E84TimerId::TA1; break; case E84State::LoadReady: case E84State::UnloadReady: to_arm = E84TimerId::TA2; break; case E84State::Transferring: to_arm = E84TimerId::TA3; break; case E84State::Idle: case E84State::CarrierPresent: case E84State::Complete: case E84State::EmergencyStop: case E84State::HandoffFault: case E84State::NoState: break; } for (auto id : {E84TimerId::TA1, E84TimerId::TA2, E84TimerId::TA3}) { if (to_arm && *to_arm == id) { if (!armed_[static_cast(id) - 1]) arm_timer_(id); } else { cancel_timer_(id); } } } void E84StateMachine::arm_timer_(E84TimerId id) { const auto d = timeout_for_(id); if (d.count() <= 0) return; // 0 = disabled; no arming armed_[static_cast(id) - 1] = true; if (on_arm_) on_arm_(id, d); } void E84StateMachine::cancel_timer_(E84TimerId id) { const auto idx = static_cast(id) - 1; if (!armed_[idx]) return; armed_[idx] = false; if (on_cancel_) on_cancel_(id); } void E84StateMachine::cancel_all_timers_() { for (auto id : {E84TimerId::TA1, E84TimerId::TA2, E84TimerId::TA3}) { cancel_timer_(id); } } std::chrono::milliseconds E84StateMachine::timeout_for_(E84TimerId id) const { switch (id) { case E84TimerId::TA1: return timeouts_.ta1; case E84TimerId::TA2: return timeouts_.ta2; case E84TimerId::TA3: return timeouts_.ta3; } return std::chrono::milliseconds{0}; } } // namespace secsgem::gem