blob: b9128c30b8c07069c7b0b44934af7dd0fd78d8db [file] [log] [blame] [edit]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{format_err, Error};
use fidl_fuchsia_hardware_bluetooth::{
HciTransportEvent, HciTransportEventStream, HciTransportProxy, ReceivedPacket, SentPacket,
};
use futures::executor::block_on;
use futures::{ready, Stream, StreamExt};
use std::convert::TryFrom as _;
use std::fmt;
use std::pin::Pin;
use std::task::Poll;
use crate::types::{
decode_opcode, parse_inquiry_result, EventPacketType, InquiryResult, StatusCode,
};
/// Default HCI device on the system.
/// TODO: consider supporting more than one HCI device.
const DEFAULT_DEVICE: &str = "/dev/class/bt-hci/000";
/// A CommandChannel provides a `Stream` associated with the control channel for a single HCI device.
/// This stream can be polled for `EventPacket`s coming off the channel.
pub struct CommandChannel {
pub proxy: HciTransportProxy,
event_stream: HciTransportEventStream,
is_terminated: bool,
}
impl CommandChannel {
/// Create a new CommandChannel from a device path. This opens a new command channel, returning
/// an error if the device doesn't exist or the channel cannot be created.
pub fn new(device_path: &str) -> Result<CommandChannel, Error> {
let proxy = open_hci_transport(device_path)?;
CommandChannel::from_proxy(proxy)
}
/// Take a HciTransportProxy and wrap it in a `CommandChannel`.
fn from_proxy(proxy: HciTransportProxy) -> Result<CommandChannel, Error> {
let event_stream = proxy.take_event_stream();
Ok(CommandChannel { proxy, event_stream, is_terminated: false })
}
pub async fn send_command_packet(&self, buf: Vec<u8>) -> Result<(), Error> {
self.proxy.send_(&SentPacket::Command(buf)).await?;
Ok(())
}
}
impl Unpin for CommandChannel {}
impl Stream for CommandChannel {
type Item = EventPacket;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
if self.is_terminated {
return Poll::Ready(None);
}
loop {
let result = ready!(self.event_stream.poll_next_unpin(cx));
let Some(Ok(packet)) = result else {
self.is_terminated = true;
return Poll::Ready(None);
};
match packet {
HciTransportEvent::OnReceive { payload } => {
if self.proxy.ack_receive().is_err() {
self.is_terminated = true;
return Poll::Ready(None);
}
let ReceivedPacket::Event(event) = payload else {
continue;
};
return Poll::Ready(Some(EventPacket::new(event)));
}
_ => continue,
}
}
}
}
/// Event packets from the HCI device.
#[derive(Debug)]
pub enum EventPacket {
CommandComplete { opcode: u16, payload: Vec<u8> },
CommandStatus { opcode: u16, status_code: StatusCode, payload: Vec<u8> },
InquiryResult { results: Vec<InquiryResult>, payload: Vec<u8> },
InquiryComplete { status_code: StatusCode, payload: Vec<u8> },
Unknown { payload: Vec<u8> },
}
const HCI_COMMAND_COMPLETE_MIN: usize = 5;
const HCI_COMMAND_STATUS_MIN: usize = 6;
const HCI_INQUIRY_RESULT_MIN: usize = 3;
const HCI_INQUIRY_COMPLETE_MIN: usize = 3;
impl EventPacket {
pub fn new(payload: Vec<u8>) -> EventPacket {
assert!(!payload.is_empty(), "HCI Packet is empty");
match EventPacketType::try_from(payload[0]) {
Ok(EventPacketType::CommandComplete) => {
if payload.len() < HCI_COMMAND_COMPLETE_MIN {
return EventPacket::Unknown { payload };
}
let opcode = decode_opcode(&payload[3..=4]);
EventPacket::CommandComplete { opcode, payload }
}
Ok(EventPacketType::CommandStatus) => {
if payload.len() < HCI_COMMAND_STATUS_MIN {
return EventPacket::Unknown { payload };
}
let opcode = decode_opcode(&payload[4..=5]);
let status_code =
StatusCode::try_from(payload[2]).unwrap_or(StatusCode::UnknownCommand);
EventPacket::CommandStatus { opcode, status_code, payload }
}
Ok(EventPacketType::InquiryResult) => {
if payload.len() < HCI_INQUIRY_RESULT_MIN {
return EventPacket::Unknown { payload };
}
let results = parse_inquiry_result(&payload[..]);
EventPacket::InquiryResult { results, payload }
}
Ok(EventPacketType::InquiryComplete) => {
if payload.len() < HCI_INQUIRY_COMPLETE_MIN {
return EventPacket::Unknown { payload };
}
let status_code =
StatusCode::try_from(payload[2]).unwrap_or(StatusCode::UnknownCommand);
EventPacket::InquiryComplete { status_code, payload }
}
Err(_) => EventPacket::Unknown { payload },
}
}
/// Returns a human readable representation of the event.
pub fn decode(&self) -> String {
match self {
EventPacket::CommandComplete { opcode, .. } => {
format!("HCI_Command_Complete Opcode: {} Payload: {}", opcode, self)
}
EventPacket::CommandStatus { opcode, status_code, .. } => format!(
"HCI_Command_Status Opcode: {} Status: {} Payload: {}",
opcode,
status_code.name(),
self
),
EventPacket::InquiryResult { results, .. } => {
format!("HCI_Inquiry_Result results: {:?}", results)
}
EventPacket::InquiryComplete { status_code, .. } => {
format!("HCI_Inquiry_Complete Status: {}", status_code.name())
}
EventPacket::Unknown { payload } => {
format!("Unknown Event: 0x{} Payload: {}", hex::encode(&[payload[0]]), self)
}
}
}
pub fn payload(&self) -> &Vec<u8> {
match self {
EventPacket::CommandComplete { payload, .. } => payload,
EventPacket::CommandStatus { payload, .. } => payload,
EventPacket::InquiryResult { payload, .. } => payload,
EventPacket::InquiryComplete { payload, .. } => payload,
EventPacket::Unknown { payload, .. } => payload,
}
}
pub fn opcode(&self) -> Option<u16> {
match self {
EventPacket::CommandComplete { opcode, .. } => Some(*opcode),
EventPacket::CommandStatus { opcode, .. } => Some(*opcode),
_ => None,
}
}
}
impl fmt::Display for EventPacket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex::encode(self.payload()))
}
}
fn open_hci_transport(device_path: &str) -> Result<HciTransportProxy, Error> {
let interface = fuchsia_component::client::connect_to_protocol_at_path::<
fidl_fuchsia_hardware_bluetooth::VendorMarker,
>(device_path)?;
let proxy = block_on(interface.open_hci_transport())?
.map_err(|_| format_err!("open_hci_transport failed"))?
.into_proxy();
Ok(proxy)
}
pub fn open_default_device() -> Result<CommandChannel, Error> {
CommandChannel::new(DEFAULT_DEVICE)
}
#[cfg(test)]
mod tests {
use super::*;
use bt_fidl_mocks::hci::HciTransportMock;
use bt_fidl_mocks::timeout_duration;
use fidl_fuchsia_hardware_bluetooth::{HciTransportMarker, ReceivedPacket};
use fuchsia_async as fasync;
use futures::future::join;
use futures::StreamExt;
#[test]
fn test_command_channel_stream_lifecycle() {
let mut exec = fasync::TestExecutor::new();
let (proxy, server) = fidl::endpoints::create_proxy::<HciTransportMarker>();
let (stream, control_handle) = server.into_stream_and_control_handle();
let mut command_channel = CommandChannel::from_proxy(proxy).unwrap();
let event = vec![0x0e, 0x04, 0x01, 0x03, 0x0c, 0x00];
control_handle.send_on_receive(&ReceivedPacket::Event(event.clone())).unwrap();
let event = exec.run_until_stalled(&mut command_channel.next());
assert!(event.is_ready());
drop(stream);
drop(control_handle);
let event = exec.run_until_stalled(&mut command_channel.next());
let Poll::Ready(None) = event else {
panic!("Expected None");
};
}
#[test]
fn test_command_channel() {
let mut exec = fasync::TestExecutor::new();
let (proxy, server) = fidl::endpoints::create_proxy::<HciTransportMarker>();
let (stream, control_handle) = server.into_stream_and_control_handle();
let mut command_channel = CommandChannel::from_proxy(proxy).unwrap();
let mut mock = HciTransportMock::from_stream(stream, timeout_duration());
let command = vec![0x03, 0x0c, 0x00];
let send_fut = command_channel.send_command_packet(command.clone());
let expect_fut = mock.expect_send(SentPacket::Command(command.clone()));
let send_poll = exec.run_until_stalled(&mut Box::pin(join(send_fut, expect_fut)));
let Poll::Ready((send_result, expect_result)) = send_poll else {
panic!("send future stuck pending");
};
let _ = send_result.expect("send failed");
let _ = expect_result.expect("expect failed");
let event = vec![0x0e, 0x04, 0x01, 0x03, 0x0c, 0x00];
control_handle.send_on_receive(&ReceivedPacket::Event(event.clone())).unwrap();
let event = exec.run_until_stalled(&mut command_channel.next());
assert!(event.is_ready());
match event {
Poll::Ready(packet) => {
let pkt = packet.expect("no packet");
assert_eq!(
pkt.payload(),
&vec![
0x0e, // HCI_COMMAND_COMPLETE
0x04, 0x01, //
0x03, 0x0c, // opcode (little endian)
0x00 // payload len
]
);
let opcode = pkt.opcode();
assert_eq!(opcode, Some(0x0c03));
}
_ => panic!("failed to decode packets from stream"),
}
let pending_item = exec.run_until_stalled(&mut command_channel.next());
assert!(pending_item.is_pending());
}
#[test]
fn test_event_packet_decode_cmd_complete() {
let pkt = EventPacket::new(vec![
0x0e, 0x04, // HCI_Command_Complete
0x01, //
0x03, 0x0c, // opcode (little endian)
0x00, // payload len
]);
let opcode = pkt.opcode();
assert_eq!(opcode, Some(0x0c03));
let pretty_string = pkt.decode();
assert_eq!(
pretty_string,
"HCI_Command_Complete Opcode: 3075 Payload: 0e0401030c00".to_string()
);
}
#[test]
fn test_event_packet_decode_inquiry() {
let pkt = EventPacket::new(vec![
0x02, 0x0f, // HCI_Inquiry_Result
0x01, // results count
0x7c, 0x48, 0xc6, 0x8b, 0x42, 0x74, // br_addr
0x01, // page_scan_repetition_mode:
0x00, 0x00, // reserved
0x0c, 0x02, 0x7a, // class of device
0x45, 0x25, // clock offset
]);
match pkt {
EventPacket::InquiryResult { results, .. } => {
assert!(results.len() == 1);
let result = &results[0];
assert_eq!(&result.br_addr, &[0x7c, 0x48, 0xc6, 0x8b, 0x42, 0x74]);
assert_eq!(result.page_scan_repetition_mode, 1);
assert_eq!(&result.class_of_device, &[0x0c, 0x02, 0x7a]);
assert_eq!(result.clockoffset, 9541);
}
_ => assert!(false, "packet not an inquiry result"),
}
}
#[test]
fn test_event_packet_decode_inquiry_complete() {
let pkt = EventPacket::new(vec![
0x01, 0x0f, // HCI_Inquiry_Complete
0x00,
]);
match pkt {
EventPacket::InquiryComplete { status_code, .. } => {
assert_eq!(status_code, StatusCode::Success);
}
_ => assert!(false, "packet not an inquiry complete"),
}
}
#[test]
fn test_event_packet_decode_unhandled() {
let pkt = EventPacket::new(vec![0x0e]);
let e = pkt.decode();
assert_eq!(format!("{}", e), "Unknown Event: 0x0e Payload: 0e".to_string());
}
}