Add Sperax P3 Max (wi-linktech WLT6200) protocol support#4
Open
mcdax wants to merge 8 commits into
Open
Conversation
Adds a third protocol backend alongside FTMS and WiLink for the Sperax
P3 Max walking pad (BLE name SPERAX_P3MAX). The device uses a wi-linktech
WLT6200 module with a custom framed protocol on vendor service 0xFFF0 —
not FTMS (0x1826) and not the legacy KingSmith WiLink protocol (0xFE00).
Protocol (fully documented in docs/sperax-p3max-protocol.md):
F5 | LEN | 00 | CMD | ARGS | CRC16 | FA
- byte-stuffing: body bytes 0xF0-0xFF sent as F0 (byte & 0x0F)
- CRC-16 poly 0xA327, init 0xFFFF, reflected, little-endian
- commands: hello 0x01, run 0x15 (speed = km/h x10, incline 0-2),
vibration 0x16 (level 1-4), status poll 0x19
- device only streams status while polled -> keep-alive poll loop
Reverse-engineered from a full HCI snoop capture of a real P3 Max
session; the codec is verified against 146/146 distinct captured frames
and encode() reproduces every captured command frame byte-for-byte.
Changes:
- sperax.py: SperaxController backend + frame codec + status parser
- const.py: Sperax UUIDs, name prefixes, ProtocolType.SPERAX
- controller.py: auto-detection + dispatch; set_incline/set_vibration
- __init__.py: export SperaxController and Sperax constants
- tests/test_sperax_codec.py: hardware-free codec/parser tests
- docs/sperax-p3max-protocol.md: full protocol reference
Speed range (0.5-6.0 km/h) and status counters (distance/duration/steps)
are best-effort pending confirmation on hardware; speed, belt state and
vibration level are verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- P3 Max tops out at 12.0 km/h (confirmed by device owner, hass-walkingpad#3), not the previously-assumed 6.0. The speed byte still follows km/h x10. - Record the vibration level (status offset 18) in TreadmillStatus.vibration_level so consumers can surface it. - Doc: update open items (confirmed max speed; incline/decline capture still needed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ile running - Incline range confirmed 0-10 (flat..max), no decline (full-sweep capture, hass-walkingpad#3). Raise _MAX_INCLINE 2 -> 10. - Expose TreadmillStatus.incline (status offset 16). - set_target_inclination only re-sends the run command while the belt is moving; when stopped it caches the target (applied on next start/set_speed) so adjusting incline can't start a stopped belt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Vibration level (status offset 18) is a "last selected" field the device does not clear on stop; it signals vibration-active via the state byte (offset 4 == 0x50). Report the level only while vibrating, else 0 — fixes the HA value staying stuck at the last level after turning vibration off. - stop() now sends 15 00 00 00 (full stop: belt stops AND session counters reset to 0), matching FTMS stop semantics. pause() sends 15 02 00 00 (belt stops, counters kept) instead of falling back to stop(). Both confirmed by capture in hass-walkingpad#3. - Tests + protocol doc updated for the three run states and the vibration rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The status poll loop tore down on the first failed write, so a single dropped packet over a marginal BT proxy turned into a full disconnect/reconnect cycle. Tolerate up to _MAX_POLL_FAILURES (3) consecutive failures, resetting on any success; only give up if the link is really gone (self.connected goes false). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When stay-connected keeps one controller instance alive, the cached run targets (speed + incline) define what the next start() sends. stop() is a full reset, so it now clears them (speed -> min, incline -> 0): the next start begins flat and slow. pause() leaves them intact, so the next start resumes the previous speed and incline. The incline byte in a run frame drives the bed, so the post-stop start actually levels it. Matches the pause=resume / stop=reset model requested in hass-walkingpad#3. (Connect-per-command / stay-connected-off recreates the controller each connect, so this cache only governs the stay-connected path.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Treat the device as the source of truth. A fresh controller is created on every connect (including reconnect after a dropped link), so instead of starting from min/flat defaults we adopt the device's actual speed and incline from the first status frame after connecting. Fixes a latent bug: after a reconnect the targets reset to defaults, so an incline nudge would re-send speed=min and slow the belt to 0.5. Now the speed target already matches reality, so only the incline changes. An explicit command (set_speed/set_incline/start/stop/pause) marks state as synced, so a status frame arriving just after a user command can't clobber the user's intent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Distance was never mapped, so the HA sensor stayed at 0 (reported in hass-walkingpad#3). Decoded the status counters against a real walk: - offset 8-9 (uint16 LE) = duration in seconds - offset 10-11 (uint16 LE) = distance in 10 m units (metres = value * 10) - offset 14 = steps (unchanged; owner-confirmed) Integrating speed over the session gives ~30 m and offset 10 reads 3 (x10), confirming the unit. Offset 12-13 is a slow calorie-like counter, left unmapped (the app's kcal is computed app-side). Also read duration as the full uint16 (was the low byte only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a third protocol backend (alongside FTMS and WiLink) for the Sperax P3 Max walking pad (BLE name
SPERAX_P3MAX). The device uses a wi-linktech WLT6200 module speaking a custom framed protocol on vendor service0xFFF0— it is not FTMS (0x1826) and not the legacy KingSmith WiLink protocol (0xFE00).Requested in hass-walkingpad#3.
Protocol
Fully documented in
docs/sperax-p3max-protocol.md.0xF0–0xFFare sent asF0 (byte & 0x0F)0xA327, init0xFFFF, reflected, little-endian0x01, run0x15(speed = km/h×10, incline 0–2), vibration0x16(level 1–4), status poll0x19Reverse-engineered from a full HCI snoop capture of a real P3 Max session.
Changes
sperax.py(new) —SperaxControllerbackend + frame codec (crc16/encode/decode) +0x19status parser + poll loopconst.py— Sperax UUIDs,SPERAX_NAME_PREFIXES,ProtocolType.SPERAXcontroller.py— name/service auto-detection + dispatch; newset_incline()/set_vibration()(Sperax-only)__init__.py— exportSperaxControllerand Sperax constantstests/test_sperax_codec.py(new) — hardware-free codec/parser testsdocs/sperax-p3max-protocol.md(new) — full protocol referenceThe unified
WalkingPadControllerAPI (connect/start/set_speed/stop/…) works unchanged, so downstream consumers (e.g. hass-walkingpad) need no changes.Verification
pytest tests/test_sperax_codec.py)decode()validates 146/146 real captured frames;encode()round-trips every captured command frame byte-for-byte (incl. stuffing)Not yet confirmed on hardware
🤖 Generated with Claude Code