@2702rebels/ntcore 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/src/NT4Spec.ts ADDED
@@ -0,0 +1,291 @@
1
+ import type { SubscriptionOptions, TopicProperties } from "./NT4Types";
2
+
3
+ /**
4
+ * Network constants.
5
+ */
6
+ export const NetworkSpecs = {
7
+ /** NT4 port for unsecure (ws://) connections */
8
+ portUnsecure: 5810,
9
+
10
+ /** NT4 port for secure (wss://) connections */
11
+ portSecure: 5811,
12
+
13
+ /** Subprotocol v4.1 */
14
+ protocol41: "v4.1.networktables.first.wpi.edu",
15
+
16
+ /** Subprotocol v4.0 */
17
+ protocol40: "networktables.first.wpi.edu",
18
+
19
+ /** Subprotocol RTT */
20
+ protocolRTT: "rtt.networktables.first.wpi.edu",
21
+
22
+ /** v4.0 ping period in milliseconds */
23
+ pingPeriod40: 1000,
24
+
25
+ /** v4.0 ping timeout in milliseconds */
26
+ pingTimeout40: 3000,
27
+
28
+ /** v4.1 ping period in milliseconds */
29
+ pingPeriod41: 250,
30
+
31
+ /** v4.1 ping timeout in milliseconds */
32
+ pingTimeout41: 1000,
33
+ };
34
+
35
+ /**
36
+ * Supported data types mapped to protocol identifiers.
37
+ *
38
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#supported-data-types
39
+ */
40
+ export const DataTypeCodes = {
41
+ boolean: 0,
42
+ double: 1,
43
+ int: 2,
44
+ float: 3,
45
+ string: 4,
46
+ json: 4,
47
+ raw: 5,
48
+ rpc: 5,
49
+ msgpack: 5,
50
+ protobuf: 5,
51
+ "boolean[]": 16,
52
+ "double[]": 17,
53
+ "int[]": 18,
54
+ "float[]": 19,
55
+ "string[]": 20,
56
+ } as const;
57
+
58
+ export type DataType = keyof typeof DataTypeCodes;
59
+
60
+ /**
61
+ * Publish Request Message (`publish`)
62
+ *
63
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#publish-request-message-publish
64
+ */
65
+ export type PublishRequestMessage = {
66
+ /**
67
+ * **Publish name**
68
+ *
69
+ * The topic name being published.
70
+ */
71
+ name: string;
72
+
73
+ /**
74
+ * **Published UID**
75
+ *
76
+ * A client-generated unique identifier for this publisher. Use the same UID later to unpublish.
77
+ * This is also the identifier that the client will use in MessagePack messages for this topic.
78
+ */
79
+ pubuid: number;
80
+
81
+ /**
82
+ * **Type of data**
83
+ *
84
+ * The requested data type (as a string).
85
+ * If the topic is newly created (e.g. there are no other publishers) this sets the value type.
86
+ * If the topic was previously published, this is ignored.
87
+ */
88
+ type: string;
89
+
90
+ /**
91
+ * **Properties**
92
+ *
93
+ * Initial topic properties.
94
+ * If the topic is newly created (e.g. there are no other publishers) this sets the topic properties.
95
+ * If the topic was previously published, this is ignored.
96
+ */
97
+ properties: TopicProperties;
98
+ };
99
+
100
+ /**
101
+ * Publish Release Message (`unpublish`)
102
+ *
103
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#publish-release-message-unpublish
104
+ */
105
+ export type PublishReleaseMessage = {
106
+ /**
107
+ * **Publisher UID**
108
+ *
109
+ * The same unique identifier passed to the `publish` message.
110
+ */
111
+ pubuid: number;
112
+ };
113
+
114
+ /**
115
+ * Set Properties Message (`setproperties`)
116
+ *
117
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#set-properties-message-setproperties
118
+ */
119
+ export type SetPropertiesMessage = {
120
+ /**
121
+ * **Topic name**
122
+ */
123
+ name: string;
124
+
125
+ /**
126
+ * **Properties to update**
127
+ */
128
+ update: TopicProperties;
129
+ };
130
+
131
+ /**
132
+ * Subscribe Message (`subscribe`)
133
+ *
134
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#subscribe-message-subscribe
135
+ */
136
+ export type SubscribeMessage = {
137
+ /**
138
+ * **Array of topic names or prefixes**
139
+ *
140
+ * One or more topic names or prefixes (if the `prefix` option is true) to start receiving messages for.
141
+ */
142
+ topics: Array<string>;
143
+
144
+ /**
145
+ * **Subscription UID**
146
+ *
147
+ * A client-generated unique identifier for this subscription. Use the same UID later to unsubscribe.
148
+ */
149
+ subuid: number;
150
+
151
+ /**
152
+ * **Options**
153
+ */
154
+ options: SubscriptionOptions;
155
+ };
156
+
157
+ /**
158
+ * Unsubscribe Message (`unsubscribe`)
159
+ *
160
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#unsubscribe-message-unsubscribe
161
+ */
162
+ export type UnsubscribeMessage = {
163
+ /**
164
+ * **Subscription UID**
165
+ *
166
+ * The same unique identifier passed to the `subscribe` message.
167
+ */
168
+ subuid: number;
169
+ };
170
+
171
+ /**
172
+ * Topic Announcement Message (`announce`)
173
+ *
174
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#topic-announcement-message-announce
175
+ */
176
+ export type TopicAnnouncementMessage = {
177
+ /**
178
+ * **Topic name**
179
+ */
180
+ name: string;
181
+
182
+ /**
183
+ * **Topic ID**
184
+ *
185
+ * The identifier that the server will use in MessagePack messages for this topic.
186
+ */
187
+ id: number;
188
+
189
+ /**
190
+ * **Data type**
191
+ *
192
+ * The data type for the topic (as a string).
193
+ */
194
+ type: string;
195
+
196
+ /**
197
+ * **Publisher UID**
198
+ *
199
+ * If this message was sent in response to a publish message,
200
+ * the Publisher UID provided in that message. Otherwise absent.
201
+ */
202
+ pubuid?: number;
203
+
204
+ /**
205
+ * **Properties**
206
+ */
207
+ properties: TopicProperties;
208
+ };
209
+
210
+ /**
211
+ * Topic Removed Message (`unannounce`)
212
+ *
213
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#topic-removed-message-unannounce
214
+ */
215
+ export type TopicRemovedMessage = {
216
+ /**
217
+ * **Topic name**
218
+ */
219
+ name: string;
220
+
221
+ /**
222
+ * **Topic ID**
223
+ *
224
+ * The identifier that the server was using for value updates.
225
+ */
226
+ id: number;
227
+ };
228
+
229
+ /**
230
+ * Properties Update Message (`properties`)
231
+ *
232
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#properties-update-message-properties
233
+ */
234
+ export type PropertiesUpdateMessage = {
235
+ /**
236
+ * **Topic name**
237
+ */
238
+ name: string;
239
+
240
+ /**
241
+ * **Acknowledgement**
242
+ *
243
+ * `True` if this message is in response to a `setproperties` message from the same client.
244
+ * Otherwise absent.
245
+ */
246
+ ack: boolean;
247
+
248
+ /**
249
+ * **Properties to set**
250
+ */
251
+ update: TopicProperties;
252
+ };
253
+
254
+ /**
255
+ * Supported text data frames.
256
+ *
257
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#text-data-frames
258
+ */
259
+ export type TextDataFrame =
260
+ | {
261
+ method: "publish";
262
+ params: PublishRequestMessage;
263
+ }
264
+ | {
265
+ method: "unpublish";
266
+ params: PublishReleaseMessage;
267
+ }
268
+ | {
269
+ method: "setproperties";
270
+ params: SetPropertiesMessage;
271
+ }
272
+ | {
273
+ method: "subscribe";
274
+ params: SubscribeMessage;
275
+ }
276
+ | {
277
+ method: "unsubscribe";
278
+ params: UnsubscribeMessage;
279
+ }
280
+ | {
281
+ method: "announce";
282
+ params: TopicAnnouncementMessage;
283
+ }
284
+ | {
285
+ method: "unannounce";
286
+ params: TopicRemovedMessage;
287
+ }
288
+ | {
289
+ method: "properties";
290
+ params: PropertiesUpdateMessage;
291
+ };
@@ -0,0 +1,19 @@
1
+ import type { SubscriptionOptions } from "./NT4Types";
2
+
3
+ /** Contains information about NT4 subscription. */
4
+ export class NT4Subscription {
5
+ /** Subscription UID. */
6
+ public readonly uid: number;
7
+
8
+ /** Subscription options. */
9
+ public readonly options: SubscriptionOptions;
10
+
11
+ /** Subscribed topic names or prefixes. */
12
+ public readonly topics: Set<string>;
13
+
14
+ constructor(uid: number, topics: Iterable<string>, options?: SubscriptionOptions) {
15
+ this.uid = uid;
16
+ this.topics = new Set(typeof topics === "string" ? [topics] : topics);
17
+ this.options = options ?? {};
18
+ }
19
+ }
@@ -0,0 +1,74 @@
1
+ import { DataTypeCodes } from "./NT4Spec";
2
+
3
+ import type { DataType } from "./NT4Spec";
4
+ import type { TopicProperties } from "./NT4Types";
5
+
6
+ /** Contains information about NT4 topic. */
7
+ export class NT4Topic {
8
+ /** Topic UID. */
9
+ public readonly uid: number;
10
+
11
+ /** Topic name. */
12
+ public readonly name: string;
13
+
14
+ /** Topic data type. */
15
+ public readonly type: string;
16
+
17
+ /** Topic properties. */
18
+ public readonly properties: TopicProperties;
19
+
20
+ /** Publisher UID. */
21
+ public pubuid?: number;
22
+
23
+ /** Last retained value. */
24
+ public retainedValue?: unknown;
25
+
26
+ /** Timestamp of {@link retainedValue}. */
27
+ public retainedTimestamp?: number;
28
+
29
+ constructor(uid: number, name: string, type: string, properties?: TopicProperties, pubuid?: number) {
30
+ this.uid = uid;
31
+ this.name = name;
32
+ this.type = type;
33
+ this.properties = properties ?? {};
34
+ this.pubuid = pubuid;
35
+ this.typeCode = DataTypeCodes[type as DataType] ?? 5; // defaults to binary
36
+ }
37
+
38
+ /** Merges properties with this topic. */
39
+ public mergeProperties(properties: TopicProperties) {
40
+ Object.entries(properties).forEach(([key, value]) => {
41
+ if (value === null) {
42
+ delete this.properties[key];
43
+ } else {
44
+ this.properties[key] = value;
45
+ }
46
+ });
47
+ }
48
+
49
+ /** Updates retained value if the incoming timestamp is newer (greater-than or equal-to). */
50
+ public updateRetainedValue(value: unknown, timestamp: number) {
51
+ if (this.retainedTimestamp == null || timestamp >= this.retainedTimestamp) {
52
+ this.retainedValue = value;
53
+ this.retainedTimestamp = timestamp;
54
+ }
55
+ }
56
+
57
+ /** Removes retained value except for "weak" default value. */
58
+ public removeRetainedValue() {
59
+ if (this.retainedTimestamp != 0) {
60
+ this.retainedValue = undefined;
61
+ this.retainedTimestamp = undefined;
62
+ }
63
+ }
64
+
65
+ /** Resets retained value timestamp except for "weak" default value. */
66
+ public resetRetainedTimestamp() {
67
+ if (this.retainedTimestamp != null && this.retainedTimestamp > 1) {
68
+ this.retainedTimestamp = 1;
69
+ }
70
+ }
71
+
72
+ /** Topic type code. For internal use only. */
73
+ public readonly typeCode: number;
74
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Topic properties, including well-known properties
3
+ *
4
+ * https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#properties
5
+ */
6
+ export type TopicProperties = Record<string, unknown> & {
7
+ /**
8
+ * **Persistent flag**
9
+ *
10
+ * If `true`, the last set value will be periodically saved to persistent storage on the server
11
+ * and be restored during server startup. Topics with this property set to `true` will not
12
+ * be deleted by the server when the last publisher stops publishing.
13
+ */
14
+ persistent?: boolean;
15
+
16
+ /**
17
+ * **Retained flag**
18
+ *
19
+ * Topics with this property set to `true` will not be deleted by the server when
20
+ * the last publisher stops publishing.
21
+ */
22
+ retained?: boolean;
23
+
24
+ /**
25
+ * **Cached flag**
26
+ *
27
+ * If `false`, the server and clients will not store the value of the topic.
28
+ * This means that only value updates will be available for the topic.
29
+ */
30
+ cached?: boolean;
31
+ };
32
+
33
+ /**
34
+ * Subscription options, including well-known options
35
+ */
36
+ export type SubscriptionOptions = Record<string, unknown> & {
37
+ /**
38
+ * **Periodic sweep time (in seconds)**
39
+ *
40
+ * How frequently the server should send changes. The server may send more frequently than this
41
+ * (e.g. use a combined minimum period for all values) or apply a restricted range to this value
42
+ * The default if unspecified is 100 ms (same as NT 3.0).
43
+ */
44
+ periodic?: number;
45
+
46
+ /**
47
+ * **All changes flag**
48
+ *
49
+ * If `true`, the server should send all value changes over the wire. If `false`, only
50
+ * the most recent value is sent (same as NT 3.0 behavior). If not specified, defaults to `false`.
51
+ */
52
+ all?: boolean;
53
+
54
+ /**
55
+ * **No value changes flag**
56
+ *
57
+ * If `true`, the server should not send any value changes over the wire regardless of other options.
58
+ * This is useful for only getting topic announcements. If `false`, value changes are sent in accordance
59
+ * with other options. If not specified, defaults to `false`.
60
+ */
61
+ topicsonly?: boolean;
62
+
63
+ /**
64
+ * **Prefix flag**
65
+ *
66
+ * If `true`, any topic starting with the name in the subscription topics list is subscribed to,
67
+ * not just exact matches. If not specified, defaults to `false`.
68
+ */
69
+ prefix?: boolean;
70
+ };
@@ -0,0 +1,145 @@
1
+ import { NetworkSpecs } from "./NT4Spec";
2
+
3
+ import type { TextDataFrame } from "./NT4Spec";
4
+
5
+ // version 4.1 -- v4.1.networktables.first.wpi.edu
6
+ // version 4.0 -- networktables.first.wpi.edu
7
+ const PROTOCOL_DATA = [NetworkSpecs.protocol41, NetworkSpecs.protocol40];
8
+
9
+ // https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#rtt-subprotocol
10
+ const PROTOCOL_RTT = [NetworkSpecs.protocolRTT];
11
+
12
+ /**
13
+ * A wrapper for `WebSocket` that implements async `connect` and `close` methods.
14
+ *
15
+ * This class manages the lifecycle of the raw websocket internally, creating and
16
+ * destroying the websocket as necessary when establishing and closing the connection.
17
+ */
18
+ export class NT4WebSocketAsync {
19
+ private url: string;
20
+ private rtt: boolean;
21
+ private socket: WebSocket | null = null;
22
+ private onMessage?: (_: MessageEvent) => void;
23
+ private onClose?: (_: CloseEvent) => void;
24
+
25
+ /**
26
+ * Creates a `WebSocket` wrapper to be used for NT4 communication channel.
27
+ * This constructor does not initiate the connection, see `connect`.
28
+ *
29
+ * @param address server network address
30
+ * @param port sever port
31
+ * @param clientId client identifier
32
+ * @param options connection options
33
+ */
34
+ constructor(
35
+ address: string,
36
+ port: number,
37
+ clientId: string,
38
+ options: {
39
+ /** Callback invoked when socket receives message. */
40
+ onMessage?: (ev: MessageEvent) => void;
41
+ /** Callback invoked when socket closes. */
42
+ onClose?: (ev: CloseEvent) => void;
43
+ /** `true` to use secure protocol/port; `false` to use unsecure protocol/port (default). */
44
+ secure?: boolean;
45
+ /** `true` to create RTT subprotocol channel; `false` to create default data channel (default). */
46
+ rtt?: boolean;
47
+ }
48
+ ) {
49
+ const schema = options.secure ? "wss" : "ws";
50
+
51
+ this.url = `${schema}://${address}:${port}/nt/${clientId}`;
52
+ this.rtt = options.rtt ?? false;
53
+ this.onMessage = options.onMessage;
54
+ this.onClose = options.onClose;
55
+ }
56
+
57
+ private onMessageLocal(ev: MessageEvent, source: WebSocket) {
58
+ if (this.onMessage && this.socket === source) {
59
+ this.onMessage(ev);
60
+ }
61
+ }
62
+
63
+ private onCloseLocal(ev: CloseEvent, source: WebSocket) {
64
+ if (this.onClose && this.socket === source) {
65
+ this.onClose(ev);
66
+ }
67
+ }
68
+
69
+ /** Returns `true` if the socket is in the connected state. */
70
+ public get connected() {
71
+ return this.socket != null && this.socket.readyState === WebSocket.OPEN;
72
+ }
73
+
74
+ /** Returns `true` if the socket is in the connecting state. */
75
+ public get connecting() {
76
+ return this.socket != null && this.socket.readyState === WebSocket.CONNECTING;
77
+ }
78
+
79
+ /** Returns the subprotocol negotiated with the server. */
80
+ public get protocol() {
81
+ return this.socket?.protocol;
82
+ }
83
+
84
+ /** Sends text data frame (JSON serialized). */
85
+ public sendText(data: TextDataFrame | Array<TextDataFrame>) {
86
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
87
+ this.socket.send(JSON.stringify(Array.isArray(data) ? data : [data]));
88
+ }
89
+ }
90
+
91
+ /** Sends binary data frame. */
92
+ public sendBinary(data: Uint8Array) {
93
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
94
+ this.socket.send(data);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Asynchronously terminates the connection.
100
+ *
101
+ * @param code optional code
102
+ * @param reason optional reason
103
+ */
104
+ public async close(code?: number, reason?: string): Promise<void> {
105
+ if (this.socket) {
106
+ const socket = this.socket;
107
+ this.socket = null;
108
+
109
+ // websocket is closing asynchronously, we need to wait
110
+ // for the corresponding onclose to fire:
111
+ // attach another listener here and promisify
112
+ return new Promise((resolve) => {
113
+ socket.addEventListener("close", () => {
114
+ resolve();
115
+ });
116
+
117
+ socket.close(code, reason);
118
+ });
119
+ }
120
+
121
+ return Promise.resolve();
122
+ }
123
+
124
+ /** Asynchronously initiates the connection. */
125
+ public async connect(): Promise<void> {
126
+ return new Promise((resolve, reject) => {
127
+ const socket = new WebSocket(this.url, this.rtt ? PROTOCOL_RTT : PROTOCOL_DATA);
128
+
129
+ socket.binaryType = "arraybuffer";
130
+
131
+ this.socket = socket;
132
+
133
+ socket.addEventListener("message", (_) => this.onMessageLocal(_, socket));
134
+ socket.addEventListener("close", (_) => this.onCloseLocal(_, socket));
135
+
136
+ socket.onopen = () => {
137
+ resolve();
138
+ };
139
+
140
+ socket.onerror = (event) => {
141
+ reject(event);
142
+ };
143
+ });
144
+ }
145
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./NT4Types";
2
+ export * from "./NT4Client";
3
+ export * from "./NT4Topic";
4
+ export * from "./NT4Subscription";