first commit
This commit is contained in:
155
libgfps/examples/gfps_get_battery.rs
Normal file
155
libgfps/examples/gfps_get_battery.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
//! Simple example for receiving battery info via the GFPS RFCOMM channel.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --example gfps_get_battery -- <bluetooth-device-address>
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use bluer::{Address, Session, Device};
|
||||
use bluer::rfcomm::{Profile, ReqError, Role, ProfileHandle};
|
||||
|
||||
use futures::StreamExt;
|
||||
|
||||
use gfps::msg::{Codec, DeviceEventCode, EventGroup, BatteryInfo};
|
||||
|
||||
use num_enum::FromPrimitive;
|
||||
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> bluer::Result<()> {
|
||||
// handle command line arguments
|
||||
let addr = std::env::args().nth(1).expect("need device address as argument");
|
||||
let addr = Address::from_str(&addr)?;
|
||||
|
||||
// set up session
|
||||
let session = Session::new().await?;
|
||||
let adapter = session.default_adapter().await?;
|
||||
|
||||
// get device
|
||||
let dev = adapter.device(addr)?;
|
||||
|
||||
// get RFCOMM stream
|
||||
let stream = {
|
||||
// register GFPS profile
|
||||
let profile = Profile {
|
||||
uuid: gfps::msg::UUID,
|
||||
role: Some(Role::Client),
|
||||
require_authentication: Some(false),
|
||||
require_authorization: Some(false),
|
||||
auto_connect: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut profile_handle = session.register_profile(profile).await?;
|
||||
|
||||
// connect profile
|
||||
connect_device_to_profile(&mut profile_handle, &dev).await?
|
||||
};
|
||||
|
||||
// listen to event messages
|
||||
let codec = Codec::new();
|
||||
let mut stream = codec.wrap(stream);
|
||||
|
||||
// The battery status cannot be queried via a normal command. However, it
|
||||
// is sent right after we connect to the GFPS stream. In addition, multiple
|
||||
// events are often sent in sequence. Therefore we do the following:
|
||||
// - Set a deadline for a general timeout. If this passes, we just return
|
||||
// the current state (and if necessary "unknown"):
|
||||
// - Use a timestamp for checking whether we have received any new updates
|
||||
// in a given interval. If we have not received any, we consider the
|
||||
// state to be "settled" and return the battery info.
|
||||
// - On battery events we simply store the sent information. We retreive
|
||||
// the stored information once either of the timeouts kicks in.
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
|
||||
let mut timestamp = deadline;
|
||||
let mut bat_left = BatteryInfo::Unknown;
|
||||
let mut bat_right = BatteryInfo::Unknown;
|
||||
let mut bat_case = BatteryInfo::Unknown;
|
||||
|
||||
let time_settle = std::time::Duration::from_millis(500);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// receive and handle events
|
||||
msg = stream.next() => {
|
||||
match msg {
|
||||
Some(Ok(msg)) => {
|
||||
let group = EventGroup::from_primitive(msg.group);
|
||||
if group != EventGroup::Device {
|
||||
continue;
|
||||
}
|
||||
|
||||
let code = DeviceEventCode::from_primitive(msg.code);
|
||||
if code == DeviceEventCode::BatteryInfo {
|
||||
timestamp = std::time::Instant::now();
|
||||
|
||||
bat_left = BatteryInfo::from_byte(msg.data[0]);
|
||||
bat_right = BatteryInfo::from_byte(msg.data[1]);
|
||||
bat_case = BatteryInfo::from_byte(msg.data[2]);
|
||||
}
|
||||
},
|
||||
Some(Err(err)) => {
|
||||
Err(err)?;
|
||||
},
|
||||
None => {
|
||||
let err = std::io::Error::new(
|
||||
std::io::ErrorKind::ConnectionAborted,
|
||||
"connection closed"
|
||||
);
|
||||
|
||||
Err(err)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
// timeout for determining when the state has "settled"
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_millis(time_settle.as_millis() as _)) => {
|
||||
let delta = std::time::Instant::now() - timestamp;
|
||||
|
||||
if delta > time_settle {
|
||||
break
|
||||
}
|
||||
},
|
||||
// general deadline
|
||||
_ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {
|
||||
break
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
println!("Battery status:");
|
||||
println!(" left bud: {}", bat_left);
|
||||
println!(" right bud: {}", bat_right);
|
||||
println!(" case: {}", bat_case);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_device_to_profile(profile: &mut ProfileHandle, dev: &Device)
|
||||
-> bluer::Result<bluer::rfcomm::Stream>
|
||||
{
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = async {
|
||||
let _ = dev.connect().await;
|
||||
dev.connect_profile(&gfps::msg::UUID).await
|
||||
} => {
|
||||
if let Err(err) = res {
|
||||
println!("Connecting GFPS profile failed: {:?}", err);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(3000)).await;
|
||||
},
|
||||
req = profile.next() => {
|
||||
let req = req.expect("no connection request received");
|
||||
|
||||
if req.device() == dev.address() {
|
||||
// accept our device
|
||||
break req.accept();
|
||||
} else {
|
||||
// reject unknown devices
|
||||
req.reject(ReqError::Rejected);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
419
libgfps/examples/gfps_listen.rs
Normal file
419
libgfps/examples/gfps_listen.rs
Normal file
@@ -0,0 +1,419 @@
|
||||
//! Simple example for listening to GFPS messages sent via the RFCOMM channel.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --example gfps_listen -- <bluetooth-device-address>
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use bluer::{Address, Session, Device};
|
||||
use bluer::rfcomm::{Profile, ReqError, Role, ProfileHandle};
|
||||
|
||||
use futures::StreamExt;
|
||||
|
||||
use gfps::msg::{
|
||||
AcknowledgementEventCode, Codec, DeviceActionEventCode, DeviceCapabilitySyncEventCode,
|
||||
DeviceConfigurationEventCode, DeviceEventCode, EventGroup, Message, PlatformType,
|
||||
SassEventCode, LoggingEventCode, BluetoothEventCode, BatteryInfo,
|
||||
};
|
||||
|
||||
use num_enum::FromPrimitive;
|
||||
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> bluer::Result<()> {
|
||||
// handle command line arguments
|
||||
let addr = std::env::args().nth(1).expect("need device address as argument");
|
||||
let addr = Address::from_str(&addr)?;
|
||||
|
||||
// set up session
|
||||
let session = Session::new().await?;
|
||||
let adapter = session.default_adapter().await?;
|
||||
|
||||
println!("Using adapter '{}'", adapter.name());
|
||||
|
||||
// get device
|
||||
let dev = adapter.device(addr)?;
|
||||
let uuids = {
|
||||
let mut uuids = Vec::from_iter(dev.uuids().await?
|
||||
.unwrap_or_default()
|
||||
.into_iter());
|
||||
|
||||
uuids.sort_unstable();
|
||||
uuids
|
||||
};
|
||||
|
||||
println!("Found device:");
|
||||
println!(" alias: {}", dev.alias().await?);
|
||||
println!(" address: {}", dev.address());
|
||||
println!(" paired: {}", dev.is_paired().await?);
|
||||
println!(" connected: {}", dev.is_connected().await?);
|
||||
println!(" UUIDs:");
|
||||
for uuid in uuids {
|
||||
println!(" {}", uuid);
|
||||
}
|
||||
println!();
|
||||
|
||||
// try to reconnect if connection is reset
|
||||
loop {
|
||||
let stream = {
|
||||
// register GFPS profile
|
||||
println!("Registering GFPS profile...");
|
||||
|
||||
let profile = Profile {
|
||||
uuid: gfps::msg::UUID,
|
||||
role: Some(Role::Client),
|
||||
require_authentication: Some(false),
|
||||
require_authorization: Some(false),
|
||||
auto_connect: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut profile_handle = session.register_profile(profile).await?;
|
||||
|
||||
// connect profile
|
||||
println!("Connecting GFPS profile...");
|
||||
connect_device_to_profile(&mut profile_handle, &dev).await?
|
||||
};
|
||||
|
||||
println!("Profile connected");
|
||||
|
||||
// listen to event messages
|
||||
let codec = Codec::new();
|
||||
let mut stream = codec.wrap(stream);
|
||||
|
||||
println!("Listening...");
|
||||
println!();
|
||||
|
||||
while let Some(msg) = stream.next().await {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
print_message(&msg);
|
||||
}
|
||||
Err(e) if e.raw_os_error() == Some(104) => {
|
||||
// The Pixel Buds Pro can hand off processing between each
|
||||
// other. On a switch, the connection is reset. Wait a bit
|
||||
// and then try to reconnect.
|
||||
println!();
|
||||
println!("Connection reset. Attempting to reconnect...");
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_device_to_profile(profile: &mut ProfileHandle, dev: &Device)
|
||||
-> bluer::Result<bluer::rfcomm::Stream>
|
||||
{
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = async {
|
||||
let _ = dev.connect().await;
|
||||
dev.connect_profile(&gfps::msg::UUID).await
|
||||
} => {
|
||||
if let Err(err) = res {
|
||||
println!("Connecting GFPS profile failed: {:?}", err);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(3000)).await;
|
||||
},
|
||||
req = profile.next() => {
|
||||
let req = req.expect("no connection request received");
|
||||
|
||||
if req.device() == dev.address() {
|
||||
println!("Accepting request...");
|
||||
break req.accept();
|
||||
} else {
|
||||
println!("Rejecting unknown device {}", req.device());
|
||||
req.reject(ReqError::Rejected);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_message(msg: &Message) {
|
||||
let group = EventGroup::from_primitive(msg.group);
|
||||
|
||||
match group {
|
||||
EventGroup::Bluetooth => {
|
||||
let code = BluetoothEventCode::from_primitive(msg.code);
|
||||
|
||||
println!("Bluetooth (0x{:02X}) :: ", msg.group);
|
||||
|
||||
match code {
|
||||
BluetoothEventCode::EnableSilenceMode => {
|
||||
println!("Enable Silence Mode (0x{:02X})", msg.code);
|
||||
},
|
||||
BluetoothEventCode::DisableSilenceMode => {
|
||||
println!("Disable Silence Mode (0x{:02X})", msg.code);
|
||||
},
|
||||
_ => {
|
||||
println!("Unknown (0x{:02X})", msg.code);
|
||||
},
|
||||
}
|
||||
|
||||
print_message_body_unknown(msg);
|
||||
println!();
|
||||
}
|
||||
EventGroup::Logging => {
|
||||
let code = LoggingEventCode::from_primitive(msg.code);
|
||||
|
||||
println!("Companion App (0x{:02X}) :: ", msg.group);
|
||||
|
||||
match code {
|
||||
LoggingEventCode::LogFull => {
|
||||
println!("Log Full (0x{:02X})", msg.code);
|
||||
}
|
||||
LoggingEventCode::LogSaveToBuffer => {
|
||||
println!("Log Save Buffer (0x{:02X})", msg.code);
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown (0x{:02X})", msg.code);
|
||||
}
|
||||
}
|
||||
|
||||
print_message_body_unknown(msg);
|
||||
println!();
|
||||
}
|
||||
EventGroup::Device => {
|
||||
let code = DeviceEventCode::from_primitive(msg.code);
|
||||
|
||||
print!("Device Information (0x{:02X}) :: ", msg.group);
|
||||
|
||||
match code {
|
||||
DeviceEventCode::ModelId => {
|
||||
println!("Model Id (0x{:02X})", msg.code);
|
||||
println!(" model: {:02X}{:02X}{:02X}", msg.data[0], msg.data[1], msg.data[2]);
|
||||
}
|
||||
DeviceEventCode::BleAddress => {
|
||||
println!("BLE Address (0x{:02X})", msg.code);
|
||||
println!(" address: {}", Address::new(msg.data[0..6].try_into().unwrap()));
|
||||
}
|
||||
DeviceEventCode::BatteryInfo => {
|
||||
println!("Battery Info (0x{:02X})", msg.code);
|
||||
|
||||
let left = BatteryInfo::from_byte(msg.data[0]);
|
||||
let right = BatteryInfo::from_byte(msg.data[1]);
|
||||
let case = BatteryInfo::from_byte(msg.data[2]);
|
||||
|
||||
println!(" left bud: {}", left);
|
||||
println!(" right bud: {}", right);
|
||||
println!(" case: {}", case);
|
||||
}
|
||||
DeviceEventCode::BatteryTime => {
|
||||
println!("Remaining Battery Time (0x{:02X})", msg.code);
|
||||
|
||||
let time = match msg.data.len() {
|
||||
1 => msg.data[0] as u16,
|
||||
2 => u16::from_be_bytes(msg.data[0..2].try_into().unwrap()),
|
||||
_ => panic!("invalid format"),
|
||||
};
|
||||
|
||||
println!(" time: {} minutes", time);
|
||||
}
|
||||
DeviceEventCode::ActiveComponentsRequest => {
|
||||
println!("Active Components Request (0x{:02X})", msg.code);
|
||||
}
|
||||
DeviceEventCode::ActiveComponentsResponse => {
|
||||
println!("Active Components Response (0x{:02X})", msg.code);
|
||||
println!(" components: {:08b}", msg.data[0]);
|
||||
}
|
||||
DeviceEventCode::Capability => {
|
||||
println!("Capability (0x{:02X})", msg.code);
|
||||
println!(" capabilities: {:08b}", msg.data[0]);
|
||||
}
|
||||
DeviceEventCode::PlatformType => {
|
||||
println!("Platform Type (0x{:02X})", msg.code);
|
||||
|
||||
let platform = PlatformType::from_primitive(msg.data[0]);
|
||||
match platform {
|
||||
PlatformType::Android => {
|
||||
println!(" platform: Android (0x{:02X})", msg.data[0]);
|
||||
println!(" SDK version: {:02X?})", msg.data[1]);
|
||||
}
|
||||
_ => {
|
||||
println!(" platform: Unknown (0x{:02X})", msg.data[0]);
|
||||
println!(" platform data: 0x{:02X?})", msg.data[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
DeviceEventCode::FirmwareVersion => {
|
||||
println!("Firmware Version (0x{:02X})", msg.code);
|
||||
if let Ok(ver) = std::str::from_utf8(&msg.data) {
|
||||
println!(" version: {:?}", ver);
|
||||
} else {
|
||||
println!(" version: {:02X?}", msg.data);
|
||||
}
|
||||
}
|
||||
DeviceEventCode::SectionNonce => {
|
||||
println!("Session Nonce (0x{:02X})", msg.code);
|
||||
println!(" nonce: {:02X?}", msg.data);
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown (0x{:02X})", msg.code);
|
||||
print_message_body_unknown(msg);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
EventGroup::DeviceAction => {
|
||||
let code = DeviceActionEventCode::from_primitive(msg.code);
|
||||
|
||||
print!("Device Action (0x{:02X}) :: ", msg.group);
|
||||
|
||||
match code {
|
||||
DeviceActionEventCode::Ring => {
|
||||
println!("Ring (0x{:02X})", msg.code);
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown (0x{:02X})", msg.code);
|
||||
}
|
||||
}
|
||||
|
||||
print_message_body_unknown(msg);
|
||||
println!();
|
||||
}
|
||||
EventGroup::DeviceConfiguration => {
|
||||
let code = DeviceConfigurationEventCode::from_primitive(msg.code);
|
||||
|
||||
print!("Device Configuration (0x{:02X}) :: ", msg.group);
|
||||
|
||||
match code {
|
||||
DeviceConfigurationEventCode::BufferSize => {
|
||||
println!("Buffer Size (0x{:02X})", msg.code);
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown (0x{:02X})", msg.code);
|
||||
}
|
||||
}
|
||||
|
||||
print_message_body_unknown(msg);
|
||||
println!();
|
||||
}
|
||||
EventGroup::DeviceCapabilitySync => {
|
||||
let code = DeviceCapabilitySyncEventCode::from_primitive(msg.code);
|
||||
|
||||
print!("Device Cpabilities Sync (0x{:02X}) :: ", msg.group);
|
||||
|
||||
match code {
|
||||
DeviceCapabilitySyncEventCode::CapabilityUpdate => {
|
||||
println!("Capability Update (0x{:02X})", msg.code);
|
||||
}
|
||||
DeviceCapabilitySyncEventCode::ConfigurableBufferSizeRange => {
|
||||
println!("Configurable Buffer Size Range (0x{:02X})", msg.code);
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown (0x{:02X})", msg.code);
|
||||
}
|
||||
}
|
||||
|
||||
print_message_body_unknown(msg);
|
||||
println!();
|
||||
}
|
||||
EventGroup::SmartAudioSourceSwitching => {
|
||||
let code = SassEventCode::from_primitive(msg.code);
|
||||
|
||||
print!("Smart Audio Source Switching (0x{:02X}) :: ", msg.group);
|
||||
|
||||
match code {
|
||||
SassEventCode::GetCapabilityOfSass => {
|
||||
println!("Get Capability (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::NotifyCapabilityOfSass => {
|
||||
println!("Notify Capability (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::SetMultiPointState => {
|
||||
println!("Set Multi-Point State (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::SwitchAudioSourceBetweenConnectedDevices => {
|
||||
println!("Switch Audio Source Between Connected Devices (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::SwitchBack => {
|
||||
println!("Switch Back (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::NotifyMultiPointSwitchEvent => {
|
||||
println!("Notify Multi-Point (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::GetConnectionStatus => {
|
||||
println!("Get Connection Status (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::NotifyConnectionStatus => {
|
||||
println!("Notify Connection Status (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::SassInitiatedConnection => {
|
||||
println!("SASS Initiated Connection (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::IndicateInUseAccountKey => {
|
||||
println!("Indicate In-Use Account Key (0x{:02X})", msg.code);
|
||||
}
|
||||
SassEventCode::SetCustomData => {
|
||||
println!("Set Custom Data (0x{:02X})", msg.code);
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown (0x{:02X})", msg.code);
|
||||
}
|
||||
}
|
||||
|
||||
print_message_body_unknown(msg);
|
||||
println!();
|
||||
}
|
||||
EventGroup::Acknowledgement => {
|
||||
let code = AcknowledgementEventCode::from_primitive(msg.code);
|
||||
|
||||
print!("Acknowledgement (0x{:02X}) ::", msg.group);
|
||||
|
||||
match code {
|
||||
AcknowledgementEventCode::Ack => {
|
||||
println!("ACK (0x{:02X})", msg.code);
|
||||
println!(" group: 0x{:02X}", msg.data[0]);
|
||||
println!(" code: 0x{:02X}", msg.data[1]);
|
||||
println!();
|
||||
}
|
||||
AcknowledgementEventCode::Nak => {
|
||||
println!("NAK (0x{:02X})", msg.code);
|
||||
match msg.data[0] {
|
||||
0x00 => println!(" reason: Not supported (0x00)"),
|
||||
0x01 => println!(" reason: Device busy (0x01)"),
|
||||
0x02 => println!(" reason: Not allowed due to current state (0x02)"),
|
||||
_ => println!(" reason: Unknown (0x{:02X})", msg.data[0]),
|
||||
}
|
||||
println!(" group: 0x{:02X}", msg.data[1]);
|
||||
println!(" code: 0x{:02X}", msg.data[2]);
|
||||
println!();
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown (0x{:02X})", msg.code);
|
||||
print_message_body_unknown(msg);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"Unknown (0x{:02X}) :: Unknown (0x{:02X})",
|
||||
msg.group, msg.code
|
||||
);
|
||||
print_message_body_unknown(msg);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_message_body_unknown(msg: &Message) {
|
||||
let data = pretty_hex::config_hex(
|
||||
&msg.data,
|
||||
pretty_hex::HexConfig {
|
||||
title: false,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
for line in data.lines() {
|
||||
println!(" {}", line);
|
||||
}
|
||||
}
|
||||
241
libgfps/examples/ring.rs
Normal file
241
libgfps/examples/ring.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
//! Simple example for "ringing" the buds to locate them.
|
||||
//!
|
||||
//! WARNING: DO NOT RUN THIS EXAMPLE WITH THE BUDS IN YOUR EAR! YOU HAVE BEEN WARNED.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --example ring -- <bluetooth-device-address>
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use bluer::{Address, Session, Device};
|
||||
use bluer::rfcomm::{Profile, Role, ProfileHandle, ReqError};
|
||||
|
||||
use futures::{StreamExt, SinkExt};
|
||||
|
||||
use gfps::msg::{Codec, Message, EventGroup, DeviceActionEventCode, AcknowledgementEventCode};
|
||||
|
||||
use num_enum::FromPrimitive;
|
||||
|
||||
use smallvec::smallvec;
|
||||
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> bluer::Result<()> {
|
||||
// handle command line arguments
|
||||
let addr = std::env::args().nth(1).expect("need device address as argument");
|
||||
let addr = Address::from_str(&addr)?;
|
||||
|
||||
// set up session
|
||||
let session = Session::new().await?;
|
||||
let adapter = session.default_adapter().await?;
|
||||
|
||||
// get device
|
||||
let dev = adapter.device(addr)?;
|
||||
|
||||
// get RFCOMM stream
|
||||
let stream = {
|
||||
// register GFPS profile
|
||||
let profile = Profile {
|
||||
uuid: gfps::msg::UUID,
|
||||
role: Some(Role::Client),
|
||||
require_authentication: Some(false),
|
||||
require_authorization: Some(false),
|
||||
auto_connect: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut profile_handle = session.register_profile(profile).await?;
|
||||
|
||||
// connect profile
|
||||
connect_device_to_profile(&mut profile_handle, &dev).await?
|
||||
};
|
||||
|
||||
// set up message stream
|
||||
let codec = Codec::new();
|
||||
let mut stream = codec.wrap(stream);
|
||||
|
||||
// send "ring" message
|
||||
//
|
||||
// Note: Pixel Buds Pro ignore messages with a timeout. So don't specify
|
||||
// one here.
|
||||
let msg = Message {
|
||||
group: EventGroup::DeviceAction.into(),
|
||||
code: DeviceActionEventCode::Ring.into(),
|
||||
data: smallvec![0x03], // 0b01: right, 0b10: left, 0b10|0b01 = 0b11: both
|
||||
};
|
||||
|
||||
println!("Ringing buds...");
|
||||
stream.send(&msg).await?;
|
||||
|
||||
// An ACK message should come in 1s. Wait for that.
|
||||
let timeout = tokio::time::Instant::now() + tokio::time::Duration::from_secs(1);
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
match msg {
|
||||
Some(Ok(msg)) => {
|
||||
println!("{:?}", msg);
|
||||
|
||||
let group = EventGroup::from_primitive(msg.group);
|
||||
if group != EventGroup::Acknowledgement {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ack_group = EventGroup::from_primitive(msg.data[0]);
|
||||
if ack_group != EventGroup::DeviceAction {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ack_code = DeviceActionEventCode::from_primitive(msg.data[1]);
|
||||
if ack_code != DeviceActionEventCode::Ring {
|
||||
continue;
|
||||
}
|
||||
|
||||
let code = AcknowledgementEventCode::from_primitive(msg.code);
|
||||
|
||||
if code == AcknowledgementEventCode::Ack {
|
||||
println!("Received ACK for ring command");
|
||||
break;
|
||||
|
||||
} else if code == AcknowledgementEventCode::Nak {
|
||||
println!("Received NAK for ring command");
|
||||
|
||||
let err = std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"ring has been NAK'ed by device"
|
||||
);
|
||||
|
||||
Err(err)?;
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => {
|
||||
Err(e)?;
|
||||
},
|
||||
None => {
|
||||
let err = std::io::Error::new(
|
||||
std::io::ErrorKind::ConnectionAborted,
|
||||
"connection closed"
|
||||
);
|
||||
|
||||
Err(err)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ = tokio::time::sleep_until(timeout) => {
|
||||
let err = std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"timed out, ring action might be unsupported"
|
||||
);
|
||||
|
||||
Err(err)?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Next, the device will communicate back status updates. This may include
|
||||
// an initial update to confirm ringing and follow-up updates once the user
|
||||
// has touched the buds and ringing stops.
|
||||
//
|
||||
// Stop this program once we have no more rining or once we have reached a
|
||||
// timeout of 30s.
|
||||
let mut timeout = tokio::time::Instant::now() + tokio::time::Duration::from_secs(30);
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
match msg {
|
||||
Some(Ok(msg)) => {
|
||||
println!("{:?}", msg);
|
||||
|
||||
let group = EventGroup::from_primitive(msg.group);
|
||||
if group != EventGroup::DeviceAction {
|
||||
continue;
|
||||
}
|
||||
// send ACK
|
||||
let ack = Message {
|
||||
group: EventGroup::Acknowledgement.into(),
|
||||
code: AcknowledgementEventCode::Ack.into(),
|
||||
data: smallvec![msg.group, msg.code],
|
||||
};
|
||||
|
||||
stream.send(&ack).await?;
|
||||
|
||||
let status = msg.data[0];
|
||||
|
||||
println!("Received ring update:");
|
||||
|
||||
if status & 0b01 != 0 {
|
||||
println!(" right: ringing");
|
||||
} else {
|
||||
println!(" right: not ringing");
|
||||
}
|
||||
|
||||
if status & 0b10 != 0 {
|
||||
println!(" left: ringing");
|
||||
} else {
|
||||
println!(" left: not ringing");
|
||||
}
|
||||
|
||||
if status & 0b11 == 0 {
|
||||
println!("Buds stopped ringing, exiting...");
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Some(Err(e)) => {
|
||||
Err(e)?;
|
||||
},
|
||||
None => {
|
||||
let err = std::io::Error::new(
|
||||
std::io::ErrorKind::ConnectionAborted,
|
||||
"connection closed"
|
||||
);
|
||||
|
||||
Err(err)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ = tokio::time::sleep_until(timeout) => {
|
||||
println!("Sending command to stop ringing...");
|
||||
|
||||
// send message to stop ringing
|
||||
let msg = Message {
|
||||
group: EventGroup::DeviceAction.into(),
|
||||
code: DeviceActionEventCode::Ring.into(),
|
||||
data: smallvec![0x00],
|
||||
};
|
||||
|
||||
stream.send(&msg).await?;
|
||||
|
||||
timeout = tokio::time::Instant::now() + tokio::time::Duration::from_secs(10);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_device_to_profile(profile: &mut ProfileHandle, dev: &Device)
|
||||
-> bluer::Result<bluer::rfcomm::Stream>
|
||||
{
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = async {
|
||||
let _ = dev.connect().await;
|
||||
dev.connect_profile(&gfps::msg::UUID).await
|
||||
} => {
|
||||
if let Err(err) = res {
|
||||
println!("Connecting GFPS profile failed: {:?}", err);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(3000)).await;
|
||||
},
|
||||
req = profile.next() => {
|
||||
let req = req.expect("no connection request received");
|
||||
|
||||
if req.device() == dev.address() {
|
||||
// accept our device
|
||||
break req.accept();
|
||||
} else {
|
||||
// reject unknown devices
|
||||
req.reject(ReqError::Rejected);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user