rosbag
Version:
`rosbag` is a node.js & browser compatible module for reading [rosbag](http://wiki.ros.org/rosbag) binary data files.
324 lines (260 loc) • 9.48 kB
text/typescript
// Copyright (c) 2018-present, Cruise LLC
// This source code is licensed under the Apache License, Version 2.0,
// found in the LICENSE file in the root directory of this source tree.
// You may not use this file except in compliance with the License.
import { extractTime } from "./fields";
import type { RosMsgDefinition } from "./types";
import { parseMessageDefinition } from "./parseMessageDefinition";
type TypedArrayConstructor =
| Int8ArrayConstructor
| Uint8ArrayConstructor
| Int16ArrayConstructor
| Uint16ArrayConstructor
| Int32ArrayConstructor
| Uint32ArrayConstructor
| Uint8ClampedArrayConstructor
| Float32ArrayConstructor
| Float64ArrayConstructor;
/**
* This has hard-coded buffer reading functions for each
* of the standard message types http://docs.ros.org/api/std_msgs/html/index-msg.html
* eventually custom types decompose into these standard types
*/
class StandardTypeReader {
buffer: Buffer;
offset: number;
view: DataView;
_decoder?: TextDecoder;
_decoderStatus: "NOT_INITIALIZED" | "INITIALIZED" | "NOT_AVAILABLE" = "NOT_INITIALIZED";
constructor(buffer: Buffer) {
this.buffer = buffer;
this.offset = 0;
this.view = new DataView(buffer.buffer, buffer.byteOffset);
}
_intializeTextDecoder() {
if (typeof global.TextDecoder === "undefined") {
this._decoderStatus = "NOT_AVAILABLE";
return;
}
try {
this._decoder = new global.TextDecoder("ascii");
this._decoderStatus = "INITIALIZED";
} catch (e) {
// Swallow the error if we don't support ascii encoding.
this._decoderStatus = "NOT_AVAILABLE";
}
}
json(): unknown {
const resultString = this.string();
try {
return JSON.parse(resultString);
} catch {
return `Could not parse ${resultString}`;
}
}
string() {
const len = this.int32();
const codePoints = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + this.offset, len);
this.offset += len;
// if the string is relatively short we can use apply, but longer strings can benefit from the speed of TextDecoder.
if (codePoints.length < 1000) {
// @ts-expect-error Type 'Uint8Array' is missing the following properties from type 'number[]': pop, push, concat, shift, and 5 more.
return String.fromCharCode.apply(null, codePoints);
}
// Use TextDecoder if it is available and supports the "ascii" encoding.
if (this._decoderStatus === "NOT_INITIALIZED") {
this._intializeTextDecoder();
}
if (this._decoder) {
// TextDecoder does not support Uint8Arrays that are backed by SharedArrayBuffer, so copy the array here.
// SharedArrayBuffer support has been added to the spec, but most browsers have not implemented this change.
// See spec change: https://github.com/whatwg/encoding/pull/182
// Track browser support here: https://github.com/whatwg/encoding/pull/182#issuecomment-539932294
const input = codePoints.buffer instanceof global.SharedArrayBuffer ? new Uint8Array(codePoints) : codePoints;
return this._decoder.decode(input);
}
// Otherwise, use string concatentation.
let data = "";
for (let i = 0; i < len; i++) {
data += String.fromCharCode(codePoints[i]);
}
return data;
}
bool() {
return this.uint8() !== 0;
}
int8() {
return this.view.getInt8(this.offset++);
}
uint8() {
return this.view.getUint8(this.offset++);
}
typedArray(len: number | null, ArrayType: TypedArrayConstructor) {
const arrayLength = len == null ? this.uint32() : len;
const data = new ArrayType(this.view.buffer, this.offset + this.view.byteOffset, arrayLength);
this.offset += arrayLength;
return data;
}
int16() {
const result = this.view.getInt16(this.offset, true);
this.offset += 2;
return result;
}
uint16() {
const result = this.view.getUint16(this.offset, true);
this.offset += 2;
return result;
}
int32() {
const result = this.view.getInt32(this.offset, true);
this.offset += 4;
return result;
}
uint32() {
const result = this.view.getUint32(this.offset, true);
this.offset += 4;
return result;
}
float32() {
const result = this.view.getFloat32(this.offset, true);
this.offset += 4;
return result;
}
float64() {
const result = this.view.getFloat64(this.offset, true);
this.offset += 8;
return result;
}
int64() {
const { offset } = this;
this.offset += 8;
return this.view.getBigInt64(offset, true);
}
uint64() {
const { offset } = this;
this.offset += 8;
return this.view.getBigUint64(offset, true);
}
time() {
const { offset } = this;
this.offset += 8;
return extractTime(this.buffer, offset);
}
duration() {
const { offset } = this;
this.offset += 8;
return extractTime(this.buffer, offset);
}
}
const findTypeByName = (types: RosMsgDefinition[], name: string): RosMsgDefinition => {
const matches = types.filter((type) => type.name === name);
if (matches.length !== 1) {
throw new Error(`Expected 1 top level type definition for '${name}' but found ${matches.length}.`);
}
return matches[0];
};
const friendlyName = (name: string) => name.replace(/\//g, "_");
const createParser = (types: RosMsgDefinition[], typeName: string, freeze: boolean) => {
const topLevelTypes = types.filter((type) => type.name === typeName);
if (topLevelTypes.length !== 1) {
throw new Error("multiple top-level types");
}
const [topLevelType] = topLevelTypes;
const nestedTypes: RosMsgDefinition[] = types.filter((type) => type.name !== typeName);
const constructorBody = (type: RosMsgDefinition) => {
const readerLines: string[] = [];
type.definitions.forEach((def) => {
if (def.isConstant) {
return;
}
if (def.isArray) {
if (def.type === "uint8" || def.type === "int8") {
const arrayType = def.type === "uint8" ? "Uint8Array" : "Int8Array";
readerLines.push(`this.${def.name} = reader.typedArray(${String(def.arrayLength)}, ${arrayType});`);
return;
}
const lenField = `length_${def.name}`;
// set a variable pointing to the parsed fixed array length
// or read the byte indicating the dynamic length
readerLines.push(`var ${lenField} = ${def.arrayLength ? def.arrayLength : "reader.uint32();"}`);
// only allocate an array if there is a length - skips empty allocations
const arrayName = `this.${def.name}`;
// allocate the new array to a fixed length since we know it ahead of time
readerLines.push(`${arrayName} = new Array(${lenField})`);
// start the for-loop
readerLines.push(`for (var i = 0; i < ${lenField}; i++) {`);
// if the sub type is complex we need to allocate it and parse its values
if (def.isComplex) {
const defType = findTypeByName(types, def.type);
// recursively call the constructor for the sub-type
readerLines.push(` ${arrayName}[i] = new Record.${friendlyName(defType.name)}(reader);`);
} else {
// if the subtype is not complex its a simple low-level reader operation
readerLines.push(` ${arrayName}[i] = reader.${def.type}();`);
}
readerLines.push("}"); // close the for-loop
} else if (def.isComplex) {
const defType = findTypeByName(types, def.type);
readerLines.push(`this.${def.name} = new Record.${friendlyName(defType.name)}(reader);`);
} else {
readerLines.push(`this.${def.name} = reader.${def.type}();`);
}
});
if (freeze) {
readerLines.push("Object.freeze(this);");
}
return readerLines.join("\n ");
};
let js = `
var Record = function (reader) {
${constructorBody(topLevelType)}
};\n`;
nestedTypes.forEach((t) => {
js += `
Record.${friendlyName(t.name)} = function(reader) {
${constructorBody(t)}
};\n`;
});
js += `
return function read(reader) {
return new Record(reader);
};`;
let _read: (reader: StandardTypeReader) => unknown;
try {
// eslint-disable-next-line no-eval
_read = eval(`(function buildReader() { ${js} })()`);
} catch (e) {
console.error("error building parser:", js); // eslint-disable-line no-console
throw e;
}
return function parser(buffer: Buffer) {
const reader = new StandardTypeReader(buffer);
return _read(reader);
};
};
export class MessageReader {
reader: (buffer: Buffer) => unknown;
// takes an object message definition and returns
// a message reader which can be used to read messages based
// on the message definition
constructor(
definitions: RosMsgDefinition[],
typeName: string,
options: {
freeze?: boolean;
} = {}
) {
let parsedDefinitions = definitions;
if (typeof parsedDefinitions === "string") {
// eslint-disable-next-line no-console
console.warn(
"Passing string message defintions to MessageReader is deprecated. Instead call `parseMessageDefinition` on it and pass in the resulting parsed message definition object."
);
parsedDefinitions = parseMessageDefinition(parsedDefinitions, typeName);
}
this.reader = createParser(parsedDefinitions, typeName, !!options.freeze);
}
readMessage(buffer: Buffer) {
return this.reader(buffer);
}
}