flutter_integrated_whb/lib/services/nfc_service.dart

394 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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 (_) {}
}
}