Repo of no-std crates for my personal embedded projects
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}