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 CHANGED
@@ -1,7 +1,16 @@
1
1
  # z21-client
2
2
 
3
- z21-client is a Node.js library written in TypeScript that implements the UDP protocol of the Roco/Fleischmann Z21 DCC command station.
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
+
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
@@ -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,7 +86,7 @@ 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
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
- this.transport.sendCommand(payload);
117
- this.transport.on("cvResult", (msg) => {
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
- this.transport.on("error", (msg) => {
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
- this.transport.sendCommand(payload);
150
- this.transport.on("cvResult", (msg) => {
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
- this.transport.on("error", (msg) => {
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(`[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.0.0",
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
  ],