📜 ⬆️ ⬇️

Controlling a smart BLE lamp without a smartphone

Last summer, when the mess started with the ruble, I decided to buy myself something funny, which I would never have bought under normal price conditions. The choice fell on the smart controlled LED lamp "Luminous BT Smart Bulb", about which, in fact, read before this here . In a good way, first I would have to buy a smartphone with BLE , but at that time I was not worried about such trifles. The lamp arrived, we played a little with her at work, she was pretty funny. But I could not manage it at home, so she went to the shelf. Once, however, I lent a lamp to a colleague for the birthday of a small child.


This went on until I accidentally found out that the Bluetooth 4.0 chip was installed on my laptop. I decided to use this fact somehow to control the light bulb. The minimum program is to learn how to turn on / off a light bulb, set a random color or select one of the specified modes. What came of it - read under the cut.



Everything described below was performed on OS Linux Mint 17. Perhaps there are other ways to work with the BLE stack. And remember, I am not responsible for your equipment.


Reconnaissance


Quickly googling , I realized that to work with BLE on Linux, there is the gatttool command, which is part of the bluez package. But you need to use the latest version of bluez - bluez


My bluez not installed at all, and 4.x is in the repositories, so I set it from source . At that time, the latest was version 5.23 .


Download, unpack, try to install:
 cd ~/Downloads wget https://www.kernel.org/pub/linux/bluetooth/bluez-5.23.tar.gz tar -xvf bluez-5.23.tar.xz cd bluez-5.23 ./configure 

From the first time ./configure unlikely to succeed: some packets need to be delivered.


In my case, it was necessary to install the following:
 sudo aptitude install libdbus-1-dev sudo aptitude install libudev-dev=204-5ubuntu20 sudo aptitude install libical-dev sudo aptitude install libreadline-dev 

For libudev-dev had to explicitly set the version to match the already installed libudev .


Right out of the box, bluez supports systemd integration, which I don’t have. Therefore, support had to be turned off with the --disable-systemd flag .


After that, everything worked:
 ./configure make sudo make install 

Yeah, I know about checkinstall


Going bluez pretty quickly. After the assembly, I still had the cherished gatttool team and even worked somehow. You can move on.


I screwed a light bulb into the base, earned the last selected mode (as it turned out to be an evil strobe blue), and tried out the new tools:


The light bulb is visible in the list - it gave me reassurance:
 sudo hciconfig hci0 up # Host Controller Interface sudo hcitool lescan #  LE- LE Scan ... B4:99:4C:2A:0E:4A (unknown) B4:99:4C:2A:0E:4A (unknown) B4:99:4C:2A:0E:4A (unknown) B4:99:4C:2A:0E:4A (unknown) B4:99:4C:2A:0E:4A LEDnet-4C2A0E4A B4:99:4C:2A:0E:4A (unknown) B4:99:4C:2A:0E:4A LEDnet-4C2A0E4A ... 

We try to connect (you need to use the MAC address from the first column):
 gatttool -I -b B4:99:4C:2A:0E:4A [B4:99:4C:2A:0E:4A][LE]> characteristics Command Failed: Disconnected [B4:99:4C:2A:0E:4A][LE]> connect Attempting to connect to B4:99:4C:2A:0E:4A Connection successful [B4:99:4C:2A:0E:4A][LE]> <TAB> <TAB> char-desc char-read-uuid char-write-req connect exit included primary sec-level char-read-hnd char-write-cmd characteristics disconnect help mtu quit [B4:99:4C:2A:0E:4A][LE]> primary attr handle: 0x0001, end grp handle: 0x0007 uuid: 0000180a-0000-1000-8000-00805f9b34fb attr handle: 0x0008, end grp handle: 0x000b uuid: 0000180f-0000-1000-8000-00805f9b34fb attr handle: 0x000c, end grp handle: 0x0010 uuid: 0000ffe0-0000-1000-8000-00805f9b34fb attr handle: 0x0011, end grp handle: 0x0014 uuid: 0000ffe5-0000-1000-8000-00805f9b34fb attr handle: 0x0015, end grp handle: 0x0033 uuid: 0000fff0-0000-1000-8000-00805f9b34fb attr handle: 0x0034, end grp handle: 0x0042 uuid: 0000ffd0-0000-1000-8000-00805f9b34fb attr handle: 0x0043, end grp handle: 0x004a uuid: 0000ffc0-0000-1000-8000-00805f9b34fb attr handle: 0x004b, end grp handle: 0x0057 uuid: 0000ffb0-0000-1000-8000-00805f9b34fb attr handle: 0x0058, end grp handle: 0x005f uuid: 0000ffa0-0000-1000-8000-00805f9b34fb attr handle: 0x0060, end grp handle: 0x007e uuid: 0000ff90-0000-1000-8000-00805f9b34fb attr handle: 0x007f, end grp handle: 0x0083 uuid: 0000fc60-0000-1000-8000-00805f9b34fb attr handle: 0x0084, end grp handle: 0xffff uuid: 0000fe00-0000-1000-8000-00805f9b34fb [B4:99:4C:2A:0E:4A][LE]> characteristics handle: 0x0002, char properties: 0x02, char value handle: 0x0003, uuid: 00002a23-0000-1000-8000-00805f9b34fb handle: 0x0004, char properties: 0x02, char value handle: 0x0005, uuid: 00002a26-0000-1000-8000-00805f9b34fb handle: 0x0006, char properties: 0x02, char value handle: 0x0007, uuid: 00002a29-0000-1000-8000-00805f9b34fb handle: 0x0009, char properties: 0x12, char value handle: 0x000a, uuid: 00002a19-0000-1000-8000-00805f9b34fb handle: 0x000d, char properties: 0x10, char value handle: 0x000e, uuid: 0000ffe4-0000-1000-8000-00805f9b34fb handle: 0x0012, char properties: 0x0c, char value handle: 0x0013, uuid: 0000ffe9-0000-1000-8000-00805f9b34fb ... 

