STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_sensor_tables.cpp
Go to the documentation of this file.
1// Pins fsw::config::find_pt_role / pt_role_tables — the role -> board -> channel -> Elodin table
2// walk that was lifted out of config_broadcast_service_main.cpp so the sequencer's pressure
3// subscriber can resolve a sensor by name instead of hardcoding a channel number.
4//
5// The two cases that matter most are the ones the old hardcoding got wrong on this very rig:
6//
7// "GN2 Regulated" board_id 21 -> slot 1 -> ch 6 -> {0x20, 0x16}
8// "GN2 High" board_id 22 -> slot 2 -> ch 4 -> {0x20, 0x34}
9//
10// ControllerService assigns its `P_copv` from channel 6, which is GN2 Regulated — the regulated
11// downstream pressure, not the COPV — and its subscriber filter (pid_lo < 0x11 || pid_lo > 0x1A)
12// drops every board-2 sensor, GN2 High included. Both are silent. A test that computes 0x34 from
13// config is what keeps a second consumer from inheriting either.
14//
15// Pure: no sockets, no files, no clock. Config comes in as a string.
16
17#include <iostream>
18#include <string>
19
20#include "config/Config.hpp"
24#include "t7/T7Stream.hpp"
25
26static int g_failures = 0;
27
28static void check(bool ok, const std::string& what) {
29 std::cout << (ok ? " ok " : " FAIL ") << what << std::endl;
30 if (!ok)
31 g_failures++;
32}
33
34// Two PT boards laid out as the server profile lays them out, plus a disabled third and an
35// ACTUATOR board that must never be searched for sensor roles.
36//
37// pt_board_z is declared FIRST in this text but sorts last, and it re-declares "GN2 High" on a
38// different channel. Which one find_pt_role returns is therefore a statement about cfg.boards
39// ordering — see the duplicate-role case below.
40static const char* kConfig = R"TOML(
41[boards.pt_board_z]
42type = "PT"
43ip = "192.168.2.99"
44board_id = 29
45enabled = true
46
47[boards.pt_board]
48type = "PT"
49ip = "192.168.2.21"
50board_id = 21
51enabled = true
52
53[boards.pt_board_2]
54type = "PT"
55ip = "192.168.2.22"
56board_id = 22
57enabled = true
58
59[boards.pt_board_off]
60type = "PT"
61ip = "192.168.2.28"
62board_id = 28
63enabled = false
64
65[boards.pt_board_slot10]
66type = "PT"
67ip = "192.168.2.30"
68board_id = 30
69enabled = true
70
71[boards.actuator_board]
72type = "ACTUATOR"
73ip = "192.168.2.12"
74board_id = 12
75enabled = true
76
77[sensor_roles_pt_board_z]
78"GN2 High" = 9
79
80[sensor_roles_pt_board]
81"Fuel Upstream" = 1
82"Ox Upstream" = 5
83"GN2 Regulated" = 6
84
85[sensor_roles_pt_board_2]
86"GSE High" = 1
87"GN2 High" = 4
88
89[sensor_roles_pt_board_off]
90"Ghost Sensor" = 3
91
92[sensor_roles_pt_board_slot10]
93"Slot Ten Sensor" = 2
94
95[sensor_roles_actuator_board]
96"Not A Sensor" = 1
97)TOML";
98
99int main() {
101 check(cfg.boards.size() == 6, "fixture parsed six boards");
102
103 // ── The two traps, computed from config ───────────────────────────────────────────────────
104 {
105 const auto r = fsw::config::find_pt_role(cfg, "GN2 Regulated");
106 check(r.has_value(), "GN2 Regulated resolves");
107 if (r) {
108 check(r->board_key == "pt_board", "GN2 Regulated is on pt_board");
109 check(r->channel == 6, "GN2 Regulated is channel 6");
110 check(r->board_number == 1, "board_id 21 -> slot 1");
111 check(r->table() == std::make_pair<uint8_t, uint8_t>(0x20, 0x16),
112 "GN2 Regulated table is {0x20, 0x16}");
113 }
114 }
115 {
116 const auto r = fsw::config::find_pt_role(cfg, "GN2 High");
117 check(r.has_value(), "GN2 High resolves");
118 if (r) {
119 check(r->board_number == 2, "board_id 22 -> slot 2");
120 check(r->channel == 4, "GN2 High is channel 4");
121 // The whole point: 0x34 is outside the 0x11..0x1A window ControllerService filters on,
122 // so a consumer that copies that filter drops the COPV sensor entirely.
123 check(
124 r->table() == std::make_pair<uint8_t, uint8_t>(0x20, 0x34),
125 "GN2 High table is {0x20, 0x34} — board 2, NOT in ControllerService's 0x11..0x1A");
126 check(r->board != nullptr && r->board->ip == "192.168.2.22",
127 "ref carries the owning board");
128 }
129 }
130
131 // ── slot() wraparound: board_id 30 -> slot 10, not slot 0 ─────────────────────────────────
132 {
133 const auto r = fsw::config::find_pt_role(cfg, "Slot Ten Sensor");
134 check(r.has_value(), "Slot Ten Sensor resolves");
135 if (r) {
136 check(r->board_number == 10, "board_id 30 -> slot 10 (not 0)");
137 // (10-1)*0x20 + 0x10 + 2 = 0x120 + 0x12 = 0x132, truncated to uint8_t = 0x32.
138 // Pinned as the encoding's actual behaviour at slot 10 rather than asserted correct:
139 // a slot-10 PT board would collide with slot 2's channel 2. Worth knowing.
140 check(r->table_lo == static_cast<uint8_t>((10 - 1) * 0x20 + 0x10 + 2),
141 "slot 10 table_lo follows the documented encoding");
142 }
143 }
144
145 // ── Boards that must not contribute roles ────────────────────────────────────────────────
146 check(!fsw::config::find_pt_role(cfg, "Ghost Sensor").has_value(),
147 "a disabled board's roles do not resolve");
148 check(!fsw::config::find_pt_role(cfg, "Not A Sensor").has_value(),
149 "a non-PT board's roles do not resolve");
150 check(!fsw::config::find_pt_role(cfg, "Nonexistent").has_value(),
151 "an unknown role resolves to nullopt, not a default");
152
153 // ── Duplicate role: which board wins, and is it stable? ──────────────────────────────────
154 //
155 // pt_board_z declares "GN2 High" on channel 9 and is written FIRST in the TOML text, but
156 // [boards] is parsed by iterating a toml++ table, which is key-sorted. So cfg.boards order is
157 // alphabetical by section key and "pt_board_2" precedes "pt_board_z". This asserts the rule
158 // that actually holds, so a future change to either ordering is caught here rather than by a
159 // sensor silently reading the wrong board.
160 {
161 const auto r = fsw::config::find_pt_role(cfg, "GN2 High");
162 check(r.has_value() && r->board_key == "pt_board_2",
163 "duplicate role resolves by cfg.boards order (key-sorted), not TOML text order");
164 }
165
166 // ── pt_role_tables: every role on every enabled PT board, and nothing else ───────────────
167 {
168 const auto all = fsw::config::pt_role_tables(cfg);
169 // 1 (z) + 3 (pt_board) + 2 (pt_board_2) + 1 (slot10) = 7; the disabled and ACTUATOR
170 // boards contribute nothing.
171 check(all.size() == 7, "pt_role_tables lists every enabled PT board's roles and no others");
172 bool saw_ghost = false, saw_actuator_role = false;
173 for (const auto& r : all) {
174 if (r.role == "Ghost Sensor")
175 saw_ghost = true;
176 if (r.role == "Not A Sensor")
177 saw_actuator_role = true;
178 }
179 check(!saw_ghost, "pt_role_tables omits a disabled board");
180 check(!saw_actuator_role, "pt_role_tables omits a non-PT board");
181 }
182
183 // ── LabJack T7 PT inputs resolve too, on the T7's calibrated table ───────────────────────
184 //
185 // The sequencer reads a pressure by role; a PT on a T7 is published by calibration_service on
186 // {0x26, (slot-1)*0x20 + 0x10 + ain} in the same CalibratedPTMessage layout. AIN6 on slot 1
187 // shares its low byte (0x16) with GN2 Regulated's ESP32 table: only the high byte differs.
188 {
189 const std::string t7 = std::string(kConfig) + R"TOML(
190[boards.labjack_1]
191type = "T7"
192ip = "192.168.2.81"
193board_id = 81
194
195[[boards.labjack_1.channels]]
196ain = 6
197kind = "PT"
198role = "T7 Tank"
199signal = "0-5V"
200full_scale = 1000.0
201
202[[boards.labjack_1.channels]]
203ain = 2
204kind = "TC"
205role = "T7 Nozzle"
206tc_type = "K"
207
208[boards.labjack_2]
209type = "T7"
210ip = "192.168.2.82"
211board_id = 82
212enabled = false
213
214[[boards.labjack_2.channels]]
215ain = 1
216kind = "PT"
217role = "Disabled T7 PT"
218signal = "0-5V"
219full_scale = 1000.0
220)TOML";
222 check(c.t7_errors.empty(), "T7 fixture parses without errors");
223 const auto r = fsw::config::find_pt_role(c, "T7 Tank");
224 check(r.has_value(), "a T7 PT input resolves by role");
225 if (r) {
226 check(r->t7 != nullptr && r->board == nullptr && r->board_key == "labjack_1",
227 "it carries its T7, and no ESP32 board");
228 check(r->channel == 6 && r->board_number == 1, "AIN6 on slot 1");
229 const auto id = fsw::t7::calibrated_table_id(1, 6);
230 check(r->table() == std::make_pair(id[0], id[1]) &&
231 r->table() == std::make_pair<uint8_t, uint8_t>(0x26, 0x16),
232 "its table is the T7 calibrated table {0x26, 0x16}, not {0x20, 0x16}");
233 }
234 check(!fsw::config::find_pt_role(c, "T7 Nozzle").has_value(), "a T7 TC is not a pressure");
235 check(!fsw::config::find_pt_role(c, "Disabled T7 PT").has_value(),
236 "nor does a PT on a disabled T7");
237 check(fsw::config::find_pt_role(c, "GN2 Regulated")->table() ==
238 std::make_pair<uint8_t, uint8_t>(0x20, 0x16),
239 "the ESP32 PT with the same low byte keeps its own table");
240 check(fsw::config::pt_role_tables(c).size() == 8, "pt_role_tables adds the one T7 PT");
241
242 const auto names = fsw::script::build_slug_tables(c);
243 check(names.tables.sensors.count(fsw::script::slugify("T7 Tank")) == 1,
244 "a state script may name it (slug T7_TANK)");
245
246 // t7_bridge refuses to stream while the T7 config has errors, so nothing it would carry
247 // may be named: a script reading it is refused at load instead of waiting on silence.
248 const fsw::config::Config broken =
250 "\n[[boards.labjack_1.channels]]\nain = 99\n"
251 "kind = \"PT\"\nrole = \"Bad\"\n");
252 check(!broken.t7_errors.empty() && !fsw::config::find_pt_role(broken, "T7 Tank"),
253 "with T7 config errors no T7 input resolves");
254 }
255
256 // ── board_key_of strips the prefix, and tolerates a section without one ──────────────────
257 {
259 b.section = "boards.pt_board_2";
260 check(fsw::config::board_key_of(b) == "pt_board_2", "board_key_of strips \"boards.\"");
261 b.section = "pt_board_2";
262 check(fsw::config::board_key_of(b) == "pt_board_2",
263 "board_key_of passes a bare key through");
264 }
265
266 std::cout << (g_failures == 0 ? "\nAll sensor-table checks passed.\n"
267 : "\nFAILURES: " + std::to_string(g_failures) + "\n");
268 return g_failures == 0 ? 0 : 1;
269}
static bool ok
Definition flight_e2e.c:435
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
std::optional< PtRoleRef > find_pt_role(const Config &cfg, const std::string &role)
Resolve one role by its canonical config name.
std::string board_key_of(const BoardConfig &b)
The board's section name minus a leading "boards.", e.g.
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
std::vector< PtRoleRef > pt_role_tables(const Config &cfg)
Every PT sensor role declared by an enabled PT board, in board-declaration order, then every PT input...
std::string slugify(const std::string &name)
Canonical name -> slug: trim, uppercase, collapse internal whitespace runs to a single '_'.
Definition Slug.cpp:8
TableBuild build_slug_tables(const fsw::config::Config &cfg)
Build the actuator / sensor / state slug tables from a loaded config.
std::array< uint8_t, 2 > calibrated_table_id(uint8_t slot, uint8_t ain)
Calibrated table for one AIN: table_id + 0x10, e.g.
Definition T7Stream.cpp:30
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
std::vector< std::string > t7_errors
Definition Config.hpp:277
static int g_failures
static boot_report_t r
static void check(bool ok, const std::string &what)
int main()
static std::string t7(const std::string &channels, const std::string &extra="", const std::string &name="lj", int board_id=81)
Definition test_t7.cpp:93
static const uint8_t kConfig[]
Definition test_ubx.c:21