Initial Commit

main
SYED MUSTUFA AHMED NAQVI 2026-04-06 17:19:57 +05:00
commit 2455f71296
31 changed files with 6032 additions and 0 deletions

65
.gitignore vendored Normal file
View File

@ -0,0 +1,65 @@
# Build folders
bin/
obj/
Debug/
Release/
x64/
x86/
AnyCPU/
# Visual Studio
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
# Rider / JetBrains
.idea/
*.iml
# NuGet
packages/
*.nupkg
.nuget/
# Logs
*.log
logs/
SdkLog/
attendance_logs_*.txt
face_templates_log.txt
fingerprint_templates_log.txt
attendance_records.txt
# Local config / secrets
appsettings.Development.json
appsettings.Local.json
serviceconfig.Local.json
*.secret
*.env
serviceconfig.json
appsettings.json
# Publish output
publish/
out/
# Test / temp files
*.tmp
*.temp
*.bak
*.old
# OS files
.DS_Store
Thumbs.db
# If SDK DLLs are local/generated and should not be committed
# Uncomment only if you do NOT want them in repo
# HCNetSDK/
# *.dll
# Keep sample configs if needed
# !serviceconfig.json
# !appsettings.json

175
AcsAttendanceParser.cs Normal file
View File

@ -0,0 +1,175 @@
using System;
using CHCNetSDK = EventByDeploy.CHCNetSDK;
namespace HikvisionAttendanceService;
/// <summary>
/// ACS-specific parsing helpers (major/minor codes from HCNetSDK).
/// </summary>
internal static class AcsAttendanceParser
{
public static string MapMajorCategory(uint dwMajor)
{
if (dwMajor == CHCNetSDK.MAJOR_ALARM) return "Alarm";
if (dwMajor == CHCNetSDK.MAJOR_EXCEPTION) return "Exception";
if (dwMajor == CHCNetSDK.MAJOR_OPERATION) return "Operation";
if (dwMajor == CHCNetSDK.MAJOR_EVENT) return "Event";
return "Unknown";
}
public static bool TryInferSuccessFromMinor(uint dwMajor, uint dwMinor, out bool isSuccess)
{
isSuccess = false;
if (dwMajor != CHCNetSDK.MAJOR_EVENT)
return false;
// Verification / access style minors: *_PASS / *_FAIL / *_TIMEOUT patterns in SDK.
if (IsPassMinor(dwMinor))
{
isSuccess = true;
return true;
}
if (IsFailOrTimeoutMinor(dwMinor))
{
isSuccess = false;
return true;
}
return false;
}
public static bool ResolveIsSuccess(uint dwMajor, uint dwMinor, string? eventName)
{
if (TryInferSuccessFromMinor(dwMajor, dwMinor, out var ok))
return ok;
return GuessSuccessFromEventName(eventName);
}
private static bool GuessSuccessFromEventName(string? eventName)
{
if (eventName == null)
return false;
if (eventName.IndexOf("FAIL", StringComparison.OrdinalIgnoreCase) >= 0 ||
eventName.IndexOf("TIMEOUT", StringComparison.OrdinalIgnoreCase) >= 0 ||
eventName.IndexOf("INEXISTENCE", StringComparison.OrdinalIgnoreCase) >= 0)
{
return false;
}
if (eventName.IndexOf("PASS", StringComparison.OrdinalIgnoreCase) >= 0 ||
eventName.IndexOf("SUCCESS", StringComparison.OrdinalIgnoreCase) >= 0 ||
eventName.IndexOf("OPEN", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
return false;
}
private static bool IsPassMinor(uint m)
{
return m == CHCNetSDK.MINOR_LEGAL_CARD_PASS
|| m == CHCNetSDK.MINOR_FACE_VERIFY_PASS
|| m == CHCNetSDK.MINOR_FINGERPRINT_COMPARE_PASS
|| m == CHCNetSDK.MINOR_CARD_AND_PSW_PASS
|| m == CHCNetSDK.MINOR_CARD_FINGERPRINT_VERIFY_PASS
|| m == CHCNetSDK.MINOR_CARD_FINGERPRINT_PASSWD_VERIFY_PASS
|| m == CHCNetSDK.MINOR_FINGERPRINT_PASSWD_VERIFY_PASS
|| m == CHCNetSDK.MINOR_FACE_AND_FP_VERIFY_PASS
|| m == CHCNetSDK.MINOR_FACE_AND_PW_VERIFY_PASS
|| m == CHCNetSDK.MINOR_FACE_AND_CARD_VERIFY_PASS
|| m == CHCNetSDK.MINOR_FACE_AND_PW_AND_FP_VERIFY_PASS
|| m == CHCNetSDK.MINOR_FACE_CARD_AND_FP_VERIFY_PASS
|| m == CHCNetSDK.MINOR_EMPLOYEENO_AND_FP_VERIFY_PASS
|| m == CHCNetSDK.MINOR_EMPLOYEENO_AND_FP_AND_PW_VERIFY_PASS
|| m == CHCNetSDK.MINOR_EMPLOYEENO_AND_FACE_VERIFY_PASS
|| m == CHCNetSDK.MINOR_COMBINED_VERIFY_PASS;
}
private static bool IsFailOrTimeoutMinor(uint m)
{
return m == CHCNetSDK.MINOR_FINGERPRINT_COMPARE_FAIL
|| m == CHCNetSDK.MINOR_CARD_FINGERPRINT_VERIFY_FAIL
|| m == CHCNetSDK.MINOR_CARD_FINGERPRINT_VERIFY_TIMEOUT
|| m == CHCNetSDK.MINOR_FACE_AND_FP_VERIFY_FAIL
|| m == CHCNetSDK.MINOR_FACE_AND_FP_VERIFY_TIMEOUT
|| m == CHCNetSDK.MINOR_FACE_VERIFY_FAIL
|| m == CHCNetSDK.MINOR_FACE_AND_PW_VERIFY_FAIL
|| m == CHCNetSDK.MINOR_FACE_AND_CARD_VERIFY_FAIL
|| m == CHCNetSDK.MINOR_FINGERPRINT_INEXISTENCE;
}
/// <summary>
/// Maps SDK verify mode / card reader kind to a coarse attendance method.
/// </summary>
public static string InferMethodFromAcsDetail(
uint dwMinor,
byte byCardReaderKind,
byte byCurrentVerifyMode,
string eventNameFallback)
{
// Prefer explicit minor-driven inference for MAJOR_EVENT.
string fromMinor = MapMethodFromMinor(dwMinor);
if (!string.IsNullOrEmpty(fromMinor))
return fromMinor;
// byCardReaderKind: 1 IC, 2 ID, 3 QR, 4 fingerprint head (per SDK comments).
switch (byCardReaderKind)
{
case 4:
return "Fingerprint";
case 1:
case 2:
case 3:
return "Card";
}
// Common verify modes (device-dependent; 0 = unknown).
switch (byCurrentVerifyMode)
{
case 2:
return "Fingerprint";
case 3:
case 4:
return "Face";
case 1:
return "Card";
}
return GuessMethodFromEventName(eventNameFallback);
}
private static string MapMethodFromMinor(uint m)
{
if (m == CHCNetSDK.MINOR_FACE_VERIFY_PASS || m == CHCNetSDK.MINOR_FACE_VERIFY_FAIL)
return "Face";
if (m == CHCNetSDK.MINOR_FINGERPRINT_COMPARE_PASS || m == CHCNetSDK.MINOR_FINGERPRINT_COMPARE_FAIL)
return "Fingerprint";
if (m == CHCNetSDK.MINOR_CARD_AND_PSW_PASS)
return "Card+Password";
if (m == CHCNetSDK.MINOR_FACE_AND_FP_VERIFY_PASS || m == CHCNetSDK.MINOR_FACE_AND_FP_VERIFY_FAIL)
return "Face+Fingerprint";
if (m == CHCNetSDK.MINOR_FACE_AND_CARD_VERIFY_PASS || m == CHCNetSDK.MINOR_FACE_AND_CARD_VERIFY_FAIL)
return "Face+Card";
return "";
}
private static string GuessMethodFromEventName(string? eventName)
{
if (string.IsNullOrEmpty(eventName))
return "Unknown";
string n = eventName.ToUpperInvariant();
if (n.Contains("FACE") && n.Contains("FP"))
return "Face+Fingerprint";
if (n.Contains("FACE"))
return "Face";
if (n.Contains("FINGERPRINT") || n.Contains("FINGE_RPRINT") || n.Contains("FINGER_PRINT") || n.Contains(" FP"))
return "Fingerprint";
if (n.Contains("CARD"))
return "Card";
return "Unknown";
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace HikvisionAttendanceService;
internal sealed class AllUsersTemplateExportPayload
{
public string exportedAtUtc { get; set; } = "";
public string deviceId { get; set; } = "";
public string userListSource { get; set; } = "";
public string userListCardKey { get; set; } = "employeeNo";
public string listFetchError { get; set; } = "";
public List<UserTemplateExportPayload> users { get; set; } = new List<UserTemplateExportPayload>();
}

445
AttendanceTestMode.cs Normal file
View File

@ -0,0 +1,445 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace HikvisionAttendanceService;
/// <summary>
/// Console test harness: run the same SDK + pipeline as the Windows Service without installing the service.
/// Usage: HikvisionAttendanceService.exe --test
/// HikvisionAttendanceService.exe --test --fetch "DS-K1T642MFW-7378360" --from "2026-03-01 08:00:00" --to "2026-03-28 23:59:59"
/// HikvisionAttendanceService.exe --test --export-templates "DS-K1T642MFW-FM7378360" --card "1001" [--out "C:\SdkLog\templates.json"]
/// HikvisionAttendanceService.exe --test --export-all-templates-isapi "DS-K1T642MFW-FM7378360" [--outDir "C:\SdkLog\templates_isapi"] [--pageSize 30] [--maxUsers 5000]
/// </summary>
internal static class AttendanceTestMode
{
private const string BuildMarker = "CFGDIAG_20260331_1";
public static async Task RunAsync(string[] args)
{
Environment.CurrentDirectory = AppContext.BaseDirectory;
var baseDir = AppContext.BaseDirectory;
var serviceCfgPath = Path.Combine(baseDir, "serviceconfig.json");
var appSettingsPath = Path.Combine(baseDir, "appsettings.json");
var chosenConfigPath = File.Exists(serviceCfgPath) ? serviceCfgPath : appSettingsPath;
// Startup diagnostics: prove which config file is used and what devices it contains.
// (Logger needs LogDirectory, so we bootstrap a temporary logger first.)
var bootstrapLogger = new HikvisionAttendanceWindowsService.FileLogger(@"C:\SdkLog");
bootstrapLogger.Info("TEST config: baseDir=\"" + baseDir + "\"");
bootstrapLogger.Info("TEST config: candidate serviceconfig.json=\"" + serviceCfgPath + "\" exists=" + File.Exists(serviceCfgPath));
bootstrapLogger.Info("TEST config: candidate appsettings.json=\"" + appSettingsPath + "\" exists=" + File.Exists(appSettingsPath));
bootstrapLogger.Info("TEST config: chosenConfigPath=\"" + chosenConfigPath + "\" exists=" + File.Exists(chosenConfigPath));
var config = HikvisionAttendanceWindowsService.HikvisionServiceConfig.Load(chosenConfigPath);
var logger = new HikvisionAttendanceWindowsService.FileLogger(config.LogDirectory);
logger.Info("TEST config: loadedFromPath=\"" + chosenConfigPath + "\" exists=" + File.Exists(chosenConfigPath));
logger.Info("TEST config: devicesLoadedCount=" + (config.Devices == null ? "(null)" : config.Devices.Count.ToString()));
if (config.Devices == null || config.Devices.Count == 0)
{
logger.Warn("TEST config: Devices is null/empty; StartDevices will skip all logins.");
}
else
{
foreach (var d in config.Devices)
{
logger.Info("TEST config: device entry: DeviceId=\"" + (d.DeviceId ?? "") + "\" Ip=\"" + (d.Ip ?? "") + "\" Port=" + d.Port +
" Username=\"" + (d.Username ?? "") + "\"");
}
}
var rawJoined = args.Length == 0 ? "(none)" : string.Join(" ", args.Select(QuoteForLog));
logger.Info("TEST mode entry: build=" + BuildMarker +
" baseDir=\"" + baseDir + "\"" +
" chosenConfigPath=\"" + chosenConfigPath + "\" chosenExists=" + File.Exists(chosenConfigPath) +
" rawArgCount=" + args.Length + " rawArgs=" + rawJoined);
var fetchWanted = args.Any(a => string.Equals(a, "--fetch", StringComparison.OrdinalIgnoreCase));
var exportAllWanted = args.Any(a => string.Equals(a, "--export-all-templates", StringComparison.OrdinalIgnoreCase));
var exportAllIsapiWanted = args.Any(a => string.Equals(a, "--export-all-templates-isapi", StringComparison.OrdinalIgnoreCase));
var exportTemplatesWanted = args.Any(a => string.Equals(a, "--export-templates", StringComparison.OrdinalIgnoreCase));
var exportParseOk = TryParseExportTemplateArgs(args, out var exportDeviceId, out var exportCardNo, out var exportOutPath, out var exportParseFailure);
var exportAllParseOk = TryParseExportAllTemplatesArgs(args, out var exportAllDeviceId, out var exportAllOutDir, out var exportAllPageSize, out var exportAllMaxUsers, out var exportAllParseFailure);
var exportAllIsapiParseOk = TryParseExportAllIsapiTemplatesArgs(args, out var exportAllIsapiDeviceId, out var exportAllIsapiOutDir, out var exportAllIsapiPageSize, out var exportAllIsapiMaxUsers, out var exportAllIsapiParseFailure);
var parsedOk = TryParseFetchArgs(args, out var deviceId, out var from, out var to, out var parseFailure);
logger.Info("TEST mode parse: exportTemplatesFlagPresent=" + exportTemplatesWanted + ", exportBranchWillRun=" + exportParseOk +
", exportDeviceId=" + (string.IsNullOrEmpty(exportDeviceId) ? "(empty)" : exportDeviceId) +
", exportCardNo=" + (string.IsNullOrEmpty(exportCardNo) ? "(empty)" : exportCardNo) +
", exportOutPath=" + (string.IsNullOrEmpty(exportOutPath) ? "(default under LogDirectory)" : exportOutPath) +
(exportParseOk ? "" : ", exportParseFailed=" + exportParseFailure));
logger.Info("TEST mode parse: exportAllFlagPresent=" + exportAllWanted + ", exportAllBranchWillRun=" + exportAllParseOk +
", exportAllDeviceId=" + (string.IsNullOrEmpty(exportAllDeviceId) ? "(empty)" : exportAllDeviceId) +
", exportAllOutDir=" + (string.IsNullOrEmpty(exportAllOutDir) ? "(default under LogDirectory)" : exportAllOutDir) +
", pageSize=" + exportAllPageSize +
", maxUsers=" + exportAllMaxUsers +
(exportAllParseOk ? "" : ", exportAllParseFailed=" + exportAllParseFailure));
logger.Info("TEST mode parse: exportAllIsapiFlagPresent=" + exportAllIsapiWanted + ", exportAllIsapiBranchWillRun=" + exportAllIsapiParseOk +
", exportAllIsapiDeviceId=" + (string.IsNullOrEmpty(exportAllIsapiDeviceId) ? "(empty)" : exportAllIsapiDeviceId) +
", exportAllIsapiOutDir=" + (string.IsNullOrEmpty(exportAllIsapiOutDir) ? "(default under LogDirectory)" : exportAllIsapiOutDir) +
", pageSize=" + exportAllIsapiPageSize +
", maxUsers=" + exportAllIsapiMaxUsers +
(exportAllIsapiParseOk ? "" : ", exportAllIsapiParseFailed=" + exportAllIsapiParseFailure));
logger.Info("TEST mode parse: fetchFlagPresent=" + fetchWanted + ", fetchBranchWillRun=" + parsedOk +
", parsedDeviceId=" + (string.IsNullOrEmpty(deviceId) ? "(empty)" : deviceId) +
", parsedFrom=" + (from == default ? "(unset)" : from.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)) +
", parsedTo=" + (to == default ? "(unset)" : to.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)) +
(parsedOk ? "" : ", fetchParseFailed=" + parseFailure));
Console.WriteLine("HikvisionAttendanceService TEST mode");
Console.WriteLine("Log directory: " + config.LogDirectory);
Console.WriteLine("Press Ctrl+C to exit.");
if (exportTemplatesWanted && !exportParseOk)
Console.WriteLine("Template export not started: " + exportParseFailure);
if (exportAllWanted && !exportAllParseOk)
Console.WriteLine("All template export not started: " + exportAllParseFailure);
if (exportAllIsapiWanted && !exportAllIsapiParseOk)
Console.WriteLine("All ISAPI template export not started: " + exportAllIsapiParseFailure);
if (!parsedOk && fetchWanted)
Console.WriteLine("Fetch not started: " + parseFailure);
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
cts.Cancel();
};
using var manager = new HikvisionAttendanceManager(config, logger);
var run = manager.RunAsync(cts.Token);
await Task.Delay(3500, CancellationToken.None).ConfigureAwait(false);
if (exportAllIsapiParseOk)
{
logger.Info("TEST mode: entering export-all templates ISAPI branch deviceId=" + exportAllIsapiDeviceId +
", outDir=" + (string.IsNullOrEmpty(exportAllIsapiOutDir) ? "(default LogDirectory)" : exportAllIsapiOutDir) +
", pageSize=" + exportAllIsapiPageSize +
", maxUsers=" + exportAllIsapiMaxUsers);
Console.WriteLine($"Exporting ALL face + fingerprint templates via ISAPI: device={exportAllIsapiDeviceId}, pageSize={exportAllIsapiPageSize}, maxUsers={exportAllIsapiMaxUsers}");
string jsonPath = "";
string exportErr = "";
bool exportOk = await Task.Run(() =>
manager.TryExportAllUsersTemplatesToIsapiFile(
exportAllIsapiDeviceId,
string.IsNullOrEmpty(exportAllIsapiOutDir) ? null : exportAllIsapiOutDir,
exportAllIsapiPageSize,
exportAllIsapiMaxUsers,
cts.Token,
out jsonPath,
out exportErr), cts.Token).ConfigureAwait(false);
if (exportOk)
{
logger.Info("TEST mode: export-all (ISAPI) templates finished OK jsonPath=" + jsonPath);
Console.WriteLine("ISAPI export-all templates written: " + jsonPath);
}
else
{
logger.Warn("TEST mode: export-all (ISAPI) templates failed: " + exportErr);
Console.WriteLine("ISAPI export-all templates failed: " + exportErr);
}
Console.WriteLine("Stopping test host...");
cts.Cancel();
}
else if (exportAllParseOk)
{
logger.Info("TEST mode: entering export-all templates branch deviceId=" + exportAllDeviceId +
", outDir=" + (string.IsNullOrEmpty(exportAllOutDir) ? "(default LogDirectory)" : exportAllOutDir) +
", pageSize=" + exportAllPageSize +
", maxUsers=" + exportAllMaxUsers);
Console.WriteLine($"Exporting ALL face + fingerprint templates: device={exportAllDeviceId}, pageSize={exportAllPageSize}, maxUsers={exportAllMaxUsers}");
string jsonPath = "";
string exportErr = "";
bool exportOk = await Task.Run(() =>
manager.TryExportAllUsersTemplatesToFile(
exportAllDeviceId,
string.IsNullOrEmpty(exportAllOutDir) ? null : exportAllOutDir,
exportAllPageSize,
exportAllMaxUsers,
cts.Token,
out jsonPath,
out exportErr), cts.Token).ConfigureAwait(false);
if (exportOk)
{
logger.Info("TEST mode: export-all templates finished OK jsonPath=" + jsonPath);
Console.WriteLine("All templates export written: " + jsonPath);
}
else
{
logger.Warn("TEST mode: export-all templates failed: " + exportErr);
Console.WriteLine("Export-all templates failed: " + exportErr);
}
Console.WriteLine("Stopping test host...");
cts.Cancel();
}
else if (exportParseOk)
{
logger.Info("TEST mode: entering template export branch deviceId=" + exportDeviceId + " cardNo=" + exportCardNo +
(string.IsNullOrEmpty(exportOutPath) ? "" : " out=" + exportOutPath));
Console.WriteLine($"Exporting face + fingerprint templates: device={exportDeviceId}, card={exportCardNo}");
string writtenPath = "";
string exportErr = "";
bool exportOk = await Task.Run(() =>
manager.TryExportUserTemplatesToFile(exportDeviceId, exportCardNo,
string.IsNullOrEmpty(exportOutPath) ? null : exportOutPath, out writtenPath, out exportErr), cts.Token).ConfigureAwait(false);
if (exportOk)
{
logger.Info("TEST mode: template export finished OK path=" + writtenPath);
Console.WriteLine("Template export written: " + writtenPath);
}
else
{
logger.Warn("TEST mode: template export failed: " + exportErr);
Console.WriteLine("Template export failed: " + exportErr);
}
Console.WriteLine("Stopping test host...");
cts.Cancel();
}
else if (parsedOk)
{
logger.Info("TEST mode: entering CLI fetch branch for deviceId=" + deviceId +
" from=" + from.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) +
" to=" + to.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
Console.WriteLine($"Fetching historical ACS events: device={deviceId}, from={from}, to={to}");
int n = await manager.FetchAttendanceRecordsAsync(deviceId, from, to, cts.Token).ConfigureAwait(false);
logger.Info("TEST mode: CLI fetch finished, recordsParsedOk=" + n);
Console.WriteLine("Fetch completed, records enqueued: " + n);
Console.WriteLine("Stopping test host...");
cts.Cancel();
}
else
{
logger.Info("TEST mode: live-only branch (no CLI fetch). " + (fetchWanted ? parseFailure : "Reason: " + parseFailure));
Console.WriteLine("Live ACS + scheduled historical fetch (if enabled) running. Tail today's attendance log for details.");
Console.WriteLine("Optional: --test --fetch DeviceId --from yyyy-MM-dd HH:mm:ss --to yyyy-MM-dd HH:mm:ss");
Console.WriteLine("Optional: --test --export-templates DeviceId --card CardOrEmployeeNo [--out path.json]");
Console.WriteLine("Optional: --test --export-all-templates DeviceId [--outDir path] [--pageSize 30] [--maxUsers 5000]");
Console.WriteLine("Optional: --test --export-all-templates-isapi DeviceId [--outDir path] [--pageSize 30] [--maxUsers 5000]");
Console.WriteLine("Waiting... (Ctrl+C to stop)");
}
try
{
await run.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// expected
}
}
private static string QuoteForLog(string a)
{
if (string.IsNullOrEmpty(a))
return a;
return a.Any(char.IsWhiteSpace) ? "\"" + a.Replace("\"", "\\\"") + "\"" : a;
}
private static bool TryParseExportTemplateArgs(string[] args, out string deviceId, out string cardNo, out string outPath, out string failureReason)
{
deviceId = "";
cardNo = "";
outPath = "";
failureReason = "";
if (!args.Any(a => string.Equals(a, "--export-templates", StringComparison.OrdinalIgnoreCase)))
{
failureReason = "no --export-templates in args";
return false;
}
deviceId = (GetArgAfterSwitch(args, "--export-templates") ?? "").Trim().Trim('"');
cardNo = (GetArgAfterSwitch(args, "--card") ?? "").Trim().Trim('"');
var outVal = GetArgAfterSwitch(args, "--out");
outPath = string.IsNullOrWhiteSpace(outVal) ? "" : outVal.Trim().Trim('"');
if (string.IsNullOrWhiteSpace(deviceId))
{
failureReason = "missing value after --export-templates";
return false;
}
if (string.IsNullOrWhiteSpace(cardNo))
{
failureReason = "missing --card <CardNoOrEmployeeId> (SDK enroll key on device)";
return false;
}
return true;
}
private static bool TryParseExportAllTemplatesArgs(
string[] args,
out string deviceId,
out string outDir,
out int pageSize,
out int maxUsers,
out string failureReason)
{
deviceId = "";
outDir = "";
pageSize = 30;
maxUsers = 10000;
failureReason = "";
if (!args.Any(a => string.Equals(a, "--export-all-templates", StringComparison.OrdinalIgnoreCase)))
{
failureReason = "no --export-all-templates in args";
return false;
}
deviceId = (GetArgAfterSwitch(args, "--export-all-templates") ?? "").Trim().Trim('"');
var outVal = GetArgAfterSwitch(args, "--outDir");
outDir = string.IsNullOrWhiteSpace(outVal) ? "" : outVal.Trim().Trim('"');
var pageVal = GetArgAfterSwitch(args, "--pageSize");
if (!string.IsNullOrWhiteSpace(pageVal) && int.TryParse(pageVal, out var ps))
pageSize = Math.Max(1, ps);
var maxVal = GetArgAfterSwitch(args, "--maxUsers");
if (!string.IsNullOrWhiteSpace(maxVal) && int.TryParse(maxVal, out var mu))
maxUsers = Math.Max(1, mu);
if (string.IsNullOrWhiteSpace(deviceId))
{
failureReason = "missing value after --export-all-templates";
return false;
}
return true;
}
private static bool TryParseExportAllIsapiTemplatesArgs(
string[] args,
out string deviceId,
out string outDir,
out int pageSize,
out int maxUsers,
out string failureReason)
{
deviceId = "";
outDir = "";
pageSize = 30;
maxUsers = 10000;
failureReason = "";
if (!args.Any(a => string.Equals(a, "--export-all-templates-isapi", StringComparison.OrdinalIgnoreCase)))
{
failureReason = "no --export-all-templates-isapi in args";
return false;
}
deviceId = (GetArgAfterSwitch(args, "--export-all-templates-isapi") ?? "").Trim().Trim('\"');
var outVal = GetArgAfterSwitch(args, "--outDir");
outDir = string.IsNullOrWhiteSpace(outVal) ? "" : outVal.Trim().Trim('\"');
var pageVal = GetArgAfterSwitch(args, "--pageSize");
if (!string.IsNullOrWhiteSpace(pageVal) && int.TryParse(pageVal, out var ps))
pageSize = Math.Max(1, ps);
var maxVal = GetArgAfterSwitch(args, "--maxUsers");
if (!string.IsNullOrWhiteSpace(maxVal) && int.TryParse(maxVal, out var mu))
maxUsers = Math.Max(1, mu);
if (string.IsNullOrWhiteSpace(deviceId))
{
failureReason = "missing value after --export-all-templates-isapi";
return false;
}
return true;
}
private static bool TryParseFetchArgs(string[] args, out string deviceId, out DateTime from, out DateTime to, out string failureReason)
{
deviceId = "";
from = default;
to = default;
failureReason = "";
bool wantFetch = false;
for (int i = 0; i < args.Length; i++)
{
if (string.Equals(args[i], "--fetch", StringComparison.OrdinalIgnoreCase))
wantFetch = true;
}
if (!wantFetch)
{
failureReason = "no --fetch in args";
return false;
}
var fetchVal = GetArgAfterSwitch(args, "--fetch");
var fromVal = GetArgAfterSwitch(args, "--from");
var toVal = GetArgAfterSwitch(args, "--to");
deviceId = (fetchVal ?? "").Trim().Trim('"');
bool fromOk = TryParseCliDateTime(fromVal, out from);
bool toOk = TryParseCliDateTime(toVal, out to);
if (string.IsNullOrWhiteSpace(deviceId))
{
failureReason = "missing value after --fetch (need --fetch <DeviceId>)";
return false;
}
if (!fromOk)
{
failureReason = "invalid or missing --from (got \"" + (fromVal ?? "(missing)") + "\"; use yyyy-MM-dd HH:mm:ss or yyyy-MM-dd)";
return false;
}
if (!toOk)
{
failureReason = "invalid or missing --to (got \"" + (toVal ?? "(missing)") + "\"; use yyyy-MM-dd HH:mm:ss or yyyy-MM-dd)";
return false;
}
return true;
}
private static string? GetArgAfterSwitch(string[] args, string switchName)
{
for (int i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], switchName, StringComparison.OrdinalIgnoreCase))
return args[i + 1];
}
return null;
}
private static bool TryParseCliDateTime(string? raw, out DateTime dt)
{
dt = default;
if (string.IsNullOrWhiteSpace(raw))
return false;
var s = raw.Trim().Trim('"');
string[] formats =
{
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-ddTHH:mm:ss",
"yyyy-MM-ddTHH:mm:ss.fff",
"yyyy-MM-dd"
};
if (DateTime.TryParseExact(s, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out dt))
return true;
return DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out dt);
}
}

