STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
ActuatorCommander.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4#include <map>
5#include <mutex>
6#include <set>
7#include <string>
8#include <thread>
9#include <vector>
10
13
14namespace sequencer {
15
16/**
17 * Config entry for one named actuator role loaded from config.toml [actuator_roles].
18 *
19 * Format in config.toml: role_name = ["TYPE", channel, board_id]
20 * TYPE: "NC" — normally closed (logical pos = hw state)
21 * "NO" — normally open (logical pos inverted for hw: 0→hw1, 1→hw0)
22 * An entry with a 4th element ("pwm_fuel"/"pwm_ox") is controlled by
23 * controller_service, not by the sequencer, and is skipped in FIRE.
24 */
26 int channel{0}; // 1-based actuator ID on the board
27 int board_id{0}; // Board ID (e.g. 11, 12, 13, 14)
28 std::string board_ip; // IP of the board that owns this actuator
29 bool is_no{false}; // Normally Open: invert the logical position
30 bool is_pwm{false}; // assigned to controller_service: sequencer skips it in FIRE
31 // Elodin table its commanded state is published on: [0x32] ACT_CMD for actuator boards,
32 // [0x35] BV_CMD for a ball valve board, which has its own namespace so board 71 and actuator
33 // board 11 (both slot 1) never write the same key.
34 uint8_t cmd_table_hi{0x32};
35};
36
37/**
38 * Sends UDP actuator commands based on state transitions.
39 * Manages the continuous re-send loop (~10 Hz). Debug mode: manual overrides apply until
40 * cleared (Sequencer clears them on every state transition so the CSV wins when changing state).
41 *
42 * Thread-safe: applyForState, setManualOverride, clearAllManualOverrides, stopContinuousLoop.
43 */
45public:
46 /**
47 * Load actuator roles from config content and state→actuator map from CSV.
48 * @param config_content Raw config.toml file contents (already read).
49 * @param csv_path Path to state_machine_actuators.csv (with fallbacks).
50 * @return true on success.
51 */
52 /**
53 * @param delay_csv_path Where the per-actuator staged delays live, from
54 * [state_machine].actuator_delay_csv. Empty falls back to deriving it from `csv_path`.
55 *
56 * The derivation used to be the ONLY way this was found: the delays filename was built
57 * by swapping the "state_machine_actuators.csv" suffix on the positions path. That key
58 * was parsed into the config and then read by nothing, so pointing it anywhere had no
59 * effect — and if the positions file was named anything else the swap did not fire, the
60 * POSITIONS file was opened as the delays file, every cell parsed as non-numeric, and
61 * the stand silently ran with no staggering at all. Losing a fire-column stagger with no error
62 * is not an acceptable failure mode for a missing filename.
63 */
64 bool load(const std::string& config_content, const std::string& csv_path,
65 const std::string& delay_csv_path = "");
66
67 /**
68 * Pin outgoing actuator commands to a NIC, unless [actuator_service].bind_address already
69 * named one explicitly. Call after load().
70 *
71 * bind_address defaulted to 0.0.0.0 in every shipped profile, so the bind() at the bottom of
72 * sendBatch() constrained nothing and the kernel chose the egress interface. That was
73 * harmless while the DAQ owned its machine and is not now that it shares the apps box.
74 */
75 void setDefaultBindAddress(const std::string& address);
76
77 /**
78 * Send actuator commands for the given state (one shot).
79 * Groups commands by board IP, sends one UDP packet per board.
80 * PWM actuators are skipped in FIRE state.
81 */
82 /**
83 * Command every actuator for `state`.
84 *
85 * `is_transition` selects between the two things this does. On an actual state change it runs
86 * the CSV's per-actuator delays as a staged schedule; on the 1 Hz republish it sends the
87 * settled (final) positions with no delay. Without that split the republish would re-arm every
88 * pending delay once a second and the schedule would never finish.
89 */
90 void applyForState(State state, bool is_transition = false);
91
92 /**
93 * Start the 1 Hz continuous re-send loop for the given state.
94 * Stops any previously running loop first.
95 */
96 /** `allow_delays` false makes the entry apply immediately — used for abort states, which must
97 * not sit behind a delay. */
98 /**
99 * @param defer_first_pass skip the immediate pass and start republishing one period later.
100 * For a dynamic state: the caller has already applied the column, so that pass is
101 * redundant, and running it races the script that is about to start commanding valves —
102 * a race the script loses, because the pass resolves positions and only then spends a
103 * couple of milliseconds on retransmits, landing a stale command on top of the script's.
104 */
105 void startContinuousLoop(State state, bool allow_delays = true, bool defer_first_pass = false);
106
107 /** Stop the continuous re-send loop (blocks until the thread exits). */
108 void stopContinuousLoop();
109
110 /** Send one UDP packet for a single role (debug manual command). */
111 bool sendSingleActuator(const std::string& name, int pos);
112
113 /**
114 * The logical positions the CSV declares for `state_name` (role -> 0 CLOSED / 1 OPEN).
115 * Empty when the state has no column.
116 *
117 * Exposed so a timed hold can be checked against the state it expires into: the hold closes
118 * its valves only because the return state's column says CLOSE, and if it does not, the timer
119 * expires, the state changes, and the valve stays open.
120 */
121 std::map<std::string, int> positionsForState(const std::string& state_name) const;
122
123 /**
124 * The staged delay, in seconds, that `role` waits before moving on entry to `state_name`.
125 * 0 when the state, the role, or the delays file has no entry.
126 *
127 * Exposed so a timed hold can be measured from when the valve it cares about actually opens
128 * rather than from the transition: a staggered state opens its valves at different times, so
129 * "the state was held for N" and "that valve was open for N" are different claims.
130 */
131 double delayForRole(const std::string& state_name, const std::string& role) const;
132
133 /** Debug: hold this logical position for the role until cleared or state transition. */
134 void setManualOverride(const std::string& name, int pos);
135
136 /** Clear all manual overrides (e.g. leaving debug mode or transitioning state). */
138
139 /**
140 * A running script's position for one valve — the script layering on top of its state's CSV
141 * column.
142 *
143 * Deliberately NOT manual_overrides_. That map is cleared on every transition AND whenever
144 * debug mode is switched off (SequencerService::doSetDebugMode), so a script writing there
145 * would have its valve intent silently wiped by an operator leaving debug mode while the
146 * script kept running.
147 *
148 * Setting a position also takes OWNERSHIP of the valve: any staged command still in flight for
149 * it is cancelled, and it stops being skipped by the republish's pending-delay filter. Without
150 * that, the state's own delays column could move the valve seconds after the script already
151 * commanded it, landing last and silently winning.
152 *
153 * Like setManualOverride, this is not debug-gated here — the gate belongs to the caller.
154 */
155 void setScriptPosition(const std::string& name, int pos);
156
157 /** Drop every script position. Called on any transition, beside clearAllManualOverrides. */
159
160 /** Set the Elodin client for publishing commanded state [0x32, ch] to the DB. */
161 /**
162 * Which state hands PWM actuators to controller_service. During it this commander stops
163 * commanding PWM roles entirely, so exactly one thing drives them during a burn. Was the
164 * State::FIRE enumerator; now follows [fire] state like every other fire decision.
165 */
167 fire_state_ = s;
168 }
169
171 elodin_ = client;
172 }
173
174 /** Publish initial de-energized state for all actuators to Elodin DB [0x32].
175 * Does NOT send UDP commands — only populates the DB so the frontend has data on startup. */
176 void publishInitialState();
177
178 bool isLoaded() const {
179 return loaded_;
180 }
181
182private:
183 std::map<std::string, ActuatorRole> roles_;
184 /** state -> actuator -> delay in seconds, from state_machine_actuator_delays.csv. */
185 std::map<std::string, std::map<std::string, double>> state_actuator_delays_;
186 /** Bumped on every transition so an in-flight delayed stage from the previous state aborts
187 * instead of landing after we have already moved on. */
189 std::atomic<uint64_t> schedule_gen_{0};
190 /** Roles whose delayed stage has not fired yet. The 1 Hz republish skips these: the board holds
191 * its last commanded position, which is exactly the pre-transition value we want it to keep
192 * until the delay elapses. Without this the republish would send the settled position first
193 * and any delay longer than the republish period could never be observed. */
194 std::set<std::string> pending_roles_;
196 // CSV: state_name → { actuator_name → logical_pos (0 or 1) }
197 std::map<std::string, std::map<std::string, int>> state_actuators_;
198
199 std::map<std::string, int> manual_overrides_;
201
202 /** Valves a running script has taken over, role -> logical position. Its own mutex, and never
203 * held at the same time as overrides_mutex_ — applyForState snapshots this first and then
204 * takes the other, so no call path holds both and there is no lock order to get wrong. */
205 std::map<std::string, int> script_positions_;
207
208 /** Warn once per role that a manual override is fighting a running script for a valve. */
209 static void logOverrideShadowingScript(const std::string& role);
210
211 std::thread loop_thread_;
212 std::atomic<bool> loop_running_{false};
214
215 std::string bind_addr_{"0.0.0.0"};
217 uint16_t actuator_port_{5005};
218 bool loaded_{false};
220
221 // Resolve state name case-insensitively. Returns end() if not found.
222 std::map<std::string, std::map<std::string, int>>::const_iterator findStateActuators(
223 const std::string& state_name) const;
224
225 // Build and send one UDP actuator command packet to a board.
226 /** Send to every board with the retransmits interleaved, so boards stay in step. */
227 bool sendBatch(const std::map<std::string, std::vector<std::pair<uint8_t, uint8_t>>>& by_board);
228 bool sendUDP(const std::string& board_ip,
229 const std::vector<std::pair<uint8_t, uint8_t>>& id_state_pairs);
230
231 // Publish commanded actuator state [table_hi, global_channel] to Elodin DB: 0x32 (ACT_CMD) or
232 // 0x35 (BV_CMD), from ActuatorRole::cmd_table_hi. global_channel encodes board + local channel
233 // to avoid collisions across boards.
234 void publishCommandedState(uint8_t table_hi, uint8_t global_channel, uint8_t logical_pos);
235};
236
237} // namespace sequencer
Standalone Elodin database client.
Sends UDP actuator commands based on state transitions.
void clearAllManualOverrides()
Clear all manual overrides (e.g.
std::map< std::string, int > manual_overrides_
void stopContinuousLoop()
Stop the continuous re-send loop (blocks until the thread exits).
void setFireState(State s)
Set the Elodin client for publishing commanded state [0x32, ch] to the DB.
static void logOverrideShadowingScript(const std::string &role)
Warn once per role that a manual override is fighting a running script for a valve.
State fire_state_
Bumped on every transition so an in-flight delayed stage from the previous state aborts instead of la...
std::map< std::string, std::map< std::string, int > >::const_iterator findStateActuators(const std::string &state_name) const
void applyForState(State state, bool is_transition=false)
Send actuator commands for the given state (one shot).
void publishCommandedState(uint8_t table_hi, uint8_t global_channel, uint8_t logical_pos)
fsw::elodin::ElodinClient * elodin_
bool sendBatch(const std::map< std::string, std::vector< std::pair< uint8_t, uint8_t > > > &by_board)
Send to every board with the retransmits interleaved, so boards stay in step.
void setDefaultBindAddress(const std::string &address)
Pin outgoing actuator commands to a NIC, unless [actuator_service].bind_address already named one exp...
bool sendUDP(const std::string &board_ip, const std::vector< std::pair< uint8_t, uint8_t > > &id_state_pairs)
std::map< std::string, int > positionsForState(const std::string &state_name) const
The logical positions the CSV declares for state_name (role -> 0 CLOSED / 1 OPEN).
std::set< std::string > pending_roles_
Roles whose delayed stage has not fired yet.
void startContinuousLoop(State state, bool allow_delays=true, bool defer_first_pass=false)
Start the 1 Hz continuous re-send loop for the given state.
double delayForRole(const std::string &state_name, const std::string &role) const
The staged delay, in seconds, that role waits before moving on entry to state_name.
bool load(const std::string &config_content, const std::string &csv_path, const std::string &delay_csv_path="")
Load actuator roles from config content and state→actuator map from CSV.
std::map< std::string, int > script_positions_
Valves a running script has taken over, role -> logical position.
void setElodinClient(fsw::elodin::ElodinClient *client)
std::map< std::string, std::map< std::string, int > > state_actuators_
void setManualOverride(const std::string &name, int pos)
Debug: hold this logical position for the role until cleared or state transition.
void setScriptPosition(const std::string &name, int pos)
A running script's position for one valve — the script layering on top of its state's CSV column.
std::atomic< uint64_t > schedule_gen_
bool sendSingleActuator(const std::string &name, int pos)
Send one UDP packet for a single role (debug manual command).
std::map< std::string, ActuatorRole > roles_
void clearScriptPositions()
Drop every script position.
void publishInitialState()
Publish initial de-energized state for all actuators to Elodin DB [0x32].
std::map< std::string, std::map< std::string, double > > state_actuator_delays_
state -> actuator -> delay in seconds, from state_machine_actuator_delays.csv.
static ActuatorControllerState state
Definition main.cpp:57
typename Container::const_iterator const_iterator
Definition json.hpp:19039
State
System state IDs — must match the TypeScript SystemState enum in web-gui/shared/types....
Config entry for one named actuator role loaded from config.toml [actuator_roles].