Hacking the Roomba 600


I was in the market for a new vacuum. My old manual one was on the way out, and I'd heard a lot about Roombas, but never actually seen one. I didn't know what they were like, if they were useful, or how well they worked. I came across this YouTube video from The Hook Up which describes hacking the Roomba's 'Open Interface' to talk to it over a serial link. Considering the base level Roombas were cheaper than a manual vacuum, I thought I'd give it a go.

The good news, the specification is completely documented.

The Arduino sketch given on The Hook Up github site was a good start, but I found many issues with it - and me being me - has ended up rewriting and improving just about all of the code given (and not using the Roomba library at all).

The schematic is still the same as The Hook Up's guide - so I'm not going to repeat that information here. If you have a Roomba 600 series and it ends up going to sleep on you quite often, you will need to connect GPIO 0 on the ESP-01S to the BRC pin on the serial interface. When dragged low for a pulse more often than once per minute, this pin stops the Roomba from entering deep sleep. This may not be required on the 500 series (Please leave feedback in the comments!)

When it came to the arduino code, I found many issues.

  • The Roomba would just stop while cleaning, sound a two tone error and stay put.
  • The Roomba would go to sleep and not respond to start commands.
  • An error state could put the Roomba in the wrong baud rate needing a manual reset.
  • There was no debugging information (via a web interface) to see what was going on.
  • There was no way to update firmware on the device after you unsoldered the header on the ESP01.
  • Many more minor issues

The code below allows you to update the firmware in the ESP01 from any https site. If you only use http on your internal network, or you're using an older version of the Arduino ESP8266 SDK without the required BearSSL versions, uncomment the following lines:

//#include <ESP8266HTTPClient.h>

//WiFiClient UpdateClient;
//t_httpUpdate_return result = ESPhttpUpdate.update(UpdateClient, F("http://10.1.1.93/arduino/update/"));

and then comment out the https lines:

BearSSL::WiFiClientSecure UpdateClient;
UpdateClient.setInsecure();
t_httpUpdate_return result = ESPhttpUpdate.update(UpdateClient, F("https://10.1.1.93/arduino/update/"));

This will give you a smaller binary - but you'll lose the ability to use https.

I wrote a script that checks the HTTP_X_ESP8266_STA_MAC header provided by the Arduino update client against a known list, then checks the HTTP_X_ESP8266_SKETCH_MD5 header. If the MD5 of the binary file on the server differs, we return a HTTP 200 OK with the binary file, if they match, then we return a HTTP 304. I'll try to put more of these details online in a different page later on.

If you don't want to do any of the auto-updating, you can just visit the IP address off the ESP01 and click the Update link and upload a new binary that way.

Without further ado, here is my sketch for the Roomba 600 that fixes all of the above:

Changelog

  • 2019-02-11 - Fixed num_restarts from always resetting meaning we now stop if 5 restarts fail. Usually this means the Roomba is stuck somewhere.
#include <PubSubClient.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
//#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include <ESP8266HTTPUpdateServer.h>
#include <SimpleTimer.h>

// USER CONFIGURED SECTION START //
const char* ssid = "YourSSID";
const char* password = "YourPassword";
const char* mqtt_server = "Your.MQTT.IP";
const int mqtt_port = 1883;
const char *mqtt_user = NULL;
const char *mqtt_pass = NULL;
const char *mqtt_client_name = "Roomba"; // Client connections can't have the same connection name

WiFiClient espClient;
PubSubClient client(espClient);
SimpleTimer timer;

#define noSleepPin 0
#define ROOMBA_READ_TIMEOUT 200
#define STATE_UNKNOWN 0
#define STATE_CLEANING 1
#define STATE_RETURNING 2
#define STATE_DOCKED 3

ESP8266WebServer httpServer(80);
ESP8266HTTPUpdateServer httpUpdater;

// Variables
bool boot = true;
bool query_success = false;
uint16_t battery_capacity = 0; //mAh
uint16_t battery_charge = 0; //mAh
int16_t battery_current = 0;
uint8_t battery_percent = 0;
uint8_t battery_temp = 0;
uint16_t battery_voltage = 0;
uint8_t chargeState = 0;
uint8_t num_restarts = 0;
uint8_t current_state = 0;
String last_status;
String update_status;

void setup() {
    setup_wifi();
    checkForUpdate();

    // Reset the Roomba.
    Serial.begin(115200);
    pinMode(noSleepPin, OUTPUT);
    digitalWrite(noSleepPin, HIGH);
    resetRoomba();

    httpServer.on("/", handle_root);
    httpServer.on("/reboot", reboot);
    httpUpdater.setup(&httpServer);
    httpServer.begin();

    client.setServer(mqtt_server, mqtt_port);
    client.setCallback(callback);

    timer.setInterval(10000, getSensors);
    timer.setInterval(59000, StayAwake);
    timer.setInterval(4 * 60 * 60 * 1000, checkForUpdate);  // Check for update every 4 hours.
}

