Wi-Fi Remote using an esp8266 — 5 buttons giving 25 functions for Home Automations (or whatever).

So I wanted to create a device that I could use to control all of my Home Automated devices through MQTT. This is what I came up with. It has 6 buttons: 5 usable buttons and 1 button tied to reset (to act as an on button). The 5 buttons can be pressed and held to trigger a new state, totaling 25+ functions from 5 buttons.

A few years ago I had a similar motivation and worked on my first ever designed/printed circuit board. That particular project just sends out IR codes like a normal TV remote (printed using OshPark, hence the purple board):

Being a ridiculous (but fun) project, It turned out pretty well, but certainly was not “smart”. I hadn’t yet gotten into the (then) new esp8266 wifi stuff. Since moving on to wifi connected projects, I had a strong urge to begin the esp8266 version to control anything & everything controllable through my wifi network. That lead me to this project: the 5 button Wi-Fi remote.

In the following post, I will retrace my steps as I:

  • Pieced together the 6 button top panel using simple components.
  • Hooked up the button panel to an esp8266 (d1 mini or NodeMCU, etc.).
  • Wrote an Arduino sketch to (among other functions), interpret button presses as “short presses” or “long presses” to help get the most out of the 5 usable buttons (1 button reserved to wake it up from deep sleep) .

1) Gathering the parts.

I’ve made a few versions of this while playing with variations of the code, but ultimately this is what I used:

(*info: most links in the list above link to Amazon items through my Amazon Affiliate account. It’s often less expensive to search on eBay if you don’t mind waiting for shipments from China, etc.)

2) Wiring up the button matrix.

One of my goals for this project was to get as many functions as possible from 5 physical buttons without confusing myself with functionality (minus the top right button which is tied to the reset pin for waking up the device from deep sleep). To do this, I ended up going with a button matrix layout and incorporated the Keypad.h Arduino library to assist with getting button “events” so I could easily differentiate a “short press” from a “long press” and create more functions based on that logic. You can press and hold any of the 5 buttons to enter a new “state”, giving you 5 more functions for each individual button – this gives you a total of 25 functions.

Here is a general circuit diagram to help understand the button matrix:

I also made a somewhat simpler version w/out the button matrix. In the end, you end up with 5 wires either way with this configuration… The main reason you would usually use a button matrix is to get more physical buttons using fewer pins. But since I wanted to use the Arduino, Keypad.h library to help with the button “short press” & “long press” logic, I ended up sticking with the button matrix as pictured in the circuit diagram above since that is what this library is expecting. I did end up making both versions though. Using the latter configuration, I just plugged each button into a pin and used the internal pullup resistors on the esp8266, bypassing the need for diodes. In this case, you also have to write custom code to calculate when a button is considered a “short press” or a “long press”.

I spent some time playing with the layout of the button matrix / RGB LED layout as seen above.

My aim here was to keep all the components underneath the protoboard and just have the buttons on top. I used simple breadboard-jumper wire for all the connections so that I could easily connect/disconnect everything from the esp8266 module for debugging, etc. I just snipped one end of the jumper wire and soldered that to the protoboard button matrix. This made the components somewhat “modular” and easier to replace (i.e. when I needed to replace my D1 Mini with a NodeMCU for debugging).

3) The code for the esp8266.

The code is actually a little complex but works well. If you intend to build one yourself, I recommend starting with a blank slate as it will help you mold it to your use cases and help you immensely when debugging. If you approach each problem as being a sort of “lego” that fits into other lego’s then everything becomes much less complex. When looking at my code (or anyone’s code), it’s easy to be overwhelmed with all the “lego” pieces. But taken one at a time, it makes building things a lot easier.

For my uses-cases, I wanted this thing to do the following:

  • Put the esp8266 into deep-sleep after a period of time of non-use and wake it up using the reset button (top right button).
  • Differentiate between “short presses” and “long presses” so I could trigger different MQTT messages &/or functions to get the most out of my 5 physical buttons.
  • Utilize the red, green, and blue colors in the RGB LED to signal various states/functions as the user presses buttons.
  • Use the “ArduinoOTA” library to be able and update the device “Over the Air” when put into programming mode so I never have to open it again.

