hive_btle/gatt/
characteristics.rs

1//! HIVE GATT Characteristic Definitions
2//!
3//! Defines the characteristics exposed by the HIVE GATT service.
4
5#[cfg(not(feature = "std"))]
6use alloc::{borrow::ToOwned, vec::Vec};
7
8use uuid::Uuid;
9
10use crate::{
11    HierarchyLevel, NodeId, CHAR_COMMAND_UUID, CHAR_NODE_INFO_UUID, CHAR_STATUS_UUID,
12    CHAR_SYNC_DATA_UUID, CHAR_SYNC_STATE_UUID, HIVE_SERVICE_UUID,
13};
14
15/// Characteristic properties bitfield
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct CharacteristicProperties(u8);
18
19impl CharacteristicProperties {
20    /// Characteristic supports reading
21    pub const READ: u8 = 0x02;
22    /// Characteristic supports writing without response
23    pub const WRITE_WITHOUT_RESPONSE: u8 = 0x04;
24    /// Characteristic supports writing with response
25    pub const WRITE: u8 = 0x08;
26    /// Characteristic supports notifications
27    pub const NOTIFY: u8 = 0x10;
28    /// Characteristic supports indications
29    pub const INDICATE: u8 = 0x20;
30
31    /// Create new properties
32    pub const fn new(flags: u8) -> Self {
33        Self(flags)
34    }
35
36    /// Check if read is supported
37    pub fn can_read(&self) -> bool {
38        self.0 & Self::READ != 0
39    }
40
41    /// Check if write is supported
42    pub fn can_write(&self) -> bool {
43        self.0 & Self::WRITE != 0
44    }
45
46    /// Check if notifications are supported
47    pub fn can_notify(&self) -> bool {
48        self.0 & Self::NOTIFY != 0
49    }
50
51    /// Check if indications are supported
52    pub fn can_indicate(&self) -> bool {
53        self.0 & Self::INDICATE != 0
54    }
55
56    /// Get raw flags
57    pub fn flags(&self) -> u8 {
58        self.0
59    }
60}
61
62/// HIVE characteristic UUIDs derived from base service UUID
63pub struct HiveCharacteristicUuids;
64
65impl HiveCharacteristicUuids {
66    /// Get Node Info characteristic UUID
67    pub fn node_info() -> Uuid {
68        Self::derive_uuid(CHAR_NODE_INFO_UUID)
69    }
70
71    /// Get Sync State characteristic UUID
72    pub fn sync_state() -> Uuid {
73        Self::derive_uuid(CHAR_SYNC_STATE_UUID)
74    }
75
76    /// Get Sync Data characteristic UUID
77    pub fn sync_data() -> Uuid {
78        Self::derive_uuid(CHAR_SYNC_DATA_UUID)
79    }
80
81    /// Get Command characteristic UUID
82    pub fn command() -> Uuid {
83        Self::derive_uuid(CHAR_COMMAND_UUID)
84    }
85
86    /// Get Status characteristic UUID
87    pub fn status() -> Uuid {
88        Self::derive_uuid(CHAR_STATUS_UUID)
89    }
90
91    /// Derive a characteristic UUID from the base service UUID
92    ///
93    /// Uses the standard BLE approach of modifying the 3rd and 4th bytes
94    /// of the base UUID with the 16-bit characteristic ID.
95    fn derive_uuid(char_id: u16) -> Uuid {
96        let mut bytes = HIVE_SERVICE_UUID.as_bytes().to_owned();
97        bytes[2] = (char_id >> 8) as u8;
98        bytes[3] = char_id as u8;
99        Uuid::from_bytes(bytes)
100    }
101}
102
103/// Node Info characteristic data
104///
105/// Read-only characteristic containing basic node information.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct NodeInfo {
108    /// Node identifier
109    pub node_id: NodeId,
110    /// Protocol version
111    pub protocol_version: u8,
112    /// Hierarchy level
113    pub hierarchy_level: HierarchyLevel,
114    /// Capability flags
115    pub capabilities: u16,
116    /// Battery percentage (0-100, 255 = unknown)
117    pub battery_percent: u8,
118}
119
120impl NodeInfo {
121    /// Encoded size in bytes
122    pub const ENCODED_SIZE: usize = 9;
123
124    /// Create new node info
125    pub fn new(node_id: NodeId, hierarchy_level: HierarchyLevel, capabilities: u16) -> Self {
126        Self {
127            node_id,
128            protocol_version: 1,
129            hierarchy_level,
130            capabilities,
131            battery_percent: 255,
132        }
133    }
134
135    /// Encode to bytes
136    pub fn encode(&self) -> [u8; Self::ENCODED_SIZE] {
137        let mut buf = [0u8; Self::ENCODED_SIZE];
138        let node_id = self.node_id.as_u32();
139
140        buf[0] = (node_id >> 24) as u8;
141        buf[1] = (node_id >> 16) as u8;
142        buf[2] = (node_id >> 8) as u8;
143        buf[3] = node_id as u8;
144        buf[4] = self.protocol_version;
145        buf[5] = self.hierarchy_level.into();
146        buf[6] = (self.capabilities >> 8) as u8;
147        buf[7] = self.capabilities as u8;
148        buf[8] = self.battery_percent;
149
150        buf
151    }
152
153    /// Decode from bytes
154    pub fn decode(data: &[u8]) -> Option<Self> {
155        if data.len() < Self::ENCODED_SIZE {
156            return None;
157        }
158
159        let node_id = NodeId::new(
160            ((data[0] as u32) << 24)
161                | ((data[1] as u32) << 16)
162                | ((data[2] as u32) << 8)
163                | (data[3] as u32),
164        );
165        let protocol_version = data[4];
166        let hierarchy_level = HierarchyLevel::from(data[5]);
167        let capabilities = ((data[6] as u16) << 8) | (data[7] as u16);
168        let battery_percent = data[8];
169
170        Some(Self {
171            node_id,
172            protocol_version,
173            hierarchy_level,
174            capabilities,
175            battery_percent,
176        })
177    }
178}
179
180/// Sync state values
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
182#[repr(u8)]
183pub enum SyncState {
184    /// Not syncing
185    #[default]
186    Idle = 0,
187    /// Sync in progress
188    Syncing = 1,
189    /// Sync complete
190    Complete = 2,
191    /// Sync error
192    Error = 3,
193}
194
195impl From<u8> for SyncState {
196    fn from(value: u8) -> Self {
197        match value {
198            0 => SyncState::Idle,
199            1 => SyncState::Syncing,
200            2 => SyncState::Complete,
201            3 => SyncState::Error,
202            _ => SyncState::Idle,
203        }
204    }
205}
206
207/// Sync State characteristic data
208///
209/// Read/Notify characteristic for sync status.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct SyncStateData {
212    /// Current sync state
213    pub state: SyncState,
214    /// Sync progress (0-100)
215    pub progress: u8,
216    /// Number of pending documents
217    pub pending_docs: u16,
218    /// Last sync timestamp (Unix seconds, truncated to 32 bits)
219    pub last_sync: u32,
220}
221
222impl SyncStateData {
223    /// Encoded size in bytes
224    pub const ENCODED_SIZE: usize = 8;
225
226    /// Create new sync state data
227    pub fn new(state: SyncState) -> Self {
228        Self {
229            state,
230            progress: 0,
231            pending_docs: 0,
232            last_sync: 0,
233        }
234    }
235
236    /// Encode to bytes
237    pub fn encode(&self) -> [u8; Self::ENCODED_SIZE] {
238        let mut buf = [0u8; Self::ENCODED_SIZE];
239        buf[0] = self.state as u8;
240        buf[1] = self.progress;
241        buf[2] = (self.pending_docs >> 8) as u8;
242        buf[3] = self.pending_docs as u8;
243        buf[4] = (self.last_sync >> 24) as u8;
244        buf[5] = (self.last_sync >> 16) as u8;
245        buf[6] = (self.last_sync >> 8) as u8;
246        buf[7] = self.last_sync as u8;
247        buf
248    }
249
250    /// Decode from bytes
251    pub fn decode(data: &[u8]) -> Option<Self> {
252        if data.len() < Self::ENCODED_SIZE {
253            return None;
254        }
255
256        Some(Self {
257            state: SyncState::from(data[0]),
258            progress: data[1],
259            pending_docs: ((data[2] as u16) << 8) | (data[3] as u16),
260            last_sync: ((data[4] as u32) << 24)
261                | ((data[5] as u32) << 16)
262                | ((data[6] as u32) << 8)
263                | (data[7] as u32),
264        })
265    }
266}
267
268/// Sync Data operation types
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270#[repr(u8)]
271pub enum SyncDataOp {
272    /// Document sync message
273    Document = 0x01,
274    /// Sync vector update
275    Vector = 0x02,
276    /// Acknowledgement
277    Ack = 0x03,
278    /// End of sync
279    End = 0xFF,
280}
281
282impl From<u8> for SyncDataOp {
283    fn from(value: u8) -> Self {
284        match value {
285            0x01 => SyncDataOp::Document,
286            0x02 => SyncDataOp::Vector,
287            0x03 => SyncDataOp::Ack,
288            0xFF => SyncDataOp::End,
289            _ => SyncDataOp::Document,
290        }
291    }
292}
293
294/// Sync Data characteristic header
295///
296/// Write/Indicate characteristic for sync data transfer.
297#[derive(Debug, Clone)]
298pub struct SyncDataHeader {
299    /// Operation type
300    pub op: SyncDataOp,
301    /// Sequence number
302    pub seq: u16,
303    /// Total fragments (for multi-packet transfers)
304    pub total_fragments: u8,
305    /// Current fragment index
306    pub fragment_index: u8,
307}
308
309impl SyncDataHeader {
310    /// Header size in bytes
311    pub const SIZE: usize = 5;
312
313    /// Create new header
314    pub fn new(op: SyncDataOp, seq: u16) -> Self {
315        Self {
316            op,
317            seq,
318            total_fragments: 1,
319            fragment_index: 0,
320        }
321    }
322
323    /// Encode header to bytes
324    pub fn encode(&self) -> [u8; Self::SIZE] {
325        [
326            self.op as u8,
327            (self.seq >> 8) as u8,
328            self.seq as u8,
329            self.total_fragments,
330            self.fragment_index,
331        ]
332    }
333
334    /// Decode header from bytes
335    pub fn decode(data: &[u8]) -> Option<Self> {
336        if data.len() < Self::SIZE {
337            return None;
338        }
339
340        Some(Self {
341            op: SyncDataOp::from(data[0]),
342            seq: ((data[1] as u16) << 8) | (data[2] as u16),
343            total_fragments: data[3],
344            fragment_index: data[4],
345        })
346    }
347}
348
349/// Command types
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
351#[repr(u8)]
352pub enum CommandType {
353    /// Request sync start
354    StartSync = 0x01,
355    /// Request sync stop
356    StopSync = 0x02,
357    /// Request node info refresh
358    RefreshInfo = 0x03,
359    /// Set hierarchy level (for testing)
360    SetHierarchy = 0x10,
361    /// Ping (keepalive)
362    Ping = 0xFE,
363    /// Reset connection
364    Reset = 0xFF,
365}
366
367impl From<u8> for CommandType {
368    fn from(value: u8) -> Self {
369        match value {
370            0x01 => CommandType::StartSync,
371            0x02 => CommandType::StopSync,
372            0x03 => CommandType::RefreshInfo,
373            0x10 => CommandType::SetHierarchy,
374            0xFE => CommandType::Ping,
375            0xFF => CommandType::Reset,
376            _ => CommandType::Ping,
377        }
378    }
379}
380
381/// Command characteristic data
382#[derive(Debug, Clone)]
383pub struct Command {
384    /// Command type
385    pub cmd_type: CommandType,
386    /// Command payload (variable length)
387    pub payload: Vec<u8>,
388}
389
390impl Command {
391    /// Create a new command
392    pub fn new(cmd_type: CommandType) -> Self {
393        Self {
394            cmd_type,
395            payload: Vec::new(),
396        }
397    }
398
399    /// Create a command with payload
400    pub fn with_payload(cmd_type: CommandType, payload: Vec<u8>) -> Self {
401        Self { cmd_type, payload }
402    }
403
404    /// Encode command to bytes
405    pub fn encode(&self) -> Vec<u8> {
406        let mut buf = Vec::with_capacity(1 + self.payload.len());
407        buf.push(self.cmd_type as u8);
408        buf.extend_from_slice(&self.payload);
409        buf
410    }
411
412    /// Decode command from bytes
413    pub fn decode(data: &[u8]) -> Option<Self> {
414        if data.is_empty() {
415            return None;
416        }
417
418        Some(Self {
419            cmd_type: CommandType::from(data[0]),
420            payload: data[1..].to_vec(),
421        })
422    }
423}
424
425/// Status flags
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
427pub struct StatusFlags(u8);
428
429impl StatusFlags {
430    /// Node is connected to parent
431    pub const CONNECTED: u8 = 0x01;
432    /// Node is syncing
433    pub const SYNCING: u8 = 0x02;
434    /// Node has pending data
435    pub const PENDING_DATA: u8 = 0x04;
436    /// Node is low on battery
437    pub const LOW_BATTERY: u8 = 0x08;
438    /// Node has error condition
439    pub const ERROR: u8 = 0x80;
440
441    /// Create new status flags
442    pub const fn new(flags: u8) -> Self {
443        Self(flags)
444    }
445
446    /// Check if connected
447    pub fn is_connected(&self) -> bool {
448        self.0 & Self::CONNECTED != 0
449    }
450
451    /// Check if syncing
452    pub fn is_syncing(&self) -> bool {
453        self.0 & Self::SYNCING != 0
454    }
455
456    /// Check if has pending data
457    pub fn has_pending_data(&self) -> bool {
458        self.0 & Self::PENDING_DATA != 0
459    }
460
461    /// Check if low battery
462    pub fn is_low_battery(&self) -> bool {
463        self.0 & Self::LOW_BATTERY != 0
464    }
465
466    /// Check if error
467    pub fn has_error(&self) -> bool {
468        self.0 & Self::ERROR != 0
469    }
470
471    /// Get raw flags
472    pub fn flags(&self) -> u8 {
473        self.0
474    }
475}
476
477/// Status characteristic data
478#[derive(Debug, Clone, PartialEq, Eq)]
479pub struct StatusData {
480    /// Status flags
481    pub flags: StatusFlags,
482    /// Number of connected children
483    pub child_count: u8,
484    /// RSSI to parent (-128 to 127, 127 = no parent)
485    pub parent_rssi: i8,
486    /// Uptime in minutes (max ~45 days)
487    pub uptime_minutes: u16,
488}
489
490impl StatusData {
491    /// Encoded size in bytes
492    pub const ENCODED_SIZE: usize = 5;
493
494    /// Create new status data
495    pub fn new() -> Self {
496        Self {
497            flags: StatusFlags::default(),
498            child_count: 0,
499            parent_rssi: 127, // No parent
500            uptime_minutes: 0,
501        }
502    }
503
504    /// Encode to bytes
505    pub fn encode(&self) -> [u8; Self::ENCODED_SIZE] {
506        [
507            self.flags.flags(),
508            self.child_count,
509            self.parent_rssi as u8,
510            (self.uptime_minutes >> 8) as u8,
511            self.uptime_minutes as u8,
512        ]
513    }
514
515    /// Decode from bytes
516    pub fn decode(data: &[u8]) -> Option<Self> {
517        if data.len() < Self::ENCODED_SIZE {
518            return None;
519        }
520
521        Some(Self {
522            flags: StatusFlags::new(data[0]),
523            child_count: data[1],
524            parent_rssi: data[2] as i8,
525            uptime_minutes: ((data[3] as u16) << 8) | (data[4] as u16),
526        })
527    }
528}
529
530impl Default for StatusData {
531    fn default() -> Self {
532        Self::new()
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::capabilities;
540
541    #[test]
542    fn test_characteristic_properties() {
543        let props = CharacteristicProperties::new(
544            CharacteristicProperties::READ | CharacteristicProperties::NOTIFY,
545        );
546        assert!(props.can_read());
547        assert!(props.can_notify());
548        assert!(!props.can_write());
549        assert!(!props.can_indicate());
550    }
551
552    #[test]
553    fn test_characteristic_uuids() {
554        let node_info = HiveCharacteristicUuids::node_info();
555        let sync_state = HiveCharacteristicUuids::sync_state();
556
557        // UUIDs should be different
558        assert_ne!(node_info, sync_state);
559
560        // Should be derived from base UUID
561        assert_ne!(node_info, HIVE_SERVICE_UUID);
562    }
563
564    #[test]
565    fn test_node_info_encode_decode() {
566        let info = NodeInfo::new(
567            NodeId::new(0x12345678),
568            HierarchyLevel::Squad,
569            capabilities::CAN_RELAY | capabilities::HAS_GPS,
570        );
571
572        let encoded = info.encode();
573        assert_eq!(encoded.len(), NodeInfo::ENCODED_SIZE);
574
575        let decoded = NodeInfo::decode(&encoded).unwrap();
576        assert_eq!(decoded.node_id, info.node_id);
577        assert_eq!(decoded.hierarchy_level, info.hierarchy_level);
578        assert_eq!(decoded.capabilities, info.capabilities);
579    }
580
581    #[test]
582    fn test_sync_state_encode_decode() {
583        let state = SyncStateData {
584            state: SyncState::Syncing,
585            progress: 50,
586            pending_docs: 10,
587            last_sync: 1234567890,
588        };
589
590        let encoded = state.encode();
591        assert_eq!(encoded.len(), SyncStateData::ENCODED_SIZE);
592
593        let decoded = SyncStateData::decode(&encoded).unwrap();
594        assert_eq!(decoded.state, state.state);
595        assert_eq!(decoded.progress, state.progress);
596        assert_eq!(decoded.pending_docs, state.pending_docs);
597        assert_eq!(decoded.last_sync, state.last_sync);
598    }
599
600    #[test]
601    fn test_sync_data_header() {
602        let header = SyncDataHeader::new(SyncDataOp::Document, 42);
603
604        let encoded = header.encode();
605        assert_eq!(encoded.len(), SyncDataHeader::SIZE);
606
607        let decoded = SyncDataHeader::decode(&encoded).unwrap();
608        assert_eq!(decoded.op, SyncDataOp::Document);
609        assert_eq!(decoded.seq, 42);
610    }
611
612    #[test]
613    fn test_command_encode_decode() {
614        let cmd = Command::with_payload(CommandType::SetHierarchy, vec![2]); // Set to Platoon
615
616        let encoded = cmd.encode();
617        assert_eq!(encoded[0], CommandType::SetHierarchy as u8);
618        assert_eq!(encoded[1], 2);
619
620        let decoded = Command::decode(&encoded).unwrap();
621        assert_eq!(decoded.cmd_type, CommandType::SetHierarchy);
622        assert_eq!(decoded.payload, vec![2]);
623    }
624
625    #[test]
626    fn test_status_flags() {
627        let flags = StatusFlags::new(StatusFlags::CONNECTED | StatusFlags::SYNCING);
628        assert!(flags.is_connected());
629        assert!(flags.is_syncing());
630        assert!(!flags.has_pending_data());
631        assert!(!flags.has_error());
632    }
633
634    #[test]
635    fn test_status_data_encode_decode() {
636        let status = StatusData {
637            flags: StatusFlags::new(StatusFlags::CONNECTED),
638            child_count: 3,
639            parent_rssi: -60,
640            uptime_minutes: 1440, // 24 hours
641        };
642
643        let encoded = status.encode();
644        assert_eq!(encoded.len(), StatusData::ENCODED_SIZE);
645
646        let decoded = StatusData::decode(&encoded).unwrap();
647        assert!(decoded.flags.is_connected());
648        assert_eq!(decoded.child_count, 3);
649        assert_eq!(decoded.parent_rssi, -60);
650        assert_eq!(decoded.uptime_minutes, 1440);
651    }
652}