18using CNV = toml::node_view<const toml::node>;
21 return n.value<std::string>().value_or(d);
24 return n.value<int64_t>().value_or(d);
27 return n.value<
double>().value_or(d);
30 return n.value<
bool>().value_or(d);
38 auto& owners = slot_owner[kind];
39 auto prev = owners.find(slot);
40 if (prev != owners.end()) {
41 std::cerr <<
"[Config] ERROR: board slot " <<
static_cast<int>(slot) <<
" claimed by both ["
42 << prev->second <<
"] and [" << section
43 <<
"] - their channels will merge into one Elodin entity. Give them board_ids "
47 owners.emplace(slot, section);
50 std::cerr <<
"[Config] ERROR: [" << section <<
"] board_id " <<
board_id <<
" maps to slot "
51 <<
static_cast<int>(slot)
52 <<
", but the packet id low byte only encodes slots 1-8. Its channels will "
53 "collide with another board."
65 if (type_str ==
"RTD")
69 if (type_str ==
"ENCODER")
71 if (type_str ==
"ENVIRONMENTAL")
73 if (type_str ==
"ACTUATOR")
75 if (type_str ==
"BALL_VALVE")
85 b.type =
s_or(bt[
"type"],
"");
86 b.ip =
s_or(bt[
"ip"],
"");
87 b.enabled =
b_or(bt[
"enabled"],
true);
88 const auto id_node = bt.contains(
"board_id") ? bt[
"board_id"] : bt[
"id"];
89 const auto id =
i_or(id_node, -1);
90 if (
b.enabled &&
b.type ==
"ENVIRONMENTAL" && (!id_node.is_integer() || id < 1 || id > 255)) {
91 throw std::invalid_argument(
"[Config] [" + section +
92 "] environmental board ID must be an integer from 1 to 255");
95 static_cast<int>(
b.type ==
"ENVIRONMENTAL" ? id :
i_or(bt[
"board_id"],
i_or(bt[
"id"], -1)));
96 b.num_actuators =
static_cast<int>(
i_or(bt[
"num_actuators"], 0));
97 b.send_port =
static_cast<uint16_t
>(
i_or(bt[
"send_port"], 5005));
98 b.listen_port =
static_cast<uint16_t
>(
i_or(bt[
"listen_port"], 5005));
99 b.voltage_reference =
static_cast<int>(
i_or(bt[
"voltage_reference"], 0));
100 b.necessary_for_abort =
b_or(bt[
"necessary_for_abort"],
false);
101 b.designated_survivor =
b_or(bt[
"designated_survivor"],
false);
106 esp =
static_cast<int>(*iv);
107 else if (
auto bv = bt[
"enable_serial_printing"].
value<bool>())
109 b.enable_serial_printing = std::clamp(esp, 0, 3);
111 if (
auto arr = bt[
"active_connectors"].as_array())
112 for (
const auto& el : *arr)
113 if (
auto iv = el.value<int64_t>())
114 b.active_connectors.push_back(
static_cast<int>(*iv));
116 b.pt_type =
s_or(bt[
"pt_type"],
"");
117 b.adc_ref_voltage =
d_or(bt[
"adc_ref_voltage"], 2.5);
118 b.has_hp_pt_keys = bt.contains(
"hp_pt_connectors") || bt.contains(
"hp_pt_full_scale_psi");
119 b.hp_pt_full_scale_psi =
d_or(bt[
"hp_pt_full_scale_psi"], 5000.0);
120 b.hp_pt_sense_resistor_ohms =
d_or(bt[
"hp_pt_sense_resistor_ohms"], 120.0);
125 std::map<int, std::string> owners;
126 for (
const auto&
b : cfg.boards) {
127 if (!
b.enabled ||
b.type !=
"ENVIRONMENTAL")
129 if (
b.board_id < 1 ||
b.board_id > 255) {
130 throw std::invalid_argument(
131 "[Config] [" +
b.section +
132 "] environmental board ID must be an integer from 1 to 255");
134 const auto [
owner, inserted] = owners.emplace(
b.board_id,
b.section);
136 throw std::invalid_argument(
"[Config] environmental board ID " +
137 std::to_string(
b.board_id) +
" is claimed by both [" +
138 owner->second +
"] and [" +
b.section +
"]");
149 static_cast<uint16_t
>(
i_or(t[
"network"][
"actuator_cmd_port"], 5005));
155 static_cast<uint32_t
>(
i_or(t[
"server_heartbeat"][
"interval_ms"], 1000));
157 static_cast<uint16_t
>(
i_or(t[
"server_heartbeat"][
"broadcast_port"], 5005));
163 static_cast<uint32_t
>(
i_or(t[
"heartbeat_service"][
"interval_ms"], 1000));
167 s_or(t[
"heartbeat_service"][
"broadcast_ip"],
170 static_cast<uint16_t
>(
i_or(t[
"heartbeat_service"][
"broadcast_port"],
171 i_or(t[
"server_heartbeat"][
"broadcast_port"], 5005)));
175 static_cast<uint16_t
>(
i_or(t[
"heartbeat_service"][
"elodin_port"], 2240));
184 const std::string m =
s_or(t[
"time_sync"][
"mode"],
"board_clock");
188 static_cast<uint32_t
>(
i_or(t[
"time_sync"][
"window_seconds"], 10));
190 static_cast<uint32_t
>(
i_or(t[
"time_sync"][
"max_plausible_gap_s"], 60));
192 static_cast<uint32_t
>(
i_or(t[
"time_sync"][
"max_batch_age_s"], 5));
194 static_cast<uint32_t
>(
i_or(t[
"time_sync"][
"resync_threshold_ms"], 1000));
196 static_cast<uint32_t
>(
i_or(t[
"time_sync"][
"log_interval_s"], 10));
200 static_cast<uint32_t
>(
i_or(t[
"config_broadcast_service"][
"interval_ms"], 1000));
202 if (
auto boards = t[
"boards"].as_table())
203 for (
auto&& [k, v] : *boards)
204 if (
auto bt = v.as_table())
207 if (
auto ar = t[
"actuator_roles"].as_table())
208 for (
auto&& [k, v] : *ar)
209 if (
auto arr = v.as_array(); arr && arr->size() >= 3) {
211 r.kind = (*arr)[0].value<std::string>().value_or(
"NC");
212 r.is_no = (
r.kind ==
"NO" ||
r.kind ==
"no");
213 r.channel =
static_cast<int>((*arr)[1].value<int64_t>().value_or(0));
214 r.board_id =
static_cast<int>((*arr)[2].value<int64_t>().value_or(0));
217 if (arr->size() >= 4)
218 r.controller_role = (*arr)[3].value<std::string>().value_or(
"");
225 for (
const auto&
b : c.
boards) {
230 std::cerr <<
"[Config] WARNING: actuator role '" << name
231 <<
"' is on ball valve board " <<
r.board_id
232 <<
" and is marked NO; ball valves have no NO/NC, treating it as "
233 "pass-through (1 = open)."
244 i_or(t[
"fire"][
"duration_ms"],
i_or(t[
"controller_service"][
"fire_duration_ms"], 6000)));
246 i_or(t[
"fire"][
"extended_ms"],
i_or(t[
"controller_service"][
"fire_extended_ms"], 10000)));
253 c.
flow.
max_ms =
static_cast<uint32_t
>(
i_or(t[
"flow"][
"max_ms"], 0));
260 static_cast<uint32_t
>(
i_or(t[
"controller_service"][
"fire_duration_ms"], 6000));
262 static_cast<uint32_t
>(
i_or(t[
"controller_service"][
"fire_extended_ms"], 10000));
264 b_or(t[
"controller_service"][
"sequencer_owns_valves"],
false);
268 static_cast<uint32_t
>(
i_or(t[
"controller"][
"pwm_duration_ms"], 10000));
285 d_or(t[
"calibration"][
"lc"][
"sensitivity_mv_per_v"], 2.0);
291 auto first_csv = [&](
const char* type) -> std::string {
292 if (
auto arr = t[
"calibration"][type][
"csv_paths"].as_array())
293 for (
const auto& el : *arr)
294 if (
auto sv = el.value<std::string>())
296 return std::string();
312 if (
auto states = t[
"states"].as_array())
313 for (
const auto& el : *states)
314 if (
auto st = el.as_table()) {
316 s.
id =
static_cast<int>((*st)[
"id"].value<int64_t>().value_or(-1));
317 s.
name = (*st)[
"name"].value<std::string>().value_or(
"");
318 s.
is_abort = (*st)[
"is_abort"].value<
bool>().value_or(
false);
319 s.
is_boot = (*st)[
"is_boot"].value<
bool>().value_or(
false);
320 s.
is_flow = (*st)[
"is_flow"].value<
bool>().value_or(
false);
321 s.
script_file = (*st)[
"script_file"].value<std::string>().value_or(
"");
323 static_cast<uint32_t
>((*st)[
"script_timeout_ms"].value<int64_t>().value_or(0));
325 (*st)[
"script_return_target"].value<std::string>().value_or(
"");
327 (*st)[
"script_timeout_target"].value<std::string>().value_or(
"");
332 for (
auto&& [k, v] : t) {
333 const std::string key(k.str());
334 if (key.rfind(
"sensor_roles", 0) == 0)
335 if (
auto rt = v.as_table()) {
337 for (
auto&& [rk, rv] : *rt)
338 if (
auto iv = rv.value<int64_t>())
339 m[std::string(rk.str())] =
static_cast<int>(*iv);
343 if (
auto ap = t[
"abort_pts"].as_table())
344 for (
auto&& [k, v] : *ap) {
345 if (
auto dv = v.value<
double>())
347 else if (
auto iv = v.value<int64_t>())
348 c.
abort_pts[std::string(k.str())] =
static_cast<double>(*iv);
352 for (
auto&& [k, v] : t) {
353 const std::string key(k.str());
354 if (key.rfind(
"calibration_model", 0) == 0)
355 if (
auto rt = v.as_table()) {
357 for (
auto&& [rk, rv] : *rt)
358 if (
auto sv = rv.value<std::string>())
359 m[std::string(rk.str())] = *sv;
365 for (
auto&& [k, v] : t) {
366 const std::string key(k.str());
367 std::map<std::string, std::map<std::string, double>>*
target =
nullptr;
368 if (key.rfind(
"calibration_full_scale", 0) == 0)
370 else if (key.rfind(
"calibration_sense_resistor", 0) == 0)
372 else if (key.rfind(
"calibration_sensitivity", 0) == 0)
374 else if (key.rfind(
"calibration_pga", 0) == 0)
377 if (
auto rt = v.as_table()) {
378 auto& m = (*target)[key];
379 for (
auto&& [rk, rv] : *rt) {
380 if (
auto dv = rv.value<
double>())
381 m[std::string(rk.str())] = *dv;
382 else if (
auto iv = rv.value<int64_t>())
383 m[std::string(rk.str())] =
static_cast<double>(*iv);
406 const std::string& section_key)
const {
421 const std::string& section_key)
const {
444 toml::table tbl = toml::parse_file(
path);
445 return from_table(tbl);
446 }
catch (
const toml::parse_error& e) {
447 std::cerr <<
"[Config] TOML parse error in " <<
path <<
": " << e.description()
450 }
catch (
const std::invalid_argument&) {
452 }
catch (
const std::exception& e) {
453 std::cerr <<
"[Config] Failed to read " <<
path <<
": " << e.what() << std::endl;
460 toml::table tbl = toml::parse(text);
461 return from_table(tbl);
462 }
catch (
const toml::parse_error& e) {
463 std::cerr <<
"[Config] TOML parse error (string): " << e.description() << std::endl;
469 validate_environmental_boards(cfg);
470 std::map<ActiveBoardKind, std::vector<elodin::BoardChannels>>
result;
471 std::map<ActiveBoardKind, std::map<uint8_t, std::string>> slot_owner;
472 for (
const auto&
b : cfg.boards) {
473 if (
b.type.empty() || !
b.enabled ||
b.board_id < 0)
478 const uint8_t slot =
b.slot();
481 check_board_slots(slot_owner, bt, slot,
b.section,
b.board_id);
484 bc.
board_id =
static_cast<uint8_t
>(
b.board_id);
489 for (
int ch :
b.active_connectors)
490 bc.
channels.push_back(
static_cast<uint8_t
>(
ch));
491 result[bt].push_back(std::move(bc));
497 std::map<uint8_t, PtBoardConfig>
result;
498 bool warned_legacy =
false;
499 for (
const auto&
b : cfg.boards) {
500 if (
b.type !=
"PT" || !
b.enabled ||
b.board_id < 0)
503 p.board_id =
static_cast<uint8_t
>(
b.board_id);
504 p.board_number =
b.slot();
506 if (!
b.pt_type.empty()) {
507 if (
b.pt_type ==
"4-20 mA absolute") {
509 }
else if (
b.pt_type ==
"0-5V ratiometric") {
511 }
else if (
b.pt_type ==
"0-5V absolute") {
512 std::cerr <<
"[Config] ERROR: [" <<
b.section <<
"] pt_type \"" <<
b.pt_type
513 <<
"\" is not supported by the PT firmware." << std::endl;
516 std::cerr <<
"[Config] ERROR: [" <<
b.section <<
"] unknown pt_type \"" <<
b.pt_type
517 <<
"\"; treating as 0-5V ratiometric." << std::endl;
519 }
else if (
b.has_hp_pt_keys) {
521 if (!warned_legacy) {
522 warned_legacy =
true;
523 std::cerr <<
"[Config] DEPRECATED: [" <<
b.section
524 <<
"] has no pt_type; inferring \"4-20 mA absolute\" from its hp_pt_* "
525 "keys. Add pt_type to the board."
530 p.full_scale_psi =
b.hp_pt_full_scale_psi;
531 p.sense_resistor_ohms =
b.hp_pt_sense_resistor_ohms;
532 p.adc_ref_voltage =
b.adc_ref_voltage;
static osThreadId_t volatile owner
static volatile uint32_t errors
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
detail namespace with internal helper functions
BoardConfig parse_board(const std::string §ion, const toml::table &bt)
toml::node_view< const toml::node > CNV
void check_board_slots(std::map< ActiveBoardKind, std::map< uint8_t, std::string > > &slot_owner, ActiveBoardKind kind, uint8_t slot, const std::string §ion, int board_id)
int64_t i_or(CNV n, int64_t d)
double d_or(CNV n, double d)
void validate_environmental_boards(const Config &cfg)
Config from_table(const toml::table &t)
std::string s_or(CNV n, const std::string &d)
std::vector< T7Board > parse_t7_boards(const toml::table &root, std::vector< std::string > &errors)
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()).
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 ...
ActiveBoardKind
Board kinds from [boards.
ActiveBoardKind kind_of(const std::string &type_str)
[boards.
Config load_from_string(const std::string &text)
Parse config from an in-memory TOML string (SequencerService / ActuatorCommander hold it so).
@ CURRENT_LOOP_4_20MA
4-20 mA absolute: supply-independent loop across a shunt resistor.
@ ABSOLUTE_0_5V
0-5V absolute. Not implemented in firmware; see load_pt_boards().
@ RATIOMETRIC_0_5V
0-5V ratiometric: excitation is the reference, ratio cancels in HW.
Config load(const std::string &path)
Parse config.toml.
Motorized ball valve position: raw ADC counts -> % open.
*brief user defined string literal for JSON values *sa std::size_t n
double rtd_adc_ref_voltage
double tc_adc_ref_voltage
double lc_sensitivity_mv_per_v
double lc_full_scale_value
The whole parsed config.toml, restricted to what C++ consumes.
ActuatorServiceConfig actuator_service
ServerHeartbeatConfig server_heartbeat
std::map< std::string, std::map< std::string, double > > calibration_sense_resistor
std::map< std::string, std::map< std::string, double > > calibration_sensitivity
const std::map< std::string, double > * sense_resistor_for(const std::string §ion_key) const
[calibration_sense_resistor_<board>] role->Ω map, or nullptr when absent/empty.
ControllerConfig controller
std::vector< BoardConfig > boards
std::vector< T7Board > t7_boards
const std::map< std::string, double > * full_scale_for(const std::string §ion_key) const
[calibration_full_scale_<board>] role->PSI map, or nullptr when absent/empty.
std::map< std::string, std::map< std::string, double > > calibration_full_scale
std::vector< std::string > t7_errors
CalibrationConfig calibration
fsw::time::TimeSyncConfig time_sync
const std::map< std::string, std::string > * calibration_model_for(const std::string §ion_key) const
[calibration_model_<board>] role->model map, or nullptr when absent/empty.
StateMachinePaths state_machine
HeartbeatServiceConfig heartbeat_service
std::map< std::string, double > abort_pts
const std::map< std::string, int > * sensor_roles_for(const std::string &board_key) const
[sensor_roles_<board>] with a legacy [sensor_roles] fallback.
std::map< std::string, ActuatorRole > actuator_roles
std::vector< StateDef > states
std::map< std::string, std::map< std::string, std::string > > calibration_models
const std::map< std::string, double > * sensitivity_for(const std::string §ion_key) const
[calibration_sensitivity_<board>] role->mV/V map, or nullptr when absent/empty.
const std::map< std::string, double > * pga_gain_for(const std::string §ion_key) const
[calibration_pga_<board>] role->gain map, or nullptr when absent/empty.
std::map< std::string, std::map< std::string, double > > calibration_pga_gain
ControllerServiceConfig controller_service
uint32_t config_broadcast_interval_ms
std::map< std::string, std::map< std::string, int > > sensor_roles
double controller_loop_hz
double fallback_ox_duty_cycle
std::string thrust_curve_path
double fallback_fuel_duty_cycle
uint32_t fire_extended_ms
uint32_t fire_duration_ms
bool sequencer_owns_valves
Who drives the valves during the burn.
std::string gate_actuator
The valve the burn is timed around, if the fire state staggers its actuators.
std::string expiry_target
std::string gate_actuator
The actuator the hold is timed around — the one whose opening actually starts the flow.
std::string return_target
uint16_t backend_udp_port
uint16_t actuator_cmd_port
Per-board 4-20 mA conversion parameters, keyed by Elodin slot (board_number).
bool send_from_daq_bridge
std::string script_timeout_target
Where the state lands when script_timeout_ms expires.
uint32_t script_timeout_ms
Wall-clock ceiling for the whole script, in milliseconds.
std::string script_file
Script filename inside the profile's scripts/ directory, e.g.
std::string script_return_target
Where the state lands when the script runs off its end without calling transition_to.
bool is_flow
Marks the state the characterization hold drives.
std::string transitions_csv
std::string actuator_delay_csv
Per-board channel info for board-namespaced entity registration.
std::vector< uint8_t > channels
uint32_t max_plausible_gap_s
uint32_t resync_threshold_ms
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")