RF (Radio Frequency)

Enter Your Project for a chance to win a Spectrum Analyzer for the Most Innovative RF Project!

Back to The Project14 homepage

Project14 Home
Monthly Themes
Monthly Theme Poll

 

TABLE OF CONTENTS

Sean and Connor Miller's R2D2 Controller

R2D2 Incognito Controller - The Holy Grail of the R2 Build

 

 

INTRODUCTION

This project deep dives into the custom controller we built for our life size R2D2.  It documents our quest for the Holy Grail of R2D2 RF Control - The Incognito Controller.  The original 1977 Star Wars® R2D2 as well as many droids built today by the AstroMech.net(1) community use old school RC Controllers to control their droids.  These controllers are the off-the-shelf ones you see folks using for their RC airplanes and trucks.  They are very reliable, the most affordable option, and have fail safe tech built-in.

 

Unfortunately, the classic RC Control takes away all the magic that is Star Wars®.  There is nothing that screams “It’s just a big toy!” more than having a 47 year old man like me holding a box with a 3 foot long antenna with a big dopey smile on his face standing nearby.  Also, by themselves, they aren't smart - most designs go from the RC receiver straight to the motor drivers of the droid.  One bump of the stick or loss of line of site in a crowd and someone's getting hurt.

 

Ron Hone, Star Wars® Droid Fabricator, Troubleshooting a Classic RC Controlled R2 circa 1980(2)

 

So, when we built our droid and explored our controller options, we arrived at an RF technology solution that essentially simulates virtual wires over very long distances.  With it providing multiple virtual analog, digital, and Pulsed Width Modulation (PWM) signals, we can tie the receiver to a microcontroller to design in safety interlocks - true smart control versus the dumb traditional RC control.  Also, the tech is small enough to make a circuit that fits in your pocket.  With it being hidden from the crowd, it creates that movie magic feeling getting the kids (and parents) to scream “That’s a real R2!”.

 

That tech we found, all the way back in 2015, was XBee by Digi International.  We hope you enjoy this write-up and gain some insight and design files for your own RF/Virtual Wire control projects as well.  Let's get to it!

 

 

BILL OF MATERIALS

    XBee 802.15.4 KitXBee 802.15.4 Kit

    Game Controller Thumbstick

    USB to 3.3TTL Conversion CableUSB to 3.3TTL Conversion Cable

    3D Printed Files

    https://github.com/RaisingAwesome/R2D2

 

THE QUEST FOR THE HOLY GRAIL - INCOGNITO R2 CONTROL

This section is a bit of nostalgia that was fun to reflect upon in an era when Makers transitioned from classic RC direct drive control to microcontrollers.  However, this segment isn't required for the build.  So, you may jump ahead to R2D2 Controller Design Requirements section without missing pertinent build info.

 

A Long Time Ago on a Forum Far, Far Away

As a 7 year old, I dreamed of building an R2.  It wasn't until over 30 years later, I finally did with my son.  It took years!(11)  I first joined the R2 Builders Club on Yahoo Groups in 2009 which later moved to AstroMech.net(1) .  At that time few on the community, including myself could spell Arduino and Raspberry Pi was still just a dessert.  The forum was a lot like element14 in that the builder's were always eager to help and collaborate on their designs.  However, the emphasis back then was on the craft of building - no one owned a 3D printer in the crowd and few held a soldering iron.  We hand built our droids from wood, polystyrene, clay, aluminum, and resin as I outlined in this blog:  The Making of R2D2 - The Force on Wheels .  Our electronics were plug and play, off-the-shelf solutions with the exception of a few member made Picaxe based dome control kits purchased by impatient, affluent droid assemblers that just wanted a droid versus the build experience.

 

Also, at that time, some builders would have two controllers - one was an RC controller for his foot motors and a separate XBox controller for his dome and panel servos.  Many of us started to use split rings to allow infinite rotation of the dome with the batteries solely in the body alone.  I remember that was a big thing in that folks didn't have to worry about tangled wires or charging woes for the dome power.

 

Back then, seasoned builder's did not like to entertain anything but RC control for the foot motors of R2. In part, because it would be like starting over on the project to gut out what we had.  But mostly because the droids weighed in between 150-250 lbs!  Many members did commissioned events where their droids were mobbed by 10 kids at a time.  There was always the fear that someone would get hurt.  You never ever took your eye off the droid and you turned off the RC controller when anyone approached to eliminate any chance of bumping a stick.  You also, never let anyone else control it.

 

Me (Sean Miller) Back in 2014 with my "Old RC" Transmitter in Hand(12)

Wishing she wasn't so cute when asking "Can I Drive It??  Pleeeeeeaasssseeeeee!"

 

Sometimes events could turn into a "Can I drive it?" event.  Instead of the demonstration being about the magic of R2 or the engineering behind it, the focus for the kids was more on getting to drive him.  It would be a major buzz kill when I'd say "Sorry, not this time" to the kids and parents.  So, around 2013 in the Astromech community, we had a lot of forum discussion around the Holy Grail for R2 control - the Incognito Controller.

 

It wasn't a race to who could make the best tech, just inspiring discussions and posts by less than a handful of members as others encouraged our efforts through forum replies and view count metrics.  I remember getting stoked when I broke into the top 20 of 1200 most viewed Builder Logs back then.  There was no reputation system, so that's all I had for a metric.  I attributed the interest due to two of my original designs introduced to the community:  R2D2 Neckbone Design(11) and the One-Handed, Pocket Sized Incognito Controller described in this blog.

 

From that era, three solutions that I know of emerged within the community for incognito control:

 

1)  A Two Handed "Stealth" Controller (available for purchase) , custom solution

2)  My One Handed "Incognito" Controller (open source), Netduino/C#

3)  R2 Touch Smart App Approach- this was a few years later.  Arduino based and uses a smart phone as the control. (available for purchase)

 

Other controller variations were made like PS3 and Xbox controllers, but these are still too big to fit in the pocket, so I consider them outside of the incognito category.  They are good alternatives to RC for short distances, though.  I'm certain other solutions were made during that time as well, but the developers didn't create a presence on the R2 Builders Community forum that caught attention.

 

Eventually, the member made kits (not mine) went for sale for $500+ on the forum.  In turn, at that price, the prevailing droid control is still classic RC control today in part because it can be obtained under $100, is plug and play, and requires no coding skills.  Seasoned members press the new members to finish their droid build versus get bogged down in the gadgets.  They caution that many have gotten stuck in the trap of electronic solutions and end up not having anything to show for the hobby but a box of wires that they ultimately sell on the Forum's Part's Junkyard.

 

Another reason RC prevails over DIY foot control is that most of the beginner members are not programmers, but rather Star Wars® enthusiasts that dream of crafting R2 by hand.  Seasoned members discourage leaving the safety of time-tested, fail-safe RC control and risking user bugs, loss of transmission, power failures that could lead to injury of the spectators.  Those early members knew what they were talking about.  Until you can confidently debug threaded code and establish sensor based interlocks - you should stay with RC.  I was glad for their coaching until I knew I was ready for the next level.

 

Here is a more recent pic from Rogue One where a dedicated R2 Builders community member, Colin Barker, got to showcase his droid on the big screen (requires astromech.net signup):  https://astromech.net/forums/showthread.php?13373-Rare-Droid-Behind-the-Scenes-amp-On-Set&p=436383&highlight=rogue#post4…

 

My years at the Astromech club are reflected on fondly.  That was my electronics' hobby coming of age era.  From there, I graduated to element14 where the hobby just keeps growing and growing.

 

Enough nostaligia!  Now back to the Holy Grail controller design.  As said before, it is important for it to be 100% safe by using sensors and logic to protect you.  So, now let's look at our design requirements...

 

R2D2 CONTROLLER DESIGN REQUIREMENTS

  • Invisible control of R2D2 – no one can know who is controlling him
    • Must be controllable by one hand
    • Must fit in a pocket with no visible antennas or cords
  • Must be able to handle up to 100 feet distance between R2 and the controller indoors and out
  • Must run on AAA batteries
  • Must be US FCC approved
  • Must simulate wires so that a microcontroller may receive and respond using the microcontroller’s GPIO.  This will allow us to code in interlocks to prevent unsafe motion.

 

 

