📜 ⬆️ ⬇️

The history of reverse engineering of a Chinese fitness bracelet

Buy a Chinese bracelet, be disappointed in the official software, write your own!




This story has been waiting for its publication for more than six months, during this time, much has changed, the firmware and software have been updated, and many of my achievements have become outdated.
')

Foreword


The active work of a large number of companies in the field of wearable technology and smart watches did not leave my soul at rest. I saw great potential in wearable devices with a screen. No, I'm not talking about counting steps and other fitness things, they are definitely cool, but so far, apart from the banal “Congratulations! You have passed 4km, made 20k + steps! ”And beautiful graphs of progress and regress, did not invent anything special.
But the fact that I can receive notifications directly to the display on the wrist is convenient. If I can also somehow interact with it or with something nearby, pressing 1-2-3 buttons is even cooler.

Once again, surfing on Aliexpress, I came across an iWown i5 fitness bracelet. He immediately caught my attention at an incredibly low price (at that time about 800r with free shipping) and the presence of an OLED display. Carefully reading the description of the seller and customer reviews, I decided to order this miracle.

The declared characteristics (translation of the description with aliexpress):


Opportunities:

and other "far-fetched" pluses in the style of Chinese marketing

I was very interested in the ability to track a dream and wake up at the right phase. Many of my friends bought inexpensive fitness trackers because of this feature and were happy with mi band and similar things. I always lacked a screen in them, but here it was all-in-one.
In my work, I often have to develop simple applications for Android, I decided that if I don’t have enough functionality of my native application, I’ll write my own.

The package arrived pretty quickly and I immediately rushed to study a wonderful bracelet. After an hour of playing with the Zeroner app, which you need to put on your Android device according to the instructions, I realized that the functionality is rather poor and sad. Zeroner, like all other manufacturers, focused on counting steps and calories, displaying beautiful graphics, has a phone search function (I’ll tell you about this later), can notify you of an incoming call, a message on facebook and whatsapp and send notifications from ONE to any selected application , which will be considered as an application for SMS.
The vibration of the bracelet is very controversial, on the forums they write that it is rather weak, some say normal. For me, it could have been stronger. The bracelet has a reaction to the gesture “Look at the clock”, if you look at the bracelet like a wrist watch, raising your arm and bending at the elbow, the screen will automatically turn on and will show the time or the missed notification.



In general, without hesitation, I decided to write my application, with notifications, vibration and synchronization. Looking ahead, it took 4 days off and a few long evenings ...

To business


Considering that with Bluetooth I am not in the blue-tooth-leg, with the fool I decided to try to intercept the data exchanged between the phone and the bracelet. To do this, I went to the tab for developers, and turned on the checkbox “Enable HCI Bluetooth broadcast log” . After enabling this option, the whole dump of Android communication with any Bluetooth devices is added to the file /sdcard/Android/data/btsnoop_hci.log (for different devices the path may change, the file name always seems to be the same).
After downloading WireShark, I began to study the communication logs with the bracelet and saw something similar to this:



After spending almost two hours studying the logs, making the dependencies, googling the Internet protocols, I realized that this way was not for me.

Since my phone interpreted the bracelet as an ordinary BLE device and showed it in the connected devices section, I decided to use the BLE examples from the Android SDK.
Cloning the https://github.com/googlesamples/android-BluetoothLeGatt repository, set Android Studio on the navel with the sources, compiled and launched the application. ( Link to the description of the Android SDK work with Bluetooth LE )

It turned out as in the pictures from the githab:


By running the scan, the application did not see the device. It turned out that the native application connecting to the bracelet did not allow BLE to find the device. Everything was decided by simply removing Zeroner, you could just turn it off, but it’s safer to pull it down completely.

And so, Bluetooth LE is a technology that is built on devices with low power consumption, is used in modern sensors, tags and many other devices. The basis of this technology is the Generic Attribute Profile (GATT) , a Bluetooth profile that allows you to share small portions of data, "attributes" . I will not long describe how it all works, on Habré and in the internet there is a lot of information that I also had to search through in search of solutions.

I hoped that all the data I needed was stored in the characteristics and descriptors of the bracelet, and I would be able to receive and record data without any problems. I was wrong…

