Git-tracked home for the active LilyGO Watch Gen3 / T-Watch S3 XNODE firmware.
Workspace paths:
- Active project:
C:\GitHub\XNODE - Archived legacy generations:
C:\GitHub\XNODE\obsolete\backup
These XNODE screens show the LilyGO T-Watch S3 firmware and the current T-Watch Ultra build in daily use.
Hardware listing:
XNODE is built to work with the MKME X software stack. Add the companion software below to turn the watch into part of a larger offline tactical comms, mapping, and intelligence workflow.
- XTOC - Tactical Operations Center Software Suite - Offline command-center software for SITREPs, TASKs, CHECKIN/LOC, map overlays, zones, SATCOM, ATAK/KML/CoT workflows, and packet-based field coordination. XTOC directly interfaces with XNODE over the BLE bridge to move maps, markers, locations, alerts, and operational data back and forth.
- XCOM - Offline Radio Communication Suite - Offline-first radio and mapping toolkit with repeater maps, packet stations, callsign lookup, mesh/Reticulum support, and XTOC data import. XCOM also directly interfaces with XNODE over the BLE bridge so operators can send and receive watch-ready map, alert, location, and packet data.
- XCORE - Offline Tactical AI Analyst - Local Windows AI analyst for XTOC/XCOM operational data, including AO summaries, anomaly scans, 24-hour SITREPs, aircraft pattern checks, and structured packet drafting.
- XINTEL - Radio Intel Monitor + Transcription - Local radio-intelligence monitor that transcribes legally receivable audio, watches for keyword/rule hits, decodes ViperGram bursts, and pushes structured intel into the XTOC/XCOM workflow.
Working now:
- Builds for
t-watch2020-v3-s3. - Flashes to the LilyGO Watch Gen3 / ESP32-S3 target.
- Exposes the XNODE BLE bridge to XTOC and XCOM with
sync,location,meshtastic,basemap,mapOverlay,newsNotifications, andblecapabilities. - Adds a Manual SOS launcher tile that sends a clear XTOC
SITREPpacket over the watch's Meshtastic radio, with the roster Unit ID, destination Unit ID,P1,HELP, current lat/lon, and noteManual SOS. - Adds a CheckIn launcher tile that sends a clear XTOC
CHECKIN/LOCpacket over the watch's Meshtastic radio, with the roster Unit ID,OKstatus, current lat/lon, and timestamp. - Installs the active XTOC/XCOM tactical map raster as the watch basemap in SPIFFS and selects
offline from watch flashon the watch. - Replaces the active basemap cleanly with
clearBasemapplus a streamedmapTileupload, so stale seed or old tiles do not bleed into the current map. - Persists the installed basemap center, zoom, and projection zoom so the same map returns after reboot.
- Displays local position, shared location, Meshtastic position updates, and XTOC/XCOM overlay markers on the tactical map.
- Supports watch markers for team members, mesh nodes, SITREPs, CONTACTs, TASKs, CHECKINs, resource requests, assets, zones, missions, events, phase lines, Sentinel, and routes.
- Uses host-projected
mapX/mapYmarker placement when provided, with Web Mercator lon/lat projection as the fallback. - Keeps markers aligned while zooming and panning the installed watch-flash basemap.
- Persists synced overlay markers in
/spiffs/osmmap/overlays.jsonland reloads them when the map opens or the watch reboots. - Applies replacement overlay syncs transactionally: existing markers stay visible until the expected replacement batch arrives; an intentional zero-count replacement clears the cache.
- Stores XTOC/XCOM pushed news and alert items in the XNODE alerts app, with the watch-side
show pushed newstoggle. - Keeps the launcher functions active for messages, mesh, Tac Map, media player, Alert Summary, and watchface manager.
- Inactivity timeout returns the T-Watch S3 build to standby instead of leaving it awake indefinitely.
- T-Watch S3 standby uses the LilyGo
ext1touch wake path onBOARD_TOUCH_INT. - Display timeout settings use a real
15..300second range.300is five minutes, not a hidden never-sleep mode.
Known limits:
- Watch-flash mode is a single installed raster tile, not a multi-tile slippy engine.
- Zoom is image scaling around the installed tile center.
- Panning is constrained by the visible image bounds.
- The watch keeps up to 96 overlay markers; when full, the oldest marker slot is reused.
- The active watch basemap is one current image, not a stored library of selectable maps.
- SPIFFS is small, so host-side tooling must keep the installed raster compact.
- Manual SOS requires a configured watch Unit ID, a valid watch location, and a ready Meshtastic radio/channel before it can transmit.
- CheckIn requires the same watch Unit ID, valid watch location, and ready Meshtastic radio/channel.
Current verified firmware baseline:
t-watch-ultra: built, flashed, and post-upload watchdog reset verified on the T-Watch Ultra.t-watch2020-v3-s3: built successfully after the shared power/config changes to protect the other watch variant.- Latest power audit commit:
5ae38af Improve T-Watch Ultra battery life.
| Area | T-Watch Ultra | T-Watch S3 / Gen3 |
|---|---|---|
| Idle timeout | Uses the shared display activity timer and standby request path. | Uses the shared display activity timer and restored timeout-to-standby path. |
| Display standby | AMOLED brightness is set to zero and the panel is put into display.sleep(). |
Backlight/display is turned off through the S3/LilyGo path. |
| Touch wake | Touch/display rail stays powered because it is shared; touch interrupt can wake the watch. | Uses LilyGo-style ext1 wake on BOARD_TOUCH_INT. |
| GPS at boot | Off by default after one-time config migration. | Existing behavior preserved. |
| GPS while using map | Tac map can still auto-start GPS for the user-location marker. | Existing behavior preserved. |
| GPS in standby | Off unless an app explicitly blocks standby. GPS status no longer enables standby GPS. | Existing tracker/status behavior preserved. |
| WiFi at boot | Off by default after one-time config migration; dummy setup scan disabled. | Existing behavior preserved. |
| BLE at boot | Off by default after one-time config migration; BLE stack is lazy-initialized. | Existing auto-on behavior preserved unless config says otherwise. |
| LoRa / Meshtastic | Radio chip is put into sleep on standby; regulator rail is not cut yet to avoid a risky radio re-init path. | Existing behavior preserved. |
| CPU performance mode | Active tac map and normal awake watchface use performance mode; standby handoff, hibernate, and the GPS loop do not globally pin performance. | Shared change applies; S3 build verified. |
GPS:
- Removed the Ultra-only code that forced
/gpsctl.jsonautoon=trueat every boot. - Added
GPSCTL_CONFIG_VERSIONand a one-time Ultra migration that setsautoon=falseandenable_on_standby=false. - GPS BLDO1 is disabled at Ultra PMU boot instead of being left powered.
- The Ultra auto-off path explicitly shuts down the GPS UART and GPS regulator.
- The GPS loop no longer calls
powermgm_set_perf_mode(), so GPS reads do not pin the CPU in full-speed mode. - The GPS status page powers GPS only while active and no longer requests GPS to stay on in standby.
WiFi:
- Added
WIFICTL_CONFIG_VERSIONand a one-time Ultra migration that turns off WiFi auto-on, standby WiFi, web server, and FTP server. - Ultra no longer inserts the dummy
foo/barnetwork at setup, which prevented an unwanted boot-time WiFi scan. - WiFi can still be turned on from the UI/statusbar when needed.
BLE:
- Added
BLECTL_CONFIG_VERSIONand a one-time Ultra migration that turns off BLE auto-on, advertising, standby BLE, and disconnect-only standby blocking. - BLE advertising now only starts when BLE is actually on.
- BLE stack initialization is lazy, so the controller is not brought up on Ultra boot when BLE is off.
- Turning BLE on later still initializes the normal services, including Gadgetbridge, XNODE, battery/steps, and Meshtastic BLE.
Display and PMU rails:
- Ultra GPS BLDO1 and haptic BLDO2 start disabled at PMU boot.
- Ultra haptic expander enable starts low;
WATCH_POWER_DRV2605controls the rail and enable line together. - Ultra standby defensively cuts GPS and haptic power again before entering light sleep.
- Ultra display standby now calls
display.sleep()after setting brightness to zero, and wake callsdisplay.wakeup(). - The shared display/touch rail is intentionally not cut on Ultra because touch wake depends on it.
Apps and CPU mode:
- Tac map activation uses performance mode while the map is open so zoom, pan, and map buttons stay responsive; closing/hibernating the map returns normal mode.
- Watchface activation uses performance mode for normal awake use, but the standby handoff and hibernate path force normal mode so the idle sleep path is not pinned high.
- The GPS loop no longer calls
powermgm_set_perf_mode(), so background GPS reads do not pin the CPU in full-speed mode. - Tac map still auto-starts GPS when its
autostart gpsoption is enabled so the large GPS triangle can appear. - Tac map WiFi auto-start defaults off on Ultra after a one-time
OSMMAP_CONFIG_VERSIONmigration.
Display brightness:
DISPLAY_CONFIG_VERSIONwas bumped to2.- On Ultra only, old configs above half brightness are migrated once down to
DISPLAY_MAX_BRIGHTNESS / 2. - The user can still raise brightness after the migration; the firmware does not force it down every boot.
Normal idle:
- Screen fades after the configured display timeout, the AMOLED panel sleeps, and the watch enters light sleep if no subsystem blocks standby.
- GPS, WiFi, BLE advertising, and the unused haptic rail should not be running on a clean idle boot.
- The watch should wake by touch/power/PMU events without requiring a reboot.
Opening the tac map:
- GPS may power on if map
autostart gpsis enabled. - WiFi should not auto-start unless the map/user config explicitly enables it.
- The map uses performance mode while open and returns normal mode when closed.
Opening GPS status:
- GPS powers on so live debug fields can update.
- Leaving the page restores the previous GPS auto-on and standby settings.
- The page does not keep GPS alive through standby.
Turning BLE or WiFi on manually:
- BLE and WiFi still work from the normal UI/statusbar paths.
- Those radios will cost battery while enabled, especially BLE advertising and WiFi scanning/connection attempts.
- Meshtastic LoRa regulator power is not cut in standby. The SX1262 is put to sleep, but cutting the rail safely needs a full radio re-init path on wake.
- OTA/update still uses
powermgm_set_perf_mode()intentionally while flashing or updating. powermgm_set_lightsleep(false)users in OTA and battery calibration still need a paired release audit, as noted in the older S3 audit.- Any app that explicitly enables GPS-on-standby, WiFi-on-standby, BLE always-on, or long display timeout will reduce battery life by design.
Build Ultra:
pio run -e t-watch-ultraBuild S3 / Gen3 regression target:
pio run -e t-watch2020-v3-s3Flash Ultra from this repo:
pio run -e t-watch-ultra -t uploadAfter flashing, confirm the watch re-enumerates:
pio device listScope:
- Board/environment:
t-watch2020-v3-s3 - Problem reported: the watch stayed awake, the screen did not time out reliably, and the battery drained quickly.
Root cause found:
src/gui/gui.cpphad aLILYGO_WATCH_S3special case that skipped the normal timeout-to-standby request path entirely.src/hardware/touch.cppput the S3 touch controller into monitor mode for standby, but used a custom GPIO light-sleep wake path instead of the LilyGo S3ext1touch wake path.- The S3 touch path also read touch coordinates without first checking the touch interrupt state, which increased the chance of false activity and unnecessary polling.
- The saved display timeout still treated
300as a hidden "no timeout" value, so older settings could keep the watch awake forever even after the standby path was restored. - Activity resets relied too much on LVGL inactivity tracking, which did not consistently follow every wake source on the S3 build.
Fix applied:
- Re-enabled timeout-driven
POWERMGM_STANDBY_REQUESThandling for the S3 build insrc/gui/gui.cpp. - Changed S3 standby wake to match the LilyGo library path in
lib/twatchs3_core/src/LilyGoLib.cpp:esp_sleep_enable_ext1_wakeup(_BV(BOARD_TOUCH_INT), ESP_EXT1_WAKEUP_ALL_LOW). - Gated S3 touch reads on
watch.getTouched()before reading coordinates insrc/hardware/touch.cpp. - Added a firmware-side display activity timer that is reset by touch, button presses, wake requests, alarms, notifications, and explicit keep-awake flows.
- Changed timeout handling so the persisted user setting is always
15..300seconds. Temporary keep-awake behavior now uses the internalDISPLAY_NO_TIMEOUToverride instead of the old magic300value. - Added a legacy config migration so pre-fix
/display.jsonfiles that stored300as the old never-sleep value are converted once to15seconds on boot and then rewritten. - Fixed S3 standby wake handoff so a touch wake from light sleep becomes a normal
POWERMGM_WAKEUP_REQUESTand the display comes back without a reboot.
Files changed for this fix:
src/gui/gui.cppsrc/hardware/touch.cppsrc/hardware/display.cppsrc/hardware/display.hsrc/hardware/config/displayconfig.cppsrc/hardware/button.cppsrc/hardware/powermgm.cppsrc/gui/splashscreen.cppsrc/gui/quickbar.cppsrc/gui/mainbar/mainbar.cppsrc/gui/mainbar/setup_tile/display_settings/display_setting.cppsrc/gui/mainbar/setup_tile/watchface/watchface_manager_app.cppsrc/gui/mainbar/setup_tile/update/update.cppsrc/gui/mainbar/setup_tile/bluetooth_settings/bluetooth_message.cppsrc/gui/mainbar/setup_tile/bluetooth_settings/bluetooth_media.cppsrc/app/alarm_clock/alarm_in_progress.cppsrc/app/wifimon/wifimon_app_main.cppsrc/app/sailing/sailing_setup.cpp
Build verification:
- Confirmed with:
pio run -e t-watch2020-v3-s3Flash verification:
- Last confirmed upload after this fix:
pio run -e t-watch2020-v3-s3 -t upload --upload-port COM8Display timeout:
- User setting range:
15to300seconds in Display settings. 300means300 seconds/5 minutes.- There is no normal hidden never-sleep slider value anymore.
What happens when idle:
- The firmware starts fading the backlight during the last
brightness * 8 msbefore timeout. - At the default mid brightness that fade is about
1 second. - At max brightness that fade is about
2.0 seconds. - When the timeout expires, the watch requests standby, turns the display off, and then enters ESP32-S3 light sleep if no other subsystem blocks it.
What counts as activity:
- Touch press.
- Side / power button press.
- Wake requests from notifications, media updates, alarms, splash/update UI, and other explicit wake paths.
Wake methods:
- Touch interrupt on
BOARD_TOUCH_INTusing ESP32ext1wake, matching the LilyGo S3 library. - Power / side button.
- Motion wake paths already wired through the BMA callback flow.
- RTC alarm / silence wake paths.
- PMU / charger related interrupts.
- Bluetooth notification/media wake when those options are enabled.
Touch wake interaction:
- One touch should wake the watch from standby.
- After wake, the normal next touch interaction should be able to scroll or change screens without forcing a full reboot.
Temporary no-timeout cases:
- Internal app flows can still keep the display awake with
DISPLAY_NO_TIMEOUT. - Current users are OTA update, watchface manager, Wi-Fi monitor, and the sailing app's explicit "Always on display" toggle.
- Those are runtime overrides, not saved Display settings.
Follow-up risk still open:
powermgm_set_lightsleep(false)is called insrc/utils/http_ota/http_ota.cppandsrc/gui/mainbar/setup_tile/battery_settings/battery_calibration.cpp.- Those paths do not currently show a matching release call in the same flow, so light sleep can remain disabled until reboot after those operations.
- That does not explain the idle timeout bug on a clean boot, but it is another battery-life issue worth fixing next.
The active firmware now lives here in git:
boards/data/images/lib/src/support/platformio.ini
Legacy working copies and old generation snapshots were moved under:
C:\GitHub\XNODE\obsolete\backup
That archive is for reference and rollback only. New work should happen in C:\GitHub\XNODE.
The repo also vendors the required T-Watch S3 support libraries under:
support/twatch-s3-libdeps
That removes the last build dependency on C:\GitHub\lilygo.
The XNODE watch map path is:
- XTOC or XCOM fetches one raster tile for a chosen center and zoom.
- The host clears the active watch basemap state with
clearBasemap. - The host streams the new image over the XNODE bridge with
mapTileBegin/mapTile. - The active tile is written to:
/spiffs/osmmap/current.png
- The host sends
installBasemapwith center longitude, latitude, zoom, and projection zoom. - The watch persists that manifest and uses the installed tile in
offline from watch flash. - The watch requests overlay sync for the active basemap, then stores synced overlay markers in
/spiffs/osmmap/overlays.jsonl.
Behavior on the watch:
- zoom in/out scales the installed tile
- directional controls pan around the tile
- long press recenters to the stored map center
- markers are projected with Web Mercator math and stay in the right position as zoom changes
- host-projected marker pixels are used when the host generated the installed map image
- overlay markers persist across map close/open and watch reboot
+/-: zoom the installed image- directional inputs: pan the current view
- long press center/select: recenter the map
The minimum zoom is clamped so the tile still fills the display frame. The app should never shrink to a tiny image in the middle with no usable controls.
src/hardware/ble/xnode.cpp- accepts
clearBasemap,mapTileBegin,mapTile,installBasemap,syncState,overlayBatch,packetBatch,newsItem, and location commands - creates the watch basemap directory before writing
- streams PNG chunks into
/spiffs/osmmap/current.png - acknowledges overlay counts back to the host so XTOC/XCOM can verify sync progress
- accepts
src/app/osmmap/config/osmmap_config.cppsrc/app/osmmap/config/osmmap_config.h- persist installed basemap center, zoom, and projection zoom
src/app/osmmap/osmmap_app_main.cpp- resolves watch-flash mode to the installed tile
- scales one image across zoom levels
- applies pan offsets only in map mode
- keeps swipe inversion local to the map view
- stores and restores overlay markers from
/spiffs/osmmap/overlays.jsonl - handles transactional overlay replacement so partial syncs do not wipe visible markers
src/utils/osm_map/osm_map.cppsrc/utils/osm_map/osm_map.h- Web Mercator projection helpers for marker placement
Host-side install support lives in:
C:\GitHub\XTOC\xtoc-web\src\pages\XnodePage.tsxC:\GitHub\XTOC\xtoc-web\src\core\xnodeBridge.tsC:\GitHub\xcom\xcom\modules\shared\xnode\xnodeBridge.jsC:\GitHub\xcom\xcom\modules\xnode\xnode.js
These flows now support installing the active raster tile onto the watch using the existing XNODE install path. They also sync visible tactical map markers to the watch as overlay batches, including packet/check-in style markers and host-projected marker pixels for the currently installed basemap.
Current host behavior:
- clear the watch basemap and overlay cache before installing a replacement map
- stream the active map image to
/spiffs/osmmap/current.png - send the basemap manifest with center and projection metadata
- send watch SOS configuration, including the roster-backed
Watch Unit IDandSOS ToUnit ID - push overlay batches after the watch activates the basemap
- keep overlay markers persistent on the watch until a new complete replacement sync arrives
- push XTOC/XCOM news and alerts into the XNODE alerts app
Manual SOS is a watch-side emergency shortcut. It does not send the distress packet back to the BLE host. When the operator opens SOS on the watch and taps SEND SOS, XNODE builds one clear XTOC SITREP packet and passes it to the watch Meshtastic service for transmission on the active Meshtastic channel.
Packet contents:
- packet family:
SITREPv1 - source: the configured watch
Unit ID - destination: configured
SOS ToUnit ID, orU0for broadcast - priority:
P1 - status:
HELP - location: the watch's currently stored latitude/longitude
- note:
Manual SOS
Real-world uses:
- injured, lost, trapped, or separated operator who cannot stop to use a phone or tablet
- vehicle crew, shelter lead, search team, or marshal who needs a fast distress cue on the same mesh net the TOC is already monitoring
- bad-weather, low-light, gloved, or high-stress conditions where a two-tap watch flow is more reliable than opening a full field app
Setup for success:
- In
XTOC Teamor theXCOMimported roster, give every watch wearer a stableUnit ID. - In
XTOC -> XNODEorXCOM -> XNODE, connect the watch, chooseWatch Unit IDfrom the roster, chooseSOS To, and clickSave. - The watch stores this assignment in
/xnode.jsonand reloads it after reboot. To clear it, reconnect fromXTOC/XCOM, chooseUnassigned / clear saved watch ID, and clickSave. - Set the watch location from the XNODE page with
Set watch GPS + time,Share current GPS once, or the GPS relay before relying on Manual SOS. Future GPS-equipped watches can provide this directly. Host-set location is also stored so the last known lat/lon survives reboot. - On the watch, open the
meshapp and confirm the status isMesh readyon the expected Meshtastic channel. - Send a short test mesh message and confirm the TOC mesh station can receive and auto-import XTOC packet text.
Use in the field:
- Open the watch launcher.
- Tap
SOS. - Tap
SEND SOS. - Confirm the watch shows
SOS sent over mesh.
If it fails:
Set the watch Unit ID in XTOC/XCOM first.means the watch has not received a roster Unit ID.Set the watch location before sending SOS.means no valid lat/lon has been stored yet.Meshtastic not readymeans the onboard radio did not initialize or no usable channel is active.
The receiving TOC should leave mesh packet auto-decode enabled so the inbound SITREP lands in the normal packet store, timeline, triggers, and map workflows.
CheckIn is the routine one-button position report. It also transmits from the watch Meshtastic radio, not back through the BLE host.
Packet contents:
- packet family:
CHECKIN/LOCv1 - source: the configured watch
Unit ID - status:
OK - location: the watch's currently stored latitude/longitude
- timestamp: current watch time, rounded to packet minutes
Real-world uses:
- shift start, staging arrival, checkpoint arrival, shelter arrival, route departure, vehicle stop, or post-task accountability
- routine "I am here and OK" updates from operators who should not be distracted by a phone screen
- last-known-position breadcrumbs for TOC staff when a field team only has time for a single button press
Use CheckIn for routine accountability. Use SOS when the operator needs help, safety response, or urgent TOC attention.
Setup is the same as Manual SOS:
- Assign the watch wearer a stable roster
Unit ID. - In
XTOC -> XNODEorXCOM -> XNODE, connect the watch, chooseWatch Unit ID, and clickSave. - The watch keeps the saved Unit ID across reboot until
XTOC/XCOMexplicitly savesUnassigned / clear saved watch ID. - Set the watch location from the XNODE page or GPS relay before relying on CheckIn.
- On the watch, open the
meshapp and confirmMesh readyon the intended channel.
Use in the field:
- Open the watch launcher.
- Tap
CheckIn. - Tap
CHECK IN. - Confirm the watch shows
Check-in sent over mesh.
The receiving TOC should auto-decode mesh packets so the inbound CHECKIN/LOC updates the unit's latest position on the map and in the roster.
From C:\GitHub\XNODE:
pio run -e t-watch2020-v3-s3Check the active USB port first:
Get-CimInstance Win32_SerialPort | Select-Object DeviceID, Description, PNPDeviceIDThen flash:
pio run -e t-watch2020-v3-s3 -t upload --upload-port COM8Last confirmed watch upload in this workspace used COM8.
If the watch does not auto-reset into bootloader mode, put it into boot mode manually and rerun the upload command on the current port.
- Build and flash from
C:\GitHub\XNODE. - Open XTOC or XCOM and connect to the watch.
- Load and install a map tile.
- On the watch, open the map app and use
offline from watch flash. - Confirm:
- the same image stays loaded while zoom changes
- the map still fills the screen at maximum zoom-out
- markers remain visible and aligned
- markers survive closing/reopening the map and rebooting the watch
- a new packet/check-in sync updates markers without making existing markers vanish
- panning moves the viewed area without affecting the rest of the watch UI