XBEE RF TECHNOLOGY

We found XBee Technology by Digi International(3) to fit all our design requirements.  As discussed in the nostalgia section above, at that time, there were at least two others posting on XBee in the R2 Builder's club, but no one landed on an Open Source solution (gerber files and code).  Connor and I just needed to design some circuits to land the XBee modules, configure them for our use case, and write code to handle the events the end user triggers.  It was a great time to make our first PCB.

 

The particular module type we used was the XBee S1. It uses 2.4 Ghz, 802.15.4 communication protocol.  As shown at Digi's webpage(3), the XBee line also has Broadband based modules and 900 Mhz models that can give you RF control at greater than a mile distance.  However, the 802.15.4 standard allows it to serve as a virtual wire.  By that I mean, it isn’t like an IoT device that competes for bandwidth on a Local Area Network or Cellular tower.  It is a direct peer-to-peer communication providing a seemingly latency free transmission to your device.

 

The 802.15.4 refers to an IEEE Standard(4).  Most are familiar with the 802.11 standard for WiFi on their cell phones and home internet wireless routers.  The 802.15.4 standard is intended for low rate wireless personal area networks versus the higher bandwidth of a WiFi network.  It is the basis for ZigBee which folks are now experiencing with home automation hubs such as for Philip's Hue light bulbs and Lightify.

 

 

XBee modules also in small form factors.  Here are the dimensions of the one we used:

 

XBee 802.15.4 Module(5)

This one fits in your pocket easily,

but the current modules are way smaller!

 

With that small form factor, the module can operate as low as 2.8V, so powering with two AAA’s made it a perfect solution.  Best of all, it has its own GPIO covering the bases – Analog, Digital, and PWM!

 

Pin Assignments for XBee S1 802.15.4 Module(5)

 

 

In turn, we have the capability to integrate a wireless, but virtually wired controller into our droid.

 

XBEE CONFIGURATION

The XBee S1 module is configured using simple serial AT commands.  Easy Peasy! Well, not really.  I’d say this was the biggest headache for the newcomer to XBee.  However, after much trial and error, we got the configuration sorted.  Since then, Digi International has improved their configuration software and you can do this now with their XCU tool.  We went old school and shot the AT commands directly ourselves.  By learning this behind-the-scenes, low level approach, it allowed us to be able to send AT commands on the fly with a microcontroller which served us for future projects.

 

So, to communicate the configuration to the transmitter and receiver modules using AT commands, we used a program called CoolTerm. As stated on their website, CoolTerm is a simple serial port terminal application by Roger Meier downloadable at http://freeware.the-meiers.org(6).  It that is geared towards hobbyists and professionals that have a need to exchange data with hardware connected to serial ports such as servo controllers, robotic kits, GPS receivers, microcontrollers, etc.

 

CoolTerm Application(6)

 

We then carefully entered in the AT commands(7) with a 3.3V USB to Serial converter module:

 

Setup:

    Vin must be a 3.3V - don't use a 5V USB servial converter or you risk damage

    The Com port will be something other than 1

    9600 baud

    Note:  don’t type the notes indicated by the # sign.

 

Receiver Module (the one inside R2D2) Commands to type into CoolTerm:

ATID 3001 #(USE THE SAME FOR THE TRANSMITTER)

ATDH 0

ATDL 1 #(ADDRESS OF THE TRANSMITTER TO TALK TO)

ATMY 2 #(THE ADDRESS OF THE RECEIVER, THE TRANSITTER WILL BE SET TO 1)

ATPR FF

ATIR 0

ATIA 1 #(ADDRESS OF THE TRANSMITTER OR YOU CAN USE 0XFFFF TO ALLOW ANY BOARD TO CHANGE THIS BOARD'S OUTPUT)

ATD0 2 #(A2D)

ATD1 2

ATD2 4

ATD3 0

ATD4 0

ATD5 1

ATD6 0

ATD7 1

ATT2 3

ATP0 2

ATP1 2

ATPT 4

 

Transmitter Module Commands for the handheld controller:

ATID 3001 #(USE THE SAME FOR THE TRANSMITTER)

ATDH 0

