STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
T7Config.cpp
Go to the documentation of this file.
1#include "config/T7Config.hpp"
2
3#include <arpa/inet.h>
4
5#include <algorithm>
6#include <cmath>
7#include <map>
8#include <set>
9#include <toml++/toml.hpp>
10
11namespace fsw {
12namespace config {
13
15 if (scans_per_read > 0)
16 return scans_per_read;
17 // A 50 ms block: low enough latency for the GUI, few enough reads that LJM's per-call cost
18 // does not matter.
19 return std::max(1, static_cast<int>(scan_rate_hz / 20.0));
20}
21
22namespace detail {
23
24namespace {
25
26// The T7's analog input ranges (AIN#_RANGE accepts exactly these spans).
27constexpr double kRanges[] = {10.0, 1.0, 0.1, 0.01};
28const std::set<std::string> kKinds = {"PT", "TC", "LC", "RTD", "VOLTAGE"};
29// Thermocouple types t7_bridge can convert to temperature. Only K so far: its curve is the
30// NIST ITS-90 inverse the calibration service already uses (fsw::calibration::type_k_*).
31const std::set<std::string> kTcTypes = {"K"};
32const std::set<std::string> kPtSignals = {"0-5V", "0.5-4.5V", "1-5V", "0-10V", "4-20mA"};
33
34// Above this an aggregate rate is certainly beyond the T7 (its fastest stream is 100 kS/s at
35// resolution index 0 and the widest range). The device reports STREAM_SCAN_OVERLAP for anything
36// it cannot keep up with below this; this check only catches an obvious typo.
37constexpr double kMaxAggregateSamplesPerS = 100000.0;
38
39// Stream accepts resolution indices 0-8; the T7-Pro's 9-12 are command-response only (datasheet
40// 3.2: "Resolution indices 9-12 are not supported in stream"). 0 means 1 in stream (A-1-1).
41constexpr int kMaxStreamResolutionIndex = 8;
42
43// Datasheet A-1-1, Table A.1.7, first column: a single-address stream's max scan rate by
44// resolution index 1-8, at any range (one channel needs no settling).
45constexpr double kSingleAddressMaxHz[8] = {100000, 48000, 22000, 11000, 5500, 2500, 1200, 600};
46
47// Datasheet A-1-1, Table A.1.8: interchannel delay (us) of a multi-address stream, by range and
48// resolution index 1-8; 0 = "N.S." (not supported in multi-channel stream). Summed over a scan it
49// estimates the shortest scan period. The estimate errs fast (Table A.1.7's measured maxima are
50// lower still at high resolution), so it only refuses what the T7 certainly cannot do; anything
51// else it cannot keep up with, the device reports as STREAM_SCAN_OVERLAP (2942).
52double interchannel_delay_us(double range_v, int res) {
53 static constexpr double k10[8] = {15, 25, 45, 90, 170, 335, 670, 1335};
54 static constexpr double k1[8] = {210, 220, 545, 585, 1200, 2415, 2750, 3415};
55 static constexpr double k01[8] = {1040, 2105, 0, 0, 0, 0, 0, 0};
56 const int i = std::clamp(res, 1, 8) - 1;
57 if (std::fabs(range_v - 10.0) < 1e-9)
58 return k10[i];
59 if (std::fabs(range_v - 1.0) < 1e-9)
60 return k1[i];
61 if (std::fabs(range_v - 0.1) < 1e-9)
62 return k01[i];
63 if (std::fabs(range_v - 0.01) < 1e-9)
64 return 0; // N.S. at every resolution in multi-channel stream
65 return -1; // not a T7 range; reported elsewhere
66}
67
68bool is_ipv4(const std::string& s) {
69 in_addr a{};
70 return inet_pton(AF_INET, s.c_str(), &a) == 1;
71}
72
73std::string at(const std::string& section, int idx) {
74 return "[" + section + "] channel " + std::to_string(idx + 1) + ": ";
75}
76
77// What the T7's stream cannot do with this board's scan list (its channels, plus AIN14 at +/-10 V
78// on a thermocouple board), per datasheet A-1-1. Mirrored in shared/config-validation.ts.
79std::vector<std::string> stream_limit_issues(const T7Board& b) {
80 std::vector<std::string> out;
81 if (b.channels.empty() || !(b.scan_rate_hz > 0))
82 return out;
83 const bool has_tc = std::any_of(b.channels.begin(), b.channels.end(), [](const T7Channel& c) {
84 return c.kind == "TC";
85 });
86 std::vector<double> ranges;
87 for (const auto& c : b.channels)
88 ranges.push_back(c.range_v);
89 if (has_tc)
90 ranges.push_back(10.0); // AIN14
91 const int res = std::clamp(b.resolution_index, 0, kMaxStreamResolutionIndex);
92 const int eff = res == 0 ? 1 : res;
93 auto rate_str = [](double hz) {
94 return std::to_string(static_cast<int>(std::floor(hz)));
95 };
96
97 if (ranges.size() == 1) {
98 const double max_hz = kSingleAddressMaxHz[eff - 1];
99 if (b.scan_rate_hz > max_hz)
100 out.push_back("one channel at resolution_index " + std::to_string(res) +
101 " streams at most " + rate_str(max_hz) + " scans/s (datasheet A-1-1)");
102 return out;
103 }
104 double scan_us = 0;
105 bool unsupported = false;
106 for (const auto& c : b.channels) {
107 const double d = interchannel_delay_us(c.range_v, eff);
108 if (d == 0) {
109 unsupported = true;
110 out.push_back(
111 "AIN" + std::to_string(c.ain) + ": range " +
112 (std::fabs(c.range_v - 0.01) < 1e-9 ? std::string("0.01") : std::string("0.1")) +
113 " V is not supported in a multi-channel stream" +
114 (std::fabs(c.range_v - 0.1) < 1e-9 ? std::string(" above resolution_index 2")
115 : std::string(" at any resolution")) +
116 " (datasheet A-1-1, Table A.1.7)");
117 }
118 }
119 if (unsupported)
120 return out;
121 for (double r : ranges)
122 if (interchannel_delay_us(r, eff) < 0)
123 return out;
124 for (double r : ranges)
125 scan_us += std::max(interchannel_delay_us(r, eff), b.settling_us);
126 const double max_hz = 1e6 / scan_us;
127 if (b.scan_rate_hz > max_hz)
128 out.push_back("scan_rate_hz " + rate_str(b.scan_rate_hz) +
129 " is beyond what this scan list "
130 "can do at resolution_index " +
131 std::to_string(res) + ": about " + rate_str(max_hz) + " scans/s at most (" +
132 std::to_string(ranges.size()) + " addresses, datasheet A-1-1 Table A.1.8)");
133 return out;
134}
135
136} // namespace
137
138std::vector<T7Board> parse_t7_boards(const toml::table& root, std::vector<std::string>& errors) {
139 std::vector<T7Board> out;
140 const auto* boards = root["boards"].as_table();
141 if (!boards)
142 return out;
143
144 std::map<uint8_t, std::string> slot_owner;
145 std::map<std::string, std::string> role_owner; // across every T7: the GUI keys by role
146 for (auto&& [key, node] : *boards) {
147 const auto* bt = node.as_table();
148 if (!bt || (*bt)["type"].value<std::string>().value_or("") != "T7")
149 continue;
150
151 T7Board b;
152 b.section = "boards." + std::string(key.str());
153 const std::string& sec = b.section;
154 auto err = [&](const std::string& m) {
155 errors.push_back("[" + sec + "] " + m);
156 };
157
158 b.ip = (*bt)["ip"].value<std::string>().value_or("");
159 b.mac = (*bt)["mac"].value<std::string>().value_or("");
160 b.serial = (*bt)["serial"].value<int64_t>().value_or(0);
161 b.enabled = (*bt)["enabled"].value<bool>().value_or(true);
162 b.board_id = static_cast<int>((*bt)["board_id"].value<int64_t>().value_or(-1));
163 b.scan_rate_hz = (*bt)["scan_rate_hz"].value<double>().value_or(b.scan_rate_hz);
164 b.resolution_index =
165 static_cast<int>((*bt)["resolution_index"].value<int64_t>().value_or(0));
166 b.settling_us = (*bt)["settling_us"].value<double>().value_or(0.0);
167 b.scans_per_read = static_cast<int>((*bt)["scans_per_read"].value<int64_t>().value_or(0));
168 b.cjc_offset_k = (*bt)["cjc_offset_k"].value<double>().value_or(b.cjc_offset_k);
169
170 // A disabled board is allowed to be half-filled in (that is how a new one gets added:
171 // declared, then its MAC learnt from the lease table), so only its shape is checked.
172 if (b.board_id < 1 || b.board_id > 255)
173 err("board_id must be an integer from 1 to 255");
174 if (b.enabled) {
175 if (b.ip.empty())
176 err("ip is required");
177 else if (!is_ipv4(b.ip))
178 err("ip \"" + b.ip + "\" is not an IPv4 address");
179 }
180 if (!(b.scan_rate_hz > 0.0))
181 err("scan_rate_hz must be positive");
182 if (b.resolution_index < 0 || b.resolution_index > kMaxStreamResolutionIndex)
183 err("resolution_index must be 0-" + std::to_string(kMaxStreamResolutionIndex) +
184 " (stream does not support the T7-Pro's 9-12)");
185 if (b.settling_us < 0.0)
186 err("settling_us cannot be negative");
187 if (b.scans_per_read < 0)
188 err("scans_per_read cannot be negative");
189
190 if (!std::isfinite(b.cjc_offset_k) || std::fabs(b.cjc_offset_k) > 20.0)
191 err("cjc_offset_k must be within +/-20 K");
192
193 if (const auto* arr = (*bt)["channels"].as_array()) {
194 std::map<int, int> ain_owner; // AIN -> channel index that uses it (incl. diff pair)
195 for (size_t i = 0; i < arr->size(); ++i) {
196 const int idx = static_cast<int>(i);
197 const auto* ct = (*arr)[i].as_table();
198 if (!ct) {
199 err("channels must be a list of tables ([[" + sec + ".channels]])");
200 continue;
201 }
202 T7Channel c;
203 const auto ain = (*ct)["ain"].value<int64_t>();
204 c.ain = ain ? static_cast<int>(*ain) : -1;
205 c.kind = (*ct)["kind"].value<std::string>().value_or("");
206 c.role = (*ct)["role"].value<std::string>().value_or("");
207 c.differential = (*ct)["differential"].value<bool>().value_or(false);
208 c.tc_type = (*ct)["tc_type"].value<std::string>().value_or("");
209 // A thermocouple is tens of millivolts: +/-0.1 V covers a type K to 1372 C,
210 // where the 0.01 V range would clip a K above ~245 C.
211 c.range_v = (*ct)["range"].value<double>().value_or(c.kind == "TC" ? 0.1 : 10.0);
212 c.full_scale = (*ct)["full_scale"].value<double>().value_or(0.0);
213 c.signal = (*ct)["signal"].value<std::string>().value_or("");
214 c.shunt_ohms = (*ct)["shunt_ohms"].value<double>().value_or(0.0);
216 (*ct)["sensitivity_mv_per_v"].value<double>().value_or(0.0);
217 c.excitation_v = (*ct)["excitation_v"].value<double>().value_or(0.0);
218 c.r0_ohms = (*ct)["r0_ohms"].value<double>().value_or(0.0);
219 c.ljtr_ohms = (*ct)["ljtr_ohms"].value<double>().value_or(0.0);
220 const auto vref = (*ct)["vref_v"].value<double>();
221 c.vref_v = vref.value_or(2.5);
222
223 const std::string p = at(sec, idx);
224 if (c.ain < 0 || c.ain > kT7MaxAin) {
225 errors.push_back(p + "ain must be 0-" + std::to_string(kT7MaxAin));
226 } else {
227 auto claim = [&](int n) {
228 auto [it, inserted] = ain_owner.emplace(n, idx);
229 if (!inserted)
230 errors.push_back(p + "AIN" + std::to_string(n) +
231 " is already used by channel " +
232 std::to_string(it->second + 1));
233 };
234 claim(c.ain);
235 if (c.differential) {
236 if (c.ain % 2 != 0)
237 errors.push_back(p +
238 "a differential input must be an even AIN "
239 "(it pairs with the odd one above it)");
240 else
241 claim(c.ain + 1);
242 }
243 }
244 if (!kKinds.count(c.kind))
245 errors.push_back(p + "kind \"" + c.kind +
246 "\" must be one of PT, TC, LC, RTD, VOLTAGE");
247 if (std::none_of(std::begin(kRanges), std::end(kRanges), [&](double r) {
248 return std::fabs(r - c.range_v) < 1e-9;
249 }))
250 errors.push_back(p + "range must be one of 10, 1, 0.1, 0.01 (volts)");
251 if (c.kind == "TC" && !kTcTypes.count(c.tc_type))
252 errors.push_back(p + "tc_type \"" + c.tc_type +
253 "\": only type K thermocouples are converted so far");
254 if (c.kind != "TC" && !c.tc_type.empty())
255 errors.push_back(p + "tc_type is only meaningful on kind = \"TC\"");
256
257 // The GUI lists a T7 channel by its role, like any PT/TC/LC.
258 if (c.kind != "VOLTAGE") {
259 if (c.role.empty())
260 errors.push_back(p + "a " + c.kind + " channel needs a role (its name)");
261 else if (!role_owner.emplace(c.role, sec).second)
262 errors.push_back(p + "role \"" + c.role + "\" is already used by [" +
263 role_owner[c.role] + "]");
264 }
265 auto need = [&](bool ok, const std::string& what) {
266 if (!ok)
267 errors.push_back(p + what);
268 };
269 if (c.kind == "PT") {
270 need(c.full_scale > 0, "a PT needs full_scale (psi at full output)");
271 need(kPtSignals.count(c.signal) > 0,
272 "a PT needs signal, one of 0-5V 0.5-4.5V 1-5V 0-10V 4-20mA");
273 if (c.signal == "4-20mA")
274 need(c.shunt_ohms > 0, "a 4-20mA PT needs shunt_ohms");
275 else
276 need(c.shunt_ohms == 0, "shunt_ohms is only meaningful on a 4-20mA PT");
277 }
278 if (c.kind == "LC") {
279 need(c.full_scale > 0, "an LC needs full_scale (kg at rated output)");
280 need(c.sensitivity_mv_per_v > 0, "an LC needs sensitivity_mv_per_v");
281 need(c.excitation_v > 0, "an LC needs excitation_v");
282 }
283 if (c.kind == "RTD") {
284 need(c.r0_ohms > 0,
285 "an RTD needs r0_ohms (100 for a Pt100, 1000 for a Pt1000)");
286 need(c.ljtr_ohms > 0,
287 "an RTD needs ljtr_ohms (the LJTick-Resistance's fixed resistor)");
288 need(c.vref_v > 0 && c.vref_v <= 10.0, "vref_v must be within (0, 10] V");
289 need(c.range_v >= c.vref_v, "an RTD's range must cover vref_v (use 10)");
290 } else {
291 need(c.r0_ohms == 0 && c.ljtr_ohms == 0 && !vref,
292 "r0_ohms, ljtr_ohms and vref_v are only meaningful on an RTD");
293 }
294 if (c.kind != "PT")
295 need(c.signal.empty() && c.shunt_ohms == 0,
296 "signal and shunt_ohms are only meaningful on a PT");
297 if (c.kind != "LC")
298 need(c.sensitivity_mv_per_v == 0 && c.excitation_v == 0,
299 "sensitivity_mv_per_v and excitation_v are only meaningful on an LC");
300 if (c.kind != "PT" && c.kind != "LC")
301 need(c.full_scale == 0, "full_scale is only meaningful on a PT or LC");
302 b.channels.push_back(std::move(c));
303 }
304 } else if ((*bt).contains("channels")) {
305 err("channels must be a list of tables ([[" + sec + ".channels]])");
306 }
307
308 if (b.enabled) {
309 if (b.channels.empty())
310 err("an enabled T7 needs at least one [[" + sec + ".channels]] entry");
311 const double aggregate = b.scan_rate_hz * static_cast<double>(b.channels.size());
312 if (aggregate > kMaxAggregateSamplesPerS)
313 err("scan_rate_hz x channels = " + std::to_string(static_cast<int>(aggregate)) +
314 " samples/s, beyond the T7's 100000");
315 for (const auto& m : stream_limit_issues(b))
316 err(m);
317 if (b.board_id >= 1 && b.board_id <= 255) {
318 auto [it, inserted] = slot_owner.emplace(b.slot(), sec);
319 if (!inserted)
320 err("board_id " + std::to_string(b.board_id) + " shares Elodin slot " +
321 std::to_string(b.slot()) + " with [" + it->second +
322 "]; give T7s board_ids that differ mod 10");
323 if (b.slot() > 8)
324 err("board_id " + std::to_string(b.board_id) + " maps to slot " +
325 std::to_string(b.slot()) + "; only slots 1-8 fit the table id");
326 }
327 }
328 out.push_back(std::move(b));
329 }
330 return out;
331}
332
333} // namespace detail
334} // namespace config
335} // namespace fsw
static volatile uint32_t errors
Definition board_uart9.c:13
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
json_pointer res
Definition json.hpp:13957
T & at(const key_type &key)
Definition json.hpp:19110
detail namespace with internal helper functions
Definition json.hpp:249
double interchannel_delay_us(double range_v, int res)
Definition T7Config.cpp:52
std::vector< std::string > stream_limit_issues(const T7Board &b)
Definition T7Config.cpp:79
std::vector< T7Board > parse_t7_boards(const toml::table &root, std::vector< std::string > &errors)
Definition T7Config.cpp:138
constexpr int kT7MaxAin
Highest AIN number on a T7 without an expansion mux (AIN0-AIN13).
Definition T7Config.hpp:99
Motorized ball valve position: raw ADC counts -> % open.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
static void aggregate(self_test_t *t)
Definition self_test.c:46
int scans_per_read
Scans per LJM_eStreamRead; 0 = scan_rate/20 (a 50 ms block).
Definition T7Config.hpp:78
int effective_scans_per_read() const
scans_per_read with the default applied; always >= 1.
Definition T7Config.cpp:14
One streamed analog input.
Definition T7Config.hpp:37
double ljtr_ohms
RTD: the tick's fixed resistor, 1k/10k/100k/1M by model.
Definition T7Config.hpp:57
double r0_ohms
RTD: resistance at 0 C, 100 (Pt100) or 1000 (Pt1000).
Definition T7Config.hpp:56
int ain
AIN number on the device, 0-13.
Definition T7Config.hpp:38
double vref_v
RTD: the tick's reference.
Definition T7Config.hpp:59
std::string tc_type
Thermocouple type letter (kind == "TC" only): "K".
Definition T7Config.hpp:43
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 range_v
+/- input range in volts: 10, 1, 0.1 or 0.01.
Definition T7Config.hpp:41
std::string role
Sensor name, e.g. "LOX Tank" — what the GUI calls it.
Definition T7Config.hpp:40
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
bool differential
true: AIN(ain) - AIN(ain+1); ain must be even.
Definition T7Config.hpp:42
static boot_report_t r