// lib/services/nfc_service.dart // // NFC 封装服务(包含读取和写入方法,均提供回调与 Future 两种使用方式) // 请将此文件放到你的项目中(例如 lib/services/nfc_service.dart)并直接使用。 import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:nfc_manager/nfc_manager.dart'; import 'package:nfc_manager/ndef_record.dart'; import 'package:nfc_manager/nfc_manager_android.dart'; import 'package:nfc_manager/nfc_manager_ios.dart'; import 'package:nfc_manager_ndef/nfc_manager_ndef.dart'; /// NfcService 单例:提供 NFC 的读取与写入基础能力 /// /// - startScanOnceWithCallback(...):回调式的一次性读取(发现标签即回调) /// - readOnceText(...):Future 式读取,返回解析出的文本(若失败会抛异常) /// - writeText(...):写入文本记录到标签(回调/返回写入结果) /// - stopSession():停止当前会话 /// /// 注:iOS 必须在真机测试,并在 Xcode 中打开 NFC 权限;Android 需设备支持 NFC。 class NfcService { NfcService._internal(); static final NfcService instance = NfcService._internal(); /// 是否正在进行 NFC 会话(扫描或写入) final ValueNotifier scanning = ValueNotifier(false); /// 日志广播流(方便 UI 订阅) final StreamController _logController = StreamController.broadcast(); Stream get logs => _logController.stream; /// 检查设备是否支持 NFC Future isAvailable() => NfcManager.instance.isAvailable(); /// 停止会话(若正在运行) Future stopSession() async { if (scanning.value) { try { await NfcManager.instance.stopSession(); } catch (_) {} scanning.value = false; } } // ---------------- 辅助方法 ---------------- /// bytes -> "AA:BB:CC" 形式的大写十六进制字符串 String _bytesToHex(Uint8List bytes) { return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':').toUpperCase(); } /// 递归在 Map/List 中查找第一个可用的字节数组 (List / Uint8List) List? _findFirstByteArray(dynamic value) { if (value == null) return null; if (value is Uint8List) return value.toList(); if (value is List) return value; if (value is List) { for (var item in value) { final found = _findFirstByteArray(item); if (found != null) return found; } return null; } if (value is Map) { for (final entry in value.entries) { final found = _findFirstByteArray(entry.value); if (found != null) return found; } } return null; } /// 从 tag(NfcTag / Map)中提取 UID,兼容多种实现 String extractUidFromTag(dynamic tag) { try { dynamic raw = tag; // 有些实现把数据放 data 字段 if (tag is Map && tag.containsKey('data')) raw = tag['data']; final bytes = _findFirstByteArray(raw); if (bytes != null && bytes.isNotEmpty) { return _bytesToHex(Uint8List.fromList(bytes)); } // 再尝试一些常见的键名 if (tag is Map) { final possible = ['id', 'identifier', 'Id', 'ID']; for (final k in possible) { if (tag[k] != null) { final b = _findFirstByteArray(tag[k]); if (b != null) return _bytesToHex(Uint8List.fromList(b)); } } } // 最后一招:平台特定 API (如果存在) if (Platform.isAndroid) { try { final nfcA = NfcAAndroid.from(tag); if (nfcA != null && nfcA.tag.id != null) return _bytesToHex(Uint8List.fromList(nfcA.tag.id)); } catch (_) {} } else if (Platform.isIOS) { try { final mifare = MiFareIos.from(tag); if (mifare != null && mifare.identifier != null) return _bytesToHex(Uint8List.fromList(mifare.identifier)); } catch (_) {} } } catch (e) { debugPrint('extractUidFromTag error: $e'); } return 'UNKNOWN'; } /// 从 NDEF record 的 payload 解析文本(Text Record)并返回字符串 String parseTextFromPayload(Uint8List payload) { if (payload.isEmpty) return ''; final status = payload[0]; final langLen = status & 0x3F; final textBytes = payload.sublist(1 + langLen); try { return utf8.decode(textBytes); } catch (e) { return String.fromCharCodes(textBytes); } } String _formatMessageToString(dynamic msg) { if (msg == null) return ''; try { final records = (msg is Map && msg['records'] != null) ? List.from(msg['records']) : (msg is dynamic ? (msg.records as List?) : null); if (records == null || records.isEmpty) return ''; final sb = StringBuffer(); for (var i = 0; i < records.length; i++) { final r = records[i]; sb.writeln('Record $i: ${r.toString()}'); try { // 解析 payload Uint8List? p; if (r is Map && r['payload'] != null) { final ptmp = r['payload']; if (ptmp is Uint8List) p = ptmp; else if (ptmp is List) p = Uint8List.fromList(ptmp); } else { final pr = (r as dynamic).payload; if (pr is Uint8List) p = pr; else if (pr is List) p = Uint8List.fromList(pr); } if (p != null) { final txt = parseTextFromPayload(p); sb.writeln(' text: $txt'); } } catch (_) {} } return sb.toString(); } catch (e) { return msg.toString(); } } // ---------------- 读取 API ---------------- /// 回调式的一次性扫描(发现标签后立即回调) /// /// onResult(uid, parsedText, rawMessage) - 当发现标签时回调 /// onError(error) - 出错时回调 /// timeout - 超时时间(可选) Future startScanOnceWithCallback({ required void Function(String uid, String parsedText, dynamic rawMessage) onResult, void Function(Object error)? onError, Duration? timeout, }) async { final available = await isAvailable(); if (!available) { onError?.call('NFC not available'); return; } if (scanning.value) { onError?.call('Another session running'); return; } scanning.value = true; _logController.add('NFC scanning started'); Timer? timer; if (timeout != null) { timer = Timer(timeout, () async { await stopSession(); _logController.add('NFC scanning timeout'); onError?.call('timeout'); }); } try { await NfcManager.instance.startSession( onDiscovered: (dynamic tag) async { try { final uid = extractUidFromTag(tag); dynamic rawMsg; String parsedText = ''; // 尝试用 Ndef.from 获取消息 try { final ndef = Ndef.from(tag); if (ndef != null) { rawMsg = ndef.cachedMessage; if (rawMsg != null) { // 解析第一条文本记录(若存在) if ((rawMsg.records as List).isNotEmpty) { final first = rawMsg.records.first; final payload = first.payload; if (payload is Uint8List) parsedText = parseTextFromPayload(payload); else if (payload is List) parsedText = parseTextFromPayload(Uint8List.fromList(payload)); } } } else { rawMsg = null; } } catch (_) { // 回退:尝试从 tag map 中嗅探 cachedMessage / records try { if (tag is Map) { rawMsg = tag['cachedMessage'] ?? tag['ndef']?['cachedMessage'] ?? tag['message']; if (rawMsg != null) { final recs = (rawMsg['records'] ?? (rawMsg as dynamic).records) as dynamic; if (recs != null && recs is List && recs.isNotEmpty) { final r = recs.first; final p = (r is Map) ? r['payload'] : (r as dynamic).payload; if (p is Uint8List) parsedText = parseTextFromPayload(p); else if (p is List) parsedText = parseTextFromPayload(Uint8List.fromList(p)); } } } } catch (_) {} } // 回调结果 onResult(uid, parsedText, rawMsg); _logController.add('UID: $uid\nmessage: ${_formatMessageToString(rawMsg)}'); await stopSession(); } catch (e) { onError?.call(e); await stopSession(); } finally { timer?.cancel(); scanning.value = false; } }, // 尽量多协议尝试以提升兼容性 pollingOptions: {NfcPollingOption.iso14443}, ); } catch (e) { scanning.value = false; timer?.cancel(); onError?.call(e); } } /// Future 式读取:解析第一条文本记录并返回字符串(格式:'UID\n文本') Future readOnceText({Duration timeout = const Duration(seconds: 10)}) async { final completer = Completer(); await startScanOnceWithCallback( onResult: (uid, parsedText, rawMsg) { if (parsedText.isEmpty) { completer.completeError('No text record found (UID: $uid)'); } else { completer.complete('UID: $uid\n$parsedText'); } }, onError: (err) { if (!completer.isCompleted) completer.completeError(err); }, timeout: timeout, ); return completer.future; } // ---------------- 写入 API ---------------- /// 构造 NDEF Text payload(status byte + lang + utf8 bytes) Uint8List _buildTextPayload(String text, {String lang = 'en'}) { final textBytes = utf8.encode(text); final langBytes = utf8.encode(lang); final status = langBytes.length & 0x3F; final payload = [status, ...langBytes, ...textBytes]; return Uint8List.fromList(payload); } /// 写入文本到标签(单条 Text record) /// /// - text: 要写入的文本 /// - timeout: 超时时间(可选) /// - onComplete: 回调 (ok, err) /// /// 返回 true 表示写入成功(同时 onComplete 也会被触发) Future writeText(String text, {Duration? timeout, void Function(bool ok, Object? err)? onComplete}) async { final available = await isAvailable(); if (!available) { onComplete?.call(false, 'NFC not available'); return false; } if (scanning.value) { onComplete?.call(false, 'Another session running'); return false; } scanning.value = true; Timer? timer; if (timeout != null) { timer = Timer(timeout, () async { await stopSession(); scanning.value = false; onComplete?.call(false, 'timeout'); }); } bool success = false; try { await NfcManager.instance.startSession(onDiscovered: (dynamic tag) async { try { final ndef = Ndef.from(tag); if (ndef == null) { onComplete?.call(false, 'Tag 不支持 NDEF'); await stopSession(); return; } final payload = _buildTextPayload(text, lang: 'en'); final record = NdefRecord( typeNameFormat: TypeNameFormat.wellKnown, type: Uint8List.fromList('T'.codeUnits), identifier: Uint8List(0), payload: payload, ); final message = NdefMessage(records: [record]); await ndef.write(message: message); // 取出 UID (优先取 nfca.identifier,如果没有就取顶层 id) String? uid; if (Platform.isAndroid) { try { final nfcA = NfcAAndroid.from(tag); if (nfcA != null && nfcA.tag.id != null) { uid = _bytesToHex(Uint8List.fromList(nfcA.tag.id)); } } catch (_) {} } else if (Platform.isIOS) { try { final mifare = MiFareIos.from(tag); if (mifare != null && mifare.identifier != null) { uid = _bytesToHex(Uint8List.fromList(mifare.identifier)); } } catch (_) {} } success = true; onComplete?.call(true, uid); // ✅ 把 UID 返回出去 _logController.add('NFC write success, UID=$uid'); } catch (e) { debugPrint('NFC write error: $e'); onComplete?.call(false, e); } finally { await stopSession(); timer?.cancel(); scanning.value = false; } }, pollingOptions: {NfcPollingOption.iso14443}); } catch (e) { scanning.value = false; timer?.cancel(); onComplete?.call(false, e); } return success; } /// 释放资源 void dispose() { try { _logController.close(); } catch (_) {} } }