3Shared configuration loader for the calibration stack.
5Reads config/config.toml (the single source of truth) and exposes board IPs,
6ports, sensor counts, calibration paths, and network settings so that every
7calibration script derives its parameters from the same place.
10 from config_loader import load_config, get_board_by_type, get_calibration_config, get_abort_pts
12 cfg = load_config() # full dict
13 pt = get_board_by_type("PT") # {ip, send_port, active_connectors, …}
14 cal = get_calibration_config("pt") # {json_dir, csv_paths}
15 abort_pts = get_abort_pts() # {"Fuel Upstream": 400, …} (PSI)
18from __future__
import annotations
20from pathlib
import Path
21from typing
import Any, Dict, List, Optional
25_THIS_DIR = Path(__file__).resolve().parent
26_REPO_ROOT: Optional[Path] =
None
27for _p
in [_THIS_DIR] + list(_THIS_DIR.parents):
28 if (_p /
"config" /
"config.toml").is_file():
34 _REPO_ROOT = _THIS_DIR.parent.parent
36CONFIG_PATH = _REPO_ROOT /
"config" /
"config.toml"
44 with open(path,
"rb")
as f:
45 return tomllib.load(f)
55 with open(path,
"rb")
as f:
66 return toml.load(str(path))
77 """Minimal TOML parser — enough for config.toml."""
78 text = path.read_text()
80 current_section: dict = root
82 for raw_line
in text.splitlines():
83 line = raw_line.strip()
84 if not line
or line.startswith(
"#"):
88 m = _re.match(
r"^\[([^\]]+)\]$", line)
90 keys = m.group(1).
split(
".")
93 d = d.setdefault(k, {})
98 m = _re.match(
r'^"?([^"=]+?)"?\s*=\s*(.+)$', line)
101 key = m.group(1).strip().strip(
'"')
102 val_str = m.group(2).strip()
109 if s.lower() ==
"true":
111 if s.lower() ==
"false":
114 if _re.match(
r"^0x[0-9a-fA-F]+$", s):
127 if (s.startswith(
'"')
and s.endswith(
'"'))
or (
128 s.startswith(
"'")
and s.endswith(
"'")
132 if s.startswith(
"["):
133 inner = s.strip(
"[]").strip()
137 for part
in _re.split(
r",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", inner):
145_cached_config: Optional[dict] =
None
149 """Load and cache the full config dict from config.toml."""
150 global _cached_config
151 if _cached_config
is not None and path
is None:
152 return _cached_config
153 p = path
or CONFIG_PATH
155 raise FileNotFoundError(f
"Config not found: {p}")
157 return _cached_config
165 """Return [network] section."""
167 return cfg.get(
"network", {})
171 """Return [database] section."""
173 return cfg.get(
"database", {})
177 """Return all [boards.*] as {board_name: {type, ip, …}}."""
179 return cfg.get(
"boards", {})
183 """Return the first enabled board matching the given type (PT, TC, RTD, LC, ACTUATOR).
184 Returns None if no matching enabled board is found."""
186 if board.get(
"type",
"").upper() == board_type.upper():
187 if board.get(
"enabled",
True):
188 return {**board,
"name": name}
191 if board.get(
"type",
"").upper() == board_type.upper():
192 return {**board,
"name": name}
197 """Return all boards matching the given type."""
200 if board.get(
"type",
"").upper() == board_type.upper():
201 result.append({**board,
"name": name})
206 """The connectors this board samples.
208 ``active_connectors`` is the config's only statement of which channels exist —
209 config_broadcast packs exactly this list into the board's SensorConfigPacket, and
210 the firmware's ``num_sensors`` byte is just its length. There is no separate count
211 to expand into a 1..N range, so an empty list means the board samples nothing.
213 raw = board.get(
"active_connectors")
214 if not isinstance(raw, (list, tuple)):
220 except (TypeError, ValueError):
227 Decode daq_bridge / Elodin low byte: (board_slot, connector_1_10, is_raw).
228 Matches web-gui elodin-protocol decodeLow (PT/TC/RTD/LC 0x20–0x23).
229 Returns None if not a valid raw/cal sensor slot.
233 block_offset = low & 0x1F
234 is_raw = block_offset < 0x10
235 board_slot = (low >> 5) + 1
236 connector = block_offset & 0x0F
237 if connector < 1
or connector > 10:
239 return (board_slot, connector, is_raw)
243 stype: str, board_slot: int, connector: int
246 Map physical board slot + connector to packet channel id (local connector 1–10; same as Elodin/daq_bridge).
249 if not board.get(
"enabled",
True):
251 board_id = int(board.get(
"board_id", 1))
253 slot = 10
if mod == 0
else mod
254 if slot != board_slot:
257 if connector
not in active:
259 return int(connector)
265 Build mapping (stype, packet_channel_id) → (stype, unique_ch) for relay packets.
266 Packet channel id is the local connector (1–10), matching daq_bridge / Elodin low-byte scheme.
267 unique_ch = board_id * 100 + connector_id. Returns dict for PT, TC, RTD, LC.
269 mapping: Dict[tuple, tuple] = {}
270 for stype
in (
"PT",
"TC",
"RTD",
"LC"):
272 if not board.get(
"enabled",
True):
274 board_id = board.get(
"board_id", 1)
277 unique_ch = board_id * 100 + conn
278 mapping[(stype, packet_ch)] = (stype, unique_ch)
284 True when a PT board's sensors are 4-20 mA current-loop transmitters.
286 `pt_type` is authoritative and matches what the C++ calibration service and the backend read.
287 A board written before pt_type existed is still recognised from its legacy hp_pt_* keys.
289 pt_type = board.get(
"pt_type")
290 if pt_type
is not None:
291 return pt_type ==
"4-20 mA absolute"
292 return bool(board.get(
"hp_pt_connectors"))
or "hp_pt_full_scale_psi" in board
297 Return {connector: {full_scale_psi, sense_resistor_ohms, adc_ref_voltage}} for 4-20 mA PT
298 channels. Used when the robust stack excludes HP PTs — calibration_server applies the
299 4-20 mA linear conversion.
301 The ADC reference is set once per board, so every active connector on a current-loop board
302 is a loop channel. Keys are local connector ids, so two current-loop boards would collide
303 here; board-namespacing this layer is tracked separately.
305 result: Dict[int, dict] = {}
311 "full_scale_psi": float(board.get(
"hp_pt_full_scale_psi", 5000.0)),
312 "sense_resistor_ohms": float(board.get(
"hp_pt_sense_resistor_ohms", 120.0)),
313 "adc_ref_voltage": float(board.get(
"adc_ref_voltage", 2.5)),
321 """Reverse: (stype, unique_ch) → packet_ch for legacy JSON keying."""
322 mapping: Dict[tuple, int] = {}
323 for stype
in (
"PT",
"TC",
"RTD",
"LC"):
325 if not board.get(
"enabled",
True):
327 board_id = board.get(
"board_id", 1)
330 unique_ch = board_id * 100 + conn
331 mapping[(stype, unique_ch)] = packet_ch
336 """Return [calibration.<sensor_type>] section.
337 sensor_type is case-insensitive (pt, tc, rtd, lc)."""
339 cal = cfg.get(
"calibration", {})
340 return cal.get(sensor_type.lower(), {})
344 """Return the UDP port boards send sensor data to (default 5006)."""
349 """Return [display] section."""
351 return cfg.get(
"display", {})
355 """Return [abort_pts] section: sensor role name → pressure threshold (PSI).
356 Used when building ACTUATOR_CONFIG (threshold is converted to ADC code per PT calibration).
359 return cfg.get(
"abort_pts", {})
363 """Resolve a repo-relative path from config to an absolute path."""
367 return _REPO_ROOT / p
374 """Print a human-readable summary of the loaded config."""
380 print(
" Config Summary ({})".
format(CONFIG_PATH))
383 f
" Network: bind={net.get('bind_ip','?')} sensor_port={net.get('sensor_port','?')}"
385 print(f
" Database: {db.get('host','?')}:{db.get('port','?')}")
387 for name, b
in boards.items():
388 status =
"✅" if b.get(
"enabled",
True)
else "⬜"
390 f
" {status} {name:<20s} type={b.get('type', '?'):<8s}"
391 f
" ip={b.get('ip', '?'):<16s} channels={len(active_connectors(b))}"
393 print(
" Calibration:")
394 for st
in [
"pt",
"tc",
"rtd",
"lc"]:
398 f
" {st.upper()}: json_dir={cc.get('json_dir','—')} csv={cc.get('csv_paths',['—'])}"
403if __name__ ==
"__main__":
static emmc_status_t format(emmc_log_t *log)
static std::vector< string_t > split(const string_t &reference_string)
split the string input to reference tokens
dict get_calibration_config(str sensor_type)
List[int] active_connectors(dict board)
Dict[tuple, int] build_orchestrator_key_to_packet_ch()
Dict[int, dict] get_hp_pt_packet_channels()
Path resolve_path(str rel_path)
Optional[int] packet_ch_for_board_connector(str stype, int board_slot, int connector)
Optional[tuple] decode_board_namespaced_low(int low)
List[dict] get_boards_by_type(str board_type)
Dict[str, Any] get_abort_pts()
dict get_network_config()
Dict[tuple, tuple] build_channel_to_orchestrator_key()
Optional[dict] get_board_by_type(str board_type)
Dict[str, dict] get_boards()
dict load_config(Optional[Path] path=None)
dict get_database_config()
bool is_current_loop_board(dict board)
dict get_display_config()