Skip to main content

hackrf_nusb/
discovery.rs

1//! USB discovery and exact serial-number selection.
2
3use 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/// HackRF information available from USB discovery without claiming it.
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct DeviceDescriptor {
35    /// USB vendor ID.
36    pub vid: u16,
37    /// USB product ID.
38    pub pid: u16,
39    /// Static description from the known HackRF USB ID table.
40    pub description: &'static str,
41    /// Parsed full 128-bit USB serial, if present and valid.
42    pub serial: Option<u128>,
43    /// USB product string, when available.
44    pub product_string: Option<String>,
45    /// Raw HackRF USB API version from `bcdDevice`.
46    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
95/// Parse the 32 hexadecimal characters emitted by HackRF firmware.
96pub(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}