first commit
Some checks failed
Rust / Clippy (push) Has been cancelled
Rust / Test (nightly) (push) Has been cancelled
Rust / Test (stable) (push) Has been cancelled

This commit is contained in:
2026-01-04 16:50:19 +08:00
commit 6675986579
60 changed files with 11043 additions and 0 deletions

View 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);
}
},
}
}
}

View 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
View 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);
}
},
}
}
}