oliexdev / openScale

Open-source weight and body metrics tracker, with support for Bluetooth scales
GNU General Public License v3.0
1.69k stars 295 forks source link

Insmart scale support #593

Open ChristianB86 opened 4 years ago

ChristianB86 commented 4 years ago

Insmart Android App Product Page I can not found a webpage from the vendor

To support a new scale it is necessary to gather some information.

Step 1: Read the general reverse engineer process

Step 2: Acquiring some Bluetooth traffic Attach 3 log files with the corresponding true values, read here for further information.

  1. Bluetooth HCI Snoop log file user settings in the vendors app:

    sex male, body height 193cm, age 35, activity level: not a setable value

    measured true values in the vendors app for the 1. HCI Snoop log file:

    89.60kg, 20:49 06/07/2020, water 59,4%, muscle 53,2% , fat 17,7% 

    --> Attach the 1. HCI Snoop log file here <-- 06_07_2020__20_50_02-btsnoop_hci.log

  2. Bluetooth HCI Snoop log file user settings in the vendors app:

    same as first one

    measured true values in the vendors app for the 2. HCI Snoop log file:

    93.35kg, 20:50 06/07/2020, 58.3%, 52.2% , 19.2%

--> Attach the 2. HCI Snoop log file here <-- 06_07_2020__20_51_36-btsnoop_hci.log

  1. Bluetooth HCI Snoop log file user settings in the vendors app:
    same as first one

    measured true values in the vendors app for the 3. HCI Snoop log file:

    
    110.75kg, 20:52 06

110.75kg, 20:52 06/07/2020, 53.2%, 47.6% , 26.4%


