jmri-client 4.2.0-beta.2 → 5.0.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 +3 -1
- package/dist/cjs/index.js +2382 -31
- package/dist/esm/index.js +2333 -17
- package/docs/BROWSER.md +4 -4
- package/docs/MIGRATION.md +30 -1
- package/package.json +17 -18
- package/dist/cjs/client.js +0 -366
- package/dist/cjs/core/connection-state-manager.js +0 -84
- package/dist/cjs/core/heartbeat-manager.js +0 -79
- package/dist/cjs/core/index.js +0 -25
- package/dist/cjs/core/message-queue.js +0 -59
- package/dist/cjs/core/reconnection-manager.js +0 -97
- package/dist/cjs/core/websocket-adapter.js +0 -135
- package/dist/cjs/core/websocket-client.js +0 -388
- package/dist/cjs/managers/index.js +0 -25
- package/dist/cjs/managers/light-manager.js +0 -111
- package/dist/cjs/managers/power-manager.js +0 -90
- package/dist/cjs/managers/roster-manager.js +0 -118
- package/dist/cjs/managers/system-connections-manager.js +0 -28
- package/dist/cjs/managers/throttle-manager.js +0 -233
- package/dist/cjs/managers/turnout-manager.js +0 -111
- package/dist/cjs/mocks/index.js +0 -12
- package/dist/cjs/mocks/mock-data.js +0 -237
- package/dist/cjs/mocks/mock-response-manager.js +0 -290
- package/dist/cjs/types/client-options.js +0 -66
- package/dist/cjs/types/events.js +0 -16
- package/dist/cjs/types/index.js +0 -23
- package/dist/cjs/types/jmri-messages.js +0 -95
- package/dist/cjs/types/throttle.js +0 -19
- package/dist/cjs/utils/exponential-backoff.js +0 -40
- package/dist/cjs/utils/index.js +0 -21
- package/dist/cjs/utils/message-id.js +0 -40
- package/dist/esm/client.js +0 -362
- package/dist/esm/core/connection-state-manager.js +0 -80
- package/dist/esm/core/heartbeat-manager.js +0 -75
- package/dist/esm/core/index.js +0 -9
- package/dist/esm/core/message-queue.js +0 -55
- package/dist/esm/core/reconnection-manager.js +0 -93
- package/dist/esm/core/websocket-adapter.js +0 -98
- package/dist/esm/core/websocket-client.js +0 -384
- package/dist/esm/managers/index.js +0 -9
- package/dist/esm/managers/light-manager.js +0 -107
- package/dist/esm/managers/power-manager.js +0 -86
- package/dist/esm/managers/roster-manager.js +0 -114
- package/dist/esm/managers/system-connections-manager.js +0 -24
- package/dist/esm/managers/throttle-manager.js +0 -229
- package/dist/esm/managers/turnout-manager.js +0 -107
- package/dist/esm/mocks/index.js +0 -6
- package/dist/esm/mocks/mock-data.js +0 -234
- package/dist/esm/mocks/mock-response-manager.js +0 -286
- package/dist/esm/types/client-options.js +0 -62
- package/dist/esm/types/events.js +0 -13
- package/dist/esm/types/index.js +0 -7
- package/dist/esm/types/jmri-messages.js +0 -89
- package/dist/esm/types/throttle.js +0 -15
- package/dist/esm/utils/exponential-backoff.js +0 -36
- package/dist/esm/utils/index.js +0 -5
- package/dist/esm/utils/message-id.js +0 -36
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Manager exports
|
|
4
|
-
*/
|
|
5
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
-
if (k2 === undefined) k2 = k;
|
|
7
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
-
}
|
|
11
|
-
Object.defineProperty(o, k2, desc);
|
|
12
|
-
}) : (function(o, m, k, k2) {
|
|
13
|
-
if (k2 === undefined) k2 = k;
|
|
14
|
-
o[k2] = m[k];
|
|
15
|
-
}));
|
|
16
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
17
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
|
-
};
|
|
19
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
__exportStar(require("./power-manager.js"), exports);
|
|
21
|
-
__exportStar(require("./roster-manager.js"), exports);
|
|
22
|
-
__exportStar(require("./throttle-manager.js"), exports);
|
|
23
|
-
__exportStar(require("./turnout-manager.js"), exports);
|
|
24
|
-
__exportStar(require("./light-manager.js"), exports);
|
|
25
|
-
__exportStar(require("./system-connections-manager.js"), exports);
|
|
@@ -1,111 +0,0 @@
|
|
|
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;
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Power control manager
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PowerManager = void 0;
|
|
7
|
-
const eventemitter3_1 = require("eventemitter3");
|
|
8
|
-
const jmri_messages_js_1 = require("../types/jmri-messages.js");
|
|
9
|
-
/**
|
|
10
|
-
* Manages track power control
|
|
11
|
-
*/
|
|
12
|
-
class PowerManager extends eventemitter3_1.EventEmitter {
|
|
13
|
-
constructor(client) {
|
|
14
|
-
super();
|
|
15
|
-
this.currentState = jmri_messages_js_1.PowerState.UNKNOWN;
|
|
16
|
-
this.client = client;
|
|
17
|
-
// Listen for power updates
|
|
18
|
-
this.client.on('update', (message) => {
|
|
19
|
-
if (message.type === 'power') {
|
|
20
|
-
this.handlePowerUpdate(message);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Get current track power state
|
|
26
|
-
* @param prefix - Optional JMRI connection prefix to target a specific hardware connection
|
|
27
|
-
*/
|
|
28
|
-
async getPower(prefix) {
|
|
29
|
-
const message = {
|
|
30
|
-
type: 'power',
|
|
31
|
-
...(prefix !== undefined && { data: { state: jmri_messages_js_1.PowerState.UNKNOWN, prefix } })
|
|
32
|
-
};
|
|
33
|
-
const response = await this.client.request(message);
|
|
34
|
-
if (response.data?.state !== undefined) {
|
|
35
|
-
this.currentState = response.data.state;
|
|
36
|
-
}
|
|
37
|
-
return this.currentState;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Set track power state
|
|
41
|
-
* @param state - The desired power state
|
|
42
|
-
* @param prefix - Optional JMRI connection prefix to target a specific hardware connection
|
|
43
|
-
*/
|
|
44
|
-
async setPower(state, prefix) {
|
|
45
|
-
const message = {
|
|
46
|
-
type: 'power',
|
|
47
|
-
method: 'post',
|
|
48
|
-
data: { state, ...(prefix !== undefined && { prefix }) }
|
|
49
|
-
};
|
|
50
|
-
await this.client.request(message);
|
|
51
|
-
const oldState = this.currentState;
|
|
52
|
-
this.currentState = state;
|
|
53
|
-
if (oldState !== this.currentState) {
|
|
54
|
-
this.emit('power:changed', this.currentState);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Turn track power on
|
|
59
|
-
* @param prefix - Optional JMRI connection prefix to target a specific hardware connection
|
|
60
|
-
*/
|
|
61
|
-
async powerOn(prefix) {
|
|
62
|
-
await this.setPower(jmri_messages_js_1.PowerState.ON, prefix);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Turn track power off
|
|
66
|
-
* @param prefix - Optional JMRI connection prefix to target a specific hardware connection
|
|
67
|
-
*/
|
|
68
|
-
async powerOff(prefix) {
|
|
69
|
-
await this.setPower(jmri_messages_js_1.PowerState.OFF, prefix);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Get cached power state (no network request)
|
|
73
|
-
*/
|
|
74
|
-
getCachedState() {
|
|
75
|
-
return this.currentState;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Handle unsolicited power updates from JMRI
|
|
79
|
-
*/
|
|
80
|
-
handlePowerUpdate(message) {
|
|
81
|
-
if (message.data?.state !== undefined) {
|
|
82
|
-
const oldState = this.currentState;
|
|
83
|
-
this.currentState = message.data.state;
|
|
84
|
-
if (oldState !== this.currentState) {
|
|
85
|
-
this.emit('power:changed', this.currentState);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
exports.PowerManager = PowerManager;
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Roster management
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.RosterManager = void 0;
|
|
7
|
-
/**
|
|
8
|
-
* Manages locomotive roster
|
|
9
|
-
*/
|
|
10
|
-
class RosterManager {
|
|
11
|
-
constructor(client) {
|
|
12
|
-
this.rosterCache = new Map();
|
|
13
|
-
this.client = client;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Get all roster entries
|
|
17
|
-
*/
|
|
18
|
-
async getRoster() {
|
|
19
|
-
const message = {
|
|
20
|
-
type: 'roster',
|
|
21
|
-
method: 'list'
|
|
22
|
-
};
|
|
23
|
-
const response = await this.client.request(message);
|
|
24
|
-
// Parse roster data
|
|
25
|
-
if (response.data) {
|
|
26
|
-
this.updateCache(response.data);
|
|
27
|
-
}
|
|
28
|
-
return Array.from(this.rosterCache.values());
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Get roster entry by name
|
|
32
|
-
*/
|
|
33
|
-
async getRosterEntryByName(name) {
|
|
34
|
-
// Check cache first
|
|
35
|
-
if (this.rosterCache.has(name)) {
|
|
36
|
-
return this.rosterCache.get(name);
|
|
37
|
-
}
|
|
38
|
-
// Refresh roster
|
|
39
|
-
await this.getRoster();
|
|
40
|
-
return this.rosterCache.get(name);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Get roster entry by address
|
|
44
|
-
*/
|
|
45
|
-
async getRosterEntryByAddress(address) {
|
|
46
|
-
// Ensure roster is loaded
|
|
47
|
-
if (this.rosterCache.size === 0) {
|
|
48
|
-
await this.getRoster();
|
|
49
|
-
}
|
|
50
|
-
const addressStr = address.toString();
|
|
51
|
-
for (const wrapper of this.rosterCache.values()) {
|
|
52
|
-
if (wrapper.data.address === addressStr) {
|
|
53
|
-
return wrapper;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Search roster by partial name match
|
|
60
|
-
*/
|
|
61
|
-
async searchRoster(query) {
|
|
62
|
-
// Ensure roster is loaded
|
|
63
|
-
if (this.rosterCache.size === 0) {
|
|
64
|
-
await this.getRoster();
|
|
65
|
-
}
|
|
66
|
-
const lowerQuery = query.toLowerCase();
|
|
67
|
-
const results = [];
|
|
68
|
-
for (const wrapper of this.rosterCache.values()) {
|
|
69
|
-
const entry = wrapper.data;
|
|
70
|
-
if (entry.name.toLowerCase().includes(lowerQuery) ||
|
|
71
|
-
entry.address.includes(query) ||
|
|
72
|
-
entry.road?.toLowerCase().includes(lowerQuery) ||
|
|
73
|
-
entry.number?.includes(query)) {
|
|
74
|
-
results.push(wrapper);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return results;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Get cached roster (no network request)
|
|
81
|
-
*/
|
|
82
|
-
getCachedRoster() {
|
|
83
|
-
return Array.from(this.rosterCache.values());
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Clear roster cache
|
|
87
|
-
*/
|
|
88
|
-
clearCache() {
|
|
89
|
-
this.rosterCache.clear();
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Update internal cache from roster data
|
|
93
|
-
*/
|
|
94
|
-
updateCache(rosterData) {
|
|
95
|
-
this.rosterCache.clear();
|
|
96
|
-
// Handle array format (real JMRI server) - store wrapped entries
|
|
97
|
-
if (Array.isArray(rosterData)) {
|
|
98
|
-
for (const wrapper of rosterData) {
|
|
99
|
-
if (wrapper.type === 'rosterEntry' && wrapper.data) {
|
|
100
|
-
this.rosterCache.set(wrapper.data.name, wrapper);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// Handle legacy keyed object format (for backward compatibility) - wrap entries
|
|
105
|
-
else {
|
|
106
|
-
let id = 1;
|
|
107
|
-
for (const [name, entry] of Object.entries(rosterData)) {
|
|
108
|
-
const wrapper = {
|
|
109
|
-
type: 'rosterEntry',
|
|
110
|
-
data: entry,
|
|
111
|
-
id: id++
|
|
112
|
-
};
|
|
113
|
-
this.rosterCache.set(name, wrapper);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
exports.RosterManager = RosterManager;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* System connections manager — discovers available JMRI hardware connection prefixes
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SystemConnectionsManager = void 0;
|
|
7
|
-
class SystemConnectionsManager {
|
|
8
|
-
constructor(client) {
|
|
9
|
-
this.client = client;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* List all available JMRI system connections and their prefixes.
|
|
13
|
-
* Use the returned prefix values with power and throttle commands to
|
|
14
|
-
* target a specific hardware connection when multiple are configured.
|
|
15
|
-
*/
|
|
16
|
-
async getSystemConnections() {
|
|
17
|
-
const message = {
|
|
18
|
-
type: 'systemConnections',
|
|
19
|
-
method: 'list'
|
|
20
|
-
};
|
|
21
|
-
const response = await this.client.request(message);
|
|
22
|
-
if (!response.data) {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
return Array.isArray(response.data) ? response.data : [response.data];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
exports.SystemConnectionsManager = SystemConnectionsManager;
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Throttle control manager
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ThrottleManager = void 0;
|
|
7
|
-
const eventemitter3_1 = require("eventemitter3");
|
|
8
|
-
const throttle_js_1 = require("../types/throttle.js");
|
|
9
|
-
/**
|
|
10
|
-
* Manages multiple throttles
|
|
11
|
-
*/
|
|
12
|
-
class ThrottleManager extends eventemitter3_1.EventEmitter {
|
|
13
|
-
constructor(client) {
|
|
14
|
-
super();
|
|
15
|
-
this.throttles = new Map();
|
|
16
|
-
this.client = client;
|
|
17
|
-
// Generate a unique client ID
|
|
18
|
-
this.clientId = `jmri-client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
19
|
-
// Listen for throttle updates
|
|
20
|
-
this.client.on('update', (message) => {
|
|
21
|
-
if (message.type === 'throttle') {
|
|
22
|
-
this.handleThrottleUpdate(message);
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
// Clean up throttles on disconnect
|
|
26
|
-
this.client.on('disconnected', () => {
|
|
27
|
-
this.handleDisconnect();
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Acquire a throttle for a locomotive
|
|
32
|
-
*/
|
|
33
|
-
async acquireThrottle(options) {
|
|
34
|
-
// Generate a unique throttle name using client ID and address
|
|
35
|
-
const throttleName = `${this.clientId}-${options.address}`;
|
|
36
|
-
const message = {
|
|
37
|
-
type: 'throttle',
|
|
38
|
-
data: {
|
|
39
|
-
name: throttleName,
|
|
40
|
-
address: options.address,
|
|
41
|
-
...(options.prefix !== undefined && { prefix: options.prefix })
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
const response = await this.client.request(message);
|
|
45
|
-
// JMRI returns the throttle ID in the "throttle" field, or we can use our name
|
|
46
|
-
const throttleId = response.data?.throttle || throttleName;
|
|
47
|
-
if (!throttleId) {
|
|
48
|
-
throw new Error('Failed to acquire throttle: no throttle ID returned');
|
|
49
|
-
}
|
|
50
|
-
// Initialize throttle state
|
|
51
|
-
const state = {
|
|
52
|
-
id: throttleId,
|
|
53
|
-
address: options.address,
|
|
54
|
-
speed: 0,
|
|
55
|
-
forward: true,
|
|
56
|
-
functions: new Map(),
|
|
57
|
-
acquired: true
|
|
58
|
-
};
|
|
59
|
-
this.throttles.set(throttleId, state);
|
|
60
|
-
this.emit('throttle:acquired', throttleId);
|
|
61
|
-
return throttleId;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Release a throttle
|
|
65
|
-
*/
|
|
66
|
-
async releaseThrottle(throttleId) {
|
|
67
|
-
const state = this.throttles.get(throttleId);
|
|
68
|
-
if (!state) {
|
|
69
|
-
throw new Error(`Throttle not found: ${throttleId}`);
|
|
70
|
-
}
|
|
71
|
-
const message = {
|
|
72
|
-
type: 'throttle',
|
|
73
|
-
data: {
|
|
74
|
-
throttle: throttleId,
|
|
75
|
-
release: null
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
await this.client.request(message);
|
|
79
|
-
this.throttles.delete(throttleId);
|
|
80
|
-
this.emit('throttle:released', throttleId);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Set throttle speed (0.0 to 1.0)
|
|
84
|
-
*/
|
|
85
|
-
async setSpeed(throttleId, speed) {
|
|
86
|
-
const state = this.throttles.get(throttleId);
|
|
87
|
-
if (!state) {
|
|
88
|
-
throw new Error(`Throttle not found: ${throttleId}`);
|
|
89
|
-
}
|
|
90
|
-
if (!(0, throttle_js_1.isValidSpeed)(speed)) {
|
|
91
|
-
throw new Error(`Invalid speed: ${speed}. Must be between 0.0 and 1.0`);
|
|
92
|
-
}
|
|
93
|
-
const message = {
|
|
94
|
-
type: 'throttle',
|
|
95
|
-
data: {
|
|
96
|
-
throttle: throttleId,
|
|
97
|
-
speed
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
// JMRI doesn't send responses for throttle control commands, just send
|
|
101
|
-
this.client.send(message);
|
|
102
|
-
state.speed = speed;
|
|
103
|
-
this.emit('throttle:updated', throttleId, { speed });
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Set throttle direction
|
|
107
|
-
*/
|
|
108
|
-
async setDirection(throttleId, forward) {
|
|
109
|
-
const state = this.throttles.get(throttleId);
|
|
110
|
-
if (!state) {
|
|
111
|
-
throw new Error(`Throttle not found: ${throttleId}`);
|
|
112
|
-
}
|
|
113
|
-
const message = {
|
|
114
|
-
type: 'throttle',
|
|
115
|
-
data: {
|
|
116
|
-
throttle: throttleId,
|
|
117
|
-
forward
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
// JMRI doesn't send responses for throttle control commands, just send
|
|
121
|
-
this.client.send(message);
|
|
122
|
-
state.forward = forward;
|
|
123
|
-
this.emit('throttle:updated', throttleId, { forward });
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Set throttle function (F0-F28)
|
|
127
|
-
*/
|
|
128
|
-
async setFunction(throttleId, functionKey, value) {
|
|
129
|
-
const state = this.throttles.get(throttleId);
|
|
130
|
-
if (!state) {
|
|
131
|
-
throw new Error(`Throttle not found: ${throttleId}`);
|
|
132
|
-
}
|
|
133
|
-
if (!(0, throttle_js_1.isThrottleFunctionKey)(functionKey)) {
|
|
134
|
-
throw new Error(`Invalid function key: ${functionKey}`);
|
|
135
|
-
}
|
|
136
|
-
const data = {
|
|
137
|
-
throttle: throttleId,
|
|
138
|
-
[functionKey]: value
|
|
139
|
-
};
|
|
140
|
-
const message = {
|
|
141
|
-
type: 'throttle',
|
|
142
|
-
data
|
|
143
|
-
};
|
|
144
|
-
// JMRI doesn't send responses for throttle control commands, just send
|
|
145
|
-
this.client.send(message);
|
|
146
|
-
state.functions.set(functionKey, value);
|
|
147
|
-
this.emit('throttle:updated', throttleId, { [functionKey]: value });
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Emergency stop for a throttle (speed to 0)
|
|
151
|
-
*/
|
|
152
|
-
async emergencyStop(throttleId) {
|
|
153
|
-
await this.setSpeed(throttleId, 0);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Set throttle to idle (speed to 0, maintain direction)
|
|
157
|
-
*/
|
|
158
|
-
async idle(throttleId) {
|
|
159
|
-
await this.setSpeed(throttleId, 0);
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Get throttle state
|
|
163
|
-
*/
|
|
164
|
-
getThrottleState(throttleId) {
|
|
165
|
-
return this.throttles.get(throttleId);
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Get all throttle IDs
|
|
169
|
-
*/
|
|
170
|
-
getThrottleIds() {
|
|
171
|
-
return Array.from(this.throttles.keys());
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Get all throttle states
|
|
175
|
-
*/
|
|
176
|
-
getAllThrottles() {
|
|
177
|
-
return Array.from(this.throttles.values());
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Release all throttles
|
|
181
|
-
*/
|
|
182
|
-
async releaseAllThrottles() {
|
|
183
|
-
const throttleIds = this.getThrottleIds();
|
|
184
|
-
for (const throttleId of throttleIds) {
|
|
185
|
-
try {
|
|
186
|
-
await this.releaseThrottle(throttleId);
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
// Continue releasing others even if one fails
|
|
190
|
-
this.emit('error', error);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Handle unsolicited throttle updates from JMRI
|
|
196
|
-
*/
|
|
197
|
-
handleThrottleUpdate(message) {
|
|
198
|
-
const throttleId = message.data?.throttle;
|
|
199
|
-
if (!throttleId) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
const state = this.throttles.get(throttleId);
|
|
203
|
-
if (!state) {
|
|
204
|
-
// Unknown throttle, possibly lost
|
|
205
|
-
this.emit('throttle:lost', throttleId);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
// Update state from message
|
|
209
|
-
if (message.data.speed !== undefined) {
|
|
210
|
-
state.speed = message.data.speed;
|
|
211
|
-
}
|
|
212
|
-
if (message.data.forward !== undefined) {
|
|
213
|
-
state.forward = message.data.forward;
|
|
214
|
-
}
|
|
215
|
-
// Update functions
|
|
216
|
-
for (let i = 0; i <= 28; i++) {
|
|
217
|
-
const key = `F${i}`;
|
|
218
|
-
if (message.data[key] !== undefined) {
|
|
219
|
-
state.functions.set(key, message.data[key]);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
this.emit('throttle:updated', throttleId, message.data);
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Handle disconnect - mark all throttles as not acquired
|
|
226
|
-
*/
|
|
227
|
-
handleDisconnect() {
|
|
228
|
-
for (const state of this.throttles.values()) {
|
|
229
|
-
state.acquired = false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
exports.ThrottleManager = ThrottleManager;
|