diff --git a/hil/src/boot.rs b/hil/src/boot.rs index 80bb22dc..ec56571c 100644 --- a/hil/src/boot.rs +++ b/hil/src/boot.rs @@ -1,8 +1,8 @@ use std::time::Duration; -use crate::ftdi::{FtdiGpio, OutputState}; +use crate::ftdi::{FtdiGpio, FtdiId, OutputState}; use color_eyre::{eyre::WrapErr as _, Result}; -use tracing::info; +use tracing::{debug, info}; pub const BUTTON_PIN: crate::ftdi::Pin = FtdiGpio::CTS_PIN; pub const RECOVERY_PIN: crate::ftdi::Pin = FtdiGpio::RTS_PIN; @@ -17,17 +17,34 @@ pub fn is_recovery_mode_detected() -> Result { } // Note: we are calling some blocking code from async here, but its probably fine. +/// If `device` is `None`, will get the first available device. #[tracing::instrument] -pub async fn reboot(recovery: bool) -> Result<()> { - fn make_ftdi() -> Result { - FtdiGpio::builder() - .with_default_device() +pub async fn reboot(recovery: bool, device: Option<&FtdiId>) -> Result<()> { + fn make_ftdi(device: Option) -> Result { + let builder = FtdiGpio::builder(); + let builder = match &device { + Some(FtdiId::Description(desc)) => builder.with_description(desc), + Some(FtdiId::SerialNumber(serial)) => builder.with_serial_number(serial), + Some(FtdiId::Index(idx)) => builder.with_index(*idx), + None => builder.with_default_device(), + }; + builder .and_then(|b| b.configure()) .wrap_err("failed to create ftdi device") } + info!("Turning off"); + let device_clone = device.cloned(); let ftdi = tokio::task::spawn_blocking(|| -> Result<_, color_eyre::Report> { - let mut ftdi = make_ftdi()?; + FtdiGpio::detach_all(None, None) + .wrap_err("failed to detach all ftdi devices")?; + for d in FtdiGpio::list_devices().wrap_err("failed to list ftdi devices")? { + debug!( + "ftdi device - desc:{}, serial:{}, vid:{}, pid:{}", + d.description, d.serial_number, d.vendor_id, d.product_id, + ); + } + let mut ftdi = make_ftdi(device_clone)?; ftdi.set_pin(BUTTON_PIN, OutputState::Low)?; ftdi.set_pin(RECOVERY_PIN, OutputState::High)?; Ok(ftdi) @@ -41,8 +58,9 @@ pub async fn reboot(recovery: bool) -> Result<()> { tokio::time::sleep(Duration::from_secs(4)).await; info!("Turning on"); + let device_clone = device.cloned(); let ftdi = tokio::task::spawn_blocking(move || -> Result<_, color_eyre::Report> { - let mut ftdi = make_ftdi()?; + let mut ftdi = make_ftdi(device_clone)?; let recovery_state = if recovery { OutputState::Low } else { diff --git a/hil/src/commands/reboot.rs b/hil/src/commands/reboot.rs index 7c2d9cb7..96be2efb 100644 --- a/hil/src/commands/reboot.rs +++ b/hil/src/commands/reboot.rs @@ -1,19 +1,40 @@ use clap::Parser; use color_eyre::{eyre::WrapErr as _, Result}; +use crate::ftdi::FtdiId; + #[derive(Debug, Parser)] pub struct Reboot { #[arg(short)] recovery: bool, + /// The serial number of the FTDI device to use + #[arg(long, group = "id")] + serial_num: Option, + /// The description of the FTDI device to use + #[arg(long, group = "id")] + desc: Option, + /// The index of the FTDI device to use + #[arg(long, group = "id")] + index: Option, } impl Reboot { pub async fn run(self) -> Result<()> { - crate::boot::reboot(self.recovery).await.wrap_err_with(|| { - format!( - "failed to reboot into {} mode", - if self.recovery { "recovery" } else { "normal" } - ) - }) + let device = match (self.serial_num, self.desc, self.index) { + (Some(serial), None, None) => Some(FtdiId::SerialNumber(serial)), + (None, Some(desc), None) => Some(FtdiId::Description(desc)), + (None, None, Some(index)) => Some(FtdiId::Index(index)), + (None, None, None) => None, + _ => unreachable!(), + }; + + crate::boot::reboot(self.recovery, device.as_ref()) + .await + .wrap_err_with(|| { + format!( + "failed to reboot into {} mode", + if self.recovery { "recovery" } else { "normal" } + ) + }) } } diff --git a/hil/src/ftdi.rs b/hil/src/ftdi.rs index dbe0ba67..a72de162 100644 --- a/hil/src/ftdi.rs +++ b/hil/src/ftdi.rs @@ -1,8 +1,22 @@ //! Code to control GPIO of FTDI serial adapter +//! +//! # Notes +//! +//! ## Missing EEPROM serial numbers +//! Sometimes, a FTDI device has no serial number. It may show up as an empty +//! string when using libftd2xx, or as "000000000" in lsusb and nusb. +//! +//! When this happens, its typically because the chip has no EEPROM attached, +//! or the EEPROM is blank/unprogrammed. While it is still desirable to set the +//! EEPROM to be able to unambiguously identity a particular chip, this code makes +//! affordances for situations where this is not the case, however only at most +//! one such blank FTDI device can be attached at any given time. +//! +//! Read more in section 4.2 of +//! use color_eyre::{ - eyre::{ensure, eyre}, - eyre::{OptionExt, WrapErr as _}, + eyre::{bail, ensure, eyre, OptionExt, WrapErr as _}, Result, }; use libftd2xx::FtdiCommon; @@ -31,7 +45,15 @@ mod builder_states { } } use builder_states::*; -use tracing::error; +use tracing::{debug, error, warn}; + +/// The different supported ways to address a *specific* FTDI device. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum FtdiId { + SerialNumber(String), + Description(String), + Index(u8), +} /// Type-state builder pattern for creating a [`FtdiGpio`]. #[derive(Clone, Debug)] @@ -40,39 +62,53 @@ pub struct Builder(S); impl Builder { /// Opens the first ftdi device identified. This can change across subsequent calls, /// if you need a specific device use [`Self::with_serial_number`] instead. + /// + /// Returns an error if there is more than 1 FTDI device connected. pub fn with_default_device(self) -> Result> { - let mut last_err = None; - let dinfo_vec: Vec<_> = nusb::list_devices() - .wrap_err("failed to list usb devices")? - .filter(|dinfo| dinfo.vendor_id() == libftd2xx::FTDI_VID) + let usb_device_infos: Vec<_> = nusb::list_devices() + .wrap_err("failed to enumerate devices")? + .filter(|d| d.vendor_id() == libftd2xx::FTDI_VID) .collect(); - for dinfo in dinfo_vec { - let Some(serial) = dinfo.serial_number() else { - continue; - }; - let cloned = self.clone(); - match Self::with_serial_number(cloned, serial) { - Ok(ftdi) => return Ok(ftdi), - Err(err) => last_err = Some(err), - } + let ftdi_device_count = FtdiGpio::list_devices() + .wrap_err("failed to enumerate ftdi devices")? + .count(); + if usb_device_infos.is_empty() || ftdi_device_count == 0 { + bail!("no FTDI devices found"); } - if let Some(last_err) = last_err { - Err(last_err).wrap_err( - "failed to successfully open any ftdi devices. Wrapping last error", - ) - } else { - Err(eyre!("faild to find any ftdi devices")) + if usb_device_infos.len() != 1 || ftdi_device_count != 1 { + bail!("more than one FTDI device found"); } + let usb_device_info = usb_device_infos.into_iter().last().unwrap(); + + // See module-level docs for more info about missing serial numbers. + let serial_num = usb_device_info.serial_number().unwrap_or(""); + if !serial_num.is_empty() && serial_num != "000000000" { + return self.with_serial_number(serial_num); + } + + warn!("EEPROM is either blank or missing and there is no serial number"); + let mut device = + libftd2xx::Ftdi::new().wrap_err("failed to open default ftdi device")?; + let device_info = device.device_info().wrap_err("failed to get device info")?; + debug!("using device: {device_info:?}"); + + Ok(Builder(NeedsConfiguring { device })) } /// Opens a device with the given serial number. pub fn with_serial_number(self, serial: &str) -> Result> { + ensure!(!serial.is_empty(), "serial numbers cannot be empty"); + ensure!( + serial != "000000000", + "serial numbers cannot be the special zero serial" + ); + let mut last_err = None; let usb_device_info = nusb::list_devices() .wrap_err("failed to enumerate devices")? .find(|d| d.serial_number() == Some(serial)) .ok_or_else(|| { - eyre!("usb device with matching serial {serial} not found") + eyre!("usb device with matching serial \"{serial}\" not found") })?; let usb_device = usb_device_info .open() @@ -83,7 +119,7 @@ impl Builder { // See also https://stackoverflow.com/a/34021765 let _ = usb_device.detach_kernel_driver(iinfo.interface_number()); match libftd2xx::Ftdi::with_serial_number(serial).wrap_err_with(|| { - format!("failed to open FTDI device with serial number {serial}") + format!("failed to open FTDI device with serial number \"{serial}\"") }) { Ok(ftdi) => { return Ok(Builder(NeedsConfiguring { device: ftdi })); @@ -99,6 +135,76 @@ impl Builder { Err(eyre!("faild to find any ftdi devices")) } } + + /// Opens a device with the given description. + pub fn with_description(self, desc: &str) -> Result> { + let ftdi_device = { + let mut devices = FtdiGpio::list_devices() + .wrap_err("failed to enumerate ftdi devices")? + .filter(|di| di.description == desc); + let Some(ftdi_device) = devices.next() else { + bail!( + "failed to get any ftdi devices that match the description \"{desc}\"" + ); + }; + if devices.next().is_some() { + bail!("multiple ftdi devices matched the description \"{desc}\""); + } + ftdi_device + }; + + let usb_device_info = { + let mut devices = nusb::list_devices() + .wrap_err("failed to enumerate devices")? + .filter(|d| d.vendor_id() == ftdi_device.vendor_id) + .filter(|d| d.product_id() == ftdi_device.product_id) + .filter(|d| { + // See module-level docs for more info about missing serial numbers. + let sn = d.serial_number().unwrap_or(""); + sn == "000000000" + || sn.is_empty() + || sn == ftdi_device.serial_number + }); + + let usb_device = devices.next().ok_or_eyre( + "failed to find matching device in usbfs even though we found a \ + matching device from the FTDI library \ + , maybe the device was removed just after the check", + )?; + if devices.next().is_some() { + bail!("multiple usb devices matched {ftdi_device:?}"); + } + usb_device + }; + + let usb_device = usb_device_info + .open() + .wrap_err("failed to open usb device")?; + for iinfo in usb_device_info.interfaces() { + // Detaching the iface from other kernel drivers is necessary for + // libftd2xx to work. + // See also https://stackoverflow.com/a/34021765 + let _ = usb_device.detach_kernel_driver(iinfo.interface_number()); + } + let ftdi = libftd2xx::Ftdi::with_description(desc).wrap_err_with(|| { + format!("failed to open FTDI device with description \"{desc}\"") + })?; + + Ok(Builder(NeedsConfiguring { device: ftdi })) + } + + /// NOTE: Do not use this unless you have to. The official docs say that the + /// order in which devices enumerate is not stable. + pub fn with_index(self, index: u8) -> Result> { + let mut device = + libftd2xx::Ftdi::with_index(index.into()).wrap_err_with(|| { + format!("failed to open ftdi device with index {index}") + })?; + let info = device.device_info().wrap_err("failed to get device info")?; + debug!("opened device: {info:?}"); + + todo!() + } } impl Builder { @@ -111,16 +217,15 @@ impl Builder { .wrap_err("Failed to set device into async bitbang mode")?; let current_pin_state = read_pins(&mut self.0.device) .wrap_err("failed to read initial pin state")?; - let serial = self + let device_info = self .0 .device .device_info() - .wrap_err("faild to get device serial")? - .serial_number; + .wrap_err("failed to get device info")?; Ok(FtdiGpio { device: self.0.device, desired_state: current_pin_state, - serial, + device_info, is_destroyed: false, }) } @@ -132,7 +237,7 @@ impl Builder { pub struct FtdiGpio { device: libftd2xx::Ftdi, desired_state: u8, - serial: String, + device_info: libftd2xx::DeviceInfo, is_destroyed: bool, } @@ -140,7 +245,6 @@ impl FtdiGpio { pub const RTS_PIN: Pin = Pin(2); pub const CTS_PIN: Pin = Pin(3); - #[allow(dead_code)] pub fn list_devices() -> Result> { libftd2xx::list_devices() .wrap_err("failed to list devices") @@ -166,12 +270,21 @@ impl FtdiGpio { write_pins(&mut self.device, self.desired_state) } + pub fn device_info(&mut self) -> Result { + self.device + .device_info() + .wrap_err("failed to get device info") + } + /// Destroys the ftdi device, and fully resets its usb interface. Using this /// instead of Drop allows for explicit handling of errors. pub fn destroy(mut self) -> Result<()> { self.destroy_helper() } + /// # Panics + /// Panics if there is more than one matching device found. We don't have + /// the ability to handle this case elegantly so better to just panic. fn destroy_helper(&mut self) -> Result<()> { if self.is_destroyed { return Ok(()); @@ -181,10 +294,27 @@ impl FtdiGpio { .set_bit_mode(0, libftd2xx::BitMode::Reset) .unwrap(); self.device.close().unwrap(); - let usb_device_info = nusb::list_devices() + let devices: Vec<_> = nusb::list_devices() .wrap_err("failed to enumerate devices")? - .find(|d| d.serial_number() == Some(&self.serial)) - .ok_or_eyre("device with matching serial not found")?; + .filter(|d| d.vendor_id() == self.device_info.vendor_id) + .filter(|d| d.product_id() == self.device_info.product_id) + .filter(|d| { + // See module-level docs for more info about missing serial numbers. + let sn = d.serial_number().unwrap_or(""); + sn == "000000000" + || sn.is_empty() + || sn == self.device_info.serial_number + }) + .collect(); + + if devices.is_empty() { + bail!("no matching devices found"); + } + if devices.len() > 1 { + panic!("more than one matching device found"); + } + let usb_device_info = devices.into_iter().last().unwrap(); + let usb_device = usb_device_info .open() .wrap_err("failed to open usb device")?; @@ -197,6 +327,29 @@ impl FtdiGpio { Ok(()) } + + /// Will use FTDI's VID as a default if no other is provided + pub fn detach_all(vid: Option, pid: Option) -> Result<()> { + for d in nusb::list_devices() + .wrap_err("failed to enumerate nusb devices")? + .filter(|d| d.vendor_id() == vid.unwrap_or(libftd2xx::FTDI_VID)) + .filter(|d| { + if let Some(pid) = pid { + d.product_id() == pid + } else { + true + } + }) + { + let device = d.open().wrap_err("failed to open device")?; + for iface in d.interfaces() { + device + .detach_kernel_driver(iface.interface_number()) + .wrap_err("failed to detach kernel driver")?; + } + } + Ok(()) + } } fn write_pins(device: &mut libftd2xx::Ftdi, pin_state: u8) -> Result<()> {