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
S3 fix (CHG_VOL_4V2 → CHG_VOL_4V35 in TWatchS3Board.cpp
Notes screen:
WatchNotesScreen.h → both variants/lilygo_twatch_s3/ and variants/lilygo_twatch_s3_plus/ (byte-identical copies, like TWatchComposeScreens.h).
TWatchComposeScreens.h → both variant dirs. Adds TWKB_NOTE to the Purpose enum and a setInitialText() (clamped at the 133-char MAXLEN) for prefill editing.
UITask.h → watch_notes_screen member, isOnWatchNotesScreen(), getWatchNotesScreen(), all MECK_TWATCH-gated, mirroring the alarm-screen pattern exactly.
UITask.cpp → include, construction in begin() (after the LittleFS mount, like the alarm), gotoNotesScreen() now routes to the watch screen on MECK_TWATCH (T-Deck/T5S3 behaviour untouched — the SD NotesScreen path below is unreached on watches), the TWKB_NOTE result case (save via applyComposedNote, alert on error, return to the notes screen), and the loop poll that opens the keyboard — prefilling via setInitialText(getEditText()) when editPending().
main.cpp → include plus the tap-dispatch block, same shape as the alarm's.
S3 (MECK_PMU_BUTTON) — short PWR click on Contacts opens the path editor for the selected contact; anywhere else it's still enter/select (KEY_ENTER); first click on a dark screen just wakes it. The existing comment is now actually true.
S3 Plus (LILYGO_TWATCH_S3_PLUS) — boot button restored: click on Contacts opens the path editor; elsewhere wakes / KEY_NEXT as before.
CPUPowerManager.h — S3 variant, CPU_FREQ_BOOST capped to 80 MHz.
main.cpp — locked-loop throttle extended to the watches via the two MECK_TWATCH guard additions.
Throttle the ESP32 lazy contact save from a 5s debounce to a 12h
interval (CONTACTS_SAVE_INTERVAL). A new _nextContactSaveDue anchors
the next flush 12h out on the first dirty pass and only writes once
reached, clearing after each save so the next dirty event re-anchors.
Cuts the recurring ~95KB LittleFS rewrites on busy meshes. nRF52/STM32
blocking-save path unchanged.
Flush contacts (blocking) in the AUTO_SHUTDOWN_MILLIVOLTS path before
shutdown() so low-battery power-off doesn't drop up to 12h of contact
learning. Manual power-off left untouched.
Gate the SHUTDOWN home page out on the watch (MECK_TWATCH): removed
from the HomePage enum with render/input/isOnShutdownPage guarded.
The AXP2101 PWR-button long-press is the shutdown path there.
Files: MyMesh.h, MyMesh.cpp, UITask.cpp
Both watches undercount steps. Two divergences from LilyGo's own configuration
for this hardware, applied to both boards:
1. Watermark. LilyGoLib calls setStepCounterWatermark(1) and SensorLib's
BMA423_StepDetector example passes step_counter_wm = 1. Meck's raw-I2C enable
never touched those bits, leaving whatever the config-file blob defaults to.
The watermark is BMA423_STEP_CNTR_WM_MSK (0x03FF), spanning feature_config
[0x36] as LSB and the low two bits of [0x37] as MSB; it does not collide with
the enable bit, which is bit 4 of [0x37]. Both are now written in one
read-modify-write. The bit arithmetic was checked exhaustively against
bma423_step_counter_set_watermark() + feature_enable() over all 65536 starting
states of cfg[0x36]/cfg[0x37]: identical.
2. ODR. SensorLib's step detector example runs the accelerometer at 100 Hz. Meck
used 50 Hz. Everything else already matched: NORMAL, FS_2G, OSR2_AVG2,
CIC_AVG_MODE. Reverting the single 100.0f literal to 50.0f A/Bs this against
the watermark change.
Ruled out: the axis remap is BOTTOM_LAYER_TOP_RIGHT_CORNER on both, identical to
LilyGoLib, and the enable bit itself was already register-for-register what
bma423_feature_enable(BMA423_STEP_CNTR, TRUE) writes.
Not attempted: the 25-value pedometer parameter block at feature_config[0x04..0x35],
reachable via bma423_stepcounter_set_parameter() but with no reference values.
The alarm was self-dismissing after ~10-15 seconds. Cause: AUTO_OFF_MILLIS is
15000, so the display turned off shortly after the alarm fired; UITask.cpp:2769
raise-to-wake then saw !_display->isOn() && board.tiltFired() -- the buzzing motor
on the wrist is exactly what trips the BMA423 tilt detector -- and called
setCurrScreen(lock_screen). The 'navigating away counts as a dismiss' rule added
with the alarm screen then killed it. Any tap did the same.
While ringing, UITask::loop now holds the alarm screen current and pushes
_auto_off forward each iteration. The display therefore stays on, raise-to-wake
is never reached, and checkDisplayOn() passes KEY_ENTER through to handleInput()
rather than swallowing the first press as a wake.
Dismissal is now the PWR key only (KEY_ENTER). Taps are swallowed. The
WATCH_ALARM_RINGING_MS timeout (5 minutes) is unchanged and remains the backstop.
Ringing footer updated from 'Tap to dismiss' to 'Press PWR to dismiss'.
The ordering inside loop() makes this safe: the button dispatch and
curr->handleInput() both run before the alarm hold, so a dismiss takes effect on
the same iteration and the hold is not re-applied. The 90s per-slot fire cooldown
prevents an immediate re-fire within the alarm's own minute.
Fixes a pre-existing bug on both watches. SensorBMA423::begin() calls
bma423_write_config_file(), which re-flashes the BMA423 feature engine and zeroes
the step register on every boot. _stepBaseline and _lastStepDay were RAM-only, so
a reboot mid-day silently reset the day's steps to zero. Persisting the baseline
would not have helped, since the raw count it is measured against restarts at 0.
Replaces the baseline model with accumulate-plus-reset-detection:
delta = (raw >= lastRaw) ? raw - lastRaw : raw
todaySteps += delta
lastRaw is seeded from the chip on the first read rather than from the file,
which is correct whether or not the counter survived the reset.
Persists lastStepDay, todaySteps and history[6] to /steps.dat on the LittleFS
'maps' partition, already mounted before UITask::begin(). Written on day rollover
and at most every 5 minutes otherwise: a >=6s PWR hold is a hardware power cut by
the AXP2101, so there is no clean-shutdown hook to flush from. A rollover across
several days shifts in the completed day then pads with zeros for the days the
watch was off; a clock corrected backwards re-anchors without disturbing history.
Adds StepsHistoryScreen: today plus the previous six days as labelled bars, today
in the tile blue. Reached by long-pressing the steps screen, which on the S3 the
PWR key's short press also delivers (both arrive as KEY_ENTER). Any input returns
to the steps screen.
Gated on MECK_TWATCH, so the Plus gets both the fix and the screen.
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.