Arduino + Unity. Радио fpv-машинка на геймпаде

Привет. Расскажу про то, как сделал машинку на Arduino-контроллере, а Unity принимал сигналы с геймпада, управлял машиной по радиоканалу, отображал пользовательский интерфейс и изображение fpv-камеры.

e8707418725562e6d98f6b669d22c620.jpeg

Зачем

Целью проекта было
• Опробовать связку Arduino и Unity
• Управлять машиной дальнобойным радиосигналом вместо вайфай
• Принимать видео-изображение
• Управлять посредством геймпада
• Все через одно окно Unity

88d59877f068130bc23402ae942cb805.PNG

Почему

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

Алгоритм работы

  1. Игровой движок принимает сигнал с геймпада

  2. Преобразовывает Vector2 в командную строку и отправляет на подключенную по usb ардуину

  3. Ардуина имеет модуль передатчика, который отправляет команду по радиоканалу

  4. Бортовая ардуина с модулем приемника получает радиосигнал, преобразует для подачи напряжения на мотор-колеса и сервомашинки

  5. FPV-камера на борту, передает аналоговый видео-сигнал, работает независимо от остальной системы

  6. Приемник видеосигнала подключен к пк, оцифровывает видео и передает в Unity

  7. То, что видит камера, отображается на экране

  8. На экран накладывается пользовательский интерфейс

Подробней

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

IEnumerator update()
    {
        while(true)
        {
            float[] axes = { Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), Input.GetAxis("Stick Y"), Input.GetAxis("Stick X") };

            byte[] signals = new byte[4];
            signal = "";
            for (int i = 0; i < axes.Length; i++)
            {
                signals[i] = (byte)((axes[i] + 1) / 2 * _maxIntSignal); // 0 - 255
                signal += signals[i] + (i == axes.Length - 1 ? ";" : ",");
            }
            controller.SendSerialMessage(signal);

            yield return new WaitForSeconds(1 / (float)_signalRate);
        }
    }

Для передачи радио сигнала с компьютера на бортовую ардуинку, я использовал еще одну плату, которую подключал по usb порту к ноутбуку.

Кстати, вы знали, что существуют встроенные радио-модули прямо в плату ардуино? Я нет. Я использовал плату Arduino Uno и радио модуль nrf24l01

Для работы этой части алгоритма я использовал два скрипта. Один со стороны Unity, который работает с ардуиной, преобразует игровой Vector2 и остальные сигналы в строку, удобную для передачи по радио-каналу. Использовал ассет Ardity.

256 байт для управления, Карл! Каждая ось джойстика занимает 1 байт в радиопередаче, осталось 253! Для чего еще использовать остаток? Например, для булевых команд: включение фар, поворотников, поршней и сервомашинок, открыть люк или поднять кран…

Второй со стороны ардуино для передачи сигнала.

код ардуино передатчика

#include 
#include 
#include 

RF24 radio(9, 10);
byte address[][6] = {"1Node", "2Node", "3Node", "4Node", "5Node", "6Node"}; //возможные номера труб
byte values[4] = {0,0,0,0};


void setup()
{
  Serial.begin(9600);
  Serial.setTimeout(10);
  txSetup();
  Serial.println("Arduino is alive!!");
  delay(100);
}


void loop()
{
  while (Serial.available())
  {
    String input = Serial.readStringUntil(";");
    SetArray(input);
    radio.write(&values, sizeof(values));
  }
}


void SetArray(String input)
{
  input += ".";
  if (!isDigit(input[0])) return;
  
  int intIndex = 0;
  String buf = "";

  for(int i = 0; i < input.length(); i++)
  {
    if (isDigit(input[i]))
    {
      buf += input[i];
    }
    else
    {
      values[intIndex] = buf.toInt();
      buf = "";
      intIndex++;
    }
  }
}


void txSetup()
{
  radio.begin();              // активировать модуль
  //radio.setAutoAck(1);        // режим подтверждения приёма, 1 вкл 0 выкл
  //radio.setRetries(0, 15);    // (время между попыткой достучаться, число попыток)
  //radio.enableAckPayload();   // разрешить отсылку данных в ответ на входящий сигнал
  radio.setPayloadSize(4);   // размер пакета, в байтах // 32

  radio.openWritingPipe(address[5]);  // мы - труба 0, открываем канал для передачи данных
  radio.setChannel(0x79);             // выбираем канал (в котором нет шумов!)

  radio.setPALevel (RF24_PA_LOW);   // уровень мощности передатчика. На выбор RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX
  radio.setDataRate (RF24_250KBPS); // скорость обмена. На выбор RF24_2MBPS, RF24_1MBPS, RF24_250KBPS
  //должна быть одинакова на приёмнике и передатчике!
  //при самой низкой скорости имеем самую высокую чувствительность и дальность!!

  radio.powerUp();        // начать работу
  radio.stopListening();  // не слушаем радиоэфир, мы передатчик
}

Для получения и преобразования радио-сигнала написал скетч для второй ардуинки. Колеса работают по схеме танка — два ведущих с разницей скоростей создают поворот (или разворот на месте). Ардуино подает сигналы на драйвер MRL298, а он уже распределяет напряжение на моторы.

код ардуино на борту машины

#include 
#include 
#include 
#include 

#define motor1in 2
#define motor1pwm 5
#define motor2in 4
#define motor2pwm 6
#define fpvYpin 3