ATDL 2 #(ADDRESS OF THE TRANSMITTER TO TALK TO)

ATMY 1 #(THE ADDRESS OF THE RECEIVER, THE TRANSITTER WILL BE SET TO 1)

ATIR 14 #(SAMPLE RATE - THIS PREVENTS THE SIGNAL FROM FLICKERING)

ATIA FFFFFFFFFFFFFFFF

ATD0 3 #(DIGITAL INPUT) OR 2 (ANALOG INPUT), ETC

 

Further information on these commands can found in the XBEE AT Command Reference(7) file.

 

 

SCHEMATICS

When designing the circuit for the controller, we found that the buttons on digital inputs of the XBee modules should actually take the pin to ground versus bring Vcc to the pin.  Otherwise, we’d needed a pulldown resistor and give the XBee module a command to turn off the XBee internal pull up resistor.  In turn, the receiver always has voltage until the transmitter button is pressed, but that is no big deal in software as we’ll show in the Code section.

 

I shared pictures of these same circuits in a past contest, Robots on Wheels.  In that blog, I documented the physical construction of the droid, but didn’t cover the design, building, and coding of the RF Controller.  Let’s deep dive on the circuits here.

 

Handheld Transmitter:

Here are the key details:

  • The 1.5V batteries in series are simply AAAs.  The controller has a switch (SG1) to kill current flow when idle.  Even without it, it will get a few hours of use. So, AAAs were found to be the most convenient power source and met the power needs of the XBee module.
  • The joystick is a thumbstick as one would find in a Play Station 2 Controller.  They are very inexpensive and readily available.
  • JP1 is a couple of pins to allow serial communication.  They come in handy for configuration without having to take the controller back apart.
  • R2 (the resistor, not the droid) is a 10K Pull up to 3.3V.  This is so I can push in/click the joystick and take the signal to ground as discussed above.  When it's not push, its a steady 3.3V to let the XBee know he's idle.
  • The Joystick provides analog signals to reflect how far the stick is pushed in any direction. When received by the microcontroller, this translates to speed and direction.

 

 

Handheld Thumbstick Controller

 

For any virtual wire handheld project you can download this Eagle Schematic and Board file to have this fabricated at your favorite PCB fabricator such as Oshpark.com.  Be sure to add your own flare to the design.

    https://github.com/RaisingAwesome/R2D2/blob/master/XBeeTransmitterV2.0.zip

 

Here are some pics of the resulting fabricated board:

Handheld Controller Showing What is Under the Hood of the Radio

A Little Glue Gun Action Goes a Long Way for

Stress Relief on Wire-to-Board Connections

 

Battery Side of the Handheld Controller

Just 2 AAA's easily get you through an all day event with R2

Microcontroller Receiver:

The receiver circuit brought a new learning to us – Digital to Analog conversion of PWM.  Since we are sending a signal with a virtual wire to a module without its own ADC, we had to devise a circuit that could emulate the analog signal that the joystick produces.  To do this, we made what is called an RC Filter as discussed in the Digi Knowledge Base(8):

 

RC Filter for PWM to Analog Conversion(8)

 

 

With a little math and trial and error with resistor values, we determined a 470 ohm resitor and 330 ohm resitor going to function confirming LEDs would do the trick for simulating an analog signal from a PWM digital signal relayed by the XBee.  The way it works is, as the Pulse Width Modulation pulses, the resistor slows the capacitor's ability to drain back down during the off cycle of the pulse.  In turn, the slower the pulse the less the voltage available.  The faster the pulse the higher the voltage.  The resulting steady voltages present an effective analog signal over the virtual wire that is the XBee.

 

Here is the complete circuit for the receiver:

And, last, here is the fabricated shield plugged into our microcontroller:

Custom XBee Receiver Module Shield:

It serves as Virtual Analog, Digital, and PWMWires to the Handheld Controller

 

 

Since the LEDs are in the RC Filter circuit, they brighten as the RF controller joystick is used to verify function and troubleshoot as needed.  This design is Arduino Uno compatible and can be used in a number of projects.  You can obtain it from my GitHub and have it manufactured at your favorite PCB fabricator such as Oshpark.com.  I'd suggest editing the file if your name is Sean or I hope your wife is named Brenda!