Here is the full code used for this project:

/* @file wooden-remote-v1_pseudo-channel-RGB.ino
  || @version 1.0
  || @author pseudo-blog
  || @contact 
  ||
  || @description
  || | 5 button WiFi Remote for use with an esp8266 (D1 Mini, NodeMCU, etc.).  
  || #
*/

#include "C:\Users\Justin\Documents\Arduino\credentials.h" //remove
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <Keypad.h>
#include <Ticker.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <ESP8266HTTPClient.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

#define NB_TRYWIFI 60

/**
 * Wifi Credentions (currently using values from 
 * my creds in the ...\credentials.h file imported above.
 */
const char* ssid = STASSID;
const char* password = STAPSK;
/**
 * MQTT Server/Credentions 
 */
const char* mqtt_server = MQTT_SERVER; // mqqt server
const char* mqtt_user = MQTT_USER;
const char* mqtt_password = MQTT_PASSWORD;
/**
 * MQTT Channel 
 */
char* mqtt_channel = "wooden-remote";
/**
 * MQTT client ID: must be unique or will not work. 
 */
const char* clientId = "wooden-remote"; // MUST BE UNIQUE OR MQTT WILL NOT WORK
/**
 * HTTP GET URL 
 */
String baseURLHTTPGet = "http://192.168.1.127/schedule.php?tv=RasPlex2&action=";
/**
 * Sleep timer value in seconds
 */
int sleepTimerSeconds = 45;

#define D0 16
#define D1 5 // I2C Bus SCL (clock)
#define D2 4 // I2C Bus SDA (data)
#define D3 0
#define D4 2 // Same as "D4", but inverted logic
#define D5 14 // SPI Bus SCK (clock)
#define D6 12 // SPI Bus MISO 
#define D7 13 // SPI Bus MOSI
#define D8 15 // SPI Bus SS (CS)
#define D9 3 // RX0 (Serial console)
#define D80 1 // TX0 (Serial console)

/**
 * Keypad.h setup
 */
const byte ROWS = 3; //four rows
const byte COLS = 2; //three columns
char keys[ROWS][COLS] = {
  {'1'},
  {'2', '3'},
  {'4', '5'}
};
byte rowPins[ROWS] = {D1, D2, D3}; //connect to the row pinouts of the keypad
byte colPins[COLS] = {D4, D7}; //connect to the column pinouts of the keypad

Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

/**
 * Using Ticker.h for the watchdog timer, the sleep timer and for the blinking of the LED's.
 */
Ticker watchdogTick;
Ticker sleepTimerTick;
Ticker blinkTick;
Ticker batteryTick;

WiFiClient espClient;

volatile int8_t watchdogCount = 0;
volatile int8_t sleepCounter = 0;

boolean shouldHandleOTA = false;

DynamicJsonDocument doc(1024);

boolean debug = true;

/**
 * LED Pin numbers
 */
// Output
int redPin = D5;   // Red LED, 
int grnPin = D8;  // Green LED,
int bluPin = D6;  // Blue LED,

/**
 * Used to trigger RGB fading when in programming mode
 */
// Color arrays
int black[3]  = { 0, 0, 0 };
int white[3]  = { 100, 100, 100 };
int red[3]    = { 100, 0, 0 };
int green[3]  = { 0, 100, 0 };
int blue[3]   = { 0, 0, 100 };
int yellow[3] = { 40, 95, 0 };
int dimWhite[3] = { 30, 30, 30 };
// etc.

// Set initial color
int redVal = black[0];
int grnVal = black[1]; 
int bluVal = black[2];

// RGB Stuff 
int wait = 2;      // 10ms internal crossFade delay; increase for slower fades
int hold = 0;       // Optional hold when a color is complete, before the next crossFade
int DEBUG = 0;      // DEBUG counter; if set to 1, will write values back via serial
int loopCount = 60; // How often should DEBUG report?
int repeat = 0;     // How many times should we loop before stopping? (0 for no stop)
int j = 0;          // Loop counter for repeat

