Home IoT – Part 3 – What about my garden irrigation system

This blog post is part of a series of posts were I’ve automated, and Internet of Things (IoT) enabled my garage door, swing gate and Bluetooth enabled garden irrigation system. Links below to each part!

I was about to fall asleep, and then it hit me

So after a late night just completing part 2, I was laying in bed when my mind was wondering of all the things I could do with my ESP32. Hrmm, could I convince my key stakeholder to wire up the lights in the house to this controller? Nah, she was pretty clear with the rule, nothing inside the house. Hrmm, what is there outside the house? My mind went blank as I started to drift to sleep when I forgot about watering the pots in the patio. Crap! I’ll do it tomorrow.

Just as I’m about to nod off, it hits me like a train. WAIT FOR A SECOND, I hate that darn Android app for my Bluetooth garden irrigation system. The ESP32 has a Bluetooth radio! Maybe I can make the same calls to replace the app and water those pots? Maybe I can say to Google, hey, water my garden for 10 minutes. I was excited about the challenge! Okay, after some long, painful nights, I have successfully got my ESP32 making Bluetooth Low Energy (BLE) calls to the Holman BTX-8 Outdoor Garden Irrigation System. For details on the Holman unit, it’s just one that you can find at your local Bunnings store.

Here’s how I IoT enabled my garden irrigation system.

Capturing BLE packets on my Android Phone

So to start with this, I first needed to capture the BLE calls the phone was sending and receiving. If I was to have any chance of replacing the app, I first needed to know what the app does. Turning on developer mode and capturing HCI logs was relatively straight forward on my Samsung S9 phone. I followed this guide and was able to get the logs into WireShark where I could start seeing the important parts that make up Bluetooth client to server communication like Service UUID and the Characteristic UUID.

WireShark view of the logs

Not long after finding the right write characteristic, I could see the hexadecimal calls being made. Thankfully this write characteristic didn’t look to be based on a read value of something else, so the challenge was simplified a bit. Being honest though, this step took a while, with lots of trial and error, calling to stop and start stations so I could pick up the patterns in the captured logs. I could see numbers changing, but I was only 90% sure I had the logic right. At this point, I felt I had a chance of not making a bad write characteristic to the Holman device which could, though unlikely, scramble its brain. To test locally on my PC first before going all-in on coding it on the ESP32, I ended up getting an app called Bluetooth LE Lab. This app was great. I recommended it to anyone trying to reverse engineer Bluetooth calls.

Bluetooth BLE Lab App

After some fun figuring out some of the other values, I concluded that there was a 10-byte hex value that made the Holman device do something. This value broken down looks like:

  • Turns off all the solenoids.
    • 00 00 00 00 00 00 00 00 00 00 –
  • To run a station (open a solenoid)
    • 01 (run)
    • 00 (station 1, starting at 0)
    • 13 (19 hrs in hex)
    • 12 (18 mins in hex)
    • 00 00 00 00 00 00 (used for scheduling a station at day/time, instead of immediately running it)

To state the obvious because it would have been silly, I did not run a station for 19 hours and 18 minutes! During the testing and validation, I also decided that I didn’t need to schedule the station to run as I had a plan to manage those smarts outside of the controller itself.

Coding it up in the ESP32


// The remote service we wish to connect to.
static BLEUUID serviceUUID("C521F000-0D70-4D4F-X-X");
// The characteristic of the remote service we are interested in.
static BLEUUID charUUID("0000F006-0000-1000-X-X");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLEAdvertisedDevice* myDevice;

class MyClientCallback : public BLEClientCallbacks {
    void onConnect(BLEClient* pclient) {
    }

    void onDisconnect(BLEClient* pclient) {
      connected = false;
      Serial.println("onDisconnect");
    }
};

void connectToServer() {
  Serial.print("BLE - Forming a connection to ");
  Serial.println(myDevice->getAddress().toString().c_str());

  BLEClient*  pClient  = BLEDevice::createClient();
  Serial.println("BLE - Created client.");

  pClient->setClientCallbacks(new MyClientCallback());

  // Connect to the remove BLE Server.
  pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
  Serial.println("BLE - Client connected.");

  delay(1000);

  // Obtain a reference to the service we are after in the remote BLE server.
  BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("BLE - Failed to find our service UUID: ");
    Serial.println(serviceUUID.toString().c_str());
    connected = false;
  }
  Serial.println("BLE - Found our service");

  pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
  if (pRemoteCharacteristic == nullptr) {
    Serial.print("BLE - Failed to find our characteristic UUID: ");
    Serial.println(charUUID.toString().c_str());
    connected = false;
  }
  Serial.println("BLE - Found our characteristic");
  connected = true;
}

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.print("BLE - Advertised Device found: ");
      Serial.println(advertisedDevice.toString().c_str());

      if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {
        BLEDevice::getScan()->stop();

        Serial.println("BLE - Correct device found.");
        myDevice = new BLEAdvertisedDevice(advertisedDevice);
        doConnect = true;
        doScan = true;
      }
    }
};

