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 +21 -0
- package/README.md +162 -0
- package/dist/Z21Client.d.ts +18 -0
- package/dist/Z21Client.js +47 -0
- package/dist/Z21Commands.d.ts +14 -0
- package/dist/Z21Commands.js +17 -0
- package/dist/controllers/AccessoryController.d.ts +13 -0
- package/dist/controllers/AccessoryController.js +47 -0
- package/dist/controllers/EngineController.d.ts +36 -0
- package/dist/controllers/EngineController.js +163 -0
- package/dist/controllers/SystemController.d.ts +22 -0
- package/dist/controllers/SystemController.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/parsers/commandStationStatus.d.ts +7 -0
- package/dist/parsers/commandStationStatus.js +9 -0
- package/dist/parsers/feedbackParser.d.ts +7 -0
- package/dist/parsers/feedbackParser.js +45 -0
- package/dist/parsers/lanParser.d.ts +11 -0
- package/dist/parsers/lanParser.js +45 -0
- package/dist/parsers/lanXParser.d.ts +11 -0
- package/dist/parsers/lanXParser.js +166 -0
- package/dist/parsers/lanXReturnCodes.d.ts +9 -0
- package/dist/parsers/lanXReturnCodes.js +11 -0
- package/dist/parsers/parserResult.d.ts +80 -0
- package/dist/parsers/parserResult.js +2 -0
- package/dist/transport/Z21UdpTransport.d.ts +34 -0
- package/dist/transport/Z21UdpTransport.js +85 -0
- package/package.json +53 -0
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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,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,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,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
|
+
}
|