2019-02-20

Wifi Remote Control

I have started to automate my home and thought that a dedicated remote control would be useful as an alternative to web pages and phone apps. Pushing a button on the remote performs an HTTP request that in turn controls some equipment at home.

An ESP8266 acts as wifi modem. Out of the box, it implements a serial protocol that is used to control its networking functions. An Arduino Pro Mini is master in the system and is responsible for monitoring the buttons and communicating with the ESP8266 to perform the desired action.

When the system is idle the ESP2866 is turned off and the Arduino is put to sleep. It is woken when a button push generates an interrupt on pin 2 or 3 (interrupt 0 and 1). An internal pull up resistor prevents the interrupt from being triggered when the button is inactive.

#include <avr/sleep.h>

#define GREEN_COLUMN 2
#define RED_COLUMN   3

#define FIRST_ROW   A3
#define SECOND_ROW  A2
#define THIRD_ROW   A1


int activeButton = 0;

void green_isr() { ... }

void red_isr() { ... }

void setup()
{
  pinMode(FIRST_ROW, OUTPUT); 
  pinMode(SECOND_ROW, OUTPUT);
  pinMode(THIRD_ROW, OUTPUT);

  digitalWrite(FIRST_ROW, 0);
  digitalWrite(SECOND_ROW, 0);
  digitalWrite(THIRD_ROW, 0);

  pinMode(GREEN_COLUMN, INPUT_PULLUP);
  pinMode(RED_COLUMN, INPUT_PULLUP);
 

  // Low power mode, no ADC
  ADCSRA &= ~(1<<ADEN);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
 

}

void loop()
{
  if (activeButton == 0) {
    attachInterrupt(0, green_isr, LOW);
    attachInterrupt(1, red_isr, LOW);

    ...
    sleep_cpu();
    ...

  } 
  else {
    detachInterrupt(0);
    detachInterrupt(1);

    ...
  }
}

You may notice that I don't use CLI and SEI before SLEEP which is necessary to make sure I don't miss an interrupt. This would have been required with for example interrupt mode FALLING but with interrupt mode LOW new interrupts keep getting generated and it doesn't matter if one is missed. We need the button to be pushed for longer than one interrupt anyway as we need some time to figure out which button is pressed, which leads us to the next step.

The 6 LED buttons are connected to the Arduino in a 3x2 matrix to share the two interrupt enabled pins, one for each column. By setting the row outputs to 0 one by one, it is possible to figure out which button is currently active.

#define GREEN1 5
#define GREEN2 7
#define GREEN3 9
#define RED1   4
#define RED2   6
#define RED3   8
 


int greens[4] = {0, GREEN1, GREEN2, GREEN3};
int reds[4] = {0, RED1, RED2, RED3};
 

int scanRow = 0;

void green_isr() 
{
  if (scanRow != 0) {
    activeButton = greens[scanRow];
  }
}

void red_isr()
{
  if (scanRow != 0) {
    activeButton = reds[scanRow];
  }
}
 

void loop()
{
    ...
    sleep_cpu();

    digitalWrite(FIRST_ROW, 1);
    digitalWrite(SECOND_ROW, 1);
    digitalWrite(THIRD_ROW, 1);

    scanRow = 1;
    digitalWrite(FIRST_ROW, 0);
    digitalWrite(FIRST_ROW, 1);

    scanRow = 2;
    digitalWrite(SECOND_ROW, 0);
    digitalWrite(SECOND_ROW, 1);

    scanRow = 3;
    digitalWrite(THIRD_ROW, 0);
    digitalWrite(THIRD_ROW, 1);

    scanRow = 0;
    digitalWrite(FIRST_ROW, 0);
    digitalWrite(SECOND_ROW, 0);
    digitalWrite(THIRD_ROW, 0);
    ...
}

The interrupt will be triggered when the correct row output is toggled. This results in an assignment of activeButton and the program moves to the second phase where the ESP8266 is started and instructed to do an HTTP request.

The serial data received from ESP8266 is written to a small circular buffer. Whenever a carriage return ("\r") is detected the buffer is inspected to see if we have received a message that we are interested in. The following messages will trigger an action in the code.

ready
OK
GOT IP
CONNECT
CLOSED
FAIL
ERROR

Whenever "ready" or "OK" is received, a command will be sent to the ESP8266. A normal exchange looks something like this.

< ready\r
> AT+CIPMUX=1\r\n
< OK\r
> AT+CWJAP="SSID","password"\r\n
< GOT IP\r
< OK\r
> AT+CIPMUX=1\r\n
< OK\r
> AT+CIPSTART=0,"TCP","192.168.0.102",8081\r\n
< CONNECT\r
< OK\r
> AT+CIPSEND=0,31\r\n
< OK\r
> GET /on?group=Nere HTTP/1.1\r\n\r\n
< OK\r
< OK\r
> AT+CIPCLOSE\r\n
< CLOSED\r

Note the HTTP GET and the dual CRLF which executes the command on the web server. The second "OK" thereafter is part of the response from the web server.

The messages "CLOSED", "FAIL" and "ERROR" terminates the communication, turns off the ESP8266 and puts the Arduino back in sleep mode. There is also a timeout condition that does the same.

#define ESP32_CS 10
#define ESP32_RST 11
 

unsigned long start = 0;

byte next = 0;
#define BUFSZ 16
byte buffer[BUFSZ] = { 0 };

byte   ready[5] = { 'r', 'e', 'a', 'd', 'y' };
byte   gotip[6] = { 'G', 'O', 'T', ' ', 'I', 'P' };
byte      ok[2] = { 'O', 'K' };
byte connect[7] = { 'C', 'O', 'N', 'N', 'E', 'C', 'T' };
byte  closed[6] = { 'C', 'L', 'O', 'S', 'E', 'D' };
byte    fail[4] = { 'F', 'A', 'I', 'L' };
byte   error[5] = { 'E', 'R', 'R', 'O', 'R' };

byte cmd = 0;


void setup()
{
  Serial.begin(115200);


  pinMode(ESP32_CS, OUTPUT);
  pinMode(ESP32_RST, OUTPUT);
  ...

}

void allOff()
{
  digitalWrite(GREEN1, 1);
  digitalWrite(GREEN2, 1);
  digitalWrite(GREEN3, 1);
  digitalWrite(RED1, 1);
  digitalWrite(RED2, 1);
  digitalWrite(RED3, 1);
}

bool received(byte* msg, byte n)
{
  int i;
  for (i = 0; i < n; i++) {
    if (buffer[ ( BUFSZ + next - (n+1) + i ) % BUFSZ ] != msg[i]) {
      return 0;
    }
  }
  return 1;
}

bool activeIsGreen()
{
  return activeButton == GREEN1 || activeButton == GREEN2 || activeButton == GREEN3;
}


void loop()
{
  if (activeButton == 0) {
    ...
    digitalWrite(ESP32_CS, 0);
    digitalWrite(ESP32_RST, 0);

    ...
    start = millis();
  }
  else {

    ...
    digitalWrite(ESP32_CS, 1);
    digitalWrite(ESP32_RST, 1);

    digitalWrite(activeButton, 0);

    if (Serial.available())
    {
      buffer[ ( next++ ) % BUFSZ ] = Serial.read();
      if (buffer[ ( BUFSZ + next-1 ) % BUFSZ ] == '\r') {
        if (received(ok, sizeof(ok))) {
          if (cmd == 1) {
            Serial.print("AT+CWJAP=\"SSID\",\"password\"\r\n");
          }
          else if (cmd == 2) {
            cmd = 3;
            Serial.print("AT+CIPMUX=1\r\n");
          }
          else if (cmd == 3) {
            Serial.print("AT+CIPSTART=0,\"TCP\",\"192.168.0.102\",8081\r\n");
          }
          else if (cmd == 4) {
            cmd = 5;
            if (activeIsGreen()) {
              Serial.print("AT+CIPSEND=0,31\r\n");
            }
            else {
              Serial.print("AT+CIPSEND=0,32\r\n");
            }
          }
          else if (cmd == 5) {
            cmd = 6;
            if (activeIsGreen()) {
              Serial.print("GET /on?group=Nere HTTP/1.1\r\n\r\n");
            }
            else {
              Serial.print("GET /off?group=Nere HTTP/1.1\r\n\r\n");
            }
          }
          else if (cmd == 6) {
            cmd = 7;
            // Wait for HTTP GET 200 OK
          }
          else if (cmd == 7) {
            cmd = 8;
            Serial.print("AT+CIPCLOSE\r\n");
          }
        }
        else if (received(ready, sizeof(ready))) {
          cmd = 1;
          digitalWrite(activeIsGreen() ? RED1 : GREEN1, 0);
          Serial.print("AT+CIPMUX=1\r\n");
        }
        else if (received(gotip, sizeof(gotip))) {
          cmd = 2;
          digitalWrite(activeIsGreen() ? RED2 : GREEN2, 0);
          start = millis();
        }
        else if (received(connect, sizeof(connect))) {
          cmd = 4;
          digitalWrite(activeIsGreen() ? RED3 : GREEN3, 0);
          start = millis();
        }
        else if (received(closed, sizeof(closed)) ||
                 received(fail, sizeof(fail)) ||
                 received(error, sizeof(error))) {
          cmd = 0;
          allOff();
          activeButton = 0;

        }
      }
    }
    else if (millis() - start > 8000UL) {
      cmd = 0;
      allOff();
      activeButton = 0;

    }     
  }

}

Worth noting here is the timeout expression "millis() - start > 8000UL" which always works, especially when the number of millseconds rolls over to zero. A slightly different expression like "millis() > start + 8000UL" does not work as expected close to the rollover point.

As you can see, I have the same action on all green and red buttons, but this can easily be extended with a couple of if statements where the command is sent.

The three LED buttons of the opposite color of the one being pushed are used as a tiny progress bar.

To reduce the power consumption in idle mode I removed the power indicator LEDs from the Arduino and the ESP8266.

When debugging the serial communication I used my PC and the program socat to print a trace of the communication between ESP8266 and Arduino. I used two FTDI USB to Serial cables connected to the Arduino and an adapter board for the ESP8266 which besides the connector for FTDI also contains a 3.3V regulator and voltage dividers on RST, CS and RXT to allow it to be driven from the 5V Arduino. It is visible in the image, here connected directly to the Arduino.

$ socat -v /dev/ttyUSB0 /dev/ttyUSB1

It is highly recommended as a debugging tool.

Felfri produktutveckling

Skojade bara. När vi konstruerar och tillverkar produkter är det alltid möjligt att göra misstag. Konstruktionen fungerar inte som det var t...