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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # z21-client
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/z21-client)](https://www.npmjs.com/package/z21-client)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
6
+ [![Tests](https://github.com/nmeunier/z21-client/workflows/Tests%20%26%20Build/badge.svg)](https://github.com/nmeunier/z21-client/actions)
7
+ [![codecov](https://codecov.io/gh/nmeunier/z21-client/branch/main/graph/badge.svg)](https://codecov.io/gh/nmeunier/z21-client)
8
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen.svg)](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).
@@ -14,5 +14,8 @@ export declare class Z21Client extends EventEmitter {
14
14
  * @param debug Enable debug mode (default: false).
15
15
  */
16
16
  constructor(host: string, port?: number, debug?: boolean);
17
+ /**
18
+ * Close the Z21Client connection.
19
+ */
17
20
  close(): Promise<void>;
18
21
  }
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(`[Z21Client] Init`);
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
- let xpressNetFrame = [0xE4, steps, addrH, addrL, speedByte];
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('state must be "on", "off" or "toggle"');
89
+ throw new Error("state must be \"on\", \"off\" or \"toggle\"");
90
90
  }
91
- // Set the specific function bit
92
- functionByte |= (1 << (functionNumber - 1)); // Set the bit corresponding to the function number
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
- this.transport.sendCommand(payload);
117
- this.transport.on("cvResult", (msg) => {
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
- this.transport.on("error", (msg) => {
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
- this.transport.sendCommand(payload);
150
- this.transport.on("cvResult", (msg) => {
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
- this.transport.on("error", (msg) => {
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(`[Z21UdpTransport] Init`);
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.0",
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
  },