STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
T7Stream.cpp
Go to the documentation of this file.
1#include "t7/T7Stream.hpp"
2
3#include <algorithm>
4#include <chrono>
5#include <cmath>
6#include <cstdio>
7#include <fstream>
8#include <limits>
9#include <sstream>
10#include <thread>
11
12#include "t7/T7Convert.hpp"
13
14namespace fsw {
15namespace t7 {
16
21
22std::array<uint8_t, 2> table_id(uint8_t slot, uint8_t ain) {
23 return {kTypeHi, static_cast<uint8_t>((slot - 1) * 0x20 + ain)};
24}
25
26std::string entity_name(uint8_t slot, uint8_t ain) {
27 return "T7" + std::to_string(slot) + ".AIN" + std::to_string(ain);
28}
29
30std::array<uint8_t, 2> calibrated_table_id(uint8_t slot, uint8_t ain) {
31 return {kTypeHi, static_cast<uint8_t>((slot - 1) * 0x20 + 0x10 + ain)};
32}
33
34std::string calibrated_entity_name(uint8_t slot, uint8_t ain) {
35 return "T7" + std::to_string(slot) + "_Cal.AIN" + std::to_string(ain);
36}
37
40 auto set = [&](const std::string& name, double value) {
41 p.setup_names.push_back(name);
42 p.setup_values.push_back(value);
43 };
44 // Stream timing: start immediately on the internal clock. Stream takes its resolution and
45 // settling from these two registers, NOT from AIN#_RESOLUTION_INDEX / AIN#_SETTLING_US,
46 // which only apply to command-response reads.
47 set("STREAM_TRIGGER_INDEX", 0);
48 set("STREAM_CLOCK_SOURCE", 0);
49 set("STREAM_RESOLUTION_INDEX", board.resolution_index);
50 set("STREAM_SETTLING_US", board.settling_us);
51 for (const auto& c : board.channels) {
52 const std::string ain = "AIN" + std::to_string(c.ain);
53 set(ain + "_RANGE", c.range_v);
54 set(ain + "_NEGATIVE_CH", c.negative_ch());
55 // An extended feature left on by a previous user (Kipling, an old script) changes what a
56 // command-response read of this input means; clear it so the device state is ours.
57 set(ain + "_EF_INDEX", 0);
58 p.scan_names.push_back(ain);
59 p.ains.push_back(static_cast<uint8_t>(c.ain));
60 p.channels.emplace_back(c);
61 }
62 // A thermocouple reads the difference between its hot end and the terminals, so the terminal
63 // temperature rides along in every scan: AIN14, the internal sensor. Last, so the first scan
64 // entry stays a configured AIN (auto-recovery marks gaps in the first channel).
65 const bool has_tc =
66 std::any_of(board.channels.begin(), board.channels.end(), [](const config::T7Channel& c) {
67 return c.kind == "TC";
68 });
69 if (has_tc) {
70 const std::string ain = "AIN" + std::to_string(kCjcAin);
71 set(ain + "_RANGE", 10.0);
72 set(ain + "_NEGATIVE_CH", 199);
73 set(ain + "_EF_INDEX", 0);
74 p.cjc_index = static_cast<int>(p.scan_names.size());
75 p.scan_names.push_back(ain);
76 p.ains.push_back(kCjcAin);
77 p.channels.emplace_back(std::nullopt);
78 }
79 return p;
80}
81
82T7Session::T7Session(config::T7Board board, std::unique_ptr<T7Device> device,
83 const time::TimeSyncConfig& time_sync, Clock clock)
84 : board_(std::move(board)),
85 device_(std::move(device)),
86 clock_sync_(time_sync),
87 clock_(std::move(clock)),
89 cjc_device_offset_k_(std::numeric_limits<double>::quiet_NaN()) {
90}
91
93 device_->open(board_.ip);
94 open_ = true;
95 // Always read it: the bridge logs it, so bring-up can tell which unit answered at that IP.
96 serial_ = device_->serial_number();
97 if (board_.serial != 0 && serial_ != board_.serial)
98 throw T7Error("[" + board_.section + "] the T7 at " + board_.ip + " has serial " +
99 std::to_string(serial_) + ", config says " + std::to_string(board_.serial));
100 // A T7 keeps streaming after the process that started the stream dies (a bridge killed
101 // mid-run, a server torn down, Kipling), and refuses configuration while it does
102 // (STREAM_IS_ACTIVE). So whoever opens it stops whatever is running first. eStreamStop from a
103 // new handle reaches the device (synnaxlabs/synnax#4142, T7 firmware 1.0292, LJM 1.23.2);
104 // "not running" is the normal answer. Anything else is a real failure and is not swallowed.
105 stopped_leftover_ = false;
106 const std::string tag = "[" + board_.section + "] ";
107 try {
108 device_->stream_stop();
109 stopped_leftover_ = true;
110 } catch (const T7Error& e) {
111 if (!is_stream_not_running(e.code()))
112 throw T7Error(tag + "could not stop the stream already running on the T7 at " +
113 board_.ip + ": " + e.what(),
114 e.code());
115 }
116 try {
117 device_->write_names(plan_.setup_names, plan_.setup_values);
118 } catch (const T7Error& e) {
120 throw;
121 // Still streaming although the stop found nothing to stop: an LJM that answered for this
122 // handle alone. Stop it at the device (STREAM_ENABLE = 0) and configure again.
123 device_->write_names({"STREAM_ENABLE"}, {0.0});
124 stopped_leftover_ = true;
125 device_->write_names(plan_.setup_names, plan_.setup_values);
126 }
127 if (plan_.cjc_index >= 0) {
128 // The device's own calibrated temperature beside the raw AIN14 it comes from pins this
129 // unit's offset; only the nominal slope is then trusted, over the few K it drifts.
130 // Command-response, so it has to happen before the stream takes the analog inputs.
131 const auto v = device_->read_names({"TEMPERATURE_DEVICE_K", "AIN14"});
132 if (v.size() != 2 || !(v[0] > 200.0 && v[0] < 400.0))
133 throw T7Error("[" + board_.section + "] implausible TEMPERATURE_DEVICE_K " +
134 (v.empty() ? std::string("(none)") : std::to_string(v[0])));
136 }
137 const int spr = board_.effective_scans_per_read();
138 try {
139 actual_rate_hz_ = device_->stream_start(plan_.scan_names, spr, board_.scan_rate_hz);
140 } catch (const T7Error& e) {
142 throw;
143 // It was stopped a moment ago: something else is streaming this T7 right now.
144 throw T7Error(tag + "another client started a stream on the T7 at " + board_.ip +
145 " while this one configured it (a second t7_bridge or server? "
146 "Kipling?): " +
147 e.what(),
148 e.code());
149 }
150 if (!(actual_rate_hz_ > 0.0))
151 throw T7Error("[" + board_.section + "] device reported a scan rate of " +
152 std::to_string(actual_rate_hz_));
153 buffer_.assign(static_cast<size_t>(spr) * plan_.ains.size(), 0.0);
154 next_scan_ = 0;
155 return actual_rate_hz_;
156}
157
159 Block b;
160 b.backlog = device_->stream_read(buffer_);
161 const uint64_t arrival_ns = clock_();
162 const size_t n = plan_.ains.size();
163 const uint64_t scans = buffer_.size() / n;
164 b.scans = scans;
165 if (scans == 0)
166 return b;
167
168 // Device time of scan k, in ns since the first scan.
169 const double period_ns = 1e9 / actual_rate_hz_;
170 auto device_ns = [&](uint64_t k) {
171 return static_cast<uint64_t>(std::llround(static_cast<double>(k) * period_ns));
172 };
173 auto device_ms = [&](uint64_t k) {
174 return static_cast<uint32_t>(device_ns(k) / 1000000ULL);
175 };
176
177 const uint64_t first = next_scan_;
178 const uint64_t last = first + scans - 1;
179 // Anchor the newest scan through the clock sync; it works in whole board ms, so add back the
180 // sub-millisecond part, but never past the arrival time (a scan cannot postdate its delivery).
181 const uint64_t last_ns = device_ns(last);
182 const uint64_t sub_ms = last_ns % 1000000ULL;
183 uint64_t anchor =
184 clock_sync_.stamp_packet(board_.section, arrival_ns, {device_ms(last)}).front() + sub_ms;
185 anchor = std::min(anchor, arrival_ns);
186
187 b.samples.reserve(buffer_.size());
188 for (uint64_t s = 0; s < scans; ++s) {
189 const uint64_t k = first + s;
190 const uint64_t back = last_ns - device_ns(k);
191 const uint64_t ts = anchor > back ? anchor - back : 0;
192 // This scan's cold junction, if the board has thermocouples. Only the bridge can work it
193 // out (the offset is taken from the device at stream start), so it is published as its
194 // own table for calibration_service, which turns TC volts into temperature.
195 if (plan_.cjc_index >= 0) {
196 const double v14 = buffer_[s * n + static_cast<size_t>(plan_.cjc_index)];
197 if (v14 != kDummyValue)
198 b.cold_junction.push_back(
199 {static_cast<float>(cjc_temp_c(v14, cjc_device_offset_k_, board_.cjc_offset_k)),
200 ts, device_ms(k)});
201 }
202 for (size_t c = 0; c < n; ++c) {
203 const double v = buffer_[s * n + c];
204 if (v == kDummyValue) {
205 ++b.skipped_samples;
206 continue;
207 }
208 b.samples.push_back(Sample{plan_.ains[c], static_cast<float>(v), ts, device_ms(k)});
209 }
210 }
211 next_scan_ = last + 1;
212 return b;
213}
214
215void T7Session::stop() noexcept {
216 if (!open_)
217 return;
218 try {
219 device_->stream_stop();
220 } catch (...) {
221 }
222 try {
223 device_->close();
224 } catch (...) {
225 }
226 open_ = false;
227}
228
229// ── FakeT7Unit ───────────────────────────────────────────────────────────────────────────────
230
231FakeT7Unit::FakeT7Unit(std::string state_path) : path_(std::move(state_path)) {
232 load();
233}
234
235double FakeT7Unit::reg(const std::string& name, double fallback) const {
236 for (const auto& r : registers)
237 if (r.first == name)
238 return r.second;
239 return fallback;
240}
241
242void FakeT7Unit::set_reg(const std::string& name, double value) {
243 for (auto& r : registers)
244 if (r.first == name) {
245 r.second = value;
246 return;
247 }
248 registers.emplace_back(name, value);
249}
250
252 streaming = false;
253 scan_names.clear();
254 registers.clear();
255 save();
256}
257
258// One line per fact: "streaming 1", "stream_id 3", "starts 2", "stops 1", "scan AIN1 AIN2",
259// "reg AIN1_RANGE 10".
261 if (path_.empty())
262 return;
263 std::ifstream in(path_);
264 if (!in)
265 return;
266 streaming = false;
267 stream_id = starts = stops = 0;
268 scan_names.clear();
269 registers.clear();
270 std::string line;
271 while (std::getline(in, line)) {
272 std::istringstream ls(line);
273 std::string key;
274 ls >> key;
275 if (key == "streaming")
276 ls >> streaming;
277 else if (key == "stream_id")
278 ls >> stream_id;
279 else if (key == "starts")
280 ls >> starts;
281 else if (key == "stops")
282 ls >> stops;
283 else if (key == "scan")
284 for (std::string n; ls >> n;)
285 scan_names.push_back(n);
286 else if (key == "reg") {
287 std::string name;
288 double v = 0;
289 if (ls >> name >> v)
290 registers.emplace_back(name, v);
291 }
292 }
293}
294
295void FakeT7Unit::save() const {
296 if (path_.empty())
297 return;
298 const std::string tmp = path_ + ".tmp";
299 {
300 std::ofstream out(tmp, std::ios::trunc);
301 out << "streaming " << (streaming ? 1 : 0) << "\n"
302 << "stream_id " << stream_id << "\n"
303 << "starts " << starts << "\n"
304 << "stops " << stops << "\n"
305 << "scan";
306 for (const auto& n : scan_names)
307 out << " " << n;
308 out << "\n";
309 out.precision(17);
310 for (const auto& r : registers)
311 out << "reg " << r.first << " " << r.second << "\n";
312 }
313 std::rename(tmp.c_str(), path_.c_str()); // atomic: another bridge never reads half a file
314}
315
316// ── FakeT7Device ─────────────────────────────────────────────────────────────────────────────
317
319 if (!opt_.unit)
320 opt_.unit = std::make_shared<FakeT7Unit>();
321}
322
323void FakeT7Device::open(const std::string& ip) {
324 opened_ip = ip;
325 closed = false;
326 opt_.unit->load();
327}
328
330 return opt_.serial;
331}
332
333void FakeT7Device::write_names(const std::vector<std::string>& names,
334 const std::vector<double>& values) {
335 FakeT7Unit& u = *opt_.unit;
336 u.load();
337 for (size_t i = 0; i < names.size(); ++i) {
338 // STREAM_ENABLE is the one register a running stream accepts: 0 stops it, whoever
339 // started it.
340 if (names[i] == "STREAM_ENABLE" && values.at(i) == 0.0) {
341 writes.emplace_back(names[i], 0.0);
342 if (u.streaming) {
343 u.streaming = false;
344 ++u.stops;
345 }
346 continue;
347 }
348 if (u.streaming) {
349 u.save();
350 throw T7Error("fake T7: STREAM_IS_ACTIVE writing " + names[i],
352 }
353 writes.emplace_back(names[i], values.at(i));
354 u.set_reg(names[i], values.at(i));
355 }
357 u.save();
358}
359
360std::vector<double> FakeT7Device::read_names(const std::vector<std::string>& names) {
361 opt_.unit->load();
362 if (opt_.unit->streaming)
363 throw T7Error("fake T7: STREAM_IS_ACTIVE (command-response AIN read while streaming)",
365 std::vector<double> out;
366 for (const auto& n : names) {
367 reads.push_back(n);
368 if (n == "TEMPERATURE_DEVICE_K")
369 out.push_back(opt_.device_temp_k);
370 else if (n.rfind("AIN", 0) == 0)
371 out.push_back(volts(std::stoi(n.substr(3)), 0.0));
372 else
373 throw T7Error("fake T7: no register " + n, 1294);
374 }
375 return out;
376}
377
378double FakeT7Device::volts(int ain, double t) const {
379 if (ain == kCjcAin) // the internal sensor at device_temp_k, on LabJack's nominal line
380 return (opt_.device_temp_k - kAin14NominalOffsetK) / kAin14SlopeKPerV;
381 if (opt_.signal)
382 return opt_.signal(ain, t);
383 return 0.1 * ain + 0.05 * std::sin(2.0 * M_PI * 0.2 * t);
384}
385
386double FakeT7Device::stream_start(const std::vector<std::string>& names, int scans_per_read,
387 double scan_rate_hz) {
388 FakeT7Unit& u = *opt_.unit;
389 u.load();
390 if (u.streaming)
391 throw T7Error("fake T7: STREAM_IS_ACTIVE (stream already running)",
393 scan_names = names;
394 scans_per_read_ = scans_per_read;
395 rate_ = opt_.rate_cap_hz > 0.0 ? std::min(scan_rate_hz, opt_.rate_cap_hz) : scan_rate_hz;
396 scan_ = 0;
397 u.streaming = true;
398 u.stream_id += 1;
399 u.starts += 1;
400 u.scan_names = names;
401 my_stream_ = u.stream_id;
402 streaming = true;
403 u.save();
404 return rate_;
405}
406
408 if (my_stream_ == 0)
409 throw T7Error("fake T7: read with no stream running", ljm_code::kStreamNotInitialized);
410 opt_.unit->load();
411 if (!owns_stream()) {
412 // The device stopped this stream (another client took it over): LJM just stops
413 // receiving data.
414 streaming = false;
415 throw T7Error("fake T7: no stream data (the stream was stopped by another client)",
417 }
418 if (opt_.fail_read_after >= 0 && reads_ >= opt_.fail_read_after)
419 throw T7Error("fake T7: link lost", ljm_code::kSynchronizationTimeout);
420 ++reads_;
421 if (opt_.realtime)
422 std::this_thread::sleep_for(std::chrono::duration<double>(scans_per_read_ / rate_));
423 const size_t n = scan_names.size();
424 data.resize(static_cast<size_t>(scans_per_read_) * n);
425 for (int s = 0; s < scans_per_read_; ++s, ++scan_) {
426 const bool lost = opt_.dummy_scan_every > 0 && (scan_ + 1) % opt_.dummy_scan_every == 0;
427 const double t = static_cast<double>(scan_) / rate_;
428 for (size_t c = 0; c < n; ++c) {
429 const int ain = std::stoi(scan_names[c].substr(3));
430 data[s * n + c] = lost ? kDummyValue : volts(ain, t);
431 }
432 }
433 return {};
434}
435
437 FakeT7Unit& u = *opt_.unit;
438 u.load();
439 ++stop_calls;
440 if (opt_.fail_stop_code != 0)
441 throw T7Error("fake T7: stop failed", opt_.fail_stop_code);
442 const bool mine = owns_stream();
443 my_stream_ = 0;
444 streaming = false;
445 if (opt_.stop_only_own_stream && !mine)
446 throw T7Error("fake T7: LJME_STREAM_NOT_INITIALIZED (this handle started no stream)",
448 if (!u.streaming)
449 throw T7Error("fake T7: STREAM_NOT_RUNNING", ljm_code::kStreamNotRunning);
450 u.streaming = false;
451 ++u.stops;
452 u.save();
453}
454
456 // Closing a connection does not stop the device's stream: only a stop does. A bridge that
457 // dies without one leaves the T7 streaming to nobody.
458 streaming = false;
459 my_stream_ = 0;
460 closed = true;
461}
462
463} // namespace t7
464} // namespace fsw
double stream_start(const std::vector< std::string > &scan_names, int scans_per_read, double scan_rate_hz) override
Starts the stream; returns the scan rate the device will actually run at.
Definition T7Stream.cpp:386
std::vector< std::string > scan_names
Definition T7Stream.hpp:288
Backlog stream_read(std::vector< double > &data) override
Fills data with scans_per_read scans, channels interleaved.
Definition T7Stream.cpp:407
void write_names(const std::vector< std::string > &names, const std::vector< double > &values) override
Definition T7Stream.cpp:333
void stream_stop() override
Definition T7Stream.cpp:436
void open(const std::string &ip) override
Definition T7Stream.cpp:323
uint64_t my_stream_
unit stream_id this handle started; 0 = none
Definition T7Stream.hpp:307
int64_t serial_number() override
Definition T7Stream.cpp:329
bool owns_stream() const
Definition T7Stream.hpp:308
std::vector< std::string > reads
every name read_names() was asked for
Definition T7Stream.hpp:289
std::string opened_ip
Definition T7Stream.hpp:286
std::vector< double > read_names(const std::vector< std::string > &names) override
Command-response reads; only valid while no stream is running.
Definition T7Stream.cpp:360
bool streaming
this handle's own stream is running
Definition T7Stream.hpp:290
void close() override
Definition T7Stream.cpp:455
std::vector< std::pair< std::string, double > > writes
Definition T7Stream.hpp:287
double volts(int ain, double t) const
Volts on ain at t: Options::signal, else the default described above.
Definition T7Stream.cpp:378
The stand-in T7 itself, as distinct from a connection to it.
Definition T7Stream.hpp:213
std::vector< std::pair< std::string, double > > registers
last value written per name
Definition T7Stream.hpp:220
std::string path_
Definition T7Stream.hpp:232
void load()
from state_path; no-op without one or when the file is absent
Definition T7Stream.cpp:260
void power_cycle()
Power off and on: no stream, registers back to defaults.
Definition T7Stream.cpp:251
void save() const
Definition T7Stream.cpp:295
std::vector< std::string > scan_names
the running (or last) stream's scan list
Definition T7Stream.hpp:219
void set_reg(const std::string &name, double value)
Definition T7Stream.cpp:242
double reg(const std::string &name, double fallback=0.0) const
Definition T7Stream.cpp:235
FakeT7Unit(std::string state_path="")
Definition T7Stream.cpp:231
uint64_t stream_id
bumped by every stream start; a handle reads only its own stream
Definition T7Stream.hpp:218
A device failure.
Definition T7Stream.hpp:62
int code() const
Definition T7Stream.hpp:66
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
std::vector< double > buffer_
Definition T7Stream.hpp:195
const config::T7Board & board() const
Definition T7Stream.hpp:162
time::BoardClockSync clock_sync_
Definition T7Stream.hpp:188
T7Session(config::T7Board board, std::unique_ptr< T7Device > device, const time::TimeSyncConfig &time_sync, Clock clock)
Definition T7Stream.cpp:82
std::function< uint64_t()> Clock
epoch ns
Definition T7Stream.hpp:146
config::T7Board board_
Definition T7Stream.hpp:186
double cjc_device_offset_k_
Definition T7Stream.hpp:197
std::unique_ptr< T7Device > device_
Definition T7Stream.hpp:187
void stop() noexcept
Stops the stream and closes the device; never throws.
Definition T7Stream.cpp:215
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
return reference_tokens back()
return o
Definition json.hpp:13897
constexpr int kStreamNotInitialized
LJME_STREAM_NOT_INITIALIZED (LJM, this handle).
Definition T7Stream.hpp:51
constexpr int kDeviceStreamNotRunning
device: "Stream was already disabled"
Definition T7Stream.hpp:55
constexpr int kStreamNotRunning
LJME_STREAM_NOT_RUNNING.
Definition T7Stream.hpp:52
constexpr int kSynchronizationTimeout
LJME_SYNCHRONIZATION_TIMEOUT (no stream data).
Definition T7Stream.hpp:53
constexpr int kStreamIsActive
device: "can not be performed while stream is running"
Definition T7Stream.hpp:54
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
constexpr double kAin14SlopeKPerV
The T7's internal temperature sensor (an LM94021 on AIN14) reads "Device temperature K =volts * -92....
Definition T7Convert.hpp:25
StreamPlan plan_stream(const config::T7Board &board)
Definition T7Stream.cpp:38
bool is_stream_not_running(int code)
True for every way a stop can answer "there was no stream to stop".
Definition T7Stream.cpp:17
std::string entity_name(uint8_t slot, uint8_t ain)
Elodin entity name for one AIN, e.g.
Definition T7Stream.cpp:26
constexpr uint8_t kTypeHi
Elodin table-id high byte for T7 analog inputs (0x20-0x25 are the ESP32 sensor types).
Definition T7Stream.hpp:27
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
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
constexpr uint8_t kCjcAin
The T7's internal temperature sensor, streamed for thermocouple cold-junction compensation whenever a...
Definition T7Stream.hpp:47
std::string calibrated_entity_name(uint8_t slot, uint8_t ain)
Calibrated entity name, e.g.
Definition T7Stream.cpp:34
constexpr double kDummyValue
What LJM writes into a scan lost to stream auto-recovery (LJM_DUMMY_VALUE).
Definition T7Stream.hpp:30
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
constexpr double kAin14NominalOffsetK
Nominal AIN14 offset (K), datasheet 18.0.
Definition T7Convert.hpp:27
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
STL namespace.
One streamed analog input.
Definition T7Config.hpp:37
A decoded LJM_eStreamRead block.
Definition T7Stream.hpp:125
One published value.
Definition T7Stream.hpp:110
What to write before streaming and what to stream, derived from a board's config.
Definition T7Stream.hpp:98
static boot_report_t r
std::string board(const std::string &name, const std::string &id, bool enabled=true)