33
DeviceIdentity.cs Normal file
View File

@ -0,0 +1,33 @@
using System.Text.RegularExpressions;
namespace HikvisionAttendanceService;
/// <summary>
/// Single place for DeviceId trimming and session/fetch lookup keys so config, CLI, and logs stay aligned.
/// </summary>
internal static class DeviceIdentity
{
private static readonly Regex FMNumericSuffix = new Regex(@"-FM(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
/// <summary>Value stored from JSON after load — trim only; never replace users chosen id.</summary>
public static string NormalizeConfigured(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return "";
return raw.Trim();
}
/// <summary>
/// Stable key for FindSession / fetch: same physical device if only the "-FM" serial prefix differs
/// (e.g. DS-K1T642MFW-FM7378360 vs DS-K1T642MFW-7378360).
/// </summary>
public static string CanonicalLookupKey(string? raw)
{
var s = NormalizeConfigured(raw);
if (s.Length == 0)
return "";
s = FMNumericSuffix.Replace(s, "-$1");
return s.ToUpperInvariant();
}
}

BIN
HCNetSDK/AudioRender.dll Normal file

Binary file not shown.

BIN
HCNetSDK/EagleEyeRender.dll Normal file

Binary file not shown.

BIN
HCNetSDK/HCCore.dll Normal file

Binary file not shown.

BIN
HCNetSDK/HCNetSDK.dll Normal file

Binary file not shown.

BIN
HCNetSDK/HXVA.dll Normal file

Binary file not shown.

BIN
HCNetSDK/HmMerge.dll Normal file

Binary file not shown.

BIN
HCNetSDK/MP_Render.dll Normal file

Binary file not shown.

BIN
HCNetSDK/MP_VIE.dll Normal file

Binary file not shown.

BIN
HCNetSDK/NPQos.dll Normal file

Binary file not shown.

BIN
HCNetSDK/OpenAL32.dll Normal file

Binary file not shown.

BIN
HCNetSDK/PlayCtrl.dll Normal file

Binary file not shown.

BIN
HCNetSDK/SuperRender.dll Normal file

Binary file not shown.

BIN
HCNetSDK/hlog.dll Normal file

Binary file not shown.

BIN
HCNetSDK/hpr.dll Normal file

Binary file not shown.

BIN
HCNetSDK/libcrypto-1_1.dll Normal file

Binary file not shown.

BIN
HCNetSDK/libssl-1_1.dll Normal file

Binary file not shown.

BIN
HCNetSDK/zlib1.dll Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net48</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-HikvisionAttendanceService-be7e0e28-c864-4273-ade4-819e35c57527</UserSecretsId>
<PlatformTarget>x86</PlatformTarget>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<!-- Hikvision interop (imported directly from the SDK demo source files) -->
<Compile Include="D:\EN-HCNetSDKV6.1.9.48_build20230410_win32\EN-HCNetSDKV6.1.9.48_build20230410_win32\C# demo\8-ACS_Optimization_ALL\EventByDeploy\EventByDeploy\HCNetSDK.cs"
Link="Interop\EventByDeploy\HCNetSDK.cs" />
<Compile Include="D:\EN-HCNetSDKV6.1.9.48_build20230410_win32\EN-HCNetSDKV6.1.9.48_build20230410_win32\C# demo\8-ACS_Optimization_ALL\EventByDeploy\EventByDeploy\TypeMap.cs"
Link="Interop\EventByDeploy\TypeMap.cs" />
<!-- Login_V30 + core callback helpers -->
<Compile Include="D:\EN-HCNetSDKV6.1.9.48_build20230410_win32\EN-HCNetSDKV6.1.9.48_build20230410_win32\C# demo\9-AppsDemo_build20201230191326\CommonBase\Head\HCNetSDK.cs"
Link="Interop\AppsDemo\CommonBase\HCNetSDK_LoginV30.cs" />
</ItemGroup>
<ItemGroup>
<!-- Ensure serviceconfig is copied next to the exe -->
<None Include="serviceconfig.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<!-- Copy native SDK DLLs into output/HCNetSDK so AppsDemo interop can resolve them -->
<Content Include="HCNetSDK\**\*.dll" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<!-- ServiceBase -->
<Reference Include="System.ServiceProcess" />
<!-- Needed for JavaScriptSerializer JSON parsing in STDXMLConfig diagnostics -->
<Reference Include="System.Web.Extensions" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,51 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Service", "Service", "{226567F5-DD15-7888-5BF1-C4407778305A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HikvisionAttendanceService", "HikvisionAttendanceService.csproj", "{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{1AD244E0-696C-4A2A-31B1-1832C4FA60E4}"
ProjectSection(SolutionItems) = preProject
appsettings.json = appsettings.json
serviceconfig.json = serviceconfig.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{FA46DF89-6585-3869-AC2D-4F5A2BEB4D3F}"
ProjectSection(SolutionItems) = preProject
Interop\AppsDemoStubs.cs = Interop\AppsDemoStubs.cs
Interop\HikvisionTemplateInterop.cs = Interop\HikvisionTemplateInterop.cs
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Debug|x64.ActiveCfg = Debug|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Debug|x64.Build.0 = Debug|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Debug|x86.ActiveCfg = Debug|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Debug|x86.Build.0 = Debug|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Release|Any CPU.Build.0 = Release|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Release|x64.ActiveCfg = Release|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Release|x64.Build.0 = Release|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Release|x86.ActiveCfg = Release|Any CPU
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B85FFCC7-E3DA-46AC-BE26-DEA0F65BDB91} = {226567F5-DD15-7888-5BF1-C4407778305A}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,367 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Threading;
using System.Threading.Tasks;
using System.ServiceProcess;
namespace HikvisionAttendanceService;
public sealed class HikvisionAttendanceWindowsService : ServiceBase
{
private CancellationTokenSource? _cts;
private Task? _mainTask;
private HikvisionAttendanceManager? _manager;
public HikvisionAttendanceWindowsService()
{
ServiceName = "HikvisionAttendanceService";
CanStop = true;
CanPauseAndContinue = false;
AutoLog = false; // we use our own file logger
}
protected override void OnStart(string[] args)
{
_cts = new CancellationTokenSource();
Environment.CurrentDirectory = AppContext.BaseDirectory;
var baseDir = AppContext.BaseDirectory;
var configPath = Path.Combine(baseDir, "serviceconfig.json");
if (!File.Exists(configPath))
{
// Fallback to appsettings.json (developer convenience).
configPath = Path.Combine(baseDir, "appsettings.json");
}
var config = HikvisionServiceConfig.Load(configPath);
var logger = new FileLogger(config.LogDirectory);
_manager = new HikvisionAttendanceManager(config, logger);
_mainTask = _manager.RunAsync(_cts.Token);
}
protected override void OnStop()
{
if (_cts is null)
{
return;
}
try
{
_cts.Cancel();
}
catch
{
// ignore
}
if (_mainTask is not null)
{
try { _mainTask.Wait(TimeSpan.FromSeconds(20)); } catch { /* ignore */ }
}
_manager?.Dispose();
_manager = null;
}
[DataContract]
internal sealed class HikvisionServiceConfig
{
[DataMember]
public string LogDirectory { get; set; } = @"C:\SdkLog";
[DataMember]
public string SqlConnectionString { get; set; } = "";
/// <summary>When true and SqlConnectionString is set, attendance rows are INSERTed. Default false until schema is finalized.</summary>
[DataMember]
public bool EnableDatabasePersistence { get; set; }
/// <summary>Plain-text attendance log. Empty = LogDirectory\attendance_records.txt</summary>
[DataMember]
public string AttendanceTextFilePath { get; set; } = "";
[DataMember]
public string AttendanceTableName { get; set; } = "dbo.HikvisionAttendanceEvents";
[DataMember]
public string HrExportPath { get; set; } = "";
[DataMember]
public bool AutoDoorControlOnSuccess { get; set; } = true;
[DataMember]
public int AutoDoorCloseDelaySeconds { get; set; } = 3;
[DataMember]
public List<DeviceConfig> Devices { get; set; } = new List<DeviceConfig>();
/// <summary>0 = all majors (per GetACSEvent demo).</summary>
[DataMember]
public uint AcsHistoryMajor { get; set; }
/// <summary>0 = all minors.</summary>
[DataMember]
public uint AcsHistoryMinor { get; set; }
/// <summary>0 = disabled. Otherwise interval in minutes for NET_DVR_GET_ACS_EVENT sync.</summary>
[DataMember]
public int HistoricalFetchIntervalMinutes { get; set; }
[DataMember]
public int HistoricalFetchLookbackMinutes { get; set; } = 1440;
public static HikvisionServiceConfig Load(string configPath)
{
// Minimal JSON config loader (no external packages).
try
{
if (!File.Exists(configPath))
{
return new HikvisionServiceConfig
{
LogDirectory = @"C:\SdkLog",
SqlConnectionString = "",
EnableDatabasePersistence = false,
AttendanceTextFilePath = "",
AttendanceTableName = "dbo.HikvisionAttendanceEvents",
HrExportPath = "",
AutoDoorControlOnSuccess = true,
AutoDoorCloseDelaySeconds = 3,
AcsHistoryMajor = 0,
AcsHistoryMinor = 0,
HistoricalFetchIntervalMinutes = 0,
HistoricalFetchLookbackMinutes = 1440
};
}
var raw = File.ReadAllText(configPath);
var serializer = new DataContractJsonSerializer(typeof(HikvisionServiceConfig));
// First attempt: strict JSON.
try
{
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(raw)))
{
var cfg = (HikvisionServiceConfig)serializer.ReadObject(ms);
NormalizeLoadedDeviceConfig(cfg);
return cfg;
}
}
catch
{
// Second attempt: tolerate // and /* */ comments (your serviceconfig.json includes them).
var sanitized = StripJsonComments(raw);
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(sanitized)))
{
var cfg = (HikvisionServiceConfig)serializer.ReadObject(ms);
NormalizeLoadedDeviceConfig(cfg);
return cfg;
}
}
}
catch
{
return new HikvisionServiceConfig
{
LogDirectory = @"C:\SdkLog",
SqlConnectionString = "",
EnableDatabasePersistence = false,
AttendanceTextFilePath = "",
AttendanceTableName = "dbo.HikvisionAttendanceEvents",
HrExportPath = "",
AutoDoorControlOnSuccess = true,
AutoDoorCloseDelaySeconds = 3,
AcsHistoryMajor = 0,
AcsHistoryMinor = 0,
HistoricalFetchIntervalMinutes = 0,
HistoricalFetchLookbackMinutes = 1440
};
}
}
private static string StripJsonComments(string input)
{
if (string.IsNullOrEmpty(input))
return input;
var sb = new StringBuilder(input.Length);
bool inString = false;
char stringQuote = '\0';
bool escape = false;
for (int i = 0; i < input.Length; i++)
{
char c = input[i];
if (inString)
{
sb.Append(c);
if (escape)
{
escape = false;
continue;
}
if (c == '\\')
{
escape = true;
continue;
}
if (c == stringQuote)
{
inString = false;
stringQuote = '\0';
}
continue;
}
// Not in a string
if (c == '"' || c == '\'')
{
// JSON strings are double-quoted; single-quote is not valid JSON,
// but supporting it here doesn't hurt for comment stripping.
inString = true;
stringQuote = c;
sb.Append(c);
continue;
}
// Line comment //
if (c == '/' && i + 1 < input.Length && input[i + 1] == '/')
{
i += 1; // consume second '/'
// skip until newline or end
while (i + 1 < input.Length)
{
i += 1;
if (input[i] == '\r' || input[i] == '\n')
{
sb.Append(input[i]);
break;
}
}
continue;
}
// Block comment /* ... */
if (c == '/' && i + 1 < input.Length && input[i + 1] == '*')
{
i += 1; // consume '*'
while (i + 1 < input.Length)
{
i += 1;
if (input[i] == '*' && i + 1 < input.Length && input[i + 1] == '/')
{
i += 1; // consume '/'
break;
}
}
continue;
}
sb.Append(c);
}
return sb.ToString();
}
private static void NormalizeLoadedDeviceConfig(HikvisionServiceConfig? cfg)
{
if (cfg?.Devices == null)
return;
foreach (var d in cfg.Devices)
{
d.DeviceId = DeviceIdentity.NormalizeConfigured(d.DeviceId);
if (!string.IsNullOrWhiteSpace(d.Ip))
d.Ip = d.Ip.Trim();
}
}
}
[DataContract]
internal sealed class DeviceConfig
{
[DataMember]
public string DeviceId { get; set; } = "";
[DataMember]
public string Ip { get; set; } = "";
[DataMember]
public int Port { get; set; } = 8000;
[DataMember]
public string Username { get; set; } = "";
[DataMember]
public string Password { get; set; } = "";
// Door index used by NET_DVR_ControlGateway for manual open/close.
[DataMember]
public int GatewayDoorIndex { get; set; } = 1;
// For template enrollment/sync (optional).
[DataMember]
public int FingerPrintReaderNo { get; set; } = 1;
[DataMember]
public int FaceReaderNo { get; set; } = 1;
/// <summary>Optional: documented in JSON for support logs (not sent to SDK).</summary>
[DataMember]
public string Model { get; set; } = "";
[DataMember]
public string SerialNumber { get; set; } = "";
[DataMember]
public string FirmwareVersion { get; set; } = "";
[DataMember]
public string SubnetMask { get; set; } = "";
[DataMember]
public string DefaultGateway { get; set; } = "";
}
internal sealed class FileLogger : IDisposable
{
private readonly string _directory;
private readonly object _sync = new();
public FileLogger(string directory)
{
_directory = directory;
Directory.CreateDirectory(_directory);
}
public void Info(string message) => Write("INFO", message);
public void Warn(string message) => Write("WARN", message);
public void Error(string message, Exception? ex = null) => Write("ERROR", message + (ex is null ? "" : $" | {ex}"));
private void Write(string level, string message)
{
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}{Environment.NewLine}";
var logName = "attendance_logs_" + DateTime.Now.ToString("yyyy-MM-dd") + ".txt";
var path = Path.Combine(_directory, logName);
lock (_sync)
{
File.AppendAllText(path, line);
}
}
public void Dispose() { }
}
// Manager implementation is in a separate file below for readability.
}

