add:海康sdk

This commit is contained in:
ltx 2026-07-02 10:05:51 +08:00
parent 92df3bf9ba
commit 5e27c2aa7c
16 changed files with 21300 additions and 10 deletions

View 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

File diff suppressed because it is too large Load Diff

63
Models/AlarmRecord.cs Normal file
View 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
View 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;
}

View 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; }
}

View 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;
}

View File

@ -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();

View 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");
}
}

View 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; }
}
}

View 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();
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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
}
]
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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",