// Initialize color variables
int prevR = redVal;
int prevG = grnVal;
int prevB = bluVal;

void callback(char* topic, byte* payload, unsigned int length) {

  String message = (char*)payload;
  if(debug){
    Serial.println(message);
  }

}

PubSubClient client(mqtt_server, 1883, callback, espClient);

void ISRwatchdog() {
  watchdogCount++;
  if (watchdogCount == 5) {
    if(debug){
      Serial.println();
      Serial.println("Watchdog ERROR - RESET!");
    }
    ESP.reset();
  }
  //Serial.printf("Watchdog counter = %d\n", watchdogCount);
}

void ISRsleepTimer() {
  sleepCounter++;
  if (sleepCounter == sleepTimerSeconds) {
    if(debug){
      Serial.println();
      Serial.println("Going to sleep!");
    }
    digitalWrite(bluPin, HIGH);
    delayMicroseconds(50000);
    digitalWrite(bluPin, LOW);
    delayMicroseconds(50000);
    digitalWrite(bluPin, HIGH);
    delayMicroseconds(50000);
    digitalWrite(bluPin, LOW);
    delayMicroseconds(500);
    ESP.deepSleep(0);
  }
}

void ISRBlink() {
  int ledState = digitalRead(bluPin);
  if(debug){
    Serial.print(ledState);
  }
  if (ledState) {
    digitalWrite(bluPin, LOW);
  } else {
    digitalWrite(bluPin, HIGH);
  }
}

void ISRBatteryMonitor() {

  int batteryLevel = analogRead(A0);
  if(debug){
    //Serial.print("+++ Battery level: ");
    //Serial.println(batteryLevel);
  }
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    // Attempt to connect
    if (client.connect(clientId, mqtt_user, mqtt_password)) {

    } else {

      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

int getBattLevel() {
  int batteryLevel = analogRead(A0);
  return batteryLevel;
}

void setup() {
  
  pinMode(redPin, OUTPUT);   
  pinMode(grnPin, OUTPUT);   
  pinMode(bluPin, OUTPUT); 
  digitalWrite(redPin, LOW);
  digitalWrite(grnPin, LOW);
  digitalWrite(bluPin, LOW);
  
  if(debug){
    Serial.begin(9600);
    Serial.println(""); Serial.print("Reason startup :"); Serial.println(ESP.getResetReason());
  }

  digitalWrite(grnPin, HIGH);
  delay(5);
  digitalWrite(grnPin, LOW);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  if(debug){
    Serial.println("Connecting to WiFi.");
  }
  int _try = 0;
  while (WiFi.status() != WL_CONNECTED) {
    if(debug){
      Serial.print(".");
    }

    delay(500);
    _try++;
    if ( _try >= NB_TRYWIFI ) {
      if(debug){
        Serial.println("Impossible to connect WiFi network, go to deep sleep");
      }
      digitalWrite(bluPin, HIGH);
      delay(1000);
      digitalWrite(bluPin, LOW);
      delay(1000);
      digitalWrite(bluPin, HIGH);
      delay(1000);
      digitalWrite(bluPin, LOW);
      delay(1000);
      digitalWrite(bluPin, HIGH);
      delay(1000);
      digitalWrite(bluPin, LOW);
      delay(1000);
      digitalWrite(bluPin, HIGH);
      delay(1000);
      digitalWrite(bluPin, LOW);

      ESP.deepSleep(0);
    }
  }
  digitalWrite(bluPin, HIGH);
  delay(200);
  digitalWrite(bluPin, LOW);
  if(debug){
    Serial.println("Connected to the WiFi network");
    Serial.println("");
    Serial.print("Connected to ");
    Serial.println(ssid);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  }

  //keypad.setDebounceTime(20);
  keypad.addEventListener(keypadEvent); // Add an event listener for this keypad
  keypad.setHoldTime(300);

  if(debug){
    Serial.println("should be setting up keypad loop");
  }
  
  watchdogTick.attach(1, ISRwatchdog);
  
  sleepTimerTick.attach(1, ISRsleepTimer);

  //batteryTick.attach(1, ISRBatteryMonitor);

/**
* ArduinoOTA Setup taken from the example code.
*/
  // Port defaults to 8266
  // ArduinoOTA.setPort(8266);

  // Hostname defaults to esp8266-[ChipID]
  ArduinoOTA.setHostname("esp8266-wooden-remote");

  // No authentication by default
  // ArduinoOTA.setPassword("admin");

  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");

  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH) {
      type = "sketch";
    } else { // U_FS
      type = "filesystem";
    }
  
    Serial.println("Start updating " + type);
  });
  ArduinoOTA.onEnd([]() {
    if(debug){
      Serial.println("\nEnd");
    }
    
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    if(debug){
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    }
    
  });
  ArduinoOTA.onError([](ota_error_t error) {
    if(debug){
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) {
        Serial.println("Auth Failed");
      } else if (error == OTA_BEGIN_ERROR) {
        Serial.println("Begin Failed");
      } else if (error == OTA_CONNECT_ERROR) {
        Serial.println("Connect Failed");
      } else if (error == OTA_RECEIVE_ERROR) {
        Serial.println("Receive Failed");
      } else if (error == OTA_END_ERROR) {
        Serial.println("End Failed");
      }
    }
  });
  ArduinoOTA.begin();
  if(debug){
    Serial.println("Ready");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  }
}

