hive_btle/gatt/
characteristics.rs

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