Skip to content

feat: Peloton Tread support (FTMS Treadmill Data 0x2ACD) - #70

Open
aaearon wants to merge 1 commit into
doudar:developfrom
aaearon:feature/tread-upstream
Open

feat: Peloton Tread support (FTMS Treadmill Data 0x2ACD)#70
aaearon wants to merge 1 commit into
doudar:developfrom
aaearon:feature/tread-upstream

Conversation

@aaearon

@aaearon aaearon commented Aug 20, 2026

Copy link
Copy Markdown

This adds Tread support alongside the existing Bike support. On a Tread, grupetto reads live speed and incline from the tablet's ITreadInterface and broadcasts them as FTMS Treadmill Data (0x2ACD); the overlay HUD switches to speed/incline. Nothing about the Bike path changes.

Detecting a Tread turned out to be the tricky part. Build.MODEL doesn't work — PLTN-TTR01 is the tablet, and the Bike+, Tread and Row all use the same one (it's literally Peloton's own isTopaz() check). So detection reads Settings.Global["peloton_platform"] instead, which is "prism" on a Tread. Anything unknown or unreadable falls back to the Bike path, and the detected values get logged at startup.

Since the Tread is motorized, I kept the binder read-only: it only sends allowlisted read/lifecycle transactions through one guarded wrapper — no motion or write codes anywhere — registers under grupetto's own package and unregisters exactly once, including on error and cancellation, so it can't trip Peloton's pin-lock watchdog. The bind is cancellation-aware too.

This is based on reverse-engineering Peloton's affernetservice on real hardware, the same way the existing BikeData.java reads the Bike.

49 unit tests cover the parsing, scaling, sensor selection, the 0x2ACD layout and bind cancellation. I tested it on my own Tread (confirmed 0x2ACD in nRF Connect) and my Bike v1 to check I didn't regress it. I don't have a Bike+ or Row to test, so their detection is unverified — but the platform check plus the Bike-path fallback are meant to fail safe there.

No dependency, Gradle, or CI changes.

Reads live speed/incline from the tablet's ITreadInterface and broadcasts
FTMS Treadmill Data (0x2ACD); the overlay HUD switches to speed/incline. The
Bike path is unchanged.

Detection reads Settings.Global["peloton_platform"] ("prism" = Tread), not
Build.MODEL: PLTN-TTR01 is the tablet, shared by Bike+/Tread/Row (Peloton's
own isTopaz() check). Unknown/unreadable platform falls back to the Bike path.

The Tread is motorized, so the binder is read-only: allowlisted read/lifecycle
transactions through one guarded wrapper (no motion/write codes), registered
under grupetto's own package and unregistered exactly once including on error
and cancellation, to avoid Peloton's pin-lock watchdog. Bind is cancellation-aware.

Based on reverse-engineering affernetservice on real hardware, the same way
BikeData.java reads the Bike. 49 unit tests; verified on a real Tread
(0x2ACD confirmed via nRF Connect) and a Bike Gen 1 regression pass. Bike+/Row
detection is unverified (no hardware); platform check + Bike-path fallback fail safe.
@doudar

doudar commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Amazing! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants