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

22
libgfps/Cargo.toml Normal file
View 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"] }

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

6
libgfps/src/lib.rs Normal file
View 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
View 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
View 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
View 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)")
}
}
}
}