void loop() {
    httpServer.handleClient();
    if (!client.connected()) 
    {
        reconnect();
    }
    client.loop();
    timer.run();
    delay(50);
}

void checkForUpdate() {
    update_status = F("Checking for update...\n");

    BearSSL::WiFiClientSecure UpdateClient;
    UpdateClient.setInsecure();
    t_httpUpdate_return result = ESPhttpUpdate.update(UpdateClient, F("https://10.1.1.93/arduino/update/"));

    //WiFiClient UpdateClient;
    //t_httpUpdate_return result = ESPhttpUpdate.update(UpdateClient, F("http://10.1.1.93/arduino/update/"));

    update_status += F("Returned: ");
    switch(result) {
        case HTTP_UPDATE_FAILED:
            update_status += F("Update failed:\nLastError: ");
            update_status += ESPhttpUpdate.getLastError();
            update_status += F("\nError: ");
            update_status += ESPhttpUpdate.getLastErrorString().c_str();
            update_status += F("\n");
            break;
        case HTTP_UPDATE_NO_UPDATES:
            update_status += F("No Update Available.\n");
            break;
        case HTTP_UPDATE_OK:
            update_status += F("Updated OK.\n");
            break;
    }
}

//Functions
void setup_wifi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
    }
}

void reconnect() {
    if (client.connect(mqtt_client_name, mqtt_user, mqtt_pass, "roomba/status", 0, 0, "Disconnected")) {
        // Once connected, publish an announcement...
        if(boot == false) {
            client.publish("checkIn/roomba", "Reconnected");
        }
        if(boot == true) {
            client.publish("checkIn/roomba", "Rebooted");
            boot = false;
        }
        // ... and resubscribe
        client.subscribe("roomba/commands");
    }
}

void callback(char* topic, byte* payload, unsigned int length) {
    String newTopic = topic;
    payload[length] = '\0';
    String newPayload = String((char *)payload);

    if (newTopic == "roomba/commands") {
        if (newPayload == "start") {
            num_restarts = 0;
            startCleaning();
        }
        if (newPayload == "stop") {
            num_restarts = 0;
            stopCleaning();
        }
    }
}

void getSensors() {
    char tmp[5];
    char buffer[10];

    last_status = "getSensors - Running\n";

    // Clear any read buffer remaining.
    int i = 0;
    while ( Serial.available() > 0 ) {
        Serial.read();
        i++;
        delay(5);
    }
    if ( i > 0 ) {
        last_status = "Dumped ";
        last_status += i;
        last_status += " bytes.\n";
    }

    // Ask for sensor group 3.
    // 21 (1 byte reply) - charge state
    // 22 (2 byte reply) - battery voltage
    // 23 (2 byte reply) - battery_current
    // 24 (1 byte reply) - battery_temp
    // 25 (2 byte reply) - battery charge
    // 26 (2 byte reply) - battery capacity
    byte command[] = { 128, 149, 1, 3 };
    SendCommandList( command, 4 );

    // We should get 10 bytes back.
    i = 0;
    while ( Serial.available() > 0) {
        buffer[i] = Serial.read();
        i++;
        delay(3);
    }

    // Handle if the Roomba stops responding.
    if ( i == 0 ) {
        last_status += "ERROR: No response\n";
        client.publish("roomba/status", "Not Responding");
        client.publish("roomba/currentStatus", "Not Responding");
        return;
    }

    // Handle an incomplete packet (too much or too little data)
    if ( i != 10 ) {
        last_status += "ERROR: Incomplete packet recieved ";
        last_status += i;
        last_status += " bytes.\n";
        return;
    }

    // Parse the buffer...
    chargeState = buffer[0];
    battery_voltage = (uint16_t)word(buffer[1], buffer[2]);
    battery_current = (int16_t)word(buffer[3], buffer[4]);
    battery_temp = buffer[5];
    battery_charge = (uint16_t)word(buffer[6], buffer[7]);
    battery_capacity = (uint16_t)word(buffer[8], buffer[9]);

    // Sanity check some data...
    if ( chargeState > 6 ) { return; }              // Values should be 0-6
    if ( battery_capacity == 0 ) { return; }        // We should never get this - but we don't want to divide by zero!
    if ( battery_capacity > 6000 ) { return; }      // Usually around 2050 or so.
    if ( battery_charge > 6000 ) { return; }        // Can't be greater than battery_capacity
    if ( battery_voltage > 18000 ) { return; }      // Should be about 17v on charge, down to ~13.1v when flat.

    battery_percent = 100 * battery_charge / battery_capacity;
    if ( battery_percent > 100 ) { return; }

    // If we're supposed to be cleaning, but still docked, and finished charging, send the start command again.
    if ( current_state == STATE_CLEANING && chargeState == 4 ) {
        startCleaning();
        return;
    }

    if ( num_restarts > 5 && current_state != STATE_UNKNOWN ) {
        current_state = STATE_UNKNOWN;
        client.publish("roomba/status", "Error");
        client.publish("roomba/currentStatus", "Error");
        return;
    }

    // If we're cleaning or returning, and the battery current is between 0 and 300mA, something is wrong.
    if ( ( current_state == STATE_CLEANING or current_state == STATE_RETURNING ) && battery_current > -300 && battery_current < 0 ) {
        num_restarts++;
        // If we're over 40% battery, start cleaning again, else search for the dock.
        if ( battery_percent >= 40 ) {
            startCleaning();
            return;
        } else {
            stopCleaning();
            return;
        }
    }

    if ( chargeState == 2 or chargeState == 4 ) {
        current_state = STATE_DOCKED;
    }

    // Return if less than 40% battery
    if ( current_state == STATE_CLEANING && battery_percent < 40 ) {
        stopCleaning();
    }

    // Reset num_restarts if current draw is over 300mA
    if ( battery_current < -300 && num_restarts != 0 ) {
        num_restarts = 0;
    }

    itoa(chargeState, tmp, 10);
    client.publish("roomba/charging", tmp);
    itoa(battery_percent, tmp, 10);
    client.publish("roomba/battery", tmp);

    last_status += "getSensors - Success\n";
}

