C1: ExceptionStateMachine FSM + ExceptionStore
Per-EXID exception lifecycle for E5 §9. States mirror the wire flow: Posted equipment sent S5F9, awaiting host or autonomous clear Recovering host's S5F13 accepted; equipment running recovery RecoverFailed S5F15 reported a failed result; host may retry Cleared terminal — store removes the row Events: Created synthetic NoState->Posted observer signal Recover host's S5F13 (Posted/RecoverFailed -> Recovering) RecoveryComplete equipment internal (Recovering -> Cleared) RecoveryFailed equipment internal (Recovering -> RecoverFailed) RecoveryAbort host's S5F17 (Recovering -> Posted) Clear equipment internal (Posted/RecoverFailed -> Cleared) ExceptionStore mirrors ProcessJobStore: per-EXID FSMs heap-allocated via unique_ptr, non-movable to keep `this`-captures safe, synthetic Created fires after the row lands so observers can decide whether to emit S5F9 out of band. on_recover validates EXRECVRA against the candidates the post advertised. The store joins EquipmentDataModel alongside process_jobs / control_jobs. S5F9-F18 server-side dispatch lands in C2. Tests (12 cases) cover FSM transitions including retry, abort, and autonomous clear, plus store-level duplicate-rejection, EXRECVRA validation, and Cleared-removes-the-row semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/store/alarms.hpp" // AlarmAck (ACKC5)
|
||||
|
||||
// E5 §9 Exception lifecycle.
|
||||
//
|
||||
// An *exception* (distinct from a plain *alarm*) is a recoverable
|
||||
// equipment fault whose handling involves a host/equipment dialogue:
|
||||
// the equipment Posts an S5F9 with one or more candidate EXRECVRA
|
||||
// recovery actions; the host picks one via S5F13 to drive equipment
|
||||
// recovery; equipment Notifies completion via S5F15 (with EXRESULT
|
||||
// signalling success or failure); the host may abort an in-progress
|
||||
// recovery via S5F17.
|
||||
//
|
||||
// This state machine lives per-EXID inside an `ExceptionStore`; the
|
||||
// transitions mirror the request/response cadence on the wire so the
|
||||
// store can be wired into the server's S5F13 / S5F17 / internal
|
||||
// recovery hooks with a single dispatch through `on_host_command`
|
||||
// (host-initiated) or `on_internal` (equipment-initiated).
|
||||
//
|
||||
// The Posted/Cleared distinction matches E5 §9.3 figure 9 — when an
|
||||
// exception's underlying condition clears autonomously without ever
|
||||
// being recovered (rare but legal), the FSM transitions Posted →
|
||||
// Cleared directly via the `Clear` event.
|
||||
namespace secsgem::gem {
|
||||
|
||||
enum class ExceptionState : uint8_t {
|
||||
Posted = 0, // S5F9 sent, awaiting host action or autonomous clear
|
||||
Recovering = 1, // S5F13 accepted; equipment running the recovery
|
||||
RecoverFailed = 2, // S5F15 reported a failed recovery; retry possible
|
||||
Cleared = 3, // exception resolved (terminal; store removes the row)
|
||||
NoState = 255,
|
||||
};
|
||||
|
||||
const char* exception_state_name(ExceptionState s);
|
||||
std::optional<ExceptionState> parse_exception_state(const std::string& s);
|
||||
|
||||
enum class ExceptionEvent {
|
||||
Created, // synthetic NoState -> Posted (observer signal)
|
||||
Recover, // host's S5F13 — Posted/RecoverFailed -> Recovering
|
||||
RecoveryComplete, // internal — Recovering -> Cleared
|
||||
RecoveryFailed, // internal — Recovering -> RecoverFailed
|
||||
RecoveryAbort, // host's S5F17 — Recovering -> Posted
|
||||
Clear, // internal — Posted/RecoverFailed -> Cleared
|
||||
};
|
||||
|
||||
const char* exception_event_name(ExceptionEvent e);
|
||||
std::optional<ExceptionEvent> parse_exception_event(const std::string& s);
|
||||
|
||||
struct ExceptionTransition {
|
||||
ExceptionState from;
|
||||
ExceptionEvent on;
|
||||
std::optional<ExceptionState> to;
|
||||
std::optional<uint8_t> ack_code; // ACKC5 (AlarmAck) when applicable
|
||||
};
|
||||
|
||||
class ExceptionTransitionTable {
|
||||
public:
|
||||
void add(ExceptionTransition row);
|
||||
const ExceptionTransition* find(ExceptionState from, ExceptionEvent on) const;
|
||||
std::size_t size() const { return rows_.size(); }
|
||||
const std::vector<ExceptionTransition>& rows() const { return rows_; }
|
||||
|
||||
// The default table matches data/exception_state.yaml (when present);
|
||||
// kept in code so tests don't depend on the YAML being readable.
|
||||
static ExceptionTransitionTable default_table();
|
||||
|
||||
private:
|
||||
std::vector<ExceptionTransition> rows_;
|
||||
};
|
||||
|
||||
class ExceptionStateMachine {
|
||||
public:
|
||||
using StateChangeHandler =
|
||||
std::function<void(ExceptionState from, ExceptionState to,
|
||||
ExceptionEvent trigger)>;
|
||||
|
||||
ExceptionStateMachine();
|
||||
explicit ExceptionStateMachine(ExceptionTransitionTable table,
|
||||
ExceptionState initial = ExceptionState::Posted);
|
||||
|
||||
ExceptionState state() const { return state_; }
|
||||
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
|
||||
|
||||
// Host-initiated event (S5F13 / S5F17). Returns AlarmAck::Accept on a
|
||||
// valid transition, AlarmAck::Error when no row matches. The default
|
||||
// table is permissive — every host event either advances state or is
|
||||
// silently accepted.
|
||||
AlarmAck on_host_command(ExceptionEvent event);
|
||||
|
||||
// Equipment-internal event (RecoveryComplete / RecoveryFailed / Clear).
|
||||
// Returns true if a row fired.
|
||||
bool on_internal(ExceptionEvent event);
|
||||
|
||||
private:
|
||||
const ExceptionTransition* fire(ExceptionEvent on);
|
||||
void transition(ExceptionState next, ExceptionEvent trigger);
|
||||
|
||||
ExceptionTransitionTable table_;
|
||||
ExceptionState state_;
|
||||
StateChangeHandler on_change_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
Reference in New Issue
Block a user