jmri-client 3.7.1 → 4.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.
- package/README.md +32 -0
- package/dist/browser/jmri-client.js +213 -0
- package/dist/cjs/client.js +48 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/managers/index.js +1 -0
- package/dist/cjs/managers/light-manager.js +111 -0
- package/dist/cjs/mocks/mock-data.js +7 -0
- package/dist/cjs/mocks/mock-response-manager.js +41 -0
- package/dist/cjs/types/jmri-messages.js +28 -1
- package/dist/esm/client.js +48 -0
- package/dist/esm/index.js +3 -2
- package/dist/esm/managers/index.js +1 -0
- package/dist/esm/managers/light-manager.js +107 -0
- package/dist/esm/mocks/mock-data.js +7 -0
- package/dist/esm/mocks/mock-response-manager.js +42 -1
- package/dist/esm/types/jmri-messages.js +26 -0
- package/dist/types/client.d.ts +32 -2
- package/dist/types/index.d.ts +3 -2
- package/dist/types/managers/index.d.ts +1 -0
- package/dist/types/managers/light-manager.d.ts +47 -0
- package/dist/types/mocks/mock-data.d.ts +30 -0
- package/dist/types/mocks/mock-response-manager.d.ts +10 -1
- package/dist/types/types/events.d.ts +2 -1
- package/dist/types/types/jmri-messages.d.ts +33 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ WebSocket client for [JMRI](http://jmri.sourceforge.net/) with real-time updates
|
|
|
19
19
|
- ✅ **Heartbeat monitoring** - Automatic ping/pong keepalive
|
|
20
20
|
- ✅ **TypeScript** - Full type definitions included
|
|
21
21
|
- ✅ **Dual module support** - ESM and CommonJS
|
|
22
|
+
- ✅ **Extensible** - Subclass `JmriClient` to add support for additional JMRI object types
|
|
22
23
|
|
|
23
24
|
## Installation
|
|
24
25
|
|
|
@@ -105,6 +106,37 @@ client.on('reconnecting', (attempt, delay) => {
|
|
|
105
106
|
});
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
### Extending JmriClient
|
|
110
|
+
|
|
111
|
+
`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):
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { JmriClient } from 'jmri-client';
|
|
115
|
+
import type { PartialClientOptions } from 'jmri-client';
|
|
116
|
+
|
|
117
|
+
class MyExtendedClient extends JmriClient {
|
|
118
|
+
constructor(options?: PartialClientOptions) {
|
|
119
|
+
super(options);
|
|
120
|
+
|
|
121
|
+
// this.wsClient is available — use it to send/receive JMRI JSON messages
|
|
122
|
+
this.wsClient.on('update', (message: any) => {
|
|
123
|
+
if (message.type === 'sensor') {
|
|
124
|
+
this.emit('sensor:changed', message.data.name, message.data.state);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async listSensors() {
|
|
130
|
+
const response = await this.wsClient.request({ type: 'sensor', method: 'list' });
|
|
131
|
+
return Array.isArray(response?.data)
|
|
132
|
+
? response.data.map((r: any) => r.data ?? r)
|
|
133
|
+
: [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`WebSocketClient` is also exported for direct use if you need it. See its `send()`, `request()`, and `on('update', ...)` API for low-level messaging.
|
|
139
|
+
|
|
108
140
|
## Testing
|
|
109
141
|
|
|
110
142
|
**Unit Tests** (no hardware required):
|
|
@@ -638,6 +638,23 @@ function turnoutStateToString(state) {
|
|
|
638
638
|
return "UNKNOWN";
|
|
639
639
|
}
|
|
640
640
|
}
|
|
641
|
+
var LightState = /* @__PURE__ */ ((LightState2) => {
|
|
642
|
+
LightState2[LightState2["UNKNOWN"] = 0] = "UNKNOWN";
|
|
643
|
+
LightState2[LightState2["ON"] = 2] = "ON";
|
|
644
|
+
LightState2[LightState2["OFF"] = 4] = "OFF";
|
|
645
|
+
return LightState2;
|
|
646
|
+
})(LightState || {});
|
|
647
|
+
function lightStateToString(state) {
|
|
648
|
+
switch (state) {
|
|
649
|
+
case 2 /* ON */:
|
|
650
|
+
return "ON";
|
|
651
|
+
case 4 /* OFF */:
|
|
652
|
+
return "OFF";
|
|
653
|
+
case 0 /* UNKNOWN */:
|
|
654
|
+
default:
|
|
655
|
+
return "UNKNOWN";
|
|
656
|
+
}
|
|
657
|
+
}
|
|
641
658
|
|
|
642
659
|
// src/mocks/mock-data.ts
|
|
643
660
|
var mockData = {
|
|
@@ -823,6 +840,13 @@ var mockData = {
|
|
|
823
840
|
}
|
|
824
841
|
}
|
|
825
842
|
},
|
|
843
|
+
"light": {
|
|
844
|
+
"list": [
|
|
845
|
+
{ "type": "light", "data": { "name": "IL1", "userName": "Yard Light", "comment": null, "properties": [], "state": 4 } },
|
|
846
|
+
{ "type": "light", "data": { "name": "IL2", "userName": "Platform Light", "comment": null, "properties": [], "state": 4 } },
|
|
847
|
+
{ "type": "light", "data": { "name": "IL3", "userName": "Signal Lamp", "comment": null, "properties": [], "state": 2 } }
|
|
848
|
+
]
|
|
849
|
+
},
|
|
826
850
|
"turnout": {
|
|
827
851
|
"list": [
|
|
828
852
|
{ "type": "turnout", "data": { "name": "LT1", "userName": "Main Diverge", "state": 2 } },
|
|
@@ -868,6 +892,11 @@ var mockData = {
|
|
|
868
892
|
var MockResponseManager = class {
|
|
869
893
|
constructor(options = {}) {
|
|
870
894
|
this.throttles = /* @__PURE__ */ new Map();
|
|
895
|
+
this.lights = /* @__PURE__ */ new Map([
|
|
896
|
+
["IL1", 4 /* OFF */],
|
|
897
|
+
["IL2", 4 /* OFF */],
|
|
898
|
+
["IL3", 2 /* ON */]
|
|
899
|
+
]);
|
|
871
900
|
this.turnouts = /* @__PURE__ */ new Map([
|
|
872
901
|
["LT1", 2 /* CLOSED */],
|
|
873
902
|
["LT2", 2 /* CLOSED */],
|
|
@@ -892,6 +921,8 @@ var MockResponseManager = class {
|
|
|
892
921
|
return this.getRosterResponse(message);
|
|
893
922
|
case "throttle":
|
|
894
923
|
return this.getThrottleResponse(message);
|
|
924
|
+
case "light":
|
|
925
|
+
return this.getLightResponse(message);
|
|
895
926
|
case "turnout":
|
|
896
927
|
return this.getTurnoutResponse(message);
|
|
897
928
|
case "ping":
|
|
@@ -1007,6 +1038,26 @@ var MockResponseManager = class {
|
|
|
1007
1038
|
data: {}
|
|
1008
1039
|
};
|
|
1009
1040
|
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Get light response
|
|
1043
|
+
*/
|
|
1044
|
+
getLightResponse(message) {
|
|
1045
|
+
if (message.method === "list") {
|
|
1046
|
+
return {
|
|
1047
|
+
type: "light",
|
|
1048
|
+
data: JSON.parse(JSON.stringify(mockData.light.list))
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
const name = message.data?.name;
|
|
1052
|
+
if (!name) {
|
|
1053
|
+
return { type: "light", data: { name: "", state: 0 /* UNKNOWN */ } };
|
|
1054
|
+
}
|
|
1055
|
+
if (message.method === "post" && message.data?.state !== void 0) {
|
|
1056
|
+
this.lights.set(name, message.data.state);
|
|
1057
|
+
}
|
|
1058
|
+
const state = this.lights.get(name) ?? 0 /* UNKNOWN */;
|
|
1059
|
+
return { type: "light", data: { name, state } };
|
|
1060
|
+
}
|
|
1010
1061
|
/**
|
|
1011
1062
|
* Get turnout response
|
|
1012
1063
|
*/
|
|
@@ -1057,6 +1108,12 @@ var MockResponseManager = class {
|
|
|
1057
1108
|
getThrottles() {
|
|
1058
1109
|
return this.throttles;
|
|
1059
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Get all light states (for testing)
|
|
1113
|
+
*/
|
|
1114
|
+
getLights() {
|
|
1115
|
+
return this.lights;
|
|
1116
|
+
}
|
|
1060
1117
|
/**
|
|
1061
1118
|
* Get all turnout states (for testing)
|
|
1062
1119
|
*/
|
|
@@ -1069,6 +1126,11 @@ var MockResponseManager = class {
|
|
|
1069
1126
|
reset() {
|
|
1070
1127
|
this.powerState = 4 /* OFF */;
|
|
1071
1128
|
this.throttles.clear();
|
|
1129
|
+
this.lights = /* @__PURE__ */ new Map([
|
|
1130
|
+
["IL1", 4 /* OFF */],
|
|
1131
|
+
["IL2", 4 /* OFF */],
|
|
1132
|
+
["IL3", 2 /* ON */]
|
|
1133
|
+
]);
|
|
1072
1134
|
this.turnouts = /* @__PURE__ */ new Map([
|
|
1073
1135
|
["LT1", 2 /* CLOSED */],
|
|
1074
1136
|
["LT2", 2 /* CLOSED */],
|
|
@@ -1891,6 +1953,104 @@ var TurnoutManager = class extends import_index.default {
|
|
|
1891
1953
|
}
|
|
1892
1954
|
};
|
|
1893
1955
|
|
|
1956
|
+
// src/managers/light-manager.ts
|
|
1957
|
+
var LightManager = class extends import_index.default {
|
|
1958
|
+
constructor(client) {
|
|
1959
|
+
super();
|
|
1960
|
+
this.lights = /* @__PURE__ */ new Map();
|
|
1961
|
+
this.client = client;
|
|
1962
|
+
this.client.on("update", (message) => {
|
|
1963
|
+
if (message.type === "light") {
|
|
1964
|
+
this.handleLightUpdate(message);
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Get the current state of a light.
|
|
1970
|
+
* Also registers a server-side listener so subsequent changes are pushed.
|
|
1971
|
+
*/
|
|
1972
|
+
async getLight(name) {
|
|
1973
|
+
const message = {
|
|
1974
|
+
type: "light",
|
|
1975
|
+
data: { name }
|
|
1976
|
+
};
|
|
1977
|
+
const response = await this.client.request(message);
|
|
1978
|
+
const state = response.data?.state ?? 0 /* UNKNOWN */;
|
|
1979
|
+
this.lights.set(name, state);
|
|
1980
|
+
return state;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Set a light to the given state
|
|
1984
|
+
*/
|
|
1985
|
+
async setLight(name, state) {
|
|
1986
|
+
const message = {
|
|
1987
|
+
type: "light",
|
|
1988
|
+
method: "post",
|
|
1989
|
+
data: { name, state }
|
|
1990
|
+
};
|
|
1991
|
+
await this.client.request(message);
|
|
1992
|
+
const oldState = this.lights.get(name);
|
|
1993
|
+
this.lights.set(name, state);
|
|
1994
|
+
if (oldState !== state) {
|
|
1995
|
+
this.emit("light:changed", name, state);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Turn a light on
|
|
2000
|
+
*/
|
|
2001
|
+
async turnOnLight(name) {
|
|
2002
|
+
return this.setLight(name, 2 /* ON */);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Turn a light off
|
|
2006
|
+
*/
|
|
2007
|
+
async turnOffLight(name) {
|
|
2008
|
+
return this.setLight(name, 4 /* OFF */);
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* List all lights known to JMRI
|
|
2012
|
+
*/
|
|
2013
|
+
async listLights() {
|
|
2014
|
+
const message = {
|
|
2015
|
+
type: "light",
|
|
2016
|
+
method: "list"
|
|
2017
|
+
};
|
|
2018
|
+
const response = await this.client.request(message);
|
|
2019
|
+
const entries = Array.isArray(response?.data) ? response.data.map((r) => r.data ?? r) : [];
|
|
2020
|
+
for (const entry of entries) {
|
|
2021
|
+
if (entry.name && entry.state !== void 0) {
|
|
2022
|
+
this.lights.set(entry.name, entry.state);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return entries;
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Get cached light state without a network request
|
|
2029
|
+
*/
|
|
2030
|
+
getLightState(name) {
|
|
2031
|
+
return this.lights.get(name);
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Get all cached light states
|
|
2035
|
+
*/
|
|
2036
|
+
getCachedLights() {
|
|
2037
|
+
return new Map(this.lights);
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Handle unsolicited light state updates from JMRI
|
|
2041
|
+
*/
|
|
2042
|
+
handleLightUpdate(message) {
|
|
2043
|
+
const name = message.data?.name;
|
|
2044
|
+
const state = message.data?.state;
|
|
2045
|
+
if (!name || state === void 0) return;
|
|
2046
|
+
const oldState = this.lights.get(name);
|
|
2047
|
+
this.lights.set(name, state);
|
|
2048
|
+
if (oldState !== state) {
|
|
2049
|
+
this.emit("light:changed", name, state);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
};
|
|
2053
|
+
|
|
1894
2054
|
// src/types/client-options.ts
|
|
1895
2055
|
var DEFAULT_CLIENT_OPTIONS = {
|
|
1896
2056
|
host: "localhost",
|
|
@@ -1967,6 +2127,7 @@ var JmriClient = class extends import_index.default {
|
|
|
1967
2127
|
this.rosterManager = new RosterManager(this.wsClient);
|
|
1968
2128
|
this.throttleManager = new ThrottleManager(this.wsClient);
|
|
1969
2129
|
this.turnoutManager = new TurnoutManager(this.wsClient);
|
|
2130
|
+
this.lightManager = new LightManager(this.wsClient);
|
|
1970
2131
|
this.wsClient.on("connected", () => this.emit("connected"));
|
|
1971
2132
|
this.wsClient.on("disconnected", (reason) => this.emit("disconnected", reason));
|
|
1972
2133
|
this.wsClient.on(
|
|
@@ -1994,6 +2155,10 @@ var JmriClient = class extends import_index.default {
|
|
|
1994
2155
|
"turnout:changed",
|
|
1995
2156
|
(name, state) => this.emit("turnout:changed", name, state)
|
|
1996
2157
|
);
|
|
2158
|
+
this.lightManager.on(
|
|
2159
|
+
"light:changed",
|
|
2160
|
+
(name, state) => this.emit("light:changed", name, state)
|
|
2161
|
+
);
|
|
1997
2162
|
this.throttleManager.on(
|
|
1998
2163
|
"throttle:acquired",
|
|
1999
2164
|
(id) => this.emit("throttle:acquired", id)
|
|
@@ -2142,6 +2307,51 @@ var JmriClient = class extends import_index.default {
|
|
|
2142
2307
|
return this.turnoutManager.getCachedTurnouts();
|
|
2143
2308
|
}
|
|
2144
2309
|
// ============================================================================
|
|
2310
|
+
// Light Control
|
|
2311
|
+
// ============================================================================
|
|
2312
|
+
/**
|
|
2313
|
+
* Get the current state of a light
|
|
2314
|
+
*/
|
|
2315
|
+
async getLight(name) {
|
|
2316
|
+
return this.lightManager.getLight(name);
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Set a light to the given state
|
|
2320
|
+
*/
|
|
2321
|
+
async setLight(name, state) {
|
|
2322
|
+
return this.lightManager.setLight(name, state);
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Turn a light on
|
|
2326
|
+
*/
|
|
2327
|
+
async turnOnLight(name) {
|
|
2328
|
+
return this.lightManager.turnOnLight(name);
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Turn a light off
|
|
2332
|
+
*/
|
|
2333
|
+
async turnOffLight(name) {
|
|
2334
|
+
return this.lightManager.turnOffLight(name);
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* List all lights known to JMRI
|
|
2338
|
+
*/
|
|
2339
|
+
async listLights() {
|
|
2340
|
+
return this.lightManager.listLights();
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Get cached light state without a network request
|
|
2344
|
+
*/
|
|
2345
|
+
getLightState(name) {
|
|
2346
|
+
return this.lightManager.getLightState(name);
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Get all cached light states
|
|
2350
|
+
*/
|
|
2351
|
+
getCachedLights() {
|
|
2352
|
+
return this.lightManager.getCachedLights();
|
|
2353
|
+
}
|
|
2354
|
+
// ============================================================================
|
|
2145
2355
|
// Throttle Control
|
|
2146
2356
|
// ============================================================================
|
|
2147
2357
|
/**
|
|
@@ -2239,11 +2449,14 @@ var JmriClient = class extends import_index.default {
|
|
|
2239
2449
|
export {
|
|
2240
2450
|
ConnectionState,
|
|
2241
2451
|
JmriClient,
|
|
2452
|
+
LightState,
|
|
2242
2453
|
MockResponseManager,
|
|
2243
2454
|
PowerState,
|
|
2244
2455
|
TurnoutState,
|
|
2456
|
+
WebSocketClient,
|
|
2245
2457
|
isThrottleFunctionKey,
|
|
2246
2458
|
isValidSpeed,
|
|
2459
|
+
lightStateToString,
|
|
2247
2460
|
mockData,
|
|
2248
2461
|
mockResponseManager,
|
|
2249
2462
|
powerStateToString,
|
package/dist/cjs/client.js
CHANGED
|
@@ -10,6 +10,7 @@ const power_manager_js_1 = require("./managers/power-manager.js");
|
|
|
10
10
|
const roster_manager_js_1 = require("./managers/roster-manager.js");
|
|
11
11
|
const throttle_manager_js_1 = require("./managers/throttle-manager.js");
|
|
12
12
|
const turnout_manager_js_1 = require("./managers/turnout-manager.js");
|
|
13
|
+
const light_manager_js_1 = require("./managers/light-manager.js");
|
|
13
14
|
const client_options_js_1 = require("./types/client-options.js");
|
|
14
15
|
/**
|
|
15
16
|
* JMRI WebSocket Client
|
|
@@ -43,6 +44,7 @@ class JmriClient extends eventemitter3_1.EventEmitter {
|
|
|
43
44
|
this.rosterManager = new roster_manager_js_1.RosterManager(this.wsClient);
|
|
44
45
|
this.throttleManager = new throttle_manager_js_1.ThrottleManager(this.wsClient);
|
|
45
46
|
this.turnoutManager = new turnout_manager_js_1.TurnoutManager(this.wsClient);
|
|
47
|
+
this.lightManager = new light_manager_js_1.LightManager(this.wsClient);
|
|
46
48
|
// Forward events from WebSocket client
|
|
47
49
|
this.wsClient.on('connected', () => this.emit('connected'));
|
|
48
50
|
this.wsClient.on('disconnected', (reason) => this.emit('disconnected', reason));
|
|
@@ -57,6 +59,7 @@ class JmriClient extends eventemitter3_1.EventEmitter {
|
|
|
57
59
|
// Forward events from managers
|
|
58
60
|
this.powerManager.on('power:changed', (state) => this.emit('power:changed', state));
|
|
59
61
|
this.turnoutManager.on('turnout:changed', (name, state) => this.emit('turnout:changed', name, state));
|
|
62
|
+
this.lightManager.on('light:changed', (name, state) => this.emit('light:changed', name, state));
|
|
60
63
|
this.throttleManager.on('throttle:acquired', (id) => this.emit('throttle:acquired', id));
|
|
61
64
|
this.throttleManager.on('throttle:updated', (id, data) => this.emit('throttle:updated', id, data));
|
|
62
65
|
this.throttleManager.on('throttle:released', (id) => this.emit('throttle:released', id));
|
|
@@ -196,6 +199,51 @@ class JmriClient extends eventemitter3_1.EventEmitter {
|
|
|
196
199
|
return this.turnoutManager.getCachedTurnouts();
|
|
197
200
|
}
|
|
198
201
|
// ============================================================================
|
|
202
|
+
// Light Control
|
|
203
|
+
// ============================================================================
|
|
204
|
+
/**
|
|
205
|
+
* Get the current state of a light
|
|
206
|
+
*/
|
|
207
|
+
async getLight(name) {
|
|
208
|
+
return this.lightManager.getLight(name);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Set a light to the given state
|
|
212
|
+
*/
|
|
213
|
+
async setLight(name, state) {
|
|
214
|
+
return this.lightManager.setLight(name, state);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Turn a light on
|
|
218
|
+
*/
|
|
219
|
+
async turnOnLight(name) {
|
|
220
|
+
return this.lightManager.turnOnLight(name);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Turn a light off
|
|
224
|
+
*/
|
|
225
|
+
async turnOffLight(name) {
|
|
226
|
+
return this.lightManager.turnOffLight(name);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* List all lights known to JMRI
|
|
230
|
+
*/
|
|
231
|
+
async listLights() {
|
|
232
|
+
return this.lightManager.listLights();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get cached light state without a network request
|
|
236
|
+
*/
|
|
237
|
+
getLightState(name) {
|
|
238
|
+
return this.lightManager.getLightState(name);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get all cached light states
|
|
242
|
+
*/
|
|
243
|
+
getCachedLights() {
|
|
244
|
+
return this.lightManager.getCachedLights();
|
|
245
|
+
}
|
|
246
|
+
// ============================================================================
|
|
199
247
|
// Throttle Control
|
|
200
248
|
// ============================================================================
|
|
201
249
|
/**
|
package/dist/cjs/index.js
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
* WebSocket client for JMRI with real-time updates and throttle control
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.mockData = exports.mockResponseManager = exports.MockResponseManager = exports.turnoutStateToString = exports.powerStateToString = exports.isValidSpeed = exports.isThrottleFunctionKey = exports.ConnectionState = exports.TurnoutState = exports.PowerState = exports.JmriClient = void 0;
|
|
7
|
+
exports.mockData = exports.mockResponseManager = exports.MockResponseManager = exports.lightStateToString = exports.turnoutStateToString = exports.powerStateToString = exports.isValidSpeed = exports.isThrottleFunctionKey = exports.ConnectionState = exports.LightState = exports.TurnoutState = exports.PowerState = exports.WebSocketClient = exports.JmriClient = void 0;
|
|
8
8
|
var client_js_1 = require("./client.js");
|
|
9
9
|
Object.defineProperty(exports, "JmriClient", { enumerable: true, get: function () { return client_js_1.JmriClient; } });
|
|
10
|
+
var websocket_client_js_1 = require("./core/websocket-client.js");
|
|
11
|
+
Object.defineProperty(exports, "WebSocketClient", { enumerable: true, get: function () { return websocket_client_js_1.WebSocketClient; } });
|
|
10
12
|
// Export types
|
|
11
13
|
var index_js_1 = require("./types/index.js");
|
|
12
14
|
// JMRI message types
|
|
13
15
|
Object.defineProperty(exports, "PowerState", { enumerable: true, get: function () { return index_js_1.PowerState; } });
|
|
14
16
|
Object.defineProperty(exports, "TurnoutState", { enumerable: true, get: function () { return index_js_1.TurnoutState; } });
|
|
17
|
+
Object.defineProperty(exports, "LightState", { enumerable: true, get: function () { return index_js_1.LightState; } });
|
|
15
18
|
// Event types
|
|
16
19
|
Object.defineProperty(exports, "ConnectionState", { enumerable: true, get: function () { return index_js_1.ConnectionState; } });
|
|
17
20
|
// Export utility functions
|
|
@@ -21,6 +24,7 @@ Object.defineProperty(exports, "isValidSpeed", { enumerable: true, get: function
|
|
|
21
24
|
var jmri_messages_js_1 = require("./types/jmri-messages.js");
|
|
22
25
|
Object.defineProperty(exports, "powerStateToString", { enumerable: true, get: function () { return jmri_messages_js_1.powerStateToString; } });
|
|
23
26
|
Object.defineProperty(exports, "turnoutStateToString", { enumerable: true, get: function () { return jmri_messages_js_1.turnoutStateToString; } });
|
|
27
|
+
Object.defineProperty(exports, "lightStateToString", { enumerable: true, get: function () { return jmri_messages_js_1.lightStateToString; } });
|
|
24
28
|
// Export mock system for testing and demo purposes
|
|
25
29
|
var index_js_2 = require("./mocks/index.js");
|
|
26
30
|
Object.defineProperty(exports, "MockResponseManager", { enumerable: true, get: function () { return index_js_2.MockResponseManager; } });
|
|
@@ -21,3 +21,4 @@ __exportStar(require("./power-manager.js"), exports);
|
|
|
21
21
|
__exportStar(require("./roster-manager.js"), exports);
|
|
22
22
|
__exportStar(require("./throttle-manager.js"), exports);
|
|
23
23
|
__exportStar(require("./turnout-manager.js"), exports);
|
|
24
|
+
__exportStar(require("./light-manager.js"), exports);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Light manager
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LightManager = void 0;
|
|
7
|
+
const eventemitter3_1 = require("eventemitter3");
|
|
8
|
+
const jmri_messages_js_1 = require("../types/jmri-messages.js");
|
|
9
|
+
/**
|
|
10
|
+
* Manages JMRI light state
|
|
11
|
+
*/
|
|
12
|
+
class LightManager extends eventemitter3_1.EventEmitter {
|
|
13
|
+
constructor(client) {
|
|
14
|
+
super();
|
|
15
|
+
this.lights = new Map();
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.client.on('update', (message) => {
|
|
18
|
+
if (message.type === 'light') {
|
|
19
|
+
this.handleLightUpdate(message);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get the current state of a light.
|
|
25
|
+
* Also registers a server-side listener so subsequent changes are pushed.
|
|
26
|
+
*/
|
|
27
|
+
async getLight(name) {
|
|
28
|
+
const message = {
|
|
29
|
+
type: 'light',
|
|
30
|
+
data: { name }
|
|
31
|
+
};
|
|
32
|
+
const response = await this.client.request(message);
|
|
33
|
+
const state = response.data?.state ?? jmri_messages_js_1.LightState.UNKNOWN;
|
|
34
|
+
this.lights.set(name, state);
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Set a light to the given state
|
|
39
|
+
*/
|
|
40
|
+
async setLight(name, state) {
|
|
41
|
+
const message = {
|
|
42
|
+
type: 'light',
|
|
43
|
+
method: 'post',
|
|
44
|
+
data: { name, state }
|
|
45
|
+
};
|
|
46
|
+
await this.client.request(message);
|
|
47
|
+
const oldState = this.lights.get(name);
|
|
48
|
+
this.lights.set(name, state);
|
|
49
|
+
if (oldState !== state) {
|
|
50
|
+
this.emit('light:changed', name, state);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Turn a light on
|
|
55
|
+
*/
|
|
56
|
+
async turnOnLight(name) {
|
|
57
|
+
return this.setLight(name, jmri_messages_js_1.LightState.ON);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Turn a light off
|
|
61
|
+
*/
|
|
62
|
+
async turnOffLight(name) {
|
|
63
|
+
return this.setLight(name, jmri_messages_js_1.LightState.OFF);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* List all lights known to JMRI
|
|
67
|
+
*/
|
|
68
|
+
async listLights() {
|
|
69
|
+
const message = {
|
|
70
|
+
type: 'light',
|
|
71
|
+
method: 'list'
|
|
72
|
+
};
|
|
73
|
+
const response = await this.client.request(message);
|
|
74
|
+
const entries = Array.isArray(response?.data)
|
|
75
|
+
? response.data.map((r) => r.data ?? r)
|
|
76
|
+
: [];
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (entry.name && entry.state !== undefined) {
|
|
79
|
+
this.lights.set(entry.name, entry.state);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get cached light state without a network request
|
|
86
|
+
*/
|
|
87
|
+
getLightState(name) {
|
|
88
|
+
return this.lights.get(name);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get all cached light states
|
|
92
|
+
*/
|
|
93
|
+
getCachedLights() {
|
|
94
|
+
return new Map(this.lights);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Handle unsolicited light state updates from JMRI
|
|
98
|
+
*/
|
|
99
|
+
handleLightUpdate(message) {
|
|
100
|
+
const name = message.data?.name;
|
|
101
|
+
const state = message.data?.state;
|
|
102
|
+
if (!name || state === undefined)
|
|
103
|
+
return;
|
|
104
|
+
const oldState = this.lights.get(name);
|
|
105
|
+
this.lights.set(name, state);
|
|
106
|
+
if (oldState !== state) {
|
|
107
|
+
this.emit('light:changed', name, state);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.LightManager = LightManager;
|
|
@@ -188,6 +188,13 @@ exports.mockData = {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
},
|
|
191
|
+
"light": {
|
|
192
|
+
"list": [
|
|
193
|
+
{ "type": "light", "data": { "name": "IL1", "userName": "Yard Light", "comment": null, "properties": [], "state": 4 } },
|
|
194
|
+
{ "type": "light", "data": { "name": "IL2", "userName": "Platform Light", "comment": null, "properties": [], "state": 4 } },
|
|
195
|
+
{ "type": "light", "data": { "name": "IL3", "userName": "Signal Lamp", "comment": null, "properties": [], "state": 2 } }
|
|
196
|
+
]
|
|
197
|
+
},
|
|
191
198
|
"turnout": {
|
|
192
199
|
"list": [
|
|
193
200
|
{ "type": "turnout", "data": { "name": "LT1", "userName": "Main Diverge", "state": 2 } },
|
|
@@ -13,6 +13,11 @@ const mock_data_js_1 = require("./mock-data.js");
|
|
|
13
13
|
class MockResponseManager {
|
|
14
14
|
constructor(options = {}) {
|
|
15
15
|
this.throttles = new Map();
|
|
16
|
+
this.lights = new Map([
|
|
17
|
+
['IL1', jmri_messages_js_1.LightState.OFF],
|
|
18
|
+
['IL2', jmri_messages_js_1.LightState.OFF],
|
|
19
|
+
['IL3', jmri_messages_js_1.LightState.ON]
|
|
20
|
+
]);
|
|
16
21
|
this.turnouts = new Map([
|
|
17
22
|
['LT1', jmri_messages_js_1.TurnoutState.CLOSED],
|
|
18
23
|
['LT2', jmri_messages_js_1.TurnoutState.CLOSED],
|
|
@@ -39,6 +44,8 @@ class MockResponseManager {
|
|
|
39
44
|
return this.getRosterResponse(message);
|
|
40
45
|
case 'throttle':
|
|
41
46
|
return this.getThrottleResponse(message);
|
|
47
|
+
case 'light':
|
|
48
|
+
return this.getLightResponse(message);
|
|
42
49
|
case 'turnout':
|
|
43
50
|
return this.getTurnoutResponse(message);
|
|
44
51
|
case 'ping':
|
|
@@ -164,6 +171,29 @@ class MockResponseManager {
|
|
|
164
171
|
data: {}
|
|
165
172
|
};
|
|
166
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Get light response
|
|
176
|
+
*/
|
|
177
|
+
getLightResponse(message) {
|
|
178
|
+
// List all lights
|
|
179
|
+
if (message.method === 'list') {
|
|
180
|
+
return {
|
|
181
|
+
type: 'light',
|
|
182
|
+
data: JSON.parse(JSON.stringify(mock_data_js_1.mockData.light.list))
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const name = message.data?.name;
|
|
186
|
+
if (!name) {
|
|
187
|
+
return { type: 'light', data: { name: '', state: jmri_messages_js_1.LightState.UNKNOWN } };
|
|
188
|
+
}
|
|
189
|
+
// Set light state
|
|
190
|
+
if (message.method === 'post' && message.data?.state !== undefined) {
|
|
191
|
+
this.lights.set(name, message.data.state);
|
|
192
|
+
}
|
|
193
|
+
// Get or confirm current state
|
|
194
|
+
const state = this.lights.get(name) ?? jmri_messages_js_1.LightState.UNKNOWN;
|
|
195
|
+
return { type: 'light', data: { name, state } };
|
|
196
|
+
}
|
|
167
197
|
/**
|
|
168
198
|
* Get turnout response
|
|
169
199
|
*/
|
|
@@ -217,6 +247,12 @@ class MockResponseManager {
|
|
|
217
247
|
getThrottles() {
|
|
218
248
|
return this.throttles;
|
|
219
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Get all light states (for testing)
|
|
252
|
+
*/
|
|
253
|
+
getLights() {
|
|
254
|
+
return this.lights;
|
|
255
|
+
}
|
|
220
256
|
/**
|
|
221
257
|
* Get all turnout states (for testing)
|
|
222
258
|
*/
|
|
@@ -229,6 +265,11 @@ class MockResponseManager {
|
|
|
229
265
|
reset() {
|
|
230
266
|
this.powerState = jmri_messages_js_1.PowerState.OFF;
|
|
231
267
|
this.throttles.clear();
|
|
268
|
+
this.lights = new Map([
|
|
269
|
+
['IL1', jmri_messages_js_1.LightState.OFF],
|
|
270
|
+
['IL2', jmri_messages_js_1.LightState.OFF],
|
|
271
|
+
['IL3', jmri_messages_js_1.LightState.ON]
|
|
272
|
+
]);
|
|
232
273
|
this.turnouts = new Map([
|
|
233
274
|
['LT1', jmri_messages_js_1.TurnoutState.CLOSED],
|
|
234
275
|
['LT2', jmri_messages_js_1.TurnoutState.CLOSED],
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Based on JMRI JSON protocol specification
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.TurnoutState = exports.PowerState = void 0;
|
|
7
|
+
exports.LightState = exports.TurnoutState = exports.PowerState = void 0;
|
|
8
8
|
exports.powerStateToString = powerStateToString;
|
|
9
9
|
exports.turnoutStateToString = turnoutStateToString;
|
|
10
|
+
exports.lightStateToString = lightStateToString;
|
|
10
11
|
/**
|
|
11
12
|
* Power state values (from JMRI JSON protocol constants)
|
|
12
13
|
* UNKNOWN = 0 (state cannot be determined)
|
|
@@ -66,3 +67,29 @@ function turnoutStateToString(state) {
|
|
|
66
67
|
return 'UNKNOWN';
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Light state values (from JMRI JSON protocol constants)
|
|
72
|
+
* UNKNOWN = 0 (state cannot be determined)
|
|
73
|
+
* ON = 2 (light is on)
|
|
74
|
+
* OFF = 4 (light is off)
|
|
75
|
+
*/
|
|
76
|
+
var LightState;
|
|
77
|
+
(function (LightState) {
|
|
78
|
+
LightState[LightState["UNKNOWN"] = 0] = "UNKNOWN";
|
|
79
|
+
LightState[LightState["ON"] = 2] = "ON";
|
|
80
|
+
LightState[LightState["OFF"] = 4] = "OFF";
|
|
81
|
+
})(LightState || (exports.LightState = LightState = {}));
|
|
82
|
+
/**
|
|
83
|
+
* Convert LightState enum to human-readable string
|
|
84
|
+
*/
|
|
85
|
+
function lightStateToString(state) {
|
|
86
|
+
switch (state) {
|
|
87
|
+
case LightState.ON:
|
|
88
|
+
return 'ON';
|
|
89
|
+
case LightState.OFF:
|
|
90
|
+
return 'OFF';
|
|
91
|
+
case LightState.UNKNOWN:
|
|
92
|
+
default:
|
|
93
|
+
return 'UNKNOWN';
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/esm/client.js
CHANGED
|
@@ -7,6 +7,7 @@ import { PowerManager } from './managers/power-manager.js';
|
|
|
7
7
|
import { RosterManager } from './managers/roster-manager.js';
|
|
8
8
|
import { ThrottleManager } from './managers/throttle-manager.js';
|
|
9
9
|
import { TurnoutManager } from './managers/turnout-manager.js';
|
|
10
|
+
import { LightManager } from './managers/light-manager.js';
|
|
10
11
|
import { mergeOptions } from './types/client-options.js';
|
|
11
12
|
/**
|
|
12
13
|
* JMRI WebSocket Client
|
|
@@ -40,6 +41,7 @@ export class JmriClient extends EventEmitter {
|
|
|
40
41
|
this.rosterManager = new RosterManager(this.wsClient);
|
|
41
42
|
this.throttleManager = new ThrottleManager(this.wsClient);
|
|
42
43
|
this.turnoutManager = new TurnoutManager(this.wsClient);
|
|
44
|
+
this.lightManager = new LightManager(this.wsClient);
|
|
43
45
|
// Forward events from WebSocket client
|
|
44
46
|
this.wsClient.on('connected', () => this.emit('connected'));
|
|
45
47
|
this.wsClient.on('disconnected', (reason) => this.emit('disconnected', reason));
|
|
@@ -54,6 +56,7 @@ export class JmriClient extends EventEmitter {
|
|
|
54
56
|
// Forward events from managers
|
|
55
57
|
this.powerManager.on('power:changed', (state) => this.emit('power:changed', state));
|
|
56
58
|
this.turnoutManager.on('turnout:changed', (name, state) => this.emit('turnout:changed', name, state));
|
|
59
|
+
this.lightManager.on('light:changed', (name, state) => this.emit('light:changed', name, state));
|
|
57
60
|
this.throttleManager.on('throttle:acquired', (id) => this.emit('throttle:acquired', id));
|
|
58
61
|
this.throttleManager.on('throttle:updated', (id, data) => this.emit('throttle:updated', id, data));
|
|
59
62
|
this.throttleManager.on('throttle:released', (id) => this.emit('throttle:released', id));
|
|
@@ -193,6 +196,51 @@ export class JmriClient extends EventEmitter {
|
|
|
193
196
|
return this.turnoutManager.getCachedTurnouts();
|
|
194
197
|
}
|
|
195
198
|
// ============================================================================
|
|
199
|
+
// Light Control
|
|
200
|
+
// ============================================================================
|
|
201
|
+
/**
|
|
202
|
+
* Get the current state of a light
|
|
203
|
+
*/
|
|
204
|
+
async getLight(name) {
|
|
205
|
+
return this.lightManager.getLight(name);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Set a light to the given state
|
|
209
|
+
*/
|
|
210
|
+
async setLight(name, state) {
|
|
211
|
+
return this.lightManager.setLight(name, state);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Turn a light on
|
|
215
|
+
*/
|
|
216
|
+
async turnOnLight(name) {
|
|
217
|
+
return this.lightManager.turnOnLight(name);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Turn a light off
|
|
221
|
+
*/
|
|
222
|
+
async turnOffLight(name) {
|
|
223
|
+
return this.lightManager.turnOffLight(name);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* List all lights known to JMRI
|
|
227
|
+
*/
|
|
228
|
+
async listLights() {
|
|
229
|
+
return this.lightManager.listLights();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get cached light state without a network request
|
|
233
|
+
*/
|
|
234
|
+
getLightState(name) {
|
|
235
|
+
return this.lightManager.getLightState(name);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get all cached light states
|
|
239
|
+
*/
|
|
240
|
+
getCachedLights() {
|
|
241
|
+
return this.lightManager.getCachedLights();
|
|
242
|
+
}
|
|
243
|
+
// ============================================================================
|
|
196
244
|
// Throttle Control
|
|
197
245
|
// ============================================================================
|
|
198
246
|
/**
|
package/dist/esm/index.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* WebSocket client for JMRI with real-time updates and throttle control
|
|
4
4
|
*/
|
|
5
5
|
export { JmriClient } from './client.js';
|
|
6
|
+
export { WebSocketClient } from './core/websocket-client.js';
|
|
6
7
|
// Export types
|
|
7
8
|
export {
|
|
8
9
|
// JMRI message types
|
|
9
|
-
PowerState, TurnoutState,
|
|
10
|
+
PowerState, TurnoutState, LightState,
|
|
10
11
|
// Event types
|
|
11
12
|
ConnectionState } from './types/index.js';
|
|
12
13
|
// Export utility functions
|
|
13
14
|
export { isThrottleFunctionKey, isValidSpeed } from './types/throttle.js';
|
|
14
|
-
export { powerStateToString, turnoutStateToString } from './types/jmri-messages.js';
|
|
15
|
+
export { powerStateToString, turnoutStateToString, lightStateToString } from './types/jmri-messages.js';
|
|
15
16
|
// Export mock system for testing and demo purposes
|
|
16
17
|
export { MockResponseManager, mockResponseManager, mockData } from './mocks/index.js';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light manager
|
|
3
|
+
*/
|
|
4
|
+
import { EventEmitter } from 'eventemitter3';
|
|
5
|
+
import { LightState } from '../types/jmri-messages.js';
|
|
6
|
+
/**
|
|
7
|
+
* Manages JMRI light state
|
|
8
|
+
*/
|
|
9
|
+
export class LightManager extends EventEmitter {
|
|
10
|
+
constructor(client) {
|
|
11
|
+
super();
|
|
12
|
+
this.lights = new Map();
|
|
13
|
+
this.client = client;
|
|
14
|
+
this.client.on('update', (message) => {
|
|
15
|
+
if (message.type === 'light') {
|
|
16
|
+
this.handleLightUpdate(message);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the current state of a light.
|
|
22
|
+
* Also registers a server-side listener so subsequent changes are pushed.
|
|
23
|
+
*/
|
|
24
|
+
async getLight(name) {
|
|
25
|
+
const message = {
|
|
26
|
+
type: 'light',
|
|
27
|
+
data: { name }
|
|
28
|
+
};
|
|
29
|
+
const response = await this.client.request(message);
|
|
30
|
+
const state = response.data?.state ?? LightState.UNKNOWN;
|
|
31
|
+
this.lights.set(name, state);
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Set a light to the given state
|
|
36
|
+
*/
|
|
37
|
+
async setLight(name, state) {
|
|
38
|
+
const message = {
|
|
39
|
+
type: 'light',
|
|
40
|
+
method: 'post',
|
|
41
|
+
data: { name, state }
|
|
42
|
+
};
|
|
43
|
+
await this.client.request(message);
|
|
44
|
+
const oldState = this.lights.get(name);
|
|
45
|
+
this.lights.set(name, state);
|
|
46
|
+
if (oldState !== state) {
|
|
47
|
+
this.emit('light:changed', name, state);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Turn a light on
|
|
52
|
+
*/
|
|
53
|
+
async turnOnLight(name) {
|
|
54
|
+
return this.setLight(name, LightState.ON);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Turn a light off
|
|
58
|
+
*/
|
|
59
|
+
async turnOffLight(name) {
|
|
60
|
+
return this.setLight(name, LightState.OFF);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* List all lights known to JMRI
|
|
64
|
+
*/
|
|
65
|
+
async listLights() {
|
|
66
|
+
const message = {
|
|
67
|
+
type: 'light',
|
|
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.lights.set(entry.name, entry.state);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get cached light state without a network request
|
|
83
|
+
*/
|
|
84
|
+
getLightState(name) {
|
|
85
|
+
return this.lights.get(name);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get all cached light states
|
|
89
|
+
*/
|
|
90
|
+
getCachedLights() {
|
|
91
|
+
return new Map(this.lights);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handle unsolicited light state updates from JMRI
|
|
95
|
+
*/
|
|
96
|
+
handleLightUpdate(message) {
|
|
97
|
+
const name = message.data?.name;
|
|
98
|
+
const state = message.data?.state;
|
|
99
|
+
if (!name || state === undefined)
|
|
100
|
+
return;
|
|
101
|
+
const oldState = this.lights.get(name);
|
|
102
|
+
this.lights.set(name, state);
|
|
103
|
+
if (oldState !== state) {
|
|
104
|
+
this.emit('light:changed', name, state);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -185,6 +185,13 @@ export const mockData = {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
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
|
+
},
|
|
188
195
|
"turnout": {
|
|
189
196
|
"list": [
|
|
190
197
|
{ "type": "turnout", "data": { "name": "LT1", "userName": "Main Diverge", "state": 2 } },
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Mock Response Manager
|
|
3
3
|
* Generates mock JMRI responses for testing and demo purposes
|
|
4
4
|
*/
|
|
5
|
-
import { PowerState, TurnoutState } from '../types/jmri-messages.js';
|
|
5
|
+
import { PowerState, TurnoutState, LightState } from '../types/jmri-messages.js';
|
|
6
6
|
import { mockData } from './mock-data.js';
|
|
7
7
|
/**
|
|
8
8
|
* Manages mock responses for JMRI protocol
|
|
@@ -10,6 +10,11 @@ import { mockData } from './mock-data.js';
|
|
|
10
10
|
export class MockResponseManager {
|
|
11
11
|
constructor(options = {}) {
|
|
12
12
|
this.throttles = new Map();
|
|
13
|
+
this.lights = new Map([
|
|
14
|
+
['IL1', LightState.OFF],
|
|
15
|
+
['IL2', LightState.OFF],
|
|
16
|
+
['IL3', LightState.ON]
|
|
17
|
+
]);
|
|
13
18
|
this.turnouts = new Map([
|
|
14
19
|
['LT1', TurnoutState.CLOSED],
|
|
15
20
|
['LT2', TurnoutState.CLOSED],
|
|
@@ -36,6 +41,8 @@ export class MockResponseManager {
|
|
|
36
41
|
return this.getRosterResponse(message);
|
|
37
42
|
case 'throttle':
|
|
38
43
|
return this.getThrottleResponse(message);
|
|
44
|
+
case 'light':
|
|
45
|
+
return this.getLightResponse(message);
|
|
39
46
|
case 'turnout':
|
|
40
47
|
return this.getTurnoutResponse(message);
|
|
41
48
|
case 'ping':
|
|
@@ -161,6 +168,29 @@ export class MockResponseManager {
|
|
|
161
168
|
data: {}
|
|
162
169
|
};
|
|
163
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Get light response
|
|
173
|
+
*/
|
|
174
|
+
getLightResponse(message) {
|
|
175
|
+
// List all lights
|
|
176
|
+
if (message.method === 'list') {
|
|
177
|
+
return {
|
|
178
|
+
type: 'light',
|
|
179
|
+
data: JSON.parse(JSON.stringify(mockData.light.list))
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const name = message.data?.name;
|
|
183
|
+
if (!name) {
|
|
184
|
+
return { type: 'light', data: { name: '', state: LightState.UNKNOWN } };
|
|
185
|
+
}
|
|
186
|
+
// Set light state
|
|
187
|
+
if (message.method === 'post' && message.data?.state !== undefined) {
|
|
188
|
+
this.lights.set(name, message.data.state);
|
|
189
|
+
}
|
|
190
|
+
// Get or confirm current state
|
|
191
|
+
const state = this.lights.get(name) ?? LightState.UNKNOWN;
|
|
192
|
+
return { type: 'light', data: { name, state } };
|
|
193
|
+
}
|
|
164
194
|
/**
|
|
165
195
|
* Get turnout response
|
|
166
196
|
*/
|
|
@@ -214,6 +244,12 @@ export class MockResponseManager {
|
|
|
214
244
|
getThrottles() {
|
|
215
245
|
return this.throttles;
|
|
216
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Get all light states (for testing)
|
|
249
|
+
*/
|
|
250
|
+
getLights() {
|
|
251
|
+
return this.lights;
|
|
252
|
+
}
|
|
217
253
|
/**
|
|
218
254
|
* Get all turnout states (for testing)
|
|
219
255
|
*/
|
|
@@ -226,6 +262,11 @@ export class MockResponseManager {
|
|
|
226
262
|
reset() {
|
|
227
263
|
this.powerState = PowerState.OFF;
|
|
228
264
|
this.throttles.clear();
|
|
265
|
+
this.lights = new Map([
|
|
266
|
+
['IL1', LightState.OFF],
|
|
267
|
+
['IL2', LightState.OFF],
|
|
268
|
+
['IL3', LightState.ON]
|
|
269
|
+
]);
|
|
229
270
|
this.turnouts = new Map([
|
|
230
271
|
['LT1', TurnoutState.CLOSED],
|
|
231
272
|
['LT2', TurnoutState.CLOSED],
|
|
@@ -61,3 +61,29 @@ export function turnoutStateToString(state) {
|
|
|
61
61
|
return 'UNKNOWN';
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Light state values (from JMRI JSON protocol constants)
|
|
66
|
+
* UNKNOWN = 0 (state cannot be determined)
|
|
67
|
+
* ON = 2 (light is on)
|
|
68
|
+
* OFF = 4 (light is off)
|
|
69
|
+
*/
|
|
70
|
+
export var LightState;
|
|
71
|
+
(function (LightState) {
|
|
72
|
+
LightState[LightState["UNKNOWN"] = 0] = "UNKNOWN";
|
|
73
|
+
LightState[LightState["ON"] = 2] = "ON";
|
|
74
|
+
LightState[LightState["OFF"] = 4] = "OFF";
|
|
75
|
+
})(LightState || (LightState = {}));
|
|
76
|
+
/**
|
|
77
|
+
* Convert LightState enum to human-readable string
|
|
78
|
+
*/
|
|
79
|
+
export function lightStateToString(state) {
|
|
80
|
+
switch (state) {
|
|
81
|
+
case LightState.ON:
|
|
82
|
+
return 'ON';
|
|
83
|
+
case LightState.OFF:
|
|
84
|
+
return 'OFF';
|
|
85
|
+
case LightState.UNKNOWN:
|
|
86
|
+
default:
|
|
87
|
+
return 'UNKNOWN';
|
|
88
|
+
}
|
|
89
|
+
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Main JMRI client class
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from 'eventemitter3';
|
|
5
|
+
import { WebSocketClient } from './core/websocket-client.js';
|
|
5
6
|
import { PartialClientOptions } from './types/client-options.js';
|
|
6
|
-
import { PowerState, RosterEntryWrapper, TurnoutState, TurnoutData } from './types/jmri-messages.js';
|
|
7
|
+
import { PowerState, RosterEntryWrapper, TurnoutState, TurnoutData, LightState, LightData } from './types/jmri-messages.js';
|
|
7
8
|
import { ConnectionState } from './types/events.js';
|
|
8
9
|
import { ThrottleAcquireOptions, ThrottleFunctionKey, ThrottleState } from './types/throttle.js';
|
|
9
10
|
/**
|
|
@@ -12,11 +13,12 @@ import { ThrottleAcquireOptions, ThrottleFunctionKey, ThrottleState } from './ty
|
|
|
12
13
|
*/
|
|
13
14
|
export declare class JmriClient extends EventEmitter {
|
|
14
15
|
private options;
|
|
15
|
-
|
|
16
|
+
protected wsClient: WebSocketClient;
|
|
16
17
|
private powerManager;
|
|
17
18
|
private rosterManager;
|
|
18
19
|
private throttleManager;
|
|
19
20
|
private turnoutManager;
|
|
21
|
+
private lightManager;
|
|
20
22
|
/**
|
|
21
23
|
* Create a new JMRI client
|
|
22
24
|
*
|
|
@@ -111,6 +113,34 @@ export declare class JmriClient extends EventEmitter {
|
|
|
111
113
|
* Get all cached turnout states
|
|
112
114
|
*/
|
|
113
115
|
getCachedTurnouts(): Map<string, TurnoutState>;
|
|
116
|
+
/**
|
|
117
|
+
* Get the current state of a light
|
|
118
|
+
*/
|
|
119
|
+
getLight(name: string): Promise<LightState>;
|
|
120
|
+
/**
|
|
121
|
+
* Set a light to the given state
|
|
122
|
+
*/
|
|
123
|
+
setLight(name: string, state: LightState): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Turn a light on
|
|
126
|
+
*/
|
|
127
|
+
turnOnLight(name: string): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Turn a light off
|
|
130
|
+
*/
|
|
131
|
+
turnOffLight(name: string): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* List all lights known to JMRI
|
|
134
|
+
*/
|
|
135
|
+
listLights(): Promise<LightData[]>;
|
|
136
|
+
/**
|
|
137
|
+
* Get cached light state without a network request
|
|
138
|
+
*/
|
|
139
|
+
getLightState(name: string): LightState | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* Get all cached light states
|
|
142
|
+
*/
|
|
143
|
+
getCachedLights(): Map<string, LightState>;
|
|
114
144
|
/**
|
|
115
145
|
* Acquire a throttle for a locomotive
|
|
116
146
|
*
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* WebSocket client for JMRI with real-time updates and throttle control
|
|
4
4
|
*/
|
|
5
5
|
export { JmriClient } from './client.js';
|
|
6
|
-
export {
|
|
6
|
+
export { WebSocketClient } from './core/websocket-client.js';
|
|
7
|
+
export { JmriClientOptions, PartialClientOptions, ReconnectionOptions, HeartbeatOptions, MockOptions, PowerState, TurnoutState, LightState, RosterEntry, TurnoutData, LightData, JmriMessage, PowerMessage, TurnoutMessage, LightMessage, ThrottleMessage, RosterMessage, ConnectionState, EventPayloads, ThrottleAcquireOptions, ThrottleFunctionKey, ThrottleState } from './types/index.js';
|
|
7
8
|
export { isThrottleFunctionKey, isValidSpeed } from './types/throttle.js';
|
|
8
|
-
export { powerStateToString, turnoutStateToString } from './types/jmri-messages.js';
|
|
9
|
+
export { powerStateToString, turnoutStateToString, lightStateToString } from './types/jmri-messages.js';
|
|
9
10
|
export { MockResponseManager, mockResponseManager, mockData } from './mocks/index.js';
|
|
10
11
|
export type { MockResponseManagerOptions } from './mocks/index.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light manager
|
|
3
|
+
*/
|
|
4
|
+
import { EventEmitter } from 'eventemitter3';
|
|
5
|
+
import { WebSocketClient } from '../core/websocket-client.js';
|
|
6
|
+
import { LightState, LightData } from '../types/jmri-messages.js';
|
|
7
|
+
/**
|
|
8
|
+
* Manages JMRI light state
|
|
9
|
+
*/
|
|
10
|
+
export declare class LightManager extends EventEmitter {
|
|
11
|
+
private client;
|
|
12
|
+
private lights;
|
|
13
|
+
constructor(client: WebSocketClient);
|
|
14
|
+
/**
|
|
15
|
+
* Get the current state of a light.
|
|
16
|
+
* Also registers a server-side listener so subsequent changes are pushed.
|
|
17
|
+
*/
|
|
18
|
+
getLight(name: string): Promise<LightState>;
|
|
19
|
+
/**
|
|
20
|
+
* Set a light to the given state
|
|
21
|
+
*/
|
|
22
|
+
setLight(name: string, state: LightState): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Turn a light on
|
|
25
|
+
*/
|
|
26
|
+
turnOnLight(name: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Turn a light off
|
|
29
|
+
*/
|
|
30
|
+
turnOffLight(name: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* List all lights known to JMRI
|
|
33
|
+
*/
|
|
34
|
+
listLights(): Promise<LightData[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Get cached light state without a network request
|
|
37
|
+
*/
|
|
38
|
+
getLightState(name: string): LightState | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Get all cached light states
|
|
41
|
+
*/
|
|
42
|
+
getCachedLights(): Map<string, LightState>;
|
|
43
|
+
/**
|
|
44
|
+
* Handle unsolicited light state updates from JMRI
|
|
45
|
+
*/
|
|
46
|
+
private handleLightUpdate;
|
|
47
|
+
}
|
|
@@ -263,6 +263,36 @@ export declare const mockData: {
|
|
|
263
263
|
};
|
|
264
264
|
};
|
|
265
265
|
};
|
|
266
|
+
readonly light: {
|
|
267
|
+
readonly list: readonly [{
|
|
268
|
+
readonly type: "light";
|
|
269
|
+
readonly data: {
|
|
270
|
+
readonly name: "IL1";
|
|
271
|
+
readonly userName: "Yard Light";
|
|
272
|
+
readonly comment: null;
|
|
273
|
+
readonly properties: readonly [];
|
|
274
|
+
readonly state: 4;
|
|
275
|
+
};
|
|
276
|
+
}, {
|
|
277
|
+
readonly type: "light";
|
|
278
|
+
readonly data: {
|
|
279
|
+
readonly name: "IL2";
|
|
280
|
+
readonly userName: "Platform Light";
|
|
281
|
+
readonly comment: null;
|
|
282
|
+
readonly properties: readonly [];
|
|
283
|
+
readonly state: 4;
|
|
284
|
+
};
|
|
285
|
+
}, {
|
|
286
|
+
readonly type: "light";
|
|
287
|
+
readonly data: {
|
|
288
|
+
readonly name: "IL3";
|
|
289
|
+
readonly userName: "Signal Lamp";
|
|
290
|
+
readonly comment: null;
|
|
291
|
+
readonly properties: readonly [];
|
|
292
|
+
readonly state: 2;
|
|
293
|
+
};
|
|
294
|
+
}];
|
|
295
|
+
};
|
|
266
296
|
readonly turnout: {
|
|
267
297
|
readonly list: readonly [{
|
|
268
298
|
readonly type: "turnout";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Mock Response Manager
|
|
3
3
|
* Generates mock JMRI responses for testing and demo purposes
|
|
4
4
|
*/
|
|
5
|
-
import { JmriMessage, PowerState, TurnoutState } from '../types/jmri-messages.js';
|
|
5
|
+
import { JmriMessage, PowerState, TurnoutState, LightState } from '../types/jmri-messages.js';
|
|
6
6
|
export interface MockResponseManagerOptions {
|
|
7
7
|
/**
|
|
8
8
|
* Delay in milliseconds before returning responses (simulates network latency)
|
|
@@ -21,6 +21,7 @@ export declare class MockResponseManager {
|
|
|
21
21
|
private responseDelay;
|
|
22
22
|
private powerState;
|
|
23
23
|
private throttles;
|
|
24
|
+
private lights;
|
|
24
25
|
private turnouts;
|
|
25
26
|
constructor(options?: MockResponseManagerOptions);
|
|
26
27
|
/**
|
|
@@ -43,6 +44,10 @@ export declare class MockResponseManager {
|
|
|
43
44
|
* Get throttle response
|
|
44
45
|
*/
|
|
45
46
|
private getThrottleResponse;
|
|
47
|
+
/**
|
|
48
|
+
* Get light response
|
|
49
|
+
*/
|
|
50
|
+
private getLightResponse;
|
|
46
51
|
/**
|
|
47
52
|
* Get turnout response
|
|
48
53
|
*/
|
|
@@ -67,6 +72,10 @@ export declare class MockResponseManager {
|
|
|
67
72
|
* Get all throttles (for testing)
|
|
68
73
|
*/
|
|
69
74
|
getThrottles(): Map<string, any>;
|
|
75
|
+
/**
|
|
76
|
+
* Get all light states (for testing)
|
|
77
|
+
*/
|
|
78
|
+
getLights(): Map<string, LightState>;
|
|
70
79
|
/**
|
|
71
80
|
* Get all turnout states (for testing)
|
|
72
81
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Event types for JmriClient EventEmitter
|
|
3
3
|
*/
|
|
4
|
-
import { PowerState, TurnoutState } from './jmri-messages.js';
|
|
4
|
+
import { PowerState, TurnoutState, LightState } from './jmri-messages.js';
|
|
5
5
|
import { ThrottleData } from './jmri-messages.js';
|
|
6
6
|
/**
|
|
7
7
|
* Connection states
|
|
@@ -24,6 +24,7 @@ export interface EventPayloads {
|
|
|
24
24
|
'error': Error;
|
|
25
25
|
'power:changed': PowerState;
|
|
26
26
|
'turnout:changed': [name: string, state: TurnoutState];
|
|
27
|
+
'light:changed': [name: string, state: LightState];
|
|
27
28
|
'throttle:acquired': string;
|
|
28
29
|
'throttle:updated': [throttleId: string, data: ThrottleData];
|
|
29
30
|
'throttle:released': string;
|
|
@@ -183,6 +183,38 @@ export interface TurnoutMessage extends JmriMessage {
|
|
|
183
183
|
type: 'turnout';
|
|
184
184
|
data?: TurnoutData;
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Light state values (from JMRI JSON protocol constants)
|
|
188
|
+
* UNKNOWN = 0 (state cannot be determined)
|
|
189
|
+
* ON = 2 (light is on)
|
|
190
|
+
* OFF = 4 (light is off)
|
|
191
|
+
*/
|
|
192
|
+
export declare enum LightState {
|
|
193
|
+
UNKNOWN = 0,
|
|
194
|
+
ON = 2,
|
|
195
|
+
OFF = 4
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Convert LightState enum to human-readable string
|
|
199
|
+
*/
|
|
200
|
+
export declare function lightStateToString(state: LightState): string;
|
|
201
|
+
/**
|
|
202
|
+
* Light data structure
|
|
203
|
+
*/
|
|
204
|
+
export interface LightData {
|
|
205
|
+
name: string;
|
|
206
|
+
userName?: string;
|
|
207
|
+
comment?: string | null;
|
|
208
|
+
properties?: any[];
|
|
209
|
+
state?: LightState;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Light message
|
|
213
|
+
*/
|
|
214
|
+
export interface LightMessage extends JmriMessage {
|
|
215
|
+
type: 'light';
|
|
216
|
+
data?: LightData;
|
|
217
|
+
}
|
|
186
218
|
/**
|
|
187
219
|
* Ping message (heartbeat)
|
|
188
220
|
*/
|
|
@@ -231,4 +263,4 @@ export interface ErrorMessage extends JmriMessage {
|
|
|
231
263
|
/**
|
|
232
264
|
* Union type of all possible JMRI messages
|
|
233
265
|
*/
|
|
234
|
-
export type AnyJmriMessage = PowerMessage | ThrottleMessage | RosterMessage | TurnoutMessage | PingMessage | PongMessage | HelloMessage | GoodbyeMessage | ErrorMessage;
|
|
266
|
+
export type AnyJmriMessage = PowerMessage | ThrottleMessage | RosterMessage | TurnoutMessage | LightMessage | PingMessage | PongMessage | HelloMessage | GoodbyeMessage | ErrorMessage;
|
package/package.json
CHANGED