void StayAwake() {
    last_status += "Pulsing the BRC pin...\n";
    digitalWrite(noSleepPin, LOW);
    delay(100);
    digitalWrite(noSleepPin, HIGH);
}

void resetRoomba() {
    byte command[] = { 128, 129, 11, 7 };
    digitalWrite(noSleepPin, LOW);
    delay(100);
    digitalWrite(noSleepPin, HIGH);
    Serial.begin(19200);
    SendCommandList( command, 4 );
    delay(500);
    Serial.begin(115200);
    SendCommandList( command, 4 );
    digitalWrite(noSleepPin, LOW);
    delay(100);
    digitalWrite(noSleepPin, HIGH);
}

void handle_root() {
    String webpage = "<html><head><meta http-equiv=\"refresh\" content=\"5\"></head><body><h3>Roomba stats</h3><ul>";
    webpage += "<li>chargeState: ";
    switch (chargeState) {
        case 0: webpage += "Not Charging"; break;
        case 1: webpage += "Reconditioning"; break;
        case 2: webpage += "Charging"; break;
        case 3: webpage += "Trickle Charge"; break;
        case 4: webpage += "Charged"; break;
        case 5: webpage += "Charging Fault"; break;
    }
    webpage += "</li><li>battery_voltage: ";
    webpage += battery_voltage;
    webpage += " mV</li><li>battery_current: ";
    webpage += battery_current;
    webpage += " mA</li><li>battery_temp: ";
    webpage += battery_temp;
    webpage += " C</li><li>battery_percent: ";
    webpage += battery_percent;
    webpage += " %</li><li>battery_charge: ";
    webpage += battery_charge;
    webpage += " mAh</li><li>battery_capacity: ";
    webpage += battery_capacity;
    webpage += " mAh</li><li>num_restarts: ";
    webpage += num_restarts;
    webpage += "</li><li>current_state: ";
    switch (current_state) {
        case STATE_UNKNOWN: webpage += "Unknown"; break;
        case STATE_CLEANING: webpage += "Cleaning"; break;
        case STATE_RETURNING: webpage += "Returning to Dock"; break;
        case STATE_DOCKED: webpage += "Docked"; break;
    }
    webpage += "</li></ul>Last Status:<br><pre>";
    webpage += last_status;
    webpage += "</pre><br>- <a href=\"/reboot\">Reboot</a><br>- <a href=\"/update\">Update</a><br><br>Update Status:<br><pre>";
    webpage += update_status;
    webpage += "</pre><font size=\"-1\">";
    webpage += ESP.getFullVersion();
    webpage += "</font></body></html>";
    httpServer.send(200, "text/html", webpage);
}

void reboot() {
    String webpage = "<html><head><meta http-equiv=\"refresh\" content=\"10;url=/\"></head><body>Rebooting</body></html>";
    httpServer.send(200, "text/html", webpage);
    client.publish("roomba/status", "Rebooting");
    httpServer.handleClient();
    client.loop();
    delay(100);
    ESP.restart();
}

void startCleaning() {
    current_state = STATE_CLEANING;
    byte command[] = { 131, 135, 128 };
    SendCommandList( command, 3 );
    client.publish("roomba/status", "Cleaning");
}

void stopCleaning() {
    current_state = STATE_RETURNING;
    byte command[] = { 131, 143, 128 };
    SendCommandList( command, 3 );
    client.publish("roomba/status", "Searching for Dock");
}

void SendCommandList( byte *ptr, byte len ) {
    last_status += "TX:";
    for ( int i = 0; i < len ; i++ ) {
        last_status += " ";
        last_status += ptr[i];
        Serial.write(ptr[i]);
        delay(25);
    }
    last_status += "\n";
}

Comments

Comments powered by Disqus