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    pub fn encode(&self) -> Vec<u8> {
271        let counter_data = self.counter.encode();
272        let peripheral_data = self.peripheral.as_ref().map(|p| p.encode());
273        let emergency_data = self.emergency.as_ref().map(|e| e.encode());
274
275        // Calculate total size
276        let mut size = 8 + counter_data.len(); // header + counter
277        if let Some(ref pdata) = peripheral_data {
278            size += 4 + pdata.len(); // marker + reserved + len + peripheral
279        }
280        if let Some(ref edata) = emergency_data {
281            size += 4 + edata.len(); // marker + reserved + len + emergency
282        }
283
284        let mut buf = Vec::with_capacity(size);
285
286        // Header
287        buf.extend_from_slice(&self.version.to_le_bytes());
288        buf.extend_from_slice(&self.node_id.as_u32().to_le_bytes());
289
290        // GCounter
291        buf.extend_from_slice(&counter_data);
292
293        // Extended section (if peripheral present)
294        if let Some(pdata) = peripheral_data {
295            buf.push(EXTENDED_MARKER);
296            buf.push(0); // reserved
297            buf.extend_from_slice(&(pdata.len() as u16).to_le_bytes());
298            buf.extend_from_slice(&pdata);
299        }
300
301        // Emergency section (if emergency present)
302        if let Some(edata) = emergency_data {
303            buf.push(EMERGENCY_MARKER);
304            buf.push(0); // reserved
305            buf.extend_from_slice(&(edata.len() as u16).to_le_bytes());
306            buf.extend_from_slice(&edata);
307        }
308
309        buf
310    }
311
312    /// Decode from bytes received over BLE
313    pub fn decode(data: &[u8]) -> Option<Self> {
314        if data.len() < MIN_DOCUMENT_SIZE {
315            return None;
316        }
317
318        // Header
319        let version = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
320        let node_id = NodeId::new(u32::from_le_bytes([data[4], data[5], data[6], data[7]]));
321
322        // GCounter (starts at offset 8)
323        let counter = GCounter::decode(&data[8..])?;
324
325        // Calculate where counter ends
326        let num_entries = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
327        let mut offset = 8 + 4 + num_entries * 12;
328
329        let mut peripheral = None;
330        let mut emergency = None;
331
332        // Parse extended sections (can have peripheral and/or emergency)
333        while offset < data.len() {
334            let marker = data[offset];
335
336            if marker == EXTENDED_MARKER {
337                // Parse peripheral section
338                if data.len() < offset + 4 {
339                    break;
340                }
341                let _reserved = data[offset + 1];
342                let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
343
344                let section_start = offset + 4;
345                if data.len() < section_start + section_len {
346                    break;
347                }
348
349                peripheral = Peripheral::decode(&data[section_start..section_start + section_len]);
350                offset = section_start + section_len;
351            } else if marker == EMERGENCY_MARKER {
352                // Parse emergency section
353                if data.len() < offset + 4 {
354                    break;
355                }
356                let _reserved = data[offset + 1];
357                let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
358
359                let section_start = offset + 4;
360                if data.len() < section_start + section_len {
361                    break;
362                }
363
364                emergency =
365                    EmergencyEvent::decode(&data[section_start..section_start + section_len]);
366                offset = section_start + section_len;
367            } else {
368                // Unknown marker, stop parsing
369                break;
370            }
371        }
372
373        Some(Self {
374            version,
375            node_id,
376            counter,
377            peripheral,
378            emergency,
379        })
380    }
381
382    /// Get the total counter value
383    pub fn total_count(&self) -> u64 {
384        self.counter.value()
385    }
386
387    /// Get the encoded size of this document
388    ///
389    /// Use this to check if the document fits within BLE MTU constraints.
390    pub fn encoded_size(&self) -> usize {
391        let counter_size = 4 + self.counter.node_count_total() * 12;
392        let peripheral_size = self.peripheral.as_ref().map_or(0, |p| 4 + p.encode().len());
393        let emergency_size = self.emergency.as_ref().map_or(0, |e| 4 + e.encode().len());
394        8 + counter_size + peripheral_size + emergency_size
395    }
396
397    /// Check if the document exceeds the target size for single-packet transmission
398    ///
399    /// Returns `true` if the document is larger than [`TARGET_DOCUMENT_SIZE`].
400    pub fn exceeds_target_size(&self) -> bool {
401        self.encoded_size() > TARGET_DOCUMENT_SIZE
402    }
403
404    /// Check if the document exceeds the maximum size
405    ///
406    /// Returns `true` if the document is larger than [`MAX_DOCUMENT_SIZE`].
407    pub fn exceeds_max_size(&self) -> bool {
408        self.encoded_size() > MAX_DOCUMENT_SIZE
409    }
410}
411
412/// Result from merging a received document
413#[derive(Debug, Clone)]
414pub struct MergeResult {
415    /// Node ID that sent this document
416    pub source_node: NodeId,
417
418    /// Event contained in the document (if any)
419    pub event: Option<PeripheralEvent>,
420
421    /// Whether the counter changed (indicates new data)
422    pub counter_changed: bool,
423
424    /// Whether the emergency state changed (new emergency or ACK updates)
425    pub emergency_changed: bool,
426
427    /// Updated total count after merge
428    pub total_count: u64,
429}
430
431impl MergeResult {
432    /// Check if this result contains an emergency event
433    pub fn is_emergency(&self) -> bool {
434        self.event
435            .as_ref()
436            .is_some_and(|e| e.event_type == EventType::Emergency)
437    }
438
439    /// Check if this result contains an ACK event
440    pub fn is_ack(&self) -> bool {
441        self.event
442            .as_ref()
443            .is_some_and(|e| e.event_type == EventType::Ack)
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use crate::sync::crdt::PeripheralType;
451
452    #[test]
453    fn test_document_encode_decode_minimal() {
454        let node_id = NodeId::new(0x12345678);
455        let doc = HiveDocument::new(node_id);
456
457        let encoded = doc.encode();
458        assert_eq!(encoded.len(), 12); // 8 header + 4 counter (0 entries)
459
460        let decoded = HiveDocument::decode(&encoded).unwrap();
461        assert_eq!(decoded.version, 1);
462        assert_eq!(decoded.node_id.as_u32(), 0x12345678);
463        assert_eq!(decoded.counter.value(), 0);
464        assert!(decoded.peripheral.is_none());
465    }
466
467    #[test]
468    fn test_document_encode_decode_with_counter() {
469        let node_id = NodeId::new(0x12345678);
470        let mut doc = HiveDocument::new(node_id);
471        doc.increment_counter();
472        doc.increment_counter();
473
474        let encoded = doc.encode();
475        // 8 header + 4 num_entries + 1 entry (12 bytes) = 24
476        assert_eq!(encoded.len(), 24);
477
478        let decoded = HiveDocument::decode(&encoded).unwrap();
479        assert_eq!(decoded.counter.value(), 2);
480    }
481
482    #[test]
483    fn test_document_encode_decode_with_peripheral() {
484        let node_id = NodeId::new(0x12345678);
485        let peripheral =
486            Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor).with_callsign("ALPHA-1");
487
488        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
489
490        let encoded = doc.encode();
491        let decoded = HiveDocument::decode(&encoded).unwrap();
492
493        assert!(decoded.peripheral.is_some());
494        let p = decoded.peripheral.unwrap();
495        assert_eq!(p.id, 0xAABBCCDD);
496        assert_eq!(p.callsign_str(), "ALPHA-1");
497    }
498
499    #[test]
500    fn test_document_encode_decode_with_event() {
501        let node_id = NodeId::new(0x12345678);
502        let mut peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor);
503        peripheral.set_event(EventType::Emergency, 1234567890);
504
505        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
506
507        let encoded = doc.encode();
508        let decoded = HiveDocument::decode(&encoded).unwrap();
509
510        assert!(decoded.peripheral.is_some());
511        let p = decoded.peripheral.unwrap();
512        assert!(p.last_event.is_some());
513        let event = p.last_event.unwrap();
514        assert_eq!(event.event_type, EventType::Emergency);
515        assert_eq!(event.timestamp, 1234567890);
516    }
517
518    #[test]
519    fn test_document_merge() {
520        let node1 = NodeId::new(0x11111111);
521        let node2 = NodeId::new(0x22222222);
522
523        let mut doc1 = HiveDocument::new(node1);
524        doc1.increment_counter();
525
526        let mut doc2 = HiveDocument::new(node2);
527        doc2.counter.increment(&node2, 3);
528
529        // Merge doc2 into doc1
530        let changed = doc1.merge(&doc2);
531        assert!(changed);
532        assert_eq!(doc1.counter.value(), 4); // 1 + 3
533    }
534
535    #[test]
536    fn test_merge_result_helpers() {
537        let emergency_event = PeripheralEvent::new(EventType::Emergency, 123);
538        let result = MergeResult {
539            source_node: NodeId::new(0x12345678),
540            event: Some(emergency_event),
541            counter_changed: true,
542            emergency_changed: false,
543            total_count: 10,
544        };
545
546        assert!(result.is_emergency());
547        assert!(!result.is_ack());
548
549        let ack_event = PeripheralEvent::new(EventType::Ack, 456);
550        let result = MergeResult {
551            source_node: NodeId::new(0x12345678),
552            event: Some(ack_event),
553            counter_changed: false,
554            emergency_changed: false,
555            total_count: 10,
556        };
557
558        assert!(!result.is_emergency());
559        assert!(result.is_ack());
560    }
561
562    #[test]
563    fn test_document_size_calculation() {
564        use crate::sync::crdt::PeripheralType;
565
566        let node_id = NodeId::new(0x12345678);
567
568        // Minimal document: 8 header + 4 counter (0 entries) = 12 bytes
569        let doc = HiveDocument::new(node_id);
570        assert_eq!(doc.encoded_size(), 12);
571        assert!(!doc.exceeds_target_size());
572
573        // With one counter entry: 8 + (4 + 12) = 24 bytes
574        let mut doc = HiveDocument::new(node_id);
575        doc.increment_counter();
576        assert_eq!(doc.encoded_size(), 24);
577
578        // With peripheral: adds ~42 bytes (4 marker/len + 38 data)
579        let peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor);
580        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
581        let encoded = doc.encode();
582        assert_eq!(doc.encoded_size(), encoded.len());
583
584        // Verify size stays under target for reasonable mesh
585        let mut doc = HiveDocument::new(node_id);
586        for i in 0..10 {
587            doc.counter.increment(&NodeId::new(i), 1);
588        }
589        assert!(doc.encoded_size() < TARGET_DOCUMENT_SIZE);
590        assert!(!doc.exceeds_max_size());
591    }
592}