touchWASD turns an M5Stack AtomS3 (or any ESP32-S3) into a USB HID keyboard controlled by a circular touch overlay on your phone or tablet. Open a browser, touch a direction on the circle, and the corresponding WASD or arrow key is pressed on your computer — no software to install on the target machine.
[ Phone/Tablet ] --WiFi--> [ AtomS3 ] --USB--> [ Computer ]
(touch circle (keys pressed (sees standard
on web page) via WebSocket) USB keyboard)
Perfect for gaming, presentations, KVM control, or any situation where you want a wireless WASD/arrow input.
- Zero software on the target PC — shows up as a standard USB keyboard
- Web-based 8-slice touch circle — intuitive directional input from any phone or tablet
- WASD mode (default) — sends
WASDto the host PC - Arrow key mode — sends ↑ ← ↓ → instead
- Diagonal support — NE/SE/SW/NW slices send two simultaneous keys (e.g.,
W+D) - Slide-typing — drag your finger across slices; keys release from the old slice and press on the new one
- 4 sizes — Small, Medium, Large, Full, persisted in browser storage
- 7 positions — place the circle at center, top, bottom, or any corner
- Auto-relocating gear — the settings button moves out of the way when placed at top-right
- Multi-client — multiple tablets can connect simultaneously; key presses are reference-counted
- WiFiManager — configure WiFi via captive portal on first boot
- Customizable mDNS hostname — default
touchwasd.local, configurable in the captive portal - OTA updates — upload firmware wirelessly via
espota.pyor browser (/update) - AtomS3 display — shows IP, hostname, mode, and client count on the built-in 128×128 screen
| Component | Required | Notes |
|---|---|---|
| ESP32-S3 with native USB | Yes | M5Stack AtomS3 recommended; any ESP32-S3 works |
| USB-C cable | Yes | Connects ESP32 to target computer |
| Phone / Tablet | Yes | Any device with a web browser |
# AtomS3
arduino-cli compile --fqbn esp32:esp32:m5stack_atoms3 \
--board-options "PartitionScheme=default_8MB,USBMode=default,CDCOnBoot=default" \
/home/pi/touchwasd
# Hold Reset button 2-3s on AtomS3 for download mode (LED turns green)
arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:m5stack_atoms3 \
--board-options "PartitionScheme=default_8MB,USBMode=default,CDCOnBoot=default" \
/home/pi/touchwasd- Add
https://espressif.github.io/arduino-esp32/package_esp32_index.jsonto Additional Boards Manager URLs - Install ESP32 board package and libraries: WiFiManager, M5GFX (AtomS3 only), WebSockets
- Select M5AtomS3 (or ESP32S3 Dev Module for generic boards)
- Set Tools → USB Mode → USB-OTG (TinyUSB)
- Set Tools → USB CDC On Boot → Disabled
- AtomS3 only: Tools → Partition Scheme → 8M with spiffs (3MB APP/1.5MB SPIFFS)
- Open
touchwasd.inoand upload
AtomS3 bootloader mode: With CDC ACM disabled, press and hold the small Reset button for 2–3 seconds. The LED turns green solid. Upload immediately after.
On first boot, the ESP32 starts an access point named touchWASD-Config. Connect to it with your phone — a captive portal opens. Select your WiFi network and enter the password.
Once connected, open a browser on any device on the same network:
http://touchwasd.local
A circular 8-slice touch overlay appears. Tap or drag on the circle to send keystrokes to the host computer.
Connect the ESP32 to the computer via its native USB port. It enumerates as a standard USB HID keyboard — no drivers needed.
The circle is divided into 8 slices (45° each), centered at 0° (straight up):
| Slice | Label | Keys Sent |
|---|---|---|
| North (top) | W ↑ | W |
| Northeast | W+D ↗ | W + D |
| East (right) | D → | D |
| Southeast | D+S ↘ | D + S |
| South (bottom) | S ↓ | S |
| Southwest | S+A ↙ | S + A |
| West (left) | A ← | A |
| Northwest | A+W ↖ | A + W |
Single tap: Press and release — sends the key(s) then releases immediately.
Hold: Sends and holds the key(s). Release your finger to release.
Slide: Drag across slices — held keys release and new ones press as you cross slice boundaries.
Tap the ⚙ cog button to open the settings panel. From here you can:
- Mode: toggle between WASD and Arrow keys (persisted on the device)
- Size: choose Small, Medium, Large, or Full (persisted in browser)
- Position: place the circle at center, top, bottom, or any corner (persisted in browser)
The cog button auto-relocates to avoid overlapping the circle.
Open http://touchwasd.local on multiple phones or tablets. All clients share the same key state — pressing W on one device and D on another simultaneously produces W+D. Reference counting ensures keys release only when every client has released the key.
Once the device is online and connected, upload firmware wirelessly:
arduino-cli compile --fqbn esp32:esp32:m5stack_atoms3 \
--board-options "PartitionScheme=default_8MB,USBMode=default,CDCOnBoot=default" \
/home/pi/touchwasd --output-dir /tmp/touchwasd-build \
&& python3 /home/pi/.arduino15/packages/esp32/hardware/esp32/3.3.8/tools/espota.py \
-i touchwasd.local -f /tmp/touchwasd-build/touchwasd.ino.bin -r -dOr use the web interface at http://touchwasd.local/update.
By default, OTA updates require no password — anyone on your network can upload firmware. To enable password protection:
- Open
touchwasd.inoand uncomment this line near the top:// #define OTA_PASS "your-password-here" - Change
"your-password-here"to your chosen password. - Recompile and upload via serial.
Password applies to both ArduinoOTA (espota.py -a "<password>") and the Web OTA interface (HTTP Basic Auth).
Hold the built-in button for 5 seconds:
- AtomS3: hold GPIO41 button (display shows "Resetting WiFi...")
- Generic: hold BOOT button (GPIO0)
WiFi credentials are erased and the device reboots into the touchWASD-Config captive portal.
AtomS3: esp32:esp32:m5stack_atoms3:PartitionScheme=default_8MB,USBMode=default,CDCOnBoot=default
Generic: esp32:esp32:esp32s3:USBMode=default,CDCOnBoot=default
- WiFiManager by tzapu
- WebSockets by Markus Sattler
- M5GFX by M5Stack (AtomS3 only)
Python 3 test suite at test/. Tests both core logic and wire protocol:
# Mock device tests (no hardware needed)
python3 -m pytest test/ -v
# Live device tests (requires AtomS3 on the network)
python3 -m pytest test/test_protocol.py --host touchwasd.local -vTests requiring USB HID state inspection (e.g., verifying a key is pressed) are skipped in live mode. Only protocol-level assertions run against the real device.
charToHID()lookup table validation against every ASCII character- Key state management: press/release lifecycle, 6-key limit, swap-removal
- Reference counting: multi-client, overflow cap at 255
- Mode mapping: WASD vs Arrow key translation
- HID report format: 8-byte report structure
MockTouchWASDDevice: simulated ESP32 with WS+HTTP servers;--hostflag connects to a real device instead- WebSocket handshake, key press/release, diagonal, release-all (
~) - Mode switch round-trip (
#MODE:wasd/#MODE:arrows) - Two-client reference counting over real WS connections
- Disconnect resets state, new client receives mode sync
- HTTP root page serving
MIT