From d19b5a629dae415e997be74f1ddd74d8b1017fb9 Mon Sep 17 00:00:00 2001 From: SuperSodaSea Date: Thu, 25 Jun 2026 00:06:39 +0800 Subject: [PATCH 1/3] Move USB device backends to separate files --- Cargo.toml | 4 +- src/usb_device.rs | 369 ++------------------------------- src/usb_device/libusb.rs | 126 +++++++++++ src/usb_device/wch_link_dll.rs | 215 +++++++++++++++++++ 4 files changed, 356 insertions(+), 358 deletions(-) create mode 100644 src/usb_device/libusb.rs create mode 100644 src/usb_device/wch_link_dll.rs diff --git a/Cargo.toml b/Cargo.toml index 11fb3d8..79e96f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ object = { version = "0.38", default-features = false, features = [ ] } indicatif = "0.18" serialport = "4.7" -libloading = "0.9" chrono = "0.4" clap-verbosity-flag = "3" + +[target.'cfg(all(target_os = "windows", target_arch = "x86"))'.dependencies] +libloading = "0.9" diff --git a/src/usb_device.rs b/src/usb_device.rs index 71ab957..1c14561 100644 --- a/src/usb_device.rs +++ b/src/usb_device.rs @@ -2,10 +2,15 @@ use crate::Result; use std::{ - fmt::{Debug, Display}, + fmt::Debug, time::Duration, }; +pub mod libusb; + +#[cfg(all(target_os = "windows", target_arch = "x86"))] +pub mod wch_link_dll; + pub trait USBDeviceBackend: Debug { fn set_timeout(&mut self, _timeout: Duration) {} @@ -20,22 +25,20 @@ pub trait USBDeviceBackend: Debug { pub fn open_nth(vid: u16, pid: u16, nth: usize) -> Result> { #[cfg(all(target_os = "windows", target_arch = "x86"))] - { - ch375_driver::CH375USBDevice::open_nth(vid, pid, nth) - .or_else(|_| libusb::NusbDevice::open_nth(vid, pid, nth)) - } - #[cfg(not(all(target_os = "windows", target_arch = "x86")))] - { - libusb::NusbDevice::open_nth(vid, pid, nth) + if let Ok(backend) = wch_link_dll::CH375USBDevice::open_nth(vid, pid, nth) { + return Ok(backend); } + + libusb::NusbDevice::open_nth(vid, pid, nth) } pub fn list_devices(vid: u16, pid: u16) -> Result> { let mut ret = vec![]; + #[cfg(all(target_os = "windows", target_arch = "x86"))] { ret.extend( - ch375_driver::list_devices(vid, pid)? + wch_link_dll::list_devices(vid, pid)? .into_iter() .map(|s| s.to_string()), ); @@ -49,351 +52,3 @@ pub fn list_devices(vid: u16, pid: u16) -> Result> { Ok(ret) } - -pub mod libusb { - use std::fmt; - use std::io::{Read, Write}; - - use super::*; - use nusb::MaybeFuture; - use nusb::transfer::{Bulk, In, Out}; - - pub fn list_libusb_devices(vid: u16, pid: u16) -> Result> { - let devices = nusb::list_devices().wait().map_err(crate::Error::Usb)?; - let mut result = vec![]; - let mut idx = 0; - - for device in devices { - if device.vendor_id() == vid && device.product_id() == pid { - let serial = device - .serial_number() - .map(|s| s.to_string()) - .unwrap_or_else(|| "N/A".to_string()); - - result.push(format!( - " ID {:04x}:{:04x} Serial {} ({})", - idx, - device.vendor_id(), - device.product_id(), - serial, - get_speed(device.speed()) - )); - idx += 1; - } - } - Ok(result) - } - - pub struct NusbDevice { - interface: nusb::Interface, - #[allow(dead_code)] - timeout: Duration, - } - - impl fmt::Debug for NusbDevice { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("USBDevice") - .field("provider", &"nusb") - .finish() - } - } - - impl USBDeviceBackend for NusbDevice { - fn set_timeout(&mut self, timeout: Duration) { - self.timeout = timeout; - } - - fn open_nth(vid: u16, pid: u16, nth: usize) -> Result> { - let devices: Vec<_> = nusb::list_devices() - .wait() - .map_err(crate::Error::Usb)? - .filter(|d| d.vendor_id() == vid && d.product_id() == pid) - .collect(); - - if nth >= devices.len() { - return Err(crate::Error::ProbeNotFound); - } - - let device_info = &devices[nth]; - log::trace!( - "Device: {:04x}:{:04x}", - device_info.vendor_id(), - device_info.product_id() - ); - - if let Some(serial) = device_info.serial_number() { - log::debug!("Serial number: {:?}", serial); - } - - let device = device_info.open().wait().map_err(|e| { - log::error!("Failed to open USB device: {}", e); - #[cfg(target_os = "windows")] - log::warn!("It's likely no WinUSB driver installed. Please install it from Zadig. See also: https://zadig.akeo.ie"); - #[cfg(target_os = "linux")] - log::warn!("It's likely the udev rules are not installed properly. Please refer to README.md for more details."); - crate::Error::Usb(e) - })?; - - let interface = device - .claim_interface(0) - .wait() - .map_err(crate::Error::Usb)?; - - Ok(Box::new(NusbDevice { - interface, - timeout: Duration::from_millis(5000), - })) - } - - fn read_endpoint(&mut self, ep: u8, buf: &mut [u8]) -> Result { - let endpoint = self - .interface - .endpoint::(ep) - .map_err(|e| crate::Error::Custom(format!("Failed to get endpoint: {}", e)))?; - let mut reader = endpoint.reader(64); - let n = reader.read(buf)?; - Ok(n) - } - - fn write_endpoint(&mut self, ep: u8, buf: &[u8]) -> Result<()> { - let endpoint = self - .interface - .endpoint::(ep) - .map_err(|e| crate::Error::Custom(format!("Failed to get endpoint: {}", e)))?; - let mut writer = endpoint.writer(64); - writer.write_all(buf)?; - writer.flush()?; - Ok(()) - } - } - - fn get_speed(speed: Option) -> &'static str { - match speed { - Some(nusb::Speed::SuperPlus) => "USB-SS+ 10000 Mbps", - Some(nusb::Speed::Super) => "USB-SS 5000 Mbps", - Some(nusb::Speed::High) => "USB-HS 480 Mbps", - Some(nusb::Speed::Full) => "USB-FS 12 Mbps", - Some(nusb::Speed::Low) => "USB-LS 1.5 Mbps", - _ => "(unknown)", - } - } -} - -#[cfg(all(target_os = "windows", target_arch = "x86"))] -pub mod ch375_driver { - use libloading::os::windows::*; - use std::fmt; - use std::sync::OnceLock; - - use super::*; - use crate::Error; - - static CH375_DRIVER: OnceLock> = OnceLock::new(); - - fn ensure_library_load() -> Result<&'static Library> { - let result = CH375_DRIVER.get_or_init(|| { - let lib = match unsafe { Library::new("WCHLinkDLL.dll") } { - Ok(lib) => lib, - Err(_) => return Err("WCHLinkDLL.dll not found".to_string()), - }; - - let get_version: Symbol u32> = - unsafe { lib.get(b"CH375GetVersion").unwrap() }; - let get_driver_version: Symbol u32> = - unsafe { lib.get(b"CH375GetDrvVersion").unwrap() }; - - log::debug!( - "DLL version {}, driver version {}", - unsafe { get_version() }, - unsafe { get_driver_version() } - ); - Ok(lib) - }); - - match result { - Ok(lib) => Ok(lib), - Err(e) => Err(Error::Custom(e.clone())), - } - } - - #[allow(non_snake_case, unused)] - #[derive(Debug)] - #[repr(packed)] - pub struct UsbDeviceDescriptor { - bLength: u8, - bDescriptorType: u8, - bcdUSB: u16, - bDeviceClass: u8, - bDeviceSubClass: u8, - bDeviceProtocol: u8, - bMaxPacketSize0: u8, - idVendor: u16, - idProduct: u16, - bcdDevice: u16, - iManufacturer: u8, - iProduct: u8, - iSerialNumber: u8, - bNumConfigurations: u8, - } - - pub fn list_devices(vid: u16, pid: u16) -> Result> { - let lib = ensure_library_load()?; - let mut ret: Vec = vec![]; - - let open_device: Symbol u32> = - unsafe { lib.get(b"CH375OpenDevice").unwrap() }; - let close_device: Symbol = - unsafe { lib.get(b"CH375CloseDevice").unwrap() }; - let get_device_descriptor: Symbol< - unsafe extern "stdcall" fn(u32, *mut UsbDeviceDescriptor, *mut u32) -> bool, - > = unsafe { lib.get(b"CH375GetDeviceDescr").unwrap() }; - - const INVALID_HANDLE: u32 = 0xffffffff; - - for i in 0..8 { - let h = unsafe { open_device(i) }; - if h != INVALID_HANDLE { - let mut descr = unsafe { core::mem::zeroed() }; - let mut len = core::mem::size_of::() as u32; - let _ = unsafe { get_device_descriptor(i, &mut descr, &mut len) }; - - if descr.idVendor == vid && descr.idProduct == pid { - ret.push(format!( - " CH375Driver Device {:04x}:{:04x}", - i, vid, pid - )); - - log::debug!("Device #{}: {:04x}:{:04x}", i, vid, pid); - } - unsafe { close_device(i) }; - } - } - - Ok(ret) - } - - /// USB Device implementation provided by CH375 Windows driver - pub struct CH375USBDevice { - index: u32, - } - - impl fmt::Debug for CH375USBDevice { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("USBDevice") - .field("provider", &"ch375") - .field("device", &self.index) - .finish() - } - } - - impl USBDeviceBackend for CH375USBDevice { - fn open_nth(vid: u16, pid: u16, nth: usize) -> Result> { - let lib = ensure_library_load()?; - /*HANDLE WINAPI CH375OpenDevice( // Open CH375 device, return the handle, invalid if error - ULONG iIndex ); */ - let open_device: Symbol u32> = - unsafe { lib.get(b"CH375OpenDevice").unwrap() }; - /*VOID WINAPI CH375CloseDevice( // Close the CH375 device - ULONG iIndex ); // Specify the serial number of the CH375 device */ - let close_device: Symbol = - unsafe { lib.get(b"CH375CloseDevice").unwrap() }; - let get_device_descriptor: Symbol< - unsafe extern "stdcall" fn(u32, *mut UsbDeviceDescriptor, *mut u32) -> bool, - > = unsafe { lib.get(b"CH375GetDeviceDescr").unwrap() }; - - const INVALID_HANDLE: u32 = 0xffffffff; - - let mut idx = 0; - for i in 0..8 { - let h = unsafe { open_device(i) }; - if h != INVALID_HANDLE { - let mut descr = unsafe { core::mem::zeroed() }; - let mut len = core::mem::size_of::() as u32; - let _ = unsafe { get_device_descriptor(i, &mut descr, &mut len) }; - - if descr.idVendor == vid && descr.idProduct == pid { - if idx == nth { - log::debug!("Device #{}: {:04x}:{:04x}", i, vid, pid); - return Ok(Box::new(CH375USBDevice { index: i })); - } else { - idx += 1; - } - } - unsafe { close_device(i) }; - } - } - - return Err(crate::Error::ProbeNotFound); - } - - fn read_endpoint(&mut self, ep: u8, buf: &mut [u8]) -> Result { - let lib = ensure_library_load()?; - /* - BOOL WINAPI CH375ReadEndP( // read data block - ULONG iIndex, // Specify the serial number of the CH375 device - ULONG iPipeNum, // Endpoint number, valid values are 1 to 8. - PVOID oBuffer, // Point to a buffer large enough to hold the read data - PULONG ioLength); // Point to the length unit, the length to be read when input, and the actual read length after return - */ - let read_end_point: Symbol< - unsafe extern "stdcall" fn(u32, u32, *mut u8, *mut u32) -> bool, - > = unsafe { lib.get(b"CH375ReadEndP").unwrap() }; - - let mut len = buf.len() as u32; - let ep = (ep & 0x7f) as u32; - - let ret = unsafe { read_end_point(self.index, ep, buf.as_mut_ptr(), &mut len) }; - - if ret { - Ok(len as usize) - } else { - Err(Error::Driver) - } - } - - fn write_endpoint(&mut self, ep: u8, buf: &[u8]) -> Result<()> { - let lib = ensure_library_load()?; - /* - BOOL WINAPI CH375WriteEndP( // write out data block - ULONG iIndex, // Specify the serial number of the CH375 device - ULONG iPipeNum, // Endpoint number, valid values are 1 to 8. - PVOID iBuffer, // Point to a buffer where the data to be written is placed - PULONG ioLength); // Point to the length unit, the length to be written out when input, and the length actually written out after returnF */ - let write_end_point: Symbol< - unsafe extern "stdcall" fn(u32, u32, *mut u8, *mut u32) -> bool, - > = unsafe { lib.get(b"CH375WriteEndP").unwrap() }; - - let mut len = buf.len() as u32; - let ret = unsafe { - write_end_point(self.index, ep as u32, buf.as_ptr() as *mut u8, &mut len) - }; - if ret { Ok(()) } else { Err(Error::Driver) } - } - - fn set_timeout(&mut self, timeout: Duration) { - let lib = ensure_library_load().unwrap(); - - let set_timeout_ex: Symbol< - unsafe extern "stdcall" fn(u32, u32, u32, u32, u32) -> bool, - > = unsafe { lib.get(b"CH375SetTimeoutEx").unwrap() }; - - let ds = timeout.as_millis() as u32; - - unsafe { - set_timeout_ex(self.index, ds, ds, ds, ds); - } - } - } - - impl Drop for CH375USBDevice { - fn drop(&mut self) { - if let Ok(lib) = ensure_library_load() { - let close_device: Symbol = - unsafe { lib.get(b"CH375CloseDevice").unwrap() }; - unsafe { - close_device(self.index); - } - } - } - } -} diff --git a/src/usb_device/libusb.rs b/src/usb_device/libusb.rs new file mode 100644 index 0000000..8313d04 --- /dev/null +++ b/src/usb_device/libusb.rs @@ -0,0 +1,126 @@ +use std::fmt::{self, Display}; +use std::io::{Read, Write}; + +use super::*; +use nusb::MaybeFuture; +use nusb::transfer::{Bulk, In, Out}; + +pub fn list_libusb_devices(vid: u16, pid: u16) -> Result> { + let devices = nusb::list_devices().wait().map_err(crate::Error::Usb)?; + let mut result = vec![]; + let mut idx = 0; + + for device in devices { + if device.vendor_id() == vid && device.product_id() == pid { + let serial = device + .serial_number() + .map(|s| s.to_string()) + .unwrap_or_else(|| "N/A".to_string()); + + result.push(format!( + " ID {:04x}:{:04x} Serial {} ({})", + idx, + device.vendor_id(), + device.product_id(), + serial, + get_speed(device.speed()) + )); + idx += 1; + } + } + Ok(result) +} + +pub struct NusbDevice { + interface: nusb::Interface, + #[allow(dead_code)] + timeout: Duration, +} + +impl fmt::Debug for NusbDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("USBDevice") + .field("provider", &"nusb") + .finish() + } +} + +impl USBDeviceBackend for NusbDevice { + fn set_timeout(&mut self, timeout: Duration) { + self.timeout = timeout; + } + + fn open_nth(vid: u16, pid: u16, nth: usize) -> Result> { + let devices: Vec<_> = nusb::list_devices() + .wait() + .map_err(crate::Error::Usb)? + .filter(|d| d.vendor_id() == vid && d.product_id() == pid) + .collect(); + + if nth >= devices.len() { + return Err(crate::Error::ProbeNotFound); + } + + let device_info = &devices[nth]; + log::trace!( + "Device: {:04x}:{:04x}", + device_info.vendor_id(), + device_info.product_id() + ); + + if let Some(serial) = device_info.serial_number() { + log::debug!("Serial number: {:?}", serial); + } + + let device = device_info.open().wait().map_err(|e| { + log::error!("Failed to open USB device: {}", e); + #[cfg(target_os = "windows")] + log::warn!("It's likely no WinUSB driver installed. Please install it from Zadig. See also: https://zadig.akeo.ie"); + #[cfg(target_os = "linux")] + log::warn!("It's likely the udev rules are not installed properly. Please refer to README.md for more details."); + crate::Error::Usb(e) + })?; + + let interface = device + .claim_interface(0) + .wait() + .map_err(crate::Error::Usb)?; + + Ok(Box::new(NusbDevice { + interface, + timeout: Duration::from_millis(5000), + })) + } + + fn read_endpoint(&mut self, ep: u8, buf: &mut [u8]) -> Result { + let endpoint = self + .interface + .endpoint::(ep) + .map_err(|e| crate::Error::Custom(format!("Failed to get endpoint: {}", e)))?; + let mut reader = endpoint.reader(64); + let n = reader.read(buf)?; + Ok(n) + } + + fn write_endpoint(&mut self, ep: u8, buf: &[u8]) -> Result<()> { + let endpoint = self + .interface + .endpoint::(ep) + .map_err(|e| crate::Error::Custom(format!("Failed to get endpoint: {}", e)))?; + let mut writer = endpoint.writer(64); + writer.write_all(buf)?; + writer.flush()?; + Ok(()) + } +} + +fn get_speed(speed: Option) -> &'static str { + match speed { + Some(nusb::Speed::SuperPlus) => "USB-SS+ 10000 Mbps", + Some(nusb::Speed::Super) => "USB-SS 5000 Mbps", + Some(nusb::Speed::High) => "USB-HS 480 Mbps", + Some(nusb::Speed::Full) => "USB-FS 12 Mbps", + Some(nusb::Speed::Low) => "USB-LS 1.5 Mbps", + _ => "(unknown)", + } +} diff --git a/src/usb_device/wch_link_dll.rs b/src/usb_device/wch_link_dll.rs new file mode 100644 index 0000000..b518d05 --- /dev/null +++ b/src/usb_device/wch_link_dll.rs @@ -0,0 +1,215 @@ +use libloading::os::windows::*; +use std::fmt::{self, Display}; +use std::sync::OnceLock; + +use super::*; +use crate::Error; + +static WCH_LINK_DLL: OnceLock> = OnceLock::new(); + +fn ensure_library_load() -> Result<&'static Library> { + let result = WCH_LINK_DLL.get_or_init(|| { + let lib = match unsafe { Library::new("WCHLinkDLL.dll") } { + Ok(lib) => lib, + Err(_) => return Err("WCHLinkDLL.dll not found".to_string()), + }; + + let get_version: Symbol u32> = + unsafe { lib.get(b"CH375GetVersion").unwrap() }; + let get_driver_version: Symbol u32> = + unsafe { lib.get(b"CH375GetDrvVersion").unwrap() }; + + log::debug!( + "DLL version {}, driver version {}", + unsafe { get_version() }, + unsafe { get_driver_version() } + ); + Ok(lib) + }); + + match result { + Ok(lib) => Ok(lib), + Err(e) => Err(Error::Custom(e.clone())), + } +} + +#[allow(non_snake_case, unused)] +#[derive(Debug)] +#[repr(packed)] +pub struct UsbDeviceDescriptor { + bLength: u8, + bDescriptorType: u8, + bcdUSB: u16, + bDeviceClass: u8, + bDeviceSubClass: u8, + bDeviceProtocol: u8, + bMaxPacketSize0: u8, + idVendor: u16, + idProduct: u16, + bcdDevice: u16, + iManufacturer: u8, + iProduct: u8, + iSerialNumber: u8, + bNumConfigurations: u8, +} + +pub fn list_devices(vid: u16, pid: u16) -> Result> { + let lib = ensure_library_load()?; + let mut ret: Vec = vec![]; + + let open_device: Symbol u32> = + unsafe { lib.get(b"CH375OpenDevice").unwrap() }; + let close_device: Symbol = + unsafe { lib.get(b"CH375CloseDevice").unwrap() }; + let get_device_descriptor: Symbol< + unsafe extern "stdcall" fn(u32, *mut UsbDeviceDescriptor, *mut u32) -> bool, + > = unsafe { lib.get(b"CH375GetDeviceDescr").unwrap() }; + + const INVALID_HANDLE: u32 = 0xffffffff; + + for i in 0..8 { + let h = unsafe { open_device(i) }; + if h != INVALID_HANDLE { + let mut descr = unsafe { core::mem::zeroed() }; + let mut len = core::mem::size_of::() as u32; + let _ = unsafe { get_device_descriptor(i, &mut descr, &mut len) }; + + if descr.idVendor == vid && descr.idProduct == pid { + ret.push(format!( + " CH375Driver Device {:04x}:{:04x}", + i, vid, pid + )); + + log::debug!("Device #{}: {:04x}:{:04x}", i, vid, pid); + } + unsafe { close_device(i) }; + } + } + + Ok(ret) +} + +/// USB Device implementation provided by CH375 Windows driver +pub struct CH375USBDevice { + index: u32, +} + +impl fmt::Debug for CH375USBDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("USBDevice") + .field("provider", &"ch375") + .field("device", &self.index) + .finish() + } +} + +impl USBDeviceBackend for CH375USBDevice { + fn open_nth(vid: u16, pid: u16, nth: usize) -> Result> { + let lib = ensure_library_load()?; + /*HANDLE WINAPI CH375OpenDevice( // Open CH375 device, return the handle, invalid if error + ULONG iIndex ); */ + let open_device: Symbol u32> = + unsafe { lib.get(b"CH375OpenDevice").unwrap() }; + /*VOID WINAPI CH375CloseDevice( // Close the CH375 device + ULONG iIndex ); // Specify the serial number of the CH375 device */ + let close_device: Symbol = + unsafe { lib.get(b"CH375CloseDevice").unwrap() }; + let get_device_descriptor: Symbol< + unsafe extern "stdcall" fn(u32, *mut UsbDeviceDescriptor, *mut u32) -> bool, + > = unsafe { lib.get(b"CH375GetDeviceDescr").unwrap() }; + + const INVALID_HANDLE: u32 = 0xffffffff; + + let mut idx = 0; + for i in 0..8 { + let h = unsafe { open_device(i) }; + if h != INVALID_HANDLE { + let mut descr = unsafe { core::mem::zeroed() }; + let mut len = core::mem::size_of::() as u32; + let _ = unsafe { get_device_descriptor(i, &mut descr, &mut len) }; + + if descr.idVendor == vid && descr.idProduct == pid { + if idx == nth { + log::debug!("Device #{}: {:04x}:{:04x}", i, vid, pid); + return Ok(Box::new(CH375USBDevice { index: i })); + } else { + idx += 1; + } + } + unsafe { close_device(i) }; + } + } + + return Err(crate::Error::ProbeNotFound); + } + + fn read_endpoint(&mut self, ep: u8, buf: &mut [u8]) -> Result { + let lib = ensure_library_load()?; + /* + BOOL WINAPI CH375ReadEndP( // read data block + ULONG iIndex, // Specify the serial number of the CH375 device + ULONG iPipeNum, // Endpoint number, valid values are 1 to 8. + PVOID oBuffer, // Point to a buffer large enough to hold the read data + PULONG ioLength); // Point to the length unit, the length to be read when input, and the actual read length after return + */ + let read_end_point: Symbol< + unsafe extern "stdcall" fn(u32, u32, *mut u8, *mut u32) -> bool, + > = unsafe { lib.get(b"CH375ReadEndP").unwrap() }; + + let mut len = buf.len() as u32; + let ep = (ep & 0x7f) as u32; + + let ret = unsafe { read_end_point(self.index, ep, buf.as_mut_ptr(), &mut len) }; + + if ret { + Ok(len as usize) + } else { + Err(Error::Driver) + } + } + + fn write_endpoint(&mut self, ep: u8, buf: &[u8]) -> Result<()> { + let lib = ensure_library_load()?; + /* + BOOL WINAPI CH375WriteEndP( // write out data block + ULONG iIndex, // Specify the serial number of the CH375 device + ULONG iPipeNum, // Endpoint number, valid values are 1 to 8. + PVOID iBuffer, // Point to a buffer where the data to be written is placed + PULONG ioLength); // Point to the length unit, the length to be written out when input, and the length actually written out after returnF */ + let write_end_point: Symbol< + unsafe extern "stdcall" fn(u32, u32, *mut u8, *mut u32) -> bool, + > = unsafe { lib.get(b"CH375WriteEndP").unwrap() }; + + let mut len = buf.len() as u32; + let ret = unsafe { + write_end_point(self.index, ep as u32, buf.as_ptr() as *mut u8, &mut len) + }; + if ret { Ok(()) } else { Err(Error::Driver) } + } + + fn set_timeout(&mut self, timeout: Duration) { + let lib = ensure_library_load().unwrap(); + + let set_timeout_ex: Symbol< + unsafe extern "stdcall" fn(u32, u32, u32, u32, u32) -> bool, + > = unsafe { lib.get(b"CH375SetTimeoutEx").unwrap() }; + + let ds = timeout.as_millis() as u32; + + unsafe { + set_timeout_ex(self.index, ds, ds, ds, ds); + } + } +} + +impl Drop for CH375USBDevice { + fn drop(&mut self) { + if let Ok(lib) = ensure_library_load() { + let close_device: Symbol = + unsafe { lib.get(b"CH375CloseDevice").unwrap() }; + unsafe { + close_device(self.index); + } + } + } +} From 5b9f7d059bc1f1790f8b67b7bbb1ee2b014b2787 Mon Sep 17 00:00:00 2001 From: SuperSodaSea Date: Thu, 25 Jun 2026 04:16:35 +0800 Subject: [PATCH 2/3] Add WCHLinkUSBDevice --- Cargo.lock | 52 ++++++ Cargo.toml | 10 + src/error.rs | 3 + src/usb_device.rs | 17 ++ src/usb_device/wch_link_driver.rs | 301 ++++++++++++++++++++++++++++++ 5 files changed, 383 insertions(+) create mode 100644 src/usb_device/wch_link_driver.rs diff --git a/Cargo.lock b/Cargo.lock index cfa3805..ef99c9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,6 +840,27 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -853,6 +874,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -881,6 +913,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -934,6 +976,15 @@ dependencies = [ "windows_x86_64_msvc", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" @@ -1002,4 +1053,5 @@ dependencies = [ "serialport", "simplelog", "thiserror", + "windows", ] diff --git a/Cargo.toml b/Cargo.toml index 79e96f1..d65f0bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,13 @@ clap-verbosity-flag = "3" [target.'cfg(all(target_os = "windows", target_arch = "x86"))'.dependencies] libloading = "0.9" + +[target.'cfg(all(target_os = "windows"))'.dependencies] +windows = { version = "0.62.2", features = [ + "Win32_Devices_DeviceAndDriverInstallation", + "Win32_Devices_Usb", + "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_IO", +] } diff --git a/src/error.rs b/src/error.rs index fd0216b..e286230 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,6 +47,9 @@ pub enum Error { Io(#[from] std::io::Error), #[error("Driver error")] Driver, + #[cfg(all(target_os = "windows"))] + #[error("Windows error: {0}")] + Windows(#[from] windows::core::Error), } #[derive(Debug, Clone, Copy)] diff --git a/src/usb_device.rs b/src/usb_device.rs index 1c14561..c3113ef 100644 --- a/src/usb_device.rs +++ b/src/usb_device.rs @@ -11,6 +11,9 @@ pub mod libusb; #[cfg(all(target_os = "windows", target_arch = "x86"))] pub mod wch_link_dll; +#[cfg(all(target_os = "windows"))] +pub mod wch_link_driver; + pub trait USBDeviceBackend: Debug { fn set_timeout(&mut self, _timeout: Duration) {} @@ -29,6 +32,11 @@ pub fn open_nth(vid: u16, pid: u16, nth: usize) -> Result Result> { ); } + #[cfg(all(target_os = "windows"))] + { + ret.extend( + wch_link_driver::list_devices(vid, pid)? + .into_iter() + .map(|s| s.to_string()), + ); + } + ret.extend( libusb::list_libusb_devices(vid, pid)? .into_iter() diff --git a/src/usb_device/wch_link_driver.rs b/src/usb_device/wch_link_driver.rs new file mode 100644 index 0000000..b55c603 --- /dev/null +++ b/src/usb_device/wch_link_driver.rs @@ -0,0 +1,301 @@ +use std::fmt::{self, Display}; +use windows::core::{GUID, HRESULT, PCWSTR}; +use windows::Win32::{ + Devices::DeviceAndDriverInstallation::*, + Devices::Usb::*, + Foundation::*, + Storage::FileSystem::*, + System::IO::*, +}; + +use super::*; +use crate::Error; + +static GUID_DEVINTERFACE_WCH_LINK: GUID = GUID::from_values(0xF8D5EDCA, 0xB647, 0x4E9C, [0x9B, 0xD3, 0xA5, 0xBD, 0x23, 0x28, 0xD5, 0x5C]); +static WCH_LINK_DRIVER_IOCTL: u32 = 0x00223CDC; + +struct DeviceInfoSet { + hdevinfo: HDEVINFO, +} + +impl DeviceInfoSet { + fn new() -> windows::core::Result { + Ok(DeviceInfoSet { + hdevinfo: unsafe { + SetupDiGetClassDevsW( + Some(&GUID_DEVINTERFACE_WCH_LINK), + None, + None, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE, + ) + }?, + }) + } + + fn enum_device_interfaces(&mut self, index: u32) -> windows::core::Result { + let mut device_interface = SP_DEVICE_INTERFACE_DATA::default(); + device_interface.cbSize = core::mem::size_of::() as u32; + + unsafe { + SetupDiEnumDeviceInterfaces( + self.hdevinfo, + None, + &GUID_DEVINTERFACE_WCH_LINK, + index, + &mut device_interface, + ) + }?; + + Ok(device_interface) + } + + fn get_device_interface_detail(&mut self, device_interface: &SP_DEVICE_INTERFACE_DATA) -> windows::core::Result> { + let mut required_size = 0u32; + + match unsafe { + SetupDiGetDeviceInterfaceDetailW( + self.hdevinfo, + device_interface, + None, + 0, + Some(&mut required_size), + None, + ) + } { + Ok(_) => {} + Err(e) => { + if e.code() != HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER.0) { + return Err(e.into()); + } + } + } + + let device_path_offset = core::mem::offset_of!(SP_DEVICE_INTERFACE_DETAIL_DATA_W, DevicePath); + let length = (required_size as usize - device_path_offset) / core::mem::size_of::(); + let mut device_path = vec![0u16; length]; + + unsafe { + let layout = std::alloc::Layout::from_size_align( + required_size as usize, + core::mem::align_of::(), + ).unwrap(); + let device_interface_detail = + std::alloc::alloc(layout).cast::(); + (*device_interface_detail).cbSize = core::mem::size_of::() as u32; + + SetupDiGetDeviceInterfaceDetailW( + self.hdevinfo, + device_interface, + Some(device_interface_detail), + required_size, + None, + None, + )?; + + core::ptr::copy_nonoverlapping( + device_interface_detail.cast::().add(device_path_offset), + device_path.as_mut_ptr().cast::(), + length * core::mem::size_of::(), + ); + + std::alloc::dealloc(device_interface_detail.cast::(), layout); + } + + Ok(device_path) + } +} + +impl Drop for DeviceInfoSet { + fn drop(&mut self) { + unsafe { + let _ = SetupDiDestroyDeviceInfoList(self.hdevinfo); + } + } +} + +pub struct WCHLinkUSBDevice { + handle: HANDLE, +} + +impl WCHLinkUSBDevice { + fn open_device_path(device_path: &[u16]) -> Result { + let handle = unsafe { + CreateFileW( + PCWSTR::from_raw(device_path.as_ptr()), + (GENERIC_READ | GENERIC_WRITE).0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + None, + ) + }?; + + Ok(WCHLinkUSBDevice { handle }) + } + + fn command(&mut self, buffer: &mut [u8], in_size: usize, out_size: usize) -> Result { + let mut bytes_returned: u32 = 0; + unsafe { + DeviceIoControl( + self.handle, + WCH_LINK_DRIVER_IOCTL, + Some(buffer.as_ptr().cast()), + in_size as u32, + Some(buffer.as_mut_ptr().cast()), + out_size as u32, + Some(&mut bytes_returned), + None, + ) + }?; + Ok(bytes_returned as usize) + } + + fn get_device_descriptor(&mut self) -> Result { + let mut buffer = vec![0u8; 8 + core::mem::size_of::()]; + buffer[0..16].copy_from_slice(&[ + 0x04, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, + 0x80, 0x06, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + ]); + buffer[14..16].copy_from_slice(&(core::mem::size_of::() as u16).to_ne_bytes()); + + let len = buffer.len(); + self.command(&mut buffer, len, len)?; + + let descriptor: USB_DEVICE_DESCRIPTOR = unsafe { + core::ptr::read(buffer[8..].as_ptr() as *const _) + }; + Ok(descriptor) + } +} + +impl USBDeviceBackend for WCHLinkUSBDevice { + fn open_nth(vid: u16, pid: u16, nth: usize) -> Result> { + let mut device_info_set = DeviceInfoSet::new()?; + + let device_interface = device_info_set.enum_device_interfaces(nth as u32)?; + + let device_path = device_info_set.get_device_interface_detail(&device_interface)?; + + let mut device = WCHLinkUSBDevice::open_device_path(&device_path)?; + + let device_descriptor = device.get_device_descriptor()?; + if (device_descriptor.idVendor != vid) || (device_descriptor.idProduct != pid) { + return Err(Error::ProbeNotFound); + } + + Ok(Box::new(device)) + } + + fn read_endpoint(&mut self, ep: u8, buf: &mut [u8]) -> Result { + let mut buffer = vec![0u8; 8 + buf.len()]; + buffer[0] = (ep & 0x7F) - 1; + buffer[2] = 0x01; + buffer[4..8].copy_from_slice(&(buf.len() as u32).to_ne_bytes()); + + let len = buffer.len(); + self.command(&mut buffer, 8, len)?; + + let length = u32::from_ne_bytes(buffer[4..8].try_into().unwrap()) as usize; + if length > buf.len() { + return Err(Error::InvalidPayloadLength); + } + + buf[..length].copy_from_slice(&buffer[8..8 + length]); + Ok(length) + } + + fn write_endpoint(&mut self, ep: u8, buf: &[u8]) -> Result<()> { + let mut buffer = vec![0u8; 8 + buf.len()]; + buffer[0] = ep - 1; + buffer[2] = 0x02; + buffer[4..8].copy_from_slice(&(buf.len() as u32).to_ne_bytes()); + buffer[8..8 + buf.len()].copy_from_slice(buf); + + let len = buffer.len(); + self.command(&mut buffer,len, 8)?; + + Ok(()) + } + + fn set_timeout(&mut self, timeout: Duration) { + let mut buffer = [ + 0x0F, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + let ds = timeout.as_millis() as u32; + buffer[8..12].copy_from_slice(&ds.to_ne_bytes()); + buffer[12..16].copy_from_slice(&ds.to_ne_bytes()); + buffer[16..20].copy_from_slice(&ds.to_ne_bytes()); + buffer[20..24].copy_from_slice(&ds.to_ne_bytes()); + + let len = buffer.len(); + self.command(&mut buffer, len, len).unwrap(); + } +} + +impl Drop for WCHLinkUSBDevice { + fn drop(&mut self) { + unsafe { + let _ = CloseHandle(self.handle); + } + } +} + +impl fmt::Debug for WCHLinkUSBDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("USBDevice") + .field("provider", &"wch_link") + .field("device", &self.handle) + .finish() + } +} + +pub fn list_devices(vid: u16, pid: u16) -> Result> { + let mut result: Vec = vec![]; + + let mut device_info_set = DeviceInfoSet::new()?; + + let mut index = 0u32; + + loop { + match device_info_set.enum_device_interfaces(index) { + Ok(device_interface) => { + let device_path = device_info_set.get_device_interface_detail(&device_interface)?; + + let mut device = WCHLinkUSBDevice::open_device_path(&device_path)?; + + let device_descriptor = device.get_device_descriptor()?; + if (device_descriptor.idVendor != vid) || (device_descriptor.idProduct != pid) { + index += 1; + continue; + } + + result.push(format!( + " WCHLinkDriver Device {:04x}:{:04x} ({})", + index, + vid, + pid, + String::from_utf16_lossy(device_path.as_slice().split_last().unwrap().1), + )); + + index += 1; + } + Err(e) => { + if e.code() == HRESULT::from_win32(ERROR_NO_MORE_ITEMS.0) { + break; + } else { + return Err(e.into()); + } + } + } + } + + Ok(result) +} From d263b408022cf20265150bd913d20de5efc4b1b8 Mon Sep 17 00:00:00 2001 From: SuperSodaSea Date: Thu, 25 Jun 2026 04:17:27 +0800 Subject: [PATCH 3/3] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef821e1..9675a67 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - [x] Enable or Disable 3.3V, 5V output - [x] [SDI print](https://www.cnblogs.com/liaigu/p/17628184.html) support, requires 2.10+ firmware - [x] [Serial port watching](https://github.com/ch32-rs/wlink/pull/36) for a smooth development experience -- [x] Windows native driver support, no need to install libusb manually (requires x86 build) +- [x] Windows native driver support, no need to install libusb manually ## Tested On