flutter_integrated_whb/lib/services/nfc_service.dart

458 lines
14 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();
2025-08-27 16:14:50 +08:00
2025-08-25 11:09:23 +08:00
static final NfcService instance = NfcService._internal();
/// 是否正在进行 NFC 会话(扫描或写入)
final ValueNotifier<bool> scanning = ValueNotifier(false);
/// 日志广播流(方便 UI 订阅)
final StreamController<String> _logController = StreamController.broadcast();
2025-08-27 16:14:50 +08:00
2025-08-25 11:09:23 +08:00
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) {
2025-08-27 16:14:50 +08:00
return bytes
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(':')
.toUpperCase();
2025-08-25 11:09:23 +08:00
}
/// 递归在 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);
2025-08-27 16:14:50 +08:00
if (nfcA != null && nfcA.tag.id != null)
return _bytesToHex(Uint8List.fromList(nfcA.tag.id));
2025-08-25 11:09:23 +08:00
} catch (_) {}
} else if (Platform.isIOS) {
try {
final mifare = MiFareIos.from(tag);
2025-08-27 16:14:50 +08:00
if (mifare != null && mifare.identifier != null)
return _bytesToHex(Uint8List.fromList(mifare.identifier));
2025-08-25 11:09:23 +08:00
} 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 {
2025-08-27 16:14:50 +08:00
final records =
(msg is Map && msg['records'] != null)
? List<dynamic>.from(msg['records'])
: (msg is dynamic ? (msg.records as List<dynamic>?) : null);
2025-08-25 11:09:23 +08:00
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'];
2025-08-27 16:14:50 +08:00
if (ptmp is Uint8List)
p = ptmp;
else if (ptmp is List<int>)
p = Uint8List.fromList(ptmp);
2025-08-25 11:09:23 +08:00
} else {
final pr = (r as dynamic).payload;
2025-08-27 16:14:50 +08:00
if (pr is Uint8List)
p = pr;
else if (pr is List<int>)
p = Uint8List.fromList(pr);
2025-08-25 11:09:23 +08:00
}
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({
2025-08-27 16:14:50 +08:00
required void Function(String uid, String parsedText, dynamic rawMessage)
onResult,
2025-08-25 11:09:23 +08:00
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;
2025-08-27 16:14:50 +08:00
if (payload is Uint8List)
parsedText = parseTextFromPayload(payload);
else if (payload is List<int>)
parsedText = parseTextFromPayload(
Uint8List.fromList(payload),
);
2025-08-25 11:09:23 +08:00
}
}
} else {
rawMsg = null;
}
} catch (_) {
// 回退:尝试从 tag map 中嗅探 cachedMessage / records
try {
if (tag is Map) {
2025-08-27 16:14:50 +08:00
rawMsg =
tag['cachedMessage'] ??
tag['ndef']?['cachedMessage'] ??
tag['message'];
2025-08-25 11:09:23 +08:00
if (rawMsg != null) {
2025-08-27 16:14:50 +08:00
final recs =
(rawMsg['records'] ?? (rawMsg as dynamic).records)
as dynamic;
2025-08-25 11:09:23 +08:00
if (recs != null && recs is List && recs.isNotEmpty) {
final r = recs.first;
2025-08-27 16:14:50 +08:00
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),
);
2025-08-25 11:09:23 +08:00
}
}
}
} catch (_) {}
}
// 回调结果
onResult(uid, parsedText, rawMsg);
2025-08-27 16:14:50 +08:00
_logController.add(
'UID: $uid\nmessage: ${_formatMessageToString(rawMsg)}',
);
2025-08-25 11:09:23 +08:00
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文本'
2025-08-27 16:14:50 +08:00
Future<String> readOnceText({
Duration timeout = const Duration(seconds: 10),
}) async {
2025-08-25 11:09:23 +08:00
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 也会被触发)
2025-08-27 16:14:50 +08:00
Future<bool> writeText(
String text, {
Duration? timeout,
void Function(bool ok, Object? err)? onComplete,
}) async {
debugPrint('writeText called - scanning=${scanning.value}');
2025-08-25 11:09:23 +08:00
final available = await isAvailable();
2025-08-27 16:14:50 +08:00
debugPrint('writeText: isAvailable=$available');
2025-08-25 11:09:23 +08:00
if (!available) {
onComplete?.call(false, 'NFC not available');
return false;
}
if (scanning.value) {
onComplete?.call(false, 'Another session running');
return false;
}
2025-08-27 16:14:50 +08:00
try {
await NfcManager.instance.stopSession();
} catch (e) {
debugPrint('stopSession ignore: $e');
}
2025-08-25 11:09:23 +08:00
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 {
2025-08-27 16:14:50 +08:00
// 修改了 pollingOptions 配置
final polling =
Platform.isIOS
? {NfcPollingOption.iso14443} // iOS 使用更具体的配置
: {
NfcPollingOption.iso14443,
NfcPollingOption.iso15693,
NfcPollingOption.iso18092,
};
2025-08-25 11:09:23 +08:00
2025-08-27 16:14:50 +08:00
await NfcManager.instance.startSession(
pollingOptions: polling,
onDiscovered: (dynamic tag) async {
timer?.cancel();
try {
final ndef = Ndef.from(tag);
if (ndef == null) {
onComplete?.call(false, 'Tag not NDEF');
await stopSession();
scanning.value = false;
return;
}
2025-08-25 11:09:23 +08:00
2025-08-27 16:14:50 +08:00
// 检查是否可写
if (Platform.isIOS) {
final miFare = MiFareIos.from(tag);
if (miFare == null) {
onComplete?.call(false, 'Unsupported tag type on iOS');
await stopSession();
scanning.value = false;
return;
2025-08-25 11:09:23 +08:00
}
2025-08-27 16:14:50 +08:00
}
2025-08-25 11:09:23 +08:00
2025-08-27 16:14:50 +08:00
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 返回出去
} catch (e, st) {
debugPrint('iOS NFC Write Error: $e');
debugPrint('Stack trace: $st');
onComplete?.call(false, e);
} finally {
await stopSession();
scanning.value = false;
}
},
);
} catch (e, st) {
debugPrint('startSession exception: $e\n$st');
2025-08-25 11:09:23 +08:00
timer?.cancel();
2025-08-27 16:14:50 +08:00
scanning.value = false;
2025-08-25 11:09:23 +08:00
onComplete?.call(false, e);
}
return success;
}
/// 释放资源
void dispose() {
try {
_logController.close();
} catch (_) {}
}
}