diff --git a/README.md b/README.md index 7c11932c..a8f8546d 100644 --- a/README.md +++ b/README.md @@ -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)) | diff --git a/changelog_ev.md b/changelog_ev.md index fd2804ac..24d6bd28 100644 --- a/changelog_ev.md +++ b/changelog_ev.md @@ -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. diff --git a/examples/companion_radio/CompanionSNMP.cpp b/examples/companion_radio/CompanionSNMP.cpp index 072c95f9..4ce00cbe 100644 --- a/examples/companion_radio/CompanionSNMP.cpp +++ b/examples/companion_radio/CompanionSNMP.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -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); }); @@ -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 { @@ -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() { diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 34f2966d..37bc029d 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -9,21 +9,7 @@ #include #include // derive a region's flood-scope key from its #hashtag name #include // gmtime_r for the "clock" CLI command -#ifdef ESP32 -#include // esp_restart for the "bootloader" CLI command -#if __has_include() -#include // 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() -#include // esp_read_mac for the "mac" / "get mac" CLI commands -#endif -#endif #include -#include #include #include "WiFiConfig.h" #include "CompanionSNMP.h" @@ -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 { @@ -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")) { @@ -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); diff --git a/src/MeshCore.h b/src/MeshCore.h index 4591344c..dc2acac8 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -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; } diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index f29c0e24..1b17c304 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -8,6 +8,14 @@ #include #include #include +#include +#include +#if __has_include() +#include // esp_read_mac (IDF 5; esp_system.h has it on IDF 4) +#endif +#if __has_include() +#include // 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; @@ -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 #include diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 5cafdf61..8cd8b4aa 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -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; } diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index f2fa7743..355f102a 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -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 diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 43944b5f..3202f30b 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -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;