1use nusb::MaybeFuture;
4
5use crate::errors::{Error, Result};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub(crate) struct UsbDeviceId {
9 pub(crate) vid: u16,
10 pub(crate) pid: u16,
11 pub(crate) description: &'static str,
12}
13
14pub(crate) const USB_DEVICE_IDS: &[UsbDeviceId] = &[
15 UsbDeviceId {
16 vid: 0x1d50,
17 pid: 0x604b,
18 description: "HackRF Jawbreaker",
19 },
20 UsbDeviceId {
21 vid: 0x1d50,
22 pid: 0x6089,
23 description: "HackRF One / HackRF Pro",
24 },
25 UsbDeviceId {
26 vid: 0x1d50,
27 pid: 0xcc15,
28 description: "rad1o",
29 },
30];
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct DeviceDescriptor {
35 pub vid: u16,
37 pub pid: u16,
39 pub description: &'static str,
41 pub serial: Option<u128>,
43 pub product_string: Option<String>,
45 pub usb_api_version: u16,
47}
48
49impl DeviceDescriptor {
50 pub(crate) fn from_nusb(info: &nusb::DeviceInfo) -> Option<Self> {
51 let id = find_usb_device_id(info.vendor_id(), info.product_id())?;
52 Some(Self {
53 vid: id.vid,
54 pid: id.pid,
55 description: id.description,
56 serial: info.serial_number().and_then(parse_serial),
57 product_string: info.product_string().map(str::to_owned),
58 usb_api_version: info.device_version(),
59 })
60 }
61}
62
63pub(crate) fn find_usb_device_id(vid: u16, pid: u16) -> Option<UsbDeviceId> {
64 USB_DEVICE_IDS
65 .iter()
66 .copied()
67 .find(|candidate| candidate.vid == vid && candidate.pid == pid)
68}
69
70pub(crate) fn list_devices() -> impl MaybeFuture<Output = Result<Vec<DeviceDescriptor>>> {
71 nusb::list_devices().map(|devices| {
72 Ok(devices
73 .map_err(Error::from)?
74 .filter_map(|info| DeviceDescriptor::from_nusb(&info))
75 .collect())
76 })
77}
78
79pub(crate) fn select_device(
80 serial: Option<u128>,
81) -> impl MaybeFuture<Output = Result<nusb::DeviceInfo>> {
82 nusb::list_devices().map(move |devices| {
83 devices
84 .map_err(Error::from)?
85 .find(|info| matches_device(info, serial))
86 .ok_or(Error::DeviceNotFound)
87 })
88}
89
90fn matches_device(info: &nusb::DeviceInfo, serial: Option<u128>) -> bool {
91 find_usb_device_id(info.vendor_id(), info.product_id()).is_some()
92 && serial.is_none_or(|wanted| info.serial_number().and_then(parse_serial) == Some(wanted))
93}
94
95pub(crate) fn parse_serial(value: &str) -> Option<u128> {
97 let value = value.trim();
98 if value.len() != 32 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
99 return None;
100 }
101 u128::from_str_radix(value, 16).ok()
102}
103
104#[cfg(any(target_arch = "wasm32", test))]
105pub(crate) fn format_serial(value: u128) -> String {
106 format!("{value:032x}")
107}
108
109#[cfg(target_arch = "wasm32")]
110async fn request_nusb_device(serial: Option<u128>) -> Result<Option<nusb::DeviceInfo>> {
111 let selectors = USB_DEVICE_IDS
112 .iter()
113 .map(|id| {
114 let selector = nusb::DeviceSelector::all().with_vid_pid(id.vid, id.pid);
115 match serial {
116 Some(serial) => selector.with_serial_number(format_serial(serial)),
117 None => selector,
118 }
119 })
120 .collect::<Vec<_>>();
121 nusb::request_device(&selectors).await.map_err(Error::from)
122}
123
124#[cfg(target_arch = "wasm32")]
125pub(crate) async fn request_device_permission(serial: Option<u128>) -> Result<()> {
126 for info in nusb::list_devices().await.map_err(Error::from)? {
127 if matches_device(&info, serial) {
128 return Ok(());
129 }
130 }
131
132 request_nusb_device(serial)
133 .await?
134 .filter(|info| matches_device(info, serial))
135 .map(|_| ())
136 .ok_or(Error::DeviceNotFound)
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn serial_round_trip_preserves_leading_zeroes() {
145 let serial = 0x0011_2233_4455_6677_8899_aabb_ccdd_eeff;
146 let text = format_serial(serial);
147 assert_eq!(text, "00112233445566778899aabbccddeeff");
148 assert_eq!(parse_serial(&text), Some(serial));
149 assert_eq!(parse_serial(&text.to_uppercase()), Some(serial));
150 }
151
152 #[test]
153 fn serial_parser_requires_exact_firmware_shape() {
154 assert_eq!(parse_serial("1234"), None);
155 assert_eq!(parse_serial("00112233445566778899aabbccddeefg"), None);
156 assert_eq!(
157 parse_serial(" 00112233445566778899aabbccddeeff "),
158 Some(0x0011_2233_4455_6677_8899_aabb_ccdd_eeff)
159 );
160 }
161
162 #[test]
163 fn all_libhackrf_ids_are_known() {
164 for pid in [0x604b, 0x6089, 0xcc15] {
165 assert!(find_usb_device_id(0x1d50, pid).is_some());
166 }
167 }
168}