z21-client 1.0.0 → 1.1.1
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 +11 -1
- package/dist/Z21Client.d.ts +3 -0
- package/dist/Z21Client.js +5 -1
- package/dist/controllers/EngineController.js +50 -10
- package/dist/parsers/feedbackParser.js +1 -1
- package/dist/parsers/lanParser.js +25 -0
- package/dist/parsers/lanXParser.js +1 -0
- package/dist/parsers/parserResult.d.ts +9 -1
- package/dist/transport/Z21UdpTransport.js +1 -2
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# z21-client
|
|
2
2
|
|
|
3
|
-
z21-client
|
|
3
|
+
[](https://www.npmjs.com/package/z21-client)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://github.com/nmeunier/z21-client/actions)
|
|
7
|
+
[](https://codecov.io/gh/nmeunier/z21-client)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
z21-client is a Node.js library written in TypeScript that implements the Roco/Fleischmann Z21 DCC command station.
|
|
4
12
|
It provides a strongly-typed, event-driven API to control locomotives and accessories, read and write CVs, and monitor system events in real time.
|
|
13
|
+
Connection to the Z21 command station is performed over the LAN (Ethernet/UDP).
|
|
5
14
|
|
|
6
15
|
---
|
|
7
16
|
|
|
@@ -109,6 +118,7 @@ new Z21Client(host: string, port?: number, debug?: boolean)
|
|
|
109
118
|
- `"engineInfo"`: Engine info updates
|
|
110
119
|
- `"cvResult"`: CV read/write result
|
|
111
120
|
- `"accessoryInfo"`: Accessory/turnout info
|
|
121
|
+
- `"feedback"`: Feedback module updates ()
|
|
112
122
|
- `"unknownBroadcast"`: Unknown broadcast received
|
|
113
123
|
- `"error"`: UDP or protocol errors
|
|
114
124
|
- `"debug"`: Debug messages
|
package/dist/Z21Client.d.ts
CHANGED
package/dist/Z21Client.js
CHANGED
|
@@ -34,11 +34,15 @@ class Z21Client extends events_1.EventEmitter {
|
|
|
34
34
|
this.transport.on("cvResult", (msg) => this.emit("cvResult", msg));
|
|
35
35
|
this.transport.on("accessoryInfo", (msg) => this.emit("accessoryInfo", msg));
|
|
36
36
|
this.transport.on("unknownBroadcast", (err) => this.emit("unknownBroadcast", err));
|
|
37
|
+
this.transport.on("feedback", (msg) => this.emit("feedback", msg));
|
|
37
38
|
this.transport.on("error", (err) => this.emit("error", err));
|
|
38
39
|
if (debug) {
|
|
39
|
-
console.log(
|
|
40
|
+
console.log("[Z21Client] Init");
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Close the Z21Client connection.
|
|
45
|
+
*/
|
|
42
46
|
async close() {
|
|
43
47
|
await this.system.logout();
|
|
44
48
|
await this.transport.close();
|
|
@@ -48,7 +48,7 @@ class EngineController {
|
|
|
48
48
|
steps = 0x10; // DCC14 for 14-speed
|
|
49
49
|
}
|
|
50
50
|
// XpressNet frame: E4 = drive engine command
|
|
51
|
-
|
|
51
|
+
const xpressNetFrame = [0xE4, steps, addrH, addrL, speedByte];
|
|
52
52
|
// --- Calculate XOR checksum ---
|
|
53
53
|
let xor = 0;
|
|
54
54
|
for (const byte of xpressNetFrame) {
|
|
@@ -86,7 +86,7 @@ class EngineController {
|
|
|
86
86
|
functionByte = 0x80;
|
|
87
87
|
break;
|
|
88
88
|
default:
|
|
89
|
-
throw new Error(
|
|
89
|
+
throw new Error("state must be \"on\", \"off\" or \"toggle\"");
|
|
90
90
|
}
|
|
91
91
|
// Set the specific function bit
|
|
92
92
|
functionByte |= (1 << (functionNumber - 1)); // Set the bit corresponding to the function number
|
|
@@ -113,16 +113,36 @@ class EngineController {
|
|
|
113
113
|
xpressNetFrame.push(xor);
|
|
114
114
|
// LAN_X header: [0x40, 0x00]
|
|
115
115
|
const payload = [0x40, 0x00, ...xpressNetFrame];
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const timeoutMs = 30000; // 30 seconds
|
|
117
|
+
let timer = setTimeout(() => {
|
|
118
|
+
cleanup();
|
|
119
|
+
reject(new Error("cvRead timeout"));
|
|
120
|
+
}, timeoutMs);
|
|
121
|
+
const onCv = (msg) => {
|
|
118
122
|
if (msg.cv === cv) {
|
|
123
|
+
cleanup();
|
|
119
124
|
resolve(msg);
|
|
120
125
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
126
|
+
};
|
|
127
|
+
const onError = (msg) => {
|
|
123
128
|
if (msg.code === "nack" || msg.code === "nack-sc") {
|
|
129
|
+
cleanup();
|
|
124
130
|
reject(msg);
|
|
125
131
|
}
|
|
132
|
+
};
|
|
133
|
+
const cleanup = () => {
|
|
134
|
+
if (timer) {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
timer = null;
|
|
137
|
+
}
|
|
138
|
+
this.transport.removeListener("cvResult", onCv);
|
|
139
|
+
this.transport.removeListener("error", onError);
|
|
140
|
+
};
|
|
141
|
+
this.transport.on("cvResult", onCv);
|
|
142
|
+
this.transport.on("error", onError);
|
|
143
|
+
this.transport.sendCommand(payload).catch((err) => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(err);
|
|
126
146
|
});
|
|
127
147
|
});
|
|
128
148
|
}
|
|
@@ -146,16 +166,36 @@ class EngineController {
|
|
|
146
166
|
xpressNetFrame.push(xor);
|
|
147
167
|
// LAN_X header: [0x40, 0x00]
|
|
148
168
|
const payload = [0x40, 0x00, ...xpressNetFrame];
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
const timeoutMs = 30000; // 30 seconds
|
|
170
|
+
let timer = setTimeout(() => {
|
|
171
|
+
cleanup();
|
|
172
|
+
reject(new Error("cvWrite timeout"));
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
const onCv = (msg) => {
|
|
151
175
|
if (msg.cv === cv) {
|
|
176
|
+
cleanup();
|
|
152
177
|
resolve(msg);
|
|
153
178
|
}
|
|
154
|
-
}
|
|
155
|
-
|
|
179
|
+
};
|
|
180
|
+
const onError = (msg) => {
|
|
156
181
|
if (msg.code === "nack" || msg.code === "nack-sc") {
|
|
182
|
+
cleanup();
|
|
157
183
|
reject(msg);
|
|
158
184
|
}
|
|
185
|
+
};
|
|
186
|
+
const cleanup = () => {
|
|
187
|
+
if (timer) {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
timer = null;
|
|
190
|
+
}
|
|
191
|
+
this.transport.removeListener("cvResult", onCv);
|
|
192
|
+
this.transport.removeListener("error", onError);
|
|
193
|
+
};
|
|
194
|
+
this.transport.on("cvResult", onCv);
|
|
195
|
+
this.transport.on("error", onError);
|
|
196
|
+
this.transport.sendCommand(payload).catch((err) => {
|
|
197
|
+
cleanup();
|
|
198
|
+
reject(err);
|
|
159
199
|
});
|
|
160
200
|
});
|
|
161
201
|
}
|
|
@@ -35,7 +35,7 @@ class FeedbackParser {
|
|
|
35
35
|
// LAN_X command: remove first 2 bytes of LAN_X header
|
|
36
36
|
return this.lanXParser.parse(Buffer.from(data.subarray(2)));
|
|
37
37
|
}
|
|
38
|
-
else if (data[0] === 0x10 || data[0] === 0x51) {
|
|
38
|
+
else if (data[0] === 0x10 || data[0] === 0x51 || data[0] === 0x80) {
|
|
39
39
|
// LAN command
|
|
40
40
|
return this.lanParser.parse(data[0], data.subarray(2));
|
|
41
41
|
}
|
|
@@ -37,6 +37,31 @@ class LanParser {
|
|
|
37
37
|
console.log("[LAN Parser] Payload too short for GET_BROADCAST_FLAGS");
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
|
+
case 0x80: // LAN_RMBUS_DATACHANGED
|
|
41
|
+
// Structure: [groupIndex (1 byte), feedback status (10 bytes)]
|
|
42
|
+
if (payload.length >= 11) {
|
|
43
|
+
const groupIndex = payload[0];
|
|
44
|
+
const feedbackStatus = payload.subarray(1, 11); // 10 bytes
|
|
45
|
+
const feedbacks = [];
|
|
46
|
+
for (let i = 0; i < 10; i++) {
|
|
47
|
+
const moduleAddress = groupIndex * 10 + (i + 1);
|
|
48
|
+
const byte = feedbackStatus[i];
|
|
49
|
+
if (byte) {
|
|
50
|
+
const activeInputs = [];
|
|
51
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
52
|
+
if (byte & (1 << bit)) {
|
|
53
|
+
activeInputs.push(bit + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
feedbacks.push({ address: moduleAddress, activeInputs });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
type: "feedback",
|
|
61
|
+
value: feedbacks
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
40
65
|
default:
|
|
41
66
|
return null;
|
|
42
67
|
}
|
|
@@ -112,6 +112,7 @@ class LanXParser {
|
|
|
112
112
|
const direction = (db3 & 0x80) ? "forward" : "reverse";
|
|
113
113
|
const speed = db3 & 0x7F;
|
|
114
114
|
const db4 = payload[4];
|
|
115
|
+
// Not officially documented in Z21/LAN_X protocol; may always be false
|
|
115
116
|
const doubleTraction = (db4 & 0b01000000) !== 0;
|
|
116
117
|
const functions = {
|
|
117
118
|
F0: (db4 & 0b00010000) !== 0,
|
|
@@ -74,7 +74,15 @@ export interface CvResultData {
|
|
|
74
74
|
cv: number;
|
|
75
75
|
value: number;
|
|
76
76
|
}
|
|
77
|
+
export interface FeedbackModuleStatus {
|
|
78
|
+
address: number;
|
|
79
|
+
activeInputs: number[];
|
|
80
|
+
}
|
|
81
|
+
export interface FeedbackResult {
|
|
82
|
+
type: "feedback";
|
|
83
|
+
value: FeedbackModuleStatus[];
|
|
84
|
+
}
|
|
77
85
|
/**
|
|
78
86
|
* Union type for all possible results
|
|
79
87
|
*/
|
|
80
|
-
export type ParserResult = ErrorResult | SerialNumberResult | BroadcastFlagsResult | StatusResult | TrackPowerResult | ProgrammingModeResult | ShortCircuitResult | UnknownBroadcastResult | AccessoryInfoResult | EngineInfoResult | CvResult;
|
|
88
|
+
export type ParserResult = ErrorResult | SerialNumberResult | BroadcastFlagsResult | StatusResult | TrackPowerResult | ProgrammingModeResult | ShortCircuitResult | UnknownBroadcastResult | AccessoryInfoResult | EngineInfoResult | CvResult | FeedbackResult;
|
|
@@ -23,7 +23,7 @@ class Z21UdpTransport extends events_1.EventEmitter {
|
|
|
23
23
|
this.close();
|
|
24
24
|
});
|
|
25
25
|
if (this.debug) {
|
|
26
|
-
console.log(
|
|
26
|
+
console.log("[Z21UdpTransport] Init");
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
@@ -61,7 +61,6 @@ class Z21UdpTransport extends events_1.EventEmitter {
|
|
|
61
61
|
*/
|
|
62
62
|
async sendCommand(payload) {
|
|
63
63
|
const frame = this.buildFrame(payload);
|
|
64
|
-
console.log(frame);
|
|
65
64
|
await this.sendFrame(frame);
|
|
66
65
|
}
|
|
67
66
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z21-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "TypeScript UDP client for Roco/Fleischmann Z21 DCC command station",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"dev": "ts-node-dev --respawn exemples/index.ts",
|
|
14
|
+
"lint": "eslint 'src/**/*.{ts,js}' --ext .ts",
|
|
14
15
|
"test": "jest",
|
|
15
16
|
"test:coverage": "jest --coverage"
|
|
16
17
|
},
|
|
@@ -21,14 +22,12 @@
|
|
|
21
22
|
"fleischmann",
|
|
22
23
|
"model-railway",
|
|
23
24
|
"model-train",
|
|
24
|
-
"railroad",
|
|
25
25
|
"train-control",
|
|
26
26
|
"locomotive",
|
|
27
27
|
"turnout",
|
|
28
28
|
"accessory",
|
|
29
29
|
"decoder",
|
|
30
30
|
"cv",
|
|
31
|
-
"udp",
|
|
32
31
|
"typescript",
|
|
33
32
|
"nodejs"
|
|
34
33
|
],
|