HOWTO: Flash firmware on a Sonoff T1 switch

Sonoff is a very popular vendor of IoT devices for the makers among us for two big reasons. First, they are very affordable — and second — like the Sonoff T1, they are very hackable!

The heart of every Sonoff device is a widely-supported microprocessor with built in WiFi called the ESP8266. Many of my own devices are built on this platform (typically using a developer version of the chip), and my firmware CoogleIOT is designed specifically for it.

We’ve discussed hacking off-the-shelf hardware in the past — for example my article on hacking a 4 outlet smart plug to run custom firmware was based on the same ESP8266 microprocessor technology.

Today we’re going to discuss the steps I took to flash my custom firmware onto a Sonoff single-gang switch (the Sonoff T1 US). These devices are great specifically because they are designed to fit right into a standard wall light switch box and support 1 to three individual switches. It’s really nice thing to have when you want both the benefit of a physical switch and IoT support, or the thing the switch controls isn’t something that a smart bulb like Phillips Hue can solve.

In my case, while I have Phillips Hue bulbs for the vast majority of lighting in my house there were a couple of places where they wouldn’t work — specifically my downstairs bathroom that had the lights and the exhaust fan wired together to the same switch. Meanwhile, at my local maker space i3Detroit we use the Sonoff T1 and a whole host of other Sonoff devices to automate everything in the space from air compressors to our laser cutters!

Hacking Sonoff devices isn’t new — in fact there is an entire custom firmware devoted to them and others called Tasmota. But there isn’t a lot of documentation around right now on how to hack the T1 US single gang model, so I thought I’d share how I did it. Let’s get started!

Opening Up to Sonoff T1 US

When you get your Sonoff T1 US, opening it up is a pleasure (I’m pretty sure they’re designed to let us hack them). The front glass panel easily can be removed without any tools at all to expose the underlying capacitive sensor / ESP8266 control board. This board is attached to a power supply board behind it by a standard 8 pin header and no screws — just pull the board out of the case carefully as to not damage the pins.

The Sonoff T1 US control board

For the Sonoff T1 US the ESP8266 chip is tightly integrated into the circuit board rather than being on a soldered on breakout. That’s okay, but it’s going to make our lives a bit more challenging later on.  On the right side you’ll find two sets of headers without any pins, and when you flip the board over you’ll find the corresponding labels indicating their use. We are interested in the top set of headers which are used to program the ESP8266 chip using a standard serial connection.

Soldering the necessary connections

Once you’ve located the serial connection header — the top right unsoldered header — you’re going to want to solder a male header to the first 4 pins (out of 5) representing Vcc, RX, TX, and Ground. The last pin appears to map to the ESP8266’s GPIO2, and isn’t important for us right now.

Header soldered on to the Sonoff T1 US programming pins

While your soldering on those connections, take note as well of the second header block we aren’t really using and note the one labeled ground (GND) — you’ll need to use that in a minute!

Getting ready to program the ESP8266

Now that we have our header soldered on, we need to get ready to program the ESP8266 chip with our new firmware. To do this we’ll need a programming jig or to wire up the appropriate circuit on a breadboard. There are plenty of versions of this jig out there already, for example this one, so I won’t get too deep into how to do that. For my case I etched a custom PCB that attaches to my FTDI USB-to-Serial adapter for this purpose.

Whatever you used for your jig, wire your newly-soldered header appropriately to it for Vcc (3.3V), Serial TX/RX, and of course ground.

Next, you’ll need to of course have a firmware. As I mentioned previously there are a lot of custom firmwares out there you could use, but in my case I have my coogle-switch firmware based on CoogleIOT. For the Sonoff T1 US the ESP8266 chip on board uses the following pins for the following tasks:

  • GPIO 12: Controls the relay of the device (HIGH – on)
  • GPIO 13: Controls the built-in WiFi status light / ESP8266 status LED
  • GPIO 0: Controls the capacitive switch to turn the light on/off

Getting the ESP8266 into programming mode

You’ll note that GPIO 0, which is important in the programming of the ESP8266, is used for the capacitive switch to turn the relay on and off. You might think to yourself that all you would need to do then is power-cycle the ESP8266 while touching the capacitive switch to put the microprocessor into programming mode but you’d be wrong. Unfortunately, this switch appears to simply not be fast enough to make that work and the ESP never gets into programming mode.

