ntcore-ts-client
Version:
A TypeScript library for communication over [WPILib's NetworkTables 4.1 protocol](https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc).
445 lines • 17.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkTablesSocket = void 0;
var tslib_1 = require("tslib");
var msgpack_1 = require("@msgpack/msgpack");
var isomorphic_ws_1 = tslib_1.__importDefault(require("isomorphic-ws"));
var schemas_1 = require("../types/schemas");
var types_1 = require("../types/types");
var util_1 = require("../util/util");
/** Socket for NetworkTables 4.1 */
var NetworkTablesSocket = /** @class */ (function () {
/**
* Creates a new NetworkTables socket.
* @param serverUrl - The URL of the server to connect to.
* @param onSocketOpen - Called when the socket is opened.
* @param onSocketClose - Called when the socket is closed.
* @param onTopicUpdate - Called when a topic is updated.
* @param onAnnounce - Called when a topic is announced.
* @param onUnannounce - Called when a topic is unannounced.
* @param onProperties - Called when a topic's properties are updated.
* @param autoConnect - Whether to automatically connect to the server.
*/
function NetworkTablesSocket(serverUrl, onSocketOpen, onSocketClose, onTopicUpdate, onAnnounce, onUnannounce, onProperties, autoConnect) {
this.connectionListeners = new Set();
this.lastHeartbeatDate = 0;
this.offset = 0;
this.bestRtt = -1;
this.autoConnect = true;
this.messageQueue = [];
// Connect to the server using the provided URL
this._websocket = new isomorphic_ws_1.default(serverUrl, [NetworkTablesSocket.PROTOCOL_V4_1, NetworkTablesSocket.PROTOCOL_V4_0]);
this.serverUrl = serverUrl;
this.onSocketOpen = onSocketOpen;
this.onSocketClose = onSocketClose;
this.onTopicUpdate = onTopicUpdate;
this.onAnnounce = onAnnounce;
this.onUnannounce = onUnannounce;
this.onProperties = onProperties;
this.autoConnect = autoConnect;
this.init();
}
Object.defineProperty(NetworkTablesSocket.prototype, "websocket", {
get: function () {
return this._websocket;
},
set: function (websocket) {
this._websocket = websocket;
},
enumerable: false,
configurable: true
});
/**
* Gets the instance of the NetworkTables socket.
* @param serverUrl - The URL of the server to connect to.
* @param onSocketOpen - Called when the socket is opened.
* @param onSocketClose - Called when the socket is closed.
* @param onTopicUpdate - Called when a topic is updated.
* @param onAnnounce - Called when a topic is announced.
* @param onUnannounce - Called when a topic is unannounced.
* @param onProperties - Called when a topic's properties are updated.
* @param autoConnect - Whether to automatically connect to the server.
* @returns The instance of the NetworkTables socket.
*/
NetworkTablesSocket.getInstance = function (serverUrl, onSocketOpen, onSocketClose, onTopicUpdate, onAnnounce, onUnannounce, onProperties, autoConnect) {
if (autoConnect === void 0) { autoConnect = true; }
var instance = this.instances.get(serverUrl);
if (!instance) {
instance = new this(serverUrl, onSocketOpen, onSocketClose, onTopicUpdate, onAnnounce, onUnannounce, onProperties, autoConnect);
this.instances.set(serverUrl, instance);
}
return instance;
};
/**
* Initialization. This is done outside of the constructor to allow for
* the socket to refresh itself.
*/
NetworkTablesSocket.prototype.init = function () {
var _this = this;
var heartbeatInterval;
if (this._websocket) {
// Open handler
this._websocket.onopen = function () {
// Setup heartbeat or RTT
if (_this._websocket.protocol === 'v4.1.networktables.first.wpi.edu') {
// eslint-disable-next-line no-console
console.info('Connected on NT 4.1');
}
else {
// eslint-disable-next-line no-console
console.info('Connected on NT 4.0');
// Start heartbeat
// Only send heartbeat at this rate if we are on NT 4.0
heartbeatInterval = setInterval(function () {
if (_this.isConnected()) {
_this.heartbeat();
}
}, NetworkTablesSocket.RTT_PERIOD_V4_0);
}
// eslint-disable-next-line no-console
console.info('Robot Connected!');
_this.updateConnectionListeners();
_this.onSocketOpen();
_this.sendQueuedMessages();
};
// Close handler
this._websocket.onclose = function (e) {
// Notify client and cancel heartbeat
_this.updateConnectionListeners();
_this.onSocketClose();
if (heartbeatInterval != null) {
clearInterval(heartbeatInterval);
}
// Lost connection message
console.warn('Unable to connect to Robot', e.reason);
// Attempt to reconnect
if (_this.autoConnect) {
console.warn("Reconnect will be attempted in ".concat(NetworkTablesSocket.RECONNECT_TIMEOUT, " milliseconds."));
setTimeout(function () {
_this._websocket = new isomorphic_ws_1.default(_this.serverUrl, [
NetworkTablesSocket.PROTOCOL_V4_1,
NetworkTablesSocket.PROTOCOL_V4_0,
]);
_this.init();
}, NetworkTablesSocket.RECONNECT_TIMEOUT);
}
};
this._websocket.binaryType = 'arraybuffer';
// Set up event listeners for messages and errors
this._websocket.onmessage = function (event) { return _this.onMessage(event); };
this._websocket.onerror = function (event) { return _this.onError(event); };
}
};
/**
* Reset the socket and reconnect to the server.
* @param serverUrl - The URL of the server to connect to.
*/
NetworkTablesSocket.prototype.reinstantiate = function (serverUrl) {
this.close();
this.serverUrl = serverUrl;
this._websocket = new isomorphic_ws_1.default(this.serverUrl, [
NetworkTablesSocket.PROTOCOL_V4_1,
NetworkTablesSocket.PROTOCOL_V4_0,
]);
this.init();
};
/**
* Returns whether the socket is connected.
* @returns Whether the socket is connected.
*/
NetworkTablesSocket.prototype.isConnected = function () {
return this._websocket.readyState === isomorphic_ws_1.default.OPEN;
};
/**
* Returns whether the socket is connecting.
* @returns Whether the socket is connecting.
*/
NetworkTablesSocket.prototype.isConnecting = function () {
return this._websocket.readyState === isomorphic_ws_1.default.CONNECTING;
};
/**
* Returns whether the socket is closing.
* @returns Whether the socket is closing.
*/
NetworkTablesSocket.prototype.isClosing = function () {
return this._websocket.readyState === isomorphic_ws_1.default.CLOSING;
};
/**
* Returns whether the socket is closed.
* @returns Whether the socket is closed.
*/
NetworkTablesSocket.prototype.isClosed = function () {
return this._websocket.readyState === isomorphic_ws_1.default.CLOSED;
};
/**
* Wait for the socket to connect.
* @returns A promise that resolves when the socket is connected.
*/
NetworkTablesSocket.prototype.waitForConnection = function () {
var _this = this;
return new Promise(function (resolve) {
if (_this.isConnected()) {
resolve();
}
else {
var listener_1 = function () {
if (_this.isConnected()) {
_this.removeConnectionListener(listener_1);
resolve();
}
};
_this.addConnectionListener(listener_1);
}
});
};
/**
* Create a connection listener.
* @param callback - Called when the connection state changes.
* @param immediateNotify - Whether to immediately notify the callback of the current connection state.
* @returns A function that removes the listener.
*/
NetworkTablesSocket.prototype.addConnectionListener = function (callback, immediateNotify) {
var _this = this;
this.connectionListeners.add(callback);
if (immediateNotify) {
callback(this.isConnected());
}
return function () { return _this.removeConnectionListener(callback); };
};
/**
* Remove a connection listener.
* @param callback - The callback to remove.
*/
NetworkTablesSocket.prototype.removeConnectionListener = function (callback) {
this.connectionListeners.delete(callback);
};
/**
* Updates all connection listeners with the current connection state.
*/
NetworkTablesSocket.prototype.updateConnectionListeners = function () {
var _this = this;
this.connectionListeners.forEach(function (callback) { return callback(_this.isConnected()); });
};
/**
* Stops auto-reconnecting to the server.
*/
NetworkTablesSocket.prototype.stopAutoConnect = function () {
this.autoConnect = false;
};
/**
* Starts auto-reconnecting to the server.
*/
NetworkTablesSocket.prototype.startAutoConnect = function () {
this.autoConnect = true;
};
/**
* Handle a message from the websocket.
* @param event - The message event.
*/
NetworkTablesSocket.prototype.onMessage = function (event) {
var _this = this;
var _a;
(_a = this.connectionListeners) === null || _a === void 0 ? void 0 : _a.forEach(function (f) { return f(_this.isConnected()); });
if (event.data instanceof ArrayBuffer || event.data instanceof Uint8Array) {
this.handleBinaryFrame(event.data);
}
else {
this.handleTextFrame(event.data);
}
};
/**
* Handle an error from the websocket.
* @param event - The error event.
*/
NetworkTablesSocket.prototype.onError = function (event) {
// Log the error to the console
console.error('WebSocket error:', event);
};
/**
* Handle a binary frame from the websocket.
* @param frame - The frame.
*/
NetworkTablesSocket.prototype.handleBinaryFrame = function (frame) {
var e_1, _a;
try {
// TODO: Use streams
for (var _b = tslib_1.__values((0, msgpack_1.decodeMulti)(frame)), _c = _b.next(); !_c.done; _c = _b.next()) {
var f = _c.value;
var message = schemas_1.msgPackSchema.parse(f);
var messageData = {
topicId: message[0],
serverTime: message[1],
typeInfo: util_1.Util.getNetworkTablesTypeFromTypeNum(message[2]),
value: message[3],
};
// Heartbeat message
if (messageData.topicId === -1) {
this.handleRTT(messageData.serverTime);
}
else {
this.onTopicUpdate(messageData);
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
};
/**
* Handle a text frame from the websocket.
* @param frame - The frame.
*/
NetworkTablesSocket.prototype.handleTextFrame = function (frame) {
var _this = this;
// Parse the message from the server
var messageData = JSON.parse(frame);
var messages = schemas_1.messageSchema.parse(messageData);
messages.forEach(function (message) {
// Check the type of the message and handle it accordingly
switch (message.method) {
case 'announce':
_this.handleAnnounceParams(message.params);
break;
case 'unannounce':
_this.handleUnannounceParams(message.params);
break;
case 'properties':
_this.handlePropertiesParams(message.params);
break;
default:
console.warn('Client does not handle message method:', message.method);
}
});
};
/**
* Handle an announce message from the server.
* @param params - The message params.
*/
NetworkTablesSocket.prototype.handleAnnounceParams = function (params) {
this.onAnnounce(params);
};
/**
* Handle an unannounce message from the server.
* @param params - The message params.
*/
NetworkTablesSocket.prototype.handleUnannounceParams = function (params) {
this.onUnannounce(params);
};
/**
* Handle a properties message from the server.
* @param params - The message params.
*/
NetworkTablesSocket.prototype.handlePropertiesParams = function (params) {
this.onProperties(params);
};
/**
* Send a text frame to the server.
* @param message - The message to send.
*/
NetworkTablesSocket.prototype.sendTextFrame = function (message) {
// Send the message to the server
if (this.isConnected()) {
this._websocket.send(JSON.stringify([message]));
}
else {
this.messageQueue.push(JSON.stringify([message]));
}
};
/**
* Send a binary frame to the server.
* @param message - The message to send.
*/
NetworkTablesSocket.prototype.sendBinaryFrame = function (message) {
var cleanMsg = schemas_1.msgPackSchema.parse(message);
// Send the message to the server
if (this.isConnected()) {
this._websocket.send((0, msgpack_1.encode)(cleanMsg));
}
else {
this.messageQueue.push((0, msgpack_1.encode)(cleanMsg));
}
};
/**
* Function to send queued messages whenever the WebSocket connection is opened
*/
NetworkTablesSocket.prototype.sendQueuedMessages = function () {
if (this.isConnected()) {
while (this.messageQueue.length > 0) {
var message = this.messageQueue.shift();
if (message) {
this._websocket.send(message);
}
}
}
};
/**
* Send a message to a topic.
* @param id - The topic ID.
* @param value - The value to send.
* @param typeInfo - The type info for the value.
* @returns The time the message was sent.
*/
NetworkTablesSocket.prototype.sendValueToTopic = function (id, value, typeInfo) {
var time = Math.ceil(this.getServerTime());
var message = util_1.Util.createBinaryMessage(id, time, value, typeInfo);
this.sendBinaryFrame(message);
return time;
};
/**
* Send a heartbeat message to the server.
*/
NetworkTablesSocket.prototype.heartbeat = function () {
var time = util_1.Util.getMicros();
this.sendValueToTopic(-1, time, types_1.NetworkTablesTypeInfos.kDouble);
this.lastHeartbeatDate = time;
};
/**
* Handle a round trip time message from the server.
*
* This is used to calculate the offset between the client and server time
* in order to estimate the current server time for binary messages.
* @param serverTime - The server time.
*/
NetworkTablesSocket.prototype.handleRTT = function (serverTime) {
var rtt = this.calcTimeDelta(this.lastHeartbeatDate);
if (rtt < this.bestRtt || this.bestRtt === -1) {
this.bestRtt = rtt;
this.offset = util_1.Util.getMicros() - serverTime;
}
};
/**
* Get the current server time.
* @returns The current server time.
*/
NetworkTablesSocket.prototype.getServerTime = function () {
return util_1.Util.getMicros() - this.offset + this.bestRtt / 2;
};
/**
* Calculate the time delta between the current time and a given time.
* @param sentDate - The time to calculate the delta from.
* @returns The time delta.
*/
NetworkTablesSocket.prototype.calcTimeDelta = function (sentDate) {
return util_1.Util.getMicros() - sentDate;
};
/**
* Close the websocket connection.
*/
NetworkTablesSocket.prototype.close = function () {
this._websocket.close();
};
NetworkTablesSocket.instances = new Map();
NetworkTablesSocket.PROTOCOL_V4_0 = 'networktables.first.wpi.edu';
NetworkTablesSocket.PROTOCOL_V4_1 = 'v4.1.networktables.first.wpi.edu';
NetworkTablesSocket.RECONNECT_TIMEOUT = 1000;
NetworkTablesSocket.RTT_PERIOD_V4_0 = 1000;
NetworkTablesSocket.TIMEOUT_V4_1 = 1000;
return NetworkTablesSocket;
}());
exports.NetworkTablesSocket = NetworkTablesSocket;
exports.default = NetworkTablesSocket;
//# sourceMappingURL=socket.js.map