Опубликовано 18 января 2010 г. 17:55 | Coding4Fun

Если вы побывали на PDC09, то, возможно, вам удалось поиграть в «WI-FI Warthogs» (джипы-монстры с Wi-Fi) — игру с роботами, на которые установлены лазерные метки. В качестве роботов используются машинки Power Wheels, дистанционно управляемые компьютерами с подключенными контроллерами Xbox 360. В этой статье я расскажу, как создать собственную игру в стиле «WI-FI Warthogs» — от начала и до конца.

clip_image001Автор: Тим Хиггинс (Tim Higgins)
Исходный код: загрузить
Сложность: средняя
Необходимое время: от 50 до 100 часов
Затраты: более 350 долларов США
Оборудование: одна машинка с электродвигателем (Power Wheels), нетбук или лэптоп, комплектующие от Phidgets (Phidgets devices), контроллер Xbox 360 с подключением к ПК
ПО: пример использования XNA для доступа к контроллеру 360, библиотеки Phidgets, Visual C# Express

Приступаем

Начнем с оборудования, а уж потом перейдем к основному приложению и механизмам, управляющим роботами.

Для начала хватит комплектующих и программного обеспечения, перечисленных выше; ну и, конечно, желательны «прямые» руки. Необходимые комплектующие я нашел в самых разных местах, и одно из лучших — Trossen Robotics (на сайте этого интернет-магазина вдобавок есть отличный форум). Реле для автомобильных устройств и электропроводку я обнаружил на сайте All Electronics, дешевые машинки Power Wheels (по 15 долларов США) — на распродажах во дворах возле гаражей, а дополнительные детали — на местной свалке автомашин.

Если ваша машинка Power Wheels нуждается в ремонте, зайдите на сайт Modified Power Wheels — там есть полезная информация.

Ниже представлен видеоролик, демонстрирующий снимки, которые я начал делать с первого дня всего цикла изготовления опытной модели.

Оборудование

Модификация машинок

Чтобы модифицировать машинку Power Wheels, я удалил из своей версии Power Wheels для Барби всю управляющую начинку и соединил проводами ее задние моторы. (Учтите: чтобы машинка могла ездить вперед и назад, полярность проводов на одном из моторов надо менять на обратную. Иначе, если вы соедините вместе два красных проводка и два синих попарно, одно колесо будет крутиться в направлении вперед, а другое — в обратном.)

clip_image002

Затем я подключил моторы к набору автомобильных реле, создав тем самым двухполюсный перекидной переключатель (DPDT switch). Эту штуковину я в свою очередь соединил с платой от Phidgets, на которой установлены четыре реле. Два из них будут управлять трансмиссией.

Для рулевого управления я использовал направляющие устройства (tracks) от автомобильного сиденья, регулируемого электрическими сервомоторами (electric car seat), чтобы двигать колеса справа налево. Годится любое сиденье с 12-вольтовыми моторами — по разумной цене их обычно можно раздобыть на площадке разделки битых автомашин. Также можно было бы использовать старую аккумуляторную дрель, как показано здесь.

Заметьте, что вам придется переделать свою машинку для установки блока управления. Для этого я вставил в переднюю секцию машинки лоток размером 2 × 4 дюйма. Саму плату я привинтил к направляющему устройству от сиденья, что позволило более-менее жестко закрепить ее.

Монтажная схема направляющего мотора была такой же, как и для трансмиссии, — с тем исключением, что мне нужно было добавить переключатели-ограничители, чтобы колеса не поворачивались на слишком большой угол влево или вправо. Я соединил их с автомобильными реле, а эти реле с четырехрелейной платой Phidgets. (Детали см. на монтажной схеме, а более подробные сведения об автомобильных реле см. здесь.)

clip_image003

У меня было два джипа разных версий, и в каждом случае весь процесс несколько отличался, так что вам, возможно, придется действовать методом проб и ошибок, чтобы правильно оборудовать свою машинку.

Пластмассовые винты с шайбами и саморезы отлично подойдут для крепления любых деталей.

Также помните, что реле и электромоторы могут вызывать электромагнитные помехи. Как справиться с ними, читайте здесь.

clip_image005

Надписи на схеме:

Constant 12V+ — Постоянный ток +12 В
Ground — Земля
12V Lock Input — Точка подачи запирающего напряжения 12 В
Transmission — Трансмиссия
12V Unlock Input — Точка подачи отпирающего напряжения 12 В
Positive — Плюс
Limit switches — Ограничивающие переключатели
Steering — Управление направлением
Battery connect so «nc» terminal for «normally closed» — Аккумулятор подключается так, чтобы оконечное устройство, соединенное с разъемом «nc», было в состоянии «нормально замкнуто»
Relays are connected to «no» for «normally open» — Реле подключаются к разъему «no», чтобы оконечное устройство было в состояние «нормально разомкнуто»
Phidgets 004 interface board — Интерфейсная плата Phidgets 004

