50 using namespace std::chrono;
51 return static_cast<uint64_t
>(
52 duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count());
97 }
catch (
const std::exception& e) {
98 std::cerr <<
"[SequencerService] command threw: " << e.what() << std::endl;
100 std::cerr <<
"[SequencerService] command threw an unknown exception" << std::endl;
110 auto result = std::make_shared<std::promise<bool>>();
111 auto future =
result->get_future();
121 std::future_status::ready) {
123 <<
"s waiting for the worker — reporting failure (it may still run)" << std::endl;
141 std::ifstream f(
path);
144 std::ostringstream ss;
150 const char* fallbacks[] = {
151 "config/config.toml",
152 "../config/config.toml",
153 "../../config/config.toml",
160 for (
const char* fb : fallbacks) {
161 if (std::string(fb) ==
path)
172 std::cerr <<
"[SequencerService] config.toml not found; using defaults" << std::endl;
174 std::cout <<
"[SequencerService] Loaded config: " <<
config_path_ << std::endl;
184 if (slash == std::string::npos)
193 const char* prefixes[] = {
198 for (
const char* pfx : prefixes) {
199 std::string candidate = std::string(pfx) + rel;
200 if (std::ifstream(candidate).good())
221 std::string sm_csv =
resolveDataPath(cfg.state_machine.transitions_csv);
224 <<
"[SequencerService] Failed to load state_transitions.csv (tried relative to cwd: "
225 << sm_csv <<
")" << std::endl;
234 std::string delay_csv =
resolveDataPath(cfg.state_machine.actuator_delay_csv);
236 std::cerr <<
"[SequencerService] Failed to load state_machine_actuators.csv (tried: "
237 << act_csv <<
")" << std::endl;
259 std::cerr <<
"[SequencerService] [server_heartbeat].broadcast_ip is not a valid address"
270 const std::string
fs = cfg.fire.state;
283 std::cerr <<
"[SequencerService] [fire] state \"" <<
fs
284 <<
"\" is not a known state — the fire timer is DISABLED" << std::endl;
289 const std::string ft = cfg.fire.expiry_target;
296 std::cerr <<
"[SequencerService] [fire] expiry_target \"" << ft
297 <<
"\" is not a declared state — fire auto-expiry disabled" << std::endl;
309 std::cout <<
"[SequencerService] Fire state: (none) — fire timer disabled" << std::endl;
313 << cfg.fire.duration_ms <<
" ms (extended " << cfg.fire.extended_ms <<
" ms)"
319 std::cerr <<
"[SequencerService] WARNING: " <<
StateMachine::name(fire_state) <<
" → "
321 <<
" is not an allowed transition — the fire timer will expire into a "
322 "refused transition and the system will stay in fire."
330 std::map<State, HoldRule> rules;
334 r.default_ms = cfg.fire.duration_ms;
335 r.max_ms = cfg.fire.duration_ms;
336 r.extended_ms = cfg.fire.extended_ms;
337 r.return_state = fire_expiry;
338 r.gui_settable =
false;
339 r.gate_actuator = cfg.fire.gate_actuator;
340 if (!
r.gate_actuator.empty()) {
345 r.gate_open_delay_ms =
static_cast<uint32_t
>(din * 1000.0 + 0.5);
346 r.gate_close_delay_ms =
static_cast<uint32_t
>(dout * 1000.0 + 0.5);
347 std::cout <<
"[SequencerService] Fire gate: \"" <<
r.gate_actuator <<
"\" opens +"
348 <<
r.gate_open_delay_ms <<
" ms, closes +" <<
r.gate_close_delay_ms
349 <<
" ms; burn window = " <<
r.default_ms <<
" ms of THAT valve being open"
355 for (
const auto& [role, pos] :
362 std::cerr <<
"[SequencerService] WARNING: fire opens \"" << role <<
"\" " << d
363 <<
" s after entry, but no [fire].gate_actuator is set — the "
364 "burn window is measured from the TRANSITION, so that valve is "
366 << (
static_cast<double>(
r.default_ms) / 1000.0 - d) <<
" s, not "
367 << (
static_cast<double>(
r.default_ms) / 1000.0) <<
" s." << std::endl;
370 rules[fire_state] =
r;
379 for (
const auto& sd : cfg.states) {
380 if (!sd.is_flow || sd.id < 0 || sd.id > 255 || sd.name.empty())
382 const State s =
static_cast<State>(
static_cast<uint8_t
>(sd.id));
386 std::cerr <<
"[SequencerService] More than one state carries is_flow — ignoring \""
397 const std::string& rt = cfg.flow.return_target;
400 if (flow_state == fire_state) {
401 std::cerr <<
"[SequencerService] ERROR: is_flow is set on the fire state — REFUSED. "
402 "A burn window must not become client-settable."
404 }
else if (cfg.flow.duration_ms == 0) {
406 <<
"\" is marked is_flow but [flow].duration_ms is unset — NOT holdable."
408 }
else if (ret == flow_state) {
413 <<
"[SequencerService] ERROR: [flow].return_target is the flow state itself — "
414 "the hold would re-arm forever with its valves open. NOT holdable."
417 std::cerr <<
"[SequencerService] ERROR: [flow].return_target \"" << rt
419 <<
"\" is NOT holdable. Its valves cannot be stranded open because the "
420 "transition into it is now refused."
425 <<
" is not an allowed transition — the hold would expire into a refused "
426 "transition and leave its valves open. NOT holdable."
430 r.default_ms = cfg.flow.duration_ms;
431 r.max_ms = cfg.flow.max_ms ? cfg.flow.max_ms : cfg.flow.duration_ms;
433 r.return_state = ret;
434 r.gui_settable =
true;
435 r.gate_actuator = cfg.flow.gate_actuator;
436 if (!
r.gate_actuator.empty()) {
444 r.gate_open_delay_ms =
static_cast<uint32_t
>(din * 1000.0 + 0.5);
445 r.gate_close_delay_ms =
static_cast<uint32_t
>(dout * 1000.0 + 0.5);
447 rules[flow_state] =
r;
448 std::cout <<
"[SequencerService] Flow hold: " <<
StateMachine::name(flow_state) <<
" "
449 <<
r.default_ms <<
" ms (max " <<
r.max_ms <<
") → "
451 if (!
r.gate_actuator.empty()) {
452 std::cout <<
"[SequencerService] Flow gate: \"" <<
r.gate_actuator <<
"\" opens +"
453 <<
r.gate_open_delay_ms <<
" ms after entry, closes +"
454 <<
r.gate_close_delay_ms <<
" ms after expiry; hold = requested + "
455 <<
r.gate_open_delay_ms <<
" - " <<
r.gate_close_delay_ms
456 <<
" ms, so the requested duration is how long IT is open" << std::endl;
457 if (
r.gate_close_delay_ms >=
r.gate_open_delay_ms +
r.default_ms)
458 std::cerr <<
"[SequencerService] WARNING: the gate closes later than the hold "
459 "would end — the window cannot be honoured and will be clamped."
470 for (
const auto& [role, pos] : opens) {
473 const auto it = closes.find(role);
474 if (it == closes.end() || it->second != 0)
477 <<
" does not close it — the hold will end with that valve still open"
504 for (
const auto& [st, why] : loaded.refused)
505 std::cerr <<
"[SequencerService] ERROR: dynamic state REFUSED — " << why
506 <<
". It is not enterable." << std::endl;
508 for (
auto& [st, ds] : loaded.dynamic) {
515 r.default_ms = ds.timeout_ms;
516 r.max_ms = ds.timeout_ms;
518 r.return_state = ds.timeout_target;
519 r.gui_settable =
false;
522 std::cout <<
"[SequencerService] Dynamic state: " << ds.name <<
" — "
523 << ds.program.stmts.size() <<
" statement(s), timeout " << ds.timeout_ms
526 if (!ds.pressure_roles.empty()) {
527 std::cout <<
", reads";
528 for (
const auto& role : ds.pressure_roles)
529 std::cout <<
" \"" << role <<
"\"";
531 std::cout << std::endl;
538 std::set<std::string> roles;
539 for (
const auto& [st, ds] : loaded.dynamic)
540 roles.insert(ds.pressure_roles.begin(), ds.pressure_roles.end());
541 if (!roles.empty()) {
561 std::cout <<
"[SequencerService] Fire valve ownership: "
563 ?
"sequencer (controller_service is never told a burn started)"
564 :
"controller_service (FIRE_START / FIRE_STOP sent)")
584 : std::vector<fsw::elodin::BoardChannels>{};
587 (it_bv != boards_map.end()) ? it_bv->second : std::vector<fsw::elodin::BoardChannels>{};
594 std::cerr <<
"[SequencerService] Cannot connect to Elodin yet — retrying every "
620 std::cout <<
"[SequencerService] Initialized. Current state: "
639 std::string* refusal_reason) {
643 return enqueueAndWait([
this, state_name, requested_hold_ms, refusal_reason]() {
646 std::cerr <<
"[SequencerService] Unknown state: " << state_name << std::endl;
648 *refusal_reason =
"unknown state";
660 std::string* refusal_reason) {
661 return enqueueAndWait([
this, to, requested_hold_ms, refusal_reason]() {
667 std::string* refusal_reason) {
671 bool has_rule =
false;
686 if (requested_hold_ms != 0) {
687 const char* why =
nullptr;
689 why =
"state is not holdable";
691 why =
"duration not permitted for this state";
692 else if (requested_hold_ms > rule.
max_ms)
693 why =
"duration exceeds max";
696 std::cerr <<
"[SequencerService] Refused " << requested_hold_ms <<
" ms hold on "
698 if (refusal_reason) {
699 *refusal_reason = why;
700 if (std::string(why) ==
"duration exceeds max")
701 *refusal_reason +=
" (" + std::to_string(rule.
max_ms) +
" ms)";
721 << it->second << std::endl;
726 *refusal_reason = it->second;
743 const std::vector<std::string> roles = it->second.pressure_roles;
744 const std::string state_name = it->second.name;
746 for (
const auto& role : roles) {
751 std::cerr <<
"[SequencerService] Refused entry to " << state_name <<
": "
768 *refusal_reason =
"transition rejected";
845 const uint32_t requested = requested_hold_ms != 0 ? requested_hold_ms : rule.
default_ms;
849 const int64_t want =
static_cast<int64_t
>(requested) +
853 const uint32_t hold_ms = want > 0 ?
static_cast<uint32_t
>(want) : 1;
856 <<
" ms) swallows the requested " << requested
857 <<
" ms window — holding 1 ms. Fix the delays CSV." << std::endl;
864 [
this, return_state]() {
884 ? std::function<void(bool)>([
this](
bool active) {
902 bool to_is_dynamic =
false;
951 std::cout <<
"[SequencerService] Debug mode: " << (enabled ?
"ON" :
"OFF") << std::endl;
958 std::string* refusal_reason) {
965 std::string* refusal_reason) {
967 std::cerr <<
"[SequencerService] Manual actuator commands require debug mode" << std::endl;
969 *refusal_reason =
"debug mode required";
989 std::cerr <<
"[SequencerService] EXTEND_FIRE ignored: not in FIRE state" << std::endl;
1011 std::cerr <<
"[SequencerService] retrying " << (
active ?
"FIRE_START" :
"FIRE_STOP")
1012 <<
" to controller_service" << std::endl;
1015 std::cerr <<
"[SequencerService] ❌ controller_service did not acknowledge "
1018 <<
" after 2 attempts — the PWM gate is NOT under sequencer control. The controller "
1019 "falls back to the Elodin sequencer-state parity path; verify the burn ended."
1024 const std::string msg =
active ?
"FIRE_START\n" :
"FIRE_STOP\n";
1025 int sock = socket(AF_INET, SOCK_STREAM, 0);
1028 struct timeval tv{.tv_sec = 1, .tv_usec = 0};
1029 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv,
sizeof(tv));
1030 struct sockaddr_in dest{};
1031 dest.sin_family = AF_INET;
1044 const int flags = fcntl(sock, F_GETFL, 0);
1045 if (flags < 0 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
1050 bool connected =
false;
1051 if (connect(sock,
reinterpret_cast<struct sockaddr*
>(&dest),
sizeof(dest)) == 0) {
1053 }
else if (errno == EINPROGRESS) {
1054 struct pollfd pfd{.fd = sock, .events = POLLOUT, .revents = 0};
1061 socklen_t len =
sizeof(err);
1062 if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &len) != 0)
1074 if (connected && fcntl(sock, F_SETFL, flags) == 0) {
1075 ssize_t
n =
send(sock, msg.c_str(), msg.size(), 0);
1076 if (
n !=
static_cast<ssize_t
>(msg.size())) {
1077 std::cerr <<
"[SequencerService] short send to controller_service (" <<
n <<
"/"
1078 << msg.size() <<
"): " << strerror(errno) << std::endl;
1087 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &rtv,
sizeof(rtv));
1088 char reply[16] = {0};
1089 ssize_t
r = recv(sock,
reply,
sizeof(
reply) - 1, 0);
1090 if (
r > 0 && std::string(
reply,
static_cast<size_t>(
r)).rfind(
"OK", 0) == 0) {
1093 std::cerr <<
"[SequencerService] controller_service refused "
1094 << (
active ?
"FIRE_START" :
"FIRE_STOP") <<
": "
1095 << std::string(
reply,
static_cast<size_t>(
r)) << std::endl;
1097 std::cerr <<
"[SequencerService] no ACK from controller_service for "
1098 << (
active ?
"FIRE_START" :
"FIRE_STOP") <<
" ("
1099 << (
r == 0 ?
"connection closed" : strerror(errno))
1100 <<
") — the command may not have been acted on" << std::endl;
1104 std::cerr <<
"[SequencerService] could not reach controller_service at " <<
controller_host_
1106 <<
" (" << strerror(errno) <<
")" << std::endl;
1111 std::cout <<
"[SequencerService] → controller: " << (
active ?
"FIRE_START" :
"FIRE_STOP")
1112 <<
" (acked)" << std::endl;
1128 std::vector<std::pair<State, std::vector<std::string>>> sensor_states;
1132 const uint8_t
id =
static_cast<uint8_t
>(st);
1134 mask &= ~(1u << id);
1137 if (!ds.pressure_roles.empty())
1138 sensor_states.emplace_back(st, ds.pressure_roles);
1149 for (
const auto& [st, roles] : sensor_states) {
1150 const uint8_t
id =
static_cast<uint8_t
>(st);
1153 for (
const auto& role : roles) {
1156 mask &= ~(1u << id);
1167 std::cerr <<
"[SequencerService] Failed to publish sequencer state to Elodin" << std::endl;
1177 env.
set_valve = [
this](
const std::string& role,
int pos) {
1195 env.
read_pressure = [
this](
const std::string& role,
double& psi) {
1210 auto sanitize = [](std::string s) {
1211 std::replace(s.begin(), s.end(),
':',
' ');
1212 std::replace(s.begin(), s.end(),
'\n',
' ');
1219 out +=
"SCRIPT:" + std::to_string(
static_cast<int>(st)) +
":" + sanitize(ds.name) +
1220 ":OK:" + std::to_string(ds.program.stmts.size()) +
":" +
1221 std::to_string(ds.timeout_ms) +
"\n";
1224 out +=
"SCRIPT:" + std::to_string(
static_cast<int>(st)) +
":" +
1231 for (
const auto& role : ds.pressure_roles) {
1236 out +=
"SCRIPT:" + std::to_string(
static_cast<int>(st)) +
":" + sanitize(ds.name) +
1281 std::this_thread::sleep_for(std::chrono::milliseconds(100));
static bool register_non_sensor_tables(ElodinClient &client, const std::vector< BoardChannels > &act_boards={})
Register sequencer/controller and ACT_CMD tables.
static bool register_ball_valve_cmd_tables(ElodinClient &client, const std::vector< BoardChannels > &bv_boards)
Register ball valve commanded-state VTables [0x35] (sequencer_service).
static std::string explain(const std::string &role, Status s)
Human phrasing for a refusal, e.g.
bool setDebugMode(bool enabled)
Enable or disable debug mode.
uint32_t fire_duration_ms_
std::map< State, DynamicState > dynamic_states_
Dynamic states whose scripts loaded and validated.
std::atomic< bool > cmd_stop_
bool sequencer_owns_valves_
[controller_service].sequencer_owns_valves.
bool tryConnectElodin()
Connect to Elodin and do everything that depends on the connection: register the ACT_CMD VTables,...
bool applyConfig(const fsw::config::Config &cfg)
Everything a config load has to apply, in the one order that works.
bool init(const std::string &config_path="config/config.toml")
Load config, CSVs, connect to Elodin.
void notifyControllerFire(bool active)
Send FIRE_START / FIRE_STOP to controller_service.
static constexpr int kElodinRetrySeconds
static constexpr uint32_t kAbortDoneDelayMs
Delay between ABORT and ABORT_DONE.
std::condition_variable cmd_cv_
StateMachine state_machine_
std::string daq_bind_address_
Local address board-facing UDP leaves from, resolved once in init().
void enqueueDetached(std::function< bool()> fn)
Post work and return immediately.
std::string config_content_
fsw::elodin::ElodinClient elodin_
std::deque< Command > cmd_queue_
bool transitionTo(const std::string &state_name)
Request a state transition by name (e.g.
std::string controller_host_
std::string scriptStatusReport() const
What this process made of its dynamic states at startup, as newline-terminated text.
bool manualActuator(const std::string &name, int pos, std::string *refusal_reason=nullptr)
Debug mode only: manual OPEN/CLOSE for one role.
PressureFeed pressure_feed_
Live calibrated pressures for pressure(), on its own Elodin connection.
bool doTransitionTo(State to, uint32_t requested_hold_ms=0, std::string *refusal_reason=nullptr)
ScriptEnv makeScriptEnv(const DynamicState &ds)
Build the callbacks a script is allowed to reach the world through.
ActuatorCommander actuator_commander_
static constexpr int kCommandTimeoutSeconds
How long a caller waits for its command to run before giving up and reporting failure.
std::string configDir() const
Directory containing config_path_, or "." — where scripts/ is resolved against.
bool notifyControllerFireOnce(bool active)
One attempt: connect, send, wait for the controller's "OK".
bool enqueueAndWait(std::function< bool()> fn)
Post work and block until the worker has run it.
std::atomic< State > current_state_
std::atomic< bool > debug_mode_
ScriptRunner script_runner_
void publishStateTransition(State from, State to)
bool doSetDebugMode(bool enabled)
std::atomic< bool > elodin_retry_stop_
std::vector< fsw::elodin::BoardChannels > ball_valve_boards_
static bool isAbortState(State s)
AbortBroadcaster abort_broadcaster_
std::thread elodin_retry_thread_
uint32_t fire_extended_ms_
uint16_t controller_port_
std::map< State, std::string > refused_states_
States that declare a script which did NOT hold up, with the reason.
bool doManualActuator(const std::string &name, int pos, std::string *refusal_reason=nullptr)
std::map< State, HoldRule > hold_rules_
Holdable states, by target.
std::vector< fsw::elodin::BoardChannels > actuator_boards_
bool loadConfig(const std::string &path)
bool extendFire()
Extend the FIRE window (only valid while in FIRE 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 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).
static void send(void *ctx, const uint8_t *bytes, uint16_t len)
std::map< ActiveBoardKind, std::vector< elodin::BoardChannels > > load_active_boards(const std::string &config_path)
Parse [boards.
Config load_from_string(const std::string &text)
Parse config from an in-memory TOML string (SequencerService / ActuatorCommander hold it so).
DaqBindResult resolveDaqBindAddress(const ::fsw::config::Config &cfg, const char *who)
Decide which local address board traffic should leave from, and log the decision.
*brief user defined string literal for JSON values *sa std::size_t n
static constexpr uint16_t VTABLE_STATE_TRANSITION
static constexpr int kControllerConnectTimeoutMs
static std::string readFile(const std::string &path)
comms::CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, uint32_t, uint8_t > SequencerStateMsg
static std::string resolveDataPath(const std::string &rel)
ScriptLoadResult load_dynamic_states(const fsw::config::Config &cfg, const std::string &config_text, const ScriptLoadContext &ctx)
Resolve, load and validate every [[states]] entry carrying a script_file.
comms::CommsMessage< uint64_t, uint8_t, uint8_t, uint8_t > StateTransitionMsg
static constexpr uint16_t VTABLE_SEQUENCER_STATE
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
static self_test_reply_t reply(self_test_result_t result, int32_t diagnostic)
CommsMessage - Template-based message serialization.
The whole parsed config.toml, restricted to what C++ consumes.
One dynamic state, resolved and proven usable.
What one hold is: how long, and who to tell.
What a running script is allowed to do to the world.
std::function< void(const std::string &role, int pos)> set_valve
Command one valve by canonical config name.
std::function< bool(const std::string &role, double &psi)> read_pressure
Read a calibrated pressure by canonical role name.
std::function< void(State target)> transition
Leave the state.
std::string state_name
Where the script's log lines are attributed.
Everything load_dynamic_states needs that it cannot get from Config alone.
const StateMachine * state_machine
Transition matrix, already loaded.
State fire_state
The [fire] state, so a script cannot be attached to the burn.
std::function< bool(const std::string &state_name)> has_actuator_column
True when the named state has a column in state_machine_actuators.csv.
std::string config_dir
Directory holding config.toml; scripts resolve to <config_dir>/scripts/<script_file>.
What a config load made of its dynamic states.
std::shared_ptr< std::promise< bool > > result
std::function< bool()> fn
One state's timed hold: how long, where it lands, and whether a client may set the length.
uint32_t gate_close_delay_ms
The same actuator's delay in the RETURN state's column.
uint32_t extended_ms
What EXTEND_FIRE restarts at.
uint32_t gate_open_delay_ms
Milliseconds the gate actuator waits after the transition before it opens, from the delays CSV.