mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-27 00:38:05 +00:00
V4-R8 perf pass + M9 UI polish (prefs v49/v50)
Heltec V4-R8 — "are we getting full performance?" audit. Compute side was already at spec; six gaps closed (record: variants/heltec_v4/R8_AUDIT.md "Perf pass"): - FEM LNA defaulted OFF on a KCT8103L board: prefs v49 defaults it ON on the R8 (one-time flip of existing installs, no new field). Toggle stays in Radio & Mesh. - Display SPI 40 -> 80 MHz (LGFX_SPI_WRITE_HZ; -D to 40000000 steps back). - Async DMA band flush (LGFXDisplay::flushBandRGB565): swap into a 12 KB internal DMA buffer, one no-convert DMA per band, lv_disp_flush_ready at once so LVGL renders band N+1 while band N drains; lv_disp_flush_is_last closes the frame transaction so the shared micro-SD gets the bus between frames. Sync fallback. - Wi-Fi modem power save is a pref (wifi_ps; toggle in Wi-Fi settings, live once associated). Default ON everywhere (unchanged), OFF on the R8. - micro-SD 4 -> 20 MHz after the proven mount, read-verified (probe-file head captured at 4 MHz and byte-compared after the raise; falls back). Wired at all three mount sites; no-op without SD_SPI_FAST_HZ. Note: SDFS::begin returns true if already mounted, so every clock change goes through SD.end() first. - R8 env boots at ESP32_CPU_FREQ=240 (setup() no longer runs at the V4's 80 MHz). - Settings -> About "Perf:" row on the R8 (no serial console on that board): "CPU 240 · TFT 80 MHz DMA · SD 20 MHz · LNA on" is the all-green reading. ThinkNode M9 UI: - Spectrum page root gets NAV_SKIP_FLAG: nothing on it is actionable, so the d-pad no longer highlights the chart / scale box; Back closes it as before. - Map "Show tile z/x/y" line defaults OFF (prefs v50, one-time flip; toggle kept). - The tab bar is bypassed entirely on the M9: the accent "glow" indicator lived on the screen, not in the bar, so hiding the zero-height bar left it glowing over the content's bottom edge. No bar styling / gestures / key hints / indicator are built on the M9 now. Cross-compiled: R8, V4, M9, T-Deck. Flashed + booted on the user's R8 and M9. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
16e883187c
commit
844d8c3357
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
// Post-mount micro-SD operating-clock raise (perf pass 2026-08-20; Heltec V4-R8 first).
|
||||
//
|
||||
// Every SPI-SD board mounts the card with a conservative ladder that tops out at 4 MHz,
|
||||
// and SD.begin()'s clock is the operating clock for the whole session — so the card that
|
||||
// is the PRIMARY store on the R8 (contacts, chat history, sync log, file manager) ran at
|
||||
// ~400 KB/s on traces that carry the display at 80 MHz. Standard SD bring-up is "initialise
|
||||
// slow, then raise": once the 4 MHz mount has proved the card, re-begin at SD_SPI_FAST_HZ
|
||||
// and READ-VERIFY it — a probe file's first 512 bytes are read at the proven clock first
|
||||
// and compared byte-for-byte after the raise (SPI-mode SD has no data CRC by default, so a
|
||||
// bare "mount succeeded" would not catch a marginal clock). Any mismatch or failure drops
|
||||
// straight back to the clock that just worked. Boards that do not define SD_SPI_FAST_HZ
|
||||
// (or set it <= 4 MHz) get a no-op — nothing changes for the T-Deck/M9/Pager ladders.
|
||||
//
|
||||
// Returns the operating clock after the call (cur_hz if the raise was refused or failed),
|
||||
// or 0 if the card could not be re-mounted at all (the caller treats that as unmounted).
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#if defined(ESP32)
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef SD_SPI_FAST_HZ
|
||||
#define SD_SPI_FAST_HZ 0
|
||||
#endif
|
||||
|
||||
static inline uint32_t sdTryFastClock(uint8_t cs_pin, SPIClass& spi, uint32_t cur_hz, const char* tag) {
|
||||
#if SD_SPI_FAST_HZ > 4000000
|
||||
if (cur_hz == 0 || cur_hz >= (uint32_t)SD_SPI_FAST_HZ) return cur_hz;
|
||||
|
||||
// 1) Pick a probe file and capture its head at the proven clock.
|
||||
char probe_path[96] = {0};
|
||||
uint8_t ref[512];
|
||||
size_t ref_len = 0;
|
||||
{
|
||||
File root = SD.open("/");
|
||||
if (root && root.isDirectory()) {
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
File e = root.openNextFile();
|
||||
if (!e) break;
|
||||
if (!e.isDirectory() && e.size() > 0 && e.path() && strlen(e.path()) < sizeof probe_path) {
|
||||
strlcpy(probe_path, e.path(), sizeof probe_path);
|
||||
ref_len = e.size() < sizeof ref ? e.size() : sizeof ref;
|
||||
if (e.read(ref, ref_len) != ref_len) { probe_path[0] = 0; ref_len = 0; }
|
||||
e.close();
|
||||
break;
|
||||
}
|
||||
e.close();
|
||||
}
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Re-begin at the fast clock.
|
||||
SD.end();
|
||||
delay(20);
|
||||
bool ok = SD.begin(cs_pin, spi, SD_SPI_FAST_HZ, "/sd", 6) && SD.cardType() != CARD_NONE;
|
||||
|
||||
// 3) Verify: directory walk always; byte-compare of the probe head when we have one.
|
||||
if (ok) {
|
||||
File root = SD.open("/");
|
||||
ok = root && root.isDirectory();
|
||||
if (ok) { File e = root.openNextFile(); if (e) e.close(); root.close(); }
|
||||
}
|
||||
if (ok && probe_path[0]) {
|
||||
uint8_t now[512];
|
||||
File p = SD.open(probe_path, FILE_READ);
|
||||
ok = p && p.read(now, ref_len) == ref_len && memcmp(now, ref, ref_len) == 0;
|
||||
if (p) p.close();
|
||||
}
|
||||
if (ok) {
|
||||
Serial.printf("[%s] SD clock %lu -> %lu Hz (%s)\n", tag, (unsigned long)cur_hz,
|
||||
(unsigned long)SD_SPI_FAST_HZ, probe_path[0] ? "read-verified" : "dir-verified, no probe file");
|
||||
return (uint32_t)SD_SPI_FAST_HZ;
|
||||
}
|
||||
|
||||
// 4) Fall back to the clock that just worked.
|
||||
Serial.printf("[%s] SD %lu Hz verify FAILED; back to %lu Hz\n", tag,
|
||||
(unsigned long)SD_SPI_FAST_HZ, (unsigned long)cur_hz);
|
||||
for (int attempt = 0; attempt < 2; ++attempt) {
|
||||
SD.end();
|
||||
delay(attempt == 0 ? 60 : 150);
|
||||
if (SD.begin(cs_pin, spi, cur_hz, "/sd", 6) && SD.cardType() != CARD_NONE) return cur_hz;
|
||||
}
|
||||
Serial.printf("[%s] SD remount at %lu Hz failed\n", tag, (unsigned long)cur_hz);
|
||||
return 0;
|
||||
#else
|
||||
(void)cs_pin; (void)spi; (void)tag;
|
||||
return cur_hz;
|
||||
#endif
|
||||
}
|
||||
#endif // ESP32
|
||||
@@ -252,6 +252,17 @@ build_flags =
|
||||
${env:heltec_v4_tft_companion_radio_usb_tcp_touch.build_flags}
|
||||
; --- V4-R8 deltas (later -D wins on the GCC command line) ---
|
||||
-D HELTEC_LORA_V4_R8
|
||||
; Boot at the S3's full clock. The base env's ESP32_CPU_FREQ=80 is applied in
|
||||
; ESP32Board::begin(), so ALL of setup() — radio init, the SD mount ladder, the
|
||||
; store load, mesh begin, Wi-Fi bring-up — ran at a third speed until UITask::begin
|
||||
; bumped it to 240. The screen-off DFS drop to 80 MHz (setCpuForScreen) is unaffected.
|
||||
-D ESP32_CPU_FREQ=240
|
||||
; micro-SD operating clock (perf pass 2026-08-20): after the conservative 4 MHz mount
|
||||
; succeeds, re-begin at this clock with a directory + file READ verify and fall back
|
||||
; to 4 MHz if it fails (include/SdFastClock.h). The same traces already carry the
|
||||
; display at 80 MHz, and the card is the primary store on this board (contacts, chat
|
||||
; history, sync log) — 4 MHz was the M9 ladder's value, never re-tuned here.
|
||||
-D SD_SPI_FAST_HZ=20000000
|
||||
-D P_LORA_TX_LED=46
|
||||
-D PIN_VEXT_EN=40
|
||||
-D PIN_VEXT_EN_ACTIVE=LOW
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
namespace TouchPrefsSchema {
|
||||
|
||||
static constexpr uint16_t MAGIC = 0x5743; // 'WC' (WadaCfg)
|
||||
static constexpr uint8_t CURRENT_VERSION = 48;
|
||||
static constexpr uint8_t CURRENT_VERSION = 50; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields)
|
||||
static constexpr uint8_t BROKEN_MID_INSERT_VERSION = 44;
|
||||
|
||||
// Persisted byte layout. New fields must be appended at the end: older blobs
|
||||
|
||||
@@ -37,7 +37,7 @@ static bool s_begun = false;
|
||||
// short read (→ treat as absent → defaults); `ver` lets later builds add fields.
|
||||
static const char* KEY_CFG = "cfg";
|
||||
static const uint16_t TOUCH_CFG_MAGIC = TouchPrefsSchema::MAGIC;
|
||||
static const uint8_t TOUCH_CFG_VER = TouchPrefsSchema::CURRENT_VERSION; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large; v8 ui_scale; v9 tb_keypad; v10 sleep_idle; v11 nav_keys; v12 map_zoom_buttons; v13 nav_dir_keys; v14 home_is_drawer; v15 kbd_nav default ON (one-time migrate); v16 nav_scroll_keys; v17 notify_new_contact; v18 kbd_nav OFF by default (reverses v15; T-Deck/V4 only, Tanmatsu stays on); v19 show_sensors_tab; v20 map_show_links; v21 map_style (0=OSM default, 1=OpenTopoMap); v22 tb_nav; v23 scope_direct (opt-in: scope direct/login floods to the region); v24 tb_nav default OFF (experimental); v25 fem_lna (Heltec V4.3 high-gain FEM LNA, opt-in); v26 msg_flash (flash keyboard backlight + wake screen on a new message, opt-in); v27 flood_adv_hrs + local_adv_min (periodic self-advert intervals, the standard MeshCore flood/local advert on a timer); v28 beta_updates (opt-in to test/beta firmware on the OTA update check + install); v29 ui_scale default -> Large/150% (Tanmatsu; bumps the old 100% default, leaves an explicit Large/Huge choice); v30 boot_advert (opt-in one-shot flood self-advert ~6s after boot, all boards, #76); v31 compact_chat (opt-in IRC-style dense chat rows instead of bubbles); v32 clock_floor (highest epoch handed out — monotonic send-timestamp floor across reboots, #89); v33 rx_queue (buffered LoRa receive: drain task + packet ring, experimental, default OFF); v34 web_mirror (web control panel: mirror the live UI to a phone browser + inject taps, opt-in, default OFF); v35 remote_mode (render the UI off-screen at a web resolution instead of the panel; boot mode, default OFF); v36 remote_landscape (remote mode orientation: landscape 800x480 vs portrait 480x800); v37 remote_landscape now defaults ON (remote mode = landscape/desktop by default; one-time flip of existing installs, portrait stays a toggle); v38 web_terminal (web mesh CLI terminal served on the device IP; runtime toggle, mutually exclusive with VNC, default OFF); v40 hist_sync_after (chat-history flush: consecutive off-thread write failures before the blocking loop-task fallback, 0 = never); v41 p4_antenna (T-Display P4 antenna select; now RESERVED/unused - the choice is session-only so every boot comes up on the on-board antenna); v42 hist_per_chat (max stored messages PER chat, default 250 - a busy public channel used to be able to fill the whole shared ring and drag the UI down); v43 Pager UI-size presets (reset the previously ignored large-screen default to Small once); v44 broken retry_echo mid-struct insertion; v45 moves retry_echo to the actual tail and resets the ambiguous v44 suffix
|
||||
static const uint8_t TOUCH_CFG_VER = TouchPrefsSchema::CURRENT_VERSION; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large; v8 ui_scale; v9 tb_keypad; v10 sleep_idle; v11 nav_keys; v12 map_zoom_buttons; v13 nav_dir_keys; v14 home_is_drawer; v15 kbd_nav default ON (one-time migrate); v16 nav_scroll_keys; v17 notify_new_contact; v18 kbd_nav OFF by default (reverses v15; T-Deck/V4 only, Tanmatsu stays on); v19 show_sensors_tab; v20 map_show_links; v21 map_style (0=OSM default, 1=OpenTopoMap); v22 tb_nav; v23 scope_direct (opt-in: scope direct/login floods to the region); v24 tb_nav default OFF (experimental); v25 fem_lna (Heltec V4.3 high-gain FEM LNA, opt-in); v26 msg_flash (flash keyboard backlight + wake screen on a new message, opt-in); v27 flood_adv_hrs + local_adv_min (periodic self-advert intervals, the standard MeshCore flood/local advert on a timer); v28 beta_updates (opt-in to test/beta firmware on the OTA update check + install); v29 ui_scale default -> Large/150% (Tanmatsu; bumps the old 100% default, leaves an explicit Large/Huge choice); v30 boot_advert (opt-in one-shot flood self-advert ~6s after boot, all boards, #76); v31 compact_chat (opt-in IRC-style dense chat rows instead of bubbles); v32 clock_floor (highest epoch handed out — monotonic send-timestamp floor across reboots, #89); v33 rx_queue (buffered LoRa receive: drain task + packet ring, experimental, default OFF); v34 web_mirror (web control panel: mirror the live UI to a phone browser + inject taps, opt-in, default OFF); v35 remote_mode (render the UI off-screen at a web resolution instead of the panel; boot mode, default OFF); v36 remote_landscape (remote mode orientation: landscape 800x480 vs portrait 480x800); v37 remote_landscape now defaults ON (remote mode = landscape/desktop by default; one-time flip of existing installs, portrait stays a toggle); v38 web_terminal (web mesh CLI terminal served on the device IP; runtime toggle, mutually exclusive with VNC, default OFF); v40 hist_sync_after (chat-history flush: consecutive off-thread write failures before the blocking loop-task fallback, 0 = never); v41 p4_antenna (T-Display P4 antenna select; now RESERVED/unused - the choice is session-only so every boot comes up on the on-board antenna); v42 hist_per_chat (max stored messages PER chat, default 250 - a busy public channel used to be able to fill the whole shared ring and drag the UI down); v43 Pager UI-size presets (reset the previously ignored large-screen default to Small once); v44 broken retry_echo mid-struct insertion; v45 moves retry_echo to the actual tail and resets the ambiguous v44 suffix; v46 app_hide; v47 MQTT hidden by default; v48 lang_file; v49 fem_lna default ON on the V4-R8 (KCT8103L FEM; one-time flip of existing installs, no new field); v50 map_show_tilexyz default OFF (tile z/x/y line hidden; one-time flip, no new field)
|
||||
|
||||
// Defaults (kept identical to the historical per-key defaults).
|
||||
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
|
||||
@@ -106,7 +106,7 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
c.map_night = 0; // default: normal (light) tiles
|
||||
c.map_zoom = 0; // 0 = unset -> auto-snap on first map open
|
||||
c.map_show_coords = 1; // default: show coords / tile line / contacts
|
||||
c.map_show_tilexyz = 1;
|
||||
c.map_show_tilexyz = 0; // v50: the "z12 12/2105/1376" tile-path line is developer clutter on the map; opt-in via Map options
|
||||
c.map_show_contacts = 1;
|
||||
c.app_grid_large = 0; // default: compact app grid (T-Deck 4 cols / V4 3 cols)
|
||||
#if defined(TLORA_PAGER)
|
||||
@@ -121,7 +121,13 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
#endif
|
||||
c.tb_nav = 0; // T-Deck trackball: soft-cursor by default. D-pad UI nav is EXPERIMENTAL (opt-in)
|
||||
c.scope_direct = 0; // OFF: direct/login floods stay unscoped (cross-region safe). Opt-in per issue #64.
|
||||
#if defined(HELTEC_LORA_V4_R8)
|
||||
c.fem_lna = 1; // ON (v49): the V4-R8 is a V4.3.1-generation board with the KCT8103L FEM, whose
|
||||
// switchable ~17 dB LNA is the whole point of that FEM revision — shipping it
|
||||
// bypassed left RX sensitivity on the table. Toggle stays in Radio & Mesh.
|
||||
#else
|
||||
c.fem_lna = 0; // OFF: V4.3 FEM LNA bypassed (matches the hardware default). Opt-in high-gain RX.
|
||||
#endif
|
||||
c.msg_flash = 0; // OFF: opt-in new-message keyboard/screen flash
|
||||
c.flood_adv_hrs = 0; // OFF: no periodic flood self-advert (advertise manually)
|
||||
c.local_adv_min = 0; // OFF: no periodic zero-hop self-advert
|
||||
@@ -217,6 +223,13 @@ static void cfgLoadOrMigrate() {
|
||||
if (stored_version < 24) s_cfg.tb_nav = 0;
|
||||
// v25: new trailing field — V4.3 FEM LNA OFF on existing installs (matches hardware default).
|
||||
if (stored_version < 25) s_cfg.fem_lna = 0;
|
||||
#if defined(HELTEC_LORA_V4_R8)
|
||||
// v49: FEM LNA ON by default on the V4-R8 (KCT8103L). One-time flip of existing
|
||||
// installs so they match the new default; an explicit later off/on persists.
|
||||
if (stored_version < 49) s_cfg.fem_lna = 1;
|
||||
#endif
|
||||
// v50: map tile z/x/y overlay line OFF by default (one-time flip; the Map-options toggle persists afterwards).
|
||||
if (stored_version < 50) s_cfg.map_show_tilexyz = 0;
|
||||
if (stored_version < 26) s_cfg.msg_flash = 0;
|
||||
if (stored_version < 27) { s_cfg.flood_adv_hrs = 0; s_cfg.local_adv_min = 0; }
|
||||
if (stored_version < 28) s_cfg.beta_updates = 0;
|
||||
|
||||
@@ -17,6 +17,12 @@ static const char *WIFI_CONFIG_PWD_KEY = "wifi_pwd";
|
||||
static const char *WIFI_CONFIG_RADIO_EN_KEY = "wifi_radio_en";
|
||||
static const char *WIFI_CONFIG_WIFI_CHOSEN_KEY = "wifi_chosen";
|
||||
static const char *WIFI_CONFIG_BLE_EN_KEY = "ble_en"; // BLE radio on/off (default on)
|
||||
static const char *WIFI_CONFIG_PS_KEY = "wifi_ps"; // modem power save once associated
|
||||
#if defined(HELTEC_LORA_V4_R8)
|
||||
#define WIFI_CONFIG_PS_DEFAULT 0 // USB-powered Expansion Kit: latency + link stability over mA
|
||||
#else
|
||||
#define WIFI_CONFIG_PS_DEFAULT 1 // battery boards keep the DTIM sleep they always had
|
||||
#endif
|
||||
|
||||
static SdNvsPrefs s_prefs;
|
||||
static bool s_begun = false;
|
||||
@@ -143,6 +149,20 @@ void wifiConfigSetBleEnabled(bool enabled) {
|
||||
s_begun = s_prefs.begin(WIFI_CONFIG_NAMESPACE, true);
|
||||
}
|
||||
|
||||
bool wifiConfigGetPowerSave() {
|
||||
if (!s_begun) wifiConfigBegin();
|
||||
return s_prefs.getUChar(WIFI_CONFIG_PS_KEY, WIFI_CONFIG_PS_DEFAULT) != 0;
|
||||
}
|
||||
|
||||
void wifiConfigSetPowerSave(bool enabled) {
|
||||
if (!s_begun) wifiConfigBegin();
|
||||
s_prefs.end();
|
||||
if (!s_prefs.begin(WIFI_CONFIG_NAMESPACE, false)) return;
|
||||
s_prefs.putUChar(WIFI_CONFIG_PS_KEY, enabled ? 1 : 0);
|
||||
s_prefs.end();
|
||||
s_begun = s_prefs.begin(WIFI_CONFIG_NAMESPACE, true);
|
||||
}
|
||||
|
||||
#if defined(TLORA_PAGER)
|
||||
void wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase phase) {
|
||||
s_pager_wifi_ble_phase = phase;
|
||||
|
||||
@@ -27,6 +27,14 @@ void wifiConfigSetRadioEnabled(bool enabled);
|
||||
bool wifiConfigGetBleEnabled();
|
||||
void wifiConfigSetBleEnabled(bool enabled);
|
||||
|
||||
/* Wi-Fi modem power save (DTIM sleep) once associated. ON saves power and gives BLE
|
||||
* coexistence airtime; OFF keeps the RX chain up for a lower-latency companion-TCP /
|
||||
* web-mirror link and holds a marginal association better. Default ON everywhere
|
||||
* except the V4-R8 (USB-powered Expansion Kit form factor; perf pass 2026-08-20).
|
||||
* Applied by the main loop after association and live by the Wi-Fi settings toggle. */
|
||||
bool wifiConfigGetPowerSave();
|
||||
void wifiConfigSetPowerSave(bool enabled);
|
||||
|
||||
#if defined(TLORA_PAGER)
|
||||
/* Pager coexistence ownership. Wi-Fi intent alone cannot answer whether a
|
||||
* cold NimBLE start is safe: touch builds keep the STA scannable even with no
|
||||
|
||||
+14
-1
@@ -51,6 +51,7 @@ static uint32_t _atoi(const char* sp) {
|
||||
#include <SPIFFS.h>
|
||||
#if defined(HAS_TDECK_GT911) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#include <SD.h>
|
||||
#include "SdFastClock.h" // post-mount operating-clock raise (SD_SPI_FAST_HZ boards)
|
||||
#include <Preferences.h>
|
||||
#if defined(TLORA_PAGER)
|
||||
#include <mbedtls/sha256.h>
|
||||
@@ -996,6 +997,7 @@ void setup() {
|
||||
delay(60);
|
||||
if (SD.begin(PIN_SD_CS, *_spi, 4000000, "/sd", 6) && SD.cardType() != CARD_NONE) {
|
||||
Serial.printf("[BOOT] SD renegotiated %lu -> 4000000 Hz\n", (unsigned long)mounted_hz);
|
||||
mounted_hz = 4000000;
|
||||
} else {
|
||||
SD.end();
|
||||
delay(120);
|
||||
@@ -1003,6 +1005,9 @@ void setup() {
|
||||
if (sd_mounted) Serial.printf("[BOOT] SD stays at %lu Hz (4 MHz renegotiation failed)\n", (unsigned long)mounted_hz);
|
||||
}
|
||||
}
|
||||
// Operating-clock raise with read-verify (SD_SPI_FAST_HZ boards only; no-op elsewhere).
|
||||
if (sd_mounted) { mounted_hz = sdTryFastClock(PIN_SD_CS, *_spi, mounted_hz, "BOOT"); sd_mounted = mounted_hz != 0; }
|
||||
if (sd_mounted) { extern uint32_t g_sd_operating_hz; g_sd_operating_hz = mounted_hz; } // About-page readout (UITask.cpp)
|
||||
}
|
||||
#endif
|
||||
if (sd_mounted) {
|
||||
@@ -1686,8 +1691,16 @@ void loop() {
|
||||
// BLE coexistence airtime). Deferred to here on purpose: enabling it on the
|
||||
// unassociated STA naps the radio through a scan dwell and breaks the setup
|
||||
// wizard's WiFi.scanNetworks() ("no networks found"). One-shot.
|
||||
// Persisted preference (Wi-Fi settings -> "Power save"): default ON, except the V4-R8
|
||||
// where it defaults OFF (perf pass 2026-08-20 — lower-latency app link, steadier
|
||||
// association on its weak 2.4 GHz path). The toggle also applies live once associated.
|
||||
static bool modem_sleep_set = false;
|
||||
if (!modem_sleep_set) { WiFi.setSleep(true); modem_sleep_set = true; }
|
||||
if (!modem_sleep_set) {
|
||||
const bool ps = wifiConfigGetPowerSave();
|
||||
WiFi.setSleep(ps);
|
||||
Serial.printf("[wifi] modem power save %s\n", ps ? "on" : "off");
|
||||
modem_sleep_set = true;
|
||||
}
|
||||
if (!sntp_kicked) {
|
||||
/* Brussels timezone with DST rules baked in (POSIX "CET-1CEST,...").
|
||||
* On touch builds the base is shifted by the user's manual hour offset
|
||||
|
||||
+105
-21
@@ -62,6 +62,7 @@
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#include <SD.h> // microSD — T-Deck/M9 on the LoRa SPI, V4-R8 on the TFT SPI
|
||||
#include "SdFastClock.h" // post-mount operating-clock raise (SD_SPI_FAST_HZ boards)
|
||||
#include "sd_diskio.h" // internal Arduino-SD drive helpers (sdcard_init / sd_*_raw)
|
||||
extern SPIClass* tdeckSharedSPI();
|
||||
// FatFs mkfs (the prebuilt ESP-IDF compiles f_setlabel OUT — FF_USE_LABEL=0 —
|
||||
@@ -2149,6 +2150,10 @@ static bool g_cap_touch_hw_started = false;
|
||||
// LVGL just calls flush_cb more often.
|
||||
static constexpr int LV_DRAW_BUF_LINES = 24;
|
||||
static lv_color_t* g_draw_buffer = nullptr;
|
||||
// Operating clock of the LAST successful SD.begin() on SPI-SD boards (0 = none). Recorded at
|
||||
// every mount/remount site (main.cpp boot adoption + the two UITask mounts) because SD.begin's
|
||||
// clock is the session clock; Settings -> About reports it on the R8 (no serial console there).
|
||||
uint32_t g_sd_operating_hz = 0;
|
||||
static uint32_t g_draw_buf_px = 240 * LV_DRAW_BUF_LINES; // actual buffer size in px; shrinks if the full alloc fails at boot
|
||||
#if CAP_LARGE_SCREEN
|
||||
// UI resolution scaling (Tanmatsu, no touchscreen). LVGL renders at s_lv_pw x s_lv_ph (PHYSICAL
|
||||
@@ -3230,7 +3235,16 @@ static void lvglFlush(lv_disp_drv_t* disp_drv, const lv_area_t* area, lv_color_t
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if defined(HELTEC_LORA_V4_R8)
|
||||
// Async DMA band flush: returns as soon as the swapped copy is handed to the SPI DMA, so
|
||||
// LVGL renders the next band while this one drains; the last band of the refresh closes
|
||||
// the frame transaction. The screenshot/web-mirror copies below read color_p, which the
|
||||
// driver has already copied out — unaffected. (LGFXDisplay::flushBandRGB565)
|
||||
display.flushBandRGB565(area->x1, area->y1, w, h, reinterpret_cast<uint16_t*>(color_p),
|
||||
lv_disp_flush_is_last(disp_drv));
|
||||
#else
|
||||
display.writePixelsRGB565(area->x1, area->y1, w, h, reinterpret_cast<uint16_t*>(color_p));
|
||||
#endif
|
||||
if (g_shot_buf) { // mirror this area into the screenshot buffer
|
||||
for (int32_t row = 0; row < h; ++row) {
|
||||
const int32_t dy = area->y1 + row;
|
||||
@@ -3821,10 +3835,10 @@ static void navRefocusFirstVisible(lv_obj_t* p) {
|
||||
}
|
||||
// Small key hints over each menubar icon — shown only while keyboard nav is on.
|
||||
static void navMenubarKeysSync() {
|
||||
#if defined(HAS_TANMATSU) || defined(TLORA_PAGER)
|
||||
#if defined(HAS_TANMATSU) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
// Tanmatsu menubar uses the coloured F-key shapes, not letter hotkeys. The
|
||||
// pager prints each fixed mnemonic beside its icon directly in the tab label,
|
||||
// so neither target needs this optional overlay. A plain `return` isn't enough
|
||||
// so neither target needs this optional overlay. The M9 has no tab bar at all. A plain `return` isn't enough
|
||||
// since the body below still needs s_kbd_nav to exist at compile time; exclude it.
|
||||
#else
|
||||
if (!g_lv.tabview) return;
|
||||
@@ -10639,7 +10653,8 @@ static void buildRadioSettings() {
|
||||
|
||||
#if defined(HELTEC_LORA_V4_TFT)
|
||||
// Heltec V4.3 only: the external FEM's high-gain receive amplifier (~17 dB). Bypassed by
|
||||
// default; a big win in quiet/remote sites, but can desensitize in noisy areas. This is
|
||||
// default on the plain V4 (ON by default on the V4-R8 since prefs v49); a big win in
|
||||
// quiet/remote sites, but can desensitize in noisy areas. This is
|
||||
// SEPARATE from the SX1262's tiny internal "boosted gain". Hidden on V4.2 (no switchable LNA).
|
||||
if (board.femLnaControllable()) {
|
||||
int rh = settingsRowLabel(body, y, 4, TR("High-gain receiver (FEM LNA)"), COLOR_TEXT, &g_font_12, 56);
|
||||
@@ -11434,6 +11449,7 @@ static void sdRestoreRun() {
|
||||
g_lv.task->persistHistoryNow();
|
||||
discoveredFlushNow();
|
||||
the_mesh.flushContactsIfDirty();
|
||||
the_mesh.persistSyncHistoryNow();
|
||||
if (!touchPrefsFlush()) {
|
||||
g_sd_migration_blocked = true;
|
||||
g_lv.task->showAlert(TR("Copy blocked: internal data is busy"), 2600);
|
||||
@@ -11449,7 +11465,6 @@ static void sdRestoreRun() {
|
||||
|
||||
#if defined(TLORA_PAGER)
|
||||
if (!meshcomodSdProfileMatchesInternal()) {
|
||||
the_mesh.persistSyncHistoryNow();
|
||||
g_lv.task->showAlert(TR("Copy blocked: SD card holds a different or unreadable profile"), 3600);
|
||||
return;
|
||||
}
|
||||
@@ -13387,6 +13402,22 @@ static void buildDeviceSettings(int sec) {
|
||||
mk_info(TR("Model:"), "Heltec LoRa32 V4 TFT (touch)");
|
||||
#else
|
||||
mk_info(TR("Model:"), "Heltec LoRa32 V4");
|
||||
#endif
|
||||
#if defined(HELTEC_LORA_V4_R8)
|
||||
// Perf-pass readout (2026-08-20). This board has no reachable serial console, so the
|
||||
// four runtime facts that pass needs verified live here: CPU clock, display bus clock +
|
||||
// flush mode (DMA = async band flush active), micro-SD operating clock, FEM LNA state.
|
||||
{
|
||||
char sd[16];
|
||||
if (g_sd_operating_hz >= 1000000) snprintf(sd, sizeof sd, "%lu MHz", (unsigned long)(g_sd_operating_hz / 1000000));
|
||||
else if (g_sd_operating_hz > 0) snprintf(sd, sizeof sd, "%lu kHz", (unsigned long)(g_sd_operating_hz / 1000));
|
||||
else snprintf(sd, sizeof sd, "none");
|
||||
snprintf(buf, sizeof(buf), "CPU %u · TFT %u MHz %s · SD %s · LNA %s",
|
||||
(unsigned)getCpuFrequencyMhz(), (unsigned)(LGFX_SPI_WRITE_HZ / 1000000),
|
||||
display.asyncFlushActive() ? "DMA" : "sync", sd,
|
||||
board.femLnaControllable() ? (touchPrefsGetFemLna() ? "on" : "off") : "n/a");
|
||||
mk_info(TR("Perf:"), buf);
|
||||
}
|
||||
#endif
|
||||
// Public key prefix (first 8 bytes = 16 hex)
|
||||
{
|
||||
@@ -14912,6 +14943,18 @@ static void buildMqttSettings() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
// Wi-Fi modem power save: persist + apply live. Only touches the driver once associated —
|
||||
// WiFi.setSleep() on an unassociated STA naps the radio through scan dwells (see main.cpp).
|
||||
// esp_wifi_set_ps is a plain setter (no disconnect/begin), so it is safe from the LVGL ctx.
|
||||
static void wifiPowerSaveToggleCb(lv_event_t* e) {
|
||||
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
|
||||
const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED);
|
||||
wifiConfigSetPowerSave(on);
|
||||
if (WiFi.status() == WL_CONNECTED) WiFi.setSleep(on);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void buildWifiSettings() {
|
||||
lv_obj_t* body = createSettingsModal("", SettingsModalKind::Wifi); // no group header — the top bar already says "Wi-Fi"
|
||||
int y = 0;
|
||||
@@ -14932,6 +14975,18 @@ static void buildWifiSettings() {
|
||||
lv_label_set_text(g_set_modal.wifi_sta_status_l, TR("Loading..."));
|
||||
y += SC(30);
|
||||
|
||||
// Modem power save (DTIM sleep) — see wifiPowerSaveToggleCb. Default ON; OFF on the V4-R8.
|
||||
{
|
||||
int rh = settingsRowLabel(body, y, 4, TR("Power save (modem sleep)"), COLOR_TEXT, &g_font_12, 56);
|
||||
lv_obj_t* sw = lv_switch_create(body);
|
||||
lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y);
|
||||
if (wifiConfigGetPowerSave()) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(sw, wifiPowerSaveToggleCb, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
y += LV_MAX(34, rh + 10);
|
||||
y += settingsRowLabel(body, y, 0, TR("Off: snappier app/TCP link, steadier on a weak signal. On: less power, kinder to Bluetooth."),
|
||||
COLOR_SUB, &g_font_12, 0) + 2;
|
||||
}
|
||||
|
||||
// Make sure the network we're currently using shows up as saved, and on first
|
||||
// run import any legacy 3-slot profiles into the new known-networks store.
|
||||
{
|
||||
@@ -19868,8 +19923,13 @@ static bool fmSdTryMount() {
|
||||
SD.end();
|
||||
delay(120);
|
||||
mounted = SD.begin(PIN_SD_CS, *spi, mounted_hz, "/sd", 6) && SD.cardType() != CARD_NONE;
|
||||
} else {
|
||||
mounted_hz = 4000000;
|
||||
}
|
||||
}
|
||||
// Operating-clock raise with read-verify (SD_SPI_FAST_HZ boards only; no-op elsewhere).
|
||||
if (mounted) { mounted_hz = sdTryFastClock(PIN_SD_CS, *spi, mounted_hz, "SD"); mounted = mounted_hz != 0; }
|
||||
if (mounted) g_sd_operating_hz = mounted_hz;
|
||||
}
|
||||
#endif
|
||||
if (mounted) {
|
||||
@@ -23489,6 +23549,12 @@ static void openSpectrumPage() {
|
||||
lv_obj_set_style_bg_opa(s_spec_root, LV_OPA_COVER, LV_PART_MAIN);
|
||||
lv_obj_clear_flag(s_spec_root, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_event_cb(s_spec_root, spectrumDismissCb, LV_EVENT_CLICKED, nullptr);
|
||||
// Keyboard/d-pad nav (M9, T-Deck, Tanmatsu): nothing on this page is actionable — the
|
||||
// chart and the waterfall's scale box are plain containers, so the nav collector was
|
||||
// highlighting them for no reason. Skip the whole subtree (same as the lock screen):
|
||||
// the focus group stays empty and the only way out is Back (hardware key / bar chevron /
|
||||
// Esc — all routed through the page ladder, none of which needs focus).
|
||||
lv_obj_add_flag(s_spec_root, NAV_SKIP_FLAG);
|
||||
|
||||
// Settings-subpage chrome: the GLOBAL status bar goes tall and shows "‹ Spectrum"
|
||||
// (tap the bar = Back). Content insets below the bar's lower row — no second header.
|
||||
@@ -27765,11 +27831,11 @@ static bool s_map_show_links = true;
|
||||
static lv_obj_t* s_map_link_objs[k_map_links_max] = {};
|
||||
static lv_point_t s_map_link_pts[k_map_links_max][2];
|
||||
|
||||
// Per-element visibility of the map's on-screen text/markers (persisted; all
|
||||
// default shown). Coords = bottom-left read-out, TileXYZ = the zoom + tile path
|
||||
// line, Contacts = the contact markers.
|
||||
// Per-element visibility of the map's on-screen text/markers (persisted; coords +
|
||||
// contacts default shown, the tile line default hidden since prefs v50). Coords =
|
||||
// bottom-left read-out, TileXYZ = the zoom + tile path line, Contacts = the contact markers.
|
||||
static bool s_map_show_coords = true;
|
||||
static bool s_map_show_tilexyz = true;
|
||||
static bool s_map_show_tilexyz = false;
|
||||
static bool s_map_show_contacts = true;
|
||||
static bool s_map_tile_debug = false; // developer: tile-pipeline diagnostic overlay on the zoom line (off by default)
|
||||
static bool s_map_direct_only = false; // when true, only 0-hop (directly-heard) contacts appear
|
||||
@@ -38413,6 +38479,7 @@ static void powerOffCb(lv_event_t* e) {
|
||||
g_lv.task->persistHistoryNow(); // flush chat before we go down
|
||||
discoveredFlushNow(); // and the Discovered ring
|
||||
the_mesh.flushContactsIfDirty(); // and any coalesced contacts refresh
|
||||
the_mesh.persistSyncHistoryNow(); // and the app-sync replay ring (RAM is lost in deep sleep)
|
||||
touchPrefsFlush(); // and all queued A/B preference snapshots
|
||||
#if defined(HELTEC_LORA_V4_R8)
|
||||
g_lv.task->showAlert(TR("Powering off\xE2\x80\xA6 press BOOT to wake"), 1500);
|
||||
@@ -38479,7 +38546,6 @@ static void rebootToDownloadMode() {
|
||||
#endif
|
||||
|
||||
static void powerDownloadCb(lv_event_t* e) {
|
||||
the_mesh.persistSyncHistoryNow(); // and the app-sync replay ring (RAM is lost in deep sleep)
|
||||
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
|
||||
closePowerMenu();
|
||||
#if defined(ESP32)
|
||||
@@ -38488,6 +38554,7 @@ static void powerDownloadCb(lv_event_t* e) {
|
||||
if (g_lv.task) {
|
||||
g_lv.task->persistHistoryNow(); // flush chat before we go down
|
||||
discoveredFlushNow(); // and the Discovered ring
|
||||
the_mesh.persistSyncHistoryNow(); // and the app-sync replay ring
|
||||
touchPrefsFlush(); // and all queued A/B preference snapshots
|
||||
g_lv.task->showAlert(TR("Download mode\xE2\x80\xA6 reflash over USB"), 1500);
|
||||
}
|
||||
@@ -38554,7 +38621,6 @@ static void openPowerMenu() {
|
||||
}
|
||||
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, nullptr);
|
||||
lv_obj_t* l = lv_label_create(b);
|
||||
the_mesh.persistSyncHistoryNow(); // and the app-sync replay ring
|
||||
lv_label_set_text(l, TR(txt));
|
||||
lv_obj_set_style_text_font(l, &g_font_14, LV_PART_MAIN);
|
||||
lv_obj_set_style_text_color(l, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
|
||||
@@ -44417,10 +44483,14 @@ static void buildUiTree() {
|
||||
|
||||
lv_obj_t* tab_btns = lv_tabview_get_tab_btns(g_lv.tabview);
|
||||
#if defined(HAS_THINKNODE_M9)
|
||||
// TABBAR_H is 0 on this board (see its definition) — hide the zero-height
|
||||
// btnmatrix outright so it can never paint, hit-test, or take focus.
|
||||
// No tab bar on this board (TABBAR_H == 0, see its definition): hide the zero-height
|
||||
// btnmatrix outright so it can never paint, hit-test, or take focus — and build NONE of
|
||||
// the bar chrome below (surface styling, Home re-tap / swipe-up gestures, key hints, the
|
||||
// tab font, the accent "glow" indicator). The M9 switches screens with its dedicated
|
||||
// HOME/MESSAGE/MAP keys and the app drawer; a bottom highlight over content was the only
|
||||
// visible leftover of the bar once the icons went.
|
||||
lv_obj_add_flag(tab_btns, LV_OBJ_FLAG_HIDDEN);
|
||||
#endif
|
||||
#else
|
||||
// Tab bar bar itself sits on pure BG, not the panel — keeps the bottom
|
||||
// strip indistinguishable from the rest of the screen except for the
|
||||
// active-tab highlight.
|
||||
@@ -44453,6 +44523,7 @@ static void buildUiTree() {
|
||||
#if CAP_TRACKBALL
|
||||
lv_obj_add_event_cb(tab_btns, navMenubarSizeCb, LV_EVENT_SIZE_CHANGED, nullptr); // keep the keyboard-nav key hints positioned
|
||||
#endif
|
||||
#endif // !HAS_THINKNODE_M9 — tab-bar chrome
|
||||
|
||||
// Tab labels: icons-only on touch targets; the 480px-wide Pager prefixes each
|
||||
// icon with its physical-keyboard mnemonic so the shortcuts are discoverable.
|
||||
@@ -44491,7 +44562,9 @@ static void buildUiTree() {
|
||||
lv_obj_t* tab_settings = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_SETTINGS);
|
||||
#endif
|
||||
// Slightly larger font for icons so they're easy to tap.
|
||||
#if CAP_UI_SIZE
|
||||
#if defined(HAS_THINKNODE_M9)
|
||||
// no tab bar on the M9 (see above) — nothing to size
|
||||
#elif CAP_UI_SIZE
|
||||
lv_obj_set_style_text_font(tab_btns, &g_font_tab, LV_PART_MAIN);
|
||||
#else
|
||||
lv_obj_set_style_text_font(tab_btns, &g_font_16, LV_PART_MAIN);
|
||||
@@ -44566,11 +44639,14 @@ static void buildUiTree() {
|
||||
#endif
|
||||
lv_obj_add_flag(s_chat_unread_badge, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
#if !defined(HAS_THINKNODE_M9)
|
||||
// Thin rounded accent "glow" bar that marks the active tab. A child of the
|
||||
// screen (like s_update_badge) created BEFORE the chat overlays so those cover
|
||||
// it, and above the tabview so it shows over the bottom bar. A soft accent
|
||||
// shadow gives it the glow; updateTabIndicator() slides it under the active
|
||||
// tab and hides it on the map.
|
||||
// tab and hides it on the map. NOT built on the M9 (no tab bar): it sat on the
|
||||
// screen, not in the bar, so hiding the bar left it glowing over the content's
|
||||
// bottom edge — the "lingering tab highlight". updateTabIndicator() null-guards.
|
||||
s_tab_indicator = lv_obj_create(lv_scr_act());
|
||||
lv_obj_remove_style_all(s_tab_indicator);
|
||||
lv_obj_clear_flag(s_tab_indicator, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
|
||||
@@ -44583,6 +44659,7 @@ static void buildUiTree() {
|
||||
lv_obj_set_style_shadow_opa(s_tab_indicator, LV_OPA_40, LV_PART_MAIN);
|
||||
lv_obj_set_style_shadow_spread(s_tab_indicator, 0, LV_PART_MAIN);
|
||||
updateTabIndicator(); // place it under the initial active tab
|
||||
#endif // !HAS_THINKNODE_M9
|
||||
|
||||
// Create full-screen detail overlays (hidden until a thread is tapped)
|
||||
makeChatDetail(g_lv.dm);
|
||||
@@ -48502,7 +48579,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
// sluggishness. 160 MHz is the known-good 2x bump; 240 MHz showed RGB565
|
||||
// noise on the map's SJPG decode, so 160 is the ceiling. The T-Deck sets no
|
||||
// ESP32_CPU_FREQ so it already boots at the 240 MHz default — bumping only
|
||||
// the V4 here avoids dragging the T-Deck *down* to 160.
|
||||
// the V4 here avoids dragging the T-Deck *down* to 160. The V4-R8 env now
|
||||
// sets ESP32_CPU_FREQ=240 itself (whole of setup() at full clock), so this
|
||||
// is a no-op there.
|
||||
#if !defined(HAS_TDECK_GT911)
|
||||
setCpuFrequencyMhz(240); // S3 max; watch the map tiles for SJPG decode noise (drop to 160 if it shows)
|
||||
#endif
|
||||
@@ -49308,8 +49387,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
// mesh so it survives reboot. OFF by default — no effect unless the user enabled it.
|
||||
the_mesh.setScopeDirectFloods(touchPrefsGetScopeDirect());
|
||||
#if defined(HELTEC_LORA_V4_TFT)
|
||||
// Heltec V4.3 high-gain FEM LNA: apply the saved state at boot (default OFF / bypassed,
|
||||
// matching the hardware). No-op on a V4.2 board (femLnaControllable() == false).
|
||||
// Heltec V4.3 high-gain FEM LNA: apply the saved state at boot (default OFF / bypassed on
|
||||
// the plain V4; ON on the V4-R8 since prefs v49). No-op on a V4.2 board (femLnaControllable() == false).
|
||||
if (board.femLnaControllable()) board.setFemLnaEnable(touchPrefsGetFemLna());
|
||||
#endif
|
||||
#if defined(HAS_TDISPLAY_P4)
|
||||
@@ -50488,6 +50567,7 @@ void UITask::rebootDevice() {
|
||||
}
|
||||
discoveredFlushNow(); // persist the Discovered ring before we go down
|
||||
the_mesh.flushContactsIfDirty(); // and any coalesced contacts refresh (card-less devices)
|
||||
the_mesh.persistSyncHistoryNow(); // and the app-sync replay ring
|
||||
touchPrefsFlush(); // finish queued A/B snapshots before reset
|
||||
if (_board) _board->reboot();
|
||||
}
|
||||
@@ -50567,7 +50647,6 @@ void UITask::newMsgImpl(uint8_t path_len, const char* from_name, const char* tex
|
||||
if (touchPrefsGetIgnoreTinyMsgs()) {
|
||||
const char* b = body ? body : "";
|
||||
while (*b == ' ' || *b == '\t' || *b == '\r' || *b == '\n') b++; // whitespace is not content
|
||||
the_mesh.persistSyncHistoryNow(); // and the app-sync replay ring
|
||||
size_t blen = strlen(b);
|
||||
while (blen > 0 && (b[blen-1] == ' ' || b[blen-1] == '\t' ||
|
||||
b[blen-1] == '\r' || b[blen-1] == '\n')) blen--;
|
||||
@@ -50931,8 +51010,13 @@ static void sdHealthTick() {
|
||||
const bool remounted = begin_ok && SD.cardType() != CARD_NONE;
|
||||
#else
|
||||
SD.end();
|
||||
const bool remounted = SD.begin(PIN_SD_CS, *spi, 4000000, "/sd", 6) &&
|
||||
SD.cardType() != CARD_NONE;
|
||||
bool remounted = SD.begin(PIN_SD_CS, *spi, 4000000, "/sd", 6) &&
|
||||
SD.cardType() != CARD_NONE;
|
||||
if (remounted) {
|
||||
const uint32_t hz = sdTryFastClock(PIN_SD_CS, *spi, 4000000, "SD"); // no-op unless SD_SPI_FAST_HZ
|
||||
remounted = hz != 0;
|
||||
if (remounted) g_sd_operating_hz = hz;
|
||||
}
|
||||
#endif
|
||||
if (remounted) {
|
||||
s_sd_mounted = true;
|
||||
|
||||
@@ -3,14 +3,24 @@
|
||||
|
||||
#include "LGFXDisplay.h"
|
||||
#include <Arduino.h>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
#ifndef LGFX_INVERT_COLOR
|
||||
#define LGFX_INVERT_COLOR true // default: black bg (ST7789 INVON)
|
||||
#endif
|
||||
// Largest LVGL band this driver ever sees: the R8's draw buffer is 240 x LV_DRAW_BUF_LINES
|
||||
// (24) px = 5760 px, in either orientation (LVGL re-splits to the buffer size). Rounded up.
|
||||
#ifndef LGFX_SWAP_BUF_PX
|
||||
#define LGFX_SWAP_BUF_PX 6144
|
||||
#endif
|
||||
|
||||
LGFXDisplay::LGFXDisplay(RefCountedDigitalPin* peripher_power)
|
||||
: DisplayDriver(240, 320),
|
||||
_periph_power(peripher_power)
|
||||
_periph_power(peripher_power),
|
||||
_swap_buf(nullptr),
|
||||
_swap_px(0),
|
||||
_swap_alloc_failed(false),
|
||||
_frame_open(false)
|
||||
{
|
||||
_isOn = false;
|
||||
_color = 0xFFFF;
|
||||
@@ -22,11 +32,13 @@ LGFXDisplay::LGFXDisplay(RefCountedDigitalPin* peripher_power)
|
||||
auto cfg = _bus.config();
|
||||
cfg.spi_host = SPI2_HOST;
|
||||
cfg.spi_mode = 0;
|
||||
cfg.freq_write = 40000000; // 40 MHz — the old 20 MHz made a full-screen flush
|
||||
// ~61 ms of pure bus time (~4x the plain V4's 80 MHz
|
||||
// driver); ST7789 typically takes 62.5-80 MHz, so 40
|
||||
// is still the conservative choice. If hardware shows
|
||||
// tearing/garbled bands, fall back to 26.6 MHz.
|
||||
// 80 MHz (perf pass 2026-08-20) — parity with the plain V4's TFT_eSPI driver on the
|
||||
// same ST7789 panel family. History: 20 MHz made a full-screen flush ~61 ms of pure
|
||||
// bus time, 40 MHz ~31 ms, 80 MHz ~15 ms. The S3's SPI clock is 80 MHz / n, so the
|
||||
// only rungs are 80 / 40 / 26.7 / 20; build with -D LGFX_SPI_WRITE_HZ=40000000 to
|
||||
// step back down if a unit shows tearing or garbled bands (the micro-SD shares
|
||||
// SCLK/MOSI, so the trace load is higher than the plain V4's).
|
||||
cfg.freq_write = LGFX_SPI_WRITE_HZ;
|
||||
cfg.freq_read = 16000000;
|
||||
cfg.spi_3wire = false;
|
||||
cfg.use_lock = true;
|
||||
@@ -107,13 +119,14 @@ void LGFXDisplay::turnOn() {
|
||||
}
|
||||
void LGFXDisplay::turnOff() {
|
||||
if (!_isOn) return;
|
||||
finishFrame();
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
|
||||
_isOn = false;
|
||||
if (_periph_power) _periph_power->release();
|
||||
}
|
||||
void LGFXDisplay::clear() { _lcd.fillScreen(0x0000); }
|
||||
void LGFXDisplay::startFrame(ColorVal) { _lcd.fillScreen(0x0000); }
|
||||
void LGFXDisplay::clear() { finishFrame(); _lcd.fillScreen(0x0000); }
|
||||
void LGFXDisplay::startFrame(ColorVal) { finishFrame(); _lcd.fillScreen(0x0000); }
|
||||
void LGFXDisplay::setTextSize(int sz) { _lcd.setTextSize(sz); }
|
||||
|
||||
void LGFXDisplay::setColor(ColorVal c) {
|
||||
@@ -136,9 +149,61 @@ void LGFXDisplay::writePixelsRGB565(int x, int y, int w, int h, const uint16_t*
|
||||
_lcd.endWrite();
|
||||
}
|
||||
|
||||
// Async LVGL flush (perf pass 2026-08-20).
|
||||
//
|
||||
// LVGL renders little-endian RGB565 into ONE draw buffer; the ST7789 wants big-endian.
|
||||
// The sync path above hands that buffer to LovyanGFX with setSwapBytes(true), which is
|
||||
// LGFX's *convert* path: a per-pixel byte swap into 32..256 px chunks, each chunk its own
|
||||
// DMA kick, and the call only returns once the whole band is on the wire — so LVGL's
|
||||
// render of band N+1 never overlapped the transfer of band N (13 bands per full frame).
|
||||
//
|
||||
// Here the swap is one 16-bit rotate per pixel into our own internal DMA buffer, the band
|
||||
// goes out as ONE no-convert DMA (swap=false -> src depth == panel depth -> no_convert ->
|
||||
// Bus_SPI::writeBytes DMA), and we return at once: LVGL renders the next band while the
|
||||
// SPI drains this one. Ordering is LGFX's: the next writeBytes waits on SPI_USR before it
|
||||
// starts, and we waitDMA() before overwriting the buffer. One startWrite()/endWrite()
|
||||
// transaction spans the frame and is closed on the last band, so the micro-SD (bus_shared)
|
||||
// gets the bus back between frames exactly as before — just with ~13 fewer transaction
|
||||
// setups per frame. Total bus time is unchanged; what changes is that the CPU is free
|
||||
// during it.
|
||||
void LGFXDisplay::flushBandRGB565(int x, int y, int w, int h, const uint16_t* pixels, bool last) {
|
||||
if (!_isOn || !pixels || w <= 0 || h <= 0) { if (last) finishFrame(); return; }
|
||||
const size_t px = (size_t)w * (size_t)h;
|
||||
if (!_swap_buf && !_swap_alloc_failed) {
|
||||
_swap_px = LGFX_SWAP_BUF_PX;
|
||||
_swap_buf = (uint16_t*)heap_caps_malloc(_swap_px * sizeof(uint16_t),
|
||||
MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
||||
if (!_swap_buf) {
|
||||
_swap_alloc_failed = true; // internal DRAM exhausted: stay on the sync path for good
|
||||
Serial.println("[TFT] async flush: no internal DMA RAM for the swap buffer; sync flush");
|
||||
}
|
||||
}
|
||||
if (!_swap_buf || px > _swap_px) { // fallback: synchronous convert path
|
||||
writePixelsRGB565(x, y, w, h, pixels);
|
||||
if (last) finishFrame();
|
||||
return;
|
||||
}
|
||||
if (!_frame_open) { _lcd.startWrite(); _frame_open = true; }
|
||||
_lcd.waitDMA(); // the previous band may still be reading _swap_buf
|
||||
const uint16_t* s = pixels;
|
||||
uint16_t* d = _swap_buf;
|
||||
for (size_t i = 0; i < px; ++i) d[i] = __builtin_bswap16(s[i]);
|
||||
_lcd.setAddrWindow(x, y, w, h);
|
||||
_lcd.writePixelsDMA(_swap_buf, (int32_t)px, /*swap=*/false); // already panel byte order
|
||||
if (last) finishFrame();
|
||||
}
|
||||
|
||||
void LGFXDisplay::finishFrame() {
|
||||
if (!_frame_open) return;
|
||||
_lcd.waitDMA();
|
||||
_lcd.endWrite();
|
||||
_frame_open = false;
|
||||
}
|
||||
|
||||
// UI contract (see UITask applyRotation): called with 1 for ROT_90, 3 for ROT_270; portrait
|
||||
// stays as-inited (rotation 0). Honor the argument — the old hardcoded 1 ignored it.
|
||||
void LGFXDisplay::setDisplayRotation(uint8_t r) {
|
||||
finishFrame();
|
||||
_lcd.setRotation(r & 3);
|
||||
setLogicalSize(_lcd.width(), _lcd.height());
|
||||
}
|
||||
@@ -152,6 +217,7 @@ void LGFXDisplay::panelSleep(bool sleep) {
|
||||
// setSleep writes the bare command with no delay: t_SLPIN wants 5 ms of bus
|
||||
// quiet after SLPIN, and >=5 ms must pass after SLPOUT before the next
|
||||
// command (the wake path fires backlight + LVGL flushes immediately after).
|
||||
finishFrame();
|
||||
if (sleep) { _lcd.sleep(); delay(5); } // SLPIN
|
||||
else { _lcd.wakeup(); delay(6); } // SLPOUT
|
||||
}
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
#define LGFX_USE_V1
|
||||
#include <LovyanGFX.hpp>
|
||||
|
||||
// Display SPI write clock. 80 MHz (perf pass 2026-08-20) = parity with the plain V4's
|
||||
// TFT_eSPI driver; the S3's rungs are 80 / 40 / 26.7 / 20 MHz. Override with
|
||||
// -D LGFX_SPI_WRITE_HZ=40000000 if a unit shows tearing / garbled bands. Exposed here
|
||||
// (not just in the .cpp) so Settings -> About can report it.
|
||||
#ifndef LGFX_SPI_WRITE_HZ
|
||||
#define LGFX_SPI_WRITE_HZ 80000000
|
||||
#endif
|
||||
|
||||
class LGFXDisplay : public DisplayDriver {
|
||||
private:
|
||||
lgfx::Panel_ST7789 _panel;
|
||||
@@ -27,6 +35,14 @@ private:
|
||||
uint16_t _color;
|
||||
RefCountedDigitalPin* _periph_power;
|
||||
|
||||
// Async LVGL flush state (see flushBandRGB565): one internal DMA-capable band buffer
|
||||
// holding the byte-swapped copy of the LVGL band currently on the wire, and whether a
|
||||
// frame-spanning startWrite() transaction is open on the (micro-SD-shared) bus.
|
||||
uint16_t* _swap_buf;
|
||||
size_t _swap_px;
|
||||
bool _swap_alloc_failed;
|
||||
bool _frame_open;
|
||||
|
||||
public:
|
||||
LGFXDisplay(RefCountedDigitalPin* peripher_power = nullptr);
|
||||
bool begin();
|
||||
@@ -47,8 +63,22 @@ public:
|
||||
uint16_t getTextWidth(const char* str) override;
|
||||
void endFrame() override;
|
||||
|
||||
// ---- LVGL flush entry point ----
|
||||
// ---- LVGL flush entry points ----
|
||||
// Synchronous: byte-swap + write, returns once the band is on the wire (LovyanGFX's
|
||||
// convert path). Kept for non-LVGL callers and as the fallback of the async path.
|
||||
void writePixelsRGB565(int x, int y, int w, int h, const uint16_t* pixels);
|
||||
// Asynchronous (lvglFlush on the R8): swaps the band into an internal DMA buffer, kicks a
|
||||
// single DMA and returns immediately so LVGL renders band N+1 while band N drains. `last`
|
||||
// = lv_disp_flush_is_last(): waits for the DMA and closes the frame transaction so the
|
||||
// shared micro-SD gets the bus back between frames. Falls back to the sync path if the
|
||||
// DMA buffer could not be allocated.
|
||||
void flushBandRGB565(int x, int y, int w, int h, const uint16_t* pixels, bool last);
|
||||
// Wait for any in-flight band DMA and end the frame transaction (no-op if none is open).
|
||||
// Every non-LVGL bus user below (sleep, rotation, clear) calls this first.
|
||||
void finishFrame();
|
||||
// True once the async path has its internal DMA buffer (i.e. LVGL flushes are async);
|
||||
// false before the first flush or if internal DRAM was exhausted (sync fallback).
|
||||
bool asyncFlushActive() const { return _swap_buf != nullptr; }
|
||||
|
||||
// ---- Hardware panel rotation ----
|
||||
void setDisplayRotation(uint8_t r);
|
||||
|
||||
@@ -141,3 +141,53 @@ on the physical device.
|
||||
- Refuted during verification: the About "Model:" mislabel (code is inside
|
||||
`#if 0`); slot-pool/cull, drain-order, tab-bar-geometry concerns (M9-pass
|
||||
parity checks) — all verified non-issues on the R8.
|
||||
|
||||
---
|
||||
|
||||
# Perf pass (2026-08-20)
|
||||
|
||||
Question asked: "are we achieving full performance out of the R8 build?" Audit of
|
||||
CPU / memory / flash / display / SD / Wi-Fi / RF paths. Compute side was already at
|
||||
spec (240 MHz runtime clock, octal PSRAM @ 80 MHz, QIO flash @ 80 MHz, LVGL heap in
|
||||
PSRAM, draw buffer in internal DMA RAM, 16 ms refresh). Six gaps found and fixed,
|
||||
all flashed to the user's unit the same day:
|
||||
|
||||
1. **FEM LNA defaulted OFF.** The R8 is a V4.3.1-generation board (KCT8103L FEM with
|
||||
the software-switchable ~17 dB RX LNA); `fem_lna` defaulted to 0 = bypassed.
|
||||
Prefs schema v49: default ON on `HELTEC_LORA_V4_R8`, one-time flip of existing
|
||||
installs (no new field). Toggle stays in Radio & Mesh. **[HW]** confirm the boot
|
||||
line `[R8] FEM type=1` / About "LNA on"; expect better RX in quiet sites, possibly
|
||||
worse in RF-noisy ones (then turn it off).
|
||||
2. **Display bus 40 → 80 MHz** (`LGFX_SPI_WRITE_HZ`, LGFXDisplay.h): parity with
|
||||
the plain V4's TFT_eSPI driver. Full-frame bus time ~31 → ~15 ms. **[HW]** watch
|
||||
for tearing / garbled bands; `-D LGFX_SPI_WRITE_HZ=40000000` steps back.
|
||||
3. **Async DMA band flush** (`LGFXDisplay::flushBandRGB565`, used by `lvglFlush` on
|
||||
the R8 only). Before: LVGL's LE pixels went through LovyanGFX's convert path
|
||||
(per-pixel swap into 32..256 px chunks, each its own DMA kick) and the flush
|
||||
returned only when the band was on the wire — render and transfer never
|
||||
overlapped. Now: one 16-bit rotate per pixel into an internal DMA buffer (12 KB),
|
||||
ONE no-convert DMA per band, `lv_disp_flush_ready` immediately; the last band
|
||||
(`lv_disp_flush_is_last`) waits and closes the frame transaction so the shared
|
||||
micro-SD gets the bus between frames exactly as before. Sync fallback if the
|
||||
buffer can't be allocated. `finishFrame()` guards panel sleep / rotation / clear.
|
||||
4. **Wi-Fi modem power save is now a pref** (`wifi_ps` in the NVS Wi-Fi store;
|
||||
toggle in Wi-Fi settings, applies live once associated). Default ON everywhere
|
||||
(unchanged behaviour), OFF on the R8 (USB-powered kit; lower-latency TCP/app
|
||||
link and steadier association on this unit's weak 2.4 GHz path).
|
||||
5. **micro-SD operating clock 4 → 20 MHz** (`SD_SPI_FAST_HZ`, include/SdFastClock.h):
|
||||
after the proven 4 MHz mount, re-begin at 20 MHz and READ-VERIFY (a probe file's
|
||||
first 512 B captured at 4 MHz and byte-compared after the raise; SPI-mode SD has
|
||||
no data CRC so a bare mount success would not catch a marginal clock). Falls
|
||||
back to 4 MHz. Wired at all three mount sites (boot adoption in main.cpp, the
|
||||
UITask mount ladder, the reinsert remount). No-op on boards without the flag.
|
||||
6. **Boot at 240 MHz**: the R8 env now sets `ESP32_CPU_FREQ=240`; previously all of
|
||||
setup() (radio init, SD ladder, store load, mesh begin) ran at the base env's
|
||||
80 MHz until UITask bumped it. Screen-off DFS to 80 MHz unchanged.
|
||||
|
||||
Verification aid: Settings → About on the R8 gained a `Perf:` row —
|
||||
`CPU 240 · TFT 80 MHz DMA · SD 20 MHz · LNA on` is the all-green reading.
|
||||
("sync" = DMA buffer alloc failed; "SD 4 MHz" = the 20 MHz verify failed and it
|
||||
fell back; both are safe degradations, not faults.)
|
||||
|
||||
Deliberately NOT done: `-O2` (image is 3.70 MB in a 3.875 MB OTA slot), bigger data
|
||||
cache (baked into Arduino's prebuilt IDF libs), radio BW/SF/CR (network parameters).
|
||||
|
||||
Reference in New Issue
Block a user