phytrace_sdk/models/domains/
hri.rs

1//! HRI Domain - Human-Robot Interaction.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use validator::Validate;
6
7use super::common::Position3D;
8use crate::models::enums::{HandoverState, HumanActivity, InteractionState};
9
10/// HRI domain containing human-robot interaction information.
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct HriDomain {
13    /// Current interaction state
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub interaction_state: Option<InteractionState>,
16
17    /// Tracked humans
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub tracked_humans: Option<Vec<TrackedHuman>>,
20
21    /// Human count nearby
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub human_count: Option<u32>,
24
25    /// Voice interaction
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub voice: Option<VoiceInteraction>,
28
29    /// Handover operation
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub handover: Option<Handover>,
32
33    /// Social navigation
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub social: Option<SocialNavigation>,
36
37    /// Gesture recognition
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub gestures: Option<Vec<DetectedGesture>>,
40}
41
42/// Tracked human information.
43#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
44pub struct TrackedHuman {
45    /// Human ID (tracking ID)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub human_id: Option<String>,
48
49    /// Position relative to robot
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub position: Option<Position3D>,
52
53    /// Distance to robot (m)
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[validate(range(min = 0.0))]
56    pub distance_m: Option<f64>,
57
58    /// Bearing from robot (degrees)
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub bearing_deg: Option<f64>,
61
62    /// Velocity (m/s)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub velocity_mps: Option<f64>,
65
66    /// Heading (degrees)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub heading_deg: Option<f64>,
69
70    /// Detected activity
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub activity: Option<HumanActivity>,
73
74    /// Whether human is looking at robot
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub looking_at_robot: Option<bool>,
77
78    /// Tracking confidence (0-1)
79    #[serde(skip_serializing_if = "Option::is_none")]
80    #[validate(range(min = 0.0, max = 1.0))]
81    pub confidence: Option<f64>,
82
83    /// Time first detected
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub first_detected: Option<DateTime<Utc>>,
86
87    /// Whether this is the interacting human
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub is_interacting: Option<bool>,
90}
91
92/// Voice interaction information.
93#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94pub struct VoiceInteraction {
95    /// Whether listening
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub is_listening: Option<bool>,
98
99    /// Whether speaking
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub is_speaking: Option<bool>,
102
103    /// Last recognized utterance
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub last_utterance: Option<String>,
106
107    /// Recognition confidence (0-1)
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub recognition_confidence: Option<f64>,
110
111    /// Detected intent
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub detected_intent: Option<String>,
114
115    /// Current response/output
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub current_response: Option<String>,
118
119    /// Noise level (dB)
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub ambient_noise_db: Option<f64>,
122}
123
124/// Handover operation information.
125#[derive(Debug, Clone, Default, Serialize, Deserialize)]
126pub struct Handover {
127    /// Handover state
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub state: Option<HandoverState>,
130
131    /// Target human ID
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub human_id: Option<String>,
134
135    /// Object being handed over
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub object_id: Option<String>,
138
139    /// Handover position
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub position: Option<Position3D>,
142
143    /// Whether human is ready
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub human_ready: Option<bool>,
146
147    /// Force threshold for release (N)
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub release_force_n: Option<f64>,
150
151    /// Detected pull force (N)
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub detected_force_n: Option<f64>,
154}
155
156/// Social navigation parameters.
157#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
158pub struct SocialNavigation {
159    /// Whether social navigation enabled
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub enabled: Option<bool>,
162
163    /// Personal space radius (m)
164    #[serde(skip_serializing_if = "Option::is_none")]
165    #[validate(range(min = 0.0))]
166    pub personal_space_m: Option<f64>,
167
168    /// Passing side preference
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub passing_side: Option<String>,
171
172    /// Whether yielding to human
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub is_yielding: Option<bool>,
175
176    /// Group detection
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub detected_groups: Option<u32>,
179}
180
181/// Detected gesture.
182#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
183pub struct DetectedGesture {
184    /// Gesture type
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub gesture_type: Option<String>,
187
188    /// Human ID
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub human_id: Option<String>,
191
192    /// Confidence (0-1)
193    #[serde(skip_serializing_if = "Option::is_none")]
194    #[validate(range(min = 0.0, max = 1.0))]
195    pub confidence: Option<f64>,
196
197    /// Timestamp
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub timestamp: Option<DateTime<Utc>>,
200}
201
202impl HriDomain {
203    /// Create with interaction state.
204    pub fn new(state: InteractionState) -> Self {
205        Self {
206            interaction_state: Some(state),
207            ..Default::default()
208        }
209    }
210
211    /// Add a tracked human.
212    pub fn with_human(mut self, human: TrackedHuman) -> Self {
213        let humans = self.tracked_humans.get_or_insert_with(Vec::new);
214        humans.push(human);
215        self.human_count = Some(humans.len() as u32);
216        self
217    }
218
219    /// Add voice interaction.
220    pub fn with_voice(mut self, voice: VoiceInteraction) -> Self {
221        self.voice = Some(voice);
222        self
223    }
224
225    /// Add handover.
226    pub fn with_handover(mut self, handover: Handover) -> Self {
227        self.handover = Some(handover);
228        self
229    }
230}
231
232impl TrackedHuman {
233    /// Create a tracked human at a distance.
234    pub fn at_distance(human_id: impl Into<String>, distance_m: f64) -> Self {
235        Self {
236            human_id: Some(human_id.into()),
237            distance_m: Some(distance_m),
238            first_detected: Some(Utc::now()),
239            ..Default::default()
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_hri_domain() {
250        let hri = HriDomain::new(InteractionState::HumanDetected)
251            .with_human(TrackedHuman::at_distance("human-001", 2.5));
252
253        assert_eq!(hri.human_count, Some(1));
254    }
255}