gopro_controller/
lib.rs

1mod command;
2#[cfg(feature = "query")]
3mod query;
4mod services;
5mod settings;
6#[cfg(test)]
7mod tests;
8pub use crate::command::GoProCommand;
9#[cfg(feature = "query")]
10pub use crate::query::{GoProQuery, QueryResponse};
11pub use crate::services::{
12    GoProControlAndQueryCharacteristics as GPCharac, GoProServices, Sendable, ToUUID,
13};
14pub use crate::settings::GoProSetting;
15use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter, WriteType};
16use btleplug::api::{CharPropFlags, ValueNotification};
17use btleplug::platform::{Adapter, Manager, Peripheral};
18use futures::stream::StreamExt;
19use std::error::Error;
20
21///Represents a connected GoPro device
22pub struct GoPro {
23    device: Peripheral,
24}
25
26impl GoPro {
27    ///Sends a command to the GoPro without checking for a response
28    /// 
29    /// # Arguments
30    /// * `command` - The command to send to the GoPro
31    pub async fn send_command_unchecked(
32        &self,
33        command: &GoProCommand,
34    ) -> Result<(), Box<dyn Error>> {
35        let characteristics = self.device.characteristics();
36
37        let command_write_char = characteristics
38            .iter()
39            .find(|c| c.uuid == GPCharac::Command.to_uuid())
40            .unwrap();
41
42        self.device
43            .write(
44                &command_write_char,
45                command.as_bytes(),
46                WriteType::WithoutResponse,
47            )
48            .await?;
49
50        Ok(())
51    }
52
53    ///Sends a command to the GoPro and checks for a response, erroring if the response is incorrect
54    /// 
55    /// # Arguments
56    /// * `command` - The command to send to the GoPro
57    pub async fn send_command(&self, command: &GoProCommand) -> Result<(), Box<dyn Error>> {
58        self.send_command_unchecked(command).await?;
59        let res = self.get_next_notification().await?;
60        if res.is_none() {
61            return Err("No response from GoPro".into());
62        }
63        let res = res.unwrap();
64        if res.uuid != GPCharac::CommandResponse.to_uuid() {
65            return Err("Response from GoPro came from incorrect UUID".into());
66        }
67        if res.value != command.response_value_bytes() {
68            return Err("Response from GoPro was incorrect".into());
69        }
70        Ok(())
71    }
72
73    ///Sends a setting to the GoPro without checking for a response
74    /// 
75    /// # Arguments
76    /// * `setting` - The setting to send to the GoPro
77    pub async fn send_setting_unchecked(
78        &self,
79        setting: &GoProSetting,
80    ) -> Result<(), Box<dyn Error>> {
81        let characteristics = self.device.characteristics();
82
83        let settings_write_char = characteristics
84            .iter()
85            .find(|c| c.uuid == GPCharac::Settings.to_uuid())
86            .unwrap();
87
88        self.device
89            .write(
90                &settings_write_char,
91                setting.as_bytes(),
92                WriteType::WithoutResponse,
93            )
94            .await?;
95
96        Ok(())
97    }
98
99    ///Sends a setting to the GoPro and checks for a response, erroring if the response is incorrect
100    /// 
101    /// # Arguments
102    /// * `setting` - The setting to send to the GoPro
103    pub async fn send_setting(&self, setting: &GoProSetting) -> Result<(), Box<dyn Error>> {
104        self.send_setting_unchecked(setting).await?;
105        let res = self.get_next_notification().await?;
106        if res.is_none() {
107            return Err("No response from GoPro".into());
108        }
109        let res = res.unwrap();
110        if res.uuid != GPCharac::SettingsResponse.to_uuid() {
111            return Err("Response from GoPro came from incorrect UUID".into());
112        }
113        if res.value != setting.response_value_bytes() {
114            return Err("Response from GoPro was incorrect".into());
115        }
116        Ok(())
117    }
118
119    #[cfg(feature = "query")]
120    ///Sends a query to the GoPro and returns the response
121    /// 
122    /// # Arguments
123    /// * `query` - The query to send to the GoPro
124    pub async fn query(&self, query: &GoProQuery) -> Result<QueryResponse, Box<dyn Error>> {
125        let characteristics = self.device.characteristics();
126
127        let query_write_char = characteristics
128            .iter()
129            .find(|c| c.uuid == GPCharac::Query.to_uuid())
130            .unwrap();
131
132        self.device
133            .write(
134                &query_write_char,
135                query.as_bytes().as_ref(),
136                WriteType::WithoutResponse,
137            )
138            .await?;
139
140        let res = self.get_next_notification().await?;
141        if res.is_none() {
142            return Err("No response from GoPro".into());
143        }
144        let res = res.unwrap();
145        if res.uuid != GPCharac::QueryResponse.to_uuid() {
146            return Err("Response from GoPro came from incorrect UUID".into());
147        }
148
149        let query_response = QueryResponse::deserialize(&res.value)?;
150        Ok(query_response)
151    }
152
153    ///Gets the next notification (response from a command) from the GoPro
154    /// 
155    /// # Returns
156    /// * `Ok(Some(ValueNotification))` - If a notification was received
157    /// * `Ok(None)` - If no notification was received
158    /// * `Err(Box<dyn Error>)` - If an error occurred
159    pub async fn get_next_notification(&self) -> Result<Option<ValueNotification>, Box<dyn Error>> {
160        let mut response_stream = self.device.notifications().await?;
161        let notification = response_stream.next().await;
162        Ok(notification)
163    }
164
165    ///Disconnects the GoPro
166    pub async fn disconnect(self) -> Result<(), Box<dyn Error>> {
167        self.device.disconnect().await?;
168        Ok(())
169    }
170
171    ///Disconnects the GoPro and powers it off
172    /// 
173    /// # Note
174    /// 
175    /// The camera will continue to send advertisement packets for 10 hours after being powered off
176    /// allowing for an auto wake on reconnecting
177    pub async fn disconnect_and_poweroff(self) -> Result<(), Box<dyn Error>> {
178        self.send_command(GoProCommand::Sleep.as_ref()).await?;
179        self.device.disconnect().await?;
180        Ok(())
181    }
182}
183
184///Inits the bluetooth adapter (central) and returns it to the caller
185/// 
186/// # Arguments
187/// * `adapter_index` - An optional index into the list of bluetooth adapters in case the caller has more than one
188pub async fn init(adapter_index: Option<usize>) -> Result<Adapter, Box<dyn Error>> {
189    let manager = Manager::new().await.unwrap();
190
191    //manage multiple adapters ?
192    let index = adapter_index.unwrap_or(0);
193    // get the first bluetooth adapter
194    let adapters = manager.adapters().await?;
195
196    if adapters.len() <= 0 {
197        return Err("No Bluetooth Adapters".into());
198    }
199
200    let central = adapters.into_iter().nth(index).unwrap();
201    Ok(central)
202}
203
204///Scans for GoPro devices and returns a list of their names
205///(may also return previously connected devices some of which may not be GoPros)
206/// 
207/// # Arguments
208/// * `central` - The bluetooth adapter to use for scanning
209pub async fn scan(central: &mut Adapter) -> Result<Vec<String>, Box<dyn Error>> {
210    // start scanning for devices
211    let scan_filter = ScanFilter {
212        services: vec![GoProServices::ControlAndQuery.to_uuid()],
213    };
214
215    central.start_scan(scan_filter).await?;
216
217    let mut devices_names: Vec<String> = Vec::with_capacity(central.peripherals().await?.len());
218
219    for p in central.peripherals().await? {
220        let properties = p.properties().await?;
221        let name = properties
222            .unwrap()
223            .local_name
224            .unwrap_or("Unknown".to_string());
225        devices_names.push(name);
226    }
227    Ok(devices_names)
228}
229
230///
231///Connects to a GoPro device by name and returns a GoPro object if successful
232/// 
233/// # Arguments
234/// * `gopro_local_name` - The name of the GoPro device to connect to
235/// * `central` - The bluetooth adapter to use for connecting
236pub async fn connect(
237    gopro_local_name: String,
238    central: &mut Adapter,
239) -> Result<GoPro, Box<dyn Error>> {
240    let device = filter_peripherals(central.peripherals().await?, gopro_local_name).await?;
241    if device.is_none() {
242        return Err("GoPro not found".into());
243    }
244    let device = device.unwrap();
245
246    // connect to the device
247    device.connect().await?;
248
249    //discover all the services on the device
250    device.discover_services().await?;
251
252    //subscribe to the proper notify characteristics
253    let characteristics = device.characteristics();
254
255    if characteristics.len() == 0 {
256        return Err("No characteristics found on this GoPro".into());
257    }
258
259    //Subscribe to all the characteristics that have the notify property
260    //TODO: Send off subscriptions concurently ?
261    for c in &characteristics {
262        if c.properties.bits() == CharPropFlags::NOTIFY.bits() {
263            device.subscribe(&c).await?;
264        }
265    }
266
267    Ok(GoPro { device })
268}
269
270///Filters a list of peripherals by name and returns the first one that matches
271async fn filter_peripherals(
272    peripherals: Vec<Peripheral>,
273    device_name: String,
274) -> Result<Option<Peripheral>, Box<dyn Error>> {
275    for p in peripherals {
276        let properties = p.properties().await?;
277        let name = properties
278            .unwrap()
279            .local_name
280            .unwrap_or("Unknown".to_string());
281        if name.eq(&device_name) {
282            return Ok(Some(p));
283        }
284    }
285    Ok(None)
286}