Test BLE application showed me only 4 services:
0000180f-0000-1000-8000-00805f9b34fb
00001800-0000-1000-8000-00805f9b34fb
0000ff20-0000-1000-8000-00805f9b34fb
00001801-0000-1000-8000-00805f9b34fb

they had very few characteristics, those that were read, returned void or zeros, and it was useless to write. But I was inspired by the fact that I was able to connect and get at least some data.

Further, I decided that it was impossible to act blindly and decided to dissect the Zeroner application. Having dug up a couple of online APK decompilers on the Internet, I fed them zeroner.apk and received 2 zip archives at the output.
The first was the JADX version, and the second one contained the result of the apktool operation.

Rummaging in the source, I was terrified by the Chinese code (although I often come across it in the form of backends for websites and services, but it never ceases to amaze with its tortuosity and ingenuity, but anyway, it is terribly hard to read)
After much research, I finally came across the WristBandDevice.java file, which was on the com.kunekt / bluetooth path.
In this class, all the work with the device was hidden, but again, an ambush waited for me.
As it turned out later, in the previous firmware of the bracelet more services were used in the characteristics (as I previously assumed), but later, the developers left only 2, one for reading, the second for writing. All commands are transmitted in one package.

Understanding how the package should look like was not so easy, I decided to clearly define what I want from the bracelet in the first place, in order to start tracking function calls. And I wanted to display custom messages on the bracelet.
Without hesitation, I climbed into com.kunekt / receiver / CallReceiver.java , since incoming calls were displayed very stable and even Russian characters, I decided that this was a great start, considering that I had already come across an incoming call event in Android, an idea of how it might work already.

Opening the file, I saw this:
A large piece of Chinese code
public void onReceive(Context context, Intent intent) { Log.e(this.TAG, "+++ ON RECEIVE +++"); switch (((TelephonyManager) context.getSystemService("phone")).getCallState()) { case C08571.POSITION_OPEN /*0*/: if (ZeronerApplication.newAPI) { BackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, WristBandDevice.getInstance(context).setPhoneStatue())); } case BitmapCacheManagementTask.MESSAGE_INIT_DISK_CACHE /*1*/: incomingNumber = intent.getStringExtra("incoming_number"); Contact contact = getContact(context, incomingNumber); if (!WristBandDevice.getInstance(context).isConnected() || !ZeronerApplication.phoneAlert) { return; } if (ZeronerApplication.newAPI) { this.fMdeviceInfo = jsonToFMdeviceInfo(UserConfig.getInstance(context).getDevicesInfo()); if (this.fMdeviceInfo.getModel().indexOf("5+") != -1) { if (UserConfig.getInstance(context).getFont_lib() == 1 || UserConfig.getInstance(context).getFont_lib() == 2 || UserConfig.getInstance(context).getSysFont().equalsIgnoreCase("en") || UserConfig.getInstance(context).getSysFont().equalsIgnoreCase("es")) { if (contact.getDisplayName().length() > 11) { WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName().substring(0, 11)); } else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) { WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName()); } else { WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName().substring(0, contact.getDisplayName().length())); } } else if (contact.getDisplayName().length() > 11) { WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, 11)); } else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) { WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName()); } else { WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, contact.getDisplayName().length())); } } else if (contact.getDisplayName().length() > 11) { WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, 11)); } else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) { WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName()); } else { WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, contact.getDisplayName().length())); } } else if (contact.getDisplayName().length() > 11) { WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName().substring(0, 11)); } else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) { WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName()); } else { WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName().substring(0, contact.getDisplayName().length())); } case BitmapCacheManagementTask.MESSAGE_FLUSH /*2*/: if (ZeronerApplication.newAPI) { BackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, WristBandDevice.getInstance(context).setPhoneStatue())); } default: } } 



Here we clearly see that there are 2 variants of the API and the names of them are very logical newAPI, and the second, respectively, oldAPI. In all this abundance of conditions, I was interested in only one, repeated line:
WristBandDevice.getInstance (context) .writeWristBandPhoneAlertNew (context, contact.getDisplayName .....)

