添加契约和网络传输类库

This commit is contained in:
2025-12-29 08:09:14 +08:00
parent 231247c80f
commit 8cd36f44ac
14 changed files with 748 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
using SHH.Contracts; // 引用契约
namespace SHH.CameraService
{
/// <summary>
/// 摄像头工作者
/// 职责管理海康SDK生命周期产出 VideoPayload 数据流
/// </summary>
public class HikCameraWorker : IDisposable
{
// 定义一个事件:当产生新图片时触发
// 参数是我们的标准快递盒 VideoPayload
public event Action<VideoPayload> OnNewFrame;
private bool _isRunning = false;
private string _ip;
public HikCameraWorker(string ip)
{
_ip = ip;
}
/// <summary>
/// 启动取流
/// </summary>
public void Start()
{
if (_isRunning) return;
// TODO: 【在此处填入海康 SDK 初始化代码】
// CHCNetSDK.NET_DVR_Init();
// CHCNetSDK.NET_DVR_Login_V40(...);
// CHCNetSDK.NET_DVR_RealPlay_V40(...);
Console.WriteLine($"[HikWorker] 摄像头 {_ip} 已启动,开始取流...");
_isRunning = true;
// 模拟一个后台线程不断产出视频帧 (仅用于演示架构)
// 实际中,这里应该是海康的 RealDataCallBack 函数
Task.Run(() => MockCaptureLoop());
}
/// <summary>
/// 停止取流
/// </summary>
public void Stop()
{
_isRunning = false;
// TODO: 【在此处填入海康 SDK 释放代码】
// CHCNetSDK.NET_DVR_StopRealPlay(...);
// CHCNetSDK.NET_DVR_Logout(...);
Console.WriteLine($"[HikWorker] 摄像头 {_ip} 已停止。");
}
/// <summary>
/// 模拟抓图循环 (实际开发中请替换为 SDK 回调函数)
/// </summary>
private void MockCaptureLoop()
{
while (_isRunning)
{
// 1. 模拟拿到了一张 JPG 图片 (假设 100KB)
byte[] mockJpg = new byte[1024 * 100];
// 2. 立即封装成标准包
var payload = new VideoPayload
{
CameraId = _ip, // 使用 IP 或 ID 作为标记
CaptureTime = DateTime.Now,
OriginalWidth = 1920,
OriginalHeight = 1080,
OriginalImageBytes = mockJpg, // 填入原始数据
TargetImageBytes = null // SDK 只产出原图,还没有处理图
};
// 3. 【核心】触发事件,把包扔给上层 (主程序)
// ?.Invoke 确保如果没有人订阅,不会报错
OnNewFrame?.Invoke(payload);
// 模拟 25fps (每40ms一帧)
Thread.Sleep(40);
}
}
public void Dispose()
{
Stop();
}
}
}

View File

