Skip to content

Commit 3cd9bf3

Browse files
committed
fix: support special command complete
Command complete may not have status as part of the payload according to the specification: https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-54/out/en/host-controller-interface/host-controller-interface-functional-specification.html#UUID-76d31a33-1a9e-07bc-87c4-8ebffee065fd This breaks controllers that emit this special command complete event. Add unit test for external controller Fixes #59
1 parent 5c8b60b commit 3cd9bf3

File tree

6 files changed

+230
-5
lines changed

6 files changed

+230
-5
lines changed

Cargo.lock

Lines changed: 80 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ edition = "2021"
55
license = "MIT OR Apache-2.0"
66
name = "bt-hci"
77
repository = "https://github.com/embassy-rs/bt-hci"
8-
version = "0.5.0"
8+
version = "0.6.0"
99
documentation = "https://docs.rs/bt-hci"
1010
keywords = ["bluetooth", "hci", "BLE"]
1111
categories = ["embedded", "hardware-support", "no-std"]
@@ -38,3 +38,4 @@ serde = { version = "^1", optional = true, features = [
3838

3939
[dev-dependencies]
4040
postcard = "1.1"
41+
futures-test = "0.3"

src/cmd.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ param!(
5252
);
5353

5454
impl Opcode {
55+
/// Special opcode for command events with no associated command
56+
pub const UNSOLICITED: Opcode = Opcode::new(OpcodeGroup(0), 0);
57+
5558
/// Create an `Opcode` with the given OGF and OCF values
5659
pub const fn new(ogf: OpcodeGroup, ocf: u16) -> Self {
5760
Self(((ogf.0 as u16) << 10) | ocf)

src/controller.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use embedded_io::ErrorType;
1313
use futures_intrusive::sync::LocalSemaphore;
1414

1515
use crate::cmd::{Cmd, CmdReturnBuf};
16-
use crate::event::{CommandComplete, CommandStatus, EventKind};
16+
use crate::event::{CommandComplete, CommandCompleteWithStatus, CommandStatus, EventKind};
1717
use crate::param::{RemainingBytes, Status};
1818
use crate::transport::Transport;
1919
use crate::{cmd, data, ControllerToHostPacket, FixedSizeValue, FromHciBytes, FromHciBytesError};
@@ -104,6 +104,10 @@ where
104104
ControllerToHostPacket::Event(ref event) => match event.kind {
105105
EventKind::CommandComplete => {
106106
let e = CommandComplete::from_hci_bytes_complete(event.data)?;
107+
if !e.has_status() {
108+
return Ok(value);
109+
}
110+
let e: CommandCompleteWithStatus = e.try_into()?;
107111
self.slots.complete(
108112
e.cmd_opcode,
109113
e.status,
@@ -199,6 +203,10 @@ where
199203
ControllerToHostPacket::Event(ref event) => match event.kind {
200204
EventKind::CommandComplete => {
201205
let e = CommandComplete::from_hci_bytes_complete(event.data)?;
206+
if !e.has_status() {
207+
return Ok(value);
208+
}
209+
let e: CommandCompleteWithStatus = e.try_into()?;
202210
self.slots.complete(
203211
e.cmd_opcode,
204212
e.status,
@@ -242,7 +250,7 @@ where
242250

243251
let result = slot.wait().await;
244252
let return_param_bytes = RemainingBytes::from_hci_bytes_complete(&retval.as_ref()[..result.len]).unwrap();
245-
let e = CommandComplete {
253+
let e = CommandCompleteWithStatus {
246254
num_hci_cmd_pkts: 0,
247255
status: result.status,
248256
cmd_opcode: C::OPCODE,
@@ -412,3 +420,60 @@ impl<F: FnOnce()> Drop for OnDrop<F> {
412420
unsafe { self.f.as_ptr().read()() }
413421
}
414422
}
423+
424+
#[cfg(test)]
425+
mod tests {
426+
use super::*;
427+
428+
pub struct TestTransport<'d> {
429+
pub rx: &'d [u8],
430+
}
431+
432+
#[derive(Clone, Copy, Debug, PartialEq)]
433+
pub struct Error;
434+
435+
impl From<FromHciBytesError> for Error {
436+
fn from(_: FromHciBytesError) -> Self {
437+
Self
438+
}
439+
}
440+
441+
impl ErrorType for TestTransport<'_> {
442+
type Error = Error;
443+
}
444+
impl embedded_io::Error for Error {
445+
fn kind(&self) -> embedded_io::ErrorKind {
446+
embedded_io::ErrorKind::Other
447+
}
448+
}
449+
impl Transport for TestTransport<'_> {
450+
fn read<'a>(&self, rx: &'a mut [u8]) -> impl Future<Output = Result<ControllerToHostPacket<'a>, Self::Error>> {
451+
async {
452+
let to_read = rx.len().min(self.rx.len());
453+
454+
rx[..to_read].copy_from_slice(&self.rx[..to_read]);
455+
let pkt = ControllerToHostPacket::from_hci_bytes_complete(&rx[..to_read])?;
456+
Ok(pkt)
457+
}
458+
}
459+
460+
fn write<T: crate::HostToControllerPacket>(&self, _val: &T) -> impl Future<Output = Result<(), Self::Error>> {
461+
async { todo!() }
462+
}
463+
}
464+
465+
#[futures_test::test]
466+
pub async fn test_can_handle_unsolicited_command_complete() {
467+
let t = TestTransport {
468+
rx: &[
469+
4, 0x0e, 3, // header
470+
1, 0, 0, // special command
471+
],
472+
};
473+
let c: ExternalController<_, 10> = ExternalController::new(t);
474+
475+
let mut rx = [0; 255];
476+
let pkt = c.read(&mut rx).await;
477+
assert!(pkt.is_ok());
478+
}
479+
}

src/event.rs

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,7 @@ events! {
283283
struct CommandComplete<'a>(0x0e) {
284284
num_hci_cmd_pkts: u8,
285285
cmd_opcode: Opcode,
286-
status: Status, // All return parameters have status as the first field
287-
return_param_bytes: RemainingBytes<'a>,
286+
bytes: RemainingBytes<'a>,
288287
}
289288

290289
/// Command Status event [📖](https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-54/out/en/host-controller-interface/host-controller-interface-functional-specification.html#UUID-4d87067c-be74-d2ff-d5c4-86416bf7af91)
@@ -736,6 +735,44 @@ impl<'de> ReadHci<'de> for Event<'de> {
736735
}
737736

738737
impl CommandComplete<'_> {
738+
/// Whether or not this event has a status
739+
pub fn has_status(&self) -> bool {
740+
self.cmd_opcode != Opcode::UNSOLICITED
741+
}
742+
}
743+
744+
impl<'d> TryFrom<CommandComplete<'d>> for CommandCompleteWithStatus<'d> {
745+
type Error = FromHciBytesError;
746+
fn try_from(e: CommandComplete<'d>) -> Result<CommandCompleteWithStatus<'d>, Self::Error> {
747+
if e.cmd_opcode == Opcode::UNSOLICITED {
748+
return Err(FromHciBytesError::InvalidSize);
749+
}
750+
let bytes = e.bytes.into_inner();
751+
let (status, remaining) = Status::from_hci_bytes(bytes)?;
752+
let return_param_bytes: RemainingBytes<'d> = RemainingBytes::from_hci_bytes_complete(remaining)?;
753+
Ok(Self {
754+
num_hci_cmd_pkts: e.num_hci_cmd_pkts,
755+
cmd_opcode: e.cmd_opcode,
756+
status,
757+
return_param_bytes,
758+
})
759+
}
760+
}
761+
762+
/// Struct representing a command complete event with status
763+
#[derive(Debug, Clone, PartialEq, Eq)]
764+
pub struct CommandCompleteWithStatus<'d> {
765+
/// Number of packets complete.
766+
pub num_hci_cmd_pkts: u8,
767+
/// Command opcode.
768+
pub cmd_opcode: Opcode,
769+
/// Command status.
770+
pub status: Status,
771+
/// Return parameters
772+
pub return_param_bytes: RemainingBytes<'d>,
773+
}
774+
775+
impl CommandCompleteWithStatus<'_> {
739776
/// Gets the connection handle associated with the command that has completed.
740777
///
741778
/// For commands that return the connection handle provided as a parameter as
@@ -921,6 +958,7 @@ impl InquiryResultWithRssi<'_> {
921958
#[cfg(test)]
922959
mod tests {
923960
use super::*;
961+
use crate::cmd::OpcodeGroup;
924962
use crate::event::le::LeEventPacket;
925963
use crate::param::*;
926964

@@ -1273,4 +1311,36 @@ mod tests {
12731311
assert_eq!(e.handle, ConnHandle::new(1));
12741312
assert!(matches!(e.central_clock_accuracy, ClockAccuracy::Ppm250));
12751313
}
1314+
1315+
#[test]
1316+
fn test_special_command_complete() {
1317+
let data = [
1318+
0x0e, 3, // header
1319+
1, 0, 0, // special command
1320+
];
1321+
1322+
let event = EventPacket::from_hci_bytes_complete(&data).unwrap();
1323+
assert!(matches!(event.kind, EventKind::CommandComplete));
1324+
let event = CommandComplete::from_hci_bytes_complete(event.data).unwrap();
1325+
assert_eq!(event.cmd_opcode, Opcode::new(OpcodeGroup::new(0), 0));
1326+
}
1327+
1328+
#[test]
1329+
fn test_normal_command_complete() {
1330+
let opcode = Opcode::new(OpcodeGroup::LE, 0x000D).to_raw().to_le_bytes();
1331+
let data = [
1332+
0x0e, 4, // header
1333+
1, opcode[0], opcode[1], // special command
1334+
0, // success
1335+
];
1336+
1337+
let event = EventPacket::from_hci_bytes_complete(&data).unwrap();
1338+
assert!(matches!(event.kind, EventKind::CommandComplete));
1339+
let event = CommandComplete::from_hci_bytes_complete(event.data).unwrap();
1340+
assert_eq!(event.cmd_opcode, Opcode::new(OpcodeGroup::LE, 0x000d));
1341+
1342+
let event: CommandCompleteWithStatus = event.try_into().unwrap();
1343+
assert_eq!(event.cmd_opcode, Opcode::new(OpcodeGroup::LE, 0x000d));
1344+
assert_eq!(Status::SUCCESS, event.status);
1345+
}
12761346
}

src/param.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ impl<'a> FromHciBytes<'a> for RemainingBytes<'a> {
6262
}
6363
}
6464

65+
impl<'a> RemainingBytes<'a> {
66+
pub(crate) fn into_inner(self) -> &'a [u8] {
67+
self.0
68+
}
69+
}
70+
6571
param!(struct BdAddr([u8; 6]));
6672

6773
impl BdAddr {

0 commit comments

Comments
 (0)