A bridge application that streams 6DOF spatial input device data to apps over the network. Zero-config discovery via mDNS and W3C Web of Things, with WebTransport (HTTP/3 QUIC) for low-latency streaming and WebSocket fallback.
Download the latest release for your platform:
| Platform | Download |
|---|---|
| macOS (Apple Silicon) | SatMouse.app |
| Linux (x64) | satmouse-linux-x64.tar.gz |
| Windows (x64) | satmouse-win32-x64.tar.gz |
Or install via npm:
npx @kelnishi/satmouseMove SatMouse.app to /Applications or ~/Applications before launching (running from Downloads will fail due to App Translocation). Double-click the app — a 🛰 icon appears in the menu bar. No dock icon, no terminal needed.
A bundled Safari Web Extension bypasses Safari's mixed-content and Local Network Access restrictions. Enable it in Safari → Settings → Extensions after first launch.
Extract the tarball and run SatMouse.cmd. A SatMouse icon appears in the notification area (system tray). Right-click for options: Open Web Client, Refresh Devices, Quit.
Extract the tarball and run the installer:
tar xzf satmouse-linux-x64.tar.gz
bash install.shThis installs a systemd user service that auto-starts on login. Manage with:
systemctl --user start satmouse # start
systemctl --user stop satmouse # stop
systemctl --user restart satmouse # restart (rescan devices)
journalctl --user -u satmouse -f # view logsLinux requires libspnav for SpaceMouse support (apt install libspnav-dev or equivalent, with spacenavd running).
npm install @kelnishi/satmouse-clientimport { SatMouseConnection } from "@kelnishi/satmouse-client";
import { InputManager } from "@kelnishi/satmouse-client/utils";
const connection = new SatMouseConnection();
const manager = new InputManager();
manager.addConnection(connection);
manager.onSpatialData((data) => {
console.log(data.translation, data.rotation);
});
await connection.connect();Or with React:
import { SatMouseProvider, useSpatialData } from "@kelnishi/satmouse-client/react";
function App() {
return (
<SatMouseProvider>
<Scene />
</SatMouseProvider>
);
}
function Scene() {
const data = useSpatialData();
// data.translation.x/y/z, data.rotation.x/y/z
}With the bridge running, open http://localhost:18945/client/ — a Three.js demo with a 6DOF-controlled cube.
- Launch — SatMouse detects connected spatial input devices via platform-specific plugins
- Broadcast — Advertises
_wot._tcpvia mDNS with a WoT Thing Description - Connect — Clients fetch
/td.json, pick WebTransport or WebSocket - Stream — 6DOF translation + rotation data flows at device rate (~60-120 Hz)
Clients can also connect via the satmouse:// URL scheme:
satmouse://connect?host=192.168.1.42— connect to a specific bridgesatmouse://launch— launch the app (or open the download page if not installed)
| Plugin | Devices | macOS | Windows | Linux |
|---|---|---|---|---|
| SpaceMouse | SpaceNavigator, SpaceMouse Pro/Wireless/Compact/Enterprise, SpacePilot | 3DconnexionClient.framework | 3DxWare SDK | libspnav |
| SpaceFox | SpaceFox, SpaceFox Wireless | 3DconnexionClient.framework | 3DxWare SDK | libspnav |
| Orbion | Orbion rotary dial | 3DconnexionClient.framework | 3DxWare SDK | libspnav |
| CadMouse | CadMouse Pro/Compact (buttons only) | 3DconnexionClient.framework | 3DxWare SDK | libspnav |
| HID | Space Mushroom, Xbox/PlayStation controllers, any USB HID | node-hid | node-hid | node-hid |
SatMouse has a plugin architecture so hardware vendors and community contributors can add support for new devices. Each plugin implements the DevicePlugin interface:
import { DevicePlugin, type DeviceInfo, type SpatialData } from "./devices/types.js";
export class MyDevicePlugin extends DevicePlugin {
readonly id = "my-device";
readonly name = "My 6DOF Device";
readonly supportedPlatforms: NodeJS.Platform[] = ["darwin", "win32", "linux"];
async isAvailable(): Promise<boolean> {
// Return true if the device SDK/driver is installed on this machine
}
async connect(): Promise<void> {
// Open the device and start emitting events:
// this.emit("spatialData", { translation: {x,y,z}, rotation: {x,y,z}, timestamp })
// this.emit("buttonEvent", { button: 0, pressed: true, timestamp })
// this.emit("deviceConnected", deviceInfo)
// this.emit("deviceDisconnected", deviceInfo)
}
disconnect(): void {
// Release device resources
}
getDevices(): DeviceInfo[] {
// Return currently connected devices
}
}Then register it in src/main.ts:
deviceManager.registerPlugin(new MyDevicePlugin());src/devices/plugins/my-device/
index.ts # Plugin class (implements DevicePlugin)
For devices that use a shared native SDK (like 3Dconnexion devices), you can also create a shared driver under src/devices/drivers/ — see src/devices/drivers/connexion/ for an example.
For USB HID devices, you don't need to write a plugin — add a mapping profile to the existing HID plugin instead:
import { HIDPlugin, type HIDDeviceMapping } from "./devices/plugins/hid/index.js";
const myMapping: HIDDeviceMapping = {
name: "My Device",
vendorId: 0x1234,
productId: 0x5678,
axes: [
{ sourceAxis: 0, target: "tx" },
{ sourceAxis: 1, target: "ty" },
{ sourceAxis: 2, target: "tz" },
{ sourceAxis: 3, target: "rx", invert: true },
{ sourceAxis: 4, target: "ry", deadZone: 0.05 },
{ sourceAxis: 5, target: "rz", scale: 2.0 },
],
buttons: [
{ sourceButton: 0, targetButton: 0 },
{ sourceButton: 1, targetButton: 1 },
],
};
const hid = new HIDPlugin([myMapping]);
deviceManager.registerPlugin(hid);- Fork the repo
- Add your plugin under
src/devices/plugins/<name>/ - Register it in
src/main.ts - Add your device to the compatibility table in this README
- Open a PR
| Client | Type | Integration | Status |
|---|---|---|---|
| Kelcite | 3D modeling web app | @kelnishi/satmouse-client/react |
Integrated |
| Reference Client | Three.js demo | Built into SatMouse | Included |
If your app integrates SatMouse, submit a PR adding a row to the table above. Include:
- Link to your app
- Brief description
- Which SDK module you use (or "custom" if using the WebSocket/WebTransport protocol directly)
@kelnishi/satmouse-client — four tree-shakeable modules:
| Module | Import | Purpose |
|---|---|---|
| core | @kelnishi/satmouse-client |
SatMouseConnection, discovery (fetchThingDescription, resolveEndpoints), binary decode, launchSatMouse(). Zero dependencies. |
| utils | @kelnishi/satmouse-client/utils |
InputManager — unified device service with per-device axis routing (flip, scale, remap), dead zone, dominant mode, multi-device merge, settings persistence. |
| react | @kelnishi/satmouse-client/react |
<SatMouseProvider>, useSpatialData(), useRawSpatialData(), useButtonEvent(), <ConnectionStatus>, <SettingsPanel>, <DeviceInfo>, <DebugPanel> |
| elements | @kelnishi/satmouse-client/elements |
Web Components: <satmouse-status>, <satmouse-devices>, <satmouse-debug>. Shadow DOM, works in any framework. |
import { SatMouseConnection } from "@kelnishi/satmouse-client";
const connection = new SatMouseConnection({
// All optional — defaults to localhost:18945
tdUrl: "http://localhost:18945/td.json",
transports: ["webtransport", "websocket"],
maxRetries: 3,
});
connection.on("spatialData", (data) => { /* SpatialData */ });
connection.on("buttonEvent", (data) => { /* ButtonEvent */ });
connection.on("stateChange", (state, protocol) => { /* "connected" | "disconnected" | ... */ });
connection.on("deviceStatus", (event, device) => { /* "connected" | "disconnected" */ });
await connection.connect();import { InputManager } from "@kelnishi/satmouse-client/utils";
const manager = new InputManager({
translateScale: 0.001,
rotateScale: 0.001,
deadZone: 0,
dominant: false,
lockPosition: false,
lockRotation: false,
// Defaults by device class (spacemouse, gamepad, dial, joystick, 6dof, other)
deviceClasses: {
spacemouse: { rotateScale: 0.0005 },
gamepad: {
routes: [
{ source: "tx", target: "tx" },
{ source: "tz", target: "tz" },
{ source: "rx", target: "rx" },
{ source: "rz", target: "rz" },
{ source: "ty", target: "ty" },
{ source: "ry", target: "ty", flip: true },
],
deadZone: 0.05,
},
},
// Overrides by device ID or vendor pattern
devices: {
"hid-54c-*": { translateScale: 0.002 },
},
});
manager.addConnection(connection);
manager.onSpatialData((data) => { /* processed, merged, transformed */ });
manager.onButtonEvent((event) => { /* button press/release */ });
// Per-device config at runtime
manager.updateDeviceConfig("cnx-c635", { translateScale: 0.0005 });Config resolution per device: exact ID → ID pattern → device class → device axes metadata → global defaults.
<script type="module">
import { SatMouseConnection } from "@kelnishi/satmouse-client";
import { InputManager } from "@kelnishi/satmouse-client/utils";
import { registerSatMouse } from "@kelnishi/satmouse-client/elements";
const connection = new SatMouseConnection();
const manager = new InputManager();
manager.addConnection(connection);
registerSatMouse(manager);
await connection.connect();
</script>
<satmouse-status></satmouse-status>
<satmouse-devices></satmouse-devices>
<satmouse-debug></satmouse-debug>Any app that speaks WebSocket or WebTransport can connect to SatMouse directly — the client SDK is optional but provides typed APIs, auto-discovery, transforms, and framework integration out of the box.
# Install dependencies
npm install
# Generate dev TLS certs (required for WebTransport)
npm run generate-certs
# Run in development mode
npm run dev
# Build client bundle
npm run build:client| Endpoint | Protocol | Purpose |
|---|---|---|
http://localhost:18945/td.json |
HTTP | WoT Thing Description |
https://localhost:18947/td.json |
HTTPS | WoT Thing Description (for HTTPS clients) |
http://localhost:18945/client/ |
HTTP | Reference web client |
ws://localhost:18945/spatial |
WebSocket | Spatial data stream (fallback) |
https://localhost:18946 |
WebTransport | Spatial data stream (primary) |
http://localhost:18945/api/device |
HTTP | Connected device info |
- WoT Thing Description — W3C Web of Things TD
- AsyncAPI — AsyncAPI 3.0 event protocol
- JSON Schemas — Data payload schemas
- Wire Protocol — Binary and JSON formats
- Discovery — mDNS + WoT handshake flow
MIT
