1pub mod actions;
11pub mod ndef;
12
13use crate::client::llm_budget::call_llm_for_client;
14use crate::client::nfc::actions::*;
15use crate::llm::ollama_client::OllamaClient;
16use crate::protocol::Event;
17use crate::state::app_state::AppState;
18use crate::state::ClientId;
19use anyhow::{anyhow, Context, Result};
20use serde_json::{json, Value};
21use std::net::SocketAddr;
22use std::sync::Arc;
23use tokio::sync::mpsc;
24use tracing::{error, info, trace, warn};
25
26pub use actions::NfcClientProtocol;
28
29const MAX_FOLLOWUP_DEPTH: u8 = 6;
41
42const MAX_SHORT_READ: usize = 255;
45
46const MAX_SHORT_WRITE: usize = 249;
49
50pub enum NfcApplied {
51 Sent {
54 bytes_sent: usize,
55 response_hex: String,
56 },
57 Executed(String),
59 Disconnected,
61}
62
63pub struct NfcClient;
65
66impl NfcClient {
67 pub async fn connect_with_llm_actions(
69 llm_client: OllamaClient,
70 app_state: Arc<AppState>,
71 status_tx: mpsc::UnboundedSender<String>,
72 client_id: ClientId,
73 startup_params: Value,
74 ) -> Result<SocketAddr> {
75 info!("Starting NFC client via PC/SC...");
76
77 let reader_index = startup_params["reader_index"].as_u64().unwrap_or(0) as usize;
79 let reader_name = startup_params["reader_name"]
80 .as_str()
81 .map(|s| s.to_string());
82
83 let ctx = pcsc::Context::establish(pcsc::Scope::User)
85 .context("Failed to establish PC/SC context. Is pcscd running (Linux)?")?;
86
87 let readers_buf = ctx
89 .list_readers_owned()
90 .context("Failed to list PC/SC readers")?;
91
92 if readers_buf.is_empty() {
93 return Err(anyhow!(
94 "No PC/SC readers found. Please connect an NFC reader (e.g., ACR122U)"
95 ));
96 }
97
98 let readers: Vec<String> = readers_buf
99 .iter()
100 .map(|r| r.to_string_lossy().to_string())
101 .collect();
102
103 info!("Found {} PC/SC reader(s): {:?}", readers.len(), readers);
104
105 let selected_reader: std::ffi::CString = if let Some(name) = reader_name {
108 readers_buf
109 .iter()
110 .find(|r| r.to_string_lossy().contains(&name))
111 .ok_or_else(|| anyhow!("Reader '{}' not found", name))?
112 .clone()
113 } else {
114 readers_buf
115 .get(reader_index)
116 .ok_or_else(|| anyhow!("Reader index {} out of range", reader_index))?
117 .clone()
118 };
119
120 let selected_reader_name = selected_reader.to_string_lossy().to_string();
121 info!("Using PC/SC reader: {}", selected_reader_name);
122 let _ = status_tx.send(format!("Using NFC reader: {}", selected_reader_name));
123
124 let command_rx =
130 crate::client::command_support::register_command_channel(&app_state, client_id).await;
131 let cmd_task = tokio::spawn({
132 let ctx = ctx.clone();
133 let reader = selected_reader.clone();
134 let app_state = app_state.clone();
135 let llm_client = llm_client.clone();
136 let status_tx = status_tx.clone();
137 async move {
138 Self::command_loop(
139 command_rx, ctx, reader, client_id, app_state, llm_client, status_tx,
140 )
141 .await;
142 }
143 });
144 app_state.register_client_task(client_id, cmd_task).await;
145
146 let watch_ctx = ctx.clone();
154 let watch_reader = selected_reader.clone();
155 let watch_state = app_state.clone();
156 let watch_llm = llm_client.clone();
157 let watch_tx = status_tx.clone();
158 let watch_task = tokio::spawn(async move {
159 let mut present = false;
160 loop {
161 if watch_state.get_client(client_id).await.is_none() {
162 return;
163 }
164 let probe_ctx = watch_ctx.clone();
170 let probe_reader = watch_reader.clone();
171 let probe = tokio::task::spawn_blocking(move || -> Option<(String, String)> {
172 let card = probe_ctx
173 .connect(&probe_reader, pcsc::ShareMode::Shared, pcsc::Protocols::ANY)
174 .ok()?;
175 let status = card.status2_owned().ok()?;
176 let protocol = match status.protocol2() {
177 Some(pcsc::Protocol::T0) => "T0",
178 Some(pcsc::Protocol::T1) => "T1",
179 Some(pcsc::Protocol::RAW) => "RAW",
180 None => "undefined",
181 };
182 Some((hex::encode_upper(status.atr()), protocol.to_string()))
183 })
184 .await
185 .unwrap_or(None);
186
187 match (present, probe) {
188 (false, Some((atr, protocol))) => {
189 present = true;
190 info!(
191 "NFC client {} card detected (ATR {}, protocol {})",
192 client_id, atr, protocol
193 );
194 Self::raise_card_event(
195 &NFC_CARD_DETECTED_EVENT,
196 serde_json::json!({ "atr": atr, "protocol": protocol }),
197 &watch_ctx,
198 &watch_reader,
199 0,
200 client_id,
201 &watch_state,
202 &watch_llm,
203 &watch_tx,
204 )
205 .await;
206 }
207 (true, None) => {
208 present = false;
209 info!("NFC client {} card removed", client_id);
210 Self::raise_card_event(
211 &NFC_CARD_DISCONNECTED_EVENT,
212 serde_json::json!({}),
213 &watch_ctx,
214 &watch_reader,
215 0,
216 client_id,
217 &watch_state,
218 &watch_llm,
219 &watch_tx,
220 )
221 .await;
222 }
223 _ => {}
224 }
225 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
226 }
227 });
228 app_state.register_client_task(client_id, watch_task).await;
229
230 {
232 let event = Event::new(
233 &NFC_READERS_LISTED_EVENT,
234 json!({ "readers": readers.clone() }),
235 );
236
237 let instruction = startup_params["instruction"]
239 .as_str()
240 .unwrap_or("Monitor NFC reader and respond to card events");
241
242 let protocol = Arc::new(NfcClientProtocol);
244
245 match call_llm_for_client(
249 &llm_client,
250 &app_state,
251 client_id.to_string(),
252 instruction,
253 "", Some(&event),
255 protocol.as_ref(),
256 &status_tx,
257 )
258 .await
259 {
260 Ok(result) => {
261 for action in result.actions {
262 let applied = Self::apply_nfc_action(
263 &ctx,
264 &selected_reader,
265 action.clone(),
266 0,
267 client_id,
268 &app_state,
269 &llm_client,
270 &status_tx,
271 )
272 .await;
273 if matches!(applied, Ok(NfcApplied::Disconnected)) {
274 break;
275 }
276 if let Err(e) = applied {
277 error!("NFC client {} action error: {}", client_id, e);
278 }
279 }
280 }
281 Err(e) => {
282 error!("LLM error for NFC client {}: {}", client_id, e);
283 }
284 }
285 }
286
287 Ok(SocketAddr::from(([127, 0, 0, 1], 0)))
289 }
290
291 async fn command_loop(
301 mut command_rx: tokio::sync::mpsc::Receiver<crate::state::client_handles::ClientCommand>,
302 ctx: pcsc::Context,
303 reader: std::ffi::CString,
304 client_id: ClientId,
305 app_state: Arc<AppState>,
306 llm_client: OllamaClient,
307 status_tx: mpsc::UnboundedSender<String>,
308 ) {
309 use crate::llm::actions::protocol_trait::Protocol;
310 use crate::state::client_handles::ClientSendOutcome;
311 use crate::state::AccessLogOwner;
312
313 let protocol = NfcClientProtocol;
314
315 while let Some(command) = command_rx.recv().await {
316 let action = command.action.clone();
317 let mut extra_log: Vec<serde_json::Value> = Vec::new();
318
319 let outcome: anyhow::Result<ClientSendOutcome> = match Self::apply_nfc_action(
320 &ctx,
321 &reader,
322 action.clone(),
323 0,
324 client_id,
325 &app_state,
326 &llm_client,
327 &status_tx,
328 )
329 .await
330 {
331 Ok(NfcApplied::Sent {
332 bytes_sent,
333 response_hex,
334 }) => {
335 extra_log.push(json!({ "response_hex": response_hex }));
336 Ok(ClientSendOutcome::Sent { bytes_sent })
337 }
338 Ok(NfcApplied::Executed(detail)) => Ok(ClientSendOutcome::Executed { detail }),
339 Ok(NfcApplied::Disconnected) => Ok(ClientSendOutcome::Disconnected),
340 Err(e) => Ok(ClientSendOutcome::Rejected {
341 error: e.to_string(),
342 }),
343 };
344
345 let mut responses = vec![match &outcome {
346 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
347 Err(e) => json!({ "error": e.to_string() }),
348 }];
349 responses.append(&mut extra_log);
350 app_state
351 .record_access_log(
352 AccessLogOwner::Client(client_id.as_u32()),
353 protocol.protocol_name(),
354 None,
355 "injected_action",
356 action,
357 responses,
358 )
359 .await;
360
361 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
362 let _ = status_tx.send("__UPDATE_UI__".to_string());
363 crate::client::command_support::reply(command, outcome);
364
365 if disconnect {
366 break;
367 }
368 }
369
370 app_state.remove_client_handle(client_id).await;
373 let _ = status_tx.send("__UPDATE_UI__".to_string());
374 }
375
376 #[allow(clippy::too_many_arguments)]
386 async fn raise_card_event(
387 event_type: &'static std::sync::LazyLock<crate::protocol::EventType>,
388 data: serde_json::Value,
389 ctx: &pcsc::Context,
390 reader: &std::ffi::CStr,
391 depth: u8,
392 client_id: ClientId,
393 app_state: &Arc<AppState>,
394 llm_client: &OllamaClient,
395 status_tx: &mpsc::UnboundedSender<String>,
396 ) {
397 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
398 return;
399 };
400 let memory = app_state
401 .get_memory_for_client(client_id)
402 .await
403 .unwrap_or_default();
404 let event = Event::new(event_type, data);
405 match call_llm_for_client(
406 llm_client,
407 app_state,
408 client_id.to_string(),
409 &instruction,
410 &memory,
411 Some(&event),
412 &NfcClientProtocol,
413 status_tx,
414 )
415 .await
416 {
417 Ok(result) => {
418 if let Some(mem) = result.memory_updates {
419 app_state.set_memory_for_client(client_id, mem).await;
420 }
421 Self::run_followups(
422 result.actions,
423 ctx,
424 reader,
425 depth,
426 client_id,
427 app_state,
428 llm_client,
429 status_tx,
430 )
431 .await;
432 }
433 Err(e) => {
434 error!("NFC client {} LLM error on card event: {}", client_id, e);
435 }
436 }
437 }
438
439 fn ndef_file_id(data: &serde_json::Value) -> [u8; 2] {
442 data.get("file_id")
443 .and_then(|v| v.as_str())
444 .and_then(|s| hex::decode(s.trim()).ok())
445 .filter(|b| b.len() == 2)
446 .map(|b| [b[0], b[1]])
447 .unwrap_or([0xE1, 0x04])
448 }
449
450 async fn transmit_apdu(
452 ctx: &pcsc::Context,
453 reader: &std::ffi::CStr,
454 apdu: Vec<u8>,
455 ) -> Result<Vec<u8>> {
456 let ctx = ctx.clone();
457 let reader = reader.to_owned();
458 tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
459 let card = ctx
460 .connect(&reader, pcsc::ShareMode::Shared, pcsc::Protocols::ANY)
461 .context("PC/SC connect failed (is a card on the reader?)")?;
462 let mut rx = vec![0u8; pcsc::MAX_BUFFER_SIZE];
463 let response = card
464 .transmit(&apdu, &mut rx)
465 .context("PC/SC transmit failed")?;
466 Ok(response.to_vec())
467 })
468 .await
469 .context("PC/SC blocking task panicked")?
470 }
471
472 fn apdu_ok(response: &[u8]) -> bool {
474 response.len() >= 2 && response[response.len() - 2..] == [0x90, 0x00]
475 }
476
477 async fn read_ndef(
493 ctx: &pcsc::Context,
494 reader: &std::ffi::CStr,
495 file_id: [u8; 2],
496 ) -> Result<Vec<u8>> {
497 let select_app = vec![
499 0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00,
500 ];
501 let r = Self::transmit_apdu(ctx, reader, select_app).await?;
502 if !Self::apdu_ok(&r) {
503 anyhow::bail!(
504 "SELECT NDEF application refused (SW {})",
505 hex::encode_upper(&r)
506 );
507 }
508
509 let select_file = vec![0x00, 0xA4, 0x00, 0x0C, 0x02, file_id[0], file_id[1]];
511 let r = Self::transmit_apdu(ctx, reader, select_file).await?;
512 if !Self::apdu_ok(&r) {
513 anyhow::bail!("SELECT NDEF file refused (SW {})", hex::encode_upper(&r));
514 }
515
516 let r = Self::transmit_apdu(ctx, reader, vec![0x00, 0xB0, 0x00, 0x00, 0x02]).await?;
518 if !Self::apdu_ok(&r) || r.len() < 4 {
519 anyhow::bail!("READ BINARY of NLEN failed (SW {})", hex::encode_upper(&r));
520 }
521 let nlen = u16::from_be_bytes([r[0], r[1]]) as usize;
522 if nlen == 0 {
523 return Ok(Vec::new());
524 }
525
526 let mut message = Vec::with_capacity(nlen);
527 let mut offset = 2usize;
528 while message.len() < nlen {
529 let want = std::cmp::min(MAX_SHORT_READ, nlen - message.len());
535 let offset_bytes = Self::read_binary_offset(offset)?;
536 let apdu = vec![
537 0x00,
538 0xB0,
539 offset_bytes[0],
540 offset_bytes[1],
541 want as u8,
543 ];
544 let r = Self::transmit_apdu(ctx, reader, apdu).await?;
545 if !Self::apdu_ok(&r) {
546 anyhow::bail!("READ BINARY failed (SW {})", hex::encode_upper(&r));
547 }
548 let got = &r[..r.len() - 2];
549 if got.is_empty() {
550 anyhow::bail!(
551 "READ BINARY returned 9000 with no data at offset {offset} while {} of {} \
552 byte(s) are still outstanding; the tag is not making progress",
553 nlen - message.len(),
554 nlen
555 );
556 }
557 message.extend_from_slice(got);
558 offset += got.len();
559 }
560 message.truncate(nlen);
561 Ok(message)
562 }
563
564 fn read_binary_offset(offset: usize) -> Result<[u8; 2]> {
569 let offset = u16::try_from(offset).map_err(|_| {
570 anyhow!(
571 "offset {offset} does not fit the two P1 P2 bytes of a short-form \
572 READ/UPDATE BINARY (max {})",
573 u16::MAX
574 )
575 })?;
576 Ok(offset.to_be_bytes())
577 }
578
579 async fn write_ndef(
587 ctx: &pcsc::Context,
588 reader: &std::ffi::CStr,
589 file_id: [u8; 2],
590 message: &[u8],
591 ) -> Result<()> {
592 if message.len() > ndef::MAX_MESSAGE_LEN {
597 anyhow::bail!(
598 "NDEF message is {} bytes; a Type 4 tag's two-byte NLEN describes at most {}",
599 message.len(),
600 ndef::MAX_MESSAGE_LEN
601 );
602 }
603
604 let select_app = vec![
605 0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00,
606 ];
607 let r = Self::transmit_apdu(ctx, reader, select_app).await?;
608 if !Self::apdu_ok(&r) {
609 anyhow::bail!(
610 "SELECT NDEF application refused (SW {})",
611 hex::encode_upper(&r)
612 );
613 }
614 let select_file = vec![0x00, 0xA4, 0x00, 0x0C, 0x02, file_id[0], file_id[1]];
615 let r = Self::transmit_apdu(ctx, reader, select_file).await?;
616 if !Self::apdu_ok(&r) {
617 anyhow::bail!("SELECT NDEF file refused (SW {})", hex::encode_upper(&r));
618 }
619
620 let r = Self::transmit_apdu(ctx, reader, vec![0x00, 0xD6, 0x00, 0x00, 0x02, 0x00, 0x00])
622 .await?;
623 if !Self::apdu_ok(&r) {
624 anyhow::bail!(
625 "UPDATE BINARY of NLEN failed (SW {})",
626 hex::encode_upper(&r)
627 );
628 }
629
630 let mut written = 0usize;
631 while written < message.len() {
632 let chunk = std::cmp::min(MAX_SHORT_WRITE, message.len() - written);
633 let offset = 2 + written;
634 let offset_bytes = Self::read_binary_offset(offset)?;
635 let mut apdu = vec![
636 0x00,
637 0xD6,
638 offset_bytes[0],
639 offset_bytes[1],
640 chunk as u8,
642 ];
643 apdu.extend_from_slice(&message[written..written + chunk]);
644 let r = Self::transmit_apdu(ctx, reader, apdu).await?;
645 if !Self::apdu_ok(&r) {
646 anyhow::bail!("UPDATE BINARY failed (SW {})", hex::encode_upper(&r));
647 }
648 written += chunk;
649 }
650
651 let nlen = (message.len() as u16).to_be_bytes();
654 let r = Self::transmit_apdu(
655 ctx,
656 reader,
657 vec![0x00, 0xD6, 0x00, 0x00, 0x02, nlen[0], nlen[1]],
658 )
659 .await?;
660 if !Self::apdu_ok(&r) {
661 anyhow::bail!(
662 "UPDATE BINARY of final NLEN failed (SW {})",
663 hex::encode_upper(&r)
664 );
665 }
666 Ok(())
667 }
668
669 #[allow(clippy::too_many_arguments)]
676 async fn run_followups(
677 actions: Vec<serde_json::Value>,
678 ctx: &pcsc::Context,
679 reader: &std::ffi::CStr,
680 depth: u8,
681 client_id: ClientId,
682 app_state: &Arc<AppState>,
683 llm_client: &OllamaClient,
684 status_tx: &mpsc::UnboundedSender<String>,
685 ) {
686 if actions.is_empty() {
687 return;
688 }
689 if depth >= MAX_FOLLOWUP_DEPTH {
690 warn!(
691 "NFC client {} stopped a follow-up chain at depth {}: {} action(s) not executed",
692 client_id,
693 depth,
694 actions.len()
695 );
696 let _ = status_tx.send(format!(
697 "[CLIENT] ⚠ nfc client {} hit the follow-up depth limit ({}); {} action(s) were \
698 not executed",
699 client_id,
700 MAX_FOLLOWUP_DEPTH,
701 actions.len()
702 ));
703 return;
704 }
705 for action in actions {
706 match Self::apply_nfc_action(
707 ctx,
708 reader,
709 action,
710 depth + 1,
711 client_id,
712 app_state,
713 llm_client,
714 status_tx,
715 )
716 .await
717 {
718 Ok(NfcApplied::Disconnected) => return,
719 Ok(_) => trace!("NFC client {} follow-up action completed", client_id),
720 Err(e) => {
721 error!("NFC client {} follow-up action failed: {:#}", client_id, e);
722 let _ = status_tx.send(format!(
723 "[CLIENT] nfc client {client_id} follow-up failed: {e}"
724 ));
725 }
726 }
727 }
728 }
729
730 #[allow(clippy::too_many_arguments)]
734 fn apply_nfc_action<'a>(
735 ctx: &'a pcsc::Context,
736 reader: &'a std::ffi::CStr,
737 action: Value,
738 depth: u8,
739 client_id: ClientId,
740 app_state: &'a Arc<AppState>,
741 llm_client: &'a OllamaClient,
742 status_tx: &'a mpsc::UnboundedSender<String>,
743 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<NfcApplied>> + Send + 'a>> {
744 Box::pin(async move {
745 use crate::llm::actions::client_trait::{Client, ClientActionResult};
746
747 match NfcClientProtocol.execute_action(action)? {
748 ClientActionResult::Custom { name, data } if name == "send_apdu" => {
749 let apdu_hex = data
750 .get("apdu_hex")
751 .and_then(|v| v.as_str())
752 .context("Missing apdu_hex in action data")?
753 .to_string();
754 let apdu = hex::decode(apdu_hex.trim())
755 .context("apdu_hex is not valid hexadecimal")?;
756 let sent = apdu.len();
757
758 let card_ctx = ctx.clone();
763 let card_reader = reader.to_owned();
764 let transmit = tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
765 let card = card_ctx
766 .connect(&card_reader, pcsc::ShareMode::Shared, pcsc::Protocols::ANY)
767 .context("PC/SC connect failed (is a card on the reader?)")?;
768 let mut rx = vec![0u8; pcsc::MAX_BUFFER_SIZE];
769 let response = card
770 .transmit(&apdu, &mut rx)
771 .context("PC/SC transmit failed")?;
772 Ok(response.to_vec())
773 })
774 .await
775 .context("PC/SC blocking task panicked")?;
776
777 match transmit {
778 Ok(response) => {
779 let response_hex = hex::encode_upper(&response);
780 info!(
781 "NFC client {} APDU {} bytes -> response {}",
782 client_id, sent, response_hex
783 );
784 let _ = status_tx.send(format!(
785 "[CLIENT] NFC client {} APDU response: {}",
786 client_id, response_hex
787 ));
788
789 Self::notify_apdu_response(
792 &response, &ctx, &reader, depth, client_id, app_state, llm_client,
793 status_tx,
794 )
795 .await;
796
797 Ok(NfcApplied::Sent {
798 bytes_sent: sent,
799 response_hex,
800 })
801 }
802 Err(e) => {
803 warn!("NFC client {} APDU failed: {:#}", client_id, e);
804 Ok(NfcApplied::Executed(format!(
805 "send_apdu did not reach a card: {e:#}"
806 )))
807 }
808 }
809 }
810 ClientActionResult::Custom { name, data } if name == "read_ndef" => {
811 let file_id = Self::ndef_file_id(&data);
812 match Self::read_ndef(ctx, reader, file_id).await {
813 Ok(message) => {
814 let hex_msg = hex::encode_upper(&message);
815 info!(
816 "NFC client {} read {} byte NDEF message",
817 client_id,
818 message.len()
819 );
820 Self::notify_ndef_read(
823 &message, &ctx, &reader, depth, client_id, app_state, llm_client,
824 status_tx,
825 )
826 .await;
827 Ok(NfcApplied::Executed(format!(
828 "read_ndef: {} bytes ({})",
829 message.len(),
830 if hex_msg.is_empty() {
831 "empty"
832 } else {
833 &hex_msg
834 }
835 )))
836 }
837 Err(e) => Ok(NfcApplied::Executed(format!("read_ndef failed: {e:#}"))),
838 }
839 }
840 ClientActionResult::Custom { name, data } if name == "write_ndef" => {
841 let file_id = Self::ndef_file_id(&data);
842 let record_count = data
848 .get("records")
849 .and_then(|v| v.as_array())
850 .map(|r| r.len())
851 .unwrap_or(0);
852 let bytes =
853 match data.get("message_hex").and_then(|v| v.as_str()) {
854 Some(h) => match hex::decode(h.trim()) {
855 Ok(b) => b,
856 Err(e) => {
857 return Ok(NfcApplied::Executed(format!(
858 "write_ndef: encoded message is not valid hexadecimal: {e}"
859 )))
860 }
861 },
862 None => return Ok(NfcApplied::Executed(
863 "write_ndef: no encoded message; 'records' must be a non-empty \
864 array of typed NDEF records"
865 .to_string(),
866 )),
867 };
868 match Self::write_ndef(ctx, reader, file_id, &bytes).await {
869 Ok(()) => Ok(NfcApplied::Sent {
870 bytes_sent: bytes.len(),
871 response_hex: format!(
872 "write_ndef ok ({record_count} record(s), {} NDEF byte(s))",
873 bytes.len()
874 ),
875 }),
876 Err(e) => Ok(NfcApplied::Executed(format!("write_ndef failed: {e:#}"))),
877 }
878 }
879 ClientActionResult::Custom { name, .. } => Ok(NfcApplied::Executed(format!(
880 "'{name}' is declared but not implemented by the NFC client; \
881 send_apdu / send_apdu_raw / read_ndef / write_ndef reach the card \
882 (see src/client/nfc/CLAUDE.md)"
883 ))),
884 ClientActionResult::Disconnect => {
885 info!("NFC client {} disconnecting card session", client_id);
886 app_state
887 .update_client_status(client_id, crate::state::ClientStatus::Disconnected)
888 .await;
889 app_state.remove_client_handle(client_id).await;
893 let _ = status_tx.send("__UPDATE_UI__".to_string());
894 Ok(NfcApplied::Disconnected)
895 }
896 ClientActionResult::WaitForMore => {
897 Ok(NfcApplied::Executed("wait_for_more".to_string()))
898 }
899 other => Ok(NfcApplied::Executed(format!(
900 "unhandled action result {other:?}"
901 ))),
902 }
903 })
904 }
905
906 #[allow(clippy::too_many_arguments)]
911 async fn notify_ndef_read(
912 message: &[u8],
913 ctx: &pcsc::Context,
914 reader: &std::ffi::CStr,
915 depth: u8,
916 client_id: ClientId,
917 app_state: &Arc<AppState>,
918 llm_client: &OllamaClient,
919 status_tx: &mpsc::UnboundedSender<String>,
920 ) {
921 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
922 return;
923 };
924 let records = ndef::decode_message(message).unwrap_or_default();
929 let event = Event::new(
930 &NFC_NDEF_READ_EVENT,
931 json!({
932 "records": records,
933 "length": message.len(),
934 "message_hex": hex::encode_upper(message),
936 "message_text": String::from_utf8_lossy(message).to_string(),
937 }),
938 );
939 let memory = app_state
940 .get_memory_for_client(client_id)
941 .await
942 .unwrap_or_default();
943 match call_llm_for_client(
944 llm_client,
945 app_state,
946 client_id.to_string(),
947 &instruction,
948 &memory,
949 Some(&event),
950 &NfcClientProtocol,
951 status_tx,
952 )
953 .await
954 {
955 Ok(result) => {
956 if let Some(mem) = result.memory_updates {
957 app_state.set_memory_for_client(client_id, mem).await;
958 }
959 Self::run_followups(
960 result.actions,
961 ctx,
962 reader,
963 depth,
964 client_id,
965 app_state,
966 llm_client,
967 status_tx,
968 )
969 .await;
970 }
971 Err(e) => {
972 error!("NFC client {} LLM error on nfc_ndef_read: {}", client_id, e);
973 }
974 }
975 }
976
977 #[allow(clippy::too_many_arguments)]
979 async fn notify_apdu_response(
980 response: &[u8],
981 ctx: &pcsc::Context,
982 reader: &std::ffi::CStr,
983 depth: u8,
984 client_id: ClientId,
985 app_state: &Arc<AppState>,
986 llm_client: &OllamaClient,
987 status_tx: &mpsc::UnboundedSender<String>,
988 ) {
989 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
990 return;
991 };
992 let (data, sw) = if response.len() >= 2 {
993 response.split_at(response.len() - 2)
994 } else {
995 (response, &[][..])
996 };
997 let event = Event::new(
998 &NFC_APDU_RESPONSE_EVENT,
999 json!({
1000 "response_hex": hex::encode_upper(response),
1001 "sw1": sw.first().map(|b| format!("{b:02X}")).unwrap_or_default(),
1002 "sw2": sw.get(1).map(|b| format!("{b:02X}")).unwrap_or_default(),
1003 "data_hex": hex::encode_upper(data),
1004 }),
1005 );
1006 let memory = app_state
1007 .get_memory_for_client(client_id)
1008 .await
1009 .unwrap_or_default();
1010 match call_llm_for_client(
1011 llm_client,
1012 app_state,
1013 client_id.to_string(),
1014 &instruction,
1015 &memory,
1016 Some(&event),
1017 &NfcClientProtocol,
1018 status_tx,
1019 )
1020 .await
1021 {
1022 Ok(result) => {
1023 if let Some(mem) = result.memory_updates {
1024 app_state.set_memory_for_client(client_id, mem).await;
1025 }
1026 Self::run_followups(
1027 result.actions,
1028 ctx,
1029 reader,
1030 depth,
1031 client_id,
1032 app_state,
1033 llm_client,
1034 status_tx,
1035 )
1036 .await;
1037 }
1038 Err(e) => {
1039 error!("LLM error on nfc_apdu_response for {}: {}", client_id, e);
1040 }
1041 }
1042 }
1043}