1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
mod command;
#[cfg(feature = "query")]
mod query;
mod services;
mod settings;
#[cfg(test)]
mod tests;
use crate::command::GoProCommand;
#[cfg(feature = "query")]
use crate::query::{GoProQuery, QueryResponse};
use crate::services::{
    GoProControlAndQueryCharacteristics as GPCharac, GoProServices, Sendable, ToUUID,
};
use crate::settings::GoProSetting;
use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter, WriteType};
use btleplug::api::{CharPropFlags, ValueNotification};
use btleplug::platform::{Adapter, Manager, Peripheral};
use futures::stream::StreamExt;
use std::error::Error;

///Represents a connected GoPro device
pub struct GoPro {
    device: Peripheral,
}

impl GoPro {
    ///Sends a command to the GoPro without checking for a response
    /// 
    /// # Arguments
    /// * `command` - The command to send to the GoPro
    pub async fn send_command_unchecked(
        &self,
        command: &GoProCommand,
    ) -> Result<(), Box<dyn Error>> {
        let characteristics = self.device.characteristics();

        let command_write_char = characteristics
            .iter()
            .find(|c| c.uuid == GPCharac::Command.to_uuid())
            .unwrap();

        self.device
            .write(
                &command_write_char,
                command.as_bytes(),
                WriteType::WithoutResponse,
            )
            .await?;

        Ok(())
    }

    ///Sends a command to the GoPro and checks for a response, erroring if the response is incorrect
    /// 
    /// # Arguments
    /// * `command` - The command to send to the GoPro
    pub async fn send_command(&self, command: &GoProCommand) -> Result<(), Box<dyn Error>> {
        self.send_command_unchecked(command).await?;
        let res = self.get_next_notification().await?;
        if res.is_none() {
            return Err("No response from GoPro".into());
        }
        let res = res.unwrap();
        if res.uuid != GPCharac::CommandResponse.to_uuid() {
            return Err("Response from GoPro came from incorrect UUID".into());
        }
        if res.value != command.response_value_bytes() {
            return Err("Response from GoPro was incorrect".into());
        }
        Ok(())
    }

    ///Sends a setting to the GoPro without checking for a response
    /// 
    /// # Arguments
    /// * `setting` - The setting to send to the GoPro
    pub async fn send_setting_unchecked(
        &self,
        setting: &GoProSetting,
    ) -> Result<(), Box<dyn Error>> {
        let characteristics = self.device.characteristics();

        let settings_write_char = characteristics
            .iter()
            .find(|c| c.uuid == GPCharac::Settings.to_uuid())
            .unwrap();

        self.device
            .write(
                &settings_write_char,
                setting.as_bytes(),
                WriteType::WithoutResponse,
            )
            .await?;

        Ok(())
    }

    ///Sends a setting to the GoPro and checks for a response, erroring if the response is incorrect
    /// 
    /// # Arguments
    /// * `setting` - The setting to send to the GoPro
    pub async fn send_setting(&self, setting: &GoProSetting) -> Result<(), Box<dyn Error>> {
        self.send_setting_unchecked(setting).await?;
        let res = self.get_next_notification().await?;
        if res.is_none() {
            return Err("No response from GoPro".into());
        }
        let res = res.unwrap();
        if res.uuid != GPCharac::SettingsResponse.to_uuid() {
            return Err("Response from GoPro came from incorrect UUID".into());
        }
        if res.value != setting.response_value_bytes() {
            return Err("Response from GoPro was incorrect".into());
        }
        Ok(())
    }

    #[cfg(feature = "query")]
    ///Sends a query to the GoPro and returns the response
    /// 
    /// # Arguments
    /// * `query` - The query to send to the GoPro
    pub async fn query(&self, query: &GoProQuery) -> Result<QueryResponse, Box<dyn Error>> {
        let characteristics = self.device.characteristics();

        let query_write_char = characteristics
            .iter()
            .find(|c| c.uuid == GPCharac::Query.to_uuid())
            .unwrap();

        self.device
            .write(
                &query_write_char,
                query.as_bytes().as_ref(),
                WriteType::WithoutResponse,
            )
            .await?;

        let res = self.get_next_notification().await?;
        if res.is_none() {
            return Err("No response from GoPro".into());
        }
        let res = res.unwrap();
        if res.uuid != GPCharac::QueryResponse.to_uuid() {
            return Err("Response from GoPro came from incorrect UUID".into());
        }

        let query_response = QueryResponse::deserialize(&res.value)?;
        Ok(query_response)
    }

