Jump to content

Reverse engineering XIMA bluetooth protocol


Kevin

Recommended Posts

I have a BLE sniffer that I've used to capture some packets going between a IPS XIMA LHOTZ wheel and my iPhone. My goal is to reverse engineer them with the following goals:

1) Better understand the existing app and whether some of its bugs are fixable or not

2) Develop a more user friendly app for iOs, possibly add features like map/GPS tracking in the distant future

3) Investigate the possibility of writing a HUD app for some kind of smart glasses

I'll be looking at this in my spare time, but if anyone else is interested in taking a look I can provide what data I capture :)

 

**UPDATE: I've created an iOs app based on the findings in this thread here:

I would like to release the source code at some point in the future, but need to do some cleanup first and I'm feeling lazy right now :)

 

Link to comment
Share on other sites

I could take a look once I have a little more time, just drop me the captured data. Also if there's an Android-app available, I could take a look at that too, if it isn't obfuscated, it usually helps with deciphering the data. The downside on "raw" data captures is that the captured data also contains the BT-protocol frame data, which must be cleaned / picked out to find out the real data moving between the wheel and the app, Wireshark can do this, but I didn't find an option to export the payload only. Easier if you can connect directly to the wheel from you computer with an USB-dongle and custom software just to record the payload, or write a simple app for the phone that does that.

Link to comment
Share on other sites

These are the capture files I got from WireShark. When I view it in wireshark it parses out the frame data nicely, though I think I have a plugin installed with the sniffer dongle that may be responsible for that.

Unfortunately I don't see an android app :(

So far I've been able to determine the following:

  1. The 'service' UUID is FF00
  2. There are only 2 BLE characteristics, with UUID's FF01 and FF02, neither of which is readable. FF01 is subscribable for notifications.
  3. In the traces, it appears there are a lot of writes of value 0x100 to handle 0x000c, I believe this is the handle for the 'client characteristic configuration' descriptor on the FF01 characteristic (the characteristic handle is 0x000a and value handle is 0x000b). So although it is spamming this characteristic, I don't think it's having any effect after the first one.
  4. The important bit seems to be a write to handle 0x000e, which appears to be the value handle for FF02. This is always followed by a notification on handle 0x000b, i.e. the value-handle for FF01.
    1. I suspect the format of the write to 0x000e is 0x9000[1-byte op code][some payload]. The response on 0x000b always begins with the same 0x9000xx code as the write to 0x000e, however sometimes it appears multiple 0x9000xx responses are encoded in a single notification.
    2. 0x900001 probably corresponds to "read speed", as it appears at about 0.5 second intervals, the same speed as the speedometer updates in the app. However I haven't been able to pick out with certainty how to parse the payload of the response, as 0kph appears to be 0x[opcode]0000010b.

The information that the app displays are:

  • Firmware version
  • Battery %
  • Speed (kph)
  • Speed limit (it starts at '--' until a wheel connects)
  • 'Full riding mileage'

xima_hispeed.pcapng

xima_lowspeed.pcapng

xima1.pcapng

Link to comment
Share on other sites

These are the capture files I got from WireShark. When I view it in wireshark it parses out the frame data nicely, though I think I have a plugin installed with the sniffer dongle that may be responsible for that.

Yup, for anyone else wanting to give a try, one is available here for Windows:  https://www.nordicsemi.com/eng/Products/Bluetooth-Smart-Bluetooth-low-energy/nRF-Sniffer    Extract, grab the plugin-dll under Sniffer\plugins\1.12\windows\<x86 or x64>\ and place it under Wireshark\plugins\<versionnumber>\ 

Unfortunately I don't see an android app :(

It appears there isn't one yet available.

So far I've been able to determine the following:

  1. The 'service' UUID is FF00
  2. There are only 2 BLE characteristics, with UUID's FF01 and FF02, neither of which is readable. FF01 is subscribable for notifications.
  3. In the traces, it appears there are a lot of writes of value 0x100 to handle 0x000c, I believe this is the handle for the 'client characteristic configuration' descriptor on the FF01 characteristic (the characteristic handle is 0x000a and value handle is 0x000b). So although it is spamming this characteristic, I don't think it's having any effect after the first one.
  4. The important bit seems to be a write to handle 0x000e, which appears to be the value handle for FF02. This is always followed by a notification on handle 0x000b, i.e. the value-handle for FF01.
    1. I suspect the format of the write to 0x000e is 0x9000[1-byte op code][some payload]. The response on 0x000b always begins with the same 0x9000xx code as the write to 0x000e, however sometimes it appears multiple 0x9000xx responses are encoded in a single notification.
    2. 0x900001 probably corresponds to "read speed", as it appears at about 0.5 second intervals, the same speed as the speedometer updates in the app. However I haven't been able to pick out with certainty how to parse the payload of the response, as 0kph appears to be 0x[opcode]0000010b.

Nice start, I'll see if I can dig out anything coherent about the values just by looking at the captures, although might not have time for that until maybe later tonight, if even then.

 

The information that the app displays are:

  • Firmware version
  • Battery %
  • Speed (kph)
  • Speed limit (it starts at '--' until a wheel connects)
  • 'Full riding mileage'

xima_hispeed.pcapng

xima_lowspeed.pcapng

xima1.pcapng

It would help to know what sort of values to look for, "hispeed" and "lowspeed" don't really tell much, what sort of speeds were there, what about the battery voltages (or percentages), should we look for values closer to full (say, above 64V) or empty (lower than 60V), what was the mileage when the captures were taken..? ;) 

Link to comment
Share on other sites

It would help to know what sort of values to look for, "hispeed" and "lowspeed" don't really tell much, what sort of speeds were there, what about the battery voltages (or percentages), should we look for values closer to full (say, above 64V) or empty (lower than 60V), what was the mileage when the captures were taken..? ;) 

Haha right, sorry it was super late when I posted.

Here's a couple screenshots of what the app shows, the values should be basically the same as they were last night since the unit was on full charge and I was just pushing it back and forth with my hands. I'm not 100% sure that the mileage will correspond to the figure shown - when I've been riding for a while and connect, it first shows one number, then updates to a slightly higher number, then a much higher number - however when I reconnect it will then show one of the two lower numbers again.

'lowspeed' would have been with me pushing it back and forth around 0.1 or 0.2kph, whereas with 'hispeed' I tried to yank it as hard as I could, so there should be intervals of low-speed alternating with higher spikes up to 4kph-8kph ish. When I get a chance I'll try recording a video of the app while repeating this test so that I can get a record of the actual values displayed.

