Рейтинг@Mail.ru

Создаём программу дистанционного управления роботом EV3 с заводской прошивкой

Автор: Alex. Опубликовано в Программирование . просмотров: 49900

Рейтинг:  4 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда не активна
 

Эта статья будет интересна тем, кто хочет сделать программу для дистанционного управления роботом EV3 со стандартной заводской прошивкой через Bluetooth, WiFi или USB и не важно, с какого устройства или операционной системы. Здесь мы рассмотрим протокол взаимодействия между модулем EV3 и вашей программой.

Создаём программу дистанционного управления роботом EV3 с заводской прошивкой

Основная идея статьи состоит в том, чтобы приложение могло управлять роботом EV3, со стандартной заводской прошивкой. Т.е. мы не будем рассматривать здесь всевозможные прошивки, которые загружаются с SD-карт, такие как leJOS EV3, ev3dev или MonoBrick EV3 Firmware.

LEGO MINDSTORMS EV3 API для .NET

Больше всего здесь повезло разработчикам .NET. Для них уже есть бесплатное разработанное API с открытым исходным кодом, с помощью которого можно сделать приложение, работающее на ПК под управлением Windows, Windows Phone или WinRT. Есть даже поддержка Android, но только для тех, кто разрабатывает с помощью Xamarin. API позволяет подключаться к модулю EV3 через Bluetooth, WiFi или USB, управлять моторами и считывать показания датчиков. Не буду останавливаться здесь подробно, просто приведу несколько простых примеров.

Вот пример, как будет выглядеть подключение к EV3 через USB-кабель:

Brick brick = new Brick(new UsbCommunication());

После этого вы можете управлять моторами, например, вот так:

//Поворачиваем мотор, подключенный к порту A, 5 сек. с мощностью 50%.
await brick.DirectCommand.TurnMotorAtPowerAsync(OutputPort.A, 50, 5000);

Устанавливать режим работы датчикам, например, так:

//Устанавливаем датчику касания режим возврата количества нажатий.
brick.Ports[InputPort.One].SetMode(TouchMode.Bumps);

И считывать показания датчиков в трёх разных форматах: Raw (как есть, без изменений), SI (международная система единиц) или проценты. Вот пример получения данных в SI:

//Выводим значение датчика, подключенного к порту 1.
Debug.WriteLine(brick.Ports[InputPort.One].SIValue);

Есть даже событие, оповещающее об изменении какого либо свойства EV3, будь то нажатие на кнопку модуля EV3 или изменение значения любого из датчиков. Работает это вот так:

//Подписываемся на событие.
brick.BrickChanged += OnBrickChanged;
 
...
 
void OnBrickChanged(object sender, BrickChangedEventArgs e)
{
    //Выводим значение датчика, подключенного к порту 1.
    Debug.WriteLine(e.Ports[InputPort.One].SIValue);
}

MonoBrick Communication Library

Эта библиотека тоже бесплатна и доступна только разработчикам .NET. Скачать библиотеку можно здесь, изучить примеры здесь, а посмотреть документацию здесь. Есть версия библиотеки для разработки Android-приложений в Xamarin. Пользоваться библиотекой так же просто, как и API, который был описан выше. Здесь я тоже не буду углубляться в подробности, а просто продемонстрирую несколько примеров.

Вот так подключается EV3 через USB:

var ev3 = new Brick<Sensor,Sensor,Sensor,Sensor>("usb");
ev3.Connection.Open();

Так происходит управление моторами:

//Включаем мотор A.
ev3.MotorA.On(50);
//Ждём 3 секунды.
System.Threading.Thread.Sleep(3000); 
//Выключаем мотор A.
ev3.MotorA.Off();

А можно сразу управлять тележкой или роботом на гусеницах вот так:

//Задаём свойства транспортного средства.
ev3.Vehicle.LeftPort = MotorPort.OutA;
ev3.Vehicle.RightPort = MotorPort.OutD;
ev3.Vehicle.ReverseLeft = false;
ev3.Vehicle.ReverseRight = false;
//Едем вперёд.
ev3.Vehicle.Forward(50);
System.Threading.Thread.Sleep(3000); 
//Вращаемся на месте вправо.
ev3.Vehicle.SpinRight(50);
System.Threading.Thread.Sleep(3000);
//Поворачиваем налево
ev3.Vehicle.TurnLeftForward(50, 50);
System.Threading.Thread.Sleep(3000);
//Останавливаемся.
ev3.Vehicle.Off();

Так читаем значения датчиков:

