#[cfg(feature = "nfc-mfrc522")]
mod mfrc522;
mod ndef;
#[cfg(feature = "nfc-mfrc522")]
pub use mfrc522::{Mfrc522Config, Mfrc522InitError, serve_blocking as serve_mfrc522};
pub use ndef::{NdefError, NdefMessageParser, NdefTextRecord, parse_ndef_text_record};
use bloop_protocol::NfcUid;
use thiserror::Error;
use tokio::sync::{mpsc, oneshot};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum NfcReaderError {
#[error("the NFC reader backend is no longer running")]
Disconnected,
#[error("failed to read NDEF text record: {0}")]
Read(String),
}
#[derive(Debug)]
pub enum NfcReaderRequest {
WaitForCard(oneshot::Sender<NfcUid>),
WaitForRemoval(oneshot::Sender<()>),
ReadNdefText(oneshot::Sender<Result<String, String>>),
}
pub type NfcReaderBackend = mpsc::Receiver<NfcReaderRequest>;
#[derive(Clone, Debug)]
pub struct NfcReader {
tx: mpsc::Sender<NfcReaderRequest>,
}
impl NfcReader {
pub fn channel() -> (Self, NfcReaderBackend) {
let (tx, rx) = mpsc::channel(4);
(Self { tx }, rx)
}
#[cfg(feature = "nfc-mfrc522")]
pub async fn spawn_mfrc522(config: Mfrc522Config) -> Result<Self, Mfrc522InitError> {
let (reader, backend) = Self::channel();
mfrc522::spawn(config, backend).await?;
Ok(reader)
}
pub async fn wait_for_card(&self) -> Result<NfcUid, NfcReaderError> {
self.roundtrip(NfcReaderRequest::WaitForCard).await
}
pub async fn wait_for_removal(&self) -> Result<(), NfcReaderError> {
self.roundtrip(NfcReaderRequest::WaitForRemoval).await
}
pub async fn read_ndef_text(&self) -> Result<String, NfcReaderError> {
self.roundtrip(NfcReaderRequest::ReadNdefText)
.await?
.map_err(NfcReaderError::Read)
}
async fn roundtrip<T>(
&self,
request: impl FnOnce(oneshot::Sender<T>) -> NfcReaderRequest,
) -> Result<T, NfcReaderError> {
let (response_tx, response_rx) = oneshot::channel();
self.tx
.send(request(response_tx))
.await
.map_err(|_| NfcReaderError::Disconnected)?;
response_rx.await.map_err(|_| NfcReaderError::Disconnected)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn requests_round_trip_through_a_backend() {
let (reader, mut backend) = NfcReader::channel();
let uid = NfcUid::try_from(&[1u8, 2, 3, 4][..]).unwrap();
tokio::spawn(async move {
while let Some(request) = backend.recv().await {
match request {
NfcReaderRequest::WaitForCard(response) => {
let _ = response.send(uid);
}
NfcReaderRequest::WaitForRemoval(response) => {
let _ = response.send(());
}
NfcReaderRequest::ReadNdefText(response) => {
let _ = response.send(Ok("hello".to_string()));
}
}
}
});
assert_eq!(reader.wait_for_card().await.unwrap(), uid);
reader.wait_for_removal().await.unwrap();
assert_eq!(reader.read_ndef_text().await.unwrap(), "hello");
}
#[tokio::test]
async fn dropped_wait_shows_as_closed_response() {
let (reader, mut backend) = NfcReader::channel();
let wait = reader.wait_for_card();
drop(wait);
let in_flight = tokio::spawn(async move { reader.wait_for_card().await });
let request = backend.recv().await.unwrap();
let NfcReaderRequest::WaitForCard(mut response) = request else {
panic!("unexpected request");
};
in_flight.abort();
response.closed().await;
assert!(response.is_closed());
}
#[tokio::test]
async fn gone_backend_reports_disconnected() {
let (reader, backend) = NfcReader::channel();
drop(backend);
assert!(matches!(
reader.wait_for_card().await,
Err(NfcReaderError::Disconnected)
));
}
}