/**
* Using ArduinoJson.h to send a JSON payload via MQTT
*/
void sendMQTTMessage(char* topic, char* button) {

  doc["sensor"] = "wooden-remote";
  doc["button"] = String(button);
  doc["batt"] = getBattLevel();

  char buffer[512];
  size_t n = serializeJson(doc, buffer);
  client.publish(topic, buffer, n);

  if(debug){
    Serial.println("Sending MQTT");
    Serial.println(button);
  }

}

char holdKey;
char prevKey;

char previousPressedKey;

boolean hasReleasedKey = false;

KeyState keyState;

/**
* Used to send HTTP GET requests.
*/
void sendHTTPGetRequest(String requestPath) {
  if(debug){
    Serial.println("Sending http request");
  }


  WiFiClient client;
  HTTPClient http;  //Declare an object of class HTTPClient

  if(debug){
    Serial.print("[HTTP] begin...\n");
  }
  
  if (http.begin(client, requestPath)) {  // HTTP

    if(debug){
      Serial.print("[HTTP] GET...\n");
    }
    // start connection and send HTTP header
    int httpCode = http.GET();
    
    // httpCode will be negative on error
    if (httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      if(debug){
        Serial.printf("[HTTP] GET... code: %d\n", httpCode);
      }
      // file found at server
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
        //String payload = http.getString();
        //Serial.println(payload);
      }
    } else {
      if(debug){
        Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
      }
    }
    http.end();
  } else {
    if(debug){
      Serial.printf("[HTTP} Unable to connect\n");
    }
  }
  
  //delay(3000);
  
}

/**
* Triggering OTA updates only when a key combination is pressed.
*/
void handleOTA(){
  
  if(shouldHandleOTA){
  
    ArduinoOTA.handle();
    
  }
  
}

boolean rgbFade = false;
void RGBLedFade(){
  
  if(rgbFade){

    crossFade(red);
    crossFade(green);
    crossFade(blue);
    crossFade(yellow);
  
    if (repeat) { // Do we loop a finite number of times?
      j += 1;
      if (j >= repeat) { // Are we there yet?
        exit(j);         // If so, stop.
      }
    }
    
  }
  
}

/**
* Main Loop. All of the button logic happens in the "keypadEvent()" function.
*/
void loop() {

  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  handleOTA();

  RGBLedFade();

  char key = keypad.getKey();

  watchdogCount = 0;

  yield();
}