ev3.Sensor1 = new IRSensor(IRMode.Proximity); 
ev3.Sensor2 = new TouchSensor(); 
ev3.Sensor3 = new ColorSensor(ColorMode.Color); 
ev3.Sensor4 = new GyroSensor(GyroMode.Angle); 
Console.WriteLine("S1: " + ev3.Sensor1.ReadAsString()); 
Console.WriteLine("S2: " + ev3.Sensor2.ReadAsString()); 
Console.WriteLine("S3: " + ev3.Sensor3.ReadAsString()); 
Console.WriteLine("S4: " + ev3.Sensor4.ReadAsString());

А вот так можно задать режим работы датчиков:

var ev3 = new Brick<TouchSensor,ColorSensor,IRSensor,Sensor> ("usb"); 
ev3.Connection.Open();
ev3.Sensor1.Mode = TouchMode.Count;
ev3.Sensor2.Mode = ColorMode.Color;
ev3.Sensor3.Mode = IRMode.Seek;
ev3.Sensor1.Reset();
Console.WriteLine("S1: " + ev3.Sensor1.ReadAsString());
Color color = ev3.Sensor2.ReadColor();
int value = ev3.Sensor3.Read();

И ещё здесь можно отправить сообщение модулю EV3 вот так:

ev3.Mailbox.Send("mailbox1", input, false);

А при работе с подключенными «в гирлянду» несколькими модулями EV3 код будет таким:

ev3.MotorA.DaisyChainLayer = DaisyChainLayer.First; 
ev3.MotorA.On(50); 
Thread.Sleep(2000); 
ev3.MotorA.Off(); 
ev3.Sensor1.DaisyChainLayer = DaisyChainLayer.Second; 
Console.WriteLine(ev3.Sensor1.ReadAsString()); 
ev3.Connection.Close();

legoev3cpp

legoev3cpp - это небольшое кроссплатформенное API для C++ 14. И хотя разработка заявлена как кроссплатформенная, на сегодняшний момент реализована поддержка только iOS. Разработчик приглашает присоединиться к проекту всех заинтересовавшихся. Страничка проекта находится здесь. В папке «Jove's Landing» вы найдёте приложение демонстрирующее использование legoev3cpp.

ev3-Nodejs-bluetooth-Api

Это API предназначено для любителей языка JavaScript и платформы Node.js. Автор признаётся, что создавал API с помощью обратной разработки официального Android-приложения LEGO MINDSTORMS Commander. Скачать API и пример можно здесь, посмотреть видео демонстрацию – здесь.

Коммуникационный интерфейс

Всем кому по тем или иным причинам не подходит разработка с помощью готовых API, описанных выше, придётся возиться с коммуникационным интерфейсом. Протокол общения между модулем LEGO MINDSTORMS EV3 и любым типом подключаемого устройства одинаковый для разных типов подключения: Bluetooth, USB или WiFi. Чтобы его освоить, нужно изучать официальную документацию или анализировать API для .NET, описанный выше.

Официальную документацию можно найти на странице загрузок LEGO MINDSTORMS EV3. Здесь вы можете скачать описание прошивки (EV3 Firmware Developer Kit) и комплект разработчика системы передачи данных (LEGO MINDSTORMS EV3 Communication Developer Kit). Эти два документа можно скачать и с нашего сайта:

Файлы:
EV3 Firmware Developer Kit

Описание прошивки LEGO Mindstorms EV3.

Дата 08.10.2015 Размер файла 686.56 KB Закачек 5026

EV3 Communication Developer Kit

Комплект разработчика системы передачи данных LEGO MINDSTORMS EV3.

Дата 08.10.2015 Размер файла 274.19 KB Закачек 4417

Все команды, которые вы будете использовать, разделяются на прямые и системные. Прямые команды представляют из себя микропрограммы, состоящие из набора определённых байт-кодов, и выполняются параллельно с работающими пользовательскими программами. Использовать прямые команды нужно очень осторожно, т.к. здесь нет никаких ограничений на использование «опасных» кодов или конструкций, например, здесь возможны блокировки и зацикливания. Однако, в таких случаях, пользовательская программа будет продолжать работать нормально. Описание прямых команд вы сможете найти в первом документе (EV3 Firmware Developer Kit) в разделе 4 (Byte code definition and functionality), а примеры использования – во втором (LEGO MINDSTORMS EV3 Communication Developer Kit) в разделе 4 (Direct Commands).

Системные команды используются для передачи данных в модуль EV3 или из него. Описание этих команд вы можете найти во втором документе (LEGO MINDSTORMS EV3 Communication Developer Kit) в разделе 3 (System Command).

После того как мы чуть-чуть поговорили о теории, попробуем отправить какую-нибудь команду модулю EV3. Здесь я буду приводить примеры кода на языке Java для Android-приложения. Для начала рассмотрим самый простой пример, в котором по нажатию на кнопку мы подключаемся к EV3 и посылаем команду для проигрывания звука:

