519 lines
17 KiB
Python
519 lines
17 KiB
Python
"""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")
|