Added Seeed Wio Tracker L2 Pro variant

https://github.com/meshcore-dev/MeshCore/pull/3381
This commit is contained in:
Kevin Le committed 2026-10-08 10:01:28 +07:00
1 parent 78e134b2ba
commit 6c7b751eb8
11 files changed
+1142

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi",
"partitions": "default_16MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino"],
"name": "Seeed Wio Tracker L2 (16 MB flash, 8 MB OPI PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.seeedstudio.com/",
"vendor": "Seeed Studio"
}
+139
View File
@@ -0,0 +1,139 @@
#pragma once
#include <Arduino.h>
// Transparent wrapper around the GNSS UART. MicroNMEA ignores GSV sentences,
// so this watches the byte stream for them and keeps a running count of
// satellites in view, strong satellites and best SNR, which separates RF
// faults from a receiver that only lacks sky view. It also records the
// fix flags straight from GGA/RMC so a parser problem upstream can't hide a
// real fix, and can hand every complete line to a logger (SD card).
class GpsTapStream : public Stream {
Stream& _src;
char _line[120];
uint8_t _len = 0;
struct Talker { char id[3]; uint8_t in_view; uint8_t strong; uint8_t strong_acc; unsigned long at; };
Talker _talkers[6] = {};
int _best_snr = 0;
unsigned long _best_at = 0;
unsigned long _last_gsv = 0;
int _gga_fix = -1; // GGA fix quality (0 = no fix)
int _gga_used = 0; // GGA satellites used
char _rmc_status = '?'; // RMC status: A = valid, V = void
unsigned long _last_fix_sentence = 0;
static const unsigned long FRESH_MS = 6000;
static const int STRONG_DB = 30;
// pointer to comma-separated field n (0 = sentence id), NULL if absent
static const char* field(const char* s, int n) {
const char* p = s;
for (int i = 0; i < n; i++) {
p = strchr(p, ',');
if (p == NULL) return NULL;
p++;
}
return p;
}
void parseGsv(const char* s) {
// $GPGSV,total,msg,in_view,prn,elev,az,snr,...*cs
char talker[3] = {s[1], s[2], 0};
const char* f;
int total = (f = field(s, 1)) ? atoi(f) : 0;
int msg = (f = field(s, 2)) ? atoi(f) : 0;
int in_view = (f = field(s, 3)) ? atoi(f) : -1;
if (in_view < 0) return;
int strong_here = 0;
for (int k = 0; k < 4; k++) {
const char* snr = field(s, 7 + 4 * k);
if (snr == NULL || *snr == ',' || *snr == '*' || *snr == 0) continue;
int v = atoi(snr);
if (v >= STRONG_DB) strong_here++;
if (v > _best_snr || millis() - _best_at > FRESH_MS) { _best_snr = v; _best_at = millis(); }
}
int slot = -1;
for (int i = 0; i < 6; i++) {
if (strcmp(_talkers[i].id, talker) == 0) { slot = i; break; }
if (slot < 0 && _talkers[i].id[0] == 0) slot = i;
}
if (slot < 0) return;
Talker& t = _talkers[slot];
memcpy(t.id, talker, 3);
t.in_view = (uint8_t) in_view;
if (msg <= 1) t.strong_acc = 0;
t.strong_acc += strong_here;
if (msg >= total) t.strong = t.strong_acc;
t.at = millis();
_last_gsv = millis();
}
void parseFixFlags(const char* s) {
const char* type = s + 3;
if (strncmp(type, "GGA", 3) == 0) {
const char* f;
_gga_fix = (f = field(s, 6)) && *f != ',' ? atoi(f) : 0;
_gga_used = (f = field(s, 7)) && *f != ',' ? atoi(f) : 0;
_last_fix_sentence = millis();
} else if (strncmp(type, "RMC", 3) == 0) {
const char* f = field(s, 2);
_rmc_status = (f != NULL && *f != ',' && *f != 0) ? *f : '?';
_last_fix_sentence = millis();
}
}
void feed(int c) {
if (echo) Serial.write((uint8_t) c);
if (c == '\n' || c == '\r') {
if (_len > 6 && _line[0] == '$') {
_line[_len] = 0;
if (on_line != NULL) on_line(_line);
if (strncmp(_line + 3, "GSV", 3) == 0) parseGsv(_line);
else parseFixFlags(_line);
}
_len = 0;
} else if (_len < sizeof(_line) - 1) {
_line[_len++] = (char) c;
}
}
public:
bool echo = false; // mirror raw NMEA to USB Serial
void (*on_line)(const char* line) = NULL; // complete-sentence hook (e.g. SD log)
explicit GpsTapStream(Stream& src) : _src(src) {}
int satsInView() const {
int n = 0;
for (int i = 0; i < 6; i++) {
if (_talkers[i].id[0] != 0 && millis() - _talkers[i].at < FRESH_MS) n += _talkers[i].in_view;
}
return n;
}
int strongSats() const {
int n = 0;
for (int i = 0; i < 6; i++) {
if (_talkers[i].id[0] != 0 && millis() - _talkers[i].at < FRESH_MS) n += _talkers[i].strong;
}
return n;
}
int bestSnr() const { return millis() - _best_at < FRESH_MS ? _best_snr : 0; }
bool streaming() const { return millis() - _last_gsv < FRESH_MS; }
int ggaFix() const { return _gga_fix; }
int ggaUsed() const { return _gga_used; }
char rmcStatus() const { return _rmc_status; }
// Stream interface: pass-through with a tap on read()
int available() override { return _src.available(); }
int peek() override { return _src.peek(); }
int read() override {
int c = _src.read();
if (c >= 0) feed(c);
return c;
}
void flush() override { _src.flush(); }
size_t write(uint8_t b) override { return _src.write(b); }
size_t write(const uint8_t* buf, size_t n) override { return _src.write(buf, n); }
};
+65
View File
@@ -0,0 +1,65 @@
# MeshCore for Seeed Wio Tracker L2 Pro
MeshCore port for the **Seeed Studio Wio Tracker L2 Pro** (pre-release hardware;
pin map may change on production units).
## Hardware
| Component | Detail |
|-----------|--------|
| MCU module | Wio-S3: ESP32-S3, 16 MB flash (QIO), 8 MB PSRAM (OPI) |
| LoRa | SX1262, 862-930 MHz (SCK 4, MISO 5, MOSI 6, CS 21, RST 7, BUSY 8, DIO1 9, DIO2 = RF switch, TCXO 1.8 V) |
| GNSS | Quectel L76K (GPS/BeiDou/GLONASS/QZSS), UART: module TX to GPIO 18, module RX from GPIO 17, 9600 baud |
| Display | 3.2" 320x240 NV3031B on quad-SPI (SCLK 42, IO0-3 = 41/40/39/38, CS 46), 75 MHz |
| Touch | GT911 capacitive, I2C 0x5D |
| Backlight | LP5814 LED driver, I2C 0x2C |
| I/O expander | TCA9535, I2C 0x21 - gates power/reset for GNSS, LCD, touch, SD, speaker amp, battery ADC, Grove |
| Battery ADC | ADS1115, I2C 0x48, AIN0 through x2 divider |
| USB-C detect | AW35615 CC controller, I2C 0x22 (charge detection) |
| Audio | ES8311 codec (speaker, alert tones) + ES7243E mic ADC (unused) on I2S |
| I2C bus | SDA 47, SCL 48 |
| Buttons | User/Boot = GPIO 0; Wake/lock button on expander P00 |
| SD card | SDIO 1-bit: CLK 2, CMD 3, D0 1 - offline map tiles, logs |
## Firmware flavors
| Env | What it does |
|-----|--------------|
| `Wio_Tracker_L2_companion_radio_ble` | **Phone companion.** Standard display UI, MeshCore app over BLE (pairing PIN `123456`), on-screen Bluetooth toggle page. |
| `Wio_Tracker_L2_companion_radio_usb` | Standard display companion over USB-C serial. |
| `Wio_Tracker_L2_repeater` | Standalone mesh repeater. |
| `Wio_Tracker_L2_room_server` | Standalone room/BBS server. |
## Build & flash
```bash
# from the MeshCore repo root
pio run -e Wio_Tracker_L2_companion_radio_ble
# bootloader mode if needed: hold User/Boot, tap RST, release - then:
pio run -e Wio_Tracker_L2_companion_radio_ble -t upload
```
The board enumerates as a native USB-CDC port (auto-reset via 1200-bps touch
is enabled). For the BLE companion build, pair from the MeshCore app with the
PIN shown on the device screen.
## Port notes
- **Everything hangs off the TCA9535 expander.** `WioTrackerL2Board::begin()`
replays Seeed's power-up sequence; order and delays matter (especially the
500 ms LCD reset settle). If the expander probe fails, the firmware still
runs but GPS/display/battery reads are dead.
- **Battery** is read via the ADS1115 at +/-4.096 V FSR and mapped through a
LiPo discharge curve.
- **GNSS**: GPS+BeiDou+GLONASS are enabled at start (`$PCAS04,7`). A watchdog
resets the module if it tracks 4+ strong satellites for minutes without
producing a fix. Raw NMEA can be logged to SD for diagnosis.
- **Power**: CPU drops to 80 MHz while the screen sleeps; the speaker amp and
I2S clocks run only while a tone plays; the GNSS and Grove rails are gated;
idle Bluetooth switches to slow advertising.
- **Display orientation**: `offset_rotation=1` (landscape). If a unit shows
the UI upside-down, use 3 in `WioTrackerL2Display.h`.
- Pin map source: `meshtastic/firmware` [PR #10909](https://github.com/meshtastic/firmware/pull/10909),
since merged as [`variants/esp32s3/seeed_wio_tracker_L2`](https://github.com/meshtastic/firmware/tree/master/variants/esp32s3/seeed_wio_tracker_L2),
and the `meshtastic/device-ui` [`wio-l2` branch](https://github.com/meshtastic/device-ui/tree/wio-l2).
+157
View File
@@ -0,0 +1,157 @@
#include "Sound.h"
#include <MeshCore.h>
#include <Arduino.h>
#include <Wire.h>
#include <SPIFFS.h>
#include <math.h>
#include "AudioBoard.h" // pschatzmann/arduino-audio-driver
#include <driver/i2s.h>
#include <esp_heap_caps.h>
// Wio-S3 audio wiring: ES8311 codec on I2C (0x18), I2S MCLK 10 / BCK 11 /
// WS 12 / DOUT 16; speaker amp power (PA EN) is raised by the board init.
#define SND_I2S_MCLK 10
#define SND_I2S_BCK 11
#define SND_I2S_WS 12
#define SND_I2S_DOUT 16
#define SND_SAMPLE_RATE 44100
static DriverPins snd_pins;
static AudioBoard snd_codec(AudioDriverES8311, snd_pins);
static bool snd_ready = false;
static void (*snd_amp_ctl)(bool) = NULL; // board hook: speaker amp power
void soundSetAmpControl(void (*fn)(bool on)) { snd_amp_ctl = fn; }
void soundInit() {
// i2s_driver_install crashes inside IDF's cleanup if its DMA allocation
// fails (LoadProhibited boot loop) - refuse to try without clear headroom
if (heap_caps_get_free_size(MALLOC_CAP_DMA) < 60000) {
MESH_DEBUG_PRINTLN("sound: low DMA heap - sound disabled this boot");
return;
}
snd_pins.addI2C(PinFunction::CODEC, Wire);
snd_pins.addI2S(PinFunction::CODEC, SND_I2S_MCLK, SND_I2S_BCK, SND_I2S_WS, SND_I2S_DOUT, -1);
CodecConfig cfg;
cfg.input_device = ADC_INPUT_NONE;
cfg.output_device = DAC_OUTPUT_ALL;
cfg.i2s.bits = BIT_LENGTH_16BITS;
cfg.i2s.rate = RATE_44K;
if (!snd_codec.begin(cfg)) {
MESH_DEBUG_PRINTLN("sound: ES8311 init failed - sound disabled");
return;
}
snd_codec.setVolume(70);
i2s_config_t i2s_cfg = {};
i2s_cfg.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
i2s_cfg.sample_rate = SND_SAMPLE_RATE;
i2s_cfg.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
i2s_cfg.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
i2s_cfg.communication_format = I2S_COMM_FORMAT_STAND_I2S;
i2s_cfg.intr_alloc_flags = 0;
i2s_cfg.dma_buf_count = 2; // keep the DMA footprint minimal
i2s_cfg.dma_buf_len = 256;
i2s_cfg.tx_desc_auto_clear = true;
if (i2s_driver_install(I2S_NUM_0, &i2s_cfg, 0, NULL) != ESP_OK) {
MESH_DEBUG_PRINTLN("sound: i2s driver install failed");
return;
}
i2s_pin_config_t pin_cfg = {};
pin_cfg.mck_io_num = SND_I2S_MCLK;
pin_cfg.bck_io_num = SND_I2S_BCK;
pin_cfg.ws_io_num = SND_I2S_WS;
pin_cfg.data_out_num = SND_I2S_DOUT;
pin_cfg.data_in_num = I2S_PIN_NO_CHANGE;
if (i2s_set_pin(I2S_NUM_0, &pin_cfg) != ESP_OK) {
MESH_DEBUG_PRINTLN("sound: i2s pin config failed");
return;
}
i2s_stop(I2S_NUM_0); // clocks off until a tone plays (GNSS-adjacent EMI, battery)
snd_ready = true;
MESH_DEBUG_PRINTLN("sound: ES8311 + I2S ready");
}
bool soundReady() { return snd_ready; }
void soundBeep(int freq_hz, int duration_ms) {
if (!snd_ready) return;
if (snd_amp_ctl) snd_amp_ctl(true);
i2s_start(I2S_NUM_0);
delay(3); // amp settle before the first samples
const int total = SND_SAMPLE_RATE * duration_ms / 1000;
const float amp = 6000.0f; // gentle level; codec volume does the rest
static int16_t buf[256 * 2]; // 256 stereo frames per chunk
int written_frames = 0;
while (written_frames < total) {
int chunk = total - written_frames;
if (chunk > 256) chunk = 256;
for (int i = 0; i < chunk; i++) {
float t = (float)(written_frames + i) / SND_SAMPLE_RATE;
// short attack/decay envelope so the beep doesn't click
float env = 1.0f;
int pos = written_frames + i;
if (pos < 220) env = pos / 220.0f;
else if (pos > total - 220) env = (total - pos) / 220.0f;
int16_t s = (int16_t)(amp * env * sinf(2.0f * (float)M_PI * freq_hz * t));
buf[i * 2] = s;
buf[i * 2 + 1] = s;
}
size_t written_bytes = 0;
i2s_write(I2S_NUM_0, buf, chunk * 2 * sizeof(int16_t), &written_bytes, portMAX_DELAY);
written_frames += chunk;
}
i2s_zero_dma_buffer(I2S_NUM_0);
i2s_stop(I2S_NUM_0);
if (snd_amp_ctl) snd_amp_ctl(false);
}
// selectable alert melodies; {0,0} terminates a style early
struct ToneNote { uint16_t freq; uint16_t ms; };
static const ToneNote TONE_STYLES[SOUND_TONE_STYLES][5] = {
{{880, 90}, {1175, 120}, {0, 0}}, // Classic
{{1400, 40}, {1800, 40}, {2200, 60}, {0, 0}}, // Chirp
{{1319, 140}, {988, 180}, {0, 0}}, // Ding dong
{{1568, 45}, {1245, 45}, {1568, 45}, {1245, 90}, {0, 0}}, // Trill
{{659, 70}, {880, 70}, {1175, 70}, {1568, 110}, {0, 0}}, // Rise
{{1047, 120}, {523, 120}, {1047, 120}, {0, 0}}, // Alarm
};
static int tone_style = 0;
void soundLoadTonePref() {
File f = SPIFFS.open("/tone", "r");
if (f) {
int v = f.parseInt();
f.close();
if (v >= 0 && v < SOUND_TONE_STYLES) tone_style = v;
}
}
void soundSetToneStyle(int style) {
if (style < 0 || style >= SOUND_TONE_STYLES) return;
tone_style = style;
File f = SPIFFS.open("/tone", "w");
if (f) {
f.print(style);
f.close();
}
}
int soundGetToneStyle() { return tone_style; }
void soundMessageTone() {
const ToneNote* n = TONE_STYLES[tone_style];
for (int i = 0; i < 5 && n[i].freq != 0; i++) soundBeep(n[i].freq, n[i].ms);
}
void soundChannelTone() {
// softer cousin of the chosen alert: first note, dropped a fourth
soundBeep(TONE_STYLES[tone_style][0].freq * 3 / 4, 100);
}
void soundAckTone() { soundBeep(1568, 50); }
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// Board audio backend for the Wio Tracker L2 Pro (ES8311 codec + speaker).
// UI code includes "Sound.h" and gets whatever the variant provides; all
// functions are safe no-ops if the codec failed to initialize.
#define SOUND_TONE_STYLES 6 // Classic, Chirp, Ding dong, Trill, Rise, Alarm
void soundInit(); // call once after Wire is up (board.begin done)
void soundSetAmpControl(void (*fn)(bool on)); // board hook to gate the speaker amp power
bool soundReady();
void soundBeep(int freq_hz, int duration_ms); // short blocking tone
void soundMessageTone(); // incoming DM (plays the selected alert style)
void soundChannelTone(); // incoming channel message (softer variant)
void soundAckTone(); // delivery ack / confirm
void soundLoadTonePref(); // read /tone from SPIFFS (safe before soundInit)
void soundSetToneStyle(int style); // select + persist an alert style
int soundGetToneStyle();
@@ -0,0 +1,204 @@
#include "WioTrackerL2Board.h"
// TCA9535 register map
static const uint8_t TCA_REG_OUTPUT0 = 0x02;
static const uint8_t TCA_REG_OUTPUT1 = 0x03;
static const uint8_t TCA_REG_CONFIG0 = 0x06; // 1 = input, 0 = output
static const uint8_t TCA_REG_CONFIG1 = 0x07;
// ADS1115 registers / config bits
static const uint8_t ADS_REG_CONVERSION = 0x00;
static const uint8_t ADS_REG_CONFIG = 0x01;
// OS=1 (start single shot) | MUX=100 (AIN0 vs GND) | PGA=001 (+/-4.096V)
// | MODE=1 (single shot) | DR=100 (128 SPS) | COMP_QUE=11 (disabled)
static const uint16_t ADS_CFG_BATT = 0x8000 | 0x4000 | 0x0200 | 0x0100 | 0x0080 | 0x0003;
// +/-4.096V FSR -> 125uV/LSB; battery is behind a x2 divider -> 0.25 mV/LSB
static const float ADS_MV_PER_LSB = 0.125f * 2.0f;
bool WioTrackerL2Board::expWriteReg(uint8_t reg, uint8_t val) {
Wire.beginTransmission(TCA9535_ADDR);
Wire.write(reg);
Wire.write(val);
return Wire.endTransmission() == 0;
}
void WioTrackerL2Board::expSetOutput(uint8_t pin, bool initial_level) {
uint8_t port = pin >> 3, bit = pin & 7;
// set desired level first so the pin doesn't glitch when direction flips
if (initial_level) out_shadow[port] |= (1 << bit);
else out_shadow[port] &= ~(1 << bit);
expWriteReg(port ? TCA_REG_OUTPUT1 : TCA_REG_OUTPUT0, out_shadow[port]);
cfg_shadow[port] &= ~(1 << bit); // 0 = output
expWriteReg(port ? TCA_REG_CONFIG1 : TCA_REG_CONFIG0, cfg_shadow[port]);
}
void WioTrackerL2Board::expSetInput(uint8_t pin) {
uint8_t port = pin >> 3, bit = pin & 7;
cfg_shadow[port] |= (1 << bit);
expWriteReg(port ? TCA_REG_CONFIG1 : TCA_REG_CONFIG0, cfg_shadow[port]);
}
void WioTrackerL2Board::expWritePin(uint8_t pin, bool level) {
uint8_t port = pin >> 3, bit = pin & 7;
if (level) out_shadow[port] |= (1 << bit);
else out_shadow[port] &= ~(1 << bit);
expWriteReg(port ? TCA_REG_OUTPUT1 : TCA_REG_OUTPUT0, out_shadow[port]);
}
// Power-up sequence mirrors Seeed's reference firmware for this board
// (order and delays matter, especially the LCD reset settle time).
bool WioTrackerL2Board::initExpander() {
Wire.beginTransmission(TCA9535_ADDR);
if (Wire.endTransmission() != 0) {
return false; // expander not responding
}
expSetInput(EXP_PIN_WAKE_BTN);
expSetInput(EXP_PIN_I2C_IRQ);
expSetInput(EXP_PIN_SD_DETECT);
expSetOutput(EXP_PIN_OTG_EN, LOW);
delay(10);
expSetOutput(EXP_PIN_PA_EN, LOW); // amp only powered while a tone plays
delay(10);
expSetOutput(EXP_PIN_TF_EN, HIGH);
delay(10);
expSetOutput(EXP_PIN_BAT_ADC_EN, HIGH);
delay(10);
expSetOutput(EXP_PIN_GNSS_EN, HIGH);
delay(10);
// GNSS reset is active HIGH; hold 10ms then release LOW so the L76K runs
expSetOutput(EXP_PIN_GNSS_RST, HIGH);
delay(10);
expWritePin(EXP_PIN_GNSS_RST, LOW);
// user LED is driven active-low through the expander; idle level HIGH = off
expSetOutput(EXP_PIN_USER_LED, HIGH);
delay(10);
expSetOutput(EXP_PIN_GROVE_EN, HIGH);
delay(10);
expSetOutput(EXP_PIN_LCD_EN, HIGH);
delay(50);
expSetOutput(EXP_PIN_LCD_RST, HIGH);
delay(5);
expWritePin(EXP_PIN_LCD_RST, LOW);
delay(10);
expWritePin(EXP_PIN_LCD_RST, HIGH);
delay(500); // NV3031B needs a long settle after reset before init commands
expSetOutput(EXP_PIN_LCD_CS, HIGH);
delay(10);
// GT911 touch reset sequence: INT low during reset selects I2C addr 0x5D
expSetOutput(EXP_PIN_TP_RST, LOW);
expSetOutput(EXP_PIN_TP_INT, LOW);
delay(10);
expWritePin(EXP_PIN_TP_RST, HIGH);
delay(60);
// capture WAKE button idle level (polarity unknown on alpha hardware)
int inputs = expReadInputs();
wake_btn_baseline = inputs >= 0 ? (uint8_t)(inputs & 1) : 0;
return true;
}
void WioTrackerL2Board::begin() {
// GNSS UART: NMEA arrives at ~500 B/s and the main loop can stall for
// hundreds of ms on SD tile decodes; the 256-byte default overflowed
Serial1.setRxBufferSize(1024);
uint32_t t0 = millis();
while (!Serial && millis() - t0 < 3000) { delay(50); }
MESH_DEBUG_PRINTLN("WioTrackerL2Board: ESP32Board init");
ESP32Board::begin(); // starts Wire on PIN_BOARD_SDA/PIN_BOARD_SCL (47/48)
pinMode(PIN_USER_BTN, INPUT_PULLUP);
pinMode(P_LORA_MISO, INPUT_PULLUP);
MESH_DEBUG_PRINTLN("WioTrackerL2Board: TCA9535 expander init");
expander_ok = initExpander();
if (!expander_ok) {
Serial.println("ERROR: TCA9535 IO expander not found - peripherals unpowered!");
}
Wire.beginTransmission(0x22);
aw_ok = Wire.endTransmission() == 0;
MESH_DEBUG_PRINTLN("WioTrackerL2Board: init done");
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) {
startup_reason = BD_STARTUP_RX_PACKET; // LoRa packet woke us from deep sleep
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
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
if (expander_ok) {
expWritePin(EXP_PIN_USER_LED, !on);
}
}
int WioTrackerL2Board::expReadInputs() {
Wire.beginTransmission(TCA9535_ADDR);
Wire.write((uint8_t)0x00); // input port 0 register
if (Wire.endTransmission() != 0) return -1;
if (Wire.requestFrom((int)TCA9535_ADDR, 2) != 2) return -1;
int lo = Wire.read();
int hi = Wire.read();
return lo | (hi << 8);
}
bool WioTrackerL2Board::isExternalPowered() {
if (!aw_ok) return false;
Wire.beginTransmission(0x22);
Wire.write((uint8_t)0x40); // STATUS0
if (Wire.endTransmission() != 0) return false;
if (Wire.requestFrom(0x22, 1) != 1) return false;
return (Wire.read() & 0x80) != 0; // VBUSOK bit
}
bool WioTrackerL2Board::readWakeButton() {
if (!expander_ok) return false;
int v = expReadInputs();
if (v < 0) return false;
return ((uint8_t)(v & 1)) != wake_btn_baseline;
}
int16_t WioTrackerL2Board::adsReadRaw() {
Wire.beginTransmission(ADS1115_ADDR);
Wire.write(ADS_REG_CONFIG);
Wire.write((uint8_t)(ADS_CFG_BATT >> 8));
Wire.write((uint8_t)(ADS_CFG_BATT & 0xFF));
if (Wire.endTransmission() != 0) return -1;
delay(10); // 128 SPS -> ~8ms conversion time
Wire.beginTransmission(ADS1115_ADDR);
Wire.write(ADS_REG_CONVERSION);
if (Wire.endTransmission() != 0) return -1;
if (Wire.requestFrom((int)ADS1115_ADDR, 2) != 2) return -1;
// sequence the two reads explicitly: operand evaluation order of <<|
// is unspecified and each read dequeues a byte
uint8_t hi = Wire.read();
uint8_t lo = Wire.read();
int16_t raw = ((int16_t)hi << 8) | lo;
return raw < 0 ? 0 : raw;
}
uint16_t WioTrackerL2Board::getBattMilliVolts() {
if (!expander_ok) return 0; // BAT_ADC_EN rail never came up
int16_t raw = adsReadRaw();
if (raw < 0) return 0;
return (uint16_t)(raw * ADS_MV_PER_LSB);
}
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include <Arduino.h>
#include <Wire.h>
#include "helpers/ESP32Board.h"
// ---------------------------------------------------------------------------
// Seeed Wio Tracker L2 Pro (Wio-S3 module: ESP32-S3 + SX1262)
//
// Nearly all peripheral power rails and reset lines are behind a TCA9535
// 16-bit I2C IO expander at address 0x21. Nothing (GNSS, LCD, touch, SD,
// battery ADC, speaker amp) works until the expander is configured, so
// begin() must run before any display / GPS / sensor init.
//
// Battery voltage is read through an ADS1115 16-bit I2C ADC at 0x48,
// channel AIN0, behind a x2 resistive divider (rail gated by expander).
// ---------------------------------------------------------------------------
#define TCA9535_ADDR 0x21
#define ADS1115_ADDR 0x48
// TCA9535 pin numbering: 0..7 = port 0 (P00..P07), 8..15 = port 1 (P10..P17)
#define EXP_PIN_WAKE_BTN 0 // input - side WAKE button
#define EXP_PIN_I2C_IRQ 1 // input - shared I2C IRQ
#define EXP_PIN_SD_DETECT 2 // input - microSD card detect
#define EXP_PIN_TP_INT 3 // output - touch panel interrupt (driven for reset seq)
#define EXP_PIN_LCD_CS 4 // output - LCD chip select (idle high)
#define EXP_PIN_LCD_EN 5 // output - LCD power enable
#define EXP_PIN_LCD_RST 6 // output - LCD reset
#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_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
#define EXP_PIN_TF_EN 14 // output - microSD power
#define EXP_PIN_BAT_ADC_EN 15 // output - battery ADC divider enable
class WioTrackerL2Board : public ESP32Board {
public:
void begin();
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override {
return "Seeed Wio Tracker L2 Pro";
}
void onBeforeTransmit() override { setLed(true); }
void onAfterTransmit() override { setLed(false); }
// Mesh/user LED lives on the IO expander, not a GPIO
void setLed(bool on);
// speaker amplifier power (expander P12); off when idle to keep the GNSS
// antenna away from class-D switching noise and to save battery
void setSpeakerAmp(bool on) { expWritePin(EXP_PIN_PA_EN, on); }
// hardware reset pulse to the L76K GNSS (active HIGH on this board); the
// module keeps almanac/ephemeris across it, so this is a warm restart
// GNSS rail: off saves the whole receiver when GPS is disabled; on re-runs
// the power-up reset so the module comes back cleanly
void setGnssPower(bool on) {
if (on) {
expWritePin(EXP_PIN_GNSS_EN, HIGH);
delay(10);
gnssReset();
} else {
expWritePin(EXP_PIN_GNSS_EN, LOW);
}
}
// Grove expansion port rail (nothing on-board depends on it)
void setGrovePower(bool on) { expWritePin(EXP_PIN_GROVE_EN, on); }
void gnssReset() {
expWritePin(EXP_PIN_GNSS_RST, HIGH);
delay(10);
expWritePin(EXP_PIN_GNSS_RST, LOW);
}
// WAKE button on expander P00: pressed = level differs from boot baseline
bool readWakeButton();
// VBUS presence via the AW35615 USB-C controller (I2C 0x22)
bool isExternalPowered() override;
bool expanderOK() const { return expander_ok; }
private:
uint8_t out_shadow[2] = { 0xFF, 0xFF }; // TCA9535 output regs default high
uint8_t cfg_shadow[2] = { 0xFF, 0xFF }; // 1 = input (power-on default)
bool expander_ok = false;
bool aw_ok = false; // AW35615 USB-C controller responded at probe
uint8_t wake_btn_baseline = 0; // idle level of P00, captured at init
int expReadInputs(); // 16-bit input register pair, -1 on error
bool expWriteReg(uint8_t reg, uint8_t val);
void expSetOutput(uint8_t pin, bool initial_level);
void expSetInput(uint8_t pin);
void expWritePin(uint8_t pin, bool level);
bool initExpander();
int16_t adsReadRaw();
};
@@ -0,0 +1,210 @@
#pragma once
#include <helpers/ui/LGFXDisplay.h>
#include <Wire.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
// ---------------------------------------------------------------------------
// Wio Tracker L2 Pro display stack:
// - NV3031B 320x240 panel on quad-SPI (SCLK 42, IO0-3 = 41/40/39/38, CS 46)
// - GT911 capacitive touch on I2C 0x5D (SDA 47 / SCL 48)
// - LP5814 4-channel LED driver on I2C 0x2C used as backlight
// Panel power/reset lines are on the TCA9535 expander and are already
// sequenced by WioTrackerL2Board::begin() before this driver initializes.
// ---------------------------------------------------------------------------
#ifndef L2_SPI_FREQUENCY
#define L2_SPI_FREQUENCY 75000000
#endif
#define LP5814_I2C_ADDR 0x2C
// LP5814 used as backlight controller (all 4 channels in parallel)
class WioTrackerL2Backlight : public lgfx::v1::ILight {
static constexpr uint8_t REG_DEVICE_CONFIG0 = 0x00;
static constexpr uint8_t REG_MAX_CURRENT = 0x01;
static constexpr uint8_t REG_ENABLE_CONTROL = 0x02;
static constexpr uint8_t REG_DIM_MODE = 0x04;
static constexpr uint8_t REG_ENGINE_MODE = 0x05;
static constexpr uint8_t REG_UPDATE = 0x0F;
static constexpr uint8_t REG_LED0_DC = 0x14;
static constexpr uint8_t REG_LED0_PWM = 0x18;
uint8_t _brightness = 153; // 60%
void writeReg(uint8_t reg, uint8_t value) {
Wire.beginTransmission(LP5814_I2C_ADDR);
Wire.write(reg);
Wire.write(value);
Wire.endTransmission();
}
public:
bool init(uint8_t brightness) override {
Wire.beginTransmission(LP5814_I2C_ADDR);
if (Wire.endTransmission() != 0) {
return false; // LP5814 not found
}
writeReg(REG_DEVICE_CONFIG0, 0x01); // chip enable
writeReg(REG_MAX_CURRENT, 0x01); // 51 mA max current
writeReg(REG_ENABLE_CONTROL, 0x00); // outputs off while configuring
writeReg(REG_DIM_MODE, 0x4E);
writeReg(REG_ENGINE_MODE, 0xF0);
for (uint8_t i = 0; i < 4; i++) {
writeReg(REG_LED0_DC + i, 200);
}
writeReg(REG_ENABLE_CONTROL, 0x0F); // enable all 4 channels
writeReg(REG_UPDATE, 0x55); // latch (LP5814 requires 0x55)
delay(5);
setBrightness(brightness);
return true;
}
void setBrightness(uint8_t brightness) override {
for (uint8_t i = 0; i < 4; i++) {
writeReg(REG_LED0_PWM + i, brightness);
}
_brightness = brightness;
}
uint8_t getBrightness() const { return _brightness; }
virtual ~WioTrackerL2Backlight() = default;
};
class LGFX_WioTrackerL2 : public lgfx::LGFX_Device {
lgfx::Panel_NV3031B _panel_instance;
lgfx::Bus_SPI _bus_instance;
lgfx::Touch_GT911 _touch_instance;
WioTrackerL2Backlight _light_instance;
public:
bool init_impl(bool use_reset, bool use_clear) override {
// bring up backlight controller while the I2C bus is still clean
_light_instance.init(_light_instance.getBrightness());
bool result = LGFX_Device::init_impl(use_reset, use_clear);
// GT911 probe can leave the ESP32 I2C peripheral with a stuck BUSY flag;
// cycling Wire resets it so later LP5814/sensor traffic doesn't time out
Wire.end();
Wire.begin(47, 48);
return result;
}
LGFX_WioTrackerL2(void) {
{
auto cfg = _bus_instance.config();
cfg.spi_host = SPI3_HOST;
cfg.spi_mode = 3;
cfg.freq_write = L2_SPI_FREQUENCY;
cfg.freq_read = 16000000;
cfg.pin_sclk = 42;
// quad SPI data pins
cfg.pin_io0 = 41;
cfg.pin_io1 = 40;
cfg.pin_io2 = 39;
cfg.pin_io3 = 38;
_bus_instance.config(cfg);
_panel_instance.setBus(&_bus_instance);
}
{
auto cfg = _panel_instance.config();
cfg.pin_cs = 46;
cfg.pin_rst = -1; // reset is on the IO expander, done in board init
cfg.pin_busy = -1;
cfg.panel_width = 240; // native portrait orientation
cfg.panel_height = 320;
cfg.memory_width = 240;
cfg.memory_height = 320;
cfg.offset_x = 0;
cfg.offset_y = 0;
cfg.offset_rotation = 1; // panel mounted landscape (320x240)
cfg.invert = true;
cfg.rgb_order = true;
cfg.dlen_16bit = false;
cfg.bus_shared = false;
_panel_instance.config(cfg);
}
{
auto cfg = _touch_instance.config();
cfg.pin_cs = -1;
cfg.x_min = 0;
cfg.x_max = 239;
cfg.y_min = 0;
cfg.y_max = 319;
cfg.pin_int = -1; // INT is on the IO expander
cfg.offset_rotation = 2;
cfg.i2c_port = 0;
cfg.i2c_addr = 0x5D;
cfg.pin_sda = 47;
cfg.pin_scl = 48;
cfg.bus_shared = false;
cfg.freq = 400000;
_touch_instance.config(cfg);
_panel_instance.setTouch(&_touch_instance);
}
_panel_instance.setLight(&_light_instance);
setPanel(&_panel_instance);
}
};
class WioTrackerL2Display : public LGFXDisplay {
LGFX_WioTrackerL2 disp;
public:
WioTrackerL2Display() : LGFXDisplay(320, 240, disp) {}
// direct access to the LGFX device (LVGL flush/touch glue)
lgfx::LGFX_Device* lgfxDevice() { return &disp; }
// reliable touch read in logical (UI_ZOOM-scaled) coords; returns false when
// not touched (LGFXDisplay::getTouch reads an uninitialized point on release)
bool readTouch(int& x, int& y) {
lgfx::touch_point_t tp;
if (disp.getTouch(&tp, 1) == 0) return false;
x = tp.x / UI_ZOOM;
y = tp.y / UI_ZOOM;
return true;
}
// shadows LGFXDisplay::begin() (non-virtual, called on the concrete type in
// main.cpp) - identical except rotation: our panel config already carries
// offset_rotation=1 for the landscape mounting, so no extra rotation here
bool begin() {
// dark theme: deep navy ground, MeshCore blue accents (RGB565)
UIColor::window_bkg = 0x0885; // dark navy
UIColor::title_bkg = 0x1299; // meshcore blue
UIColor::title_txt = 0xFFFF;
UIColor::primary_txt = 0xE73C; // near-white
UIColor::secondary_txt = 0x8D59; // muted blue-gray
UIColor::warning_txt = 0xFD20; // orange
UIColor::popup_bkg = 0x1299;
UIColor::popup_txt = 0xFFFF;
UIColor::corp_blue = 0x5DBF; // sky blue (own messages, icons)
turnOn();
display->init();
display->setRotation(0);
display->setColorDepth(8);
// The zoomed sprite blit stops a pixel short of the panel edge, so those
// pixels keep whatever the panel powered up with (they showed as coloured
// lines down the right side and along the bottom). Clear the panel once.
display->fillScreen(UIColor::window_bkg);
display->setBrightness(153);
display->setTextColor(TFT_WHITE);
buffer.setColorDepth(8);
buffer.setPsram(true);
buffer.createSprite(width(), height());
return true;
}
};
+134
View File
@@ -0,0 +1,134 @@
; Seeed Wio Tracker L2 Pro (alpha) - Wio-S3 module: ESP32-S3R8 + SX1262
; Pin map derived from Seeed's Meshtastic support branch (meshtastic/firmware
; PR #10909 + device-ui `wio-l2` branch).
[Wio_Tracker_L2]
extends = esp32_base
board = seeed_wio_tracker_l2
build_flags = ${esp32_base.build_flags}
${sensor_base.build_flags}
-I variants/wio-tracker-l2
-D SEEED_WIO_TRACKER_L2
-D PIN_BOARD_SDA=47
-D PIN_BOARD_SCL=48
-D PIN_USER_BTN=0
-D USE_SX1262
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D P_LORA_NSS=21
-D P_LORA_RESET=7
-D P_LORA_BUSY=8
-D P_LORA_DIO_1=9
-D P_LORA_SCLK=4
-D P_LORA_MISO=5
-D P_LORA_MOSI=6
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D ENV_INCLUDE_GPS=1
-D PIN_GPS_TX=18 ; L76K TX -> ESP32 RX
-D PIN_GPS_RX=17 ; L76K RX <- ESP32 TX
-D GPS_BAUD_RATE=9600
-D ENV_SKIP_GPS_DETECT=1 ; GPS is always present, powered via IO expander
-D DISPLAY_CLASS=WioTrackerL2Display
-D UI_ZOOM=2
; disable ALL env sensor I2C probes: this bus carries six onboard chips
; (TCA9535 0x21, AW35615 0x22, LP5814 0x2C, ADS1115 0x48, GT911 0x5D,
; ES8311 0x18) and stray probe matches can hang sensors.begin()
-D ENV_INCLUDE_AHTX0=0
-D ENV_INCLUDE_BME280=0
-D ENV_INCLUDE_BMP280=0
-D ENV_INCLUDE_SHTC3=0
-D ENV_INCLUDE_SHT4X=0
-D ENV_INCLUDE_INA3221=0
-D ENV_INCLUDE_INA219=0
-D ENV_INCLUDE_INA226=0
-D ENV_INCLUDE_INA260=0
-D ENV_INCLUDE_MLX90614=0
-D ENV_INCLUDE_VL53L0X=0
-D ENV_INCLUDE_BME680=0
-D ENV_INCLUDE_BMP085=0
-D ENV_INCLUDE_LPS22HB=0
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/wio-tracker-l2>
+<helpers/sensors/*.cpp>
+<helpers/ui/LGFXDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
lovyan03/LovyanGFX @ 1.2.30 ; pinned: first release that links NV3031B under gnu++14
https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip
; -------- Companion: BLE phone app, standard display UI --------------------
[env:Wio_Tracker_L2_companion_radio_ble]
extends = Wio_Tracker_L2
build_flags =
${Wio_Tracker_L2.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
-D HAS_TOUCH=1
-D AUTO_OFF_MILLIS=60000 ; LCD, no burn-in worry; 1 min screen timeout
build_src_filter = ${Wio_Tracker_L2.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Wio_Tracker_L2.lib_deps}
densaugeo/base64 @ ~1.4.0
; -------- Companion: USB serial link, standard display UI ------------------
[env:Wio_Tracker_L2_companion_radio_usb]
extends = Wio_Tracker_L2
build_flags =
${Wio_Tracker_L2.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D OFFLINE_QUEUE_SIZE=256
-D ENABLE_USB_INTERFACE
-D HAS_TOUCH=1
-D AUTO_OFF_MILLIS=60000
build_src_filter = ${Wio_Tracker_L2.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Wio_Tracker_L2.lib_deps}
densaugeo/base64 @ ~1.4.0
; -------- Standalone repeater: extends mesh coverage, no phone needed -------
[env:Wio_Tracker_L2_repeater]
extends = Wio_Tracker_L2
build_flags =
${Wio_Tracker_L2.build_flags}
-D ADVERT_NAME='"WioL2 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
build_src_filter = ${Wio_Tracker_L2.build_src_filter}
+<../examples/simple_repeater/*.cpp>
lib_deps =
${Wio_Tracker_L2.lib_deps}
${esp32_ota.lib_deps}
; -------- Standalone room server: shared message board / BBS ----------------
[env:Wio_Tracker_L2_room_server]
extends = Wio_Tracker_L2
build_flags =
${Wio_Tracker_L2.build_flags}
-D ADVERT_NAME='"WioL2 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
build_src_filter = ${Wio_Tracker_L2.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${Wio_Tracker_L2.lib_deps}
${esp32_ota.lib_deps}
+35
View File
@@ -0,0 +1,35 @@
#include <Arduino.h>
#include "target.h"
WioTrackerL2Board board;
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
GpsTapStream gps_tap(Serial1); // GSV sats-in-view / SNR watcher
MicroNMEALocationProvider gps(gps_tap, &rtc_clock);
EnvironmentSensorManager sensors(gps);
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
#endif
bool radio_init() {
MESH_DEBUG_PRINTLN("radio_init: rtc + sx1262 init");
fallback_clock.begin();
rtc_clock.begin(Wire); // Wire already running on 47/48 from board.begin()
bool ok = radio.std_init(&spi);
MESH_DEBUG_PRINTLN("radio_init: SX1262 %s", ok ? "OK" : "FAILED");
return ok;
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include "WioTrackerL2Board.h"
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
#include "GpsTap.h"
#ifdef DISPLAY_CLASS
#include "WioTrackerL2Display.h"
#include <helpers/ui/MomentaryButton.h>
#endif
extern WioTrackerL2Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
extern GpsTapStream gps_tap;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
mesh::LocalIdentity radio_new_identity();