Wi-Fi Warthogs

If you were at PDC09, you might have played “WI-FI Warthogs”, a computer robotics Laser Tag Game that uses the Xbox 360 controller to remotely control Power Wheels vehicles. In this article, I'll tell you how to build your very own WI-FI Warthogs game, from start to finish.

Getting Started

I'll start with the Hardware and then move on to the main application and engines that run the robotics.

To get started, all you need are the parts and software listed above, and some basic DIY skills. I found my parts at many different places, one of the best of which was Trossen Robotics (which also has a great forum site). I found automotive relays and wiring harnesses at All Electronics website, cheap Power Wheels ($15) at garage sales, and additional parts at the local junk yard.

If your Power Wheels car is in need of repairs, check out the Modified Power Wheels website for more info.

The video below shows pictures I started taking on the first day—they continue all the way through the prototyping life cycle.

Hardware

Modifying the Cars

To modify the car, I removed all the existing controls from my Power Wheels Barbie Jeeps and wired their rear motors together. (Keep in mind that in order for the car to go forward or in reverse, the wires on ONE of the motors has to be reversed. Otherwise, when you connect the two red wires together and the two blue ones together and give it power, one wheel will spin forward while the other one spins backwards.)

clip_image004

Next, I attached the motors to a set of automotive relays, forming a DPDT switch. This was in turn connected to a Phidgets four relay board. Two of the Phidgets relays will control the transmission.

For the steering, I used the tracks from an electric car seat to move the tires from right to left. Any 12 volt electric car seat should work—you can get them at an auto salvage yard, usually at a reasonable price. You could also use an old cordless drill, as seen here.

Note that you'll have to modify your car for the steering unit. To do this, I fitted a 2x4 into the front section of the car. On the board, I screwed in the seat track, giving it something solid to sit on.

The wiring for the steering motor was the same as the transmission, except I needed to add limiting switches to keep the tires from going too far left or right. I tied these to the automotive relays. Then, I wired the relays for steering to the Phidgets 4 relay board. (See wiring diagram. Or, for more on automotive relay, see here.)

clip_image006

I had two different versions of the jeeps and my process was a little different each time, so you might have a little trial and error to find the right fit for the car.

The plastic and drywall screws work great to hold everything in place but you might have a few extra screw holes when you're finished. I call that “character”.

Also, relays and motors can cause EMI noise. For ways to control it, read this.

clip_image008

You can now drive the car forward, reverse, left and right. To test this, try running the Phidgets test program or use my code. For a Phidgets test, right click on the Phidgets item in the tray on your computer. Then, select the 004 device and right click to bring up a screen that lets you manually control the device.

Remember to NEVER turn on both relays to transmission or steering at same time, or you will blow your motors, Phidgets and possibly the USB port to your computer. You can turn on one from steering and one from transmission—just turn them off before you switch to the other relays. My program does all of this for you. (The Phidgets driver also does all this for you, but beware: it gives you total control, so you can turn all relays on at once. As mentioned, this would cause bad things to happen.)

Adding a Gun Turret

To control the gun, we'll use three more Phidgets devices: the 888 interface IO board, the voltage board, and dual relay daughter boards. The voltage board will tell the 888 board when it sees a voltage spike, which happens when one gun hits another. The dual relay board acts like a simple switch; it fires the gun and allows it to reload ammo.

gunwire[1]

To aim the gun and move it from right to left, we'll use the Phidgets servo controller. As a cheaper option, you could replace the laser tag guns with just about anything else. (In a prior version, for example, I used only a relay and was able to shoot water from a windshield washer pump by using the relay as a switch and turning the power to the pump on and off.) We now have our hardware in place.

The gun is mounted in a holster that can be turned by the servo. I used the “lazy Susan” concept, the same as what most people have in their houses. I made a wooden box that the gun fits in and mounted it on a ball bearing swivel. The servo is mounted directly below it, and everything is control from the Xbox controller (you can buy them from Sparkfun.com). It's now time to control the car with the computer.

clip_image012

Computers

I used Asus EeePCs models 904 and 1000HE, but any computer with XP or newer installed on it will work. You can pick up a used Netbook (a great and cheap option for robotics) from EBay for little to nothing—I recently bought one that had a 32GB SSD and the 1.6 GHz Atom processor for $250. If possible, get one with a shock-resistant SSD drive. The ones we used at PDC all had standard hard drives and worked fine, but you have to watch for hard hits, which can damage the hard drives.