27
Interop/AppsDemoStubs.cs Normal file
View File

@ -0,0 +1,27 @@
using System;
namespace Common
{
// The AppsDemo interop file includes optional UI/plugin helpers (e.g., AddLog)
// that reference types from the full demo application. Our Windows Service
// doesn't use those helpers, so we provide minimal stubs to satisfy the compiler.
internal interface IDeviceTree
{
DeviceInfo GetSelectedDeviceInfo();
sealed class DeviceInfo
{
public string sDeviceIP;
public string sDeviceName;
}
}
internal static class PluginsFactory
{
public static IDeviceTree GetDeviceTreeInstance()
{
return null; // not used by the service runtime
}
}
}

View File

@ -0,0 +1,274 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace HikvisionAttendanceService.Interop;
internal static class HikvisionTemplateInterop
{
// These wrappers allow template query/delete/sync with direct HCNetSDK.dll calls.
[DllImport("HCNetSDK.dll", CallingConvention = CallingConvention.StdCall)]
private static extern bool NET_DVR_SetDeviceConfig(
int lUserID,
uint dwCommand,
int lChannel,
IntPtr lpInBuffer,
uint dwInBufferSize,
IntPtr lpStatusList,
IntPtr lpInParamBuffer,
uint dwInParamBufferSize);
[DllImport("HCNetSDK.dll", CallingConvention = CallingConvention.StdCall)]
private static extern bool NET_DVR_GetDeviceConfig(
int lUserID,
uint dwCommand,
int lChannel,
IntPtr lpOutBuffer,
uint dwOutBufferSize,
ref uint lpBytesReturned,
IntPtr lpStatusList,
IntPtr lpInParamBuffer,
uint dwInParamBufferSize);
// Command IDs are the official ACS face/fingerprint config commands.
private const uint NET_DVR_SET_FACE_PARAM_CFG = 2568;
private const uint NET_DVR_GET_FACE_PARAM_CFG = 2569;
private const uint NET_DVR_DEL_FACE_PARAM_CFG = 2570;
private const uint NET_DVR_SET_FINGERPRINT_PARAM = 2573;
private const uint NET_DVR_GET_FINGERPRINT_PARAM = 2574;
private const uint NET_DVR_DEL_FINGERPRINT_PARAM = 2575;
[StructLayout(LayoutKind.Sequential)]
internal struct NET_DVR_FACE_PARAM_CFG
{
public uint dwSize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] byCardNo;
public uint dwFaceLen;
public IntPtr pFaceBuffer;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)]
public byte[] byRes;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NET_DVR_FINGERPRINT_PARAM
{
public uint dwSize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] byCardNo;
public byte byFingerPrintID;
public byte byEnableCardReader;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
public byte[] byRes1;
public uint dwFingerPrintLen;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2048)]
public byte[] byFingerData;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] byRes2;
}
internal static bool SetFaceParam(int userId, int channel, string cardNo, byte[] faceData, out string error)
{
error = "";
var cfg = new NET_DVR_FACE_PARAM_CFG
{
dwSize = (uint)Marshal.SizeOf(typeof(NET_DVR_FACE_PARAM_CFG)),
byCardNo = new byte[32],
byRes = new byte[128],
dwFaceLen = (uint)(faceData?.Length ?? 0),
pFaceBuffer = IntPtr.Zero
};
CopyAscii(cardNo, cfg.byCardNo);
IntPtr cfgPtr = IntPtr.Zero;
IntPtr facePtr = IntPtr.Zero;
try
{
if (faceData == null || faceData.Length == 0)
{
error = "faceData is empty";
return false;
}
facePtr = Marshal.AllocHGlobal(faceData.Length);
Marshal.Copy(faceData, 0, facePtr, faceData.Length);
cfg.pFaceBuffer = facePtr;
cfgPtr = Marshal.AllocHGlobal((int)cfg.dwSize);
Marshal.StructureToPtr(cfg, cfgPtr, false);
return NET_DVR_SetDeviceConfig(userId, NET_DVR_SET_FACE_PARAM_CFG, channel, cfgPtr, cfg.dwSize, IntPtr.Zero, IntPtr.Zero, 0);
}
finally
{
if (cfgPtr != IntPtr.Zero) Marshal.FreeHGlobal(cfgPtr);
if (facePtr != IntPtr.Zero) Marshal.FreeHGlobal(facePtr);
}
}
internal static bool GetFaceParam(int userId, int channel, string cardNo, out byte[] faceData)
{
faceData = Array.Empty<byte>();
var cfg = new NET_DVR_FACE_PARAM_CFG
{
dwSize = (uint)Marshal.SizeOf(typeof(NET_DVR_FACE_PARAM_CFG)),
byCardNo = new byte[32],
byRes = new byte[128]
};
CopyAscii(cardNo, cfg.byCardNo);
uint returned = 0;
IntPtr cfgPtr = IntPtr.Zero;
try
{
cfgPtr = Marshal.AllocHGlobal((int)cfg.dwSize);
Marshal.StructureToPtr(cfg, cfgPtr, false);
if (!NET_DVR_GetDeviceConfig(userId, NET_DVR_GET_FACE_PARAM_CFG, channel, cfgPtr, cfg.dwSize, ref returned, IntPtr.Zero, IntPtr.Zero, 0))
return false;
var outCfg = Marshal.PtrToStructure<NET_DVR_FACE_PARAM_CFG>(cfgPtr);
if (outCfg.dwFaceLen == 0 || outCfg.pFaceBuffer == IntPtr.Zero)
return true;
faceData = new byte[outCfg.dwFaceLen];
Marshal.Copy(outCfg.pFaceBuffer, faceData, 0, (int)outCfg.dwFaceLen);
return true;
}
finally
{
if (cfgPtr != IntPtr.Zero) Marshal.FreeHGlobal(cfgPtr);
}
}
internal static bool DeleteFaceParam(int userId, int channel, string cardNo)
{
var cfg = new NET_DVR_FACE_PARAM_CFG
{
dwSize = (uint)Marshal.SizeOf(typeof(NET_DVR_FACE_PARAM_CFG)),
byCardNo = new byte[32],
byRes = new byte[128]
};
CopyAscii(cardNo, cfg.byCardNo);
IntPtr cfgPtr = IntPtr.Zero;
try
{
cfgPtr = Marshal.AllocHGlobal((int)cfg.dwSize);
Marshal.StructureToPtr(cfg, cfgPtr, false);
return NET_DVR_SetDeviceConfig(userId, NET_DVR_DEL_FACE_PARAM_CFG, channel, cfgPtr, cfg.dwSize, IntPtr.Zero, IntPtr.Zero, 0);
}
finally
{
if (cfgPtr != IntPtr.Zero) Marshal.FreeHGlobal(cfgPtr);
}
}
internal static bool SetFingerprintParam(int userId, int channel, string cardNo, byte fingerId, byte[] fpData)
{
var cfg = new NET_DVR_FINGERPRINT_PARAM
{
dwSize = (uint)Marshal.SizeOf(typeof(NET_DVR_FINGERPRINT_PARAM)),
byCardNo = new byte[32],
byRes1 = new byte[2],
byFingerData = new byte[2048],
byRes2 = new byte[64],
byFingerPrintID = fingerId,
byEnableCardReader = 1,
dwFingerPrintLen = (uint)Math.Min(fpData?.Length ?? 0, 2048)
};
CopyAscii(cardNo, cfg.byCardNo);
if (fpData != null && fpData.Length > 0)
Buffer.BlockCopy(fpData, 0, cfg.byFingerData, 0, (int)cfg.dwFingerPrintLen);
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal((int)cfg.dwSize);
Marshal.StructureToPtr(cfg, ptr, false);
return NET_DVR_SetDeviceConfig(userId, NET_DVR_SET_FINGERPRINT_PARAM, channel, ptr, cfg.dwSize, IntPtr.Zero, IntPtr.Zero, 0);
}
finally
{
if (ptr != IntPtr.Zero) Marshal.FreeHGlobal(ptr);
}
}
internal static bool GetFingerprintParam(int userId, int channel, string cardNo, byte fingerId, out byte[] fpData)
{
fpData = Array.Empty<byte>();
var cfg = new NET_DVR_FINGERPRINT_PARAM
{
dwSize = (uint)Marshal.SizeOf(typeof(NET_DVR_FINGERPRINT_PARAM)),
byCardNo = new byte[32],
byRes1 = new byte[2],
byFingerData = new byte[2048],
byRes2 = new byte[64],
byFingerPrintID = fingerId,
byEnableCardReader = 1
};
CopyAscii(cardNo, cfg.byCardNo);
uint returned = 0;
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal((int)cfg.dwSize);
Marshal.StructureToPtr(cfg, ptr, false);
if (!NET_DVR_GetDeviceConfig(userId, NET_DVR_GET_FINGERPRINT_PARAM, channel, ptr, cfg.dwSize, ref returned, IntPtr.Zero, IntPtr.Zero, 0))
return false;
var outCfg = Marshal.PtrToStructure<NET_DVR_FINGERPRINT_PARAM>(ptr);
if (outCfg.dwFingerPrintLen == 0)
return true;
fpData = new byte[outCfg.dwFingerPrintLen];
Buffer.BlockCopy(outCfg.byFingerData, 0, fpData, 0, (int)outCfg.dwFingerPrintLen);
return true;
}
finally
{
if (ptr != IntPtr.Zero) Marshal.FreeHGlobal(ptr);
}
}
internal static bool DeleteFingerprintParam(int userId, int channel, string cardNo, byte fingerId)
{
var cfg = new NET_DVR_FINGERPRINT_PARAM
{
dwSize = (uint)Marshal.SizeOf(typeof(NET_DVR_FINGERPRINT_PARAM)),
byCardNo = new byte[32],
byRes1 = new byte[2],
byFingerData = new byte[2048],
byRes2 = new byte[64],
byFingerPrintID = fingerId,
byEnableCardReader = 1
};
CopyAscii(cardNo, cfg.byCardNo);
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal((int)cfg.dwSize);
Marshal.StructureToPtr(cfg, ptr, false);
return NET_DVR_SetDeviceConfig(userId, NET_DVR_DEL_FINGERPRINT_PARAM, channel, ptr, cfg.dwSize, IntPtr.Zero, IntPtr.Zero, 0);
}
finally
{
if (ptr != IntPtr.Zero) Marshal.FreeHGlobal(ptr);
}
}
private static void CopyAscii(string text, byte[] target)
{
if (target == null || target.Length == 0)
return;
if (string.IsNullOrWhiteSpace(text))
return;
var bytes = Encoding.ASCII.GetBytes(text.Trim());
var len = Math.Min(bytes.Length, target.Length);
Buffer.BlockCopy(bytes, 0, target, 0, len);
}
}

