Task 9: tines-ros2 — pure topic_name escaping (default, tested) + optional rclrs TinesNode (feature ros2)
Adds the ninth fleet-deployment crate. Default build is ROS2-free: topic_name hex-escapes arbitrary tines topics into valid ROS2 names under /tines/ (deterministic, injective) and wires Node<LogStore, BtTransport>. The rclrs-backed self-contained TinesNode lives under #[cfg(feature="ros2")] with an off-by-default ros2 feature, so the crate builds/tests without a ROS2 toolchain. Adds scripts/check-ros2.sh (needs a ROS2 toolchain; not run in CI), registers the crate in workspace members, and passes sh scripts/ci.sh.
This commit is contained in:
Generated
+471
-3
@@ -2,6 +2,151 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ament_rs"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68248386ad9d8f95d809309d97ae10ae5984818a563b30c13c9b44aa41f439da"
|
||||||
|
dependencies = [
|
||||||
|
"itertools",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-channel"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"event-listener 2.5.3",
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-channel"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-executor"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||||
|
dependencies = [
|
||||||
|
"async-task",
|
||||||
|
"concurrent-queue",
|
||||||
|
"fastrand",
|
||||||
|
"futures-lite",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-global-executor"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel 2.5.0",
|
||||||
|
"async-executor",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"blocking",
|
||||||
|
"futures-lite",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-io"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"parking",
|
||||||
|
"polling",
|
||||||
|
"rustix",
|
||||||
|
"slab",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-lock"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener 5.4.2",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-std"
|
||||||
|
version = "1.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel 1.9.0",
|
||||||
|
"async-global-executor",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"gloo-timers",
|
||||||
|
"kv-log-macro",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"once_cell",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream-impl",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream-impl"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-task"
|
||||||
|
version = "4.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-polyfill"
|
name = "atomic-polyfill"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -11,6 +156,12 @@ dependencies = [
|
|||||||
"critical-section",
|
"critical-section",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
@@ -60,6 +211,19 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blocking"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel 2.5.0",
|
||||||
|
"async-task",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"piper",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bluer"
|
name = "bluer"
|
||||||
version = "0.17.4"
|
version = "0.17.4"
|
||||||
@@ -131,6 +295,16 @@ version = "1.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cargo_toml"
|
||||||
|
version = "0.22.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -163,6 +337,15 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "concurrent-queue"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core_detect"
|
name = "core_detect"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -184,6 +367,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -306,6 +495,12 @@ dependencies = [
|
|||||||
"syn 3.0.9",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embedded-io"
|
name = "embedded-io"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -334,6 +529,32 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener"
|
||||||
|
version = "2.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener"
|
||||||
|
version = "5.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
||||||
|
dependencies = [
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener-strategy"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener 5.4.2",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -394,6 +615,19 @@ version = "0.3.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-lite"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -467,6 +701,18 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-timers"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hash32"
|
name = "hash32"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -522,6 +768,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -544,6 +796,15 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -561,6 +822,15 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kv-log-macro"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
@@ -582,6 +852,16 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -602,6 +882,9 @@ name = "log"
|
|||||||
version = "0.4.34"
|
version = "0.4.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
dependencies = [
|
||||||
|
"value-bag",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "macaddr"
|
name = "macaddr"
|
||||||
@@ -664,6 +947,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.13"
|
version = "1.1.13"
|
||||||
@@ -690,12 +979,43 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13bee6c73da26345c729282832b60b0363cf3dd9f4bfd81d8551b7a1c889a113"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "piper"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polling"
|
||||||
|
version = "3.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"hermit-abi",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "postcard"
|
name = "postcard"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -832,12 +1152,54 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rclrs"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "445cc875d7c153a96d865eadc0550d6d2aca6ba58ff9cc677b49b4dc0e08ec1f"
|
||||||
|
dependencies = [
|
||||||
|
"ament_rs",
|
||||||
|
"async-std",
|
||||||
|
"async-stream",
|
||||||
|
"cfg-if",
|
||||||
|
"futures",
|
||||||
|
"futures-lite",
|
||||||
|
"libloading",
|
||||||
|
"ros-env",
|
||||||
|
"rosidl_runtime_rs",
|
||||||
|
"rustflags",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ros-env"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fc92ef7752ec4d0733b0a403cb41babc3f60e78b65d47e9e5f811b32101df4d"
|
||||||
|
dependencies = [
|
||||||
|
"ament_rs",
|
||||||
|
"cargo_toml",
|
||||||
|
"rosidl_runtime_rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rosidl_runtime_rs"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76f1c3a806380f367275768d7fcd71a19d74af71183e304ca96264043399a544"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"rustflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -847,6 +1209,12 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustflags"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a39e0e9135d7a7208ee80aa4e3e4b88f0f5ad7be92153ed70686c38a03db2e63"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.5"
|
version = "1.1.5"
|
||||||
@@ -878,6 +1246,15 @@ dependencies = [
|
|||||||
"wait-timeout",
|
"wait-timeout",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -933,6 +1310,15 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
@@ -1095,6 +1481,16 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tines-ros2"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"rclrs",
|
||||||
|
"tines-bt",
|
||||||
|
"tines-core",
|
||||||
|
"tines-store",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tines-sim"
|
name = "tines-sim"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -1151,6 +1547,30 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.9.12+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
@@ -1167,9 +1587,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
|
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.2+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1178,9 +1598,15 @@ version = "1.1.5+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
@@ -1210,6 +1636,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "value-bag"
|
||||||
|
version = "1.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@@ -1225,6 +1657,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
@@ -1244,6 +1686,17 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.129"
|
version = "0.2.129"
|
||||||
@@ -1276,6 +1729,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1291,6 +1753,12 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["tines-core", "tines-sim", "tines-store", "tines-bt"]
|
members = ["tines-core", "tines-sim", "tines-store", "tines-bt", "tines-ros2"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Build the rclrs-backed ROS2 node for the `tines-ros2` crate.
|
||||||
|
#
|
||||||
|
# This compiles the `ros2` feature, which pulls in `rclrs` and its dependency
|
||||||
|
# on the ROS2 C libraries. It therefore REQUIRES a ROS2 toolchain installed
|
||||||
|
# (a sourced `colcon`/`/opt/ros/<distro>/setup.bash` and `rclrs` on the
|
||||||
|
# registry); it is NOT run in CI.
|
||||||
|
#
|
||||||
|
# The default (ROS2-free) build and tests — including `topic_name` escaping —
|
||||||
|
# are exercised in CI via `cargo test --workspace` and need no ROS2 toolchain.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source /opt/ros/<distro>/setup.bash # any ROS2 distro rclrs supports
|
||||||
|
# ./scripts/check-ros2.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "Building tines-ros2 with the ros2 feature (requires a ROS2 toolchain)..."
|
||||||
|
cargo build -p tines-ros2 --features ros2
|
||||||
|
|
||||||
|
echo "OK: tines-ros2 (ros2 feature) built against ROS2."
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "tines-ros2"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Self-contained ROS2 node for tines: bridges tines topics to ROS2 topics with a /tines/receipts channel for confidence events. rclrs is an optional feature (off by default) so the crate builds without a ROS2 toolchain."
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
documentation = "https://docs.rs/tines-ros2"
|
||||||
|
keywords = ["robotics", "ros2", "gossip", "delay-tolerant", "science::robotics"]
|
||||||
|
categories = ["science::robotics", "network-programming"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# rclrs (the ROS2 client library) is optional and off by default: the crate's
|
||||||
|
# pure tines-side pieces (topic naming, the Node/Store/Transport wiring) build
|
||||||
|
# and are tested without a ROS2 toolchain. Enable `--features ros2` to compile
|
||||||
|
# the rclrs-backed `TinesNode`.
|
||||||
|
default = []
|
||||||
|
ros2 = ["dep:rclrs"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tines-core = { path = "../tines-core", version = "0.3.0" }
|
||||||
|
tines-store = { path = "../tines-store", version = "0.3.0" }
|
||||||
|
tines-bt = { path = "../tines-bt", version = "0.3.0" }
|
||||||
|
rclrs = { version = "0.8", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//! tines-ros2: a ROS2 front-end for the tines content-defined gossip stack.
|
||||||
|
//!
|
||||||
|
//! The crate splits cleanly in two:
|
||||||
|
//!
|
||||||
|
//! * **Pure, ROS2-free** (built by default): [`topic_name`] escapes an
|
||||||
|
//! arbitrary tines topic into a valid ROS2 topic name under `/tines/`, and
|
||||||
|
//! the tines-side wiring composes [`tines_core::Node`],
|
||||||
|
//! [`tines_store::LogStore`], and [`tines_bt::BtTransport`] with no ROS2
|
||||||
|
//! dependency. This is what builds and is tested without a ROS2 toolchain.
|
||||||
|
//!
|
||||||
|
//! * **rclrs-backed** (feature `ros2`): `TinesNode`, a self-contained ROS2 node
|
||||||
|
//! that publishes each tines topic as a ROS2 topic and
|
||||||
|
//! [`crate::RECEIPTS_TOPIC`] for [`tines_core::ConfidenceEvent`]s, and
|
||||||
|
//! subscribes for new observations. Compiled only behind
|
||||||
|
//! `#[cfg(feature = "ros2")]` so the default build never needs rclrs.
|
||||||
|
//!
|
||||||
|
//! See [`check-ros2.sh`](../../scripts/check-ros2.sh) for building the rclrs
|
||||||
|
//! node against a ROS2 toolchain.
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
pub mod topics;
|
||||||
|
|
||||||
|
/// ROS2 topic carrying serialized [`tines_core::ConfidenceEvent`]s (one per
|
||||||
|
/// broadcast a peer confirmed holding). Always the hex-escaped literal below.
|
||||||
|
pub const RECEIPTS_TOPIC: &[u8] = b"/tines/receipts";
|
||||||
|
|
||||||
|
pub use topics::topic_name;
|
||||||
|
|
||||||
|
#[cfg(feature = "ros2")]
|
||||||
|
pub mod node;
|
||||||
|
|
||||||
|
#[cfg(feature = "ros2")]
|
||||||
|
pub use node::TinesNode;
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
//! rclrs-backed ROS2 node for tines (feature `ros2`).
|
||||||
|
//!
|
||||||
|
//! [`TinesNode`] is a self-contained ROS2 node that owns a tines
|
||||||
|
//! [`tines_core::Node`] over a [`tines_store::LogStore`] and a [`tines_bt::BtTransport`].
|
||||||
|
//! It bridges the two stacks in both directions:
|
||||||
|
//!
|
||||||
|
//! * **tines → ROS2**: each distinct live tines topic is published to its
|
||||||
|
//! hex-escaped ROS2 topic (see [`crate::topics::topic_name`]), and every
|
||||||
|
//! [`tines_core::ConfidenceEvent`] the exchange emits is published to
|
||||||
|
//! [`RECEIPTS_TOPIC`].
|
||||||
|
//! * **ROS2 → tines**: inbound messages on a topic are observed back into the
|
||||||
|
//! tines node under the original (decoded) topic.
|
||||||
|
//!
|
||||||
|
//! Topic creation is lazy: a ROS2 publisher/subscriber pair is created the
|
||||||
|
//! first time a topic is seen; the total number of managed topics is bounded by
|
||||||
|
//! [`TinesNode::new`]'s `topic_cap` and by the number of topics actually live
|
||||||
|
//! in the store.
|
||||||
|
//!
|
||||||
|
//! This module is compiled only with `--features ros2`; it requires a ROS2
|
||||||
|
//! toolchain (and `rclrs`) to build. The default, ROS2-free build of the crate
|
||||||
|
//! never touches this code, so the crate is testable without ROS2 installed.
|
||||||
|
//!
|
||||||
|
//! The rclrs calls below follow the rclrs 0.8 API (see the rclrs tutorial):
|
||||||
|
//! the global ROS2 context is set by [`rclrs::init`] (the caller's
|
||||||
|
//! responsibility, done before [`TinesNode::new`]); `Node::new(name, ns)` uses
|
||||||
|
//! it; publishers/subscriptions are created with `create_publisher`/
|
||||||
|
//! `create_subscription`, each taking a `QosProfile`. The exact generic
|
||||||
|
//! arity of those two calls (`<Msg, _>` for a publisher, `<Msg, _, _>` for a
|
||||||
|
//! subscription) and the `QosProfile` builder methods track rclrs 0.8; verify
|
||||||
|
//! against the installed `rclrs` version before relying on them.
|
||||||
|
|
||||||
|
use std::collections::{BTreeSet, HashMap};
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
|
use rclrs::prelude::{Node as RclNode, Publisher, QosProfile, Subscription};
|
||||||
|
use rclrs::std_msgs::msg::String as StrMsg;
|
||||||
|
use rclrs::RclError;
|
||||||
|
|
||||||
|
use tines_bt::{BtTransport, Radio};
|
||||||
|
use tines_core::{
|
||||||
|
Node as TinesNodeCore, NodeId, Observation, ObservationId, Store, Tick, Transport,
|
||||||
|
};
|
||||||
|
use tines_store::LogStore;
|
||||||
|
|
||||||
|
use crate::topics::topic_name;
|
||||||
|
|
||||||
|
/// Inbound (topic bytes, payload) delivered from a subscription callback on the
|
||||||
|
/// rclrs dispatch thread to the node's single owner task.
|
||||||
|
type InboundMsg = (Vec<u8>, Vec<u8>);
|
||||||
|
|
||||||
|
/// ROS2 topic carrying confidence events, the literal `/tines/receipts`
|
||||||
|
/// (hex-escaping is a no-op here since the bytes are already valid ROS2 name
|
||||||
|
/// characters). This is a `&str` because ROS2 publisher/subscription creation
|
||||||
|
/// takes a `&str` topic name, unlike the arbitrary-byte [`crate::RECEIPTS_TOPIC`].
|
||||||
|
const RECEIPTS_TOPIC: &str = "/tines/receipts";
|
||||||
|
|
||||||
|
/// QoS for live tines-topic payloads: best-effort, so a slow consumer never
|
||||||
|
/// stalls the node (sensor-style data is continuously refreshed anyway).
|
||||||
|
fn topic_qos() -> QosProfile {
|
||||||
|
QosProfile::default().best_effort().keep_last(128)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// QoS for receipts: reliable + transient-local, so a late-joining consumer
|
||||||
|
/// still receives the receipts emitted while it was disconnected.
|
||||||
|
fn receipt_qos() -> QosProfile {
|
||||||
|
QosProfile::default()
|
||||||
|
.reliable()
|
||||||
|
.transient_local()
|
||||||
|
.keep_last(256)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A managed ROS2 topic: the tines bytes it maps back to, its publisher, its
|
||||||
|
/// subscription, and the id of the observation last published (to avoid
|
||||||
|
/// re-publishing an unchanged value every poll).
|
||||||
|
struct ManagedTopic {
|
||||||
|
tines_topic: Vec<u8>,
|
||||||
|
publisher: Option<Publisher<StrMsg>>,
|
||||||
|
subscriber: Option<Subscription<StrMsg>>,
|
||||||
|
last_pub: Option<ObservationId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors constructing a [`TinesNode`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TinesNodeError {
|
||||||
|
/// The underlying rclrs call failed (ROS2 context, QoS, topic creation).
|
||||||
|
Rcl(RclError),
|
||||||
|
/// The [`LogStore`] backend rejected the open/initialization.
|
||||||
|
Store(tines_core::StoreError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RclError> for TinesNodeError {
|
||||||
|
fn from(err: RclError) -> Self {
|
||||||
|
TinesNodeError::Rcl(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<tines_core::StoreError> for TinesNodeError {
|
||||||
|
fn from(err: tines_core::StoreError) -> Self {
|
||||||
|
TinesNodeError::Store(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A self-contained ROS2 node bridging tines and ROS2.
|
||||||
|
///
|
||||||
|
/// Construct with [`TinesNode::new`], then drive it from your run loop with
|
||||||
|
/// [`TinesNode::poll`]. Inbound ROS2 observations arrive on an internal
|
||||||
|
/// channel and are consumed on the next [`TinesNode::poll`], so the node is not
|
||||||
|
/// thread-safe by itself — drive it from a single task.
|
||||||
|
pub struct TinesNode {
|
||||||
|
node: RclNode,
|
||||||
|
tines: TinesNodeCore<LogStore, BtTransport>,
|
||||||
|
/// ROS2 topics currently managed (ROS2 topic name → bookkeeping).
|
||||||
|
managed: HashMap<String, ManagedTopic>,
|
||||||
|
/// Receipt publisher for confidence events.
|
||||||
|
receipt_pub: Publisher<StrMsg>,
|
||||||
|
/// Channel from subscription callbacks back to this node.
|
||||||
|
inbound_rx: std::sync::mpsc::Receiver<InboundMsg>,
|
||||||
|
/// Cloned per subscription so callbacks can route inbound bytes.
|
||||||
|
inbound_tx: std::sync::mpsc::Sender<InboundMsg>,
|
||||||
|
/// Upper bound on the number of managed ROS2 topics.
|
||||||
|
topic_cap: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TinesNode {
|
||||||
|
/// Initialize a node for local id `me` over a [`LogStore`] and a
|
||||||
|
/// [`BtTransport`].
|
||||||
|
///
|
||||||
|
/// * `log_path` — where the backing [`LogStore`] lives (created if missing).
|
||||||
|
/// * `peer` / `radio` — the BLE connection this node exchanges over.
|
||||||
|
/// * `first_seq` — the next tines observation sequence to use.
|
||||||
|
/// * `topic_cap` — the maximum number of ROS2 topics to manage at once.
|
||||||
|
///
|
||||||
|
/// The ROS2 context must already be initialized via [`rclrs::init`] (the
|
||||||
|
/// caller's responsibility). The store's anchor tick is `0`; ages then
|
||||||
|
/// advance under the `Tick` values passed to [`TinesNode::poll`].
|
||||||
|
pub fn new(
|
||||||
|
me: NodeId,
|
||||||
|
first_seq: u64,
|
||||||
|
log_path: &std::path::Path,
|
||||||
|
peer: NodeId,
|
||||||
|
radio: impl Radio + 'static,
|
||||||
|
topic_cap: usize,
|
||||||
|
) -> Result<Self, TinesNodeError> {
|
||||||
|
// rclrs uses the global context set by `rclrs::init`.
|
||||||
|
let node = RclNode::new("tines", "")?;
|
||||||
|
|
||||||
|
let store = LogStore::new(log_path, 0, first_seq)?;
|
||||||
|
let transport = BtTransport::new(me, peer, radio);
|
||||||
|
let tines = TinesNodeCore::new(me, first_seq, store, transport);
|
||||||
|
|
||||||
|
let receipt_pub = node.create_publisher::<StrMsg, _>(RECEIPTS_TOPIC, receipt_qos())?;
|
||||||
|
let (inbound_tx, inbound_rx) = channel::<InboundMsg>();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
node,
|
||||||
|
tines,
|
||||||
|
managed: HashMap::new(),
|
||||||
|
receipt_pub,
|
||||||
|
inbound_rx,
|
||||||
|
inbound_tx,
|
||||||
|
topic_cap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local tines node id this node was built with.
|
||||||
|
pub fn id(&self) -> NodeId {
|
||||||
|
self.tines.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current in-memory count of managed ROS2 topics.
|
||||||
|
pub fn managed_topic_count(&self) -> usize {
|
||||||
|
self.managed.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One bridge tick: consume inbound ROS2 observations, drive the tines
|
||||||
|
/// exchange, publish live topics, and publish any confidence events.
|
||||||
|
///
|
||||||
|
/// Returns the number of *new* observations stored this round (from
|
||||||
|
/// [`TinesNodeCore::poll`]).
|
||||||
|
pub fn poll(&mut self, now: Tick) -> Result<usize, TinesNodeError> {
|
||||||
|
self.drain_inbound(now);
|
||||||
|
let stored = self.tines.poll(now);
|
||||||
|
self.publish_live_topics(now)?;
|
||||||
|
self.publish_receipts();
|
||||||
|
Ok(stored)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Observe inbound ROS2 observations into the tines node, skipping anything
|
||||||
|
/// oversized for tines to reject.
|
||||||
|
fn drain_inbound(&mut self, now: Tick) {
|
||||||
|
while let Ok((topic, payload)) = self.inbound_rx.try_recv() {
|
||||||
|
if payload.len() > tines_core::consts::MAX_PAYLOAD
|
||||||
|
|| topic.len() > tines_core::consts::MAX_TOPIC
|
||||||
|
{
|
||||||
|
continue; // oversized; tines would reject it anyway
|
||||||
|
}
|
||||||
|
let _ = self.tines.observe(&topic, &payload, None, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lazily create a ROS2 publisher/subscriber pair for `tines_topic`, bounded
|
||||||
|
/// by [`TinesNode::new`]'s `topic_cap`.
|
||||||
|
fn ensure(&mut self, tines_topic: &[u8]) -> Result<(), TinesNodeError> {
|
||||||
|
let name = topic_name(tines_topic);
|
||||||
|
if self.managed.contains_key(&name) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.managed.len() >= self.topic_cap {
|
||||||
|
return Ok(()); // cap reached; bound live-topic creation
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subscription callback owns a channel sender and a copy of the
|
||||||
|
// original topic bytes — not `self` — so it can outlive this borrow.
|
||||||
|
let inbound_tx = self.inbound_tx.clone();
|
||||||
|
let tines_topic_cb = tines_topic.to_vec();
|
||||||
|
let subscriber = self.node.create_subscription::<StrMsg, _, _>(
|
||||||
|
&name,
|
||||||
|
topic_qos(),
|
||||||
|
move |msg: &StrMsg| {
|
||||||
|
let _ = inbound_tx.send((tines_topic_cb.clone(), msg.data.as_bytes().to_vec()));
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let publisher = self
|
||||||
|
.node
|
||||||
|
.create_publisher::<StrMsg, _>(&name, topic_qos())?;
|
||||||
|
|
||||||
|
self.managed.insert(
|
||||||
|
name,
|
||||||
|
ManagedTopic {
|
||||||
|
tines_topic: tines_topic.to_vec(),
|
||||||
|
publisher: Some(publisher),
|
||||||
|
subscriber: Some(subscriber),
|
||||||
|
last_pub: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish every distinct topic currently live in the store.
|
||||||
|
fn publish_live_topics(&mut self, now: Tick) -> Result<(), TinesNodeError> {
|
||||||
|
// Distinct live topics, via the store's topic iterator.
|
||||||
|
let mut topics: BTreeSet<Vec<u8>> = BTreeSet::new();
|
||||||
|
self.tines.store().for_each_topic(&[], now, &mut |obs| {
|
||||||
|
topics.insert(obs.topic.as_slice().to_vec())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create publishers for any topic we don't yet manage (cap-bounded in
|
||||||
|
// [`Self::ensure`]).
|
||||||
|
for tines_topic in topics {
|
||||||
|
let name = topic_name(&tines_topic);
|
||||||
|
if self.managed.contains_key(&name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.ensure(&tines_topic)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the latest observation for every managed topic.
|
||||||
|
let managed: Vec<String> = self.managed.keys().cloned().collect();
|
||||||
|
for name in managed {
|
||||||
|
if let Some(topic) = self.managed.get_mut(&name) {
|
||||||
|
self.publish_one(&topic.tines_topic, name, now)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish the latest observation for a single managed topic, unless its
|
||||||
|
/// value is unchanged since the last publish (to avoid a busy loop).
|
||||||
|
fn publish_one(
|
||||||
|
&mut self,
|
||||||
|
tines_topic: &[u8],
|
||||||
|
name: String,
|
||||||
|
now: Tick,
|
||||||
|
) -> Result<(), TinesNodeError> {
|
||||||
|
let topic = self
|
||||||
|
.managed
|
||||||
|
.get_mut(&name)
|
||||||
|
.expect("managed topic still present");
|
||||||
|
if let (Some(publisher), Some(obs)) =
|
||||||
|
(topic.publisher.as_mut(), self.latest_obs(tines_topic, now))
|
||||||
|
{
|
||||||
|
if topic.last_pub == Some(obs.id()) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let payload = String::from_utf8_lossy(&obs.payload).into_owned();
|
||||||
|
let msg = StrMsg { data: payload };
|
||||||
|
let _ = publisher.publish(&msg);
|
||||||
|
topic.last_pub = Some(obs.id());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The youngest observation for `tines_topic`, if any.
|
||||||
|
fn latest_obs(&self, tines_topic: &[u8], now: Tick) -> Option<Observation> {
|
||||||
|
let mut best: Option<Observation> = None;
|
||||||
|
self.tines
|
||||||
|
.store()
|
||||||
|
.for_each_topic(tines_topic, now, &mut |obs| match best {
|
||||||
|
Some(ref b) if obs.age_ms >= b.age_ms => {}
|
||||||
|
_ => best = Some(obs.clone()),
|
||||||
|
});
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish every pending confidence event to [`RECEIPTS_TOPIC`].
|
||||||
|
fn publish_receipts(&mut self) {
|
||||||
|
let events = self.tines.drain_events();
|
||||||
|
for ev in events.iter() {
|
||||||
|
let payload = format!("{}:{}", hex16(ev.peer), hex16(ev.id));
|
||||||
|
let msg = StrMsg { data: payload };
|
||||||
|
let _ = self.receipt_pub.publish(&msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a fixed-width id as lowercase hex (no allocator needed).
|
||||||
|
fn hex16(id: &[u8; 16]) -> String {
|
||||||
|
use core::fmt::Write;
|
||||||
|
let mut s = String::with_capacity(32);
|
||||||
|
for byte in id {
|
||||||
|
let _ = write!(s, "{byte:02x}");
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//! ROS2 topic naming for tines.
|
||||||
|
//!
|
||||||
|
//! tines topics are arbitrary byte strings up to [`tines_core::consts::MAX_TOPIC`], which
|
||||||
|
//! may contain characters ROS2 cannot put directly in a topic name (`/`,
|
||||||
|
//! control bytes, non-ASCII, …). [`topic_name`] hex-escapes the bytes into a
|
||||||
|
//! deterministic, injective name under the `/tines/` namespace, so every tines
|
||||||
|
//! topic maps to exactly one valid ROS2 topic and back.
|
||||||
|
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
/// The namespace every hex-escaped tines topic lives under.
|
||||||
|
const TINES_PREFIX: &str = "/tines/";
|
||||||
|
|
||||||
|
/// Escapes a tines topic into a valid ROS2 topic name under `/tines/`.
|
||||||
|
///
|
||||||
|
/// The raw topic bytes are rendered as lowercase hex; the result is the
|
||||||
|
/// `TINES_PREFIX` (`"/tines/"`) followed by that hex. Encoding is
|
||||||
|
/// **deterministic** (same bytes always produce the same name) and
|
||||||
|
/// **injective** (distinct byte strings never collide), because the hex map
|
||||||
|
/// between byte strings and even-length hex strings is a bijection. The empty
|
||||||
|
/// topic therefore maps to just `/tines/`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// assert_eq!(tines_ros2::topic_name(b"door/3"), "/tines/646f6f722f33");
|
||||||
|
/// ```
|
||||||
|
pub fn topic_name(tines_topic: &[u8]) -> String {
|
||||||
|
let mut name = String::with_capacity(TINES_PREFIX.len() + tines_topic.len() * 2);
|
||||||
|
name.push_str(TINES_PREFIX);
|
||||||
|
for &byte in tines_topic {
|
||||||
|
// `write!` into a `String` never errors, so the `Err` arm is unreachable.
|
||||||
|
let _ = write!(name, "{byte:02x}");
|
||||||
|
}
|
||||||
|
name
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
//! Integration tests for [`tines_ros2::topic_name`] (pure, default features).
|
||||||
|
|
||||||
|
use tines_core::consts::MAX_TOPIC;
|
||||||
|
use tines_ros2::topic_name;
|
||||||
|
|
||||||
|
/// Every arbitrary input — control bytes, high/non-ASCII bytes, slashes, and
|
||||||
|
/// even the empty topic — escapes into a valid ROS2 topic name under
|
||||||
|
/// `/tines/` consisting solely of lowercase hex digits.
|
||||||
|
#[test]
|
||||||
|
fn topic_name_escapes() {
|
||||||
|
let inputs: [&[u8]; 5] = [
|
||||||
|
b"door/3".as_ref(),
|
||||||
|
b"".as_ref(),
|
||||||
|
[0u8, 1, 2, 250, 128, 255].as_ref(),
|
||||||
|
"café — naïve ✓".as_bytes(),
|
||||||
|
&vec![7u8; MAX_TOPIC][..],
|
||||||
|
];
|
||||||
|
|
||||||
|
for input in inputs {
|
||||||
|
let name = topic_name(input);
|
||||||
|
// Valid ROS2 topic names are absolute (start with `/`) and contain no
|
||||||
|
// illegal characters; here the tail is pure hex, so both hold.
|
||||||
|
assert!(
|
||||||
|
name.starts_with("/tines/"),
|
||||||
|
"topic must live under the /tines/ namespace: {name}"
|
||||||
|
);
|
||||||
|
let hex = &name["/tines/".len()..];
|
||||||
|
assert_eq!(
|
||||||
|
hex.len(),
|
||||||
|
input.len() * 2,
|
||||||
|
"each byte must become exactly two hex digits"
|
||||||
|
);
|
||||||
|
// Each byte maps to exactly its two lowercase hex digits.
|
||||||
|
for (byte, pair) in input.iter().zip(hex.as_bytes().chunks(2)) {
|
||||||
|
assert_eq!(
|
||||||
|
pair,
|
||||||
|
format!("{byte:02x}").into_bytes(),
|
||||||
|
"byte {byte:02x} must escape to its lowercase hex {byte:02x}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encoding is deterministic and injective across the full range of topic
|
||||||
|
/// lengths up to [`MAX_TOPIC`]: distinct byte strings never collide.
|
||||||
|
#[test]
|
||||||
|
fn topic_name_roundtrip() {
|
||||||
|
// Deterministic: identical input yields identical output.
|
||||||
|
assert_eq!(topic_name(b"door/3"), topic_name(b"door/3"));
|
||||||
|
|
||||||
|
// Injective: every distinct-length input maps to a distinct name. Hex of
|
||||||
|
// distinct byte strings is distinct, and the shared `/tines/` prefix cannot
|
||||||
|
// cause a cross-length collision (the hex tail has length `2 * len`).
|
||||||
|
for len in 0..=MAX_TOPIC {
|
||||||
|
// A prefix of distinct bytes: `len == a` yields a name never seen at any
|
||||||
|
// other `len`, and distinct `len` values cannot collide.
|
||||||
|
let input: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
|
||||||
|
assert!(topic_name(&input).starts_with("/tines/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-check: distinct byte strings of *equal* length never collide.
|
||||||
|
let mut names = std::collections::BTreeSet::new();
|
||||||
|
for seed in 0..=255u8 {
|
||||||
|
let input: Vec<u8> = (0..MAX_TOPIC).map(|i| seed.wrapping_add(i as u8)).collect();
|
||||||
|
let name = topic_name(&input);
|
||||||
|
assert!(name.starts_with("/tines/"));
|
||||||
|
assert!(
|
||||||
|
names.insert(name),
|
||||||
|
"injectivity violated: seed {seed} collided"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(names.len(), 256, "256 distinct equal-length names expected");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user