IMG_0146.PNG

IMG_0147.PNG

Link to comment
Share on other sites

Here's a quick strip of data from xima_hispeed.pcapng, as reading it via Wireshark isn't exactly easy. I first filtered & exported the "Rcvd Handle Value Notification, Handle: 0x000b"-data packets plain text and then wrote a quick & dirty piece of program to strip out Value-fields, sort them by the first 3 bytes (like 900001), parse hex strings, both as Big-Endian (BE) and Little-Endian (LE) -formats and then output them in the order they occurred for that "id" (the first three bytes), hope it'll give some clues. There are a few "outliers", where the value-field has contained two values (like 900001000000010b900011d956683c49), didn't bother to fix them now, as I made it in like 15 minutes ;):

900010:
Hex: c800f04233, 8-bit BE: 51, 16-bit BE:16947, 32-bit BE: 15745587, 40-bit BE: 859009204787
  Hex: 3342f000, 8-bit LE: 51, 16-bit LE: 13122, 32-bit LE: 860024832

900011:
Hex: 636a153b38, 8-bit BE: 56, 16-bit BE:15160, 32-bit BE: 1779776312, 40-bit BE: 426981538616
  Hex: 383b156a, 8-bit LE: 56, 16-bit LE: 14395, 32-bit LE: 943396202
Hex: 630d723b38, 8-bit BE: 56, 16-bit BE:15160, 32-bit BE: 225590072, 40-bit BE: 425427352376
  Hex: 383b720d, 8-bit LE: 56, 16-bit LE: 14395, 32-bit LE: 943419917
Hex: b2b0ce3b4b, 8-bit BE: 75, 16-bit BE:15179, 32-bit BE: -1328661685, 40-bit BE: 767470484299
  Hex: 4b3bceb0, 8-bit LE: 75, 16-bit LE: 19259, 32-bit LE: 1262210736
Hex: b9c9153c49, 8-bit BE: 73, 16-bit BE:15433, 32-bit BE: -921355191, 40-bit BE: 797942561865
  Hex: 493c15c9, 8-bit LE: 73, 16-bit LE: 18748, 32-bit LE: 1228674505
Hex: d957673c49, 8-bit BE: 73, 16-bit BE:15433, 32-bit BE: 1466383433, 40-bit BE: 933474286665
  Hex: 493c6757, 8-bit LE: 73, 16-bit LE: 18748, 32-bit LE: 1228695383
Hex: d956683c49, 8-bit BE: 73, 16-bit BE:15433, 32-bit BE: 1449671753, 40-bit BE: 933457574985
  Hex: 493c6856, 8-bit LE: 73, 16-bit LE: 18748, 32-bit LE: 1228695638
Hex: d956683c49, 8-bit BE: 73, 16-bit BE:15433, 32-bit BE: 1449671753, 40-bit BE: 933457574985
  Hex: 493c6856, 8-bit LE: 73, 16-bit LE: 18748, 32-bit LE: 1228695638
Hex: d956683c49, 8-bit BE: 73, 16-bit BE:15433, 32-bit BE: 1449671753, 40-bit BE: 933457574985
  Hex: 493c6856, 8-bit LE: 73, 16-bit LE: 18748, 32-bit LE: 1228695638
Hex: 39f6683c49, 8-bit BE: 73, 16-bit BE:15433, 32-bit BE: -160940983, 40-bit BE: 248947162185
  Hex: 493c68f6, 8-bit LE: 73, 16-bit LE: 18748, 32-bit LE: 1228695798

9000d1:
Hex: 000000041b, 8-bit BE: 27, 16-bit BE:1051, 32-bit BE: 1051, 40-bit BE: 1051
  Hex: 1b040000, 8-bit LE: 27, 16-bit LE: 6916, 32-bit LE: 453246976

9000d0:
Hex: 0000000218, 8-bit BE: 24, 16-bit BE:536, 32-bit BE: 536, 40-bit BE: 536
  Hex: 18020000, 8-bit LE: 24, 16-bit LE: 6146, 32-bit LE: 402784256

79063b:
Hex: 38, 8-bit BE: 56

920001:
Hex: 0000008f21, 8-bit BE: 33, 16-bit BE:36641, 32-bit BE: 36641, 40-bit BE: 36641
  Hex: 218f0000, 8-bit LE: 33, 16-bit LE: 8591, 32-bit LE: 563019776

900001:
Hex: 0000000913, 8-bit BE: 19, 16-bit BE:2323, 32-bit BE: 2323, 40-bit BE: 2323
  Hex: 13090000, 8-bit LE: 19, 16-bit LE: 4873, 32-bit LE: 319356928
Hex: 0000000610, 8-bit BE: 16, 16-bit BE:1552, 32-bit BE: 1552, 40-bit BE: 1552
  Hex: 10060000, 8-bit LE: 16, 16-bit LE: 4102, 32-bit LE: 268828672
Hex: 0000000913, 8-bit BE: 19, 16-bit BE:2323, 32-bit BE: 2323, 40-bit BE: 2323
  Hex: 13090000, 8-bit LE: 19, 16-bit LE: 4873, 32-bit LE: 319356928
Hex: 0000000e18, 8-bit BE: 24, 16-bit BE:3608, 32-bit BE: 3608, 40-bit BE: 3608
  Hex: 180e0000, 8-bit LE: 24, 16-bit LE: 6158, 32-bit LE: 403570688
Hex: 000000030d, 8-bit BE: 13, 16-bit BE:781, 32-bit BE: 781, 40-bit BE: 781
  Hex: 0d030000, 8-bit LE: 13, 16-bit LE: 3331, 32-bit LE: 218300416
Hex: 000000100b, 8-bit BE: 11, 16-bit BE:4107, 32-bit BE: 4107, 40-bit BE: 4107
  Hex: 0b100000, 8-bit LE: 11, 16-bit LE: 2832, 32-bit LE: 185597952
Hex: 000000130e, 8-bit BE: 14, 16-bit BE:4878, 32-bit BE: 4878, 40-bit BE: 4878
  Hex: 0e130000, 8-bit LE: 14, 16-bit LE: 3603, 32-bit LE: 236126208
Hex: 000000140f, 8-bit BE: 15, 16-bit BE:5135, 32-bit BE: 5135, 40-bit BE: 5135
  Hex: 0f140000, 8-bit LE: 15, 16-bit LE: 3860, 32-bit LE: 252968960
