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;
35extern crate core;
36#[cfg(feature = "std")]
37extern crate std;
38#[cfg(feature = "std")]
39use std::io::Error;
40
41use crate::config::{
42    DataEndpoint, DataType, STATIC_HEX_LENGTH, STATIC_STRING_LENGTH, get_endpoint_meta,
43    get_message_meta, max_data_type_id, max_endpoint_id,
44};
45use crate::macros::{ReprI32Enum, ReprU32Enum};
46use alloc::string::ToString;
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
166mod c_api;
167pub mod config;
168#[cfg(feature = "cryptography")]
169pub mod crypto;
170pub mod diagnostics;
171#[cfg(feature = "discovery")]
172pub mod discovery;
173mod lock;
174mod macros;
175pub mod packet;
176mod queue;
177pub mod relay;
178pub mod router;
179mod small_payload;
180#[cfg(feature = "timesync")]
181pub mod timesync;
182pub mod wire_format;
183// ============================================================================
184//  Schema-derived global constants
185// ============================================================================
186
187/// Maximum enum value for `DataEndpoint` (inclusive), derived from the schema.
188pub const MAX_VALUE_DATA_ENDPOINT: u32 = 255;
189
190/// Maximum enum value for `DataType` (inclusive), derived from the schema.
191pub const MAX_VALUE_DATA_TYPE: u32 = 4095;
192
193/// Maximum enum value for `RouteSelectionMode` (inclusive).
194pub const MAX_VALUE_ROUTE_SELECTION_MODE: i32 = 2;
195
196impl crate::macros::ReprU32Enum for DataType {
197    const MAX: u32 = MAX_VALUE_DATA_TYPE;
198
199    #[inline]
200    fn from_u32(x: u32) -> Option<Self> {
201        DataType::try_from_u32(x)
202    }
203}
204
205impl crate::macros::ReprU32Enum for DataEndpoint {
206    const MAX: u32 = MAX_VALUE_DATA_ENDPOINT;
207
208    #[inline]
209    fn from_u32(x: u32) -> Option<Self> {
210        DataEndpoint::try_from_u32(x)
211    }
212}
213
214#[inline]
215pub fn current_max_endpoint_id() -> u32 {
216    max_endpoint_id()
217}
218
219#[inline]
220pub fn current_max_data_type_id() -> u32 {
221    max_data_type_id()
222}
223
224#[repr(i32)]
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
226pub enum RouteSelectionMode {
227    Fanout = 0,
228    Weighted = 1,
229    Failover = 2,
230}
231
232impl_repr_i32_enum!(
233    RouteSelectionMode,
234    RouteSelectionMode::Fanout as i32,
235    RouteSelectionMode::Failover as i32
236);
237
238#[inline]
239const fn parse_usize(s: &str) -> usize {
240    let bytes = s.as_bytes();
241    let mut i = 0;
242    let mut val = 0;
243
244    while i < bytes.len() {
245        let c = bytes[i];
246        if c < b'0' || c > b'9' {
247            panic!("Invalid digit");
248        }
249        val = val * 10 + (c - b'0') as usize;
250        i += 1;
251    }
252    val
253}
254
255#[inline]
256pub const fn parse_f64(s: &str) -> f64 {
257    let bytes = s.as_bytes();
258    let mut i = 0;
259
260    if bytes.is_empty() {
261        panic!("empty string");
262    }
263
264    // sign
265    let mut sign = 1.0;
266    if bytes[i] == b'-' {
267        sign = -1.0;
268        i += 1;
269    } else if bytes[i] == b'+' {
270        i += 1;
271    }
272
273    let mut int_part: f64 = 0.0;
274    let mut has_digits = false;
275
276    while i < bytes.len() && bytes[i] >= b'0' && bytes[i] <= b'9' {
277        int_part = int_part * 10.0 + (bytes[i] - b'0') as f64;
278        i += 1;
279        has_digits = true;
280    }
281
282    let mut frac_part: f64 = 0.0;
283    let mut scale: f64 = 1.0;
284
285    if i < bytes.len() && bytes[i] == b'.' {
286        i += 1;
287
288        while i < bytes.len() && bytes[i] >= b'0' && bytes[i] <= b'9' {
289            scale *= 10.0;
290            frac_part += (bytes[i] - b'0') as f64 / scale;
291            i += 1;
292            has_digits = true;
293        }
294    }
295
296    if !has_digits || i != bytes.len() {
297        panic!("invalid f64 literal");
298    }
299
300    sign * (int_part + frac_part)
301}
302
303#[inline(always)]
304const fn parse_strings(s: &str) -> &str {
305    s
306}
307
308#[inline]
309pub const fn parse_u8(s: &str) -> u8 {
310    let bytes = s.as_bytes();
311    let mut i = 0;
312    let mut val: u16 = 0;
313
314    if bytes.is_empty() {
315        panic!("empty string");
316    }
317
318    while i < bytes.len() {
319        let c = bytes[i];
320        if c < b'0' || c > b'9' {
321            panic!("invalid digit in u8");
322        }
323
324        val = val * 10 + (c - b'0') as u16;
325        if val > 255 {
326            panic!("u8 overflow");
327        }
328
329        i += 1;
330    }
331
332    val as u8
333}
334
335#[inline]
336pub const fn parse_u128(s: &str) -> u128 {
337    let bytes = s.as_bytes();
338    let mut i = 0;
339    let mut val: u128 = 0;
340
341    if bytes.is_empty() {
342        panic!("empty string");
343    }
344
345    while i < bytes.len() {
346        let c = bytes[i];
347        if c < b'0' || c > b'9' {
348            panic!("invalid digit in u128");
349        }
350
351        let digit = (c - b'0') as u128;
352
353        // Overflow check: val*10 + digit <= u128::MAX
354        // i.e. val <= (u128::MAX - digit) / 10
355        if val > (u128::MAX - digit) / 10 {
356            panic!("u128 overflow");
357        }
358
359        val = val * 10 + digit;
360        i += 1;
361    }
362
363    val
364}
365
366// ============================================================================
367//  Message metadata (element counts, data types, sizes)
368// ============================================================================
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
370pub struct EndpointMeta {
371    /// Static name of the endpoint
372    name: &'static str,
373    /// Human-readable description used by schema lookup APIs.
374    description: &'static str,
375    /// Restrict remote forwarding to link-local/software-bus sides only.
376    link_local_only: bool,
377}
378
379impl EndpointMeta {
380    /// Return a stable string representation used in logs and in
381    /// `Packet::to_string()` output.
382    ///
383    /// This should remain stable over time for compatibility with tests and
384    /// external tooling.
385    pub fn as_str(&self) -> &'static str {
386        self.name
387    }
388
389    /// Return the human-readable endpoint description.
390    #[inline]
391    pub fn description(&self) -> &'static str {
392        self.description
393    }
394
395    /// Return whether this endpoint is restricted to link-local/software-bus sides.
396    #[inline]
397    pub fn is_link_local_only(&self) -> bool {
398        self.link_local_only
399    }
400}
401
402impl DataEndpoint {
403    /// Return a stable string representation used in logs and in
404    /// `Packet::to_string()` output.
405    ///
406    /// This should remain stable over time for compatibility with tests and
407    /// external tooling.
408    pub fn as_str(&self) -> &'static str {
409        get_endpoint_meta(*self).name
410    }
411
412    /// Return the human-readable endpoint description.
413    pub fn description(&self) -> &'static str {
414        get_endpoint_meta(*self).description
415    }
416
417    /// Return whether this endpoint is restricted to link-local/software-bus sides.
418    #[inline]
419    pub fn is_link_local_only(&self) -> bool {
420        get_endpoint_meta(*self).link_local_only
421    }
422}
423
424/// Describes how many elements are present for a given message type.
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
426pub enum MessageElement {
427    /// Fixed number of elements.
428    ///
429    /// (count, MessageDataType, MessageClass)
430    Static(usize, MessageDataType, MessageClass),
431    /// Variable number of elements (payload size can vary).
432    ///
433    /// (MessageDataType, MessageClass)
434    Dynamic(MessageDataType, MessageClass),
435}
436
437impl MessageElement {
438    /// Get the `MessageDataType` for this element count.
439    #[inline]
440    pub const fn data_type(&self) -> MessageDataType {
441        match self {
442            MessageElement::Static(_, dt, _) => *dt,
443            MessageElement::Dynamic(dt, _) => *dt,
444        }
445    }
446
447    /// Get the `MessageType` for this element count.
448    #[inline]
449    pub const fn message_type(&self) -> MessageClass {
450        match self {
451            MessageElement::Static(_, _, mt) => *mt,
452            MessageElement::Dynamic(_, mt) => *mt,
453        }
454    }
455}
456
457/// Reliable delivery mode for a data type.
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
459pub enum ReliableMode {
460    /// No reliable delivery/acknowledgement on the wire.
461    None,
462    /// Reliable delivery with strict ordering.
463    Ordered,
464    /// Reliable delivery without ordering guarantees.
465    Unordered,
466}
467
468/// End-to-end cryptography preference for a data type.
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
470pub enum E2eEncryptionPolicy {
471    /// Send unencrypted unless a router-level policy forces cryptography.
472    PreferOff,
473    /// Encrypt when the local router supports E2E cryptography, but allow plaintext fallback.
474    PreferOn,
475    /// Require E2E cryptography support before sending or locally consuming this type.
476    RequireOn,
477}
478
479/// Static metadata for a message type: element count and valid endpoints.
480#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
481pub struct MessageMeta {
482    name: &'static str,
483    /// Human-readable description used by schema lookup APIs.
484    description: &'static str,
485    /// How many elements are present (fixed vs dynamic).
486    element: MessageElement,
487    /// Allowed endpoints for this message type.
488    endpoints: &'static [DataEndpoint],
489    /// Reliable delivery mode for this type.
490    reliable: ReliableMode,
491    /// Queue priority for this type. Higher values are serviced first.
492    priority: u8,
493    /// End-to-end cryptography policy for this type.
494    e2e_encryption: E2eEncryptionPolicy,
495}
496
497impl DataType {
498    /// Get the string representation of the DataType
499    pub fn as_str(&self) -> &'static str {
500        get_message_meta(*self).name
501    }
502
503    /// Return the human-readable data type description.
504    pub fn description(&self) -> &'static str {
505        get_message_meta(*self).description
506    }
507}
508/// Lookup `MessageMeta` for a given [`DataType`] using the generated config.
509/// # Arguments
510/// - `ty`: Logical data type to query.
511/// # Returns
512/// - `MessageMeta` struct with element count and allowed endpoints.
513#[inline]
514pub fn message_meta(ty: DataType) -> MessageMeta {
515    get_message_meta(ty)
516}
517
518/// Return whether the given [`DataType`] is configured for reliable delivery.
519#[inline]
520pub fn is_reliable_type(ty: DataType) -> bool {
521    !matches!(get_message_meta(ty).reliable, ReliableMode::None)
522}
523
524/// Return the reliable delivery mode for the given [`DataType`].
525#[inline]
526pub fn reliable_mode(ty: DataType) -> ReliableMode {
527    get_message_meta(ty).reliable
528}
529
530/// Return the queue priority for the given [`DataType`].
531#[inline]
532pub fn message_priority(ty: DataType) -> u8 {
533    get_message_meta(ty).priority
534}
535
536/// Return the end-to-end cryptography policy for a data type.
537#[inline]
538pub fn message_e2e_encryption_policy(ty: DataType) -> E2eEncryptionPolicy {
539    get_message_meta(ty).e2e_encryption
540}
541
542// ---- Convenience multiplication helpers ----
543
544impl Mul<MessageElement> for usize {
545    type Output = usize;
546
547    #[inline]
548    fn mul(self, rhs: MessageElement) -> usize {
549        self * rhs.into()
550    }
551}
552
553impl Mul<usize> for MessageElement {
554    type Output = usize;
555
556    #[inline]
557    fn mul(self, rhs: usize) -> usize {
558        self.into() * rhs
559    }
560}
561
562impl MessageElement {
563    /// Convert the element count to a `usize`.
564    ///
565    /// - `Static(n)` → `n`
566    /// - `Dynamic`   → `0` (caller must handle dynamic sizing separately)
567    #[inline]
568    fn into(self) -> usize {
569        match self {
570            MessageElement::Static(a, _, _) => a,
571            _ => 0,
572        }
573    }
574}
575
576/// Return the total payload size (in bytes) required for a given `DataType`
577/// under the *static* schema.
578///
579/// This is `element_size * element_count`. For dynamic types, the
580/// configuration ensures we only call this where it makes sense.
581/// # Arguments
582/// - `ty`: Logical data type to query.
583/// # Returns
584/// - Total static payload size in bytes.
585#[inline]
586pub fn get_needed_message_size(ty: DataType) -> usize {
587    data_type_size(get_data_type(ty)) * get_message_meta(ty).element
588}
589
590/// Return the logical "info" type (Info/Error) for a given `DataType`.
591/// # Arguments
592/// - `ty`: Logical data type to query.
593/// # Returns
594/// - `MessageType` enum value.
595#[inline]
596pub fn get_info_type(ty: DataType) -> MessageClass {
597    get_message_meta(ty).element.message_type()
598}
599
600/// Return the *element* data type (e.g., `Float32`, `Int16`, `String`) for a
601/// given `DataType`.
602/// # Arguments
603/// - `ty`: Logical data type to query.
604/// # Returns
605/// - `MessageDataType` enum value.
606#[inline]
607pub fn get_data_type(ty: DataType) -> MessageDataType {
608    get_message_meta(ty).element.data_type()
609}
610
611/// Return the message name for a given `DataType`.
612/// # Arguments
613/// - `ty`: Logical data type to query.
614/// # Returns
615/// - Static string name of the message type.
616#[inline]
617pub fn get_message_name(ty: DataType) -> &'static str {
618    get_message_meta(ty).name
619}
620
621/// Return the default endpoints for a given `DataType`.
622/// # Arguments
623/// - `ty`: Logical data type to query.
624/// # Returns
625/// - Slice of allowed `DataEndpoint` values.
626#[inline]
627pub fn endpoints_from_datatype(ty: DataType) -> &'static [DataEndpoint] {
628    get_message_meta(ty).endpoints
629}
630
631/// Primitive element type used by a message.
632///
633/// This is the underlying "slot" type, not the high-level `DataType`
634/// (which is the logical schema type).
635#[allow(dead_code)]
636#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
637pub enum MessageDataType {
638    Float64,
639    Float32,
640    UInt8,
641    UInt16,
642    UInt32,
643    UInt64,
644    UInt128,
645    Int8,
646    Int16,
647    Int32,
648    Int64,
649    Int128,
650    Bool,
651    String,
652    Binary,
653    NoData,
654}
655
656/// Size in bytes of a single element for the given [`MessageDataType`].
657///
658/// For `String` / `Hex`, this returns the fixed maximum static length
659/// configured by the schema (`MAX_STATIC_STRING_LENGTH`, `MAX_STATIC_HEX_LENGTH`).
660/// # Arguments
661/// - `dt`: Logical data type to query.
662/// # Returns
663/// - Size in bytes of a single element of that type.
664#[inline]
665pub const fn data_type_size(dt: MessageDataType) -> usize {
666    match dt {
667        MessageDataType::Float64 => size_of::<f64>(),
668        MessageDataType::Float32 => size_of::<f32>(),
669        MessageDataType::UInt8 => size_of::<u8>(),
670        MessageDataType::UInt16 => size_of::<u16>(),
671        MessageDataType::UInt32 => size_of::<u32>(),
672        MessageDataType::UInt64 => size_of::<u64>(),
673        MessageDataType::UInt128 => size_of::<u128>(),
674        MessageDataType::Int8 => size_of::<i8>(),
675        MessageDataType::Int16 => size_of::<i16>(),
676        MessageDataType::Int32 => size_of::<i32>(),
677        MessageDataType::Int64 => size_of::<i64>(),
678        MessageDataType::Int128 => size_of::<i128>(),
679        MessageDataType::Bool => size_of::<bool>(),
680        MessageDataType::String => STATIC_STRING_LENGTH,
681        MessageDataType::Binary => STATIC_HEX_LENGTH,
682        MessageDataType::NoData => 0,
683    }
684}
685
686/// High-level classification of message kind.
687#[allow(dead_code)]
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
689pub enum MessageClass {
690    /// Informational telemetry.
691    Data,
692    /// Error / fault telemetry.
693    Error,
694    /// Warning telemetry.
695    Warning,
696}
697
698// ============================================================================
699//  Error types and error codes
700// ============================================================================
701
702/// Rich error type used throughout the telemetry crate.
703///
704/// Most public APIs expose a `TelemetryResult<T>` alias for
705/// `Result<T, TelemetryError>`.
706#[derive(Debug, Clone, PartialEq, Eq)]
707pub enum TelemetryError {
708    /// Generic / unspecified error.
709    GenericError(Option<Arc<str>>),
710
711    /// Logical type ID is not a valid [`DataType`].
712    InvalidType,
713
714    /// Payload size doesn't match the schema's expectations.
715    SizeMismatch { expected: usize, got: usize },
716
717    /// Legacy / generic size mismatch error (for C/Python parity).
718    SizeMismatchError,
719
720    /// No endpoints were supplied where they are required.
721    EmptyEndpoints,
722
723    /// Timestamp is invalid (e.g., zero when disallowed).
724    TimestampInvalid,
725
726    /// A packet is missing its payload bytes.
727    MissingPayload,
728
729    /// A handler (C/Python callback) returned an error.
730    HandlerError(&'static str),
731
732    /// Generic invalid argument from caller.
733    BadArg,
734
735    /// Operation is not permitted for the current router/device policy.
736    PermissionDenied,
737
738    /// Packing error.
739    Pack(&'static str),
740
741    /// Unpacking error.
742    Unpack(&'static str),
743
744    /// IO / transport error.
745    Io(&'static str),
746
747    /// UTF-8 decoding failed where string payloads are expected.
748    InvalidUtf8,
749
750    /// Payload type size mismatch.
751    TypeMismatch { expected: usize, got: usize },
752
753    /// Invalid link ID provided.
754    InvalidLinkId(&'static str),
755
756    /// Packet is bigger than the queue size
757    PacketTooLarge(&'static str),
758}
759
760impl TelemetryError {
761    /// Map a rich [`TelemetryError`] to a stable numeric error code
762    /// used by the FFI layers.
763    pub const fn to_error_code(&self) -> TelemetryErrorCode {
764        match self {
765            TelemetryError::GenericError(_) => TelemetryErrorCode::GenericError,
766            TelemetryError::InvalidType => TelemetryErrorCode::InvalidType,
767            TelemetryError::SizeMismatch { .. } => TelemetryErrorCode::SizeMismatch,
768            TelemetryError::SizeMismatchError => TelemetryErrorCode::SizeMismatchError,
769            TelemetryError::EmptyEndpoints => TelemetryErrorCode::EmptyEndpoints,
770            TelemetryError::TimestampInvalid => TelemetryErrorCode::TimestampInvalid,
771            TelemetryError::MissingPayload => TelemetryErrorCode::MissingPayload,
772            TelemetryError::HandlerError(_) => TelemetryErrorCode::HandlerError,
773            TelemetryError::BadArg => TelemetryErrorCode::BadArg,
774            TelemetryError::PermissionDenied => TelemetryErrorCode::PermissionDenied,
775            TelemetryError::Pack(_) => TelemetryErrorCode::Pack,
776            TelemetryError::Unpack(_) => TelemetryErrorCode::Unpack,
777            TelemetryError::Io(_) => TelemetryErrorCode::Io,
778            TelemetryError::InvalidUtf8 => TelemetryErrorCode::InvalidUtf8,
779            TelemetryError::TypeMismatch { .. } => TelemetryErrorCode::TypeMismatch,
780            TelemetryError::InvalidLinkId(_) => TelemetryErrorCode::InvalidLinkId,
781            TelemetryError::PacketTooLarge(_) => TelemetryErrorCode::PacketTooLarge,
782        }
783    }
784}
785
786/// Allow conversion of `TelemetryError` to human-readable string.
787impl core::fmt::Display for TelemetryError {
788    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
789        f.write_str(&TelemetryError::to_string(self))
790    }
791}
792
793/// Implement `std::error::Error` for `TelemetryError` when `std` is enabled.
794#[cfg(feature = "std")]
795impl std::error::Error for TelemetryError {
796    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
797        None
798    }
799}
800
801/// Allow the conversion from std error to telemetry error
802#[cfg(feature = "std")]
803impl From<Error> for TelemetryError {
804    fn from(error: Error) -> Self {
805        let str = error.to_string();
806        let astr: Arc<str> = Arc::from(str.as_str());
807        TelemetryError::GenericError(Some(astr))
808    }
809}
810
811/// Allow the conversion from boxed std error to telemetry error
812#[cfg(feature = "std")]
813impl From<Box<dyn std::error::Error>> for TelemetryError {
814    fn from(err: Box<dyn std::error::Error>) -> Self {
815        let str = err.to_string();
816        let astr: Arc<str> = Arc::from(str.as_str());
817        TelemetryError::GenericError(Some(astr))
818    }
819}
820
821/// Numeric error codes used on the C/Python FFI boundary.
822///
823/// Negative values are used to avoid collisions with success codes
824/// and other positive return values (e.g. lengths).
825#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826#[repr(i32)]
827pub enum TelemetryErrorCode {
828    GenericError = -2,
829    InvalidType = -3,
830    SizeMismatch = -4,
831    SizeMismatchError = -5,
832    EmptyEndpoints = -6,
833    TimestampInvalid = -7,
834    MissingPayload = -8,
835    HandlerError = -9,
836    BadArg = -10,
837    PermissionDenied = -11,
838    Pack = -12,
839    Unpack = -13,
840    Io = -14,
841    InvalidUtf8 = -15,
842    TypeMismatch = -16,
843    InvalidLinkId = -17,
844    PacketTooLarge = -18,
845}
846
847// Generate ReprI32Enum helpers for TelemetryErrorCode
848impl_repr_i32_enum!(
849    TelemetryErrorCode,
850    TelemetryErrorCode::MAX,
851    TelemetryErrorCode::MIN
852);
853
854impl TelemetryErrorCode {
855    /// Maximum valid numeric error code value.
856    pub const MAX: i32 = TelemetryErrorCode::InvalidType as i32;
857
858    /// Minimum valid numeric error code value.
859    pub const MIN: i32 = TelemetryErrorCode::PacketTooLarge as i32;
860
861    /// Human-readable string for logging / debugging.
862    /// # Returns
863    /// - Static string representation of the error code.
864    #[inline]
865    pub fn as_str(&self) -> &'static str {
866        match self {
867            TelemetryErrorCode::GenericError => "GenericError",
868            TelemetryErrorCode::InvalidType => "{Invalid Type}",
869            TelemetryErrorCode::SizeMismatch => "{Size Mismatch}",
870            TelemetryErrorCode::SizeMismatchError => "{Size Mismatch Error}",
871            TelemetryErrorCode::EmptyEndpoints => "{Empty Endpoints}",
872            TelemetryErrorCode::TimestampInvalid => "{Timestamp Invalid}",
873            TelemetryErrorCode::MissingPayload => "{Missing Payload}",
874            TelemetryErrorCode::HandlerError => "{Handler Error}",
875            TelemetryErrorCode::BadArg => "{Bad Arg}",
876            TelemetryErrorCode::PermissionDenied => "{Permission Denied}",
877            TelemetryErrorCode::Pack => "{Pack Error}",
878            TelemetryErrorCode::Unpack => "{Unpack Error}",
879            TelemetryErrorCode::Io => "{IO Error}",
880            TelemetryErrorCode::InvalidUtf8 => "{Invalid UTF-8}",
881            TelemetryErrorCode::TypeMismatch => "{Type Mismatch}",
882            TelemetryErrorCode::InvalidLinkId => "{Invalid Link ID}",
883            TelemetryErrorCode::PacketTooLarge => "{Packet Too Large}",
884        }
885    }
886
887    /// Try to convert a raw i32 error code into a [`TelemetryErrorCode`].
888    ///
889    /// Returns `None` if the code is out of range or not recognized.
890    /// # Arguments
891    /// - `x`: Raw i32 error code to convert.
892    /// # Returns
893    /// - `Some(TelemetryErrorCode)` if valid, `None` if invalid.
894    #[inline]
895    pub fn try_from_i32(x: i32) -> Option<Self> {
896        try_enum_from_i32(x)
897    }
898}
899
900/// Common result alias for telemetry operations.
901pub type TelemetryResult<T> = Result<T, TelemetryError>;
902
903// ============================================================================
904//  Generic enum helpers (repr(u32) / repr(i32))
905// ============================================================================
906
907/// Try to convert a `u32` into a `#[repr(u32)]` enum `E`.
908///
909/// Returns `None` if the value is out of range (greater than `E::MAX`).
910/// # Arguments
911/// - `x`: Raw u32 value to convert.
912/// # Returns
913/// - `Some(E)` if valid, `None` if invalid.
914#[inline]
915pub fn try_enum_from_u32<E: ReprU32Enum>(x: u32) -> Option<E> {
916    if x > E::MAX {
917        return None;
918    }
919    E::from_u32(x)
920}
921
922/// Try to convert an `i32` into a `#[repr(i32)]` enum `E`.
923///
924/// Returns `None` if the value is outside the `[E::MIN, E::MAX]` range.
925/// # Arguments
926/// - `x`: Raw i32 value to convert.
927/// # Returns
928/// - `Some(E)` if valid, `None` if invalid.
929#[inline]
930pub fn try_enum_from_i32<E: ReprI32Enum>(x: i32) -> Option<E> {
931    if x < E::MIN || x > E::MAX {
932        return None;
933    }
934
935    // SAFETY: `E` is promised to be a fieldless #[repr(i32)] enum (thus 4 bytes, Copy).
936    let e = unsafe { (&x as *const i32 as *const E).read() };
937    Some(e)
938}