Note: This is a community-maintained project and is not affiliated with or endorsed by the Zenoh or LCM teams.
A Zenoh plugin and standalone bridge for LCM (Lightweight Communications and Marshalling) UDP multicast traffic.
LCM is a lightweight publish/subscribe message-passing system widely used in robotics (MIT, UMich, WHOI). It uses UDP multicast for low-latency, decentralized communication — but is limited to the local network.
This plugin transparently bridges LCM multicast traffic into and out of the Zenoh network, allowing existing LCM applications to benefit from Zenoh's WAN routing, wireless optimization, and security — without any code changes to the LCM applications.
How it works:
- An LCM publication on channel
SENSOR_IMUis routed as a Zenoh publication on key expressionlcm/SENSOR_IMU - A Zenoh publication on key expression
lcm/MOTOR_CMDis routed as an LCM multicast message on channelMOTOR_CMD - Payloads are relayed opaquely — the bridge does not decode LCM message contents, only parses the UDP headers for the channel name and fragmentation metadata
Some examples of use cases:
- Bridging LCM traffic between robots across the internet (WAN)
- Connecting LCM-based robots to cloud services via Zenoh
- Multi-robot coordination where each robot runs LCM locally
- Recording LCM traffic via Zenoh storage backends
- Accessing LCM data via the Zenoh REST API
The plugin is available either as a dynamic library to be loaded by the Zenoh router (zenohd), or as a standalone executable (zenoh-bridge-lcm) that acts as a Zenoh peer or client.
LCM App A LCM App B
| ^
| UDP multicast UDP multicast |
v |
┌─────────────┐ Zenoh Network ┌─────────────┐
│ zenoh-bridge │ ◄──────────────────────► │ zenoh-bridge │
│ -lcm │ (TCP/UDP/QUIC) │ -lcm │
└─────────────┘ └─────────────┘
Site A (LAN) Site B (LAN)
Loop prevention is handled via a dual mechanism:
- Zenoh Locality filtering: publications from LCM use
allowed_destination(Locality::Remote), and the Zenoh subscriber usesallowed_origin(Locality::Remote), preventing the bridge from consuming its own messages within the same Zenoh session. - LCM sequence number tracking: sequence numbers of messages sent to LCM multicast are recorded so they can be recognized and skipped when received back via multicast loopback.
zenoh-plugin-lcm/
├── lcm-transport/ # Pure Rust LCM UDP multicast transport (no Zenoh dependency)
│ ├── src/
│ │ ├── protocol.rs # LCM wire protocol (LC02/LC03) encode/decode
│ │ ├── fragment.rs # Fragment reassembly with timeout and size bounds
│ │ ├── multicast.rs # Async UDP multicast transport (tokio)
│ │ └── types.rs # LcmMessage, LcmUrl types
│ └── tests/
│ └── multicast.rs # Integration tests over real multicast loopback
├── zenoh-plugin-lcm/ # Zenoh plugin (cdylib + rlib)
│ └── src/
│ ├── lib.rs # Plugin lifecycle, admin space
│ ├── config.rs # Configuration with allow/deny regex
│ ├── lcm_to_zenoh.rs # LCM multicast → Zenoh publisher
│ ├── zenoh_to_lcm.rs # Zenoh subscriber → LCM multicast
│ └── mapping.rs # Channel ↔ key expression mapping
├── zenoh-bridge-lcm/ # Standalone bridge binary
│ └── src/main.rs # CLI with clap, RuntimeBuilder
├── DEFAULT_CONFIG.json5 # Commented default configuration
├── Dockerfile # Multi-stage build (builder, tester, runtime)
└── docker-compose.test.yml # One-command test execution
⚠️ WARNING: Zenoh and its ecosystem are under active development. When building from the "main" branch, the plugin may not be compatible with released Zenoh binaries. Always build the plugin and zenoh router from the same branch.
-
Install Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
Clone and build:
git clone https://github.com/christomaszewski/zenoh-plugin-lcm.git cd zenoh-plugin-lcm cargo build --release -
The build produces:
- Standalone bridge:
target/release/zenoh-bridge-lcm - Plugin library:
target/release/libzenoh_plugin_lcm.so(Linux),.dylib(macOS),.dll(Windows)
- Standalone bridge:
The simplest way to run the bridge:
./target/release/zenoh-bridge-lcmThis starts the bridge with default settings:
- LCM multicast group:
239.255.76.67:7667(TTL=0, localhost only) - Zenoh key prefix:
lcm - Mode: Zenoh peer
- All LCM channels bridged
Bridge LCM to a remote Zenoh router:
zenoh-bridge-lcm -m client -e tcp/192.168.1.10:7447 --lcm-url "udpm://239.255.76.67:7667?ttl=1"Bridge only specific channels:
zenoh-bridge-lcm --allow "SENSOR_.*|ROBOT_STATE" --deny "DEBUG_.*"Use a custom Zenoh key prefix:
zenoh-bridge-lcm --key-prefix "robot1/lcm"This maps LCM channel SENSOR_IMU to Zenoh key robot1/lcm/SENSOR_IMU.
Bind to a specific network interface:
zenoh-bridge-lcm --network-interface 192.168.1.100Use a configuration file:
zenoh-bridge-lcm -c my_config.json5See DEFAULT_CONFIG.json5 for a fully commented example. CLI arguments override settings from the config file.
Enable the REST API:
zenoh-bridge-lcm --rest-http-port 8000Then query the admin space:
curl -s http://localhost:8000/@/*/lcm/** | jqAdd the plugin configuration to your zenoh router config file:
{
plugins: {
lcm: {
lcm_url: "udpm://239.255.76.67:7667?ttl=1",
key_prefix: "lcm",
allow: "SENSOR_.*",
}
}
}The router will automatically load libzenoh_plugin_lcm.so (or .dylib/.dll) if it is in the library search path.
Build the runtime image:
docker build --target runtime -t zenoh-bridge-lcm .Run with host networking (required for LCM multicast to reach host LCM applications):
docker run --rm --network host zenoh-bridge-lcm \
--lcm-url "udpm://239.255.76.67:7667?ttl=1"zenoh-bridge-lcm can be configured via a JSON5 file passed via the -c argument. See the commented example: DEFAULT_CONFIG.json5.
The "lcm" part of this configuration file can also be used in the configuration file for the Zenoh router (within its "plugins" part).
zenoh-bridge-lcm also accepts the following command-line arguments. If set, each argument will override the corresponding setting from the configuration file:
-c, --config <FILE>: A JSON5 configuration file.-m, --mode <MODE>: The Zenoh session mode. Default:peer. Possible values:peerorclient. See Zenoh documentation for more details.-l, --listen <ENDPOINT>: A locator on which this bridge will listen for incoming sessions. Repeat this option to open several listeners. Example:tcp/0.0.0.0:7447.-e, --connect <ENDPOINT>: A peer locator this bridge will try to connect to. Repeat this option to connect to several peers. Example:tcp/192.168.1.10:7447.--no-multicast-scouting: Disable the Zenoh scouting protocol that allows automatic discovery of Zenoh peers and routers.-i, --id <HEX_STRING>: The identifier (as a hex string) that the bridge must use. WARNING: this identifier must be unique in the system! If not set, a random UUIDv4 will be used.--rest-http-port [PORT | IP:PORT]: Configures the HTTP interface for the REST API (disabled by default).
--lcm-url <URL>: The LCM multicast URL. Default:"udpm://239.255.76.67:7667?ttl=0".- Format:
udpm://GROUP:PORT?ttl=N&recv_buf_size=M ttl=0: packets stay on localhost (for local testing)ttl=1: packets stay on the local network (typical LAN usage)recv_buf_size: kernel UDP receive buffer size in bytes
- Format:
-p, --key-prefix <STRING>: Prefix for all Zenoh key expressions. Default:"lcm". LCM channelFOObecomes{prefix}/FOOin Zenoh.-a, --allow <REGEX>: A regular expression matching LCM channel names to route via Zenoh. Default: all channels allowed.--deny <REGEX>: A regular expression matching LCM channel names to not route. Default: no channels denied. If both--allowand--denyare set, a channel is allowed only if it matches--allowand does not match--deny.--network-interface <IP>: Bind to a specific network interface by IP address for LCM multicast. Useful on multi-homed machines.--max-message-size <BYTES>: Maximum reassembled LCM message size in bytes. Default:4194304(4 MB).
These settings only apply when running as a Zenoh router plugin (not the standalone bridge):
work_thread_num: Number of worker threads in the plugin's async runtime. Default:2.max_block_thread_num: Number of blocking threads in the plugin's async runtime. Default:50.
The bridge implements the full LCM UDP multicast wire protocol:
| Message Type | Magic | Header Size | Description |
|---|---|---|---|
| Short (LC02) | 0x4c433032 |
8 bytes | Single-datagram messages: magic(4) + seqno(4) + channel\0 + data |
| Fragment (LC03) | 0x4c433033 |
20 bytes | Fragmented messages: magic(4) + seqno(4) + msg_size(4) + offset(4) + frag_no(2) + n_frags(2) |
Fragment reassembly features:
- Keyed by
(sender_address, sequence_number)to safely handle multiple publishers - Timeout-based expiry for incomplete fragment sets
- Maximum message size guard against memory exhaustion
- Duplicate fragment detection
cargo test --workspaceThis runs 92 tests:
- 49 unit tests (
lcm-transport): wire protocol encoding/decoding, fragment reassembly with timeout and error paths, malformed packet handling, URL parsing edge cases - 7 integration tests (
lcm-transport): real UDP multicast send/receive over loopback, including fragmented messages and multi-transport scenarios - 33 unit tests (
zenoh-plugin-lcm): configuration parsing, channel/key-expression mapping, allow/deny filtering, reserved character rejection, sequence number pruning - 3 integration tests (
zenoh-plugin-lcm): end-to-end bridge tests with a real Zenoh session — LCM→Zenoh, Zenoh→LCM, and loop prevention
The integration tests use multicast with TTL=0 (localhost only) and unique ports per test, so they work on any system with a loopback interface and require no special privileges.
docker compose -f docker-compose.test.yml up --build --exit-code-from testThis builds the project and runs the full test suite inside a container.
cargo test -p lcm-transport --test multicastcargo test -p lcm-transport --lib
cargo test -p zenoh-plugin-lcm --libThe bridge exposes status information via the Zenoh admin space. If the REST plugin is enabled (--rest-http-port), you can query it:
# Get bridge version
curl -s http://localhost:8000/@/*/lcm/version | jq
# Get current configuration
curl -s http://localhost:8000/@/*/lcm/config | jqThis project is dual-licensed under the Eclipse Public License 2.0 and the Apache License 2.0. You may use this software under the terms of either license.