Skip to content

Commit 5c074b8

Browse files
committed
feat(hil): ftdi cmd: list + eeprom read/write
Refactored FTDI serial terminology - Clear distinction between: - `--usb-serial` (physical USB device, e.g., FT7ABC12) - `--ftdi-serial` (channel-specific, e.g., FT7ABC12C) - `--desc` (e.g., FT4232H C) Added FtdiChannel enum with FromStr impl for parsing A/B/C/D Updated commands (button-ctrl, reboot, ftdi read/write) to use the new terminology Added `ftdi list` command to enumerate all FTDI devices/channels Fixed serial comparison in destroy_helper() to strip channel suffix when comparing with USB serial
1 parent 7862e72 commit 5c074b8

File tree

5 files changed

+383
-84
lines changed

5 files changed

+383
-84
lines changed

hil/src/boot.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use std::time::Duration;
22

3-
use crate::ftdi::{FtdiGpio, FtdiId, OutputState};
3+
use crate::ftdi::{FtdiChannel, FtdiGpio, FtdiId, OutputState};
44
use color_eyre::{eyre::WrapErr as _, Result};
55
use tracing::{debug, info};
66

7-
pub const BUTTON_PIN: crate::ftdi::Pin = FtdiGpio::CTS_PIN;
8-
pub const RECOVERY_PIN: crate::ftdi::Pin = FtdiGpio::RTS_PIN;
7+
pub const BUTTON_PIN: crate::ftdi::Pin = FtdiGpio::DTR_PIN;
8+
pub const RECOVERY_PIN: crate::ftdi::Pin = FtdiGpio::CTS_PIN;
99
pub const NVIDIA_VENDOR_ID: u16 = 0x0955;
1010
pub const NVIDIA_USB_ETHERNET: u16 = 0x7035;
1111

@@ -20,15 +20,28 @@ pub async fn is_recovery_mode_detected() -> Result<bool> {
2020
Ok(num_nvidia_devices > 0)
2121
}
2222

