Skip to main content

isaac_sim_bridge/articulation/
cmd_vel.rs

1// SPDX-License-Identifier: MPL-2.0
2use std::sync::{Arc, OnceLock};
3
4use crate::ffi::CmdVel;
5use crate::producer::{ProducerRegistry, ProducerSlot};
6use crate::sensor::Sensor;
7
8/// Type-level marker for the cmd_vel articulation channel.
9///
10/// Articulation reverses the data direction (Rust→C++): downstream
11/// adapters publish CmdVel values into a per-target slot; the C++ tick
12/// reads the latest via `poll_cmd_vel`. The `Sensor` impl gives us
13/// uniform NAME-derived env var / log labels even though the data
14/// direction is opposite to the sensor consumers.
15pub struct CmdVelChannel;
16
17impl Sensor for CmdVelChannel {
18    const NAME: &'static str = "cmd_vel";
19}
20
21#[unsafe(no_mangle)]
22pub extern "C" fn isaac_sim_bridge_registry_cmd_vel() -> *const ProducerRegistry<CmdVel> {
23    static SLOT: OnceLock<Box<ProducerRegistry<CmdVel>>> = OnceLock::new();
24    let r = SLOT.get_or_init(|| Box::new(ProducerRegistry::new()));
25    Box::as_ref(r) as *const ProducerRegistry<CmdVel>
26}
27
28fn registry() -> &'static ProducerRegistry<CmdVel> {
29    unsafe { &*isaac_sim_bridge_registry_cmd_vel() }
30}
31
32/// Register (or fetch) a cmd_vel producer for `target_id` (typically
33/// the articulation prim path). Multiple writers can hold their own
34/// Arc to the slot — last `publish` wins.
35pub fn register_cmd_vel_producer(target_id: impl Into<String>) -> Arc<ProducerSlot<CmdVel>> {
36    registry().register(target_id)
37}
38
39/// Number of distinct articulation targets that have registered a producer slot.
40pub fn cmd_vel_producer_count() -> usize {
41    registry().count()
42}
43
44/// Register a consumer that observes every Twist published into any
45/// cmd_vel producer slot. The closure receives `(target_id, &CmdVel)`
46/// — filter on `target_id` if you only care about one articulation.
47///
48/// This is the symmetric "publisher" direction: anything that wants
49/// to watch the actuation stream (dora downstream output, telemetry
50/// logger, second simulator) hooks in here without coupling to the
51/// upstream source.
52pub fn register_cmd_vel_consumer<F>(cb: F)
53where
54    F: Fn(&str, &CmdVel) + Send + Sync + 'static,
55{
56    registry().add_observer(cb);
57}
58
59/// Number of registered cmd_vel observers (adapters watching the actuation stream).
60pub fn cmd_vel_consumer_count() -> usize {
61    registry().observer_count()
62}
63
64/// C++-facing poll. Looks up the slot for `target_id`, copies the
65/// latest published value into `out`, and returns true on hit.
66/// Returns false if no producer is registered for that target or if
67/// no value has been published yet.
68pub fn poll_cmd_vel(target_id: &str, out: &mut CmdVel) -> bool {
69    if let Some(slot) = registry().lookup(target_id) {
70        if let Some(v) = slot.latest() {
71            *out = *v;
72            return true;
73        }
74    }
75    false
76}
77
78/// Rust-friendly variant of `poll_cmd_vel` for tests + adapter
79/// inspection. Returns `Some(CmdVel)` when a producer is registered
80/// and has published at least once; `None` otherwise.
81pub fn peek_cmd_vel(target_id: &str) -> Option<CmdVel> {
82    let mut out = CmdVel::default();
83    poll_cmd_vel(target_id, &mut out).then_some(out)
84}
85
86#[cfg(test)]
87mod tests {
88    use std::sync::atomic::{AtomicUsize, Ordering};
89    use std::sync::{Arc, Mutex};
90
91    use super::*;
92
93    fn make_cmd_vel(linear_x: f32) -> CmdVel {
94        CmdVel {
95            linear_x,
96            linear_y: 0.0,
97            linear_z: 0.0,
98            angular_x: 0.0,
99            angular_y: 0.0,
100            angular_z: 0.1,
101            timestamp_ns: 1_000_000,
102        }
103    }
104
105    #[test]
106    fn poll_returns_published_value() {
107        let target = "/test/articulation/poll_returns";
108        let slot = register_cmd_vel_producer(target);
109        slot.publish(make_cmd_vel(0.5));
110
111        let mut out = CmdVel::default();
112        assert!(poll_cmd_vel(target, &mut out));
113        assert_eq!(out.linear_x, 0.5);
114    }
115
116    #[test]
117    fn poll_misses_when_no_producer() {
118        let mut out = CmdVel::default();
119        assert!(!poll_cmd_vel(
120            "/test/articulation/never_registered",
121            &mut out
122        ));
123    }
124
125    #[test]
126    fn poll_misses_until_first_publish() {
127        let target = "/test/articulation/no_publish_yet";
128        let _slot = register_cmd_vel_producer(target);
129        let mut out = CmdVel::default();
130        assert!(!poll_cmd_vel(target, &mut out));
131    }
132
133    #[test]
134    fn peek_misses_until_first_publish() {
135        let target = "/test/articulation/peek_no_publish_yet";
136        let slot = register_cmd_vel_producer(target);
137        assert!(
138            peek_cmd_vel(target).is_none(),
139            "peek returns None before first publish"
140        );
141        slot.publish(make_cmd_vel(0.7));
142        let got = peek_cmd_vel(target).expect("peek returns Some after publish");
143        assert_eq!(got.linear_x, 0.7);
144    }
145
146    #[test]
147    fn two_consumers_see_correct_target_and_peek_reads_current_value() {
148        let target_a = "/test/articulation/observer_a";
149        let target_b = "/test/articulation/observer_b";
150
151        let slot_a = register_cmd_vel_producer(target_a);
152        let slot_b = register_cmd_vel_producer(target_b);
153
154        let fires_a = Arc::new(AtomicUsize::new(0));
155        let fires_b = Arc::new(AtomicUsize::new(0));
156        let peeked_a: Arc<Mutex<Option<CmdVel>>> = Arc::new(Mutex::new(None));
157        let peeked_b: Arc<Mutex<Option<CmdVel>>> = Arc::new(Mutex::new(None));
158
159        let fires_a2 = Arc::clone(&fires_a);
160        let peeked_a2 = Arc::clone(&peeked_a);
161        let ta = target_a.to_string();
162        register_cmd_vel_consumer(move |tid, _v| {
163            if tid == ta {
164                fires_a2.fetch_add(1, Ordering::SeqCst);
165                // peek must see the value just stored (store-before-notify ordering)
166                *peeked_a2.lock().unwrap() = peek_cmd_vel(&ta);
167            }
168        });
169
170        let fires_b2 = Arc::clone(&fires_b);
171        let peeked_b2 = Arc::clone(&peeked_b);
172        let tb = target_b.to_string();
173        register_cmd_vel_consumer(move |tid, _v| {
174            if tid == tb {
175                fires_b2.fetch_add(1, Ordering::SeqCst);
176                *peeked_b2.lock().unwrap() = peek_cmd_vel(&tb);
177            }
178        });
179
180        slot_a.publish(make_cmd_vel(1.0));
181        slot_b.publish(make_cmd_vel(2.0));
182
183        assert_eq!(fires_a.load(Ordering::SeqCst), 1, "consumer_a fired once");
184        assert_eq!(fires_b.load(Ordering::SeqCst), 1, "consumer_b fired once");
185
186        let pa = peeked_a.lock().unwrap().expect("consumer_a peeked Some");
187        assert_eq!(pa.linear_x, 1.0, "consumer_a saw linear_x=1.0 via peek");
188
189        let pb = peeked_b.lock().unwrap().expect("consumer_b peeked Some");
190        assert_eq!(pb.linear_x, 2.0, "consumer_b saw linear_x=2.0 via peek");
191    }
192}