1use std::ptr;
14
15use pamoja_core::{Actuator, Sensor};
16use pamoja_sim::{RecordingActuator, Replay, SimRobot, SimSensor};
17
18use crate::motion::{PamojaPose, PamojaTwist};
19use crate::{runtime, set_last_error, PamojaStatus};
20
21pub struct PamojaSimSensor {
23 inner: SimSensor,
24}
25
26pub struct PamojaReplay {
28 inner: Replay,
29}
30
31pub struct PamojaRecordingActuator {
33 inner: RecordingActuator<f32>,
34}
35
36pub struct PamojaSimRobot {
38 inner: SimRobot,
39}
40
41#[no_mangle]
54pub extern "C" fn pamoja_sim_sensor_new(
55 baseline: f32,
56 drift_per_read: f32,
57 noise: f32,
58 seed: u32,
59) -> *mut PamojaSimSensor {
60 let mut sensor = SimSensor::new(baseline);
61 if drift_per_read != 0.0 {
62 sensor = sensor.with_drift(drift_per_read);
63 }
64 if noise != 0.0 {
65 sensor = sensor.with_noise(noise);
66 }
67 if seed != 0 {
68 sensor = sensor.with_seed(seed);
69 }
70 Box::into_raw(Box::new(PamojaSimSensor { inner: sensor }))
71}
72
73#[no_mangle]
89pub unsafe extern "C" fn pamoja_sim_sensor_read(
90 sensor: *mut PamojaSimSensor,
91 out_reading: *mut f32,
92) -> PamojaStatus {
93 if sensor.is_null() || out_reading.is_null() {
94 set_last_error("sensor and out_reading must not be null".to_owned());
95 return PamojaStatus::InvalidArgument;
96 }
97 match runtime().block_on((*sensor).inner.read()) {
98 Ok(reading) => {
99 *out_reading = reading;
100 PamojaStatus::Ok
101 }
102 Err(error) => fail(error),
103 }
104}
105
106#[no_mangle]
115pub unsafe extern "C" fn pamoja_sim_sensor_free(sensor: *mut PamojaSimSensor) {
116 if !sensor.is_null() {
117 drop(Box::from_raw(sensor));
118 }
119}
120
121#[no_mangle]
142pub unsafe extern "C" fn pamoja_replay_new(
143 readings: *const f32,
144 count: usize,
145 repeating: bool,
146) -> *mut PamojaReplay {
147 if count != 0 && readings.is_null() {
148 set_last_error("readings must not be null when count is non-zero".to_owned());
149 return ptr::null_mut();
150 }
151 let values = if count == 0 {
152 Vec::new()
153 } else {
154 std::slice::from_raw_parts(readings, count).to_vec()
155 };
156 let inner = if repeating {
157 Replay::repeating(values)
158 } else {
159 Replay::new(values)
160 };
161 Box::into_raw(Box::new(PamojaReplay { inner }))
162}
163
164#[no_mangle]
181pub unsafe extern "C" fn pamoja_replay_read(
182 replay: *mut PamojaReplay,
183 out_reading: *mut f32,
184) -> PamojaStatus {
185 if replay.is_null() || out_reading.is_null() {
186 set_last_error("replay and out_reading must not be null".to_owned());
187 return PamojaStatus::InvalidArgument;
188 }
189 match runtime().block_on((*replay).inner.read()) {
190 Ok(reading) => {
191 *out_reading = reading;
192 PamojaStatus::Ok
193 }
194 Err(error) => fail(error),
195 }
196}
197
198#[no_mangle]
207pub unsafe extern "C" fn pamoja_replay_free(replay: *mut PamojaReplay) {
208 if !replay.is_null() {
209 drop(Box::from_raw(replay));
210 }
211}
212
213#[no_mangle]
219pub extern "C" fn pamoja_recording_actuator_new() -> *mut PamojaRecordingActuator {
220 Box::into_raw(Box::new(PamojaRecordingActuator {
221 inner: RecordingActuator::new(),
222 }))
223}
224
225#[no_mangle]
240pub unsafe extern "C" fn pamoja_recording_actuator_apply(
241 actuator: *mut PamojaRecordingActuator,
242 command: f32,
243) -> PamojaStatus {
244 if actuator.is_null() {
245 set_last_error("actuator must not be null".to_owned());
246 return PamojaStatus::InvalidArgument;
247 }
248 match runtime().block_on((*actuator).inner.apply(command)) {
249 Ok(()) => PamojaStatus::Ok,
250 Err(error) => fail(error),
251 }
252}
253
254#[no_mangle]
269pub unsafe extern "C" fn pamoja_recording_actuator_len(
270 actuator: *const PamojaRecordingActuator,
271) -> usize {
272 if actuator.is_null() {
273 return 0;
274 }
275 (*actuator).inner.log().len()
276}
277
278#[no_mangle]
296pub unsafe extern "C" fn pamoja_recording_actuator_commands(
297 actuator: *const PamojaRecordingActuator,
298 out_commands: *mut f32,
299 capacity: usize,
300) -> usize {
301 if actuator.is_null() || capacity == 0 || out_commands.is_null() {
302 return 0;
303 }
304 let commands = (*actuator).inner.log().commands();
305 let written = commands.len().min(capacity);
306 ptr::copy_nonoverlapping(commands.as_ptr(), out_commands, written);
307 written
308}
309
310#[no_mangle]
319pub unsafe extern "C" fn pamoja_recording_actuator_free(actuator: *mut PamojaRecordingActuator) {
320 if !actuator.is_null() {
321 drop(Box::from_raw(actuator));
322 }
323}
324
325#[no_mangle]
336pub extern "C" fn pamoja_sim_robot_new(start: PamojaPose, dt: f32) -> *mut PamojaSimRobot {
337 Box::into_raw(Box::new(PamojaSimRobot {
338 inner: SimRobot::starting_at(start.into(), dt),
339 }))
340}
341
342#[no_mangle]
357pub unsafe extern "C" fn pamoja_sim_robot_apply(
358 robot: *mut PamojaSimRobot,
359 command: PamojaTwist,
360) -> PamojaStatus {
361 if robot.is_null() {
362 set_last_error("robot must not be null".to_owned());
363 return PamojaStatus::InvalidArgument;
364 }
365 match runtime().block_on((*robot).inner.apply(command.into())) {
366 Ok(()) => PamojaStatus::Ok,
367 Err(error) => fail(error),
368 }
369}
370
371#[no_mangle]
385pub unsafe extern "C" fn pamoja_sim_robot_pose(robot: *const PamojaSimRobot) -> PamojaPose {
386 if robot.is_null() {
387 return PamojaPose {
388 x: 0.0,
389 y: 0.0,
390 theta: 0.0,
391 };
392 }
393 (*robot).inner.pose().into()
394}
395
396#[no_mangle]
405pub unsafe extern "C" fn pamoja_sim_robot_free(robot: *mut PamojaSimRobot) {
406 if !robot.is_null() {
407 drop(Box::from_raw(robot));
408 }
409}
410
411fn fail(error: pamoja_core::Error) -> PamojaStatus {
413 let status = PamojaStatus::from_error(&error);
414 set_last_error(error.to_string());
415 status
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn a_seeded_sensor_repeats_its_run() {
424 unsafe {
425 let first = pamoja_sim_sensor_new(20.0, 0.5, 1.0, 42);
426 let second = pamoja_sim_sensor_new(20.0, 0.5, 1.0, 42);
427
428 for _ in 0..5 {
429 let (mut a, mut b) = (0.0, 0.0);
430 assert_eq!(pamoja_sim_sensor_read(first, &mut a), PamojaStatus::Ok);
431 assert_eq!(pamoja_sim_sensor_read(second, &mut b), PamojaStatus::Ok);
432 assert_eq!(a, b, "the same seed gives the same readings");
433 }
434
435 pamoja_sim_sensor_free(first);
436 pamoja_sim_sensor_free(second);
437 }
438 }
439
440 #[test]
441 fn a_replay_reads_back_what_was_recorded() {
442 unsafe {
443 let readings = [21.0f32, 21.5, 22.0];
444 let replay = pamoja_replay_new(readings.as_ptr(), readings.len(), true);
445
446 for _ in 0..2 {
448 for want in readings {
449 let mut got = 0.0;
450 assert_eq!(pamoja_replay_read(replay, &mut got), PamojaStatus::Ok);
451 assert_eq!(got, want);
452 }
453 }
454
455 pamoja_replay_free(replay);
456 }
457 }
458
459 #[test]
460 fn an_actuator_records_what_it_was_told() {
461 unsafe {
462 let actuator = pamoja_recording_actuator_new();
463 for command in [0.0f32, 0.5, 1.0] {
464 assert_eq!(
465 pamoja_recording_actuator_apply(actuator, command),
466 PamojaStatus::Ok
467 );
468 }
469 assert_eq!(pamoja_recording_actuator_len(actuator), 3);
470
471 let mut commands = [0.0f32; 3];
472 assert_eq!(
473 pamoja_recording_actuator_commands(actuator, commands.as_mut_ptr(), 3),
474 3
475 );
476 assert_eq!(commands, [0.0, 0.5, 1.0]);
477
478 let mut room_for_one = [0.0f32; 1];
480 assert_eq!(
481 pamoja_recording_actuator_commands(actuator, room_for_one.as_mut_ptr(), 1),
482 1
483 );
484 assert_eq!(room_for_one, [0.0]);
485
486 pamoja_recording_actuator_free(actuator);
487 }
488 }
489
490 #[test]
491 fn a_robot_driven_forward_ends_up_ahead() {
492 unsafe {
493 let start = PamojaPose {
494 x: 0.0,
495 y: 0.0,
496 theta: 0.0,
497 };
498 let robot = pamoja_sim_robot_new(start, 1.0);
499
500 let forward = PamojaTwist {
501 vx: 1.0,
502 vy: 0.0,
503 omega: 0.0,
504 };
505 assert_eq!(pamoja_sim_robot_apply(robot, forward), PamojaStatus::Ok);
506
507 let pose = pamoja_sim_robot_pose(robot);
508 assert!(
509 (pose.x - 1.0).abs() < 1e-5,
510 "one second at one meter a second"
511 );
512 assert!(pose.y.abs() < 1e-5);
513
514 pamoja_sim_robot_free(robot);
515 }
516 }
517
518 #[test]
519 fn a_null_handle_is_refused_rather_than_dereferenced() {
520 unsafe {
521 assert_eq!(
522 pamoja_sim_sensor_read(ptr::null_mut(), ptr::null_mut()),
523 PamojaStatus::InvalidArgument
524 );
525 assert_eq!(pamoja_recording_actuator_len(ptr::null()), 0);
526 assert_eq!(pamoja_sim_robot_pose(ptr::null()).x, 0.0);
527 pamoja_sim_sensor_free(ptr::null_mut());
528 pamoja_replay_free(ptr::null_mut());
529 pamoja_recording_actuator_free(ptr::null_mut());
530 pamoja_sim_robot_free(ptr::null_mut());
531 }
532 }
533}