peat_btle/lib.rs
1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! PEAT-BTLE: Bluetooth Low Energy mesh transport for Peat Protocol
17//!
18//! This crate provides BLE-based peer-to-peer mesh networking for Peat,
19//! supporting discovery, advertisement, connectivity, and Peat-Lite sync.
20//!
21//! ## Overview
22//!
23//! PEAT-BTLE implements the pluggable transport abstraction (ADR-032) for
24//! Bluetooth Low Energy, enabling Peat Protocol to operate over BLE in
25//! resource-constrained environments like smartwatches.
26//!
27//! ## Key Features
28//!
29//! - **Cross-platform**: Linux, Android, macOS, iOS, Windows, ESP32
30//! - **Power efficient**: Designed for 18+ hour battery life on watches
31//! - **Long range**: Coded PHY support for 300m+ range
32//! - **Peat-Lite sync**: Optimized CRDT sync over GATT
33//!
34//! ## Architecture
35//!
36//! ```text
37//! ┌─────────────────────────────────────────────────┐
38//! │ Application │
39//! ├─────────────────────────────────────────────────┤
40//! │ BluetoothLETransport │
41//! │ (implements MeshTransport from ADR-032) │
42//! ├─────────────────────────────────────────────────┤
43//! │ BleAdapter Trait │
44//! ├──────────┬──────────┬──────────┬────────────────┤
45//! │ Linux │ Android │ Apple │ Windows │
46//! │ (BlueZ) │ (JNI) │(CoreBT) │ (WinRT) │
47//! └──────────┴──────────┴──────────┴────────────────┘
48//! ```
49//!
50//! ## Quick Start
51//!
52//! ```ignore
53//! use peat_btle::{BleConfig, BluetoothLETransport, NodeId};
54//!
55//! // Create Peat-Lite optimized config for battery efficiency
56//! let config = BleConfig::peat_lite(NodeId::new(0x12345678));
57//!
58//! // Create transport with platform adapter
59//! #[cfg(feature = "linux")]
60//! let adapter = peat_btle::platform::linux::BluerAdapter::new()?;
61//!
62//! let transport = BluetoothLETransport::new(config, adapter);
63//!
64//! // Start advertising and scanning
65//! transport.start().await?;
66//!
67//! // Connect to a peer
68//! let conn = transport.connect(&peer_id).await?;
69//! ```
70//!
71//! ## Feature Flags
72//!
73//! - `std` (default): Standard library support
74//! - `transport-only`: Pure BLE transport, no app-layer CRDTs
75//! - `legacy-chat`: Deprecated ChatCRDT support (will be removed in 0.2.0)
76//! - `linux`: Linux/BlueZ support via `bluer`
77//! - `android`: Android support via JNI
78//! - `macos`: macOS support via CoreBluetooth
79//! - `ios`: iOS support via CoreBluetooth
80//! - `windows`: Windows support via WinRT
81//! - `embedded`: Embedded/no_std support
82//! - `coded-phy`: Enable Coded PHY for extended range
83//! - `extended-adv`: Enable extended advertising
84//!
85//! ## External Crate Usage (peat-ffi)
86//!
87//! This crate exports platform adapters for use by external crates like `peat-ffi`.
88//! Each platform adapter is conditionally exported based on feature flags:
89//!
90//! ```toml
91//! # In your Cargo.toml
92//! [dependencies]
93//! peat-btle = { version = "0.2.0", features = ["linux"] }
94//! ```
95//!
96//! Then use the appropriate adapter:
97//!
98//! ```ignore
99//! use peat_btle::{BleConfig, BluerAdapter, PeatMesh, NodeId};
100//!
101//! // Platform adapter is automatically available via feature flag
102//! let adapter = BluerAdapter::new().await?;
103//! let config = BleConfig::peat_lite(NodeId::new(0x12345678));
104//! ```
105//!
106//! ### Platform → Adapter Mapping
107//!
108//! | Feature | Target | Adapter Type |
109//! |---------|--------|--------------|
110//! | `linux` | Linux | `BluerAdapter` |
111//! | `android` | Android | `AndroidAdapter` |
112//! | `macos` | macOS | `CoreBluetoothAdapter` |
113//! | `ios` | iOS | `CoreBluetoothAdapter` |
114//! | `windows` | Windows | `WinRtBleAdapter` |
115//!
116//! ### Document Encoding for Translation Layer
117//!
118//! For translating between Automerge (full Peat) and peat-btle documents:
119//!
120//! ```ignore
121//! use peat_btle::PeatDocument;
122//!
123//! // Decode bytes received from BLE
124//! let doc = PeatDocument::from_bytes(&received_bytes)?;
125//!
126//! // Encode for BLE transmission
127//! let bytes = doc.to_bytes();
128//! ```
129//!
130//! ## Power Profiles
131//!
132//! | Profile | Duty Cycle | Watch Battery |
133//! |---------|------------|---------------|
134//! | Aggressive | 20% | ~6 hours |
135//! | Balanced | 10% | ~12 hours |
136//! | **LowPower** | **2%** | **~20+ hours** |
137//!
138//! ## Related ADRs
139//!
140//! - ADR-039: PEAT-BTLE Mesh Transport Crate
141//! - ADR-032: Pluggable Transport Abstraction
142//! - ADR-035: Peat-Lite Embedded Nodes
143//! - ADR-037: Resource-Constrained Device Optimization
144
145#![cfg_attr(not(feature = "std"), no_std)]
146#![warn(missing_docs)]
147#![warn(rustdoc::missing_crate_level_docs)]
148
149#[cfg(not(feature = "std"))]
150extern crate alloc;
151
152pub mod address_rotation;
153pub mod config;
154pub mod discovery;
155pub mod document;
156pub mod document_sync;
157pub mod error;
158pub mod gatt;
159#[cfg(feature = "std")]
160pub mod gossip;
161pub mod mesh;
162pub mod observer;
163pub mod peat_mesh;
164pub mod peer;
165pub mod peer_lifetime;
166pub mod peer_manager;
167#[cfg(feature = "std")]
168pub mod persistence;
169pub mod phy;
170pub mod platform;
171pub mod power;
172pub mod reconnect;
173pub mod registry;
174pub mod relay;
175
176pub mod security;
177pub mod sync;
178pub mod transport;
179
180// ADR-059 cross-transport bridging — `BleTranslator` + the
181// `peat_mesh::transport::Translator` impl. Gated behind the
182// `mesh-translator` Cargo feature so standalone BLE consumers
183// (Bitchat-style, embedded sensors) keep peat-mesh out of their dep
184// graph. See `translator.rs` module docs for the placement rule.
185#[cfg(feature = "mesh-translator")]
186pub mod translator;
187
188/// Receives [`MeshDocument`]s decoded from inbound BLE translator frames
189/// (ADR-059 Amendment 1).
190///
191/// Invoked by peat-btle's GATT receive dispatch after a 0xB6 frame is
192/// successfully routed through `BleTranslator::decode_inbound`. The
193/// callback is expected to be **non-blocking and panic-free**; consumers
194/// that need async ingest into `peat_mesh::Node::publish_with_origin`
195/// should spawn their own task.
196///
197/// `collection` is the BleTranslator collection name (e.g. `"tracks"`),
198/// suitable for direct use as the first argument to
199/// `Node::publish_with_origin`. `peer` carries the BLE peer's identifier
200/// for diagnostics and `target_nodes` checks (ADR-046).
201///
202/// Storage on `PeatMesh` is `Option<Arc<dyn DecodedDocumentCallback>>`
203/// initialized to `None` — the slice's specified release-skew window
204/// between peat-btle (1.b.3) shipping and peat-ffi (1.b.4) installing
205/// the callback is real. The no-callback path is no-op-drop *plus* a
206/// `PeatEvent::TranslatorNoCallback { collection, peer }` event on the
207/// observer channel, so the gap is operator-observable in real time
208/// rather than buried behind a `log::debug!` line.
209#[cfg(feature = "mesh-translator")]
210pub trait DecodedDocumentCallback: Send + Sync + 'static {
211 /// Invoked once per successfully-decoded inbound translator frame.
212 fn on_document(&self, collection: &str, doc: MeshDocument, peer: Option<&str>);
213}
214
215/// UniFFI-exported counterpart of [`DecodedDocumentCallback`]
216/// (ADR-059 Amendment 2).
217///
218/// Same wiring point on `PeatMesh` (set via
219/// [`PeatMesh::set_decoded_document_json_callback`](crate::peat_mesh::PeatMesh::set_decoded_document_json_callback)),
220/// same firing path inside the receive dispatch — but the
221/// `peat_mesh::sync::Document` payload is serialized to JSON before
222/// invocation so a Kotlin / Swift host can forward it through
223/// peat-ffi's existing `publishDocument`-shaped FFI without needing a
224/// UniFFI binding for `Document` itself.
225///
226/// **Why JSON-string instead of typed `Document`.** UniFFI 0.31 can't
227/// pass `peat_mesh::sync::Document` across the FFI boundary without
228/// bindings for the entire schema graph (`Document`, `Field`,
229/// `DocumentId`, all of `serde_json::Value`'s shape). peat-mesh has
230/// no UniFFI dependency today and adding one for a single callback's
231/// payload type is disproportionate. Hosts that already round-trip
232/// docs as JSON strings via peat-ffi gain a directly-forwardable
233/// payload; Rust-native consumers (peat-sim integration tests, future
234/// Rust hosts) keep using [`DecodedDocumentCallback`] and receive the
235/// typed `Document` directly.
236///
237/// Both callbacks fire independently when both are installed —
238/// neither suppresses the other. `PeatEvent::TranslatorNoCallback`
239/// fires only when **both** callback slots are empty (the
240/// release-skew window before any host has wired anything up).
241#[cfg(all(feature = "mesh-translator", feature = "uniffi"))]
242#[uniffi::export(callback_interface)]
243pub trait DecodedDocumentJsonCallback: Send + Sync {
244 /// `collection` is the BleTranslator collection name. `doc_json`
245 /// is the serde-JSON serialization of the decoded `Document`.
246 /// `peer` is the BLE peer identifier from the receive context.
247 fn on_document(&self, collection: String, doc_json: String, peer: Option<String>);
248}
249
250/// Re-export of `peat_mesh::sync::Document` as `MeshDocument`.
251///
252/// The local `crate::peat_mesh` module (containing the `PeatMesh` facade)
253/// shadows the extern crate name in any inline path written from
254/// `lib.rs`. The leading `::` forces extern_prelude resolution to reach
255/// the external `peat_mesh` crate, sidestepping the shadow. Identical
256/// to the alias used inside `crate::translator` (where the submodule
257/// scope reaches extern_prelude without the `::` prefix).
258#[cfg(feature = "mesh-translator")]
259pub use ::peat_mesh::sync::Document as MeshDocument;
260
261// UniFFI bindings (generates Kotlin + Swift)
262#[cfg(feature = "uniffi")]
263pub mod uniffi_bindings;
264
265// UniFFI scaffolding - must be at crate root
266#[cfg(feature = "uniffi")]
267uniffi::setup_scaffolding!();
268
269// Re-exports for convenience
270pub use config::{
271 BleConfig, BlePhy, DiscoveryConfig, GattConfig, MeshConfig, PowerProfile, DEFAULT_MESH_ID,
272};
273#[cfg(feature = "std")]
274pub use discovery::Scanner;
275pub use discovery::{Advertiser, PeatBeacon, ScanFilter};
276pub use error::{BleError, Result};
277#[cfg(feature = "std")]
278pub use gatt::PeatGattService;
279pub use gatt::SyncProtocol;
280#[cfg(feature = "std")]
281pub use mesh::MeshManager;
282pub use mesh::{MeshRouter, MeshTopology, TopologyConfig, TopologyEvent};
283pub use phy::{PhyCapabilities, PhyController, PhyStrategy};
284pub use platform::{BleAdapter, ConnectionEvent, DisconnectReason, DiscoveredDevice, StubAdapter};
285
286// Platform-specific adapter re-exports for external crates (peat-ffi)
287// These allow external crates to use platform adapters via feature flags
288#[cfg(all(feature = "linux", target_os = "linux"))]
289pub use platform::linux::BluerAdapter;
290
291#[cfg(feature = "android")]
292pub use platform::android::AndroidAdapter;
293
294#[cfg(any(feature = "macos", feature = "ios"))]
295pub use platform::apple::CoreBluetoothAdapter;
296
297#[cfg(feature = "windows")]
298pub use platform::windows::WinRtBleAdapter;
299
300#[cfg(feature = "std")]
301pub use platform::mock::MockBleAdapter;
302pub use power::{BatteryState, RadioScheduler, SyncPriority};
303pub use sync::{GattSyncProtocol, SyncConfig, SyncState};
304pub use transport::{BleConnection, BluetoothLETransport, MeshTransport, TransportCapabilities};
305
306// New centralized mesh management types
307pub use document::{
308 MergeResult, PeatDocument, ENCRYPTED_MARKER, EXTENDED_MARKER, KEY_EXCHANGE_MARKER,
309 PEER_E2EE_MARKER,
310};
311
312// Security (mesh-wide and per-peer encryption)
313pub use document_sync::{DocumentCheck, DocumentSync};
314#[cfg(feature = "std")]
315pub use observer::{CollectingObserver, ObserverManager};
316pub use observer::{DisconnectReason as PeatDisconnectReason, PeatEvent, PeatObserver};
317#[cfg(feature = "std")]
318pub use peat_mesh::{DataReceivedResult, PeatMesh, PeatMeshConfig, RelayDecision};
319pub use peer::{
320 ConnectionState, ConnectionStateGraph, FullStateCountSummary, IndirectPeer, PeatPeer,
321 PeerConnectionState, PeerDegree, PeerManagerConfig, SignalStrength, StateCountSummary,
322 MAX_TRACKED_DEGREE,
323};
324pub use peer_manager::PeerManager;
325
326// Device identity and attestation
327pub use security::{
328 DeviceIdentity, IdentityAttestation, IdentityError, IdentityRecord, IdentityRegistry,
329 RegistryResult,
330};
331// Mesh genesis and credentials
332pub use security::{MembershipPolicy, MeshCredentials, MeshGenesis};
333
334// Phase 1: Mesh-wide encryption
335pub use security::{EncryptedDocument, EncryptionError, MeshEncryptionKey};
336// Phase 2: Per-peer E2EE
337#[cfg(feature = "std")]
338pub use security::{
339 KeyExchangeMessage, PeerEncryptedMessage, PeerIdentityKey, PeerSession, PeerSessionKey,
340 PeerSessionManager, SessionState,
341};
342
343// Credential persistence
344#[cfg(feature = "std")]
345pub use security::{
346 MemoryStorage, PersistedState, PersistenceError, SecureStorage, PERSISTED_STATE_VERSION,
347};
348
349// Gossip and persistence abstractions
350#[cfg(feature = "std")]
351pub use gossip::{BroadcastAll, EmergencyAware, GossipStrategy, RandomFanout, SignalBasedFanout};
352#[cfg(feature = "std")]
353pub use persistence::{DocumentStore, FileStore, MemoryStore, SharedStore};
354
355// Multi-hop relay support
356pub use relay::{
357 MessageId, RelayEnvelope, RelayFlags, SeenMessageCache, DEFAULT_MAX_HOPS, DEFAULT_SEEN_TTL_MS,
358 RELAY_ENVELOPE_MARKER,
359};
360
361// Extensible document registry for app-layer types
362pub use registry::{
363 decode_header, decode_typed, encode_with_header, AppOperation, DocumentRegistry, DocumentType,
364 APP_OP_BASE, APP_TYPE_MAX, APP_TYPE_MIN,
365};
366
367/// Peat BLE Service UUID (128-bit)
368///
369/// All Peat nodes advertise this UUID for discovery.
370pub const PEAT_SERVICE_UUID: uuid::Uuid = uuid::uuid!("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
371
372/// Peat BLE Service UUID (16-bit short form)
373///
374/// Derived from the first two bytes of the 128-bit UUID (0xA1B2 from a1b2c3d4).
375/// Used for space-constrained advertising to fit within 31-byte limit.
376pub const PEAT_SERVICE_UUID_16BIT: u16 = 0xA1B2;
377
378/// PEAT Node Info Characteristic UUID
379pub const CHAR_NODE_INFO_UUID: u16 = 0x0001;
380
381/// PEAT Sync State Characteristic UUID
382pub const CHAR_SYNC_STATE_UUID: u16 = 0x0002;
383
384/// PEAT Sync Data Characteristic UUID
385pub const CHAR_SYNC_DATA_UUID: u16 = 0x0003;
386
387/// PEAT Command Characteristic UUID
388pub const CHAR_COMMAND_UUID: u16 = 0x0004;
389
390/// PEAT Status Characteristic UUID
391pub const CHAR_STATUS_UUID: u16 = 0x0005;
392
393/// Crate version
394pub const VERSION: &str = env!("CARGO_PKG_VERSION");
395
396/// Node identifier
397///
398/// Represents a unique node in the Peat mesh. For BLE, this is typically
399/// derived from the Bluetooth MAC address or a configured value.
400#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
401pub struct NodeId {
402 /// 32-bit node identifier
403 id: u32,
404}
405
406impl NodeId {
407 /// Create a new node ID from a 32-bit value
408 pub fn new(id: u32) -> Self {
409 Self { id }
410 }
411
412 /// Get the raw 32-bit ID value
413 pub fn as_u32(&self) -> u32 {
414 self.id
415 }
416
417 /// Create from a string representation (hex format)
418 pub fn parse(s: &str) -> Option<Self> {
419 // Try parsing as hex (with or without 0x prefix)
420 let s = s.trim_start_matches("0x").trim_start_matches("0X");
421 u32::from_str_radix(s, 16).ok().map(Self::new)
422 }
423
424 /// Derive a NodeId from a BLE MAC address.
425 ///
426 /// Uses the last 4 bytes of the 6-byte MAC address as the 32-bit node ID.
427 /// This provides a consistent node ID derived from the device's Bluetooth
428 /// hardware address.
429 ///
430 /// # Arguments
431 /// * `mac` - 6-byte MAC address array (e.g., [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF])
432 ///
433 /// # Example
434 /// ```
435 /// use peat_btle::NodeId;
436 ///
437 /// let mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55];
438 /// let node_id = NodeId::from_mac_address(&mac);
439 /// assert_eq!(node_id.as_u32(), 0x22334455);
440 /// ```
441 pub fn from_mac_address(mac: &[u8; 6]) -> Self {
442 // Use last 4 bytes: mac[2], mac[3], mac[4], mac[5]
443 let id = ((mac[2] as u32) << 24)
444 | ((mac[3] as u32) << 16)
445 | ((mac[4] as u32) << 8)
446 | (mac[5] as u32);
447 Self::new(id)
448 }
449
450 /// Derive a NodeId from a MAC address string.
451 ///
452 /// Parses a MAC address in "AA:BB:CC:DD:EE:FF" format and derives
453 /// the node ID from the last 4 bytes.
454 ///
455 /// # Arguments
456 /// * `mac_str` - MAC address string in colon-separated hex format
457 ///
458 /// # Returns
459 /// `Some(NodeId)` if parsing succeeds, `None` otherwise
460 ///
461 /// # Example
462 /// ```
463 /// use peat_btle::NodeId;
464 ///
465 /// let node_id = NodeId::from_mac_string("00:11:22:33:44:55").unwrap();
466 /// assert_eq!(node_id.as_u32(), 0x22334455);
467 /// ```
468 pub fn from_mac_string(mac_str: &str) -> Option<Self> {
469 let parts: Vec<&str> = mac_str.split(':').collect();
470 if parts.len() != 6 {
471 return None;
472 }
473
474 let mut mac = [0u8; 6];
475 for (i, part) in parts.iter().enumerate() {
476 mac[i] = u8::from_str_radix(part, 16).ok()?;
477 }
478
479 Some(Self::from_mac_address(&mac))
480 }
481}
482
483impl core::fmt::Display for NodeId {
484 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
485 write!(f, "{:08X}", self.id)
486 }
487}
488
489impl From<u32> for NodeId {
490 fn from(id: u32) -> Self {
491 Self::new(id)
492 }
493}
494
495impl From<NodeId> for u32 {
496 fn from(node_id: NodeId) -> Self {
497 node_id.id
498 }
499}
500
501/// Node capability flags
502///
503/// Advertised in the Peat beacon to indicate what this node can do.
504pub mod capabilities {
505 /// This is an Peat-Lite node (minimal state, single parent)
506 pub const LITE_NODE: u16 = 0x0001;
507 /// Has accelerometer sensor
508 pub const SENSOR_ACCEL: u16 = 0x0002;
509 /// Has temperature sensor
510 pub const SENSOR_TEMP: u16 = 0x0004;
511 /// Has button input
512 pub const SENSOR_BUTTON: u16 = 0x0008;
513 /// Has LED output
514 pub const ACTUATOR_LED: u16 = 0x0010;
515 /// Has vibration motor
516 pub const ACTUATOR_VIBRATE: u16 = 0x0020;
517 /// Has display
518 pub const HAS_DISPLAY: u16 = 0x0040;
519 /// Can relay messages (not a leaf)
520 pub const CAN_RELAY: u16 = 0x0080;
521 /// Supports Coded PHY
522 pub const CODED_PHY: u16 = 0x0100;
523 /// Has GPS
524 pub const HAS_GPS: u16 = 0x0200;
525}
526
527/// Hierarchy levels in the Peat mesh
528#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
529#[repr(u8)]
530pub enum HierarchyLevel {
531 /// Platform/soldier level (leaf nodes)
532 #[default]
533 Platform = 0,
534 /// Squad level
535 Squad = 1,
536 /// Platoon level
537 Platoon = 2,
538 /// Company level
539 Company = 3,
540}
541
542impl From<u8> for HierarchyLevel {
543 fn from(value: u8) -> Self {
544 match value {
545 0 => HierarchyLevel::Platform,
546 1 => HierarchyLevel::Squad,
547 2 => HierarchyLevel::Platoon,
548 3 => HierarchyLevel::Company,
549 _ => HierarchyLevel::Platform,
550 }
551 }
552}
553
554impl From<HierarchyLevel> for u8 {
555 fn from(level: HierarchyLevel) -> Self {
556 level as u8
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 #[test]
565 fn test_node_id() {
566 let id = NodeId::new(0x12345678);
567 assert_eq!(id.as_u32(), 0x12345678);
568 assert_eq!(id.to_string(), "12345678");
569 }
570
571 #[test]
572 fn test_node_id_parse() {
573 assert_eq!(NodeId::parse("12345678").unwrap().as_u32(), 0x12345678);
574 assert_eq!(NodeId::parse("0x12345678").unwrap().as_u32(), 0x12345678);
575 assert!(NodeId::parse("not_hex").is_none());
576 }
577
578 #[test]
579 fn test_node_id_from_mac_address() {
580 // MAC: AA:BB:CC:DD:EE:FF -> NodeId from last 4 bytes: 0xCCDDEEFF
581 let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
582 let node_id = NodeId::from_mac_address(&mac);
583 assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
584 }
585
586 #[test]
587 fn test_node_id_from_mac_string() {
588 let node_id = NodeId::from_mac_string("AA:BB:CC:DD:EE:FF").unwrap();
589 assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
590
591 // Lowercase should work too
592 let node_id = NodeId::from_mac_string("aa:bb:cc:dd:ee:ff").unwrap();
593 assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
594
595 // Invalid formats
596 assert!(NodeId::from_mac_string("invalid").is_none());
597 assert!(NodeId::from_mac_string("AA:BB:CC:DD:EE").is_none()); // Too short
598 assert!(NodeId::from_mac_string("AA:BB:CC:DD:EE:FF:GG").is_none()); // Too long
599 assert!(NodeId::from_mac_string("ZZ:BB:CC:DD:EE:FF").is_none()); // Invalid hex
600 }
601
602 #[test]
603 fn test_hierarchy_level() {
604 assert_eq!(HierarchyLevel::from(0), HierarchyLevel::Platform);
605 assert_eq!(HierarchyLevel::from(3), HierarchyLevel::Company);
606 assert_eq!(u8::from(HierarchyLevel::Squad), 1);
607 }
608
609 #[test]
610 fn test_service_uuid() {
611 assert_eq!(
612 PEAT_SERVICE_UUID.to_string(),
613 "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
614 );
615 }
616
617 #[test]
618 fn test_capabilities() {
619 let caps = capabilities::LITE_NODE | capabilities::SENSOR_ACCEL | capabilities::HAS_GPS;
620 assert_eq!(caps, 0x0203);
621 }
622}