Skip to main content

device_envoy_core/
servo_player.rs

1//! A device abstraction for servo animation control primitives shared across platforms.
2//!
3//! This module provides the platform-independent command engine used by
4//! platform crates to build servo-player APIs.
5
6#![allow(clippy::future_not_send, reason = "single-threaded")]
7
8use core::borrow::Borrow;
9
10use embassy_futures::select::{Either, select};
11use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
12use embassy_sync::signal::Signal;
13use embassy_time::{Duration, Timer};
14use heapless::Vec;
15
16use crate::servo::Servo;
17
18/// Commands sent to the servo player device loop.
19enum PlayerCommand<const MAX_STEPS: usize> {
20    Set {
21        degrees: u16,
22    },
23    Animate {
24        steps: Vec<(u16, Duration), MAX_STEPS>,
25        mode: AtEnd,
26    },
27    Hold,
28    Relax,
29}
30
31/// Animation end behavior.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33#[cfg_attr(feature = "defmt", derive(defmt::Format))]
34pub enum AtEnd {
35    /// Repeat the animation sequence indefinitely.
36    Loop,
37    /// Hold the final position when animation completes.
38    Hold,
39    /// Stop holding position after animation completes (servo relaxes).
40    Relax,
41}
42
43/// Build a const linear sequence of animation steps as an array.
44///
45/// This function creates `N` evenly spaced degree targets from `start_degrees`
46/// to `end_degrees`, each using the same per-step duration.
47///
48/// **Syntax:**
49///
50/// ```text
51/// linear::<N>(<start_degrees>, <end_degrees>, <total_duration>)
52/// ```
53///
54/// **Behavior:**
55///
56/// - `N` must be greater than zero.
57/// - When `N > 1`, the first step is `start_degrees` and the last step is
58///   `end_degrees`.
59/// - Each returned step uses the same duration, derived from
60///   `total_duration / N`.
61///
62/// This uses [`embassy_time::Duration`](https://docs.rs/embassy-time/latest/embassy_time/struct.Duration.html) for step timing.
63///
64/// See the [servo module documentation](mod@crate::servo) for usage examples.
65#[must_use]
66pub const fn linear<const N: usize>(
67    start_degrees: u16,
68    end_degrees: u16,
69    total_duration: embassy_time::Duration,
70) -> [(u16, embassy_time::Duration); N] {
71    assert!(N > 0, "at least one step required");
72    let step_duration = Duration::from_micros(total_duration.as_micros() / (N as u64));
73    let delta = end_degrees as i32 - start_degrees as i32;
74    let denom = if N == 1 { 1 } else { (N - 1) as i32 };
75
76    let mut result = [(0u16, Duration::from_micros(0)); N];
77    let mut step_index = 0;
78    // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
79    while step_index < N {
80        let degrees = if N == 1 {
81            start_degrees
82        } else {
83            let step_delta = delta * (step_index as i32) / denom;
84            (start_degrees as i32 + step_delta) as u16
85        };
86        result[step_index] = (degrees, step_duration);
87        step_index += 1;
88    }
89    result
90}
91
92/// Combine two animation step arrays into one larger array.
93///
94/// This uses [`embassy_time::Duration`](https://docs.rs/embassy-time/latest/embassy_time/struct.Duration.html) for step timing.
95#[must_use]
96pub const fn combine<const N1: usize, const N2: usize, const OUT_N: usize>(
97    first: [(u16, embassy_time::Duration); N1],
98    second: [(u16, embassy_time::Duration); N2],
99) -> [(u16, embassy_time::Duration); OUT_N] {
100    assert!(OUT_N == N1 + N2, "OUT_N must equal N1 + N2");
101
102    let mut result = [(0u16, Duration::from_micros(0)); OUT_N];
103    let mut first_index = 0;
104    // TODO_NIGHTLY When nightly feature const_for becomes stable, replace these while loops with for loops.
105    while first_index < N1 {
106        result[first_index] = first[first_index];
107        first_index += 1;
108    }
109    let mut second_index = 0;
110    while second_index < N2 {
111        result[N1 + second_index] = second[second_index];
112        second_index += 1;
113    }
114    result
115}
116
117/// Static resources for [`ServoPlayer`].
118#[doc(hidden)] // Public for macro-expanded plumbing; not part of user-facing API docs.
119pub struct ServoPlayerStatic<const MAX_STEPS: usize> {
120    command: Signal<CriticalSectionRawMutex, PlayerCommand<MAX_STEPS>>,
121}
122
123impl<const MAX_STEPS: usize> ServoPlayerStatic<MAX_STEPS> {
124    /// Create static resources for the servo player device.
125    #[must_use]
126    pub const fn new_static() -> Self {
127        Self {
128            command: Signal::new(),
129        }
130    }
131
132    fn signal(&self, command: PlayerCommand<MAX_STEPS>) {
133        self.command.signal(command);
134    }
135
136    async fn wait(&self) -> PlayerCommand<MAX_STEPS> {
137        self.command.wait().await
138    }
139}
140
141/// Internal servo-player command handle used by platform macro-generated types.
142///
143/// This must remain `pub` because `servo_player!` macro expansions in platform crates
144/// construct this handle type.
145#[doc(hidden)]
146pub struct ServoPlayerHandle<const MAX_STEPS: usize> {
147    servo_player_static: &'static ServoPlayerStatic<MAX_STEPS>,
148}
149
150impl<const MAX_STEPS: usize> ServoPlayerHandle<MAX_STEPS> {
151    /// Create static resources for a servo player.
152    #[must_use]
153    pub const fn new_static() -> ServoPlayerStatic<MAX_STEPS> {
154        ServoPlayerStatic::new_static()
155    }
156
157    /// Create a servo player handle. The device loop must already be running.
158    #[must_use]
159    pub const fn new(servo_player_static: &'static ServoPlayerStatic<MAX_STEPS>) -> Self {
160        Self {
161            servo_player_static,
162        }
163    }
164}
165
166/// Platform-agnostic servo-player device contract.
167///
168/// Platform crates implement this trait for generated servo player types so servo
169/// operations resolve through trait methods instead of inherent methods.
170///
171/// This trait extends [`Servo`], so a servo player always supports the base
172/// servo control operations (`set_degrees`, `hold`, and `relax`) in addition to
173/// [`ServoPlayer::animate`].
174///
175/// # Example: Basic Servo Control
176///
177/// This example demonstrates basic servo control: moving to a position, relaxing,
178/// and using animation.
179///
180/// ```rust,no_run
181/// use device_envoy_core::servo::{AtEnd, ServoPlayer};
182/// use embassy_time::{Duration, Timer};
183///
184/// async fn basic_servo_control<const MAX_STEPS: usize>(servo_player: &impl ServoPlayer<MAX_STEPS>) {
185///     // Move to 90 degrees, wait 1 second, then relax.
186///     servo_player.set_degrees(90);
187///     Timer::after(Duration::from_secs(1)).await;
188///     servo_player.relax();
189///
190///     // Animate: hold at 180 degrees for 1 second, then 0 degrees for 1 second, then relax.
191///     const STEPS: [(u16, Duration); 2] = [
192///         (180, Duration::from_secs(1)),
193///         (0, Duration::from_secs(1)),
194///     ];
195///     // AtEnd::Relax quiets the servo; AtEnd::Hold keeps driving pulses to hold
196///     // position; AtEnd::Loop repeats.
197///     servo_player.animate(STEPS, AtEnd::Relax);
198/// }
199///
200/// # use device_envoy_core::servo::Servo;
201/// # struct ServoPlayerMock;
202/// # impl Servo for ServoPlayerMock {
203/// #     const DEFAULT_MAX_DEGREES: u16 = 180;
204/// #     fn set_degrees(&self, _degrees: u16) {}
205/// #     fn hold(&self) {}
206/// #     fn relax(&self) {}
207/// # }
208/// # impl ServoPlayer<40> for ServoPlayerMock {
209/// #     const MAX_STEPS: usize = 40;
210/// #     fn animate<I>(&self, _steps: I, _at_end: AtEnd)
211/// #     where
212/// #         I: IntoIterator,
213/// #         I::Item: core::borrow::Borrow<(u16, embassy_time::Duration)>,
214/// #     {
215/// #     }
216/// # }
217/// # let servo_player = ServoPlayerMock;
218/// # let _future = basic_servo_control(&servo_player);
219/// ```
220///
221/// # Example: Multi-Step Animation
222///
223/// This example combines 40 animation steps using [`linear`] and [`combine`] to
224/// sweep up, hold, sweep down, hold.
225///
226/// ```rust,no_run
227/// use device_envoy_core::servo::{AtEnd, ServoPlayer, combine, linear};
228/// use embassy_time::Duration;
229///
230/// async fn run_sweep_animation(servo_player: &impl ServoPlayer<40>) {
231///     // Combine 40 animation steps into one array.
232///     const STEPS_UP_AND_HOLD: [(u16, Duration); 20] = combine::<19, 1, 20>(
233///         linear::<19>(0, 180, Duration::from_secs(2)), // 19 steps from 0 degrees to 180 degrees
234///         [(180, Duration::from_millis(400))],          // Hold at 180 degrees for 400 ms
235///     );
236///     const STEPS_DOWN_AND_HOLD: [(u16, Duration); 20] = combine::<19, 1, 20>(
237///         linear::<19>(180, 0, Duration::from_secs(2)), // 19 steps from 180 degrees to 0 degrees
238///         [(0, Duration::from_millis(400))],            // Hold at 0 degrees for 400 ms
239///     );
240///     const STEPS: [(u16, Duration); 40] =
241///         combine::<20, 20, 40>(STEPS_UP_AND_HOLD, STEPS_DOWN_AND_HOLD);
242///
243///     servo_player.animate(STEPS, AtEnd::Loop); // Loop the sweep animation
244///
245///     // Let it run in the background for 10 seconds, then relax.
246///     embassy_time::Timer::after(Duration::from_secs(10)).await;
247///     servo_player.relax();
248/// }
249///
250/// # use device_envoy_core::servo::Servo;
251/// # struct ServoPlayerMock;
252/// # impl Servo for ServoPlayerMock {
253/// #     const DEFAULT_MAX_DEGREES: u16 = 180;
254/// #     fn set_degrees(&self, _degrees: u16) {}
255/// #     fn hold(&self) {}
256/// #     fn relax(&self) {}
257/// # }
258/// # impl ServoPlayer<40> for ServoPlayerMock {
259/// #     const MAX_STEPS: usize = 40;
260/// #     fn animate<I>(&self, _steps: I, _at_end: AtEnd)
261/// #     where
262/// #         I: IntoIterator,
263/// #         I::Item: core::borrow::Borrow<(u16, embassy_time::Duration)>,
264/// #     {
265/// #     }
266/// # }
267/// # let servo_player = ServoPlayerMock;
268/// # let _future = run_sweep_animation(&servo_player);
269/// ```
270pub trait ServoPlayer<const MAX_STEPS: usize>: Servo {
271    /// Maximum number of animation steps accepted by [`ServoPlayer::animate`].
272    const MAX_STEPS: usize;
273
274    /// Animate through a sequence of angles with per-step hold durations.
275    ///
276    /// This uses [`embassy_time::Duration`](https://docs.rs/embassy-time/latest/embassy_time/struct.Duration.html) for step timing.
277    ///
278    /// See the [ServoPlayer trait documentation](Self) for usage examples.
279    fn animate<I>(&self, steps: I, at_end: AtEnd)
280    where
281        I: IntoIterator,
282        I::Item: Borrow<(u16, embassy_time::Duration)>;
283}
284
285// Must remain `pub` because platform-crate macro expansions call this helper.
286#[doc(hidden)]
287pub fn __servo_player_set_degrees<const MAX_STEPS: usize>(
288    servo_player_handle: &ServoPlayerHandle<MAX_STEPS>,
289    degrees: u16,
290) {
291    servo_player_handle
292        .servo_player_static
293        .signal(PlayerCommand::Set { degrees });
294}
295
296// Must remain `pub` because platform-crate macro expansions call this helper.
297#[doc(hidden)]
298pub fn __servo_player_hold<const MAX_STEPS: usize>(
299    servo_player_handle: &ServoPlayerHandle<MAX_STEPS>,
300) {
301    servo_player_handle
302        .servo_player_static
303        .signal(PlayerCommand::Hold);
304}
305
306// Must remain `pub` because platform-crate macro expansions call this helper.
307#[doc(hidden)]
308pub fn __servo_player_relax<const MAX_STEPS: usize>(
309    servo_player_handle: &ServoPlayerHandle<MAX_STEPS>,
310) {
311    servo_player_handle
312        .servo_player_static
313        .signal(PlayerCommand::Relax);
314}
315
316// Must remain `pub` because platform-crate macro expansions call this helper.
317#[doc(hidden)]
318pub fn __servo_player_animate<I, const MAX_STEPS: usize>(
319    servo_player_handle: &ServoPlayerHandle<MAX_STEPS>,
320    steps: I,
321    at_end: AtEnd,
322) where
323    I: IntoIterator,
324    I::Item: Borrow<(u16, embassy_time::Duration)>,
325{
326    assert!(MAX_STEPS > 0, "animate disabled: max_steps is 0");
327    let mut sequence: Vec<(u16, Duration), MAX_STEPS> = Vec::new();
328    for step in steps {
329        let step = *step.borrow();
330        assert!(
331            step.1.as_micros() > 0,
332            "animation step duration must be positive"
333        );
334        sequence
335            .push(step)
336            .expect("animate sequence fits within max_steps");
337    }
338    assert!(!sequence.is_empty(), "animate requires at least one step");
339
340    servo_player_handle
341        .servo_player_static
342        .signal(PlayerCommand::Animate {
343            steps: sequence,
344            mode: at_end,
345        });
346}
347
348impl<const MAX_STEPS: usize> ServoPlayer<MAX_STEPS> for ServoPlayerHandle<MAX_STEPS> {
349    const MAX_STEPS: usize = MAX_STEPS;
350
351    fn animate<I>(&self, steps: I, at_end: AtEnd)
352    where
353        I: IntoIterator,
354        I::Item: Borrow<(u16, embassy_time::Duration)>,
355    {
356        __servo_player_animate(self, steps, at_end);
357    }
358}
359
360impl<const MAX_STEPS: usize> Servo for ServoPlayerHandle<MAX_STEPS> {
361    const DEFAULT_MAX_DEGREES: u16 = 180;
362
363    fn set_degrees(&self, degrees: u16) {
364        __servo_player_set_degrees(self, degrees);
365    }
366
367    fn hold(&self) {
368        __servo_player_hold(self);
369    }
370
371    fn relax(&self) {
372        __servo_player_relax(self);
373    }
374}
375
376/// Shared command loop for servo-player devices.
377pub async fn device_loop<const MAX_STEPS: usize, O>(
378    servo_player_static: &'static ServoPlayerStatic<MAX_STEPS>,
379    mut servo_player_output: O,
380) -> !
381where
382    O: Servo,
383{
384    let mut current_degrees: u16 = 0;
385    servo_player_output.set_degrees(current_degrees);
386
387    let mut command = servo_player_static.wait().await;
388    loop {
389        match command {
390            PlayerCommand::Set { degrees } => {
391                current_degrees = degrees;
392                servo_player_output.set_degrees(current_degrees);
393                command = servo_player_static.wait().await;
394            }
395            PlayerCommand::Hold => {
396                servo_player_output.hold();
397                command = servo_player_static.wait().await;
398            }
399            PlayerCommand::Relax => {
400                servo_player_output.relax();
401                command = servo_player_static.wait().await;
402            }
403            PlayerCommand::Animate { steps, mode } => {
404                command = run_animation(
405                    &steps,
406                    mode,
407                    &mut servo_player_output,
408                    servo_player_static,
409                    &mut current_degrees,
410                )
411                .await;
412            }
413        }
414    }
415}
416
417async fn run_animation<const MAX_STEPS: usize, O>(
418    steps: &[(u16, Duration)],
419    mode: AtEnd,
420    servo_player_output: &mut O,
421    servo_player_static: &'static ServoPlayerStatic<MAX_STEPS>,
422    current_degrees: &mut u16,
423) -> PlayerCommand<MAX_STEPS>
424where
425    O: Servo,
426{
427    loop {
428        for step in steps {
429            if *current_degrees != step.0 {
430                servo_player_output.set_degrees(step.0);
431                *current_degrees = step.0;
432            }
433            match select(Timer::after(step.1), servo_player_static.wait()).await {
434                Either::First(_) => {}
435                Either::Second(command) => return command,
436            }
437        }
438
439        match mode {
440            AtEnd::Loop => {}
441            AtEnd::Hold => return servo_player_static.wait().await,
442            AtEnd::Relax => {
443                servo_player_output.relax();
444                return servo_player_static.wait().await;
445            }
446        }
447    }
448}