Simple python script that can control KingSmith WalkingPad A1. Others report the similar models, such as R1 PRO work on the same principle.
The belt communicates via Bluetooth LE GATT. Only one device can be connected to the belt at a time, i.e., if original app is connected, the controller won't be able to connect.
For the best understanding start jupyter-notebook and take a look at belt_control.ipynb
# Install jupyter-notebook
pip3 install jupyter
# Start jupyter-notebook in this repository
jupyter-notebook .
# open belt_control.ipynb
The main controller class is Controller
in pad.py
Controller enables to control the belt via CLI shell.
Install the library:
pip install -U ph4-walkingpad
Start controller:
# Note: use module notation to run the script, no direct script invocation.
python -m ph4_walkingpad.main --stats 750 --json-file ~/walking.json
Or alternatively, if package was installed with pip:
ph4-walkingpad-ctl --stats 750 --json-file ~/walking.json
The command asks for periodic statistics fetching at 750 ms, storing records to ~/walking.json
.
Output
---------------------------------------------------------------------------
WalkingPad controller
---------------------------------------------------------------------------
$> help
Documented commands (use 'help -v' for verbose/'help <topic>' for details):
===========================================================================
alias help py quit set speed stop
ask_stats history Q run_pyscript shell start switch_mode
edit macro q run_script shortcuts status tasks
$> status
WalkingPadCurStatus(dist=0.0, time=0, steps=0, speed=0.0, state=5, mode=2, app_speed=0.06666666666666667, button=2, rest=0000)
$> start
$> speed 30
$> speed 15
$> status
WalkingPadCurStatus(dist=0.01, time=16, steps=18, speed=1.8, state=1, mode=1, app_speed=1.5, button=1, rest=0000)
$> status
WalkingPadCurStatus(dist=0.01, time=17, steps=20, speed=1.5, state=1, mode=1, app_speed=1.5, button=1, rest=0000)
$> speed 30
$> s
$> WalkingPadCurStatus(dist=0.98, time=670, steps=1195, speed=6.0, state=1, mode=1, app_speed=6.0, button=1, rest=0000), cal: 38.73, net: 30.89, total: 73.65, total net: 57.91
$> stop
$> start
$> speed 30
$> status
Due to nature of the BluetoothLE callbacks being executed on the main thread we cannot use readline to read from the console, so the shell CLI does not support auto-complete, ctrl-r, up-arrow for the last command, etc. Readline does not have async support at the moment.
This project uses Bleak Bluetooth library. It was reported that OSX 12+ changed Bluetooth scanning logic, so it is not possible to connect to a device without scanning Bluetooth first. Moreover, it blocks for the whole timeout interval.
Thus, when using on OSX 12+:
-a
parameter--filter
and specify device address prefix--scan-timeout
Minimal required version of Bleak is 0.14.1
If the process is still crashing, it may be it does not have permissions to access Bluetooth. To fix it, add your Terminal app (in my case iTerm2.app) to System Preferences -> Security & Privacy -> Bluetooth.
Related resources: https://github.com/hbldh/bleak/issues/635, https://github.com/hbldh/bleak/pull/692
If the -p profile.json
argument is passed, profile of the person is loaded from the file, so the controller can count burned calories.
Units are in a metric system.
{
"id": "user1",
"male": true,
"age": 25,
"weight": 80,
"height": 1.80,
"token": "JWT-token",
"did": "ff:ff:ff:ff:ff:ff",
"email": "your-account@gmail.com",
"password": "service-login-password",
"password_md5": "or md5hash of password, hexcoded, to avoid plaintext password in config"
}
did
is optional field, associates your records with pad MAC address when uploading to the serviceemail
and (password
or password_md5
) are optional. If filled, you can call login
to generate a fresh JWT usable for service auth.Note that once you use login
command, other JWTs become invalid, e.g., on your phone.
If you want to use the service on both devices, login with mobile phone while logging output with adb
and capture JWT from logs (works only for Android phones).
The following arguments enable data collection to a statistic file:
--stats 750 --json-file ~/walking.json
In order to guarantee file consistency the format is one JSON record per file, so it is easy to append to a file at any time without need to read and rewrite it with each update (helps to prevent a data loss in cause of a crash).
Example:
{"time": 554, "dist": 79, "steps": 977, "speed": 60, "app_speed": 180, "belt_state": 1, "controller_button": 0, "manual_mode": 1, "raw": "f8a2013c0100022a00004f0003d1b4000000e3fd", "rec_time": 1615644982.5917802, "pid": "ph4r05", "ccal": 23.343, "ccal_net": 18.616, "ccal_sum": 58.267, "ccal_net_sum": 45.644}
{"time": 554, "dist": 79, "steps": 978, "speed": 60, "app_speed": 180, "belt_state": 1, "controller_button": 0, "manual_mode": 1, "raw": "f8a2013c0100022a00004f0003d2b4000000e4fd", "rec_time": 1615644983.345463, "pid": "ph4r05", "ccal": 23.343, "ccal_net": 18.616, "ccal_sum": 58.267, "ccal_net_sum": 45.644}
{"time": 555, "dist": 79, "steps": 980, "speed": 60, "app_speed": 180, "belt_state": 1, "controller_button": 0, "manual_mode": 1, "raw": "f8a2013c0100022b00004f0003d4b4000000e7fd", "rec_time": 1615644984.0991402, "pid": "ph4r05", "ccal": 23.476, "ccal_net": 18.722, "ccal_sum": 58.4, "ccal_net_sum": 45.749}
{"time": 556, "dist": 79, "steps": 981, "speed": 60, "app_speed": 180, "belt_state": 1, "controller_button": 0, "manual_mode": 1, "raw": "f8a2013c0100022c00004f0003d5b4000000e9fd", "rec_time": 1615644984.864169, "pid": "ph4r05", "ccal": 23.608, "ccal_net": 18.828, "ccal_sum": 58.533, "ccal_net_sum": 45.855}
{"time": 557, "dist": 80, "steps": 982, "speed": 60, "app_speed": 180, "belt_state": 1, "controller_button": 0, "manual_mode": 1, "raw": "f8a2013c0100022d0000500003d6b4000000ecfd", "rec_time": 1615644985.606997, "pid": "ph4r05", "ccal": 23.741, "ccal_net": 18.933, "ccal_sum": 58.665, "ccal_net_sum": 45.961}
The benefit of having detailed data is an option to analyze data from the whole run, e.g., how step size varies over the time during one session, collect preferred speeds, etc...
Also, if the original app fails to fetch the final state from the Belt, having continuous data stream is helpful to avoid data loss.
I used the easiest way I found - the original Android application is quite generously logging all Bluetooth requests and responses; and network requests and responses (JWT included).
After few trial/error attempts I managed to reverse binary packet protocol format. See pad.py for protocol internals.
You can query from the belt a status message (app does so each 750 ms, approx). The status contains speed, distance, steps, and very simple CRC code (sum of the payload). Interestingly, calories are not part of the status message and cannot be queried either.
For obtaining logs just plug Android phone via USB, enable development mode on the phone, enable ADB connection and run:
adb logcat
(Or use AndroidStudio)
You then can see the app communication with the belt in real-time. When using the app, it logs also requests so you can figure out how commands for e.g., speed change look like.
Should vendor remove the logging from the app and you are unable to find APK in archives with the logging, you can always enable Bluetooth logs in the Phone development settings.
This approach is not that straightforward as from logs as you cannot see belt responses in real-time.
The Bluetooth log can be obtained from the device via adb
and opened in Wireshark.
You may need to do own journal with times and commands you issued so you can experiment with the belt (e.g., change speeds), the commands get logged to the Bluetooth log. Then after the experiment, download the Bluetooth log and map your log entries to the packets from the log.
This is substantially difficult compared to the easy way - message logs.
The original application is implemented in Flutter, so direct application reversing is quite painful process. Flutter compiles the source language (TypeScript I guess) to a binary form. It runs on top of a Flutter virtual machine, thus compiled binary has only one primary entry point, a dispatch function. Disassembly does not yield anything sensible, it requires special tools. Also, decompilation tools require the Flutter version to precisely match the version used to compile the application.
For those willing to spend time on this: 1, 2, 3, 4.
Manual sniffer capture:
./nrf_sniffer_ble.sh --extcap-interface /dev/cu.usbserial-0001 --capture --fifo /tmp/fi
I was using the WalkingPad app to reverse engineer packet formats:
Other reported apps may be less obfuscated and easier to analyze (didn't test):
Protocol internals are implemented in pad.py.
[x0, x1, x2]
, where integer form is
x = x0*65536 + x1*256 + x0
(big endian on 3 bytes)cmd
be the whole received payload,
checksum is computed as: cmd[-2] = sum(cmd[1:-2]) % 256
. For more, check WalkingPadCurStatus
WalkingPadLastStatus
.
Another request from the app clears the last run status.Example of a status message m
:
f8a2010f01000fd10000ab0012ae3c0000003afd
When logged by the application, it is printed out as array if bytes:
[248, 162, 1, 15, 1, 0, 15, 209, 0, 0, 171, 0, 18, 174, 60, 0, 0, 0, 58, 253]
[248, 162]
or f8a2
is a fixed prefix, probably the message ID.m[2] == 1
is a belt statem[3] == 15
is a belt speed * 10, here 1.5 kmphm[4] == 1
is a flag signalizing manual mode (vs automatic = 0)m[5:8] == [0, 15, 209]
is encoded time in seconds, here 4049s = 67 min, 29sm[8:11] == [0, 0, 171]
is distance in 10 meters, here 171 = 1.71 kmm[11:14] == [0, 18, 174]
is number of steps, here 4782m[14] == 60
is the last set app speed, 60 units, 6 kmphm[15]
unknownm[16]
last controller button pressedm[17] == 58
is the checksumm[18] == 253
is a fixed suffixMeaning of some fields are not known (15) or the value space was not explored. m[15]
could be for example heart rate
for those models measuring it.
Another reverse engineer of the protocol (under GPL, tldr): https://github.com/DorianRudolph/QWalkingPad/blob/master/Protocol.h
Thanks to all contributors and to the community.
This project was awarded by the Google Open Source Peer Bonus in Feb 2022.
Install pre-commit hooks defined by .pre-commit-config.yaml
pip3 install -U pre-commit pytest mypy types-requests
mypy --install-types
pre-commit install
Auto fix
pre-commit run --all-files
Plugin version update
pre-commit autoupdate
Thanks for considering donation if you find this project useful:
1DBr1tfuqv6xphg5rzNTPxqiUbqbRHrM2E
(No Lightning for now, hopefully soon)
87KDQUP7yVKd7inmX2WXuaQUBrxeGN9X9AuQwfaUkJ3KQXSRe6KbhnLRvWNK4mx2SeBwcFdHYgS71fzYFS5mtNf7Dn8SdpJ