emdrtool-v2
EMDR Tapper Tool v2
A real-time bilateral stimulation system for EMDR therapy. A therapist controls tapper speed, intensity, and on/off remotely via a web interface, while the client’s browser communicates with a USB-connected ESP32-S3 device that drives vibration motors and LEDs.
Architecture
Therapist Browser ──WebSocket──▶ Server ──WebSocket──▶ Client Browser ──USB CDC──▶ ESP32-S3
(controls) (relay) (serial bridge) (motors/LEDs)
Three components:
| Component | Tech | Purpose |
|---|---|---|
| Web app | SvelteKit 5 + Socket.IO | UI for client, therapist, and admin |
| Server | Node.js + Socket.IO | Session management, WebSocket relay |
| Firmware | ESP-IDF + TinyUSB | PWM control of motors and LEDs via USB CDC |
How It Works
- Client plugs in the tapper device, opens the web app, clicks “Connect Device”
- A 4-digit PIN is generated and displayed
- Therapist enters the PIN on their device to join the session
- Therapist adjusts speed (0.2-5.0 Hz), intensity (1-3), and enable/disable
- Settings are relayed via WebSocket to the client browser, which sends serial commands to the ESP32-S3
- The ESP32-S3 alternates left/right vibration motors and LEDs at the configured frequency
Project Structure
emdrtool-v2/
├── web/ # SvelteKit web application + server
│ ├── src/
│ │ ├── lib/
│ │ │ ├── serial.ts # Web Serial API (TapperSerial class)
│ │ │ ├── socket.ts # Socket.IO client singleton
│ │ │ ├── esptool.ts # Browser-based firmware flashing
│ │ │ ├── pin.ts # PIN generation & validation
│ │ │ ├── types.ts # TapperSettings, Session types
│ │ │ ├── stores.ts # Svelte reactive stores
│ │ │ ├── Controls.svelte # Speed/intensity/enable controls
│ │ │ └── TapperAnimation.svelte
│ │ └── routes/
│ │ ├── +page.svelte # Landing (role selection)
│ │ ├── client/+page.svelte # Client: connect device, show PIN
│ │ ├── therapist/+page.svelte # Therapist: enter PIN, controls
│ │ └── admin/+page.svelte # Admin: sessions, firmware mgmt
│ ├── server.js # Production server (HTTP + Socket.IO)
│ ├── Dockerfile # Container build
│ └── package.json
├── firmware/ # ESP-IDF project (ESP32-S3)
│ ├── main/
│ │ ├── main.c # Entry point, task creation, serial loop
│ │ ├── usb_cdc.c/.h # TinyUSB CDC init, read/write
│ │ ├── ledc_pwm.c/.h # PWM channels for motors & LEDs
│ │ ├── serial_parser.c/.h # Command parser (S/I/E protocol)
│ │ └── CMakeLists.txt
│ ├── CMakeLists.txt
│ └── sdkconfig.defaults # USB descriptor: "EMDR Tapper"
└── deploy/
└── cloudbuild.yaml # Google Cloud Run deployment
Serial Protocol
Commands sent from browser to ESP32-S3 over USB CDC at 115200 baud:
S<speed>,I<intensity>,E<enabled>\n
| Field | Type | Range | Example |
|---|---|---|---|
| Speed | float | 0.2 - 5.0 Hz | S2.50 |
| Intensity | int | 1, 2, 3 | I2 |
| Enabled | int | 0 or 1 | E1 |
Response: OK\n or ERR:<message>\n
Example: S2.50,I2,E1\n — 2.5 Hz, medium intensity, tappers on
Hardware
MCU: ESP32-S3-WROOM-1 (N4)
| Pin | Function |
|---|---|
| GPIO 21 | Left LED |
| GPIO 13 | Right LED |
| GPIO 12 | Left vibration motor |
| GPIO 14 | Right vibration motor |
| GPIO 19/20 | USB D-/D+ (TinyUSB CDC) |
PWM: 100 Hz, 13-bit resolution (8192 max duty)
| Intensity | LED duty | Motor duty |
|---|---|---|
| 1 (low) | 300 | 1500 |
| 2 (medium) | 1000 | 3000 |
| 3 (high) | 8192 | 8192 |
USB Descriptor: Shows as “EMDR Tapper” (manufacturer “EMDR Tool”) in Chrome’s serial port picker.
Development
Web App
cd web
npm install
npm run dev # Dev server at http://localhost:5173
npm test # Run tests
npm run build # Production build
npm start # Production server at http://localhost:3000
Firmware
Requires ESP-IDF v5.5+.
cd firmware
source ~/esp/esp-idf/export.sh
idf.py set-target esp32s3
idf.py build
idf.py -p /dev/cu.usbserial-0001 flash # Via UART adapter
Flashing over native USB: The ESP32-S3 must be put into download mode first (hold BOOT/GPIO 0 low, press RESET, release). After flashing, press RESET or power-cycle to boot.
UART adapter: Connect TX→RX, RX→TX, GND→GND, and optionally DTR→EN, RTS→GPIO0 for auto-reset.
Environment Variables
| Variable | Default | Description |
|---|---|---|
PORT |
3000 | Server port (Cloud Run uses 8080) |
ADMIN_PASSWORD |
admin | Admin panel password |
Socket.IO Events
Client → Server
client:register— Create session, get PIN- `d
[README truncated]
Recent Activity
48a4be4migrated to npm to get past chrome serial bug that prevents seamless ota usb upd (2026-03-05)ff18e3alots of bug fixes and working web update need to test on cloud (2026-03-04)18b461dworking firmware and website with new usb comms - no more wifi (2026-03-04)
Languages
- C: 79%
- JavaScript: 7%
- CMake: 6%
- Assembly: 3%
- Makefile: 1%
- Python: 1%