This was a fun exercise to get started with my new ESP32 dev board from Amazon. The robot is cobbled together: Servos and Battery Packs are connected to the Breadboard using neon duct tape, and the R2D2 caster is held on with Velcro. It consists of 2 parts: the ESP32 Robot, and the ESP8266 remote. It could be just as easy to send commands with a cell phone browser or custom app.

The Robot is based around an ESP32 Dev kit. The ESP32 acts as a WiFi access point (AP), and as an HTTP server. I set the on-board LED to turn on whenever the AP has clients connected. Once connected to the access point, the robot responds to GET requests on port 80. The AP defaults to an IP of 192.168.4.1, and accepts 3 main commands for the servos:
  1. Forward: http://192.168.4.1/servo/up
  2. Backward: http://192.168.4.1/servo/down
  3. Stop: http://192.168.4.1/servo/stop

The code is designed so that the Forward command goes more-or-less straight forward, but the Backward command makes it back up and turn, like a child's first RC car. It could be a fun exercise to add support for specifying exact speed or controlling the servos independently, but my remote only had three buttons so that's all I needed. Figuring out how to control the servos was a fun experience, as the analogWrite() command has not yet been implemented in the ESP32 Arduino Core. Thank you to Hackaday's Elliot Williams for introducing me to ledcWrite() and its supporting functions! At 50 Hz and 16-bit depth, values between 1500 and 8000 worked to cover most if not all of the range of my RadioShack (RIP) servos. I had the servos lying around. A couple years ago I modified them for continuous rotation using this tutorial from societyofrobots.com.

The batteries include a 6000 mAh USB power pack and a 4xAA battery holder. I originally powered it using just the USB power, but the system crashed with almost every servo request. Turns out the servos were sucking too much power and browning out the chip. Now the four AA batteries power the servos, the USB pack powers the board, and everything is great. I've read that the ESP32 is supposed to have some special Brownout reset functionality or something. I must not have been taking advantage of this, because the servo calls were causing the processor to halt, not reset.

The Remote is based around the Adafruit Feather HUZZAH and OLED Feather Wing that I received in ADABOX003. The ESP8266 acts as a WiFi Station (STA), and as an HTTP client. All of the Feathers have built in LiPo battery connectors and chargers, so it makes a perfect handheld remote! When turned on, the ESP8266 attempts to connect to the ESP32's access point. SSID and PSK are hard-coded into both AP and STA firmwares. The OLED Feather Wing conveniently includes three buttons labeled A, B and C, so those serve as the Foward, Stop, and Backward buttons. When a button is pressed, the corresponding GET request is sent to the ESP32, and a "+", "0", or "-" is printed on the display.

I initially had issues getting the ESP8266 to connect to the ESP32's AP, or getting "Connection Refused" when making the GET requests. These issues did not manifest when the roles were switched (8266 as AP and 32 as STA works fine all day). All of my issues were solved by these commands in the setup function on the 8266:

WiFi.mode(WIFI_OFF);  // Turn off radio completely
...
delay(1000);          // Wait for '32 to decide '8266 is not connected anymore
...
WiFi.mode(WIFI_STA);  // Turn on radio in STAtion mode: don't broadcast my own SSID

In retrospect, I think the two boards were automatically remembering and connecting to each other's access points, giving each other conflicting identical IP addresses, and mayhem ensued. I'm still getting to know the ESP8266, and I'm often pleasantly surprised by all the features included in such an inexpensive device. The WiFi stack takes care of automatically reconnecting to the AP if the network disappears and reappears (which happened often while flashing and re-flashing the robot). The stack also remembers the SSID and PSK of the last network it connected to, even after reset or power loss. The default behavior is to automatically connect using those saved credentials, and I believe that feature is what caused me my headaches in the first place. The old question: Is it a bug or is it a feature?

Here's the Arduino code running on the robot:

/*
 * servoServer32.ino
 * Matt Gorr
 * 7/9/2017
 *
 */
 
#include<WiFi.h>

const int LEFT_SERVO = 12;
const int RIGHT_SERVO = 14;

char * ssid = "Node32s";
char * password = "Nodemcu32";

