Skip to main content

omnitiles/drivers/
actuonix_linear.rs

1// SPDX-License-Identifier: MIT
2// © 2025–2026 Christopher Liu
3
4//! Generic driver for Actuonix 16-series (P16, T16) Linear Actuators with potentiometer control.
5//!
6//! Wraps a DRV8873 H-Bridge for motor control and an ADC channel for position feedback.
7//!
8//! Wiring (Option -P):
9//! - Pin 1 (Orange): Potentiometer Ground
10//! - Pin 2 (Purple): Potentiometer Wiper (ADC Input)
11//! - Pin 3 (Red):    Motor Terminal A (+)
12//! - Pin 4 (Black):  Motor Terminal B (-)
13//! - Pin 5 (Yellow): Potentiometer Reference (3.3V)
14
15use crate::drivers::drv8873::{Drv8873, Fault};
16use crate::hw::spi::CsControl;
17use crate::hw::SpiBus;
18
19use stm32f7xx_hal::{
20    gpio::{self, Output, PushPull},
21    prelude::*,
22    spi,
23};
24
25/// Logical drive direction for the linear actuator.
26#[derive(Copy, Clone, Debug, PartialEq)]
27pub enum Direction {
28    Extend,
29    Retract,
30    Brake,
31}
32
33/// Generic driver for Actuonix linear actuators (P16, T16).
34///
35/// Supports ganging `N` physical actuators driven in parallel by the same
36/// H-bridge but each with their own potentiometer channel. Channels marked
37/// `inverted` have their raw reading mirrored (`4095 - raw`) before fusion,
38/// which undoes swapped wiper wiring on mechanically opposed units. The
39/// per-channel `enabled` flags select which pots contribute to the fused
40/// position estimate; disabled channels are still sampled so telemetry can
41/// see them, they just don't influence control.
42///
43/// `ReadPos` is a closure that returns raw 12-bit ADC readings (0..4095) for
44/// all `N` channels in one call.
45pub struct ActuonixLinear<
46    CS: CsControl,
47    const SLP_P: char,
48    const SLP_N: u8,
49    const DIS_P: char,
50    const DIS_N: u8,
51    Pwm1,
52    Pwm2,
53    ReadPos,
54    const N: usize,
55> {
56    drv: Drv8873<CS>,
57    pwm1: Pwm1,
58    pwm2: Pwm2,
59    nsleep: gpio::Pin<SLP_P, SLP_N, Output<PushPull>>,
60    disable: gpio::Pin<DIS_P, DIS_N, Output<PushPull>>,
61    read_positions: ReadPos,
62    adc_history: [[u16; N]; 5],
63    adc_idx: usize,
64    last_medians: [u16; N],
65    inverted: [bool; N],
66    enabled: [bool; N],
67    stroke_len_mm: f32,
68    inverted_pair_sum_mm: f32,
69    buffer_bottom_mm: f32,
70    buffer_top_mm: f32,
71    current_speed: f32,
72    limit_brake_active: bool,
73}
74
75impl<
76        CS: CsControl,
77        const SLP_P: char,
78        const SLP_N: u8,
79        const DIS_P: char,
80        const DIS_N: u8,
81        Pwm1,
82        Pwm2,
83        ReadPos,
84        const N: usize,
85    > ActuonixLinear<CS, SLP_P, SLP_N, DIS_P, DIS_N, Pwm1, Pwm2, ReadPos, N>
86where
87    Pwm1: _embedded_hal_PwmPin<Duty = u16>,
88    Pwm2: _embedded_hal_PwmPin<Duty = u16>,
89    ReadPos: FnMut() -> [u16; N],
90{
91    /// Construct a new Actuonix driver with Hardware PWM.
92    ///
93    /// `inverted[i]` should be true for channels whose pot wiper is wired
94    /// inversely to the extension direction (e.g., mechanically opposed units
95    /// that share the same drive signal). All channels start enabled.
96    pub fn new<SlpMode, DisMode>(
97        drv: Drv8873<CS>,
98        pwm1: Pwm1,
99        pwm2: Pwm2,
100        nsleep: gpio::Pin<SLP_P, SLP_N, SlpMode>,
101        disable: gpio::Pin<DIS_P, DIS_N, DisMode>,
102        mut read_positions: ReadPos,
103        inverted: [bool; N],
104        stroke_len_mm: f32,
105        inverted_pair_sum_mm: f32,
106        buffer_bottom_mm: f32,
107        buffer_top_mm: f32,
108    ) -> Self {
109        let mut nsleep = nsleep.into_push_pull_output();
110        let mut disable = disable.into_push_pull_output();
111
112        // Default: Awake, Enabled
113        nsleep.set_high();
114        disable.set_low();
115
116        let initial = (read_positions)();
117
118        Self {
119            drv,
120            pwm1,
121            pwm2,
122            nsleep,
123            disable,
124            read_positions,
125            adc_history: [initial; 5],
126            adc_idx: 0,
127            last_medians: initial,
128            inverted,
129            enabled: [true; N],
130            stroke_len_mm,
131            inverted_pair_sum_mm,
132            buffer_bottom_mm,
133            buffer_top_mm,
134            current_speed: 0.0,
135            limit_brake_active: false,
136        }
137    }
138
139    /// Enable or disable a specific potentiometer channel. Disabled channels
140    /// are still sampled for telemetry but do not contribute to the fused
141    /// position estimate used by control.
142    pub fn set_channel_enabled(&mut self, channel: usize, enabled: bool) {
143        if channel < N {
144            self.enabled[channel] = enabled;
145        }
146    }
147
148    /// True if at least one potentiometer channel is enabled for fusion.
149    #[inline]
150    pub fn any_channel_enabled(&self) -> bool {
151        self.enabled.iter().any(|e| *e)
152    }
153
154    /// Access the last computed per-channel medians (raw, uninverted). Useful
155    /// for telemetry. Returns the values from the most recent refresh.
156    #[inline]
157    pub fn channel_medians(&self) -> &[u16; N] {
158        &self.last_medians
159    }
160
161    /// Sample all channels once, update the per-channel median filter, and
162    /// cache the new medians. Called by [`position_raw`](Self::position_raw).
163    fn refresh(&mut self) {
164        let raw = (self.read_positions)();
165        self.adc_history[self.adc_idx] = raw;
166        self.adc_idx = (self.adc_idx + 1) % 5;
167
168        for i in 0..N {
169            let mut column = [0u16; 5];
170            for r in 0..5 {
171                column[r] = self.adc_history[r][i];
172            }
173            column.sort_unstable();
174            self.last_medians[i] = column[2];
175        }
176    }
177
178    /// Compute the fused raw position from the current cached medians. Returns
179    /// `None` if no channels are enabled.
180    fn fused_raw_from_cache(&self) -> Option<u16> {
181        let mut sum: u32 = 0;
182        let mut count: u32 = 0;
183        for i in 0..N {
184            if self.enabled[i] {
185                let m = self.last_medians[i] as u32;
186                let logical = if self.inverted[i] {
187                    let offset = (self.inverted_pair_sum_mm / self.stroke_len_mm * 4095.0) as u32;
188                    offset.saturating_sub(m)
189                } else {
190                    m
191                };
192                sum += logical;
193                count += 1;
194            }
195        }
196        if count == 0 {
197            None
198        } else {
199            Some((sum / count) as u16)
200        }
201    }
202
203    /// Set the motor speed and direction.
204    ///
205    /// `speed` - A float from -1.0 (Full Retract) to 1.0 (Full Extend).
206    ///
207    /// If no pot channels are enabled, soft-limit checks are skipped so
208    /// manual drive still works without position feedback.
209    pub fn set_speed(&mut self, speed: f32) {
210        // Any explicit speed command clears "limit brake" state.
211        self.limit_brake_active = false;
212
213        // Clamp speed to valid range
214        let mut speed = speed.clamp(-1.0, 1.0);
215
216        if let Some(pos) = self.position_mm() {
217            let max_pos = self.stroke_len_mm - self.buffer_top_mm;
218            let min_pos = self.buffer_bottom_mm;
219
220            // Prevent starting a movement that goes deeper into the out-of-bounds area
221            if speed > 0.0 && pos >= max_pos {
222                speed = 0.0;
223            } else if speed < 0.0 && pos <= min_pos {
224                speed = 0.0;
225            }
226        }
227
228        self.current_speed = speed;
229
230        let max_duty = self.pwm1.get_max_duty(); // Assuming Pwm1/Pwm2 have same resolution
231
232        // Calculate target duty cycle
233        let duty = (speed.abs() * max_duty as f32) as u16;
234
235        if speed > 0.001 {
236            // Extend: IN1 PWM, IN2 Low
237            self.pwm1.set_duty(duty);
238            self.pwm2.set_duty(0);
239            self.pwm1.enable();
240            self.pwm2.enable();
241        } else if speed < -0.001 {
242            // Retract: IN1 Low, IN2 PWM
243            self.pwm1.set_duty(0);
244            self.pwm2.set_duty(duty);
245            self.pwm1.enable();
246            self.pwm2.enable();
247        } else {
248            self.brake();
249        }
250    }
251
252    /// Continuously check the ADC position and brake if the actuator exceeds the software limits.
253    /// This should be called regularly in the main application loop. No-op when no
254    /// channels are enabled (manual drive without feedback).
255    pub fn enforce_limits(&mut self) {
256        if self.current_speed.abs() < 0.001 {
257            return;
258        }
259
260        let Some(pos) = self.position_mm() else {
261            return;
262        };
263        let max_pos = self.stroke_len_mm - self.buffer_top_mm;
264        let min_pos = self.buffer_bottom_mm;
265
266        if self.current_speed > 0.0 && pos >= max_pos {
267            self.brake_due_to_limit();
268            self.current_speed = 0.0;
269        } else if self.current_speed < 0.0 && pos <= min_pos {
270            self.brake_due_to_limit();
271            self.current_speed = 0.0;
272        }
273    }
274
275    /// Extend the actuator at full speed.
276    #[inline]
277    pub fn extend(&mut self) {
278        self.set_speed(1.0);
279    }
280
281    /// Retract the actuator at full speed.
282    #[inline]
283    pub fn retract(&mut self) {
284        self.set_speed(-1.0);
285    }
286
287    /// Brake (stops quickly by shorting motor terminals).
288    #[inline]
289    pub fn brake(&mut self) {
290        self.limit_brake_active = false;
291        self.brake_raw();
292    }
293
294    #[inline]
295    fn brake_due_to_limit(&mut self) {
296        self.limit_brake_active = true;
297        self.brake_raw();
298    }
299
300    #[inline]
301    fn brake_raw(&mut self) {
302        let max = self.pwm1.get_max_duty();
303        self.pwm1.set_duty(max);
304        self.pwm2.set_duty(max);
305        self.pwm1.enable();
306        self.pwm2.enable();
307    }
308
309    /// True when we are currently braking due to software limit enforcement.
310    #[inline]
311    pub fn is_limit_braking(&self) -> bool {
312        self.limit_brake_active
313    }
314
315    /// Refresh all channels and return the fused raw 12-bit position
316    /// (0..4095). Returns `None` if no channels are enabled.
317    #[inline]
318    pub fn position_raw(&mut self) -> Option<u16> {
319        self.refresh();
320        self.fused_raw_from_cache()
321    }
322
323    /// Read position as a fraction (0.0 = Retracted, 1.0 = Extended).
324    pub fn position_percent(&mut self) -> Option<f32> {
325        self.position_raw().map(|r| (r as f32) / 4095.0)
326    }
327
328    /// Read position in millimeters.
329    pub fn position_mm(&mut self) -> Option<f32> {
330        self.position_percent().map(|p| p * self.stroke_len_mm)
331    }
332
333    /// Get the max stroke length.
334    pub fn stroke_len_mm(&self) -> f32 {
335        self.stroke_len_mm
336    }
337
338    /// Access the inner DRV8873 for fault reading.
339    pub fn drv(&mut self) -> &mut Drv8873<CS> {
340        &mut self.drv
341    }
342
343    /// Put the driver into sleep mode.
344    ///
345    /// This shuts down most of the internal circuitry to reduce power consumption.
346    #[inline]
347    pub fn sleep(&mut self) {
348        self.nsleep.set_low();
349    }
350
351    /// Wake the driver from sleep mode.
352    #[inline]
353    pub fn wake(&mut self) {
354        self.nsleep.set_high();
355    }
356
357    /// Enable the motor and wake the driver if in sleep.
358    #[inline]
359    pub fn enable_outputs(&mut self) {
360        self.wake();
361        self.disable.set_low();
362    }
363
364    /// Disable the motor and brake.
365    #[inline]
366    pub fn disable_outputs(&mut self) {
367        self.brake();
368        self.disable.set_high();
369    }
370
371    /// Read the FAULT status register from the DRV8873.
372    pub fn read_fault<I, PINS>(
373        &mut self,
374        spi_bus: &mut SpiBus<I, PINS>,
375    ) -> Result<Fault, spi::Error>
376    where
377        I: spi::Instance,
378        PINS: spi::Pins<I>,
379    {
380        self.drv.read_fault(spi_bus)
381    }
382}