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
26const _: () = assert!(PAMOJA_PCA9685_INTERNAL_OSC_HZ == pca9685::INTERNAL_OSC_HZ);
29const _: () = assert!(PAMOJA_PCA9685_CHANNELS == pca9685::CHANNELS);
30const _: () = assert!(PAMOJA_PCA9685_COUNTS == pca9685::COUNTS);
31
32#[repr(C)]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct PamojaPwm {
39 pub on_low: u8,
41 pub on_high: u8,
43 pub off_low: u8,
45 pub off_high: u8,
47}
48
49#[repr(C)]
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum PamojaStepDirection {
53 Forward = 0,
55 Backward = 1,
57}
58
59#[repr(C)]
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum PamojaStepDrive {
63 Wave = 0,
65 FullStep = 1,
67 HalfStep = 2,
69}
70
71pub struct PamojaStepper {
75 sequencer: stepper::Sequencer,
76 position: stepper::Position,
77}
78
79#[no_mangle]
90pub unsafe extern "C" fn pamoja_pca9685_channel_register(
91 channel: u8,
92 out_register: *mut u8,
93) -> PamojaStatus {
94 if out_register.is_null() {
95 set_last_error("out_register must not be null".to_owned());
96 return PamojaStatus::InvalidArgument;
97 }
98 if channel >= pca9685::CHANNELS {
99 set_last_error(format!("channel must be below {}", pca9685::CHANNELS));
100 return PamojaStatus::InvalidArgument;
101 }
102 *out_register = pca9685::channel_register(channel);
103 PamojaStatus::Ok
104}
105
106#[no_mangle]
112pub extern "C" fn pamoja_pca9685_prescale_for_frequency(update_rate_hz: u32, osc_hz: u32) -> u8 {
113 pca9685::prescale_for_frequency(update_rate_hz, osc_hz)
114}
115
116#[no_mangle]
122pub extern "C" fn pamoja_pca9685_frequency_for_prescale(prescale: u8, osc_hz: u32) -> f32 {
123 pca9685::frequency_for_prescale(prescale, osc_hz)
124}
125
126#[no_mangle]
132pub extern "C" fn pamoja_pwm_from_counts(on: u16, off: u16) -> PamojaPwm {
133 pca9685::Pwm::from_counts(on, off).into()
134}
135
136#[no_mangle]
142pub extern "C" fn pamoja_pwm_duty(off: u16) -> PamojaPwm {
143 pca9685::Pwm::duty(off).into()
144}
145
146#[no_mangle]
154pub extern "C" fn pamoja_pwm_servo(pulse_micros: u32, update_rate_hz: u32) -> PamojaPwm {
155 pca9685::Pwm::servo(pulse_micros, update_rate_hz).into()
156}
157
158#[no_mangle]
169pub unsafe extern "C" fn pamoja_pwm_counts(
170 pwm: PamojaPwm,
171 out_on: *mut u16,
172 out_off: *mut u16,
173) -> PamojaStatus {
174 if out_on.is_null() || out_off.is_null() {
175 set_last_error("out_on and out_off must not be null".to_owned());
176 return PamojaStatus::InvalidArgument;
177 }
178 let setting = pca9685::Pwm::from_bytes(&[pwm.on_low, pwm.on_high, pwm.off_low, pwm.off_high]);
179 *out_on = setting.on();
180 *out_off = setting.off();
181 PamojaStatus::Ok
182}
183
184#[no_mangle]
190pub extern "C" fn pamoja_pwm_full_on() -> PamojaPwm {
191 pca9685::Pwm::full_on().into()
192}
193
194#[no_mangle]
200pub extern "C" fn pamoja_pwm_full_off() -> PamojaPwm {
201 pca9685::Pwm::full_off().into()
202}
203
204#[no_mangle]
210pub extern "C" fn pamoja_stepper_new(drive: PamojaStepDrive) -> *mut PamojaStepper {
211 Box::into_raw(Box::new(PamojaStepper {
212 sequencer: stepper::Sequencer::new(drive.into()),
213 position: stepper::Position::new(),
214 }))
215}
216
217#[no_mangle]
228pub unsafe extern "C" fn pamoja_stepper_step(
229 stepper: *mut PamojaStepper,
230 direction: PamojaStepDirection,
231) -> u8 {
232 if stepper.is_null() {
233 return 0;
234 }
235 let stepper = &mut *stepper;
236 stepper.position.step(direction.into());
237 stepper.sequencer.step(direction.into())
238}
239
240#[no_mangle]
250pub unsafe extern "C" fn pamoja_stepper_coils(stepper: *const PamojaStepper) -> u8 {
251 if stepper.is_null() {
252 return 0;
253 }
254 (*stepper).sequencer.coils()
255}
256
257#[no_mangle]
267pub unsafe extern "C" fn pamoja_stepper_steps(stepper: *const PamojaStepper) -> i32 {
268 if stepper.is_null() {
269 return 0;
270 }
271 (*stepper).position.steps()
272}
273
274#[no_mangle]
280pub extern "C" fn pamoja_stepper_step_count(drive: PamojaStepDrive) -> usize {
281 stepper::Drive::from(drive).step_count()
282}
283
284#[no_mangle]
293pub unsafe extern "C" fn pamoja_stepper_free(stepper: *mut PamojaStepper) {
294 if !stepper.is_null() {
295 drop(Box::from_raw(stepper));
296 }
297}
298
299#[no_mangle]
305pub extern "C" fn pamoja_stepper_steps_for_degrees(degrees: f32, steps_per_revolution: u32) -> i32 {
306 stepper::steps_for_degrees(degrees, steps_per_revolution)
307}
308
309impl From<pca9685::Pwm> for PamojaPwm {
310 fn from(value: pca9685::Pwm) -> Self {
311 let [on_low, on_high, off_low, off_high] = value.bytes();
312 PamojaPwm {
313 on_low,
314 on_high,
315 off_low,
316 off_high,
317 }
318 }
319}
320
321impl From<PamojaStepDirection> for stepper::Direction {
322 fn from(value: PamojaStepDirection) -> Self {
323 match value {
324 PamojaStepDirection::Forward => stepper::Direction::Forward,
325 PamojaStepDirection::Backward => stepper::Direction::Backward,
326 }
327 }
328}
329
330impl From<PamojaStepDrive> for stepper::Drive {
331 fn from(value: PamojaStepDrive) -> Self {
332 match value {
333 PamojaStepDrive::Wave => stepper::Drive::Wave,
334 PamojaStepDrive::FullStep => stepper::Drive::FullStep,
335 PamojaStepDrive::HalfStep => stepper::Drive::HalfStep,
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use std::ptr;
344
345 #[test]
346 fn a_half_brightness_channel_writes_its_midpoint() {
347 assert_eq!(
348 pamoja_pwm_duty(2048),
349 PamojaPwm {
350 on_low: 0x00,
351 on_high: 0x00,
352 off_low: 0x00,
353 off_high: 0x08,
354 }
355 );
356 }
357
358 #[test]
359 fn fully_off_is_its_own_encoding_not_a_zero_duty() {
360 assert_eq!(
361 pamoja_pwm_full_off(),
362 PamojaPwm {
363 on_low: 0x00,
364 on_high: 0x00,
365 off_low: 0x00,
366 off_high: 0x10,
367 },
368 "a zero duty still glitches high for one count; the flag does not"
369 );
370 assert_eq!(pamoja_pwm_full_on().on_high, 0x10);
371 }
372
373 #[test]
374 fn a_servo_pulse_scales_against_its_update_rate() {
375 let centre = pamoja_pwm_servo(1_500, 50);
377 let counts = u16::from(centre.off_low) | (u16::from(centre.off_high) << 8);
378 assert_eq!(counts, 307, "1500 us * 4096 * 50 / 1e6");
379 }
380
381 #[test]
382 fn a_channel_beyond_the_part_is_refused() {
383 let mut register = 0u8;
384 let status = unsafe { pamoja_pca9685_channel_register(16, &mut register) };
386 assert_eq!(status, PamojaStatus::InvalidArgument);
387 }
388
389 #[test]
390 fn the_update_rate_round_trips_through_its_prescale() {
391 let prescale = pamoja_pca9685_prescale_for_frequency(50, PAMOJA_PCA9685_INTERNAL_OSC_HZ);
392 let frequency =
393 pamoja_pca9685_frequency_for_prescale(prescale, PAMOJA_PCA9685_INTERNAL_OSC_HZ);
394 assert!((frequency - 50.0).abs() < 1.0, "got {frequency} Hz");
395 }
396
397 #[test]
398 fn a_full_electrical_cycle_returns_to_its_first_pattern() {
399 let stepper = pamoja_stepper_new(PamojaStepDrive::HalfStep);
400 unsafe {
402 let first = pamoja_stepper_coils(stepper);
403 for _ in 0..pamoja_stepper_step_count(PamojaStepDrive::HalfStep) {
404 pamoja_stepper_step(stepper, PamojaStepDirection::Forward);
405 }
406 assert_eq!(pamoja_stepper_coils(stepper), first);
407 assert_eq!(pamoja_stepper_steps(stepper), 8);
408 pamoja_stepper_free(stepper);
409 }
410 }
411
412 #[test]
413 fn stepping_back_and_forth_returns_the_position_to_zero() {
414 let stepper = pamoja_stepper_new(PamojaStepDrive::FullStep);
415 unsafe {
417 pamoja_stepper_step(stepper, PamojaStepDirection::Forward);
418 pamoja_stepper_step(stepper, PamojaStepDirection::Backward);
419 assert_eq!(pamoja_stepper_steps(stepper), 0);
420 pamoja_stepper_free(stepper);
421 }
422 }
423
424 #[test]
425 fn calls_on_a_null_stepper_are_rejected_without_dereferencing() {
426 unsafe {
428 assert_eq!(
429 pamoja_stepper_step(ptr::null_mut(), PamojaStepDirection::Forward),
430 0
431 );
432 assert_eq!(pamoja_stepper_coils(ptr::null()), 0);
433 assert_eq!(pamoja_stepper_steps(ptr::null()), 0);
434 pamoja_stepper_free(ptr::null_mut());
435 }
436 }
437
438 #[test]
439 fn a_quarter_turn_is_a_quarter_of_the_revolution() {
440 assert_eq!(pamoja_stepper_steps_for_degrees(90.0, 200), 50);
441 assert_eq!(pamoja_stepper_steps_for_degrees(-90.0, 200), -50);
442 }
443}