From 4862b30dc69e6ec1c3298b6d3139d97b062177e9 Mon Sep 17 00:00:00 2001 From: Nirmalya Sengupta Date: Tue, 18 Aug 2026 22:40:45 +0530 Subject: [PATCH 1/2] Add uProtocol Rust tutorial Signed-off-by: Nirmalya Sengupta --- tutorial/README.md | 146 + tutorial/phases/01_raw_sockets/Cargo.lock | 829 +++ tutorial/phases/01_raw_sockets/Cargo.toml | 7 + .../up-battery-telemetry-publisher/Cargo.toml | 12 + .../src/main.rs | 58 + .../crates/up-frame-codec/Cargo.toml | 10 + .../crates/up-frame-codec/src/lib.rs | 61 + .../crates/up-telemetry-subscriber/Cargo.toml | 12 + .../up-telemetry-subscriber/src/main.rs | 75 + .../phases/02_uprotocol_semantics/Cargo.lock | 1175 +++++ .../phases/02_uprotocol_semantics/Cargo.toml | 9 + .../up-battery-telemetry-publisher/Cargo.toml | 16 + .../src/main.rs | 71 + .../crates/up-bms-proto/Cargo.toml | 12 + .../crates/up-bms-proto/build.rs | 13 + .../up-bms-proto/proto/bms_telemetry.proto | 13 + .../crates/up-bms-proto/src/lib.rs | 22 + .../crates/up-frame-codec/Cargo.toml | 10 + .../crates/up-frame-codec/src/lib.rs | 61 + .../crates/up-telemetry-subscriber/Cargo.toml | 16 + .../up-telemetry-subscriber/src/main.rs | 87 + .../Cargo.toml | 16 + .../src/lib.rs | 307 ++ tutorial/phases/03_zenoh_topology/Cargo.lock | 4482 +++++++++++++++++ tutorial/phases/03_zenoh_topology/Cargo.toml | 8 + .../up-battery-telemetry-publisher/Cargo.toml | 16 + .../src/main.rs | 75 + .../crates/up-bms-proto/Cargo.toml | 12 + .../crates/up-bms-proto/build.rs | 13 + .../up-bms-proto/proto/bms_telemetry.proto | 13 + .../crates/up-bms-proto/src/lib.rs | 21 + .../crates/up-telemetry-subscriber/Cargo.toml | 16 + .../up-telemetry-subscriber/src/main.rs | 98 + .../up-thermal-logging-subscriber/Cargo.toml | 16 + .../up-thermal-logging-subscriber/src/main.rs | 107 + tutorial/tutorial-text/Tutorial-Phase-1.md | 477 ++ tutorial/tutorial-text/Tutorial-Phase-2.md | 806 +++ tutorial/tutorial-text/Tutorial-Phase-3.md | 466 ++ 38 files changed, 9664 insertions(+) create mode 100644 tutorial/README.md create mode 100644 tutorial/phases/01_raw_sockets/Cargo.lock create mode 100644 tutorial/phases/01_raw_sockets/Cargo.toml create mode 100644 tutorial/phases/01_raw_sockets/crates/up-battery-telemetry-publisher/Cargo.toml create mode 100644 tutorial/phases/01_raw_sockets/crates/up-battery-telemetry-publisher/src/main.rs create mode 100644 tutorial/phases/01_raw_sockets/crates/up-frame-codec/Cargo.toml create mode 100644 tutorial/phases/01_raw_sockets/crates/up-frame-codec/src/lib.rs create mode 100644 tutorial/phases/01_raw_sockets/crates/up-telemetry-subscriber/Cargo.toml create mode 100644 tutorial/phases/01_raw_sockets/crates/up-telemetry-subscriber/src/main.rs create mode 100644 tutorial/phases/02_uprotocol_semantics/Cargo.lock create mode 100644 tutorial/phases/02_uprotocol_semantics/Cargo.toml create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-battery-telemetry-publisher/Cargo.toml create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-battery-telemetry-publisher/src/main.rs create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/Cargo.toml create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/build.rs create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/proto/bms_telemetry.proto create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/src/lib.rs create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-frame-codec/Cargo.toml create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-frame-codec/src/lib.rs create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-telemetry-subscriber/Cargo.toml create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-telemetry-subscriber/src/main.rs create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/Cargo.toml create mode 100644 tutorial/phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/src/lib.rs create mode 100644 tutorial/phases/03_zenoh_topology/Cargo.lock create mode 100644 tutorial/phases/03_zenoh_topology/Cargo.toml create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/Cargo.toml create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/src/main.rs create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-bms-proto/Cargo.toml create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-bms-proto/build.rs create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-bms-proto/proto/bms_telemetry.proto create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-bms-proto/src/lib.rs create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/Cargo.toml create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/src/main.rs create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/Cargo.toml create mode 100644 tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/src/main.rs create mode 100644 tutorial/tutorial-text/Tutorial-Phase-1.md create mode 100644 tutorial/tutorial-text/Tutorial-Phase-2.md create mode 100644 tutorial/tutorial-text/Tutorial-Phase-3.md diff --git a/tutorial/README.md b/tutorial/README.md new file mode 100644 index 00000000..e656c777 --- /dev/null +++ b/tutorial/README.md @@ -0,0 +1,146 @@ +# uProtocol Tutorial — From Raw Sockets to Zenoh Fan-Out + +## Prologue + +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](https://nsengupta.github.io/blog/why-explore-software-defined-vehicle/). + +One of the technologies that had captured my interest was [uProtocol](https://github.com/eclipse-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](https://petelevasseur.com/articles/index.html)), 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 `UMessage` bytes. +- 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*). + +## What we will learn + +- How uProtocol's `UUri`, `UAttributes`, `UPayload`, and `UMessage` map 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 `UUri` metadata 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. + + +## Repository layout + +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. + +## The tutorial documents + +Follow the tutorials for each phase, kept under [`tutorial-text/`](./tutorial-text/): + +| File | What it covers | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`Tutorial-Phase-1.md`](./tutorial-text/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`](./tutorial-text/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`](./tutorial-text/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). | + +## Quick start — run the demo + +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). + +### Phase 1 — Raw sockets + +Start the subscriber, then the publisher (two terminals): + +```bash +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-publisher +``` + +### Phase 2 — uProtocol semantics + +Start the subscriber, then the publisher (two terminals): + +```bash +cargo build --manifest-path phases/02_uprotocol_semantics/Cargo.toml + +# Terminal 1 — subscriber (start this first) +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-publisher +``` + +Optional — enable `RUST_LOG=trace` on both terminals to see `up-unix-domain-socket-transport` dispatch logs. + +Run the Phase 2 transport crate tests: + +```bash +cargo test --manifest-path phases/02_uprotocol_semantics/Cargo.toml -p up-unix-domain-socket-transport +``` + +### Phase 3 — Zenoh topology (multi-subscriber, single producer) + +**Start both subscribers before the publisher** (three terminals). If the publisher runs first, its five messages can finish before either subscriber has registered interest. Background on Zenoh peer mode and why this demo does not use `zenohd`: [`tutorial-text/Tutorial-Phase-3.md`](./tutorial-text/Tutorial-Phase-3.md) (Chapter 6). + +```bash +cargo build --manifest-path phases/03_zenoh_topology/Cargo.toml +``` + +```bash +# 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-publisher +``` + +Expected: 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. + +### Prerequisites + +- Rust toolchain (edition 2024, in my set-up) +- Linux (Unix Domain Sockets for Phases 1–2; Phase 3 demo also runs on Linux) +- **Phase 3 only:** [Zenoh](https://zenoh.io/) via the `up-transport-zenoh` dependency (pulled by Cargo); no separate `zenohd` install for this demo +- No prior uProtocol knowledge assumed + +### Declaration + +I indeed have taken some help from [Cursor](https://cursor.com) and [Ralph](https://ralphy-server.fly.dev/) 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. + +## License + +This project is licensed under the [Apache License, Version 2.0](./LICENSE.txt). + +The entire tutorial text, notes, and sample Rust code in `phases/` are covered by that license +unless noted otherwise. \ No newline at end of file diff --git a/tutorial/phases/01_raw_sockets/Cargo.lock b/tutorial/phases/01_raw_sockets/Cargo.lock new file mode 100644 index 00000000..62f8ac8f --- /dev/null +++ b/tutorial/phases/01_raw_sockets/Cargo.lock @@ -0,0 +1,829 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mediatype" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f490ea2ae935dd8ac89c472d4df28c7f6b87cc20767e1b21fd5ed6a16e7f61e4" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "up-battery-telemetry-publisher" +version = "0.1.0" +dependencies = [ + "anyhow", + "rand", + "tokio", + "up-frame-codec", + "up-rust", +] + +[[package]] +name = "up-frame-codec" +version = "0.1.0" +dependencies = [ + "anyhow", + "protobuf", + "up-rust", +] + +[[package]] +name = "up-rust" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b015cef2972e0d26960821b9e815cab28d4c447ea49f2da5e72aed0a9fb4a436" +dependencies = [ + "async-trait", + "bytes", + "mediatype", + "protobuf", + "protobuf-codegen", + "protoc-bin-vendored", + "rand", + "regex", + "thiserror", + "tokio", + "tracing", + "uriparse", + "uuid-simd", +] + +[[package]] +name = "up-telemetry-subscriber" +version = "0.1.0" +dependencies = [ + "anyhow", + "protobuf", + "tokio", + "up-frame-codec", + "up-rust", +] + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/tutorial/phases/01_raw_sockets/Cargo.toml b/tutorial/phases/01_raw_sockets/Cargo.toml new file mode 100644 index 00000000..25d40649 --- /dev/null +++ b/tutorial/phases/01_raw_sockets/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" +members = [ + "crates/up-frame-codec", + "crates/up-battery-telemetry-publisher", + "crates/up-telemetry-subscriber", +] diff --git a/tutorial/phases/01_raw_sockets/crates/up-battery-telemetry-publisher/Cargo.toml b/tutorial/phases/01_raw_sockets/crates/up-battery-telemetry-publisher/Cargo.toml new file mode 100644 index 00000000..1e8e7ae0 --- /dev/null +++ b/tutorial/phases/01_raw_sockets/crates/up-battery-telemetry-publisher/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "up-battery-telemetry-publisher" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-frame-codec = { path = "../up-frame-codec" } +up-rust = "0.9.0" +tokio = { version = "1.52.3", features = ["full"] } +anyhow = "1.0" +rand = "0.9" diff --git a/tutorial/phases/01_raw_sockets/crates/up-battery-telemetry-publisher/src/main.rs b/tutorial/phases/01_raw_sockets/crates/up-battery-telemetry-publisher/src/main.rs new file mode 100644 index 00000000..8a5cb75d --- /dev/null +++ b/tutorial/phases/01_raw_sockets/crates/up-battery-telemetry-publisher/src/main.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use tokio::io::AsyncWriteExt; +use tokio::net::UnixStream; +use rand::Rng; +use up_rust::{UMessageBuilder, UPayloadFormat, UUri}; + +use up_frame_codec::serialize_for_unix_socket; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + println!("--- Battery telemetry publisher starting ---"); + + // 1. Build a source UUri for the battery telemetry publisher entity. + // Pattern: authority + numeric entity + version + numeric resource. + let source_uri = UUri::try_from_parts("my_own_car", 0x1010, 1, 0x8001)?; + + // 2. Build and send 5 messages with random battery/temperature values. + let mut rng = rand::rng(); + + for i in 1..=5 { + let battery_pct: f32 = rng.random_range(75.0..78.9); + let temp_c: i8 = rng.random_range(20..=25); + + println!("Message {}: SoC = {:.1}%, Temp = {}°C", i, battery_pct, temp_c); + + let message = UMessageBuilder::publish(source_uri.clone()) + .with_ttl(5000) + .build_with_payload( + pack_bms_can_frame(battery_pct, temp_c).to_vec(), + UPayloadFormat::UPAYLOAD_FORMAT_RAW, + )?; + + let framed = serialize_for_unix_socket(&message)?; + + let socket_path = up_frame_codec::socket_path()?; + let mut stream = UnixStream::connect(&socket_path).await?; + stream.write_all(&framed).await?; + stream.flush().await?; + + println!(" Sent {} bytes.\n", framed.len()); + } + + Ok(()) +} + +fn pack_bms_can_frame(battery_level_pct: f32, temperature_c: i8) -> [u8; 8] { + let mut can_data = [0u8; 8]; + + // Convert using a DBC scale of 0.5 (e.g. 75.0 / 0.5 = 150) + let raw_soc = (battery_level_pct / 0.5) as u8; + + can_data[0] = raw_soc; + can_data[1] = temperature_c as u8; + + can_data +} diff --git a/tutorial/phases/01_raw_sockets/crates/up-frame-codec/Cargo.toml b/tutorial/phases/01_raw_sockets/crates/up-frame-codec/Cargo.toml new file mode 100644 index 00000000..695d5ef5 --- /dev/null +++ b/tutorial/phases/01_raw_sockets/crates/up-frame-codec/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "up-frame-codec" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-rust = "0.9.0" +protobuf = "3.7.2" +anyhow = "1.0" diff --git a/tutorial/phases/01_raw_sockets/crates/up-frame-codec/src/lib.rs b/tutorial/phases/01_raw_sockets/crates/up-frame-codec/src/lib.rs new file mode 100644 index 00000000..2fdc8ebc --- /dev/null +++ b/tutorial/phases/01_raw_sockets/crates/up-frame-codec/src/lib.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use std::path::PathBuf; + +use protobuf::Message; +use up_rust::UMessage; + +/// Socket file name under `{current_working_dir}/tmp/`. +pub const SOCKET_FILE_NAME: &str = "uprotocol_twin.sock"; + +/// Returns `{current_working_dir}/tmp/uprotocol_twin.sock`. +/// +/// Both publisher and subscriber must be started with the same working directory +/// so they share this path. Does not create directories. +pub fn socket_path() -> Result { + let mut path = std::env::current_dir()?; + path.push("tmp"); + path.push(SOCKET_FILE_NAME); + Ok(path) +} + +/// Ensures `{current_working_dir}/tmp` exists, then returns [`socket_path`]. +pub fn ensure_socket_dir() -> Result { + let path = socket_path()?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + Ok(path) +} + +/// Encodes a UMessage into a Unix-socket compatible framed byte buffer. +/// +/// The frame consists of a 4-byte Big-Endian length prefix followed by the +/// protobuf-encoded UMessage payload. +pub fn serialize_for_unix_socket(msg: &UMessage) -> Result, anyhow::Error> { + let envelope_bytes = msg.write_to_bytes()?; + + let msg_len = envelope_bytes.len() as u32; + let mut framed_buffer = msg_len.to_be_bytes().to_vec(); + framed_buffer.append(&mut envelope_bytes.to_vec()); // Length is prefixed + + Ok(framed_buffer) +} + +/// Decodes a framed byte buffer produced by [`serialize_for_unix_socket`]. +/// +/// Expects the same layout: 4-byte Big-Endian length prefix, then protobuf body. +pub fn deserialize_for_unix_socket(framed: &[u8]) -> Result { + if framed.len() < 4 { + anyhow::bail!("framed buffer too short for length prefix"); + } + + let body_len = u32::from_be_bytes(framed[0..4].try_into()?) as usize; + let end = 4 + body_len; + if framed.len() < end { + anyhow::bail!("framed buffer too short for declared body length"); + } + + Ok(UMessage::parse_from_bytes(&framed[4..end])?) +} diff --git a/tutorial/phases/01_raw_sockets/crates/up-telemetry-subscriber/Cargo.toml b/tutorial/phases/01_raw_sockets/crates/up-telemetry-subscriber/Cargo.toml new file mode 100644 index 00000000..bff79273 --- /dev/null +++ b/tutorial/phases/01_raw_sockets/crates/up-telemetry-subscriber/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "up-telemetry-subscriber" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-frame-codec = { path = "../up-frame-codec" } +up-rust = "0.9.0" +tokio = { version = "1.52.3", features = ["full"] } +anyhow = "1.0" +protobuf = "3.7.2" diff --git a/tutorial/phases/01_raw_sockets/crates/up-telemetry-subscriber/src/main.rs b/tutorial/phases/01_raw_sockets/crates/up-telemetry-subscriber/src/main.rs new file mode 100644 index 00000000..b3a4c5d3 --- /dev/null +++ b/tutorial/phases/01_raw_sockets/crates/up-telemetry-subscriber/src/main.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use tokio::io::AsyncReadExt; +use tokio::net::UnixListener; +use std::io::{Write, stdout}; + +use up_frame_codec::deserialize_for_unix_socket; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Bind under `{cwd}/tmp/` (create the directory if needed; clean stale socket). + let socket_path = up_frame_codec::ensure_socket_dir()?; + let _ = std::fs::remove_file(&socket_path); + let listener = UnixListener::bind(&socket_path)?; + println!( + "Battery telemetry subscriber listening on: {}", + socket_path.display() + ); + + loop { + let (mut stream, _) = listener.accept().await?; + + // Spawn a dedicated task for each incoming socket connection. + tokio::spawn(async move { + // Step A: Read length prefix header (4 bytes). + let mut len_bytes = [0u8; 4]; + if stream.read_exact(&mut len_bytes).await.is_err() { + return; + } + let body_len = u32::from_be_bytes(len_bytes) as usize; + + // Step B: Read matching body bytes based on length header. + let mut body_bytes = vec![0u8; body_len]; + if stream.read_exact(&mut body_bytes).await.is_err() { + return; + } + + // Step C: Reassemble the framed payload and decode via the codec. + let mut framed = len_bytes.to_vec(); + framed.extend_from_slice(&body_bytes); + + match deserialize_for_unix_socket(&framed) { + Ok(u_message) => { + if let Some(payload_data) = u_message.payload.as_ref() { + let extracted_bytes: Vec = payload_data.clone().into(); + let (soc, temp) = unpack_bms_can_frame(&extracted_bytes); + + let output = format!( + "[Battery telemetry subscriber] Processing incoming CAN telemetry...\n\ + -> State of Charge: {:.1}%\n\ + -> Cell Temp: {} °C", + soc, temp, + ); + println!("{}", output); + let _ = stdout().flush(); + } + } + Err(e) => eprintln!("Decode error: {:?}", e), + } + }); + } +} + +fn unpack_bms_can_frame(can_data: &[u8]) -> (f32, i8) { + if can_data.len() < 2 { return (0.0, 0); } + + // Unpack according to DBC rules + let raw_soc = can_data[0]; + let battery_level_pct = raw_soc as f32 * 0.5; + + let temperature_c = can_data[1] as i8; + + (battery_level_pct, temperature_c) +} diff --git a/tutorial/phases/02_uprotocol_semantics/Cargo.lock b/tutorial/phases/02_uprotocol_semantics/Cargo.lock new file mode 100644 index 00000000..8c18bd4a --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/Cargo.lock @@ -0,0 +1,1175 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "defmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fragile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jiff" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +dependencies = [ + "defmt", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mediatype" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f490ea2ae935dd8ac89c472d4df28c7f6b87cc20767e1b21fd5ed6a16e7f61e4" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "up-battery-telemetry-publisher" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger", + "log", + "rand", + "tokio", + "up-bms-proto", + "up-frame-codec", + "up-rust", + "up-unix-domain-socket-transport", +] + +[[package]] +name = "up-bms-proto" +version = "0.1.0" +dependencies = [ + "protobuf", + "protobuf-codegen", + "protoc-bin-vendored", +] + +[[package]] +name = "up-frame-codec" +version = "0.1.0" +dependencies = [ + "anyhow", + "protobuf", + "up-rust", +] + +[[package]] +name = "up-rust" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b015cef2972e0d26960821b9e815cab28d4c447ea49f2da5e72aed0a9fb4a436" +dependencies = [ + "async-trait", + "bytes", + "mediatype", + "mockall", + "protobuf", + "protobuf-codegen", + "protoc-bin-vendored", + "rand", + "regex", + "thiserror 1.0.69", + "tokio", + "tracing", + "uriparse", + "uuid-simd", +] + +[[package]] +name = "up-telemetry-subscriber" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "env_logger", + "log", + "tokio", + "up-bms-proto", + "up-frame-codec", + "up-rust", + "up-unix-domain-socket-transport", +] + +[[package]] +name = "up-unix-domain-socket-transport" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "protobuf", + "tokio", + "up-frame-codec", + "up-rust", +] + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/tutorial/phases/02_uprotocol_semantics/Cargo.toml b/tutorial/phases/02_uprotocol_semantics/Cargo.toml new file mode 100644 index 00000000..a3ccc68b --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +resolver = "2" +members = [ + "crates/up-frame-codec", + "crates/up-bms-proto", + "crates/up-unix-domain-socket-transport", + "crates/up-battery-telemetry-publisher", + "crates/up-telemetry-subscriber", +] diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-battery-telemetry-publisher/Cargo.toml b/tutorial/phases/02_uprotocol_semantics/crates/up-battery-telemetry-publisher/Cargo.toml new file mode 100644 index 00000000..8511c27c --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-battery-telemetry-publisher/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "up-battery-telemetry-publisher" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-bms-proto = { path = "../up-bms-proto" } +up-frame-codec = { path = "../up-frame-codec" } +up-unix-domain-socket-transport = { path = "../up-unix-domain-socket-transport" } +up-rust = "0.9.0" +tokio = { version = "1.52.3", features = ["full"] } +anyhow = "1.0" +rand = "0.9" +log = "0.4" +env_logger = "0.11" diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-battery-telemetry-publisher/src/main.rs b/tutorial/phases/02_uprotocol_semantics/crates/up-battery-telemetry-publisher/src/main.rs new file mode 100644 index 00000000..43452122 --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-battery-telemetry-publisher/src/main.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use std::sync::Arc; + +// Phase-1 used ttl: Some(5000) via UMessageBuilder. Here we pass the same TTL +// through CallOptions, which SimplePublisher converts into UAttributes.ttl. +// See: https://docs.rs/up-rust/latest/up_rust/communication/struct.CallOptions.html + +use rand::Rng; +use up_bms_proto::constants::*; +use up_bms_proto::BatteryTelemetry; +use up_rust::communication::{CallOptions, Publisher, SimplePublisher, UPayload}; +use up_rust::{StaticUriProvider, UTransport}; +use up_unix_domain_socket_transport::UnixDomainSocketTransport; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + env_logger::init(); + + println!("--- Battery telemetry publisher starting ---"); + + let uri_provider = Arc::new(StaticUriProvider::new( + AUTHORITY_NAME, + PUBLISHER_UE_ID, + PUBLISHER_UE_VERSION, + )); + let socket_path = up_frame_codec::socket_path()?; + let transport: Arc = + UnixDomainSocketTransport::connect(&socket_path); + log::trace!( + "using UnixDomainSocketTransport::connect → {} (L1 UTransport)", + socket_path.display() + ); + let publisher = SimplePublisher::new(transport, uri_provider); + + let mut rng = rand::rng(); + + for i in 1..=EXPECTED_MESSAGE_COUNT { + let telemetry = BatteryTelemetry { + soc_percent: rng.random_range(75.0..78.9), + temp_celsius: rng.random_range(20..=25), + ..Default::default() + }; + + println!( + "Message {}: SoC = {:.1}%, Temp = {}°C", + i, telemetry.soc_percent, telemetry.temp_celsius + ); + + // L2: UPayload wraps protobuf bytes + format for SimplePublisher. + let payload = UPayload::try_from_protobuf(telemetry)?; + log::trace!( + "SimplePublisher::publish → UTransport::send (resource_id=0x{BATTERY_TELEMETRY_RESOURCE_ID:04x})" + ); + publisher + .publish( + BATTERY_TELEMETRY_RESOURCE_ID, + // `CallOptions::for_publish(ttl, priority, sink)`. + // - ttl (ms): Some(5000) matches Phase 1's explicit TTL of 5 seconds. + CallOptions::for_publish(Some(5000), None, None), + Some(payload), + ) + .await + .map_err(|err| anyhow::anyhow!("publish failed: {err}"))?; + + println!(); + } + + Ok(()) +} diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/Cargo.toml b/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/Cargo.toml new file mode 100644 index 00000000..0443d303 --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "up-bms-proto" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +protobuf = "3.7.2" + +[build-dependencies] +protobuf-codegen = "3.7.2" +protoc-bin-vendored = "3.2.0" diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/build.rs b/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/build.rs new file mode 100644 index 00000000..c37a7812 --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/build.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +fn main() -> Result<(), Box> { + protobuf_codegen::Codegen::new() + .protoc() + .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap()) + .include("proto") + .input("proto/bms_telemetry.proto") + .cargo_out_dir("gen") + .run_from_script(); + Ok(()) +} diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/proto/bms_telemetry.proto b/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/proto/bms_telemetry.proto new file mode 100644 index 00000000..7a55614c --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/proto/bms_telemetry.proto @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +syntax = "proto3"; + +package tutorial.bms.v1; + +// Battery management telemetry published by the demo publisher. +// SoC and cell temperature only. +message BatteryTelemetry { + float soc_percent = 1; + int32 temp_celsius = 2; +} diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/src/lib.rs b/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/src/lib.rs new file mode 100644 index 00000000..b5d39f16 --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-bms-proto/src/lib.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +//! Shared BMS telemetry protobuf types and Phase 2 demo constants. +//! +//! The `.proto` schema lives at `proto/bms_telemetry.proto`; Rust types are generated +//! at build time by `build.rs` (see Phase 2 tutorial). + +pub mod constants { + /// Authority for all Phase 2 demo URIs (aligned with Phase 1). + pub const AUTHORITY_NAME: &str = "my_own_car"; + pub const PUBLISHER_UE_ID: u32 = 0x1010; + pub const PUBLISHER_UE_VERSION: u8 = 0x01; + pub const BATTERY_TELEMETRY_RESOURCE_ID: u16 = 0x8001; + pub const EXPECTED_MESSAGE_COUNT: u32 = 5; +} + +mod generated { + include!(concat!(env!("OUT_DIR"), "/gen/mod.rs")); +} + +pub use generated::bms_telemetry::BatteryTelemetry; diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-frame-codec/Cargo.toml b/tutorial/phases/02_uprotocol_semantics/crates/up-frame-codec/Cargo.toml new file mode 100644 index 00000000..695d5ef5 --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-frame-codec/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "up-frame-codec" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-rust = "0.9.0" +protobuf = "3.7.2" +anyhow = "1.0" diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-frame-codec/src/lib.rs b/tutorial/phases/02_uprotocol_semantics/crates/up-frame-codec/src/lib.rs new file mode 100644 index 00000000..2fdc8ebc --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-frame-codec/src/lib.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use std::path::PathBuf; + +use protobuf::Message; +use up_rust::UMessage; + +/// Socket file name under `{current_working_dir}/tmp/`. +pub const SOCKET_FILE_NAME: &str = "uprotocol_twin.sock"; + +/// Returns `{current_working_dir}/tmp/uprotocol_twin.sock`. +/// +/// Both publisher and subscriber must be started with the same working directory +/// so they share this path. Does not create directories. +pub fn socket_path() -> Result { + let mut path = std::env::current_dir()?; + path.push("tmp"); + path.push(SOCKET_FILE_NAME); + Ok(path) +} + +/// Ensures `{current_working_dir}/tmp` exists, then returns [`socket_path`]. +pub fn ensure_socket_dir() -> Result { + let path = socket_path()?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + Ok(path) +} + +/// Encodes a UMessage into a Unix-socket compatible framed byte buffer. +/// +/// The frame consists of a 4-byte Big-Endian length prefix followed by the +/// protobuf-encoded UMessage payload. +pub fn serialize_for_unix_socket(msg: &UMessage) -> Result, anyhow::Error> { + let envelope_bytes = msg.write_to_bytes()?; + + let msg_len = envelope_bytes.len() as u32; + let mut framed_buffer = msg_len.to_be_bytes().to_vec(); + framed_buffer.append(&mut envelope_bytes.to_vec()); // Length is prefixed + + Ok(framed_buffer) +} + +/// Decodes a framed byte buffer produced by [`serialize_for_unix_socket`]. +/// +/// Expects the same layout: 4-byte Big-Endian length prefix, then protobuf body. +pub fn deserialize_for_unix_socket(framed: &[u8]) -> Result { + if framed.len() < 4 { + anyhow::bail!("framed buffer too short for length prefix"); + } + + let body_len = u32::from_be_bytes(framed[0..4].try_into()?) as usize; + let end = 4 + body_len; + if framed.len() < end { + anyhow::bail!("framed buffer too short for declared body length"); + } + + Ok(UMessage::parse_from_bytes(&framed[4..end])?) +} diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-telemetry-subscriber/Cargo.toml b/tutorial/phases/02_uprotocol_semantics/crates/up-telemetry-subscriber/Cargo.toml new file mode 100644 index 00000000..971eae6d --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-telemetry-subscriber/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "up-telemetry-subscriber" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-bms-proto = { path = "../up-bms-proto" } +up-frame-codec = { path = "../up-frame-codec" } +up-unix-domain-socket-transport = { path = "../up-unix-domain-socket-transport" } +up-rust = "0.9.0" +tokio = { version = "1.52.3", features = ["full"] } +anyhow = "1.0" +async-trait = "0.1" +log = "0.4" +env_logger = "0.11" diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-telemetry-subscriber/src/main.rs b/tutorial/phases/02_uprotocol_semantics/crates/up-telemetry-subscriber/src/main.rs new file mode 100644 index 00000000..7de16392 --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-telemetry-subscriber/src/main.rs @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, +}; + +use async_trait::async_trait; +use tokio::sync::Notify; +use up_bms_proto::constants::*; +use up_bms_proto::BatteryTelemetry; +use up_rust::{LocalUriProvider, StaticUriProvider, UListener, UMessage, UTransport}; +use up_unix_domain_socket_transport::UnixDomainSocketTransport; + +struct BatteryTelemetryListener { + received: Arc, + shutdown: Arc, +} + +#[async_trait] +impl UListener for BatteryTelemetryListener { + async fn on_receive(&self, msg: UMessage) { + match msg.extract_protobuf::() { + Ok(telemetry) => { + let count = self.received.fetch_add(1, Ordering::SeqCst) + 1; + log::trace!( + "UListener::on_receive via UnixDomainSocketTransport dispatch (message {count}/{EXPECTED_MESSAGE_COUNT})" + ); + println!( + "[Battery telemetry subscriber] Processing incoming telemetry...\n\ + -> State of Charge: {:.1}%\n\ + -> Cell Temp: {} °C", + telemetry.soc_percent, telemetry.temp_celsius, + ); + + if count >= EXPECTED_MESSAGE_COUNT { + self.shutdown.notify_one(); + } + } + Err(err) => eprintln!("Failed to decode BatteryTelemetry payload: {err}"), + } + } +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + env_logger::init(); + + let uri_provider = StaticUriProvider::new( + AUTHORITY_NAME, + PUBLISHER_UE_ID, + PUBLISHER_UE_VERSION, + ); + let source_filter = uri_provider.get_resource_uri(BATTERY_TELEMETRY_RESOURCE_ID); + + let received = Arc::new(AtomicU32::new(0)); + let shutdown = Arc::new(Notify::new()); + let listener = Arc::new(BatteryTelemetryListener { + received, + shutdown: shutdown.clone(), + }); + + let socket_path = up_frame_codec::ensure_socket_dir()?; + let transport = UnixDomainSocketTransport::bind(&socket_path).await?; + log::trace!( + "using UnixDomainSocketTransport::bind → {} (L1 UTransport)", + socket_path.display() + ); + transport + .register_listener(&source_filter, None, listener) + .await?; + log::trace!( + "registered UListener with source filter resource_id=0x{BATTERY_TELEMETRY_RESOURCE_ID:04x}" + ); + + println!( + "Battery telemetry subscriber listening on: {} (expecting {} messages)", + socket_path.display(), + EXPECTED_MESSAGE_COUNT + ); + + shutdown.notified().await; + println!("Received {EXPECTED_MESSAGE_COUNT} messages — exiting."); + + Ok(()) +} diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/Cargo.toml b/tutorial/phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/Cargo.toml new file mode 100644 index 00000000..65bbc20e --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "up-unix-domain-socket-transport" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-frame-codec = { path = "../up-frame-codec" } +up-rust = "0.9.0" +tokio = { version = "1.52.3", features = ["net", "io-util", "rt", "macros", "sync"] } +async-trait = "0.1" +protobuf = "3.7.2" +anyhow = "1.0" + +[dev-dependencies] +up-rust = { version = "0.9.0", features = ["test-util"] } diff --git a/tutorial/phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/src/lib.rs b/tutorial/phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/src/lib.rs new file mode 100644 index 00000000..ce2de0ea --- /dev/null +++ b/tutorial/phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/src/lib.rs @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +//! [`UnixDomainSocketTransport`] — a [`UTransport`] over a local Unix Domain Socket. +//! +//! Both processes use the **same** type: +//! - [`UnixDomainSocketTransport::bind`] — owns the socket path, accepts connections, +//! dispatches to [`UListener`]s via [`UTransport::register_listener`]. +//! - [`UnixDomainSocketTransport::connect`] — send-only attachment to that path +//! ([`UTransport::send`]); listener registration is not available in this mode. +//! +//! Bind vs connect is wire setup for Unix Domain Sockets, not a Client/Server split +//! in the uProtocol application model. + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use async_trait::async_trait; +use protobuf::Message; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{UnixListener, UnixStream}; +use tokio::sync::RwLock; + +use up_frame_codec::serialize_for_unix_socket; +use up_rust::{ + verify_filter_criteria, ComparableListener, UCode, UListener, UMessage, UStatus, UTransport, + UUri, +}; + +#[derive(Eq, PartialEq, Hash)] +struct RegisteredListener { + source_filter: UUri, + sink_filter: Option, + listener: ComparableListener, +} + +impl RegisteredListener { + fn matches_msg(&self, msg: &UMessage) -> bool { + let Some(attribs) = msg.attributes.as_ref() else { + return false; + }; + let Some(source) = attribs.source.as_ref() else { + return false; + }; + + if !self.source_filter.matches(source) { + return false; + } + + if let Some(pattern) = &self.sink_filter { + attribs + .sink + .as_ref() + .is_some_and(|candidate| pattern.matches(candidate)) + } else { + attribs.sink.is_none() + } + } + + async fn on_receive(&self, msg: UMessage) { + self.listener.on_receive(msg).await; + } +} + +/// L1 transport agent over a Unix Domain Socket (implements [`UTransport`]). +pub struct UnixDomainSocketTransport { + socket_path: PathBuf, + listeners: Arc>>, + /// `true` after [`Self::bind`] (accept loop running); `false` after [`Self::connect`]. + accepts_connections: bool, +} + +impl UnixDomainSocketTransport { + /// Attach for sending only: connect to `socket_path` on each [`UTransport::send`]. + pub fn connect(socket_path: impl AsRef) -> Arc { + Arc::new(Self { + socket_path: socket_path.as_ref().to_path_buf(), + listeners: Arc::new(RwLock::new(HashSet::new())), + accepts_connections: false, + }) + } + + /// Bind `socket_path`, spawn the accept/dispatch loop, and return a shareable handle. + /// + /// Creates the parent directory when needed (see [`up_frame_codec::ensure_socket_dir`]). + pub async fn bind(socket_path: impl AsRef) -> Result, UStatus> { + let socket_path = socket_path.as_ref().to_path_buf(); + if let Some(parent) = socket_path.parent() { + std::fs::create_dir_all(parent).map_err(|err| { + UStatus::fail_with_code( + UCode::INTERNAL, + format!("failed to create socket directory {}: {err}", parent.display()), + ) + })?; + } + let _ = std::fs::remove_file(&socket_path); + let listener = UnixListener::bind(&socket_path).map_err(|err| { + UStatus::fail_with_code( + UCode::INTERNAL, + format!( + "failed to bind Unix Domain Socket {}: {err}", + socket_path.display() + ), + ) + })?; + + let transport = Arc::new(Self { + socket_path: socket_path.clone(), + listeners: Arc::new(RwLock::new(HashSet::new())), + accepts_connections: true, + }); + + let dispatch_transport = Arc::clone(&transport); + tokio::spawn(async move { + loop { + let Ok((stream, _)) = listener.accept().await else { + continue; + }; + let transport = Arc::clone(&dispatch_transport); + tokio::spawn(async move { + if let Ok(message) = read_framed_message(stream).await { + transport.dispatch(message).await; + } + }); + } + }); + + Ok(transport) + } + + /// Socket path this transport is attached to. + pub fn socket_path(&self) -> &Path { + &self.socket_path + } + + async fn dispatch(&self, message: UMessage) { + let listeners = self.listeners.read().await; + for registered in listeners.iter() { + if registered.matches_msg(&message) { + registered.on_receive(message.clone()).await; + } + } + } + + async fn send_framed(&self, message: UMessage) -> Result<(), UStatus> { + let framed = serialize_for_unix_socket(&message).map_err(|err| { + UStatus::fail_with_code( + UCode::INTERNAL, + format!("failed to frame UMessage: {err}"), + ) + })?; + + let mut stream = UnixStream::connect(&self.socket_path).await.map_err(|err| { + UStatus::fail_with_code( + UCode::UNAVAILABLE, + format!( + "failed to connect to Unix Domain Socket {}: {err}", + self.socket_path.display() + ), + ) + })?; + + stream.write_all(&framed).await.map_err(|err| { + UStatus::fail_with_code(UCode::INTERNAL, format!("failed to write message: {err}")) + })?; + stream.flush().await.map_err(|err| { + UStatus::fail_with_code(UCode::INTERNAL, format!("failed to flush stream: {err}")) + })?; + + Ok(()) + } +} + +#[async_trait] +impl UTransport for UnixDomainSocketTransport { + async fn send(&self, message: UMessage) -> Result<(), UStatus> { + self.send_framed(message).await + } + + async fn register_listener( + &self, + source_filter: &UUri, + sink_filter: Option<&UUri>, + listener: Arc, + ) -> Result<(), UStatus> { + if !self.accepts_connections { + return Err(UStatus::fail_with_code( + UCode::UNIMPLEMENTED, + "register_listener requires UnixDomainSocketTransport::bind on the receiving side", + )); + } + + verify_filter_criteria(source_filter, sink_filter)?; + + let registered = RegisteredListener { + source_filter: source_filter.to_owned(), + sink_filter: sink_filter.map(|u| u.to_owned()), + listener: ComparableListener::new(listener), + }; + + let mut listeners = self.listeners.write().await; + if listeners.contains(®istered) { + return Err(UStatus::fail_with_code( + UCode::ALREADY_EXISTS, + "listener already registered for filters", + )); + } + listeners.insert(registered); + Ok(()) + } + + async fn unregister_listener( + &self, + source_filter: &UUri, + sink_filter: Option<&UUri>, + listener: Arc, + ) -> Result<(), UStatus> { + if !self.accepts_connections { + return Err(UStatus::fail_with_code( + UCode::UNIMPLEMENTED, + "unregister_listener requires UnixDomainSocketTransport::bind on the receiving side", + )); + } + + let registered = RegisteredListener { + source_filter: source_filter.to_owned(), + sink_filter: sink_filter.map(|u| u.to_owned()), + listener: ComparableListener::new(listener), + }; + + let mut listeners = self.listeners.write().await; + if listeners.remove(®istered) { + Ok(()) + } else { + Err(UStatus::fail_with_code( + UCode::NOT_FOUND, + "no such listener registered for filters", + )) + } + } +} + +async fn read_framed_message(mut stream: UnixStream) -> Result { + let mut len_bytes = [0u8; 4]; + stream.read_exact(&mut len_bytes).await.map_err(|err| { + UStatus::fail_with_code( + UCode::INTERNAL, + format!("failed to read length prefix: {err}"), + ) + })?; + let body_len = u32::from_be_bytes(len_bytes) as usize; + + let mut body_bytes = vec![0u8; body_len]; + stream.read_exact(&mut body_bytes).await.map_err(|err| { + UStatus::fail_with_code( + UCode::INTERNAL, + format!("failed to read message body: {err}"), + ) + })?; + + UMessage::parse_from_bytes(&body_bytes).map_err(|err| { + UStatus::fail_with_code(UCode::INTERNAL, format!("failed to decode UMessage: {err}")) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use up_rust::{LocalUriProvider, MockUListener, StaticUriProvider, UMessageBuilder}; + + #[tokio::test] + async fn send_dispatches_to_matching_listener() { + let socket = std::env::temp_dir().join("uprotocol_unix_domain_socket_transport_test.sock"); + let _ = std::fs::remove_file(&socket); + const RESOURCE_ID: u16 = 0x8001; + let uri_provider = StaticUriProvider::new("my_own_car", 0x1010, 0x01); + + let mut mock = MockUListener::new(); + mock.expect_on_receive().times(1).return_const(()); + let listener = Arc::new(mock); + + let server = UnixDomainSocketTransport::bind(&socket).await.unwrap(); + server + .register_listener( + &uri_provider.get_resource_uri(RESOURCE_ID), + None, + listener, + ) + .await + .unwrap(); + + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + let client = UnixDomainSocketTransport::connect(&socket); + client + .send( + UMessageBuilder::publish(uri_provider.get_resource_uri(RESOURCE_ID)) + .build() + .unwrap(), + ) + .await + .unwrap(); + + let _ = std::fs::remove_file(&socket); + } +} diff --git a/tutorial/phases/03_zenoh_topology/Cargo.lock b/tutorial/phases/03_zenoh_topology/Cargo.lock new file mode 100644 index 00000000..9c8d6edd --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/Cargo.lock @@ -0,0 +1,4482 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.4", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +dependencies = [ + "defmt", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keyed-set" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d255a6b6ecd77bb93ce91de984d7039bff7503f500eb4851a1269732f22baf" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4_flex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "mediatype" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f490ea2ae935dd8ac89c472d4df28c7f6b87cc20767e1b21fd5ed6a16e7f61e4" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonempty-collections" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e216d0e8cf9d54fa66e5780f6e1d5dc96d1c1b3c25aeba3b6758548bcbbd8b9d" +dependencies = [ + "serde", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +dependencies = [ + "pest", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "serde", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.4", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "bytes", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.4", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuffer-spsc" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3e7aa0a681b232e7cd7f856a53b10603df88ca74b79a8d8088845185492e35" +dependencies = [ + "array-init", + "crossbeam", +] + +[[package]] +name = "ron" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +dependencies = [ + "bitflags 2.13.0", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "either", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stabby" +version = "72.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b834ec7ced12095fea1e4b07dcb7e8cf2b59b18afa3eac52494d835965a5ec" +dependencies = [ + "rustversion", + "stabby-abi", +] + +[[package]] +name = "stabby-abi" +version = "72.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1a4f477858a5bdf927c9fab7f579899de9b13e39f8b3b3b300c89fbab632f4" +dependencies = [ + "rustc_version", + "rustversion", + "sha2-const-stable", + "stabby-macros", +] + +[[package]] +name = "stabby-macros" +version = "72.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31c4b2434980b67ad83f300a58088ba14d59454dcd79ba3d87419bbd924d31e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls-listener" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1461056cc1ef47003f7ee16e4cef3741068d4c7f6b627bfce49b7c00c120a530" +dependencies = [ + "futures-util", + "pin-project-lite", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "token-cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb48920ae769b58126c8c93269805011c793201f95fde28b479b81a9a531bbde" +dependencies = [ + "paste", + "portable-atomic", + "rustversion", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.6", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uhlc" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62a645e3e4e6c85b7abe49b086aa3204119431f42b6123b0070419fb6e9d24e" +dependencies = [ + "humantime", + "lazy_static", + "log", + "rand 0.8.6", + "serde", + "spin 0.10.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unzip-n" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5bb2756c16fb66f80cfbf5fb0e0c09a7001e739f453c9ec241b9c8b1556fda" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "up-battery-telemetry-publisher" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger", + "log", + "rand 0.9.4", + "tokio", + "up-bms-proto", + "up-rust", + "up-transport-zenoh", + "zenoh", +] + +[[package]] +name = "up-bms-proto" +version = "0.1.0" +dependencies = [ + "protobuf", + "protobuf-codegen", + "protoc-bin-vendored", +] + +[[package]] +name = "up-rust" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b015cef2972e0d26960821b9e815cab28d4c447ea49f2da5e72aed0a9fb4a436" +dependencies = [ + "async-trait", + "bytes", + "mediatype", + "protobuf", + "protobuf-codegen", + "protoc-bin-vendored", + "rand 0.9.4", + "regex", + "thiserror 1.0.69", + "tokio", + "tracing", + "uriparse", + "uuid-simd", +] + +[[package]] +name = "up-telemetry-subscriber" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "env_logger", + "log", + "tokio", + "up-bms-proto", + "up-rust", + "up-transport-zenoh", + "zenoh", +] + +[[package]] +name = "up-thermal-logging-subscriber" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "env_logger", + "log", + "tokio", + "up-bms-proto", + "up-rust", + "up-transport-zenoh", + "zenoh", +] + +[[package]] +name = "up-transport-zenoh" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17b48615830f4a590c436e69b57074cc164bb8d7f6f29b29aa77c632a10c4e" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "protobuf", + "tokio", + "tracing", + "up-rust", + "zenoh", +] + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "validated_struct" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869a93e8a7286e339e1128630051d82babbcd75d585975af07b9f3327220e60e" +dependencies = [ + "json5", + "serde", + "serde_json", + "validated_struct_macros", +] + +[[package]] +name = "validated_struct_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c44ce98e7227a04eeb4cf9c784109a5c9710e54849ceb4f09f8597247897f1e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unzip-n", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zenoh" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e22d7002ac149ef17fe400bb40a267ebbba40a83413bab03da7762256fa94e" +dependencies = [ + "ahash", + "arc-swap", + "async-trait", + "bytes", + "const_format", + "flate2", + "flume", + "futures", + "git-version", + "itertools", + "json5", + "lazy_static", + "nonempty-collections", + "once_cell", + "petgraph", + "phf", + "rand 0.8.6", + "rustc_version", + "serde", + "serde_json", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "uhlc", + "vec_map", + "zenoh-buffers", + "zenoh-codec", + "zenoh-collections", + "zenoh-config", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-link", + "zenoh-link-commons", + "zenoh-macros", + "zenoh-plugin-trait", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-transport", + "zenoh-util", +] + +[[package]] +name = "zenoh-buffers" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89c9e2427102e8efd533716f0935389a3900a818e7334004dd647ac0bd029dc" +dependencies = [ + "zenoh-collections", +] + +[[package]] +name = "zenoh-codec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31930531a8e387160bc3680c6d62f80a201020cf4d8aa36bd46988b425a66306" +dependencies = [ + "tracing", + "uhlc", + "zenoh-buffers", + "zenoh-protocol", +] + +[[package]] +name = "zenoh-collections" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fc5195efe3ad44786275f559bbd6f13c6612470e9706c9a9a8b5b47388d51e1" +dependencies = [ + "ahash", +] + +[[package]] +name = "zenoh-config" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8672f4eaf88fd486f0503c59d19edfc25e7dd689bccd90d3cb731a5f627e0df" +dependencies = [ + "json5", + "nonempty-collections", + "num_cpus", + "secrecy", + "serde", + "serde_json", + "serde_with", + "serde_yaml", + "tracing", + "uhlc", + "validated_struct", + "zenoh-core", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-protocol", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-core" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1525319e4d9ef2af54fc9c74abf419236e32e6535081339321f3f55e2f34ce2f" +dependencies = [ + "lazy_static", + "tokio", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-crypto" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b80a042fc71419fc4952a90c9cbcfb323c0ced048125d8b44fd362f184045f" +dependencies = [ + "aes", + "hmac", + "rand 0.8.6", + "rand_chacha 0.3.1", + "sha3", + "zenoh-result", +] + +[[package]] +name = "zenoh-keyexpr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f04c82f0728f6704a1a397a04b38de5b2fd5a9a886a232cf650c9af294ba5d" +dependencies = [ + "getrandom 0.2.17", + "hashbrown 0.16.1", + "keyed-set", + "rand 0.8.6", + "schemars 1.2.1", + "serde", + "token-cell", + "zenoh-result", +] + +[[package]] +name = "zenoh-link" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a71103cfe96a851ef5ff781d64dda95c70e70208f74e186d6d294ba21e89cc64" +dependencies = [ + "zenoh-config", + "zenoh-link-commons", + "zenoh-link-quic", + "zenoh-link-quic_datagram", + "zenoh-link-tcp", + "zenoh-link-tls", + "zenoh-link-udp", + "zenoh-link-unixsock_stream", + "zenoh-link-ws", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-commons" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc91a5163793f842b4b016b2d50640db5a3533370348eb796e7078c576e87cf" +dependencies = [ + "async-trait", + "base64", + "bytes", + "flume", + "futures", + "quinn", + "quinn-proto", + "rcgen", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "rustls-webpki", + "secrecy", + "serde", + "socket2 0.5.10", + "time", + "tokio", + "tokio-util", + "tracing", + "webpki-roots", + "x509-parser", + "zenoh-buffers", + "zenoh-codec", + "zenoh-config", + "zenoh-core", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-util", +] + +[[package]] +name = "zenoh-link-quic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d065833147be895b7091cc3e505433c2c8b3172e36c9e06e84a5779a4aa655" +dependencies = [ + "async-trait", + "rustls-webpki", + "time", + "tracing", + "zenoh-core", + "zenoh-link-commons", + "zenoh-link-quic_datagram", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-quic_datagram" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e81b15df31a3d0ddde9a4fb3fbc928e9a2a6d6a4de38bcf55badd3a5208947a4" +dependencies = [ + "async-trait", + "rustls-webpki", + "time", + "tokio-util", + "tracing", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-tcp" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e912ac36902173dfc295317e001dc630e5ac9a70c2780f2d2eac500b205a700" +dependencies = [ + "async-trait", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "zenoh-config", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", +] + +[[package]] +name = "zenoh-link-tls" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f6b308ae2599f9c1344fbcd3418b2c3a3a9fe287ec39501ba834168493ba37" +dependencies = [ + "async-trait", + "base64", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "rustls-webpki", + "secrecy", + "socket2 0.5.10", + "time", + "tls-listener", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "webpki-roots", + "x509-parser", + "zenoh-config", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-link-udp" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca106ca8c3b7625e7071b9d7408955726e994c8f35fd68e00de8ac58b185d52d" +dependencies = [ + "async-trait", + "libc", + "socket2 0.5.10", + "tokio", + "tokio-util", + "tracing", + "windows-sys 0.61.2", + "zenoh-buffers", + "zenoh-core", + "zenoh-link-commons", + "zenoh-link-quic_datagram", + "zenoh-protocol", + "zenoh-result", + "zenoh-sync", + "zenoh-util", +] + +[[package]] +name = "zenoh-link-unixsock_stream" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2b7f60cdb5ad771d1a4f8e2eda15529e96dbe81c0ad2c1015b4c194347676" +dependencies = [ + "async-trait", + "nix", + "tokio", + "tokio-util", + "tracing", + "uuid", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-link-ws" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7fb54899f7fbdfc4fd02e647f9bba0d6e089cca4355acafb9499c510ebea79" +dependencies = [ + "async-trait", + "futures-util", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tracing", + "url", + "zenoh-core", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-util", +] + +[[package]] +name = "zenoh-macros" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9310b02a8f6dc4bd04d9ce6b318b9d00182aeeeeca60410003307d63a2569a3f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "zenoh-keyexpr", +] + +[[package]] +name = "zenoh-plugin-trait" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e235815d14b22448aa1db948560328761530932fa7a2f9a3c14853b3f0267941" +dependencies = [ + "git-version", + "libloading", + "serde", + "stabby", + "tracing", + "zenoh-config", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", + "zenoh-util", +] + +[[package]] +name = "zenoh-protocol" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeab45020bbecc077f14f06ee8f5aee65ce760af72481e663cac58b5dbfa66dd" +dependencies = [ + "const_format", + "rand 0.8.6", + "serde", + "uhlc", + "zenoh-buffers", + "zenoh-keyexpr", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-result" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca8e65b08f211833fe31cf38d73a48c6e1d6d900914e1ddd8cb176b3355b75b" +dependencies = [ + "anyhow", +] + +[[package]] +name = "zenoh-runtime" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd3d0c1558f909c9a74bde5e398c5733f81eb954818451baac2a6c09f48d6a5e" +dependencies = [ + "lazy_static", + "ron", + "serde", + "tokio", + "tracing", + "zenoh-macros", + "zenoh-result", +] + +[[package]] +name = "zenoh-sync" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9588f87db82b414a3e73d13312026be826332f53d270966e3e19f77f7fa1f06d" +dependencies = [ + "arc-swap", + "event-listener", + "futures", + "tokio", + "zenoh-buffers", + "zenoh-collections", + "zenoh-core", +] + +[[package]] +name = "zenoh-task" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2ac601598a27152b366ba1c6825216f01f77bb898f719b7752099a3594ce72" +dependencies = [ + "futures", + "tokio", + "tokio-util", + "tracing", + "zenoh-core", + "zenoh-runtime", +] + +[[package]] +name = "zenoh-transport" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80800c4adc26dbe81418735068541cf39820a95ec988114f04dd014775ba7c97" +dependencies = [ + "async-trait", + "crossbeam-utils", + "flume", + "futures", + "lazy_static", + "lz4_flex", + "rand 0.8.6", + "ringbuffer-spsc", + "rsa", + "serde", + "sha3", + "tokio", + "tokio-util", + "tracing", + "zenoh-buffers", + "zenoh-codec", + "zenoh-config", + "zenoh-core", + "zenoh-crypto", + "zenoh-link", + "zenoh-link-commons", + "zenoh-protocol", + "zenoh-result", + "zenoh-runtime", + "zenoh-sync", + "zenoh-task", + "zenoh-util", +] + +[[package]] +name = "zenoh-util" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b10369df18a781a3e675c9a2cbf54adb44c9dc2a376c1014c5e488410df2179" +dependencies = [ + "async-trait", + "const_format", + "flume", + "home", + "humantime", + "lazy_static", + "libc", + "libloading", + "pnet_datalink", + "schemars 1.2.1", + "serde", + "serde_json", + "shellexpand", + "tokio", + "tracing", + "tracing-subscriber", + "winapi", + "zenoh-core", + "zenoh-result", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tutorial/phases/03_zenoh_topology/Cargo.toml b/tutorial/phases/03_zenoh_topology/Cargo.toml new file mode 100644 index 00000000..e25759de --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +resolver = "2" +members = [ + "crates/up-bms-proto", + "crates/up-battery-telemetry-publisher", + "crates/up-telemetry-subscriber", + "crates/up-thermal-logging-subscriber", +] diff --git a/tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/Cargo.toml b/tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/Cargo.toml new file mode 100644 index 00000000..12d7a5b6 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "up-battery-telemetry-publisher" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-bms-proto = { path = "../up-bms-proto" } +up-rust = "0.9.0" +tokio = { version = "1.52.3", features = ["full"] } +anyhow = "1.0" +rand = "0.9" +log = "0.4" +env_logger = "0.11" +up-transport-zenoh = "0.9.0" +zenoh = "1.6.2" diff --git a/tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/src/main.rs b/tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/src/main.rs new file mode 100644 index 00000000..53feb389 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/src/main.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use std::sync::Arc; + +use rand::Rng; +use up_bms_proto::constants::*; +use up_bms_proto::BatteryTelemetry; +use up_rust::communication::{CallOptions, Publisher, SimplePublisher, UPayload}; +use up_rust::{StaticUriProvider, UTransport}; +use up_transport_zenoh::{zenoh_config, UPTransportZenoh}; + +#[tokio::main] +#[allow(unreachable_code, unused_variables)] +async fn main() -> Result<(), anyhow::Error> { + env_logger::init(); + + println!("--- Battery telemetry publisher starting ---"); + + let uri_provider = Arc::new(StaticUriProvider::new( + AUTHORITY_NAME, + PUBLISHER_UE_ID, + PUBLISHER_UE_VERSION, + )); + + // Phase 3 — Zenoh-backed UTransport via up-transport-zenoh. + // Replaces Phase 2's UnixDomainSocketTransport::connect. + // + // Config::default() opens a Zenoh *peer* with UDP multicast scouting. + // Peers can discover each other without a zenohd router (peer-to-peer). + // An optional router is fine too; for a remote endpoint you can set: + // config.connect.endpoints = vec!["tcp/:7447".parse()?]; + let transport: Arc = + Arc::new( + UPTransportZenoh::builder(AUTHORITY_NAME) + .map_err(|e| anyhow::anyhow!("builder failed: {e}"))? + .with_config(zenoh_config::Config::default()) + .build() + .await + .map_err(|e| anyhow::anyhow!("Zenoh transport build failed: {e}"))?, + ); + + let publisher = SimplePublisher::new(transport, uri_provider); + let mut rng = rand::rng(); + + for i in 1..=EXPECTED_MESSAGE_COUNT { + let telemetry = BatteryTelemetry { + soc_percent: rng.random_range(75.0..78.9), + temp_celsius: rng.random_range(20..=25), + ..Default::default() + }; + + println!( + "Message {}: SoC = {:.1}%, Temp = {}°C", + i, telemetry.soc_percent, telemetry.temp_celsius + ); + + let payload = UPayload::try_from_protobuf(telemetry)?; + log::trace!( + "SimplePublisher::publish → UTransport::send (resource_id=0x{BATTERY_TELEMETRY_RESOURCE_ID:04x})" + ); + publisher + .publish( + BATTERY_TELEMETRY_RESOURCE_ID, + CallOptions::for_publish(Some(5000), None, None), + Some(payload), + ) + .await + .map_err(|err| anyhow::anyhow!("publish failed: {err}"))?; + + println!(); + } + + Ok(()) +} diff --git a/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/Cargo.toml b/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/Cargo.toml new file mode 100644 index 00000000..0443d303 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "up-bms-proto" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +protobuf = "3.7.2" + +[build-dependencies] +protobuf-codegen = "3.7.2" +protoc-bin-vendored = "3.2.0" diff --git a/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/build.rs b/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/build.rs new file mode 100644 index 00000000..c37a7812 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/build.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +fn main() -> Result<(), Box> { + protobuf_codegen::Codegen::new() + .protoc() + .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap()) + .include("proto") + .input("proto/bms_telemetry.proto") + .cargo_out_dir("gen") + .run_from_script(); + Ok(()) +} diff --git a/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/proto/bms_telemetry.proto b/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/proto/bms_telemetry.proto new file mode 100644 index 00000000..adb81669 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/proto/bms_telemetry.proto @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +syntax = "proto3"; + +package tutorial.bms.v1; + +// Battery management telemetry published by the demo publisher. +// SoC and cell temperature only — thermal logging is a separate consumer (Stage 3 §3.4). +message BatteryTelemetry { + float soc_percent = 1; + int32 temp_celsius = 2; +} diff --git a/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/src/lib.rs b/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/src/lib.rs new file mode 100644 index 00000000..5412a830 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-bms-proto/src/lib.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +//! Shared BMS telemetry protobuf types and demo constants (Phase 2 → Phase 3 copy-forward). +//! +//! Schema: `proto/bms_telemetry.proto` — generated at build time by `build.rs`. + +pub mod constants { + /// Authority for all Phase 3 demo URIs (aligned with Phases 1–2). + pub const AUTHORITY_NAME: &str = "my_own_car"; + pub const PUBLISHER_UE_ID: u32 = 0x1010; + pub const PUBLISHER_UE_VERSION: u8 = 0x01; + pub const BATTERY_TELEMETRY_RESOURCE_ID: u16 = 0x8001; + pub const EXPECTED_MESSAGE_COUNT: u32 = 5; +} + +mod generated { + include!(concat!(env!("OUT_DIR"), "/gen/mod.rs")); +} + +pub use generated::bms_telemetry::BatteryTelemetry; diff --git a/tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/Cargo.toml b/tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/Cargo.toml new file mode 100644 index 00000000..c5614455 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "up-telemetry-subscriber" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-bms-proto = { path = "../up-bms-proto" } +up-rust = "0.9.0" +tokio = { version = "1.52.3", features = ["full"] } +anyhow = "1.0" +async-trait = "0.1" +log = "0.4" +env_logger = "0.11" +up-transport-zenoh = "0.9.0" +zenoh = "1.6.2" diff --git a/tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/src/main.rs b/tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/src/main.rs new file mode 100644 index 00000000..b7b1dd6a --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/src/main.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, +}; + +use async_trait::async_trait; +use tokio::sync::Notify; +use up_bms_proto::constants::*; +use up_bms_proto::BatteryTelemetry; +use up_rust::{LocalUriProvider, StaticUriProvider, UListener, UMessage, UTransport}; +use up_transport_zenoh::{zenoh_config, UPTransportZenoh}; + +struct BatteryTelemetryListener { + received: Arc, + shutdown: Arc, +} + +#[async_trait] +impl UListener for BatteryTelemetryListener { + async fn on_receive(&self, msg: UMessage) { + match msg.extract_protobuf::() { + Ok(telemetry) => { + let count = self.received.fetch_add(1, Ordering::SeqCst) + 1; + log::trace!( + "UListener::on_receive via transport dispatch (message {count}/{EXPECTED_MESSAGE_COUNT})" + ); + println!( + "[Battery telemetry subscriber] Processing incoming telemetry...\n\ + -> State of Charge: {:.1}%\n\ + -> Cell Temp: {} °C", + telemetry.soc_percent, telemetry.temp_celsius, + ); + + if count >= EXPECTED_MESSAGE_COUNT { + self.shutdown.notify_one(); + } + } + Err(err) => eprintln!("Failed to decode BatteryTelemetry payload: {err}"), + } + } +} + +#[tokio::main] +#[allow(unreachable_code, unused_variables)] +async fn main() -> Result<(), anyhow::Error> { + env_logger::init(); + + let uri_provider = StaticUriProvider::new( + AUTHORITY_NAME, + PUBLISHER_UE_ID, + PUBLISHER_UE_VERSION, + ); + let source_filter = uri_provider.get_resource_uri(BATTERY_TELEMETRY_RESOURCE_ID); + + let received = Arc::new(AtomicU32::new(0)); + let shutdown = Arc::new(Notify::new()); + let listener = Arc::new(BatteryTelemetryListener { + received, + shutdown: shutdown.clone(), + }); + + // Phase 3 — Zenoh-backed UTransport via up-transport-zenoh. + // Replaces Phase 2's UnixDomainSocketTransport::bind. + // + // Config::default() opens a Zenoh *peer* with UDP multicast scouting. + // Peers can discover each other without a zenohd router (peer-to-peer). + // An optional router is fine too; for a remote endpoint you can set: + // config.connect.endpoints = vec!["tcp/:7447".parse()?]; + let transport: Arc = + Arc::new( + UPTransportZenoh::builder(AUTHORITY_NAME) + .map_err(|e| anyhow::anyhow!("builder failed: {e}"))? + .with_config(zenoh_config::Config::default()) + .build() + .await + .map_err(|e| anyhow::anyhow!("Zenoh transport build failed: {e}"))?, + ); + + transport + .register_listener(&source_filter, None, listener) + .await?; + log::trace!( + "registered UListener with source filter resource_id=0x{BATTERY_TELEMETRY_RESOURCE_ID:04x}" + ); + + println!( + "Battery telemetry subscriber listening (expecting {} messages)", + EXPECTED_MESSAGE_COUNT + ); + + shutdown.notified().await; + println!("Received {EXPECTED_MESSAGE_COUNT} messages — exiting."); + + Ok(()) +} diff --git a/tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/Cargo.toml b/tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/Cargo.toml new file mode 100644 index 00000000..83087e90 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "up-thermal-logging-subscriber" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +up-bms-proto = { path = "../up-bms-proto" } +up-rust = "0.9.0" +tokio = { version = "1.52.3", features = ["full"] } +anyhow = "1.0" +async-trait = "0.1" +log = "0.4" +env_logger = "0.11" +up-transport-zenoh = "0.9.0" +zenoh = "1.6.2" diff --git a/tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/src/main.rs b/tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/src/main.rs new file mode 100644 index 00000000..73edb570 --- /dev/null +++ b/tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/src/main.rs @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Nirmalya Sengupta (https://github.com/nsengupta) + +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, +}; + +use async_trait::async_trait; +use tokio::sync::Notify; +use up_bms_proto::constants::*; +use up_bms_proto::BatteryTelemetry; +use up_rust::{LocalUriProvider, StaticUriProvider, UListener, UMessage, UTransport}; +use up_transport_zenoh::{zenoh_config, UPTransportZenoh}; + +/// Listener that logs temperature warnings and tracks message count. +/// +/// §3.4 — a second consumer on the same Zenoh data space, demonstrating +/// fan-out without sharing the battery subscriber's process or socket. +struct ThermalLoggingListener { + received: Arc, + shutdown: Arc, +} + +#[async_trait] +impl UListener for ThermalLoggingListener { + async fn on_receive(&self, msg: UMessage) { + match msg.extract_protobuf::() { + Ok(telemetry) => { + let count = self.received.fetch_add(1, Ordering::SeqCst) + 1; + log::trace!( + "ThermalLoggingListener::on_receive via transport dispatch (message {count}/{EXPECTED_MESSAGE_COUNT})" + ); + + let temp = telemetry.temp_celsius; + if temp > 25 { + println!( + "[Thermal logging subscriber] ⚠️ WARNING — cell temperature {temp}°C exceeds 25°C threshold" + ); + } else { + println!( + "[Thermal logging subscriber] Cell temperature {temp}°C — OK" + ); + } + + if count >= EXPECTED_MESSAGE_COUNT { + self.shutdown.notify_one(); + } + } + Err(err) => eprintln!( + "Failed to decode BatteryTelemetry payload: {err}" + ), + } + } +} + +#[tokio::main] +#[allow(unreachable_code, unused_variables)] +async fn main() -> Result<(), anyhow::Error> { + env_logger::init(); + + let uri_provider = StaticUriProvider::new( + AUTHORITY_NAME, + PUBLISHER_UE_ID, + PUBLISHER_UE_VERSION, + ); + let source_filter = uri_provider.get_resource_uri(BATTERY_TELEMETRY_RESOURCE_ID); + + let received = Arc::new(AtomicU32::new(0)); + let shutdown = Arc::new(Notify::new()); + let listener = Arc::new(ThermalLoggingListener { + received, + shutdown: shutdown.clone(), + }); + + // Phase 3 — Same Zenoh-backed UTransport as the battery subscriber. + // Fan-out is Zenoh's native pub/sub (same transport) — not uProtocol L3 uSubscription. + // + // Config::default() opens a Zenoh *peer* with UDP multicast scouting. + // Peers can discover each other without a zenohd router (peer-to-peer). + let transport: Arc = + Arc::new( + UPTransportZenoh::builder(AUTHORITY_NAME) + .map_err(|e| anyhow::anyhow!("builder failed: {e}"))? + .with_config(zenoh_config::Config::default()) + .build() + .await + .map_err(|e| anyhow::anyhow!("Zenoh transport build failed: {e}"))?, + ); + + transport + .register_listener(&source_filter, None, listener) + .await?; + log::trace!( + "registered ThermalLoggingListener with source filter resource_id=0x{BATTERY_TELEMETRY_RESOURCE_ID:04x}" + ); + + println!( + "Thermal logging subscriber listening (expecting {} messages)", + EXPECTED_MESSAGE_COUNT + ); + + shutdown.notified().await; + println!("Received {EXPECTED_MESSAGE_COUNT} messages — exiting."); + + Ok(()) +} diff --git a/tutorial/tutorial-text/Tutorial-Phase-1.md b/tutorial/tutorial-text/Tutorial-Phase-1.md new file mode 100644 index 00000000..d16e2184 --- /dev/null +++ b/tutorial/tutorial-text/Tutorial-Phase-1.md @@ -0,0 +1,477 @@ + +### Prologue +The story began when I was exploring the world of Eclipse-SDV ([here](https://eclipsesdv.org/)) out of curiosity. This +was an area hitherto completely unknown to me. Yet, I was drawn towards it. Why? I have captured +the reasons [here](https://nsengupta.github.io/blog/why-explore-software-defined-vehicle/). +One of the technologies that captured my interest was [uProtocol](https://github.com/eclipse-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 uProtocol REPO, blogs (Pete Le Vasseur, +chief maintainer of [uProtocol](https://petelevasseur.com/articles/index.html)) 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 tutrial follows how I approached learning uProtocol; +hopefully, this will be useful for us too. + +---- + +### Chapter 1 + +### What shall we build + +We will build a small application using uProtocol's up-rust ([here](https://github.com/eclipse-uprotocol/up-rust)) implementation. Along the +way, we will clarify the _what_-s and the _why_-s. I believe this is good way to let things fall +in the appropriate _conceptual_ place. Every crate under `phases/01_raw_sockets/` pins +`up-rust = "0.9.0"`; API links in this chapter point at that version on docs.rs. + +The application is simple. There exists: + +- An application that can send out ( _publish_ ) two pieces of information from a car's battery + telemetry: + (a) State-of-charge and (b) Temperature; in the code, this is `up-battery-telemetry-publisher` +- An application that can read these values and print; in the code, this is `up-telemetry-subscriber` + +Each application runs in its own address-space (two different processes, run from separate +shells on my Linux machine). + +This tutorial is **Phase-1**. The code lives in `phases/01_raw_sockets/`: + +``` +phases/01_raw_sockets/ +├── Cargo.toml +└── crates/ + ├── up-frame-codec/ # length-prefix serialize + deserialize + ├── up-battery-telemetry-publisher # sends UMessage over Unix Domain Socket + └── up-telemetry-subscriber # receives UMessage over Unix Domain Socket +``` +A later phase (Phase-2, in `phases/02_uprotocol_semantics/`) will refactor this same +functionality using uProtocol's own L1 transport and L2 communication helpers. + +For any such application where two (or more, but that's later) processes converse between +themselves, two aspects are important (obvious, one may quip): + +1. What is shared +2. How is it shared + +The 'what' part needs a little more understanding. One application doesn't 'know' the other +application. Therefore, both the sides have to have complete knowledge of 'what' - the _structure_ +and the mechanism to _interpret_ the structure. One side cannot change the 'what' without the +other side being aware of it. They are bound by this pre-condition. + +The 'how' is the aspect of transportation: either side must be equipped to hook itself with and +collect from the same transportation facility or 'channels'. This is the other binding factor. +It is important to note here, that each such 'channel' comes with its own facilities and +limitations. Again, both the sides have to agree to adjust to and/or abide by these. We will +revisit this later in this tutorial. + +Let's focus on the *what* part first. 2 pieces of data are sent and received: +1) Percent of charge remaining in battery (a `float` value) +2) Temperature of battery (again, a `float` value) + +We assume that these two are packed in a CAN-frame (we don't care where do these originate; not +important for the tutorial). + +```rust +fn pack_bms_can_frame(battery_level_pct: f32, temperature_c: i8) -> [u8; 8] { + let mut can_data = [0u8; 8]; + // Convert using a DBC scale of 0.5 (e.g. 75.0 / 0.5 = 150) + let raw_soc = (battery_level_pct / 0.5) as u8; + can_data[0] = raw_soc; + can_data[1] = temperature_c as u8; + can_data +} +``` + +This array is the application _payload_ (raw bytes) that is transported and collected at the other +end. We want to put those bytes in a uProtocol envelope: a [UMessage](https://docs.rs/up-rust/0.9.0/up_rust/struct.UMessage.html). + +Before we build the message, the envelope needs a **source** identity — who/what this PUBLISH is +from. That identity is a [UUri](https://docs.rs/up-rust/0.9.0/up_rust/struct.UUri.html). + +A UUri address is always: + +**authority + numeric entity id + version + numeric resource id.** + +Our **battery telemetry topic** — State-of-Charge and temperature from the battery publisher — is: + +```text +up://my_own_car/1010/1/8001 + +authority my_own_car +entity 1010 (0x1010) ← our battery publisher entity +version 1 +resource 8001 (0x8001) ← SoC / temperature telemetry resource +``` + +The labels on the right are for us as readers; they are **not** URI path segments. In code we build +the same address with `UUri::try_from_parts` (Chapter 2 also shows parsing from a string with +`UUri::from_str`): + +```rust +let source_uri = UUri::try_from_parts("my_own_car", 0x1010, 1, 0x8001)?; +``` + +We will revisit `UUri` in detail in Chapter 2. + +Application code should not assemble `UAttributes` / `UMessage` structs by hand. This tutorial +follows current recommended `up-rust` practice: build messages with +[UMessageBuilder](https://docs.rs/up-rust/0.9.0/up_rust/struct.UMessageBuilder.html), which sets +the fields required for a **PUBLISH** correctly (and keeps the same pattern for other message +types when we meet them later). + +> **Side note:** In an upcoming `up-rust` release, the constructors for `UAttributes` and +> `UMessage` will no longer be public — another reason to learn the builder now rather than +> struct literals. + +```rust +let message = UMessageBuilder::publish(source_uri) + .with_ttl(5000) + .build_with_payload( + pack_bms_can_frame(battery_pct, temp_c).to_vec(), + UPayloadFormat::UPAYLOAD_FORMAT_RAW, + )?; +``` + +- `publish(source_uri)` marks the message as **PUBLISH** and sets **source** to our battery + telemetry topic URI (`up://my_own_car/1010/1/8001`). +- `with_ttl(5000)` sets a 5-second usefulness window. +- `build_with_payload(bytes, format)` attaches the raw CAN bytes and a + [UPayloadFormat](https://docs.rs/up-rust/0.9.0/up_rust/enum.UPayloadFormat.html) hint (`RAW` here). + +L1 is concerned with **`UMessage` instances**. The L2 helper type `UPayload` is useful with +communication APIs such as `SimplePublisher`; we defer it to Phase 2. Here we only need bytes + +format on the builder. + +Now that the 'what' part is done (more or less; Chapter 2 goes deeper), the next part to deal with +is 'how'. + +These two applications are running on a single Linux host. One of the easiest ways to connect +them is to use a **Unix Domain Socket** (not to be confused with automotive Unified Diagnostic +Services). It is quite easy to send a byte-stream through a Unix Domain Socket. However, +the socket behaves as if it is forwarding a stream; the start and end of a message is not +interpreted. So, we have to arrange for marking these two. One standard way to achieve this is to +prefix the buffer with its length. The length is an integer (4 bytes); so the receiving +application can read the length (first 4 bytes), and then read _that many_ bytes from the buffer +that arrived. + +In the publisher side +```rust +pub fn serialize_for_unix_socket(msg: &UMessage) -> Result, anyhow::Error> { + let envelope_bytes = msg.write_to_bytes()?; + let msg_len = envelope_bytes.len() as u32; + let mut framed_buffer = msg_len.to_be_bytes().to_vec(); + framed_buffer.append(&mut envelope_bytes.to_vec()); // Length is prefixed + + Ok(framed_buffer) +} +``` +Conversely, in the subscriber side: +```rust +pub fn deserialize_for_unix_socket(framed: &[u8]) -> Result { + if framed.len() < 4 { + anyhow::bail!("framed buffer too short for length prefix"); + } + + let body_len = u32::from_be_bytes(framed[0..4].try_into()?) as usize; + let end = 4 + body_len; + if framed.len() < end { + anyhow::bail!("framed buffer too short for declared body length"); + } + + Ok(UMessage::parse_from_bytes(&framed[4..end])?) +} +``` +[Jump to 'How to run' for a sample output](#how-to-run) + +So, there we are. A uProtocol `UMessage` has been shared between two applications using an +OS-provided transport facility. + +One important inference that we can draw is that uProtocol's types and operations are **completely +independent** of the transport facility that is used to move the messages. We will revisit this +aspect in the next sections. + +---- + +### Chapter 2 + +Let's take a good look at types that uProtocol gives us. + +This is what we have seen: + +- We identified the publish topic with a `UUri`. +- We built a PUBLISH `UMessage` with `UMessageBuilder` (raw bytes + `UPayloadFormat`). +- Message **attributes** describe purpose, source, lifetime, and identity; the builder fills them in. +- Payload **bytes** ride inside the `UMessage`. + +On the uProtocol layer map (the same stack Phases 2–3 will reuse), Phase 1 works at the bottom +two bands only: + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Application / uEntity │ +│ publisher · subscriber │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ L2 — Communication (Phase 2: SimplePublisher, UPayload, …) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ L1 — Transport (Phase 2: UTransport, UListener, …) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Envelope (every message) ← Phase 1 │ +│ UMessage · UAttributes (incl. source/sink UUri, payload format, …) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Wire ← Phase 1 │ +│ Unix Domain Socket + length-prefix framing (`up-frame-codec`) │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +``` +╔═══════════╗ +║ UUri ║ +╚═══════════╝ +``` +uProtocol needs a way to name a software entity / resource anywhere in the vehicle. In this +tutorial, that includes our **battery telemetry topic** (`up://my_own_car/1010/1/8001`) — +State-of-Charge and temperature from the battery publisher. + +A valid address for that topic is: + +```text +up://my_own_car/1010/1/8001 +``` + +After the authority, the path segments are **hex encodings of unsigned integers** (entity id, +version, resource id) — not free-form names. See the current +[UUri specification](https://github.com/eclipse-uprotocol/up-spec/blob/main/basics/uri.adoc). + +```text +┌──────────┬──────────────────┬────────────────┬────────────────────────────────┐ +│ Field │ Code property │ Value in URI │ Our label (commentary only) │ +├──────────┼──────────────────┼────────────────┼────────────────────────────────┤ +│ Scheme │ up │ up │ uProtocol │ +│ Authority│ authority_name │ my_own_car │ deployment namespace │ +│ uEntity │ ue_id │ 1010 (0x1010) │ battery telemetry publisher │ +│ Version │ ue_version_major │ 1 │ API major version │ +│ uResource│ resource_id │ 8001 (0x8001) │ SoC / temperature telemetry │ +└──────────┴──────────────────┴────────────────┴────────────────────────────────┘ +``` + +The last column is for us, humans. It is **not** part of the URI. + +Note: A _[uEntity](https://github.com/eclipse-uprotocol/up-spec/blob/main/basics/README.adoc#uentity)_ +(uProtocol software entity) is a piece of software deployed somewhere on a network host (in our +case, the car itself). uEntities are uniquely identified within a system by means of the type and +version of the service interface that they implement — expressed as numeric ids, not dotted names +like `powertrain.battery`. + +Names such as “battery SoC” are useful in docs and conversation. The **address** on the wire is +numeric so it stays compact, stable, and machine-checkable; not to be put in the URI path. + +In application code we use the `UUri` type, which carries the same four fields. Create instances +with the supported constructors — not struct literals: + +```rust +// When we already know the numeric parts: +let source_uri = UUri::try_from_parts("my_own_car", 0x1010, 1, 0x8001)?; + +// When we have a URI string: +let source_uri = UUri::from_str("up://my_own_car/1010/1/8001")?; +``` + +**Are those IDs arbitrary?** No. They are _car-level constants_ assigned for this system (and, in +this tutorial, chosen demo constants — see the registry below). + +Let's start with _Authority_. It is the root of a tree holding specific resources, laying out a +_namespace_; just like 'github.com' defines the root of all resources held on that server. In the +seemingly endless Internet, if I want to reach a particular source code of mine, I can type +'www.github.com/nsengupta/personal_project/...' in the browser. The DNS finds where that +'github.com' physically is and hands the HTTP request to it. In the same vein, in an SDV-run car, +the _Authority_ can hold values like `my_own_car`, `central_compute`, or `front_left_zone`. In the +entire car there can be only one `my_own_car` or `front_left_zone` — just as on the Internet there +can be only one 'github.com'. + +**Authority is a name; entity and resource under it are numeric IDs.** + +Inside the authority there can be more than one uEntity. For example, a battery publisher, a +head-lamp service, and a tyre service each get their own `ue_id`. A given uEntity may expose one +or more **resources** (topics or methods) — each with its own `resource_id`. + +Remember: **uEntity** = the software component; **resource** = one part of that entity's interface. +For tyre pressure, *tyre* is the entity and *pressure* is a resource of that entity. + +Here is a second topic — head-lamp is-on — **same four-part shape**, distinct IDs: + +```text +up://my_own_car/1020/1/8002 + +authority my_own_car +entity 1020 (0x1020) ← head-lamp entity +version 1 +resource 8002 (0x8002) ← is-on +``` + +```rust +let head_lamp_uri = UUri::try_from_parts("my_own_car", 0x1020, 1, 0x8002)?; +``` + +And a third card so the pattern sticks — tyre / pressure: + +```text +up://my_own_car/101F/1/A010 + +authority my_own_car +entity 101F (0x101F) ← tyre entity +version 1 +resource A010 (0xA010) ← pressure resource +``` + +```rust +let tyre_pressure_uri = UUri::try_from_parts("my_own_car", 0x101F, 1, 0xA010)?; +``` + +Different topic, **same four-part shape**. + +| Topic | Authority | Entity | Resource | Labels (commentary only) | +|-------|-----------|--------|----------|---------------------------| +| Battery telemetry (this phase's runnable source) | `my_own_car` | `0x1010` | `0x8001` | battery publisher / SoC | +| Head-lamp is-on | `my_own_car` | `0x1020` | `0x8002` | head-lamp / is-on | +| Tyre pressure | `my_own_car` | `0x101F` | `0xA010` | tyre / pressure | + +```text ++-- +| +| Rules exist to assign uEntity IDs. Read more about assignment of IDs to uEntities +| [here](https://github.com/eclipse-uprotocol/up-spec/blob/main/up-l3/README.adoc#31-uentity-id-ranges). +| ++-- +``` + +With a `source_uri` in hand, Chapter 1 built a PUBLISH `UMessage` via `UMessageBuilder`. We now +look at that envelope more carefully. + +``` +╔══════════════╗ +║ UMessage ║ +╚══════════════╝ +``` + +`UMessage` is the L1 unit we send and receive. Application code creates a PUBLISH message like +this (same path as the publisher crate): + +```rust +let message = UMessageBuilder::publish(source_uri) + .with_ttl(5000) + .build_with_payload( + pack_bms_can_frame(battery_pct, temp_c).to_vec(), + UPayloadFormat::UPAYLOAD_FORMAT_RAW, + )?; +``` + +``` +╔══════════════════╗ +║ UAttributes ║ +╚══════════════════╝ +``` + +_UAttributes_ play a very important role in _uProtocol_'s design. They + +- tell the world what the purpose of the message is, +- hint at how to interpret the contents, +- augment upstream routing logic of the transporters. + +We do **not** fill an `UAttributes { ... }` struct ourselves. The builder populates attributes for +a PUBLISH. Conceptually we care about: + +- **type** — `UMESSAGE_TYPE_PUBLISH`: interested parties may consume; this is not an RPC request. +- **source** — our battery telemetry topic (`up://my_own_car/1010/1/8001`). +- **ttl** — usefulness window (we set 5000 ms). +- **id** — unique message id (assigned by the builder). + +The complete data-model for _UAttributes_ is [here](https://docs.rs/up-rust/0.9.0/up_rust/struct.UAttributes.html). +For other values that message `type` can have, see [here](https://docs.rs/up-rust/0.9.0/up_rust/enum.UMessageType.html). + +After the builder returns, we can **inspect** those attributes on the `UMessage`: + +```rust +assert_eq!(message.type_unchecked(), UMessageType::UMESSAGE_TYPE_PUBLISH); +assert_eq!(message.source_unchecked(), &source_uri); +assert_eq!(message.ttl_unchecked(), 5000); +``` + +The `source` helps transporters and consumers reason about who is interested in this topic — some +wait for tyre pressure (`0x101F` / `0xA010`), others for head-lamp is-on (`0x1020` / `0x8002`). The +`ttl` helps decide if a late message is still useful. + +``` +╔══════════════════╗ +║ UPayloadFormat ║ +╚══════════════════╝ +``` + +Receivers need a hint for how to interpret the payload bytes. Phase 1 uses +`UPayloadFormat::UPAYLOAD_FORMAT_RAW` because we still pack a hand-rolled CAN layout. The L2 type +`UPayload` (bytes + format as a communication helper) arrives in Phase 2 together with protobuf +and `SimplePublisher`. + +Once we have the `UMessage`, we flatten it to a stream of bytes so it can be transported. +`UMessage.write_to_bytes()` gives us the series of octets that is transportation-ready; our +length-prefix framing then wraps that buffer for the Unix Domain Socket. + +---- + +### How to run + +Both processes use a Unix Domain Socket at **`{current working directory}/tmp/uprotocol_twin.sock`** +(defined in `up_frame_codec::socket_path`). Start them from the **same directory** (the repo root +below) so they share that path. The subscriber creates `tmp/` if needed. + +```shell +# From the repo root +cargo build --manifest-path phases/01_raw_sockets/Cargo.toml + +# Terminal 1 — subscriber +cargo run --manifest-path phases/01_raw_sockets/Cargo.toml -p up-telemetry-subscriber + +# Terminal 2 — publisher +cargo run --manifest-path phases/01_raw_sockets/Cargo.toml -p up-battery-telemetry-publisher +``` +Expected output: + +```shell +--- Battery telemetry publisher starting --- +Message 1: SoC = 76.6%, Temp = 24°C + Sent 67 bytes. + +Message 2: SoC = 77.9%, Temp = 23°C + Sent 67 bytes. + +Message 3: SoC = 76.6%, Temp = 21°C + Sent 67 bytes. + +Message 4: SoC = 75.3%, Temp = 20°C + Sent 67 bytes. + +Message 5: SoC = 78.3%, Temp = 22°C + Sent 67 bytes. + +``` +```shell +Battery telemetry subscriber listening on: …/tmp/uprotocol_twin.sock +[Battery telemetry subscriber] Processing incoming CAN telemetry... +-> State of Charge: 76.5% +-> Cell Temp: 24 °C +[Battery telemetry subscriber] Processing incoming CAN telemetry... +-> State of Charge: 77.5% +-> Cell Temp: 23 °C +[Battery telemetry subscriber] Processing incoming CAN telemetry... +-> State of Charge: 76.5% +-> Cell Temp: 21 °C +[Battery telemetry subscriber] Processing incoming CAN telemetry... +-> State of Charge: 75.0% +-> Cell Temp: 20 °C +[Battery telemetry subscriber] Processing incoming CAN telemetry... +-> State of Charge: 78.0% +-> Cell Temp: 22 °C + +``` diff --git a/tutorial/tutorial-text/Tutorial-Phase-2.md b/tutorial/tutorial-text/Tutorial-Phase-2.md new file mode 100644 index 00000000..d27426d0 --- /dev/null +++ b/tutorial/tutorial-text/Tutorial-Phase-2.md @@ -0,0 +1,806 @@ +### Prologue + +This is the second phase of the tutorial where we will build on everything that was established in Phase-1 (the previous chapter) and then some. So, if we +have not read the previous tutorial [Phase 1 tutorial](Tutorial-Phase-1.md), please do so. The code +corresponding to that (previous) tutorial is in `phases/01_raw_sockets/`. This chapter's +code lives in `phases/02_uprotocol_semantics/`. + + +In this chapter, we are going to: + +- See where the Phase-1 architecture falls short +- Introduce _uProtocol_'s built-in layering and how it helps +- Replace the hand-rolled socket code with one L1 type, `UnixDomainSocketTransport` (`connect` / + `bind`) — uProtocol's `UTransport` over a Unix Domain Socket +- Introduce `UPayload` properly at L2 (deferred from Phase 1) and replace RAW CAN bytes with typed + Protobuf messages +- Replace Phase 1's `UMessageBuilder` publish loop with `SimplePublisher` + `CallOptions` (L2) +- Replace the ad-hoc subscriber decode loop with `UListener` callbacks +- Be honest about what still doesn't work + +Let's dive in. + +---- + +### Chapter 1: Where Phase-1 left us + +In Phase-1, we successfully sent a `UMessage` from the battery-telemetry-publisher to the +telemetry-subscriber over a Unix Domain Socket. We packed SoC and temperature into 8 RAW bytes +using `pack_bms_can_frame` and unpacked with `unpack_bms_can_frame`. It worked. + +But, let's re-read the subscriber code from Phase-1: + +```rust +// Phase-1 subscriber — a transport program in disguise +loop { + let (mut stream, _) = listener.accept().await?; + tokio::spawn(async move { + let mut len_bytes = [0u8; 4]; + if stream.read_exact(&mut len_bytes).await.is_err() { return; } + let body_len = u32::from_be_bytes(len_bytes) as usize; + let mut body_bytes = vec![0u8; body_len]; + if stream.read_exact(&mut body_bytes).await.is_err() { return; } + // ... decode, unpack CAN frame, print ... + }); +} +``` +And the publisher: + +```rust +// Phase-1 publisher — builder + raw socket (no L1/L2 helpers yet) +let source_uri = UUri::try_from_parts("my_own_car", 0x1010, 1, 0x8001)?; +let message = UMessageBuilder::publish(source_uri) + .with_ttl(5000) + .build_with_payload( + pack_bms_can_frame(battery_pct, temp_c).to_vec(), + UPayloadFormat::UPAYLOAD_FORMAT_RAW, + )?; +let framed = serialize_for_unix_socket(&message)?; +let socket_path = up_frame_codec::socket_path()?; +let mut stream = UnixStream::connect(&socket_path).await?; +stream.write_all(&framed).await?; +``` +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ │ +│ Publisher (main.rs) Subscriber (main.rs) │ +│ ┌───────────────────┐ ┌───────────────────┐ │ +│ │ 1. Build UMessage │ │ 1. Accept socket │ │ +│ │ 2. Serialize │ Unix Domain Socket │ 2. Read + decode │ │ +│ │ 3. Connect + send │ ───────────────────────▶ │ 3. Unpack CAN │ │ +│ │ │ │ 4. Print telemetry│ │ +│ └───────────────────┘ └───────────────────┘ │ +│ │ +│ -- Everything in one binary — no separation of transport logic from │ +│ from business logic. │ +│ -- CAN byte layout is a shared secret. │ +│ -- No way to add a second consumer. │ +└─────────────────────────────────────────────────────────────────────────┘ +``` +### Something is not quite right there + +Both binaries embed transport details that do not belong in battery telemetry code. + +- The **publisher** does three jobs: build a `UMessage`, frame it, and connect+write to a socket. +- The **subscriber** does three jobs: accept a connection, read+decode a frame, interpret the payload +- If we want a second consumer for the same telemetry (say, a thermal management logging engine), + we cannot give it access to the subscriber's socket — there is only one accept loop. +- The **payload** is RAW bytes with a secret CAN-byte layout. If we change the layout, then both + sides must be updated in lockstep. + +Phase-2 fixes these using uProtocol's own facilities: L1 Transport, L2 Communication patterns, +and typed Protobuf payloads. + +---- + +### Chapter 2: The layers of uProtocol (a map) + +uProtocol is organized in layers. This diagram is the **canonical layer map** for the tutorial — +Phases 1 and 3 use the same stack; only which bands are active changes. + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Application / uEntity │ +│ up-battery-telemetry-publisher · up-telemetry-subscriber │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ L2 — Communication │ +│ SimplePublisher · CallOptions · UPayload │ +│ "PUBLISH this resource; attach typed payload + options" │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ L1 — Transport │ +│ UTransport · UListener · UnixDomainSocketTransport │ +│ send · register_listener │ +│ "Move this UMessage; invoke matching listeners when it arrives" │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Envelope (every message) │ +│ UMessage · UAttributes (incl. source/sink UUri, payload format, …) │ +│ (UUri is a field inside UAttributes — not a separate metadata layer) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Wire │ +│ socket_path() → {cwd}/tmp/uprotocol_twin.sock │ +│ serialize_for_unix_socket / deserialize_for_unix_socket │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +Phase 1 lived mainly at the **envelope** and **wire** layers (builder → `UMessage` → framed +socket bytes). Phase 2 introduces L1 (`UTransport`, `UListener`, `UnixDomainSocketTransport`) and +L2 (`SimplePublisher`, `CallOptions`, `UPayload`). The wire stays a Unix Domain Socket with +length-prefix framing — wrapped behind those abstractions. L3 application services +(uSubscription, uDiscovery, …) are not part of this phase. Specs for those layers live in +[uP-L1 (Transport)](https://github.com/eclipse-uprotocol/up-spec/tree/main/up-l1), +[uP-L2 (Communication)](https://github.com/eclipse-uprotocol/up-spec/tree/main/up-l2), and +[uP-L3 (Application)](https://github.com/eclipse-uprotocol/up-spec/tree/main/up-l3). +Crates under `phases/02_uprotocol_semantics/` keep the same pin as Phase 1: `up-rust = "0.9.0"`. + +Both processes attach the **same** L1 type. Socket bind vs connect is wire setup, not a Client/Server +split in the application model. L2 appears only on the publish path in this demo (`SimplePublisher`); +the subscriber registers an L1 `UListener` directly — uProtocol also defines an L2 `Subscriber`, but +we do not use it here (same-transport interest does not need it). + +``` +┌────────────────────────────────────────────────────────────────────────────────────┐ +│ Phase 2 — peer apps; same L1 transport on both sides │ +│ │ +│ Publisher app Subscriber app │ +│ ┌───────────────────────────┐ ┌────────────────────────────────┐ +│ │ BatteryTelemetry │ │ BatteryTelemetryListener │ │ +│ │ + UPayload │ │ (impl UListener) │ │ +│ └─────────────┬─────────────┘ └─────────────▲──────────────────┘ │ +│ │ publish() │ on_receive() │ +│ ┌─────────────▼─────────────┐ │ │ +│ │ SimplePublisher (L2) │ (no L2 Subscriber in this demo) │ +│ │ CallOptions │ │ │ +│ └─────────────┬─────────────┘ │ │ +│ │ UTransport::send │ │ +│ ┌─────────────▼─────────────┐ ┌─────────────────┴──────────────────┐ │ +│ │ UnixDomainSocketTransport │ ──────▶ │ UnixDomainSocketTransport::bind │ │ +│ │ ::connect │ UMessage │ │ │ +│ │ │ │ register_listener(...) │ │ +│ └───────────────────────────┘ └────────────────────────────────────┘ │ +│ Unix Domain Socket │ +│ │ +│ Shared: up-bms-proto · up-frame-codec · up-unix-domain-socket-transport │ +└────────────────────────────────────────────────────────────────────────────────────┘ +``` +---- + +### Chapter 3: What changed in the workspace + +Phase-2 lives in `phases/02_uprotocol_semantics/`. The workspace grew from 3 to 5 crates: + +``` +phases/02_uprotocol_semantics/ +├── Cargo.toml +└── crates/ + ├── up-frame-codec/ # length-prefix serialize (aligned with Phase 1) + ├── up-bms-proto/ # NEW: shared protobuf schema + demo constants + ├── up-unix-domain-socket-transport/ # NEW: UTransport over a Unix Domain Socket + ├── up-battery-telemetry-publisher/ # REFACTORED: SimplePublisher + UPayload + └── up-telemetry-subscriber/ # REFACTORED: UListener +``` +---- + +### Chapter 4: `up-bms-proto` — why typed protobuf payloads + +Phase 1 deliberately avoided `UPayload`. The L1 path builds a `UMessage` with +`UMessageBuilder::build_with_payload(raw_bytes, UPAYLOAD_FORMAT_RAW)`. The application still had +to invent a byte layout: + +```rust +// Phase 1 — RAW bytes in the envelope; no UPayload type +.build_with_payload( + pack_bms_can_frame(battery_pct, temp_c).to_vec(), + UPayloadFormat::UPAYLOAD_FORMAT_RAW, +) +``` +And on the subscriber side: + +```rust +let payload = msg.payload.unwrap(); +let (pct, temp) = unpack_bms_can_frame(&payload); +``` +`pack_bms_can_frame` and `unpack_bms_can_frame` are **C code in Rust clothing**. They exist +because the publisher and subscriber need a way to agree on how SoC and temperature are laid +out in a byte buffer. In C-based automotive systems, this agreement comes from a DBC file (CAN +database). Here, it comes from a pair of functions. + +Let's look at the actual functions from Phase-1: + +```rust +// 1 LSB = 0.5% SoC — this is a DBC convention in disguise +fn pack_bms_can_frame(soc_pct: f32, temp_c: f32) -> [u8; 8] { + let soc_encoded = (soc_pct / 0.5) as u16; + let temp_encoded = (temp_c * 10.0) as i16; + let mut buf = [0u8; 8]; + buf[0..2].copy_from_slice(&soc_encoded.to_be_bytes()); + buf[2..4].copy_from_slice(&temp_encoded.to_be_bytes()); + buf +} +``` +We can spot the problems: + +1. **Scale factors are buried in code.** `0.5` and `10.0` are kind of, magic numbers. If we + change the SoC resolution from 0.5% to 0.1%, we must find and update both pack *and* unpack functions + in lockstep. If we miss one, the subscriber prints garbage. + +2. **Byte layout is implicit.** `buf[0..2]` for SoC, `buf[2..4]` for temperature. These offsets + are invisible to anyone reading the code unless they carefully trace both functions. + +3. **There is no schema.** Nothing tells us "this buffer contains two fields named + `soc_percent` and `temp_celsius`". The field names exist only in the programmer's head and + in Rust's variable names. A DBC file at least names the signals. + +4. **Adding a third field is error-prone.** Double the buffer? Shift offsets? Every change + requires a coordinated deploy of both publisher and subscriber. + +**Protobuf solves all four problems in one move.** + +Instead of a pair of pack/unpack functions with hidden offsets, we write a schema file: + +```protobuf +// proto/bms_telemetry.proto +syntax = "proto3"; +package tutorial.bms.v1; + +message BatteryTelemetry { + float soc_percent = 1; + int32 temp_celsius = 2; +} +``` +- **No more bit-level encoding.** The raw CAN buffer required dividing SoC by 0.5 and + multiplying temperature by 10 to fit into `u16`/`i16` — then the reverse on the subscriber + side. Protobuf lets us express `soc_percent` as `float` and `temp_celsius` as `int32` + directly. The scale conversion (how the sensor's ADC count maps to a physical value) is + still an application concern — protobuf cannot annotate units — but the *wire-level* + encode/decode arithmetic is gone. +- **Byte layout is gone.** Protobuf field numbers (`= 1`, `= 2`) replace byte offsets. The + encoding is handled by the protobuf library. +- **The schema is the contract.** Both publisher and subscriber compile from the same `.proto` + file. Any change to the schema is immediately visible at compile time — not at 2 AM when a + subscriber prints -273.15°C because of a scale mismatch. +- **Adding a third field is safe.** Appending `optional int32 cell_voltage = 3;` to the message + does not break existing publishers or subscribers — protobuf skips unknown fields at deserialisation. + +The `up-bms-proto` crate wraps this proto file in a Cargo crate: + +```rust +// build.rs +fn main() -> Result<(), Box> { + protobuf_codegen::Codegen::new() + .protoc() + .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap()) + .include("proto") + .input("proto/bms_telemetry.proto") + .cargo_out_dir("gen") + .run_from_script(); + Ok(()) +} +``` +It also exports demo URI constants (authority aligned with Phase 1; the socket path lives in +`up-frame-codec`, not here): + +```rust +// up-bms-proto/src/lib.rs +pub mod constants { + pub const AUTHORITY_NAME: &str = "my_own_car"; + pub const PUBLISHER_UE_ID: u32 = 0x1010; + pub const PUBLISHER_UE_VERSION: u8 = 0x01; + pub const BATTERY_TELEMETRY_RESOURCE_ID: u16 = 0x8001; + pub const EXPECTED_MESSAGE_COUNT: u32 = 5; +} +``` +Both the publisher and subscriber depend on `up-bms-proto`. There is no more DBC secret — the +`.proto` file is the **contract**. + +---- + +A quick note on what this *does not* solve: protobuf gives us a typed data contract, but the +URI conventions (`PUBLISHER_UE_ID = 0x1010`, `BATTERY_TELEMETRY_RESOURCE_ID = 0x8001`) and the +socket path from `up_frame_codec::socket_path()` (`{cwd}/tmp/uprotocol_twin.sock`) are still +shared out of band. Those will move to configuration or discovery in later phases. For now, the +constants crate plus the shared frame-codec helpers keep the demo honest. + +### Chapter 5: What does the Phase-1 subscriber *actually* do? + +Before we look at `up-unix-domain-socket-transport`, let's visit Phase-1 subscriber code +and trace what it does for every incoming message. The key section here: + +```rust +// Phase-1 subscriber (simplified accept loop) +loop { + let (mut stream, _) = listener.accept().await?; + tokio::spawn(async move { + // Step 1 — read the 4-byte length prefix + let mut len_bytes = [0u8; 4]; + if stream.read_exact(&mut len_bytes).await.is_err() { return; } + let body_len = u32::from_be_bytes(len_bytes) as usize; + + // Step 2 — read that many bytes + let mut body_bytes = vec![0u8; body_len]; + if stream.read_exact(&mut body_bytes).await.is_err() { return; } + + // Step 3 — deserialise the protobuf UMessage + let msg = UMessage::parse_from_bytes(&body_bytes[..]).ok()?; + + // Step 4 — extract the CAN payload and interpret + let payload = msg.payload?; + let (pct, temp) = unpack_bms_can_frame(&payload); + println!("-> State of Charge: {:.1}%", pct); + println!("-> Cell Temp: {} °C", temp); + }); +} +``` +Let's count the implicit responsibilities in that one spawned closure: + +| Step | Responsibility | Who should own it | +|---|---|---| +| 1–2 | Read framed bytes from a Unix socket | **Transport layer** — the socket code | +| 3 | Deserialise a protobuf `UMessage` | **Transport layer** — `read_framed_message` | +| 4 | Extract the typed payload and print | **Application** — the subscriber | + +Three responsibilities. Two of them have nothing to do with battery telemetry. + +If we wanted to add a second consumer — say, a thermal management logging engine — we would have +to copy the socket-reading code. If we wanted to switch from Unix Domain Socket to another +transport - say, Zenoh - we would rewrite the socket-reading code. If we wanted to run two +listeners inside the same process (one for SoC, one for temperature), we would need to duplicate the accept loop or build our own dispatch table. + +**This is where a transport crate enters the story.** + +The transport crate's job is to own steps 1–3 so the application code (step 4) does not need to +know about sockets, length prefixes, or protobuf envelope parsing. uProtocol gives us a trait +for this: + +``` +╔═══════════════════════════════════════════════╗ +║ UTransport (trait) ║ +╠═══════════════════════════════════════════════╣ +║ send(message) ║ +║ register_listener(filter, listener) ║ +║ unregister_listener(filter, listener) ║ +╚═══════════════════════════════════════════════╝ +``` +`UTransport` says: "I know how to move a `UMessage` from point A to point B. I know how to +accept registrations from interested parties. I know when to call those parties. We tell it +_who_ is interested and _what_ we want to send; the transport handles the rest." + +This is not an abstraction for the joy of abstraction. It is a **responsibility boundary**. +The transport code lives in `up-unix-domain-socket-transport`. The subscriber code lives in +`up-telemetry-subscriber`. They depend on each other through the `UTransport` + `UListener` +traits, not through a shared socket path embedded in application code. + +In the same way, the publisher should not know about socket I/O either. The publisher says +"publish this data"; the *transport* says "I will frame it, connect to the socket, and write +it." Both sides use the **same** type — `UnixDomainSocketTransport` — with `connect` on the +publisher and `bind` on the subscriber. That is wire setup, not a Client/Server split in the +uProtocol model. + +Let's look at `up-unix-domain-socket-transport` carefully. + +---- + +### Chapter 6: `up-unix-domain-socket-transport` — the transport crate + +`up-unix-domain-socket-transport` lives at +`phases/02_uprotocol_semantics/crates/up-unix-domain-socket-transport/`. It exposes **one** +`UTransport` type: + +**`UnixDomainSocketTransport::bind`** — binds a socket path, spawns an accept loop, reads framed +messages, and dispatches to registered listeners: + +```rust +let socket_path = up_frame_codec::ensure_socket_dir()?; +let transport = UnixDomainSocketTransport::bind(&socket_path).await?; +transport + .register_listener(&source_filter, None, listener) + .await?; +``` +The dispatch logic (simplified from the crate): + +```rust +for registered in listeners.iter() { + if registered.matches_msg(&message) { + registered.on_receive(message.clone()).await; + } +} +``` +Where `matches_msg` uses `up-rust`'s `UUri::matches` — the same URI matching rules that +`LocalTransport` uses. A listener fires when the source filter matches the message's source URI. + +**`UnixDomainSocketTransport::connect`** — send-only attachment: connect per `send`, frame via +`up-frame-codec`, write bytes: + +```rust +let socket_path = up_frame_codec::socket_path()?; +let transport = UnixDomainSocketTransport::connect(&socket_path); +transport.send(message).await?; +``` +`register_listener` after `connect` returns `UNIMPLEMENTED` — listeners live on the bind side. + +#### How does this compare to Phase-1? + +Phase-1's subscriber was doing the transport layer's job. It read raw bytes from a socket, +decoded them, and interpreted the payload — all in one spawned task. The transport crate now +owns the read-decode-dispatch pipeline. The subscriber only implements `UListener::on_receive`. + +``` +Phase-1: socket → read_exact(4) → read_exact(N) → parse → application logic +Phase-2: socket → [UnixDomainSocketTransport] → filter match → UListener::on_receive + ↑ + (centralised, reusable) +``` +---- + +### Chapter 7: Refactored publisher — `SimplePublisher` + L2 `UPayload` + +Phase 1 put raw bytes into the envelope with `UMessageBuilder::build_with_payload`. Phase 2 +introduces **`UPayload` as an L2 handle**: bytes plus format hint that `SimplePublisher` folds +into `UAttributes` / `UMessage`. L1 still only sees `UMessage`; applications talk L2 with +`UPayload::try_from_protobuf(...)`. + +Let's look at the publisher code. Compare it with the Phase-1 version from the previous chapter. + +```rust +// up-battery-telemetry-publisher/src/main.rs (Phase 2) + +use up_bms_proto::constants::*; +use up_bms_proto::BatteryTelemetry; +use up_rust::communication::{CallOptions, Publisher, SimplePublisher, UPayload}; +use up_rust::{StaticUriProvider, UTransport}; +use up_unix_domain_socket_transport::UnixDomainSocketTransport; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + env_logger::init(); + println!("--- Battery telemetry publisher starting ---"); + + let uri_provider = Arc::new(StaticUriProvider::new( + AUTHORITY_NAME, + PUBLISHER_UE_ID, + PUBLISHER_UE_VERSION, + )); + let socket_path = up_frame_codec::socket_path()?; + let transport: Arc = + UnixDomainSocketTransport::connect(&socket_path); + let publisher = SimplePublisher::new(transport, uri_provider); + + let mut rng = rand::rng(); + + for i in 1..=EXPECTED_MESSAGE_COUNT { + let telemetry = BatteryTelemetry { + soc_percent: rng.random_range(75.0..78.9), + temp_celsius: rng.random_range(20..=25), + ..Default::default() + }; + + println!("Message {}: SoC = {:.1}%, Temp = {}°C", + i, telemetry.soc_percent, telemetry.temp_celsius); + + // L2: typed protobuf + format → SimplePublisher builds the UMessage + let payload = UPayload::try_from_protobuf(telemetry)?; + publisher + .publish( + BATTERY_TELEMETRY_RESOURCE_ID, + // Same 5 s usefulness window as Phase 1's with_ttl(5000): + // the *application* still chooses TTL; CallOptions carries it. + CallOptions::for_publish(Some(5000), None, None), + Some(payload), + ) + .await + .map_err(|err| anyhow::anyhow!("publish failed: {err}"))?; + println!(); + } + Ok(()) +} +``` + +**Where did `with_ttl(5000)` go?** It did not disappear. Phase 1 set TTL on the builder +(`UMessageBuilder::publish(...).with_ttl(5000)`). Phase 2 still has the application choose that +value; it passes it through L2 as `CallOptions::for_publish(Some(5000), ...)`. +`SimplePublisher` copies options into `UAttributes.ttl` when it builds the `UMessage` — it does +not invent a default TTL for us. Omit `Some(5000)` (pass `None`) and the published attributes +have no TTL. + +#### What did we manage to get rid of? + +| Phase-1 code | Phase-2 equivalent | +|---|---| +| `UUri::try_from_parts(...)` | `StaticUriProvider::new(`
`authority, ue_id, version)` | +| `UMessageBuilder::publish(...)`
`.with_ttl(...)`
`.build_with_payload(raw, RAW)` | `SimplePublisher::publish`
+ `CallOptions` (TTL, …)
+ `UPayload` | +| RAW CAN `pack_bms_can_frame` | `UPayload::try_from_protobuf(`
`telemetry)` | +| `serialize_for_unix_socket`
+ `UnixStream::connect`
+ `write_all` | `UnixDomainSocketTransport`
`::connect` → `UTransport::send` | + +**We no longer create `UMessage` by hand.** The `SimplePublisher` (from `up-rust`'s L2 helpers) +assembles the envelope from three application-supplied pieces: source URI (`StaticUriProvider`), +options such as TTL (`CallOptions`), and payload bytes + format (`UPayload`). Message type +`UMESSAGE_TYPE_PUBLISH` and a fresh message ID are filled in by the helper. + +**We no longer pack CAN bytes.** `UPayload::try_from_protobuf(telemetry)` serialises the +`BatteryTelemetry` protobuf message and wraps it with format hint +`UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY`. No DBC offsets, no bit-shifting. + +**We no longer connect and write to the socket.** That is inside +`UnixDomainSocketTransport::send` (after `connect`), which implements `UTransport::send`. The +publisher only says "publish this data"; the how of moving bytes lives in the transport layer. + +---- + +### Chapter 8: Refactored subscriber — `UListener` + `UnixDomainSocketTransport::bind` + +Now the subscriber. This is a bigger change. The entire socket loop is gone. + +``` +╔════════════════════════════════╗ +║ UListener (trait) ║ +╠════════════════════════════════╣ +║ async fn on_receive( ║ +║ &self, ║ +║ msg: UMessage ║ +║ ) ║ +╚════════════════════════════════╝ +``` +We implement this trait: + +```rust +struct BatteryTelemetryListener { + received: Arc, + shutdown: Arc, +} + +#[async_trait] +impl UListener for BatteryTelemetryListener { + async fn on_receive(&self, msg: UMessage) { + match msg.extract_protobuf::() { + Ok(telemetry) => { + let count = self.received.fetch_add(1, Ordering::SeqCst) + 1; + println!( + "[Battery telemetry subscriber] Processing incoming telemetry...\n\ + -> State of Charge: {:.1}%\n\ + -> Cell Temp: {} °C", + telemetry.soc_percent, telemetry.temp_celsius, + ); + if count >= EXPECTED_MESSAGE_COUNT { + self.shutdown.notify_one(); + } + } + Err(err) => eprintln!("Failed to decode BatteryTelemetry payload: {err}"), + } + } +} +``` +And wire it into `main`: + +```rust +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + env_logger::init(); + + let uri_provider = StaticUriProvider::new( + AUTHORITY_NAME, + PUBLISHER_UE_ID, + PUBLISHER_UE_VERSION, + ); + let source_filter = uri_provider.get_resource_uri(BATTERY_TELEMETRY_RESOURCE_ID); + + let received = Arc::new(AtomicU32::new(0)); + let shutdown = Arc::new(Notify::new()); + let listener = Arc::new(BatteryTelemetryListener { + received, + shutdown: shutdown.clone(), + }); + + let socket_path = up_frame_codec::ensure_socket_dir()?; + let transport = UnixDomainSocketTransport::bind(&socket_path).await?; + transport + .register_listener(&source_filter, None, listener) + .await?; + + println!( + "Battery telemetry subscriber listening on: {} (expecting {} messages)", + socket_path.display(), + EXPECTED_MESSAGE_COUNT + ); + + shutdown.notified().await; + println!("Received {EXPECTED_MESSAGE_COUNT} messages — exiting."); + Ok(()) +} +``` +#### What changed from Phase-1? + +Every line related to socket I/O is gone. In its place: + +- `UnixDomainSocketTransport::bind(&socket_path)` — binds the socket and runs the accept loop +- `register_listener(&source_filter, None, listener)` — declares interest: "I want messages from + resource 0x8001 from entity 0x1010" +- `UListener::on_receive` — receives an already-decoded `UMessage` with a typed protobuf payload + +**The subscriber no longer reads raw bytes.** It does not know about length prefixes, protobuf +envelope decoding, or `read_exact`. It receives a `UMessage` and calls `extract_protobuf` to get +the typed `BatteryTelemetry` struct. + +**Dispatch is declarative.** The transport crate's dispatch iterates registered listeners and +calls only those whose filters match. Phase-1's subscriber accepted every byte that arrived on +the socket. Phase-2's subscriber says "only nudge me for resource 0x8001 from the battery entity." + +---- + +### Chapter 9: Build and run + +```shell +# From the repo root +cargo build --manifest-path phases/02_uprotocol_semantics/Cargo.toml + +# Terminal 1 — subscriber +cargo run --manifest-path phases/02_uprotocol_semantics/Cargo.toml -p up-telemetry-subscriber + +# Terminal 2 — publisher +cargo run --manifest-path phases/02_uprotocol_semantics/Cargo.toml -p up-battery-telemetry-publisher +``` +Expected output (subscriber): + +``` +Battery telemetry subscriber listening on: .../tmp/uprotocol_twin.sock (expecting 5 messages) +[Battery telemetry subscriber] Processing incoming telemetry... +-> State of Charge: 76.3% +-> Cell Temp: 22 °C +[Battery telemetry subscriber] Processing incoming telemetry... +-> State of Charge: 77.8% +-> Cell Temp: 24 °C +... (5 messages total) +Received 5 messages — exiting. +``` +---- + +### Chapter 10: Looking back at what we have done + +Let's look back at the architecture: + +``` +╔═════════════════════════════════════════════════════════════╗ +║ Publisher (Phase 2) ║ +║ ║ +║ BatteryTelemetry { soc_percent, temp_celsius } ║ +║ ↓ ║ +║ UPayload::try_from_protobuf (L2) ║ +║ ↓ ║ +║ SimplePublisher::publish(resource_id, CallOptions, ...) ║ +║ ↓ ║ +║ UnixDomainSocketTransport::connect → send ║ +║ ↓ ║ +║ Unix Domain Socket ║ +╚═════════════════════════════════════════════════════════════╝ + +╔═════════════════════════════════════════════════════════════╗ +║ Subscriber (Phase 2) ║ +║ ║ +║ Unix Domain Socket ║ +║ ↓ ║ +║ UnixDomainSocketTransport::bind → dispatch ║ +║ ↓ ║ +║ filter match → UListener::on_receive(msg) ║ +║ ↓ ║ +║ msg.extract_protobuf::() ║ +║ ↓ ║ +║ SoC: 76.3%, Temp: 22°C ║ +╚═════════════════════════════════════════════════════════════╝ +``` +The publisher does not know about sockets. The subscriber does not know about sockets. The +transport crate owns the wire; the application code owns the business logic. + +**This is the uProtocol promise:** separate _what_ we want to happen (publish this telemetry, +listen for that resource) from _how_ bytes move (Unix Domain Socket, Zenoh, MQTT, ...). + +---- + +### Chapter 11: Where Unix Domain Sockets still fall short + +The refactor buys us cleaner APIs and production-parity interfaces. But the **execution path** +is still bottlenecked by a Unix Domain Socket. Let's be honest about this. + +#### Limitation 1 — Point-to-point Unix Domain Socket (no fan-out) + +`UnixDomainSocketTransport::bind` accepts a connection, reads one message, and dispatches to +listeners **inside that process**. A second process (say, a thermal management logging engine) +cannot independently subscribe to the same battery telemetry stream. The socket is consumed by +our subscriber's accept loop. + +Phase 1's problem — "how do we add a second consumer?" — is **unresolved**. The URI filters help +within one process, but they do not clone bytes to arbitrary peers on the network. + +``` +Phase-1 wall: Phase-2: +1 publisher → 1 subscriber Same socket, same topology + UTransport + URI filters + + listener dispatch in-process + (still one process, one socket) + Thermal engine still cannot tap the stream +``` +#### Limitation 2 — Filesystem socket path (local only) + +```text +{cwd}/tmp/uprotocol_twin.sock +``` +This path exists only on one machine. Move the battery uEntity to a Zone ECU and the display to +a central compute node, and Unix Domain Socket breaks — there is no cross-machine Unix socket. + +#### Limitation 3 — No location transparency + +Both sides must agree on a literal path string and be on the same machine. There is +no way to spin up a subscriber on a different ECU without changing the transport entirely. + +#### Limitation 4 — No vehicle-wide discovery + +`register_listener` is a local, in-process registration on the bind-side transport. It is not +vehicle-wide topic discovery. A new uEntity cannot find our battery telemetry event without +out-of-band configuration. (Phase 3 will touch topology; L3 services such as uDiscovery are a +later story.) + +---- + +### Chapter 12: Looking ahead — Phase 3 + +The four limitations above trace to one root cause: Unix Domain Socket is a local, point-to-point transport. +uProtocol's trait-based design lets us swap it without touching publisher or subscriber +business logic. + +#### What Phase 3 will bring + +| Aspect | Phase-2 (keep) | Phase-3 (replace wire) | +|---|---|---| +| Envelope | `UMessage`, `UAttributes` (UUri inside) | Unchanged | +| L2 patterns | `SimplePublisher`, `CallOptions`, `UPayload` | Unchanged | +| L1 API | `UTransport::send`, `register_listener`, `UListener::on_receive` | **Same trait** — different plugin | +| Physical transport | Unix Domain Socket + length framing | Zenoh (network-transparent) | +| Multi-subscriber | Blocked by one socket accept loop | Native pub/sub fan-out on the same transport | +| Demo scope | One battery subscriber | Thermal engine + fan-out payoff | + +The key insight: **Phase-3 replaces transport execution; Phase-2 semantics stay.** We are +not relearning uProtocol in Phase-3 — we are unblocking topology. + +---- + +### Appendix: Key takeaways + +1. **L1 (`UTransport` / `UListener`) separates message moving from message handling.** + Our business logic should implement `on_receive`, not `read_exact`. One type — + `UnixDomainSocketTransport` — with `connect` / `bind` for wire setup. + +2. **L2 (`SimplePublisher` / `CallOptions` / `UPayload`) separates intent from envelope construction.** + We say "publish" with a typed `UPayload` and options — the library fills in + `UMESSAGE_TYPE_PUBLISH` and message ID. **TTL stays the application's choice**, passed via + `CallOptions::for_publish(Some(5000), ...)` (same 5 s window as Phase 1's `with_ttl(5000)`). + Source URI comes from `StaticUriProvider`; payload format from `UPayload`. + +3. **Typed protobuf payloads make the data contract explicit.** + The `.proto` file is the source of truth, not a DBC offset comment. + +4. **The Unix Domain Socket wire stays the same; the semantics around it change.** + Phase-3 swaps the wire (Zenoh) but keeps these same L1/L2 APIs. That is the uProtocol + promise — code to the trait, not the transport. + +---- + +### Appendix B: Protobuf schema (bms_telemetry.proto) + +The `up-bms-proto` crate in Phase-2 defines the payload contract using protobuf. +This replaces the ad-hoc `pack_bms_can_frame` used in Phase-1 with a schema that +both publisher and subscriber derive code from. + +```protobuf +syntax = "proto3"; +package tutorial.bms.v1; + +// Battery management telemetry published by the demo publisher. +// SoC and cell temperature only. +message BatteryTelemetry { + float soc_percent = 1; + int32 temp_celsius = 2; +} +``` +The publisher creates a `BatteryTelemetry` protobuf message and wraps it with +`UPayload::try_from_protobuf`. The subscriber uses `extract_protobuf` — the `.proto` file is +the single source of truth. + +---- + diff --git a/tutorial/tutorial-text/Tutorial-Phase-3.md b/tutorial/tutorial-text/Tutorial-Phase-3.md new file mode 100644 index 00000000..874fc64c --- /dev/null +++ b/tutorial/tutorial-text/Tutorial-Phase-3.md @@ -0,0 +1,466 @@ +### Phase 3: The Scaling Limit & The Zenoh Payoff + +> **Prerequisites:** Phase 2 (`tutorial-text/Tutorial-Phase-2.md`, code in `phases/02_uprotocol_semantics/`). +> **Active code:** `phases/03_zenoh_topology/` — copy-forward from Phase 2. + +Phase 2 ended with an honest confession: Unix Domain Socket works for one publisher and one subscriber on a single Linux host, but it breaks the moment we need a second consumer — even on the same machine. Phase 3 swaps the wire for **Zenoh** and delivers the fan-out payoff that Phase 1's thermal logger narrative promised. + +**Demo scope:** All processes run on **one Linux host** (separate terminals), same as Phases 1–2. The cross-ECU vehicle story is narrative only — we do not simulate multiple machines. + +Let's dive in. + +--- + +### Chapter 1: Why Unix Domain Sockets had to retire (recap) + +Phase 2's `up-unix-domain-socket-transport` solved the right problem: separating business logic from socket I/O using `UTransport`. But the Unix Domain Socket **wire** still fails on one host when we need: + +1. **Fan-out** — two subscriber *processes* cannot both receive the same stream (point-to-point; one bind owner). +2. **A logical address** — `{cwd}/tmp/uprotocol_twin.sock` is a filename, not a URI; only one process can bind it. +3. **Location transparency** — publisher and subscriber must share a kernel. + +These limits bite us on a single laptop, which is why Phase 3 retires `up-unix-domain-socket-transport` even before we mention cross-ECU: + +``` +Same Linux host — Phase 2 Unix Domain Socket (still broken for fan-out) + + up-battery-telemetry-publisher up-telemetry-subscriber + │ ▲ + │ one Unix Domain Socket connection │ UnixDomainSocketTransport::bind + └──────────────► {cwd}/tmp/...sock ────┘ (one bind-side process) + + up-thermal-logging-subscriber (what we want in Phase 3) + │ + └── cannot share the stream — second process has no socket to read +``` + +**We do not need a second machine to hit this wall.** Phase 1 introduced the thermal logging process as a second consumer; Phase 2 made URI interest declarative but did not deliver bytes to a second process. Zenoh's native pub/sub gives each process its own subscription without a manual forward loop. + +> We are not exploring multiple Zone ECUs in this tutorial, intentionally. Networked Zone ECUs behave like apps on different Linux hosts — the same fan-out problem remains. + +--- + +### Chapter 2: Zenoh on the same layer map + +[Zenoh](https://zenoh.io/) is a data-space transport: publish/subscribe with location transparency and native fan-out. Eclipse uProtocol provides [`up-transport-zenoh`](https://github.com/eclipse-uprotocol/up-transport-zenoh-rust) — a `UTransport` implementation backed by Zenoh. + +Phase 2 introduced the **canonical layer map**. Phase 3 keeps every band above the wire; we swap only the L1 plugin and the wire. Crates under `phases/03_zenoh_topology/` still pin `up-rust = "0.9.0"`. + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Application / uEntity │ +│ publisher · battery subscriber · thermal subscriber │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ L3 — Application services (uSubscription, uDiscovery, …) │ +│ Not used in this demo — same authority (`my_own_car`) + same transport │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ L2 — Communication │ +│ SimplePublisher · CallOptions · UPayload (unchanged from Phase 2) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ L1 — Transport │ +│ UTransport · UListener · UPTransportZenoh (plugin swap) │ +│ send · register_listener │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Envelope (every message) │ +│ UMessage · UAttributes (incl. source/sink UUri, payload format, …) │ +│ (UUri is a field inside UAttributes — not a separate metadata layer) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Wire │ +│ Zenoh data space (peer mode — see Chapter 6) │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +Layer specs (same as Phase 2): [uP-L1](https://github.com/eclipse-uprotocol/up-spec/tree/main/up-l1), +[uP-L2](https://github.com/eclipse-uprotocol/up-spec/tree/main/up-l2), +[uP-L3](https://github.com/eclipse-uprotocol/up-spec/tree/main/up-l3). + +``` +Phase 2 — Unix Domain Socket (one process owns the socket) + + Publisher ───► UnixDomainSocketTransport::bind ───► one subscriber + + +Phase 3 — Zenoh (data space, no single-owner socket) + + Publisher ───► Zenoh data space ──┬──► subscriber 1 (battery telemetry) + └──► subscriber 2 (thermal logging) +``` + +What the wire swap buys us (same L1/L2 APIs): + +- **Fan-out** — N processes subscribe independently; no shared socket. +- **Address** — URI-derived Zenoh keys, not a filesystem path. +- **Listener ownership** — each process registers its own `UListener` via `UPTransportZenoh`. +- **Framing** — Zenoh owns message boundaries (`up-frame-codec` retires). +- **Cross-host readiness** — same APIs; this demo still runs on one host. + +Every process opens a Zenoh session with the default **peer** config. How peers find each other — and why this demo does not run `zenohd` — is explained in Chapter 6. + +#### What Zenoh is not + +- **Not a replacement for uProtocol** — it is the *wire plugin* for L1. +- **Not a COVESA deep dive** — we use Zenoh as a data-space transport only. +- **Not a multi-host requirement** — the Phase 3 demo stays on one Linux host. +- **Not a mandatory broker** — see Chapter 6 (peer scouting vs `zenohd`). +- **Not L3 uSubscription** — same-transport fan-out stays on L1 `register_listener` (Chapter 5). + +--- + +### Chapter 3: What uProtocol puts alongside the actual message + +Before the code swap, recall what travels *with* each message. Zenoh uses parts of the envelope in a way Unix Domain Socket could not. + +#### One metadata level: `UAttributes` + +uProtocol has **one** metadata level on the wire: **`UAttributes`**. They hold everything needed for routing and for describing the payload — message type, TTL, priority, source/sink `UUri`, correlation IDs, and so on. + +When we use the L2 API (e.g. `SimplePublisher`), those attributes are **assembled from three sources**: + +- **`LocalUriProvider` / `StaticUriProvider`** — source `UUri` (authority, entity, version, resource) +- **`CallOptions`** — TTL, priority, … +- **`UPayload`** — payload format hint (plus the application bytes) + +```rust +publisher.publish( + BATTERY_TELEMETRY_RESOURCE_ID, + CallOptions::for_publish(Some(5000), None, None), // app still chooses TTL + Some(payload), +) +``` + +`SimplePublisher` builds a `UMessage` whose `UAttributes` combine those pieces (type=`PUBLISH`, TTL=5000 ms, source from the URI provider, format from `UPayload`). Conceptually: + +```text +UMessage { + attributes: UAttributes { // ← the single metadata level + type: PUBLISH, + ttl: 5000, + source: UUri { authority: "my_own_car", ue_id: 0x1010, ue_version: 1, resource_id: 0x8001 }, + // … other attribute fields as applicable … + }, + payload: , // format recorded in attributes +} +``` + +`UUri` is not a separate metadata layer beside `UAttributes`. Source and sink are **fields inside** +`UAttributes` — composed into that single metadata level, not peers of it. + +#### Why Unix Domain Sockets did not use this for routing + +In Phase 2, `SimplePublisher` assembled the envelope, then `UnixDomainSocketTransport::send` treated it as **opaque bytes** on a socket path. Listener matching lived only in a local table inside the bind-side process. The `UUri` in `UAttributes` did not route on the wire. + +> **Unix Domain Socket has no routing layer** — one socket, one connection, one direction. + +#### How Zenoh makes the envelope useful + +Zenoh is **content-aware**. When `UPTransportZenoh` receives a `UMessage` via `UTransport::send`: + +1. It **reads the source `UUri`** from `UAttributes` and maps it to a Zenoh key expression (e.g. derived from `my_own_car` / `0x1010` / `1` / `0x8001`). +2. It **publishes** into the Zenoh data space under that key. +3. Each subscriber's `UPTransportZenoh` registers a **Zenoh subscriber** for its `source_filter`. +4. Arriving publications are reconstructed as `UMessage` and delivered to `UListener::on_receive`. + +```text +Zenoh flow (Phase 3) + + Publisher UPTransportZenoh Zenoh data space UPTransportZenoh Subscriber + │ │ │ │ │ + │──UMessage───────────────►│ │ │ │ + │ │──publish(key, …)──────►│──fan-out to matches────►│──reconstruct──────► on_receive + │ │ │──fan-out to thermal────►│──reconstruct──────► on_receive +``` + +**What changed:** the `UUri` inside `UAttributes` is now **first-class routing information**. In Phase 2, matching lived only in a local `HashMap` inside the bind-side process. In Phase 3, matching spans **multiple processes** — and, if we deploy that way, **multiple hosts**. + +--- + +### Chapter 4: Code — swapping Unix Domain Sockets for Zenoh + +The Phase 3 workspace (`phases/03_zenoh_topology/`) copy-forwards `up-bms-proto`, the publisher, and the battery subscriber. It **retires** `up-unix-domain-socket-transport` and `up-frame-codec`, and **adds** `up-transport-zenoh` plus `up-thermal-logging-subscriber`. + +The only application change in publisher and subscribers is **how the `UTransport` is constructed**. L2 (`SimplePublisher`, `CallOptions`, `UPayload`), URI filters, authority `my_own_car`, and the `BatteryTelemetry` schema stay as in Phase 2. + +```rust +// Phase 2 (Unix Domain Socket) — retired +// let transport = UnixDomainSocketTransport::connect(&socket_path); + +// Phase 3 (Zenoh) — same construction on every process +let transport: Arc = Arc::new( + UPTransportZenoh::builder(AUTHORITY_NAME)? + .with_config(zenoh_config::Config::default()) + .build() + .await?, +); +``` + +Everything after that — `SimplePublisher::publish`, `register_listener`, `on_receive` — is unchanged. + +#### Publisher (same loop, Zenoh transport) + +```rust +// up-battery-telemetry-publisher/src/main.rs — Phase 3 +use up_transport_zenoh::{zenoh_config, UPTransportZenoh}; + +let transport: Arc = Arc::new( + UPTransportZenoh::builder(AUTHORITY_NAME)? + .with_config(zenoh_config::Config::default()) + .build() + .await?, +); +let publisher = SimplePublisher::new(transport, uri_provider); + +for i in 1..=EXPECTED_MESSAGE_COUNT { + let telemetry = BatteryTelemetry { /* … */ }; + let payload = UPayload::try_from_protobuf(telemetry)?; + publisher + .publish( + BATTERY_TELEMETRY_RESOURCE_ID, + CallOptions::for_publish(Some(5000), None, None), // TTL still the app's choice + Some(payload), + ) + .await?; +} +``` + +#### Subscriber (same on_receive, Zenoh transport) + +```rust +// up-telemetry-subscriber/src/main.rs — Phase 3 +let transport: Arc = Arc::new( + UPTransportZenoh::builder(AUTHORITY_NAME)? + .with_config(zenoh_config::Config::default()) + .build() + .await?, +); +transport + .register_listener(&source_filter, None, listener) + .await?; +``` + +The URI filter (`source_filter` with resource ID `0x8001`) works identically. Zenoh delivers matching publications to each subscriber process. + +#### Transport config — socket path replaced + +Phase 2 used `{cwd}/tmp/uprotocol_twin.sock`. Phase 3 uses `zenoh_config::Config::default()` — Zenoh **peer** mode with UDP multicast scouting, and no filesystem path. Chapter 6 explains what that mode means and why we do not start a `zenohd` router alongside the three binaries. + +If we wanted an explicit remote endpoint (for example a router or a peer on another host): + +```rust +let mut config = zenoh_config::Config::default(); +config.connect.endpoints = vec!["tcp/192.168.1.100:7447".parse()?]; +``` + +--- + +### Chapter 5: Fan-out payoff — and what is *not* L3 uSubscription + +Phase 1 mentioned the **Thermal Management Logging Application** as a second consumer of the battery telemetry stream. It checks cell temperature and prints a warning if the temperature is over 25°C. + +If we had implemented it on Unix Domain Socket, it would have had to live in the same process as the telemetry subscriber. Phase 3 delivers what Phase 1 only described: an **independent thermal logging subscriber** — a separate application that receives the same protobuf messages from the Zenoh data space. + +#### The thermal subscriber + +```rust +// up-thermal-logging-subscriber/src/main.rs — new in Phase 3 +struct ThermalLoggingListener { /* … */ } + +#[async_trait] +impl UListener for ThermalLoggingListener { + async fn on_receive(&self, msg: UMessage) { + if let Ok(telemetry) = msg.extract_protobuf::() { + let temp = telemetry.temp_celsius; + if temp > 25 { + println!("WARNING — cell temperature {temp}°C exceeds threshold"); + } else { + println!("Cell temperature {temp}°C — OK"); + } + } + } +} + +// main: same UPTransportZenoh::builder + register_listener as the battery subscriber +``` + +This listener extracts the same `BatteryTelemetry` struct, but applies **thermal-specific logic**. No new schema; no socket path; no shared process. + +#### Transport-native pub/sub vs L3 uSubscription + +Phase 2's `register_listener` was a **local** operation on the bind-side `UnixDomainSocketTransport`. Phase 3 still calls `register_listener` — but under Zenoh that interest is a **Zenoh subscription**. When the publisher sends resource `0x8001`, **every process** that registered a matching filter receives the message. That is **Zenoh's native pub/sub** (L1), not a special uProtocol L3 registration step. + +uProtocol also defines an L3 application service named **[uSubscription](https://github.com/eclipse-uprotocol/up-spec/tree/main/up-l3/usubscription)**. That service matters when a +uEntity wants topics from a **different authority**, or when publisher and subscriber sit on +**different transports** (for example Zenoh on one side and MQTT5 on the other) and still need +transparent pub/sub. In those cases, transport-native interest alone is not enough; uSubscription +signals that interest across the uProtocol network. + +**This tutorial does not use uSubscription.** Our publisher and both subscribers share one +authority (`my_own_car`) and one transport (Zenoh), so Zenoh's own pub/sub is sufficient. Fan-out +here is only `UTransport::register_listener` + Zenoh — not an L3 “PUBLISH registration” step. +Look back at the Chapter 2 layer map: the L3 band is present for orientation, and stays idle in this demo. + +--- + +### Chapter 6: Running the multi-subscriber demo + +#### Peer mode, routers, and why this demo skips `zenohd` + +Zenoh can run processes in more than one role. Two that matter for reading our Phase 3 code: + +1. **Peer** — a process that both sends and receives in the Zenoh data space. With `zenoh_config::Config::default()`, each of our binaries opens a **peer** session. Peers can discover one another using Zenoh’s **UDP multicast scouting**: on a network that allows multicast, they find each other without a central process. +2. **Router (`zenohd`)** — a long-running Zenoh process that **relays** traffic and helps clients that cannot (or should not) rely on multicast alone. Typical reasons to introduce a router: peers on different subnets or hosts where multicast does not cross the boundary; locked-down environments that block scouting; larger fleets where a stable rendezvous point is easier to operate than mesh discovery. + +Our demo does **not** start `zenohd`. The publisher, battery subscriber, and thermal subscriber all run on **one Linux host**, all with the default peer config. In that layout, multicast scouting is enough: the three sessions form a small peer mesh and Zenoh delivers publications to every matching `register_listener`. Adding a router would not change the uProtocol lesson of this chapter (fan-out via the same L1/L2 APIs after a wire swap). We mention routers so the absence of `zenohd` is a deliberate choice, not an omission. + +The optional snippet in Chapter 4 (`config.connect.endpoints = …`) is what we would use later if we *did* point at a router or a remote peer — it is not required for the run below. + +#### Commands (subscribers first) + +Start the two subscribers first, then the publisher (three terminals). The publisher sends a short burst and exits; if a subscriber is not yet registered, it can miss messages. + +```bash +# 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) +cargo run --manifest-path phases/03_zenoh_topology/Cargo.toml -p up-thermal-logging-subscriber + +# Terminal 3 — battery publisher (start last) +cargo run --manifest-path phases/03_zenoh_topology/Cargo.toml -p up-battery-telemetry-publisher +``` + +The publisher sends five messages (same as Phases 1–2). Both subscribers should receive all five. No process shares a socket, a listener table, or a filesystem path. + +Build the whole workspace: + +```bash +cargo build --manifest-path phases/03_zenoh_topology/Cargo.toml +``` + +--- + +### Chapter 7: A look at what we have done + +``` +╔═════════════════════════════════════════════════════════════╗ +║ Publisher (Phase 3) ║ +║ ║ +║ BatteryTelemetry { soc_percent, temp_celsius } ║ +║ ↓ ║ +║ UPayload::try_from_protobuf (L2) ║ +║ ↓ ║ +║ SimplePublisher::publish(resource_id, CallOptions, ...) ║ +║ ↓ ║ +║ UPTransportZenoh::send → Zenoh data space ║ +╚═════════════════════════════════════════════════════════════╝ + +╔═════════════════════════════════════════════════════════════╗ +║ Battery Subscriber (Phase 3) ║ +║ ║ +║ Zenoh data space → UPTransportZenoh ║ +║ ↓ ║ +║ filter match → UListener::on_receive(msg) ║ +║ ↓ ║ +║ msg.extract_protobuf::() ║ +║ ↓ ║ +║ SoC: 76.3%, Temp: 22°C ║ +╚═════════════════════════════════════════════════════════════╝ + +╔═════════════════════════════════════════════════════════════╗ +║ Thermal Subscriber (Phase 3 — new) ║ +║ ║ +║ Zenoh data space → UPTransportZenoh ║ +║ ↓ ║ +║ filter match → UListener::on_receive(msg) ║ +║ ↓ ║ +║ msg.extract_protobuf::() ║ +║ ↓ ║ +║ temp > 25°C? → WARNING or temp OK ║ +╚═════════════════════════════════════════════════════════════╝ +``` + +Notice the shape: both subscribers are **structurally identical**. They differ only in what they *do* with the telemetry data. That is the payoff. + +--- + +### Chapter 8: What we learned + +- **Zenoh replaces Unix Domain Sockets as the L1 transport** — same uProtocol API, different plugin. +- **Fan-out works on one host** — the thermal logger attaches without touching the battery subscriber. +- **Business logic survived the swap** — `SimplePublisher`, `UListener`, and `BatteryTelemetry` are unchanged. +- **`UAttributes` is the single metadata level** — L2 assembles it from URI provider + `CallOptions` + `UPayload` format; Zenoh uses the embedded `UUri` for keys. +- **Same-transport fan-out is Zenoh native pub/sub** — not L3 uSubscription (that service is for cross-authority / cross-transport interest). +- **Zenoh peer mode on one host — no `zenohd`** — Chapter 6: peers scout via UDP multicast; a router would not change this chapter’s fan-out lesson. +- **Phase 2 limits are resolved** — fan-out, address, listener ownership, framing, and cross-host readiness. + +Phase 3 proves that uProtocol's layer design pays off: **swap the wire, keep the application**. + +--- + +### Chapter 9: Where we still fall short + +Phase 3 resolves the fan-out problem that Phase 1 identified and Phase 2 documented. Still open: + +- **No L3 [uDiscovery](https://github.com/eclipse-uprotocol/up-spec/tree/main/up-l3/udiscovery)** — listeners are URI-filtered, but there is no registry of "who publishes what" (the service exists in the architecture; this tutorial has no implementation). +- **Hardcoded constants** — `AUTHORITY_NAME`, entity/resource IDs live in `up-bms-proto::constants`. +- **Single-topic demo** — only `BatteryTelemetry` (resource `0x8001`). +- **Cross-transport pub/sub** — would involve L3 **uSubscription**, unused while all peers share Zenoh. + +These are configuration / later-service gaps, not a failure of the L1/L2 design we already have. + +--- + +### Appendix: Key takeaways + +1. **L1 (`UTransport` / `UListener`) absorbs the transport swap.** The binaries changed the transport construction block. The business logic (publish loop, `on_receive`) is untouched. + +2. **Zenoh's data space enables fan-out where Unix Domain Socket could not.** A second subscriber attaches by running a new binary and registering the same URI filter — no socket sharing, no `SOCKET_PATH`. Chapter 6 covers peer mode vs `zenohd`. + +3. **One metadata level: `UAttributes`.** Assembled at L2 from `LocalUriProvider` / `StaticUriProvider`, `CallOptions`, and `UPayload` format. Zenoh turns the source `UUri` into a key expression. + +4. **Phase 1's thermal logger finally gets its own process.** Three phases of narrative resolve in one extra `cargo run`. + +--- + +### Appendix B: Workspace diff — Phase 2 vs Phase 3 + +``` +phases/02_uprotocol_semantics/ phases/03_zenoh_topology/ +(frozen) (active — Phase 3) +───────────────────────────── ───────────────────────────── +up-bms-proto ──► up-bms-proto (copy-forward) +up-battery-telemetry-publisher ──► up-battery-telemetry-publisher +up-telemetry-subscriber ──► up-telemetry-subscriber +up-unix-domain-socket-transport ──X (retired) +up-frame-codec ──X (retired) + up-thermal-logging-subscriber (new) + + dependency: up-transport-zenoh +``` + +**Nothing was deleted from the repo** — Phase 2 code remains in `phases/02_uprotocol_semantics/` for side-by-side study. + +--- + +### Appendix C: Protobuf schema (bms_telemetry.proto) + +Identical to Phase 2. The `.proto` file is unchanged because the payload contract has not changed: + +```protobuf +syntax = "proto3"; + +package tutorial.bms.v1; + +message BatteryTelemetry { + float soc_percent = 1; + int32 temp_celsius = 2; +} +``` + +The publisher still creates `BatteryTelemetry`, wraps it with `UPayload::try_from_protobuf`, and publishes. Subscribers still `extract_protobuf`. Zenoh routes by `UUri`, not by schema. + +--- + +### Appendix D: References + +- [Zenoh documentation](https://zenoh.io/docs/) — data-space transport overview +- [Eclipse uProtocol — up-transport-zenoh-rust](https://github.com/eclipse-uprotocol/up-transport-zenoh-rust) +- [Vehicle Signal Specification (VSS)](https://covesa.github.io/vehicle_signal_specification/) — COVESA standard for vehicle data modelling From 6c4cc6c7a2c115fb853f72ed850162a58180699a Mon Sep 17 00:00:00 2001 From: Nirmalya Sengupta Date: Tue, 29 Sep 2026 21:51:10 +0530 Subject: [PATCH 2/2] Rewrite the in-tree tutorial around LocalTransport and this checkout of up-rust. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the Zenoh Phase 3 from this crate so the tutorial does not depend on a transport that would cycle-link up-rust. Phases 1–2 stay two-process Unix Domain Sockets; Phase 3 is one-process LocalTransport with two listeners. Path-depend every tutorial workspace on this tree (0.10.0-SNAPSHOT) so CI catches envelope API drift (ProtobufMappable). Add a tutorial build/test job. Document start-order as operational convention, snapshot dispatch before on_receive, and the Phase 2 exit-without-unregister shortcut. Signed-off-by: Nirmalya Sengupta --- .github/workflows/check.yaml | 23 + .gitignore | 1 + README.md | 10 +- tutorial/README.md | 123 +- tutorial/phases/01_raw_sockets/Cargo.lock | 284 +- tutorial/phases/01_raw_sockets/Cargo.toml | 3 + .../up-battery-telemetry-publisher/Cargo.toml | 4 +- .../src/main.rs | 10 +- .../crates/up-frame-codec/Cargo.toml | 3 +- .../crates/up-frame-codec/src/lib.rs | 25 +- .../crates/up-telemetry-subscriber/Cargo.toml | 4 +- .../up-telemetry-subscriber/src/main.rs | 11 +- .../phases/02_uprotocol_semantics/Cargo.lock | 290 +- .../phases/02_uprotocol_semantics/Cargo.toml | 3 + .../up-battery-telemetry-publisher/Cargo.toml | 4 +- .../src/main.rs | 13 +- .../crates/up-frame-codec/Cargo.toml | 3 +- .../crates/up-frame-codec/src/lib.rs | 25 +- .../crates/up-telemetry-subscriber/Cargo.toml | 4 +- .../up-telemetry-subscriber/src/main.rs | 16 +- .../Cargo.toml | 7 +- .../src/lib.rs | 213 +- tutorial/phases/03_local_transport/Cargo.lock | 887 ++++ tutorial/phases/03_local_transport/Cargo.toml | 9 + .../crates/up-bms-local-demo/Cargo.toml | 13 + .../crates/up-bms-local-demo/src/main.rs | 175 + .../crates/up-bms-proto/Cargo.toml | 0 .../crates/up-bms-proto/build.rs | 0 .../up-bms-proto/proto/bms_telemetry.proto | 2 +- .../crates/up-bms-proto/src/lib.rs | 5 +- tutorial/phases/03_zenoh_topology/Cargo.lock | 4482 ----------------- tutorial/phases/03_zenoh_topology/Cargo.toml | 8 - .../up-battery-telemetry-publisher/Cargo.toml | 16 - .../src/main.rs | 75 - .../crates/up-telemetry-subscriber/Cargo.toml | 16 - .../up-telemetry-subscriber/src/main.rs | 98 - .../up-thermal-logging-subscriber/Cargo.toml | 16 - .../up-thermal-logging-subscriber/src/main.rs | 107 - tutorial/tutorial-text/Tutorial-Phase-1.md | 198 +- tutorial/tutorial-text/Tutorial-Phase-2.md | 151 +- tutorial/tutorial-text/Tutorial-Phase-3.md | 433 +- 41 files changed, 2029 insertions(+), 5741 deletions(-) create mode 100644 tutorial/phases/03_local_transport/Cargo.lock create mode 100644 tutorial/phases/03_local_transport/Cargo.toml create mode 100644 tutorial/phases/03_local_transport/crates/up-bms-local-demo/Cargo.toml create mode 100644 tutorial/phases/03_local_transport/crates/up-bms-local-demo/src/main.rs rename tutorial/phases/{03_zenoh_topology => 03_local_transport}/crates/up-bms-proto/Cargo.toml (100%) rename tutorial/phases/{03_zenoh_topology => 03_local_transport}/crates/up-bms-proto/build.rs (100%) rename tutorial/phases/{03_zenoh_topology => 03_local_transport}/crates/up-bms-proto/proto/bms_telemetry.proto (76%) rename tutorial/phases/{03_zenoh_topology => 03_local_transport}/crates/up-bms-proto/src/lib.rs (66%) delete mode 100644 tutorial/phases/03_zenoh_topology/Cargo.lock delete mode 100644 tutorial/phases/03_zenoh_topology/Cargo.toml delete mode 100644 tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/Cargo.toml delete mode 100644 tutorial/phases/03_zenoh_topology/crates/up-battery-telemetry-publisher/src/main.rs delete mode 100644 tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/Cargo.toml delete mode 100644 tutorial/phases/03_zenoh_topology/crates/up-telemetry-subscriber/src/main.rs delete mode 100644 tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/Cargo.toml delete mode 100644 tutorial/phases/03_zenoh_topology/crates/up-thermal-logging-subscriber/src/main.rs diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index ba39da6c..8ee79f43 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -26,6 +26,7 @@ on: - "examples/**" - "src/**" - "tests/**" + - "tutorial/**" - "Cargo.*" - "build.rs" - "deny.toml" @@ -75,6 +76,28 @@ jobs: run: | cargo check --workspace --all-targets --all-features + tutorial: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: "recursive" + persist-credentials: false + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + - name: Build tutorial phases + run: | + cargo build --manifest-path tutorial/phases/01_raw_sockets/Cargo.toml + cargo build --manifest-path tutorial/phases/02_uprotocol_semantics/Cargo.toml + cargo build --manifest-path tutorial/phases/03_local_transport/Cargo.toml + - name: Test tutorial phases + run: | + cargo test --manifest-path tutorial/phases/01_raw_sockets/Cargo.toml -p up-frame-codec + cargo test --manifest-path tutorial/phases/02_uprotocol_semantics/Cargo.toml -p up-frame-codec + cargo test --manifest-path tutorial/phases/02_uprotocol_semantics/Cargo.toml -p up-unix-domain-socket-transport + cargo test --manifest-path tutorial/phases/03_local_transport/Cargo.toml -p up-bms-local-demo + # [impl->req~up-language-ci-linter~1] fmt: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index fd31b4e1..1a6d6b78 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ target/ DASH_SUMMARY.txt DEPS.txt +TUTORIAL_UPROTOCOL_HANDOFF.md diff --git a/README.md b/README.md index 503497fe..4e37da31 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,23 @@ The crate can be used to Covers: - req~up-language-documentation~ --> -The crate needs to be added to the `[dependencies]` section of the `Cargo.toml` file: +The crate needs to be added to the `[dependencies]` section of the `Cargo.toml` file. +The snippet below is an **example** of depending on a **published** release (crates.io). It is +not the dependency used by this repository's tutorial. ```toml [dependencies] up-rust = { version = "0.9" } ``` +The tutorial does not use that pin: it path-depends on this checkout (`0.10.0-SNAPSHOT`). + Most developers will want to use the Communication Level API and its default implementation which are provided by the `communication` module. Please refer to the [examples](./examples/) for inspiration how to use this crate. +## Tutorial + +A three-phase walkthrough (raw Unix Domain Sockets, then L1/L2 on that wire, then `LocalTransport` in one process) lives in [`tutorial/`](./tutorial/). Open [tutorial/README.md](./tutorial/README.md) and **`cd tutorial`** before copying the `cargo` commands. The tutorial crates depend on this checkout via a path, so they move when the library API moves. + ## Building from Source