STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
StateMachine.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <fstream>
5#include <iostream>
6#include <map>
7#include <set>
8#include <sstream>
9#include <string>
10
11#include "config/Config.hpp"
12
13namespace sequencer {
14
15// ─────────────────────────────────────────────────────────────────────────────
16// CSV name → State map (mirrors TypeScript CSV_STATE_MAP in state-transitions.ts)
17// ─────────────────────────────────────────────────────────────────────────────
18const std::map<std::string, State>& StateMachine::csvStateMap() {
19 static const std::map<std::string, State> map = {
20 {"Debug", State::DEBUG},
21 {"Idle", State::IDLE},
22 {"Armed", State::ARMED},
23 {"Fuel Fill", State::FUEL_FILL},
24 {"Ox Fill", State::OX_FILL},
25 {"Press Standby", State::PRESS_STANDBY},
26 {"GN2 Low Press", State::GN2_LOW_PRESS},
27 {"GN2 Low Vent", State::GN2_VENT},
28 {"Fuel Press", State::FUEL_PRESS},
29 {"Fuel Vent", State::FUEL_VENT},
30 {"Ox Press", State::OX_PRESS},
31 {"Ox Vent", State::OX_VENT},
32 {"GN2 High Press", State::GN2_HIGH_PRESS},
33 {"GN2 High Vent", State::GN2_HIGH_VENT},
34 {"Vent", State::VENT},
35 {"Calibrate", State::CALIBRATE},
36 {"Ready", State::READY},
37 {"Fire", State::FIRE},
38 {"Engine Abort", State::ENGINE_ABORT},
39 {"GSE Abort", State::GSE_ABORT},
40 {"Emergency Abort", State::EMERGENCY_ABORT},
41 // Legacy names
42 {"GN2 Press", State::GN2_LOW_PRESS},
43 {"GN2 Vent", State::GN2_VENT},
44 {"Quick Fire", State::READY},
45 {"High Press", State::GN2_HIGH_PRESS},
46 {"Abort", State::EMERGENCY_ABORT},
47 };
48 return map;
49}
50
51// ─────────────────────────────────────────────────────────────────────────────
52// Config-declared states ([[states]]) — an override layer over the built-in table
53// ─────────────────────────────────────────────────────────────────────────────
54namespace {
55
57 std::map<State, std::string> names; // id → display name
58 std::map<std::string, State> by_name; // display name → id
59 std::set<State> aborts;
61 bool boot_set = false;
62 bool loaded = false;
63};
64
66 static ConfigStates cs;
67 return cs;
68}
69
70} // namespace
71
72void StateMachine::loadStatesFromConfig(const std::string& config_content) {
73 ConfigStates fresh;
74 const fsw::config::Config cfg = fsw::config::load_from_string(config_content);
75 for (const auto& s : cfg.states) {
76 // An entry needs a valid id and a name; skip incomplete ones. First is_boot wins.
77 if (s.id < 0 || s.id > 255 || s.name.empty())
78 continue;
79 const State st = static_cast<State>(static_cast<uint8_t>(s.id));
80 fresh.names[st] = s.name;
81 fresh.by_name[s.name] = st;
82 if (s.is_abort)
83 fresh.aborts.insert(st);
84 if (s.is_boot && !fresh.boot_set) {
85 fresh.boot = st;
86 fresh.boot_set = true;
87 }
88 }
89
90 fresh.loaded = !fresh.names.empty();
91 configStates() = fresh;
92 if (fresh.loaded)
93 std::cout << "[StateMachine] Loaded " << fresh.names.size() << " state(s) from config"
94 << (fresh.aborts.empty() ? ""
95 : " (" + std::to_string(fresh.aborts.size()) + " abort)")
96 << std::endl;
97}
98
100 const auto& cs = configStates();
101 // Once config declares states it is the sole authority — even when it flags none is_abort,
102 // which means "this rig has no abort states," not "fall back to 17/18/19." The compiled ids
103 // only stand in when no config states are loaded at all (no config / parse failure).
104 if (cs.loaded)
105 return cs.aborts.count(s) > 0;
107}
108
110 const auto& cs = configStates();
111 if (cs.loaded && cs.boot_set)
112 return cs.boot;
113 if (cs.loaded && !cs.names.empty()) {
114 // Config declares states but flags none is_boot. Start in the lowest-id declared state
115 // rather than a compiled IDLE(1) that names a different state on a renumbered rig, and say
116 // so loudly — this is a misconfig the config editor should have prevented.
117 const State first = cs.names.begin()->first;
118 std::cerr << "[StateMachine] No is_boot state in config — booting into lowest-id state \""
119 << cs.names.begin()->second << "\"" << std::endl;
120 return first;
121 }
122 return State::IDLE;
123}
124
125// ─────────────────────────────────────────────────────────────────────────────
126// State → display name (for logging)
127// ─────────────────────────────────────────────────────────────────────────────
128std::string StateMachine::name(State s) {
129 {
130 const auto& cs = configStates();
131 auto it = cs.names.find(s);
132 if (it != cs.names.end())
133 return it->second;
134 }
135 switch (s) {
136 case State::DEBUG:
137 return "Debug";
138 case State::IDLE:
139 return "Idle";
140 case State::ARMED:
141 return "Armed";
142 case State::FUEL_FILL:
143 return "Fuel Fill";
144 case State::OX_FILL:
145 return "Ox Fill";
147 return "Press Standby";
149 return "GN2 Low Press";
150 case State::GN2_VENT:
151 return "GN2 Low Vent";
153 return "Fuel Press";
154 case State::FUEL_VENT:
155 return "Fuel Vent";
156 case State::OX_PRESS:
157 return "Ox Press";
158 case State::OX_VENT:
159 return "Ox Vent";
161 return "GN2 High Press";
163 return "GN2 High Vent";
164 case State::VENT:
165 return "Vent";
166 case State::CALIBRATE:
167 return "Calibrate";
168 case State::READY:
169 return "Ready";
170 case State::FIRE:
171 return "Fire";
173 return "Engine Abort";
174 case State::GSE_ABORT:
175 return "GSE Abort";
177 return "Emergency Abort";
178 default:
179 return "Unknown";
180 }
181}
182
183// ─────────────────────────────────────────────────────────────────────────────
184State StateMachine::fromName(const std::string& name) {
185 {
186 const auto& cs = configStates();
187 auto it = cs.by_name.find(name);
188 if (it != cs.by_name.end())
189 return it->second;
190 // Config is authoritative once loaded: a name it does not declare resolves to UNKNOWN, not
191 // a compiled/legacy id that names a different state on a renumbered rig. The table below
192 // stands in only when no config states are loaded at all (no config, or a parse failure).
193 if (cs.loaded)
194 return State::UNKNOWN;
195 }
196 const auto& map = csvStateMap();
197 auto it = map.find(name);
198 if (it != map.end())
199 return it->second;
200
201 // Case-insensitive fallback
202 std::string lower = name;
203 std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
204 for (const auto& [k, v] : map) {
205 std::string kl = k;
206 std::transform(kl.begin(), kl.end(), kl.begin(), ::tolower);
207 if (kl == lower)
208 return v;
209 }
210 return State::UNKNOWN;
211}
212
213// ─────────────────────────────────────────────────────────────────────────────
214// CSV parsing
215// ─────────────────────────────────────────────────────────────────────────────
216static std::string trimCell(const std::string& s) {
217 size_t a = s.find_first_not_of(" \t\r\n\"");
218 size_t b = s.find_last_not_of(" \t\r\n\"");
219 return (a == std::string::npos) ? "" : s.substr(a, b - a + 1);
220}
221
222bool StateMachine::load(const std::string& csv_path) {
223 const char* fallbacks[] = {
224 "config/state_transitions.csv",
225 "../config/state_transitions.csv",
226 "../../config/state_transitions.csv",
227 // Generated config/*.csv are absent on a fresh checkout (they are gitignored deploy
228 // output); fall back to the profile that owns them.
229 "config/profiles/default/state_transitions.csv",
230 "../config/profiles/default/state_transitions.csv",
231 "../../config/profiles/default/state_transitions.csv",
232 };
233
234 std::ifstream f(csv_path);
235 std::string used_path = csv_path;
236 if (!f.is_open()) {
237 for (const char* fb : fallbacks) {
238 if (std::string(fb) == csv_path)
239 continue;
240 f.open(fb);
241 if (f.is_open()) {
242 used_path = fb;
243 break;
244 }
245 }
246 }
247 if (!f.is_open()) {
248 std::cerr << "[StateMachine] Cannot open state_transitions.csv (tried: " << csv_path;
249 for (const char* fb : fallbacks) {
250 if (std::string(fb) != csv_path)
251 std::cerr << ", " << fb;
252 }
253 std::cerr << ")" << std::endl;
254 return false;
255 }
256
257 std::string line;
258 if (!std::getline(f, line))
259 return false;
260
261 // Header row: ,Idle,Armed,...
262 std::vector<State> column_states;
263 {
264 std::istringstream iss(line);
265 std::string cell;
266 bool first = true;
267 while (std::getline(iss, cell, ',')) {
268 if (first) {
269 first = false;
270 continue;
271 } // skip leading empty cell
272 State s = fromName(trimCell(cell));
273 column_states.push_back(s);
274 }
275 }
276
277 transitions_.clear();
278 int rows = 0;
279 while (std::getline(f, line)) {
280 std::vector<std::string> cells;
281 std::istringstream iss(line);
282 std::string cell;
283 while (std::getline(iss, cell, ','))
284 cells.push_back(trimCell(cell));
285 if (cells.empty() || cells[0].empty())
286 continue;
287
288 State from = fromName(cells[0]);
289 if (from == State::UNKNOWN)
290 continue;
291
292 for (size_t col = 1; col < cells.size() && col - 1 < column_states.size(); ++col) {
293 if (cells[col] == "1") {
294 State to = column_states[col - 1];
295 if (to != State::UNKNOWN)
296 transitions_[from].insert(to);
297 }
298 }
299 ++rows;
300 }
301
302 loaded_ = true;
303 std::cout << "[StateMachine] Loaded " << rows << " states from " << used_path << std::endl;
304 return true;
305}
306
307// ─────────────────────────────────────────────────────────────────────────────
308bool StateMachine::isAllowed(State from, State to) const {
309 auto it = transitions_.find(from);
310 if (it == transitions_.end())
311 return false;
312 return it->second.count(to) > 0;
313}
314
315std::vector<State> StateMachine::allowedFrom(State state) const {
316 std::vector<State> result;
317 auto it = transitions_.find(state);
318 if (it != transitions_.end()) {
319 for (State s : it->second)
320 result.push_back(s);
321 }
322 return result;
323}
324
326 uint32_t mask = 0;
327 auto it = transitions_.find(state);
328 if (it != transitions_.end()) {
329 for (State s : it->second) {
330 uint8_t val = static_cast<uint8_t>(s);
331 if (val < 32)
332 mask |= (1u << val);
333 }
334 }
335 return mask;
336}
337
338} // namespace sequencer
bool load(const std::string &csv_path)
Load from a state_transitions.csv file.
std::vector< State > allowedFrom(State state) const
Returns the list of states reachable from the given state.
static State fromName(const std::string &name)
Translate a CSV name (e.g.
static std::string name(State s)
Human-readable name for a State (e.g.
static const std::map< std::string, State > & csvStateMap()
uint32_t allowedBitmask(State state) const
Returns a bitmask where bit N is set if state N is reachable from the given state.
bool isAllowed(State from, State to) const
Returns true if transitioning from→to is allowed.
static State bootState()
The state to boot into (config is_boot, else Idle).
static void loadStatesFromConfig(const std::string &config_content)
Adopt the [[states]] entries from a config as the source of truth for state names and semantics.
static bool isAbort(State s)
True if the state carries the abort semantics (config is_abort, else the built-in trio).
std::map< State, std::set< State > > transitions_
static ActuatorControllerState state
Definition main.cpp:57
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
json_pointer result
Definition json.hpp:14064
Config load_from_string(const std::string &text)
Parse config from an in-memory TOML string (SequencerService / ActuatorCommander hold it so).
Definition Config.cpp:458
static std::string trimCell(const std::string &s)
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264