1- //! FTDI EEPROM read/write operations for FT4232H chips.
1+ //! FTDI device operations for FT4232H chips.
22
33use camino:: Utf8PathBuf ;
44use clap:: { Parser , Subcommand } ;
@@ -11,7 +11,9 @@ use nusb::MaybeFuture;
1111use serde:: { Deserialize , Serialize } ;
1212use 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 ) ]
1618pub struct FtdiCmd {
1719 #[ command( subcommand) ]
@@ -20,23 +22,34 @@ pub struct FtdiCmd {
2022
2123#[ derive( Debug , Subcommand ) ]
2224enum 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 ) ]
3139struct 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 {
4558struct 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 {
130148impl 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+
139189impl 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 {
174224impl 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