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}