@@ -0,0 +1,250 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using SHH.CameraSdk; // 引用你的业务核心
using SHH.NetMQ;
namespace SHH.CameraService;
public class Program
{
public static async Task Main(string[] args)
{
// =============================================================
// 1. 端口与身份计算
// =============================================================
int processId = 1;
if (args.Length > 0 && int.TryParse(args[0], out int pid)) processId = pid;
int port = 5000 + (processId - 1);
Console.Title = $"SHH Gateway - Instance #{processId} (Port: {port})";
// =============================================================
// 2. 硬件环境预热 (【重要】必须在一切开始前调用)
// =============================================================
InitHardwareEnv();
// =============================================================
// 3. 构建 WebHost
// =============================================================
var builder = WebApplication.CreateBuilder(args);
// --- A. 注册 ZeroMQ 组件 (传输层) ---
string zmqBind = $"tcp://*:{5555 + (processId - 1)}";
string zmqTarget = "tcp://127.0.0.1:6000";
builder.Services.AddSingleton(new DistributorServer(zmqBind));
builder.Services.AddSingleton(new ForwarderClient(zmqTarget));
// --- B. 注册核心业务服务 ---
builder.Services.AddSingleton<IStorageService>(new FileStorageService(processId));
// CameraManager 注册为单例,生命周期由 CameraEngineWorker 管理
builder.Services.AddSingleton<CameraManager>();
builder.Services.AddSingleton<ProcessingConfigManager>();
builder.Services.AddSingleton<DisplayWindowManager>();
// --- C. 注册图像处理集群 (修复版) ---
// 我们需要确保 ImageScaleCluster 和 ImageEnhanceCluster 都能被独立注入,
// 同时它们之间又要建立链式关系。我们使用一个专门的 HostedService 来做连接。
// 1. 注册 Scale 实例
builder.Services.AddSingleton<ImageScaleCluster>(sp =>
{
var config = sp.GetRequiredService<ProcessingConfigManager>();
return new ImageScaleCluster(4, config);
});
// 2. 注册 Enhance 实例
builder.Services.AddSingleton<ImageEnhanceCluster>(sp =>
{
var config = sp.GetRequiredService<ProcessingConfigManager>();
return new ImageEnhanceCluster(4, config);
});
// 3. 注册一个启动服务来连接这两个集群 (Chain of Responsibility)
builder.Services.AddHostedService<PipelineConfigurator>();
// --- D. 注册 Web 基础服务 ---
builder.Services.AddControllers()
.AddApplicationPart(typeof(CamerasController).Assembly) // 加载 SDK 中的控制器
.AddApplicationPart(typeof(MonitorController).Assembly)
.AddControllersAsServices();
// 注册全局操作日志过滤器 (防止 500 错误)
builder.Services.AddScoped<UserActionFilter>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = $"Gateway #{processId}", Version = "v1" });
});
// --- E. 注册后台服务 (Worker) ---
// 1. 核心引擎工作者 (负责 StartAsync 和 ConfigureBusinessLogic)
builder.Services.AddHostedService<CameraEngineWorker>();
// 2. 网络哨兵 (负责断线重连)
// 假设 ConnectivitySentinel 实现了 IHostedService 或者它是一个简单的类
// 如果它实现了 IHostedService:
// builder.Services.AddHostedService<ConnectivitySentinel>();
// 如果它只是一个普通类,需要在 CameraEngineWorker 里启动它,或者注册为单例并手动启动
// 这里假设我们需要显式注册它以便让它工作:
builder.Services.AddSingleton<ConnectivitySentinel>(); // 注册单例
// 注意ConnectivitySentinel 的启动逻辑我们放到 CameraEngineWorker 里去调用
// 3. ZeroMQ 桥梁
builder.Services.AddHostedService<ZeroMqBridgeService>();
// 4. 配置 CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
});
// =============================================================
// 4. 启动应用
// =============================================================
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors("AllowAll"); // 启用 CORS
app.MapControllers();
Console.WriteLine($"[System] 绑定 Web 端口: {port}");
Console.WriteLine($"[System] 绑定 ZMQ 端口: {zmqBind}");
await app.RunAsync($"http://0.0.0.0:{port}");
}
static void InitHardwareEnv()
{
Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.5 框架版) ===");
Console.WriteLine("[硬件] 海康驱动预热中...");
try
{
HikNativeMethods.NET_DVR_Init();
HikSdkManager.ForceWarmUp();
Console.WriteLine("[硬件] 预热完成。");
}
catch (Exception ex)
{
Console.WriteLine($"[硬件] 预热失败: {ex.Message}");
// 不抛出异常,允许程序继续尝试启动(可能是在无 DLL 环境调试)
}
}
}
/// <summary>
/// 负责图像处理管道的组装 (Scale -> Enhance -> Global)
/// </summary>
public class PipelineConfigurator : IHostedService
{
private readonly ImageScaleCluster _scale;
private readonly ImageEnhanceCluster _enhance;
public PipelineConfigurator(ImageScaleCluster scale, ImageEnhanceCluster enhance)
{
_scale = scale;
_enhance = enhance;
}
public Task StartAsync(CancellationToken cancellationToken)
{
// 建立责任链: Scale -> Enhance
_scale.SetNext(_enhance);
// 挂载到全局路由 (驱动层回调会把流推给 Scale)
GlobalPipelineRouter.SetProcessor(_scale);
Console.WriteLine("[Pipeline] 图像处理链组装完成: Scale -> Enhance");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
/// <summary>
/// 负责 CameraManager 的生命周期管理和业务初始化
/// </summary>
public class CameraEngineWorker : BackgroundService
{
private readonly CameraManager _manager;
private readonly ConnectivitySentinel _sentinel; // 注入哨兵
public CameraEngineWorker(CameraManager manager, ConnectivitySentinel sentinel)
{
_manager = manager;
_sentinel = sentinel;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("[Engine] 正在启动摄像头管理器...");
// 1. 启动管理器 (加载文件配置)
await _manager.StartAsync();
// 2. 启动哨兵 (开始监控断线)
// 假设 ConnectivitySentinel 有一个 Start 或类似的方法,如果没有,说明它在构造函数里就启动了 timers
// _sentinel.Start();
// 3. 加载默认业务逻辑 (添加测试设备)
await ConfigureBusinessLogic(_manager);
Console.WriteLine("[Engine] 业务逻辑加载完成。");
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("[Engine] 正在停止...");
await _manager.DisposeAsync();
await base.StopAsync(cancellationToken);
}
// 以前 Program 类里的静态方法,现在移到这里
private async Task ConfigureBusinessLogic(CameraManager manager)
{
try
{
//// 检查是否已经有设备了,如果没有才添加默认的
//if (manager.GetAllCameras().Any()) return;
Console.WriteLine("[Engine] 检测到空配置,正在添加默认测试设备...");
var config = new VideoSourceConfig
{
Id = 101,
Brand = DeviceBrand.HikVision,
IpAddress = "192.168.5.9",
Port = 8000,
Username = "admin",
Password = "RRYFOA",
StreamType = 0
};
manager.AddDevice(config);
var config2 = new VideoSourceConfig
{
Id = 102,
Brand = DeviceBrand.HikVision,
IpAddress = "172.16.41.20",
Port = 8000,
Username = "admin",
Password = "abcd1234",
StreamType = 0
};
manager.AddDevice(config2);
}
catch (Exception ex)
{
Console.WriteLine($"[Engine] 添加默认设备失败: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NetMQ" Version="4.0.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SHH.CameraSdk\SHH.CameraSdk.csproj" />
<ProjectReference Include="..\SHH.Contracts\SHH.Contracts.csproj" />
<ProjectReference Include="..\SHH.NetMQ\SHH.NetMQ.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,75 @@
using Microsoft.Extensions.Hosting;
using OpenCvSharp;
using SHH.Contracts;
using SHH.NetMQ;
namespace SHH.CameraSdk
{
public class ZeroMqBridgeService : BackgroundService
{
private readonly DistributorServer _distributor;
private readonly ForwarderClient _forwarder;
public ZeroMqBridgeService(DistributorServer distributor, ForwarderClient forwarder)
{
_distributor = distributor;
_forwarder = forwarder;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("[Bridge] 正在连接全局广播总线...");
// 【关键修改】直接订阅静态的全局事件
// 不需要传入 APP_ID因为这是 C# 原生事件,不是字典查找
GlobalStreamDispatcher.OnGlobalFrame += BridgeHandler;
Console.WriteLine("[Bridge] 全局总线连接成功!任何动态增删的设备都会自动转发。");
return Task.CompletedTask;
}
// 真正的事件处理函数
private void BridgeHandler(long deviceId, SmartFrame frame)
{
try
{
// 1. 安全检查
var sourceMat = frame.TargetMat ?? frame.InternalMat;
if (sourceMat == null || sourceMat.Empty()) return;
// 2. 内存克隆 (Deep Copy) - 这一步不能省
using var safeMat = sourceMat.Clone();
// 3. 编码 & 封装
// 建议:可以在这里判断一下 deviceId如果某些设备不想发可以在这里 return
var jpgParams = new int[] { (int)ImwriteFlags.JpegQuality, 70 };
byte[] jpgBytes = safeMat.ImEncode(".jpg", jpgParams);
var payload = new VideoPayload
{
CameraId = deviceId.ToString(),
CaptureTime = DateTime.Now,
DispatchTime = DateTime.Now,
OriginalWidth = safeMat.Width,
OriginalHeight = safeMat.Height,
OriginalImageBytes = jpgBytes
};
// 4. 发射
_distributor.Broadcast(payload);
_forwarder.Push(payload);
}
catch (Exception ex)
{
// Console.WriteLine(ex.Message); // 生产环境建议注释掉,防止日志刷屏
}
}
public override Task StopAsync(CancellationToken cancellationToken)
{
// 优雅退订,防止内存泄漏
GlobalStreamDispatcher.OnGlobalFrame -= BridgeHandler;
return base.StopAsync(cancellationToken);
}
}
}