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

24
cli/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "pbpctrl"
authors = ["Maximilian Luz <m@mxnluz.io>"]
version = "0.1.8"
edition = "2024"
license = "MIT/Apache-2.0"
description = "Command-line utility for controlling Google Pixel Buds Pro"
repository = "https://github.com/qzed/pbpctrl"
[dependencies]
anyhow = "1.0.98"
bluer = { version = "0.17.4", features = ["bluetoothd", "rfcomm"] }
clap = { version = "4.5.37", features = ["derive"] }
futures = "0.3.31"
maestro = { path = "../libmaestro" }
tokio = { version = "1.44.2", features = ["rt", "macros", "signal"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
[build-dependencies]
bluer = { version = "0.17.4" }
clap = { version = "4.5.37", features = ["derive"] }
clap_complete = "4.5.47"
maestro = { path = "../libmaestro" }

21
cli/build.rs Normal file
View File

@@ -0,0 +1,21 @@
use std::env;
use clap::CommandFactory;
use clap_complete::shells;
#[allow(dead_code)]
#[path = "src/cli.rs"]
mod cli;
use cli::*;
fn main() {
let outdir = env::var_os("CARGO_TARGET_DIR")
.or_else(|| env::var_os("OUT_DIR"))
.unwrap();
let mut cmd = Args::command();
clap_complete::generate_to(shells::Bash, &mut cmd, "pbpctrl", &outdir).unwrap();
clap_complete::generate_to(shells::Zsh, &mut cmd, "pbpctrl", &outdir).unwrap();
clap_complete::generate_to(shells::Fish, &mut cmd, "pbpctrl", &outdir).unwrap();
}

90
cli/src/bt.rs Normal file
View File

@@ -0,0 +1,90 @@
use std::time::Duration;
use anyhow::Result;
use bluer::{Adapter, Address, Device, Session};
use bluer::rfcomm::{ProfileHandle, Role, ReqError, Stream, Profile};
use futures::StreamExt;
const PIXEL_BUDS_CLASS: u32 = 0x240404;
const PIXEL_BUDS2_CLASS: u32 = 0x244404;
pub async fn find_maestro_device(adapter: &Adapter) -> Result<Device> {
for addr in adapter.device_addresses().await? {
let dev = adapter.device(addr)?;
let class = dev.class().await?.unwrap_or(0);
if class != PIXEL_BUDS_CLASS && class != PIXEL_BUDS2_CLASS {
continue;
}
let uuids = dev.uuids().await?.unwrap_or_default();
if !uuids.contains(&maestro::UUID) {
continue;
}
tracing::debug!(address=%addr, "found compatible device");
return Ok(dev);
}
tracing::debug!("no compatible device found");
anyhow::bail!("no compatible device found")
}
pub async fn connect_maestro_rfcomm(session: &Session, dev: &Device) -> Result<Stream> {
let maestro_profile = Profile {
uuid: maestro::UUID,
role: Some(Role::Client),
require_authentication: Some(false),
require_authorization: Some(false),
auto_connect: Some(false),
..Default::default()
};
tracing::debug!("registering maestro profile");
let mut handle = session.register_profile(maestro_profile).await?;
tracing::debug!("connecting to maestro profile");
let stream = tokio::try_join!(
try_connect_profile(dev),
handle_requests_for_profile(&mut handle, dev.address()),
)?.1;
Ok(stream)
}
async fn try_connect_profile(dev: &Device) -> Result<()> {
const RETRY_TIMEOUT: Duration = Duration::from_secs(1);
const MAX_TRIES: u32 = 3;
let mut i = 0;
while let Err(err) = dev.connect_profile(&maestro::UUID).await {
if i >= MAX_TRIES { return Err(err.into()) }
i += 1;
tracing::warn!(error=?err, "connecting to profile failed, trying again ({}/{})", i, MAX_TRIES);
tokio::time::sleep(RETRY_TIMEOUT).await;
}
tracing::debug!(address=%dev.address(), "maestro profile connected");
Ok(())
}
async fn handle_requests_for_profile(handle: &mut ProfileHandle, address: Address) -> Result<Stream> {
while let Some(req) = handle.next().await {
tracing::debug!(address=%req.device(), "received new profile connection request");
if req.device() == address {
tracing::debug!(address=%req.device(), "accepting profile connection request");
return Ok(req.accept()?);
} else {
req.reject(ReqError::Rejected);
}
}
anyhow::bail!("profile terminated without requests")
}

317
cli/src/cli.rs Normal file
View File

@@ -0,0 +1,317 @@
use bluer::Address;
use clap::{Parser, Subcommand, ValueEnum};
use maestro::service::settings;
/// Control Google Pixel Buds Pro from the command line
#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
pub struct Args {
/// Device to use (search for compatible device if unspecified)
#[arg(short, long, global=true)]
pub device: Option<Address>,
#[command(subcommand)]
pub command: Command
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Show device information
Show {
#[command(subcommand)]
command: ShowCommand
},
/// Read settings value
Get {
#[command(subcommand)]
setting: GetSetting
},
/// Write settings value
Set {
#[command(subcommand)]
setting: SetSetting
},
}
#[derive(Debug, Subcommand)]
pub enum ShowCommand {
/// Show software information.
Software,
/// Show hardware information.
Hardware,
/// Show runtime information.
Runtime,
/// Show battery status.
Battery,
}
#[derive(Debug, Subcommand)]
pub enum GetSetting {
/// Get automatic over-the-air update status
AutoOta,
/// Get on-head-detection state (enabled/disabled)
Ohd,
/// Get the flag indicating whether the out-of-box experience phase is
/// finished
OobeIsFinished,
/// Get gesture state (enabled/disabled)
Gestures,
/// Get diagnostics state (enabled/disabled)
Diagnostics,
/// Get out-of-box-experience mode state (enabled/disabled)
OobeMode,
/// Get hold-gesture action
GestureControl,
/// Get multipoint audio state (enabled/disabled)
Multipoint,
/// Get adaptive noise-cancelling gesture loop
AncGestureLoop,
/// Get adaptive noise-cancelling state
Anc,
/// Get volume-dependent EQ state (enabled/disabled)
VolumeEq,
/// Get 5-band EQ
Eq,
/// Get volume balance
Balance,
/// Get mono output state
Mono,
/// Get volume exposure notifications state (enabled/disabled)
VolumeExposureNotifications,
/// Get automatic transparency mode state (enabled/disabled)
SpeechDetection,
}
#[derive(Debug, Subcommand)]
pub enum SetSetting {
/// Enable/disable automatic over-the-air updates
///
/// Note: Updates are initiated by the Google Buds app on your phone. This
/// flag controls whether updates can be done automatically when the device
/// is not in use.
AutoOta {
/// Whether to enable or disable automatic over-the-air (OTA) updates
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Enable/disable on-head detection
Ohd {
/// Whether to enable or disable on-head detection
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Set the flag indicating whether the out-of-box experience phase is
/// finished
///
/// Note: You normally do not want to change this flag. It is used to
/// indicate whether the out-of-box experience (OOBE) phase has been
/// concluded, i.e., the setup wizard has been run and the device has been
/// set up.
OobeIsFinished {
/// Whether the OOBE setup has been finished
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Enable/disable gestures
Gestures {
/// Whether to enable or disable gestures
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Enable/disable diagnostics
///
/// Note: This will also cause the Google Buds app on your phone to send
/// diagnostics data to Google.
Diagnostics {
/// Whether to enable or disable diagnostics
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Enable/disable out-of-box-experience mode
///
/// Note: You normally do not want to enable this mode. It is used to
/// intercept and block touch gestures during the setup wizard.
OobeMode {
/// Whether to enable or disable the out-of-box experience mode
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Set hold-gesture action
GestureControl {
/// Left gesture action
#[arg(value_enum)]
left: HoldGestureAction,
/// Right gesture action
#[arg(value_enum)]
right: HoldGestureAction,
},
/// Enable/disable multipoint audio
Multipoint {
/// Whether to enable or disable multipoint audio
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Set adaptive noise-cancelling gesture loop
AncGestureLoop {
/// Enable 'off' mode in loop
#[arg(action=clap::ArgAction::Set)]
off: bool,
/// Enable 'active' mode in loop
#[arg(action=clap::ArgAction::Set)]
active: bool,
/// Enable 'aware' mode in loop
#[arg(action=clap::ArgAction::Set)]
aware: bool,
/// Enable 'adaptive' mode in loop
#[arg(action=clap::ArgAction::Set)]
adaptive: bool,
},
/// Set adaptive noise-cancelling state
Anc {
/// New ANC state or action to change state
#[arg(value_enum)]
value: AncState,
},
/// Enable/disable volume-dependent EQ
VolumeEq {
/// Whether to enable or disable volume-dependent EQ
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Set 5-band EQ
Eq {
/// Low-bass band (min: -6.0, max: 6.0)
#[arg(value_parser=parse_eq_value)]
low_bass: f32,
/// Bass band (min: -6.0, max: 6.0)
#[arg(value_parser=parse_eq_value)]
bass: f32,
/// Mid band (min: -6.0, max: 6.0)
#[arg(value_parser=parse_eq_value)]
mid: f32,
/// Treble band (min: -6.0, max: 6.0)
#[arg(value_parser=parse_eq_value)]
treble: f32,
/// Upper treble band (min: -6.0, max: 6.0)
#[arg(value_parser=parse_eq_value)]
upper_treble: f32,
},
/// Set volume balance
Balance {
/// Volume balance from -100 (left) to +100 (right)
#[arg(value_parser=parse_balance)]
value: i32,
},
/// Set mono output
Mono {
/// Whether to force mono output
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Enable/disable volume level exposure notifications
VolumeExposureNotifications {
/// Whether to enable or disable volume level exposure notifications
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
/// Enable/disable automatic transparency mode via speech detection
SpeechDetection {
/// Whether to enable or disable the automatic transparency mode via speech detection
#[arg(action=clap::ArgAction::Set)]
value: bool,
},
}
#[derive(Debug, ValueEnum, Clone, Copy)]
pub enum AncState {
Off,
Active,
Aware,
Adaptive,
CycleNext,
CyclePrev,
}
#[derive(Debug, ValueEnum, Clone, Copy)]
pub enum HoldGestureAction {
Anc,
Assistant,
}
impl From<HoldGestureAction> for settings::RegularActionTarget {
fn from(value: HoldGestureAction) -> Self {
match value {
HoldGestureAction::Anc => settings::RegularActionTarget::AncControl,
HoldGestureAction::Assistant => settings::RegularActionTarget::AssistantQuery,
}
}
}
fn parse_eq_value(s: &str) -> std::result::Result<f32, String> {
let val = s.parse().map_err(|e| format!("{e}"))?;
if val > settings::EqBands::MAX_VALUE {
Err(format!("exceeds maximum of {}", settings::EqBands::MAX_VALUE))
} else if val < settings::EqBands::MIN_VALUE {
Err(format!("exceeds minimum of {}", settings::EqBands::MIN_VALUE))
} else {
Ok(val)
}
}
fn parse_balance(s: &str) -> std::result::Result<i32, String> {
let val = s.parse().map_err(|e| format!("{e}"))?;
if val > 100 {
Err("exceeds maximum of 100".to_string())
} else if val < -100 {
Err("exceeds minimum of -100".to_string())
} else {
Ok(val)
}
}

506
cli/src/main.rs Normal file
View File

@@ -0,0 +1,506 @@
mod bt;
mod cli;
use anyhow::Result;
use clap::{Parser, CommandFactory};
use futures::{Future, StreamExt};
use maestro::protocol::{utils, addr};
use maestro::pwrpc::client::{Client, ClientHandle};
use maestro::protocol::codec::Codec;
use maestro::service::MaestroService;
use maestro::service::settings::{self, Setting, SettingValue};
use cli::*;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse();
// set up session
let session = bluer::Session::new().await?;
let adapter = session.default_adapter().await?;
// set up device
let dev = if let Some(address) = args.device {
tracing::debug!("using provided address: {}", address);
adapter.device(address)?
} else {
tracing::debug!("no device specified, searching for compatible one");
bt::find_maestro_device(&adapter).await?
};
// set up profile
let stream = bt::connect_maestro_rfcomm(&session, &dev).await?;
// set up codec
let codec = Codec::new();
let stream = codec.wrap(stream);
// set up RPC client
let mut client = Client::new(stream);
let handle = client.handle();
// resolve channel
let channel = utils::resolve_channel(&mut client).await?;
match args.command {
Command::Show { command } => match command {
ShowCommand::Software => run(client, cmd_show_software(handle, channel)).await,
ShowCommand::Hardware => run(client, cmd_show_hardware(handle, channel)).await,
ShowCommand::Runtime => run(client, cmd_show_runtime(handle, channel)).await,
ShowCommand::Battery => run(client, cmd_show_battery(handle, channel)).await,
},
Command::Get { setting } => match setting {
GetSetting::AutoOta => {
run(client, cmd_get_setting(handle, channel, settings::id::AutoOtaEnable)).await
},
GetSetting::Ohd => {
run(client, cmd_get_setting(handle, channel, settings::id::OhdEnable)).await
},
GetSetting::OobeIsFinished => {
run(client, cmd_get_setting(handle, channel, settings::id::OobeIsFinished)).await
},
GetSetting::Gestures => {
run(client, cmd_get_setting(handle, channel, settings::id::GestureEnable)).await
},
GetSetting::Diagnostics => {
run(client, cmd_get_setting(handle, channel, settings::id::DiagnosticsEnable)).await
}
GetSetting::OobeMode => {
run(client, cmd_get_setting(handle, channel, settings::id::OobeMode)).await
},
GetSetting::GestureControl => {
run(client, cmd_get_setting(handle, channel, settings::id::GestureControl)).await
},
GetSetting::Multipoint => {
run(client, cmd_get_setting(handle, channel, settings::id::MultipointEnable)).await
},
GetSetting::AncGestureLoop => {
run(client, cmd_get_setting(handle, channel, settings::id::AncrGestureLoop)).await
}
GetSetting::Anc => {
run(client, cmd_get_setting(handle, channel, settings::id::CurrentAncrState)).await
},
GetSetting::VolumeEq => {
run(client, cmd_get_setting(handle, channel, settings::id::VolumeEqEnable)).await
},
GetSetting::Eq => {
run(client, cmd_get_setting(handle, channel, settings::id::CurrentUserEq)).await
},
GetSetting::Balance => {
run(client, cmd_get_setting(handle, channel, settings::id::VolumeAsymmetry)).await
},
GetSetting::Mono => {
run(client, cmd_get_setting(handle, channel, settings::id::SumToMono)).await
},
GetSetting::VolumeExposureNotifications => {
run(client, cmd_get_setting(handle, channel, settings::id::VolumeExposureNotifications)).await
},
GetSetting::SpeechDetection => {
run(client, cmd_get_setting(handle, channel, settings::id::SpeechDetection)).await
},
},
Command::Set { setting } => match setting {
SetSetting::AutoOta { value } => {
let value = SettingValue::AutoOtaEnable(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::Ohd { value } => {
let value = SettingValue::OhdEnable(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::OobeIsFinished { value } => {
let value = SettingValue::OobeIsFinished(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::Gestures { value } => {
let value = SettingValue::GestureEnable(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::Diagnostics { value } => {
let value = SettingValue::DiagnosticsEnable(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::OobeMode { value } => {
let value = SettingValue::OobeMode(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::GestureControl { left, right } => {
let value = settings::GestureControl { left: left.into(), right: right.into() };
let value = SettingValue::GestureControl(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::Multipoint { value } => {
let value = SettingValue::MultipointEnable(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::AncGestureLoop { off, active, aware, adaptive } => {
let value = settings::AncrGestureLoop { off, active, aware, adaptive };
if !value.is_valid() {
use clap::error::ErrorKind;
let mut cmd = Args::command();
let err = cmd.error(
ErrorKind::InvalidValue,
"This command requires at least tow enabled ('true') modes"
);
err.exit();
}
let value = SettingValue::AncrGestureLoop(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::Anc { value } => {
match value {
AncState::Off => {
let value = SettingValue::CurrentAncrState(settings::AncState::Off);
run(client, cmd_set_setting(handle, channel, value)).await
},
AncState::Aware => {
let value = SettingValue::CurrentAncrState(settings::AncState::Aware);
run(client, cmd_set_setting(handle, channel, value)).await
},
AncState::Active => {
let value = SettingValue::CurrentAncrState(settings::AncState::Active);
run(client, cmd_set_setting(handle, channel, value)).await
},
AncState::Adaptive => {
let value = SettingValue::CurrentAncrState(settings::AncState::Adaptive);
run(client, cmd_set_setting(handle, channel, value)).await
},
AncState::CycleNext => {
run(client, cmd_anc_cycle(handle, channel, true)).await
},
AncState::CyclePrev => {
run(client, cmd_anc_cycle(handle, channel, false)).await
},
}
},
SetSetting::VolumeEq { value } => {
let value = SettingValue::VolumeEqEnable(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::Eq { low_bass, bass, mid, treble, upper_treble } => {
let value = settings::EqBands::new(low_bass, bass, mid, treble, upper_treble);
let value = SettingValue::CurrentUserEq(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::Balance { value } => {
let value = settings::VolumeAsymmetry::from_normalized(value);
let value = SettingValue::VolumeAsymmetry(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::Mono { value } => {
let value = SettingValue::SumToMono(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::VolumeExposureNotifications { value } => {
let value = SettingValue::VolumeExposureNotifications(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
SetSetting::SpeechDetection { value } => {
let value = SettingValue::SpeechDetection(value);
run(client, cmd_set_setting(handle, channel, value)).await
},
},
}
}
async fn cmd_show_software(handle: ClientHandle, channel: u32) -> Result<()> {
let mut service = MaestroService::new(handle, channel);
let info = service.get_software_info().await?;
let fw_ver_case = info.firmware.as_ref()
.and_then(|fw| fw.case.as_ref())
.map(|fw| fw.version_string.as_str())
.unwrap_or("unknown");
let fw_ver_left = info.firmware.as_ref()
.and_then(|fw| fw.left.as_ref())
.map(|fw| fw.version_string.as_str())
.unwrap_or("unknown");
let fw_ver_right = info.firmware.as_ref()
.and_then(|fw| fw.right.as_ref())
.map(|fw| fw.version_string.as_str())
.unwrap_or("unknown");
let fw_unk_case = info.firmware.as_ref()
.and_then(|fw| fw.case.as_ref())
.map(|fw| fw.unknown.as_str())
.unwrap_or("unknown");
let fw_unk_left = info.firmware.as_ref()
.and_then(|fw| fw.left.as_ref())
.map(|fw| fw.unknown.as_str())
.unwrap_or("unknown");
let fw_unk_right = info.firmware.as_ref()
.and_then(|fw| fw.right.as_ref())
.map(|fw| fw.unknown.as_str())
.unwrap_or("unknown");
println!("firmware:");
println!(" case: {fw_ver_case} ({fw_unk_case})");
println!(" left bud: {fw_ver_left} ({fw_unk_left})");
println!(" right bud: {fw_ver_right} ({fw_unk_right})");
Ok(())
}
async fn cmd_show_hardware(handle: ClientHandle, channel: u32) -> Result<()> {
let mut service = MaestroService::new(handle, channel);
let info = service.get_hardware_info().await?;
let serial_case = info.serial_number.as_ref()
.map(|ser| ser.case.as_str())
.unwrap_or("unknown");
let serial_left = info.serial_number.as_ref()
.map(|ser| ser.left.as_str())
.unwrap_or("unknown");
let serial_right = info.serial_number.as_ref()
.map(|ser| ser.right.as_str())
.unwrap_or("unknown");
println!("serial numbers:");
println!(" case: {serial_case}");
println!(" left bud: {serial_left}");
println!(" right bud: {serial_right}");
Ok(())
}
async fn cmd_show_runtime(handle: ClientHandle, channel: u32) -> Result<()> {
let mut service = MaestroService::new(handle, channel);
let mut call = service.subscribe_to_runtime_info()?;
let info = call.stream().next().await
.ok_or_else(|| anyhow::anyhow!("stream terminated without item"))??;
let bat_level_case = info.battery_info.as_ref()
.and_then(|b| b.case.as_ref())
.map(|b| b.level);
let bat_state_case = info.battery_info.as_ref()
.and_then(|b| b.case.as_ref())
.map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" })
.unwrap_or("unknown");
let bat_level_left = info.battery_info.as_ref()
.and_then(|b| b.left.as_ref())
.map(|b| b.level);
let bat_state_left = info.battery_info.as_ref()
.and_then(|b| b.left.as_ref())
.map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" })
.unwrap_or("unknown");
let bat_level_right = info.battery_info.as_ref()
.and_then(|b| b.right.as_ref())
.map(|b| b.level);
let bat_state_right = info.battery_info.as_ref()
.and_then(|b| b.right.as_ref())
.map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" })
.unwrap_or("unknown");
let place_left = info.placement.as_ref()
.map(|p| if p.left_bud_in_case { "in case" } else { "out of case" })
.unwrap_or("unknown");
let place_right = info.placement.as_ref()
.map(|p| if p.right_bud_in_case { "in case" } else { "out of case" })
.unwrap_or("unknown");
println!("clock: {} ms", info.timestamp_ms);
println!();
println!("battery:");
if let Some(lvl) = bat_level_case {
println!(" case: {lvl}% ({bat_state_case})");
} else {
println!(" case: unknown");
}
if let Some(lvl) = bat_level_left {
println!(" left bud: {lvl}% ({bat_state_left})");
} else {
println!(" left bud: unknown");
}
if let Some(lvl) = bat_level_right {
println!(" right bud: {lvl}% ({bat_state_right})");
} else {
println!(" right bud: unknown");
}
println!();
println!("placement:");
println!(" left bud: {place_left}");
println!(" right bud: {place_right}");
let address = addr::address_for_channel(channel);
let peer_local = address.map(|a| a.source());
let peer_remote = address.map(|a| a.target());
println!();
println!("connection:");
if let Some(peer) = peer_local {
println!(" local: {peer:?}");
} else {
println!(" local: unknown");
}
if let Some(peer) = peer_remote {
println!(" remote: {peer:?}");
} else {
println!(" remote: unknown");
}
Ok(())
}
async fn cmd_show_battery(handle: ClientHandle, channel: u32) -> Result<()> {
let mut service = MaestroService::new(handle, channel);
let mut call = service.subscribe_to_runtime_info()?;
let info = call.stream().next().await
.ok_or_else(|| anyhow::anyhow!("stream terminated without item"))??;
let bat_level_case = info.battery_info.as_ref()
.and_then(|b| b.case.as_ref())
.map(|b| b.level);
let bat_state_case = info.battery_info.as_ref()
.and_then(|b| b.case.as_ref())
.map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" })
.unwrap_or("unknown");
let bat_level_left = info.battery_info.as_ref()
.and_then(|b| b.left.as_ref())
.map(|b| b.level);
let bat_state_left = info.battery_info.as_ref()
.and_then(|b| b.left.as_ref())
.map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" })
.unwrap_or("unknown");
let bat_level_right = info.battery_info.as_ref()
.and_then(|b| b.right.as_ref())
.map(|b| b.level);
let bat_state_right = info.battery_info.as_ref()
.and_then(|b| b.right.as_ref())
.map(|b| if b.state == 2 { "charging" } else if b.state == 1 { "not charging" } else { "unknown" })
.unwrap_or("unknown");
if let Some(lvl) = bat_level_case {
println!("case: {lvl}% ({bat_state_case})");
} else {
println!("case: unknown");
}
if let Some(lvl) = bat_level_left {
println!("left bud: {lvl}% ({bat_state_left})");
} else {
println!("left bud: unknown");
}
if let Some(lvl) = bat_level_right {
println!("right bud: {lvl}% ({bat_state_right})");
} else {
println!("right bud: unknown");
}
Ok(())
}
async fn cmd_get_setting<T>(handle: ClientHandle, channel: u32, setting: T) -> Result<()>
where
T: Setting,
T::Type: std::fmt::Display,
{
let mut service = MaestroService::new(handle, channel);
let value = service.read_setting(setting).await?;
println!("{value}");
Ok(())
}
async fn cmd_set_setting(handle: ClientHandle, channel: u32, setting: SettingValue) -> Result<()> {
let mut service = MaestroService::new(handle, channel);
service.write_setting(setting).await?;
Ok(())
}
async fn cmd_anc_cycle(handle: ClientHandle, channel: u32, forward: bool) -> Result<()> {
let mut service = MaestroService::new(handle, channel);
let enabled = service.read_setting(settings::id::AncrGestureLoop).await?;
let state = service.read_setting(settings::id::CurrentAncrState).await?;
if let settings::AncState::Unknown(x) = state {
anyhow::bail!("unknown ANC state: {x}");
}
let states = [
(settings::AncState::Active, enabled.active),
(settings::AncState::Off, enabled.off),
(settings::AncState::Aware, enabled.aware),
];
let index = states.iter().position(|(s, _)| *s == state).unwrap();
for offs in 1..states.len() {
let next = if forward {
index + offs
} else {
index + states.len() - offs
} % states.len();
let (state, enabled) = states[next];
if enabled {
service.write_setting(SettingValue::CurrentAncrState(state)).await?;
break;
}
}
Ok(())
}
pub async fn run<S, E, F>(mut client: Client<S>, task: F) -> Result<()>
where
S: futures::Sink<maestro::pwrpc::types::RpcPacket>,
S: futures::Stream<Item = Result<maestro::pwrpc::types::RpcPacket, E>> + Unpin,
maestro::pwrpc::Error: From<E>,
maestro::pwrpc::Error: From<S::Error>,
F: Future<Output=Result<(), anyhow::Error>>,
{
tokio::select! {
res = client.run() => {
res?;
anyhow::bail!("client terminated unexpectedly");
},
res = task => {
res?;
tracing::trace!("task terminated successfully");
}
sig = tokio::signal::ctrl_c() => {
sig?;
tracing::trace!("client termination requested");
},
}
client.terminate().await?;
tracing::trace!("client terminated successfully");
Ok(())
}