| // 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 core::num::NonZeroU8; |
| |
| use bt_common::packet_encoding::{Decodable, Encodable, Error as PacketError}; |
| use bt_common::{decodable_enum, Uuid}; |
| |
| /// 16-bit UUID value for the Coordinated Set Identification Service and its |
| /// characteristics. |
| pub const COORDINATED_SET_IDENTIFICATION_SERVICE_UUID: Uuid = Uuid::from_u16(0x1846); |
| pub const SET_IDENTITY_RESOLVING_KEY_UUID: Uuid = Uuid::from_u16(0x2B84); |
| pub const COORDINATED_SET_SIZE_UUID: Uuid = Uuid::from_u16(0x2B85); |
| pub const SET_MEMBER_LOCK_UUID: Uuid = Uuid::from_u16(0x2B86); |
| pub const SET_MEMBER_RANK_UUID: Uuid = Uuid::from_u16(0x2B87); |
| |
| // TODO(b/534436497): Add Coordinated Set Name characteristic (CSIS v1.1 Section |
| // 5.5). |
| // TODO(b/534436497): Add SetMemberLock enum and CsisApplicationError |
| // enum. |
| |
| decodable_enum! { |
| /// The type of the Set Identity Resolving Key (SIRK). |
| /// See CSIS v1.1 Section 5.1.1, Table 5.2. |
| pub enum SirkType<u8, bt_common::packet_encoding::Error, OutOfRange> { |
| Encrypted = 0x00, |
| Plaintext = 0x01, |
| } |
| } |
| |
| /// The Set Identity Resolving Key (SIRK) characteristic exposes the key |
| /// associated with the Coordinated Set (1 octet Type + 16 octets Value). |
| /// See CSIS v1.1 Section 5.1. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| pub struct SetIdentityResolvingKey { |
| pub sirk_type: SirkType, |
| pub value: [u8; 16], |
| } |
| |
| impl SetIdentityResolvingKey { |
| pub const BYTE_SIZE: usize = 17; |
| } |
| |
| impl Decodable for SetIdentityResolvingKey { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.len() < Self::BYTE_SIZE { |
| return (Err(PacketError::UnexpectedDataLength), buf.len()); |
| } |
| let sirk_type = match SirkType::try_from(buf[0]) { |
| Ok(t) => t, |
| Err(e) => return (Err(e), Self::BYTE_SIZE), |
| }; |
| let mut value = [0; 16]; |
| value.copy_from_slice(&buf[1..17]); |
| |
| (Ok(Self { sirk_type, value }), Self::BYTE_SIZE) |
| } |
| } |
| |
| impl Encodable for SetIdentityResolvingKey { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| Self::BYTE_SIZE |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.len() < Self::BYTE_SIZE { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[0] = self.sirk_type.into(); |
| buf[1..17].copy_from_slice(&self.value); |
| Ok(()) |
| } |
| } |
| |
| /// The Set Member Rank characteristic exposes a numeric value that is unique |
| /// within a Coordinated Set (0x01 to set size). See CSIS v1.1 Section 5.4. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| pub struct SetMemberRank(pub NonZeroU8); |
| |
| impl SetMemberRank { |
| pub const BYTE_SIZE: usize = 1; |
| } |
| |
| impl Decodable for SetMemberRank { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.is_empty() { |
| return (Err(PacketError::UnexpectedDataLength), 0); |
| } |
| let val = match NonZeroU8::new(buf[0]) { |
| Some(v) => v, |
| None => { |
| return ( |
| Err(PacketError::InvalidParameter("SetMemberRank cannot be 0".to_string())), |
| 1, |
| ) |
| } |
| }; |
| (Ok(Self(val)), Self::BYTE_SIZE) |
| } |
| } |
| |
| impl Encodable for SetMemberRank { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| Self::BYTE_SIZE |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.is_empty() { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[0] = self.0.get(); |
| Ok(()) |
| } |
| } |
| |
| /// The Coordinated Set Size characteristic exposes the number of devices |
| /// comprising the Coordinated Set (0x01 to 0xFF). See CSIS v1.1 Section 5.2. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| pub struct CoordinatedSetSize(pub NonZeroU8); |
| |
| impl CoordinatedSetSize { |
| pub const BYTE_SIZE: usize = 1; |
| } |
| |
| impl Decodable for CoordinatedSetSize { |
| type Error = PacketError; |
| |
| fn decode(buf: &[u8]) -> (core::result::Result<Self, Self::Error>, usize) { |
| if buf.is_empty() { |
| return (Err(PacketError::UnexpectedDataLength), 0); |
| } |
| let val = match NonZeroU8::new(buf[0]) { |
| Some(v) => v, |
| None => { |
| return ( |
| Err(PacketError::InvalidParameter( |
| "CoordinatedSetSize cannot be 0".to_string(), |
| )), |
| 1, |
| ); |
| } |
| }; |
| (Ok(Self(val)), Self::BYTE_SIZE) |
| } |
| } |
| |
| impl Encodable for CoordinatedSetSize { |
| type Error = PacketError; |
| |
| fn encoded_len(&self) -> usize { |
| Self::BYTE_SIZE |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { |
| if buf.is_empty() { |
| return Err(PacketError::BufferTooSmall); |
| } |
| buf[0] = self.0.get(); |
| Ok(()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use assert_matches::assert_matches; |
| |
| #[test] |
| fn sirk_decoding() { |
| let mut buf = [0; 17]; |
| buf[0] = 0x01; // Plaintext |
| buf[1..17].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); |
| |
| let (res, consumed) = SetIdentityResolvingKey::decode(&buf); |
| assert_eq!(consumed, 17); |
| let sirk = res.unwrap(); |
| assert_eq!(sirk.sirk_type, SirkType::Plaintext); |
| assert_eq!(sirk.value, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); |
| } |
| |
| #[test] |
| fn sirk_encoding() { |
| let sirk = SetIdentityResolvingKey { sirk_type: SirkType::Encrypted, value: [1; 16] }; |
| let mut buf = [0; 17]; |
| sirk.encode(&mut buf).unwrap(); |
| assert_eq!(buf[0], 0x00); |
| assert_eq!(buf[1..17], [1; 16]); |
| } |
| |
| #[test] |
| fn set_member_rank_decoding() { |
| let mut buf = [0; 1]; |
| buf[0] = 0x05; |
| let (res, consumed) = SetMemberRank::decode(&buf); |
| assert_eq!(consumed, 1); |
| assert_eq!(res.unwrap().0.get(), 5); |
| |
| buf[0] = 0x00; // Invalid |
| let (res, _) = SetMemberRank::decode(&buf); |
| assert_matches!(res, Err(PacketError::InvalidParameter(_))); |
| } |
| |
| #[test] |
| fn set_member_rank_encoding() { |
| let rank = SetMemberRank(NonZeroU8::new(5).unwrap()); |
| assert_eq!(rank.encoded_len(), 1); |
| |
| let mut buf = [0; 1]; |
| rank.encode(&mut buf).unwrap(); |
| assert_eq!(buf[0], 0x05); |
| |
| let mut empty_buf = []; |
| assert_matches!(rank.encode(&mut empty_buf), Err(PacketError::BufferTooSmall)); |
| } |
| |
| #[test] |
| fn coordinated_set_size_decoding() { |
| let mut buf = [0; 1]; |
| buf[0] = 0x02; |
| let (res, consumed) = CoordinatedSetSize::decode(&buf); |
| assert_eq!(consumed, 1); |
| assert_eq!(res.unwrap().0.get(), 2); |
| |
| buf[0] = 0x00; // Invalid |
| let (res, _) = CoordinatedSetSize::decode(&buf); |
| assert_matches!(res, Err(PacketError::InvalidParameter(_))); |
| } |
| |
| #[test] |
| fn coordinated_set_size_encoding() { |
| let size = CoordinatedSetSize(NonZeroU8::new(2).unwrap()); |
| assert_eq!(size.encoded_len(), 1); |
| |
| let mut buf = [0; 1]; |
| size.encode(&mut buf).unwrap(); |
| assert_eq!(buf[0], 0x02); |
| |
| let mut empty_buf = []; |
| assert_matches!(size.encode(&mut empty_buf), Err(PacketError::BufferTooSmall)); |
| } |
| } |