1use std::fmt::{Debug, Display};
19use std::panic::{catch_unwind, AssertUnwindSafe};
20
21use pamoja_actuators::pca9685::{self, Outputs, Pwm};
22use pamoja_actuators::DriverError;
23use pamoja_hal::bus::{BusDelay, I2cBus};
24
25use crate::actuators::PamojaPwm;
26use crate::hal::{PamojaI2cBus, PamojaI2cPart};
27use crate::{set_last_error, PamojaStatus};
28
29type Pca9685Driver = pca9685::Pca9685<I2cBus, BusDelay>;
30
31pub const PAMOJA_PCA9685_DEFAULT_FREQUENCY_HZ: u32 = 200;
34
35#[repr(C)]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct PamojaPca9685Outputs {
39 pub totem_pole: u8,
41 pub inverted: u8,
43 pub change_on_ack: u8,
46}
47
48#[repr(C)]
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct PamojaPca9685Settings {
52 pub frequency_hz: u32,
55 pub oscillator_hz: u32,
58 pub outputs: PamojaPca9685Outputs,
60}
61
62pub struct PamojaPca9685 {
64 driver: Pca9685Driver,
65}
66
67impl From<PamojaPca9685Outputs> for Outputs {
68 fn from(value: PamojaPca9685Outputs) -> Self {
69 Outputs {
70 totem_pole: value.totem_pole != 0,
71 inverted: value.inverted != 0,
72 change_on_ack: value.change_on_ack != 0,
73 }
74 }
75}
76
77impl From<Outputs> for PamojaPca9685Outputs {
78 fn from(value: Outputs) -> Self {
79 PamojaPca9685Outputs {
80 totem_pole: u8::from(value.totem_pole),
81 inverted: u8::from(value.inverted),
82 change_on_ack: u8::from(value.change_on_ack),
83 }
84 }
85}
86
87#[no_mangle]
94pub extern "C" fn pamoja_pca9685_settings_default() -> PamojaPca9685Settings {
95 PamojaPca9685Settings {
96 frequency_hz: PAMOJA_PCA9685_DEFAULT_FREQUENCY_HZ,
97 oscillator_hz: pca9685::INTERNAL_OSC_HZ,
98 outputs: Outputs::default().into(),
99 }
100}
101
102#[no_mangle]
122pub unsafe extern "C" fn pamoja_pca9685_new(
123 bus: *const PamojaI2cBus,
124 address: u8,
125 settings: PamojaPca9685Settings,
126 out_driver: *mut *mut PamojaPca9685,
127) -> PamojaStatus {
128 if out_driver.is_null() {
129 set_last_error("out_driver must not be null".to_owned());
130 return PamojaStatus::InvalidArgument;
131 }
132 *out_driver = std::ptr::null_mut();
133 let Some(bus) = bus.as_ref() else {
134 set_last_error("bus must not be null".to_owned());
135 return PamojaStatus::InvalidArgument;
136 };
137 let bus = bus.bus.clone();
138 let delay = bus.delay();
139 let driver = pca9685::Pca9685::new(bus, address, delay)
140 .with_frequency(settings.frequency_hz)
141 .with_oscillator(settings.oscillator_hz)
142 .with_outputs(settings.outputs.into());
143 *out_driver = Box::into_raw(Box::new(PamojaPca9685 { driver }));
144 PamojaStatus::Ok
145}
146
147#[no_mangle]
159pub unsafe extern "C" fn pamoja_pca9685_init(driver: *mut PamojaPca9685) -> PamojaStatus {
160 run(driver, |held| held.driver.init())
161}
162
163#[no_mangle]
180pub unsafe extern "C" fn pamoja_pca9685_set_channel(
181 driver: *mut PamojaPca9685,
182 channel: u8,
183 pwm: PamojaPwm,
184) -> PamojaStatus {
185 run(driver, |held| held.driver.set_channel(channel, pwm_of(pwm)))
186}
187
188#[no_mangle]
206pub unsafe extern "C" fn pamoja_pca9685_channel(
207 driver: *mut PamojaPca9685,
208 channel: u8,
209 out_pwm: *mut PamojaPwm,
210) -> PamojaStatus {
211 let Some(out) = out_pwm.as_mut() else {
212 set_last_error("out_pwm must not be null".to_owned());
213 return PamojaStatus::InvalidArgument;
214 };
215 run(driver, |held| {
216 *out = held.driver.channel(channel)?.into();
217 Ok(())
218 })
219}
220
221#[no_mangle]
236pub unsafe extern "C" fn pamoja_pca9685_set_all(
237 driver: *mut PamojaPca9685,
238 pwm: PamojaPwm,
239) -> PamojaStatus {
240 run(driver, |held| held.driver.set_all(pwm_of(pwm)))
241}
242
243#[no_mangle]
254pub unsafe extern "C" fn pamoja_pca9685_sleep(driver: *mut PamojaPca9685) -> PamojaStatus {
255 run(driver, |held| held.driver.sleep())
256}
257
258#[no_mangle]
269pub unsafe extern "C" fn pamoja_pca9685_wake(driver: *mut PamojaPca9685) -> PamojaStatus {
270 run(driver, |held| held.driver.wake())
271}
272
273#[no_mangle]
285pub unsafe extern "C" fn pamoja_pca9685_software_reset(driver: *mut PamojaPca9685) -> PamojaStatus {
286 run(driver, |held| held.driver.software_reset())
287}
288
289#[no_mangle]
299pub unsafe extern "C" fn pamoja_pca9685_prescale(driver: *const PamojaPca9685) -> u8 {
300 driver.as_ref().map_or(0, |held| held.driver.prescale())
301}
302
303#[no_mangle]
313pub unsafe extern "C" fn pamoja_pca9685_frequency(driver: *const PamojaPca9685) -> f32 {
314 driver.as_ref().map_or(0.0, |held| held.driver.frequency())
315}
316
317#[no_mangle]
323pub unsafe extern "C" fn pamoja_pca9685_free(driver: *mut PamojaPca9685) {
324 if !driver.is_null() {
325 drop(Box::from_raw(driver));
326 }
327}
328
329#[no_mangle]
341pub extern "C" fn pamoja_pca9685_sim_part(address: u8) -> *mut PamojaI2cPart {
342 PamojaI2cPart::into_raw(pca9685::sim::part(address))
343}
344
345fn pwm_of(pwm: PamojaPwm) -> Pwm {
346 Pwm::from_bytes(&[pwm.on_low, pwm.on_high, pwm.off_low, pwm.off_high])
347}
348
349unsafe fn run<E: Debug + Display>(
355 driver: *mut PamojaPca9685,
356 call: impl FnOnce(&mut PamojaPca9685) -> Result<(), DriverError<E>>,
357) -> PamojaStatus {
358 let Some(held) = driver.as_mut() else {
359 set_last_error("driver must not be null".to_owned());
360 return PamojaStatus::InvalidArgument;
361 };
362 match catch_unwind(AssertUnwindSafe(|| call(held))) {
363 Ok(Ok(())) => PamojaStatus::Ok,
364 Ok(Err(DriverError::Command(reason))) => {
365 set_last_error(reason.to_owned());
366 PamojaStatus::InvalidArgument
367 }
368 Ok(Err(DriverError::Bus(error))) => {
369 set_last_error(error.to_string());
370 PamojaStatus::Io
371 }
372 Err(_) => {
373 set_last_error("panic at the FFI boundary".to_owned());
374 PamojaStatus::Panic
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use crate::hal::{
383 pamoja_i2c_bus_attach, pamoja_i2c_bus_free, pamoja_i2c_bus_part, pamoja_i2c_bus_simulated,
384 pamoja_i2c_bus_waited_micros, pamoja_i2c_part_free, pamoja_i2c_part_register,
385 };
386 use crate::{actuators, pamoja_last_error_message};
387 use std::ffi::CStr;
388 use std::ptr;
389
390 fn last_error() -> String {
391 unsafe { CStr::from_ptr(pamoja_last_error_message()) }
392 .to_string_lossy()
393 .into_owned()
394 }
395
396 #[test]
397 fn a_driver_programs_a_simulated_part_as_the_datasheet_asks() {
398 unsafe {
399 let part = pamoja_pca9685_sim_part(0x40);
400 let bus = pamoja_i2c_bus_simulated();
401 assert_eq!(pamoja_i2c_bus_attach(bus, part), PamojaStatus::Ok);
402 pamoja_i2c_part_free(part);
403
404 let mut settings = pamoja_pca9685_settings_default();
405 assert_eq!(settings.frequency_hz, 200);
406 assert_eq!(settings.outputs.totem_pole, 1);
407 settings.frequency_hz = 50;
408 let mut driver = ptr::null_mut();
409 assert_eq!(
410 pamoja_pca9685_new(bus, 0x40, settings, &mut driver),
411 PamojaStatus::Ok
412 );
413 assert_eq!(pamoja_pca9685_prescale(driver), 121);
414 assert!((pamoja_pca9685_frequency(driver) - 50.0).abs() < 0.1);
415
416 let servo = actuators::pamoja_pwm_servo(1_500, 50);
417 assert_eq!(
418 pamoja_pca9685_set_channel(driver, 0, servo),
419 PamojaStatus::Ok
420 );
421 assert_eq!(
422 pamoja_pca9685_set_channel(driver, 16, servo),
423 PamojaStatus::InvalidArgument
424 );
425 assert!(
426 last_error().contains("sixteen channels"),
427 "{}",
428 last_error()
429 );
430 assert_eq!(pamoja_i2c_bus_waited_micros(bus), 500);
431
432 let held = pamoja_i2c_bus_part(bus, 0x40);
433 assert_eq!(
434 pamoja_i2c_part_register(held, pca9685::register::PRE_SCALE),
435 121
436 );
437 assert_eq!(
438 pamoja_i2c_part_register(held, pca9685::register::MODE1),
439 0x20
440 );
441 let loaded = PamojaPwm {
442 on_low: pamoja_i2c_part_register(held, 0x06),
443 on_high: pamoja_i2c_part_register(held, 0x07),
444 off_low: pamoja_i2c_part_register(held, 0x08),
445 off_high: pamoja_i2c_part_register(held, 0x09),
446 };
447 assert_eq!(loaded, servo);
448 pamoja_i2c_part_free(held);
449
450 let mut read_back = actuators::pamoja_pwm_full_on();
451 assert_eq!(
452 pamoja_pca9685_channel(driver, 0, &mut read_back),
453 PamojaStatus::Ok
454 );
455 assert_eq!(read_back, servo, "the driver reads the channel back");
456 assert_eq!(
457 pamoja_pca9685_channel(driver, 16, &mut read_back),
458 PamojaStatus::InvalidArgument
459 );
460 assert_eq!(
461 pamoja_pca9685_channel(driver, 0, ptr::null_mut()),
462 PamojaStatus::InvalidArgument
463 );
464
465 assert_eq!(
466 pamoja_pca9685_set_all(driver, actuators::pamoja_pwm_full_off()),
467 PamojaStatus::Ok
468 );
469 let held = pamoja_i2c_bus_part(bus, 0x40);
470 assert_eq!(
471 pamoja_i2c_part_register(held, 0x09),
472 0x10,
473 "channel 0 off again"
474 );
475 pamoja_i2c_part_free(held);
476
477 assert_eq!(pamoja_pca9685_software_reset(driver), PamojaStatus::Io);
478 assert!(last_error().contains("0x00"), "{}", last_error());
479
480 pamoja_pca9685_free(driver);
481 pamoja_i2c_bus_free(bus);
482 }
483 }
484
485 #[test]
486 fn a_null_driver_is_refused() {
487 unsafe {
488 assert_eq!(
489 pamoja_pca9685_init(ptr::null_mut()),
490 PamojaStatus::InvalidArgument
491 );
492 assert_eq!(pamoja_pca9685_prescale(ptr::null()), 0);
493 let mut driver = ptr::null_mut();
494 assert_eq!(
495 pamoja_pca9685_new(
496 ptr::null(),
497 0x40,
498 pamoja_pca9685_settings_default(),
499 &mut driver
500 ),
501 PamojaStatus::InvalidArgument
502 );
503 assert!(driver.is_null());
504 }
505 }
506}