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