flutter_integrated_whb/lib/services/nfc_service.dart

394 lines
13 KiB
Dart
Raw Normal View History

2025-08-25 11:09:23 +08:00
// 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<bool> scanning = ValueNotifier(false);
/// 日志广播流(方便 UI 订阅)
final StreamController<String> _logController = StreamController.broadcast();
Stream<String> get logs => _logController.stream;
/// 检查设备是否支持 NFC
Future<bool> isAvailable() => NfcManager.instance.isAvailable();
/// 停止会话(若正在运行)
Future<void> 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<int> / Uint8List)
List<int>? _findFirstByteArray(dynamic value) {
if (value == null) return null;
if (value is Uint8List) return value.toList();
if (value is List<int>) 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;
}
/// 从 tagNfcTag / 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 '<empty>';
try {
final records = (msg is Map && msg['records'] != null)
? List<dynamic>.from(msg['records'])
: (msg is dynamic ? (msg.records as List<dynamic>?) : null);
if (records == null || records.isEmpty) return '<empty>';
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<int>) p = Uint8List.fromList(ptmp);
} else {
final pr = (r as dynamic).payload;
if (pr is Uint8List) p = pr;
else if (pr is List<int>) 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<void> 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<int>) 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<int>) 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<String> readOnceText({Duration timeout = const Duration(seconds: 10)}) async {
final completer = Completer<String>();
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 payloadstatus 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 = <int>[status, ...langBytes, ...textBytes];
return Uint8List.fromList(payload);
}
/// 写入文本到标签(单条 Text record
///
/// - text: 要写入的文本
/// - timeout: 超时时间(可选)
/// - onComplete: 回调 (ok, err)
///
/// 返回 true 表示写入成功(同时 onComplete 也会被触发)
Future<bool> 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 (_) {}
}
}