To get the Sonoff T1 US into programming mode you’ll need to physically and carefully actually ground GPIO0 on the physical ESP8266 chip itself when you power cycle it.

To do this, let’s take a look at the pinout for the ESP8266 chip itself:

The ESP8266 pinout

You’ll see that GPIO0 is the second pin from the right on the bottom of the chip from the picture above. Looking carefully at the chip itself on the control board and you’ll note that when the board is rotated in such a way that the header you soldered is at the top of the board, the orientation of the ESP8266 chip matches the picture above.

So to get this into programming mode, you’ll need to short pin 15 to ground manually using a jumper wire.

Remember that extra ground on the header I pointed out earlier in the article? Let’s use that.

For this you don’t need to solder anything, I just took a paperclip and a standard male/female jumper cable. I wrapped the paper clip through and around the GND pin of the extra header and attached the female end of the jumper wire to it. This gave me a male end that was attached to the circuit ground I could carefully touch to pin 15 of the ESP8266 while I power cycled it.

You’ll be able to tell if the ESP8266 is in programming mode by the small status LED near it. When it’s in programming mode it should stay a solid blue.

Flashing the ESP8266 with some code

Now we’re ready to do the fun part and actually flash our device. Our firmware can do whatever we want of course, and in my case I my coogle-switch firmware provided an MQTT client to control the relay and some code to manage the physical button to turn things on/off along with the other nice stuff CoogleIOT provides. When compiling the firmware, I used the following board options:

  • Generic ESP8266 module
  • 160 Mhz CPU Frequency
  • 80Mhz Flash Frequency
  • Flash Mode: DIO
  • Flash Size: 1M (128k SPIFFS)
  • Upload Speed: 115200

After you compile your sketch, you need to upload it to the ESP8266 obviously! This is where I had a little difficulty, as my standard tools to do this didn’t seem to want to work with this board. Instead, I needed to use the standard esptool from the command line which I installed using Python’s pip3 tool:

$ sudo pip3 install esptool

This gave me the esptool.py command, which I could then flash my firmware with. To test it to see if it works, you can use the chip_id command as follows to get some data back about the ESP8266 to make sure you’ve successfully put it into programming mode and wired everything correctly:

$ esptool.py --port /dev/cu.usbserial chip_id

Make sure you replace /dev/cu.usbserial with the proper port / device for your USB to Serial adapter!

Assuming you were able to get the chip ID without any errors, you’re now ready to flash your Sonoff T1 US with a new firmware! Put the control board back into programming mode again and use the following to flash your compiled firmware .bin file:

$ esptool.py --port /dev/cu.usbserial write_flash -fs 1MB -fm dout 0x0 /path/to/your/compiled/firmware.bin

If all went well, you should see some progress as the firmware uploads, and upon completion the chip will reboot and your in business! If you’re looking for an example of a firmware that will help you deal with the physical buttons and MQTT, here’s the bulk of the coogle-switch sketch (clone the full repo to actually build this):

/*
  +----------------------------------------------------------------------+
  | CoogleSwitch for ESP8266                                             |
  +----------------------------------------------------------------------+
  | Copyright (c) 2017 John Coggeshall                                   |
  +----------------------------------------------------------------------+
  | Licensed under the Apache License, Version 2.0 (the "License");      |
  | you may not use this file except in compliance with the License. You |
  | may obtain a copy of the License at:                                 |
  |                                                                      |
  | http://www.apache.org/licenses/LICENSE-2.0                           |
  |                                                                      |
  | Unless required by applicable law or agreed to in writing, software  |
  | distributed under the License is distributed on an "AS IS" BASIS,    |
  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or      |
  | implied. See the License for the specific language governing         |
  | permissions and limitations under the License.                       |
  +----------------------------------------------------------------------+
  | Authors: John Coggeshall <john@coggeshall.org>                       |
  +----------------------------------------------------------------------+
*/

#include <CoogleIOT.h>

#include "config.h"

#ifndef STATUS_LED
#define STATUS_LED LED_BUILTIN
#endif

#ifndef SERIAL_BAUD
#define SERIAL_BAUD 115200
#endif

#ifndef NUM_SWITCHES
#define NUM_SWITCHES 4
#endif