/**
* keypadEvent(). 
* This is where you differentiate between a single quick press and a long press
* and take action based on those events.
*/
void keypadEvent(KeypadEvent key){
    switch (keypad.getState()){
    /**
      * PRESSED 
      * 
      */
    case PRESSED:
        sleepCounter = 0;
        if(debug){
          Serial.println("PRESSED");
        }
        digitalWrite(bluPin, LOW);
        delay(50);
        digitalWrite(bluPin, HIGH);
        delay(50);
        digitalWrite(bluPin, LOW);
        prevKey = key;
        break;
     /**
      * RELEASED 
      * 
      */
    case RELEASED:
        sleepCounter = 0;
        if(debug){
          Serial.println("RELEASED");
        }

        if(holdKey != NULL && hasReleasedKey){
          if(debug){
            Serial.println(holdKey);
          }
          
          if( blinkTick.active()){
            blinkTick.detach();
          }
          
          switch (holdKey) {
    
            case '1':
              previousPressedKey = NULL;
              switch (key) {
                case '1':
                  if(debug){
                    Serial.println("Held 1 and pressed 1");
                  }
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=14");
                  sendMQTTMessage(mqtt_channel, "1:1");
                  break;
                case '2':
                  if(debug){
                    Serial.println("Held 1 and pressed 2");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=22");
                  sendMQTTMessage(mqtt_channel, "1:2");
                  break;
                case '3':
                  if(debug){
                    Serial.println("Held 1 and pressed 3");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=07");
                  sendMQTTMessage(mqtt_channel, "1:3");
                  break;
                case '4':
                  if(debug){
                    Serial.println("Held 1 and pressed 4");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=08");
                  sendMQTTMessage(mqtt_channel, "1:4");
                  break;
                case '5':
                  if(debug){
                    Serial.println("Held 1 and pressed 5");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=11");
                  sendMQTTMessage(mqtt_channel, "1:5");
                  break;
              }
              break;
            case '2':
              previousPressedKey = NULL;
              switch (key) {
                case '1':
                  if(debug){
                    Serial.println("Held 2 and pressed 1");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=50");
                  sendMQTTMessage(mqtt_channel, "2:1");
                  break;
                case '2':
                  if(debug){
                    Serial.println("Held 2 and pressed 2");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=51");
                  sendMQTTMessage(mqtt_channel, "2:2");
                  break;
                case '3':
                  if(debug){
                    Serial.println("Held 2 and pressed 3");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=52");
                  sendMQTTMessage(mqtt_channel, "2:3");
                  break;
                case '4':
                  if(debug){
                    Serial.println("Held 2 and pressed 4");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=53");
                  sendMQTTMessage(mqtt_channel, "2:4");
                  break;
                case '5':
                  if(debug){
                    Serial.println("Held 2 and pressed 5");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=54");
                  sendMQTTMessage(mqtt_channel, "2:5");
                  break;
              }
              break;
            case '3':
              previousPressedKey = NULL;
              switch (key) {
                case '1':
                  if(debug){
                    Serial.println("Held 3 and pressed 1");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=21");
                  sendMQTTMessage(mqtt_channel, "3:1");
                  break;
                case '2':
                  if(debug){
                    Serial.println("Held 3 and pressed 2");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=13");
                  sendMQTTMessage(mqtt_channel, "3:2");
                  break;
                case '3':
                  if(debug){
                    Serial.println("Held 3 and pressed 3");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=15");
                  sendMQTTMessage(mqtt_channel, "3:3");
                  break;
                case '4':
                  if(debug){
                    Serial.println("Held 3 and pressed 4");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=12");
                  sendMQTTMessage(mqtt_channel, "3:4");
                  break;
                case '5':
                  if(debug){
                    Serial.println("Held 3 and pressed 5");
                  }
    
                  sendHTTPGetRequest(baseURLHTTPGet + "channel&num=10");
                  sendMQTTMessage(mqtt_channel, "3:5");
                  break;
              }
              break;
            case '4':
              previousPressedKey = NULL;
              switch (key) {
                case '1':
                  if(debug){
                    Serial.println("Held 4 and pressed 1");
                  }
                  //sendMQTTMessage(mqtt_channel, "21");
                  break;
                case '2':
                  if(debug){
                    Serial.println("Held 4 and pressed 2");
                  }
                  //sendMQTTMessage(mqtt_channel, "22");
                  break;
                case '3':
                  if(debug){
                    Serial.println("Held 4 and pressed 3");
                  }
                  //sendMQTTMessage(mqtt_channel, "23");
                  break;
                case '4':
                  if(debug){
                    Serial.println("Held 4 and pressed 4");
                  }
                  //sendMQTTMessage(mqtt_channel, "24");
                  break;
                case '5':
                  if(debug){
                    Serial.println("Held 4 and pressed 5");
                  }
                  //sendMQTTMessage(mqtt_channel, "25");
                  break;
              }
              break;
            case '5':
              previousPressedKey = NULL;
              switch (key) {
                case '1': // Sleep after 2 minutes.
                  if(debug){
                    Serial.println("Held 5 and pressed 1");
                  }
                  sendMQTTMessage(mqtt_channel, "26");
                  sleepTimerSeconds = 120;
                  digitalWrite(grnPin, HIGH);
                  delayMicroseconds(500000);
                  digitalWrite(grnPin, LOW);
                  break;
                case '2': // Sleep after 5 minutes.
                  if(debug){
                    Serial.println("Held 5 and pressed 2"); 
                  }
                  sendMQTTMessage(mqtt_channel, "27");
                  sleepTimerSeconds = 300;
                  digitalWrite(redPin, HIGH);
                  delayMicroseconds(500000);
                  digitalWrite(redPin, LOW);
                  break;
                case '3': 
                  if(debug){
                    Serial.println("Held 5 and pressed 3");
                  }
                  sendMQTTMessage(mqtt_channel, "28");
                  sendMQTTMessage("bedside-clock", "0");
                  digitalWrite(bluPin, HIGH);
                  delayMicroseconds(500000);
                  digitalWrite(bluPin, LOW);
                  break;
                case '4': //SLEEP
                   if(debug){
                    Serial.println("Held 5 and pressed 4");
                  }
                  sendMQTTMessage(mqtt_channel, "29");
                  if(debug){
                    Serial.println();
                    Serial.println("Going to sleep!");
                  }
                  digitalWrite(bluPin, HIGH);
                  delayMicroseconds(500000);
                  digitalWrite(bluPin, LOW);
                  delayMicroseconds(50000);
                  digitalWrite(bluPin, HIGH);
                  delayMicroseconds(500000);
                  digitalWrite(bluPin, LOW);
                  delayMicroseconds(50000);
                  ESP.deepSleep(0);
                  break;
                case '5': // OTA UPDATE MODE
                   if(debug){
                    Serial.println("Held 5 and pressed 5");
                  }
                  sendMQTTMessage(mqtt_channel, "30" );

                  if(!shouldHandleOTA){
                    
                    if(watchdogTick.active()){
                      watchdogTick.detach();
                    }
                    if(sleepTimerTick.active()){
                      sleepTimerTick.detach();
                    }
                    if(batteryTick.active()){
                      batteryTick.detach();
                    }
                    
                    shouldHandleOTA = true;
  
                    //Change LED blink to a faster pace.
                    if(blinkTick.active()){
                      blinkTick.detach();
                    }
                    rgbFade = true;
                    
                  } else {
                      
                    //
                    
                  }
                  
                  break;
              }
              
              break;
    
          }
          if( blinkTick.active() && !shouldHandleOTA){
            blinkTick.detach();
          }
          holdKey = NULL;
          sleepCounter = 0;
 
          digitalWrite(bluPin, LOW);
          hasReleasedKey = false;
  
        } else if(holdKey == NULL && hasReleasedKey == false){

          if(debug){
            Serial.println("Pressed and released main keys.");
            Serial.println(key);
          }
          
          switch (key) {
          case '1':
    
            sendHTTPGetRequest(baseURLHTTPGet + "stop&tv=RasPlex2&");
            
            sendMQTTMessage(mqtt_channel, "1");
            break;
          case '2':
    
            sendHTTPGetRequest(baseURLHTTPGet + "down&tv=RasPlex2&");
    
            sendMQTTMessage(mqtt_channel, "2");
            break;
          case '3':
    
            sendHTTPGetRequest(baseURLHTTPGet + "up&tv=RasPlex2&");
    
            sendMQTTMessage(mqtt_channel, "3");
            //client.publish("debug", "should be going to channel up");
            break;
          case '4':
    
            sendHTTPGetRequest(baseURLHTTPGet + "channel&num=01");
    
            sendMQTTMessage(mqtt_channel, "4");
            break;
          case '5':
            ///
            /// If not in OTA Flash mode, normal operation, else: 
            /// Turn off OTA Mode.
            ///
            if(!shouldHandleOTA){
              sendHTTPGetRequest(baseURLHTTPGet + "channel&num=02");
            
              sendMQTTMessage(mqtt_channel, "5");
            
            } else {

              if(!watchdogTick.active()){
                watchdogTick.attach(1, ISRwatchdog);
              }
              if(!sleepTimerTick.active()){
                sleepTimerTick.attach(1, ISRsleepTimer);
              }
              /*
              if(!batteryTick.active()){
                batteryTick.attach(1, ISRBatteryMonitor);
              }
              */
              shouldHandleOTA = false;

              if( blinkTick.active()){
                blinkTick.detach();
              }

              rgbFade = false;

              digitalWrite(redPin, LOW);
              digitalWrite(grnPin, LOW);
              digitalWrite(bluPin, LOW);
              
            }  
            
            break;
    
          }
                  
        } else { //HOLD KEY

          hasReleasedKey = true;
          
        }
        
        break;
    /**
      * HOLD 
      * 
      */
    case HOLD:
        sleepCounter = 0;
        if(debug){
          Serial.println("HOLD");
        }
        holdKey = key;
        hasReleasedKey = false;

        digitalWrite(bluPin, HIGH);
        
        if(!blinkTick.active()){
          blinkTick.attach(.5, ISRBlink);
        }

        switch (key) {
          case '1':
            sendMQTTMessage(mqtt_channel, "hold:1");
            break;
          case '2':
            sendMQTTMessage(mqtt_channel, "hold:2");
            break;
          case '3':
            sendMQTTMessage(mqtt_channel, "hold:3");
            break;
          case '4':
            sendMQTTMessage(mqtt_channel, "hold:4");
            break;
          case '5':
            sendMQTTMessage(mqtt_channel, "hold:5");
            break;
        }
        
        break;
    }
    
}

/*
 * RGB Fade (taken from): https://www.arduino.cc/en/Tutorial/ColorCrossfader
 */
/* BELOW THIS LINE IS THE MATH -- YOU SHOULDN'T NEED TO CHANGE THIS FOR THE BASICS
* 
* The program works like this:
* Imagine a crossfade that moves the red LED from 0-10, 
*   the green from 0-5, and the blue from 10 to 7, in
*   ten steps.
*   We'd want to count the 10 steps and increase or 
*   decrease color values in evenly stepped increments.
*   Imagine a + indicates raising a value by 1, and a -
*   equals lowering it. Our 10 step fade would look like:
* 
*   1 2 3 4 5 6 7 8 9 10
* R + + + + + + + + + +
* G   +   +   +   +   +
* B     -     -     -
* 
* The red rises from 0 to 10 in ten steps, the green from 
* 0-5 in 5 steps, and the blue falls from 10 to 7 in three steps.
* 
* In the real program, the color percentages are converted to 
* 0-255 values, and there are 1020 steps (255*4).
* 
* To figure out how big a step there should be between one up- or
* down-tick of one of the LED values, we call calculateStep(), 
* which calculates the absolute gap between the start and end values, 
* and then divides that gap by 1020 to determine the size of the step  
* between adjustments in the value.
*/

int calculateStep(int prevValue, int endValue) {
  int step = endValue - prevValue; // What's the overall gap?
  if (step) {                      // If its non-zero, 
    step = 1020/step;              //   divide by 1020
  } 
  return step;
}

/* The next function is calculateVal. When the loop value, i,
*  reaches the step size appropriate for one of the
*  colors, it increases or decreases the value of that color by 1. 
*  (R, G, and B are each calculated separately.)
*/

int calculateVal(int step, int val, int i) {

  if ((step) && i % step == 0) { // If step is non-zero and its time to change a value,
    if (step > 0) {              //   increment the value if step is positive...
      val += 1;           
    } 
    else if (step < 0) {         //   ...or decrement it if step is negative
      val -= 1;
    } 
  }
  // Defensive driving: make sure val stays in the range 0-255
  if (val > 255) {
    val = 255;
  } 
  else if (val < 0) {
    val = 0;
  }
  return val;
}

/* crossFade() converts the percentage colors to a 
*  0-255 range, then loops 1020 times, checking to see if  
*  the value needs to be updated each time, then writing
*  the color values to the correct pins.
*/

void crossFade(int color[3]) {
  // Convert to 0-255
  int R = (color[0] * 255) / 100;
  int G = (color[1] * 255) / 100;
  int B = (color[2] * 255) / 100;

  int stepR = calculateStep(prevR, R);
  int stepG = calculateStep(prevG, G); 
  int stepB = calculateStep(prevB, B);

  for (int i = 0; i <= 1020; i++) {
    redVal = calculateVal(stepR, redVal, i);
    grnVal = calculateVal(stepG, grnVal, i);
    bluVal = calculateVal(stepB, bluVal, i);

    analogWrite(redPin, redVal);   // Write current values to LED pins
    analogWrite(grnPin, grnVal);      
    analogWrite(bluPin, bluVal); 

    delay(wait); // Pause for 'wait' milliseconds before resuming the loop

    if (DEBUG) { // If we want serial output, print it at the 
      if (i == 0 or i % loopCount == 0) { // beginning, and every loopCount times
        
        if(debug){
          Serial.print("Loop/RGB: #");
          Serial.print(i);
          Serial.print(" | ");
          Serial.print(redVal);
          Serial.print(" / ");
          Serial.print(grnVal);
          Serial.print(" / ");  
          Serial.println(bluVal); 
        }
        
      } 
      DEBUG += 1;
    }
  }
  // Update current values for next loop
  prevR = redVal; 
  prevG = grnVal; 
  prevB = bluVal;
  delay(hold); // Pause for optional 'wait' milliseconds before resuming the loop
}

4) How does it work?

When you press the top right button, the device resets and “wakes up” from deep-sleep and connects to Wi-Fi. It stays awake for 45 seconds or as long as it is being used. It slips back into deep-sleep 45 seconds after the last button pressed.

For this setup, each button – when pressed, triggers a function to send an MQTT message over the “wooden-remote” topic carrying the button number with it. From here I have Home Assistant set up and listening to the “wooden-remote” MQTT topic to trigger corresponding automations (i.e. flipping on/off a light, etc.). Some buttons also trigger an HTTP GET request to change channels using Pseudo Channel. When you press and hold any of the 5 usable buttons, it puts the device in a new button state, so each button can trigger a new function. For instance: when you press and hold the 5th button, & press the 5th button again, it puts the device in “programming mode”. This turns off the deep-sleep timer and triggers the ArduinoOTA function so I can program it wirelessly – a function that turned out to be very convenient as I no longer have to open it to program it.

Here it is in action (putting it into programming mode):

Some problems encountered…

I’ve also included a “watchdog timer” so that the device will reset in the case it freezes for some reason. This became helpful in the beginning as I kept running into this bug. After a few button presses, the device would freeze up, the watchdog timer caught it after a few seconds and reset it. This is fixed now. I haven’t completely figured out why this happened but I think it was due to using the wrong LiPo charge module. The bug seemed to pop its head up after the battery would fully discharge (maybe there was a current spike that hurt the esp8266?). After messing up a few D1 Mini’s, I switched to the new LiPo module and used a NodeMCU, and it has been working well ever since.

Conclusion.

This turned out to be one of my most useful projects (I use it daily). Having a simple device to control everything controllable (over wifi) with decent battery life is pretty rad. I’ve since made a few versions placed around the house that stay plugged in without going to sleep. These latter versions are nice as I don’t have to wait for it to connect to wifi if I’m already sitting next to it. A little mini-command center on my side-table. Make one.

4 Replies to “Wi-Fi Remote using an esp8266 — 5 buttons giving 25 functions for Home Automations (or whatever).”

Leave a Reply

Your email address will not be published. Required fields are marked *