use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use std::fmt;
use tokio::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParamInfo {
pub node_name: String,
pub param_name: String,
pub value: Option<String>,
pub param_type: String,
}
#[derive(Debug)]
pub enum ParamError {
Io(std::io::Error),
ParseError(String),
InvalidType(String),
}
impl fmt::Display for ParamError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParamError::Io(e) => write!(f, "IO error: {}", e),
ParamError::ParseError(s) => write!(f, "Parse error: {}", s),
ParamError::InvalidType(s) => write!(f, "Type validation error: {}", s),
}
}
}
impl From<std::io::Error> for ParamError {
fn from(err: std::io::Error) -> Self {
ParamError::Io(err)
}
}
pub async fn get_param_list() -> Result<Vec<ParamInfo>, ParamError> {
let start_time = std::time::Instant::now();
crate::debug_log("Executing: ros2 param list");
let command_str = "ros2 param list".to_string();
let output = Command::new("bash")
.args(["-c", &command_str])
.output()
.await?;
let _duration = start_time.elapsed();
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
crate::debug_log(&format!("Command exit status: {}", output.status));
crate::debug_log(&format!("Command stderr: '{}'", stderr));
crate::debug_log(&format!("Command stdout: '{}'", stdout));
if !output.status.success() {
crate::debug_log(&format!(
"ros2 param list command failed with status: {}",
output.status
));
return Err(ParamError::ParseError(format!(
"ros2 param list failed: {} - stderr: {}",
output.status, stderr
)));
}
crate::debug_log(&format!("ros2 param list raw output: '{}'", stdout));
let params = parse_param_list(&stdout)?;
crate::debug_log(&format!("Parsed {} parameters from output", params.len()));
Ok(params)
}
pub fn parse_param_list(output: &str) -> Result<Vec<ParamInfo>, ParamError> {
let mut params = Vec::new();
let mut current_node = String::new();
crate::debug_log(&format!(
"Parsing parameter list from {} lines",
output.lines().count()
));
for (line_num, line) in output.lines().enumerate() {
let original_line = line;
let trimmed_line = line.trim();
crate::debug_log(&format!(
"Line {}: '{}' (original: '{}')",
line_num, trimmed_line, original_line
));
if trimmed_line.is_empty()
|| trimmed_line.contains("WARN")
|| trimmed_line.contains("ERROR")
|| trimmed_line.starts_with("20")
{
crate::debug_log(&format!(
" -> Skipping non-parameter line: '{}'",
trimmed_line
));
continue;
}
if trimmed_line.starts_with('/') && trimmed_line.ends_with(':') {
current_node = trimmed_line[0..trimmed_line.len() - 1].to_string();
crate::debug_log(&format!(" -> Found node: '{}'", current_node));
} else if !current_node.is_empty() && original_line.starts_with(' ') {
let param_name = trimmed_line.to_string();
crate::debug_log(&format!(
" -> Found parameter '{}' for node '{}'",
param_name, current_node
));
if param_name.contains('.') {
create_namespace_hierarchy(&mut params, ¤t_node, ¶m_name);
} else {
params.push(ParamInfo {
node_name: current_node.clone(),
param_name,
value: None,
param_type: String::new(),
});
}
} else {
crate::debug_log(&format!(" -> Unmatched line: '{}'", trimmed_line));
}
}
crate::debug_log(&format!(
"Finished parsing - found {} parameters",
params.len()
));
Ok(params)
}
fn create_namespace_hierarchy(params: &mut Vec<ParamInfo>, node_name: &str, param_name: &str) {
let parts: Vec<&str> = param_name.split('.').collect();
for i in 0..parts.len() {
let current_path = parts[0..=i].join(".");
let is_leaf = i == parts.len() - 1;
let exists = params
.iter()
.any(|p| p.node_name == node_name && p.param_name == current_path);
if !exists {
if is_leaf {
crate::debug_log(&format!(" -> Created parameter: '{}'", current_path));
params.push(ParamInfo {
node_name: node_name.to_string(),
param_name: current_path,
value: None,
param_type: String::new(),
});
} else {
crate::debug_log(&format!(" -> Created namespace: '{}'", current_path));
params.push(ParamInfo {
node_name: node_name.to_string(),
param_name: current_path,
value: Some("namespace".to_string()),
param_type: "Namespace".to_string(),
});
}
}
}
}
pub fn convert_to_ros2_format(value: &str) -> String {
if value.starts_with('[') && value.ends_with(']') {
let inner = &value[1..value.len() - 1]; let elements: Vec<String> = inner
.split(',')
.map(|s| s.trim().to_string()) .collect();
format!("[{}]", elements.join(",")) } else {
value.to_string()
}
}
pub async fn set_param_value(
node_name: &str,
param_name: &str,
value: &str,
) -> Result<String, ParamError> {
let ros2_value = convert_to_ros2_format(value);
let command_str = format!(
"ros2 param set '{}' '{}' {}",
node_name, param_name, ros2_value
);
crate::debug_log(&format!("Executing: {}", command_str));
let output = Command::new("bash")
.args(["-c", &command_str])
.output()
.await?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if stdout.contains("Set parameter successful") {
return Ok("Set parameter successful".to_string());
}
if stdout.contains("Setting parameter failed") {
let error_msg = stdout
.lines()
.find(|line| line.contains("Setting parameter failed"))
.unwrap_or("Setting parameter failed")
.to_string();
return Err(ParamError::ParseError(error_msg));
}
if !stderr.is_empty() {
return Err(ParamError::ParseError(stderr.to_string()));
}
Err(ParamError::ParseError(
"Unknown parameter setting result".to_string(),
))
}
pub async fn dump_params(node_name: &str, file_path: &str) -> Result<(), ParamError> {
let command_str = format!("ros2 param dump {} > {}", node_name, file_path);
let output = Command::new("bash")
.args(["-c", &command_str])
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ParamError::ParseError(format!(
"ros2 param dump failed: {}",
stderr
)));
}
Ok(())
}
pub async fn load_params(node_name: &str, file_path: &str) -> Result<(), ParamError> {
let command_str = format!("ros2 param load {} {}", node_name, file_path);
let output = Command::new("bash")
.args(["-c", &command_str])
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ParamError::ParseError(format!(
"ros2 param load failed: {}",
stderr
)));
}
Ok(())
}
pub async fn get_params_for_node(node_name: &str) -> Result<Vec<ParamInfo>, ParamError> {
let command_str = format!("ros2 param list {}", node_name);
let output = Command::new("bash")
.args(["-c", &command_str])
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ParamError::ParseError(format!(
"ros2 param list failed: {}",
stderr
)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut params = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if !line.is_empty() && !line.starts_with(node_name) {
params.push(ParamInfo {
node_name: node_name.to_string(),
param_name: line.to_string(),
value: None,
param_type: String::new(),
});
}
}
Ok(params)
}
pub async fn get_node_list() -> Result<Vec<String>, ParamError> {
crate::debug_log("Executing: ros2 node list");
let command_str = "ros2 node list".to_string();
let output = Command::new("bash")
.args(["-c", &command_str])
.output()
.await?;
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
crate::debug_log(&format!("ros2 node list exit status: {}", output.status));
crate::debug_log(&format!("ros2 node list stderr: '{}'", stderr));
crate::debug_log(&format!("ros2 node list stdout: '{}'", stdout));
if !output.status.success() {
return Err(ParamError::ParseError(format!(
"ros2 node list failed: {} - stderr: {}",
output.status, stderr
)));
}
let nodes: Vec<String> = stdout
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty() && line.starts_with('/'))
.map(|line| line.to_string())
.collect();
crate::debug_log(&format!("Found {} nodes", nodes.len()));
Ok(nodes)
}
pub async fn get_node_params_dump(node_name: &str) -> Result<Vec<ParamInfo>, ParamError> {
crate::debug_log(&format!("Executing: ros2 param dump {}", node_name));
let command_str = format!("ros2 param dump '{}'", node_name);
let output = Command::new("bash")
.args(["-c", &command_str])
.output()
.await?;
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
crate::debug_log(&format!("ros2 param dump exit status: {}", output.status));
crate::debug_log(&format!("ros2 param dump stderr: '{}'", stderr));
crate::debug_log(&format!("ros2 param dump stdout length: {}", stdout.len()));
if !output.status.success() {
crate::debug_log(&format!(
"Node {} has no parameters or dump failed: {}",
node_name, stderr
));
return Ok(Vec::new());
}
parse_param_dump_yaml(node_name, &stdout)
}
fn parse_param_dump_yaml(
node_name: &str,
yaml_content: &str,
) -> Result<Vec<ParamInfo>, ParamError> {
crate::debug_log(&format!("Parsing YAML dump for node: {}", node_name));
if yaml_content.trim().is_empty() {
crate::debug_log("Empty YAML content");
return Ok(Vec::new());
}
let yaml_value: Value = serde_yaml::from_str(yaml_content)
.map_err(|e| ParamError::ParseError(format!("Failed to parse YAML: {}", e)))?;
let mut params = Vec::new();
if let Some(node_data) = yaml_value.get(node_name) {
if let Some(ros_params) = node_data.get("ros__parameters") {
if let Some(param_map) = ros_params.as_mapping() {
parse_nested_params(&mut params, node_name, "", param_map);
}
}
}
crate::debug_log(&format!(
"Parsed {} parameters from YAML dump",
params.len()
));
Ok(params)
}
fn parse_nested_params(
params: &mut Vec<ParamInfo>,
node_name: &str,
prefix: &str,
param_map: &serde_yaml::Mapping,
) {
for (param_name, param_value) in param_map {
if let Some(param_name_str) = param_name.as_str() {
let full_param_name = if prefix.is_empty() {
param_name_str.to_string()
} else {
format!("{}.{}", prefix, param_name_str)
};
if let Some(nested_map) = param_value.as_mapping() {
crate::debug_log(&format!("Found namespace: {}", full_param_name));
parse_nested_params(params, node_name, &full_param_name, nested_map);
} else {
let (value_str, type_str) = extract_value_and_type(param_value);
crate::debug_log(&format!(
"Parsed param: {} = {} ({})",
full_param_name, value_str, type_str
));
params.push(ParamInfo {
node_name: node_name.to_string(),
param_name: full_param_name,
value: Some(value_str),
param_type: type_str,
});
}
}
}
}
pub fn detect_array_element_type(seq: &[Value]) -> String {
if seq.is_empty() {
return "Array".to_string();
}
match &seq[0] {
Value::Bool(_) => "Boolean".to_string(),
Value::Number(n) => {
if n.is_i64() {
"Integer".to_string()
} else {
"Double".to_string()
}
}
Value::String(_) => "String".to_string(),
_ => "Array".to_string(), }
}
fn format_array_value(seq: &[Value]) -> String {
if seq.is_empty() {
return "[]".to_string();
}
const MAX_DISPLAY_ITEMS: usize = 8;
const MAX_TOTAL_LENGTH: usize = 100;
let mut elements = Vec::new();
let mut total_length = 2; let mut truncated = false;
for (i, item) in seq.iter().enumerate() {
if i >= MAX_DISPLAY_ITEMS {
truncated = true;
break;
}
let item_str = match item {
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => format!("\"{}\"", s),
_ => format!("{:?}", item),
};
let separator_len = if i == 0 { 0 } else { 2 }; if total_length + separator_len + item_str.len() > MAX_TOTAL_LENGTH {
truncated = true;
break;
}
elements.push(item_str);
total_length += separator_len + elements.last().unwrap().len();
}
let mut result = format!("[{}]", elements.join(", "));
if truncated {
result.pop();
result.push_str(", ...]");
}
result
}
fn extract_value_and_type(value: &Value) -> (String, String) {
match value {
Value::Bool(b) => (b.to_string(), "Boolean".to_string()),
Value::Number(n) => {
if n.is_i64() {
(n.as_i64().unwrap().to_string(), "Integer".to_string())
} else if n.is_f64() {
let f_val = n.as_f64().unwrap();
let f_str = f_val.to_string();
if f_str.contains('.') {
(f_str, "Double".to_string())
} else {
(format!("{}.0", f_str), "Double".to_string())
}
} else {
(n.to_string(), "Number".to_string())
}
}
Value::String(s) => (s.clone(), "String".to_string()),
Value::Sequence(seq) => {
let formatted_array = format_array_value(seq);
let array_type = detect_array_element_type(seq);
(formatted_array, array_type)
}
Value::Mapping(_) => (
serde_yaml::to_string(value)
.unwrap_or_default()
.trim()
.to_string(),
"Object".to_string(),
),
Value::Null => ("null".to_string(), "Null".to_string()),
_ => (format!("{:?}", value), "Unknown".to_string()),
}
}
pub async fn get_param_list_with_values() -> Result<Vec<ParamInfo>, ParamError> {
crate::debug_log("Starting improved parameter fetch using ros2 param dump");
let nodes = get_node_list().await?;
crate::debug_log(&format!("Found {} nodes to query", nodes.len()));
let mut all_params = Vec::new();
for node_name in nodes {
match get_node_params_dump(&node_name).await {
Ok(mut node_params) => {
crate::debug_log(&format!(
"Node {} has {} parameters",
node_name,
node_params.len()
));
all_params.append(&mut node_params);
}
Err(e) => {
crate::debug_log(&format!(
"Failed to get parameters for node {}: {}",
node_name, e
));
}
}
}
crate::debug_log(&format!("Total parameters collected: {}", all_params.len()));
Ok(all_params)
}
pub async fn get_node_params_with_values(node_name: &str) -> Result<Vec<ParamInfo>, ParamError> {
crate::debug_log(&format!(
"Getting parameters for node {} using ros2 param dump",
node_name
));
let command_str = format!("ros2 param dump '{}'", node_name);
let output = Command::new("bash")
.args(["-c", &command_str])
.output()
.await?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
return Err(ParamError::ParseError(format!(
"ros2 param dump failed for {}: {}",
node_name, stderr
)));
}
let yaml_value: Value = serde_yaml::from_str(&stdout).map_err(|e| {
ParamError::ParseError(format!("Failed to parse YAML for {}: {}", node_name, e))
})?;
let mut params = Vec::new();
if let Value::Mapping(node_map) = yaml_value {
if let Some((_, node_params)) = node_map.iter().next() {
if let Some(nested_map) = node_params.as_mapping() {
if let Some(ros_params) = nested_map.get("ros__parameters") {
if let Some(ros_params_map) = ros_params.as_mapping() {
parse_nested_params(&mut params, node_name, "", ros_params_map);
}
} else {
parse_nested_params(&mut params, node_name, "", nested_map);
}
}
}
}
crate::debug_log(&format!(
"Node {} has {} parameters",
node_name,
params.len()
));
Ok(params)
}
pub async fn get_single_param_value(
node_name: &str,
param_name: &str,
) -> Result<(String, String), ParamError> {
crate::debug_log(&format!(
"Getting parameter {}/{} using ros2 param dump",
node_name, param_name
));
let params = get_node_params_with_values(node_name).await?;
for param in params {
if param.param_name == param_name {
if let Some(value) = param.value {
return Ok((value, param.param_type));
}
}
}
Err(ParamError::ParseError(format!(
"Parameter {}/{} not found",
node_name, param_name
)))
}