47#include <unordered_map>
157 const char* v = std::getenv(
"CAL_VERBOSE");
158 return v && (v[0] ==
'1' || v[0] ==
'y' || v[0] ==
'Y');
162 const char* v = std::getenv(name);
163 return v && (v[0] ==
'1' || v[0] ==
'y' || v[0] ==
'Y' || v[0] ==
't' || v[0] ==
'T');
270 constexpr double ADC_MAX = 2147483648.0;
271 if (!(full_scale_psi > 0.0))
273 const double frac =
static_cast<double>(adc_raw) / ADC_MAX;
275 if (!std::isfinite(psi))
277 return std::clamp(psi, -0.05 * full_scale_psi, full_scale_psi * 1.05);
286static double select_pt_psi(uint16_t uid,
double psi_fac,
double psi_rob,
double psi_phys,
287 bool fac_ok,
bool has_robust) {
296 return has_robust ? psi_rob : 0.0;
298 if (fac_ok && has_robust) {
299 constexpr double kFactoryWeight = 0.25;
300 return (1.0 - kFactoryWeight) * psi_rob + kFactoryWeight * psi_fac;
302 return fac_ok ? psi_fac : (has_robust ? psi_rob : 0.0);
307 return fac_ok ? psi_fac : 0.0;
319static double select_lc_kg(uint16_t uid,
double kg_fit,
double kg_phys,
bool fit_ok) {
322 return fit_ok ? kg_fit : kg_phys;
324 return fit_ok ? kg_fit : 0.0;
341 double full_scale_psi,
double sense_resistor_ohms,
342 std::array<uint8_t, 11>& live_state) {
345 const double adc_ref_voltage =
board.adc_ref_voltage;
347 constexpr double ADC_MAX = 2147483648.0;
348 constexpr double I_MIN_MA = 4.0;
349 constexpr double I_SPAN_MA = 16.0;
351 const char* hyst_env = std::getenv(
"CAL_HP_MA_HYST");
352 const double hyst = (hyst_env && hyst_env[0] ==
'0' && hyst_env[1] ==
'\0') ? 0.0 : 0.08;
353 const double i_on = I_MIN_MA + hyst;
354 const double i_off = I_MIN_MA - std::min(hyst * 0.75, 0.06);
356 if (local_ch < 1 || local_ch > 10)
358 if (sense_resistor_ohms <= 0.0 || !std::isfinite(adc_ref_voltage))
362 uint32_t
adc = std::min(adc_raw,
static_cast<uint32_t
>(ADC_MAX) - 1u);
364 double v_sense = (
static_cast<double>(
adc) / ADC_MAX) * adc_ref_voltage;
366 double i_ma = (v_sense / sense_resistor_ohms) * 1000.0;
367 if (!std::isfinite(v_sense) || !std::isfinite(i_ma))
370 uint8_t& live = live_state[local_ch];
389 i_ma = std::max(i_ma, I_MIN_MA);
391 double psi = ((i_ma - I_MIN_MA) / I_SPAN_MA) * full_scale_psi;
392 if (!std::isfinite(psi))
394 return std::clamp(psi, 0.0, full_scale_psi * 1.05);
404 constexpr double ADC_MAX = 2147483648.0;
405 double voltage_v = (
static_cast<double>(adc_raw) / ADC_MAX) * adc_ref_voltage;
406 double v_mv = voltage_v * 1000.0;
410 return std::isfinite(t) ? t : 0.0;
418 if (!std::isfinite(t_c))
420 return std::clamp(t_c, -300.0, 2200.0);
428 double excitation_ua,
double r0_ohm) {
438 constexpr double ADC_MAX_12BIT = 4095.0;
439 constexpr double V_REF = 3.3;
440 return (
static_cast<double>(adc_raw) / ADC_MAX_12BIT) * V_REF;
449 double full_scale_value) {
450 constexpr double ADC_FULL_SCALE = 2147483648.0;
451 if (pga_gain <= 0.0 || sensitivity_mv_per_v <= 0.0)
453 double code_fs = (sensitivity_mv_per_v / 1000.0) * pga_gain * ADC_FULL_SCALE;
456 return (
static_cast<double>(adc_raw) / code_fs) * full_scale_value;
460 std::cout <<
"\n[CalibrationService] Caught signal, shutting down..." << std::endl;
469 return std::chrono::duration<double>(std::chrono::system_clock::now().time_since_epoch())
474 return static_cast<uint64_t
>(std::chrono::duration_cast<std::chrono::nanoseconds>(
475 std::chrono::steady_clock::now().time_since_epoch())
484 const std::vector<fsw::elodin::BoardChannels>& pt_boards) {
485 if (
ch == 0 ||
ch > 10)
486 return static_cast<uint16_t
>(100u +
ch);
489 unsigned delta =
static_cast<unsigned>(type_lo -
ch);
490 bn_slot =
static_cast<uint8_t
>(delta / 0x20u + 1u);
496 for (
const auto& bc : pt_boards) {
497 int mod =
static_cast<int>(bc.board_id % 10);
498 int slot = (mod == 0) ? 10 : mod;
499 if (slot ==
static_cast<int>(bn_slot))
500 return static_cast<uint16_t
>(bc.board_id) * 100u +
ch;
502 return static_cast<uint16_t
>(100u +
ch);
511 const std::vector<fsw::elodin::BoardChannels>& lc_boards) {
512 if (
ch == 0 ||
ch > 10)
513 return static_cast<uint16_t
>(100u +
ch);
516 unsigned delta =
static_cast<unsigned>(type_lo -
ch);
517 bn_slot =
static_cast<uint8_t
>(delta / 0x20u + 1u);
523 for (
const auto& bc : lc_boards) {
524 int mod =
static_cast<int>(bc.board_id % 10);
525 int slot = (mod == 0) ? 10 : mod;
526 if (slot ==
static_cast<int>(bn_slot))
527 return static_cast<uint16_t
>(bc.board_id) * 100u +
ch;
529 return static_cast<uint16_t
>(100u +
ch);
556 double target_psi,
bool force_physics,
559 if (!(target_psi > 0.0))
564 const bool has_rob = robust_manager.
has_sensor(uid);
565 auto forward = [&](int64_t
adc) ->
double {
566 const int32_t
a =
static_cast<int32_t
>(
adc);
567 std::array<uint8_t, 11> hyst{};
568 const double psi_phys =
569 is_loop && loop_board !=
nullptr
571 static_cast<uint32_t
>(
adc), *loop_board, full_scale,
572 sense_resistor, hyst)
578 return select_pt_psi(uid, psi_fac, psi_rob, psi_phys, fac_ok, has_rob);
580 constexpr int64_t kAdcMax = 2147483647;
581 if (forward(kAdcMax) < target_psi)
583 if (forward(0) > target_psi)
585 int64_t lo = 0, hi = kAdcMax;
586 for (
int i = 0; i < 40 && hi - lo > 1; ++i) {
587 const int64_t mid = lo + (hi - lo) / 2;
588 if (forward(mid) < target_psi)
593 return static_cast<int32_t
>(hi);
596int main(
int argc,
char* argv[]) {
597 std::string config_path =
"config/config.toml";
598 std::string elodin_host =
"127.0.0.1";
599 uint16_t elodin_port = 2240;
600 std::string adjustments_path =
"scripts/calibration/calibrations/adjustments.json";
602 for (
int i = 1; i < argc; ++i) {
603 std::string arg = argv[i];
604 if (arg ==
"--config" && i + 1 < argc)
605 config_path = argv[++i];
606 else if ((arg ==
"--host" || arg ==
"--elodin-host") && i + 1 < argc)
607 elodin_host = argv[++i];
608 else if ((arg ==
"--port" || arg ==
"--elodin-port") && i + 1 < argc)
609 elodin_port =
static_cast<uint16_t
>(std::atoi(argv[++i]));
610 else if (arg ==
"--adjustments" && i + 1 < argc)
611 adjustments_path = argv[++i];
612 else if (arg ==
"--help" || arg ==
"-h") {
613 std::cout <<
"Usage: " << argv[0]
614 <<
" [--config PATH] [--elodin-host HOST] [--elodin-port PORT]\n";
621 const std::string default_adj =
"scripts/calibration/calibrations/adjustments.json";
622 if (adjustments_path == default_adj) {
623 const char* env_override = std::getenv(
"CAL_BACKUP_PATH");
624 if (env_override && env_override[0] !=
'\0') {
625 adjustments_path = env_override;
627 const std::string backup_dir =
"calibration_backups";
628 std::string best_path;
629 auto best_time = std::filesystem::file_time_type::min();
631 for (
const auto& entry : std::filesystem::directory_iterator(backup_dir)) {
632 if (!entry.is_regular_file())
634 const std::string name = entry.path().filename().string();
635 if (entry.path().extension() !=
".json")
637 if (name.rfind(
"calibration_backup_", 0) != 0)
639 const auto t = entry.last_write_time();
642 best_path = entry.path().string();
648 if (!best_path.empty())
649 adjustments_path = best_path;
653 std::cout <<
"=== Calibration Service (C++) ===" << std::endl;
654 std::cout <<
" Elodin DB: " << elodin_host <<
":" << elodin_port << std::endl;
661 auto or_default = [](
const std::string& v,
const char* d) {
662 return v.empty() ? d : v;
672 "scripts/calibration/calibrations",
673 "firmware/PT_Board/Calibration/PT Calibration Attempt 2026-02-04_test2.csv");
694 std::cout <<
"[Calibration] PT: physics-or-nothing baseline — cubics come from operator "
695 "captures / bench-cal import only (resumed from the cubic store below)"
697 std::cout <<
"[Calibration] TC: " << tc_calibration.
calibrated_count() <<
" channels"
699 std::cout <<
"[Calibration] RTD: " << rtd_calibration.
calibrated_count() <<
" channels"
701 std::cout <<
"[Calibration] LC: " << lc_calibration.
calibrated_count() <<
" channels"
710 auto it = active_boards.find(kind);
711 return it == active_boards.end() ? std::vector<BoardChannels>{} : it->second;
728 auto it = pt_board_configs.find(board_number);
734 for (
const auto& [slot,
board] : pt_board_configs) {
737 size_t channel_count = 0;
738 for (
const auto& bc : pt_boards)
739 if (bc.board_number == slot)
740 channel_count = bc.channels.size();
741 std::cout <<
"[Calibration] HP PT slot " <<
static_cast<int>(slot) <<
" (board_id "
742 <<
static_cast<int>(
board.board_id) <<
"): " << channel_count
743 <<
" channels (4-20 mA, " <<
board.full_scale_psi <<
" PSI full scale, "
744 <<
board.sense_resistor_ohms <<
" ohm shunt)" << std::endl;
756 std::cout <<
"[Calibration] TC default: ITS-90 K-type, Vref=" << tc_adc_ref_voltage <<
"V"
758 std::cout <<
"[Calibration] RTD default: CVD Pt" << (int)rtd_r0_ohm
759 <<
", Vref=" << rtd_adc_ref_voltage <<
"V, I=" << rtd_excitation_ua <<
"µA"
761 std::cout <<
"[Calibration] LC default: " << lc_sensitivity_mv_per_v
762 <<
"mV/V, PGA=" << lc_pga_gain <<
", FS=" << lc_full_scale_value <<
"kg" << std::endl;
784 for (
const auto&
b : cal_cfg.
boards) {
785 if (
b.type !=
"PT" || !
b.enabled ||
b.board_id < 0)
787 const bool is_loop =
b.has_hp_pt_keys ||
b.pt_type ==
"4-20 mA absolute";
788 const std::string board_key =
789 b.section.rfind(
"boards.", 0) == 0 ?
b.section.substr(7) :
b.section;
791 if (roles ==
nullptr)
794 const auto* full_scales = cal_cfg.
full_scale_for(
"calibration_full_scale_" + board_key);
795 const auto* resistors =
799 const double board_full_scale = is_loop ?
b.hp_pt_full_scale_psi : 1000.0;
800 const double board_resistor =
b.hp_pt_sense_resistor_ohms;
801 for (
const auto& [role, connector] : *roles) {
802 if (connector < 1 || connector > 99)
804 const uint16_t uid =
static_cast<uint16_t
>(
b.board_id * 100 + connector);
808 if (models !=
nullptr) {
809 auto it = models->find(role);
810 if (it != models->end())
816 double fs = board_full_scale, rs = board_resistor;
817 if (full_scales !=
nullptr) {
818 auto it = full_scales->find(role);
819 if (it != full_scales->end() && it->second > 0.0)
822 if (resistors !=
nullptr) {
823 auto it = resistors->find(role);
824 if (it != resistors->end() && it->second > 0.0)
840 for (
const auto&
b : cal_cfg.
boards) {
841 if (
b.type !=
"LC" || !
b.enabled ||
b.board_id < 0)
843 const std::string board_key =
844 b.section.rfind(
"boards.", 0) == 0 ?
b.section.substr(7) :
b.section;
846 if (roles ==
nullptr)
849 const auto* full_scales = cal_cfg.
full_scale_for(
"calibration_full_scale_" + board_key);
850 const auto* sensitivities = cal_cfg.
sensitivity_for(
"calibration_sensitivity_" + board_key);
851 const auto* pga_gains = cal_cfg.
pga_gain_for(
"calibration_pga_" + board_key);
852 for (
const auto& [role, connector] : *roles) {
853 if (connector < 1 || connector > 99)
855 const uint16_t uid =
static_cast<uint16_t
>(
b.board_id * 100 + connector);
858 if (models !=
nullptr) {
859 auto it = models->find(role);
860 if (it != models->end())
863 if (full_scales !=
nullptr) {
864 auto it = full_scales->find(role);
865 if (it != full_scales->end() && it->second > 0.0)
868 if (sensitivities !=
nullptr) {
869 auto it = sensitivities->find(role);
870 if (it != sensitivities->end() && it->second > 0.0)
873 if (pga_gains !=
nullptr) {
874 auto it = pga_gains->find(role);
875 if (it != pga_gains->end() && it->second > 0.0)
891 std::cerr <<
"[Calibration] T7: config has " << cal_cfg.
t7_errors.size()
892 <<
" problem(s); t7_bridge will not stream, so no T7 input is calibrated"
894 for (
const auto& t : t7_map.
all()) {
895 if (t.channel.kind !=
"PT" && t.channel.kind !=
"LC")
898 std::optional<std::string> model;
899 if (
const auto* models =
901 auto it = models->find(t.channel.role);
902 if (it != models->end())
905 if (t.channel.kind ==
"PT")
910 std::cout <<
"[Calibration] T7: " << t7_map.
all().size() <<
" input(s) on "
911 << cal_cfg.
t7_boards.size() <<
" board(s)" << std::endl;
918 std::map<uint16_t, std::string> uid_role;
920 if (t7_map.
by_uid(u) ==
nullptr)
921 uid_role.emplace(u,
r);
932 "scripts/calibration/calibrations/cubic_calibration.json");
943 for (
const auto& bc : pt_boards) {
944 for (uint8_t local_ch : bc.channels) {
945 const uint16_t uid =
static_cast<uint16_t
>(bc.board_id) * 100u + local_ch;
946 const uint8_t log_ch =
949 const std::string role = rit !=
g_uid_role.end() ? rit->second : std::string();
963 for (
const auto& bc : lc_boards) {
965 for (uint8_t local_ch : bc.channels) {
966 const uint16_t uid =
static_cast<uint16_t
>(bc.board_id) * 100u + local_ch;
967 const uint8_t log_ch =
970 const std::string role = rit !=
g_uid_role.end() ? rit->second : std::string();
980 for (
const auto& t : t7_map.
all()) {
981 const bool is_lc = t.channel.kind ==
"LC";
982 if (t.channel.kind !=
"PT" && !is_lc)
987 t.uid,
static_cast<uint8_t
>(t.board_id),
static_cast<uint8_t
>(t.channel.ain),
988 t.logical_ch, t.channel.role,
999 auto sample_robust_curve = [&](uint16_t uid) -> std::vector<std::pair<double, double>> {
1000 std::vector<std::pair<double, double>> curve;
1002 if (cch ==
nullptr || cch->
points.empty())
1004 double amin = cch->
points.front().adc, amax = amin;
1005 for (
const auto&
p : cch->
points) {
1006 amin = std::min(amin,
p.adc);
1007 amax = std::max(amax,
p.adc);
1011 constexpr int kSamples = 40;
1012 curve.reserve(kSamples + 1);
1013 for (
int i = 0; i <= kSamples; ++i) {
1014 const double adc = amin + (amax - amin) * i / kSamples;
1017 curve.emplace_back(
adc, psi);
1032 auto write_abort_thresholds = [&]() {
1033 const std::string out_path =
"scripts/calibration/calibrations/abort_thresholds.txt";
1034 const long long now_unix = std::chrono::duration_cast<std::chrono::seconds>(
1035 std::chrono::system_clock::now().time_since_epoch())
1037 std::ostringstream body;
1038 body <<
"# abort thresholds — one line per PT: uid target_psi raw_adc\n";
1039 body <<
"# regenerated on every capture/clear/reload. generated_unix " << now_unix <<
"\n";
1040 for (
const auto& [role, target_psi] : cal_cfg.
abort_pts) {
1050 std::cerr <<
"[Calibration] abort_pts \"" << role
1051 <<
"\": no PT sensor_role declares this name — no threshold emitted"
1055 if (t7_map.
by_uid(uid) !=
nullptr) {
1058 std::cerr <<
"[Calibration] abort_pts \"" << role
1059 <<
"\" is a LabJack T7 input — board abort thresholds can only use an "
1060 "ESP32 PT; no threshold emitted"
1067 uint8_t board_number =
static_cast<uint8_t
>((uid / 100) % 10);
1068 if (board_number == 0)
1071 const bool is_loop = loop_board !=
nullptr;
1072 const int npoints =
static_cast<int>(cch->
points.size());
1077 std::optional<int32_t>
adc =
1079 sparse, pt_calibration, robust_manager);
1085 true, pt_calibration, robust_manager);
1087 std::cerr <<
"[Calibration] abort_pts \"" << role <<
"\" (" << target_psi
1088 <<
" psi): no usable mapping (full_scale 0?) — no threshold emitted"
1093 std::cout <<
"[Calibration] abort_pts \"" << role <<
"\": only " << npoints
1095 <<
") — using physics threshold" << std::endl;
1096 body << uid <<
" " << target_psi <<
" " <<
static_cast<uint32_t
>(*adc) <<
"\n";
1098 const std::string tmp = out_path +
".tmp";
1100 std::ofstream f(tmp, std::ios::trunc);
1104 std::filesystem::rename(tmp, out_path, ec);
1106 std::cerr <<
"[Calibration] abort_thresholds write failed: " << ec.message()
1110 auto apply_pt_capture = [&](uint16_t uid,
double adc_avg,
double ref) {
1113 if (fit.
valid && cch !=
nullptr)
1116 robust_manager.
update_calibration(uid,
static_cast<int32_t
>(std::llround(adc_avg)), ref);
1124 write_abort_thresholds();
1131 return [&, uid](
double adc) ->
double {
1134 const int32_t code =
static_cast<int32_t
>(
adc);
1135 const bool cubic_ok = lc_calibration.
is_calibrated(t->logical_ch);
1136 const double kg_cubic =
1137 cubic_ok ? lc_calibration.
calculate(t->logical_ch, code) : 0.0;
1139 return select_lc_kg(uid, kg_cubic, std::isfinite(kg_phys) ? kg_phys : 0.0,
1142 uint8_t board_number =
static_cast<uint8_t
>((uid / 100) % 10);
1143 if (board_number == 0)
1145 const uint8_t connector =
static_cast<uint8_t
>(uid % 100);
1146 const uint8_t lc_log_ch =
1148 const int32_t code =
static_cast<int32_t
>(
adc);
1149 const bool cubic_ok = lc_calibration.
is_calibrated(lc_log_ch);
1150 const double kg_cubic = cubic_ok ? lc_calibration.
calculate(lc_log_ch, code) : 0.0;
1164 auto recompute_tare = [&](uint16_t uid) {
1166 lc_tare_store.
recompute(uid, lc_eval_for(uid));
1167 lc_tare_store.
save();
1169 auto recompute_all_tares = [&]() {
1172 return lc_eval_for(u);
1174 lc_tare_store.
save();
1178 auto apply_lc_capture = [&](uint16_t uid,
double adc_avg,
double ref) {
1181 if (fit.
valid && cch !=
nullptr)
1183 fit.
A, fit.
B, fit.
C, fit.
D,
"kg"));
1186 recompute_tare(uid);
1197 std::cout <<
"[Cal] " << what <<
" uid=" <<
static_cast<int>(uid) <<
" REJECTED ("
1198 <<
r.reason <<
"): newest sample " <<
r.age_ms <<
"ms old, " <<
r.n
1199 <<
" in last " <<
r.window_ms <<
"ms (limit " <<
r.max_age_ms <<
"ms)"
1206 std::ostringstream
o;
1207 o <<
"n=" <<
r.n <<
" span=" <<
r.span_ms <<
"ms age=" <<
r.age_ms <<
"ms adc=" <<
r.adc_avg
1208 <<
" spread=" <<
r.spread <<
" drift=" <<
r.drift <<
" z=" <<
r.drift_z
1209 << (
r.settled ?
"" :
" UNSETTLED");
1216 auto is_lc_uid = [&](uint16_t uid) {
1220 auto apply_capture = [&](uint16_t uid,
double adc_avg,
double ref) {
1222 apply_lc_capture(uid, adc_avg, ref);
1224 apply_pt_capture(uid, adc_avg, ref);
1228 auto apply_pt_clear = [&](uint16_t uid) {
1241 write_abort_thresholds();
1243 auto apply_lc_clear = [&](uint16_t uid) {
1252 recompute_tare(uid);
1254 auto apply_clear = [&](uint16_t uid) {
1256 apply_lc_clear(uid);
1258 apply_pt_clear(uid);
1267 const std::filesystem::path live_path(
1268 "scripts/calibration/calibrations/cubic_calibration.json");
1269 const std::filesystem::path seed_path(
1270 "scripts/calibration/calibrations/cubic_calibration.default.json");
1272 if (!std::filesystem::exists(live_path, ec) && std::filesystem::exists(seed_path, ec)) {
1273 std::filesystem::copy_file(seed_path, live_path,
1274 std::filesystem::copy_options::overwrite_existing, ec);
1276 std::cout <<
"[Calibration] Cubic: seed copy failed (" << ec.message() <<
")"
1280 <<
"[Calibration] Cubic: seeded live store from cubic_calibration.default.json"
1294 auto reload_live_store = [&](
bool restore_learned) ->
size_t {
1295 const size_t loaded = cubic_store.
load();
1296 for (uint16_t uid : cubic_store.
uids()) {
1309 const bool fit_ok = fit !=
nullptr && fit->
valid;
1315 fit->
A, fit->
B, fit->
C, fit->
D,
"kg"));
1338 std::map<uint8_t, std::string> live_logical;
1339 for (uint16_t uid : cubic_store.
uids()) {
1341 if (c !=
nullptr && c->
live)
1343 (c->
role.empty() ? std::string(
"<no role>") : c->
role) +
" (uid " +
1344 std::to_string(uid) +
")";
1347 for (uint16_t uid : cubic_store.
uids()) {
1349 if (c ==
nullptr || c->
live)
1352 const auto hit = live_logical.find(c->
logical_ch);
1353 if (hit == live_logical.end())
1355 std::cout <<
"[Calibration] Cubic: uid " << uid <<
" ('"
1356 << (c->
role.empty() ?
"<no role>" : c->
role) <<
"', board "
1358 <<
" is not in the running config) sits on logical channel "
1359 <<
static_cast<int>(c->
logical_ch) <<
", which belongs to " << hit->second
1360 <<
" — its curve is NOT applied and does not touch that "
1365 std::cout <<
"[Calibration] Cubic: " << orphans
1366 <<
" entr(ies) kept for boards not in the running config (points "
1367 "preserved; they re-attach by role when the board returns)."
1371 if (restore_learned)
1376 for (uint16_t uid : cubic_store.
uids()) {
1380 if (cch !=
nullptr && !cch->
points.empty())
1385 write_abort_thresholds();
1389 recompute_all_tares();
1393 std::cout <<
"[Calibration] Robust adjustments path: " << adjustments_path << std::endl;
1394 std::cout <<
"[Calibration] (override with --adjustments, CAL_BACKUP_PATH, or "
1395 "calibration_backups/calibration_backup_*.json mtime)"
1404 const size_t tares_loaded = lc_tare_store.
load();
1405 if (tares_loaded > 0)
1406 std::cout <<
"[Calibration] LC tare: resumed " << tares_loaded
1407 <<
" standing tare(s) from lc_tare.json" << std::endl;
1410 const size_t cubic_loaded = reload_live_store(
true);
1411 if (cubic_loaded > 0)
1412 std::cout <<
"[Calibration] Cubic: resumed " << cubic_loaded
1413 <<
" channel(s) from cubic_calibration.json" << std::endl;
1422 return lc_eval_for(u);
1425 std::cout <<
"[Calibration] LC tare: WARNING — " << stale
1426 <<
" tare(s) were stale against the live curves and have been re-derived. "
1427 "A curve changed without recomputing its tare; check that lc_tare_store"
1428 ".load() still runs BEFORE the first reload_live_store()."
1430 lc_tare_store.
save();
1439 ?
"75% robust + 25% factory (CAL_USE_ROBUST_BLEND)"
1440 :
"100% robust when initialized (CAL_USE_ROBUST_PT)";
1441 std::cout <<
"[Calibration] LP PT: env override — all sensors " << label << std::endl;
1443 size_t robust_n = 0, blend_n = 0, physics_n = 0;
1453 <<
"[Calibration] PT: per-sensor model from config — default cubic (0-5 V) / physics "
1455 << robust_n <<
" robust, " << physics_n <<
" physics, " << blend_n <<
" blend"
1458 std::cout <<
"[Calibration] uid " << uid <<
" -> " <<
pt_model_name(m) << std::endl;
1461 size_t lc_linear_n = 0, lc_cubic_n = 0, lc_physics_n = 0;
1468 std::cout <<
"[Calibration] LC: per-sensor model from config — default linear (physics "
1470 << lc_linear_n <<
" linear, " << lc_cubic_n <<
" cubic, " << lc_physics_n
1471 <<
" physics" << std::endl;
1475 std::cout <<
"[Cal] CAL_VERBOSE=1 — debug output enabled" << std::endl;
1483 auto register_calibrated = [&]() {
1485 elodin_client, pt_boards, tc_boards, rtd_boards, lc_boards, enc_boards, act_boards);
1486 if (!bv_boards.empty())
1489 std::map<uint8_t, std::vector<fsw::elodin::DatabaseConfig::T7Table>> t7_by_slot;
1490 for (
const auto& t : t7_map.
all())
1491 t7_by_slot[t.slot].push_back(
1493 for (
const auto& [slot,
tables] : t7_by_slot)
1500 auto raw_tables = [&]() {
1502 lc_boards, enc_boards);
1504 t.insert(t.end(), bv.begin(), bv.end());
1506 t.insert(t.end(),
t7.begin(),
t7.end());
1514 auto subscribe_all = [&]() ->
bool {
1515 sub_tracker.
reset();
1516 auto tables = raw_tables();
1519 for (
const auto& t :
tables)
1524 auto connect_and_register = [&]() ->
bool {
1525 if (!elodin_client.
connect(elodin_host, elodin_port)) {
1526 std::cerr <<
"[Cal] Failed to connect to Elodin at " << elodin_host <<
":"
1527 << elodin_port << std::endl;
1530 register_calibrated();
1534 if (!subscribe_all()) {
1535 std::cerr <<
"[Cal] Failed to subscribe to raw sensor tables / CalibrationCommand"
1539 std::cout <<
"[Cal] Connected to Elodin, registered calibrated VTables, subscribed."
1544 if (!connect_and_register())
1557 int packet_count = 0;
1558 static std::atomic<bool> logged_ch5{
false};
1559 auto last_save = std::chrono::steady_clock::now();
1560 std::map<uint16_t, int32_t> last_adc_map;
1566 std::cerr <<
"[Cal] Elodin disconnected, retrying in 2s..." << std::endl;
1567 std::this_thread::sleep_for(std::chrono::seconds(2));
1569 register_calibrated();
1574 capture_window.
clear();
1575 last_adc_map.clear();
1576 std::cout <<
"[Cal] Reconnected to Elodin" << std::endl;
1592 const auto now_s = std::chrono::steady_clock::now();
1593 for (
const auto& t : sub_tracker.
due(now_s))
1603 const auto t = sub_tracker.
rejected(
pkt_buf[7], std::chrono::steady_clock::now());
1604 if (t.first != 0 || t.second != 0)
1605 std::cout <<
"[Cal] DB refused the subscription to [0x" << std::hex
1606 <<
static_cast<int>(t.first) <<
",0x" <<
static_cast<int>(t.second)
1607 << std::dec <<
"] (not registered yet) — asking again" << std::endl;
1611 const uint8_t type_hi =
pkt_buf[5];
1612 const uint8_t type_lo =
pkt_buf[6];
1613 const uint8_t ty =
pkt_buf[4];
1616 if (ty != 0 && ty != 1)
1621 static int debug_limit = 0;
1622 if (debug_limit < 10 && type_hi < 0x80) {
1623 std::cout <<
"[Cal] Received packet ty=" << (int)ty <<
" id=[0x" << std::hex
1624 << (
int)type_hi <<
",0x" << (int)type_lo << std::dec <<
"]"
1625 <<
" pkt_len=" << pkt_len << std::endl;
1630 if (type_hi == 0x46) {
1634 if (pkt_len >= 8 + 16) {
1636 if (command_dedupe.
seen(
p))
1638 uint8_t cmd_type =
p[8];
1639 uint16_t sensor_id =
1640 static_cast<uint16_t
>(
p[10]) | (
static_cast<uint16_t
>(
p[11]) << 8);
1645 std::memcpy(&ref_val,
p + 12,
sizeof(ref_val));
1647 std::cout <<
"[Cal] Received CalibrationCommand: type=" << (int)cmd_type
1648 <<
" sensor=" <<
static_cast<int>(sensor_id) <<
" ref=" << ref_val
1651 if (cmd_type == 0) {
1658 if (sensor_id == 0) {
1662 std::vector<uint16_t> skipped;
1663 for (uint16_t
id : capture_window.
uids()) {
1672 skipped.push_back(
id);
1675 apply_capture(
id,
r.adc_avg, 0.0);
1678 std::cout <<
"[Cal] Zero All: captured 0 on " << zeroed <<
" PT/LC sensors";
1679 if (!skipped.empty()) {
1680 std::cout <<
"; skipped " << skipped.size() <<
" with no fresh data (";
1681 for (
size_t i = 0; i < skipped.size(); i++)
1682 std::cout << (i ?
" " :
"") <<
static_cast<int>(skipped[i]);
1685 std::cout << std::endl;
1689 apply_capture(sensor_id,
r.adc_avg, 0.0);
1692 std::cout <<
"[Cal] Zero: captured 0 on uid " <<
static_cast<int>(sensor_id)
1693 <<
" (" << zeroed <<
")" << std::endl;
1695 }
else if (cmd_type == 1) {
1699 take_capture(sensor_id,
"Capture ref");
1702 static_cast<int32_t
>(
r.adc_avg), ref_val);
1704 }
else if (cmd_type == 2) {
1706 std::cout <<
"[Cal] Adjustments saved to " << adjustments_path << std::endl;
1707 }
else if (cmd_type == 3) {
1709 take_capture(sensor_id,
"Cubic capture");
1710 const bool have_adc = cr.
ok;
1711 const double adc_avg = cr.
adc_avg;
1714 apply_capture(sensor_id, adc_avg, ref_val);
1715 std::cout <<
"[Cal] Cubic capture uid=" <<
static_cast<int>(sensor_id)
1716 <<
" adc=" << adc_avg <<
" psi=" << ref_val << std::endl;
1718 std::cout <<
"[Cal] Cubic capture: no ADC seen yet for uid "
1719 <<
static_cast<int>(sensor_id) << std::endl;
1721 }
else if (cmd_type == 4) {
1722 apply_clear(sensor_id);
1723 std::cout <<
"[Cal] Cubic cleared uid=" <<
static_cast<int>(sensor_id)
1725 }
else if (cmd_type == 5) {
1728 apply_capture(sensor_id, cr.
adc_avg, ref_val);
1735 std::cout <<
"[Cal] Capture uid=" <<
static_cast<int>(sensor_id) <<
" ("
1737 << capture_detail(cr) <<
" ref=" << ref_val << std::endl;
1739 std::cout <<
"[Cal] Capture: no ADC seen yet for uid "
1740 <<
static_cast<int>(sensor_id) << std::endl;
1742 }
else if (cmd_type == 6) {
1743 apply_clear(sensor_id);
1744 std::cout <<
"[Cal] New calibration uid=" <<
static_cast<int>(sensor_id) <<
" ("
1746 }
else if (cmd_type == 7) {
1753 const size_t n = reload_live_store(
false);
1754 std::cout <<
"[Cal] Reloaded live calibration store (" <<
n
1755 <<
" channel(s)) after profile swap" << std::endl;
1756 }
else if (cmd_type == 8) {
1765 const bool clearing = ref_val >= 0.5f;
1766 std::vector<uint16_t> targets;
1767 if (sensor_id == 0) {
1769 targets = lc_tare_store.
uids();
1773 for (uint16_t
id : capture_window.
uids())
1775 targets.push_back(
id);
1777 }
else if (is_lc_uid(sensor_id)) {
1778 targets.push_back(sensor_id);
1780 std::cout <<
"[Cal] Tare: uid " <<
static_cast<int>(sensor_id)
1781 <<
" is not a load cell — ignored" << std::endl;
1785 for (uint16_t
id : targets) {
1787 lc_tare_store.
clear(
id);
1797 const uint8_t
board_id =
static_cast<uint8_t
>(
id / 100);
1798 const uint8_t connector =
static_cast<uint8_t
>(
id % 100);
1802 const std::string entity =
1804 t7->slot,
static_cast<uint8_t
>(
t7->channel.ain))
1806 if (lc_tare_store.
set(
id, entity,
r.adc_avg, lc_eval_for(
id))) {
1809 std::cout <<
"[Cal] Tare uid=" <<
static_cast<int>(id) <<
" "
1810 << capture_detail(
r)
1811 <<
" offset=" << (t !=
nullptr ? t->offset_kg : 0.0) <<
"kg"
1815 lc_tare_store.
save();
1816 std::cout <<
"[Cal] " << (clearing ?
"Tare clear" :
"Tare") <<
": " << done
1817 <<
" load cell(s)" << std::endl;
1827 const uint8_t offset = type_lo & 0x1F;
1828 if (offset >= 0x10 || pkt_len - 8 < 21)
1832 float volts_f = 0.0f;
1833 std::memcpy(&ts_ns,
p,
sizeof(ts_ns));
1834 std::memcpy(&volts_f,
p + 12,
sizeof(volts_f));
1835 const uint64_t rx_ns =
mono_ns();
1836 const uint8_t slot =
static_cast<uint8_t
>((type_lo >> 5) + 1);
1837 if (!std::isfinite(volts_f))
1846 const double volts = volts_f;
1848 const uint16_t uid = t->uid;
1849 const uint8_t ain =
static_cast<uint8_t
>(t->channel.ain);
1853 ts_ns, ain, std::array<uint8_t, 3>{0, 0, 0},
static_cast<float>(
value), code,
1855 elodin_client.
publish(out_id, msg);
1859 if (t->channel.kind ==
"PT") {
1860 last_adc_map[uid] = code;
1861 capture_window.
push(uid, code, ts_ns, rx_ns);
1862 const bool fac_ok = pt_calibration.
is_calibrated(t->logical_ch);
1863 const double psi_fac =
1867 if (!std::isfinite(psi_phys))
1869 const bool has_rob = robust_manager.
has_sensor(uid);
1870 double psi =
select_pt_psi(uid, psi_fac, psi_rob, psi_phys, fac_ok, has_rob);
1872 uint8_t cal_status = 1;
1874 cal_status = fac_ok ? 1u : 0u;
1876 cal_status = has_rob ? 1u : 0u;
1878 cal_status = (fac_ok && has_rob) ? 1u : 0u;
1880 psi = std::isfinite(psi) ? std::clamp(psi, -3000.0, 20000.0) : 0.0;
1882 }
else if (t->channel.kind ==
"LC") {
1883 last_adc_map[uid] = code;
1884 capture_window.
push(uid, code, ts_ns, rx_ns);
1885 const bool cubic_ok = lc_calibration.
is_calibrated(t->logical_ch);
1886 const double kg_cubic =
1887 cubic_ok ? lc_calibration.
calculate(t->logical_ch, code) : 0.0;
1889 if (!std::isfinite(kg_phys))
1891 double kg =
select_lc_kg(uid, kg_cubic, kg_phys, cubic_ok);
1892 const uint8_t cal_status =
1894 publish(std::isfinite(kg) ? kg : 0.0, cal_status);
1895 }
else if (t->channel.kind ==
"TC") {
1899 : std::numeric_limits<double>::quiet_NaN();
1900 if (std::isfinite(temp_c))
1902 }
else if (t->channel.kind ==
"RTD") {
1905 if (std::isfinite(temp_c))
1913 if ((type_hi < 0x20 || type_hi > 0x24) && type_hi != 0x30 &&
1919 uint8_t block_offset = type_lo & 0x1F;
1920 if (type_hi != 0x30) {
1921 if (block_offset >= 0x10)
1923 if (block_offset == 0 || block_offset > 10)
1929 if (block_offset == 0 || block_offset > 0x0F)
1933 const ssize_t payload_len = pkt_len - 8;
1934 if (payload_len < 21) {
1935 if (debug_limit < 20) {
1936 std::cout <<
"[Cal] Dropped small payload: " << payload_len << std::endl;
1944 const uint64_t ts_ns = *
reinterpret_cast<const uint64_t*
>(
p);
1947 const uint64_t rx_ns =
mono_ns();
1948 const uint8_t ch_payload =
p[8];
1949 const uint8_t ch_eff = block_offset;
1950 if (ch_payload != ch_eff && type_hi == 0x20) {
1951 static int ch_mismatch_warn = 0;
1952 if (ch_mismatch_warn < 8) {
1953 std::cerr <<
"[Cal] WARN: PT payload ch=" <<
static_cast<int>(ch_payload)
1954 <<
" != packet-id ch=" <<
static_cast<int>(ch_eff) <<
" [0x" << std::hex
1955 <<
static_cast<int>(type_hi) <<
"," <<
static_cast<int>(type_lo)
1956 << std::dec <<
"]" << std::endl;
1960 const int32_t adc_i32 = *
reinterpret_cast<const int32_t*
>(
p + 12);
1961 const uint32_t adc_u32 = *
reinterpret_cast<const uint32_t*
>(
p + 12);
1967 uint8_t cal_lo =
static_cast<uint8_t
>(type_lo + 0x10);
1968 uint8_t board_number =
static_cast<uint8_t
>((type_lo >> 5) + 1);
1972 if (type_hi == 0x20) {
1974 const bool is_loop = loop_board !=
nullptr;
1981 const int32_t cal_adc = is_loop ?
static_cast<int32_t
>(hp_wire_adc) : adc_i32;
1983 last_adc_map[uid] = cal_adc;
1984 capture_window.
push(uid, cal_adc, ts_ns, rx_ns);
1986 const uint8_t pt_log_ch =
1988 const bool fac_ok = pt_calibration.
is_calibrated(pt_log_ch);
1989 const double psi_fac =
1992 const double psi_phys =
1998 const bool has_rob = robust_manager.
has_sensor(uid);
2000 double psi =
select_pt_psi(uid, psi_fac, psi_rob, psi_phys, fac_ok, has_rob);
2005 uint8_t cal_status = 1;
2007 cal_status = fac_ok ? 1u : 0u;
2009 cal_status = has_rob ? 1u : 0u;
2011 cal_status = (fac_ok && has_rob) ? 1u : 0u;
2016 if (
verbose() && packet_count % 100 == 0)
2017 std::cout <<
"[Cal] PT B" <<
static_cast<int>(board_number) <<
" ch"
2018 <<
static_cast<int>(ch_eff) << (is_loop ?
" (4-20mA)" :
"")
2019 <<
" adc=" << cal_adc <<
" psi=" << psi << std::endl;
2021 if (!std::isfinite(psi))
2026 psi = std::clamp(psi, -3000.0, 20000.0);
2029 ts_ns, ch_eff, std::array<uint8_t, 3>{0, 0, 0},
static_cast<float>(psi),
2030 is_loop ? adc_u32 :
static_cast<uint32_t
>(adc_i32), cal_status);
2031 elodin_client.
publish(
static_cast<uint16_t
>((type_hi << 8) | cal_lo), cal_msg);
2033 }
else if (type_hi == 0x21) {
2037 temp_c = tc_calibration.
calculate(ch_eff, adc_i32);
2045 ts_ns, ch_eff, std::array<uint8_t, 3>{0, 0, 0},
static_cast<float>(temp_c),
2046 static_cast<uint32_t
>(adc_i32), cal_status);
2047 elodin_client.
publish(
static_cast<uint16_t
>((type_hi << 8) | cal_lo), cal_msg);
2049 }
else if (type_hi == 0x22) {
2053 temp_c = rtd_calibration.
calculate(ch_eff, adc_i32);
2063 if (std::isfinite(temp_c)) {
2066 ts_ns, ch_eff, std::array<uint8_t, 3>{0, 0, 0},
static_cast<float>(temp_c),
2067 static_cast<uint32_t
>(adc_i32), cal_status);
2068 elodin_client.
publish(
static_cast<uint16_t
>((type_hi << 8) | cal_lo), cal_msg);
2071 }
else if (type_hi == 0x23) {
2075 last_adc_map[uid] = adc_i32;
2076 capture_window.
push(uid, adc_i32, ts_ns, rx_ns);
2078 const uint8_t lc_log_ch =
2080 const bool cubic_ok = lc_calibration.
is_calibrated(lc_log_ch);
2081 const double kg_cubic = cubic_ok ? lc_calibration.
calculate(lc_log_ch, adc_i32) : 0.0;
2086 double force_kg =
select_lc_kg(uid, kg_cubic, kg_phys, cubic_ok);
2087 const uint8_t cal_status =
2089 if (!std::isfinite(force_kg))
2093 ts_ns, ch_eff, std::array<uint8_t, 3>{0, 0, 0},
static_cast<float>(force_kg),
2094 static_cast<uint32_t
>(adc_i32), cal_status);
2095 elodin_client.
publish(
static_cast<uint16_t
>((type_hi << 8) | cal_lo), cal_msg);
2097 }
else if (type_hi == 0x30) {
2099 uint8_t cal_status = 1;
2102 ts_ns, ch_eff, std::array<uint8_t, 3>{0, 0, 0},
static_cast<float>(current_a),
2103 adc_u32, cal_status);
2104 elodin_client.
publish(
static_cast<uint16_t
>((0x31 << 8) | cal_lo), cal_msg);
2108 ts_ns, ch_eff, std::array<uint8_t, 3>{0, 0, 0}, pos.percent_open, adc_u32,
2117 if (packet_count % 10000 == 0)
2118 std::cout <<
"[Cal] Processed " << packet_count <<
" raw packets (type=0x" << std::hex
2119 <<
static_cast<int>(type_hi) <<
" ch=" <<
static_cast<int>(ch_eff) << std::dec
2120 <<
")" << std::endl;
2123 auto now = std::chrono::steady_clock::now();
2124 if (std::chrono::duration_cast<std::chrono::seconds>(now - last_save).
count() > 300) {
2133 std::cout <<
"[Cal] Saved robust adjustments on shutdown → " << adjustments_path
2137 std::cout <<
"[Cal] Stopped." << std::endl;
The ADC a calibration capture records: a bounded, recent, checked mean.
static uint16_t resolve_pt_sensor_uid(uint8_t type_lo, uint8_t ch, const std::vector< fsw::elodin::BoardChannels > &pt_boards)
Map Elodin PT raw packet low byte + connector ch → robust calibration uid (board_id*100+ch).
static PtModel pt_model_for(uint16_t uid)
Per-sensor streaming model; sensors absent from the map default to factory cubic.
static double pt_sense_resistor_for(uint16_t uid)
Per-sensor 4-20 mA shunt resistance (Ω); default 120 when unset.
static double convert_lc_adc_to_force(int32_t adc_raw, double sensitivity_mv_per_v, double pga_gain, double full_scale_value)
Ratiometric load cell: raw ADC → force (kg).
static double select_pt_psi(uint16_t uid, double psi_fac, double psi_rob, double psi_phys, bool fac_ok, bool has_robust)
The single PT source-selection rule, shared by the streaming path and the Zero-All helper so both agr...
constexpr double kRatiometricPtZeroFrac
Ratiometric PT, 0.5-4.5 V at 5 V excitation: excitation is the ADC reference, so the output is 10 % o...
static LcModel parse_lc_model(const std::string &s)
Parse a config model string for an LC role: "linear" and "cubic" map to their fits,...
static double lc_full_scale_for(uint16_t uid, double fallback)
Per-sensor LC physics full-scale kg; falls back to the [calibration.lc] global when unset.
static double lc_sensitivity_for(uint16_t uid, double fallback)
Per-sensor LC sensitivity mV/V; falls back to the [calibration.lc] global when unset.
static double convert_ratiometric_pt_to_pressure(int32_t adc_raw, double full_scale_psi)
static double convert_hp_pt_to_pressure(uint8_t local_ch, uint32_t adc_raw, const fsw::config::PtBoardConfig &board, double full_scale_psi, double sense_resistor_ohms, std::array< uint8_t, 11 > &live_state)
4-20 mA HP PT → PSI.
static double pt_full_scale_for(uint16_t uid)
Per-sensor physics full-scale PSI; default 1000 (0-5 V -> 0-1000 PSI) when unset.
static LcModel lc_model_for(uint16_t uid)
Per-sensor LC streaming model; a uid absent from g_lc_model defaults to Linear, which streams physics...
static const char * lc_model_name(LcModel m)
Config-string form of an LC model, for the cubic store's per-uid active_model tag.
static uint64_t mono_ns()
constexpr double kRatiometricPtSpanFrac
static bool lc_streams_physics(uint16_t uid, bool fit_ok)
True when this LC's published kilograms are its datasheet physics conversion: physics itself,...
static uint16_t resolve_lc_sensor_uid(uint8_t type_lo, uint8_t ch, const std::vector< fsw::elodin::BoardChannels > &lc_boards)
Map Elodin LC raw packet low byte + connector ch -> LC uid (board_id*100+ch).
static double unix_now()
Monotonic now, in ns.
static double select_lc_kg(uint16_t uid, double kg_fit, double kg_phys, bool fit_ok)
The LC source-selection rule, mirroring select_pt_psi minus robust/blend (LC doesn't learn drift).
static double lc_pga_gain_for(uint16_t uid, double fallback)
Per-sensor LC PGA gain; falls back to the [calibration.lc] global when unset.
static double clamp_tc_rtd_temp_publish_c(double t_c)
TC/RTD calibrated temperature before Elodin publish.
static double convert_rtd_adc_to_temp_c(int32_t adc_raw, double adc_ref_voltage, double excitation_ua, double r0_ohm)
Pt1000 RTD: raw ADC → voltage → resistance → temperature (°C), on the full IEC 60751 curve (rtd::rtd_...
static std::optional< int32_t > invert_abort_psi_to_adc(uint16_t uid, uint8_t pt_log_ch, bool is_loop, const fsw::config::PtBoardConfig *loop_board, double target_psi, bool force_physics, const fsw::calibration::PTCalibrationManager &pt_calibration, fsw::calibration::RobustCalibrationManager &robust_manager)
Invert an abort-procedure pressure (the level the boards vent DOWN below before completing an autonom...
static bool env_flag_true(const char *name)
static PtModel parse_pt_model(const std::string &s)
Parse a config model string to the enum; "cubic" and anything unrecognized map to Cubic.
static double convert_tc_adc_to_temp_c(int32_t adc_raw, double adc_ref_voltage)
K-type thermocouple: raw ADC → voltage → temperature (°C).
static const char * pt_model_name(PtModel m)
Config-string form of a model, for the cubic store's per-uid active_model tag.
constexpr int kMinAbortCalPoints
A sensor needs at least this many captured points before its cubic/robust curve is trusted to invert ...
static void signalHandler(int)
static double convert_act_adc_to_current(uint32_t adc_raw)
Actuator current-sense: 12-bit ADC → current (amps).
static std::atomic< bool > running
Per-sensor ring of recent samples, and the capture decision over it.
CaptureResult capture(uint16_t uid, uint64_t now_rx_ns) const
What this sensor reads now, or why that cannot be answered.
void clear()
Forget everything.
void push(uint16_t uid, int32_t adc, uint64_t ts_ns, uint64_t rx_ns)
Record one sample.
std::vector< uint16_t > uids() const
Every uid with at least one sample.
Drops a CalibrationCommand the service has already handled.
bool seen(const uint8_t *cmd)
true if this exact command was already seen (drop it); otherwise records it.
Owns the per-channel captured points and the fitted cubic for the calibration service.
void note_capture(uint16_t uid, const CaptureQuality &q)
Record how the last capture on this uid went, for the UI to show.
size_t load()
Load the JSON record, resume points, and re-fit each channel.
void clear_channel(uint16_t uid)
Drop all points, fit, and fit-curve for a uid (channel goes back to PENDING).
const CubicFit * fit_for(uint16_t uid) const
void register_channel(uint16_t uid, uint8_t board_id, uint8_t connector, uint8_t logical_ch, const std::string &role, const std::string &active_model="cubic", SensorKind kind=SensorKind::PT, const std::string &raw_unit="adc")
Attach/refresh identity + role + streaming model for a uid (idempotent; call at startup).
bool load_failed() const
True when load() could not read an existing file, so every curve here is a fallback rather than the o...
std::vector< uint16_t > uids() const
CubicFit add_point(uint16_t uid, double adc, double psi)
Append a capture point (raw ADC, reference PSI), cap history, and re-fit.
const CubicChannel * channel(uint16_t uid) const
void set_fit_curve(uint16_t uid, const std::vector< std::pair< double, double > > &curve)
Replace a robust uid's display fit-curve (sampled predict_pressure_psi points).
bool save() const
Atomically write the JSON record (temp + rename).
Owns the per-channel tares for the calibration service.
bool set(uint16_t uid, const std::string &entity, double adc_at_tare, const Evaluator &eval)
Record a tare for uid at adc_at_tare and derive its offset.
size_t load()
Load the JSON record.
const LcTare * tare_for(uint16_t uid) const
void recompute(uint16_t uid, const Evaluator &eval)
Re-derive offset_kg for one uid under the current curve.
void set_curves_trusted(bool trusted)
False while the curves cannot be trusted — set when the cubic store failed to load.
std::vector< uint16_t > uids() const
size_t recompute_stale(const std::function< Evaluator(uint16_t)> &eval_for)
Re-derive only the tares whose recorded fingerprint disagrees with the curve now live,...
bool save() const
Atomically write the JSON record (temp + rename).
void recompute_all(const std::function< Evaluator(uint16_t)> &eval_for)
Re-derive every tare; eval_for supplies each uid's evaluator.
std::function< double(double adc)> Evaluator
adc -> kg through whichever model this uid streams (select_lc_kg: cubic fit or the datasheet physics ...
void set_calibration(uint8_t channel_id, const PTCalibrationCoeffs &coeffs)
Set/replace the cubic coefficients for a logical channel at runtime.
bool is_calibrated(uint8_t channel_id) const
Check if a channel has calibration.
double calculate_pressure(uint8_t channel_id, int32_t adc_code) const
Calculate calibrated pressure for a channel.
void clear_calibration(uint8_t channel_id)
Remove any cubic for a logical channel, so is_calibrated() returns false again.
static void set_default_paths(const std::string &json_dir, const std::string &csv_path)
Set default calibration file paths.
double step(uint16_t uid, double value)
Feed one reading; returns the smoothed value.
Runtime PT calibration via RobustCalibrationFramework (9-D basis, TLS / Bayesian / RLS / GLR / bias),...
void update_calibration(uint16_t sensor_id, int32_t adc_code, double reference_pressure)
bool load_adjustments(const std::string &path, const std::map< uint16_t, std::string > *uid_to_role=nullptr)
Restore per-sensor θ.
bool save_adjustments(const std::string &path, const std::map< uint16_t, std::string > *uid_to_role=nullptr) const
Persist per-sensor θ.
void reseed_sensor(uint16_t sensor_id, const PTCalibrationCoeffs &baseline)
Reseed a sensor's framework cleanly from the given cubic baseline, discarding any learned adjustments...
void reset_adjustment(uint16_t sensor_id)
double predict_pressure_psi(uint16_t sensor_id, int32_t adc_code)
Mean PSI from the robust model (default environment).
bool has_sensor(uint16_t sensor_id) const
True if this sensor_id was initialized for robust tracking (factory-baselined).
double calculate(uint8_t channel_id, int32_t raw_counts) const
bool load_calibration(const std::string &json_dir, const std::string &csv_path)
Auto-discover & load from default directories.
void set_calibration(uint8_t channel_id, const PolynomialCalibration &cal)
Manually set coefficients for a channel.
size_t calibrated_count() const
bool is_calibrated(uint8_t channel_id) const
void clear_calibration(uint8_t channel_id)
Remove a channel's calibration so is_calibrated() returns false again.
static bool register_ball_valve_calibrated_tables(ElodinClient &client, const std::vector< BoardChannels > &bv_boards)
Register ball valve CALIBRATED position VTables [0x34] (calibration_service).
static bool register_calibrated_tables(ElodinClient &client, const std::vector< BoardChannels > &pt_boards, const std::vector< BoardChannels > &tc_boards, const std::vector< BoardChannels > &rtd_boards, const std::vector< BoardChannels > &lc_boards, const std::vector< BoardChannels > &enc_boards, const std::vector< BoardChannels > &act_boards)
Register CALIBRATED VTables.
static bool register_t7_calibrated_tables(ElodinClient &client, uint8_t slot, const std::vector< T7Table > &tables)
Standalone Elodin database client.
bool flush_batch()
Flush (send) the accumulated batch in a single TCP write.
bool is_connected() const
Check if connected.
void set_recv_timeout_ms(int timeout_ms)
Set socket receive timeout so read_packet() can yield.
bool publish(uint16_t message_id, const MessageType &message)
Publish a message to a table.
void begin_batch()
Begin batching — subsequent publish() calls go into an internal buffer.
bool connect(const std::string &host, uint16_t port=2240)
Connect to Elodin database.
ssize_t read_packet(uint8_t *packet_buffer, size_t max_len)
Read complete packet from Elodin (header + payload).
bool subscribe_table(uint8_t hi, uint8_t lo, uint8_t request_id)
One table, under request_id, which the DB echoes if it refuses it (ErrorResponse [0xE0,...
bool reconnect()
Attempt to reconnect using the last-used host/port.
void reset()
A new connection: every subscription is sent afresh.
Table rejected(uint8_t request_id, Clock::time_point now)
The DB refused the subscription sent under request_id.
std::vector< Table > due(Clock::time_point now)
Refused tables whose wait is over; each is forgotten here until rejected again.
uint8_t assign(Table table)
The request id to subscribe table under.
Every PT / LC / TC input on every enabled T7, from config.
const T7CalChannel * by_uid(uint16_t uid) const
const std::vector< T7CalChannel > & all() const
std::vector< std::pair< uint8_t, uint8_t > > raw_tables() const
Raw tables calibration_service subscribes to: every input above, plus each TC slot's CJC.
const T7CalChannel * by_table(uint8_t slot, uint8_t ain) const
The input behind raw table {0x26, (slot-1)*0x20 + ain}, or nullptr.
static uint8_t pkt_buf[64]
static STAR_MCP3201 adc(MCP3201_CS_PIN, &mcp_spi)
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
*brief create JSON pointer *sa reference_tokens const string_t & a
size_type count(const key_type &key) const
PtModel
Which model a PT streams: operator/factory cubic, the robust learner, the 75/25 blend,...
std::unordered_map< uint16_t, double > g_lc_pga_gain
std::unordered_map< uint16_t, double > g_pt_full_scale
Per-sensor physics-mode parameters (from [calibration_full_scale_*] / [calibration_sense_resistor_*],...
std::unordered_map< uint16_t, double > g_lc_full_scale
Per-sensor LC physics-mode parameters (from [calibration_full_scale_*] / [calibration_sensitivity_*] ...
std::unordered_map< uint16_t, double > g_pt_sense_resistor
std::unordered_map< uint16_t, LcModel > g_lc_model
uid -> LC streaming model.
std::set< uint8_t > g_lc_board_ids
Board ids of every LC board, so a connector outside active_connectors still routes as LC.
fsw::calibration::PtSmoother g_pt_smoother
Per-sensor PT output smoothing (ESP32 and T7), seeded with each sensor's first reading.
std::set< uint16_t > g_lc_uids
Every LC uid, populated at startup — the discriminant apply_capture/apply_clear/ reload_live_store us...
std::optional< PtModel > g_env_override
Set once at startup: when a CAL_USE_* PT env var is present it forces every sensor,...
LcModel
Which model an LC streams: a straight-line fit through the operator's captured points,...
std::map< uint8_t, std::array< uint8_t, 11 > > g_hp_ma_live
Hysteresis: 0 = treat as below loop, 1 = in 4–20 mA band (stops 3.99↔4.01 PSI chatter).
std::unordered_map< uint16_t, double > g_lc_sensitivity
std::unordered_map< uint16_t, std::string > g_uid_role
uid -> role name (from [sensor_roles_*]).
std::map< uint8_t, double > g_t7_cold_junction_c
Latest thermocouple cold-junction temperature (°C) per T7 slot, from t7_bridge's CJC table {0x26,...
std::unordered_map< uint16_t, PtModel > g_pt_model
uid (board_id*100 + connector) -> streaming model.
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, uint32_t, uint8_t > CalibratedRTDMessage
Calibrated RTD message for Elodin.
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, uint32_t, uint8_t > CalibratedLCMessage
Calibrated Load Cell message for Elodin.
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, uint32_t, uint8_t > CalibratedTCMessage
Calibrated Thermocouple message for Elodin.
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, uint32_t, uint8_t > CalibratedACTMessage
Calibrated Actuator current-sense message for Elodin.
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, uint32_t, uint8_t > CalibratedPTMessage
Calibrated Pressure Transducer message for Elodin.
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, int32_t, uint8_t > T7CalibratedMessage
One T7 channel in engineering units, published by t7_bridge beside T7RawMessage.
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, uint32_t, uint8_t > CalibratedValvePositionMessage
Calibrated ball valve position message for Elodin.
double rtd_board_adc_to_temp_c(int32_t adc_raw, double adc_ref_voltage, double excitation_ua, double r0_ohms)
An ESP32 RTD board's reading -> °C: the ADC code (2^31 = adc_ref_voltage) is the voltage across the e...
Reading from_counts(uint32_t counts)
CalTarget cal_target(const CubicChannel &ch)
CalTarget
Which live calibration manager a store entry's fit belongs in — the one rule every caller that applie...
uint32_t hp_pt_shunt_code(uint32_t wire)
A 4-20 mA PT's wire code -> the shunt code the conversions and calibrations work in.
constexpr const char * kRawUnitMicrovolts
std::string lc_tare_entity(uint8_t board_id, uint8_t connector)
The publish-path entity name for an LC channel: "LC<slot>_Cal.CH<connector>", where slot is board_id ...
uint8_t pt_logical_calibration_channel(uint8_t board_slot_number, uint8_t local_connector)
PT board slot (1–10, from board_id % 10 with 0→10) + local connector (1–10) → factory/JSON logical ch...
double type_k_c_from_mv(double mv)
Temperature (C) for a type K EMF in mV, referenced to 0 C.
std::map< ActiveBoardKind, std::vector< elodin::BoardChannels > > load_active_boards(const std::string &config_path)
Parse [boards.
std::map< uint8_t, PtBoardConfig > load_pt_boards(const std::string &config_path)
Parse the PT boards' sensor interface and 4-20 mA conversion parameters, keyed by Elodin slot.
bool is_current_loop(const PtBoardConfig &b)
True when this board's channels convert through the 4-20 mA current-loop path.
ActiveBoardKind
Board kinds from [boards.
Config load(const std::string &path)
Parse config.toml.
constexpr std::pair< uint8_t, uint8_t > kTableCalibrationCommand
constexpr uint8_t kBallValveCalHi
bool is_error_response(const uint8_t *pkt, size_t len)
ErrorResponse: header len(4) | ty(1) | msg id [0xE0, 0x1D] | request id(1).
std::vector< std::pair< uint8_t, uint8_t > > raw_sensor_tables(const std::vector< BoardChannels > &pt_boards, const std::vector< BoardChannels > &act_boards, const std::vector< BoardChannels > &tc_boards, const std::vector< BoardChannels > &rtd_boards, const std::vector< BoardChannels > &lc_boards, const std::vector< BoardChannels > &enc_boards)
Elodin table ids (hi, lo) for the RAW sensor VTables the given boards publish.
constexpr uint8_t kBallValveRawHi
Ball valve boards (BALL_VALVE) have their own type_hi bytes, so a valve never shares a key with an AC...
std::vector< std::pair< uint8_t, uint8_t > > ball_valve_raw_tables(const std::vector< BoardChannels > &bv_boards)
constexpr uint8_t kCjcTableOffset
Raw-table offset (within a slot's 32-entry block) of the cold-junction table: AIN15 does not exist on...
double lc_kg(double volts, const config::T7Channel &ch)
LC: a bridge's output is sensitivity x excitation at rated load.
constexpr uint8_t kTypeHi
Elodin table-id high byte for T7 analog inputs (0x20-0x25 are the ESP32 sensor types).
int32_t t7_counts(double volts)
The calibration "ADC code" for a T7 reading: microvolts, rounded and clamped to int32.
std::array< uint8_t, 2 > calibrated_table_id(uint8_t slot, uint8_t ain)
Calibrated table for one AIN: table_id + 0x10, e.g.
const char * calibrated_field(const std::string &kind)
The Elodin field a calibrated channel of this kind publishes ("pressure_psi", ...),...
double t7_volts(double counts)
Volts from a calibration code.
double tc_c(double volts, double cold_junction_c)
Type K thermocouple: the measured EMF plus the EMF the cold junction subtracts.
std::string calibrated_entity_name(uint8_t slot, uint8_t ain)
Calibrated entity name, e.g.
double pt_psi(double volts, const config::T7Channel &ch)
PT: linear between the signal's zero and full-scale outputs.
double rtd_c(double volts, const config::T7Channel &ch)
RTD: ohms -> C on the IEC 60751 curve (full Callendar-Van Dusen, C term below 0 C).
*brief user defined string literal for JSON values *sa std::size_t n
static void publish(self_test_t *t, uint32_t now_ms)
Per-board channel info for board-namespaced entity registration.
How trustworthy the most recent capture on this channel was.
double drift_z
drift in standard errors
uint64_t window_ms
the window this sensor's rate called for
bool ok
false → the caller must NOT create a point
double drift
|mean(2nd half) - mean(1st half)|
double spread
max - min inside the window
size_t n
samples inside the window
bool settled
false → recorded, but flagged
Per-sensor calibration record: identity, captured points, and the current fit.
bool live
True when the RUNNING config declares a board/connector that owns this uid, i.e.
std::vector< CubicPoint > points
Result of a least-squares polynomial fit over a channel's captured points.
One load cell's tare: the scale reads zero at the load it was holding when the operator pressed Tare.
PT Calibration Coefficients.
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.
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.
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::vector< std::string > t7_errors
CalibrationConfig calibration
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.
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.
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.
Per-board 4-20 mA conversion parameters, keyed by Elodin slot (board_number).
Per-board channel info for board-namespaced entity registration.
One T7 input as calibration sees it.
std::string board(const std::string &name, const std::string &id, bool enabled=true)
static double volts(uint32_t code)
Shunt volts for a code, as calibration_main's convert_hp_pt_to_pressure computes it.
static SlugTables tables()
The tables every case validates against.
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")
static std::string t7(const std::string &channels, const std::string &extra="", const std::string &name="lj", int board_id=81)