walkingpad-controller 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/PKG-INFO +1 -1
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/pyproject.toml +1 -1
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/const.py +8 -3
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/controller.py +10 -1
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/ftms.py +45 -8
- walkingpad_controller-0.4.1/docs/issue-1-mc21-investigation.md +0 -150
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/.github/workflows/publish.yml +0 -0
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/.gitignore +0 -0
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/LICENSE +0 -0
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/README.md +0 -0
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/__init__.py +0 -0
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/models.py +0 -0
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/wilink.py +0 -0
- {walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/tests/test_real_device.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: walkingpad-controller
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Python library for controlling KingSmith WalkingPad treadmills over BLE (FTMS and legacy WiLink protocols)
|
|
5
5
|
Project-URL: Homepage, https://github.com/mcdax/walkingpad-controller
|
|
6
6
|
Project-URL: Repository, https://github.com/mcdax/walkingpad-controller
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "walkingpad-controller"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Python library for controlling KingSmith WalkingPad treadmills over BLE (FTMS and legacy WiLink protocols)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
{walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/const.py
RENAMED
|
@@ -132,6 +132,11 @@ class TreadmillDataFlags:
|
|
|
132
132
|
# These devices have service 0x1826 but NOT 0xFE00.
|
|
133
133
|
FTMS_NAME_PREFIXES = ("KS-HD-",)
|
|
134
134
|
|
|
135
|
-
# Default connection parameters
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
# Default connection parameters.
|
|
136
|
+
# KingSmith FTMS firmware can be left in a bad state for several seconds
|
|
137
|
+
# after a previous abrupt disconnect — Bleak/BlueZ then accepts the next
|
|
138
|
+
# connect() call but the device closes the link before service discovery
|
|
139
|
+
# completes. A handful of retries with a few seconds between them rides
|
|
140
|
+
# this out reliably.
|
|
141
|
+
MAX_CONNECT_RETRIES = 5
|
|
142
|
+
RETRY_DELAY_SECONDS = 3.0
|
{walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/controller.py
RENAMED
|
@@ -102,7 +102,16 @@ class WalkingPadController:
|
|
|
102
102
|
|
|
103
103
|
@property
|
|
104
104
|
def connected(self) -> bool:
|
|
105
|
-
"""Whether the device is currently connected.
|
|
105
|
+
"""Whether the device is currently connected.
|
|
106
|
+
|
|
107
|
+
Defers to the active backend so the result reflects the live BLE
|
|
108
|
+
state, not just a cached bool that can drift if the firmware
|
|
109
|
+
unilaterally drops the link before the disconnect callback fires.
|
|
110
|
+
"""
|
|
111
|
+
if self._ftms is not None:
|
|
112
|
+
return self._ftms.connected
|
|
113
|
+
if self._wilink is not None:
|
|
114
|
+
return self._wilink.connected
|
|
106
115
|
return self._connected
|
|
107
116
|
|
|
108
117
|
@property
|
{walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/ftms.py
RENAMED
|
@@ -128,6 +128,12 @@ class FTMSController:
|
|
|
128
128
|
|
|
129
129
|
Args:
|
|
130
130
|
ble_device: The BLE device to connect to.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
BleakError: If the underlying BLE link drops before setup
|
|
134
|
+
finishes (e.g. shortly after a previous disconnect, the
|
|
135
|
+
firmware sometimes accepts the connection and then closes
|
|
136
|
+
it again before service discovery completes).
|
|
131
137
|
"""
|
|
132
138
|
_LOGGER.info("FTMS: Connecting to %s", ble_device.address)
|
|
133
139
|
|
|
@@ -151,6 +157,18 @@ class FTMSController:
|
|
|
151
157
|
# Request control
|
|
152
158
|
await self._request_control()
|
|
153
159
|
|
|
160
|
+
# Sanity check: if the link dropped at any point during setup
|
|
161
|
+
# (Bleak's is_connected goes False, our _connected bit is flipped
|
|
162
|
+
# by the disconnect callback), surface that as a failed connect
|
|
163
|
+
# rather than silently claiming success — otherwise callers see
|
|
164
|
+
# `connected == False` immediately after `connect()` "succeeds"
|
|
165
|
+
# and have no clean signal that they should retry.
|
|
166
|
+
if not self.connected:
|
|
167
|
+
raise BleakError(
|
|
168
|
+
"FTMS: BLE link dropped during connection setup; treating "
|
|
169
|
+
"as a failed connect."
|
|
170
|
+
)
|
|
171
|
+
|
|
154
172
|
async def disconnect(self) -> None:
|
|
155
173
|
"""Disconnect from the device."""
|
|
156
174
|
if self._client and self._client.is_connected:
|
|
@@ -543,17 +561,18 @@ class FTMSController:
|
|
|
543
561
|
if not self._has_control:
|
|
544
562
|
await self._request_control()
|
|
545
563
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
564
|
+
if not self.connected:
|
|
565
|
+
_LOGGER.warning("FTMS: Not connected; cannot start")
|
|
566
|
+
return False
|
|
567
|
+
|
|
568
|
+
accepted = await self._write_control_point(FTMSOpcode.START_OR_RESUME)
|
|
551
569
|
|
|
552
570
|
if not self.connected:
|
|
553
|
-
_LOGGER.warning("FTMS: Connection lost
|
|
571
|
+
_LOGGER.warning("FTMS: Connection lost during START_OR_RESUME")
|
|
554
572
|
return False
|
|
555
573
|
|
|
556
|
-
if
|
|
574
|
+
if accepted:
|
|
575
|
+
_LOGGER.info("FTMS: START_OR_RESUME accepted (cold start)")
|
|
557
576
|
belt_running = await self._wait_for_belt_moving(timeout=15.0)
|
|
558
577
|
if not belt_running:
|
|
559
578
|
if not self.connected:
|
|
@@ -565,8 +584,26 @@ class FTMSController:
|
|
|
565
584
|
"FTMS: Cold start complete — belt running at %.1f km/h",
|
|
566
585
|
self._status.speed,
|
|
567
586
|
)
|
|
587
|
+
return True
|
|
588
|
+
|
|
589
|
+
# The device rejected START_OR_RESUME (non-success indication).
|
|
590
|
+
# That can mean either (a) belt is already running, or (b) the
|
|
591
|
+
# device is in a state that doesn't accept start right now —
|
|
592
|
+
# e.g. just transitioned through stop and isn't fully settled.
|
|
593
|
+
# Disambiguate by looking at the live speed.
|
|
594
|
+
if self._status.speed > 0:
|
|
595
|
+
_LOGGER.info(
|
|
596
|
+
"FTMS: START_OR_RESUME rejected but belt is running at "
|
|
597
|
+
"%.1f km/h — treating as success",
|
|
598
|
+
self._status.speed,
|
|
599
|
+
)
|
|
600
|
+
return True
|
|
568
601
|
|
|
569
|
-
|
|
602
|
+
_LOGGER.warning(
|
|
603
|
+
"FTMS: START_OR_RESUME rejected and belt is not moving "
|
|
604
|
+
"(device may need a moment after stop)"
|
|
605
|
+
)
|
|
606
|
+
return False
|
|
570
607
|
|
|
571
608
|
async def _wait_for_belt_moving(self, timeout: float = 15.0) -> bool:
|
|
572
609
|
"""Wait for the belt to report speed > 0 after a cold start.
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# Issue #1 — KingSmith MC-21 speed control investigation
|
|
2
|
-
|
|
3
|
-
Background: https://github.com/mcdax/walkingpad-controller/issues/1
|
|
4
|
-
|
|
5
|
-
User report (`@flyzet-prog`): on a `KS-MC21-D06BFD` treadmill, start/stop work via the HA integration, but the speed slider has no effect. The HA slider only ever reflects what the physical remote sets. Reverse engineering with `nRF Connect` showed BLE writes are rejected unless KS Fit is opened first.
|
|
6
|
-
|
|
7
|
-
## TL;DR
|
|
8
|
-
|
|
9
|
-
Speed control over BLE is **not possible on the MC-21**. KS Fit cannot do it either — the `setSpeed` function in KS Fit's BLE module exists only in the legacy WiLink (`0xFE00`) protocol path, and the MC-21 doesn't expose that service. Start/stop is the only thing that works on the MC-21 over BLE.
|
|
10
|
-
|
|
11
|
-
The library can still be improved: it currently aborts when FTMS `REQUEST_CONTROL` fails, but KS Fit ignores that failure and proceeds. Mirroring that behavior would make start/stop reliable on MC-21.
|
|
12
|
-
|
|
13
|
-
## Evidence
|
|
14
|
-
|
|
15
|
-
### From the HCI snoop logs
|
|
16
|
-
|
|
17
|
-
User attached three Bluetooth HCI snoop logs (~17 MB total). Decoded with `btmon -r`.
|
|
18
|
-
|
|
19
|
-
GATT services advertised by the MC-21:
|
|
20
|
-
|
|
21
|
-
| Service | UUID | Notes |
|
|
22
|
-
|---|---|---|
|
|
23
|
-
| Generic Access Profile | `0x1800` | standard |
|
|
24
|
-
| Generic Attribute Profile | `0x1801` | standard |
|
|
25
|
-
| Fitness Machine | `0x1826` | FTMS — handles `0x000e–0x0028` |
|
|
26
|
-
| Device Information | `0x180a` | standard |
|
|
27
|
-
| Battery Service | `0x180f` | standard |
|
|
28
|
-
| TI vendor service | `f000ffc0-0451-4000-b000-000000000000` | TI BLE-Stack default base UUID; KS Fit does not reference it |
|
|
29
|
-
|
|
30
|
-
Notable absences:
|
|
31
|
-
|
|
32
|
-
- **No** `0x0000FE00` (legacy WiLink service) — this is what KingSmith's older treadmills (and the `ph4-walkingpad` library) use.
|
|
33
|
-
- **No** `24e2521c-…fdf7` (KS-HD-* "supplement" service) — used on other FTMS-based KingSmith models for vendor commands.
|
|
34
|
-
|
|
35
|
-
Inside the FTMS service there is one extra writable characteristic:
|
|
36
|
-
|
|
37
|
-
- Handle `0x0028`, UUID `d18d2c10-c44c-11e8-a355-529269fb1459`, properties `0x08` (write only).
|
|
38
|
-
|
|
39
|
-
KS Fit's behavior in the snoop:
|
|
40
|
-
|
|
41
|
-
1. Writes a fixed 8-byte payload `01 00 0d 00 06 0b 0f 0d` to handle `0x0028`. Done 40 times across the session, byte-for-byte identical, so the payload is *not* dynamic data (not speed, not a timestamp).
|
|
42
|
-
2. Writes `00` (`REQUEST_CONTROL`) to the FTMS Control Point (`0x0022`). The device responds **every single time** with the indication `80 00 04` — i.e. response code `0x80`, request opcode `0x00`, result `0x04` (`OPERATION_FAILED`). 40 attempts, 40 failures.
|
|
43
|
-
3. Despite that, KS Fit writes `07` (`START_OR_RESUME`) anyway, and the belt actually starts moving (Treadmill Data later shows speed climbing to 2.0 km/h).
|
|
44
|
-
4. **In the entire 16 MB log, KS Fit never writes opcode `0x02` (`SET_TARGET_SPEED`).** Only `0x00` and `0x07`.
|
|
45
|
-
5. Pairing/bonding is attempted by the Android stack (SMP `Pairing Request`) and fails (`Pairing Failed (0x05)` with reason "Unspecified"). The treadmill operates without bonding.
|
|
46
|
-
|
|
47
|
-
### From the KS Fit APKs
|
|
48
|
-
|
|
49
|
-
User attached two APKs:
|
|
50
|
-
|
|
51
|
-
- `KS_Fit-5.9.10.apk` (133 MB) — older release. arm64-v8a libs included.
|
|
52
|
-
- `KS Fit_6.0.7_APKPure.xapk` (185 MB) — newer release in xapk format. Only `config.armeabi_v7a.apk` split bundled.
|
|
53
|
-
|
|
54
|
-
KS Fit is built on Flutter, so the `.dex` files are mostly Flutter framework boilerplate; the actual application logic is in `lib/<arch>/libapp.so` as a Dart AOT snapshot. No Dart decompiler was used here — only `strings` on the AOT binary, which exposes class names, method names, source file paths, and log format strings via the symbol table.
|
|
55
|
-
|
|
56
|
-
Key strings found in `libapp.so` (cross-checked between v5.9.10 arm64 and v6.0.7 armv7):
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
package:ks_blue/src/wilink/wilink_protocol.dart
|
|
60
|
-
package:ks_blue/src/wilink/wilink_device.dart
|
|
61
|
-
package:ks_blue/src/wilink/treadmill_data.dart
|
|
62
|
-
package:ks_blue/src/wilink/ftms_action_executor.dart
|
|
63
|
-
package:ks_blue/src/spinning/spinning_ftms_device.dart
|
|
64
|
-
package:ks_blue/src/ble/protocol.dart
|
|
65
|
-
|
|
66
|
-
WilinkDeviceActionExt|setSpeed
|
|
67
|
-
WilinkDeviceActionExt|_setSpeedCommand
|
|
68
|
-
------do setSpeed new speed:
|
|
69
|
-
-----> WilinkBleDevice _winlinkDevice.setSpeed :
|
|
70
|
-
setSpeed new:
|
|
71
|
-
|
|
72
|
-
KsTreadmillDevice
|
|
73
|
-
KsTreadmillDevice startOrPause mode:
|
|
74
|
-
KsTreadmillDevice listenRunningStateChange event:
|
|
75
|
-
Init KsTreadmillDevice bleModel:
|
|
76
|
-
|
|
77
|
-
FtmsActionExecutor
|
|
78
|
-
FtmsActionExecutor extParamCmd
|
|
79
|
-
FtmsActionExecutor extVoidCmd
|
|
80
|
-
FtmsActionExecutor supplement STR action[
|
|
81
|
-
FtmsActionExecutor supplement UINT16 action[
|
|
82
|
-
FtmsActionExecutor supplement Void action[
|
|
83
|
-
|
|
84
|
-
2ad9: request control, result:
|
|
85
|
-
2ad9: request start, result:
|
|
86
|
-
2ad9: request pause or stop, result:
|
|
87
|
-
2ad9: request reset, result:
|
|
88
|
-
2ad9: response:
|
|
89
|
-
2ad9: write error:
|
|
90
|
-
|
|
91
|
-
HW-KS-HC-MC21A
|
|
92
|
-
KS-MC21
|
|
93
|
-
KS-SMC21C
|
|
94
|
-
get:isMC21
|
|
95
|
-
get:plan_not_support_mc21
|
|
96
|
-
_uploadMc21Record@1418084129
|
|
97
|
-
--> mc21 fireRecord
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Interpretation:
|
|
101
|
-
|
|
102
|
-
- **`setSpeed` lives only in the WiLink path.** The only string `setSpeed` references (across both APK versions) are `WilinkDeviceActionExt|setSpeed`, `WilinkDeviceActionExt|_setSpeedCommand`, `WilinkBleDevice _winlinkDevice.setSpeed`. There is no FTMS-side `setSpeed`. The WiLink protocol requires the `0xFE00` service, which the MC-21 doesn't have.
|
|
103
|
-
- **`KsTreadmillDevice` is the FTMS treadmill class.** Its visible methods are `startOrPause`, `listenRunningStateChange`, `close`, `Init`. No setSpeed, no setTargetSpeed.
|
|
104
|
-
- **`FtmsActionExecutor` is the FTMS extension command path.** It works against the supplement service (`24e2521c-…`), with `extParamCmd` / `extVoidCmd` / `supplement STR/UINT16/Void action`. The MC-21 doesn't have the supplement service, so this path also doesn't apply.
|
|
105
|
-
- **KS Fit explicitly knows the MC-21 is constrained.** `isMC21` getter, `plan_not_support_mc21` flag, separate `_uploadMc21Record` upload routine, dedicated MC-21 logging. The hardware identifier `HW-KS-HC-MC21A` confirms model recognition.
|
|
106
|
-
|
|
107
|
-
The vendor write to handle `0x0028` (`01 00 0d 00 06 0b 0f 0d`) does not appear to come from any documented control flow in the Dart code — based on the strings, it's almost certainly KS Fit's BLE plugin opportunistically writing to a discovered writable characteristic, not a deliberate handshake. The snoops also show it doesn't unlock anything: `REQUEST_CONTROL` still fails after the write.
|
|
108
|
-
|
|
109
|
-
### Why the user's HA log shows result `5` while KS Fit gets result `4`
|
|
110
|
-
|
|
111
|
-
User's HA log: `FTMS: Command 0x00 result: 5` (`CONTROL_NOT_PERMITTED`).
|
|
112
|
-
KS Fit snoop: response indication `80 00 04` (`OPERATION_FAILED`).
|
|
113
|
-
|
|
114
|
-
Both mean "no, can't do that." The exact code probably depends on prior state (whether some other client recently held control, whether the device just powered on, etc.). It's not the load-bearing detail — the load-bearing detail is that `REQUEST_CONTROL` fails on this device regardless of caller, and KS Fit just ignores it.
|
|
115
|
-
|
|
116
|
-
## Implications for the library
|
|
117
|
-
|
|
118
|
-
1. **Add MC-21 to FTMS name detection.** `FTMS_NAME_PREFIXES` is currently `("KS-HD-",)`. The MC-21 advertises as `KS-MC21-…` and falls through to the service-UUID probe path — that part works correctly today, but adding `"KS-MC21-"` (and likely `"KS-SMC21"` per the strings) would skip the probe round-trip on first connect.
|
|
119
|
-
2. **Tolerate `REQUEST_CONTROL` rejection.** `FTMSController._request_control` sets `_has_control` only on success; subsequent commands then keep retrying `REQUEST_CONTROL`. Per the snoop, the MC-21 will *never* grant control via REQUEST_CONTROL, but it does accept `START_OR_RESUME` / `STOP_OR_PAUSE` directly. The library should attempt control once, log the failure, and proceed — same as KS Fit. Don't gate start/stop on `_has_control`.
|
|
120
|
-
3. **Document MC-21 speed limitation.** Add a note (README "Known Behavior" or a per-device caveat) that on MC-21 the BLE interface exposes start/stop and telemetry only; speed is controlled exclusively by the physical remote. The slider in HA will reflect the physical speed but cannot drive it. This is a firmware constraint, not a library bug.
|
|
121
|
-
4. **Optional: emit a friendlier error if `SET_TARGET_SPEED` is rejected.** Today it just logs `result: 5` warnings. On a known-constrained model like MC-21 we could short-circuit and either no-op the call or raise a clearer "speed control not supported on this device" exception.
|
|
122
|
-
|
|
123
|
-
## What's not yet confirmed
|
|
124
|
-
|
|
125
|
-
The strings analysis is suggestive but not definitive — class and method names exist in the symbol table, but their actual call graphs do not. To rule out the possibility that KS Fit drives speed via the vendor characteristic at handle `0x0028` with a payload format the snoop didn't capture, the user would need to:
|
|
126
|
-
|
|
127
|
-
1. Start a fresh HCI snoop.
|
|
128
|
-
2. Open KS Fit, connect, start the belt.
|
|
129
|
-
3. Explicitly drag the speed slider in the app and confirm the belt physically responds (without touching the remote).
|
|
130
|
-
4. Attach the resulting log.
|
|
131
|
-
|
|
132
|
-
If that capture also contains only the fixed `01 00 0d 00 06 0b 0f 0d` writes, "no BLE speed control on MC-21" is settled. If it contains a different payload, that's the missing command path and worth implementing.
|
|
133
|
-
|
|
134
|
-
A proper Dart decompiler (`blutter`, `reFlutter`, `Doldrums`) would also resolve the question by direct inspection of the Dart code — not used here.
|
|
135
|
-
|
|
136
|
-
## Methodology / repro
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
# Decode HCI snoop logs
|
|
140
|
-
btmon -r btsnoop_hci_*.log > decoded.txt
|
|
141
|
-
|
|
142
|
-
# Find FTMS Control Point activity
|
|
143
|
-
grep -nE "Handle: 0x0022 Type: Fitness Machine Control Point|Data\[" decoded.txt
|
|
144
|
-
|
|
145
|
-
# Extract Flutter AOT binary from APK
|
|
146
|
-
unzip APK.apk "lib/<arch>/libapp.so"
|
|
147
|
-
|
|
148
|
-
# String inventory
|
|
149
|
-
strings -a libapp.so | grep -iE "setSpeed|FTMS|wilink|MC21|2ad9"
|
|
150
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/__init__.py
RENAMED
|
File without changes
|
{walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/models.py
RENAMED
|
File without changes
|
{walkingpad_controller-0.4.1 → walkingpad_controller-0.4.2}/src/walkingpad_controller/wilink.py
RENAMED
|
File without changes
|
|
File without changes
|