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::DiscoverySchema
661        | DataType::DiscoveryTopologyRequest
662        | DataType::DiscoverySchemaRequest
663        | DataType::DiscoveryLeave
664        | DataType::DiscoveryLinkCapabilities
665        | DataType::DiscoveryAddress => 255,
666
667        // Managed variables and time sync intentionally share a band.
668        DataType::ManagedVariableRequest
669        | DataType::ManagedVariableValue
670        | DataType::TimeSyncAnnounce
671        | DataType::TimeSyncRequest
672        | DataType::TimeSyncResponse => 254,
673
674        // Preserve application ordering below the reserved control bands.
675        _ => message_priority(ty).min(253),
676    }
677}
678
679// Internal name retained so the router and relay code remain explicit about
680// where this priority is applied.
681#[inline]
682pub(crate) fn scheduler_priority(ty: DataType) -> u8 {
683    transport_priority(ty)
684}
685
686/// Return the end-to-end cryptography policy for a data type.
687#[inline]
688pub fn message_e2e_encryption_policy(ty: DataType) -> E2eEncryptionPolicy {
689    get_message_meta(ty).e2e_encryption
690}
691
692// ---- Convenience multiplication helpers ----
693
694impl Mul<MessageElement> for usize {
695    type Output = usize;
696
697    #[inline]
698    fn mul(self, rhs: MessageElement) -> usize {
699        self * rhs.into()
700    }
701}
702
703impl Mul<usize> for MessageElement {
704    type Output = usize;
705
706    #[inline]
707    fn mul(self, rhs: usize) -> usize {
708        self.into() * rhs
709    }
710}
711
712impl MessageElement {
713    /// Convert the element count to a `usize`.
714    ///
715    /// - `Static(n)` → `n`
716    /// - `Dynamic`   → `0` (caller must handle dynamic sizing separately)
717    #[inline]
718    fn into(self) -> usize {
719        match self {
720            MessageElement::Static(a, _, _) => a,
721            _ => 0,
722        }
723    }
724}
725
726/// Return the total payload size (in bytes) required for a given `DataType`
727/// under the *static* schema.
728///
729/// This is `element_size * element_count`. For dynamic types, the
730/// configuration ensures we only call this where it makes sense.
731/// # Arguments
732/// - `ty`: Logical data type to query.
733/// # Returns
734/// - Total static payload size in bytes.
735#[inline]
736pub fn get_needed_message_size(ty: DataType) -> usize {
737    data_type_size(get_data_type(ty)) * get_message_meta(ty).element
738}
739
740/// Return the logical "info" type (Info/Error) for a given `DataType`.
741/// # Arguments
742/// - `ty`: Logical data type to query.
743/// # Returns
744/// - `MessageType` enum value.
745#[inline]
746pub fn get_info_type(ty: DataType) -> MessageClass {
747    get_message_meta(ty).element.message_type()
748}
749
750/// Return the *element* data type (e.g., `Float32`, `Int16`, `String`) for a
751/// given `DataType`.
752/// # Arguments
753/// - `ty`: Logical data type to query.
754/// # Returns
755/// - `MessageDataType` enum value.
756#[inline]
757pub fn get_data_type(ty: DataType) -> MessageDataType {
758    get_message_meta(ty).element.data_type()
759}
760
761/// Return the message name for a given `DataType`.
762/// # Arguments
763/// - `ty`: Logical data type to query.
764/// # Returns
765/// - Static string name of the message type.
766#[inline]
767#[cfg(feature = "std")]
768pub fn get_message_name(ty: DataType) -> Arc<str> {
769    get_message_meta(ty).name
770}
771
772#[cfg(not(feature = "std"))]
773pub fn get_message_name(ty: DataType) -> &'static str {
774    get_message_meta(ty).name
775}
776
777/// Return the default endpoints for a given `DataType`.
778/// # Arguments
779/// - `ty`: Logical data type to query.
780/// # Returns
781/// - Slice of allowed `DataEndpoint` values.
782#[inline]
783#[cfg(feature = "std")]
784pub fn endpoints_from_datatype(ty: DataType) -> Arc<[DataEndpoint]> {
785    get_message_meta(ty).endpoints
786}
787
788#[cfg(not(feature = "std"))]
789pub fn endpoints_from_datatype(ty: DataType) -> &'static [DataEndpoint] {
790    get_message_meta(ty).endpoints
791}
792
793/// Primitive element type used by a message.
794///
795/// This is the underlying "slot" type, not the high-level `DataType`
796/// (which is the logical schema type).
797#[allow(dead_code)]
798#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
799pub enum MessageDataType {
800    Float64,
801    Float32,
802    UInt8,
803    UInt16,
804    UInt32,
805    UInt64,
806    UInt128,
807    Int8,
808    Int16,
809    Int32,
810    Int64,
811    Int128,
812    Bool,
813    String,
814    Binary,
815    NoData,
816}
817
818/// Size in bytes of a single element for the given [`MessageDataType`].
819///
820/// For `String` / `Hex`, this returns the fixed maximum static length
821/// configured by the schema (`MAX_STATIC_STRING_LENGTH`, `MAX_STATIC_HEX_LENGTH`).
822/// # Arguments
823/// - `dt`: Logical data type to query.
824/// # Returns
825/// - Size in bytes of a single element of that type.
826#[inline]
827pub fn data_type_size(dt: MessageDataType) -> usize {
828    match dt {
829        MessageDataType::Float64 => size_of::<f64>(),
830        MessageDataType::Float32 => size_of::<f32>(),
831        MessageDataType::UInt8 => size_of::<u8>(),
832        MessageDataType::UInt16 => size_of::<u16>(),
833        MessageDataType::UInt32 => size_of::<u32>(),
834        MessageDataType::UInt64 => size_of::<u64>(),
835        MessageDataType::UInt128 => size_of::<u128>(),
836        MessageDataType::Int8 => size_of::<i8>(),
837        MessageDataType::Int16 => size_of::<i16>(),
838        MessageDataType::Int32 => size_of::<i32>(),
839        MessageDataType::Int64 => size_of::<i64>(),
840        MessageDataType::Int128 => size_of::<i128>(),
841        MessageDataType::Bool => size_of::<bool>(),
842        MessageDataType::String => runtime_static_string_length(),
843        MessageDataType::Binary => runtime_static_hex_length(),
844        MessageDataType::NoData => 0,
845    }
846}
847
848/// High-level classification of message kind.
849#[allow(dead_code)]
850#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
851pub enum MessageClass {
852    /// Informational telemetry.
853    Data,
854    /// Error / fault telemetry.
855    Error,
856    /// Warning telemetry.
857    Warning,
858}
859
860// ============================================================================
861//  Error types and error codes
862// ============================================================================
863
864/// Rich error type used throughout the telemetry crate.
865///
866/// Most public APIs expose a `TelemetryResult<T>` alias for
867/// `Result<T, TelemetryError>`.
868#[derive(Debug, Clone, PartialEq, Eq)]
869pub enum TelemetryError {
870    /// Generic / unspecified error.
871    GenericError(Option<Arc<str>>),
872
873    /// Logical type ID is not a valid [`DataType`].
874    InvalidType,
875
876    /// Payload size doesn't match the schema's expectations.
877    SizeMismatch { expected: usize, got: usize },
878
879    /// Legacy / generic size mismatch error (for C/Python parity).
880    SizeMismatchError,
881
882    /// No endpoints were supplied where they are required.
883    EmptyEndpoints,
884
885    /// Timestamp is invalid (e.g., zero when disallowed).
886    TimestampInvalid,
887
888    /// A packet is missing its payload bytes.
889    MissingPayload,
890
891    /// A handler (C/Python callback) returned an error.
892    HandlerError(&'static str),
893
894    /// Generic invalid argument from caller.
895    BadArg,
896
897    /// Operation is not permitted for the current router/device policy.
898    PermissionDenied,
899
900    /// Packing error.
901    Pack(&'static str),
902
903    /// Unpacking error.
904    Unpack(&'static str),
905
906    /// IO / transport error.
907    Io(&'static str),
908
909    /// UTF-8 decoding failed where string payloads are expected.
910    InvalidUtf8,
911
912    /// Payload type size mismatch.
913    TypeMismatch { expected: usize, got: usize },
914
915    /// Invalid link ID provided.
916    InvalidLinkId(&'static str),
917
918    /// Packet is bigger than the queue size
919    PacketTooLarge(&'static str),
920}
921
922impl TelemetryError {
923    /// Map a rich [`TelemetryError`] to a stable numeric error code
924    /// used by the FFI layers.
925    pub const fn to_error_code(&self) -> TelemetryErrorCode {
926        match self {
927            TelemetryError::GenericError(_) => TelemetryErrorCode::GenericError,
928            TelemetryError::InvalidType => TelemetryErrorCode::InvalidType,
929            TelemetryError::SizeMismatch { .. } => TelemetryErrorCode::SizeMismatch,
930            TelemetryError::SizeMismatchError => TelemetryErrorCode::SizeMismatchError,
931            TelemetryError::EmptyEndpoints => TelemetryErrorCode::EmptyEndpoints,
932            TelemetryError::TimestampInvalid => TelemetryErrorCode::TimestampInvalid,
933            TelemetryError::MissingPayload => TelemetryErrorCode::MissingPayload,
934            TelemetryError::HandlerError(_) => TelemetryErrorCode::HandlerError,
935            TelemetryError::BadArg => TelemetryErrorCode::BadArg,
936            TelemetryError::PermissionDenied => TelemetryErrorCode::PermissionDenied,
937            TelemetryError::Pack(_) => TelemetryErrorCode::Pack,
938            TelemetryError::Unpack(_) => TelemetryErrorCode::Unpack,
939            TelemetryError::Io(_) => TelemetryErrorCode::Io,
940            TelemetryError::InvalidUtf8 => TelemetryErrorCode::InvalidUtf8,
941            TelemetryError::TypeMismatch { .. } => TelemetryErrorCode::TypeMismatch,
942            TelemetryError::InvalidLinkId(_) => TelemetryErrorCode::InvalidLinkId,
943            TelemetryError::PacketTooLarge(_) => TelemetryErrorCode::PacketTooLarge,
944        }
945    }
946}
947
948/// Allow conversion of `TelemetryError` to human-readable string.
949impl core::fmt::Display for TelemetryError {
950    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
951        f.write_str(&TelemetryError::to_string(self))
952    }
953}
954
955/// Implement `std::error::Error` for `TelemetryError` when `std` is enabled.
956#[cfg(feature = "std")]
957impl std::error::Error for TelemetryError {
958    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
959        None
960    }
961}
962
963/// Allow the conversion from std error to telemetry error
964#[cfg(feature = "std")]
965impl From<Error> for TelemetryError {
966    fn from(error: Error) -> Self {
967        let str = error.to_string();
968        let astr: Arc<str> = Arc::from(str.as_str());
969        TelemetryError::GenericError(Some(astr))
970    }
971}
972
973/// Allow the conversion from boxed std error to telemetry error
974#[cfg(feature = "std")]
975impl From<Box<dyn std::error::Error>> for TelemetryError {
976    fn from(err: Box<dyn std::error::Error>) -> Self {
977        let str = err.to_string();
978        let astr: Arc<str> = Arc::from(str.as_str());
979        TelemetryError::GenericError(Some(astr))
980    }
981}
982
983/// Numeric error codes used on the C/Python FFI boundary.
984///
985/// Negative values are used to avoid collisions with success codes
986/// and other positive return values (e.g. lengths).
987#[derive(Debug, Clone, Copy, PartialEq, Eq)]
988#[repr(i32)]
989pub enum TelemetryErrorCode {
990    GenericError = -2,
991    InvalidType = -3,
992    SizeMismatch = -4,
993    SizeMismatchError = -5,
994    EmptyEndpoints = -6,
995    TimestampInvalid = -7,
996    MissingPayload = -8,
997    HandlerError = -9,
998    BadArg = -10,
999    PermissionDenied = -11,
1000    Pack = -12,
1001    Unpack = -13,
1002    Io = -14,
1003    InvalidUtf8 = -15,
1004    TypeMismatch = -16,
1005    InvalidLinkId = -17,
1006    PacketTooLarge = -18,
1007}
1008
1009// Generate ReprI32Enum helpers for TelemetryErrorCode
1010impl_repr_i32_enum!(
1011    TelemetryErrorCode,
1012    TelemetryErrorCode::MAX,
1013    TelemetryErrorCode::MIN
1014);
1015
1016impl TelemetryErrorCode {
1017    /// Maximum valid numeric error code value.
1018    pub const MAX: i32 = TelemetryErrorCode::InvalidType as i32;
1019
1020    /// Minimum valid numeric error code value.
1021    pub const MIN: i32 = TelemetryErrorCode::PacketTooLarge as i32;
1022
1023    /// Human-readable string for logging / debugging.
1024    /// # Returns
1025    /// - Static string representation of the error code.
1026    #[inline]
1027    pub fn as_str(&self) -> &'static str {
1028        match self {
1029            TelemetryErrorCode::GenericError => "GenericError",
1030            TelemetryErrorCode::InvalidType => "{Invalid Type}",
1031            TelemetryErrorCode::SizeMismatch => "{Size Mismatch}",
1032            TelemetryErrorCode::SizeMismatchError => "{Size Mismatch Error}",
1033            TelemetryErrorCode::EmptyEndpoints => "{Empty Endpoints}",
1034            TelemetryErrorCode::TimestampInvalid => "{Timestamp Invalid}",
1035            TelemetryErrorCode::MissingPayload => "{Missing Payload}",
1036            TelemetryErrorCode::HandlerError => "{Handler Error}",
1037            TelemetryErrorCode::BadArg => "{Bad Arg}",
1038            TelemetryErrorCode::PermissionDenied => "{Permission Denied}",
1039            TelemetryErrorCode::Pack => "{Pack Error}",
1040            TelemetryErrorCode::Unpack => "{Unpack Error}",
1041            TelemetryErrorCode::Io => "{IO Error}",
1042            TelemetryErrorCode::InvalidUtf8 => "{Invalid UTF-8}",
1043            TelemetryErrorCode::TypeMismatch => "{Type Mismatch}",
1044            TelemetryErrorCode::InvalidLinkId => "{Invalid Link ID}",
1045            TelemetryErrorCode::PacketTooLarge => "{Packet Too Large}",
1046        }
1047    }
1048
1049    /// Try to convert a raw i32 error code into a [`TelemetryErrorCode`].
1050    ///
1051    /// Returns `None` if the code is out of range or not recognized.
1052    /// # Arguments
1053    /// - `x`: Raw i32 error code to convert.
1054    /// # Returns
1055    /// - `Some(TelemetryErrorCode)` if valid, `None` if invalid.
1056    #[inline]
1057    pub fn try_from_i32(x: i32) -> Option<Self> {
1058        try_enum_from_i32(x)
1059    }
1060}
1061
1062/// Common result alias for telemetry operations.
1063pub type TelemetryResult<T> = Result<T, TelemetryError>;
1064
1065// ============================================================================
1066//  Generic enum helpers (repr(u32) / repr(i32))
1067// ============================================================================
1068
1069/// Try to convert a `u32` into a `#[repr(u32)]` enum `E`.
1070///
1071/// Returns `None` if the value is out of range (greater than `E::MAX`).
1072/// # Arguments
1073/// - `x`: Raw u32 value to convert.
1074/// # Returns
1075/// - `Some(E)` if valid, `None` if invalid.
1076#[inline]
1077pub fn try_enum_from_u32<E: ReprU32Enum>(x: u32) -> Option<E> {
1078    if x > E::MAX {
1079        return None;
1080    }
1081    E::from_u32(x)
1082}
1083
1084/// Try to convert an `i32` into a `#[repr(i32)]` enum `E`.
1085///
1086/// Returns `None` if the value is outside the `[E::MIN, E::MAX]` range.
1087/// # Arguments
1088/// - `x`: Raw i32 value to convert.
1089/// # Returns
1090/// - `Some(E)` if valid, `None` if invalid.
1091#[inline]
1092pub fn try_enum_from_i32<E: ReprI32Enum>(x: i32) -> Option<E> {
1093    if x < E::MIN || x > E::MAX {
1094        return None;
1095    }
1096
1097    // SAFETY: `E` is promised to be a fieldless #[repr(i32)] enum (thus 4 bytes, Copy).
1098    let e = unsafe { (&x as *const i32 as *const E).read() };
1099    Some(e)
1100}