From d1fc94be1c479a06b85edcd1192c3597c4440087 Mon Sep 17 00:00:00 2001 From: wilson Date: Tue, 3 Mar 2026 13:55:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=A4=A7=E5=8D=8E=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=AF=B9=E4=BA=91=E5=8F=B0=E7=A7=BB=E5=8A=A8=E3=80=81?= =?UTF-8?q?=E7=BC=A9=E6=94=BE=E3=80=81=E8=81=9A=E9=9B=86=E3=80=81=E5=85=89?= =?UTF-8?q?=E5=9C=88=E3=80=81=E6=A0=A1=E6=97=B6=E3=80=81=E9=87=8D=E5=90=AF?= =?UTF-8?q?=E7=9A=84=E6=94=AF=E6=8C=81=20=E5=A2=9E=E5=8A=A0=E6=B5=B7?= =?UTF-8?q?=E5=BA=B7=E3=80=81=E5=A4=A7=E5=8D=8E=E5=AF=B9=E9=A2=84=E7=BD=AE?= =?UTF-8?q?=E7=82=B9=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SHH.CameraSdk/Abstractions/IDahuaContext.cs | 14 +++ SHH.CameraSdk/Abstractions/ISyncFeature.cs | 35 ++++-- .../Drivers/DaHua/DahuaVideoSource.cs | 94 ++++++++++++++-- .../DaHua/Features/DahuaPresetProvider.cs | 81 ++++++++++++++ .../DaHua/Features/DahuaPtzProvider.cs | 100 +++++++++++++++++ .../DaHua/Features/DahuaRebootProvider.cs | 44 ++++++++ .../DaHua/Features/DahuaTimeSyncProvider.cs | 101 ++++++++++++++++++ SHH.CameraSdk/Drivers/DaHua/NetSDK.cs | 8 +- .../HikVision/Features/HikPresetProvider.cs | 81 ++++++++++++++ .../HikVision/Features/HikRebootProvider.cs | 18 ++-- .../Drivers/HikVision/HikNativeMethods.cs | 27 ++--- .../Drivers/HikVision/HikVideoSource.cs | 44 ++++---- .../Handlers/PresetControlHandler.cs | 97 +++++++++++++++++ .../Utils/ServiceCollectionExtensions.cs | 1 + SHH.Contracts.Grpc/Payloads/ProtocolCodes.cs | 3 + 15 files changed, 683 insertions(+), 65 deletions(-) create mode 100644 SHH.CameraSdk/Abstractions/IDahuaContext.cs create mode 100644 SHH.CameraSdk/Drivers/DaHua/Features/DahuaPresetProvider.cs create mode 100644 SHH.CameraSdk/Drivers/DaHua/Features/DahuaPtzProvider.cs create mode 100644 SHH.CameraSdk/Drivers/DaHua/Features/DahuaRebootProvider.cs create mode 100644 SHH.CameraSdk/Drivers/DaHua/Features/DahuaTimeSyncProvider.cs create mode 100644 SHH.CameraSdk/Drivers/HikVision/Features/HikPresetProvider.cs create mode 100644 SHH.CameraService/GrpcImpls/Handlers/PresetControlHandler.cs diff --git a/SHH.CameraSdk/Abstractions/IDahuaContext.cs b/SHH.CameraSdk/Abstractions/IDahuaContext.cs new file mode 100644 index 0000000..a1aca99 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/IDahuaContext.cs @@ -0,0 +1,14 @@ +namespace SHH.CameraSdk; + +/// +/// 海康驱动上下文 +/// 作用:允许功能组件(如校时、云台)访问主驱动的核心数据,而无需公开给外部 +/// +public interface IDahuaContext +{ + /// 获取 SDK 登录句柄 (lUserId) + IntPtr GetUserId(); + + /// 获取设备 IP (用于日志) + string GetDeviceIp(); +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/ISyncFeature.cs b/SHH.CameraSdk/Abstractions/ISyncFeature.cs index 74c50d1..321695a 100644 --- a/SHH.CameraSdk/Abstractions/ISyncFeature.cs +++ b/SHH.CameraSdk/Abstractions/ISyncFeature.cs @@ -20,21 +20,38 @@ public interface ITimeSyncFeature /// public interface IRebootFeature { - /// - /// 发送重启指令 - /// - /// 任务完成表示指令发送成功 + /// 发送重启指令 Task RebootAsync(); } -/// -/// 能力接口:云台控制 -/// +/// 能力接口:云台控制 public interface IPtzFeature { - // 原有的手动控制 (按下/松开) + /// 原有的手动控制 (按下/松开) Task PtzControlAsync(PtzAction action, bool stop, int speed = 4); - // [新增] 点动控制 (自动复位) + /// 点动控制 (自动复位) Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4); +} + +/// +/// [功能接口] 预置点管理服务 +/// 核心职责:抽象各品牌 SDK 的预置点操作,包括跳转、保存与删除 +/// +public interface IPresetFeature +{ + /// 跳转到指定预置点 + /// 预置点编号 (通常范围 1-255) + /// 异步任务 + Task GotoPresetAsync(int presetIndex); + + /// 将当前位置保存为预置点 + /// 预置点编号 (若已存在则通常会覆盖) + /// 异步任务 + Task SetPresetAsync(int presetIndex); + + /// 删除指定的预置点 + /// 预置点编号 + /// 异步任务 + Task RemovePresetAsync(int presetIndex); } \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs b/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs index 43a2b4d..367224e 100644 --- a/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs +++ b/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs @@ -2,6 +2,9 @@ using Lennox.LibYuvSharp; using OpenCvSharp; using Serilog; +using SHH.CameraSdk.DahuaFeatures; +using SHH.CameraSdk.HikFeatures; +using System; using System.Runtime.ExceptionServices; using System.Security; using static SHH.CameraSdk.DahuaPlaySDK; @@ -12,12 +15,15 @@ namespace SHH.CameraSdk; /// [大华驱动] 工业级视频源实现 (依照官方 Demo 逻辑重构版) /// 当前模块: AiVideo | 核心原则: 低耦合、高并发、零拷贝 /// -public class DahuaVideoSource : BaseVideoSource +public class DahuaVideoSource : BaseVideoSource, + IDahuaContext, ITimeSyncFeature, IRebootFeature, IPtzFeature, IPresetFeature { - protected override ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.DaHuaSdk); - #region --- 1. 静态资源与回调持有 (Static Resources) --- + /// 大华 SDK 专用日志实例 + protected override ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.DaHuaSdk); + + /// 全局句柄映射表:用于静态异常回调分发至具体实例 private static readonly ConcurrentDictionary _instances = new(); // 必须保持静态引用,防止被 GC 回收导致回调崩溃 @@ -29,6 +35,11 @@ public class DahuaVideoSource : BaseVideoSource #region --- 2. 实例成员 (Instance Members) --- + private readonly DahuaRebootProvider _rebootProvider; + private readonly DahuaTimeSyncProvider _timeProvider; + private readonly DahuaPtzProvider _ptzProvider; + private readonly DahuaPresetProvider _presetProvider; + private IntPtr _loginId = IntPtr.Zero; private IntPtr _realPlayId = IntPtr.Zero; private int _playPort = -1; @@ -42,9 +53,76 @@ public class DahuaVideoSource : BaseVideoSource #endregion - public DahuaVideoSource(VideoSourceConfig config) : base(config) { } + #region --- 3. 构造函数 (Constructor) --- - #region --- 3. 生命周期实现 (Lifecycle Overrides) --- + /// 大华视频源实现 + /// + public DahuaVideoSource(VideoSourceConfig config) : base(config) + { + _rebootProvider = new DahuaRebootProvider(this); + _timeProvider = new DahuaTimeSyncProvider(this); + _ptzProvider = new DahuaPtzProvider(this); + _presetProvider = new DahuaPresetProvider(this); + } + + #endregion + + #region --- 4. 接口实现:IHikContext & Features (Interface Impls) --- + + /// 获取登录句柄 + /// + public IntPtr GetUserId() => _loginId; // 暴露父类或私有的 _loginId + + /// 获取设备IP + /// + public string GetDeviceIp() => Config.IpAddress; + + /// + /// 核心逻辑:全部委托给 _timeProvider 处理,自己不写一行逻辑 + /// + /// + public Task GetTimeAsync() => _timeProvider.GetTimeAsync(); + + /// 设置设备时间 + /// + /// + public Task SetTimeAsync(DateTime time) => _timeProvider.SetTimeAsync(time); + + /// 重启设备 + /// + public Task RebootAsync() => _rebootProvider.RebootAsync(); + + /// PTZ 控制 + /// + /// + /// + /// + public Task PtzControlAsync(PtzAction action, bool stop, int speed = 4) + => _ptzProvider.PtzControlAsync(action, stop, speed); + + /// PTZ 步长 + /// + /// + /// + /// + public Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4) + => _ptzProvider.PtzStepAsync(action, durationMs, speed); + + /// 跳转到预置点 + public Task GotoPresetAsync(int presetIndex) + => _presetProvider.GotoPresetAsync(presetIndex); + + /// 设置/保存当前位置为预置点 + public Task SetPresetAsync(int presetIndex) + => _presetProvider.SetPresetAsync(presetIndex); + + /// 删除预置点 + public Task RemovePresetAsync(int presetIndex) + => _presetProvider.RemovePresetAsync(presetIndex); + + #endregion + + #region --- 5. 生命周期实现 (Lifecycle Overrides) --- protected override async Task OnStartAsync(CancellationToken token) { @@ -137,7 +215,7 @@ public class DahuaVideoSource : BaseVideoSource #endregion - #region --- 4. 核心逻辑:解码与分发 (Core Logic) --- + #region --- 6. 核心逻辑:解码与分发 (Core Logic) --- /// /// 静态回调:分发数据至具体实例 @@ -325,8 +403,8 @@ public class DahuaVideoSource : BaseVideoSource // 如果发现图像发蓝,请将 pU 和 pV 的位置对调 LibYuv.I420ToRGB24( pY, width, - pU, width / 2, pV, width / 2, + pU, width / 2, pDst, width * 3, width, height ); @@ -378,7 +456,7 @@ public class DahuaVideoSource : BaseVideoSource #endregion - #region --- 5. 静态初始化器 (Statics) --- + #region --- 7. 静态初始化器 (Statics) --- private static void InitSdkGlobal() { diff --git a/SHH.CameraSdk/Drivers/DaHua/Features/DahuaPresetProvider.cs b/SHH.CameraSdk/Drivers/DaHua/Features/DahuaPresetProvider.cs new file mode 100644 index 0000000..dbdb970 --- /dev/null +++ b/SHH.CameraSdk/Drivers/DaHua/Features/DahuaPresetProvider.cs @@ -0,0 +1,81 @@ +using Serilog; +using Ayay.SerilogLogs; + +namespace SHH.CameraSdk.DahuaFeatures; + +/// +/// [大华功能组件] 预置点管理实现 +/// 适配说明:使用 NETClient.PTZControl 接口,指令码为 10 (PTZ_POINT_CONTROL) +/// +public class DahuaPresetProvider : IPresetFeature +{ + private ILogger _sdkLog = Log.ForContext("SourceContext", LogModules.DaHuaSdk); + + private readonly IDahuaContext _context; + + // 大华底层预置点控制命令常量 + private const uint PTZ_POINT_CONTROL = 10; + + public DahuaPresetProvider(IDahuaContext context) + { + _context = context; + } + + /// 跳转到预置点 + /// 预置点编号 (1-255) + public async Task GotoPresetAsync(int presetIndex) + { + await ExecutePresetAction(presetIndex, 2, "调用"); + } + + /// 设置/保存当前位置为预置点 + public async Task SetPresetAsync(int presetIndex) + { + await ExecutePresetAction(presetIndex, 0, "保存"); + } + + /// 删除预置点 + public async Task RemovePresetAsync(int presetIndex) + { + await ExecutePresetAction(presetIndex, 1, "删除"); + } + + /// 统一执行预置点动作 + /// 编号 + /// 0:保存, 1:删除, 2:跳转 + /// 日志描述 + private async Task ExecutePresetAction(int index, int actionType, string actionName) + { + IntPtr loginId = _context.GetUserId(); + if (loginId == IntPtr.Zero) return; + + await Task.Run(() => + { + // Modified: [原因] 严格适配 NETClient.PTZControl 的 8 参数签名 + // lParam1: 0 (无意义) + // lParam2: 预置点值 (nIndex) + // lParam3: 动作类型 (0-保存, 1-删除, 2-跳转) + bool result = NETClient.PTZControl( + loginId, + 0, // nChannelID + PTZ_POINT_CONTROL, // dwPTZCommand = 10 + 0, // lParam1 + index, // lParam2: 预置点编号 + actionType, // lParam3: 动作类型 + false, // dwStop: 预置点操作不涉及停止位 + IntPtr.Zero // param4 + ); + + if (!result) + { + string error = NETClient.GetLastError(); + _sdkLog.Warning("[SDK] Dahua 预置点{Action}失败. Index: {Index}, Error: {Error}", + actionName, index, error); + } + else + { + _sdkLog.Debug("[SDK] Dahua 预置点{Action}成功. Index: {Index}", actionName, index); + } + }); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/DaHua/Features/DahuaPtzProvider.cs b/SHH.CameraSdk/Drivers/DaHua/Features/DahuaPtzProvider.cs new file mode 100644 index 0000000..328d234 --- /dev/null +++ b/SHH.CameraSdk/Drivers/DaHua/Features/DahuaPtzProvider.cs @@ -0,0 +1,100 @@ +using Ayay.SerilogLogs; +using Serilog; +using SHH.CameraSdk.HikFeatures; + +namespace SHH.CameraSdk.DahuaFeatures; + +/// +/// [大华功能组件] 云台与镜头控制 +/// 适配说明:严格匹配 NETClient.PTZControl(IntPtr, int, uint, int, int, int, bool, IntPtr) 接口 +/// +public class DahuaPtzProvider : IPtzFeature +{ + private readonly IDahuaContext _context; + + #region --- 大华 PTZ 命令常量 (对应 dwPTZCommand) --- + + private const uint PTZ_UP = 0; + private const uint PTZ_DOWN = 1; + private const uint PTZ_LEFT = 2; + private const uint PTZ_RIGHT = 3; + private const uint PTZ_ZOOM_ADD = 4; // 变倍+ + private const uint PTZ_ZOOM_DEC = 5; // 变倍- + private const uint PTZ_FOCUS_ADD = 6; // 聚焦+ + private const uint PTZ_FOCUS_DEC = 7; // 聚焦- + private const uint PTZ_IRIS_ADD = 8; // 光圈+ + private const uint PTZ_IRIS_DEC = 9; // 光圈- + + #endregion + + public DahuaPtzProvider(IDahuaContext context) + { + _context = context; + } + + public async Task PtzControlAsync(PtzAction action, bool stop, int speed) + { + IntPtr loginId = _context.GetUserId(); + if (loginId == IntPtr.Zero) return; + + await Task.Run(() => + { + // 1. 映射指令 + uint dwCommand = action switch + { + PtzAction.Up => PTZ_UP, + PtzAction.Down => PTZ_DOWN, + PtzAction.Left => PTZ_LEFT, + PtzAction.Right => PTZ_RIGHT, + PtzAction.ZoomIn => PTZ_ZOOM_ADD, + PtzAction.ZoomOut => PTZ_ZOOM_DEC, + PtzAction.FocusFar => PTZ_FOCUS_ADD, + PtzAction.FocusNear => PTZ_FOCUS_DEC, + PtzAction.IrisOpen => PTZ_IRIS_ADD, + PtzAction.IrisClose => PTZ_IRIS_DEC, + _ => 999 + }; + + if (dwCommand == 999) return; + + // 2. 准备速度参数 (大华一般 1-8) + // Modified: [原因] 严格适配 8 参数接口。lParam1=水平速度, lParam2=垂直速度, lParam3=0 + int s = Math.Clamp(speed, 1, 8); + int lParam1 = s; + int lParam2 = s; + int lParam3 = 0; + + // 3. 调用你提供的接口 + // Modified: [原因] 匹配签名: (IntPtr, int, uint, int, int, int, bool, IntPtr) + bool result = NETClient.PTZControl( + loginId, + 0, // nChannelID + dwCommand, // dwPTZCommand + lParam1, + lParam2, + lParam3, + stop, // dwStop + IntPtr.Zero // param4 + ); + + if (!result) + { + string error = NETClient.GetLastError(); + Log.ForContext("SourceContext", LogModules.DaHuaSdk) + .Warning("[SDK] Dahua PTZ 失败. Action: {Action}, Stop: {Stop}, Error: {Error}, 可能操作太快.", + action, stop, error); + } + else + { + + } + }); + } + + public async Task PtzStepAsync(PtzAction action, int durationMs, int speed) + { + await PtzControlAsync(action, false, speed); + await Task.Delay(durationMs); + await PtzControlAsync(action, true, speed); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/DaHua/Features/DahuaRebootProvider.cs b/SHH.CameraSdk/Drivers/DaHua/Features/DahuaRebootProvider.cs new file mode 100644 index 0000000..7d5d3b6 --- /dev/null +++ b/SHH.CameraSdk/Drivers/DaHua/Features/DahuaRebootProvider.cs @@ -0,0 +1,44 @@ +using Serilog; +using Ayay.SerilogLogs; + +namespace SHH.CameraSdk.DahuaFeatures; + +/// +/// [大华功能组件] 远程重启实现 +/// +public class DahuaRebootProvider : IRebootFeature +{ + private ILogger _sdkLog = Log.ForContext("SourceContext", LogModules.DaHuaSdk); + + private readonly IDahuaContext _context; + + public DahuaRebootProvider(IDahuaContext context) + { + _context = context; + } + + /// 执行异步重启 + public async Task RebootAsync() + { + // 1. 检查登录状态 (参照海康逻辑) + IntPtr loginId = _context.GetUserId(); + if (loginId == IntPtr.Zero) + throw new InvalidOperationException("大华设备未登录或句柄失效,无法发送重启指令"); + + // 2. 执行 SDK 调用 + await Task.Run(() => + { + bool result = NETClient.ControlDevice(loginId, EM_CtrlType.REBOOT, IntPtr.Zero, 5000); + + if (!result) + { + string err = NETClient.GetLastError(); + _sdkLog.Error("[SDK] Dahua 重启指令下发失败. Error: {Error}", err); + + throw new Exception($"大华重启指令发送失败,错误码: {err}"); + } + + _sdkLog.Information("[SDK] Dahua 重启指令下发成功,设备即将断开连接。"); + }); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/DaHua/Features/DahuaTimeSyncProvider.cs b/SHH.CameraSdk/Drivers/DaHua/Features/DahuaTimeSyncProvider.cs new file mode 100644 index 0000000..5d6b665 --- /dev/null +++ b/SHH.CameraSdk/Drivers/DaHua/Features/DahuaTimeSyncProvider.cs @@ -0,0 +1,101 @@ +using System.Runtime.InteropServices; +using Serilog; +using Ayay.SerilogLogs; + +namespace SHH.CameraSdk.DahuaFeatures; + +/// +/// [大华功能组件] 时间同步实现 +/// 参照原代码逻辑重构,实现 EM_DEV_CFG_TYPE.TIMECFG 配置下发 +/// +public class DahuaTimeSyncProvider : ITimeSyncFeature +{ + private readonly IDahuaContext _context; + + public DahuaTimeSyncProvider(IDahuaContext context) + { + _context = context; + } + + /// + /// 获取设备当前时间 + /// + public async Task GetTimeAsync() + { + IntPtr loginId = _context.GetUserId(); + if (loginId == IntPtr.Zero) throw new InvalidOperationException("大华设备未登录"); + + return await Task.Run(() => + { + NET_TIME time = new NET_TIME(); + uint retLen = 0; + int nSize = Marshal.SizeOf(typeof(NET_TIME)); + IntPtr inPtr = Marshal.AllocHGlobal(nSize); + + try + { + // Optimized: [原因] 沿用原代码的 GetDevConfig 逻辑与 TIMECFG 指令 + Marshal.StructureToPtr(time, inPtr, true); + bool result = NETClient.GetDevConfig(loginId, EM_DEV_CFG_TYPE.TIMECFG, -1, inPtr, (uint)nSize, ref retLen, 5000); + + if (result && retLen == (uint)nSize) + { + time = (NET_TIME)Marshal.PtrToStructure(inPtr, typeof(NET_TIME)); + // 使用你现有的 ToDateTime() 扩展方法 + return time.ToDateTime(); + } + else + { + string err = NETClient.GetLastError(); + throw new Exception($"[SDK] Dahua 获取时间失败: {err}"); + } + } + finally + { + Marshal.FreeHGlobal(inPtr); + } + }); + } + + /// + /// 设置设备时间 (校时) + /// + public async Task SetTimeAsync(DateTime targetTime) + { + IntPtr loginId = _context.GetUserId(); + if (loginId == IntPtr.Zero) throw new InvalidOperationException("大华设备未登录"); + + await Task.Run(() => + { + // Modified: [原因] 使用你原有的 FromDateTime 静态方法进行结构体转换 + NET_TIME time = NET_TIME.FromDateTime(targetTime); + int nSize = Marshal.SizeOf(typeof(NET_TIME)); + IntPtr inPtr = Marshal.AllocHGlobal(nSize); + + try + { + Marshal.StructureToPtr(time, inPtr, true); + + // Optimized: [原因] 沿用原代码的 SetDevConfig 逻辑 + bool result = NETClient.SetDevConfig(loginId, EM_DEV_CFG_TYPE.TIMECFG, -1, inPtr, (uint)nSize, 5000); + + if (result) + { + Log.ForContext("SourceContext", LogModules.DaHuaSdk) + .Information("[SDK] Dahua 校时成功 => {Time}", targetTime.ToString("yyyy-MM-dd HH:mm:ss")); + } + else + { + string err = NETClient.GetLastError(); + Log.ForContext("SourceContext", LogModules.DaHuaSdk) + .Error("[SDK] Dahua 校时指令失败. Error: {Error}", err); + throw new Exception($"大华校时失败: {err}"); + } + } + finally + { + Marshal.FreeHGlobal(inPtr); + } + }); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/DaHua/NetSDK.cs b/SHH.CameraSdk/Drivers/DaHua/NetSDK.cs index 4c2be82..eabaae2 100644 --- a/SHH.CameraSdk/Drivers/DaHua/NetSDK.cs +++ b/SHH.CameraSdk/Drivers/DaHua/NetSDK.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Runtime.InteropServices; -using System.ComponentModel; - -namespace SHH.CameraSdk +namespace SHH.CameraSdk { public static class NETClient { diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/HikPresetProvider.cs b/SHH.CameraSdk/Drivers/HikVision/Features/HikPresetProvider.cs new file mode 100644 index 0000000..f1c66c9 --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/Features/HikPresetProvider.cs @@ -0,0 +1,81 @@ +using Serilog; +using Ayay.SerilogLogs; + +namespace SHH.CameraSdk.HikFeatures; + +/// +/// [海康功能组件] 预置点管理实现 +/// 适配说明:使用 NET_DVR_PTZPreset_Other 接口 +/// +public class HikPresetProvider : IPresetFeature +{ + private readonly IHikContext _context; + + #region --- 海康 PTZ 预置点命令常量 --- + + private const uint SET_PRESET = 8; // 设置预置点 + private const uint CLE_PRESET = 9; // 清除预置点 + private const uint GOTO_PRESET = 39; // 转到预置点 + + #endregion + + public HikPresetProvider(IHikContext context) + { + _context = context; + } + + /// 跳转到指定预置点 + public async Task GotoPresetAsync(int presetIndex) + { + await ExecutePresetAction(presetIndex, GOTO_PRESET, "调用"); + } + + /// 将当前位置保存为预置点 + public async Task SetPresetAsync(int presetIndex) + { + await ExecutePresetAction(presetIndex, SET_PRESET, "保存"); + } + + /// 删除指定的预置点 + public async Task RemovePresetAsync(int presetIndex) + { + await ExecutePresetAction(presetIndex, CLE_PRESET, "删除"); + } + + /// 统一执行海康预置点 SDK 调用 + private async Task ExecutePresetAction(int index, uint command, string actionName) + { + int userId = _context.GetUserId(); + if (userId < 0) return; + + // 位置是从 1 开始, 调用 0 会导致设备重启 + if (index == 0) return; + + // 海康工业相机通道号通常为 1 + int channel = 1; + + await Task.Run(() => + { + // Optimized: [原因] 使用 _Other 接口确保在单机多路并发下通道句柄准确 + bool result = HikNativeMethods.NET_DVR_PTZPreset_Other( + userId, + channel, + command, + (uint)index + ); + + if (!result) + { + uint err = HikNativeMethods.NET_DVR_GetLastError(); + Log.ForContext("SourceContext", LogModules.HikVisionSdk) + .Warning("[SDK] Hik 预置点{Action}失败. Index: {Index}, Error: {Error}", + actionName, index, err); + } + else + { + Log.ForContext("SourceContext", LogModules.HikVisionSdk) + .Debug("[SDK] Hik 预置点{Action}成功. Index: {Index}", actionName, index); + } + }); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs b/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs index b601b2a..c2866d6 100644 --- a/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs +++ b/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs @@ -1,7 +1,12 @@ -namespace SHH.CameraSdk.HikFeatures; +using Ayay.SerilogLogs; +using Serilog; + +namespace SHH.CameraSdk.HikFeatures; public class HikRebootProvider : IRebootFeature { + private ILogger _sdkLog = Log.ForContext("SourceContext", LogModules.HikVisionSdk); + private readonly IHikContext _context; public HikRebootProvider(IHikContext context) @@ -9,6 +14,7 @@ public class HikRebootProvider : IRebootFeature _context = context; } + /// 执行异步重启 public async Task RebootAsync() { // 1. 检查登录状态 @@ -23,14 +29,12 @@ public class HikRebootProvider : IRebootFeature if (!result) { uint err = HikNativeMethods.NET_DVR_GetLastError(); + _sdkLog.Error("[SDK] Hik 重启指令下发失败. Error: {Error}", err); + throw new Exception($"重启指令发送失败,错误码: {err}"); } - }); - // 3. 注意: - // 重启指令发送成功后,设备会断开网络。 - // 宿主类(HikVideoSource)的保活机制(KeepAlive)会检测到断线, - // 并自动开始尝试重连,直到设备重启完成上线。 - // 所以这里我们不需要手动断开连接,交给底层自愈机制即可。 + _sdkLog.Information("[SDK] Hik 重启指令下发成功,设备即将断开连接。"); + }); } } \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs b/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs index 63af1b0..8aa2b47 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs @@ -349,18 +349,14 @@ public static partial class HikNativeMethods [DllImport(DllName)] public static extern uint NET_DVR_GetLastError(); - /// - /// 设置网络连接超时时间和连接尝试次数 - /// + /// 设置网络连接超时时间和连接尝试次数 /// 超时时间(毫秒),推荐 3000ms /// 连接尝试次数,推荐 1 次 /// 设置成功返回 true,失败返回 false [DllImport(DllName)] public static extern bool NET_DVR_SetConnectTime(uint dwWaitTime, uint dwTryTimes); - /// - /// 设置自动重连功能 - /// + /// 设置自动重连功能 /// 重连间隔(毫秒),推荐 10000ms /// 是否启用重连:0-禁用,1-启用 /// 设置成功返回 true,失败返回 false @@ -461,9 +457,7 @@ public static partial class HikNativeMethods #region --- 异常回调接口 (Exception Callback Interfaces) --- - /// - /// 设置连接超时时间和重连策略(兼容旧版本) - /// + /// 设置连接超时时间和重连策略(兼容旧版本) /// 重连间隔(毫秒),建议 3000 /// 是否启用重连:1-启用,0-禁用 /// 设置成功返回 true,失败返回 false @@ -512,11 +506,20 @@ public static partial class HikNativeMethods [DllImport(DllName)] public static extern bool NET_DVR_SetDVRConfig(int lUserID, uint dwCommand, int lChannel, System.IntPtr lpInBuffer, uint dwInBufferSize); - /// - /// 设备重启 - /// + /// 设备重启 /// /// [DllImport(DllName)] public static extern bool NET_DVR_RebootDVR(int lUserID); + + /// + /// [海康 SDK 调用] 云台预置点配置(扩展) + /// + /// NET_DVR_Login_V40 的返回值 + /// 通道号 (工业相机通常为 1) + /// 预置点操作命令 (见下文枚举) + /// 预置点序号 (1~255) + /// TRUE表示成功,FALSE表示失败 + [DllImport(DllName)] + public static extern bool NET_DVR_PTZPreset_Other(int lUserID, int lChannel, uint dwPTZPresetCmd, uint dwPresetIndex); } \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index cff1bd8..75349a0 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -19,7 +19,7 @@ namespace SHH.CameraSdk; /// ✅ 4. [Feat C] 性能优化:在解码回调中使用 竞争锁,有效规避在设备断开瞬间可能产生的驱动层死锁 /// public class HikVideoSource : BaseVideoSource, - IHikContext, ITimeSyncFeature, IRebootFeature, IPtzFeature + IHikContext, ITimeSyncFeature, IRebootFeature, IPtzFeature, IPresetFeature { #region --- 1. 静态资源与全局路由 (Static Resources) --- @@ -43,6 +43,7 @@ public class HikVideoSource : BaseVideoSource, private readonly HikTimeSyncProvider _timeProvider; private readonly HikRebootProvider _rebootProvider; private readonly HikPtzProvider _ptzProvider; + private readonly HikPresetProvider _presetProvider; // SDK 句柄与资源 private int _userId = -1; // SDK 登录句柄 @@ -68,9 +69,7 @@ public class HikVideoSource : BaseVideoSource, #region --- 3. 构造函数 (Constructor) --- - /// - /// 海康视频源实现 - /// + /// 海康视频源实现 /// public HikVideoSource(VideoSourceConfig config) : base(config) { @@ -78,6 +77,7 @@ public class HikVideoSource : BaseVideoSource, _timeProvider = new HikTimeSyncProvider(this); _rebootProvider = new HikRebootProvider(this); _ptzProvider = new HikPtzProvider(this); + _presetProvider = new HikPresetProvider(this); // Modified: [Fix GC Crash] 移除此处的 new REALDATACALLBACK // 直接使用构造函数初始化的 _realDataCallBack,保证委托地址在整个对象生命周期内不变 @@ -89,15 +89,11 @@ public class HikVideoSource : BaseVideoSource, #region --- 4. 接口实现:IHikContext & Features (Interface Impls) --- - /// - /// 获取登录句柄 - /// + /// 获取登录句柄 /// public int GetUserId() => _userId; // 暴露父类或私有的 _userId - /// - /// 获取设备IP - /// + /// 获取设备IP /// public string GetDeviceIp() => Config.IpAddress; @@ -107,22 +103,16 @@ public class HikVideoSource : BaseVideoSource, /// public Task GetTimeAsync() => _timeProvider.GetTimeAsync(); - /// - /// 设置设备时间 - /// + /// 设置设备时间 /// /// public Task SetTimeAsync(DateTime time) => _timeProvider.SetTimeAsync(time); - /// - /// 重启设备 - /// + /// 重启设备 /// public Task RebootAsync() => _rebootProvider.RebootAsync(); - /// - /// PTZ 控制 - /// + /// PTZ 控制 /// /// /// @@ -130,9 +120,7 @@ public class HikVideoSource : BaseVideoSource, public Task PtzControlAsync(PtzAction action, bool stop, int speed = 4) => _ptzProvider.PtzControlAsync(action, stop, speed); - /// - /// PTZ 步长 - /// + /// PTZ 步长 /// /// /// @@ -140,6 +128,18 @@ public class HikVideoSource : BaseVideoSource, public Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4) => _ptzProvider.PtzStepAsync(action, durationMs, speed); + /// 跳转到预置点 + public Task GotoPresetAsync(int presetIndex) + => _presetProvider.GotoPresetAsync(presetIndex); + + /// 设置/保存当前位置为预置点 + public Task SetPresetAsync(int presetIndex) + => _presetProvider.SetPresetAsync(presetIndex); + + /// 删除预置点 + public Task RemovePresetAsync(int presetIndex) + => _presetProvider.RemovePresetAsync(presetIndex); + #endregion #region --- 5. 生命周期重写 (Lifecycle Overrides) --- diff --git a/SHH.CameraService/GrpcImpls/Handlers/PresetControlHandler.cs b/SHH.CameraService/GrpcImpls/Handlers/PresetControlHandler.cs new file mode 100644 index 0000000..f521dba --- /dev/null +++ b/SHH.CameraService/GrpcImpls/Handlers/PresetControlHandler.cs @@ -0,0 +1,97 @@ +using Ayay.SerilogLogs; +using Newtonsoft.Json.Linq; +using Serilog; +using SHH.CameraSdk; +using SHH.Contracts; + +namespace SHH.CameraService; + +/// +/// 预置点控制指令处理器 +/// 响应 gRpc 指令:ProtocolCodes.Preset_Control +/// +public class PresetControlHandler : ICommandHandler +{ + private readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + private readonly CameraManager _cameraManager; + + /// 指令名称(需与网关下发的 CmdCode 一致) + public string ActionName => ProtocolCodes.Device_Preset; + + public PresetControlHandler(CameraManager cameraManager) + { + _cameraManager = cameraManager ?? throw new ArgumentNullException(nameof(cameraManager)); + } + + public async Task ExecuteAsync(JToken payload) + { + // 1. 解析预置点控制参数 + // 假设 PresetControlDto 包含 DeviceId, PresetIndex, 和 Action (GOTO/SET/REMOVE) + var presetDto = payload.ToObject(); + if (presetDto == null || presetDto.DeviceId <= 0) + { + _sysLog.Warning("[Preset] 无效指令:参数缺失或设备ID非法"); + return; + } + + // 2. 获取目标设备并校验能力 + var device = _cameraManager.GetDevice(presetDto.DeviceId); + if (device == null) + { + _sysLog.Warning($"[Preset] 设备 {presetDto.DeviceId} 不存在"); + return; + } + if (!device.IsPhysicalOnline) + { + _sysLog.Warning($"[Preset] 设备 {presetDto.DeviceId} 未在线,无法执行预置点控制"); + return; + } + + // Optimized: [原因] 检查设备是否实现了预置点功能接口 + if (!(device is IPresetFeature presetFeature)) + { + _sysLog.Warning($"[Preset] 设备 {presetDto.DeviceId} ({device.Config.Name}) 不支持预置点功能"); + return; + } + + // 3. 分发执行逻辑 + try + { + switch (presetDto.Action.ToUpper()) + { + case "GOTO": + await presetFeature.GotoPresetAsync(presetDto.PresetIndex); + _sysLog.Information($"[Preset] 设备 {presetDto.DeviceId} 跳转至预置点: {presetDto.PresetIndex}"); + break; + case "SET": + await presetFeature.SetPresetAsync(presetDto.PresetIndex); + _sysLog.Information($"[Preset] 设备 {presetDto.DeviceId} 设置当前位置为预置点: {presetDto.PresetIndex}"); + break; + case "REMOVE": + await presetFeature.RemovePresetAsync(presetDto.PresetIndex); + _sysLog.Information($"[Preset] 设备 {presetDto.DeviceId} 删除预置点: {presetDto.PresetIndex}"); + break; + default: + _sysLog.Warning($"[Preset] 未知操作类型: {presetDto.Action}"); + break; + } + } + catch (Exception ex) + { + _sysLog.Error(ex, $"[Preset] 设备 {presetDto.DeviceId} 预置点操作失败"); + } + } +} + +/// PresetControlDto 参数 +public class PresetControlDto +{ + /// 设备ID + public int DeviceId { get; set; } + + /// 预置点编号 (1-255) + public int PresetIndex { get; set; } + + /// 动作:GOTO, SET, REMOVE + public string Action { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/SHH.CameraService/Utils/ServiceCollectionExtensions.cs b/SHH.CameraService/Utils/ServiceCollectionExtensions.cs index 8bedcd5..020fe36 100644 --- a/SHH.CameraService/Utils/ServiceCollectionExtensions.cs +++ b/SHH.CameraService/Utils/ServiceCollectionExtensions.cs @@ -62,6 +62,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } #endregion diff --git a/SHH.Contracts.Grpc/Payloads/ProtocolCodes.cs b/SHH.Contracts.Grpc/Payloads/ProtocolCodes.cs index 4d29643..671b5e7 100644 --- a/SHH.Contracts.Grpc/Payloads/ProtocolCodes.cs +++ b/SHH.Contracts.Grpc/Payloads/ProtocolCodes.cs @@ -51,6 +51,9 @@ /// 时间同步指令 public static string Device_TimeSync { get; } = "Device_TimeSync"; + /// 预置点控制指令 + public static string Device_Preset { get; } = "Device_Preset"; + #endregion } } \ No newline at end of file