1use std::collections::BTreeMap;
9
10use crate::component::capability::CapabilityKind;
11use crate::identity::{CapabilityId, LinkId};
12
13#[derive(
15 phoxal_macros::DescribeWire, serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq,
16)]
17#[serde(deny_unknown_fields)]
18pub struct Simulation {
19 capabilities: BTreeMap<CapabilityId, Capability>,
20 links: BTreeMap<LinkId, Link>,
21}
22
23#[derive(
25 phoxal_macros::DescribeWire, serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq,
26)]
27#[serde(deny_unknown_fields)]
28pub struct Link {
29 contact_material: Option<String>,
30}
31
32impl Simulation {
33 pub(crate) fn new(
34 capabilities: BTreeMap<CapabilityId, Capability>,
35 links: BTreeMap<LinkId, Option<String>>,
36 ) -> Self {
37 Self {
38 capabilities,
39 links: links
40 .into_iter()
41 .map(|(id, contact_material)| (id, Link { contact_material }))
42 .collect(),
43 }
44 }
45
46 pub fn capabilities(&self) -> impl ExactSizeIterator<Item = (&CapabilityId, &Capability)> {
48 self.capabilities.iter()
49 }
50
51 #[must_use]
53 pub fn capability(&self, id: &str) -> Option<&Capability> {
54 self.capabilities.get(id)
55 }
56
57 pub fn links(&self) -> impl ExactSizeIterator<Item = (&LinkId, &Link)> {
59 self.links.iter()
60 }
61}
62
63impl Link {
64 #[must_use]
66 pub fn contact_material(&self) -> Option<&str> {
67 self.contact_material.as_deref()
68 }
69}
70
71#[derive(
73 phoxal_macros::DescribeWire, serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq,
74)]
75#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
76pub enum Capability {
77 Motor(Motor),
78 Encoder(Encoder),
79 Accelerometer(Accelerometer),
80 Gyroscope(Gyroscope),
81 Magnetometer(Magnetometer),
82 Imu(Imu),
83 Gnss(Gnss),
84 Camera(Camera),
85 Depth(Depth),
86 Range(Range),
87 Lidar(Lidar),
88 Mmwave(Mmwave),
89 Microphone(Microphone),
90 Speaker,
91 Battery,
92 Led,
93 EmergencyStop,
94}
95
96impl Capability {
97 #[must_use]
99 pub const fn kind(&self) -> CapabilityKind {
100 match self {
101 Self::Motor(_) => CapabilityKind::Motor,
102 Self::Encoder(_) => CapabilityKind::Encoder,
103 Self::Accelerometer(_) => CapabilityKind::Accelerometer,
104 Self::Gyroscope(_) => CapabilityKind::Gyroscope,
105 Self::Magnetometer(_) => CapabilityKind::Magnetometer,
106 Self::Imu(_) => CapabilityKind::Imu,
107 Self::Gnss(_) => CapabilityKind::Gnss,
108 Self::Camera(_) => CapabilityKind::Camera,
109 Self::Depth(_) => CapabilityKind::Depth,
110 Self::Range(_) => CapabilityKind::Range,
111 Self::Lidar(_) => CapabilityKind::Lidar,
112 Self::Mmwave(_) => CapabilityKind::Mmwave,
113 Self::Microphone(_) => CapabilityKind::Microphone,
114 Self::Speaker => CapabilityKind::Speaker,
115 Self::Battery => CapabilityKind::Battery,
116 Self::Led => CapabilityKind::Led,
117 Self::EmergencyStop => CapabilityKind::EmergencyStop,
118 }
119 }
120}
121
122#[derive(
123 phoxal_macros::DescribeWire,
124 serde::Serialize,
125 serde::Deserialize,
126 Debug,
127 Clone,
128 Copy,
129 PartialEq,
130 Eq,
131 Default,
132)]
133#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
134#[serde(rename_all = "snake_case")]
135pub enum ActuatorType {
136 #[default]
137 Velocity,
138 Position,
139 Torque,
140}
141
142#[derive(
143 phoxal_macros::DescribeWire,
144 serde::Serialize,
145 serde::Deserialize,
146 Debug,
147 Clone,
148 Copy,
149 PartialEq,
150 Eq,
151)]
152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
153#[serde(rename_all = "snake_case")]
154pub enum CameraProjection {
155 Planar,
156 Cylindrical,
157 Spherical,
158}
159
160#[derive(
161 phoxal_macros::DescribeWire,
162 serde::Serialize,
163 serde::Deserialize,
164 Debug,
165 Clone,
166 PartialEq,
167 Default,
168)]
169#[serde(deny_unknown_fields)]
170pub struct Motor {
171 pub actuator_type: ActuatorType,
172 pub acceleration_radps2: Option<f64>,
173 pub control_pid: Option<Vec<f64>>,
174 pub sampling_period_torque_hz: Option<f64>,
175}
176
177#[derive(
178 phoxal_macros::DescribeWire,
179 serde::Serialize,
180 serde::Deserialize,
181 Debug,
182 Clone,
183 Copy,
184 PartialEq,
185 Default,
186)]
187#[serde(deny_unknown_fields)]
188pub struct Encoder {
189 pub sampling_period_hz: f64,
190 pub resolution: Option<f64>,
191 pub noise: Option<f64>,
192}
193
194#[derive(
195 phoxal_macros::DescribeWire,
196 serde::Serialize,
197 serde::Deserialize,
198 Debug,
199 Clone,
200 PartialEq,
201 Default,
202)]
203#[serde(deny_unknown_fields)]
204pub struct Accelerometer {
205 pub sampling_period_hz: f64,
206 pub resolution: Option<f64>,
207 pub lookup_table: Option<Vec<Vec<f64>>>,
208}
209
210#[derive(
211 phoxal_macros::DescribeWire,
212 serde::Serialize,
213 serde::Deserialize,
214 Debug,
215 Clone,
216 PartialEq,
217 Default,
218)]
219#[serde(deny_unknown_fields)]
220pub struct Gyroscope {
221 pub sampling_period_hz: f64,
222 pub resolution: Option<f64>,
223 pub lookup_table: Option<Vec<Vec<f64>>>,
224}
225
226#[derive(
227 phoxal_macros::DescribeWire,
228 serde::Serialize,
229 serde::Deserialize,
230 Debug,
231 Clone,
232 PartialEq,
233 Default,
234)]
235#[serde(deny_unknown_fields)]
236pub struct Magnetometer {
237 pub sampling_period_hz: f64,
238 pub resolution: Option<f64>,
239 pub lookup_table: Option<Vec<Vec<f64>>>,
240}
241
242#[derive(
243 phoxal_macros::DescribeWire,
244 serde::Serialize,
245 serde::Deserialize,
246 Debug,
247 Clone,
248 Copy,
249 PartialEq,
250 Default,
251)]
252#[serde(deny_unknown_fields)]
253pub struct Imu {
254 pub sampling_period_hz: f64,
255 pub resolution: Option<f64>,
256 pub noise: Option<f64>,
257}
258
259#[derive(
260 phoxal_macros::DescribeWire,
261 serde::Serialize,
262 serde::Deserialize,
263 Debug,
264 Clone,
265 Copy,
266 PartialEq,
267 Default,
268)]
269#[serde(deny_unknown_fields)]
270pub struct Gnss {
271 pub sampling_period_hz: f64,
272 pub resolution: Option<f64>,
273 pub accuracy: Option<f64>,
274 pub noise_correlation: Option<f64>,
275 pub speed_resolution: Option<f64>,
276 pub speed_noise: Option<f64>,
277}
278
279#[derive(
280 phoxal_macros::DescribeWire,
281 serde::Serialize,
282 serde::Deserialize,
283 Debug,
284 Clone,
285 PartialEq,
286 Default,
287)]
288#[serde(deny_unknown_fields)]
289pub struct Camera {
290 pub sampling_period_hz: f64,
291 pub projection: Option<CameraProjection>,
292 pub near: Option<f64>,
293 pub far: Option<f64>,
294 pub exposure: Option<f64>,
295 pub anti_aliasing: Option<bool>,
296 pub ambient_occlusion_radius: Option<f64>,
297 pub bloom_threshold: Option<f64>,
298 pub noise: Option<f64>,
299 pub motion_blur: Option<f64>,
300 pub noise_mask_url: Option<String>,
301}
302
303#[derive(
304 phoxal_macros::DescribeWire,
305 serde::Serialize,
306 serde::Deserialize,
307 Debug,
308 Clone,
309 Copy,
310 PartialEq,
311 Default,
312)]
313#[serde(deny_unknown_fields)]
314pub struct Depth {
315 pub sampling_period_hz: f64,
316 pub noise: Option<f64>,
317 pub resolution: Option<f64>,
318 pub motion_blur: Option<f64>,
319}
320
321#[derive(
322 phoxal_macros::DescribeWire,
323 serde::Serialize,
324 serde::Deserialize,
325 Debug,
326 Clone,
327 Copy,
328 PartialEq,
329 Default,
330)]
331#[serde(deny_unknown_fields)]
332pub struct Range {
333 pub sampling_period_hz: f64,
334 pub noise: Option<f64>,
335 pub resolution: Option<f64>,
336}
337
338#[derive(
339 phoxal_macros::DescribeWire,
340 serde::Serialize,
341 serde::Deserialize,
342 Debug,
343 Clone,
344 Copy,
345 PartialEq,
346 Default,
347)]
348#[serde(deny_unknown_fields)]
349pub struct Lidar {
350 pub sampling_period_hz: f64,
351 pub noise: Option<f64>,
352 pub resolution: Option<f64>,
353}
354
355#[derive(
356 phoxal_macros::DescribeWire,
357 serde::Serialize,
358 serde::Deserialize,
359 Debug,
360 Clone,
361 PartialEq,
362 Default,
363)]
364#[serde(deny_unknown_fields)]
365pub struct Mmwave {
366 pub sampling_period_hz: f64,
367 pub noise: Option<f64>,
368 pub resolution: Option<f64>,
369 pub lookup_table: Option<Vec<Vec<f64>>>,
370}
371
372#[derive(
373 phoxal_macros::DescribeWire,
374 serde::Serialize,
375 serde::Deserialize,
376 Debug,
377 Clone,
378 Copy,
379 PartialEq,
380 Default,
381)]
382#[serde(deny_unknown_fields)]
383pub struct Microphone {
384 pub sampling_period_hz: f64,
385 pub aperture: Option<f64>,
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn a_simulation_keeps_its_bare_string_map_keys_on_the_wire() {
394 let simulation = Simulation::new(
397 [(
398 CapabilityId::new("spin").expect("a normalized capability id"),
399 Capability::Speaker,
400 )]
401 .into_iter()
402 .collect(),
403 [(LinkId::new("body"), Some("rubber".to_string()))]
404 .into_iter()
405 .collect(),
406 );
407 let json = serde_json::to_string(&simulation).expect("the simulation serializes");
408 assert_eq!(
409 json,
410 r#"{"capabilities":{"spin":{"kind":"speaker"}},"links":{"body":{"contact_material":"rubber"}}}"#
411 );
412 assert_eq!(
413 serde_json::from_str::<Simulation>(&json).expect("the simulation round-trips"),
414 simulation
415 );
416 }
417
418 #[test]
419 fn a_simulated_capability_reports_the_kind_it_models() {
420 assert_eq!(Capability::Led.kind(), CapabilityKind::Led);
421 assert_eq!(
422 Capability::Encoder(Encoder::default()).kind(),
423 CapabilityKind::Encoder
424 );
425 }
426}