#ifndef TOPIC_ID
#define TOPIC_ID "coogle-switch"
#endif

#ifndef SWITCH_BASE_TOPIC
#define SWITCH_BASE_TOPIC ""
#endif

#define SWITCH_BASE_TOPIC "/" TOPIC_ID "/switch/"

CoogleIOT *iot;
PubSubClient *mqtt;

char msg[150];

void setup() {

  iot = new CoogleIOT(STATUS_LED);

  iot->enableSerial(SERIAL_BAUD);
  iot->initialize();

  iot->info("CoogleSwitch Initializing...");
  iot->info("-=-=-=-=--=--=-=-=-=-=-=-=-=-=-=-=-=-");
  iot->logPrintf(INFO, "Number of Switches: %d", NUM_SWITCHES);
  iot->logPrintf(INFO, "MQTT Topic ID: %s", TOPIC_ID);

  iot->info("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-");
  iot->info("Switches");
  iot->info("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-");

  for(int i = 0; i < sizeof(switches) / sizeof(int); i++) {
  pinMode(switches[i], OUTPUT);
  iot->logPrintf(INFO, "  Switch %d on pin %d", i+1, switches[i]);
  }

  iot->info("");

#ifdef HAS_BUTTON
  for(int i = 0; i < sizeof(buttons) / sizeof(int); i++) {
    pinMode(buttons[i], INPUT);
  }
#endif

  if(!iot->mqttActive()) {
  iot->error("Initialization failure, invalid MQTT Server connection.");
    return;
  }

  mqtt = iot->getMQTTClient();

  mqtt->setCallback(mqttCallback);

  for(int i = 0; i < sizeof(switches) / sizeof(int); i++) {
    snprintf(msg, 150, SWITCH_BASE_TOPIC "%d/state", i+1);
    mqtt->publish(msg, "0", true);

    iot->logPrintf(DEBUG, "Publishing to state topic '%s'", msg);

    snprintf(msg, 150, SWITCH_BASE_TOPIC "%d", i+1);
    mqtt->subscribe(msg);

    iot->logPrintf(DEBUG, "Subscribed to action topic '%s'", msg);
  }

  iot->info("Coogle Smart Switch Initialized!");
}

void mqttCallback(char *topic, byte *payload, unsigned int length)
{
    unsigned int switchPin = 0;
    unsigned int switchId = 0;
    unsigned int switchState = 0;

    iot->logPrintf(DEBUG, "MQTT Callback Triggered. Topic: %s\n", topic);

    for(int i = 0; i < sizeof(switches) / sizeof(int); i++) {
    	  snprintf(msg, 150, SWITCH_BASE_TOPIC "%d", i + 1);

    	  if(strcmp(topic, msg) == 0) {
    		  switchPin = switches[i];
    		  switchId = i + 1;
    	  }
    }

    if(switchPin <= 0) {
      return;
    }

    iot->logPrintf(DEBUG, "Processing request for switch %d on pin %d\n", switchId, switchPin);

    if((char)payload[0] == '1') {
      digitalWrite(switchPin, HIGH);
      switchState = HIGH;
    } else if((char)payload[0] == '0') {
      digitalWrite(switchPin, LOW);
      switchState = LOW;
    }

    snprintf(msg, 150, SWITCH_BASE_TOPIC "%d/state", switchId);

    mqtt->publish(msg, switchState == HIGH ? "1" : "0", true);
}

void loop() {
  iot->loop();

#ifdef HAS_BUTTON

  for(int i = 0; i < sizeof(buttons) / sizeof(int); i++) {

  int currentReading;

  currentReading = digitalRead(buttons[i]);

  if(currentReading == LOW) {

    if(buttonReadings[i] == HIGH) {

      if((millis() - lastToggleTimes[i]) > DEBOUNCE_TIME) {

        if(digitalRead(switches[i]) == HIGH) {
          digitalWrite(switches[i], LOW);
        } else {
          digitalWrite(switches[i], HIGH);
        }

        lastToggleTimes[i] = millis();

        iot->flashStatus(200);
      }
    }
    }

    buttonReadings[i] = currentReading;

  }

#endif

}

That’s it for today! If you’re having problems feel free to leave a comment and I’ll try to help, otherwise happy coding!