z21-client 1.1.0 → 1.1.2
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 +8 -0
- package/dist/Z21Client.d.ts +3 -0
- package/dist/Z21Client.js +5 -1
- package/dist/controllers/EngineController.js +55 -12
- 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 -1
package/README.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# z21-client
|
|
2
2
|
|
|
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
|
+
|
|
3
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.
|
|
5
13
|
Connection to the Z21 command station is performed over the LAN (Ethernet/UDP).
|
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,10 +86,13 @@ 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
|
-
// Set
|
|
92
|
-
|
|
91
|
+
// Set function number in bits 0-5 per Z21 LAN protocol specification
|
|
92
|
+
// See: Z21 LAN Protocol v1.13, Section 4.2 (LAN_X_SET_LOCO_FUNCTION)
|
|
93
|
+
// DB3 byte format: bits 7-6 = switch type (00=off, 01=on, 10=toggle), bits 5-0 = function index
|
|
94
|
+
// Reference: https://www.z21.eu/media/Kwc_Basic_DownloadTag_Component/root-en-main_47-702/default/69bad87e/1712141518/z21-lan-protokoll-en.pdf
|
|
95
|
+
functionByte |= (functionNumber & 0x3F);
|
|
93
96
|
// Build the command payload
|
|
94
97
|
// E4 = set function command, 0x40 = LAN_X header, 0x00 = XpressNet command
|
|
95
98
|
// Payload format: [0x40, 0x00, 0xE4, addrH, addrL, functionByte]
|
|
@@ -113,16 +116,36 @@ class EngineController {
|
|
|
113
116
|
xpressNetFrame.push(xor);
|
|
114
117
|
// LAN_X header: [0x40, 0x00]
|
|
115
118
|
const payload = [0x40, 0x00, ...xpressNetFrame];
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
const timeoutMs = 30000; // 30 seconds
|
|
120
|
+
let timer = setTimeout(() => {
|
|
121
|
+
cleanup();
|
|
122
|
+
reject(new Error("cvRead timeout"));
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
const onCv = (msg) => {
|
|
118
125
|
if (msg.cv === cv) {
|
|
126
|
+
cleanup();
|
|
119
127
|
resolve(msg);
|
|
120
128
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
129
|
+
};
|
|
130
|
+
const onError = (msg) => {
|
|
123
131
|
if (msg.code === "nack" || msg.code === "nack-sc") {
|
|
132
|
+
cleanup();
|
|
124
133
|
reject(msg);
|
|
125
134
|
}
|
|
135
|
+
};
|
|
136
|
+
const cleanup = () => {
|
|
137
|
+
if (timer) {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
timer = null;
|
|
140
|
+
}
|
|
141
|
+
this.transport.removeListener("cvResult", onCv);
|
|
142
|
+
this.transport.removeListener("error", onError);
|
|
143
|
+
};
|
|
144
|
+
this.transport.on("cvResult", onCv);
|
|
145
|
+
this.transport.on("error", onError);
|
|
146
|
+
this.transport.sendCommand(payload).catch((err) => {
|
|
147
|
+
cleanup();
|
|
148
|
+
reject(err);
|
|
126
149
|
});
|
|
127
150
|
});
|
|
128
151
|
}
|
|
@@ -146,16 +169,36 @@ class EngineController {
|
|
|
146
169
|
xpressNetFrame.push(xor);
|
|
147
170
|
// LAN_X header: [0x40, 0x00]
|
|
148
171
|
const payload = [0x40, 0x00, ...xpressNetFrame];
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
const timeoutMs = 30000; // 30 seconds
|
|
173
|
+
let timer = setTimeout(() => {
|
|
174
|
+
cleanup();
|
|
175
|
+
reject(new Error("cvWrite timeout"));
|
|
176
|
+
}, timeoutMs);
|
|
177
|
+
const onCv = (msg) => {
|
|
151
178
|
if (msg.cv === cv) {
|
|
179
|
+
cleanup();
|
|
152
180
|
resolve(msg);
|
|
153
181
|
}
|
|
154
|
-
}
|
|
155
|
-
|
|
182
|
+
};
|
|
183
|
+
const onError = (msg) => {
|
|
156
184
|
if (msg.code === "nack" || msg.code === "nack-sc") {
|
|
185
|
+
cleanup();
|
|
157
186
|
reject(msg);
|
|
158
187
|
}
|
|
188
|
+
};
|
|
189
|
+
const cleanup = () => {
|
|
190
|
+
if (timer) {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
timer = null;
|
|
193
|
+
}
|
|
194
|
+
this.transport.removeListener("cvResult", onCv);
|
|
195
|
+
this.transport.removeListener("error", onError);
|
|
196
|
+
};
|
|
197
|
+
this.transport.on("cvResult", onCv);
|
|
198
|
+
this.transport.on("error", onError);
|
|
199
|
+
this.transport.sendCommand(payload).catch((err) => {
|
|
200
|
+
cleanup();
|
|
201
|
+
reject(err);
|
|
159
202
|
});
|
|
160
203
|
});
|
|
161
204
|
}
|
|
@@ -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.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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
|
},
|