rust/bt-mcs: Add initial type definitions Add the initial package definition for the bt-mcs crate, introducing UUID constants, error types, state enums, bitmask flags, and wire codecs for the Media Control Service (MCS v1.0.1) and Generic Media Control Service (GMCS v1.0.1). Include the bitflags crate in the toplevel project repository to be used by the MCS types. Define a crate-specific Error type for clients interacting with the MCS library. Bug: 540400364 Test: cargo test --package bt-mcs, ./presubmit.sh Change-Id: Ia44a18f06abfeaf2b944fddb31b25872dc64b9c9 Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/3520
diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 4ed2d16..622e389 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml
@@ -18,6 +18,7 @@ bt-common = { path = "bt-common" } bt-csis = { path = "bt-csis" } bt-gatt = { path = "bt-gatt" } +bt-mcs = { path = "bt-mcs" } bt-pacs = { path = "bt-pacs" } bt-set-coordinator = { path = "bt-set-coordinator" } @@ -26,6 +27,7 @@ aes = "0.9.1" assert_matches = "1.5.0" bitfield = "0.19.4" +bitflags = "2.11.1" futures = "=0.3.31" futures-test = "=0.3.31" log = { version = "0.4.29", features = ["kv", "std"] }
diff --git a/rust/bt-mcs/Cargo.toml b/rust/bt-mcs/Cargo.toml new file mode 100644 index 0000000..1db1b45 --- /dev/null +++ b/rust/bt-mcs/Cargo.toml
@@ -0,0 +1,11 @@ +[package] +name = "bt-mcs" +version = "0.0.1" +edition.workspace = true +license.workspace = true + +[dependencies] +bitflags.workspace = true +bt-common.workspace = true +bt-gatt.workspace = true +thiserror.workspace = true
diff --git a/rust/bt-mcs/src/error.rs b/rust/bt-mcs/src/error.rs new file mode 100644 index 0000000..eec71eb --- /dev/null +++ b/rust/bt-mcs/src/error.rs
@@ -0,0 +1,17 @@ +// 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 thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("GATT operation error: {0}")] + Gatt(#[from] bt_gatt::types::Error), + + #[error("Packet encoding/decoding error: {0}")] + Packet(#[from] bt_common::packet_encoding::Error), + + #[error("Other error: {0}")] + Other(String), +}
diff --git a/rust/bt-mcs/src/lib.rs b/rust/bt-mcs/src/lib.rs new file mode 100644 index 0000000..531578c --- /dev/null +++ b/rust/bt-mcs/src/lib.rs
@@ -0,0 +1,8 @@ +// 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. + +pub mod error; +pub mod types; + +pub use crate::error::Error;
diff --git a/rust/bt-mcs/src/types.rs b/rust/bt-mcs/src/types.rs new file mode 100644 index 0000000..ca765f5 --- /dev/null +++ b/rust/bt-mcs/src/types.rs
@@ -0,0 +1,649 @@ +// 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); + } +}