add:海康sdk
This commit is contained in:
parent
92df3bf9ba
commit
5e27c2aa7c
81
Controllers/HikvisionController.cs
Normal file
81
Controllers/HikvisionController.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using YD_TrainingCompetition_Api.Models;
|
||||
using YD_TrainingCompetition_Api.Options;
|
||||
using YD_TrainingCompetition_Api.Services;
|
||||
|
||||
namespace YD_TrainingCompetition_Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class HikvisionController : ControllerBase
|
||||
{
|
||||
private readonly IHikvisionAlarmStore _alarmStore;
|
||||
private readonly ILogger<HikvisionController> _logger;
|
||||
|
||||
public HikvisionController(IHikvisionAlarmStore alarmStore, ILogger<HikvisionController> logger)
|
||||
{
|
||||
_alarmStore = alarmStore;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有报警记录
|
||||
/// </summary>
|
||||
[HttpGet("alarms")]
|
||||
public ActionResult<IReadOnlyCollection<AlarmRecord>> GetAlarms(
|
||||
[FromQuery] string? deviceId = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
return Ok(_alarmStore.GetByDeviceId(deviceId));
|
||||
}
|
||||
return Ok(_alarmStore.GetAll());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最新报警
|
||||
/// </summary>
|
||||
[HttpGet("alarms/latest")]
|
||||
public ActionResult<AlarmRecord?> GetLatestAlarm(
|
||||
[FromQuery] string? deviceId = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
var record = _alarmStore.GetLatestByDevice(deviceId);
|
||||
return record is null ? NotFound() : Ok(record);
|
||||
}
|
||||
|
||||
var latest = _alarmStore.GetLatest();
|
||||
return latest is null ? NotFound() : Ok(latest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有摄像头状态
|
||||
/// </summary>
|
||||
[HttpGet("cameras")]
|
||||
public ActionResult<IReadOnlyCollection<HikvisionCameraStatus>> GetCameraStatuses()
|
||||
{
|
||||
return Ok(_alarmStore.GetAllCameraStatuses());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个摄像头状态
|
||||
/// </summary>
|
||||
[HttpGet("cameras/{deviceId}")]
|
||||
public ActionResult<HikvisionCameraStatus> GetCameraStatus(string deviceId)
|
||||
{
|
||||
var status = _alarmStore.GetCameraStatus(deviceId);
|
||||
return status is null ? NotFound() : Ok(status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空报警记录
|
||||
/// </summary>
|
||||
[HttpDelete("alarms")]
|
||||
public IActionResult ClearAlarms()
|
||||
{
|
||||
_alarmStore.ClearAlarms();
|
||||
_logger.LogInformation("Alarms cleared via API");
|
||||
return Ok(new { message = "All alarms cleared" });
|
||||
}
|
||||
}
|
||||
20433
Hikvision/CHCNetSDK.cs
Normal file
20433
Hikvision/CHCNetSDK.cs
Normal file
File diff suppressed because it is too large
Load Diff
63
Models/AlarmRecord.cs
Normal file
63
Models/AlarmRecord.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace YD_TrainingCompetition_Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 报警记录,包含 JSON/XML 数据和图片信息
|
||||
/// </summary>
|
||||
public sealed class AlarmRecord
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
[JsonPropertyName("deviceId")]
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ip")]
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("dataType")]
|
||||
public string DataType { get; set; } = string.Empty; // "xml" or "json"
|
||||
|
||||
/// <summary>
|
||||
/// 原始报警数据(JSON 或 XML 字符串)
|
||||
/// </summary>
|
||||
[JsonPropertyName("rawData")]
|
||||
public string RawData { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 报警数据长度(字节)
|
||||
/// </summary>
|
||||
[JsonPropertyName("dataLength")]
|
||||
public uint DataLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 附带图片数量
|
||||
/// </summary>
|
||||
[JsonPropertyName("pictureCount")]
|
||||
public int PictureCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图片数据(Base64 编码,仅保留最新报警的图片)
|
||||
/// </summary>
|
||||
[JsonPropertyName("pictures")]
|
||||
public List<AlarmPicture> Pictures { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 报警附带图片
|
||||
/// </summary>
|
||||
public sealed class AlarmPicture
|
||||
{
|
||||
[JsonPropertyName("fileName")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("length")]
|
||||
public uint Length { get; set; }
|
||||
|
||||
[JsonPropertyName("base64")]
|
||||
public string Base64 { get; set; } = string.Empty;
|
||||
}
|
||||
33
Models/CameraConfig.cs
Normal file
33
Models/CameraConfig.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace YD_TrainingCompetition_Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头连接配置
|
||||
/// </summary>
|
||||
public sealed class CameraConfig
|
||||
{
|
||||
[JsonPropertyName("deviceId")]
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ip")]
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("port")]
|
||||
public ushort Port { get; set; } = 8000;
|
||||
|
||||
[JsonPropertyName("userName")]
|
||||
public string UserName { get; set; } = "admin";
|
||||
|
||||
[JsonPropertyName("password")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("channel")]
|
||||
public int Channel { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用该摄像头
|
||||
/// </summary>
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
36
Models/HikvisionCameraStatus.cs
Normal file
36
Models/HikvisionCameraStatus.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace YD_TrainingCompetition_Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 海康摄像头实时状态
|
||||
/// </summary>
|
||||
public sealed class HikvisionCameraStatus
|
||||
{
|
||||
[JsonPropertyName("deviceId")]
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ip")]
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("online")]
|
||||
public bool Online { get; set; }
|
||||
|
||||
[JsonPropertyName("armed")]
|
||||
public bool Armed { get; set; }
|
||||
|
||||
[JsonPropertyName("userId")]
|
||||
public int UserId { get; set; } = -1;
|
||||
|
||||
[JsonPropertyName("alarmHandle")]
|
||||
public int AlarmHandle { get; set; } = -1;
|
||||
|
||||
[JsonPropertyName("lastAlarmTime")]
|
||||
public DateTimeOffset? LastAlarmTime { get; set; }
|
||||
|
||||
[JsonPropertyName("lastSeen")]
|
||||
public DateTimeOffset LastSeen { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
69
Options/HikvisionOptions.cs
Normal file
69
Options/HikvisionOptions.cs
Normal file
@ -0,0 +1,69 @@
|
||||
namespace YD_TrainingCompetition_Api.Options;
|
||||
|
||||
public sealed class HikvisionOptions
|
||||
{
|
||||
public const string SectionName = "Hikvision";
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用海康威视 SDK 接入
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// SDK 日志目录
|
||||
/// </summary>
|
||||
public string SdkLogPath { get; set; } = "C:\\SdkLog\\";
|
||||
|
||||
/// <summary>
|
||||
/// 报警数据图片分离:0-不分离(COMM_VCA_ALARM),1-分离(COMM_ISAPI_ALARM)
|
||||
/// </summary>
|
||||
public byte AlarmJsonPictureSeparate { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 报警存储最大条数
|
||||
/// </summary>
|
||||
public int MaxAlarmRecords { get; set; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头列表
|
||||
/// </summary>
|
||||
public List<CameraConfigItem> Cameras { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class CameraConfigItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 设备唯一标识
|
||||
/// </summary>
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头 IP 地址
|
||||
/// </summary>
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 端口号
|
||||
/// </summary>
|
||||
public ushort Port { get; set; } = 8000;
|
||||
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = "admin";
|
||||
|
||||
/// <summary>
|
||||
/// 密码
|
||||
/// </summary>
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 通道号
|
||||
/// </summary>
|
||||
public int Channel { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
@ -15,6 +15,7 @@ builder.WebHost.UseUrls(listenUrls);
|
||||
builder.Services.Configure<ScoringOptions>(builder.Configuration.GetSection(ScoringOptions.SectionName));
|
||||
builder.Services.Configure<Serial24GOptions>(builder.Configuration.GetSection(Serial24GOptions.SectionName));
|
||||
builder.Services.Configure<RosterOptions>(builder.Configuration.GetSection(RosterOptions.SectionName));
|
||||
builder.Services.Configure<HikvisionOptions>(builder.Configuration.GetSection(HikvisionOptions.SectionName));
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
@ -39,8 +40,10 @@ builder.Services.AddSingleton<IJumpProtocolParser, JumpProtocolParser>();
|
||||
builder.Services.AddSingleton<IRosterService, RosterService>();
|
||||
builder.Services.AddSingleton<IParticipantDirectory, ParticipantDirectory>();
|
||||
builder.Services.AddSingleton<ICompetitionSessionService, CompetitionSessionService>();
|
||||
builder.Services.AddSingleton<IHikvisionAlarmStore, HikvisionAlarmStore>();
|
||||
builder.Services.AddHostedService<SerialIngestWorker>();
|
||||
builder.Services.AddHostedService<CompetitionCountdownWorker>();
|
||||
builder.Services.AddHostedService<HikvisionIngestWorker>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
92
Services/HikvisionAlarmStore.cs
Normal file
92
Services/HikvisionAlarmStore.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using YD_TrainingCompetition_Api.Models;
|
||||
using YD_TrainingCompetition_Api.Options;
|
||||
|
||||
namespace YD_TrainingCompetition_Api.Services;
|
||||
|
||||
public sealed class HikvisionAlarmStore : IHikvisionAlarmStore
|
||||
{
|
||||
private readonly ConcurrentQueue<AlarmRecord> _alarms = new();
|
||||
private readonly ConcurrentDictionary<string, HikvisionCameraStatus> _cameraStatuses = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HikvisionOptions _options;
|
||||
private readonly ILogger<HikvisionAlarmStore> _logger;
|
||||
private int _alarmCount;
|
||||
|
||||
public HikvisionAlarmStore(IOptions<HikvisionOptions> options, ILogger<HikvisionAlarmStore> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Add(AlarmRecord record)
|
||||
{
|
||||
_alarms.Enqueue(record);
|
||||
|
||||
var newCount = Interlocked.Increment(ref _alarmCount);
|
||||
|
||||
// 超出上限则移除最旧的记录
|
||||
while (newCount > _options.MaxAlarmRecords)
|
||||
{
|
||||
if (_alarms.TryDequeue(out _))
|
||||
{
|
||||
Interlocked.Decrement(ref _alarmCount);
|
||||
newCount--;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Alarm stored: DeviceId={DeviceId}, Id={Id}, QueueSize={Size}",
|
||||
record.DeviceId, record.Id, newCount);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<AlarmRecord> GetAll()
|
||||
{
|
||||
return _alarms.Reverse().ToArray();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<AlarmRecord> GetByDeviceId(string deviceId)
|
||||
{
|
||||
return _alarms
|
||||
.Where(x => string.Equals(x.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase))
|
||||
.Reverse()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public AlarmRecord? GetLatest()
|
||||
{
|
||||
return _alarms.LastOrDefault();
|
||||
}
|
||||
|
||||
public AlarmRecord? GetLatestByDevice(string deviceId)
|
||||
{
|
||||
return _alarms
|
||||
.LastOrDefault(x => string.Equals(x.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public void UpdateCameraStatus(HikvisionCameraStatus status)
|
||||
{
|
||||
status.LastSeen = DateTimeOffset.UtcNow;
|
||||
_cameraStatuses[status.DeviceId] = status;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<HikvisionCameraStatus> GetAllCameraStatuses()
|
||||
{
|
||||
return _cameraStatuses.Values
|
||||
.OrderBy(x => x.DeviceId, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public HikvisionCameraStatus? GetCameraStatus(string deviceId)
|
||||
{
|
||||
return _cameraStatuses.TryGetValue(deviceId, out var status) ? status : null;
|
||||
}
|
||||
|
||||
public void ClearAlarms()
|
||||
{
|
||||
while (_alarms.TryDequeue(out _))
|
||||
{
|
||||
Interlocked.Decrement(ref _alarmCount);
|
||||
}
|
||||
_logger.LogInformation("All alarms cleared");
|
||||
}
|
||||
}
|
||||
382
Services/HikvisionIngestWorker.cs
Normal file
382
Services/HikvisionIngestWorker.cs
Normal file
@ -0,0 +1,382 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using YD_TrainingCompetition_Api.Hikvision;
|
||||
using YD_TrainingCompetition_Api.Models;
|
||||
using YD_TrainingCompetition_Api.Options;
|
||||
|
||||
namespace YD_TrainingCompetition_Api.Services;
|
||||
|
||||
public sealed class HikvisionIngestWorker : BackgroundService
|
||||
{
|
||||
// 静态实例引用,供静态回调使用
|
||||
private static HikvisionIngestWorker? s_instance;
|
||||
|
||||
private readonly ILogger<HikvisionIngestWorker> _logger;
|
||||
private readonly IHikvisionAlarmStore _alarmStore;
|
||||
private readonly IRealtimeNotifier _notifier;
|
||||
private readonly HikvisionOptions _options;
|
||||
private readonly List<CameraSession> _sessions = new();
|
||||
private bool _sdkInitialized;
|
||||
|
||||
public HikvisionIngestWorker(
|
||||
ILogger<HikvisionIngestWorker> logger,
|
||||
IHikvisionAlarmStore alarmStore,
|
||||
IRealtimeNotifier notifier,
|
||||
IOptions<HikvisionOptions> options)
|
||||
{
|
||||
_logger = logger;
|
||||
_alarmStore = alarmStore;
|
||||
_notifier = notifier;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
_logger.LogInformation("Hikvision SDK ingest is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
s_instance = this;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 初始化 SDK
|
||||
if (!CHCNetSDK.NET_DVR_Init())
|
||||
{
|
||||
_logger.LogError("NET_DVR_Init failed, error={Error}", CHCNetSDK.NET_DVR_GetLastError());
|
||||
return;
|
||||
}
|
||||
_sdkInitialized = true;
|
||||
_logger.LogInformation("NET_DVR_Init succeeded");
|
||||
|
||||
// 2. 设置 SDK 日志
|
||||
CHCNetSDK.NET_DVR_SetLogToFile(3, _options.SdkLogPath, true);
|
||||
|
||||
// 3. 设置本地配置:报警 JSON 图片分离
|
||||
SetLocalConfig();
|
||||
|
||||
// 4. 设置报警回调
|
||||
var callbackDelegate = new CHCNetSDK.MSGCallBack_V31(AlarmCallbackBridge);
|
||||
CHCNetSDK.NET_DVR_SetDVRMessageCallBack_V31(callbackDelegate, IntPtr.Zero);
|
||||
_logger.LogInformation("Alarm callback registered");
|
||||
|
||||
// 5. 登录所有启用的摄像头
|
||||
foreach (var cam in _options.Cameras.Where(c => c.Enabled))
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
await LoginCameraAsync(cam, stoppingToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Hikvision ingest running, {Count} camera(s) connected", _sessions.Count);
|
||||
|
||||
// 6. 保持运行,直到取消
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(5000, stoppingToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Hikvision ingest worker cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Hikvision ingest worker fatal error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await CleanupAsync();
|
||||
s_instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLocalConfig()
|
||||
{
|
||||
var localCfg = new CHCNetSDK.NET_DVR_LOCAL_GENERAL_CFG
|
||||
{
|
||||
byAlarmJsonPictureSeparate = _options.AlarmJsonPictureSeparate
|
||||
};
|
||||
|
||||
int size = Marshal.SizeOf(localCfg);
|
||||
IntPtr ptr = Marshal.AllocHGlobal(size);
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(localCfg, ptr, false);
|
||||
if (!CHCNetSDK.NET_DVR_SetSDKLocalCfg(17, ptr))
|
||||
{
|
||||
_logger.LogWarning("NET_DVR_SetSDKLocalCfg failed, error={Error}", CHCNetSDK.NET_DVR_GetLastError());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Local config set: AlarmJsonPictureSeparate={Val}", _options.AlarmJsonPictureSeparate);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoginCameraAsync(CameraConfigItem cam, CancellationToken ct)
|
||||
{
|
||||
var session = new CameraSession
|
||||
{
|
||||
DeviceId = cam.DeviceId,
|
||||
Ip = cam.Ip
|
||||
};
|
||||
|
||||
var loginInfo = new CHCNetSDK.NET_DVR_USER_LOGIN_INFO
|
||||
{
|
||||
sDeviceAddress = Encoding.UTF8.GetBytes(cam.Ip.PadRight(129, '\0')),
|
||||
sUserName = Encoding.UTF8.GetBytes(cam.UserName.PadRight(64, '\0')),
|
||||
sPassword = Encoding.UTF8.GetBytes(cam.Password.PadRight(64, '\0')),
|
||||
wPort = cam.Port,
|
||||
bUseAsynLogin = false,
|
||||
byUseTransport = 0
|
||||
};
|
||||
|
||||
var deviceInfo = new CHCNetSDK.NET_DVR_DEVICEINFO_V40();
|
||||
|
||||
// 在后台线程执行同步登录
|
||||
int userId = await Task.Run(() =>
|
||||
CHCNetSDK.NET_DVR_Login_V40(ref loginInfo, ref deviceInfo), ct);
|
||||
|
||||
if (userId < 0)
|
||||
{
|
||||
uint err = CHCNetSDK.NET_DVR_GetLastError();
|
||||
_logger.LogError("Login failed: DeviceId={DeviceId}, Ip={Ip}, Error={Error}", cam.DeviceId, cam.Ip, err);
|
||||
|
||||
_alarmStore.UpdateCameraStatus(new HikvisionCameraStatus
|
||||
{
|
||||
DeviceId = cam.DeviceId,
|
||||
Ip = cam.Ip,
|
||||
Online = false,
|
||||
ErrorMessage = $"Login failed, error={err}"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
session.UserId = userId;
|
||||
_logger.LogInformation("Login success: DeviceId={DeviceId}, Ip={Ip}, UserId={UserId}", cam.DeviceId, cam.Ip, userId);
|
||||
|
||||
// 设置布防
|
||||
var alarmParam = new CHCNetSDK.NET_DVR_SETUPALARM_PARAM
|
||||
{
|
||||
dwSize = (uint)Marshal.SizeOf<CHCNetSDK.NET_DVR_SETUPALARM_PARAM>(),
|
||||
byLevel = 0, // 0-一级报警(最高)
|
||||
byAlarmInfoType = 0,
|
||||
byDeployType = 1 // 1-实时布防
|
||||
};
|
||||
|
||||
int alarmHandle = await Task.Run(() =>
|
||||
CHCNetSDK.NET_DVR_SetupAlarmChan_V41(userId, ref alarmParam), ct);
|
||||
|
||||
if (alarmHandle < 0)
|
||||
{
|
||||
uint err = CHCNetSDK.NET_DVR_GetLastError();
|
||||
_logger.LogWarning("SetupAlarmChan failed: DeviceId={DeviceId}, Error={Error}", cam.DeviceId, err);
|
||||
}
|
||||
else
|
||||
{
|
||||
session.AlarmHandle = alarmHandle;
|
||||
session.Armed = true;
|
||||
_logger.LogInformation("Alarm armed: DeviceId={DeviceId}, Handle={Handle}", cam.DeviceId, alarmHandle);
|
||||
}
|
||||
|
||||
_sessions.Add(session);
|
||||
|
||||
_alarmStore.UpdateCameraStatus(new HikvisionCameraStatus
|
||||
{
|
||||
DeviceId = cam.DeviceId,
|
||||
Ip = cam.Ip,
|
||||
Online = true,
|
||||
Armed = session.Armed,
|
||||
UserId = userId,
|
||||
AlarmHandle = session.AlarmHandle
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 静态回调桥接 — SDK 回调在非托管线程上执行
|
||||
/// </summary>
|
||||
private static bool AlarmCallbackBridge(int lCommand, ref CHCNetSDK.NET_DVR_ALARMER pAlarmer, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser)
|
||||
{
|
||||
var instance = s_instance;
|
||||
if (instance == null) return true;
|
||||
|
||||
try
|
||||
{
|
||||
instance.HandleAlarm(lCommand, ref pAlarmer, pAlarmInfo, dwBufLen);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
instance._logger.LogError(ex, "Unhandled error in alarm callback");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleAlarm(int lCommand, ref CHCNetSDK.NET_DVR_ALARMER pAlarmer, IntPtr pAlarmInfo, uint dwBufLen)
|
||||
{
|
||||
if (lCommand != CHCNetSDK.COMM_ISAPI_ALARM)
|
||||
{
|
||||
// 仅处理 ISAPI 报警(JSON/XML 数据分离后的类型)
|
||||
_logger.LogDebug("Ignored alarm command: 0x{Command:X}", lCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
string ip = GetString(pAlarmer.sDeviceIP);
|
||||
|
||||
try
|
||||
{
|
||||
var struAlarm = Marshal.PtrToStructure<CHCNetSDK.NET_DVR_ALARM_ISAPI_INFO>(pAlarmInfo);
|
||||
|
||||
// 提取报警数据
|
||||
string alarmData = string.Empty;
|
||||
string dataType = "unknown";
|
||||
|
||||
if (struAlarm.dwAlarmDataLen > 0 && struAlarm.pAlarmData != IntPtr.Zero)
|
||||
{
|
||||
byte[] by = new byte[struAlarm.dwAlarmDataLen];
|
||||
Marshal.Copy(struAlarm.pAlarmData, by, 0, (int)struAlarm.dwAlarmDataLen);
|
||||
alarmData = Encoding.UTF8.GetString(by);
|
||||
|
||||
dataType = struAlarm.byDataType switch
|
||||
{
|
||||
1 => "xml",
|
||||
2 => "json",
|
||||
_ => "unknown"
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"[ISAPI Alarm] DeviceIp={Ip}, DataType={Type}, DataLen={Len}",
|
||||
ip, dataType, struAlarm.dwAlarmDataLen);
|
||||
|
||||
// 控制台输出
|
||||
Console.WriteLine($"========== ISAPI报警{dataType.ToUpper()}数据 (设备IP: {ip}) ==========");
|
||||
Console.WriteLine(alarmData);
|
||||
Console.WriteLine("==============================================================\r\n");
|
||||
}
|
||||
|
||||
// 提取图片数据
|
||||
var pictures = new List<AlarmPicture>();
|
||||
if (struAlarm.byPicturesNumber > 0 && struAlarm.pPicPackData != IntPtr.Zero)
|
||||
{
|
||||
int picStructSize = Marshal.SizeOf<CHCNetSDK.NET_DVR_ALARM_ISAPI_PICDATA>();
|
||||
for (int i = 0; i < struAlarm.byPicturesNumber; i++)
|
||||
{
|
||||
IntPtr picPtr = IntPtr.Add(struAlarm.pPicPackData, i * picStructSize);
|
||||
var struPic = Marshal.PtrToStructure<CHCNetSDK.NET_DVR_ALARM_ISAPI_PICDATA>(picPtr);
|
||||
|
||||
if (struPic.dwPicLen > 0 && struPic.pPicData != IntPtr.Zero)
|
||||
{
|
||||
byte[] picBytes = new byte[struPic.dwPicLen];
|
||||
Marshal.Copy(struPic.pPicData, picBytes, 0, (int)struPic.dwPicLen);
|
||||
|
||||
string fileName = Encoding.UTF8.GetString(struPic.szFilename).TrimEnd('\0');
|
||||
pictures.Add(new AlarmPicture
|
||||
{
|
||||
FileName = fileName,
|
||||
Length = struPic.dwPicLen,
|
||||
Base64 = Convert.ToBase64String(picBytes)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("[ISAPI Alarm] Pictures count: {Count}", pictures.Count);
|
||||
}
|
||||
|
||||
// 查找匹配的设备 ID
|
||||
string deviceId = _sessions
|
||||
.FirstOrDefault(s => string.Equals(s.Ip, ip, StringComparison.OrdinalIgnoreCase))
|
||||
?.DeviceId ?? ip;
|
||||
|
||||
// 创建报警记录
|
||||
var record = new AlarmRecord
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
Ip = ip,
|
||||
DataType = dataType,
|
||||
RawData = alarmData,
|
||||
DataLength = struAlarm.dwAlarmDataLen,
|
||||
PictureCount = pictures.Count,
|
||||
Pictures = pictures,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
_alarmStore.Add(record);
|
||||
|
||||
// 更新摄像头最后报警时间
|
||||
var status = _alarmStore.GetCameraStatus(deviceId);
|
||||
if (status != null)
|
||||
{
|
||||
status.LastAlarmTime = record.Timestamp;
|
||||
_alarmStore.UpdateCameraStatus(status);
|
||||
}
|
||||
|
||||
// 通过 SignalR 推送报警数据到前端
|
||||
_ = _notifier.BroadcastHikvisionAlarmAsync(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing ISAPI alarm from {Ip}", ip);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupAsync()
|
||||
{
|
||||
foreach (var session in _sessions)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (session.AlarmHandle >= 0)
|
||||
{
|
||||
await Task.Run(() => CHCNetSDK.NET_DVR_CloseAlarmChan_V30(session.AlarmHandle));
|
||||
_logger.LogInformation("Alarm channel closed: DeviceId={DeviceId}", session.DeviceId);
|
||||
}
|
||||
|
||||
if (session.UserId >= 0)
|
||||
{
|
||||
await Task.Run(() => CHCNetSDK.NET_DVR_Logout(session.UserId));
|
||||
_logger.LogInformation("Logout: DeviceId={DeviceId}", session.DeviceId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error cleaning up session: DeviceId={DeviceId}", session.DeviceId);
|
||||
}
|
||||
}
|
||||
|
||||
_sessions.Clear();
|
||||
|
||||
if (_sdkInitialized)
|
||||
{
|
||||
await Task.Run(() => CHCNetSDK.NET_DVR_Cleanup());
|
||||
_logger.LogInformation("SDK cleanup completed");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetString(byte[] bytes)
|
||||
{
|
||||
if (bytes == null || bytes.Length == 0) return string.Empty;
|
||||
int nullIdx = Array.IndexOf(bytes, (byte)0);
|
||||
return nullIdx >= 0
|
||||
? Encoding.UTF8.GetString(bytes, 0, nullIdx)
|
||||
: Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头会话状态
|
||||
/// </summary>
|
||||
private sealed class CameraSession
|
||||
{
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
public int UserId { get; set; } = -1;
|
||||
public int AlarmHandle { get; set; } = -1;
|
||||
public bool Armed { get; set; }
|
||||
}
|
||||
}
|
||||
51
Services/IHikvisionAlarmStore.cs
Normal file
51
Services/IHikvisionAlarmStore.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using YD_TrainingCompetition_Api.Models;
|
||||
|
||||
namespace YD_TrainingCompetition_Api.Services;
|
||||
|
||||
public interface IHikvisionAlarmStore
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储一条报警记录
|
||||
/// </summary>
|
||||
void Add(AlarmRecord record);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有报警记录(按时间倒序)
|
||||
/// </summary>
|
||||
IReadOnlyCollection<AlarmRecord> GetAll();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定摄像头的报警记录
|
||||
/// </summary>
|
||||
IReadOnlyCollection<AlarmRecord> GetByDeviceId(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取最新一条报警记录
|
||||
/// </summary>
|
||||
AlarmRecord? GetLatest();
|
||||
|
||||
/// <summary>
|
||||
/// 获取最新一条报警记录(按设备)
|
||||
/// </summary>
|
||||
AlarmRecord? GetLatestByDevice(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// 更新摄像头状态
|
||||
/// </summary>
|
||||
void UpdateCameraStatus(HikvisionCameraStatus status);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有摄像头状态
|
||||
/// </summary>
|
||||
IReadOnlyCollection<HikvisionCameraStatus> GetAllCameraStatuses();
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个摄像头状态
|
||||
/// </summary>
|
||||
HikvisionCameraStatus? GetCameraStatus(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// 清空报警记录
|
||||
/// </summary>
|
||||
void ClearAlarms();
|
||||
}
|
||||
@ -7,5 +7,20 @@ public interface IRealtimeNotifier
|
||||
{
|
||||
Task BroadcastTelemetryAsync(DeviceSnapshot snapshot, CancellationToken cancellationToken = default);
|
||||
Task BroadcastCompetitionStateAsync(CompetitionSessionState state, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 通用广播方法,用于向所有客户端发送指定消息
|
||||
/// </summary>
|
||||
Task SendAsync(string method, object? payload, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 广播海康报警记录
|
||||
/// </summary>
|
||||
Task BroadcastHikvisionAlarmAsync(AlarmRecord record, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 广播海康摄像头状态变更
|
||||
/// </summary>
|
||||
Task BroadcastHikvisionCameraStatusAsync(HikvisionCameraStatus status, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
|
||||
@ -23,5 +23,20 @@ public sealed class SignalRRealtimeNotifier : IRealtimeNotifier
|
||||
{
|
||||
return _hubContext.Clients.All.SendAsync("competition-state", state, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendAsync(string method, object? payload, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _hubContext.Clients.All.SendAsync(method, payload ?? new { }, cancellationToken);
|
||||
}
|
||||
|
||||
public Task BroadcastHikvisionAlarmAsync(AlarmRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _hubContext.Clients.All.SendAsync("HikvisionAlarmReceived", record, cancellationToken);
|
||||
}
|
||||
|
||||
public Task BroadcastHikvisionCameraStatusAsync(HikvisionCameraStatus status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _hubContext.Clients.All.SendAsync("HikvisionCameraStatusChanged", status, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,5 +21,22 @@
|
||||
},
|
||||
"Roster": {
|
||||
"FolderPath": "C:\\Users\\tangl\\Desktop\\长征小学版本(2)\\长征小学版本(2)\\长征小学版本\\Jump_window\\bin\\Debug\\秩序册"
|
||||
},
|
||||
"Hikvision": {
|
||||
"Enabled": false,
|
||||
"SdkLogPath": "C:\\SdkLog\\",
|
||||
"AlarmJsonPictureSeparate": 1,
|
||||
"MaxAlarmRecords": 200,
|
||||
"Cameras": [
|
||||
{
|
||||
"DeviceId": "camera-01",
|
||||
"Ip": "192.168.1.64",
|
||||
"Port": 8000,
|
||||
"UserName": "admin",
|
||||
"Password": "",
|
||||
"Channel": 1,
|
||||
"Enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"C:\\Project\\赛事\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj": {}
|
||||
"C:\\Project\\Event\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"C:\\Project\\赛事\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj": {
|
||||
"C:\\Project\\Event\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Project\\赛事\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"projectUniqueName": "C:\\Project\\Event\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"projectName": "YD_TrainingCompetition_Api",
|
||||
"projectPath": "C:\\Project\\赛事\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"projectPath": "C:\\Project\\Event\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"packagesPath": "C:\\Users\\19304\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Project\\赛事\\YD_TrainingCompetition_Api\\obj\\",
|
||||
"outputPath": "C:\\Project\\Event\\YD_TrainingCompetition_Api\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
|
||||
@ -1044,11 +1044,11 @@
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Project\\赛事\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"projectUniqueName": "C:\\Project\\Event\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"projectName": "YD_TrainingCompetition_Api",
|
||||
"projectPath": "C:\\Project\\赛事\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"projectPath": "C:\\Project\\Event\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"packagesPath": "C:\\Users\\19304\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Project\\赛事\\YD_TrainingCompetition_Api\\obj\\",
|
||||
"outputPath": "C:\\Project\\Event\\YD_TrainingCompetition_Api\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "MwWq4leAOS8=",
|
||||
"dgSpecHash": "cuY1YmDuD/k=",
|
||||
"success": true,
|
||||
"projectFilePath": "C:\\Project\\赛事\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"projectFilePath": "C:\\Project\\Event\\YD_TrainingCompetition_Api\\YD_TrainingCompetition_Api.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\19304\\.nuget\\packages\\closedxml\\0.104.0\\closedxml.0.104.0.nupkg.sha512",
|
||||
"C:\\Users\\19304\\.nuget\\packages\\closedxml.parser\\1.2.0\\closedxml.parser.1.2.0.nupkg.sha512",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user