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