So, at this stage I was convinced that the connection with a light bulb from a laptop is a reality, which means that further it was necessary to look for control methods. In fact, I started experimenting with the lamp as soon as I connected to it and only then read about GATT , the protocol used by BLE devices. It was necessary to do the opposite, it would save a lot of time. Therefore, I will give here the absolute minimum necessary for understanding.


Crash Course for BLE


There is a small but good article on the Internet on this topic , and I will not tell you better than it. I recommend to get acquainted.


In short, BLE devices consist of a set of services , which in turn consist of a set of characteristics . Services are primary and secondary, but it is not used in the light bulb. Services and features have handles and unique identifiers (UUIDs) . Before reading the above article, I did not understand why we need two unique characteristics. The key feature (very useful for understanding the code below) is that the UUID is a type of service / feature, and the handle is the address where the service / feature is accessed. Those. A device may have several characteristics with some type (for example, several temperature sensors, with the same UUID, but different addresses). Even on two different devices, there may be characteristics with the same UUID and these characteristics should behave the same. Many types have fixed UUIDs (for example, 0x2800 is the primary service, 0x180A is a service with information about the device , etc. ).


You can view all the service / device characteristics in gatttool using the primary and characteristics commands, respectively. You can read the data with the command char-read , write - char-write . Record and reading are made to addresses (handles). Actually, control of any BLE-device occurs through the recording of characteristics, and by reading them we learn the status of devices.


In general, this should be sufficient to understand the principles of lamp control.


The first steps


Only a trifle remained - to find out the addresses of unknown characteristics, where you need to write magical byte sequences, which in one way or another will affect the lamp. Well, at the same time try not to spoil anything.


Initially, I thought that it would be enough to remove the dumps of all-all data from the lamp in different states, compare them, and immediately it becomes clear what is responsible for what. In fact, this was not the case. The only characteristic that really changes from dump to dump is the internal clock. Nevertheless, I will give the code for removing the dump:


Removing a dump from a light bulb
 #!/usr/bin/env groovy def MAC = 'B4:99:4C:2A:0E:4A' def parsePrimaryEntry = { primaryEntry -> def primaryEntryRegex = /attr handle = (.+), end grp handle = (.+) uuid: (.+)/ def matchers = (primaryEntry =~ primaryEntryRegex) if (matchers){ return [ 'attr_handle' : matchers[0][1], 'end_grp_handle' : matchers[0][2], 'uuid' : matchers[0][3] ] } } def parseNestedEntry = { nestedEntry -> def nestedEntryRegex = /handle = (.+), char properties = (.+), char value handle = (.+), uuid = (.+)/ def matchers = (nestedEntry =~ nestedEntryRegex) if (matchers){ return [ 'handle' : matchers[0][1], 'char_properties' : matchers[0][2], 'char_value_handle' : matchers[0][3], 'uuid' : matchers[0][4] ] } } def parseCharacteristicEntry = { characteristicEntry -> def characteristicEntryRegex = /handle = (.+), uuid = (.+)/ def matchers = (characteristicEntry =~ characteristicEntryRegex) if (matchers){ return [ 'handle' : matchers[0][1], 'uuid' : matchers[0][2] ] } } def charReadByHandle = { handle -> def value = "gatttool -b ${MAC} --char-read -a ${handle}".execute().text.trim() } def charReadByUUID = { uuid -> def value = "gatttool -b ${MAC} --char-read -u ${uuid}".execute().text.trim() } def decode = { string -> def matches = (string =~ /Characteristic value\/descriptor\: (.+)/) if(matches) { return matches[0][1].split().collect {Long.parseLong(it, 16)}.inject(''){acc, value -> acc + (value as char)} } } def dump = [:] dump.entries = [] def primaryEntries = "gatttool -b ${MAC} --primary".execute() primaryEntries.in.eachLine { primaryEntry -> def primaryEntryParsed = parsePrimaryEntry(primaryEntry) def entry = [:] primaryEntryParsed.attr_handle_raw_value = charReadByHandle(primaryEntryParsed.attr_handle) primaryEntryParsed.attr_handle_string_value = decode(primaryEntryParsed.attr_handle_raw_value) primaryEntryParsed.end_grp_handle_raw_value = charReadByHandle(primaryEntryParsed.end_grp_handle) primaryEntryParsed.end_grp_handle_string_value = decode(primaryEntryParsed.end_grp_handle_raw_value) primaryEntryParsed.uuid_raw_value = charReadByUUID(primaryEntryParsed.uuid) entry.primary = primaryEntryParsed if ((primaryEntryParsed?.attr_handle) && (primaryEntryParsed?.end_grp_handle)){ entry.nested = [] def nestedEntries = "gatttool -b ${MAC} --characteristics -s ${primaryEntryParsed.attr_handle} -e ${primaryEntryParsed.end_grp_handle}".execute() nestedEntries.in.eachLine { nestedEntry -> def nestedEntryParsed = parseNestedEntry(nestedEntry) nestedEntryParsed.handle_raw_value = charReadByHandle(nestedEntryParsed.handle) nestedEntryParsed.handle_string_value = decode(nestedEntryParsed.handle_string_value) nestedEntryParsed.char_value_handle_raw_value = charReadByHandle(nestedEntryParsed.char_value_handle) nestedEntryParsed.char_value_handle_string_value = decode(nestedEntryParsed.char_value_handle_raw_value) nestedEntryParsed.uuid_raw_value = charReadByUUID(nestedEntryParsed.uuid) entry.nested.add(nestedEntryParsed) } } dump.entries.add(entry) } dump.characteristics = [] def characteristicEntries = "gatttool -b ${MAC} --char-desc".execute() characteristicEntries.in.eachLine { characteristicEntry -> dump.characteristics.add(parseCharacteristicEntry(characteristicEntry)) } def json = new groovy.json.JsonBuilder(dump).toPrettyString() println json 

Of the interesting: in the dumps taken, you can consider the manufacturer of the BLE chip - "SZ RF STAR CO., LTD.".


We'll have to look for other ways. I really didn’t want to dig into mobile apps (I’m not good at Android and I don’t understand at all on iOS), so I first asked for advice from smart uncles on StackOverflow . No one answered and I decided to ask the developer of the Android application. He did not answer either. It turned out that there are several identical applications in the market (judging by the screenshots) for controlling such lamps. The guys from SuperLegend answered me and even sent me to some kind of dock, but, unfortunately, it was not from my light bulb. I found this out by comparing the UUID services in the decompiled application code and in the dock. I compared the decompiled code of both applications and it is absolutely the same, maybe I just sent the documentation from another lamp. I didn't dare to ask again. So, there is only a variant of the analysis of the decompiled code.


Code investigation


A little bit about the actual reverse engineering. It's no secret that two tools are used to research Android applications - apktool and dex2jar . apktool "parses" the APK into its components: resources, XML descriptors and executable code. But these are not Java classes, but special bytecode - smali. Some argue that it is easier to read than Java, but I was born too recently to understand it without a dictionary. However, the resources extracted by apktool will be useful later. To get the usual class files, use dex2jar . After that, classes can be decompiled with a regular decompiler. Taking this opportunity, I would like to recommend any of the latest decompilers: Procyon , CFR or FernFlower . Habitual JADs and other JDs are simply outdated! I also tried Krakatau , but this one seems to be too damp.


Usually I use Procyon, but he has poorly digested input classes. The code of many methods was a mess of named tags and nothing could be understood. Some methods could not be parsed at all. Just at that time, the guys from JetBrains opened their decompiler on Github (FernFlower, for which a special thanks to them) and I tried it. He turned out to be good! The output was quite adequate Java code. True, he also could not decompile some parts, which, fortunately, were too tough for Procyon and CFR. I took as a basis for the analysis the result of the work of FernFlower, and replaced the missing parts with the same pieces from CFR / Procyon (I chose those that were prettier).


A small lesson I learned from the decompilation of obfuscated Android applications: use deobfuscation code built-in to dex2jar . The fact is that the names of classes and methods when building an Android application are reduced to meaningless one- and two-letter. dex2jar can expand them to three- and five-character strings, which makes it easier to navigate by code. Procyon, EMNIP, can do the same thing on its own. Even when using Procyon, the option -ei will be useful, including explicit imports and prohibiting the use of import abc* structures - it is much easier to work with static methods (which are enough). FernFlower and CFR do not use such imports by default.


