Skip to main content

hive_btle/sync/
crdt.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//! CRDT (Conflict-free Replicated Data Types) for HIVE-Lite
17//!
18//! Provides lightweight CRDT implementations optimized for BLE sync:
19//! - LWW-Register: Last-Writer-Wins for single values
20//! - G-Counter: Grow-only counter for metrics
21//!
22//! These are designed for minimal memory footprint and efficient
23//! serialization over constrained BLE connections.
24
25#[cfg(not(feature = "std"))]
26use alloc::{collections::BTreeMap, string::String, string::ToString, vec, vec::Vec};
27#[cfg(feature = "std")]
28use std::collections::BTreeMap;
29
30use crate::NodeId;
31
32// ============================================================================
33// Security Validation Constants
34// ============================================================================
35// These constants define bounds for validating decoded data to prevent
36// spoofing, injection attacks, and garbage data from corrupting state.
37
38/// Minimum valid timestamp: 2020-01-01 00:00:00 UTC (milliseconds)
39/// Any timestamp before this is rejected as invalid/spoofed.
40pub const MIN_VALID_TIMESTAMP: u64 = 1577836800000;
41
42/// Maximum valid battery percentage (100%)
43pub const MAX_BATTERY_PERCENT: u8 = 100;
44
45/// Minimum valid heart rate (BPM) - anything below is likely garbage
46pub const MIN_HEART_RATE: u8 = 20;
47
48/// Maximum valid heart rate (BPM) - anything above is likely garbage
49pub const MAX_HEART_RATE: u8 = 250;
50
51/// Maximum number of ACK entries in an emergency event (prevents DoS)
52pub const MAX_EMERGENCY_ACKS: usize = 256;
53
54/// Maximum number of counter entries (prevents DoS)
55pub const MAX_COUNTER_ENTRIES: usize = 256;
56
57/// Timestamp for CRDT operations (milliseconds since epoch or monotonic)
58pub type Timestamp = u64;
59
60/// A Last-Writer-Wins Register
61///
62/// Stores a single value where concurrent writes are resolved by timestamp.
63/// Higher timestamp wins. In case of tie, higher node ID wins.
64#[derive(Debug, Clone, PartialEq)]
65pub struct LwwRegister<T: Clone> {
66    /// Current value
67    value: T,
68    /// Timestamp when value was set
69    timestamp: Timestamp,
70    /// Node that set the value
71    node_id: NodeId,
72}
73
74impl<T: Clone + Default> Default for LwwRegister<T> {
75    fn default() -> Self {
76        Self {
77            value: T::default(),
78            timestamp: 0,
79            node_id: NodeId::default(),
80        }
81    }
82}
83
84impl<T: Clone> LwwRegister<T> {
85    /// Create a new register with an initial value
86    pub fn new(value: T, timestamp: Timestamp, node_id: NodeId) -> Self {
87        Self {
88            value,
89            timestamp,
90            node_id,
91        }
92    }
93
94    /// Get the current value
95    pub fn get(&self) -> &T {
96        &self.value
97    }
98
99    /// Get the timestamp
100    pub fn timestamp(&self) -> Timestamp {
101        self.timestamp
102    }
103
104    /// Get the node that set the value
105    pub fn node_id(&self) -> &NodeId {
106        &self.node_id
107    }
108
109    /// Set a new value if it has a higher timestamp
110    ///
111    /// Returns true if the value was updated
112    pub fn set(&mut self, value: T, timestamp: Timestamp, node_id: NodeId) -> bool {
113        if self.should_update(timestamp, &node_id) {
114            self.value = value;
115            self.timestamp = timestamp;
116            self.node_id = node_id;
117            true
118        } else {
119            false
120        }
121    }
122
123    /// Merge with another register (LWW semantics)
124    ///
125    /// Returns true if our value was updated
126    pub fn merge(&mut self, other: &LwwRegister<T>) -> bool {
127        if self.should_update(other.timestamp, &other.node_id) {
128            self.value = other.value.clone();
129            self.timestamp = other.timestamp;
130            self.node_id = other.node_id;
131            true
132        } else {
133            false
134        }
135    }
136
137    /// Check if we should update based on timestamp/node_id
138    fn should_update(&self, timestamp: Timestamp, node_id: &NodeId) -> bool {
139        timestamp > self.timestamp
140            || (timestamp == self.timestamp && node_id.as_u32() > self.node_id.as_u32())
141    }
142}
143
144/// A Grow-only Counter (G-Counter)
145///
146/// Each node maintains its own count, total is the sum of all counts.
147/// Only supports increment operations.
148#[derive(Debug, Clone, Default)]
149pub struct GCounter {
150    /// Per-node counts
151    counts: BTreeMap<u32, u64>,
152}
153
154impl GCounter {
155    /// Create a new empty counter
156    pub fn new() -> Self {
157        Self {
158            counts: BTreeMap::new(),
159        }
160    }
161
162    /// Get the total count
163    pub fn value(&self) -> u64 {
164        self.counts.values().sum()
165    }
166
167    /// Increment the counter for a node
168    pub fn increment(&mut self, node_id: &NodeId, amount: u64) {
169        let count = self.counts.entry(node_id.as_u32()).or_insert(0);
170        *count = count.saturating_add(amount);
171    }
172
173    /// Get the count for a specific node
174    pub fn node_count(&self, node_id: &NodeId) -> u64 {
175        self.counts.get(&node_id.as_u32()).copied().unwrap_or(0)
176    }
177
178    /// Merge with another counter
179    ///
180    /// Takes the max of each node's count
181    pub fn merge(&mut self, other: &GCounter) {
182        for (&node_id, &count) in &other.counts {
183            let our_count = self.counts.entry(node_id).or_insert(0);
184            *our_count = (*our_count).max(count);
185        }
186    }
187
188    /// Get the number of nodes that have contributed
189    pub fn node_count_total(&self) -> usize {
190        self.counts.len()
191    }
192
193    /// Get all entries as (node_id, count) pairs
194    ///
195    /// Returns an iterator over all nodes and their counts.
196    /// Used for building delta documents.
197    pub fn entries(&self) -> impl Iterator<Item = (u32, u64)> + '_ {
198        self.counts.iter().map(|(&k, &v)| (k, v))
199    }
200
201    /// Encode to bytes for transmission
202    pub fn encode(&self) -> Vec<u8> {
203        let mut buf = Vec::with_capacity(4 + self.counts.len() * 12);
204        // Number of entries
205        buf.extend_from_slice(&(self.counts.len() as u32).to_le_bytes());
206        // Each entry: node_id (4 bytes) + count (8 bytes)
207        for (&node_id, &count) in &self.counts {
208            buf.extend_from_slice(&node_id.to_le_bytes());
209            buf.extend_from_slice(&count.to_le_bytes());
210        }
211        buf
212    }
213
214    /// Decode from bytes with validation
215    ///
216    /// # Security
217    /// Limits number of entries to prevent DoS via huge allocations.
218    /// Skips zero node IDs.
219    pub fn decode(data: &[u8]) -> Option<Self> {
220        if data.len() < 4 {
221            return None;
222        }
223        let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
224
225        // Security: Limit entries to prevent DoS via huge allocations
226        if num_entries > MAX_COUNTER_ENTRIES {
227            return None;
228        }
229
230        if data.len() < 4 + num_entries * 12 {
231            return None;
232        }
233
234        let mut counts = BTreeMap::new();
235        let mut offset = 4;
236        for _ in 0..num_entries {
237            let node_id = u32::from_le_bytes([
238                data[offset],
239                data[offset + 1],
240                data[offset + 2],
241                data[offset + 3],
242            ]);
243            let count = u64::from_le_bytes([
244                data[offset + 4],
245                data[offset + 5],
246                data[offset + 6],
247                data[offset + 7],
248                data[offset + 8],
249                data[offset + 9],
250                data[offset + 10],
251                data[offset + 11],
252            ]);
253            // Security: Skip zero node IDs
254            if node_id != 0 {
255                counts.insert(node_id, count);
256            }
257            offset += 12;
258        }
259
260        Some(Self { counts })
261    }
262}
263
264/// Position data with LWW semantics
265#[derive(Debug, Clone, Default, PartialEq)]
266pub struct Position {
267    /// Latitude in degrees
268    pub latitude: f32,
269    /// Longitude in degrees
270    pub longitude: f32,
271    /// Altitude in meters (optional)
272    pub altitude: Option<f32>,
273    /// Accuracy in meters (optional)
274    pub accuracy: Option<f32>,
275}
276
277impl Position {
278    /// Create a new position
279    pub fn new(latitude: f32, longitude: f32) -> Self {
280        Self {
281            latitude,
282            longitude,
283            altitude: None,
284            accuracy: None,
285        }
286    }
287
288    /// Create with altitude
289    pub fn with_altitude(mut self, altitude: f32) -> Self {
290        self.altitude = Some(altitude);
291        self
292    }
293
294    /// Create with accuracy
295    pub fn with_accuracy(mut self, accuracy: f32) -> Self {
296        self.accuracy = Some(accuracy);
297        self
298    }
299
300    /// Encode to bytes (12-20 bytes)
301    pub fn encode(&self) -> Vec<u8> {
302        let mut buf = Vec::with_capacity(20);
303        buf.extend_from_slice(&self.latitude.to_le_bytes());
304        buf.extend_from_slice(&self.longitude.to_le_bytes());
305
306        // Flags byte: bit 0 = has altitude, bit 1 = has accuracy
307        let mut flags = 0u8;
308        if self.altitude.is_some() {
309            flags |= 0x01;
310        }
311        if self.accuracy.is_some() {
312            flags |= 0x02;
313        }
314        buf.push(flags);
315
316        if let Some(alt) = self.altitude {
317            buf.extend_from_slice(&alt.to_le_bytes());
318        }
319        if let Some(acc) = self.accuracy {
320            buf.extend_from_slice(&acc.to_le_bytes());
321        }
322        buf
323    }
324
325    /// Decode from bytes with validation
326    ///
327    /// # Security
328    /// Validates that latitude/longitude are within valid ranges and not NaN/Inf.
329    /// Rejects garbage data that could indicate spoofing or corruption.
330    pub fn decode(data: &[u8]) -> Option<Self> {
331        if data.len() < 9 {
332            return None;
333        }
334
335        let latitude = f32::from_le_bytes([data[0], data[1], data[2], data[3]]);
336        let longitude = f32::from_le_bytes([data[4], data[5], data[6], data[7]]);
337        let flags = data[8];
338
339        // Security: Reject NaN, Inf, or out-of-range coordinates
340        if !latitude.is_finite() || !longitude.is_finite() {
341            return None;
342        }
343        if !(-90.0..=90.0).contains(&latitude) {
344            return None;
345        }
346        if !(-180.0..=180.0).contains(&longitude) {
347            return None;
348        }
349
350        let mut pos = Self::new(latitude, longitude);
351        let mut offset = 9;
352
353        if flags & 0x01 != 0 {
354            if data.len() < offset + 4 {
355                return None;
356            }
357            let alt = f32::from_le_bytes([
358                data[offset],
359                data[offset + 1],
360                data[offset + 2],
361                data[offset + 3],
362            ]);
363            // Security: Reject NaN/Inf altitude, allow wide range for valid Earth elevations
364            if !alt.is_finite() || !(-1000.0..=100000.0).contains(&alt) {
365                return None;
366            }
367            pos.altitude = Some(alt);
368            offset += 4;
369        }
370
371        if flags & 0x02 != 0 {
372            if data.len() < offset + 4 {
373                return None;
374            }
375            let acc = f32::from_le_bytes([
376                data[offset],
377                data[offset + 1],
378                data[offset + 2],
379                data[offset + 3],
380            ]);
381            // Security: Reject NaN/Inf/negative accuracy
382            if !acc.is_finite() || acc < 0.0 {
383                return None;
384            }
385            pos.accuracy = Some(acc);
386        }
387
388        Some(pos)
389    }
390}
391
392/// Health status data with LWW semantics
393#[derive(Debug, Clone, Default, PartialEq)]
394pub struct HealthStatus {
395    /// Battery percentage (0-100)
396    pub battery_percent: u8,
397    /// Heart rate BPM (optional)
398    pub heart_rate: Option<u8>,
399    /// Activity level (0=still, 1=walking, 2=running, 3=vehicle)
400    pub activity: u8,
401    /// Alert status flags
402    pub alerts: u8,
403}
404
405impl HealthStatus {
406    /// Alert flag: Man down
407    pub const ALERT_MAN_DOWN: u8 = 0x01;
408    /// Alert flag: Low battery
409    pub const ALERT_LOW_BATTERY: u8 = 0x02;
410    /// Alert flag: Out of range
411    pub const ALERT_OUT_OF_RANGE: u8 = 0x04;
412    /// Alert flag: Custom alert 1
413    pub const ALERT_CUSTOM_1: u8 = 0x08;
414
415    /// Create a new health status
416    pub fn new(battery_percent: u8) -> Self {
417        Self {
418            battery_percent,
419            heart_rate: None,
420            activity: 0,
421            alerts: 0,
422        }
423    }
424
425    /// Set heart rate
426    pub fn with_heart_rate(mut self, hr: u8) -> Self {
427        self.heart_rate = Some(hr);
428        self
429    }
430
431    /// Set activity level
432    pub fn with_activity(mut self, activity: u8) -> Self {
433        self.activity = activity;
434        self
435    }
436
437    /// Set alert flag
438    pub fn set_alert(&mut self, flag: u8) {
439        self.alerts |= flag;
440    }
441
442    /// Clear alert flag
443    pub fn clear_alert(&mut self, flag: u8) {
444        self.alerts &= !flag;
445    }
446
447    /// Check if alert is set
448    pub fn has_alert(&self, flag: u8) -> bool {
449        self.alerts & flag != 0
450    }
451
452    /// Encode to bytes (3-4 bytes)
453    pub fn encode(&self) -> Vec<u8> {
454        vec![
455            self.battery_percent,
456            self.activity,
457            self.alerts,
458            // Heart rate: 0 means not present, otherwise value
459            self.heart_rate.unwrap_or(0),
460        ]
461    }
462
463    /// Decode from bytes with validation
464    ///
465    /// # Security
466    /// Validates battery percentage and heart rate are within physiological bounds.
467    /// Rejects garbage data that could indicate spoofing or corruption.
468    pub fn decode(data: &[u8]) -> Option<Self> {
469        if data.len() < 4 {
470            return None;
471        }
472
473        let battery_percent = data[0];
474        let activity = data[1];
475        let alerts = data[2];
476        let heart_rate_raw = data[3];
477
478        // Security: Reject battery percentage > 100%
479        if battery_percent > MAX_BATTERY_PERCENT {
480            return None;
481        }
482
483        // Security: Validate activity is a known value (0-3)
484        if activity > 3 {
485            return None;
486        }
487
488        let mut status = Self::new(battery_percent);
489        status.activity = activity;
490        status.alerts = alerts;
491
492        // Security: Validate heart rate is in physiological range if present
493        if heart_rate_raw > 0 {
494            if !(MIN_HEART_RATE..=MAX_HEART_RATE).contains(&heart_rate_raw) {
495                return None;
496            }
497            status.heart_rate = Some(heart_rate_raw);
498        }
499
500        Some(status)
501    }
502}
503
504// ============================================================================
505// Peripheral (Sub-node) Types - for soldier-attached devices like M5Stack Core2
506// ============================================================================
507
508/// Type of peripheral device
509#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
510#[repr(u8)]
511pub enum PeripheralType {
512    /// Unknown/unspecified
513    #[default]
514    Unknown = 0,
515    /// Soldier-worn sensor (e.g., M5Stack Core2)
516    SoldierSensor = 1,
517    /// Fixed/stationary sensor
518    FixedSensor = 2,
519    /// Mesh relay only (no sensors)
520    Relay = 3,
521}
522
523impl PeripheralType {
524    /// Convert from u8 value
525    pub fn from_u8(v: u8) -> Self {
526        match v {
527            1 => Self::SoldierSensor,
528            2 => Self::FixedSensor,
529            3 => Self::Relay,
530            _ => Self::Unknown,
531        }
532    }
533}
534
535/// Event types that a peripheral can emit (e.g., from tap input)
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
537#[repr(u8)]
538pub enum EventType {
539    /// No event / cleared
540    #[default]
541    None = 0,
542    /// "I'm OK" ping
543    Ping = 1,
544    /// Request assistance
545    NeedAssist = 2,
546    /// Emergency / SOS
547    Emergency = 3,
548    /// Moving / in transit
549    Moving = 4,
550    /// In position / stationary
551    InPosition = 5,
552    /// Acknowledged / copy
553    Ack = 6,
554}
555
556impl EventType {
557    /// Convert from u8 value
558    pub fn from_u8(v: u8) -> Self {
559        match v {
560            1 => Self::Ping,
561            2 => Self::NeedAssist,
562            3 => Self::Emergency,
563            4 => Self::Moving,
564            5 => Self::InPosition,
565            6 => Self::Ack,
566            _ => Self::None,
567        }
568    }
569
570    /// Human-readable label for display
571    pub fn label(&self) -> &'static str {
572        match self {
573            Self::None => "",
574            Self::Ping => "PING",
575            Self::NeedAssist => "NEED ASSIST",
576            Self::Emergency => "EMERGENCY",
577            Self::Moving => "MOVING",
578            Self::InPosition => "IN POSITION",
579            Self::Ack => "ACK",
580        }
581    }
582}
583
584/// An event emitted by a peripheral (e.g., tap on Core2)
585#[derive(Debug, Clone, Default, PartialEq)]
586pub struct PeripheralEvent {
587    /// Type of event
588    pub event_type: EventType,
589    /// Timestamp when event occurred (ms since epoch or boot)
590    pub timestamp: u64,
591}
592
593impl PeripheralEvent {
594    /// Create a new peripheral event
595    pub fn new(event_type: EventType, timestamp: u64) -> Self {
596        Self {
597            event_type,
598            timestamp,
599        }
600    }
601
602    /// Encode to bytes (9 bytes)
603    pub fn encode(&self) -> Vec<u8> {
604        let mut buf = Vec::with_capacity(9);
605        buf.push(self.event_type as u8);
606        buf.extend_from_slice(&self.timestamp.to_le_bytes());
607        buf
608    }
609
610    /// Decode from bytes with validation
611    ///
612    /// # Security
613    /// Validates timestamp is within reasonable bounds.
614    /// Rejects garbage data that could indicate spoofing or corruption.
615    pub fn decode(data: &[u8]) -> Option<Self> {
616        if data.len() < 9 {
617            return None;
618        }
619
620        let event_type = EventType::from_u8(data[0]);
621        let timestamp = u64::from_le_bytes([
622            data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8],
623        ]);
624
625        // Security: Reject events with timestamps before 2020
626        // Note: timestamp could be 0 for "no event" scenarios, so we allow that
627        if timestamp != 0 && timestamp < MIN_VALID_TIMESTAMP {
628            return None;
629        }
630
631        Some(Self {
632            event_type,
633            timestamp,
634        })
635    }
636}
637
638/// An emergency event with acknowledgment tracking (CRDT)
639///
640/// Represents a single emergency incident with distributed ACK tracking.
641/// Each node in the mesh can acknowledge the emergency, and this state
642/// is replicated across all nodes using CRDT semantics.
643///
644/// ## CRDT Semantics
645///
646/// - **Identity**: Events are uniquely identified by (source_node, timestamp)
647/// - **Merge for same event**: ACK maps merge with OR (once acked, stays acked)
648/// - **Merge for different events**: Higher timestamp wins (newer emergency replaces older)
649/// - **Monotonic**: ACK state only moves from false → true, never back
650///
651/// ## Wire Format
652///
653/// ```text
654/// source_node: 4 bytes (LE)
655/// timestamp:   8 bytes (LE)
656/// num_acks:    4 bytes (LE)
657/// acks[N]:
658///   node_id:   4 bytes (LE)
659///   acked:     1 byte (0 or 1)
660/// ```
661#[derive(Debug, Clone, PartialEq, Default)]
662pub struct EmergencyEvent {
663    /// Node that triggered the emergency
664    source_node: u32,
665    /// Timestamp when emergency was triggered (for uniqueness)
666    timestamp: u64,
667    /// ACK status for each known peer: node_id -> has_acked
668    acks: BTreeMap<u32, bool>,
669}
670
671impl EmergencyEvent {
672    /// Create a new emergency event
673    ///
674    /// # Arguments
675    /// * `source_node` - Node ID that triggered the emergency
676    /// * `timestamp` - When the emergency was triggered
677    /// * `known_peers` - List of peer node IDs to track for ACKs
678    ///
679    /// The source node is automatically marked as acknowledged.
680    pub fn new(source_node: u32, timestamp: u64, known_peers: &[u32]) -> Self {
681        let mut acks = BTreeMap::new();
682
683        // Source node implicitly ACKs their own emergency
684        acks.insert(source_node, true);
685
686        // All other known peers start as not-acked
687        for &peer_id in known_peers {
688            if peer_id != source_node {
689                acks.entry(peer_id).or_insert(false);
690            }
691        }
692
693        Self {
694            source_node,
695            timestamp,
696            acks,
697        }
698    }
699
700    /// Get the source node that triggered the emergency
701    pub fn source_node(&self) -> u32 {
702        self.source_node
703    }
704
705    /// Get the timestamp when the emergency was triggered
706    pub fn timestamp(&self) -> u64 {
707        self.timestamp
708    }
709
710    /// Check if a specific node has acknowledged
711    pub fn has_acked(&self, node_id: u32) -> bool {
712        self.acks.get(&node_id).copied().unwrap_or(false)
713    }
714
715    /// Record an acknowledgment from a node
716    ///
717    /// Returns true if this was a new ACK (state changed)
718    pub fn ack(&mut self, node_id: u32) -> bool {
719        let entry = self.acks.entry(node_id).or_insert(false);
720        if !*entry {
721            *entry = true;
722            true
723        } else {
724            false
725        }
726    }
727
728    /// Add a peer to track (if not already present)
729    ///
730    /// New peers start as not-acked. This is useful when discovering
731    /// new peers after the emergency was created.
732    pub fn add_peer(&mut self, node_id: u32) {
733        self.acks.entry(node_id).or_insert(false);
734    }
735
736    /// Get list of nodes that have acknowledged
737    pub fn acked_nodes(&self) -> Vec<u32> {
738        self.acks
739            .iter()
740            .filter(|(_, &acked)| acked)
741            .map(|(&node_id, _)| node_id)
742            .collect()
743    }
744
745    /// Get list of nodes that have NOT acknowledged
746    pub fn pending_nodes(&self) -> Vec<u32> {
747        self.acks
748            .iter()
749            .filter(|(_, &acked)| !acked)
750            .map(|(&node_id, _)| node_id)
751            .collect()
752    }
753
754    /// Get all tracked node IDs (both acked and pending)
755    pub fn all_nodes(&self) -> Vec<u32> {
756        self.acks.keys().copied().collect()
757    }
758
759    /// Check if all tracked nodes have acknowledged
760    pub fn all_acked(&self) -> bool {
761        !self.acks.is_empty() && self.acks.values().all(|&acked| acked)
762    }
763
764    /// Get the total number of tracked nodes
765    pub fn peer_count(&self) -> usize {
766        self.acks.len()
767    }
768
769    /// Get the number of nodes that have acknowledged
770    pub fn ack_count(&self) -> usize {
771        self.acks.values().filter(|&&acked| acked).count()
772    }
773
774    /// Merge with another emergency event (CRDT semantics)
775    ///
776    /// # Returns
777    /// `true` if our state changed
778    ///
779    /// # Semantics
780    /// - Same event (source_node, timestamp): merge ACK maps with OR
781    /// - Different event: take the one with higher timestamp
782    pub fn merge(&mut self, other: &EmergencyEvent) -> bool {
783        // Different emergency - take newer one
784        if self.source_node != other.source_node || self.timestamp != other.timestamp {
785            if other.timestamp > self.timestamp {
786                *self = other.clone();
787                return true;
788            }
789            return false;
790        }
791
792        // Same emergency - merge ACK maps with OR
793        let mut changed = false;
794        for (&node_id, &other_acked) in &other.acks {
795            let entry = self.acks.entry(node_id).or_insert(false);
796            if other_acked && !*entry {
797                *entry = true;
798                changed = true;
799            }
800        }
801        changed
802    }
803
804    /// Encode to bytes for transmission
805    ///
806    /// Format: source_node(4) + timestamp(8) + num_acks(4) + acks[N](5 each)
807    pub fn encode(&self) -> Vec<u8> {
808        let mut buf = Vec::with_capacity(16 + self.acks.len() * 5);
809
810        buf.extend_from_slice(&self.source_node.to_le_bytes());
811        buf.extend_from_slice(&self.timestamp.to_le_bytes());
812        buf.extend_from_slice(&(self.acks.len() as u32).to_le_bytes());
813
814        for (&node_id, &acked) in &self.acks {
815            buf.extend_from_slice(&node_id.to_le_bytes());
816            buf.push(if acked { 1 } else { 0 });
817        }
818
819        buf
820    }
821
822    /// Decode from bytes with validation
823    ///
824    /// # Security
825    /// Validates source_node, timestamp, and limits ACK entries to prevent DoS.
826    /// Rejects garbage data that could indicate spoofing or corruption.
827    pub fn decode(data: &[u8]) -> Option<Self> {
828        if data.len() < 16 {
829            return None;
830        }
831
832        let source_node = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
833        let timestamp = u64::from_le_bytes([
834            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
835        ]);
836        let num_acks = u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
837
838        // Security: Reject zero source_node (invalid)
839        if source_node == 0 {
840            return None;
841        }
842
843        // Security: Reject timestamps before 2020
844        if timestamp < MIN_VALID_TIMESTAMP {
845            return None;
846        }
847
848        // Security: Limit ACK entries to prevent DoS via huge allocations
849        if num_acks > MAX_EMERGENCY_ACKS {
850            return None;
851        }
852
853        if data.len() < 16 + num_acks * 5 {
854            return None;
855        }
856
857        let mut acks = BTreeMap::new();
858        let mut offset = 16;
859        for _ in 0..num_acks {
860            let node_id = u32::from_le_bytes([
861                data[offset],
862                data[offset + 1],
863                data[offset + 2],
864                data[offset + 3],
865            ]);
866            // Security: Skip zero node_ids in ACK list
867            if node_id != 0 {
868                let acked = data[offset + 4] != 0;
869                acks.insert(node_id, acked);
870            }
871            offset += 5;
872        }
873
874        Some(Self {
875            source_node,
876            timestamp,
877            acks,
878        })
879    }
880}
881
882/// A peripheral device attached to a Node (soldier)
883///
884/// Peripherals are sub-tier devices that augment a soldier's capabilities
885/// with sensors and input (e.g., M5Stack Core2 wearable).
886#[derive(Debug, Clone, Default)]
887pub struct Peripheral {
888    /// Unique peripheral ID (derived from device MAC or similar)
889    pub id: u32,
890    /// Parent Node ID this peripheral is attached to (0 if not paired)
891    pub parent_node: u32,
892    /// Type of peripheral
893    pub peripheral_type: PeripheralType,
894    /// Callsign/name (inherited from parent or configured)
895    pub callsign: [u8; 12],
896    /// Current health status
897    pub health: HealthStatus,
898    /// Most recent event (if any)
899    pub last_event: Option<PeripheralEvent>,
900    /// Current location (if available)
901    pub location: Option<Position>,
902    /// Last update timestamp
903    pub timestamp: u64,
904}
905
906impl Peripheral {
907    /// Create a new peripheral
908    pub fn new(id: u32, peripheral_type: PeripheralType) -> Self {
909        Self {
910            id,
911            parent_node: 0,
912            peripheral_type,
913            callsign: [0u8; 12],
914            health: HealthStatus::default(),
915            last_event: None,
916            location: None,
917            timestamp: 0,
918        }
919    }
920
921    /// Set the callsign (truncated to 12 bytes)
922    pub fn with_callsign(mut self, callsign: &str) -> Self {
923        let bytes = callsign.as_bytes();
924        let len = bytes.len().min(12);
925        self.callsign[..len].copy_from_slice(&bytes[..len]);
926        self
927    }
928
929    /// Update callsign in place
930    pub fn set_callsign(&mut self, callsign: &str) {
931        self.callsign = [0u8; 12];
932        let bytes = callsign.as_bytes();
933        let len = bytes.len().min(12);
934        self.callsign[..len].copy_from_slice(&bytes[..len]);
935    }
936
937    /// Get callsign as string
938    pub fn callsign_str(&self) -> &str {
939        let len = self.callsign.iter().position(|&b| b == 0).unwrap_or(12);
940        core::str::from_utf8(&self.callsign[..len]).unwrap_or("")
941    }
942
943    /// Set parent node
944    pub fn with_parent(mut self, parent_node: u32) -> Self {
945        self.parent_node = parent_node;
946        self
947    }
948
949    /// Set location (builder pattern)
950    pub fn with_location(mut self, location: Position) -> Self {
951        self.location = Some(location);
952        self
953    }
954
955    /// Update location in place
956    pub fn set_location(&mut self, latitude: f32, longitude: f32, altitude: Option<f32>) {
957        let mut pos = Position::new(latitude, longitude);
958        if let Some(alt) = altitude {
959            pos = pos.with_altitude(alt);
960        }
961        self.location = Some(pos);
962    }
963
964    /// Clear location
965    pub fn clear_location(&mut self) {
966        self.location = None;
967    }
968
969    /// Record an event
970    pub fn set_event(&mut self, event_type: EventType, timestamp: u64) {
971        self.last_event = Some(PeripheralEvent::new(event_type, timestamp));
972        self.timestamp = timestamp;
973    }
974
975    /// Clear the last event
976    pub fn clear_event(&mut self) {
977        self.last_event = None;
978    }
979
980    /// Encode to bytes for BLE transmission
981    /// Format: [id:4][parent:4][type:1][callsign:12][health:4][has_event:1][event:9?][timestamp:8][has_location:1][location:9-20?]
982    /// Size: 35 bytes minimum (no event, no location), up to 64 bytes with both
983    pub fn encode(&self) -> Vec<u8> {
984        let mut buf = Vec::with_capacity(64);
985        buf.extend_from_slice(&self.id.to_le_bytes());
986        buf.extend_from_slice(&self.parent_node.to_le_bytes());
987        buf.push(self.peripheral_type as u8);
988        buf.extend_from_slice(&self.callsign);
989        buf.extend_from_slice(&self.health.encode());
990
991        if let Some(ref event) = self.last_event {
992            buf.push(1); // has event
993            buf.extend_from_slice(&event.encode());
994        } else {
995            buf.push(0); // no event
996        }
997
998        buf.extend_from_slice(&self.timestamp.to_le_bytes());
999
1000        // Location encoding (added in v0.1.0-rc.7)
1001        if let Some(ref location) = self.location {
1002            buf.push(1); // has location
1003            buf.extend_from_slice(&location.encode());
1004        } else {
1005            buf.push(0); // no location
1006        }
1007
1008        buf
1009    }
1010
1011    /// Decode from bytes with validation
1012    ///
1013    /// # Security
1014    /// Validates peripheral ID, callsign UTF-8, and timestamp.
1015    /// Rejects garbage data that could indicate spoofing or corruption.
1016    pub fn decode(data: &[u8]) -> Option<Self> {
1017        if data.len() < 34 {
1018            return None;
1019        }
1020
1021        let id = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1022        let parent_node = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
1023        let peripheral_type = PeripheralType::from_u8(data[8]);
1024
1025        // Security: Reject zero peripheral ID (invalid)
1026        if id == 0 {
1027            return None;
1028        }
1029
1030        let mut callsign = [0u8; 12];
1031        callsign.copy_from_slice(&data[9..21]);
1032
1033        // Security: Validate callsign is valid UTF-8
1034        // Find the actual length (up to first null or end)
1035        let callsign_len = callsign.iter().position(|&b| b == 0).unwrap_or(12);
1036        if callsign_len > 0 && core::str::from_utf8(&callsign[..callsign_len]).is_err() {
1037            return None;
1038        }
1039
1040        // HealthStatus::decode now validates internally
1041        let health = HealthStatus::decode(&data[21..25])?;
1042
1043        let has_event = data[25] != 0;
1044        let (last_event, timestamp_offset) = if has_event {
1045            if data.len() < 43 {
1046                return None;
1047            }
1048            // PeripheralEvent::decode now validates timestamp internally
1049            (PeripheralEvent::decode(&data[26..35]), 35)
1050        } else {
1051            (None, 26)
1052        };
1053
1054        if data.len() < timestamp_offset + 8 {
1055            return None;
1056        }
1057
1058        let timestamp = u64::from_le_bytes([
1059            data[timestamp_offset],
1060            data[timestamp_offset + 1],
1061            data[timestamp_offset + 2],
1062            data[timestamp_offset + 3],
1063            data[timestamp_offset + 4],
1064            data[timestamp_offset + 5],
1065            data[timestamp_offset + 6],
1066            data[timestamp_offset + 7],
1067        ]);
1068
1069        // Security: Reject timestamps before 2020 (0 allowed for "not set")
1070        if timestamp != 0 && timestamp < MIN_VALID_TIMESTAMP {
1071            return None;
1072        }
1073
1074        // Decode location if present (added in v0.1.0-rc.7)
1075        // For backward compatibility, treat missing has_location byte as no location
1076        let location_offset = timestamp_offset + 8;
1077        let location = if data.len() > location_offset {
1078            let has_location = data[location_offset] != 0;
1079            if has_location && data.len() > location_offset + 1 {
1080                Position::decode(&data[location_offset + 1..])
1081            } else {
1082                None
1083            }
1084        } else {
1085            None
1086        };
1087
1088        Some(Self {
1089            id,
1090            parent_node,
1091            peripheral_type,
1092            callsign,
1093            health,
1094            last_event,
1095            location,
1096            timestamp,
1097        })
1098    }
1099}
1100
1101// ============================================================================
1102// ChatCRDT - Add-only set of chat messages for mesh-wide messaging
1103// (gated behind "legacy-chat" feature, deprecated in favor of CannedMessage)
1104// ============================================================================
1105
1106#[cfg(feature = "legacy-chat")]
1107/// Maximum message text length in bytes
1108pub const CHAT_MAX_TEXT_LEN: usize = 128;
1109
1110#[cfg(feature = "legacy-chat")]
1111/// Maximum sender name length in bytes
1112pub const CHAT_MAX_SENDER_LEN: usize = 12;
1113
1114#[cfg(feature = "legacy-chat")]
1115/// Maximum number of messages to retain in the CRDT
1116///
1117/// Older messages are pruned to keep memory bounded on embedded devices.
1118pub const CHAT_MAX_MESSAGES: usize = 32;
1119
1120#[cfg(feature = "legacy-chat")]
1121/// Maximum number of chat messages to include in sync documents
1122///
1123/// BLE GATT notifications have a limited MTU (typically 512 bytes).
1124/// To avoid exceeding this limit, sync documents only include the
1125/// most recent messages. The full history is kept in the local CRDT.
1126pub const CHAT_SYNC_LIMIT: usize = 8;
1127
1128#[cfg(feature = "legacy-chat")]
1129/// A single chat message in the mesh
1130///
1131/// Messages are uniquely identified by `(origin_node, timestamp)`.
1132/// This allows deduplication across mesh sync while preserving message ordering.
1133#[derive(Debug, Clone, PartialEq)]
1134pub struct ChatMessage {
1135    /// Node that originated this message
1136    pub origin_node: u32,
1137    /// Timestamp when message was created (ms since epoch)
1138    pub timestamp: u64,
1139    /// Sender name/callsign (up to 12 bytes)
1140    sender: [u8; CHAT_MAX_SENDER_LEN],
1141    sender_len: u8,
1142    /// Message text (up to 128 bytes)
1143    text: [u8; CHAT_MAX_TEXT_LEN],
1144    text_len: u8,
1145    /// Whether this is a broadcast message (vs directed)
1146    pub is_broadcast: bool,
1147    /// Whether ACK is requested
1148    pub requires_ack: bool,
1149    /// Reply-to: origin node of the message being replied to (0 = not a reply)
1150    pub reply_to_node: u32,
1151    /// Reply-to: timestamp of the message being replied to (0 = not a reply)
1152    pub reply_to_timestamp: u64,
1153}
1154
1155#[cfg(feature = "legacy-chat")]
1156impl Default for ChatMessage {
1157    fn default() -> Self {
1158        Self {
1159            origin_node: 0,
1160            timestamp: 0,
1161            sender: [0u8; CHAT_MAX_SENDER_LEN],
1162            sender_len: 0,
1163            text: [0u8; CHAT_MAX_TEXT_LEN],
1164            text_len: 0,
1165            is_broadcast: true,
1166            requires_ack: false,
1167            reply_to_node: 0,
1168            reply_to_timestamp: 0,
1169        }
1170    }
1171}
1172
1173#[cfg(feature = "legacy-chat")]
1174impl ChatMessage {
1175    /// Create a new chat message
1176    pub fn new(origin_node: u32, timestamp: u64, sender: &str, text: &str) -> Self {
1177        let mut msg = Self {
1178            origin_node,
1179            timestamp,
1180            ..Default::default()
1181        };
1182        msg.set_sender(sender);
1183        msg.set_text(text);
1184        msg
1185    }
1186
1187    /// Set the sender name (truncated to 12 bytes)
1188    pub fn set_sender(&mut self, sender: &str) {
1189        let bytes = sender.as_bytes();
1190        let len = bytes.len().min(CHAT_MAX_SENDER_LEN);
1191        self.sender[..len].copy_from_slice(&bytes[..len]);
1192        self.sender_len = len as u8;
1193    }
1194
1195    /// Get the sender name as a string
1196    pub fn sender(&self) -> &str {
1197        core::str::from_utf8(&self.sender[..self.sender_len as usize]).unwrap_or("")
1198    }
1199
1200    /// Set the message text (truncated to 128 bytes)
1201    pub fn set_text(&mut self, text: &str) {
1202        let bytes = text.as_bytes();
1203        let len = bytes.len().min(CHAT_MAX_TEXT_LEN);
1204        self.text[..len].copy_from_slice(&bytes[..len]);
1205        self.text_len = len as u8;
1206    }
1207
1208    /// Get the message text as a string
1209    pub fn text(&self) -> &str {
1210        core::str::from_utf8(&self.text[..self.text_len as usize]).unwrap_or("")
1211    }
1212
1213    /// Set reply-to information
1214    pub fn set_reply_to(&mut self, node: u32, timestamp: u64) {
1215        self.reply_to_node = node;
1216        self.reply_to_timestamp = timestamp;
1217    }
1218
1219    /// Check if this is a reply to another message
1220    pub fn is_reply(&self) -> bool {
1221        self.reply_to_node != 0 || self.reply_to_timestamp != 0
1222    }
1223
1224    /// Get the unique message ID (combines origin_node and timestamp)
1225    ///
1226    /// Format: `(origin_node as u64) << 32 | (timestamp & 0xFFFFFFFF)`
1227    /// This provides a sortable key where messages from same node are ordered by time.
1228    pub fn message_id(&self) -> u64 {
1229        ((self.origin_node as u64) << 32) | (self.timestamp & 0xFFFFFFFF)
1230    }
1231
1232    /// Encode to bytes for transmission
1233    ///
1234    /// Wire format:
1235    /// ```text
1236    /// origin_node:       4 bytes (LE)
1237    /// timestamp:         8 bytes (LE)
1238    /// sender_len:        1 byte
1239    /// sender:            sender_len bytes
1240    /// text_len:          1 byte
1241    /// text:              text_len bytes
1242    /// flags:             1 byte (bit 0: is_broadcast, bit 1: requires_ack)
1243    /// reply_to_node:     4 bytes (LE)
1244    /// reply_to_timestamp: 8 bytes (LE)
1245    /// ```
1246    pub fn encode(&self) -> Vec<u8> {
1247        let size = 4 + 8 + 1 + self.sender_len as usize + 1 + self.text_len as usize + 1 + 4 + 8;
1248        let mut buf = Vec::with_capacity(size);
1249
1250        buf.extend_from_slice(&self.origin_node.to_le_bytes());
1251        buf.extend_from_slice(&self.timestamp.to_le_bytes());
1252        buf.push(self.sender_len);
1253        buf.extend_from_slice(&self.sender[..self.sender_len as usize]);
1254        buf.push(self.text_len);
1255        buf.extend_from_slice(&self.text[..self.text_len as usize]);
1256
1257        let mut flags = 0u8;
1258        if self.is_broadcast {
1259            flags |= 0x01;
1260        }
1261        if self.requires_ack {
1262            flags |= 0x02;
1263        }
1264        buf.push(flags);
1265
1266        buf.extend_from_slice(&self.reply_to_node.to_le_bytes());
1267        buf.extend_from_slice(&self.reply_to_timestamp.to_le_bytes());
1268
1269        buf
1270    }
1271
1272    /// Decode from bytes with strict validation
1273    ///
1274    /// Returns `None` if the data is malformed or fails validation checks.
1275    /// This is a security-critical function - malformed messages could be
1276    /// spoofed or part of an attack.
1277    ///
1278    /// # Validation checks:
1279    /// - origin_node must be non-zero
1280    /// - timestamp must be non-zero and within reasonable bounds
1281    /// - sender must be non-empty and valid UTF-8
1282    /// - text must be valid UTF-8 (can be empty for ACK messages)
1283    /// - All length fields must be within bounds
1284    pub fn decode(data: &[u8]) -> Option<(Self, usize)> {
1285        if data.len() < 14 {
1286            // Minimum: 4 + 8 + 1 + 0 + 1 + 0 + 0 (no reply fields in old format)
1287            return None;
1288        }
1289
1290        let origin_node = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1291        let timestamp = u64::from_le_bytes([
1292            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
1293        ]);
1294
1295        // Security: Reject messages with zero origin_node (invalid/spoofed)
1296        if origin_node == 0 {
1297            return None;
1298        }
1299
1300        // Security: Reject messages with timestamps before 2020
1301        if timestamp < MIN_VALID_TIMESTAMP {
1302            return None;
1303        }
1304
1305        let sender_len = data[12] as usize;
1306        if sender_len > CHAT_MAX_SENDER_LEN || data.len() < 13 + sender_len + 1 {
1307            return None;
1308        }
1309
1310        // Security: Reject messages with empty sender (required field)
1311        if sender_len == 0 {
1312            return None;
1313        }
1314
1315        let mut sender = [0u8; CHAT_MAX_SENDER_LEN];
1316        sender[..sender_len].copy_from_slice(&data[13..13 + sender_len]);
1317
1318        // Security: Validate sender is valid UTF-8 (reject garbage/binary data)
1319        if core::str::from_utf8(&sender[..sender_len]).is_err() {
1320            return None;
1321        }
1322
1323        let text_offset = 13 + sender_len;
1324        let text_len = data[text_offset] as usize;
1325        if text_len > CHAT_MAX_TEXT_LEN || data.len() < text_offset + 1 + text_len + 1 {
1326            return None;
1327        }
1328
1329        let mut text = [0u8; CHAT_MAX_TEXT_LEN];
1330        text[..text_len].copy_from_slice(&data[text_offset + 1..text_offset + 1 + text_len]);
1331
1332        // Security: Validate text is valid UTF-8 (reject garbage/binary data)
1333        // Empty text is allowed (e.g., for ACK messages)
1334        if text_len > 0 && core::str::from_utf8(&text[..text_len]).is_err() {
1335            return None;
1336        }
1337
1338        let flags_offset = text_offset + 1 + text_len;
1339        let flags = data[flags_offset];
1340        let is_broadcast = flags & 0x01 != 0;
1341        let requires_ack = flags & 0x02 != 0;
1342
1343        // Reply-to fields (optional for backward compat)
1344        let mut reply_to_node = 0u32;
1345        let mut reply_to_timestamp = 0u64;
1346        let mut total_len = flags_offset + 1;
1347
1348        if data.len() >= flags_offset + 1 + 12 {
1349            reply_to_node = u32::from_le_bytes([
1350                data[flags_offset + 1],
1351                data[flags_offset + 2],
1352                data[flags_offset + 3],
1353                data[flags_offset + 4],
1354            ]);
1355            reply_to_timestamp = u64::from_le_bytes([
1356                data[flags_offset + 5],
1357                data[flags_offset + 6],
1358                data[flags_offset + 7],
1359                data[flags_offset + 8],
1360                data[flags_offset + 9],
1361                data[flags_offset + 10],
1362                data[flags_offset + 11],
1363                data[flags_offset + 12],
1364            ]);
1365            total_len = flags_offset + 13;
1366        }
1367
1368        Some((
1369            Self {
1370                origin_node,
1371                timestamp,
1372                sender,
1373                sender_len: sender_len as u8,
1374                text,
1375                text_len: text_len as u8,
1376                is_broadcast,
1377                requires_ack,
1378                reply_to_node,
1379                reply_to_timestamp,
1380            },
1381            total_len,
1382        ))
1383    }
1384}
1385
1386#[cfg(feature = "legacy-chat")]
1387/// Chat CRDT - Add-only set of messages
1388///
1389/// Implements add-only set semantics where messages are identified by
1390/// `(origin_node, timestamp)`. Once a message is added, it cannot be removed
1391/// (tombstone-free design optimized for mesh networks).
1392///
1393/// ## CRDT Semantics
1394///
1395/// - **Merge**: Union of all messages from both sets
1396/// - **Identity**: `(origin_node, timestamp)` - duplicates are ignored
1397/// - **Ordering**: Messages are stored sorted by message_id for efficient iteration
1398/// - **Pruning**: Oldest messages are removed when exceeding `CHAT_MAX_MESSAGES`
1399///
1400/// ## Wire Format
1401///
1402/// ```text
1403/// num_messages: 2 bytes (LE)
1404/// messages[N]:  variable (see ChatMessage::encode)
1405/// ```
1406#[derive(Debug, Clone, Default)]
1407pub struct ChatCRDT {
1408    /// Messages indexed by message_id for deduplication
1409    messages: BTreeMap<u64, ChatMessage>,
1410}
1411
1412#[cfg(feature = "legacy-chat")]
1413impl ChatCRDT {
1414    /// Create a new empty chat CRDT
1415    pub fn new() -> Self {
1416        Self {
1417            messages: BTreeMap::new(),
1418        }
1419    }
1420
1421    /// Add a message to the chat
1422    ///
1423    /// Returns `true` if the message was new (not a duplicate)
1424    pub fn add_message(&mut self, message: ChatMessage) -> bool {
1425        let id = message.message_id();
1426        if self.messages.contains_key(&id) {
1427            return false;
1428        }
1429
1430        self.messages.insert(id, message);
1431        self.prune_if_needed();
1432        true
1433    }
1434
1435    /// Create and add a new message
1436    pub fn send_message(
1437        &mut self,
1438        origin_node: u32,
1439        timestamp: u64,
1440        sender: &str,
1441        text: &str,
1442    ) -> bool {
1443        let msg = ChatMessage::new(origin_node, timestamp, sender, text);
1444        self.add_message(msg)
1445    }
1446
1447    /// Get a message by its ID
1448    pub fn get_message(&self, origin_node: u32, timestamp: u64) -> Option<&ChatMessage> {
1449        let id = ((origin_node as u64) << 32) | (timestamp & 0xFFFFFFFF);
1450        self.messages.get(&id)
1451    }
1452
1453    /// Get all messages, ordered by message_id
1454    pub fn messages(&self) -> impl Iterator<Item = &ChatMessage> {
1455        self.messages.values()
1456    }
1457
1458    /// Get messages newer than a given timestamp
1459    pub fn messages_since(&self, since_timestamp: u64) -> impl Iterator<Item = &ChatMessage> {
1460        self.messages
1461            .values()
1462            .filter(move |m| m.timestamp > since_timestamp)
1463    }
1464
1465    /// Get the number of messages
1466    pub fn len(&self) -> usize {
1467        self.messages.len()
1468    }
1469
1470    /// Check if there are no messages
1471    pub fn is_empty(&self) -> bool {
1472        self.messages.is_empty()
1473    }
1474
1475    /// Get the newest message timestamp (if any)
1476    pub fn newest_timestamp(&self) -> Option<u64> {
1477        self.messages.values().map(|m| m.timestamp).max()
1478    }
1479
1480    /// Merge with another ChatCRDT
1481    ///
1482    /// Returns `true` if any new messages were added
1483    pub fn merge(&mut self, other: &ChatCRDT) -> bool {
1484        let mut changed = false;
1485        for (id, msg) in &other.messages {
1486            if !self.messages.contains_key(id) {
1487                self.messages.insert(*id, msg.clone());
1488                changed = true;
1489            }
1490        }
1491        if changed {
1492            self.prune_if_needed();
1493        }
1494        changed
1495    }
1496
1497    /// Prune oldest messages if we exceed the limit
1498    fn prune_if_needed(&mut self) {
1499        while self.messages.len() > CHAT_MAX_MESSAGES {
1500            // Remove the entry with the lowest timestamp
1501            if let Some(&oldest_id) = self.messages.keys().next() {
1502                self.messages.remove(&oldest_id);
1503            }
1504        }
1505    }
1506
1507    /// Encode to bytes for transmission
1508    pub fn encode(&self) -> Vec<u8> {
1509        let mut buf = Vec::new();
1510
1511        // Number of messages
1512        buf.extend_from_slice(&(self.messages.len() as u16).to_le_bytes());
1513
1514        // Each message
1515        for msg in self.messages.values() {
1516            buf.extend_from_slice(&msg.encode());
1517        }
1518
1519        buf
1520    }
1521
1522    /// Decode from bytes
1523    pub fn decode(data: &[u8]) -> Option<Self> {
1524        if data.len() < 2 {
1525            return None;
1526        }
1527
1528        let num_messages = u16::from_le_bytes([data[0], data[1]]) as usize;
1529        let mut messages = BTreeMap::new();
1530        let mut offset = 2;
1531
1532        for _ in 0..num_messages {
1533            if offset >= data.len() {
1534                break;
1535            }
1536            if let Some((msg, len)) = ChatMessage::decode(&data[offset..]) {
1537                let id = msg.message_id();
1538                messages.insert(id, msg);
1539                offset += len;
1540            } else {
1541                break;
1542            }
1543        }
1544
1545        Some(Self { messages })
1546    }
1547
1548    /// Get the encoded size of this CRDT
1549    pub fn encoded_size(&self) -> usize {
1550        2 + self
1551            .messages
1552            .values()
1553            .map(|m| m.encode().len())
1554            .sum::<usize>()
1555    }
1556
1557    /// Create a copy limited to the most recent messages for sync
1558    ///
1559    /// Returns a new ChatCRDT containing only the N most recent messages,
1560    /// where N is `CHAT_SYNC_LIMIT`. This is used when building sync documents
1561    /// to avoid exceeding BLE MTU limits.
1562    ///
1563    /// The local CRDT retains all messages up to `CHAT_MAX_MESSAGES`.
1564    pub fn for_sync(&self) -> Self {
1565        if self.messages.len() <= CHAT_SYNC_LIMIT {
1566            return self.clone();
1567        }
1568
1569        // BTreeMap is ordered by key (message_id), and message_id encodes
1570        // timestamp in lower bits, so we take from the end for newest
1571        let messages: BTreeMap<u64, ChatMessage> = self
1572            .messages
1573            .iter()
1574            .rev()
1575            .take(CHAT_SYNC_LIMIT)
1576            .map(|(&k, v)| (k, v.clone()))
1577            .collect();
1578
1579        Self { messages }
1580    }
1581}
1582
1583/// CRDT operation types for sync
1584#[derive(Debug, Clone)]
1585pub enum CrdtOperation {
1586    /// Update a position register
1587    UpdatePosition {
1588        /// Node ID that owns this position
1589        node_id: NodeId,
1590        /// Position data
1591        position: Position,
1592        /// Timestamp of the update
1593        timestamp: Timestamp,
1594    },
1595    /// Update health status register
1596    UpdateHealth {
1597        /// Node ID that owns this status
1598        node_id: NodeId,
1599        /// Health status data
1600        status: HealthStatus,
1601        /// Timestamp of the update
1602        timestamp: Timestamp,
1603    },
1604    /// Increment a counter
1605    IncrementCounter {
1606        /// Counter identifier
1607        counter_id: u8,
1608        /// Node performing the increment
1609        node_id: NodeId,
1610        /// Amount to increment
1611        amount: u64,
1612    },
1613    /// Generic LWW update (key-value)
1614    UpdateRegister {
1615        /// Key for the register
1616        key: String,
1617        /// Value data
1618        value: Vec<u8>,
1619        /// Timestamp of the update
1620        timestamp: Timestamp,
1621        /// Node that set the value
1622        node_id: NodeId,
1623    },
1624}
1625
1626impl CrdtOperation {
1627    /// Get the approximate size in bytes
1628    pub fn size(&self) -> usize {
1629        match self {
1630            CrdtOperation::UpdatePosition { position, .. } => 4 + 8 + position.encode().len(),
1631            CrdtOperation::UpdateHealth { status, .. } => 4 + 8 + status.encode().len(),
1632            CrdtOperation::IncrementCounter { .. } => 1 + 4 + 8,
1633            CrdtOperation::UpdateRegister { key, value, .. } => 4 + 8 + key.len() + value.len(),
1634        }
1635    }
1636
1637    /// Encode to bytes
1638    pub fn encode(&self) -> Vec<u8> {
1639        let mut buf = Vec::new();
1640        match self {
1641            CrdtOperation::UpdatePosition {
1642                node_id,
1643                position,
1644                timestamp,
1645            } => {
1646                buf.push(0x01); // Type tag
1647                buf.extend_from_slice(&node_id.as_u32().to_le_bytes());
1648                buf.extend_from_slice(&timestamp.to_le_bytes());
1649                buf.extend_from_slice(&position.encode());
1650            }
1651            CrdtOperation::UpdateHealth {
1652                node_id,
1653                status,
1654                timestamp,
1655            } => {
1656                buf.push(0x02); // Type tag
1657                buf.extend_from_slice(&node_id.as_u32().to_le_bytes());
1658                buf.extend_from_slice(&timestamp.to_le_bytes());
1659                buf.extend_from_slice(&status.encode());
1660            }
1661            CrdtOperation::IncrementCounter {
1662                counter_id,
1663                node_id,
1664                amount,
1665            } => {
1666                buf.push(0x03); // Type tag
1667                buf.push(*counter_id);
1668                buf.extend_from_slice(&node_id.as_u32().to_le_bytes());
1669                buf.extend_from_slice(&amount.to_le_bytes());
1670            }
1671            CrdtOperation::UpdateRegister {
1672                key,
1673                value,
1674                timestamp,
1675                node_id,
1676            } => {
1677                buf.push(0x04); // Type tag
1678                buf.extend_from_slice(&node_id.as_u32().to_le_bytes());
1679                buf.extend_from_slice(&timestamp.to_le_bytes());
1680                buf.push(key.len() as u8);
1681                buf.extend_from_slice(key.as_bytes());
1682                buf.extend_from_slice(&(value.len() as u16).to_le_bytes());
1683                buf.extend_from_slice(value);
1684            }
1685        }
1686        buf
1687    }
1688
1689    /// Decode from bytes
1690    pub fn decode(data: &[u8]) -> Option<Self> {
1691        if data.is_empty() {
1692            return None;
1693        }
1694
1695        match data[0] {
1696            0x01 => {
1697                // UpdatePosition
1698                if data.len() < 13 {
1699                    return None;
1700                }
1701                let node_id = NodeId::new(u32::from_le_bytes([data[1], data[2], data[3], data[4]]));
1702                let timestamp = u64::from_le_bytes([
1703                    data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12],
1704                ]);
1705                let position = Position::decode(&data[13..])?;
1706                Some(CrdtOperation::UpdatePosition {
1707                    node_id,
1708                    position,
1709                    timestamp,
1710                })
1711            }
1712            0x02 => {
1713                // UpdateHealth
1714                if data.len() < 13 {
1715                    return None;
1716                }
1717                let node_id = NodeId::new(u32::from_le_bytes([data[1], data[2], data[3], data[4]]));
1718                let timestamp = u64::from_le_bytes([
1719                    data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12],
1720                ]);
1721                let status = HealthStatus::decode(&data[13..])?;
1722                Some(CrdtOperation::UpdateHealth {
1723                    node_id,
1724                    status,
1725                    timestamp,
1726                })
1727            }
1728            0x03 => {
1729                // IncrementCounter
1730                if data.len() < 14 {
1731                    return None;
1732                }
1733                let counter_id = data[1];
1734                let node_id = NodeId::new(u32::from_le_bytes([data[2], data[3], data[4], data[5]]));
1735                let amount = u64::from_le_bytes([
1736                    data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13],
1737                ]);
1738                Some(CrdtOperation::IncrementCounter {
1739                    counter_id,
1740                    node_id,
1741                    amount,
1742                })
1743            }
1744            0x04 => {
1745                // UpdateRegister
1746                if data.len() < 14 {
1747                    return None;
1748                }
1749                let node_id = NodeId::new(u32::from_le_bytes([data[1], data[2], data[3], data[4]]));
1750                let timestamp = u64::from_le_bytes([
1751                    data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12],
1752                ]);
1753                let key_len = data[13] as usize;
1754                if data.len() < 14 + key_len + 2 {
1755                    return None;
1756                }
1757                let key = core::str::from_utf8(&data[14..14 + key_len])
1758                    .ok()?
1759                    .to_string();
1760                let value_len =
1761                    u16::from_le_bytes([data[14 + key_len], data[15 + key_len]]) as usize;
1762                if data.len() < 16 + key_len + value_len {
1763                    return None;
1764                }
1765                let value = data[16 + key_len..16 + key_len + value_len].to_vec();
1766                Some(CrdtOperation::UpdateRegister {
1767                    key,
1768                    value,
1769                    timestamp,
1770                    node_id,
1771                })
1772            }
1773            _ => None,
1774        }
1775    }
1776}
1777
1778#[cfg(test)]
1779mod tests {
1780    use super::*;
1781
1782    // Valid timestamp for tests: 2024-01-15 00:00:00 UTC
1783    // All tests should use this or timestamps derived from it
1784    const TEST_TIMESTAMP: u64 = 1705276800000;
1785
1786    #[test]
1787    fn test_lww_register_basic() {
1788        let mut reg = LwwRegister::new(42u32, 100, NodeId::new(1));
1789        assert_eq!(*reg.get(), 42);
1790        assert_eq!(reg.timestamp(), 100);
1791
1792        // Higher timestamp wins
1793        assert!(reg.set(99, 200, NodeId::new(2)));
1794        assert_eq!(*reg.get(), 99);
1795
1796        // Lower timestamp loses
1797        assert!(!reg.set(50, 150, NodeId::new(3)));
1798        assert_eq!(*reg.get(), 99);
1799    }
1800
1801    #[test]
1802    fn test_lww_register_tiebreak() {
1803        let mut reg = LwwRegister::new(1u32, 100, NodeId::new(1));
1804
1805        // Same timestamp, higher node_id wins
1806        assert!(reg.set(2, 100, NodeId::new(2)));
1807        assert_eq!(*reg.get(), 2);
1808
1809        // Same timestamp, lower node_id loses
1810        assert!(!reg.set(3, 100, NodeId::new(1)));
1811        assert_eq!(*reg.get(), 2);
1812    }
1813
1814    #[test]
1815    fn test_lww_register_merge() {
1816        let mut reg1 = LwwRegister::new(1u32, 100, NodeId::new(1));
1817        let reg2 = LwwRegister::new(2u32, 200, NodeId::new(2));
1818
1819        assert!(reg1.merge(&reg2));
1820        assert_eq!(*reg1.get(), 2);
1821    }
1822
1823    #[test]
1824    fn test_gcounter_basic() {
1825        let mut counter = GCounter::new();
1826        let node1 = NodeId::new(1);
1827        let node2 = NodeId::new(2);
1828
1829        counter.increment(&node1, 5);
1830        counter.increment(&node2, 3);
1831        counter.increment(&node1, 2);
1832
1833        assert_eq!(counter.value(), 10);
1834        assert_eq!(counter.node_count(&node1), 7);
1835        assert_eq!(counter.node_count(&node2), 3);
1836    }
1837
1838    #[test]
1839    fn test_gcounter_merge() {
1840        let mut counter1 = GCounter::new();
1841        let mut counter2 = GCounter::new();
1842        let node1 = NodeId::new(1);
1843        let node2 = NodeId::new(2);
1844
1845        counter1.increment(&node1, 5);
1846        counter2.increment(&node1, 3);
1847        counter2.increment(&node2, 4);
1848
1849        counter1.merge(&counter2);
1850
1851        assert_eq!(counter1.value(), 9); // max(5,3) + 4
1852        assert_eq!(counter1.node_count(&node1), 5);
1853        assert_eq!(counter1.node_count(&node2), 4);
1854    }
1855
1856    #[test]
1857    fn test_gcounter_encode_decode() {
1858        let mut counter = GCounter::new();
1859        counter.increment(&NodeId::new(1), 5);
1860        counter.increment(&NodeId::new(2), 10);
1861
1862        let encoded = counter.encode();
1863        let decoded = GCounter::decode(&encoded).unwrap();
1864
1865        assert_eq!(decoded.value(), counter.value());
1866        assert_eq!(decoded.node_count(&NodeId::new(1)), 5);
1867        assert_eq!(decoded.node_count(&NodeId::new(2)), 10);
1868    }
1869
1870    #[test]
1871    fn test_position_encode_decode() {
1872        let pos = Position::new(37.7749, -122.4194)
1873            .with_altitude(100.0)
1874            .with_accuracy(5.0);
1875
1876        let encoded = pos.encode();
1877        let decoded = Position::decode(&encoded).unwrap();
1878
1879        assert_eq!(decoded.latitude, pos.latitude);
1880        assert_eq!(decoded.longitude, pos.longitude);
1881        assert_eq!(decoded.altitude, pos.altitude);
1882        assert_eq!(decoded.accuracy, pos.accuracy);
1883    }
1884
1885    #[test]
1886    fn test_position_minimal_encode() {
1887        let pos = Position::new(0.0, 0.0);
1888        let encoded = pos.encode();
1889        assert_eq!(encoded.len(), 9); // lat + lon + flags
1890
1891        let pos_with_alt = Position::new(0.0, 0.0).with_altitude(0.0);
1892        let encoded_alt = pos_with_alt.encode();
1893        assert_eq!(encoded_alt.len(), 13);
1894    }
1895
1896    #[test]
1897    fn test_health_status() {
1898        let mut status = HealthStatus::new(85).with_heart_rate(72).with_activity(1);
1899
1900        assert_eq!(status.battery_percent, 85);
1901        assert_eq!(status.heart_rate, Some(72));
1902        assert!(!status.has_alert(HealthStatus::ALERT_MAN_DOWN));
1903
1904        status.set_alert(HealthStatus::ALERT_MAN_DOWN);
1905        assert!(status.has_alert(HealthStatus::ALERT_MAN_DOWN));
1906
1907        let encoded = status.encode();
1908        let decoded = HealthStatus::decode(&encoded).unwrap();
1909        assert_eq!(decoded.battery_percent, 85);
1910        assert_eq!(decoded.heart_rate, Some(72));
1911        assert!(decoded.has_alert(HealthStatus::ALERT_MAN_DOWN));
1912    }
1913
1914    #[test]
1915    fn test_crdt_operation_position() {
1916        let op = CrdtOperation::UpdatePosition {
1917            node_id: NodeId::new(0x1234),
1918            position: Position::new(37.0, -122.0),
1919            timestamp: 1000,
1920        };
1921
1922        let encoded = op.encode();
1923        let decoded = CrdtOperation::decode(&encoded).unwrap();
1924
1925        if let CrdtOperation::UpdatePosition {
1926            node_id,
1927            position,
1928            timestamp,
1929        } = decoded
1930        {
1931            assert_eq!(node_id.as_u32(), 0x1234);
1932            assert_eq!(timestamp, 1000);
1933            assert_eq!(position.latitude, 37.0);
1934        } else {
1935            panic!("Wrong operation type");
1936        }
1937    }
1938
1939    #[test]
1940    fn test_crdt_operation_counter() {
1941        let op = CrdtOperation::IncrementCounter {
1942            counter_id: 1,
1943            node_id: NodeId::new(0x5678),
1944            amount: 42,
1945        };
1946
1947        let encoded = op.encode();
1948        let decoded = CrdtOperation::decode(&encoded).unwrap();
1949
1950        if let CrdtOperation::IncrementCounter {
1951            counter_id,
1952            node_id,
1953            amount,
1954        } = decoded
1955        {
1956            assert_eq!(counter_id, 1);
1957            assert_eq!(node_id.as_u32(), 0x5678);
1958            assert_eq!(amount, 42);
1959        } else {
1960            panic!("Wrong operation type");
1961        }
1962    }
1963
1964    #[test]
1965    fn test_crdt_operation_size() {
1966        let pos_op = CrdtOperation::UpdatePosition {
1967            node_id: NodeId::new(1),
1968            position: Position::new(0.0, 0.0),
1969            timestamp: 0,
1970        };
1971        assert!(pos_op.size() > 0);
1972
1973        let counter_op = CrdtOperation::IncrementCounter {
1974            counter_id: 0,
1975            node_id: NodeId::new(1),
1976            amount: 1,
1977        };
1978        assert_eq!(counter_op.size(), 13);
1979    }
1980
1981    // ============================================================================
1982    // Peripheral Tests
1983    // ============================================================================
1984
1985    #[test]
1986    fn test_peripheral_type_from_u8() {
1987        assert_eq!(PeripheralType::from_u8(0), PeripheralType::Unknown);
1988        assert_eq!(PeripheralType::from_u8(1), PeripheralType::SoldierSensor);
1989        assert_eq!(PeripheralType::from_u8(2), PeripheralType::FixedSensor);
1990        assert_eq!(PeripheralType::from_u8(3), PeripheralType::Relay);
1991        assert_eq!(PeripheralType::from_u8(99), PeripheralType::Unknown);
1992    }
1993
1994    #[test]
1995    fn test_event_type_from_u8() {
1996        assert_eq!(EventType::from_u8(0), EventType::None);
1997        assert_eq!(EventType::from_u8(1), EventType::Ping);
1998        assert_eq!(EventType::from_u8(2), EventType::NeedAssist);
1999        assert_eq!(EventType::from_u8(3), EventType::Emergency);
2000        assert_eq!(EventType::from_u8(4), EventType::Moving);
2001        assert_eq!(EventType::from_u8(5), EventType::InPosition);
2002        assert_eq!(EventType::from_u8(6), EventType::Ack);
2003        assert_eq!(EventType::from_u8(99), EventType::None);
2004    }
2005
2006    #[test]
2007    fn test_event_type_labels() {
2008        assert_eq!(EventType::None.label(), "");
2009        assert_eq!(EventType::Emergency.label(), "EMERGENCY");
2010        assert_eq!(EventType::Ping.label(), "PING");
2011    }
2012
2013    #[test]
2014    fn test_peripheral_event_encode_decode() {
2015        let event = PeripheralEvent::new(EventType::Emergency, TEST_TIMESTAMP);
2016        let encoded = event.encode();
2017        assert_eq!(encoded.len(), 9);
2018
2019        let decoded = PeripheralEvent::decode(&encoded).unwrap();
2020        assert_eq!(decoded.event_type, EventType::Emergency);
2021        assert_eq!(decoded.timestamp, TEST_TIMESTAMP);
2022    }
2023
2024    #[test]
2025    fn test_peripheral_new() {
2026        let peripheral = Peripheral::new(0x12345678, PeripheralType::SoldierSensor);
2027        assert_eq!(peripheral.id, 0x12345678);
2028        assert_eq!(peripheral.peripheral_type, PeripheralType::SoldierSensor);
2029        assert_eq!(peripheral.parent_node, 0);
2030        assert!(peripheral.last_event.is_none());
2031    }
2032
2033    #[test]
2034    fn test_peripheral_with_callsign() {
2035        let peripheral = Peripheral::new(1, PeripheralType::SoldierSensor).with_callsign("ALPHA-1");
2036        assert_eq!(peripheral.callsign_str(), "ALPHA-1");
2037
2038        // Test truncation
2039        let peripheral2 = Peripheral::new(2, PeripheralType::SoldierSensor)
2040            .with_callsign("THIS_IS_A_VERY_LONG_CALLSIGN");
2041        assert_eq!(peripheral2.callsign_str(), "THIS_IS_A_VE");
2042    }
2043
2044    #[test]
2045    fn test_peripheral_set_event() {
2046        let mut peripheral = Peripheral::new(1, PeripheralType::SoldierSensor);
2047        peripheral.set_event(EventType::Emergency, TEST_TIMESTAMP);
2048
2049        assert!(peripheral.last_event.is_some());
2050        let event = peripheral.last_event.as_ref().unwrap();
2051        assert_eq!(event.event_type, EventType::Emergency);
2052        assert_eq!(event.timestamp, TEST_TIMESTAMP);
2053        assert_eq!(peripheral.timestamp, TEST_TIMESTAMP);
2054
2055        peripheral.clear_event();
2056        assert!(peripheral.last_event.is_none());
2057    }
2058
2059    #[test]
2060    fn test_peripheral_encode_decode_without_event() {
2061        let peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor)
2062            .with_callsign("BRAVO-2")
2063            .with_parent(0x11223344);
2064
2065        let encoded = peripheral.encode();
2066        assert_eq!(encoded.len(), 35); // No event + 1 byte has_location flag
2067
2068        let decoded = Peripheral::decode(&encoded).unwrap();
2069        assert_eq!(decoded.id, 0xAABBCCDD);
2070        assert_eq!(decoded.parent_node, 0x11223344);
2071        assert_eq!(decoded.peripheral_type, PeripheralType::SoldierSensor);
2072        assert_eq!(decoded.callsign_str(), "BRAVO-2");
2073        assert!(decoded.last_event.is_none());
2074        assert!(decoded.location.is_none());
2075    }
2076
2077    #[test]
2078    fn test_peripheral_encode_decode_with_event() {
2079        let mut peripheral = Peripheral::new(0x12345678, PeripheralType::SoldierSensor)
2080            .with_callsign("CHARLIE")
2081            .with_parent(0x87654321);
2082        peripheral.health = HealthStatus::new(85);
2083        peripheral.set_event(EventType::NeedAssist, TEST_TIMESTAMP);
2084
2085        let encoded = peripheral.encode();
2086        assert_eq!(encoded.len(), 44); // With event + 1 byte has_location flag
2087
2088        let decoded = Peripheral::decode(&encoded).unwrap();
2089        assert_eq!(decoded.id, 0x12345678);
2090        assert_eq!(decoded.parent_node, 0x87654321);
2091        assert_eq!(decoded.callsign_str(), "CHARLIE");
2092        assert_eq!(decoded.health.battery_percent, 85);
2093        assert!(decoded.last_event.is_some());
2094        let event = decoded.last_event.as_ref().unwrap();
2095        assert_eq!(event.event_type, EventType::NeedAssist);
2096        assert_eq!(event.timestamp, TEST_TIMESTAMP);
2097        assert!(decoded.location.is_none());
2098    }
2099
2100    #[test]
2101    fn test_peripheral_encode_decode_with_location() {
2102        let location = Position::new(37.7749, -122.4194).with_altitude(10.0);
2103        let peripheral = Peripheral::new(0x12345678, PeripheralType::SoldierSensor)
2104            .with_callsign("DELTA")
2105            .with_location(location);
2106
2107        let encoded = peripheral.encode();
2108        // 35 base + 13 location bytes (lat:4 + lon:4 + flags:1 + alt:4)
2109        assert_eq!(encoded.len(), 48);
2110
2111        let decoded = Peripheral::decode(&encoded).unwrap();
2112        assert_eq!(decoded.id, 0x12345678);
2113        assert_eq!(decoded.callsign_str(), "DELTA");
2114        assert!(decoded.location.is_some());
2115
2116        let loc = decoded.location.unwrap();
2117        assert!((loc.latitude - 37.7749).abs() < 0.0001);
2118        assert!((loc.longitude - (-122.4194)).abs() < 0.0001);
2119        assert!(loc.altitude.is_some());
2120        assert!((loc.altitude.unwrap() - 10.0).abs() < 1.0);
2121    }
2122
2123    #[test]
2124    fn test_peripheral_decode_invalid_data() {
2125        // Too short
2126        assert!(Peripheral::decode(&[0u8; 10]).is_none());
2127
2128        // Valid length but id=0 is rejected
2129        let mut data = vec![0u8; 34];
2130        data[25] = 0; // no event flag
2131        assert!(Peripheral::decode(&data).is_none()); // id=0 rejected
2132
2133        // Valid id but no event - should succeed
2134        data[0..4].copy_from_slice(&1u32.to_le_bytes()); // id=1
2135        assert!(Peripheral::decode(&data).is_some());
2136
2137        // Claims to have event but too short
2138        data[25] = 1; // has event flag
2139        assert!(Peripheral::decode(&data).is_none());
2140    }
2141
2142    // ============================================================================
2143    // EmergencyEvent Tests
2144    // ============================================================================
2145
2146    #[test]
2147    fn test_emergency_event_new() {
2148        let peers = vec![0x22222222, 0x33333333];
2149        let event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2150
2151        assert_eq!(event.source_node(), 0x11111111);
2152        assert_eq!(event.timestamp(), TEST_TIMESTAMP);
2153        assert_eq!(event.peer_count(), 3); // source + 2 peers
2154
2155        // Source is auto-acked
2156        assert!(event.has_acked(0x11111111));
2157        // Others are not
2158        assert!(!event.has_acked(0x22222222));
2159        assert!(!event.has_acked(0x33333333));
2160    }
2161
2162    #[test]
2163    fn test_emergency_event_ack() {
2164        let peers = vec![0x22222222, 0x33333333];
2165        let mut event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2166
2167        assert_eq!(event.ack_count(), 1); // just source
2168        assert!(!event.all_acked());
2169
2170        // ACK from first peer
2171        assert!(event.ack(0x22222222)); // returns true - new ack
2172        assert_eq!(event.ack_count(), 2);
2173        assert!(!event.all_acked());
2174
2175        // Duplicate ACK
2176        assert!(!event.ack(0x22222222)); // returns false - already acked
2177
2178        // ACK from second peer
2179        assert!(event.ack(0x33333333));
2180        assert_eq!(event.ack_count(), 3);
2181        assert!(event.all_acked());
2182    }
2183
2184    #[test]
2185    fn test_emergency_event_pending_nodes() {
2186        let peers = vec![0x22222222, 0x33333333];
2187        let mut event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2188
2189        let pending = event.pending_nodes();
2190        assert_eq!(pending.len(), 2);
2191        assert!(pending.contains(&0x22222222));
2192        assert!(pending.contains(&0x33333333));
2193
2194        event.ack(0x22222222);
2195        let pending = event.pending_nodes();
2196        assert_eq!(pending.len(), 1);
2197        assert!(pending.contains(&0x33333333));
2198    }
2199
2200    #[test]
2201    fn test_emergency_event_encode_decode() {
2202        let peers = vec![0x22222222, 0x33333333];
2203        let mut event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2204        event.ack(0x22222222);
2205
2206        let encoded = event.encode();
2207        let decoded = EmergencyEvent::decode(&encoded).unwrap();
2208
2209        assert_eq!(decoded.source_node(), 0x11111111);
2210        assert_eq!(decoded.timestamp(), TEST_TIMESTAMP);
2211        assert!(decoded.has_acked(0x11111111));
2212        assert!(decoded.has_acked(0x22222222));
2213        assert!(!decoded.has_acked(0x33333333));
2214    }
2215
2216    #[test]
2217    fn test_emergency_event_merge_same_event() {
2218        // Two nodes have the same emergency, different ack states
2219        let peers = vec![0x22222222, 0x33333333];
2220        let mut event1 = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2221        let mut event2 = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2222
2223        event1.ack(0x22222222);
2224        event2.ack(0x33333333);
2225
2226        // Merge event2 into event1
2227        let changed = event1.merge(&event2);
2228        assert!(changed);
2229        assert!(event1.has_acked(0x22222222));
2230        assert!(event1.has_acked(0x33333333));
2231        assert!(event1.all_acked());
2232    }
2233
2234    #[test]
2235    fn test_emergency_event_merge_different_events() {
2236        // Old emergency
2237        let mut old_event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &[0x22222222]);
2238        old_event.ack(0x22222222);
2239
2240        // New emergency from different source
2241        let new_event =
2242            EmergencyEvent::new(0x33333333, TEST_TIMESTAMP + 1000, &[0x11111111, 0x22222222]);
2243
2244        // Merge new into old - should replace
2245        let changed = old_event.merge(&new_event);
2246        assert!(changed);
2247        assert_eq!(old_event.source_node(), 0x33333333);
2248        assert_eq!(old_event.timestamp(), TEST_TIMESTAMP + 1000);
2249        // Old ack state should be gone
2250        assert!(!old_event.has_acked(0x22222222));
2251    }
2252
2253    #[test]
2254    fn test_emergency_event_merge_older_event_ignored() {
2255        // Current emergency
2256        let mut current = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP + 1000, &[0x22222222]);
2257
2258        // Older emergency
2259        let older = EmergencyEvent::new(0x33333333, TEST_TIMESTAMP, &[0x11111111]);
2260
2261        // Merge older into current - should NOT replace
2262        let changed = current.merge(&older);
2263        assert!(!changed);
2264        assert_eq!(current.source_node(), 0x11111111);
2265        assert_eq!(current.timestamp(), TEST_TIMESTAMP + 1000);
2266    }
2267
2268    #[test]
2269    fn test_emergency_event_add_peer() {
2270        let mut event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &[]);
2271
2272        // Add a peer discovered after emergency started
2273        event.add_peer(0x22222222);
2274        assert!(!event.has_acked(0x22222222));
2275        assert_eq!(event.peer_count(), 2);
2276
2277        // Adding same peer again doesn't change ack status
2278        event.ack(0x22222222);
2279        event.add_peer(0x22222222);
2280        assert!(event.has_acked(0x22222222)); // still acked
2281    }
2282
2283    #[test]
2284    fn test_emergency_event_decode_invalid() {
2285        // Too short
2286        assert!(EmergencyEvent::decode(&[0u8; 10]).is_none());
2287
2288        // Valid header but claims more acks than data
2289        let mut data = vec![0u8; 16];
2290        data[12] = 5; // claims 5 ack entries
2291        assert!(EmergencyEvent::decode(&data).is_none());
2292    }
2293
2294    // ============================================================================
2295    // ChatMessage Tests (gated behind legacy-chat)
2296    // ============================================================================
2297
2298    #[cfg(feature = "legacy-chat")]
2299    mod chat_tests {
2300        use super::*;
2301
2302        #[test]
2303        fn test_chat_message_new() {
2304            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "ALPHA-1", "Hello mesh!");
2305            assert_eq!(msg.origin_node, 0x12345678);
2306            assert_eq!(msg.timestamp, TEST_TIMESTAMP);
2307            assert_eq!(msg.sender(), "ALPHA-1");
2308            assert_eq!(msg.text(), "Hello mesh!");
2309            assert!(msg.is_broadcast);
2310            assert!(!msg.requires_ack);
2311            assert!(!msg.is_reply());
2312        }
2313
2314        #[test]
2315        fn test_chat_message_reply_to() {
2316            let mut msg =
2317                ChatMessage::new(0x12345678, TEST_TIMESTAMP + 1000, "BRAVO", "Roger that");
2318            msg.set_reply_to(0xAABBCCDD, TEST_TIMESTAMP);
2319
2320            assert!(msg.is_reply());
2321            assert_eq!(msg.reply_to_node, 0xAABBCCDD);
2322            assert_eq!(msg.reply_to_timestamp, TEST_TIMESTAMP);
2323        }
2324
2325        #[test]
2326        fn test_chat_message_truncation() {
2327            // Test sender truncation (max 12 bytes)
2328            let msg = ChatMessage::new(0x1, TEST_TIMESTAMP, "VERY_LONG_CALLSIGN", "Hi");
2329            assert_eq!(msg.sender(), "VERY_LONG_CA"); // 12 chars
2330
2331            // Test text truncation (max 128 bytes)
2332            let long_text = "A".repeat(200);
2333            let msg = ChatMessage::new(0x1, TEST_TIMESTAMP, "X", &long_text);
2334            assert_eq!(msg.text().len(), 128);
2335        }
2336
2337        #[test]
2338        fn test_chat_message_id() {
2339            let msg = ChatMessage::new(0x12345678, 0x18D4A51_ABCDEF01, "X", "Y");
2340            let id = msg.message_id();
2341            // ID = (origin << 32) | (timestamp & 0xFFFFFFFF)
2342            assert_eq!(id, (0x12345678u64 << 32) | 0xABCDEF01);
2343        }
2344
2345        #[test]
2346        fn test_chat_message_encode_decode() {
2347            let mut msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "CHARLIE", "Test message");
2348            msg.is_broadcast = true;
2349            msg.requires_ack = true;
2350            msg.set_reply_to(0xAABBCCDD, TEST_TIMESTAMP - 1000);
2351
2352            let encoded = msg.encode();
2353            let (decoded, len) = ChatMessage::decode(&encoded).unwrap();
2354
2355            assert_eq!(len, encoded.len());
2356            assert_eq!(decoded.origin_node, 0x12345678);
2357            assert_eq!(decoded.timestamp, TEST_TIMESTAMP);
2358            assert_eq!(decoded.sender(), "CHARLIE");
2359            assert_eq!(decoded.text(), "Test message");
2360            assert!(decoded.is_broadcast);
2361            assert!(decoded.requires_ack);
2362            assert_eq!(decoded.reply_to_node, 0xAABBCCDD);
2363            assert_eq!(decoded.reply_to_timestamp, TEST_TIMESTAMP - 1000);
2364        }
2365
2366        #[test]
2367        fn test_chat_message_decode_empty_text() {
2368            // Message with valid sender but empty text (allowed for ACK messages)
2369            let msg = ChatMessage::new(0x1, TEST_TIMESTAMP, "ACK-NODE", "");
2370            let encoded = msg.encode();
2371            let (decoded, _) = ChatMessage::decode(&encoded).unwrap();
2372            assert_eq!(decoded.sender(), "ACK-NODE");
2373            assert_eq!(decoded.text(), "");
2374        }
2375
2376        // ============================================================================
2377        // ChatMessage Validation/Security Tests
2378        // ============================================================================
2379
2380        #[test]
2381        fn test_chat_message_decode_rejects_zero_origin() {
2382            // Build message bytes manually with origin_node = 0
2383            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "TEST", "msg");
2384            let mut encoded = msg.encode();
2385            // Zero out the origin_node (first 4 bytes)
2386            encoded[0] = 0;
2387            encoded[1] = 0;
2388            encoded[2] = 0;
2389            encoded[3] = 0;
2390
2391            assert!(ChatMessage::decode(&encoded).is_none());
2392        }
2393
2394        #[test]
2395        fn test_chat_message_decode_rejects_old_timestamp() {
2396            // Build message bytes manually with timestamp before 2020
2397            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "TEST", "msg");
2398            let mut encoded = msg.encode();
2399            // Set timestamp to 1000 (way before 2020)
2400            let old_ts: u64 = 1000;
2401            encoded[4..12].copy_from_slice(&old_ts.to_le_bytes());
2402
2403            assert!(ChatMessage::decode(&encoded).is_none());
2404        }
2405
2406        #[test]
2407        fn test_chat_message_decode_rejects_empty_sender() {
2408            // Build message bytes manually with sender_len = 0
2409            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "X", "msg");
2410            let mut encoded = msg.encode();
2411            // Set sender_len to 0 at offset 12
2412            encoded[12] = 0;
2413            // Adjust text position (move text_len and text to right after sender_len)
2414            // This is tricky - we need to rebuild the encoding properly
2415            // Actually easier to just build raw bytes:
2416            let mut raw = Vec::new();
2417            raw.extend_from_slice(&0x12345678u32.to_le_bytes()); // origin_node
2418            raw.extend_from_slice(&TEST_TIMESTAMP.to_le_bytes()); // timestamp
2419            raw.push(0); // sender_len = 0 (INVALID)
2420            raw.push(3); // text_len = 3
2421            raw.extend_from_slice(b"msg"); // text
2422            raw.push(0x01); // flags
2423
2424            assert!(ChatMessage::decode(&raw).is_none());
2425        }
2426
2427        #[test]
2428        fn test_chat_message_decode_rejects_invalid_utf8_sender() {
2429            // Build message with invalid UTF-8 in sender
2430            let mut raw = Vec::new();
2431            raw.extend_from_slice(&0x12345678u32.to_le_bytes()); // origin_node
2432            raw.extend_from_slice(&TEST_TIMESTAMP.to_le_bytes()); // timestamp
2433            raw.push(4); // sender_len = 4
2434            raw.extend_from_slice(&[0x66, 0x59, 0xFF, 0xFE]); // "fY" + invalid UTF-8
2435            raw.push(3); // text_len = 3
2436            raw.extend_from_slice(b"msg"); // text
2437            raw.push(0x01); // flags
2438            raw.extend_from_slice(&0u32.to_le_bytes()); // reply_to_node
2439            raw.extend_from_slice(&0u64.to_le_bytes()); // reply_to_timestamp
2440
2441            assert!(ChatMessage::decode(&raw).is_none());
2442        }
2443
2444        #[test]
2445        fn test_chat_message_decode_rejects_invalid_utf8_text() {
2446            // Build message with invalid UTF-8 in text
2447            let mut raw = Vec::new();
2448            raw.extend_from_slice(&0x12345678u32.to_le_bytes()); // origin_node
2449            raw.extend_from_slice(&TEST_TIMESTAMP.to_le_bytes()); // timestamp
2450            raw.push(4); // sender_len = 4
2451            raw.extend_from_slice(b"TEST"); // valid sender
2452            raw.push(4); // text_len = 4
2453            raw.extend_from_slice(&[0x80, 0x81, 0x82, 0x83]); // invalid UTF-8
2454            raw.push(0x01); // flags
2455            raw.extend_from_slice(&0u32.to_le_bytes()); // reply_to_node
2456            raw.extend_from_slice(&0u64.to_le_bytes()); // reply_to_timestamp
2457
2458            assert!(ChatMessage::decode(&raw).is_none());
2459        }
2460
2461        #[test]
2462        fn test_chat_message_decode_accepts_valid_utf8() {
2463            // Build message with valid UTF-8 including unicode
2464            let mut raw = Vec::new();
2465            raw.extend_from_slice(&0x12345678u32.to_le_bytes()); // origin_node
2466            raw.extend_from_slice(&TEST_TIMESTAMP.to_le_bytes()); // timestamp
2467            raw.push(6); // sender_len = 6 (UTF-8 encoded)
2468            raw.extend_from_slice("TËST".as_bytes()); // valid UTF-8 with umlaut (5 bytes: T, Ë=2bytes, S, T)
2469                                                      // Wait, "TËST" is 5 bytes not 6. Let me fix:
2470            let sender_bytes = "TËST1".as_bytes(); // T(1) + Ë(2) + S(1) + T(1) + 1(1) = 6 bytes
2471            raw[12] = sender_bytes.len() as u8;
2472            raw.truncate(13);
2473            raw.extend_from_slice(sender_bytes);
2474            raw.push(4); // text_len = 4
2475            raw.extend_from_slice(b"test"); // text
2476            raw.push(0x01); // flags
2477            raw.extend_from_slice(&0u32.to_le_bytes()); // reply_to_node
2478            raw.extend_from_slice(&0u64.to_le_bytes()); // reply_to_timestamp
2479
2480            let result = ChatMessage::decode(&raw);
2481            assert!(result.is_some());
2482            let (msg, _) = result.unwrap();
2483            assert_eq!(msg.sender(), "TËST1");
2484        }
2485
2486        // ============================================================================
2487        // ChatCRDT Tests
2488        // ============================================================================
2489
2490        #[test]
2491        fn test_chat_crdt_new() {
2492            let chat = ChatCRDT::new();
2493            assert!(chat.is_empty());
2494            assert_eq!(chat.len(), 0);
2495        }
2496
2497        #[test]
2498        fn test_chat_crdt_add_message() {
2499            let mut chat = ChatCRDT::new();
2500
2501            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "ALPHA", "Hello");
2502            assert!(chat.add_message(msg.clone()));
2503            assert_eq!(chat.len(), 1);
2504
2505            // Duplicate should be rejected
2506            assert!(!chat.add_message(msg));
2507            assert_eq!(chat.len(), 1);
2508        }
2509
2510        #[test]
2511        fn test_chat_crdt_send_message() {
2512            let mut chat = ChatCRDT::new();
2513
2514            assert!(chat.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "First"));
2515            assert!(chat.send_message(0x2, TEST_TIMESTAMP + 1, "BRAVO", "Second"));
2516            assert_eq!(chat.len(), 2);
2517
2518            // Same node, same timestamp = duplicate
2519            assert!(!chat.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "Duplicate"));
2520            assert_eq!(chat.len(), 2);
2521        }
2522
2523        #[test]
2524        fn test_chat_crdt_get_message() {
2525            let mut chat = ChatCRDT::new();
2526            chat.send_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "Test");
2527
2528            let msg = chat.get_message(0x12345678, TEST_TIMESTAMP);
2529            assert!(msg.is_some());
2530            assert_eq!(msg.unwrap().text(), "Test");
2531
2532            // Non-existent message
2533            assert!(chat.get_message(0x99999999, TEST_TIMESTAMP).is_none());
2534        }
2535
2536        #[test]
2537        fn test_chat_crdt_merge() {
2538            let mut chat1 = ChatCRDT::new();
2539            let mut chat2 = ChatCRDT::new();
2540
2541            chat1.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "From 1");
2542            chat2.send_message(0x2, TEST_TIMESTAMP + 1, "BRAVO", "From 2");
2543
2544            // Merge chat2 into chat1
2545            let changed = chat1.merge(&chat2);
2546            assert!(changed);
2547            assert_eq!(chat1.len(), 2);
2548
2549            // Merge again - no changes
2550            let changed = chat1.merge(&chat2);
2551            assert!(!changed);
2552            assert_eq!(chat1.len(), 2);
2553        }
2554
2555        #[test]
2556        fn test_chat_crdt_merge_duplicates() {
2557            let mut chat1 = ChatCRDT::new();
2558            let mut chat2 = ChatCRDT::new();
2559
2560            // Both have the same message
2561            chat1.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "Same message");
2562            chat2.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "Same message");
2563
2564            // Merge should not create duplicates
2565            chat1.merge(&chat2);
2566            assert_eq!(chat1.len(), 1);
2567        }
2568
2569        #[test]
2570        fn test_chat_crdt_pruning() {
2571            let mut chat = ChatCRDT::new();
2572
2573            // Add more than CHAT_MAX_MESSAGES with valid timestamps
2574            for i in 0..(CHAT_MAX_MESSAGES + 10) {
2575                chat.send_message(i as u32 + 1, TEST_TIMESTAMP + i as u64, "X", "Y");
2576            }
2577
2578            // Should be pruned to max
2579            assert_eq!(chat.len(), CHAT_MAX_MESSAGES);
2580
2581            // Oldest messages should be removed (first 10)
2582            // Message IDs are (node << 32) | (timestamp & 0xFFFFFFFF)
2583            // node=1, ts=TEST_TIMESTAMP has lowest ID, etc.
2584            assert!(chat.get_message(1, TEST_TIMESTAMP).is_none());
2585            assert!(chat.get_message(10, TEST_TIMESTAMP + 9).is_none());
2586            // Newer messages should remain
2587            assert!(chat.get_message(11, TEST_TIMESTAMP + 10).is_some());
2588        }
2589
2590        #[test]
2591        fn test_chat_crdt_encode_decode() {
2592            let mut chat = ChatCRDT::new();
2593            chat.send_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "First message");
2594            chat.send_message(0xAABBCCDD, TEST_TIMESTAMP + 1000, "BRAVO", "Second message");
2595
2596            let encoded = chat.encode();
2597            let decoded = ChatCRDT::decode(&encoded).unwrap();
2598
2599            assert_eq!(decoded.len(), 2);
2600            assert!(decoded.get_message(0x12345678, TEST_TIMESTAMP).is_some());
2601            assert!(decoded
2602                .get_message(0xAABBCCDD, TEST_TIMESTAMP + 1000)
2603                .is_some());
2604        }
2605
2606        #[test]
2607        fn test_chat_crdt_messages_since() {
2608            let mut chat = ChatCRDT::new();
2609            chat.send_message(0x1, TEST_TIMESTAMP, "A", "Old");
2610            chat.send_message(0x2, TEST_TIMESTAMP + 1000, "B", "Mid");
2611            chat.send_message(0x3, TEST_TIMESTAMP + 2000, "C", "New");
2612
2613            let recent: Vec<_> = chat.messages_since(TEST_TIMESTAMP + 500).collect();
2614            assert_eq!(recent.len(), 2);
2615        }
2616
2617        #[test]
2618        fn test_chat_crdt_newest_timestamp() {
2619            let mut chat = ChatCRDT::new();
2620            assert!(chat.newest_timestamp().is_none());
2621
2622            chat.send_message(0x1, TEST_TIMESTAMP, "A", "1");
2623            assert_eq!(chat.newest_timestamp(), Some(TEST_TIMESTAMP));
2624
2625            chat.send_message(0x2, TEST_TIMESTAMP + 2000, "B", "2");
2626            assert_eq!(chat.newest_timestamp(), Some(TEST_TIMESTAMP + 2000));
2627
2628            chat.send_message(0x3, TEST_TIMESTAMP + 1000, "C", "3"); // older timestamp
2629            assert_eq!(chat.newest_timestamp(), Some(TEST_TIMESTAMP + 2000));
2630        }
2631
2632        // ============================================================================
2633        // ChatCRDT Security/Validation Tests
2634        // ============================================================================
2635
2636        #[test]
2637        fn test_chat_crdt_decode_skips_invalid_messages() {
2638            // Build a CRDT with 2 messages, one valid and one invalid
2639            let mut valid_chat = ChatCRDT::new();
2640            valid_chat.send_message(0x12345678, TEST_TIMESTAMP, "VALID", "Good message");
2641
2642            let encoded = valid_chat.encode();
2643
2644            // Decode should work and contain the valid message
2645            let decoded = ChatCRDT::decode(&encoded).unwrap();
2646            assert_eq!(decoded.len(), 1);
2647            assert!(decoded.get_message(0x12345678, TEST_TIMESTAMP).is_some());
2648        }
2649
2650        #[test]
2651        fn test_chat_crdt_decode_handles_truncated_data() {
2652            let mut chat = ChatCRDT::new();
2653            chat.send_message(0x12345678, TEST_TIMESTAMP, "TEST", "Message");
2654
2655            let encoded = chat.encode();
2656
2657            // Truncate to just the message count
2658            let truncated = &encoded[..2];
2659            let decoded = ChatCRDT::decode(truncated);
2660            assert!(decoded.is_some());
2661            assert_eq!(decoded.unwrap().len(), 0); // No messages decoded
2662        }
2663    } // mod chat_tests
2664}