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    #[cfg(feature = "std")]
572    pub(crate) fn name_ref(&self) -> &str {
573        self.name.as_ref()
574    }
575
576    #[cfg(not(feature = "std"))]
577    pub(crate) fn name_ref(&self) -> &str {
578        self.name
579    }
580
581    #[cfg(feature = "std")]
582    pub(crate) fn description_ref(&self) -> &str {
583        self.description.as_ref()
584    }
585
586    #[cfg(not(feature = "std"))]
587    pub(crate) fn description_ref(&self) -> &str {
588        self.description
589    }
590
591    #[cfg(feature = "std")]
592    pub(crate) fn endpoints_ref(&self) -> &[DataEndpoint] {
593        self.endpoints.as_ref()
594    }
595
596    #[cfg(not(feature = "std"))]
597    pub(crate) fn endpoints_ref(&self) -> &[DataEndpoint] {
598        self.endpoints
599    }
600}
601
602impl DataType {
603    /// Get the string representation of the DataType
604    #[cfg(feature = "std")]
605    pub fn as_str(&self) -> Arc<str> {
606        get_message_meta(*self).name
607    }
608
609    #[cfg(not(feature = "std"))]
610    pub fn as_str(&self) -> &'static str {
611        get_message_meta(*self).name
612    }
613
614    /// Return the human-readable data type description.
615    #[cfg(feature = "std")]
616    pub fn description(&self) -> Arc<str> {
617        get_message_meta(*self).description
618    }
619
620    #[cfg(not(feature = "std"))]
621    pub fn description(&self) -> &'static str {
622        get_message_meta(*self).description
623    }
624}
625/// Lookup `MessageMeta` for a given [`DataType`] using the generated config.
626/// # Arguments
627/// - `ty`: Logical data type to query.
628/// # Returns
629/// - `MessageMeta` struct with element count and allowed endpoints.
630#[inline]
631pub fn message_meta(ty: DataType) -> MessageMeta {
632    get_message_meta(ty)
633}
634
635/// Return whether the given [`DataType`] is configured for reliable delivery.
636#[inline]
637pub fn is_reliable_type(ty: DataType) -> bool {
638    !matches!(get_message_meta(ty).reliable, ReliableMode::None)
639}
640
641/// Return the reliable delivery mode for the given [`DataType`].
642#[inline]
643pub fn reliable_mode(ty: DataType) -> ReliableMode {
644    get_message_meta(ty).reliable
645}
646
647/// Return the queue priority for the given [`DataType`].
648#[inline]
649pub fn message_priority(ty: DataType) -> u8 {
650    get_message_meta(ty).priority
651}
652
653/// Priority used by the router/relay schedulers.
654///
655/// Protocol control traffic has reserved bands so an application schema
656/// cannot accidentally starve discovery, managed variables, or time sync by
657/// assigning a user message priority of 255. The priority stored in the
658/// schema is otherwise preserved, including the relative ordering of all
659/// application-defined messages.
660#[inline]
661pub fn transport_priority(ty: DataType) -> u8 {
662    match ty {
663        // Delivery control must be able to release reliable packets already
664        // occupying queues. Treat it as part of the highest control band.
665        DataType::ReliableAck | DataType::ReliablePartialAck | DataType::ReliablePacketRequest => {
666            255
667        }
668
669        // A usable route is a prerequisite for every other network service.
670        DataType::DiscoveryAnnounce
671        | DataType::DiscoveryTimeSyncSources
672        | DataType::DiscoveryTopology
673        | DataType::DiscoveryTopologyRequest
674        | DataType::DiscoveryLeave
675        | DataType::DiscoveryLinkCapabilities
676        | DataType::DiscoveryAddress => 255,
677
678        // Schema transfer is discovery-plane repair, but unlike address and
679        // topology advertisements it is not needed to establish a route.
680        // Keep large schema frames below route convergence so they cannot
681        // head-of-line block a constrained serial or radio link.
682        DataType::DiscoverySchema
683        | DataType::DiscoverySchemaRequest
684        | DataType::ManagedVariableRequest
685        | DataType::ManagedVariableValue
686        | DataType::TimeSyncAnnounce
687        | DataType::TimeSyncRequest
688        | DataType::TimeSyncResponse => 254,
689
690        // Preserve application ordering below the reserved control bands.
691        _ => message_priority(ty).min(253),
692    }
693}
694
695/// Small frozen byte controls retain their delivery class across relays that
696/// are not local readers. Ordinary telemetry keeps its schema priority.
697#[inline]
698pub(crate) fn small_frozen_control(
699    ty: DataType,
700    owners: usize,
701    shape: Option<MessageElement>,
702) -> bool {
703    if !(1..=3).contains(&owners) {
704        return false;
705    }
706    config::try_get_message_meta(ty).is_some_and(|meta| {
707        matches!(
708            meta.element,
709            MessageElement::Static(0..=16, MessageDataType::UInt8, _)
710        ) && matches!(
711            shape,
712            None | Some(MessageElement::Static(0..=16, MessageDataType::UInt8, _))
713        )
714    })
715}
716
717// Internal scheduler name emphasizes where this priority is applied.
718#[inline]
719pub(crate) fn scheduler_priority(ty: DataType) -> u8 {
720    transport_priority(ty)
721}
722
723/// Return the end-to-end cryptography policy for a data type.
724#[inline]
725pub fn message_e2e_encryption_policy(ty: DataType) -> E2eEncryptionPolicy {
726    get_message_meta(ty).e2e_encryption
727}
728
729// ---- Convenience multiplication helpers ----
730
731impl Mul<MessageElement> for usize {
732    type Output = usize;
733
734    #[inline]
735    fn mul(self, rhs: MessageElement) -> usize {
736        self * rhs.into()
737    }
738}
739
740impl Mul<usize> for MessageElement {
741    type Output = usize;
742
743    #[inline]
744    fn mul(self, rhs: usize) -> usize {
745        self.into() * rhs
746    }
747}
748
749impl MessageElement {
750    /// Convert the element count to a `usize`.
751    ///
752    /// - `Static(n)` → `n`
753    /// - `Dynamic`   → `0` (caller must handle dynamic sizing separately)
754    #[inline]
755    fn into(self) -> usize {
756        match self {
757            MessageElement::Static(a, _, _) => a,
758            _ => 0,
759        }
760    }
761}
762
763/// Return the total payload size (in bytes) required for a given `DataType`
764/// under the *static* schema.
765///
766/// This is `element_size * element_count`. For dynamic types, the
767/// configuration ensures we only call this where it makes sense.
768/// # Arguments
769/// - `ty`: Logical data type to query.
770/// # Returns
771/// - Total static payload size in bytes.
772#[inline]
773pub fn get_needed_message_size(ty: DataType) -> usize {
774    data_type_size(get_data_type(ty)) * get_message_meta(ty).element
775}
776
777/// Return the logical "info" type (Info/Error) for a given `DataType`.
778/// # Arguments
779/// - `ty`: Logical data type to query.
780/// # Returns
781/// - `MessageType` enum value.
782#[inline]
783pub fn get_info_type(ty: DataType) -> MessageClass {
784    get_message_meta(ty).element.message_type()
785}
786
787/// Return the *element* data type (e.g., `Float32`, `Int16`, `String`) for a
788/// given `DataType`.
789/// # Arguments
790/// - `ty`: Logical data type to query.
791/// # Returns
792/// - `MessageDataType` enum value.
793#[inline]
794pub fn get_data_type(ty: DataType) -> MessageDataType {
795    get_message_meta(ty).element.data_type()
796}
797
798/// Return the message name for a given `DataType`.
799/// # Arguments
800/// - `ty`: Logical data type to query.
801/// # Returns
802/// - Static string name of the message type.
803#[inline]
804#[cfg(feature = "std")]
805pub fn get_message_name(ty: DataType) -> Arc<str> {
806    get_message_meta(ty).name
807}
808
809#[cfg(not(feature = "std"))]
810pub fn get_message_name(ty: DataType) -> &'static str {
811    get_message_meta(ty).name
812}
813
814/// Return the default endpoints for a given `DataType`.
815/// # Arguments
816/// - `ty`: Logical data type to query.
817/// # Returns
818/// - Slice of allowed `DataEndpoint` values.
819#[inline]
820#[cfg(feature = "std")]
821pub fn endpoints_from_datatype(ty: DataType) -> Arc<[DataEndpoint]> {
822    get_message_meta(ty).endpoints
823}
824
825#[cfg(not(feature = "std"))]
826pub fn endpoints_from_datatype(ty: DataType) -> &'static [DataEndpoint] {
827    get_message_meta(ty).endpoints
828}
829
830/// Primitive element type used by a message.
831///
832/// This is the underlying "slot" type, not the high-level `DataType`
833/// (which is the logical schema type).
834#[allow(dead_code)]
835#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
836pub enum MessageDataType {
837    Float64,
838    Float32,
839    UInt8,
840    UInt16,
841    UInt32,
842    UInt64,
843    UInt128,
844    Int8,
845    Int16,
846    Int32,
847    Int64,
848    Int128,
849    Bool,
850    String,
851    Binary,
852    NoData,
853}
854
855/// Size in bytes of a single element for the given [`MessageDataType`].
856///
857/// For `String` / `Hex`, this returns the fixed maximum static length
858/// configured by the schema (`MAX_STATIC_STRING_LENGTH`, `MAX_STATIC_HEX_LENGTH`).
859/// # Arguments
860/// - `dt`: Logical data type to query.
861/// # Returns
862/// - Size in bytes of a single element of that type.
863#[inline]
864pub fn data_type_size(dt: MessageDataType) -> usize {
865    match dt {
866        MessageDataType::Float64 => size_of::<f64>(),
867        MessageDataType::Float32 => size_of::<f32>(),
868        MessageDataType::UInt8 => size_of::<u8>(),
869        MessageDataType::UInt16 => size_of::<u16>(),
870        MessageDataType::UInt32 => size_of::<u32>(),
871        MessageDataType::UInt64 => size_of::<u64>(),
872        MessageDataType::UInt128 => size_of::<u128>(),
873        MessageDataType::Int8 => size_of::<i8>(),
874        MessageDataType::Int16 => size_of::<i16>(),
875        MessageDataType::Int32 => size_of::<i32>(),
876        MessageDataType::Int64 => size_of::<i64>(),
877        MessageDataType::Int128 => size_of::<i128>(),
878        MessageDataType::Bool => size_of::<bool>(),
879        MessageDataType::String => runtime_static_string_length(),
880        MessageDataType::Binary => runtime_static_hex_length(),
881        MessageDataType::NoData => 0,
882    }
883}
884
885/// High-level classification of message kind.
886#[allow(dead_code)]
887#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
888pub enum MessageClass {
889    /// Informational telemetry.
890    Data,
891    /// Error / fault telemetry.
892    Error,
893    /// Warning telemetry.
894    Warning,
895}
896
897// ============================================================================
898//  Error types and error codes
899// ============================================================================
900
901/// Rich error type used throughout the telemetry crate.
902///
903/// Most public APIs expose a `TelemetryResult<T>` alias for
904/// `Result<T, TelemetryError>`.
905#[derive(Debug, Clone, PartialEq, Eq)]
906pub enum TelemetryError {
907    /// Generic / unspecified error.
908    GenericError(Option<Arc<str>>),
909
910    /// Logical type ID is not a valid [`DataType`].
911    InvalidType,
912
913    /// Payload size doesn't match the schema's expectations.
914    SizeMismatch { expected: usize, got: usize },
915
916    /// Legacy / generic size mismatch error (for C/Python parity).
917    SizeMismatchError,
918
919    /// No endpoints were supplied where they are required.
920    EmptyEndpoints,
921
922    /// Timestamp is invalid (e.g., zero when disallowed).
923    TimestampInvalid,
924
925    /// A packet is missing its payload bytes.
926    MissingPayload,
927
928    /// A handler (C/Python callback) returned an error.
929    HandlerError(&'static str),
930
931    /// Generic invalid argument from caller.
932    BadArg,
933
934    /// Operation is not permitted for the current router/device policy.
935    PermissionDenied,
936
937    /// Packing error.
938    Pack(&'static str),
939
940    /// Unpacking error.
941    Unpack(&'static str),
942
943    /// IO / transport error.
944    Io(&'static str),
945
946    /// UTF-8 decoding failed where string payloads are expected.
947    InvalidUtf8,
948
949    /// Payload type size mismatch.
950    TypeMismatch { expected: usize, got: usize },
951
952    /// Invalid link ID provided.
953    InvalidLinkId(&'static str),
954
955    /// Packet is bigger than the queue size
956    PacketTooLarge(&'static str),
957}
958
959impl TelemetryError {
960    /// Map a rich [`TelemetryError`] to a stable numeric error code
961    /// used by the FFI layers.
962    pub const fn to_error_code(&self) -> TelemetryErrorCode {
963        match self {
964            TelemetryError::GenericError(_) => TelemetryErrorCode::GenericError,
965            TelemetryError::InvalidType => TelemetryErrorCode::InvalidType,
966            TelemetryError::SizeMismatch { .. } => TelemetryErrorCode::SizeMismatch,
967            TelemetryError::SizeMismatchError => TelemetryErrorCode::SizeMismatchError,
968            TelemetryError::EmptyEndpoints => TelemetryErrorCode::EmptyEndpoints,
969            TelemetryError::TimestampInvalid => TelemetryErrorCode::TimestampInvalid,
970            TelemetryError::MissingPayload => TelemetryErrorCode::MissingPayload,
971            TelemetryError::HandlerError(_) => TelemetryErrorCode::HandlerError,
972            TelemetryError::BadArg => TelemetryErrorCode::BadArg,
973            TelemetryError::PermissionDenied => TelemetryErrorCode::PermissionDenied,
974            TelemetryError::Pack(_) => TelemetryErrorCode::Pack,
975            TelemetryError::Unpack(_) => TelemetryErrorCode::Unpack,
976            TelemetryError::Io(_) => TelemetryErrorCode::Io,
977            TelemetryError::InvalidUtf8 => TelemetryErrorCode::InvalidUtf8,
978            TelemetryError::TypeMismatch { .. } => TelemetryErrorCode::TypeMismatch,
979            TelemetryError::InvalidLinkId(_) => TelemetryErrorCode::InvalidLinkId,
980            TelemetryError::PacketTooLarge(_) => TelemetryErrorCode::PacketTooLarge,
981        }
982    }
983}
984
985/// Allow conversion of `TelemetryError` to human-readable string.
986impl core::fmt::Display for TelemetryError {
987    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
988        // ToString delegates to Display: calling it here recurses until the
989        // caller overflows its stack, including on embedded error paths.
990        // Derived Debug formats every variant and its details without recursion
991        // or an intermediate allocation.
992        core::fmt::Debug::fmt(self, f)
993    }
994}
995
996/// Implement `std::error::Error` for `TelemetryError` when `std` is enabled.
997#[cfg(feature = "std")]
998impl std::error::Error for TelemetryError {
999    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1000        None
1001    }
1002}
1003
1004/// Allow the conversion from std error to telemetry error
1005#[cfg(feature = "std")]
1006impl From<Error> for TelemetryError {
1007    fn from(error: Error) -> Self {
1008        let str = error.to_string();
1009        let astr: Arc<str> = Arc::from(str.as_str());
1010        TelemetryError::GenericError(Some(astr))
1011    }
1012}
1013
1014/// Allow the conversion from boxed std error to telemetry error
1015#[cfg(feature = "std")]
1016impl From<Box<dyn std::error::Error>> for TelemetryError {
1017    fn from(err: Box<dyn std::error::Error>) -> Self {
1018        let str = err.to_string();
1019        let astr: Arc<str> = Arc::from(str.as_str());
1020        TelemetryError::GenericError(Some(astr))
1021    }
1022}
1023
1024/// Numeric error codes used on the C/Python FFI boundary.
1025///
1026/// Negative values are used to avoid collisions with success codes
1027/// and other positive return values (e.g. lengths).
1028#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1029#[repr(i32)]
1030pub enum TelemetryErrorCode {
1031    GenericError = -2,
1032    InvalidType = -3,
1033    SizeMismatch = -4,
1034    SizeMismatchError = -5,
1035    EmptyEndpoints = -6,
1036    TimestampInvalid = -7,
1037    MissingPayload = -8,
1038    HandlerError = -9,
1039    BadArg = -10,
1040    PermissionDenied = -11,
1041    Pack = -12,
1042    Unpack = -13,
1043    Io = -14,
1044    InvalidUtf8 = -15,
1045    TypeMismatch = -16,
1046    InvalidLinkId = -17,
1047    PacketTooLarge = -18,
1048}
1049
1050// Generate ReprI32Enum helpers for TelemetryErrorCode
1051impl_repr_i32_enum!(
1052    TelemetryErrorCode,
1053    TelemetryErrorCode::MAX,
1054    TelemetryErrorCode::MIN
1055);
1056
1057impl TelemetryErrorCode {
1058    /// Maximum valid numeric error code value.
1059    pub const MAX: i32 = TelemetryErrorCode::InvalidType as i32;
1060
1061    /// Minimum valid numeric error code value.
1062    pub const MIN: i32 = TelemetryErrorCode::PacketTooLarge as i32;
1063
1064    /// Human-readable string for logging / debugging.
1065    /// # Returns
1066    /// - Static string representation of the error code.
1067    #[inline]
1068    pub fn as_str(&self) -> &'static str {
1069        match self {
1070            TelemetryErrorCode::GenericError => "GenericError",
1071            TelemetryErrorCode::InvalidType => "{Invalid Type}",
1072            TelemetryErrorCode::SizeMismatch => "{Size Mismatch}",
1073            TelemetryErrorCode::SizeMismatchError => "{Size Mismatch Error}",
1074            TelemetryErrorCode::EmptyEndpoints => "{Empty Endpoints}",
1075            TelemetryErrorCode::TimestampInvalid => "{Timestamp Invalid}",
1076            TelemetryErrorCode::MissingPayload => "{Missing Payload}",
1077            TelemetryErrorCode::HandlerError => "{Handler Error}",
1078            TelemetryErrorCode::BadArg => "{Bad Arg}",
1079            TelemetryErrorCode::PermissionDenied => "{Permission Denied}",
1080            TelemetryErrorCode::Pack => "{Pack Error}",
1081            TelemetryErrorCode::Unpack => "{Unpack Error}",
1082            TelemetryErrorCode::Io => "{IO Error}",
1083            TelemetryErrorCode::InvalidUtf8 => "{Invalid UTF-8}",
1084            TelemetryErrorCode::TypeMismatch => "{Type Mismatch}",
1085            TelemetryErrorCode::InvalidLinkId => "{Invalid Link ID}",
1086            TelemetryErrorCode::PacketTooLarge => "{Packet Too Large}",
1087        }
1088    }
1089
1090    /// Try to convert a raw i32 error code into a [`TelemetryErrorCode`].
1091    ///
1092    /// Returns `None` if the code is out of range or not recognized.
1093    /// # Arguments
1094    /// - `x`: Raw i32 error code to convert.
1095    /// # Returns
1096    /// - `Some(TelemetryErrorCode)` if valid, `None` if invalid.
1097    #[inline]
1098    pub fn try_from_i32(x: i32) -> Option<Self> {
1099        try_enum_from_i32(x)
1100    }
1101}
1102
1103/// Common result alias for telemetry operations.
1104pub type TelemetryResult<T> = Result<T, TelemetryError>;
1105
1106// ============================================================================
1107//  Generic enum helpers (repr(u32) / repr(i32))
1108// ============================================================================
1109
1110/// Try to convert a `u32` into a `#[repr(u32)]` enum `E`.
1111///
1112/// Returns `None` if the value is out of range (greater than `E::MAX`).
1113/// # Arguments
1114/// - `x`: Raw u32 value to convert.
1115/// # Returns
1116/// - `Some(E)` if valid, `None` if invalid.
1117#[inline]
1118pub fn try_enum_from_u32<E: ReprU32Enum>(x: u32) -> Option<E> {
1119    if x > E::MAX {
1120        return None;
1121    }
1122    E::from_u32(x)
1123}
1124
1125/// Try to convert an `i32` into a `#[repr(i32)]` enum `E`.
1126///
1127/// Returns `None` if the value is outside the `[E::MIN, E::MAX]` range.
1128/// # Arguments
1129/// - `x`: Raw i32 value to convert.
1130/// # Returns
1131/// - `Some(E)` if valid, `None` if invalid.
1132#[inline]
1133pub fn try_enum_from_i32<E: ReprI32Enum>(x: i32) -> Option<E> {
1134    if x < E::MIN || x > E::MAX {
1135        return None;
1136    }
1137
1138    // SAFETY: `E` is promised to be a fieldless #[repr(i32)] enum (thus 4 bytes, Copy).
1139    let e = unsafe { (&x as *const i32 as *const E).read() };
1140    Some(e)
1141}