90 std::map<std::string, int>& abort_map) {
91 std::ifstream f(csv_path);
95 if (!std::getline(f, line))
97 std::vector<std::string> headers;
98 std::istringstream iss(line);
100 while (std::getline(iss, cell,
','))
101 headers.push_back(
trim(cell));
103 int vent_col = -1, abort_col = -1;
104 for (
size_t i = 1; i < headers.size(); ++i) {
105 if (headers[i] ==
"Vent")
106 vent_col =
static_cast<int>(i);
107 if (headers[i] ==
"Engine Abort")
108 abort_col =
static_cast<int>(i);
110 if (vent_col < 0 || abort_col < 0)
113 while (std::getline(f, line)) {
114 std::vector<std::string> cells;
115 std::istringstream is(line);
116 while (std::getline(is, cell,
','))
117 cells.push_back(
trim(cell));
118 if (cells.size() <=
static_cast<size_t>(std::max(vent_col, abort_col)))
120 std::string name = cells[0];
121 if (name.empty() || name ==
"Test Actuator 2")
123 std::string v = cells[vent_col];
124 std::string
a = cells[abort_col];
125 std::transform(v.begin(), v.end(), v.begin(), ::toupper);
126 std::transform(
a.begin(),
a.end(),
a.begin(), ::toupper);
127 vent_map[name] = (v ==
"OPEN") ? 1 : 0;
128 abort_map[name] = (
a ==
"OPEN") ? 1 : 0;
185std::vector<ConfigPacket>
buildPackets(
const std::string& config_path) {
190 std::vector<BoardInfo> boards;
191 for (
const auto&
b : cfg.boards) {
195 const std::vector<int>&
active =
b.active_connectors;
196 std::string ip =
b.ip;
197 if (ip.empty() &&
b.board_id > 0)
198 ip =
"192.168.2." + std::to_string(
b.board_id);
199 boards.push_back({
b.board_id, ip,
b.type,
b.enabled,
b.designated_survivor,
200 b.necessary_for_abort,
b.voltage_reference,
b.enable_serial_printing,
204 std::string designated_ip;
205 int designated_id = -1;
206 for (
const auto&
b : boards) {
207 if (
b.enabled &&
b.type ==
"ACTUATOR" &&
b.designated_survivor) {
208 designated_ip =
b.ip;
209 designated_id =
b.id;
214 if (designated_ip.empty())
217 std::map<int, std::string> board_id_to_ip;
218 for (
const auto&
b : boards)
220 board_id_to_ip[
b.id] =
b.ip;
222 std::map<std::string, int> vent_map, abort_map;
223 std::string csv_path = cfg.state_machine.actuator_csv;
224 const char* csv_fbs[] = {
225 "config/state_machine_actuators.csv",
226 "../config/state_machine_actuators.csv",
227 "../../config/state_machine_actuators.csv",
229 for (
const char* fb : csv_fbs) {
238 const std::map<std::string, double>& abort_pts = cfg.abort_pts;
244 std::map<std::string, std::tuple<int, int, bool>> actuator_roles;
245 for (
const auto& [name,
r] : cfg.actuator_roles)
246 if (
r.channel >= 1 &&
r.channel <= 255 && !
r.ball_valve)
247 actuator_roles[name] = {
r.channel,
r.board_id > 0 ?
r.board_id : 12,
r.is_no};
266 std::vector<std::tuple<uint32_t, uint8_t, uint32_t>> abort_pt_list;
267 std::set<std::string> abort_warnings;
268 for (
const auto& [sensor_name, threshold_psi] : abort_pts) {
269 const std::string tag =
"[ConfigBroadcast] abort_pts \"" + sensor_name +
"\": ";
275 abort_warnings.insert(tag +
"no PT sensor_role declares this name — NO board trip");
278 if (ref->t7 !=
nullptr) {
279 abort_warnings.insert(tag +
"is a LabJack T7 input — no board can trip on it");
282 const auto&
b = *ref->board;
283 const std::string& board_key = ref->board_key;
284 const int channel = ref->channel;
285 if (channel < 1 || channel > 255) {
286 abort_warnings.insert(tag +
"channel out of range — no board abort threshold");
289 const uint16_t uid =
static_cast<uint16_t
>(
b.board_id * 100 + channel);
295 auto cit = cal_thresholds.find(uid);
296 if (cit != cal_thresholds.end() && std::abs(cit->second.first - threshold_psi) < 0.5) {
297 abort_pt_list.push_back(
298 {
ipToU32Le(
b.ip),
static_cast<uint8_t
>(channel), cit->second.second});
306 const bool is_loop =
b.has_hp_pt_keys ||
b.pt_type ==
"4-20 mA absolute";
307 std::string model = is_loop ?
"physics" :
"cubic";
308 if (
const auto* models = cfg.calibration_model_for(
"calibration_model_" + board_key)) {
309 auto mit = models->find(sensor_name);
310 if (mit != models->end())
313 double full_scale = is_loop ?
b.hp_pt_full_scale_psi : 1000.0;
314 if (
const auto* fss = cfg.full_scale_for(
"calibration_full_scale_" + board_key)) {
315 auto fit = fss->find(sensor_name);
316 if (fit != fss->end() && fit->second > 0.0)
317 full_scale = fit->second;
320 if (model !=
"physics" || is_loop) {
321 abort_warnings.insert(tag +
"model \"" + model + (is_loop ?
" (4-20 mA)" :
"") +
322 "\" cannot be inverted for a board trip — set "
323 "calibration_model = \"physics\", or this sensor has NO trip");
326 if (!(full_scale > 0.0) || !(threshold_psi > 0.0)) {
327 abort_warnings.insert(tag +
"non-positive full_scale/threshold — NO board trip");
330 constexpr double ADC_MAX = 2147483648.0;
333 std::clamp((0.1 + 0.8 * (threshold_psi / full_scale)) * ADC_MAX, 0.0, ADC_MAX - 1.0);
334 abort_pt_list.push_back(
335 {
ipToU32Le(
b.ip),
static_cast<uint8_t
>(channel),
static_cast<uint32_t
>(llround(
adc))});
340 static std::set<std::string> s_last_abort_warnings;
341 for (
const auto& w : abort_warnings)
342 if (s_last_abort_warnings.count(w) == 0)
343 std::cerr <<
"⚠️ " << w << std::endl;
344 if (abort_warnings.empty() && !s_last_abort_warnings.empty())
345 std::cout <<
"[ConfigBroadcast] all abort_pts thresholds resolved" << std::endl;
346 s_last_abort_warnings = abort_warnings;
350 int enable_serial) -> std::vector<uint8_t> {
351 std::vector<std::tuple<uint32_t, uint8_t, uint8_t, uint8_t>> abort_actuators;
352 for (
const auto& [name, tup] : actuator_roles) {
353 int ch = std::get<0>(tup), bid = std::get<1>(tup);
354 std::string ip = board_id_to_ip.count(bid) ? board_id_to_ip[bid] : designated_ip;
355 uint8_t vent =
static_cast<uint8_t
>(vent_map.count(name) ? vent_map[name] : 0);
356 uint8_t
abort =
static_cast<uint8_t
>(abort_map.count(name) ? abort_map[name] : 0);
357 abort_actuators.push_back({
ipToU32Le(ip),
static_cast<uint8_t
>(
ch), vent,
abort});
360 size_t N = std::min(abort_actuators.size(),
size_t(255));
361 size_t X = std::min(abort_pt_list.size(),
size_t(255));
362 size_t body = 1 + 1 + N * 7 + 1 + X * 9 + 1;
363 size_t total = 6 + body;
365 std::vector<uint8_t> buf(total);
372 buf[off++] =
static_cast<uint8_t
>(N);
373 for (
size_t i = 0; i < N; ++i) {
374 auto [ip, aid, vent,
abort] = abort_actuators[i];
381 buf[off++] =
static_cast<uint8_t
>(X);
382 for (
size_t i = 0; i < X; ++i) {
383 auto [ip, sid,
adc] = abort_pt_list[i];
390 buf[off] =
static_cast<uint8_t
>(enable_serial);
394 auto build_sensor_config = [&](
const BoardInfo&
b) -> std::vector<uint8_t> {
396 for (
int c :
b.active_connectors)
397 if (c >= 1 && c <= 255)
398 channels.push_back(
static_cast<uint8_t
>(c));
399 size_t num = std::min(
channels.size(),
size_t(255));
400 size_t body = 1 + num + 1 + 1 + (
b.necessary_for_abort ? 4 : 0) + 1;
401 size_t total = 6 + body;
403 std::vector<uint8_t> buf(total);
409 buf[off++] =
static_cast<uint8_t
>(num);
410 for (
size_t i = 0; i < num; ++i)
412 buf[off++] =
static_cast<uint8_t
>(std::min(2, std::max(0,
b.voltage_reference)));
413 buf[off++] =
b.necessary_for_abort ? 1 : 0;
414 if (
b.necessary_for_abort) {
415 uint32_t ip_be =
ipToU32Be(designated_ip);
416 buf[off] = (ip_be >> 24) & 0xFF;
417 buf[off + 1] = (ip_be >> 16) & 0xFF;
418 buf[off + 2] = (ip_be >> 8) & 0xFF;
419 buf[off + 3] = ip_be & 0xFF;
422 buf[off] =
static_cast<uint8_t
>(
b.enable_serial_printing);
426 std::vector<ConfigPacket> packets;
427 for (
const auto&
b : boards) {
430 if (
b.type ==
"ACTUATOR") {
431 int is_abort = (
b.id == designated_id) ? 1 : 0;
432 auto pkt = build_actuator_config(is_abort,
b.enable_serial_printing);
435 }
else if (
b.type ==
"BALL_VALVE") {
438 auto pkt = build_actuator_config(0,
b.enable_serial_printing);
441 }
else if (
b.type ==
"PT" ||
b.type ==
"TC" ||
b.type ==
"RTD" ||
b.type ==
"LC" ||
442 b.type ==
"ENCODER") {
443 auto pkt = build_sensor_config(
b);