123 std::cout <<
"=== T7 config ===" << std::endl;
127 check(c.t7_errors.empty(),
"a valid T7 parses with no errors");
128 check(c.t7_boards.size() == 1,
"exactly the one type = \"T7\" board is a T7 board");
129 const T7Board&
b = c.t7_boards.at(0);
130 check(
b.section ==
"boards.labjack_1" &&
b.ip ==
"192.168.2.81" &&
b.board_id == 81 &&
131 b.mac ==
"aa:bb:cc:dd:ee:81",
132 "section, ip, board_id and mac are read");
133 check(
b.slot() == 1,
"board_id 81 is Elodin slot 1");
134 check(
b.effective_scans_per_read() == 5,
"scans_per_read defaults to a 50 ms block");
135 check(
b.channels.size() == 3,
"three channels");
136 check(
b.channels[0].ain == 1 &&
b.channels[0].kind ==
"PT" &&
137 b.channels[0].role ==
"LOX Tank" &&
b.channels[0].range_v == 10.0,
138 "PT channel: ain, kind, role, range");
139 check(
b.channels[0].negative_ch() == 199,
"single-ended by default (NEGATIVE_CH 199)");
140 check(
b.channels[1].range_v == 0.1 &&
b.channels[1].tc_type ==
"K",
141 "a TC with no range gets +/-0.1 V, not the 10 V default");
142 check(
b.channels[2].negative_ch() == 5,
"differential AIN4 pairs with AIN5");
147 for (
const auto& [kind, boards] :
active)
149 check(
n == 1,
"active_boards() still sees only the PT board, not the T7");
153 "[boards.pt]\ntype = \"PT\"\nip = \"192.168.2.21\"\nboard_id = 21\n");
154 check(c.t7_boards.empty() && c.t7_errors.empty(),
"a config with no T7 has no T7 errors");
159 "[boards.new_lj]\ntype = \"T7\"\nboard_id = 81\nenabled = false\nmac = \"\"\n");
161 check(c.t7_errors.empty() && c.t7_boards.size() == 1 && !c.t7_boards[0].enabled,
162 "a disabled T7 with no ip and no channels is not an error");
166 std::string
toml, needle, what;
169 {
t7(
ch(1,
"PT") +
ch(1,
"TC",
"tc_type = \"K\"")),
"AIN1 is already used",
170 "the same AIN twice"},
171 {
t7(
ch(0,
"LC",
"differential = true") +
ch(1,
"PT")),
"AIN1 is already used",
172 "an AIN that is the other half of a differential pair"},
173 {
t7(
ch(3,
"LC",
"differential = true")),
"must be an even AIN",
"an odd differential AIN"},
174 {
t7(
ch(14,
"PT")),
"ain must be 0-13",
"AIN14 (a T7 has AIN0-13)"},
175 {
t7(
ch(1,
"PT",
"range = 5.0")),
"range must be one of",
"a range the T7 does not have"},
176 {
t7(
ch(1,
"TC")),
"only type K",
"a thermocouple with no type"},
177 {
t7(
ch(1,
"TC",
"tc_type = \"J\"")),
"only type K",
178 "a thermocouple type with no curve yet (J)"},
179 {
t7(
ch(1,
"PT",
"signal = \"0-5V\"\nrole = \"A\"")),
"needs full_scale",
180 "a PT with no full_scale"},
181 {
t7(
ch(1,
"PT",
"full_scale = 100.0\nrole = \"A\"")),
"needs signal",
182 "a PT with no signal"},
183 {
t7(
ch(1,
"PT",
"full_scale = 100.0\nsignal = \"0-3V\"\nrole = \"A\"")),
"needs signal",
184 "a PT signal that is not one"},
185 {
t7(
ch(1,
"PT",
"full_scale = 100.0\nsignal = \"4-20mA\"\nrole = \"A\"")),
186 "needs shunt_ohms",
"a 4-20mA PT with no shunt"},
187 {
t7(
ch(1,
"PT",
"full_scale = 1.0\nsignal = \"0-5V\"\nshunt_ohms = 250.0\nrole = \"A\"")),
188 "only meaningful on a 4-20mA",
"a shunt on a voltage PT"},
189 {
t7(
ch(1,
"LC",
"full_scale = 1.0\nexcitation_v = 5.0\nrole = \"A\"")),
190 "needs sensitivity_mv_per_v",
"an LC with no sensitivity"},
191 {
t7(
ch(1,
"LC",
"full_scale = 1.0\nsensitivity_mv_per_v = 2.0\nrole = \"A\"")),
192 "needs excitation_v",
"an LC with no excitation"},
193 {
t7(
ch(1,
"TC",
"tc_type = \"K\"\nsignal = \"0-5V\"\nrole = \"A\"")),
194 "only meaningful on a PT",
"a PT signal on a TC"},
195 {
t7(
ch(1,
"TC",
"tc_type = \"K\"\nfull_scale = 5.0\nrole = \"A\"")),
196 "only meaningful on a PT or LC",
"full_scale on a TC"},
197 {
t7(
ch(1,
"TC",
"tc_type = \"K\"")),
"needs a role",
"a TC with no role"},
198 {
t7(
ch(1,
"TC",
"tc_type = \"K\"\nrole = \"A\"") +
199 ch(2,
"TC",
"tc_type = \"K\"\nrole = \"A\"")),
200 "role \"A\" is already used",
"the same role twice"},
201 {
t7(
ch(1,
"PT"),
"cjc_offset_k = 40.0"),
"cjc_offset_k",
"an absurd cjc_offset_k"},
202 {
t7(
ch(1,
"PT",
"tc_type = \"K\"")),
"only meaningful",
"tc_type on a PT"},
203 {
t7(
ch(1,
"STRAIN")),
"kind \"STRAIN\"",
"an unknown kind"},
204 {
t7(
""),
"at least one",
"an enabled T7 with no channels"},
205 {
"[boards.lj]\ntype = \"T7\"\nboard_id = 81\n" +
ch(1,
"PT"),
"ip is required",
206 "an enabled T7 with no ip"},
207 {
"[boards.lj]\ntype = \"T7\"\nip = \"192.168.2\"\nboard_id = 81\n" +
ch(1,
"PT"),
208 "not an IPv4",
"a malformed ip"},
209 {
t7(
ch(1,
"PT"),
"",
"lj", 300),
"board_id must be",
"board_id out of range"},
210 {
t7(
ch(1,
"PT"),
"resolution_index = 9"),
"resolution_index must be 0-8",
211 "a resolution index stream does not support"},
212 {
t7(
ch(1,
"PT") +
ch(2,
"PT"),
"scan_rate_hz = 60000.0"),
"beyond the T7",
213 "an aggregate rate past 100 kS/s"},
214 {
t7(
ch(1,
"PT"),
"scan_rate_hz = 0.0"),
"must be positive",
"a zero scan rate"},
216 {
t7(
ch(0,
"LC",
"range = 0.01") +
ch(2,
"PT")),
"0.01 V is not supported",
217 "the 0.01 V range in a multi-channel stream"},
218 {
t7(
ch(1,
"TC",
"tc_type = \"K\"\nrole = \"A\""),
"resolution_index = 3"),
219 "0.1 V is not supported",
"the 0.1 V range in a multi-channel stream above res 2"},
220 {
t7(
ch(1,
"PT",
"range = 1.0") +
ch(2,
"PT",
"range = 1.0"),
"scan_rate_hz = 3000.0"),
221 "beyond what this scan list",
"two 1 V channels faster than 2 x 210 us allows"},
222 {
t7(
ch(1,
"PT"),
"resolution_index = 8\nscan_rate_hz = 1000.0"),
"at most 600",
223 "one channel at res 8 past Table A.1.7's 600 scans/s"},
224 {
t7(
ch(1,
"PT"),
"",
"lj", 81) +
t7(
ch(1,
"PT",
"",
"lj2"),
"",
"lj2", 91),
225 "shares Elodin slot 1",
"two T7s on the same slot"},
226 {
t7(
ch(1,
"PT"),
"",
"lj", 89),
"only slots 1-8",
"a slot past 8"},
228 for (
const auto&
b : bad) {
239 auto limit_errors = [](
const Config& c) {
241 for (
const auto& e : c.t7_errors)
242 n += e.find(
"not supported") != std::string::npos ||
243 e.find(
"beyond what") != std::string::npos ||
244 e.find(
"at most") != std::string::npos;
249 ch(4,
"LC",
"differential = true\nrange = 0.1")));
254 t7(
ch(1,
"PT") +
ch(2,
"TC",
"tc_type = \"K\""),
"scan_rate_hz = 400.0"));
255 check(limit_errors(mixed) == 0 && limit_errors(single) == 0 && limit_errors(at250) == 0,
256 "stream limits accept the stand's mix at 100 Hz, a lone 0.01 V channel, a TC at 400 "
258 if (limit_errors(mixed) + limit_errors(single) + limit_errors(at250)) {
268 t7(
ch(1,
"PT",
"range = 10\nsignal = \"0-5V\"\nfull_scale = 5000\nrole = \"A\"")));
270 check(c.t7_errors.empty() && c.t7_boards.at(0).channels.at(0).range_v == 10.0 &&
271 c.t7_boards.at(0).channels.at(0).full_scale == 5000.0,
272 "integer range / full_scale (as the GUI writes them) are read as numbers");
275 std::cout <<
"=== type K ===" << std::endl;
280 const double table[][2] = {
281 {-100, -3.554}, {0, 0.0}, {100, 4.096}, {400, 16.397}, {800, 33.275}};
282 bool inv =
true, fwd =
true;
283 for (
const auto& [c, mv] :
table) {
284 inv = inv &&
near(type_k_c_from_mv(mv), c, 0.1);
285 fwd = fwd &&
near(type_k_mv_from_c(c), mv, 0.005);
287 check(inv,
"mV -> C matches the NIST table within 0.1 C");
288 check(fwd,
"C -> mV matches the NIST table within 5 uV");
290 for (
double c = -200; c <= 1300; c += 7.3)
291 round = round &&
near(type_k_c_from_mv(type_k_mv_from_c(c)), c, 1e-6);
292 check(round,
"C -> mV -> C round-trips across -200..1300 C");
293 check(std::isnan(type_k_c_from_mv(80.0)) && std::isnan(type_k_mv_from_c(2000.0)),
294 "off the curve is NaN, not a number");
297 std::cout <<
"=== conversions ===" << std::endl;
304 "0-5V PT: 2.5 V is half scale");
307 "0.5-4.5V PT: 0.5 V is zero, 4.5 V full scale");
317 "4-20mA PT across 250 ohm: 1 V zero, 3 V half, 5 V full");
328 "LC: 10 mV of a 20 mV rated output is half load; sign follows the bridge");
332 "CJC: AIN14 at the reference reading gives the device's own temperature");
336 "CJC: -92.6 K per AIN14 volt (datasheet 18.0), plus cjc_offset_k");
343 "TC: the cold junction's EMF is added back");
345 "TC: without it the reading is ~25 C low (the junction matters)");
349 std::cout <<
"=== stream plan ===" << std::endl;
352 check(
p.scan_names == std::vector<std::string>({
"AIN1",
"AIN2",
"AIN4",
"AIN14"}),
353 "scan list is the configured AINs in config order, then AIN14 for the TC");
354 check(
p.cjc_index == 3 && !
p.channels[3].has_value() &&
p.channels[1]->kind ==
"TC",
355 "AIN14 is marked as the cold junction and carries no channel");
357 check(q.scan_names == std::vector<std::string>({
"AIN1",
"AIN2",
"AIN4"}) &&
359 "no thermocouple, no AIN14");
360 check(
p.scan_names.front().rfind(
"AIN", 0) == 0,
361 "first scan entry is an AIN (auto-recovery marks gaps in the first channel)");
362 auto value = [&](
const std::string&
n) ->
double {
363 for (
size_t i = 0; i <
p.setup_names.size(); ++i)
364 if (
p.setup_names[i] ==
n)
365 return p.setup_values[i];
369 value(
"AIN4_RANGE") == 0.1 &&
value(
"AIN14_RANGE") == 10.0,
370 "each AIN's range is written");
371 check(
value(
"AIN1_NEGATIVE_CH") == 199 &&
value(
"AIN4_NEGATIVE_CH") == 5,
372 "single-ended and differential inputs are written");
373 check(
value(
"STREAM_RESOLUTION_INDEX") == 0 &&
value(
"STREAM_SETTLING_US") == 0 &&
374 value(
"STREAM_TRIGGER_INDEX") == 0 &&
value(
"STREAM_CLOCK_SOURCE") == 0,
375 "stream timing registers are written");
376 bool only_config =
true;
377 for (
const auto&
n :
p.setup_names)
378 if (
n.find(
"DIO") != std::string::npos ||
n.find(
"DAC") != std::string::npos ||
379 n.find(
"FIO") != std::string::npos ||
n.find(
"EIO") != std::string::npos ||
380 n.find(
"CIO") != std::string::npos ||
n.find(
"MIO") != std::string::npos)
382 check(only_config,
"nothing written drives an output (no DIO/FIO/EIO/CIO/MIO/DAC)");
385 "table id is {0x26, (slot-1)*0x20 + ain}");
390 "calibrated table is the raw id + 0x10, entity T7<slot>_Cal.AIN<n>");
395 "calibrated fields have the ESP32 boards' names; VOLTAGE has none");
398 std::cout <<
"=== session ===" << std::endl;
401 auto dev = std::make_unique<FakeT7Device>();
403 uint64_t now = 1'700'000'000'000'000'000ULL;
407 const double rate = s.
start();
408 check(d->
opened_ip ==
"192.168.2.81",
"opens the T7 at the configured ip");
409 check(rate == 100.0 && d->
streaming,
"stream started at the asked rate");
410 check(d->
writes.size() == s.plan().setup_names.size(),
411 "every setup register written, before the stream started");
414 now += 42'000'000ULL;
415 const auto b1 = s.read();
416 check(b1.scans == 5 && b1.samples.size() == 15,
"a block is 5 scans x 3 AINs");
417 check(b1.samples[0].ain == 1 && b1.samples[1].ain == 2 && b1.samples[2].ain == 4,
418 "samples carry their AIN, in scan order");
419 check(std::fabs(b1.samples[3].volts - (0.1 + 0.05 * std::sin(2 * M_PI * 0.2 * 0.01))) <
421 "volts are the device's values for that AIN and scan");
422 check(b1.samples[3].sample_ms == 10 && b1.samples[14].sample_ms == 40,
423 "sample_ms is scan time since stream start (10 ms per scan at 100 Hz)");
424 const uint64_t t0 = b1.samples[0].timestamp_ns, t4 = b1.samples[12].timestamp_ns;
425 check(t4 - t0 == 40'000'000ULL,
"scans in a block are 10 ms apart");
426 check(b1.samples[13].timestamp_ns == t4,
"one timestamp for every AIN in a scan");
427 check(t4 <= now,
"the newest scan is not stamped after it arrived");
429 now += 50'000'000ULL;
430 const auto b2 = s.read();
431 check(b2.samples[0].timestamp_ns - t4 == 10'000'000ULL,
432 "the next block continues one scan period after the last");
433 check(b2.samples[0].sample_ms == 50,
"and its sample_ms continues the count");
441 o.rate_cap_hz = 50.0;
442 uint64_t now = 1'700'000'000'000'000'000ULL;
446 check(s.start() == 50.0,
"start() returns the device's actual rate");
447 now += 100'000'000ULL;
448 const auto b = s.read();
449 check(
b.samples[3].timestamp_ns -
b.samples[0].timestamp_ns == 20'000'000ULL &&
450 b.samples[3].sample_ms == 20,
451 "scan spacing uses the actual rate (20 ms at 50 Hz)");
457 b.scan_rate_hz = 300.0;
458 uint64_t now = 1'700'000'000'000'000'000ULL;
459 T7Session s(
b, std::make_unique<FakeT7Device>(), board_clock, [&] {
463 now += 46'000'000ULL;
464 for (
int i = 0; i < 3; ++i) {
465 const auto blk = s.read();
466 check(blk.samples.back().timestamp_ns <= now,
467 "300 Hz block " + std::to_string(i + 1) +
": newest scan not after arrival");
468 now += 50'000'000ULL;
473 o.dummy_scan_every = 5;
474 uint64_t now = 1'700'000'000'000'000'000ULL;
479 now += 50'000'000ULL;
480 const auto b = s.read();
481 bool any_dummy =
false;
482 for (
const auto& x :
b.samples)
483 any_dummy = any_dummy || x.volts == -9999.0f;
484 check(
b.skipped_samples == 3 &&
b.samples.size() == 12,
485 "auto-recovery scans are counted and not published");
486 check(!any_dummy,
"no -9999 reaches Elodin");
491 uint64_t now = 1'700'000'000'000'000'000ULL;
496 now += 777'000'000ULL;
497 const auto b = s.read();
498 check(
b.samples.back().timestamp_ns == now &&
499 now -
b.samples.front().timestamp_ns == 40'000'000ULL,
500 "[time_sync] arrival: newest scan at arrival, the rest a period apart behind it");
504 b.serial = 470099999;
505 auto dev = std::make_unique<FakeT7Device>();
507 T7Session s(
b, std::move(dev), board_clock, [] {
517 "a T7 with the wrong serial is refused before anything is written to it");
522 auto dev = std::make_unique<FakeT7Device>();
526 check(s.serial() == 0,
"serial() is 0 before start()");
528 check(s.serial() == 470000000,
"start() reads the serial even when the config gives none");
533 o.fail_read_after = 1;
534 auto dev = std::make_unique<FakeT7Device>(
o);
537 return 1'700'000'000'000'000'000ULL;
545 threw = e.
code() == 1306;
547 check(threw,
"a lost link surfaces as T7Error with LJM's code");
549 check(d->
closed,
"and the session still closes the device");
552 std::cout <<
"=== restart: a stream left running ===" << std::endl;
558 auto unit = std::make_shared<FakeT7Unit>();
561 return std::make_unique<FakeT7Device>(
o);
563 const auto now = [] {
564 return 1'700'000'000'000'000'000ULL;
570 check(!
a.stopped_leftover_stream(),
"a fresh T7: nothing to stop");
572 check(unit->streaming && unit->stops == 0,
573 "a bridge that dies without stopping leaves the T7 streaming");
580 T7Session b(edited, std::move(dev), board_clock, now);
586 std::cout <<
" start: " << e.what() << std::endl;
588 check(started &&
b.stopped_leftover_stream(),
589 "the next bridge stops the leftover stream and starts its own");
590 check(unit->stops == 1 && unit->starts == 2 && unit->streaming,
591 "one stop, then a second stream start");
594 return w.first ==
"STREAM_ENABLE";
596 "by eStreamStop on the new handle (what a real T7 answers), not the fallback");
597 check(unit->reg(
"AIN1_RANGE") == 1.0,
598 "the device is reconfigured to the new config, not left on the old one");
599 check(d->
streaming &&
b.read().samples.size() == 15,
"and the new stream delivers");
602 check(!unit->streaming,
"a clean stop leaves the T7 idle");
611 auto unit = std::make_shared<fsw::t7::FakeT7Unit>();
614 o.stop_only_own_stream =
true;
615 const auto now = [] {
616 return 1'700'000'000'000'000'000ULL;
622 auto dev = std::make_unique<FakeT7Device>(
o);
630 std::cout <<
" start: " << e.what() << std::endl;
632 const bool wrote_enable =
634 std::pair<std::string, double>(
"STREAM_ENABLE", 0.0)) != d->
writes.end();
635 check(started && wrote_enable &&
b.stopped_leftover_stream(),
636 "a stop LJM keeps to itself falls back to STREAM_ENABLE = 0, then configures");
644 auto unit = std::make_shared<fsw::t7::FakeT7Unit>();
647 const auto now = [] {
648 return 1'700'000'000'000'000'000ULL;
660 check(code == 1306,
"the bridge whose stream was taken over gets no more data (1306)");
668 o.fail_stop_code = 1239;
669 auto dev = std::make_unique<FakeT7Device>(
o);
672 return 1'700'000'000'000'000'000ULL;
681 "a stop that fails for a real reason stops start(), before any write");
686 const std::string
path =
"/tmp/test_t7_unit_" + std::to_string(::getpid()) +
".state";
687 std::remove(
path.c_str());
688 const auto now = [] {
689 return 1'700'000'000'000'000'000ULL;
693 o.unit = std::make_shared<fsw::t7::FakeT7Unit>(
path);
698 o.unit = std::make_shared<fsw::t7::FakeT7Unit>(
path);
699 check(
o.unit->streaming &&
o.unit->scan_names.size() == 3,
700 "a state file carries the running stream to the next process");
703 check(
b.stopped_leftover_stream() &&
b.read().samples.size() == 15,
704 "which stops it and streams");
707 std::remove(
path.c_str());
710 std::cout <<
"=== cold junction ===" << std::endl;
715 o.device_temp_k = 300.0;
716 o.signal = [](
int ain, double) {
717 return ain == 1 ? 1.25 : 0.001;
719 auto dev = std::make_unique<FakeT7Device>(
o);
721 uint64_t now = 1'700'000'000'000'000'000ULL;
726 check(d->
reads == std::vector<std::string>({
"TEMPERATURE_DEVICE_K",
"AIN14"}),
727 "a TC board reads TEMPERATURE_DEVICE_K beside AIN14 before streaming");
728 check(
near(s.cjc_device_offset_k(), 467.6, 1e-6),
729 "and solves AIN14's offset from them (the fake sits on LabJack's nominal line)");
730 now += 50'000'000ULL;
731 const auto b = s.read();
732 check(
b.samples.size() == 20,
"5 scans x (3 AINs + AIN14), all raw volts");
733 check(
b.cold_junction.size() == 5,
"one cold-junction reading per scan");
735 for (
size_t k = 0; k <
b.cold_junction.size(); ++k)
736 cj_ok = cj_ok &&
near(
b.cold_junction[k].temp_c, 300.0 - 273.15 - 3.0, 1e-4) &&
737 b.cold_junction[k].timestamp_ns ==
b.samples[k * 4].timestamp_ns &&
738 b.cold_junction[k].sample_ms ==
b.samples[k * 4].sample_ms;
740 "at the terminals' temperature (device + cjc_offset_k), stamped like its scan");
742 "the PT is published as its volts, unconverted");
746 return 1'700'000'000'000'000'000ULL;
749 check(s.read().cold_junction.empty(),
"a board with no TC publishes no cold junction");
752 std::cout <<
"=== RTD (LJTick-Resistance) ===" << std::endl;
759 const CVDCoeffs pt100;
760 bool table_ok =
true;
761 for (
auto [t,
r] : std::vector<std::pair<double, double>>{
762 {-200, 18.52}, {-100, 60.26}, {0, 100.00}, {100, 138.51}, {200, 175.86}})
763 table_ok = table_ok &&
near(temp_to_resistance_cvd(t, pt100),
r, 0.01) &&
764 near(resistance_to_temp_cvd_full(
r, pt100), t, 0.03);
765 check(table_ok,
"the full CVD curve and its inverse match the IEC 60751 Pt100 table");
768 bool round_ok =
true;
769 for (
double t : {-196.0, -183.0, -50.0, -0.5, 0.0, 25.0, 300.0})
770 round_ok = round_ok &&
771 near(resistance_to_temp_cvd_full(temp_to_resistance_cvd(t, pt1000), pt1000),
773 check(round_ok,
"the inverse round-trips from -196 C to 300 C (Pt1000)");
774 const double r_lox = temp_to_resistance_cvd(-183.0, pt1000);
776 std::fabs(resistance_to_temp_cvd(r_lox, pt1000) - -183.0) > 1.0,
777 "the quadratic-only inverse is off by over 1 C at LOX temperature (why _full exists)");
778 check(std::isnan(resistance_to_temp_cvd_full(0.0, pt1000)) &&
779 std::isnan(resistance_to_temp_cvd_full(-5.0, pt1000)),
780 "no temperature for a non-positive resistance");
784 t7(
ch(3,
"RTD",
"role = \"LOX RTD\"\nr0_ohms = 1000.0\nljtr_ohms = 1000.0")));
786 check(c.t7_errors.empty(),
"an RTD channel with r0_ohms and ljtr_ohms is valid");
787 const auto& rtd = c.t7_boards.at(0).channels.at(0);
788 check(rtd.range_v == 10.0 && rtd.vref_v == 2.5,
789 "it defaults to the 10 V range and the tick's 2.5 V reference");
792 "half of Vref is R = R_fixed: 1000 ohm, 0 C");
793 const double v_lox = 2.5 * 1000.0 / (1000.0 + r_lox);
795 "the divider's output at LOX temperature reads -183 C");
798 "an open (0 V) or shorted (Vref) RTD reads nothing, not a temperature");
800 "it publishes temperature_c, as an ESP32 RTD");
802 check(m.
all().size() == 1 && m.
all().at(0).channel.kind ==
"RTD",
803 "calibration_service calibrates it");
806 t7(
ch(3,
"RTD",
"role = \"R\"\nljtr_ohms = 1000.0"))),
808 "an RTD without r0_ohms is refused");
812 "an RTD without ljtr_ohms is refused");
815 3,
"RTD",
"role = \"R\"\nr0_ohms = 100.0\nljtr_ohms = 1000.0\nrange = 1.0"))),
816 "must cover vref_v"),
817 "a range below the reference is refused");
821 "role = \"P\"\nsignal = \"0-5V\"\nfull_scale = 100.0\nr0_ohms = 100.0"))),
822 "only meaningful on an RTD"),
823 "RTD settings on another kind are refused");
826 std::cout <<
"=== calibration map ===" << std::endl;
830 check(m.
all().size() == 3,
"every PT / TC / LC input on an enabled T7");
831 const auto* pt = m.
by_uid(8101);
832 check(pt !=
nullptr && pt->channel.kind ==
"PT" && pt->board_key ==
"labjack_1" &&
833 pt->slot == 1 && pt->logical_ch == 129,
834 "uid board_id*100+ain; logical channel 128 + (slot-1)*16 + ain");
836 "lookup by raw table (slot, ain) finds the same input");
837 check(m.
by_table(1, 14) ==
nullptr,
"AIN14 (CJC) is not a calibrated input");
839 std::set<std::pair<uint8_t, uint8_t>> raw(raw_list.begin(), raw_list.end());
840 check(raw == std::set<std::pair<uint8_t, uint8_t>>(
841 {{0x26, 0x01}, {0x26, 0x02}, {0x26, 0x04}, {0x26, 0x0F}}),
842 "subscribes each input's raw table, plus the TC board's cold junction (0x0F)");
844 "T7 logical channels span 128-255, clear of the ESP32 boards' 1-80");
847 "the calibration code is microvolts, rounded and clamped");
851 c.t7_errors.push_back(
"[boards.labjack_1] something");
853 "a T7 config the bridge refuses calibrates nothing (it is not streaming)");
857 c2.t7_boards.push_back(off);
862 o.device_temp_k = 150.0;
872 check(threw,
"an implausible device temperature stops the start (no wrong CJC)");
876 std::cout << (
g_failures ?
"FAILED: " :
"PASSED")