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? _scanCompleter; StreamController? _base64ImageController; StreamSubscription? _imageSubscription; // ── Status stream state ────────────────────────────────────────────────── StreamSubscription? _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? _deviceReadyCompleter; static const int _maxConnectAttempts = 5; static const Duration _attemptTimeout = Duration(seconds: 4); static const Duration _retryDelay = Duration(seconds: 4); static const List _successStatuses = [ FingerStatusType.STARTED_ALREADY, FingerStatusType.STARTED_SUCCESS, ]; static const List _usbConnectionFailureStatuses = [ FingerStatusType.FINGER_USB_PERMISSION_DENIED, FingerStatusType.FINGER_USB_PERMISSION_ERROR, ]; // ── Stream accessor ────────────────────────────────────────────────────── Stream get base64ImageStream => _base64ImageController?.stream ?? const Stream.empty(); ZktFingerPrintScanner(); // ── Status callback ────────────────────────────────────────────────────── void _updateStatus(dynamic value) { try { final statusMap = value as Map; 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 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(); 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 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 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.broadcast(); _scanCompleter = Completer(); 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; } }