z21-client 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nicolas Meunier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # z21-client
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.
4
+ 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
+
6
+ ---
7
+
8
+ ## Features
9
+
10
+ - Send and receive UDP commands to/from a Z21 command station
11
+ - Control track power, emergency stop, turnouts, and engine functions
12
+ - Drive engines with speed and direction
13
+ - Read and write CVs (configuration variables)
14
+ - Query status, serial number, and broadcast flags
15
+ - Subscribe to feedback and status updates
16
+ - Fully typed API for Node.js
17
+ - Extensive unit tests
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ npm install z21-client
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Usage Example
30
+
31
+ ```typescript
32
+ import { Z21Client } from "z21-client";
33
+
34
+ const z21 = new Z21Client("192.168.0.111", 21105);
35
+
36
+ z21.on("status", (status) => {
37
+ console.log("Z21 status:", status);
38
+ });
39
+
40
+ (async () => {
41
+ await z21.system.setTrackPowerOn();
42
+ await z21.system.getStatus();
43
+ z21.engines.setDriveEngine(3, 50, true); // address, speed, forward
44
+ await z21.engines.setEngineFunctions(3, 1, "on");
45
+ await z21.engines.cvWrite(17, 192);
46
+ const cvResult = await z21.engines.cvRead(17);
47
+ await z21.accessories.switchTurnout(5, true); // address 5, output 2 (true), activate (default)
48
+ })();
49
+ ```
50
+
51
+ ---
52
+
53
+ ## API
54
+
55
+ ### Constructor
56
+
57
+ ```typescript
58
+ new Z21Client(host: string, port?: number, debug?: boolean)
59
+ ```
60
+
61
+ - `host`: IP address of your Z21 (e.g. `"192.168.0.111"`)
62
+ - `port`: UDP port (default: `21105`)
63
+ - `debug`: Enable debug logs (default: `false`)
64
+
65
+ ---
66
+
67
+ ### System Controller
68
+
69
+ - `system.setTrackPowerOn()`: Turn track power on
70
+ - `system.setTrackPowerOff()`: Turn track power off
71
+ - `system.emergencyStop()`: Emergency stop
72
+ - `system.setBroadcastFlags(engine?: boolean, accessory?: boolean, feedback?: boolean)`: Set broadcast flags
73
+ - `system.getBroadcastFlags()`: Get broadcast flags
74
+ - `system.getSerialNumber()`: Get Z21 serial number
75
+ - `system.getStatus()`: Get Z21 status
76
+
77
+ ---
78
+
79
+ ### Engine Controller
80
+
81
+ - `engines.getEngineInfo(address: number)`: Request information about an engine and subscribe to updates
82
+ - `engines.setDriveEngine(address: number, speed: number, forward: boolean, engineSpeedSteps?: number)`: Drive an engine with speed and direction (speed steps: 14, 28, or 128)
83
+ - `engines.setEngineFunctions(address: number, functionNumber: number, state: "on" | "off" | "toggle")`: Set a function state on an engine (F1-F28)
84
+ - `engines.cvRead(cv: number)`: Read a CV in direct mode
85
+ - `engines.cvWrite(cv: number, value: number)`: Write a CV in direct mode
86
+
87
+ ---
88
+
89
+ ### Accessory Controller
90
+
91
+ - `accessories.switchTurnout(address: number, output?: boolean, activate?: boolean, queue?: boolean)`: Switch turnout/accessory
92
+
93
+ ---
94
+
95
+ ### General
96
+
97
+ - `close()`: Close UDP socket and logout
98
+
99
+ ---
100
+
101
+ ## Events
102
+
103
+ - `"status"`: Z21 status updates
104
+ - `"broadcastFlags"`: Broadcast flags updates
105
+ - `"serialNumber"`: Serial number received
106
+ - `"trackPower"`: Track power state
107
+ - `"programmingMode"`: Programming mode state
108
+ - `"shortCircuit"`: Short circuit detected
109
+ - `"engineInfo"`: Engine info updates
110
+ - `"cvResult"`: CV read/write result
111
+ - `"accessoryInfo"`: Accessory/turnout info
112
+ - `"unknownBroadcast"`: Unknown broadcast received
113
+ - `"error"`: UDP or protocol errors
114
+ - `"debug"`: Debug messages
115
+
116
+ ---
117
+
118
+ ## TypeScript
119
+
120
+ This library is fully typed. All types and interfaces are included automatically.
121
+
122
+ ---
123
+
124
+ ## Development & Testing
125
+
126
+ - Run all tests:
127
+ ```sh
128
+ npm test
129
+ ```
130
+ - Run tests with coverage:
131
+ ```sh
132
+ npm run test:coverage
133
+ ```
134
+
135
+ ---
136
+
137
+ ## License
138
+
139
+ MIT
140
+
141
+ ---
142
+
143
+ ## Author
144
+
145
+ Nicolas Meunier
146
+
147
+ ---
148
+
149
+ ## Contributing
150
+
151
+ Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
152
+
153
+ ---
154
+
155
+ ## Links
156
+
157
+ - [Roco Z21 Documentation (EN)](https://www.z21.eu/en/downloads)
158
+ - [Node.js](https://nodejs.org/)
159
+ - [TypeScript](https://www.typescriptlang.org/)
160
+
161
+ ---
162
+
@@ -0,0 +1,18 @@
1
+ import { EventEmitter } from "events";
2
+ import { EngineController } from "./controllers/EngineController";
3
+ import { AccessoryController } from "./controllers/AccessoryController";
4
+ import { SystemController } from "./controllers/SystemController";
5
+ export declare class Z21Client extends EventEmitter {
6
+ readonly engines: EngineController;
7
+ readonly accessories: AccessoryController;
8
+ readonly system: SystemController;
9
+ private readonly transport;
10
+ /**
11
+ * Create a new Z21Client instance.
12
+ * @param host The hostname or IP address of the Z21 device.
13
+ * @param port The port number to connect to (default: 21105).
14
+ * @param debug Enable debug mode (default: false).
15
+ */
16
+ constructor(host: string, port?: number, debug?: boolean);
17
+ close(): Promise<void>;
18
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Z21Client = void 0;
4
+ // src/Z21Client.ts
5
+ const events_1 = require("events");
6
+ const Z21UdpTransport_1 = require("./transport/Z21UdpTransport");
7
+ const EngineController_1 = require("./controllers/EngineController");
8
+ const AccessoryController_1 = require("./controllers/AccessoryController");
9
+ const SystemController_1 = require("./controllers/SystemController");
10
+ const feedbackParser_1 = require("./parsers/feedbackParser");
11
+ class Z21Client extends events_1.EventEmitter {
12
+ /**
13
+ * Create a new Z21Client instance.
14
+ * @param host The hostname or IP address of the Z21 device.
15
+ * @param port The port number to connect to (default: 21105).
16
+ * @param debug Enable debug mode (default: false).
17
+ */
18
+ constructor(host, port = 21105, debug = false) {
19
+ super();
20
+ this.transport = new Z21UdpTransport_1.Z21UdpTransport(host, port, debug, new feedbackParser_1.FeedbackParser());
21
+ // Initialize controllers
22
+ this.engines = new EngineController_1.EngineController(this.transport);
23
+ this.accessories = new AccessoryController_1.AccessoryController(this.transport);
24
+ this.system = new SystemController_1.SystemController(this.transport);
25
+ // Forward transport events to Z21Client
26
+ this.transport.on("debug", (msg) => this.emit("debug", msg));
27
+ this.transport.on("serialNumber", (msg) => this.emit("serialNumber", msg));
28
+ this.transport.on("broadcastFlags", (msg) => this.emit("broadcastFlags", msg));
29
+ this.transport.on("status", (msg) => this.emit("status", msg));
30
+ this.transport.on("trackPower", (msg) => this.emit("trackPower", msg));
31
+ this.transport.on("programmingMode", (msg) => this.emit("programmingMode", msg));
32
+ this.transport.on("shortCircuit", (msg) => this.emit("shortCircuit", msg));
33
+ this.transport.on("engineInfo", (msg) => this.emit("engineInfo", msg));
34
+ this.transport.on("cvResult", (msg) => this.emit("cvResult", msg));
35
+ this.transport.on("accessoryInfo", (msg) => this.emit("accessoryInfo", msg));
36
+ this.transport.on("unknownBroadcast", (err) => this.emit("unknownBroadcast", err));
37
+ this.transport.on("error", (err) => this.emit("error", err));
38
+ if (debug) {
39
+ console.log(`[Z21Client] Init`);
40
+ }
41
+ }
42
+ async close() {
43
+ await this.system.logout();
44
+ await this.transport.close();
45
+ }
46
+ }
47
+ exports.Z21Client = Z21Client;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Commands payloads (no length)
3
+ */
4
+ export declare const commands: {
5
+ LAN_LOGOFF: number[];
6
+ LAN_GET_SERIAL_NUMBER: number[];
7
+ LAN_GET_BROADCAST_FLAGS: number[];
8
+ LAN_SET_BROADCAST_FLAGS: number[];
9
+ LAN_X_GET_STATUS: number[];
10
+ LAN_X_GET_VERSION: number[];
11
+ LAN_X_TRACK_POWER_OFF: number[];
12
+ LAN_X_TRACK_POWER_ON: number[];
13
+ LAN_X_SET_STOP: number[];
14
+ };
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commands = void 0;
4
+ /**
5
+ * Commands payloads (no length)
6
+ */
7
+ exports.commands = {
8
+ LAN_LOGOFF: [0x40, 0x00, 0x30, 0x00],
9
+ LAN_GET_SERIAL_NUMBER: [0x10, 0x00],
10
+ LAN_GET_BROADCAST_FLAGS: [0x51, 0x00],
11
+ LAN_SET_BROADCAST_FLAGS: [0x50, 0x00],
12
+ LAN_X_GET_STATUS: [0x40, 0x00, 0x21, 0x24, 0x05],
13
+ LAN_X_GET_VERSION: [0x40, 0x00, 0x21, 0x21, 0x00],
14
+ LAN_X_TRACK_POWER_OFF: [0x40, 0x00, 0x21, 0x80, 0xa1],
15
+ LAN_X_TRACK_POWER_ON: [0x40, 0x00, 0x21, 0x81, 0xa0],
16
+ LAN_X_SET_STOP: [0x80, 0x80],
17
+ };
@@ -0,0 +1,13 @@
1
+ import { Z21UdpTransport } from "../transport/Z21UdpTransport";
2
+ export declare class AccessoryController {
3
+ private transport;
4
+ constructor(transport: Z21UdpTransport);
5
+ /**
6
+ * Switch a turnout (accessory) using LAN_X_SET_TURNOUT.
7
+ * @param address Turnout address (1-2047)
8
+ * @param output Output: false = output 1, true = output 2 (default: false)
9
+ * @param activate true to activate, false to deactivate (default: true)
10
+ * @param queue Set to true to queue the command (default: false)
11
+ */
12
+ switchTurnout(address: number, output?: boolean, activate?: boolean, queue?: boolean): Promise<void>;
13
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccessoryController = void 0;
4
+ class AccessoryController {
5
+ constructor(transport) {
6
+ this.transport = transport;
7
+ }
8
+ /**
9
+ * Switch a turnout (accessory) using LAN_X_SET_TURNOUT.
10
+ * @param address Turnout address (1-2047)
11
+ * @param output Output: false = output 1, true = output 2 (default: false)
12
+ * @param activate true to activate, false to deactivate (default: true)
13
+ * @param queue Set to true to queue the command (default: false)
14
+ */
15
+ async switchTurnout(address, output = false, activate = true, queue = false) {
16
+ address = address - 1; // Convert to 0-based address
17
+ // Address split
18
+ const FAdr_MSB = (address >> 8) & 0xFF;
19
+ const FAdr_LSB = address & 0xFF;
20
+ // 10Q0A00P
21
+ // Bit 7: 1 (always)
22
+ // Bit 6: 0 (always)
23
+ // Bit 5: Q (queue)
24
+ // Bit 4: 0 (always)
25
+ // Bit 3: A (activate, 1=activate, 0=deactivate)
26
+ // Bits 2-1: 0 (always)
27
+ // Bit 0: P (output)
28
+ let db2 = 0x80; // Bit 7 set
29
+ if (queue)
30
+ db2 |= 0x20; // Q = bit 5
31
+ if (activate)
32
+ db2 |= 0x08; // A = bit 3
33
+ if (output)
34
+ db2 |= 0x01; // P = bit 0 (true = output 2, false = output 1)
35
+ // Build XpressNet frame: [0x53, FAdr_MSB, FAdr_LSB, db2]
36
+ const xpressNetFrame = [0x53, FAdr_MSB, FAdr_LSB, db2];
37
+ // XOR checksum for XpressNet frame
38
+ let xor = 0;
39
+ for (const byte of xpressNetFrame)
40
+ xor ^= byte;
41
+ xpressNetFrame.push(xor);
42
+ // LAN_X header: [0x40, 0x00]
43
+ const payload = [0x40, 0x00, ...xpressNetFrame];
44
+ await this.transport.sendCommand(payload);
45
+ }
46
+ }
47
+ exports.AccessoryController = AccessoryController;
@@ -0,0 +1,36 @@
1
+ import { CvResultData, ErrorResultData } from "../parsers/parserResult";
2
+ import { Z21UdpTransport } from "../transport/Z21UdpTransport";
3
+ export declare class EngineController {
4
+ private transport;
5
+ constructor(transport: Z21UdpTransport);
6
+ /**
7
+ * Request information about an engine and subscribe to updates for this address.
8
+ * @param address Engine address (1-10239)
9
+ */
10
+ getEngineInfo(address: number): Promise<void>;
11
+ /**
12
+ * Drive an engine with speed and direction.
13
+ * @param address Engine address (1-10239)
14
+ * @param speed Speed value (0-127)
15
+ * @param forward Direction (true=forward, false=backward)
16
+ */
17
+ setDriveEngine(address: number, speed: number, forward: boolean, engineSpeedSteps?: number): void;
18
+ /**
19
+ * Set a function state on an engine.
20
+ * @param address Engine address (1-10239)
21
+ * @param functionNumber Function number (1-28)
22
+ * @param state Function state (on, off, toggle)
23
+ */
24
+ setEngineFunctions(address: number, functionNumber: number, state: string): Promise<void>;
25
+ /**
26
+ * Read a CV in direct mode.
27
+ * @param cv CV number (1-1024)
28
+ */
29
+ cvRead(cv: number): Promise<ErrorResultData | CvResultData>;
30
+ /**
31
+ * Write a CV in direct mode.
32
+ * @param cv CV number (1-1024)
33
+ * @param value Value to write (0-255)
34
+ */
35
+ cvWrite(cv: number, value: number): Promise<ErrorResultData | CvResultData>;
36
+ }
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EngineController = void 0;
4
+ class EngineController {
5
+ constructor(transport) {
6
+ this.transport = transport;
7
+ }
8
+ /**
9
+ * Request information about an engine and subscribe to updates for this address.
10
+ * @param address Engine address (1-10239)
11
+ */
12
+ async getEngineInfo(address) {
13
+ // Split address into MSB and LSB
14
+ let addrMsb = (address >> 8) & 0x3F;
15
+ const addrLsb = address & 0xFF;
16
+ // For addresses >= 128, set the two highest bits in MSB
17
+ if (address >= 128) {
18
+ addrMsb = 0xC0 | addrMsb;
19
+ }
20
+ // Build XpressNet frame: [0xE3, 0xF0, Adr_MSB, Adr_LSB]
21
+ const xpressNetFrame = [0xE3, 0xF0, addrMsb, addrLsb];
22
+ // XOR checksum for XpressNet frame
23
+ let xor = 0;
24
+ for (const byte of xpressNetFrame)
25
+ xor ^= byte;
26
+ xpressNetFrame.push(xor);
27
+ // LAN_X header: [0x40, 0x00]
28
+ const payload = [0x40, 0x00, ...xpressNetFrame];
29
+ await this.transport.sendCommand(payload);
30
+ }
31
+ /**
32
+ * Drive an engine with speed and direction.
33
+ * @param address Engine address (1-10239)
34
+ * @param speed Speed value (0-127)
35
+ * @param forward Direction (true=forward, false=backward)
36
+ */
37
+ setDriveEngine(address, speed, forward, engineSpeedSteps = 128) {
38
+ // --- Build XpressNet engine control frame ---
39
+ const addrL = address & 0xFF; // Low byte of engine address
40
+ const addrH = (address >> 8) & 0x3F; // High byte (14-bit address max)
41
+ const directionBit = forward ? 0x80 : 0x00; // Set direction bit if forward
42
+ const speedByte = directionBit | (speed & 0x7F); // Combine direction and speed
43
+ let steps = 0x13; // Default to DCC128
44
+ if (engineSpeedSteps === 28) {
45
+ steps = 0x12; // DCC27 for 27-speed
46
+ }
47
+ else if (engineSpeedSteps === 14) {
48
+ steps = 0x10; // DCC14 for 14-speed
49
+ }
50
+ // XpressNet frame: E4 = drive engine command
51
+ let xpressNetFrame = [0xE4, steps, addrH, addrL, speedByte];
52
+ // --- Calculate XOR checksum ---
53
+ let xor = 0;
54
+ for (const byte of xpressNetFrame) {
55
+ xor ^= byte;
56
+ }
57
+ xpressNetFrame.push(xor);
58
+ // --- Wrap in LAN_X payload (0x40 0x00 = LAN_X header, 0x24 = XpressNet command) ---
59
+ const payload = [0x40, 0x00, ...xpressNetFrame];
60
+ // --- Build complete Z21 frame and send ---
61
+ this.transport.sendCommand(payload);
62
+ }
63
+ /**
64
+ * Set a function state on an engine.
65
+ * @param address Engine address (1-10239)
66
+ * @param functionNumber Function number (1-28)
67
+ * @param state Function state (on, off, toggle)
68
+ */
69
+ async setEngineFunctions(address, functionNumber, state) {
70
+ const addrL = address & 0xFF; // Low byte of engine address
71
+ const addrH = (address >> 8) & 0x3F; // High byte (14-bit address max)
72
+ // Generation of functionByte according to functionNumber (1-28) and state
73
+ let functionByte = 0x00;
74
+ if (functionNumber < 0 || functionNumber > 28) {
75
+ throw new Error("functionNumber must be between 0 and 28");
76
+ }
77
+ // define functionByte based on state
78
+ switch (state) {
79
+ case "on":
80
+ functionByte = 0x40;
81
+ break;
82
+ case "off":
83
+ functionByte = 0x00;
84
+ break;
85
+ case "toggle":
86
+ functionByte = 0x80;
87
+ break;
88
+ default:
89
+ throw new Error('state must be "on", "off" or "toggle"');
90
+ }
91
+ // Set the specific function bit
92
+ functionByte |= (1 << (functionNumber - 1)); // Set the bit corresponding to the function number
93
+ // Build the command payload
94
+ // E4 = set function command, 0x40 = LAN_X header, 0x00 = XpressNet command
95
+ // Payload format: [0x40, 0x00, 0xE4, addrH, addrL, functionByte]
96
+ const payload = [0x40, 0x00, 0xE4, 0xF8, addrH, addrL, functionByte];
97
+ await this.transport.sendCommand(payload);
98
+ }
99
+ /**
100
+ * Read a CV in direct mode.
101
+ * @param cv CV number (1-1024)
102
+ */
103
+ async cvRead(cv) {
104
+ return new Promise((resolve, reject) => {
105
+ const cvAddr = cv - 1;
106
+ const cvMsb = (cvAddr >> 8) & 0xFF;
107
+ const cvLsb = cvAddr & 0xFF;
108
+ // Build XpressNet frame: [0x23, 0x11, cvMsb, cvLsb]
109
+ const xpressNetFrame = [0x23, 0x11, cvMsb, cvLsb];
110
+ let xor = 0;
111
+ for (const byte of xpressNetFrame)
112
+ xor ^= byte;
113
+ xpressNetFrame.push(xor);
114
+ // LAN_X header: [0x40, 0x00]
115
+ const payload = [0x40, 0x00, ...xpressNetFrame];
116
+ this.transport.sendCommand(payload);
117
+ this.transport.on("cvResult", (msg) => {
118
+ if (msg.cv === cv) {
119
+ resolve(msg);
120
+ }
121
+ });
122
+ this.transport.on("error", (msg) => {
123
+ if (msg.code === "nack" || msg.code === "nack-sc") {
124
+ reject(msg);
125
+ }
126
+ });
127
+ });
128
+ }
129
+ /**
130
+ * Write a CV in direct mode.
131
+ * @param cv CV number (1-1024)
132
+ * @param value Value to write (0-255)
133
+ */
134
+ async cvWrite(cv, value) {
135
+ return new Promise((resolve, reject) => {
136
+ // CVs are 1-based in docs, but 0-based in protocol
137
+ const cvAddr = cv - 1;
138
+ const cvMsb = (cvAddr >> 8) & 0xFF;
139
+ const cvLsb = cvAddr & 0xFF;
140
+ // Build XpressNet frame: [0x24, 0x12, cvMsb, cvLsb, value]
141
+ const xpressNetFrame = [0x24, 0x12, cvMsb, cvLsb, value];
142
+ // XOR checksum for XpressNet frame
143
+ let xor = 0;
144
+ for (const byte of xpressNetFrame)
145
+ xor ^= byte;
146
+ xpressNetFrame.push(xor);
147
+ // LAN_X header: [0x40, 0x00]
148
+ const payload = [0x40, 0x00, ...xpressNetFrame];
149
+ this.transport.sendCommand(payload);
150
+ this.transport.on("cvResult", (msg) => {
151
+ if (msg.cv === cv) {
152
+ resolve(msg);
153
+ }
154
+ });
155
+ this.transport.on("error", (msg) => {
156
+ if (msg.code === "nack" || msg.code === "nack-sc") {
157
+ reject(msg);
158
+ }
159
+ });
160
+ });
161
+ }
162
+ }
163
+ exports.EngineController = EngineController;
@@ -0,0 +1,22 @@
1
+ import { Z21UdpTransport } from "../transport/Z21UdpTransport";
2
+ export declare class SystemController {
3
+ private transport;
4
+ constructor(transport: Z21UdpTransport);
5
+ /** Enable engine, accessory and feedback broadcast info */
6
+ setBroadcastFlags(engine?: boolean, accessory?: boolean, feedback?: boolean): Promise<void>;
7
+ /** Request Z21 serial number */
8
+ getSerialNumber(): Promise<void>;
9
+ /** Request current broadcast flags */
10
+ getBroadcastFlags(): Promise<void>;
11
+ /** Request Z21 status */
12
+ getStatus(): Promise<void>;
13
+ /** Turn track power on */
14
+ setTrackPowerOn(): Promise<void>;
15
+ /** Turn track power off */
16
+ setTrackPowerOff(): Promise<void>;
17
+ /** Emergency stop all engines */
18
+ emergencyStop(): Promise<void>;
19
+ /** Logout from Z21 */
20
+ logout(): Promise<void>;
21
+ delay(ms: number): Promise<unknown>;
22
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SystemController = void 0;
4
+ const Z21Commands_1 = require("../Z21Commands");
5
+ class SystemController {
6
+ constructor(transport) {
7
+ this.transport = transport;
8
+ }
9
+ /** Enable engine, accessory and feedback broadcast info */
10
+ async setBroadcastFlags(engine = true, accessory = true, feedback = true) {
11
+ let flags = 0;
12
+ if (engine)
13
+ flags |= 0x01;
14
+ if (accessory)
15
+ flags |= 0x02;
16
+ if (feedback)
17
+ flags |= 0x04;
18
+ // Flags on 4 bytes (Little Endian)
19
+ const payload = [
20
+ ...Z21Commands_1.commands.LAN_SET_BROADCAST_FLAGS,
21
+ flags, 0x00, 0x00, 0x00
22
+ ];
23
+ await this.transport.sendCommand(payload);
24
+ }
25
+ /** Request Z21 serial number */
26
+ async getSerialNumber() {
27
+ await this.transport.sendCommand(Z21Commands_1.commands.LAN_GET_SERIAL_NUMBER);
28
+ }
29
+ /** Request current broadcast flags */
30
+ async getBroadcastFlags() {
31
+ await this.transport.sendCommand(Z21Commands_1.commands.LAN_GET_BROADCAST_FLAGS);
32
+ }
33
+ /** Request Z21 status */
34
+ async getStatus() {
35
+ await this.transport.sendCommand(Z21Commands_1.commands.LAN_X_GET_STATUS);
36
+ }
37
+ /** Turn track power on */
38
+ async setTrackPowerOn() {
39
+ await this.transport.sendCommand(Z21Commands_1.commands.LAN_X_TRACK_POWER_ON);
40
+ }
41
+ /** Turn track power off */
42
+ async setTrackPowerOff() {
43
+ await this.transport.sendCommand(Z21Commands_1.commands.LAN_X_TRACK_POWER_OFF);
44
+ }
45
+ /** Emergency stop all engines */
46
+ async emergencyStop() {
47
+ await this.transport.sendCommand(Z21Commands_1.commands.LAN_X_SET_STOP);
48
+ }
49
+ /** Logout from Z21 */
50
+ async logout() {
51
+ await this.transport.sendCommand(Z21Commands_1.commands.LAN_LOGOFF);
52
+ this.delay(500); // Wait for logoff to complete
53
+ }
54
+ async delay(ms) {
55
+ return new Promise((resolve) => setTimeout(resolve, ms));
56
+ }
57
+ }
58
+ exports.SystemController = SystemController;
@@ -0,0 +1 @@
1
+ export { Z21Client } from "./Z21Client";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Z21Client = void 0;
4
+ var Z21Client_1 = require("./Z21Client");
5
+ Object.defineProperty(exports, "Z21Client", { enumerable: true, get: function () { return Z21Client_1.Z21Client; } });
@@ -0,0 +1,7 @@
1
+ export declare const CS_STATUS: {
2
+ readonly CS_EMERGENCY_STOP: 1;
3
+ readonly CS_TRACK_VOLTAGE_OFF: 2;
4
+ readonly CS_SHORT_CIRCUIT: 4;
5
+ readonly CS_PROGRAMMING_MODE_ACTIVE: 32;
6
+ };
7
+ export type CommandStationStatusFlag = typeof CS_STATUS[keyof typeof CS_STATUS];
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CS_STATUS = void 0;
4
+ exports.CS_STATUS = {
5
+ CS_EMERGENCY_STOP: 0x01, // The emergency stop is switched on
6
+ CS_TRACK_VOLTAGE_OFF: 0x02, // The track voltage is switched off
7
+ CS_SHORT_CIRCUIT: 0x04, // Short-circuit
8
+ CS_PROGRAMMING_MODE_ACTIVE: 0x20 // The programming mode is active
9
+ };
@@ -0,0 +1,7 @@
1
+ import { ParserResult } from "./parserResult";
2
+ export declare class FeedbackParser {
3
+ private lanParser;
4
+ private lanXParser;
5
+ constructor();
6
+ parse(payload: Buffer): ParserResult | null;
7
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeedbackParser = void 0;
4
+ const lanXParser_1 = require("./lanXParser");
5
+ const lanParser_1 = require("./lanParser");
6
+ class FeedbackParser {
7
+ constructor() {
8
+ this.lanParser = new lanParser_1.LanParser();
9
+ this.lanXParser = new lanXParser_1.LanXParser();
10
+ }
11
+ parse(payload) {
12
+ if (!Buffer.isBuffer(payload) || payload.length < 2) {
13
+ return {
14
+ type: "error",
15
+ value: {
16
+ code: "invalid-payload",
17
+ message: "Invalid payload length"
18
+ }
19
+ };
20
+ }
21
+ // Expected length is indicated in the first two bytes
22
+ const expectedLength = payload.readUInt16LE(0);
23
+ if (payload.length < expectedLength) {
24
+ return {
25
+ type: "error",
26
+ value: {
27
+ code: "invalid-payload",
28
+ message: "Payload shorter than expected length"
29
+ }
30
+ };
31
+ }
32
+ // Buffer without the first 2 bytes (length)
33
+ const data = Buffer.from(payload.subarray(2));
34
+ if (data[0] === 0x40) {
35
+ // LAN_X command: remove first 2 bytes of LAN_X header
36
+ return this.lanXParser.parse(Buffer.from(data.subarray(2)));
37
+ }
38
+ else if (data[0] === 0x10 || data[0] === 0x51) {
39
+ // LAN command
40
+ return this.lanParser.parse(data[0], data.subarray(2));
41
+ }
42
+ return null;
43
+ }
44
+ }
45
+ exports.FeedbackParser = FeedbackParser;
@@ -0,0 +1,11 @@
1
+ import { ParserResult } from "./parserResult";
2
+ export declare class LanParser {
3
+ /**
4
+ * Parse Z21 LAN commands (0x40 command class).
5
+ * These are non-LAN_X commands like serial number, hardware info, etc.
6
+ * @param opcode - LAN command opcode
7
+ * @param payload - Data after the opcode
8
+ * @returns Parsed object or undefined if payload is invalid/unknown opcode
9
+ */
10
+ parse(opcode: number, payload: Buffer): ParserResult | null;
11
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LanParser = void 0;
4
+ class LanParser {
5
+ /**
6
+ * Parse Z21 LAN commands (0x40 command class).
7
+ * These are non-LAN_X commands like serial number, hardware info, etc.
8
+ * @param opcode - LAN command opcode
9
+ * @param payload - Data after the opcode
10
+ * @returns Parsed object or undefined if payload is invalid/unknown opcode
11
+ */
12
+ parse(opcode, payload) {
13
+ switch (opcode) {
14
+ case 0x10: // GET_SERIAL_NUMBER
15
+ if (payload.length === 4) {
16
+ const serialNumber = payload.readUInt32LE(0);
17
+ return { type: "serialNumber", value: { "serialNumber": serialNumber } };
18
+ }
19
+ else {
20
+ console.log("[LAN Parser] Wrong Payload for GET_SERIAL_NUMBER");
21
+ return null;
22
+ }
23
+ case 0x51: // GET_BROADCAST_FLAGS
24
+ if (payload.length >= 4) {
25
+ const flags = payload.readUInt32LE(0);
26
+ return {
27
+ type: "broadcastFlags",
28
+ value: {
29
+ raw: flags,
30
+ engine: !!(flags & 0x01),
31
+ accessory: !!(flags & 0x02),
32
+ feedback: !!(flags & 0x04),
33
+ },
34
+ };
35
+ }
36
+ else {
37
+ console.log("[LAN Parser] Payload too short for GET_BROADCAST_FLAGS");
38
+ return null;
39
+ }
40
+ default:
41
+ return null;
42
+ }
43
+ }
44
+ }
45
+ exports.LanParser = LanParser;
@@ -0,0 +1,11 @@
1
+ import { ParserResult } from "./parserResult";
2
+ /**
3
+ * Parser for LAN_X commands (0x40 command class).
4
+ * Handles status changes, broadcasts, turnout info, and engine info.
5
+ */
6
+ export declare class LanXParser {
7
+ parse(payload: Buffer): ParserResult | null;
8
+ private parseTurnoutInfo;
9
+ private parseEngineInfo;
10
+ private parseCV;
11
+ }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LanXParser = void 0;
4
+ const lanXReturnCodes_1 = require("./lanXReturnCodes");
5
+ const commandStationStatus_1 = require("./commandStationStatus");
6
+ /**
7
+ * Parser for LAN_X commands (0x40 command class).
8
+ * Handles status changes, broadcasts, turnout info, and engine info.
9
+ */
10
+ class LanXParser {
11
+ parse(payload) {
12
+ const opcode = payload[0];
13
+ switch (opcode) {
14
+ case 0x62: // LAN_X_STATUS_CHANGED
15
+ if (payload[1] === 0x22) {
16
+ const statusMap = {
17
+ [commandStationStatus_1.CS_STATUS.CS_EMERGENCY_STOP]: "Emergency Stop Activated",
18
+ [commandStationStatus_1.CS_STATUS.CS_TRACK_VOLTAGE_OFF]: "Track Voltage Off",
19
+ [commandStationStatus_1.CS_STATUS.CS_SHORT_CIRCUIT]: "Short Circuit",
20
+ [commandStationStatus_1.CS_STATUS.CS_PROGRAMMING_MODE_ACTIVE]: "Programming Mode Active",
21
+ };
22
+ const status = statusMap[payload[2]] ?? "Unknown Status";
23
+ return { type: "status", value: status };
24
+ }
25
+ break;
26
+ case 0x61: // LAN_X_BROADCAST
27
+ switch (payload[1]) {
28
+ case lanXReturnCodes_1.LAN_X_BC_CODES.LAN_X_BC_TRACK_POWER_OFF:
29
+ return { type: "trackPower", value: "off" };
30
+ case lanXReturnCodes_1.LAN_X_BC_CODES.LAN_X_BC_TRACK_POWER_ON:
31
+ return { type: "trackPower", value: "on" };
32
+ case lanXReturnCodes_1.LAN_X_BC_CODES.LAN_X_BC_PROGRAMMING_MODE:
33
+ return { type: "programmingMode", value: "active" };
34
+ case lanXReturnCodes_1.LAN_X_BC_CODES.LAN_X_BC_TRACK_SHORT_CIRCUIT:
35
+ return { type: "shortCircuit", value: "detected" };
36
+ case lanXReturnCodes_1.LAN_X_BC_CODES.LAN_X_CV_NACK:
37
+ return {
38
+ type: "error", value: {
39
+ code: "nack",
40
+ message: "CV Read/Write NACK"
41
+ }
42
+ };
43
+ case lanXReturnCodes_1.LAN_X_BC_CODES.LAN_X_CV_NACK_SC:
44
+ return {
45
+ type: "error", value: {
46
+ code: "nack-sc",
47
+ message: "CV Read/Write NACK due to short-circuit"
48
+ }
49
+ };
50
+ default:
51
+ console.warn(`[LAN_X Parser] Unknown broadcast code: 0x${payload[1].toString(16)}`);
52
+ return { type: "unknownBroadcast", value: payload[1] };
53
+ }
54
+ case 0x64: // LAN_X_CV_RESULT
55
+ if (payload[1] === 0x14) {
56
+ return this.parseCV(payload);
57
+ }
58
+ break;
59
+ case 0x43: // LAN_X_TURNOUT_INFO
60
+ return this.parseTurnoutInfo(payload.subarray(1));
61
+ case 0xEF: // LAN_X_ENGINE_INFO
62
+ return this.parseEngineInfo(payload.subarray(1));
63
+ default:
64
+ console.warn(`[LAN_X Parser] Unknown opcode: 0x${opcode.toString(16)}`);
65
+ return null;
66
+ }
67
+ return null;
68
+ }
69
+ parseTurnoutInfo(payload) {
70
+ const adrMsb = payload[0] & 0x3F;
71
+ const adrLsb = payload[1];
72
+ const turnoutAddress = ((adrMsb << 8) + adrLsb) + 1;
73
+ const turnoutStatus = payload[2] & 0b11;
74
+ let position;
75
+ switch (turnoutStatus) {
76
+ case 0b00:
77
+ position = "not_switched";
78
+ break;
79
+ case 0b01:
80
+ position = "P0";
81
+ break;
82
+ case 0b10:
83
+ position = "P1";
84
+ break;
85
+ default:
86
+ position = "invalid";
87
+ }
88
+ return { type: "accessoryInfo", value: { address: turnoutAddress, position } };
89
+ }
90
+ parseEngineInfo(payload) {
91
+ const adrMsb = payload[0] & 0x3F;
92
+ const adrLsb = payload[1];
93
+ const engineAddress = (adrMsb << 8) + adrLsb;
94
+ const db2 = payload[2];
95
+ const busy = (db2 & 0b00001000) !== 0;
96
+ const kkk = db2 & 0b00000111;
97
+ let speedSteps;
98
+ switch (kkk) {
99
+ case 0:
100
+ speedSteps = 14;
101
+ break;
102
+ case 2:
103
+ speedSteps = 28;
104
+ break;
105
+ case 4:
106
+ speedSteps = 128;
107
+ break;
108
+ default:
109
+ speedSteps = "unknown";
110
+ }
111
+ const db3 = payload[3];
112
+ const direction = (db3 & 0x80) ? "forward" : "reverse";
113
+ const speed = db3 & 0x7F;
114
+ const db4 = payload[4];
115
+ const doubleTraction = (db4 & 0b01000000) !== 0;
116
+ const functions = {
117
+ F0: (db4 & 0b00010000) !== 0,
118
+ F1: (db4 & 0b00000001) !== 0,
119
+ F2: (db4 & 0b00000010) !== 0,
120
+ F3: (db4 & 0b00000100) !== 0,
121
+ F4: (db4 & 0b00001000) !== 0,
122
+ };
123
+ if (payload.length > 5) {
124
+ for (let i = 0; i < 8; i++)
125
+ functions[`F${5 + i}`] = (payload[5] & (1 << i)) !== 0;
126
+ }
127
+ if (payload.length > 6) {
128
+ for (let i = 0; i < 8; i++)
129
+ functions[`F${13 + i}`] = (payload[6] & (1 << i)) !== 0;
130
+ }
131
+ if (payload.length > 7) {
132
+ for (let i = 0; i < 8; i++)
133
+ functions[`F${21 + i}`] = (payload[7] & (1 << i)) !== 0;
134
+ }
135
+ if (payload.length > 8) {
136
+ for (let i = 0; i < 3; i++)
137
+ functions[`F${29 + i}`] = (payload[8] & (1 << i)) !== 0;
138
+ }
139
+ return {
140
+ type: "engineInfo",
141
+ value: {
142
+ address: engineAddress,
143
+ busy,
144
+ speedSteps,
145
+ direction,
146
+ speed,
147
+ doubleTraction,
148
+ functions,
149
+ },
150
+ };
151
+ }
152
+ parseCV(payload) {
153
+ const cvMsb = payload[2];
154
+ const cvLsb = payload[3];
155
+ const value = payload[4];
156
+ const cvAddress = (cvMsb << 8) + cvLsb;
157
+ return {
158
+ type: "cvResult",
159
+ value: {
160
+ cv: cvAddress + 1, // CVs are 1-based for user
161
+ value,
162
+ },
163
+ };
164
+ }
165
+ }
166
+ exports.LanXParser = LanXParser;
@@ -0,0 +1,9 @@
1
+ export declare const LAN_X_BC_CODES: {
2
+ readonly LAN_X_BC_TRACK_POWER_OFF: 0;
3
+ readonly LAN_X_BC_TRACK_POWER_ON: 1;
4
+ readonly LAN_X_BC_PROGRAMMING_MODE: 2;
5
+ readonly LAN_X_BC_TRACK_SHORT_CIRCUIT: 8;
6
+ readonly LAN_X_CV_NACK_SC: 18;
7
+ readonly LAN_X_CV_NACK: 19;
8
+ };
9
+ export type LanXBCCode = typeof LAN_X_BC_CODES[keyof typeof LAN_X_BC_CODES];
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LAN_X_BC_CODES = void 0;
4
+ exports.LAN_X_BC_CODES = {
5
+ LAN_X_BC_TRACK_POWER_OFF: 0x00, // Track power off
6
+ LAN_X_BC_TRACK_POWER_ON: 0x01, // Track power on
7
+ LAN_X_BC_PROGRAMMING_MODE: 0x02, // Programming mode active
8
+ LAN_X_BC_TRACK_SHORT_CIRCUIT: 0x08, // Track short-circuit
9
+ LAN_X_CV_NACK_SC: 0x12, // CV Read Write NACK short-circuit
10
+ LAN_X_CV_NACK: 0x13, // CV Read Write NACK
11
+ };
@@ -0,0 +1,80 @@
1
+ export interface ErrorResult {
2
+ type: "error";
3
+ value: ErrorResultData;
4
+ }
5
+ export interface ErrorResultData {
6
+ code: "invalid-payload" | "nack" | "nack-sc";
7
+ message: string;
8
+ }
9
+ export interface SerialNumberResult {
10
+ type: "serialNumber";
11
+ value: SerialNumberResultData;
12
+ }
13
+ export interface SerialNumberResultData {
14
+ serialNumber: number;
15
+ }
16
+ export interface BroadcastFlagsResult {
17
+ type: "broadcastFlags";
18
+ value: BroadcastFlagsResultData;
19
+ }
20
+ export interface BroadcastFlagsResultData {
21
+ raw: number;
22
+ engine: boolean;
23
+ accessory: boolean;
24
+ feedback: boolean;
25
+ }
26
+ export type StatusValue = "Emergency Stop Activated" | "Track Voltage Off" | "Short Circuit" | "Programming Mode Active" | "Unknown Status";
27
+ export type SpeedSteps = 14 | 28 | 128 | "unknown";
28
+ export interface StatusResult {
29
+ type: "status";
30
+ value: StatusValue;
31
+ }
32
+ export interface TrackPowerResult {
33
+ type: "trackPower";
34
+ value: "on" | "off";
35
+ }
36
+ export interface ProgrammingModeResult {
37
+ type: "programmingMode";
38
+ value: "active" | "inactive";
39
+ }
40
+ export interface ShortCircuitResult {
41
+ type: "shortCircuit";
42
+ value: "detected";
43
+ }
44
+ export interface UnknownBroadcastResult {
45
+ type: "unknownBroadcast";
46
+ value: number;
47
+ }
48
+ export interface AccessoryInfoResult {
49
+ type: "accessoryInfo";
50
+ value: AccessoryInfoResultData;
51
+ }
52
+ export interface AccessoryInfoResultData {
53
+ address: number;
54
+ position: "not_switched" | "P0" | "P1" | "invalid";
55
+ }
56
+ export interface EngineInfoResult {
57
+ type: "engineInfo";
58
+ value: EngineInfoResultData;
59
+ }
60
+ export interface EngineInfoResultData {
61
+ address: number;
62
+ busy: boolean;
63
+ speedSteps: SpeedSteps;
64
+ direction: "forward" | "reverse";
65
+ speed: number;
66
+ doubleTraction: boolean;
67
+ functions: Record<string, boolean>;
68
+ }
69
+ export interface CvResult {
70
+ type: "cvResult";
71
+ value: CvResultData;
72
+ }
73
+ export interface CvResultData {
74
+ cv: number;
75
+ value: number;
76
+ }
77
+ /**
78
+ * Union type for all possible results
79
+ */
80
+ export type ParserResult = ErrorResult | SerialNumberResult | BroadcastFlagsResult | StatusResult | TrackPowerResult | ProgrammingModeResult | ShortCircuitResult | UnknownBroadcastResult | AccessoryInfoResult | EngineInfoResult | CvResult;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from "events";
2
+ import { FeedbackParser } from "../parsers/feedbackParser";
3
+ export declare class Z21UdpTransport extends EventEmitter {
4
+ private socket;
5
+ private host;
6
+ private port;
7
+ private debug;
8
+ private feedbackParser;
9
+ constructor(host: string, port: number | undefined, debug: boolean | undefined, feedbackParser: FeedbackParser);
10
+ /**
11
+ * Handle incoming UDP messages, parse and emit events.
12
+ * @param msg Incoming UDP message buffer
13
+ */
14
+ private handleMessage;
15
+ /**
16
+ * Build a Z21 frame.
17
+ * Frame format: [length (2 bytes LE)+[payload...]
18
+ * Length includes entire frame length (header + payload).
19
+ * @param payload Command bytes after header
20
+ * @returns Buffer frame to send
21
+ */
22
+ private buildFrame;
23
+ /**
24
+ * Send a command frame asynchronously.
25
+ * @param payload Command payload bytes
26
+ */
27
+ sendCommand(payload: number[]): Promise<void>;
28
+ /**
29
+ * Send UDP frame and return Promise.
30
+ * @param frame Buffer to send
31
+ */
32
+ private sendFrame;
33
+ close(): void;
34
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Z21UdpTransport = void 0;
7
+ const dgram_1 = __importDefault(require("dgram"));
8
+ const events_1 = require("events");
9
+ class Z21UdpTransport extends events_1.EventEmitter {
10
+ constructor(host, port = 21105, debug = false, feedbackParser) {
11
+ super();
12
+ this.host = host;
13
+ this.port = port;
14
+ this.debug = debug;
15
+ this.socket = dgram_1.default.createSocket("udp4");
16
+ this.feedbackParser = feedbackParser;
17
+ // Listen to incoming UDP messages
18
+ this.socket.on("message", (msg) => this.handleMessage(msg));
19
+ // Handle UDP errors
20
+ this.socket.on("error", (err) => {
21
+ console.log(`[Z21UdpTransport] UDP socket error: ${err.message}`);
22
+ this.emit("error", err);
23
+ this.close();
24
+ });
25
+ if (this.debug) {
26
+ console.log(`[Z21UdpTransport] Init`);
27
+ }
28
+ }
29
+ /**
30
+ * Handle incoming UDP messages, parse and emit events.
31
+ * @param msg Incoming UDP message buffer
32
+ */
33
+ handleMessage(msg) {
34
+ if (this.debug) {
35
+ this.emit("debug", msg);
36
+ }
37
+ const parsed = this.feedbackParser.parse(msg);
38
+ if (parsed) {
39
+ this.emit(parsed.type, parsed.value);
40
+ }
41
+ }
42
+ /**
43
+ * Build a Z21 frame.
44
+ * Frame format: [length (2 bytes LE)+[payload...]
45
+ * Length includes entire frame length (header + payload).
46
+ * @param payload Command bytes after header
47
+ * @returns Buffer frame to send
48
+ */
49
+ buildFrame(payload) {
50
+ const length = payload.length + 2; // 2 bytes length
51
+ const buffer = Buffer.alloc(length);
52
+ buffer.writeUInt16LE(length, 0);
53
+ for (let i = 0; i < payload.length; i++) {
54
+ buffer[2 + i] = payload[i];
55
+ }
56
+ return buffer;
57
+ }
58
+ /**
59
+ * Send a command frame asynchronously.
60
+ * @param payload Command payload bytes
61
+ */
62
+ async sendCommand(payload) {
63
+ const frame = this.buildFrame(payload);
64
+ console.log(frame);
65
+ await this.sendFrame(frame);
66
+ }
67
+ /**
68
+ * Send UDP frame and return Promise.
69
+ * @param frame Buffer to send
70
+ */
71
+ sendFrame(frame) {
72
+ return new Promise((resolve, reject) => {
73
+ this.socket.send(frame, this.port, this.host, (err) => {
74
+ if (err)
75
+ reject(err);
76
+ else
77
+ resolve();
78
+ });
79
+ });
80
+ }
81
+ close() {
82
+ this.socket.close();
83
+ }
84
+ }
85
+ exports.Z21UdpTransport = Z21UdpTransport;
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "z21-client",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript UDP client for Roco/Fleischmann Z21 DCC command station",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/nmeunier/z21-client.git"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "ts-node-dev --respawn exemples/index.ts",
14
+ "test": "jest",
15
+ "test:coverage": "jest --coverage"
16
+ },
17
+ "keywords": [
18
+ "dcc",
19
+ "z21",
20
+ "roco",
21
+ "fleischmann",
22
+ "model-railway",
23
+ "model-train",
24
+ "railroad",
25
+ "train-control",
26
+ "locomotive",
27
+ "turnout",
28
+ "accessory",
29
+ "decoder",
30
+ "cv",
31
+ "udp",
32
+ "typescript",
33
+ "nodejs"
34
+ ],
35
+ "author": "Nicolas Meunier",
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@types/jest": "^29.0.0",
39
+ "@types/node": "^20.0.0",
40
+ "@typescript-eslint/eslint-plugin": "^8.39.0",
41
+ "@typescript-eslint/parser": "^8.39.0",
42
+ "eslint": "^9.33.0",
43
+ "eslint-plugin-jest": "^29.0.1",
44
+ "jest": "^29.0.0",
45
+ "ts-jest": "^29.0.0",
46
+ "ts-node": "^10.0.0",
47
+ "ts-node-dev": "^2.0.0",
48
+ "typescript": "^5.0.0"
49
+ },
50
+ "files": [
51
+ "dist"
52
+ ]
53
+ }