Skip to main content

shapes_demo_viewer/
shapes_demo_viewer.rs

1//! ShapesDemo-Live-Viewer (Terminal).
2//!
3//! Subscribiert auf alle drei Standard-Shapes-Topics (Square / Circle /
4//! Triangle) und zeichnet die jeweils zuletzt empfangenen Sample-
5//! Positionen als Unicode-Blocks in einem ANSI-Terminal-Fenster.
6//!
7//! Interop-Test:
8//! - Starte `rtishapesdemo` (RTI 7.7.0), Cyclone- oder FastDDS-
9//!   ShapesDemo, oder den ZeroDDS-Publisher (`shapes_demo_publisher`),
10//!   und du siehst die Forme live im Terminal wandern.
11//! - Multiple Publishers (z.B. Cyclone schickt Square, ZeroDDS schickt
12//!   Circle, RTI schickt Triangle) sind moeglich — jeder publisher
13//!   landet auf seinem eigenen Topic.
14//!
15//! # Usage
16//!
17//! ```text
18//! cargo run -p zerodds-dcps --example shapes_demo_viewer
19//! cargo run -p zerodds-dcps --example shapes_demo_viewer -- 0   # domain id
20//! ```
21//!
22//! Beenden: Ctrl-C.
23//!
24//! # Layout
25//!
26//! ShapesDemo-Canvas geht (per Cyclone/RTI/FastDDS-Default) von 0..240
27//! horizontal und 0..270 vertikal. Wir mappen das auf 80×30 Terminal-
28//! Zellen. Farbe der Forme wird via ANSI-256-Color-Code re-mapped
29//! (BLUE → Blau, RED → Rot, etc.).
30
31#![allow(clippy::print_stdout, clippy::print_stderr)]
32
33use std::collections::HashMap;
34use std::env;
35use std::io::Write;
36use std::sync::Arc;
37use std::sync::atomic::{AtomicBool, Ordering};
38use std::thread;
39use std::time::Duration;
40
41use zerodds_dcps::interop::ShapeType;
42use zerodds_dcps::{
43    DataReaderQos, DomainParticipantFactory, DomainParticipantQos, SubscriberQos, TopicQos,
44};
45
46const VIEW_W: usize = 80;
47const VIEW_H: usize = 30;
48const SHAPES_CANVAS_W: i32 = 240;
49const SHAPES_CANVAS_H: i32 = 270;
50
51/// ANSI-Foreground-Color-Code fuer ShapesDemo-Standard-Color-Strings.
52fn ansi_color(name: &str) -> &'static str {
53    match name.to_ascii_uppercase().as_str() {
54        "BLUE" => "\x1B[34m",
55        "RED" => "\x1B[31m",
56        "GREEN" => "\x1B[32m",
57        "ORANGE" => "\x1B[38;5;208m",
58        "YELLOW" => "\x1B[33m",
59        "MAGENTA" => "\x1B[35m",
60        "CYAN" => "\x1B[36m",
61        "PURPLE" => "\x1B[38;5;93m",
62        _ => "\x1B[37m",
63    }
64}
65
66const ANSI_RESET: &str = "\x1B[0m";
67
68fn glyph(topic: &str) -> char {
69    match topic {
70        "Square" => '■',
71        "Circle" => '●',
72        "Triangle" => '▲',
73        _ => '?',
74    }
75}
76
77fn map_x(x: i32) -> usize {
78    let r = x.clamp(0, SHAPES_CANVAS_W - 1);
79    (usize::try_from(r).unwrap_or(0) * VIEW_W) / usize::try_from(SHAPES_CANVAS_W).unwrap_or(1)
80}
81
82fn map_y(y: i32) -> usize {
83    let r = y.clamp(0, SHAPES_CANVAS_H - 1);
84    (usize::try_from(r).unwrap_or(0) * VIEW_H) / usize::try_from(SHAPES_CANVAS_H).unwrap_or(1)
85}
86
87fn install_signal_handler(stop: Arc<AtomicBool>) {
88    // Best-effort: SIGINT setzt stop-flag.
89    let s = stop.clone();
90    ctrlc_setter(move || s.store(true, Ordering::Relaxed));
91}
92
93#[cfg(target_os = "linux")]
94fn ctrlc_setter<F: Fn() + Send + Sync + 'static>(f: F) {
95    use std::sync::Mutex;
96    static HOOK: Mutex<Option<Box<dyn Fn() + Send + Sync>>> = Mutex::new(None);
97    if let Ok(mut g) = HOOK.lock() {
98        *g = Some(Box::new(f));
99    }
100    extern "C" fn handler(_: i32) {
101        if let Ok(g) = HOOK.lock() {
102            if let Some(h) = g.as_ref() {
103                h();
104            }
105        }
106    }
107    // SAFETY: libc::signal nimmt C-ABI-Funktionspointer; `handler` ist
108    // `extern "C"` und hat exakt die erwartete Signatur (i32).
109    unsafe {
110        libc::signal(libc::SIGINT, handler as usize);
111    }
112}
113
114#[cfg(not(target_os = "linux"))]
115fn ctrlc_setter<F: Fn() + Send + Sync + 'static>(_: F) {}
116
117fn main() -> Result<(), Box<dyn std::error::Error>> {
118    let args: Vec<String> = env::args().collect();
119    let domain_id: i32 = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
120
121    let stop = Arc::new(AtomicBool::new(false));
122    install_signal_handler(stop.clone());
123
124    let factory = DomainParticipantFactory::instance();
125    let participant = factory.create_participant(domain_id, DomainParticipantQos::default())?;
126    let subscriber = participant.create_subscriber(SubscriberQos::default());
127
128    let topics = ["Square", "Circle", "Triangle"];
129    let mut readers = Vec::new();
130    for t in &topics {
131        let topic = participant.create_topic::<ShapeType>(t, TopicQos::default())?;
132        let reader = subscriber.create_datareader::<ShapeType>(&topic, DataReaderQos::default())?;
133        readers.push((*t, reader));
134    }
135
136    eprintln!(
137        "shapes_demo_viewer: Domain={domain_id} — subscribed to Square / Circle / Triangle. Ctrl-C beendet."
138    );
139    eprintln!("Warte auf Discovery + erste Samples...");
140
141    // Hide cursor + clear screen.
142    print!("\x1B[?25l\x1B[2J");
143    let mut stdout = std::io::stdout();
144    stdout.flush().ok();
145
146    // Pro (topic, color) speichern wir die letzte Position.
147    let mut shapes: HashMap<(String, String), (i32, i32)> = HashMap::new();
148    let mut sample_count: u64 = 0;
149    let mut grid: Vec<Vec<(char, &'static str)>> = vec![vec![(' ', ""); VIEW_W]; VIEW_H];
150
151    while !stop.load(Ordering::Relaxed) {
152        // 1) Take all pending samples per topic.
153        for (topic_name, reader) in &readers {
154            if let Ok(samples) = reader.take() {
155                for sample in samples {
156                    shapes.insert(
157                        ((*topic_name).to_string(), sample.color.clone()),
158                        (sample.x, sample.y),
159                    );
160                    sample_count += 1;
161                }
162            }
163        }
164
165        // 2) Clear grid.
166        for row in &mut grid {
167            for cell in row {
168                *cell = (' ', "");
169            }
170        }
171
172        // 3) Plot shapes.
173        for ((topic, color), (x, y)) in &shapes {
174            let gx = map_x(*x);
175            let gy = map_y(*y);
176            if gy < VIEW_H && gx < VIEW_W {
177                grid[gy][gx] = (glyph(topic), ansi_color(color));
178            }
179        }
180
181        // 4) Render: home, draw, status line.
182        print!("\x1B[H");
183        // top border
184        print!("┌");
185        for _ in 0..VIEW_W {
186            print!("─");
187        }
188        println!("┐");
189        for row in &grid {
190            print!("│");
191            for (ch, color) in row {
192                if color.is_empty() {
193                    print!(" ");
194                } else {
195                    print!("{color}{ch}{ANSI_RESET}");
196                }
197            }
198            println!("│");
199        }
200        print!("└");
201        for _ in 0..VIEW_W {
202            print!("─");
203        }
204        println!("┘");
205        println!(
206            "shapes={:3} samples={:6} domain={} — Ctrl-C beendet                            ",
207            shapes.len(),
208            sample_count,
209            domain_id,
210        );
211        stdout.flush().ok();
212
213        thread::sleep(Duration::from_millis(60));
214    }
215
216    // Show cursor again.
217    print!("\x1B[?25h");
218    println!("[shapes_demo_viewer] beendet. Total samples empfangen: {sample_count}");
219    Ok(())
220}