Работаем с Lego Mindstorms EV3 из .NET

Традиционно роботы, построенные на платформе Lego Mindstorms EV3, программируются с использованием графической среды LabVIEW. В этом случае программы запускаются на контроллере EV3 и робот работает автономно. Здесь я расскажу про альтернативный способ управления роботом — использование платформы .NET, запущенной на компьютере.

Но прежде чем мы перейдем непосредственно к программированию, давайте рассмотрим случаи, когда это может быть полезно:

  • Требуется удаленное управление роботом с ноутбука (например, по нажатию кнопок)
  • Требуется собирать данные с контроллера EV3 и обрабатывать их на внешней системе (например, для IoT-систем)
  • Любые другие ситуации, когда хочется написать алгоритм управления на .NET и запускать его с компьютера, подключенного к контроллеру EV3

LEGO MINDSTORMS EV3 API for .NET

Управление контроллером EV3 из внешней системы осуществляется путем отправки команд в последовательный порт. Сам формат команд описан в Communication Developer Kit.

Но реализация этого протокола вручную — дело скучное. Поэтому можно воспользоваться готовой .NET-оберткой, которую заботливо написал Brian Peek. Исходные коды этой библиотеки размещены на Github, а готовый к использованию пакет можно найти в Nuget.

Подключение к контроллеру EV3

Для связи с контроллером EV3 используется класс Brick. При создании этого объекта в конструктор требуется передать реализацию интерфейса ICommunication — объект, описывающий способ подключения к контроллеру EV3. Доступны реализации UsbCommunication, BluetoothCommunication и NetworkCommunication (подключение через WiFi).

Наиболее популярный способ подключения - через Bluetooth. Рассмотрим поподробнее этот способ подключения.

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

После того, как контроллер подключен, идём в настройки Bluetooth и выбираем вкладку COM-порты. Находим наш контроллер, нам нужен исходящий порт. Его и будем указывать при создании объекта BluetoothCommunication.

Код для подключения к контроллеру будет выглядеть так:

public async Task Connect(ICommunication communication)
{
    var communication = new BluetoothCommunication("COM9");
    var brick = _brick = new Brick(communication);
    await _brick.ConnectAsync();
}

Опционально можно указать таймаут подключения к контроллеру:

await _brick.ConnectAsync(TimeSpan.FromSeconds(5));

Подключение к блоку через USB или WiFi осуществляется аналогично, за тем исключением, что используются объекты UsbCommunication и NetworkCommunication.

Все дальнейшие действия, выполняемые с контроллером, осуществляются через объект Brick.

Покрутим моторами

Для выполнения команд на контроллере EV3 обратимся к свойству DirectCommand объекта Brick. Для начала попробуем запустить моторы.

Предположим, что наш мотор подключен к порту A контроллера, тогда запуск этого мотора на мощности 50% будет выглядеть так:

await _brick.DirectCommand.TurnMotorAtPowerAsync(OutputPort.A, 50);

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

Принудительная остановка осуществляется методом StopMotorAsync():

await _brick.DirectCommand.StopMotorAsync(OutputPort.A, true);

Второй параметр указывает на использование тормоза. Если его установить в false, то мотор будет останавливаться «накатом».

Чтение значений с датчиков

Контроллер EV3 имеет четыре порта для подключения сенсоров. Дополнительно к этому, моторы также имеют встроеные энкодеры, что позволяет использовать их как сенсоры. В итоге мы имеем 8 портов, с которых можно считывать значения.

Доступ к портам для считывания значений можно получить через свойство Ports объекта Brick. Ports — это коллекция портов, доступных на контроллере. Поэтому для работы с конкретным портом нужно его выбрать. InputPort.One...InputPort.Four — это порты для датчиков, а InputPort.A...InputPort.D — это энкодеры моторов.

var port1 = _brick.Ports[InputPort.One];

Датчики в EV3 могут работать в разных режимах. Например, датчик цвета EV3 можно использовать для измерения внешнего освещения, измерения отраженного света или для определения цвета. Поэтому, чтобы «сообщить» сенсору о том, как именно мы хотим его использовать, нужно задать его режим:

_brick.Ports[InputPort.One].SetMode(ColorMode.Reflective);

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

float si = _brick.Ports[InputPort.One].SIValue;
int raw = _brick.Ports[InputPort.One].RawValue;
byte percent = _brick.Ports[InputPort.One].PercentValue;

Свойство SIValue возвращает обработанные данные. Здесь все зависит от того, какой именно датчик используется и в каком режиме. Например, при измерении отраженного света мы получим значения от 0 до 100 в зависимости от интенсивности отраженного света (черный/белый).

Свойство RawValue возвращает «сырое» значение, полученное с АЦП. Иногда удобнее использовать именно его для последующей обработки и использования. Кстати, в среде разработки EV3 тоже есть возможность получения «сырых» значений — для этого нужно воспользоваться блоком из синей панели.

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

Выполнение команд «пачкой»

Предположим, в нашем распоряжении есть робот-тележка с двумя колесами и мы хотим развернуть его на месте. В этом случае два колеса должны вращаться в противоположном направлении. Если мы воспользуемся DirectCommand и последовательно отправим две команды контроллеру, между их выполнением может пройти некоторое время:

await _brick.DirectCommand.TurnMotorAtPowerAsync(OutputPort.A, 50);
await _brick.DirectCommand.TurnMotorAtPowerAsync(OutputPort.B, -50);

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

Если для нас критически важно заставить крутится моторы одновременно, можно отправлять команды контроллеру «пачкой». В этом случае следует воспользоваться свойством BatchCommand вместо DirectCommand:

_brick.BatchCommand.TurnMotorAtPower(OutputPort.A, 50);
_brick.BatchCommand.TurnMotorAtPower(OutputPort.B, -50);
await _brick.BatchCommand.SendCommandAsync();

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

Что ещё можно сделать

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

  • CleanUIAsync(), DrawTextAsync(), DrawLineAsync() и др. — манипуляция встроенным экраном контроллера EV3
  • PlayToneAsync() и PlaySoundAsync() — использование встроенного динамика для воспроизведения звуков
  • WriteFileAsync(), CopyFileAsync(), DeleteFileAsync() (из SystemCommand) — работа с файлами

Заключение

Использование .NET для управления роботами Mindstorms EV3 хорошо демонстрирует как технологии «из разных миров» могут работать совместно. В качестве результата исследования EV3 API для .NET было создано небольшое приложение, которое позволяет управлять роботом EV3 с компьютера. К сожалению, аналогичные приложения существуют для NXT, а EV3 они обошли стороной. В то же время они полезны на сорвнованиях управляемых роботов, например в футболе роботов.

Приложение можно загрузить и установить по этой ссылке: EV3 Remote Control.

Исходные коды доступны на Github.