STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
t7_bridge_main.cpp
Go to the documentation of this file.
1// t7_bridge — streams LabJack T7 analog inputs into Elodin.
2//
3// Inputs only: this process never writes a DIO, DAC or anything else that drives hardware. The
4// only device writes are the stream configuration in fsw::t7::plan_stream() (ranges, input mode,
5// stream timing), sent to the T7s declared in config and nowhere else.
6//
7// One thread per enabled [boards.*] type = "T7". Each opens its T7 by IP over Ethernet, streams
8// the configured AINs, and publishes every sample to Elodin table {0x26, (slot-1)*0x20 + ain}
9// (entity T7<slot>.AIN<n>, value in volts). A board with a thermocouple also publishes its cold
10// junction (°C) at {0x26, (slot-1)*0x20 + 0x0F}. Calibration — volts to psi / °C / kg, captures,
11// models, tares — is calibration_service's, the same as for every ESP32 sensor
12// (t7/T7Calibration.hpp). A lost link, a stream error or a missing Elodin is retried with backoff;
13// samples taken while Elodin is down are counted and dropped.
14//
15// Usage: t7_bridge [--config config/config.toml] [--fake] [--board <section>]
16// [--elodin-host H] [--elodin-port P]
17// --fake stand-in T7s (FakeT7Device) instead of LJM: for a dev box with no hardware or LJM.
18// --fake-state DIR keep each stand-in T7's stream and registers in DIR/<section>.state, so
19// the next bridge finds what this one left, as on a real T7: kill -9 a bridge and the
20// next one must stop the stream it left running before it can configure the device.
21
22#include <signal.h>
23
24#include <algorithm>
25#include <array>
26#include <atomic>
27#include <chrono>
28#include <cmath>
29#include <cstdlib>
30#include <functional>
31#include <iostream>
32#include <map>
33#include <memory>
34#include <mutex>
35#include <sstream>
36#include <string>
37#include <thread>
38#include <vector>
39
41#include "calibration/TypeK.hpp"
43#include "config/Config.hpp"
46#include "t7/T7Calibration.hpp"
47#include "t7/T7Convert.hpp"
48#include "t7/T7Stream.hpp"
49
50#ifdef T7_HAVE_LJM
51#include "LjmT7Device.hpp"
52#endif
53
54namespace {
55
56std::atomic<bool> g_running{true};
57
58// One line per statement even with a thread per T7: buffer, then write under a lock.
59std::mutex g_log_mutex;
60class Line {
61public:
62 explicit Line(std::ostream& out) : out_(out) {
63 }
65 std::lock_guard<std::mutex> lock(g_log_mutex);
66 out_ << buf_.str() << std::flush;
67 }
68 template <typename T>
69 Line& operator<<(const T& v) {
70 buf_ << v;
71 return *this;
72 }
73 Line& operator<<(std::ostream& (*manip)(std::ostream&)) {
74 buf_ << manip;
75 return *this;
76 }
77
78private:
79 std::ostream& out_;
80 std::ostringstream buf_;
81};
82
83void on_signal(int) {
84 g_running = false;
85}
86
87uint64_t epoch_ns() {
88 return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
89 std::chrono::system_clock::now().time_since_epoch())
90 .count());
91}
92
93// Sleep that wakes early on shutdown.
94void nap(std::chrono::milliseconds d) {
95 const auto until = std::chrono::steady_clock::now() + d;
96 while (g_running && std::chrono::steady_clock::now() < until)
97 std::this_thread::sleep_for(std::chrono::milliseconds(100));
98}
99
100struct Options {
101 std::string config_path = "config/config.toml";
102 std::string only_board; // "" = every enabled T7
103 std::string elodin_host;
104 int elodin_port = 0;
105 bool fake = false;
106 std::string fake_state; // --fake-state DIR: one file per stand-in T7, shared across runs
107};
108
109// For --fake: each channel reads something plausible for what the config says is wired to it,
110// so a simulated run shows pressures, temperatures and loads in the GUI rather than volts.
111std::function<double(int, double)> fake_signal(const fsw::config::T7Board& board) {
112 std::map<int, fsw::config::T7Channel> by_ain;
113 for (const auto& c : board.channels)
114 by_ain[c.ain] = c;
115 return [by_ain](int ain, double t) {
116 const auto it = by_ain.find(ain);
117 const double wobble = std::sin(2.0 * M_PI * 0.1 * t + ain);
118 if (it == by_ain.end())
119 return 0.0;
120 const auto& c = it->second;
121 if (c.kind == "PT") { // 30 % of full scale, +/- 3 %. pt_psi is linear: invert it.
122 const double psi0 = fsw::t7::pt_psi(0.0, c), psi_per_v = fsw::t7::pt_psi(1.0, c) - psi0;
123 return ((0.30 + 0.03 * wobble) * c.full_scale - psi0) / psi_per_v;
124 }
125 if (c.kind == "LC") // 20 % of rated load
126 return (0.20 + 0.02 * wobble) * c.sensitivity_mv_per_v / 1000.0 * c.excitation_v;
127 if (c.kind == "RTD") { // -150 C +/- 20 (a cryogenic line): the divider's output
129 cvd.R0 = c.r0_ohms;
130 const double r =
131 fsw::calibration::rtd::temp_to_resistance_cvd(-150.0 + 20.0 * wobble, cvd);
132 return c.vref_v * c.ljtr_ohms / (c.ljtr_ohms + r);
133 }
134 if (c.kind == "TC") { // 60 C +/- 10 against a ~20 C junction (fake device at 25 C)
136 return (type_k_mv_from_c(60.0 + 10.0 * wobble) - type_k_mv_from_c(20.1)) / 1000.0;
137 }
138 return 2.5 + 0.5 * wobble;
139 };
140}
141
142std::unique_ptr<fsw::t7::T7Device> make_device(bool fake, const fsw::config::T7Board& board,
143 const std::shared_ptr<fsw::t7::FakeT7Unit>& unit) {
144 if (fake) {
146 o.realtime = true;
147 o.signal = fake_signal(board);
148 o.unit = unit;
149 return std::make_unique<fsw::t7::FakeT7Device>(o);
150 }
151#ifdef T7_HAVE_LJM
152 return std::make_unique<fsw::t7::LjmT7Device>();
153#else
154 return nullptr; // main() refuses to start without --fake in this build
155#endif
156}
157
159 const std::string& db_host, uint16_t db_port, bool fake,
160 const std::string& fake_state) {
161 const std::string tag = "[t7_bridge " + board.section + "] ";
162 // The stand-in device outlives each connection to it, as a T7 does: across this loop's
163 // reconnects always, and across bridge processes when it is a --fake-state file.
164 std::shared_ptr<fsw::t7::FakeT7Unit> unit;
165 if (fake)
166 unit = std::make_shared<fsw::t7::FakeT7Unit>(
167 fake_state.empty() ? std::string() : fake_state + "/" + board.section + ".state");
169 auto next_db_attempt = std::chrono::steady_clock::now();
170 auto backoff = std::chrono::milliseconds(1000);
171
172 while (g_running) {
173 fsw::t7::T7Session session(board, make_device(fake, board, unit), time_sync, epoch_ns);
174 try {
175 const double rate = session.start();
176 if (session.stopped_leftover_stream())
177 Line(std::cout) << tag << "stopped a stream already running on " << board.ip
178 << " (left by a bridge or server that did not shut down "
179 "cleanly?) and reconfigured it"
180 << std::endl;
181 // Raw tables only, plus the cold junction on a TC board: turning volts into psi / °C /
182 // kg is calibration_service's job, exactly as for the ESP32 boards.
183 const bool has_cjc = session.plan().cjc_index >= 0;
184 Line(std::cout) << tag << "streaming " << session.plan().ains.size() << " AIN from "
185 << board.ip << " (serial " << session.serial() << ") at " << rate
186 << " scans/s (asked " << board.scan_rate_hz << ")" << std::endl;
187 backoff = std::chrono::milliseconds(1000);
188
189 uint64_t published = 0, skipped = 0, dropped = 0;
190 auto next_log = std::chrono::steady_clock::now() + std::chrono::seconds(10);
191
192 while (g_running) {
193 // Elodin comes and goes independently of the T7; keep streaming either way so the
194 // device buffer never backs up, and (re)register the tables on every connect.
195 if (!db.is_connected() && std::chrono::steady_clock::now() >= next_db_attempt) {
196 next_db_attempt = std::chrono::steady_clock::now() + std::chrono::seconds(2);
197 if (db.connect(db_host, db_port)) {
199 db, board.slot(), session.plan().ains, has_cjc))
200 Line(std::cout)
201 << tag << "Elodin connected, tables registered" << std::endl;
202 else
203 Line(std::cerr)
204 << tag << "Elodin table registration incomplete" << std::endl;
205 }
206 }
207
208 const fsw::t7::Block block = session.read();
209 skipped += block.skipped_samples;
210 if (block.skipped_samples > 0)
211 Line(std::cerr) << tag << "stream auto-recovery: " << block.skipped_samples
212 << " samples lost (device buffer overflowed)" << std::endl;
213
214 if (db.is_connected()) {
215 db.begin_batch();
216 // Cold junction first, so calibration_service has this block's before the
217 // thermocouple samples it applies to.
218 for (const auto& cj : block.cold_junction) {
220 msg.setField<0>(cj.timestamp_ns);
222 msg.setField<2>(std::array<uint8_t, 3>{0, 0, 0});
223 msg.setField<3>(cj.temp_c);
224 msg.setField<4>(cj.sample_ms);
225 msg.setField<5>(uint8_t{0});
226 db.publish(fsw::t7::cjc_table_id(board.slot()), msg);
227 }
228 for (const auto& s : block.samples) {
230 msg.setField<0>(s.timestamp_ns);
231 msg.setField<1>(s.ain);
232 msg.setField<2>(std::array<uint8_t, 3>{0, 0, 0});
233 msg.setField<3>(s.volts);
234 msg.setField<4>(s.sample_ms);
235 msg.setField<5>(uint8_t{0});
236 db.publish(fsw::t7::table_id(board.slot(), s.ain), msg);
237 }
238 if (db.flush_batch()) {
239 published += block.samples.size();
240 } else {
241 dropped += block.samples.size();
242 Line(std::cerr) << tag << "Elodin write failed (" << db.last_error()
243 << "), reconnecting" << std::endl;
244 db.disconnect();
245 }
246 // Drain the db's replies so its socket buffer never fills and stalls us.
247 std::array<uint8_t, 4096> drain;
248 while (db.is_connected() && db.read_data(drain.data(), drain.size()) > 0) {
249 }
250 } else {
251 dropped += block.samples.size();
252 }
253
254 if (std::chrono::steady_clock::now() >= next_log) {
255 next_log += std::chrono::seconds(10);
256 Line(std::cout)
257 << tag << "published " << published << ", dropped (no Elodin) " << dropped
258 << ", lost to auto-recovery " << skipped << " | backlog device "
259 << block.backlog.device_scans << " / LJM " << block.backlog.ljm_scans
260 << " scans" << std::endl;
261 }
262 }
263 } catch (const fsw::t7::T7Error& e) {
264 Line(std::cerr) << tag << e.what() << " — retrying in " << backoff.count() << " ms"
265 << std::endl;
266 }
267 session.stop();
268 if (g_running) {
269 nap(backoff);
270 backoff = std::min(backoff * 2, std::chrono::milliseconds(10000));
271 }
272 }
273 Line(std::cout) << tag << "stopped" << std::endl;
274}
275
276} // namespace
277
278int main(int argc, char* argv[]) {
279 Options opt;
280 for (int i = 1; i < argc; ++i) {
281 const std::string a = argv[i];
282 auto next = [&]() -> std::string {
283 if (i + 1 >= argc) {
284 std::cerr << a << " needs a value" << std::endl;
285 std::exit(2);
286 }
287 return argv[++i];
288 };
289 if (a == "--config")
290 opt.config_path = next();
291 else if (a == "--board")
292 opt.only_board = next();
293 else if (a == "--elodin-host")
294 opt.elodin_host = next();
295 else if (a == "--elodin-port")
296 opt.elodin_port = std::atoi(next().c_str());
297 else if (a == "--fake")
298 opt.fake = true;
299 else if (a == "--fake-state")
300 opt.fake_state = next();
301 else {
302 std::cout << "Usage: " << argv[0]
303 << " [--config path] [--fake [--fake-state DIR]] [--board boards.<name>] "
304 "[--elodin-host H]"
305 " [--elodin-port P]"
306 << std::endl;
307 return a == "--help" || a == "-h" ? 0 : 2;
308 }
309 }
310
311#ifndef T7_HAVE_LJM
312 if (!opt.fake) {
313 std::cerr << "[t7_bridge] built without LabJack LJM (libLabJackM not found at configure "
314 "time). Install LJM and rebuild, or run with --fake."
315 << std::endl;
316 return 3;
317 }
318#endif
319
320 const fsw::config::Config cfg = fsw::config::load(opt.config_path);
321 if (!cfg.t7_errors.empty()) {
322 std::cerr << "[t7_bridge] refusing to start, the T7 config has problems:" << std::endl;
323 for (const auto& e : cfg.t7_errors)
324 std::cerr << " " << e << std::endl;
325 return 2;
326 }
327
328 std::vector<fsw::config::T7Board> boards;
329 for (const auto& b : cfg.t7_boards)
330 if (b.enabled && (opt.only_board.empty() || b.section == opt.only_board))
331 boards.push_back(b);
332 if (boards.empty()) {
333 std::cout << "[t7_bridge] no enabled T7 in " << opt.config_path
334 << (opt.only_board.empty() ? "" : " matching " + opt.only_board)
335 << "; nothing to do" << std::endl;
336 return 0;
337 }
338
339 const std::string db_host = opt.elodin_host.empty() ? cfg.database.host : opt.elodin_host;
340 const uint16_t db_port =
341 static_cast<uint16_t>(opt.elodin_port > 0 ? opt.elodin_port : cfg.database.port);
342
343 signal(SIGINT, on_signal);
344 signal(SIGTERM, on_signal);
345 signal(SIGPIPE, SIG_IGN); // a dropped Elodin socket must not kill the process
346
347 std::cout << "[t7_bridge] " << boards.size() << " T7(s), Elodin " << db_host << ":" << db_port
348 << (opt.fake ? ", FAKE devices" : "") << std::endl;
349 std::vector<std::thread> threads;
350 for (const auto& b : boards)
351 threads.emplace_back(run_board, b, cfg.time_sync, db_host, db_port, opt.fake,
352 opt.fake_state);
353 for (auto& t : threads)
354 t.join();
355 return 0;
356}
Line & operator<<(std::ostream &(*manip)(std::ostream &))
static bool register_t7_raw_tables(ElodinClient &client, uint8_t slot, const std::vector< uint8_t > &ains, bool cjc)
Register one LabJack T7's RAW VTables (t7_bridge).
Standalone Elodin database client.
bool flush_batch()
Flush (send) the accumulated batch in a single TCP write.
bool is_connected() const
Check if connected.
ssize_t read_data(void *buffer, size_t max_len)
Read incoming data packet from Elodin (non-blocking).
std::string last_error() const
Get last error message.
bool publish(uint16_t message_id, const MessageType &message)
Publish a message to a table.
void disconnect()
Disconnect from database.
void begin_batch()
Begin batching — subsequent publish() calls go into an internal buffer.
bool connect(const std::string &host, uint16_t port=2240)
Connect to Elodin database.
A device failure.
Definition T7Stream.hpp:62
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
int64_t serial() const
The device's serial number, read by start(); 0 before then.
Definition T7Stream.hpp:177
const StreamPlan & plan() const
Definition T7Stream.hpp:165
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
*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
static osMutexId_t lock
Definition mode.c:11
void nap(std::chrono::milliseconds d)
void run_board(const fsw::config::T7Board &board, const fsw::time::TimeSyncConfig &time_sync, const std::string &db_host, uint16_t db_port, bool fake, const std::string &fake_state)
std::unique_ptr< fsw::t7::T7Device > make_device(bool fake, const fsw::config::T7Board &board, const std::shared_ptr< fsw::t7::FakeT7Unit > &unit)
std::function< double(int, double)> fake_signal(const fsw::config::T7Board &board)
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, uint32_t, uint8_t > T7RawMessage
One LabJack T7 analog-input sample, published by t7_bridge.
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 type_k_mv_from_c(double c)
EMF (mV, referenced to 0 C) a type K produces at c.
Definition TypeK.cpp:38
Config load(const std::string &path)
Parse config.toml.
Definition Config.cpp:442
constexpr uint8_t kCjcTableOffset
Raw-table offset (within a slot's 32-entry block) of the cold-junction table: AIN15 does not exist on...
std::array< uint8_t, 2 > cjc_table_id(uint8_t slot)
Cold-junction table id for a slot.
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
double pt_psi(double volts, const config::T7Channel &ch)
PT: linear between the signal's zero and full-scale outputs.
Definition T7Convert.cpp:25
static uint32_t until(uint32_t now, uint32_t t)
void setField(ValueType &&value)
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
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
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
std::vector< uint8_t > ains
same order as scan_names
Definition T7Stream.hpp:102
int cjc_index
position of AIN14 in the scan, or -1 when there is no TC
Definition T7Stream.hpp:105
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
int main()
static boot_report_t r
static void drain(emmc_log_t *log, uint32_t now)
std::string board(const std::string &name, const std::string &id, bool enabled=true)