Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions companion/src/firmwares/edgetx/yaml_generalsettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,14 @@ const YamlLookupTable uartModeLut = {
{ GeneralSettings::AUX_SERIAL_TELE_MIRROR, "TELEMETRY_MIRROR" },
{ GeneralSettings::AUX_SERIAL_TELE_IN, "TELEMETRY_IN" },
{ GeneralSettings::AUX_SERIAL_SBUS_TRAINER, "SBUS_TRAINER" },
{ GeneralSettings::AUX_SERIAL_SBUS_TRAINER_INV, "SBUS_TRAINER_INV" },
{ GeneralSettings::AUX_SERIAL_LUA, "LUA" },
{ GeneralSettings::AUX_SERIAL_CLI, "CLI" },
{ GeneralSettings::AUX_SERIAL_GPS, "GPS" },
{ GeneralSettings::AUX_SERIAL_DEBUG, "DEBUG" },
{ GeneralSettings::AUX_SERIAL_SPACEMOUSE, "SPACEMOUSE" },
{ GeneralSettings::AUX_SERIAL_EXT_MODULE, "EXT_MODULE" },
{ GeneralSettings::AUX_SERIAL_CRSF_TRAINER, "CRSF_TRAINER" },
};

const YamlLookupTable antennaModeLut = {
Expand Down
14 changes: 14 additions & 0 deletions companion/src/firmwares/generalsettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,10 @@ QString GeneralSettings::serialModeToString(int value)
return tr("SpaceMouse");
case AUX_SERIAL_EXT_MODULE:
return tr("External module");
case AUX_SERIAL_SBUS_TRAINER_INV:
return tr("SBUS Trainer Inv.");
case AUX_SERIAL_CRSF_TRAINER:
return tr("CRSF Trainer");
default:
return CPN_STR_UNKNOWN_ITEM;
}
Expand Down Expand Up @@ -613,8 +617,18 @@ AbstractStaticItemModel * GeneralSettings::serialModeItemModel()
if (i == AUX_SERIAL_EXT_MODULE) {
contexts &= ~(AUX2Context | VCPContext);
}
else if (i == AUX_SERIAL_SBUS_TRAINER_INV &&
Boards::getCapability(getCurrentBoard(), Board::IsF4)) {
// needs the USART to invert RX, which F4 cannot do
contexts = 0;
}
else if (i == AUX_SERIAL_CRSF_TRAINER) {
// driven from a receive callback, which only USB-VCP provides
contexts &= VCPContext;
}
else if (i == AUX_SERIAL_TELE_IN ||
i == AUX_SERIAL_SBUS_TRAINER ||
i == AUX_SERIAL_SBUS_TRAINER_INV ||
i == AUX_SERIAL_GPS ||
i == AUX_SERIAL_SPACEMOUSE ||
i == AUX_SERIAL_EXT_MODULE) {
Expand Down
2 changes: 2 additions & 0 deletions companion/src/firmwares/generalsettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ class GeneralSettings {
AUX_SERIAL_DEBUG,
AUX_SERIAL_SPACEMOUSE,
AUX_SERIAL_EXT_MODULE,
AUX_SERIAL_SBUS_TRAINER_INV,
AUX_SERIAL_CRSF_TRAINER,
AUX_SERIAL_COUNT
};

Expand Down
15 changes: 11 additions & 4 deletions companion/src/firmwares/modeldata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1853,10 +1853,17 @@ bool ModelData::isTrainerModeAvailable(const GeneralSettings & generalSettings,
moduleData[1].protocol != PULSES_OFF)
return false;

if (value == TRAINER_MODE_MASTER_SERIAL &&
(generalSettings.serialPort[GeneralSettings::SP_AUX1] != GeneralSettings::AUX_SERIAL_SBUS_TRAINER &&
generalSettings.serialPort[GeneralSettings::SP_AUX2] != GeneralSettings::AUX_SERIAL_SBUS_TRAINER))
return false;
if (value == TRAINER_MODE_MASTER_SERIAL) {
// inverted SBUS relies on the USART inverting RX, which F4 cannot do
auto isSbusTrainer = [&](unsigned int mode) {
return mode == GeneralSettings::AUX_SERIAL_SBUS_TRAINER ||
(mode == GeneralSettings::AUX_SERIAL_SBUS_TRAINER_INV &&
!Boards::getCapability(board, Board::IsF4));
};
if (!isSbusTrainer(generalSettings.serialPort[GeneralSettings::SP_AUX1]) &&
!isSbusTrainer(generalSettings.serialPort[GeneralSettings::SP_AUX2]))
return false;
}

if ((value == TRAINER_MODE_MASTER_BLUETOOTH || value == TRAINER_MODE_SLAVE_BLUETOOTH) &&
!IS_TARANIS_X9E(board) &&
Expand Down
82 changes: 82 additions & 0 deletions companion/src/tests/serial_mode_yaml_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/

// Serial port modes are stored by name. A name Companion does not know
// decodes to OFF, and is then silently lost when the settings are saved, so
// every mode the firmware can write must be known here.

#include "gtests.h"

#include <QByteArray>

#include "firmwares/eeprominterface.h"
#include "firmwares/edgetx/edgetxinterface.h"
#include "firmwares/generalsettings.h"
#include "firmwares/modeldata.h"

namespace {

void setFirmware(const char* flavour)
{
Firmware::setCurrentVariant(Firmware::getFirmwareForFlavour(flavour));
ASSERT_NE(getCurrentFirmware(), nullptr) << flavour << " firmware must be registered";
}

} // namespace

TEST(SerialModeYaml, trainerModesRoundTrip)
{
setFirmware("tx15");

GeneralSettings in;
in.serialPort[GeneralSettings::SP_AUX1] = GeneralSettings::AUX_SERIAL_SBUS_TRAINER_INV;
in.serialPort[GeneralSettings::SP_VCP] = GeneralSettings::AUX_SERIAL_CRSF_TRAINER;

QByteArray yaml;
ASSERT_TRUE(writeRadioSettingsToYaml(in, yaml));
EXPECT_TRUE(yaml.contains("SBUS_TRAINER_INV")) << yaml.constData();
EXPECT_TRUE(yaml.contains("CRSF_TRAINER")) << yaml.constData();

GeneralSettings out;
ASSERT_TRUE(loadRadioSettingsFromYaml(out, yaml));
EXPECT_EQ(out.serialPort[GeneralSettings::SP_AUX1],
(unsigned)GeneralSettings::AUX_SERIAL_SBUS_TRAINER_INV);
EXPECT_EQ(out.serialPort[GeneralSettings::SP_VCP],
(unsigned)GeneralSettings::AUX_SERIAL_CRSF_TRAINER);
}

// Master/Serial needs an SBUS trainer port. The inverted variant only counts
// where the firmware offers it, i.e. not on F4 (see serialGetSbusTrainerPort()).
TEST(SerialModeYaml, masterSerialAcceptsInvertedSbusOffF4Only)
{
GeneralSettings gs;
gs.serialPort[GeneralSettings::SP_AUX1] = GeneralSettings::AUX_SERIAL_SBUS_TRAINER_INV;
ModelData model;

setFirmware("tx15"); // STM32H7
EXPECT_TRUE(model.isTrainerModeAvailable(gs, getCurrentFirmware(), TRAINER_MODE_MASTER_SERIAL));

setFirmware("tx16s"); // STM32F4
EXPECT_FALSE(model.isTrainerModeAvailable(gs, getCurrentFirmware(), TRAINER_MODE_MASTER_SERIAL));

gs.serialPort[GeneralSettings::SP_AUX1] = GeneralSettings::AUX_SERIAL_SBUS_TRAINER;
EXPECT_TRUE(model.isTrainerModeAvailable(gs, getCurrentFirmware(), TRAINER_MODE_MASTER_SERIAL));
}
159 changes: 159 additions & 0 deletions radio/src/crsf_trainer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/

#include "crsf_trainer.h"

#include <string.h>

#include "crc.h"
#include "edgetx.h"
#include "telemetry/crossfire.h"
#include "telemetry/telemetry.h"

//
// CRSF frame: [addr][len][type][payload...][crc8]
//
// len counts type + payload + crc, so the frame occupies len + 2 bytes.
// The CRC covers type + payload, i.e. bytes [2 .. len].
//
// A frame may be split over several USB packets and several frames may arrive
// in one, so the assembler keeps its partial-frame state across chunks.
//
// Invariant: _crsf_len == 0, or _crsf_buf[0] is a valid address byte.
//
#define CRSF_MIN_FRAME_LEN 3

// len byte of a CHANNELS_ID frame: type + 16 x 11-bit channels (22) + crc
#define CRSF_CHANNELS_FRAME_LEN 24

static uint8_t _crsf_buf[TELEMETRY_RX_PACKET_SIZE];
static uint8_t _crsf_len = 0;

static const etx_serial_driver_t* _crsf_drv = nullptr;
static void* _crsf_ctx = nullptr;

static inline bool _validAddr(uint8_t b)
{
return b == RADIO_ADDRESS || b == UART_SYNC;
}

static inline bool _lenIsSane(uint32_t len)
{
// at least type + payload + crc, and must fit the buffer
return len > 2 && len < TELEMETRY_RX_PACKET_SIZE - 1;
}

static bool _checkCRC(const uint8_t* frame)
{
uint8_t len = frame[1];
return crc8(&frame[2], len - 1) == frame[len + 1];
}

// Drop the leading byte of a rejected frame and re-sync on the next valid
// address byte held in what is left. Never discards the whole buffer: a valid
// frame may well have started inside it.
static void _resync()
{
uint8_t i = 1;
while (i < _crsf_len && !_validAddr(_crsf_buf[i])) i++;

_crsf_len -= i;
if (_crsf_len > 0) {
memmove(_crsf_buf, _crsf_buf + i, _crsf_len);
}
}

void crsfTrainerReceiveData(uint8_t* data, uint32_t len)
{
// The model is not asking for CRSF trainer input: stay out of the way.
if (g_model.trainerData.mode != TRAINER_MODE_CRSF) {
_crsf_len = 0;
return;
}

while (len > 0) {
// Hunt for an address byte while no frame is being assembled
if (_crsf_len == 0 && !_validAddr(*data)) {
data++; len--;
continue;
}

_crsf_buf[_crsf_len++] = *data++;
len--;

// Need the length byte before the frame size is known
if (_crsf_len < 2) continue;

uint32_t pkt_len = (uint32_t)_crsf_buf[1] + 2;
if (!_lenIsSane(pkt_len)) {
// Bogus length: this was not a frame start after all
_resync();
continue;
}

if (_crsf_len < pkt_len) continue; // incomplete, wait for more bytes

// A truncated CHANNELS_ID frame would be decoded from stale buffer bytes
if (_checkCRC(_crsf_buf) && _crsf_buf[2] == CHANNELS_ID &&
_crsf_buf[1] >= CRSF_CHANNELS_FRAME_LEN) {
// Fills trainerInput[] and resets the trainer validity timer
crossfireProcessChannelsFrame(_crsf_buf);
}

// Consume the whole frame either way, as _processFrames() does for the
// module path. With a valid address and a sane length, a CRC failure is
// far more likely to be corruption inside a real frame than a false lock,
// so dropping the frame keeps the stream aligned; byte-wise re-sync would
// instead risk latching onto a payload byte that happens to look like an
// address. Frame types other than CHANNELS_ID are consumed and ignored.
_crsf_len = 0;
}
}

void crsfTrainerStart(void* ctx, const etx_serial_driver_t* drv)
{
if (!drv || !drv->setReceiveCb) return;

_crsf_len = 0;
_crsf_drv = drv;
_crsf_ctx = ctx;

drv->setReceiveCb(ctx, crsfTrainerReceiveData);
}

void crsfTrainerStop()
{
auto drv = _crsf_drv;
auto ctx = _crsf_ctx;

// Disarm first: no further callback can start once this returns, and the
// next user of the port gets a clean stream
if (drv && drv->setReceiveCb) drv->setReceiveCb(ctx, nullptr);

_crsf_drv = nullptr;
_crsf_ctx = nullptr;
_crsf_len = 0;
}

void crsfTrainerReleaseCtx(void* ctx)
{
if (ctx && ctx == _crsf_ctx) crsfTrainerStop();
}
42 changes: 42 additions & 0 deletions radio/src/crsf_trainer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/

#pragma once

#include "hal/serial_driver.h"

//
// CRSF trainer input over a serial port.
//
// Unlike the SBUS trainer path, which relies on the USART idle line to mark
// frame boundaries, CRSF is self-framing: every frame carries a length byte
// and a CRC. That makes it usable on ports that deliver arbitrarily chunked
// buffers with no idle-line detection at all, i.e. USB-VCP.
//
// Attach / detach the driver's receive callback:
void crsfTrainerStart(void* ctx, const etx_serial_driver_t* drv);
void crsfTrainerStop();

// Stop if attached to this driver context, before the context goes away
void crsfTrainerReleaseCtx(void* ctx);

// Receive callback: feeds a chunk of the byte stream into the frame assembler
void crsfTrainerReceiveData(uint8_t* data, uint32_t len);
1 change: 1 addition & 0 deletions radio/src/dataconstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ enum UartModes {
UART_MODE_DEBUG,
UART_MODE_SPACEMOUSE,
UART_MODE_EXT_MODULE,
UART_MODE_CRSF_TRAINER,
UART_MODE_COUNT SKIP,
UART_MODE_MAX SKIP = UART_MODE_COUNT-1
};
Expand Down
14 changes: 14 additions & 0 deletions radio/src/gui/gui_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,16 @@ bool isSerialModeAvailable(uint8_t port_nr, int mode)
return false;
#endif

// CRSF trainer input is driven from a receive callback, which only the USB
// CDC driver provides today (the STM32 USART driver has no setReceiveCb).
if (mode == UART_MODE_CRSF_TRAINER) {
#if defined(USB_SERIAL) && defined(CROSSFIRE)
if (port_nr != SP_VCP) return false;
#else
return false;
#endif
}

auto p = serialGetModePort(mode);
if (p >= 0 && p != port_nr) return false;
return true;
Expand Down Expand Up @@ -1272,6 +1282,10 @@ bool isTrainerModeAvailable(int mode)
#if !defined(CROSSFIRE)
return false;
#else
// CRSF trainer frames can also arrive on a serial port instead of a
// module's telemetry stream, in which case no module has to be enabled.
if (serialGetModePort(UART_MODE_CRSF_TRAINER) >= 0) return true;

if ((!IS_INTERNAL_MODULE_ENABLED() && !IS_EXTERNAL_MODULE_ENABLED()) ||
(!(isModuleELRS(INTERNAL_MODULE) && CRSF_ELRS_MIN_VER(INTERNAL_MODULE, 4, 0)) &&
!(isModuleELRS(EXTERNAL_MODULE) && CRSF_ELRS_MIN_VER(EXTERNAL_MODULE, 4, 0))))
Expand Down
Loading
Loading