Sniffing the EtekCity ESF24 Fitness scale

HACK -️ January 6, 2022

Please respect the license under which this work is made available. (See terms and conditions at the end of the page)


It's January, a time when we reflect on the excesses of Christmas and decide to get fit. To this end we have an EtekCity scale. It comes with a Bluetooth facility for wirelessly reading the measurements and this is accessed through a large Android application called VeSync.

The aims of this project are:

  • Gain insight into the communications protool used by the ESF24 scale so that we can read the measurments without using a proprietary app.
  • Build a convenient, automatic, app-less logger for the scale that works like this :

    • Scale is placed always in range of a strategically placed Bluetooth host.
    • User steps on, server automatically detects this and logs their weight.
    • User can view log later using a web browser or app.

The server will be built in a part 2.

The Protocol


The protocol spoken by the ESF24 scale is proprietary and undocumented so to understand it we need to do a little protocol analysis.

The application that talks to the scale is called VeSync and runs on Android. Android has the ability to log bluetooth communications so we'll use that to log and then inspect the protocol to see if we can reverse engineer the relevant details.

Getting a protocol log from Android

There are a number of guides on the Internet describing how to enable Bluetooth data logging on Android. The exact guide you choose will depend on your version of Android. These steps are for version 12 :

  1. Under developer options (a hidden menu) set the "Enable Bluetooth HCI snoop log" option to "Enabled."
  2. Switch off Bluetooth then switch it on again. (This is so the Bluetooth software picks up the new setting.)
  3. Start the app that talks to your device (in this case VeSync) and exercise its functionality. It helps if you keep a timestamped record of what you did so you can match it up later to the log.
  4. When done, use adb to trigger the generation and download of a bugreport. The bugreport is a zip file containing, amongst other things, a file called btsnoop_hci.log.
  5. Turn off Bluetooth logging and then switch on/off bluetooth. (Don't forget this or you will be left with a large hidden file taking up all your disk space!)

Bluetooth analysis

The btsnoop_hci.log log file can be viewed in Wireshark. The log contains a lot of noise (especially if you have other Bluetooth devices connected) so it's helpful to identify the MAC address of your scale and then set a Wireshark filter as follows :

bluetooth.addr == 04:ac:44:01:02:03

The first part of the communications is the standard BLE GATT service discovery where the app is asking the scales what services it supports.

0000180f-0000-1000-8000-00805f9b34fb (Battery service)
    00002A19-0000-1000-8000-00805f9b34fb (Battery characteristic)

0000fff0-0000-1000-8000-00805f9b34fb (Unknown service)
    0000fff1-0000-1000-8000-00805f9b34fb (Notify characteristic)
    0000fff2-0000-1000-8000-00805f9b34fb (Write characteristic)

Communications with the scale are performed through the "Unknown service" by :

  • Enabling the notification characteristic. This enables unsolicited streaming message delivery from the scale.
  • Sending proprietary commands through the write characteristic.

Protocol analysis

Selecting the right data

Having identified the communications channel, we can further filter the Wireshark log to show only these comms as follows :

bluetooth.addr == 04:ac:44:01:02:03 && (btatt.opcode == 0x52 || btatt.opcode == 0x1b)

And then we export them as JSON and process them using this throwaway script :

import json

a = json.loads(open("a.json").read())

for doc in a:
    btatt = doc["_source"]["layers"]["btatt"]
    d = int(btatt["btatt.opcode"],16)
    val = btatt["btatt.value"].replace(":","")
    print("<" if d==0x1b else ">", val)

The script prefixes each line with a < for data received from the scale and a > for data transmitted to the scale.

The edited (identifying information removed) output is below :

< 120f150xxxxxxxREDACTED    # Hello from scale message.
> 1309150110be340034        # Configures the scale.
< 140b150000010000000035
> 2008151fd86929c6          # Send the current time to the scales.
< 210515013c
< 100b150000000000000030    # Weight message
> 2204153b
< 100b150000000000000030    # Weight message
< 231415000000000000000000000000000000004c
< 100b150bea000000000025    # Weight message
< 100b1525ad000000000002    # Weight message
... lots more of these all changing as scale was stepped on
< 100b1528e60101f801afe8    # Contains the final weight + resistance measurements. Fat bastard.
> 1f05151049                # Stops sending the final measurement.

Overall structure

Examining the above trace we can deduce the likely message structure as follows :

Packet format

Other observations are described in the test implementation.

Building a test implementation

Using the above analysis we built a test implementation and used it to talk to the scales.

This was a success and we can reliably get measurements from it.

The code for this is here :

Install it into a Python virtual environment and run like this :

python -metekcity_scale.test 04:AC:44:01:02:03

Example output:

Battery bytearray(b'd')
Found scales service
UnknownPacket(type=35, raw_value=bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'))
2.15 0 0 0
19.7 0 0 0
19.7 1 434 363