@foxglove/rosmsg-serialization
Version:
ROS 1 message serialization, for reading and writing bags and network messages
271 lines • 10.5 kB
JavaScript
"use strict";
// This file incorporates work covered by the following copyright and
// permission notice:
//
// Copyright 2018-2021 Cruise LLC
//
// This source code is licensed under the Apache License, Version 2.0,
// found at http://www.apache.org/licenses/LICENSE-2.0
// You may not use this file except in compliance with the License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageReader = exports.createParsers = exports.StandardTypeReader = void 0;
const tslib_1 = require("tslib");
const decodeString_1 = tslib_1.__importDefault(require("./decodeString"));
// 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;
offset;
view;
constructor(buffer) {
this.buffer = buffer;
this.offset = 0;
this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
}
json() {
const resultString = this.string();
try {
return JSON.parse(resultString);
}
catch {
return `Could not parse ${resultString}`;
}
}
string() {
const len = this.uint32();
const totalOffset = this.view.byteOffset + this.offset;
const maxLen = this.view.byteLength - this.offset;
if (len < 0 || len > maxLen) {
throw new RangeError(`String deserialization error: length ${len}, maxLength ${maxLen}`);
}
const data = new Uint8Array(this.view.buffer, totalOffset, len);
this.offset += len;
return (0, decodeString_1.default)(data);
}
bool() {
return this.uint8() !== 0;
}
int8() {
return this.view.getInt8(this.offset++);
}
uint8() {
return this.view.getUint8(this.offset++);
}
typedArray(len, TypedArrayConstructor) {
const arrayLength = len ?? this.uint32();
const view = this.view;
const totalOffset = this.offset + view.byteOffset;
this.offset += arrayLength * TypedArrayConstructor.BYTES_PER_ELEMENT;
// new TypedArray(...) will throw if you try to make a typed array on unaligned boundary
// but for aligned access we can use a typed array and avoid any extra memory alloc/copy
if (totalOffset % TypedArrayConstructor.BYTES_PER_ELEMENT === 0) {
return new TypedArrayConstructor(view.buffer, totalOffset, arrayLength);
}
// copy the data to align it
// using _set_ is slightly faster than slice on the array buffer according to benchmarks when written
const size = TypedArrayConstructor.BYTES_PER_ELEMENT * arrayLength;
const copy = new Uint8Array(size);
copy.set(new Uint8Array(view.buffer, totalOffset, size));
return new TypedArrayConstructor(copy.buffer, copy.byteOffset, arrayLength);
}
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.offset;
this.offset += 8;
return this.view.getBigInt64(offset, true);
}
uint64() {
const offset = this.offset;
this.offset += 8;
return this.view.getBigUint64(offset, true);
}
time() {
const offset = this.offset;
this.offset += 8;
const sec = this.view.getUint32(offset, true);
const nsec = this.view.getUint32(offset + 4, true);
return { sec, nsec };
}
duration() {
const offset = this.offset;
this.offset += 8;
const sec = this.view.getInt32(offset, true);
const nsec = this.view.getInt32(offset + 4, true);
return { sec, nsec };
}
}
exports.StandardTypeReader = StandardTypeReader;
const findTypeByName = (types, name = "") => {
let foundName = ""; // track name separately in a non-null variable to appease Flow
const matches = types.filter((type) => {
const typeName = type.name ?? "";
// if the search is empty, return unnamed types
if (!name) {
return !typeName;
}
// return if the search is in the type name
// or matches exactly if a fully-qualified name match is passed to us
const nameEnd = name.includes("/") ? name : `/${name}`;
if (typeName.endsWith(nameEnd)) {
foundName = typeName;
return true;
}
return false;
});
if (matches.length !== 1) {
throw new Error(`Expected 1 top level type definition for '${name}' but found ${matches.length}.`);
}
return { ...matches[0], name: foundName };
};
const friendlyName = (name) => name.replace(/\//g, "_");
function toTypedArrayType(rosType) {
switch (rosType) {
case "int8":
return "Int8Array";
case "uint8":
return "Uint8Array";
case "int16":
return "Int16Array";
case "uint16":
return "Uint16Array";
case "int32":
return "Int32Array";
case "uint32":
return "Uint32Array";
case "int64":
return "BigInt64Array";
case "uint64":
return "BigUint64Array";
case "float32":
return "Float32Array";
case "float64":
return "Float64Array";
default:
return undefined;
}
}
const createParsers = ({ definitions, options = {}, topLevelReaderKey, }) => {
if (definitions.length === 0) {
throw new Error(`no types given`);
}
const unnamedTypes = definitions.filter((type) => !type.name);
if (unnamedTypes.length > 1) {
throw new Error("multiple unnamed types");
}
const unnamedType = unnamedTypes.length > 0 ? unnamedTypes[0] : definitions[0];
// keep only definitions with a name
const namedTypes = definitions.filter((type) => !!type.name);
const constructorBody = (type) => {
const readerLines = [];
type.definitions.forEach((def) => {
if (def.isConstant === true) {
return;
}
if (def.isArray === true) {
// detect if typed array
const typedArrayType = toTypedArrayType(def.type);
if (typedArrayType != undefined) {
readerLines.push(`this.${def.name} = reader.typedArray(${String(def.arrayLength)}, ${typedArrayType});`);
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 ?? "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 === true) {
const defType = findTypeByName(definitions, 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 === true) {
const defType = findTypeByName(definitions, def.type);
readerLines.push(`this.${def.name} = new Record.${friendlyName(defType.name)}(reader);`);
}
else {
readerLines.push(`this.${def.name} = reader.${def.type}();`);
}
});
if (options.freeze === true) {
readerLines.push("Object.freeze(this);");
}
return readerLines.join("\n ");
};
let js = `
const builtReaders = new Map();
var Record = function (reader) {
${constructorBody(unnamedType)}
};
builtReaders.set(topLevelReaderKey, Record);
`;
for (const type of namedTypes) {
js += `
Record.${friendlyName(type.name)} = function(reader) {
${constructorBody(type)}
};
builtReaders.set(${JSON.stringify(type.name)}, Record.${friendlyName(type.name)});
`;
}
js += `return builtReaders;`;
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func, @typescript-eslint/no-unsafe-call
return new Function("topLevelReaderKey", js)(topLevelReaderKey);
};
exports.createParsers = createParsers;
class MessageReader {
reader;
// takes an object message definition and returns
// a message reader which can be used to read messages based
// on the message definition
constructor(definitions, options = {}) {
this.reader = (0, exports.createParsers)({ definitions, options, topLevelReaderKey: "<toplevel>" }).get("<toplevel>");
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
readMessage(buffer) {
const standardReaders = new StandardTypeReader(buffer);
return new this.reader(standardReaders);
}
}
exports.MessageReader = MessageReader;
//# sourceMappingURL=MessageReader.js.map