UNPKG

@foxglove/ws-protocol-examples

Version:

Foxglove WebSocket protocol examples

219 lines 9.93 kB
"use strict"; 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