PVC Dadbot

by SeanMiller in Circuits > Microcontrollers

778 Views, 4 Favorites, 0 Comments

PVC Dadbot

DadBot1.png

There was a time I was faced with having to travel a lot for work. My boy and I were bummed out until we got the idea to make a Dadbot. Using Facetime, PVC pipe, and a microcontroller, we tricked out a PVC frame with a mechanized body I could control from my hotel room. This allowed me to hang out with my son using Facetime in a way that felt like I was right there with him!

Supplies

Arduino Nano 33 IoT or Netduino 3 Wifi

1/2 Inch PVC Elbows

1/2 Inch PVC Tees

1/2 Inch PVC Pipe

12V DC Motor (2)

Wheels (2)

Servo

3D Printed Parts

Dimension Engineering Sabertooth Motor Controller

12V-24V Battery

Cutting Board

DESIGN YOUR BOTS STRUCTURE

dadbotsketch.png

Since building with PVC is essentially just combining parts like Lego bricks, we can readily draw up a good design and start building. By drawing it first, it will reduce the trial and error of building your frame.

In our design, the very bottom will have a cutting board that we will mount the microcontroller, battery, and motors to. The rest of the frame is to give Dadbot some height to install his head.

Cut and Assemble the PVC Frame

dadbotcutter.png
dadbotassembly.png
dadbotassembled.png

Referencing our design, it's now just a matter of cutting the pipe and assembling. No need to measure, you can just wing it and mark the cut points as you fit it.

Don't use any glue as the PVC parts will have a snug fit without it and it will allow you to experiment with your design.

The finishing touch is to add the sweatshirt you wear the most to give your bot character.

Print the Phone Holder

dadbotfusion.png
dadbot3dprint.png
Dadbot3DPrinted.png

We are using facetime to allow for Dadbot's face. Using a servo and a random motion routine on the microcontroller, it will allow for his head to move. To design the holder for the phone, we used Autodesk Fusion 360 and printed with a 3D Printer.

Assemble the Electronics

dadbotboard.png
dadbotfullboard.png

If you were wondering what the cutting board was for, it just makes a great plate to mount electronics and motors! The plastic drills easily for bolt holes to firmly mount everything.

Programming the Microcontroller

To move the head and dance him around, I did a hack on my existing R2D2 controller code. The code below will allow one to drive Dadbot and while his servo on his neck moves around.



using System;
using System.IO.Ports;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;



namespace DadbotV2
{
    public class Program
    {   
        //v2.1 by Sean J. Miller MIT License


        
        static AnalogInput pin_XBee1 = new AnalogInput(AnalogChannels.ANALOG_PIN_A0); //testing the xbee for a joystick
        static AnalogInput pin_XBee2 = new AnalogInput(AnalogChannels.ANALOG_PIN_A1); //testing the xbee for a joystick
        
        static AnalogInput pin_ProximitySensorSignal_Front = new AnalogInput(AnalogChannels.ANALOG_PIN_A2); //a short name for a critical variable I'll be using.
        static AnalogInput pin_DomeForward = new AnalogInput(AnalogChannels.ANALOG_PIN_A3);//reads the reed switch
        static bool DOME_FORWARD = false; static bool WAITING_FOR_GOOD_SIGNAL = true; static bool IDLE = true;//IDLE is set if the controller is turned off.
        const int sound_scream = 253;
        static int the_volume = 0;//variable used to set the volume in the set volume function;
        static int chatter_interval = 12;//used to trigger the frequency of chattering
        static bool SOUND_PLAYING = false;  //this is used to prevent clipping a sound that is playing if the person moves from zone to zone.
        const bool INTERRUPT_TRACK = true;
        const bool DONT_INTERRUPT_TRACK = false;
        static bool PLAY_SOUND_AUTONOMOUSLY = true;
        static SerialPort serial_MP3TriggerBoard = new SerialPort(SerialPorts.COM1, 38400, Parity.None, 8, StopBits.One);


