diff --git a/sw/host/opentitanlib/src/transport/qemu/mod.rs b/sw/host/opentitanlib/src/transport/qemu/mod.rs index 439e1b4e4ed2f..e6435fc66b0f6 100644 --- a/sw/host/opentitanlib/src/transport/qemu/mod.rs +++ b/sw/host/opentitanlib/src/transport/qemu/mod.rs @@ -34,7 +34,7 @@ use crate::transport::qemu::monitor::{Chardev, ChardevKind, Monitor}; use crate::transport::qemu::reset::QemuReset; use crate::transport::qemu::spi::QemuSpi; use crate::transport::qemu::uart::QemuUart; -use crate::transport::qemu::usbdev::QemuVbusSense; +use crate::transport::qemu::usbdev::{QemuUsbHost, QemuVbusSense}; use crate::transport::{ Capabilities, Capability, Transport, TransportError, TransportInterfaceType, }; @@ -73,6 +73,8 @@ pub struct Qemu { /// VBUS sense pin (actually goes via the `usbdev-cmd` chardev). vbus_sense: Option>, + _usb_host: Option>, + /// QEMU log modelled as a UART. log: Option>, @@ -152,6 +154,25 @@ impl Qemu { } }; + // USBDEV host: + let usb_host = match find_chardev(&chardevs, "usbdev-host") { + Some(ChardevKind::Pty { path }) => { + let tty = serialport::new( + path.to_str().context("TTY path not UTF8")?, + CONSOLE_BAUDRATE, + ) + .open_native() + .context("failed to open QEMU usbdev-host PTY")?; + + let usb_host = Rc::new(QemuUsbHost::new(tty)); + Some(usb_host) + } + _ => { + log::info!("could not find pty chardev with id=usbdev-host, skipping USBDEV"); + None + } + }; + // QEMU log, not really a UART but modelled as one: let log = match find_chardev(&chardevs, "log") { Some(ChardevKind::Pty { path }) => { @@ -237,6 +258,7 @@ impl Qemu { reset, uarts, vbus_sense, + _usb_host: usb_host, log, spi, i2cs, diff --git a/sw/host/opentitanlib/src/transport/qemu/usbdev.rs b/sw/host/opentitanlib/src/transport/qemu/usbdev.rs index 1271f59498fca..44dc911fea512 100644 --- a/sw/host/opentitanlib/src/transport/qemu/usbdev.rs +++ b/sw/host/opentitanlib/src/transport/qemu/usbdev.rs @@ -2,9 +2,14 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 +use anyhow::{Context, anyhow, bail, ensure}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; use serialport::TTYPort; use std::cell::{Cell, RefCell}; -use std::io::Write; +use std::io::{Read, Write}; +use std::sync::mpsc; +use std::thread; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned, byteorder::little_endian}; use crate::io::gpio::{GpioPin, PinMode, PullMode}; @@ -63,3 +68,728 @@ impl GpioPin for QemuVbusSense { Ok(()) } } + +/// Events sent by otlib to the virtual host thread. +enum HostChannelEvent { + /// Gracefully shutdown. + Shutdown, + /// A message was sent by QEMU to the host. + QemuMessage { cmd: u32, id: u32, data: Vec }, +} + +/// Minimal virtual USB host to drive the QEMU usbdev. +pub struct QemuUsbHost { + /// Sender to notify the virtual host thread of various events + /// from the otlib side. + host_channel: mpsc::Sender, +} + +impl Drop for QemuUsbHost { + fn drop(&mut self) { + // Nicely ask the host thread to stop. + let _ = self.host_channel.send(HostChannelEvent::Shutdown); + } +} + +impl QemuUsbHost { + pub fn new(usbdev: TTYPort) -> QemuUsbHost { + let (send, recv) = mpsc::channel(); + let send_thread = send.clone(); + thread::spawn(move || { + let mut host_thread = QemuHostThread::new(usbdev, send_thread, recv); + if let Err(res) = host_thread.run() { + log::error!("QEMU USB host failed with error: {res:?}"); + } + }); + + QemuUsbHost { host_channel: send } + } +} + +/// USBDEV Command, see QEMU documentation. +#[repr(u32)] +#[allow(dead_code)] +#[derive(Debug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive, Clone, Copy)] +enum QemuUsbdevCmd { + Invalid, + Hello, + VbusOn, + VbusOff, + Connect, + Disconnect, + Reset, + Resume, + Suspend, + Setup, + Transfer, + Complete, +} + +/// USBDEV packet header, see QEMU documentation. +#[derive(Clone, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)] +#[repr(C)] +pub struct QemuUsbdevPacketHeader { + command: little_endian::U32, + size: little_endian::U32, + id: little_endian::U32, +} + +/// USBDEV hello payload, see QEMU documentation. +#[derive(Clone, IntoBytes, FromBytes, KnownLayout, Immutable, Unaligned)] +#[repr(C)] +pub struct QemuUsbdevHelloPayload { + magic: [u8; 4], + major_version: little_endian::U16, + minor_version: little_endian::U16, +} + +/// USBDEV setup payload, see QEMU documentation. +#[derive(Clone, IntoBytes, KnownLayout, Immutable, Unaligned)] +#[repr(C)] +pub struct QemuUsbdevSetupPayload { + address: u8, + endpoint: u8, + reserved: [u8; 2], + setup: [u8; 8], +} + +/// USBDEV transfer flags, see QEMU documentation. +#[repr(u8)] +#[allow(dead_code)] +#[derive(Debug, Eq, PartialEq, IntoPrimitive, Clone, Copy)] +enum QemuUsbdevTransferFlags { + Zlp = 1 << 0, +} + +/// USBDEV transfer payload, see QEMU documentation. +#[derive(Clone, IntoBytes, KnownLayout, Immutable, Unaligned)] +#[repr(C)] +pub struct QemuUsbdevTransferPayload { + address: u8, + endpoint: u8, + max_packet_size: little_endian::U16, + flags: u8, + reserved: [u8; 3], + transfer_size: little_endian::U32, +} + +/// USBDEV transfer status, see QEMU documentation. +#[repr(u8)] +#[allow(dead_code)] +#[derive(Debug, Eq, PartialEq, TryFromPrimitive, Clone, Copy)] +enum QemuUsbdevTransferStatus { + Success, + Stalled, + Cancelled, + Error, +} + +/// USBDEV complete payload, see QEMU documentation. +#[derive(Clone, FromBytes, Immutable, Unaligned)] +#[repr(C)] +pub struct QemuUsbdevCompletePayload { + status: u8, + reserved: [u8; 3], + xfer_size: little_endian::U32, +} + +/// This structure records an event received from QEMU on the chardev. +/// It only parses the packet header and passes the rest of the payload +/// as-is. +#[derive(Debug)] +struct QemuUsbdevEvent { + cmd: QemuUsbdevCmd, + id: u32, + data: Vec, +} + +/// This structure records a complete event received from QEMU on the chardev. +/// The complete payload is parsed and the received data in stored as-is. +#[derive(Debug)] +struct QemuUsbdevCompleteEvent { + id: u32, + status: QemuUsbdevTransferStatus, + // Temporary until we have a more complete stack that uses this field. + #[allow(dead_code)] + xfer_size: u32, + data: Vec, +} + +/// Standard USB Setup packet. +#[derive(Clone, IntoBytes, KnownLayout, Immutable, Unaligned)] +#[repr(C)] +pub struct UsbSetupPacket { + req_type: u8, + request: u8, + value: little_endian::U16, + index: little_endian::U16, + length: little_endian::U16, +} + +/// Errors that can occur when waiting for a QEMU event. +enum WaitError { + /// Shutdown was requested. + Shutdown, + /// Device disconnected before end of sequence. + Disconnected, +} + +/// Error type used when waiting for a specific QEMU event +/// that could get interrupted in some way. +type WaitResult = Result; + +/// This structure represents an enumerated and configured device. +#[derive(Clone, Debug)] +struct DeviceInfo { + // Temporary until we have a more complete stack that uses this field. + #[allow(dead_code)] + dev_desc: Vec, +} + +/// This structure holds the state of the USB host thread. +struct QemuHostThread { + /// Communication channel to QEMU USBDEV. + usbdev: TTYPort, + /// Channel to receive/send events from otlib and reader. + channel_recv: mpsc::Receiver, + channel_send: mpsc::Sender, + /// Next ID. + next_id: u32, +} + +impl QemuHostThread { + const USBDEV_HELLO_MAGIC: [u8; 4] = *b"UDCX"; + const USBDEV_HELLO_MAJOR: u16 = 1; + const USBDEV_HELLO_MINOR: u16 = 0; + + /// Standard USB mask for EP IN in Setup packets. + const USBDEV_TRANSFER_EP_IN: u8 = 1 << 7; + + // Every device must be able to handle 8 bytes packets. + const USB_FS_SAFE_PACKET_SIZE: u16 = 8; + + // Some standard request types. + const USB_REQ_TYPE_IN: u8 = 0x80; + const USB_REQ_TYPE_STANDARD: u8 = 0; + const USB_REQ_TYPE_DEVICE: u8 = 0; + + // Some standard USB requests. + const USB_GET_DESCRIPTOR: u8 = 6; + + // Some standard descriptor types. + const USB_DEVICE_DESCRIPTOR: u8 = 1; + const USB_DEVICE_DESCRIPTOR_LENGTH: usize = 18; + + // Location of the bMaxPacketSize field in the device descriptor. + const USV_DEV_DESC_MAX_PACKET_SIZE_OFFSET: usize = 7; + + fn new( + usbdev: TTYPort, + channel_send: mpsc::Sender, + channel_recv: mpsc::Receiver, + ) -> Self { + QemuHostThread { + usbdev, + channel_recv, + channel_send, + next_id: 0, + } + } + + /// Send a message to the QEMU USBDEV driver. On success, return the ID + /// of the packet. + fn send_packet(&mut self, cmd: QemuUsbdevCmd, data: &[&[u8]]) -> anyhow::Result { + let tot_len: usize = data.as_ref().iter().map(|data| data.len()).sum(); + let hdr = QemuUsbdevPacketHeader { + command: Into::::into(cmd).into(), + size: (tot_len as u32).into(), + id: self.next_id.into(), + }; + + let pkt_id = self.next_id; + self.next_id += 1; + + self.usbdev + .write_all(hdr.as_bytes()) + .context("Failed to write packet header to usbdev tty")?; + for data in data { + self.usbdev + .write_all(data) + .context("Failed to write packet data to usbdev tty")?; + } + self.usbdev.flush().context("Failed to flush usbdev tty")?; + Ok(pkt_id) + } + + /// Send a Hello command and return the ID. + fn send_hello(&mut self) -> anyhow::Result { + let hello = QemuUsbdevHelloPayload { + magic: Self::USBDEV_HELLO_MAGIC, + major_version: Self::USBDEV_HELLO_MAJOR.into(), + minor_version: Self::USBDEV_HELLO_MINOR.into(), + }; + + self.send_packet(QemuUsbdevCmd::Hello, &[hello.as_bytes()]) + } + + /// Send a Setup command and return the ID. + fn send_setup(&mut self, addr: u8, ep: u8, setup: &[u8; 8]) -> anyhow::Result { + let setup_cmd = QemuUsbdevSetupPayload { + address: addr, + endpoint: ep, + reserved: [0u8; 2], + setup: *setup, + }; + self.send_packet(QemuUsbdevCmd::Setup, &[setup_cmd.as_bytes()]) + } + + /// Send a Transfer command and return the ID. + fn send_transfer_internal( + &mut self, + addr: u8, + ep: u8, + flags: u8, + max_pkt_size: u16, + transfer_size: u32, + data: &[u8], + ) -> anyhow::Result { + let xfer_cmd = QemuUsbdevTransferPayload { + address: addr, + endpoint: ep, + max_packet_size: max_pkt_size.into(), + flags, + reserved: [0u8; 3], + transfer_size: transfer_size.into(), + }; + + self.send_packet(QemuUsbdevCmd::Transfer, &[xfer_cmd.as_bytes(), data]) + } + + /// Helper function to send a Transfer IN command and return the ID. + fn send_transfer_in( + &mut self, + addr: u8, + ep: u8, + max_pkt_size: u16, + transfer_size: usize, + ) -> anyhow::Result { + self.send_transfer_internal( + addr, + ep | Self::USBDEV_TRANSFER_EP_IN, + /* flags */ 0, + max_pkt_size, + transfer_size as u32, + &[], + ) + } + + /// Helper function to send a Transfer OUT command and return the ID. + fn send_transfer_out( + &mut self, + addr: u8, + ep: u8, + zlp: bool, + max_pkt_size: u16, + data: &[u8], + ) -> anyhow::Result { + let flags: u8 = if zlp { + QemuUsbdevTransferFlags::Zlp.into() + } else { + 0 + }; + self.send_transfer_internal(addr, ep, flags, max_pkt_size, data.len() as u32, data) + } + + /// Send a Setup command followed by a Transfer IN command of the requested length. + /// Return the ID of the expected Complete event corresponding to the data IN. + #[allow(clippy::too_many_arguments)] + fn send_control_in( + &mut self, + addr: u8, + ep: u8, + req_type: u8, + req: u8, + value: u16, + index: u16, + length: usize, + max_pkt_size: u16, + ) -> anyhow::Result { + let setup = UsbSetupPacket { + req_type, + request: req, + value: value.into(), + index: index.into(), + length: TryInto::::try_into(length) + .context("control IN length does not fit in 16 bits")? + .into(), + }; + let _ = self + .send_setup(addr, ep, setup.as_bytes().try_into().unwrap()) + .context("Failed to send SETUP for control in")?; + self.send_transfer_in(addr, ep, max_pkt_size, length) + } + + /// Wait for a QEMU command and return its payload. Return an error if something goes + /// wrong. + fn wait_qemu_event(&mut self) -> anyhow::Result> { + let Ok(event) = self.channel_recv.recv() else { + bail!("USB host: lost communication with otlib!"); + }; + Ok(match event { + HostChannelEvent::Shutdown => Err(WaitError::Shutdown), + HostChannelEvent::QemuMessage { cmd, id, data } => { + let cmd = QemuUsbdevCmd::try_from(cmd) + .with_context(|| format!("Unknown command {cmd} from USBDEV"))?; + Ok(QemuUsbdevEvent { cmd, id, data }) + } + }) + } + + /// Wait for a HELLO packet and return the payload. Return an error if something goes + /// wrong or an unexpected packet is received. + fn wait_hello( + &mut self, + expected_id: u32, + ) -> anyhow::Result> { + Ok(match self.wait_qemu_event()? { + Ok(QemuUsbdevEvent { cmd, id, data }) => { + ensure!( + cmd == QemuUsbdevCmd::Hello, + "Expected an HELLO event, got {cmd:?}" + ); + ensure!( + id == expected_id, + "Expected HELLO with ID={expected_id}, got {id}" + ); + let (payload, data) = QemuUsbdevHelloPayload::read_from_prefix(&data) + .map_err(|err| anyhow!("{err:?}")) + .context("Could not parse HELLO payload")?; + ensure!( + data.is_empty(), + "HELLO payload is larger than expected by {} bytes", + data.len() + ); + ensure!( + payload.magic == Self::USBDEV_HELLO_MAGIC, + "HELLO payload has the wrong magic bytes" + ); + Ok(payload) + } + Err(err) => Err(err), + }) + } + + /// Wait for a CONNECT packet and return the ID. Return an error if something goes + /// wrong or an unexpected packet is received. + fn wait_connect(&mut self) -> anyhow::Result> { + Ok(match self.wait_qemu_event()? { + Ok(QemuUsbdevEvent { cmd, id, data }) => { + // If device was previously disconnect, the only valid event that the device + // can send is a CONNECT event. + ensure!( + cmd == QemuUsbdevCmd::Connect, + "Expected a CONNECT event, got {cmd:?}" + ); + ensure!(data.is_empty(), "CONNECT payload is non-empty"); + Ok(id) + } + Err(err) => Err(err), + }) + } + + /// Wait for a COMPLETE packet and return the data. Return an error if something goes + /// wrong. + fn wait_complete(&mut self) -> anyhow::Result> { + Ok(match self.wait_qemu_event()? { + Ok(QemuUsbdevEvent { cmd, id, data }) => { + match cmd { + QemuUsbdevCmd::Disconnect => Err(WaitError::Disconnected), + QemuUsbdevCmd::Complete => { + let (payload, data) = QemuUsbdevCompletePayload::read_from_prefix(&data) + .map_err(|err| anyhow!("{err:?}")) + .context("Could not parse HELLO payload")?; + // FIXME here it is somewhat inefficient because we are taking a slice of the + // data and turning into back into vector... + Ok(QemuUsbdevCompleteEvent { + id, + status: QemuUsbdevTransferStatus::try_from(payload.status) + .with_context(|| { + format!( + "Unknown transfer status {} from USBDEV", + payload.status + ) + })?, + xfer_size: payload.xfer_size.into(), + data: data.to_vec(), + }) + } + _ => bail!("unexpected event {cmd:?}"), + } + } + Err(err) => Err(err), + }) + } + + /// Perform a full control IN transaction: + /// - send a Setup + /// - send a Transfer IN + /// - wait for data IN + /// - send a Transfer OUT (status stage) + /// - wait for status + /// + /// If at any point an error occurs, the error will be returned. + /// If any wait is interrupt, an Ok(Err(wait_error)) will be returned. + /// If any transfer fails (e.g. stalled), an Ok(Ok(Err(status))) will be returned. + /// If the transfer is successful, an Ok(Ok(Ok(data))) will be returned. + #[allow(clippy::too_many_arguments)] + fn send_and_wait_control_in( + &mut self, + addr: u8, + ep: u8, + req_type: u8, + req: u8, + value: u16, + index: u16, + length: usize, + max_pkt_size: u16, + ) -> anyhow::Result, QemuUsbdevTransferStatus>>> { + let expected_id = self + .send_control_in(addr, ep, req_type, req, value, index, length, max_pkt_size) + .context("Failed to send control IN transfer")?; + let QemuUsbdevCompleteEvent { + id, status, data, .. + } = match self + .wait_complete() + .context("Failed to receive control IN data")? + { + Ok(res) => res, + Err(res) => return Ok(Err(res)), + }; + ensure!( + id == expected_id, + "Unexpected command ID when waiting for control IN data" + ); + ensure!( + data.len() <= length, + "Device returned too much data for control IN transfer" + ); + /* Return early if the transfer failed, no need for a status stage */ + if status != QemuUsbdevTransferStatus::Success { + return Ok(Ok(Err(status))); + } + /* Status stage */ + let expected_id = self + .send_transfer_out(addr, ep, /* zlp */ true, max_pkt_size, &[]) + .context("Failed to send control IN status packet")?; + let QemuUsbdevCompleteEvent { id, status, .. } = match self + .wait_complete() + .context("Failed to receive control IN status")? + { + Ok(res) => res, + Err(res) => return Ok(Err(res)), + }; + if status != QemuUsbdevTransferStatus::Success { + return Ok(Ok(Err(status))); + } + ensure!( + id == expected_id, + "Unexpected command ID when waiting for control IN status" + ); + Ok(Ok(Ok(data))) + } + + /// Perform a full enumeration sequence, retrieving the + /// device and config descriptors, as well as assigning + /// an address to the device. If the device fails to enumetate + /// with an otherwise non-fatal error, this function returns Ok(Ok(None)) + fn enumerate_device(&mut self) -> anyhow::Result>> { + // Send a reset to the device. + let _ = self + .send_packet(QemuUsbdevCmd::Reset, &[]) + .context("Failed to send RESET command")?; + + // HACK Wait for a while. This should really be done in QEMU to emulate the reset. + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Start a typical enumeration sequence by asking for the first 8 bytes + // of the device descriptor. + let trunc_dev_desc = match self + .send_and_wait_control_in( + 0, // Address 0 + 0, // Endpoint 0 + Self::USB_REQ_TYPE_IN | Self::USB_REQ_TYPE_STANDARD | Self::USB_REQ_TYPE_DEVICE, + Self::USB_GET_DESCRIPTOR, + (Self::USB_DEVICE_DESCRIPTOR as u16) << 8, + 0, // value + Self::USB_FS_SAFE_PACKET_SIZE.into(), // length + Self::USB_FS_SAFE_PACKET_SIZE, + ) + .context("Failed to send GET_DESC")? + { + Ok(res) => res, + Err(res) => return Ok(Err(res)), + }; + let trunc_dev_desc = match trunc_dev_desc { + Ok(desc) => desc, + Err(xfer_err) => { + log::info!( + "USB Host: enumeration failed (could not retrieve max packet size): {xfer_err:?}" + ); + return Ok(Ok(None)); + } + }; + let max_packet_size = trunc_dev_desc[Self::USV_DEV_DESC_MAX_PACKET_SIZE_OFFSET] as u16; + log::info!("USB Host: device report a maximum packet size of {max_packet_size} on EP0"); + // TODO sanity check max packet size + + // Retrieve the full device descriptor. + let dev_desc = match self + .send_and_wait_control_in( + 0, // Address 0 + 0, // Endpoint 0 + Self::USB_REQ_TYPE_IN | Self::USB_REQ_TYPE_STANDARD | Self::USB_REQ_TYPE_DEVICE, + Self::USB_GET_DESCRIPTOR, + (Self::USB_DEVICE_DESCRIPTOR as u16) << 8, + 0, // value + Self::USB_DEVICE_DESCRIPTOR_LENGTH, + max_packet_size, + ) + .context("Failed to send GET_DESC")? + { + Ok(res) => res, + Err(res) => return Ok(Err(res)), + }; + let dev_desc = match dev_desc { + Ok(desc) => desc, + Err(xfer_err) => { + log::info!( + "USB Host: enumeration failed (could not retrieve device descriptor): {xfer_err:?}" + ); + return Ok(Ok(None)); + } + }; + + Ok(Ok(Some(DeviceInfo { dev_desc }))) + } + + /// This thread simulates a USB host. It can react to events sent by otlib + /// other the channel. + fn run(&mut self) -> anyhow::Result<()> { + log::debug!("USB host: thread started"); + // We spawn a thread just to listen to events send by QEMU USBDEV. + // Those will be sent to the same channel used otlib to gives us + // requests. + + let usbdev_for_reader = self + .usbdev + .try_clone_native() + .expect("Could not clone TTYPort for reading"); + let channel_send_for_reader = self.channel_send.clone(); + thread::spawn(move || { + if let Err(res) = + Self::host_qemu_reader_thread(usbdev_for_reader, channel_send_for_reader) + { + log::error!("QEMU reader failed with error: {res:?}"); + } + // TODO: may notify host in case of reader fatal error to shutdown? + }); + + // Send HELLO. + let id = self.send_hello().context("Failed to send HELLO packet")?; + // Expect HELLO back. + let Ok(hello) = self + .wait_hello(id) + .context("Failed waiting for an HELLO packet")? + else { + // Stop on shutdown. + return Ok(()); + }; + log::info!( + "USB host: device is using protocol version {}.{}", + hello.major_version, + hello.minor_version + ); + + // Power on VBUS. + let _ = self + .send_packet(QemuUsbdevCmd::VbusOn, &[]) + .context("Failed to send VBUS_ON command")?; + + loop { + // Wait for device to connect. + let _id = self + .wait_connect() + .context("Failed while waiting for a device to connect")?; + + log::info!("USB Host: device connected"); + + // Perform enumeration and get back the descriptors. + let _enum_res = self + .enumerate_device() + .context("Failed while enumerating the device")?; + let dev_info = match _enum_res { + Err(WaitError::Shutdown) => return Ok(()), + Err(WaitError::Disconnected) => { + log::info!("USB Host: device disconnected"); + continue; + } + Ok(dev_info) => dev_info, + }; + log::info!("USB Host: device configured: {dev_info:?}"); + + break; + } + + Ok(()) + } + + fn blocking_read_exact(read: &mut T, buf: &mut [u8]) -> anyhow::Result<()> { + let mut pos = 0usize; + while pos < buf.len() { + match read.read(&mut buf[pos..]) { + Ok(amount) => pos += amount, + Err(err) + if err.kind() == std::io::ErrorKind::TimedOut + || err.kind() == std::io::ErrorKind::Interrupted => + { + continue; + } + err => { + // FIXME: surely there is a better way? + let _ = err?; + } + } + } + Ok(()) + } + + /// This thread is responsible for receiving messages from QEMU (following the usbdev + /// protocol). When done, it sends a message to the virtual host channel with the content. + fn host_qemu_reader_thread( + mut usbdev: TTYPort, + channel_send: mpsc::Sender, + ) -> anyhow::Result<()> { + log::debug!("USB host: QEMU reader thread started"); + + loop { + // Wait for a header. + let mut hdr = [0u8; size_of::()]; + Self::blocking_read_exact(&mut usbdev, &mut hdr) + .context("Failed to read a packet header from QEMU")?; + let hdr = QemuUsbdevPacketHeader::ref_from_bytes(&hdr).unwrap(); + // TODO sanity check the size + + let mut data = vec![0; Into::::into(hdr.size) as usize]; + Self::blocking_read_exact(&mut usbdev, &mut data) + .context("Failed to read a packet data from QEMU")?; + + channel_send + .send(HostChannelEvent::QemuMessage { + cmd: hdr.command.into(), + id: hdr.id.into(), + data, + }) + .context("Failed to send a message to the host thread")?; + } + } +}