Hex: 000000230f, 8-bit BE: 15, 16-bit BE:8975, 32-bit BE: 8975, 40-bit BE: 8975
  Hex: 0f230000, 8-bit LE: 15, 16-bit LE: 3875, 32-bit LE: 253952000
Hex: 000000230f, 8-bit BE: 15, 16-bit BE:8975, 32-bit BE: 8975, 40-bit BE: 8975
  Hex: 0f230000, 8-bit LE: 15, 16-bit LE: 3875, 32-bit LE: 253952000
Hex: 0000002915, 8-bit BE: 21, 16-bit BE:10517, 32-bit BE: 10517, 40-bit BE: 10517
  Hex: 15290000, 8-bit LE: 21, 16-bit LE: 5417, 32-bit LE: 355008512
Hex: 0000000c16, 8-bit BE: 22, 16-bit BE:3094, 32-bit BE: 3094, 40-bit BE: 3094
  Hex: 160c0000, 8-bit LE: 22, 16-bit LE: 5644, 32-bit LE: 369885184
Hex: 000000020c, 8-bit BE: 12, 16-bit BE:524, 32-bit BE: 524, 40-bit BE: 524
  Hex: 0c020000, 8-bit LE: 12, 16-bit LE: 3074, 32-bit LE: 201457664
Hex: 0000001914, 8-bit BE: 20, 16-bit BE:6420, 32-bit BE: 6420, 40-bit BE: 6420
  Hex: 14190000, 8-bit LE: 20, 16-bit LE: 5145, 32-bit LE: 337182720
Hex: 0000015111, 8-bit BE: 17, 16-bit BE:20753, 32-bit BE: 86289, 40-bit BE: 86289
  Hex: 11510100, 8-bit LE: 17, 16-bit LE: 4433, 32-bit LE: 290521344
Hex: 0000016415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 91157, 40-bit BE: 91157
  Hex: 15640100, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875392
Hex: 000000b81d, 8-bit BE: 29, 16-bit BE:47133, 32-bit BE: 47133, 40-bit BE: 47133
  Hex: 1db80000, 8-bit LE: 29, 16-bit LE: 7608, 32-bit LE: 498597888
Hex: 000000bd22, 8-bit BE: 34, 16-bit BE:48418, 32-bit BE: 48418, 40-bit BE: 48418
  Hex: 22bd0000, 8-bit LE: 34, 16-bit LE: 8893, 32-bit LE: 582811648
Hex: 000001f01a, 8-bit BE: 26, 16-bit BE:61466, 32-bit BE: 127002, 40-bit BE: 127002
  Hex: 1af00100, 8-bit LE: 26, 16-bit LE: 6896, 32-bit LE: 451936512
Hex: 000001220f, 8-bit BE: 15, 16-bit BE:8719, 32-bit BE: 74255, 40-bit BE: 74255
  Hex: 0f220100, 8-bit LE: 15, 16-bit LE: 3874, 32-bit LE: 253886720
Hex: 0000016011, 8-bit BE: 17, 16-bit BE:24593, 32-bit BE: 90129, 40-bit BE: 90129
  Hex: 11600100, 8-bit LE: 17, 16-bit LE: 4448, 32-bit LE: 291504384
Hex: 000001d21a, 8-bit BE: 26, 16-bit BE:53786, 32-bit BE: 119322, 40-bit BE: 119322
  Hex: 1ad20100, 8-bit LE: 26, 16-bit LE: 6866, 32-bit LE: 449970432
Hex: 000000ae22, 8-bit BE: 34, 16-bit BE:44578, 32-bit BE: 44578, 40-bit BE: 44578
  Hex: 22ae0000, 8-bit LE: 34, 16-bit LE: 8878, 32-bit LE: 581828608
Hex: 000000d81f, 8-bit BE: 31, 16-bit BE:55327, 32-bit BE: 55327, 40-bit BE: 55327
  Hex: 1fd80000, 8-bit LE: 31, 16-bit LE: 8152, 32-bit LE: 534249472
Hex: 0000021613, 8-bit BE: 19, 16-bit BE:5651, 32-bit BE: 136723, 40-bit BE: 136723
  Hex: 13160200, 8-bit LE: 19, 16-bit LE: 4886, 32-bit LE: 320209408
Hex: 0000014d1c, 8-bit BE: 28, 16-bit BE:19740, 32-bit BE: 85276, 40-bit BE: 85276
  Hex: 1c4d0100, 8-bit LE: 28, 16-bit LE: 7245, 32-bit LE: 474808576
Hex: 0000002f1b, 8-bit BE: 27, 16-bit BE:12059, 32-bit BE: 12059, 40-bit BE: 12059
  Hex: 1b2f0000, 8-bit LE: 27, 16-bit LE: 6959, 32-bit LE: 456065024
Hex: 000001e720, 8-bit BE: 32, 16-bit BE:59168, 32-bit BE: 124704, 40-bit BE: 124704
  Hex: 20e70100, 8-bit LE: 32, 16-bit LE: 8423, 32-bit LE: 552009984
Hex: 0000013412, 8-bit BE: 18, 16-bit BE:13330, 32-bit BE: 78866, 40-bit BE: 78866
  Hex: 12340100, 8-bit LE: 18, 16-bit LE: 4660, 32-bit LE: 305398016
Hex: 0000005817, 8-bit BE: 23, 16-bit BE:22551, 32-bit BE: 22551, 40-bit BE: 22551
  Hex: 17580000, 8-bit LE: 23, 16-bit LE: 5976, 32-bit LE: 391643136
Hex: 000001b319, 8-bit BE: 25, 16-bit BE:45849, 32-bit BE: 111385, 40-bit BE: 111385
  Hex: 19b30100, 8-bit LE: 25, 16-bit LE: 6579, 32-bit LE: 431161600
Hex: 000001a318, 8-bit BE: 24, 16-bit BE:41752, 32-bit BE: 107288, 40-bit BE: 107288
  Hex: 18a30100, 8-bit LE: 24, 16-bit LE: 6307, 32-bit LE: 413335808
Hex: 000000ab1f, 8-bit BE: 31, 16-bit BE:43807, 32-bit BE: 43807, 40-bit BE: 43807
  Hex: 1fab0000, 8-bit LE: 31, 16-bit LE: 8107, 32-bit LE: 531300352
Hex: 000001971b, 8-bit BE: 27, 16-bit BE:38683, 32-bit BE: 104219, 40-bit BE: 104219
  Hex: 1b970100, 8-bit LE: 27, 16-bit LE: 7063, 32-bit LE: 462881024
