diff --git a/CameraService.txt b/CameraService.txt new file mode 100644 index 0000000..7ad9d0f --- /dev/null +++ b/CameraService.txt @@ -0,0 +1,1040 @@ + + +============================================================================== +FILE PATH: CameraEngineWorker.cs +============================================================================== + +using Microsoft.Extensions.Hosting; +using SHH.CameraSdk; + +namespace SHH.CameraService; + +/// +/// 相机服务核心引擎工作者(后台长驻服务) +/// 核心职责: +/// 1. 管理 CameraManager 全生命周期(启动、配置、释放) +/// 2. 初始化网络哨兵(ConnectivitySentinel),监控设备网络连通性 +/// 3. 无配置时自动添加默认测试设备,降低调试门槛 +/// 设计说明: +/// - 基于 BackgroundService,运行在独立后台线程,不阻塞 Web 主线程 +/// - 与 CameraManager 强绑定,是所有相机设备的统一入口 +/// - 包含容错机制,添加设备失败不影响整体服务启动 +public class CameraEngineWorker : BackgroundService +{ + #region --- 依赖注入字段 --- + + /// + /// 相机管理器实例(核心业务对象) + /// 功能:管理所有相机设备的生命周期、状态监控、配置更新 + /// + private readonly CameraManager _manager; + + /// + /// 网络连通性哨兵实例 + /// 功能:周期性 Ping 设备、检测网络状态、触发断线重连 + /// + private readonly ConnectivitySentinel _sentinel; + + #endregion + + #region --- 构造函数 --- + /// + /// 初始化相机引擎工作者实例 + /// + /// 相机管理器(通过 DI 注入,已关联存储服务) + /// 网络哨兵(通过 DI 注入,已预设监控周期) + public CameraEngineWorker(CameraManager manager, ConnectivitySentinel sentinel) + { + _manager = manager ?? throw new ArgumentNullException( + nameof(manager), "相机管理器实例不能为空,核心引擎启动失败"); + _sentinel = sentinel ?? throw new ArgumentNullException( + nameof(sentinel), "网络哨兵实例不能为空,设备监控功能失效"); + } + + #endregion + + #region --- BackgroundService 核心实现 --- + + /// + /// 启动引擎:初始化相机管理器并加载业务配置 + /// 执行流程: + /// 1. 启动 CameraManager(加载本地配置文件中的设备信息) + /// 2. 加载默认业务逻辑(无设备时添加测试设备) + /// 注意:网络哨兵的启动逻辑已内置在其构造函数中,此处无需额外调用 + /// + /// 服务停止令牌(响应应用关闭/重启信号) + /// 异步任务(引擎启动完成后结束) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Console.WriteLine("[Engine] 核心引擎启动中..."); + Console.WriteLine("[Engine] 启动相机管理器(加载设备配置)"); + + try + { + // 启动相机管理器:加载 App_Data/Process_X 目录下的设备配置文件 + await _manager.StartAsync(); + Console.WriteLine("[Engine] 相机管理器启动成功,已加载配置文件中的设备"); + } + catch (Exception ex) + { + Console.WriteLine($"[Engine] 相机管理器启动失败:{ex.Message}"); + Console.WriteLine("[Engine] 警告:核心引擎将继续运行,但无法管理任何相机设备"); + return; + } + + Console.WriteLine("[Engine] 核心引擎启动完成,进入运行状态"); + Console.WriteLine("[Engine] 提示:可通过 API 接口添加/编辑/删除设备,实时生效"); + } + + /// + /// 停止引擎:优雅释放资源 + /// 执行流程: + /// 1. 调用 CameraManager.DisposeAsync(),释放所有设备连接、句柄、线程资源 + /// 2. 调用基类 StopAsync(),标记服务停止状态 + /// 注意:必须先释放 CameraManager,避免设备连接泄露 + /// + /// 取消令牌(用于强制终止释放流程) + /// 异步任务(资源释放完成后结束) + public override async Task StopAsync(CancellationToken cancellationToken) + { + Console.WriteLine("[Engine] 核心引擎正在停止..."); + + try + { + // 释放相机管理器:停止所有设备取流、注销登录、释放非托管资源 + await _manager.DisposeAsync(); + Console.WriteLine("[Engine] 相机管理器资源已释放"); + } + catch (Exception ex) + { + Console.WriteLine($"[Engine] 资源释放异常:{ex.Message}"); + } + + // 调用基类方法,完成服务停止 + await base.StopAsync(cancellationToken); + Console.WriteLine("[Engine] 核心引擎已停止"); + } + + #endregion +} + +============================================================================== +FILE PATH: CommandBusProcessor.cs +============================================================================== + +using Newtonsoft.Json; +using SHH.CameraSdk; +using SHH.Contracts; + +namespace SHH.CameraService +{ + /// + /// 指令业务逻辑分发器 (纯逻辑层) + /// 职责:解析业务参数 -> 调用 CameraManager -> 返回执行结果 + /// 注意:本类不处理网络协议,也不负责 RequestId 的回填,只关注业务本身 + /// + public static class CommandBusProcessor + { + /// + /// 核心业务入口 + /// + /// 相机管理器实例 + /// 已解析的指令包 + /// 执行结果 (不含 RequestId,由调用方补充) + public static CommandResult ProcessBusinessLogic(CameraManager manager, CommandPayload payload) + { + string cmd = payload.CmdCode.ToUpper(); + + // 忽略客户端发回的 ACK (如果是双向确认模式) + if (cmd == "REGISTER_ACK") return CommandResult.Ok(); + + // 解析 TargetId (CameraId) + long deviceId = 0; + // 只有非 SYSTEM 指令才需要解析设备ID + if (payload.TargetId != "SYSTEM" && !long.TryParse(payload.TargetId, out deviceId)) + { + return CommandResult.Fail($"Invalid Device ID: {payload.TargetId}"); + } + + try + { + switch (cmd) + { + // ========================================== + // 1. PTZ 云台控制 + // ========================================== + case "PTZ": + { + var device = manager.GetDevice(deviceId); + if (device == null) return CommandResult.Fail("Device Not Found"); + if (!device.IsOnline) return CommandResult.Fail("Device Offline"); + + // 检查设备是否支持 PTZ 能力 (接口模式匹配) + if (device is IPtzFeature ptzFeature) + { + var ptzDto = JsonConvert.DeserializeObject(payload.JsonParams); + if (ptzDto == null) return CommandResult.Fail("Invalid PTZ Params"); + + // 异步转同步执行 (Task.Wait 在后台线程是安全的) + if (ptzDto.Duration > 0) + { + // 点动模式 (例如:向左转 500ms) + ptzFeature.PtzStepAsync(ptzDto.Action, ptzDto.Duration, ptzDto.Speed).Wait(); + } + else + { + // 持续模式 (开始转/停止转) + ptzFeature.PtzControlAsync(ptzDto.Action, ptzDto.Stop, ptzDto.Speed).Wait(); + } + return CommandResult.Ok("PTZ Executed"); + } + return CommandResult.Fail("Device does not support PTZ"); + } + + // ========================================== + // 2. 远程重启 + // ========================================== + case "REBOOT": + { + var device = manager.GetDevice(deviceId); + if (device == null) return CommandResult.Fail("Device Not Found"); + + if (device is IRebootFeature rebootFeature) + { + rebootFeature.RebootAsync().Wait(); + return CommandResult.Ok("Reboot command sent"); + } + return CommandResult.Fail("Device does not support Reboot"); + } + + // ========================================== + // 3. 时间同步 + // ========================================== + case "SYNC_TIME": + { + var device = manager.GetDevice(deviceId); + if (device == null) return CommandResult.Fail("Device Not Found"); + + if (device is ITimeSyncFeature timeFeature) + { + timeFeature.SetTimeAsync(DateTime.Now).Wait(); + return CommandResult.Ok("Time synced"); + } + return CommandResult.Fail("Device does not support TimeSync"); + } + + // ========================================== + // 4. 系统级指令 (心跳/诊断) + // ========================================== + case "PING": + return CommandResult.Ok("PONG"); + + default: + return CommandResult.Fail($"Unknown Command: {cmd}"); + } + } + catch (AggregateException ae) + { + // 捕获异步任务内部的异常 + return CommandResult.Fail($"Execution Error: {ae.InnerException?.Message}"); + } + catch (Exception ex) + { + return CommandResult.Fail($"Execution Error: {ex.Message}"); + } + } + } +} + +============================================================================== +FILE PATH: CommandBusService.cs +============================================================================== + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using NetMQ; +using NetMQ.Sockets; +using Newtonsoft.Json; +using SHH.CameraSdk; +using SHH.Contracts; + +namespace SHH.CameraService +{ + /// + /// 双模指令总线服务 (Enterprise V2) + /// 核心职责:建立 TCP 指令通道,接收客户端指令并分发给 CameraManager + /// 增强特性: + /// 1. 支持双模:被动监听 (Bind) 与 主动投递 (Connect) + /// 2. 幂等性控制:利用 MemoryCache 防止客户端重试导致的重复执行 + /// 3. 顺序一致性:利用时间戳防止指令乱序 + /// + public class CommandBusService : BackgroundService + { + #region --- 1. 字段与依赖 --- + + private readonly CameraManager _cameraManager; + private readonly IConfiguration _config; + private readonly IMemoryCache _cache; // 核心:用于请求去重 + private readonly int _processId; + + // 运行状态标志 + private volatile bool _isRunning = false; + + // 两种模式的 Socket (互斥存在) + private ResponseSocket? _repSocket; // 模式A: 被动监听 (Server-Listening) + private DealerSocket? _dealerSocket; // 模式B: 主动投递 (Server-Dialing) + + // 顺序一致性锁:记录每个设备最后处理的指令时间戳 + // Key: TargetId (设备ID), Value: Timestamp (最后执行时间) + private readonly Dictionary _deviceLastCmdTime = new(); + + #endregion + + #region --- 2. 构造函数 --- + + /// + /// 构造函数 (注意:必须在 Program.cs 注册 AddMemoryCache) + /// + public CommandBusService(CameraManager manager, IConfiguration config, IMemoryCache cache) + { + _cameraManager = manager; + _config = config; + _cache = cache; + // 获取当前进程 ID (默认为 1) + _processId = _config.GetValue("ProcessId", 1); + } + + #endregion + + #region --- 3. 核心生命周期 --- + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + // 在后台线程启动,避免阻塞 Web 主线程 + return Task.Run(() => + { + _isRunning = true; + + // 1. 读取网络策略 + // 优先读取配置中的主动目标,如果没有则回退到被动监听 + string? activeTargetIp = _config["Network:ActiveTargets:0:Ip"]; + bool isActiveMode = !string.IsNullOrEmpty(activeTargetIp); + + try + { + if (isActiveMode) + { + // === 模式 B: 主动投递 (Server Connects Client) === + // 场景:服务器在内网,主动连接公网/固定IP的客户端 + int cmdPort = _config.GetValue("Network:ActiveTargets:0:CmdPort", 7000); + string addr = $"tcp://{activeTargetIp}:{cmdPort}"; + RunActiveMode(addr, stoppingToken); + } + else + { + // === 模式 A: 被动监听 (Server Binds Port) === + // 场景:服务器有固定IP,等待客户端连接 + int basePort = _config.GetValue("Network:Passive:CmdPortBase", 7000); + int listenPort = basePort + (_processId - 1); + string addr = $"tcp://*:{listenPort}"; + RunPassiveMode(addr, stoppingToken); + } + } + catch (Exception ex) + { + Console.WriteLine($"[CmdBus] 致命错误停止: {ex.Message}"); + } + finally + { + _isRunning = false; + CleanupSockets(); + } + + }, stoppingToken); + } + + private void CleanupSockets() + { + try + { + _repSocket?.Dispose(); + _dealerSocket?.Dispose(); + } + catch { /* 忽略销毁时的异常 */ } + } + + #endregion + + #region --- 4. 模式实现:被动监听 (Passive) --- + + private void RunPassiveMode(string address, CancellationToken token) + { + using (_repSocket = new ResponseSocket()) + { + _repSocket.Bind(address); + Console.WriteLine($"[CmdBus] [被动模式] 指令监听已启动: {address}"); + + while (!token.IsCancellationRequested) + { + try + { + // 1. 阻塞等待请求 (超时1秒以便响应 Cancel 信号) + if (!_repSocket.TryReceiveFrameString(TimeSpan.FromSeconds(1), out string reqJson)) + continue; + + // 2. 处理业务 (带 去重 + ID回填 逻辑) + CommandResult result = this.ProcessRequest(reqJson); + + // 3. 发送回执 + // 注意:REP 模式必须发送应答,即使 result 为 null (Fire-and-Forget) 也建议发一个空 ACK 防止 Socket 状态错乱 + // 但为了协议统一,建议 Passive 模式下总是返回结果 + string respJson = result != null ? JsonConvert.SerializeObject(result) : "{}"; + _repSocket.SendFrame(respJson); + } + catch (Exception ex) + { + Console.WriteLine($"[CmdBus-Passive] 异常: {ex.Message}"); + } + } + } + } + + #endregion + + #region --- 5. 模式实现:主动投递 (Active) --- + + private void RunActiveMode(string address, CancellationToken token) + { + // 外层循环:断线重连机制 + while (!token.IsCancellationRequested) + { + try + { + using (_dealerSocket = new DealerSocket()) + { + Console.WriteLine($"[CmdBus] [主动模式] 正在连接指令中心: {address}"); + _dealerSocket.Connect(address); + + // ★★★ 关键步骤:连接成功后,立即发送【身份注册包】 ★★★ + // 客户端收到这个包后,才能在界面上显示"设备在线" + SendRegistration(_dealerSocket); + + // 内层循环:消息收发 + while (!token.IsCancellationRequested) + { + // 1. 接收指令 + // DealerSocket 是异步全双工的,这里即使没收到消息也不会阻塞发送 + if (!_dealerSocket.TryReceiveFrameString(TimeSpan.FromSeconds(1), out string reqJson)) + { + // 空闲周期,可在此处添加心跳发送逻辑 (Ping) + continue; + } + + // 2. 处理业务 (带 去重 + ID回填 逻辑) + CommandResult result = this.ProcessRequest(reqJson); + + // 3. 发送结果 (QoS控制) + // 如果结果为 null,说明指令是 Fire-and-Forget (无需回执),则不发送网络包节省带宽 + if (result != null) + { + _dealerSocket.SendFrame(JsonConvert.SerializeObject(result)); + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[CmdBus-Active] 连接中断或异常: {ex.Message}"); + // 避免死循环狂刷 CPU,等待 3 秒再重连 + Thread.Sleep(3000); + } + } + } + + /// + /// 发送身份注册包 (Active 模式专用) + /// + private void SendRegistration(DealerSocket socket) + { + try + { + // 计算实际端口信息 + int portOffset = _processId - 1; + var regInfo = new ServerRegistrationDto + { + ProcessId = _processId, + InstanceId = $"Gateway_{_processId}", + ServerIp = GetLocalIpAddress(), + WebApiPort = 5000 + portOffset, + VideoPort = 5555 + portOffset, + CmdPort = 7000 + portOffset, + StartTime = DateTime.Now, + Description = "Active Mode Connection (V2)" + }; + + // 封装信封 (系统级指令) + var payload = new CommandPayload + { + CmdCode = "SERVER_REGISTER", + TargetId = "SYSTEM", + JsonParams = JsonConvert.SerializeObject(regInfo), + RequestId = Guid.NewGuid().ToString("N"), + RequireAck = false // 注册包通常不需要回执,只要连上就行 + }; + + socket.SendFrame(JsonConvert.SerializeObject(payload)); + Console.WriteLine($"[CmdBus] 身份注册包已发送 -> {regInfo.ServerIp}:{regInfo.WebApiPort}"); + } + catch (Exception ex) + { + Console.WriteLine($"[CmdBus] 注册包发送失败: {ex.Message}"); + } + } + + private string GetLocalIpAddress() + { + try + { + var host = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()); + foreach (var ip in host.AddressList) + { + if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + return ip.ToString(); + } + } + catch { } + return "127.0.0.1"; + } + + #endregion + + #region --- 6. 协议处理核心 (★★ V2 核心增强 ★★) --- + + /// + /// 统一处理请求协议:去重 -> 排序 -> 执行 -> 回填 ID + /// + private CommandResult ProcessRequest(string json) + { + if (string.IsNullOrWhiteSpace(json)) return CommandResult.Fail("Empty Request"); + + CommandPayload? payload; + try + { + payload = JsonConvert.DeserializeObject(json); + } + catch + { + return CommandResult.Fail("Invalid JSON Protocol"); + } + + if (payload == null) return CommandResult.Fail("Null Payload"); + + // ========================================================= + // A. 【幂等性检查】(Idempotency Check) + // ========================================================= + // 客户端如果没收到回执,会发起重试。我们不能让云台转两次。 + // 检查缓存中是否已有该 RequestId 的结果。 + if (_cache.TryGetValue(payload.RequestId, out CommandResult cachedResult)) + { + Console.WriteLine($"[Dedup] 拦截重复请求: {payload.RequestId} (Retry: {payload.RetryCount})"); + return cachedResult; // 直接返回上次执行的结果,不调用业务层 + } + + // ========================================================= + // B. 【顺序一致性检查】(Order Guarantee) + // ========================================================= + // 仅针对具体的设备指令 (非 SYSTEM)。防止 "停止" 指令先于 "开始" 指令被处理(乱序)。 + if (payload.TargetId != "SYSTEM") + { + lock (_deviceLastCmdTime) + { + if (_deviceLastCmdTime.TryGetValue(payload.TargetId, out DateTime lastTime)) + { + // 如果当前指令的时间戳 早于 最后一次执行的时间戳,说明是迟到的旧包 + if (payload.Timestamp < lastTime) + { + Console.WriteLine($"[Order] 丢弃乱序指令: {payload.CmdCode} (Time: {payload.Timestamp})"); + return CommandResult.Fail("Order Violation: Stale Command Dropped"); + } + } + // 更新该设备的最后指令时间 + _deviceLastCmdTime[payload.TargetId] = payload.Timestamp; + } + } + + // ========================================================= + // C. 【业务执行】 + // ========================================================= + // 调用纯逻辑层 + CommandResult result; + try + { + result = CommandBusProcessor.ProcessBusinessLogic(_cameraManager, payload); + } + catch (Exception ex) + { + result = CommandResult.Fail($"Internal Logic Error: {ex.Message}"); + } + + // ========================================================= + // D. 【闭环回填】(Back-fill RequestId) + // ========================================================= + // 无论成功失败,必须带上 RequestId,否则客户端 await 无法匹配 + result.RequestId = payload.RequestId; + + // ========================================================= + // E. 【状态缓存】 + // ========================================================= + // 将结果存入缓存,有效期设为 10 秒 (覆盖客户端的重试窗口) + // 这样 10 秒内收到同样的 RequestId,直接走上面的步骤 A 返回 + _cache.Set(payload.RequestId, result, TimeSpan.FromSeconds(10)); + + // ========================================================= + // F. 【QoS 过滤】 + // ========================================================= + // 如果客户端声明 RequireAck = false (如心跳包),返回 null + // 外层调用者会根据 null 决定不发送网络数据 + if (!payload.RequireAck) + { + return null; + } + + return result; + } + + #endregion + } +} + +============================================================================== +FILE PATH: PipelineConfigurator.cs +============================================================================== + +using Microsoft.Extensions.Hosting; +using SHH.CameraSdk; + +namespace SHH.CameraService; + +/// +/// 图像处理管道配置服务(基于责任链模式) +/// 核心职责: +/// 1. 组装图像处理集群的执行顺序,形成 "缩放 → 增强" 的固定流程 +/// 2. 将组装好的管道挂载到全局路由,统一接收驱动层输出的帧数据 +/// 设计说明: +/// - 采用责任链模式,支持动态扩展处理节点(如后续新增滤镜、裁剪等功能) +/// - 依赖 IHostedService 生命周期,确保在应用启动时完成管道初始化 +/// - 与 GlobalPipelineRouter 强关联,是帧数据进入处理流程的唯一入口 +public class PipelineConfigurator : IHostedService +{ + #region --- 依赖注入字段 --- + + /// + /// 图像缩放集群实例(责任链第一节点) + /// 功能:根据配置缩放帧分辨率、控制图像放大/缩小开关 + /// + private readonly ImageScaleCluster _scale; + + /// + /// 图像增强集群实例(责任链第二节点) + /// 功能:调整图像亮度、对比度等增强效果(基于 ProcessingConfigManager 配置) + /// + private readonly ImageEnhanceCluster _enhance; + + #endregion + + #region --- 构造函数 --- + + /// + /// 初始化管道配置服务实例 + /// + /// 图像缩放集群(通过 DI 注入,已预设并行度和配置管理器) + /// 图像增强集群(通过 DI 注入,已预设并行度和配置管理器) + + public PipelineConfigurator(ImageScaleCluster scale, ImageEnhanceCluster enhance) + { + _scale = scale; + _enhance = enhance; + } + + #endregion + + #region --- IHostedService 实现 --- + + /// + /// 启动服务:组装责任链并挂载到全局路由 + /// 执行时机:应用启动时,在所有 Singleton 服务初始化完成后触发 + /// + /// 服务停止令牌(用于响应应用关闭信号) + /// 异步任务(无返回值) + public Task StartAsync(CancellationToken cancellationToken) + { + // 1. 建立责任链关系:缩放集群处理完成后,将帧数据传递给增强集群 + // 设计逻辑:Scale 是入口节点,Enhance 是后续节点,可按需求插入更多处理节点 + _scale.SetNext(_enhance); + + // 2. 将责任链入口挂载到全局路由:驱动层输出的所有帧数据都会进入该管道 + // 关键作用:统一帧数据处理入口,屏蔽驱动层与处理层的直接依赖 + GlobalPipelineRouter.SetProcessor(_scale); + + // 启动日志:打印管道组装结果,便于运维排查 + Console.WriteLine("[Pipeline] 图像处理链组装完成: ImageScaleCluster -> ImageEnhanceCluster"); + Console.WriteLine("[Pipeline] 提示:帧数据将按 '缩放 → 增强' 顺序处理,可通过 GlobalPipelineRouter 调整流程"); + + return Task.CompletedTask; + } + + /// + /// 停止服务:空实现(无资源需要释放) + /// 说明:图像处理集群的资源释放由各自的 Dispose 方法管理,此处无需额外操作 + /// + /// 服务停止令牌 + /// 空异步任务 + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + #endregion +} + +============================================================================== +FILE PATH: Program.cs +============================================================================== + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using SHH.CameraSdk; // 引用你的业务核心 +using SHH.NetMQ; + +namespace SHH.CameraService; + +public class Program +{ + public static async Task Main(string[] args) + { + #region --- 1. 端口与身份计算 --- + + int processId = 1; + // 从命令行参数解析进程ID(默认1) + if (args.Length > 0 && int.TryParse(args[0], out int pid)) + processId = pid; + + // 计算 Web 服务端口(基础5000 + 进程ID偏移) + int port = 5000 + (processId - 1); + + Console.Title = $"SHH Gateway - Instance #{processId} (Port: {port})"; + + #endregion + + #region --- 2. 硬件环境预热 (【重要】必须在一切开始前调用) --- + + InitHardwareEnv(); + + #endregion + + #region --- 3. 构建 WebHost --- + + var builder = WebApplication.CreateBuilder(args); + + #region --- A. 注册 ZeroMQ 组件 (传输层) --- + + // 注册转发客户端(定向推送) + string zmqBind = $"tcp://*:{5555 + (processId - 1)}"; + + // ★★★ 新增:注册指令总线服务 ★★★ + string zmqTarget = "tcp://127.0.0.1:6000"; + + // 注册转发客户端(定向推送) + builder.Services.AddSingleton(new ForwarderClient(zmqTarget)); + + // ★★★ 新增:注册指令总线服务 ★★★ + builder.Services.AddHostedService(); + + // 注册分发服务器(广播) + builder.Services.AddSingleton(new DistributorServer(zmqBind)); + + #endregion + + #region --- B. 注册核心业务服务 --- + + // 注册文件存储服务(进程隔离) + builder.Services.AddSingleton(new FileStorageService(processId)); + + // CameraManager 注册为单例,生命周期由 CameraEngineWorker 管理 + builder.Services.AddSingleton(); + + // 图像处理配置管理器(单例) + builder.Services.AddSingleton(); + + // 显示窗口管理器(单例) + builder.Services.AddSingleton(); + + #endregion + + #region --- C. 注册图像处理集群 (修复版) --- + + // 说明:通过责任链模式组装 Scale → Enhance 处理流程,确保顺序执行 + // 1. 注册图像缩容集群(并行度4) + builder.Services.AddSingleton(sp => + { + var configManager = sp.GetRequiredService(); + return new ImageScaleCluster(4, configManager); + }); + + // 2. 注册图像增强集群(并行度4) + builder.Services.AddSingleton(sp => + { + var configManager = sp.GetRequiredService(); + return new ImageEnhanceCluster(4, configManager); + }); + + // 3. 注册管道配置服务(组装责任链) + builder.Services.AddHostedService(); + + #endregion + + #region --- D. 注册 Web 基础服务 --- + + // 注册控制器(加载 SDK 中的 CamerasController、MonitorController) + builder.Services.AddControllers() + .AddApplicationPart(typeof(CamerasController).Assembly) // 加载 SDK 中的控制器 + .AddApplicationPart(typeof(MonitorController).Assembly) + .AddControllersAsServices(); + + // 注册全局操作日志过滤器(捕获 API 操作日志) + builder.Services.AddScoped(); + + // 注册 Swagger 文档(区分实例ID) + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = $"Gateway #{processId}", + Version = "v1" + }); + }); + + #endregion + + #region --- E. 注册后台服务 (Worker) --- + + // 1. 核心引擎工作者 (负责 StartAsync 和 ConfigureBusinessLogic) + builder.Services.AddHostedService(); + + // 2.网络哨兵(负责断线重连)(监控设备断线重连,注册为单例) + builder.Services.AddSingleton(); + + // 3. ZeroMQ 桥梁服务(转发帧数据到外部系统) + builder.Services.AddHostedService(); + + #endregion + + #region --- F. 配置 CORS(允许所有跨域请求) --- + + builder.Services.AddCors(options => + { + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(); + }); + }); + + #endregion + + #endregion + + #region --- 4. 启动应用 --- + + var app = builder.Build(); + + // 启用 Swagger 文档 + app.UseSwagger(); + app.UseSwaggerUI(); + + // 启用 CORS 策略 + app.UseCors("AllowAll"); + + // 映射控制器路由 + app.MapControllers(); + + // 输出启动信息 + Console.WriteLine($"[System] 绑定 Web 端口: {port}"); + Console.WriteLine($"[System] 绑定 ZMQ 端口: {zmqBind}"); + + // 启动 Web 应用 + await app.RunAsync($"http://0.0.0.0:{port}"); + + #endregion + } + + #region --- 辅助方法:硬件环境预热 --- + + /// + /// 初始化硬件环境(海康 SDK 预热) + /// + static void InitHardwareEnv() + { + Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.5 框架版) ==="); + Console.WriteLine("[硬件] 海康驱动预热中..."); + try + { + // 初始化海康 SDK + HikNativeMethods.NET_DVR_Init(); + // 强制预热播放库(避免首次取流延迟) + HikSdkManager.ForceWarmUp(); + Console.WriteLine("[硬件] 预热完成。"); + } + catch (Exception ex) + { + Console.WriteLine($"[硬件] 预热失败: {ex.Message}"); + // 不抛出异常,允许程序在无 DLL 环境下调试 + } + } + + #endregion +} + +============================================================================== +FILE PATH: ZeroMqBridgeService.cs +============================================================================== + +using Microsoft.Extensions.Hosting; +using OpenCvSharp; +using SHH.Contracts; +using SHH.NetMQ; + +namespace SHH.CameraSdk +{ + /// + /// ZeroMQ 消息桥接服务(后台服务)。 + /// 核心职责:订阅系统全局视频帧广播,将帧数据编码为标准协议格式后,通过 ZeroMQ 分发至外部系统。 + /// 设计特性: + /// 1. 无侵入集成:通过订阅 GlobalStreamDispatcher 事件,无需修改原有帧处理流程。 + /// 2. 自动适配:支持动态增删设备,无需手动注册设备监听。 + /// 3. 安全隔离:帧数据深拷贝,避免跨线程内存访问冲突。 + /// + public class ZeroMqBridgeService : BackgroundService + { + #region --- 依赖注入字段 --- + + /// + /// ZeroMQ 分发服务器(用于广播帧数据至多个订阅端) + /// + private readonly DistributorServer _distributor; + + /// + /// ZeroMQ 转发客户端(用于定向推送帧数据至指定目标) + /// + private readonly ForwarderClient _forwarder; + + #endregion + + #region --- 构造函数 --- + + /// + /// 初始化 实例。 + /// + /// ZeroMQ 分发服务器实例(通过 DI 注入) + /// ZeroMQ 转发客户端实例(通过 DI 注入) + public ZeroMqBridgeService(DistributorServer distributor, ForwarderClient forwarder) + { + _distributor = distributor ?? throw new ArgumentNullException(nameof(distributor)); + _forwarder = forwarder ?? throw new ArgumentNullException(nameof(forwarder)); + } + + #endregion + + #region --- 后台服务核心逻辑 --- + + /// + /// 启动后台服务,订阅全局视频帧广播。 + /// + /// 服务停止令牌(用于优雅关闭) + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + Console.WriteLine("[ZeroMQ Bridge] 正在连接全局视频帧广播总线..."); + + // 订阅全局帧广播事件:所有设备的帧数据都会触发该事件 + // 无需手动绑定设备,动态增删的设备自动适配 + GlobalStreamDispatcher.OnGlobalFrame += OnGlobalFrameReceived; + + Console.WriteLine("[ZeroMQ Bridge] 全局总线连接成功!已开始监听所有设备帧数据。"); + Console.WriteLine("[ZeroMQ Bridge] 说明:动态增删的设备会自动转发,无需重启服务。"); + + // 返回空任务:服务通过事件驱动,无需阻塞主线程 + return Task.CompletedTask; + } + + /// + /// 停止后台服务,取消事件订阅以避免内存泄漏。 + /// + /// 取消令牌 + public override Task StopAsync(CancellationToken cancellationToken) + { + Console.WriteLine("[ZeroMQ Bridge] 正在停止服务,取消全局总线订阅..."); + + // 取消事件订阅:必须执行,否则会导致内存泄漏 + GlobalStreamDispatcher.OnGlobalFrame -= OnGlobalFrameReceived; + + Console.WriteLine("[ZeroMQ Bridge] 服务已安全停止。"); + return base.StopAsync(cancellationToken); + } + + #endregion + + #region --- 帧数据处理核心逻辑 --- + + /// + /// 全局帧数据接收回调(事件处理函数)。 + /// 处理流程:安全检查 → 帧数据深拷贝 → JPG 编码 → 封装为标准协议 → ZeroMQ 分发。 + /// + /// 产生该帧的设备唯一标识 + /// 智能帧对象(包含原始/处理后图像数据) + private void OnGlobalFrameReceived(long deviceId, SmartFrame frame) + { + try + { + // 1. 安全校验:跳过空帧或已释放的帧 + var sourceMat = frame.TargetMat ?? frame.InternalMat; + if (sourceMat == null || sourceMat.Empty() || sourceMat.IsDisposed) + return; + + // 2. 深拷贝图像数据:避免跨线程访问冲突(原帧可能被其他模块异步释放) + using var safeMat = sourceMat.Clone(); + + // 3. 图像编码:将 OpenCV Mat 转换为 JPG 字节数组(质量70,平衡画质与性能) + var jpgEncodeParams = new int[] { (int)ImwriteFlags.JpegQuality, 70 }; + byte[] jpgBytes = safeMat.ImEncode(".jpg", jpgEncodeParams); + + // 4. 封装为标准传输协议:使用 SHH.Contracts 中的 VideoPayload 统一格式 + var videoPayload = new VideoPayload + { + CameraId = deviceId.ToString(), // 设备ID(转为字符串,兼容协议标准) + CaptureTime = DateTime.Now, // 帧采集时间(当前时间) + DispatchTime = DateTime.Now, // 帧分发时间(当前时间) + OriginalWidth = safeMat.Width, // 图像原始宽度(编码后宽度) + OriginalHeight = safeMat.Height, // 图像原始高度(编码后高度) + OriginalImageBytes = jpgBytes // JPG 编码后的二进制数据 + }; + + // 5. 传递订阅者ID:保持与原帧的订阅者关联 + if (frame.SubscriberIds.Any()) + videoPayload.SubscriberIds.AddRange(frame.SubscriberIds); + + // 6. ZeroMQ 分发:同时执行广播和定向推送(根据业务需求选择,可按需注释) + _distributor.Broadcast(videoPayload); // 广播给所有订阅端 + _forwarder.Push(videoPayload); // 定向推送给指定目标 + + // 调试日志(生产环境建议注释,避免性能损耗) + // Console.WriteLine($"[ZeroMQ Bridge] 转发设备 {deviceId} 帧数据,大小:{jpgBytes.Length / 1024}KB"); + } + catch (Exception ex) + { + // 异常隔离:单个帧处理失败不影响整体服务运行 + Console.WriteLine($"[ZeroMQ Bridge] 帧转发失败(设备ID:{deviceId}):{ex.Message}"); + } + } + + #endregion + } +} diff --git a/SHH.AIServer/SHH.AIServer.slnx b/SHH.AIServer/SHH.AIServer.slnx new file mode 100644 index 0000000..8e3933d --- /dev/null +++ b/SHH.AIServer/SHH.AIServer.slnx @@ -0,0 +1,3 @@ + + + diff --git a/SHH.AIServer/SHH.AIServer/Program.cs b/SHH.AIServer/SHH.AIServer/Program.cs new file mode 100644 index 0000000..744ccbe --- /dev/null +++ b/SHH.AIServer/SHH.AIServer/Program.cs @@ -0,0 +1,10 @@ +namespace SHH.AIServer +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } + } +} diff --git a/SHH.AIServer/SHH.AIServer/SHH.AIServer.csproj b/SHH.AIServer/SHH.AIServer/SHH.AIServer.csproj new file mode 100644 index 0000000..2150e37 --- /dev/null +++ b/SHH.AIServer/SHH.AIServer/SHH.AIServer.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/SHH.CameraDashboard/App.xaml.cs b/SHH.CameraDashboard/App.xaml.cs index 613b685..41bc3e7 100644 --- a/SHH.CameraDashboard/App.xaml.cs +++ b/SHH.CameraDashboard/App.xaml.cs @@ -37,6 +37,5 @@ namespace SHH.CameraDashboard // 3. 彻底退出 Current.Shutdown(); } - } } \ No newline at end of file diff --git a/SHH.CameraDashboard/MainWindow.xaml b/SHH.CameraDashboard/MainWindow.xaml index c173575..57fe4b4 100644 --- a/SHH.CameraDashboard/MainWindow.xaml +++ b/SHH.CameraDashboard/MainWindow.xaml @@ -203,13 +203,20 @@ - - + + + + + + + + + diff --git a/SHH.CameraDashboard/MainWindowViewModel.cs b/SHH.CameraDashboard/MainWindowViewModel.cs index 14ea804..79af3ba 100644 --- a/SHH.CameraDashboard/MainWindowViewModel.cs +++ b/SHH.CameraDashboard/MainWindowViewModel.cs @@ -92,6 +92,8 @@ namespace SHH.CameraDashboard // 初始化子 ViewModel DiagnosticVM = new ServiceNodesViewModel(); + + MainContent = new VideoWallViewModel(); // 启动异步初始化流程 _ = InitializeAsync(); @@ -511,5 +513,12 @@ namespace SHH.CameraDashboard } #endregion + + private object _mainContent; + public object MainContent + { + get => _mainContent; + set { _mainContent = value; OnPropertyChanged(); } + } } } \ No newline at end of file diff --git a/SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml b/SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml new file mode 100644 index 0000000..e05ef7d --- /dev/null +++ b/SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml.cs b/SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml.cs new file mode 100644 index 0000000..1d48729 --- /dev/null +++ b/SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace SHH.CameraDashboard +{ + /// + /// VideoTile.xaml 的交互逻辑 + /// + public partial class VideoTile : UserControl + { + public VideoTile() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/SHH.CameraDashboard/Pages/CameraWall/VideoTileViewModel.cs b/SHH.CameraDashboard/Pages/CameraWall/VideoTileViewModel.cs new file mode 100644 index 0000000..85ac187 --- /dev/null +++ b/SHH.CameraDashboard/Pages/CameraWall/VideoTileViewModel.cs @@ -0,0 +1,87 @@ +using SHH.Contracts; +using System.IO; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace SHH.CameraDashboard +{ + public class VideoTileViewModel : ViewModelBase, IDisposable + { + // --- 绑定属性 --- + + private ImageSource _displayImage; + public ImageSource DisplayImage + { + get => _displayImage; + set => SetProperty(ref _displayImage, value); + } + + private string _cameraName; + public string CameraName + { + get => _cameraName; + set => SetProperty(ref _cameraName, value); + } + + private string _statusInfo; + public string StatusInfo + { + get => _statusInfo; + set => SetProperty(ref _statusInfo, value); + } + + private bool _isConnected; + public bool IsConnected + { + get => _isConnected; + set => SetProperty(ref _isConnected, value); + } + + // --- 构造函数 --- + public VideoTileViewModel(string ip, int port, string name) + { + CameraName = name; + StatusInfo = "连接中..."; + + IsConnected = true; + } + + private void HandleNewFrame(VideoPayload payload) + { + // 必须回到 UI 线程更新 ImageSource + Application.Current.Dispatcher.Invoke(() => + { + // 1. 更新图片 + byte[] data = payload.TargetImageBytes ?? payload.OriginalImageBytes; + if (data != null && data.Length > 0) + { + DisplayImage = ByteToBitmap(data); + } + + // 2. 更新状态文字 + StatusInfo = $"{payload.CaptureTime:HH:mm:ss} | {data?.Length / 1024} KB"; + }); + } + + // 简单的 Bytes 转 BitmapImage (生产环境建议优化为 WriteableBitmap) + private BitmapImage ByteToBitmap(byte[] bytes) + { + var bitmap = new BitmapImage(); + using (var stream = new MemoryStream(bytes)) + { + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.StreamSource = stream; + bitmap.EndInit(); + } + bitmap.Freeze(); // 必须冻结才能跨线程 + return bitmap; + } + + public void Dispose() + { + IsConnected = false; + } + } +} \ No newline at end of file diff --git a/SHH.CameraDashboard/Pages/CameraWall/VideoWall.xaml b/SHH.CameraDashboard/Pages/CameraWall/VideoWall.xaml new file mode 100644 index 0000000..f1aefdf --- /dev/null +++ b/SHH.CameraDashboard/Pages/CameraWall/VideoWall.xaml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + +