1use pamoja_actuators::{pca9685, stepper};
14
15use crate::{set_last_error, PamojaStatus};
16
17pub const PAMOJA_PCA9685_INTERNAL_OSC_HZ: u32 = 25_000_000;
19
20pub const PAMOJA_PCA9685_CHANNELS: u8 = 16;
22
23pub const PAMOJA_PCA9685_COUNTS: u16 = 4096;
25
26pub const PAMOJA_PCA9685_DEFAULT_ADDRESS: u8 = 0x40;
28
29pub const PAMOJA_PCA9685_OSCILLATOR_STARTUP_MICROS: u32 = 500;
31
32pub const PAMOJA_PCA9685_REGISTER_MODE1: u8 = 0x00;
34pub const PAMOJA_PCA9685_REGISTER_MODE2: u8 = 0x01;
36pub const PAMOJA_PCA9685_REGISTER_LED0_ON_L: u8 = 0x06;
38pub const PAMOJA_PCA9685_REGISTER_ALL_LED_ON_L: u8 = 0xFA;
40pub const PAMOJA_PCA9685_REGISTER_PRE_SCALE: u8 = 0xFE;
42
43pub const PAMOJA_PCA9685_MODE1_RESTART: u8 = 0x80;
45pub const PAMOJA_PCA9685_MODE1_EXTCLK: u8 = 0x40;
47pub const PAMOJA_PCA9685_MODE1_AUTO_INCREMENT: u8 = 0x20;
49pub const PAMOJA_PCA9685_MODE1_SLEEP: u8 = 0x10;
51
52pub const PAMOJA_PCA9685_MODE1_RESET: u8 = 0x11;
54pub const PAMOJA_PCA9685_MODE2_RESET: u8 = 0x04;
56pub const PAMOJA_PCA9685_PRE_SCALE_RESET: u8 = 0x1E;
58pub const PAMOJA_PCA9685_PRE_SCALE_MIN: u8 = 3;
60
61pub const PAMOJA_STEPPER_DEFAULT_STEP_MICROS: u32 = 2_000;
64
65pub const PAMOJA_STEPPER_DEFAULT_PULSE_MICROS: u32 = 10;
68
69const _: () = assert!(PAMOJA_PCA9685_INTERNAL_OSC_HZ == pca9685::INTERNAL_OSC_HZ);
72const _: () = assert!(PAMOJA_PCA9685_CHANNELS == pca9685::CHANNELS);
73const _: () = assert!(PAMOJA_PCA9685_COUNTS == pca9685::COUNTS);
74const _: () = assert!(PAMOJA_PCA9685_DEFAULT_ADDRESS == pca9685::DEFAULT_I2C_ADDRESS);
75const _: () =
76 assert!(PAMOJA_PCA9685_OSCILLATOR_STARTUP_MICROS == pca9685::OSCILLATOR_STARTUP_MICROS);
77const _: () = assert!(PAMOJA_PCA9685_REGISTER_MODE1 == pca9685::register::MODE1);
78const _: () = assert!(PAMOJA_PCA9685_REGISTER_MODE2 == pca9685::register::MODE2);
79const _: () = assert!(PAMOJA_PCA9685_REGISTER_LED0_ON_L == pca9685::register::LED0_ON_L);
80const _: () = assert!(PAMOJA_PCA9685_REGISTER_ALL_LED_ON_L == pca9685::register::ALL_LED_ON_L);
81const _: () = assert!(PAMOJA_PCA9685_REGISTER_PRE_SCALE == pca9685::register::PRE_SCALE);
82const _: () = assert!(PAMOJA_PCA9685_MODE1_RESTART == pca9685::mode1::RESTART);
83const _: () = assert!(PAMOJA_PCA9685_MODE1_EXTCLK == pca9685::mode1::EXTCLK);
84const _: () = assert!(PAMOJA_PCA9685_MODE1_AUTO_INCREMENT == pca9685::mode1::AUTO_INCREMENT);
85const _: () = assert!(PAMOJA_PCA9685_MODE1_SLEEP == pca9685::mode1::SLEEP);
86const _: () = assert!(PAMOJA_PCA9685_MODE1_RESET == pca9685::MODE1_RESET);
87const _: () = assert!(PAMOJA_PCA9685_MODE2_RESET == pca9685::MODE2_RESET);
88const _: () = assert!(PAMOJA_PCA9685_PRE_SCALE_RESET == pca9685::PRE_SCALE_RESET);
89const _: () = assert!(PAMOJA_PCA9685_PRE_SCALE_MIN == pca9685::PRE_SCALE_MIN);
90const _: () = assert!(PAMOJA_STEPPER_DEFAULT_STEP_MICROS == stepper::DEFAULT_STEP_MICROS);
91const _: () = assert!(PAMOJA_STEPPER_DEFAULT_PULSE_MICROS == stepper::DEFAULT_PULSE_MICROS);
92
93#[repr(C)]
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub struct PamojaPwm {
100 pub on_low: u8,
102 pub on_high: u8,
104 pub off_low: u8,
106 pub off_high: u8,
108}
109
110#[repr(C)]
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum PamojaStepDirection {
114 Forward = 0,
116 Backward = 1,
118}
119
120#[repr(C)]
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum PamojaStepDrive {
124 Wave = 0,
126 FullStep = 1,
128 HalfStep = 2,
130}
131
132pub struct PamojaStepper {
136 sequencer: stepper::Sequencer,
137 position: stepper::Position,
138}
139
140#[no_mangle]
151pub unsafe extern "C" fn pamoja_pca9685_channel_register(
152 channel: u8,
153 out_register: *mut u8,
154) -> PamojaStatus {
155 if out_register.is_null() {
156 set_last_error("out_register must not be null".to_owned());
157 return PamojaStatus::InvalidArgument;
158 }
159 if channel >= pca9685::CHANNELS {
160 set_last_error(format!("channel must be below {}", pca9685::CHANNELS));
161 return PamojaStatus::InvalidArgument;
162 }
163 *out_register = pca9685::channel_register(channel);
164 PamojaStatus::Ok
165}
166
167#[no_mangle]
173pub extern "C" fn pamoja_pca9685_prescale_for_frequency(update_rate_hz: u32, osc_hz: u32) -> u8 {
174 pca9685::prescale_for_frequency(update_rate_hz, osc_hz)
175}
176
177#[no_mangle]
183pub extern "C" fn pamoja_pca9685_frequency_for_prescale(prescale: u8, osc_hz: u32) -> f32 {
184 pca9685::frequency_for_prescale(prescale, osc_hz)
185}
186
187#[no_mangle]
193pub extern "C" fn pamoja_pwm_from_counts(on: u16, off: u16) -> PamojaPwm {
194 pca9685::Pwm::from_counts(on, off).into()
195}
196
197#[no_mangle]
206pub extern "C" fn pamoja_pwm_duty(off: u16) -> PamojaPwm {
207 pca9685::Pwm::duty(off).into()
208}
209
210#[no_mangle]
218pub extern "C" fn pamoja_pwm_servo(pulse_micros: u32, update_rate_hz: u32) -> PamojaPwm {
219 pca9685::Pwm::servo(pulse_micros, update_rate_hz).into()
220}
221
222#[no_mangle]
233pub unsafe extern "C" fn pamoja_pwm_counts(
234 pwm: PamojaPwm,
235 out_on: *mut u16,
236 out_off: *mut u16,
237) -> PamojaStatus {
238 if out_on.is_null() || out_off.is_null() {
239 set_last_error("out_on and out_off must not be null".to_owned());
240 return PamojaStatus::InvalidArgument;
241 }
242 let setting = pca9685::Pwm::from_bytes(&[pwm.on_low, pwm.on_high, pwm.off_low, pwm.off_high]);
243 *out_on = setting.on();
244 *out_off = setting.off();
245 PamojaStatus::Ok
246}
247
248#[no_mangle]
254pub extern "C" fn pamoja_pwm_full_on() -> PamojaPwm {
255 pca9685::Pwm::full_on().into()
256}
257
258#[no_mangle]
264pub extern "C" fn pamoja_pwm_full_off() -> PamojaPwm {
265 pca9685::Pwm::full_off().into()
266}
267
268#[no_mangle]
274pub extern "C" fn pamoja_stepper_new(drive: PamojaStepDrive) -> *mut PamojaStepper {
275 Box::into_raw(Box::new(PamojaStepper {
276 sequencer: stepper::Sequencer::new(drive.into()),
277 position: stepper::Position::new(),
278 }))
279}
280
281#[no_mangle]
292pub unsafe extern "C" fn pamoja_stepper_step(
293 stepper: *mut PamojaStepper,
294 direction: PamojaStepDirection,
295) -> u8 {
296 if stepper.is_null() {
297 return 0;
298 }
299 let stepper = &mut *stepper;
300 stepper.position.step(direction.into());
301 stepper.sequencer.step(direction.into())
302}
303
304#[no_mangle]
314pub unsafe extern "C" fn pamoja_stepper_coils(stepper: *const PamojaStepper) -> u8 {
315 if stepper.is_null() {
316 return 0;
317 }
318 (*stepper).sequencer.coils()
319}
320
321#[no_mangle]
331pub unsafe extern "C" fn pamoja_stepper_steps(stepper: *const PamojaStepper) -> i32 {
332 if stepper.is_null() {
333 return 0;
334 }
335 (*stepper).position.steps()
336}
337
338#[no_mangle]
344pub extern "C" fn pamoja_stepper_step_count(drive: PamojaStepDrive) -> usize {
345 stepper::Drive::from(drive).step_count()
346}
347
348#[no_mangle]
357pub unsafe extern "C" fn pamoja_stepper_free(stepper: *mut PamojaStepper) {
358 if !stepper.is_null() {
359 drop(Box::from_raw(stepper));
360 }
361}
362
363#[no_mangle]
369pub extern "C" fn pamoja_stepper_steps_for_degrees(degrees: f32, steps_per_revolution: u32) -> i32 {
370 stepper::steps_for_degrees(degrees, steps_per_revolution)
371}
372
373impl From<pca9685::Pwm> for PamojaPwm {
374 fn from(value: pca9685::Pwm) -> Self {
375 let [on_low, on_high, off_low, off_high] = value.bytes();
376 PamojaPwm {
377 on_low,
378 on_high,
379 off_low,
380 off_high,
381 }
382 }
383}
384
385impl From<PamojaStepDirection> for stepper::Direction {
386 fn from(value: PamojaStepDirection) -> Self {
387 match value {
388 PamojaStepDirection::Forward => stepper::Direction::Forward,
389 PamojaStepDirection::Backward => stepper::Direction::Backward,
390 }
391 }
392}
393
394impl From<PamojaStepDrive> for stepper::Drive {
395 fn from(value: PamojaStepDrive) -> Self {
396 match value {
397 PamojaStepDrive::Wave => stepper::Drive::Wave,
398 PamojaStepDrive::FullStep => stepper::Drive::FullStep,
399 PamojaStepDrive::HalfStep => stepper::Drive::HalfStep,
400 }
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use std::ptr;
408
409 #[test]
410 fn a_half_brightness_channel_writes_its_midpoint() {
411 assert_eq!(
412 pamoja_pwm_duty(2048),
413 PamojaPwm {
414 on_low: 0x00,
415 on_high: 0x00,
416 off_low: 0x00,
417 off_high: 0x08,
418 }
419 );
420 }
421
422 #[test]
423 fn fully_off_is_its_own_encoding_not_a_zero_duty() {
424 assert_eq!(
425 pamoja_pwm_full_off(),
426 PamojaPwm {
427 on_low: 0x00,
428 on_high: 0x00,
429 off_low: 0x00,
430 off_high: 0x10,
431 },
432 "the datasheet's full-off flag"
433 );
434 assert_eq!(
435 pamoja_pwm_duty(0),
436 pamoja_pwm_full_off(),
437 "a zero duty would load the same count into on and off, which the datasheet rules out"
438 );
439 assert_eq!(pamoja_pwm_full_on().on_high, 0x10);
440 }
441
442 #[test]
443 fn a_servo_pulse_scales_against_its_update_rate() {
444 let center = pamoja_pwm_servo(1_500, 50);
446 let counts = u16::from(center.off_low) | (u16::from(center.off_high) << 8);
447 assert_eq!(counts, 307, "1500 us * 4096 * 50 / 1e6");
448 }
449
450 #[test]
451 fn a_channel_beyond_the_part_is_refused() {
452 let mut register = 0u8;
453 let status = unsafe { pamoja_pca9685_channel_register(16, &mut register) };
455 assert_eq!(status, PamojaStatus::InvalidArgument);
456 }
457
458 #[test]
459 fn the_update_rate_round_trips_through_its_prescale() {
460 let prescale = pamoja_pca9685_prescale_for_frequency(50, PAMOJA_PCA9685_INTERNAL_OSC_HZ);
461 let frequency =
462 pamoja_pca9685_frequency_for_prescale(prescale, PAMOJA_PCA9685_INTERNAL_OSC_HZ);
463 assert!((frequency - 50.0).abs() < 1.0, "got {frequency} Hz");
464 }
465
466 #[test]
467 fn a_full_electrical_cycle_returns_to_its_first_pattern() {
468 let stepper = pamoja_stepper_new(PamojaStepDrive::HalfStep);
469 unsafe {
471 let first = pamoja_stepper_coils(stepper);
472 for _ in 0..pamoja_stepper_step_count(PamojaStepDrive::HalfStep) {
473 pamoja_stepper_step(stepper, PamojaStepDirection::Forward);
474 }
475 assert_eq!(pamoja_stepper_coils(stepper), first);
476 assert_eq!(pamoja_stepper_steps(stepper), 8);
477 pamoja_stepper_free(stepper);
478 }
479 }
480
481 #[test]
482 fn stepping_back_and_forth_returns_the_position_to_zero() {
483 let stepper = pamoja_stepper_new(PamojaStepDrive::FullStep);
484 unsafe {
486 pamoja_stepper_step(stepper, PamojaStepDirection::Forward);
487 pamoja_stepper_step(stepper, PamojaStepDirection::Backward);
488 assert_eq!(pamoja_stepper_steps(stepper), 0);
489 pamoja_stepper_free(stepper);
490 }
491 }
492
493 #[test]
494 fn calls_on_a_null_stepper_are_rejected_without_dereferencing() {
495 unsafe {
497 assert_eq!(
498 pamoja_stepper_step(ptr::null_mut(), PamojaStepDirection::Forward),
499 0
500 );
501 assert_eq!(pamoja_stepper_coils(ptr::null()), 0);
502 assert_eq!(pamoja_stepper_steps(ptr::null()), 0);
503 pamoja_stepper_free(ptr::null_mut());
504 }
505 }
506
507 #[test]
508 fn a_quarter_turn_is_a_quarter_of_the_revolution() {
509 assert_eq!(pamoja_stepper_steps_for_degrees(90.0, 200), 50);
510 assert_eq!(pamoja_stepper_steps_for_degrees(-90.0, 200), -50);
511 }
512}