Skip to content

Repository files navigation

Radio Presence Scanner (BLE)

License: MIT Platform macOS Platform Linux Platform Raspberry Pi

A small Python scanner that uses Bluetooth Low Energy advertisements to estimate which devices are nearby. It includes a command-line interface, a Python API, and a local web dashboard.

It can:

  • estimate distance from RSSI;
  • classify common phones, audio devices, wearables, and beacons;
  • report devices appearing, leaving, or crossing the configured radius;
  • track named devices and emit present or absent events;
  • show live results in a browser.

Distance and classification are estimates. BLE addresses can rotate, and the radar angle is synthetic. This is a proximity tool, not a people counter.

Install

Python 3.11 or newer is required.

python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e .

On macOS, grant Bluetooth access to the terminal or IDE. Linux and Raspberry Pi need BlueZ and a working Bluetooth adapter.

Run

Start the scanner and dashboard:

radio-presence-scanner --server

Open http://127.0.0.1:8787.

The dashboard has a live radar, filters, device details, exports, and RSSI calibration. It receives updates through Server-Sent Events and falls back to polling when necessary.

The supplied configuration starts the dashboard in phone-only mode. Use all BLE devices for the current run with:

radio-presence-scanner --server --all-devices

Run one scan in the terminal:

radio-presence-scanner

Or scan continuously:

radio-presence-scanner --loop

Useful options:

--config PATH
--phones-only
--all-devices
--scan-seconds 8
--log-limit 50
--server-host 127.0.0.1
--server-port 8787

Configure

Edit config.yaml. The main settings are:

ble_scan_seconds: 4.0
max_missed_scans: 0
radius_m: 2.0
ema_alpha: 0.55
tx_power_default: -59
path_loss_n: 3.0

server:
  phones_only_default: true

radius_m defines the nearby boundary. max_missed_scans controls how many missed scans are tolerated before a device disappears. The RSSI calibration tool in the dashboard can suggest values for tx_power_default, path_loss_n, and ema_alpha.

The full file also contains commented examples for named targets, notifications, storage, and identifier hashing.

Optional features

Named targets match a BLE address or device-name regular expression. Target transitions are available in the Python result and can be sent to callbacks, a webhook, or MQTT.

MQTT is disabled unless notifications.mqtt.host is set. Install its optional dependency with:

python -m pip install -e '.[mqtt]'

SQLite history is also disabled by default. It uses Python's built-in SQLite support and needs no database server:

storage:
  enabled: true
  path: radio-presence-scanner.sqlite3
  retention_hours: 168

When enabled, SQLite stores scan snapshots and target events for history, daily statistics, and export.

Device keys can be hashed before they reach the dashboard or database:

privacy:
  hash_identifiers: true
  identifier_salt: "replace-with-a-private-random-value"

Hashing does not remove device names or make BLE observations anonymous. The HTTP server has no authentication or TLS, so keep it on localhost or place it behind a trusted reverse proxy.

Python API

Each call to sample() performs one BLE scan. The library does not start a background process.

import asyncio
from radio_presence_scanner import PresenceDetector

async def main() -> None:
    detector = PresenceDetector.from_config_file("config.yaml")
    result = await detector.sample()
    print(result.snapshot.nearby_devices_count)
    print(result.snapshot.devices)

asyncio.run(main())

Synchronous code can use sample_sync(). Pass phones_only=True to either method to return the phone-only snapshot.

Target callbacks can be synchronous or asynchronous:

detector.add_event_listener(lambda event: print(event.target_id, event.event))

HTTP API

Server mode exposes:

GET  /api/state
GET  /api/events
GET  /api/health
GET  /api/history
GET  /api/stats/daily
GET  /api/export?format=csv|json
GET  /api/calibration
POST /api/settings
POST /api/calibration/sample
POST /api/calibration/reset
POST /api/history/reset

History and daily statistics are empty when SQLite is disabled. Export still returns the current snapshot.

Raspberry Pi

Install and enable Bluetooth if the adapter is not ready:

sudo apt update
sudo apt install -y bluez bluetooth
sudo systemctl enable --now bluetooth
sudo rfkill unblock bluetooth
sudo bluetoothctl power on

Check it with:

bluetoothctl show
bluetoothctl --timeout 12 scan on

Tests

python -m pip install -e '.[dev]'
ruff check src tests
PYTHONPATH=src python -m unittest discover -s tests -v

License

MIT. See LICENSE.

About

A lightweight tool to estimate nearby presence from radio signals: BLE/Bluetooth (phones, earbuds, wearables)

Resources

Stars

12 stars

Watchers

0 watching

Forks

Contributors

Languages