This document is my notebook for analyzing the display bus protocol. The finished analysis will be polished and compacted and released in another file.
The scooter's display, marked HW6173_LCD1_V1.4 has an on-board BLE module (chip label unfortunately not readable), supported by an 8051 based "CMS8S5880" microcotroller. I haven't bothered yet to reverse engineer the entire pcb as it isn't needed for my purpose.
The pinout of it's 4-pin connector to the motor driver is as follows:
Pin | Label | Function |
---|---|---|
1 | TX | Combined RX/TX |
2 | - | GND, likely switched |
3 | + | VCC (roughly 12 V when ON) |
4 | Key | Shorted to + when button is pressed |
The "TX" pin is interesting as it is used for bidirectional communication, continuing the MODBUS approach. It is using 3.3 V logic levels. The display is powered by roughly 12 V supplied by the motor controller, whenever the "Key" signal is activated or when the display keeps on actively communicating. This needs further research down the line and I expect the same kind of protocols used as in the BLE telemetry (only other more low-level stuff like accelerator position and light outputs on/off).
Hooking up a Logic Analyzer to the TX and - pins i got the Serial Interface parameters fairly quickly:
115200 baud/s, 8N1. This is excellent for signal analysis and command injection, should that be even necessary.
The protocol looks at first glance somewhat like the protocol used for the BLE telemetry, starting with an address and function code byte. There are two distinctly different messages sent by the display and the controller, both differing in length. I'm assuning that the short message is sent by the display and the long message is sent by the controller. I haven't yet disconnected the display to find out.
Example:
Request (Display): 01 17 00 26 00 0c 00 2b 00 01 02 00 00 72 59
Response (Controller): 01 17 00 26 18 01 a2 00 00 00 00 0d 02 00 00 00 00 00 5a 00 fb 11 b7 69 d6 00 01 00 00 8e ce
The frame interval between each request/response pair is approcximately 20 ms, contributing to the excellent control feeling of the accelerator/brake (they respond without any noticeable delay when driving).
During experimenting I recorded the startup sequence when you hold down the button to turn the scooter off and on. Upon holding the button to turn the scooter on, the following sequence happens on the bus:
Request 01 17 00 26 00 0c 00 2b 00 01 02 00 00 72 59
is sent.
Response 01 17 00 26 18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ca 9f
is sent ONCE (24 times 0x00)
Note: This seems to be similar to the otherr protocol heads, although with a slightly different structure:
Byte | Guess of function |
---|---|
01 | Address |
17 | Function Code/Subadress (MODBUS "Report Slave ID") |
00 26 | Starting address |
18 | Number of data bytes (excluding head and checksum) |
(24 times 00) | data |
ca 9f | checksum, MODBUS-CRC16 Big Endian |
Request 01 17 00 26 00 0c 00 2b 00 01 02 00 00 72 59
is sent and repeated 65 times over ~1.5 seconds until the controller turns on
Magic 30 32 30 32 31 38 30 32
this magic sequence is sent EXACTLY ONCE (origin yet unknown), after which the controller and display actively communicate
It corresponds to "02021802" in ASCII, the meaning of which is the "Controller Code" displayed in the app's firmware upgrade menu.
After power-up, the following sequences can be observed (accelerator, electronic brake and handbrake in resting state, scooter at resting state).
The Request 01 17 00 26 00 0c 00 2b 00 01 02 00 00 72 59
(Labeling as RQ1, this always stays the same and is always sent before a response arrives)
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0d 02 00 00 00 00 00 e3 00 db 11 b7 69 d6 00 01 00 00 6f 51
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0d 02 00 00 00 00 01 3d 01 26 11 b7 69 d6 00 01 00 00 03 e8
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0d 02 00 00 00 00 00 e3 01 2e 11 b7 69 d6 00 01 00 00 05 c0
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0d 02 00 00 00 00 00 88 01 16 11 b7 69 d6 00 01 00 00 ed 24
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0d 02 00 00 00 00 00 5a 00 fb 11 b7 69 d6 00 01 00 00 8e ce
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0d 02 00 00 00 00 00 5a 00 fb 11 b7 69 d6 00 01 00 00 8e ce
After that, a differend kind of request was slipped inbetween the other request/response pairs:
Request 01 03 00 22 00 02 04 01 28
which looks suspiciously like the "UF" mode protocol
Response 01 03 00 22 04 03 05 0f 16 24 4c
After that, the "usual" request/response pairs reappeared:
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 2d 00 cc 11 b7 69 d6 00 01 00 00 1d b6
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 00 00 b9 11 b7 69 d6 00 01 00 00 d7 48
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 00 00 a8 11 b7 69 d6 00 01 00 00 17 18
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 00 00 9a 11 b7 69 d6 00 01 00 00 5a 79
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 00 00 8d 11 b7 69 d6 00 01 00 00 b1 89
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 00 00 82 11 b7 69 d6 00 01 00 00 f0 79
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 00 00 79 11 b7 69 d6 00 01 00 00 87 4d
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 00 00 70 11 b7 69 d6 00 01 00 00 ed 1d
RQ1/Response 01 17 00 26 18 01 a2 00 00 00 00 0c 02 00 00 00 00 00 00 00 69 11 b7 69 d6 00 01 00 00 4a 8d
RQ1/Response 01 17 00 26 18 01 a2 00 00 16 00 0c 02 00 00 00 00 00 00 00 63 11 b7 69 d6 00 01 00 00 7e d8
RQ1/Response 01 17 00 26 18 01 a2 00 00 16 00 0c 02 00 00 00 00 00 00 00 5c 11 b7 69 d6 00 01 00 00 6b 29
...
so far the only fields changing are databyte 4, likely represending a control bit and the 14...15th databytes that seem to be a falling or settling value.
Databyte 4 does not correspond to simple button presses.
Using the trigger function of the Saleae Logic software, i fiddled around with the scooter's controls until finding out which databytes are responsible for each function:
01 17 00 26 00 0c 00 2b 00 01 02 00 00 72 59
Byte | Databyte | Value | Confirmed Function |
---|---|---|---|
00 | 01 | Address | |
01 | 17 | Function Code (No MODBUS, maybe "store in register"?) | |
02 | 00 | starting register high-byte | |
03 | 26 | starting register low-byte | |
04 | 00 | ||
05 | 0c | ||
06 | 00 | 00 | |
07 | 01 | 2b | |
08 | 02 | 00 | |
09 | 03 | 01 | |
10 | 04 | 02 | |
11 | 05 | 00 | Throttle/Brake position High-Byte |
12 | 06 | 00 | Throttle/Brake position Low-Byte (See below) |
13 | 72 | Checksum High-byte | |
14 | 59 | Checksum Low-byte |
The throttle/brake position logic seems to be directly lifted from Hobbywing's RC controllers. The positions of both the throttle and brake, and also the hand brake switch, are linked together in one single signed 16-bit field. There is no extra bit for the hand brake switch.
Combined, this field clearly represents the center position of an RC control stick usually used for both throttle and acceleration.
Connecting bluetooth doesn't change the reqgular request data, but introduces the tiniest bit of jitter into the stream.
01 17 00 26 18 01 a0 00 00 18 00 0c 01 00 00 00 00 00 00 13 eb 11 e8 6b c0 00 01 00 64 6f 2d
Byte | Databyte | Value | Confirmed Function |
---|---|---|---|
00 | 01 | Address | |
01 | 17 | Reply to Function Code | |
02 | 00 | starting register high-byte | |
03 | 26 | starting register low-byte | |
04 | 18 | number of databytes | |
05 | 00 | 01 | Battery Voltage High-Byte |
06 | 01 | a0 | Battery Voltage Low-Byte V*0.1 (Example: 0x01a0=416=41.6 V) |
07 | 02 | 00 | Amperage High-Byte |
08 | 03 | 00 | Amperage Low-Byte A*0.01 (Example: 0x001F=31=0.31 A) |
09 | 04 | 18 | Controller temperature |
10 | 05 | 00 | |
11 | 06 | 0c | Corresponds with lock/unlock |
12 | 07 | 01 | Control bits (Bit 4 on when beeper is active, Bit 3= Light on/off, Bits 0..1: Gear select) |
13 | 08 | 00 | |
14 | 09 | 00 | |
15 | 10 | 00 | Copy of Request Throttle value High-Byte |
16 | 11 | 00 | Copy of Request Throttle value Low-Byte |
17 | 12 | 00 | Actual Speed High-Byte |
18 | 13 | 00 | Actual Speed Low-Byte, km/h*0.001 (e.g. 0x55F0=22000=22.000 km/h) |
19 | 14 | 13 | Settling value High-Byte |
20 | 15 | eb | Settling value Low-Byte, increases with wheel motion, decreases when wheel is stopped. Not related to power-off time-out. |
21 | 16 | 11 | Trip Distance Mid-Byte |
22 | 17 | e8 | Trip Distance Low-Byte |
23 | 18 | 6b | Total Distance Mid-Byte |
24 | 19 | c0 | Total Distance Low-byte |
25 | 20 | 00 | Trip Distance High-Byte, km*0.01 (e.g. 0x0011e8=4584=45.84 km; 0x001238=4664=46.64km) |
26 | 21 | 01 | Total Distance Mid2 Byte |
27 | 22 | 00 | Total Distance High Byte km*0.001 (e.g. 0x00016ee0=93920=93.920 km). Always counts up regardless of wheel direction |
28 | 23 | 64 | Battery SOC in % (0x64=100 %) |
29 | 6f | Checksum High-byte | |
30 | 2d | Checksum Low-Byte |
While having the app connected, setting the scooter to "locked" resulted in the following command injected into the regular datastream:
01 10 00 00 00 01 02 00 02 27 91
...resulting in the following response and the scooter locking up the front wheel drive:
01 10 00 00 00 01 01 c9
among other request/response pairs (not captured/analyzed)
Setting the scooter from "Kickstart" to "Zero Start" resulted in the following command injected into the regular datastream:
01 10 00 00 00 01 02 08 22 21 89
...resulting in the following responses and one single beep from the display:
01 10 00 00 00 01 01 c9
Another subsequent request made by the display:
01 17 00 22 00 02 00 22 00 02 04 03 05 0f 16 a8 82
which contains the now familiar sequence of 05 0f 16
corresponding to the three default gear speeds of 5, 15 and 22 km/h.
The controller responds with: 01 17 00 22 04 03 05 0f 16 24 b3
This didnt have any real consequences, since Kickstart is the default and only allowed mode.
After that, the usual RQ1/Response interrogation resumes.
Gear 3 is the usually selected gear when driving at maximum speed. You can choose a speed between 6 and 20 (22) km/h in the app.
Setting the scooter's maximum speed from 22 to 6 km/h in gear 3 (light off, unlocked, kickstart, km/h display) results in the following command injected into the regular datastream:
Request: 01 10 00 00 00 01 02 08 02 20 51
(very likely control bits or a magic sequence for allowing a change in value)
Response: 01 10 00 00 00 01 01 c9
Request: 01 17 00 22 00 02 00 22 00 02 04 03 05 0f 06 a9 4e
contains the now changed sequence 05 0f 06
Response: 01 17 00 22 04 03 05 0f 06 25 7f
The controller repeats what it got
After that, the usual RQ1/Response interrogation resumes.
Changing into UF mode using Uniscooter's extended settings not available in the ePowerFun app changes the interrogation scheme quite a bit. The interrogation frame interval is increased to 250 ms.
Requests in the following form are made:
01 03 00 XX 00 YY CS CS
where XX is a constantly increasing number (from 0x00 to 0x90 in 0x10 size steps). YY is 0x10 for most of the requests, only for XX=0x40 it is 0x0F.
Req: 01 03 00 00 00 10 44 06
(MODBUS Addr. 01, Function Code 3 read holding regs, start at 0x0000, read 0x0010 (16) registers)
Response: 01 03 00 00 20 08 03 00 00 55 55 00 00 00 0f 0e d8 09 99 0a 66 13 88 75 30 52 08 04 e0 04 00 1f 40 02 58 03 20 2b 9f
Since this reads an entire memory map, I will list it here as a table without the header and checksums:
XX | Data |
---|---|
++ | +0000 +0001 +0002 +0003 +0004 +0005 +0006 +0007 +0008 +0009 +000a +000b +000c +000d +000e +000f |
00 | 08 03 00 00 55 55 00 00 00 0f 0e d8 09 99 0a 66 13 88 75 30 52 08 04 e0 04 00 1f 40 02 58 03 20 |
10 | 3e 80 03 20 3e 80 01 36 01 d1 00 01 6a aa 00 f1 c0 3d 00 27 63 35 00 06 03 fd 00 00 00 00 00 00 |
20 | 00 3c 00 01 03 05 0f 16 00 00 00 00 01 9d 00 00 19 00 0c 00 00 00 2c 12 00 00 10 e5 12 bb 73 fe |
30 | 00 01 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 aa aa |
40 | 07 b8 07 c5 07 c2 00 00 00 00 00 00 00 00 00 00 00 00 12 70 00 00 00 00 00 00 00 00 00 00 |
50 | 00 00 00 00 00 00 00 61 01 04 00 03 20 50 00 00 00 00 00 00 61 00 04 01 03 00 50 20 00 00 00 00 |
60 | 00 00 00 61 01 04 00 03 20 50 00 00 00 00 27 8e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
70 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
80 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
90 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
changing extended settings in UF mode (bridge mode, direct interfacing to controller through bluetooth)
Changing a setting in the app while in "UF" mode, a bit of lag is introduced into the communication followed by a request of the following form:
01 17 00 09 00 01 00 09 00 01 02 3a 98 97 12
with the controller responding with: 01 17 00 09 02 3a 98 b9 b1
I need to collect a few examples to be sure what does what.
Starting with settings "Acceleration response=10", "Brake response=10", "Max. speed=22 km/h"
changing acceleration response from 10 to 9
Request: 01 17 00 09 00 01 00 09 00 01 02 69 78 aa 6a
Response: 01 17 00 09 02 69 78 84 c9
changing acceleration response from 9 to 8
Request: 01 17 00 09 00 01 00 09 00 01 02 5d c0 bc d8
Response: 01 17 00 09 02 5d c0 92 7b
changing acceleration response from 8 to 7
Request: 01 17 00 09 00 01 00 09 00 01 02 52 08 b8 be
Response: 01 17 00 09 02 52 08 96 1d
changing acceleration response from 7 to 10
Request: 01 17 00 09 00 01 00 09 00 01 02 75 30 a2 9c
Response: 01 17 00 09 02 75 30 8c 3f
It seems like the different acceleration response settings are actually 16-bit preset values.
changing brake response from 10 to 9
Request: 01 17 00 0a 00 01 00 0a 00 01 02 69 78 5a 56
Response: 01 17 00 0a 02 69 78 84 8d
changing brake response from 9 to 1
Request: 01 17 00 0a 00 01 00 0a 00 01 02 0b b8 73 66
Response: 01 17 00 0a 02 0b b8 ad bd
changing brake response from 1 to 10
Request: 01 17 00 0a 00 01 00 0a 00 01 02 75 30 52 a0
Response: 01 17 00 0a 02 75 30 8c 7b
It seems like the different brake response settings are actually 16-bit preset values that are identical to the acceleration response values.
They range between 0x0bb8
and 0x7530
, 3000 to 30000 in decimal.
This maximum speed limit parameter is a different parameter than the three individually settable gear speeds. If this parameter is set lower than for example gear 3's speed value, this parameter limits the top speed (even though gear 3 would technically allow a faster speed).
changing maximum speed from 22 to 20 km/h
Request: 01 17 00 20 00 01 00 20 00 01 02 00 c8 53 32
Response 01 17 00 20 02 00 c8 a3 71
changing maximum speed from 20 to 19 km/h
Request: 01 17 00 20 00 01 00 20 00 01 02 00 be 53 32
Response 01 17 00 20 02 00 be a3 71
changing maximum speed from 19 to 10 km/h
Request: 01 17 00 20 00 01 00 20 00 01 02 00 64 53 4f
Response 01 17 00 20 02 00 64 a3 0c
It looks like the speed limiter is set in 16-bit values as well. The scaling is km/h*0.1, with the values lining up neatly:
0xc8=200=20.0 km/h, 0xbe=190=19.0 km/h, 0x64=100=10.0 km/h.
In theory, Request 01 17 00 20 00 01 00 20 00 01 02 00 ff 12 e4
sets the scooter to 25.5 km/h. The firmware limits it to 22.0 km/h, keeping it within legal bounds. This corresponds with the value in Uniscooter jumping back to 22 km/h if you set it higher than 22.