23-
/// If `device` is `None`, will get the first available device.
23+
/// The default channel used for button/recovery control on the HIL.
24+
pub const DEFAULT_CHANNEL: FtdiChannel = FtdiChannel::C;
25+
26+
/// If `device` is `None`, will get the first available device, or fall back to
27+
/// the default channel (FT4232H C) if multiple devices are found.
2428
#[tracing::instrument]
2529
pub async fn reboot(recovery: bool, device: Option<&FtdiId>) -> Result<()> {
2630
fn make_ftdi(device: Option<FtdiId>) -> Result<FtdiGpio> {
2731
let builder = FtdiGpio::builder();
2832
let builder = match &device {
2933
Some(FtdiId::Description(desc)) => builder.with_description(desc),
30-
Some(FtdiId::SerialNumber(serial)) => builder.with_serial_number(serial),
31-
None => builder.with_default_device(),
34+
Some(FtdiId::FtdiSerial(serial)) => builder.with_ftdi_serial(serial),
35+
None => match builder.with_default_device() {
36+
Ok(b) => return b.configure().wrap_err("failed to configure ftdi"),
37+
Err(_) => {
38+
// Fall back to default channel when multiple devices exist
39+
return FtdiGpio::builder()
40+
.with_description(DEFAULT_CHANNEL.description_suffix())
41+
.and_then(|b| b.configure())
42+
.wrap_err("failed to create ftdi device with default channel");
43+
}
44+
},
3245
};
3346
builder
3447
.and_then(|b| b.configure())

hil/src/commands/button_ctrl.rs

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,112 @@ use clap::Parser;
22
use color_eyre::{eyre::WrapErr as _, Result};
33
use humantime::parse_duration;
44
use std::time::Duration;
5-
use tracing::info;
5+
use tracing::{debug, info};
66

7-
use crate::boot::BUTTON_PIN;
8-
use crate::ftdi::{FtdiGpio, OutputState};
7+
use crate::boot::{BUTTON_PIN, DEFAULT_CHANNEL};
8+
use crate::ftdi::{FtdiChannel, FtdiGpio, OutputState};
99

10+
/// Control the orb button over the debug board
1011
#[derive(Debug, Parser)]
1112
pub struct ButtonCtrl {
12-
///Button press duration (e.g., "1s", "500ms")
13+
/// Button press duration (e.g., "1s", "500ms")
1314
#[arg(long, default_value = "1s", value_parser = parse_duration)]
1415
press_duration: Duration,
16+
17+
/// The USB serial number of the FTDI chip (e.g., "FT7ABC12").
18+
/// Will use the default channel (C) for this chip.
19+
#[arg(long, conflicts_with_all = ["ftdi_serial", "desc"])]
20+
usb_serial: Option<String>,
21+
22+
/// The FTDI serial number including channel (e.g., "FT7ABC12C").
23+
/// This is the USB serial + channel letter (A/B/C/D).
24+
#[arg(long, conflicts_with_all = ["usb_serial", "desc"])]
25+
ftdi_serial: Option<String>,
26+
27+
/// The FTDI description (e.g., "FT4232H C").
28+
#[arg(long, conflicts_with_all = ["usb_serial", "ftdi_serial"])]
29+
desc: Option<String>,
30+
31+
/// The channel to use when --usb-serial is provided (A, B, C, or D).
32+
/// Defaults to C.
33+
#[arg(long, default_value = "C", requires = "usb_serial")]
34+
channel: FtdiChannel,
1535
}
1636

1737
impl ButtonCtrl {
1838
pub async fn run(self) -> Result<()> {
19-
fn make_ftdi() -> Result<FtdiGpio> {
20-
FtdiGpio::builder()
21-
.with_default_device()
22-
.and_then(|b| b.configure())
23-
.wrap_err("failed to create ftdi device")
24-
}
39+
let usb_serial = self.usb_serial.clone();
40+
let ftdi_serial = self.ftdi_serial.clone();
41+
let desc = self.desc.clone();
42+
let channel = self.channel;
43+
44+
let make_ftdi = move || -> Result<FtdiGpio> {
45+
let builder = FtdiGpio::builder();
46+
match (usb_serial.as_ref(), ftdi_serial.as_ref(), desc.as_ref()) {
47+
(Some(usb_serial), None, None) => {
48+
debug!(
49+
"using FTDI device with USB serial: {usb_serial}, channel: {:?}",
50+
channel
51+
);
52+
builder
53+
.with_usb_serial(usb_serial, channel)
54+
.and_then(|b| b.configure())
55+
.wrap_err("failed to create ftdi device with USB serial")
56+
}
57+
(None, Some(ftdi_serial), None) => {
58+
debug!("using FTDI device with FTDI serial: {ftdi_serial}");
59+
builder
60+
.with_ftdi_serial(ftdi_serial)
61+
.and_then(|b| b.configure())
62+
.wrap_err("failed to create ftdi device with FTDI serial")
63+
}
64+
(None, None, Some(desc)) => {
65+
debug!("using FTDI device with description: {desc}");
66+
builder
67+
.with_description(desc)
68+
.and_then(|b| b.configure())
69+
.wrap_err("failed to create ftdi device with description")
70+
}
71+
(None, None, None) => {
72+
// Try default device first, fall back to default channel description
73+
match builder.with_default_device() {
74+
Ok(b) => {
75+
b.configure().wrap_err("failed to configure ftdi device")
76+
}
77+
Err(e) => {
78+
debug!("default device selection failed: {e}");
79+
let desc_suffix = DEFAULT_CHANNEL.description_suffix();
80+
debug!(
81+
"attempting to find device with description '{desc_suffix}'"
82+
);
83+
FtdiGpio::builder()
84+
.with_description(desc_suffix)
85+
.and_then(|b| b.configure())
86+
.wrap_err_with(|| {
87+
format!(
88+
"failed to open FTDI device with description \
89+
'{desc_suffix}'"
90+
)
91+
})
92+
}
93+
}
94+
}
95+
_ => unreachable!(),
96+
}
97+
};
2598

2699
info!(
27100
"Holding button for {} seconds",
28101
self.press_duration.as_secs_f32()
29102
);
103+
let press_duration = self.press_duration;
30104
tokio::task::spawn_blocking(move || -> Result<(), color_eyre::Report> {
31105
let mut ftdi = make_ftdi()?;
32106
ftdi.set_pin(BUTTON_PIN, OutputState::Low)?;
33-
std::thread::sleep(self.press_duration);
107+
std::thread::sleep(press_duration);
34108
ftdi.set_pin(BUTTON_PIN, OutputState::High)?;
35109
ftdi.destroy().wrap_err("failed to destroy ftdi")?;
110+
36111
Ok(())
37112
})
38113
.await

hil/src/commands/ftdi.rs

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! FTDI EEPROM read/write operations for FT4232H chips.
1+
//! FTDI device operations for FT4232H chips.
22
33
use camino::Utf8PathBuf;
44
use clap::{Parser, Subcommand};
@@ -11,7 +11,9 @@ use nusb::MaybeFuture;
1111
use serde::{Deserialize, Serialize};
1212
use tracing::{debug, info};
1313

14-
/// FTDI EEPROM operations
14+
use crate::ftdi::{detach_all_ftdi_kernel_drivers, FtdiGpio};
15+
16+
/// FTDI device operations
1517
#[derive(Debug, Parser)]
1618
pub struct FtdiCmd {
1719
#[command(subcommand)]
@@ -20,23 +22,34 @@ pub struct FtdiCmd {
2022

2123
#[derive(Debug, Subcommand)]
2224
enum FtdiSubcommand {
25+
/// List all connected FTDI devices/channels
26+
List(ListCmd),
2327
/// Read EEPROM content and dump to a file
2428
Read(ReadCmd),
2529
/// Write EEPROM content from a file
2630
Write(WriteCmd),
2731
}
2832

33+
/// List all connected FTDI devices
34+
#[derive(Debug, Parser)]
35+
struct ListCmd;
36+
2937
/// Read EEPROM content to a file or stdout
3038
#[derive(Debug, Parser)]
3139
struct ReadCmd {
3240
/// Output file path (JSON format). If not specified, prints to stdout.
3341
#[arg(long, short)]
3442
file: Option<Utf8PathBuf>,
35-
/// The serial number of the FTDI device to use
43+
44+
/// The USB serial number of the FTDI chip (e.g., "FT7ABC12").
45+
/// Note: EEPROM is shared across all channels of a chip, so any channel
46+
/// works for reading/writing EEPROM.
3647
#[arg(long, conflicts_with = "desc")]
37-
serial_num: Option<String>,
38-
/// The description of the FTDI device to use
39-
#[arg(long, conflicts_with = "serial_num")]
48+
usb_serial: Option<String>,
49+
50+
/// The FTDI description (e.g., "FT4232H A").
51+
/// Any channel description works since EEPROM is chip-wide.
52+
#[arg(long, conflicts_with = "usb_serial")]
4053
desc: Option<String>,
4154
}
4255

@@ -45,11 +58,16 @@ struct ReadCmd {
4558
struct WriteCmd {
4659
/// Input file path (JSON format)
4760
input: Utf8PathBuf,
48-
/// The serial number of the FTDI device to use
61+
62+
/// The USB serial number of the FTDI chip (e.g., "FT7ABC12").
63+
/// Note: EEPROM is shared across all channels of a chip, so any channel
64+
/// works for reading/writing EEPROM.
4965
#[arg(long, conflicts_with = "desc")]
50-
serial_num: Option<String>,
51-
/// The description of the FTDI device to use
52-
#[arg(long, conflicts_with = "serial_num")]
66+
usb_serial: Option<String>,
67+
68+
/// The FTDI description (e.g., "FT4232H A").
69+
/// Any channel description works since EEPROM is chip-wide.
70+
#[arg(long, conflicts_with = "usb_serial")]
5371
desc: Option<String>,
5472
}
5573

@@ -130,20 +148,52 @@ impl Ft4232hEepromData {
130148
impl FtdiCmd {
131149
pub async fn run(self) -> Result<()> {
132150
match self.command {
151+
FtdiSubcommand::List(cmd) => cmd.run().await,
133152
FtdiSubcommand::Read(cmd) => cmd.run().await,
134153
FtdiSubcommand::Write(cmd) => cmd.run().await,
135154
}
136155
}
137156
}
138157

158+
impl ListCmd {
159+
async fn run(self) -> Result<()> {
160+
tokio::task::spawn_blocking(|| -> Result<()> {
161+
detach_all_ftdi_kernel_drivers();
162+
163+
let devices: Vec<_> = FtdiGpio::list_devices()
164+
.wrap_err("failed to list FTDI devices")?
165+
.collect();
166+
167+
if devices.is_empty() {
168+
println!("No FTDI devices found.");
169+
return Ok(());
170+
}
171+
172+
println!("Found {} FTDI device(s)/channel(s):\n", devices.len());
173+
for (i, device) in devices.iter().enumerate() {
174+
println!("Device {}:", i + 1);
175+
println!(" Description: {}", device.description);
176+
println!(" FTDI Serial: {}", device.serial_number);
177+
println!(" Vendor ID: 0x{:04X}", device.vendor_id);
178+
println!(" Product ID: 0x{:04X}", device.product_id);
179+
println!();
180+
}
181+
182+
Ok(())
183+
})
184+
.await
185+
.wrap_err("task panicked")?
186+
}
187+
}
188+
139189
impl ReadCmd {
140190
async fn run(self) -> Result<()> {
141191
let output_path = self.file.clone();
142-
let serial_num = self.serial_num.clone();
192+
let usb_serial = self.usb_serial.clone();
143193
let desc = self.desc.clone();
144194

145195
tokio::task::spawn_blocking(move || -> Result<()> {
146-
let mut ft4232h = open_ft4232h(serial_num.as_deref(), desc.as_deref())?;
196+
let mut ft4232h = open_ft4232h(usb_serial.as_deref(), desc.as_deref())?;
147197

148198
info!("Reading EEPROM from FT4232H device...");
149199
let (eeprom, strings) = ft4232h
@@ -174,7 +224,7 @@ impl ReadCmd {
174224
impl WriteCmd {
175225
async fn run(self) -> Result<()> {
176226
let input_path = self.input.clone();
177-
let serial_num = self.serial_num.clone();
227+
let usb_serial = self.usb_serial.clone();
178228
let desc = self.desc.clone();
179229

180230
tokio::task::spawn_blocking(move || -> Result<()> {
@@ -191,7 +241,7 @@ impl WriteCmd {
191241

192242
let (eeprom, strings) = data.to_eeprom()?;
193243

194-
let mut ft4232h = open_ft4232h(serial_num.as_deref(), desc.as_deref())?;
244+
let mut ft4232h = open_ft4232h(usb_serial.as_deref(), desc.as_deref())?;
195245

196246
ft4232h
197247
.eeprom_program(eeprom, strings)
@@ -207,14 +257,17 @@ impl WriteCmd {
207257
}
208258
}
209259

210-
/// Opens an FT4232H device with optional serial number or description filter.
211-
fn open_ft4232h(serial_num: Option<&str>, desc: Option<&str>) -> Result<Ft4232h> {
212-
match (serial_num, desc) {
213-
(Some(serial), None) => open_ft4232h_by_serial(serial),
260+
/// Opens an FT4232H device with optional USB serial or description filter.
261+
///
262+
/// Note: For USB serial, we append 'A' to get the FTDI serial of the first channel,
263+
/// since EEPROM operations work the same on any channel of the same chip.
264+
fn open_ft4232h(usb_serial: Option<&str>, desc: Option<&str>) -> Result<Ft4232h> {
265+
match (usb_serial, desc) {
266+
(Some(usb_serial), None) => open_ft4232h_by_usb_serial(usb_serial),
214267
(None, Some(desc)) => open_ft4232h_by_description(desc),
215268
(None, None) => open_default_ft4232h(),
216269
(Some(_), Some(_)) => {
217-
bail!("cannot specify both serial number and description")
270+
bail!("cannot specify both USB serial and description")
218271
}
219272
}
220273
}
@@ -231,7 +284,7 @@ fn open_default_ft4232h() -> Result<Ft4232h> {
231284
}
232285
if usb_device_infos.len() > 1 {
233286
bail!(
234-
"multiple FTDI devices found, please specify --serial-num or --desc to select one"
287+
"multiple FTDI devices found, please specify --usb-serial or --desc to select one"
235288
);
236289
}
237290

@@ -252,14 +305,14 @@ fn open_default_ft4232h() -> Result<Ft4232h> {
252305
Ok(ft4232h)
253306
}
254307

255-
fn open_ft4232h_by_serial(serial: &str) -> Result<Ft4232h> {
256-
ensure!(!serial.is_empty(), "serial number cannot be empty");
308+
fn open_ft4232h_by_usb_serial(usb_serial: &str) -> Result<Ft4232h> {
309+
ensure!(!usb_serial.is_empty(), "USB serial cannot be empty");
257310

258311
let usb_device_info = nusb::list_devices()
259312
.wait()
260313
.wrap_err("failed to enumerate devices")?
261-
.find(|d| d.serial_number() == Some(serial))
262-
.ok_or_else(|| eyre!("no device with serial number \"{serial}\" found"))?;
314+
.find(|d| d.serial_number() == Some(usb_serial))
315+
.ok_or_else(|| eyre!("no device with USB serial \"{usb_serial}\" found"))?;
263316

264317
// Detach kernel drivers if needed
265318
if let Ok(usb_device) = usb_device_info.open().wait() {
@@ -268,8 +321,11 @@ fn open_ft4232h_by_serial(serial: &str) -> Result<Ft4232h> {
268321
}
269322
}
270323

271-
let ftdi = Ftdi::with_serial_number(serial).map_err(|e| {
272-
eyre!("failed to open FTDI device with serial \"{serial}\": {e:?}")
324+
// Use channel A (append 'A') to get the FTDI serial.
325+
// EEPROM is shared across all channels, so any channel works.
326+
let ftdi_serial = format!("{usb_serial}A");
327+
let ftdi = Ftdi::with_serial_number(&ftdi_serial).map_err(|e| {
328+
eyre!("failed to open FTDI device with FTDI serial \"{ftdi_serial}\": {e:?}")
273329
})?;
274330
let ft4232h: Ft4232h = ftdi
275331
.try_into()

0 commit comments

Comments
 (0)