So, APK is downloaded to the working folder, we decompile:
 apktool d LEDBluetoothV2.apk #  d2j-dex2jar.sh LEDBluetoothV2.apk # Java- d2j-init-deobf.sh -f -o deobf LEDBluetoothV2-dex2jar.jar #   (    deobf) d2j-jar-remap.sh -f -c deobf -o LEDBluetoothV2-dex2jar-deobf.jar LEDBluetoothV2-dex2jar.jar #   mkdir src_fern java -jar ~Projects/fernflower/fernflower.jar LEDBluetoothV2-dex2jar-deobf.jar src_fern java -jar /tools/procyon/procyon-decompiler-0.5.27.jar LEDBluetoothV2-dex2jar-deobf.jar -ei -o src_procyon java -jar /tools/cfr/cfr_0_94.jar LEDBluetoothV2-dex2jar-deobf.jar --outputdir src_cfr 

I went through the code and replaced all the $FF: Couldn't be decompiled with the same code generated by other decompilers. Then I opened the code in IntelliJ IDEA with an Android plugin, set up the Android SDK (you can find out the correct version in the exhaust apktool ) and, voila!, You can apktool it out.


Where to start? After reading the article about working with BLE on Android, it became obvious that first of all you need to look for classes from the android.bluetooth package, for example android.bluetooth.BluetoothGatt . It seems that the entire code for working with BLE in this application is concentrated in the com.Zengge.LEDBluetoothV2.COMM package. Working with characteristics occurs in classes C149c and C144f (the names may be different if you do it yourself).


