# @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...


The **Devices** engine. Two hardware surfaces in one package:

1. **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.
2. **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 a `lanGuard` first and unconditionally rejects non-private callers, and the WS bridge
> must only be attached to a LAN-bound listener. `robotjs` is lazy-loaded, so on hosts without it the
> engine degrades to a safe `noop` instead of crashing.

## Usage (in leumas-api)

```js
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) → try `robotjs`, else `noop`.
- `'robotjs'` → require `robotjs` (lazy); `noop` if 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`.


---
Source: shared/engines/hardware/touch-pc/README.md
Canonical: https://docs.leumas.tech/p/engines/touch-pc
