From 318bc13dda83d090698ab23d2e4c2617bb76ae72 Mon Sep 17 00:00:00 2001 From: Hacuchino-hash <246103064+Hacuchino-hash@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:36:20 -0500 Subject: [PATCH 1/2] Add Seeed Wio Tracker L2 Pro variant ESP32-S3 with SX1262, a 3.2" 320x240 touch LCD, an L76K GNSS module and an SD slot. Everything except the radio hangs off a TCA9535 expander, so the board brings up power and reset lines in Seeed's documented order before the display and GNSS are usable. Companion (BLE and USB), repeater and room server environments run on the existing shared code. Pre-release hardware: the pin map may change on production units. --- boards/seeed_wio_tracker_l2.json | 43 ++++ variants/wio-tracker-l2/GpsTap.h | 139 ++++++++++++ variants/wio-tracker-l2/README.md | 64 ++++++ variants/wio-tracker-l2/Sound.cpp | 157 +++++++++++++ variants/wio-tracker-l2/Sound.h | 19 ++ variants/wio-tracker-l2/WioTrackerL2Board.cpp | 204 +++++++++++++++++ variants/wio-tracker-l2/WioTrackerL2Board.h | 106 +++++++++ variants/wio-tracker-l2/WioTrackerL2Display.h | 210 ++++++++++++++++++ variants/wio-tracker-l2/platformio.ini | 134 +++++++++++ variants/wio-tracker-l2/target.cpp | 35 +++ variants/wio-tracker-l2/target.h | 30 +++ 11 files changed, 1141 insertions(+) create mode 100644 boards/seeed_wio_tracker_l2.json create mode 100644 variants/wio-tracker-l2/GpsTap.h create mode 100644 variants/wio-tracker-l2/README.md create mode 100644 variants/wio-tracker-l2/Sound.cpp create mode 100644 variants/wio-tracker-l2/Sound.h create mode 100644 variants/wio-tracker-l2/WioTrackerL2Board.cpp create mode 100644 variants/wio-tracker-l2/WioTrackerL2Board.h create mode 100644 variants/wio-tracker-l2/WioTrackerL2Display.h create mode 100644 variants/wio-tracker-l2/platformio.ini create mode 100644 variants/wio-tracker-l2/target.cpp create mode 100644 variants/wio-tracker-l2/target.h diff --git a/boards/seeed_wio_tracker_l2.json b/boards/seeed_wio_tracker_l2.json new file mode 100644 index 0000000000..706ed4c170 --- /dev/null +++ b/boards/seeed_wio_tracker_l2.json @@ -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" +} diff --git a/variants/wio-tracker-l2/GpsTap.h b/variants/wio-tracker-l2/GpsTap.h new file mode 100644 index 0000000000..020f5e9b18 --- /dev/null +++ b/variants/wio-tracker-l2/GpsTap.h @@ -0,0 +1,139 @@ +#pragma once + +#include + +// 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); } +}; diff --git a/variants/wio-tracker-l2/README.md b/variants/wio-tracker-l2/README.md new file mode 100644 index 0000000000..29fbece6fb --- /dev/null +++ b/variants/wio-tracker-l2/README.md @@ -0,0 +1,64 @@ +# 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 and `meshtastic/device-ui` + (`wio-l2` branches). diff --git a/variants/wio-tracker-l2/Sound.cpp b/variants/wio-tracker-l2/Sound.cpp new file mode 100644 index 0000000000..b84f4a7719 --- /dev/null +++ b/variants/wio-tracker-l2/Sound.cpp @@ -0,0 +1,157 @@ +#include "Sound.h" + +#include + +#include +#include +#include +#include +#include "AudioBoard.h" // pschatzmann/arduino-audio-driver +#include +#include + +// 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); } diff --git a/variants/wio-tracker-l2/Sound.h b/variants/wio-tracker-l2/Sound.h new file mode 100644 index 0000000000..9e5f633c0b --- /dev/null +++ b/variants/wio-tracker-l2/Sound.h @@ -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(); diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.cpp b/variants/wio-tracker-l2/WioTrackerL2Board.cpp new file mode 100644 index 0000000000..edeaacce52 --- /dev/null +++ b/variants/wio-tracker-l2/WioTrackerL2Board.cpp @@ -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); +} diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.h b/variants/wio-tracker-l2/WioTrackerL2Board.h new file mode 100644 index 0000000000..c5644e2f20 --- /dev/null +++ b/variants/wio-tracker-l2/WioTrackerL2Board.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#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(); +}; diff --git a/variants/wio-tracker-l2/WioTrackerL2Display.h b/variants/wio-tracker-l2/WioTrackerL2Display.h new file mode 100644 index 0000000000..d9089a64d4 --- /dev/null +++ b/variants/wio-tracker-l2/WioTrackerL2Display.h @@ -0,0 +1,210 @@ +#pragma once + +#include +#include + +#define LGFX_USE_V1 +#include + +// --------------------------------------------------------------------------- +// 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; + } +}; diff --git a/variants/wio-tracker-l2/platformio.ini b/variants/wio-tracker-l2/platformio.ini new file mode 100644 index 0000000000..9434d08187 --- /dev/null +++ b/variants/wio-tracker-l2/platformio.ini @@ -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> + + + + + + +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} + + + +<../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} + + + +<../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} diff --git a/variants/wio-tracker-l2/target.cpp b/variants/wio-tracker-l2/target.cpp new file mode 100644 index 0000000000..a87da39249 --- /dev/null +++ b/variants/wio-tracker-l2/target.cpp @@ -0,0 +1,35 @@ +#include +#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 +} diff --git a/variants/wio-tracker-l2/target.h b/variants/wio-tracker-l2/target.h new file mode 100644 index 0000000000..11a66e4ec1 --- /dev/null +++ b/variants/wio-tracker-l2/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include "WioTrackerL2Board.h" +#include +#include +#include +#include +#include "GpsTap.h" +#ifdef DISPLAY_CLASS + #include "WioTrackerL2Display.h" + #include +#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(); From 16f48658da65b998bc4149966060a8c816abd663 Mon Sep 17 00:00:00 2001 From: Hacuchino-hash <246103064+Hacuchino-hash@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:36:56 -0500 Subject: [PATCH 2/2] Add the Wio Tracker L2 Pro companion build to the PR check matrix --- .github/workflows/pr-build-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index cebf0cfe5c..af848b7011 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -46,6 +46,8 @@ jobs: - wio_wm1110_repeater # SX1276 (ESP32) - Tbeam_SX1276_repeater + # Seeed Wio Tracker L2 Pro (ESP32-S3, LovyanGFX display) + - Wio_Tracker_L2_companion_radio_ble steps: - name: Clone Repo