"""Device discovery utilities for SDR devices.""" import json import re import subprocess from typing import Any, Dict, List, Tuple import click # Track loaded and failed drivers _loaded_drivers = [] _failed_drivers = [] _failure_reasons = {} def load_sdr_drivers(verbose: bool = False) -> Tuple[List[str], List[str], Dict[str, str]]: """ Load available SDR drivers. Args: verbose: Show detailed error messages Returns: Tuple of (loaded_drivers, failed_drivers, failure_reasons) """ global _loaded_drivers, _failed_drivers, _failure_reasons # noqa: F824 _loaded_drivers.clear() _failed_drivers.clear() _failure_reasons.clear() # Try to import each SDR driver drivers = { "pluto": "utils.sdr.pluto", "hackrf": "utils.sdr.hackrf", "bladerf": "utils.sdr.bladerf", "usrp": "utils.sdr.usrp", "rtlsdr": "utils.sdr.rtlsdr", "thinkrf": "utils.sdr.thinkrf", } for driver_name, module_path in drivers.items(): try: # Attempt to import the driver module if not verbose: # Suppress output for quiet loading import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore") __import__(module_path) else: __import__(module_path) _loaded_drivers.append(driver_name) except ImportError as e: _failed_drivers.append(driver_name) error_msg = str(e) if "No module named" in error_msg: module_name = error_msg.split("'")[1] if "'" in error_msg else "unknown" _failure_reasons[driver_name] = f"ModuleNotFoundError: {module_name}" else: _failure_reasons[driver_name] = f"ImportError: {error_msg}" except Exception as e: _failed_drivers.append(driver_name) _failure_reasons[driver_name] = f"{type(e).__name__}: {str(e)}" return _loaded_drivers, _failed_drivers, _failure_reasons def find_hackrf_devices() -> List[Dict[str, Any]]: """Find HackRF devices using hackrf_info command.""" devices = [] try: result = subprocess.check_output(["hackrf_info"], universal_newlines=True, stderr=subprocess.STDOUT, timeout=5) # Parse device info device = {"type": "HackRF One"} for line in result.split("\n"): if "Index: " in line: if "serial" in device: devices.append(device) device = {"type": "HackRF One", "device_index": line.split(":")[1].strip()} if "Serial number:" in line: device["serial"] = line.split(":")[1].strip() elif "Board ID Number:" in line: device["board_id"] = line.split(":")[1].strip() elif "Firmware Version:" in line: device["firmware"] = line.split(":")[1].strip() if "serial" in device: devices.append(device) except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): pass return devices def find_bladerf_devices() -> List[Dict[str, Any]]: """Find BladeRF devices using bladeRF-cli command.""" devices = [] try: result = subprocess.check_output( ["bladeRF-cli", "-p"], universal_newlines=True, stderr=subprocess.STDOUT, timeout=5 ) # Parse device info device = {"type": "BladeRF"} for line in result.strip().split("\n"): line = line.strip() if ":" in line: key, value = line.split(":", 1) device[key.strip()] = value.strip() if device: devices.append(device) except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): pass return devices def find_uhd_devices() -> List[Dict[str, Any]]: """Find USRP/UHD devices using uhd_find_devices command.""" devices = [] try: result = subprocess.check_output( ["uhd_find_devices"], universal_newlines=True, stderr=subprocess.STDOUT, timeout=10 ) # Parse device blocks if "-- UHD Device" in result: device_blocks = result.split("-- UHD Device")[1:] for block in device_blocks: device = {} lines = block.strip().split("\n") for line in lines: line = line.strip() if ":" in line and not line.startswith("--"): key, value = line.split(":", 1) device[key.strip()] = value.strip() if device: devices.append(device) except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): pass return devices def find_rtlsdr_devices() -> List[Dict[str, Any]]: """Find RTL-SDR devices using rtl_test command.""" devices = [] try: result = subprocess.check_output( ["rtl_test", "-t"], universal_newlines=True, stderr=subprocess.STDOUT, timeout=5 ) # Parse device count for line in result.split("\n"): if "Found" in line and "device" in line: match = re.search(r"Found (\d+) device", line) if match: count = int(match.group(1)) elif "SN: " in line: device_match = re.search(r"(\d+): .*SN: (\w+)", line) if device_match: devices.append( {"type": "RTL-SDR", "device_index": device_match.group(1), "serial": device_match.group(2)} ) if "count" in locals() and len(devices) != count: raise ValueError("Number of stated devices does not match number of found devices") except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): pass return devices def ping_ip(ip: str, timeout: int = 1) -> bool: """ Ping an IP address to check if device is reachable. Args: ip: IP address to ping timeout: Timeout in seconds Returns: True if ping successful, False otherwise """ try: subprocess.check_output( ["ping", "-c", "1", "-W", str(timeout), ip], stderr=subprocess.STDOUT, timeout=timeout + 1 ) return True except (subprocess.CalledProcessError, subprocess.TimeoutExpired): return False def find_pluto_network() -> List[Dict[str, Any]]: """Find PlutoSDR devices on the network by pinging common addresses.""" devices = [] network_candidates = ["pluto.local", "192.168.2.1", "192.168.3.1"] for addr in network_candidates: if ping_ip(addr, timeout=1): devices.append( { "type": "PlutoSDR", "uri": f"ip:{addr}", "description": "Network PlutoSDR", } ) return devices def find_pluto_devices() -> List[Dict[str, Any]]: """Find PlutoSDR devices using pyadi-iio.""" devices = [] try: import iio contexts = iio.scan_contexts() for uri, description in contexts.items(): if "PlutoSDR" in description or "pluto" in uri.lower(): try: ctx = iio.Context(uri) device_info = { "type": "PlutoSDR", "uri": uri, "serial": ctx.attrs.get("hw_serial", "unknown"), "firmware": ctx.attrs.get("fw_version", "unknown"), "ip_addr": ctx.attrs.get("ip,ip-addr", "unknown"), "model": ctx.attrs.get("hw_model", "unknown"), "description": description, } unique = True for existing_device in devices: if existing_device["serial"] == device_info["serial"]: unique = False if unique: devices.append(device_info) ctx._destroy() except Exception: pass except ImportError: # Fallback to network ping discovery if pyadi-iio not available devices.extend(find_pluto_network()) if not devices: usb_devices = get_usb_devices() pluto_usb = [d for d in usb_devices if "PlutoSDR" in d.get("sdr_type", "")] for pluto in pluto_usb: pluto["type"] = "PlutoSDR" pluto["uri"] = "usb:" + pluto["bus"] devices.append(pluto) return devices def find_thinkrf_devices() -> List[Dict[str, Any]]: """Find ThinkRF devices (placeholder for future implementation).""" # ThinkRF uses network-based discovery with proprietary SDK # TODO: Implement when pyrf is available and working return [] def get_usb_devices() -> List[Dict[str, Any]]: """Get USB devices using lsusb for SDR identification.""" sdr_devices = [] sdr_ids = { "2cf0:5250": "BladeRF 2.0", "2cf0:5246": "BladeRF 1.0", "0bda:2838": "RTL-SDR", "0456:b673": "PlutoSDR (ADALM-PLUTO)", "2500:0020": "USRP B210", "2500:0021": "USRP B200", "1d50:604b": "HackRF One", } try: result = subprocess.check_output(["lsusb"], universal_newlines=True, timeout=5) for line in result.strip().split("\n"): for vid_pid, device_name in sdr_ids.items(): if vid_pid in line: match = re.match(r"Bus (\d+) Device (\d+): ID ([0-9a-f:]+) (.+)", line) if match: bus, device, usb_id, description = match.groups() sdr_devices.append( { "bus": bus, "device": device, "usb_id": usb_id, "description": description, "sdr_type": device_name, } ) except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): pass return sdr_devices def discover_all_devices(verbose: bool = False, json_output: bool = False) -> int: """ Discover all SDR devices with signal-testbed style output. Args: verbose: Show detailed error messages Returns: A dictionary containing information """ load_sdr_drivers(verbose=verbose) uhd_devices = find_uhd_devices() pluto_devices = find_pluto_devices() rtlsdr_devices = find_rtlsdr_devices() bladerf_devices = find_bladerf_devices() hackrf_devices = find_hackrf_devices() # Collect all device info all_devices = [] all_devices.extend(uhd_devices) all_devices.extend(pluto_devices) all_devices.extend(rtlsdr_devices) all_devices.extend(bladerf_devices) all_devices.extend(hackrf_devices) output = { "loaded_drivers": _loaded_drivers, "failed_drivers": _failed_drivers, "devices": all_devices, "total_devices": len(all_devices), } if verbose: output["failure_reasons"] = _failure_reasons if not json_output: output["uhd_devices"] = uhd_devices output["pluto_devices"] = pluto_devices output["rtlsdr_devices"] = rtlsdr_devices output["bladerf_devices"] = bladerf_devices output["hackrf_devices"] = hackrf_devices return output def print_all_devices(device_dict: dict, verbose: bool = False) -> int: # noqa: C901 """ Print all SDR devices with signal-testbed style output. Args: device_dict: Dictionary containing all device info verbose: Show detailed error messages Returns: Total number of devices found """ total_devices = 0 # USRP/UHD Discovery - Try command-line tool even if driver failed to load uhd_devices = device_dict["uhd_devices"] if uhd_devices: click.echo(f"\nšŸ“” USRP/UHD devices ({len(uhd_devices)}):") for device in uhd_devices: name = device.get("name", "Unknown") product = device.get("product", "Unknown") serial = device.get("serial", "Unknown") click.echo(f" āœ… {name} ({product}) - Serial: {serial}") total_devices += len(uhd_devices) else: if verbose: click.echo("\nšŸ“” USRP/UHD devices: None found") # PlutoSDR Discovery - Try both pyadi-iio and USB detection pluto_devices = device_dict["pluto_devices"] pluto_count = len(pluto_devices) if pluto_count > 0: click.echo(f"\nšŸ“± PlutoSDR devices ({pluto_count}):") for device in pluto_devices: # Determine if network or USB based on URI uri = device["uri"] if uri.startswith("ip:"): click.echo(f" āœ… Network: {uri.replace('ip:', '')}") elif uri.startswith("usb:"): click.echo(f" āœ… USB: {device['description']} (Bus {uri.replace('usb:', '').split('.')[0]})") else: click.echo(f" āœ… {uri}") total_devices += pluto_count else: if verbose: click.echo("\nšŸ“± PlutoSDR devices: None found") # RTL-SDR Discovery if "rtlsdr" in _loaded_drivers: rtl_devices = device_dict["rtlsdr_devices"] if rtl_devices: click.echo(f"\nšŸ“» RTL-SDR devices ({len(rtl_devices)}):") for device in rtl_devices: idx = device.get("device_index", 0) click.echo(f" āœ… Device {idx}: {device.get('type', 'RTL-SDR')}") total_devices += len(rtl_devices) else: if verbose: click.echo("\nšŸ“» RTL-SDR devices: None found") # BladeRF Discovery if "bladerf" in _loaded_drivers: bladerf_devices = device_dict["bladerf_devices"] if bladerf_devices: click.echo(f"\n⚔ BladeRF devices ({len(bladerf_devices)}):") for device in bladerf_devices: desc = device.get("Description", "BladeRF") serial = device.get("Serial", "Unknown") click.echo(f" āœ… {desc} - Serial: {serial}") total_devices += len(bladerf_devices) else: if verbose: click.echo("\n⚔ BladeRF devices: None found") # HackRF Discovery if "hackrf" in _loaded_drivers: hackrf_devices = device_dict["hackrf_devices"] if hackrf_devices: click.echo(f"\nšŸ”§ HackRF devices ({len(hackrf_devices)}):") for device in hackrf_devices: serial = device.get("serial", "Unknown") board = device.get("board_id", "") firmware = device.get("firmware", "") info = f"Serial: {serial}" if board: info += f" - Board ID: {board}" if firmware: info += f" - FW: {firmware}" click.echo(f" āœ… {device.get('type', 'HackRF')} - {info}") total_devices += len(hackrf_devices) else: if verbose: click.echo("\nšŸ”§ HackRF devices: None found") # ThinkRF Discovery if "thinkrf" in _loaded_drivers: if verbose: click.echo("\n🌐 ThinkRF devices: Discovery not yet implemented") return total_devices @click.command(help="Discover connected SDR devices") @click.option("--verbose", "-v", is_flag=True, help="Show detailed information and errors") @click.option("--json-output", is_flag=True, help="Output in JSON format") def discover(verbose, json_output): """Discover connected SDR devices with driver loading.""" device_dict = discover_all_devices(verbose=verbose, json_output=json_output) # JSON mode: Load drivers and return structured data if json_output: click.echo(json.dumps(device_dict, indent=2)) return # Human-readable mode: Signal-testbed style # Print loaded drivers if _loaded_drivers: click.echo(f"\nāœ… Loaded drivers ({len(_loaded_drivers)}):") for driver in _loaded_drivers: click.echo(f" {driver}") else: click.echo("\nāŒ No drivers loaded successfully") # Print failed drivers if _failed_drivers: click.echo(f"\nāŒ Failed drivers ({len(_failed_drivers)}):") for driver in _failed_drivers: if verbose and driver in _failure_reasons: click.echo(f" {driver}: {_failure_reasons[driver]}") else: click.echo(f" {driver}") if not verbose and _failed_drivers: click.echo("\nRun with --verbose to see failure reasons") # Device discovery click.echo("\n" + "=" * 40) click.echo("Attached Devices") click.echo("=" * 40) total_devices = print_all_devices(device_dict=device_dict, verbose=verbose) # Summary click.echo("\n" + "=" * 40) click.echo("Discovery Summary") click.echo("=" * 40) click.echo(f"Loaded drivers: {len(_loaded_drivers)}") click.echo(f"Failed drivers: {len(_failed_drivers)}") click.echo(f"Detected devices: {total_devices}") if total_devices == 0: click.echo("\nšŸ’” No devices detected - ensure they are connected and powered on")