Skip to content

Commit

Permalink
add pre-commit checks and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ph4r05 committed Sep 20, 2024
1 parent f38f6b5 commit e069b0b
Show file tree
Hide file tree
Showing 25 changed files with 628 additions and 475 deletions.
4 changes: 4 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[flake8]
max-line-length = 120
select = C,E,F,W,B,B950
extend-ignore = E203, E501, W503
22 changes: 22 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Pre-commit Check

# This workflow is triggered on push and pull request events
on: [push, pull_request]

jobs:
run-pre-commit:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.12'

- name: Install and Run pre-commit
run: |
pip3 install -U pre-commit pytest mypy types-requests
pre-commit run --all-files
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@ profile.json
profile.json.*
profile-a*.json*
account-data.json

42 changes: 42 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-json
- repo: https://github.com/timothycrosley/isort
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-bugbear]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.4.1
hooks:
- id: mypy
name: mypy
entry: /bin/bash -c "PATH=\"venv/bin:$PATH\" MYPYPATH=\"ph4_walkingpad/\" mypy --ignore-missing-imports $*"
files: "^ph4_walkingpad/"
types: [file, python]
language: system
- repo: local
hooks:
- id: pytest
name: pytest
entry: ./pytest.sh
language: python
pass_filenames: false
always_run: true
# alternatively you could `types: [python]` so it only runs when python files change
# though tests might be invalidated if you were to say change a data file
default_language_version:
python: python3.12
11 changes: 0 additions & 11 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
89 changes: 54 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WalkingPad controller

