STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
ElodinClient.hpp
Go to the documentation of this file.
1#ifndef DAQ_ELODIN_CLIENT_HPP
2#define DAQ_ELODIN_CLIENT_HPP
3
4#include <arpa/inet.h> // for htonl
5
6#include <array>
7#include <cstdint>
8#include <cstring>
9#include <iomanip>
10#include <iostream>
11#include <memory>
12#include <mutex>
13#include <string>
14#include <unordered_map>
15#include <vector>
16
17#include "ElodinProtocol.hpp"
20
21namespace fsw {
22namespace elodin {
23
24/**
25 * @brief Standalone Elodin database client
26 *
27 * No FSW dependencies - uses our own TCP client and protocol implementation.
28 */
30public:
33
34 /**
35 * @brief Connect to Elodin database
36 * @param host Database host address
37 * @param port Database port (default 2240)
38 * @return true if connection successful, false otherwise
39 */
40 bool connect(const std::string& host, uint16_t port = 2240);
41
42 /**
43 * @brief Disconnect from database
44 */
45 void disconnect();
46
47 /**
48 * @brief Check if connected
49 */
50 bool is_connected() const;
51
52 /**
53 * @brief Publish a message to a table
54 * @tparam MessageType CommsMessage type
55 * @param table_id Table identifier (2-byte array)
56 * @param message Message to publish
57 * @return true if published successfully
58 */
59 template <typename MessageType>
60 bool publish(uint16_t message_id, const MessageType& message);
61
62 // Legacy overload for array-based packet_id (converts to uint16_t)
63 template <typename MessageType>
64 bool publish(const std::array<uint8_t, 2>& table_id, const MessageType& message) {
65 uint16_t msg_id = (static_cast<uint16_t>(table_id[0]) << 8) | table_id[1];
66 return publish(msg_id, message);
67 }
68
69 /**
70 * @brief Subscribe to specific tables by ID.
71 *
72 * There is deliberately no "subscribe to everything" call. There used to be —
73 * subscribe_stream(), documented as "subscribe to all stream data" — but it was really the
74 * calibration service's table list under a general name: 480 raw sensor ids plus the
75 * calibration command table. heartbeat_service called it expecting everything, received 481
76 * tables it discarded, and never received the one table it reads, so its engine_state sat at
77 * 0 forever. Elodin has no wildcard; every subscriber must name what it consumes.
78 *
79 * See raw_sensor_tables() / calibrated_sensor_tables() in DatabaseConfig.hpp for the sensor
80 * id sets, and kTableSequencerState / kTableCalibrationCommand for the non-sensor ones.
81 *
82 * @param table_ids Vector of (hi, lo) byte pairs identifying each table.
83 * @return false if any subscribe message failed to send.
84 */
85 bool subscribe_tables(const std::vector<std::pair<uint8_t, uint8_t>>& table_ids);
86 /** One table, under `request_id`, which the DB echoes if it refuses it (ErrorResponse
87 * [0xE0, 0x1D]). subscribe_tables() sends 0 for all, so a refusal there names nothing. */
88 bool subscribe_table(uint8_t hi, uint8_t lo, uint8_t request_id);
89
90 /**
91 * @brief Begin batching — subsequent publish() calls go into an internal buffer.
92 * Call flush_batch() to send everything in one TCP write.
93 */
94 void begin_batch();
95
96 /**
97 * @brief Flush (send) the accumulated batch in a single TCP write.
98 * @return true if the write succeeded (or batch was empty).
99 */
100 bool flush_batch();
101
102 /**
103 * @brief Returns true if we're between begin_batch() and flush_batch().
104 */
105 bool is_batching() const {
106 return batching_;
107 }
108
109 /**
110 * @brief Attempt to reconnect using the last-used host/port.
111 * @return true if reconnect succeeds.
112 */
113 bool reconnect();
114
115 /**
116 * @brief Register a table ID with a name (for debugging/logging)
117 */
118 void register_table(const std::array<uint8_t, 2>& table_id, const std::string& name);
119
120 /**
121 * @brief Send a MSG packet (for VTable registration, etc.)
122 * @param packet_id Packet identifier (2-byte array)
123 * @param data Raw message data (already encoded with PacketHeader)
124 * @return true if sent successfully
125 */
126 bool send_msg(const std::array<uint8_t, 2>& packet_id, const std::vector<uint8_t>& data);
127
128 /**
129 * @brief Get last error message
130 */
131 std::string last_error() const;
132
133 /**
134 * @brief Flush the write buffer to the database
135 */
136 void flush_buffer();
137
138 /**
139 * @brief Set socket receive timeout so read_packet() can yield.
140 * 0 = blocking (default). Call after connect().
141 */
142 void set_recv_timeout_ms(int timeout_ms);
143
144 /**
145 * @brief Read incoming data packet from Elodin (non-blocking)
146 * @param buffer Buffer to read into
147 * @param max_len Maximum bytes to read
148 * @return Number of bytes read, 0 if no data available, -1 on error
149 */
150 ssize_t read_data(void* buffer, size_t max_len);
151
152 /**
153 * @brief Read packet header from Elodin (12 bytes)
154 * @param header_buffer Buffer to read header into (must be at least 12 bytes)
155 * @return true if header read successfully, false otherwise
156 */
157 bool read_packet_header(uint8_t* header_buffer);
158
159 /**
160 * @brief Blocking read of exactly len bytes (for payload reads after header).
161 * Unlike read_data (non-blocking), this waits until all bytes arrive.
162 * Use after read_packet_header to avoid packet misalignment.
163 * @return true if all bytes were read, false on error or disconnect
164 */
165 bool read_bytes_exact(uint8_t* buffer, size_t len);
166
167 /**
168 * @brief Read complete packet from Elodin (header + payload)
169 * @param packet_buffer Buffer to read packet into
170 * @param max_len Maximum buffer size
171 * @return Packet length if successful, 0 if no data, -1 on error
172 */
173 ssize_t read_packet(uint8_t* packet_buffer, size_t max_len);
174
175 /**
176 * @brief Serialize a message for Elodin (public for testing)
177 */
178 template <typename MessageType>
179 std::vector<uint8_t> serialize_msg(uint16_t message_id, const MessageType& msg);
180
181private:
182 // connect/reconnect/disconnect tear down and replace the socket that publish() writes to, so
183 // they take publish_mutex_ like every publish does. The _locked helpers exist because
184 // connect() must call disconnect() while already holding it (std::mutex is not recursive).
185 // Without this a service that publishes from one thread while retrying the connection from
186 // another — sequencer_service does exactly that — races on the socket.
187 bool connect_locked(const std::string& host, uint16_t port);
188 void disconnect_locked();
189
190 std::unique_ptr<daq_comms::transport::TCPClient> socket_;
191 std::mutex publish_mutex_;
192 std::unordered_map<uint16_t, std::string> table_names_;
193 std::string last_error_;
194
195 // Batch accumulation buffer
196 bool batching_ = false;
197 std::vector<uint8_t> batch_buffer_;
198
199 // For reconnection
200 std::string last_host_;
201 uint16_t last_port_ = 0;
202};
203
204// Convert uint16_t message_id to array<uint8_t, 2> for Elodin protocol
205inline std::array<uint8_t, 2> message_id_to_packet_id(uint16_t message_id) {
206 return {static_cast<uint8_t>((message_id >> 8) & 0xFF),
207 static_cast<uint8_t>(message_id & 0xFF)};
208}
209
210// Helper function matching serialize_msg pattern
211// CRITICAL: FSW uses NESTED MessageFactory structure:
212// Header = MessageFactory<len, type, packet_id, request_id>
213// ElodinMsg = MessageFactory<header, body>
214// When serialized, MessageFactory does memcpy of fields in order (no byte order conversion!)
215template <typename MessageType>
216std::vector<uint8_t> ElodinClient::serialize_msg(uint16_t message_id, const MessageType& msg) {
217 std::array<uint8_t, 2> packet_id = message_id_to_packet_id(message_id);
218 constexpr size_t msg_size = MessageSize<MessageType>::value;
219
220 // Match FSW's serialize_msg() EXACTLY:
221 // Header: len (uint32_t), type (PacketType), packet_id (array<uint8_t,2>), request_id (uint8_t)
222 // ElodinMsg: header (Header), body (MessageType)
223 //
224 // FSW's MessageFactory serialization just does memcpy of fields in order
225 // NO network byte order conversion happens - Elodin expects host byte order!
226
227 using Header =
228 comms::CommsMessage<uint32_t, // len (4 bytes) - NOT converted to network byte order!
229 PacketType, // type (1 byte)
230 std::array<uint8_t, 2>, // packet_id (2 bytes)
231 uint8_t>; // request_id (1 byte)
232
234
235 // Construct header: len = msg_size + 4 (matching FSW: msg.nbytes() + 4)
236 // The +4 accounts for: type(1) + packet_id(2) + request_id(1)
237 // The len field itself (4 bytes) is NOT included in this calculation
238 // FSW uses PacketType::TABLE for data messages - match exactly
239 Header header(msg_size + 4, PacketType::TABLE, packet_id, 0);
240
241 // Construct nested message (matching FSW's ElodinMsg(header, msg))
242 ElodinMsg elodin_msg(header, msg);
243
244 // Serialize the nested structure (CommsMessage recursively serializes Header, then Body)
245 // FSW's MessageFactory serialization uses Serializer::write() which does memcpy (host byte
246 // order) Elodin expects host byte order (matching FSW's behavior) - NO network byte order
247 // conversion!
248 constexpr size_t header_size = 8; // len(4) + ty(1) + id(2) + req(1)
249 std::vector<uint8_t> result(header_size + msg_size);
250 elodin_msg.serialize(reinterpret_cast<uint8_t*>(result.data()));
251
252 // FSW's MessageFactory does memcpy (host byte order) and Elodin accepts it
253 // So we should NOT convert to network byte order - send as-is!
254
255 return result;
256}
257
258// Template implementation for publishing
259template <typename MessageType>
260bool ElodinClient::publish(uint16_t message_id, const MessageType& message) {
261 if (!is_connected()) {
262 last_error_ = "Not connected to Elodin database";
263 return false;
264 }
265
266 std::lock_guard<std::mutex> lock(publish_mutex_);
267
268 try {
269 auto buf = serialize_msg(message_id, message);
270
271 if (batching_) {
272 // Append to batch buffer — will be flushed in flush_batch()
273 batch_buffer_.insert(batch_buffer_.end(), buf.begin(), buf.end());
274 return true;
275 }
276
277 // Direct write (non-batched path)
278 if (!socket_->write_all(buf.data(), buf.size())) {
279 last_error_ = socket_->last_error();
280 return false;
281 }
282
283 return true;
284 } catch (const std::exception& e) {
285 last_error_ = "Publish error: " + std::string(e.what());
286 return false;
287 }
288}
289
290} // namespace elodin
291} // namespace fsw
292
293#endif // DAQ_ELODIN_CLIENT_HPP
bool flush_batch()
Flush (send) the accumulated batch in a single TCP write.
bool publish(const std::array< uint8_t, 2 > &table_id, const MessageType &message)
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.
bool publish(uint16_t message_id, const MessageType &message)
Publish a message to a table.
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.
bool is_batching() const
Returns true if we're between begin_batch() and flush_batch().
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).
std::vector< uint8_t > serialize_msg(uint16_t message_id, const MessageType &msg)
Serialize a message for Elodin (public for testing).
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
json_pointer result
Definition json.hpp:14064
static osMutexId_t lock
Definition mode.c:11
std::array< uint8_t, 2 > message_id_to_packet_id(uint16_t message_id)
std::array< uint8_t, 2 > msg_id(std::string_view str)
Motorized ball valve position: raw ADC counts -> % open.
static constexpr std::size_t value
CommsMessage - Template-based message serialization.