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
21pub struct GoPro {
23 device: Peripheral,
24}
25
26impl GoPro {
27 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 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 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 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 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 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 pub async fn disconnect(self) -> Result<(), Box<dyn Error>> {
167 self.device.disconnect().await?;
168 Ok(())
169 }
170
171 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
184pub async fn init(adapter_index: Option<usize>) -> Result<Adapter, Box<dyn Error>> {
189 let manager = Manager::new().await.unwrap();
190
191 let index = adapter_index.unwrap_or(0);
193 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
204pub async fn scan(central: &mut Adapter) -> Result<Vec<String>, Box<dyn Error>> {
210 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
230pub 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 device.connect().await?;
248
249 device.discover_services().await?;
251
252 let characteristics = device.characteristics();
254
255 if characteristics.len() == 0 {
256 return Err("No characteristics found on this GoPro".into());
257 }
258
259 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
270async 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}