From 958627d6e97806f323de5f1b6f448468bebc1d39 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:13:39 +0200 Subject: [PATCH] fix(wio-tracker-l2): radios and codec idle when unused; buzzer and OTA fixes - BLE (ESP32): fast advertising (20-30 ms) for 30 s, then 152.5 ms, as the nRF52 interface; the library default kept 30-60 ms forever - Live map tiles: the WiFi drops after a minute with nothing missing - ES8311 to standby while the I2S clocks are off, set up again before the amp comes on - Buzzer: no channel (DMA out of memory) waits out the samples instead of spinning above the UI loop - OTA: the image header check waits for its 16 bytes - Comments: P10 is just the LED; the LCD SPI clock is 40 MHz on the wire Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/ui-lvgl/OtaScreen.h | 2 +- .../ui-lvgl/map/TileDownloader.h | 13 +++++-- src/helpers/esp32/SerialBLEInterface.cpp | 39 ++++++++++++------- src/helpers/esp32/SerialBLEInterface.h | 3 ++ src/helpers/ui/ES8311.h | 15 +++++++ src/helpers/ui/buzzer.cpp | 33 +++++++++++----- src/helpers/ui/buzzer.h | 4 +- variants/wio-tracker-l2/WioTrackerL2Board.cpp | 3 +- variants/wio-tracker-l2/WioTrackerL2Board.h | 2 +- variants/wio-tracker-l2/WioTrackerL2Display.h | 1 + 10 files changed, 84 insertions(+), 31 deletions(-) diff --git a/examples/companion_radio/ui-lvgl/OtaScreen.h b/examples/companion_radio/ui-lvgl/OtaScreen.h index 636f8dfe..3b65f754 100644 --- a/examples/companion_radio/ui-lvgl/OtaScreen.h +++ b/examples/companion_radio/ui-lvgl/OtaScreen.h @@ -110,7 +110,7 @@ static void install(HTTPClient& http, NetworkClientSecure& tls) { if (!ok) snprintf(s_msg, sizeof(s_msg), "Out of memory"); while (ok && s_written < (uint32_t)len) { size_t avail = in->available(); - if (!avail) { + if (!avail || (s_written == 0 && avail < 16)) { // the header check below wants its 16 bytes in one read if (!in->connected() || millis() - last_data > 20000) { snprintf(s_msg, sizeof(s_msg), "Download stalled"); ok = false; break; } vTaskDelay(pdMS_TO_TICKS(5)); continue; diff --git a/examples/companion_radio/ui-lvgl/map/TileDownloader.h b/examples/companion_radio/ui-lvgl/map/TileDownloader.h index d52ca964..cc28f2ce 100644 --- a/examples/companion_radio/ui-lvgl/map/TileDownloader.h +++ b/examples/companion_radio/ui-lvgl/map/TileDownloader.h @@ -24,8 +24,9 @@ // // Live tiles: while the map is open, single tiles it is missing are fetched // the same way (one at a time, between the job's steps when there is no job), -// the WiFi connected on the first miss and dropped by the caller (liveEnd) -// once the map has been closed for a while. They go to LIVE_ROOT, not +// the WiFi connected on the first miss and dropped after a minute with +// nothing missing, or by the caller (liveEnd) once the map has been closed +// for a while. They go to LIVE_ROOT, not // , and only up to the Storage limit (LiveCache.h). // // Single-TU fragment: included by ui-lvgl/UITask.cpp only (after LvglPort.h). @@ -288,6 +289,7 @@ private: struct LiveTile { int16_t z = -1; uint8_t layer = 0; int32_t x = 0, y = 0; }; // layer: 0 base, 1 trails enum LiveState : uint8_t { LV_IDLE, LV_CONNECTING, LV_UP, LV_BACKOFF }; static const int LV_QUEUE = 8, LV_FAILED = 16, LV_DONE = 8; + static const uint32_t LV_IDLE_MS = 60000; bool _live = false; LiveState _lv_state = LV_IDLE; char _lv_ssid[33] = "", _lv_pass[65] = ""; @@ -295,7 +297,7 @@ private: int _lv_n = 0, _lv_failed_next = 0, _lv_done_n = 0; bool _lv_fetching = false; uint8_t _lv_consec_fail = 0; - uint32_t _lv_since = 0; // connect start / back-off end + uint32_t _lv_since = 0; // connect start / back-off end / last activity while up void liveFailed(const LiveTile& t) { _lv_failed[_lv_failed_next++ % LV_FAILED] = t; @@ -320,7 +322,7 @@ private: return; case LV_CONNECTING: { int ns = lvport::netState(); - if (ns == lvport::NET_UP) { _lv_state = LV_UP; return; } + if (ns == lvport::NET_UP) { _lv_state = LV_UP; _lv_since = millis(); return; } if (ns == lvport::NET_FAILED || millis() - _lv_since > 20000) { lvport::netEnd(); _lv_state = LV_BACKOFF; @@ -362,8 +364,11 @@ private: liveFailed(_lv_cur); } lvport::fetchRelease(); + _lv_since = millis(); return; } + // Nothing missing for a minute: the WiFi goes (the next miss reconnects). + if (!_lv_n && millis() - _lv_since > LV_IDLE_MS) { lvport::netEnd(); _lv_state = LV_IDLE; return; } if (!_lv_n || millis() - _last_start < 120) return; // be gentle with the server _lv_cur = _lv_q[0]; for (int i = 1; i < _lv_n; i++) _lv_q[i - 1] = _lv_q[i]; diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 1c4c7c8a..c71784f5 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -10,6 +10,14 @@ #define ADVERT_RESTART_DELAY 1000 // millis +// As the nRF52 interface: 20-30 ms for the first 30 s (the app finds it at +// once), then 152.5 ms. Left alone, the library advertises every 30-60 ms +// for as long as nothing is connected. Units of 0.625 ms. +#define ADV_FAST_MIN 32 +#define ADV_FAST_MAX 48 +#define ADV_SLOW 244 +#define ADV_FAST_MS 30000 + void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) { _pin_code = pin_code; @@ -186,15 +194,19 @@ void SerialBLEInterface::enable() { // Start the service pService->start(); - // Start advertising - - //pServer->getAdvertising()->setMinInterval(500); - //pServer->getAdvertising()->setMaxInterval(1000); - - pServer->getAdvertising()->start(); + startAdvertising(true); adv_restart_time = 0; } +void SerialBLEInterface::startAdvertising(bool fast) { + BLEAdvertising* adv = pServer->getAdvertising(); + adv->stop(); // the interval only takes on a (re)start + adv->setMinInterval(fast ? ADV_FAST_MIN : ADV_SLOW); + adv->setMaxInterval(fast ? ADV_FAST_MAX : ADV_SLOW); + adv->start(); + adv_slow_time = fast ? (millis() + ADV_FAST_MS) | 1 : 0; +} + void SerialBLEInterface::disable() { _isEnabled = false; @@ -204,7 +216,7 @@ void SerialBLEInterface::disable() { pServer->disconnect(last_conn_id); pService->stop(); oldDeviceConnected = deviceConnected = false; - adv_restart_time = 0; + adv_restart_time = adv_slow_time = 0; } size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { @@ -262,10 +274,6 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { clearBuffers(); BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting..."); - - //pServer->getAdvertising()->setMinInterval(500); - //pServer->getAdvertising()->setMaxInterval(1000); - adv_restart_time = millis() + ADVERT_RESTART_DELAY; } else { BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising"); @@ -273,7 +281,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { // connecting // do stuff here on connecting pServer->getAdvertising()->stop(); - adv_restart_time = 0; + adv_restart_time = adv_slow_time = 0; } oldDeviceConnected = deviceConnected; } @@ -281,10 +289,15 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { if (adv_restart_time && millis() >= adv_restart_time) { if (pServer->getConnectedCount() == 0) { BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising"); - pServer->getAdvertising()->start(); // re-Start advertising + startAdvertising(true); // re-Start advertising } adv_restart_time = 0; } + + if (adv_slow_time && (long)(millis() - adv_slow_time) >= 0) { + adv_slow_time = 0; + if (_isEnabled && pServer->getConnectedCount() == 0) startAdvertising(false); + } return 0; } diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 0fe0adc0..e0cce5f3 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -19,6 +19,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE uint32_t _pin_code; unsigned long _last_write; unsigned long adv_restart_time; + unsigned long adv_slow_time; // when fast advertising drops to the slow interval (0 = not pending) struct Frame { uint8_t len; @@ -33,6 +34,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE Frame send_queue[FRAME_QUEUE_SIZE]; void clearBuffers(); + void startAdvertising(bool fast); protected: // BLESecurityCallbacks methods @@ -72,6 +74,7 @@ public: deviceConnected = false; oldDeviceConnected = false; adv_restart_time = 0; + adv_slow_time = 0; _isEnabled = false; _last_write = 0; last_conn_id = 0; diff --git a/src/helpers/ui/ES8311.h b/src/helpers/ui/ES8311.h index eac8d95a..36e7c83a 100644 --- a/src/helpers/ui/ES8311.h +++ b/src/helpers/ui/ES8311.h @@ -42,4 +42,19 @@ static bool begin(TwoWire& w, uint8_t addr) { return true; } +// Everything but the I2C interface off (esp-adf's es8311 suspend): DAC, ADC, +// references and the output driver. begin() brings it back, with MCLK running. +static bool standby(TwoWire& w, uint8_t addr) { + static const uint8_t SEQ[][2] = { + { 0x32, 0x00 }, { 0x17, 0x00 }, { 0x0E, 0xFF }, { 0x12, 0x02 }, { 0x14, 0x00 }, + { 0x0D, 0xFA }, { 0x15, 0x00 }, { 0x37, 0x08 }, { 0x02, 0x10 }, + { 0x00, 0x00 }, { 0x00, 0x1F }, // state machine reset + { 0x01, 0x30 }, { 0x01, 0x00 }, // clocks off + { 0x45, 0x00 }, { 0x0D, 0xFC }, { 0x02, 0x00 }, + }; + for (size_t i = 0; i < sizeof(SEQ) / sizeof(SEQ[0]); i++) + if (!write(w, addr, SEQ[i][0], SEQ[i][1])) return false; + return true; +} + } // namespace es8311 diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index d773fd80..10097191 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -426,7 +426,9 @@ static bool i2sInstall() { } static void i2sUninstall() { i2s_channel_disable(s_tx); i2s_del_channel(s_tx); s_tx = nullptr; } static void i2sWrite(const void* src, size_t n, uint32_t ms) { - if (!s_tx) return; + // No channel (a restart found no DMA memory): wait as long as the samples + // would play, or the task, above the UI loop, spins until the clocks stop. + if (!s_tx) { vTaskDelay(pdMS_TO_TICKS(n / (SAMPLE_RATE / 250)) + 1); return; } size_t w; i2s_channel_write(s_tx, src, n, &w, ms); } @@ -489,8 +491,8 @@ bool genericBuzzer::_i2sBegin() { i2sUninstall(); return false; } - _clk_on_ms = millis(); - _clk_running = true; + _clk_on_ms = _codec_on_ms = millis(); + _clk_running = _codec_on = true; delay(CLK_SETTLE_MS); // settled before the startup sound powers the amp // Above the UI loop and LVGL (priority 1), so rendering never starves it. _i2s_ok = xTaskCreate(_taskEntry, "buzzer", 3072, this, 6, (TaskHandle_t*)&_task) == pdPASS; @@ -610,12 +612,13 @@ void genericBuzzer::_start(const char* melody) { if (_amp_on) { // a replay within the settle time still waits out the rest uint32_t since = millis() - s_amp_on_ms; settle = since < AUDIO_AMP_SETTLE_MS ? (uint16_t)(AUDIO_AMP_SETTLE_MS - since) : 0; - } else if (_clk_running && millis() - _clk_on_ms >= CLK_SETTLE_MS) { + } else if (_clk_running && _codec_on && millis() - _clk_on_ms >= CLK_SETTLE_MS && + millis() - _codec_on_ms >= CLK_SETTLE_MS) { _amp_on = power_now = true; // claimed here, so the task keeps the clocks settle = AUDIO_AMP_SETTLE_MS; } else { - _amp_pending = true; // loop() powers it once the clocks have settled - settle = CLK_SETTLE_MS + AUDIO_AMP_SETTLE_MS; + _amp_pending = true; // loop() powers it once the clocks (and the codec) have settled + settle = (_codec_on ? 1 : 2) * CLK_SETTLE_MS + AUDIO_AMP_SETTLE_MS; } strncpy(_mel, melody, MEL_MAX - 1); _mel[MEL_MAX - 1] = 0; @@ -650,15 +653,27 @@ void genericBuzzer::stop() { xTaskNotifyGive((TaskHandle_t)_task); } -// The amp: on once the codec's clocks have settled (a pending start), off -// once nothing has played for AMP_LINGER_MS. +// The codec: to standby once the task has stopped the clocks (its analog side +// kept drawing with nothing to play), set up again once they run. The amp: on +// once the codec has settled (a pending start), off once nothing has played +// for AMP_LINGER_MS. Both off in between, so neither step pops. void genericBuzzer::loop() { if (!_i2s_ok) return; + if (_codec_on && !_clk_running && !_amp_on && !_amp_pending) { + es8311::standby(Wire, BUZZER_CODEC_ES8311); + _codec_on = false; + } + if (_amp_pending && !_codec_on && _task_playing && _clk_running && millis() - _clk_on_ms >= CLK_SETTLE_MS) { + _codec_on = es8311::begin(Wire, BUZZER_CODEC_ES8311); // a failed one is tried again next pass + _codec_on_ms = millis(); + } if (_amp_pending) { bool power = false; portENTER_CRITICAL(&s_mux); if (!_task_playing) _amp_pending = false; // stopped before it got going - else if (_clk_running && millis() - _clk_on_ms >= CLK_SETTLE_MS) { _amp_pending = false; _amp_on = power = true; } + else if (_clk_running && _codec_on && millis() - _clk_on_ms >= CLK_SETTLE_MS && millis() - _codec_on_ms >= CLK_SETTLE_MS) { + _amp_pending = false; _amp_on = power = true; + } portEXIT_CRITICAL(&s_mux); if (power) { buzzerAmpPower(true); s_amp_on_ms = millis(); } } diff --git a/src/helpers/ui/buzzer.h b/src/helpers/ui/buzzer.h index ac95571a..e7252b39 100644 --- a/src/helpers/ui/buzzer.h +++ b/src/helpers/ui/buzzer.h @@ -126,7 +126,7 @@ class genericBuzzer // A speaker behind an I2S codec (BUZZER_CODEC_ES8311) instead of a // PWM pin. An audio task synthesises the melody and advances its // notes by samples written, so timing holds through a stalled UI - // loop; loop() only powers the amp. See buzzer.cpp. + // loop; loop() powers the amp and the codec. See buzzer.cpp. static const int MEL_MAX = 256; char _mel[MEL_MAX]; // the melody playing (a copy: callers reuse buffers) volatile uint32_t _req = 0; // bumped by play()/stop(); the task restarts on a change @@ -138,6 +138,8 @@ class genericBuzzer volatile bool _clk_running = false; // I2S clocking the codec volatile uint32_t _clk_on_ms = 0; bool _i2s_ok = false; + bool _codec_on = false; // codec powered up (standby while the clocks are off); caller's thread only + uint32_t _codec_on_ms = 0; uint32_t _amp_off_at = 0; // amp stays on this long after the last sound void* _task = nullptr; // TaskHandle_t diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.cpp b/variants/wio-tracker-l2/WioTrackerL2Board.cpp index 5dd668f2..a9ee6342 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.cpp +++ b/variants/wio-tracker-l2/WioTrackerL2Board.cpp @@ -158,8 +158,7 @@ void WioTrackerL2Board::begin() { } void WioTrackerL2Board::setLed(bool on) { - // P10 doubles as GNSS wakeup and idles HIGH; blink = brief LOW pulses so - // the GPS never sees a sustained low level + // active low: HIGH = off if (expander_ok) { expWritePin(EXP_PIN_USER_LED, !on); } diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.h b/variants/wio-tracker-l2/WioTrackerL2Board.h index 8a5bd51b..21b745bb 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.h +++ b/variants/wio-tracker-l2/WioTrackerL2Board.h @@ -30,7 +30,7 @@ #define EXP_PIN_GROVE_EN 7 // output - Grove port power #define EXP_PIN_TP_RST 8 // output - touch panel reset #define EXP_PIN_GNSS_RST 9 // output - GNSS reset (active HIGH) -#define EXP_PIN_USER_LED 10 // output - mesh/user LED (also GNSS wakeup) +#define EXP_PIN_USER_LED 10 // output - mesh/user LED (active low) #define EXP_PIN_OTG_EN 11 // output - USB OTG power #define EXP_PIN_PA_EN 12 // output - speaker amp power #define EXP_PIN_GNSS_EN 13 // output - GNSS power diff --git a/variants/wio-tracker-l2/WioTrackerL2Display.h b/variants/wio-tracker-l2/WioTrackerL2Display.h index 5277245c..63cc40ee 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Display.h +++ b/variants/wio-tracker-l2/WioTrackerL2Display.h @@ -15,6 +15,7 @@ // sequenced by WioTrackerL2Board::begin() before this driver initializes. // --------------------------------------------------------------------------- +// The SPI clock is 80 MHz / an integer: 75 MHz asked gives 40 MHz on the wire. #ifndef L2_SPI_FREQUENCY #define L2_SPI_FREQUENCY 75000000 #endif