It was the same thing that I was looking for. Looking ahead, I’ll say that iWown still has i5 + and i6 models, their screen is bigger and, accordingly, more characters fit, for this we need all these checks. It is not clear why they did not write a class or something like that, maybe these are the decompiler's pranks, but this code is repeated in many places.
Turning to the definition of this function, I saw this:

  public void writeWristBandPhoneAlertNew(Context context, String displayName) { writeAlertNew(context, displayName, 1); } public void writeWristBandSmsAlertNew(Context context, String displayName) { writeAlertNew(context, displayName, 2); } 


Well, the same function is used to send text, just with different parameters. All functions with the word New are just our option, because as it turned out above, I have an API new.

Happily going to the definition of the writeAlertNew function, I saw the following:
 private void writeAlertNew(Context context, String displayName, int type) { ArrayList<Byte> datas = new ArrayList(); datas.add(Byte.valueOf((byte) type)); int i = 0; while (i < displayName.length()) { if (displayName.charAt(i) < '@' || (displayName.charAt(i) < '\u0080' && displayName.charAt(i) > '`')) { char e = displayName.charAt(i); datas.add(Byte.valueOf((byte) 0)); for (byte valueOf : PebbleBitmap.fromString(context, String.valueOf(e), 8, 1).data) { datas.add(Byte.valueOf(valueOf)); } } else { char c = displayName.charAt(i); datas.add(Byte.valueOf((byte) 1)); for (byte valueOf2 : PebbleBitmap.fromString(context, String.valueOf(c), 16, 1).data) { datas.add(Byte.valueOf(valueOf2)); } } i++; } byte[] data = writeWristBandDataByte(true, form_Header(3, 1), datas); for (i = 0; i < data.length; i += 20) { byte[] writeData; if (i + 20 > data.length) { writeData = Arrays.copyOfRange(data, i, data.length); } else { writeData = Arrays.copyOfRange(data, i, i + 20); } NewAgreementBackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, writeData)); } } 


It was clear that a couple of functions that are used here separate me from profit.
writeWristBandDataByte - forms a packet with a message for the bracelet, it is interesting that there is a special function form_Header (3, 1) , which forms the header of the packet by which the bracelet understands what they want from it. 3 is the number of the group of commands, and 1 is the command itself
 public static byte form_Header(int grp, int cmd) { return (byte) (((((byte) grp) & 15) << 4) | (((byte) cmd) & 15)); } 


The function is simple, copied to your project without changes. Next was this

NewAgreementBackgroundThreadManager.getInstance (). AddTask (new WriteOneDataTask (context, writeData));

As it turned out, nothing unusual, the application creates a stream in which a queue of packets is constantly checked for sending, if a packet appears in the queue, the stream writes to the specified device characteristic, if there are more than one packet, it sends them with a delay of 240 milliseconds.
Then came the most incomprehensible:

PebbleBitmap.fromString (context, String.valueOf (e), 8, 1) .data)

Why the class is called that way is not clear, because with this device Pebble has nothing in common. Having opened a class source code I saw the following:

