42#define CHECK(cond, ...) \
46 std::printf(" ❌ [%s:%d] ", __func__, __LINE__); \
47 std::printf(__VA_ARGS__); \
54constexpr uint16_t
kUid = 4201;
58 auto p = std::filesystem::temp_directory_path() /
59 (
"lc_linear_" + std::string(name) +
"_" + std::to_string(++seq) +
".json");
60 std::filesystem::remove(
p);
67 double y = 0.0, xp = 1.0;
68 for (
double c : f.
poly) {
85 const double codes[] = {400e3, 425e3, 450e3, 475e3, 500e3};
86 const double noise_kg[] = {0.3, -0.2, 0.25, -0.3, 0.2};
87 std::vector<CubicPoint> pts;
88 for (
int i = 0; i < 5; ++i)
89 pts.push_back({codes[i], (codes[i] - 400e3) * 0.001 + noise_kg[i], 0.0});
95 for (
const auto&
p : pts)
104 CHECK(f !=
nullptr,
"a linear channel with 5 points must have a fit");
108 CHECK(f->
A == 0.0 && f->
B == 0.0,
"a line has no cubic or square term (A=%g B=%g)", f->
A, f->
B);
109 CHECK(std::fabs(f->
C - 0.001) < 2e-5,
"slope should be ~0.001 kg/count, got %g", f->
C);
120 "one point is not a line: no fit, so the service streams physics instead");
125 CHECK(f !=
nullptr && f->
degree == 1,
"two points make a line");
132 fill(s,
"cubic", pts);
135 CHECK(f !=
nullptr && f->
degree == 3,
"cubic with 5 points stays degree 3");
137 CHECK(f->
A == ref.
A && f->
B == ref.
B && f->
C == ref.
C && f->
D == ref.
D,
138 "the cubic fit must be bit-identical to the pre-linear fit");
142 for (
const auto&
p : pts)
145 "a PT with the default model must still fit a cubic");
155 "switching the config to linear must refit the points as a line");
168 const auto eval = [&](
double adc) {
172 const double live_code = zero_code + tank_kg / 0.001;
179 fill(lin,
"linear", pts);
181 fill(cub,
"cubic", pts);
185 for (
double tank : {25.0, 50.0, 100.0}) {
193 " tank %5.1f kg | at home: linear %7.2f cubic %7.2f | zero at 200k: "
194 "linear %7.2f cubic %9.2f\n",
195 tank, lin_home, cub_home, lin_drift, cub_drift);
197 CHECK(std::fabs(lin_drift - lin_home) < 1e-6,
198 "on a line a zero shift must cancel exactly: %f at home vs %f drifted", lin_home,
200 CHECK(std::fabs(lin_drift - tank) < 0.5,
"linear tared reading %f for a %f kg tank",
206 CHECK(std::fabs(cub_drift - cub_home) > 0.5,
207 "expected the cubic's tared reading to move with the drift (%f at home, %f at 200k)"
208 " — if this fails the fixture no longer demonstrates the problem",
209 cub_home, cub_drift);
221 std::ifstream in(
path);
222 const auto root = nlohmann::json::parse(in);
223 const auto&
ch = root[
"cubic_state"][std::to_string(
kUid)];
224 CHECK(
ch[
"active_model"] ==
"linear",
"active_model should say linear");
225 CHECK(
ch[
"degree"] == 1,
"the active fit is the line");
226 CHECK(
ch[
"fits"][
"linear"][
"valid"] ==
true &&
ch[
"fits"][
"linear"][
"degree"] == 1,
227 "fits.linear present");
228 CHECK(
ch[
"fits"][
"cubic"][
"valid"] ==
true &&
ch[
"fits"][
"cubic"][
"degree"] == 3,
229 "fits.cubic present, for the comparison overlay");
231 const auto& raw = root[
"lc_calibration_polynomials"][
"11"];
232 CHECK(raw.is_array() && raw[0] == 0.0 && raw[1] == 0.0,
233 "lc_calibration_polynomials must carry the line (A = B = 0)");
238 CHECK(again.
load() == 1,
"reload one channel");
240 std::filesystem::remove(
path);
248 for (
double adc : {0.0, 200e3, 450e3, 1e6}) {
250 "raw and normalized disagree at %g: %f vs %f",
adc,
eval_raw(f,
adc),
258 std::printf(
"LC linear fit\n");
267 std::printf(
" ✅ all passed\n");
static CubicFit compute_fit(const std::vector< CubicPoint > &pts, int max_degree=3)
Least-squares polynomial of degree min(max_degree, n - 1) over pts.
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.
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).
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).
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.
const LcTare * tare_for(uint16_t uid) const
static STAR_MCP3201 adc(MCP3201_CS_PIN, &mcp_spi)
void both_fits_are_serialized()
void raw_coefficients_match_the_normalized_form()
void tare_cancels_zero_drift_on_linear_not_cubic()
void linear_needs_two_points()
double eval_raw(const CubicFit &f, double adc)
Evaluate a fit the way calibration_service does (raw A..D, PolynomialCalibration).
std::vector< CubicPoint > drifting_scale_points()
A real load cell over 400-500k: 100 kg across 100k counts, linear, with the few-tenths-of-a-kg scatte...
void cubic_model_is_unchanged()
void a_model_change_refits_without_a_capture()
double tared_reading(const CubicFit &f, double zero_code, double tank_kg)
The scenario.
void fill(CubicCalibrationStore &s, const std::string &model, const std::vector< CubicPoint > &pts)
double eval_norm(const CubicFit &f, double adc)
Evaluate a fit the way the browser does (normalized form).
void linear_model_fits_a_line()
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 ...
SensorKind
Which family of sensor a channel belongs to.
Per-sensor calibration record: identity, captured points, and the current fit.
Result of a least-squares polynomial fit over a channel's captured points.
std::vector< double > poly
One operator-captured calibration point: raw ADC code paired with a known reference PSI.
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 ...
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")