Firmware for the LilyGo T-Deck Pro: e-ink notepad, SSH terminal, SD-card file workflow, WireGuard support, and BLE/GPS/4G/Meshtastic-radio features.
IN DEVELOPMENT! Many features may break or change!
- Default mode is a keyboard-driven notepad rendered on the e-ink panel. Files can be saved to the SD card.
sshswitches to terminal mode (WiFi first, then VPN fallback when configured).- Files live on SD root and can be edited/saved on-device or transferred with SCP mirror sync (
upload/download). bttoggles BLE HID peripheral mode (keyboard + touch trackpad).
Requires PlatformIO (pio) and uv.
If pio is not already available:
uv tool install --force platformio
uv tool update-shell
export PATH="$(uv tool dir --bin):$PATH"
hash -rInstall Python deps used by helper scripts:
uv syncAlternative setup helper:
source scripts/setup-pio.shpio run -t upload(Optional serial output)
pio device monitorCreate /CONFIG on a FAT32 SD card (format below), insert card, and reboot.
Type in notepad mode. Single-tap MIC to open command mode. Use h for command help.
For debug automation and camera capture flow, see Development at the bottom.
/CONFIG is section-based. Section lines start with # and include one of: wifi, ssh, vpn, bt, time, msh.
Example:
# wifi
home_ssid
home_password
office_guest_open
phone_hotspot
hotspot_password
# ssh
ssh_ip_address/host
ssh_port (usually 222)
user
password
fallback_ip
# vpn
device_private_key_base64
server_public_key_base64
preshared_key_base64
device_vpn_ip
endpoint_host_or_ip
port (usually 51820)
DNS
# bt
s-term
# time
PST8PDT,M3.2.0,M11.1.0
# msh
LongFast
default
Notes:
# wifi: lines are SSID/password pairs. If password is blank (or section ends right after SSID), that AP is treated as open.# ssh: host, port, user, password, optional VPN-only host override.# vpn: private key, server pubkey, PSK, local VPN IP, endpoint, port, optional DNS.# bt: optional device name.- Bluetooth always starts off at boot. Runtime control is the
btcommand (toggle only). - Legacy
enable/disableand passkey lines in# btare ignored. - If
# timeis missing, timezone defaults toUTC0. # msh: optional Meshtastic channel config (line1=channel name,line2=key spec). Key spec supportsdefault,none, decimal index (1= default public key),hex:<...>, orbase64:<...>.
Type on the keyboard. Text wraps to the e-ink display.
Shift: sticky uppercase (one letter)Sym: one-shot symbol/number layer- Touch tap: directional arrows (tap away from center for up/down/left/right)
Run ssh from command mode to switch to terminal mode. Device connects WiFi, tries SSH directly, and falls back through WireGuard when configured and needed. Run np to return to notepad.
Alt: acts as Ctrl (Alt + Spacesends Esc)- Touch tap: sends terminal arrow keys
Bluetooth is runtime-toggleable from command mode:
bt: toggle Bluetooth HID mode on/offbs: scan nearby BLE devices
When BT mode is on:
- The display switches to a blank trackpad screen.
- Touch input sends relative mouse movement.
- Tap near center sends left click; directional-area taps send arrow keys.
- Physical keyboard keys are sent as BLE HID keyboard input.
Altworks asCtrl(same behavior as terminal mode).- Single-tap
MICopens command mode; runbtagain to shut down the BT radio.
Meshtastic/Lora radio power is runtime-toggleable from command mode. When powered on, firmware uses Meshtastic-compatible packet format/encryption for the configured channel (default: public LongFast/default key).
msh: toggle radio power on/offmss: run a quick radio + mesh status scanmss tx <text>: send a broadcast Meshtastic text packetmss tx !<node> <text>: send a direct Meshtastic text packet (request ACK)
Single-tap MIC from any mode to open command mode (bottom half of screen).
Touch arrows in command mode browse command history. In picker mode (edit/ws): Up/Down moves, Left/Right pages.
| Command | Description |
|---|---|
l / ls |
List files on SD card |
e / edit [file] |
Edit a file. With no filename, opens interactive picker (W/S move, A/D page, Enter open). |
w / save [file] |
Save notepad to current file (or provided filename) |
daily |
Open today’s file as YYYY-MM-DD.md (local timezone) |
r / rm <file> |
Delete a file |
u / upload |
Mirror SD root to ~/tdeck on SSH host (overwrite + delete extras on host) |
d / download |
Mirror ~/tdeck to SD root (overwrite + delete extras on SD) |
p / paste |
Paste notepad to SSH |
ssh |
Switch to terminal mode and connect if needed |
np |
Return to notepad mode |
dc |
Disconnect SSH |
ws |
Scan WiFi and open selector. Enter connects to selected AP (* known, o open, l locked). Open unknown APs are auto-added to runtime config and appended to /CONFIG. |
wfi |
Toggle WiFi on/off |
mds |
4G modem status scan (non-blocking) |
mdm |
Toggle 4G modem power (non-blocking) |
bs |
Scan nearby BLE devices (non-blocking) |
bt |
Toggle Bluetooth HID mode on/off |
gps |
Toggle GPS on/off |
gs / gpss |
GPS detail scan (non-blocking) |
mss |
Meshtastic radio/mesh status scan (non-blocking) |
mss tx <text> |
Send Meshtastic broadcast text |
mss tx !<node> <text> |
Send direct Meshtastic text (ACK requested) |
msh |
Toggle Meshtastic radio power |
date |
Show local date/time and sync source |
s / status |
Show WiFi/4G/SSH/BT/GPS/MSH/battery/clock status |
h / help |
Show help |
<name> or <name>.x |
Run shortcut script from /<name>.x |
GPS is off by default. When GPS has valid UTC + fix, firmware auto-syncs system clock. NTP sync (over network/VPN) updates the same clock.
Shortcut scripts are plain text files on SD root with extension .x.
Edit with edit <name>.x. Run with <name> (or <name>.x) in command mode.
Name resolution:
deployruns/deploy.xdeploy.xruns/deploy.x- Valid chars:
a-z,A-Z,0-9,.,_,- - Only one shortcut runs at a time
Parsing/execution:
- Blank lines and lines starting with
#are ignored - Max
24executable lines per file - Max line length
159chars - Steps execute in order and stop on first failure
- Progress appears in command result area (
Run <name> <i>/<n>, thenShortcut done: <name>)
Supported steps:
upload/udownload/dwait upload/wait downloadwait <ms>remote <command>/exec <command>cmd <command>- Any bare command-mode command (
daily,status, etc.)
Remote-step notes:
- If SSH is disconnected, shortcut runtime will try to connect first
- SSH connect wait window is up to ~45s per remote step
- Non-zero remote exit code fails the shortcut (
Remote failed (<code>)) - If no status arrives before timeout, step fails as
Remote timeout
Example deploy.x:
upload
wait upload
remote mkdir -p "$HOME/app/config"
remote cp -f "$HOME/tdeck/"*.json "$HOME/app/config/"
Example daily-sync.x:
daily
save
upload
wait upload
np
Entry point is src/main.cpp (setup/loop and global state).
Major firmware components are split into flat module headers in src/:
src/network_module.hpp(WiFi, SSH, VPN connectivity)src/modem_module.hpp(A7682E modem power + LTE scan helpers)src/meshtastic_module.hpp(SX126x power control + Meshtastic-compatible RX/TX/status)src/bluetooth_module.hpp(BLE HID peripheral: keyboard + mouse, pairing/bonding, runtime toggle)src/screen_module.hpp(display rendering/task logic)src/keyboard_module.hpp(keyboard input + mode handlers)src/cli_module.hpp(command parsing, SCP helpers, poweroff flow)
Shared config/constants:
src/firmware/pins.hsrc/firmware/layout.hsrc/firmware/keyboard_map.hsrc/firmware/network_config.h
Runtime uses two FreeRTOS cores:
- Core 0: e-ink display rendering
- Core 1: keyboard polling, WiFi/SSH/VPN/BLE, file I/O
SPI bus is shared between e-ink and SD via cooperative sd_busy / display_idle flags.
- Production:
pio run -t upload - Debug automation:
pio run -e debug -t upload
debug maps to T-Deck-Pro-debug and enables TDECK_AGENT_DEBUG=1 (serial automation protocol).
Production keeps it disabled.
pio run -e debug -t upload
uv run scripts/agent_smoke.py --boot-wait 2agent_smoke.py:
- Clears notepad
- Types a marker
- Forces render + waits
- Captures camera frame (default
http://10.0.44.199:4747/) - Verifies serial
text_len - Verifies capture is not black/invalid
- Prints
PASS/FAIL
Notes:
- No OCR yet; still visually confirm marker in the image.
- Output artifact path is printed.
- Custom marker should avoid
0(currentTEXTemulation limitation).
If a local webcam source is wrong or black:
uv run scripts/probe_cameras.py --max-index 5Pick an index with opened=1, frame=1, and non-trivial mean/std, then pass --camera-source "<idx>".
- Flash debug firmware:
pio run -e debug -t upload - Check serial channel:
uv run scripts/tdeck_agent.py --boot-wait 2 "PING" "STATE" - Drive scenario commands over serial
- Capture artifacts:
- image (default feed):
uv run scripts/capture_webcam.py --image artifacts/<name>.jpg - image (custom source):
uv run scripts/capture_webcam.py --source "<url-or-idx>" --image artifacts/<name>.jpg - video (custom source):
uv run scripts/capture_webcam.py --source "<url-or-idx>" --video artifacts/<name>.mp4 --duration 8
- image (default feed):
- Evaluate:
- no
AGENT ERR - expected
AGENT OK STATE ...transitions - expected screen content in image/video
- no
- Patch and repeat
Send one command per line with @ prefix.
@PING@HELP@STATE@KEY <row> <col_rev>@PRESS <token> [count]@TEXT <text-with-escapes>@CMD <device-command-mode-command>@WAIT <ms>@RENDER@BOOTOFF
Escapes in TEXT: \\n, \\r, \\t, \\\\, \\s.
PRESS tokens:
- Special:
MIC,ALT,SYM,LSHIFT,RSHIFT,SPACE,ENTER,BACKSPACE - Single-char physical key tokens: letters and symbol-key positions (
q,w,1,?,-)
Examples:
uv run scripts/tdeck_agent.py "PRESS MIC" "WAIT 500" "STATE"
uv run scripts/tdeck_agent.py "CMD ssh" "WAIT 300" "STATE"
uv run scripts/tdeck_agent.py "CMD np" "WAIT 300" "STATE"AGENT ERR: scenario failure, fix command or firmware behavior.- Camera opens but frame is black: verify stream URL or probe camera indices and increase
--warmup. TEXTfails due to active modifiers: use explicitKEY/PRESSfirst.- Need release validation: flash production env and rerun manual/non-agent checks.