Controller Application

First, I have to give credit Joel Ivory Johnson, whose article and sample will make understanding the controller code much easier. His sample is also great for testing functionality and making sure the drivers are working properly for the Xbox 360 controller.

The main controller app is a simple program: an action happens (a button pushed, joystick moved, etc). Then, a call is made to the proper method. Finally, the method fires off a message to its queue and goes back to waiting for the next action (fire and forget). I created two methods: “Drivecar” and “GunControl”. The names pretty much tell you what's going to happen.

In addition to the two main functions, there are two timer events. One controls the main loop; the other allows feedback to be given back to the main program. The feedback can come from any of the engines, or from the scoreboard. I use the feedback timer to supply the players' vibrations to their controllers when they get hit. I also use it for beginning and ending a game.

I used Microsoft message queue to send messages from the application to the different engines.

So to summarize: I have feedback, 888 IO interface, servo, and relay interface queues that are installed on each Netbook. The queues operate on their own; we'll discuss them more below.

I have 3 cars and 3 computers (you could use more or less). I can bring on more cars or switch the computers around to different cars. I do this by using the DNS name of the computer and syncing it up with a configuration file. The engines will pull all their configuration data in from this file. Below is an example of one of the nodes, along with the code I use to read the node:

<Car>    
    <carnumber>TimCar2</carnumber>
    <relaycar>FormatName:DIRECT=OS:timcar2\private$\relaycar</relaycar>
    <servocar>FormatName:DIRECT=OS:timcar2\private$\servocar</servocar>
    <interface888>FormatName:DIRECT=OS:timcar2\private$\interface888</interface888>
    <feedback>FormatName:DIRECT=OS:timcar2\private$\feedback</feedback>
    <ok2reload>Y</ok2reload>
    <relayserialnum>8694</relayserialnum>
    <serialnum888>30448</serialnum888>
    <servoserialnum>88630</servoserialnum>
    <scorecard>FormatName:DIRECT=OS:timcar3\private$\scorecard</scorecard>
    <scorecardnolimit>False</scorecardnolimit>
    <scorecardnolimittime>1800</scorecardnolimittime>
</Car>

Each node (Car) has a “Carnumber” that's equal to the DNS name of the computer. The other information you see is the location of the private queues, the serial numbers of the different Phidgets devices, and some additional configuration data on how the game operates. The file is in XML format and the values are read in with the following code:

ProjectUtils.Utilities x = new ProjectUtils.Utilities();

whatcar = System.Net.Dns.GetHostName();
scorecard = x.ReadXMLFile4CarElementValue("c:\\deploy\\carconfig.xml", whatcar, "scorecard");
relaycar1 = x.ReadXMLFile4CarElementValue("c:\\deploy\\carconfig.xml", whatcar, "relaycar");
servocar1 = x.ReadXMLFile4CarElementValue("c:\\deploy\\carconfig.xml", whatcar, "servocar");
interface888car1 = x.ReadXMLFile4CarElementValue("c:\\deploy\\carconfig.xml", whatcar, "interface888");
feedbackque1 = x.ReadXMLFile4CarElementValue("c:\\deploy\\carconfig.xml", whatcar, "feedback");

Another function of the controller program is to determine how many players (1 or 2) are attached to this receiver. If there are two players, player 1 will be the driver and player 2 will be the gunner. If there is only one player, he will need to press the “A” button to switch back and forth between the different modes of play, since it's not safe to drive and shoot at the same time.

if (this.gamePadState1.IsConnected == true)
{
    player1active = true;
    numberOfPlayers++;

    //what if only one player let them choose if they want to be in driver or
    // gunner mode
    if (this.gamePadState2.IsConnected == false)
    {
        if (!this.gamePadState1.Buttons.Equals(this.previousState1.Buttons))
        {
            if (this.gamePadState1.Buttons.A == Input.ButtonState.Pressed)
            {
                if (player1Driver == true)
                {
                    player1Driver = false;
                    lbT1Player1.Text = "Gunner";
                }
                else
                {
                    player1Driver = true;
                    lbT1Player1.Text = "Driver";
                }
            }

        }
    }

}
else
{
    lbT1Player1.Text = "Not Connected";
    player1active = false;
}

if (player1active == true)
{
    if (player1Driver == true)
    {
        lbT1Player1.Text = "Driver";
        DriveCar(gamePadState1, previousState1, 1);
        PlayRadio(gamePadState1, previousState1, 1);
    }
    else
    {
        lbT1Player1.Text = "Gunner";
        GunControl(gamePadState1, previousState1, 2);
    }
}

