EMULATOR (T-Deck Max)
- Q now quits a running game, matching the back key used across the
rest of the interface. Shift+Backspace still works. Added as a second
exit latch in the keyboard driver's raw joypad mode; the on-screen
hint reads "Q: Quit".
variants/lilygo_tdeck_max/TCA8418Keyboard.h
examples/companion_radio/ui-new/GBCEmulatorScreen.{h,cpp}
README
- New "Game Boy / Game Boy Color Emulator (T-Deck Max, v1.14+)"
subsection under Games: what it is, where to get free homebrew ROMs
(uCity, Halo: Combat Devolved), where to put them (/roms, .gb/.gbc,
32 files, names under 48 characters), how to launch and play (key
table, radio paused and CPU at 240 MHz while a game runs), what the
picture looks like and why it updates about 1.4 times a second,
saves (.sav on quit, no RTC state), and the release's limits.
- Contents entry for the new subsection; Games intro mentions the
Games tile and the Max-only emulator.
- SD Card Requirements lists Game Boy ROMs and saves.
- T-Deck Max intro cross-references the emulator.
- Road-Map: emulator done on the Max; Pro support, ES8311 sound with
mic-key mute, and RTC save state listed as to-do.
- Third-Party Libraries: Peanut-GB (MIT, Mahyar Koshkouei) added, with
a note that no Game Boy software is bundled.
Version tags on the new items are v1.14; the global version bump
follows in the next commit.
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.
Canned messages (T-Deck Max only):
- NodePrefs gains canned_msgs[10][134] as the final field; DataStore load is short-read tolerant (older prefs upgrade to empty slots), NUL-forced, appended in savePrefs. Storage is shared across boards so the prefs layout stays uniform; the Settings entry point is gated to the Max.
- Settings -> Canned Messages: ten slots, full-length inline editor (tail + n/133 counter); Enter commits, empty clears, Sh+Del cancels. T5S3 VKB path exists behind the same gate.
- Channel screen overlay: the speech-bubble pad toggles the list on a channel or DM conversation; tapping a row sends via sendComposedMessage() (channel -> that channel, DM conversation -> DM to that contact); all-empty shows an alert. Other screens keep the channel-picker action.
- Post-test fixes: tap intercept moved ahead of the vy<18 go-home rule that ate row taps and left the overlay stuck; any keyboard key closes the overlay in handleKeyboardInput (keys never reach ChannelScreen::handleInput on Pro/Max); setViewChannelIdx resets overlay state like the path overlay.
40 MHz extreme-battery test builds: meck_max_standalone_40mhz, meck_audio_standalone_40mhz, meck_4g_standalone_40mhz. CPU idle/boost/low-power all 40 MHz; MECK_40MHZ_TEST gates the modem (forced off, config kept), GPS (rail off, page hidden), browser/OTA, alarms, audio features and notification MP3s; gated tiles dither and go inert; MESH_DEBUG=1. The Pro envs deliberately re-run the March 80->40 TCA8418 config (noted in env comments).
T-Deck Pro v1.1 HYN touch (meck_audio_ble_hyn, Meck v1.12.6.HYN): opt-in MECK_PRO_HYN_TOUCH switches touch to the vendored Hynitron driver, auto-detecting CST3530/CST328 at 0x1A. Seven driver guards widened; XL9555 callbacks stay Max-only; explicit config SDA13/SCL14/RST38/INT12; the manual GPIO38 reset pulse is suppressed (driver self-resets); readTouch and the 4G dialer dispatch widened. Link fix: ui-new/*.c added to the env src filter (the cst66xx/cst3xx/cst226se providers were not compiled). Confirmed working by an affected v1.1 (CST3530) user.
Pro audio builds: the full-width Phone tile is no longer rendered (no modem; its tap was already inert).
Home UI
- Widen the tile-grid home screen (e1300190) from Max-only to all T-Deck Pro
builds: guard flips from LilyGo_TDeck_Pro_Max to LilyGo_TDeck_Pro at five
sites in UITask.cpp (HomeIcons include, tile-flag set/reset, page-dot
offset, tile render block) and at the touch hit-test in main.cpp. Max
defines both flags, so Max behaviour is unchanged -- verified by diffing
both files preprocessed with the Max define set.
Call Log (HAS_4G_MODEM)
- New CALL_LOG subview; app menu is now Dial / SMS Inbox / Call Log.
- SMSStore gains a fixed-size CallLogRecord store in /sms/calllog.dat
(timestamp, duration, type, seen, phone), capped at 32 entries with the
oldest trimmed on append: appendCallLog, loadCallLog (newest first),
deleteCallLogEntry (rewrite via tmp + rename) and markMissedSeen
(in-place flag update). Same SD idioms as the SMS store.
- Logs incoming and outgoing calls with duration, plus missed, busy and
no-answer attempts. Calls arriving with no caller ID are logged and shown
as "Unknown" rather than dropped.
- List mirrors inbox conventions: contact name via SMSContacts, detail line
with type, duration and local date/time. Enter dials, D deletes, Q backs
out. Menu row carries an unseen-missed [n] badge matching SMS Inbox.
Lock screen (HAS_4G_MODEM)
- Show outstanding unread SMS and unseen missed calls at height()-12 with
setTextSize(1). Counts derive from the inbox read flags and call log seen
flags, so they persist across re-locks and reboots and clear only when the
conversation or the Call Log is opened. State loads at boot in
setSDReady().
Phone screen fixes
- An incoming call now drops the lock screen. Keyboard and touch input are
both blocked while locked, which left the ringing screen unable to answer
or reject. The device stays unlocked afterwards.
- gotoSMSScreen() no longer calls activate() mid-call, which was resetting
the view to the app menu and wiping the ringing screen.
- Widen the app menu footer right margin so "Ent:Open" is not clipped.
Applied the same memw barrier + identical comment to twatchs3plusboard.cpp, hardening the Plus against the same latent race it was previously surviving only by luck
T-Watch:
Disabled rx boosted gain & removed meck rx duty cycle flag for S3 and S3 Plus.
Bumped charge rate on S3 to 150MA.
Added watch custom incoming path/hops view in channel message view mode.
Added diagnostic pmubtn build to S3 plus to iron out source of power drain in S3 firmware.
All boards:
ChannelScreen.h -- delivered DMs now render Delivered in the prefix position, exactly where Sending x/N and Failed sit, and the drawn tick is gone entirely (the branch, the reserved gap and the drawDeliveredTick helper -- zero references remain). For what it's worth, the photo vindicates the change: those stray pixels below the line next to "2s" are the tick misplaced, because the GFX fonts position from the baseline rather than the glyph top, so the hand-drawn glyph landed too low. Text is the robust answer.
UITask.cpp -- the toast persistence is a scheduling starvation, and the status pushes made it visible. Dismissal works by scheduling a render at _alert_expiry; but after every frame the e-ink throttle applies an 800ms floor (minNext, UITask.cpp:2856-2860), and the ~644ms partial-refresh block inside endFrame() shifts millis() before the clamp runs. So any render landing inside the alert window -- which a delivered/attempt push now triggers via forceRefresh() -- redraws the overlay and then gets its expiry wake clamped past the expiry. Your user's photo is precisely that frame: tick and toast on screen together, with the clearing render deferred. The fix is a single insertion after the shared clamp: if an alert is still active and lapses before the next scheduled render, wake at expiry instead. One bounded extra render per toast, both display branches covered, and the toast now clears on time no matter what the retry engine pushes mid-window.
Tracked DM sends now retry like the app: 3 flood attempts with no path
set, or 5 with a path (4 direct, then path reset and a final flood).
Each attempt waits its own hop-aware est_timeout. Delivered resolves on
the recipient's ack for any attempt and draws a tick after the message
prefix; Failed shows once all attempts are exhausted.
Status is session-only: SD message format unchanged at v4. Wired for
keyboard, touch, CardKB and T-Watch compose plus channel share. BLE app
sends and the voice envelope path are untouched.
The SettingsScreen SUB_CHANNELS sub-screen drives every per-channel
action from physical-keyboard hotkeys (Ent: scope, N: notif, T: tone,
X/Hold: delete), so on the touch-only watches the channel list rendered
but nothing was actionable. Replace it on MECK_TWATCH builds with a
standalone WatchChannelConfigScreen following the WatchAlarmScreen /
WatchNotesScreen conventions. Desktop builds are unchanged.
LIST shows each channel with its region scope tag and notification
state (tap to select, tap again or button to open, paged like Notes).
DETAIL offers: Region -- opens the watch keyboard via a new TWKB_SCOPE
purpose, pre-filled with the current scope, committed through the same
setChannel + saveChannels path as desktop (empty submission clears);
Clear region -- reverts to the device default directly; Notify --
tap-cycles All -> Mentions -> Off with savePrefs, matching the desktop
N-key order. Delete is button-driven rather than touch: a short press
(PWR on the S3, boot button on the Plus) arms it, a second press
within 3s confirms. Channel 0 remains protected. The tone picker is
deliberately absent -- the watch has no audio path.
Settings hands off via a _wantsWatchChannels flag polled by
UITask::loop (mirrors the _needsTextVKB handshake); Back returns to
Settings via gotoSettingsScreen. Keyboard results route through the
existing consumeSendRequest purpose dispatch.
Note: deleteChannel replicates SettingsScreen's compaction verbatim,
including its existing quirk of not shifting channel_notif[] -- kept
faithful rather than silently diverging; fix both together if desired.
Files: WatchChannelConfigScreen.h (new, both variant dirs),
TWatchComposeScreens.h (both variant dirs, TWKB_SCOPE),
SettingsScreen.h, UITask.h, UITask.cpp, main.cpp