https://github.com/RaisingAwesome/R2D2/blob/master/XbeeSeanReceiverV2.0.zip

 

With the XBee's configured, we can take a look at how the Virtual Wire's behave with the controller.   Below is a video showing the output pins of the receiver.  The analog read at the transmitter is translated to PWM output from the receiver.

 

Oscilloscope of the Various Xbee Signals of the R2D2 Incognito Controller

 

3D PRINTED ENCLOSURE

The enclosure was designed to comfortably fit in ones hand and not lose grip.  It readily fits even in ones jeans pocket – well, not skinny jeans, but this 47 year old man's frumpy jeans.

 

Autodesk Fusion 360 Rendering of our R2D2 Controller - Designed to Look Awesome in Your Pocket!

 

 

The STL files for the enclosure can be downloaded from my GitHub Repository:  https://github.com/RaisingAwesome/R2D2(9)

 

 

3D Printed XBee Transmitter Enclosure:  Pocket Sized Ready to Trick the Crowd

 

 

CODE

For the handheld controller, there is no code required at all after you complete the one time configuration with the AT commands shown above.  We simply turn it on and use it like we would a wire attached to to the PCB we made to wire up the joystick.

 

On the receiver module, though, you code your microcontroller exactly as you would a wired project.  In this case, we made the shield pictured in the prior section to be the virtual wire interface to our microcontroller.

 

The microcontroller we used allowed for C# programming.  It's an elegant language that handles coding interrupts and threads in an intuitive way.  So, I made a thread that handled the state of the GPIO which then updated desired direction and desired speed variables available to the other threads.  A separate thread took that data and drove his foot motors (sorry for the spoiler – there are motors hidden in his fake battery compartments at his feet - sometimes there was even a guy in there in the movie!).

 

Secret:  There are Motors in R2's Feet Disguised as Battery Boxes!

 

Here is the full code implementation to control R2D2 with the XBee modules - includes autonomous dome control, safety interlocks, 8-bit songs, and smart audio responses:

