Беспроводной адаптер симулятора для 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 после недолгих размышлений отказался, уж больно там все мелкое.

Надо будет потом еще все это упихать в какую-нибудь коробочку.

Сначала картинки:
Общий вид

Arduino

Питание для NEO

Настройки

Ну и код скетча. Местами даже с комментариями. Все по простому, заворачивать в классы не стал, не тот проект.

#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();
  }
}

//-----------------------------------------------------------------------------------------------------------------------------------------//
  • 1480
Comments
Kormag

Победил все таки! Молодец!

rolic

Ага, спасибо 😃

mil-lion

SBUS мог не пойти по одной простой причине - нужен инвертер на входе Arduino. Можно было попробовать через транзистор сделать. Я брал неинвертированный SBUS с приемника перед транзистором, а то не работало. Здесь наверное так же.
А так очень здорово что получилось. Будет время - попробую с SBUS замутить.

rolic

Возможно, что именно так. Но и DSM вполне устраивает.

Спасибо за идею ))

AndreyI

Повторил, только для оригинального DSMX сателлита. Пришлось немного переделать определение начала пакета что бы корректно работало с сателлитом. И что то не получилось завести восьмой канал. Увеличение “#define SRC_FRAME_CHAN_COUNT” только все портит. Но т.к. не больно то и надо было, решил не разбираться и оставил семь каналов.
Спасибо за труды!

rolic

В пакете идет семь каналов, это жесткое ограничение. Если я правильно понял, то когда их больше, то остальные идут во втором пакете. Специально скопировал из спецификации предупреждение, что порядковый номер в пакете не обязательно равен номеру канала.

AndreyI

И точно, там не все просто… До этого читал мануал через строчку 😃 Согласен что остальные каналы не очень то и нужны, мне и шести каналов хватило.

Забыл упомянуть, т.к. сателлит питается от 3.3в, пришлось на Леонардо заменить стабилизатор.

rolic

Еще небольшой лайфхак. Даже при питании напрямую от USB, NEO продолжает жаловаться на напряжение ниже 4.5V, но хотя бы инициализируется и работает без проблем. Вообще, исправить это несложно, может и сделаю. Но это привело к другой проблеме. Все эти стоны пишутся в логи. А флеш, как известно, на VBC аж целых 8 мега байт, из которых свободно около 5.5. Пока я возился с программированием и настройкой, а потом наслаждался полетами в симе, все это свободное место было съедено логами.

В воскресенье вернулся с поля, дай думаю посмотрю, что там и как было. Опаньки - а файлики-то все пустые, а места-то свободного 0. Зато директория с логами от сима ого-го.

Первой идеей было сделать директорию логов сима read only. Не помогло, VBC на это кладет. Хорошо, удалил директорию и создал файл с таким же именем. Вжик сказала японская пила, ну в смысле немецкий пульт и перестал писать логи для сима. Профит.

rolic

Писал выше, что при питании Neo от USB напрямую напряжение получается на пределе по нижней границе. Решил все же довести до полного феншуя и купил пяток вот таких step up. Шли почти месяц…

Сначала при попытке предварительной настройки модуля ждало легкое разочарование. Оба проверенных на выходе давали на 0.1V меньше входа (step up, ага) независимо от того, что накручено подстроечником, который согласно описанию и должен регулировать вольтаж. Хотел уже без особой надежды проверять остальные, но потом немного подумал, посмотрел что там и как, да и решил попробовать закоротить пару ног (1 и 2) у подстроечника. И все стало замечательно регулироваться. Не знаю, это такая нигде не описанная фича или мне пришли модули из криво разведенной партии. Выставил 5V и все замечательно работает. VBC стабильно показывает 5.0 и не жалуется в лог на низкое напряжение.

Но логи для сима все равно потом снова отключил, как писал чуть выше, нечего место занимать.

Kormag

Настроил сим хели х по сбас. Все работает замечательно. Правда я настроил 4 канала: элероны, элеватор, рудер и шаг. Свитчи настраивать не стал, ибо и так все ясно.
Нео по мимо юсб rx2sim еще питается от лифе батарейки. Надо как то решить вопрос с питанием и с записью логов. По логам наверно можно сделать, как у тебя в дневнике.
Еще вопрос по мощности передатчика. В всим он работает в маломощном режиме.
Короче благодаря тебе все в принципе получилось. Кстати по сбас и 5мс ползунки в симе как влитые. По ппм не пробавал.

rolic

Надо было с самого начала выложить - github.com/alrusov/wireless-sim-dsmx