88 return static_cast<uint64_t
>(std::chrono::duration_cast<std::chrono::nanoseconds>(
89 std::chrono::system_clock::now().time_since_epoch())
94void nap(std::chrono::milliseconds d) {
95 const auto until = std::chrono::steady_clock::now() + d;
97 std::this_thread::sleep_for(std::chrono::milliseconds(100));
112 std::map<int, fsw::config::T7Channel> by_ain;
113 for (
const auto& c :
board.channels)
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())
120 const auto& c = it->second;
121 if (c.kind ==
"PT") {
123 return ((0.30 + 0.03 * wobble) * c.full_scale - psi0) / psi_per_v;
126 return (0.20 + 0.02 * wobble) * c.sensitivity_mv_per_v / 1000.0 * c.excitation_v;
127 if (c.kind ==
"RTD") {
132 return c.vref_v * c.ljtr_ohms / (c.ljtr_ohms +
r);
134 if (c.kind ==
"TC") {
136 return (type_k_mv_from_c(60.0 + 10.0 * wobble) - type_k_mv_from_c(20.1)) / 1000.0;
138 return 2.5 + 0.5 * wobble;
143 const std::shared_ptr<fsw::t7::FakeT7Unit>& unit) {
149 return std::make_unique<fsw::t7::FakeT7Device>(
o);
152 return std::make_unique<fsw::t7::LjmT7Device>();
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 +
"] ";
164 std::shared_ptr<fsw::t7::FakeT7Unit> unit;
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);
175 const double rate = session.
start();
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"
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);
189 uint64_t published = 0, skipped = 0, dropped = 0;
190 auto next_log = std::chrono::steady_clock::now() + std::chrono::seconds(10);
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)) {
201 << tag <<
"Elodin connected, tables registered" << std::endl;
204 << tag <<
"Elodin table registration incomplete" << std::endl;
212 <<
" samples lost (device buffer overflowed)" << std::endl;
222 msg.
setField<2>(std::array<uint8_t, 3>{0, 0, 0});
228 for (
const auto& s : block.
samples) {
232 msg.
setField<2>(std::array<uint8_t, 3>{0, 0, 0});
239 published += block.
samples.size();
241 dropped += block.
samples.size();
242 Line(std::cerr) << tag <<
"Elodin write failed (" << db.
last_error()
243 <<
"), reconnecting" << std::endl;
247 std::array<uint8_t, 4096>
drain;
251 dropped += block.
samples.size();
254 if (std::chrono::steady_clock::now() >= next_log) {
255 next_log += std::chrono::seconds(10);
257 << tag <<
"published " << published <<
", dropped (no Elodin) " << dropped
258 <<
", lost to auto-recovery " << skipped <<
" | backlog device "
260 <<
" scans" << std::endl;
264 Line(std::cerr) << tag << e.what() <<
" — retrying in " << backoff.count() <<
" ms"
270 backoff = std::min(backoff * 2, std::chrono::milliseconds(10000));
273 Line(std::cout) << tag <<
"stopped" << std::endl;
278int main(
int argc,
char* argv[]) {
280 for (
int i = 1; i < argc; ++i) {
281 const std::string
a = argv[i];
282 auto next = [&]() -> std::string {
284 std::cerr <<
a <<
" needs a value" << std::endl;
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")
299 else if (
a ==
"--fake-state")
300 opt.fake_state = next();
302 std::cout <<
"Usage: " << argv[0]
303 <<
" [--config path] [--fake [--fake-state DIR]] [--board boards.<name>] "
307 return a ==
"--help" ||
a ==
"-h" ? 0 : 2;
313 std::cerr <<
"[t7_bridge] built without LabJack LJM (libLabJackM not found at configure "
314 "time). Install LJM and rebuild, or run with --fake."
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;
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))
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;
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);
345 signal(SIGPIPE, SIG_IGN);
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,
353 for (
auto& t : threads)
Line & operator<<(std::ostream &(*manip)(std::ostream &))
Line & operator<<(const T &v)
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.
One T7's stream: open, configure, start, then read blocks until something throws.
Block read()
Reads and decodes one block.
double start()
Opens the device, checks its serial number if the config gives one, writes the setup,...
int64_t serial() const
The device's serial number, read by start(); 0 before then.
const StreamPlan & plan() const
bool stopped_leftover_stream() const
start() found a stream already running on the device (left by a bridge that was killed,...
void stop() noexcept
Stops the stream and closes the device; never throws.
*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
void nap(std::chrono::milliseconds d)
std::atomic< bool > g_running
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.
Config load(const std::string &path)
Parse config.toml.
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.
double pt_psi(double volts, const config::T7Channel &ch)
PT: linear between the signal's zero and full-scale outputs.
static uint32_t until(uint32_t now, uint32_t t)
void setField(ValueType &&value)
The whole parsed config.toml, restricted to what C++ consumes.
A decoded LJM_eStreamRead block.
uint64_t skipped_samples
values that were kDummyValue (auto-recovery), not published
std::vector< Sample > samples
std::vector< ColdJunction > cold_junction
One per scan with a readable AIN14, on a board with a TC; empty otherwise.
T7Device::Backlog backlog
std::vector< uint8_t > ains
same order as scan_names
int cjc_index
position of AIN14 in the scan, or -1 when there is no TC
int device_scans
scans still in the T7's buffer
int ljm_scans
scans still in LJM's buffer
static void drain(emmc_log_t *log, uint32_t now)
std::string board(const std::string &name, const std::string &id, bool enabled=true)