SatCat5
net_telemetry.cc
1 // Copyright 2023-2025 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <satcat5/eth_checksum.h>
7 #include <satcat5/log.h>
9 #include <satcat5/utils.h>
10 
11 // Start of conditional compilation...
12 #if SATCAT5_CBOR_ENABLE
13 
14 #include <qcbor/qcbor.h>
15 
16 // Set up basic shortcuts.
20 
21 // Thin wrappers for the Ethernet and UDP constructors.
22 // (Do this first, to avoid namespace conflicts later.)
25  bool concat_tiers)
26  : AddressContainer(eth)
27  , TelemetryAggregator(concat_tiers)
28  , TelemetrySink(this)
29 {
30  // Nothing else to initialize.
31 }
32 
35  const satcat5::eth::MacType& type)
36  : Protocol(satcat5::net::Type(type.value))
37  , satcat5::net::TelemetryRx()
38  , m_iface(iface)
39 {
40  m_iface->add(this);
41 }
42 
43 #if SATCAT5_ALLOW_DELETION
44 satcat5::eth::TelemetryRx::~TelemetryRx() {
45  m_iface->remove(this);
46 }
47 #endif
48 
50  telem_packet(src); // No extra headers, just message contents.
51 }
52 
55  bool concat_tiers)
56  : AddressContainer(udp)
57  , TelemetryAggregator(concat_tiers)
58  , TelemetrySink(this)
59 {
60  // Nothing else to initialize.
61 }
62 
65  const satcat5::udp::Port& port)
66  : Protocol(satcat5::net::Type(port.value))
67  , satcat5::net::TelemetryRx()
68  , m_iface(iface)
69 {
70  m_iface->add(this);
71 }
72 
73 #if SATCAT5_ALLOW_DELETION
74 satcat5::udp::TelemetryRx::~TelemetryRx() {
75  m_iface->remove(this);
76 }
77 #endif
78 
80  telem_packet(src); // No extra headers, just message contents.
81 }
82 
83 // Namespace conflicts resolved, configure remaining shortcuts.
94 
95 TelemetryAggregator::TelemetryAggregator(bool concat_tiers)
96  : m_tlm_concat(concat_tiers)
97 {
98  timer_every(100); // Default 100 msec = 10 Hz polling
99 }
100 
102  if (!m_tlm_concat) {
103  // Per-tier mode: create and send a TelemetryCbor for each tier.
104  TelemetryTier* tier = m_tiers.head();
105  while (tier) {
106  TelemetryCbor cbor;
107  tier->telem_poll(cbor);
108  telem_send(cbor, tier->m_tier_id);
109  tier = m_tiers.next(tier);
110  }
111  } else {
112  // In concatenated mode, send all accumulated data at the end.
113  TelemetryCbor cbor;
114  TelemetryTier* tier = m_tiers.head();
115  while (tier) {
116  tier->telem_poll(cbor);
117  tier = m_tiers.next(tier);
118  }
119  telem_send(cbor, 0);
120  }
121 }
122 
124  // Close out the TelemetryCbor object and reuse backing buffer without copy.
125  if (!cbor.close()) { return; }
126  UsefulBufC encoded = cbor.get_encoded(); // Zero copy
127 
128  // Don't bother sending an empty message.
129  // Note: Empty CBOR map {...} is exactly one byte.
130  if (encoded.len < 2) return;
131 
132  // Write data to each TelemetrySink object.
133  TelemetrySink* sink = m_sinks.head();
134  while (sink) {
135  sink->telem_ready(tier_id, encoded.len, encoded.ptr);
136  sink = m_sinks.next(sink);
137  }
138 }
139 
140 TelemetryKey::TelemetryKey(const char* label)
141  : key(label)
142  , hash(crc32(strlen(label), label))
143 {
144  // Nothing else to initialize.
145 }
146 
148  : TelemetryWatcher(rx)
149 {
150  if (kstr) m_filter = TelemetryKey(kstr).hash;
151 }
152 
154  : TelemetryWatcher(rx)
155 {
156  m_filter = key;
157 }
158 
160  u32 key, const QCBORItem& item,
161  QCBORDecodeContext* cbor)
162 {
163  // If a filter is configured, ignore non-matching keys.
164  if (m_filter && m_filter.value() != key) return;
165 
166  // Log the received key and value.
167  satcat5::log::Log(satcat5::log::INFO, "Telemetry")
169 }
170 
172  : TelemetrySink(src)
173  , m_dst(dst)
174 {
175  // Nothing else to initialize.
176 }
177 
178 void TelemetryLoopback::telem_ready(u32 tier_id, unsigned nbytes, const void* data) {
179  satcat5::io::ArrayRead rd(nbytes, data);
180  satcat5::io::LimitedRead lrd(&rd);
181  if (m_dst) m_dst->telem_packet(lrd);
182 }
183 
185  // Copy input data to a working buffer.
186  u8 buff[SATCAT5_QCBOR_BUFFER];
187  unsigned len = min_unsigned(sizeof(buff), src.get_read_ready());
188  src.read_bytes(len, buff);
189 
190  // Create QCBOR parser and confirm message contains a dictionary.
191  QCBORDecodeContext cbor;
192  QCBORDecode_Init(&cbor, {buff, len}, QCBOR_DECODE_MODE_NORMAL);
193  QCBORItem item;
194  QCBORDecode_EnterMap(&cbor, &item);
195  if (QCBORDecode_GetError(&cbor) != QCBOR_SUCCESS) return;
196 
197  // Iterate over the dictionary contents...
198  while (1) {
199  // Peek at the next item. If it's an array or map, enter it now.
200  // Otherwise, read and consume self-contained items with GetNext().
201  int errcode = QCBORDecode_PeekNext(&cbor, &item);
202  if (errcode || item.uNestingLevel < 1) break;
203  if (item.uDataType == QCBOR_TYPE_ARRAY) {
204  QCBORDecode_EnterArray(&cbor, &item);
205  telem_item(&cbor, item);
206  QCBORDecode_ExitArray(&cbor);
207  } else if (item.uDataType == QCBOR_TYPE_MAP) {
208  QCBORDecode_EnterMap(&cbor, &item);
209  telem_item(&cbor, item);
210  QCBORDecode_ExitMap(&cbor);
211  } else {
212  QCBORDecode_GetNext(&cbor, &item);
213  telem_item(nullptr, item);
214  }
215  }
216 }
217 
218 void TelemetryRx::telem_item(QCBORDecodeContext* cbor, const QCBORItem& item) {
219  // Ignore items at the wrong nesting level.
220  if (item.uNestingLevel > 1) return;
221 
222  // Determine the integer key for this object.
223  u32 key = u32(item.label.int64);
224  if (item.uLabelType == QCBOR_TYPE_BYTE_STRING ||
225  item.uLabelType == QCBOR_TYPE_TEXT_STRING) {
226  key = crc32(item.label.string.len, item.label.string.ptr);
227  }
228 
229  // Notify each registered TelemetryWatcher.
230  // Complex data-structures provide the "cbor" pointer for further parsing.
231  // To prevent side-effects with multiple watchers, rewind after each call.
232  TelemetryWatcher* callback = m_watchers.head();
233  while (callback) {
234  callback->telem_rcvd(key, item, cbor);
235  callback = m_watchers.next(callback);
236  if (cbor) QCBORDecode_Rewind(cbor);
237  }
238 }
239 
241  : m_tlm(tlm)
242  , m_next(0)
243 {
244  // Add ourselves to the parent's sink list.
245  m_tlm->m_sinks.add(this);
246 }
247 
248 #if SATCAT5_ALLOW_DELETION
249 TelemetrySink::~TelemetrySink() {
250  // Remove ourselves from the parent's list.
251  m_tlm->m_sinks.remove(this);
252 }
253 #endif // SATCAT5_ALLOW_DELETION
254 
256  TelemetryAggregator* tlm,
257  TelemetrySource* src,
258  u32 tier_id,
259  unsigned interval_msec)
260  : m_tier_id(tier_id)
261  , m_next(0)
262  , m_tlm(tlm)
263  , m_src(src)
264  , m_time_interval(0)
265  , m_time_count(0)
266 {
267  // Configure timer state.
268  set_interval(interval_msec);
269 
270  // Add ourselves to the parent's tier list.
271  m_tlm->m_tiers.add(this);
272 }
273 
274 #if SATCAT5_ALLOW_DELETION
275 TelemetryTier::~TelemetryTier() {
276  // Remove ourselves from the parent's list.
277  m_tlm->m_tiers.remove(this);
278 }
279 #endif
280 
282  // Immediately gather data, then send it.
283  TelemetryCbor cbor;
284  m_src->telem_event(m_tier_id, cbor);
285  m_tlm->telem_send(cbor, m_tier_id);
286 }
287 
288 void TelemetryTier::set_interval(unsigned interval_msec) {
289  // Update internal time interval.
290  m_time_interval = interval_msec;
291 
292  // No further action if we're shutting down (interval = 0).
293  if (!interval_msec) return;
294 
295  // Update the parent's polling interval?
296  if (m_tlm->timer_interval() > m_time_interval)
297  m_tlm->timer_every(m_time_interval);
298 
299  // If user disables and re-enables a given timer, we want to maintain
300  // continuity so the next event happens when it would have originally.
301  // (This helps ensure once-per-second events stay aligned, for example.)
302  m_time_count = m_time_count % m_time_interval;
303 }
304 
305 void TelemetryTier::telem_poll(TelemetryCbor& cbor) {
306  // Always increment the time since last event.
307  m_time_count += m_tlm->timer_interval();
308 
309  // Elapsed time since the last polling event?
310  if (m_time_interval > 0 && m_time_count >= m_time_interval) {
311  m_time_count -= m_time_interval;
312  m_src->telem_event(m_tier_id, cbor);
313  }
314 }
315 
317  : m_rx(rx)
318  , m_next(nullptr)
319 {
320  m_rx->add_watcher(this);
321 }
322 
323 #if SATCAT5_ALLOW_DELETION
324 TelemetryWatcher::~TelemetryWatcher() {
325  m_rx->remove_watcher(this);
326 }
327 #endif
328 
329 #endif // SATCAT5_CBOR_ENABLE
bool close()
Close the QCBOR encoder by calling QCBOREncode_Finish and copy the resulting bytes to m_dst,...
Definition: io_cbor.cc:34
UsefulBufC get_encoded()
Get encoded data as a UsefulBuf, useful if no dst was given.
Definition: io_cbor.cc:51
Inheritable container for a eth::Address.
Definition: eth_address.h:60
Implemention of "net::Dispatch" for Ethernet frames.
Definition: eth_dispatch.h:21
Accept incoming Ethernet packets by EtherType.
Definition: eth_protocol.h:24
Telemetry(satcat5::eth::Dispatch *eth, bool concat_tiers=true)
Link this object to a network interface.
Thin wrapper for receiving CBOR telemetry over raw-Ethernet.
void frame_rcvd(satcat5::io::LimitedRead &src)
Dispatch calls frame_rcvd(...) for each incoming frame with with a matching net::Type value.
Ephemeral Readable interface for a simple array.
Definition: io_readable.h:206
Limited read of next N bytes.
Definition: io_readable.h:255
bool read_bytes(unsigned nbytes, void *dst) override
Read 0 or more bytes into a buffer.
Definition: io_readable.cc:296
unsigned get_read_ready() const override
How many bytes can be read without blocking?
Definition: io_readable.cc:293
The Log class creates and formats one log message.
Definition: log.h:195
Log & write_obj(const T &obj)
Templated wrapper for custom output formatting.
Definition: log.h:251
void add(satcat5::net::Protocol *proto)
Register a Protocol object.
Definition: net_dispatch.h:55
Protocol-agnostic handler for one or more TelemetryTier objects.
void telem_send(TelemetryCbor &cbor, u32 tier_id)
Send data to all attached TelemetrySink objects.
void timer_event() override
Child class MUST override this method.
unsigned timer_interval() const
Query the polling interval for this aggregator.
Legacy ephemeral wrapper class for the CBOR encoder.
Example TelemetryWatcher that logs received key/value pairs.
TelemetryLogger(satcat5::net::TelemetryRx *rx, const char *kstr=nullptr)
Constructor for string keys, or default null = no filter.
void telem_rcvd(u32 key, const QCBORItem &item, QCBORDecodeContext *cbor) override
Callback for each received key/value pair.
Loopback adapter for telemetry messages.
TelemetryLoopback(satcat5::net::TelemetryAggregator *src, satcat5::net::TelemetryRx *dst)
Link source and destination interfaces.
void telem_ready(u32 tier_id, unsigned nbytes, const void *data) override
Carbon-copy outgoing messages to the designated interface.
Parse incoming CBOR telemetry and notify TelemetryWatcher callbacks.
void telem_item(QCBORDecodeContext *cbor, const QCBORItem &item)
Internal callback delivers one item to each watcher.
satcat5::util::List< satcat5::net::TelemetryWatcher > m_watchers
Linked list of registered callback objects.
void telem_packet(satcat5::io::LimitedRead &src)
The child object MUST call this method for each received message.
void remove_watcher(TelemetryWatcher *callback)
Manage the list of registered callback objects.
TelemetryRx()
Constructor is only accessible to the child object.
void add_watcher(TelemetryWatcher *callback)
Manage the list of registered callback objects.
User data sinks must inherit from the TelemetrySink class.
satcat5::net::TelemetryAggregator *const m_tlm
Pointer to the parent object.
TelemetrySink(satcat5::net::TelemetryAggregator *tlm)
Only children can safely access constructor/destructor.
virtual void telem_ready(u32 tier_id, unsigned nbytes, const void *data)=0
This method is called for each outgoing telemetry message.
User data sources must inherit from the TelemetrySource class.
virtual void telem_event(u32 tier_id, satcat5::net::TelemetryCbor &cbor)=0
User method for writing each telemetry message.
Rate control for a particular telemetry "tier".
void set_interval(unsigned interval_msec)
Set the reporting interval for this tier, or zero to disable.
void send_now()
Immediately send a message at this tier.
TelemetryTier(satcat5::net::TelemetryAggregator *tlm, satcat5::net::TelemetrySource *src, u32 tier_id, unsigned interval_msec=0)
Constructor is typically called by the TelemetrySource.
const u32 m_tier_id
Tier-ID for this object.
Callback API for incoming telemetry items.
satcat5::net::TelemetryRx *const m_rx
Pointer to the receive-and-decode object.
virtual void telem_rcvd(u32 key, const QCBORItem &item, QCBORDecodeContext *cbor)=0
Callback for each received key/value pair.
TelemetryWatcher(satcat5::net::TelemetryRx *rx)
Constructor and destructor are accessible to the child only.
void timer_every(unsigned msec)
Configure a repeating notification every X milliseconds.
Definition: polling.cc:321
Inheritable container for a udp::Address.
Definition: udp_core.h:149
Dispatcher sorts incoming UDP messages by port index.
Definition: udp_dispatch.h:20
Telemetry(satcat5::udp::Dispatch *udp, bool concat_tiers=true)
Link this object to a network interface.
Thin wrapper for receiving CBOR telemetry over UDP.
void frame_rcvd(satcat5::io::LimitedRead &src)
Dispatch calls frame_rcvd(...) for each incoming frame with with a matching net::Type value.
T * next(const T *item) const
Fetch pointer to the next item.
Definition: list.h:261
void add(T *item)
Add new item to front or back, whichever is simpler.
Definition: list.h:221
void remove(T *item)
Remove the designated item from the list.
Definition: list.h:277
Inline Ethernet Checksum insertion and verification.
u32 crc32(unsigned nbytes, const void *data)
Directly calculate CRC32 on a block of data.
#define SATCAT5_QCBOR_BUFFER
Set the default size for the QCBOR buffer.
Definition: io_cbor.h:52
Diagnostic logging to UART and/or Ethernet ports.
State-of-health telemetry using QCBOR.
Helper object for logging contents a QCBOR item.
Definition: io_cbor.h:828
EtherType field (uint16) is used a protocol-ID [1536..65535].
Definition: eth_header.h:96
UDP and TCP ports are both 16-bit unsigned integers.
Definition: ip_core.h:119
String constant, plus the CRC32 hash of that string.
const u32 hash
CRC32 of that string.
T value() const
Fetch the inner value.
Definition: utils.h:64
Miscellaneous mathematical utility functions.
constexpr unsigned min_unsigned(unsigned a, unsigned b)
Min and max functions.
Definition: utils.h:111