Теперь вы можете управлять машинкой, чтобы она ездила вперед, назад, влево и вправо. Для проверки попробуйте запустить тестовую программу Phidgets или воспользоваться моим кодом. Для запуска теста Phidgets щелкните правой кнопкой мыши индикатор Phidgets в системном лотке на панели задач своего компьютера. Потом выберите устройство 004 и щелкните правой кнопкой мыши — появится экран, с помощью которого вы сможете вручную управлять устройством.

Никогда не включайте оба реле для трансмиссии или рулевого управления одновременно — иначе вы спалите свои моторчики, устройство Phidgets и, возможно, USB-порт в компьютере. Включать одновременно можно только по одному ре��е для трансмиссии и рулевого управления — просто выключайте их перед переключением на другие реле. Все это моя программа делает сама. (Драйвер Phidgets делает то же самое, но будьте осторожны: он предоставляет полный контроль, поэтому вы можете нечаянно включить сразу все реле. А это, как я уже говорил, может привести к крупным неприятностям.)

Добавление турели

Для управления оружием мы задействуем еще три устройства Phidgets: интерфейсную плату ввода-вывода 888, плату измерения напряжений и двухрелейные дочерние платы. Плата измерения напряжений будет сообщать устройству 888 об обнаруженных пиках напряжения, которые возникают, когда одна пушка попадает в другую. Двухрелейная плата действует как простой переключатель: выстрел из оружия и его перезарядка.

clip_image006

Для прицеливания и поворота оружия справа налево мы воспользуемся контроллером сервомоторов Phidgets (Phidgets servo controller). Как более дешевый вариант вы могли бы заменить оружие с лазерными метками чем-то другим. (Например, в предыдущей версии я использовал только реле и мог стрелять водой из насоса стеклоочистителя лобового стекла, применяя реле как переключатель и подавая или отключая электропитание этого насоса.) Теперь все детали у нас на месте.

Оружие монтируется на станине, поворачиваемой сервомотором. Я воспользовался так называемой «ленивой Сьюзен» («lazy Susan») — такие штуки есть в домах у многих. Я изготовил деревянную коробку, в которую помещается оружие, и смонтировал ее на шаровом шарнире. Сервомотор размещается прямо под ним, и все управляется с помощью контроллера Xbox (его можно купить в Sparkfun.com). А теперь пора заняться управлением машинкой с компьютера.

clip_image007

Компьютеры

Я использовал компьютеры Asus Eee PC моделей 904 и 1000HE, но подойдут любые компьютеры с XP или более поздней версией операционной системы. Можно взять подержанный нетбук (отличный и очень дешевый вариант для роботостроения) на EBay практически за бесценок — недавно всего за 250 баксов я купил один такой с 32 Гб SSD и процессором Atom на 1,6 ГГц. По возможности берите компьютер с ударостойким SSD-диском. Все компьютеры, которые мы использовали на конференции PDC, были оборудованы стандартными жесткими дисками и отлично работали, но нужно было избегать сильных ударов, чтобы не повредить жесткие диски.

Приложение контроллера

Сначала я хочу отдать должное Джоэлу Айвори Джонсону (Joel Ivory Johnson), чья статья и пример намного упростят понимание кода контроллера. Его пример также великолепно подходит для тестирования функциональности и проверки нормальной работы драйверов для контроллера Xbox 360.

Основное приложение контроллера — программа простая: когда выполняется какое-то действие (нажимается кнопка, перемещается джойстик и т. д.), вызывается соответствующий метод, а потом этот метод создает сообщение в своей очереди и возвращается к ожиданию следующего действия (по принципу «выстрелил и забыл»). Я создал два метода: Drivecar (Вести машину) и GunControl (Управление пушкой). Из имен этих методов совершенно понятно, что именно они делают.

В дополнение к этим двум основным функциям имеются два таймера. Один управляет основным циклом, а другой обеспечивает обратную связь с основной программой. Соответствующие данные могут поступать от любого механизма или от табло. Я использовал таймер обратной связи для передачи вибраций на контроллеры игроков при попадании в них. Кроме того, я использовал его для начала и окончания игры.

Сообщения от приложения разным механизмам я посылал с помощью Microsoft Message Queue.

Подведем итог: у меня есть очереди обратной связи, интерфейса ввода-вывода (плата 888), управления сервомоторами и релейного интерфейса, устанавливаемые на каждом нетбуке. Очереди работают самостоятельно, детали мы обсудим позже.