    ///Gets the next notification (response from a command) from the GoPro
    /// 
    /// # Returns
    /// * `Ok(Some(ValueNotification))` - If a notification was received
    /// * `Ok(None)` - If no notification was received
    /// * `Err(Box<dyn Error>)` - If an error occurred
    pub async fn get_next_notification(&self) -> Result<Option<ValueNotification>, Box<dyn Error>> {
        let mut response_stream = self.device.notifications().await?;
        let notification = response_stream.next().await;
        Ok(notification)
    }

    ///Disconnects the GoPro
    pub async fn disconnect(self) -> Result<(), Box<dyn Error>> {
        self.device.disconnect().await?;
        Ok(())
    }

    ///Disconnects the GoPro and powers it off
    /// 
    /// # Note
    /// 
    /// The camera will continue to send advertisement packets for 10 hours after being powered off
    /// allowing for an auto wake on reconnecting
    pub async fn disconnect_and_poweroff(self) -> Result<(), Box<dyn Error>> {
        self.send_command(GoProCommand::Sleep.as_ref()).await?;
        self.device.disconnect().await?;
        Ok(())
    }
}

///Inits the bluetooth adapter (central) and returns it to the caller
/// 
/// # Arguments
/// * `adapter_index` - An optional index into the list of bluetooth adapters in case the caller has more than one
pub async fn init(adapter_index: Option<usize>) -> Result<Adapter, Box<dyn Error>> {
    let manager = Manager::new().await.unwrap();

    //manage multiple adapters ?
    let index = adapter_index.unwrap_or(0);
    // get the first bluetooth adapter
    let adapters = manager.adapters().await?;

    if adapters.len() <= 0 {
        return Err("No Bluetooth Adapters".into());
    }

    let central = adapters.into_iter().nth(index).unwrap();
    Ok(central)
}

///Scans for GoPro devices and returns a list of their names
///(may also return previously connected devices some of which may not be GoPros)
/// 
/// # Arguments
/// * `central` - The bluetooth adapter to use for scanning
pub async fn scan(central: &mut Adapter) -> Result<Vec<String>, Box<dyn Error>> {
    // start scanning for devices
    let scan_filter = ScanFilter {
        services: vec![GoProServices::ControlAndQuery.to_uuid()],
    };

    central.start_scan(scan_filter).await?;

    let mut devices_names: Vec<String> = Vec::with_capacity(central.peripherals().await?.len());

    for p in central.peripherals().await? {
        let properties = p.properties().await?;
        let name = properties
            .unwrap()
            .local_name
            .unwrap_or("Unknown".to_string());
        devices_names.push(name);
    }
    Ok(devices_names)
}

///
///Connects to a GoPro device by name and returns a GoPro object if successful
/// 
/// # Arguments
/// * `gopro_local_name` - The name of the GoPro device to connect to
/// * `central` - The bluetooth adapter to use for connecting
pub async fn connect(
    gopro_local_name: String,
    central: &mut Adapter,
) -> Result<GoPro, Box<dyn Error>> {
    let device = filter_peripherals(central.peripherals().await?, gopro_local_name).await?;
    if device.is_none() {
        return Err("GoPro not found".into());
    }
    let device = device.unwrap();

    // connect to the device
    device.connect().await?;

    //discover all the services on the device
    device.discover_services().await?;

    //subscribe to the proper notify characteristics
    let characteristics = device.characteristics();

    if characteristics.len() == 0 {
        return Err("No characteristics found on this GoPro".into());
    }

    //Subscribe to all the characteristics that have the notify property
    //TODO: Send off subscriptions concurently ?
    for c in &characteristics {
        if c.properties.bits() == CharPropFlags::NOTIFY.bits() {
            device.subscribe(&c).await?;
        }
    }

    Ok(GoPro { device })
}

///Filters a list of peripherals by name and returns the first one that matches
async fn filter_peripherals(
    peripherals: Vec<Peripheral>,
    device_name: String,
) -> Result<Option<Peripheral>, Box<dyn Error>> {
    for p in peripherals {
        let properties = p.properties().await?;
        let name = properties
            .unwrap()
            .local_name
            .unwrap_or("Unknown".to_string());
        if name.eq(&device_name) {
            return Ok(Some(p));
        }
    }
    Ok(None)
}