33using json = nlohmann::json;
37#define CHECK(cond, ...) \
41 std::printf(" ❌ [%s:%d] ", __func__, __LINE__); \
42 std::printf(__VA_ARGS__); \
52 auto p = std::filesystem::temp_directory_path() /
53 (
"lc_tare_" + std::string(name) +
"_" + std::to_string(++seq) +
".json");
54 std::filesystem::remove(
p);
59 std::ifstream f(
path);
60 return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
65 return [kg_per_count](
double adc) {
66 return adc * kg_per_count;
76 const auto bad =
linear(0.018);
79 CHECK(t !=
nullptr,
"tare should exist");
80 CHECK(std::fabs(t->offset_kg - 18.0) < 1e-9,
"offset under the bad fit should be 18, got %f",
85 const auto good =
linear(0.020);
89 CHECK(std::fabs(t->offset_kg - 20.0) < 1e-9,
90 "offset must re-derive to 20 under the better fit, got %f", t->offset_kg);
91 CHECK(std::fabs(t->adc_at_tare - 1000.0) < 1e-9,
"adc_at_tare is the truth and must not move");
94 const double displayed = good(1000.0) - t->offset_kg;
95 CHECK(std::fabs(displayed) < 1e-9,
"the tank must still read 0 after the re-cal, got %f kg",
105 CHECK(std::fabs(good_offset - 20.0) < 1e-9,
"baseline offset should be 20, got %f",
115 "an untrusted recompute must keep the last good offset, got %f",
133 const auto blown_up = [](double) {
134 return std::numeric_limits<double>::quiet_NaN();
137 CHECK(s.
tare_for(4201) ==
nullptr,
"nothing may be recorded for a refused tare");
139 const auto inf_curve = [](double) {
140 return std::numeric_limits<double>::infinity();
143 CHECK(s.
size() == 0,
"store must still be empty, has %zu", s.
size());
181 t[
"entity"] =
"LC2_Cal.CH1";
182 t[
"adc_at_tare"] = 1000.0;
183 t[
"offset_kg"] = 18.0;
184 t[
"set_at_ms"] = 1757800000000.0;
185 t[
"curve_fp"] = 12345;
188 std::ofstream f(
path);
193 CHECK(s.
load() == 1,
"one tare should load");
202 "the startup recompute must correct a stale offset, got %f", s.
tare_for(4201)->
offset_kg);
212 "the same curve must fingerprint the same — otherwise every read looks stale");
214 "a changed curve must change the fingerprint");
222 "fp must follow a recompute onto a new curve");
234 return linear(0.020);
236 "an unchanged curve must not be reported stale");
244 CHECK(stale == 1,
"a moved curve must be reported stale, got %zu", stale);
250 return linear(0.030);
252 "the audit must be quiet once it has healed");
266 CHECK(s2.
load() == 2,
"two tares should load");
275 CHECK(s3.
load() == 0,
"absent file loads nothing");
282 std::ofstream f(
path);
283 f <<
"{ this is not json";
286 CHECK(s.
load() == 0,
"corrupt file loads nothing");
288 CHECK(!s.
save(),
"save must be refused while load_failed");
289 CHECK(
read_text(
path) ==
"{ this is not json",
"the file must be left exactly as found");
295 std::printf(
"LcTareStore tests\n");
307 std::printf(
" ✅ all passed\n");
* array(stored with pointer to save storage) array_t *array
Owns the per-channel tares for the calibration service.
bool load_failed() const
Set when load() could not read an existing file; blocks save() so an unreadable store is never replac...
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.
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 ...
static uint64_t fingerprint(const Evaluator &eval)
A fingerprint of what a curve does, not of the coefficients that describe it: the evaluator is sample...
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
basic_json<> json
default specialization
void untrusted_curves_keep_the_last_good_offset()
void recompute_carries_tare_across_a_better_fit()
void stale_audit_finds_a_missed_recompute()
void save_load_round_trip()
void entity_matches_the_publish_path()
void loaded_tare_is_corrected_by_recompute()
void unreadable_file_is_not_overwritten()
void fingerprint_tracks_what_the_curve_does()
LcTareStore::Evaluator linear(double kg_per_count)
A linear adc->kg curve, standing in for select_lc_kg.
void non_finite_offset_is_never_recorded()
std::string read_text(const std::string &path)
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 ...
One load cell's tare: the scale reads zero at the load it was holding when the operator pressed Tare.
static uint8_t scratch[BS]
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 ...
std::string lc_tare_entity(uint8_t board_id, uint8_t connector)
LcTareStore — the guards that keep a load-cell tare from becoming a confident wrong number.