device_envoy/ir/mapping.rs
1//! A device abstraction for mapping IR remote buttons to application-specific actions.
2//!
3//! See [`IrMapping`] for usage examples.
4
5use embassy_executor::Spawner;
6use embassy_rp::Peri;
7use embassy_rp::gpio::Pin;
8use embassy_rp::pio::PioPin;
9use heapless::LinearMap;
10
11use crate::Result;
12use crate::ir::{Ir, IrEvent, IrPioPeripheral, IrStatic};
13
14/// Static channel for IR mapping events.
15///
16/// See [`IrMapping`] for usage examples.
17pub struct IrMappingStatic(IrStatic);
18
19impl IrMappingStatic {
20 /// Create static mapping resources.
21 #[must_use]
22 pub(crate) const fn new() -> Self {
23 Self(Ir::new_static())
24 }
25
26 /// Get a reference to the inner channel resources.
27 #[must_use]
28 pub(crate) const fn inner(&self) -> &IrStatic {
29 &self.0
30 }
31}
32
33/// A generic device abstraction that maps IR remote button presses to user-defined button types.
34///
35/// # Examples
36/// ```rust,no_run
37/// # #![no_std]
38/// # #![no_main]
39/// use device_envoy::ir::{IrMapping, IrMappingStatic};
40/// # #[panic_handler]
41/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
42/// #[derive(Debug, Clone, Copy)]
43/// enum RemoteButton { Power, Play, Stop }
44/// async fn example(
45/// p: embassy_rp::Peripherals,
46/// spawner: embassy_executor::Spawner,
47/// ) -> device_envoy::Result<()> {
48/// let button_map = [
49/// (0x0000, 0x45, RemoteButton::Power),
50/// (0x0000, 0x0C, RemoteButton::Play),
51/// (0x0000, 0x08, RemoteButton::Stop),
52/// ];
53///
54/// static IR_MAPPING_STATIC: IrMappingStatic = IrMapping::<RemoteButton, 3>::new_static();
55/// let ir_mapping: IrMapping<RemoteButton, 3> = IrMapping::new(&IR_MAPPING_STATIC, p.PIN_15, p.PIO0, &button_map, spawner)?;
56///
57/// loop {
58/// let button = ir_mapping.wait_for_press().await;
59/// // Use button...
60/// }
61/// }
62/// ```
63pub struct IrMapping<'a, B, const N: usize> {
64 ir: Ir<'a>,
65 button_map: LinearMap<(u16, u8), B, N>,
66}
67
68impl<'a, B, const N: usize> IrMapping<'a, B, N>
69where
70 B: Copy,
71{
72 /// Create static channel resources for IR mapping events.
73 ///
74 /// See [`IrMapping`] for usage examples.
75 #[must_use]
76 pub const fn new_static() -> IrMappingStatic {
77 IrMappingStatic::new()
78 }
79
80 /// Create a new IR remote button mapper.
81 ///
82 /// # Parameters
83 /// - `ir_mapping_static`: Static reference to the channel resources
84 /// - `pin`: GPIO pin connected to the IR receiver
85 /// - `pio`: PIO peripheral to use (PIO0, PIO1, or PIO2)
86 /// - `button_map`: Array mapping (address, command) pairs to button types
87 /// - `spawner`: Embassy spawner for background task
88 ///
89 /// See [`IrMapping`] for usage examples.
90 ///
91 /// # Errors
92 /// Returns an error if the background task cannot be spawned.
93 pub fn new<P, PIO>(
94 ir_mapping_static: &'static IrMappingStatic,
95 pin: Peri<'static, P>,
96 pio: Peri<'static, PIO>,
97 button_map: &[(u16, u8, B)],
98 spawner: Spawner,
99 ) -> Result<Self>
100 where
101 P: Pin + PioPin,
102 PIO: IrPioPeripheral,
103 {
104 let ir = Ir::new(ir_mapping_static.inner(), pin, pio, spawner)?;
105
106 // Convert the flat array to a LinearMap
107 let mut map = LinearMap::new();
108 for &(addr, cmd, button) in button_map {
109 map.insert((addr, cmd), button).ok();
110 }
111
112 Ok(Self {
113 ir,
114 button_map: map,
115 })
116 }
117
118 /// Wait for the next recognized button press.
119 ///
120 /// Ignores button presses that are not in the button map.
121 ///
122 /// See [`IrMapping`] for usage examples.
123 pub async fn wait_for_press(&self) -> B {
124 loop {
125 let IrEvent::Press { addr, cmd } = self.ir.wait_for_press().await;
126 #[cfg(feature = "defmt")]
127 defmt::info!("IR received - addr=0x{:04X} cmd=0x{:02X}", addr, cmd);
128 if let Some(&button) = self.button_map.get(&(addr, cmd)) {
129 return button;
130 }
131 #[cfg(feature = "defmt")]
132 defmt::info!(" (unrecognized - ignoring)");
133 }
134 }
135}