bool makeBLECall(uint8_t* value)
{
  char dataString[30] = {0};
  sprintf(dataString, "%02X %02X %02X %02X%", value[0], value[1], value[2], value[3]);
  String output = dataString;

  Serial.print("BLE - ");
  Serial.println(output);

  if (connected) {
    Serial.println("BLE - Call made.");
    pRemoteCharacteristic->writeValue(value, 4);
    return true;
  }
  return false;
}

void BLEEnable(){
  if (BLEDevice::getInitialized() == false){
    esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
    BLEDevice::init("Cortana Design IoT");
    BLEDevice::setPower(ESP_PWR_LVL_P9);

    Serial.println("BLE - Enabling.");
    BLEScan* pBLEScan = BLEDevice::getScan();
    pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
    pBLEScan->setActiveScan(true);
    pBLEScan->start(5, false);

    Serial.println("BLE - Delaying for 3 seconds.");
    delay(3000);

    if (doConnect == true) {
      connectToServer();
      doConnect = false;
    }

    Serial.println("BLE - Delaying for 3 seconds.");
    delay(3000);
  }
}

void BLEDisable(){
  if (BLEDevice::getInitialized() == true){
    Serial.println("BLE - Disabling.");
    BLEDevice::deinit(false);
  }
}


void setup() {
      server.on("/irrigation", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage0;
    String inputParam0;
    String inputMessage1;
    String inputParam1;
    String inputMessage2;
    String inputParam2;
    String inputMessage3;
    String inputParam3;
    if (request->hasParam(PARAM_INPUT_3) & request->hasParam(PARAM_INPUT_4) & request->hasParam(PARAM_INPUT_5) & request->hasParam(PARAM_INPUT_6)) {
      inputMessage0 = request->getParam(PARAM_INPUT_3)->value();
      inputParam0 = PARAM_INPUT_3;
      inputMessage1 = request->getParam(PARAM_INPUT_4)->value();
      inputParam1 = PARAM_INPUT_4;
      inputMessage2 = request->getParam(PARAM_INPUT_5)->value();
      inputParam2 = PARAM_INPUT_5;
      inputMessage3 = request->getParam(PARAM_INPUT_6)->value();
      inputParam3 = PARAM_INPUT_6;

      uint8_t value[4] = {inputMessage0.toInt(),(inputMessage1.toInt()-1),inputMessage2.toInt(),inputMessage3.toInt()};
      if (makeBLECall(value)){
        request->send(200, "text/plain", "OK");
      }
      else{
        request->send(400, "text/plain", "Something went wrong.");
      }
    }
    else {
      inputMessage0 = "BLE - Incorrect message sent.";
      inputParam0 = "none";
      Serial.println(inputMessage0);
      request->send(400, "text/plain", "Bad Request");
    }
  });
}

When it came to coding this up, I learnt a tough lesson about the importance of keeping your ESP32 codebase small, efficient and optimised as much as possible. It turns out the BLE library for ESP32 is quite big, and when trying to run that with WiFi, a WebServer and MQTT subscription and publishing to Azure IoT Hub, I was overflowing on the 4mb memory of the controller. To solve this, I had to change the memory partitions, but this meant that over the air (OTA) updates were now no longer possible.

This change made updating my C++ code and the webserver inside it only possible over USB, which made progress tedious and a lot slower. I also ran into another issue that as I kept moving the ESP32 back to where I wanted it to be, it couldn’t see or scan for the Holman device. This issue was quite problematic to troubleshoot as I wasn’t able to see the serial output as it wasn’t near my PC. But when I brought it back which subsequently meant it was nearer to the Holman device (located outside on the office PC wall), it worked fine! It took a good day to realise I needed to bump up the power gain settings on the ESP32 to reach the distance I wanted it to.

With a few changes to the webserver, I now had the irrigation controlled via this much simpler interface, which actually works. Did I mention the Holman app is horrible?! Though this webserver was still only on the network, so I also created a new subscriber in Azure IoT too.

It’s now possible, through the ESP32, to control my Bluetooth irrigation system from anywhere in the world!

ESP32 Webserver with the irrigation controls
Integration of the irrigation using IFTTT, the WebHook + Runbook and Azure IoT Hub

