A Bluetooth Low Energy attendance system for classrooms. Clicker devices (currently in our prototype: authorized Raspberry Pi Picos) broadcast a signed beacon; a scanner on the host machine verifies it and writes daily check-in records that a local web dashboard reads.
Each Pico is flashed with a student ID and a shared secret salt. On boot it broadcasts a BLE manufacturer advertisement containing the student ID and a 2-byte SHA-256 HMAC derived from student_id + SECRET_SALT. The scanner verifies the hash — if it matches, the student is checked in. The secret never goes over the air.
Pico → [student_id | sha256(id+salt)[:2]] → BLE advert
Scanner → recompute hash → match? → write attendance/YYYY-MM-DD.json
Browser → GET /api/today → dashboard
Python 3.9+
bleak==3.0.1
# macOS only — remove or comment out on Linux:
# pyobjc-core==12.1
# pyobjc-framework-Cocoa==12.1
# pyobjc-framework-CoreBluetooth==12.1
# pyobjc-framework-libdispatch==12.1
Linux only — ensure bluez is installed and running:
sudo apt install bluetooth bluez
sudo systemctl enable --now bluetoothgit clone <repo>
cd <repo>
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtEdit the config block at the top of scanner.py to match your Pico's firmware:
MY_UUID = "797CDF70-..." # must match the Pico
SECRET_SALT = "Networks" # must match the Pico
COMPANY_ID = 0xFFFF# Terminal 1 — BLE scanner (needs Bluetooth access)
python scanner.py
# Terminal 2 — web dashboard
python attendance_server.py
# → http://localhost:8080Both scripts must run from the same directory. The scanner writes to attendance/ and the server reads from it.
.
├── scanner.py # BLE scanner + attendance writer
├── attendance_server.py # HTTP server (stdlib, no extra deps)
├── index.html # Web dashboard (served at /)
├── requirements.txt
└── attendance/
├── 2025-01-20.json
└── 2025-01-21.json
Each attendance file looks like:
{
"date": "2025-01-21",
"checkins": {
"42": { "time": "09:03:11", "rssi": -67 },
"7": { "time": "09:05:44", "rssi": -72 }
}
}| Endpoint | Description |
|---|---|
GET /api/today |
Today's check-ins |
GET /api/date/YYYY-MM-DD |
A specific day's check-ins |
GET /api/dates |
All days that have records |
- The 2-byte hash truncation means 65,536 possible values — sufficient for a closed classroom, not for production.
- There is no replay protection. A sniffed packet can be rebroadcast. Add a rolling counter or timestamp to the payload if this matters.
- Anyone with
scanner.pyhas the secret salt. Treat the source as sensitive.