| // Copyright 2026 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 bitflags::bitflags; |
| use bt_common::packet_encoding::{Decodable, Encodable, Error as PacketError}; |
| use bt_common::{decodable_enum, Uuid}; |
| |
| /// 16-bit UUID for the Media Control Service (MCS). |
| /// Defined in MCS v1.0.1 Section 2 |
| pub(crate) const MEDIA_CONTROL_SERVICE_UUID: Uuid = Uuid::from_u16(0x1848); |
| /// 16-bit UUID for the Generic Media Control Service (GMCS). |
| /// Defined in MCS v1.0.1 Section 2 |
| pub(crate) const GENERIC_MEDIA_CONTROL_SERVICE_UUID: Uuid = Uuid::from_u16(0x1849); |
| |
| // ============================================================================ |
| // Characteristic UUIDs (Mandatory, defined in MCS v1.0.1 Section 3) |
| // ============================================================================ |
| |
| /// Media Player Name characteristic UUID. |
| pub(crate) const MEDIA_PLAYER_NAME_UUID: Uuid = Uuid::from_u16(0x2B93); |
| /// Track Changed characteristic UUID. |
| pub(crate) const TRACK_CHANGED_UUID: Uuid = Uuid::from_u16(0x2B96); |
| /// Track Title characteristic UUID. |
| pub(crate) const TRACK_TITLE_UUID: Uuid = Uuid::from_u16(0x2B97); |
| /// Track Duration characteristic UUID. |
| pub(crate) const TRACK_DURATION_UUID: Uuid = Uuid::from_u16(0x2B98); |
| /// Track Position characteristic UUID. |
| pub(crate) const TRACK_POSITION_UUID: Uuid = Uuid::from_u16(0x2B99); |
| /// Playback Speed characteristic UUID. |
| pub(crate) const PLAYBACK_SPEED_UUID: Uuid = Uuid::from_u16(0x2B9A); |
| /// Seeking Speed characteristic UUID. |
| pub(crate) const SEEKING_SPEED_UUID: Uuid = Uuid::from_u16(0x2B9B); |
| /// Playing Order characteristic UUID. |
| pub(crate) const PLAYING_ORDER_UUID: Uuid = Uuid::from_u16(0x2BA1); |
| /// Playing Orders Supported characteristic UUID. |
| pub(crate) const PLAYING_ORDERS_SUPPORTED_UUID: Uuid = Uuid::from_u16(0x2BA2); |
| /// Media State characteristic UUID. |
| pub(crate) const MEDIA_STATE_UUID: Uuid = Uuid::from_u16(0x2BA3); |
| /// Media Control Point characteristic UUID. |
| pub(crate) const MEDIA_CONTROL_POINT_UUID: Uuid = Uuid::from_u16(0x2BA4); |
| /// Media Control Point Opcodes Supported characteristic UUID. |
| pub(crate) const MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_UUID: Uuid = Uuid::from_u16(0x2BA5); |
| /// Content Control ID (CCID) characteristic UUID. |
| pub(crate) const CONTENT_CONTROL_ID_UUID: Uuid = Uuid::from_u16(0x2BA8); |
| |
| decodable_enum! { |
| /// State of the media player. |
| pub enum MediaState<u8, PacketError, OutOfRange> { |
| /// Media player is inactive. |
| Inactive = 0x00, |
| /// Media player is currently playing. |
| Playing = 0x01, |
| /// Media player is paused. |
| Paused = 0x02, |
| /// Media player is fast forwarding or rewinding. |
| Seeking = 0x03, |
| } |
| } |
| |
| impl Encodable for MediaState { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| 1 |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.is_empty() { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[0] = (*self).into(); |
| Ok(()) |
| } |
| } |
| |
| impl Decodable for MediaState { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) { |
| if buf.is_empty() { |
| return (Err(PacketError::UnexpectedDataLength), 0); |
| } |
| (Self::try_from(buf[0]), 1) |
| } |
| } |
| |
| decodable_enum! { |
| /// Result code returned in a Media Control Point notification. |
| pub enum ControlPointResultCode<u8, PacketError, OutOfRange> { |
| /// The procedure completed successfully. |
| Success = 0x01, |
| /// The opcode is not supported by the media player. |
| OpcodeNotSupported = 0x02, |
| /// The media player is inactive and cannot perform the operation. |
| MediaPlayerInactive = 0x03, |
| /// The command cannot be completed. |
| CommandCannotBeCompleted = 0x04, |
| } |
| } |
| |
| impl Encodable for ControlPointResultCode { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| 1 |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.is_empty() { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[0] = (*self).into(); |
| Ok(()) |
| } |
| } |
| |
| impl Decodable for ControlPointResultCode { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) { |
| if buf.is_empty() { |
| return (Err(PacketError::UnexpectedDataLength), 0); |
| } |
| (Self::try_from(buf[0]), 1) |
| } |
| } |
| |
| decodable_enum! { |
| /// Playing order mode for media playback. |
| pub enum PlayingOrder<u8, PacketError, OutOfRange> { |
| /// Play a single track once, then stop. |
| SingleOnce = 0x01, |
| /// Play a single track repeatedly. |
| SingleRepeat = 0x02, |
| /// Play all tracks in normal sequence once, then stop. |
| Normal = 0x03, |
| /// Play all tracks in normal sequence repeatedly. |
| NormalRepeat = 0x04, |
| /// Play all tracks in shuffled order once, then stop. |
| ShuffleOnce = 0x05, |
| /// Play all tracks in shuffled order repeatedly. |
| ShuffleRepeat = 0x06, |
| } |
| } |
| |
| impl Encodable for PlayingOrder { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| 1 |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.is_empty() { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[0] = (*self).into(); |
| Ok(()) |
| } |
| } |
| |
| impl Decodable for PlayingOrder { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) { |
| if buf.is_empty() { |
| return (Err(PacketError::UnexpectedDataLength), 0); |
| } |
| (Self::try_from(buf[0]), 1) |
| } |
| } |
| |
| bitflags! { |
| /// Bitmask representing supported playing orders (MCS v1.0.1 Section 3.10). |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| pub struct SupportedPlayingOrders: u16 { |
| const SINGLE_ONCE = 0x0001; |
| const SINGLE_REPEAT = 0x0002; |
| const NORMAL = 0x0004; |
| const NORMAL_REPEAT = 0x0008; |
| const SHUFFLE_ONCE = 0x0010; |
| const SHUFFLE_REPEAT = 0x0020; |
| } |
| } |
| |
| impl From<PlayingOrder> for SupportedPlayingOrders { |
| fn from(order: PlayingOrder) -> Self { |
| match order { |
| PlayingOrder::SingleOnce => Self::SINGLE_ONCE, |
| PlayingOrder::SingleRepeat => Self::SINGLE_REPEAT, |
| PlayingOrder::Normal => Self::NORMAL, |
| PlayingOrder::NormalRepeat => Self::NORMAL_REPEAT, |
| PlayingOrder::ShuffleOnce => Self::SHUFFLE_ONCE, |
| PlayingOrder::ShuffleRepeat => Self::SHUFFLE_REPEAT, |
| } |
| } |
| } |
| |
| impl Encodable for SupportedPlayingOrders { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| 2 |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.len() < 2 { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[..2].copy_from_slice(&self.bits().to_le_bytes()); |
| Ok(()) |
| } |
| } |
| |
| impl Decodable for SupportedPlayingOrders { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) { |
| if buf.len() < 2 { |
| return (Err(PacketError::UnexpectedDataLength), 0); |
| } |
| let raw = u16::from_le_bytes([buf[0], buf[1]]); |
| (Ok(Self::from_bits_truncate(raw)), 2) |
| } |
| } |
| |
| /// Media Control Point Opcode. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub enum MediaControlOpcode { |
| /// Start playing current track. |
| Play, |
| /// Pause playback. |
| Pause, |
| /// Fast rewind. |
| FastRewind, |
| /// Fast forward. |
| FastForward, |
| /// Stop playback. |
| Stop, |
| /// Move playback position relative to current position in seconds. |
| MoveRelative(i32), |
| /// Jump to previous segment. |
| PreviousSegment, |
| /// Jump to next segment. |
| NextSegment, |
| /// Jump to first segment. |
| FirstSegment, |
| /// Jump to last segment. |
| LastSegment, |
| /// Jump to specified segment number. |
| GotoSegment(i32), |
| /// Jump to previous track. |
| PreviousTrack, |
| /// Jump to next track. |
| NextTrack, |
| /// Jump to first track. |
| FirstTrack, |
| /// Jump to last track. |
| LastTrack, |
| /// Jump to specified track number. |
| GotoTrack(i32), |
| /// Jump to previous group. |
| PreviousGroup, |
| /// Jump to next group. |
| NextGroup, |
| /// Jump to first group. |
| FirstGroup, |
| /// Jump to last group. |
| LastGroup, |
| /// Jump to specified group number. |
| GotoGroup(i32), |
| } |
| |
| impl MediaControlOpcode { |
| /// Returns the raw 1-byte opcode value. |
| pub fn raw_opcode(&self) -> u8 { |
| match self { |
| Self::Play => 0x01, |
| Self::Pause => 0x02, |
| Self::FastRewind => 0x03, |
| Self::FastForward => 0x04, |
| Self::Stop => 0x05, |
| Self::MoveRelative(_) => 0x20, |
| Self::PreviousSegment => 0x21, |
| Self::NextSegment => 0x22, |
| Self::FirstSegment => 0x23, |
| Self::LastSegment => 0x24, |
| Self::GotoSegment(_) => 0x25, |
| Self::PreviousTrack => 0x30, |
| Self::NextTrack => 0x31, |
| Self::FirstTrack => 0x32, |
| Self::LastTrack => 0x33, |
| Self::GotoTrack(_) => 0x34, |
| Self::PreviousGroup => 0x40, |
| Self::NextGroup => 0x41, |
| Self::FirstGroup => 0x42, |
| Self::LastGroup => 0x43, |
| Self::GotoGroup(_) => 0x44, |
| } |
| } |
| } |
| |
| impl Encodable for MediaControlOpcode { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| match self { |
| Self::MoveRelative(_) |
| | Self::GotoSegment(_) |
| | Self::GotoTrack(_) |
| | Self::GotoGroup(_) => 5, |
| _ => 1, |
| } |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.len() < self.encoded_len() { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[0] = self.raw_opcode(); |
| match self { |
| Self::MoveRelative(param) |
| | Self::GotoSegment(param) |
| | Self::GotoTrack(param) |
| | Self::GotoGroup(param) => { |
| buf[1..5].copy_from_slice(¶m.to_le_bytes()); |
| } |
| _ => {} |
| } |
| Ok(()) |
| } |
| } |
| |
| impl Decodable for MediaControlOpcode { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) { |
| if buf.is_empty() { |
| return (Err(PacketError::UnexpectedDataLength), 0); |
| } |
| |
| let opcode = buf[0]; |
| match opcode { |
| 0x01 => (Ok(Self::Play), 1), |
| 0x02 => (Ok(Self::Pause), 1), |
| 0x03 => (Ok(Self::FastRewind), 1), |
| 0x04 => (Ok(Self::FastForward), 1), |
| 0x05 => (Ok(Self::Stop), 1), |
| 0x20 => { |
| if buf.len() < 5 { |
| (Err(PacketError::UnexpectedDataLength), buf.len()) |
| } else { |
| let param = i32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]); |
| (Ok(Self::MoveRelative(param)), 5) |
| } |
| } |
| 0x21 => (Ok(Self::PreviousSegment), 1), |
| 0x22 => (Ok(Self::NextSegment), 1), |
| 0x23 => (Ok(Self::FirstSegment), 1), |
| 0x24 => (Ok(Self::LastSegment), 1), |
| 0x25 => { |
| if buf.len() < 5 { |
| (Err(PacketError::UnexpectedDataLength), buf.len()) |
| } else { |
| let param = i32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]); |
| (Ok(Self::GotoSegment(param)), 5) |
| } |
| } |
| 0x30 => (Ok(Self::PreviousTrack), 1), |
| 0x31 => (Ok(Self::NextTrack), 1), |
| 0x32 => (Ok(Self::FirstTrack), 1), |
| 0x33 => (Ok(Self::LastTrack), 1), |
| 0x34 => { |
| if buf.len() < 5 { |
| (Err(PacketError::UnexpectedDataLength), buf.len()) |
| } else { |
| let param = i32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]); |
| (Ok(Self::GotoTrack(param)), 5) |
| } |
| } |
| 0x40 => (Ok(Self::PreviousGroup), 1), |
| 0x41 => (Ok(Self::NextGroup), 1), |
| 0x42 => (Ok(Self::FirstGroup), 1), |
| 0x43 => (Ok(Self::LastGroup), 1), |
| 0x44 => { |
| if buf.len() < 5 { |
| (Err(PacketError::UnexpectedDataLength), buf.len()) |
| } else { |
| let param = i32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]); |
| (Ok(Self::GotoGroup(param)), 5) |
| } |
| } |
| _ => (Err(PacketError::OutOfRange), 1), |
| } |
| } |
| } |
| |
| bitflags! { |
| /// Bitmask representing supported Media Control Point opcodes (MCS v1.0.1 Section 3.13). |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| pub struct SupportedOpcodes: u32 { |
| const PLAY = 0x00000001; |
| const PAUSE = 0x00000002; |
| const FAST_REWIND = 0x00000004; |
| const FAST_FORWARD = 0x00000008; |
| const STOP = 0x00000010; |
| const MOVE_RELATIVE = 0x00000020; |
| const PREVIOUS_SEGMENT = 0x00000040; |
| const NEXT_SEGMENT = 0x00000080; |
| const FIRST_SEGMENT = 0x00000100; |
| const LAST_SEGMENT = 0x00000200; |
| const GOTO_SEGMENT = 0x00000400; |
| const PREVIOUS_TRACK = 0x00000800; |
| const NEXT_TRACK = 0x00001000; |
| const FIRST_TRACK = 0x00002000; |
| const LAST_TRACK = 0x00004000; |
| const GOTO_TRACK = 0x00008000; |
| const PREVIOUS_GROUP = 0x00010000; |
| const NEXT_GROUP = 0x00020000; |
| const FIRST_GROUP = 0x00040000; |
| const LAST_GROUP = 0x00080000; |
| const GOTO_GROUP = 0x00100000; |
| } |
| } |
| |
| impl From<&MediaControlOpcode> for SupportedOpcodes { |
| fn from(opcode: &MediaControlOpcode) -> Self { |
| match opcode { |
| MediaControlOpcode::Play => Self::PLAY, |
| MediaControlOpcode::Pause => Self::PAUSE, |
| MediaControlOpcode::FastRewind => Self::FAST_REWIND, |
| MediaControlOpcode::FastForward => Self::FAST_FORWARD, |
| MediaControlOpcode::Stop => Self::STOP, |
| MediaControlOpcode::MoveRelative(_) => Self::MOVE_RELATIVE, |
| MediaControlOpcode::PreviousSegment => Self::PREVIOUS_SEGMENT, |
| MediaControlOpcode::NextSegment => Self::NEXT_SEGMENT, |
| MediaControlOpcode::FirstSegment => Self::FIRST_SEGMENT, |
| MediaControlOpcode::LastSegment => Self::LAST_SEGMENT, |
| MediaControlOpcode::GotoSegment(_) => Self::GOTO_SEGMENT, |
| MediaControlOpcode::PreviousTrack => Self::PREVIOUS_TRACK, |
| MediaControlOpcode::NextTrack => Self::NEXT_TRACK, |
| MediaControlOpcode::FirstTrack => Self::FIRST_TRACK, |
| MediaControlOpcode::LastTrack => Self::LAST_TRACK, |
| MediaControlOpcode::GotoTrack(_) => Self::GOTO_TRACK, |
| MediaControlOpcode::PreviousGroup => Self::PREVIOUS_GROUP, |
| MediaControlOpcode::NextGroup => Self::NEXT_GROUP, |
| MediaControlOpcode::FirstGroup => Self::FIRST_GROUP, |
| MediaControlOpcode::LastGroup => Self::LAST_GROUP, |
| MediaControlOpcode::GotoGroup(_) => Self::GOTO_GROUP, |
| } |
| } |
| } |
| |
| impl From<MediaControlOpcode> for SupportedOpcodes { |
| fn from(opcode: MediaControlOpcode) -> Self { |
| Self::from(&opcode) |
| } |
| } |
| |
| impl Encodable for SupportedOpcodes { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| 4 |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.len() < 4 { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[..4].copy_from_slice(&self.bits().to_le_bytes()); |
| Ok(()) |
| } |
| } |
| |
| impl Decodable for SupportedOpcodes { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (Result<Self, Self::Error>, usize) { |
| if buf.len() < 4 { |
| return (Err(PacketError::UnexpectedDataLength), 0); |
| } |
| let raw = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); |
| (Ok(Self::from_bits_truncate(raw)), 4) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_media_state_encode_decode() { |
| for &state in MediaState::VARIANTS { |
| let mut buf = [0u8; 1]; |
| assert!(state.encode(&mut buf).is_ok()); |
| let (decoded, consumed) = MediaState::decode(&buf); |
| assert_eq!(consumed, 1); |
| assert_eq!(decoded.unwrap(), state); |
| } |
| |
| // Test out of range |
| let (err, consumed) = MediaState::decode(&[0xFF]); |
| assert_eq!(consumed, 1); |
| assert_eq!(err.unwrap_err(), PacketError::OutOfRange); |
| |
| // Test empty buffer |
| let (err, consumed) = MediaState::decode(&[]); |
| assert_eq!(consumed, 0); |
| assert_eq!(err.unwrap_err(), PacketError::UnexpectedDataLength); |
| } |
| |
| #[test] |
| fn test_control_point_result_code_encode_decode() { |
| for &code in ControlPointResultCode::VARIANTS { |
| let mut buf = [0u8; 1]; |
| assert!(code.encode(&mut buf).is_ok()); |
| let (decoded, consumed) = ControlPointResultCode::decode(&buf); |
| assert_eq!(consumed, 1); |
| assert_eq!(decoded.unwrap(), code); |
| } |
| |
| // Test out of range |
| let (err, consumed) = ControlPointResultCode::decode(&[0x00]); |
| assert_eq!(consumed, 1); |
| assert_eq!(err.unwrap_err(), PacketError::OutOfRange); |
| } |
| |
| #[test] |
| fn test_playing_order_and_supported_bitmask() { |
| for &order in PlayingOrder::VARIANTS { |
| let mut buf = [0u8; 1]; |
| assert!(order.encode(&mut buf).is_ok()); |
| let (decoded, consumed) = PlayingOrder::decode(&buf); |
| assert_eq!(consumed, 1); |
| assert_eq!(decoded.unwrap(), order); |
| } |
| |
| let supported = SupportedPlayingOrders::SINGLE_ONCE |
| | SupportedPlayingOrders::NORMAL |
| | SupportedPlayingOrders::SHUFFLE_REPEAT; |
| |
| assert!(supported.contains(PlayingOrder::SingleOnce.into())); |
| assert!(supported.contains(PlayingOrder::Normal.into())); |
| assert!(supported.contains(PlayingOrder::ShuffleRepeat.into())); |
| assert!(!supported.contains(PlayingOrder::SingleRepeat.into())); |
| assert!(!supported.contains(PlayingOrder::NormalRepeat.into())); |
| assert!(!supported.contains(PlayingOrder::ShuffleOnce.into())); |
| |
| let mut buf = [0u8; 2]; |
| assert!(supported.encode(&mut buf).is_ok()); |
| let (decoded, consumed) = SupportedPlayingOrders::decode(&buf); |
| assert_eq!(consumed, 2); |
| assert_eq!(decoded.unwrap(), supported); |
| } |
| |
| #[test] |
| fn test_media_control_opcode_all_non_parameterized() { |
| let opcodes = [ |
| (MediaControlOpcode::Play, 0x01), |
| (MediaControlOpcode::Pause, 0x02), |
| (MediaControlOpcode::FastRewind, 0x03), |
| (MediaControlOpcode::FastForward, 0x04), |
| (MediaControlOpcode::Stop, 0x05), |
| (MediaControlOpcode::PreviousSegment, 0x21), |
| (MediaControlOpcode::NextSegment, 0x22), |
| (MediaControlOpcode::FirstSegment, 0x23), |
| (MediaControlOpcode::LastSegment, 0x24), |
| (MediaControlOpcode::PreviousTrack, 0x30), |
| (MediaControlOpcode::NextTrack, 0x31), |
| (MediaControlOpcode::FirstTrack, 0x32), |
| (MediaControlOpcode::LastTrack, 0x33), |
| (MediaControlOpcode::PreviousGroup, 0x40), |
| (MediaControlOpcode::NextGroup, 0x41), |
| (MediaControlOpcode::FirstGroup, 0x42), |
| (MediaControlOpcode::LastGroup, 0x43), |
| ]; |
| |
| for (opcode, raw_byte) in opcodes { |
| assert_eq!(opcode.raw_opcode(), raw_byte); |
| assert_eq!(opcode.encoded_len(), 1); |
| |
| let mut buf = [0u8; 1]; |
| assert!(opcode.encode(&mut buf).is_ok()); |
| assert_eq!(buf[0], raw_byte); |
| |
| let (decoded, consumed) = MediaControlOpcode::decode(&buf); |
| assert_eq!(consumed, 1); |
| assert_eq!(decoded.unwrap(), opcode); |
| } |
| } |
| |
| #[test] |
| fn test_media_control_opcode_parameterized() { |
| let test_cases = [ |
| (MediaControlOpcode::MoveRelative(10), 0x20, 10i32), |
| (MediaControlOpcode::MoveRelative(-15), 0x20, -15i32), |
| (MediaControlOpcode::GotoSegment(3), 0x25, 3i32), |
| (MediaControlOpcode::GotoTrack(42), 0x34, 42i32), |
| (MediaControlOpcode::GotoGroup(1), 0x44, 1i32), |
| ]; |
| |
| for (opcode, raw_byte, param) in test_cases { |
| assert_eq!(opcode.raw_opcode(), raw_byte); |
| assert_eq!(opcode.encoded_len(), 5); |
| |
| let mut buf = [0u8; 5]; |
| assert!(opcode.encode(&mut buf).is_ok()); |
| assert_eq!(buf[0], raw_byte); |
| assert_eq!(&buf[1..5], ¶m.to_le_bytes()); |
| |
| let (decoded, consumed) = MediaControlOpcode::decode(&buf); |
| assert_eq!(consumed, 5); |
| assert_eq!(decoded.unwrap(), opcode); |
| } |
| } |
| |
| #[test] |
| fn test_media_control_opcode_truncated_buffer() { |
| // Opcode 0x20 (MoveRelative) requires 5 bytes, only 3 provided |
| let truncated = [0x20, 0x01, 0x02]; |
| let (err, consumed) = MediaControlOpcode::decode(&truncated); |
| assert_eq!(consumed, 3); |
| assert_eq!(err.unwrap_err(), PacketError::UnexpectedDataLength); |
| } |
| |
| #[test] |
| fn test_supported_opcodes_bitmask() { |
| let supported = SupportedOpcodes::PLAY |
| | SupportedOpcodes::PAUSE |
| | SupportedOpcodes::MOVE_RELATIVE |
| | SupportedOpcodes::NEXT_TRACK; |
| |
| assert!(supported.contains(MediaControlOpcode::Play.into())); |
| assert!(supported.contains(MediaControlOpcode::Pause.into())); |
| assert!(supported.contains(MediaControlOpcode::MoveRelative(30).into())); |
| assert!(supported.contains(MediaControlOpcode::NextTrack.into())); |
| assert!(!supported.contains(MediaControlOpcode::PreviousTrack.into())); |
| assert!(!supported.contains(MediaControlOpcode::FastForward.into())); |
| assert!(!supported.contains(MediaControlOpcode::GotoTrack(1).into())); |
| |
| let mut buf = [0u8; 4]; |
| assert!(supported.encode(&mut buf).is_ok()); |
| let (decoded, consumed) = SupportedOpcodes::decode(&buf); |
| assert_eq!(consumed, 4); |
| assert_eq!(decoded.unwrap(), supported); |
| } |
| } |