UNPKG

@foxglove/rosmsg-serialization

Version:

ROS1 (Robot Operating System) message serialization, for reading and writing bags and network messages

289 lines 10.7 kB
// 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. import { stringLengthUtf8 } from "./stringLengthUtf8"; // write a Time object to a DataView. function writeTime(time, view, offset) { view.setUint32(offset, time.sec, true); view.setUint32(offset + 4, time.nsec, true); } class StandardTypeOffsetCalculator { constructor() { this.offset = 0; } // Returns the current offset and increments the next offset by `byteCount`. _incrementAndReturn(byteCount) { const offset = this.offset; this.offset += byteCount; return offset; } // These are not actually used in the StandardTypeWriter, so they must be kept in sync with those implementations. json(value) { return this.string(JSON.stringify(value)); } // The following are used in the StandardTypeWriter. string(value) { // uint32 length if (typeof value !== "string") { throw new Error(`Expected string but got ${typeof value}`); } const length = 4 + stringLengthUtf8(value); return this._incrementAndReturn(length); } bool() { return this.uint8(); } int8() { return this._incrementAndReturn(1); } uint8() { return this._incrementAndReturn(1); } int16() { return this._incrementAndReturn(2); } uint16() { return this._incrementAndReturn(2); } int32() { return this._incrementAndReturn(4); } uint32() { return this._incrementAndReturn(4); } float32() { return this._incrementAndReturn(4); } float64() { return this._incrementAndReturn(8); } int64() { return this._incrementAndReturn(8); } uint64() { return this._incrementAndReturn(8); } time() { return this._incrementAndReturn(8); } duration() { return this._incrementAndReturn(8); } } // this has hard-coded data writing 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 StandardTypeWriter { constructor(data) { this.data = data; this.view = new DataView(data.buffer, data.byteOffset, data.byteLength); this.offsetCalculator = new StandardTypeOffsetCalculator(); } json(value) { this.string(JSON.stringify(value)); } string(value) { if (this.textEncoder == undefined) { this.textEncoder = new TextEncoder(); } const stringOffset = this.offsetCalculator.string(value); const stringLength = this.offsetCalculator.offset - stringOffset - 4; this.view.setUint32(stringOffset, stringLength, true); const { read, written } = this.textEncoder.encodeInto(value, this.data.subarray(stringOffset + 4)); if (read !== value.length) { throw new Error(`Not enough space to encode string into subarray (wrote ${read} of ${value.length} code units into ${written} of ${this.data.subarray(stringOffset + 4).length} bytes)`); } } // eslint-disable-next-line @foxglove/no-boolean-parameters bool(value) { this.uint8(value ? 1 : 0); } int8(value) { this.view.setInt8(this.offsetCalculator.int8(), value); } uint8(value) { this.view.setUint8(this.offsetCalculator.uint8(), value); } int16(value) { this.view.setInt16(this.offsetCalculator.int16(), value, true); } uint16(value) { this.view.setUint16(this.offsetCalculator.uint16(), value, true); } int32(value) { this.view.setInt32(this.offsetCalculator.int32(), value, true); } uint32(value) { this.view.setUint32(this.offsetCalculator.uint32(), value, true); } float32(value) { this.view.setFloat32(this.offsetCalculator.float32(), value, true); } float64(value) { this.view.setFloat64(this.offsetCalculator.float64(), value, true); } int64(value) { this.view.setBigInt64(this.offsetCalculator.int64(), BigInt(value), true); } uint64(value) { this.view.setBigUint64(this.offsetCalculator.uint64(), BigInt(value), true); } time(time) { writeTime(time, this.view, this.offsetCalculator.time()); } duration(time) { writeTime(time, this.view, this.offsetCalculator.time()); } } 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.length === 0) { return typeName.length === 0; } // 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 createWriterAndSizeCalculator(types) { if (types.length === 0) { throw new Error(`no types given`); } const unnamedTypes = types.filter((type) => type.name == undefined); if (unnamedTypes.length > 1) { throw new Error("multiple unnamed types"); } const unnamedType = unnamedTypes.length > 0 ? unnamedTypes[0] : types[0]; const namedTypes = types.filter((type) => type.name != undefined); const constructorBody = (type, argName) => { const lines = []; type.definitions.forEach((def) => { if (def.isConstant ?? false) { return; } // Accesses the field we are currently writing. Pulled out for easy reuse. const accessMessageField = `message["${def.name}"]`; if (def.isArray ?? false) { const lenField = `length_${def.name}`; // set a variable pointing to the parsed fixed array length // or write the byte indicating the dynamic length if (def.arrayLength != undefined) { lines.push(`var ${lenField} = ${def.arrayLength};`); } else { lines.push(`var ${lenField} = ${accessMessageField}.length;`); lines.push(`${argName}.uint32(${lenField});`); } // start the for-loop lines.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 ?? false) { const defType = findTypeByName(types, def.type); // recursively call the function for the sub-type lines.push(` ${friendlyName(defType.name)}(${argName}, ${accessMessageField}[i]);`); } else { // if the subtype is not complex its a simple low-level operation lines.push(` ${argName}.${def.type}(${accessMessageField}[i]);`); } lines.push("}"); // close the for-loop } else if (def.isComplex ?? false) { const defType = findTypeByName(types, def.type); lines.push(`${friendlyName(defType.name)}(${argName}, ${accessMessageField});`); } else { // Call primitives directly. lines.push(`${argName}.${def.type}(${accessMessageField});`); } }); return lines.join("\n "); }; let writerJs = ""; let calculateSizeJs = ""; namedTypes.forEach((t) => { writerJs += ` function ${friendlyName(t.name)}(writer, message) { ${constructorBody(t, "writer")} };\n`; calculateSizeJs += ` function ${friendlyName(t.name)}(offsetCalculator, message) { ${constructorBody(t, "offsetCalculator")} };\n`; }); writerJs += ` return function write(writer, message) { ${constructorBody(unnamedType, "writer")} return writer.data; };`; calculateSizeJs += ` return function calculateSize(offsetCalculator, message) { ${constructorBody(unnamedType, "offsetCalculator")} return offsetCalculator.offset; };`; let write; let calculateSize; try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,no-eval write = eval(`(function buildWriter() { ${writerJs} })()`); } catch (e) { console.error("error building writer:", writerJs); throw e; } try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,no-eval calculateSize = eval(`(function buildSizeCalculator() { ${calculateSizeJs} })()`); } catch (e) { console.error("error building size calculator:", calculateSizeJs); throw e; } return { writer(message, data) { const writer = new StandardTypeWriter(data); return write(writer, message); }, byteSizeCalculator(message) { const offsetCalculator = new StandardTypeOffsetCalculator(); return calculateSize(offsetCalculator, message); }, }; } export class MessageWriter { // takes an object string message definition and returns // a message writer which can be used to write messages based // on the message definition constructor(definitions) { const { writer, byteSizeCalculator } = createWriterAndSizeCalculator(definitions); this.writer = writer; this.byteSizeCalculator = byteSizeCalculator; } // Calculates the byte size needed to write this message in bytes. calculateByteSize(message) { return this.byteSizeCalculator(message); } // output is optional - if it is not provided, a Uint8Array will be generated. writeMessage(message, output) { return this.writer(message, output ?? new Uint8Array(this.calculateByteSize(message))); } } //# sourceMappingURL=MessageWriter.js.map