emdrtool-v2

5 Mar 2026
EMDR

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

  1. Client plugs in the tapper device, opens the web app, clicks “Connect Device”
  2. A 4-digit PIN is generated and displayed
  3. Therapist enters the PIN on their device to join the session
  4. Therapist adjusts speed (0.2-5.0 Hz), intensity (1-3), and enable/disable
  5. Settings are relayed via WebSocket to the client browser, which sends serial commands to the ESP32-S3
  6. 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

  • 48a4be4 migrated to npm to get past chrome serial bug that prevents seamless ota usb upd (2026-03-05)
  • ff18e3a lots of bug fixes and working web update need to test on cloud (2026-03-04)
  • 18b461d working 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%