Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
target
node_modules
.cargo
.venv-mcp/
compile_commands.json
_*
!crates/ros-z-py/python/**/__init__.py
Expand Down Expand Up @@ -61,3 +62,9 @@ core

# Cached nix dev shell environment (generated by sandbox.nu)
.state/

# Demo video output (generated by scripts/demos/generate.nu)
scripts/demos/results/

# TUI test output (generated by tests/run-vhs-tests.nu)
tests/results/
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
# Tools

- [ros-z-console](./chapters/console.md)
- [TUI Demos](./chapters/console-demos.md)

# Language Bindings

Expand Down
82 changes: 82 additions & 0 deletions book/src/chapters/console-demos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# ros-z-console TUI Demos

These demos use the classic `z_pubsub` (talker/listener) and `z_srvcli` (AddTwoInts server)
examples as a live ROS 2 system.

**Setup:**

```bash
# Terminal 1
zenohd

# Terminal 2
ros-z-console tcp/127.0.0.1:7447 0
```

---

## Startup

ros-z-console connects to the Zenoh router and discovers all live entities in the graph.

![ros-z-console connecting to a ROS 2 system and discovering topics and nodes](../img/01-startup.gif)

---

## Topics Panel

The Topics panel (default) lists all active ROS 2 topics with their type and publisher/subscriber counts.
Navigate with `j` / `k` (or arrow keys). Press `l` or `Enter` to open the detail pane.

![Browsing the topics list in ros-z-console](../img/02-navigation.gif)

---

## Topic Detail

Select a topic and press `l` or `Enter` to view publishers, subscribers, type hash, and QoS profiles.

![Topic detail view showing publishers, subscribers and QoS](../img/05-topic-detail.gif)

---

## Services Panel

Press `2` (or `Tab`) to switch to the Services panel. Lists all active ROS 2 services with their type.

![Services panel listing active ROS 2 services](../img/06-services.gif)

---

## Nodes Panel

Press `3` to switch to the Nodes panel. Lists all active nodes. Select a node and press `l` or `Enter`
to see its publishers, subscribers, and services.

![Nodes panel with per-node topic and service associations](../img/07-nodes.gif)

---

## Rate Measurement

Press `r` on a selected topic for a quick rate check (cached 30s). Switch to the Measure panel
(`4` or `m`) for a continuous measurement with a 60-second time-series chart.

![Rate measurement and time-series chart in ros-z-console](../img/08-measurement.gif)

---

## Filter Mode

Press `/` to enter filter mode and start typing. The list narrows to matching items in real time.
Press `Ctrl+U` to clear, `Escape` to exit filter mode.

![Filter mode with type-ahead search in ros-z-console](../img/03-filter.gif)

---

## Help Overlay

Press `?` to toggle the help overlay showing all keybindings.

