use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::LazyLock;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ConfigOverride {
pub key: &'static str,
pub value: Value,
pub reason: &'static str,
}
#[derive(Debug, Clone)]
pub struct DistroDefaults {
pub supports_type_hash: bool,
}
impl DistroDefaults {
pub const fn humble() -> Self {
Self {
supports_type_hash: false,
}
}
pub const fn kilted() -> Self {
Self {
supports_type_hash: true,
}
}
pub const fn rolling() -> Self {
Self {
supports_type_hash: true,
}
}
pub const fn jazzy() -> Self {
Self {
supports_type_hash: true,
}
}
pub const fn current() -> Self {
if cfg!(feature = "humble") {
return Self::humble();
}
if cfg!(feature = "kilted") {
return Self::kilted();
}
if cfg!(feature = "rolling") {
return Self::rolling();
}
Self::jazzy()
}
}
fn common_overrides() -> &'static [ConfigOverride] {
static COMMON: LazyLock<Vec<ConfigOverride>> = LazyLock::new(|| {
vec![
ConfigOverride {
key: "scouting/multicast/enabled",
value: serde_json::json!(false),
reason: "Disable multicast discovery - use TCP gossip instead",
},
ConfigOverride {
key: "scouting/gossip/target",
value: serde_json::json!({"router": ["router", "peer"], "peer": ["router"]}),
reason: "Peers send gossip only to router (not to other peers) to minimize traffic at launch",
},
ConfigOverride {
key: "timestamping/enabled",
value: serde_json::json!({"router": true, "peer": true, "client": true}),
reason: "Enable timestamping for peers and clients (required for transient_local durability)",
},
ConfigOverride {
key: "transport/unicast/open_timeout",
value: serde_json::json!(60000),
reason: "Increased from 10s to 60s to avoid timeout when opening links with many nodes",
},
ConfigOverride {
key: "transport/unicast/accept_timeout",
value: serde_json::json!(60000),
reason: "Increased from 10s to 60s to avoid timeout when accepting links with many nodes",
},
ConfigOverride {
key: "transport/unicast/accept_pending",
value: serde_json::json!(10000),
reason: "Increased from 100 to 10000 to handle many simultaneous connection handshakes",
},
ConfigOverride {
key: "transport/unicast/max_sessions",
value: serde_json::json!(10000),
reason: "Increased from 1000 to 10000 to support large number of concurrent sessions",
},
ConfigOverride {
key: "transport/link/tx/lease",
value: serde_json::json!(60000),
reason: "Increased from 10s to 60s to avoid lease expiration at launch with many nodes",
},
ConfigOverride {
key: "transport/link/tx/keep_alive",
value: serde_json::json!(2),
reason: "Decreased from 4 to 2 for loopback where packet loss is minimal",
},
ConfigOverride {
key: "transport/shared_memory/enabled",
value: serde_json::json!(false),
reason: "Disabled by default until fully tested in production ROS environments",
},
]
});
&COMMON
}
fn router_specific_overrides() -> &'static [ConfigOverride] {
static ROUTER_SPECIFIC: LazyLock<Vec<ConfigOverride>> = LazyLock::new(|| {
vec![
ConfigOverride {
key: "mode",
value: serde_json::json!("router"),
reason: "Router mode required for ROS 2 discovery/routing",
},
ConfigOverride {
key: "listen/endpoints",
value: serde_json::json!(["tcp/[::]:7447"]),
reason: "Standard ROS 2 port 7447, IPv6 wildcard for all interfaces",
},
ConfigOverride {
key: "connect/endpoints",
value: serde_json::json!([]),
reason: "Router does not connect to other endpoints (empty list)",
},
ConfigOverride {
key: "routing/router/peers_failover_brokering",
value: serde_json::json!(false),
reason: "Changed from true to false - unnecessary when peers connect directly, reduces overhead",
},
ConfigOverride {
key: "transport/link/tx/queue/congestion_control/block/wait_before_close",
value: serde_json::json!(5000000),
reason: "Keep at 5s (vs 60s for session) - router routes to WiFi, lower value prevents long blocks",
},
]
});
&ROUTER_SPECIFIC
}
fn session_specific_overrides() -> &'static [ConfigOverride] {
static SESSION_SPECIFIC: LazyLock<Vec<ConfigOverride>> = LazyLock::new(|| {
vec![
ConfigOverride {
key: "mode",
value: serde_json::json!("peer"),
reason: "Peer mode for ROS nodes - connects to router for discovery and routing",
},
ConfigOverride {
key: "connect/endpoints",
value: serde_json::json!(["tcp/localhost:7447"]),
reason: "Connect to Zenoh router on localhost at standard ROS 2 port 7447",
},
ConfigOverride {
key: "listen/endpoints",
value: serde_json::json!(["tcp/localhost:0"]),
reason: "Accept connections only from localhost - external traffic routed via router",
},
ConfigOverride {
key: "scouting/gossip/autoconnect_strategy",
value: serde_json::json!({"peer": {"to_router": "always", "to_peer": "greater-zid"}}),
reason: "Changed peer-to-peer from 'always' to 'greater-zid' to avoid redundant connections on loopback",
},
ConfigOverride {
key: "queries_default_timeout",
value: serde_json::json!(60000),
reason: "Increased from 10s to 60s to handle slow service servers at launch",
},
ConfigOverride {
key: "transport/link/tx/queue/congestion_control/block/wait_before_close",
value: serde_json::json!(60000000),
reason: "Increased from 5s to 60s to avoid premature link closure during launch congestion on loopback",
},
]
});
&SESSION_SPECIFIC
}
pub fn router_overrides() -> Vec<ConfigOverride> {
let mut overrides =
Vec::with_capacity(router_specific_overrides().len() + common_overrides().len());
overrides.extend_from_slice(router_specific_overrides());
overrides.extend_from_slice(common_overrides());
overrides
}
pub fn session_overrides() -> Vec<ConfigOverride> {
let mut overrides =
Vec::with_capacity(session_specific_overrides().len() + common_overrides().len());
overrides.extend_from_slice(session_specific_overrides());
overrides.extend_from_slice(common_overrides());
overrides
}
fn build_config(overrides: &[ConfigOverride]) -> zenoh::Result<zenoh::Config> {
let mut config = zenoh::Config::default();
for override_ in overrides {
let value_str = serde_json::to_string(&override_.value)?;
config.insert_json5(override_.key, &value_str)?;
}
Ok(config)
}
pub fn router_config() -> zenoh::Result<zenoh::Config> {
build_config(&router_overrides())
}
pub fn session_config() -> zenoh::Result<zenoh::Config> {
build_config(&session_overrides())
}
pub fn generate_json5(overrides: &[ConfigOverride], name: &str) -> String {
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
let mut output = format!("// GENERATED: {} - DO NOT EDIT\n", name);
output.push_str("// This file is auto-generated from hiroz/src/config.rs\n");
output.push_str("// Edit the source file and rebuild to make changes\n");
let mut root = JsonValue::Object(serde_json::Map::new());
let mut comments: BTreeMap<String, String> = BTreeMap::new();
for override_ in overrides {
let path_parts: Vec<&str> = override_.key.split('/').collect();
comments.insert(override_.key.to_string(), override_.reason.to_string());
let mut current = &mut root;
for (i, part) in path_parts.iter().enumerate() {
if i == path_parts.len() - 1 {
if let JsonValue::Object(map) = current {
map.insert(part.to_string(), override_.value.clone());
}
} else {
if let JsonValue::Object(map) = current {
current = map
.entry(part.to_string())
.or_insert_with(|| JsonValue::Object(serde_json::Map::new()));
}
}
}
}
output.push_str(&generate_json5_with_comments(&root, &comments, "", 0));
output
}
fn generate_json5_with_comments(
value: &serde_json::Value,
comments: &std::collections::BTreeMap<String, String>,
current_path: &str,
indent_level: usize,
) -> String {
use serde_json::Value as JsonValue;
let indent = " ".repeat(indent_level);
let mut output = String::new();
match value {
JsonValue::Object(map) => {
output.push_str("{\n");
let entries: Vec<_> = map.iter().collect();
for (i, (key, val)) in entries.iter().enumerate() {
let new_path = if current_path.is_empty() {
key.to_string()
} else {
format!("{}/{}", current_path, key)
};
if let Some(comment) = comments.get(&new_path) {
output.push_str(&format!("{} // {}\n", indent, comment));
}
output.push_str(&format!("{} \"{}\": ", indent, key));
let nested =
generate_json5_with_comments(val, comments, &new_path, indent_level + 1);
let nested_trimmed = if matches!(val, JsonValue::Object(_) | JsonValue::Array(_)) {
nested
} else {
nested.trim().to_string()
};
output.push_str(&nested_trimmed);
if i < entries.len() - 1 {
output.push_str(",\n");
} else {
output.push('\n');
}
}
output.push_str(&format!("{}}}", indent));
}
JsonValue::Array(arr) => {
if arr.is_empty() {
output.push_str("[]");
} else if arr
.iter()
.all(|v| !matches!(v, JsonValue::Object(_) | JsonValue::Array(_)))
{
output.push('[');
for (i, item) in arr.iter().enumerate() {
output.push_str(&serde_json::to_string(item).unwrap());
if i < arr.len() - 1 {
output.push_str(", ");
}
}
output.push(']');
} else {
output.push_str("[\n");
for (i, item) in arr.iter().enumerate() {
output.push_str(&format!("{} ", indent));
output.push_str(
generate_json5_with_comments(
item,
comments,
current_path,
indent_level + 1,
)
.trim(),
);
if i < arr.len() - 1 {
output.push(',');
}
output.push('\n');
}
output.push_str(&format!("{}]", indent));
}
}
other => {
output.push_str(&serde_json::to_string_pretty(other).unwrap());
}
}
output
}
#[derive(Clone)]
pub struct RouterConfigBuilder {
overrides: Vec<ConfigOverride>,
}
impl RouterConfigBuilder {
pub fn new() -> Self {
Self {
overrides: router_overrides(),
}
}
pub fn with_listen_port(mut self, port: u16) -> Self {
if let Some(listen) = self
.overrides
.iter_mut()
.find(|o| o.key == "listen/endpoints")
{
listen.value = serde_json::json!([format!("tcp/[::]:{}", port)]);
}
self
}
pub fn with_listen_endpoint(mut self, endpoint: &str) -> Self {
if let Some(listen) = self
.overrides
.iter_mut()
.find(|o| o.key == "listen/endpoints")
{
listen.value = serde_json::json!([endpoint]);
}
self
}
pub fn with_override(mut self, key: &'static str, value: Value, reason: &'static str) -> Self {
if let Some(existing) = self.overrides.iter_mut().find(|o| o.key == key) {
existing.value = value;
existing.reason = reason;
} else {
self.overrides.push(ConfigOverride { key, value, reason });
}
self
}
pub fn build_config(self) -> zenoh::Result<zenoh::Config> {
build_config(&self.overrides)
}
}
impl Default for RouterConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct SessionConfigBuilder {
overrides: Vec<ConfigOverride>,
}
impl SessionConfigBuilder {
pub fn new() -> Self {
Self {
overrides: session_overrides(),
}
}
pub fn with_router_endpoint(mut self, endpoint: &str) -> Self {
if let Some(connect) = self
.overrides
.iter_mut()
.find(|o| o.key == "connect/endpoints")
{
connect.value = serde_json::json!([endpoint]);
}
self
}
pub fn with_override(mut self, key: &'static str, value: Value, reason: &'static str) -> Self {
if let Some(existing) = self.overrides.iter_mut().find(|o| o.key == key) {
existing.value = value;
existing.reason = reason;
} else {
self.overrides.push(ConfigOverride { key, value, reason });
}
self
}
pub fn build_config(self) -> zenoh::Result<zenoh::Config> {
build_config(&self.overrides)
}
}
impl Default for SessionConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Builder;
#[test]
fn test_common_overrides_shared() {
let router = router_overrides();
let session = session_overrides();
let common = common_overrides();
for common_override in common {
assert!(
router.iter().any(|o| o.key == common_override.key),
"Router missing common override: {}",
common_override.key
);
assert!(
session.iter().any(|o| o.key == common_override.key),
"Session missing common override: {}",
common_override.key
);
}
}
#[test]
fn test_router_config_creates_valid_session() {
let config = RouterConfigBuilder::new()
.with_listen_endpoint("tcp/[::]:0")
.build()
.expect("Failed to build router config");
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
let session = zenoh::open(config).await;
assert!(
session.is_ok(),
"Failed to create Zenoh session with router config: {:?}",
session.err()
);
});
}
#[test]
fn test_session_config_creates_valid_session() {
let config = session_config().expect("Failed to build session config");
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
let session = zenoh::open(config).await;
assert!(
session.is_ok(),
"Failed to create Zenoh session with peer config: {:?}",
session.err()
);
});
}
#[test]
fn test_all_overrides_produce_valid_config() {
for override_ in &router_overrides() {
let mut config = zenoh::Config::default();
let value_str = serde_json::to_string(&override_.value).unwrap();
let result = config.insert_json5(override_.key, &value_str);
assert!(
result.is_ok(),
"Router override '{}' is invalid: {:?}",
override_.key,
result.err()
);
}
for override_ in &session_overrides() {
let mut config = zenoh::Config::default();
let value_str = serde_json::to_string(&override_.value).unwrap();
let result = config.insert_json5(override_.key, &value_str);
assert!(
result.is_ok(),
"Session override '{}' is invalid: {:?}",
override_.key,
result.err()
);
}
}
#[test]
fn test_router_builder_custom_port() {
let config = RouterConfigBuilder::new()
.with_listen_port(7448)
.build()
.expect("Failed to build router config");
assert_eq!(config.mode().unwrap().to_string(), "router");
}
#[test]
fn test_session_builder_custom_endpoint() {
let config = SessionConfigBuilder::new()
.with_router_endpoint("tcp/192.168.1.1:7447")
.build()
.expect("Failed to build session config");
assert_eq!(config.mode().unwrap().to_string(), "peer");
}
#[test]
fn test_builder_with_custom_override() {
let config = RouterConfigBuilder::new()
.with_override(
"transport/unicast/max_sessions",
serde_json::json!(20000),
"Custom increased sessions",
)
.build()
.expect("Failed to build");
assert_eq!(config.mode().unwrap().to_string(), "router");
}
#[test]
fn test_generate_json5_router() {
let json5 = generate_json5(&router_overrides(), "Test Router Config");
assert!(json5.contains("GENERATED"));
assert!(json5.contains("mode"));
assert!(json5.contains("router"));
}
#[test]
fn test_generate_json5_session() {
let json5 = generate_json5(&session_overrides(), "Test Session Config");
assert!(json5.contains("GENERATED"));
assert!(json5.contains("mode"));
assert!(json5.contains("peer"));
}
}