1use crate::action::{Action, ActionId};
7use crate::capability::{CapabilityType, OperationCapability};
8use lazy_static::lazy_static;
9use serde::{Deserialize, Serialize};
10
11#[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#[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#[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#[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 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 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#[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#[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#[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#[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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
137pub struct NfcSessionReceipt {
138 pub session_id: Option<String>,
139 pub completed: bool,
140}
141
142#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
144pub struct NfcError {
145 pub code: String,
146 pub message: String,
147}
148
149impl NfcError {
150 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 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#[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}