Skip to main content

device_envoy_esp/
ir.rs

1//! A device abstraction for infrared receivers using the NEC protocol.
2//!
3//! This page provides the primary documentation and examples for receiving NEC infrared input on ESP devices.
4//! It covers raw address/command events, mapped application keys, and Kepler remote keys.
5//! Traits define the shared API; macros generate concrete device types.
6//! Choose [`ir!`](macro@crate::ir) for raw NEC events, [`ir_mapping!`](macro@crate::ir_mapping)
7//! when mapping to your own enum, and [`ir_kepler!`](macro@crate::ir_kepler) for the
8//! SunFounder Kepler remote.
9//!
10//! **After reading the examples below, see also:**
11//!
12//! - **IR: Raw events** — [`ir!`](macro@crate::ir), [`Ir`](trait@crate::ir::Ir), [`IrGenerated`](ir_generated::IrGenerated)
13//! - **IrMapping: Mapped events** — [`ir_mapping!`](macro@crate::ir_mapping), [`IrMapping`](trait@crate::ir::IrMapping), [`IrMappingGenerated`](ir_generated::IrMappingGenerated)
14//! - **IrKepler: Kepler mapped events** — [`ir_kepler!`](macro@crate::ir_kepler), [`IrKepler`](trait@crate::ir::IrKepler), [`IrKeplerGenerated`](ir_generated::IrKeplerGenerated)
15//!
16//! # Example: Read Raw NEC Events
17//!
18//! In this example, the generated `Ir7` type emits raw NEC press events with address and command bytes.
19//!
20//! ```rust,no_run
21//! # #![no_std]
22//! # #![no_main]
23//! use device_envoy_esp::{Result, init_and_start, init_and_start::rmt_mode, ir, ir::{Ir as _, IrEvent}};
24//! # use esp_backtrace as _;
25//! # use log::info;
26//! #
27//! ir! {
28//!     Ir7 { pin: GPIO7 }
29//! }
30//!
31//! # #[esp_rtos::main]
32//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
33//! #     match example(spawner).await {
34//! #         Ok(infallible) => match infallible {},
35//! #         Err(error) => panic!("{error:?}"),
36//! #     }
37//! # }
38//! async fn example(spawner: embassy_executor::Spawner) -> Result<core::convert::Infallible> {
39//!     init_and_start!(p, rmt80: rmt80, mode: rmt_mode::Async);
40//!     esp_println::logger::init_logger(log::LevelFilter::Info);
41//!
42//!     #[cfg(target_arch = "xtensa")]
43//!     let channel_creator = rmt80.channel4;
44//!     #[cfg(not(target_arch = "xtensa"))]
45//!     let channel_creator = rmt80.channel2;
46//!
47//!     let ir7 = Ir7::new(p.GPIO7, channel_creator, spawner)?;
48//!
49//!     loop {
50//!         let IrEvent::Press { addr, cmd } = ir7.wait_for_press().await;
51//!         info!("IR press: addr=0x{:04X}, cmd=0x{:02X}", addr, cmd);
52//!     }
53//! }
54//! ```
55//!
56//! # Example: Map NEC Events to App Keys
57//!
58//! In this example, the generated `IrMapping7` type maps raw NEC address/command pairs into
59//! an application-defined enum.
60//!
61//! ```rust,no_run
62//! # #![no_std]
63//! # #![no_main]
64//! use device_envoy_esp::{Result, init_and_start, init_and_start::rmt_mode, ir::IrMapping as _, ir_mapping};
65//! # use esp_backtrace as _;
66//! #
67//! #[derive(Clone, Copy, Debug, Eq, PartialEq)]
68//! enum RemoteKeys {
69//!     Power,
70//!     Plus,
71//!     Minus,
72//! }
73//!
74//! ir_mapping! {
75//!     IrMapping7 {
76//!         pin: GPIO7,
77//!         button: RemoteKeys,
78//!         capacity: 3,
79//!     }
80//! }
81//!
82//! const REMOTE_KEYS_MAP: [(u16, u8, RemoteKeys); 3] = [
83//!     (0x0000, 0x45, RemoteKeys::Power),
84//!     (0x0000, 0x09, RemoteKeys::Plus),
85//!     (0x0000, 0x15, RemoteKeys::Minus),
86//! ];
87//!
88//! # #[esp_rtos::main]
89//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
90//! #     match example(spawner).await {
91//! #         Ok(infallible) => match infallible {},
92//! #         Err(error) => panic!("{error:?}"),
93//! #     }
94//! # }
95//! async fn example(spawner: embassy_executor::Spawner) -> Result<core::convert::Infallible> {
96//!     init_and_start!(p, rmt80: rmt80, mode: rmt_mode::Async);
97//!
98//!     #[cfg(target_arch = "xtensa")]
99//!     let channel_creator = rmt80.channel4;
100//!     #[cfg(not(target_arch = "xtensa"))]
101//!     let channel_creator = rmt80.channel2;
102//!
103//!     let ir_mapping7 = IrMapping7::new(p.GPIO7, channel_creator, &REMOTE_KEYS_MAP, spawner)?;
104//!
105//!     loop {
106//!         let remote_key = ir_mapping7.wait_for_press().await;
107//!         match remote_key {
108//!             RemoteKeys::Power => {}
109//!             RemoteKeys::Plus => {}
110//!             RemoteKeys::Minus => {}
111//!         }
112//!     }
113//! }
114//! ```
115//!
116//! # Example: Read Kepler Remote Keys
117//!
118//! In this example, the generated `IrKepler7` type returns typed keys from the SunFounder
119//! Kepler remote key mapping.
120//!
121//! ```rust,no_run
122//! # #![no_std]
123//! # #![no_main]
124//! use device_envoy_esp::{Result, init_and_start, init_and_start::rmt_mode, ir::IrKepler as _, ir::KeplerKeys, ir_kepler};
125//! # use esp_backtrace as _;
126//! # use log::info;
127//! #
128//! ir_kepler! {
129//!     IrKepler7 { pin: GPIO7 }
130//! }
131//!
132//! # #[esp_rtos::main]
133//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
134//! #     match example(spawner).await {
135//! #         Ok(infallible) => match infallible {},
136//! #         Err(error) => panic!("{error:?}"),
137//! #     }
138//! # }
139//! async fn example(spawner: embassy_executor::Spawner) -> Result<core::convert::Infallible> {
140//!     init_and_start!(p, rmt80: rmt80, mode: rmt_mode::Async);
141//!     esp_println::logger::init_logger(log::LevelFilter::Info);
142//!
143//!     #[cfg(target_arch = "xtensa")]
144//!     let channel_creator = rmt80.channel4;
145//!     #[cfg(not(target_arch = "xtensa"))]
146//!     let channel_creator = rmt80.channel2;
147//!
148//!     let ir_kepler7 = IrKepler7::new(p.GPIO7, channel_creator, spawner)?;
149//!
150//!     loop {
151//!         let kepler_key = ir_kepler7.wait_for_press().await;
152//!         match kepler_key {
153//!             KeplerKeys::Power => info!("Power"),
154//!             KeplerKeys::PlayPause => info!("PlayPause"),
155//!             _ => info!("Other: {:?}", kepler_key),
156//!         }
157//!     }
158//! }
159//! ```
160#![cfg_attr(not(target_os = "none"), allow(dead_code))]
161
162pub mod ir_generated;
163mod kepler;
164mod mapping;
165
166pub use device_envoy_core::ir::{Ir, IrEvent, IrKepler, IrMapping};
167// Must be `pub` for macro expansion at downstream call sites.
168#[doc(hidden)]
169pub use device_envoy_core::ir::IrStatic as __IrStatic;
170// Must be `pub` for macro expansion at downstream call sites.
171#[doc(hidden)]
172pub use device_envoy_core::ir::kepler::KEPLER_MAPPING as __KEPLER_MAPPING;
173// Must be `pub` for macro expansion at downstream call sites.
174pub use kepler::KeplerKeys;
175pub use mapping::__build_button_map;
176#[doc(hidden)]
177pub use paste;
178
179#[cfg(target_os = "none")]
180use device_envoy_core::ir::decode_nec_frame;
181
182/// Shared IR receive loop used by macro-generated per-instance tasks.
183#[doc(hidden)]
184#[cfg(target_os = "none")]
185pub async fn __ir_receiver_task_loop(
186    mut channel: esp_hal::rmt::Channel<'static, esp_hal::Async, esp_hal::rmt::Rx>,
187    ir_static: &'static __IrStatic,
188) -> ! {
189    let mut pulse_codes = [esp_hal::rmt::PulseCode::default(); 96];
190
191    loop {
192        for pulse_code in &mut pulse_codes {
193            pulse_code.reset();
194        }
195
196        if let Ok(symbol_count) = channel.receive(&mut pulse_codes).await {
197            if let Some((addr, cmd)) = decode_nec_from_pulses(&pulse_codes[..symbol_count]) {
198                ir_static.send(IrEvent::Press { addr, cmd }).await;
199            }
200        }
201    }
202}
203
204#[cfg(target_os = "none")]
205fn decode_nec_from_pulses(pulse_codes: &[esp_hal::rmt::PulseCode]) -> Option<(u16, u8)> {
206    use esp_hal::gpio::Level;
207
208    let mut runs = [(Level::Low, 0u16); 256];
209    let mut run_count = 0usize;
210
211    for pulse_code in pulse_codes {
212        let length1 = pulse_code.length1();
213        if length1 > 0 && run_count < runs.len() {
214            runs[run_count] = (pulse_code.level1(), length1);
215            run_count += 1;
216        }
217
218        let length2 = pulse_code.length2();
219        if length2 == 0 {
220            break;
221        }
222        if run_count < runs.len() {
223            runs[run_count] = (pulse_code.level2(), length2);
224            run_count += 1;
225        }
226    }
227
228    if run_count < 2 {
229        return None;
230    }
231
232    if is_nec_repeat_runs(&runs[..run_count]) {
233        return None;
234    }
235
236    let mut leader_index = None;
237    for run_index in 0..(run_count - 1) {
238        let (level0, duration0) = runs[run_index];
239        let (level1, duration1) = runs[run_index + 1];
240        if level0 == Level::Low
241            && level1 == Level::High
242            && within(duration0, 9000, 2200)
243            && within(duration1, 4500, 1600)
244        {
245            leader_index = Some(run_index + 2);
246            break;
247        }
248    }
249    let mut run_index = leader_index?;
250
251    let mut frame = 0u32;
252    for bit_index in 0..32u32 {
253        if run_index + 1 >= run_count {
254            return None;
255        }
256        let (mark_level, mark_duration) = runs[run_index];
257        let (space_level, space_duration) = runs[run_index + 1];
258        run_index += 2;
259
260        if mark_level != Level::Low || space_level != Level::High {
261            return None;
262        }
263        if !(250..=900).contains(&mark_duration) {
264            return None;
265        }
266
267        let bit_value = if (250..=900).contains(&space_duration) {
268            0u32
269        } else if (1200..=2200).contains(&space_duration) {
270            1u32
271        } else {
272            return None;
273        };
274
275        frame |= bit_value << bit_index;
276    }
277
278    // TODO Handle NEC repeat frames explicitly (leader + 2.25ms + 560us pattern).
279    decode_nec_frame(frame)
280}
281
282#[inline]
283#[cfg(target_os = "none")]
284fn within(value: u16, target: u16, tolerance: u16) -> bool {
285    let min = target.saturating_sub(tolerance);
286    let max = target.saturating_add(tolerance);
287    (min..=max).contains(&value)
288}
289
290#[cfg(target_os = "none")]
291fn is_nec_repeat_runs(runs: &[(esp_hal::gpio::Level, u16)]) -> bool {
292    use esp_hal::gpio::Level;
293
294    if runs.len() < 2 {
295        return false;
296    }
297
298    let (level0, duration0) = runs[0];
299    let (level1, duration1) = runs[1];
300
301    level0 == Level::Low
302        && level1 == Level::High
303        && within(duration0, 9000, 2200)
304        && within(duration1, 2250, 1000)
305}
306
307#[doc(hidden)]
308#[macro_export]
309macro_rules! irs {
310    (
311        $group_name:ident {
312            $first_name:ident : { pin: $first_pin:ident $(,)? }
313            $(, $rest_name:ident : { pin: $rest_pin:ident $(,)? })* $(,)?
314        }
315    ) => {
316        $crate::__irs_impl! {
317            $group_name,
318            [($first_name, $first_pin) $(, ($rest_name, $rest_pin))*]
319        }
320    };
321}
322
323/// Internal implementation helper for [`irs!`].
324#[doc(hidden)]
325#[macro_export]
326macro_rules! __irs_impl {
327    (
328        $group_name:ident,
329        [($name0:ident, $pin0:ident)]
330    ) => {
331        $crate::ir::paste::paste! {
332            static [<$name0:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
333            static [<$name0:upper _IR>]: $name0 = $name0 { ir_static: &[<$name0:upper _IR_STATIC>] };
334
335            #[embassy_executor::task]
336            async fn [<__ $name0:lower _ir_receiver_task>](
337                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
338                ir_static: &'static $crate::ir::__IrStatic,
339            ) -> ! {
340                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
341            }
342
343            pub struct $name0 {
344                ir_static: &'static $crate::ir::__IrStatic,
345            }
346
347            impl $name0 {
348                pub fn new(
349                    pin: $crate::esp_hal::peripherals::$pin0<'static>,
350                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
351                    spawner: embassy_executor::Spawner,
352                ) -> $crate::Result<&'static Self> {
353                    let channel = channel_creator
354                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
355                        .map_err($crate::Error::Rmt)?;
356                    spawner
357                        .spawn([<__ $name0:lower _ir_receiver_task>](channel, &[<$name0:upper _IR_STATIC>]))
358                        .map_err($crate::Error::TaskSpawn)?;
359                    Ok(&[<$name0:upper _IR>])
360                }
361            }
362
363            impl $crate::ir::Ir for $name0 {
364                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
365                    self.ir_static.receive().await
366                }
367            }
368
369            pub struct $group_name;
370            impl $group_name {
371                pub fn new(
372                    pin0: $crate::esp_hal::peripherals::$pin0<'static>,
373                    channel_creator0: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
374                    spawner: embassy_executor::Spawner,
375                ) -> $crate::Result<(&'static $name0,)> {
376                    let name0 = $name0::new(pin0, channel_creator0, spawner)?;
377                    Ok((name0,))
378                }
379            }
380        }
381    };
382    (
383        $group_name:ident,
384        [($name0:ident, $pin0:ident), ($name1:ident, $pin1:ident)]
385    ) => {
386        $crate::ir::paste::paste! {
387            static [<$name0:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
388            static [<$name1:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
389
390            static [<$name0:upper _IR>]: $name0 = $name0 { ir_static: &[<$name0:upper _IR_STATIC>] };
391            static [<$name1:upper _IR>]: $name1 = $name1 { ir_static: &[<$name1:upper _IR_STATIC>] };
392
393            #[embassy_executor::task]
394            async fn [<__ $name0:lower _ir_receiver_task>](
395                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
396                ir_static: &'static $crate::ir::__IrStatic,
397            ) -> ! {
398                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
399            }
400
401            #[embassy_executor::task]
402            async fn [<__ $name1:lower _ir_receiver_task>](
403                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
404                ir_static: &'static $crate::ir::__IrStatic,
405            ) -> ! {
406                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
407            }
408
409            pub struct $name0 {
410                ir_static: &'static $crate::ir::__IrStatic,
411            }
412
413            pub struct $name1 {
414                ir_static: &'static $crate::ir::__IrStatic,
415            }
416
417            impl $name0 {
418                pub fn new(
419                    pin: $crate::esp_hal::peripherals::$pin0<'static>,
420                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
421                    spawner: embassy_executor::Spawner,
422                ) -> $crate::Result<&'static Self> {
423                    let channel = channel_creator
424                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
425                        .map_err($crate::Error::Rmt)?;
426                    spawner
427                        .spawn([<__ $name0:lower _ir_receiver_task>](channel, &[<$name0:upper _IR_STATIC>]))
428                        .map_err($crate::Error::TaskSpawn)?;
429                    Ok(&[<$name0:upper _IR>])
430                }
431            }
432
433            impl $name1 {
434                pub fn new(
435                    pin: $crate::esp_hal::peripherals::$pin1<'static>,
436                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
437                    spawner: embassy_executor::Spawner,
438                ) -> $crate::Result<&'static Self> {
439                    let channel = channel_creator
440                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
441                        .map_err($crate::Error::Rmt)?;
442                    spawner
443                        .spawn([<__ $name1:lower _ir_receiver_task>](channel, &[<$name1:upper _IR_STATIC>]))
444                        .map_err($crate::Error::TaskSpawn)?;
445                    Ok(&[<$name1:upper _IR>])
446                }
447            }
448
449            impl $crate::ir::Ir for $name0 {
450                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
451                    self.ir_static.receive().await
452                }
453            }
454
455            impl $crate::ir::Ir for $name1 {
456                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
457                    self.ir_static.receive().await
458                }
459            }
460
461            pub struct $group_name;
462            impl $group_name {
463                pub fn new(
464                    pin0: $crate::esp_hal::peripherals::$pin0<'static>,
465                    channel_creator0: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
466                    pin1: $crate::esp_hal::peripherals::$pin1<'static>,
467                    channel_creator1: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
468                    spawner: embassy_executor::Spawner,
469                ) -> $crate::Result<(&'static $name0, &'static $name1)> {
470                    let name0 = $name0::new(pin0, channel_creator0, spawner)?;
471                    let name1 = $name1::new(pin1, channel_creator1, spawner)?;
472                    Ok((name0, name1))
473                }
474            }
475        }
476    };
477    (
478        $group_name:ident,
479        [($name0:ident, $pin0:ident), ($name1:ident, $pin1:ident), ($name2:ident, $pin2:ident)]
480    ) => {
481        $crate::ir::paste::paste! {
482            static [<$name0:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
483            static [<$name1:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
484            static [<$name2:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
485
486            static [<$name0:upper _IR>]: $name0 = $name0 { ir_static: &[<$name0:upper _IR_STATIC>] };
487            static [<$name1:upper _IR>]: $name1 = $name1 { ir_static: &[<$name1:upper _IR_STATIC>] };
488            static [<$name2:upper _IR>]: $name2 = $name2 { ir_static: &[<$name2:upper _IR_STATIC>] };
489
490            #[embassy_executor::task]
491            async fn [<__ $name0:lower _ir_receiver_task>](
492                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
493                ir_static: &'static $crate::ir::__IrStatic,
494            ) -> ! {
495                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
496            }
497
498            #[embassy_executor::task]
499            async fn [<__ $name1:lower _ir_receiver_task>](
500                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
501                ir_static: &'static $crate::ir::__IrStatic,
502            ) -> ! {
503                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
504            }
505
506            #[embassy_executor::task]
507            async fn [<__ $name2:lower _ir_receiver_task>](
508                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
509                ir_static: &'static $crate::ir::__IrStatic,
510            ) -> ! {
511                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
512            }
513
514            pub struct $name0 {
515                ir_static: &'static $crate::ir::__IrStatic,
516            }
517
518            pub struct $name1 {
519                ir_static: &'static $crate::ir::__IrStatic,
520            }
521
522            pub struct $name2 {
523                ir_static: &'static $crate::ir::__IrStatic,
524            }
525
526            impl $name0 {
527                pub fn new(
528                    pin: $crate::esp_hal::peripherals::$pin0<'static>,
529                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
530                    spawner: embassy_executor::Spawner,
531                ) -> $crate::Result<&'static Self> {
532                    let channel = channel_creator
533                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
534                        .map_err($crate::Error::Rmt)?;
535                    spawner
536                        .spawn([<__ $name0:lower _ir_receiver_task>](channel, &[<$name0:upper _IR_STATIC>]))
537                        .map_err($crate::Error::TaskSpawn)?;
538                    Ok(&[<$name0:upper _IR>])
539                }
540            }
541
542            impl $name1 {
543                pub fn new(
544                    pin: $crate::esp_hal::peripherals::$pin1<'static>,
545                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
546                    spawner: embassy_executor::Spawner,
547                ) -> $crate::Result<&'static Self> {
548                    let channel = channel_creator
549                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
550                        .map_err($crate::Error::Rmt)?;
551                    spawner
552                        .spawn([<__ $name1:lower _ir_receiver_task>](channel, &[<$name1:upper _IR_STATIC>]))
553                        .map_err($crate::Error::TaskSpawn)?;
554                    Ok(&[<$name1:upper _IR>])
555                }
556            }
557
558            impl $name2 {
559                pub fn new(
560                    pin: $crate::esp_hal::peripherals::$pin2<'static>,
561                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
562                    spawner: embassy_executor::Spawner,
563                ) -> $crate::Result<&'static Self> {
564                    let channel = channel_creator
565                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
566                        .map_err($crate::Error::Rmt)?;
567                    spawner
568                        .spawn([<__ $name2:lower _ir_receiver_task>](channel, &[<$name2:upper _IR_STATIC>]))
569                        .map_err($crate::Error::TaskSpawn)?;
570                    Ok(&[<$name2:upper _IR>])
571                }
572            }
573
574            impl $crate::ir::Ir for $name0 {
575                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
576                    self.ir_static.receive().await
577                }
578            }
579
580            impl $crate::ir::Ir for $name1 {
581                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
582                    self.ir_static.receive().await
583                }
584            }
585
586            impl $crate::ir::Ir for $name2 {
587                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
588                    self.ir_static.receive().await
589                }
590            }
591
592            pub struct $group_name;
593            impl $group_name {
594                pub fn new(
595                    pin0: $crate::esp_hal::peripherals::$pin0<'static>,
596                    channel_creator0: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
597                    pin1: $crate::esp_hal::peripherals::$pin1<'static>,
598                    channel_creator1: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
599                    pin2: $crate::esp_hal::peripherals::$pin2<'static>,
600                    channel_creator2: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
601                    spawner: embassy_executor::Spawner,
602                ) -> $crate::Result<(&'static $name0, &'static $name1, &'static $name2)> {
603                    let name0 = $name0::new(pin0, channel_creator0, spawner)?;
604                    let name1 = $name1::new(pin1, channel_creator1, spawner)?;
605                    let name2 = $name2::new(pin2, channel_creator2, spawner)?;
606                    Ok((name0, name1, name2))
607                }
608            }
609        }
610    };
611    (
612        $group_name:ident,
613        [($name0:ident, $pin0:ident), ($name1:ident, $pin1:ident), ($name2:ident, $pin2:ident), ($name3:ident, $pin3:ident)]
614    ) => {
615        $crate::ir::paste::paste! {
616            static [<$name0:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
617            static [<$name1:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
618            static [<$name2:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
619            static [<$name3:upper _IR_STATIC>]: $crate::ir::__IrStatic = $crate::ir::__IrStatic::new();
620
621            static [<$name0:upper _IR>]: $name0 = $name0 { ir_static: &[<$name0:upper _IR_STATIC>] };
622            static [<$name1:upper _IR>]: $name1 = $name1 { ir_static: &[<$name1:upper _IR_STATIC>] };
623            static [<$name2:upper _IR>]: $name2 = $name2 { ir_static: &[<$name2:upper _IR_STATIC>] };
624            static [<$name3:upper _IR>]: $name3 = $name3 { ir_static: &[<$name3:upper _IR_STATIC>] };
625
626            #[embassy_executor::task]
627            async fn [<__ $name0:lower _ir_receiver_task>](
628                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
629                ir_static: &'static $crate::ir::__IrStatic,
630            ) -> ! {
631                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
632            }
633
634            #[embassy_executor::task]
635            async fn [<__ $name1:lower _ir_receiver_task>](
636                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
637                ir_static: &'static $crate::ir::__IrStatic,
638            ) -> ! {
639                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
640            }
641
642            #[embassy_executor::task]
643            async fn [<__ $name2:lower _ir_receiver_task>](
644                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
645                ir_static: &'static $crate::ir::__IrStatic,
646            ) -> ! {
647                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
648            }
649
650            #[embassy_executor::task]
651            async fn [<__ $name3:lower _ir_receiver_task>](
652                channel: $crate::esp_hal::rmt::Channel<'static, $crate::esp_hal::Async, $crate::esp_hal::rmt::Rx>,
653                ir_static: &'static $crate::ir::__IrStatic,
654            ) -> ! {
655                $crate::ir::__ir_receiver_task_loop(channel, ir_static).await
656            }
657
658            pub struct $name0 {
659                ir_static: &'static $crate::ir::__IrStatic,
660            }
661
662            pub struct $name1 {
663                ir_static: &'static $crate::ir::__IrStatic,
664            }
665
666            pub struct $name2 {
667                ir_static: &'static $crate::ir::__IrStatic,
668            }
669
670            pub struct $name3 {
671                ir_static: &'static $crate::ir::__IrStatic,
672            }
673
674            impl $name0 {
675                pub fn new(
676                    pin: $crate::esp_hal::peripherals::$pin0<'static>,
677                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
678                    spawner: embassy_executor::Spawner,
679                ) -> $crate::Result<&'static Self> {
680                    let channel = channel_creator
681                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
682                        .map_err($crate::Error::Rmt)?;
683                    spawner
684                        .spawn([<__ $name0:lower _ir_receiver_task>](channel, &[<$name0:upper _IR_STATIC>]))
685                        .map_err($crate::Error::TaskSpawn)?;
686                    Ok(&[<$name0:upper _IR>])
687                }
688            }
689
690            impl $name1 {
691                pub fn new(
692                    pin: $crate::esp_hal::peripherals::$pin1<'static>,
693                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
694                    spawner: embassy_executor::Spawner,
695                ) -> $crate::Result<&'static Self> {
696                    let channel = channel_creator
697                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
698                        .map_err($crate::Error::Rmt)?;
699                    spawner
700                        .spawn([<__ $name1:lower _ir_receiver_task>](channel, &[<$name1:upper _IR_STATIC>]))
701                        .map_err($crate::Error::TaskSpawn)?;
702                    Ok(&[<$name1:upper _IR>])
703                }
704            }
705
706            impl $name2 {
707                pub fn new(
708                    pin: $crate::esp_hal::peripherals::$pin2<'static>,
709                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
710                    spawner: embassy_executor::Spawner,
711                ) -> $crate::Result<&'static Self> {
712                    let channel = channel_creator
713                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
714                        .map_err($crate::Error::Rmt)?;
715                    spawner
716                        .spawn([<__ $name2:lower _ir_receiver_task>](channel, &[<$name2:upper _IR_STATIC>]))
717                        .map_err($crate::Error::TaskSpawn)?;
718                    Ok(&[<$name2:upper _IR>])
719                }
720            }
721
722            impl $name3 {
723                pub fn new(
724                    pin: $crate::esp_hal::peripherals::$pin3<'static>,
725                    channel_creator: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
726                    spawner: embassy_executor::Spawner,
727                ) -> $crate::Result<&'static Self> {
728                    let channel = channel_creator
729                        .configure_rx(pin, $crate::init_and_start::rmt::nec_rx_config())
730                        .map_err($crate::Error::Rmt)?;
731                    spawner
732                        .spawn([<__ $name3:lower _ir_receiver_task>](channel, &[<$name3:upper _IR_STATIC>]))
733                        .map_err($crate::Error::TaskSpawn)?;
734                    Ok(&[<$name3:upper _IR>])
735                }
736            }
737
738            impl $crate::ir::Ir for $name0 {
739                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
740                    self.ir_static.receive().await
741                }
742            }
743
744            impl $crate::ir::Ir for $name1 {
745                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
746                    self.ir_static.receive().await
747                }
748            }
749
750            impl $crate::ir::Ir for $name2 {
751                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
752                    self.ir_static.receive().await
753                }
754            }
755
756            impl $crate::ir::Ir for $name3 {
757                async fn wait_for_press(&self) -> $crate::ir::IrEvent {
758                    self.ir_static.receive().await
759                }
760            }
761
762            pub struct $group_name;
763            impl $group_name {
764                pub fn new(
765                    pin0: $crate::esp_hal::peripherals::$pin0<'static>,
766                    channel_creator0: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
767                    pin1: $crate::esp_hal::peripherals::$pin1<'static>,
768                    channel_creator1: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
769                    pin2: $crate::esp_hal::peripherals::$pin2<'static>,
770                    channel_creator2: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
771                    pin3: $crate::esp_hal::peripherals::$pin3<'static>,
772                    channel_creator3: impl $crate::esp_hal::rmt::RxChannelCreator<'static, $crate::esp_hal::Async>,
773                    spawner: embassy_executor::Spawner,
774                ) -> $crate::Result<(&'static $name0, &'static $name1, &'static $name2, &'static $name3)> {
775                    let name0 = $name0::new(pin0, channel_creator0, spawner)?;
776                    let name1 = $name1::new(pin1, channel_creator1, spawner)?;
777                    let name2 = $name2::new(pin2, channel_creator2, spawner)?;
778                    let name3 = $name3::new(pin3, channel_creator3, spawner)?;
779                    Ok((name0, name1, name2, name3))
780                }
781            }
782        }
783    };
784    (
785        $group_name:ident,
786        [($name0:ident, $pin0:ident), ($name1:ident, $pin1:ident), ($name2:ident, $pin2:ident), ($name3:ident, $pin3:ident), ($($tail:tt)+)]
787    ) => {
788        compile_error!("irs! currently supports up to 4 receivers in one group.");
789    };
790}
791
792#[doc(hidden)]
793#[macro_export]
794macro_rules! ir {
795    (
796        $(#[$attrs:meta])*
797        $vis:vis $name:ident : { $($deprecated_fields:tt)* }
798    ) => {
799        compile_error!(
800            "ir! no longer supports `Name: { ... }`. Use `Name { ... }` instead."
801        );
802    };
803    (
804        $(#[$attrs:meta])*
805        $vis:vis $name:ident { pin: $pin:ident $(,)? }
806    ) => {
807        $crate::ir::paste::paste! {
808            $crate::irs! {
809                [<__ $name:camel Group>] {
810                    [<__ $name:camel Ir>]: { pin: $pin }
811                }
812            }
813
814            $(#[$attrs])*
815            $vis type $name = [<__ $name:camel Ir>];
816        }
817    };
818}
819
820/// Macro to generate a Kepler IR struct type (includes syntax details).
821///
822/// **See the [ir module documentation](mod@crate::ir) for usage examples.**
823///
824/// **Syntax:**
825///
826/// ```text
827/// ir_kepler! {
828///     [attrs...]
829///     [vis?] <Name> {
830///         pin: <pin_ident>,
831///     }
832/// }
833/// ```
834///
835/// **Required fields:**
836///
837/// - `pin` — GPIO input pin connected to the IR receiver
838///
839/// # Related Macros
840///
841/// - [`ir_keplers!`](crate::ir_keplers) — Build multiple Kepler IR receivers
842/// - [`ir!`](crate::ir!) — Generate a raw IR receiver type
843#[allow(unused_imports)]
844#[doc(inline)]
845pub use crate::ir_kepler;
846/// Macro to generate multiple Kepler IR struct types (includes syntax details).
847///
848/// **See the [ir module documentation](mod@crate::ir) for usage examples.**
849///
850/// **Syntax:**
851///
852/// ```text
853/// ir_keplers! {
854///     <GroupName> {
855///         <Name0>: { pin: <pin0_ident> },
856///         <Name1>: { pin: <pin1_ident> }, // optional
857///         <Name2>: { pin: <pin2_ident> }, // optional
858///         <Name3>: { pin: <pin3_ident> }, // optional
859///     }
860/// }
861/// ```
862///
863/// **Required fields:**
864///
865/// - `pin` — One pin entry per generated Kepler receiver
866///
867/// Supports one to four generated receivers per invocation.
868///
869/// # Related Macros
870///
871/// - [`ir_kepler!`](crate::ir_kepler) — Generate a single Kepler IR receiver type
872/// - [`irs!`](crate::irs) — Generate raw IR receivers
873#[allow(unused_imports)]
874#[doc(inline)]
875pub use crate::ir_keplers;
876/// Macro to generate an IR mapping struct type (includes syntax details).
877///
878/// **See the [ir module documentation](mod@crate::ir) for usage examples.**
879///
880/// **Syntax:**
881///
882/// ```text
883/// ir_mapping! {
884///     [attrs...]
885///     [vis?] <Name> {
886///         pin: <pin_ident>,
887///         button: <button_type>,
888///         capacity: <usize_expr>,
889///     }
890/// }
891/// ```
892///
893/// **Required fields:**
894///
895/// - `pin` — GPIO input pin connected to the IR receiver
896/// - `button` — Output button/key type for mapping
897/// - `capacity` — Maximum mapping entries (`heapless::LinearMap` capacity)
898///
899/// # Related Macros
900///
901/// - [`ir_mappings!`](crate::ir_mappings) — Generate multiple mapping receivers
902/// - [`ir!`](crate::ir!) — Generate a raw IR receiver type
903#[allow(unused_imports)]
904#[doc(inline)]
905pub use crate::ir_mapping;
906/// Macro to generate multiple IR mapping struct types (includes syntax details).
907///
908/// **See the [ir module documentation](mod@crate::ir) for usage examples.**
909///
910/// **Syntax:**
911///
912/// ```text
913/// ir_mappings! {
914///     button: <button_type>,
915///     capacity: <usize_expr>,
916///     <GroupName> {
917///         <Name0>: { pin: <pin0_ident> },
918///         <Name1>: { pin: <pin1_ident> }, // optional
919///         <Name2>: { pin: <pin2_ident> }, // optional
920///         <Name3>: { pin: <pin3_ident> }, // optional
921///     }
922/// }
923/// ```
924///
925/// **Required fields:**
926///
927/// - `button` — Output button/key type for all generated mappings
928/// - `capacity` — Maximum mapping entries (`heapless::LinearMap` capacity)
929/// - `pin` — One pin entry per generated mapping receiver
930///
931/// Supports one to four generated mapping receivers per invocation.
932///
933/// # Related Macros
934///
935/// - [`ir_mapping!`](crate::ir_mapping) — Generate a single IR mapping receiver type
936/// - [`irs!`](crate::irs) — Generate raw IR receivers
937#[allow(unused_imports)]
938#[doc(inline)]
939pub use crate::ir_mappings;
940/// Macro to generate an IR receiver struct type (includes syntax details).
941///
942/// **See the [ir module documentation](mod@crate::ir) for usage examples.**
943///
944/// **Syntax:**
945///
946/// ```text
947/// ir! {
948///     [attrs...]
949///     [vis?] <Name> {
950///         pin: <pin_ident>,
951///     }
952/// }
953/// ```
954///
955/// **Required fields:**
956///
957/// - `pin` — GPIO input pin connected to the IR receiver
958///
959/// # Related Macros
960///
961/// - [`irs!`](crate::irs) — Generate multiple IR receivers
962/// - [`ir_mapping!`](crate::ir_mapping) — Generate a mapped-button IR receiver type
963#[allow(unused_imports)]
964#[doc(inline)]
965pub use ir;
966/// Macro to generate multiple IR receiver struct types (includes syntax details).
967///
968/// **See the [ir module documentation](mod@crate::ir) for usage examples.**
969///
970/// **Syntax:**
971///
972/// ```text
973/// irs! {
974///     <GroupName> {
975///         <Name0>: { pin: <pin0_ident> },
976///         <Name1>: { pin: <pin1_ident> }, // optional
977///         <Name2>: { pin: <pin2_ident> }, // optional
978///         <Name3>: { pin: <pin3_ident> }, // optional
979///     }
980/// }
981/// ```
982///
983/// **Required fields:**
984///
985/// - `pin` — One pin entry per generated receiver
986///
987/// Supports one to four generated receivers per invocation.
988///
989/// # Related Macros
990///
991/// - [`ir!`](crate::ir!) — Generate a single IR receiver type
992/// - [`ir_mappings!`](crate::ir_mappings) — Generate mapped-button receivers
993#[allow(unused_imports)]
994#[doc(inline)]
995pub use irs;