Driver: setTextWrap(false) once at init. GFX no longer moves text to the next line by itself, anywhere. This is what put the stray "t" on the margin; without it, a line the screen miscounts is clipped at the edge instead, which is the lesser evil for a UI that does all its own wrapping. Applies to every e-ink build, Pro and Max.
Compose: the usable line width is now the panel width less the driver's 2-unit x offset, so the compose wrap and the glyphs' real positions agree and lines wrap one word earlier when they need to.
GxEPD2_310_GDEQ031T10_FAST.cpp → fingerprint 473 (was 464). The 9-frame bytes (0x89 / 0x49 / 0x09) become 0x8C / 0x4C / 0x0C: same level bits, count 12. Improves colour density in Max fastlut test build.
EXPERIMENT (Max only, env meck_max_ble_fastlut; all other builds unchanged)
- New vendored driver src/helpers/ui/GxEPD2_310_GDEQ031T10_FAST.{h,cpp}:
the GxEPD2 GDEQ031T10 driver with a selectable partial-refresh
waveform. Default is "LUT from register" loaded with the register
tables bitbank2/bb_epaper uses for this panel on the T-Deck Pro (one
9-frame push per pixel); the factory OTP waveform stays available.
Full refreshes unchanged. Measured on the Max: partial refresh 244 ms
(652 ms on the factory waveform) on home, menus, ROM list and typing.
Cost: one short push per pixel gives weaker blacks and half-develops
dithered stipple, so it suits text and does not suit the game.
- GxEPDDisplay: setFastWaveform() and fastPartialsSinceFull(), no-ops
outside the experiment env. Switching back to the factory waveform
hibernates the panel so the next refresh goes through a hardware
reset (tested as a ghosting fix; it was not one, kept as a clean
hand-over).
- UITask::setCurrScreen: after EINK_FAST_FULL_EVERY (20) fast partials
the redraw at the next screen change is a full refresh, so the
register waveform's ink imbalance is cleared where a redraw is
happening anyway rather than mid-typing.
- GBC emulator switches the driver to the factory waveform while a
game runs (its dithered picture needs the longer drive) and back to
fast on quit.
- Finding, for the record: moving-sprite ghosting in Pokemon Gold
appears on the stock build too, so it is the game plus dither plus
the factory partial waveform, not the experiment.
GBC EMULATOR (all T-Deck builds)
- "Loading..." alert box the moment Enter is pressed on a ROM row; the
multi-second SD read now runs on the next loop pass, after the box is
on the panel, instead of inside the key handler.
- Games start with a full refresh (clean panel; added during the
ghosting investigation, kept pending review).
- Save writes are timed and the durations logged (remove/open/write/
close), after one unexplained ~45 s UI stall coincided with a save.
Files: examples/companion_radio/ui-new/GBCEmulatorScreen.{h,cpp},
examples/companion_radio/ui-new/UITask.cpp, src/helpers/ui/GxEPDDisplay.h,
src/helpers/ui/GxEPD2_310_GDEQ031T10_FAST.{h,cpp},
variants/lilygo_tdeck_max/platformio.ini
Follow-up to the v1.14 emulator commits. T-Deck Max unless noted.
MEMORY AND STABILITY (GBCEmulatorScreen.cpp)
- Emulator task stack is now 8 KB, static, reserved at boot. Once
Bluetooth has been switched on in a boot its stack stays resident
(SerialBLEInterface::disable() never releases it) and a later game
launch could not find 12 KB of internal RAM in one piece. Measured
use is about 2 KB.
- Core context released on every failed launch, not just on quit; a
failure no longer holds 50 KB.
- No periodic full refresh during play (too distracting). The full
refresh on quit stays, to clear ghosting before the ROM list.
SAVES
- .sav written in the background: cart-RAM writes mark it dirty and
the UI task writes the file once the game has left its cart RAM
alone for 2 s, i.e. shortly after the user saves in-game. Quit
writes only if still dirty, and no longer draws and refreshes an
intermediate "Saving..." frame (that frame cost 650 ms by itself).
INPUT
- Both-shifts keyboard-backlight chord works mid-game: it is the one
key raw joypad mode lets through. The toggle body moved from
handleKeyboardInput() into toggleKeyboardBacklight() (main.cpp) so
the game and the normal path share one on/off state.
- On quit the keyboard stays in raw mode, still drained through the
panel's busy wait, until the ROM list has been drawn. Keys pressed
during the quit flash used to queue in the TCA8418 and replay as
navigation afterwards (one held Q ran four screens back).
PICTURE
- Game pictures after the first refresh only the 216-line game window
(GxEPDDisplay::requestWindowRefresh, consumed by endFrame). Measured
at 652 ms either way, the same as full-panel: the panel's waveform
time is the floor, so this saves data transfer only.
TYPING (main.cpp, all e-ink builds)
- COMPOSE_REFRESH_INTERVAL 700 -> 0. lastComposeRefresh is stamped
after the blocking e-ink refresh completes, so the interval only
added dead time on top of it. Worst-case keystroke-to-screen drops
from about 1.6 s to about 0.9 s. COMPOSE_TYPING_PAUSE unchanged.
GAMES MENUS
- Games menu and Game Boy ROM list rows follow the user's font size
and style via the NodePrefs helpers (as the channel picker does);
Max touch hit-test updated to match. Footers read Q:Back.
BUILD ENVS (Pro)
- meck_audio_wifi_hyn and meck_audio_standalone_hyn, the HYN-touch
counterparts of the existing meck_audio_ble_hyn.
Files: examples/companion_radio/{main.cpp, ui-new/GBCEmulatorScreen.{h,cpp},
ui-new/GamesMenuScreen.h}, src/helpers/ui/GxEPDDisplay.{h,cpp},
variants/lilygo_tdeck_max/TCA8418Keyboard.h,
variants/lilygo_tdeck_pro/platformio.ini
Follow-up to the initial Game Boy emulator commit (T-Deck Max).
SPEED (GBCEmulatorScreen.cpp)
- Pacing keeps its deficit: a frame that finishes late is paid back by
running the next ones back-to-back instead of resetting the deadline
and forgiving the lost time, which held Pokemon Gold at 0.96x. Only a
stall of more than 250 ms resyncs. Now measures 1.00x on every 5 s
window. A 1 ms yield every 30 undelayed frames keeps the idle task
(and the task watchdog) fed.
- The core context (49952 bytes) is allocated from internal RAM for
the duration of a game and freed on quit, with PSRAM as the fallback
if no internal block is free; the launch log says which it got.
- Speed line printed to serial every 5 s (window fps, average, ratio to
real time).
INPUT (GBCEmulatorScreen.cpp, GxEPDDisplay.h)
- GxEPDDisplay::setBusyCallback() exposes GxEPD2's busy-wait hook. The
emulator registers a keyboard poll on it while a game runs, so key
presses and releases are seen through the ~650 ms panel refresh
instead of waiting for it to finish.
PICTURE (GBCEmulatorScreen.cpp, UITask.cpp, GxEPDDisplay.{h,cpp})
- 1.5x nearest-neighbour scale to 240x216, the full panel width,
centred; title line removed, quit hint kept.
- Ordered 4x4 Bayer dithering replaces the single threshold: the four
Game Boy shades map to 0, 6, 11 and 16 dark pixels in 16, so
sprites with mid-tone colours are no longer flattened to white or
black. The pattern is fixed to panel position so it does not crawl
between refreshes.
- UITask: the 800 ms minimum interval between e-ink refreshes is
lifted while a game runs, so pictures follow at the panel's own
partial-refresh rate (about 0.7 s).
- GxEPDDisplay::requestFullRefresh(): the next endFrame() uses the
full black/white waveform regardless of frame change. The emulator
requests one every 45 pictures (about 30 s) and once on quit, to
clear the residue that continuous partial refreshes leave on the
game screen and the home screen after it. No other screen calls it.
Files: examples/companion_radio/ui-new/GBCEmulatorScreen.{h,cpp},
examples/companion_radio/ui-new/UITask.cpp,
src/helpers/ui/GxEPDDisplay.{h,cpp}
MAPS FIX (all T-Deck Pro and Max builds)
Opening Maps from the home tile showed "SD card not found" until the G key
had been pressed once that boot. The map screen's SD flag was only set by
the G key handler; the touch tile and CardKB routes opened the screen
without it. The G handler's setup (SD flag, GPS position, contact markers,
CPU boost) is now openMapScreen() in main.cpp and all three routes call
it. G on the map (re-centre) is unchanged.
PLATFORMIO (variants/lilygo_tdeck_max)
- New meck_max_ble_garmin env for the Garmin watch companion: identical to
meck_max_ble with BLE_DEBUG_LOGGING, MECK_BLE_JUST_WORKS (no-passkey
pairing), MECK_BLE_SMALL_MTU_SPLIT (23-byte ATT frame slicing),
MECK_WATCH_EXT (watch extension commands) and MECK_BLE_SLOW_INTERVAL
switched on.
- meck_max_ble now has those five flags commented out, so the standard
phone-app build is isolated from the watch experiments.
GAME BOY / GAME BOY COLOR EMULATOR (T-Deck Max only in this build)
New "Game Boy" entry in the Games menu. Lists .gb/.gbc ROMs in /roms on
the SD card and runs them with the Peanut-GB core (same patched copy as
Meck-P4) in its own task on core 0 at the real 59.73 fps, drawing a
black-and-white snapshot to the e-ink at the panel's refresh rate.
- Keys: W/A/S/D d-pad, K = A, J = B, Enter = Start, Space = Select,
Shift+Backspace = quit (save written). No sound yet.
- Saves: .sav next to the ROM, written on quit.
- LoRa radio in standby and mesh loop paused while a game runs (reuses
the OTA pause). CPU held at 240 MHz.
- Keyboard polled through the e-ink busy wait so input is not delayed by
the ~650 ms refresh; the 800 ms refresh floor is lifted during play.
- Speed line printed to serial every 5 s.
Files:
- ui-new/GBCEmulatorScreen.{h,cpp} (new), ui-new/peanut_gb.h (new)
- ui-new/GamesMenuScreen.h: GAME_GBC entry
- ui-new/UITask.{h,cpp}: gbc_screen, goto/is/get, resetIdleTimer(),
no refresh floor while a game runs
- main.cpp: launch, exit and key routing
- variants/lilygo_tdeck_max/TCA8418Keyboard.h: raw joypad mode
- src/helpers/ui/GxEPDDisplay.h: setBusyCallback()
Pro builds compile unchanged (all emulator code is guarded).
- ChannelDetails gains scope_key[16]; MyMesh::setChannel derives it from
scope_name on every write (screens, serial, phone sync, boot load), so
sending copies bytes instead of doing a name lookup and derivation per
message. Port of the Meck Watch regions fix.
- getChannelScopeName now copies out instead of returning a pointer into
a local (dangling; caused intermittent unscoped sends).
- BLE small-MTU slices become [tag][len][payload] (18 payload bytes) so
stream transports (nRF52) can find slice boundaries; same format on
ESP32 for one dialect across nodes and the Garmin app.
- New MECK_BLE_SLOW_INTERVAL flag: request a 100-200 ms connection
interval instead of 15-20 ms for the watch build.
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.
get dutycycle → reports the current duty as a percentage (e.g. > 50%)
set dutycycle <10-100> → sets it, saves to prefs, persists on reboot; anything below 10 or above 100 returns ERR - duty cycle must be 10-100
set af <value> is untouched, for anyone who prefers the raw factor
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.
SerialBLEInterface.cpp — added esp_bt.h include and three esp_ble_tx_power_set calls at +9 dBm after BLEDevice::init(), covering default, advertising, and scan power types.
MyMesh.h — FIRMWARE_VER_CODE bumped from 10 → 11.
MyMesh.cpp — The RESP_CODE_DEVICE_INFO frame construction now:
Byte 2: sends 0xFF (sentinel) when MAX_CONTACTS > 510, otherwise the normal MAX_CONTACTS / 2. Older apps interpret 0xFF as 510 contacts — completely harmless.
Bytes 80-81 (new, appended after the version string): uint16_t little-endian with the true MAX_CONTACTS value. Apps that understand v11+ read it here. Apps < v11 ignore trailing bytes — the BLE/serial frame protocol is length-delimited, so extra bytes at the tail are safe.
platformio.ini — Both BLE builds (meck_audio_ble, meck_4g_ble) bumped from 510 → 2000.
mymesh.cpp: writeContactRespFrame return type change (return _serial->writeFrame() result)
checkSerialInterface() batch-fill loop.