├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.ts ├── package-lock.json ├── package.json ├── test └── test.ts ├── tsconfig.json └── typings └── ieee754.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | definitions/test/ 40 | 41 | .vscode/ 42 | types/ 43 | *.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | .gitignore 4 | tsconfig.json 5 | *.ts 6 | *.log 7 | !*.d.ts 8 | types/test/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tomas Rakusan 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WPILIB Network Tables Client 2 | This client uses version [3.0](https://github.com/wpilibsuite/ntcore/blob/master/doc/networktables3.adoc) 3 | of the **Network Tables Protocol**. With a failover to [2.0](https://github.com/wpilibsuite/ntcore/blob/master/doc/networktables2.adoc) 4 | 5 | ## Installation 6 | ``` 7 | npm install wpilib-nt-client 8 | ``` 9 | 10 | ## Usage 11 | 12 | ```js 13 | // Decleration 14 | const ntClient = require('wpilib-nt-client'); 15 | 16 | const client = new ntClient.Client() 17 | 18 | // Connects the client to the server on team 3571's roborio 19 | client.start((isConnected, err) => { 20 | // Displays the error and the state of connection 21 | console.log({ isConnected, err }); 22 | }, 'roborio-3571.local'); 23 | 24 | // Adds a listener to the client 25 | client.addListener((key, val, type, id) => { 26 | console.log({ key, val, type, id }); 27 | }) 28 | ``` 29 | ## Constructor 30 | - `Client()` 31 | - Standard Constructor 32 | - `Client(options)` 33 | - **options** an object containing is **strictInput** with a boolean value 34 | - **strictInput** Does not try to correct incorrect types 35 | 36 | ## Properties 37 | - `.start((connected, err, is2_0) => any, address, port)` 38 | - Connects the client to a specific address and port 39 | - **connected** - True if the client has successfully completed its handshake 40 | - **err** - Contains the error if one has occurred 41 | - **is2_0** - True if the client had to failover to 2.0 of the Network Tables protocol 42 | - **address** - The address of the Server. Defaults to loopback 43 | - **port** - The port of the server 44 | - `.stop()` 45 | - Tries to stop the client by sending a fin packet 46 | - `.destroy()` 47 | - Closes the connection forcefully 48 | - `.setReconnectDelay(delay)` 49 | - **delay** set the delay before trying to reconnect 50 | - If the dellay is less than 20 than it wll not attempt reconnect 51 | - `.addListener((key, value, valueType, type, id, flags) => any, getCurrent)` 52 | - Adds a callback to be called when a value has been added, updated, or deleted, and returns the Listener 53 | - **key** - The Key for the Entry 54 | - **value** - The value associated with the key 55 | - **valueType** - The type of the value Possible Types are listed Bellow 56 | - **type** - The type of the callback. Possible Types are: "add", "update", "delete", "flagChange" 57 | - **id** - The ID of the Entry 58 | - **flags** - The flags of the Entry 59 | - **getCurrent** - immediatly callback if connected with known entries 60 | - `.removeListener(listener)` 61 | - Removes the specified listener 62 | - **listener** - The Listener returned from `.addListener()` 63 | - `.isConnected()` 64 | - Returns true if the client is connected and has completed its handshake 65 | - `.uses2_0()` 66 | - Returns true if the client has switched to using version 2.0 of the NetworkTables protocol 67 | - `.getKeyID(key)` 68 | - Returns the ID of a key or All of the keys if **key** is left out 69 | - `.getEntry(id)` 70 | - Returns an Entry identified with an ID 71 | - `.Assign(val, name, persist)` 72 | - Sets a new Entry 73 | - **val** - The Value being added 74 | - **name** - The Key for the Entry 75 | - **persist** - An optional boolean value of whether the value shoud stay on the server after a restart 76 | - Can return an error if type is an RPC 77 | - `.Update(id, val)` 78 | - **id** - The ID of the Entry to be updated 79 | - **val** - The new Value 80 | - Can Return an error if the Entry does not exist ot the value is of the wrong type 81 | - `Flag(id, persist)` 82 | - Updates the persist flag 83 | - **id** - The ID of the Entry to be updated 84 | - **persist** - An optional boolean value of whether the value shoud stay on the server after a restart 85 | - Can return an error if the Entry does not exist 86 | - `.Delete(id)` 87 | - Deletes an Entry 88 | - **id** - The ID of the entry being Deleted 89 | - Can Return an error if the Entry does not exist 90 | - `.DeleteAll()` 91 | - Deletes all of the Entries 92 | - Returns an error if the type is the client is using 2.0 93 | - `.RPCExec(id, val, (result) => any)` 94 | - Calls a Remote Procedure 95 | - **id** - The ID of the procedure 96 | - **val** - The Parameters of the Procedure 97 | - **result** - The result of the call 98 | - Can Return an error of parameter type does not corespond to the definition, the Entry is not an RPC, or the Entry does not Exist 99 | - `.write(buf)` 100 | - Sends a Message dirrectly to the server 101 | - DO NOT USE unless you know what you are doing 102 | 103 | ## Types 104 | - valueType 105 | - Boolean 106 | - Number 107 | - String 108 | - Buffer 109 | - BooleanArray 110 | - NumberArray 111 | - StringArray 112 | - RPC 113 | - valueID 114 | - 0x00 : "Boolean" 115 | - 0x01 : "Number" 116 | - 0x02 : "String" 117 | - 0x03 : "Buffer" 118 | - 0x10 : "BooleanArray" 119 | - 0x11 : "NumberArray" 120 | - 0x12 : "StringArray" 121 | - 0x20 : "RPC" 122 | 123 | ## In 2.0 124 | - Delete does not work 125 | - Flags do not exist 126 | - RPC does not exist 127 | 128 | ## RPC Entry Definition 129 | Remote Procedure Call 130 | ```js 131 | RPC:{ 132 | // The name of the Call 133 | name, 134 | // The parameters of the call 135 | par:{ 136 | // The Id of the type of the parameter 137 | typeId, 138 | // The name of the Type of the parameter 139 | typeName, 140 | // Name of the Parameter 141 | name, 142 | // Default value for the parameter 143 | default 144 | }[], 145 | // The format of the results 146 | results:{ 147 | // The Id of the type of the result 148 | typeId, 149 | // The name of the Type of the result 150 | typeName, 151 | // The name of the result 152 | name 153 | }[] 154 | } 155 | ``` 156 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as ieee754 from "ieee754"; 2 | import * as net from "net"; 3 | import { Buffer } from "buffer"; 4 | import * as url from 'url' 5 | var strLenIdent = numTo128; 6 | export type Listener = ( 7 | key: string, 8 | value: any, 9 | valueType: String, 10 | type: "add" | "delete" | "update" | "flagChange", 11 | id: number, 12 | flags: number 13 | ) => any; 14 | export class Client { 15 | private debug = (level: debugType, st: any) => { } 16 | serverName: String; 17 | clientName = "NodeJS" + Date.now(); 18 | private client: net.Socket; 19 | private connected = false; 20 | private socketConnected = false 21 | private entries: { [key: number]: Entry } = {}; 22 | private oldEntries: { [key: number]: Entry } = {}; 23 | private keymap: { [key: string]: number } = {}; 24 | private newKeyMap: newEntry[] = []; 25 | private updatedIDs: number[] = []; 26 | private reconnect = false; 27 | private address: string; 28 | private port: number; 29 | private known = false; 30 | private listeners: Listener[] = []; 31 | private RPCExecCallback: { [key: number]: (result: Object) => any } = {}; 32 | private lateCallbacks: (() => any)[] = []; 33 | private conCallback: ( 34 | connected: boolean, 35 | err: Error, 36 | is2_0: boolean 37 | ) => any; 38 | private is2_0 = false; 39 | private reAssign: { [key: string]: { val: any; flags: number } } = {}; 40 | private beingAssigned: string[] = []; 41 | private continuation: { buf: Buffer; offset: number }; 42 | private strictInput = false; 43 | private reconnectDelay = 0 44 | private autoReconnect:NodeJS.Timer 45 | private tryReconnect=false 46 | 47 | constructor(options?: clientOptions) { 48 | if (options == undefined) return; 49 | if (options.strictInput) this.strictInput = true; 50 | } 51 | /** 52 | * True if the Client has completed its hello and is connected 53 | */ 54 | isConnected() { 55 | return this.connected; 56 | } 57 | /** 58 | * True if the client has switched to 2.0 59 | */ 60 | uses2_0() { 61 | return this.is2_0; 62 | } 63 | 64 | /** 65 | * Set and activate the reconnect feature 66 | * 67 | * 68 | * Delay of 20 or less will deactivate this feature 69 | * @param delay Time in milisecconds before the next reconnect attempt 70 | */ 71 | setReconnectDelay(delay: number) { 72 | if(delay<20){ 73 | this.tryReconnect=false 74 | clearTimeout(this.autoReconnect) 75 | } 76 | this.reconnectDelay = delay 77 | this.debug(debugType.basic, `Setting Reconnect dellay to ${delay}`) 78 | } 79 | 80 | /** 81 | * Start the Client 82 | * @param callback Called on connect or error 83 | * @param address Address of the Server. Default = "localhost" 84 | * @param port Port of the Server. Default = 1735 85 | */ 86 | start( 87 | callback?: (connected: boolean, err: Error, is2_0: boolean) => any, 88 | address = "127.0.0.1", 89 | port = 1735 90 | ) { 91 | this.tryReconnect = false; 92 | clearTimeout(this.autoReconnect); 93 | 94 | /:\/\/\w/.test(address) 95 | let parsedAddress = url.parse((/:\/\/\w/.test(address) ? "" : "tcp://") + address) 96 | address = parsedAddress.hostname 97 | port = parseInt(parsedAddress.port) || port 98 | this.conCallback = callback; 99 | this.debug(debugType.basic, `Connecting to ${address} on port ${port}`) 100 | this.__connect(address,port) 101 | } 102 | private __connect(address:string,port:number){ 103 | this.reconnect=false 104 | this.connected = false; 105 | this.address = address; 106 | this.port = port; 107 | this.reAssign = {}; 108 | this.beingAssigned = []; 109 | this.client = net 110 | .connect(port, address, () => { 111 | this.socketConnected = true 112 | this.toServer.Hello(this.clientName); 113 | this.client.on("data", data => { 114 | let pos = 0, 115 | buf = data; 116 | if (this.continuation != null) { 117 | pos = this.continuation.offset; 118 | buf = Buffer.concat([this.continuation.buf, buf]); 119 | this.continuation = null; 120 | } 121 | try { 122 | this.read(buf, pos); 123 | } catch (e) { 124 | this.conCallback(true, e, this.is2_0); 125 | } 126 | }); 127 | }) 128 | .on("close", hadError => { 129 | this.debug(debugType.basic, 'Closing socket') 130 | if(this.reconnectDelay > 20 && this.connected) this.tryReconnect = true 131 | if(this.tryReconnect && !this.reconnect){ 132 | clearTimeout(this.autoReconnect) 133 | this.autoReconnect = setTimeout(()=>{ 134 | this.debug(debugType.basic, `Trying to reconnect to ${address}:${port}`) 135 | this.__connect(address, port); 136 | },this.reconnectDelay) 137 | } 138 | this.socketConnected = false 139 | this.connected = false; 140 | this.oldEntries = this.entries; 141 | this.entries = {}; 142 | this.keymap = {}; 143 | 144 | let reconn: NodeJS.Timer 145 | if (this.reconnect) { 146 | this.__connect(address, port); 147 | } else if (!hadError) this.conCallback(false, null, this.is2_0); 148 | }) 149 | .on("error", err => { 150 | let mesgPar = err.message.split(' ') 151 | if (mesgPar.length < 2 || mesgPar[1] != 'ECONNREFUSED' || this.reconnectDelay < 20) { 152 | this.conCallback(false, err, this.is2_0) 153 | }else{ 154 | this.conCallback(false, null, this.is2_0) 155 | } 156 | }) 157 | .on('end', () => { 158 | this.socketConnected = false 159 | }) 160 | } 161 | /** Attempts to stop the client */ 162 | stop() { 163 | this.client.end() 164 | this.socketConnected = false 165 | this.reconnectDelay = 0 166 | } 167 | /** Immediately closes the client */ 168 | destroy() { 169 | this.client.destroy() 170 | this.socketConnected = false 171 | this.reconnectDelay = 0 172 | this.connected = false 173 | } 174 | /** 175 | * Adds and returns a Listener to be called on change of an Entry 176 | * @param callback Listener 177 | */ 178 | addListener(callback: Listener, getCurrent?: boolean) { 179 | this.listeners.push(callback); 180 | if (getCurrent && this.connected) { 181 | for (let key in this.keymap) { 182 | let entry = this.entries[this.keymap[key]] 183 | callback(key, entry.val, typeNames[entry.typeID], "add", this.keymap[key], entry.flags) 184 | } 185 | } 186 | return callback; 187 | } 188 | /** 189 | * Removes a Listener 190 | * @param listener the Listener to remove 191 | */ 192 | removeListener(listener: Listener) { 193 | var index = this.listeners.indexOf(listener); 194 | if (index > -1) { 195 | this.listeners.splice(index, 1); 196 | } 197 | } 198 | /** 199 | * Get the unique ID of a key or the IDs of all keys if called empty 200 | * @param key name of the key 201 | */ 202 | getKeyID(): { [key: string]: number }; 203 | getKeyID(key: string): number; 204 | getKeyID(key?: string) { 205 | if (key == undefined) { 206 | return this.keymap; 207 | } else return this.keymap[key]; 208 | } 209 | /** 210 | * Gets an Entry 211 | * @param id ID of an Entry 212 | */ 213 | getEntry(id: number) { 214 | return this.entries[id]; 215 | } 216 | /** 217 | * Get an Array of Keys 218 | */ 219 | getKeys() { 220 | return Object.keys(this.keymap); 221 | } 222 | /** 223 | * Get All of the Entries 224 | */ 225 | getEntries() { 226 | return this.entries; 227 | } 228 | private read(buf: Buffer, off: number) { 229 | checkBufLen(buf, off, 1); 230 | if (buf.length == off) return; 231 | if (typeof this.recProto[buf[off]] != 'undefined') { 232 | try { 233 | off = this.recProto[buf[off]](buf, off + 1); 234 | this.read(buf, off); 235 | } catch (e) { 236 | if (e instanceof LengthError) { 237 | this.continuation = { buf, offset: off }; 238 | return; 239 | } else throw e; 240 | } 241 | } else throw new Error("Unknown Message Type " + buf[off]); 242 | } 243 | private readonly recProto: { 244 | [key: number]: (buf: Buffer, offset: number) => number; 245 | } = { 246 | /** Keep Alive */ 247 | 0x00: (buf, off) => { 248 | return off; 249 | }, 250 | /** Protocol Version Unsupported */ 251 | 0x02: (buf, off) => { 252 | checkBufLen(buf, off, 2); 253 | var ver = `${buf[off++]}.${buf[off++]}`; 254 | this.debug(debugType.basic, `version ${this.is2_0 ? '2.0' : '3.0'}`) 255 | this.debug(debugType.basic, `Server supports version ${ver}`) 256 | if (ver === "2.0") { 257 | this.reconnect = true; 258 | this.is2_0 = true; 259 | strLenIdent = numTo2Byte; 260 | } else 261 | this.conCallback( 262 | false, 263 | new Error("Unsupported protocol: " + ver), 264 | this.is2_0 265 | ); 266 | return off; 267 | }, 268 | /** Server Hello Complete */ 269 | 0x03: (buf, off) => { 270 | this.debug(debugType.messageType, 'Received Server Hello Complete') 271 | this.connected = true; 272 | for (let key in this.oldEntries) { 273 | if (typeof this.entries[key] == 'undefined') { 274 | let old = this.oldEntries[key]; 275 | this.Assign(old.val, old.name, old.flags > 0); 276 | } 277 | } 278 | if (this.is2_0) { 279 | this.afterConnect(); 280 | } else { 281 | this.newKeyMap.map(e => { 282 | if (typeof this.keymap[e.name] == 'undefined') { 283 | this.Assign(e.val, e.name, e.flags > 0); 284 | } 285 | }); 286 | if (this.oldEntries != null && this.reconnectDelay >= 20) { 287 | let keys = Object.keys(this.oldEntries) 288 | for (let i = 0; i < keys.length; i++) { 289 | if (typeof this.entries[keys[i]] == 'undefined') { 290 | let entry = this.oldEntries[keys[i]] as Entry 291 | this.Assign(entry.val, entry.name, entry.flags) 292 | } 293 | } 294 | } 295 | this.toServer.HelloComplete(); 296 | if (this.known) { 297 | while (this.updatedIDs.length > 0) { 298 | let e = this.updatedIDs.pop(); 299 | if (typeof this.entries[e] != 'undefined') 300 | this.Update(e, this.entries[e].val); 301 | } 302 | } 303 | } 304 | return off; 305 | }, 306 | /** Server Hello */ 307 | 0x04: (buf, off) => { 308 | this.debug(debugType.messageType, `Received Server Hello`) 309 | checkBufLen(buf, off, 1); 310 | let flags = this.is2_0 ? 0 : buf[off++]; 311 | this.known = flags > 0; 312 | let sName = TypesFrom[e.String](buf, off); 313 | this.serverName = sName.val; 314 | this.debug(debugType.messages, { serverName: sName.val, isKnown: flags > 0 }) 315 | return sName.offset; 316 | }, 317 | /** Entry Assignment */ 318 | 0x10: (buf, off) => { 319 | let keyName = TypesFrom[e.String](buf, off); 320 | this.debug(debugType.messageType, `Received entry assignment for ${keyName.val}`) 321 | off = keyName.offset; 322 | checkBufLen(buf, off, 5 + (this.is2_0 ? 0 : 1)); 323 | let type = buf[off++], 324 | id = (buf[off++] << 8) + buf[off++], 325 | typeName = typeNames[type], 326 | key = keyName.val, 327 | entry: Entry = { 328 | typeID: type, 329 | name: key, 330 | sn: (buf[off++] << 8) + buf[off++], 331 | flags: this.is2_0 ? 0 : buf[off++] 332 | }; 333 | let val = TypesFrom[entry.typeID](buf, off); 334 | entry.val = val.val; 335 | this.entries[id] = entry; 336 | this.keymap[key] = id; 337 | this.callListeners(keyName.val, val.val, typeName, "add", id, entry.flags); 338 | if (typeof this.reAssign[key] != 'undefined') { 339 | let toUpdate = this.reAssign[key]; 340 | this.Update(id, toUpdate.val); 341 | if (!this.is2_0 && entry.flags !== toUpdate.flags) { 342 | this.Flag(id, toUpdate.flags); 343 | } 344 | delete this.reAssign[key]; 345 | } 346 | this.debug(debugType.messages, { key: keyName.val, type, id, sequenceNumber: entry.sn, flags: entry.flags, value: entry.val }) 347 | return val.offset; 348 | }, 349 | /** Entry Update */ 350 | 0x11: (buf, off) => { 351 | this.debug(debugType.messageType, 'Received an entry update') 352 | checkBufLen(buf, off, 4 + (this.is2_0 ? 0 : 1)); 353 | let id = (buf[off++] << 8) + buf[off++], 354 | sn = (buf[off++] << 8) + buf[off++], 355 | type = this.is2_0 ? this.entries[id].typeID : buf[off++], 356 | val = TypesFrom[type](buf, off), 357 | typeName = typeNames[type], 358 | name = ""; 359 | if (typeof this.entries[id] != 'undefined' && type === this.entries[id].typeID) { 360 | let entry = this.entries[id]; 361 | entry.sn = sn; 362 | entry.val = val.val; 363 | name = entry.name; 364 | this.callListeners( 365 | name, 366 | val.val, 367 | typeName, 368 | "update", 369 | id, 370 | entry.flags 371 | ); 372 | } 373 | this.debug(debugType.messages, { id, sequenceNumber: sn, type, value: val.val }) 374 | return val.offset; 375 | }, 376 | /** Entry Flags Update */ 377 | 0x12: (buf, off) => { 378 | this.debug(debugType.messageType, 'Received a flags update') 379 | checkBufLen(buf, off, 3); 380 | let id = (buf[off++] << 8) + buf[off++], 381 | flags = buf[off++]; 382 | if (typeof this.entries[id] != 'undefined') { 383 | let entry = this.entries[id]; 384 | entry.flags = flags; 385 | this.callListeners( 386 | entry.name, 387 | entry.val, 388 | typeNames[entry.typeID], 389 | "flagChange", 390 | id, 391 | flags 392 | ) 393 | } 394 | this.debug(debugType.messages, { id, flags }) 395 | return off; 396 | }, 397 | /** Entry Delete */ 398 | 0x13: (buf, off) => { 399 | this.debug(debugType.messageType, 'Received an entry delete') 400 | checkBufLen(buf, off, 2); 401 | let id = (buf[off++] << 8) + buf[off++], 402 | name = this.entries[id].name, 403 | typename = typeNames[this.entries[id].typeID], 404 | flags = this.entries[id].flags; 405 | delete this.entries[id]; 406 | delete this.keymap[name]; 407 | this.callListeners( 408 | name, 409 | null, 410 | typename, 411 | "delete", 412 | id, 413 | flags 414 | ); 415 | this.debug(debugType.messages, { id }) 416 | return off; 417 | }, 418 | /** Clear All Entries */ 419 | 0x14: (buf, off) => { 420 | this.debug(debugType.messageType, 'Received an entry update') 421 | checkBufLen(buf, off, 4); 422 | let val = 0; 423 | for (let i = 0; i < 4; i++) { 424 | val = (val << 8) + buf[off + i]; 425 | } 426 | if (val === 0xd06cb27a) { 427 | this.entries = {}; 428 | this.keymap = {}; 429 | } 430 | this.debug(debugType.messages, { val, isCorrect: val === 0xd06cb27a }) 431 | return off + 4; 432 | }, 433 | /** RPC Response */ 434 | 0x21: (buf, off) => { 435 | this.debug(debugType.messageType, 'Received an RPC Response') 436 | checkBufLen(buf, off, 4); 437 | let id = (buf[off++] << 8) + buf[off++], 438 | executeID = (buf[off++] << 8) + buf[off++], 439 | len = numFrom128(buf, off), 440 | res = (this.entries[id].val).results, 441 | results = {}, 442 | s: { val: any; offset: number }; 443 | for (let i = 0; i < res.length; i++) { 444 | for (let i = 0; i < res.length; i++) { 445 | s = TypesFrom[res[i].typeId](buf, off); 446 | off = s.offset; 447 | results[res[i].name] = s.val; 448 | } 449 | } 450 | if (typeof this.RPCExecCallback[executeID] != 'undefined') { 451 | this.RPCExecCallback[executeID](results); 452 | delete this.RPCExecCallback[executeID]; 453 | } 454 | this.debug(debugType.messages, { id, executeID, results }) 455 | return off; 456 | } 457 | }; 458 | private afterConnect() { 459 | this.conCallback(true, null, this.is2_0); 460 | while (this.lateCallbacks.length) { 461 | this.lateCallbacks.shift()(); 462 | } 463 | } 464 | private readonly toServer = { 465 | Hello: (serverName: string) => { 466 | this.debug(debugType.messageType, 'Sending a Hello') 467 | if (this.is2_0) { 468 | this.write(toServer.hello2_0); 469 | } else { 470 | let s = TypeBuf[e.String].toBuf(serverName), 471 | buf = Buffer.allocUnsafe(s.length + 3); 472 | buf[0] = 0x01; 473 | buf[1] = 3; 474 | buf[2] = 0; 475 | s.write(buf, 3); 476 | this.write(buf, true); 477 | } 478 | }, 479 | HelloComplete: () => { 480 | this.debug(debugType.messageType, 'Sending a Hello Complete') 481 | this.write(toServer.helloComplete, true); 482 | this.afterConnect(); 483 | } 484 | }; 485 | /** 486 | * Add an Entry 487 | * @param val The Value 488 | * @param name The Key of the Entry 489 | * @param persist Whether the Value should persist on the server through a restart 490 | */ 491 | Assign(val: any, name: string, persist: boolean | number = false) { 492 | this.debug(debugType.messageType, `Assigning ${name}`) 493 | let type = getType(val); 494 | if (this.is2_0 && type === e.RawData) 495 | return new Error("2.0 does not have Raw Data"); 496 | if (type === e.RPC) return new Error("Clients can not assign an RPC"); 497 | if (!this.connected) { 498 | let nID = this.newKeyMap.length; 499 | this.newKeyMap[nID] = { 500 | typeID: type, 501 | val, 502 | flags: +persist, 503 | name: name 504 | }; 505 | this.listeners.map(e => 506 | e(name, val, typeNames[type], "add", -nID - 1, +persist) 507 | ); 508 | return; 509 | } 510 | if (typeof this.keymap[name] != 'undefined') { 511 | return this.Update(this.keymap[name], val); 512 | } 513 | if (this.beingAssigned.indexOf(name) >= 0) { 514 | this.reAssign[name] = { val, flags: +persist }; 515 | return; 516 | } else { 517 | this.beingAssigned.push(name); 518 | } 519 | let n = TypeBuf[e.String].toBuf(name), 520 | f = TypeBuf[type].toBuf(val), 521 | nlen = n.length, 522 | assignLen = this.is2_0 ? 6 : 7, 523 | len = f.length + nlen + assignLen, 524 | buf = Buffer.allocUnsafe(len); 525 | buf[0] = 0x10; 526 | n.write(buf, 1); 527 | buf[nlen + 1] = type; 528 | buf[nlen + 2] = 0xff; 529 | buf[nlen + 3] = 0xff; 530 | buf[nlen + 4] = 0; 531 | buf[nlen + 5] = 0; 532 | if (!this.is2_0) buf[nlen + 6] = +persist; 533 | f.write(buf, nlen + assignLen); 534 | this.debug(debugType.messages, { key: name, type, flags: +persist, val }) 535 | this.write(buf); 536 | } 537 | /** 538 | * Updates an Entry 539 | * @param id The ID of the Entry 540 | * @param val The value of the Entry 541 | */ 542 | Update(id: number, val: any): Error { 543 | this.debug(debugType.messageType, `Updating Entry`) 544 | if (id < 0) { 545 | let nEntry = this.newKeyMap[-id - 1]; 546 | let testVal = this.fixType(val, nEntry.typeID); 547 | if (testVal != null) { 548 | val = testVal; 549 | if (this.connected) { 550 | if (typeof this.keymap[nEntry.name] != 'undefined') { 551 | id = this.keymap[nEntry.name]; 552 | } else { 553 | return this.Assign(val, nEntry.name, nEntry.flags > 0); 554 | } 555 | } else { 556 | nEntry.val = val; 557 | this.listeners.map(e => 558 | e( 559 | nEntry.name, 560 | val, 561 | typeNames[nEntry.typeID], 562 | "update", 563 | id, 564 | nEntry.val 565 | ) 566 | ); 567 | return; 568 | } 569 | } else 570 | return new Error( 571 | `Wrong Type: ${val} is not a ${typeNames[nEntry.typeID]}` 572 | ); 573 | } 574 | if (typeof this.entries[id] == 'undefined') return new Error("ID not found"); 575 | let entry = this.entries[id], 576 | testVal = this.fixType(val, entry.typeID); 577 | if (testVal == null) 578 | return new Error( 579 | `Wrong Type: ${val} is not a ${typeNames[entry.typeID]}` 580 | ); 581 | val = entry.val = testVal; 582 | entry.sn++; 583 | if (!this.connected) { 584 | if (this.updatedIDs.indexOf(id) < 0) this.updatedIDs.push(id); 585 | this.listeners.map(e => 586 | e( 587 | entry.name, 588 | val, 589 | typeNames[entry.typeID], 590 | "update", 591 | id, 592 | entry.flags 593 | ) 594 | ); 595 | return; 596 | } 597 | let f = TypeBuf[entry.typeID].toBuf(val), 598 | updateLen = this.is2_0 ? 5 : 6, 599 | len = f.length + updateLen, 600 | buf = Buffer.allocUnsafe(len); 601 | buf[0] = 0x11; 602 | buf[1] = id >> 8; 603 | buf[2] = id & 0xff; 604 | buf[3] = entry.sn >> 8; 605 | buf[4] = entry.sn & 0xff; 606 | if (!this.is2_0) buf[5] = entry.typeID; 607 | f.write(buf, updateLen); 608 | this.debug(debugType.messages, { id, sequenceNumber: entry.sn, type: entry.typeID, value: val }) 609 | this.write(buf); 610 | this.listeners.map(e => 611 | e( 612 | entry.name, 613 | val, 614 | typeNames[entry.typeID], 615 | "update", 616 | id, 617 | entry.flags 618 | ) 619 | ); 620 | } 621 | /** 622 | * Updates the Flag of an Entry 623 | * @param id The ID of the Entry 624 | * @param flags Whether the Entry should persist through a restart on the server 625 | */ 626 | Flag(id: number, flags: boolean | number = false) { 627 | this.debug(debugType.messageType, `Updating Flags`) 628 | if (this.is2_0) return new Error("2.0 does not support flags"); 629 | if (typeof this.entries[id] == 'undefined') return new Error("Does not exist"); 630 | this.debug(debugType.messages, { id, flags: +flags }) 631 | this.write(Buffer.from([0x12, id >> 8, id & 0xff, +flags])); 632 | } 633 | /** 634 | * Deletes an Entry 635 | * @param id The ID of the Entry 636 | */ 637 | Delete(id: number) { 638 | this.debug(debugType.messageType, `Delete Entry`) 639 | if (this.is2_0) return new Error("2.0 does not support delete"); 640 | if (typeof this.entries[id] == 'undefined') return new Error("Does not exist"); 641 | this.write(Buffer.from([0x13, id >> 8, id & 0xff])); 642 | this.debug(debugType.messages, `Delete ${id}`) 643 | } 644 | /** 645 | * Deletes All Entries 646 | */ 647 | DeleteAll() { 648 | this.debug(debugType.messageType, `Delete All Entries`) 649 | if (this.is2_0) return new Error("2.0 does not support delete"); 650 | this.write(toServer.deleteAll); 651 | this.entries = {}; 652 | this.keymap = {}; 653 | } 654 | /** 655 | * Executes an RPC 656 | * @param id The ID of the RPC Entry 657 | * @param val The Values of the Parameters 658 | * @param callback To be called with the Results 659 | */ 660 | RPCExec(id: number, val: Object, callback: (result: Object) => any) { 661 | this.debug(debugType.messageType, `Execute RPC`) 662 | if (this.is2_0) return new Error("2.0 does not support RPC"); 663 | if (typeof this.entries[id] == 'undefined') return new Error("Does not exist"); 664 | let entry = this.entries[id]; 665 | if (entry.typeID !== e.RPC) return new Error("Is not an RPC"); 666 | let par = (entry.val).par, 667 | f: toBufRes[] = [], 668 | value: any, 669 | len = 0, 670 | parName = ""; 671 | for (let i = 0; i < par.length; i++) { 672 | parName = par[i].name; 673 | value = typeof val[parName] != 'undefined' ? val[parName] : par[i].default; 674 | let testVal = this.fixType(value, par[i].typeId); 675 | if (testVal == null) 676 | return new Error( 677 | `Wrong Type: ${value} is not a ${typeNames[par[i].typeId]}` 678 | ); 679 | let n = TypeBuf[par[i].typeId].toBuf(testVal); 680 | len += n.length; 681 | f.push(n); 682 | } 683 | let encLen = numTo128(len), 684 | buf = Buffer.allocUnsafe(len + encLen.length + 5), 685 | off = 5 + encLen.length, 686 | randId = Math.floor(Math.random() * 0xffff); 687 | buf[0] = 0x21; 688 | buf[1] = id >> 8; 689 | buf[2] = id & 0xff; 690 | buf[3] = randId >> 8; 691 | buf[4] = randId & 0xff; 692 | encLen.copy(buf, 5); 693 | for (let i = 0; i < f.length; i++) { 694 | f[i].write(buf, off); 695 | off += f[i].length; 696 | } 697 | this.debug(debugType.messages, { id, randId, val }) 698 | this.write(buf); 699 | this.RPCExecCallback[randId] = callback; 700 | } 701 | private keys: string[]; 702 | private readonly keepAlive = Buffer.from([0]); 703 | private aliveTimer: NodeJS.Timer; 704 | private bufferTimer: NodeJS.Timer; 705 | private buffersToSend: Buffer[] = []; 706 | /** 707 | * Direct Write to the Server 708 | * @param buf The Buffer to be sent 709 | * @param immediate whether the write should happen right away 710 | */ 711 | write(buf: Buffer, immediate = false) { 712 | if (this.aliveTimer) clearTimeout(this.aliveTimer); 713 | if (!this.socketConnected) return 714 | this.aliveTimer = setTimeout(() => { 715 | this.write(this.keepAlive); 716 | }, 1000); 717 | if (this.aliveTimer.unref) this.aliveTimer.unref(); 718 | this.buffersToSend.push(buf); 719 | let writeFunc = () => { 720 | this.debug(debugType.basic, `Writing to Server`) 721 | this.debug(debugType.everything, buf) 722 | this.client.write(Buffer.concat(this.buffersToSend)); 723 | this.bufferTimer = null; 724 | this.buffersToSend = [] 725 | } 726 | if (immediate) { 727 | writeFunc() 728 | } 729 | else { 730 | this.debug(debugType.everything, 'Buffering write') 731 | if (this.bufferTimer == null) 732 | this.bufferTimer = setTimeout(writeFunc, 20); 733 | } 734 | } 735 | startDebug(name: string, debugLevel = debugType.basic) { 736 | if (typeof name == 'string' && name.length > 0) { 737 | this.debug = (level: debugType, st: any) => { 738 | if (level > debugLevel) return 739 | if (typeof st == 'string') { 740 | console.log(name + ': ' + st) 741 | } else { 742 | console.log({ [name]: st }) 743 | } 744 | } 745 | } 746 | } 747 | private fixType(val: any, type: e) { 748 | if (Array.isArray(val)) { 749 | if (type === e.BoolArray) { 750 | if (val.every(e => typeof e === "boolean")) return val; 751 | else if (!this.strictInput) { 752 | let tryVal = []; 753 | for (let i = 0; i < val.length; i++) { 754 | if (val[i] == "true" || val[i] == "false") 755 | tryVal.push(val[i] == "true"); 756 | else return; 757 | } 758 | return tryVal; 759 | } 760 | } else if (type === e.DoubleArray) { 761 | if (val.every(e => typeof e === "number")) { 762 | return val; 763 | } else if (!this.strictInput) { 764 | let tryVal = []; 765 | for (let i = 0; i < val.length; i++) { 766 | let testVal = parseFloat(val[i]); 767 | if (Number.isNaN(testVal)) return; 768 | else tryVal.push(testVal); 769 | } 770 | return tryVal; 771 | } 772 | } else if (type === e.StringArray) { 773 | if (val.every(e => typeof e === "string")) { 774 | return val; 775 | } else if (!this.strictInput) { 776 | return val.map(a => a.toString()); 777 | } 778 | } 779 | } else { 780 | if (type === e.Boolean) { 781 | if (typeof val === "boolean") { 782 | return val; 783 | } else if ( 784 | !this.strictInput && 785 | (val == "true" || val == "false") 786 | ) { 787 | return val == "true"; 788 | } 789 | } else if (type === e.Double) { 790 | if (typeof val === "number") { 791 | return val; 792 | } else if (!this.strictInput) { 793 | let testVal = parseFloat(val); 794 | if (!Number.isNaN(testVal)) { 795 | return testVal; 796 | } 797 | } 798 | } else if (type === e.String) { 799 | if (!this.strictInput || typeof val == "string") 800 | return val.toString(); 801 | } else if (type === e.RawData && Buffer.isBuffer(val)) return val; 802 | } 803 | if (type === e.RawData && !this.strictInput) { 804 | if ( 805 | typeof val == "number" && 806 | val <= 0xff && 807 | val >= 0 && 808 | Number.isInteger(val) 809 | ) { 810 | return Buffer.from([val]); 811 | } else if ( 812 | Array.isArray(val) && 813 | val.every( 814 | a => 815 | typeof a == "number" && 816 | a >= 0 && 817 | a <= 0xff && 818 | Number.isInteger(a) 819 | ) 820 | ) { 821 | return Buffer.from(val); 822 | } else if (typeof val == "string") { 823 | return Buffer.from(val); 824 | } 825 | } 826 | } 827 | private callListeners: Listener = (key, val, valType, type, id, flags) => { 828 | for (let i = 0; i < this.listeners.length; i++) { 829 | if (this.connected) { 830 | this.listeners[i](key, val, valType, type, id, flags); 831 | } else { 832 | this.lateCallbacks.push(() => 833 | this.listeners[i](key, val, valType, type, id, flags) 834 | ); 835 | } 836 | } 837 | } 838 | } 839 | const typeNames = { 840 | 0x00: "Boolean", 841 | 0x01: "Number", 842 | 0x02: "String", 843 | 0x03: "Buffer", 844 | 0x10: "BooleanArray", 845 | 0x11: "NumberArray", 846 | 0x12: "StringArray", 847 | 0x20: "RPC" 848 | }; 849 | function checkTypeI(val: any, type: number) { 850 | if (Array.isArray(val)) { 851 | if (type === e.BoolArray && val.every(e => typeof e === "boolean")) 852 | return true; 853 | else if ( 854 | type === e.DoubleArray && 855 | val.every(e => typeof e === "number") 856 | ) 857 | return true; 858 | else if ( 859 | type === e.StringArray && 860 | val.every(e => typeof e === "string") 861 | ) 862 | return true; 863 | else return false; 864 | } else { 865 | if (type === e.Boolean && typeof val === "boolean") return true; 866 | else if (type === e.Double && typeof val === "number") return true; 867 | else if (type === e.String && typeof val === "string") return true; 868 | else if (type === e.RawData && Buffer.isBuffer(val)) return true; 869 | else return false; 870 | } 871 | } 872 | function getType(val: any) { 873 | if (Array.isArray(val)) { 874 | if (typeof val[0] === "boolean") return 0x10; 875 | else if (typeof val[0] === "number") return 0x11; 876 | else if (typeof val[0] === "string") return 0x12; 877 | else if (typeof val[0] === "object") return 0x20; 878 | } else { 879 | if (typeof val === "boolean") return 0x00; 880 | else if (typeof val === "number") return 0x01; 881 | else if (typeof val === "string") return 0x02; 882 | else if (Buffer.isBuffer(val)) return 0x03; 883 | } 884 | } 885 | const toServer = { 886 | helloComplete: Buffer.from([0x05]), 887 | deleteAll: Buffer.from([0x14, 0xd0, 0x6c, 0xb2, 0x7a]), 888 | hello2_0: Buffer.from([0x01, 2, 0]) 889 | }; 890 | export interface Entry { 891 | typeID: number; 892 | name: string; 893 | sn: number; 894 | flags: number; 895 | val?: any; 896 | } 897 | interface newEntry { 898 | typeID: number; 899 | name: string; 900 | val: any; 901 | flags: number; 902 | oldID?: number; 903 | } 904 | const enum e { 905 | Boolean = 0x00, 906 | Double = 0x01, 907 | String = 0x02, 908 | RawData = 0x03, 909 | BoolArray = 0x10, 910 | DoubleArray = 0x11, 911 | StringArray = 0x12, 912 | RPC = 0x20 913 | } 914 | interface RPC { 915 | name: string; 916 | par: RPCPar[]; 917 | results: RPCResult[]; 918 | } 919 | interface RPCPar { 920 | typeId: number; 921 | typeName: string; 922 | name: string; 923 | default: any; 924 | } 925 | interface RPCResult { 926 | typeId: number; 927 | typeName: string; 928 | name: string; 929 | } 930 | type bufFrom = ( 931 | buf: Buffer, 932 | offset: number 933 | ) => { 934 | offset: number; 935 | val: T; 936 | }; 937 | interface toBufRes { 938 | length: number; 939 | write: (buf: Buffer, off: number) => any; 940 | } 941 | interface f { 942 | toBuf?: (val: T) => toBufRes; 943 | fromBuf: ( 944 | buf: Buffer, 945 | offset: number 946 | ) => { 947 | offset: number; 948 | val: T; 949 | }; 950 | } 951 | interface fromBuf { 952 | [key: number]: f; 953 | 0x00: f; 954 | 0x01: f; 955 | 0x02: f; 956 | 0x03: f; 957 | 0x10: f; 958 | 0x11: f; 959 | 0x12: f; 960 | 0x20: f; 961 | } 962 | 963 | const TypeBuf: fromBuf = { 964 | 0x00: >{ 965 | toBuf: val => { 966 | return { 967 | length: 1, 968 | write: (buf, off) => { 969 | buf[off] = +val; 970 | } 971 | }; 972 | }, 973 | fromBuf: (buf, off) => { 974 | checkBufLen(buf, off, 1); 975 | return { 976 | offset: off + 1, 977 | val: buf[off] > 0 978 | }; 979 | } 980 | }, 981 | 0x01: >{ 982 | toBuf: val => { 983 | return { 984 | length: 8, 985 | write: (buf, off) => { 986 | ieee754.write(buf, val, off, false, 52, 8); 987 | } 988 | }; 989 | }, 990 | fromBuf: (buf, off) => { 991 | checkBufLen(buf, off, 8); 992 | return { 993 | offset: off + 8, 994 | val: ieee754.read(buf, off, false, 52, 8) 995 | }; 996 | } 997 | }, 998 | 0x02: >{ 999 | toBuf: val => { 1000 | let bufT = Buffer.concat([ 1001 | strLenIdent(val.length), 1002 | Buffer.from(val, "utf8") 1003 | ]); 1004 | return { 1005 | length: bufT.length, 1006 | write: (buf, off) => { 1007 | bufT.copy(buf, off); 1008 | } 1009 | }; 1010 | }, 1011 | fromBuf: (buf, off) => { 1012 | return fromLEBuf(buf, off); 1013 | } 1014 | }, 1015 | 0x03: >{ 1016 | toBuf: val => { 1017 | let len = numTo128(val.length); 1018 | return { 1019 | length: val.length + len.length, 1020 | write: (buf, off) => { 1021 | len.copy(buf, off); 1022 | val.copy(buf, off + len.length); 1023 | } 1024 | }; 1025 | }, 1026 | fromBuf: (buf, off) => { 1027 | let { val, offset } = numFrom128(buf, off), 1028 | nbuf = Buffer.allocUnsafe(val), 1029 | end = offset + val; 1030 | checkBufLen(buf, off, val); 1031 | buf.copy(nbuf, 0, offset); 1032 | return { 1033 | offset: end, 1034 | val: nbuf 1035 | }; 1036 | } 1037 | }, 1038 | 0x10: >{ 1039 | toBuf: val => { 1040 | return { 1041 | length: val.length + 1, 1042 | write: (buf, off) => { 1043 | buf[off] = val.length; 1044 | for (let i = 0; i < val.length; i++) { 1045 | buf[off + i] = +val[i]; 1046 | } 1047 | } 1048 | }; 1049 | }, 1050 | fromBuf: (buf, off) => { 1051 | checkBufLen(buf, off, 1); 1052 | let len = buf[off], 1053 | res: boolean[] = []; 1054 | off++; 1055 | checkBufLen(buf, off, len); 1056 | for (let i = 0; i < len; i++) { 1057 | res.push(buf[off + i] > 0); 1058 | } 1059 | return { 1060 | offset: off + len, 1061 | val: res 1062 | }; 1063 | } 1064 | }, 1065 | 0x11: >{ 1066 | toBuf: val => { 1067 | let len = val.length; 1068 | return { 1069 | length: 8 * val.length + 1, 1070 | write: (buf, off) => { 1071 | for (let i = 0; i < val.length; i++) { 1072 | buf[off] = val.length; 1073 | off++; 1074 | ieee754.write(buf, val[i], off + 8 * i, false, 52, 8); 1075 | } 1076 | } 1077 | }; 1078 | }, 1079 | fromBuf: (buf, off) => { 1080 | checkBufLen(buf, off, 1); 1081 | let val = buf[off], 1082 | num: number[] = []; 1083 | off++; 1084 | checkBufLen(buf, off, 8 * val); 1085 | for (let i = 0; i < val; i++) { 1086 | num.push(ieee754.read(buf, off + i * 8, false, 52, 8)); 1087 | } 1088 | return { 1089 | offset: off + val * 8, 1090 | val: num 1091 | }; 1092 | } 1093 | }, 1094 | 0x12: >{ 1095 | toBuf: val => { 1096 | let lens: Buffer[] = [], 1097 | len = 1; 1098 | for (let i = 0; i < val.length; i++) { 1099 | lens[i] = Buffer.concat([ 1100 | strLenIdent(val[i].length), 1101 | Buffer.from(val[i]) 1102 | ]); 1103 | len += lens[i].length; 1104 | } 1105 | return { 1106 | length: len, 1107 | write: (buf, off) => { 1108 | buf[off] = val.length; 1109 | off++; 1110 | for (let i = 0; i < val.length; i++) { 1111 | lens[i].copy(buf, off); 1112 | off += lens[i].length; 1113 | } 1114 | } 1115 | }; 1116 | }, 1117 | fromBuf: (buf, off) => { 1118 | checkBufLen(buf, off, 1); 1119 | let len = buf[off], 1120 | s: string[] = [], 1121 | st: { offset: number; val: string }; 1122 | off++; 1123 | for (let i = 0; i < len; i++) { 1124 | st = fromLEBuf(buf, off); 1125 | s[i] = st.val; 1126 | off = st.offset; 1127 | } 1128 | return { 1129 | offset: off, 1130 | val: s 1131 | }; 1132 | } 1133 | }, 1134 | 0x20: >{ 1135 | fromBuf: (buf, off) => { 1136 | checkBufLen(buf, off, 1); 1137 | let st: { val: string; offset: number }; 1138 | if (buf[off] !== 1) throw new Error("Unsupported RPC Definition"); 1139 | off++; 1140 | st = fromLEBuf(buf, off); 1141 | off = st.offset; 1142 | checkBufLen(buf, off, 1); 1143 | let name = st.val, 1144 | parNum = buf[off], 1145 | par: RPCPar[] = [], 1146 | results: RPCResult[] = [], 1147 | s = { offset: 0, val: "" }, 1148 | resNum = 0; 1149 | off++; 1150 | for (let i = 0; i < parNum; i++) { 1151 | let lastPar: RPCPar = { 1152 | typeId: 0, 1153 | typeName: "", 1154 | name: "", 1155 | default: 0 1156 | }; 1157 | checkBufLen(buf, off, 1); 1158 | lastPar.typeId = buf[off]; 1159 | lastPar.typeName = typeNames[lastPar.typeId]; 1160 | s = fromLEBuf(buf, off); 1161 | lastPar.name = s.val; 1162 | off = s.offset; 1163 | let t = TypesFrom[lastPar.typeId](buf, off); 1164 | lastPar.default = t.val; 1165 | off = t.offset; 1166 | par.push(lastPar); 1167 | } 1168 | checkBufLen(buf, off, 1); 1169 | resNum = buf[off++]; 1170 | for (let i = 0; i < resNum; i++) { 1171 | let res: RPCResult = { typeId: 0, typeName: "", name: "" }; 1172 | checkBufLen(buf, off, 1); 1173 | res.typeId = buf[off]; 1174 | res.typeName = typeNames[res.typeId]; 1175 | s = fromLEBuf(buf, off + 1); 1176 | res.name = s.val; 1177 | off = s.offset; 1178 | results.push(res); 1179 | } 1180 | return { 1181 | offset: off, 1182 | val: { 1183 | name, 1184 | par, 1185 | results 1186 | } 1187 | }; 1188 | } 1189 | } 1190 | }; 1191 | interface typesFrom { 1192 | [key: number]: bufFrom; 1193 | 0x00: bufFrom; 1194 | 0x01: bufFrom; 1195 | 0x02: bufFrom; 1196 | 0x03: bufFrom; 1197 | 0x10: bufFrom; 1198 | 0x11: bufFrom; 1199 | 0x12: bufFrom; 1200 | 0x20: bufFrom; 1201 | //0x21: bufFrom 1202 | } 1203 | var TypesFrom: typesFrom = { 1204 | 0x00: TypeBuf[e.Boolean].fromBuf, 1205 | 0x01: TypeBuf[e.Double].fromBuf, 1206 | 0x02: TypeBuf[e.String].fromBuf, 1207 | 0x03: TypeBuf[e.RawData].fromBuf, 1208 | 0x10: TypeBuf[e.BoolArray].fromBuf, 1209 | 0x11: TypeBuf[e.DoubleArray].fromBuf, 1210 | 0x12: TypeBuf[e.StringArray].fromBuf, 1211 | 0x20: TypeBuf[e.RPC].fromBuf 1212 | //0x21: TypeBuf[e.Byte].fromBuf 1213 | }; 1214 | /** 1215 | * Decodes String where first bytes are length encoded using LEB128 1216 | * @param buf Buffer to red from 1217 | * @param offset position to start reading from 1218 | * @throws LengthError 1219 | */ 1220 | function fromLEBuf(buf: Buffer, offset: number) { 1221 | let res = numFrom128(buf, offset), 1222 | end = res.offset + res.val; 1223 | checkBufLen(buf, res.offset, res.val); 1224 | return { offset: end, val: buf.slice(res.offset, end).toString("utf8") }; 1225 | } 1226 | 1227 | function numTo128(num: number) { 1228 | let n = num; 1229 | let r: number[] = []; 1230 | while (n > 0x07f) { 1231 | r.push((n & 0x7f) | 0x80); 1232 | n = n >> 7; 1233 | } 1234 | r.push(n); 1235 | return Buffer.from(r); 1236 | } 1237 | function numTo2Byte(num: number) { 1238 | return Buffer.from([(this >> 8) & 0xff, this & 0xff]); 1239 | } 1240 | 1241 | /** 1242 | * Decodes a number encoded in LEB128 1243 | * @param buf Buffer to red from 1244 | * @param offset position to start reading from 1245 | * @throws LengthError 1246 | */ 1247 | function numFrom128(buf: Buffer, offset: number) { 1248 | let r = 0, 1249 | n = buf[offset]; 1250 | offset++; 1251 | r = n & 0x7f; 1252 | while (n > 0x7f) { 1253 | checkBufLen(buf, offset, 1); 1254 | n = buf[offset]; 1255 | r = (r << 7) + (n & 0x7f); 1256 | offset++; 1257 | } 1258 | return { 1259 | val: r, 1260 | offset 1261 | }; 1262 | } 1263 | /** 1264 | * Error thrown when buffer is too short 1265 | */ 1266 | export class LengthError extends Error { 1267 | buf: Buffer; 1268 | position: number; 1269 | constructor(buf: Buffer, possition: number, length: number); 1270 | constructor(mesg: string); 1271 | constructor(mesg: string | Buffer, pos = 0, length = 1) { 1272 | if (typeof mesg !== "string") { 1273 | super( 1274 | `Trying to read ${length} bytes from position ${pos} of a buffer that is ${ 1275 | mesg.length 1276 | } long` 1277 | ); 1278 | this.buf = mesg; 1279 | this.position = pos; 1280 | } else super(mesg); 1281 | } 1282 | } 1283 | /** 1284 | * Check if the Buffer is long enought 1285 | * @param buf Buffer to check the length of 1286 | * @param start Position to read from 1287 | * @param length Number of bytes that will be read 1288 | * @throws LengthError 1289 | */ 1290 | function checkBufLen(buf: Buffer, start: number, length: number) { 1291 | if (buf.length < start + length - 1) 1292 | throw new LengthError(buf, start, length); 1293 | } 1294 | 1295 | export interface clientOptions { 1296 | strictInput?: boolean; 1297 | } 1298 | export const enum debugType { 1299 | /** Client connection status */ 1300 | basic, 1301 | /** All message types received */ 1302 | messageType, 1303 | /** All decoded messages */ 1304 | messages, 1305 | /** Client Status and write data */ 1306 | everything, 1307 | 1308 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wpilib-nt-client", 3 | "version": "1.7.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "8.10.59", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", 10 | "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==", 11 | "dev": true 12 | }, 13 | "ieee754": { 14 | "version": "1.1.13", 15 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 16 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wpilib-nt-client", 3 | "version": "1.7.2", 4 | "description": "Client for FRC Network Tables", 5 | "main": "index.js", 6 | "types": "types/index.d.ts", 7 | "engines": { 8 | "node": ">4.0" 9 | }, 10 | "scripts": { 11 | "test": "node test/test.js", 12 | "prepack": "tsc" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/rakusan2/FRC-NT-Client.git" 17 | }, 18 | "keywords": [ 19 | "FIRST", 20 | "FRC", 21 | "NetworkTables", 22 | "wpilib" 23 | ], 24 | "author": "Tomas Rakusan", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/rakusan2/FRC-NT-Client/issues" 28 | }, 29 | "homepage": "https://github.com/rakusan2/FRC-NT-Client#readme", 30 | "dependencies": { 31 | "ieee754": "^1.1.8" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "~8.10.59" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import { Client, debugType } from "../"; 2 | const ntClient = new Client(); 3 | const ntClient2 = new Client(); 4 | let args = process.argv.slice(2); 5 | ntClient.startDebug("Debug1", debugType.messages); 6 | ntClient2.startDebug("Debug2", debugType.messageType); 7 | ntClient.start((con, err, is2) => { 8 | console.log({ con, err,is2 }); 9 | if (err != null) throw err; 10 | if(!con)return 11 | ntClient.Assign("3", "/SmartDashboard/test"); 12 | ntClient.Assign("4", "/SmartDashboard/test"); 13 | ntClient.Assign("5", "/SmartDashboard/test"); 14 | ntClient.Assign("6", "/SmartDashboard/test"); 15 | setTimeout(() => { 16 | let id = ntClient.getKeyID("/SmartDashboard/test"), 17 | entry = ntClient.getEntry(id), 18 | ids = ntClient.getKeyID(); 19 | console.log({ id, entry, ids }); 20 | if(entry == null) throw new Error("Non Existant Entry") 21 | ntClient2.start((con, err) => { 22 | console.log({ con, err, type: "2nd" }); 23 | 24 | if(!con)return 25 | setTimeout(() => { 26 | console.log("CLOSING") 27 | ntClient.stop() 28 | ntClient2.destroy() 29 | }, 2000); 30 | 31 | },args[0],parseInt(args[1])||undefined); 32 | }, 1000); 33 | },args[0],parseInt(args[1])||undefined); 34 | ntClient.addListener((key, val, valType, type, id, flags) => { 35 | console.log({ key, val, valType, type, id, flags }); 36 | }); 37 | ntClient2.addListener((key, val, valType, type, id, flags) => { 38 | if (key === "/SmartDashboard/test") { 39 | console.log({ t: "2", key, val, valType, type, id, flags }); 40 | } 41 | }); 42 | ntClient.setReconnectDelay(1000); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "noImplicitAny": false, 6 | "sourceMap": false, 7 | "declaration": true, 8 | "declarationDir": "types", 9 | "moduleResolution": "node" 10 | }, 11 | "exclude": [ 12 | "types" 13 | ] 14 | } -------------------------------------------------------------------------------- /typings/ieee754.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ieee754'{ 2 | /** 3 | * Read IEEE754 value from buffer 4 | * @param buffer The Buffer 5 | * @param offset Offset into the buffer 6 | * @param isLE Is little endian 7 | * @param mLen Mantissa length 8 | * @param nBytes Number of bytes 9 | */ 10 | function read(buffer:Buffer,offset:number,isLE:boolean,mLen:number,nBytes:number):number 11 | /** 12 | * Write IEEE754 value to buffer 13 | * @param buffer The Buffer 14 | * @param value Value to set 15 | * @param offset Offset into the buffer 16 | * @param isLE Is little endian 17 | * @param mLen Mantissa length 18 | * @param nBytes Number of bytes 19 | */ 20 | function write(buffer:Buffer,value:number,offset:number,isLE:boolean,mLen:number,nBytes:number):void 21 | } 22 | --------------------------------------------------------------------------------