I've been unable to get together with my technically minded friends and talk about interests. And so, I decided to make robot friend. Bender.
As everyone knows, Bender shot to fame on the Futurama television show that aired from March 28, 1999 to August 10, 2003. I decided to build him because of all the robots on television, the movies and comic books he is the most human like. Bender was built in the year 2996 and designated Bending Unit 22, unit number 1,729, serial number 2716057. The Unit Number 1,729 is significant in that it is the smallest number expressible as the sum of two cubes in two different ways. But I digress. His human like characteristics include extreme narcissism, over confidence in his abilities, and ill-tempered rudeness. Sometimes I really miss the workplace.
Building Bender
Bender is a 3d printed project with servos, WS2812b LEDs, and DFRobot sound module driven by an Arduino MKR1010. He was designed in Fusion360 and printed on an Anycubic i3 Mega. The exterior is "silky" silver PLA and unpainted. There are 6 main structural parts:
- Lower head
- Middle head
- Mouth carousel
- Eye box
- Upper head
- Antenna
The lower head holds the mouth carousel in place and has a central pin that it rotates on. The carousel design was inspired by a very interesting build by James Bruton.
The middle head holds the eye box in place and slips on with a friction fit to the lower head. There is also a motor bracket and servo to rotate the mouth carousel. Not shown are the 3D printed mounting attachments and brackets for the microcontroller, battery, speaker, and so on.
The eye box holds the 3d printed ovoid eyes (printed in two pieces and glued together), two servos, and associated motor mounting hardware. It slides into the middle head and is held in place with a dab of hot glue.
The upper head and antenna are printed in two pieces and slip over the middle head.
According to my research Bender's is 5 feet 6 inches (1.68 m) tall and a little over 6 feet (1.8 m) with the antenna. This build is a 1:1 scale reproduction. The lower head and middle head took about 8 hours to print and the upper head 16 hours. The other pieces took between 15 minutes and 4 hours to print so the printer has been busy and was working around 35 hours total.
Electronics
Bender runs on alcohol which he uses to produce electricity. In this reproduction a 18650 battery is used to power the Arduino MKR1010 microcontroller, 3 servos, 5 WS2812b LEDs, and DFRobot DFMini Pro sound unit as shown in the schematic below.
The Arduino and DFPlayer Mini are mounted on a prototyping board which also contains headers for the servos and LEDs for easy assembly and repair. Not shown is a WeMos boost converter and charging PCB for a 18650 battery. The servos and LEDs are powered directly off of the 5V supply of the boost converter. The Arduino receives USB power from the boost converter and supplies 3V3 to the sound module.
Assembly
Below we see Bender on the factory floor in various stages of assembly and testing. The WS2812b LEDs are mounted on PCB strips that were designed by me some time back for toys for the grandkids and can be snapped apart at mouse bites to the desired length. The paper mouth inserts into a slot that can be seen in the mouth carousel. Bender is fairly easily disassembled and reassembled for such things as parts replacements and repair when needed.
{gallery} Assembly on the Factory Floor |
---|
Ready for Assembly |
Eye Box Assembly |
Preparing for Brain Transplant |
Brains |
Assembly complete |
Code
The Arduino sketch used to control Bender over WiFi is very similar to what was used in Cat Commander. Feel free to look through and use it but it is over 600 lines and a total hack. Even for me it is a total hack. There are 9 facial expressions and 32 phrases that can be controlled although more could be easily added.
/* make robot friend Bender * developed on Arduino MKR 1010 * * WiFi control - Add arduino_secrets.h with following lines to set up * #define SECRET_SSID "SSID" * #define SECRET_PASS "password" * Eye and mouth movement using 3 servos * Lights eyes and mouth with six WS2812 LEDs * Speech using DFRobot DFPlayer Mini * * by fmilburn April 2021 * * This code is in the public domain */ #include "arduino_secrets.h" #include <WiFiNINA.h> #include <WiFiUdp.h> #include <Adafruit_NeoPixel.h> #include <Servo.h> #include <DFRobotDFPlayerMini.h> // Miscellaneous const int DEBUG = false; // true turns on serial print to terminal const int DELAYMOVE = 1000; // pause during expressions // Pixels const int PIXELPIN = 6; const int NUMPIXELS = 6; // Eye Servos const int LEFTEYEPIN = 4; const int RIGHTEYEPIN = 5; const int SLOW = 30; // servo movement speeds const int MEDIUM = 15; const int FAST = 5; const int LEFTADJ = 35; // variables to adjust servo error const int RIGHTADJ = 20; const int SERVOFRONT = 90; // unadjusted servo eye direction positons const int SERVORIGHT = 50; const int SERVOLEFT = 130; // Mouth Servo const int MOUTHPIN = 7; const int MOUTHRIGHT = 70; // mouth movements during speech const int MOUTHLEFT = 110; // Speech const int VOLUME = 20; // volume in range 1 to 30 const int TRACKS = 35; // number of tracks unsigned long trackLength[TRACKS]={ // stores the track length in milliseconds trackLength[0] = 0, // not used trackLength[1] = 2300, // Bite trackLength[2] = 2100, // Evil laugh trackLength[3] = 2200, // A cake trackLength[4] = 5700, // Afterlife trackLength[5] = 1600, // All the money trackLength[6] = 3400, // Bad computer trackLength[7] = 1200, // You kidding trackLength[8] = 4500, // Beer trackLength[9] = 1900, // Bring it on trackLength[10] = 6400, // Cheers me up trackLength[11] = 3700, // Compare trackLength[12] = 1600, // Death to humans trackLength[13] = 1700, // Dream on trackLength[14] = 2400, // Enjoying it trackLength[15] = 3100, // Everyone else's fault trackLength[16] = 1800, // Hello peasants trackLength[17] = 2600, // Hilarity unit trackLength[18] = 7500, // Hitting them trackLength[19] = 1200, // I'm Bender trackLength[20] = 2600, // Lawsuit trackLength[21] = 2200, // Like best trackLength[22] = 3000, // Lovable Rascal trackLength[23] = 5200, // Mechanical heart trackLength[24] = 7000, // Nanosecond trackLength[25] = 1600, // Nerds trackLength[26] = 1000, // No thanks trackLength[27] = 2400, // Noise hole trackLength[28] = 2600, // None of your business trackLength[29] = 900, // Oh my god trackLength[30] = 1800, // So beautiful trackLength[31] = 1800, // Square trackLength[32] = 1500, // Terrible shame trackLength[33] = 1400, // Thank you trackLength[34] = 7300}; // The end // WiFi char ssid[] = SECRET_SSID; // network SSID (name) char pass[] = SECRET_PASS; // network password int keyIndex = 0; // network key Index number (needed for WEP) int status = WL_IDLE_STATUS; // Server int track = 0; int expression = 0; // Instantiation WiFiServer server(80); // server socket WiFiClient client = server.available(); DFRobotDFPlayerMini speech; Adafruit_NeoPixel pixels(NUMPIXELS, PIXELPIN, NEO_GRB + NEO_KHZ800); Servo leftEye; Servo rightEye; Servo mouth; // ----------------------------------------------------------------- // setup // ----------------------------------------------------------------- void setup(){ if (DEBUG == true){ Serial.begin(115200); while(!Serial); Serial.println("Started!"); } initWifi(); if (DEBUG == true){ printWifiStatus(); } Serial1.begin(9600); // Speech on Serial1 while(!Serial1); while (!speech.begin(Serial1)); pixels.begin(); // WS2812 / neopixels pixels.clear(); leftEye.attach(LEFTEYEPIN); // servos rightEye.attach(RIGHTEYEPIN); mouth.attach(MOUTHPIN); // intro showPixels(60, 0, 0, 200, 1200, 80); // red, yellow waggleEyes(MEDIUM); playTrack(2, VOLUME, trackLength[2], MEDIUM); showPixels(50, 40, 10, 200, 1200, 80); // yellow, yellow } // ----------------------------------------------------------------- // loop // ----------------------------------------------------------------- void loop(){ client = server.available(); if (client){ webPage(); if (expression > 0){ playExpression(expression); expression = 0; } if (track > 0){ playTrack(track, VOLUME, trackLength[track], MEDIUM); track = 0; } } } // ----------------------------------------------------------------- // showPixels // ----------------------------------------------------------------- void showPixels(int redEye, int greenEye, int blueEye, int redMouth, int greenMouth, int blueMouth){ pixels.setPixelColor(0, pixels.Color(redMouth, greenMouth, blueMouth)); pixels.setPixelColor(1, pixels.Color(redMouth, greenMouth, blueMouth)); pixels.setPixelColor(2, pixels.Color(redMouth, greenMouth, blueMouth)); pixels.setPixelColor(3, pixels.Color(redMouth, greenMouth, blueMouth)); pixels.setPixelColor(4, pixels.Color(redEye, greenEye, blueEye)); pixels.setPixelColor(5, pixels.Color(redEye, greenEye, blueEye)); pixels.show(); } // ----------------------------------------------------------------- // lookFrontToLeft // ----------------------------------------------------------------- void lookFrontToLeft(unsigned long eyePause){ int servoPos; for (servoPos = SERVOFRONT; servoPos <= SERVOLEFT; servoPos += 1){ leftEye.write(servoPos+LEFTADJ); rightEye.write(servoPos+RIGHTADJ); delay(eyePause); } } // ----------------------------------------------------------------- // lookLeftToFront // ----------------------------------------------------------------- void lookLeftToFront(unsigned long eyePause){ int servoPos; for (servoPos = SERVOLEFT; servoPos >= SERVOFRONT; servoPos -= 1){ leftEye.write(servoPos+LEFTADJ); rightEye.write(servoPos+RIGHTADJ); delay(eyePause); } } // ----------------------------------------------------------------- // lookFrontToRight // ----------------------------------------------------------------- void lookFrontToRight(unsigned long eyePause){ int servoPos; for (servoPos = SERVOFRONT; servoPos >= SERVORIGHT; servoPos -= 1) { leftEye.write(servoPos+LEFTADJ); rightEye.write(servoPos+RIGHTADJ); delay(eyePause); } } // ----------------------------------------------------------------- // lookRightToFront // ----------------------------------------------------------------- void lookRightToFront(unsigned long eyePause){ int servoPos; for (servoPos = SERVORIGHT; servoPos <= SERVOFRONT; servoPos += 1){ leftEye.write(servoPos+LEFTADJ); rightEye.write(servoPos+RIGHTADJ); delay(eyePause); } } // ----------------------------------------------------------------- // waggleEyes // ----------------------------------------------------------------- void waggleEyes(unsigned long eyePause){ int servoPos; for (servoPos = SERVOFRONT; servoPos <= 115; servoPos += 1){ leftEye.write(servoPos+LEFTADJ); rightEye.write(servoPos+RIGHTADJ); delay(eyePause); } for (servoPos = 115; servoPos >= 65; servoPos -= 1){ leftEye.write(servoPos+LEFTADJ); rightEye.write(servoPos+RIGHTADJ); delay(eyePause); } for (servoPos = 65; servoPos <= SERVOFRONT; servoPos += 1){ leftEye.write(servoPos+LEFTADJ); rightEye.write(servoPos+RIGHTADJ); delay(eyePause); } } // ----------------------------------------------------------------- // waggleMouth // ----------------------------------------------------------------- void waggleMouth(int trackLen, unsigned long mouthSpeed){ unsigned long stopWaggle = millis() + trackLen; int servoPos = SERVOFRONT; int servoDir = 1; while (millis() <= stopWaggle){ if (servoPos > MOUTHLEFT && servoDir == 1){ servoDir = -1; } if (servoPos < MOUTHRIGHT && servoDir == -1){ servoDir = 1; } servoPos = servoPos + servoDir; mouth.write(servoPos); delay(mouthSpeed); } mouth.write(SERVOFRONT); } // ----------------------------------------------------------------- // playExpression // ----------------------------------------------------------------- void playExpression(int expressionNo){ if (DEBUG == true){ Serial.print("Expression "); Serial.print(expressionNo); Serial.println(" requested"); } if (expressionNo == 1){ showPixels(50, 40, 10, 200, 1200, 80); // yellow, yellow } if (expressionNo == 2){ showPixels(60, 0, 0, 200, 1200, 80); // red, yellow } if (expressionNo == 3) { showPixels(50, 40, 10, 200, 1200, 80); // yellow, yellow lookFrontToRight(FAST); delay(DELAYMOVE*2); lookRightToFront(SLOW); } if (expressionNo == 4) { showPixels(60, 0, 0, 200, 1200, 80); // red, yellow lookFrontToRight(FAST); delay(DELAYMOVE*2); lookRightToFront(SLOW); } if (expressionNo == 5) { showPixels(50, 40, 10, 200, 1200, 80); // yellow, yellow lookFrontToLeft(FAST); delay(DELAYMOVE*2); lookLeftToFront(SLOW); } if (expressionNo == 6) { showPixels(60, 0, 0, 200, 1200, 80); // red, yellow lookFrontToLeft(FAST); delay(DELAYMOVE*2); lookLeftToFront(SLOW); } if (expressionNo == 7) { showPixels(50, 40, 10, 200, 1200, 80); // yellow, yellow waggleEyes(SLOW); } if (expressionNo == 8) { showPixels(60, 0, 0, 200, 1200, 80); // red, yellow waggleEyes(SLOW); } if (expressionNo == 9) { waggleMouth(1000, MEDIUM); } } // ----------------------------------------------------------------- // playTrack // ----------------------------------------------------------------- void playTrack(int trackNo, int volume, int trackLen, unsigned long mouthSpeed){ if (DEBUG == true){ Serial.print("Track "); Serial.print(trackNo); Serial.println(" requested"); } speech.volume(volume); //Set volume from 0 to 30 speech.play(trackNo); unsigned long stopWaggle = millis() + trackLen; int servoPos = SERVOFRONT; int servoDir = 1; while (millis() <= stopWaggle){ if (servoPos > MOUTHLEFT && servoDir == 1){ servoDir = -1; } if (servoPos < MOUTHRIGHT && servoDir == -1){ servoDir = 1; } servoPos = servoPos + servoDir; mouth.write(servoPos); delay(mouthSpeed); } mouth.write(SERVOFRONT); } // ----------------------------------------------------------------- // initialize WiFi and start server // ----------------------------------------------------------------- void initWifi(){ // check for the WiFi module: if (WiFi.status() == WL_NO_MODULE) { while (true){ // print error msg here } } // attempt to connect to Wifi network: while (status != WL_CONNECTED) { // Connect to WPA/WPA2 network. Change this line if using open or WEP network: status = WiFi.begin(ssid, pass); // wait 10 seconds for connection: delay(10000); } server.begin(); } // ----------------------------------------------------------------- // webPage // ----------------------------------------------------------------- void webPage() { if (client) { // check if there is a cllent String currentLine = ""; // string to hold incoming data from the client while (client.connected()) { // loop while the client's connected if (client.available()) { // check for byte to read from the client, char c = client.read(); // read a byte, then if (c == '\n') { // respond only when there is new line // if the current line is blank there were two newline characters in a row. // that's the end of the client HTTP request, so send a response: if (currentLine.length() == 0) { // standard response header client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html"); client.println("Connection: close"); client.println(); // web page client.println("<!DOCTYPE html>"); client.println("<html>"); client.println("<head>"); client.println("<title>Bender</title>"); client.println("</head>"); client.println("<body>"); // Title client.println("<h1>Bender</h1>"); client.print("------------- Expressions -------------<br>"); client.print("<a href=\"/NORMAL\">Normal</a> eyes<br>"); // 1 client.print("<a href=\"/ANGRY\">Angry</a> eyes<br>"); // 2 client.print("<a href=\"/NORMAL_R\">Right</a> look normal<br>"); // 3 client.print("<a href=\"/ANGRY_R\">Right</a> look angry<br>"); // 4 client.print("<a href=\"/NORMAL_L\">Left</a> look normal<br>"); // 5 client.print("<a href=\"/ANGRY_L\">Left</a> look angry<br>"); // 6 client.print("<a href=\"/NORMAL_EYE_WAGGLE\">Waggle</a> eyes normal<br>"); // 7 client.print("<a href=\"/ANGRY_EYE_WAGGLE\">Waggle</a> eyes angry<br>"); // 8 client.print("<a href=\"/MOUTH_WAGGLE\">Waggle</a> mouth<br>"); // 9 client.print("<br>---------------- Phrases ---------------<br>"); client.print("<a href=\"/BITE\">1 Bite my shiny</a><br>"); client.print("<a href=\"/EVIL\">2 Evil laugh</a><br>"); client.print("<a href=\"/ACAK\">3 A cake you want</a><br>"); client.print("<a href=\"/AFTE\">4 Afterlife</a><br>"); client.print("<a href=\"/ALLT\">5 All the money</a><br>"); client.print("<a href=\"/BADC\">6 Bad computer</a><br>"); client.print("<a href=\"/YOUK\">7 You kidding</a><br>"); client.print("<a href=\"/BEER\">8 Beer</a><br>"); client.print("<a href=\"/BRIN\">9 Bring it on baby</a><br>"); client.print("<a href=\"/CHEE\">10 Cheers me up</a><br>"); client.print("<a href=\"/COMP\">11 Compare</a><br>"); client.print("<a href=\"/DREA\">12 Dream on</a><br>"); client.print("<a href=\"/EVER\">13 Everyone elses fault</a><br>"); client.print("<a href=\"/HELLO\">14 Hello peasants</a><br>"); client.print("<a href=\"/HILA\">15 Hilarity</a> <br>"); client.print("<a href=\"/HITT\">16 Hitting them</a><br>"); client.print("<a href=\"/IMBE\">17 I'm Bender</a><br>"); client.print("<a href=\"/LAWS\">18 Lawsuit</a><br>"); client.print("<a href=\"/LIKE\">19 Like best</a><br>"); client.print("<a href=\"/LOVA\">20 Lovable rascal</a><br>"); client.print("<a href=\"/MECH\">21 Mechanical heart</a><br>"); client.print("<a href=\"/NANO\">22 Nanosecond</a><br>"); client.print("<a href=\"/NERD\">23 Nerds</a> <br>"); client.print("<a href=\"/NOTH\">24 No thanks</a><br>"); client.print("<a href=\"/NOIS\">25 Noise hole</a><br>"); client.print("<a href=\"/NONE\">26 None of your business</a><br>"); client.print("<a href=\"/OHMY\">27 Oh my god</a><br>"); client.print("<a href=\"/SOBE\">28 So beautiful</a><br>"); client.print("<a href=\"/SQUA\">29 Square</a> <br>"); client.print("<a href=\"/TERR\">30 Terrible shame</a><br>"); client.print("<a href=\"/THAN\">31 Thank you</a><br>"); client.print("<a href=\"/THEE\">32 The end</a><br>"); //client.println("<\h2>"); //client.println(); client.println("</body>"); client.println("</html>"); // break out of the while loop: break; } else { // if you got a newline, then clear currentLine: currentLine = ""; } } else if (c != '\r') { // if you got anything else but a carriage return character, currentLine += c; // add it to the end of the currentLine } if (currentLine.endsWith("GET /NORMAL")) { expression = 1; } if (currentLine.endsWith("GET /ANGRY")) { expression = 2; } if (currentLine.endsWith("GET /NORMAL_R")) { expression = 3; } if (currentLine.endsWith("GET /ANGRY_R")) { expression = 4; } if (currentLine.endsWith("GET /NORMAL_L")) { expression = 5; } if (currentLine.endsWith("GET /ANGRY_L")) { expression = 6; } if (currentLine.endsWith("GET /NORMAL_EYE_WAGGLE")) { expression = 7; } if (currentLine.endsWith("GET /ANGRY_EYE_WAGGLE")) { expression = 8; } if (currentLine.endsWith("GET /MOUTH_WAGGLE")) { expression = 9; } if (currentLine.endsWith("GET /BITE")) { track = 1; } if (currentLine.endsWith("GET /EVIL")) { track = 2; } if (currentLine.endsWith("GET /ACAK")) { track = 3; } if (currentLine.endsWith("GET /AFTE")) { track = 4; } if (currentLine.endsWith("GET /ALLT")) { track = 5; } if (currentLine.endsWith("GET /BADC")) { track = 6; } if (currentLine.endsWith("GET /YOUK")) { track = 7; } if (currentLine.endsWith("GET /BEER")) { track = 8; } if (currentLine.endsWith("GET /BRIN")) { track = 9; } if (currentLine.endsWith("GET /CHEE")) { track = 10; } if (currentLine.endsWith("GET /COMP")) { track = 11; } if (currentLine.endsWith("GET /DEAT")) { track = 12; } if (currentLine.endsWith("GET /DREA")) { track = 13; } if (currentLine.endsWith("GET /ENJO")) { track = 14; } if (currentLine.endsWith("GET /EVER")) { track = 15; } if (currentLine.endsWith("GET /HELLO")) { track = 16; } if (currentLine.endsWith("GET /HILA")) { track = 17; } if (currentLine.endsWith("GET /HITT")) { track = 18; } if (currentLine.endsWith("GET /IMBE")) { track = 19; } if (currentLine.endsWith("GET /LAWS")) { track = 20; } if (currentLine.endsWith("GET /LIKE")) { track = 21; } if (currentLine.endsWith("GET /LOVA")) { track = 22; } if (currentLine.endsWith("GET /MECH")) { track = 23; } if (currentLine.endsWith("GET /NANO")) { track = 24; } if (currentLine.endsWith("GET /NERD")) { track = 25; } if (currentLine.endsWith("GET /NOTH")) { track = 26; } if (currentLine.endsWith("GET /NOIS")) { track = 27; } if (currentLine.endsWith("GET /NONE")) { track = 28; } if (currentLine.endsWith("GET /OHMY")) { track = 29; } if (currentLine.endsWith("GET /SOBE")) { track = 30; } if (currentLine.endsWith("GET /SQUA")) { track = 31; } if (currentLine.endsWith("GET /TERR")) { track = 32; } if (currentLine.endsWith("GET /THAN")) { track = 33; } if (currentLine.endsWith("GET /THEE")) { track = 34; } } } // close the connection: client.stop(); } } // ----------------------------------------------------------------- // print the WiFi Status // ----------------------------------------------------------------- void printWifiStatus() { IPAddress ip = WiFi.localIP(); if (DEBUG == true){ // print the SSID of the network you're attached to: Serial.print("SSID: "); Serial.println(WiFi.SSID()); // print your board's IP address: Serial.print("IP Address: "); Serial.println(ip); // print the received signal strength: long rssi = WiFi.RSSI(); Serial.print("signal strength (RSSI):"); Serial.print(rssi); Serial.println(" dBm"); } }
Video Demonstration
In this short 2 1/2 minute video I interview Bender and he gives us a taste of what the Robot Overlords will really be like. Excuse his crude manners, he knows not better.
Conclusion
I'm out of time so need to publish. This project will serve as my National Robotics Week contribution (it ends today) as well as a belated Arduino Day celebratory project. And hopefully it cheers you up a little. I need to use my inkjet printer for the mouth instead of freehand drawing and that can be easily done and changed out. The WiFi user interface is a bit rough. The eye servos are cheap and metal gears with less backlash would be better. The mouth carousel is noisy and a proper bearing should be used. Other than that there are a few changes that could be done to make assembly easier but I like the way it turned out. And my videos are amateurish. If you made it this far thanks for reading and as always questions, comments and suggestions are appreciated.
Top Comments