diff --git a/SHH.CameraSdk/Abstractions/IHikContext.cs b/SHH.CameraSdk/Abstractions/IHikContext.cs new file mode 100644 index 0000000..9989a1d --- /dev/null +++ b/SHH.CameraSdk/Abstractions/IHikContext.cs @@ -0,0 +1,14 @@ +namespace SHH.CameraSdk; + +/// +/// 海康驱动上下文 +/// 作用:允许功能组件(如校时、云台)访问主驱动的核心数据,而无需公开给外部 +/// +public interface IHikContext +{ + /// 获取 SDK 登录句柄 (lUserId) + int GetUserId(); + + /// 获取设备 IP (用于日志) + string GetDeviceIp(); +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/ISyncFeature.cs b/SHH.CameraSdk/Abstractions/ISyncFeature.cs new file mode 100644 index 0000000..74c50d1 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/ISyncFeature.cs @@ -0,0 +1,40 @@ +using SHH.CameraSdk.HikFeatures; + +namespace SHH.CameraSdk; + +/// +/// 能力接口:时间同步 +/// 只有实现了此接口的设备,才支持 WebAPI 的时间查询与设置 +/// +public interface ITimeSyncFeature +{ + /// 获取设备当前时间 + Task GetTimeAsync(); + + /// 设置设备时间 + Task SetTimeAsync(DateTime time); +} + +/// +/// 能力接口:设备重启 +/// +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); +} \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs index 33c446b..22c0c98 100644 --- a/SHH.CameraSdk/Controllers/CamerasController.cs +++ b/SHH.CameraSdk/Controllers/CamerasController.cs @@ -399,4 +399,126 @@ public class CamerasController : ControllerBase // 3. 返回 JSON 给前端 return Ok(options); } + + /// + /// 获取设备时间 (支持海康/大华等具备此能力的设备) + /// + [HttpGet("{id}/time")] + public async Task GetDeviceTime(long id) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(new { error = "Device not found" }); + + if (!device.IsPhysicalOnline) return BadRequest(new { error = "Device is offline" }); + + // 【核心】模式匹配:判断这个设备是否实现了“时间同步接口” + if (device is ITimeSyncFeature timeFeature) + { + try + { + var time = await timeFeature.GetTimeAsync(); + return Ok(new { deviceId = id, currentTime = time }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = ex.Message }); + } + } + + // 如果是 RTSP/USB 等不支持的设备 + return BadRequest(new { error = "This device does not support time synchronization." }); + } + + /// + /// 设置设备时间 + /// + [HttpPost("{id}/time")] + public async Task SetDeviceTime(long id, [FromBody] DateTime time) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(); + + if (device is ITimeSyncFeature timeFeature) + { + try + { + await timeFeature.SetTimeAsync(time); + return Ok(new { success = true, message = $"Time synced to {time}" }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = ex.Message }); + } + } + + return BadRequest(new { error = "This device does not support time synchronization." }); + } + + /// + /// 远程重启设备 + /// + [HttpPost("{id}/reboot")] + public async Task RebootDevice(long id) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(new { error = "Device not found" }); + + // 依然是两道防线:先检查在线,再检查能力 + if (!device.IsOnline) return BadRequest(new { error = "Device is offline" }); + + if (device is IRebootFeature rebootFeature) + { + try + { + await rebootFeature.RebootAsync(); + + // 记录审计日志 (建议加上) + device.AddAuditLog("用户执行了远程重启"); + + return Ok(new { success = true, message = "重启指令已发送,设备将在几分钟后重新上线。" }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = ex.Message }); + } + } + + return BadRequest(new { error = "This device does not support remote reboot." }); + } + + [HttpPost("{id}/ptz")] + public async Task PtzControl(long id, [FromBody] PtzControlDto dto) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(); + if (!device.IsOnline) return BadRequest("Device offline"); + + if (device is IPtzFeature ptz) + { + try + { + // 逻辑分流 + if (dto.Duration > 0) + { + // 场景:点动模式 (一次调用,自动停止) + // 建议限制一下最大时长,防止前端传个 10000秒 导致云台转疯了 + int safeDuration = Math.Clamp(dto.Duration, 50, 2000); + await ptz.PtzStepAsync(dto.Action, safeDuration, dto.Speed); + } + else + { + // 场景:手动模式 (按下/松开) + await ptz.PtzControlAsync(dto.Action, dto.Stop, dto.Speed); + } + + return Ok(); + } + catch (Exception ex) + { + return StatusCode(500, new { error = ex.Message }); + }// + } + + return BadRequest("Device does not support PTZ."); + } } \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/Dto/PtzControlDto.cs b/SHH.CameraSdk/Controllers/Dto/PtzControlDto.cs new file mode 100644 index 0000000..9e7f175 --- /dev/null +++ b/SHH.CameraSdk/Controllers/Dto/PtzControlDto.cs @@ -0,0 +1,22 @@ +using SHH.CameraSdk.HikFeatures; + +namespace SHH.CameraSdk; + +public class PtzControlDto +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public PtzAction Action { get; set; } + + /// + /// 仅用于手动模式:true=停止, false=开始 + /// + public bool Stop { get; set; } + + public int Speed { get; set; } = 4; + + /// + /// [新增] 点动耗时 (毫秒) + /// 如果此值 > 0,则忽略 Stop 参数,执行 "开始 -> 等待 -> 停止" 的原子操作 + /// + public int Duration { get; set; } = 0; +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/FeaturesEnums.cs b/SHH.CameraSdk/Drivers/HikVision/Features/FeaturesEnums.cs new file mode 100644 index 0000000..6bf8e09 --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/Features/FeaturesEnums.cs @@ -0,0 +1,19 @@ +namespace SHH.CameraSdk.HikFeatures; + +/// +/// 云台动作枚举 +/// +public enum PtzAction +{ + Up, + Down, + Left, + Right, + ZoomIn, // 放大 + ZoomOut, // 缩小 + FocusNear, // 聚焦近 + FocusFar, // 聚焦远 + IrisOpen, // 光圈大 + IrisClose, // 光圈小 + Wiper, // 雨刷 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/HikPtzProvider.cs b/SHH.CameraSdk/Drivers/HikVision/Features/HikPtzProvider.cs new file mode 100644 index 0000000..580f8ec --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/Features/HikPtzProvider.cs @@ -0,0 +1,71 @@ +namespace SHH.CameraSdk.HikFeatures; + +public class HikPtzProvider : IPtzFeature +{ + private readonly IHikContext _context; + + public HikPtzProvider(IHikContext context) + { + _context = context; + } + + public async Task PtzControlAsync(PtzAction action, bool stop, int speed) + { + int userId = _context.GetUserId(); + if (userId < 0) throw new InvalidOperationException("设备离线"); + + // 1. 映射指令 + uint hikCommand = action switch + { + PtzAction.Up => HikNativeMethods.TILT_UP, + PtzAction.Down => HikNativeMethods.TILT_DOWN, + PtzAction.Left => HikNativeMethods.PAN_LEFT, + PtzAction.Right => HikNativeMethods.PAN_RIGHT, + PtzAction.ZoomIn => HikNativeMethods.ZOOM_IN, + PtzAction.ZoomOut => HikNativeMethods.ZOOM_OUT, + PtzAction.FocusNear => HikNativeMethods.FOCUS_NEAR, + PtzAction.FocusFar => HikNativeMethods.FOCUS_FAR, + PtzAction.IrisOpen => HikNativeMethods.IRIS_OPEN, + PtzAction.IrisClose => HikNativeMethods.IRIS_CLOSE, + PtzAction.Wiper => HikNativeMethods.WIPER_PWRON, + _ => 0 + }; + + if (hikCommand == 0) return; + + // 2. 转换停止标志 (海康: 0=开始, 1=停止) + uint dwStop = stop ? 1u : 0u; + + // 3. 限制速度范围 (1-7) + uint dwSpeed = (uint)Math.Clamp(speed, 1, 7); + + // 4. 调用 SDK + await Task.Run(() => + { + // Channel 默认为 1 + bool result = HikNativeMethods.NET_DVR_PTZControlWithSpeed_Other(userId, 1, hikCommand, dwStop, dwSpeed); + + if (!result) + { + // 这里通常不抛异常,因为云台繁忙或到头是常态,记录日志即可 + // Console.WriteLine($"PTZ Error: {HikNativeMethods.NET_DVR_GetLastError()}"); + } + }); + } + + public async Task PtzStepAsync(PtzAction action, int durationMs, int speed) + { + // 1. 开始转动 (调用已有的逻辑,stop=false) + await PtzControlAsync(action, false, speed); + + // 2. 等待指定时间 (非阻塞等待) + // 注意:这里使用 Task.Delay,精度对云台控制来说足够了 + if (durationMs > 0) + { + await Task.Delay(durationMs); + } + + // 3. 停止转动 (调用已有的逻辑,stop=true) + await PtzControlAsync(action, true, speed); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs b/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs new file mode 100644 index 0000000..b601b2a --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/Features/HikRebootProvider.cs @@ -0,0 +1,36 @@ +namespace SHH.CameraSdk.HikFeatures; + +public class HikRebootProvider : IRebootFeature +{ + private readonly IHikContext _context; + + public HikRebootProvider(IHikContext context) + { + _context = context; + } + + public async Task RebootAsync() + { + // 1. 检查登录状态 + int userId = _context.GetUserId(); + if (userId < 0) throw new InvalidOperationException("设备未登录或离线,无法发送重启指令"); + + // 2. 执行 SDK 调用 + await Task.Run(() => + { + bool result = HikNativeMethods.NET_DVR_RebootDVR(userId); + + if (!result) + { + uint err = HikNativeMethods.NET_DVR_GetLastError(); + throw new Exception($"重启指令发送失败,错误码: {err}"); + } + }); + + // 3. 注意: + // 重启指令发送成功后,设备会断开网络。 + // 宿主类(HikVideoSource)的保活机制(KeepAlive)会检测到断线, + // 并自动开始尝试重连,直到设备重启完成上线。 + // 所以这里我们不需要手动断开连接,交给底层自愈机制即可。 + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/Features/HikTimeSyncProvider.cs b/SHH.CameraSdk/Drivers/HikVision/Features/HikTimeSyncProvider.cs new file mode 100644 index 0000000..3f5ce3e --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/Features/HikTimeSyncProvider.cs @@ -0,0 +1,98 @@ +namespace SHH.CameraSdk.HikFeatures; + +/// +/// 海康时间同步组件 +/// +public class HikTimeSyncProvider : ITimeSyncFeature +{ + private readonly IHikContext _context; + + public HikTimeSyncProvider(IHikContext context) + { + _context = context; + } + + public async Task GetTimeAsync() + { + // 1. 获取句柄 + int userId = _context.GetUserId(); + if (userId < 0) throw new InvalidOperationException("设备未登录"); + + return await Task.Run(() => + { + uint returned = 0; + uint size = (uint)Marshal.SizeOf(typeof(HikNativeMethods.NET_DVR_TIME)); + IntPtr ptr = Marshal.AllocHGlobal((int)size); + + try + { + // 调用 SDK: NET_DVR_GET_TIME (命令号 118) + bool result = HikNativeMethods.NET_DVR_GetDVRConfig( + userId, + HikNativeMethods.NET_DVR_GET_TIMECFG, + 0, + ptr, + size, + ref returned); + + if (!result) + { + var err = HikNativeMethods.NET_DVR_GetLastError(); + throw new Exception($"获取时间失败, 错误码: {err}"); + } + + var t = Marshal.PtrToStructure(ptr); + return new DateTime((int)t.dwYear, (int)t.dwMonth, (int)t.dwDay, (int)t.dwHour, (int)t.dwMinute, (int)t.dwSecond); + } + finally + { + Marshal.FreeHGlobal(ptr); + } + }); + } + + public async Task SetTimeAsync(DateTime time) + { + int userId = _context.GetUserId(); + if (userId < 0) throw new InvalidOperationException("设备未登录"); + + await Task.Run(() => + { + var t = new HikNativeMethods.NET_DVR_TIME + { + dwYear = (uint)time.Year, + dwMonth = (uint)time.Month, + dwDay = (uint)time.Day, + dwHour = (uint)time.Hour, + dwMinute = (uint)time.Minute, + dwSecond = (uint)time.Second + }; + + uint size = (uint)Marshal.SizeOf(t); + IntPtr ptr = Marshal.AllocHGlobal((int)size); + + try + { + Marshal.StructureToPtr(t, ptr, false); + + // 调用 SDK: NET_DVR_SET_TIME (命令号 119) + bool result = HikNativeMethods.NET_DVR_SetDVRConfig( + userId, + HikNativeMethods.NET_DVR_SET_TIMECFG, + 0, + ptr, + size); + + if (!result) + { + var err = HikNativeMethods.NET_DVR_GetLastError(); + throw new Exception($"设置时间失败, 错误码: {err}"); + } + } + finally + { + Marshal.FreeHGlobal(ptr); + } + }); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs b/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs index 37cbd5d..4299773 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs @@ -252,6 +252,9 @@ public static partial class HikNativeMethods /// 命令常量:获取时间配置 public const uint NET_DVR_GET_TIMECFG = 118; + /// 命令常量:设置时间 + public const int NET_DVR_SET_TIMECFG = 119; + #endregion #region --- PTZ 控制相关 (PTZ Control) --- @@ -492,4 +495,16 @@ public static partial class HikNativeMethods ref uint lpBytesReturned); #endregion + + + [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); } \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index 4a80e1f..9b30d2e 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -1,4 +1,6 @@ using OpenCvSharp; +using SHH.CameraSdk.HikFeatures; +using System; namespace SHH.CameraSdk; @@ -9,7 +11,8 @@ namespace SHH.CameraSdk; /// 2. [Feat A] 热更新支持:实现 OnApplyOptions,支持码流/句柄不亦断线热切换。 /// 3. [Feat B] 审计集成:全面接入 AddAuditLog,对接 Web 运维仪表盘。 /// -public class HikVideoSource : BaseVideoSource +public class HikVideoSource : BaseVideoSource, + IHikContext, ITimeSyncFeature, IRebootFeature, IPtzFeature { #region --- 静态资源 (Global Resources) --- @@ -22,6 +25,33 @@ public class HikVideoSource : BaseVideoSource #endregion + // 声明组件 + private readonly HikTimeSyncProvider _timeProvider; + private readonly HikRebootProvider _rebootProvider; + private readonly HikPtzProvider _ptzProvider; + + // ========================================== + // 实现 IHikContext (核心数据暴露) + // ========================================== + public int GetUserId() => _userId; // 暴露父类或私有的 _userId + public string GetDeviceIp() => Config.IpAddress; + + // ========================================== + // 实现 ITimeSyncFeature (路由转发) + // ========================================== + // 核心逻辑:全部委托给 _timeProvider 处理,自己不写一行逻辑 + public Task GetTimeAsync() => _timeProvider.GetTimeAsync(); + + public Task SetTimeAsync(DateTime time) => _timeProvider.SetTimeAsync(time); + + public Task RebootAsync() => _rebootProvider.RebootAsync(); + + public Task PtzControlAsync(PtzAction action, bool stop, int speed = 4) + => _ptzProvider.PtzControlAsync(action, stop, speed); + + public Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4) + => _ptzProvider.PtzStepAsync(action, durationMs, speed); + #region --- 实例成员 (Instance Members) --- private int _userId = -1; // SDK 登录句柄 @@ -53,7 +83,10 @@ public class HikVideoSource : BaseVideoSource public HikVideoSource(VideoSourceConfig config) : base(config) { - // 构造函数保持简洁 + // 初始化组件,将 "this" 作为上下文传进去 + _timeProvider = new HikTimeSyncProvider(this); + _rebootProvider = new HikRebootProvider(this); + _ptzProvider = new HikPtzProvider(this); } #endregion diff --git a/SHH.CameraSdk/Htmls/CameraControl.html b/SHH.CameraSdk/Htmls/CameraControl.html index 346daa9..611787a 100644 --- a/SHH.CameraSdk/Htmls/CameraControl.html +++ b/SHH.CameraSdk/Htmls/CameraControl.html @@ -7,80 +7,197 @@
-