Repo of no-std crates for my personal embedded projects
at main 7.2 kB view raw
1#![no_std] 2 3use heapless::Vec; 4use sachy_fmt::assert; 5 6const BR_EDR_NOT_SUPPORTED: u8 = 4; 7const LE_GENERAL_DISCOVERABLE: u8 = 2; 8 9const BTHOME_AD_HEADER: [u8; 8] = [ 10 0x02, 11 0x01, 12 LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED, 13 0x04, 14 0x16, 15 0xD2, 16 0xFC, 17 0x40, 18]; 19 20pub const BTHOME_UUID16: u16 = 0xFCD2; 21 22macro_rules! impl_fields { 23 { $(($name:ident, $id:literal, $internal_repr:ty, $external_repr:ty),)+ } => { 24 $( 25 #[derive(Debug, Clone)] 26 #[cfg_attr(feature = "defmt", derive(::defmt::Format))] 27 pub struct $name($internal_repr); 28 29 impl $name { 30 const ID: u8 = $id; 31 const SIZE: usize = core::mem::size_of::<$internal_repr>() - 1; 32 33 #[inline] 34 pub fn get(&self) -> $external_repr { 35 let mut bytes = [0u8; core::mem::size_of::<$external_repr>()]; 36 bytes[0..Self::SIZE].copy_from_slice(&self.0[1..]); 37 <$external_repr>::from_le_bytes(bytes) 38 } 39 } 40 41 impl From<$name> for BtHomeEnum { 42 fn from(value: $name) -> Self { 43 Self::$name(value) 44 } 45 } 46 47 impl From<$external_repr> for $name { 48 #[inline] 49 fn from(value: $external_repr) -> Self { 50 let mut bytes = [0u8; core::mem::size_of::<$internal_repr>()]; 51 bytes[0] = Self::ID; 52 bytes[1..].copy_from_slice(&value.to_le_bytes()[0..Self::SIZE]); 53 $name(bytes) 54 } 55 } 56 )* 57 58 #[derive(Debug, Clone)] 59 #[cfg_attr(feature = "defmt", derive(::defmt::Format))] 60 pub enum BtHomeEnum { 61 $( 62 $name($name), 63 )* 64 } 65 66 impl PartialEq for BtHomeEnum { 67 fn eq(&self, other: &Self) -> bool { 68 self.id() == other.id() 69 } 70 } 71 72 impl Eq for BtHomeEnum {} 73 74 impl PartialOrd for BtHomeEnum { 75 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> { 76 Some(self.cmp(other)) 77 } 78 } 79 80 impl Ord for BtHomeEnum { 81 fn cmp(&self, other: &Self) -> core::cmp::Ordering { 82 self.id().cmp(&other.id()) 83 } 84 } 85 86 impl BtHomeEnum { 87 pub const fn id(&self) -> u8 { 88 match self { 89 $( 90 Self::$name(_) => $id, 91 )* 92 } 93 } 94 95 pub fn encode(&self) -> &[u8] { 96 match self { 97 $( 98 Self::$name(repr) => &repr.0, 99 )* 100 } 101 } 102 } 103 } 104} 105 106impl_fields! { 107 (Battery1Per, 0x01, [u8; 2], u8), 108 (Temperature10mK, 0x02, [u8; 3], i16), 109 (Humidity10mPer, 0x03, [u8; 3], u16), 110 (Illuminance10mLux, 0x05, [u8; 4], u32), 111 (Voltage1mV, 0x0C, [u8; 3], u16), 112 (Moisture10mPer, 0x14, [u8; 3], u16), 113 (Humidity1Per, 0x2E, [u8; 2], u8), 114 (Moisture1Per, 0x2F, [u8; 2], u8), 115} 116 117#[derive(Debug, Clone)] 118#[cfg_attr(feature = "defmt", derive(defmt::Format))] 119pub struct BtHomeAd<const N: usize> { 120 buffer: Vec<u8, N>, 121} 122 123impl<const N: usize> BtHomeAd<N> { 124 pub fn new() -> Self { 125 assert!(N >= BTHOME_AD_HEADER.len(), "Ad buffer is too small"); 126 127 let buffer = Vec::from_iter(BTHOME_AD_HEADER); 128 129 Self { buffer } 130 } 131 132 fn encode_data(&mut self, payload: BtHomeEnum) -> &mut Self { 133 let encoded = payload.encode(); 134 135 assert!( 136 self.buffer.len() + encoded.len() < N, 137 "Can't fit data into buffer! {}+{}", 138 self.buffer.len(), 139 encoded.len() 140 ); 141 142 self.buffer[3] += encoded.len() as u8; 143 self.buffer.extend_from_slice(encoded).ok(); 144 145 self 146 } 147 148 #[inline] 149 pub fn add_data(&mut self, payload: impl Into<BtHomeEnum>) -> &mut Self { 150 self.encode_data(payload.into()) 151 } 152 153 pub fn add_local_name(&mut self, name: &str) -> &Self { 154 let len = name.len() + 1; 155 156 assert!( 157 self.buffer.len() + len < N, 158 "Can't fit local name into buffer!" 159 ); 160 161 self.buffer.extend_from_slice(&[len as u8, 0x09]).ok(); 162 self.buffer.extend_from_slice(name.as_bytes()).ok(); 163 164 // Reborrow as ref to prevent further mutation after we have 165 // added the local name to the ad. 166 &*self 167 } 168 169 pub fn encode(&self) -> &[u8] { 170 &self.buffer 171 } 172} 173 174impl Default for BtHomeAd<31> { 175 #[inline] 176 fn default() -> Self { 177 Self::new() 178 } 179} 180 181#[cfg(test)] 182mod tests { 183 use super::*; 184 185 #[test] 186 fn basic_add_name() { 187 let mut home = BtHomeAd::default(); 188 189 let name = "hello"; 190 191 home.add_local_name(name); 192 193 assert_eq!(home.buffer.len(), 15); 194 195 assert_eq!( 196 home.encode(), 197 &[ 198 0x02, 199 0x01, 200 LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED, 201 0x04, 202 0x16, 203 0xD2, 204 0xFC, 205 0x40, 206 (name.len() + 1) as u8, 207 0x09, 208 b"h"[0], 209 b"e"[0], 210 b"l"[0], 211 b"l"[0], 212 b"o"[0] 213 ] 214 ); 215 } 216 217 #[test] 218 fn add_data() { 219 let mut home = BtHomeAd::default(); 220 221 home.add_data(Battery1Per::from(34)) 222 .add_data(Temperature10mK::from(2255)); 223 224 assert_eq!( 225 home.encode(), 226 &[ 227 0x02, 228 0x01, 229 LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED, 230 0x09, 231 0x16, 232 0xD2, 233 0xFC, 234 0x40, 235 0x01, 236 34, 237 0x02, 238 207, 239 8, 240 ] 241 ); 242 } 243 244 #[test] 245 fn full_payload() { 246 let mut home = BtHomeAd::default(); 247 248 let encoded = home 249 .add_data(Battery1Per::from(34)) 250 .add_data(Temperature10mK::from(2255)) 251 .add_data(Humidity10mPer::from(3400)) 252 .add_data(Illuminance10mLux::from(45000)) 253 .add_data(Moisture10mPer::from(3632)) 254 .add_local_name("rsachy") 255 .encode(); 256 257 // Final payload is within the max size for the advertising payload 258 assert_eq!(encoded.len(), 31); 259 assert_eq!(home.buffer[3], 19); 260 261 let mut home = BtHomeAd::default(); 262 263 let encoded = home 264 .add_data(Battery1Per::from(34)) 265 .add_data(Temperature10mK::from(2255)) 266 .add_data(Illuminance10mLux::from(45000)) 267 .add_data(Voltage1mV::from(2800)) 268 .add_data(Humidity1Per::from(34)) 269 .add_data(Moisture1Per::from(36)) 270 .add_local_name("sachy") 271 .encode(); 272 273 // Final payload is within the max size for the advertising payload 274 assert_eq!(encoded.len(), 31); 275 assert_eq!(home.buffer[3], 20); 276 } 277}