@foxglove/ws-protocol-examples
Version:
Foxglove WebSocket protocol examples
219 lines • 9.93 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const Zstd = tslib_1.__importStar(require("@foxglove/wasm-zstd"));
const ws_protocol_1 = require("@foxglove/ws-protocol");
const core_1 = require("@mcap/core");
const commander_1 = require("commander");
const debug_1 = tslib_1.__importDefault(require("debug"));
const promises_1 = tslib_1.__importDefault(require("fs/promises"));
const path_1 = tslib_1.__importDefault(require("path"));
const promise_queue_1 = tslib_1.__importDefault(require("promise-queue"));
const ws_1 = require("ws");
const log = (0, debug_1.default)("foxglove:mcap-record");
debug_1.default.enable("foxglove:*");
// Mcap IWritable interface for nodejs FileHandle
class FileHandleWritable {
#handle;
#totalBytesWritten = 0;
constructor(handle) {
this.#handle = handle;
}
async write(buffer) {
const written = await this.#handle.write(buffer);
this.#totalBytesWritten += written.bytesWritten;
}
position() {
return BigInt(this.#totalBytesWritten);
}
}
// eslint-disable-next-line @typescript-eslint/promise-function-async
function delay(durationMs) {
return new Promise((resolve) => setTimeout(resolve, durationMs));
}
async function waitForServer(address, signal) {
log("connecting to %s", address);
while (!signal.aborted) {
const maybeClient = await new Promise((resolve) => {
const ws = new ws_1.WebSocket(address, [ws_protocol_1.FoxgloveClient.SUPPORTED_SUBPROTOCOL]);
const client = new ws_protocol_1.FoxgloveClient({ ws });
const onClose = (event) => {
log("connection failed, code=%s reason=%s wasClean=%s", event.code, event.reason, event.wasClean);
resolve(undefined);
};
client.on("close", onClose);
client.on("open", () => {
client.off("close", onClose);
signal.addEventListener("abort", () => {
client.close();
});
resolve(client);
});
});
if (maybeClient) {
return maybeClient;
}
log("trying again in 5 seconds...");
await delay(5000);
}
return undefined;
}
async function main(address, options) {
await Zstd.isLoaded;
await promises_1.default.mkdir(path_1.default.dirname(options.output), { recursive: true });
const fileHandle = await promises_1.default.open(options.output, "w");
const fileHandleWritable = new FileHandleWritable(fileHandle);
const textEncoder = new TextEncoder();
const maxPendingPromises = 1;
const maxQueuedPromises = options.queueSize > 0 ? options.queueSize : Infinity;
/** Used to ensure all operations on the McapWriter are sequential */
const writeMsgQueue = new promise_queue_1.default(maxPendingPromises, maxQueuedPromises);
const writer = new core_1.McapWriter({
writable: fileHandleWritable,
chunkSize: options.chunkSize,
compressChunk: options.compression
? (data) => ({
compression: "zstd",
compressedData: Zstd.compress(data, options.compressionLevel),
})
: undefined,
});
await writer.start({
profile: "",
library: "mcap-record",
});
const controller = new AbortController();
process.on("SIGINT", () => {
log("shutting down...");
controller.abort();
});
try {
const client = await waitForServer(address, controller.signal);
if (!client) {
return;
}
await new Promise((resolve) => {
const wsChannelsByMcapChannel = new Map();
const subscriptionsById = new Map();
const activeChannelIds = new Set();
client.on("serverInfo", (event) => {
log(event);
});
client.on("status", (event) => {
log(event);
});
client.on("error", (err) => {
log("server error: %o", err);
});
client.on("advertise", (newChannels) => {
void Promise.all(newChannels.map(async (channel) => {
if (activeChannelIds.has(channel.id)) {
log("skipping channel %d on topic %s as a channel with the same id has been advertised before.", channel.id, channel.topic);
return;
}
activeChannelIds.add(channel.id);
let schemaEncoding = channel.schemaEncoding;
if (schemaEncoding == undefined) {
schemaEncoding = {
json: "jsonschema",
protobuf: "protobuf",
flatbuffer: "flatbuffer",
omgidl: "omgidl",
ros1: "ros1msg",
ros2: "ros2msg",
}[channel.encoding];
if (schemaEncoding == undefined) {
log("unable to infer schema encoding from message encoding %s on topic %s, messages will be recorded without schema", channel.encoding, channel.topic);
}
}
let schemaData;
switch (schemaEncoding) {
case "jsonschema":
case "omgidl":
case "ros1msg":
case "ros2msg":
schemaData = textEncoder.encode(channel.schema);
break;
case "protobuf":
case "flatbuffer":
schemaData = Buffer.from(channel.schema, "base64");
break;
default:
log("unknown schema encoding %s, messages will be recorded without schema", schemaEncoding);
break;
case undefined:
break;
}
let schemaId = 0;
if (schemaData != undefined && schemaEncoding != undefined) {
// workaround to help TS type refinement
const nonnullSchemaData = schemaData;
const nonnullSchemaEncoding = schemaEncoding;
schemaId = await writeMsgQueue.add(async () => await writer.registerSchema({
name: channel.schemaName,
encoding: nonnullSchemaEncoding,
data: nonnullSchemaData,
}));
}
const mcapChannelId = (await writeMsgQueue.add(async () => await writer.registerChannel({
schemaId,
topic: channel.topic,
messageEncoding: channel.encoding,
metadata: new Map(),
})));
wsChannelsByMcapChannel.set(mcapChannelId, channel.id);
log("subscribing to %s (channel %d)", channel.topic, channel.id);
const subscriptionId = client.subscribe(channel.id);
subscriptionsById.set(subscriptionId, { messageCount: 0, mcapChannelId });
}));
});
client.on("unadvertise", (channelIds) => {
for (const channelId of channelIds) {
log("channel %d has been unadvertised", channelId);
activeChannelIds.delete(channelId);
}
});
client.on("message", (event) => {
const subscription = subscriptionsById.get(event.subscriptionId);
if (subscription == undefined) {
log("received message for unknown subscription %s", event.subscriptionId);
return;
}
writeMsgQueue
.add(async () => {
await writer.addMessage({
channelId: subscription.mcapChannelId,
sequence: subscription.messageCount++,
logTime: BigInt(Date.now()) * 1000000n,
publishTime: event.timestamp,
data: new Uint8Array(event.data.buffer, event.data.byteOffset, event.data.byteLength),
});
})
.catch((error) => {
log(error);
});
});
client.on("close", (event) => {
log("server disconnected, code=%s reason=%s wasClean=%s", event.code, event.reason, event.wasClean);
resolve();
});
});
// Wait until all queued messages have been written.
while (writeMsgQueue.getPendingLength() + writeMsgQueue.getQueueLength() > 0) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
finally {
await writer.end();
}
}
exports.default = new commander_1.Command("mcap-record")
.description("connect to a WebSocket server and record an MCAP file")
.argument("<address>", "WebSocket address, e.g. ws://localhost:8765")
.option("-o, --output <file>", "path to write MCAP file")
.option("-n, --no-compression", "do not compress chunks")
.option("--chunk-size <value>", "chunk size in bytes", parseInt, 5 * 1024 * 1024)
.option("--compression-level <value>", "Zstandard compression level", parseInt, 3)
.option("-q, --queue-size <value>", "Size of incoming message queue. Choose 0 for unlimited queue length (default)", parseInt, 0)
.action(main);
//# sourceMappingURL=mcap-record.js.map