feat(l2): Arduino-ESP32 3.3.12 / IDF 5.5 env, NimBLE and new I2S driver ports, large UI buffers in PSRAM

- New env Wio_Tracker_L2_companion_solo_lvgl_v3 on pioarduino 55.03.312-1;
  the 2.0.17 env stays until the new one passes the hardware checklist
- SerialBLEInterface: NimBLE variant (PIN pairing through encrypted,
  authenticated characteristic properties; setAccessPermissions is a no-op)
- buzzer: i2s_std driver on IDF 5 (legacy driver kept for 2.0.17)
- ESPNOWRadio: IDF 5 callback signatures
- ui-lvgl: psramBuf() for trail drawing, tile cache, map marks, list rows,
  keyboard maps and message metadata; internal heap back to 2.0.17 level

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-26 09:14:58 +02:00
co-authored by Claude Opus 5.5
parent ba4d01aa77
commit 331a4e64c4
12 changed files with 221 additions and 55 deletions
+31 -6
View File
@@ -5,6 +5,16 @@
#if defined(ESP32)
#include <esp_heap_caps.h>
#endif
#include <new>
// A zeroed buffer of `n` T in PSRAM when the board has it: internal RAM is what
// BLE, WiFi and TLS need. Allocated once (at startup or first use), never freed.
template <class T> static T* psramBuf(size_t n) {
#if defined(ESP32)
if (void* p = heap_caps_calloc(n, sizeof(T), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)) return (T*)p;
#endif
return (T*)calloc(n, sizeof(T));
}
#include "../ui-core/UiCore.h" // shared UI Core (header-only, this TU)
#include "../ui-core/NearbyModel.h"
@@ -254,6 +264,19 @@ void UITask::loop() {
_core->loop();
drainCoreEvents();
#if defined(UI_HEAP_REPORT) && defined(ESP32)
// -D UI_HEAP_REPORT: internal / PSRAM heap once, 20 s after boot (the
// framework comparison in docs/development/l2-roadmap.md).
static bool heap_done = false;
if (!heap_done && millis() > 20000) {
heap_done = true;
Serial.printf("HEAP internal free %u largest %u min %u | psram free %u | idf %s\n",
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL),
(unsigned)heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL),
(unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM), esp_get_idf_version());
}
#endif
#ifdef PIN_BUZZER
_buzzer.loop();
if (soundctl::autoTick(_prefs, _buzzer, isClientConnected()) && !_asleep) refreshStatusBar();
@@ -853,7 +876,8 @@ static void onOpenChannel(lv_event_t* e) {
// DM rows carry a 4-byte prefix; kept in a static table the rows point into.
static uint8_t s_dm_rows[MessageHistory::DM_HIST_MAX][4];
static uint8_t s_contact_rows[64][PUB_KEY_SIZE];
static const int CONTACT_ROWS_MAX = 64;
static uint8_t (*s_contact_rows)[PUB_KEY_SIZE] = psramBuf<uint8_t[PUB_KEY_SIZE]>(CONTACT_ROWS_MAX);
static void onOpenDMRow(lv_event_t* e) {
s_ui->openDM(s_dm_rows[(uintptr_t)lv_event_get_user_data(e)]);
@@ -865,7 +889,8 @@ static void onNewChat(lv_event_t* e) { (void)e; s_ui->showContacts(); }
static void onChanRowHold(lv_event_t* e); // ChannelScreen.h
static void onChanAdd(lv_event_t* e);
static void onChanThreadMenu(lv_event_t* e);
static uint8_t s_room_rows[16][PUB_KEY_SIZE];
static const int ROOM_ROWS_MAX = 16;
static uint8_t (*s_room_rows)[PUB_KEY_SIZE] = psramBuf<uint8_t[PUB_KEY_SIZE]>(ROOM_ROWS_MAX);
static void onDMRowHold(lv_event_t* e); // ConversationScreen.h
static void onRoomRow(lv_event_t* e);
static void onRoomRowHold(lv_event_t* e);
@@ -1007,7 +1032,7 @@ void UITask::buildChats() {
else snprintf(rt, sizeof(rt), "ROOMS");
sectionWithFilter(body, rt, room_fav_only, CF_ROOMS);
int nrooms = 0, total = the_mesh.getNumContacts();
const int MAX_ROOMS = (int)(sizeof(s_room_rows) / sizeof(s_room_rows[0]));
const int MAX_ROOMS = ROOM_ROWS_MAX;
for (int pass = fav_first ? 0 : 1; pass < 2; pass++) {
for (int i = 0; i < total && nrooms < MAX_ROOMS; i++) {
ContactInfo c;
@@ -1055,7 +1080,7 @@ void UITask::buildContacts() {
int rows = 0;
// +MAX_ANON_CONTACTS: getContactByIdx() takes the raw table index (see
// MessageHistory::contactByPrefix()).
for (int i = 0; i < total && rows < (int)(sizeof(s_contact_rows) / sizeof(s_contact_rows[0])); i++) {
for (int i = 0; i < total && rows < CONTACT_ROWS_MAX; i++) {
ContactInfo c;
if (!the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c)) continue;
if (c.type != ADV_TYPE_CHAT) continue;
@@ -1723,12 +1748,12 @@ uint32_t UITask::threadSignature() const {
// for the Go / Save buttons under such a bubble.
struct MsgLoc { int32_t lat, lon; char label[WAYPOINT_LABEL_LEN * 2]; };
static const int THREAD_MAX_SHOWN = 30;
static MsgLoc s_msg_locs[THREAD_MAX_SHOWN];
static MsgLoc* s_msg_locs = psramBuf<MsgLoc>(THREAD_MAX_SHOWN);
static int s_msg_loc_n = 0;
// What a held bubble is about: its history ring entry, sender, position slot.
struct MsgMeta { int pos; bool channel; bool own; int loc; char from[32]; };
static MsgMeta s_msg_meta[THREAD_MAX_SHOWN];
static MsgMeta* s_msg_meta = psramBuf<MsgMeta>(THREAD_MAX_SHOWN);
static int s_msg_meta_n = 0;
static int noteMsgMeta(int pos, bool channel, bool own, int loc, const char* from) {