19 return std::max(1,
static_cast<int>(
scan_rate_hz / 20.0));
27constexpr double kRanges[] = {10.0, 1.0, 0.1, 0.01};
28const std::set<std::string>
kKinds = {
"PT",
"TC",
"LC",
"RTD",
"VOLTAGE"};
32const std::set<std::string>
kPtSignals = {
"0-5V",
"0.5-4.5V",
"1-5V",
"0-10V",
"4-20mA"};
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)
59 if (std::fabs(range_v - 1.0) < 1e-9)
61 if (std::fabs(range_v - 0.1) < 1e-9)
63 if (std::fabs(range_v - 0.01) < 1e-9)
70 return inet_pton(AF_INET, s.c_str(), &
a) == 1;
73std::string
at(
const std::string& section,
int idx) {
74 return "[" + section +
"] channel " + std::to_string(idx + 1) +
": ";
80 std::vector<std::string> out;
81 if (
b.channels.empty() || !(
b.scan_rate_hz > 0))
83 const bool has_tc = std::any_of(
b.channels.begin(),
b.channels.end(), [](
const T7Channel& c) {
84 return c.kind ==
"TC";
86 std::vector<double> ranges;
87 for (
const auto& c :
b.channels)
90 ranges.push_back(10.0);
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)));
97 if (ranges.size() == 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)");
105 bool unsupported =
false;
106 for (
const auto& c :
b.channels) {
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)");
121 for (
double r : ranges)
124 for (
double r : ranges)
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)");
139 std::vector<T7Board> out;
140 const auto* boards = root[
"boards"].as_table();
144 std::map<uint8_t, std::string> slot_owner;
145 std::map<std::string, std::string> role_owner;
146 for (
auto&& [key, node] : *boards) {
147 const auto* bt = node.as_table();
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);
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);
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);
172 if (
b.board_id < 1 ||
b.board_id > 255)
173 err(
"board_id must be an integer from 1 to 255");
176 err(
"ip is required");
177 else if (!is_ipv4(
b.ip))
178 err(
"ip \"" +
b.ip +
"\" is not an IPv4 address");
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");
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");
193 if (
const auto* arr = (*bt)[
"channels"].as_array()) {
194 std::map<int, int> ain_owner;
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();
199 err(
"channels must be a list of tables ([[" + sec +
".channels]])");
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(
"");
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);
223 const std::string
p =
at(sec, idx);
227 auto claim = [&](
int n) {
228 auto [it, inserted] = ain_owner.emplace(
n, idx);
230 errors.push_back(
p +
"AIN" + std::to_string(
n) +
231 " is already used by channel " +
232 std::to_string(it->second + 1));
238 "a differential input must be an even AIN "
239 "(it pairs with the odd one above it)");
244 if (!kKinds.count(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;
250 errors.push_back(
p +
"range must be one of 10, 1, 0.1, 0.01 (volts)");
253 "\": only type K thermocouples are converted so far");
255 errors.push_back(
p +
"tc_type is only meaningful on kind = \"TC\"");
258 if (c.
kind !=
"VOLTAGE") {
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] +
"]");
265 auto need = [&](
bool ok,
const std::string& what) {
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");
274 need(c.
shunt_ohms > 0,
"a 4-20mA PT needs shunt_ohms");
276 need(c.
shunt_ohms == 0,
"shunt_ohms is only meaningful on a 4-20mA PT");
278 if (c.
kind ==
"LC") {
279 need(c.
full_scale > 0,
"an LC needs full_scale (kg at rated output)");
283 if (c.
kind ==
"RTD") {
285 "an RTD needs r0_ohms (100 for a Pt100, 1000 for a Pt1000)");
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)");
292 "r0_ohms, ljtr_ohms and vref_v are only meaningful on an RTD");
296 "signal and shunt_ohms are only meaningful on a PT");
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));
304 }
else if ((*bt).contains(
"channels")) {
305 err(
"channels must be a list of tables ([[" + sec +
".channels]])");
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))
317 if (
b.board_id >= 1 &&
b.board_id <= 255) {
318 auto [it, inserted] = slot_owner.emplace(
b.slot(), sec);
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");
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");
328 out.push_back(std::move(
b));
static volatile uint32_t errors
*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
T & at(const key_type &key)
detail namespace with internal helper functions
constexpr int kMaxStreamResolutionIndex
bool is_ipv4(const std::string &s)
constexpr double kMaxAggregateSamplesPerS
const std::set< std::string > kPtSignals
double interchannel_delay_us(double range_v, int res)
const std::set< std::string > kTcTypes
const std::set< std::string > kKinds
constexpr double kRanges[]
constexpr double kSingleAddressMaxHz[8]
std::vector< std::string > stream_limit_issues(const T7Board &b)
std::vector< T7Board > parse_t7_boards(const toml::table &root, std::vector< std::string > &errors)
constexpr int kT7MaxAin
Highest AIN number on a T7 without an expansion mux (AIN0-AIN13).
Motorized ball valve position: raw ADC counts -> % open.
*brief user defined string literal for JSON values *sa std::size_t n
static void aggregate(self_test_t *t)
int scans_per_read
Scans per LJM_eStreamRead; 0 = scan_rate/20 (a 50 ms block).
int effective_scans_per_read() const
scans_per_read with the default applied; always >= 1.
One streamed analog input.
double ljtr_ohms
RTD: the tick's fixed resistor, 1k/10k/100k/1M by model.
double r0_ohms
RTD: resistance at 0 C, 100 (Pt100) or 1000 (Pt1000).
int ain
AIN number on the device, 0-13.
double vref_v
RTD: the tick's reference.
std::string tc_type
Thermocouple type letter (kind == "TC" only): "K".
std::string kind
"PT" | "TC" | "LC" | "RTD" | "VOLTAGE" — what is wired to it.
double shunt_ohms
PT "4-20mA": the resistor the loop current crosses.
std::string signal
PT output: "0-5V" "0.5-4.5V" "1-5V" "0-10V" "4-20mA".
double full_scale
PT: psi at full output. LC: kg at rated output.
double range_v
+/- input range in volts: 10, 1, 0.1 or 0.01.
std::string role
Sensor name, e.g. "LOX Tank" — what the GUI calls it.
double excitation_v
LC: bridge excitation voltage.
double sensitivity_mv_per_v
LC: rated output, mV per V of excitation.
bool differential
true: AIN(ain) - AIN(ain+1); ain must be even.