From 23b304056b828c2c0bbaf909fb3d3277cdee40dc Mon Sep 17 00:00:00 2001 From: Amigache Date: Mon, 5 Oct 2026 11:47:24 +0200 Subject: [PATCH] Add FrSky BLE trainer (head tracker) support to the TX backpack Adds an opt-in BLE central to the TX backpack so a device advertising the FrSky trainer service (0xFFF0 / 0xFFF6), most notably a HeadTracker in "Wireless Transmitter (Head)" mode, can feed the radio's trainer input without the BTWifiModule hardware. The backpack connects over BLE, parses the FrSky trainer frame (8 channels), converts it to CRSF values and emits MSP_ELRS_BACKPACK_SET_PTR to the TX module over the backpack UART. The module already understands that opcode (HT Enabled / HT Start Channel = EdgeTX), so no ExpressLRS change is needed for the core feature. Although head tracking is the main use case, the implementation is generic: any FrSky trainer source works, hence the name "FrSky BLE trainer". Highlights: - src/BleTrainer.cpp + include/BleTrainer.h: NimBLE central (scan 0xFFF0, pair strongest RSSI, store MAC, reconnect on boot, Forget), per-notification FrSky parser, PWM->CRSF conversion, SET_PTR emission. - Steady 50 Hz republication so EdgeTX's trainer input does not time out. - 8 ms default connection interval, 4 s supervision timeout. - BLE + MAVLink WiFi coexistence (WiFi preference, BLE P3 in MAVLINK_TX). - Config v6: trainerPeerMac/Type/IntervalMs/Source (Auto/Native/BLE). - WebUI tab "FrSky BLE Trainer"; POST /settrainer; JSON config.trainer. - Long button press forgets the peer. - HAS_BLE_TRAINER + NimBLE-Arduino for the C3 TX backpack target. - MAVLink WiFi mode persistent across boots (telemMode). - Dedup repeated MSP_ELRS_BACKPACK_CONFIG telemetry-mode to avoid reboot loops. Validated on the Nomad: SET_PTR ~50/s, EdgeTX trainer channels move, HT + MAVLink WiFi simultaneously (10-min soak, 0 resets), module power up to 250 mW, cold boot and updater cycle. Build: ~1.19 MB (60.4% of min_spiffs), RAM 55.980 B. --- html/scan.js | 59 ++++++ html/txbp_index.html | 43 +++++ include/BleTrainer.h | 26 +++ lib/BUTTON/devButton.cpp | 13 ++ lib/WIFI/devWIFI.cpp | 138 ++++++++++++- lib/config/config.cpp | 66 +++++++ lib/config/config.h | 30 ++- src/BleTrainer.cpp | 404 +++++++++++++++++++++++++++++++++++++++ src/Tx_main.cpp | 100 ++++++++-- targets/txbp_esp.ini | 2 + 10 files changed, 853 insertions(+), 28 deletions(-) create mode 100644 include/BleTrainer.h create mode 100644 src/BleTrainer.cpp diff --git a/html/scan.js b/html/scan.js index 4ee678be..6f80ae54 100644 --- a/html/scan.js +++ b/html/scan.js @@ -7,6 +7,7 @@ function _(el) { function init() { initAat(); initMavLink(); + initBleTrainer(); // sends XMLHttpRequest, so do it last initOptions(); @@ -47,6 +48,55 @@ function initMavLink() { xmlhttp.open('GET', '/mavlink', true); xmlhttp.send(); } + +function initBleTrainer() { + const form = _('bt_form'); + if (!form) + return; + form.addEventListener('submit', (e) => { e.preventDefault(); btSubmit(false); }); + _('bt_forget').addEventListener('click', (e) => { e.preventDefault(); btSubmit(true); }); + _('bt_source').addEventListener('change', () => btToggleBleSettings(_('bt_source').value)); +} + +// Posts the FrSky BLE Trainer form (or a Forget request) and updates the page +// in place, so no manual reload is needed. +function btSubmit(clearPeer) { + const body = clearPeer ? new URLSearchParams({ forget: 1 }) + : new URLSearchParams(new FormData(_('bt_form'))); + const xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function () { + if (this.readyState != 4) + return; + if (this.status == 200) { + if (clearPeer) { + _('bt_status').textContent = 'Not paired'; + _('bt_mac').textContent = '-'; + } + cuteAlert({ + type: 'success', + title: 'Bluetooth Trainer', + message: 'Bluetooth trainer settings saved' + }); + } else { + cuteAlert({ + type: 'error', + title: 'Bluetooth Trainer', + message: 'An error occurred saving the Bluetooth trainer settings' + }); + } + }; + xmlhttp.open('POST', '/settrainer', true); + xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xmlhttp.send(body); +} + +// Bluetooth-only fields are hidden when the native source is selected. +function btToggleBleSettings(source) { + const el = _('bt_ble_settings'); + if (el) + el.style.display = (source === 'native') ? 'none' : 'block'; +} + function initOptions() { const xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { @@ -136,6 +186,15 @@ function updateConfig(data) { if (config['head-tracking']) { if (_('httab')) _('httab').style.display = 'table-cell'; } + if (config['trainer']) { + if (_('bttab')) _('bttab').style.display = 'table-cell'; + const bt = config.trainer; + _('bt_status').textContent = bt.paired ? 'Paired' : 'Not paired'; + _('bt_mac').textContent = bt.paired ? bt.mac : '-'; + _('bt_interval').value = bt.interval; + _('bt_source').value = bt.source; + btToggleBleSettings(bt.source); + } if((!data.stm32 || data.stm32==="no") && _('tx_tab')) { mui.tabs.activate('pane-justified-2'); _('tx_tab').style.display = 'none'; diff --git a/html/txbp_index.html b/html/txbp_index.html index 08c78af2..44190928 100644 --- a/html/txbp_index.html +++ b/html/txbp_index.html @@ -26,6 +26,7 @@

ExpressLRS

  • Backpack
  • Network
  • MavLink
  • +
    @@ -146,6 +147,48 @@

    MavLink Statistics

    + +
    +

    FrSky BLE Trainer

    +

    The backpack forwards trainer channels to EdgeTX (as trainer input) from one of + two sources: the native system (ESP-NOW, e.g. a VRX or goggles with IMU) or a + Bluetooth device advertising the FrSky trainer service (e.g. an Arduino-based + HeadTracker). Both respect the radio's HT Enable; use the selector to choose which one + is used.

    +
    +
    + + +
    +
    +

    Bluetooth device

    + + + + + + + + + +
    Status:-
    Paired address:-
    +
    + Connection interval (ms) + +
    +

    The trainer sends one frame per Bluetooth connection event, so a low + interval is what keeps the movement fluid. 8 ms suits a HeadTracker; higher + values only slow the stream down.

    + +

    +
    + +
    +