jmri-client 4.2.0-beta.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +3 -1
  2. package/dist/browser/jmri-client.js +88 -28
  3. package/dist/cjs/index.js +2442 -31
  4. package/dist/esm/index.js +2393 -17
  5. package/dist/types/client.d.ts +9 -1
  6. package/dist/types/index.d.ts +1 -1
  7. package/dist/types/managers/roster-manager.d.ts +9 -1
  8. package/dist/types/mocks/mock-data.d.ts +30 -6
  9. package/dist/types/mocks/mock-response-manager.d.ts +7 -2
  10. package/dist/types/types/jmri-messages.d.ts +22 -0
  11. package/docs/API.md +8 -0
  12. package/docs/BROWSER.md +4 -4
  13. package/docs/MIGRATION.md +30 -1
  14. package/docs/MOCK_MODE.md +15 -9
  15. package/package.json +17 -18
  16. package/dist/cjs/client.js +0 -366
  17. package/dist/cjs/core/connection-state-manager.js +0 -84
  18. package/dist/cjs/core/heartbeat-manager.js +0 -79
  19. package/dist/cjs/core/index.js +0 -25
  20. package/dist/cjs/core/message-queue.js +0 -59
  21. package/dist/cjs/core/reconnection-manager.js +0 -97
  22. package/dist/cjs/core/websocket-adapter.js +0 -135
  23. package/dist/cjs/core/websocket-client.js +0 -388
  24. package/dist/cjs/managers/index.js +0 -25
  25. package/dist/cjs/managers/light-manager.js +0 -111
  26. package/dist/cjs/managers/power-manager.js +0 -90
  27. package/dist/cjs/managers/roster-manager.js +0 -118
  28. package/dist/cjs/managers/system-connections-manager.js +0 -28
  29. package/dist/cjs/managers/throttle-manager.js +0 -233
  30. package/dist/cjs/managers/turnout-manager.js +0 -111
  31. package/dist/cjs/mocks/index.js +0 -12
  32. package/dist/cjs/mocks/mock-data.js +0 -237
  33. package/dist/cjs/mocks/mock-response-manager.js +0 -290
  34. package/dist/cjs/types/client-options.js +0 -66
  35. package/dist/cjs/types/events.js +0 -16
  36. package/dist/cjs/types/index.js +0 -23
  37. package/dist/cjs/types/jmri-messages.js +0 -95
  38. package/dist/cjs/types/throttle.js +0 -19
  39. package/dist/cjs/utils/exponential-backoff.js +0 -40
  40. package/dist/cjs/utils/index.js +0 -21
  41. package/dist/cjs/utils/message-id.js +0 -40
  42. package/dist/esm/client.js +0 -362
  43. package/dist/esm/core/connection-state-manager.js +0 -80
  44. package/dist/esm/core/heartbeat-manager.js +0 -75
  45. package/dist/esm/core/index.js +0 -9
  46. package/dist/esm/core/message-queue.js +0 -55
  47. package/dist/esm/core/reconnection-manager.js +0 -93
  48. package/dist/esm/core/websocket-adapter.js +0 -98
  49. package/dist/esm/core/websocket-client.js +0 -384
  50. package/dist/esm/managers/index.js +0 -9
  51. package/dist/esm/managers/light-manager.js +0 -107
  52. package/dist/esm/managers/power-manager.js +0 -86
  53. package/dist/esm/managers/roster-manager.js +0 -114
  54. package/dist/esm/managers/system-connections-manager.js +0 -24
  55. package/dist/esm/managers/throttle-manager.js +0 -229
  56. package/dist/esm/managers/turnout-manager.js +0 -107
  57. package/dist/esm/mocks/index.js +0 -6
  58. package/dist/esm/mocks/mock-data.js +0 -234
  59. package/dist/esm/mocks/mock-response-manager.js +0 -286
  60. package/dist/esm/types/client-options.js +0 -62
  61. package/dist/esm/types/events.js +0 -13
  62. package/dist/esm/types/index.js +0 -7
  63. package/dist/esm/types/jmri-messages.js +0 -89
  64. package/dist/esm/types/throttle.js +0 -15
  65. package/dist/esm/utils/exponential-backoff.js +0 -36
  66. package/dist/esm/utils/index.js +0 -5
  67. package/dist/esm/utils/message-id.js +0 -36