        static double temp_front_pin_voltage = 0;
        static DateTime dome_time = DateTime.Now.AddSeconds(1);
        static bool DOME_CONTROL_ONLY = true;//This will help at startup to ensure the gear_switch is down.
        static int preferred_dome = 0;//The prefferred direction and speed of the dome as determined by the thumbstick or method
        static int current_dome = 0;//The last set speed and direction of the dome for comparison logic.
        static int preferred_straight_speed = 0;//The preferred direction and speed of the robot in a straight line.  0 is stop -100 is full backwards. 100 is full forwards.
        static int current_straight_speed = 0;//The last set speed and direction of the robot in a straight line.
        static int preferred_direction = 0;//The preferred direction and speed of the robot turn.  0 is stop -100 is full clockwise. 100 is full counterclockwise.
        static int current_direction = 0;//The last set speed and direction of the robot turn.
        static double K = 1.0; //for ramping of straight motor control .1 slows it down, 1 is no ramping
        static double K2 = 1.0; //for ramping of direction .3 slows it down, 1 is no ramping
        static System.Threading.Timer timer_sound_reset;//this is to auto reset SOUND_PLAYING to false after 20 seconds just in case I miss the serial RX event.
        static TimerCallback myTimerCallbackMethod;
        static double distance_increment = .06; //distance increment is an analog voltage increment.  Used Debug.Print to assess values for the zones.
        static double furthest_distance_right = 10.0;//used to find the closest object;
        
        static int front_distance_zone = 10;//this is to track which increment zone the object is in per the proximity sensor.  It starts out at 10 arbitrarily.
        static Random random_number;                
        static InterruptPort inputPort7_JoystickClucka;
        static float currentUpStickVoltage = .5f;
        static float currentDirectionStickVoltage = .5f;
        static float currentLeftRightStickVoltage = .5f;
        //Netduino Pulse Out
        static Microsoft.SPOT.Hardware.PWM PWM_DomeMovementToPin9, PWM_StraightMovementToPin11, PWM_TurnMovementToPin10;  //Dome, Straight Line, and Turn pulses to motor drivers
        static Thread soundThread, distanceThread, pulseThread;//One thread to handle dome and feet motors and the other is to handle playing sounds.
        //static Thread movementThread, autonomousDomeThread;
        //End of Variables


        public static void Main()
        {   //This initiates the program and starts the forever looping routine named update.


            myTimerCallbackMethod = new TimerCallback(clearTime);
            timer_sound_reset = new System.Threading.Timer(myTimerCallbackMethod, null, 20000, 60000);//used to reset audio the Netduino misses the X sent back by the MP3Trigger Board
            pin_XBee1.Offset = .06; pin_XBee2.Offset = .012;//tweak the voltage to match the characteristics of the Xbee shield.
            setupSerialCommunication();
            setUpInputInterruptPorts(); 
            setupOutputPWMpins();


            Thread.Sleep(1000);//give one second for interrupts to do their thing to set parameters.
           


            random_number = new Random((int)(pin_XBee1.Read() * 1000.0f));//will set the randomizer based on the voltage on pin 7.
            soundThread = new Thread(performSound); soundThread.Start();//start the thread that beeps and bloops
            distanceThread = new Thread(senseProximityAndDomePosition); distanceThread.Start();//start the thread that categorizes distance from objects into zones which is used to rome autonomously or fire interlocks on thumbstick control
            pulseThread = new Thread(pulseToMotorDrivers); pulseThread.Start();
            roamAround();
            Thread.Sleep(Timeout.Infinite);//sleep and let the threads do their magic
        }


        static void roamAround()
        {
            do
            {
                findFurthestObject();
                turnToFurthestObject();
                driveForward();
                Thread.Sleep(4000);
            } while (1 == 1);
        }


