rust/bt-pacs: Add debug logging to PACS

Add debug! and trace! logging to PACS characteristic decoding and
reading to improve visibility into audio capability handling.

Debugging PACS against a real peer requires seeing what was read from
each characteristic and how it was interpreted; without it a malformed
record is indistinguishable from a missing one.

 - Log PAC record decoding, including record counts and codec ids
 - Log audio location and audio context decoding results
 - Add try_read implementations that log the handle being read and the
   number of bytes returned, sharing a read_characteristic_value
   helper that handles truncated reads
 - Implement Decodable for AudioContexts and log the decoded contexts
 - Depend on the log crate
 - Add unit tests for Verbose and -v/-vv flags in the debug CLI

Test: cargo test -p bt-pacs
      cargo test --workspace
Change-Id: I5187de3d249bf40d68ffe11ce11012516a6a6964
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/4121
diff --git a/rust/bt-pacs/Cargo.toml b/rust/bt-pacs/Cargo.toml
index c260971..e9b051a 100644
--- a/rust/bt-pacs/Cargo.toml
+++ b/rust/bt-pacs/Cargo.toml
@@ -12,6 +12,7 @@
 
 ### Others
 futures.workspace = true
+log.workspace = true
 pin-project.workspace = true
 thiserror.workspace = true
 parking_lot.workspace = true
