diff --git a/README.md b/README.md new file mode 100644 index 0000000..702337b --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# ESP32 "CYD" ThermoPro Bridge + +A proof-of-concept bridge that reads temperature data from a **ThermoPro TP930** (among others) Bluetooth BBQ thermometer and ships it to a self-hosted HTTP API for storage and display. + +## Architecture + +```plain +ThermoPro TP930 (BLE) + │ + ▼ +thermopro_cli.py ←── BLE reader (reverse-engineered protocol, Linux only) + │ + ▼ + bridge_poc.py ←── polls CLI, normalizes payload, POSTs to API + │ + ▼ + receiver.php ←── stores latest reading + NDJSON log, serves status page +``` + +## Components + +### `thermopro_cli.py` + +Local copy of the [thermopro-cli](thermopro-cli/) tool. Communicates directly with the ThermoPro thermometer over BLE using a reverse-engineered protocol. Outputs probe temperatures, battery level, and unit as JSON. + +### `bridge_poc.py` + +Polling bridge that: + +1. Calls `thermopro_cli.py` on a configurable interval +2. Normalizes the raw CLI output into a structured payload +3. POSTs the reading to the configured HTTP endpoint with a shared secret token + +### `receiver.php` + +Minimal PHP API server that: + +- `POST /api/thermopro/readings` — accepts and validates a reading, writes it to `data/latest.json` and appends to `data/readings.ndjson` +- `GET /api/thermopro/latest` — returns the most recent reading as JSON +- `GET /` or `GET /status` — HTML status page (auto-refreshes every 10 seconds) + +## Data Format + +Each reading payload looks like: + +```json +{ + "deviceId": "tp930-smoker", + "device": "ThermoPro TP930", + "mac": "C9:48:1D:B1:E1:E7", + "connected": true, + "battery": 90, + "unit": "F", + "probeCount": 4, + "probes": [ + { "id": 1, "name": "Probe 1", "temperature": 266.2, "connected": true }, + { "id": 2, "name": "Probe 2", "temperature": null, "connected": false }, + { "id": 3, "name": "Probe 3", "temperature": null, "connected": false }, + { "id": 4, "name": "Probe 4", "temperature": null, "connected": false } + ], + "readingTime": "2026-06-06T12:00:00+00:00", + "bridgeTime": "2026-06-06T12:00:01+00:00", + "source": "thermopro_cli" +} +``` + +## Requirements + +- **Bridge host:** Linux with Bluetooth (BlueZ), Python 3.8+, `bleak` library +- **API server:** PHP 8.x with write access to the `data/` directory + +## Setup + +### 1. Install Python dependencies + +```bash +pip install bleak +``` + +### 2. Configure the receiver + +Deploy `receiver.php` to a PHP-capable web server. Set the `THERMOPRO_BRIDGE_TOKEN` environment variable (or it defaults to `dev-secret`): + +```bash +export THERMOPRO_BRIDGE_TOKEN="your-secret-token" +``` + +### 3. Run the bridge + +```bash +python bridge_poc.py \ + --cli thermopro_cli.py \ + --endpoint https://your-server/api/thermopro/readings \ + --token your-secret-token \ + --device-id tp930-smoker \ + --mac C9:48:1D:B1:E1:E7 \ + --interval 15 +``` + +**Options:** + +| Flag | Default | Description | +| --- | --- | --- | +| `--cli` | `thermopro_cli.py` | Path to `thermopro_cli.py` | +| `--endpoint` | `http://127.0.0.1:8000/api/thermopro/readings` | API endpoint | +| `--token` | `dev-secret` | Shared token (`X-Bridge-Token` header) | +| `--device-id` | `tp930-smoker` | Stable identifier for this device | +| `--mac` | `C9:48:1D:B1:E1:E7` | ThermoPro BLE MAC address | +| `--interval` | `15` | Polling interval in seconds | +| `--once` | — | Read and POST once, then exit | + +### Finding your device's MAC address + +```bash +python thermopro_cli.py scan +``` + +## Data Storage + +| File | Contents | +| --- | --- | +| `data/latest.json` | Most recent reading (overwritten each poll) | +| `data/readings.ndjson` | Append-only log of all readings (newline-delimited JSON) | + +## Security + +- The API endpoint is protected by a shared secret sent as the `X-Bridge-Token` request header. +- Use HTTPS for the endpoint in production to keep the token confidential. +- The token is compared with `hash_equals()` to prevent timing attacks.