first commit
This commit is contained in:
24
cli/Cargo.toml
Normal file
24
cli/Cargo.toml
Normal 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
21
cli/build.rs
Normal 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
90
cli/src/bt.rs
Normal 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
317
cli/src/cli.rs
Normal 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
506
cli/src/main.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user