��ривет. Расскажу про то, как сделал машинку на 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-порта, очень громоздко. Я предлагаю свое решение. Есть идеи? Пиши, что думаешь по этому поводу.
