Skip to main content

sedsnet/
lib.rs

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