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 = 64;
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    LeSetEventMask = 0x0001,
52    LeSetRandomAddress = 0x0005,
53    SetAdvertisingParams = 0x0006,
54    SetAdvertisingData = 0x0008,
55    SetScanResponseData = 0x0009,
56    SetAdvertisingEnable = 0x000a,
57    SetScanParams = 0x000b,
58    SetScanEnable = 0x000c,
59    CreateConnection = 0x000d,
60    CreateConnectionCancel = 0x000e,
61    ReadFilterAcceptListSize = 0x000f,
62    ClearFilterAcceptList = 0x0010,
63    AddDeviceToFilterAcceptList = 0x0011,
64    RemoveDeviceFromFilterAcceptList = 0x0012,
65    PeriodicAdvertisingCreateSync = 0x0044,
66    PeriodicAdvertisingCreateSyncCancel = 0x0045,
67    AddDeviceToPeriodicAdvertiserList = 0x0047,
68    RemoveDeviceFromPeriodicAdvertiserList = 0x0048,
69    PeriodicAdvertisingReceiveEnable = 0x0059,
70    PeriodicAdvertisingSyncTransfer = 0x005a,
71}
72
73pub fn make_hci_opcode(ogf: HciOgf, ocf: HciOcf) -> u16 {
74    ((ogf as u16) << 10) | ocf as u16
75}
76
77#[derive(Format)]
78/// See [Bluetooth Assigned Numbers, section 2.3: Common Data Types](https://www.bluetooth.com/specifications/assigned-numbers/)
79pub enum AdvData<'a> {
80    Flags(u8),
81    Incomplete16BitUuids(&'a [u8]),
82    Complete16BitUuids(&'a [u8]), // len = 2 × n
83    Incomplete32BitUuids(&'a [u8]),
84    Complete32BitUuids(&'a [u8]),
85    Incomplete128BitUuids(&'a [u8]),
86    Complete128BitUuids(&'a [u8]),
87    ShortenedLocalName(&'a str),
88    CompleteLocalName(&'a str),
89    ClassOfDevice(&'a [u8]), // todo: type
90    DeviceId(&'a [u8]),      // todo: Type
91    ServiceData16Bit(&'a [u8]),
92    Manufacturer { company: u16, data: &'a [u8] },
93    Other { typ: u8, data: &'a [u8] },
94}
95
96/// An advertising report
97// todo: Derive Format once we get defmt working with Heapless.
98// #[derive(Format)]
99pub struct AdvReport<'a> {
100    pub evt_type: u8,   // ADV_IND, ADV_NONCONN_IND, SCAN_RSP …
101    pub addr_type: u8,  // 0 = public, 1 = random, …
102    pub addr: [u8; 6],  // LSB first (as on the wire)
103    pub data: &'a [u8], // advertising data (slice into original buf)
104    pub rssi: i8,       // signed dBm
105    pub data_parsed: Vec<AdvData<'a>, MAX_NUM_ADV_DATA>,
106}
107
108// todo temp for heapless::Vec missing defmt
109impl<'a> Format for AdvReport<'a> {
110    fn format(&self, f: Formatter) {
111        // Print the header line with fixed fields.
112        defmt::write!(
113            f,
114            "AdvReport {{ evt_type: {}, addr_type: {}, addr: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}, \
115             rssi: {} dBm, data_len: {}",
116            self.evt_type,
117            self.addr_type,
118            // reverse for human-friendly big-endian display
119            self.addr[5],
120            self.addr[4],
121            self.addr[3],
122            self.addr[2],
123            self.addr[1],
124            self.addr[0],
125            self.rssi,
126            self.data.len(),
127        );
128
129        // Start the parsed-data list.
130        defmt::write!(f, ", data_parsed: \n[");
131
132        // Iterate over every AdvData entry, separated by commas.
133        let mut first = true;
134        for ad in &self.data_parsed {
135            if !first {
136                defmt::write!(f, ", ");
137            }
138            first = false;
139            defmt::write!(f, "{}", ad); // assumes AdvData already impls `Format`
140        }
141
142        // Close the list and the struct.
143        defmt::write!(f, "] }}");
144    }
145}
146
147#[derive(Clone, Copy, Format, TryFromPrimitive, Default)]
148#[repr(u8)]
149pub enum BleScanType {
150    Passive = 0,
151    #[default]
152    Active = 1,
153}
154
155#[derive(Clone, Copy, Format)]
156#[repr(u8)]
157pub enum BleOwnAddrType {
158    Public = 0,
159    Private = 1,
160}
161
162#[derive(Clone, Copy, Format)]
163#[repr(u8)]
164pub enum FilterPolicy {
165    AcceptAll = 0,
166    WhitelistOnly = 1,
167}
168
169pub struct BleScanParams {
170    pub scan_type: BleScanType,
171    pub interval: u16, // ms
172    /// Must be shorter than, or equal to the interval.
173    pub window: u16, // ms
174    pub own_address_type: BleOwnAddrType,
175    pub filter_policy: FilterPolicy,
176}
177
178impl BleScanParams {
179    pub fn to_bytes(&self) -> [u8; 7] {
180        let mut result = [0; 7];
181
182        // Convert to time units of 0.625ms.
183        let interval = ((self.interval as f32) / 0.625).round() as u16;
184        let window = ((self.window as f32) / 0.625).round() as u16;
185
186        result[0] = self.scan_type as u8;
187        result[1..3].copy_from_slice(&interval.to_le_bytes());
188        result[3..5].copy_from_slice(&window.to_le_bytes());
189        result[5] = self.own_address_type as u8;
190        result[6] = self.filter_policy as u8;
191
192        result
193    }
194}
195
196/// Build helper to push (pkt_type, opcode, params) into an ESP-Hosted frame.
197/// Construct the opcode from OGF and OCF, using `make_hci_opcode()`.
198pub fn make_hci_cmd(opcode: u16, params: &[u8]) -> ([u8; HCI_TX_MAX_LEN], usize) {
199    let mut payload = [0; HCI_TX_MAX_LEN];
200
201    // payload[0] = HciPkt::Cmd as u8;
202    payload[0..2].copy_from_slice(&(opcode).to_le_bytes());
203    payload[2] = params.len() as u8;
204    payload[3..3 + params.len()].copy_from_slice(params);
205
206    // println!("Writing HCI payload: {:?}", payload[..3 + params.len()]);
207
208    (payload, HCI_HDR_SIZE + params.len())
209}
210
211pub fn parse_adv_data(mut d: &[u8]) -> Vec<AdvData<'_>, MAX_NUM_ADV_DATA> {
212    let mut result = Vec::<AdvData, MAX_NUM_ADV_DATA>::new();
213
214    while !d.is_empty() {
215        let len = d[0] as usize;
216        if len == 0 || len > d.len() - 1 {
217            break;
218        }
219
220        let ad_type = d[1];
221        let val = &d[2..1 + len];
222
223        // https://www.bluetooth.com/specifications/assigned-numbers/
224        match ad_type {
225            0x01 if val.len() == 1 => {
226                let _ = result.push(AdvData::Flags(val[0]));
227            }
228            0x02 if val.len() == 1 => {
229                let _ = result.push(AdvData::Incomplete16BitUuids(val));
230            }
231            0x03 => {
232                let _ = result.push(AdvData::Complete16BitUuids(val));
233            }
234            0x04 => {
235                let _ = result.push(AdvData::Incomplete32BitUuids(val));
236            }
237            0x05 => {
238                let _ = result.push(AdvData::Complete32BitUuids(val));
239            }
240            0x06 => {
241                let _ = result.push(AdvData::Incomplete128BitUuids(val));
242            }
243            0x07 => {
244                let _ = result.push(AdvData::Complete128BitUuids(val));
245            }
246            0x08 => {
247                if let Ok(s) = core::str::from_utf8(val) {
248                    let _ = result.push(AdvData::ShortenedLocalName(s));
249                }
250            }
251            0x09 => {
252                if let Ok(s) = core::str::from_utf8(val) {
253                    let _ = result.push(AdvData::CompleteLocalName(s));
254                }
255            }
256            0x16 => {
257                let _ = result.push(AdvData::ServiceData16Bit(val));
258            }
259            0x0d => {
260                let _ = result.push(AdvData::ClassOfDevice(val));
261            }
262            0x10 => {
263                let _ = result.push(AdvData::DeviceId(val));
264            }
265            0xFF if val.len() >= 2 => {
266                let company = u16::from_le_bytes([val[0], val[1]]);
267                let _ = result.push(AdvData::Manufacturer {
268                    company,
269                    data: &val[2..],
270                });
271            }
272            _ => {
273                let _ = result.push(AdvData::Other {
274                    typ: ad_type,
275                    data: val,
276                });
277            }
278        }
279
280        d = &d[1 + len..];
281    }
282
283    // println!("Adv data len: {:?}", result.len()); // todo temp
284
285    result
286}
287
288// #[derive(Format)]
289pub enum HciEvent<'a> {
290    CommandComplete {
291        n_cmd: u8, // todo: Is this the cmd?
292        opcode: u16,
293        status: u8,
294        rest: &'a [u8],
295    },
296    AdvertisingReport {
297        reports: Vec<AdvReport<'a>, MAX_NUM_ADV_REPS>, // up to 4 reports per event
298    },
299    Unknown {
300        evt: u8,
301        params: &'a [u8],
302    },
303}
304
305// todo: Until format works on heapless::Vec.
306impl<'a> Format for HciEvent<'a> {
307    fn format(&self, fmt: Formatter) {
308        match self {
309            HciEvent::CommandComplete {
310                n_cmd,
311                opcode,
312                status,
313                rest,
314            } => {
315                defmt::write!(
316                    fmt,
317                    "CommandComplete {{ n_cmd: {}, opcode: {}, status: {}, rest: {=[u8]} }}",
318                    *n_cmd,
319                    *opcode,
320                    *status,
321                    rest
322                );
323            }
324            HciEvent::AdvertisingReport { reports } => {
325                // Vec<AdvReport> doesn’t impl Format, so just show how many reports we have
326                defmt::write!(fmt, "Advertising reports:");
327                for rep in reports {
328                    defmt::write!(fmt, "\n-{}; ", rep);
329                }
330            }
331            HciEvent::Unknown { evt, params } => {
332                defmt::write!(fmt, "Unknown {{ evt: {}, params: {=[u8]} }}", *evt, params);
333            }
334        }
335    }
336}
337
338#[derive(Clone, Copy, PartialEq, Format, TryFromPrimitive)]
339#[repr(u8)]
340pub enum HciEventType {
341    InquiryComplete = 0x01,
342    InquiryResult = 0x02,
343    ConnectionComplete = 0x03,
344    ConnectionRequest = 0x04,
345    CommandComplete = 0x0E,
346    LeAdvertising = 0x3E,
347    // todo: more A/R
348}
349
350pub fn parse_hci_events(buf: &[u8]) -> Result<Vec<HciEvent, MAX_HCI_EVS>, EspError> {
351    let mut result = Vec::<HciEvent, MAX_HCI_EVS>::new();
352
353    let mut i = 0;
354
355    while i + HCI_HDR_SIZE <= buf.len() {
356        // Parse all packets present in this payload.
357        if buf[i] != HciPkt::Evt as u8 {
358            // todo: This is causing early aborts!
359            // println!("Non-event HCI packet: {:?}", buf[i..i + 30]);
360            // println!("HCI pkt count: {:?}. buf len: {:?} / {:?}", result.len(), i, buf.len()); // todo temp
361            // println!("HCI pkt count: {:?}", result.len()); // todo temp
362
363            return Ok(result);
364        }
365
366        // Parse the HCI header.
367        let evt_type: HciEventType = match buf[i + 1].try_into() {
368            Ok(evt) => evt,
369            Err(e) => {
370                println!("Error parsing HCI event: {:?}", buf[i + 1]); // todo temp
371                return Err(EspError::InvalidData);
372            }
373        };
374
375        let packet_len = buf[i + 2] as usize;
376
377        if i + 3 + packet_len > buf.len() {
378            println!("Buf not long enough for HCI event");
379            return Err(EspError::InvalidData);
380        }
381
382        let params = &buf[i + 3..i + 3 + packet_len];
383
384        match evt_type {
385            HciEventType::CommandComplete => {
386                let n_cmd = params[0];
387                let opcode = u16::from_le_bytes([params[1], params[2]]);
388
389                let status = params[3];
390                result
391                    .push(HciEvent::CommandComplete {
392                        n_cmd,
393                        opcode,
394                        status,
395                        rest: &params[4..],
396                    })
397                    .ok();
398            }
399
400            //  LE Advertising Report
401            HciEventType::LeAdvertising => {
402                if params[0] == 0x02 {
403                    // sub-event 0x02, params[1] = number of reports
404                    let num = params[1] as usize;
405                    let mut idx = 2;
406                    let mut reports = Vec::<AdvReport, MAX_NUM_ADV_REPS>::new();
407
408                    for _ in 0..num {
409                        // minimum bytes per report: 1(evt) + 1(addr_t) + 6(addr)
410                        // + 1(data_len) + 0(data) + 1(rssi) = 10
411                        if idx + 10 > params.len() {
412                            break;
413                        }
414
415                        let evt_type = params[idx];
416                        idx += 1;
417                        let addr_type = params[idx];
418                        idx += 1;
419
420                        let mut addr = [0u8; 6];
421                        addr.copy_from_slice(&params[idx..idx + 6]);
422                        idx += 6;
423
424                        let data_len = params[idx] as usize;
425                        idx += 1;
426                        if idx + data_len + 1 > params.len() {
427                            break;
428                        }
429
430                        let data = &params[idx..idx + data_len];
431                        idx += data_len;
432
433                        let rssi = params[idx] as i8;
434                        idx += 1;
435
436                        reports
437                            .push(AdvReport {
438                                evt_type,
439                                addr_type,
440                                addr,
441                                data,
442                                rssi,
443                                data_parsed: parse_adv_data(data),
444                            })
445                            .ok();
446                    }
447
448                    // println!("Reps len: {:?}", reports.len()); // todo temp
449
450                    result.push(HciEvent::AdvertisingReport { reports }).ok();
451                }
452            }
453
454            _ => {
455                println!("\n\nUnknown HCI evt type: {:?}", evt_type);
456
457                if result
458                    .push(HciEvent::Unknown {
459                        evt: evt_type as u8,
460                        params,
461                    })
462                    .is_err()
463                {
464                    return Err(EspError::Capacity);
465                }
466            }
467        }
468
469        i += HCI_HDR_SIZE + packet_len;
470    }
471
472    // todo: Should return a capacity error here probably.
473
474    Ok(result)
475}
476
477/// integer conversion: ms -> 0.625ms units with rounding
478#[inline]
479fn ms_to_0p625_units(ms: u16) -> u16 {
480    // units = round(ms / 0.625) = round(ms * 1600 / 1000)
481    let v = (ms as u32) * 1600 + 500; // +500 for rounding
482    (v / 1_000) as u16
483}
484
485/// 15-byte payload for LE Set Advertising Parameters (Core v5.x spec)
486pub fn le_set_adv_params_bytes(interval_ms: u16, adv_type: u8, own_addr_type: u8) -> [u8; 15] {
487    let units = ms_to_0p625_units(interval_ms);
488    let mut p = [0u8; 15];
489
490    // Advertising_Interval_Min / Max
491    p[0..2].copy_from_slice(&units.to_le_bytes());
492    p[2..4].copy_from_slice(&units.to_le_bytes());
493
494    // Advertising_Type (0x00 = ADV_IND, 0x03 = ADV_NONCONN_IND)
495    p[4] = adv_type;
496
497    // Own_Address_Type (0x00 = public)
498    p[5] = 0x00;
499
500    // Peer_Address_Type (ignored for *_UND* types)
501    // 0: public. 1: random
502    p[6] = own_addr_type;
503
504    // Peer_Address (ignored for *_UND* types)
505    // already zeros at p[7..13]
506
507    // Advertising_Channel_Map (all three channels)
508    p[13] = 0x07;
509
510    // Advertising_Filter_Policy (process scan & connect req from any)
511    p[14] = 0x00;
512
513    p
514}
515
516// 32-byte payload for LE Set Advertising Data: [len_used, data[31]]
517pub fn le_set_adv_data_manu(company_id: u16, manu_data: &[u8]) -> Result<[u8; 32], EspError> {
518    // AD structure: [ad_len, 0xFF, company_le(2), manu_payload...]
519    // Total bytes consumed inside the 31-byte field:
520    //   used = 1 /*ad_len*/ + (1 /*type*/ + 2 /*company*/ + manu.len())
521    let used = 1 + 1 + 2 + manu_data.len();
522    if used > 31 {
523        return Err(EspError::Capacity);
524    }
525
526    let mut params = [0u8; 32];
527    params[0] = used as u8; // Advertising_Data_Length
528
529    let ad_len = (1 + 2 + manu_data.len()) as u8;
530    params[1] = ad_len; // AD length
531    params[2] = 0xFF; // AD type = Manufacturer Specific Data
532    params[3..5].copy_from_slice(&company_id.to_le_bytes());
533    params[5..5 + manu_data.len()].copy_from_slice(manu_data);
534    // remaining bytes already zero-padded
535
536    Ok(params)
537}
538
539pub fn le_set_scan_rsp_name(name: &[u8]) -> Result<[u8; 32], EspError> {
540    let use_len = core::cmp::min(name.len(), 29); // 31 total: 1(len) + 1(type) + N
541    let typ = if name.len() <= 29 { 0x09 } else { 0x08 }; // Complete or Shortened
542
543    let mut p = [0u8; 32];
544    p[0] = (1 + 1 + use_len) as u8; // Scan_Response_Data_Length
545    p[1] = (1 + use_len) as u8; // AD length
546    p[2] = typ; // AD type: 0x09=Complete Name, 0x08=Shortened
547    p[3..3 + use_len].copy_from_slice(&name[..use_len]);
548
549    Ok(p)
550}