30
Program.cs Normal file
View File

@ -0,0 +1,30 @@
using System;
using System.Linq;
using System.ServiceProcess;
using System.Threading.Tasks;
namespace HikvisionAttendanceService;
internal static class Program
{
private static bool ArgsContainTestFlag(string[] args) =>
args.Any(a => string.Equals(a, "--test", StringComparison.OrdinalIgnoreCase));
private static async Task Main(string[] args)
{
// Console so mis-invocation is visible even before the daily log file is opened.
if (ArgsContainTestFlag(args))
Console.WriteLine("Program entry (test): rawArgCount=" + args.Length + " rawArgs=" + string.Join(" ", args));
if (ArgsContainTestFlag(args))
{
await AttendanceTestMode.RunAsync(args).ConfigureAwait(false);
return;
}
ServiceBase.Run(new ServiceBase[]
{
new HikvisionAttendanceWindowsService()
});
}
}

View File

@ -0,0 +1,12 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"HikvisionAttendanceService": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace HikvisionAttendanceService;
/// <summary>JSON-serializable result of exporting face + fingerprint templates for one user (SDK key = card number / employee string).</summary>
internal sealed class UserTemplateExportPayload
{
public string exportedAtUtc { get; set; } = "";
public string deviceId { get; set; } = "";
public string cardNo { get; set; } = "";
/// <summary>Documents the actual SDK calls used in this build.</summary>
public string sdkImplementationNote { get; set; } =
"Face/Fingerprints exported via NET_DVR_StartRemoteConfig (preferred), with ISAPI fallback via NET_DVR_STDXMLConfig.";
public FaceTemplateExportItem face { get; set; } = new FaceTemplateExportItem();
public List<FingerprintTemplateExportItem> fingerprints { get; set; } = new List<FingerprintTemplateExportItem>();
}
internal sealed class FaceTemplateExportItem
{
public bool attempted { get; set; }
public bool present { get; set; }
public int byteLength { get; set; }
public string dataBase64 { get; set; } = "";
public string error { get; set; } = "";
}
internal sealed class FingerprintTemplateExportItem
{
public int fingerId { get; set; }
public bool attempted { get; set; }
public bool present { get; set; }
public int byteLength { get; set; }
public string dataBase64 { get; set; } = "";
public string error { get; set; } = "";
}