For example, C144f
 package com.Zengge.LEDBluetoothV2.COMM; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattService; import android.content.Context; import com.Zengge.LEDBluetoothV2.COMM.C145g; import com.Zengge.LEDBluetoothV2.COMM.C149c; import java.util.Iterator; import smb.p06a.C087a; public class C144f extends C149c { static Object Fr = new Object(); C144f fm = this; BluetoothGattService fn; BluetoothGattService fo; boolean fp = false; Object fq = new Object(); boolean fs = false; BluetoothGattCallback ft = new C145g(this); BluetoothGattCharacteristic fu; BluetoothGattCharacteristic fv; public C144f(BluetoothDevice var1) { super(var1); this.fb = var1; } // $FF: synthetic method static BluetoothGattCharacteristic Ma(C144f var0) { if(var0.fd == null) { return null; } else { Iterator var1 = var0.fd.getCharacteristics().iterator(); while(var1.hasNext()) { BluetoothGattCharacteristic var2 = (BluetoothGattCharacteristic)var1.next(); if(Long.toHexString(var2.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FFE4")) { return var2; } } return null; } } // $FF: synthetic method static void Mb(C144f var0) { var0.setChanged(); } private BluetoothGattCharacteristic mpj() { if(this.fo == null) { return null; } else { Iterator var1 = this.fo.getCharacteristics().iterator(); while(var1.hasNext()) { BluetoothGattCharacteristic var2 = (BluetoothGattCharacteristic)var1.next(); if(Long.toHexString(var2.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FE01")) { return var2; } } return null; } } public final BluetoothGatt mPa() { return this.fc; } public final void mPa(byte[] var1) { if(var1.length <= 20) { this.mPa((byte[])var1, 1); } else { this.mPa((byte[])var1, 2); } } public final void mPa(byte[] var1, int var2) { BluetoothGattCharacteristic var3; if(this.ff != null) { var3 = this.ff; } else { Iterator var4 = this.fe.getCharacteristics().iterator(); while(true) { if(!var4.hasNext()) { var3 = null; break; } var3 = (BluetoothGattCharacteristic)var4.next(); if(Long.toHexString(var3.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FFE9")) { this.ff = var3; break; } } } if(var3 != null) { var3.setWriteType(var2); var3.setValue(var1); this.fc.writeCharacteristic(var3); (new StringBuilder("---sendData:")).append(C087a.MPb(var1)).append(" by:").append((Object)var3.getUuid()).toString(); } } public final boolean mPa(Context context, int n) { synchronized (C144f.Fr) { synchronized (this.fq) { if (this.fc == null) { this.fc = this.fb.connectGatt(context, false, this.ft); } if (!(this.fp || this.fc.connect())) { throw new Exception("the connection attempt initiated failed."); } this.fs = false; this.fq.wait(n); } boolean bl = this.fs; this.fs = false; } return bl; } public final void mPb(byte[] var1) { BluetoothGattCharacteristic var2; if(this.fn == null) { var2 = null; } else { Iterator var3 = this.fn.getCharacteristics().iterator(); do { if(!var3.hasNext()) { var2 = null; break; } var2 = (BluetoothGattCharacteristic)var3.next(); } while(!Long.toHexString(var2.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FFF1")); } if(var2 != null) { var2.setWriteType(2); var2.setValue(var1); this.fc.writeCharacteristic(var2); } } public final boolean mPb() { return this.fc != null && this.fd != null && this.fe != null; } public final void mPc(byte[] var1) { BluetoothGattCharacteristic var2; if(this.fn == null) { var2 = null; } else { Iterator var3 = this.fn.getCharacteristics().iterator(); do { if(!var3.hasNext()) { var2 = null; break; } var2 = (BluetoothGattCharacteristic)var3.next(); } while(!Long.toHexString(var2.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FFF2")); } if(var2 != null) { var2.setWriteType(2); var2.setValue(var1); this.fc.writeCharacteristic(var2); } } public final boolean mPc() { return this.fp; } public final void mPd() { if(this.fc != null) { this.fc.disconnect(); this.fc.close(); this.fc = null; } this.fd = null; this.fe = null; this.fp = false; } public final void mPd(byte[] var1) { BluetoothGattCharacteristic var2 = this.mpj(); if(var2 != null) { var2.setWriteType(2); var2.setValue(var1); this.fc.writeCharacteristic(var2); } } public final void mPe() { if(this.fu == null) { BluetoothGattCharacteristic var1; if(this.fn == null) { var1 = null; } else { Iterator var2 = this.fn.getCharacteristics().iterator(); do { if(!var2.hasNext()) { var1 = null; break; } var1 = (BluetoothGattCharacteristic)var2.next(); } while(!Long.toHexString(var1.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FFF3")); } this.fu = var1; } this.fc.readCharacteristic(this.fu); } public final void mPf() { if(this.fv == null) { this.fv = this.mpj(); } this.fc.readCharacteristic(this.fv); } public final BluetoothGattCharacteristic mPg() { if(this.fo == null) { return null; } else { Iterator var1 = this.fo.getCharacteristics().iterator(); while(var1.hasNext()) { BluetoothGattCharacteristic var2 = (BluetoothGattCharacteristic)var1.next(); if(Long.toHexString(var2.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FE03")) { return var2; } } return null; } } public final BluetoothGattCharacteristic mPh() { if(this.fo == null) { return null; } else { Iterator var1 = this.fo.getCharacteristics().iterator(); while(var1.hasNext()) { BluetoothGattCharacteristic var2 = (BluetoothGattCharacteristic)var1.next(); if(Long.toHexString(var2.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FE05")) { return var2; } } return null; } } public final BluetoothGattCharacteristic mPi() { if(this.fo == null) { return null; } else { Iterator var1 = this.fo.getCharacteristics().iterator(); while(var1.hasNext()) { BluetoothGattCharacteristic var2 = (BluetoothGattCharacteristic)var1.next(); if(Long.toHexString(var2.getUuid().getMostSignificantBits()).substring(0, 4).equalsIgnoreCase("FE06")) { return var2; } } return null; } } } 

Yes, and here you have to work with it.


Please note that the characteristics are searched by UUID (types), since the addresses can be different on different lamps (have you forgotten the crash course on BLE ?).


I spent a few evenings renaming methods to something meaningful, such as find_FE03_Characteristic or setAndWrite_FFE9 , and just learning random pieces of code. The logic began to slowly become clearer.


It became clear that those two classes ( C149c and C144f ) are a kind of connection to light bulbs. It seems that a connection is created on each light bulb and it communicates with the lamp through it. Why two classes?


A piece of code that slightly clarifies this point.
 public final void handleMessage(Message var1) { if (var1.what == 0) { C156j.Ma(C157k.Ma(this.fa)); C157k.Ma(this.fa).notifyObservers(); } else if (var1.what == 1) { BluetoothDevice var2 = (BluetoothDevice) var1.obj; (new StringBuilder("onLeScan handleMessage bleDevice:")).append(var2.getName()).toString(); if (var2 != null) { String var3 = var2.getAddress(); String var4 = var2.getName(); if (!C156j.Mb(C157k.Ma(this.fa)).containsKey(var3)) { if (var4 == null) { C144f var5 = new C144f(var2); C156j.Mb(C157k.Ma(this.fa)).put(var3, var5); return; } Boolean isNot_LEDBLUE_or_LEDBLE; if (!var4.startsWith("LEDBlue") && !var4.startsWith("LEDBLE")) { isNot_LEDBLUE_or_LEDBLE = true; } else { isNot_LEDBLUE_or_LEDBLE = false; } if (isNot_LEDBLUE_or_LEDBLE.booleanValue()) { C144f var7 = new C144f(var2); C156j.Mb(C157k.Ma(this.fa)).put(var3, var7); return; } C149c var8 = new C149c(var2); C156j.Mb(C157k.Ma(this.fa)).put(var3, var8); return; } } } } 

This code is called for each detected device . Looks like there are two types of lamps. First names begin with "LEDBlue" or "LEDBLE". The names of the second - do not begin. For work with "LEDBlue" / "LEDBLE" lamps the class C149c , for work with the others - C144f . The name of my light bulb is "LEDnet-4C2A0E4A", so it refers to the second type of lamp. I also noticed in a couple of places a comparison of the device version with the constant "3". If the version is more than three, the class 114f is used (the second type of lamps). Well, the reason to believe that I have the latest lamp versions. Hereinafter, I will call the “LEDBlue” and “LEDBLE” lamps “old”, and the rest “new”.


Periodically, in decompiled code, there are unused StringBuilder 's - unused logging during build. From these lines you can learn a lot of interesting things, such as the names of methods, or at least their purpose. Error messages also help:


I wonder what this method does?
 private boolean startRequestIsPowerOn() { boolean bl; block9: { Object object = Fd; // MONITORENTER : object Object object2 = this.fc; // MONITORENTER : object2 this.fb = null; this.fa.setAndRead_FFF3_Characteristic(); this.fc.wait(5000); // MONITOREXIT : object2 if (this.fb == null) { throw new Exception("request time out:startRequestIsPowerOn!"); } if (this.fb[0] != 0x3f) { byte by = this.fb[0]; bl = false; if (by != -1) break block9; } bl = true; } this.fb = null; // MONITOREXIT : object return bl; } 

All code is replete with synchronized blocks ( MONITOREXIT - it cannot be decompiled), wait 's and notify ' s. Whether this is the result of decompilation, or it is customary to write under Android, or the author ... There are many more Observable . If he had not even been obfuscated, it would have been difficult to read.


Aha We read the characteristic with type FFF3 and find out if the lamp is on. Check on the light bulb (well, when is practice already there on schedule?): If 0xFF written there, it means the lamp is on. Soon we will learn how to turn off the lamp programmatically and find out that 0x3B is stored there in the off state.


From the shell it can be done like this:
 gatttool -b B4:99:4C:2A:0E:4A --char-read -a 0x001d Characteristic value/descriptor: 3f gatttool -b B4:99:4C:2A:0E:4A --char-read -a 0x001d Characteristic value/descriptor: 3b 

Hereinafter we will use the non-interactive mode gatttool (without the -I flag). Addresses of the characteristics can be found in the dump.


The on / off code is a bit more complicated. "" . : "" , , , :


 public static C153o switchBulb(final C144f c144f) { boolean b = true; final C153o c153o = new C153o(); final C142h c142h = new C142h(c144f); try { final boolean mPb = c142h.requestIsPowerOn(); c142h.write_0x4_to_FFF1(); Thread.sleep(200L); if (mPb) { b = false; } c142h.switchBulb(b); c153o.initWithData(true); return c153o; } catch (Exception ex) { c153o.setErrorMessage(ex.getMessage()); return c153o; } finally { c142h.mPa(); } } ... // C142h public final void switchBulb(boolean on) { if (on) { byte[] var2 = new byte[]{(byte) 0x3f}; this.fa.setAndWrite_FFF2_Characteristic(var2); } else { byte[] var3 = new byte[]{(byte) 0x00}; this.fa.setAndWrite_FFF2_Characteristic(var3); } } 

, . , .


, / 0x04 FFF1 , 200 , FFF2 .


:
 gatttool -b B4:99:4C:2A:0E:4A --char-write-req -a 0x0017 -n 04 && sleep 0.2s && gatttool -b B4:99:4C:2A:0E:4A --char-write-req -a 0x001a -n 00 # gatttool -b B4:99:4C:2A:0E:4A --char-write-req -a 0x0017 -n 04 && sleep 0.2s && gatttool -b B4:99:4C:2A:0E:4A --char-write-req -a 0x001a -n 3F # 

, ( -n ) — , , 0x .


'' :
 //      while (var3.hasNext()) { String var4 = (String) var3.next(); C149c var5 = var2.mPb(var4); if (var5.getClass() == C144f.class) { if (var2.mPc(var4).mPe() >= 3) { if (var5 != null) { //   "",      C148b.switchBulb((C144f) var5, Boolean.valueOf(this.fpc)); } } else { // ,    ... var2.mPa(var4, C152n.generateSwitchBulbPowerCommandBytes(this.fpc)); } } else { // ...    var2.mPa(var4, C152n.generateSwitchBulbPowerCommandBytes(this.fpc)); } } ... // var2's class public final boolean mPa(String string, byte[] arrby) { Object object = Fpe; synchronized (object) { C149c c149c = (C149c) this.fpf.get(string); if (c149c == null) return false; c149c.setAndWrite_FFE9(arrby); return true; } } ... public static byte[] generateSwitchBulbPowerCommandBytes(boolean on) { byte[] var1 = new byte[]{(byte) 0xCC, (byte) 0, (byte) 0}; if (on) { var1[1] = 0x23; } else { var1[1] = 0x24; } var1[2] = 0x33; return var1; } 

[0xCC, (0x23|0x24), 0x33] FFE9 . , 0x23 == , 0x24 == . .


, . , . , LEDRGBFragment , :


 static void Ma(LEDRGBFragment var0, int var1) { int red = Color.red(var1); int green = Color.green(var1); int blue = Color.blue(var1); if (var0.fb == C014a.FPf) { byte[] var5 = C152n.MPa(red, green, blue); if (!C156j.MPa().mPa(var0.fa, var5)) { var0.getActivity().finish(); } } else if (var0.fb == C014a.FPb || var0.fb == C014a.FPc || var0.fb == C014a.FPd) { byte[] var6 = C152n.MPb(red, green, blue); if (!C156j.MPa().mPa(var0.fa, var6)) { var0.getActivity().finish(); return; } } } ... //C152n.MPa public static byte[] MPb(int red, int green, int blue) { return new byte[]{(byte) 0x56, (byte) red, (byte) green, (byte) blue, (byte) 0x00, (byte) 0xF0, (byte) 0xAA}; } ... //C156j.MPa().mPa public final boolean mPa(final String[] array, final byte[] array2) { boolean b = true; synchronized (C156j.Fpe) { boolean b2; for (int length = array.length, i = 0; i < length; ++i, b = b2) { final C149c c149c = this.fpf.get(array[i]); if (c149c != null && c149c.isServicesAndGattSet()) { c149c.setAndWrite_FFE9(array2); b2 = b; } else { b2 = false; } } return b; } } 

[0x56, <red>, <green>, <blue>, 0x00, 0xF0, 0xAA] FFE9 (, , ) . C152n , .


:
 gatttool -b B4:99:4C:2A:0E:4A --char-write -a 0x0013 -n 56FF000000F0AA # gatttool -b B4:99:4C:2A:0E:4A --char-write -a 0x0013 -n 5600FF0000F0AA # gatttool -b B4:99:4C:2A:0E:4A --char-write -a 0x0013 -n 560000FF00F0AA # gatttool -b B4:99:4C:2A:0E:4A --char-write -a 0x0013 -n 565A009D00F0AA #  

LEDRGBFragment — LEDWarmWhileFragment . ( [0x56, 0x00, 0x00, 0x00, <value>, 0x0F, 0xAA] ) :


 static void Ma(LEDWarmWhileFragment var0, float var1) { if (var1 == 0.0F) { var1 = 0.01F; } if (var0.fb == C014a.FPe) { C156j.MPa().mPa(var0.fa, C152n.MPa(0, 0, 0, (int) (var1 * 255.0F))); } else { if (var0.fb == C014a.FPb || var0.fb == C014a.FPc || var0.fb == C014a.FPd) { int var3 = (int) (var1 * 255.0F); byte[] var4 = new byte[]{(byte) 0x56, (byte) 0, (byte) 0, (byte) 0, (byte) var3, (byte) 0x0F, (byte) 0xAA}; C156j.MPa().mPa(var0.fa, var4); return; } if (var0.fb == C014a.FPi || var0.fb == C014a.FPh || var0.fb == C014a.FPg) { C156j.MPa().mPa(var0.fa, C152n.MPa((int) (var1 * 255.0F), 0)); return; } } } 

, . "Warm While", -. , . "warm" ( ?) . , " " RGB.


? , apktool ':


- strings.xml
 ... <string name="java_Mode_01">1.Seven color cross fade</string> <string name="java_Mode_02">2.Red gradual change</string> <string name="java_Mode_03">3.Green gradual change</string> <string name="java_Mode_04">4.Blue gradual change</string> <string name="java_Mode_05">5.Yellow gradual change</string> <string name="java_Mode_06">6.Cyan gradual change</string> <string name="java_Mode_07">7.Purple gradual change</string> <string name="java_Mode_08">8.White gradual change</string> <string name="java_Mode_09">9.Red, Green cross fade</string> <string name="java_Mode_10">10.Red blue cross fade</string> <string name="java_Mode_11">11.Green blue cross fade</string> <string name="java_Mode_13">13.Red strobe flash</string> <string name="java_Mode_12">12.Seven color stobe flash</string> <string name="java_Mode_14">14.Green strobe flash</string> <string name="java_Mode_15">15.Blue strobe flash</string> <string name="java_Mode_16">16.Yellow strobe flash</string> <string name="java_Mode_17">17.Cyan strobe flash</string> <string name="java_Mode_18">18.Purple strobe flash</string> <string name="java_Mode_19">19.White strobe flash</string> <string name="java_Mode_20">20.Seven color jumping change</string> ... 

, :


public.xml
 ... <public type="string" name="java_Mode_01" id="0x7f08003f" /> <public type="string" name="java_Mode_02" id="0x7f080040" /> <public type="string" name="java_Mode_03" id="0x7f080041" /> <public type="string" name="java_Mode_04" id="0x7f080042" /> <public type="string" name="java_Mode_05" id="0x7f080043" /> <public type="string" name="java_Mode_06" id="0x7f080044" /> <public type="string" name="java_Mode_07" id="0x7f080045" /> <public type="string" name="java_Mode_08" id="0x7f080046" /> <public type="string" name="java_Mode_09" id="0x7f080047" /> <public type="string" name="java_Mode_10" id="0x7f080048" /> <public type="string" name="java_Mode_11" id="0x7f080049" /> <public type="string" name="java_Mode_13" id="0x7f08004a" /> <public type="string" name="java_Mode_12" id="0x7f08004b" /> <public type="string" name="java_Mode_14" id="0x7f08004c" /> <public type="string" name="java_Mode_15" id="0x7f08004d" /> <public type="string" name="java_Mode_16" id="0x7f08004e" /> <public type="string" name="java_Mode_17" id="0x7f08004f" /> <public type="string" name="java_Mode_18" id="0x7f080050" /> <public type="string" name="java_Mode_19" id="0x7f080051" /> <public type="string" name="java_Mode_20" id="0x7f080052" /> ... 

id ( , ). . , , !, :


 public static ArrayList<BuiltInMode> MPa(Context var0) { ArrayList<BuiltInMode> result = new ArrayList(); result.add(new BuiltInMode((byte) 0x25, "1.Seven color cross fade")); result.add(new BuiltInMode((byte) 0x26, "2.Red gradual change")); result.add(new BuiltInMode((byte) 0x27, "3.Green gradual change")); result.add(new BuiltInMode((byte) 0x28, "4.Blue gradual change")); result.add(new BuiltInMode((byte) 0x29, "5.Yellow gradual change")); result.add(new BuiltInMode((byte) 0x2a, "6.Cyan gradual change")); result.add(new BuiltInMode((byte) 0x2b, "7.Purple gradual change")); result.add(new BuiltInMode((byte) 0x2c, "8.White gradual change")); result.add(new BuiltInMode((byte) 0x2d, "9.Red, Green cross fade")); result.add(new BuiltInMode((byte) 0x2e, "10.Red blue cross fade")); result.add(new BuiltInMode((byte) 0x2f, "11.Green blue cross fade")); result.add(new BuiltInMode((byte) 0x30, "12.Seven color stobe flash")); result.add(new BuiltInMode((byte) 0x31, "13.Red strobe flash")); result.add(new BuiltInMode((byte) 0x32, "14.Green strobe flash")); result.add(new BuiltInMode((byte) 0x33, "15.Blue strobe flash")); result.add(new BuiltInMode((byte) 0x34, "16.Yellow strobe flash")); result.add(new BuiltInMode((byte) 0x35, "17.Cyan strobe flash")); result.add(new BuiltInMode((byte) 0x36, "18.Purple strobe flash")); result.add(new BuiltInMode((byte) 0x37, "19.White strobe flash")); result.add(new BuiltInMode((byte) 0x38, "20.Seven color jumping change")); return result; } 

. Call Hierarchy (, ) , LEDFunctionsFragment , :


 static void setPredefinedMode(LEDFunctionsFragment var0, int builtInModeIndex, float frequency) { //      mPa,    FFE9 C156j.MPa().mPa(var0.fa, new byte[]{ (byte) 0xBB, (byte) (var0.fi.get(builtInModeIndex)).modeIdByte, (byte) (31 - Math.round(29.0F * frequency)), (byte) 0x44}); } 

. 0x01 — , 0x1F — . 0x1F .


 gatttool -b B4:99:4C:2A:0E:4A --char-write -a 0x0013 -n BB250144 #   


- ! , ; , . / / . . , , , .


"" "" FE01 . , . ( groovysh ):


,


 createDateArray = { def instance = Calendar.getInstance(); def year = instance.get(Calendar.YEAR); def month = 1 + instance.get(Calendar.MONTH); // +1 in order to Jan to be "1" def date = instance.get(Calendar.DAY_OF_MONTH); def hour = instance.get(Calendar.HOUR_OF_DAY); def minute = instance.get(Calendar.MINUTE); def second = instance.get(Calendar.SECOND); [(byte)second, (byte)minute, (byte)hour, (byte)date, (byte)month, (byte)(year & 0xFF), (byte)(0xFF & year >> 8)] as byte[] } createDateValue = { createDateArray().collect{Integer.toHexString(it & 0xFF)}.inject(''){acc, val -> acc + val.padLeft(2, '0')} } parseDate = { string -> def array = string.split().collect{Integer.parseInt(it, 16)} def year = (array[6] << 8) | (array[5]) def month = array[4] - 1 def date = array[3] def hour = array[2] def minute = array[1] def second = array[0] def calendar = Calendar.getInstance() calendar.set(year, month, date, hour, minute, second) calendar.time } 

 gatttool -b B4:99:4C:2A:0E:4A --char-read -a 0x0086 Characteristic value/descriptor: 08 36 01 01 01 d0 07 groovy:000> parseDate('08 36 01 01 01 d0 07') ===> Sat Jan 01 01:54:08 FET 2000 groovy:000> createDateValue() ===> 3b1f011e01df07 gatttool -b B4:99:4C:2A:0E:4A --char-write -a 0x0086 -n 3b1f011e01df07 gatttool -b B4:99:4C:2A:0E:4A --char-read -a 0x0086 Characteristic value/descriptor: 04 20 01 1e 01 df 07 groovy:000> parseDate('04 20 01 1e 01 df 07') ===> Fri Jan 30 01:32:04 FET 2015 

FFE9 . , — FFE4 .


At last


, , , . C++ - , libbluetooth node.js , .


, , , - . — pre-BLE :



')

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


All Articles