{"id":51576964,"url":"https://github.com/korniychuk/treadmill-bluetooth-macos","last_synced_at":"2026-07-11T01:02:25.069Z","repository":{"id":369694508,"uuid":"1289322171","full_name":"korniychuk/treadmill-bluetooth-macos","owner":"korniychuk","description":"🏃 macOS BLE daemon for a Yesoul treadmill — live stats, step goals \u0026 speed control (Rust)","archived":false,"fork":false,"pushed_at":"2026-07-06T12:37:40.000Z","size":1507,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-06T14:12:47.223Z","etag":null,"topics":["ble","bluetooth","bluetooth-low-energy","corebluetooth","fitness","ftms","rust","treadmill","yesoul"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/korniychuk.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-04T15:47:28.000Z","updated_at":"2026-07-06T12:37:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/korniychuk/treadmill-bluetooth-macos","commit_stats":null,"previous_names":["korniychuk/treadmill-bluetooth-macos"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/korniychuk/treadmill-bluetooth-macos","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korniychuk%2Ftreadmill-bluetooth-macos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korniychuk%2Ftreadmill-bluetooth-macos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korniychuk%2Ftreadmill-bluetooth-macos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korniychuk%2Ftreadmill-bluetooth-macos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/korniychuk","download_url":"https://codeload.github.com/korniychuk/treadmill-bluetooth-macos/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/korniychuk%2Ftreadmill-bluetooth-macos/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35347554,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ble","bluetooth","bluetooth-low-energy","corebluetooth","fitness","ftms","rust","treadmill","yesoul"],"created_at":"2026-07-11T01:02:24.094Z","updated_at":"2026-07-11T01:02:25.057Z","avatar_url":"https://github.com/korniychuk.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🏃 treadmill-bluetooth-macos\n\n[![CI](https://github.com/korniychuk/treadmill-bluetooth-macos/actions/workflows/ci.yml/badge.svg)](https://github.com/korniychuk/treadmill-bluetooth-macos/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n[![Platform: macOS](https://img.shields.io/badge/platform-macOS-black.svg)](#-limitations)\n[![Made with Rust](https://img.shields.io/badge/Rust-2024-orange.svg)](https://www.rust-lang.org)\n\nA **macOS** Bluetooth Low Energy connector for a **Yesoul** treadmill, written in **Rust** 🦀.\n\nIt discovers the treadmill over BLE (CoreBluetooth), connects, and streams live\ntelemetry — speed, distance, steps — over the standard **Fitness Machine Service**\n(FTMS) GATT profile. A background daemon keeps the link alive, detects whether you\nare actually walking (presence), splits your day into workouts, tracks step goals,\nand can drive the treadmill (start / stop / target speed). 🏃💨\n\n\u003e ⚠️ **Unofficial.** Not affiliated with Yesoul. The BLE protocol was\n\u003e reverse-engineered against a single unit (Yesoul W2 Pro). It speaks generic\n\u003e FTMS, so other FTMS treadmills *may* work, but only the W2 Pro is verified.\n\u003e See [Limitations](#-limitations).\n\n---\n\n## ✨ Features\n\n- 🔎 **Scan \u0026 connect** to the first FTMS treadmill nearby.\n- 📈 **Live telemetry** — speed, distance, steps — streamed and logged.\n- 🧠 **Presence detection** — belt moving but steps not rising ⇒ \"away while\n  running\"; workouts are split on real activity, not wall-clock.\n- 📊 **Daily stats** — steps / distance / walking-time, restart-safe.\n- ❤️ **Heart rate** — pair a chest-strap BLE sensor (e.g. Polar H10) and get a\n  compact `♥ avg/max` summary alongside your steps, plus a live bpm in the\n  widget and a sensor-battery check (`tm status`, low-battery glyph in the\n  widget). Optional — everything degrades silently when no sensor is worn.\n- 🎯 **Step-goal milestones** — up to 3 daily goals with native macOS toasts.\n- 🎛️ **Control** — start / stop / set target speed on a live link.\n- 🖥️ **tmux status-bar widget** — see the current workout in your status line\n  (see [`scripts/tmux/`](./scripts/tmux)).\n- 🛟 **Self-healing daemon** — auto reconnect, watchdog, pause/resume speed\n  restore, AC-power awareness (won't drain the battery when idle).\n\n## 📸 Demo\n\n**Live workout in your tmux status bar** (walking = green, paused = yellow):\n\n![tmux status-bar widget — walking](./docs/screenshots/tmux-widget-walking.png)\n\n**Native macOS toasts** for milestones, presence and connection state:\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg width=\"400\" src=\"./docs/screenshots/toast-milestone.png\" alt=\"12,000 steps milestone toast\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg width=\"400\" src=\"./docs/screenshots/toast-goal.png\" alt=\"daily step goal reached toast\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg width=\"400\" src=\"./docs/screenshots/toast-speed-restore.png\" alt=\"pause / resume speed restore toast\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg width=\"400\" src=\"./docs/screenshots/toast-connection-lost.png\" alt=\"connection lost toast\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg width=\"400\" src=\"./docs/screenshots/toast-away.png\" alt=\"away-while-running toast\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg width=\"400\" src=\"./docs/screenshots/toast-back.png\" alt=\"steps counting again toast\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n**CLI** — control the belt, then read stats \u0026 status:\n\n![tm start / stop / stats / status](./docs/screenshots/cli-overview.png)\n\n## 📋 Requirements\n\n- 🍎 **macOS** (Apple Silicon or Intel). No Linux / Windows — see [Limitations](#-limitations).\n- 📡 Bluetooth, and a treadmill exposing FTMS (`0x1826`).\n- 🦀 To build from source: **Rust 1.95+** (edition 2024). `rustup` recommended.\n- 🎨 To regenerate the app icon only: Xcode Command Line Tools (`swift`).\n\n## 🚀 Quickstart (from source)\n\n```bash\ngit clone https://github.com/korniychuk/treadmill-bluetooth-macos.git\ncd treadmill-bluetooth-macos\n\ncargo run                 # scan: list nearby BLE devices (diagnostic)\ncargo run -- connect      # connect to the first FTMS treadmill and stream data\n```\n\nOn first run macOS asks for **Bluetooth permission** — grant it, otherwise the\nscan returns nothing. (The permission is attributed to a stable app identity via\nan embedded `Info.plist`; see [`docs/tasks/002`](./docs/tasks/002-macos-bluetooth-permission.md).)\n\nVerbose logs:\n\n```bash\nRUST_LOG=debug cargo run -- connect\n```\n\n## 🎛️ Install the background daemon\n\nThe daemon auto-connects, tracks presence and stats, shows toasts, and owns the\nBLE link so CLI control commands can be routed to it.\n\n```bash\nscripts/install-daemon.sh      # build, code-sign, install LaunchAgent, symlink `tm`\nscripts/uninstall-daemon.sh    # remove the LaunchAgent (keeps your data)\n```\n\n`install-daemon.sh` builds a release binary, code-signs it, registers a\nnotification identity, writes a **LaunchAgent** (`~/Library/LaunchAgents/…`,\nauto-starts at login), and symlinks a short `tm` alias into `~/.bin` so you can\nrun `tm stats` / `tm status` from anywhere. Add `~/.bin` to your `PATH` if it\nisn't already.\n\n\u003e 🔁 **Re-run `install-daemon.sh` after every rebuild** — it re-signs and reloads\n\u003e the LaunchAgent. A bare `cargo build` leaves the daemon pointing at a stale or\n\u003e differently-signed binary.\n\n### 🕹️ Commands\n\n```bash\ntm                    # = scan: list nearby BLE devices\ntm connect            # connect and stream (foreground)\ntm daemon             # run the background loop (normally launched by launchd)\ntm stats              # today's stats;  tm stats --all  → every day\ntm status             # daemon / connection / presence snapshot\ntm widget             # compact TSV of the current workout (for status bars)\ntm hr                 # diagnostic: connect to a heart-rate sensor, print battery + live bpm\ntm speed \u003ckmh\u003e        # set target speed on the live link\ntm start | tm stop    # start / stop the belt\ntm recompute-segments # rebuild workout segments from raw samples (no BLE)\ntm default-speed      # show the computed default start speed (no BLE)\ntm --help             # full command list\n```\n\n## ⬇️ Install a prebuilt binary (no Rust needed)\n\nEach tagged release ships an **unsigned, ad-hoc** macOS binary (Apple Silicon)\nas a `.tar.gz` on the\n[Releases](https://github.com/korniychuk/treadmill-bluetooth-macos/releases) page.\nOne-liner — grab the latest and install it as a daemon:\n\n```bash\ncurl -fsSL https://github.com/korniychuk/treadmill-bluetooth-macos/releases/latest/download/treadmill-bluetooth-macos-macos-arm64.tar.gz | tar -xz\ncd treadmill-bluetooth-macos-macos-arm64\n./scripts/install-prebuilt.sh\n```\n\n`install-prebuilt.sh` strips the quarantine attribute, ad-hoc-signs the binary,\ninstalls it to a stable location, registers the notification identity, and loads\nthe LaunchAgent — **no cargo, no toolchain**. To sign with your own certificate\nfor a rebuild-stable Bluetooth grant, pass `IDENTITY=\"\u003ccert name\u003e\"`.\n\n\u003e 🍏 **Apple Silicon only** for prebuilt binaries (CI builds `arm64`). On Intel,\n\u003e build from source (see [Quickstart](#-quickstart-from-source)).\n\nPrefer to just run it by hand instead of installing the daemon:\n\n```bash\nxattr -d com.apple.quarantine ./treadmill-bluetooth-macos\n./treadmill-bluetooth-macos connect\n```\n\n## ⚙️ Configuration\n\nPer-user config (TOML) lives **outside this repo**, at:\n\n```\n~/.config/treadmill-bluetooth-macos/config.toml\n```\n\nCopy [`config/config.example.toml`](./config/config.example.toml) there and edit it\n(every key is optional; the example documents each default as a commented line):\n\n```toml\ngoals = [8000, 10000, 12000]\n# workout_gap_minutes = 15\n# auto_pause_minutes = 5\n```\n\n- 🎯 `goals` — up to 3 thresholds; each is celebrated once per day with a toast.\n- ⏱️ `workout_gap_minutes` (optional, default 15) — segments closer than this merge\n  into one displayed workout. Applied **at read time**, so changing it is\n  retroactive; no recompute needed.\n- ⏸️ `auto_pause_minutes` (optional, default 5, `0` = off) — how long the belt may\n  keep running while nobody is walking (you stepped off) before the daemon pauses\n  it; the machine's own shutoff then powers it down.\n\nEdits are **hot-reloaded** by the daemon within ~5s while it is connected to the\ntreadmill (see [`docs/tasks/017`](./docs/tasks/017-hot-reload-goals-config.md)).\nA missing file is fine (built-in defaults `[8000, 10000, 12000]`); a malformed\nfile logs a WARN and falls back to defaults. Override the path with the\n`TREADMILL_CONFIG` env var. `tm status` shows the config the daemon currently\nhas loaded and when it last read it.\n\n## 🖥️ tmux status-bar widget\n\n`tm widget` prints a compact, tab-separated line for a status bar (empty output\nwhen the treadmill is off, so the segment hides). A reference renderer for\n**Dracula** (and a plain-tmux variant) lives in\n[`scripts/tmux/`](./scripts/tmux) — see its README for the install recipe and\nthe exact output contract.\n\n![tmux widget — paused state](./docs/screenshots/tmux-widget-paused.png)\n\n## ⚠️ Limitations\n\n- 🍎 **macOS only.** The permission flow, notifications, code-signing, LaunchAgent\n  and IOKit/CoreFoundation glue are all macOS-specific. No Linux / Windows.\n- 🧪 **Verified on one device** — Yesoul W2 Pro (`FW SDC_W2_BT_V3.03-50-54`). Written\n  as generic FTMS, so other FTMS treadmills may work, but are untested.\n- ⛰️ **No incline.** The W2 Pro does not expose inclination over FTMS\n  (`SetTargetInclination` → *Operation Failed*, no `0x2AD5`). Incline is remote-only.\n- 🎚️ **Partial control.** Start / stop and target-speed are implemented and\n  hardware-verified. Incline is not. LED backlight control is\n  [backlog](./docs/backlog/004-led-control-via-hci-capture.md), not started.\n- 🔕 **Not in the macOS Bluetooth menu — by design.** The treadmill is app-managed\n  BLE, without OS-level pairing/bonding (avoids a race for the single BLE central\n  with the phone app). See [ADR 0001](./docs/adr/0001-no-macos-bluetooth-device-list.md).\n  Status is via `tm status` / the widget, not System Settings.\n- 🔏 **Code signing.** Without your own signing identity, macOS re-prompts for\n  Bluetooth on every rebuild (ad-hoc signatures change the cdhash). Release\n  binaries are **not notarized** — Gatekeeper will warn unless you strip\n  quarantine or build locally.\n\n## 🧑‍💻 Development\n\n```bash\ncargo test     # unit tests\ncargo clippy   # lints\ncargo fmt      # format\n```\n\nCI (GitHub Actions, `macos-latest`) runs fmt / clippy / build / test on every\npush and PR. See [`CONTRIBUTING.md`](./CONTRIBUTING.md).\n\nArchitecture and protocol notes live in [`CLAUDE.md`](./CLAUDE.md); research,\ndecisions (ADRs) and the task journal live in [`docs/`](./docs).\n\n## 📄 License\n\nMIT — see [`LICENSE`](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkorniychuk%2Ftreadmill-bluetooth-macos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkorniychuk%2Ftreadmill-bluetooth-macos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkorniychuk%2Ftreadmill-bluetooth-macos/lists"}