- HTML 48.1%
- Python 42.5%
- Shell 9.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| screenshots | ||
| site | ||
| .gitignore | ||
| LICENSE | ||
| rapt_ctl.sh | ||
| rapt_pill.py | ||
| README.md | ||
| recorder.log | ||
| recorder.pid | ||
RAPT Pill Monitor
A small, self-hosted Bluetooth recorder and web dashboard for the Kegland RAPT Pill hydrometer. It listens for the Pill's BLE telemetry, writes the data to a CSV file, and serves a static dashboard from a Raspberry Pi or any Linux host with Bluetooth.
No cloud account, mobile app, or internet connection is required after setup. As long as your Pi or other receiving Bluetooth device is in range of the Pill your good to go!
Contents
- Background
- Features
- How it works
- Requirements
- Pill setup
- Installation
- Usage
- Dashboard
- CSV format
- Direct script usage
- Troubleshooting
- Project layout
- Security and limitations
- License
Background
The RAPT Pill is an excellent little ESP32-based hydrometer: charge it, drop it in a fermenter, and it reports gravity and temperature. The official workflow is built around the RAPT mobile app and the RAPT cloud. In practice, I found the Bluetooth side was the reliable way to get data out of the Pill, while the app and cloud logging were inconsistent — especially once the Pill was submerged.
This project takes the simple route:
- Configure the Pill for Bluetooth telemetry.
- Let a nearby Linux host listen for the Pill's BLE broadcasts.
- Log the readings to a CSV file.
- View the data in a local web dashboard.
The Pill is never polled. It wakes on its own telemetry interval, broadcasts a reading, and goes back to sleep. The recorder simply listens.
Features
- Passive BLE listener — no cloud, no app, no account.
- Manual
start/stoprecordings, one session per brew. - 5-minute median aggregation by default, which rejects isolated outlier packets without discarding the rest of the window.
- Raw CSV storage, one file with a
sessioncolumn. - Static dashboard with:
- current SG, temperature and estimated ABV
- editable OG (defaults to the first reading in the session)
- SG change over the selected range, with rate in points/day
- recording length
- SG and temperature graphs
- 24 h / 3 d / 7 d / All range buttons
- Runs as an ordinary user (
brew), not root. - Optional systemd services for Bluetooth prep and the web dashboard.
How it works
RAPT Pill ──BLE advertisements──> rapt_pill.py ──CSV──> site/data/readings.csv
│
▼
rapt-web.service
│
▼
http://<host-ip>/
rapt_pill.pyscans for RAPT manufacturer data and decodes the documented v1/v2 packets.rapt_ctl.shstarts and stops the recorder, manages the session metadata, and flushes the final aggregation window on stop.site/index.htmlis a dependency-free static dashboard. It fetches the CSV andstatus.jsonevery 5 seconds.rapt-web.serviceserves thesite/directory over HTTP.
Requirements
- Linux host with BlueZ and a working Bluetooth adapter.
- Python 3.11+ (tested on Debian 13 / Raspberry Pi with Python 3.13).
bleakinstalled in a virtual environment.- A RAPT Pill configured for Bluetooth telemetry.
- The host must be within BLE range of the Pill. Stainless fermenters attenuate the signal heavily, so place the host close to the fermenter.
- Optional: systemd, a static IP/DHCP reservation for the host.
Tested on:
- Raspberry Pi (Debian 13, aarch64)
- Python 3.13
- Bleak 3.x
Pill setup
The Pill must be configured for Bluetooth telemetry before the recorder can see sensor data.
- Register/update the Pill in the RAPT Brew App over Wi-Fi first, if you have not already done so. Firmware updates and initial registration require Wi-Fi.
- Connect to the Pill in the app over Bluetooth.
- Open Setup and select Telemetry via Bluetooth.
- Leave Smart Mode enabled so you can reconnect later for calibration or settings changes.
- Pick a telemetry interval. A shorter interval gives more samples; the recorder aggregates them into 5-minute medians by default.
Important: A Pill configured for Wi-Fi telemetry does not broadcast sensor data over BLE. It may still be connectable for configuration, but there is no telemetry stream to record.
Installation
The scripts assume the project lives at /home/brew/rapt-pill and runs as a
user called brew. If you use a different path or user, update BASE in
rapt_ctl.sh and the paths in the systemd units below.
1. Create the user and install the files
sudo apt update
sudo apt install -y git python3-venv bluetooth rfkill
# Create the service user (skip if it already exists)
sudo useradd -m -s /bin/bash brew
sudo usermod -aG bluetooth brew
# Clone the repository as brew
sudo install -d -o brew -g brew /home/brew/rapt-pill
sudo -u brew -H git clone <repository-url> /home/brew/rapt-pill
# Make sure everything is owned by brew
sudo chown -R brew:brew /home/brew/rapt-pill
sudo install -d -o brew -g brew /home/brew/rapt-pill/site/data
If you are copying files instead of cloning:
sudo cp -r rapt_pill.py rapt_ctl.sh site /home/brew/rapt-pill/
sudo chown -R brew:brew /home/brew/rapt-pill
2. Create the Python environment
sudo -u brew -H bash -lc '
cd /home/brew/rapt-pill
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install bleak
'
Verify the decoder without needing a Pill:
sudo -u brew -H /home/brew/rapt-pill/.venv/bin/python \
/home/brew/rapt-pill/rapt_pill.py --selftest
3. Find your Pill's BLE address
bluetoothctl scan on
# Look for "RAPT Pill Hydrometer", note its address, then Ctrl-C
bluetoothctl devices
The default address in rapt_ctl.sh is AC:15:18:DF:18:9E (the address of the
Pill used during development). Change it to your Pill's address either by
editing ADDRESS in rapt_ctl.sh or by exporting RAPT_ADDRESS:
export RAPT_ADDRESS="AA:BB:CC:DD:EE:FF"
The Pill's BLE address is usually the Wi-Fi MAC address with the last octet reduced by 2.
4. Enable Bluetooth
On many Raspberry Pi images Bluetooth is rfkill soft-blocked at boot:
sudo rfkill unblock bluetooth
sudo bluetoothctl power on
The optional rapt-bt.service below does this automatically at boot.
5. Install the systemd services
The recorder itself is not a service — it runs only between start and
stop. The web dashboard is a service, and a small oneshot service prepares
Bluetooth at boot.
Create the units:
sudo tee /etc/systemd/system/rapt-bt.service >/dev/null <<'EOF'
[Unit]
Description=Unblock and power on Bluetooth for RAPT Pill
After=bluetooth.service
Wants=bluetooth.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'for d in /sys/class/rfkill/*; do if [ "$(cat "$d/type" 2>/dev/null)" = bluetooth ]; then echo 0 > "$d/soft"; fi; done; exit 0'
ExecStart=-/usr/bin/bluetoothctl power on
[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/systemd/system/rapt-web.service >/dev/null <<'EOF'
[Unit]
Description=RAPT Pill static dashboard
After=network-online.target rapt-bt.service
Wants=network-online.target rapt-bt.service
[Service]
Type=simple
User=brew
Group=brew
WorkingDirectory=/home/brew/rapt-pill/site
ExecStart=/usr/bin/python3 -m http.server 80 --bind 0.0.0.0 --directory /home/brew/rapt-pill/site
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now rapt-bt.service rapt-web.service
If port 80 is already in use, change the port in rapt-web.service (for
example, python3 -m http.server 8080 ...), then browse to
http://<host-ip>:8080/.
6. Open the dashboard
http://<host-ip>/
Usage
# Start a recording. The label is optional.
/home/brew/rapt-pill/rapt_ctl.sh start "ipa-oct-2026"
# Check whether it is running and show the last row
/home/brew/rapt-pill/rapt_ctl.sh status
# Stop. The final partial aggregation window is written to CSV.
/home/brew/rapt-pill/rapt_ctl.sh stop
# Stop and immediately start a new session
/home/brew/rapt-pill/rapt_ctl.sh restart "next-brew"
The recorder is started with nohup, so it keeps running after you log out.
Each start creates a new session; the dashboard lets you switch between
sessions.
Aggregation
By default, each CSV row is the median of all readings over a 5-minute window:
# 10-minute medians
RAPT_AVERAGE_SECONDS=600 /home/brew/rapt-pill/rapt_ctl.sh start "10-min medians"
# Raw readings, one row per Pill broadcast
RAPT_AVERAGE_SECONDS=0 /home/brew/rapt-pill/rapt_ctl.sh start "raw"
# Arithmetic mean instead of median
RAPT_AGGREGATE=mean /home/brew/rapt-pill/rapt_ctl.sh start "means"
Environment variables used by rapt_ctl.sh:
| Variable | Default | Purpose |
|---|---|---|
RAPT_ADDRESS |
AC:15:18:DF:18:9E |
BLE address of the Pill |
RAPT_AVERAGE_SECONDS |
300 |
Aggregation window in seconds; 0 = raw |
RAPT_AGGREGATE |
median |
median or mean |
Dashboard
The dashboard is a single static page served from site/index.html.
- Current reading — latest SG and temperature.
- Estimated ABV —
(OG - current SG) * 131.25. The OG input defaults to the first reading in the selected session and can be edited. Overrides are stored per session in the browser'slocalStorage. - SG change — change between the first and last reading in the selected range, plus duration and rate in gravity points/day.
- Recording length — duration of the whole selected session, independent of the graph range.
- Range buttons — 24 h, 3 d, 7 d, All. Ranges are relative to the last reading in the selected session.
- Graphs — SG and temperature over the selected range. The y-axis auto-scales to the visible data and the chart is downsampled to at most 1,500 points for long sessions.
The dashboard is intentionally dependency-free and works offline. It has no authentication; run it on a trusted LAN only.
CSV format
site/data/readings.csv is appended to, with one header row:
timestamp,address,session,version,mac,gravity,temperature_c,battery_percent,rssi,gravity_velocity,gravity_velocity_valid,accel_x,accel_y,accel_z
timestampis ISO-8601 with timezone.- With aggregation enabled,
timestampis the last sample in the window and the numeric fields are the selected statistic for that window. sessionis the label generated byrapt_ctl.sh start.battery_percentis still logged even though the dashboard currently hides the battery card (some Pills report 0%).
Direct script usage
rapt_ctl.sh is just a convenience wrapper. You can run rapt_pill.py directly:
cd /home/brew/rapt-pill
.venv/bin/python rapt_pill.py --help
# Listen forever and append raw readings to a CSV
.venv/bin/python rapt_pill.py \
--address AA:BB:CC:DD:EE:FF \
--csv readings.csv \
--session my-brew
# JSON lines instead of CSV
.venv/bin/python rapt_pill.py --address AA:BB:CC:DD:EE:FF --json
# Run the offline decoder tests
.venv/bin/python rapt_pill.py --selftest
Useful options: --average-seconds, --average-method, --adapter,
--scan-mode, --dedupe, --verbose and --duration.
Troubleshooting
No data appears
-
Confirm the Pill is set to Bluetooth telemetry, not Wi-Fi telemetry.
-
Confirm the host is within BLE range. Move it closer to the fermenter.
-
Check Bluetooth is powered and unblocked:
bluetoothctl show | grep -E 'Powered|PowerState' hciconfig -a | head -
Confirm the Pill address in
rapt_ctl.sh/RAPT_ADDRESSmatches the device. -
Check the recorder log and status:
/home/brew/rapt-pill/rapt_ctl.sh status tail -n 50 /home/brew/rapt-pill/recorder.log
“No powered Bluetooth adapters found”
sudo rfkill unblock bluetooth
sudo bluetoothctl power on
sudo systemctl restart rapt-bt.service
“Operation already in progress”
Another process is already scanning (often a leftover bluetoothctl scan on).
Stop it and retry:
bluetoothctl scan off
/home/brew/rapt-pill/rapt_ctl.sh restart "brew"
rapt_pill.py retries a few times; if a stale scan persists, stop it manually
with bluetoothctl scan off and restart the recorder.
Dashboard does not update
systemctl status rapt-web.service
journalctl -u rapt-web.service -n 50 --no-pager
The page polls once every 5 seconds. Check that site/data/readings.csv is
growing and that site/data/status.json is being updated.
Dashboard says STOPPED but the recorder is running
Check rapt_ctl.sh status as the brew user. kill -0 on a process owned by
another user fails, so running the status command as a different user can give a
false “STOPPED”.
Battery shows 0%
Some Pills report a battery value of zero even while running. The dashboard hides the battery card for now; the raw value remains in the CSV.
Project layout
.
├── rapt_pill.py # BLE listener, packet decoder and CSV writer
├── rapt_ctl.sh # start/stop/status wrapper
├── site/
│ ├── index.html # static dashboard
│ └── data/ # runtime CSV and status JSON
├── screenshots/
│ └── RAPT_Pill_Fermentation.png
├── README.md
└── LICENSE
Runtime files such as recorder.log, recorder.pid and site/data/* are
generated locally and should not normally be committed.
Security and limitations
- The dashboard has no authentication. Serve it only on a trusted network.
- BLE range is limited, especially through stainless steel fermenters.
- The recorder does not modify the Pill; it only listens.
- This project is not affiliated with KegLand. Use it at your own risk.
- The Pill's BLE packet format is decoded according to the public Kegland
documentation; unknown/newer packet types are ignored (use
--verboseto print them).
License
Released under the GNU General Public License v3.0. See LICENSE.