In the last blog post, I’ll share the final solution and recap some learnings that I’ve taken away from this project. Stay tuned!

Home IoT – Part 2 – Putting it together and integration

This blog post is part of a series of posts were I’ve automated, and Internet of Things (IoT) enabled my garage door, swing gate and Bluetooth enabled garden irrigation system. Links below to each part!

Okay, time to put this together

So not long after following Rui Santos’s blog post and video (see part 1), I had my ESP32 controller on our home Wi-Fi network and running a webserver that would switch on/off a relay with a toggle function.

To start, I had only wired up the first relay to the appropriate GPIO, but I had compiled the C++ code to make use of all four relays. Yep, I only have the garage and gate to automate, but why not think big!

ESP32 dev-board wired to one relay
Webserver on the ESP32

The first problem with the example code is that I needed my gate and garage door buttons to be a momentary press rather than a switch. A necessary change because I planned to hook up the relay to the normally open (NO) loop of both the garage and gate and with it potentially stuck in the closed position, they were essentially locked out from any other action from the conventional remotes. It also didn’t make sense to turn on and then immediately off the switch to get the functionality I needed. So with a bit of HTML, JS and CSS changes plus giving the GPIOs nicer labels in the C++ code, I had what I was after.

At this point, I was pretty excited. I had a web server that gave me the ability to control my gate and garage when on the network. However, I faced another problem the very next day. A bit more back story, the gate I’ve installed blocks any access to the front of the house and subsequently, the meter boxes. This design decision became a sticking point when the meter reader rang my Ring doorbell on the gate, and I couldn’t let him in because no one was home or on the network. At that point, I was like huh, maybe I could connect my Ring Doorbell to this? Perhaps get it to send a push notification to my phone when it’s rung, that I can acknowledge, and make the call to trigger the momentary button press on the gate?!

The gate in question

In comes Azure IoT Hub and the power of MQTT. Essentially, I needed my ESP32 to be listening to Azure IoT Hub for the call to action and do the thing I needed it to do.

Azure IoT Hub integration with the ESP32

The best way to describe this is a boy (the ESP32) constantly nagging for the chocolate bar (the action) at the exit aisle by pestering mum (Azure IoT Hub). When Azure IoT Hub says yes after persistently saying no, as in changes from 0 to 1, little Jimmy ESP32 gets his chocolate (opens the gate).

To call it out, as it’s sometimes misunderstood, there is no backdoor (inbound access) to the ESP32 from the outside world. MQTT is all about subscribing and publishing messages, so by nature of that principle; it’s always outbound traffic.

Setting up the subscribing and publishing of values was relatively straight forward. In my C++ code, all I needed to do was to connect to the Azure IoT Hub and set up a function in the loop() that checked if the value had changed, do the function to open the gate and then, have another function then publish back after write HIGH (back to LOW) 0 again.

void publishAzuredata(char* event, unsigned int value){
  client.publish(event, value);
}
void subscibeAzuredata(char* event){
  client.subscribe(iothub_subscribe_endpoint);
}

void reconnect() {
   while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (client.connect(iothub_deviceid, iothub_user, iothub_sas_token)) {
      Serial.println("connected");
  
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println("try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void callback(char* topic, byte* payload, unsigned int length) {
  if (String(topic) == "device/deviceID/gate") {
    if (value == "1") {
      digitalWrite(gatePin, HIGH);
      delay(2000);
      client.publish(topic, 0);
      digitalWrite(gatePin, LOW);
    }
    else if (value == "0") {
      //do nothing really
      //digitalWrite(gatePin, LOW);
    }
  }
}

void setup(){
  client.setServer(mqttServer, mqttPort);
  client.setCallback(callback);
  subscibeAzuredata("device/deviceID/gate");
  subscibeAzuredata("device/deviceID/garage");
  connect_mqtt();
}
void loop(){
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

To interface with Azure IoT Hub itself and change the value of 0 to 1, I had a few options like a Logic App or Function App in Azure. However, to keep it simple, I decided to stick with what I know and write a PowerShell script hosted in a Runbook that I could call with a WebHook. That way I could do an HTTP Post with a JSON body that could say open the gate or garage or even both at the same time.

Now that I have the WebHook, I could theoretically call it from anywhere in the world, but I wanted to have a nice way of doing it besides using Postman. Here is where I leveraged IFTTT applets, one for my phone (push notification with IFTTT app), and another for Google Assistant.

Integration with IFTTT and the WebHook

The end result of it all coming together…

ESP32 dev-board and the 4ch relay in a box to be installed

Stay tuned for the next part where I integrate my Bluetooth garden irrigation system with my ESP32 so I can smarten up my irrigation system!