first commit
This commit is contained in:
22
libgfps/Cargo.toml
Normal file
22
libgfps/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "gfps"
|
||||
authors = ["Maximilian Luz <m@mxnluz.io>"]
|
||||
version = "0.1.3"
|
||||
edition = "2024"
|
||||
license = "MIT/Apache-2.0"
|
||||
description = "Google Fast Pair Service (GFPS) protocol client library"
|
||||
repository = "https://github.com/qzed/pbpctrl"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.10.1"
|
||||
num_enum = "0.7.3"
|
||||
smallvec = { version = "1.15.0", features = ["union"] }
|
||||
tokio = "1.44.2"
|
||||
tokio-util = { version = "0.7.14", features = ["codec"] }
|
||||
uuid = "1.16.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bluer = { version = "0.17.3", features = ["bluetoothd", "rfcomm"] }
|
||||
futures = "0.3.31"
|
||||
pretty-hex = "0.4.1"
|
||||
tokio = { version = "1.44.2", features = ["rt", "macros"] }
|
||||
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);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
6
libgfps/src/lib.rs
Normal file
6
libgfps/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! Library for the Google Fast Pair Service protocol (GFPS). Focussed on
|
||||
//! communication via the dedicated GFPS RFCOMM channel.
|
||||
//!
|
||||
//! See <https://developers.google.com/nearby/fast-pair> for the specification.
|
||||
|
||||
pub mod msg;
|
||||
184
libgfps/src/msg/codec.rs
Normal file
184
libgfps/src/msg/codec.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use super::Message;
|
||||
|
||||
use bytes::{Buf, BytesMut, BufMut};
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_util::codec::{Decoder, Framed, Encoder};
|
||||
|
||||
|
||||
const MAX_FRAME_SIZE: u16 = 4096;
|
||||
|
||||
|
||||
pub struct Codec {}
|
||||
|
||||
impl Codec {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub fn wrap<T>(self, io: T) -> Framed<T, Codec>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
{
|
||||
Framed::with_capacity(io, self, MAX_FRAME_SIZE as _)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Codec {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for Codec {
|
||||
type Item = Message;
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if src.len() < 4 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let group = src[0];
|
||||
let code = src[1];
|
||||
|
||||
let mut length = [0; 2];
|
||||
length.copy_from_slice(&src[2..4]);
|
||||
let length = u16::from_be_bytes(length);
|
||||
|
||||
if length > MAX_FRAME_SIZE {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Frame of length {length} is too large (group: {group}, code: {code})."),
|
||||
))?;
|
||||
}
|
||||
|
||||
let size = 4 + length as usize;
|
||||
|
||||
if src.len() < size as _ {
|
||||
src.reserve(size - src.len());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let data = src[4..size].into();
|
||||
src.advance(size);
|
||||
|
||||
Ok(Some(Message {
|
||||
group,
|
||||
code,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<&Message> for Codec {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn encode(&mut self, msg: &Message, buf: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
let size = msg.data.len() + 4;
|
||||
|
||||
if size > MAX_FRAME_SIZE as usize {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Frame of length {size} is too large."),
|
||||
))?;
|
||||
}
|
||||
|
||||
buf.reserve(size);
|
||||
buf.put_u8(msg.group);
|
||||
buf.put_u8(msg.code);
|
||||
buf.put_slice(&(msg.data.len() as u16).to_be_bytes());
|
||||
buf.put_slice(&msg.data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::msg::{EventGroup, DeviceEventCode, Message};
|
||||
|
||||
use bytes::BytesMut;
|
||||
|
||||
use smallvec::smallvec;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_encode() {
|
||||
let mut buf = BytesMut::new();
|
||||
let mut codec = Codec::new();
|
||||
|
||||
let msg = Message {
|
||||
group: EventGroup::Device.into(),
|
||||
code: DeviceEventCode::ModelId.into(),
|
||||
data: smallvec![0x00, 0x01, 0x02, 0x04, 0x05],
|
||||
};
|
||||
|
||||
// try to encode the message
|
||||
codec.encode(&msg, &mut buf)
|
||||
.expect("error encode message");
|
||||
|
||||
let raw = [0x03, 0x01, 0x00, 0x05, 0x00, 0x01, 0x02, 0x04, 0x05];
|
||||
assert_eq!(&buf[..], &raw[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let mut codec = Codec::new();
|
||||
|
||||
let raw = [0x03, 0x01, 0x00, 0x03, 0x00, 0x01, 0x02];
|
||||
let mut buf = BytesMut::from(&raw[..]);
|
||||
|
||||
let msg = Message {
|
||||
group: EventGroup::Device.into(),
|
||||
code: DeviceEventCode::ModelId.into(),
|
||||
data: smallvec![0x00, 0x01, 0x02],
|
||||
};
|
||||
|
||||
// try to encode the message
|
||||
let decoded = codec.decode(&mut buf)
|
||||
.expect("error decoding message")
|
||||
.expect("message incomplete");
|
||||
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_incomplete() {
|
||||
let mut codec = Codec::new();
|
||||
|
||||
let raw = [0x03, 0x01, 0x00, 0x03, 0x00];
|
||||
let mut buf = BytesMut::from(&raw[..]);
|
||||
|
||||
// try to encode the message
|
||||
let decoded = codec.decode(&mut buf)
|
||||
.expect("error decoding message");
|
||||
|
||||
assert_eq!(decoded, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode() {
|
||||
let mut buf = BytesMut::new();
|
||||
let mut codec = Codec::new();
|
||||
|
||||
let msg = Message {
|
||||
group: 0,
|
||||
code: 0,
|
||||
data: smallvec![0x00, 0x01, 0x02],
|
||||
};
|
||||
|
||||
// try to encode the message
|
||||
codec.encode(&msg, &mut buf)
|
||||
.expect("error encode message");
|
||||
|
||||
// try to decode the message we just encoded
|
||||
let decoded = codec.decode(&mut buf)
|
||||
.expect("error decoding message")
|
||||
.expect("message incomplete");
|
||||
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
}
|
||||
14
libgfps/src/msg/mod.rs
Normal file
14
libgfps/src/msg/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
//! Types for GFPS Message Stream via RFCOMM.
|
||||
|
||||
use uuid::{uuid, Uuid};
|
||||
|
||||
/// UUID under which the GFPS Message Stream is advertised.
|
||||
///
|
||||
/// Defined as `df21fe2c-2515-4fdb-8886-f12c4d67927c`.
|
||||
pub const UUID: Uuid = uuid!("df21fe2c-2515-4fdb-8886-f12c4d67927c");
|
||||
|
||||
mod codec;
|
||||
pub use codec::Codec;
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
194
libgfps/src/msg/types.rs
Normal file
194
libgfps/src/msg/types.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! RFCOMM events and event-related enums.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use num_enum::{IntoPrimitive, FromPrimitive};
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
pub group: u8,
|
||||
pub code: u8,
|
||||
pub data: SmallVec<[u8; 8]>,
|
||||
}
|
||||
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum EventGroup {
|
||||
Bluetooth = 0x01,
|
||||
Logging = 0x02,
|
||||
Device = 0x03,
|
||||
DeviceAction = 0x04,
|
||||
DeviceConfiguration = 0x05,
|
||||
DeviceCapabilitySync = 0x06,
|
||||
SmartAudioSourceSwitching = 0x07,
|
||||
Acknowledgement = 0xff,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8) = 0xfe,
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum BluetoothEventCode {
|
||||
EnableSilenceMode = 0x01,
|
||||
DisableSilenceMode = 0x02,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum LoggingEventCode {
|
||||
LogFull = 0x01,
|
||||
LogSaveToBuffer = 0x02,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum DeviceEventCode {
|
||||
ModelId = 0x01,
|
||||
BleAddress = 0x02,
|
||||
BatteryInfo = 0x03,
|
||||
BatteryTime = 0x04,
|
||||
ActiveComponentsRequest = 0x05,
|
||||
ActiveComponentsResponse = 0x06,
|
||||
Capability = 0x07,
|
||||
PlatformType = 0x08,
|
||||
FirmwareVersion = 0x09,
|
||||
SectionNonce = 0x0a,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum DeviceActionEventCode {
|
||||
Ring = 0x01,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum DeviceConfigurationEventCode {
|
||||
BufferSize = 0x01,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum DeviceCapabilitySyncEventCode {
|
||||
CapabilityUpdate = 0x01,
|
||||
ConfigurableBufferSizeRange = 0x02,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum SassEventCode {
|
||||
GetCapabilityOfSass = 0x10,
|
||||
NotifyCapabilityOfSass = 0x11,
|
||||
SetMultiPointState = 0x12,
|
||||
SwitchAudioSourceBetweenConnectedDevices = 0x30,
|
||||
SwitchBack = 0x31,
|
||||
NotifyMultiPointSwitchEvent = 0x32,
|
||||
GetConnectionStatus = 0x33,
|
||||
NotifyConnectionStatus = 0x34,
|
||||
SassInitiatedConnection = 0x40,
|
||||
IndicateInUseAccountKey = 0x41,
|
||||
SetCustomData = 0x42,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum AcknowledgementEventCode {
|
||||
Ack = 0x01,
|
||||
Nak = 0x02,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, FromPrimitive)]
|
||||
pub enum PlatformType {
|
||||
Android = 0x01,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum BatteryInfo {
|
||||
#[default]
|
||||
Unknown,
|
||||
Known {
|
||||
is_charging: bool,
|
||||
percent: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl BatteryInfo {
|
||||
pub fn from_byte(value: u8) -> Self {
|
||||
if value & 0x7F == 0x7F {
|
||||
BatteryInfo::Unknown
|
||||
} else {
|
||||
BatteryInfo::Known {
|
||||
is_charging: (value & 0x80) != 0,
|
||||
percent: value & 0x7F,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_byte(&self) -> u8 {
|
||||
match self {
|
||||
BatteryInfo::Unknown => 0xFF,
|
||||
BatteryInfo::Known { is_charging: true, percent } => 0x80 | (0x7F & percent),
|
||||
BatteryInfo::Known { is_charging: false, percent } => 0x7F & percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BatteryInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
BatteryInfo::Unknown => {
|
||||
write!(f, "unknown")
|
||||
}
|
||||
BatteryInfo::Known { is_charging: true, percent } => {
|
||||
write!(f, "{percent}% (charging)")
|
||||
}
|
||||
BatteryInfo::Known { is_charging: false, percent } => {
|
||||
write!(f, "{percent}% (not charging)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user