blob: c8f176f473e04662429877da19976a7da18d91c0 [file]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use bt_common::core::CodecId;
use bt_common::debug_command::{CommandRunner, CommandSet};
use bt_common::gen_commandset;
use std::sync::Mutex;
use bt_gatt::{client::PeerServiceHandle, Client};
use crate::client::{AudioStreamControlServiceClient, QosConfigurationRequest};
use crate::server::ASCS_UUID;
use crate::types::*;
gen_commandset! {
AscsCmd {
Print = ("print", [], [], "Print the current ASCS status and active configuration details"),
ConfigCodec = ("config-codec", [], ["configs"], "Configure codecs for one or more ASEs. Format for each ASE: <ase_id>,<latency>,<phy>,<codec>"),
ConfigQos = ("config-qos", [], ["configs"], "Configure QoS for one or more ASEs. Each config is a comma-separated string: <ase_id>,<cig_id>,<cis_id>,<sdu_interval>,<max_sdu>"),
Enable = ("enable", [], ["ase_ids"], "Enable ASEs (comma-separated)"),
ReceiverStartReady = ("receiver-start-ready", [], ["ase_ids"], "Signal receiver start ready for source ASEs (comma-separated)"),
Disable = ("disable", [], ["ase_ids"], "Disable ASEs (comma-separated)"),
ReceiverStopReady = ("receiver-stop-ready", [], ["ase_ids"], "Signal receiver stop ready for source ASEs (comma-separated)"),
UpdateMetadata = ("update-metadata", [], ["ase_ids"], "Update metadata for ASEs (comma-separated, empty metadata)"),
Release = ("release", [], ["ase_ids"], "Release ASEs (comma-separated)"),
}
}
pub struct AscsDebug<T: bt_gatt::GattTypes> {
_peer_client: T::Client,
client: Mutex<Option<AudioStreamControlServiceClient<T>>>,
}
impl<T: bt_gatt::GattTypes> AscsDebug<T>
where
<T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
{
pub async fn new(client: T::Client) -> Result<Self, Error> {
let handles = client.find_service(ASCS_UUID).await?;
let handle = handles.iter().find(|h| h.is_primary()).ok_or_else(|| {
Error::Gatt(bt_gatt::types::Error::ScanFailed(
"Primary ASCS service not found".to_string(),
))
})?;
let service = handle.connect().await?;
let ascs_client = AudioStreamControlServiceClient::create(service).await?;
Ok(Self { _peer_client: client, client: Mutex::new(Some(ascs_client)) })
}
}
fn invalid_input_err(msg: impl Into<String>) -> Error {
Error::Other(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput, msg.into())))
}
fn parse_ase_id(s: &str) -> Result<AseId, Error> {
let val = s.parse::<u8>().map_err(|_| invalid_input_err("Invalid ASE ID"))?;
Ok(AseId(val))
}
fn parse_codec_configuration(s: &str) -> Result<CodecConfiguration, Error> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 4 {
return Err(invalid_input_err(
"Invalid config format. Expected: <ase_id>,<latency>,<phy>,<codec>",
));
}
let ase_id = parse_ase_id(parts[0])?;
let target_latency = match parts[1].to_lowercase().as_str() {
"low" => TargetLatency::TargetLowLatency,
"balanced" => TargetLatency::TargetBalanced,
"high" => TargetLatency::TargetHighReliability,
_ => {
return Err(invalid_input_err(
"Invalid target latency. Expected 'low', 'balanced', or 'high'",
));
}
};
let target_phy = match parts[2].to_lowercase().as_str() {
"1m" => TargetPhy::Le1MPhy,
"2m" => TargetPhy::Le2MPhy,
"coded" => TargetPhy::LeCodedPhy,
_ => {
return Err(invalid_input_err("Invalid target PHY. Expected '1m', '2m', or 'coded'"));
}
};
let codec_id = match parts[3].to_lowercase().as_str() {
"lc3" => CodecId::Assigned(bt_common::core::CodingFormat::Lc3),
"cvsd" => CodecId::Assigned(bt_common::core::CodingFormat::Cvsd),
_ => {
return Err(invalid_input_err("Invalid codec format. Expected 'lc3' or 'cvsd'"));
}
};
Ok(CodecConfiguration {
ase_id,
target_latency,
target_phy,
codec_id,
codec_specific_configuration: vec![],
})
}
fn parse_qos_configuration(s: &str) -> Result<QosConfigurationRequest, Error> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 5 {
return Err(invalid_input_err(
"Invalid config format. Expected: <ase_id>,<cig_id>,<cis_id>,<sdu_interval>,<max_sdu>",
));
}
let ase_id = parse_ase_id(parts[0])?;
let cig_id =
CigId::try_from(parts[1].parse::<u8>().map_err(|_| invalid_input_err("Invalid CIG ID"))?)
.map_err(|_| invalid_input_err("CIG ID out of bounds"))?;
let cis_id =
CisId::try_from(parts[2].parse::<u8>().map_err(|_| invalid_input_err("Invalid CIS ID"))?)
.map_err(|_| invalid_input_err("CIS ID out of bounds"))?;
let micros = parts[3].parse::<u64>().map_err(|_| invalid_input_err("Invalid SDU interval"))?;
let sdu_interval = SduInterval::try_from(std::time::Duration::from_micros(micros))
.map_err(|_| invalid_input_err("SDU interval out of bounds"))?;
let max_sdu = MaxSdu::try_from(
parts[4].parse::<u16>().map_err(|_| invalid_input_err("Invalid Max SDU"))?,
)
.map_err(|_| invalid_input_err("Max SDU out of bounds"))?;
Ok(QosConfigurationRequest::Preferred { ase_id, cig_id, cis_id, sdu_interval, max_sdu })
}
fn parse_ase_ids(s: &str) -> Result<Vec<AseId>, Error> {
if s.is_empty() {
return Err(invalid_input_err("Empty ASE ID list"));
}
s.split(',').map(parse_ase_id).collect()
}
fn parse_single_arg_ase_ids(args: &[String]) -> Result<Vec<AseId>, Error> {
if args.len() != 1 {
return Err(invalid_input_err("Expected comma-separated ASE IDs"));
}
parse_ase_ids(&args[0])
}
impl<T: bt_gatt::GattTypes> CommandRunner for AscsDebug<T>
where
<T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send,
{
type Set = AscsCmd;
fn run(
&self,
cmd: Self::Set,
args: Vec<String>,
) -> impl futures::Future<Output = Result<(), impl std::error::Error>> {
async move {
let mut client = {
let mut lock = self.client.lock().unwrap();
lock.take().ok_or_else(|| {
Error::Other(Box::new(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"Failed to connect to ASCS service",
)))
})?
};
let result: Result<(), Error> = match cmd {
AscsCmd::Print => {
println!("ASCS Status:");
println!("-- Sink Endpoints --");
for endpoint in client.endpoints.sink_ases() {
if let Some(ep) =
client.endpoints.lookup_by_ase_id(endpoint).map(|e| &e.endpoint)
{
println!(
" ASE ID: {:?}, Handle: {:?}, Direction: Sink, State: {:?}\n Details: {:?}",
ep.ase_id, ep.handle, ep.state, ep.additional
);
}
}
println!("-- Source Endpoints --");
for endpoint in client.endpoints.source_ases() {
if let Some(ep) =
client.endpoints.lookup_by_ase_id(endpoint).map(|e| &e.endpoint)
{
println!(
" ASE ID: {:?}, Handle: {:?}, Direction: Source, State: {:?}\n Details: {:?}",
ep.ase_id, ep.handle, ep.state, ep.additional
);
}
}
Ok(())
}
AscsCmd::ConfigCodec => {
let mut configs = Vec::new();
for arg in args {
configs.push(parse_codec_configuration(&arg)?);
}
let outcome = client.configure_codec(configs).await?;
println!("Codec Configured Outcome: {:?}", outcome);
Ok(())
}
AscsCmd::ConfigQos => {
let mut requests = Vec::new();
for arg in args {
requests.push(parse_qos_configuration(&arg)?);
}
let outcome = client.configure_qos(requests).await?;
println!("QoS Configured Outcome: {:?}", outcome);
Ok(())
}
AscsCmd::Enable => {
let ase_ids = parse_single_arg_ase_ids(&args)?;
let requests = ase_ids
.into_iter()
.map(|ase_id| AseIdWithMetadata { ase_id, metadata: vec![] })
.collect();
let outcome = client.enable(requests).await?;
println!("Enable Outcome: {:?}", outcome);
Ok(())
}
AscsCmd::ReceiverStartReady => {
let ase_ids = parse_single_arg_ase_ids(&args)?;
let outcome = client.receiver_start_ready(ase_ids).await?;
println!("Receiver Start Ready Outcome: {:?}", outcome);
Ok(())
}
AscsCmd::Disable => {
let ase_ids = parse_single_arg_ase_ids(&args)?;
let outcome = client.disable(ase_ids).await?;
println!("Disable Outcome: {:?}", outcome);
Ok(())
}
AscsCmd::ReceiverStopReady => {
let ase_ids = parse_single_arg_ase_ids(&args)?;
let outcome = client.receiver_stop_ready(ase_ids).await?;
println!("Receiver Stop Ready Outcome: {:?}", outcome);
Ok(())
}
AscsCmd::UpdateMetadata => {
let ase_ids = parse_single_arg_ase_ids(&args)?;
let requests = ase_ids
.into_iter()
.map(|ase_id| AseIdWithMetadata { ase_id, metadata: vec![] })
.collect();
let outcome = client.update_metadata(requests).await?;
println!("Update Metadata Outcome: {:?}", outcome);
Ok(())
}
AscsCmd::Release => {
let ase_ids = parse_single_arg_ase_ids(&args)?;
let outcome = client.release(ase_ids).await?;
println!("Release Outcome: {:?}", outcome);
Ok(())
}
};
*self.client.lock().unwrap() = Some(client);
result
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ase_id() {
assert_eq!(parse_ase_id("1").unwrap(), AseId(1));
assert_eq!(parse_ase_id("255").unwrap(), AseId(255));
assert!(parse_ase_id("").is_err());
assert!(parse_ase_id("abc").is_err());
assert!(parse_ase_id("256").is_err());
}
#[test]
fn test_parse_single_arg_ase_ids() {
assert_eq!(
parse_single_arg_ase_ids(&[String::from("1,2")]).unwrap(),
vec![AseId(1), AseId(2)]
);
assert!(parse_single_arg_ase_ids(&[]).is_err());
assert!(parse_single_arg_ase_ids(&[String::from("1"), String::from("2")]).is_err());
}
#[test]
fn test_parse_ase_ids() {
assert_eq!(parse_ase_ids("1,2,3").unwrap(), vec![AseId(1), AseId(2), AseId(3)]);
assert_eq!(parse_ase_ids("1").unwrap(), vec![AseId(1)]);
assert!(parse_ase_ids("").is_err());
assert!(parse_ase_ids("1,abc").is_err());
}
#[test]
fn test_parse_codec_configuration() {
let valid = "2,low,2m,lc3";
let config = parse_codec_configuration(valid).unwrap();
assert_eq!(config.ase_id, AseId(2));
assert_eq!(config.target_latency, TargetLatency::TargetLowLatency);
assert_eq!(config.target_phy, TargetPhy::Le2MPhy);
assert_eq!(config.codec_id, CodecId::Assigned(bt_common::core::CodingFormat::Lc3));
let valid_cvsd = "1,balanced,1m,cvsd";
let config_cvsd = parse_codec_configuration(valid_cvsd).unwrap();
assert_eq!(config_cvsd.ase_id, AseId(1));
assert_eq!(config_cvsd.target_latency, TargetLatency::TargetBalanced);
assert_eq!(config_cvsd.target_phy, TargetPhy::Le1MPhy);
assert_eq!(config_cvsd.codec_id, CodecId::Assigned(bt_common::core::CodingFormat::Cvsd));
// missing codec
assert!(parse_codec_configuration("2,low,2m").is_err());
// invalid target latency string
assert!(parse_codec_configuration("2,unknown,2m,lc3").is_err());
// invalid target PHY string
assert!(parse_codec_configuration("2,low,9m,lc3").is_err());
// invalid codec format string
assert!(parse_codec_configuration("2,low,2m,aac").is_err());
}
#[test]
fn test_parse_qos_configuration() {
let valid = "1,2,3,10000,120";
let request = parse_qos_configuration(valid).unwrap();
match request {
QosConfigurationRequest::Preferred {
ase_id,
cig_id,
cis_id,
sdu_interval,
max_sdu,
} => {
assert_eq!(ase_id, AseId(1));
assert_eq!(cig_id, CigId::try_from(2).unwrap());
assert_eq!(cis_id, CisId::try_from(3).unwrap());
assert_eq!(
sdu_interval,
SduInterval::try_from(std::time::Duration::from_micros(10000)).unwrap()
);
assert_eq!(max_sdu, MaxSdu::try_from(120).unwrap());
}
_ => panic!("Expected Preferred request"),
}
// missing max_sdu
assert!(parse_qos_configuration("1,2,3,10000").is_err());
// CIG ID out of bounds
assert!(parse_qos_configuration("1,255,3,10000,120").is_err());
// CIS ID out of bounds
assert!(parse_qos_configuration("1,2,255,10000,120").is_err());
// SDU interval out of bounds
assert!(parse_qos_configuration("1,2,3,100000000,120").is_err());
}
}