| // 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()); |
| } |
| } |