ria-toolkit-oss/ria_toolkit_oss_cli/ria_toolkit_oss/discover.py

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")