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