Close

Important high level stuff

A project log for Controll yer Roomba 600 series with ESP8266

Another ESP8266 project. If some hardware has an interface / API to connect to, it's waiting to be assimilated into the home-hive.

Simon JansenSimon Jansen 01/22/2022 at 20:420 Comments

Ok. So it works and cleans my house on schedule. That's nice and all, but I noticed it can play tunes and I want more.

This project can't continue before it has come to life. I've spend at least a week getting this far and all I got was an extra scheduling feature that you can buy. But can you buy this??:

I don't think so! (because, why would you?)

Using a library to interface with the Roomba:

Up until now I commanded the Roomba with Serial.write commands. This is something like re-inventing the wheel (or using duct tape and a swiss army knife to build a bamboo plane). It's hard and inefficient. 

So I decided to make use of a library someone else wrote. The iRobot documents point to this library: https://github.com/brinnLabs/Create2

On inspection, it uses way too much code. It uses two ways to write to serial in every command. It has all the scheduling functions which my Roomba doesn't have. 

But it seems as good a start as any. 

I started with removing all the double stuff and functions I don't need. I then tried to use it and get it to compile an run stable with all the other functions (OTA + MQTT).

When this was up and running, where back to where we were, but now with a library. That's just classy (see what I did there?)

Composing:

One of the reasons I chose this library is because it has all the MIDI notes defined in the header file:

enum notes{
	N_G1 = 31,
	N_G1S = 32,
	N_A1 = 33,
	N_A1S = 34,
	N_B1 = 35,
	N_C1 = 36,
	N_C1S = 37,
	N_D1 = 38,
	N_D1S = 39,
	N_E1 = 40,
	N_F1 = 41,
	N_F1S = 42,
	N_G2 = 43,
	N_G2S = 44,
	N_A2 = 45,
	N_A2S = 46,
*
*
	N_A7 = 105,
	N_A7S = 106,
	N_B7 = 107
};

And there is a struct for a "note":

struct Notes {
	byte note;
	byte duration;
};

It seems that I have to find the note and the duration and I can make it sing! Simple!

The Roomba lets you create 4 "songs" of 16 notes. These can then be recalled. 

In the example:

Notes songNotes[16];
songNotes[0].note = 76;
songNotes[1].note = 76;
songNotes[2].note = 76;
songNotes[3].note = 76;
songNotes[4].note = 76;
songNotes[5].note = 76;
songNotes[6].note = 76;
songNotes[7].note = 79;
songNotes[8].note = 72;
songNotes[9].note = 74;
songNotes[10].note = 76;
  
songNotes[0].duration = 16;
songNotes[1].duration = 16;
songNotes[2].duration = 32;
songNotes[3].duration = 16;
songNotes[4].duration = 16;
songNotes[5].duration = 32;
songNotes[6].duration = 16;
songNotes[7].duration = 16;
songNotes[8].duration = 16;
songNotes[9].duration = 16;
songNotes[10].duration = 64;
  
roomba.createSong(1, 11, songNotes);
  
for(int i=0;i<11;i++){
    totalDuration1 += songNotes[i].duration;
}
roomba.playSong(1);
totalDuration1 = 100+1000*(totalDuration1/64.0);
delay(totalDuration1);

I couldn't find good source material for single notes, I don't have a piano at hand and I cant read musical notation (any more).  So the best I could come up with is a Youtube tutorial play allong guitar hero style:

I then transcribed the played notes and length to code. But it sounded like my poor Roomba had had a severe stroke :|

