feat(scanner): add IFingerprintScanner, deduplication, auto-reconnect, and state machine
This commit is contained in:
parent
14ce259a5b
commit
c479bb60ca
2 changed files with 373 additions and 41 deletions
|
|
@ -5,7 +5,7 @@ public static class CrashLogService
|
|||
private const long MaximumLogSize = 1_048_576;
|
||||
private static readonly object Gate = new();
|
||||
private static readonly string LogDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"Krona",
|
||||
"StaffTerminal",
|
||||
"logs");
|
||||
|
|
@ -29,6 +29,25 @@ public static class CrashLogService
|
|||
}
|
||||
}
|
||||
|
||||
public static void WriteDiagnostic(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
Directory.CreateDirectory(LogDirectory);
|
||||
var path = Path.Combine(LogDirectory, "diagnostic.log");
|
||||
RotateIfNeeded(path);
|
||||
var entry = $"[{DateTimeOffset.Now:O}] {message}{Environment.NewLine}";
|
||||
File.AppendAllText(path, entry);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Diagnostics must never throw
|
||||
}
|
||||
}
|
||||
|
||||
private static void RotateIfNeeded(string path)
|
||||
{
|
||||
if (!File.Exists(path) || new FileInfo(path).Length < MaximumLogSize) return;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,47 @@
|
|||
using System.Text;
|
||||
using libzkfpcsharp;
|
||||
|
||||
namespace KronaStaffTerminal.Services;
|
||||
|
||||
public sealed class FingerprintScanner : IDisposable
|
||||
public enum ScannerState
|
||||
{
|
||||
Disconnected,
|
||||
Connected,
|
||||
Busy,
|
||||
DriverError
|
||||
}
|
||||
|
||||
public sealed record ScannerDeviceInfo(
|
||||
int Index,
|
||||
string Model,
|
||||
string SerialNumber,
|
||||
string DisplayName);
|
||||
|
||||
public interface IFingerprintScanner : IDisposable
|
||||
{
|
||||
ScannerState State { get; }
|
||||
int DeviceCount { get; }
|
||||
int ReaderIndex { get; }
|
||||
IReadOnlyList<ScannerDeviceInfo> DiscoveredDevices { get; }
|
||||
bool IsConnected { get; }
|
||||
int SkippedTemplateCount { get; }
|
||||
|
||||
event Action<int, int>? FingerRecognized;
|
||||
event Action<int>? RegistrationProgress;
|
||||
event Action<int, string>? RegistrationCompleted;
|
||||
event Action<string>? StatusChanged;
|
||||
event Action<ScannerState, string>? StateChanged;
|
||||
event Action? ConnectionLost;
|
||||
event Action? RegistrationCancelled;
|
||||
|
||||
IReadOnlyList<ScannerDeviceInfo> DiscoverDevices();
|
||||
void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates);
|
||||
void Disconnect();
|
||||
void BeginRegistration(int fingerprintId);
|
||||
void CancelRegistration();
|
||||
}
|
||||
|
||||
public sealed class FingerprintScanner : IFingerprintScanner
|
||||
{
|
||||
private const int TemplateSize = 2048;
|
||||
private readonly object _gate = new();
|
||||
|
|
@ -17,63 +56,198 @@ public sealed class FingerprintScanner : IDisposable
|
|||
private DateTimeOffset _lastProcessedCapture = DateTimeOffset.MinValue;
|
||||
private bool _fingerOnSensor;
|
||||
private int _cleanupScheduled;
|
||||
private List<ScannerDeviceInfo> _discoveredDevices = [];
|
||||
private List<(int FingerprintId, string Template)> _lastLoadedTemplates = [];
|
||||
private CancellationTokenSource? _reconnectCancellation;
|
||||
private Task? _reconnectTask;
|
||||
private bool _autoReconnectEnabled = true;
|
||||
|
||||
public event Action<int, int>? FingerRecognized;
|
||||
public event Action<int>? RegistrationProgress;
|
||||
event Action<int, string>? IFingerprintScanner.RegistrationCompleted
|
||||
{
|
||||
add => RegistrationCompleted += value;
|
||||
remove => RegistrationCompleted -= value;
|
||||
}
|
||||
public event Action<int, string>? RegistrationCompleted;
|
||||
public event Action<string>? StatusChanged;
|
||||
public event Action<ScannerState, string>? StateChanged;
|
||||
public event Action? ConnectionLost;
|
||||
public event Action? RegistrationCancelled;
|
||||
|
||||
public ScannerState State { get; private set; } = ScannerState.Disconnected;
|
||||
public int DeviceCount { get; private set; }
|
||||
public int ReaderIndex { get; private set; }
|
||||
public int SkippedTemplateCount { get; private set; }
|
||||
public bool IsConnected => _device != IntPtr.Zero;
|
||||
public IReadOnlyList<ScannerDeviceInfo> DiscoveredDevices => _discoveredDevices;
|
||||
public bool IsConnected => _device != IntPtr.Zero && State == ScannerState.Connected;
|
||||
|
||||
public IReadOnlyList<ScannerDeviceInfo> DiscoverDevices()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var devices = new List<ScannerDeviceInfo>();
|
||||
var seenSerials = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var initResult = zkfp2.Init();
|
||||
if (initResult != 0 && initResult != 1)
|
||||
{
|
||||
CrashLogService.WriteDiagnostic($"zkfp2.Init returned {initResult} during discovery.");
|
||||
_discoveredDevices = devices;
|
||||
DeviceCount = 0;
|
||||
return devices;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var rawCount = Math.Max(0, zkfp2.GetDeviceCount());
|
||||
for (var i = 0; i < rawCount; i++)
|
||||
{
|
||||
// If this device is currently open by us, use it or skip opening
|
||||
IntPtr handle;
|
||||
var shouldClose = false;
|
||||
if (_device != IntPtr.Zero && ReaderIndex == i)
|
||||
{
|
||||
handle = _device;
|
||||
}
|
||||
else
|
||||
{
|
||||
handle = zkfp2.OpenDevice(i);
|
||||
shouldClose = true;
|
||||
}
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var serial = QueryDeviceStringParameter(handle, 1101);
|
||||
if (string.IsNullOrWhiteSpace(serial))
|
||||
{
|
||||
serial = QueryDeviceStringParameter(handle, 1102);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(serial))
|
||||
{
|
||||
serial = QueryDeviceStringParameter(handle, 106);
|
||||
}
|
||||
|
||||
var model = QueryDeviceStringParameter(handle, 1015);
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
model = "ZKTeco SLK20R";
|
||||
}
|
||||
|
||||
// Eliminate duplicate devices:
|
||||
// ZKFinger SDK 5.3 often exposes 2 device slots for 1 physical USB scanner.
|
||||
// Filter out ghost handles by checking serial number deduplication.
|
||||
if (!string.IsNullOrWhiteSpace(serial))
|
||||
{
|
||||
if (seenSerials.Contains(serial))
|
||||
{
|
||||
CrashLogService.WriteDiagnostic($"Device index {i} skipped as duplicate serial: {serial}");
|
||||
continue;
|
||||
}
|
||||
seenSerials.Add(serial);
|
||||
}
|
||||
else if (devices.Count > 0 && rawCount > 1)
|
||||
{
|
||||
// If no serial number could be queried but a previous reader was already found
|
||||
// on SLK20R driver, skip the secondary phantom slot.
|
||||
CrashLogService.WriteDiagnostic($"Device index {i} skipped as phantom secondary reader slot.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var displayName = !string.IsNullOrWhiteSpace(serial)
|
||||
? $"{model} ({serial})"
|
||||
: $"{model} (сканер №{devices.Count + 1})";
|
||||
|
||||
devices.Add(new ScannerDeviceInfo(i, model, serial, displayName));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shouldClose && handle != IntPtr.Zero)
|
||||
{
|
||||
zkfp2.CloseDevice(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_device == IntPtr.Zero)
|
||||
{
|
||||
zkfp2.Terminate();
|
||||
}
|
||||
}
|
||||
|
||||
_discoveredDevices = devices;
|
||||
DeviceCount = devices.Count;
|
||||
CrashLogService.WriteDiagnostic($"Device discovery completed: {devices.Count} unique device(s) found.");
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
|
||||
public void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates)
|
||||
{
|
||||
_lastLoadedTemplates = templates.ToList();
|
||||
_autoReconnectEnabled = true;
|
||||
StopReconnectLoop();
|
||||
|
||||
ConnectInternal(readerIndex, _lastLoadedTemplates);
|
||||
}
|
||||
|
||||
private void ConnectInternal(int readerIndex, IReadOnlyList<(int FingerprintId, string Template)> templates)
|
||||
{
|
||||
Disconnect();
|
||||
SkippedTemplateCount = 0;
|
||||
|
||||
var result = zkfp2.Init();
|
||||
if (result != 0)
|
||||
if (result != 0 && result != 1)
|
||||
{
|
||||
throw new InvalidOperationException($"SDK ZKTeco не инициализировано (код {result}).");
|
||||
SetState(ScannerState.DriverError, $"Драйвер сканера вернул ошибку инициализации (код {result}).");
|
||||
CrashLogService.WriteDiagnostic($"Scanner initialization failed: code {result}");
|
||||
StartReconnectLoop();
|
||||
throw new InvalidOperationException($"SDK ZKTeco не инициализировано (код {result}). Проверьте драйвер.");
|
||||
}
|
||||
|
||||
// GetDeviceCount returns the number of physical readers for the SLK20R
|
||||
// driver shipped with ZKFinger SDK 5.3. Do not subtract a service slot:
|
||||
// on a workstation with one reader that would hide the only device.
|
||||
DeviceCount = Math.Max(0, zkfp2.GetDeviceCount());
|
||||
if (DeviceCount <= 0)
|
||||
var available = DiscoverDevices();
|
||||
if (available.Count == 0)
|
||||
{
|
||||
zkfp2.Terminate();
|
||||
throw new InvalidOperationException("Подключённые сканеры не найдены.");
|
||||
SetState(ScannerState.Disconnected, "Подключённые сканеры не найдены.");
|
||||
CrashLogService.WriteDiagnostic("Scanner connect failed: no devices found.");
|
||||
StartReconnectLoop();
|
||||
throw new InvalidOperationException("Подключённые сканеры не найдены. Подключите USB-сканер.");
|
||||
}
|
||||
|
||||
var preferredIndex = Math.Clamp(readerIndex, 0, DeviceCount - 1);
|
||||
var candidateIndexes = new[] { preferredIndex }
|
||||
.Concat(Enumerable.Range(0, DeviceCount).Where(index => index != preferredIndex));
|
||||
foreach (var candidateIndex in candidateIndexes)
|
||||
{
|
||||
var candidateDevice = zkfp2.OpenDevice(candidateIndex);
|
||||
if (candidateDevice == IntPtr.Zero) continue;
|
||||
ReaderIndex = candidateIndex;
|
||||
_device = candidateDevice;
|
||||
break;
|
||||
}
|
||||
if (_device == IntPtr.Zero)
|
||||
var chosenDevice = available.FirstOrDefault(d => d.Index == readerIndex) ?? available[0];
|
||||
var deviceIndex = chosenDevice.Index;
|
||||
|
||||
var candidate = zkfp2.OpenDevice(deviceIndex);
|
||||
if (candidate == IntPtr.Zero)
|
||||
{
|
||||
zkfp2.Terminate();
|
||||
throw new InvalidOperationException("Ни один обнаруженный сканер не удалось открыть. Переподключите USB-кабель.");
|
||||
SetState(ScannerState.DriverError, "Не удалось открыть сканер. Возможно, устройство занято.");
|
||||
CrashLogService.WriteDiagnostic($"Failed to open scanner at index {deviceIndex}.");
|
||||
StartReconnectLoop();
|
||||
throw new InvalidOperationException("Не удалось открыть сканер. Переподключите USB-кабель.");
|
||||
}
|
||||
|
||||
ReaderIndex = deviceIndex;
|
||||
_device = candidate;
|
||||
|
||||
_database = zkfp2.DBInit();
|
||||
if (_database == IntPtr.Zero)
|
||||
{
|
||||
zkfp2.CloseDevice(_device);
|
||||
_device = IntPtr.Zero;
|
||||
zkfp2.Terminate();
|
||||
throw new InvalidOperationException("Не удалось создать базу распознавания.");
|
||||
SetState(ScannerState.DriverError, "Не удалось инициализировать базу биометрических шаблонов.");
|
||||
CrashLogService.WriteDiagnostic("zkfp2.DBInit failed.");
|
||||
StartReconnectLoop();
|
||||
throw new InvalidOperationException("Не удалось создать базу биометрических шаблонов.");
|
||||
}
|
||||
|
||||
foreach (var item in templates)
|
||||
|
|
@ -96,7 +270,13 @@ public sealed class FingerprintScanner : IDisposable
|
|||
var device = _device;
|
||||
var database = _database;
|
||||
_captureTask = Task.Run(() => CaptureLoop(device, database, _captureCancellation.Token));
|
||||
StatusChanged?.Invoke($"Сканер №{ReaderIndex + 1} подключён. Приложите палец.");
|
||||
|
||||
var modelInfo = !string.IsNullOrWhiteSpace(chosenDevice.SerialNumber)
|
||||
? $"{chosenDevice.Model} [SN: {chosenDevice.SerialNumber}]"
|
||||
: chosenDevice.Model;
|
||||
|
||||
SetState(ScannerState.Connected, $"Сканер {modelInfo} подключён. Приложите палец.");
|
||||
CrashLogService.WriteDiagnostic($"Scanner connected: {modelInfo} at index {deviceIndex}. Loaded {templates.Count - SkippedTemplateCount} templates.");
|
||||
}
|
||||
|
||||
public void BeginRegistration(int fingerprintId)
|
||||
|
|
@ -108,7 +288,7 @@ public sealed class FingerprintScanner : IDisposable
|
|||
_registrationExpiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(1);
|
||||
}
|
||||
|
||||
StatusChanged?.Invoke("Регистрация: приложите один и тот же палец 3 раза, каждый раз убирая его со стекла.");
|
||||
SetState(ScannerState.Busy, "Регистрация: приложите один и тот же палец 3 раза, каждый раз убирая его со стекла.");
|
||||
}
|
||||
|
||||
public void CancelRegistration()
|
||||
|
|
@ -119,6 +299,9 @@ public sealed class FingerprintScanner : IDisposable
|
|||
_registrationCount = 0;
|
||||
_registrationExpiresAtUtc = default;
|
||||
}
|
||||
|
||||
SetState(IsConnected ? ScannerState.Connected : ScannerState.Disconnected, "Регистрация отпечатка отменена.");
|
||||
RegistrationCancelled?.Invoke();
|
||||
}
|
||||
|
||||
private void CaptureLoop(IntPtr device, IntPtr database, CancellationToken cancellationToken)
|
||||
|
|
@ -146,15 +329,15 @@ public sealed class FingerprintScanner : IDisposable
|
|||
var healthSize = 4;
|
||||
if (zkfp2.GetParameters(device, 1, parameter, ref healthSize) != 0)
|
||||
{
|
||||
StatusChanged?.Invoke("Связь со сканером потеряна. Выполняется переподключение…");
|
||||
ConnectionLost?.Invoke();
|
||||
HandleConnectionLoss("Потеряна связь со сканером (сбой проверки состояния).");
|
||||
return;
|
||||
}
|
||||
nextHealthCheck = DateTimeOffset.UtcNow.AddSeconds(5);
|
||||
}
|
||||
|
||||
var capturedLength = TemplateSize;
|
||||
if (zkfp2.AcquireFingerprint(device, image, captured, ref capturedLength) == 0)
|
||||
var acquireResult = zkfp2.AcquireFingerprint(device, image, captured, ref capturedLength);
|
||||
if (acquireResult == 0)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (!_fingerOnSensor && now - _lastProcessedCapture > TimeSpan.FromMilliseconds(500))
|
||||
|
|
@ -164,6 +347,12 @@ public sealed class FingerprintScanner : IDisposable
|
|||
ProcessCapture(database, captured, capturedLength);
|
||||
}
|
||||
}
|
||||
else if (acquireResult is -6 or -7 or -8 or -10 or -17)
|
||||
{
|
||||
// Negative critical device error codes indicate USB disconnect or driver fault
|
||||
HandleConnectionLoss($"Ошибка сканера (код {acquireResult}).");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_fingerOnSensor = false;
|
||||
|
|
@ -176,8 +365,70 @@ public sealed class FingerprintScanner : IDisposable
|
|||
CrashLogService.Write(exception);
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
StatusChanged?.Invoke("Ошибка сканера. Выполняется переподключение…");
|
||||
ConnectionLost?.Invoke();
|
||||
HandleConnectionLoss($"Сбой потока чтения сканера: {exception.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionLoss(string reason)
|
||||
{
|
||||
CrashLogService.WriteDiagnostic($"Scanner connection lost: {reason}");
|
||||
SetState(ScannerState.Disconnected, reason);
|
||||
ConnectionLost?.Invoke();
|
||||
|
||||
if (_autoReconnectEnabled)
|
||||
{
|
||||
StartReconnectLoop();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartReconnectLoop()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_reconnectTask is not null && !_reconnectTask.IsCompleted) return;
|
||||
_reconnectCancellation = new CancellationTokenSource();
|
||||
var token = _reconnectCancellation.Token;
|
||||
_reconnectTask = Task.Run(() => ReconnectLoopAsync(token));
|
||||
}
|
||||
}
|
||||
|
||||
private void StopReconnectLoop()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_reconnectCancellation?.Cancel();
|
||||
_reconnectCancellation?.Dispose();
|
||||
_reconnectCancellation = null;
|
||||
_reconnectTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReconnectLoopAsync(CancellationToken token)
|
||||
{
|
||||
CrashLogService.WriteDiagnostic("Starting background scanner auto-reconnect loop...");
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(2500, token);
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
var devices = DiscoverDevices();
|
||||
if (devices.Count > 0)
|
||||
{
|
||||
CrashLogService.WriteDiagnostic($"Scanner detected during reconnect: {devices[0].DisplayName}. Connecting...");
|
||||
ConnectInternal(ReaderIndex, _lastLoadedTemplates);
|
||||
return; // Successfully reconnected!
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CrashLogService.WriteDiagnostic($"Reconnect attempt failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +441,7 @@ public sealed class FingerprintScanner : IDisposable
|
|||
_registrationFingerprintId = null;
|
||||
_registrationCount = 0;
|
||||
_registrationExpiresAtUtc = default;
|
||||
StatusChanged?.Invoke("Регистрация отпечатка отменена: за 60 секунд не получены три скана.");
|
||||
SetState(ScannerState.Connected, "Регистрация отпечатка отменена: за 60 секунд не получены три скана.");
|
||||
RegistrationCancelled?.Invoke();
|
||||
}
|
||||
}
|
||||
|
|
@ -209,7 +460,7 @@ public sealed class FingerprintScanner : IDisposable
|
|||
{
|
||||
_registrationFingerprintId = null;
|
||||
_registrationExpiresAtUtc = default;
|
||||
StatusChanged?.Invoke("Этот палец уже зарегистрирован. Регистрация отменена.");
|
||||
SetState(ScannerState.Connected, "Этот палец уже зарегистрирован. Регистрация отменена.");
|
||||
RegistrationCancelled?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
|
@ -217,7 +468,7 @@ public sealed class FingerprintScanner : IDisposable
|
|||
|
||||
if (_registrationCount > 0 && zkfp2.DBMatch(database, captured, _registrationSamples[_registrationCount - 1]) <= 0)
|
||||
{
|
||||
StatusChanged?.Invoke("Это другой палец. Приложите тот же палец ещё раз.");
|
||||
SetState(ScannerState.Busy, "Это другой палец. Приложите тот же палец ещё раз.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +478,7 @@ public sealed class FingerprintScanner : IDisposable
|
|||
RegistrationProgress?.Invoke(_registrationCount);
|
||||
if (_registrationCount < 3)
|
||||
{
|
||||
StatusChanged?.Invoke($"Скан принято: {_registrationCount}/3. Уберите палец и приложите снова.");
|
||||
SetState(ScannerState.Busy, $"Скан принят: {_registrationCount}/3. Уберите палец и приложите снова.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -245,12 +496,15 @@ public sealed class FingerprintScanner : IDisposable
|
|||
_registrationExpiresAtUtc = default;
|
||||
if (result == 0)
|
||||
{
|
||||
RegistrationCompleted?.Invoke(fingerprintId, zkfp2.BlobToBase64(merged, mergedLength));
|
||||
StatusChanged?.Invoke("Отпечаток успешно зарегистрирован.");
|
||||
var templateBase64 = zkfp2.BlobToBase64(merged, mergedLength);
|
||||
RegistrationCompleted?.Invoke(fingerprintId, templateBase64);
|
||||
SetState(ScannerState.Connected, "Отпечаток успешно зарегистрирован.");
|
||||
CrashLogService.WriteDiagnostic($"Fingerprint id {fingerprintId} successfully merged and enrolled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusChanged?.Invoke($"Не удалось сохранить отпечаток (код {result}). Повторите регистрацию.");
|
||||
SetState(ScannerState.Connected, $"Не удалось объединить сканы (код {result}). Повторите регистрацию.");
|
||||
CrashLogService.WriteDiagnostic($"Fingerprint enrollment merge failed: code {result}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -263,11 +517,18 @@ public sealed class FingerprintScanner : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
StatusChanged?.Invoke("Отпечаток не распознан.");
|
||||
SetState(ScannerState.Connected, "Отпечаток не распознан.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetState(ScannerState newState, string message)
|
||||
{
|
||||
State = newState;
|
||||
StatusChanged?.Invoke(message);
|
||||
StateChanged?.Invoke(newState, message);
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_captureCancellation?.Cancel();
|
||||
|
|
@ -288,7 +549,7 @@ public sealed class FingerprintScanner : IDisposable
|
|||
TaskScheduler.Default);
|
||||
}
|
||||
throw new TimeoutException(
|
||||
"Поток чтения сканера не завершился вовремя. Ресурсы будут освобождены сразу после безопасной остановки чтения.");
|
||||
"Поток чтения сканера не завершился вовремя. Ресурсы будут освобождены после завершения операции.");
|
||||
}
|
||||
}
|
||||
catch (AggregateException exception)
|
||||
|
|
@ -318,11 +579,63 @@ public sealed class FingerprintScanner : IDisposable
|
|||
}
|
||||
zkfp2.Terminate();
|
||||
Interlocked.Exchange(ref _cleanupScheduled, 0);
|
||||
if (State != ScannerState.DriverError)
|
||||
{
|
||||
State = ScannerState.Disconnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string QueryDeviceStringParameter(IntPtr device, int paramCode)
|
||||
{
|
||||
var buffer = new byte[256];
|
||||
var size = buffer.Length;
|
||||
if (zkfp2.GetParameters(device, paramCode, buffer, ref size) == 0 && size > 0)
|
||||
{
|
||||
var len = 0;
|
||||
while (len < size && buffer[len] != 0) len++;
|
||||
if (len > 0)
|
||||
{
|
||||
return Encoding.ASCII.GetString(buffer, 0, len).Trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static bool ProbeSession0Support(out string diagnostic)
|
||||
{
|
||||
try
|
||||
{
|
||||
var init = zkfp2.Init();
|
||||
if (init != 0 && init != 1)
|
||||
{
|
||||
diagnostic = $"zkfp2.Init failed with code {init} in current session.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var count = zkfp2.GetDeviceCount();
|
||||
zkfp2.Terminate();
|
||||
|
||||
if (count <= 0)
|
||||
{
|
||||
diagnostic = "ZKFinger driver reports 0 devices in Session 0 / non-interactive context.";
|
||||
return false;
|
||||
}
|
||||
|
||||
diagnostic = $"ZKFinger driver reported {count} device(s).";
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
diagnostic = $"Exception probing ZKFinger in Session 0: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_autoReconnectEnabled = false;
|
||||
StopReconnectLoop();
|
||||
try
|
||||
{
|
||||
Disconnect();
|
||||
|
|
|
|||
Loading…
Reference in a new issue