int blue = LED_BUILTIN;
int button = 0;
int servoSpeed = 50;

WiFiServer server(80);

void setup() {
  Serial.begin(115200);
  //Serial.setDebugOutput(true);

  //  LEDs
  pinMode(blue, OUTPUT);
  digitalWrite(blue, LOW);

  //  Servos
  initServos();

  // Wireless
  Serial.print("Creating Access Point");
  WiFi.softAPdisconnect();
  WiFi.mode(WIFI_MODE_AP);
  while (!WiFi.softAP(ssid, password)) {
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("Network Created!");
  Serial.print("Soft-AP IP address = ");
  Serial.println(WiFi.softAPIP());

  // Server
  Serial.print("Creating HTTP Server");
  server.begin();
  //WiFi.printDiag(Serial);
}

void loop() {
  digitalWrite(blue, WiFi.softAPgetStationNum());
  WiFiClient client = server.available();
  if (client) {
    // we have a new client sending some request
    String req = client.readStringUntil('\r');
    Serial.println(req);
    String response = processRequest(req);
    while (client.connected()) {
      if (client.available()) {
        String line = client.readStringUntil('\r');
        Serial.print(line);
        if (line.length() == 1 && line[0] == '\n') {
          client.println(prepareHtmlPage(response));
          break;
        }
      }
    }
    delay(1); // give the web browser time to receive the data

    // close the connection:
    client.stop();
    Serial.println("[Client disonnected]");
  }
}

String processRequest(String req) {
  int val = -1;
  if (req.indexOf("/led/0") != -1) {
    val = 0;
  } else if (req.indexOf("/led/1") != -1) {
    val = 1;
  } else if (req.indexOf("/servo/up") != -1) {
    val = -2;
  } else if (req.indexOf("/servo/down") != -1) {
    val = -3;
  } else if (req.indexOf("/servo/stop") != -1) {
    val = -4;
  }

  if (val >= 0) {
    return "No LED functionality right now";
    //digitalWrite(blue, val);
    //return "LED is now " + val ? "ON" : "OFF";
  } else {
    switch (val) {
      case -1:
        return "Thank you for coming";
        break;
      case -2:
        servoSpeed = 100;
        servoControl(servoSpeed);
        return "UP!";
        break;
      case -3:
        servoSpeed = 0;
        servoWrite(0, 0);
        servoWrite(1, 60);
        return "DOWN!";
        break;
      case -4:
        servoSpeed = 50;
        servoControl(servoSpeed);
        return "STOP!";
        break;
    }
  }
  return "Something went wrong";
}
int servoControl(int duty) {
  Serial.print("Recieved command to set servo duty "); Serial.println(duty);

  duty = constrain(duty, 0, 100);
  servoWrite(0, duty);
  servoWrite(1, 100 - duty);
  return duty;
}
String prepareHtmlPage(String output) {
  String htmlPage =
    String("HTTP/1.1 200 OK\r\n") +
    "Content-Type: text/html\r\n" +
    "Connection: close\r\n" +  // the connection will be closed after completion of the response
    "\r\n" +
    "<!DOCTYPE HTML>" +
    "<html>" +
    output +
    "</html>" +
    "\r\n";
  return htmlPage;
}

void initServos() {
  ledcSetup(0, 50, 16); // channel 0, 50 Hz, 16-bit depth
  ledcAttachPin(LEFT_SERVO , 0);
  ledcSetup(1, 50, 16); // channel 1, 50 Hz, 16-bit depth
  ledcAttachPin(RIGHT_SERVO , 1);

}
void servoWrite(int channel, int dutyPercent) {
  int pulse = map(dutyPercent, 0, 100, 1500, 8000);
  ledcWrite(channel, pulse);
  Serial.print("Wrote "); Serial.print(pulse); Serial.print(" to Servo "); Serial.println(channel);
}

Here's the Arduino code running on the Remote:

/*
 * servoClient8266.ino
 * Matt Gorr
 * 7/9/2017
 * 
 */
#include <ESP8266WiFi.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define BUTTON_C 2
#define BUTTON_B 16
#define BUTTON_A 0

#if (SSD1306_LCDHEIGHT != 32)
#error("Height incorrect, please fix Adafruit_SSD1306.h!");
#endif

char * ssid = "Node32s";
char * password = "Nodemcu32";
char* host = "192.168.4.1";

Adafruit_SSD1306 display = Adafruit_SSD1306();
int retryDelaySeconds = 5;

void setup() {
  WiFi.mode(WIFI_OFF);
  Serial.begin(115200);
  //Serial.setDebugOutput(true);

  //  Buttons
  pinMode(BUTTON_A, INPUT_PULLUP);
  pinMode(BUTTON_B, INPUT_PULLUP);
  pinMode(BUTTON_C, INPUT_PULLUP);

  //  OLED Display
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C); 
  display.setTextSize(2);
  display.setTextColor(WHITE);
  display.clearDisplay();
  display.setCursor(0, 32 / 2 - 8);
  display.print("SERVO");
  display.display();
  display.setTextSize(1);
  delay(1000);
  clearScreen();

  //  Wireless
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  long startMillis = millis();
  displayprint("Connecting to " + (String)ssid);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    displayprint(".");
    if (millis() > startMillis + 10000) {
      displayprintln("\nTimed Out :(");
      displayprintln((String)"Retrying in " + retryDelaySeconds + " s");
      WiFi.mode(WIFI_OFF);
      delay(retryDelaySeconds * 1000);
      displayprint("Connecting to " + (String)ssid);
      WiFi.mode(WIFI_STA);
      WiFi.begin(ssid, password);
      startMillis = millis();
    }
  }
  Serial.println();
  Serial.print("Connected, IP address: ");
  Serial.println(WiFi.localIP());

  clearScreen();
  displayprintln("Connected to " + (String)ssid + "!");
  displayprint("IP: "); display.println(WiFi.localIP());
  display.display();
}

