Skip to main content

device_envoy/
rfid.rs

1//! A device abstraction for RFID readers using the MFRC522 chip.
2//!
3//! See [`Rfid`] for the primary example; helper functions link back here.
4
5use defmt::info;
6use embassy_executor::Spawner;
7use embassy_rp::Peri;
8use embassy_rp::dma::Channel;
9use embassy_rp::gpio::{Level, Output, Pin};
10use embassy_rp::peripherals::SPI0;
11use embassy_rp::spi::{ClkPin, Config as SpiConfig, MisoPin, MosiPin, Phase, Polarity, Spi};
12use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
13use embassy_sync::channel::Channel as EmbassyChannel;
14use embassy_time::{Instant, Timer};
15use embedded_hal_bus::spi::{ExclusiveDevice, NoDelay};
16use esp_hal_mfrc522::MFRC522;
17use esp_hal_mfrc522::consts::UidSize;
18use esp_hal_mfrc522::drivers::SpiDriver;
19
20use crate::{Error, Result};
21
22/// Events received from the RFID reader.
23#[derive(Debug, Clone, Copy)]
24pub enum RfidEvent {
25    /// A card was detected with the given unique identifier.
26    CardDetected {
27        /// The 10-byte UID of the detected card.
28        uid: [u8; 10],
29    },
30}
31
32/// Static type for RFID reader events
33pub type Mfrc522Device = MFRC522<
34    SpiDriver<
35        ExclusiveDevice<Spi<'static, SPI0, embassy_rp::spi::Async>, Output<'static>, NoDelay>,
36    >,
37>;
38
39/// Static type for the `Rfid` device abstraction.
40pub type RfidStatic = EmbassyChannel<CriticalSectionRawMutex, RfidEvent, 4>;
41
42/// A device abstraction for an RFID reader using the MFRC522 chip.
43///
44/// ```rust,no_run
45/// # #![no_std]
46/// # use panic_probe as _;
47/// # fn main() {}
48/// use device_envoy::rfid::{Rfid, RfidEvent, RfidStatic};
49///
50/// async fn example(
51///     p: embassy_rp::Peripherals,
52///     spawner: embassy_executor::Spawner,
53/// ) -> device_envoy::Result<()> {
54///     static RFID_STATIC: RfidStatic = Rfid::new_static();
55///     let rfid = Rfid::new(
56///         &RFID_STATIC,
57///         p.SPI0,
58///         p.PIN_2,
59///         p.PIN_3,
60///         p.PIN_0,
61///         p.DMA_CH0,
62///         p.DMA_CH1,
63///         p.PIN_1,
64///         p.PIN_4,
65///         spawner,
66///     )
67///     .await?;
68///
69///     loop {
70///         let RfidEvent::CardDetected { uid } = rfid.wait_for_tap().await;
71///         defmt::info!("RFID uid: {:?}", uid);
72///     }
73/// }
74/// ```
75pub struct Rfid<'a> {
76    rfid_static: &'a RfidStatic,
77}
78
79impl Rfid<'_> {
80    /// Create static channel resources for the RFID reader
81    #[must_use]
82    pub const fn new_static() -> RfidStatic {
83        EmbassyChannel::new()
84    }
85
86    /// Create a new RFID reader device abstraction
87    ///
88    /// Note: Currently hardcoded to SPI0. All peripherals must have 'static lifetime.
89    pub async fn new<Sck, Mosi, Miso, Dma0, Dma1, Cs, Rst>(
90        rfid_static: &'static RfidStatic,
91        spi: Peri<'static, SPI0>,
92        sck: Peri<'static, Sck>,
93        mosi: Peri<'static, Mosi>,
94        miso: Peri<'static, Miso>,
95        dma_ch0: Peri<'static, Dma0>,
96        dma_ch1: Peri<'static, Dma1>,
97        cs: Peri<'static, Cs>,
98        rst: Peri<'static, Rst>,
99        spawner: Spawner,
100    ) -> Result<Self>
101    where
102        Sck: Pin + ClkPin<SPI0>,
103        Mosi: Pin + MosiPin<SPI0>,
104        Miso: Pin + MisoPin<SPI0>,
105        Dma0: Channel,
106        Dma1: Channel,
107        Cs: Pin,
108        Rst: Pin,
109    {
110        // Initialize the hardware
111        let mfrc522 =
112            init_mfrc522_hardware(spi, sck, mosi, miso, dma_ch0, dma_ch1, cs, rst).await?;
113
114        // Spawn the polling task
115        let token = rfid_polling_task(mfrc522, rfid_static);
116        spawner.spawn(token).map_err(Error::TaskSpawn)?;
117
118        Ok(Self { rfid_static })
119    }
120
121    /// Wait for the next RFID event (card detection)
122    pub async fn wait_for_tap(&self) -> RfidEvent {
123        self.rfid_static.receive().await
124    }
125}
126
127/// Convert UID bytes to a fixed-size array, padding with zeros if needed
128fn uid_to_fixed_array(uid_bytes: &[u8]) -> [u8; 10] {
129    let mut uid_key = [0u8; 10];
130    #[expect(clippy::indexing_slicing, reason = "Length checked")]
131    for (i, &byte) in uid_bytes.iter().enumerate() {
132        if i < 10 {
133            uid_key[i] = byte;
134        }
135    }
136    uid_key
137}
138
139/// Embassy task that continuously polls for RFID cards
140#[embassy_executor::task]
141async fn rfid_polling_task(mut mfrc522: Mfrc522Device, rfid_static: &'static RfidStatic) -> ! {
142    info!("RFID polling task started");
143
144    loop {
145        // Try to detect a card
146        let Ok(()) = mfrc522.picc_is_new_card_present().await else {
147            Timer::after_millis(500).await;
148            continue;
149        };
150
151        info!("Card detected!");
152
153        // Try to read UID
154        let Ok(uid) = mfrc522.get_card(UidSize::Four).await else {
155            info!("UID read error");
156            Timer::after_millis(500).await;
157            continue;
158        };
159
160        info!("UID read successfully ({} bytes)", uid.uid_bytes.len());
161
162        // Convert to fixed-size array
163        let uid_key = uid_to_fixed_array(&uid.uid_bytes);
164
165        // Send event to channel
166        rfid_static
167            .send(RfidEvent::CardDetected { uid: uid_key })
168            .await;
169
170        // Wait to prevent repeated detections of the same card
171        Timer::after_millis(50).await;
172    }
173}
174
175/// Initialize MFRC522 hardware (internal helper function)
176async fn init_mfrc522_hardware<Sck, Mosi, Miso, Dma0, Dma1, Cs, Rst>(
177    spi: Peri<'static, SPI0>,
178    sck: Peri<'static, Sck>,
179    mosi: Peri<'static, Mosi>,
180    miso: Peri<'static, Miso>,
181    dma_ch0: Peri<'static, Dma0>,
182    dma_ch1: Peri<'static, Dma1>,
183    cs: Peri<'static, Cs>,
184    rst: Peri<'static, Rst>,
185) -> Result<Mfrc522Device>
186where
187    Sck: Pin + ClkPin<SPI0>,
188    Mosi: Pin + MosiPin<SPI0>,
189    Miso: Pin + MisoPin<SPI0>,
190    Dma0: Channel,
191    Dma1: Channel,
192    Cs: Pin,
193    Rst: Pin,
194{
195    // Initialize async SPI for RFID
196    let spi = Spi::new(spi, sck, mosi, miso, dma_ch0, dma_ch1, {
197        let mut config = SpiConfig::default();
198        config.frequency = 1_000_000; // 1 MHz
199        config.polarity = Polarity::IdleLow;
200        config.phase = Phase::CaptureOnFirstTransition;
201        config
202    });
203
204    // CS pin for MFRC522
205    let cs = Output::new(cs, Level::High);
206
207    // Reset RFID module
208    let mut rst = Output::new(rst, Level::High);
209    rst.set_low();
210    Timer::after_millis(10).await;
211    rst.set_high();
212    Timer::after_millis(50).await;
213
214    // Wrap SPI+CS in ExclusiveDevice to implement SpiDevice trait
215    let spi_device = ExclusiveDevice::new_no_delay(spi, cs).expect("CS pin is infallible");
216    let spi_driver = SpiDriver::new(spi_device);
217    let mut mfrc522 = MFRC522::new(spi_driver, || Instant::now().as_millis());
218
219    // Initialize the MFRC522 chip
220    mfrc522.pcd_init().await.map_err(Error::Mfrc522Init)?;
221    info!("MFRC522 initialized");
222
223    let _version = mfrc522
224        .pcd_get_version()
225        .await
226        .map_err(Error::Mfrc522Version)?;
227    info!("MFRC522 version read successfully");
228
229    Ok(mfrc522)
230}