RF24 radio(9, 10);
byte address[][6] = {"1Node", "2Node", "3Node", "4Node", "5Node", "6Node"}; //возможные номера труб
byte values[4] = {0, 0, 0, 0};
Servo fpvY;


void setup() {
  fpvY.attach(fpvYpin);
  Serial.begin(9600);
  Serial.setTimeout(10);
  motorSetup();
  rxSetup();
  delay(100);
}


void loop()
{
  if (radio.available())
  {
    while (radio.available())
    {
      radio.read(&values, sizeof(values));

      setMotors(map(values[0], 0, 255, -255, 255), map(values[1], 0, 255, -255, 255));
      fpvY.write(map(values[3], 0, 255, 45, 135));
    }
  }
}


void setMotors(int inx, int iny) // -255 to 255
{
  setMotor(iny + inx, motor1pwm, motor1in);
  setMotor(iny - inx, motor2pwm, motor2in);
}
void setMotor(int mspeed, int pinPwm, int pinIn)
{
  if (mspeed > 0) // forward
  {
    analogWrite(pinPwm, mspeed);
    digitalWrite(pinIn, 0);
  }
  else if (mspeed < 0) // back
  {
    analogWrite(pinPwm, 255 + mspeed);
    digitalWrite(pinIn, 1);
  }
  else // brake
  {
    digitalWrite(pinPwm, 0);
    digitalWrite(pinIn, 0);
  }
}


void motorSetup()
{
  pinMode(motor1in, OUTPUT);
  pinMode(motor2in, OUTPUT);

  pinMode(motor1pwm, OUTPUT);
  pinMode(motor2pwm, OUTPUT);
}


void rxSetup()
{
  radio.begin();              // активировать модуль
  //radio.setAutoAck(1);        // режим подтверждения приёма, 1 вкл 0 выкл
  //radio.setRetries(0, 15);    // (время между попыткой достучаться, число попыток)
  //radio.enableAckPayload();   // разрешить отсылку данных в ответ на входящий сигнал
  radio.setPayloadSize(4);   // размер пакета, в байтах // 32

  radio.openReadingPipe(1, address[5]);   // хотим слушать трубу 0
  radio.setChannel(0x79);     // выбираем канал (в котором нет шумов!)

  radio.setPALevel (RF24_PA_LOW);   // уровень мощности передатчика. На выбор RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX
  radio.setDataRate (RF24_250KBPS); // скорость обмена. На выбор RF24_2MBPS, RF24_1MBPS, RF24_250KBPS
  //должна быть одинакова на приёмнике и передатчике!
  //при самой низкой скорости имеем самую высокую чувствительность и дальность!!

  radio.powerUp();        // начать работу
  radio.startListening(); // начинаем слушать эфир, мы приёмный модуль
}

Note Bene: контроллер по дефолту настроен на принятие сигналов не чаще чем 1 секунда, что неприемлемо для нон-стоп управления с контроллера.

Для передачи видеосигнала использовал камеру 3в1 от Eachine (камера, передатчик, антенна) на борту машинки, которая работает независимо от остальной системы машинки, живет своей жизнью.

Для получения видеосигнала использовал отдельный приемник, подключенный через usb к ноуту. Приемник EasyCAP преобразует аналоговый видео-поток в цифровой и распознается ноутбуком как вэб-камера, а в игровом движке есть решения по работе с такими устройствами.

код для видеопотока

using System.Linq;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(RawImage))]
public class WebTexture : MonoBehaviour
{
    RawImage _raw;
    WebCamTexture _texture;

    private void Start()
    {
        foreach (var c in WebCamTexture.devices)
            Debug.Log(c.name);

        if (WebCamTexture.devices.Length > 0)
            SetTexture(WebCamTexture.devices[0]);
    }

    public void _SwitchWebCam()
    {
        var devicesEnumerator = WebCamTexture.devices.Where(x => new WebCamTexture(x.name) != null); // TODO How check not virtual cam?
        var devices = devicesEnumerator.ToArray();
        if (devices.Length > 1)
        {
            int usedDeviceIndex = 0;
            for (int i = 0; i < devices.Length; i++)
                if (devices[i].name == _texture.deviceName)
                    usedDeviceIndex = i;

            int newDeviceIndex = usedDeviceIndex == (devices.Length - 1) ? 0 : usedDeviceIndex + 1;
            SetTexture(devices[newDeviceIndex]);
        }
    }

    private void SetTexture(WebCamDevice device)
    {
        if (_texture != null && _texture.isPlaying)
            _texture.Stop();

        _texture = new WebCamTexture(device.name);
        _texture.requestedFPS = 30;
        _raw = GetComponent();
        _raw.texture = _texture;
        _texture.Play();
    }
}

Итого получаем сырой продукт с огромным потенциалом.

fbd23d2cd71f1d6672a6b56611a57e4a.gif

Это моя первая публикация. Посмотрим, что из этого получится. Проект лежит на GitHub в свободном доступе. Не забудьте скачать ассет для работы с Serial-портом.

Не отрицаю, что, вероятно, есть более лаконичные решения для того, чтобы подружить комп с радио-машиной, при этом не используя сразу два usb-порта, очень громоздко. Я предлагаю свое решение. Есть идеи? Пиши, что думаешь по этому поводу.

© Habrahabr.ru