PebbleBitmap class source
 public class PebbleBitmap { public static boolean f1285D; public final byte[] data; public final UnsignedInteger flags; public final short height; public int index; public int offset; public final UnsignedInteger rowLengthBytes; public final short width; public final short f1286x; public final short f1287y; static { f1285D = true; } private PebbleBitmap(UnsignedInteger _rowLengthBytes, UnsignedInteger _flags, short _x, short _y, short _width, short _height, byte[] _data) { this.offset = 0; this.index = 0; this.rowLengthBytes = _rowLengthBytes; this.flags = _flags; this.f1286x = _x; this.f1287y = _y; this.width = _width; this.height = _height; this.data = _data; } public static PebbleBitmap fromString(Context context, String text, int w, int l) { TextPaint textPaint = new TextPaint(); textPaint.setAntiAlias(true); textPaint.setTextSize(16.5f); if (w == 32) { textPaint.setTextAlign(Align.CENTER); } textPaint.setTypeface(ZeronerApplication.unifont); StaticLayout sl = new StaticLayout(text, textPaint, w, Alignment.ALIGN_NORMAL, 1.0f, 0.49f, false); int h = sl.getHeight(); if (h > l * 16) { h = l * 16; } Bitmap newBitmap = Bitmap.createBitmap(w, h, Config.ARGB_8888); sl.draw(new Canvas(newBitmap)); return fromAndroidBitmap(newBitmap); } public static PebbleBitmap fromAndroidBitmap(Bitmap bitmap) { int width = bitmap.getWidth(); int height = bitmap.getHeight(); int rowLengthBytes = width / 8; ByteBuffer data = ByteBuffer.allocate(rowLengthBytes * height); data.order(ByteOrder.LITTLE_ENDIAN); StringBuffer stringBuffer = new StringBuffer(StatConstants.MTA_COOPERATION_TAG); for (int y = 0; y < height; y++) { int[] pixels = new int[width]; bitmap.getPixels(pixels, 0, width * 2, 0, y, width, 1); stringBuffer = new StringBuffer(StatConstants.MTA_COOPERATION_TAG); for (int x = 0; x < width; x++) { if (pixels[x] == 0) { stringBuffer.append(Constants.VIA_RESULT_SUCCESS); if (f1285D) { stringBuffer.append("-"); } } else { stringBuffer.append(Constants.VIA_TO_TYPE_QQ_GROUP); if (f1285D) { stringBuffer.append("#"); } } } for (int k = 0; k < rowLengthBytes * 8; k += 8) { ByteBuffer byteBuffer = data; byteBuffer.put(Byte.valueOf((byte) new BigInteger(stringBuffer.substring(k, k + 8), 2).intValue()).byteValue()); } if (f1285D) { stringBuffer.append("\n"); } Log.i("info", stringBuffer.toString()); } if (f1285D) { System.out.println(stringBuffer.toString()); } if (!(bitmap == null || bitmap.isRecycled())) { bitmap.recycle(); } System.gc(); return new PebbleBitmap(UnsignedInteger.fromIntBits(rowLengthBytes), UnsignedInteger.fromIntBits(DfuSettingsConstants.SETTINGS_DEFAULT_MBR_SIZE), (short) 0, (short) 0, (short) width, (short) height, data.array()); } public static PebbleBitmap fromPng(InputStream paramInputStream) throws IOException { return fromAndroidBitmap(BitmapFactory.decodeStream(paramInputStream)); } } 



After a long comprehension, I came to the conclusion that fromString creates a picture with a letter using a certain font (which is sewn into the application), and then converts the pixels to 0 or 1 depending on the fill, so the letter O will look something like this:
00011100
01100011
01100011
01100011
00011100

Without really going into details, I copied everything into my project using the BLE GATT example from Google.
And ... lo and behold !!! The bracelet vibrated! But the message did not appear, the empty line and the icon of the incoming call.
It turned out that a bunch of size checks is not casual, the bracelet stupidly ignores too long messages and messages, which are 11 characters long, although 12 displays normally. A couple of hours of dancing around these functions finally gave results, I learned to display both Russian and English text, and at the same time I learned that there are several modes of operation in the message group:
  1. Incoming call. The handset is displayed, the caller's name and the bracelet are vibrating
  2. Message. Displays text and envelope icon. Vibrates 2 times
  3. Little cloud The same as 2, but instead of an envelope, a cloud icon
  4. Mistake. Same as 2, that only an icon with an exclamation mark.




Having taught my application to send me notifications from different applications, whatsapp, vk, viber, telegram and others, I decided that it was time to teach the bracelet to respond to incoming calls and already, in the end, use the only button to reset incoming calls.

I will not describe this process, the post and it turned out to be bloated, let me just say that it turned out to be not difficult to respond to the incoming ones, but not to use the button.

All incoming messages from the bracelet, Zeroner intercepted in a special class. The incoming packet had a header for a group of commands and a command number. After a long debug and tests, I fished out the groups I used, and then I found the description in the Zeroner code.

Bracelet groups and teams
// HEADER GROUPS //
DEVICE = 0
CONFIG = 1
DATALOG = 2
MSG = 3
PHONE_MSG = 4

