В новостях рассказывают о дронах — беспилотных летательных аппаратах — буквально каждый день. Области применения у них самые разные: разведка и боевые операции, фото- и видеосъемка, да и просто развлечения. Технология дронов достаточно новая и заслуживает интереса.

Разработчики могут создавать приложения для управления дронами. Дрон в конечном итоге является обычным программируемым устройством, поэтому к нему можно подключаться и отдавать команды для выполнения нужных действий с помощью обычных приложений для ПК и смартфонов. Для этой статьи я выбрал один из дронов с самыми мощными возможностями программирования — AR.Drone 2.0 компании Parrot.

Мы узнаем, как взаимодействовать с таким дроном и управлять им с помощью библиотеки, написанной на C#. Опираясь на эту основу, мы добавим речевые команды для управления дроном с помощью Intel RealSense SDK.

Модель AR.Drone 2.0 компании Parrot — один из наиболее интересных дронов, предлагаемых на рынке для энтузиастов. Этот дрон обладает множеством функций и включает встроенную систему помощи с интерфейсами стабилизации и калибровки. Дрон оснащен защитным каркасом из прочного пенополистирола, предохраняющим лопасти винтов и движущиеся части в случае падения или столкновения с неподвижными препятствиями.


AR.Drone* 2.0 компании Parrot

Оборудование дрона обеспечивает его подключение по собственной сети Wi-Fi* к внешним устройствам (смартфонам, планшетам, ПК). Протокол связи основан на АТ-подобных сообщениях (подобные команды несколько лет назад использовались для программирования модемов для связи по телефонной сети).

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

Программное управление дроном


У дрона есть собственная сеть Wi-Fi, поэтому подключимся к ней для передачи команд управления. Всю нужную информацию мы нашли в руководстве для разработчиков AR.Drone 2.0. Например, в руководстве сказано, что нужно отправлять команды по протоколу UDP на IP-адрес 192.168.1.1, порт 5556. Это простые строки в формате AT:

  • AT * REF — управление взлетом и посадкой;
  • AT * PCMD — движение дрона (направление, скорость, высота).

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

Сначала нужно подключиться к устройству.

public static async Task ConnectAsync(string hostName = HOST_NAME, string port = REMOTE_PORT)
        {
             // Set up the UDP connection.
             var droneIP = new HostName(hostName);

             udpSocket = new DatagramSocket();
             await udpSocket.BindServiceNameAsync(port);
             await udpSocket.ConnectAsync(droneIP, port);
             udpWriter = new DataWriter(udpSocket.OutputStream);

             udpWriter.WriteByte(1);
             await udpWriter.StoreAsync();

             var loop = Task.Run(() => DroneLoop());
        }

Как уже было сказано ранее, нужно использовать протокол UDP, следовательно, нужен объект DatagramSocket. После подключения с помощью метода ConnectAsync мы создаем DataWriter в выходном потоке для отправки команд. И наконец, мы отправляем первый байт по Wi-Fi. Он служит только для инициализации системы и будет отброшен дроном.

Проверим команду, отправленную дрону.

        private static async Task DroneLoop()
        {
            while (true)
            {

                var commandToSend = DroneState.GetNextCommand(sequenceNumber);
                await SendCommandAsync(commandToSend);

                sequenceNumber++;
                await Task.Delay(30);
            }
        }

Тег DroneState.GetNextCommand форматирует строковую АТ-команду, которую нужно отправить устройству. Для этого нужен порядковый номер: дрон ожидает, что каждая команда сопровождается порядковым номером, и игнорирует все команды, номера которых меньше или равны номерам уже полученных команд.

