STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
PressureFeed.cpp
Go to the documentation of this file.
2
3#include <iostream>
4
8
9namespace sequencer {
10
11namespace {
12/** Long enough that the loop is not spinning, short enough that stop() is prompt. */
13constexpr int kRecvTimeoutMs = 200;
14constexpr int kReconnectBackoffMs = 200;
15
16/**
17 * How often a table that has never delivered is asked for again.
18 *
19 * subscribe_tables() only reports whether the SOCKET WRITE succeeded — it sends request id 0
20 * and never reads a reply, and the C++ client has no ErrorResponse handling at all. So a
21 * subscription the DB refuses is silent, and on a cold start it is refused routinely: the
22 * sequencer and the calibration service come up together, and whichever loses the race
23 * subscribes to a calibrated PT table before calibration_service has registered its VTable.
24 * Measured on the stand 2026-09-16: PressureFeed subscribed at 16:45:08.348, the calibrated
25 * VTables were registered at 16:45:08.494 — 146 ms later. The DB answered "invalid msg id",
26 * nothing retried, and the feed stayed blind for the life of the process. Every dynamic state
27 * reading a pressure was then refused entry with "has produced no reading yet" for the whole
28 * session, decided by ~150 ms of startup ordering.
29 */
30constexpr int kResubscribeMs = 1000;
31} // namespace
32
36
37void PressureFeed::start(const fsw::config::Config& cfg, const std::vector<std::string>& roles,
38 const std::string& host, uint16_t port) {
39 stop();
40 if (roles.empty())
41 return; // nothing reads a pressure — do not open a socket at all
42
43 {
44 std::lock_guard<std::mutex> lk(mtx_);
45 role_by_table_.clear();
46 readings_.clear();
47 for (const auto& role : roles) {
48 const auto ref = fsw::config::find_pt_role(cfg, role);
49 if (!ref) {
50 // A script naming this was already refused at load, so this is belt and braces.
51 std::cerr
52 << "[PressureFeed] \"" << role
53 << "\" is not declared by any enabled PT board or T7 PT input — not subscribed"
54 << std::endl;
55 continue;
56 }
57 role_by_table_[ref->table()] = role;
58 std::cout << "[PressureFeed] \"" << role << "\" -> "
59 << (ref->t7 ? "LabJack T7 " : "board ") << int(ref->board_number)
60 << (ref->t7 ? " AIN " : " ch ") << ref->channel << " (table 0x" << std::hex
61 << int(ref->table_hi) << ",0x" << int(ref->table_lo) << std::dec << ")"
62 << std::endl;
63 }
64 if (role_by_table_.empty())
65 return;
66 }
67
68 running_ = true;
69 thread_ = std::thread(&PressureFeed::runLoop, this, host, port);
70}
71
73 running_ = false;
74 if (thread_.joinable())
75 thread_.join();
76}
77
78void PressureFeed::runLoop(std::string host, uint16_t port) {
80 auto last_resubscribe = std::chrono::steady_clock::now();
81 /** Say "still waiting" once, not once a second for the life of the process. */
82 bool silent_logged = false;
83
84 // A separate client from the sequencer's publisher, on purpose: ElodinClient's publish_mutex_
85 // does not guard the read path, so reading and publishing through one object from two threads
86 // is unprotected. ControllerService splits them for the same reason.
87 while (running_) {
88 if (!client.is_connected()) {
89 if (!client.connect(host, port)) {
90 std::this_thread::sleep_for(std::chrono::milliseconds(kReconnectBackoffMs));
91 continue;
92 }
93 std::vector<std::pair<uint8_t, uint8_t>> tables;
94 {
95 std::lock_guard<std::mutex> lk(mtx_);
96 for (const auto& [id, role] : role_by_table_)
97 tables.push_back(id);
98 }
99 // Re-subscribed on EVERY reconnect. The VTables live in the db process, so a
100 // reconnection that skips this comes back permanently silent — connected, reading
101 // nothing, with no error to show for it.
102 if (!client.subscribe_tables(tables)) {
103 std::cerr << "[PressureFeed] subscribe failed — dropping the socket to retry"
104 << std::endl;
105 client.disconnect();
106 std::this_thread::sleep_for(std::chrono::milliseconds(kReconnectBackoffMs));
107 continue;
108 }
109 // Without a receive timeout read_packet() blocks forever, and this thread could never
110 // be shut down or re-subscribed.
111 client.set_recv_timeout_ms(kRecvTimeoutMs);
112 std::cout << "[PressureFeed] subscribed to " << tables.size()
113 << " calibrated PT table(s)" << std::endl;
114 last_resubscribe = std::chrono::steady_clock::now();
115 silent_logged = false;
116 }
117
118 // Ask again for anything that has never arrived. Only those: a table that HAS delivered
119 // is demonstrably subscribed, and re-sending it would spawn a second stream task in the
120 // db and double its rate. A table that has delivered nothing cannot be duplicated, so
121 // this is free to repeat until the publisher shows up — which is the only signal
122 // available, the refusal itself being unobservable from here.
123 if (client.is_connected()) {
124 const auto now = std::chrono::steady_clock::now();
125 if (now - last_resubscribe >= std::chrono::milliseconds(kResubscribeMs)) {
126 std::vector<std::pair<uint8_t, uint8_t>> missing;
127 {
128 std::lock_guard<std::mutex> lk(mtx_);
129 for (const auto& [id, role] : role_by_table_)
130 if (readings_.find(role) == readings_.end())
131 missing.push_back(id);
132 }
133 if (!missing.empty()) {
134 if (!silent_logged) {
135 std::cout << "[PressureFeed] " << missing.size()
136 << " table(s) have produced nothing yet — re-subscribing until "
137 "their publisher registers"
138 << std::endl;
139 silent_logged = true;
140 }
141 client.subscribe_tables(missing);
142 } else if (silent_logged) {
143 std::cout << "[PressureFeed] every subscribed table is delivering" << std::endl;
144 silent_logged = false;
145 }
146 last_resubscribe = now;
147 }
148 }
149
150 uint8_t buf[4096];
151 const ssize_t n = client.read_packet(buf, sizeof(buf));
152 if (n < 0) {
153 client.disconnect();
154 continue;
155 }
156 if (n < 8)
157 continue; // receive timeout
158
159 // Header: len(4) ty(1) id_hi(1) id_lo(1) req_id(1) — EIGHT bytes. The dead
160 // PressureStateMachine uses twelve and reads a float four bytes past the one it wants.
161 const uint8_t type_hi = buf[5];
162 const uint8_t type_lo = buf[6];
163
164 std::string role;
165 {
166 std::lock_guard<std::mutex> lk(mtx_);
167 auto it = role_by_table_.find({type_hi, type_lo});
168 if (it == role_by_table_.end())
169 continue; // a table we did not ask for
170 role = it->second;
171 }
172
173 const size_t payload_len = static_cast<size_t>(n) - 8;
175 continue;
176
178 msg.deserialize(buf + 8);
179
180 Reading r;
181 r.psi = static_cast<double>(msg.getField<3>());
182 r.cal_status = msg.getField<5>();
183 r.at = std::chrono::steady_clock::now();
184
185 // Keyed by the table the packet arrived on, never by the message's own channel_id field —
186 // that field is not always populated, and ControllerService carries a fallback for exactly
187 // that. The table id came from the role, so it cannot name a different sensor.
188 std::lock_guard<std::mutex> lk(mtx_);
189 readings_[role] = r;
190 }
191
192 client.disconnect();
193}
194
195PressureFeed::Status PressureFeed::read(const std::string& role, double& psi) const {
196 std::lock_guard<std::mutex> lk(mtx_);
197
198 bool subscribed = false;
199 for (const auto& [id, r] : role_by_table_) {
200 if (r == role) {
201 subscribed = true;
202 break;
203 }
204 }
205 if (!subscribed)
207
208 auto it = readings_.find(role);
209 if (it == readings_.end())
210 return Status::NoReading;
211
212 const auto age = std::chrono::duration_cast<std::chrono::milliseconds>(
213 std::chrono::steady_clock::now() - it->second.at)
214 .count();
215 if (age > kMaxAgeMs)
216 return Status::Stale;
217 if (it->second.cal_status != 1)
219
220 psi = it->second.psi;
221 return Status::Ok;
222}
223
224std::string PressureFeed::explain(const std::string& role, Status s) {
225 switch (s) {
226 case Status::Ok:
227 return role + " is readable";
229 return role + " is not subscribed (no script reads it, or config does not declare it)";
231 return role + " has produced no reading yet";
232 case Status::Stale:
233 return role + "'s last reading is older than " + std::to_string(kMaxAgeMs) + " ms";
235 return role +
236 " is publishing UNCALIBRATED values (a cubic model with no captured "
237 "points streams a plausible 0.0 PSI)";
238 }
239 return role + " cannot be read";
240}
241
242std::vector<std::string> PressureFeed::subscribedRoles() const {
243 std::lock_guard<std::mutex> lk(mtx_);
244 std::vector<std::string> out;
245 for (const auto& [id, role] : role_by_table_)
246 out.push_back(role);
247 return out;
248}
249
250} // namespace sequencer
Standalone Elodin database client.
Status
Why a role could not be read.
void start(const fsw::config::Config &cfg, const std::vector< std::string > &roles, const std::string &host, uint16_t port)
Subscribe to exactly the roles given, and nothing else.
std::vector< std::string > subscribedRoles() const
Roles this feed is subscribed to, for diagnostics.
std::atomic< bool > running_
Status read(const std::string &role, double &psi) const
std::map< std::pair< uint8_t, uint8_t >, std::string > role_by_table_
Elodin table id -> canonical role name, built once from config.
void runLoop(std::string host, uint16_t port)
static std::string explain(const std::string &role, Status s)
Human phrasing for a refusal, e.g.
std::map< std::string, Reading > readings_
static constexpr int kMaxAgeMs
How old a reading may be and still be acted on.
static const emmc_port_t port
Definition emmc_stm32.c:210
CommsMessage< uint64_t, uint8_t, std::array< uint8_t, 3 >, float, uint32_t, uint8_t > CalibratedPTMessage
Calibrated Pressure Transducer message for Elodin.
std::optional< PtRoleRef > find_pt_role(const Config &cfg, const std::string &role)
Resolve one role by its canonical config name.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
constexpr int kResubscribeMs
How often a table that has never delivered is asked for again.
constexpr int kRecvTimeoutMs
Long enough that the loop is not spinning, short enough that stop() is prompt.
void deserialize(const uint8_t *buffer)
The whole parsed config.toml, restricted to what C++ consumes.
Definition Config.hpp:264
static boot_report_t r
static SlugTables tables()
The tables every case validates against.