UNPKG

@foxglove/ws-protocol-examples

Version:

Foxglove WebSocket protocol examples

229 lines 9.83 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const wasm_lz4_1 = tslib_1.__importDefault(require("@foxglove/wasm-lz4")); 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 ws_1 = require("ws"); const boxen_1 = tslib_1.__importDefault(require("../boxen")); const setupSigintHandler_1 = require("./util/setupSigintHandler"); const log = (0, debug_1.default)("foxglove:mcap-play"); debug_1.default.enable("foxglove:*"); // eslint-disable-next-line @typescript-eslint/promise-function-async function delay(durationMs) { return new Promise((resolve) => setTimeout(resolve, durationMs)); } let cachedDecompressHandlers; async function getDecompressHandlers() { if (cachedDecompressHandlers) { return cachedDecompressHandlers; } await wasm_lz4_1.default.isLoaded; await Zstd.isLoaded; cachedDecompressHandlers = { lz4: (buffer, decompressedSize) => (0, wasm_lz4_1.default)(buffer, Number(decompressedSize)), zstd: (buffer, decompressedSize) => Zstd.decompress(buffer, Number(decompressedSize)), }; return cachedDecompressHandlers; } function readableFromFileHandle(handle) { let buffer = new ArrayBuffer(4096); return { async size() { return BigInt((await handle.stat()).size); }, async read(offset, length) { if (offset > Number.MAX_SAFE_INTEGER || length > Number.MAX_SAFE_INTEGER) { throw new Error(`Read too large: offset ${offset}, length ${length}`); } if (length > buffer.byteLength) { buffer = new ArrayBuffer(Number(length * 2n)); } const result = await handle.read({ buffer: new DataView(buffer, 0, Number(length)), position: Number(offset), }); if (result.bytesRead !== Number(length)) { throw new Error(`Read only ${result.bytesRead} bytes from offset ${offset}, expected ${length}`); } return new Uint8Array(result.buffer.buffer, result.buffer.byteOffset, result.bytesRead); }, }; } async function* readMcapFile(filePath) { const decompressHandlers = await getDecompressHandlers(); const handle = await promises_1.default.open(filePath, "r"); try { const reader = await core_1.McapIndexedReader.Initialize({ readable: readableFromFileHandle(handle), decompressHandlers, }); for (const schema of reader.schemasById.values()) { yield schema; } for (const channel of reader.channelsById.values()) { yield channel; } for await (const message of reader.readMessages()) { yield message; } } catch (err) { throw new Error(`Unable to read file as indexed: ${err.toString()}`); } finally { await handle.close(); } } async function main(file, options) { const server = new ws_protocol_1.FoxgloveServer({ name: file }); const port = 8765; const ws = new ws_1.WebSocketServer({ port, handleProtocols: (protocols) => server.handleProtocols(protocols), }); const signal = (0, setupSigintHandler_1.setupSigintHandler)(log, ws); const schemasById = new Map(); const mcapChannelsByWsChannel = new Map(); const wsChannelsByMcapChannel = new Map(); const subscribedChannels = new Set(); const skippedChannelIds = new Set(); let running = false; const runLoop = async () => { let firstIteration = true; outer: do { log("starting playback"); let startTime; let firstMessageTime; for await (const record of readMcapFile(file)) { if (!running || signal.aborted) { break outer; } // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (record.type) { case "Schema": if (!firstIteration) { break; } schemasById.set(record.id, record); break; case "Channel": { if (!firstIteration) { break; } const schema = schemasById.get(record.schemaId); if (!schema) { log("Channel %d has unknown schema %d", record.id, record.schemaId); break; } let schemaData; switch (schema.encoding) { case "ros1msg": case "ros2msg": case "ros2idl": case "omgidl": case "jsonschema": schemaData = new TextDecoder().decode(schema.data); break; case "protobuf": case "flatbuffer": schemaData = Buffer.from(schema.data).toString("base64"); break; default: log("Unknown schema encoding %s for channel %d, skipping", schema.encoding, record.id); skippedChannelIds.add(record.id); continue; } const wsChannelId = server.addChannel({ topic: record.topic, schemaName: schema.name, encoding: record.messageEncoding, schemaEncoding: schema.encoding, schema: schemaData, }); mcapChannelsByWsChannel.set(wsChannelId, record.id); wsChannelsByMcapChannel.set(record.id, wsChannelId); break; } case "Message": { const wsChannelId = wsChannelsByMcapChannel.get(record.channelId); if (wsChannelId == undefined) { if (!skippedChannelIds.has(record.channelId)) { log("Message on unknown channel %d", record.channelId); } break; } if (firstMessageTime == undefined || startTime == undefined) { startTime = performance.now(); firstMessageTime = record.logTime; } // Time from the first message to this message const elapsedMessageTimeMs = Number(record.logTime - firstMessageTime) / 1_000_000; // Wall time from start until now const elapsedWallTimeMs = performance.now() - startTime; const timeToWaitMs = (elapsedMessageTimeMs - elapsedWallTimeMs) / options.rate; if (timeToWaitMs > 0) { await delay(timeToWaitMs); } if (subscribedChannels.has(wsChannelId)) { server.sendMessage(wsChannelId, record.logTime, record.data); } break; } default: log("Unexpected record type %s", record.type); break; } } firstIteration = false; } while (options.loop); log("done!"); process.exit(0); }; ws.on("listening", () => { void (0, boxen_1.default)(`📡 Server listening on localhost:${port}. To see data, visit:\n` + `https://app.foxglove.dev/~/view?ds=foxglove-websocket&ds.url=ws://localhost:${port}/`, { borderStyle: "round", padding: 1 }) .then(log) .then(() => { log("Waiting for client connection..."); }); }); ws.on("connection", (conn, req) => { const name = `${req.socket.remoteAddress}:${req.socket.remotePort}`; log("connection from %s via %s", name, req.url); server.handleConnection(conn, name); if (!running) { running = true; void runLoop(); } conn.on("close", () => { log("client %s disconnected"); }); }); server.on("subscribe", (chanId) => { subscribedChannels.add(chanId); }); server.on("unsubscribe", (chanId) => { subscribedChannels.delete(chanId); }); server.on("error", (err) => { log("server error: %o", err); }); } exports.default = new commander_1.Command("mcap-play") .description("play an MCAP file over a WebSocket client in real time") .argument("<file>", "path to MCAP file") .option("--loop", "automatically restart playback from the beginning", false) .option("--rate <rate>", "playback rate as a multiple of realtime", (val) => { const result = parseFloat(val); if (result <= 0 || !isFinite(result)) { throw new Error(`Invalid rate: ${val}`); } return result; }, 1) .action(main); //# sourceMappingURL=mcap-play.js.map