Skip to main content

sedsnet/
lib.rs

1// std on host/tests; no_std when the `std` feature is OFF
2
3// Dear programmer:
4// When I wrote this code, only god and I knew how it worked.
5// Now, only god knows it!
6// Therefore, if you are trying to optimize
7// this routine, and it fails (it most surely will),
8// please increase this counter as a warning for the next person:
9// total hours wasted on this project = 3154
10
11#![cfg_attr(not(feature = "std"), no_std)]
12#![allow(unused_doc_comments)]
13//! SEDSnet is a compact networking stack for embedded and host telemetry systems.
14//!
15//! It provides runtime schema registration, compact packet packing, discovery-driven routing,
16//! reliable delivery, managed state synchronization, time synchronization, P2P service ports and
17//! streams, optional E2E payload cryptography, and C/Python bindings.
18//!
19//! Most user-facing APIs live in:
20//! - [`config`]: schema + data type/endpoint configuration.
21//! - [`router`]: routers, relays, sides, discovery, routing policy, P2P, and managed variables.
22//! - [`packet`]: `Packet` and friends.
23//! - [`wire_format`]: packet packing, unpacking, and wire inspection helpers.
24//!
25//! Version 4.0.0 highlights:
26//! - Telemetry endpoints and data types are runtime IDs with process-local registry metadata.
27//! - The build no longer reads `telemetry_config.json` or generates schema-specific Rust enums.
28//! - A JSON schema can still seed the runtime registry with `SEDSNET_STATIC_SCHEMA_PATH`.
29//! - Nodes can export known endpoints/types, sync schemas through discovery, and register new
30//!   schema entries over time.
31//! - Discovery assigns compact node addresses and hostnames for P2P service traffic while
32//!   broadcast endpoint telemetry continues to use endpoint subscriptions.
33
34extern crate alloc;
35
36extern crate core;
37#[cfg(feature = "std")]
38extern crate std;
39#[cfg(feature = "std")]
40use std::io::Error;
41
42use crate::config::{
43    DataEndpoint, DataType, get_endpoint_meta, get_message_meta, max_data_type_id, max_endpoint_id,
44    runtime_static_hex_length, runtime_static_string_length,
45};
46use crate::macros::{ReprI32Enum, ReprU32Enum};
47use alloc::sync::Arc;
48use core::fmt::Formatter;
49use core::mem::size_of;
50use core::ops::Mul;
51
52// ============================================================================
53//  Test / Python FFI modules (std-only)
54// ============================================================================
55
56#[cfg(all(test, feature = "std"))]
57mod tests;
58
59#[cfg(feature = "python")]
60#[cfg(feature = "std")]
61mod python_api;
62
63// ============================================================================
64//  Allocator & panic handlers (embedded no_std)
65// ============================================================================
66//
67// For EMBEDDED builds (no_std + bare-metal target), provide Telemetry allocator
68// + panic handler. Host builds rely on the system allocator / default panic.
69
70#[cfg(all(not(feature = "std"), target_os = "none"))]
71unsafe extern "C" {
72    fn seds_error_msg(msg: *const u8, len: usize);
73}
74
75#[cfg(all(not(feature = "std"), target_os = "none"))]
76mod embedded_alloc {
77    use core::alloc::{GlobalAlloc, Layout};
78    use core::mem::size_of;
79
80    unsafe extern "C" {
81        fn telemetryMalloc(size: usize) -> *mut core::ffi::c_void;
82        fn telemetryFree(ptr: *mut core::ffi::c_void);
83        fn telemetry_panic_hook(msg: *const u8, len: usize);
84    }
85
86    /// Global allocator that forwards to `telemetryMalloc` / `telemetryFree`
87    /// provided by the host environment.
88    struct TelemetryAlloc;
89
90    #[inline]
91    fn align_up(addr: usize, align: usize) -> usize {
92        (addr + (align - 1)) & !(align - 1)
93    }
94
95    unsafe impl GlobalAlloc for TelemetryAlloc {
96        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
97            let align = layout.align().max(size_of::<usize>());
98            let header = size_of::<usize>();
99            let total = match layout
100                .size()
101                .checked_add(align)
102                .and_then(|v| v.checked_add(header))
103            {
104                Some(v) => v,
105                None => return core::ptr::null_mut(),
106            };
107
108            let raw = unsafe { telemetryMalloc(total) as *mut u8 };
109            if raw.is_null() {
110                return core::ptr::null_mut();
111            }
112
113            let base = raw as usize + header;
114            let aligned = align_up(base, align) as *mut u8;
115
116            // Store the original pointer just before the aligned pointer.
117            unsafe {
118                let slot = (aligned as *mut usize).offset(-1);
119                *slot = raw as usize;
120            }
121
122            aligned
123        }
124
125        unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
126            if ptr.is_null() {
127                return;
128            }
129
130            let raw = unsafe {
131                let slot = (ptr as *mut usize).offset(-1);
132                *slot as *mut core::ffi::c_void
133            };
134            unsafe { telemetryFree(raw) };
135        }
136    }
137
138    #[global_allocator]
139    static A: TelemetryAlloc = TelemetryAlloc;
140
141    // Panic handler for embedded
142    use core::panic::PanicInfo;
143
144    #[panic_handler]
145    fn panic(_info: &PanicInfo) -> ! {
146        let msg = b"rust panic";
147        unsafe {
148            telemetry_panic_hook(msg.as_ptr(), msg.len());
149        }
150
151        // Halt forever after panic.
152        loop {}
153    }
154
155    // ensure cortex-m only compiles on embedded
156    // use cortex_m as _;
157}
158
159// For HOST builds (std is ON), the system allocator is used automatically.
160// No custom panic handler needed.
161
162// ============================================================================
163//  Portable core logic: modules
164// ============================================================================
165
166#[cfg(any(feature = "std", feature = "fair-admission"))]
167mod admission_fairness;
168mod binding_options;
169mod c_api;
170pub mod config;
171#[cfg(feature = "cryptography")]
172pub mod crypto;
173pub mod diagnostics;
174#[cfg(feature = "discovery")]
175pub mod discovery;
176mod lock;
177mod macros;
178pub mod memory_admission;
179pub mod packet;
180mod shared_bytes;
181pub use shared_bytes::SharedBytes;
182#[cfg(feature = "compact-packet-store")]
183pub mod packet_store;
184mod queue;
185pub mod relay;
186pub mod router;
187mod side_transport;
188mod small_payload;
189mod small_sort;
190#[cfg(feature = "timesync")]
191pub mod timesync;
192mod varint;
193pub mod wire_format;
194// ============================================================================
195//  Schema-derived global constants
196// ============================================================================
197
198/// Maximum enum value for `DataEndpoint` (inclusive), derived from the schema.
199pub const MAX_VALUE_DATA_ENDPOINT: u32 = 255;
200
201/// Maximum enum value for `DataType` (inclusive), derived from the schema.
202pub const MAX_VALUE_DATA_TYPE: u32 = 4095;
203
204/// Maximum enum value for `RouteSelectionMode` (inclusive).
205pub const MAX_VALUE_ROUTE_SELECTION_MODE: i32 = 2;
206
207impl ReprU32Enum for DataType {
208    const MAX: u32 = u32::MAX;
209
210    #[inline]
211    fn from_u32(x: u32) -> Option<Self> {
212        DataType::try_from_u32(x)
213    }
214}
215
216impl ReprU32Enum for DataEndpoint {
217    const MAX: u32 = u32::MAX;
218
219    #[inline]
220    fn from_u32(x: u32) -> Option<Self> {
221        DataEndpoint::try_from_u32(x)
222    }
223}
224
225#[inline]
226pub fn current_max_endpoint_id() -> u32 {
227    max_endpoint_id()
228}
229
230#[inline]
231pub fn current_max_data_type_id() -> u32 {
232    max_data_type_id()
233}
234
235#[repr(i32)]
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
237pub enum RouteSelectionMode {
238    Fanout = 0,
239    Weighted = 1,
240    Failover = 2,
241}
242
243impl_repr_i32_enum!(
244    RouteSelectionMode,
245    RouteSelectionMode::Fanout as i32,
246    RouteSelectionMode::Failover as i32
247);
248
249#[inline]
250const fn parse_usize(s: &str) -> usize {
251    let bytes = s.as_bytes();
252    let mut i = 0;
253    let mut val = 0;
254
255    while i < bytes.len() {
256        let c = bytes[i];
257        if c < b'0' || c > b'9' {
258            panic!("Invalid digit");
259        }
260        val = val * 10 + (c - b'0') as usize;
261        i += 1;
262    }
263    val
264}
265
266#[inline]
267pub const fn parse_f64(s: &str) -> f64 {
268    let bytes = s.as_bytes();
269    let mut i = 0;
270
271    if bytes.is_empty() {
272        panic!("empty string");
273    }
274
275    // sign
276    let mut sign = 1.0;
277    if bytes[i] == b'-' {
278        sign = -1.0;
279        i += 1;
280    } else if bytes[i] == b'+' {
281        i += 1;
282    }
283
284    let mut int_part: f64 = 0.0;
285    let mut has_digits = false;
286
287    while i < bytes.len() && bytes[i] >= b'0' && bytes[i] <= b'9' {
288        int_part = int_part * 10.0 + (bytes[i] - b'0') as f64;
289        i += 1;
290        has_digits = true;
291    }
292
293    let mut frac_part: f64 = 0.0;
294    let mut scale: f64 = 1.0;
295
296    if i < bytes.len() && bytes[i] == b'.' {
297        i += 1;
298
299        while i < bytes.len() && bytes[i] >= b'0' && bytes[i] <= b'9' {
300            scale *= 10.0;
301            frac_part += (bytes[i] - b'0') as f64 / scale;
302            i += 1;
303            has_digits = true;
304        }
305    }
306
307    if !has_digits || i != bytes.len() {
308        panic!("invalid f64 literal");
309    }
310
311    sign * (int_part + frac_part)
312}
313
314#[inline(always)]
315const fn parse_strings(s: &str) -> &str {
316    s
317}
318
319#[inline]
320pub const fn parse_u8(s: &str) -> u8 {
321    let bytes = s.as_bytes();
322    let mut i = 0;
323    let mut val: u16 = 0;
324
325    if bytes.is_empty() {
326        panic!("empty string");
327    }
328
329    while i < bytes.len() {
330        let c = bytes[i];
331        if c < b'0' || c > b'9' {
332            panic!("invalid digit in u8");
333        }
334
335        val = val * 10 + (c - b'0') as u16;
336        if val > 255 {
337            panic!("u8 overflow");
338        }
339
340        i += 1;
341    }
342
343    val as u8
344}
345
346#[inline]
347pub const fn parse_u128(s: &str) -> u128 {
348    let bytes = s.as_bytes();
349    let mut i = 0;
350    let mut val: u128 = 0;
351
352    if bytes.is_empty() {
353        panic!("empty string");
354    }
355
356    while i < bytes.len() {
357        let c = bytes[i];
358        if c < b'0' || c > b'9' {
359            panic!("invalid digit in u128");
360        }
361
362        let digit = (c - b'0') as u128;
363
364        // Overflow check: val*10 + digit <= u128::MAX
365        // i.e. val <= (u128::MAX - digit) / 10
366        if val > (u128::MAX - digit) / 10 {
367            panic!("u128 overflow");
368        }
369
370        val = val * 10 + digit;
371        i += 1;
372    }
373
374    val
375}
376
377// ============================================================================
378//  Message metadata (element counts, data types, sizes)
379// ============================================================================
380#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
381pub struct EndpointMeta {
382    /// Static name of the endpoint
383    #[cfg(feature = "std")]
384    name: Arc<str>,
385    #[cfg(not(feature = "std"))]
386    name: &'static str,
387    /// Human-readable description used by schema lookup APIs.
388    #[cfg(feature = "std")]
389    description: Arc<str>,
390    #[cfg(not(feature = "std"))]
391    description: &'static str,
392    /// Restrict remote forwarding to link-local/software-bus sides only.
393    link_local_only: bool,
394}
395
396impl EndpointMeta {
397    #[cfg(feature = "std")]
398    pub(crate) fn name_ref(&self) -> &str {
399        self.name.as_ref()
400    }
401
402    #[cfg(not(feature = "std"))]
403    pub(crate) fn name_ref(&self) -> &str {
404        self.name
405    }
406
407    #[cfg(feature = "std")]
408    pub(crate) fn description_ref(&self) -> &str {
409        self.description.as_ref()
410    }
411
412    #[cfg(not(feature = "std"))]
413    pub(crate) fn description_ref(&self) -> &str {
414        self.description
415    }
416
417    /// Return a stable string representation used in logs and in
418    /// `Packet::to_string()` output.
419    ///
420    /// This should remain stable over time for compatibility with tests and
421    /// external tooling.
422    #[cfg(feature = "std")]
423    pub fn as_str(&self) -> Arc<str> {
424        self.name.clone()
425    }
426
427    #[cfg(not(feature = "std"))]
428    pub fn as_str(&self) -> &'static str {
429        self.name
430    }
431
432    /// Return the human-readable endpoint description.
433    #[inline]
434    #[cfg(feature = "std")]
435    pub fn description(&self) -> Arc<str> {
436        self.description.clone()
437    }
438
439    #[cfg(not(feature = "std"))]
440    pub fn description(&self) -> &'static str {
441        self.description
442    }
443
444    /// Return whether this endpoint is restricted to link-local/software-bus sides.
445    #[inline]
446    pub fn is_link_local_only(&self) -> bool {
447        self.link_local_only
448    }
449}
450
451impl DataEndpoint {
452    /// Return a stable string representation used in logs and in
453    /// `Packet::to_string()` output.
454    ///
455    /// This should remain stable over time for compatibility with tests and
456    /// external tooling.
457    #[cfg(feature = "std")]
458    pub fn as_str(&self) -> Arc<str> {
459        get_endpoint_meta(*self).name
460    }
461
462    #[cfg(not(feature = "std"))]
463    pub fn as_str(&self) -> &'static str {
464        get_endpoint_meta(*self).name
465    }
466
467    /// Return the human-readable endpoint description.
468    #[cfg(feature = "std")]
469    pub fn description(&self) -> Arc<str> {
470        get_endpoint_meta(*self).description
471    }
472
473    #[cfg(not(feature = "std"))]
474    pub fn description(&self) -> &'static str {
475        get_endpoint_meta(*self).description
476    }
477
478    /// Return whether this endpoint is restricted to link-local/software-bus sides.
479    #[inline]
480    pub fn is_link_local_only(&self) -> bool {
481        get_endpoint_meta(*self).link_local_only
482    }
483}
484
485/// Describes how many elements are present for a given message type.
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
487pub enum MessageElement {
488    /// Fixed number of elements.
489    ///
490    /// (count, MessageDataType, MessageClass)
491    Static(usize, MessageDataType, MessageClass),
492    /// Variable number of elements (payload size can vary).
493    ///
494    /// (MessageDataType, MessageClass)
495    Dynamic(MessageDataType, MessageClass),
496}
497
498impl MessageElement {
499    /// Get the `MessageDataType` for this element count.
500    #[inline]
501    pub const fn data_type(&self) -> MessageDataType {
502        match self {
503            MessageElement::Static(_, dt, _) => *dt,
504            MessageElement::Dynamic(dt, _) => *dt,
505        }
506    }
507
508    /// Get the `MessageType` for this element count.
509    #[inline]
510    pub const fn message_type(&self) -> MessageClass {
511        match self {
512            MessageElement::Static(_, _, mt) => *mt,
513            MessageElement::Dynamic(_, mt) => *mt,
514        }
515    }
516}
517
518/// Reliable delivery mode for a data type.
519#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
520#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
521pub enum ReliableMode {
522    /// No reliable delivery/acknowledgement on the wire.
523    #[cfg_attr(feature = "serde", serde(rename = "Off"))]
524    None,
525    /// Reliable delivery with strict ordering.
526    Ordered,
527    /// Reliable delivery without ordering guarantees.
528    Unordered,
529}
530
531/// End-to-end cryptography preference for a data type.
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
533pub enum E2eEncryptionPolicy {
534    /// Send unencrypted unless a router-level policy forces cryptography.
535    PreferOff,
536    /// Encrypt when the local router supports E2E cryptography, but allow plaintext fallback.
537    PreferOn,
538    /// Require E2E cryptography support before sending or locally consuming this type.
539    RequireOn,
540}
541
542/// Static metadata for a message type: element count and valid endpoints.
543#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
544pub struct MessageMeta {
545    #[cfg(feature = "std")]
546    name: Arc<str>,
547    #[cfg(not(feature = "std"))]
548    name: &'static str,
549    /// Human-readable description used by schema lookup APIs.
550    #[cfg(feature = "std")]
551    description: Arc<str>,
552    #[cfg(not(feature = "std"))]
553    description: &'static str,
554    /// How many elements are present (fixed vs dynamic).
555    element: MessageElement,
556    /// Allowed endpoints for this message type.
557    #[cfg(feature = "std")]
558    endpoints: Arc<[DataEndpoint]>,
559    #[cfg(not(feature = "std"))]
560    endpoints: &'static [DataEndpoint],
561    /// Reliable delivery mode for this type.
562    reliable: ReliableMode,
563    /// Queue priority for this type. Higher values are serviced first.
564    priority: u8,
565    /// End-to-end cryptography policy for this type.
566    e2e_encryption: E2eEncryptionPolicy,
567}
568
569impl MessageMeta {
570    #[cfg(feature = "std")]
571    pub(crate) fn name_ref(&self) -> &str {
572        self.name.as_ref()
573    }
574
575    #[cfg(not(feature = "std"))]
576    pub(crate) fn name_ref(&self) -> &str {
577        self.name
578    }
579
580    #[cfg(feature = "std")]
581    pub(crate) fn description_ref(&self) -> &str {
582        self.description.as_ref()
583    }
584
585    #[cfg(not(feature = "std"))]
586    pub(crate) fn description_ref(&self) -> &str {
587        self.description
588    }
589
590    #[cfg(feature = "std")]
591    pub(crate) fn endpoints_ref(&self) -> &[DataEndpoint] {
592        self.endpoints.as_ref()
593    }
594
595    #[cfg(not(feature = "std"))]
596    pub(crate) fn endpoints_ref(&self) -> &[DataEndpoint] {
597        self.endpoints
598    }
599}
600
601impl DataType {
602    /// Get the string representation of the DataType
603    #[cfg(feature = "std")]
604    pub fn as_str(&self) -> Arc<str> {
605        get_message_meta(*self).name
606    }
607
608    #[cfg(not(feature = "std"))]
609    pub fn as_str(&self) -> &'static str {
610        get_message_meta(*self).name
611    }
612
613    /// Return the human-readable data type description.
614    #[cfg(feature = "std")]
615    pub fn description(&self) -> Arc<str> {
616        get_message_meta(*self).description
617    }
618
619    #[cfg(not(feature = "std"))]
620    pub fn description(&self) -> &'static str {
621        get_message_meta(*self).description
622    }
623}
624/// Lookup `MessageMeta` for a given [`DataType`] using the generated config.
625/// # Arguments
626/// - `ty`: Logical data type to query.
627/// # Returns
628/// - `MessageMeta` struct with element count and allowed endpoints.
629#[inline]
630pub fn message_meta(ty: DataType) -> MessageMeta {
631    get_message_meta(ty)
632}
633
634/// Return whether the given [`DataType`] is configured for reliable delivery.
635#[inline]
636pub fn is_reliable_type(ty: DataType) -> bool {
637    !matches!(get_message_meta(ty).reliable, ReliableMode::None)
638}
639
640/// Return the reliable delivery mode for the given [`DataType`].
641#[inline]
642pub fn reliable_mode(ty: DataType) -> ReliableMode {
643    get_message_meta(ty).reliable
644}
645
646/// Return the queue priority for the given [`DataType`].
647#[inline]
648pub fn message_priority(ty: DataType) -> u8 {
649    get_message_meta(ty).priority
650}
651
652/// Priority used by the router/relay schedulers.
653///
654/// Protocol control traffic has reserved bands so an application schema
655/// cannot accidentally starve discovery, managed variables, or time sync by
656/// assigning a user message priority of 255. The priority stored in the
657/// schema is otherwise preserved, including the relative ordering of all
658/// application-defined messages.
659#[inline]
660pub fn transport_priority(ty: DataType) -> u8 {
661    match ty {
662        // Delivery control must be able to release reliable packets already
663        // occupying queues. Treat it as part of the highest control band.
664        DataType::ReliableAck | DataType::ReliablePartialAck | DataType::ReliablePacketRequest => {
665            255
666        }
667
668        // A usable route is a prerequisite for every other network service.
669        DataType::DiscoveryAnnounce
670        | DataType::DiscoveryTimeSyncSources
671        | DataType::DiscoveryTopology
672        | DataType::DiscoveryTopologyRequest
673        | DataType::DiscoveryLeave
674        | DataType::DiscoveryLinkCapabilities
675        | DataType::DiscoveryAddress => 255,
676
677        // Schema transfer is discovery-plane repair, but unlike address and
678        // topology advertisements it is not needed to establish a route.
679        // Keep large schema frames below route convergence so they cannot
680        // head-of-line block a constrained serial or radio link.
681        DataType::DiscoverySchema
682        | DataType::DiscoverySchemaRequest
683        | DataType::ManagedVariableRequest
684        | DataType::ManagedVariableValue
685        | DataType::TimeSyncAnnounce
686        | DataType::TimeSyncRequest
687        | DataType::TimeSyncResponse => 254,
688
689        // Preserve application ordering below the reserved control bands.
690        _ => message_priority(ty).min(253),
691    }
692}
693
694/// Small frozen byte controls retain their delivery class across relays that
695/// are not local readers. Ordinary telemetry keeps its schema priority.
696#[inline]
697pub(crate) fn small_frozen_control(
698    ty: DataType,
699    owners: usize,
700    shape: Option<MessageElement>,
701) -> bool {
702    if !(1..=3).contains(&owners) {
703        return false;
704    }
705    config::try_get_message_meta(ty).is_some_and(|meta| {
706        matches!(
707            meta.element,
708            MessageElement::Static(0..=16, MessageDataType::UInt8, _)
709        ) && matches!(
710            shape,
711            None | Some(MessageElement::Static(0..=16, MessageDataType::UInt8, _))
712        )
713    })
714}
715
716// Internal scheduler name emphasizes where this priority is applied.
717#[inline]
718pub(crate) fn scheduler_priority(ty: DataType) -> u8 {
719    transport_priority(ty)
720}
721
722/// Return the end-to-end cryptography policy for a data type.
723#[inline]
724pub fn message_e2e_encryption_policy(ty: DataType) -> E2eEncryptionPolicy {
725    get_message_meta(ty).e2e_encryption
726}
727
728// ---- Convenience multiplication helpers ----
729
730impl Mul<MessageElement> for usize {
731    type Output = usize;
732
733    #[inline]
734    fn mul(self, rhs: MessageElement) -> usize {
735        self * rhs.into()
736    }
737}
738
739impl Mul<usize> for MessageElement {
740    type Output = usize;
741
742    #[inline]
743    fn mul(self, rhs: usize) -> usize {
744        self.into() * rhs
745    }
746}
747
748impl MessageElement {
749    /// Convert the element count to a `usize`.
750    ///
751    /// - `Static(n)` → `n`
752    /// - `Dynamic`   → `0` (caller must handle dynamic sizing separately)
753    #[inline]
754    fn into(self) -> usize {
755        match self {
756            MessageElement::Static(a, _, _) => a,
757            _ => 0,
758        }
759    }
760}
761
762/// Return the total payload size (in bytes) required for a given `DataType`
763/// under the *static* schema.
764///
765/// This is `element_size * element_count`. For dynamic types, the
766/// configuration ensures we only call this where it makes sense.
767/// # Arguments
768/// - `ty`: Logical data type to query.
769/// # Returns
770/// - Total static payload size in bytes.
771#[inline]
772pub fn get_needed_message_size(ty: DataType) -> usize {
773    data_type_size(get_data_type(ty)) * get_message_meta(ty).element
774}
775
776/// Return the logical "info" type (Info/Error) for a given `DataType`.
777/// # Arguments
778/// - `ty`: Logical data type to query.
779/// # Returns
780/// - `MessageType` enum value.
781#[inline]
782pub fn get_info_type(ty: DataType) -> MessageClass {
783    get_message_meta(ty).element.message_type()
784}
785
786/// Return the *element* data type (e.g., `Float32`, `Int16`, `String`) for a
787/// given `DataType`.
788/// # Arguments
789/// - `ty`: Logical data type to query.
790/// # Returns
791/// - `MessageDataType` enum value.
792#[inline]
793pub fn get_data_type(ty: DataType) -> MessageDataType {
794    get_message_meta(ty).element.data_type()
795}
796
797/// Return the message name for a given `DataType`.
798/// # Arguments
799/// - `ty`: Logical data type to query.
800/// # Returns
801/// - Static string name of the message type.
802#[inline]
803#[cfg(feature = "std")]
804pub fn get_message_name(ty: DataType) -> Arc<str> {
805    get_message_meta(ty).name
806}
807
808#[cfg(not(feature = "std"))]
809pub fn get_message_name(ty: DataType) -> &'static str {
810    get_message_meta(ty).name
811}
812
813/// Return the default endpoints for a given `DataType`.
814/// # Arguments
815/// - `ty`: Logical data type to query.
816/// # Returns
817/// - Slice of allowed `DataEndpoint` values.
818#[inline]
819#[cfg(feature = "std")]
820pub fn endpoints_from_datatype(ty: DataType) -> Arc<[DataEndpoint]> {
821    get_message_meta(ty).endpoints
822}
823
824#[cfg(not(feature = "std"))]
825pub fn endpoints_from_datatype(ty: DataType) -> &'static [DataEndpoint] {
826    get_message_meta(ty).endpoints
827}
828
829/// Primitive element type used by a message.
830///
831/// This is the underlying "slot" type, not the high-level `DataType`
832/// (which is the logical schema type).
833#[allow(dead_code)]
834#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
835pub enum MessageDataType {
836    Float64,
837    Float32,
838    UInt8,
839    UInt16,
840    UInt32,
841    UInt64,
842    UInt128,
843    Int8,
844    Int16,
845    Int32,
846    Int64,
847    Int128,
848    Bool,
849    String,
850    Binary,
851    NoData,
852}
853
854/// Size in bytes of a single element for the given [`MessageDataType`].
855///
856/// For `String` / `Hex`, this returns the fixed maximum static length
857/// configured by the schema (`MAX_STATIC_STRING_LENGTH`, `MAX_STATIC_HEX_LENGTH`).
858/// # Arguments
859/// - `dt`: Logical data type to query.
860/// # Returns
861/// - Size in bytes of a single element of that type.
862#[inline]
863pub fn data_type_size(dt: MessageDataType) -> usize {
864    match dt {
865        MessageDataType::Float64 => size_of::<f64>(),
866        MessageDataType::Float32 => size_of::<f32>(),
867        MessageDataType::UInt8 => size_of::<u8>(),
868        MessageDataType::UInt16 => size_of::<u16>(),
869        MessageDataType::UInt32 => size_of::<u32>(),
870        MessageDataType::UInt64 => size_of::<u64>(),
871        MessageDataType::UInt128 => size_of::<u128>(),
872        MessageDataType::Int8 => size_of::<i8>(),
873        MessageDataType::Int16 => size_of::<i16>(),
874        MessageDataType::Int32 => size_of::<i32>(),
875        MessageDataType::Int64 => size_of::<i64>(),
876        MessageDataType::Int128 => size_of::<i128>(),
877        MessageDataType::Bool => size_of::<bool>(),
878        MessageDataType::String => runtime_static_string_length(),
879        MessageDataType::Binary => runtime_static_hex_length(),
880        MessageDataType::NoData => 0,
881    }
882}
883
884/// High-level classification of message kind.
885#[allow(dead_code)]
886#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
887pub enum MessageClass {
888    /// Informational telemetry.
889    Data,
890    /// Error / fault telemetry.
891    Error,
892    /// Warning telemetry.
893    Warning,
894}
895
896// ============================================================================
897//  Error types and error codes
898// ============================================================================
899
900/// Rich error type used throughout the telemetry crate.
901///
902/// Most public APIs expose a `TelemetryResult<T>` alias for
903/// `Result<T, TelemetryError>`.
904#[derive(Debug, Clone, PartialEq, Eq)]
905pub enum TelemetryError {
906    /// Generic / unspecified error.
907    GenericError(Option<Arc<str>>),
908
909    /// Logical type ID is not a valid [`DataType`].
910    InvalidType,
911
912    /// Payload size doesn't match the schema's expectations.
913    SizeMismatch { expected: usize, got: usize },
914
915    /// Legacy / generic size mismatch error (for C/Python parity).
916    SizeMismatchError,
917
918    /// No endpoints were supplied where they are required.
919    EmptyEndpoints,
920
921    /// Timestamp is invalid (e.g., zero when disallowed).
922    TimestampInvalid,
923
924    /// A packet is missing its payload bytes.
925    MissingPayload,
926
927    /// A handler (C/Python callback) returned an error.
928    HandlerError(&'static str),
929
930    /// Generic invalid argument from caller.
931    BadArg,
932
933    /// Operation is not permitted for the current router/device policy.
934    PermissionDenied,
935
936    /// Packing error.
937    Pack(&'static str),
938
939    /// Unpacking error.
940    Unpack(&'static str),
941
942    /// IO / transport error.
943    Io(&'static str),
944
945    /// UTF-8 decoding failed where string payloads are expected.
946    InvalidUtf8,
947
948    /// Payload type size mismatch.
949    TypeMismatch { expected: usize, got: usize },
950
951    /// Invalid link ID provided.
952    InvalidLinkId(&'static str),
953
954    /// Packet is bigger than the queue size
955    PacketTooLarge(&'static str),
956}
957
958impl TelemetryError {
959    /// Map a rich [`TelemetryError`] to a stable numeric error code
960    /// used by the FFI layers.
961    pub const fn to_error_code(&self) -> TelemetryErrorCode {
962        match self {
963            TelemetryError::GenericError(_) => TelemetryErrorCode::GenericError,
964            TelemetryError::InvalidType => TelemetryErrorCode::InvalidType,
965            TelemetryError::SizeMismatch { .. } => TelemetryErrorCode::SizeMismatch,
966            TelemetryError::SizeMismatchError => TelemetryErrorCode::SizeMismatchError,
967            TelemetryError::EmptyEndpoints => TelemetryErrorCode::EmptyEndpoints,
968            TelemetryError::TimestampInvalid => TelemetryErrorCode::TimestampInvalid,
969            TelemetryError::MissingPayload => TelemetryErrorCode::MissingPayload,
970            TelemetryError::HandlerError(_) => TelemetryErrorCode::HandlerError,
971            TelemetryError::BadArg => TelemetryErrorCode::BadArg,
972            TelemetryError::PermissionDenied => TelemetryErrorCode::PermissionDenied,
973            TelemetryError::Pack(_) => TelemetryErrorCode::Pack,
974            TelemetryError::Unpack(_) => TelemetryErrorCode::Unpack,
975            TelemetryError::Io(_) => TelemetryErrorCode::Io,
976            TelemetryError::InvalidUtf8 => TelemetryErrorCode::InvalidUtf8,
977            TelemetryError::TypeMismatch { .. } => TelemetryErrorCode::TypeMismatch,
978            TelemetryError::InvalidLinkId(_) => TelemetryErrorCode::InvalidLinkId,
979            TelemetryError::PacketTooLarge(_) => TelemetryErrorCode::PacketTooLarge,
980        }
981    }
982}
983
984/// Allow conversion of `TelemetryError` to human-readable string.
985impl core::fmt::Display for TelemetryError {
986    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
987        // ToString delegates to Display: calling it here recurses until the
988        // caller overflows its stack, including on embedded error paths.
989        // Derived Debug formats every variant and its details without recursion
990        // or an intermediate allocation.
991        core::fmt::Debug::fmt(self, f)
992    }
993}
994
995/// Implement `std::error::Error` for `TelemetryError` when `std` is enabled.
996#[cfg(feature = "std")]
997impl std::error::Error for TelemetryError {
998    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
999        None
1000    }
1001}
1002
1003/// Allow the conversion from std error to telemetry error
1004#[cfg(feature = "std")]
1005impl From<Error> for TelemetryError {
1006    fn from(error: Error) -> Self {
1007        let str = error.to_string();
1008        let astr: Arc<str> = Arc::from(str.as_str());
1009        TelemetryError::GenericError(Some(astr))
1010    }
1011}
1012
1013/// Allow the conversion from boxed std error to telemetry error
1014#[cfg(feature = "std")]
1015impl From<Box<dyn std::error::Error>> for TelemetryError {
1016    fn from(err: Box<dyn std::error::Error>) -> Self {
1017        let str = err.to_string();
1018        let astr: Arc<str> = Arc::from(str.as_str());
1019        TelemetryError::GenericError(Some(astr))
1020    }
1021}
1022
1023/// Numeric error codes used on the C/Python FFI boundary.
1024///
1025/// Negative values are used to avoid collisions with success codes
1026/// and other positive return values (e.g. lengths).
1027#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1028#[repr(i32)]
1029pub enum TelemetryErrorCode {
1030    GenericError = -2,
1031    InvalidType = -3,
1032    SizeMismatch = -4,
1033    SizeMismatchError = -5,
1034    EmptyEndpoints = -6,
1035    TimestampInvalid = -7,
1036    MissingPayload = -8,
1037    HandlerError = -9,
1038    BadArg = -10,
1039    PermissionDenied = -11,
1040    Pack = -12,
1041    Unpack = -13,
1042    Io = -14,
1043    InvalidUtf8 = -15,
1044    TypeMismatch = -16,
1045    InvalidLinkId = -17,
1046    PacketTooLarge = -18,
1047}
1048
1049// Generate ReprI32Enum helpers for TelemetryErrorCode
1050impl_repr_i32_enum!(
1051    TelemetryErrorCode,
1052    TelemetryErrorCode::MAX,
1053    TelemetryErrorCode::MIN
1054);
1055
1056impl TelemetryErrorCode {
1057    /// Maximum valid numeric error code value.
1058    pub const MAX: i32 = TelemetryErrorCode::InvalidType as i32;
1059
1060    /// Minimum valid numeric error code value.
1061    pub const MIN: i32 = TelemetryErrorCode::PacketTooLarge as i32;
1062
1063    /// Human-readable string for logging / debugging.
1064    /// # Returns
1065    /// - Static string representation of the error code.
1066    #[inline]
1067    pub fn as_str(&self) -> &'static str {
1068        match self {
1069            TelemetryErrorCode::GenericError => "GenericError",
1070            TelemetryErrorCode::InvalidType => "{Invalid Type}",
1071            TelemetryErrorCode::SizeMismatch => "{Size Mismatch}",
1072            TelemetryErrorCode::SizeMismatchError => "{Size Mismatch Error}",
1073            TelemetryErrorCode::EmptyEndpoints => "{Empty Endpoints}",
1074            TelemetryErrorCode::TimestampInvalid => "{Timestamp Invalid}",
1075            TelemetryErrorCode::MissingPayload => "{Missing Payload}",
1076            TelemetryErrorCode::HandlerError => "{Handler Error}",
1077            TelemetryErrorCode::BadArg => "{Bad Arg}",
1078            TelemetryErrorCode::PermissionDenied => "{Permission Denied}",
1079            TelemetryErrorCode::Pack => "{Pack Error}",
1080            TelemetryErrorCode::Unpack => "{Unpack Error}",
1081            TelemetryErrorCode::Io => "{IO Error}",
1082            TelemetryErrorCode::InvalidUtf8 => "{Invalid UTF-8}",
1083            TelemetryErrorCode::TypeMismatch => "{Type Mismatch}",
1084            TelemetryErrorCode::InvalidLinkId => "{Invalid Link ID}",
1085            TelemetryErrorCode::PacketTooLarge => "{Packet Too Large}",
1086        }
1087    }
1088
1089    /// Try to convert a raw i32 error code into a [`TelemetryErrorCode`].
1090    ///
1091    /// Returns `None` if the code is out of range or not recognized.
1092    /// # Arguments
1093    /// - `x`: Raw i32 error code to convert.
1094    /// # Returns
1095    /// - `Some(TelemetryErrorCode)` if valid, `None` if invalid.
1096    #[inline]
1097    pub fn try_from_i32(x: i32) -> Option<Self> {
1098        try_enum_from_i32(x)
1099    }
1100}
1101
1102/// Common result alias for telemetry operations.
1103pub type TelemetryResult<T> = Result<T, TelemetryError>;
1104
1105// ============================================================================
1106//  Generic enum helpers (repr(u32) / repr(i32))
1107// ============================================================================
1108
1109/// Try to convert a `u32` into a `#[repr(u32)]` enum `E`.
1110///
1111/// Returns `None` if the value is out of range (greater than `E::MAX`).
1112/// # Arguments
1113/// - `x`: Raw u32 value to convert.
1114/// # Returns
1115/// - `Some(E)` if valid, `None` if invalid.
1116#[inline]
1117pub fn try_enum_from_u32<E: ReprU32Enum>(x: u32) -> Option<E> {
1118    if x > E::MAX {
1119        return None;
1120    }
1121    E::from_u32(x)
1122}
1123
1124/// Try to convert an `i32` into a `#[repr(i32)]` enum `E`.
1125///
1126/// Returns `None` if the value is outside the `[E::MIN, E::MAX]` range.
1127/// # Arguments
1128/// - `x`: Raw i32 value to convert.
1129/// # Returns
1130/// - `Some(E)` if valid, `None` if invalid.
1131#[inline]
1132pub fn try_enum_from_i32<E: ReprI32Enum>(x: i32) -> Option<E> {
1133    if x < E::MIN || x > E::MAX {
1134        return None;
1135    }
1136
1137    // SAFETY: `E` is promised to be a fieldless #[repr(i32)] enum (thus 4 bytes, Copy).
1138    let e = unsafe { (&x as *const i32 as *const E).read() };
1139    Some(e)
1140}