Roomba More Smarter
This is a stub; will be updated later.
Motivation
My apartment tends to be a dirt & dust magnet. There is an exhaust room of some kind which gets fairly dusty (and since it internally builds up dust, it can't be cleaned until it is exhausted...), and outside dirt and stuff tends to get tracked. The only solution to this problem is to clean the dust, however, even though I clean the dust frequently, it basically never goes away. Also, I would prefer if I could be lazy about it. So, naturally, I looked at robot vaccums.
Robot Vaccums
Robot vaccums are basically the lazy man's way of cleaning. You tell them to clean, and they clean until they have no more battery. Simple enough, right?
Turns out, while robot vaccums can certainly get the job done, there are a few drawbacks.
The first one: robot vaccums are expensive. Most of them go for around $400+, with the highest I've seen reaching around $800! Not worth it when manual cleaning with a vaccum is significantly cheaper...
The second one: not all of the robots have the same cleaning algorithms. Some of them are fairly bad at cleaning. For example, I got my hands on a bobsweep Pet Hair Plus for $30 at Goodwill. Besides the fact that it has an extremely long name, it is extremely dumb. It can only consistently clean a 3x3 square in our apartment, and that's it. Otherwise, it keeps trying to clean random spots of the apartment, before dying while trying to reach the charger. I attempted to try and modify the device (and dump the firmware), but the motherboard suffered a short circuit while I was researching it. So, I eventually gave up on it, until I came across a new discovery.
iRobot Roomba 655
The All-Campus Makerspace at our school has a "Free Store", where makers are welcome to come in and get resources for their projects. Its a relatively new concept, and has been relatively successful so far.
One day, I decided to go to look for networking equipment for a project, and came across a defunct Roomba 655 in the "to take apart" section. It was stripped of its battery & cover, but looked fine otherwise. I decided to take the Roomba, since it'd be interesting to get it working.
A few days later, I sat down to inspect it; this required me to take it apart. Almost immediately after removing the dustbin and top cover, I was greeted with a funny looking interface of some kind.
This was a Mini-DIN connector, a smaller version of the connector which is commonly used for MIDI peripherals. I took note of this, and decided to investigate it further when I got the Roomba to power up.
Afterwards, I was able to find the proper power orientation for the Roomba battery, and manually powered it with a benchtop power supply at approximately 12V. The device turned on, and made a sound indicating that the battery was missing. After fixing this by shorting the two thermistor(?) pins in the battery connector region, I was able to get the Roomba to operate normally.
After doing some digging, I was able to discover that this was a serial communication interface (or, as they call it, Roomba SCI).
Because of this discovery, when I was putting it back together, I decided to drill a hole in the chassis cover over the interface. By default, this is covered, so I would have to remove the entire top cover if I want to access it. This makes it so it is significantly easier to work with.
Serial Communication Interface (SCI)
When looking into the SCI, I was able to find the Roomba SCI Specification, which tells me which command corresponds to what action. It turns out that the Roomba SCI provides full access and control to the Roomba's motors, sensors, and actuators.
Physical Connection Interface
The pinout for it is as follows:
It is very important to understand how this interface works before we start trying to use it.
-
The logic level (the voltage which the UART communication protocol uses) is 5V, as shown by the transmit (
TX
) and receive (RX
) pins. This means that to send a logical 1, the voltage needs to be 5V for a predetermined amount of time. -
The baud rate (amount of bits which can be sent to the interface per second) is configurable, but by default should be 115200 on the Roomba 655. This was determined experimentally by probing with an oscilloscope, as the documentation had mentioned it utilized a baud rate of 57600 (which is half of that).
-
The unregulated battery voltage is passed through pins 1 and 2. This means the battery voltage is directly passed through, irrespective of if the device is on or not. If you connect a device to this power source, ensure that the device does not draw power when the battery voltage is too low (i.e 12V). Failure to do this may result in overdischarge of the battery, which can cause chemical damage to it (which may interfere with its ability to perform, or its safety).
Talking to the Roomba - Wiring
The SCI uses the Universal Asynchronous Receiver-Transmitter (UART) protocol, which is a simple two-wire communication protocol.
The protocol works fairly simply: if you have two devices A and B, the transmit (TX
) line of A is connected to the receive (RX
) line of B, and the transmit line of B is connected to the receive line of A.
From there, if A wants to send a byte to B, A will (assuming a standard 8N1 UART configuration):
- Pull its transmit line to 0V (Ground) for a short time period to send a logical 0.
- Send a logical 0 or 1 (by pulling the line to 0 or 5V, respectively, for a short period of time) for each bit in the byte.
- Send a logical 1 (by pulling the line to 5V) after it finishes sending all 8 bits.
The same process goes for if B wants to send a message to A.
The receiving device will then parse the bits at a given rate that it is configured to read at. This rate is known as the baud rate, which is measured in bits per second. It is important that both devices that are communicating have the same baud rate, otherwise there will be miscommunication.
In our case, A would be our serial communication tool. I will use an Espressif ESP32 Dev Module, since it is the only thing I have on hand. However, since the ESP32 has a logic level of 3.3V, we must use a bidirectional level shifter to step the ESP32's communication voltages up to 5V. Otherwise, when the Roomba sends 5V to the ESP32's 3.3V GPIO pins, the ESP32 will get damaged as it is not rated for that voltage.
Of course, B will be our Roomba, since it should be receiving the commands. The bytes we are sending from A to B will correspond to commands for the Roomba, each of which have varying parameters.
Based on the above information, we can wire up our ESP32 Dev Module. The wiring is as follows:
Note: I used an ESP32 Dev Module, so I was able to program it over USB with a USB-Serial converter (CH340G).
Talking to the Roomba - Programming
Now that we've wired up the Roomba, we need to send commands to it to control it. Here is a short list, from the quick reference section of the documentation.
In the control flow presented by the documentation, the first command we need to send is CMD_START
(0x80/128). This command takes no parameters, so we just need to send this one byte.
Afterwards, we need to tell the Roomba exactly how much control this serial interface has on its operations.
To do this:
- We first send a command to allow manual user control,
CMD_CONTORL
(0x82/130). - We then send a command to specify how much control we want; either
CMD_SAFE
(0x83/131), orCMD_FULL
(0x84/132).CMD_SAFE
is just safe mode, where asCMD_FULL
is full control.
After that, we can do as much as we want, and send any command we want!
The code I wrote to achieve this initial with the ESP32 is as follows:
#define WAKEUP 4
#define RX2 16
#define TX2 17
HardwareSerial g_RoombaSerial(1);
enum roomba_cmd {
CMD_START = 128,
CMD_CONTROL = 130,
CMD_FULL = 132
};
void setup() {
Serial.begin(9600);
pinMode(WAKEUP, OUTPUT);
// Software serial to instrument Roomba.
g_RoombaSerial.begin(115200, SERIAL_8N1, RX2, TX2, false);
// Put Roomba in full control mode.
delay(5000);
digitalWrite(WAKEUP, LOW);
delay(10);
digitalWrite(WAKEUP, HIGH);
delay(10);
g_RoombaSerial.write((uint8_t) CMD_START);
g_RoombaSerial.write((uint8_t) CMD_CONTROL);
g_RoombaSerial.write((uint8_t) CMD_FULL);
}
void loop() {
}
Essentially, we wait 5 seconds for the ESP32 to fully turn on, then toggle the WAKEUP
pin to force the Roomba to wake up the SCI interface. Afterwards, we send the commands requesting full control.
Gran-Turismo Roomba
As a test, let us try to manually drive the Roomba around through SCI. But, I want to use a controller, since it would give us more precision. As such, I stumbled upon Bluepad32, which allows me to use my game controllers on the ESP32.
I mapped each button on the controller to a corresponding action on the Roomba.
For example, the joystick controls the rotation of the Roomba, the right trigger controls the forward velocity, the left trigger controls the backwards velocity, and the cross & circle buttons control the state of the vaccums (on/off).
From there, whenever the corresponding action is pressed, I send the corresponding Roomba command (which I dynamically create based on the structure in the SCI specification) to the serial interface we created.
The code I used to do this is below.
#include
#define RX2 16
#define TX2 17
#define DEADZONE 16
ControllerPtr connected_controller;
HardwareSerial g_RoombaSerial(1);
int16_t g_PreviousSpeed = 0, g_PreviousRotationSpeed = 0;
void onConnect(ControllerPtr ctl);
void onDisconnect(ControllerPtr ctl);
enum roomba_cmd {
CMD_START = 128,
CMD_CONTROL = 130,
CMD_FULL = 132,
CMD_SENSOR = 148,
};
void setup() {
Serial.begin(9600);
pinMode(4, OUTPUT);
BP32.setup(&onConnect, &onDisconnect);
BP32.forgetBluetoothKeys();
BP32.enableNewBluetoothConnections(true);
// Software serial to instrument Roomba.
g_RoombaSerial.begin(115200, SERIAL_8N1, RX2, TX2, false);
RoombaInit();
}
void onConnect(ControllerPtr ctl) {
if (connected_controller == nullptr) {
connected_controller = ctl;
ControllerProperties properties = ctl->getProperties();
char buf[80];
sprintf(buf, "BTAddr: %02x:%02x:%02x:%02x:%02x:%02x, VID/PID: %04x:%04x, flags: 0x%02x", properties.btaddr[0], properties.btaddr[1], properties.btaddr[2], properties.btaddr[3], properties.btaddr[4], properties.btaddr[5], properties.vendor_id, properties.product_id, properties.flags);
Serial.println(buf);
} else {
Serial.println("Controller already connected; discarding.");
}
}
void onDisconnect(ControllerPtr ctl) {
connected_controller = nullptr;
}
void HandleGamepad(ControllerPtr gamepad) {
int16_t c_NewSpeed = 0, c_NewRotationSpeed = 0;
if (gamepad->throttle() > 20) {
c_NewSpeed = floor(gamepad->throttle() * 500.0 / 1024.0);
} else if (gamepad->brake() > 20) {
c_NewSpeed = -floor(gamepad->brake() * 500.0 / 1024.0);
}
if (gamepad->axisX() < -DEADZONE || gamepad->axisX() > DEADZONE) {
c_NewRotationSpeed = floor(gamepad->axisX() * 200.0 / 512.0);
if (!c_NewSpeed && c_NewRotationSpeed < 0) {
c_NewRotationSpeed = -1;
c_NewSpeed = abs(floor(gamepad->axisX() * 500.0 / 512.0));
} else if (!c_NewSpeed && c_NewRotationSpeed > 0) {
c_NewRotationSpeed = 1;
c_NewSpeed = abs(floor(gamepad->axisX() * 500.0 / 512.0));
}
} else {
c_NewRotationSpeed = 0x8000;
}
if (g_PreviousSpeed != c_NewSpeed || g_PreviousRotationSpeed != c_NewRotationSpeed) {
// Send command.
Serial.print("Speed: ");
Serial.println(c_NewSpeed);
Serial.print("Rotation: ");
Serial.println(gamepad->axisX());
g_RoombaSerial.write((uint8_t) 137);
g_RoombaSerial.write((uint8_t) (c_NewSpeed >> 0x8) & 0xFF);
g_RoombaSerial.write((uint8_t) (c_NewSpeed) & 0xFF);
g_RoombaSerial.write((uint8_t) (c_NewRotationSpeed >> 0x8) & 0xFF);
g_RoombaSerial.write((uint8_t) c_NewRotationSpeed & 0xFF);
delay(10);
}
g_PreviousSpeed = c_NewSpeed;
g_PreviousRotationSpeed = c_NewRotationSpeed;
if (gamepad->buttons() & 1) {
g_RoombaSerial.write((uint8_t) 138);
g_RoombaSerial.write((uint8_t) 7);
} else if (gamepad->buttons() & 2) {
g_RoombaSerial.write((uint8_t) 138);
g_RoombaSerial.write((uint8_t) 0);
}
}
void loop() {
BP32.update();
if (connected_controller) {
HandleGamepad(connected_controller);
}
}
void RoombaInit() {
// Put Roomba in full control mode.
delay(5000);
digitalWrite(4, LOW);
delay(10);
digitalWrite(4, HIGH);
delay(10);
g_RoombaSerial.write((uint8_t) CMD_START);
g_RoombaSerial.write((uint8_t) CMD_CONTROL);
g_RoombaSerial.write((uint8_t) CMD_FULL);
}
As a result, we were able to get the Roomba to move to where I want it to (including spinning it on its own axis, angled turns, etc), and clean stuff up with the vaccum.
<Video Here?>
The Ultimate Plan
So, now we're ready to make the Roomba smarter! One of my roommates and I came up with an idea to try and make the machine smarter: hacking on it to implement a better mapping algorithm for our apartment.
Since the Roomba can now be interfaced with a microcontroller like our ESP32, we can very easily instrument the Roomba, and even implement features which iRobot never did, such as WiFi and Bluetooth support (for our own purposes). This gives us increased control, and allows us to use the Roomba as a more versatile tool.
So, ideally, what we can do is use the Roomba to collect data, and build a map of our apartment. From there, we can use a path planning algorithm to find a more optimal cleaning path, compared to the "traveling salesman" approach we took previously.
I will update this eventually with the progress.