        static void driveForward()
        {
            do
            {   preferred_straight_speed = 70;
                preferred_direction = 5;
                Thread.Sleep(10);
            } while (temp_front_pin_voltage < .5);
            
            preferred_straight_speed = -80;
            preferred_direction = -90;
            Thread.Sleep(2700);
            stopRobot();
            preferred_straight_speed = 80;


            Thread.Sleep(3000);
            stopRobot();
        }
        static void turnToFurthestObject()
        {
            int ii = 0;
            stopRobot();
            do
            {
                preferred_direction = -80;
                Thread.Sleep(10);
                ii++;
            } while (temp_front_pin_voltage*.98 > furthest_distance_right&&ii<600);
            stopRobot();
        }
        static void findFurthestObject()
        {
            stopRobot();
            furthest_distance_right = 10;
            preferred_direction = 80;
            Thread.Sleep(6000);
            stopRobot();
            Thread.Sleep(1000);
        }
        static private void stopRobot()
        {
            preferred_direction = 0;
            preferred_straight_speed = 0;
        }
        static void setUpInputInterruptPorts()
        {   //EdgeLow parameter:  Fire only when it changes state.EdgeLevelLow would fire until it went back high.
            inputPort7_JoystickClucka = new InterruptPort(Pins.GPIO_PIN_D7, true, Port.ResistorMode.PullDown, Port.InterruptMode.InterruptEdgeLow);
            inputPort7_JoystickClucka.OnInterrupt += new NativeEventHandler(inputPort_OnInterrupt7); //disabled due to glucka not working on joystick
        }        
        static void inputPort_OnInterrupt7(uint data1, uint data2, DateTime time)
        {//Joystick button click
            
            inputPort7_JoystickClucka.DisableInterrupt();
            if (the_volume == 20) the_volume = 40; else the_volume = 20;
            //PlayTrack(sound_scream, INTERRUPT_TRACK);
            PlayTrack(254, INTERRUPT_TRACK);
            inputPort7_JoystickClucka.EnableInterrupt();
          
        }
        static void setupOutputPWMpins()
        {
            PWM_DomeMovementToPin9 = new Microsoft.SPOT.Hardware.PWM(PWMChannels.PWM_PIN_D9, 20000, 1500, PWM.ScaleFactor.Microseconds, false);//Goes to Syrene10 for Dome
            PWM_TurnMovementToPin10 = new Microsoft.SPOT.Hardware.PWM(PWMChannels.PWM_PIN_D10, 20000, 1500, PWM.ScaleFactor.Microseconds, false);//Goes to Sabertooth for feet
            PWM_StraightMovementToPin11 = new Microsoft.SPOT.Hardware.PWM(PWMChannels.PWM_PIN_D11, 20000, 1500, PWM.ScaleFactor.Microseconds, false);//Goes to Sabertooth for feet
        
        }
        static void setupSerialCommunication()
        {
            //sets up serial to tell MP3Triggerboard what sound to play.  Also sets up for dome serial communication.
            serial_MP3TriggerBoard.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler_TriggerBoard);
            serial_MP3TriggerBoard.Open(); serial_MP3TriggerBoard.Flush();
        }
        static void senseProximityAndDomePosition()
        {   //Identify what zone the object is that is infront of the robot.  It runs in the distanceThread, so
            //we will always be aware of the distance from the sensors.


            while (true)
            {
                //handle distance
                temp_front_pin_voltage = pin_ProximitySensorSignal_Front.Read();
                if (temp_front_pin_voltage < furthest_distance_right) furthest_distance_right = temp_front_pin_voltage;
                if (temp_front_pin_voltage < distance_increment) front_distance_zone = 1;
                else if (temp_front_pin_voltage < 2 * distance_increment) front_distance_zone = 2;
                else if (temp_front_pin_voltage < 3 * distance_increment) front_distance_zone = 3;
                else if (temp_front_pin_voltage < 4 * distance_increment) front_distance_zone = 4;
                else if (temp_front_pin_voltage < 5 * distance_increment) front_distance_zone = 5;
                else if (temp_front_pin_voltage < 6 * distance_increment) front_distance_zone = 6;
                else if (temp_front_pin_voltage < 7 * distance_increment) front_distance_zone = 7;
                else if (temp_front_pin_voltage < 8 * distance_increment) front_distance_zone = 8;
                else if (temp_front_pin_voltage < 9 * distance_increment) front_distance_zone = 9;
                else if (temp_front_pin_voltage < 10 * distance_increment) front_distance_zone = 10;
                else front_distance_zone = 10;
                //End Handle Distance


               
                Thread.Sleep(10);
            }
        }
        static void performSound()
        {   //This is the main program that monitors sensors and loops infinitely.
            int last_front_distance_zone = -1;//set initially to something that will ensure sound is triggered initially.
            int ticker = 0;//used to trigger on time.
            while (true)
            {
                if (PLAY_SOUND_AUTONOMOUSLY)
                {
                    if ((last_front_distance_zone != front_distance_zone) && ticker > 4)
                    {   //trigger on distance
                        if (front_distance_zone == 1)
                        {
                            PlayTrack(randomInRange(front_distance_zone), DONT_INTERRUPT_TRACK);
                           // PlayTrack(sound_scream, INTERRUPT_TRACK);
                            Thread.Sleep(1000);
                        }
                        else
                        {
                            PlayTrack(randomInRange(front_distance_zone), DONT_INTERRUPT_TRACK);
                        }
                        last_front_distance_zone = front_distance_zone;//this keeps him from chattering too much.
                        ticker = 0;//resets the timer for silence.
                    }


                    if (ticker++ > chatter_interval * 2)//Trigger on time if one hasn't played based on distance for a period.  Chatter_interval is in seconds and works with the thread.sleep being 500 below.
                    {
                        PlayTrack(randomInRange(front_distance_zone), DONT_INTERRUPT_TRACK);
                        ticker = 0;
                    }
                }
                Thread.Sleep(500);
            }
        }
        static void calculateDomeStickPreferredSpeedAndDirection()
        {   //Xbee pin 7(maybe) pin to analog 1 on Netduino.
            currentDirectionStickVoltage = (float)pin_XBee1.Read();
            if (currentDirectionStickVoltage >= .51)
            {
                preferred_dome = (int) (100-(     100f*(100-currentDirectionStickVoltage*100f)/50f ));
            }
            else if (currentDirectionStickVoltage > .480 && currentDirectionStickVoltage < .509)
            {
                preferred_dome = -5;
            }
            else
            {
                preferred_dome = (int)(100 - (100f * (100 - currentDirectionStickVoltage * 100f) / 50f));
            }
          //  Debug.Print("Dome Direction and speed:" + preferred_dome);
        }
        static void calculateRobotStickPreferredDirection()
        {   //Xbee pin 7(maybe) pin to analog 1 on Netduino.


            if (WAITING_FOR_GOOD_SIGNAL)//this variable is set to true in the interrupt code of glucka.
            {
                stopRobot();
                return;
            }
            
            currentDirectionStickVoltage = (float)pin_XBee1.Read();


            if (!inputPort7_JoystickClucka.Read())
            {
                preferred_direction = 0;//if the remote is out of range or turned off, this will catch it and stop.
            }
            else if (currentDirectionStickVoltage >= .515)
            {
                preferred_direction = -(int)(100 - (100f * (100 - currentDirectionStickVoltage * 100f) / 50f));
            }
            else if (currentDirectionStickVoltage > .485 && currentDirectionStickVoltage < .497)
            {
                preferred_direction = 0;
            }
            else
            {
                preferred_direction = -(int)(100 - (100f * (100 - currentDirectionStickVoltage * 100f) / 50f));
            }


        }
        static void calculateRobotStickPreferredSpeed()
        {   //Xbee pin 7(maybe) pin to analog 1 on Netduino.
            if (WAITING_FOR_GOOD_SIGNAL)//this variable is set to true in the interrupt code of glucka.
            {
                stopRobot();
                return;
            }
            currentUpStickVoltage = (float)pin_XBee2.Read();
            if (!inputPort7_JoystickClucka.Read())
            {
                preferred_straight_speed = 0;//if the remote is out of range or turned off, this will catch it and stop.
            }
            else if (currentUpStickVoltage >= .515)
            {
                preferred_straight_speed = -(int)(100 - (100f * (100 - currentUpStickVoltage * 100f) / 50f));
            }
            else if (currentUpStickVoltage > .485 && currentUpStickVoltage < .514)
            {
                preferred_straight_speed = 0;
            }
            else
            {
                preferred_straight_speed = -(int)(100 - (100f * (100 - currentUpStickVoltage * 100f) / 50f));
            }
        }
        static void pulseToMotorDrivers()


