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

Зачем

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

Почему

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

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

  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 <nRF24L01.h>
#include <RF24.h>
#include <SPI.h>

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 <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <Servo.h>

#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<RawImage>();
        _raw.texture = _texture;
        _texture.Play();
    }
}

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

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

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