Skip to main content

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;
163#[cfg(feature = "peat-lite-frame")]
164pub mod peat_lite_frame;
165pub mod peat_mesh;
166pub mod peer;
167pub mod peer_lifetime;
168pub mod peer_manager;
169#[cfg(feature = "std")]
170pub mod persistence;
171pub mod phy;
172pub mod platform;
173pub mod power;
174pub mod reconnect;
175pub mod registry;
176pub mod relay;
177
178pub mod security;
179pub mod sync;
180pub mod transport;
181
182// ADR-059 cross-transport bridging — `BleTranslator` typed-struct
183// surface, postcard scaffolding, JSON projection helpers, 0xB6
184// frame marker constant. Gated behind the `translator-codec` Cargo
185// feature so standalone BLE consumers (Bitchat-style, embedded
186// sensors) keep peat-mesh out of their dep tree.
187//
188// **Layering rule (ADR-059 Amendment 4 Slice 4.b).** The `peat_mesh::transport::Translator`
189// trait *impl* over these primitives lives in peat-mesh's tree, not
190// here, so peat-btle has zero peat-mesh dep. peat-mesh's `bluetooth`
191// feature consumes the `translator-codec` feature here and wraps the
192// primitives with its own Translator impl. See ADR-059 Amendment 4
193// for the full rationale.
194#[cfg(feature = "translator-codec")]
195pub mod translator;
196
197/// UniFFI-exported callback that receives JSON-serialized
198/// [`peat_mesh::sync::Document`]s decoded from inbound BLE translator
199/// frames (ADR-059 Amendment 2; back-edge-free shape preserved by Slice 4.b).
200///
201/// Same wiring point on `PeatMesh` (set via
202/// [`PeatMesh::set_decoded_document_json_callback`](crate::peat_mesh::PeatMesh::set_decoded_document_json_callback)),
203/// same firing path inside the receive dispatch — fires on every
204/// successfully-decoded 0xB6 frame.
205///
206/// **Why JSON-string instead of typed `Document`.** UniFFI 0.31 can't
207/// pass `peat_mesh::sync::Document` across the FFI boundary without
208/// bindings for the entire schema graph (`Document`, `Field`,
209/// `DocumentId`, all of `serde_json::Value`'s shape). Hosts that
210/// already round-trip docs as JSON strings via peat-ffi's
211/// `publishDocumentWithOrigin`-shaped FFI gain a directly-forwardable
212/// payload, and peat-btle has no peat-mesh dep at all (Slice 4.b
213/// closed that back-edge), so the JSON shape is the only one this
214/// crate produces.
215///
216/// `PeatEvent::TranslatorNoCallback` fires when no callback is
217/// installed (the release-skew window before any host has wired
218/// anything up).
219#[cfg(all(feature = "translator-codec", feature = "uniffi"))]
220#[uniffi::export(callback_interface)]
221pub trait DecodedDocumentJsonCallback: Send + Sync {
222    /// `collection` is the BleTranslator collection name. `doc_json`
223    /// is the serde-JSON serialization of the decoded document.
224    /// `peer` is the BLE peer identifier from the receive context.
225    fn on_document(&self, collection: String, doc_json: String, peer: Option<String>);
226}
227
228// UniFFI bindings (generates Kotlin + Swift)
229#[cfg(feature = "uniffi")]
230pub mod uniffi_bindings;
231
232// UniFFI scaffolding - must be at crate root
233#[cfg(feature = "uniffi")]
234uniffi::setup_scaffolding!();
235
236// Re-exports for convenience
237pub use config::{
238    BleConfig, BlePhy, DiscoveryConfig, GattConfig, MeshConfig, PowerProfile, DEFAULT_MESH_ID,
239};
240#[cfg(feature = "std")]
241pub use discovery::Scanner;
242pub use discovery::{Advertiser, PeatBeacon, ScanFilter};
243pub use error::{BleError, Result};
244#[cfg(feature = "std")]
245pub use gatt::PeatGattService;
246pub use gatt::SyncProtocol;
247#[cfg(feature = "std")]
248pub use mesh::MeshManager;
249pub use mesh::{MeshRouter, MeshTopology, TopologyConfig, TopologyEvent};
250pub use phy::{PhyCapabilities, PhyController, PhyStrategy};
251pub use platform::{BleAdapter, ConnectionEvent, DisconnectReason, DiscoveredDevice, StubAdapter};
252
253// Platform-specific adapter re-exports for external crates (peat-ffi)
254// These allow external crates to use platform adapters via feature flags
255#[cfg(all(feature = "linux", target_os = "linux"))]
256pub use platform::linux::BluerAdapter;
257
258#[cfg(feature = "android")]
259pub use platform::android::AndroidAdapter;
260
261#[cfg(any(feature = "macos", feature = "ios"))]
262pub use platform::apple::CoreBluetoothAdapter;
263
264#[cfg(feature = "windows")]
265pub use platform::windows::WinRtBleAdapter;
266
267#[cfg(feature = "std")]
268pub use platform::mock::MockBleAdapter;
269pub use power::{BatteryState, RadioScheduler, SyncPriority};
270pub use sync::{GattSyncProtocol, SyncConfig, SyncState};
271pub use transport::{BleConnection, BluetoothLETransport, MeshTransport, TransportCapabilities};
272
273// New centralized mesh management types
274pub use document::{
275    MergeResult, PeatDocument, ENCRYPTED_MARKER, EXTENDED_MARKER, KEY_EXCHANGE_MARKER,
276    PEER_E2EE_MARKER,
277};
278
279// Security (mesh-wide and per-peer encryption)
280pub use document_sync::{DocumentCheck, DocumentSync};
281#[cfg(feature = "std")]
282pub use observer::{CollectingObserver, ObserverManager};
283pub use observer::{DisconnectReason as PeatDisconnectReason, PeatEvent, PeatObserver};
284#[cfg(feature = "std")]
285pub use peat_mesh::{DataReceivedResult, PeatMesh, PeatMeshConfig, RelayDecision};
286pub use peer::{
287    BlePeerLinkInfo, ConnectionState, ConnectionStateGraph, FullStateCountSummary, IndirectPeer,
288    PeatPeer, PeerConnectionState, PeerDegree, PeerManagerConfig, SignalStrength,
289    StateCountSummary, MAX_TRACKED_DEGREE,
290};
291pub use peer_manager::PeerManager;
292
293// Device identity and attestation
294pub use security::{
295    DeviceIdentity, IdentityAttestation, IdentityError, IdentityRecord, IdentityRegistry,
296    RegistryResult,
297};
298// Mesh genesis and credentials
299pub use security::{MembershipPolicy, MeshCredentials, MeshGenesis};
300
301// Phase 1: Mesh-wide encryption
302pub use security::{EncryptedDocument, EncryptionError, MeshEncryptionKey};
303// Phase 2: Per-peer E2EE
304#[cfg(feature = "std")]
305pub use security::{
306    KeyExchangeMessage, PeerEncryptedMessage, PeerIdentityKey, PeerSession, PeerSessionKey,
307    PeerSessionManager, SessionState,
308};
309
310// Credential persistence
311#[cfg(feature = "std")]
312pub use security::{
313    MemoryStorage, PersistedState, PersistenceError, SecureStorage, PERSISTED_STATE_VERSION,
314};
315
316// Gossip and persistence abstractions
317#[cfg(feature = "std")]
318pub use gossip::{BroadcastAll, EmergencyAware, GossipStrategy, RandomFanout, SignalBasedFanout};
319#[cfg(feature = "std")]
320pub use persistence::{DocumentStore, FileStore, MemoryStore, SharedStore};
321
322// Multi-hop relay support
323pub use relay::{
324    MessageId, RelayEnvelope, RelayFlags, SeenMessageCache, DEFAULT_MAX_HOPS, DEFAULT_SEEN_TTL_MS,
325    RELAY_ENVELOPE_MARKER,
326};
327
328// Extensible document registry for app-layer types
329pub use registry::{
330    decode_header, decode_typed, encode_with_header, AppOperation, DocumentRegistry, DocumentType,
331    APP_OP_BASE, APP_TYPE_MAX, APP_TYPE_MIN,
332};
333
334/// Peat BLE Service UUID (128-bit)
335///
336/// All Peat nodes advertise this UUID for discovery.
337pub const PEAT_SERVICE_UUID: uuid::Uuid = uuid::uuid!("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
338
339/// Peat BLE Service UUID (16-bit short form)
340///
341/// Derived from the first two bytes of the 128-bit UUID (0xA1B2 from a1b2c3d4).
342/// Used for space-constrained advertising to fit within 31-byte limit.
343pub const PEAT_SERVICE_UUID_16BIT: u16 = 0xA1B2;
344
345/// PEAT Node Info Characteristic UUID
346pub const CHAR_NODE_INFO_UUID: u16 = 0x0001;
347
348/// PEAT Sync State Characteristic UUID
349pub const CHAR_SYNC_STATE_UUID: u16 = 0x0002;
350
351/// PEAT Sync Data Characteristic UUID
352pub const CHAR_SYNC_DATA_UUID: u16 = 0x0003;
353
354/// PEAT Command Characteristic UUID
355pub const CHAR_COMMAND_UUID: u16 = 0x0004;
356
357/// PEAT Status Characteristic UUID
358pub const CHAR_STATUS_UUID: u16 = 0x0005;
359
360/// Crate version
361pub const VERSION: &str = env!("CARGO_PKG_VERSION");
362
363/// Node identifier
364///
365/// Represents a unique node in the Peat mesh. For BLE, this is typically
366/// derived from the Bluetooth MAC address or a configured value.
367#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
368pub struct NodeId {
369    /// 32-bit node identifier
370    id: u32,
371}
372
373impl NodeId {
374    /// Create a new node ID from a 32-bit value
375    pub fn new(id: u32) -> Self {
376        Self { id }
377    }
378
379    /// Get the raw 32-bit ID value
380    pub fn as_u32(&self) -> u32 {
381        self.id
382    }
383
384    /// Create from a string representation (hex format)
385    pub fn parse(s: &str) -> Option<Self> {
386        // Try parsing as hex (with or without 0x prefix)
387        let s = s.trim_start_matches("0x").trim_start_matches("0X");
388        u32::from_str_radix(s, 16).ok().map(Self::new)
389    }
390
391    /// Derive a NodeId from a BLE MAC address.
392    ///
393    /// Uses the last 4 bytes of the 6-byte MAC address as the 32-bit node ID.
394    /// This provides a consistent node ID derived from the device's Bluetooth
395    /// hardware address.
396    ///
397    /// # Arguments
398    /// * `mac` - 6-byte MAC address array (e.g., [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF])
399    ///
400    /// # Example
401    /// ```
402    /// use peat_btle::NodeId;
403    ///
404    /// let mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55];
405    /// let node_id = NodeId::from_mac_address(&mac);
406    /// assert_eq!(node_id.as_u32(), 0x22334455);
407    /// ```
408    pub fn from_mac_address(mac: &[u8; 6]) -> Self {
409        // Use last 4 bytes: mac[2], mac[3], mac[4], mac[5]
410        let id = ((mac[2] as u32) << 24)
411            | ((mac[3] as u32) << 16)
412            | ((mac[4] as u32) << 8)
413            | (mac[5] as u32);
414        Self::new(id)
415    }
416
417    /// Derive a NodeId from a MAC address string.
418    ///
419    /// Parses a MAC address in "AA:BB:CC:DD:EE:FF" format and derives
420    /// the node ID from the last 4 bytes.
421    ///
422    /// # Arguments
423    /// * `mac_str` - MAC address string in colon-separated hex format
424    ///
425    /// # Returns
426    /// `Some(NodeId)` if parsing succeeds, `None` otherwise
427    ///
428    /// # Example
429    /// ```
430    /// use peat_btle::NodeId;
431    ///
432    /// let node_id = NodeId::from_mac_string("00:11:22:33:44:55").unwrap();
433    /// assert_eq!(node_id.as_u32(), 0x22334455);
434    /// ```
435    pub fn from_mac_string(mac_str: &str) -> Option<Self> {
436        let parts: Vec<&str> = mac_str.split(':').collect();
437        if parts.len() != 6 {
438            return None;
439        }
440
441        let mut mac = [0u8; 6];
442        for (i, part) in parts.iter().enumerate() {
443            mac[i] = u8::from_str_radix(part, 16).ok()?;
444        }
445
446        Some(Self::from_mac_address(&mac))
447    }
448}
449
450impl core::fmt::Display for NodeId {
451    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
452        write!(f, "{:08X}", self.id)
453    }
454}
455
456impl From<u32> for NodeId {
457    fn from(id: u32) -> Self {
458        Self::new(id)
459    }
460}
461
462impl From<NodeId> for u32 {
463    fn from(node_id: NodeId) -> Self {
464        node_id.id
465    }
466}
467
468/// Node capability flags
469///
470/// Advertised in the Peat beacon to indicate what this node can do.
471pub mod capabilities {
472    /// This is an Peat-Lite node (minimal state, single parent)
473    pub const LITE_NODE: u16 = 0x0001;
474    /// Has accelerometer sensor
475    pub const SENSOR_ACCEL: u16 = 0x0002;
476    /// Has temperature sensor
477    pub const SENSOR_TEMP: u16 = 0x0004;
478    /// Has button input
479    pub const SENSOR_BUTTON: u16 = 0x0008;
480    /// Has LED output
481    pub const ACTUATOR_LED: u16 = 0x0010;
482    /// Has vibration motor
483    pub const ACTUATOR_VIBRATE: u16 = 0x0020;
484    /// Has display
485    pub const HAS_DISPLAY: u16 = 0x0040;
486    /// Can relay messages (not a leaf)
487    pub const CAN_RELAY: u16 = 0x0080;
488    /// Supports Coded PHY
489    pub const CODED_PHY: u16 = 0x0100;
490    /// Has GPS
491    pub const HAS_GPS: u16 = 0x0200;
492}
493
494/// Hierarchy levels in the Peat mesh
495#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
496#[repr(u8)]
497pub enum HierarchyLevel {
498    /// Platform/soldier level (leaf nodes)
499    #[default]
500    Platform = 0,
501    /// Squad level
502    Squad = 1,
503    /// Platoon level
504    Platoon = 2,
505    /// Company level
506    Company = 3,
507}
508
509impl From<u8> for HierarchyLevel {
510    fn from(value: u8) -> Self {
511        match value {
512            0 => HierarchyLevel::Platform,
513            1 => HierarchyLevel::Squad,
514            2 => HierarchyLevel::Platoon,
515            3 => HierarchyLevel::Company,
516            _ => HierarchyLevel::Platform,
517        }
518    }
519}
520
521impl From<HierarchyLevel> for u8 {
522    fn from(level: HierarchyLevel) -> Self {
523        level as u8
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_node_id() {
533        let id = NodeId::new(0x12345678);
534        assert_eq!(id.as_u32(), 0x12345678);
535        assert_eq!(id.to_string(), "12345678");
536    }
537
538    #[test]
539    fn test_node_id_parse() {
540        assert_eq!(NodeId::parse("12345678").unwrap().as_u32(), 0x12345678);
541        assert_eq!(NodeId::parse("0x12345678").unwrap().as_u32(), 0x12345678);
542        assert!(NodeId::parse("not_hex").is_none());
543    }
544
545    #[test]
546    fn test_node_id_from_mac_address() {
547        // MAC: AA:BB:CC:DD:EE:FF -> NodeId from last 4 bytes: 0xCCDDEEFF
548        let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
549        let node_id = NodeId::from_mac_address(&mac);
550        assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
551    }
552
553    #[test]
554    fn test_node_id_from_mac_string() {
555        let node_id = NodeId::from_mac_string("AA:BB:CC:DD:EE:FF").unwrap();
556        assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
557
558        // Lowercase should work too
559        let node_id = NodeId::from_mac_string("aa:bb:cc:dd:ee:ff").unwrap();
560        assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
561
562        // Invalid formats
563        assert!(NodeId::from_mac_string("invalid").is_none());
564        assert!(NodeId::from_mac_string("AA:BB:CC:DD:EE").is_none()); // Too short
565        assert!(NodeId::from_mac_string("AA:BB:CC:DD:EE:FF:GG").is_none()); // Too long
566        assert!(NodeId::from_mac_string("ZZ:BB:CC:DD:EE:FF").is_none()); // Invalid hex
567    }
568
569    #[test]
570    fn test_hierarchy_level() {
571        assert_eq!(HierarchyLevel::from(0), HierarchyLevel::Platform);
572        assert_eq!(HierarchyLevel::from(3), HierarchyLevel::Company);
573        assert_eq!(u8::from(HierarchyLevel::Squad), 1);
574    }
575
576    #[test]
577    fn test_service_uuid() {
578        assert_eq!(
579            PEAT_SERVICE_UUID.to_string(),
580            "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
581        );
582    }
583
584    #[test]
585    fn test_capabilities() {
586        let caps = capabilities::LITE_NODE | capabilities::SENSOR_ACCEL | capabilities::HAS_GPS;
587        assert_eq!(caps, 0x0203);
588    }
589}