Skip to main content

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