//Copyright 2020
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.

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 R2D2NetduinoXBee
{
    public class Program
    {   
        //v2.1 by Sean J. Miller 12/29/2019
        //PROXIMITY SENSOR SIGNAL GOES TO ANALOG 2
        //MP3TRIGGER RX GOES TO NETDUINO DIGITAL 0
        //MP3TRIGGER TX GOES TO NETDUINO DIGITAL 1
        
        //MP3TRIGGER GN GOES TO NETDUINO DIGITAL GN
        //ANALOG 0 FROM JOYSTICK UPDOWN (RSSI)
        //ANALOG 1 FROM JOYSTICK LEFTRIGHT
        //ANALOG 2 FROM PROXIMITY SENSOR
        //ANALOG 3 FROM REED SWITCH FOR DOME CENTERING
        //DIGITAL 2 OPEN
        //DIGITAL 7 FROM XBEE DIO2 - the glucka of the joystick
        //DIGITAL 9 TO SYREN 10 S1 (PWM Out)
        //DIGITAL 10 GOES TO SABERTOOTH S2 (PWM Out)
        //DIGITAL 11 GOES TO SABERTOOTH S1 (PWM Out)
        
        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 = 40;
        static int chatter_interval = 12;
        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 = .25; //for ramping of straight motor control
        static double K2 = .3; //for ramping of direction
        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 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 movementThread, soundThread, distanceThread, pulseThread, autonomousDomeThread;//One thread to handle dome and feet motors and the other is to handle playing sounds.
        //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 of the xbee pin.
            soundThread = new Thread(performSound); soundThread.Start();//start the thread that beeps and bloops
            autonomousDomeThread = new Thread(performAutonomousDomeMovement);//for when it needs to auto move the dome.
            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
            movementThread = new Thread(performMovements); movementThread.Start();//start the thread that handles movement
            pulseThread = new Thread(pulseToMotorDrivers); pulseThread.Start();
            Thread.Sleep(Timeout.Infinite);//sleep and let the threads do their magic
        }
        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 >55) the_volume = 5; else the_volume = the_volume+10;
            PlayTrack(254, INTERRUPT_TRACK);//Song
            inputPort7_JoystickClucka.EnableInterrupt();
        }
        static void setupOutputPWMpins()
        {//Sets all to no movement
            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 < 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
                //Handle Dome position 
                if (pin_DomeForward.Read() == 1)
                {
                    dome_time = DateTime.Now.AddSeconds(2);//keep it flagged for 1 second
                    DOME_FORWARD = true;
                }
                else
                {
                    //DOME_FORWARD = false;
                    if (DateTime.Compare(dome_time, DateTime.Now) < 0) DOME_FORWARD = false; else DOME_FORWARD = true;
                }
                //End Handle Distance
                Thread.Sleep(50);
            }
        }
        static void performSound()
        {   
            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 ((temp_front_pin_voltage < distance_increment*.7) && (last_front_distance_zone != front_distance_zone)) PlayTrack(sound_scream, INTERRUPT_TRACK);
                    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.  No longer an option unless I add some more XBee functionality.  Found automonomous is better for the crowd.
        //    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));
            }
         //   Debug.Print("Direction Voltage:" + currentDirectionStickVoltage + " Forward Voltage:" + currentUpStickVoltage);
         //   Debug.Print("Direction Speed:" + preferred_direction + " Straight Speed:" + preferred_straight_speed);
        }
        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()
        //This continuously loops to adjust the output pulse frequencies of the pins per the current variable settings.
        {   //First, start pulsing the stop position value.
            
            PWM_TurnMovementToPin10.Start();
            PWM_StraightMovementToPin11.Start();
            PWM_DomeMovementToPin9.Start();
            
            while (true)
            {
                //Pulse Dome
                if (System.Math.Abs(preferred_dome - current_dome) > 3)
                {
                    PWM_DomeMovementToPin9.Duration=((uint)(1500 + (5 * preferred_dome)));
                    current_dome = preferred_dome;
                }
                if (preferred_straight_speed == 0) current_straight_speed = (int)((float)current_straight_speed/(2.2f*2));//slow it down mildly
                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 (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(100);
            }             
        }
        static void performAutonomousDomeMovement()
        {
            int the_multiplier = 1;
            while (true)
            {
                if (!DOME_CONTROL_ONLY||IDLE)
                {
                    the_multiplier = 1;
                    preferred_dome = the_multiplier * (70 - random_number.Next(80)); Thread.Sleep(300 + random_number.Next(1000));
                    preferred_dome = -5; Thread.Sleep(1000 + random_number.Next(3000));
                    //look forward.
                    the_multiplier = -1;
                    preferred_dome = the_multiplier * (70 - random_number.Next(80));
                    DateTime the_time = DateTime.Now.AddSeconds(6);
                    while (!DOME_FORWARD && (DateTime.Compare(the_time, DateTime.Now) > 0))
                    {
                        Thread.Sleep(50);
                    }
                    Thread.Sleep(random_number.Next(500));
                    preferred_dome = -5;
                    Thread.Sleep(1000+random_number.Next(500));
                    //randomly spin in a fast circle
                    if (random_number.Next(3)==2) {
                        preferred_dome=(random_number.Next(3)-1)*100;//randomize the direction
                        if (preferred_dome!=0) Thread.Sleep(1000+random_number.Next(3000));
                        preferred_dome = -5;
                    }
                }
                
                Thread.Sleep(3000 + random_number.Next(3000));
            }
        }
        static void waitForRemoteControlOn()
        {   //don't let him start moving until the controller is set in manual control after power on
            stopRobot();
            //this forces you to pull the stick back at least once before letting him take off.
            while (!inputPort7_JoystickClucka.Read())
            {
                PlayTrack(28, INTERRUPT_TRACK);
              //  Debug.Print("Waiting for remote:");
                Thread.Sleep(150);
            }
            stopRobot();
         //   PlayTrack(sound_scream, INTERRUPT_TRACK);
       //     Debug.Print("Awoke");
            WAITING_FOR_GOOD_SIGNAL = false;
            DOME_CONTROL_ONLY = false; IDLE = false;
        }
        static void performMovements()
        {
            waitForRemoteControlOn();//this will ensure he stays in the stopped position until all readings from the RC remote are stable in their idle positions.            
            current_straight_speed = 0; preferred_direction = 0;//to prevent a kick and buck on startup.
            autonomousDomeThread.Start();
            while (true)
            {
                //check to make sure the RC controller is still on; otherwise, failsafe to the stop position.
                if (!DOME_CONTROL_ONLY)
                {   //The user will control the foot drives and the dome will move autonomously.
                    calculateRobotStickPreferredDirection();
                    calculateRobotStickPreferredSpeed();
                }
                else
                {   //The user will just move the dome.
                   // stopRobot();
                    //if (!IDLE) calculateDomeStickPreferredSpeedAndDirection();
                }
                Thread.Sleep(250);
            }
        }
        static private void stopRobot()
        {
            preferred_direction = 0;
            preferred_straight_speed = 0;
        }
        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);
        }
    }       
}

 

 