Simple python script that can control KingSmith WalkingPad A1.
Simple python script that can control KingSmith WalkingPad A1.
[Others report](https://github.com/ph4r05/ph4-walkingpad/issues/1) the similar models, such as R1 PRO work on the same principle.

The belt communicates via [Bluetooth LE GATT](https://www.oreilly.com/library/view/getting-started-with/9781491900550/ch04.html).
Expand All @@ -17,7 +17,7 @@ Only one device can be connected to the belt at a time, i.e., if original app is
- start type (intelli)
- Sensitivity in automatic mode
- Display
- Child lock
- Child lock
- Units (miles/km)
- Target (time, distance, calories, steps)
- Ask for current state (speed, time, distance, steps)
Expand Down Expand Up @@ -51,7 +51,7 @@ Install the library:
pip install -U ph4-walkingpad
```

Start controller:
Start controller:
```bash
# Note: use module notation to run the script, no direct script invocation.
python -m ph4_walkingpad.main --stats 750 --json-file ~/walking.json
Expand All @@ -75,9 +75,9 @@ $> help
Documented commands (use 'help -v' for verbose/'help <topic>' for details):
===========================================================================
alias help py quit set speed stop
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
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)
Expand All @@ -90,7 +90,7 @@ $> 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
$> 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
Expand All @@ -99,11 +99,11 @@ $> 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.
Readline does not have async support at the moment.

### OSX Troubleshooting

This project uses [Bleak Bluetooth library](https://github.com/hbldh/bleak).
This project uses [Bleak Bluetooth library](https://github.com/hbldh/bleak).
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.

Expand Down Expand Up @@ -131,7 +131,7 @@ Units are in a metric system.
"age": 25,
"weight": 80,
"height": 1.80,
"token": "JWT-token",
"token": "JWT-token",
"did": "ff:ff:ff:ff:ff:ff",
"email": "[email protected]",
"password": "service-login-password",
Expand All @@ -142,18 +142,18 @@ Units are in a metric system.
- `did` is optional field, associates your records with pad MAC address when uploading to the service
- `email` 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.
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).

### Stats file

The following arguments enable data collection to a statistic file:
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
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:
Expand All @@ -170,18 +170,18 @@ The benefit of having detailed data is an option to analyze data from the whole

Also, if the original app fails to fetch the final state from the Belt, having continuous data stream is helpful to avoid data loss.

### Reversing Belt API
### Reversing Belt API

#### Easy way - Android logs
I used the easiest way I found - the original Android application is quite generously logging all
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](/ph4_walkingpad/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.
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:

Expand All @@ -191,39 +191,39 @@ 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
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.

#### Medium - Bluetooth logs

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.
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.
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.
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.

#### Hard way - Flutter disassembly
The original application is implemented in [Flutter](https://flutter.dev), so direct application reversing is quite painful process.
The original application is implemented in [Flutter](https://flutter.dev), 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](https://tinyhack.com/2021/03/07/reversing-a-flutter-app-by-recompiling-flutter-engine/),
[2](https://www.programmersought.com/article/28206180369/),
[2](https://www.programmersought.com/article/28206180369/),
[3](https://rloura.wordpress.com/2020/12/04/reversing-flutter-for-android-wip/),
[4](https://blog.tst.sh/reverse-engineering-flutter-apps-part-1/).


#### Hack way - BLE sniffer

- Buy Nordic nRF52832 or nRF52870 USB dongle for BLE sniffing
- Install plugin to Wireshark
- Install plugin to Wireshark
- https://www.nordicsemi.com/Products/Development-tools/nRF-Sniffer-for-Bluetooth-LE/Download#infotabs
- https://www.szrfstar.com/upload/file/1587092285.pdf
- In Wireshark, go to View -> Interface Toolbars -> nRF Sniffer for Bluetooth LE
Expand All @@ -234,7 +234,7 @@ For those willing to spend time on this: [1](https://tinyhack.com/2021/03/07/rev
Manual sniffer capture:

```bash
./nrf_sniffer_ble.sh --extcap-interface /dev/cu.usbserial-0001 --capture --fifo /tmp/fi
./nrf_sniffer_ble.sh --extcap-interface /dev/cu.usbserial-0001 --capture --fifo /tmp/fi
```

#### Alternatives
Expand All @@ -254,18 +254,18 @@ Protocol internals are implemented in [pad.py](ph4_walkingpad/pad.py).
- Belt communicates over BT LE GATT messages.
- Controlling app sends a simple binary message to the belt for control and status fetch (request)
- App sends periodically status requests (~ 750 ms), belt responds with a binary message containing:
current belt state, manual mode indicator, belt running time in seconds, distance in 10 meters (1km = 100 units),
current belt state, manual mode indicator, belt running time in seconds, distance in 10 meters (1km = 100 units),
number of steps, last set speed, last button pressed on controller (calories are not reported by the belt)
- Large numbers, such as distance, steps and time are encoded in 3 bytes in the following form: `[x0, x1, x2]`, where integer form is
`x = x0*65536 + x1*256 + x0` (big endian on 3 bytes)
- Packet contains a simple checksum. If the checksum is invalid, belt ignores the command. Let `cmd` be the whole received payload,
checksum is computed as: `cmd[-2] = sum(cmd[1:-2]) % 256`. For more, check `WalkingPadCurStatus`
checksum is computed as: `cmd[-2] = sum(cmd[1:-2]) % 256`. For more, check `WalkingPadCurStatus`
- Belt stores the last run status in memory. On query from the app the belt returns it in a different status message form, check `WalkingPadLastStatus`.
Another request from the app clears the last run status.
- It seems that the belt stores the last run status only for a limited time and does not survive power cut, thus this might be the reason
why users are reporting apps are not fetching the statistics completely from the belt. Final stats are fetched after the belt is stopped,
why users are reporting apps are not fetching the statistics completely from the belt. Final stats are fetched after the belt is stopped,
thus if app is not running when belt stops (e.g., auto stop, or by controller), app sometimes does not make the status fetch in time and the run status is lost.

Example of a status message `m`:

```
Expand All @@ -291,18 +291,38 @@ When logged by the application, it is printed out as array if bytes:
- `m[17] == 58` is the checksum
- `m[18] == 253` is a fixed suffix

Meaning 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.
Meaning 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.

### Related work
Another reverse engineer of the protocol (under GPL, [tldr](https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3))): https://github.com/DorianRudolph/QWalkingPad/blob/master/Protocol.h

### Thanks
Thanks to all contributors and to the community.
Thanks to all contributors and to the community.

This project was awarded by the [
Google Open Source Peer Bonus](https://opensource.googleblog.com/2022/03/Announcing-First-Group-of-Google-Open-Source-Peer-Bonus-Winners-in-2022.html) in Feb 2022.

## Development

Install pre-commit hooks defined by `.pre-commit-config.yaml`

```shell
pip3 install -U pre-commit pytest mypy types-requests
mypy --install-types
pre-commit install
```

Auto fix
```shell
pre-commit run --all-files
```

Plugin version update
```shell
pre-commit autoupdate
```

### Donate

Thanks for considering donation if you find this project useful:
Expand All @@ -315,11 +335,10 @@ Thanks for considering donation if you find this project useful:
(No Lightning for now, hopefully soon)

#### Monero

```
87KDQUP7yVKd7inmX2WXuaQUBrxeGN9X9AuQwfaUkJ3KQXSRe6KbhnLRvWNK4mx2SeBwcFdHYgS71fzYFS5mtNf7Dn8SdpJ
```

#### PayPal
[PayPal link](https://www.paypal.com/donate?hosted_button_id=LC2LK4FGHSUCQ)

2 changes: 1 addition & 1 deletion analysis.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2721,4 +2721,4 @@
},
"nbformat": 4,
"nbformat_minor": 1
}
}
4 changes: 4 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os
import sys

sys.path.insert(0, os.path.dirname(__file__))
Loading

0 comments on commit e069b0b

Please sign in to comment.