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