An ESP32-S3 Rust firmware that turns a round AMOLED smartwatch into a wearable agent terminal: remote Claude-agent sessions over MQTT (session list + live vt100 terminal on your wrist), voice input through a Whisper service, Tailscale connectivity, and OTA updates.
VibeWatch is a Rust firmware for the Waveshare ESP32-S3-Touch-AMOLED-2.06 watch (1.32" round AMOLED, capacitive touch, speaker + microphone) that puts your coding agents on your wrist:
- Remote agent sessions — connects to the companion vibetty bridge over MQTT and shows every running session; pick one and watch/drive its terminal live from the watch.
- Voice input (ASR) — hold-to-speak on the session screen; mic audio is streamed as WAV to a Whisper-compatible service you configure, and the recognized text is submitted to the session (with an inline editor for corrections).
- Tailscale — joins your tailnet via the microlink component, so the watch can reach brokers and services living inside it (see Configuring Tailscale).
- BLE provisioning + OTA — configure WiFi / broker / ASR from a web page over Web Bluetooth; update firmware from the browser or straight from GitHub releases.
- Clock face — large time display with battery indicator; the watch idles here between sessions.
- Session list — subscribes to presence for all of your vibetty sessions at once, one row per session with a live status dot (amber = agent working, green = idle); the list refreshes as sessions come and go.
- Live terminal (TUI) — vt100 terminal emulation with incremental dirty-rect rendering; a bottom action bar gives quick controls (Speak / Accept / Next / Yolo / Del / Esc), and right-swipe returns to the session list.
- ASR (voice input) — recognition runs against an HTTP Whisper service (set
asr_configinsetup.html:uri/api_key/model); recognized text opens an inline editor before submission. - Web provisioning — one
setup.htmlpage configures WiFi networks, MQTT broker, ASR service, and Tailscale over Web Bluetooth; stored in NVS. - Dual-partition OTA — the new image is written to the inactive OTA slot and the watch reboots into it. Two update sources: browser upload (HTTP PUT), or download-latest directly from GitHub releases.
- Time sync — timezone from IP + HTTP-Date fast check, SNTP only when the clock is actually off (needed for TLS, including the DERP relay).
The watch boots into the clock face. Tap to open the main menu with two entries — Remote and Setting; back gestures return up one level everywhere.
Remote mode connects to your MQTT broker and lists every vibetty session it announces. Select a session to open its live terminal; use the action bar for quick commands, or the ASR flow to speak input. Exiting a session returns to the session list, and the settings entry inside the remote UI opens the same options as the local one.
Options: OTA Update, Sync Time, Enable BLE, Tailscale, Reboot, Power Off. OTA Update enters OTA mode in-process: it connects WiFi, starts an HTTP server for browser upload, and offers a download-latest button (with a progress bar) to fetch the newest firmware from GitHub releases.
The device keeps a list of WiFi credentials rather than a single network. On boot it scans the surroundings and connects to the first network in the list that is currently in range — the list order is the priority order.
- Up to 8 credentials are stored in NVS (
MAX_WIFI_CREDS). - Every mode shares the same list and the same priority logic, so an over-the-air update also connects from wherever you are.
Configuration (WiFi networks, MQTT broker URL, ASR service, Tailscale key, background GIF, audio prompt) lives in NVS and is written through a single web page — assets/setup.html — over Web Bluetooth (BLE), no cable, no app:
- On the watch: Setting → Enable BLE. It advertises as
Watchand shows a hint screen. - Open
setup.htmlin a Web-Bluetooth-capable browser (Chrome / Edge) and click Connect to VibeWatch. - Fill in your WiFi networks, MQTT broker URL, and ASR service, then Save Changes; RESET is written automatically so the config applies on reboot. Uploads for the audio prompt (WAV) and watch background (GIF) go through their own cards.
You can re-run this any time settings change.
The watch joins your tailnet with an auth key provisioned over BLE. Everything below happens on the device — no USB or build flags required.
In the Tailscale admin console, generate an auth key (tskey-auth-...). A one-off key is enough: after the first registration the watch caches its own node keys, and new keys can be bound the same way at any time.
In setup.html, scroll to the Tailscale card, paste the auth key, and tap Bind Tailscale.
Binding wipes any previous tailnet registration on the watch, stores the key, and switches the screen to the Tailscale page: your VPN IP and the list of nodes in the tailnet (online nodes highlighted). The page refreshes once per second; back button or right swipe exits (a Shutting down box shows while the tunnel is torn down).
Setting → Tailscale opens the same node-list page using the key stored in NVS. If no key has been provisioned yet, the page says so and returns.
If the MQTT broker URL points at a tailnet address (100.64.0.0/10), the watch brings the tunnel up by itself before connecting: it joins the tailnet, moves its DERP mailbox to the broker's home region, kicks the WireGuard handshake, and only then starts MQTT. Time is (re-)synced first because the DERP relay speaks TLS.
CONFIG_ML_DERP_REGION in sdkconfig.defaults (default 3, Singapore) selects the watch's own relay region. Set it to the region your other devices home on — relayed packets only reach peers connected to the same region until a direct path is punched (the watch probes for one automatically).
Waveshare ESP32-S3-Touch-AMOLED-2.06: ESP32-S3 + PSRAM, 1.32" round AMOLED (410×502) with capacitive touch, ES8311 speaker + ES7210 microphone (I2S).
Built on Rust + ESP-IDF, target xtensa-esp32s3-espidf. Common commands:
./build.sh factory # merged image (bootloader + partition table + app): vibewatch_factory.bin
./build.sh ota # OTA image (app only, for OTA upload / download-latest): vibewatch_ota.bin
./build.sh all # bothThe OTA partition layout is symmetric (ota_0 / ota_1, each 4 MB). The factory image includes the bootloader + partition table for first-time flashing; the bare OTA image is app-only for over-the-air updates.
First build downloads the microlink component from its git repository and compiles ESP-IDF; subsequent builds are incremental.