esp_hosted/
ble.rs

1//! Minimal HCI support for Bluetooth operations.
2//!
3//! See [the BLE docs, Part E. Host Controller Interface Functional Specification](https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-61/out/en/host-controller-interface/host-controller-interface-functional-specification.html)
4
5use defmt::{Format, Formatter, println};
6use heapless::Vec;
7use num_enum::TryFromPrimitive;
8use num_traits::float::FloatCore;
9
10use crate::EspError;
11
12// todo: Experiment; set these A/R. Are these configured
13pub const MAX_HCI_EVS: usize = 2; // Measured typical: ~1
14const MAX_NUM_ADV_DATA: usize = 5; // Measured typical: ~3
15pub const MAX_NUM_ADV_REPS: usize = 3; // Measured typical: ~1
16
17// For Event Packets (0x04), Byte 0 is the Event Code (e.g. 0x3E for LE Meta‐Event).
18// For Command Packets (0x01), Bytes 0–1 together form the OpCode, and Byte 2 is the parameter length.
19const HCI_HDR_SIZE: usize = 3;
20
21const HCI_TX_MAX_LEN: usize = 30; // todo: Raise A/R.
22
23#[derive(Clone, Copy, PartialEq, TryFromPrimitive, Format)]
24#[repr(u8)]
25pub enum HciPkt {
26    Cmd = 0x01,
27    Acl = 0x02,
28    Sco = 0x03,
29    Evt = 0x04,
30}
31
32/// https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-61/out/en/host-controller-interface/host-controller-interface-functional-specification.html#UUID-b0b17bc4-7719-7867-d773-1cd21c76fcd5
33#[derive(Clone, Copy, PartialEq, Format, TryFromPrimitive)]
34#[repr(u8)]
35pub enum HciOgf {
36    NoOperation = 0x00,
37    LinkControl = 0x01,
38    LinkPolicy = 0x02,
39    ControllerAndBaseboard = 0x03,
40    InformationParams = 0x04,
41    StatusParams = 0x05,
42    TestingCmds = 0x06,
43    LeController = 0x08,
44    VendorSPecificCmds = 0x3f,
45}
46
47/// https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-61/out/en/host-controller-interface/host-controller-interface-functional-specification.html#UUID-b0b17bc4-7719-7867-d773-1cd21c76fcd5
48#[derive(Clone, Copy, PartialEq, Format, TryFromPrimitive)]
49#[repr(u16)]
50pub enum HciOcf {
51    SetAdvertisingData = 0x0008,
52    SetScanResponseData = 0x0009,
53    SetAdvertisingEnable = 0x000a,
54    SetScanParams = 0x000b,
55    SetScanEnable = 0x000c,
56    CreateConnection = 0x000d,
57    CreateConnectionCancel = 0x000e,
58    ReadFilterAcceptListSize = 0x000f,
59    ClearFilterAcceptList = 0x0010,
60    AddDeviceToFilterAcceptList = 0x0011,
61    RemoveDeviceFromFilterAcceptList = 0x0012,
62    PeriodicAdvertisingCreateSync = 0x0044,
63    PeriodicAdvertisingCreateSyncCancel = 0x0045,
64    AddDeviceToPeriodicAdvertiserList = 0x0047,
65    RemoveDeviceFromPeriodicAdvertiserList = 0x00487,
66    PeriodicAdvertisingReceiveEnable = 0x0059,
67    PeriodicAdvertisingSyncTransfer = 0x005a,
68}
69
70pub fn make_hci_opcode(ogf: HciOgf, ocf: HciOcf) -> u16 {
71    ((ogf as u16) << 10) | ocf as u16
72}
73
74#[derive(Format)]
75/// See [Bluetooth Assigned Numbers, section 2.3: Common Data Types](https://www.bluetooth.com/specifications/assigned-numbers/)
76pub enum AdvData<'a> {
77    Flags(u8),
78    Incomplete16BitUuids(&'a [u8]),
79    Complete16BitUuids(&'a [u8]), // len = 2 × n
80    Incomplete32BitUuids(&'a [u8]),
81    Complete32BitUuids(&'a [u8]),
82    Incomplete128BitUuids(&'a [u8]),
83    Complete128BitUuids(&'a [u8]),
84    ShortenedLocalName(&'a str),
85    CompleteLocalName(&'a str),
86    ClassOfDevice(&'a [u8]), // todo: type
87    DeviceId(&'a [u8]),      // todo: Type
88    ServiceData16Bit(&'a [u8]),
89    Manufacturer { company: u16, data: &'a [u8] },
90    Other { typ: u8, data: &'a [u8] },
91}
92
93/// An advertising report
94// todo: Derive Format once we get defmt working with Heapless.
95// #[derive(Format)]
96pub struct AdvReport<'a> {
97    pub evt_type: u8,   // ADV_IND, ADV_NONCONN_IND, SCAN_RSP …
98    pub addr_type: u8,  // 0 = public, 1 = random, …
99    pub addr: [u8; 6],  // LSB first (as on the wire)
100    pub data: &'a [u8], // advertising data (slice into original buf)
101    pub rssi: i8,       // signed dBm
102    pub data_parsed: Vec<AdvData<'a>, MAX_NUM_ADV_DATA>,
103}
104
105// todo temp for heapless::Vec missing defmt
106impl<'a> Format for AdvReport<'a> {
107    fn format(&self, f: Formatter) {
108        // Print the header line with fixed fields.
109        defmt::write!(
110            f,
111            "AdvReport {{ evt_type: {}, addr_type: {}, addr: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}, \
112             rssi: {} dBm, data_len: {}",
113            self.evt_type,
114            self.addr_type,
115            // reverse for human-friendly big-endian display
116            self.addr[5],
117            self.addr[4],
118            self.addr[3],
119            self.addr[2],
120            self.addr[1],
121            self.addr[0],
122            self.rssi,
123            self.data.len(),
124        );
125
126        // Start the parsed-data list.
127        defmt::write!(f, ", data_parsed: \n[");
128
129        // Iterate over every AdvData entry, separated by commas.
130        let mut first = true;
131        for ad in &self.data_parsed {
132            if !first {
133                defmt::write!(f, ", ");
134            }
135            first = false;
136            defmt::write!(f, "{}", ad); // assumes AdvData already impls `Format`
137        }
138
139        // Close the list and the struct.
140        defmt::write!(f, "] }}");
141    }
142}
143
144#[derive(Clone, Copy, Format, TryFromPrimitive, Default)]
145#[repr(u8)]
146pub enum BleScanType {
147    Passive = 0,
148    #[default]
149    Active = 1,
150}
151
152#[derive(Clone, Copy, Format)]
153#[repr(u8)]
154pub enum BleOwnAddrType {
155    Public = 0,
156    Private = 1,
157}
158
159#[derive(Clone, Copy, Format)]
160#[repr(u8)]
161pub enum FilterPolicy {
162    AcceptAll = 0,
163    WhitelistOnly = 1,
164}
165
166pub struct BleScanParams {
167    pub scan_type: BleScanType,
168    pub interval: u16, // ms
169    /// Must be shorter than, or equal to the interval.
170    pub window: u16, // ms
171    pub own_address_type: BleOwnAddrType,
172    pub filter_policy: FilterPolicy,
173}
174
175impl BleScanParams {
176    pub fn to_bytes(&self) -> [u8; 7] {
177        let mut result = [0; 7];
178
179        // Convert to time units of 0.625ms.
180        let interval = ((self.interval as f32) / 0.625).round() as u16;
181        let window = ((self.window as f32) / 0.625).round() as u16;
182
183        result[0] = self.scan_type as u8;
184        result[1..3].copy_from_slice(&interval.to_le_bytes());
185        result[3..5].copy_from_slice(&window.to_le_bytes());
186        result[5] = self.own_address_type as u8;
187        result[6] = self.filter_policy as u8;
188
189        result
190    }
191}
192
193/// Build helper to push (pkt_type, opcode, params) into an ESP-Hosted frame.
194/// Construct the opcode from OGF and OCF, using `make_hci_opcode()`.
195pub fn make_hci_cmd(opcode: u16, params: &[u8]) -> ([u8; HCI_TX_MAX_LEN], usize) {
196    let mut payload = [0; HCI_TX_MAX_LEN];
197
198    // payload[0] = HciPkt::Cmd as u8;
199    payload[0..2].copy_from_slice(&(opcode).to_le_bytes());
200    payload[2] = params.len() as u8;
201    payload[3..3 + params.len()].copy_from_slice(params);
202
203    // println!("Writing HCI payload: {:?}", payload[..3 + params.len()]);
204
205    (payload, HCI_HDR_SIZE + params.len())
206}
207
208pub fn parse_adv_data(mut d: &[u8]) -> Vec<AdvData<'_>, MAX_NUM_ADV_DATA> {
209    let mut result = Vec::<AdvData, MAX_NUM_ADV_DATA>::new();
210
211    while !d.is_empty() {
212        let len = d[0] as usize;
213        if len == 0 || len > d.len() - 1 {
214            break;
215        }
216
217        let ad_type = d[1];
218        let val = &d[2..1 + len];
219
220        // https://www.bluetooth.com/specifications/assigned-numbers/
221        match ad_type {
222            0x01 if val.len() == 1 => {
223                let _ = result.push(AdvData::Flags(val[0]));
224            }
225            0x02 if val.len() == 1 => {
226                let _ = result.push(AdvData::Incomplete16BitUuids(val));
227            }
228            0x03 => {
229                let _ = result.push(AdvData::Complete16BitUuids(val));
230            }
231            0x04 => {
232                let _ = result.push(AdvData::Incomplete32BitUuids(val));
233            }
234            0x05 => {
235                let _ = result.push(AdvData::Complete32BitUuids(val));
236            }
237            0x06 => {
238                let _ = result.push(AdvData::Incomplete128BitUuids(val));
239            }
240            0x07 => {
241                let _ = result.push(AdvData::Complete128BitUuids(val));
242            }
243            0x08 => {
244                if let Ok(s) = core::str::from_utf8(val) {
245                    let _ = result.push(AdvData::ShortenedLocalName(s));
246                }
247            }
248            0x09 => {
249                if let Ok(s) = core::str::from_utf8(val) {
250                    let _ = result.push(AdvData::CompleteLocalName(s));
251                }
252            }
253            0x16 => {
254                let _ = result.push(AdvData::ServiceData16Bit(val));
255            }
256            0x0d => {
257                let _ = result.push(AdvData::ClassOfDevice(val));
258            }
259            0x10 => {
260                let _ = result.push(AdvData::DeviceId(val));
261            }
262            0xFF if val.len() >= 2 => {
263                let company = u16::from_le_bytes([val[0], val[1]]);
264                let _ = result.push(AdvData::Manufacturer {
265                    company,
266                    data: &val[2..],
267                });
268            }
269            _ => {
270                let _ = result.push(AdvData::Other {
271                    typ: ad_type,
272                    data: val,
273                });
274            }
275        }
276
277        d = &d[1 + len..];
278    }
279
280    // println!("Adv data len: {:?}", result.len()); // todo temp
281
282    result
283}
284
285// #[derive(Format)]
286pub enum HciEvent<'a> {
287    CommandComplete {
288        n_cmd: u8, // todo: Is this the cmd?
289        opcode: u16,
290        status: u8,
291        rest: &'a [u8],
292    },
293    AdvertisingReport {
294        reports: Vec<AdvReport<'a>, MAX_NUM_ADV_REPS>, // up to 4 reports per event
295    },
296    Unknown {
297        evt: u8,
298        params: &'a [u8],
299    },
300}
301
302// todo: Until format works on heapless::Vec.
303impl<'a> Format for HciEvent<'a> {
304    fn format(&self, fmt: Formatter) {
305        match self {
306            HciEvent::CommandComplete {
307                n_cmd,
308                opcode,
309                status,
310                rest,
311            } => {
312                defmt::write!(
313                    fmt,
314                    "CommandComplete {{ n_cmd: {}, opcode: {}, status: {}, rest: {=[u8]} }}",
315                    *n_cmd,
316                    *opcode,
317                    *status,
318                    rest
319                );
320            }
321            HciEvent::AdvertisingReport { reports } => {
322                // Vec<AdvReport> doesn’t impl Format, so just show how many reports we have
323                defmt::write!(fmt, "Advertising reports:");
324                for rep in reports {
325                    defmt::write!(fmt, "\n-{}; ", rep);
326                }
327            }
328            HciEvent::Unknown { evt, params } => {
329                defmt::write!(fmt, "Unknown {{ evt: {}, params: {=[u8]} }}", *evt, params);
330            }
331        }
332    }
333}
334
335#[derive(Clone, Copy, PartialEq, Format, TryFromPrimitive)]
336#[repr(u8)]
337pub enum HciEventType {
338    InquiryComplete = 0x01,
339    InquiryResult = 0x02,
340    ConnectionComplete = 0x03,
341    ConnectionRequest = 0x04,
342    CommandComplete = 0x0E,
343    LeAdvertising = 0x3E,
344    // todo: more A/R
345}
346
347pub fn parse_hci_events(buf: &[u8]) -> Result<Vec<HciEvent, MAX_HCI_EVS>, EspError> {
348    let mut result = Vec::<HciEvent, MAX_HCI_EVS>::new();
349
350    let mut i = 0;
351
352    while i + HCI_HDR_SIZE <= buf.len() {
353        // Parse all packets present in this payload.
354        if buf[i] != HciPkt::Evt as u8 {
355            // todo: This is causing early aborts!
356            // println!("Non-event HCI packet: {:?}", buf[i..i + 30]);
357            // println!("HCI pkt count: {:?}. buf len: {:?} / {:?}", result.len(), i, buf.len()); // todo temp
358            // println!("HCI pkt count: {:?}", result.len()); // todo temp
359
360            return Ok(result);
361        }
362
363        // Parse the HCI header.
364        let evt_type: HciEventType = match buf[i + 1].try_into() {
365            Ok(evt) => evt,
366            Err(e) => {
367                println!("Error parsing HCI event: {:?}", buf[i + 1]); // todo temp
368                return Err(EspError::InvalidData);
369            }
370        };
371
372        let packet_len = buf[i + 2] as usize;
373
374        if i + 3 + packet_len > buf.len() {
375            println!("Buf not long enough for HCI event");
376            return Err(EspError::InvalidData);
377        }
378
379        let params = &buf[i + 3..i + 3 + packet_len];
380
381        match evt_type {
382            HciEventType::CommandComplete => {
383                let n_cmd = params[0];
384                let opcode = u16::from_le_bytes([params[1], params[2]]);
385
386                let status = params[3];
387                result
388                    .push(HciEvent::CommandComplete {
389                        n_cmd,
390                        opcode,
391                        status,
392                        rest: &params[4..],
393                    })
394                    .ok();
395            }
396
397            //  LE Advertising Report
398            HciEventType::LeAdvertising => {
399                if params[0] == 0x02 {
400                    // sub-event 0x02, params[1] = number of reports
401                    let num = params[1] as usize;
402                    let mut idx = 2;
403                    let mut reports = Vec::<AdvReport, MAX_NUM_ADV_REPS>::new();
404
405                    for _ in 0..num {
406                        // minimum bytes per report: 1(evt) + 1(addr_t) + 6(addr)
407                        // + 1(data_len) + 0(data) + 1(rssi) = 10
408                        if idx + 10 > params.len() {
409                            break;
410                        }
411
412                        let evt_type = params[idx];
413                        idx += 1;
414                        let addr_type = params[idx];
415                        idx += 1;
416
417                        let mut addr = [0u8; 6];
418                        addr.copy_from_slice(&params[idx..idx + 6]);
419                        idx += 6;
420
421                        let data_len = params[idx] as usize;
422                        idx += 1;
423                        if idx + data_len + 1 > params.len() {
424                            break;
425                        }
426
427                        let data = &params[idx..idx + data_len];
428                        idx += data_len;
429
430                        let rssi = params[idx] as i8;
431                        idx += 1;
432
433                        reports
434                            .push(AdvReport {
435                                evt_type,
436                                addr_type,
437                                addr,
438                                data,
439                                rssi,
440                                data_parsed: parse_adv_data(data),
441                            })
442                            .ok();
443                    }
444
445                    // println!("Reps len: {:?}", reports.len()); // todo temp
446
447                    result.push(HciEvent::AdvertisingReport { reports }).ok();
448                }
449            }
450
451            _ => {
452                println!("\n\nUnknown HCI evt type: {:?}", evt_type);
453
454                if result
455                    .push(HciEvent::Unknown {
456                        evt: evt_type as u8,
457                        params,
458                    })
459                    .is_err()
460                {
461                    return Err(EspError::Capacity);
462                }
463            }
464        }
465
466        i += HCI_HDR_SIZE + packet_len;
467    }
468
469    // todo: Should return a capacity error here probably.
470
471    Ok(result)
472}