Hex: 0000025d1e900011f959453c49, 8-bit BE: 73, 16-bit BE:15433, 32-bit BE: 1497709641
  Hex: 493c4559f91100901e5d0200, 8-bit LE: 73, 16-bit LE: 18748, 32-bit LE: 1228686681
Hex: 000000c41a, 8-bit BE: 26, 16-bit BE:50202, 32-bit BE: 50202, 40-bit BE: 50202
  Hex: 1ac40000, 8-bit LE: 26, 16-bit LE: 6852, 32-bit LE: 449052672
Hex: 0000018619, 8-bit BE: 25, 16-bit BE:34329, 32-bit BE: 99865, 40-bit BE: 99865
  Hex: 19860100, 8-bit LE: 25, 16-bit LE: 6534, 32-bit LE: 428212480
Hex: 0000026113, 8-bit BE: 19, 16-bit BE:24851, 32-bit BE: 155923, 40-bit BE: 155923
  Hex: 13610200, 8-bit LE: 19, 16-bit LE: 4961, 32-bit LE: 325124608
Hex: 0000010e19, 8-bit BE: 25, 16-bit BE:3609, 32-bit BE: 69145, 40-bit BE: 69145
  Hex: 190e0100, 8-bit LE: 25, 16-bit LE: 6414, 32-bit LE: 420348160
Hex: 0000008618, 8-bit BE: 24, 16-bit BE:34328, 32-bit BE: 34328, 40-bit BE: 34328
  Hex: 18860000, 8-bit LE: 24, 16-bit LE: 6278, 32-bit LE: 411435008
Hex: 0000001b16, 8-bit BE: 22, 16-bit BE:6934, 32-bit BE: 6934, 40-bit BE: 6934
  Hex: 161b0000, 8-bit LE: 22, 16-bit LE: 5659, 32-bit LE: 370868224
Hex: 0000000e18, 8-bit BE: 24, 16-bit BE:3608, 32-bit BE: 3608, 40-bit BE: 3608
  Hex: 180e0000, 8-bit LE: 24, 16-bit LE: 6158, 32-bit LE: 403570688
Hex: 000000030d, 8-bit BE: 13, 16-bit BE:781, 32-bit BE: 781, 40-bit BE: 781
  Hex: 0d030000, 8-bit LE: 13, 16-bit LE: 3331, 32-bit LE: 218300416
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b900011d956683c49, 8-bit BE: 73, 16-bit BE:15433, 32-bit BE: 1449671753
  Hex: 493c6856d91100900b010000, 8-bit LE: 73, 16-bit LE: 18748, 32-bit LE: 1228695638
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912
Hex: 000000030d, 8-bit BE: 13, 16-bit BE:781, 32-bit BE: 781, 40-bit BE: 781
  Hex: 0d030000, 8-bit LE: 13, 16-bit LE: 3331, 32-bit LE: 218300416
Hex: 000000020c, 8-bit BE: 12, 16-bit BE:524, 32-bit BE: 524, 40-bit BE: 524
  Hex: 0c020000, 8-bit LE: 12, 16-bit LE: 3074, 32-bit LE: 201457664
Hex: 000000010b, 8-bit BE: 11, 16-bit BE:267, 32-bit BE: 267, 40-bit BE: 267
  Hex: 0b010000, 8-bit LE: 11, 16-bit LE: 2817, 32-bit LE: 184614912

900002:
Hex: 00000064159000010000000a1490001163, 8-bit BE: 99, 16-bit BE:4451, 32-bit BE: -1879043741
  Hex: 63110090140a00000001009015640000, 8-bit LE: 99, 16-bit LE: 25361, 32-bit LE: 1662058640
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415, 8-bit BE: 21, 16-bit BE:25621, 32-bit BE: 25621, 40-bit BE: 25621
  Hex: 15640000, 8-bit LE: 21, 16-bit LE: 5476, 32-bit LE: 358875136
Hex: 0000006415900001000000020c, 8-bit BE: 12, 16-bit BE:524, 32-bit BE: 524
  Hex: 0c0200000001009015640000, 8-bit LE: 12, 16-bit LE: 3074, 32-bit LE: 201457664

900012:
Hex: 0000040212, 8-bit BE: 18, 16-bit BE:530, 32-bit BE: 262674, 40-bit BE: 262674
  Hex: 12020400, 8-bit LE: 18, 16-bit LE: 4610, 32-bit LE: 302121984

900013:
Hex: 0000001e1c, 8-bit BE: 28, 16-bit BE:7708, 32-bit BE: 7708, 40-bit BE: 7708
  Hex: 1c1e0000, 8-bit LE: 28, 16-bit LE: 7198, 32-bit LE: 471728128

The speed data could be in some weird format (like rpm, or revolutions / second, or meters/second), battery may be sent as a voltage-value, percentage or discrete steps (like 0...100% = 0...255 or 0...65535 or whatever), odometer can be represented as meters, or they could actually send float-values (but then we'd likely see less zeroes, as the mantissa and exponent should be something else than 0) etc. Let's see if we find any patterns here.

There could be multiple values in some packets.

Link to comment
Share on other sites

Stroke of luck! I contacted the seller directly and they sent me the Android app directly (there are two versions, one which unlocks the 30kph immediately and the other which requires riding 100km first).

 

XIMA-ver1.1.3-limit-30.apk

Nice, I'll pick it apart, this should tell fairly easily how the data is read (unless it's obfuscated...) ;)

Link to comment
Share on other sites

What tools do you use for that? I've never tried decompiling an app before.

