#pragma once #include #include #include #include #include #include "secsgem/gem/exception_state.hpp" namespace secsgem::gem { // One Exception record. EXID keys the row; EXTYPE / EXMESSAGE are E5 // §9 metadata; EXRECVRA lists the recovery-action names the host may // pick from on S5F13. The FSM is heap-allocated so the per-exception // state-change handler can capture a stable pointer. struct Exception { uint32_t exid; std::string extype; std::string exmessage; std::vector exrecvra; std::unique_ptr fsm; }; class ExceptionStore { public: using TransitionTableFactory = std::function; using StateChangeHandler = std::function; ExceptionStore() : factory_([] { return ExceptionTransitionTable::default_table(); }) {} // Same `this`-capture caveat as ProcessJobStore: keep a stable address. ExceptionStore(const ExceptionStore&) = delete; ExceptionStore& operator=(const ExceptionStore&) = delete; ExceptionStore(ExceptionStore&&) = delete; ExceptionStore& operator=(ExceptionStore&&) = delete; void set_table_factory(TransitionTableFactory f) { factory_ = std::move(f); } void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); } enum class PostResult { Posted, Denied_AlreadyExists, }; // Equipment posts a new exception (S5F9). Returns Denied_AlreadyExists // if an entry for `exid` is already live. Fires a synthetic NoState -> // Posted change so the dispatcher can decide whether to actually emit // the S5F9 (skip if alerts disabled, etc.). PostResult post(uint32_t exid, std::string extype, std::string exmessage, std::vector exrecvra) { if (by_id_.count(exid)) return PostResult::Denied_AlreadyExists; auto fsm = std::make_unique(factory_(), ExceptionState::Posted); fsm->set_state_change_handler( [this, exid](ExceptionState from, ExceptionState to, ExceptionEvent trig) { if (on_change_) on_change_(exid, from, to, trig); }); by_id_.emplace(exid, Exception{exid, std::move(extype), std::move(exmessage), std::move(exrecvra), std::move(fsm)}); if (on_change_) { on_change_(exid, ExceptionState::NoState, ExceptionState::Posted, ExceptionEvent::Created); } return PostResult::Posted; } bool has(uint32_t exid) const { return by_id_.count(exid) > 0; } const Exception* get(uint32_t exid) const { auto it = by_id_.find(exid); return it == by_id_.end() ? nullptr : &it->second; } Exception* get(uint32_t exid) { auto it = by_id_.find(exid); return it == by_id_.end() ? nullptr : &it->second; } ExceptionState state(uint32_t exid) const { auto it = by_id_.find(exid); return it == by_id_.end() ? ExceptionState::NoState : it->second.fsm->state(); } // S5F13 recovery dispatch. Validates EXRECVRA matches one the post // advertised; returns AlarmAck::Error on unknown EXID or unknown action. AlarmAck on_recover(uint32_t exid, const std::string& exrecvra) { auto* ex = get(exid); if (!ex) return AlarmAck::Error; bool known = false; for (const auto& a : ex->exrecvra) if (a == exrecvra) { known = true; break; } if (!known) return AlarmAck::Error; return ex->fsm->on_host_command(ExceptionEvent::Recover); } // S5F17 recovery-abort dispatch. AlarmAck on_recover_abort(uint32_t exid) { auto* ex = get(exid); if (!ex) return AlarmAck::Error; return ex->fsm->on_host_command(ExceptionEvent::RecoveryAbort); } // Equipment-internal lifecycle events. Cleared transitions remove the // entry — exceptions are not retained past clearance, matching the // E5 §9.3 model where Cleared is the lifecycle's terminus. bool fire_internal(uint32_t exid, ExceptionEvent event) { auto* ex = get(exid); if (!ex) return false; const bool fired = ex->fsm->on_internal(event); if (fired && ex->fsm->state() == ExceptionState::Cleared) { by_id_.erase(exid); } return fired; } std::size_t size() const { return by_id_.size(); } std::vector ids() const { std::vector out; out.reserve(by_id_.size()); for (const auto& kv : by_id_) out.push_back(kv.first); return out; } private: std::map by_id_; TransitionTableFactory factory_; StateChangeHandler on_change_; }; } // namespace secsgem::gem