Skip to main content

peat_btle/gatt/
service.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//! Peat GATT Service Implementation
17//!
18//! Provides the GATT service structure and handlers for Peat Protocol BLE communication.
19//!
20//! Note: This module requires the `std` feature for full functionality.
21
22use std::sync::{Arc, RwLock};
23
24use crate::error::Result;
25use crate::{HierarchyLevel, NodeId, PEAT_SERVICE_UUID};
26
27use super::characteristics::{
28    CharacteristicProperties, Command, CommandType, NodeInfo, PeatCharacteristicUuids, StatusData,
29    StatusFlags, SyncDataHeader, SyncDataOp, SyncState, SyncStateData,
30};
31
32/// GATT service event handler callback type
33pub type GattEventCallback = Box<dyn Fn(GattEvent) + Send + Sync>;
34
35/// Events emitted by the GATT service
36#[derive(Debug, Clone)]
37pub enum GattEvent {
38    /// Client connected
39    ClientConnected {
40        /// Client address
41        address: String,
42    },
43    /// Client disconnected
44    ClientDisconnected {
45        /// Client address
46        address: String,
47    },
48    /// Client subscribed to notifications
49    NotificationSubscribed {
50        /// Characteristic name
51        characteristic: String,
52    },
53    /// Client unsubscribed from notifications
54    NotificationUnsubscribed {
55        /// Characteristic name
56        characteristic: String,
57    },
58    /// Command received
59    CommandReceived {
60        /// Command that was received
61        command: CommandType,
62        /// Command payload
63        payload: Vec<u8>,
64    },
65    /// Sync data received
66    SyncDataReceived {
67        /// Sync data header
68        header: SyncDataHeader,
69        /// Sync data payload
70        payload: Vec<u8>,
71    },
72    /// MTU changed
73    MtuChanged {
74        /// New MTU value
75        mtu: u16,
76    },
77}
78
79/// GATT characteristic descriptor
80#[derive(Debug, Clone)]
81pub struct CharacteristicDescriptor {
82    /// Characteristic UUID
83    pub uuid: uuid::Uuid,
84    /// Human-readable name
85    pub name: &'static str,
86    /// Properties (read, write, notify, etc.)
87    pub properties: CharacteristicProperties,
88    /// Whether encryption is required
89    pub encrypted: bool,
90}
91
92/// Characteristic definitions for Peat GATT service
93pub struct PeatCharacteristics;
94
95impl PeatCharacteristics {
96    /// Node Info characteristic descriptor
97    pub fn node_info() -> CharacteristicDescriptor {
98        CharacteristicDescriptor {
99            uuid: PeatCharacteristicUuids::node_info(),
100            name: "Node Info",
101            properties: CharacteristicProperties::new(CharacteristicProperties::READ),
102            encrypted: true,
103        }
104    }
105
106    /// Sync State characteristic descriptor
107    pub fn sync_state() -> CharacteristicDescriptor {
108        CharacteristicDescriptor {
109            uuid: PeatCharacteristicUuids::sync_state(),
110            name: "Sync State",
111            properties: CharacteristicProperties::new(
112                CharacteristicProperties::READ | CharacteristicProperties::NOTIFY,
113            ),
114            encrypted: true,
115        }
116    }
117
118    /// Sync Data characteristic descriptor
119    pub fn sync_data() -> CharacteristicDescriptor {
120        CharacteristicDescriptor {
121            uuid: PeatCharacteristicUuids::sync_data(),
122            name: "Sync Data",
123            properties: CharacteristicProperties::new(
124                CharacteristicProperties::WRITE | CharacteristicProperties::INDICATE,
125            ),
126            encrypted: true,
127        }
128    }
129
130    /// Command characteristic descriptor
131    pub fn command() -> CharacteristicDescriptor {
132        CharacteristicDescriptor {
133            uuid: PeatCharacteristicUuids::command(),
134            name: "Command",
135            properties: CharacteristicProperties::new(CharacteristicProperties::WRITE),
136            encrypted: true,
137        }
138    }
139
140    /// Status characteristic descriptor
141    pub fn status() -> CharacteristicDescriptor {
142        CharacteristicDescriptor {
143            uuid: PeatCharacteristicUuids::status(),
144            name: "Status",
145            properties: CharacteristicProperties::new(
146                CharacteristicProperties::READ | CharacteristicProperties::NOTIFY,
147            ),
148            encrypted: true,
149        }
150    }
151
152    /// Get all characteristic descriptors
153    pub fn all() -> Vec<CharacteristicDescriptor> {
154        vec![
155            Self::node_info(),
156            Self::sync_state(),
157            Self::sync_data(),
158            Self::command(),
159            Self::status(),
160        ]
161    }
162}
163
164/// Internal state for the GATT service
165struct ServiceState {
166    /// Node information
167    node_info: NodeInfo,
168    /// Current sync state
169    sync_state: SyncStateData,
170    /// Current status
171    status: StatusData,
172    /// Connected clients (addresses)
173    connected_clients: Vec<String>,
174    /// Clients subscribed to sync state notifications
175    sync_state_subscribers: Vec<String>,
176    /// Clients subscribed to status notifications
177    status_subscribers: Vec<String>,
178    /// Negotiated MTU
179    mtu: u16,
180}
181
182impl ServiceState {
183    fn new(node_id: NodeId, hierarchy_level: HierarchyLevel, capabilities: u16) -> Self {
184        Self {
185            node_info: NodeInfo::new(node_id, hierarchy_level, capabilities),
186            sync_state: SyncStateData::new(SyncState::Idle),
187            status: StatusData::new(),
188            connected_clients: Vec::new(),
189            sync_state_subscribers: Vec::new(),
190            status_subscribers: Vec::new(),
191            mtu: 23, // Default BLE MTU
192        }
193    }
194}
195
196/// Peat GATT Service
197///
198/// Manages the GATT service lifecycle and provides handlers for characteristic operations.
199pub struct PeatGattService {
200    /// Service UUID
201    pub uuid: uuid::Uuid,
202    /// Internal state
203    state: Arc<RwLock<ServiceState>>,
204    /// Event callback
205    #[allow(dead_code)]
206    event_callback: Option<GattEventCallback>,
207}
208
209impl PeatGattService {
210    /// Create a new Peat GATT service
211    pub fn new(node_id: NodeId, hierarchy_level: HierarchyLevel, capabilities: u16) -> Self {
212        Self {
213            uuid: PEAT_SERVICE_UUID,
214            state: Arc::new(RwLock::new(ServiceState::new(
215                node_id,
216                hierarchy_level,
217                capabilities,
218            ))),
219            event_callback: None,
220        }
221    }
222
223    /// Set event callback
224    pub fn set_event_callback(&mut self, callback: GattEventCallback) {
225        self.event_callback = Some(callback);
226    }
227
228    /// Get service UUID
229    pub fn service_uuid(&self) -> uuid::Uuid {
230        self.uuid
231    }
232
233    /// Get all characteristic descriptors
234    pub fn characteristics(&self) -> Vec<CharacteristicDescriptor> {
235        PeatCharacteristics::all()
236    }
237
238    // === Read Handlers ===
239
240    /// Handle Node Info read request
241    pub fn read_node_info(&self) -> Vec<u8> {
242        let state = self.state.read().unwrap();
243        state.node_info.encode().to_vec()
244    }
245
246    /// Handle Sync State read request
247    pub fn read_sync_state(&self) -> Vec<u8> {
248        let state = self.state.read().unwrap();
249        state.sync_state.encode().to_vec()
250    }
251
252    /// Handle Status read request
253    pub fn read_status(&self) -> Vec<u8> {
254        let state = self.state.read().unwrap();
255        state.status.encode().to_vec()
256    }
257
258    // === Write Handlers ===
259
260    /// Handle Sync Data write request
261    pub fn write_sync_data(&self, data: &[u8]) -> Result<Option<Vec<u8>>> {
262        let header = SyncDataHeader::decode(data).ok_or_else(|| {
263            crate::error::BleError::GattError("Invalid sync data header".to_string())
264        })?;
265
266        let payload = if data.len() > SyncDataHeader::SIZE {
267            data[SyncDataHeader::SIZE..].to_vec()
268        } else {
269            Vec::new()
270        };
271
272        // Process based on operation type
273        match header.op {
274            SyncDataOp::Document => {
275                // Update sync state to syncing
276                let mut state = self.state.write().unwrap();
277                state.sync_state.state = SyncState::Syncing;
278                state.status.flags =
279                    StatusFlags::new(state.status.flags.flags() | StatusFlags::SYNCING);
280
281                // Return acknowledgement
282                let ack = SyncDataHeader::new(SyncDataOp::Ack, header.seq);
283                Ok(Some(ack.encode().to_vec()))
284            }
285            SyncDataOp::Vector => {
286                // Sync vector update
287                let ack = SyncDataHeader::new(SyncDataOp::Ack, header.seq);
288                Ok(Some(ack.encode().to_vec()))
289            }
290            SyncDataOp::End => {
291                // Sync complete
292                let mut state = self.state.write().unwrap();
293                state.sync_state.state = SyncState::Complete;
294                state.sync_state.progress = 100;
295                state.status.flags =
296                    StatusFlags::new(state.status.flags.flags() & !StatusFlags::SYNCING);
297
298                // Emit event if callback set
299                if let Some(ref callback) = self.event_callback {
300                    callback(GattEvent::SyncDataReceived { header, payload });
301                }
302
303                Ok(None)
304            }
305            SyncDataOp::Ack => {
306                // Acknowledgement received (shouldn't happen on write)
307                Ok(None)
308            }
309        }
310    }
311
312    /// Handle Command write request
313    pub fn write_command(&self, data: &[u8]) -> Result<()> {
314        let command = Command::decode(data)
315            .ok_or_else(|| crate::error::BleError::GattError("Invalid command data".to_string()))?;
316
317        match command.cmd_type {
318            CommandType::StartSync => {
319                let mut state = self.state.write().unwrap();
320                state.sync_state.state = SyncState::Syncing;
321                state.sync_state.progress = 0;
322            }
323            CommandType::StopSync => {
324                let mut state = self.state.write().unwrap();
325                state.sync_state.state = SyncState::Idle;
326            }
327            CommandType::RefreshInfo => {
328                // Trigger info refresh (no-op for now)
329            }
330            CommandType::SetHierarchy => {
331                if !command.payload.is_empty() {
332                    let mut state = self.state.write().unwrap();
333                    state.node_info.hierarchy_level = HierarchyLevel::from(command.payload[0]);
334                }
335            }
336            CommandType::Ping => {
337                // Keepalive - no action needed
338            }
339            CommandType::Reset => {
340                let mut state = self.state.write().unwrap();
341                state.sync_state = SyncStateData::new(SyncState::Idle);
342            }
343        }
344
345        // Emit event if callback set
346        if let Some(ref callback) = self.event_callback {
347            callback(GattEvent::CommandReceived {
348                command: command.cmd_type,
349                payload: command.payload,
350            });
351        }
352
353        Ok(())
354    }
355
356    // === State Updates ===
357
358    /// Update battery percentage
359    pub fn update_battery(&self, percent: u8) {
360        let mut state = self.state.write().unwrap();
361        state.node_info.battery_percent = percent.min(100);
362
363        // Update low battery flag
364        if percent < 20 {
365            state.status.flags =
366                StatusFlags::new(state.status.flags.flags() | StatusFlags::LOW_BATTERY);
367        } else {
368            state.status.flags =
369                StatusFlags::new(state.status.flags.flags() & !StatusFlags::LOW_BATTERY);
370        }
371    }
372
373    /// Update hierarchy level
374    pub fn update_hierarchy_level(&self, level: HierarchyLevel) {
375        let mut state = self.state.write().unwrap();
376        state.node_info.hierarchy_level = level;
377    }
378
379    /// Update sync progress
380    pub fn update_sync_progress(&self, progress: u8, pending_docs: u16) {
381        let mut state = self.state.write().unwrap();
382        state.sync_state.progress = progress.min(100);
383        state.sync_state.pending_docs = pending_docs;
384
385        if progress >= 100 {
386            state.sync_state.state = SyncState::Complete;
387        }
388    }
389
390    /// Update parent connection status
391    pub fn update_parent_status(&self, connected: bool, rssi: Option<i8>) {
392        let mut state = self.state.write().unwrap();
393
394        if connected {
395            state.status.flags =
396                StatusFlags::new(state.status.flags.flags() | StatusFlags::CONNECTED);
397            state.status.parent_rssi = rssi.unwrap_or(0);
398        } else {
399            state.status.flags =
400                StatusFlags::new(state.status.flags.flags() & !StatusFlags::CONNECTED);
401            state.status.parent_rssi = 127; // No parent
402        }
403    }
404
405    /// Update child count
406    pub fn update_child_count(&self, count: u8) {
407        let mut state = self.state.write().unwrap();
408        state.status.child_count = count;
409    }
410
411    /// Update uptime
412    pub fn update_uptime(&self, minutes: u16) {
413        let mut state = self.state.write().unwrap();
414        state.status.uptime_minutes = minutes;
415    }
416
417    // === Connection Management ===
418
419    /// Handle client connection
420    pub fn on_client_connected(&self, address: String) {
421        let mut state = self.state.write().unwrap();
422        if !state.connected_clients.contains(&address) {
423            state.connected_clients.push(address.clone());
424        }
425
426        if let Some(ref callback) = self.event_callback {
427            callback(GattEvent::ClientConnected { address });
428        }
429    }
430
431    /// Handle client disconnection
432    pub fn on_client_disconnected(&self, address: &str) {
433        let mut state = self.state.write().unwrap();
434        state.connected_clients.retain(|a| a != address);
435        state.sync_state_subscribers.retain(|a| a != address);
436        state.status_subscribers.retain(|a| a != address);
437
438        if let Some(ref callback) = self.event_callback {
439            callback(GattEvent::ClientDisconnected {
440                address: address.to_string(),
441            });
442        }
443    }
444
445    /// Handle notification subscription
446    pub fn on_subscribe(&self, address: String, characteristic: &str) {
447        let mut state = self.state.write().unwrap();
448
449        match characteristic {
450            "sync_state" if !state.sync_state_subscribers.contains(&address) => {
451                state.sync_state_subscribers.push(address);
452            }
453            "status" if !state.status_subscribers.contains(&address) => {
454                state.status_subscribers.push(address);
455            }
456            _ => {}
457        }
458
459        if let Some(ref callback) = self.event_callback {
460            callback(GattEvent::NotificationSubscribed {
461                characteristic: characteristic.to_string(),
462            });
463        }
464    }
465
466    /// Handle MTU change
467    pub fn on_mtu_changed(&self, mtu: u16) {
468        let mut state = self.state.write().unwrap();
469        state.mtu = mtu;
470
471        if let Some(ref callback) = self.event_callback {
472            callback(GattEvent::MtuChanged { mtu });
473        }
474    }
475
476    /// Get current MTU
477    pub fn mtu(&self) -> u16 {
478        let state = self.state.read().unwrap();
479        state.mtu
480    }
481
482    /// Get connected client count
483    pub fn connected_client_count(&self) -> usize {
484        let state = self.state.read().unwrap();
485        state.connected_clients.len()
486    }
487
488    /// Get list of addresses subscribed to sync state
489    pub fn sync_state_subscribers(&self) -> Vec<String> {
490        let state = self.state.read().unwrap();
491        state.sync_state_subscribers.clone()
492    }
493
494    /// Get list of addresses subscribed to status
495    pub fn status_subscribers(&self) -> Vec<String> {
496        let state = self.state.read().unwrap();
497        state.status_subscribers.clone()
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504    use crate::capabilities;
505
506    #[test]
507    fn test_gatt_service_creation() {
508        let service = PeatGattService::new(
509            NodeId::new(0x12345678),
510            HierarchyLevel::Squad,
511            capabilities::CAN_RELAY,
512        );
513
514        assert_eq!(service.service_uuid(), PEAT_SERVICE_UUID);
515        assert_eq!(service.characteristics().len(), 5);
516    }
517
518    #[test]
519    fn test_read_node_info() {
520        let service = PeatGattService::new(
521            NodeId::new(0x12345678),
522            HierarchyLevel::Squad,
523            capabilities::CAN_RELAY,
524        );
525
526        let data = service.read_node_info();
527        assert_eq!(data.len(), NodeInfo::ENCODED_SIZE);
528
529        let info = NodeInfo::decode(&data).unwrap();
530        assert_eq!(info.node_id, NodeId::new(0x12345678));
531        assert_eq!(info.hierarchy_level, HierarchyLevel::Squad);
532    }
533
534    #[test]
535    fn test_write_command() {
536        let service = PeatGattService::new(NodeId::new(0x12345678), HierarchyLevel::Platform, 0);
537
538        // Set hierarchy command
539        let cmd = Command::with_payload(CommandType::SetHierarchy, vec![2]); // Platoon
540        service.write_command(&cmd.encode()).unwrap();
541
542        let data = service.read_node_info();
543        let info = NodeInfo::decode(&data).unwrap();
544        assert_eq!(info.hierarchy_level, HierarchyLevel::Platoon);
545    }
546
547    #[test]
548    fn test_sync_data_flow() {
549        let service = PeatGattService::new(NodeId::new(0x12345678), HierarchyLevel::Platform, 0);
550
551        // Start sync
552        let cmd = Command::new(CommandType::StartSync);
553        service.write_command(&cmd.encode()).unwrap();
554
555        // Check sync state
556        let state_data = service.read_sync_state();
557        let state = SyncStateData::decode(&state_data).unwrap();
558        assert_eq!(state.state, SyncState::Syncing);
559
560        // Send document
561        let mut header = SyncDataHeader::new(SyncDataOp::Document, 1);
562        let mut data = header.encode().to_vec();
563        data.extend_from_slice(b"test document data");
564
565        let response = service.write_sync_data(&data).unwrap();
566        assert!(response.is_some()); // Should get ACK
567
568        // End sync
569        header = SyncDataHeader::new(SyncDataOp::End, 2);
570        service.write_sync_data(&header.encode()).unwrap();
571
572        // Check sync complete
573        let state_data = service.read_sync_state();
574        let state = SyncStateData::decode(&state_data).unwrap();
575        assert_eq!(state.state, SyncState::Complete);
576    }
577
578    #[test]
579    fn test_battery_update() {
580        let service = PeatGattService::new(NodeId::new(0x12345678), HierarchyLevel::Platform, 0);
581
582        service.update_battery(15);
583
584        let data = service.read_node_info();
585        let info = NodeInfo::decode(&data).unwrap();
586        assert_eq!(info.battery_percent, 15);
587
588        let status_data = service.read_status();
589        let status = StatusData::decode(&status_data).unwrap();
590        assert!(status.flags.is_low_battery());
591    }
592
593    #[test]
594    fn test_client_connection() {
595        let service = PeatGattService::new(NodeId::new(0x12345678), HierarchyLevel::Platform, 0);
596
597        service.on_client_connected("AA:BB:CC:DD:EE:FF".to_string());
598        assert_eq!(service.connected_client_count(), 1);
599
600        service.on_client_disconnected("AA:BB:CC:DD:EE:FF");
601        assert_eq!(service.connected_client_count(), 0);
602    }
603
604    #[test]
605    fn test_mtu_negotiation() {
606        let service = PeatGattService::new(NodeId::new(0x12345678), HierarchyLevel::Platform, 0);
607
608        assert_eq!(service.mtu(), 23); // Default
609
610        service.on_mtu_changed(251);
611        assert_eq!(service.mtu(), 251);
612    }
613
614    #[test]
615    fn test_peat_characteristics() {
616        let chars = PeatCharacteristics::all();
617        assert_eq!(chars.len(), 5);
618
619        let node_info = PeatCharacteristics::node_info();
620        assert!(node_info.properties.can_read());
621        assert!(!node_info.properties.can_write());
622
623        let sync_data = PeatCharacteristics::sync_data();
624        assert!(sync_data.properties.can_write());
625        assert!(sync_data.properties.can_indicate());
626    }
627}