walkingpad-controller 0.4.0__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.0 → walkingpad_controller-0.4.2}/PKG-INFO +1 -1
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/pyproject.toml +1 -1
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/const.py +15 -3
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/controller.py +10 -1
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/ftms.py +102 -13
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/models.py +3 -0
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/.github/workflows/publish.yml +0 -0
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/.gitignore +0 -0
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/LICENSE +0 -0
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/README.md +0 -0
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/__init__.py +0 -0
- {walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/wilink.py +0 -0
- {walkingpad_controller-0.4.0 → 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.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/const.py
RENAMED
|
@@ -22,6 +22,13 @@ SUPPLEMENT_SERVICE_UUID = "24e2521c-f63b-48ed-85be-c5330a00fdf7"
|
|
|
22
22
|
SUPPLEMENT_NOTIFY_UUID = "24e2521c-f63b-48ed-85be-c5330b00fdf7"
|
|
23
23
|
SUPPLEMENT_WRITE_UUID = "24e2521c-f63b-48ed-85be-c5330d00fdf7"
|
|
24
24
|
|
|
25
|
+
# KingSmith MC-21 vendor pre-amble. KS Fit writes a fixed 8-byte payload to
|
|
26
|
+
# this characteristic (located inside the FTMS service) before every Control
|
|
27
|
+
# Point command. Without it, MC-21 firmware rejects SET_TARGET_SPEED with
|
|
28
|
+
# CONTROL_NOT_PERMITTED. See issue #1.
|
|
29
|
+
KINGSMITH_VENDOR_PREAMBLE_UUID = "d18d2c10-c44c-11e8-a355-529269fb1459"
|
|
30
|
+
KINGSMITH_VENDOR_PREAMBLE_PAYLOAD = bytes.fromhex("01000d00060b0f0d")
|
|
31
|
+
|
|
25
32
|
# Legacy WiLink Service (for older devices)
|
|
26
33
|
WILINK_SERVICE_UUID = "0000fe00-0000-1000-8000-00805f9b34fb"
|
|
27
34
|
|
|
@@ -125,6 +132,11 @@ class TreadmillDataFlags:
|
|
|
125
132
|
# These devices have service 0x1826 but NOT 0xFE00.
|
|
126
133
|
FTMS_NAME_PREFIXES = ("KS-HD-",)
|
|
127
134
|
|
|
128
|
-
# Default connection parameters
|
|
129
|
-
|
|
130
|
-
|
|
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.0 → 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.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/ftms.py
RENAMED
|
@@ -36,6 +36,8 @@ from .const import (
|
|
|
36
36
|
FITNESS_MACHINE_STATUS_UUID,
|
|
37
37
|
FTMS_CONTROL_POINT_UUID,
|
|
38
38
|
FTMS_FEATURE_UUID,
|
|
39
|
+
KINGSMITH_VENDOR_PREAMBLE_PAYLOAD,
|
|
40
|
+
KINGSMITH_VENDOR_PREAMBLE_UUID,
|
|
39
41
|
SUPPLEMENT_SERVICE_UUID,
|
|
40
42
|
SUPPORTED_SPEED_RANGE_UUID,
|
|
41
43
|
TREADMILL_DATA_UUID,
|
|
@@ -126,6 +128,12 @@ class FTMSController:
|
|
|
126
128
|
|
|
127
129
|
Args:
|
|
128
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).
|
|
129
137
|
"""
|
|
130
138
|
_LOGGER.info("FTMS: Connecting to %s", ble_device.address)
|
|
131
139
|
|
|
@@ -149,6 +157,18 @@ class FTMSController:
|
|
|
149
157
|
# Request control
|
|
150
158
|
await self._request_control()
|
|
151
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
|
+
|
|
152
172
|
async def disconnect(self) -> None:
|
|
153
173
|
"""Disconnect from the device."""
|
|
154
174
|
if self._client and self._client.is_connected:
|
|
@@ -196,6 +216,20 @@ class FTMSController:
|
|
|
196
216
|
except Exception:
|
|
197
217
|
self._capabilities.has_supplement = False
|
|
198
218
|
|
|
219
|
+
# Check for the KingSmith MC-21 vendor pre-amble characteristic.
|
|
220
|
+
# If present, we'll write the magic payload before every Control
|
|
221
|
+
# Point command — that's what KS Fit does, and without it the
|
|
222
|
+
# firmware refuses SET_TARGET_SPEED.
|
|
223
|
+
try:
|
|
224
|
+
char = self._client.services.get_characteristic(
|
|
225
|
+
KINGSMITH_VENDOR_PREAMBLE_UUID
|
|
226
|
+
)
|
|
227
|
+
if char is not None:
|
|
228
|
+
self._capabilities.has_vendor_preamble = True
|
|
229
|
+
_LOGGER.info("FTMS: KingSmith vendor pre-amble characteristic detected")
|
|
230
|
+
except Exception:
|
|
231
|
+
self._capabilities.has_vendor_preamble = False
|
|
232
|
+
|
|
199
233
|
async def _read_capabilities(self) -> None:
|
|
200
234
|
"""Read device capabilities from FTMS characteristics."""
|
|
201
235
|
if not self._client:
|
|
@@ -424,6 +458,16 @@ class FTMSController:
|
|
|
424
458
|
_LOGGER.warning("FTMS: Not connected, cannot send command")
|
|
425
459
|
return False
|
|
426
460
|
|
|
461
|
+
if self._capabilities.has_vendor_preamble:
|
|
462
|
+
try:
|
|
463
|
+
await self._client.write_gatt_char(
|
|
464
|
+
KINGSMITH_VENDOR_PREAMBLE_UUID,
|
|
465
|
+
KINGSMITH_VENDOR_PREAMBLE_PAYLOAD,
|
|
466
|
+
response=True,
|
|
467
|
+
)
|
|
468
|
+
except BleakError as err:
|
|
469
|
+
_LOGGER.debug("FTMS: Vendor pre-amble write error: %s", err)
|
|
470
|
+
|
|
427
471
|
command = bytes([opcode]) + params
|
|
428
472
|
_LOGGER.debug("FTMS: Sending control point command: %s", command.hex())
|
|
429
473
|
|
|
@@ -437,10 +481,24 @@ class FTMSController:
|
|
|
437
481
|
_LOGGER.warning("FTMS: Write error: %s", err)
|
|
438
482
|
return False
|
|
439
483
|
|
|
440
|
-
# Wait for indication response
|
|
484
|
+
# Wait for indication response. On firmware that uses the vendor
|
|
485
|
+
# pre-amble (MC-21), the device silently accepts most commands
|
|
486
|
+
# without sending an indication — the snoop shows only the very
|
|
487
|
+
# first REQUEST_CONTROL gets one. Treat timeout as success in that
|
|
488
|
+
# case, with a shorter wait so we don't stall the caller.
|
|
489
|
+
effective_timeout = (
|
|
490
|
+
1.0 if self._capabilities.has_vendor_preamble else timeout
|
|
491
|
+
)
|
|
441
492
|
try:
|
|
442
|
-
await asyncio.wait_for(self._cp_response_event.wait(),
|
|
493
|
+
await asyncio.wait_for(self._cp_response_event.wait(), effective_timeout)
|
|
443
494
|
except asyncio.TimeoutError:
|
|
495
|
+
if self._capabilities.has_vendor_preamble:
|
|
496
|
+
_LOGGER.debug(
|
|
497
|
+
"FTMS: No indication for opcode 0x%02x — assuming success "
|
|
498
|
+
"(vendor pre-amble path)",
|
|
499
|
+
opcode,
|
|
500
|
+
)
|
|
501
|
+
return True
|
|
444
502
|
_LOGGER.warning(
|
|
445
503
|
"FTMS: Control point response timeout for opcode 0x%02x", opcode
|
|
446
504
|
)
|
|
@@ -467,13 +525,25 @@ class FTMSController:
|
|
|
467
525
|
return False
|
|
468
526
|
|
|
469
527
|
async def _request_control(self) -> bool:
|
|
470
|
-
"""Request control of the fitness machine.
|
|
528
|
+
"""Request control of the fitness machine.
|
|
529
|
+
|
|
530
|
+
Some KingSmith firmware (e.g. MC-21) rejects REQUEST_CONTROL with
|
|
531
|
+
OPERATION_FAILED / CONTROL_NOT_PERMITTED but still accepts the
|
|
532
|
+
commands that follow. KS Fit ignores the failure and proceeds; we
|
|
533
|
+
do the same — `_has_control` is set regardless of the response so
|
|
534
|
+
every subsequent command doesn't keep retrying a call we know will
|
|
535
|
+
fail. See issue #1.
|
|
536
|
+
"""
|
|
471
537
|
result = await self._write_control_point(FTMSOpcode.REQUEST_CONTROL)
|
|
538
|
+
self._has_control = True
|
|
472
539
|
if result:
|
|
473
|
-
self._has_control = True
|
|
474
540
|
_LOGGER.info("FTMS: Control acquired")
|
|
475
541
|
else:
|
|
476
|
-
_LOGGER.warning(
|
|
542
|
+
_LOGGER.warning(
|
|
543
|
+
"FTMS: REQUEST_CONTROL was rejected — proceeding anyway "
|
|
544
|
+
"(some KingSmith firmware refuses this command but still "
|
|
545
|
+
"accepts START_OR_RESUME / STOP_OR_PAUSE)"
|
|
546
|
+
)
|
|
477
547
|
return result
|
|
478
548
|
|
|
479
549
|
async def start(self) -> bool:
|
|
@@ -491,17 +561,18 @@ class FTMSController:
|
|
|
491
561
|
if not self._has_control:
|
|
492
562
|
await self._request_control()
|
|
493
563
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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)
|
|
499
569
|
|
|
500
570
|
if not self.connected:
|
|
501
|
-
_LOGGER.warning("FTMS: Connection lost
|
|
571
|
+
_LOGGER.warning("FTMS: Connection lost during START_OR_RESUME")
|
|
502
572
|
return False
|
|
503
573
|
|
|
504
|
-
if
|
|
574
|
+
if accepted:
|
|
575
|
+
_LOGGER.info("FTMS: START_OR_RESUME accepted (cold start)")
|
|
505
576
|
belt_running = await self._wait_for_belt_moving(timeout=15.0)
|
|
506
577
|
if not belt_running:
|
|
507
578
|
if not self.connected:
|
|
@@ -513,8 +584,26 @@ class FTMSController:
|
|
|
513
584
|
"FTMS: Cold start complete — belt running at %.1f km/h",
|
|
514
585
|
self._status.speed,
|
|
515
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
|
|
516
601
|
|
|
517
|
-
|
|
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
|
|
518
607
|
|
|
519
608
|
async def _wait_for_belt_moving(self, timeout: float = 15.0) -> bool:
|
|
520
609
|
"""Wait for the belt to report speed > 0 after a cold start.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/__init__.py
RENAMED
|
File without changes
|
{walkingpad_controller-0.4.0 → walkingpad_controller-0.4.2}/src/walkingpad_controller/wilink.py
RENAMED
|
File without changes
|
|
File without changes
|