Fixes a regression from the MECK_TWATCH rename. main.cpp's tap dispatch had
'#if defined(MECK_TWATCH) && HAS_GPS' wrapped around two unrelated things: the
map tap handler AND the early return that stops the watch falling through to the
T5S3 tile hit-test. MECK_TOUCH_ENABLED is defined for MECK_TWATCH (main.cpp:712),
so on the S3 that early return vanished and a plain tap on the home tile page ran
the T5S3 geometry (tileW=40, tileH=22). Split into two gates. The other three
HAS_GPS gates were audited and are genuinely map-only.
Adds WatchAlarmScreen: four slots, day-of-week presets, vibrate-only. It keeps
AlarmScreen.h's conventions (bit 0 = Sunday, dowFromEpoch, effect 14, the
3-buzz / 1200ms / 4000ms-pause cadence) but shares no code, because AlarmScreen
is bound to ESP32-audioI2S, an SD card and a 1-21 Audio volume scale, none of
which exist here. Config lives on the LittleFS 'maps' partition, already mounted.
Alarms are ticked from UITask::loop, not the screen's poll(), so one fires with
the display off or another screen showing. Navigating away while ringing counts
as a dismiss.
DRV2605Haptic.h is copied into the watch variant rather than shared, so MAX
builds are untouched. The watch needs no motorEnable(): the DRV2605 sits on
BLDO2, which power_init() already brings up.
The Maps tile becomes Alarms on LILYGO_TWATCH_S3 only. The Plus keeps Maps.
The 'buzz' CLI command now also builds on the watch and takes an optional effect
number ('buzz 14'), for characterising the motor. Exact-match behaviour of plain
'buzz' on the MAX is unchanged.
Three build failures, all mine:
1. XPowersLib access. setPowerChannelVoltage, enable/disablePowerOutput and
isPowerChannelEnable are protected on XPowersAXP2101 and public only on
XPowersLibInterface, so holding the PMU as the concrete type made every rail
call illegal. The board now keeps both pointers to the same object: PMU
(interface) for the channel ops, _axp (concrete) for setIrqLevelTime and the
PWRON press/release edges, which the interface does not declare. Verified
against XPowersLib v0.2.9, the version ^0.2.7 resolves to.
2. ENV_INCLUDE_GPS. sensor_base defaults it to 1. The variant zeroed every other
ENV_INCLUDE_* but not that one, so gpsStream, board.gpsPowerOn/Off and the
GPS home page were all still compiled in. Now -D ENV_INCLUDE_GPS=0.
3. GPS_BAUDRATE is referenced unguarded by MyMesh.cpp's gps.baud CLI command.
variant.h now supplies the same #ifndef fallback the T5S3 variant uses.
Also renames the S3 Plus envs to meck_twatch_s3_plus_standalone and
meck_twatch_s3_plus_ble, so they no longer read as the generic watch build.
Nothing outside variants/lilygo_twatch_s3_plus/platformio.ini referenced the old
names.
Same silicon and the same pin map as the T-Watch S3 Plus, verified against the
T_WATCH-S3 schematic (rev 25-03-24) and LilyGo's utilities.h. The differences
are what is populated:
- no onboard GNSS. HAS_GPS/ENV_INCLUDE_GPS are left undefined, which drops the
map screen, the GPS home page and BLDO1 rail control via the gates added in
the MECK_TWATCH rename.
- no GPIO button. Schematic sheet 1 wires the side switch SW7 to net PWR_KEY
-> AXP2101 pin 30 (PWRON), so PIN_USER_BTN is undefined and MECK_PMU_BUTTON
selects PMUButton, which reads PKEY interrupts over I2C. Short press is
enter/select; a 6s hold is a hardware power-off the firmware never sees.
- DLDO1 is the MAX98357A speaker rail (schematic sheet 6, net SPK_VDD), not
'unused' as LilyGo's doc claims. It stays off, so the amp is unpowered.
- VBACKUP is enabled at 3300 mV. It charges the MS412FE coin cell (J12) that
backs the PCF8563's single-VDD supply. Leaving it off drained the cell with
nothing to replenish it.
New envs: meck_twatch_s3_standalone, meck_twatch_s3_ble.
No existing board is affected: MECK_PMU_BUTTON is defined only here.
The macro was doing double duty: gating the 240x240 touch UI (tile grid, lock
screen, VKB, grey palette) and gating the Plus's onboard GNSS. A non-GPS watch
cannot reuse the first without dragging in the second.
MECK_TWATCH watch form factor
LILYGO_TWATCH_S3_PLUS Plus-only hardware (GNSS on BLDO1, GPIO0 user button)
MECK_PMU_BUTTON user button is the AXP2101 PWRON key, not a GPIO
84 sites renamed across 14 files. The WatchMapScreen include, construction and
its three touch handlers now also require HAS_GPS, as does the Maps tile branch
inside the home tile grid.
Behaviour-preserving: the Plus defines both MECK_TWATCH and HAS_GPS=1, so every
new condition evaluates as before. No other board defines MECK_TWATCH.
T-Watch: So many UI changes. Also swapped render battery indicator to use the AXP2101 gauge for hopefully higher accuracy. Also implemnted meck rx duty cycle for watch builds only to test out power saving effects first on watch build before implementing elsewhere.
A complete `pio run` (all 16 environments) on Linux turned up 6
failures beyond the case-sensitive-include fix already on this
branch:
1. meck_remote_repeater, meck_wifi_repeater, meck_wifi_repeater_t5s3
compile GxEPDDisplay.cpp / FastEPDDisplay.cpp, which #include
MeckFonts.h whenever HAS_MECK_FONTS is defined. HAS_MECK_FONTS is
set at the shared variant-base level (so all envs in that variant
inherit it), but the `-I examples/companion_radio/ui-new` include
path needed to find MeckFonts.h was only added to the
companion-radio envs, not the repeater envs in the same file.
Added the missing -I flag to the three repeater env sections.
2. meck_wifi_repeater_heltec_v3/v4/v4_headless failed with "missing
SConscript file 'merge-bin.py'". esp32_base's extra_scripts still
pointed at merge-bin.py, which was deleted in 451f4b01 ("remove
redundant merge script"). Other variants override extra_scripts
themselves so they never hit the stale base default; the Heltec
variants don't, so they inherited the broken reference. Pointed
esp32_base at merge_firmware.py instead, matching every variant
that already sets this explicitly.
3. After fix#1, meck_wifi_repeater_t5s3 still failed: wifimqtt.cpp
had a hardcoded `extern AutoDiscoverRTCClock rtc_clock;`, but the
T5S3 variant declares its global rtc_clock as PCF85063Clock (a
different concrete RTCClock subclass) in target.h. The local
extern was redundant anyway, since target.h is already included
at the top of this file and declares the correctly-typed global
for whichever variant is being built. Removed the stale extern so
the call resolves through the existing declaration.
Verified: `pio run` now succeeds for all 16 environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The repo has apparently only ever been built on case-insensitive
filesystems (macOS/Windows): every #include in the codebase uses
intended PascalCase/CamelCase header names (e.g. "SettingsScreen.h",
"WiFiMQTT.h"), but 28 of the actual files on disk were saved with
inconsistent casing (e.g. "Settingsscreen.h", "wifimqtt.h"). On a
case-sensitive filesystem (Linux) this is a hard compile failure, not
a cosmetic mismatch -- confirmed by running `pio run -e meck_audio_ble`
on Gentoo Linux, which failed immediately on "target.h: No such file
or directory" and a cascade of similar errors as each fix exposed the
next one.
Root causes, two flavors of the same underlying bug:
1. Header filename casing (29 files renamed via `git mv` to preserve
history): examples/companion_radio/ui-new/*, examples/simple_repeater/*,
and two variant-local headers (PCF85063Clock.h, TCA8418Keyboard.h x2).
Verified safe before renaming: every file has exactly one consistent
intended casing across all the places that #include it (checked via
a repo-wide scan comparing every #include against on-disk filenames,
zero conflicts found), so each rename is a pure no-op for behavior.
2. PlatformIO config paths using the wrong case for variant directories
that are actually lowercase on disk (variants/lilygo_tdeck_pro,
variants/lilygo_t5s3_epaper_pro):
- `-I variants/LilyGo_TDeck_Pro` / `-I variants/LilyGo_T5S3_EPaper_Pro`
in build_flags (3 occurrences, including lilygo_tdeck_max's
reference to TDeck Pro's shared headers) -- broke header resolution
for target.h and friends.
- `+<../variants/LilyGo_TDeck_Pro>` / `+<../variants/LilyGo_T5S3_EPaper_Pro>`
in build_src_filter (2 occurrences) -- silently excluded the board-init
.cpp files (TDeckBoard.cpp etc.) from compilation entirely, which
didn't fail until the *link* stage ("undefined reference to
radio_init()", `TDeckBoard::begin()`, etc.) since PlatformIO's glob
just matched nothing rather than erroring.
Verified fix: `pio run -e meck_audio_ble` now compiles, links, and
produces a firmware image cleanly (RAM 53.1%, Flash 49.6%).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keyboard LED brightness (T-Deck Pro MAX):
Adds a "Keyboard LED" row to Settings (MAX only), below Backlight
Brightness and mirroring it: 5-100% in 5% steps, default 50%. The
both-shifts keyboard backlight toggle previously used a hardcoded PWM
level of 8 (~3%), too dim for some users; it now reads the stored
percentage and maps it to PWM, taking effect on the next toggle.
- NodePrefs.h: new kb_backlight_pct field (5..100, default 50)
- Settingsscreen.h: ROW_KB_BACKLIGHT row plus label/edit handling
- DataStore.cpp: load/save/clamp, appended so existing prefs files stay
compatible (fall back to 50%)
- main.cpp: toggle uses kb_backlight_pct (percent -> PWM) instead of 8
Standalone unread counter fix:
On standalone builds the home-page MSG count and channel-picker unread
badges never updated, although messages arrived and displayed normally.
ArduinoSerialInterface::isConnected() is hardcoded to return true (a plain
UART has no connection state), so the periodic
setHasConnection(_serial->isConnected()) in MyMesh left hasConnection()
permanently true -- making UITask mark every received message read on
arrival. The same stuck flag also suppressed the DM counter and the
new-message screen wake. Guard the call so only builds with a real
companion (BLE / WiFi / wired SERIAL_RX) derive connection state from the
interface; companion-less builds report not-connected. The core
ArduinoSerialInterface is unchanged, preserving wired-companion behaviour.
- MyMesh.cpp: guard setHasConnection() for companion-less builds
GPS — fixed (the "off" toggle and boot path now actually cut the XL9555 rail). Kept in main.cpp + UITask.cpp.
BLE controller — the ~13 mA between the BLE build (BLE off) and standalone is the controller staying initialised; the boot-gate in SerialBLEInterface.cpp/.h defers BLEDevice::init() until first enable, reclaiming it. That's the meaningful idle win.
CPU, gyro rail, ES8311, frontlight — all measured and ruled out. The residual ~12 mA Max-vs-Pro is distributed always-on hardware with no software switch — hardware overhead, not a bug.
Defer ESP32 BLE controller bring-up to first user enable
MyMesh::startInterface() called serial.enable() unconditionally at boot. On the ESP32 BLE build this ran the deferred-init SerialBLEInterface's _realBegin()/BLEDevice::init() and powered the BT controller before main.cpp's boot-time disable(), which only stops advertising and cannot power the controller back down -- so the controller stayed up while "off", drawing ~13 mA at idle. Guard the enable() so ESP32 BLE builds skip it at boot; the controller now comes up lazily on the first enable() when the user turns Bluetooth on from the Bluetooth page. WiFi builds are unaffected and still enable at boot.
The fix (six sites: 948, 978, 1519, 1651, 1719, 1906): the indexer now chooses pixel-vs-char wrapping from _display->getFontStyle(), the same basis the renderer (1200), the layout sizing (1336), and the cache key already use. With all four agreeing, the indexer's recorded page boundary lands on the exact byte where the renderer's height-based stop lands — no more swallowed run between pages.
Version bump (INDEX_VERSION 13 → 14): your existing .idx caches were built by the old indexer and would otherwise be reloaded as-is (same font key), so the fix would appear to do nothing. Bumping the version makes the loader discard every v13 cache and rebuild it.
All six edits are in and braces balance (267/267). Here's what changed in Audiobookplayerscreen.h:
PNGdec include (47) and cover size 40→30 (67) — 30 virtual units ≈ 55px.
PNG draw callback coverDrawCallbackPNG (147) — nearest-neighbour downscales the 256px source line-by-line and dithers to 1-bit using the same Bayer matrix as the JPEG path. The decoder is heap-allocated per-decode (new PNG()), mirroring how the JPEG path uses new JPEGDEC(), so there's no permanent BSS cost.
decodeFolderCoverPNG() (419) + tryLoadFolderCover() (505) — reads cover.png from _currentPath into PSRAM and decodes it.
Hooks in both openBook (1099) and advanceTrack (1198), so the cover loads on open and persists as tracks auto-advance within an album. It only loads when there's no embedded cover, so M4B/ID3 embedded art still wins; music (no embedded art) gets the folder PNG.
Layout (1531, 1556) — cover now draws below the artist line, above the status/Paused-Vol line, exactly where you wanted it.
target.h — declares meck_audio_route_amp() and meck_audio_codec_init() in the bridge (54-55), guarded by HAS_ES8311_AUDIO, ready for the alarm/voice paths to reuse later.
target.cpp — includes ES8311.h (6) and defines both helpers where board and the codec driver are visible: route+amp (100-102), and the once-only es8311_init_44100_16bit() (108-110).
Audiobookplayerscreen.h — forward-declares both (54-55); ensureI2SInit() now does route+amp and the 5-arg setPinout with MCLK on MAX (268-270); and meck_audio_codec_init() runs right after connecttoFS (1142). The Pro path is untouched.