Skip to main content

device_envoy_core/
ir.rs

1//! A device abstraction for infrared receivers using the NEC protocol.
2//!
3//! This module provides platform-independent types and helpers for NEC IR decoding.
4//! See the platform-specific crate (for example `device_envoy_rp::ir` or
5//! `device_envoy_esp::ir`) for the primary documentation and examples.
6
7pub mod kepler;
8pub mod mapping;
9
10pub use kepler::{IrKepler, IrKeplerStatic, KEPLER_MAPPING, KeplerKeys};
11pub use mapping::{IrMapping, IrMappingStatic};
12
13use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
14use embassy_sync::channel::Channel as EmbassyChannel;
15
16/// Events received from the infrared receiver.
17///
18/// See the platform-specific crate for usage examples.
19#[derive(Copy, Clone, Debug, PartialEq)]
20pub enum IrEvent {
21    /// Button press with 16-bit address and 8-bit command.
22    /// Supports both standard NEC (8-bit address) and extended NEC (16-bit address).
23    Press {
24        /// 16-bit device address (or 8-bit address in low byte for standard NEC).
25        addr: u16,
26        /// 8-bit command code.
27        cmd: u8,
28    },
29}
30
31/// Platform-agnostic infrared receiver device contract.
32///
33/// Platform crates implement this for their concrete `Ir` types so shared logic can wait for
34/// NEC button press events without depending on platform-specific modules.
35///
36/// This trait is the core `wait_for_press` surface used by platform IR receiver wrappers.
37///
38/// # Example
39///
40/// ```rust,no_run
41/// use device_envoy_core::ir::{Ir, IrEvent};
42///
43/// async fn handle_ir_presses(ir: &impl Ir) -> ! {
44///     loop {
45///         let ir_event = ir.wait_for_press().await;
46///         match ir_event {
47///             IrEvent::Press { addr, cmd } => {
48///                 // Handle decoded NEC press event.
49///                 let _ = (addr, cmd);
50///             }
51///         }
52///     }
53/// }
54///
55/// # struct DemoIr;
56/// # impl Ir for DemoIr {
57/// #     async fn wait_for_press(&self) -> IrEvent {
58/// #         IrEvent::Press { addr: 0, cmd: 0 }
59/// #     }
60/// # }
61/// # fn main() {
62/// #     let ir = DemoIr;
63/// #     let _future = handle_ir_presses(&ir);
64/// # }
65/// ```
66#[allow(async_fn_in_trait)]
67pub trait Ir {
68    /// Wait for the next IR event.
69    ///
70    /// See the [Ir trait documentation](Self) for usage examples.
71    async fn wait_for_press(&self) -> IrEvent;
72}
73
74/// Static resources for the [`Ir`] device abstraction.
75///
76/// See the platform-specific crate for usage examples.
77// Public for cross-crate platform plumbing; hidden from end-user docs.
78#[doc(hidden)]
79pub struct IrStatic(EmbassyChannel<CriticalSectionRawMutex, IrEvent, 8>);
80
81impl IrStatic {
82    /// Creates static resources for the infrared receiver device.
83    #[must_use]
84    pub const fn new() -> Self {
85        Self(EmbassyChannel::new())
86    }
87
88    /// Send an IR event to waiting receivers.
89    ///
90    /// Called by the platform's hardware receiver task.
91    pub async fn send(&self, event: IrEvent) {
92        self.0.send(event).await;
93    }
94
95    /// Wait for the next IR event.
96    pub async fn receive(&self) -> IrEvent {
97        self.0.receive().await
98    }
99}
100
101/// Decode and validate a 32-bit NEC frame.
102///
103/// NEC protocol structure (32 bits, LSB first):
104/// - Byte 0: Address (8 bits)
105/// - Byte 1: Address inverse (~Address)
106/// - Byte 2: Command (8 bits)
107/// - Byte 3: Command inverse (~Command)
108///
109/// Extended NEC uses 16-bit address (bytes 0-1) without inversion check.
110///
111/// Returns `Some((address, command))` if valid, `None` if checksum fails.
112// Public for cross-crate platform plumbing; hidden from end-user docs.
113#[doc(hidden)]
114pub fn decode_nec_frame(frame: u32) -> Option<(u16, u8)> {
115    let byte0 = (frame & 0xFF) as u8;
116    let byte1 = ((frame >> 8) & 0xFF) as u8;
117    let byte2 = ((frame >> 16) & 0xFF) as u8;
118    let byte3 = ((frame >> 24) & 0xFF) as u8;
119
120    // Validate command bytes (required in both standard and extended NEC)
121    if (byte2 ^ byte3) != 0xFF {
122        return None;
123    }
124
125    // Standard NEC: 8-bit address with inverse validation
126    if (byte0 ^ byte1) == 0xFF {
127        return Some((u16::from(byte0), byte2));
128    }
129
130    // Extended NEC: 16-bit address (no inversion check on address)
131    let addr16 = ((u16::from(byte1)) << 8) | u16::from(byte0);
132    Some((addr16, byte2))
133}