Беспроводной адаптер симулятора для Vbar Control
Как известно, у VBC нет PPM выхода, зато есть USB, через который можно напрямую без всяких переходников пользоваться моим любимым Heli-X. Шнурок немного мешает, плюс есть некоторое опасение рано или поздно разворотить разъем в передатчике, поэтому хотелось бы иметь беспроводной вариант.
Когда-то ради интереса решил попробовать вывести PPM через Vbar NEO с помощью приложения Macrоcells. Для проверки использовал давно пылящийся на полке легальный Phoenix (мир праху его), который на входе в свою волшебную коробочку требует именно PPM. Все заработало. Ну попробовал и забыл, Phoenix как симулятор мне не интересен.
Но вот недавно вот тут Игорь (mil-lion) написал про свой беспроводной “свисток” для Тараниса, сделанный из приемника SBUS и Arduino. Описание есть у него в дневнике. А первоначальная идея (правда c PPM) была взята вот отсюда.
Решил и я попробовать, благо с помощью Macrocells можно и SBUS выдать. И неиспользуемый Vbar NEO у меня давно уже имеется. Первый облом случился по причине того, что имеющиеся у меня Arduino Nano через свой USB разъем могут только программироваться, использовать его для других целей нельзя. Пришлось купить вот такой.
В понедельник забрал его на почте и стал экспериментировать. Добиться чего-то от SBUS не получилось. Может я что-то не понял, может у NEO он какой-то специфический (очень может быть - см. ниже). Решил подойти с другой стороны. Macrocells ведь умеют выдавать и другие протоколы, в частности сателлита Spe k trum DSM* (почему-то обозначенный у них в меню как SPE C). Но не суть. И протокол проще сам по себе - нет этой 11-битной упаковки, а данные идут нормальными 16 битами, и для меня Spektrum как-то привычнее. Спецификацию взял вот тут. И 7 каналов - ровно столько, сколько надо (5 стандартных - THRO, AILE, ELEV, RUDD, PITCH и два переключателя - банки и спасалка). В общем, после пары часов экспериментов данные стали нормально приниматься, основная проблема была с тем, чтобы поймать начало пакета. Но быстро выяснилось, что в поле fades все время приходит одно и тоже - два байта 0xf3 и 0x17 (243 и 23). Подозреваю, что это опять специфика NEO. Как это соотносится со спецификацией я не понял, там другое написано, зато появился маркер начала пакета.
Адаптировать код Игоря не стал, как известно проще написать своё, чем переписать чужое. Взял оттуда только код для джойстика.
Что получилось можно увидеть ниже. Работает отлично. По ощущениям - лучше, чем по проводу. Ну и очень приятно то, что все настраивается в VBC - полки, экспоненты… Главное - работает hold и его хитрые комбинации с переключателем Motor, чего по USB добиться в Heli-x не удалось. Скриншоты своих настроек прикладываю.
Питание (+) для NEO сначала пытался снимать с VCC Arduino, но там оно оказалось 4.2V и NEO отказывался с ним инициализироваться, бесконечно жалуясь в лог на напряжение ниже 4.5V. Взял из кабеля USB, аккуратно вскрыв оболочку и подпаяв туда провод. От идеи подпаяться к разъему на плате Arduino после недолгих размышлений отказался, уж больно там все мелкое.
Надо будет потом еще все это упихать в какую-нибудь коробочку.
Ну и код скетча. Местами даже с комментариями. Все по простому, заворачивать в классы не стал, не тот проект.
#include <Arduino.h>
#include <Joystick.h>
//-----------------------------------------------------------------------------------------------------------------------------------------//
// Use to enable output debug information to serial
//#define DEBUG
#ifdef DEBUG
#define debugPort Serial
#include <stdarg.h>
void debugInit() {
debugPort.begin(115200);
}
void debugPrintf(char *format, ...) {
char buf[80];
va_list ap;
va_start(ap, format);
vsnprintf(buf, sizeof(buf), format, ap);
va_end(ap);
debugPort.print(buf);
}
#else
#define debugInit() ;
#define debugPrintf(...) ;
#endif
//-----------------------------------------------------------------------------------------------------------------------------------------//
// Common
typedef uint8_t byte_t;
#define PHASE_WAIT_FOR_FADE_1 0
#define PHASE_WAIT_FOR_FADE_2 1
#define PHASE_RECEIVE_DATA 2
#define PHASE_PROCESSING_DATA 3
uint8_t phase = PHASE_WAIT_FOR_FADE_1;
//-----------------------------------------------------------------------------------------------------------------------------------------//
// Source
// Specification:
// Spektrum channels layout
enum {
SRC_CHANID_THROTTLE,
SRC_CHANID_AILERON,
SRC_CHANID_ELEVATOR,
SRC_CHANID_RUDDER,
SRC_CHANID_PITCH,
SRC_CHANID_AUX_1,
SRC_CHANID_AUX_2,
SRC_CHANID_AUX_3,
SRC_CHANID_AUX_4,
SRC_CHANID_AUX_5,
SRC_CHANID_AUX_6,
SRC_CHANID_AUX_7,
SRC_CHANID_LAST
};
// Port settings
#define srcPort Serial1
#define SRC_PORT_BAUDRATE 115200 // 125000 according to the specification, but doesn't work...
#define SRC_PORT_OPTIONS SERIAL_8N1
// Obtained after the data stream monitoring.
// I didn't understand why such data and I didn't find anything like this in the specification. Perhaps this is a feature of the VBC.
#define SRC_FADE_1 0xF3 // 243
#define SRC_FADE_2 0x17 // 23
#define SRC_FRAME_CHAN_COUNT 7
#define SRC_MASK_2048_CHANID 0x7800
#define SRC_MASK_2048_POS 0x07FF
byte_t buf[sizeof(uint16_t)*SRC_FRAME_CHAN_COUNT];
uint8_t bufIndex = 0;
uint16_t channels[SRC_CHANID_LAST];
void srcInit() {
srcPort.begin(SRC_PORT_BAUDRATE, SRC_PORT_OPTIONS);
}
//-----------------------------------------------------------------------------------------------------------------------------------------//
// Joystick
#define PULSE_WIDTH_MIN 344 // Minimal pulse (experimental)
#define PULSE_WIDTH_MAX 1704 // Maximal pulse (experimental)
#define PULSE_WIDTH_MID 1024 // Middle pulse = (MIN_PULSE_WIDTH + MAX_PULSE_WIDTH)/2
#define PULSE_JITTER 1 // Dead zone. If possible, do not use it.
#define USB_STICK_MIN -32767
#define USB_STICK_MAX 32767
#define USB_STICK_MID 0 // (USB_STICK_MIN + USB_STICK_MAX)/2
// Create the Joystick
Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_JOYSTICK, 2, 0, true, true, true, true, true, true, false, false, false, false, false);
void joyInit() {
Joystick.setXAxisRange(USB_STICK_MIN, USB_STICK_MAX);
Joystick.setYAxisRange(USB_STICK_MIN, USB_STICK_MAX);
Joystick.setZAxisRange(USB_STICK_MIN, USB_STICK_MAX);
Joystick.setRxAxisRange(USB_STICK_MIN, USB_STICK_MAX);
Joystick.setRyAxisRange(USB_STICK_MIN, USB_STICK_MAX);
Joystick.setRzAxisRange(USB_STICK_MIN, USB_STICK_MAX); // Not used. But when I disabled it (and set "false" in the corresponding Joystick parameter), then there were some glitches. Didn't understand yet.
Joystick.begin(false);
}
// Convert a value from range [PULSE_WIDTH_MIN, PULSE_WIDTH_MAX] to range [USB_STICK_MIN, USB_STICK_MAX]
uint16_t joyValue(uint16_t rcVal) {
if (rcVal > (PULSE_WIDTH_MID + PULSE_JITTER)) {
return constrain(
map(rcVal, PULSE_WIDTH_MID, PULSE_WIDTH_MAX, USB_STICK_MID, USB_STICK_MAX),
USB_STICK_MID,
USB_STICK_MAX
);
}
else if (rcVal < (PULSE_WIDTH_MID - PULSE_JITTER)) {
return constrain(
map(rcVal, PULSE_WIDTH_MIN, PULSE_WIDTH_MID, USB_STICK_MIN, USB_STICK_MID),
USB_STICK_MIN,
USB_STICK_MID
);
}
else {
return USB_STICK_MID;
}
}
void joySet() {
Joystick.setXAxis(joyValue(channels[SRC_CHANID_AILERON]));
Joystick.setYAxis(joyValue(channels[SRC_CHANID_ELEVATOR]));
Joystick.setZAxis(joyValue(channels[SRC_CHANID_RUDDER]));
Joystick.setRxAxis(joyValue(channels[SRC_CHANID_THROTTLE]));
Joystick.setRyAxis(joyValue(channels[SRC_CHANID_PITCH]));
Joystick.setRzAxis(USB_STICK_MID); // Not used
Joystick.setButton(0, channels[SRC_CHANID_AUX_1] > PULSE_WIDTH_MID);
Joystick.setButton(1, channels[SRC_CHANID_AUX_2] > PULSE_WIDTH_MID);
}
//-----------------------------------------------------------------------------------------------------------------------------------------//
// Application initialization procedure
void setup() {
debugInit();
srcInit();
joyInit();
}
//-----------------------------------------------------------------------------------------------------------------------------------------//
// Source data port handler
void getDataFromSrc() {
while ((phase != PHASE_PROCESSING_DATA) && (srcPort.available() > 0)) {
byte_t b = srcPort.read();
switch (phase) {
case PHASE_WAIT_FOR_FADE_1:
//debugPrintf("-%d", b);
if (b == SRC_FADE_1) {
phase = PHASE_WAIT_FOR_FADE_2;
}
break;
case PHASE_WAIT_FOR_FADE_2:
//debugPrintf("-%d", b);
if (b == SRC_FADE_2) {
phase = PHASE_RECEIVE_DATA;
bufIndex = 0;
//debugPrintf("\n");
} else {
phase = PHASE_WAIT_FOR_FADE_1;
}
break;
case PHASE_RECEIVE_DATA:
buf[bufIndex++] = b;
if (bufIndex == sizeof(buf)) {
phase = PHASE_PROCESSING_DATA;
/*
for (uint8_t i = 0; i < sizeof(buf); i++) {
debugPrintf("%d ", buf[i]);
}
debugPrintf("\n");
*/
}
break;
}
}
}
//-----------------------------------------------------------------------------------------------------------------------------------------//
// Main loop
void loop() {
getDataFromSrc();
if (phase == PHASE_PROCESSING_DATA) {
phase = PHASE_WAIT_FOR_FADE_1;
for (uint8_t i = 0; i < SRC_FRAME_CHAN_COUNT; i++) {
// big-endian!
uint16_t data = (buf[i * 2] << 8) | buf[i * 2 + 1];
uint16_t chan = (data & SRC_MASK_2048_CHANID) >> 11;
uint16_t pos = data & SRC_MASK_2048_POS;
/*
One cannot assume that a packet will have the same data in the same index in the servo[] array in each frame; it is
necessary that the Channel ID field be examined for each index in every packet.
*/
if (chan >= SRC_CHANID_LAST) {
debugPrintf("BAD CHAN %d\n", chan);
} else {
channels[chan] = pos;
debugPrintf("%d=%d ", chan, pos);
}
}
debugPrintf("\n");
joySet();
Joystick.sendState();
}
}
//-----------------------------------------------------------------------------------------------------------------------------------------//