if (this.gamePadState2.IsConnected == true)
{
    player2active = true;
    numberOfPlayers++;
    GunControl(gamePadState2, previousState2, 2);
    lbT1Player2.Text = "Connected";
}
else
{
    lbT1Player2.Text = "Not Connected";
    player2active = false;
}

In XNA programming for the Xbox controller, there are previous and current states. For the most part I care only about the current state of the controller, but I do use the previous state to see if the button or joystick has moved between the last loop and the current loop. The loops happen every 100 milliseconds for the main loop (10 times a second, 1000 milliseconds = 1 second).

Below is the code for the “DriveCar” function. As you can see, it's pretty straightforward—I simply pass in the previous and current states of the game controller. For steering, I interrogate the state of the right joystick. If it's a different value than the last time we were here, I send a message to the “InterfaceIO” (004 Phidgets interface board) message queue, passing the current state value of the joystick. The drive engine will take the value and determine the action. The transmission is handled the same way, except that we check the left joystick.

private void DriveCar(GamePadState gameController, GamePadState prevState, int player)
{
    //either player 1 or 2 will send their current GamePadState
    int xStickCurrent;
    int xStickPrev;
    int yStickCurrent;
    int yStickPrev;
    string steerPos = "";

    if (allstop == false)
    {
        //Steering
        xStickCurrent = (int)((gameController.ThumbSticks.Right.X + 1.0f) * 100.0f / 2.0f);
        xStickPrev = (int)((prevState.ThumbSticks.Right.X + 1.0f) * 100.0f / 2.0f);

        steerPos = xStickCurrent.ToString();

        if (xStickCurrent != xStickPrev)
        {
            queueCount++;
            //6 = servo number , steerPos = joystick position
            listBox1.Items.Add("steering" + steerPos);
            qM.SendMsgNoTransaction(3, "3" + steerPos, relaycar1);
            xStickPrev = xStickCurrent;
        }

        //Transmission
        //0-49(backward) 51- 100(forward) 50 = Neutral

        yStickCurrent = (int)((gameController.ThumbSticks.Left.Y + 1.0f) * 100.0f / 2.0f);
        yStickPrev = (int)((prevState.ThumbSticks.Left.Y + 1.0f) * 100.0f / 2.0f);

        if (yStickCurrent != yStickPrev)
        {
            queueCount++;
            listBox1.Items.Add("transmission " + yStickCurrent.ToString());
            qM.SendMsgNoTransaction(3, "4" + yStickCurrent.ToString(), relaycar1);
            yStickPrev = yStickCurrent;
        }
    }
    else
    {
        //we are in all stop do not allow any sends to car...
        //if we have not sent all stop to relay engine do it..
        if (sentallstop == false)
        {
            sentallstop = true;
            //tell transmission to go to neutral
            qM.SendMsgNoTransaction(3, "450", relaycar1);
        }
    }
}

Each of the queue engines (feedback, drivetrain, 888interface) is set up pretty much the same way. The “Main” function sets up the Phidgets device it will control. It then goes into an endless loop until you cancel the program. In the loop, it checks the Message queue for any new messages and calls the proper function based on the content of the message. Then, it gives the processor back to the computer via the do events call while it waits for the next message.

Below is the “Main” function from the drivetrain queue engine. It sets itself up based on the configuration file and creates the events that the Phidgets devices require. The code then enters its loop and waits for messages to process.

