Skip to main content

device_envoy/
servo_player.rs

1//! A device abstraction for hobby servos that can animate motion sequences.
2//!
3//! This page provides the primary documentation and examples for controlling servos that can
4//! animate motion sequences. The device abstraction supports moving to angles,
5//! holding/relaxing position, and sequenced animation.
6//!
7//! **After reading the examples below, see also:**
8//!
9//! - [`servo_player!`](macro@crate::servo_player) — Macro to generate a servo player struct
10//!   type (includes syntax details). See [`ServoPlayerGenerated`](servo_player_generated::ServoPlayerGenerated)
11//!   for a sample of a generated type.
12//! - [`ServoPlayerGenerated`](servo_player_generated::ServoPlayerGenerated) — Sample struct
13//!   type showing all methods and associated constants.
14//! - [`combine!`](macro@crate::servo_player::combine) & [`linear`] — Macro and function for creating
15//!   complex motion sequences.
16//! - [`Servo`] — Direct servo control without animation support. Use `Servo` for direct,
17//!   immediate control; use `servo_player` when you want motion to continue in the background.
18
19#![doc = include_str!("../docs/how_servos_work.md")]
20
21//!
22//! This device abstraction, `servo_player`, adds a background software task around the hardware
23//! control signal.
24//!
25//! # Controlling Multiple Servos
26//!
27//! Supports up to eight servos, one per [PWM slice](crate#glossary) resource. To calculate which PWM slice a pin uses,
28//! use the formula: `PWM slice = (pin / 2) % 8`. For example, PIN_10 and PIN_11 must both use PWM_SLICE5
29//! ((10 / 2) % 8 = 5, (11 / 2) % 8 = 5). Therefore, either of these these two pins can have a servo, but not both.
30//!
31//!
32//! # Example: Basic Servo Control
33//!
34//! This example demonstrates basic servo control: moving to a position, holding, relaxing,
35//! and using animation. Here, the generated struct type is named `ServoPlayer11`.
36//!
37//! ```rust,no_run
38//! # #![no_std]
39//! # #![no_main]
40//! # use panic_probe as _;
41//! # use core::convert::Infallible;
42//! # use core::default::Default;
43//! # use core::result::Result::Ok;
44//! use device_envoy::{Result, servo_player::{AtEnd, servo_player}};
45//! use embassy_time::{Duration, Timer};
46//!
47//! // Define ServoPlayer11, a struct type for a servo on PIN_11.
48//! servo_player! {
49//!     ServoPlayer11 {
50//!         pin: PIN_11,  // GPIO pin for servo
51//!         // other inputs set to their defaults
52//!     }
53//! }
54//!
55//! # #[embassy_executor::main]
56//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
57//! #     let err = example(spawner).await.unwrap_err();
58//! #     core::panic!("{err}");
59//! # }
60//! async fn example(spawner: embassy_executor::Spawner) -> Result<Infallible> {
61//!     let p = embassy_rp::init(Default::default());
62//!
63//!     // PIN_11 uses PWM_SLICE5 (pin / 2) % 8 = (11 / 2) % 8 = 5 % 8 = 5)
64//!     let servo_player11 = ServoPlayer11::new(p.PIN_11, p.PWM_SLICE5, spawner)?;
65//!
66//!     // Move to 90°, wait 1 second, then relax.
67//!     servo_player11.set_degrees(90);
68//!     Timer::after(Duration::from_secs(1)).await;
69//!     servo_player11.relax();
70//!
71//!     // Animate: hold at 180° for 1 second, then 0° for 1 second, then relax.
72//!     const STEPS: [(u16, Duration); 2] = [
73//!         (180, Duration::from_secs(1)),
74//!         (0, Duration::from_secs(1)),
75//!     ];
76//!     // AtEnd::Relax quiets the servo; AtEnd::Hold keeps driving pulses to hold
77//!     // position; AtEnd::Loop repeats.
78//!     servo_player11.animate(STEPS, AtEnd::Relax);
79//!
80//!     core::future::pending().await // run forever
81//! }
82//! ```
83//!
84//! # Example: Multi-Step Animation
85//!
86//! This example combines 40 animation steps using `linear` and`combine!` to
87//! sweep up, hold, sweep down, hold pattern. Here, the generated struct type is named
88//! `ServoSweep`.
89//!
90//! ```rust,no_run
91//! # #![no_std]
92//! # #![no_main]
93//! # use panic_probe as _;
94//! # use core::convert::Infallible;
95//! # use core::default::Default;
96//! # use core::result::Result::Ok;
97//! use device_envoy::{Result, servo_player::{AtEnd, combine, linear, servo_player}};
98//! use embassy_time::Duration;
99//!
100//! // Define ServoSweep, a struct type for a servo on PIN_12.
101//! servo_player! {
102//!     ServoSweep {
103//!         pin: PIN_12,
104//!         max_steps: 40,          // Increase from default (16) to hold animation steps
105//!
106//!        // Optional
107//!         min_us: 500,            // Minimum pulse width (µs) for 0° (default)
108//!         max_us: 2500,           // Maximum pulse width (µs) for max_degrees (default)
109//!         max_degrees: 180,       // Maximum servo angle (degrees) (default)
110//!     }
111//! }
112//!
113//! # #[embassy_executor::main]
114//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
115//! #     let err = example(spawner).await.unwrap_err();
116//! #     core::panic!("{err}");
117//! # }
118//! async fn example(spawner: embassy_executor::Spawner) -> Result<Infallible> {
119//!     let p = embassy_rp::init(Default::default());
120//!     let servo_sweep = ServoSweep::new(p.PIN_12, p.PWM_SLICE6, spawner)?;
121//!
122//!     // Combine 40 animation steps into one array.
123//!     const STEPS: [(u16, Duration); 40] = combine!(
124//!         linear::<19>(0, 180, Duration::from_secs(2)), // 19 steps from 0° to 180°
125//!         [(180, Duration::from_millis(400))],          // Hold at 180° for 400 ms
126//!         linear::<19>(180, 0, Duration::from_secs(2)), // 19 steps from 180° to 0°
127//!         [(0, Duration::from_millis(400))]             // Hold at 0° for 400 ms
128//!     );
129//!
130//!     servo_sweep.animate(STEPS, AtEnd::Loop); // Loop the sweep animation
131//!
132//!     // Let it run in the background for 10 seconds, then relax.
133//!     embassy_time::Timer::after(Duration::from_secs(10)).await;
134//!     servo_sweep.relax();
135//!
136//!     core::future::pending().await // run forever
137//! }
138//! ```
139
140use crate::servo::Servo;
141use core::borrow::Borrow;
142use embassy_futures::select::{Either, select};
143use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
144use embassy_sync::signal::Signal;
145use embassy_time::{Duration, Timer};
146use heapless::Vec;
147
148#[doc(inline)]
149pub use crate::combine;
150/// Re-exported [`servo!`](macro@crate::servo) macro from the [`servo`](mod@crate::servo)
151/// module for convenience.
152///
153/// See the [`servo`](mod@crate::servo) module for direct servo control without animation.
154pub use crate::servo::servo;
155#[doc(hidden)]
156pub use paste;
157
158// ============================================================================
159// Submodules
160// ============================================================================
161
162pub mod servo_player_generated;
163
164/// Commands sent to the servo player device.
165enum PlayerCommand<const MAX_STEPS: usize> {
166    Set {
167        degrees: u16,
168    },
169    Animate {
170        steps: Vec<(u16, Duration), MAX_STEPS>,
171        mode: AtEnd,
172    },
173    Hold,
174    Relax,
175}
176
177/// Animation end behavior.
178///
179/// See the [servo_player module documentation](mod@crate::servo_player) for usage.
180#[derive(Clone, Copy, Debug, defmt::Format)]
181pub enum AtEnd {
182    /// Repeat the animation sequence indefinitely.
183    Loop,
184    /// Hold the final position when animation completes.
185    Hold,
186    /// Stop holding position after animation completes (servo relaxes).
187    Relax,
188}
189
190/// Build a const linear sequence of animation steps as an array.
191///
192/// Returns a fixed-size array with `N` steps interpolating linearly from `start_degrees` to
193/// `end_degrees` over `total_duration`. Can be used in const contexts.
194///
195/// See the [servo_player module documentation](mod@crate::servo_player) for usage.
196///
197/// # Parameters
198///
199/// - `N` — Number of steps in the sequence (const generic parameter)
200/// - `start_degrees` — Starting angle in degrees
201/// - `end_degrees` — Ending angle in degrees
202/// - `total_duration` — Total time for the entire sequence
203#[must_use]
204pub const fn linear<const N: usize>(
205    start_degrees: u16,
206    end_degrees: u16,
207    total_duration: Duration,
208) -> [(u16, Duration); N] {
209    assert!(N > 0, "at least one step required");
210    let step_duration = Duration::from_micros(total_duration.as_micros() / (N as u64));
211    let delta = end_degrees as i32 - start_degrees as i32;
212    let denom = if N == 1 { 1 } else { (N - 1) as i32 };
213
214    let mut result = [(0u16, Duration::from_micros(0)); N];
215    let mut step_index = 0;
216    while step_index < N {
217        let degrees = if N == 1 {
218            start_degrees
219        } else {
220            let step_delta = delta * (step_index as i32) / denom;
221            (start_degrees as i32 + step_delta) as u16
222        };
223        result[step_index] = (degrees, step_duration);
224        step_index += 1;
225    }
226    result
227}
228
229/// Combine two animation step arrays into one larger array.
230///
231/// For combining more than two arrays, use the `combine!` macro.
232///
233/// # Example
234///
235/// ```rust,no_run
236/// # #![no_std]
237/// # #![no_main]
238/// # use embassy_time::Duration;
239/// # use device_envoy::servo_player::{combine, linear};
240/// # use panic_probe as _;
241/// const SWEEP_UP: [(u16, Duration); 19] = linear(0, 180, Duration::from_secs(2));
242/// const HOLD: [(u16, Duration); 1] = [(180, Duration::from_millis(400))];
243/// const COMBINED: [(u16, Duration); 20] = combine(SWEEP_UP, HOLD);
244/// ```
245#[must_use]
246#[doc(hidden)]
247pub const fn combine<const N1: usize, const N2: usize, const OUT_N: usize>(
248    first: [(u16, Duration); N1],
249    second: [(u16, Duration); N2],
250) -> [(u16, Duration); OUT_N] {
251    assert!(OUT_N == N1 + N2, "OUT_N must equal N1 + N2");
252
253    let mut result = [(0u16, Duration::from_micros(0)); OUT_N];
254    let mut i = 0;
255    while i < N1 {
256        result[i] = first[i];
257        i += 1;
258    }
259    let mut j = 0;
260    while j < N2 {
261        result[N1 + j] = second[j];
262        j += 1;
263    }
264    result
265}
266
267/// Combine multiple animation step arrays into one larger array.
268///
269/// This macro allows combining any number of const arrays with a clean syntax.
270///
271/// **Syntax:**
272///
273/// ```text
274/// combine!()
275/// combine!(<steps_expr>)
276/// combine!(<first_steps_expr>, <second_steps_expr>, ... )
277/// ```
278///
279/// See the [servo_player module documentation](mod@crate::servo_player) for usage.
280#[doc(hidden)]
281#[macro_export]
282macro_rules! combine {
283    () => {
284        []
285    };
286    ($single:expr) => {
287        $single
288    };
289    ($first:expr, $second:expr) => {{
290        const FIRST: &[(u16, ::embassy_time::Duration)] = &$first;
291        const SECOND: &[(u16, ::embassy_time::Duration)] = &$second;
292        $crate::servo_player::combine::<{FIRST.len()}, {SECOND.len()}, {FIRST.len() + SECOND.len()}>($first, $second)
293    }};
294    ($first:expr, $($rest:expr),+ $(,)?) => {{
295        const FIRST: &[(u16, ::embassy_time::Duration)] = &$first;
296        const REST: &[(u16, ::embassy_time::Duration)] = &$crate::combine!($($rest),+);
297        $crate::servo_player::combine::<{FIRST.len()}, {REST.len()}, {FIRST.len() + REST.len()}>($first, $crate::combine!($($rest),+))
298    }};
299}
300
301// Public so macro-generated types can reference it; hidden from docs.
302#[doc(hidden)]
303/// Static resources for [`ServoPlayer`].
304pub struct ServoPlayerStatic<const MAX_STEPS: usize> {
305    command: Signal<CriticalSectionRawMutex, PlayerCommand<MAX_STEPS>>,
306}
307
308impl<const MAX_STEPS: usize> ServoPlayerStatic<MAX_STEPS> {
309    /// Create static resources for the servo player device.
310    #[must_use]
311    pub const fn new_static() -> Self {
312        Self {
313            command: Signal::new(),
314        }
315    }
316
317    fn signal(&self, command: PlayerCommand<MAX_STEPS>) {
318        self.command.signal(command);
319    }
320
321    async fn wait(&self) -> PlayerCommand<MAX_STEPS> {
322        self.command.wait().await
323    }
324}
325
326// Public so macro-generated types can deref to it; hidden from docs.
327#[doc(hidden)]
328/// Internal deref target for generated servo player types.
329///
330/// All servo player methods are available through macro-generated types.
331/// See [`servo_player!`] macro documentation for usage.
332pub struct ServoPlayer<const MAX_STEPS: usize> {
333    servo_player_static: &'static ServoPlayerStatic<MAX_STEPS>,
334}
335
336impl<const MAX_STEPS: usize> ServoPlayer<MAX_STEPS> {
337    /// Create static resources for a servo player.
338    #[must_use]
339    pub const fn new_static() -> ServoPlayerStatic<MAX_STEPS> {
340        ServoPlayerStatic::new_static()
341    }
342
343    /// Create a servo player handle. The device loop must already be running.
344    ///
345    /// See the [servo_player module documentation](mod@crate::servo_player) for usage.
346    #[must_use]
347    pub const fn new(servo_player_static: &'static ServoPlayerStatic<MAX_STEPS>) -> Self {
348        Self {
349            servo_player_static,
350        }
351    }
352
353    /// Set the target angle. The most recent command always wins.
354    ///
355    /// See the [servo_player module documentation](mod@crate::servo_player) for
356    /// usage.
357    pub fn set_degrees(&self, degrees: u16) {
358        self.servo_player_static
359            .signal(PlayerCommand::Set { degrees });
360    }
361
362    /// Hold the servo at its current position.
363    ///
364    /// See the [servo_player module documentation](mod@crate::servo_player) for
365    /// usage.
366    pub fn hold(&self) {
367        self.servo_player_static.signal(PlayerCommand::Hold);
368    }
369
370    /// Relax the servo (stops holding position, servo can move freely).
371    ///
372    /// See the [servo_player module documentation](mod@crate::servo_player) for
373    /// usage.
374    pub fn relax(&self) {
375        self.servo_player_static.signal(PlayerCommand::Relax);
376    }
377
378    /// Animate the servo through a sequence of angles with per-step hold durations.
379    ///
380    /// Each step is a tuple `(degrees, duration)`. Accepts both owned iterators and
381    /// references to collections.
382    ///
383    /// See the [servo_player module documentation](mod@crate::servo_player) for
384    /// usage.
385    pub fn animate<I>(&self, steps: I, at_end: AtEnd)
386    where
387        I: IntoIterator,
388        I::Item: Borrow<(u16, Duration)>,
389    {
390        assert!(MAX_STEPS > 0, "animate disabled: max_steps is 0");
391        let mut sequence: Vec<(u16, Duration), MAX_STEPS> = Vec::new();
392        for step in steps {
393            let step = *step.borrow();
394            assert!(
395                step.1.as_micros() > 0,
396                "animation step duration must be positive"
397            );
398            sequence
399                .push(step)
400                .expect("animate sequence fits within max_steps");
401        }
402        assert!(!sequence.is_empty(), "animate requires at least one step");
403
404        self.servo_player_static.signal(PlayerCommand::Animate {
405            steps: sequence,
406            mode: at_end,
407        });
408    }
409}
410
411/// Macro to generate a servo player struct type (includes syntax details).
412///
413/// This page provides the primary documentation for configuring individual servo players.
414///
415/// See the [servo_player module documentation](mod@crate::servo_player) for complete
416/// examples.
417
418///
419/// **After reading the configuration details below, see also:**
420///
421/// - [`ServoPlayerGenerated`](servo_player_generated::ServoPlayerGenerated) — Sample servo
422///   player type showing all methods and associated constants
423/// - [`servo_player`](mod@crate::servo_player) module — Complete examples and usage
424///   patterns
425///
426/// Use this macro when your project has a servo that needs scripted animation control.
427/// The macro generates a struct type and spawns a background
428/// task to execute animation sequences.
429///
430/// **Syntax:**
431///
432/// ```text
433/// servo_player! {
434///     [<visibility>] <Name> {
435///         pin: <pin_ident>,
436///         min_us: <u16_expr>,         // optional
437///         max_us: <u16_expr>,         // optional
438///         max_degrees: <u16_expr>,    // optional
439///         max_steps: <usize_expr>,    // optional
440///     }
441/// }
442/// ```
443///
444/// # Configuration
445///
446/// ## Required Fields
447///
448/// - `pin` — GPIO pin for servo
449///
450/// ## Optional Fields
451///
452/// - `min_us` — Minimum pulse width in microseconds for 0° (default: 500)
453/// - `max_us` — Maximum pulse width in microseconds for max_degrees
454///   (default: 2500)
455/// - `max_degrees` — Maximum servo angle in degrees (default: 180)
456/// - `max_steps` — Maximum number of animation steps (default: 16)
457///
458/// `max_steps = 0` disables animation and allocates no step storage; `set_degrees()`,
459/// `hold()`, and `relax()` are still supported.
460
461#[cfg(not(feature = "host"))]
462#[doc(hidden)]
463#[macro_export]
464macro_rules! servo_player {
465    ($($tt:tt)*) => { $crate::__servo_player_impl! { $($tt)* } };
466}
467#[doc(inline)]
468pub use servo_player;
469
470// Public for macro expansion in downstream crates.
471#[doc(hidden)]
472#[macro_export]
473macro_rules! __servo_player_impl {
474    // Entry point - name without visibility defaults to public
475    (
476        $name:ident {
477            $($fields:tt)*
478        }
479    ) => {
480        $crate::__servo_player_impl! {
481            @__fill_defaults
482            vis: pub(self),
483            name: $name,
484            pin: _UNSET_,
485            slice: _UNSET_,
486            channel: _UNSET_,
487            min_us: $crate::servo::SERVO_MIN_US_DEFAULT,
488            max_us: $crate::servo::SERVO_MAX_US_DEFAULT,
489            max_degrees: $crate::servo::Servo::DEFAULT_MAX_DEGREES,
490            max_steps: 16,
491            fields: [ $($fields)* ]
492        }
493    };
494
495    // Entry point - name with explicit visibility
496    (
497        $vis:vis $name:ident {
498            $($fields:tt)*
499        }
500    ) => {
501        $crate::__servo_player_impl! {
502            @__fill_defaults
503            vis: $vis,
504            name: $name,
505            pin: _UNSET_,
506            slice: _UNSET_,
507            channel: _UNSET_,
508            min_us: $crate::servo::SERVO_MIN_US_DEFAULT,
509            max_us: $crate::servo::SERVO_MAX_US_DEFAULT,
510            max_degrees: $crate::servo::Servo::DEFAULT_MAX_DEGREES,
511            max_steps: 16,
512            fields: [ $($fields)* ]
513        }
514    };
515
516    // Fill defaults: pin
517    (@__fill_defaults
518        vis: $vis:vis,
519        name: $name:ident,
520        pin: $pin:tt,
521        slice: $slice:tt,
522        channel: $channel:tt,
523        min_us: $min_us:expr,
524        max_us: $max_us:expr,
525        max_degrees: $max_degrees:expr,
526        max_steps: $max_steps:expr,
527        fields: [ pin: $pin_value:ident $(, $($rest:tt)* )? ]
528    ) => {
529        $crate::__servo_player_impl! {
530            @__fill_defaults
531            vis: $vis,
532            name: $name,
533            pin: $pin_value,
534            slice: $slice,
535            channel: $channel,
536            min_us: $min_us,
537            max_us: $max_us,
538            max_degrees: $max_degrees,
539            max_steps: $max_steps,
540            fields: [ $($($rest)*)? ]
541        }
542    };
543
544    (@__fill_defaults
545        vis: $vis:vis,
546        name: $name:ident,
547        pin: $pin:tt,
548        slice: $slice:tt,
549        channel: $channel:tt,
550        min_us: $min_us:expr,
551        max_us: $max_us:expr,
552        max_degrees: $max_degrees:expr,
553        max_steps: $max_steps:expr,
554        fields: [ pin: $pin_value:ident ]
555    ) => {
556        $crate::__servo_player_impl! {
557            @__fill_defaults
558            vis: $vis,
559            name: $name,
560            pin: $pin_value,
561            slice: $slice,
562            channel: $channel,
563            min_us: $min_us,
564            max_us: $max_us,
565            max_degrees: $max_degrees,
566            max_steps: $max_steps,
567            fields: [ ]
568        }
569    };
570
571    // Fill defaults: slice
572    (@__fill_defaults
573        vis: $vis:vis,
574        name: $name:ident,
575        pin: $pin:tt,
576        slice: $slice:tt,
577        channel: $channel:tt,
578        min_us: $min_us:expr,
579        max_us: $max_us:expr,
580        max_degrees: $max_degrees:expr,
581        max_steps: $max_steps:expr,
582        fields: [ slice: $slice_value:ident $(, $($rest:tt)* )? ]
583    ) => {
584        $crate::__servo_player_impl! {
585            @__fill_defaults
586            vis: $vis,
587            name: $name,
588            pin: $pin,
589            slice: $slice_value,
590            channel: $channel,
591            min_us: $min_us,
592            max_us: $max_us,
593            max_degrees: $max_degrees,
594            max_steps: $max_steps,
595            fields: [ $($($rest)*)? ]
596        }
597    };
598
599    (@__fill_defaults
600        vis: $vis:vis,
601        name: $name:ident,
602        pin: $pin:tt,
603        slice: $slice:tt,
604        channel: $channel:tt,
605        min_us: $min_us:expr,
606        max_us: $max_us:expr,
607        max_degrees: $max_degrees:expr,
608        max_steps: $max_steps:expr,
609        fields: [ slice: $slice_value:ident ]
610    ) => {
611        $crate::__servo_player_impl! {
612            @__fill_defaults
613            vis: $vis,
614            name: $name,
615            pin: $pin,
616            slice: $slice_value,
617            channel: $channel,
618            min_us: $min_us,
619            max_us: $max_us,
620            max_degrees: $max_degrees,
621            max_steps: $max_steps,
622            fields: [ ]
623        }
624    };
625
626    // Fill defaults: min_us
627    (@__fill_defaults
628        vis: $vis:vis,
629        name: $name:ident,
630        pin: $pin:tt,
631        slice: $slice:tt,
632        channel: $channel:tt,
633        min_us: $min_us:expr,
634        max_us: $max_us:expr,
635        max_degrees: $max_degrees:expr,
636        max_steps: $max_steps:expr,
637        fields: [ min_us: $min_us_value:expr $(, $($rest:tt)* )? ]
638    ) => {
639        $crate::__servo_player_impl! {
640            @__fill_defaults
641            vis: $vis,
642            name: $name,
643            pin: $pin,
644            slice: $slice,
645            channel: $channel,
646            min_us: $min_us_value,
647            max_us: $max_us,
648            max_degrees: $max_degrees,
649            max_steps: $max_steps,
650            fields: [ $($($rest)*)? ]
651        }
652    };
653
654    (@__fill_defaults
655        vis: $vis:vis,
656        name: $name:ident,
657        pin: $pin:tt,
658        slice: $slice:tt,
659        channel: $channel:tt,
660        min_us: $min_us:expr,
661        max_us: $max_us:expr,
662        max_degrees: $max_degrees:expr,
663        max_steps: $max_steps:expr,
664        fields: [ min_us: $min_us_value:expr ]
665    ) => {
666        $crate::__servo_player_impl! {
667            @__fill_defaults
668            vis: $vis,
669            name: $name,
670            pin: $pin,
671            slice: $slice,
672            channel: $channel,
673            min_us: $min_us_value,
674            max_us: $max_us,
675            max_degrees: $max_degrees,
676            max_steps: $max_steps,
677            fields: [ ]
678        }
679    };
680
681    // Fill defaults: max_us
682    (@__fill_defaults
683        vis: $vis:vis,
684        name: $name:ident,
685        pin: $pin:tt,
686        slice: $slice:tt,
687        channel: $channel:tt,
688        min_us: $min_us:expr,
689        max_us: $max_us:expr,
690        max_degrees: $max_degrees:expr,
691        max_steps: $max_steps:expr,
692        fields: [ max_us: $max_us_value:expr $(, $($rest:tt)* )? ]
693    ) => {
694        $crate::__servo_player_impl! {
695            @__fill_defaults
696            vis: $vis,
697            name: $name,
698            pin: $pin,
699            slice: $slice,
700            channel: $channel,
701            min_us: $min_us,
702            max_us: $max_us_value,
703            max_degrees: $max_degrees,
704            max_steps: $max_steps,
705            fields: [ $($($rest)*)? ]
706        }
707    };
708
709    (@__fill_defaults
710        vis: $vis:vis,
711        name: $name:ident,
712        pin: $pin:tt,
713        slice: $slice:tt,
714        channel: $channel:tt,
715        min_us: $min_us:expr,
716        max_us: $max_us:expr,
717        max_degrees: $max_degrees:expr,
718        max_steps: $max_steps:expr,
719        fields: [ max_us: $max_us_value:expr ]
720    ) => {
721        $crate::__servo_player_impl! {
722            @__fill_defaults
723            vis: $vis,
724            name: $name,
725            pin: $pin,
726            slice: $slice,
727            channel: $channel,
728            min_us: $min_us,
729            max_us: $max_us_value,
730            max_degrees: $max_degrees,
731            max_steps: $max_steps,
732            fields: [ ]
733        }
734    };
735
736    // Fill defaults: max_degrees
737    (@__fill_defaults
738        vis: $vis:vis,
739        name: $name:ident,
740        pin: $pin:tt,
741        slice: $slice:tt,
742        channel: $channel:tt,
743        min_us: $min_us:expr,
744        max_us: $max_us:expr,
745        max_degrees: $max_degrees:expr,
746        max_steps: $max_steps:expr,
747        fields: [ max_degrees: $max_degrees_value:expr $(, $($rest:tt)* )? ]
748    ) => {
749        $crate::__servo_player_impl! {
750            @__fill_defaults
751            vis: $vis,
752            name: $name,
753            pin: $pin,
754            slice: $slice,
755            channel: $channel,
756            min_us: $min_us,
757            max_us: $max_us,
758            max_degrees: $max_degrees_value,
759            max_steps: $max_steps,
760            fields: [ $($($rest)*)? ]
761        }
762    };
763
764    (@__fill_defaults
765        vis: $vis:vis,
766        name: $name:ident,
767        pin: $pin:tt,
768        slice: $slice:tt,
769        channel: $channel:tt,
770        min_us: $min_us:expr,
771        max_us: $max_us:expr,
772        max_degrees: $max_degrees:expr,
773        max_steps: $max_steps:expr,
774        fields: [ max_degrees: $max_degrees_value:expr ]
775    ) => {
776        $crate::__servo_player_impl! {
777            @__fill_defaults
778            vis: $vis,
779            name: $name,
780            pin: $pin,
781            slice: $slice,
782            channel: $channel,
783            min_us: $min_us,
784            max_us: $max_us,
785            max_degrees: $max_degrees_value,
786            max_steps: $max_steps,
787            fields: [ ]
788        }
789    };
790
791    // Fill defaults: max_steps
792    (@__fill_defaults
793        vis: $vis:vis,
794        name: $name:ident,
795        pin: $pin:tt,
796        slice: $slice:tt,
797        channel: $channel:tt,
798        min_us: $min_us:expr,
799        max_us: $max_us:expr,
800        max_degrees: $max_degrees:expr,
801        max_steps: $max_steps:expr,
802        fields: [ max_steps: $max_steps_value:expr $(, $($rest:tt)* )? ]
803    ) => {
804        $crate::__servo_player_impl! {
805            @__fill_defaults
806            vis: $vis,
807            name: $name,
808            pin: $pin,
809            slice: $slice,
810            channel: $channel,
811            min_us: $min_us,
812            max_us: $max_us,
813            max_degrees: $max_degrees,
814            max_steps: $max_steps_value,
815            fields: [ $($($rest)*)? ]
816        }
817    };
818
819    (@__fill_defaults
820        vis: $vis:vis,
821        name: $name:ident,
822        pin: $pin:tt,
823        slice: $slice:tt,
824        channel: $channel:tt,
825        min_us: $min_us:expr,
826        max_us: $max_us:expr,
827        max_degrees: $max_degrees:expr,
828        max_steps: $max_steps:expr,
829        fields: [ max_steps: $max_steps_value:expr ]
830    ) => {
831        $crate::__servo_player_impl! {
832            @__fill_defaults
833            vis: $vis,
834            name: $name,
835            pin: $pin,
836            slice: $slice,
837            channel: $channel,
838            min_us: $min_us,
839            max_us: $max_us,
840            max_degrees: $max_degrees,
841            max_steps: $max_steps_value,
842            fields: [ ]
843        }
844    };
845
846    // Fill defaults: channel overrides
847    (@__fill_defaults
848        vis: $vis:vis,
849        name: $name:ident,
850        pin: $pin:tt,
851        slice: $slice:tt,
852        channel: $channel:tt,
853        min_us: $min_us:expr,
854        max_us: $max_us:expr,
855        max_degrees: $max_degrees:expr,
856        max_steps: $max_steps:expr,
857        fields: [ channel: A $(, $($rest:tt)* )? ]
858    ) => {
859        $crate::__servo_player_impl! {
860            @__fill_defaults
861            vis: $vis,
862            name: $name,
863            pin: $pin,
864            slice: $slice,
865            channel: A,
866            min_us: $min_us,
867            max_us: $max_us,
868            max_degrees: $max_degrees,
869            max_steps: $max_steps,
870            fields: [ $($($rest)*)? ]
871        }
872    };
873
874    (@__fill_defaults
875        vis: $vis:vis,
876        name: $name:ident,
877        pin: $pin:tt,
878        slice: $slice:tt,
879        channel: $channel:tt,
880        min_us: $min_us:expr,
881        max_us: $max_us:expr,
882        max_degrees: $max_degrees:expr,
883        max_steps: $max_steps:expr,
884        fields: [ channel: A ]
885    ) => {
886        $crate::__servo_player_impl! {
887            @__fill_defaults
888            vis: $vis,
889            name: $name,
890            pin: $pin,
891            slice: $slice,
892            channel: A,
893            min_us: $min_us,
894            max_us: $max_us,
895            max_degrees: $max_degrees,
896            max_steps: $max_steps,
897            fields: [ ]
898        }
899    };
900
901    (@__fill_defaults
902        vis: $vis:vis,
903        name: $name:ident,
904        pin: $pin:tt,
905        slice: $slice:tt,
906        channel: $channel:tt,
907        min_us: $min_us:expr,
908        max_us: $max_us:expr,
909        max_degrees: $max_degrees:expr,
910        max_steps: $max_steps:expr,
911        fields: [ channel: B $(, $($rest:tt)* )? ]
912    ) => {
913        $crate::__servo_player_impl! {
914            @__fill_defaults
915            vis: $vis,
916            name: $name,
917            pin: $pin,
918            slice: $slice,
919            channel: B,
920            min_us: $min_us,
921            max_us: $max_us,
922            max_degrees: $max_degrees,
923            max_steps: $max_steps,
924            fields: [ $($($rest)*)? ]
925        }
926    };
927
928    (@__fill_defaults
929        vis: $vis:vis,
930        name: $name:ident,
931        pin: $pin:tt,
932        slice: $slice:tt,
933        channel: $channel:tt,
934        min_us: $min_us:expr,
935        max_us: $max_us:expr,
936        max_degrees: $max_degrees:expr,
937        max_steps: $max_steps:expr,
938        fields: [ channel: B ]
939    ) => {
940        $crate::__servo_player_impl! {
941            @__fill_defaults
942            vis: $vis,
943            name: $name,
944            pin: $pin,
945            slice: $slice,
946            channel: B,
947            min_us: $min_us,
948            max_us: $max_us,
949            max_degrees: $max_degrees,
950            max_steps: $max_steps,
951            fields: [ ]
952        }
953    };
954
955    (@__fill_defaults
956        vis: $vis:vis,
957        name: $name:ident,
958        pin: $pin:tt,
959        slice: $slice:tt,
960        channel: $channel:tt,
961        min_us: $min_us:expr,
962        max_us: $max_us:expr,
963        max_degrees: $max_degrees:expr,
964        max_steps: $max_steps:expr,
965        fields: [ even $(, $($rest:tt)* )? ]
966    ) => {
967        $crate::__servo_player_impl! {
968            @__fill_defaults
969            vis: $vis,
970            name: $name,
971            pin: $pin,
972            slice: $slice,
973            channel: A,
974            min_us: $min_us,
975            max_us: $max_us,
976            max_degrees: $max_degrees,
977            max_steps: $max_steps,
978            fields: [ $($($rest)*)? ]
979        }
980    };
981
982    (@__fill_defaults
983        vis: $vis:vis,
984        name: $name:ident,
985        pin: $pin:tt,
986        slice: $slice:tt,
987        channel: $channel:tt,
988        min_us: $min_us:expr,
989        max_us: $max_us:expr,
990        max_degrees: $max_degrees:expr,
991        max_steps: $max_steps:expr,
992        fields: [ even ]
993    ) => {
994        $crate::__servo_player_impl! {
995            @__fill_defaults
996            vis: $vis,
997            name: $name,
998            pin: $pin,
999            slice: $slice,
1000            channel: A,
1001            min_us: $min_us,
1002            max_us: $max_us,
1003            max_degrees: $max_degrees,
1004            max_steps: $max_steps,
1005            fields: [ ]
1006        }
1007    };
1008
1009    (@__fill_defaults
1010        vis: $vis:vis,
1011        name: $name:ident,
1012        pin: $pin:tt,
1013        slice: $slice:tt,
1014        channel: $channel:tt,
1015        min_us: $min_us:expr,
1016        max_us: $max_us:expr,
1017        max_degrees: $max_degrees:expr,
1018        max_steps: $max_steps:expr,
1019        fields: [ odd $(, $($rest:tt)* )? ]
1020    ) => {
1021        $crate::__servo_player_impl! {
1022            @__fill_defaults
1023            vis: $vis,
1024            name: $name,
1025            pin: $pin,
1026            slice: $slice,
1027            channel: B,
1028            min_us: $min_us,
1029            max_us: $max_us,
1030            max_degrees: $max_degrees,
1031            max_steps: $max_steps,
1032            fields: [ $($($rest)*)? ]
1033        }
1034    };
1035
1036    (@__fill_defaults
1037        vis: $vis:vis,
1038        name: $name:ident,
1039        pin: $pin:tt,
1040        slice: $slice:tt,
1041        channel: $channel:tt,
1042        min_us: $min_us:expr,
1043        max_us: $max_us:expr,
1044        max_degrees: $max_degrees:expr,
1045        max_steps: $max_steps:expr,
1046        fields: [ odd ]
1047    ) => {
1048        $crate::__servo_player_impl! {
1049            @__fill_defaults
1050            vis: $vis,
1051            name: $name,
1052            pin: $pin,
1053            slice: $slice,
1054            channel: B,
1055            min_us: $min_us,
1056            max_us: $max_us,
1057            max_degrees: $max_degrees,
1058            max_steps: $max_steps,
1059            fields: [ ]
1060        }
1061    };
1062
1063    // Fill defaults: terminate and build
1064    (@__fill_defaults
1065        vis: $vis:vis,
1066        name: $name:ident,
1067        pin: $pin:tt,
1068        slice: $slice:tt,
1069        channel: $channel:tt,
1070        min_us: $min_us:expr,
1071        max_us: $max_us:expr,
1072        max_degrees: $max_degrees:expr,
1073        max_steps: $max_steps:expr,
1074        fields: [ ]
1075    ) => {
1076        $crate::__servo_player_impl! {
1077            @__build
1078            vis: $vis,
1079            name: $name,
1080            pin: $pin,
1081            slice: $slice,
1082            channel: $channel,
1083            min_us: $min_us,
1084            max_us: $max_us,
1085            max_degrees: $max_degrees,
1086            max_steps: $max_steps
1087        }
1088    };
1089
1090    // Build errors for missing fields
1091    (@__build
1092        vis: $vis:vis,
1093        name: $name:ident,
1094        pin: _UNSET_,
1095        slice: $slice:tt,
1096        channel: $channel:tt,
1097        min_us: $min_us:expr,
1098        max_us: $max_us:expr,
1099        max_degrees: $max_degrees:expr,
1100        max_steps: $max_steps:expr
1101    ) => {
1102        compile_error!("servo_player! requires `pin: ...`");
1103    };
1104
1105    // Build with all fields set (slice can be _UNSET_ - it's in the new() signature)
1106    (@__build
1107        vis: $vis:vis,
1108        name: $name:ident,
1109        pin: $pin:ident,
1110        slice: _UNSET_,
1111        channel: $channel:tt,
1112        min_us: $min_us:expr,
1113        max_us: $max_us:expr,
1114        max_degrees: $max_degrees:expr,
1115        max_steps: $max_steps:expr
1116    ) => {
1117        $crate::servo_player::paste::paste! {
1118            static [<$name:upper _SERVO_PLAYER_STATIC>]: $crate::servo_player::ServoPlayerStatic<$max_steps> =
1119                $crate::servo_player::ServoPlayer::<$max_steps>::new_static();
1120            static [<$name:upper _SERVO_PLAYER_CELL>]: ::static_cell::StaticCell<$name> =
1121                ::static_cell::StaticCell::new();
1122
1123            #[allow(missing_docs)]
1124            $vis struct $name {
1125                player: $crate::servo_player::ServoPlayer<$max_steps>,
1126            }
1127
1128            #[allow(missing_docs)]
1129            impl $name {
1130                /// Create the servo player and spawn its background task.
1131                ///
1132                /// The slice is automatically determined from the pin via the type
1133                /// system.
1134                ///
1135                /// # PWM Slice Calculation
1136                ///
1137                /// Calculate which [PWM slice](crate#glossary) a pin uses:
1138                /// `slice = (pin / 2) % 8`. For example, PIN_11 uses PWM_SLICE5
1139                /// ((11 / 2) % 8 = 5).
1140                ///
1141                /// # Parameters
1142                ///
1143                /// - `pin` — GPIO pin for servo
1144                /// - `slice` — PWM slice corresponding to the pin
1145                /// - `spawner` — Task spawner for background operations
1146                ///
1147                /// See the `ServoPlayer` struct example for usage.
1148                pub fn new<S: 'static>(
1149                    pin: impl Into<::embassy_rp::Peri<'static, ::embassy_rp::peripherals::$pin>>,
1150                    slice: impl Into<::embassy_rp::Peri<'static, S>>,
1151                    spawner: ::embassy_executor::Spawner,
1152                ) -> $crate::Result<&'static Self>
1153                where
1154                    ::embassy_rp::peripherals::$pin: $crate::servo::ServoPwmPin<S>,
1155                    S: ::embassy_rp::PeripheralType,
1156                {
1157                    let pin = pin.into();
1158                    let slice = slice.into();
1159                    let servo = $crate::servo::servo_from_pin_slice(
1160                        pin,
1161                        slice,
1162                        $min_us,
1163                        $max_us,
1164                        $max_degrees
1165                    );
1166                    let token = [<$name:snake _servo_player_task>](&[<$name:upper _SERVO_PLAYER_STATIC>], servo);
1167                    spawner.spawn(token)?;
1168                    let player = $crate::servo_player::ServoPlayer::new(&[<$name:upper _SERVO_PLAYER_STATIC>]);
1169                    Ok([<$name:upper _SERVO_PLAYER_CELL>].init(Self { player }))
1170                }
1171            }
1172
1173            impl ::core::ops::Deref for $name {
1174                type Target = $crate::servo_player::ServoPlayer<$max_steps>;
1175
1176                fn deref(&self) -> &Self::Target {
1177                    &self.player
1178                }
1179            }
1180
1181            #[::embassy_executor::task]
1182            async fn [<$name:snake _servo_player_task>](
1183                servo_player_static: &'static $crate::servo_player::ServoPlayerStatic<$max_steps>,
1184                servo: $crate::servo::Servo<'static>,
1185            ) -> ! {
1186                $crate::servo_player::device_loop(servo_player_static, servo).await
1187            }
1188        }
1189    };
1190
1191    (@__build
1192        vis: $vis:vis,
1193        name: $name:ident,
1194        pin: $pin:ident,
1195        slice: $slice:ident,
1196        channel: $channel:tt,
1197        min_us: $min_us:expr,
1198        max_us: $max_us:expr,
1199        max_degrees: $max_degrees:expr,
1200        max_steps: $max_steps:expr
1201    ) => {
1202        $crate::servo_player::paste::paste! {
1203            static [<$name:upper _SERVO_PLAYER_STATIC>]: $crate::servo_player::ServoPlayerStatic<$max_steps> =
1204                $crate::servo_player::ServoPlayer::<$max_steps>::new_static();
1205            static [<$name:upper _SERVO_PLAYER_CELL>]: ::static_cell::StaticCell<$name> =
1206                ::static_cell::StaticCell::new();
1207
1208            #[allow(missing_docs)]
1209            $vis struct $name {
1210                player: $crate::servo_player::ServoPlayer<$max_steps>,
1211            }
1212
1213            #[allow(missing_docs)]
1214            impl $name {
1215                /// Create the servo player and spawn its background task.
1216                ///
1217                /// # PWM Slice Calculation
1218                ///
1219                /// Calculate which [PWM slice](crate#glossary) a pin uses:
1220                /// `slice = (pin / 2) % 8`. For example, PIN_11 uses PWM_SLICE5
1221                /// ((11 / 2) % 8 = 5).
1222                ///
1223                /// # Parameters
1224                ///
1225                /// - `pin` — GPIO pin for servo
1226                /// - `slice` — PWM slice corresponding to the pin
1227                /// - `spawner` — Task spawner for background operations
1228                ///
1229                /// See the `ServoPlayer` struct example for usage.
1230                pub fn new(
1231                    pin: impl Into<::embassy_rp::Peri<'static, ::embassy_rp::peripherals::$pin>>,
1232                    slice: impl Into<::embassy_rp::Peri<'static, ::embassy_rp::peripherals::$slice>>,
1233                    spawner: ::embassy_executor::Spawner,
1234                ) -> $crate::Result<&'static Self> {
1235                    let pin = pin.into();
1236                    let slice = slice.into();
1237                    let servo = $crate::__servo_player_impl! {
1238                        @__build_servo
1239                        pin: pin,
1240                        slice: slice,
1241                        channel: $channel,
1242                        min_us: $min_us,
1243                        max_us: $max_us,
1244                        max_degrees: $max_degrees
1245                    };
1246                    let token = [<$name:snake _servo_player_task>](&[<$name:upper _SERVO_PLAYER_STATIC>], servo);
1247                    spawner.spawn(token)?;
1248                    let player = $crate::servo_player::ServoPlayer::new(&[<$name:upper _SERVO_PLAYER_STATIC>]);
1249                    Ok([<$name:upper _SERVO_PLAYER_CELL>].init(Self { player }))
1250                }
1251            }
1252
1253            impl ::core::ops::Deref for $name {
1254                type Target = $crate::servo_player::ServoPlayer<$max_steps>;
1255
1256                fn deref(&self) -> &Self::Target {
1257                    &self.player
1258                }
1259            }
1260
1261            #[::embassy_executor::task]
1262            async fn [<$name:snake _servo_player_task>](
1263                servo_player_static: &'static $crate::servo_player::ServoPlayerStatic<$max_steps>,
1264                servo: $crate::servo::Servo<'static>,
1265            ) -> ! {
1266                $crate::servo_player::device_loop(servo_player_static, servo).await
1267            }
1268        }
1269    };
1270
1271    (@__build_servo
1272        pin: $pin:expr,
1273        slice: $slice:expr,
1274        channel: _UNSET_,
1275        min_us: $min_us:expr,
1276        max_us: $max_us:expr,
1277        max_degrees: $max_degrees:expr,
1278        max_steps: $max_steps:expr
1279    ) => {
1280        $crate::servo::servo_from_pin_slice($pin, $slice, $min_us, $max_us, $max_degrees)
1281    };
1282
1283    (@__build_servo
1284        pin: $pin:expr,
1285        slice: $slice:expr,
1286        channel: A,
1287        min_us: $min_us:expr,
1288        max_us: $max_us:expr,
1289        max_degrees: $max_degrees:expr,
1290        max_steps: $max_steps:expr
1291    ) => {
1292        $crate::servo::Servo::new_output_a(
1293            embassy_rp::pwm::Pwm::new_output_a(
1294                $slice,
1295                $pin,
1296                embassy_rp::pwm::Config::default(),
1297            ),
1298            $min_us,
1299            $max_us,
1300            $max_degrees,
1301        )
1302    };
1303
1304    (@__build_servo
1305        pin: $pin:expr,
1306        slice: $slice:expr,
1307        channel: B,
1308        min_us: $min_us:expr,
1309        max_us: $max_us:expr,
1310        max_degrees: $max_degrees:expr,
1311        max_steps: $max_steps:expr
1312    ) => {
1313        $crate::servo::Servo::new_output_b(
1314            embassy_rp::pwm::Pwm::new_output_b(
1315                $slice,
1316                $pin,
1317                embassy_rp::pwm::Config::default(),
1318            ),
1319            $min_us,
1320            $max_us,
1321            $max_degrees,
1322        )
1323    };
1324
1325    (
1326        $($fields:tt)*
1327    ) => {
1328        $crate::__servo_player_impl! {
1329            @__fill_defaults
1330            vis: pub(self),
1331            name: ServoPlayerGenerated,
1332            pin: _UNSET_,
1333            slice: _UNSET_,
1334            channel: _UNSET_,
1335            min_us: $crate::servo::SERVO_MIN_US_DEFAULT,
1336            max_us: $crate::servo::SERVO_MAX_US_DEFAULT,
1337            max_degrees: $crate::servo::Servo::DEFAULT_MAX_DEGREES,
1338            max_steps: 16,
1339            fields: [ $($fields)* ]
1340        }
1341    };
1342}
1343
1344// Called by macro-generated code in downstream crates; must be public.
1345#[doc(hidden)]
1346pub async fn device_loop<const MAX_STEPS: usize>(
1347    servo_player_static: &'static ServoPlayerStatic<MAX_STEPS>,
1348    mut servo: Servo<'static>,
1349) -> ! {
1350    let mut current_degrees: u16 = 0;
1351    servo.set_degrees(current_degrees);
1352
1353    let mut command = servo_player_static.wait().await;
1354    loop {
1355        match command {
1356            PlayerCommand::Set { degrees } => {
1357                current_degrees = degrees;
1358                servo.set_degrees(current_degrees);
1359                command = servo_player_static.wait().await;
1360            }
1361            PlayerCommand::Hold => {
1362                servo.hold();
1363                command = servo_player_static.wait().await;
1364            }
1365            PlayerCommand::Relax => {
1366                servo.relax();
1367                command = servo_player_static.wait().await;
1368            }
1369            PlayerCommand::Animate { steps, mode } => {
1370                command = run_animation(
1371                    &steps,
1372                    mode,
1373                    &mut servo,
1374                    servo_player_static,
1375                    &mut current_degrees,
1376                )
1377                .await;
1378            }
1379        }
1380    }
1381}
1382
1383async fn run_animation<const MAX_STEPS: usize>(
1384    steps: &[(u16, Duration)],
1385    mode: AtEnd,
1386    servo: &mut Servo<'static>,
1387    servo_player_static: &'static ServoPlayerStatic<MAX_STEPS>,
1388    current_degrees: &mut u16,
1389) -> PlayerCommand<MAX_STEPS> {
1390    loop {
1391        for step in steps {
1392            if *current_degrees != step.0 {
1393                servo.set_degrees(step.0);
1394                *current_degrees = step.0;
1395            }
1396            match select(Timer::after(step.1), servo_player_static.wait()).await {
1397                Either::First(_) => {}
1398                Either::Second(command) => return command,
1399            }
1400        }
1401
1402        // Animation sequence completed - handle end behavior
1403        match mode {
1404            AtEnd::Loop => {
1405                // Continue looping
1406            }
1407            AtEnd::Hold => {
1408                // Hold final position and wait for next command
1409                return servo_player_static.wait().await;
1410            }
1411            AtEnd::Relax => {
1412                // Stop holding position (servo relaxes) and wait for next command
1413                servo.relax();
1414                return servo_player_static.wait().await;
1415            }
1416        }
1417    }
1418}