@@ -1,24 +0,0 @@
1
- /**
2
- * System connections manager — discovers available JMRI hardware connection prefixes
3
- */
4
- export class SystemConnectionsManager {
5
- constructor(client) {
6
- this.client = client;
7
- }
8
- /**
9
- * List all available JMRI system connections and their prefixes.
10
- * Use the returned prefix values with power and throttle commands to
11
- * target a specific hardware connection when multiple are configured.
12
- */
13
- async getSystemConnections() {
14
- const message = {
15
- type: 'systemConnections',
16
- method: 'list'
17
- };
18
- const response = await this.client.request(message);
19
- if (!response.data) {
20
- return [];
21
- }
22
- return Array.isArray(response.data) ? response.data : [response.data];
23
- }
24
- }
@@ -1,229 +0,0 @@
1
- /**
2
- * Throttle control manager
3
- */
4
- import { EventEmitter } from 'eventemitter3';
5
- import { isThrottleFunctionKey, isValidSpeed } from '../types/throttle.js';
6
- /**
7
- * Manages multiple throttles
8
- */
9
- export class ThrottleManager extends EventEmitter {
10
- constructor(client) {
11
- super();
12
- this.throttles = new Map();
13
- this.client = client;
14
- // Generate a unique client ID
15
- this.clientId = `jmri-client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
16
- // Listen for throttle updates
17
- this.client.on('update', (message) => {
18
- if (message.type === 'throttle') {
19
- this.handleThrottleUpdate(message);
20
- }
21
- });
22
- // Clean up throttles on disconnect
23
- this.client.on('disconnected', () => {
24
- this.handleDisconnect();
25
- });
26
- }
27
- /**
28
- * Acquire a throttle for a locomotive
29
- */
30
- async acquireThrottle(options) {
31
- // Generate a unique throttle name using client ID and address
32
- const throttleName = `${this.clientId}-${options.address}`;
33
- const message = {
34
- type: 'throttle',
35
- data: {
36
- name: throttleName,
37
- address: options.address,
38
- ...(options.prefix !== undefined && { prefix: options.prefix })
39
- }
40
- };
41
- const response = await this.client.request(message);
42
- // JMRI returns the throttle ID in the "throttle" field, or we can use our name
43
- const throttleId = response.data?.throttle || throttleName;
44
- if (!throttleId) {
45
- throw new Error('Failed to acquire throttle: no throttle ID returned');
46
- }
47
- // Initialize throttle state
48
- const state = {
49
- id: throttleId,
50
- address: options.address,
51
- speed: 0,
52
- forward: true,
53
- functions: new Map(),
54
- acquired: true
55
- };
56
- this.throttles.set(throttleId, state);
57
- this.emit('throttle:acquired', throttleId);
58
- return throttleId;
59
- }
60
- /**
61
- * Release a throttle
62
- */
63
- async releaseThrottle(throttleId) {
64
- const state = this.throttles.get(throttleId);
65
- if (!state) {
66
- throw new Error(`Throttle not found: ${throttleId}`);
67
- }
68
- const message = {
69
- type: 'throttle',
70
- data: {
71
- throttle: throttleId,
72
- release: null
73
- }
74
- };
75
- await this.client.request(message);
76
- this.throttles.delete(throttleId);
77
- this.emit('throttle:released', throttleId);
78
- }
79
- /**
80
- * Set throttle speed (0.0 to 1.0)
81
- */
82
- async setSpeed(throttleId, speed) {
83
- const state = this.throttles.get(throttleId);
84
- if (!state) {
85
- throw new Error(`Throttle not found: ${throttleId}`);
86
- }
87
- if (!isValidSpeed(speed)) {
88
- throw new Error(`Invalid speed: ${speed}. Must be between 0.0 and 1.0`);
89
- }
90
- const message = {
91
- type: 'throttle',
92
- data: {
93
- throttle: throttleId,
94
- speed
95
- }
96
- };
97
- // JMRI doesn't send responses for throttle control commands, just send
98
- this.client.send(message);
99
- state.speed = speed;
100
- this.emit('throttle:updated', throttleId, { speed });
101
- }
102
- /**
103
- * Set throttle direction
104
- */
105
- async setDirection(throttleId, forward) {
106
- const state = this.throttles.get(throttleId);
107
- if (!state) {
108
- throw new Error(`Throttle not found: ${throttleId}`);
109
- }
110
- const message = {
111
- type: 'throttle',
112
- data: {
113
- throttle: throttleId,
114
- forward
115
- }
116
- };
117
- // JMRI doesn't send responses for throttle control commands, just send
118
- this.client.send(message);
119
- state.forward = forward;
120
- this.emit('throttle:updated', throttleId, { forward });
121
- }
122
- /**
123
- * Set throttle function (F0-F28)
124
- */
125
- async setFunction(throttleId, functionKey, value) {
126
- const state = this.throttles.get(throttleId);
127
- if (!state) {
128
- throw new Error(`Throttle not found: ${throttleId}`);
129
- }
130
- if (!isThrottleFunctionKey(functionKey)) {
131
- throw new Error(`Invalid function key: ${functionKey}`);
132
- }
133
- const data = {
134
- throttle: throttleId,
135
- [functionKey]: value
136
- };
137
- const message = {
138
- type: 'throttle',
139
- data
140
- };
141
- // JMRI doesn't send responses for throttle control commands, just send
142
- this.client.send(message);
143
- state.functions.set(functionKey, value);
144
- this.emit('throttle:updated', throttleId, { [functionKey]: value });
145
- }
146
- /**
147
- * Emergency stop for a throttle (speed to 0)
148
- */
149
- async emergencyStop(throttleId) {
150
- await this.setSpeed(throttleId, 0);
151
- }
152
- /**
153
- * Set throttle to idle (speed to 0, maintain direction)
154
- */
155
- async idle(throttleId) {
156
- await this.setSpeed(throttleId, 0);
157
- }
158
- /**
159
- * Get throttle state
160
- */
161
- getThrottleState(throttleId) {
162
- return this.throttles.get(throttleId);
163
- }
164
- /**
165
- * Get all throttle IDs
166
- */
167
- getThrottleIds() {
168
- return Array.from(this.throttles.keys());
169
- }
170
- /**
171
- * Get all throttle states
172
- */
173
- getAllThrottles() {
174
- return Array.from(this.throttles.values());
175
- }
176
- /**
177
- * Release all throttles
178
- */
179
- async releaseAllThrottles() {
180
- const throttleIds = this.getThrottleIds();
181
- for (const throttleId of throttleIds) {
182
- try {
183
- await this.releaseThrottle(throttleId);
184
- }
185
- catch (error) {
186
- // Continue releasing others even if one fails
187
- this.emit('error', error);
188
- }
189
- }
190
- }
191
- /**
192
- * Handle unsolicited throttle updates from JMRI
193
- */
194
- handleThrottleUpdate(message) {
195
- const throttleId = message.data?.throttle;
196
- if (!throttleId) {
197
- return;
198
- }
199
- const state = this.throttles.get(throttleId);
200
- if (!state) {
201
- // Unknown throttle, possibly lost
202
- this.emit('throttle:lost', throttleId);
203
- return;
204
- }
205
- // Update state from message
206
- if (message.data.speed !== undefined) {
207
- state.speed = message.data.speed;
208
- }
209
- if (message.data.forward !== undefined) {
210
- state.forward = message.data.forward;
211
- }
212
- // Update functions
213
- for (let i = 0; i <= 28; i++) {
214
- const key = `F${i}`;
215
- if (message.data[key] !== undefined) {
216
- state.functions.set(key, message.data[key]);
217
- }
218
- }
219
- this.emit('throttle:updated', throttleId, message.data);
220
- }
221
- /**
222
- * Handle disconnect - mark all throttles as not acquired
223
- */
224
- handleDisconnect() {
225
- for (const state of this.throttles.values()) {
226
- state.acquired = false;
227
- }
228
- }
229
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * Turnout (switch) manager
3
- */
4
- import { EventEmitter } from 'eventemitter3';
5
- import { TurnoutState } from '../types/jmri-messages.js';
6
- /**
7
- * Manages JMRI turnout (track switch) state
8
- */
9
- export class TurnoutManager extends EventEmitter {
10
- constructor(client) {
11
- super();
12
- this.turnouts = new Map();
13
- this.client = client;
14
- this.client.on('update', (message) => {
15
- if (message.type === 'turnout') {
16
- this.handleTurnoutUpdate(message);
17
- }
18
- });
19
- }
20
- /**
21
- * Get the current state of a turnout.
22
- * Also registers a server-side listener so subsequent changes are pushed.
23
- */
24
- async getTurnout(name) {
25
- const message = {
26
- type: 'turnout',
27
- data: { name }
28
- };
29
- const response = await this.client.request(message);
30
- const state = response.data?.state ?? TurnoutState.UNKNOWN;
31
- this.turnouts.set(name, state);
32
- return state;
33
- }
34
- /**
35
- * Set a turnout to the given state
36
- */
37
- async setTurnout(name, state) {
38
- const message = {
39
- type: 'turnout',
40
- method: 'post',
41
- data: { name, state }
42
- };
43
- await this.client.request(message);
44
- const oldState = this.turnouts.get(name);
45
- this.turnouts.set(name, state);
46
- if (oldState !== state) {
47
- this.emit('turnout:changed', name, state);
48
- }
49
- }
50
- /**
51
- * Throw a turnout (diverging route)
52
- */
53
- async throwTurnout(name) {
54
- return this.setTurnout(name, TurnoutState.THROWN);
55
- }
56
- /**
57
- * Close a turnout (straight through / normal)
58
- */
59
- async closeTurnout(name) {
60
- return this.setTurnout(name, TurnoutState.CLOSED);
61
- }
62
- /**
63
- * List all turnouts known to JMRI
64
- */
65
- async listTurnouts() {
66
- const message = {
67
- type: 'turnout',
68
- method: 'list'
69
- };
70
- const response = await this.client.request(message);
71
- const entries = Array.isArray(response?.data)
72
- ? response.data.map((r) => r.data ?? r)
73
- : [];
74
- for (const entry of entries) {
75
- if (entry.name && entry.state !== undefined) {
76
- this.turnouts.set(entry.name, entry.state);
77
- }
78
- }
79
- return entries;
80
- }
81
- /**
82
- * Get cached turnout state without a network request
83
- */
84
- getTurnoutState(name) {
85
- return this.turnouts.get(name);
86
- }
87
- /**
88
- * Get all cached turnout states
89
- */
90
- getCachedTurnouts() {
91
- return new Map(this.turnouts);
92
- }
93
- /**
94
- * Handle unsolicited turnout state updates from JMRI
95
- */
96
- handleTurnoutUpdate(message) {
97
- const name = message.data?.name;
98
- const state = message.data?.state;
99
- if (!name || state === undefined)
100
- return;
101
- const oldState = this.turnouts.get(name);
102
- this.turnouts.set(name, state);
103
- if (oldState !== state) {
104
- this.emit('turnout:changed', name, state);
105
- }
106
- }
107
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Mock system exports
3
- */
4
- export { MockResponseManager, mockResponseManager } from './mock-response-manager.js';
5
- // Re-export mock data for direct access if needed
6
- export { mockData } from './mock-data.js';
@@ -1,234 +0,0 @@
1
- /**
2
- * Mock data for JMRI responses
3
- * Used for testing and demo mode
4
- */
5
- export const mockData = {
6
- "hello": {
7
- "type": "hello",
8
- "data": {
9
- "JMRI": "5.9.2",
10
- "json": "5.0",
11
- "version": "v5",
12
- "heartbeat": 13500,
13
- "railroad": "Demo Railroad",
14
- "node": "jmri-server",
15
- "activeProfile": "Demo Profile"
16
- }
17
- },
18
- "power": {
19
- "get": {
20
- "on": {
21
- "type": "power",
22
- "data": {
23
- "state": 2
24
- }
25
- },
26
- "off": {
27
- "type": "power",
28
- "data": {
29
- "state": 4
30
- }
31
- }
32
- },
33
- "post": {
34
- "success": {
35
- "type": "power",
36
- "data": {
37
- "state": 2
38
- }
39
- }
40
- }
41
- },
42
- "roster": {
43
- "list": [
44
- {
45
- "type": "rosterEntry",
46
- "data": {
47
- "name": "CSX754",
48
- "address": "754",
49
- "isLongAddress": true,
50
- "road": "CSX",
51
- "number": "754",
52
- "mfg": "Athearn",
53
- "decoderModel": "DH163D",
54
- "decoderFamily": "Digitrax DH163",
55
- "model": "GP38-2",
56
- "comment": "Blue and yellow scheme",
57
- "maxSpeedPct": 100,
58
- "image": null,
59
- "icon": "/roster/CSX754/icon",
60
- "shuntingFunction": "",
61
- "owner": "",
62
- "dateModified": "2026-02-10T00:00:00.000+00:00",
63
- "functionKeys": [
64
- { "name": "F0", "label": "Headlight", "lockable": true, "icon": null, "selectedIcon": null },
65
- { "name": "F1", "label": "Bell", "lockable": true, "icon": null, "selectedIcon": null },
66
- { "name": "F2", "label": "Horn", "lockable": false, "icon": null, "selectedIcon": null },
67
- { "name": "F3", "label": null, "lockable": false, "icon": null, "selectedIcon": null },
68
- { "name": "F4", "label": "Dynamic Brake", "lockable": true, "icon": null, "selectedIcon": null },
69
- { "name": "F5", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
70
- ],
71
- "attributes": [],
72
- "rosterGroups": []
73
- },
74
- "id": 1
75
- },
76
- {
77
- "type": "rosterEntry",
78
- "data": {
79
- "name": "UP3985",
80
- "address": "3985",
81
- "isLongAddress": true,
82
- "road": "Union Pacific",
83
- "number": "3985",
84
- "mfg": "Rivarossi",
85
- "decoderModel": "Sound decoder",
86
- "decoderFamily": "ESU LokSound",
87
- "model": "Challenger 4-6-6-4",
88
- "comment": "Steam locomotive",
89
- "maxSpeedPct": 100,
90
- "image": null,
91
- "icon": "/roster/UP3985/icon",
92
- "shuntingFunction": "",
93
- "owner": "",
94
- "dateModified": "2026-02-10T00:00:00.000+00:00",
95
- "functionKeys": [
96
- { "name": "F0", "label": "Headlight", "lockable": true, "icon": null, "selectedIcon": null },
97
- { "name": "F1", "label": "Bell", "lockable": true, "icon": null, "selectedIcon": null },
98
- { "name": "F2", "label": "Whistle", "lockable": false, "icon": null, "selectedIcon": null },
99
- { "name": "F3", "label": "Steam", "lockable": true, "icon": null, "selectedIcon": null },
100
- { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null }
101
- ],
102
- "attributes": [],
103
- "rosterGroups": []
104
- },
105
- "id": 2
106
- },
107
- {
108
- "type": "rosterEntry",
109
- "data": {
110
- "name": "BNSF5240",
111
- "address": "5240",
112
- "isLongAddress": true,
113
- "road": "BNSF",
114
- "number": "5240",
115
- "mfg": "Kato",
116
- "decoderModel": "DCC Sound",
117
- "decoderFamily": "Kato",
118
- "model": "SD40-2",
119
- "comment": "Heritage II paint",
120
- "maxSpeedPct": 100,
121
- "image": null,
122
- "icon": "/roster/BNSF5240/icon",
123
- "shuntingFunction": "",
124
- "owner": "",
125
- "dateModified": "2026-02-10T00:00:00.000+00:00",
126
- "functionKeys": [
127
- { "name": "F0", "label": "Headlight", "lockable": true, "icon": null, "selectedIcon": null },
128
- { "name": "F1", "label": "Bell", "lockable": true, "icon": null, "selectedIcon": null },
129
- { "name": "F2", "label": "Horn", "lockable": false, "icon": null, "selectedIcon": null },
130
- { "name": "F3", "label": "Dynamic Brake", "lockable": true, "icon": null, "selectedIcon": null },
131
- { "name": "F4", "label": null, "lockable": false, "icon": null, "selectedIcon": null },
132
- { "name": "F5", "label": "Mars Light", "lockable": true, "icon": null, "selectedIcon": null }
133
- ],
134
- "attributes": [],
135
- "rosterGroups": []
136
- },
137
- "id": 3
138
- }
139
- ]
140
- },
141
- "throttle": {
142
- "acquire": {
143
- "success": {
144
- "type": "throttle",
145
- "data": {
146
- "throttle": "{THROTTLE_ID}",
147
- "address": "{ADDRESS}",
148
- "speed": 0,
149
- "forward": true,
150
- "F0": false,
151
- "F1": false,
152
- "F2": false,
153
- "F3": false,
154
- "F4": false
155
- }
156
- }
157
- },
158
- "release": {
159
- "success": {
160
- "type": "throttle",
161
- "data": {}
162
- }
163
- },
164
- "control": {
165
- "speed": {
166
- "type": "throttle",
167
- "data": {
168
- "throttle": "{THROTTLE_ID}",
169
- "speed": "{SPEED}"
170
- }
171
- },
172
- "direction": {
173
- "type": "throttle",
174
- "data": {
175
- "throttle": "{THROTTLE_ID}",
176
- "forward": "{FORWARD}"
177
- }
178
- },
179
- "function": {
180
- "type": "throttle",
181
- "data": {
182
- "throttle": "{THROTTLE_ID}",
183
- "{FUNCTION}": "{VALUE}"
184
- }
185
- }
186
- }
187
- },
188
- "light": {
189
- "list": [
190
- { "type": "light", "data": { "name": "IL1", "userName": "Yard Light", "comment": null, "properties": [], "state": 4 } },
191
- { "type": "light", "data": { "name": "IL2", "userName": "Platform Light", "comment": null, "properties": [], "state": 4 } },
192
- { "type": "light", "data": { "name": "IL3", "userName": "Signal Lamp", "comment": null, "properties": [], "state": 2 } }
193
- ]
194
- },
195
- "turnout": {
196
- "list": [
197
- { "type": "turnout", "data": { "name": "LT1", "userName": "Main Diverge", "state": 2 } },
198
- { "type": "turnout", "data": { "name": "LT2", "userName": "Yard Lead", "state": 2 } },
199
- { "type": "turnout", "data": { "name": "LT3", "userName": "Siding Entry", "state": 4 } }
200
- ]
201
- },
202
- "ping": {
203
- "type": "ping"
204
- },
205
- "pong": {
206
- "type": "pong"
207
- },
208
- "goodbye": {
209
- "type": "goodbye"
210
- },
211
- "error": {
212
- "throttleNotFound": {
213
- "type": "error",
214
- "data": {
215
- "code": 404,
216
- "message": "Throttle not found"
217
- }
218
- },
219
- "invalidSpeed": {
220
- "type": "error",
221
- "data": {
222
- "code": 400,
223
- "message": "Invalid speed value"
224
- }
225
- },
226
- "connectionError": {
227
- "type": "error",
228
- "data": {
229
- "code": 500,
230
- "message": "Connection error"
231
- }
232
- }
233
- }
234
- };