UNPKG

@foxglove/ros1

Version:

Standalone TypeScript implementation of the ROS 1 (Robot Operating System) protocol with a pluggable transport layer

177 lines 7.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TcpClient = void 0; const eventemitter3_1 = __importDefault(require("eventemitter3")); const RosTcpMessageStream_1 = require("./RosTcpMessageStream"); const TcpConnection_1 = require("./TcpConnection"); class TcpClient extends eventemitter3_1.default { constructor({ socket, nodeName, getPublication, log }) { super(); this._connected = true; this._receivedHeader = false; this._stats = { bytesSent: 0, bytesReceived: 0, messagesSent: 0 }; this._updateTransportInfo = async () => { let fd = -1; try { fd = (await this._socket.fd()) ?? -1; const localPort = (await this._socket.localAddress())?.port ?? -1; const addr = await this._socket.remoteAddress(); if (addr == undefined) { throw new Error(`Socket ${fd} on local port ${localPort} could not resolve remoteAddress`); } const { address, port } = addr; const host = address.includes(":") ? `[${address}]` : address; this._address = address; this._port = port; this._transportInfo = `TCPROS connection on port ${localPort} to [${host}:${port} on socket ${fd}]`; } catch (err) { this._transportInfo = `TCPROS not connected [socket ${fd}]`; this._log?.warn?.(`Cannot resolve address for tcp connection: ${err}`); this.emit("error", new Error(`Cannot resolve address for tcp connection: ${err}`)); } }; this._handleClose = () => { this._connected = false; this.emit("close"); }; this._handleError = (err) => { this._log?.warn?.(`tcp client ${this.toString()} error: ${err}`); this.emit("error", err); }; this._handleData = (chunk) => { try { this._transformer.addData(chunk); } catch (unk) { const err = unk instanceof Error ? unk : new Error(unk); this._log?.warn?.(`failed to decode ${chunk.length} byte chunk from tcp client ${this.toString()}: ${err}`); // Close the socket, the stream is now corrupt void this._socket.close(); this.emit("error", err); } }; this._handleMessage = async (msgData) => { // Check if we have already received the connection header from this client if (this._receivedHeader) { this._log?.warn?.(`tcp client ${this.toString()} sent ${msgData.length} bytes after header`); this._stats.bytesReceived += msgData.byteLength; return; } const header = TcpConnection_1.TcpConnection.ParseHeader(msgData); const topic = header.get("topic"); const destinationCallerId = header.get("callerid"); const dataType = header.get("type"); const md5sum = header.get("md5sum") ?? "*"; const tcpNoDelay = header.get("tcp_nodelay") === "1"; this._receivedHeader = true; void this._socket.setNoDelay(tcpNoDelay); if (topic == undefined || dataType == undefined || destinationCallerId == undefined) { this._log?.warn?.( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `tcp client ${this.toString()} sent incomplete header. topic="${topic}", type="${dataType}", callerid="${destinationCallerId}"`); this.close(); return; } // Check if we are publishing this topic const pub = this._getPublication(topic); if (pub == undefined) { this._log?.warn?.(`tcp client ${this.toString()} attempted to subscribe to unadvertised topic ${topic}`); this.close(); return; } this._stats.bytesReceived += msgData.byteLength; // Check the dataType matches if (dataType !== "*" && pub.dataType !== dataType) { this._log?.warn?.(`tcp client ${this.toString()} attempted to subscribe to topic ${topic} with type "${dataType}", expected "${pub.dataType}"`); this.close(); return; } // Check the md5sum matches if (md5sum !== "*" && pub.md5sum !== md5sum) { this._log?.warn?.(`tcp client ${this.toString()} attempted to subscribe to topic ${topic} with md5sum "${md5sum}", expected "${pub.md5sum}"`); this.close(); return; } // Write the response header void this._writeHeader(new Map([ ["callerid", this._nodeName], ["latching", pub.latching ? "1" : "0"], ["md5sum", pub.md5sum], ["message_definition", pub.messageDefinitionText], ["topic", pub.name], ["type", pub.dataType], ])); // Immediately send the last published message if latching is enabled const latched = pub.latchedMessage(this.transportType()); if (latched != undefined) { void this.write(latched); } this.emit("subscribe", topic, destinationCallerId); }; this._socket = socket; this._nodeName = nodeName; this._getPublication = getPublication; this._log = log; this._transformer = new RosTcpMessageStream_1.RosTcpMessageStream(); this._transportInfo = `TCPROS establishing connection`; socket.on("close", this._handleClose); socket.on("error", this._handleError); socket.on("data", this._handleData); // eslint-disable-next-line @typescript-eslint/no-misused-promises this._transformer.on("message", this._handleMessage); void this._updateTransportInfo(); // Wait for the client to send the initial connection header } transportType() { return "TCPROS"; } connected() { return this._connected; } stats() { return this._stats; } async write(data) { try { await this._socket.write(data); this._stats.messagesSent++; this._stats.bytesSent += data.length; } catch (err) { this._log?.warn?.(`failed to write ${data.length} bytes to ${this.toString()}: ${err}`); } } close() { this._socket .close() .catch((err) => this._log?.warn?.(`error closing client socket ${this.toString()}: ${err}`)); } getTransportInfo() { return this._transportInfo; } toString() { return TcpConnection_1.TcpConnection.Uri(this._address ?? "<unknown>", this._port ?? 0); } async _writeHeader(header) { const data = TcpConnection_1.TcpConnection.SerializeHeader(header); // Write the serialized header payload const buffer = new ArrayBuffer(4 + data.length); const payload = new Uint8Array(buffer); const view = new DataView(buffer); view.setUint32(0, data.length, true); payload.set(data, 4); try { await this._socket.write(payload); this._stats.bytesSent += payload.length; } catch (err) { this._log?.warn?.(`failed to write ${data.length + 4} byte header to ${this.toString()}: ${err}`); } } } exports.TcpClient = TcpClient; //# sourceMappingURL=TcpClient.js.map