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