        {   //First, start pulsing the stop position value.
            PWM_TurnMovementToPin10.Start();
            PWM_StraightMovementToPin11.Start();
            PWM_DomeMovementToPin9.Start();
            
            while (true)
            {
                if (preferred_straight_speed == 0) current_straight_speed = 0;
                if (System.Math.Abs(preferred_straight_speed - current_straight_speed) > 3)
                {
                    current_straight_speed = current_straight_speed + (int)(K * (preferred_straight_speed - current_straight_speed));
                    PWM_StraightMovementToPin11.Duration=((uint)(1500 + (5 * current_straight_speed)/2));
                }
                else if (preferred_straight_speed == 0)
                {
                    PWM_StraightMovementToPin11.Duration=((1500));
                }
                if (preferred_direction == 0) current_direction = 0;
                if (System.Math.Abs(preferred_direction - current_direction) > 3)
                {
                    current_direction = current_direction + (int)(K2 * (preferred_direction - current_direction));
                    PWM_TurnMovementToPin10.Duration=((uint)(1500 + (5 * current_direction)/2));
                }
                else if (preferred_direction == 0)
                {
                    PWM_TurnMovementToPin10.Duration=((uint)(1500));
                }             
                Thread.Sleep(10);
            }             
        }
        static void performAutonomousDomeMovement()
        {
        }
        static void waitForRemoteControlOn()
        {
        }
        static void performMovements()
        {
        }
        static private void clearTime(object state)
        {   //This is called by timer_sound_reset thread every 20 seconds as a safety net for the serial event handler not catching the "track ended" response from the trigger board.
            SOUND_PLAYING = false;
            timer_sound_reset.Change(20000, 60000);//resets the timer.
        }
        static private void DataReceivedHandler_TriggerBoard(object sender, SerialDataReceivedEventArgs e)
        {   //This is triggered whenever some TTL is piped into Digital pin 0.
            if (((SerialPort)sender).ReadByte() == 88) SOUND_PLAYING = false;//check for the Letter X from MP3Triggerboard.  This signals the track is finsihed playing.
        }
        static private int randomInRange(int the_distance)
        {
            return ((the_distance - 1) * 10 + random_number.Next(11));//This gives a random number to select random tracks within a distance zone each containing ten sounds.
        }
        static private void PlayTrack(int ii, bool interrupt)
        {   //Sparkfun MP3TriggerBoard Only!
            //first, we ensure we don't interrupt a playing track if so desired.
            setVolume(the_volume);
            if (!SOUND_PLAYING || interrupt == INTERRUPT_TRACK)
            {
                SOUND_PLAYING = true;
                timer_sound_reset.Change(20000, 60000);//resets the timer.
                byte the_track = (byte)(ii);
                serial_MP3TriggerBoard.Write(new byte[] { 0x74, the_track }, 0, 2);//write out the serial to the MP3Trigger board which is the letter t followed by a byte value of 1-255.
                //serial_MP3TriggerBoard.Flush();
            }
        }
        static private void setVolume(int ii)
        {//Sparkfun MP3TriggerBoard Only!
            byte the_setting = (byte)(ii);//according to the MP3Trigger Board, 0 is the loadest and 64 can't be heard.
            serial_MP3TriggerBoard.Write(new byte[] { 0x76, the_setting }, 0, 2);
            Thread.Sleep(300);
        }
    }       
}

Project Summary

Dadbot: Dad of the Future

This was a fun project to do with my son to help us get over our separation anxiety. It taught him about designing on paper, designing with Autodesk Fusion, 3D printing, and how servos work.

What would you build with these building blocks and PVC? Let's discuss in the comments!

-Sean and Connor

https://www.raisingawesome.site