Requirements
A browser with WebHID support ChromeEdge and a Tiller device.
Installation
No build step required. Import directly as an ES module:
import { Tiller } from './src/index.js';
Quick Start
const tiller = new Tiller();
tiller
.on('connect', () => console.log('ready'))
.on('tap', () => tiller.toggleLed())
.on('scroll', (delta) => window.scrollBy(0, delta));
// Auto-connects if device was previously paired — no user gesture needed
tiller.autoConnect();
// Keep a button as fallback for first-time pairing
document.getElementById('connect-btn').onclick = () => tiller.connect();
Connection
There are two ways to connect:
| Method | User gesture? | Description |
|---|---|---|
autoConnect() |
No | Silently connects if the device has been paired before. Also listens for the device to be plugged in after page load. Emits connect when ready. |
connect() |
Yes | Uses a previously paired device if available; otherwise shows the browser device picker. Emits connect when ready. |
The connect event fires in all three cases: autoConnect() finds a paired device on page load, a paired device is plugged in after page load, or connect() completes. Register on('connect', ...) once and it handles all three.
First-time pairing always requires a user gesture — the browser will not show a device picker without one. Call
connect() from a button click handler the first time, then autoConnect() handles every subsequent visit automatically.
Demos
Stopwatch
Tap to start/stop, double tap to lap, long press to reset. LED reflects running state.
Color Mixer
Scroll to sweep through hues. LED matches live. Tap to save colors to a palette.
Reaction Game
Double tap to start. React to the LED color as fast as possible. Track your best times.
Constructor Options
const tiller = new Tiller({
scrollSensitivity: 1, // multiplier applied to scroll delta (default: 1)
doubleTapThreshold: 300, // ms between taps to count as double tap (default: 300)
longPressThreshold: 500, // ms held down to trigger long press (default: 500)
});
Events
All events are registered with .on(event, handler), which returns the instance for chaining.
| Event | Handler | Description |
|---|---|---|
tap |
() => void |
Device tapped once |
doubletap |
() => void |
Device tapped twice in quick succession |
longpress |
() => void |
Device held down past the long press threshold |
press |
() => void |
Device pressed down (raw) |
release |
() => void |
Device released (raw) |
scroll |
(delta: number) => void |
Device rotated — delta is positive or negative |
connect |
() => void |
Device connected or reconnected |
disconnect |
() => void |
Device disconnected |
Tap vs double tap: A double tap suppresses the two individual tap events — only
Long press vs tap: Releasing after a long press only fires
doubletap fires.Long press vs tap: Releasing after a long press only fires
release —
tap is suppressed.
LED Control
// Set color (r, g, b — each 0–255) and immediately turn the LED on
await tiller.setLedColor(255, 0, 0); // red
await tiller.setLedColor(0, 255, 0); // green
await tiller.setLedColor(0, 0, 255); // blue
// Turn on with the last set color
await tiller.setLedOn();
// Turn off
await tiller.setLedOff();
// Toggle
await tiller.toggleLed();
Getters
tiller.isConnected // boolean — whether a device is currently connected
tiller.ledOn // boolean — current LED state
tiller.ledColor // { r, g, b } — last set color
Full Example
import { Tiller } from './src/index.js';
const tiller = new Tiller({ scrollSensitivity: 10 });
tiller
.on('connect', () => {
document.getElementById('connect-screen').style.display = 'none';
document.getElementById('app-screen').style.display = 'block';
})
.on('disconnect', () => console.log('Tiller disconnected'))
.on('tap', () => tiller.toggleLed())
.on('doubletap', () => tiller.setLedColor(255, 0, 0))
.on('longpress', () => tiller.setLedColor(0, 0, 255))
.on('scroll', (delta) => window.scrollBy(0, delta));
// Auto-connects on page load if previously paired
tiller.autoConnect();
// Fallback button for first-time pairing
document.getElementById('connect-btn').onclick = () => tiller.connect();