This project contains tools for connecting to and monitoring Garmin HRM Pro Plus and other Bluetooth heart rate monitors.
The BLE bridge connects to your heart rate monitor via Bluetooth and streams data to a WebSocket endpoint.
- macOS with Bluetooth enabled
- Python 3.8+ with uv virtual environment
- Garmin HRM Pro Plus or compatible BLE heart rate monitor
If you haven't already installed the dependencies in your uv environment:
# Activate your uv virtual environment first
# Then install dependencies if needed:
pip install bleak websockets- Wear the heart rate monitor - It needs skin contact to activate
- Moisten the electrode areas on the strap for better conductivity
- Ensure it's not connected to your phone, watch, or other devices
# Terminal 2: Run the BLE bridge
cd exploratory
# Option 1: Auto-detect HRM devices (looks for "HRM" in device name)
python ble_bridge.py --ws ws://localhost:8000/ws/ingest --name "HRM"
# Option 2: Search for any heart rate device
python ble_bridge.py --ws ws://localhost:8000/ws/ingest
# Option 3: Specify exact device address (if known)
python ble_bridge.py --ws ws://localhost:8000/ws/ingest --address "XX:XX:XX:XX:XX:XX"
# Option 4: With custom device ID for logging
python ble_bridge.py --ws ws://localhost:8000/ws/ingest --name "HRM" --device-id "garmin_hrm_pro"--ws(required): WebSocket endpoint URL (e.g.,ws://localhost:8000/ws/ingest)--name: Substring to search for in device name (e.g., "HRM", "Garmin", "Forerunner")--address: Exact Bluetooth MAC address of the device--device-id: Custom identifier for the device in logged data--token: API token for authentication (also reads fromAPI_TOKENenv var)
If you see "❌ Bluetooth is disabled":
- Open System Settings > Bluetooth
- Turn on Bluetooth
- Run the script again
If you see "❌ No device found" or device list without your HRM:
- Check the strap - Ensure it's worn properly with good skin contact
- Moisten electrodes - Use water or electrode gel on the contact pads
- Check battery - Replace CR2032 battery if device doesn't appear
- Disconnect from other devices - Turn off Bluetooth on paired phones/watches
- Try without --name flag - This will detect any HR device
The script will show all discovered Bluetooth devices to help identify your HRM.
If connection drops frequently:
- Move closer to your Mac (within 10 feet)
- Check for interference from other 2.4GHz devices
- Ensure good skin contact for consistent signal
The BLE bridge sends JSON data to the WebSocket endpoint:
{
"source": "ble_hr",
"device_id": "HRM",
"ts_unix_s": 1694574123.456,
"seq": 0,
"hr_bpm": 72,
"rr_s": [0.833, 0.825],
"energy_j": null,
"battery_pct": 85
}hr_bpm: Heart rate in beats per minuterr_s: RR intervals in seconds (for HRV calculation)battery_pct: Device battery percentageseq: Sequence number for ordering
exploratory/ble_bridge.py- Main BLE to WebSocket bridgeexploratory/test_ws_server.py- Test WebSocket server for developmentexploratory/intervals.py- Intervals.icu integration (if needed)exploratory/proplus.py- Additional HRM Pro Plus utilities
This project uses Supabase Functions to ingest data from external services like Google Calendar and Gmail. These functions are written in TypeScript and run on Deno.
The Supabase functions are located in the supabase/functions directory:
ingest-gcal/: Ingests events from a specified Google Calendar.ingest-gmail/: Ingests email metadata from a Gmail account._shared/: Contains shared code, such as Google OAuth token refresh logic.
- Authentication: The functions use a long-lived Google OAuth refresh token to generate new access tokens for each run.
- Data Fetching: They connect to the Google Calendar and Gmail APIs to fetch recent events and emails.
- Data Storage: The fetched data is then transformed and
upsertedinto aneventstable in your Supabase database. Each event is linked to asource(e.g.,gcal,gmail).
To run these functions, you must set the following environment variables in your Supabase project (or in a local .env file for local development):
SUPABASE_URL: Your project's Supabase URL.SERVICE_ROLE_KEY: Your project's service role key for admin-level access.GOOGLE_CLIENT_ID: Your Google Cloud project's client ID.GOOGLE_CLIENT_SECRET: Your Google Cloud project's client secret.GOOGLE_REFRESH_TOKEN: A valid Google OAuth refresh token with access to the required scopes (Gmail and/or Calendar).
The functions expect the following tables to exist:
sources: A table to define the data sources. It should have at leastsource_id(UUID),kind(text, e.g., 'gcal', 'gmail'), andlast_token(text, for sync tokens).events: The main table for storing ingested data. Key columns include:kind: The type of event (e.g., 'email', 'calendar_event').ts_range: Atstzrangerepresenting the event's time.source_id: A foreign key to thesourcestable.source_ref: The original ID from the source system (e.g., Gmail message ID).details: A JSONB column for storing raw metadata.
To ensure upserts work correctly, you need a unique constraint on the events table:
ALTER TABLE events
ADD CONSTRAINT events_source_id_source_ref_key UNIQUE (source_id, source_ref);To deploy a function, use the Supabase CLI:
# Deploy the Gmail ingestion function
supabase functions deploy ingest-gmail
# Deploy the Google Calendar ingestion function
supabase functions deploy ingest-gcalThis project is for personal/educational use.