public void onButtonClick(View v) throws IOException, InterruptedException {
    //Ищем EV3 среди подключенных устройств по имени (Bluetooth должен быть включён).
    BluetoothDevice ev3device = null;
    for (BluetoothDevice device : BluetoothAdapter.getDefaultAdapter().getBondedDevices()) {
        if (device.getName().compareTo("EV3") == 0) {
            ev3device = device;
            //Выдаём сообщение, что EV3 найден.
            Toast.makeText(getApplicationContext(), "EV3 найден :)", Toast.LENGTH_LONG).show();
            break;
        }
    }
    if (ev3device != null) {
        //Подключаемся к EV3 (требуется разрешение BLUETOOTH).
        BluetoothSocket socket = ev3device.createRfcommSocketToServiceRecord(
                UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
        socket.connect();
        try {
            //Отправляем сообщение с командой на проигрывание звука.
            socket.getOutputStream().write(new byte[]{14, 0, 0, 0, -128, 0, 0, -108, 1, 2, -126, -24, 3, -126, -24, 3});
        }
        finally {
            //Отключаемся от EV3.
            socket.close();
        }
    }
}

Подключение к EV3 производится во всех средах разработки и операционных системах по-разному, поэтому я не буду на этом останавливаться.

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

Итак, обратим внимание на сообщение, которое мы передали модулю EV3. В восьмеричной системе переданный массив байт будет выглядеть так: 0E00000080000094010282E80382E803. Первые два байта 0x0E00 – это размер сообщения в байтах (тип unsigned short), но без учёта этих двух байтов, т.е. если, как в примере, после первых двух байт идёт 14 байтов, то сюда записываем 14 (0x0E в восьмеричной системе). Младший байт записывается первым (порядок байт Little Endian).

Следующие 2 байта 0x0000 – это порядковый номер сообщения. Например, для первого сообщения нужно передавать 0, для второго – 1, и так далее по порядку. Если число достигнет максимума, т.е. 65535 (0xFFFF), можно начинать снова с нуля. Номер сообщения пригодится вам для идентификации ответа, ведь ответ будет пронумерован тем же порядковым номером, что и сообщение, для которого он получен. Порядок байт здесь тоже Little Endian.

Следующий байт – это тип команды или команд. Здесь возможны следующие варианты: 0x01 – системная команда, требуется ответ; 0x81 – системная команда, ответ не требуется; 0x00 – прямая команда или команды, требуется ответ; 0x80 (как в примере) – прямая команда или команды, ответ не требуется.

Остальные байты будут разными для разных типов команд. Для системных команд следующий байт обозначает команду, например, 0x92 – начало загрузки файла на EV3 (BEGIN_DOWNLOAD), 0x9E – запись в почтовый ящик (WRITEMAILBOX) и т.п. А затем идут байты специфичные для каждой системной команды.

Для прямых команд, как в примере, следующие два байта (в примере – это 0x0000) показывают, сколько места для локальных и глобальных переменных вы хотите зарезервировать. В примере мы не ждём ответа, поэтому место не резервируется. Формат здесь такой: в младших 10-ти битах здесь хранится количество байт выделенных для глобальных переменных, а в 6-ти старших битах – для локальных. С помощью глобальных переменных вы будете получать ответы от EV3. Если в сообщении у вас две команды, то нужно зарезервировать место для двух глобальных переменных, если три, то для трёх переменных и т.д. После получения ответа вы сможете считать результат из этих переменных. Подробнее об этом будет написано ниже.

В остальных байтах содержится одна команда или несколько команд идущих друг за другом. Первый байт – это сама команда, в примере, указана команда 0x94 (opSound) – это команда для работы со звуком. В следующих байтах содержатся параметры команды. Первый параметр – это специфический параметр, определяющий, что именно будет делать команда. В примере 0x01 обозначает, что нужно проиграть звук определённой громкости, тональности и продолжительности. Следующие три параметра - это громкость (от 0 до 100), частота (от 250 до 10000) и длительность в миллисекундах.

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

        • 0x81 – однобайтовое число unsigned byte или byte, в зависимости от команды (в документации обозначается как Data8);
        • 0x82 – двухбайтовое число unsigned short или short, в зависимости от команды (в документации - Data16);
        • 0x83 – четырёхбайтовое число unsigned int или int (в документации – Data32);
        • 0x84 – строка оканчивающаяся нулём.

Как видите, в примере, первые два параметра 0x01 и 0x02 меньше 32, и поэтому они передаются в коротком формате без указания типа значения, а третий и четвёртый параметры – это двухбайтовые числа имеющие значение 0xE803 и для них задаётся тип 0x82.

Теперь давайте отправим команду на чтение цвета с датчика, подключенного к порту 1, и считаем ответ:

//Перед отправкой сообщения не забудьте приставить к датчику какую-нибудь цветную деталь, допустим, красную.
//Отправляем сообщение с командой на измерение цвета: 0D000100000400991D000000020160
socket.getOutputStream().write(new byte[]{13, 0, 1, 0, 0, 4, 0, -103, 29, 0, 0, 0, 2, 1, 96});
//Считываем первые два байта, чтобы узнать размер ответного сообщения.
//В случае успешного выполнения команды в массиве будет: 0700
//Т.е. ещё семь байт.
byte[] replySizeBytes = new byte[2];
socket.getInputStream().read(replySizeBytes, 0, 2);
//Перекладываем число из массива в переменную (первый байт - младший, второй - старший).
int replySize = (replySizeBytes[0] < 0 ? (int)replySizeBytes[0] + 256 : (int)replySizeBytes[0])
        | ((replySizeBytes[1] < 0 ? (int)replySizeBytes[1] + 256 : (int)replySizeBytes[1]) << 8);
//Создаём массив нужного размера.
byte[] replyBytes = new byte[replySize];
//Считываем остальную часть сообщения в массив.
socket.getInputStream().read(replyBytes, 0, replySize);
//На выходе получим следующий массив: 0100020000A040
//Первые два байта (0x01 и 0x00) - это номер сообщения, третий байт (0x02) - это тип результата, остальные байты – значения переменных.
if (replyBytes[2] == 2) //Если результат 2, значит команда выполнена успешно.
{
    //Результат будем считывать с помощью класса ByteBuffer.
    ByteBuffer byteBuffer = ByteBuffer.wrap(replyBytes);
    //Устанавливаем порядок байтов (первый байт - младший, второй - старший).
    byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
    //В последних четырёх байтах результат - это число с плавающей запятой (тип float, порядок байт Little Endian).
    //Цвет указывается в пределах 0 - 8.
    float f = byteBuffer.getFloat(3);
    //Заносим в строку название цвета.
    String color = "";
    switch ((int)f)
    {
        case 0: color = "нет цвета"; break;
        case 1: color = "чёрный"; break;
        case 2: color = "синий"; break;
        case 3: color = "зелёный"; break;
        case 4: color = "жёлтый"; break;
        case 5: color = "красный"; break;
        case 6: color = "белый"; break;
        case 7: color = "коричевый"; break;
    }
    //Выводим сообщение с результатом.
    //Для красного цвета, на экране появится сообщение "Цвет: 5.0 - красный".
    Toast.makeText(getApplicationContext(),
            "Цвет: " + Float.toString(f) + " - " + color,
            Toast.LENGTH_LONG).show();
}

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

        • 0x0D00 – это размер сообщения;
        • 0x0100 – порядковый номер сообщения (номер 1);
        • 0x00 – тип команды (прямая команда, требуется ответ);
        • 0x0400 – сколько байт резервируется для переменных (здесь резервируется 4 байта для одной глобальной переменной);
        • 0x99 – команда для работы с устройствами ввода (opInput_Device), дальше идут параметры команды:
          • 0x1D – специфический параметр команды обозначающий, снятие замера с устройства ввода в международных единицах измерения;
          • 0x00 – номер модуля EV3 (если у вас соединено несколько модулей, то используем значения от 0 до 3);
          • 0x00 – номер порта (от 0 до 3: 0 обозначает 1-й порт, 1 – 2-й и т.д.);
          • 0x00 – тип подключенного к порту устройства (0 – означает, что тип менять не нужно) (таблицу с типами устройств и режимами датчиков можно найти в п. 5 «Device type list» документа EV3 Firmware Developer Kit);
          • 0x02 – режим датчика (2 – означает режим измерения цвета);
          • 0x01 – количество возвращаемых переменных (в данном случае – одна переменная);
          • 0x60 – обозначает индекс глобальной переменной, в которую будет записан результат измерения (число 0x60 обозначает, что переменная глобальная с индексом 0).

Число, указывающее индекс глобальных переменных строится сложным образом. Если значение индекса меньше 32, то индекс можно задать одним байтом вот так: 0x60 & i. Здесь 0x60 – это биты, обозначающие небольшой индекс глобальной переменной, а i – это наш индекс. С помощью такой схемы мы сможем задать индекс от 0 до 31, а байт при этом получится от 0x60 до 0x7F. Если вам нужно указать индекс больше 31, то первый байт будет обозначать тип и размер индекса, а в следующих байтах будет храниться значение индекса. Для значения индекса от 0 до 255 первый байт будет равен 0xE1, а затем следующим байтом будет идти само значение. Для значения от 0 до 65535, первый байт будет равен 0xE2, а в следующих двух байтах нужно задать значение. Для значения индекса больше 65535, первый байт будет равен 0xE3, а в следующих четырёх будет значение.

Подробно об этом написано в разделе 3.4 «Parameter encoding» документа EV3 Firmware Developer Kit. Также в качестве подсказки можно использовать макросы GV0(i), GV1(i), GV2(i) и GV4(i) в файле bytecodes.h, который можно найти в исходных кодах аппаратного ПО (исходные коды можно скачать на странице загрузок LEGO MINDSTORMS EV3).

Теперь посмотрим, что приходит в ответ. В восьмеричном формате ответ в примере будет выглядеть так: 07000100020000A040. В ответном сообщении, точно так же как и в исходящем сообщении, первые два байта будут указывать размер сообщения. Затем следуют два байта – порядковый номер (в нашем случае номер 1), а пятый байт - будет содержать тип результата: 0x03 – системная команда выполнена успешно, 0x05 – системная команда выполнена с ошибкой, 0x02 – прямая команда (или команды) выполнена успешно, 0x04 – прямая команда (или команды) выполнена с ошибкой.

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

Для прямых команд следующие байты – это пространство глобальных переменных. В нашем примере здесь только одна переменная имеющая значение 0x0000A040 (тип переменной float). Если бы мы вызывали несколько команд, то здесь было бы больше переменных, идущих друг за другом. В нашем примере в переменной приходит номер цвета от 1 до 7 или 0, если цвет не определён.

Для того, чтобы было проще записывать сообщения в поток в Java, я написал следующие функции:

//Записывает unsigned byte.
private void writeUByte(OutputStream _stream, int _ubyte) throws IOException {
    _stream.write(_ubyte > Byte.MAX_VALUE ? _ubyte - 256 : _ubyte);
}
 
//Записывает byte.
private void writeByte(OutputStream _stream, byte _byte) throws IOException {
    _stream.write(_byte);
}
 
//Записывает unsigned short.
private void writeUShort(OutputStream _stream, int _ushort) throws IOException {
    writeUByte(_stream, _ushort & 0xFF);
    writeUByte(_stream, (_ushort >> 8) & 0xFF);
}
 
//Записывает short.
private void writeShort(OutputStream _stream, short _short) throws IOException {
    byte bytes[] = new byte[2];
    ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
    byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
    byteBuffer.putShort(_short);
    _stream.write(bytes);
}
 
//Записывает команду.
private void writeCommand(OutputStream _stream, int opCode) throws IOException {
    writeUByte(_stream, opCode);
}
 
//Записывает команду и специфический параметр команды.
private void writeCommand(OutputStream _stream, int opCode, int cmd) throws IOException {
    writeUByte(_stream, opCode);
    writeUByte(_stream, cmd);
}
 
//Записывает количество зарезервированных байт для глобальных и локальных переменных.
private void writeVariablesAllocation(OutputStream _stream, int globalSize, int localSize) throws IOException {
    writeUByte(_stream, globalSize & 0xFF);
    writeUByte(_stream, ((globalSize >> 8) & 0x3) | ((localSize << 2) & 0xFC));
}
 
//Записывает индекс глобальной переменной.
private void writeGlobalIndex(OutputStream _stream, int index) throws IOException {
    if (index <= 31)
        writeUByte(_stream, index | 0x60);
    else if (index <= 255) {
        writeUByte(_stream, 0xE1);
        writeUByte(_stream, index);
    }
    else if (index <= 65535) {
        writeUByte(_stream, 0xE2);
        writeUShort(_stream, index);
    }
    else {
        writeUByte(_stream, 0xE3);
        writeUByte(_stream, index & 0xFF);
        writeUByte(_stream, (index >> 8) & 0xFF);
        writeUByte(_stream, (index >> 16) & 0xFF);
        writeUByte(_stream, (index >> 24) & 0xFF);
    }
}
 
//Записывает параметр с типом unsigned byte со значением в интервале 0-31 с помощью короткого формата.
private void writeParameterAsSmallByte(OutputStream _stream, int value) throws IOException {
    if (value < 0 && value > 31)
        throw new IllegalArgumentException("Значение должно быть в интервале от 0 до 31.");
    writeUByte(_stream, value);
}
 
//Записывает параметр с типом unsigned byte.
private void writeParameterAsUByte(OutputStream _stream, int value) throws IOException {
    if (value < 0 && value > 255)
        throw new IllegalArgumentException("Значение должно быть в интервале от 0 до 255.");
    writeUByte(_stream, 0x81);
    writeUByte(_stream, value);
}
 
//Записывает параметр с типом byte.
private void writeParameterAsByte(OutputStream _stream, int value) throws IOException {
    if (value < Byte.MIN_VALUE && value > Byte.MAX_VALUE)
        throw new IllegalArgumentException("Значение должно быть в интервале от "
                + Byte.MIN_VALUE + " до " + Byte.MAX_VALUE + ".");
    writeUByte(_stream, 0x81);
    writeByte(_stream, (byte)value);
}
 
//Записывает параметр с типом unsigned short.
private void writeParameterAsUShort(OutputStream _stream, int value) throws IOException {
    if (value < 0 && value > 65535)
        throw new IllegalArgumentException("Значение должно быть в интервале от 0 до 65535.");
    writeUByte(_stream, 0x82);
    writeUShort(_stream, value);
}
 
//Записывает параметр с типом short.
private void writeParameterAsShort(OutputStream _stream, int value) throws IOException {
    if (value < Short.MIN_VALUE && value > Short.MAX_VALUE)
        throw new IllegalArgumentException("Значение должно быть в интервале от "
                + Short.MIN_VALUE + " до " + Short.MAX_VALUE + ".");
    writeUByte(_stream, 0x82);
    writeShort(_stream, (short)value);
}
 
//Записывает параметр с типом int.
private void writeParameterAsInteger(OutputStream _stream, int value) throws IOException {
    writeUByte(_stream, 0x83);
    writeUByte(_stream, value & 0xFF);
    writeUByte(_stream, (value >> 8) & 0xFF);
    writeUByte(_stream, (value >> 16) & 0xFF);
    writeUByte(_stream, (value >> 24) & 0xFF);
}
 
//Читает unsigned byte.
private int readUByte(InputStream _stream) throws IOException {
    byte bytes[] = new byte[1];
    _stream.read(bytes);
    return bytes[0] < 0 ? (int)bytes[0] + 256 : (int)bytes[0];
}
 
//Читает unsigned short.
private int readUShort(InputStream _stream) throws IOException {
    return readUByte(_stream) | (readUByte(_stream) << 8);
}

Используя эти функции, сообщение, которое мы отправляли в первом примере, можно будет отправить так:

//Записываем сообщение в память (размер посчитаем потом).
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//Записываем номер сообщения.
writeUShort(byteArrayOutputStream, 0);
//Записываем тип команды (0x80 - прямая команда, ответ не требуется).
writeUByte(byteArrayOutputStream, 0x80);
//Записываем размер зарезервированного места для переменных (здесь ничего не резервируется).
writeVariablesAllocation(byteArrayOutputStream, 0, 0);
//Записываем код команды.
writeCommand(byteArrayOutputStream, 0x94/*opSound*/, 0x01/*TONE*/);
//Остальные параметры команды.
writeParameterAsSmallByte(byteArrayOutputStream, 2);//Громкость звука.
writeParameterAsShort(byteArrayOutputStream, 1000);//Частота звука.
writeParameterAsShort(byteArrayOutputStream, 1000);//Длительность в мс.
//Отправляем сообщение на EV3.
OutputStream outputStream = socket.getOutputStream();
//Сначала отправляем размер сообщения.
writeUShort(outputStream, byteArrayOutputStream.size());
//Затем отправляем само сообщение.
byteArrayOutputStream.writeTo(outputStream);

Как видите, с помощью функций, байт-код, отправляемый на EV3, стал понятным и удобным. А сообщение из второго примера теперь можно отправить так:

//Записываем сообщение в память (размер посчитаем потом).
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//Записываем номер сообщения.
writeUShort(byteArrayOutputStream, 1);
//Записываем тип команды (прямая команда, требуется ответ).
writeUByte(byteArrayOutputStream, 0x00);
//Записываем количество зарезервированных байт для переменных (4 байта для глобальной переменной).
writeVariablesAllocation(byteArrayOutputStream, 4, 0);
//Записываем код команды.
writeCommand(byteArrayOutputStream, 0x99/*opInput_Device*/, 0x1D/*READY_SI*/);
//Записываем номер модуля EV3.
writeParameterAsSmallByte(byteArrayOutputStream, 0);
//Записываем номер порта (1-й порт).
writeParameterAsSmallByte(byteArrayOutputStream, 0);
//Записываем тип устройства (0 – означает, что тип менять не нужно).
writeParameterAsSmallByte(byteArrayOutputStream, 0);
//Записываем режим работы датчика (чтение цвета).
writeParameterAsSmallByte(byteArrayOutputStream, 2);
//Записываем количество возвращаемых переменных (одна переменная).
writeParameterAsSmallByte(byteArrayOutputStream, 1);
//Записываем индекс глобальной переменной.
writeGlobalIndex(byteArrayOutputStream, 0);
//Отправляем сообщение на EV3.
OutputStream outputStream = socket.getOutputStream();
//Сначала отправляем размер сообщения.
writeUShort(outputStream, byteArrayOutputStream.size());
//Затем отправляем само сообщение.
byteArrayOutputStream.writeTo(outputStream);

Теперь давайте с помощью этих функций рассмотрим ещё несколько примеров. Вот пример, в котором одновременно запускаются два двигателя, подключенных к порту B и C, делают 3 оборота, причём последние пол оборота двигатели постепенно замедляются:

//Записываем сообщение в память (размер посчитаем потом).
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//Записываем номер сообщения.
writeUShort(byteArrayOutputStream, 2);
//Записываем тип команды (прямая команда, ответ не требуется).
writeUByte(byteArrayOutputStream, 0x80);
//Записываем количество зарезервированных байт для переменных (здесь ничего не резервируется).
writeVariablesAllocation(byteArrayOutputStream, 0, 0);
//Записываем код команды.
writeCommand(byteArrayOutputStream, 0xAE/*opOutput_Step_Speed*/);
//Записываем номер модуля EV3.
writeParameterAsSmallByte(byteArrayOutputStream, 0);
//Записываем номера портов (порты B и C).
writeParameterAsSmallByte(byteArrayOutputStream, 0x02 | 0x04);
//Записываем мощность (от -100 до 100).
writeParameterAsByte(byteArrayOutputStream, 50);
//Записываем, сколько оборотов двигатель будет разгоняться (0 - разгоняться будет моментально).
writeParameterAsInteger(byteArrayOutputStream, 0);
//Записываем, сколько оборотов двигатель будет крутиться на полной скорости (2,5 оборота, т.е. 900 градусов).
writeParameterAsInteger(byteArrayOutputStream, 900);
//Записываем, сколько оборотов двигатель будет замедляться (0,5 оборота, т.е. 180 градусов).
writeParameterAsInteger(byteArrayOutputStream, 180);
//Записываем, нужно ли тормозить в конце (1 - тормозить, 0 - не тормозить).
writeParameterAsUByte(byteArrayOutputStream, 1);
//Отправляем сообщение на EV3.
OutputStream outputStream = socket.getOutputStream();
//Сначала отправляем размер сообщения.
writeUShort(outputStream, byteArrayOutputStream.size());
//Затем отправляем само сообщение.
byteArrayOutputStream.writeTo(outputStream);

А здесь мы узнаём, какие датчики и моторы подключены к EV3, и к каким портам (здесь описана лишь часть возможных вариантов, полный перечень см. в описании прошивки «EV3 Firmware Developer Kit» в пункте 5 «Device type list»):

//Записываем сообщение в память (размер посчитаем потом).
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//Записываем номер сообщения.
writeUShort(byteArrayOutputStream, 3);
//Записываем тип команды (прямая команда, требуется ответ).
writeUByte(byteArrayOutputStream, 0x00);
//Записываем количество зарезервированных байт для переменных
//(здесь резервируется место для 16-ти однобайтовых переменных).
writeVariablesAllocation(byteArrayOutputStream, 16, 0);
//Записываем команды для всех датчиков и моторов.
for (int i = 0; i < 8; i++) {
    //Записываем код команду.
    writeCommand(byteArrayOutputStream, 0x99/*opInput_Device*/, 0x05/*GET_TYPEMODE*/);
    //Записываем номер модуля EV3.
    writeParameterAsSmallByte(byteArrayOutputStream, 0);
    //Записываем номер порта (0x00-0x03 - порты 1-4, 0x10-0x13 - порты A-B).
    writeParameterAsSmallByte(byteArrayOutputStream, i < 4 ? i : (i + 12));
    //Записываем индекс переменной, куда будет сохранён тип устройства ввода.
    writeGlobalIndex(byteArrayOutputStream, i * 2);
    //Записываем индекс переменной, куда будет сохранён режим измерения.
    writeGlobalIndex(byteArrayOutputStream, i * 2 + 1);
}
//Отправляем сообщение на EV3.
OutputStream outputStream = socket.getOutputStream();
//Сначала отправляем размер сообщения.
writeUShort(outputStream, byteArrayOutputStream.size());
//Затем отправляем само сообщение.
byteArrayOutputStream.writeTo(outputStream);
 
//Считываем ответ.
InputStream inputStream = socket.getInputStream();
//Считываем первые два байта, чтобы узнать размер ответного сообщения.
int replySize = readUShort(inputStream);
//Создаём массив нужного размера.
byte[] replyBytes = new byte[replySize];
//Считываем остальную часть сообщения в массив.
inputStream.read(replyBytes, 0, replySize);
//Первые два байта (0x03 и 0x00) - это номер сообщения, третий байт (0x02) - это тип результата.
if (replyBytes[2] == 2) { //Если результат 2, значит команда выполнена успешно.
    //Смотрим, что к какому порту подключено.
    String portInfo = "";
    for (int i = 0; i < 8; i++) {
        switch (i) {
            case 0: portInfo += "1: "; break;
            case 1: portInfo += "2: "; break;
            case 2: portInfo += "3: "; break;
            case 3: portInfo += "4: "; break;
            case 4: portInfo += "A: "; break;
            case 5: portInfo += "B: "; break;
            case 6: portInfo += "C: "; break;
            case 7: portInfo += "D: "; break;
        }
        int type = replyBytes[3 + i * 2];
        int mode = replyBytes[4 + i * 2];
        switch (type) {
            case 7: {
                portInfo += "большой мотор EV3, ";
                switch (type) {
                    case 0: portInfo += "градусы; "; break;
                    case 1: portInfo += "обороты; "; break;
                    case 2: portInfo += "мощность; "; break;
                }
                break;
            }
            case 8: {
                portInfo += "средний мотор EV3, ";
                switch (type) {
                    case 0: portInfo += "градусы; "; break;
                    case 1: portInfo += "обороты; "; break;
                    case 2: portInfo += "мощность; "; break;
                }
                break;
            }
            case 16: {
                portInfo += "датчик касания EV3, ";
                switch (type) {
                    case 0: portInfo += "нажат; "; break;
                    case 1: portInfo += "щелчок; "; break;
                }
                break;
            }
            case 29: {
                portInfo += "датчик цвета EV3, ";
                switch (type) {
                    case 0: portInfo += "отражённый свет; "; break;
                    case 1: portInfo += "освещённость; "; break;
                    case 2: portInfo += "цвет; "; break;
                }
                break;
            }
            case 30: {
                portInfo += "ультразвуковой датчик EV3, ";
                switch (type) {
                    case 0: portInfo += "сантиметры; "; break;
                    case 1: portInfo += "дюймы; "; break;
                    case 2: portInfo += "обнаружение; "; break;
                }
                break;
            }
            case 32: {
                portInfo += "гироскоп EV3, ";
                switch (type) {
                    case 0: portInfo += "угол; "; break;
                    case 1: portInfo += "скорость; "; break;
                }
                break;
            }
            case 0xff: {
                //Неизвестный датчик или статус.
                portInfo += "неизвестно; ";
                break;
            }
            case 0x7e: {
                //В порт ничего не подключено.
                portInfo += "порт пустой; ";
                break;
            }
            case 0x7d: {
                //Порт инициализируется.
                portInfo += "инициализируется; ";
                break;
            }
            case 0x7f: {
                //Датчик подключен в порт моторов или наоборот.
                portInfo += "неправильное подключение; ";
                break;
            }
        }
    }
    Toast.makeText(getApplicationContext(),
            "Информация о датчиках и моторах: " + portInfo,
            Toast.LENGTH_LONG).show();
}

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

Итог

С помощью описанного в статье коммуникационного интерфейса вы сможете сделать удалённое управление модулем EV3 для любых операционных систем на любых языках программирования. А разработчики .NET, iOS и Node.js смогут использовать уже готовые API.

Надеюсь, что в статье я дал достаточное количество знаний для старта. Возможно, кто-нибудь благодаря этой статье, сделает удобный пульт управления для смартфона или компьютера, или создаст API для ещё какого либо языка программирования или платформы. Своими разработками или замечаниями можете поделиться с помощью комментариев.

Tags: Обзоры инструментов для программирования Учебники по программированию LEGO Mindstorms Education EV3

Комментарии   

Тимофей1
0 #11 Тимофей1 23.02.2019 14:56
Здравствуйте, спасибо за информацию! Я хочу управлять EV3 с мобильного телефона (Работал в MIT App Inventor, получилось, хочу перейти на более профессиональную среду) на какой среде лучше писать и на каком языке? Спасибо заранее!
Цитировать
Alex
0 #12 Alex 25.02.2019 19:27
Цитирую Тимофей1:
Здравствуйте, спасибо за информацию! Я хочу управлять EV3 с мобильного телефона (Работал в MIT App Inventor, получилось, хочу перейти на более профессиональную среду) на какой среде лучше писать и на каком языке? Спасибо заранее!

Здравствуйте. Язык и среду разработки лучше выбирать из ваших потребностей и предпочтений. Например, если вы разрабатываете для Android, то это может быть Android Studio, Visual Studio или Delphi/Builder, если для iOS, то Xcode, Visual Studio или Delphi/Builder, если для Windows, то Visual Studio или Delphi/Builder.
Цитировать
Magnus
+1 #13 Magnus 25.07.2019 20:48
Greate information.

This is what I did with your RoboCam app.

www.youtube.com/watch?v=VmEzJDlAiNw

MagI
Цитировать

Добавить комментарий