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