Files
MeshCore-Solo/variants/wio-tracker-l2/WioTrackerL2Display.h
T
JakubandClaude Opus 5.5 84d8a4df2a feat(ui-lvgl): navigation map, clock tools, settings pages, radio
- Home > Map is the navigation map: waypoints (list, add at GPS / by
  coordinates / long-press spot), live shares, trail recording + tools
  (save, load, GPX export, track back), live share controls, target bar
  with ETA; the nodes map opens from Nearby.
- Map: smooth panning (no decode during a drag, idle read-ahead, cached
  coarser stand-ins), overzoom limited to 2 levels, resumable downloads,
  map credit behind a "©" and in Settings > About; target ring and trail
  computed in double.
- Home in swipeable pages; Clock (alarm, timer, stopwatch, ring card).
- Settings rendered from the Core schema (display & power with a
  brightness slider, messages & contacts, trail / live share / alerts),
  GPS switch, Radio screen; low-battery shutdown.
- WioTrackerL2Display: honour the 0-4 brightness level.
- docs: L2 hardware checklist, ui-core notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:20:42 +02:00

206 lines
6.3 KiB
C++

#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) {}
// NodePrefs::display_brightness level 0-4 -> backlight PWM
void setBrightness(uint8_t level) override {
static const uint8_t PWM[5] = { 20, 60, 120, 190, 255 };
display->setBrightness(PWM[level > 4 ? 4 : level]);
}
// 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() {
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(TFT_BLACK);
display->setBrightness(153);
display->setTextColor(TFT_WHITE);
buffer.setColorDepth(8);
buffer.setPsram(true);
buffer.createSprite(width(), height());
return true;
}
};