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)


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 and control of the BLE protool used by the ESF24 scale to be able to read the measurments for our own purposes without use of a proprietary app.
  • Build a convenient, automatic, app-less logger for the scale that, breifly, works like this:

    • Scale always in range of a strategically placed Bluetooth host.
    • User steps on, server automatically detects this and records weight.
    • User can view log later.

The server will be built in a part 2.

The Protocol


The protocol spoken by the ESF24 scale is proprietary and undocumented so this is the first thing we need to crack.

The application that talks to the scale, called VeSync, 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 for this. The exact version you choose will depend on your version of Android. We run the latest Android here, at the time of writing version 12, so these steps work for that:

  1. Under developer options (a hidden menu you first need to enable to be able to access it) 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 up the app that talks to your device (in this case VeSync) and exercise its functionality. It helps to 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. This is a binary file that you can view in Wireshark.
  5. Turn off the bluetooth logging and then switch on/off bluetooth. (Don't forget this or there might be a large file hidden on your phone somewhere you can't readily access!)

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 filter as follows :

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

The first thing we see is the usual service discovery - the application inquiring of the device it's connected to what services it supports. In our case the scale 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 just these comms as follows :

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

And then export them. (We exported as JSON and then wrote a throwaway script to extract the interesting bits:

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 will prefix each line with a < for received from the scale and a > for 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

Looking at 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 analysis we built a test implementation and used it to talk to the scales ourselves.

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

Code for this is here :

Install into a Python virtual env 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