Привет. Расскажу про то, как сделал машинку на Arduino-контроллере, а Unity принимал сигналы с геймпада, управлял машиной по радиоканалу, отображал пользовательский интерфейс и изображение fpv-камеры.
Зачем
Целью проекта было
• Опробовать связку Arduino и Unity
• Управлять машиной дальнобойным радиосигналом вместо вайфай
• Принимать видео-изображение
• Управлять посредством геймпада
• Все через одно окно Unity
Почему
На канале Гайвера я познакомился с аппаратурой коптеров и с ардуино контроллерами. Интерес со временем пропал в силу интереса к игровому движку и индустрии в целом. Что же могло еще случиться, как не попытка объединить полученный ранее опыт.

Алгоритм работы
Игровой движок принимает сигнал с геймпада
Преобразовывает Vector2 в командную строку и отправляет на подключенную по usb ардуину
Ардуина имеет модуль передатчика, который отправляет команду по радиоканалу
Бортовая ардуина с модулем приемника получает радиосигнал, преобразует для подачи напряжения на мотор-колеса и сервомашинки
FPV-камера на борту, передает аналоговый видео-сигнал, работает независимо от остальной системы
Приемник видеосигнала подключен к пк, оцифровывает видео и передает в Unity
То, что видит камера, отображается на экране
На экран накладывается пользовательский интерфейс
Подробней
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-порта, очень громоздко. Я предлагаю свое решение. Есть идеи? Пиши, что думаешь по этому поводу.