// CONFIG = 1 ///
CMD_ID_CONFIG_GET_AC = 5
CMD_ID_CONFIG_GET_BLE = 3
CMD_ID_CONFIG_GET_HW_OPTION = 9
CMD_ID_CONFIG_GET_NMA = 7
CMD_ID_CONFIG_GET_TIME = 1

CMD_ID_CONFIG_SET_AC = 4
CMD_ID_CONFIG_SET_BLE = 2
CMD_ID_CONFIG_SET_HW_OPTION = 8
CMD_ID_CONFIG_SET_NMA = 6
CMD_ID_CONFIG_SET_TIME = 0

// DATALOG = 2 //
CMD_ID_DATALOG_CLEAR_ALL = 2
CMD_ID_DATALOG_GET_BODY_PARAM = 1
CMD_ID_DATALOG_SET_BODY_PARAM = 0

CMD_ID_DATALOG_GET_CUR_DAY_DATA = 7

CMD_ID_DATALOG_START_GET_DAY_DATA = 3
CMD_ID_DATALOG_START_GET_MINUTE_DATA = 5
CMD_ID_DATALOG_STOP_GET_DAY_DATA = 4
CMD_ID_DATALOG_STOP_GET_MINUTE_DATA = 6

// DEVICE = 0 //
CMD_ID_DEVICE_GET_BATTERY = 1
CMD_ID_DEVICE_GET_INFORMATION = 0
CMD_ID_DEVICE_RESE = 2
CMD_ID_DEVICE_UPDATE = 3

// MSG = 3 //
CMD_ID_MSG_DOWNLOAD = 1
CMD_ID_MSG_MULTI_DOWNLOAD_CONTINUE = 3
CMD_ID_MSG_MULTI_DOWNLOAD_END = 4
CMD_ID_MSG_MULTI_DOWNLOAD_START = 2
CMD_ID_MSG_UPLOAD = 0

// PHONE_MSG = 4 //
CMD_ID_PHONE_ALERT = 1
CMD_ID_PHONE_PRESSKEY = 0



Due to this, I was able to implement a full-fledged work with the bracelet. I can get data about the steps, about the dream. I can manage the settings, set alarms. I managed to get the designation of the bytes of the packet itself from the classes that store the data in the database, I implemented them all myself.

Eventually


A little thought, I decided that all this can be useful not only for me and wrote a new application that contains all the necessary data and functions for working with the bracelet, and also implements a simple interface for forwarding alerts from any application to the bracelet.

WiliX iWown for Geek

Since then, a lot of time has passed, and for many after upgrading to Android 6, the application stopped working. It also does not work stably with bracelet firmware version 2. But I hope to find time for refinement.

The source code is posted on github . You can fork and have fun as you like. All pull-request after review will be accepted, and after the tests immediately poured on Google Play.

At the moment, the application can:


A connection to Google Fit was implemented to save training data, but, as I didn’t pick the SDK to Fit, I rummaged through a bunch of links and forums, but didn’t understand how to make the fit display data from custom devices. It is not clear then why this function exists at all.
If someone has worked with Google Fit, and knows how to get him to use data from a custom sensor to display charts, tell in comments or email me, users and I will be very grateful!

It was also the idea to connect the bracelet to Sleep as Adnroid. Actually for monitoring sleep and bought a bracelet. But, as it turned out, iWown can only return the duration of sleep phases. That is, already calculated data from the accelerometer.
And Sleep as Android requires bare data from an accelerometer, and with a desired frequency of 10 seconds.

In the end. I invite developers and owners to support the project with their code, tips and anything. Leave pull-requist, do issue on Github.
The application turned out to be very popular abroad, foreigners often write to me, asking me to add / fix / translate something.

By the way, iWown i5 has several clones, with similar firmware:
Vidonn x5
Harper BFB-301
Excelvan i5

Links

Google Play - iWown for Geek
GitHub Repository
Talk on w3bsit3-dns.com

PS Starting with the 5th version, in androids there was an additional category in the curtain, which is not displayed on the lock screen.
Can someone tell me how to transfer my notice to this category? Thank!

Source: https://habr.com/ru/post/391109/


All Articles