![Help overlay showing all keybindings in ros-z-console](../img/04-help.gif)
2 changes: 2 additions & 0 deletions book/src/chapters/console.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ The interactive terminal interface provides:
TUI mode requires a terminal that supports ANSI escape codes. Most modern terminals work out of the box.
```

See [TUI Demos](./console-demos.md) for annotated walkthroughs of each workflow.

### Headless Mode

Headless mode streams events to stdout, making it ideal for:
Expand Down
Binary file added book/src/img/01-startup.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/img/02-navigation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/img/03-filter.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/img/04-help.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/img/05-topic-detail.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/img/06-services.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/img/07-nodes.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/img/08-measurement.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 20 additions & 3 deletions crates/ros-z-console/src/app/render/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ impl App {
let style = list_item_style(i == self.selected_index);

// Format: "* <namespace>/<name>"
let full_name = format!("{}/{}", namespace, name);
// When namespace is "/" (root), avoid producing "//name"
let full_name = if namespace == "/" {
format!("/{}", name)
} else {
format!("{}/{}", namespace, name)
};
let icon_width = 2; // "* "
let node_max_width = list_width.saturating_sub(icon_width);
let display_node = truncate_with_ellipsis(&full_name, node_max_width);
Expand Down Expand Up @@ -68,14 +73,26 @@ impl App {
return "No node selected".to_string();
};

let node_key = (namespace.clone(), name.clone());
// Normalize namespace for HashMap lookup: graph stores root nodes under "" not "/"
let normalized_ns = if namespace == "/" {
String::new()
} else {
namespace.clone()
};
let node_key = (normalized_ns, name.clone());
let publishers = graph.get_names_and_types_by_node(node_key.clone(), EntityKind::Publisher);
let subscribers =
graph.get_names_and_types_by_node(node_key.clone(), EntityKind::Subscription);
let services = graph.get_names_and_types_by_node(node_key.clone(), EntityKind::Service);
let clients = graph.get_names_and_types_by_node(node_key, EntityKind::Client);

let mut detail = format!("Node: {}/{}\n", namespace, name);
// Display format: avoid "//name" when namespace is "/"
let display_path = if namespace == "/" {
format!("/{}", name)
} else {
format!("{}/{}", namespace, name)
};
let mut detail = format!("Node: {}\n", display_path);

let show_types = self.detail_state.publishers_expanded;
let expand_hint = if show_types { "[-]" } else { "[+]" };
Expand Down
8 changes: 4 additions & 4 deletions crates/ros-z-console/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ use std::{
};

use parking_lot::Mutex;
use ros_z::{Builder, context::ZContext, graph::Graph, node::ZNode};
use ros_z::{
Builder, context::ZContext, dynamic::DynamicTopicSubscriber, graph::Graph, node::ZNode,
};
use tokio::sync::broadcast;

use super::{
dynamic_subscriber::DynamicTopicSubscriber, events::SystemEvent, metrics::MetricsCollector,
};
use super::{events::SystemEvent, metrics::MetricsCollector};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Backend {
Expand Down
2 changes: 0 additions & 2 deletions crates/ros-z-console/src/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
pub mod dynamic_subscriber;
pub mod engine;
pub mod events;
pub mod logger;
pub mod message_formatter;
pub mod metrics;
7 changes: 5 additions & 2 deletions crates/ros-z-console/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use app::{
};
use clap::{Parser, ValueEnum};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
Expand Down Expand Up @@ -158,9 +160,10 @@ async fn run_tui_loop(
return Ok(());
}

// Poll for events with timeout
// Poll for events with timeout (ignore Release events to prevent double-dispatch)
if event::poll(Duration::from_millis(POLL_TIMEOUT_MS))?
&& let Event::Key(key) = event::read()?
&& key.kind != KeyEventKind::Release
{
handle_key_event(app, key).await?;
}
Expand Down
8 changes: 3 additions & 5 deletions crates/ros-z-console/src/modes/headless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ use chrono::Utc;
use serde::Serialize;
use std::{collections::HashMap, time::Duration};

use crate::core::{
dynamic_subscriber::DynamicTopicSubscriber,
engine::CoreEngine,
message_formatter::{dynamic_message_to_json, format_message_pretty},
};
use ros_z::dynamic::{DynamicTopicSubscriber, dynamic_message_to_json, format_message_pretty};

use crate::core::engine::CoreEngine;
use ros_z::graph::GraphSnapshot;

/// Wrapper for initial state that adds the event field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
//!
//! Provides JSON and human-readable text formatting for dynamic messages.

use ros_z::dynamic::{DynamicMessage, DynamicValue};
use serde_json;

use crate::dynamic::{DynamicMessage, DynamicValue};

/// Convert a DynamicMessage to a JSON value
///
/// Recursively converts all fields, handling nested messages and arrays.
Expand Down Expand Up @@ -137,7 +138,7 @@ fn format_value_pretty(output: &mut String, name: &str, value: &DynamicValue, in
#[cfg(test)]
mod tests {
use super::*;
use ros_z::dynamic::{FieldType, MessageSchema};
use crate::dynamic::{FieldType, MessageSchema};

#[test]
fn test_json_primitives() {
Expand Down
4 changes: 4 additions & 0 deletions crates/ros-z/src/dynamic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@
//! ```

pub mod error;
pub mod formatter;
pub mod message;
pub mod registry;
pub mod schema;
pub mod serdes;
pub mod serialization;
pub mod subscriber;
pub mod type_description;
pub mod type_description_client;
pub mod type_description_service;
Expand All @@ -70,11 +72,13 @@ mod tests;

// Re-export main types
pub use error::DynamicError;
pub use formatter::{dynamic_message_to_json, dynamic_value_to_json, format_message_pretty};
pub use message::{DynamicMessage, DynamicMessageBuilder};
pub use registry::{SchemaRegistry, get_schema, has_schema, register_schema};
pub use schema::{FieldSchema, FieldType, MessageSchema, MessageSchemaBuilder};
pub use serdes::DynamicSerdeCdrSerdes;
pub use serialization::SerializationFormat;
pub use subscriber::DynamicTopicSubscriber;
pub use type_description::{MessageSchemaTypeDescription, type_description_msg_to_schema};
pub use type_description_client::TypeDescriptionClient;
pub use type_description_service::{
Expand Down
Loading
Loading