hive_btle/lib.rs
1//! HIVE-BTLE: Bluetooth Low Energy mesh transport for HIVE Protocol
2//!
3//! This crate provides BLE-based peer-to-peer mesh networking for HIVE,
4//! supporting discovery, advertisement, connectivity, and HIVE-Lite sync.
5//!
6//! ## Overview
7//!
8//! HIVE-BTLE implements the pluggable transport abstraction (ADR-032) for
9//! Bluetooth Low Energy, enabling HIVE Protocol to operate over BLE in
10//! resource-constrained environments like smartwatches.
11//!
12//! ## Key Features
13//!
14//! - **Cross-platform**: Linux, Android, macOS, iOS, Windows, ESP32
15//! - **Power efficient**: Designed for 18+ hour battery life on watches
16//! - **Long range**: Coded PHY support for 300m+ range
17//! - **HIVE-Lite sync**: Optimized CRDT sync over GATT
18//!
19//! ## Architecture
20//!
21//! ```text
22//! ┌─────────────────────────────────────────────────┐
23//! │ Application │
24//! ├─────────────────────────────────────────────────┤
25//! │ BluetoothLETransport │
26//! │ (implements MeshTransport from ADR-032) │
27//! ├─────────────────────────────────────────────────┤
28//! │ BleAdapter Trait │
29//! ├──────────┬──────────┬──────────┬────────────────┤
30//! │ Linux │ Android │ Apple │ Windows │
31//! │ (BlueZ) │ (JNI) │(CoreBT) │ (WinRT) │
32//! └──────────┴──────────┴──────────┴────────────────┘
33//! ```
34//!
35//! ## Quick Start
36//!
37//! ```ignore
38//! use hive_btle::{BleConfig, BluetoothLETransport, NodeId};
39//!
40//! // Create HIVE-Lite optimized config for battery efficiency
41//! let config = BleConfig::hive_lite(NodeId::new(0x12345678));
42//!
43//! // Create transport with platform adapter
44//! #[cfg(feature = "linux")]
45//! let adapter = hive_btle::platform::linux::BluerAdapter::new()?;
46//!
47//! let transport = BluetoothLETransport::new(config, adapter);
48//!
49//! // Start advertising and scanning
50//! transport.start().await?;
51//!
52//! // Connect to a peer
53//! let conn = transport.connect(&peer_id).await?;
54//! ```
55//!
56//! ## Feature Flags
57//!
58//! - `std` (default): Standard library support
59//! - `linux`: Linux/BlueZ support via `bluer`
60//! - `android`: Android support via JNI
61//! - `macos`: macOS support via CoreBluetooth
62//! - `ios`: iOS support via CoreBluetooth
63//! - `windows`: Windows support via WinRT
64//! - `embedded`: Embedded/no_std support
65//! - `coded-phy`: Enable Coded PHY for extended range
66//! - `extended-adv`: Enable extended advertising
67//!
68//! ## External Crate Usage (hive-ffi)
69//!
70//! This crate exports platform adapters for use by external crates like `hive-ffi`.
71//! Each platform adapter is conditionally exported based on feature flags:
72//!
73//! ```toml
74//! # In your Cargo.toml
75//! [dependencies]
76//! hive-btle = { version = "0.0.5", features = ["linux"] }
77//! ```
78//!
79//! Then use the appropriate adapter:
80//!
81//! ```ignore
82//! use hive_btle::{BleConfig, BluerAdapter, HiveMesh, NodeId};
83//!
84//! // Platform adapter is automatically available via feature flag
85//! let adapter = BluerAdapter::new().await?;
86//! let config = BleConfig::hive_lite(NodeId::new(0x12345678));
87//! ```
88//!
89//! ### Platform → Adapter Mapping
90//!
91//! | Feature | Target | Adapter Type |
92//! |---------|--------|--------------|
93//! | `linux` | Linux | `BluerAdapter` |
94//! | `android` | Android | `AndroidAdapter` |
95//! | `macos` | macOS | `CoreBluetoothAdapter` |
96//! | `ios` | iOS | `CoreBluetoothAdapter` |
97//! | `windows` | Windows | `WinRtBleAdapter` |
98//!
99//! ### Document Encoding for Translation Layer
100//!
101//! For translating between Automerge (full HIVE) and hive-btle documents:
102//!
103//! ```ignore
104//! use hive_btle::HiveDocument;
105//!
106//! // Decode bytes received from BLE
107//! let doc = HiveDocument::from_bytes(&received_bytes)?;
108//!
109//! // Encode for BLE transmission
110//! let bytes = doc.to_bytes();
111//! ```
112//!
113//! ## Power Profiles
114//!
115//! | Profile | Duty Cycle | Watch Battery |
116//! |---------|------------|---------------|
117//! | Aggressive | 20% | ~6 hours |
118//! | Balanced | 10% | ~12 hours |
119//! | **LowPower** | **2%** | **~20+ hours** |
120//!
121//! ## Related ADRs
122//!
123//! - ADR-039: HIVE-BTLE Mesh Transport Crate
124//! - ADR-032: Pluggable Transport Abstraction
125//! - ADR-035: HIVE-Lite Embedded Nodes
126//! - ADR-037: Resource-Constrained Device Optimization
127
128#![cfg_attr(not(feature = "std"), no_std)]
129#![warn(missing_docs)]
130#![warn(rustdoc::missing_crate_level_docs)]
131
132#[cfg(not(feature = "std"))]
133extern crate alloc;
134
135pub mod config;
136pub mod discovery;
137pub mod document;
138pub mod document_sync;
139pub mod error;
140pub mod gatt;
141#[cfg(feature = "std")]
142pub mod gossip;
143pub mod hive_mesh;
144pub mod mesh;
145pub mod observer;
146pub mod peer;
147pub mod peer_manager;
148#[cfg(feature = "std")]
149pub mod persistence;
150pub mod phy;
151pub mod platform;
152pub mod power;
153pub mod security;
154pub mod sync;
155pub mod transport;
156
157// Re-exports for convenience
158pub use config::{
159 BleConfig, BlePhy, DiscoveryConfig, GattConfig, MeshConfig, PowerProfile, DEFAULT_MESH_ID,
160};
161#[cfg(feature = "std")]
162pub use discovery::Scanner;
163pub use discovery::{Advertiser, HiveBeacon, ScanFilter};
164pub use error::{BleError, Result};
165#[cfg(feature = "std")]
166pub use gatt::HiveGattService;
167pub use gatt::SyncProtocol;
168#[cfg(feature = "std")]
169pub use mesh::MeshManager;
170pub use mesh::{MeshRouter, MeshTopology, TopologyConfig, TopologyEvent};
171pub use phy::{PhyCapabilities, PhyController, PhyStrategy};
172pub use platform::{BleAdapter, ConnectionEvent, DisconnectReason, DiscoveredDevice, StubAdapter};
173
174// Platform-specific adapter re-exports for external crates (hive-ffi)
175// These allow external crates to use platform adapters via feature flags
176#[cfg(all(feature = "linux", target_os = "linux"))]
177pub use platform::linux::BluerAdapter;
178
179#[cfg(feature = "android")]
180pub use platform::android::AndroidAdapter;
181
182#[cfg(any(feature = "macos", feature = "ios"))]
183pub use platform::apple::CoreBluetoothAdapter;
184
185#[cfg(feature = "windows")]
186pub use platform::windows::WinRtBleAdapter;
187
188#[cfg(feature = "std")]
189pub use platform::mock::MockBleAdapter;
190pub use power::{BatteryState, RadioScheduler, SyncPriority};
191pub use sync::{GattSyncProtocol, SyncConfig, SyncState};
192pub use transport::{BleConnection, BluetoothLETransport, MeshTransport, TransportCapabilities};
193
194// New centralized mesh management types
195pub use document::{
196 HiveDocument, MergeResult, ENCRYPTED_MARKER, EXTENDED_MARKER, KEY_EXCHANGE_MARKER,
197 PEER_E2EE_MARKER,
198};
199
200// Security (mesh-wide and per-peer encryption)
201pub use document_sync::{DocumentCheck, DocumentSync};
202#[cfg(feature = "std")]
203pub use hive_mesh::{DataReceivedResult, HiveMesh, HiveMeshConfig};
204#[cfg(feature = "std")]
205pub use observer::{CollectingObserver, ObserverManager};
206pub use observer::{DisconnectReason as HiveDisconnectReason, HiveEvent, HiveObserver};
207pub use peer::{HivePeer, PeerManagerConfig, SignalStrength};
208pub use peer_manager::PeerManager;
209// Phase 1: Mesh-wide encryption
210pub use security::{EncryptedDocument, EncryptionError, MeshEncryptionKey};
211// Phase 2: Per-peer E2EE
212#[cfg(feature = "std")]
213pub use security::{
214 KeyExchangeMessage, PeerEncryptedMessage, PeerIdentityKey, PeerSession, PeerSessionKey,
215 PeerSessionManager, SessionState,
216};
217
218// Gossip and persistence abstractions
219#[cfg(feature = "std")]
220pub use gossip::{BroadcastAll, EmergencyAware, GossipStrategy, RandomFanout, SignalBasedFanout};
221#[cfg(feature = "std")]
222pub use persistence::{DocumentStore, FileStore, MemoryStore, SharedStore};
223
224/// HIVE BLE Service UUID (128-bit)
225///
226/// All HIVE nodes advertise this UUID for discovery.
227pub const HIVE_SERVICE_UUID: uuid::Uuid = uuid::uuid!("f47ac10b-58cc-4372-a567-0e02b2c3d479");
228
229/// HIVE BLE Service UUID (16-bit short form)
230///
231/// Derived from the first two bytes of the 128-bit UUID (0xF47A from f47ac10b).
232/// Used for space-constrained advertising to fit within 31-byte limit.
233pub const HIVE_SERVICE_UUID_16BIT: u16 = 0xF47A;
234
235/// HIVE Node Info Characteristic UUID
236pub const CHAR_NODE_INFO_UUID: u16 = 0x0001;
237
238/// HIVE Sync State Characteristic UUID
239pub const CHAR_SYNC_STATE_UUID: u16 = 0x0002;
240
241/// HIVE Sync Data Characteristic UUID
242pub const CHAR_SYNC_DATA_UUID: u16 = 0x0003;
243
244/// HIVE Command Characteristic UUID
245pub const CHAR_COMMAND_UUID: u16 = 0x0004;
246
247/// HIVE Status Characteristic UUID
248pub const CHAR_STATUS_UUID: u16 = 0x0005;
249
250/// Crate version
251pub const VERSION: &str = env!("CARGO_PKG_VERSION");
252
253/// Node identifier
254///
255/// Represents a unique node in the HIVE mesh. For BLE, this is typically
256/// derived from the Bluetooth MAC address or a configured value.
257#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
258pub struct NodeId {
259 /// 32-bit node identifier
260 id: u32,
261}
262
263impl NodeId {
264 /// Create a new node ID from a 32-bit value
265 pub fn new(id: u32) -> Self {
266 Self { id }
267 }
268
269 /// Get the raw 32-bit ID value
270 pub fn as_u32(&self) -> u32 {
271 self.id
272 }
273
274 /// Create from a string representation (hex format)
275 pub fn parse(s: &str) -> Option<Self> {
276 // Try parsing as hex (with or without 0x prefix)
277 let s = s.trim_start_matches("0x").trim_start_matches("0X");
278 u32::from_str_radix(s, 16).ok().map(Self::new)
279 }
280
281 /// Derive a NodeId from a BLE MAC address.
282 ///
283 /// Uses the last 4 bytes of the 6-byte MAC address as the 32-bit node ID.
284 /// This provides a consistent node ID derived from the device's Bluetooth
285 /// hardware address.
286 ///
287 /// # Arguments
288 /// * `mac` - 6-byte MAC address array (e.g., [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF])
289 ///
290 /// # Example
291 /// ```
292 /// use hive_btle::NodeId;
293 ///
294 /// let mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55];
295 /// let node_id = NodeId::from_mac_address(&mac);
296 /// assert_eq!(node_id.as_u32(), 0x22334455);
297 /// ```
298 pub fn from_mac_address(mac: &[u8; 6]) -> Self {
299 // Use last 4 bytes: mac[2], mac[3], mac[4], mac[5]
300 let id = ((mac[2] as u32) << 24)
301 | ((mac[3] as u32) << 16)
302 | ((mac[4] as u32) << 8)
303 | (mac[5] as u32);
304 Self::new(id)
305 }
306
307 /// Derive a NodeId from a MAC address string.
308 ///
309 /// Parses a MAC address in "AA:BB:CC:DD:EE:FF" format and derives
310 /// the node ID from the last 4 bytes.
311 ///
312 /// # Arguments
313 /// * `mac_str` - MAC address string in colon-separated hex format
314 ///
315 /// # Returns
316 /// `Some(NodeId)` if parsing succeeds, `None` otherwise
317 ///
318 /// # Example
319 /// ```
320 /// use hive_btle::NodeId;
321 ///
322 /// let node_id = NodeId::from_mac_string("00:11:22:33:44:55").unwrap();
323 /// assert_eq!(node_id.as_u32(), 0x22334455);
324 /// ```
325 pub fn from_mac_string(mac_str: &str) -> Option<Self> {
326 let parts: Vec<&str> = mac_str.split(':').collect();
327 if parts.len() != 6 {
328 return None;
329 }
330
331 let mut mac = [0u8; 6];
332 for (i, part) in parts.iter().enumerate() {
333 mac[i] = u8::from_str_radix(part, 16).ok()?;
334 }
335
336 Some(Self::from_mac_address(&mac))
337 }
338}
339
340impl core::fmt::Display for NodeId {
341 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
342 write!(f, "{:08X}", self.id)
343 }
344}
345
346impl From<u32> for NodeId {
347 fn from(id: u32) -> Self {
348 Self::new(id)
349 }
350}
351
352impl From<NodeId> for u32 {
353 fn from(node_id: NodeId) -> Self {
354 node_id.id
355 }
356}
357
358/// Node capability flags
359///
360/// Advertised in the HIVE beacon to indicate what this node can do.
361pub mod capabilities {
362 /// This is a HIVE-Lite node (minimal state, single parent)
363 pub const LITE_NODE: u16 = 0x0001;
364 /// Has accelerometer sensor
365 pub const SENSOR_ACCEL: u16 = 0x0002;
366 /// Has temperature sensor
367 pub const SENSOR_TEMP: u16 = 0x0004;
368 /// Has button input
369 pub const SENSOR_BUTTON: u16 = 0x0008;
370 /// Has LED output
371 pub const ACTUATOR_LED: u16 = 0x0010;
372 /// Has vibration motor
373 pub const ACTUATOR_VIBRATE: u16 = 0x0020;
374 /// Has display
375 pub const HAS_DISPLAY: u16 = 0x0040;
376 /// Can relay messages (not a leaf)
377 pub const CAN_RELAY: u16 = 0x0080;
378 /// Supports Coded PHY
379 pub const CODED_PHY: u16 = 0x0100;
380 /// Has GPS
381 pub const HAS_GPS: u16 = 0x0200;
382}
383
384/// Hierarchy levels in the HIVE mesh
385#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
386#[repr(u8)]
387pub enum HierarchyLevel {
388 /// Platform/soldier level (leaf nodes)
389 #[default]
390 Platform = 0,
391 /// Squad level
392 Squad = 1,
393 /// Platoon level
394 Platoon = 2,
395 /// Company level
396 Company = 3,
397}
398
399impl From<u8> for HierarchyLevel {
400 fn from(value: u8) -> Self {
401 match value {
402 0 => HierarchyLevel::Platform,
403 1 => HierarchyLevel::Squad,
404 2 => HierarchyLevel::Platoon,
405 3 => HierarchyLevel::Company,
406 _ => HierarchyLevel::Platform,
407 }
408 }
409}
410
411impl From<HierarchyLevel> for u8 {
412 fn from(level: HierarchyLevel) -> Self {
413 level as u8
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_node_id() {
423 let id = NodeId::new(0x12345678);
424 assert_eq!(id.as_u32(), 0x12345678);
425 assert_eq!(id.to_string(), "12345678");
426 }
427
428 #[test]
429 fn test_node_id_parse() {
430 assert_eq!(NodeId::parse("12345678").unwrap().as_u32(), 0x12345678);
431 assert_eq!(NodeId::parse("0x12345678").unwrap().as_u32(), 0x12345678);
432 assert!(NodeId::parse("not_hex").is_none());
433 }
434
435 #[test]
436 fn test_node_id_from_mac_address() {
437 // MAC: AA:BB:CC:DD:EE:FF -> NodeId from last 4 bytes: 0xCCDDEEFF
438 let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
439 let node_id = NodeId::from_mac_address(&mac);
440 assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
441 }
442
443 #[test]
444 fn test_node_id_from_mac_string() {
445 let node_id = NodeId::from_mac_string("AA:BB:CC:DD:EE:FF").unwrap();
446 assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
447
448 // Lowercase should work too
449 let node_id = NodeId::from_mac_string("aa:bb:cc:dd:ee:ff").unwrap();
450 assert_eq!(node_id.as_u32(), 0xCCDDEEFF);
451
452 // Invalid formats
453 assert!(NodeId::from_mac_string("invalid").is_none());
454 assert!(NodeId::from_mac_string("AA:BB:CC:DD:EE").is_none()); // Too short
455 assert!(NodeId::from_mac_string("AA:BB:CC:DD:EE:FF:GG").is_none()); // Too long
456 assert!(NodeId::from_mac_string("ZZ:BB:CC:DD:EE:FF").is_none()); // Invalid hex
457 }
458
459 #[test]
460 fn test_hierarchy_level() {
461 assert_eq!(HierarchyLevel::from(0), HierarchyLevel::Platform);
462 assert_eq!(HierarchyLevel::from(3), HierarchyLevel::Company);
463 assert_eq!(u8::from(HierarchyLevel::Squad), 1);
464 }
465
466 #[test]
467 fn test_service_uuid() {
468 assert_eq!(
469 HIVE_SERVICE_UUID.to_string(),
470 "f47ac10b-58cc-4372-a567-0e02b2c3d479"
471 );
472 }
473
474 #[test]
475 fn test_capabilities() {
476 let caps = capabilities::LITE_NODE | capabilities::SENSOR_ACCEL | capabilities::HAS_GPS;
477 assert_eq!(caps, 0x0203);
478 }
479}