Back in the early 2010s, when Bluetooth meant classic and Python 2 was still the norm, I built a little experiment: a local Bluetooth scanner that picked up nearby devices and sent their data to a web server.
The idea was simple: like a digital radar for your surroundings. Each time the scanner found a phone, speaker, or headset, it would POST that device’s address and name to a web endpoint. The server would respond:
1: already known2: new device
That response decided whether to log it, greet the user, or just nod silently in the background.
The original project was barely over a hundred lines and relied solely on PyBluez. Simple, a bit clunky, but it worked. It even printed each new device in the terminal, a small victory in the early days of DIY IoT.
Fast-Forward 12 Years
Now, over a decade later, I decided to bring it back, this time in modern Python, with full BLE support, a web dashboard, and proper persistence.
This rewrite isn’t just a touch-up. It’s a complete re-architecture designed to feel like a small but capable IoT edge service, the kind of thing that could live quietly on a Raspberry Pi, monitoring its environment and reporting back when something new enters range.
What’s New
🌀 Dual Scanning
The scanner now uses Bleak for Bluetooth Low Energy and PyBluez for classic devices, both running in parallel. Each cycle merges the results into a unified stream with deduplication and metadata tagging.
🌐 Live Web UI
Instead of console text, there’s now a small Flask + SSE dashboard at http://localhost:8080.
You get a sortable device table, live status indicators, and a purge button to clear results. It’s surprisingly satisfying watching new devices pop into view in real time.
💾 Persistent Device Store
Each discovery is logged in a local SQLite database, meaning your data survives restarts and can be analyzed later.
📡 Smart Reporting
Point the app to any webhook via REPORT_URL, and it will send device batches with retry logic, deduplication, and back-off timing.
No webhook? No problem! The queue simply idles until you’re ready to connect it to something.
📊 Eddystone Support
If a BLE beacon advertises a URL frame, the app automatically decodes it, letting you see what local beacons around you are broadcasting.
🔧 Cleaner Management
Everything, adapters, queues, and configurations, is viewable right from the UI. You can even see when Bluetooth is disabled or when an adapter is missing.
Under the Hood
The project is now organized like a full Python service:
src/
└── combined_scanner.py # Unified BLE + Classic scanner
templates/
└── index.html # Flask web interface
tests/
├── test_db_and_helpers.py
├── test_endpoints.py
└── test_reporting.py
run_scanner.py # Cross-platform launcher
run_scanner.bat / .sh # Startup scripts
With linting, pre-commit hooks, and automated testing, it’s a proper little modern app, not the hacked-together script of old.
Technical Notes
Dual-Mode Scanning:
The core loop spawns two asynchronous coroutines:
bleak.BleakScanner.discover()handles BLE devices.bluetooth.discover_devices()(from PyBluez) handles classic ones.
Results are merged into a thread-safe queue, normalized into a unified format, and written to the SQLite store.
Device Persistence:
Each device entry contains its MAC address, name, RSSI (for BLE), and timestamps. If the same address reappears, it’s updated rather than duplicated.
Web Interface:
Flask serves the HTML dashboard, and Server-Sent Events (SSE) stream live updates to the browser — no refreshes required.
A small REST API powers the “Purge” and “Config” buttons.
Reporting Logic:
When REPORT_URL is defined, the scanner batches up new discoveries and posts them as JSON. Each batch includes the local sensor ID (THIS_SENSOR) so multiple scanners can feed the same backend.
Retries and exponential delays make the system resilient against temporary network drops.
Eddystone Decoding:
BLE packets that start with the Eddystone prefix are parsed for their embedded URLs. It’s a neat way to find tiny “broadcast web pages” from nearby beacons.
Testing & CI:
Everything is covered by a pytest suite and validated through GitHub Actions on multiple OS environments.
Linting, formatting, and import sorting are handled automatically via pre-commit.
Looking Back
There’s something satisfying about modernizing old code, seeing the evolution not just of Python and Bluetooth APIs, but of your own coding philosophy.
In 2013, this was a curious little script running on a laptop.
In 2025, it’s a self-contained IoT service with proper architecture, tests, and live reporting, and it’s still doing the same thing: quietly listening to the invisible world around it.
Sometimes, that’s what progress looks like. The same idea, refined and rebuilt with a decade of experience behind it.