void loop() {
  int command = checkButtons();
  switch (command) {
    case BUTTON_A:
      makeRequest(host, "/servo/up");
      displayprint("+");
      break;
    case BUTTON_B:
      makeRequest(host, "/servo/stop");
      displayprint("0");
      break;
    case BUTTON_C:
      makeRequest(host, "/servo/down");
      displayprint("-");
      break;
  }
}

int checkButtons() {
  int del = 100;
  if (!digitalRead(BUTTON_A)) {
    delay(del);
    if (!digitalRead(BUTTON_A)) {
      return BUTTON_A;
    }
  } else if (!digitalRead(BUTTON_B)) {
    delay(del);
    if (!digitalRead(BUTTON_B)) {
      return BUTTON_B;
    }
  } else if (!digitalRead(BUTTON_C)) {
    delay(del);
    if (!digitalRead(BUTTON_C)) {
      return BUTTON_C;
    }
  }
  return -1;
}

boolean makeRequest(char* host, String path) {
  //  Client
  if (WiFi.status() == WL_CONNECTED) {
    //WiFi.printDiag(Serial);
    WiFiClient client;
    Serial.printf("\n[Connecting to %s ... ", host);
    if (client.connect(host, 80)) {
      Serial.println("connected]");
      Serial.println("[Sending a request for " + path + "]");
      client.print(String("GET ") + path + " HTTP/1.1\r\n" +
                   "Host: " + host + "\r\n" +
                   "Connection: close\r\n" +
                   "\r\n"
                  );
      Serial.println("[Response:]");
      while (client.connected()) {
        if (client.available()) {
          String line = client.readStringUntil('\n');
          Serial.println(line);
        }
      }
      client.stop();
      Serial.println("\n[Disconnected]");
      return true;
    } else {
      Serial.println("connection failed!]");
      client.stop();
      delay(5000);
    }
    return false;
  }
}

void displayprint(String text) {
  for (int i = 0; i < text.length(); i++) {
    if (display.getCursorY() > 31) {
      display.clearDisplay();
      display.setCursor(0, 0);
    }
    display.print(text[i]);
  }
  display.display();
}
void displayprintln(String text) {
  displayprint(text + "\n");
}
void clearScreen() {
  display.clearDisplay();
  display.setCursor(0, 0);
}