krona-staff/apps/KronaStaffTerminal/Services/FingerprintScanner.cs
Ochenstarik efd5bd5c60
Some checks are pending
ci / verify (push) Waiting to run
feat: add SLK20R attendance terminal
2026-09-17 20:51:20 +07:00

196 lines
7.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using libzkfpcsharp;
namespace KronaStaffTerminal.Services;
public sealed class FingerprintScanner : IDisposable
{
private const int TemplateSize = 2048;
private readonly object _gate = new();
private readonly byte[][] _registrationSamples = [new byte[TemplateSize], new byte[TemplateSize], new byte[TemplateSize]];
private IntPtr _device = IntPtr.Zero;
private IntPtr _database = IntPtr.Zero;
private CancellationTokenSource? _captureCancellation;
private int _registrationCount;
private int? _registrationFingerprintId;
private DateTimeOffset _lastCapture = DateTimeOffset.MinValue;
public event Action<int, int>? FingerRecognized;
public event Action<int>? RegistrationProgress;
public event Action<int, string>? RegistrationCompleted;
public event Action<string>? StatusChanged;
public int DeviceCount { get; private set; }
public int ReaderIndex { get; private set; }
public bool IsConnected => _device != IntPtr.Zero;
public void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates)
{
Disconnect();
var result = zkfp2.Init();
if (result != 0)
{
throw new InvalidOperationException($"SDK ZKTeco не инициализировано (код {result}).");
}
DeviceCount = zkfp2.GetDeviceCount();
if (DeviceCount <= 0)
{
zkfp2.Terminate();
throw new InvalidOperationException("Подключённые сканеры не найдены.");
}
ReaderIndex = Math.Clamp(readerIndex, 0, DeviceCount - 1);
_device = zkfp2.OpenDevice(ReaderIndex);
if (_device == IntPtr.Zero)
{
zkfp2.Terminate();
throw new InvalidOperationException($"Не удалось открыть сканер №{ReaderIndex + 1}.");
}
_database = zkfp2.DBInit();
if (_database == IntPtr.Zero)
{
zkfp2.CloseDevice(_device);
_device = IntPtr.Zero;
zkfp2.Terminate();
throw new InvalidOperationException("Не удалось создать базу распознавания.");
}
foreach (var item in templates)
{
var template = zkfp2.Base64ToBlob(item.Template);
zkfp2.DBAdd(_database, item.FingerprintId, template);
}
_captureCancellation = new CancellationTokenSource();
_ = Task.Run(() => CaptureLoop(_captureCancellation.Token));
StatusChanged?.Invoke($"Сканер №{ReaderIndex + 1} подключён. Приложите палец.");
}
public void BeginRegistration(int fingerprintId)
{
lock (_gate)
{
_registrationFingerprintId = fingerprintId;
_registrationCount = 0;
}
StatusChanged?.Invoke("Регистрация: приложите один и тот же палец 3 раза, каждый раз убирая его со стекла.");
}
public void CancelRegistration()
{
lock (_gate)
{
_registrationFingerprintId = null;
_registrationCount = 0;
}
}
private void CaptureLoop(CancellationToken cancellationToken)
{
var parameter = new byte[4];
var size = 4;
var width = 0;
var height = 0;
zkfp2.GetParameters(_device, 1, parameter, ref size);
zkfp2.ByteArray2Int(parameter, ref width);
size = 4;
zkfp2.GetParameters(_device, 2, parameter, ref size);
zkfp2.ByteArray2Int(parameter, ref height);
var image = new byte[Math.Max(1, width * height)];
var captured = new byte[TemplateSize];
while (!cancellationToken.IsCancellationRequested)
{
var capturedLength = TemplateSize;
if (zkfp2.AcquireFingerprint(_device, image, captured, ref capturedLength) == 0)
{
if (DateTimeOffset.UtcNow - _lastCapture > TimeSpan.FromMilliseconds(900))
{
_lastCapture = DateTimeOffset.UtcNow;
ProcessCapture(captured, capturedLength);
}
}
Thread.Sleep(150);
}
}
private void ProcessCapture(byte[] captured, int capturedLength)
{
lock (_gate)
{
if (_registrationFingerprintId is int fingerprintId)
{
if (_registrationCount > 0 && zkfp2.DBMatch(_database, captured, _registrationSamples[_registrationCount - 1]) <= 0)
{
StatusChanged?.Invoke("Это другой палец. Приложите тот же палец ещё раз.");
return;
}
Array.Clear(_registrationSamples[_registrationCount]);
Array.Copy(captured, _registrationSamples[_registrationCount], capturedLength);
_registrationCount++;
RegistrationProgress?.Invoke(_registrationCount);
if (_registrationCount < 3)
{
StatusChanged?.Invoke($"Скан принято: {_registrationCount}/3. Уберите палец и приложите снова.");
return;
}
var merged = new byte[TemplateSize];
var mergedLength = TemplateSize;
var result = zkfp2.DBMerge(_database, _registrationSamples[0], _registrationSamples[1], _registrationSamples[2], merged, ref mergedLength);
if (result == 0)
{
zkfp2.DBDel(_database, fingerprintId);
result = zkfp2.DBAdd(_database, fingerprintId, merged);
}
_registrationFingerprintId = null;
_registrationCount = 0;
if (result == 0)
{
RegistrationCompleted?.Invoke(fingerprintId, zkfp2.BlobToBase64(merged, mergedLength));
StatusChanged?.Invoke("Отпечаток успешно зарегистрирован.");
}
else
{
StatusChanged?.Invoke($"Не удалось сохранить отпечаток (код {result}). Повторите регистрацию.");
}
return;
}
var foundId = 0;
var score = 0;
if (zkfp2.DBIdentify(_database, captured, ref foundId, ref score) == 0)
{
FingerRecognized?.Invoke(foundId, score);
}
else
{
StatusChanged?.Invoke("Отпечаток не распознан.");
}
}
}
public void Disconnect()
{
_captureCancellation?.Cancel();
_captureCancellation = null;
Thread.Sleep(200);
if (_database != IntPtr.Zero)
{
zkfp2.DBFree(_database);
_database = IntPtr.Zero;
}
if (_device != IntPtr.Zero)
{
zkfp2.CloseDevice(_device);
_device = IntPtr.Zero;
}
zkfp2.Terminate();
}
public void Dispose() => Disconnect();
}