После этого мы используем WriteString для отправки в поток команд через StreamSocket, при этом StoreAsync записывает команды в буфер и отправляет их. И наконец, мы увеличиваем порядковый номер и используем параметр Task Delay, чтобы ввести задержку в 30 миллисекунд перед следующей итерацией.
Класс DroneState определяет, какую команду отправить.

    public static class DroneState
    {
       public static double StrafeX { get; set; }
       public static double StrafeY { get; set; }
       public static double AscendY { get; set; }
       public static double RollX { get; set; }
       public static bool Flying { get; set; }
       public static bool isFlying { get; set; }

        internal static string GetNextCommand(uint sequenceNumber)
        {
            // Determine if the drone needs to take off or land
            if (Flying && !isFlying)
            {
                isFlying = true;
                return DroneMovement.GetDroneTakeoff(sequenceNumber);
            }
            else if (!Flying && isFlying)
            {
                isFlying = false;
                return DroneMovement.GetDroneLand(sequenceNumber);
            }

            // If the drone is flying, sends movement commands to it.
            if (isFlying && (StrafeX != 0 || StrafeY != 0 || AscendY != 0 || RollX != 0))
                return DroneMovement.GetDroneMove(sequenceNumber, StrafeX, StrafeY, AscendY, RollX);

            return DroneMovement.GetHoveringCommand(sequenceNumber);
        }
    }


Свойства StrafeX, StrafeY, AscendY и RollX определяют соответственно скорость движения влево и вправо, вперед и назад, высоту и угол вращения дрона. Эти свойства имеют тип данных Double, допустимые значения — от 1 до -1. Например, если задать для свойства StrafeX значение -0,5, то дрон будет перемещаться влево с половиной максимальной скорости; если задать 1, то дрон полетит вправо с максимальной скоростью.

Переменная Flying определяет взлет и посадку. В методе GetNextCommand мы проверяем значения этих полей, чтобы определить, какую команду отправить дрону. Эти команды, в свою очередь, находятся под управлением класса DroneMovement.
Обратите внимание, что, если команды не заданы, последняя инструкция создают так называемую команду Hovering. Это пустая команда, поддерживающая открытый канал связи между дроном и устройством. Дрон должен постоянно получать сообщения от управляющего им приложения, даже если не нужно выполнять никаких действий и ничего не изменилось.

Самый интересный метод класса DroneMovement — метод GetDroneMove, который фактически и занимается составлением и отправкой команд дрону. Другие методы, связанные с движением, см. в этом примере.

public static string GetDroneMove(uint sequenceNumber, double velocityX, double velocityY, double velocityAscend, double velocityRoll)
    {
        var valueX = FloatConversion(velocityX);
        var valueY = FloatConversion(velocityY);
        var valueAscend = FloatConversion(velocityAscend);
        var valueRoll = FloatConversion(velocityRoll);

        var command = string.Format("{0},{1},{2},{3}", valueX, valueY, valueAscend, valueRoll);
        return CreateATPCMDCommand(sequenceNumber, command);
    }
private static string CreateATPCMDCommand(uint sequenceNumber, string command, int mode = 1)
    {
        return string.Format("AT*PCMD={0},{1},{2}{3}", sequenceNumber, mode, command, Environment.NewLine);
    }

Метод FloatConversion не указан здесь, но он преобразует значение типа Double диапазона от -1 до 1 в целочисленное значение со знаком, которое может быть использовано АТ-командами, например строкой PCMD для управления движением.

Показанный здесь код доступен в виде бесплатной библиотеки на сайте NuGet (AR.Drone 2.0 Interaction Library). Эта библиотека предоставляет все необходимое для управления — от взлета до посадки.


Пользовательский интерфейс AR.Drone UI на сайте NuGet

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

Intel RealSense SDK


Теперь посмотрим на одну из самых интересных и удобных в использовании (для меня) возможностей Intel RealSense SDK — распознавание речи.

В SDK поддерживается два подхода к распознаванию речи.

  • Распознавание команд (по заданному словарю).
  • Распознавание свободного текста (диктовка).

Первый подход представляет собой своего рода список команд, заданный приложением, на указанном языке, который обрабатывается «распознавателем». Все слова, которых нет в списке, игнорируются.

Второй подход — что-то типа диктофона, «понимающего» любой текст в свободной форме. Этот подход идеален для стенографирования, автоматического создания субтитров и т. п.

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

        private PXCMSession Session;
        private PXCMSpeechRecognition SpeechRecognition;
        private PXCMAudioSource AudioSource;
        private PXCMSpeechRecognition.Handler RecognitionHandler;

Session — тег, необходимый для доступа к вводу-выводу и к алгоритмам SDK, поскольку все последующие действия унаследованы от этого экземпляра.
SpeechRecognition — экземпляр модуля распознавания, созданного функцией CreateImpl в среде Session.
AudioSource — интерфейс устройства, позволяющий установить и выбрать входное аудиоустройство (в нашем примере кода мы для простоты выбираем первое доступное аудиоустройство).
RecognitionHandler — фактический обработчик, назначающий обработчик событий для события OnRecognition.

Теперь инициализируем сеанс, AudioSource и экземпляр SpeechRecognition.

            Session = PXCMSession.CreateInstance();
            if (Session != null)
            {
                // session is a PXCMSession instance.
                AudioSource = Session.CreateAudioSource();
                // Scan and Enumerate audio devices
                AudioSource.ScanDevices();

                PXCMAudioSource.DeviceInfo dinfo = null;

                for (int d = AudioSource.QueryDeviceNum() - 1; d >= 0; d--)
                {
                    AudioSource.QueryDeviceInfo(d, out dinfo);
                }
                AudioSource.SetDevice(dinfo);

                Session.CreateImpl<PXCMSpeechRecognition>(out SpeechRecognition);

Как было отмечено ранее, для простоты кода мы выбираем первое доступное аудиоустройство.

PXCMSpeechRecognition.ProfileInfo pinfo;
              SpeechRecognition.QueryProfile(0, out pinfo);
              SpeechRecognition.SetProfile(pinfo);

Затем нужно опросить систему, узнать фактический параметр конфигурации и назначить его переменной (pinfo).

Также нужно настроить ряд параметров в профиле, чтобы изменить язык распознавания. Задайте уровень достоверности распознавания (при более высоком значении требуется более уверенное распознавание), интервал окончания распознавания и т. д.

В нашем случае параметр по умолчанию устанавливается как в профиле 0 (полученном из Queryprofile).

                String[] cmds = new String[] { "Takeoff", "Land", "Rotate Left", "Rotate Right", "Advance",
                    "Back", "Up", "Down", "Left", "Right", "Stop" , "Dance"};
                int[] labels = new int[] { 1, 2, 4, 5, 8, 16, 32, 64, 128, 256, 512, 1024 };
                // Build the grammar.
                SpeechRecognition.BuildGrammarFromStringList(1, cmds, labels);
                // Set the active grammar.
                SpeechRecognition.SetGrammar(1);

Затем задаем грамматический словарь для обучения системы распознавания. С помощью BuildGrammarFromStringList мы создаем простой список глаголов и соответствующих возвращаемых значений, определяя грамматику номер 1.

Можно задать несколько грамматик для использования в приложении и включать одну из них при необходимости, поэтому можно создать разные словари команд для всех поддерживаемых языков и предоставить пользователю возможность переключаться между языками, распознаваемыми в SDK. В этом случае нужно установить соответствующие DLL-файлы поддержки языка, поскольку при установке SDK по умолчанию устанавливается поддержка только для языка «Английский (США)». В этом примере мы используем только грамматику, установленную по умолчанию вместе с языком «Английский (США)».

Затем выбираем, какую грамматику следует назначить активной в экземпляре SpeechRecognition.

                RecognitionHandler = new PXCMSpeechRecognition.Handler();

                RecognitionHandler.onRecognition = OnRecognition;

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

        public void OnRecognition(PXCMSpeechRecognition.RecognitionData data)
        {
            var RecognizedValue = data.scores[0].label;
            double movement = 0.3;
            TimeSpan duration = new TimeSpan(0, 0, 0, 500);
            switch (RecognizedValue)
            {
                case 1:
                    DroneState.TakeOff();
                    WriteInList("Takeoff");
                    break;
                case 2:
                    DroneState.Land();
                    WriteInList("Land");
                    break;
                case 4:
                    DroneState.RotateLeftForAsync(movement, duration);
                    WriteInList("Rotate Left");
                    break;
                case 5:
                    DroneState.RotateRightForAsync(movement, duration);
                    WriteInList("Rotate Right");
                    break;
                case 8:
                    DroneState.GoForward(movement);
                    Thread.Sleep(500);
                    DroneState.Stop();
                    WriteInList("Advance");
                    break;
                case 16:
                    DroneState.GoBackward(movement);
                    Thread.Sleep(500);
                    DroneState.Stop();
                    WriteInList("Back");
                    break;
                case 32:
                    DroneState.GoUp(movement);
                    Thread.Sleep(500);
                    DroneState.Stop();
                    WriteInList("Up");
                    break;
                case 64:
                    DroneState.GoDown(movement);
                    Thread.Sleep(500);
                    DroneState.Stop();
                    WriteInList("Down");
                    break;
                case 128:
                    DroneState.StrafeX = .5;
                    Thread.Sleep(500);
                    DroneState.StrafeX = 0;
                    WriteInList("Left");
                    break;
                case 256:
                    DroneState.StrafeX = -.5;
                    Thread.Sleep(500);
                    DroneState.StrafeX = 0;
                    WriteInList("Right");
                    break;
                case 512:
                    DroneState.Stop();
                    WriteInList("Stop");
                    break;
                case 1024:
                    WriteInList("Dance");
                    DroneState.RotateLeft(movement);
                    Thread.Sleep(500);
                    DroneState.RotateRight(movement);
                    Thread.Sleep(500);
                    DroneState.RotateRight(movement);
                    Thread.Sleep(500);
                    DroneState.RotateLeft(movement);
                    Thread.Sleep(500);
                    DroneState.GoForward(movement);
                    Thread.Sleep(500);
                    DroneState.GoBackward(movement);
                    Thread.Sleep(500);
                    DroneState.Stop();
                    break;
                default:
                    break;

            }
            Debug.WriteLine(data.grammar.ToString());
            Debug.WriteLine(data.scores[0].label.ToString());
            Debug.WriteLine(data.scores[0].sentence);
            // Process Recognition Data
        }

Это метод получения значения, возвращенного из данных распознавания, и выполнения соответствующей команды (в нашем случае — соответствующей команды управления полетом дрона).

Каждая команда дрона относится к вызову DroneState с определенным методом (TakeOff, GoUp, DoDown и т. д.) и с определенным параметром движения или длительности, который в каждом случае касается определенного количества или длительности движения.

Некоторым командам требуется явный вызов метода Stop для остановки текущего действия, иначе дрон продолжит двигаться согласно полученной команде (команды см. в предыдущем фрагменте кода).

В некоторых случаях нужно вставить Thread.Sleep между двумя разными командами, чтобы дождаться завершения предыдущего действия перед отправкой новой команды.

Для проверки распознавания, даже если нет доступного дрона, я вставил переменную (она управляется флажком в главном окне), которая включает функциональный режим Drone Stub (в этом режиме команды создаются, но не отправляются).

Чтобы закрыть приложение, вызовите метод OnClosing для закрытия и уничтожения всех экземпляров и обработчиков и для общей очистки системы.

В коде содержатся некоторые команды отладки, выводящие полезную информацию в окнах отладки Visual Studio* при тестировании системы.

Заключение


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

Комментарии (3)


  1. grossws
    15.12.2015 11:26

    Самое интересное не показали, failsafe поведение.
    Представил, как оператор орёт «Stop! Halt! F***!», а дрон летит в стену. Или мимокрокодил говорит «shut up and take my money!», увидев такое прекрасное творение, оно уходит в потолок, и будущий покупатель уже передумал.

    не мог не вспомнить


    1. olegych76
      16.12.2015 13:08

      А я не мог не вспомнить


  1. kozyabka
    15.12.2015 13:44
    +2

    «Модель AR.Drone 2.0 компании Parrot — один из наиболее интересных дронов, предлагаемых на рынке для энтузиастов.» Интересно, как эта свистулька по тормозному wifi с радиусом действия 30 метров стала наиболее интересной для энтузиастов?
    Ну и да, голосовое управление для квадрика это неминуемы креш. В аналоговом управлении помимо команды «верх» или «влево» есть еще сила с которой эту команду следует совершать. Как вы голосом будете передавать насколько градусов квадру нужно развернуться, насколько быстро опуститься или ускориться?