You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
9.6 KiB
Dart

import 'dart:convert';
import 'dart:async';
import 'dart:typed_data';
import 'package:zkfinger10/finger_status.dart';
import 'package:zkfinger10/finger_status_type.dart';
import 'package:zkfinger10/zk_finger.dart';
import 'finger_print_manager.dart';
class ZktFingerPrintScanner implements FingerprintManager {
bool _isConnected = false;
// ── Scan state (fresh per scan call) ────────────────────────────────────
Completer<String>? _scanCompleter;
StreamController<String>? _base64ImageController;
StreamSubscription<dynamic>? _imageSubscription;
// ── Status stream state ──────────────────────────────────────────────────
StreamSubscription<dynamic>? _statusSubscription;
FingerStatus? lastStatus;
FingerStatusType? _tempStatusType;
/// Set during connect(); completed by _updateStatus when a success status
/// arrives. Reset at the start of every retry attempt.
Completer<void>? _deviceReadyCompleter;
static const int _maxConnectAttempts = 5;
static const Duration _attemptTimeout = Duration(seconds: 4);
static const Duration _retryDelay = Duration(seconds: 4);
static const List<FingerStatusType> _successStatuses = [
FingerStatusType.STARTED_ALREADY,
FingerStatusType.STARTED_SUCCESS,
];
static const List<FingerStatusType> _usbConnectionFailureStatuses = [
FingerStatusType.FINGER_USB_PERMISSION_DENIED,
FingerStatusType.FINGER_USB_PERMISSION_ERROR,
];
// ── Stream accessor ──────────────────────────────────────────────────────
Stream<String> get base64ImageStream =>
_base64ImageController?.stream ?? const Stream.empty();
ZktFingerPrintScanner();
// ── Status callback ──────────────────────────────────────────────────────
void _updateStatus(dynamic value) {
try {
final statusMap = value as Map<dynamic, dynamic>;
final statusType =
FingerStatusType.values[statusMap['fingerStatus'] as int];
lastStatus = FingerStatus(
statusMap['message'],
statusType,
statusMap['id'],
statusMap['data'],
);
if (statusType == _tempStatusType &&
_tempStatusType == FingerStatusType.CAPTURE_ERROR) {
// Ignore repeated CAPTURE_ERROR — device is stuck, do nothing
} else {
_tempStatusType = statusType;
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted &&
_usbConnectionFailureStatuses.contains(statusType) ){
_deviceReadyCompleter!.completeError(Exception(
'Device not connected'
));
}
// If we are in the connect-retry loop and got a success status, resolve
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted &&
lastStatus != null &&
_successStatuses.contains(statusType) && _successStatuses.contains(lastStatus!.statusType)) {
_deviceReadyCompleter!.complete();
}
}
} catch (e) {
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted) {
_deviceReadyCompleter!.completeError(e);
}
}
}
// ── Image callback ───────────────────────────────────────────────────────
void _onNativeImage(dynamic imageBytes) {
try {
final base64Image = uint8ListTob64(imageBytes as Uint8List);
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
_base64ImageController!.add(base64Image);
}
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!.complete(base64Image);
}
} catch (e) {
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
_base64ImageController!.addError(e);
}
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!.completeError(e);
}
}
}
// ── Public API ───────────────────────────────────────────────────────────
@override
Future<void> connect() async {
try {
if (_isConnected) await disconnect();
final result = await ZkFinger.openConnection();
if (result != true) {
throw Exception('Failed to initialize fingerprint device');
}
// ── Start status stream ──────────────────────────────────────────────
await _statusSubscription?.cancel();
_statusSubscription =
ZkFinger.statusChangeStream.receiveBroadcastStream().listen(
_updateStatus,
onError: (e) {
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted) {
_deviceReadyCompleter!.completeError(e);
}
},
);
// ── Retry loop: up to 5 attempts, 5 s window each ───────────────────
bool deviceReady = false;
for (int attempt = 1; attempt <= _maxConnectAttempts; attempt++) {
_deviceReadyCompleter = Completer<void>();
final result = await ZkFinger.openConnection();
if (result != true) {
throw Exception('Failed to initialize fingerprint device');
}
try {
await _deviceReadyCompleter!.future.timeout(_attemptTimeout);
// Success status received within the window
deviceReady = true;
break;
} on TimeoutException {
if (attempt < _maxConnectAttempts) {
await Future.delayed(_retryDelay);
}
}
}
if (!deviceReady) {
// Clean up status subscription and close hardware
await _statusSubscription?.cancel();
_statusSubscription = null;
try { await ZkFinger.closeConnection(); } catch (_) {}
throw Exception(
'Device not ready after $_maxConnectAttempts attempts. '
'Last status: ${lastStatus?.message ?? "no status received"}',
);
}
_isConnected = true;
// ── Subscribe to image stream now that device is confirmed ready ─────
_imageSubscription =
ZkFinger.imageStream.receiveBroadcastStream().listen(
_onNativeImage,
onError: (e) {
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!.completeError(e);
}
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
_base64ImageController!.addError(e);
}
},
);
} catch (e) {
_isConnected = false;
rethrow; // preserve original message
}
}
@override
Future<void> disconnect() async {
try { await ZkFinger.closeConnection(); } catch (_) {}
try { await _imageSubscription?.cancel(); } catch (_) {}
_imageSubscription = null;
try { await _statusSubscription?.cancel(); } catch (_) {}
_statusSubscription = null;
if (_deviceReadyCompleter != null &&
!_deviceReadyCompleter!.isCompleted) {
_deviceReadyCompleter!.completeError(StateError('Disconnected'));
}
_deviceReadyCompleter = null;
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!.completeError(StateError('Disconnected'));
}
_scanCompleter = null;
try {
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
await _base64ImageController!.close();
}
} catch (_) {}
_base64ImageController = null;
_isConnected = false;
_tempStatusType = null;
}
/// Returns a base64-encoded fingerprint for a single scan.
/// Creates a fresh [Completer] + [StreamController] each call so
/// consecutive scans always work.
@override
Future<String?> scan(
{Duration timeout = const Duration(seconds: 20)}) async {
try {
if (!_isConnected) await connect();
try {
if (_base64ImageController != null &&
!_base64ImageController!.isClosed) {
await _base64ImageController!.close();
}
} catch (_) {}
_base64ImageController = StreamController<String>.broadcast();
_scanCompleter = Completer<String>();
final timer = Timer(timeout, () {
if (_scanCompleter != null && !_scanCompleter!.isCompleted) {
_scanCompleter!
.completeError(TimeoutException('Scan timed out', timeout));
}
});
try {
return await _scanCompleter!.future;
} finally {
timer.cancel();
}
} catch (e) {
throw Exception('Fingerprint scan failed: ${e.toString()}');
}
}
// ── Helpers ──────────────────────────────────────────────────────────────
String uint8ListTob64(Uint8List uint8list) {
return 'data:image/png;base64,${base64Encode(uint8list)}';
}
bool get isConnected => _isConnected;
void dispose() {
_imageSubscription?.cancel();
_statusSubscription?.cancel();
_scanCompleter = null;
_deviceReadyCompleter = null;
_base64ImageController?.close();
_base64ImageController = null;
}
}