diff --git a/rust/bt-pacs/src/debug.rs b/rust/bt-pacs/src/debug.rs
index 9b815d7..ce95664 100644
--- a/rust/bt-pacs/src/debug.rs
+++ b/rust/bt-pacs/src/debug.rs
@@ -48,7 +48,6 @@
 
                 let chrs = service.discover_characteristics(None).await?;
                 for chr in chrs {
-                    let mut buf = [0; 120];
                     match chr.uuid {
                         SourcePac::UUID => {
                             let source_pac = SourcePac::try_read::<T>(chr, &service).await?;
@@ -64,24 +63,18 @@
                             println!("{locations:?}");
                         }
                         SourceAudioLocations::UUID => {
-                            let (bytes, _trunc) =
-                                service.read_characteristic(&chr.handle, 0, &mut buf).await?;
-                            let locations = SourceAudioLocations::from_chr(chr, &buf[..bytes])
-                                .map_err(bt_gatt::types::Error::other)?;
+                            let locations =
+                                SourceAudioLocations::try_read::<T>(chr, &service).await?;
                             println!("{locations:?}");
                         }
                         AvailableAudioContexts::UUID => {
-                            let (bytes, _trunc) =
-                                service.read_characteristic(&chr.handle, 0, &mut buf).await?;
-                            let contexts = AvailableAudioContexts::from_chr(chr, &buf[..bytes])
-                                .map_err(bt_gatt::types::Error::other)?;
+                            let contexts =
+                                AvailableAudioContexts::try_read::<T>(chr, &service).await?;
                             println!("{contexts:?}");
                         }
                         SupportedAudioContexts::UUID => {
-                            let (bytes, _trunc) =
-                                service.read_characteristic(&chr.handle, 0, &mut buf).await?;
-                            let contexts = SupportedAudioContexts::from_chr(chr, &buf[..bytes])
-                                .map_err(bt_gatt::types::Error::other)?;
+                            let contexts =
+                                SupportedAudioContexts::try_read::<T>(chr, &service).await?;
                             println!("{contexts:?}");
                         }
                         _x => println!("Unrecognized Chr {}", chr.uuid.recognize()),
@@ -92,3 +85,70 @@
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use bt_common::debug_command::CliCommand;
+    use bt_gatt::test_utils::{FakeClient, FakeTypes};
+    use log::LevelFilter;
+
+    fn setup_fake_pacs_debug() -> PacsDebug<FakeTypes> {
+        PacsDebug::new(FakeClient::new())
+    }
+
+    #[test]
+    fn verbosity_controls_and_flags() {
+        let debug = setup_fake_pacs_debug();
+
+        log::set_max_level(LevelFilter::Info);
+
+        // Verbose with no args displays current verbosity without changing it
+        let res = futures::executor::block_on(debug.run(CliCommand::Verbose, vec![]));
+        assert!(res.is_ok());
+        assert_eq!(log::max_level(), LevelFilter::Info);
+
+        // Update verbosity to Debug
+        let res =
+            futures::executor::block_on(debug.run(CliCommand::Verbose, vec!["debug".to_string()]));
+        assert!(res.is_ok());
+        assert_eq!(log::max_level(), LevelFilter::Debug);
+
+        // Update verbosity to Trace
+        let res =
+            futures::executor::block_on(debug.run(CliCommand::Verbose, vec!["trace".to_string()]));
+        assert!(res.is_ok());
+        assert_eq!(log::max_level(), LevelFilter::Trace);
+
+        // Update verbosity to Off
+        let res =
+            futures::executor::block_on(debug.run(CliCommand::Verbose, vec!["off".to_string()]));
+        assert!(res.is_ok());
+        assert_eq!(log::max_level(), LevelFilter::Off);
+
+        // Invalid level does not change verbosity
+        let res = futures::executor::block_on(
+            debug.run(CliCommand::Verbose, vec!["invalid_level".to_string()]),
+        );
+        assert!(res.is_ok());
+        assert_eq!(log::max_level(), LevelFilter::Off);
+
+        // Reset back to Info for scoped flags testing
+        log::set_max_level(LevelFilter::Info);
+
+        // Executing Print with -v flag:
+        // 1. debug.run() strips the flag before run_command.
+        // 2. Temporarily elevates max log level during execution.
+        // 3. Executes successfully.
+        let res = futures::executor::block_on(debug.run(PacsCmd::Print, vec!["-v".to_string()]));
+        assert!(res.is_ok());
+
+        // Persistent verbosity remains Info after scoped execution completes
+        assert_eq!(log::max_level(), LevelFilter::Info);
+
+        // Executing Print with -vv flag elevates to Trace
+        let res = futures::executor::block_on(debug.run(PacsCmd::Print, vec!["-vv".to_string()]));
+        assert!(res.is_ok());
+        assert_eq!(log::max_level(), LevelFilter::Info);
+    }
+}
diff --git a/rust/bt-pacs/src/lib.rs b/rust/bt-pacs/src/lib.rs
index d27ad26..cbb71e6 100644
--- a/rust/bt-pacs/src/lib.rs
+++ b/rust/bt-pacs/src/lib.rs
@@ -9,7 +9,11 @@
 use bt_common::generic_audio::{AudioLocation, ContextType};
 use bt_common::packet_encoding::{Decodable, Encodable};
 use bt_common::Uuid;
-use bt_gatt::{client::FromCharacteristic, Characteristic};
+use bt_gatt::{
+    client::{FromCharacteristic, PeerService},
+    types::Error as GattError,
+    Characteristic,
+};
 
 use std::collections::HashSet;
 
@@ -71,7 +75,14 @@
         let metadata = results.into_iter().filter_map(Result::ok).collect();
         idx += consumed;
 
-        (Ok(Self { codec_id, codec_specific_capabilities, metadata }), idx)
+        let pac_record = Self { codec_id, codec_specific_capabilities, metadata };
+        log::trace!(
+            "Decoded PacRecord: codec_id={:?}, capabilities={}, metadata={}",
+            pac_record.codec_id,
+            pac_record.codec_specific_capabilities.len(),
+            pac_record.metadata.len()
+        );
+        (Ok(pac_record), idx)
     }
 }
 
@@ -126,6 +137,7 @@
         return Err(bt_common::packet_encoding::Error::UnexpectedDataLength);
     }
     let num_of_pac_records = value[0] as usize;
+    log::trace!("Decoding {} PAC records from {} bytes", num_of_pac_records, value.len());
     let mut next_idx = 1;
     let mut capabilities = Vec::with_capacity(num_of_pac_records);
     for _ in 0..num_of_pac_records {
@@ -133,9 +145,27 @@
         capabilities.push(cap?);
         next_idx += consumed;
     }
+    log::debug!("Successfully decoded {} PAC records", capabilities.len());
     Ok(capabilities)
 }
 
+async fn read_characteristic_value<T: bt_gatt::GattTypes>(
+    handle: &bt_gatt::types::Handle,
+    service: &T::PeerService,
+) -> Result<Vec<u8>, GattError> {
+    let mut buf = [0; 128];
+    let (bytes, mut truncated) = service.read_characteristic(handle, 0, &mut buf).await?;
+    let mut vec = Vec::with_capacity(bytes);
+    vec.extend_from_slice(&buf[..bytes]);
+    while truncated {
+        let (bytes, still_truncated) =
+            service.read_characteristic(handle, vec.len() as u16, &mut buf).await?;
+        vec.extend_from_slice(&buf[..bytes]);
+        truncated = still_truncated;
+    }
+    Ok(vec)
+}
+
 /// One Sink Published Audio Capability Characteristic, or Sink PAC, exposed on
 /// a service. More than one Sink PAC can exist on a given PACS service.  If
 /// multiple are exposed, they are returned separately and can be notified by
@@ -156,6 +186,7 @@
     ) -> Result<Self, bt_common::packet_encoding::Error> {
         let handle = characteristic.handle;
         let capabilities = pac_records_from_bytes(value)?;
+        log::trace!("Parsed SinkPac (handle: {:?}) with {} records", handle, capabilities.len());
         Ok(Self { handle, capabilities })
     }
 
@@ -163,6 +194,21 @@
         self.capabilities = pac_records_from_bytes(new_value)?;
         Ok(self)
     }
+
+    fn try_read<T: bt_gatt::GattTypes>(
+        characteristic: Characteristic,
+        service: &T::PeerService,
+    ) -> impl futures::Future<Output = Result<Self, GattError>> {
+        async move {
+            if characteristic.uuid != Self::UUID {
+                return Err(GattError::ScanFailed("Wrong UUID".to_owned()));
+            }
+            log::debug!("Reading Sink PAC characteristic (handle {:?})", characteristic.handle);
+            let raw = read_characteristic_value::<T>(&characteristic.handle, service).await?;
+            log::trace!("Read {} bytes from Sink PAC characteristic", raw.len());
+            Self::from_chr(characteristic, &raw).map_err(Into::into)
+        }
+    }
 }
 
 /// One Sink Published Audio Capability Characteristic, or Sink PAC, exposed on
@@ -185,6 +231,7 @@
     ) -> Result<Self, bt_common::packet_encoding::Error> {
         let handle = characteristic.handle;
         let capabilities = pac_records_from_bytes(value)?;
+        log::trace!("Parsed SourcePac (handle: {:?}) with {} records", handle, capabilities.len());
         Ok(Self { handle, capabilities })
     }
 
@@ -192,6 +239,21 @@
         self.capabilities = pac_records_from_bytes(new_value)?;
         Ok(self)
     }
+
+    fn try_read<T: bt_gatt::GattTypes>(
+        characteristic: Characteristic,
+        service: &T::PeerService,
+    ) -> impl futures::Future<Output = Result<Self, GattError>> {
+        async move {
+            if characteristic.uuid != Self::UUID {
+                return Err(GattError::ScanFailed("Wrong UUID".to_owned()));
+            }
+            log::debug!("Reading Source PAC characteristic (handle {:?})", characteristic.handle);
+            let raw = read_characteristic_value::<T>(&characteristic.handle, service).await?;
+            log::trace!("Read {} bytes from Source PAC characteristic", raw.len());
+            Self::from_chr(characteristic, &raw).map_err(Into::into)
+        }
+    }
 }
 
 #[derive(Debug, PartialEq, Clone, Default)]
@@ -209,10 +271,26 @@
         let locations =
             AudioLocation::from_bits(u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]))
                 .collect();
+        log::trace!("Decoded AudioLocations: {:?}", locations);
         (Ok(AudioLocations { locations }), 4)
     }
 }
 
+impl AudioLocations {
+    pub async fn try_read<T: bt_gatt::GattTypes>(
+        characteristic: Characteristic,
+        service: &T::PeerService,
+    ) -> Result<Self, GattError> {
+        log::debug!("Reading Audio Locations characteristic (handle {:?})", characteristic.handle);
+        let raw = read_characteristic_value::<T>(&characteristic.handle, service).await?;
+        log::trace!("Read {} bytes from Audio Locations characteristic", raw.len());
+        let (res, _) = Self::decode(&raw);
+        let locations = res.map_err(GattError::other)?;
+        log::debug!("Parsed Audio Locations: {:?}", locations);
+        Ok(locations)
+    }
+}
+
 impl Encodable for AudioLocations {
     type Error = bt_common::packet_encoding::Error;
 
@@ -255,6 +333,7 @@
     ) -> Result<Self, bt_common::packet_encoding::Error> {
         let handle = characteristic.handle;
         let locations = AudioLocations::decode(value).0?;
+        log::trace!("Parsed SourceAudioLocations (handle: {:?}): {:?}", handle, locations);
         Ok(Self { handle, locations })
     }
 
