31using json = nlohmann::json;
35#define CHECK(cond, ...) \
39 std::printf(" ❌ [%s:%d] ", __func__, __LINE__); \
40 std::printf(__VA_ARGS__); \
50 auto p = std::filesystem::temp_directory_path() /
51 (
"cubic_ns_" + std::string(name) +
"_" + std::to_string(++seq) +
".json");
52 std::filesystem::remove(
p);
57 std::ifstream f(
path);
64 const double A = arr[0], B = arr[1], C = arr[2], D = arr[3];
65 return A * x * x * x + B * x * x + C * x + D;
96 CHECK(j.contains(
"calibration_polynomials") && j[
"calibration_polynomials"].contains(
"11"),
97 "PT map should hold logical 11");
98 CHECK(j.contains(
"lc_calibration_polynomials") &&
99 j[
"lc_calibration_polynomials"].contains(
"11"),
100 "LC map should hold logical 11");
103 const double psi =
eval_abcd(j[
"calibration_polynomials"][
"11"], 579791093.0);
104 CHECK(std::fabs(psi - 501.0) < 5.0,
"PT logical 11 should give ~501 psi, got %.4g", psi);
107 const double kg =
eval_abcd(j[
"lc_calibration_polynomials"][
"11"], 1114800.0);
108 CHECK(std::fabs(kg - 11.34) < 0.1,
"LC logical 11 should give ~11.34 kg, got %.4g", kg);
122 const double psi =
eval_abcd(j[
"calibration_polynomials"][
"1"], 579791093.0);
123 const double kg =
eval_abcd(j[
"lc_calibration_polynomials"][
"1"], 1114800.0);
124 CHECK(std::fabs(psi - 501.0) < 5.0,
"PT logical 1 should give ~501 psi, got %.4g", psi);
125 CHECK(std::fabs(kg - 11.34) < 0.1,
"LC logical 1 should give ~11.34 kg, got %.4g", kg);
142 CHECK(s2.
load() == 2,
"both channels should load");
143 CHECK(s2.
save(),
"re-save should succeed");
146 CHECK(j[
"calibration_polynomials"].
contains(
"11"),
"PT map should survive a blind reload");
147 CHECK(j[
"lc_calibration_polynomials"].
contains(
"11"),
"LC map should survive a blind reload");
148 const double psi =
eval_abcd(j[
"calibration_polynomials"][
"11"], 579791093.0);
149 CHECK(std::fabs(psi - 501.0) < 5.0,
"PT curve should still be the PT's, got %.4g", psi);
157 j[
"cubic_state"] = json::object();
158 for (
const auto& [uid, role] : {std::pair<const char*, const char*>{
"2201",
"GN2 High"},
159 std::pair<const char*, const char*>{
"4201",
"LOX Scale"}}) {
161 cj[
"boardId"] = std::string(uid).substr(0, 2) ==
"22" ? 22 : 42;
163 cj[
"logicalCh"] = 11;
165 cj[
"active_model"] =
"cubic";
167 j[
"cubic_state"][uid] = cj;
169 std::ofstream f(
path);
177 CHECK(s.
load() == 2,
"legacy entries should load");
183 const double psi =
eval_abcd(j[
"calibration_polynomials"][
"11"], 579791093.0);
184 const double kg =
eval_abcd(j[
"lc_calibration_polynomials"][
"11"], 1114800.0);
185 CHECK(std::fabs(psi - 501.0) < 5.0,
"legacy PT should land in the PT map, got %.4g", psi);
186 CHECK(std::fabs(kg - 11.34) < 0.1,
"legacy LC should land in the LC map, got %.4g", kg);
195 const auto fit = s.
add_point(9999, 123456.0, 10.0);
196 CHECK(!fit.valid,
"an unregistered uid must not produce a fit");
197 CHECK(s.
channel(9999) ==
nullptr,
"an unregistered uid must not create a channel");
198 CHECK(s.
uids().size() == 1,
"uids() should still hold just the registered one, got %zu",
214 j[
"calibration_polynomials"][
"11"] = j[
"lc_calibration_polynomials"][
"11"];
215 j.erase(
"lc_calibration_polynomials");
217 std::ofstream f(
path);
222 CHECK(std::fabs(bad - 501.0) > 1000.0,
223 "sanity: the corrupted curve should be absurd (%.4g)", bad);
230 CHECK(s.
load() == 2,
"both channels should load from cubic_state");
234 const double psi =
eval_abcd(j[
"calibration_polynomials"][
"11"], 579791093.0);
235 CHECK(std::fabs(psi - 501.0) < 5.0,
"PT curve should be restored to ~501 psi, got %.4g", psi);
242 std::ofstream f(
path);
243 f <<
"{ this is not json";
246 CHECK(s.
load() == 0,
"a corrupt file loads nothing");
247 CHECK(!s.
save(),
"save must refuse after a failed load — the file is the only copy");
249 std::ifstream f(
path);
250 std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
251 CHECK(content ==
"{ this is not json",
"the original bytes must be untouched, got '%s'",
268 "an ESP32 channel is saved with rawUnit adc");
273 s.
register_channel(8101, 81, 1, 129,
"LOX Tank",
"cubic", SensorKind::PT,
"uV");
276 CHECK(
t7 !=
nullptr &&
t7->points.empty() && !
t7->fit.valid,
277 "the ADC-code fit must NOT attach to the T7 channel");
278 const auto* old = s.
channel(2101);
279 CHECK(old !=
nullptr && old->points.size() == 4 && old->raw_unit ==
"adc",
280 "the ESP32 points stay, as an orphan at their own uid");
283 CHECK(j[
"cubic_state"].
contains(
"2101") && j[
"cubic_state"][
"2101"][
"numPoints"] == 4,
284 "and survive the next save instead of being dropped");
285 CHECK(j[
"cubic_state"][
"8101"][
"rawUnit"] ==
"uV",
"the T7 channel is saved as uV");
294 s.
register_channel(8101, 81, 1, 129,
"LOX Tank",
"cubic", SensorKind::PT,
"uV");
303 s.
register_channel(8103, 81, 3, 131,
"LOX Tank",
"cubic", SensorKind::PT,
"uV");
305 const auto* moved = s.
channel(8103);
306 CHECK(moved !=
nullptr && moved->points.size() == 3 && moved->fit.valid,
307 "a uV fit follows its role to another T7 input");
314 s.
register_channel(2101, 21, 1, 1,
"Fuel Upstream",
"cubic", SensorKind::PT);
316 const auto* orphan = s.
channel(8103);
317 CHECK(orphan !=
nullptr && orphan->logical_ch == 131 && orphan->raw_unit ==
"uV",
318 "a T7 orphan keeps its own logical channel and unit");
322 "nothing lands on ESP32 logical channel 1");
346 CHECK(s.
load() == 2,
"both entries should still load — an orphan's points are kept");
348 const auto* pt = s.
channel(2101);
349 const auto* orphan = s.
channel(4101);
350 CHECK(pt !=
nullptr && orphan !=
nullptr,
"both channels should be present");
351 if (pt ==
nullptr || orphan ==
nullptr)
356 CHECK(pt->logical_ch == 1 && orphan->logical_ch == 1,
357 "premise: PT 2101 and LC 4101 share logical channel 1 (got %u and %u)",
358 static_cast<unsigned>(pt->logical_ch),
static_cast<unsigned>(orphan->logical_ch));
360 CHECK(!orphan->fit.valid,
"premise: a one-point linear fit is invalid");
361 CHECK(pt->fit.valid,
"premise: the PT's own cubic is good");
364 "a live PT applies to the PT manager");
366 "a disabled board's entry must reach NEITHER manager — it would clear logical 1");
370 CHECK(orphan->kind == SensorKind::LC,
"the orphan still knows it is a load cell");
396 CHECK(s.
load() == 2,
"both entries load");
398 const auto* orphan = s.
channel(9101);
399 CHECK(orphan !=
nullptr && orphan->logical_ch == 1 && orphan->fit.valid,
400 "premise: the orphan shares logical channel 1 AND still has a valid fit");
404 CHECK(j[
"calibration_polynomials"].
contains(
"1"),
"the live PT keeps its map entry");
405 const double psi =
eval_abcd(j[
"calibration_polynomials"][
"1"], 579791093.0);
406 CHECK(std::fabs(psi - 501.0) < 5.0,
407 "logical 1 must be the LIVE sensor's curve (~501 psi), got %.4g", psi);
409 CHECK(j[
"cubic_state"].
contains(
"9101") && j[
"cubic_state"][
"9101"][
"numPoints"] == 4,
410 "the orphan's points are kept in cubic_state");
414 std::printf(
"CubicCalibrationStore PT/LC namespace tests\n");
428 std::printf(
" ✅ all passed\n");
* array(stored with pointer to save storage) array_t *array
Owns the per-channel captured points and the fitted cubic for the calibration service.
size_t load()
Load the JSON record, resume points, and re-fit each channel.
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).
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
bool save() const
Atomically write the JSON record (temp + rename).
bool contains(const BasicJsonType *ptr) const
basic_json<> json
default specialization
void add_lc_points(CubicCalibrationStore &s, uint16_t uid)
The LOX Scale's real captured shape: a load cell, codes in the hundreds of thousands.
void legacy_file_without_kind()
void logical_1_collision()
void add_pt_points(CubicCalibrationStore &s, uint16_t uid)
GN2 High's real captured shape: a 5000 psi PT, ADC codes in the hundreds of millions.
void add_point_rejects_unknown_uid()
json read_json(const std::string &path)
void pt_lc_logical_collision()
void recovery_regenerates_overwritten_pt_curve()
double eval_abcd(const json &arr, double x)
void unreadable_file_is_not_overwritten()
CalTarget cal_target(const CubicChannel &ch)
SensorKind
Which family of sensor a channel belongs to.
void removed_board_does_not_win_the_logical_map()
void t7_role_follows_within_t7()
void role_moved_to_t7_keeps_its_unit()
void disabled_board_orphan_is_not_applied()
static uint8_t scratch[BS]
static std::string t7(const std::string &channels, const std::string &extra="", const std::string &name="lj", int board_id=81)