The .apk is a zip-file, extract classes.dex from it, then use something like dex2jar to get a jar-package from that. Then use a decompiler (I've used jd-gui) on the jar-package and extract the decompiled classes. Here's the decompiled code (it won't be runnable or even compilable, some classes just don't decompile and the code will be "impossible" at parts, meaning it's doing things like reusing wrong type variables to store other info etc., but if you know Java, you can still read it): 

 xima-classes.src.zip

I've now been poking at it for about an hour, and this is how far I've got:

 

Here are the commands I found so far:

  public static final String READ_MILEAGE_CMD_HEX = "90000700000000106FFFF8FFFFFFFFEF";
  public static final String READ_POWER_CMD_HEX = "900002000000000B6FFFFDFFFFFFFFF4";
  public static final String READ_VELOCITY_CMD_HEX = "900001000000000A6FFFFEFFFFFFFFF5";
  

 So 900007 is odometer, 900002 power (so probably battery?) and 900001 is speed, as you suspected.
 
 Here are the Gatt-attributes, didn't find the Characteristics assigned number (2902) from the Bluetooth-site (although I don't have an account, so I don't see the whole list) from https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx :
 

  public static String CLIENT_CHARACTERISTIC_CONFIG;
  public static String RX;
  public static String SERVICE = "0000ff00-0000-1000-8000-00805f9b34fb";
  public static String TX;
  private static HashMap<String, String> attributes;
  
  static
  {
    RX = "0000ff01-0000-1000-8000-00805f9b34fb";
    TX = "0000ff02-0000-1000-8000-00805f9b34fb";
    CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
    attributes = new HashMap();
    attributes.put(SERVICE, "Type A Solowheel Service");
    attributes.put(RX, "Receiver Attribute");
    attributes.put(TX, "Transmission Attribute");
  }

There also seems to be also some WebService-class that contacts iamips.oicp.net, apparently to download something (maybe app autoupdate?). 

Found also the part which builds the command to sent to the wheel, unfortunately the CommandEnum-class didn't decompile, so the cases are all ???:

 

public static byte[] compileCmd(CommandEnum paramCommandEnum)
  {
    byte[] arrayOfByte = new byte[8];
    byte[] tmp6_5 = arrayOfByte;
    tmp6_5[0] = -112;
    byte[] tmp11_6 = tmp6_5;
    tmp11_6[1] = 0;
    byte[] tmp16_11 = tmp11_6;
    tmp16_11[2] = 1;
    byte[] tmp21_16 = tmp16_11;
    tmp21_16[3] = 0;
    byte[] tmp26_21 = tmp21_16;
    tmp26_21[4] = 0;
    byte[] tmp31_26 = tmp26_21;
    tmp31_26[5] = 0;
    byte[] tmp36_31 = tmp31_26;
    tmp36_31[6] = 0;
    byte[] tmp42_36 = tmp36_31;
    tmp42_36[7] = 0;
    tmp42_36;
    switch (paramCommandEnum)
    {
    default: 
      return arrayOfByte;
    case ???: 
      arrayOfByte[2] = 1;
      return arrayOfByte;
    case ???: 
      arrayOfByte[2] = 2;
      return arrayOfByte;
      
      /** SNIP: lots of cases that tweak a byte or two in the original arrayOfByte **/
  }
  


The application seems to have firmware update-capabilities, but that's just a guess based on these:   

 

     private static final int UPDATE_FIRMWARE_BYTES_AMOUNT = 64;
     private long firmwareFileSize;
     private FileInputStream firmwareUpdateFileInputStream;

 

Then to the actual meat, BluetootLeService.class & DashBoardFragment (UI-class) that show how to read the data:

 

    public void onCharacteristicChanged(BluetoothGatt paramAnonymousBluetoothGatt, BluetoothGattCharacteristic paramAnonymousBluetoothGattCharacteristic)
    {
      paramAnonymousBluetoothGatt = paramAnonymousBluetoothGattCharacteristic.getValue();
      paramAnonymousBluetoothGattCharacteristic = new StringBuilder();
      int j = paramAnonymousBluetoothGatt.length;
      int i = 0;
      while (i < j)
      {
        paramAnonymousBluetoothGattCharacteristic.append(String.format("%02x", new Object[] { Integer.valueOf(paramAnonymousBluetoothGatt & 0xFF) }));
        i += 1;
      }
      Log.i(BluetoothLeService.TAG + ": bytes - ", paramAnonymousBluetoothGattCharacteristic.toString());
      BluetoothLeService.this.parseReturnData(paramAnonymousBluetoothGatt);
    }

BluetoothGatt & BluetoothGattCharacteristic are just Android-classes:  https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html & https://developer.android.com/reference/android/bluetooth/BluetoothGattCharacteristic.html
    
Here's the parsing, it looks weird because the class has been optimized by compiler during build time, and the decompilation is never perfect (it produces uncompilable code, for example the for-loop would complain of dead code in reality, as there's branching that cannot be ever reached, in reality the cases probably should be inside it, and the paramArrayOfByte couldn't be reused for the Event-objects, as it's wrong type):

 

  public void parseReturnData(byte[] paramArrayOfByte)
  {
    if (paramArrayOfByte.length != 8)
    {
      Log.e(TAG, "Return Data length is not 8");
      return;
    }
    switch (paramArrayOfByte[2] & 0xFF)
    {
    default: 
      paramArrayOfByte = new NullEvent();
    case 1: 
    case 2: 
    case 16: 
    case 17: 
    case 18: 
    case 19: 
    case 208: 
    case 209: 
    case 80: 
    case 81: 
      for (;;)
      {
        EventBus.getDefault().post(paramArrayOfByte);
        return;
        paramArrayOfByte = new GetSpeedEvent(((paramArrayOfByte[5] & 0xF) * 256 + ((paramArrayOfByte[6] & 0xF0) >> 4) * 16 + (paramArrayOfByte[6] & 0xF)) / 100.0F);
        continue;
        paramArrayOfByte = new GetPowerEvent(paramArrayOfByte[6]);
        continue;
        paramArrayOfByte = new GetSumMileageEvent(ByteBuffer.wrap(new byte[] { paramArrayOfByte[3], paramArrayOfByte[4], paramArrayOfByte[5], paramArrayOfByte[6] }).order(ByteOrder.nativeOrder()).getFloat());
        continue;
        paramArrayOfByte = new GetMileageEvent(ByteBuffer.wrap(new byte[] { paramArrayOfByte[3], paramArrayOfByte[4], paramArrayOfByte[5], paramArrayOfByte[6] }).order(ByteOrder.nativeOrder()).getFloat());
        continue;
        paramArrayOfByte = new GetVernumEvent(String.valueOf(paramArrayOfByte[5]) + "." + String.valueOf(paramArrayOfByte[6]));
        continue;
        int j = paramArrayOfByte[6] & 0xFF;
        int i = j;
        if (j == 19) {
          i = 20;
        }
        paramArrayOfByte = new GetSpeedlimitEvent(i);
        continue;
        paramArrayOfByte = new GetLightStatusEvent(new LightStatus(CommandEnum.???????, paramArrayOfByte[6]));
        continue;
        paramArrayOfByte = new GetLightStatusEvent(new LightStatus(CommandEnum.??????, paramArrayOfByte[6]));
        continue;
        if (paramArrayOfByte[6] == 0) {}
        for (bool = true;; bool = false)
        {
          paramArrayOfByte = new SetSpeedLimitDoneEvent(bool);
          break;
        }
        paramArrayOfByte = new GetSumMileageEvent(0.0F);
      }
    }
    if (paramArrayOfByte[6] == 0) {}
    for (boolean bool = true;; bool = false)
    {
      paramArrayOfByte = new RenamingSucceeded(bool);
      break;
    }
  }

Btw, the GetPowerEvent etc. classes are nothing but simple holders for values:
  

public class GetPowerEvent
{
  private final int power;
  
  public GetPowerEvent(int paramInt)
  {
    this.power = paramInt;
  }
  
  public float getPower()
  {
    return this.power;
  }
}

public class GetSpeedEvent
{
  private final float speed;
  
  public GetSpeedEvent(float paramFloat)
  {
    this.speed = paramFloat;
  }
  
  public float getSpeed()
  {
    return this.speed;
  }
}

 

Skimming through the DashboardFragment & DigitVelocityFragment, they're just formatting strings and adding stuff like %-sign on the battery value, so the data is already there before the UI. They're also adding to the total mileage (so it looks like the mileage-data is sent as accumulated since last call, that's probably why it behaves weirdly?).
  
Here's for example updating the speed in the DigitVelocityFragment

 

  public void onEventMainThread(GetSpeedEvent paramGetSpeedEvent)
  {
    Log.i(TAG, "Received speed data(km/h):" + paramGetSpeedEvent.getSpeed());
    this.semiCircleView.setAngle(paramGetSpeedEvent.getSpeed());
    this.semiCircleView.invalidate();
    this.speed.setText(String.format("%.1f", new Object[] { Float.valueOf(Math.abs(paramGetSpeedEvent.getSpeed())) }));
  }

 


So, here's the actual parts which read the data:

SPEED:
        paramArrayOfByte = new GetSpeedEvent(((paramArrayOfByte[5] & 0xF) * 256 + ((paramArrayOfByte[6] & 0xF0) >> 4) * 16 + (paramArrayOfByte[6] & 0xF)) / 100.0F);

Don't know if that's done by the decompilation (unnecessarily complex, for example shifting right 4 bits to get the 4 uppermost bits and then multiplying by 16 cancel each other out?), but it seems basically what that computation does is same as:

(((paramArrayOfByte[5] & 0xF) << 8) + (paramArrayOfByte[6] & 0xFF)) / 100.0F;

So it takes the 4 lowermost bits from the 5th index and the whole value from the 6th index (casting it to int with & 0xFF without taking the sign into account, this is how you deal with binary data in Java due to the missing unsigned-types), and then divides by 100.0 (so the original value is "tens of meters per hour" before division).


For example, taking one of the hex-strings from the earlier datacapture (Frame 1358 in the hispeed-capture):

Value: 900001000000ab1f

At first I thought that [5] is 0xab and [6] is 1f => 
0xab & 0x0f = b
Shifted left 8 bits (same as multiplying by 256) => 0x0b00
Get the entire lower byte and add it => 0x0b1f = 2847
Divide by 100, result is in km/h (according to DigitVelocityFragment, which just drops the last fractional digit and places the value into the UI)  =>  28.47km/h. Too high if the wheel was only being moved back and forth by hand...

Then I tested the suspected "0" -speed, 900001000000010b

0x01 & 0x0f = 0x01
After shift => 0x0100
Plus the lower part => 0x0101 = 257
Divided by 100 results... 2.57km/h? That can't be right, there's something else going behind the scenes... 

Then I took a look at:        
        
POWER:        
        paramArrayOfByte = new GetPowerEvent(paramArrayOfByte[6]);

        
So it's just one byte from the value sent by the wheel, same position (6th index) as the latter byte used in speed, and the battery is supposed to be at 100%? 100 = 0x64, let's take a look at the battery data:

9000020000006415

Oh, look at that... there it is, 0x64... No idea what the last byte is for, checksum? So I was off-by-one with the speed data. Let's revisit that speed data:

900001000000ab1f

Now, if [5] = 0x00 and [6] = 0xab, it's simply
0x00 & 0x00 = 0, shifting doesn't change it
Lower part is 0xab = 171, divide by 100 => 1.71km/h. Plausible, maybe...

"Zero-speed": 900001000000010b
It's just "one", I'll skip explaining the calculation => 0.01km/h. Plausible, as the UI drops the last digit (so it shows just 0.0km/h)...

And that's pretty much where I am now.
        
        
TODO:
        
TOTAL MILEAGE?
        paramArrayOfByte = new GetSumMileageEvent(ByteBuffer.wrap(new byte[] { paramArrayOfByte[3], paramArrayOfByte[4], paramArrayOfByte[5], paramArrayOfByte[6] }).order(ByteOrder.nativeOrder()).getFloat());

ACCUMULATED MILEAGE?        
        paramArrayOfByte = new GetMileageEvent(ByteBuffer.wrap(new byte[] { paramArrayOfByte[3], paramArrayOfByte[4], paramArrayOfByte[5], paramArrayOfByte[6] }).order(ByteOrder.nativeOrder()).getFloat());
        
VERSION NUMBER:        
        paramArrayOfByte = new GetVernumEvent(String.valueOf(paramArrayOfByte[5]) + "." + String.valueOf(paramArrayOfByte[6]));

EDIT: Actually the above are pretty self-explanatory, the mileages are 32bit floats and the version number is major version in one byte, and minor in the following... 

 

 

xima-classes.src.zip

Link to comment
Share on other sites

Here's the speed data read from the hispeed-capture as above (reading the 12-bit value & dividing by 100, then formatting to show only one digit after the decimal point):

SPEED: 0,1 km/h
SPEED: 0,1 km/h
SPEED: 0,1 km/h
SPEED: 0,1 km/h
SPEED: 0,0 km/h
SPEED: 0,2 km/h
SPEED: 0,2 km/h
SPEED: 0,2 km/h
SPEED: 0,3 km/h
SPEED: 0,3 km/h
SPEED: 0,4 km/h
SPEED: 0,1 km/h
SPEED: 0,0 km/h
SPEED: 0,3 km/h
SPEED: 3,4 km/h
SPEED: 3,6 km/h
SPEED: 1,8 km/h
SPEED: 1,9 km/h
SPEED: 5,0 km/h
SPEED: 2,9 km/h
SPEED: 3,5 km/h
SPEED: 4,7 km/h
SPEED: 1,7 km/h
SPEED: 2,2 km/h
SPEED: 5,3 km/h
SPEED: 3,3 km/h
SPEED: 0,5 km/h
SPEED: 4,9 km/h
SPEED: 3,1 km/h
SPEED: 0,9 km/h
SPEED: 4,3 km/h
SPEED: 4,2 km/h
SPEED: 1,7 km/h
SPEED: 4,1 km/h
SPEED: 6,1 km/h
SPEED: 2,0 km/h
SPEED: 3,9 km/h
SPEED: 6,1 km/h
SPEED: 2,7 km/h
SPEED: 1,3 km/h
SPEED: 0,3 km/h
SPEED: 0,1 km/h
SPEED: 0,0 km/h
SPEED: 0,0 km/h
SPEED: 0,0 km/h
SPEED: 0,0 km/h
SPEED: 0,0 km/h

+ more 0km/h values

Does this seem about right to you?

Link to comment
Share on other sites

Wow, you sure don't mess around :blink:

Well, having the original code decompiled made this pretty easy, otherwise I'd been taking wild guesses at the captured data for days ;)  I just fixed a couple of minor details in the above posts, looks like I can't count anymore (24-bit values from 2 bytes? nope... but at least I was reading 12-bit values in the code :D).

 

The values look entirely plausible! I don't have time to look at this until later tonight unfortunately... this is going to be distracting me all day :(

When you have the time, you could try building a simple test-app to see if you can read the values directly from the wheel and if they still look alright, then the protocol-side can be pretty much considered solved... ;)  The hard part of course is building the actual app... I had never dealt with Bluetooth or even Android before making the GW/Kingsong-proto, and I have never dealt with BT LE before, based on this it works very differently from "classic" BT, kind of reminds me of Zigbee.

Link to comment
Share on other sites

When you have the time, you could try building a simple test-app to see if you can read the values directly from the wheel and if they still look alright, then the protocol-side can be pretty much considered solved... ;)  The hard part of course is building the actual app... I had never dealt with Bluetooth or even Android before making the GW/Kingsong-proto, and I have never dealt with BT LE before, based on this it works very differently from "classic" BT, kind of reminds me of Zigbee.

Yeah, BT LE is more or less just a set of registers that the device defines and that the phone reads from/writes to... with small extensions to support notifications when a register value changes. Last night I whipped up a test app that replicated the 900001 command and verified that I got the expected response back (this is how I got the info about what characteristics were readable/writable/notifiable, actually). I had pre written code for something similar, so it was a bit easier :)