_--> Attach the 3. HCI Snoop log file here <--_
[06_07_2020__20_52_34-btsnoop_hci.log](https://github.com/oliexdev/openScale/files/4886782/06_07_2020__20_52_34-btsnoop_hci.log)

**Step 3: Discover Bluetooth services and characteristic**
_Read [here](https://github.com/oliexdev/openScale/wiki/How-to-reverse-engineer-a-Bluetooth-4.x-scale#2-find-out-the-bluetooth-services-and-characteristic) how to create the openScale debug file_

_--> Attach the openScale debug log file here <--_
[openScale_2020-07-05_20-52.txt](https://github.com/oliexdev/openScale/files/4886795/openScale_2020-07-05_20-52.txt)

While standing on the scale, the app shows different weights, like the scale itself until the measurement is over. I'am not able to connect to the scale via Bluetooth LE. Is it possible, that the scale sends all data as advertisment? I can use multiple Apps on different phones at the same time.

Thanks in advance for help
oliexdev commented 4 years ago

While standing on the scale, the app shows different weights, like the scale itself until the measurement is over. I'am not able to connect to the scale via Bluetooth LE. Is it possible, that the scale sends all data as advertisment? I can use multiple Apps on different phones at the same time.

normally the scale sends the weight during a measurement as Bluetooth advertisment but that measurements are normally ignored. After the impedance measurement the final result are saved. You are not able connect via Bluetooth but the app works? You can't use multiple Apps on different phones at the same time because the scale itself can only connect to one device.

ChristianB86 commented 4 years ago

I finally figured out how to get the weight data from the scale. First: The scale is nerver connectable. And i definitely CAN use multiple apps on different phones at the same time and it works. I was also able to send fake advertisements by my own and the App recognized me as a scale an displayed the data. The app accept me as a scale when i send a Service UUID 0xFFB0 an manufacturer data with company id AC A0.

The weight data is in the advertisement after the company id an the mac-adress. The next byte is a0 during the measurement an 20 after it finished. the next three bytes are the weight data. Next byte is always (for weight data) "0x0d" and the next one is a checksumm.

For example: ac a0 db 58 e2 53 91 a0 20 c9 f8 42 0d b0

ac a0 is company id, db to a0 is mac address. Then the next byte seems to indicate the final value. c9 f8 42 has the data. But (and this takes me many time to figure out): To get valid data you need to replace the third and fifth 'character' of the hex string like this:

0 1 2 3 4 5 6 7 8 9 a b c d e f Normal a b 8 9 e f c d 2 3 0 1 6 7 4 5 Icomon

Afterwards you subtract 0xc80000 and divide by 1000. And that's it...

c8a0a0 show the app as 0,00kg. If you replace the third and fifth character with this table it is: c80000. Subtract c80000 = 0. I tested this with various data and it always works.

I wrote a python-script to test, here it is:

#!/usr/bin/env python3
import sys
# 0 1 2 3 4 5 6 7 8 9 a b c d e f Humans
# a b 8 9 e f c d 2 3 0 1 6 7 4 5 Icomon

def main():
  hex_code = sys.argv[1]
  ic = ['a','b','8','9','e','f','c','d','2','3','0','1','6','7','4','5']

  i=0
  out=''
  for byte in hex_code:
    if i in [2, 4]:
      index = int(f'0x{byte}', 16)
      out = out+ic[index]
    else: out=out+byte  
    i=i+1
  value = round((int(f'0x{out}', 16)-0xc80000)/1000,2)
  value=round05(value)
  print(value)

def round05(number):
  return (round(number * 20) / 20)

main()
ChristianB86 commented 4 years ago

In the Advertisements like 'ac a0 db 58 e2 53 91 a0 - a2 af a0 a3 06 ba - 07 09 41 41 41 30 30 36 b7' the impendance measurement is in 'af a0 a3'. First byte after the mac is a2 and fourth byte is 06. In between is the measurement data. But i don't know how to calculate this and i don't really need this. It's just an information

oliexdev commented 4 years ago

strange calculation :thinking:

The body measurements calculation is done in the libICBodyFatAlgorithms.so which have to be reversed engineered.

Could you send a PR with the implementation for the weight calculation?

ChristianB86 commented 4 years ago

So you also worked thru the Fitdays sourcecode. It's so a mess. I disassembled one of the native libraries to get the calculation for the weight data. But reading assembler code is way over my abilities.

I another community, a user told me the 'strange calculation' is just XOR. Probably used to hide the data. On https://xor.pw/ i can enter a value from the scale in 1. input, c8a0a0 in 2. input and get my weight data. I can send a PR for this. Just the calculation? Should i put it in a new file, utils/Icomon.java or something?

oliexdev commented 4 years ago

So you also worked thru the Fitdays sourcecode. It's so a mess. I disassembled one of the native libraries to get the calculation for the weight data. But reading assembler code is way over my abilities.

Unfortunately reverse engineering is not easy. I can recommend to use https://ghidra-sre.org/

I another community, a user told me the 'strange calculation' is just XOR. Probably used to hide the data. On https://xor.pw/ i can enter a value from the scale in 1. input, c8a0a0 in 2. input and get my weight data.

Ok then you could XOR it back with c8a0a0.

I can send a PR for this. Just the calculation? Should i put it in a new file, utils/Icomon.java or something?

Thanks I added the Icomon branch in the repository were you can find the frame for the icomon scale support. We need to find out how to trigger the measurements (which Bluetooth services and characteristic are used). Would be great if you could check this branch out and implement a first version of you findings.

ChristianB86 commented 4 years ago

I looks like "onBluetoothNotify" is only called when the WEIGHT_MEASUREMENT_CHARACTERISTIC is received. But the scale doesn't send a characteristic. The original app recognize my fake advertisement as an insmart scale when i send a service UUID 0xFFB0 and as company id in the manufacturer data "a0ac". I think the scale will not send any other data.

oliexdev commented 4 years ago

We need to find out the Service and Charachteristic UUID which sends the data.

Can you attach a new openScale log file or use the BLE Scanner App by Bluepixel Technology LLP to discover the Bluetooth services and characteristc of the scale.

enavarro222 commented 3 years ago

Hi,

I just get a scale that works with this same android software. It's a Livoo DOM428.

This device send BLE advertise when it weigh something :

[15:40:28][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A0.2C.F3.94.0D.A0 (12)
[15:40:29][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A0.2D.EA.E6.0D.AA (12)
[15:40:31][D][ble_adv:144]: 2B.7A.CD.58.91.A0.20.2D.EA.CE.0D.B2 (12)
[15:40:32][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:33][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:33][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:34][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:35][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:36][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:37][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:40][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:41][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:42][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)
[15:40:43][D][ble_adv:144]: 2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 (12)

(I use an esp32 + esphome to catch that values)

The scale stream the data as log as weight is changing, and when stabilized the same datafralme (kind of "end of measurement") is sent during ~10sec (2B.7A.CD.58.91.A0.A2.AC.A0.A2.06.B6 here)

@ChristianB86 This is seems to be the same layout that with your scale :

Also with this scale one has to xor the weight with 0x2CA0A0 (the value for 0kg) and then to divide it by 1000 to get the kg value.

I don't know how the app know is kind of xor key (0x2CA0A0) as it seems to vary from one scale to an other...

I'll try to share other information I found on that scale protocol !

rexmightlag commented 2 years ago

Hello, any update on this? I also have a scale that uses the same app.

Duncaen commented 5 months ago

I have a scale with the same protocol and reverse engineered a bit more, by disassembling the native library they use.

@enavarro222 was pretty close for the weight data. to xor the message instead of 0x2CA0A0, every byte is xor'ed with 0xA0, which comes from the company id in the bluetooth packet (the first 2 bytes that i added to the example packets I decoded). This then leaves a few bits of the weight to indicate support and state, the last byte is basically just an indicator for the unit that is used on the scales display and a primitive "checksum".

Here is the python script I wrote to inspect the packets.

def decode(buf):
    xor = buf[1]
    data = [b ^ xor for b in buf[8:8+6]]
    # print(*[f"{b:02X}" for b in data])
    chk = sum(data[:5])
    if (chk & 0x1F) != (data[5] & 0x1F):
        print("chk failed")
        return
    if data[4] == 0xAD:
        value = ((data[3] << 0) | (data[2] << 8) | (data[1] << 16) | (data[0] << 24))
        print("weight", value & 0x3FFFF, (value & 0x3FFFF)/1000,
              "state", value >> 0x1F,
              "dianji", (value >> 0x18) & 1,
              "supportHr", (value >> 0x19) & 1,
              "supportPh", (value >> 0x1A) & 1,
              "supportZx", (value >> 0x1B) & 1,
              "unit", (data[5] >> 5) & 7,
              "precision_kg", 2 if ((value >> 0x12) & 7) < 3 else 1,
              "precision_lb", 2 if ((value >> 0x15) & 7) < 3 else 1)
    elif data[4] == 0xA6:
        print("adc", (data[1] << 0) | (data[0] << 8),
              "hr", data[2],
              "bfaType", data[3],
              "unit", (data[5] >> 5) & 7)

decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA0, 0x2C, 0xF3, 0x94, 0x0D, 0xA0]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA0, 0x2D, 0xEA, 0xE6, 0x0D, 0xAA]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0x20, 0x2D, 0xEA, 0xCE, 0x0D, 0xB2]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
decode(bytes([0xAC, 0xA0, 0x2B, 0x7A, 0xCD, 0x58, 0x91, 0xA0, 0xA2, 0xAC, 0xA0, 0xA2, 0x06, 0xB6]))
weight 21300 21.3 state 0 dianji 0 supportHr 0 supportPh 0 supportZx 0 unit 0 precision_kg 1 precision_lb 1
weight 84550 84.55 state 0 dianji 0 supportHr 0 supportPh 0 supportZx 0 unit 0 precision_kg 1 precision_lb 1
weight 84590 84.59 state 1 dianji 0 supportHr 0 supportPh 0 supportZx 0 unit 0 precision_kg 1 precision_lb 1
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0
adc 524 hr 0 bfaType 2 unit 0