rust/bt-mcs: Add McsServerBuilder and GATT service definition Implement `McsServerBuilder` for configuring and constructing the GATT `ServiceDefinition` for Generic Media Control Service (GMCS) and Media Control Service (MCS). Both services are largely the same and can be configured. Register all 13 mandatory characteristics as defined in MCS v1.0.1 Section 3. Bug: 540400364 Test: cargo test -p bt-mcs Change-Id: I3c7b4b3c405f3d52ab7a3937081a6b3981c9749b Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/3660
diff --git a/rust/bt-gatt/src/types.rs b/rust/bt-gatt/src/types.rs index 07527e3..e8beec7 100644 --- a/rust/bt-gatt/src/types.rs +++ b/rust/bt-gatt/src/types.rs
@@ -230,6 +230,21 @@ WritableAuxiliaries = 0x200, } +impl CharacteristicProperty { + /// Characteristic is readable and can send notifications. + pub const READ_NOTIFY: &'static [Self] = &[Self::Read, Self::Notify]; + + /// Control Point property set: writable (with and without response) and can + /// send notifications. + pub const WRITE_NOTIFY: &'static [Self] = + &[Self::Write, Self::WriteWithoutResponse, Self::Notify]; + + /// Readable, writable (with and without response), and can send + /// notifications. + pub const READ_WRITE_NOTIFY: &'static [Self] = + &[Self::Read, Self::Write, Self::WriteWithoutResponse, Self::Notify]; +} + impl std::ops::BitOr for CharacteristicProperty { type Output = CharacteristicProperties;
diff --git a/rust/bt-mcs/src/lib.rs b/rust/bt-mcs/src/lib.rs index 531578c..da58e1a 100644 --- a/rust/bt-mcs/src/lib.rs +++ b/rust/bt-mcs/src/lib.rs
@@ -2,7 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -pub mod error; +mod error; +pub mod server; pub mod types; pub use crate::error::Error; +pub use crate::server::McsServerBuilder;
diff --git a/rust/bt-mcs/src/server.rs b/rust/bt-mcs/src/server.rs new file mode 100644 index 0000000..f4b43d0 --- /dev/null +++ b/rust/bt-mcs/src/server.rs
@@ -0,0 +1,273 @@ +// 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. + +//! Implements the Media Control Service (MCS) server. + +use bt_common::Uuid; +use bt_gatt::server::{ServiceDefinition, ServiceId}; +use bt_gatt::types::{ + AttributePermissions, CharacteristicProperties, CharacteristicProperty, Handle, SecurityLevels, + ServiceKind, +}; +use bt_gatt::Characteristic; + +use crate::types::*; +use crate::Error; + +// ============================================================================ +// Mandatory Characteristic Handle Definitions (MCS v1.0.1 Section 3) +// ============================================================================ + +/// Handle assigned to the Media Player Name characteristic. +const MEDIA_PLAYER_NAME_HANDLE: Handle = Handle(1); +/// Handle assigned to the Track Changed characteristic. +const TRACK_CHANGED_HANDLE: Handle = Handle(2); +/// Handle assigned to the Track Title characteristic. +const TRACK_TITLE_HANDLE: Handle = Handle(3); +/// Handle assigned to the Track Duration characteristic. +const TRACK_DURATION_HANDLE: Handle = Handle(4); +/// Handle assigned to the Track Position characteristic. +const TRACK_POSITION_HANDLE: Handle = Handle(5); +/// Handle assigned to the Playback Speed characteristic. +const PLAYBACK_SPEED_HANDLE: Handle = Handle(6); +/// Handle assigned to the Seeking Speed characteristic. +const SEEKING_SPEED_HANDLE: Handle = Handle(7); +/// Handle assigned to the Playing Order characteristic. +const PLAYING_ORDER_HANDLE: Handle = Handle(8); +/// Handle assigned to the Playing Orders Supported characteristic. +const PLAYING_ORDERS_SUPPORTED_HANDLE: Handle = Handle(9); +/// Handle assigned to the Media State characteristic. +const MEDIA_STATE_HANDLE: Handle = Handle(10); +/// Handle assigned to the Media Control Point characteristic. +const MEDIA_CONTROL_POINT_HANDLE: Handle = Handle(11); +/// Handle assigned to the Media Control Point Opcodes Supported characteristic. +const MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE: Handle = Handle(12); +/// Handle assigned to the Content Control ID (CCID) characteristic. +const CONTENT_CONTROL_ID_HANDLE: Handle = Handle(13); + +/// Specification of a mandatory GATT characteristic for MCS. +struct CharacteristicSpec { + handle: Handle, + uuid: Uuid, + properties: &'static [CharacteristicProperty], +} + +/// All the mandatory characteristics defined in MCS v1.0.1 Section 3. +const MANDATORY_CHARACTERISTICS: [CharacteristicSpec; 13] = [ + CharacteristicSpec { + handle: MEDIA_PLAYER_NAME_HANDLE, + uuid: MEDIA_PLAYER_NAME_UUID, + properties: CharacteristicProperty::READ_NOTIFY, + }, + CharacteristicSpec { + handle: TRACK_CHANGED_HANDLE, + uuid: TRACK_CHANGED_UUID, + properties: &[CharacteristicProperty::Notify], + }, + CharacteristicSpec { + handle: TRACK_TITLE_HANDLE, + uuid: TRACK_TITLE_UUID, + properties: CharacteristicProperty::READ_NOTIFY, + }, + CharacteristicSpec { + handle: TRACK_DURATION_HANDLE, + uuid: TRACK_DURATION_UUID, + properties: CharacteristicProperty::READ_NOTIFY, + }, + CharacteristicSpec { + handle: TRACK_POSITION_HANDLE, + uuid: TRACK_POSITION_UUID, + properties: CharacteristicProperty::READ_WRITE_NOTIFY, + }, + CharacteristicSpec { + handle: PLAYBACK_SPEED_HANDLE, + uuid: PLAYBACK_SPEED_UUID, + properties: CharacteristicProperty::READ_WRITE_NOTIFY, + }, + CharacteristicSpec { + handle: SEEKING_SPEED_HANDLE, + uuid: SEEKING_SPEED_UUID, + properties: CharacteristicProperty::READ_NOTIFY, + }, + CharacteristicSpec { + handle: PLAYING_ORDER_HANDLE, + uuid: PLAYING_ORDER_UUID, + properties: CharacteristicProperty::READ_WRITE_NOTIFY, + }, + CharacteristicSpec { + handle: PLAYING_ORDERS_SUPPORTED_HANDLE, + uuid: PLAYING_ORDERS_SUPPORTED_UUID, + properties: &[CharacteristicProperty::Read], + }, + CharacteristicSpec { + handle: MEDIA_STATE_HANDLE, + uuid: MEDIA_STATE_UUID, + properties: CharacteristicProperty::READ_NOTIFY, + }, + CharacteristicSpec { + handle: MEDIA_CONTROL_POINT_HANDLE, + uuid: MEDIA_CONTROL_POINT_UUID, + properties: CharacteristicProperty::WRITE_NOTIFY, + }, + CharacteristicSpec { + handle: MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE, + uuid: MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_UUID, + properties: CharacteristicProperty::READ_NOTIFY, + }, + CharacteristicSpec { + handle: CONTENT_CONTROL_ID_HANDLE, + uuid: CONTENT_CONTROL_ID_UUID, + properties: &[CharacteristicProperty::Read], + }, +]; + +/// Constructs a characteristic definition with the specified handle, UUID, +/// properties, and encryption-required permissions conforming to MCS v1.0.1 +/// Section 3. +fn build_mandatory_characteristic(spec: &CharacteristicSpec) -> Characteristic { + let properties: CharacteristicProperties = spec.properties.iter().copied().collect(); + Characteristic { + handle: spec.handle, + uuid: spec.uuid, + properties: properties.clone(), + permissions: AttributePermissions::with_levels( + &properties, + &SecurityLevels::encryption_required(), + ), + descriptors: Vec::new(), + } +} + +/// Builder for configuring an MCS or GMCS GATT service. +#[derive(Debug, Clone, PartialEq)] +pub struct McsServerBuilder { + /// Service UUID assigned to the service. + service_uuid: Uuid, + /// Content Control ID (CCID) identifying this media service instance. This + /// is unique across all MCS/GMCS instances on the host server. + ccid: u8, + /// Human-readable media player application name. + player_name: String, +} + +impl McsServerBuilder { + /// Creates a builder for the Generic Media Control Service. + pub fn generic(ccid: u8, player_name: impl Into<String>) -> Self { + Self::new(GENERIC_MEDIA_CONTROL_SERVICE_UUID, ccid, player_name) + } + + /// Creates a builder for an application-specific Media Control Service. + pub fn instance(ccid: u8, player_name: impl Into<String>) -> Self { + Self::new(MEDIA_CONTROL_SERVICE_UUID, ccid, player_name) + } + + fn new(service_uuid: Uuid, ccid: u8, player_name: impl Into<String>) -> Self { + Self { service_uuid, ccid, player_name: player_name.into() } + } + + /// Constructs the complete GATT [`ServiceDefinition`] containing all 13 + /// Mandatory Characteristics defined in MCS v1.0.1 Section 3. + pub fn build_service_definition(&self) -> Result<ServiceDefinition, Error> { + // The local `ServiceId` is derived from the provided `CCID`. This is valid + // because the CCID must be unique across all MCS/GMCS instances on the + // host server. Adding a duplicate characteristic will result in an + // Error. + let mut service_def = ServiceDefinition::new( + ServiceId::new(self.ccid.into()), + self.service_uuid, + ServiceKind::Primary, + ); + + for spec in &MANDATORY_CHARACTERISTICS { + service_def.add_characteristic(build_mandatory_characteristic(spec))?; + } + + Ok(service_def) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builder_generic_service_definition() { + let builder = McsServerBuilder::generic(0x42, "Test Generic Player"); + let service_def = builder + .build_service_definition() + .expect("generic service definition builds successfully"); + assert_eq!(service_def.uuid(), GENERIC_MEDIA_CONTROL_SERVICE_UUID); + assert_eq!(service_def.id(), ServiceId::new(0x42)); + assert_eq!(service_def.kind(), ServiceKind::Primary); + assert_eq!(service_def.characteristics().count(), 13); + } + + #[test] + fn builder_instance_service_definition() { + let builder = McsServerBuilder::instance(0x07, "Test Instance Player"); + let service_def = builder + .build_service_definition() + .expect("instance service definition builds successfully"); + assert_eq!(service_def.uuid(), MEDIA_CONTROL_SERVICE_UUID); + assert_eq!(service_def.id(), ServiceId::new(0x07)); + assert_eq!(service_def.kind(), ServiceKind::Primary); + assert_eq!(service_def.characteristics().count(), 13); + } + + #[test] + fn mandatory_characteristic_handles_uuids_and_properties() { + let builder = McsServerBuilder::generic(0x01, "Player"); + let service_def = + builder.build_service_definition().expect("service definition builds successfully"); + let characteristics: Vec<&Characteristic> = service_def.characteristics().collect(); + + assert_eq!(characteristics.len(), MANDATORY_CHARACTERISTICS.len()); + + for (i, spec) in MANDATORY_CHARACTERISTICS.iter().enumerate() { + let chrc = characteristics[i]; + assert_eq!(chrc.handle, spec.handle); + assert_eq!(chrc.uuid, spec.uuid); + assert_eq!(chrc.properties.0.as_slice(), spec.properties); + + let expected_properties: CharacteristicProperties = + spec.properties.iter().copied().collect(); + let expected_permissions = AttributePermissions::with_levels( + &expected_properties, + &SecurityLevels::encryption_required(), + ); + assert_eq!( + chrc.permissions.read.map(|s| s.encryption), + expected_permissions.read.map(|s| s.encryption) + ); + assert_eq!( + chrc.permissions.write.map(|s| s.encryption), + expected_permissions.write.map(|s| s.encryption) + ); + assert_eq!( + chrc.permissions.update.map(|s| s.encryption), + expected_permissions.update.map(|s| s.encryption) + ); + } + } + + #[test] + fn service_id_tracks_ccid() { + let builder1 = McsServerBuilder::instance(0x05, "Player 1"); + let builder2 = McsServerBuilder::instance(0x05, "Player 2"); + let builder3 = McsServerBuilder::instance(0x06, "Player 3"); + + let def1 = builder1.build_service_definition().expect("valid definition"); + let def2 = builder2.build_service_definition().expect("valid definition"); + let def3 = builder3.build_service_definition().expect("valid definition"); + + assert_eq!(def1.id(), def2.id()); + assert_eq!(def1.id(), ServiceId::new(5)); + assert_ne!(def1.id(), def3.id()); + assert_eq!(def3.id(), ServiceId::new(6)); + } + + // TODO(b/540400364): Add a test verifying that publishing two McsServer + // instances with the same CCID to a GATT server fails with an + // AlreadyPublished error. +}