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 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-27 14:13:39 +02:00
co-authored by Claude Opus 5.5
parent 782f8aa567
commit 958627d6e9
10 changed files with 84 additions and 31 deletions
+1 -1
View File
@@ -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;
@@ -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
// <root>, 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];
+26 -13
View File
@@ -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;
}
+3
View File
@@ -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;
+15
View File
@@ -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
+24 -9
View File
@@ -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(); }
}
+3 -1
View File
@@ -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
@@ -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);
}
+1 -1
View File
@@ -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
@@ -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