1use std::ptr;
14
15use pamoja_core::{Actuator, Sensor};
16use pamoja_kit::{Pose, Twist};
17use pamoja_sim::{RecordingActuator, Replay, SimRobot, SimSensor};
18
19use crate::{runtime, set_last_error, PamojaStatus};
20
21#[repr(C)]
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct PamojaPose {
25 pub x: f32,
27 pub y: f32,
29 pub theta: f32,
31}
32
33#[repr(C)]
35#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct PamojaTwist {
37 pub vx: f32,
39 pub vy: f32,
41 pub omega: f32,
43}
44
45pub struct PamojaSimSensor {
47 inner: SimSensor,
48}
49
50pub struct PamojaReplay {
52 inner: Replay,
53}
54
55pub struct PamojaRecordingActuator {
57 inner: RecordingActuator<f32>,
58}
59
60pub struct PamojaSimRobot {
62 inner: SimRobot,
63}
64
65#[no_mangle]
78pub extern "C" fn pamoja_sim_sensor_new(
79 baseline: f32,
80 drift_per_read: f32,
81 noise: f32,
82 seed: u32,
83) -> *mut PamojaSimSensor {
84 let mut sensor = SimSensor::new(baseline);
85 if drift_per_read != 0.0 {
86 sensor = sensor.with_drift(drift_per_read);
87 }
88 if noise != 0.0 {
89 sensor = sensor.with_noise(noise);
90 }
91 if seed != 0 {
92 sensor = sensor.with_seed(seed);
93 }
94 Box::into_raw(Box::new(PamojaSimSensor { inner: sensor }))
95}
96
97#[no_mangle]
113pub unsafe extern "C" fn pamoja_sim_sensor_read(
114 sensor: *mut PamojaSimSensor,
115 out_reading: *mut f32,
116) -> PamojaStatus {
117 if sensor.is_null() || out_reading.is_null() {
118 set_last_error("sensor and out_reading must not be null".to_owned());
119 return PamojaStatus::InvalidArgument;
120 }
121 match runtime().block_on((*sensor).inner.read()) {
122 Ok(reading) => {
123 *out_reading = reading;
124 PamojaStatus::Ok
125 }
126 Err(error) => fail(error),
127 }
128}
129
130#[no_mangle]
139pub unsafe extern "C" fn pamoja_sim_sensor_free(sensor: *mut PamojaSimSensor) {
140 if !sensor.is_null() {
141 drop(Box::from_raw(sensor));
142 }
143}
144
145#[no_mangle]
166pub unsafe extern "C" fn pamoja_replay_new(
167 readings: *const f32,
168 count: usize,
169 repeating: bool,
170) -> *mut PamojaReplay {
171 if count != 0 && readings.is_null() {
172 set_last_error("readings must not be null when count is non-zero".to_owned());
173 return ptr::null_mut();
174 }
175 let values = if count == 0 {
176 Vec::new()
177 } else {
178 std::slice::from_raw_parts(readings, count).to_vec()
179 };
180 let inner = if repeating {
181 Replay::repeating(values)
182 } else {
183 Replay::new(values)
184 };
185 Box::into_raw(Box::new(PamojaReplay { inner }))
186}
187
188#[no_mangle]
204pub unsafe extern "C" fn pamoja_replay_read(
205 replay: *mut PamojaReplay,
206 out_reading: *mut f32,
207) -> PamojaStatus {
208 if replay.is_null() || out_reading.is_null() {
209 set_last_error("replay and out_reading must not be null".to_owned());
210 return PamojaStatus::InvalidArgument;
211 }
212 match runtime().block_on((*replay).inner.read()) {
213 Ok(reading) => {
214 *out_reading = reading;
215 PamojaStatus::Ok
216 }
217 Err(error) => fail(error),
218 }
219}
220
221#[no_mangle]
230pub unsafe extern "C" fn pamoja_replay_free(replay: *mut PamojaReplay) {
231 if !replay.is_null() {
232 drop(Box::from_raw(replay));
233 }
234}
235
236#[no_mangle]
242pub extern "C" fn pamoja_recording_actuator_new() -> *mut PamojaRecordingActuator {
243 Box::into_raw(Box::new(PamojaRecordingActuator {
244 inner: RecordingActuator::new(),
245 }))
246}
247
248#[no_mangle]
263pub unsafe extern "C" fn pamoja_recording_actuator_apply(
264 actuator: *mut PamojaRecordingActuator,
265 command: f32,
266) -> PamojaStatus {
267 if actuator.is_null() {
268 set_last_error("actuator must not be null".to_owned());
269 return PamojaStatus::InvalidArgument;
270 }
271 match runtime().block_on((*actuator).inner.apply(command)) {
272 Ok(()) => PamojaStatus::Ok,
273 Err(error) => fail(error),
274 }
275}
276
277#[no_mangle]
292pub unsafe extern "C" fn pamoja_recording_actuator_len(
293 actuator: *const PamojaRecordingActuator,
294) -> usize {
295 if actuator.is_null() {
296 return 0;
297 }
298 (*actuator).inner.log().len()
299}
300
301#[no_mangle]
319pub unsafe extern "C" fn pamoja_recording_actuator_commands(
320 actuator: *const PamojaRecordingActuator,
321 out_commands: *mut f32,
322 capacity: usize,
323) -> usize {
324 if actuator.is_null() || capacity == 0 || out_commands.is_null() {
325 return 0;
326 }
327 let commands = (*actuator).inner.log().commands();
328 let written = commands.len().min(capacity);
329 ptr::copy_nonoverlapping(commands.as_ptr(), out_commands, written);
330 written
331}
332
333#[no_mangle]
342pub unsafe extern "C" fn pamoja_recording_actuator_free(actuator: *mut PamojaRecordingActuator) {
343 if !actuator.is_null() {
344 drop(Box::from_raw(actuator));
345 }
346}
347
348#[no_mangle]
359pub extern "C" fn pamoja_sim_robot_new(start: PamojaPose, dt: f32) -> *mut PamojaSimRobot {
360 Box::into_raw(Box::new(PamojaSimRobot {
361 inner: SimRobot::starting_at(Pose::new(start.x, start.y, start.theta), dt),
362 }))
363}
364
365#[no_mangle]
380pub unsafe extern "C" fn pamoja_sim_robot_apply(
381 robot: *mut PamojaSimRobot,
382 command: PamojaTwist,
383) -> PamojaStatus {
384 if robot.is_null() {
385 set_last_error("robot must not be null".to_owned());
386 return PamojaStatus::InvalidArgument;
387 }
388 let twist = Twist::new(command.vx, command.vy, command.omega);
389 match runtime().block_on((*robot).inner.apply(twist)) {
390 Ok(()) => PamojaStatus::Ok,
391 Err(error) => fail(error),
392 }
393}
394
395#[no_mangle]
409pub unsafe extern "C" fn pamoja_sim_robot_pose(robot: *const PamojaSimRobot) -> PamojaPose {
410 if robot.is_null() {
411 return PamojaPose {
412 x: 0.0,
413 y: 0.0,
414 theta: 0.0,
415 };
416 }
417 let pose = (*robot).inner.pose();
418 PamojaPose {
419 x: pose.x,
420 y: pose.y,
421 theta: pose.theta,
422 }
423}
424
425#[no_mangle]
434pub unsafe extern "C" fn pamoja_sim_robot_free(robot: *mut PamojaSimRobot) {
435 if !robot.is_null() {
436 drop(Box::from_raw(robot));
437 }
438}
439
440fn fail(error: pamoja_core::Error) -> PamojaStatus {
442 let status = PamojaStatus::from_error(&error);
443 set_last_error(error.to_string());
444 status
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn a_seeded_sensor_repeats_its_run() {
453 unsafe {
454 let first = pamoja_sim_sensor_new(20.0, 0.5, 1.0, 42);
455 let second = pamoja_sim_sensor_new(20.0, 0.5, 1.0, 42);
456
457 for _ in 0..5 {
458 let (mut a, mut b) = (0.0, 0.0);
459 assert_eq!(pamoja_sim_sensor_read(first, &mut a), PamojaStatus::Ok);
460 assert_eq!(pamoja_sim_sensor_read(second, &mut b), PamojaStatus::Ok);
461 assert_eq!(a, b, "the same seed gives the same readings");
462 }
463
464 pamoja_sim_sensor_free(first);
465 pamoja_sim_sensor_free(second);
466 }
467 }
468
469 #[test]
470 fn a_replay_reads_back_what_was_recorded() {
471 unsafe {
472 let readings = [21.0f32, 21.5, 22.0];
473 let replay = pamoja_replay_new(readings.as_ptr(), readings.len(), true);
474
475 for _ in 0..2 {
477 for want in readings {
478 let mut got = 0.0;
479 assert_eq!(pamoja_replay_read(replay, &mut got), PamojaStatus::Ok);
480 assert_eq!(got, want);
481 }
482 }
483
484 pamoja_replay_free(replay);
485 }
486 }
487
488 #[test]
489 fn an_actuator_records_what_it_was_told() {
490 unsafe {
491 let actuator = pamoja_recording_actuator_new();
492 for command in [0.0f32, 0.5, 1.0] {
493 assert_eq!(
494 pamoja_recording_actuator_apply(actuator, command),
495 PamojaStatus::Ok
496 );
497 }
498 assert_eq!(pamoja_recording_actuator_len(actuator), 3);
499
500 let mut commands = [0.0f32; 3];
501 assert_eq!(
502 pamoja_recording_actuator_commands(actuator, commands.as_mut_ptr(), 3),
503 3
504 );
505 assert_eq!(commands, [0.0, 0.5, 1.0]);
506
507 let mut room_for_one = [0.0f32; 1];
509 assert_eq!(
510 pamoja_recording_actuator_commands(actuator, room_for_one.as_mut_ptr(), 1),
511 1
512 );
513 assert_eq!(room_for_one, [0.0]);
514
515 pamoja_recording_actuator_free(actuator);
516 }
517 }
518
519 #[test]
520 fn a_robot_driven_forward_ends_up_ahead() {
521 unsafe {
522 let start = PamojaPose {
523 x: 0.0,
524 y: 0.0,
525 theta: 0.0,
526 };
527 let robot = pamoja_sim_robot_new(start, 1.0);
528
529 let forward = PamojaTwist {
530 vx: 1.0,
531 vy: 0.0,
532 omega: 0.0,
533 };
534 assert_eq!(pamoja_sim_robot_apply(robot, forward), PamojaStatus::Ok);
535
536 let pose = pamoja_sim_robot_pose(robot);
537 assert!(
538 (pose.x - 1.0).abs() < 1e-5,
539 "one second at one metre a second"
540 );
541 assert!(pose.y.abs() < 1e-5);
542
543 pamoja_sim_robot_free(robot);
544 }
545 }
546
547 #[test]
548 fn a_null_handle_is_refused_rather_than_dereferenced() {
549 unsafe {
550 assert_eq!(
551 pamoja_sim_sensor_read(ptr::null_mut(), ptr::null_mut()),
552 PamojaStatus::InvalidArgument
553 );
554 assert_eq!(pamoja_recording_actuator_len(ptr::null()), 0);
555 assert_eq!(pamoja_sim_robot_pose(ptr::null()).x, 0.0);
556 pamoja_sim_sensor_free(ptr::null_mut());
557 pamoja_replay_free(ptr::null_mut());
558 pamoja_recording_actuator_free(ptr::null_mut());
559 pamoja_sim_robot_free(ptr::null_mut());
560 }
561 }
562}