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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ wifi.status
| `wifi clear` | Clear runtime creds (fall back to compile-time on next boot) |
| `tcp on` / `tcp off` / `tcp status` | Control/query the TCP server |
| `ble on` / `ble off` / `ble status` | Control/query Bluetooth |
| `mac` | Show the device Wi-Fi and BLE MAC addresses (also `get mac`, `get wifi.mac`, `get ble.mac` in the app console or over USB serial) |
| `mac` | Show the device Wi-Fi and BLE MAC addresses, `n/a` for an interface the chip lacks (also `get mac`, `get wifi.mac`, `get ble.mac` in the app console or over USB serial) |
| `ota check` | Ask the OTA server whether a newer build exists for this device |
| `ota update` | Install that build over Wi-Fi (see [Update over Wi-Fi](#update-over-wi-fi)) |

Expand Down
5 changes: 4 additions & 1 deletion changelog_ev.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ Newest first. Each entry names the PR that landed it.
- **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.

### Changed

- **Chip features behind the board** (#28): the device MAC, the `bootloader` reboot into download mode, the SNMP memory figures and the `ota status` progress now come from new `MainBoard` methods (`getDeviceMac`, `canRebootToBootloader` / `rebootToBootloader`, `getMemStats`, `getHttpOtaProgress`) with "not available" defaults. The ESP32 code, including the S2/S3/C3-only force-download register, moves into `ESP32Board.cpp`, so the companion `MyMesh.cpp` and SNMP agent no longer carry chip `#if` guards for these. New boards and chips get a working build by default; a chip that can do more overrides the method. Replies are unchanged, except two: `bootloader` on non-ESP32 boards now says `not supported on this chip` like the classic ESP32 does, and `mac` on nRF52 boards now shows the BLE address (from FICR) instead of `n/a`. Builds (S3, classic ESP32, C6, nRF52), not tested on hardware.

### CI and build

- **One board list: `companion_boards.ini`** (#27): the released companion boards now live in one fork-owned file, one section per board (name, MCU, display, envs, status, PR-check envs). `scripts/companion_boards.py` derives the `build.sh` release list, the PR build matrix and the README device table from it, and a new Companion Boards Check workflow fails when the README table is stale, an env is listed twice or has no `[env:...]` section. Adding a board is now one ini section plus `python3 scripts/companion_boards.py readme --write`. The env list and PR matrix are unchanged by the move (same 59 release envs in the same order, same 5 PR envs).

- **Companion releases build without debug logging** (#26): the companion release workflow now sets `DISABLE_DEBUG=1`, as the repeater release already did. Before, released envs that enable `BLE_DEBUG_LOGGING` or `MESH_DEBUG` in their ini (several nRF52 BLE boards, e.g. the Wio Tracker L1) shipped with it on. The Wio Tracker L1 BLE build drops about 2.4 KB of flash.
- **PR build check builds through `build.sh`** (#26): it now runs `build.sh build-firmware` with `DISABLE_DEBUG=1`, like the release, instead of plain `pio run`, so the flags `build.sh` adds (the manifest OTA code via `OTA_VARIANT`, the debug-flag removal) compile on every PR. Changes to `build.sh` and `scripts/` now trigger the check.
- **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.
Expand Down
21 changes: 13 additions & 8 deletions examples/companion_radio/CompanionSNMP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include <WiFi.h>
#include <WiFiUdp.h>
#include <SNMP_Agent.h>
#include <esp_heap_caps.h>
#include <math.h>
#include <string.h>
#include <string>
Expand Down Expand Up @@ -80,6 +79,12 @@ static void copyCounters() {
// the observer's meaning, type and unit; an OID the companion has no data for is not
// registered, so it reads noSuchObject instead of a 0 that looks real. Companion-only values
// use .9 and up, which the observer keeps free.
static mesh::MainBoard::MemStats memStats() {
mesh::MainBoard::MemStats s;
board.getMemStats(s);
return s;
}

static void registerHandlers(SNMPAgent* a) {
// System (.1.x.0)
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".1.1.0", []() -> int { return (int)(millis() / 1000); });
Expand Down Expand Up @@ -116,12 +121,12 @@ static void registerHandlers(SNMPAgent* a) {
// MQTT (.3.x.0): no bridge on the companion, so only the NTP flag, which it does have.
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".3.4.0", []() -> int { return s_ntp_synced ? 1 : 0; });

// Memory (.4.x.0). heap_caps reports 0 for PSRAM on boards without it.
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.1.0", []() -> int { return (int)ESP.getFreeHeap(); });
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.2.0", []() -> int { return (int)ESP.getMaxAllocHeap(); });
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.3.0", []() -> int { return (int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL); });
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.4.0", []() -> int { return (int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM); });
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.5.0", []() -> int { return (int)ESP.getMinFreeHeap(); });
// Memory (.4.x.0), from the board. PSRAM reads 0 on boards without it.
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.1.0", []() -> int { return (int)memStats().free_heap; });
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.2.0", []() -> int { return (int)memStats().max_alloc_heap; });
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.3.0", []() -> int { return (int)memStats().free_internal; });
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.4.0", []() -> int { return (int)memStats().free_psram; });
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".4.5.0", []() -> int { return (int)memStats().min_free_heap; });

// Network (.5.x.0)
a->addDynamicIntegerHandler(MESHCORE_OID_BASE ".5.1.0", []() -> int {
Expand Down Expand Up @@ -171,7 +176,7 @@ static void startAgent() {
s_agent->restartUDP(); // the old socket died with the link
}
s_running = true;
Serial.printf("[snmp] agent up on udp/161 (free=%u)\n", (unsigned)ESP.getFreeHeap());
Serial.printf("[snmp] agent up on udp/161 (free=%u)\n", (unsigned)memStats().free_heap);
}

static void stopAgent() {
Expand Down
80 changes: 22 additions & 58 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,7 @@
#include <string.h>
#include <SHA256.h> // derive a region's flood-scope key from its #hashtag name
#include <time.h> // gmtime_r for the "clock" CLI command
#ifdef ESP32
#include <esp_system.h> // esp_restart for the "bootloader" CLI command
#if __has_include(<soc/rtc_cntl_reg.h>)
#include <soc/rtc_cntl_reg.h> // RTC_CNTL_OPTION1_REG / FORCE_DOWNLOAD_BOOT
#endif
// The force-download bit exists on the S2/S3 and C3, not on the classic ESP32 or the C6.
#if defined(RTC_CNTL_OPTION1_REG) && defined(RTC_CNTL_FORCE_DOWNLOAD_BOOT)
#define MESHCOMOD_HAS_FORCE_DOWNLOAD 1
#endif
#if __has_include(<esp_mac.h>)
#include <esp_mac.h> // esp_read_mac for the "mac" / "get mac" CLI commands
#endif
#endif
#include <helpers/AdvertDataHelpers.h>
#include <helpers/HttpOtaDisplayState.h>
#include <helpers/RepeaterTcpOtaEmit.h>
#include "WiFiConfig.h"
#include "CompanionSNMP.h"
Expand Down Expand Up @@ -239,24 +225,18 @@ static const char* kMeshcomodHelpMsg =
"ble status\n"
"mac";

// Factory MAC of one ESP32 interface as "AA:BB:CC:DD:EE:FF", or "n/a" when the chip has no
// such interface (ESP32-S2 has no Bluetooth) or this is not an ESP32. The address is read from
// eFuse, so it is valid whether or not that radio is currently on.
#ifdef ESP32
static void formatDeviceMac(esp_mac_type_t type, char* out, size_t len) {
// Factory MAC of one interface as "AA:BB:CC:DD:EE:FF", or "n/a" when the board has no such
// interface (no Wi-Fi on the nRF52, no Bluetooth on the ESP32-S2).
static void formatDeviceMac(uint8_t kind, char* out, size_t len) {
uint8_t m[6];
if (esp_read_mac(m, type) == ESP_OK) {
if (board.getDeviceMac(kind, m)) {
snprintf(out, len, "%02X:%02X:%02X:%02X:%02X:%02X", m[0], m[1], m[2], m[3], m[4], m[5]);
} else {
StrHelper::strncpy(out, "n/a", len);
}
}
static void getWifiMac(char* out, size_t len) { formatDeviceMac(ESP_MAC_WIFI_STA, out, len); }
static void getBleMac(char* out, size_t len) { formatDeviceMac(ESP_MAC_BT, out, len); }
#else
static void getWifiMac(char* out, size_t len) { StrHelper::strncpy(out, "n/a", len); }
static void getBleMac(char* out, size_t len) { StrHelper::strncpy(out, "n/a", len); }
#endif
static void getWifiMac(char* out, size_t len) { formatDeviceMac(mesh::MainBoard::DEVICE_MAC_WIFI, out, len); }
static void getBleMac(char* out, size_t len) { formatDeviceMac(mesh::MainBoard::DEVICE_MAC_BLE, out, len); }

#define MESHCOMOD_CMD_CACHE_SIZE 6
struct MeshcomodCmdCacheEntry {
Expand Down Expand Up @@ -623,30 +603,15 @@ bool MyMesh::handleMeshcomodCommand(const char* text, int text_len) {
return true; // not reached
}
if (isCmd(p, "bootloader") || isCmd(p, "dfu")) {
#if defined(ESP32) && defined(MESHCOMOD_HAS_FORCE_DOWNLOAD)
// Force the ROM into serial/USB download mode on the next reset, so the
// board can be flashed without the (flaky) trackball+reset combo. The
// FORCE_DOWNLOAD_BOOT bit lives in the RTC domain and survives the restart.
pushMeshcomodReply("rebooting into download mode (screen goes dark)...");
delay(200);
#ifdef PIN_TFT_LEDA_CTL
if (PIN_TFT_LEDA_CTL >= 0) { // blank the backlight = clear "in download mode" signal
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
// Reboot into the ROM's serial/USB download mode, so the board can be flashed without
// the (flaky) button+reset combo. Which chips can do this is the board's business.
if (board.canRebootToBootloader()) {
pushMeshcomodReply("rebooting into download mode (screen goes dark)...");
delay(200);
board.rebootToBootloader();
} else {
pushMeshcomodReply("bootloader: not supported on this chip");
}
#endif
// Read-modify-write ONLY the force-download bit. A full REG_WRITE zeroes the
// rest of RTC_CNTL_OPTION1 and wedges the RTC so esp_restart() hangs instead
// of resetting (that was the earlier "freeze"). board.reboot() == esp_restart,
// the proven reset path on this board; the RTC bit survives it.
uint32_t opt1 = REG_READ(RTC_CNTL_OPTION1_REG);
REG_WRITE(RTC_CNTL_OPTION1_REG, opt1 | RTC_CNTL_FORCE_DOWNLOAD_BOOT);
board.reboot();
#elif defined(ESP32)
pushMeshcomodReply("bootloader: not supported on this chip");
#else
pushMeshcomodReply("bootloader: ESP32-only");
#endif
return true; // not reached when download mode is supported
}
if (isCmd(p, "get")) {
Expand Down Expand Up @@ -840,17 +805,16 @@ bool MyMesh::handleMeshcomodCommand(const char* text, int text_len) {
}
if (strncasecmp(p, "status", 6) == 0 && (p[6] == '\0' || p[6] == ' ' || p[6] == '\t')) {
char line[96];
#ifdef ESP32 // the progress globals live in ESP32Board.cpp; no HTTP OTA elsewhere
if (g_meshcore_http_ota_display_active) {
if (g_meshcore_http_ota_display_pct == 0xFF) {
snprintf(line, sizeof(line), "ota: active\n%s", g_meshcore_http_ota_display_line[0] ? g_meshcore_http_ota_display_line : "working");
uint8_t pct;
const char* status = "";
if (board.getHttpOtaProgress(pct, status)) {
const char* what = status[0] ? status : "working";
if (pct == 0xFF) {
snprintf(line, sizeof(line), "ota: active\n%s", what);
} else {
snprintf(line, sizeof(line), "ota: %u%%\n%s", (unsigned)g_meshcore_http_ota_display_pct,
g_meshcore_http_ota_display_line[0] ? g_meshcore_http_ota_display_line : "working");
snprintf(line, sizeof(line), "ota: %u%%\n%s", (unsigned)pct, what);
}
} else
#endif
{
} else {
snprintf(line, sizeof(line), "ota: idle");
}
pushMeshcomodReply(line);
Expand Down
23 changes: 23 additions & 0 deletions src/MeshCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@ class MainBoard {
return false;
}

// Chip capabilities the companion CLI and SNMP agent report. Defaults say "not
// available", so app code needs no chip #if: each platform's board answers for itself.
enum { DEVICE_MAC_WIFI = 0, DEVICE_MAC_BLE = 1 };
/** Factory MAC of the Wi-Fi station or BLE interface into mac[6], most significant byte
* first. Returns false when the chip has no such interface. */
virtual bool getDeviceMac(uint8_t kind, uint8_t mac[6]) { (void)kind; (void)mac; return false; }
/** True when rebootToBootloader() can enter the ROM serial/USB download mode. */
virtual bool canRebootToBootloader() const { return false; }
/** Reboot into download mode. Does not return when supported; does nothing otherwise. */
virtual void rebootToBootloader() { }
struct MemStats {
uint32_t free_heap; // bytes free now
uint32_t max_alloc_heap; // largest block that can be allocated
uint32_t min_free_heap; // lowest free heap since boot
uint32_t free_internal; // free internal RAM
uint32_t free_psram; // free PSRAM, 0 without PSRAM
};
/** Heap figures in bytes. Returns false, with s zeroed, when the board does not report them. */
virtual bool getMemStats(MemStats& s) { s = MemStats(); return false; }
/** A running HTTP OTA download: pct is 0-100 or 0xFF when the size is unknown, line a short
* status text (may be empty). Returns false when no download is running. */
virtual bool getHttpOtaProgress(uint8_t& pct, const char*& line) { (void)pct; (void)line; return false; }

virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
virtual bool canControlLoRaFemLna() const { return false; }
virtual bool isLoRaFemLnaEnabled() const { return false; }
Expand Down
55 changes: 55 additions & 0 deletions src/helpers/ESP32Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
#include <helpers/HttpOtaWifiSession.h>
#include <helpers/RepeaterTcpOtaEmit.h>
#include <helpers/OtaManifest.h>
#include <esp_system.h>
#include <esp_heap_caps.h>
#if __has_include(<esp_mac.h>)
#include <esp_mac.h> // esp_read_mac (IDF 5; esp_system.h has it on IDF 4)
#endif
#if __has_include(<soc/rtc_cntl_reg.h>)
#include <soc/rtc_cntl_reg.h> // RTC_CNTL_OPTION1_REG / FORCE_DOWNLOAD_BOOT
#endif

static void (*s_http_ota_minimal_prepare)(uint8_t) = nullptr;
static void (*s_http_ota_minimal_restore)() = nullptr;
Expand All @@ -29,6 +37,53 @@ volatile int g_meshcore_http_ota_display_active = 0;
volatile uint8_t g_meshcore_http_ota_display_pct = 0xFF;
char g_meshcore_http_ota_display_line[28] = {0};

bool ESP32Board::getHttpOtaProgress(uint8_t& pct, const char*& line) {
if (!g_meshcore_http_ota_display_active) return false;
pct = g_meshcore_http_ota_display_pct;
line = g_meshcore_http_ota_display_line;
return true;
}

// The address is read from eFuse, so it is valid whether or not that radio is on. The S2 has
// no Bluetooth, so its BLE read fails.
bool ESP32Board::getDeviceMac(uint8_t kind, uint8_t mac[6]) {
esp_mac_type_t type = kind == DEVICE_MAC_BLE ? ESP_MAC_BT : ESP_MAC_WIFI_STA;
return esp_read_mac(mac, type) == ESP_OK;
}

// Download mode on the next reset via the RTC force-download bit, which only the S2, S3 and C3
// have (not the classic ESP32 or the C6).
#if defined(RTC_CNTL_OPTION1_REG) && defined(RTC_CNTL_FORCE_DOWNLOAD_BOOT)
bool ESP32Board::canRebootToBootloader() const { return true; }

void ESP32Board::rebootToBootloader() {
#ifdef PIN_TFT_LEDA_CTL
if (PIN_TFT_LEDA_CTL >= 0) { // blank the backlight: a dark screen signals download mode
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
}
#endif
// Read-modify-write ONLY the force-download bit. A full REG_WRITE zeroes the rest of
// RTC_CNTL_OPTION1 and wedges the RTC so esp_restart() hangs instead of resetting.
// The bit lives in the RTC domain and survives esp_restart().
uint32_t opt1 = REG_READ(RTC_CNTL_OPTION1_REG);
REG_WRITE(RTC_CNTL_OPTION1_REG, opt1 | RTC_CNTL_FORCE_DOWNLOAD_BOOT);
reboot();
}
#else
bool ESP32Board::canRebootToBootloader() const { return false; }
void ESP32Board::rebootToBootloader() { }
#endif

bool ESP32Board::getMemStats(MemStats& s) {
s.free_heap = ESP.getFreeHeap();
s.max_alloc_heap = ESP.getMaxAllocHeap();
s.min_free_heap = ESP.getMinFreeHeap();
s.free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
s.free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); // 0 without PSRAM
return true;
}

#if (defined(ADMIN_PASSWORD) || defined(MULTI_TRANSPORT_COMPANION)) && !defined(DISABLE_WIFI_OTA)
#include <WiFi.h>
#include <AsyncTCP.h>
Expand Down
6 changes: 6 additions & 0 deletions src/helpers/ESP32Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ class ESP32Board : public mesh::MainBoard {
void pollHttpOtaReboot();
bool checkManifestOta(const char* current_ver, char* file_url, size_t file_url_sz, char reply[]) override;

bool getDeviceMac(uint8_t kind, uint8_t mac[6]) override;
bool canRebootToBootloader() const override;
void rebootToBootloader() override;
bool getMemStats(MemStats& s) override;
bool getHttpOtaProgress(uint8_t& pct, const char*& line) override;

void setInhibitSleep(bool inhibit) {
inhibit_sleep = inhibit;
}
Expand Down
16 changes: 16 additions & 0 deletions src/helpers/NRF52Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,22 @@ void NRF52Board::powerOff() {
}
}

// The BLE address the SoftDevice uses by default: the static random address in FICR, with
// the two top bits set. Read from FICR so it works before (or without) Bluefruit.begin().
// No Wi-Fi on the nRF52.
bool NRF52Board::getDeviceMac(uint8_t kind, uint8_t mac[6]) {
if (kind != DEVICE_MAC_BLE) return false;
uint32_t lo = NRF_FICR->DEVICEADDR[0];
uint32_t hi = NRF_FICR->DEVICEADDR[1];
mac[0] = ((hi >> 8) & 0xFF) | 0xC0;
mac[1] = hi & 0xFF;
mac[2] = (lo >> 24) & 0xFF;
mac[3] = (lo >> 16) & 0xFF;
mac[4] = (lo >> 8) & 0xFF;
mac[5] = lo & 0xFF;
return true;
}

bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) {
static const char BOOTLOADER_MARKER[] = "UF2 Bootloader ";
const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B
Expand Down
1 change: 1 addition & 0 deletions src/helpers/NRF52Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class NRF52Board : public mesh::MainBoard {
virtual void powerOff() override;
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
virtual bool startOTAUpdate(const char *id, char reply[]) override;
bool getDeviceMac(uint8_t kind, uint8_t mac[6]) override;
virtual void sleep(uint32_t secs) override;
bool isExternalPowered() override;

Expand Down
Loading