ros2-cdr
v0.1.0
Published
CDR encode/decode against ros2msg definition text, for the rosbridge Foxglove-protocol server
Maintainers
Readme
ros2-cdr
CDR encode/decode against a structured ROS 2 type definition for Node.js — the client-side counterpart to the rosbridge server.
That server never decodes a ROS message: it relays opaque CDR byte payloads,
leaving encode/decode to the client. ros2-cdr is that client piece: give it the
structured type definition the server emits and the bytes, and it hands back
structured values — and builds the bytes back for client-publish, service calls,
and action goals.
The codec walks a pre-parsed field tree (a TypeDefinition) rather than
re-parsing .msg text on the client: the server already parsed the IDL with
rosidl, so it advertises the structure directly. Each channel/service/action
is advertised with schemaEncoding: "ros2typedef" and its schema field set to
the TypeDefinition object (from parse_schema.read_*_type_definition) — so a
client reads channel.schema and hands it straight to MessageCodec. (This
replaces the ros2msg text schema; the bridge no longer targets Foxglove
Studio.) A text adapter is still available if you have ros2msg text from
elsewhere — see From ros2msg text.
Zero runtime dependencies. No ROS install, no code generation. Written in TypeScript; ships compiled JS + type declarations.
Install
npm install ros2-cdrUsage
import { MessageCodec, type TypeDefinition } from "ros2-cdr";
// `typeDefinition` is what the bridge emits for a channel (see below).
const codec = new MessageCodec(typeDefinition);
const value = codec.decode(cdrBytes); // -> nested object
const payload = codec.encode(value); // -> Uint8Array of CDR bytesBuild the MessageCodec once per type and reuse it. Module-level
decode(typeDefinition, data) / encode(typeDefinition, value) helpers exist
for one-offs.
The type definition
A TypeDefinition is a root type name plus a flat map of every (transitively
referenced) message type to its ordered fields — exactly the JSON
parse_schema.read_*_type_definition produces:
{
"rootType": "geometry_msgs/PointStamped",
"types": {
"geometry_msgs/PointStamped": [
{ "name": "header", "type": "std_msgs/Header", "isArray": false, "arrayLength": null },
{ "name": "point", "type": "geometry_msgs/Point", "isArray": false, "arrayLength": null }
],
"geometry_msgs/Point": [
{ "name": "x", "type": "float64", "isArray": false, "arrayLength": null },
{ "name": "y", "type": "float64", "isArray": false, "arrayLength": null },
{ "name": "z", "type": "float64", "isArray": false, "arrayLength": null }
]
// ... std_msgs/Header, builtin_interfaces/Time, ...
}
}Each field's type is a normalized primitive name (float64, uint8,
string, …) or a pkg/Type reference; arrayLength is a number for a
fixed-size array or null for a dynamic/bounded sequence. builtin_interfaces/Time
and Duration always resolve even if omitted from types.
From ros2msg text
If all you have is the Foxglove-style concatenated ros2msg schema string
(schemaEncoding: "ros2msg"), build a codec from it directly:
const codec = MessageCodec.fromRos2msgText(definitionText, "geometry_msgs/PoseStamped");This parses the text into a TypeDefinition and is otherwise identical.
parseRos2msgText(text, rootType) is also exported if you want the structure.
Value mapping
| ROS 2 type | JavaScript value |
| -------------------------------- | ------------------------------------------------- |
| bool | boolean |
| 8/16/32-bit integers | number |
| int64 / uint64 | bigint |
| float32 / float64 | number |
| string / wstring | string |
| T[N] (fixed) / T[] (dynamic) | Array |
| uint8[] / byte[] | Uint8Array (encode also accepts number[]) |
| nested message | object |
Encoding requires every field to be present (no defaults are filled in); decoding returns every field.
What it implements
- Classic CDR v1 encapsulation (the ROS 2 wire format): 4-byte header, little- or big-endian, with alignment measured from the byte after the header.
- All ROS primitive types,
string/wstring, fixed and dynamic/bounded arrays, and arbitrarily nested messages. - A structured
TypeDefinitionas the codec's native input, with aros2msgdefinition-text adapter (MSG: pkg/Typeblocks,Header/pkg/msg/Typeshorthands, same-package bare references; constants and default values skipped).builtin_interfaces/TimeandDurationalways resolve.
Layout
src/cdr.ts CdrReader / CdrWriter — CDR primitives, strings, alignment.
src/definition.ts TypeDefinition/FieldDefinition types + parseRos2msgText adapter.
src/codec.ts MessageCodec — walk a TypeDefinition over the CDR primitives.
test/ node:test suite: alignment, hand-computed byte vectors, round-trips.Produced on the server by srv/parse_schema.py's read_msg_type_definition /
read_srv_type_definition / read_action_type_definition.
Develop
npm install
npm test # runs the TypeScript sources directly via Node's type stripping
npm run build # emits dist/ (JS + .d.ts) via tscRequires Node.js >= 20 (the test runner uses native TypeScript type stripping;
Node >= 22.6 for that, or run against the compiled dist/).
Limitations
- Targets CDR v1 (the classic FastRTPS/CycloneDDS ROS 2 wire format), not XCDR2.
wstringis treated as UTF-16 code units (matching Foxglove Studio) and is the least exercised path.- Comment/constant stripping is line-based, so a
#or=inside a string default value could be misparsed — defaults don't affect the wire format, so this only matters if you inspect the parsed schema.
License
Apache-2.0. See LICENSE.
