Skip to main content

fission_core/
platform_nfc.rs

1//! NFC host capabilities.
2//!
3//! The core crate only defines portable typed requests, results, and capability
4//! identities. Shells decide which platform NFC APIs can satisfy them.
5
6use crate::action::{Action, ActionId};
7use crate::capability::{CapabilityType, OperationCapability};
8use lazy_static::lazy_static;
9use serde::{Deserialize, Serialize};
10
11/// NFC operation support reported by the active shell.
12#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct NfcAvailability {
14    pub supported: bool,
15    pub enabled: bool,
16    pub read: bool,
17    pub write: bool,
18    pub card_emulation: bool,
19}
20
21/// NFC technology family requested by an app or discovered on a tag.
22#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum NfcTechnology {
24    IsoDep,
25    NfcA,
26    NfcB,
27    NfcF,
28    NfcV,
29    Ndef,
30    MifareClassic,
31    MifareUltralight,
32    Felica,
33    Other(String),
34}
35
36/// NFC Forum NDEF type-name-format value.
37#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
38pub enum NfcRecordTypeNameFormat {
39    Empty,
40    #[default]
41    WellKnown,
42    MimeMedia,
43    AbsoluteUri,
44    External,
45    Unknown,
46    Unchanged,
47    Reserved(u8),
48}
49
50/// Portable NDEF-like record payload.
51#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
52pub struct NfcRecord {
53    pub type_name_format: NfcRecordTypeNameFormat,
54    pub type_name: Vec<u8>,
55    pub id: Vec<u8>,
56    pub payload: Vec<u8>,
57}
58
59impl NfcRecord {
60    /// Creates a portable NFC text record.
61    ///
62    /// `language` should be a BCP-47-style language code such as `en` or
63    /// `en-GB`. `text` is encoded as UTF-8 in the record payload so the host can
64    /// write or emulate an NDEF text value without app code building raw bytes.
65    pub fn text(language: impl Into<String>, text: impl Into<String>) -> Self {
66        let language = language.into();
67        let text = text.into();
68        let mut payload = Vec::with_capacity(1 + language.len() + text.len());
69        payload.push(language.len().min(63) as u8);
70        payload.extend_from_slice(language.as_bytes());
71        payload.extend_from_slice(text.as_bytes());
72        Self {
73            type_name_format: NfcRecordTypeNameFormat::WellKnown,
74            type_name: b"T".to_vec(),
75            id: Vec::new(),
76            payload,
77        }
78    }
79
80    /// Creates a portable NFC URI record.
81    ///
82    /// `uri` should be the full URI the tag should carry. The helper stores it as
83    /// a UTF-8 payload with a URI record type so reducers do not need to know the
84    /// raw NDEF byte layout.
85    pub fn uri(uri: impl Into<String>) -> Self {
86        let uri = uri.into();
87        let mut payload = Vec::with_capacity(1 + uri.len());
88        payload.push(0);
89        payload.extend_from_slice(uri.as_bytes());
90        Self {
91            type_name_format: NfcRecordTypeNameFormat::WellKnown,
92            type_name: b"U".to_vec(),
93            id: Vec::new(),
94            payload,
95        }
96    }
97}
98
99/// A tag returned by a scan operation.
100#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
101pub struct NfcTag {
102    pub id: Option<Vec<u8>>,
103    pub technologies: Vec<NfcTechnology>,
104    pub records: Vec<NfcRecord>,
105    pub raw_payload: Option<Vec<u8>>,
106}
107
108/// One-shot NFC read request.
109#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
110pub struct NfcScanRequest {
111    pub technologies: Vec<NfcTechnology>,
112    pub message: Option<String>,
113    pub timeout_ms: Option<u64>,
114    pub read_multiple_records: bool,
115}
116
117/// NFC write request. Hosts may require the user to tap a writable tag after
118/// this operation starts.
119#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
120pub struct NfcWriteRequest {
121    pub records: Vec<NfcRecord>,
122    pub message: Option<String>,
123    pub timeout_ms: Option<u64>,
124    pub make_read_only: bool,
125}
126
127/// NFC card-emulation request for hosts that support it.
128#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
129pub struct NfcEmulationRequest {
130    pub records: Vec<NfcRecord>,
131    pub message: Option<String>,
132    pub timeout_ms: Option<u64>,
133}
134
135/// Receipt for write/emulation/session operations.
136#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
137pub struct NfcSessionReceipt {
138    pub session_id: Option<String>,
139    pub completed: bool,
140}
141
142/// Portable NFC error payload.
143#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
144pub struct NfcError {
145    pub code: String,
146    pub message: String,
147}
148
149impl NfcError {
150    /// Creates a portable NFC error payload.
151    ///
152    /// `code` should be stable enough for reducers or tests to match. `message`
153    /// should explain the host failure, such as missing hardware, disabled NFC,
154    /// timeout, incompatible tag, or unsupported write/emulation mode.
155    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
156        Self {
157            code: code.into(),
158            message: message.into(),
159        }
160    }
161
162    /// Creates the standard unsupported-operation NFC error.
163    ///
164    /// `operation` should name the attempted NFC operation, for example `scan`,
165    /// `write`, or `emulate`. Hosts should use this when the capability exists
166    /// but the current platform or hardware cannot perform that operation.
167    pub fn unsupported(operation: impl Into<String>) -> Self {
168        Self::new(
169            "unsupported",
170            format!(
171                "NFC operation `{}` is not supported by this host",
172                operation.into()
173            ),
174        )
175    }
176}
177
178pub struct GetNfcAvailabilityCapability;
179impl OperationCapability for GetNfcAvailabilityCapability {
180    type Request = ();
181    type Ok = NfcAvailability;
182    type Err = NfcError;
183}
184
185pub struct ScanNfcTagCapability;
186impl OperationCapability for ScanNfcTagCapability {
187    type Request = NfcScanRequest;
188    type Ok = NfcTag;
189    type Err = NfcError;
190}
191
192pub struct WriteNfcTagCapability;
193impl OperationCapability for WriteNfcTagCapability {
194    type Request = NfcWriteRequest;
195    type Ok = NfcSessionReceipt;
196    type Err = NfcError;
197}
198
199pub struct EmulateNfcTagCapability;
200impl OperationCapability for EmulateNfcTagCapability {
201    type Request = NfcEmulationRequest;
202    type Ok = NfcSessionReceipt;
203    type Err = NfcError;
204}
205
206pub struct CancelNfcSessionCapability;
207impl OperationCapability for CancelNfcSessionCapability {
208    type Request = ();
209    type Ok = ();
210    type Err = NfcError;
211}
212
213pub const GET_NFC_AVAILABILITY: CapabilityType<GetNfcAvailabilityCapability> =
214    CapabilityType::new("fission.nfc.get_availability");
215pub const SCAN_NFC_TAG: CapabilityType<ScanNfcTagCapability> =
216    CapabilityType::new("fission.nfc.scan_tag");
217pub const WRITE_NFC_TAG: CapabilityType<WriteNfcTagCapability> =
218    CapabilityType::new("fission.nfc.write_tag");
219pub const EMULATE_NFC_TAG: CapabilityType<EmulateNfcTagCapability> =
220    CapabilityType::new("fission.nfc.emulate_tag");
221pub const CANCEL_NFC_SESSION: CapabilityType<CancelNfcSessionCapability> =
222    CapabilityType::new("fission.nfc.cancel_session");
223
224/// Built-in action for host-delivered NFC tag events.
225#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
226pub struct NfcTagDiscovered {
227    pub tag: NfcTag,
228}
229
230impl Action for NfcTagDiscovered {
231    fn static_id() -> ActionId {
232        lazy_static! {
233            static ref ID: ActionId = ActionId::from_name("fission_core::NfcTagDiscovered");
234        }
235        *ID
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn nfc_records_round_trip() {
245        let records = vec![
246            NfcRecord::text("en", "Tap received"),
247            NfcRecord::uri("https://fission.rs/docs"),
248        ];
249        let bytes = serde_json::to_vec(&records).unwrap();
250        let decoded: Vec<NfcRecord> = serde_json::from_slice(&bytes).unwrap();
251        assert_eq!(decoded, records);
252    }
253
254    #[test]
255    fn nfc_scan_request_round_trips() {
256        let request = NfcScanRequest {
257            technologies: vec![NfcTechnology::Ndef, NfcTechnology::IsoDep],
258            message: Some("Hold near the tag".into()),
259            timeout_ms: Some(30_000),
260            read_multiple_records: true,
261        };
262
263        let bytes = serde_json::to_vec(&request).unwrap();
264        let decoded: NfcScanRequest = serde_json::from_slice(&bytes).unwrap();
265
266        assert_eq!(decoded, request);
267    }
268
269    #[test]
270    fn nfc_inbound_action_round_trips() {
271        let action = NfcTagDiscovered {
272            tag: NfcTag {
273                id: Some(vec![1, 2, 3, 4]),
274                technologies: vec![NfcTechnology::Ndef],
275                records: vec![NfcRecord::uri("fission://open/1")],
276                raw_payload: None,
277            },
278        };
279
280        let envelope: crate::ActionEnvelope = action.clone().into();
281        let decoded: NfcTagDiscovered = serde_json::from_slice(&envelope.payload).unwrap();
282
283        assert_eq!(decoded, action);
284    }
285}