STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
Config.hpp
Go to the documentation of this file.
1#pragma once
2
3// Centralized, typed view of config.toml, parsed once via toml++ (see Config.cpp).
4//
5// Every C++ binary used to re-open and hand-scan config.toml with its own copy-pasted parser; this
6// module is the single source of truth. Each field carries the historical call-site default so a
7// missing key reproduces the old behavior exactly. Only sections that C++ actually consumes are
8// modeled — [sim_pt_targets], [gui.*], [pressure_limits.*], [adc], [discovery] are TS/Python-only.
9//
10// NOTE: distinct from the unrelated legacy global `class Config` in lib/include/Config.h.
11
12#include <cstdint>
13#include <map>
14#include <string>
15#include <vector>
16
17#include "config/LoadActiveBoards.hpp" // ActiveBoardKind, PtInterface, PtBoardConfig, BoardChannels
18#include "config/T7Config.hpp" // T7Board
19#include "time/BoardClockSync.hpp" // fsw::time::TimeSyncConfig
20
21namespace fsw {
22namespace config {
23
25 std::string bind_ip = "0.0.0.0"; // [network].bind_ip
26 uint16_t sensor_port = 5006; // [network].sensor_port
27 uint16_t actuator_cmd_port = 5005; // [network].actuator_cmd_port
28};
29
31 std::string host = "127.0.0.1"; // [database].host
32 uint16_t port = 2240; // [database].port
33};
34
35struct ServerHeartbeatConfig { // [server_heartbeat]
36 uint32_t interval_ms = 1000; // daq_bridge default (file usually supplies)
37 uint16_t broadcast_port = 5005;
38 std::string broadcast_ip = "255.255.255.255";
39 bool send_from_daq_bridge = true; // synthesized: false when [heartbeat_service].enabled
40};
41
42struct HeartbeatServiceConfig { // [heartbeat_service]
43 bool enabled = false;
44 uint32_t interval_ms = 1000; // clamped >=100 at use site
45 std::string broadcast_ip = "192.168.2.255";
46 uint16_t broadcast_port = 5005;
47 std::string elodin_host = "127.0.0.1";
48 uint16_t elodin_port = 2240;
49};
50
51struct LogsConfig {
52 uint16_t backend_udp_port = 8092; // [logs].backend_udp_port
53};
54
55struct ControllerServiceConfig { // [controller_service]
56 uint16_t port = 9999;
57 std::string host = "127.0.0.1";
58 uint32_t fire_duration_ms = 6000;
59 uint32_t fire_extended_ms = 10000;
60 /**
61 * Who drives the valves during the burn. Default false keeps the historical split, where the
62 * sequencer hands PWM roles to controller_service and tells it when the gate opens.
63 *
64 * true means the sequencer owns them outright: it never sends FIRE_START, and the controller
65 * refuses to run its PWM gate at all. A rig with no PWM press valves wants this — otherwise
66 * the controller looks for roles it cannot find and, historically, fell back to hardcoded
67 * channel numbers that belonged to entirely different valves.
68 */
70};
71
72struct ControllerConfig { // [controller]
73 // Which actuators the controller drives is NOT stated here — it is the 4th element of the
74 // [actuator_roles] entry (see ActuatorRole::controller_role), so the assignment lives with
75 // the actuator and the sequencer's fire-state handoff reads the same fact.
76 double pwm_frequency_hz = 10.0;
77 uint32_t pwm_duration_ms = 10000;
78 double controller_loop_hz = 10.0;
79 double P_copv_min_pa = 0.0;
82 std::string lut_path;
83 std::string thrust_curve_path;
84};
85
86struct ActuatorServiceConfig { // [actuator_service]
87 uint16_t port = 9998;
88 std::string bind_address = "0.0.0.0";
89};
90
91struct CalibrationConfig { // [calibration.tc/.rtd/.lc]
92 double tc_adc_ref_voltage = 2.5;
93 double rtd_adc_ref_voltage = 2.5;
94 double rtd_excitation_ua = 1000.0;
95 double rtd_r0_ohm = 1000.0;
97 double lc_pga_gain = 32.0;
98 double lc_full_scale_value = 300.0;
99 // Per-type coefficient sources from the TOML (previously ignored; the service used to hardcode
100 // these). json_dir = newest-*.json dir of per-channel coeffs; csv_path = first of
101 // [calibration.*] csv_paths. Empty string keeps the historical hardcoded default at the call
102 // site.
106};
107
108struct FireConfig { // [fire]
109 std::string state; // "" when absent; consumers guard on empty and keep their own default
110 std::string expiry_target;
111 uint32_t duration_ms = 6000;
112 uint32_t extended_ms = 10000;
113 /**
114 * The valve the burn is timed around, if the fire state staggers its actuators.
115 *
116 * Same meaning as [flow].gate_actuator, and it matters MORE here. A fire column that opens an
117 * upstream valve first and the main a second later, with the window measured from the
118 * transition, burns for (duration - stagger) — and if the stagger equals the duration it does
119 * not burn at all while still commanding an ignition sequence. Naming the gate makes
120 * duration_ms mean "this valve is open for that long", which is what a burn time is.
121 */
122 std::string gate_actuator;
123};
124
125/**
126 * [flow] — the characterization hold: open a valve for an exact interval, then close it.
127 *
128 * Mirrors [fire], with one deliberate difference. [fire] names its state by string, which is the
129 * coupling that let a rename strand the rig mid-burn; [flow] never does. WHICH state this is comes
130 * from the `is_flow` flag on a [[states]] entry, so the operator can rename it, delete it, or move
131 * the flag to another state without touching anything here.
132 */
133struct FlowConfig { // [flow]
134 std::string return_target; // where the hold expires to; "" disables the flow hold
135 uint32_t duration_ms = 0; // default window when the client supplies none
136 uint32_t max_ms = 0; // ceiling on a client-supplied window; 0 falls back to duration_ms
137 /**
138 * The actuator the hold is timed around — the one whose opening actually starts the flow.
139 *
140 * A staggered state does not open everything at once: a pressure-regulated feed may crack an
141 * upstream valve first and only then the main, using the per-actuator delays in
142 * state_machine_actuator_delays.csv. The requested duration is what the operator wants THIS
143 * valve open for, so the sequencer holds for (its delay + the requested duration). Empty means
144 * no stagger and the hold is measured from the transition.
145 */
146 std::string gate_actuator;
147};
148
149struct StateMachinePaths { // [state_machine]
150 std::string transitions_csv = "config/state_transitions.csv";
151 std::string actuator_csv = "config/state_machine_actuators.csv";
152 std::string actuator_delay_csv = "config/state_machine_actuator_delays.csv";
153};
154
155struct StateDef { // one per [[states]]
156 int id = -1;
157 std::string name;
158 bool is_abort = false;
159 bool is_boot = false;
160 /** Marks the state the characterization hold drives. A flag rather than a name in [flow], so
161 * renaming the state cannot break it. First one wins, as with is_boot. */
162 bool is_flow = false;
163
164 // ── Dynamic states ────────────────────────────────────────────────────────────────────────
165 //
166 // A state with a script runs it on entry: the CSV column is applied first, putting every valve
167 // in a defined position, and the script then layers on top.
168 //
169 // There is deliberately NO `is_dynamic` flag. A non-empty script_file is what makes a state
170 // dynamic, for the reason [fire] demonstrates the other way: when a flag and the data it
171 // describes are separate, they can disagree, and the disagreement is silent.
172
173 /** Script filename inside the profile's scripts/ directory, e.g. "copv_press.script".
174 *
175 * A bare filename, never a path — it arrives from operator-editable config and the backend
176 * writes files at this name, so it must match ^[A-Za-z0-9_-]+\.script$ with no separators and
177 * no "..". Empty means this state is not dynamic.
178 *
179 * The script lives beside config.toml rather than inside it because writeConfig() is a full
180 * TOML re-stringify that destroys every comment and all layout on each save. Out of the TOML,
181 * a script survives a config save byte-for-byte and diffs cleanly. */
182 std::string script_file;
183
184 /** Wall-clock ceiling for the whole script, in milliseconds. Mandatory for a dynamic state:
185 * an unbounded script has no safe degraded mode, so config that omits it leaves the state not
186 * enterable rather than enterable-and-unbounded. */
187 uint32_t script_timeout_ms = 0;
188
189 /** Where the state lands when the script runs off its end without calling transition_to.
190 * Mandatory. Unreachable when every path ends in an explicit transition_to, and required
191 * anyway — it is the backstop for the path the author did not think about. */
193
194 /** Where the state lands when script_timeout_ms expires. Mandatory, and deliberately NOT
195 * defaulted to script_return_target: a runaway may want somewhere more conservative than a
196 * clean finish, and a safety landing that appears by default is the kind that is wrong
197 * silently. */
199};
200
201// [actuator_roles] value ["NC"|"NO", channel, board_id, controller_role?]
202//
203// The optional 4th element assigns the actuator to controller_service: "pwm_fuel" or "pwm_ox".
204// It is the single statement of which hardware the controller drives — the sequencer stops
205// commanding an assigned actuator in the fire state (so there is one writer during a burn), and
206// the controller resolves its PWM outputs from the same entries.
207//
208// It is a separate slot from `kind` deliberately. `kind` used to carry a third value "PWM" for
209// this, but it is also where NC/NO polarity lives, so an actuator could not be both PWM-driven
210// and normally open — and since the shipped configs needed NC/NO, nothing was ever marked "PWM"
211// and the fire-state handoff never engaged on a real rig.
213 std::string kind = "NC"; // raw first element; polarity only (NC/NO)
214 bool is_no = false; // kind == "NO"; always false on a ball valve board
215 bool ball_valve = false; // its board_id names a [boards.*] with type = "BALL_VALVE"
216 int channel = 0;
217 int board_id = 0;
218 std::string controller_role; // "pwm_fuel" | "pwm_ox"; empty = sequencer-owned
219};
220
221// One [boards.*] section. Superset of every reader's keys so a single parse serves all binaries.
223 std::string section; // e.g. "boards.pt_board" (order/diagnostics)
224 std::string type; // "PT"/"TC"/"RTD"/"LC"/"ACTUATOR"/"ENCODER"/"BALL_VALVE" (raw, unquoted)
225 std::string ip;
226 int board_id = -1; // board_id, with legacy "id" fallback
227 bool enabled = true;
229 uint16_t send_port = 5005;
230 uint16_t listen_port = 5005;
231 int enable_serial_printing = 0; // tolerate bool-or-int; clamped 0..3
235 /** Connector ids this board samples — the ONLY statement of which channels exist.
236 *
237 * These are identifiers, not a range: {1, 3, 5, 7} is ordinary, and getAdcChannel()
238 * maps each id to hardware, so an id above the count is normal. It goes on the wire
239 * verbatim as SensorConfigPacket's `N x uint8_t sensor_id`, and the packet's
240 * `num_sensors` byte is simply N — the firmware reads it as `active_count` and loops
241 * over the ids (see LC_Hotfire collect_chunk_impl).
242 *
243 * There used to be a separate config-only `num_sensors` that meant "expand to 1..N
244 * when this list is empty". Nothing on the wire or in firmware carried that meaning,
245 * and having both let a board declare {1, 2, 6} beside num_sensors = 4, which no
246 * layer defined. Empty now means no channels, which is what it reads like. */
247 std::vector<int> active_connectors;
248
249 // PT-specific (feeds pt_boards()):
250 std::string pt_type; // "" if absent
251 double adc_ref_voltage = 2.5;
252 bool has_hp_pt_keys = false; // hp_pt_connectors or hp_pt_full_scale_psi present (legacy infer)
253 double hp_pt_full_scale_psi = 5000.0;
255
256 /** Elodin slot: board_id % 10, with 0 mapping to 10. */
257 uint8_t slot() const {
258 int m = board_id % 10;
259 return static_cast<uint8_t>(m == 0 ? 10 : m);
260 }
261};
262
263/** The whole parsed config.toml, restricted to what C++ consumes. */
264struct Config {
271 uint32_t config_broadcast_interval_ms = 1000; // [config_broadcast_service].interval_ms
272 std::vector<BoardConfig> boards; // parse order preserved
273 // [boards.*] with type = "T7" (LabJack, read by t7_bridge only). They also appear in `boards`
274 // with type "T7", which every other reader skips as an unknown type. A board named in any
275 // t7_errors entry must not be streamed; t7_bridge refuses to start while the list is non-empty.
276 std::vector<T7Board> t7_boards;
277 std::vector<std::string> t7_errors;
278 std::map<std::string, ActuatorRole> actuator_roles;
286 std::vector<StateDef> states;
287 // section name -> {role name -> channel}, e.g. "sensor_roles_pt_board" -> {"Fuel Upstream": 1}.
288 std::map<std::string, std::map<std::string, int>> sensor_roles;
289 std::map<std::string, double> abort_pts; // [abort_pts]
290 // section name -> {role name -> "cubic"|"robust"|"physics"|"blend"} for a PT board, or
291 // "linear"|"cubic"|"physics" for an LC board (linear is the LC default), e.g.
292 // "calibration_model_pt_board" -> {"Ox Upstream": "robust"}. Per-sensor streaming model.
293 std::map<std::string, std::map<std::string, std::string>> calibration_models;
294 // Per-sensor physics-mode parameters, role-keyed, parallel to calibration_models:
295 // [calibration_full_scale_<board>] role->PSI (PT) or role->kg (LC) and
296 // [calibration_sense_resistor_<board>] role->Ω (PT 4-20 mA only).
297 std::map<std::string, std::map<std::string, double>> calibration_full_scale;
298 std::map<std::string, std::map<std::string, double>> calibration_sense_resistor;
299 // LC-only physics params, role-keyed, parallel to calibration_full_scale:
300 // [calibration_sensitivity_<board>] role->mV/V and [calibration_pga_<board>] role->gain.
301 std::map<std::string, std::map<std::string, double>> calibration_sensitivity;
302 std::map<std::string, std::map<std::string, double>> calibration_pga_gain;
303
304 /** [sensor_roles_<board>] with a legacy [sensor_roles] fallback. */
305 const std::map<std::string, int>* sensor_roles_for(const std::string& board_key) const;
306
307 /** [calibration_model_<board>] role->model map, or nullptr when absent/empty. */
308 const std::map<std::string, std::string>* calibration_model_for(
309 const std::string& section_key) const;
310
311 /** [calibration_full_scale_<board>] role->PSI map, or nullptr when absent/empty. */
312 const std::map<std::string, double>* full_scale_for(const std::string& section_key) const;
313
314 /** [calibration_sense_resistor_<board>] role->Ω map, or nullptr when absent/empty. */
315 const std::map<std::string, double>* sense_resistor_for(const std::string& section_key) const;
316
317 /** [calibration_sensitivity_<board>] role->mV/V map, or nullptr when absent/empty. */
318 const std::map<std::string, double>* sensitivity_for(const std::string& section_key) const;
319
320 /** [calibration_pga_<board>] role->gain map, or nullptr when absent/empty. */
321 const std::map<std::string, double>* pga_gain_for(const std::string& section_key) const;
322};
323
324/** Parse config.toml. Invalid environmental identities throw std::invalid_argument;
325 * other read/parse errors log and return a default-constructed Config. */
326Config load(const std::string& path);
327
328/** Parse config from an in-memory TOML string (SequencerService / ActuatorCommander hold it so). */
329Config load_from_string(const std::string& text);
330
331/**
332 * Derive the active-board table (enabled boards -> Elodin BoardChannels), with the same
333 * slot-collision validation the old load_active_boards() logged. Free-function form used by
334 * daq_bridge/calibration/sequencer.
335 */
336std::map<ActiveBoardKind, std::vector<elodin::BoardChannels>> active_boards(const Config& cfg);
337
338/** [boards.*].type string -> kind; anything unrecognised is UNKNOWN. */
339ActiveBoardKind kind_of(const std::string& type_str);
340
341/** Derive PT board 4-20 mA parameters keyed by Elodin slot (mirrors old load_pt_boards()). */
342std::map<uint8_t, PtBoardConfig> pt_boards(const Config& cfg);
343
344} // namespace config
345} // namespace fsw
Per-board clock sync for DAQ sample timestamping.
std::map< uint8_t, PtBoardConfig > pt_boards(const Config &cfg)
Derive PT board 4-20 mA parameters keyed by Elodin slot (mirrors old load_pt_boards()).
Definition Config.cpp:496
std::map< ActiveBoardKind, std::vector< elodin::BoardChannels > > active_boards(const Config &cfg)
Derive the active-board table (enabled boards -> Elodin BoardChannels), with the same slot-collision ...
Definition Config.cpp:468
ActiveBoardKind
Board kinds from [boards.
ActiveBoardKind kind_of(const std::string &type_str)
[boards.
Definition Config.cpp:60
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
Config load(const std::string &path)
Parse config.toml.
Definition Config.cpp:442
Motorized ball valve position: raw ADC counts -> % open.
std::string controller_role
Definition Config.hpp:218
std::vector< int > active_connectors
Connector ids this board samples — the ONLY statement of which channels exist.
Definition Config.hpp:247
uint8_t slot() const
Elodin slot: board_id % 10, with 0 mapping to 10.
Definition Config.hpp:257
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
ActuatorServiceConfig actuator_service
Definition Config.hpp:283
ServerHeartbeatConfig server_heartbeat
Definition Config.hpp:267
std::map< std::string, std::map< std::string, double > > calibration_sense_resistor
Definition Config.hpp:298
FireConfig fire
Definition Config.hpp:279
DatabaseConfig database
Definition Config.hpp:266
std::map< std::string, std::map< std::string, double > > calibration_sensitivity
Definition Config.hpp:301
const std::map< std::string, double > * sense_resistor_for(const std::string &section_key) const
[calibration_sense_resistor_<board>] role->Ω map, or nullptr when absent/empty.
Definition Config.cpp:420
ControllerConfig controller
Definition Config.hpp:282
std::vector< BoardConfig > boards
Definition Config.hpp:272
std::vector< T7Board > t7_boards
Definition Config.hpp:276
FlowConfig flow
Definition Config.hpp:280
const std::map< std::string, double > * full_scale_for(const std::string &section_key) const
[calibration_full_scale_<board>] role->PSI map, or nullptr when absent/empty.
Definition Config.cpp:413
std::map< std::string, std::map< std::string, double > > calibration_full_scale
Definition Config.hpp:297
std::vector< std::string > t7_errors
Definition Config.hpp:277
CalibrationConfig calibration
Definition Config.hpp:284
fsw::time::TimeSyncConfig time_sync
Definition Config.hpp:270
const std::map< std::string, std::string > * calibration_model_for(const std::string &section_key) const
[calibration_model_<board>] role->model map, or nullptr when absent/empty.
Definition Config.cpp:405
StateMachinePaths state_machine
Definition Config.hpp:285
HeartbeatServiceConfig heartbeat_service
Definition Config.hpp:268
std::map< std::string, double > abort_pts
Definition Config.hpp:289
const std::map< std::string, int > * sensor_roles_for(const std::string &board_key) const
[sensor_roles_<board>] with a legacy [sensor_roles] fallback.
Definition Config.cpp:395
std::map< std::string, ActuatorRole > actuator_roles
Definition Config.hpp:278
std::vector< StateDef > states
Definition Config.hpp:286
std::map< std::string, std::map< std::string, std::string > > calibration_models
Definition Config.hpp:293
const std::map< std::string, double > * sensitivity_for(const std::string &section_key) const
[calibration_sensitivity_<board>] role->mV/V map, or nullptr when absent/empty.
Definition Config.cpp:428
const std::map< std::string, double > * pga_gain_for(const std::string &section_key) const
[calibration_pga_<board>] role->gain map, or nullptr when absent/empty.
Definition Config.cpp:435
NetworkConfig network
Definition Config.hpp:265
std::map< std::string, std::map< std::string, double > > calibration_pga_gain
Definition Config.hpp:302
ControllerServiceConfig controller_service
Definition Config.hpp:281
uint32_t config_broadcast_interval_ms
Definition Config.hpp:271
LogsConfig logs
Definition Config.hpp:269
std::map< std::string, std::map< std::string, int > > sensor_roles
Definition Config.hpp:288
bool sequencer_owns_valves
Who drives the valves during the burn.
Definition Config.hpp:69
std::string gate_actuator
The valve the burn is timed around, if the fire state staggers its actuators.
Definition Config.hpp:122
std::string expiry_target
Definition Config.hpp:110
[flow] — the characterization hold: open a valve for an exact interval, then close it.
Definition Config.hpp:133
std::string gate_actuator
The actuator the hold is timed around — the one whose opening actually starts the flow.
Definition Config.hpp:146
std::string return_target
Definition Config.hpp:134
uint16_t backend_udp_port
Definition Config.hpp:52
std::string script_timeout_target
Where the state lands when script_timeout_ms expires.
Definition Config.hpp:198
uint32_t script_timeout_ms
Wall-clock ceiling for the whole script, in milliseconds.
Definition Config.hpp:187
std::string script_file
Script filename inside the profile's scripts/ directory, e.g.
Definition Config.hpp:182
std::string script_return_target
Where the state lands when the script runs off its end without calling transition_to.
Definition Config.hpp:192
bool is_flow
Marks the state the characterization hold drives.
Definition Config.hpp:162