static void Main(string[] args)
{
    whatcar = System.Net.Dns.GetHostName();
    relaycar = projectUtils.ReadXMLFile4CarElementValue("c:\\deploy\\carconfig.xml", whatcar, "relaycar");
    ioboardserialnum = projectUtils.ReadXMLFile4CarElementValue("c:\\deploy\\carconfig.xml", whatcar, "relayserialnum");

    //write to the console what is happening, useful for debugging
    Console.WriteLine("Configured Car number - " + whatcar + "\r\n");
    Console.WriteLine("Configured Relay serial number - " + ioboardserialnum + "\r\n");

    //Wiring up the Phidgets devices with their events
    ifKit = new InterfaceKit();

    ifKit.Attach += new AttachEventHandler(ifKit_Attach);
    ifKit.Detach += new DetachEventHandler(ifKit_Detach);
    ifKit.Error += new ErrorEventHandler(ifKit_Error);

    ifKit.InputChange += new InputChangeEventHandler(ifKit_InputChange);
    ifKit.OutputChange += new OutputChangeEventHandler(ifKit_OutputChange);
    ifKit.SensorChange += new SensorChangeEventHandler(ifKit_SensorChange);

    ifKit.open(int.Parse(ioboardserialnum));

    while (true)
    {
        try
        {
            // get the message from the message queue 
            o = qM.PullMsg(relaycar);

            //body will have for servo 1 to 7 rest of string is position to move too.
            if (o != null && o.QueMsgBody != "")
            {
                mycount++;
                Console.WriteLine("process body and count " + o.QueMsgBody + "  " + mycount.ToString() + "\r\n");
                inputDeviceNum = Int32.Parse(o.QueMsgBody.Substring(0, 1));
                msgMainBody = int.Parse(o.QueMsgBody.Substring(1));

                switch (inputDeviceNum)
                {
                    case 3:
                        steering(msgMainBody);
                        break;
                    case 4:
                        transmission(msgMainBody);
                        break;
                }
            }
            else
            {
                Console.WriteLine("null or 0 length queue message" + "\r\n");
            }

            Application.DoEvents();
            Thread.Sleep(100);
        }
        catch (Exception ex)
        {
            projectUtils.WriteEventToApplicationLog("PhidgetsIO", "Main -> " + ex.ToString());
        }
    }
}

This snippet of code is the “transmission” function, which allows the car to move forward, in reverse or stop. This is where I determine what action I need to do based on the joystick's position.

private static void transmission(int myPos)
{
    try
    {
        if (myPos < 20)
        {
            myMsg = "Moving transmission to => Reverse";
            currentSteerPos = "R";
        }
        else if (myPos > 80)
        {
            myMsg = "Moving transmission in Forward";
            currentSteerPos = "F";
        }
        // giving most range from the joystick to stop the car, being safe
        else
        {
            myMsg = "Moving Transmission to =>  Neutral";
            currentSteerPos = "N";
        }

        Console.WriteLine(myMsg + "\r\n");
        //first stop motor, then wait  and finally do new command.
       // we stop only long enough for the electricity to stop energising the relay
        //since we are turning both relays to off (false) that is the same as Neutral so we
        //do not need to check for it.

        if (ifKit.Attached == true)
        {
            //set relay 3 to not be engerized, relays start with 0 base count
            ifKit.outputs[2] = false;
            Thread.Sleep(20);
        }

        if (ifKit.Attached == true)
        {
            //set relay 3 to not be engerized, relays start with 0 base count
            ifKit.outputs[3] = false;
            Thread.Sleep(20);
        }

        if (currentSteerPos == "F")
        {
            if (ifKit.Attached == true)
            {
                // turn on relays to power motors forward
                ifKit.outputs[2] = true;
            }
        }
        else if (currentSteerPos == "R")
        {
            // turn on relays to power motors reverse
            if (ifKit.Attached == true)
            {
                ifKit.outputs[3] = true;
            }
        }
    }
    catch (Exception ex)
    {
        projectUtils.WriteEventToApplicationLog("PhidgesIO", "Transmission -> " + ex.ToString());
    }
}

Notes

First, remember to always turn off relays before changing direction, so you never have both relays running at the same time.

Also, there is more code in the “main” function (instead of in a timer routine) because of performance issues. When I created a timer and added all the code to the event, the processor showed like it was redlined at 100%. When I put the “do events” in the timer event everything worked fine, but it still looked like the process was redlined. However, when I moved everything into the “Main “ function and added a “do events”, everything worked fine and the processor dropped to about 8% usage—even with all of the engines running and the main program processing the Xbox controller commands.

Conclusion

This was a fun project that gave me a better understanding of robotics and C# functionality. Like I said in the beginning, there are many things you could do to make it better and even faster—I simply gave you a starting point. Questions to ponder: Why console apps and not Windows services? Why MSMQ and not WFC?

If you have any questions or thoughts, feel free to ask them here or over at my blog.

About The Author

Tim is an IT Director in the Enterprise Architect Group for Maritz, a sales and marketing services company. He has developed many systems in numerous languages over the years and is currently working with C#. In his off time he enjoys coming up with off the wall ideas and then seeing if he can make them happen. Tim's blog is finally getting started and can be seen at Waterhobo.com where he tries to explain them; he can be emailed at tbh726@gmail.com.

Tags:

Follow the Discussion

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.