Taking the virtual wire approach allows us to put a microcontroller between us and the foot motors.  In turn, we can always be sensing the environment to determine if the droid will even allow motion or not.  The approach I took was to have program threads running similar to the involuntary systems in the human body.  They are named movementThread, soundThread, distanceThread, pulseThread, autonomousDomeThread.

 

movementThread - this looks at all factors including the user's thumb stick operation, but also considers proximity sensors.  It's end function is to assess if the droid should move or not, how fast, and what direction - and then either do what the user wanted, limit the speed, or let out a scream and stop entirely.

soundThread - this looks at all factors to determine what sound it should play - a yell, a beep, a sound, or whaterver.  It takes input from variables set by the other threads to determine what to play.

distanceThread - this thread is continuously using a proximity sensor to determine if it should interlock movement should someone be two close - regardless of what the person controlling it wants.

pulseThread - this is translating the analog and digital inputs from the XBee to meaningful variable updates so the other threads can assess what they should be doing.

automomousDomeThread - this is rotating the dome based on the variables set by the rest of the threads to give him a truly autonomous dome movement system

 

You can see where the XBee pins come into play with a skim for XBee in the code.  It's really quite simple in that you just treat the XBee pins as you would a wire connected to your GPIO.

 

PROJECT DEMO

In our project demo, Connor takes you through the features of our XBee based control system.  He demonstrates a toggle button and analog signals from the joystick.  He also shows the receiver shield mounted inside R2's belly.

 

R2D2 Incognito Controller Demo

 

PROJECT SUMMARY

This was a great RF application to establish a solution for effective long distance virtual wires for our microcontroller projects.  This includes signaling analog, digital, and even PWM with seemingly no latency.  With it, we met our goal of making an incognito controller for R2D2 that fits even in Connor's skinny jeans.  Although R2 does have a fully autonomous moving dome and panels - his foot motors, sound volume, and the ability to queue a song is controlled by a us.  People meeting him don't have to know that - which is what made the small form factor of the XBee a great RF controller solution.  So, when the grown ups ask, "What's making him move?!"  I reply, "Well...the Force, of course."

 

We hope this gives you an option for your future builds where you need virtual wires with no latency.  It's like having the Force in your hands!

 

Happy New Year!

-Sean and Connor

 

May the Force be with You!

REFERENCES

  1. Astromech.net :: R2 Builders Club Official Website
  2. Ron Hone Picture from Astromech.net Behind the Scenes, Bio on imdb.com
  3. Digi International:  Manufacturer of XBee modules - https://www.digi.com/
  4. IEEE 802.15.4 Standard - https://standards.ieee.org/standard/802_15_4v-2017.html
  5. XBee 802.15.4 Product Manual
  6. CoolTerm Software Application
  7. XBEE AT Command Reference
  8. Digi Knowledge Base:  PWM to Analog
  9. Sean Miller's R2D2 GitHub Repository: https://github.com/RaisingAwesome/R2D2
  10. Sean Miller's R2D2 Build Log:  https://astromech.net/forums/showthread.php?6676-Sean-s-R2D2C
  11. Sean Miller's R2D2 Build Videos:  https://www.youtube.com/user/sjmill01
  12. The Berkley Independent Newspaper:  DuPont Employees Take Their Kids to Work