Tonight I'd like to put in some parsing for what you've found and then go for a ride - I have a pretty good idea of the speed at which the wheel starts beeping, so I can use that to confirm the numbers at the upper end.

Link to comment
Share on other sites

Alright Im gonna sound like a complete noob, but is it possible to create an app function to control the 'hardness' setting of the Lhotz?

Don't think so, unless it's an hidden option (didn't notice anything like that when going through the app). It is possible to send commands to the wheel, but only those that the wheel firmware understands.

Link to comment
Share on other sites

@esaj

I'm slowly crawling through the decompiled source now. I see what you mean now by assigning to the wrong type - I thought it was a decompiler bug at first, but it actually makes sense after thinking about it. I think from now on in my code I am going to follow the compiler's example and just use Object for every variable and cast to the actual type only when I want to access methods/fields :P

Command-building:

public static byte[] sendRequestByType(CommandEnum paramCommandEnum)

This seems to basically take an enum value, uses it to lookup a fixed sequence of 7 bytes (that's more or less what compileCmd does that you pasted above), then passes it through getResultBytes() which (1) adds a 1-byte checksum to the end, and (2) inverts the bits (including checksum) and appends the inverted result to the end. So as far as I can tell each command enum value maps 1:1 to a fixed sequence of 16 bytes.    **I just doublechecked from the packet trace, 90:00:01:00:00:00:00:0a:6f:ff:fe:ff:ff:ff:ff:f5 -> the last 8 bytes are just the first 8 inverted.

So the good news is that it's a lot simpler than I expected and we can just use a packet sniffer & replay the packet payload to replicate the behavior exactly, but the bad news is that there are only 20 commands that we can execute with no ability to parameterize or similar. Based on the switch-case logic, the 3rd byte gives the actual command code as expected and in a few specific cases the 7th byte specifies a parameter, but the parameter is always just some integer from 1-5.

 

And.... err, I guess that actually basically covers it. So it seems there isn't a lot that can be done with the app other than minor tweaks/bugfixes, unless adding non-wheel-related features like GPS tracking etc...

Link to comment
Share on other sites

Finally got around to implementing some of this in-app. It took a little while because I'm a bit picky about how I structure my code :P

A little snippet from my logging output:

2015-09-12 01:36:23.981 XimaApp[495:398148] BLE: XIMA_UUID_SERIAL_RX received: <90000100 00003916>
2015-09-12 01:36:23.982 XimaApp[495:398148] XimaProtocol received packet: <90000100 00003916>
2015-09-12 01:36:23.982 XimaApp[495:398148] Speed: 0.6
2015-09-12 01:36:24.191 XimaApp[495:398148] BLE: XIMA_UUID_SERIAL_RX received: <90000100 00009417>
2015-09-12 01:36:24.192 XimaApp[495:398148] XimaProtocol received packet: <90000100 00009417>
2015-09-12 01:36:24.192 XimaApp[495:398148] Speed: 1.5
2015-09-12 01:36:24.370 XimaApp[495:398148] BLE: XIMA_UUID_SERIAL_RX received: <90000100 00012916>
2015-09-12 01:36:24.371 XimaApp[495:398148] XimaProtocol received packet: <90000100 00012916>
2015-09-12 01:36:24.372 XimaApp[495:398148] Speed: 3.0
2015-09-12 01:36:24.580 XimaApp[495:398148] BLE: XIMA_UUID_SERIAL_RX received: <90000100 0001dc24>
2015-09-12 01:36:24.581 XimaApp[495:398148] XimaProtocol received packet: <90000100 0001dc24>
2015-09-12 01:36:24.582 XimaApp[495:398148] Speed: 4.8
2015-09-12 01:36:24.760 XimaApp[495:398148] BLE: XIMA_UUID_SERIAL_RX received: <90000100 00017517>
2015-09-12 01:36:24.762 XimaApp[495:398148] XimaProtocol received packet: <90000100 00017517>
2015-09-12 01:36:24.762 XimaApp[495:398148] Speed: 3.7
2015-09-12 01:36:24.971 XimaApp[495:398148] BLE: XIMA_UUID_SERIAL_RX received: <90000100 0000da21>
2015-09-12 01:36:24.972 XimaApp[495:398148] XimaProtocol received packet: <90000100 0000da21>
2015-09-12 01:46:31.842 XimaApp[502:399502] BLE: XIMA_UUID_SERIAL_RX received: <90000200 00006415>
2015-09-12 01:46:31.843 XimaApp[502:399502] XimaProtocol received packet: <90000200 00006415>
2015-09-12 01:46:31.843 XimaApp[502:399502] Battery: 100%

Looking pretty good so far! Next step is to do some more packet captures doing various commands and see what app action each of these command strings (reconstructed by code inspection of android app) corresponds to:

    uint8_t cmdStrings[][16] = {
        {0x90,0x00,0x01,0x00,0x00,0x00,0x00},  // Speed request
        {0x90,0x00,0x02,0x00,0x00,0x00,0x00},  // Battery request
        
        {0x90,0x00,0xA0,0x00,0x00,0x00,0x02},
        {0x90,0x00,0xA0,0x00,0x00,0x00,0x01},
        
        {0x90,0x00,0xA1,0x00,0x00,0x00,0x01},
        {0x90,0x00,0xA1,0x00,0x00,0x00,0x02},
        {0x90,0x00,0xA1,0x00,0x00,0x00,0x03},
        {0x90,0x00,0xA1,0x00,0x00,0x00,0x04},
        {0x90,0x00,0xA1,0x00,0x00,0x00,0x05},
        
        {0x90,0x00,0xD0,0x00,0x00,0x00,0x00},
        {0x90,0x00,0xD1,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x16,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x17,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x18,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x19,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x80,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x81,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x82,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x83,0x00,0x00,0x00,0x00},
        {0x90,0x00,0x84,0x00,0x00,0x00,0x00},
        
        {0x90,0x00,0x0A,0x00,0x00,0x00,0x00},
    };

Some have the potential to be dangerous (firmware updates possibly), so I don't want to blindly throw packets at it.

Link to comment
Share on other sites

Managed to figure out most of the remaining commands by looking at the values in the receive logic, and then turning on/off the various light options and re-querying light status.

typedef enum xima_cmd_e_ {
    XC_SPEED                    = 0x01,
    XC_BATTERY                  = 0x02,
    XC_SETLIGHT_FRONTBACK       = 0xA0,
    XC_SETLIGHT_SIDES           = 0xA1,
    XC_LIGHTSTATUS_FRONTBACK    = 0xD0,
    XC_LIGHTSTATUS_SIDES        = 0xD1,
    XC_SUM_MILEAGE              = 0x10,
    XC_MILEAGE                  = 0x11,
    XC_VERSION                  = 0x12,
    XC_GETSPEEDLIMIT            = 0x13,
    XC_SETSPEEDLIMIT_RESULT     = 0x50,  // not 100% sure
    XC_RENAME_RESULT            = 0x51,  // not 100% sure
    XC_UNK12                    = 0x52,  // no receive logic
    XC_UNK13                    = 0x53,  // no receive logic
    XC_UNK14                    = 0x54,  // no receive logic
    XC_UNK15                    = 0x0A,  // no receive logic
} xima_cmd_e;

typedef enum xima_lightstate_fb_ {
    XLSFB_OFF = 1,
    XLSFB_ON = 2
} xima_lightstate_fb;

typedef enum xima_lightstate_sides_ {
    XLSS_OFF = 1,
    XLSS_LOW = 2,
    XLSS_HI = 3,
    XLSS_ROTATE = 4,
    XLSS_POWER = 5
} xima_lightstate_sides;
- (NSData*)packetForCmd:(xima_cmd_e)cmd {
    return [self packetForCmd:cmd arg:0];
}

- (NSData*)packetForCmd:(xima_cmd_e)cmd arg:(uint8_t)arg {
    uint8_t packet[16] = {0x90, 0x00, cmd, 0x00, 0x00, 0x00, arg};
    [self completeCmdPacket:packet];
    
    return [NSData dataWithBytes:packet length:sizeof(packet)];
}

- (void)completeCmdPacket:(uint8_t*)packet {
    for (int i = 0; i < 7; ++i) {
        packet[7] += ((packet[i] & 0x0F) + (packet[i] >> 4));
    }
    for (int i = 0; i < 8; ++i) {
        packet[8+i] = ~packet[i];
    }
}

Just need to sniff the rename and set-speedlimit commands now.

I could see some real potential for more serious hacking by doing firmware upgrade (kind of looks like the android app has something for that?), but at this point that's way too much work :P

Link to comment
Share on other sites

Looking good, keep it up  B)

I could see some real potential for more serious hacking by doing firmware upgrade (kind of looks like the android app has something for that?), but at this point that's way too much work :P

Be careful not to brick your wheel  :P  Explaining that in warranty repair might be hard ;)  Btw, I poked around the app when I was deciphering the protocol, it seems it loads the firmware-binary from some webservice (SOAP). Tried a couple requests with a REST-debugging tool, but got nothing out of there.

 

