The in-tree tutorial in eclipse-uprotocol/up-rust ends Phase 3 in one process (LocalTransport, two listeners). This repo continues that thread: independent processes on Zenoh.
| Official Phase 3 | This repo, Phase 3 |
|---|---|
One cargo run |
Three terminals — both subscribers, then the publisher |
LocalTransport |
UPTransportZenoh (up-transport-zenoh-rust) |
Same APIs: UTransport, UListener, SimplePublisher. Only how you construct the L1 plugin changes.
Start here: how to run Phase 3 and the walk-through in tutorial-text/Tutorial-Phase-3.md. You do not need to re-do Phases 1–2 unless you want the Unix Domain Socket chapters on this pin.
This tutorial is written and verified against crates.io 0.9, not against up-rust main / SNAPSHOT:
| Crate | Pin in this repo | Locked version (see Cargo.lock) |
|---|---|---|
up-rust |
"0.9.0" (all phases) |
0.9.0 |
up-transport-zenoh |
"=0.9.0" (Phase 3 only) |
0.9.0 |
zenoh |
not a direct app dependency | 1.9.0 (pulled by up-transport-zenoh 0.9.0) |
Phase 3 apps pin up-transport-zenoh = "=0.9.0" (exact). That is deliberate: a looser "0.9.0" can resolve to 0.9.1 and pull a newer Zenoh stack (e.g. 1.10.x). We stay on 0.9.0 → Zenoh 1.9.0, recorded in phases/03_zenoh_topology/Cargo.lock.
Normal cargo build / cargo run already honor that lock when it is present. You do not need extra flags for the demo. (Optional CI-style check: cargo build --locked --manifest-path phases/03_zenoh_topology/Cargo.toml fails if the lock would change.)
The official in-tree tutorial may already show newer call sites; this public Zenoh walk stays on the table above until you personally bump it.
New to uProtocol? Begin at the Prologue and Phase 1.
The story begins when I had been exploring the world of Eclipse-SDV out of curiosity. This was an area hitherto completely unknown to me. Yet, I was drawn towards it. Why? I have captured the reasons in this blog post.
One of the technologies that had captured my interest was uProtocol. I am familiar with the problem it was trying to solve (I have worked in the area of location-agnostic, multi-machine-architecture-friendly, network-carried, multiplex-able middleware for a good part of my career), but the domain was different.
My aim was to understand the landscape well, and Eclipse SDV sites helped; so did the uProtocol repo, blogs (viz., Pete Le Vasseur's Articles | plog), and YouTube videos — but what I didn't find was a classical tutorial; a tutorial which helped a software developer to lay her/his hands on the code to solidify the understanding along with the specifications and examples, and helped create a mental map of what was what.
So, I decided to write one myself. This tutorial follows how I approached learning uProtocol; hopefully, this will be useful for you too.
- We start with raw Unix Domain Sockets and manually frame uProtocol
UMessagebytes. - Then we refactor uProtocol's own L1 (
UTransport/UListener) and L2 (SimplePublisher/CallOptions) abstractions — still on the same Unix Domain Socket wire. - Then, we replace the transport with Zenoh, add a second subscriber (the thermal logger mentioned in Phase 1), and prove that business logic survives the change of transport (from Unix Domain Socket to Zenoh).
- How uProtocol's
UUri,UAttributes,UPayload, andUMessagemap to the wire. - Why stream transports (Unix Domain Sockets) need explicit length-prefix framing.
- How uProtocol's L1 (
UTransport/UListener) separates message moving from message handling. - How uProtocol's L2 (
SimplePublisher/CallOptions) separates publishing intent from envelope construction. - Why Unix Domain Sockets fails for multi-process fan-out even on one host — and why a data-space transport (Zenoh) fills that gap.
- How swapping the L1 transport plugin leaves publisher and subscriber business logic unchanged.
- How
UUrimetadata becomes first-class routing information in Zenoh (vs opaque bytes on a Unix Domain Socket). - How L3 PUBLISH registration lets independent processes subscribe to the same resource URI.
Each tutorial chapter is a self-contained Cargo workspace under phases/ — no git checkout needed to run a given phase. Narrative drafts live in tutorial-text/.
| Phase directory | Chapter | Code |
|---|---|---|
phases/01_raw_sockets/ |
Phase 1 | up-frame-codec, one publisher / one subscriber — Unix Domain Socket transport, length-framed UMessage |
phases/02_uprotocol_semantics/ |
Phase 2 | up-bms-proto, up-unix-domain-socket-transport, refactored one publisher / one subscriber — uProtocol L1/L2 over (still) Unix Domain Socket transport |
phases/03_zenoh_topology/ |
Phase 3 | Use of up-transport-zenoh, one publisher / two subscribers - uProtocol L1/L2/L3 over Zenoh transport |
├── phases/
│ ├── 01_raw_sockets/ # Phase 1 – Unix Domain Socket + length-prefix framing
│ ├── 02_uprotocol_semantics/ # Phase 2 – uP-L1/L2 over the same Unix Domain Socket wire
│ └── 03_zenoh_topology/ # Phase 3 – Zenoh data-space transport, multi-subscriber
├── tutorial-text/
│ ├── Tutorial-Phase-1.md # Phase 1 narrative
│ ├── Tutorial-Phase-2.md # Phase 2 narrative
│ └── Tutorial-Phase-3.md # Phase 3 narrative — Zenoh, fan-out, L3
└── Notes/ # Maintainer notes and feedback
Phase 3 retires up-unix-domain-socket-transport and up-frame-codec (frozen in Phase 2 only) and carries forward up-bms-proto, the publisher/subscriber logic, and the protobuf schema unchanged.
Follow the tutorials for each phase, kept under tutorial-text/:
| File | What it covers |
|---|---|
Tutorial-Phase-1.md |
Phase 1 — Raw Unix Domain Sockets. Build UMessage envelopes by hand, frame them with a 4-byte length prefix, and send over a Unix Domain Socket transport (but raw socket calls). Two processes — a publisher and a subscriber — exchange battery telemetry (SoC, temperature) as raw packed bytes. One sees every layer of the wire with no library hiding the details. |
Tutorial-Phase-2.md |
Phase 2 — uProtocol semantics. A Unix Domain Socket-based transport is wrapped behind uProtocol's L1 (UTransport, UListener) and L2 (SimplePublisher, CallOptions). The raw CAN-frame packing is replaced by a protobuf schema (bms_telemetry.proto). The transport crate handcrafted (up-unix-domain-socket-transport) centralizes framing and dispatch; application code no longer touches sockets or byte headers. |
Tutorial-Phase-3.md |
Phase 3 — Zenoh topology. Unix Domain Sockets retire; Zenoh (up-transport-zenoh) becomes the L1 plugin. Same SimplePublisher and UListener bodies, but a second process (up-thermal-logging-subscriber) receives the same stream independently — the fan-out payoff Phase 1 promised and Phase 2 documented but could not deliver. All on one Linux host (Zenoh peer mode). |
Each phase is an independent Cargo workspace. Run all commands from the repo root.
Start order (all phases): start every subscriber first, then the publisher. The publisher sends a short burst and exits; if no subscriber is listening yet, those messages are missed (especially visible in Phase 3’s multi-subscriber demo). That order is how you run the demo — nothing in dispatch “waits” for a subscriber.
Start the subscriber, then the publisher (two terminals):
cargo build --manifest-path phases/01_raw_sockets/Cargo.toml
# Terminal 1 — subscriber (start this first)
cargo run --manifest-path phases/01_raw_sockets/Cargo.toml -p up-telemetry-subscriber
# Terminal 2 — publisher (sends 5 messages, then exits)
cargo run --manifest-path phases/01_raw_sockets/Cargo.toml -p up-battery-telemetry-publisherThe publisher exits after five messages; the subscriber keeps listening until you stop it with Ctrl+C (raw socket server). Phase 2’s subscriber exits by itself after five messages.
Start the subscriber, then the publisher (two terminals):
cargo build --manifest-path phases/02_uprotocol_semantics/Cargo.toml
# Terminal 1 — subscriber (start this first; exits after 5 messages)
cargo run --manifest-path phases/02_uprotocol_semantics/Cargo.toml -p up-telemetry-subscriber
# Terminal 2 — publisher (sends 5 messages, then exits)
cargo run --manifest-path phases/02_uprotocol_semantics/Cargo.toml -p up-battery-telemetry-publisherOptional — run the Phase 2 transport crate tests:
cargo test --manifest-path phases/02_uprotocol_semantics/Cargo.toml -p up-unix-domain-socket-transportStart both subscribers before the publisher (three terminals). The apps use Zenoh peer mode with UDP multicast scouting — no zenohd required on a typical single Linux host. If the publisher runs first, its five messages can finish before either subscriber has registered interest. Peer mode vs optional router: tutorial-text/Tutorial-Phase-3.md (Chapter 6).
cargo build --manifest-path phases/03_zenoh_topology/Cargo.toml# Terminal 1 — battery telemetry subscriber (start first)
cargo run --manifest-path phases/03_zenoh_topology/Cargo.toml -p up-telemetry-subscriber
# Terminal 2 — thermal logging subscriber (start second; new in Phase 3)
cargo run --manifest-path phases/03_zenoh_topology/Cargo.toml -p up-thermal-logging-subscriber
# Terminal 3 — publisher last (sends 5 messages, then exits)
cargo run --manifest-path phases/03_zenoh_topology/Cargo.toml -p up-battery-telemetry-publisherExpected: both subscribers receive all five messages independently — no shared socket path, no broker in application code. The publisher and battery subscriber on_receive bodies are identical to Phase 2; only transport construction changed. A Zenoh ERROR … session closed line on subscriber exit is normal shutdown noise — see Tutorial-Phase-3.md Chapter 6.
If subscribers stay silent (multicast blocked on your network/VM), start a local router in another terminal (zenohd — install) and re-run the three processes.
- Rust toolchain (edition 2024, in my set-up)
- Linux (Unix Domain Sockets for Phases 1–2; Phase 3 demo also runs on Linux)
- All phases:
up-rust0.9.0 (crates.io) - Phase 3:
up-transport-zenohexactly 0.9.0, which pulls Zenoh 1.9.0 via the committedCargo.lock(see Version pins). No separatezenohdfor the usual one-host peer-mode run; install a router only if peer discovery fails. - No prior uProtocol knowledge assumed
I indeed have taken some help from Cursor and Ralph for writing draft code, but the concept behind this tutorial, and the choice of the problem and solutions as well the final documentation/code-structure/code are entirely mine.
This project is licensed under the Apache License, Version 2.0.
The entire tutorial text, notes, and sample Rust code in phases/ are covered by that license
unless noted otherwise.