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