I was very lucky in finding a used Roomba at a garage sale a few years back. I bought for all of $5. And then I left it in a box, moved, and kept ignoring it.
The box contained the Roomba, a charging station, the power supply, and an electronic fence emitter.
I guess part of the reason I was hesitant to play with it was because it would have been a messy ordeal. After all, what do you expect from a $5 Roomba?
Being the hacker I am, I started unscrewing this and that, and didn't take the obvious path. It turns out that Roombas are quite well designed. Most everything clicks apart. The dust bin was super easy to remove, and yeah, it was full of caked-on dust. The "filter" was so clogged up that it, itself, was pretty clean. All the other dust was blocking air passage through it. After a short time and using a vacuum to vacuum the vacuum, I had a nice, cleaner machine.
The only broken part I could find was that one of the wheels was missing some of its tread, but it didn't affect the movement.
I then went to google to find out how to get it to interface with an Arduino. I recall having done that before, and that led to another reason I was scared to touch it. Roombas of ca. 2005 can be pre-2005 or post-2005, and the earlier ones do not support SCI interfacing. (Credit to www.jimisaak.com for the pointers here -- http://www.jimisaak.com/Home/stem-4-all/roomba-arduino.)
I flipped my Roomba over, and didn't see the model number, simply because it was blocked by the spinner brush. But I did find a 2005 trademark. As it turns out, it's a model 4130.
I charged up the battery a bit, and then hit the "clean" button, and it worked! 14 years later, and it still runs!
Wiring and wake-up
The web site mentioned earlier said I could interface with the machine through its port, and then talk to it via 57600 baud serial Rx/Tx. I found the little 8-pin DIN female port for interfacing. It's not exactly a mini 8-in DIN like they had in old computers, but roughly it fits. If you view it from one angle, it looks like this:From this perspective, it has pins 1 and 2 on the top row, 3/null/4 on the middle row, and 5/6/7 at the bottom. I avoided pins 1 and 2. They carry Vpwr, unregulated. Pins 3 and 4 are Rx and Tx, respectively. Pin 5 is an active low signal to wake up the Roomba. Pins 6 and 7 are ground.
So as an initial test, I wired up a switch. It went from pin 5 to the switch to a 1k resistor and then back to either pin 6 or 7. After to a 1k resistor and over to ground.
I clicked the switch and the Roomba woke up. Test complete!
SCI support?
The next step was to wire up to the Arduino. I was fortunate enough to have an Arduino Mega2560 handy. And the native Arduino examples made things super easy. There's a basic example called MultiSerialMega that does the trick.The sketch talks to the Serial Monitor over the usual Serial object, but also communicates to anything else over Serial1. Anything you type into the Serial Monitor gets conveyed to Serial1, and anything received on Serial1 gets printed to the Serial Monitor. The only change required was to modify the Serial1.begin(9600) line to communicate at 57600 baud instead.
As with anything, before connecting any wires between the machines, I only connected the Arduino to my PC USB port, then built the sketch, and uploaded it to the Arduino. That way, I'd have some certainty as to what the pins would be doing.
I then wired things up. The Mega2560 has TX1 on pin 18, and RX1 on pin 19. So I connected Roomba 3 (RXD) to Arduino 18 (TX1)
Roomba 4 (TXD) to Arduino 19 (RX1)
Roomba 5 (Wake-up) still connected as before
Roomba 6 or 7 to Arduino GND
With the Roomba powered off, I started the Arduino Serial Monitor, and clicked the Wake-up switch. And that gave me a happy message that my Roomba was from a time point in 2005, which meant it probably would talk SCI.
SCI
The SCI command protocol is pretty basic. It has single byte opcodes and operands. The number of operands depends on the opcode.Many of the opcodes and operands would end up being byte values that would end up as non-printable characters. So I had to modify the MultiSerialMega example to make things more interactive.
The loop() code already had a sequence where it would check for available input from Serial, and then read() the value. But in its original form, it would transmit that right away to Serial1.
Instead, I started make conditional blocks, so the sketch would respond to different "commands" that I would type. If the character read was an 'S', it would send a "start" and "full operation" set of opcodes to the Roomba. If an 's' was seen, it would start the spinner motor. 'x' would turn off all motors. 'X' would send a power-off command. All those simply worked.
The set of SCI commands is interesting. You can find it documented at various places online, like here.
An example set of code would be like this:
int inByte = Serial.read();
if (inByte == 'S') {
Serial1.write((byte)128); // Start
delay(120);
Serial1.write((byte)132); // Full control mode
}
if (inByte == 'x') {
Serial1.write((byte)138); // Motors
Serial1.write((byte)1); // 2^0 bit is the side motor
}
if (inByte == 'X') {
Serial1.write((byte)138); // Motors
Serial1.write((byte)0); // All motors off
}
LEDs
The LEDs are controllable by SCI, but different ones have different capabilities and colors. The main spot, clean, and max buttons have plain green LEDs. To the right of those is a blue LED for "dirt detect". Each can be turned on or off using the four lowest bits of the first data byte operand of opcode 139.The next LED to the left is the "power" button's. Its color and intensity are set by the second and third data byte operands of the command. Finally, the leftmost LED is the status indicator, and its color and on/off setting is determined by bits 4 and 5 of the first data byte.
As such, doing a Cylon-style LED sweep is a little complex. For a basic implementation, I set up a couple of byte arrays that would set the right bits at the right times for each of the first and third bytes, and always set the "power" button's color to green.
I used the same structure as above, but that was (and remains) hokey. I was running a "for" loop within the loop() function, thus hogging the Arduino CPU while cycling lights.
A better implementation would be to use a state indicating I'm in Cylon mode. When in that state, every time I get called in loop(), I would check the current time, do some dividing and modulo'ing based on frequency of state change, and compute a byte offset into the arrays, and then send the right value if it's different than before.
Gremlins
While still using the "for" loop approach above, I changed the delay time between LED cycle changes. At some values, it would make it through a few iterations of the LED sweep, but then quite disturbingly, it would kick the Roomba into "clean" mode! Fortunately, there was still the emergency stop button (physically hit the power button).Why would it do that? The best I can guess is that I'm running too fast for the Roomba, or maybe overrunning its input buffer. Then, as I send in an operand for lighting up lights, the Roomba ends up seeing that operand as an opcode instead (opcode 135, 136, 137, or 138 possibly could get the Roomba moving).
The behavior changed as I'd adjust the delay time between opcode sends, but I lost interest because I had other things to try.
Music
The Roomba can produce "songs". You can store up to 16 songs, each having 16 notes. Each note also has a duration, specified in 1/64 second increments.Before long, I had my Roomba playing "Baba Yetu" and a rough approximation of a Pac Man starting theme.
But most songs are longer than 16 notes. Both of the ones I attempted were. I tried splitting the songs across multiple 16-note songs, and that worked pretty well. But just like today's streamed music, if you play two songs that were joined together on vinyl, they end up having a hiccup between songs nowadays. This happens on Arduino because it takes time to send a new "play song" opcode and operand.
For note durations, I used these kinds of values
1/4 note = 40/64ths
1/8 note = 20/64ths
1/16 note = 10/64ths
1/32 note = 5/64ths
That didn't play all that well. The 32nd notes come out stuttered. So it seems that while the Roomba supports 1/64th-second increments, there's a real life lower bound for the minimum duration of a tone.
Time to move
At this point, I tired of playing with the SCI interface. I hadn't had it tell me the state of sensors yet, but trusted that that would work. Instead, I wanted to clean up the interface, using a real mini DIN-8 connector instead of jumper wires, and do other stuff with it.A trip to the Goodwill yielded four great things:
- $1.50 = A purple USB-A cable. If I'm going to interface my PC to an Arduino, why not do it in purple?
- $1.50 = A serial cable with the right mini DIN-8 headers!
- $2.49 = A remote control Hot Wheels (27 MHz) car
- $14.99 = A Shark Lift-Away vacuum cleaner. Score! Okay, that's not relevant to this blog, but still, score!
My new project: take the Hot Wheels car apart, and have its electronics move the Roomba.
The Hot Wheels RC car
This is the Hot Wheels 27 MHz radio-controlled car. that I got. Normally the body goes cleanly onto the chassis, but I've already messed with it, so it's loose here. Slick, right? As one of the guys at the lab said, I would have loved to have had something like this when I was a kid.Hot Wheels RC car and transmitter |
RC transmitter |
The remote control has front/back controls on the left rocker switch, and right/left controls on the right rocker switch.
The middle button is like a "turbo" mode, and when in that mode, the forward thrust is engaged (overriding a neutral or reverse position on the thrust button, if that's pressed at the same time). When in turbo, it also lights two blue LEDs (those clear rectangular things on the image below).
I took the car body off the chassis to expose the innards. The antenna wire was soldered to a hunk of copper tape, and that was stuck to the roof of the body. I just peeled that off. The circuit board processing the radio signal and controlling the motors was more complex than expected.
Circuit board under side |
MX1508 on board |
I was hoping for something where I could tap in to the radio interpretation signals to figure out which button was pressed. One possibility would have been to pop off the MX1508, and solder to the pads, but it looked a bit intrusive and risky.
Instead, I decided to check the motor outputs. There are two pairs of wires coming out. The red-white line was controlling direction, and red-blue controlled thrust.
After driving the car around, it wasn't clear if the motors were just getting pure positive and negative DC, or if some kind of PWM was going on. I de-soldered the motor-driving wire pairs, and tested them on the voltmeter. What I got was somewhat expected of a cheap RC car:
Forward thrust was a about +5VDC, reverse was -5VDC.
Similar happened for the right/left turn motor voltages.
The voltage would drop a tad if both buttons were pressed at the same time.
Interestingly hitting the turbo button would give the voltages a bump of about 0.4VDC (positive or negative), in addition to turning on the LEDs.
Converting +/- voltage to Arduino inputs
My goal then became converting the motor voltages to something I could use as Arduino inputs. But the Arduino would be happiest (i.e., I wouldn't risk killing the pins) if I could use a 0 to +5VDC range for the input pins, avoiding the negative voltage and the Turbo voltage boost.So I drew up a circuit diagram. I basically wanted some diodes to act as dams to prevent negative voltage, and subsequently I figured I'd use optocouplers to keep the Arduino voltages isolated from the RC car voltages. Without involving optocouplers just yet, it would be sufficient to just wire to LEDs and appropriate resistors, and make sure I wouldn't burn out the LEDs.
Circuit sketch |
So in the initial measurement I had a diode, no LEDs, and a voltmeter to measure. In just testing one rocker switch (e.g., left/right affecting the red/white line), I would get +5V in one direction, and something weird like -0.10V in the opposite direction. The opposite movement wired to the other wire, would give me -5V to +0.10V. So, for the "negative" side, I could still get a 5.1V delta, and use that, just by orienting the wires differently.
That led to the diagram above and the use of optocouplers.
In practice, the wiring of the circuit took on a slightly different but nicely parallelized look. I started with a simple set of passives, directly soldered (no breadboard), just to see if I could light and not burn out some LEDs. Effectively, the same would happen for the LED inside an optocoupler.
"Half rectifier"? for changing +/- voltage to two positive voltage lines |
The nice thing about this is that it's entirely reversible. I wanted "forward" to be yellow (headlights) and "back" to be the red LED. If I got it wrong, I'd just switch the solder connections to the blue and red wires.
The exact same circuit was used for the right/left turn direction indicators. I longed for red (port) and green (starboard) lights for that, but only got a red bulb and a clear bulb, and even the clear one shone red when powered. Sad.
In the end, the real range of voltage ends up being about 5.5VDC before hitting the resistor. That's because the voltage range is 5.0 (basic button press) + 0.4 (if turbo is turned on) - (-0.1).
(V-Vdrop) = I * R
Supposing 1.8V voltage drop for both red and yellow LEDs, I get
(5.5V-1.8V) = I * 220ohm
I = (5.5V-1.8V) / 220ohms = 16 mA current across the LED, which is safe (max 20mA, probably). I could adjust accordingly for optocoupler's specs for its internal LEDs.
Here's what it looks like wired up with the thrust and turn connections. (I also have the battery power wired through my own switch instead of using the native one.) Eventually, I will remove the LEDs and connect to optocouplers instead, and swap in the correct resistors. In this way, a variance of 0.4V due to turbo won't greatly affect things. As long as the LED lights up, the optocoupler will let it's transistor do its thing.
Car wired up with to demonstrate LED signal detection instead of motors |
I also can detach the boost LEDs. They're through-hole, so they should be pretty easy to de-solder, and I can use their signal to light up a fifth optocoupler to know when that button is pressed. Granted, when that button is pressed, it also forces the "forward" button to be pressed, but I can distinguish that in software by checking the state of the turbo signal, and only consider the thrust signal when turbo is off.
Here are some sample images of what the car looks like when I'm pressing the buttons.
This is backwards and right at the same time.
And this is forwards (yellow) and right at the same time
Full build
I found a Mini DIN-8 connector and figured out its wires so I could have a solid connection to the Roomba.The most important things for my purposes are:
- Roomba V+
- Gnd
- Rx
- Tx
Power
I wired Roomba V+ and Gnd through a buck converter, and adjusted it to product 9V. That fed into the Arduino Vin/Gnd pins by way of a cheesy lamp cord switch.I then built my own PCBs on the HackerLab CNC 3040 mill. After an initial attempt to render the board as double-sided and mimicking the cross-wired resistor/diode thing I had earlier, I realized how clumsy that was to solder.
The second PCB iteration had me using a one-sided board for each signal.
Shield
At some subsequent iteration, I decided to make it into an Arduino Mega2560 shield. I ordered up the headers that have 23mm length. The ones I got were from Amazon. I got two kinds to compare, but either would have worked. One was "ELEDIY 40-Pin Stacking Header Extra Tall Header for Arduino/Raspberry Pi (pack of 5)". The other was "Arduino A000040 Stackable Female Header for Shields - 1x8 and 1x6 Position, 23 mm H(Pack of 3)" For the latter, "pack of 3" meant you got three 1x8 headers, and three 1x6 headers.There are other header pins out there, but 23mm length is the important part for me. I didn't want Arduino pins or components touching any of the shield copper, and there would be a lot of exposed copper since I was milling my own PCB.
Designing the shield itself was a bit cumbersome but doable in Target 3001!. First, there are various places on the web that describe the pin locations, but nothing really in x/y text form. The important thing there is that there's a set of headers at the top of the board that are offset 0.06" left from the others. Then, there were considerations for creating headers, which was necessary to ensure circuit isolation for them when engraved. Finally, I had to rejigger some of the diode pads in case I used old-school through-hole diodes, some of which have fatter wires.
I also had to consider having access to the reset button on the board, because I was finding that I had to stop the system and restart it now and then. That's especially important because I connect to Serial1 on startup and never after, so if I ever cycle power on the Roomba, I have to cycle power on the Arduino afterward.
In the end, I built a shield that met these requirements:
- Vin and Gnd could be shunted in from buck converter + and - outputs running at 9V.
- There would be inputs for each motor's wire pairs. So there would have to be 4 inputs running in the range of -5V to +5V.
- The motor inputs would "half rectify" and feed their output to optocouplers. Arduino Vcc and Gnd would go through the opposite sides of the optocouplers to generate a normally HIGH signal, and it would go LOW upon button press.
- Access would still be available to the reset button. (I have a knock-off Sainsmart Arduino Mega2560, one that has this reset button.
- Rx and Tx lines were presented as headers so that I could wire the corresponding Tx and Rx lines from the Roomba.
As it turns out, I could still use the third "half rectifier" board I'd constructed earlier. I unwired one of the RC car's "turbo" LEDs, and checked its voltage and amperage. I didn't know the LED specs of my remaining optocoupler, but figured it would be close enough compared to the "turbo" LED. I also figured the car must already have some kind of current limiting mechanism for its LED, so I could rely on that for my optocoupler LED. So I changed my "half rectifier" board, replacing its 300 ohm resistor with a 1 (one) ohm resistor. I made that wire into another input on the Arduino Mega2560, which was available (uncovered by the shield).
Coding - movement
Much of the basis of the coding of the Roomba was straightforward.The loop() function would regularly poll to see if any of the direction buttons had been pressed.
The initial iterations of the code looked for on/off transitions. A transition from neutral to on was treated as a momentary button press.
SCI has command 137 for setting both radius and velocity at once. The format is:
byte 1 = 137
byte 2 = velocity high
byte 3 = velocity low
byte 4 = radius high
byte 5 = radius low
Velocity is a signed 16-bit quantity ranging from -500mm/sec to +500mm/sec. -1 and +1 have special meanings for spinning in place clockwise and counter-clockwise, respectively.
For thrust, a global velocity variable, vel, was kept and limited to a range of -2..+2. In this way, a single press forward would move at a predetermined low speed, a second press would make the velocity increase, and any subsequent forward presses would stay at speed +2. Similarly, the speed could be reduced back to zero, or even put the Roomba in reverse.
https://www.usna.edu/Users/weapron/esposito/_files/roomba.matlab/Roomba_SCI.pdf
For turns, it was trickier. The SCI interface allows you to specify the radius of movement. If you look later in the spec for information about sensors, you can request Packet Subset 2 and get its "Angle" computation. That value is computed based on a 258mm wheel separation. In more recent Roomba 600 Open Interface specs, the value drops to 235mm.
Radius is a signed 16-bit quantity ranging from -2000mm to +2000mm, with special value 0x8000000 representing "straight".
I modeled the turn direction similar to how I'd done velocity. Initially I'd envisioned having multiple values, and doing some math to compute different angles. However, from a user interface standpoint, it became clear that a simple RC car-type interface would probably have been best. As it is, I ended up with a simple range:
- Three clicks = rotate in place
- Two clicks = tight turn
- One click = gradual turn
- zero position = go straight.
Also, positive velocity with positive radius is a LEFT turn, whereas positive velocity with negative radius is a RIGHT turn.
If you think of these as indexable, it takes on a weird array structure like this:
long radiusValues[] =
{
-1 // counter-clockwise
, 300 // tight LEFT turn when positive
, 1000 // gradual LEFT turn when positive
, 0x8000000 // special straight value
, -1000
, -300
, 1
};
Even with that, my code isn't prepared currently to handle negative movement followed by a "spin" command, because in those conditions, I really should be swapping -1 and 1 values.
In a maze running mode, it probably would be better to have a simple left/straight/right set of values, and set the radius value equal to the wheel separation distance.
"Turbo" = vacuum on/off
When I added recognition of the Turbo button, I re-encountered the problem where the native RC Car turbo would light LEDs and put the car in forward motion. I put Turbo button detection early in the event detection if/else list, and made it so that other buttons would be ignored when Turbo transitioned low to high.Then, it was a simple matter of retaining vacuum on/off state, and toggling it. When it toggled and ended up in an "on" position, I would fire up a Motors opcode (138) followed by an operand turning on all vacuum related motors (0x7 = Main 0x04 | Vacuum 0x2 | Spin 0x1). Turning off was the same, but using operand 0.
Sensors
After driving around a bit, I found myself crashing the Roomba into things and then having to rush over and lift it up. Since the Roomba was running in Safe mode, it would reset upon wheel drop, and I'd have to start over again.I added recognition of sensors as the first sequence in the loop() function. That way it could detect a bump or wheel drop first thing. In my initial iteration with this, detection of any event like that would stop all motors and set velocity to zero. However, I need to be able to back out of such a condition without resetting the Roomba. My idea on this is that I'll set a timer, recognizing when the crash occurred, stop all motors, and give the driver about two seconds to compensate. After those two seconds are up, the normal sensor detection would kick in again.
If I don't use a timer, it will run through the loop, find that the bumper is still engaged, and stop motors again. Since the loop does event (and control input) detection every 100ms or so, I'd never catch it in time and it would just keep stopping, even if I were to mash the buttons.
The biggest issue with sensors is the actual implementation of the API. The sensor API has me send in a two byte command. The opcode is 142 followed by an operand value of 0, 1, 2, or 3. The Roomba returns 26, 10, 6, or 10 bytes in response to the command. Operand 0 is saying "Send back the results of packet subsets 1, 2, and 3 in sequence".
Packet subset 1 is the one that provides the sensor information, including bumper, wheel drop, motor overcurrent, cliff detection, wall detection, and dirt levels.
Packet subset 2 provides information about remote control button presses, on-machine button presses, distance, and angle.
Packet subset 3 provides underlying electronics info, like battery charge, temperature,
For my purposes, I was only interested in Packet subset 1, and even then, only the first byte.
However, after sending in bytes {142,0} it wasn't clear how the timing would work, and how I should handle the possibility of transmission errors. If I were to read 10 bytes back from the Roomba, how would I know if all ten were ready to transmit? Does the Arduino Serial1.read() function block? It seems it would not, because there's also a Serial1.available() function.
Furthermore, the Serial1 line could still have garbage on it at startup. Remember that in early experiments with the Roomba, I would just receive and echo Serial1 characters, and that is when I saw the big string reporting the birth date of the machine.
I added code to wait and discard input from Serial1 in the initial boot-up phase.
But it was less clear how to handle the problem of the returned data. I ended up using a very crude iterative loop, waiting to read each of the ten response bytes, and bailing after a brief period (250 milliseconds?) had gone by. In initial experiments where I'd block and keep waiting for input, I would sometimes get into an infinite loop and then the robot would drive into a wall with no ability to control it (because the Serial1 check code wasn't relinquishing control back to the main loop() function).
So at this point I have these issues
- improper clockwise/counter-clockwise spin motion when in reverse
- need to have Serial1.read() in a breakable loop in case of insufficient sensor response data
- cannot stop the Roomba robot without causing Safe mode to end (lift robot, drop wheels)
- does not restart Serial1 if the robot stops and restarts
I also have had several requests to make the robot emit some kind of epithets or similar upon sensor activation. To do that, I'd need to spend a bit more on an MP3 audio board for Arduino, and an amplifier and speakers. I'm not sure if I want to make that investment yet. Adafruit provides a fully functioning shield, minus speakers for $36.
There are some out there like this:
HiLetgo Mp3 Lossless Decoders Decoding Power Amplifier Mp3 Player Audio Module Mp3 Decoder Board support TF Card USB, $5.50
but those just have buttons to play back MP3 files with no control over which to play. It may be hackable to get one, remove its soldered buttons, and replace those with FETs or similar. However, I think some button debouncing on the device might cause you to have to scale back the frequency of Arduino-controlled "button clicking".
Another is:
Aideepen 5PCS DFPlayer Mini Mp3 Player Board YX5200 Module Support TF Micro SD Card U Disk Audio Music for Arduino, $14 for 5 ($2.80 each).
These look much more promising. You can just trigger a pin to play back a sound.
HiLetgo 2pcs Arduino mp3 Player Mini MP3 Player Audio Voice Module TF Card U Disk Board DFPlayer Audio Voice Music Module
has a link to a nice YouTube video, showing it in action, and it looks dead simple to wire up, so long as all you're doing is playing back pre-recorded sounds.
The DFPlayer looks more promising than the older WTV20. The WTV20 appears to require an AD4, mono audio file, and it seems (not sure) that DFPlayer is more modern, allowing for MP3 files natively (but are there restrictions?). To interface with the DFPlayer, though, I need to use my Serial2 lines to send it Play, Pause, and Next commands. It also would need GND and 5V from the Arduino, and I've already stolen those pins for the Turbo optocoupler, I think. Otherwise, it seems pretty simple to use and integrate.