Link to comment
Share on other sites

Looking good, keep it up  B)

Be careful not to brick your wheel  :P  Explaining that in warranty repair might be hard ;)  Btw, I poked around the app when I was deciphering the protocol, it seems it loads the firmware-binary from some webservice (SOAP). Tried a couple requests with a REST-debugging tool, but got nothing out of there.

What, bricking due to loading a poorly reverse-engineered firmware image over a 3rd party tool isn't covered under manufacturer defects? :P

But yeah I am not going to touch firmware update. I can't really think of anything firmware related I would want to improve anyway.

Link to comment
Share on other sites

Kevin, would you consider making you work open source?   It could be used a base to build a general purpose open source app for wheels perhaps?   github?   The solowheel xtreme is also bluetooth LE so could there could be some shared code.

Link to comment
Share on other sites

  • 2 weeks later...

Karma caught up to me today for being lazy about wrapping this up :blink:

My wheel factory reset itself (?!) and locked itself back to 20kph with aggressive tilt-back at 16kph. And of course I didn't realize it until this morning at the top of a mountain at the start of a 45km trail...

If I'd noticed in the morning I could have loaded the no-limit android APK and unlocked it immediately... or if I'd finished writing my 'upgraded' iOs app I could have done the same. But I was lazy, and so ended up stuck with a long sloooooooow ride down, holding up the whole group of 15 other cyclists <_<

Kick in the butt, time to get back to work...

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...