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