@leumas/touch-pc
Devices engine — turn a phone/tablet into a LAN remote for the host PC (mouse/keyboard input over a WebSocket) and drive multi-display setups (kiosk scenes on each monitor). Input synthesis comes...
@leumas/touch-pc
The Devices engine. Two hardware surfaces in one package:
- Remote control — turn a phone/tablet into a LAN remote for the host PC. A device pairs with a
rotating PIN, gets a session token, then streams mouse/keyboard frames over a WebSocket; the host applies them via an input engine (robotjs, or a noop fallback when native input isn't available). Remote layouts (touchpad, keyboard, controller, joystick, gyro, hybrid) are served to the device.
- Multi-display — register the host's monitors and throw full-screen kiosk scenes onto them.
Scenes are saved JSON layouts; the actual browser launch is delegated to an injectable launcher.
Absorbs (DRY-consolidated from): tools/leumas-touch-pc (input engine + WS protocol + PIN pairing / sessions + remotes registry + LAN security) and tools/infinity-displays (multi-display kiosk scenes / slideshows).
LAN-only — gate hard. This engine synthesizes real host input and controls displays. The router runs alanGuardfirst and unconditionally rejects non-private callers, and the WS bridge must only be attached to a LAN-bound listener.robotjsis lazy-loaded, so on hosts without it the engine degrades to a safenoopinstead of crashing.
Usage (in leumas-api)
import { createTouchPcRouter, createPinPairing, createSessionStore } from '@leumas/touch-pc';
import { createTouchPcWsServer } from '@leumas/touch-pc/ws';
import { requireAuth } from '@leumas/auth';
const pinPairing = createPinPairing(); // rotating 6-digit PIN
const sessionStore = createSessionStore(); // in-memory device sessions
app.use('/api/devices', createTouchPcRouter({ pinPairing, sessionStore, requireAuth }));
// Attach the input WebSocket to the SAME http server. LAN-only.
createTouchPcWsServer({ server, pinPairing, sessionStore, inputEngine: 'auto' });
console.log('Pairing PIN:', pinPairing.getPin());
Routes (/api/devices)
| Method | Path | Purpose |
|---|---|---|
| GET | /config | client bootstrap { wsPath, pairingRequired, remotes } |
| POST | /pair | { pin, deviceName } → { sessionToken, sessionId } |
| GET | /sessions | paired devices + pairing status |
| GET | /remotes | available remote layouts |
| GET/POST | /displays | list / register monitors |
| POST | /displays/:id/scene | assign + launch a scene on a display |
| GET/POST | /scenes | list / upsert scenes |
| DELETE | /scenes/:id | remove a scene |
The input engine
createInputEngine({ inputEngine }) returns one object implementing moveMouse / moveMouseAbs / mouseDown / mouseUp / click / scroll / keyTap / keyDown / keyUp / typeText.
'auto'(default) → tryrobotjs, elsenoop.'robotjs'→ requirerobotjs(lazy);noopif it can't load.'noop'→ swallow everything (safe default for headless/CI).
The WS protocol
Client frames are JSON { type, payload }; type ∈ { auth, mouse.move, mouse.down, mouse.up, mouse.click, mouse.scroll, key.tap, key.down, key.up, text.type, system.ping }. The server rate-limits (token bucket, 120/s), requires an auth frame with a valid session token when pairing is on, then applies each frame to the input engine. system.ping → system.pong.
The display launcher contract
launcher.launch({ display, sceneId }) → { ok, launched, ... }
Default is a noop launcher. In a host app (Electron / a kiosk agent) inject one that spawns a full-screen browser at the scene URL positioned on display.bounds.