Skip to content
Merged
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ A build with the receive filter reports it in `ver`, e.g. `v1.18.0.1-<sha>-dmc-f

## What's different in meshcomod

A single firmware image supports **USB, Bluetooth, and TCP** companion connections at the same time (e.g. Home Assistant on USB, the MeshCore app on BLE, the web client on TCP). Use one, two, or all three; BLE and TCP can be toggled from the device UI.
On the Wi-Fi boards (every ESP32-S3 `_usb_tcp` build), a single firmware image supports **USB, Bluetooth, and TCP** companion connections at the same time (e.g. Home Assistant on USB, the MeshCore app on BLE, the web client on TCP). Use one, two, or all three; BLE and TCP can be toggled from the device UI.

- **USB** — always on when the device is powered.
- **Bluetooth** — the pairing PIN is shown on the device's **Bluetooth tab** (state + pairing info). **Long-press** the tab to enable/disable BLE; the footer reads "ON: long press" / "OFF: long press".
Expand Down Expand Up @@ -163,6 +163,8 @@ No local build needed. This fork's builds are published as **GitHub pre-releases
2. Choose **Custom firmware** and upload the `-merged.bin` for your board from the [latest fork pre-release](https://github.com/Elektr0Vodka/meshcomod/releases?q=companion).
3. Connect via USB, BLE (PIN on the display), and/or TCP.

> **nRF52 boards** (Wio Tracker L1, T-Echo, T-Echo Card) do not use the web flasher. Double-tap reset, then copy the `.uf2` for your board onto the USB drive that appears. The `.zip` in the same release is the DFU package for updating over BLE. Pick the `_ble` or `_usb` image for the transport you want.

> If unsure which binary to flash, use **merged** (it includes the bootloader + partition table and writes from `0x0`).

Rolling back: flash an upstream build with the flasher's Easy mode, or an older fork pre-release. Upstream pinned versions are in the [Release log (RELEASES.md)](RELEASES.md) and the [upstream GitHub Releases](https://github.com/ALLFATHER-BV/meshcomod/releases).
Expand Down Expand Up @@ -214,7 +216,7 @@ sh build.sh build-firmware Heltec_v3_companion_radio_usb_tcp

Other targets use the same flow with their env name from the [Supported devices](#supported-devices) table. Build all shipped companions at once with `sh build.sh build-meshcomod-companion-firmwares`.

**Outputs** (every target): app-only `out/<env>-<version>-<sha>.bin` and **merged** `out/<env>-<version>-<sha>-merged.bin` (flash from `0x0`). The merged image is also at `.pio/build/<env>/firmware-merged.bin`. The `*_companion_radio_usb_tcp` targets also get `out/<env>.partsig` (partition layout, used by `ota update`) and have the OTA manifest base baked in (`OTA_MANIFEST_BASE_URL` overrides it).
**Outputs** (ESP32 targets): app-only `out/<env>-<version>-<sha>.bin` and **merged** `out/<env>-<version>-<sha>-merged.bin` (flash from `0x0`). The merged image is also at `.pio/build/<env>/firmware-merged.bin`. The `*_companion_radio_usb_tcp` targets also get `out/<env>.partsig` (partition layout, used by `ota update`) and have the OTA manifest base baked in (`OTA_MANIFEST_BASE_URL` overrides it). nRF52 targets produce `out/<env>-<version>-<sha>.uf2` and a DFU `.zip` instead.

### Black screen after flashing

Expand Down
6 changes: 4 additions & 2 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,12 @@ for section, options in data:
fi
}

# disable all debug logging flags if DISABLE_DEBUG=1 is set
# disable all debug logging flags if DISABLE_DEBUG=1 is set.
# CFG_DEBUG is set to 0 rather than undefined: the nRF52 core reads it unconditionally
# (rtos.cpp), and its builder only adds the default when the flags don't mention it.
disable_debug_flags() {
if [ "$DISABLE_DEBUG" == "1" ]; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -UCFG_DEBUG -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UMESH_DEBUG -UBLE_DEBUG_LOGGING -UWIFI_DEBUG_LOGGING -UBRIDGE_DEBUG -UGPS_NMEA_DEBUG -UCORE_DEBUG_LEVEL -UESPNOW_DEBUG_LOGGING -UDEBUG_RP2040_WIRE -UDEBUG_RP2040_SPI -UDEBUG_RP2040_CORE -UDEBUG_RP2040_PORT -URADIOLIB_DEBUG_SPI -DCFG_DEBUG=0 -URADIOLIB_DEBUG_BASIC -URADIOLIB_DEBUG_PROTOCOL"
fi
}

Expand Down
16 changes: 9 additions & 7 deletions changelog_ev.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ Newest first. Each entry names the PR that landed it.

### Added

- **Eight more multi-transport companion boards (experimental)**: `usb_tcp` envs (USB + TCP + WebSocket + BLE, SNMP, manifest OTA) for the Heltec Vision Master E290 and T190, Heltec RC32, LilyGo T-Beam 1W, Meshnology W12, Station G2, Station G3 (ESP32-S3) and Elecrow ThinkNode M9, all added to the release list. All are 16 MB ESP32-S3 boards and reuse the display driver of their stock companion builds. The T-Beam 1W and both Stations default to the 4 MB partition layout, so their envs select `default_16MB.csv`. The ThinkNode M9 `variant.h` macro `DONE` (unused) is renamed `PIN_CHARGE_DONE` because it broke the SNMP library's parser enum. Builds, not tested on hardware.
- **Six more multi-transport companion boards (experimental)**: `usb_tcp` envs (USB + TCP + WebSocket + BLE, SNMP, manifest OTA) for the Heltec Wireless Tracker V2, Heltec Vision Master E213, Seeed Xiao S3, RAK3112, LilyGo T-ETH Elite and LilyGo T-Beam S3 Supreme, all added to the release list. Each is an ESP32-S3 with at least 8 MB flash on its existing partition table and reuses the display driver of the board's stock companion builds (RAK3112 and T-ETH Elite are headless). The T-ETH Elite build runs over Wi-Fi; its Ethernet port is not used, as upstream has no Ethernet companion for it either. Marked experimental in the README: nobody working on the fork owns these boards, so beyond building, nothing board specific (display, buttons, battery, free heap) has been checked. Builds, not tested on hardware.
- **Seeed Wio Tracker L1 / L1 Pro companions**: the existing upstream `WioTrackerL1_companion_radio_ble` and `WioTrackerL1_companion_radio_usb` envs are now in the release list, so the fork builds and releases them (`.uf2` + DFU `.zip`). The nRF52840 has no Wi-Fi, so these are single-transport (BLE or USB) with no TCP, SNMP or manifest OTA. The CAD toggle and receive filter build in. Builds, not tested on hardware.
- **Eight more multi-transport companion boards (experimental)** (#18): `usb_tcp` envs (USB + TCP + WebSocket + BLE, SNMP, manifest OTA) for the Heltec Vision Master E290 and T190, Heltec RC32, LilyGo T-Beam 1W, Meshnology W12, Station G2, Station G3 (ESP32-S3) and Elecrow ThinkNode M9, all added to the release list. All are 16 MB ESP32-S3 boards and reuse the display driver of their stock companion builds. The T-Beam 1W and both Stations default to the 4 MB partition layout, so their envs select `default_16MB.csv`. The ThinkNode M9 `variant.h` macro `DONE` (unused) is renamed `PIN_CHARGE_DONE` because it broke the SNMP library's parser enum. Builds, not tested on hardware.
- **Six more multi-transport companion boards (experimental)** (#15): `usb_tcp` envs (USB + TCP + WebSocket + BLE, SNMP, manifest OTA) for the Heltec Wireless Tracker V2, Heltec Vision Master E213, Seeed Xiao S3, RAK3112, LilyGo T-ETH Elite and LilyGo T-Beam S3 Supreme, all added to the release list. Each is an ESP32-S3 with at least 8 MB flash on its existing partition table and reuses the display driver of the board's stock companion builds (RAK3112 and T-ETH Elite are headless). The T-ETH Elite build runs over Wi-Fi; its Ethernet port is not used, as upstream has no Ethernet companion for it either. Marked experimental in the README: nobody working on the fork owns these boards, so beyond building, nothing board specific (display, buttons, battery, free heap) has been checked. Builds, not tested on hardware.
- **Seeed Wio Tracker L1 / L1 Pro companions** (#14): the existing upstream `WioTrackerL1_companion_radio_ble` and `WioTrackerL1_companion_radio_usb` envs are now in the release list, so the fork builds and releases them (`.uf2` + DFU `.zip`). The nRF52840 has no Wi-Fi, so these are single-transport (BLE or USB) with no TCP, SNMP or manifest OTA. The CAD toggle and receive filter build in. Builds, not tested on hardware.
- **LilyGo T-Echo companions** (#16): the existing upstream `LilyGo_T-Echo_companion_radio_ble` and `LilyGo_T-Echo_companion_radio_usb` envs are now in the release list (`.uf2` + DFU `.zip`). Like the Wio Tracker L1 this is an nRF52840 without Wi-Fi, so the builds are single-transport (BLE or USB) with no TCP, SNMP or manifest OTA. The e-ink ui-new UI, CAD toggle and receive filter build in. The T-Echo Lite is not included. Builds, not tested on hardware.
- **LilyGo T-Echo Card companions** (#16): `LilyGo_T-Echo_Card_companion_radio_ble` and `_usb` join the release list, single-transport like the T-Echo, with the ui-tiny UI and its CAD page. Builds, not tested on hardware.

### CI and build

- **PR build check covers classic ESP32**: `Heltec_v2_companion_radio_ble` joins the PR matrix so companion code that only builds on the S3, like the `bootloader` register write fixed in #17, is caught before merge.
- **PR build check covers nRF52**: `WioTrackerL1_companion_radio_ble` joins the PR matrix so fork companion code that only builds on ESP32 is caught.
- **PR build check covers classic ESP32** (#21): `Heltec_v2_companion_radio_ble` joins the PR matrix so companion code that only builds on the S3, like the `bootloader` register write fixed in #17, is caught before merge.
- **PR build check covers nRF52** (#14): `WioTrackerL1_companion_radio_ble` joins the PR matrix so fork companion code that only builds on ESP32 is caught.

### Docs

Expand All @@ -35,10 +35,12 @@ Newest first. Each entry names the PR that landed it.

### Fixed

- **ESP32-C6 companions build** (#22): the C6 envs run Arduino core 3.x but inherited NimBLE-Arduino 1.4.3, which only builds against core 2.x. `esp32c6_base` now uses NimBLE-Arduino 2.3.9, and `SerialBLEInterface` implements both callback APIs (picked at compile time), so the S3 boards stay on 1.4.3. All C6 companion envs (Xiao C6, Meshimi, WHY2025 badge, T-LoRa C6, M5Stack Unit C6L) build. Builds, not tested on hardware.
- **`DISABLE_DEBUG=1` builds on nRF52** (#20): `build.sh` undefined `CFG_DEBUG`, which the nRF52 Arduino core reads unconditionally, so every nRF52 build with `DISABLE_DEBUG=1` failed. It now sets `CFG_DEBUG=0`.
- **T-Echo Card companions build again** (#16): the ui-tiny UI still took the upstream 1.18 `MultiSerialInterface*`, while meshcomod passes the board's own serial interface. It now takes `BaseSerialInterface*` like ui-new.
- **Companion builds on classic ESP32** (#17): the `bootloader` CLI command wrote the RTC force-download bit, which only the S2, S3 and C3 have, so every classic ESP32 companion (Heltec V2, T-Beam, T-LoRa and others) failed to compile. Chips without the bit now reply `bootloader: not supported on this chip`. The ESP32-C6 envs still do not build: they run Arduino core 3.x while `esp32_base` pins NimBLE-Arduino 1.4.3, which needs core 2.x.
- **Companion builds on nRF52**: two pieces of upstream meshcomod companion code assumed ESP32 and broke any non-ESP32 companion build. The backup import used the ESP32 PSRAM allocator (`heap_caps_*`); it now uses the default allocator elsewhere. `ota status` read HTTP OTA progress globals that only `ESP32Board.cpp` defines; it now reports `ota: idle` on boards without HTTP OTA.
- **Docs site deploy**: the Pages workflow no longer writes upstream's `docs.meshcore.nz` CNAME into `gh-pages`, and `mkdocs.yml` points at this fork (`elektr0vodka.github.io/meshcomod`) instead of `meshcore-dev`. GitHub Pages must serve the `gh-pages` branch for the mkdocs site to show; it was serving `main` `/docs` through Jekyll.
- **Companion builds on nRF52** (#14): two pieces of upstream meshcomod companion code assumed ESP32 and broke any non-ESP32 companion build. The backup import used the ESP32 PSRAM allocator (`heap_caps_*`); it now uses the default allocator elsewhere. `ota status` read HTTP OTA progress globals that only `ESP32Board.cpp` defines; it now reports `ota: idle` on boards without HTTP OTA.
- **Docs site deploy** (#12): the Pages workflow no longer writes upstream's `docs.meshcore.nz` CNAME into `gh-pages`, and `mkdocs.yml` points at this fork (`elektr0vodka.github.io/meshcomod`) instead of `meshcore-dev`. GitHub Pages must serve the `gh-pages` branch for the mkdocs site to show; it was serving `main` `/docs` through Jekyll.

## 2026-10-16

Expand Down
6 changes: 3 additions & 3 deletions docs/companion_cad_toggle.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ The on-device control depends on the UI the board uses:

| UI variant | Boards (examples) | CAD control |
|-----------|-------------------|-------------|
| ui-new | Heltec V4, Heltec V3 (OLED) | Dedicated CAD page. Swipe to it, ENTER or long-press toggles. Tested on hardware. |
| ui-new | Heltec V4 and V3 (OLED), every other released `_usb_tcp` board with a display, the Wio Tracker L1 and the T-Echo | Dedicated CAD page. Swipe to it, ENTER or long-press toggles. Tested on hardware on the V4 and V3 only. |
| ui-touch | Heltec V4 TFT touch | Settings > Experimental > CAD switch, then Save. |
| ui-tiny | LilyGo T-Echo Card | Dedicated CAD page. ENTER toggles. |
| ui-orig | Meshtracker X1, MinewSemi ME25LS01, Muziworks R1 Neo, RAK 3112, and similar | Read-only CAD status on the home screen. No on-device toggle (this UI is gesture-only with no free gesture); change CAD from the app. |
| ui-tiny | LilyGo T-Echo Card (released from this fork) | Dedicated CAD page. ENTER toggles. |
| ui-orig | ThinkNode M7 (released from this fork), Meshtracker X1, MinewSemi ME25LS01, Muziworks R1 Neo, the stock RAK 3112 builds, and similar | Read-only CAD status on the home screen. No on-device toggle (this UI is gesture-only with no free gesture); change CAD from the app. |

## Default and persistence

Expand Down
4 changes: 4 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ lib_deps =
[esp32c6_base]
extends = esp32_base
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13-1/platform-espressif32.zip
; core 3.x needs NimBLE-Arduino 2.x (1.4.x only builds against core 2.x); the S3 boards
; stay on 1.4.3 via esp32_base. SerialBLEInterface handles both callback APIs.
lib_deps = ${arduino_base.lib_deps}
h2zero/NimBLE-Arduino @ 2.3.9

; ----------------- NRF52 ---------------------

Expand Down
109 changes: 84 additions & 25 deletions src/helpers/esp32/SerialBLEInterface.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#include "SerialBLEInterface.h"
#if __has_include(<esp_mac.h>)
#include <esp_mac.h> // esp_efuse_mac_get_default moved here in core 3.x
#endif

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
Expand Down Expand Up @@ -74,15 +77,84 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
// was unidentifiable in a list of peers. The name rides the scan response when
// it doesn't fit beside the 128-bit service UUID in the 31-byte adv packet.
adv->setName(dev_name);
#ifdef MESHCOMOD_NIMBLE2
adv->enableScanResponse(true);
#else
adv->setScanResponse(true);
#endif
}

void SerialBLEInterface::startAdvertising() {
NimBLEDevice::getAdvertising()->start();
adv_restart_time = 0;
}

// -------- NimBLEServerCallbacks: security
// -------- callback bodies shared by both NimBLE APIs

void SerialBLEInterface::handleAuthComplete(bool encrypted, uint16_t conn_handle) {
if (encrypted) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
deviceConnected = true;
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*");
if (pServer) pServer->disconnect(conn_handle);
adv_restart_time = millis() + ADVERT_RESTART_DELAY;
}
}

void SerialBLEInterface::handleConnect(uint16_t conn_handle, const uint8_t* peer_addr) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d", conn_handle);
last_conn_id = conn_handle;
memcpy(_peer_bda, peer_addr, 6); // NimBLE addr is little-endian
_peer_bda_valid = true;
}

void SerialBLEInterface::handleDisconnect() {
BLE_DEBUG_PRINTLN("onDisconnect()");
_peer_bda_valid = false;
if (_isEnabled) {
adv_restart_time = millis() + ADVERT_RESTART_DELAY;
// loop() will detect this on next loop, and set deviceConnected to false
}
}

#ifdef MESHCOMOD_NIMBLE2

// -------- NimBLE 2.x callbacks

uint32_t SerialBLEInterface::onPassKeyDisplay() {
BLE_DEBUG_PRINTLN("onPassKeyDisplay()");
return _pin_code;
}

void SerialBLEInterface::onConfirmPassKey(NimBLEConnInfo& connInfo, uint32_t pass_key) {
BLE_DEBUG_PRINTLN("onConfirmPassKey(%u)", pass_key);
NimBLEDevice::injectConfirmPasskey(connInfo, true);
}

void SerialBLEInterface::onAuthenticationComplete(NimBLEConnInfo& connInfo) {
handleAuthComplete(connInfo.isEncrypted(), connInfo.getConnHandle());
}

void SerialBLEInterface::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) {
handleConnect(connInfo.getConnHandle(), connInfo.getAddress().getVal());
}

void SerialBLEInterface::onMTUChange(uint16_t MTU, NimBLEConnInfo& connInfo) {
BLE_DEBUG_PRINTLN("onMTUChange(), mtu=%d", MTU);
}

void SerialBLEInterface::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) {
handleDisconnect();
}

void SerialBLEInterface::onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) {
handleWrite(pCharacteristic);
}

#else

// -------- NimBLE 1.4.x callbacks

uint32_t SerialBLEInterface::onPassKeyRequest() {
BLE_DEBUG_PRINTLN("onPassKeyRequest()");
Expand All @@ -95,43 +167,30 @@ bool SerialBLEInterface::onConfirmPIN(uint32_t pass_key) {
}

void SerialBLEInterface::onAuthenticationComplete(ble_gap_conn_desc* desc) {
if (desc && desc->sec_state.encrypted) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
deviceConnected = true;
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*");
if (pServer) pServer->disconnect(desc ? desc->conn_handle : last_conn_id);
adv_restart_time = millis() + ADVERT_RESTART_DELAY;
}
handleAuthComplete(desc && desc->sec_state.encrypted, desc ? desc->conn_handle : last_conn_id);
}

// -------- NimBLEServerCallbacks: connection lifecycle

void SerialBLEInterface::onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d", desc ? desc->conn_handle : -1);
if (desc) {
last_conn_id = desc->conn_handle;
memcpy(_peer_bda, desc->peer_ota_addr.val, 6); // NimBLE addr is little-endian
_peer_bda_valid = true;
}
if (desc) handleConnect(desc->conn_handle, desc->peer_ota_addr.val);
}

void SerialBLEInterface::onMTUChange(uint16_t MTU, ble_gap_conn_desc* desc) {
BLE_DEBUG_PRINTLN("onMTUChange(), mtu=%d", MTU);
}

void SerialBLEInterface::onDisconnect(NimBLEServer* pServer) {
BLE_DEBUG_PRINTLN("onDisconnect()");
_peer_bda_valid = false;
if (_isEnabled) {
adv_restart_time = millis() + ADVERT_RESTART_DELAY;
// loop() will detect this on next loop, and set deviceConnected to false
}
handleDisconnect();
}

// -------- NimBLECharacteristicCallbacks

void SerialBLEInterface::onWrite(NimBLECharacteristic* pCharacteristic) {
handleWrite(pCharacteristic);
}

#endif

// -------- RX characteristic write

void SerialBLEInterface::handleWrite(NimBLECharacteristic* pCharacteristic) {
NimBLEAttValue val = pCharacteristic->getValue();
int len = val.length();
const uint8_t* rxValue = val.data();
Expand Down
Loading
Loading