STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
ElodinClient.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstring>
5#include <iostream>
6
7namespace fsw {
8namespace elodin {
9
11 socket_ = std::make_unique<daq_comms::transport::TCPClient>();
12}
13
17
18bool ElodinClient::connect(const std::string& host, uint16_t port) {
19 std::lock_guard<std::mutex> lock(publish_mutex_);
20 return connect_locked(host, port);
21}
22
23bool ElodinClient::connect_locked(const std::string& host, uint16_t port) {
25
26 std::cout << "[ElodinClient] Connecting to Elodin database at " << host << ":" << port << "..."
27 << std::endl;
28
29 // Remember the target BEFORE attempting, not after succeeding. reconnect() keys off these, so
30 // recording them only on success meant a service whose FIRST connect failed could never retry
31 // ("No previous connection to reconnect to") and stayed dead for the life of the process.
32 last_host_ = host;
34
35 if (!socket_->connect(host, port)) {
36 last_error_ = socket_->last_error();
37 std::cerr << "[ElodinClient] ❌ Connection failed: " << last_error_ << std::endl;
38 return false;
39 }
40
41 std::cout << "[ElodinClient] ✅ Connected to Elodin database at " << host << ":" << port
42 << std::endl;
43 return true;
44}
45
47 std::lock_guard<std::mutex> lock(publish_mutex_);
48 if (last_host_.empty() || last_port_ == 0) {
49 last_error_ = "No previous connection to reconnect to";
50 return false;
51 }
52 std::cout << "[ElodinClient] Attempting reconnect to " << last_host_ << ":" << last_port_
53 << "..." << std::endl;
55}
56
58 std::lock_guard<std::mutex> lock(publish_mutex_);
60}
61
63 if (socket_ && socket_->is_connected()) {
64 std::cout << "[ElodinClient] Disconnecting from Elodin database..." << std::endl;
65 socket_->flush();
66 socket_->disconnect();
67 std::cout << "[ElodinClient] ✅ Disconnected" << std::endl;
68 }
69}
70
72 return socket_ && socket_->is_connected();
73}
74
75void ElodinClient::register_table(const std::array<uint8_t, 2>& table_id, const std::string& name) {
76 uint16_t id = (static_cast<uint16_t>(table_id[0]) << 8) | table_id[1];
77 table_names_[id] = name;
78}
79
80std::string ElodinClient::last_error() const {
81 return last_error_;
82}
83
85 if (socket_ && socket_->is_connected()) {
86 socket_->flush();
87 }
88}
89
91 if (socket_)
92 socket_->set_recv_timeout_ms(timeout_ms);
93}
94
95bool ElodinClient::subscribe_tables(const std::vector<std::pair<uint8_t, uint8_t>>& table_ids) {
96 bool ok = true;
97 for (const auto& [hi, lo] : table_ids)
98 // Was fire-and-forget returning true unconditionally, which made every
99 // `if (!subscribe_...())` error path in the callers dead code.
100 if (!subscribe_table(hi, lo, 0))
101 ok = false;
102 return ok;
103}
104
105bool ElodinClient::subscribe_table(uint8_t hi, uint8_t lo, uint8_t request_id) {
106 std::array<uint8_t, 2> msgstream_id = {0x11, 0x0d};
107 std::vector<uint8_t> data(10, 0x00);
108 uint32_t len = 2 + 4;
109 std::memcpy(data.data(), &len, 4);
110 data[4] = static_cast<uint8_t>(fsw::elodin::PacketType::MSG);
111 data[5] = msgstream_id[0];
112 data[6] = msgstream_id[1];
113 // The DB echoes this in an ErrorResponse, which is the only way to tell which subscription it
114 // refused (SubscriptionTracker).
115 data[7] = request_id;
116 data[8] = hi;
117 data[9] = lo;
118 return send_msg(msgstream_id, data);
119}
120
122 batching_ = true;
123 batch_buffer_.clear();
124 batch_buffer_.reserve(4096); // pre-allocate ~4KB
125}
126
128 batching_ = false;
129 if (batch_buffer_.empty()) {
130 return true; // nothing to send
131 }
132 if (!is_connected()) {
133 last_error_ = "Not connected to Elodin database";
134 batch_buffer_.clear();
135 return false;
136 }
137
138 std::lock_guard<std::mutex> lock(publish_mutex_);
139 bool ok = socket_->write_all(batch_buffer_.data(), batch_buffer_.size());
140 if (!ok) {
141 last_error_ = socket_->last_error();
142 }
143 batch_buffer_.clear();
144 return ok;
145}
146
147ssize_t ElodinClient::read_data(void* buffer, size_t max_len) {
148 if (!is_connected()) {
149 last_error_ = "Not connected to Elodin database";
150 return -1;
151 }
152
153 std::lock_guard<std::mutex> lock(publish_mutex_);
154 return socket_->read(buffer, max_len);
155}
156
157bool ElodinClient::read_packet_header(uint8_t* header_buffer) {
158 if (!is_connected()) {
159 last_error_ = "Not connected to Elodin database";
160 return false;
161 }
162
163 std::lock_guard<std::mutex> lock(publish_mutex_);
164 return socket_->read_exact(header_buffer, 8);
165}
166
167bool ElodinClient::read_bytes_exact(uint8_t* buffer, size_t len) {
168 if (!is_connected()) {
169 last_error_ = "Not connected to Elodin database";
170 return false;
171 }
172 std::lock_guard<std::mutex> lock(publish_mutex_);
173 return socket_->read_exact(buffer, len);
174}
175
176ssize_t ElodinClient::read_packet(uint8_t* packet_buffer, size_t max_len) {
177 if (!is_connected()) {
178 last_error_ = "Not connected to Elodin database";
179 return -1;
180 }
181
182 if (max_len < 8) {
183 last_error_ = "Buffer too small for packet header";
184 return -1;
185 }
186
187 std::lock_guard<std::mutex> lock(publish_mutex_);
188
189 // Read packet header (8 bytes)
190 if (!socket_->read_exact(packet_buffer, 8)) {
191 last_error_ = socket_->last_error();
192 // SO_RCVTIMEO fired — yield without treating as a connection error
193 if (last_error_ == "TIMEOUT")
194 return 0;
195 return -1;
196 }
197
198 // Header layout: len(4) | ty(1)@4 | packet_id(2 BE)@5 | request_id(1)@7. Only the length is
199 // consumed — nothing here matches a reply to the request that asked for it, and the other three
200 // fields were decoded into locals that were never read (GCC -Wunused-variable, cppcheck
201 // unreadVariable). The layout stays documented above rather than in dead assignments.
202 // memcpy, not a uint32_t* cast: packet_buffer is caller-supplied and carries no alignment
203 // guarantee.
204 uint32_t packet_len;
205 std::memcpy(&packet_len, packet_buffer, sizeof(packet_len));
206
207 // A malformed length is unrecoverable — there is no way to know where the next packet starts.
208 if (packet_len < 4) {
209 last_error_ = "Invalid packet length: " + std::to_string(packet_len);
210 socket_->disconnect();
211 return -1;
212 }
213
214 // Read payload (packet_len - 4 bytes)
215 size_t payload_len = packet_len - 4;
216
217 // Note the bound: this writes 8 + (packet_len - 4) = packet_len + 4 bytes, so the check has to
218 // be against max_len - 4. It used to be `packet_len > max_len`, which let packet_len == max_len
219 // through and overran the caller's buffer by 4 bytes (confirmed with a guard page: the kernel
220 // returned EFAULT at exactly that boundary). max_len >= 8 is already guaranteed above.
221 if (packet_len + 4 > max_len) {
222 // Drain and skip rather than abandoning the body in the socket. The old code returned -1
223 // here having consumed the 8-byte header but not the payload, so every subsequent read
224 // parsed payload bytes as a header — the connection was silently and permanently dead
225 // while is_connected() still reported true and the caller looped. Losing one packet is a
226 // recoverable cost; losing stream framing is not.
227 std::cerr << "[ElodinClient] packet " << packet_len << "B exceeds " << max_len
228 << "B buffer — skipping it" << std::endl;
229 uint8_t scratch[1024];
230 size_t remaining = payload_len;
231 while (remaining > 0) {
232 const size_t chunk = std::min(remaining, sizeof(scratch));
233 if (!socket_->read_exact(scratch, chunk)) {
234 // Could not resynchronize; the stream is unusable, so force a reconnect.
235 last_error_ = "Failed draining oversized packet: " + socket_->last_error();
236 socket_->disconnect();
237 return -1;
238 }
239 remaining -= chunk;
240 }
241 last_error_ = "Skipped oversized packet: " + std::to_string(packet_len);
242 return 0; // "nothing usable this round" — what every caller already does on 0
243 }
244
245 if (payload_len > 0) {
246 if (!socket_->read_exact(packet_buffer + 8, payload_len)) {
247 last_error_ = socket_->last_error();
248 return -1;
249 }
250 }
251
252 return static_cast<ssize_t>(packet_len + 4);
253}
254
255bool ElodinClient::send_msg(const std::array<uint8_t, 2>& /* packet_id */,
256 const std::vector<uint8_t>& data) {
257 if (!is_connected()) {
258 last_error_ = "Not connected to Elodin database";
259 return false;
260 }
261
262 std::lock_guard<std::mutex> lock(publish_mutex_);
263
264 try {
265 // Match FSW exactly: Socket::write() writes directly, no buffering
266 // FSW: LocalSock->write(buf.data(), buf.size());
267 // Our equivalent: write_all() which calls send() directly
268 if (!socket_->write_all(data.data(), data.size())) {
269 last_error_ = socket_->last_error();
270 std::cerr << "[ElodinClient] ERROR: write_all failed: " << last_error_ << "\n";
271 return false;
272 }
273
274 static size_t send_count = 0;
275 send_count++;
276 std::cout << "[ElodinClient] ✅ Sent registration message #" << send_count
277 << ", size=" << data.size() << " bytes" << std::endl;
278
279 return true;
280 } catch (const std::exception& e) {
281 last_error_ = "Send MSG error: " + std::string(e.what());
282 std::cerr << "[ElodinClient] ERROR sending registration: " << last_error_ << "\n";
283 return false;
284 }
285}
286
287} // namespace elodin
288} // namespace fsw
static uint8_t chunk[64]
Definition board_uart9.c:12
bool flush_batch()
Flush (send) the accumulated batch in a single TCP write.
bool is_connected() const
Check if connected.
bool send_msg(const std::array< uint8_t, 2 > &packet_id, const std::vector< uint8_t > &data)
Send a MSG packet (for VTable registration, etc.).
bool read_bytes_exact(uint8_t *buffer, size_t len)
Blocking read of exactly len bytes (for payload reads after header).
ssize_t read_data(void *buffer, size_t max_len)
Read incoming data packet from Elodin (non-blocking).
bool connect_locked(const std::string &host, uint16_t port)
std::string last_error() const
Get last error message.
void set_recv_timeout_ms(int timeout_ms)
Set socket receive timeout so read_packet() can yield.
void disconnect()
Disconnect from database.
bool subscribe_tables(const std::vector< std::pair< uint8_t, uint8_t > > &table_ids)
Subscribe to specific tables by ID.
std::unique_ptr< daq_comms::transport::TCPClient > socket_
void begin_batch()
Begin batching — subsequent publish() calls go into an internal buffer.
void register_table(const std::array< uint8_t, 2 > &table_id, const std::string &name)
Register a table ID with a name (for debugging/logging).
bool connect(const std::string &host, uint16_t port=2240)
Connect to Elodin database.
ssize_t read_packet(uint8_t *packet_buffer, size_t max_len)
Read complete packet from Elodin (header + payload).
std::vector< uint8_t > batch_buffer_
bool subscribe_table(uint8_t hi, uint8_t lo, uint8_t request_id)
One table, under request_id, which the DB echoes if it refuses it (ErrorResponse [0xE0,...
void flush_buffer()
Flush the write buffer to the database.
bool read_packet_header(uint8_t *header_buffer)
Read packet header from Elodin (12 bytes).
bool reconnect()
Attempt to reconnect using the last-used host/port.
std::unordered_map< uint16_t, std::string > table_names_
static const emmc_port_t port
Definition emmc_stm32.c:210
static bool ok
Definition flight_e2e.c:435
static osMutexId_t lock
Definition mode.c:11
Motorized ball valve position: raw ADC counts -> % open.
static uint8_t scratch[BS]