@@ -262,6 +341,24 @@
         self.locations = AudioLocations::decode(new_value).0?;
         Ok(self)
     }
+
+    fn try_read<T: bt_gatt::GattTypes>(
+        characteristic: Characteristic,
+        service: &T::PeerService,
+    ) -> impl futures::Future<Output = Result<Self, GattError>> {
+        async move {
+            if characteristic.uuid != Self::UUID {
+                return Err(GattError::ScanFailed("Wrong UUID".to_owned()));
+            }
+            log::debug!(
+                "Reading Source Audio Locations characteristic (handle {:?})",
+                characteristic.handle
+            );
+            let raw = read_characteristic_value::<T>(&characteristic.handle, service).await?;
+            log::trace!("Read {} bytes from Source Audio Locations characteristic", raw.len());
+            Self::from_chr(characteristic, &raw).map_err(Into::into)
+        }
+    }
 }
 
 #[derive(Debug, PartialEq, Clone)]
@@ -288,6 +385,7 @@
     ) -> Result<Self, bt_common::packet_encoding::Error> {
         let handle = characteristic.handle;
         let locations = AudioLocations::decode(value).0?;
+        log::trace!("Parsed SinkAudioLocations (handle: {:?}): {:?}", handle, locations);
         Ok(Self { handle, locations })
     }
 
@@ -295,6 +393,24 @@
         self.locations = AudioLocations::decode(new_value).0?;
         Ok(self)
     }
+
+    fn try_read<T: bt_gatt::GattTypes>(
+        characteristic: Characteristic,
+        service: &T::PeerService,
+    ) -> impl futures::Future<Output = Result<Self, GattError>> {
+        async move {
+            if characteristic.uuid != Self::UUID {
+                return Err(GattError::ScanFailed("Wrong UUID".to_owned()));
+            }
+            log::debug!(
+                "Reading Sink Audio Locations characteristic (handle {:?})",
+                characteristic.handle
+            );
+            let raw = read_characteristic_value::<T>(&characteristic.handle, service).await?;
+            log::trace!("Read {} bytes from Sink Audio Locations characteristic", raw.len());
+            Self::from_chr(characteristic, &raw).map_err(Into::into)
+        }
+    }
 }
 
 #[derive(Debug, PartialEq, Clone)]
@@ -322,11 +438,13 @@
             return (Err(bt_common::packet_encoding::Error::UnexpectedDataLength), 2);
         }
         let encoded = u16::from_le_bytes([buf[0], buf[1]]);
-        if encoded == 0 {
-            (Ok(Self::NotAvailable), 2)
+        let contexts = if encoded == 0 {
+            Self::NotAvailable
         } else {
-            (Ok(Self::Available(ContextType::from_bits(encoded).collect())), 2)
-        }
+            Self::Available(ContextType::from_bits(encoded).collect())
+        };
+        log::trace!("Decoded AvailableContexts: {:?}", contexts);
+        (Ok(contexts), 2)
     }
 }
 
@@ -391,6 +509,12 @@
         }
         let sink = AvailableContexts::decode(&value[0..2]).0?;
         let source = AvailableContexts::decode(&value[2..4]).0?;
+        log::trace!(
+            "Decoded AvailableAudioContexts for handle {:?}: sink={:?}, source={:?}",
+            handle,
+            sink,
+            source
+        );
         Ok(Self { handle, sink, source })
     }
 
@@ -407,6 +531,24 @@
         self.source = source;
         Ok(self)
     }
+
+    fn try_read<T: bt_gatt::GattTypes>(
+        characteristic: Characteristic,
+        service: &T::PeerService,
+    ) -> impl futures::Future<Output = Result<Self, GattError>> {
+        async move {
+            if characteristic.uuid != Self::UUID {
+                return Err(GattError::ScanFailed("Wrong UUID".to_owned()));
+            }
+            log::debug!(
+                "Reading Available Audio Contexts characteristic (handle {:?})",
+                characteristic.handle
+            );
+            let raw = read_characteristic_value::<T>(&characteristic.handle, service).await?;
+            log::trace!("Read {} bytes from Available Audio Contexts characteristic", raw.len());
+            Self::from_chr(characteristic, &raw).map_err(Into::into)
+        }
+    }
 }
 
 #[derive(Debug, Clone)]
@@ -455,6 +597,12 @@
         }
         let sink = ContextType::from_bits(u16::from_le_bytes([value[0], value[1]])).collect();
         let source = ContextType::from_bits(u16::from_le_bytes([value[2], value[3]])).collect();
