Read the data off your Kegland Rapt Pill and store locally without the bullshit and headaches that go with using their app.
  • HTML 48.1%
  • Python 42.5%
  • Shell 9.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-23 00:42:52 +11:00
screenshots Update of README and screenshot.. 2026-10-23 00:42:52 +11:00
site Initial commit. 2026-10-22 11:10:44 +11:00
.gitignore README updates for a bit of project context. 2026-10-23 00:29:52 +11:00
LICENSE README updates for a bit of project context. 2026-10-23 00:29:52 +11:00
rapt_ctl.sh Initial commit. 2026-10-22 11:10:44 +11:00
rapt_pill.py Initial commit. 2026-10-22 11:10:44 +11:00
README.md Update of README and screenshot.. 2026-10-23 00:42:52 +11:00
recorder.log Initial commit. 2026-10-22 11:10:44 +11:00
recorder.pid Initial commit. 2026-10-22 11:10:44 +11:00

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!

RAPT Pill Monitor dashboard

Contents

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:

  1. Configure the Pill for Bluetooth telemetry.
  2. Let a nearby Linux host listen for the Pill's BLE broadcasts.
  3. Log the readings to a CSV file.
  4. 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 / stop recordings, 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 session column.
  • 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.py scans for RAPT manufacturer data and decodes the documented v1/v2 packets.
  • rapt_ctl.sh starts and stops the recorder, manages the session metadata, and flushes the final aggregation window on stop.
  • site/index.html is a dependency-free static dashboard. It fetches the CSV and status.json every 5 seconds.
  • rapt-web.service serves the site/ 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).
  • bleak installed 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.

  1. 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.
  2. Connect to the Pill in the app over Bluetooth.
  3. Open Setup and select Telemetry via Bluetooth.
  4. Leave Smart Mode enabled so you can reconnect later for calibration or settings changes.
  5. 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's localStorage.
  • 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
  • timestamp is ISO-8601 with timezone.
  • With aggregation enabled, timestamp is the last sample in the window and the numeric fields are the selected statistic for that window.
  • session is the label generated by rapt_ctl.sh start.
  • battery_percent is 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

  1. Confirm the Pill is set to Bluetooth telemetry, not Wi-Fi telemetry.

  2. Confirm the host is within BLE range. Move it closer to the fermenter.

  3. Check Bluetooth is powered and unblocked:

    bluetoothctl show | grep -E 'Powered|PowerState'
    hciconfig -a | head
    
  4. Confirm the Pill address in rapt_ctl.sh / RAPT_ADDRESS matches the device.

  5. 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 --verbose to print them).

License

Released under the GNU General Public License v3.0. See LICENSE.