STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_t7.cpp
Go to the documentation of this file.
1/**
2 * LabJack T7 support, without a T7: the [boards.*] type = "T7" config, what t7_bridge writes to
3 * the device before streaming, and how a stream block becomes timestamped samples.
4 *
5 * The device is FakeT7Device, which records every call. Nothing here opens a socket.
6 */
7#include <unistd.h>
8
9#include <algorithm>
10#include <climits>
11#include <cmath>
12#include <iostream>
13#include <memory>
14#include <set>
15#include <string>
16#include <vector>
17
19#include "calibration/TypeK.hpp"
20#include "config/Config.hpp"
21#include "t7/T7Calibration.hpp"
22#include "t7/T7Convert.hpp"
23#include "t7/T7Stream.hpp"
24
30
31static int g_failures = 0;
32
33static void check(bool ok, const std::string& what) {
34 std::cout << (ok ? " ok " : " FAIL ") << what << std::endl;
35 if (!ok)
36 g_failures++;
37}
38
39static bool has_error(const Config& c, const std::string& needle) {
40 for (const auto& e : c.t7_errors)
41 if (e.find(needle) != std::string::npos)
42 return true;
43 return false;
44}
45
46static void print_errors(const Config& c) {
47 for (const auto& e : c.t7_errors)
48 std::cout << " " << e << std::endl;
49}
50
51// A valid T7 with a PT, a TC and a differential load cell.
52static const char* kGood = R"(
53[boards.pt_board]
54type = "PT"
55ip = "192.168.2.21"
56board_id = 21
57active_connectors = [1, 2]
58
59[boards.labjack_1]
60type = "T7"
61ip = "192.168.2.81"
62mac = "aa:bb:cc:dd:ee:81"
63board_id = 81
64enabled = true
65scan_rate_hz = 100.0
66
67[[boards.labjack_1.channels]]
68ain = 1
69kind = "PT"
70role = "LOX Tank"
71range = 10.0
72signal = "0-5V"
73full_scale = 1000.0
74
75[[boards.labjack_1.channels]]
76ain = 2
77kind = "TC"
78tc_type = "K"
79role = "Nozzle"
80
81[[boards.labjack_1.channels]]
82ain = 4
83kind = "LC"
84role = "Thrust"
85differential = true
86range = 0.1
87full_scale = 500.0
88sensitivity_mv_per_v = 2.0
89excitation_v = 10.0
90)";
91
92// One T7 board section around a channel table, for the error cases.
93static std::string t7(const std::string& channels, const std::string& extra = "",
94 const std::string& name = "lj", int board_id = 81) {
95 return "[boards." + name + "]\ntype = \"T7\"\nip = \"192.168.2." + std::to_string(board_id) +
96 "\"\nboard_id = " + std::to_string(board_id) + "\n" + extra + "\n" + channels;
97}
98static std::string ch(int ain, const std::string& kind, const std::string& extra = "",
99 const std::string& board = "lj") {
100 return "[[boards." + board + ".channels]]\nain = " + std::to_string(ain) + "\nkind = \"" +
101 kind + "\"\n" + extra + "\n";
102}
103
107
108// The same board with AIN2 as a plain voltage input: no thermocouple, so no AIN14 in the scan.
109// The stream-timing cases below use it so each scan is exactly the three configured inputs.
111 T7Board b = good_board();
112 b.channels[1].kind = "VOLTAGE";
113 b.channels[1].tc_type.clear();
114 b.channels[1].range_v = 10.0;
115 return b;
116}
117
118static bool near(double a, double b, double tol) {
119 return std::fabs(a - b) <= tol;
120}
121
122int main() {
123 std::cout << "=== T7 config ===" << std::endl;
124 {
126 print_errors(c);
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");
143
144 // Every other reader must keep ignoring a T7.
145 const auto active = fsw::config::active_boards(c);
146 size_t n = 0;
147 for (const auto& [kind, boards] : active)
148 n += boards.size();
149 check(n == 1, "active_boards() still sees only the PT board, not the T7");
150 }
151 {
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");
155 }
156 {
157 // Disabled and half filled in is how a T7 gets added: allowed.
159 "[boards.new_lj]\ntype = \"T7\"\nboard_id = 81\nenabled = false\nmac = \"\"\n");
160 print_errors(c);
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");
163 }
164
165 struct Bad {
166 std::string toml, needle, what;
167 };
168 const Bad bad[] = {
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"},
215 // Stream limits, datasheet A-1-1 (support.labjack.com/docs/a-1-1-stream-data-rates-...).
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"},
227 };
228 for (const auto& b : bad) {
229 const Config c = fsw::config::load_from_string(b.toml);
230 const bool ok = has_error(c, b.needle);
231 check(ok, "rejects " + b.what);
232 if (!ok)
233 print_errors(c);
234 }
235
236 {
237 // What the stand will run stays inside the stream limits: a TC at 0.1 V beside a 10 V PT
238 // and a 0.1 V differential LC at 100 Hz, res 0 (= 1), and one 0.01 V channel on its own.
239 auto limit_errors = [](const Config& c) {
240 int n = 0;
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;
245 return n;
246 };
247 const Config mixed =
248 fsw::config::load_from_string(t7(ch(1, "PT") + ch(2, "TC", "tc_type = \"K\"") +
249 ch(4, "LC", "differential = true\nrange = 0.1")));
250 const Config single = fsw::config::load_from_string(t7(ch(1, "LC", "range = 0.01")));
251 // LJM's 2631 (the temperature sensor above 250 Hz) is the T8's limit (datasheet 18.0:
252 // "the maximum sampling rate of the T8 temperature sensors is 250 Hz"), not the T7's.
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 "
257 "Hz");
258 if (limit_errors(mixed) + limit_errors(single) + limit_errors(at250)) {
259 print_errors(mixed);
260 print_errors(single);
261 print_errors(at250);
262 }
263 }
264
265 {
266 // The GUI's config editor writes through smol-toml, which turns 10.0 into 10.
268 t7(ch(1, "PT", "range = 10\nsignal = \"0-5V\"\nfull_scale = 5000\nrole = \"A\"")));
269 print_errors(c);
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");
273 }
274
275 std::cout << "=== type K ===" << std::endl;
276 {
279 // NIST ITS-90 type K table points; these are also the sub-range boundaries of the inverse.
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);
286 }
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");
289 bool round = true;
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");
295 }
296
297 std::cout << "=== conversions ===" << std::endl;
298 {
300 pt.kind = "PT";
301 pt.full_scale = 1000;
302 pt.signal = "0-5V";
303 check(near(fsw::t7::pt_psi(2.5, pt), 500, 1e-9) && near(fsw::t7::pt_psi(0, pt), 0, 1e-9),
304 "0-5V PT: 2.5 V is half scale");
305 pt.signal = "0.5-4.5V";
306 check(near(fsw::t7::pt_psi(0.5, pt), 0, 1e-9) && near(fsw::t7::pt_psi(4.5, pt), 1000, 1e-9),
307 "0.5-4.5V PT: 0.5 V is zero, 4.5 V full scale");
308 pt.signal = "1-5V";
309 check(near(fsw::t7::pt_psi(3.0, pt), 500, 1e-9), "1-5V PT: 3 V is half scale");
310 pt.signal = "0-10V";
311 check(near(fsw::t7::pt_psi(10.0, pt), 1000, 1e-9), "0-10V PT: 10 V is full scale");
312 pt.signal = "4-20mA";
313 pt.shunt_ohms = 250; // 4 mA -> 1 V, 20 mA -> 5 V
314 check(near(fsw::t7::pt_psi(1.0, pt), 0, 1e-9) &&
315 near(fsw::t7::pt_psi(3.0, pt), 500, 1e-9) &&
316 near(fsw::t7::pt_psi(5.0, pt), 1000, 1e-9),
317 "4-20mA PT across 250 ohm: 1 V zero, 3 V half, 5 V full");
318 pt.signal = "";
319 check(std::isnan(fsw::t7::pt_psi(2.5, pt)), "a PT with no signal converts to NaN");
320
322 lc.kind = "LC";
323 lc.full_scale = 500;
324 lc.sensitivity_mv_per_v = 2.0;
325 lc.excitation_v = 10.0; // rated output 20 mV
326 check(near(fsw::t7::lc_kg(0.010, lc), 250, 1e-9) &&
327 near(fsw::t7::lc_kg(-0.020, lc), -500, 1e-9),
328 "LC: 10 mV of a 20 mV rated output is half load; sign follows the bridge");
329
330 const double off = fsw::t7::cjc_device_offset_k(300.0, 1.8);
331 check(near(fsw::t7::cjc_temp_c(1.8, off, 0.0), 300.0 - 273.15, 1e-9),
332 "CJC: AIN14 at the reference reading gives the device's own temperature");
333 check(near(fsw::t7::cjc_temp_c(1.81, off, 0.0) - fsw::t7::cjc_temp_c(1.8, off, 0.0), -0.926,
334 1e-9) &&
335 near(fsw::t7::cjc_temp_c(1.8, off, -3.0), 300.0 - 273.15 - 3.0, 1e-9),
336 "CJC: -92.6 K per AIN14 volt (datasheet 18.0), plus cjc_offset_k");
337
338 // A thermocouple at 200 C, terminals at 25 C, reads EMF(200) - EMF(25).
339 const double v =
341 1000.0;
342 check(near(fsw::t7::tc_c(v, 25.0), 200.0, 1e-6),
343 "TC: the cold junction's EMF is added back");
344 check(!near(fsw::t7::tc_c(v, 0.0), 200.0, 10.0),
345 "TC: without it the reading is ~25 C low (the junction matters)");
346 check(std::isnan(fsw::t7::tc_c(0.5, 25.0)), "TC: 500 mV is off the curve -> NaN");
347 }
348
349 std::cout << "=== stream plan ===" << std::endl;
350 {
351 const auto p = fsw::t7::plan_stream(good_board());
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");
356 const auto q = fsw::t7::plan_stream(stream_board());
357 check(q.scan_names == std::vector<std::string>({"AIN1", "AIN2", "AIN4"}) &&
358 q.cjc_index == -1,
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];
366 return std::nan("");
367 };
368 check(value("AIN1_RANGE") == 10.0 && value("AIN2_RANGE") == 0.1 &&
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)
381 only_config = false;
382 check(only_config, "nothing written drives an output (no DIO/FIO/EIO/CIO/MIO/DAC)");
383 check(fsw::t7::table_id(1, 3) == std::array<uint8_t, 2>{0x26, 0x03} &&
384 fsw::t7::table_id(2, 13) == std::array<uint8_t, 2>{0x26, 0x2D},
385 "table id is {0x26, (slot-1)*0x20 + ain}");
386 check(fsw::t7::entity_name(1, 3) == "T71.AIN3", "entity name T7<slot>.AIN<n>");
387 check(fsw::t7::calibrated_table_id(1, 3) == std::array<uint8_t, 2>{0x26, 0x13} &&
388 fsw::t7::calibrated_table_id(2, 13) == std::array<uint8_t, 2>{0x26, 0x3D} &&
389 fsw::t7::calibrated_entity_name(1, 3) == "T71_Cal.AIN3",
390 "calibrated table is the raw id + 0x10, entity T7<slot>_Cal.AIN<n>");
391 check(std::string(fsw::t7::calibrated_field("PT")) == "pressure_psi" &&
392 std::string(fsw::t7::calibrated_field("TC")) == "temperature_c" &&
393 std::string(fsw::t7::calibrated_field("LC")) == "force_kg" &&
394 std::string(fsw::t7::calibrated_field("VOLTAGE")).empty(),
395 "calibrated fields have the ESP32 boards' names; VOLTAGE has none");
396 }
397
398 std::cout << "=== session ===" << std::endl;
399 const fsw::time::TimeSyncConfig board_clock; // default mode BoardClock
400 {
401 auto dev = std::make_unique<FakeT7Device>();
402 FakeT7Device* d = dev.get();
403 uint64_t now = 1'700'000'000'000'000'000ULL;
404 T7Session s(stream_board(), std::move(dev), board_clock, [&] {
405 return now;
406 });
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");
412
413 // Block 1 arrives 2 ms after its newest scan (scan 4, 40 ms in).
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))) <
420 1e-6,
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");
428
429 now += 50'000'000ULL; // next block, same latency
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");
434
435 s.stop();
436 check(!d->streaming && d->closed, "stop() stops the stream and closes the device");
437 }
438 {
439 // The device rounds the rate: timestamps must follow what it runs at, not the ask.
441 o.rate_cap_hz = 50.0;
442 uint64_t now = 1'700'000'000'000'000'000ULL;
443 T7Session s(stream_board(), std::make_unique<FakeT7Device>(o), board_clock, [&] {
444 return now;
445 });
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)");
452 }
453 {
454 // At 300 Hz scans fall between whole milliseconds. The clock sync works in whole ms and
455 // the sub-ms part is added back, which must not push the newest scan past its arrival.
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, [&] {
460 return now;
461 });
462 s.start();
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;
469 }
470 }
471 {
473 o.dummy_scan_every = 5; // scan 4 of each 5-scan block is lost
474 uint64_t now = 1'700'000'000'000'000'000ULL;
475 T7Session s(stream_board(), std::make_unique<FakeT7Device>(o), board_clock, [&] {
476 return now;
477 });
478 s.start();
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");
487 }
488 {
491 uint64_t now = 1'700'000'000'000'000'000ULL;
492 T7Session s(stream_board(), std::make_unique<FakeT7Device>(), arrival, [&] {
493 return now;
494 });
495 s.start();
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");
501 }
502 {
504 b.serial = 470099999;
505 auto dev = std::make_unique<FakeT7Device>(); // serial 470000000
506 FakeT7Device* d = dev.get();
507 T7Session s(b, std::move(dev), board_clock, [] {
508 return uint64_t{0};
509 });
510 bool threw = false;
511 try {
512 s.start();
513 } catch (const T7Error&) {
514 threw = true;
515 }
516 check(threw && !d->streaming && d->writes.empty(),
517 "a T7 with the wrong serial is refused before anything is written to it");
518 s.stop();
519 }
520 {
521 // No serial in config: start() still reads it, for the bridge's bring-up log line.
522 auto dev = std::make_unique<FakeT7Device>(); // serial 470000000
523 T7Session s(stream_board(), std::move(dev), board_clock, [] {
524 return uint64_t{0};
525 });
526 check(s.serial() == 0, "serial() is 0 before start()");
527 s.start();
528 check(s.serial() == 470000000, "start() reads the serial even when the config gives none");
529 s.stop();
530 }
531 {
533 o.fail_read_after = 1;
534 auto dev = std::make_unique<FakeT7Device>(o);
535 FakeT7Device* d = dev.get();
536 T7Session s(stream_board(), std::move(dev), board_clock, [] {
537 return 1'700'000'000'000'000'000ULL;
538 });
539 s.start();
540 s.read();
541 bool threw = false;
542 try {
543 s.read();
544 } catch (const T7Error& e) {
545 threw = e.code() == 1306;
546 }
547 check(threw, "a lost link surfaces as T7Error with LJM's code");
548 s.stop();
549 check(d->closed, "and the session still closes the device");
550 }
551
552 std::cout << "=== restart: a stream left running ===" << std::endl;
553 {
554 // A T7 keeps streaming after the bridge that started it dies, and refuses configuration
555 // while it does. A killed bridge (or a server torn down mid-run) is modelled by dropping
556 // its session without stop(): the unit is the device, and it outlives the connection.
558 auto unit = std::make_shared<FakeT7Unit>();
559 auto handle = [&](FakeT7Device::Options o = {}) {
560 o.unit = unit;
561 return std::make_unique<FakeT7Device>(o);
562 };
563 const auto now = [] {
564 return 1'700'000'000'000'000'000ULL;
565 };
566 {
567 T7Session a(stream_board(), handle(), board_clock, now);
568 a.start();
569 a.read();
570 check(!a.stopped_leftover_stream(), "a fresh T7: nothing to stop");
571 } // killed: no stop()
572 check(unit->streaming && unit->stops == 0,
573 "a bridge that dies without stopping leaves the T7 streaming");
574
575 // The next bridge comes up with an edited config: AIN1 moved to the 1 V range.
576 T7Board edited = stream_board();
577 edited.channels[0].range_v = 1.0;
578 auto dev = handle();
579 FakeT7Device* d = dev.get();
580 T7Session b(edited, std::move(dev), board_clock, now);
581 bool started = true;
582 try {
583 b.start();
584 } catch (const T7Error& e) {
585 started = false;
586 std::cout << " start: " << e.what() << std::endl;
587 }
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");
592 check(d->stop_calls == 1 && std::none_of(d->writes.begin(), d->writes.end(),
593 [](const auto& w) {
594 return w.first == "STREAM_ENABLE";
595 }),
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");
600
601 b.stop();
602 check(!unit->streaming, "a clean stop leaves the T7 idle");
603 T7Session c(stream_board(), handle(), board_clock, now);
604 c.start();
605 check(!c.stopped_leftover_stream(), "and the bridge after a clean stop finds nothing");
606 c.stop();
607 }
608 {
609 // If LJM answers a new handle's stop locally (1225) and never tells the device, the
610 // setup write hits STREAM_IS_ACTIVE: stop it at the device with STREAM_ENABLE = 0.
611 auto unit = std::make_shared<fsw::t7::FakeT7Unit>();
613 o.unit = unit;
614 o.stop_only_own_stream = true;
615 const auto now = [] {
616 return 1'700'000'000'000'000'000ULL;
617 };
618 {
619 T7Session a(stream_board(), std::make_unique<FakeT7Device>(o), board_clock, now);
620 a.start();
621 }
622 auto dev = std::make_unique<FakeT7Device>(o);
623 FakeT7Device* d = dev.get();
624 T7Session b(stream_board(), std::move(dev), board_clock, now);
625 bool started = true;
626 try {
627 b.start();
628 } catch (const T7Error& e) {
629 started = false;
630 std::cout << " start: " << e.what() << std::endl;
631 }
632 const bool wrote_enable =
633 std::find(d->writes.begin(), d->writes.end(),
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");
637 b.stop();
638 }
639 {
640 // Two bridges on one T7 (an old server never shut down): the new one takes the stream
641 // and the old one's reads fail rather than silently stalling. They then keep taking it
642 // from each other on every retry (a stop from any connection stops the device), which is
643 // why the bridge logs each takeover: two live bridges on one T7 is a setup error.
644 auto unit = std::make_shared<fsw::t7::FakeT7Unit>();
646 o.unit = unit;
647 const auto now = [] {
648 return 1'700'000'000'000'000'000ULL;
649 };
650 T7Session old_one(stream_board(), std::make_unique<FakeT7Device>(o), board_clock, now);
651 old_one.start();
652 T7Session new_one(stream_board(), std::make_unique<FakeT7Device>(o), board_clock, now);
653 new_one.start();
654 int code = 0;
655 try {
656 old_one.read();
657 } catch (const T7Error& e) {
658 code = e.code();
659 }
660 check(code == 1306, "the bridge whose stream was taken over gets no more data (1306)");
661 check(new_one.read().samples.size() == 15, "while the new one streams");
662 new_one.stop();
663 old_one.stop();
664 }
665 {
666 // A stop that fails for a real reason is not "nothing was running".
668 o.fail_stop_code = 1239; // LJME_RECONNECT_FAILED
669 auto dev = std::make_unique<FakeT7Device>(o);
670 FakeT7Device* d = dev.get();
671 T7Session s(stream_board(), std::move(dev), board_clock, [] {
672 return 1'700'000'000'000'000'000ULL;
673 });
674 int code = 0;
675 try {
676 s.start();
677 } catch (const T7Error& e) {
678 code = e.code();
679 }
680 check(code == 1239 && d->writes.empty(),
681 "a stop that fails for a real reason stops start(), before any write");
682 s.stop();
683 }
684 {
685 // Across processes: the unit is a file, and a new FakeT7Unit (a new bridge) reads it.
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;
690 };
691 {
693 o.unit = std::make_shared<fsw::t7::FakeT7Unit>(path);
694 T7Session a(stream_board(), std::make_unique<FakeT7Device>(o), board_clock, now);
695 a.start();
696 } // process 1 killed
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");
701 T7Session b(stream_board(), std::make_unique<FakeT7Device>(o), board_clock, now);
702 b.start();
703 check(b.stopped_leftover_stream() && b.read().samples.size() == 15,
704 "which stops it and streams");
705 b.stop();
706 check(!fsw::t7::FakeT7Unit(path).streaming, "and a clean stop is saved too");
707 std::remove(path.c_str());
708 }
709
710 std::cout << "=== cold junction ===" << std::endl;
711 {
712 // The bridge publishes raw volts only; what it alone can add is each scan's cold
713 // junction, which calibration_service needs to turn TC volts into temperature.
715 o.device_temp_k = 300.0; // board cjc_offset_k -3.0 -> junction at 23.85 C
716 o.signal = [](int ain, double) {
717 return ain == 1 ? 1.25 : 0.001;
718 };
719 auto dev = std::make_unique<FakeT7Device>(o);
720 FakeT7Device* d = dev.get();
721 uint64_t now = 1'700'000'000'000'000'000ULL;
722 T7Session s(good_board(), std::move(dev), board_clock, [&] {
723 return now;
724 });
725 s.start();
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");
734 bool cj_ok = true;
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;
739 check(cj_ok,
740 "at the terminals' temperature (device + cjc_offset_k), stamped like its scan");
741 check(near(b.samples[0].volts, 1.25, 1e-6),
742 "the PT is published as its volts, unconverted");
743 }
744 {
745 T7Session s(stream_board(), std::make_unique<FakeT7Device>(), board_clock, [] {
746 return 1'700'000'000'000'000'000ULL;
747 });
748 s.start();
749 check(s.read().cold_junction.empty(), "a board with no TC publishes no cold junction");
750 }
751
752 std::cout << "=== RTD (LJTick-Resistance) ===" << std::endl;
753 {
758 // IEC 60751 Pt100 table values (ohms), independent of the formula under test.
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");
766 CVDCoeffs pt1000;
767 pt1000.R0 = 1000.0;
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),
772 t, 1e-6);
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);
775 check(
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");
781
782 // A Pt1000 on an LJTick-Resistance-1k: Vout = 2.5 * 1000 / (1000 + R).
784 t7(ch(3, "RTD", "role = \"LOX RTD\"\nr0_ohms = 1000.0\nljtr_ohms = 1000.0")));
785 print_errors(c);
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");
790 check(near(fsw::t7::rtd_ohms(1.25, rtd), 1000.0, 1e-9) &&
791 near(fsw::t7::rtd_c(1.25, rtd), 0.0, 1e-9),
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);
794 check(near(fsw::t7::rtd_c(v_lox, rtd), -183.0, 1e-6),
795 "the divider's output at LOX temperature reads -183 C");
796 check(std::isnan(fsw::t7::rtd_c(0.0, rtd)) && std::isnan(fsw::t7::rtd_c(2.5, rtd)) &&
797 std::isnan(fsw::t7::rtd_c(-0.1, rtd)),
798 "an open (0 V) or shorted (Vref) RTD reads nothing, not a temperature");
799 check(std::string(fsw::t7::calibrated_field("RTD")) == "temperature_c",
800 "it publishes temperature_c, as an ESP32 RTD");
801 const fsw::t7::T7CalibrationMap m(c);
802 check(m.all().size() == 1 && m.all().at(0).channel.kind == "RTD",
803 "calibration_service calibrates it");
804
806 t7(ch(3, "RTD", "role = \"R\"\nljtr_ohms = 1000.0"))),
807 "needs r0_ohms"),
808 "an RTD without r0_ohms is refused");
810 fsw::config::load_from_string(t7(ch(3, "RTD", "role = \"R\"\nr0_ohms = 100.0"))),
811 "needs ljtr_ohms"),
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");
818 check(
820 1, "PT",
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");
824 }
825
826 std::cout << "=== calibration map ===" << std::endl;
827 {
829 const fsw::t7::T7CalibrationMap m(c);
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");
835 check(m.by_table(1, 4) == m.by_uid(8104) && m.by_table(1, 4)->channel.kind == "LC",
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");
838 const auto raw_list = m.raw_tables();
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");
845 check(fsw::t7::t7_counts(1.25) == 1250000 && fsw::t7::t7_counts(-0.0000014) == -1 &&
846 fsw::t7::t7_counts(1e9) == INT32_MAX,
847 "the calibration code is microvolts, rounded and clamped");
848 }
849 {
851 c.t7_errors.push_back("[boards.labjack_1] something");
853 "a T7 config the bridge refuses calibrates nothing (it is not streaming)");
854 T7Board off = good_board();
855 Config c2;
856 off.enabled = false;
857 c2.t7_boards.push_back(off);
858 check(fsw::t7::T7CalibrationMap(c2).empty(), "nor does a disabled T7");
859 }
860 {
862 o.device_temp_k = 150.0; // a broken internal sensor
863 T7Session s(good_board(), std::make_unique<FakeT7Device>(o), board_clock, [] {
864 return uint64_t{0};
865 });
866 bool threw = false;
867 try {
868 s.start();
869 } catch (const T7Error&) {
870 threw = true;
871 }
872 check(threw, "an implausible device temperature stops the start (no wrong CJC)");
873 s.stop();
874 }
875
876 std::cout << (g_failures ? "FAILED: " : "PASSED")
877 << (g_failures ? std::to_string(g_failures) : "") << std::endl;
878 return g_failures ? 1 : 0;
879}
static const uint32_t channels[BOARD_ADC_COUNT]
Definition board_adc.c:7
A connection to a stand-in T7, for tests and for running the bridge with no hardware (t7_bridge --fak...
Definition T7Stream.hpp:249
std::vector< std::string > reads
every name read_names() was asked for
Definition T7Stream.hpp:289
std::string opened_ip
Definition T7Stream.hpp:286
bool streaming
this handle's own stream is running
Definition T7Stream.hpp:290
std::vector< std::pair< std::string, double > > writes
Definition T7Stream.hpp:287
The stand-in T7 itself, as distinct from a connection to it.
Definition T7Stream.hpp:213
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.
A device failure.
Definition T7Stream.hpp:62
int code() const
Definition T7Stream.hpp:66
One T7's stream: open, configure, start, then read blocks until something throws.
Definition T7Stream.hpp:144
Block read()
Reads and decodes one block.
Definition T7Stream.cpp:158
double start()
Opens the device, checks its serial number if the config gives one, writes the setup,...
Definition T7Stream.cpp:92
bool stopped_leftover_stream() const
start() found a stream already running on the device (left by a bridge that was killed,...
Definition T7Stream.hpp:173
void stop() noexcept
Stops the stream and closes the device; never throws.
Definition T7Stream.cpp:215
uint8_t value
static uint8_t board_id
Definition main.cpp:30
static bool ok
Definition flight_e2e.c:435
int32_t p
Definition flight_e2e.c:78
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
return o
Definition json.hpp:13897
double resistance_to_temp_cvd(double resistance_ohms, const CVDCoeffs &cvd=CVDCoeffs{})
Convert resistance (Ω) to temperature (°C) using iterative CVD inverse.
double temp_to_resistance_cvd(double t_c, const CVDCoeffs &cvd=CVDCoeffs{})
Resistance (Ω) at temperature (°C), IEC 60751 Callendar-Van Dusen, the C term below 0 °C.
double resistance_to_temp_cvd_full(double resistance_ohms, const CVDCoeffs &cvd=CVDCoeffs{})
The full CVD inverse: exact above 0 °C (the quadratic), Newton on the C term below it.
double type_k_mv_from_c(double c)
EMF (mV, referenced to 0 C) a type K produces at c.
Definition TypeK.cpp:38
double type_k_c_from_mv(double mv)
Temperature (C) for a type K EMF in mV, referenced to 0 C.
Definition TypeK.cpp:9
std::map< ActiveBoardKind, std::vector< elodin::BoardChannels > > active_boards(const Config &cfg)
Derive the active-board table (enabled boards -> Elodin BoardChannels), with the same slot-collision ...
Definition Config.cpp:468
Config load_from_string(const std::string &text)
Parse config from an in-memory TOML string (SequencerService / ActuatorCommander hold it so).
Definition Config.cpp:458
double cjc_device_offset_k(double device_temp_k, double ain14_v)
AIN14 offset (K) that makes slope*ain14_v land on the device's own temperature reading.
Definition T7Convert.cpp:17
double lc_kg(double volts, const config::T7Channel &ch)
LC: a bridge's output is sensitivity x excitation at rated load.
Definition T7Convert.cpp:46
StreamPlan plan_stream(const config::T7Board &board)
Definition T7Stream.cpp:38
std::string entity_name(uint8_t slot, uint8_t ain)
Elodin entity name for one AIN, e.g.
Definition T7Stream.cpp:26
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.
Definition T7Stream.cpp:30
double rtd_ohms(double volts, const config::T7Channel &ch)
RTD on an LJTick-Resistance: the divider's output volts -> the RTD's ohms.
Definition T7Convert.cpp:58
std::array< uint8_t, 2 > table_id(uint8_t slot, uint8_t ain)
Table id for one AIN: {0x26, (slot-1)*0x20 + ain}, the same layout as the ESP32 boards.
Definition T7Stream.cpp:22
const char * calibrated_field(const std::string &kind)
The Elodin field a calibrated channel of this kind publishes ("pressure_psi", ...),...
Definition T7Convert.cpp:70
double tc_c(double volts, double cold_junction_c)
Type K thermocouple: the measured EMF plus the EMF the cold junction subtracts.
Definition T7Convert.cpp:53
std::string calibrated_entity_name(uint8_t slot, uint8_t ain)
Calibrated entity name, e.g.
Definition T7Stream.cpp:34
double cjc_temp_c(double ain14_v, double device_offset_k, double cjc_offset_k)
Cold-junction temperature (C) from a streamed AIN14 reading.
Definition T7Convert.cpp:21
double pt_psi(double volts, const config::T7Channel &ch)
PT: linear between the signal's zero and full-scale outputs.
Definition T7Convert.cpp:25
uint8_t t7_logical_channel(uint8_t slot, uint8_t ain)
Logical calibration channel for a T7 input.
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).
Definition T7Convert.cpp:64
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
std::vector< T7Board > t7_boards
Definition Config.hpp:276
std::vector< T7Channel > channels
Definition T7Config.hpp:86
One streamed analog input.
Definition T7Config.hpp:37
std::string kind
"PT" | "TC" | "LC" | "RTD" | "VOLTAGE" — what is wired to it.
Definition T7Config.hpp:39
double shunt_ohms
PT "4-20mA": the resistor the loop current crosses.
Definition T7Config.hpp:50
std::string signal
PT output: "0-5V" "0.5-4.5V" "1-5V" "0-10V" "4-20mA".
Definition T7Config.hpp:49
double full_scale
PT: psi at full output. LC: kg at rated output.
Definition T7Config.hpp:48
double excitation_v
LC: bridge excitation voltage.
Definition T7Config.hpp:52
double sensitivity_mv_per_v
LC: rated output, mV per V of excitation.
Definition T7Config.hpp:51
std::vector< Sample > samples
Definition T7Stream.hpp:126
config::T7Channel channel
static int g_failures
static boot_report_t r
std::string board(const std::string &name, const std::string &id, bool enabled=true)
static bool has_error(const Config &c, const std::string &needle)
Definition test_t7.cpp:39
static std::string ch(int ain, const std::string &kind, const std::string &extra="", const std::string &board="lj")
Definition test_t7.cpp:98
static const char * kGood
Definition test_t7.cpp:52
static bool near(double a, double b, double tol)
Definition test_t7.cpp:118
static T7Board good_board()
Definition test_t7.cpp:104
static void check(bool ok, const std::string &what)
Definition test_t7.cpp:33
static std::string t7(const std::string &channels, const std::string &extra="", const std::string &name="lj", int board_id=81)
Definition test_t7.cpp:93
static void print_errors(const Config &c)
Definition test_t7.cpp:46
int main()
Definition test_t7.cpp:122
static T7Board stream_board()
Definition test_t7.cpp:110