8a48ffeed4
The single-slot set_*_handler pattern was a structural blocker, hit twice: the daemon could not observe control-state changes because register_default_handlers owns the slot, forcing GetControlState to read the FSM cross-thread (a data race), and blocking WatchHealth and the Subscribe stream's ControlStateChange variant. HandlerSlot<Args...> keeps a primary slot with exact legacy semantics (set_ replaces — one existing test depends on replacement) plus an append-only observer list (add_) that survives set_ calls. Fire sites are textually unchanged (operator bool / operator() / assign-from-function). Applied to ControlStateMachine + ProcessJobStore + ControlJobStore (the roadmap-critical three; the remaining single-slot classes follow the same 3-line pattern as needed). EquipmentRuntime gains an atomic control-state mirror registered as an observer — control_state() is now safe from any thread, retiring the GetControlState race — plus add_control_state_observer and add_link_observer (selected/closed fan-out), the hooks WatchHealth and Subscribe need. Tests: observer ordering, set-replaces-primary-but-observers-survive, observers-without-primary, PJ-store coexistence, and the runtime scenario that was previously impossible (mirror + observer + default-handlers set_). Core 464/464 (2816 assertions), daemon 16/16, live GEM300 demo passes with single-fire control-state transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
125 lines
3.7 KiB
C++
125 lines
3.7 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "secsgem/gem/handler_slot.hpp"
|
|
|
|
namespace secsgem::gem {
|
|
|
|
// E30 §6.2 Control State Model.
|
|
enum class ControlState {
|
|
EquipmentOffline,
|
|
AttemptOnline,
|
|
HostOffline,
|
|
OnlineLocal,
|
|
OnlineRemote,
|
|
};
|
|
|
|
const char* control_state_name(ControlState s);
|
|
std::optional<ControlState> parse_control_state(const std::string& s);
|
|
bool is_online(ControlState s);
|
|
|
|
enum class ControlEvent {
|
|
OperatorSwitchOnline,
|
|
OperatorSwitchOffline,
|
|
OperatorSwitchLocal,
|
|
OperatorSwitchRemote,
|
|
AttemptComplete,
|
|
AttemptFailed,
|
|
HostRequestOnline,
|
|
HostRequestOffline,
|
|
};
|
|
|
|
const char* control_event_name(ControlEvent e);
|
|
std::optional<ControlEvent> parse_control_event(const std::string& s);
|
|
|
|
enum class OnlineAck : uint8_t {
|
|
Accept = 0,
|
|
NotAccept = 1,
|
|
AlreadyOnline = 2,
|
|
};
|
|
|
|
enum class OfflineAck : uint8_t {
|
|
Accept = 0,
|
|
};
|
|
|
|
enum class CommAck : uint8_t {
|
|
Accept = 0,
|
|
Denied = 1,
|
|
};
|
|
|
|
// One row of the control-state transition table (E30 §6.2 + extensions).
|
|
// `to` and `then` are both optional: a row may produce no transition (e.g.
|
|
// host_request_online while already OnlineRemote — ack only); a row may also
|
|
// chain straight through the transient AttemptOnline state via `then`.
|
|
// `ack_code` is the raw uint8_t carried by S1F18 (ONLACK) or S1F16 (OFLACK);
|
|
// its interpretation depends on the triggering event.
|
|
struct ControlTransition {
|
|
ControlState from;
|
|
ControlEvent on;
|
|
std::optional<ControlState> to;
|
|
std::optional<ControlState> then;
|
|
std::optional<uint8_t> ack_code;
|
|
};
|
|
|
|
// Pure data table — no behaviour. Lookup is first-match on (from, on).
|
|
class ControlTransitionTable {
|
|
public:
|
|
void add(ControlTransition row);
|
|
const ControlTransition* find(ControlState from, ControlEvent on) const;
|
|
std::size_t size() const { return rows_.size(); }
|
|
const std::vector<ControlTransition>& rows() const { return rows_; }
|
|
|
|
// Built-in default table, matching data/control_state.yaml exactly.
|
|
// Used by tests so they don't depend on the YAML file being present.
|
|
static ControlTransitionTable default_table();
|
|
|
|
private:
|
|
std::vector<ControlTransition> rows_;
|
|
};
|
|
|
|
// The E30 control state machine, driven by a transition table. All
|
|
// behavioural rules live in the table; this class is just the engine.
|
|
class ControlStateMachine {
|
|
public:
|
|
using StateChangeHandler =
|
|
std::function<void(ControlState from, ControlState to, ControlEvent trigger)>;
|
|
|
|
ControlStateMachine();
|
|
explicit ControlStateMachine(ControlTransitionTable table,
|
|
ControlState initial = ControlState::HostOffline);
|
|
|
|
ControlState state() const { return state_; }
|
|
bool online() const { return is_online(state_); }
|
|
|
|
// Replaces the primary handler (legacy single-slot semantics).
|
|
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
|
|
// Appends an observer that survives set_state_change_handler calls.
|
|
void add_state_change_handler(StateChangeHandler h) { on_change_.add(std::move(h)); }
|
|
|
|
// Operator actions. Return true if a transition (or self-ack) was found.
|
|
bool operator_online();
|
|
bool operator_offline();
|
|
bool operator_local();
|
|
bool operator_remote();
|
|
|
|
// Host requests. Return the ack code from the matching table row.
|
|
OnlineAck on_host_request_online();
|
|
OfflineAck on_host_request_offline();
|
|
|
|
private:
|
|
// Apply the matching row (if any) for (state_, event); returns the row.
|
|
const ControlTransition* fire(ControlEvent on);
|
|
void transition(ControlState next, ControlEvent trigger);
|
|
|
|
ControlTransitionTable table_;
|
|
ControlState state_;
|
|
HandlerSlot<ControlState, ControlState, ControlEvent> on_change_;
|
|
};
|
|
|
|
} // namespace secsgem::gem
|