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;
pub struct GoPro {
device: Peripheral,
}
impl 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(())
}
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(())
}
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(())
}
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")]
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)
}
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)
}
pub async fn disconnect(self) -> Result<(), Box<dyn Error>> {
self.device.disconnect().await?;
Ok(())
}
pub async fn disconnect_and_poweroff(self) -> Result<(), Box<dyn Error>> {
self.send_command(GoProCommand::Sleep.as_ref()).await?;
self.device.disconnect().await?;
Ok(())
}
}
pub async fn init(adapter_index: Option<usize>) -> Result<Adapter, Box<dyn Error>> {
let manager = Manager::new().await.unwrap();
let index = adapter_index.unwrap_or(0);
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)
}
pub async fn scan(central: &mut Adapter) -> Result<Vec<String>, Box<dyn Error>> {
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)
}
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();
device.connect().await?;
device.discover_services().await?;
let characteristics = device.characteristics();
if characteristics.len() == 0 {
return Err("No characteristics found on this GoPro".into());
}
for c in &characteristics {
if c.properties.bits() == CharPropFlags::NOTIFY.bits() {
device.subscribe(&c).await?;
}
}
Ok(GoPro { device })
}
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)
}