tenways-ble 0.2.0__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.
- tenways_ble-0.2.0/.github/workflows/publish.yml +23 -0
- tenways_ble-0.2.0/.github/workflows/tests.yml +19 -0
- tenways_ble-0.2.0/.gitignore +8 -0
- tenways_ble-0.2.0/LICENSE +21 -0
- tenways_ble-0.2.0/PKG-INFO +191 -0
- tenways_ble-0.2.0/README.md +174 -0
- tenways_ble-0.2.0/docs/RESEARCH.md +86 -0
- tenways_ble-0.2.0/pyproject.toml +39 -0
- tenways_ble-0.2.0/src/tenways_ble/__init__.py +26 -0
- tenways_ble-0.2.0/src/tenways_ble/cli.py +144 -0
- tenways_ble-0.2.0/src/tenways_ble/client.py +260 -0
- tenways_ble-0.2.0/src/tenways_ble/const.py +13 -0
- tenways_ble-0.2.0/src/tenways_ble/models.py +18 -0
- tenways_ble-0.2.0/src/tenways_ble/protocol.py +240 -0
- tenways_ble-0.2.0/tests/__init__.py +0 -0
- tenways_ble-0.2.0/tests/frames.py +30 -0
- tenways_ble-0.2.0/tests/test_basic.py +11 -0
- tenways_ble-0.2.0/tests/test_client.py +129 -0
- tenways_ble-0.2.0/tests/test_protocol.py +201 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
- run: pip install -e '.[dev]' build
|
|
19
|
+
- run: pytest
|
|
20
|
+
- name: Check tag matches version
|
|
21
|
+
run: test "v$(python -c 'import tenways_ble; print(tenways_ble.__version__)')" = "$GITHUB_REF_NAME"
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: ${{ matrix.python-version }}
|
|
18
|
+
- run: pip install -e '.[dev]'
|
|
19
|
+
- run: pytest
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christoph Berliner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tenways-ble
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Read-only BLE client, King Meter protocol and diagnostic CLI for TENWAYS e-bikes
|
|
5
|
+
Project-URL: Homepage, https://github.com/Styx85/tenways-ble
|
|
6
|
+
Project-URL: Issues, https://github.com/Styx85/tenways-ble/issues
|
|
7
|
+
Author: Christoph Berliner
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ble,bluetooth,ebike,home-assistant,king-meter,tenways
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: bleak>=0.22
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# tenways-ble
|
|
19
|
+
|
|
20
|
+
Small experimental Python library and CLI for local BLE access to TENWAYS e-bikes.
|
|
21
|
+
|
|
22
|
+
Read-only by design:
|
|
23
|
+
|
|
24
|
+
- discovers nearby TENWAYS BLE devices;
|
|
25
|
+
- decodes the proprietary **King Meter** frame protocol (CRC-16/MODBUS, TLV fields) used by the TENWAYS Longtail Duo;
|
|
26
|
+
- reads live telemetry: traction battery, odometer, trip, max/average speed, ride time;
|
|
27
|
+
- reads firmware and serial number;
|
|
28
|
+
- reads the standard Bluetooth Battery Level characteristic (`0x2A19`), which is **not** the traction battery;
|
|
29
|
+
- does **not** send any write, lock, light or configuration commands.
|
|
30
|
+
|
|
31
|
+
The protocol was reconstructed from an Android HCI capture of the official TENWAYS app; all test vectors are the original captured frames. Live operation against the bike is still being validated.
|
|
32
|
+
|
|
33
|
+
## Library usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from tenways_ble import TenwaysBike
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
async with TenwaysBike("E8:88:6C:31:B3:0D") as bike:
|
|
41
|
+
status = await bike.read_status()
|
|
42
|
+
print(status.battery_percent, status.odometer_km)
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`TenwaysBike` also accepts an already connected `BleakClient` (for example from `bleak-retry-connector` in Home Assistant). It then only speaks the protocol and leaves the connection to the caller.
|
|
48
|
+
|
|
49
|
+
The sans-I/O protocol module `tenways_ble.protocol` builds and parses frames without any Bluetooth dependency.
|
|
50
|
+
|
|
51
|
+
## Devices
|
|
52
|
+
|
|
53
|
+
| Device | Advertised name | Notes |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Longtail Duo drive | `Longtail Duo` | King Meter service, telemetry source. Standard battery reads ~1 %, not the traction battery. |
|
|
56
|
+
| Separate BLE device | `Tenways-ble-XXXXXX` | HID service, standard battery 100 %, also exposes the King Meter service. Function unconfirmed (remote/control unit?). |
|
|
57
|
+
|
|
58
|
+
## Known GATT layout
|
|
59
|
+
|
|
60
|
+
Standard battery service:
|
|
61
|
+
|
|
62
|
+
- Service: `0000180f-0000-1000-8000-00805f9b34fb`
|
|
63
|
+
- Battery Level: `00002a19-0000-1000-8000-00805f9b34fb` (`READ`, `NOTIFY`)
|
|
64
|
+
|
|
65
|
+
King Meter service (`43480001-…` contains `KING METER` in ASCII):
|
|
66
|
+
|
|
67
|
+
- Service: `43480001-f001-4b49-4e47-204d45544552`
|
|
68
|
+
- Write: `43480002-f001-4b49-4e47-204d45544552` (`WRITE WITHOUT RESPONSE`)
|
|
69
|
+
- Notify: `43480003-f001-4b49-4e47-204d45544552` (`NOTIFY`)
|
|
70
|
+
- Read: `43480004-f001-4b49-4e47-204d45544552` (`READ`, no useful content observed)
|
|
71
|
+
|
|
72
|
+
## King Meter protocol
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
[TYPE] [00] [LEN] [CMD] [PAYLOAD…] [CRC_HI] [CRC_LO] [45]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Element | Meaning |
|
|
79
|
+
|---|---|
|
|
80
|
+
| TYPE | `0x62` read, `0x63` write |
|
|
81
|
+
| `00` | constant header byte |
|
|
82
|
+
| LEN | length of CMD + PAYLOAD; frame length is LEN + 6 |
|
|
83
|
+
| CRC | CRC-16/MODBUS (`0xA001` reflected, init `0xFFFF`) over TYPE…PAYLOAD, **high byte first** |
|
|
84
|
+
| `45` | end marker |
|
|
85
|
+
|
|
86
|
+
A read request lists field keys; the reply returns `[key][size][value]` entries, big endian. Replies (54 bytes for status) exceed the default 20-byte notification payload and are reassembled.
|
|
87
|
+
|
|
88
|
+
Status request (`CMD 0x0B`), as sent by the app: `62 00 0c 0b 06 07 08 09 11 0a 0b 0c 0f 23 4f 84 91 45`
|
|
89
|
+
|
|
90
|
+
| Key | Size | Meaning | Status |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| `0x06` | 2 | current speed, 0.1 km/h | plausible, 0 while parked |
|
|
93
|
+
| `0x07` | 2 | max speed, 0.1 km/h | matches app |
|
|
94
|
+
| `0x08` | 2 | average speed, 0.1 km/h | matches app |
|
|
95
|
+
| `0x09` | 4 | odometer, 0.1 km | matches app |
|
|
96
|
+
| `0x0A` | 4 | ride time, s | matches app |
|
|
97
|
+
| `0x0B` | 2 | unknown (655 at 68 %; range in 0.1 km?) | open |
|
|
98
|
+
| `0x0C` | 4 | trip, 0.1 km | matches app |
|
|
99
|
+
| `0x0F` | 1 | traction battery, % | plausible |
|
|
100
|
+
| `0x11` | 1 | unknown | open |
|
|
101
|
+
| `0x23` | 2 | unknown | open |
|
|
102
|
+
| `0x4F` | 1 | unknown | open |
|
|
103
|
+
|
|
104
|
+
Device info request (`CMD 0x04`, keys `04 05 4f 70`): key `0x04` holds three version bytes (`2.20.16` observed; the app shows `1.4.0` elsewhere, so this may be another component), key `0x70` the ASCII serial number.
|
|
105
|
+
|
|
106
|
+
The app also writes a clock sync (`TYPE 0x63`, `CMD 0x01`, key `0x3C`). This library deliberately does not implement it.
|
|
107
|
+
|
|
108
|
+
## Install for development
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python3 -m venv .venv
|
|
112
|
+
source .venv/bin/activate
|
|
113
|
+
python -m pip install -U pip
|
|
114
|
+
pip install -e '.[dev]'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
On Linux, BlueZ and a working Bluetooth adapter are required.
|
|
118
|
+
|
|
119
|
+
## CLI
|
|
120
|
+
|
|
121
|
+
Scan for likely TENWAYS devices:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
tenways scan
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Read live telemetry (traction battery, odometer, …):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
tenways status E8:88:6C:31:B3:0D
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Read firmware and serial number:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
tenways device-info E8:88:6C:31:B3:0D
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Read the standard BLE battery percentage (not the traction battery):
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
tenways battery E8:88:6C:31:B3:0D
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Show basic GATT information and battery level:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
tenways info E8:88:6C:31:B3:0D
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Subscribe to the proprietary notification characteristic:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
tenways listen E8:88:6C:31:B3:0D
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Stop with `Ctrl+C`.
|
|
158
|
+
|
|
159
|
+
## Optional HCI trace on Linux
|
|
160
|
+
|
|
161
|
+
For traffic generated by this library/CLI on the same Linux Bluetooth controller, BlueZ `btmon` is useful:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
sudo btmon -i hci0 -w tenways.btsnoop
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Then run the CLI in another shell. The resulting `tenways.btsnoop` file can be inspected with Wireshark.
|
|
168
|
+
|
|
169
|
+
This does **not** passively capture a BLE connection between an Android phone and the bicycle. For the original TENWAYS app, use Android HCI snoop logging or a dedicated BLE sniffer.
|
|
170
|
+
|
|
171
|
+
## Safety
|
|
172
|
+
|
|
173
|
+
`tenways-ble` 0.1.0 never writes arbitrary data to the proprietary TENWAYS write characteristic. Reverse engineering should start from observed traffic rather than guessed commands.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
178
|
+
|
|
179
|
+
Never publish a raw phone HCI log; it contains traffic and identifiers of other devices.
|
|
180
|
+
|
|
181
|
+
## Releasing
|
|
182
|
+
|
|
183
|
+
Releases are published to PyPI by GitHub Actions via trusted publishing when a `v*` tag is pushed. The tag must match `__version__`.
|
|
184
|
+
|
|
185
|
+
## Acknowledgements / Credits
|
|
186
|
+
|
|
187
|
+
Based on and inspired by [stein155/cgo600-bridge](https://github.com/stein155/cgo600-bridge), which provided valuable groundwork for understanding the proprietary TENWAYS telemetry protocol and its integration with Home Assistant.
|
|
188
|
+
|
|
189
|
+
Many thanks to the original author and contributors.
|
|
190
|
+
|
|
191
|
+
The Longtail Duo implementation was developed using independently captured Bluetooth HCI traffic from the official TENWAYS app. It uses the King Meter BLE transport rather than the Nordic UART Service used by the CGO600 bridge.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# tenways-ble
|
|
2
|
+
|
|
3
|
+
Small experimental Python library and CLI for local BLE access to TENWAYS e-bikes.
|
|
4
|
+
|
|
5
|
+
Read-only by design:
|
|
6
|
+
|
|
7
|
+
- discovers nearby TENWAYS BLE devices;
|
|
8
|
+
- decodes the proprietary **King Meter** frame protocol (CRC-16/MODBUS, TLV fields) used by the TENWAYS Longtail Duo;
|
|
9
|
+
- reads live telemetry: traction battery, odometer, trip, max/average speed, ride time;
|
|
10
|
+
- reads firmware and serial number;
|
|
11
|
+
- reads the standard Bluetooth Battery Level characteristic (`0x2A19`), which is **not** the traction battery;
|
|
12
|
+
- does **not** send any write, lock, light or configuration commands.
|
|
13
|
+
|
|
14
|
+
The protocol was reconstructed from an Android HCI capture of the official TENWAYS app; all test vectors are the original captured frames. Live operation against the bike is still being validated.
|
|
15
|
+
|
|
16
|
+
## Library usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
from tenways_ble import TenwaysBike
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
async with TenwaysBike("E8:88:6C:31:B3:0D") as bike:
|
|
24
|
+
status = await bike.read_status()
|
|
25
|
+
print(status.battery_percent, status.odometer_km)
|
|
26
|
+
|
|
27
|
+
asyncio.run(main())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`TenwaysBike` also accepts an already connected `BleakClient` (for example from `bleak-retry-connector` in Home Assistant). It then only speaks the protocol and leaves the connection to the caller.
|
|
31
|
+
|
|
32
|
+
The sans-I/O protocol module `tenways_ble.protocol` builds and parses frames without any Bluetooth dependency.
|
|
33
|
+
|
|
34
|
+
## Devices
|
|
35
|
+
|
|
36
|
+
| Device | Advertised name | Notes |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| Longtail Duo drive | `Longtail Duo` | King Meter service, telemetry source. Standard battery reads ~1 %, not the traction battery. |
|
|
39
|
+
| Separate BLE device | `Tenways-ble-XXXXXX` | HID service, standard battery 100 %, also exposes the King Meter service. Function unconfirmed (remote/control unit?). |
|
|
40
|
+
|
|
41
|
+
## Known GATT layout
|
|
42
|
+
|
|
43
|
+
Standard battery service:
|
|
44
|
+
|
|
45
|
+
- Service: `0000180f-0000-1000-8000-00805f9b34fb`
|
|
46
|
+
- Battery Level: `00002a19-0000-1000-8000-00805f9b34fb` (`READ`, `NOTIFY`)
|
|
47
|
+
|
|
48
|
+
King Meter service (`43480001-…` contains `KING METER` in ASCII):
|
|
49
|
+
|
|
50
|
+
- Service: `43480001-f001-4b49-4e47-204d45544552`
|
|
51
|
+
- Write: `43480002-f001-4b49-4e47-204d45544552` (`WRITE WITHOUT RESPONSE`)
|
|
52
|
+
- Notify: `43480003-f001-4b49-4e47-204d45544552` (`NOTIFY`)
|
|
53
|
+
- Read: `43480004-f001-4b49-4e47-204d45544552` (`READ`, no useful content observed)
|
|
54
|
+
|
|
55
|
+
## King Meter protocol
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
[TYPE] [00] [LEN] [CMD] [PAYLOAD…] [CRC_HI] [CRC_LO] [45]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Element | Meaning |
|
|
62
|
+
|---|---|
|
|
63
|
+
| TYPE | `0x62` read, `0x63` write |
|
|
64
|
+
| `00` | constant header byte |
|
|
65
|
+
| LEN | length of CMD + PAYLOAD; frame length is LEN + 6 |
|
|
66
|
+
| CRC | CRC-16/MODBUS (`0xA001` reflected, init `0xFFFF`) over TYPE…PAYLOAD, **high byte first** |
|
|
67
|
+
| `45` | end marker |
|
|
68
|
+
|
|
69
|
+
A read request lists field keys; the reply returns `[key][size][value]` entries, big endian. Replies (54 bytes for status) exceed the default 20-byte notification payload and are reassembled.
|
|
70
|
+
|
|
71
|
+
Status request (`CMD 0x0B`), as sent by the app: `62 00 0c 0b 06 07 08 09 11 0a 0b 0c 0f 23 4f 84 91 45`
|
|
72
|
+
|
|
73
|
+
| Key | Size | Meaning | Status |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `0x06` | 2 | current speed, 0.1 km/h | plausible, 0 while parked |
|
|
76
|
+
| `0x07` | 2 | max speed, 0.1 km/h | matches app |
|
|
77
|
+
| `0x08` | 2 | average speed, 0.1 km/h | matches app |
|
|
78
|
+
| `0x09` | 4 | odometer, 0.1 km | matches app |
|
|
79
|
+
| `0x0A` | 4 | ride time, s | matches app |
|
|
80
|
+
| `0x0B` | 2 | unknown (655 at 68 %; range in 0.1 km?) | open |
|
|
81
|
+
| `0x0C` | 4 | trip, 0.1 km | matches app |
|
|
82
|
+
| `0x0F` | 1 | traction battery, % | plausible |
|
|
83
|
+
| `0x11` | 1 | unknown | open |
|
|
84
|
+
| `0x23` | 2 | unknown | open |
|
|
85
|
+
| `0x4F` | 1 | unknown | open |
|
|
86
|
+
|
|
87
|
+
Device info request (`CMD 0x04`, keys `04 05 4f 70`): key `0x04` holds three version bytes (`2.20.16` observed; the app shows `1.4.0` elsewhere, so this may be another component), key `0x70` the ASCII serial number.
|
|
88
|
+
|
|
89
|
+
The app also writes a clock sync (`TYPE 0x63`, `CMD 0x01`, key `0x3C`). This library deliberately does not implement it.
|
|
90
|
+
|
|
91
|
+
## Install for development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python3 -m venv .venv
|
|
95
|
+
source .venv/bin/activate
|
|
96
|
+
python -m pip install -U pip
|
|
97
|
+
pip install -e '.[dev]'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
On Linux, BlueZ and a working Bluetooth adapter are required.
|
|
101
|
+
|
|
102
|
+
## CLI
|
|
103
|
+
|
|
104
|
+
Scan for likely TENWAYS devices:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
tenways scan
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Read live telemetry (traction battery, odometer, …):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
tenways status E8:88:6C:31:B3:0D
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Read firmware and serial number:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
tenways device-info E8:88:6C:31:B3:0D
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Read the standard BLE battery percentage (not the traction battery):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
tenways battery E8:88:6C:31:B3:0D
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Show basic GATT information and battery level:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
tenways info E8:88:6C:31:B3:0D
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Subscribe to the proprietary notification characteristic:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
tenways listen E8:88:6C:31:B3:0D
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Stop with `Ctrl+C`.
|
|
141
|
+
|
|
142
|
+
## Optional HCI trace on Linux
|
|
143
|
+
|
|
144
|
+
For traffic generated by this library/CLI on the same Linux Bluetooth controller, BlueZ `btmon` is useful:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
sudo btmon -i hci0 -w tenways.btsnoop
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Then run the CLI in another shell. The resulting `tenways.btsnoop` file can be inspected with Wireshark.
|
|
151
|
+
|
|
152
|
+
This does **not** passively capture a BLE connection between an Android phone and the bicycle. For the original TENWAYS app, use Android HCI snoop logging or a dedicated BLE sniffer.
|
|
153
|
+
|
|
154
|
+
## Safety
|
|
155
|
+
|
|
156
|
+
`tenways-ble` 0.1.0 never writes arbitrary data to the proprietary TENWAYS write characteristic. Reverse engineering should start from observed traffic rather than guessed commands.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
161
|
+
|
|
162
|
+
Never publish a raw phone HCI log; it contains traffic and identifiers of other devices.
|
|
163
|
+
|
|
164
|
+
## Releasing
|
|
165
|
+
|
|
166
|
+
Releases are published to PyPI by GitHub Actions via trusted publishing when a `v*` tag is pushed. The tag must match `__version__`.
|
|
167
|
+
|
|
168
|
+
## Acknowledgements / Credits
|
|
169
|
+
|
|
170
|
+
Based on and inspired by [stein155/cgo600-bridge](https://github.com/stein155/cgo600-bridge), which provided valuable groundwork for understanding the proprietary TENWAYS telemetry protocol and its integration with Home Assistant.
|
|
171
|
+
|
|
172
|
+
Many thanks to the original author and contributors.
|
|
173
|
+
|
|
174
|
+
The Longtail Duo implementation was developed using independently captured Bluetooth HCI traffic from the official TENWAYS app. It uses the King Meter BLE transport rather than the Nordic UART Service used by the CGO600 bridge.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Research leads
|
|
2
|
+
|
|
3
|
+
Leads for decoding more of the Longtail Duo. **Nothing here is confirmed for
|
|
4
|
+
the King Meter BLE interface (`43480001-…`)**; only frames captured from the
|
|
5
|
+
official TENWAYS app count as evidence. Never derive write commands from these
|
|
6
|
+
sources.
|
|
7
|
+
|
|
8
|
+
## Hardware chain (hypothesis)
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
TENWAYS app / Home Assistant
|
|
12
|
+
│ King Meter BLE (43480001-…) — confirmed by HCI capture
|
|
13
|
+
Display / BLE module — possibly DigiTech BC351
|
|
14
|
+
│ internal bus, probably CAN
|
|
15
|
+
Bafang M410 drive (250 W, 80 Nm, integrated controller) — confirmed by TENWAYS
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The display may forward only a subset of motor data over BLE, or may answer
|
|
19
|
+
further, not yet captured BLE field keys. CAN frames cannot be assumed to pass
|
|
20
|
+
through the BLE channel.
|
|
21
|
+
|
|
22
|
+
## King Meter display protocols (serial, not BLE)
|
|
23
|
+
|
|
24
|
+
- [stancecoke/BMSBattery_S_controllers_firmware `display_kingmeter.c`](https://github.com/stancecoke/BMSBattery_S_controllers_firmware/blob/Master/display_kingmeter.c)
|
|
25
|
+
— C implementation of the 901U/KM5S display protocol, including a handshake
|
|
26
|
+
and OEM-specific handshake tables.
|
|
27
|
+
- [EBiCS/EBiCS_Firmware](https://github.com/EBiCS/EBiCS_Firmware) — separate
|
|
28
|
+
implementations for KINGMETER_618U, KINGMETER_901U, Bafang LCD and TFT.
|
|
29
|
+
- [King Meter J-LCD5S protocol (PDF)](https://www.emubikes.com/wp-content/uploads/2020/12/J-LCD5S-protocol-From-KING-METER_1589857045208.pdf)
|
|
30
|
+
— speed, battery, assist levels, error codes, lights. Useful for guessing
|
|
31
|
+
which data exists, not for BLE field keys.
|
|
32
|
+
|
|
33
|
+
No public source found that documents the King Meter BLE UUID or the
|
|
34
|
+
`62 00 … CRC 45` framing.
|
|
35
|
+
|
|
36
|
+
## Bafang CAN (different transport)
|
|
37
|
+
|
|
38
|
+
- [speedysheep/ebike-protocols `BAFANG_CAN_PROTOCOL.md`](https://github.com/speedysheep/ebike-protocols/blob/main/bafang/BAFANG_CAN_PROTOCOL.md)
|
|
39
|
+
— reverse-engineered Bafang CAN codes, e.g. `0x32/0x00` (state of charge,
|
|
40
|
+
cadence, torque, remaining range), `0x32/0x01` (speed, motor current, battery
|
|
41
|
+
voltage, controller and motor temperature; 16-bit little endian, temperatures
|
|
42
|
+
offset −40 °C), `0x63/0x00` (assist level, mode, light bits), `0x64/0x02–05`
|
|
43
|
+
(cell voltages, mV).
|
|
44
|
+
- [andrey-pr/OpenBafangTool](https://github.com/andrey-pr/OpenBafangTool) —
|
|
45
|
+
diagnosis and configuration for Bafang UART and CAN.
|
|
46
|
+
- [Kaspars Dambis: Bafang CAN bus programming](https://kaspars.net/blog/bafang-canbus)
|
|
47
|
+
— CAN adapter, HIGO connectors, 250 kbit/s, SocketCAN; comments mention a
|
|
48
|
+
TENWAYS AGO-X with M410.
|
|
49
|
+
|
|
50
|
+
A direct CAN capture would be a later, separate option and needs opening the
|
|
51
|
+
bike's wiring. Not planned for now.
|
|
52
|
+
|
|
53
|
+
## Display: DigiTech BC351 (unconfirmed)
|
|
54
|
+
|
|
55
|
+
A dealer lists the Longtail Duo display as DigiTech BC351 with Bluetooth. Not
|
|
56
|
+
an official TENWAYS statement for every hardware revision.
|
|
57
|
+
|
|
58
|
+
- 3.5" colour display, UART or CAN, optional Bluetooth 4.2 and GPS, vendor app
|
|
59
|
+
"Bikewise Pro".
|
|
60
|
+
- [DigiTech B-COCKPiT BC351 manual](https://www.manualslib.com/manual/3148608/Digitech-B-Cockpit-Bc351.html)
|
|
61
|
+
(documents Bluetooth pairing via QR code)
|
|
62
|
+
- [HEPHA BC351 manual](https://www.manualslib.de/manual/1351570/Hepha-Bc351.html)
|
|
63
|
+
— OEM display on the same base.
|
|
64
|
+
|
|
65
|
+
Do not treat DigiTech and King Meter as the same manufacturer without evidence.
|
|
66
|
+
If Bikewise Pro talks to the bike, an HCI capture of that app could reveal more
|
|
67
|
+
BLE commands.
|
|
68
|
+
|
|
69
|
+
## Open status fields and tests
|
|
70
|
+
|
|
71
|
+
| Field | Captured | Next test |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `0x0B` | 655 | Compare with range shown on display; does it fall while riding? |
|
|
74
|
+
| `0x11` | 0 | Change assist level on the parked bike, read again |
|
|
75
|
+
| `0x23` | 0 | Toggle lights, read again |
|
|
76
|
+
| `0x4F` | 1 | Correlate with display/power states |
|
|
77
|
+
| `0x06` | 0 | Capture while riding (safely, supervised) to confirm current speed |
|
|
78
|
+
|
|
79
|
+
These are hypotheses. A key number matching a Bafang CAN code does not imply
|
|
80
|
+
the same meaning in the BLE protocol.
|
|
81
|
+
|
|
82
|
+
## Capture procedure
|
|
83
|
+
|
|
84
|
+
Android HCI snoop log (`mSnoopLogSettingAtEnable = full`), retrieved via bug
|
|
85
|
+
report from `FS/data/misc/bluetooth/logs/btsnoop_hci.log`. Trigger exactly one
|
|
86
|
+
action in the app per capture and note the time. Never publish a raw capture.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tenways-ble"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Read-only BLE client, King Meter protocol and diagnostic CLI for TENWAYS e-bikes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
keywords = ["tenways", "ebike", "bluetooth", "ble", "king-meter", "home-assistant"]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Christoph Berliner" }
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"bleak>=0.22",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8",
|
|
23
|
+
"pytest-asyncio>=0.23",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/Styx85/tenways-ble"
|
|
28
|
+
Issues = "https://github.com/Styx85/tenways-ble/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
tenways = "tenways_ble.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/tenways_ble"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
addopts = "-q"
|
|
38
|
+
asyncio_mode = "auto"
|
|
39
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
TenwaysBatteryError,
|
|
3
|
+
TenwaysBike,
|
|
4
|
+
TenwaysError,
|
|
5
|
+
TenwaysTimeoutError,
|
|
6
|
+
TenwaysUnsupportedError,
|
|
7
|
+
scan_tenways,
|
|
8
|
+
)
|
|
9
|
+
from .models import TenwaysAdvertisement, TenwaysInfo
|
|
10
|
+
from .protocol import TenwaysDeviceInfo, TenwaysProtocolError, TenwaysStatus
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"TenwaysAdvertisement",
|
|
14
|
+
"TenwaysBatteryError",
|
|
15
|
+
"TenwaysBike",
|
|
16
|
+
"TenwaysDeviceInfo",
|
|
17
|
+
"TenwaysError",
|
|
18
|
+
"TenwaysInfo",
|
|
19
|
+
"TenwaysProtocolError",
|
|
20
|
+
"TenwaysStatus",
|
|
21
|
+
"TenwaysTimeoutError",
|
|
22
|
+
"TenwaysUnsupportedError",
|
|
23
|
+
"scan_tenways",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
__version__ = "0.2.0"
|