WebSocket client for JMRI with real-time updates and full throttle control.
- ✅ WebSocket-based - Real-time bidirectional communication
- ✅ Event-driven - Subscribe to power changes, throttle updates, and more
- ✅ Full Throttle Control - Speed (0.0-1.0), direction, and functions (F0-F28)
- ✅ Browser & Node.js - Works in browsers and Node.js with auto-detection
- ✅ Mock Mode - Test and demo without JMRI hardware
- ✅ Auto-reconnection - Exponential backoff with jitter
- ✅ Heartbeat monitoring - Automatic ping/pong keepalive
- ✅ TypeScript - Full type definitions included
- ✅ Dual module support - ESM and CommonJS
- ✅ Multi-connection - Target specific hardware connections by prefix when multiple are configured
- ✅ Extensible - Subclass
JmriClientto add support for additional JMRI object types
npm install jmri-clientRequirements: Node.js 22+ · JMRI 5.0 or later
getSystemConnections()and per-connection power/throttle prefix support require JMRI 5.15.7+. All other features work with any JMRI 5.x release.
import { JmriClient, PowerState } from 'jmri-client';
// Create client
const client = new JmriClient({
host: 'jmri.local',
port: 12080
});
// Listen for events
client.on('connected', () => console.log('Connected!'));
client.on('power:changed', (state) => {
const stateStr = state === PowerState.ON ? 'ON' :
state === PowerState.OFF ? 'OFF' : 'UNKNOWN';
console.log('Power:', stateStr);
});
// Control power
await client.powerOn();
// Acquire and control a throttle
const throttleId = await client.acquireThrottle({ address: 3 });
await client.setThrottleSpeed(throttleId, 0.5); // 50% speed
await client.setThrottleDirection(throttleId, true); // Forward
await client.setThrottleFunction(throttleId, 'F0', true); // Headlight on
// Clean up
await client.releaseThrottle(throttleId);
await client.disconnect();- API Reference - Complete API documentation
- Browser Usage - Using jmri-client in web browsers
- Examples - Common usage patterns
- Mock Mode - Testing without hardware
- Migration Guide - Upgrading from v2.x
- Troubleshooting - Common issues and solutions
Subscribe to real-time updates from JMRI:
client.on('connected', () => { });
client.on('disconnected', (reason) => { });
client.on('power:changed', (state) => { });
client.on('throttle:updated', (id, data) => { });Full control of DCC locomotives:
const throttle = await client.acquireThrottle({ address: 3 });
await client.setThrottleSpeed(throttle, 0.5);
await client.setThrottleDirection(throttle, true);
await client.setThrottleFunction(throttle, 'F0', true);
await client.releaseThrottle(throttle);Automatically reconnects with exponential backoff:
client.on('reconnecting', (attempt, delay) => {
console.log(`Reconnecting attempt ${attempt} in ${delay}ms`);
});When JMRI has multiple hardware connections configured, use getSystemConnections() to discover their prefixes and pass them to power and throttle commands:
const connections = await client.getSystemConnections();
// [{ name: 'LocoNet', prefix: 'L' }, { name: 'DCC++', prefix: 'D' }]
await client.powerOn('L'); // LocoNet only
const throttle = await client.acquireThrottle({ address: 3, prefix: 'L' });When no prefix is supplied the command routes to JMRI's default connection manager, which is the correct behaviour for single-connection layouts.
JmriClient exposes its wsClient as protected, so you can subclass it to add support for JMRI object types not yet built in (e.g., sensors, lights, routes, blocks):
import { JmriClient } from 'jmri-client';
import type { PartialClientOptions } from 'jmri-client';
class MyExtendedClient extends JmriClient {
constructor(options?: PartialClientOptions) {
super(options);
// this.wsClient is available — use it to send/receive JMRI JSON messages
this.wsClient.on('update', (message: any) => {
if (message.type === 'sensor') {
this.emit('sensor:changed', message.data.name, message.data.state);
}
});
}
async listSensors() {
const response = await this.wsClient.request({ type: 'sensor', method: 'list' });
return Array.isArray(response?.data)
? response.data.map((r: any) => r.data ?? r)
: [];
}
}WebSocketClient is also exported for direct use if you need it. See its send(), request(), and on('update', ...) API for low-level messaging.
Unit Tests (no hardware required):
npm testMock Mode Demo (no hardware required):
npm run demo:mockFunctional Test (requires JMRI hardware):
npm run functionalSee Mock Mode Guide and Testing Guide for complete instructions.
Issues and pull requests welcome! Please see the GitHub repository.
MIT
