STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
T7Stream.hpp
Go to the documentation of this file.
1#pragma once
2
3// LabJack T7 streaming, minus the LabJack library.
4//
5// Everything t7_bridge does that can be checked without hardware lives here: what gets written to
6// the device before a stream, how a block from LJM_eStreamRead is unpacked, how its scans are
7// timestamped, and which Elodin table each AIN lands in. The device is reached only through the
8// T7Device interface, which has two implementations: LjmT7Device (services/t7_bridge, needs
9// libLabJackM) and FakeT7Device (here, for tests and `t7_bridge --fake`).
10
11#include <array>
12#include <cstdint>
13#include <functional>
14#include <memory>
15#include <optional>
16#include <stdexcept>
17#include <string>
18#include <vector>
19
20#include "config/T7Config.hpp"
22
23namespace fsw {
24namespace t7 {
25
26/** Elodin table-id high byte for T7 analog inputs (0x20-0x25 are the ESP32 sensor types). */
27constexpr uint8_t kTypeHi = 0x26;
28
29/** What LJM writes into a scan lost to stream auto-recovery (LJM_DUMMY_VALUE). */
30constexpr double kDummyValue = -9999.0;
31
32/** Table id for one AIN: {0x26, (slot-1)*0x20 + ain}, the same layout as the ESP32 boards. The
33 * calibrated counterpart, when there is one, takes +0x10 as theirs does. */
34std::array<uint8_t, 2> table_id(uint8_t slot, uint8_t ain);
35
36/** Elodin entity name for one AIN, e.g. "T71.AIN3". */
37std::string entity_name(uint8_t slot, uint8_t ain);
38
39/** Calibrated table for one AIN: table_id + 0x10, e.g. {0x26, 0x13} for slot 1 AIN3. */
40std::array<uint8_t, 2> calibrated_table_id(uint8_t slot, uint8_t ain);
41
42/** Calibrated entity name, e.g. "T71_Cal.AIN3" (ESP32: "PT1_Cal.CH3"). */
43std::string calibrated_entity_name(uint8_t slot, uint8_t ain);
44
45/** The T7's internal temperature sensor, streamed for thermocouple cold-junction compensation
46 * whenever a board has a TC channel. Published raw like any AIN (no calibrated table). */
47constexpr uint8_t kCjcAin = 14;
48
49/** LJM and device error codes the session acts on (labjack/ljm_constants, LJM errorcodes). */
50namespace ljm_code {
51constexpr int kStreamNotInitialized = 1225; ///< LJME_STREAM_NOT_INITIALIZED (LJM, this handle)
52constexpr int kStreamNotRunning = 1303; ///< LJME_STREAM_NOT_RUNNING
53constexpr int kSynchronizationTimeout = 1306; ///< LJME_SYNCHRONIZATION_TIMEOUT (no stream data)
54constexpr int kStreamIsActive = 2605; ///< device: "can not be performed while stream is running"
55constexpr int kDeviceStreamNotRunning = 2620; ///< device: "Stream was already disabled"
56} // namespace ljm_code
57
58/** True for every way a stop can answer "there was no stream to stop". */
59bool is_stream_not_running(int code);
60
61/** A device failure. `code` is the LJM error code where there is one, else 0. */
62class T7Error : public std::runtime_error {
63public:
64 T7Error(const std::string& what, int code = 0) : std::runtime_error(what), code_(code) {
65 }
66 int code() const {
67 return code_;
68 }
69
70private:
71 int code_;
72};
73
74/** The device seam. Every call may throw T7Error. */
75class T7Device {
76public:
77 virtual ~T7Device() = default;
78 virtual void open(const std::string& ip) = 0;
79 virtual int64_t serial_number() = 0;
80 virtual void write_names(const std::vector<std::string>& names,
81 const std::vector<double>& values) = 0;
82 /** Command-response reads; only valid while no stream is running. */
83 virtual std::vector<double> read_names(const std::vector<std::string>& names) = 0;
84 /** Starts the stream; returns the scan rate the device will actually run at. */
85 virtual double stream_start(const std::vector<std::string>& scan_names, int scans_per_read,
86 double scan_rate_hz) = 0;
87 struct Backlog {
88 int device_scans = 0; ///< scans still in the T7's buffer
89 int ljm_scans = 0; ///< scans still in LJM's buffer
90 };
91 /** Fills `data` with scans_per_read scans, channels interleaved. Blocks until they arrive. */
92 virtual Backlog stream_read(std::vector<double>& data) = 0;
93 virtual void stream_stop() = 0;
94 virtual void close() = 0;
95};
96
97/** What to write before streaming and what to stream, derived from a board's config. */
98struct StreamPlan {
99 std::vector<std::string> setup_names;
100 std::vector<double> setup_values;
101 std::vector<std::string> scan_names; ///< "AIN1", ... in config order, then AIN14 for CJC
102 std::vector<uint8_t> ains; ///< same order as scan_names
103 /** The configured channel behind each scan entry; nullopt for AIN14 (CJC). */
104 std::vector<std::optional<config::T7Channel>> channels;
105 int cjc_index = -1; ///< position of AIN14 in the scan, or -1 when there is no TC
106};
108
109/** One published value. */
110struct Sample {
111 uint8_t ain = 0;
112 float volts = 0.0f;
113 uint64_t timestamp_ns = 0;
114 uint32_t sample_ms = 0; ///< ms since stream start (wraps at 2^32)
115};
116
117/** One scan's thermocouple cold-junction temperature (the T7's terminals), in °C. */
119 float temp_c = 0.0f;
120 uint64_t timestamp_ns = 0;
121 uint32_t sample_ms = 0;
122};
123
124/** A decoded LJM_eStreamRead block. */
125struct Block {
126 std::vector<Sample> samples;
127 uint64_t scans = 0; ///< scans in the block, including lost ones
128 uint64_t skipped_samples = 0; ///< values that were kDummyValue (auto-recovery), not published
129 /** One per scan with a readable AIN14, on a board with a TC; empty otherwise. */
130 std::vector<ColdJunction> cold_junction;
132};
133
134/**
135 * One T7's stream: open, configure, start, then read blocks until something throws.
136 *
137 * Timestamps. The T7 scans on its own crystal at a fixed rate, so scan k happened at
138 * k / actual_rate seconds after the stream started. That device time goes through the same
139 * BoardClockSync the ESP32 boards use, keyed by the board's config section, which estimates the
140 * device-to-host offset from when blocks arrive (arrival can only be late, never early). The newest
141 * scan of each block is anchored that way and the others are placed back from it at the exact
142 * scan period. In [time_sync] mode = "arrival" the anchor is simply the arrival time.
143 */
145public:
146 using Clock = std::function<uint64_t()>; ///< epoch ns
147
148 T7Session(config::T7Board board, std::unique_ptr<T7Device> device,
149 const time::TimeSyncConfig& time_sync, Clock clock);
150
151 /** Opens the device, checks its serial number if the config gives one, writes the setup,
152 * takes the cold-junction reference (TC boards) and starts the stream. Returns the actual
153 * scan rate. */
154 double start();
155
156 /** Reads and decodes one block. */
157 Block read();
158
159 /** Stops the stream and closes the device; never throws. */
160 void stop() noexcept;
161
162 const config::T7Board& board() const {
163 return board_;
164 }
165 const StreamPlan& plan() const {
166 return plan_;
167 }
168 double actual_rate_hz() const {
169 return actual_rate_hz_;
170 }
171 /** start() found a stream already running on the device (left by a bridge that was killed,
172 * a server torn down mid-run, or Kipling) and stopped it before configuring. */
174 return stopped_leftover_;
175 }
176 /** The device's serial number, read by start(); 0 before then. */
177 int64_t serial() const {
178 return serial_;
179 }
180 /** AIN14 offset taken at start (K); see t7::cjc_device_offset_k. NaN without a TC. */
181 double cjc_device_offset_k() const {
183 }
184
185private:
187 std::unique_ptr<T7Device> device_;
191 double actual_rate_hz_ = 0.0;
192 int64_t serial_ = 0;
193 bool stopped_leftover_ = false;
194 uint64_t next_scan_ = 0;
195 std::vector<double> buffer_;
196 bool open_ = false;
198};
199
200/**
201 * The stand-in T7 itself, as distinct from a connection to it.
202 *
203 * A real T7 keeps streaming after the process that started the stream dies, and keeps every
204 * register it was written; the next process to open it finds both (synnaxlabs/synnax#4142 saw
205 * every stream start fail with STREAM_IS_ACTIVE until the leftover stream was stopped). So the
206 * stream and the registers live here, not in a FakeT7Device handle: a handle destroyed without a
207 * stop — a killed bridge — leaves the unit streaming.
208 *
209 * With a state_path the unit is a file, re-read before and written after every operation, so
210 * separate t7_bridge processes (`--fake-state`) see one device: `kill -9` the bridge and the next
211 * one finds the stream still running. Without one it lives as long as the object.
212 */
214public:
215 explicit FakeT7Unit(std::string state_path = "");
216
217 bool streaming = false;
218 uint64_t stream_id = 0; ///< bumped by every stream start; a handle reads only its own stream
219 std::vector<std::string> scan_names; ///< the running (or last) stream's scan list
220 std::vector<std::pair<std::string, double>> registers; ///< last value written per name
221 uint64_t starts = 0, stops = 0;
222
223 double reg(const std::string& name, double fallback = 0.0) const;
224 void set_reg(const std::string& name, double value);
225 /** Power off and on: no stream, registers back to defaults. */
226 void power_cycle();
227
228 void load(); ///< from state_path; no-op without one or when the file is absent
229 void save() const;
230
231private:
232 std::string path_;
233};
234
235/**
236 * A connection to a stand-in T7, for tests and for running the bridge with no hardware
237 * (`t7_bridge --fake`).
238 *
239 * By default each AIN reads a slow sine offset by its AIN number, so the GUI shows distinct
240 * lines; Options::signal replaces that. AIN14 (the internal temperature sensor) reads 25 C and
241 * TEMPERATURE_DEVICE_K agrees with it. Every call is recorded so a test can check what the
242 * session asked of the device. Device state (stream, registers) is the shared FakeT7Unit's.
243 *
244 * Modelled on LJM and the T7 firmware: a stream start, a configuration write or a
245 * command-response read while the unit streams fails with STREAM_IS_ACTIVE (2605); a stop with
246 * no stream answers STREAM_NOT_RUNNING (1303); writing STREAM_ENABLE = 0 stops the stream from
247 * any handle; reading a stream another handle stopped times out (1306).
248 */
249class FakeT7Device : public T7Device {
250public:
251 struct Options {
252 bool realtime = false; ///< sleep each read as long as the scans would take
253 int64_t serial = 470000000;
254 double rate_cap_hz = 0.0; ///< >0: the "device" rounds the scan rate down to this
255 uint64_t dummy_scan_every = 0; ///< >0: every Nth scan comes back as kDummyValue
256 int fail_read_after = -1; ///< >=0: throw on the read after this many succeed
257 /** Volts on `ain` at `t` seconds into the stream; empty = the default sine. Not
258 * consulted for AIN14, which always follows device_temp_k. */
259 std::function<double(int ain, double t)> signal;
260 double device_temp_k = 298.15; ///< what TEMPERATURE_DEVICE_K (and AIN14) report
261 /** The device this handle talks to; null = a fresh unit of its own. */
262 std::shared_ptr<FakeT7Unit> unit;
263 /** LJM answers eStreamStop locally (1225) on a handle that did not start the stream,
264 * without telling the device. Synnax found a new handle's stop does reach a T7; this
265 * models the LJM where it would not, so the session's fallback is exercised. */
267 /** !=0: every stream stop fails with this code (e.g. the link dropped). */
269 };
272 explicit FakeT7Device(Options o);
273
274 void open(const std::string& ip) override;
275 int64_t serial_number() override;
276 void write_names(const std::vector<std::string>& names,
277 const std::vector<double>& values) override;
278 std::vector<double> read_names(const std::vector<std::string>& names) override;
279 double stream_start(const std::vector<std::string>& scan_names, int scans_per_read,
280 double scan_rate_hz) override;
281 Backlog stream_read(std::vector<double>& data) override;
282 void stream_stop() override;
283 void close() override;
284
285 // Recorded for tests.
286 std::string opened_ip;
287 std::vector<std::pair<std::string, double>> writes;
288 std::vector<std::string> scan_names;
289 std::vector<std::string> reads; ///< every name read_names() was asked for
290 bool streaming = false; ///< this handle's own stream is running
291 bool closed = false;
292 int stop_calls = 0;
293
295 return *opt_.unit;
296 }
297
298 /** Volts on `ain` at `t`: Options::signal, else the default described above. */
299 double volts(int ain, double t) const;
300
301private:
304 double rate_ = 0.0;
305 uint64_t scan_ = 0;
306 int reads_ = 0;
307 uint64_t my_stream_ = 0; ///< unit stream_id this handle started; 0 = none
308 bool owns_stream() const {
309 return my_stream_ != 0 && opt_.unit->streaming && opt_.unit->stream_id == my_stream_;
310 }
311};
312
313} // namespace t7
314} // namespace fsw
Per-board clock sync for DAQ sample timestamping.
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
FakeT7Unit & unit()
Definition T7Stream.hpp:294
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
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
The device seam.
Definition T7Stream.hpp:75
virtual double stream_start(const std::vector< std::string > &scan_names, int scans_per_read, double scan_rate_hz)=0
Starts the stream; returns the scan rate the device will actually run at.
virtual ~T7Device()=default
virtual void close()=0
virtual int64_t serial_number()=0
virtual void write_names(const std::vector< std::string > &names, const std::vector< double > &values)=0
virtual void stream_stop()=0
virtual void open(const std::string &ip)=0
virtual std::vector< double > read_names(const std::vector< std::string > &names)=0
Command-response reads; only valid while no stream is running.
virtual Backlog stream_read(std::vector< double > &data)=0
Fills data with scans_per_read scans, channels interleaved.
T7Error(const std::string &what, int code=0)
Definition T7Stream.hpp:64
int code() const
Definition T7Stream.hpp:66
Block read()
Reads and decodes one block.
Definition T7Stream.cpp:158
double actual_rate_hz() const
Definition T7Stream.hpp:168
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
int64_t serial() const
The device's serial number, read by start(); 0 before then.
Definition T7Stream.hpp:177
time::BoardClockSync clock_sync_
Definition T7Stream.hpp:188
double cjc_device_offset_k() const
AIN14 offset taken at start (K); see t7::cjc_device_offset_k.
Definition T7Stream.hpp:181
T7Session(config::T7Board board, std::unique_ptr< T7Device > device, const time::TimeSyncConfig &time_sync, Clock clock)
Definition T7Stream.cpp:82
const StreamPlan & plan() const
Definition T7Stream.hpp:165
std::function< uint64_t()> Clock
epoch ns
Definition T7Stream.hpp:146
config::T7Board board_
Definition T7Stream.hpp:186
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
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
uint8_t reg
return o
Definition json.hpp:13897
LJM and device error codes the session acts on (labjack/ljm_constants, LJM errorcodes).
Definition T7Stream.hpp:50
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
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
Motorized ball valve position: raw ADC counts -> % open.
STL namespace.
One [boards.
Definition T7Config.hpp:68
A decoded LJM_eStreamRead block.
Definition T7Stream.hpp:125
uint64_t skipped_samples
values that were kDummyValue (auto-recovery), not published
Definition T7Stream.hpp:128
uint64_t scans
scans in the block, including lost ones
Definition T7Stream.hpp:127
std::vector< Sample > samples
Definition T7Stream.hpp:126
std::vector< ColdJunction > cold_junction
One per scan with a readable AIN14, on a board with a TC; empty otherwise.
Definition T7Stream.hpp:130
T7Device::Backlog backlog
Definition T7Stream.hpp:131
One scan's thermocouple cold-junction temperature (the T7's terminals), in °C.
Definition T7Stream.hpp:118
bool stop_only_own_stream
LJM answers eStreamStop locally (1225) on a handle that did not start the stream, without telling the...
Definition T7Stream.hpp:266
int fail_read_after
>=0: throw on the read after this many succeed
Definition T7Stream.hpp:256
double rate_cap_hz
>0: the "device" rounds the scan rate down to this
Definition T7Stream.hpp:254
std::function< double(int ain, double t)> signal
Volts on ain at t seconds into the stream; empty = the default sine.
Definition T7Stream.hpp:259
uint64_t dummy_scan_every
>0: every Nth scan comes back as kDummyValue
Definition T7Stream.hpp:255
double device_temp_k
what TEMPERATURE_DEVICE_K (and AIN14) report
Definition T7Stream.hpp:260
std::shared_ptr< FakeT7Unit > unit
The device this handle talks to; null = a fresh unit of its own.
Definition T7Stream.hpp:262
int fail_stop_code
!=0: every stream stop fails with this code (e.g.
Definition T7Stream.hpp:268
bool realtime
sleep each read as long as the scans would take
Definition T7Stream.hpp:252
One published value.
Definition T7Stream.hpp:110
uint64_t timestamp_ns
Definition T7Stream.hpp:113
uint32_t sample_ms
ms since stream start (wraps at 2^32)
Definition T7Stream.hpp:114
What to write before streaming and what to stream, derived from a board's config.
Definition T7Stream.hpp:98
std::vector< std::optional< config::T7Channel > > channels
The configured channel behind each scan entry; nullopt for AIN14 (CJC).
Definition T7Stream.hpp:104
std::vector< uint8_t > ains
same order as scan_names
Definition T7Stream.hpp:102
std::vector< double > setup_values
Definition T7Stream.hpp:100
std::vector< std::string > scan_names
"AIN1", ... in config order, then AIN14 for CJC
Definition T7Stream.hpp:101
int cjc_index
position of AIN14 in the scan, or -1 when there is no TC
Definition T7Stream.hpp:105
std::vector< std::string > setup_names
Definition T7Stream.hpp:99
int device_scans
scans still in the T7's buffer
Definition T7Stream.hpp:88
int ljm_scans
scans still in LJM's buffer
Definition T7Stream.hpp:89
std::string board(const std::string &name, const std::string &id, bool enabled=true)