@foxglove/ros1
Version:
Standalone TypeScript implementation of the ROS 1 (Robot Operating System) protocol with a pluggable transport layer
177 lines • 7.9 kB
JavaScript
"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