+        log::trace!(
+            "Decoded SupportedAudioContexts for handle {:?}: sink={:?}, source={:?}",
+            handle,
+            sink,
+            source
+        );
         Ok(Self { handle, sink, source })
     }
 
@@ -471,6 +619,24 @@
             ContextType::from_bits(u16::from_le_bytes([new_value[2], new_value[3]])).collect();
         Ok(self)
     }
+
+    fn try_read<T: bt_gatt::GattTypes>(
+        characteristic: Characteristic,
+        service: &T::PeerService,
+    ) -> impl futures::Future<Output = Result<Self, GattError>> {
+        async move {
+            if characteristic.uuid != Self::UUID {
+                return Err(GattError::ScanFailed("Wrong UUID".to_owned()));
+            }
+            log::debug!(
+                "Reading Supported Audio Contexts characteristic (handle {:?})",
+                characteristic.handle
+            );
+            let raw = read_characteristic_value::<T>(&characteristic.handle, service).await?;
+            log::trace!("Read {} bytes from Supported Audio Contexts characteristic", raw.len());
+            Self::from_chr(characteristic, &raw).map_err(Into::into)
+        }
+    }
 }
 
 #[cfg(test)]
diff --git a/rust/bt-pacs/src/server/types.rs b/rust/bt-pacs/src/server/types.rs
index 1bc9afb..f209c13 100644
--- a/rust/bt-pacs/src/server/types.rs
+++ b/rust/bt-pacs/src/server/types.rs
@@ -125,7 +125,7 @@
     }
 }
 
-#[derive(Default)]
+#[derive(Debug, Default, Clone, PartialEq)]
 pub struct AudioContexts {
     pub(crate) sink: HashSet<ContextType>,
     pub(crate) source: HashSet<ContextType>,
@@ -137,6 +137,20 @@
     }
 }
 
+impl Decodable for AudioContexts {
+    type Error = bt_common::packet_encoding::Error;
+
+    fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) {
+        if buf.len() < 4 {
+            return (Err(bt_common::packet_encoding::Error::UnexpectedDataLength), buf.len());
+        }
+        let sink = ContextType::from_bits(u16::from_le_bytes([buf[0], buf[1]])).collect();
+        let source = ContextType::from_bits(u16::from_le_bytes([buf[2], buf[3]])).collect();
+        log::debug!("Decoded AudioContexts: sink={:?}, source={:?}", sink, source);
+        (Ok(AudioContexts { sink, source }), 4)
+    }
+}
+
 /// A single PAC characteristic consists of 1 or more PAC records.
 pub type PacRecords = Vec<PacRecord>;
 
@@ -197,3 +211,46 @@
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use pretty_assertions::assert_eq;
+
+    const AUDIO_CONTEXTS_ENCODED_LEN: usize = 4;
+
+    #[test]
+    fn decode_audio_contexts() {
+        let (decoded, consumed) = AudioContexts::decode(&[0x00, 0x00, 0x00, 0x00]);
+        let contexts = decoded.expect("should decode empty contexts");
+        assert_eq!(consumed, AUDIO_CONTEXTS_ENCODED_LEN);
+        assert!(contexts.sink.is_empty());
+        assert!(contexts.source.is_empty());
+
+        let (decoded, consumed) = AudioContexts::decode(&[0x08, 0x06, 0x06, 0x03, 0xCA, 0xFE]);
+        let contexts = decoded.expect("should decode valid contexts with trailing bytes");
+        assert_eq!(consumed, AUDIO_CONTEXTS_ENCODED_LEN);
+        assert_eq!(
+            contexts.sink,
+            HashSet::from([ContextType::Game, ContextType::Ringtone, ContextType::Alerts])
+        );
+        assert_eq!(
+            contexts.source,
+            HashSet::from([
+                ContextType::Conversational,
+                ContextType::Media,
+                ContextType::Notifications,
+                ContextType::Ringtone,
+            ])
+        );
+    }
+
+    #[test]
+    fn decode_audio_contexts_too_short() {
+        let buf = [0x00, 0x00, 0x02];
+        let (decoded, consumed) = AudioContexts::decode(&buf);
+        assert_eq!(decoded, Err(bt_common::packet_encoding::Error::UnexpectedDataLength));
+        assert_eq!(consumed, buf.len());
+    }
+}