A Bluetooth Low Energy (BLE) scanner with advanced Resolvable Private Address (RPA) resolution. Discover nearby BLE devices, track a specific device by MAC address, or resolve privacy-randomized addresses using an Identity Resolving Key (IRK).
Written by: David Kennedy (@HackingDave) Company: TrustedSec
- Discover All Devices - Scan for all broadcasting BLE devices in range with signal strength, estimated distance, manufacturer data, and service UUIDs
- Targeted Search - Search for a specific device by MAC address and monitor every detection
- IRK Resolution - Resolve Resolvable Private Addresses against one or more Identity Resolving Keys to identify devices using randomized addresses for privacy
- Multiple IRKs - Load multiple IRKs from a file to resolve addresses for several devices in a single scan
- RSSI Filtering - Filter out weak signals with a minimum RSSI threshold
- RSSI Averaging - Sliding window average smooths noisy BLE RSSI readings for more stable distance estimates
- Name Filtering - Filter devices by name with case-insensitive substring matching
- Active Scanning - Send SCAN_REQ to get SCAN_RSP with additional service UUIDs and device names that passive scanning misses
- Environment Presets - Indoor, outdoor, and free-space path-loss models for more accurate distance estimation
- Proximity Alerts - Audible/visual alert when a device is estimated within a configurable distance
- Web GUI - Browser-based radar interface with animated sweep, distance visualization, GPS map, signal-strength device list with pin/unpin, and hover tooltips
- Live TUI - Curses-based live-updating table sorted by signal strength
- Real-Time CSV Log - Stream each detection to a CSV file as it happens
- Batch Export - Export results to CSV, JSON, or JSONL at end of scan (supports stdout with
-o -) - GPS Location Stamping - Tag each detection with GPS coordinates via gpsd; tracks per-device best location (strongest RSSI = closest proximity). On by default, degrades gracefully if gpsd is unavailable
- Multi-Adapter - Scan with multiple Bluetooth adapters simultaneously (Linux)
- Verbose/Quiet Modes - Verbose mode shows additional details (e.g. non-matching RPAs in IRK mode); quiet mode suppresses per-device output and shows only the summary
- Python 3.9+
- Bluetooth hardware
- OS support: macOS, Linux, Windows
- gpsd (optional) — required for GPS location stamping
GPS location stamping requires the gpsd daemon running with a connected GPS receiver. If gpsd is not running, btrpa-scan continues normally without GPS.
| Platform | Install | Start |
|---|---|---|
| macOS | brew install gpsd |
gpsd -n /dev/tty.usbserial-* |
| Debian/Ubuntu | sudo apt install gpsd gpsd-clients |
sudo systemctl start gpsd |
| Fedora/RHEL | sudo dnf install gpsd gpsd-clients |
sudo systemctl start gpsd |
| Arch | sudo pacman -S gpsd |
sudo systemctl start gpsd |
| Windows | Use gpsd via WSL or MSYS2 | See WSL instructions above |
To verify gpsd is working:
# Check that gpsd is listening
gpspipe -w -n 5
# Or use the curses monitor
cgps| Platform | Notes |
|---|---|
| macOS | Uses CoreBluetooth. IRK mode leverages an undocumented API to retrieve real Bluetooth addresses instead of UUIDs. --active has no effect — CoreBluetooth always scans actively. |
| Linux | May require root or CAP_NET_ADMIN capability for scanning. |
| Windows | Native WinRT Bluetooth API — real MAC addresses available natively. TUI requires pip install windows-curses. |
This project uses pyproject.toml (PEP 621), the modern Python packaging standard. It defines the project as an installable package with a registered CLI command — no need to run .py files directly.
uvx btrpa-scan --alluvx --from git+https://github.com/hackingdave/btrpa-scan.git btrpa-scan --alluv tool install btrpa-scanOr directly from GitHub:
uv tool install git+https://github.com/hackingdave/btrpa-scan.gitpip install btrpa-scanFor GUI support (Flask-based radar interface):
pip install btrpa-scan[gui]git clone https://github.com/hackingdave/btrpa-scan.git
cd btrpa-scan
pip install .usage: btrpa-scan [-h] [-a] [--irk HEX] [--irk-file PATH] [-t TIMEOUT]
[--output {csv,json,jsonl}] [-o FILE] [--log FILE]
[-v | -q] [--min-rssi DBM] [--rssi-window N] [--active]
[--environment {free_space,indoor,outdoor}]
[--ref-rssi DBM] [--name-filter PATTERN]
[--alert-within METERS] [--tui] [--gui] [--gui-port PORT]
[--no-gps] [--adapters LIST] [mac]
BLE Scanner — discover all devices or hunt for a specific one
positional arguments:
mac Target MAC address to search for (omit to scan all)
optional arguments:
-h, --help show this help message and exit
-a, --all Scan for all broadcasting devices
--irk HEX Resolve RPAs using this Identity Resolving Key (32 hex chars)
--irk-file PATH Read IRK(s) from a file (one per line, hex format)
-t, --timeout TIMEOUT Scan timeout in seconds (default: 30, or infinite for --irk)
--output {csv,json,jsonl}
Batch output format written at end of scan
-o, --output-file FILE
Output file path (default: btrpa-scan-results.<format>;
use - for stdout)
--log FILE Stream detections to a CSV file in real time
-v, --verbose Verbose mode — show additional details
-q, --quiet Quiet mode — suppress per-device output, show summary only
--min-rssi DBM Minimum RSSI threshold (e.g. -70) — ignore weaker signals
--rssi-window N RSSI sliding window size for averaging (default: 1 = no averaging)
--active Use active scanning (sends SCAN_REQ for additional data)
--environment {free_space,indoor,outdoor}
Distance estimation path-loss model (default: free_space)
--ref-rssi DBM Calibrated RSSI at 1 metre for distance estimation
--name-filter PATTERN Filter devices by name (case-insensitive substring match)
--alert-within METERS Proximity alert when device is within this distance
--tui Live-updating terminal table instead of scrolling output
--gui Launch web-based radar interface in the browser
--gui-port PORT Port for GUI web server (default: 5000)
--no-gps Disable GPS location stamping (GPS is on by default via gpsd)
--adapters LIST Comma-separated Bluetooth adapter names (e.g. hci0,hci1)
Scan for all broadcasting BLE devices (default 30-second timeout):
btrpa-scan --allWith a custom timeout:
btrpa-scan --all -t 60Search for a specific device by MAC address:
btrpa-scan AA:BB:CC:DD:EE:FFResolve Resolvable Private Addresses using an Identity Resolving Key. This mode runs indefinitely by default until stopped with Ctrl+C:
btrpa-scan --irk 0123456789ABCDEF0123456789ABCDEFThe IRK can be provided in several formats:
| Format | Example |
|---|---|
| Plain hex | 0123456789ABCDEF0123456789ABCDEF |
| Colon-separated | 01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF |
| Dash-separated | 01-23-45-67-89-AB-CD-EF-01-23-45-67-89-AB-CD-EF |
| 0x-prefixed | 0x0123456789ABCDEF0123456789ABCDEF |
Load one or more IRKs from a file. Each line should contain one IRK in any supported hex format. Lines starting with # are treated as comments:
btrpa-scan --irk-file keys.txtExample keys.txt:
# Alice's phone
0123456789ABCDEF0123456789ABCDEF
# Bob's watch
FEDCBA9876543210FEDCBA9876543210
When multiple IRKs are loaded, each detected RPA is checked against all keys. The summary shows total matches across all keys.
Set the BTRPA_IRK environment variable to avoid passing the key on the command line:
export BTRPA_IRK=0123456789ABCDEF0123456789ABCDEF
btrpa-scanPriority: --irk > --irk-file > BTRPA_IRK
Only show devices with signal strength above a threshold:
btrpa-scan --all --min-rssi -70BLE RSSI is inherently noisy. Use a sliding window average for more stable distance estimates and to filter out spurious weak detections:
btrpa-scan --all --rssi-window 5When windowing is active, the display shows both raw and averaged RSSI (e.g. RSSI: -65 dBm (avg: -62 dBm over 5 readings)), and distance estimation uses the averaged value. The --min-rssi filter also applies to the averaged RSSI, preventing a single noisy spike from dropping a device.
Filter devices by name using a case-insensitive substring match:
btrpa-scan --all --name-filter "AirPods"Only devices whose advertised name contains the given pattern will be shown. Devices with no name are excluded when a name filter is active.
Passive scanning (the default) only sees advertisements. Active scanning sends SCAN_REQ and gets SCAN_RSP, which can reveal additional service UUIDs and device names:
btrpa-scan --all --activeNote: On macOS, CoreBluetooth always scans actively regardless of this flag. On Linux/BlueZ, active scanning may require root or
CAP_NET_ADMIN.
Distance estimation uses a path-loss exponent that varies by environment. The default (free_space, n=2.0) assumes no obstructions. For more realistic estimates indoors:
btrpa-scan --all --environment indoor| Preset | Path-Loss Exponent (n) | Use Case |
|---|---|---|
free_space |
2.0 | Open air, line of sight |
outdoor |
2.2 | Parks, parking lots |
indoor |
3.0 | Offices, homes, buildings |
Higher n values produce larger distance estimates for the same RSSI, reflecting signal attenuation from walls and obstacles.
By default btrpa-scan derives the expected RSSI at 1 metre from the advertised TX Power using an empirically validated 59 dB offset (the iBeacon standard). For even better accuracy you can supply a calibrated value measured in your own environment:
- Place the target device exactly 1 metre from the scanner.
- Run a short scan and note the average RSSI.
- Pass that value with
--ref-rssi:
btrpa-scan --all --ref-rssi -55When --ref-rssi is set, TX Power is ignored entirely. This also enables distance estimates for devices that don't advertise TX Power.
Trigger an audible bell and visual alert when a device is estimated within a given distance. Requires that the target device advertise TX Power:
btrpa-scan AA:BB:CC:DD:EE:FF --alert-within 5.0Works in all modes including IRK resolution:
btrpa-scan --irk <key> --alert-within 3.0Replace scrolling output with a live-updating terminal table, sorted by signal strength:
btrpa-scan --all --tuiThe TUI shows all detected devices in a compact table with address, name, RSSI, averaged RSSI, estimated distance, detection count, and last-seen time. Resolved IRK matches are shown in bold, and devices within the --alert-within threshold are highlighted.
Combine with other flags:
btrpa-scan --irk <key> --tui --rssi-window 5 --environment indoor --alert-within 5.0Launch a browser-based radar interface with an animated sweep, real-time device tracking, and GPS map:
btrpa-scan --all --guiThe GUI features:
- Radar display — animated sweep line with concentric distance rings (1m, 5m, 10m, 20m). Devices appear as color-coded dots positioned by estimated distance (green = close, yellow = medium, red = far). Full-height layout with matrix data rain background and ghost MAC flicker effects
- Device list — right-side panel listing all detected devices, color-coded by signal strength. Click any device to pin it for tracking
- Pinned devices — left-side panel showing pinned MAC addresses with live RSSI and distance updates. Pin multiple devices simultaneously; click × to unpin
- GPS map — Leaflet.js map with OpenStreetMap tiles showing scanner position and device locations. Automatically hidden if no GPS is available
- Hover tooltips — hover over any device (radar dot, list entry, or pinned entry) to see full device details (address, name, RSSI, TX power, distance, GPS, manufacturer data, services)
- Dark theme — matrix-green hacker aesthetic designed for field work
GUI mode scans continuously by default (no 30-second timeout). Press Ctrl+C to stop. Use -t to set a specific scan duration:
# Scan for 60 seconds with indoor path-loss model
btrpa-scan --all --gui -t 60 --environment indoor
# Custom port
btrpa-scan --all --gui --gui-port 8080
# Combine with RSSI averaging and proximity alerts
btrpa-scan --all --gui --rssi-window 5 --alert-within 5.0Note:
--guirequires Flask and flask-socketio (pip install btrpa-scan[gui]). Cannot be combined with--tuior--quiet.
Stream each detection to a CSV file as it happens (useful for long-running scans where you want incremental data):
btrpa-scan --all --log scan.csvThis can be combined with --output for a separate batch export:
btrpa-scan --all --log live.csv --output json -o results.jsonExport all results at end of scan in CSV, JSON, or JSONL (JSON Lines) format:
btrpa-scan --all --output json -o results.json -t 30
btrpa-scan --all --output csv -t 30
btrpa-scan --all --output jsonl -o results.jsonl -t 30JSONL writes one JSON object per line, making it easy to pipe through jq:
btrpa-scan --all --output jsonl -o results.jsonl -t 10
cat results.jsonl | jq .Write output to stdout for piping:
btrpa-scan --all --output json -o - -t 10 -q | jq .Scan with multiple Bluetooth adapters simultaneously for wider coverage:
btrpa-scan --all --adapters hci0,hci1Each adapter runs its own scanner instance sharing the same detection callback. All detections are merged into a single output.
GPS is on by default. Each detection is tagged with the current GPS coordinates from gpsd. The scanner also tracks the best GPS fix per device — the coordinates from the detection with the strongest RSSI (closest proximity = most accurate location).
If gpsd is not running, the scanner prints a note and continues normally without GPS:
# With gpsd running — detections include lat/lon
btrpa-scan --all --output json
# Without gpsd — works fine, GPS fields are empty
btrpa-scan --all
# Explicitly disable GPS (skips connection attempt)
btrpa-scan --all --no-gpsGPS coordinates appear in:
- Console output — "Best GPS" line per device
- TUI settings bar — current fix coordinates
- GUI — interactive map panel with device markers and scanner position
- Header — GPS connection status
- Summary tables — "Best GPS" column per device
- All exports (CSV, JSON, JSONL, real-time log) —
latitude,longitude,gps_altitudefields
Run in quiet mode (summary only, no per-device output — useful with --output or --log):
btrpa-scan --all -q --output json -t 30Run in verbose mode (show non-matching RPAs in IRK mode):
btrpa-scan --irk 0123456789ABCDEF0123456789ABCDEF -vBluetooth Low Energy devices use Resolvable Private Addresses (RPAs) to prevent tracking. An RPA is a temporary MAC address that changes periodically, but can be resolved by anyone who possesses the device's Identity Resolving Key (IRK).
An RPA consists of:
- prand (3 bytes) - A random value with the top two bits set to
01 - hash (3 bytes) - Computed as
AES-128-ECB(IRK, padding || prand)truncated to 3 bytes
btrpa-scan implements the ah() function from the Bluetooth Core Specification (Vol 3, Part H, Section 2.2.2) to resolve these addresses in real time.
Note on AES-ECB: The use of AES in ECB mode for the
ah()function is mandated by the Bluetooth Core Specification. Because only a single 16-byte block is ever encrypted, ECB's lack of diffusion across blocks is irrelevant — this is not a vulnerability.
- IRK on the command line: Command-line arguments are visible to other users on the system via
ps. To avoid exposing the IRK, use--irk-fileto read from a file, or set theBTRPA_IRKenvironment variable. Console output masks IRKs by default (showing only the first and last 4 hex characters). - GPS coordinates in exports: All export formats (CSV, JSON, JSONL) and console output include GPS coordinates when available. If sharing scan results, be aware that your location may be disclosed. Use
--no-gpsto disable GPS entirely. - Output file paths: The
--output-fileand--logoptions write to the specified path. Ensure the destination has appropriate permissions for your use case.
pip install pytest
python -m pytest test_btrpa_scan.py -vMode: DISCOVER ALL - showing every broadcasting device
Scanning: passive
GPS: connected (37.774929, -122.419418)
Timeout: 30s | Press Ctrl+C to stop
------------------------------------------------------------
============================================================
DEVICE #1 - seen 1x
============================================================
Address : AA:BB:CC:DD:EE:FF
Name : MyDevice
RSSI : -45 dBm
TX Power : -59 dBm
Est. Distance: ~0.4 m
Manufacturer : 0x004C -> 0215abcdef
Best GPS : 37.774929, -122.419418
Timestamp : 14:32:07
============================================================
------------------------------------------------------------
Scan complete - 30.0s elapsed
Total detections : 142
Unique devices : 12
Results written to btrpa-scan-results.json
Press Ctrl+C at any time to gracefully stop the scan and display summary statistics.