I tried all sort of things and every try meant

  1. Type new iteration in code;
  2. Upload code OTA to Roomba;
  3. Wait for connection with MQTT-broker (it would announce its presence by publishing to it's state topic);
  4. Send command to it's MQTT command topic;
  5. Feel sorry for the Roomba and myself and GOTO label 1;

Then I noticed the notes in the library were WRONG! The notes have the wrong octave number!

The documentation gives the Frequency:

Comparing this to a Wiki page on MIDI notes tells me the octave number doesn't start with 1 like a normal person. But instead is offset from the letters like this:

enum notes{
	N_G2 = 31,
	N_G2S = 32,
	N_A2 = 33,
	N_A2S = 34,
	N_B2 = 35,
	N_C3 = 36,
	N_C3S = 37,
	N_D3 = 38,

The rollover in octave number is from B to C and not from G to A. 

Now it started to sound reasonable. 

I ended up with the following:

#include <ESP8266WiFi.h>    //For ESP8266
#include <PubSubClient.h>   //For MQTT
#include <ESP8266mDNS.h>    //For OTA
#include <WiFiUdp.h>        //For OTA
#include <ArduinoOTA.h>     //For OTA

#include <Roomba632.h>
Roomba632 roomba;

// WIFI configuration
#define wifi_ssid "***"
#define wifi_password "***"

//OTA configuration
const char* host = "Roomba632"; //84ed23
//const char* host = "ESP8285_Testplatform"; //80e74a
const char * sketchpass = "***";

// MQTT configuration
#define mqtt_server "192.168.1.***"
#define mqtt_user "***"
#define mqtt_password "***"  
#define mqtt_sub_topic "homeassistant/device/roomba/set"
#define mqtt_pub_topic "homeassistant/device/roomba/state"
String mqtt_client_id= host;   //This text is concatenated with ChipId to get unique client_id
long lastReconnectAttempt = 0;


//Songs:
Notes macGyverNotes0[16] = {
  {N_B3, 16},
  {N_E4, 16},
  {N_A4, 16},
  {N_B4, 16},
  {N_A4, 16},
  {N_B3, 16},
  {N_E4, 16},
  {N_B3, 16},
  {0   , 16},
  {N_E4, 16},
  {N_A4, 16},
  {N_B4, 16},
  {N_A4, 16},
  {N_E4, 16},
  {N_B3, 16},
  {N_E4, 16}
};
Notes macGyverNotes1[16] = {
  {0   , 16},  
  {N_E4, 16},  
  {N_A4, 16},
  {N_B4, 16},
  {N_A4, 16},
  {N_B3, 16},
  {N_E4, 16},
  {N_B3, 32},
  {0   , 16},  
  {N_A4, 16},
  {N_D5, 16},
  {N_C5, 16},
  {N_D5, 16},
  {N_C5, 16},
  {N_B4, 16},
  {N_A4, 16}
};
Notes macGyverNotes2[16] = {
  {N_B4, 48},
  {N_A4, 76},  
  {0   , 4},  
  {N_A4, 48},
  {N_G4, 76},
  {0   , 4},  
  {N_B4,14},
  {0   , 2},
  {N_B4,48},
  {N_A4,76},
  {0   , 4},  
  {N_A4, 48},
  {N_G4,32},
  {N_A4,72},  //klopt niet
  {0   , 8},  
  {N_C5,12}
};
Notes macGyverNotes3[16] = {
  {0   , 2},  
  {N_C5,12},
  {0   , 2},  
  {N_C5,12},
  {0   , 2},  
  {N_C5,12},
  {0   , 2},  
  {N_C5,12},
  {0   , 2},  
  {N_C5,12},
  {0   , 2},  
  {N_B4,64},
  {N_F4S, 16},
  {N_A4, 32},
  {N_G4, 80},
  {N_C5, 14}
};
Notes macGyverNotes41[2] = { //speelt niet altijd?
  {0, 2},
  {N_C5, 32}
};
Notes macGyverNotes42[7] = {
  {N_B4, 32},
  {N_C5, 16},
  {N_B4, 16},
  {N_A4, 16},
  {N_G4, 16},
  {N_E5, 32},
  {N_A4, 64}
};
Notes macGyverNotes5[16] = {
  {N_C5, 14},  
  {   0, 2},
  {N_C5, 80},
  {N_F4S, 16},
  {N_A4, 32},
  {N_G4, 76},
  {N_C5, 14},
  {   0,  2},
  {N_C5, 32},
  {N_B4, 32},
  {N_C5, 16},
  {N_B4, 16},
  {N_G4, 16},
  {N_E5, 32},  
  {N_A4,64},
  {N_B4,64}
};
Notes macGyverNotes6[16] = {
  {N_C5,16},
  {N_B4,16},
  {N_A4,16},
  {N_C5,32},
  {N_B4,16},
  {N_A4,16},
  {N_D5,32},
  {N_C5,16},
  {N_B4,16},
  {N_D5,32},  
  {N_C5,16},
  {N_B4,16},
  {N_E5,32},
  {N_D5,16},
  {N_E5,16},  
  {N_F5S,32}
};
Notes macGyverNotes7[16] = {
  {N_B4,32},
  {N_G5S,48},  
  {N_F5S,32},
  {N_F5,32},
  {N_B4,32},  
  {N_G5,16},
  {N_E5,16},
  {N_B4,16},  
  {N_F5S,16},
  {N_D5,16},
  {N_A4,16},
  {N_E5,16},
  {N_C5,16},
  {N_G4,16},
  {N_D5,16},
  {N_B4,16}
};
Notes macGyverNotes8[16] = {
  {N_G4,16},
  {N_C5,16},
  {N_E4,16},
  {N_B4,16},
  {N_D4,16},
  {N_C5,16},
  {N_B4,16},
  {N_A4,16},
  {N_G4,16},
  {N_A4S,32},
  {N_A4,32},
  {N_G5,16},
  {N_G4,16},
  {N_D5,16},
  {N_G4,16},
  {N_D5S,16}
};
Notes macGyverNotes9[16] = {
  {N_D4S,16},
  {N_A4S,16},
  {N_A4,16},
  {N_G4,16},
  {N_G3,16},
  {N_D4,16},
  {N_G3,16},
  {N_D4S,16},
  {N_G3,16},
  {N_A3S,16},
  {N_A3,16},
  {N_G3,14},
  {   0, 2},
  {N_G3,14},
  {   0, 2},
  {N_G3,14}
};
Notes macGyverNotes10[9] = {
  {   0, 2},
  {N_G3,14},
  {   0, 2},
  {N_G3,14},
  {   0, 2},
  {N_G3,14},
  {   0, 2},
  {N_G3,14},
  {   0, 2}
};
Notes denyNotes[3] = {
  {N_E4, 4},
  {N_E4, 4},
  {N_E4, 4}  
};
int totalDuration =0;

void callback(char* topic, byte* payload, unsigned int length) {
  // handle message arrived
  // Serial.println("message received");
  //String strContent = "";
  //for (int i = 0; i < length; i++) {
  //  strContent += (char)payload[i];
  //}
  //Serial.println();
  char command = payload[0];
  // Switch case for commands only uses first char
  switch (command) {
    case 'C': //Clean
      roomba.start();
      roomba.safeMode();
      delay(200);
      roomba.clean(); 
      delay(5000); 
      roomba.setPowerLEDs(64, 64);
      break;
    case 'D': //Dock
      roomba.start();
      roomba.safeMode();
      delay(200);
      roomba.seekDock();
      delay(5000); 
      roomba.setPowerLEDs(32, 64);
      break;
    case 'S': //Spot
      roomba.start();
      roomba.safeMode();
      delay(200);
      roomba.spotClean();  
      delay(5000); 
      roomba.setPowerLEDs(96, 64);
      break;
    case 'M': //Music
      roomba.start();
      roomba.fullMode();
      delay(200);
      roomba.setPowerLEDs(112, 128);
      delay(200);
      playMIDI(macGyverNotes0,16);
      playMIDI(macGyverNotes1,16);
      playMIDI(macGyverNotes2,16);
      playMIDI(macGyverNotes3,16);
      playMIDI(macGyverNotes41,2);
      playMIDI(macGyverNotes42,7);
      playMIDI(macGyverNotes5,16);
      playMIDI(macGyverNotes6,16);
      playMIDI(macGyverNotes7,16);
      playMIDI(macGyverNotes8,16);
      playMIDI(macGyverNotes9,16);
      playMIDI(macGyverNotes10,9);
      roomba.passiveMode();
      break;
      
    default: //Play tune (also stops a motion action)
      roomba.start();
      roomba.fullMode();
      delay(200);
      roomba.setPowerLEDs(112, 128);
      delay(200);
      playMIDI(denyNotes,3);
      roomba.safeMode();
      break;
  }
}

// Start MQTT client
WiFiClient espClient;
PubSubClient mqtt_client(espClient);

// Necesary to make Arduino Software autodetect OTA device
//WiFiServer TelnetServer(8266);

void setup() { 
  Serial.begin(115200);
  WiFi.begin(wifi_ssid, wifi_password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  ArduinoOTA.onStart([]() { //Make sure device is in a safe mode
    //store params to EEPROM?   
  });
  ArduinoOTA.onEnd([]() { //Device will reboot

  });
  ArduinoOTA.onError([](ota_error_t error) {
    (void)error;
    ESP.restart();
  });
  ArduinoOTA.begin();
  mqtt_client.setServer(mqtt_server, 1883);
  mqtt_client.setCallback(callback);
  // Serial.println("Ready");
}
void playMIDI(Notes ArrayOfNotes[], int ArraySize){
  roomba.createSong(1, ArraySize, ArrayOfNotes);
  totalDuration = 0;
  for(int i=0;i<ArraySize;i++){
    totalDuration += ArrayOfNotes[i].duration;
  }
  roomba.playSong(1);
  totalDuration = 100+1000*(totalDuration/64.0);
  delay(totalDuration); 
}
boolean attempt_reconnect() {
  if (mqtt_client.connect(mqtt_client_id.c_str(), mqtt_user, mqtt_password)) {
    // Once connected, publish autodiscover config message and current state..
    mqtt_client.publish(mqtt_pub_topic, "up");
    // ... and resubscribe
    mqtt_client.subscribe(mqtt_sub_topic);
    // Serial.println("connected");
  }
  else {
    // Serial.print("failed, rc=");
    // Serial.print(mqtt_client.state());
    // Serial.println(" try again in 5 seconds");
  }
  return mqtt_client.connected();
}
void mqtt_handler() {
  //Serial.println("MQTT-handler");
  if (!mqtt_client.connected()){
    if (millis() > (lastReconnectAttempt + 5000)) {
      lastReconnectAttempt = millis();
      // Attempt to reconnect
      // Serial.println("attempt to reconnect");
      if (attempt_reconnect()) {
        lastReconnectAttempt = 0;
      }
    }
  } 
  else {
    // Client connected
    // Serial.println("connected to MQTT broker");
    mqtt_client.loop();
  }  
}  
void loop() {
  ArduinoOTA.handle();
  mqtt_handler();
  
  //
}

The code is still not prettyfied, but I also have done so much to the library that I'm not sure if I want to keep all this. 

Discussions