hive_btle/
document.rs

1//! HIVE Document wire format for BLE mesh sync
2//!
3//! This module provides the unified document format used across all platforms
4//! (iOS, Android, ESP32) for mesh synchronization. The format is designed for
5//! efficient BLE transmission while supporting CRDT semantics.
6//!
7//! ## Wire Format
8//!
9//! ```text
10//! Header (8 bytes):
11//!   version:  4 bytes (LE) - document version number
12//!   node_id:  4 bytes (LE) - source node identifier
13//!
14//! GCounter (4 + N*12 bytes):
15//!   num_entries: 4 bytes (LE)
16//!   entries[N]:
17//!     node_id: 4 bytes (LE)
18//!     count:   8 bytes (LE)
19//!
20//! Extended Section (optional, when peripheral data present):
21//!   marker:         1 byte (0xAB)
22//!   reserved:       1 byte
23//!   peripheral_len: 2 bytes (LE)
24//!   peripheral:     variable (34-43 bytes)
25//! ```
26
27#[cfg(not(feature = "std"))]
28use alloc::vec::Vec;
29
30use crate::sync::crdt::{EmergencyEvent, EventType, GCounter, Peripheral, PeripheralEvent};
31use crate::NodeId;
32
33/// Marker byte indicating extended section with peripheral data
34pub const EXTENDED_MARKER: u8 = 0xAB;
35
36/// Marker byte indicating emergency event section
37pub const EMERGENCY_MARKER: u8 = 0xAC;
38
39/// Marker byte indicating encrypted document (mesh-wide)
40///
41/// When present, the entire document payload following the marker is encrypted
42/// using ChaCha20-Poly1305. The marker format is:
43///
44/// ```text
45/// marker:   1 byte (0xAE)
46/// reserved: 1 byte (0x00)
47/// payload:  12 bytes nonce + variable ciphertext (includes 16-byte auth tag)
48/// ```
49///
50/// Encryption happens at the HiveMesh layer before transmission, and decryption
51/// happens upon receipt before document parsing.
52pub const ENCRYPTED_MARKER: u8 = 0xAE;
53
54/// Marker byte indicating per-peer E2EE message
55///
56/// Used for end-to-end encrypted messages between specific peer pairs.
57/// Only the sender and recipient (who share a session key) can decrypt.
58///
59/// ```text
60/// marker:     1 byte (0xAF)
61/// flags:      1 byte (bit 0: key_exchange, bit 1: forward_secrecy)
62/// recipient:  4 bytes (LE) - recipient node ID
63/// sender:     4 bytes (LE) - sender node ID
64/// counter:    8 bytes (LE) - message counter for replay protection
65/// nonce:      12 bytes
66/// ciphertext: variable (includes 16-byte auth tag)
67/// ```
68pub const PEER_E2EE_MARKER: u8 = 0xAF;
69
70/// Marker byte indicating key exchange message for per-peer E2EE
71///
72/// Used to establish E2EE sessions between peers via X25519 key exchange.
73///
74/// ```text
75/// marker:     1 byte (0xB0)
76/// sender:     4 bytes (LE) - sender node ID
77/// flags:      1 byte (bit 0: is_ephemeral)
78/// public_key: 32 bytes - X25519 public key
79/// ```
80pub const KEY_EXCHANGE_MARKER: u8 = 0xB0;
81
82/// Minimum document size (header only, no counter entries)
83pub const MIN_DOCUMENT_SIZE: usize = 8;
84
85/// Maximum recommended mesh size for reliable single-packet sync
86///
87/// Beyond this, documents may exceed typical BLE MTU (244 bytes).
88/// Size calculation: 8 (header) + 4 + (N × 12) (GCounter) + 42 (Peripheral)
89///   20 nodes = 8 + 244 + 42 = 294 bytes
90pub const MAX_MESH_SIZE: usize = 20;
91
92/// Target document size for single-packet transmission
93///
94/// Based on typical negotiated BLE MTU (247 bytes - 3 ATT overhead).
95pub const TARGET_DOCUMENT_SIZE: usize = 244;
96
97/// Hard limit before fragmentation would be required
98///
99/// BLE 5.0+ supports up to 517 byte MTU, but 512 is practical max payload.
100pub const MAX_DOCUMENT_SIZE: usize = 512;
101
102/// A HIVE document for mesh synchronization
103///
104/// Contains header information, a CRDT G-Counter for tracking mesh activity,
105/// optional peripheral data for events, and optional emergency event with ACK tracking.
106#[derive(Debug, Clone)]
107pub struct HiveDocument {
108    /// Document version (incremented on each change)
109    pub version: u32,
110
111    /// Source node ID that created/last modified this document
112    pub node_id: NodeId,
113
114    /// CRDT G-Counter tracking activity across the mesh
115    pub counter: GCounter,
116
117    /// Optional peripheral data (sensor info, events)
118    pub peripheral: Option<Peripheral>,
119
120    /// Optional active emergency event with distributed ACK tracking
121    pub emergency: Option<EmergencyEvent>,
122}
123
124impl Default for HiveDocument {
125    fn default() -> Self {
126        Self {
127            version: 1,
128            node_id: NodeId::default(),
129            counter: GCounter::new(),
130            peripheral: None,
131            emergency: None,
132        }
133    }
134}
135
136impl HiveDocument {
137    /// Create a new document for the given node
138    pub fn new(node_id: NodeId) -> Self {
139        Self {
140            version: 1,
141            node_id,
142            counter: GCounter::new(),
143            peripheral: None,
144            emergency: None,
145        }
146    }
147
148    /// Create with an initial peripheral
149    pub fn with_peripheral(mut self, peripheral: Peripheral) -> Self {
150        self.peripheral = Some(peripheral);
151        self
152    }
153
154    /// Create with an initial emergency event
155    pub fn with_emergency(mut self, emergency: EmergencyEvent) -> Self {
156        self.emergency = Some(emergency);
157        self
158    }
159
160    /// Increment the document version
161    pub fn increment_version(&mut self) {
162        self.version = self.version.wrapping_add(1);
163    }
164
165    /// Increment the counter for this node
166    pub fn increment_counter(&mut self) {
167        self.counter.increment(&self.node_id, 1);
168        self.increment_version();
169    }
170
171    /// Set an event on the peripheral
172    pub fn set_event(&mut self, event_type: EventType, timestamp: u64) {
173        if let Some(ref mut peripheral) = self.peripheral {
174            peripheral.set_event(event_type, timestamp);
175            self.increment_counter();
176        }
177    }
178
179    /// Clear the event from the peripheral
180    pub fn clear_event(&mut self) {
181        if let Some(ref mut peripheral) = self.peripheral {
182            peripheral.clear_event();
183            self.increment_version();
184        }
185    }
186
187    /// Set an emergency event
188    ///
189    /// Creates a new emergency event with the given source node, timestamp,
190    /// and list of known peers to track for ACKs.
191    pub fn set_emergency(&mut self, source_node: u32, timestamp: u64, known_peers: &[u32]) {
192        self.emergency = Some(EmergencyEvent::new(source_node, timestamp, known_peers));
193        self.increment_counter();
194    }
195
196    /// Record an ACK for the current emergency
197    ///
198    /// Returns true if the ACK was new (state changed)
199    pub fn ack_emergency(&mut self, node_id: u32) -> bool {
200        if let Some(ref mut emergency) = self.emergency {
201            if emergency.ack(node_id) {
202                self.increment_version();
203                return true;
204            }
205        }
206        false
207    }
208
209    /// Clear the emergency event
210    pub fn clear_emergency(&mut self) {
211        if self.emergency.is_some() {
212            self.emergency = None;
213            self.increment_version();
214        }
215    }
216
217    /// Get the current emergency event (if any)
218    pub fn get_emergency(&self) -> Option<&EmergencyEvent> {
219        self.emergency.as_ref()
220    }
221
222    /// Check if there's an active emergency
223    pub fn has_emergency(&self) -> bool {
224        self.emergency.is_some()
225    }
226
227    /// Merge with another document using CRDT semantics
228    ///
229    /// Returns true if our state changed (useful for triggering re-broadcast)
230    pub fn merge(&mut self, other: &HiveDocument) -> bool {
231        let mut changed = false;
232
233        // Merge counter
234        let old_value = self.counter.value();
235        self.counter.merge(&other.counter);
236        if self.counter.value() != old_value {
237            changed = true;
238        }
239
240        // Merge emergency event
241        if let Some(ref other_emergency) = other.emergency {
242            match &mut self.emergency {
243                Some(ref mut our_emergency) => {
244                    if our_emergency.merge(other_emergency) {
245                        changed = true;
246                    }
247                }
248                None => {
249                    self.emergency = Some(other_emergency.clone());
250                    changed = true;
251                }
252            }
253        }
254
255        if changed {
256            self.increment_version();
257        }
258        changed
259    }
260
261    /// Get the current event type (if any)
262    pub fn current_event(&self) -> Option<EventType> {
263        self.peripheral
264            .as_ref()
265            .and_then(|p| p.last_event.as_ref())
266            .map(|e| e.event_type)
267    }
268
269    /// Encode to bytes for BLE transmission
270    ///
271    /// Alias: [`Self::to_bytes()`]
272    pub fn encode(&self) -> Vec<u8> {
273        let counter_data = self.counter.encode();
274        let peripheral_data = self.peripheral.as_ref().map(|p| p.encode());
275        let emergency_data = self.emergency.as_ref().map(|e| e.encode());
276
277        // Calculate total size
278        let mut size = 8 + counter_data.len(); // header + counter
279        if let Some(ref pdata) = peripheral_data {
280            size += 4 + pdata.len(); // marker + reserved + len + peripheral
281        }
282        if let Some(ref edata) = emergency_data {
283            size += 4 + edata.len(); // marker + reserved + len + emergency
284        }
285
286        let mut buf = Vec::with_capacity(size);
287
288        // Header
289        buf.extend_from_slice(&self.version.to_le_bytes());
290        buf.extend_from_slice(&self.node_id.as_u32().to_le_bytes());
291
292        // GCounter
293        buf.extend_from_slice(&counter_data);
294
295        // Extended section (if peripheral present)
296        if let Some(pdata) = peripheral_data {
297            buf.push(EXTENDED_MARKER);
298            buf.push(0); // reserved
299            buf.extend_from_slice(&(pdata.len() as u16).to_le_bytes());
300            buf.extend_from_slice(&pdata);
301        }
302
303        // Emergency section (if emergency present)
304        if let Some(edata) = emergency_data {
305            buf.push(EMERGENCY_MARKER);
306            buf.push(0); // reserved
307            buf.extend_from_slice(&(edata.len() as u16).to_le_bytes());
308            buf.extend_from_slice(&edata);
309        }
310
311        buf
312    }
313
314    /// Encode to bytes for transmission (alias for [`Self::encode()`])
315    ///
316    /// This is the conventional name used by external crates like hive-ffi
317    /// for transport-agnostic document serialization.
318    #[inline]
319    pub fn to_bytes(&self) -> Vec<u8> {
320        self.encode()
321    }
322
323    /// Decode from bytes received over BLE
324    ///
325    /// Alias: [`Self::from_bytes()`]
326    pub fn decode(data: &[u8]) -> Option<Self> {
327        if data.len() < MIN_DOCUMENT_SIZE {
328            return None;
329        }
330
331        // Header
332        let version = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
333        let node_id = NodeId::new(u32::from_le_bytes([data[4], data[5], data[6], data[7]]));
334
335        // GCounter (starts at offset 8)
336        let counter = GCounter::decode(&data[8..])?;
337
338        // Calculate where counter ends
339        let num_entries = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
340        let mut offset = 8 + 4 + num_entries * 12;
341
342        let mut peripheral = None;
343        let mut emergency = None;
344
345        // Parse extended sections (can have peripheral and/or emergency)
346        while offset < data.len() {
347            let marker = data[offset];
348
349            if marker == EXTENDED_MARKER {
350                // Parse peripheral section
351                if data.len() < offset + 4 {
352                    break;
353                }
354                let _reserved = data[offset + 1];
355                let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
356
357                let section_start = offset + 4;
358                if data.len() < section_start + section_len {
359                    break;
360                }
361
362                peripheral = Peripheral::decode(&data[section_start..section_start + section_len]);
363                offset = section_start + section_len;
364            } else if marker == EMERGENCY_MARKER {
365                // Parse emergency section
366                if data.len() < offset + 4 {
367                    break;
368                }
369                let _reserved = data[offset + 1];
370                let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
371
372                let section_start = offset + 4;
373                if data.len() < section_start + section_len {
374                    break;
375                }
376
377                emergency =
378                    EmergencyEvent::decode(&data[section_start..section_start + section_len]);
379                offset = section_start + section_len;
380            } else {
381                // Unknown marker, stop parsing
382                break;
383            }
384        }
385
386        Some(Self {
387            version,
388            node_id,
389            counter,
390            peripheral,
391            emergency,
392        })
393    }
394
395    /// Decode from bytes (alias for [`Self::decode()`])
396    ///
397    /// This is the conventional name used by external crates like hive-ffi
398    /// for transport-agnostic document deserialization.
399    #[inline]
400    pub fn from_bytes(data: &[u8]) -> Option<Self> {
401        Self::decode(data)
402    }
403
404    /// Get the total counter value
405    pub fn total_count(&self) -> u64 {
406        self.counter.value()
407    }
408
409    /// Get the encoded size of this document
410    ///
411    /// Use this to check if the document fits within BLE MTU constraints.
412    pub fn encoded_size(&self) -> usize {
413        let counter_size = 4 + self.counter.node_count_total() * 12;
414        let peripheral_size = self.peripheral.as_ref().map_or(0, |p| 4 + p.encode().len());
415        let emergency_size = self.emergency.as_ref().map_or(0, |e| 4 + e.encode().len());
416        8 + counter_size + peripheral_size + emergency_size
417    }
418
419    /// Check if the document exceeds the target size for single-packet transmission
420    ///
421    /// Returns `true` if the document is larger than [`TARGET_DOCUMENT_SIZE`].
422    pub fn exceeds_target_size(&self) -> bool {
423        self.encoded_size() > TARGET_DOCUMENT_SIZE
424    }
425
426    /// Check if the document exceeds the maximum size
427    ///
428    /// Returns `true` if the document is larger than [`MAX_DOCUMENT_SIZE`].
429    pub fn exceeds_max_size(&self) -> bool {
430        self.encoded_size() > MAX_DOCUMENT_SIZE
431    }
432}
433
434/// Result from merging a received document
435#[derive(Debug, Clone)]
436pub struct MergeResult {
437    /// Node ID that sent this document
438    pub source_node: NodeId,
439
440    /// Event contained in the document (if any)
441    pub event: Option<PeripheralEvent>,
442
443    /// Whether the counter changed (indicates new data)
444    pub counter_changed: bool,
445
446    /// Whether the emergency state changed (new emergency or ACK updates)
447    pub emergency_changed: bool,
448
449    /// Updated total count after merge
450    pub total_count: u64,
451}
452
453impl MergeResult {
454    /// Check if this result contains an emergency event
455    pub fn is_emergency(&self) -> bool {
456        self.event
457            .as_ref()
458            .is_some_and(|e| e.event_type == EventType::Emergency)
459    }
460
461    /// Check if this result contains an ACK event
462    pub fn is_ack(&self) -> bool {
463        self.event
464            .as_ref()
465            .is_some_and(|e| e.event_type == EventType::Ack)
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use crate::sync::crdt::PeripheralType;
473
474    #[test]
475    fn test_document_encode_decode_minimal() {
476        let node_id = NodeId::new(0x12345678);
477        let doc = HiveDocument::new(node_id);
478
479        let encoded = doc.encode();
480        assert_eq!(encoded.len(), 12); // 8 header + 4 counter (0 entries)
481
482        let decoded = HiveDocument::decode(&encoded).unwrap();
483        assert_eq!(decoded.version, 1);
484        assert_eq!(decoded.node_id.as_u32(), 0x12345678);
485        assert_eq!(decoded.counter.value(), 0);
486        assert!(decoded.peripheral.is_none());
487    }
488
489    #[test]
490    fn test_document_encode_decode_with_counter() {
491        let node_id = NodeId::new(0x12345678);
492        let mut doc = HiveDocument::new(node_id);
493        doc.increment_counter();
494        doc.increment_counter();
495
496        let encoded = doc.encode();
497        // 8 header + 4 num_entries + 1 entry (12 bytes) = 24
498        assert_eq!(encoded.len(), 24);
499
500        let decoded = HiveDocument::decode(&encoded).unwrap();
501        assert_eq!(decoded.counter.value(), 2);
502    }
503
504    #[test]
505    fn test_document_encode_decode_with_peripheral() {
506        let node_id = NodeId::new(0x12345678);
507        let peripheral =
508            Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor).with_callsign("ALPHA-1");
509
510        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
511
512        let encoded = doc.encode();
513        let decoded = HiveDocument::decode(&encoded).unwrap();
514
515        assert!(decoded.peripheral.is_some());
516        let p = decoded.peripheral.unwrap();
517        assert_eq!(p.id, 0xAABBCCDD);
518        assert_eq!(p.callsign_str(), "ALPHA-1");
519    }
520
521    #[test]
522    fn test_document_encode_decode_with_event() {
523        let node_id = NodeId::new(0x12345678);
524        let mut peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor);
525        peripheral.set_event(EventType::Emergency, 1234567890);
526
527        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
528
529        let encoded = doc.encode();
530        let decoded = HiveDocument::decode(&encoded).unwrap();
531
532        assert!(decoded.peripheral.is_some());
533        let p = decoded.peripheral.unwrap();
534        assert!(p.last_event.is_some());
535        let event = p.last_event.unwrap();
536        assert_eq!(event.event_type, EventType::Emergency);
537        assert_eq!(event.timestamp, 1234567890);
538    }
539
540    #[test]
541    fn test_document_merge() {
542        let node1 = NodeId::new(0x11111111);
543        let node2 = NodeId::new(0x22222222);
544
545        let mut doc1 = HiveDocument::new(node1);
546        doc1.increment_counter();
547
548        let mut doc2 = HiveDocument::new(node2);
549        doc2.counter.increment(&node2, 3);
550
551        // Merge doc2 into doc1
552        let changed = doc1.merge(&doc2);
553        assert!(changed);
554        assert_eq!(doc1.counter.value(), 4); // 1 + 3
555    }
556
557    #[test]
558    fn test_merge_result_helpers() {
559        let emergency_event = PeripheralEvent::new(EventType::Emergency, 123);
560        let result = MergeResult {
561            source_node: NodeId::new(0x12345678),
562            event: Some(emergency_event),
563            counter_changed: true,
564            emergency_changed: false,
565            total_count: 10,
566        };
567
568        assert!(result.is_emergency());
569        assert!(!result.is_ack());
570
571        let ack_event = PeripheralEvent::new(EventType::Ack, 456);
572        let result = MergeResult {
573            source_node: NodeId::new(0x12345678),
574            event: Some(ack_event),
575            counter_changed: false,
576            emergency_changed: false,
577            total_count: 10,
578        };
579
580        assert!(!result.is_emergency());
581        assert!(result.is_ack());
582    }
583
584    #[test]
585    fn test_document_size_calculation() {
586        use crate::sync::crdt::PeripheralType;
587
588        let node_id = NodeId::new(0x12345678);
589
590        // Minimal document: 8 header + 4 counter (0 entries) = 12 bytes
591        let doc = HiveDocument::new(node_id);
592        assert_eq!(doc.encoded_size(), 12);
593        assert!(!doc.exceeds_target_size());
594
595        // With one counter entry: 8 + (4 + 12) = 24 bytes
596        let mut doc = HiveDocument::new(node_id);
597        doc.increment_counter();
598        assert_eq!(doc.encoded_size(), 24);
599
600        // With peripheral: adds ~42 bytes (4 marker/len + 38 data)
601        let peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor);
602        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
603        let encoded = doc.encode();
604        assert_eq!(doc.encoded_size(), encoded.len());
605
606        // Verify size stays under target for reasonable mesh
607        let mut doc = HiveDocument::new(node_id);
608        for i in 0..10 {
609            doc.counter.increment(&NodeId::new(i), 1);
610        }
611        assert!(doc.encoded_size() < TARGET_DOCUMENT_SIZE);
612        assert!(!doc.exceeds_max_size());
613    }
614}