From 78b8a9bc8678ef4fb382a5867e821b84848a8826 Mon Sep 17 00:00:00 2001 From: Syed Mustafa Ahmed Naqvi Date: Fri, 28 Aug 2026 12:45:18 +0500 Subject: [PATCH] Implement device-to-device template sync service flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the service logic for copying users and face templates from one Hikvision device to another. - Rename DbToDevice sync method to DeviceToDevice - Support source-device face download when source is connected - Fall back to stored DB face templates when source is unavailable - Register transferred users on the target machine - Record Device → Device operation history --- .../Services/TemplateSyncService.cs | 100 +++++++++++------- 1 file changed, 64 insertions(+), 36 deletions(-) diff --git a/src/HikvisionAttendanceManager.App/Services/TemplateSyncService.cs b/src/HikvisionAttendanceManager.App/Services/TemplateSyncService.cs index 9f06c39..8889a47 100644 --- a/src/HikvisionAttendanceManager.App/Services/TemplateSyncService.cs +++ b/src/HikvisionAttendanceManager.App/Services/TemplateSyncService.cs @@ -72,22 +72,22 @@ public sealed class TemplateSyncService( return entry; } - public async Task DbToDeviceAsync(Device source, Device target, IReadOnlyList serialNumbers, - IProgress? progress, CancellationToken cancellationToken) + public async Task DeviceToDeviceAsync(Device source, Device target, IReadOnlyList users, + bool sourceConnected, IProgress? progress, CancellationToken cancellationToken) { - AppLogger.TraceEnter("TEMPLATE_SYNC", nameof(DbToDeviceAsync), - $"source={source.IpAddress} target={target.IpAddress} count={serialNumbers.Count}"); + AppLogger.TraceEnter("TEMPLATE_SYNC", nameof(DeviceToDeviceAsync), + $"source={source.IpAddress} target={target.IpAddress} count={users.Count} sourceConnected={sourceConnected}"); if (string.Equals(source.MachineId, target.MachineId, StringComparison.OrdinalIgnoreCase) && string.Equals(source.IpAddress, target.IpAddress, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Source and target devices must be different."); var entry = new SyncHistoryEntry { - Operation = SyncOperationKind.DbToDevice, + Operation = SyncOperationKind.DeviceToDevice, SourceDevice = source.DisplayName, TargetDevice = target.DisplayName, - Context = $"{serialNumbers.Count} user(s)", - Total = serialNumbers.Count, + Context = $"{users.Count} user(s)", + Total = users.Count, Status = "Running" }; await history.AddAsync(entry, cancellationToken); @@ -96,18 +96,18 @@ public sealed class TemplateSyncService( var faces = 0; var skipped = 0; var failed = 0; + var registered = 0; - for (var index = 0; index < serialNumbers.Count; index++) + for (var index = 0; index < users.Count; index++) { cancellationToken.ThrowIfCancellationRequested(); - var serial = serialNumbers[index]; - progress?.Report(new UserSyncProgress(index, serialNumbers.Count, serial, "Creating user on target…", created, 0, faces, 0, skipped, failed)); + var user = users[index]; + var serial = user.SerialNumber; + var name = user.EmployeeName; + progress?.Report(new UserSyncProgress(index, users.Count, serial, "Creating user on target…", created, 0, faces, 0, skipped, failed)); try { - var dbUsers = await machineUsers.GetActiveByMachineAsync(source.MachineId, cancellationToken); - var dbUser = dbUsers.FirstOrDefault(u => string.Equals(u.SerialNumber, serial, StringComparison.OrdinalIgnoreCase)); - var name = dbUser?.EmployeeName ?? serial; - + var userCreatedNow = false; if (!await hikvision.UserExistsAsync(target, serial, cancellationToken)) { var create = await hikvision.CreateUserAsync(target, serial, name, cancellationToken); @@ -116,49 +116,77 @@ public sealed class TemplateSyncService( failed++; continue; } + created++; + userCreatedNow = true; } - var template = await faceTemplates.TryGetActiveAsync(serial, cancellationToken); - if (template?.Template is null or { Length: 0 }) + byte[]? faceBytes = null; + if (sourceConnected) { - skipped++; - continue; + progress?.Report(new UserSyncProgress(index, users.Count, serial, "Downloading face from source device…", created, 0, faces, 0, skipped, failed)); + var download = await hikvision.DownloadFaceTemplateAsync(source, serial, cancellationToken); + if (download.Success && HikvisionIsapiClient.IsUploadableFaceTemplate(download.Bytes)) + faceBytes = download.Bytes; + } + else + { + var template = await faceTemplates.TryGetActiveAsync(serial, cancellationToken); + if (template?.Template is { Length: > 0 } && HikvisionIsapiClient.IsUploadableFaceTemplate(template.Template)) + faceBytes = template.Template; } - if (!HikvisionIsapiClient.IsUploadableFaceTemplate(template.Template)) + if (faceBytes is null) { - skipped++; - AppLogger.Info($"DB→Device skipped non-JPEG template for {serial}."); - continue; - } - - progress?.Report(new UserSyncProgress(index, serialNumbers.Count, serial, "Uploading face template…", created, 0, faces, 0, skipped, failed)); - var upload = await hikvision.UploadFaceAsync(target, serial, template.Template, cancellationToken); - if (upload.Success) - { - faces++; await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken); + registered++; + skipped++; + AppLogger.Info($"Device→Device skipped face for {serial} (no uploadable template from {(sourceConnected ? "source device" : "DB")})."); + continue; } - else failed++; + + progress?.Report(new UserSyncProgress(index, users.Count, serial, "Uploading face to target…", created, 0, faces, 0, skipped, failed)); + var upload = await hikvision.UploadFaceAsync(target, serial, faceBytes, cancellationToken); + if (!upload.Success) + { + if (userCreatedNow) + await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken); + failed++; + continue; + } + + progress?.Report(new UserSyncProgress(index, users.Count, serial, "Verifying face enrollment…", created, 0, faces, 0, skipped, failed)); + var verify = await hikvision.VerifyFaceAsync(target, serial, cancellationToken); + if (!verify.Success) + { + if (userCreatedNow) + await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken); + failed++; + continue; + } + + faces++; + registered++; + await machineUsers.UpsertAsync(target.MachineId, serial, name, cancellationToken); + await faceTemplates.UpsertAsync(serial, faceBytes, DateTime.Now, true, target.MachineId, target.IpAddress, cancellationToken); } catch (Exception ex) { failed++; - AppLogger.Warning($"DB→Device failed for {serial}: {ex.Message}"); + AppLogger.Warning($"Device→Device failed for {serial}: {ex.Message}"); } } entry.CompletedAt = DateTime.Now; - entry.Success = created; + entry.Success = registered; entry.Failed = failed; entry.Skipped = skipped; - entry.Context = $"{serialNumbers.Count} user(s), {faces} face(s) uploaded"; + entry.Context = $"{users.Count} selected, {registered} registered on target, {faces} face(s) verified"; entry.Status = failed > 0 ? "Completed with errors" : "Completed"; await history.UpdateAsync(entry, cancellationToken); - progress?.Report(new UserSyncProgress(serialNumbers.Count, serialNumbers.Count, "", "Completed", created, 0, faces, 0, skipped, failed)); - AppLogger.TraceExit("TEMPLATE_SYNC", nameof(DbToDeviceAsync), - $"created={created} faces={faces} failed={failed} skipped={skipped}"); + progress?.Report(new UserSyncProgress(users.Count, users.Count, "", "Completed", created, 0, faces, 0, skipped, failed)); + AppLogger.TraceExit("TEMPLATE_SYNC", nameof(DeviceToDeviceAsync), + $"registered={registered} created={created} faces={faces} failed={failed} skipped={skipped}"); return entry; } }