У меня было три машинки и три компьютера (можно использовать больше или меньше). Я могу управлять большим количеством машинок или переключать компьютеры на разные машинки. Для этого я использую DNS-имя компьютера и синхронизирую его с конфигурационным файлом. Механизмы извлекают все свои конфигурационные данные из этого файла. Ниже приведен пример одного из узлов вместе с кодом, позволяющим считывать этот узел:

<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>

В каждом узле (Car) есть элемент Carnumber, который содержит DNS-имя компьютера. К прочей информации относятся местонахождение закрытых очередей, серийные номера различных устройств Phidgets и некоторые дополнительные конфигурационные данные, управляющие тем, как работает игра. Этот файл имеет формат XML, и значения считываются из него следующим кодом:

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");

Другая функция программы контроллера — определять, сколько игроков (один или два) подключено к этому приемнику. Если игроков двое, то игрок под номером 1 будет водителем, а под номером 2 — стрелком. Если игрок только один, ему нужно будет нажимать кнопку «A» для переключения между разными режимами игры, так как водить машинку и одновременно стрелять небезопасно.

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;
}

Программирование с помощью XNA для контроллера Xbox требует оперировать предыдущим и текущим состояниями. По большей части я заботился только о текущем состоянии контроллера, но все же использовал предыдущее состояние, чтобы проверять, была ли нажата кнопка или перемещался ли джойстик между прошлой и текущей итерацией цикла. Итерации осуществляются каждые 100 миллисекунд (мс) для основного цикла (10 раз в секунду, 1000 мс = 1 секунда).

Ниже приведен код функции DriveCar. Как видите, он достаточно прямолинеен — я просто передаю предыдущее и текущее состояния игрового контроллера. Для рулевого управления я опрашиваю состояние правого джойстика. Если полученное значение отличается от предыдущего, я посылаю сообщение в очередь InterfaceIO (интерфейсная плата Phidgets 004), передавая текущее значение состояния джойстика. Механизм привода получит это значение и определит действие. Трансмиссия обрабатывается аналогично, но проверяется левый джойстик.

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);
        }
    }
}

Каждая из очередей (обратной связи, интерфейса 888 и др.) настраиваются во многом похожим способом. Функция Main настраивает устройство Phidgets, которым она управляет. Затем она входит в бесконечный цикл до отмены программы. В этом цикле проверяется очередь Message на наличие любых новых сообщений и на основе их содержимого вызываются соответствующие функции. Далее она возвращает управление процессору компьютера и ожидает появления следующего сообщения.

Вот как выглядит функция Main для очереди drivetrain. Она настраивается на основе конфигурационного файла и создает события, необходимые устройствам Phidgets. Затем код входит в свой цикл и ждет сообщения для обработки.

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());
        }
    }
}

Этот фрагмент кода является функцией transmission, которая позволяет машинке двигаться вперед, назад или останавливаться. Именно здесь на основе позиции джойстика определяется, какое действие нужно выполнить.

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());
    }
}
 

Примечания

Во-первых, никогда не забывайте отключать реле перед сменой направления движения, чтобы оба реле не были включены одновременно.

Кроме того, в функции Main на самом деле больше кода, чем показано; этот код необходим по соображениям производительности. Когда я создавал таймер и добавлял весь код в обработчик его события, нагрузка на процессор составляла почти 100%. Почти то же самое происходило и при использовании do events в обработчике события таймера. Однако, когда я переместил все в функцию Main и добавил do events, все нормализовалось и загруженность процессора упала до 8% — даже при работе всех механизмов и обработке основной программой команд контроллера Xbox.

Заключение

Это был интересный проект, который позволил мне гораздо лучше понять основы робототехники и функциональность C#. Как я уже говорил в самом начале, есть масса вещей, которые можно делать лучше и быстрее, — я просто даю вам отправную точку. Вопросы для размышления: почему консольное приложение, а не службы Windows? Почему MSMQ, а не WFC?

Если у вас есть вопросы или какие-то мысли, сразу же пишите мне здесь или в моем блоге.

Об авторе

Тим является директором по информационным технологиям в Enterprise Architect Group из Maritz — компании, которая предоставляет услуги в области продаж и маркетинга. За годы работы создал множество систем на самых разных языках, в настоящее время использует C#. В свободное время любит высказывать весьма нестандартные идеи, а потом смотреть, можно ли их реализовать. Тим наконец-то запустил свой блог на Waterhobo.com, где он пытается объяснять свои идеи. Вы можете связаться с ним по электронной почте tbh726@gmail.com.