A blog

Reverse engineering towards a smarter Bluetooth balcony plant irrigation system

Kamoer’s “Dripper Pro”: A bluetooth-enabled plant irrigation pump

My balcony plants hate me. They complain about too little water with hanging leaves and discountenance me and my gardening skills in general. Even a robot could give them more love. So I built one.

I found a bluetooth-controlled gardening pump on Aliexpress for 25.41€ incl shipping. After hooking up the included drip irrigation hoses to my plants I downloaded the Dripping Pro companion app and set it to drip 100ml per plant, every hour for twelve hours per day (12 is the maximum number of slots per day). I was really satified with how really well this setup works and can absolutely recommend it.

The only thing I was not perfectly happy about was that the pump would water the plants in the same fashion every day, completely disregarding how sunny or rainy the day is. I figured that if I found a way to control the pump directly I could build a system that takes the weather into account. My hope was that it would (1) not completely drown the plants when they need little water and (2) save me from refilling the water tank by using only as much water as needed.

Scouting out a way inside

My first step to complete pump control was to find out the actual manufacturer’s Facebook page and ask about any API documentation or developer resources. In another project I had a great experience getting the latest firmware tools for a thermal printer through a manufacturers Facebook messenger. Unfortunately Kamoer failed to respond :/ The next alternative was to reverse engineer the pump’s Bluetooth communication – which sounded like a fun thing to do.

Using bluetoothctl or gatttool on a computer and BLE scanner on my phone, I could find the device and its mac as well as its BLE services and characteristics. It turns out most of these are pretty useless: There is a Device Information service UUID, but it gives results like Firmware version “Firmware Revision” and Model “Model Number”.
I strongly suspect that the first custom service by the device is some example code that the manufacturer didn’t bother to remove. You can see the characteristics with Descriptors like “Characteristic 3”, all different combinations of R/W/N properties, and values 0x01, 0x02, 0x03, 0x04, and 0x0102030405.
The second custom service looks more promising. The descriptor reads “Key Press State”, which is funny, because my pump doesn’t even have a button. I try to write a 0x01 to the characteristics, but nothing happens. It seems the app talks via some protocol with the pump.

Screenshot of BLEScanner‘s view of the device services and characteristics

When you search the internet for the 0000ffe0-0000-1000-8000-00805f9b34fb service UUID, it turns out that it is the UUID that almost all iBeacon tag manufacturers use. It likely comes from a TI devkit example with a “Simple Keys Service (0xFFE0)” that this application builds on. Let’s try to find out more about what messages to send (and to which characteristics) by capturing the packets that the original app is sending.

Bluetooth packet sniffing

It is possible to sniff BLE traffic with special hardware like Ubertooth or an nRF devkit, but I don’t have any of those. Instead, I will try to enable recording of Android’s Bluetooth HCI logs while using app. Enabling the logging option in developer options is not difficult (make sure to reboot after), but extracting the logs proved a little more involved. I got it working on my Galaxy S8 with adb:
adb shell dumpsys bluetooth_manager ; adb bugreport > BUG_REPORT.txt
It takes a minutes to complete, but generates a bugreport-2019-08-27-18-49-17.zip that contains a file FS/data/log/bt/btsnoop_hci.log which you can open with Wireshark.

Bluetooth sniffing log in Wireshark

The log contains a lot of data, which is difficult to parse visually. Using a display filter bthci_acl.dst.bd_addr == 90:E2:02:9C:DB:50 I can focus on the packets sent from the phone to the pump. Interestingly the app always tries to write request handle 0x37, to which the device responds with application error 0x80. Maybe it’s for a different model? After each write request follows a write command. It’s still a little difficult to switch between packet views, so I export the write command packages in plaintext format into a word processor.

Extracting Meaning from data

First, I did a bunch of regex search/replace to get only timestamps and packet data. The packet timestamps correspond to how I used the app (Open, correct one automatic-watering volume slot value, switch to manual control tab, start/stop twice). I colorized the lines based on my hypothesis for their meaning.

Colorizing and commenting bytes to figure out their meaning

It turns out the app does write to the “Key Press State” characteristic like I tried earlier, but it seems like it sends three rather complex 32 byte packets (In the document I removed the preamble and 12 trailing zero bytes) to start and another one to stop.

Crafting a dripper-control library

To confirm my hypothesis, I started an IPython shell, import gatt and jotted down a quick and dirty script. It worked! Celebration! That was fun!

I condensed the whole thing into a library which makes it a little easier to use: After a pip3 install btdripper you can run:

import btdripper
dripper = btdripper.BtDripper()

Programmatic access to the pump opens up new possibilities and a path to creating the best 25€ balcony irrigation system ever to reside on my balcony.
In Part 2 of this story I will probably fight weather APIs and Linux Crontabs. All for my gardening machine of loving grace.

« »
%d bloggers like this: