diff --git a/server/src/Controllers/VideoStreamController.cs b/server/src/Controllers/VideoStreamController.cs
index c1cc728..fe786b9 100644
--- a/server/src/Controllers/VideoStreamController.cs
+++ b/server/src/Controllers/VideoStreamController.cs
@@ -1,77 +1,22 @@
using System.ComponentModel.DataAnnotations;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using System.Security.Claims;
+using Database;
+using DotNext;
///
/// 视频流控制器,支持动态配置摄像头连接
///
[ApiController]
+[Authorize]
[Route("api/[controller]")]
public class VideoStreamController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly server.Services.HttpVideoStreamService _videoStreamService;
- ///
- /// 视频流信息结构体
- ///
- public class StreamInfoResult
- {
- ///
- /// TODO:
- ///
- public int FrameRate { get; set; }
- ///
- /// TODO:
- ///
- public int FrameWidth { get; set; }
- ///
- /// TODO:
- ///
- public int FrameHeight { get; set; }
- ///
- /// TODO:
- ///
- public string Format { get; set; } = "MJPEG";
- ///
- /// TODO:
- ///
- public string HtmlUrl { get; set; } = "";
- ///
- /// TODO:
- ///
- public string MjpegUrl { get; set; } = "";
- ///
- /// TODO:
- ///
- public string SnapshotUrl { get; set; } = "";
- ///
- /// TODO:
- ///
- public string UsbCameraUrl { get; set; } = "";
- }
-
- ///
- /// 摄像头配置请求模型
- ///
- public class CameraConfigRequest
- {
- ///
- /// 摄像头地址
- ///
- [Required]
- [RegularExpression(@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ErrorMessage = "请输入有效的IP地址")]
- public string Address { get; set; } = "";
-
- ///
- /// 摄像头端口
- ///
- [Required]
- [Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")]
- public int Port { get; set; }
- }
-
///
/// 分辨率配置请求模型
///
@@ -92,6 +37,14 @@ public class VideoStreamController : ControllerBase
public int Height { get; set; }
}
+ public class AvailableResolutionsResponse
+ {
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string Value => $"{Width}x{Height}";
+ }
+
///
/// 初始化HTTP视频流控制器
///
@@ -102,6 +55,40 @@ public class VideoStreamController : ControllerBase
_videoStreamService = videoStreamService;
}
+ private Optional TryGetBoardId()
+ {
+ var userName = User.FindFirstValue(ClaimTypes.Name);
+ if (string.IsNullOrEmpty(userName))
+ {
+ logger.Error("User name not found in claims.");
+ return Optional.None;
+ }
+
+ var db = new AppDataConnection();
+ if (db == null)
+ {
+ logger.Error("Database connection failed.");
+ return Optional.None;
+ }
+
+ var userRet = db.GetUserByName(userName);
+ if (!userRet.IsSuccessful || !userRet.Value.HasValue)
+ {
+ logger.Error("User not found.");
+ return Optional.None;
+ }
+
+ var user = userRet.Value.Value;
+ var boardId = user.BoardID;
+ if (boardId == Guid.Empty)
+ {
+ logger.Error("No board bound to this user.");
+ return Optional.None;
+ }
+
+ return boardId.ToString();
+ }
+
///
/// 获取 HTTP 视频流服务状态
///
@@ -114,8 +101,6 @@ public class VideoStreamController : ControllerBase
{
try
{
- logger.Info("GetStatus方法被调用,控制器:{Controller},路径:api/VideoStream/Status", this.GetType().Name);
-
// 使用HttpVideoStreamService提供的状态信息
var status = _videoStreamService.GetServiceStatus();
@@ -129,101 +114,18 @@ public class VideoStreamController : ControllerBase
}
}
- ///
- /// 获取 HTTP 视频流信息
- ///
- /// 流信息
- [HttpGet("StreamInfo")]
- [EnableCors("Users")]
- [ProducesResponseType(typeof(StreamInfoResult), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
- public IResult GetStreamInfo()
- {
- try
- {
- logger.Info("获取 HTTP 视频流信息");
- var result = new StreamInfoResult
- {
- FrameRate = _videoStreamService.FrameRate,
- FrameWidth = _videoStreamService.FrameWidth,
- FrameHeight = _videoStreamService.FrameHeight,
- Format = "MJPEG",
- HtmlUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/video-feed.html",
- MjpegUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/video-stream",
- SnapshotUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/snapshot",
- UsbCameraUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/usb-camera"
- };
- return TypedResults.Ok(result);
- }
- catch (Exception ex)
- {
- logger.Error(ex, "获取 HTTP 视频流信息失败");
- return TypedResults.InternalServerError(ex.Message);
- }
- }
-
- ///
- /// 配置摄像头连接参数
- ///
- /// 摄像头配置
- /// 配置结果
- [HttpPost("ConfigureCamera")]
- [EnableCors("Users")]
- [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
- public async Task ConfigureCamera([FromBody] CameraConfigRequest config)
- {
- try
- {
- logger.Info("配置摄像头连接: {Address}:{Port}", config.Address, config.Port);
-
- var success = await _videoStreamService.ConfigureCameraAsync(config.Address, config.Port);
-
- if (success)
- {
- return TypedResults.Ok(new
- {
- success = true,
- message = "摄像头配置成功",
- cameraAddress = config.Address,
- cameraPort = config.Port
- });
- }
- else
- {
- return TypedResults.BadRequest(new
- {
- success = false,
- message = "摄像头配置失败",
- cameraAddress = config.Address,
- cameraPort = config.Port
- });
- }
- }
- catch (Exception ex)
- {
- logger.Error(ex, "配置摄像头连接失败");
- return TypedResults.InternalServerError(ex.Message);
- }
- }
-
- ///
- /// 获取当前摄像头配置
- ///
- /// 摄像头配置信息
- [HttpGet("CameraConfig")]
+ [HttpGet("MyEndpoint")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
- public IResult GetCameraConfig()
+ public IResult MyEndpoint()
{
try
{
- logger.Info("获取摄像头配置");
- var cameraStatus = _videoStreamService.GetCameraStatus();
+ var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
+ var endpoint = _videoStreamService.GetVideoEndpoint(boardId);
- return TypedResults.Ok(cameraStatus);
+ return TypedResults.Ok(endpoint);
}
catch (Exception ex)
{
@@ -232,22 +134,6 @@ public class VideoStreamController : ControllerBase
}
}
- ///
- /// 控制 HTTP 视频流服务开关
- ///
- /// 是否启用服务
- /// 操作结果
- [HttpPost("SetEnabled")]
- [EnableCors("Users")]
- [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
- public async Task SetEnabled([FromQuery] bool enabled)
- {
- logger.Info("设置视频流服务开关: {Enabled}", enabled);
- await _videoStreamService.SetEnable(enabled);
- return TypedResults.Ok();
- }
-
///
/// 测试 HTTP 视频流连接
///
@@ -260,32 +146,23 @@ public class VideoStreamController : ControllerBase
{
try
{
- logger.Info("测试 HTTP 视频流连接");
+ var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
+ var endpoint = _videoStreamService.GetVideoEndpoint(boardId);
// 尝试通过HTTP请求检查视频流服务是否可访问
bool isConnected = false;
using (var httpClient = new HttpClient())
{
httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间
- var response = await httpClient.GetAsync($"http://{Global.localhost}:{_videoStreamService.ServerPort}/");
+ var response = await httpClient.GetAsync(endpoint.MjpegUrl);
// 只要能连接上就认为成功,不管返回状态
isConnected = response.IsSuccessStatusCode;
}
- logger.Info("测试摄像头连接");
+ var ret = await _videoStreamService.TestCameraConnection(boardId);
- var (isSuccess, message) = await _videoStreamService.TestCameraConnectionAsync();
-
- return TypedResults.Ok(new
- {
- isConnected = isConnected,
- success = isSuccess,
- message = message,
- cameraAddress = _videoStreamService.CameraAddress,
- cameraPort = _videoStreamService.CameraPort,
- timestamp = DateTime.Now
- });
+ return TypedResults.Ok(ret);
}
catch (Exception ex)
{
@@ -295,6 +172,23 @@ public class VideoStreamController : ControllerBase
}
}
+ [HttpPost("DisableTransmission")]
+ public async Task DisableHdmiTransmission()
+ {
+ try
+ {
+ var boardId = TryGetBoardId().OrThrow(() => new ArgumentException("Board ID is required"));
+
+ await _videoStreamService.DisableHdmiTransmissionAsync(boardId.ToString());
+ return Ok($"HDMI transmission for board {boardId} disabled.");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Failed to disable HDMI transmission for board");
+ return StatusCode(500, $"Error disabling HDMI transmission: {ex.Message}");
+ }
+ }
+
///
/// 设置视频流分辨率
///
@@ -309,16 +203,16 @@ public class VideoStreamController : ControllerBase
{
try
{
- logger.Info($"设置视频流分辨率为 {request.Width}x{request.Height}");
+ var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
- var (isSuccess, message) = await _videoStreamService.SetResolutionAsync(request.Width, request.Height);
+ var ret = await _videoStreamService.SetResolutionAsync(boardId, request.Width, request.Height);
- if (isSuccess)
+ if (ret.IsSuccessful && ret.Value)
{
return TypedResults.Ok(new
{
success = true,
- message = message,
+ message = $"成功设置分辨率为 {request.Width}x{request.Height}",
width = request.Width,
height = request.Height,
timestamp = DateTime.Now
@@ -329,7 +223,7 @@ public class VideoStreamController : ControllerBase
return TypedResults.BadRequest(new
{
success = false,
- message = message,
+ message = ret.Error?.ToString() ?? "未知错误",
timestamp = DateTime.Now
});
}
@@ -341,37 +235,6 @@ public class VideoStreamController : ControllerBase
}
}
- ///
- /// 获取当前分辨率
- ///
- /// 当前分辨率信息
- [HttpGet("Resolution")]
- [EnableCors("Users")]
- [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
- public IResult GetCurrentResolution()
- {
- try
- {
- logger.Info("获取当前视频流分辨率");
-
- var (width, height) = _videoStreamService.GetCurrentResolution();
-
- return TypedResults.Ok(new
- {
- width = width,
- height = height,
- resolution = $"{width}x{height}",
- timestamp = DateTime.Now
- });
- }
- catch (Exception ex)
- {
- logger.Error(ex, "获取当前分辨率失败");
- return TypedResults.InternalServerError($"获取当前分辨率失败: {ex.Message}");
- }
- }
-
///
/// 获取支持的分辨率列表
///
@@ -382,29 +245,19 @@ public class VideoStreamController : ControllerBase
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
public IResult GetSupportedResolutions()
{
- try
+ // (640, 480, "640x480 (VGA)"),
+ // (960, 540, "960x540 (qHD)"),
+ // (1280, 720, "1280x720 (HD)"),
+ // (1280, 960, "1280x960 (SXGA)"),
+ // (1920, 1080, "1920x1080 (Full HD)")
+ return TypedResults.Ok(new AvailableResolutionsResponse[]
{
- logger.Info("获取支持的分辨率列表");
-
- var resolutions = _videoStreamService.GetSupportedResolutions();
-
- return TypedResults.Ok(new
- {
- resolutions = resolutions.Select(r => new
- {
- width = r.Width,
- height = r.Height,
- name = r.Name,
- value = $"{r.Width}x{r.Height}"
- }),
- timestamp = DateTime.Now
- });
- }
- catch (Exception ex)
- {
- logger.Error(ex, "获取支持的分辨率列表失败");
- return TypedResults.InternalServerError($"获取支持的分辨率列表失败: {ex.Message}");
- }
+ new AvailableResolutionsResponse { Width = 640, Height = 480, Name = "640x480(VGA)" },
+ new AvailableResolutionsResponse { Width = 960, Height = 480, Name = "960x480(qHD)" },
+ new AvailableResolutionsResponse { Width = 1280, Height = 720, Name = "1280x720(HD)" },
+ new AvailableResolutionsResponse { Width = 1280, Height = 960, Name = "1280x960(SXGA)" },
+ new AvailableResolutionsResponse { Width = 1920, Height = 1080, Name = "1920x1080(Full HD)" }
+ });
}
///
@@ -420,9 +273,9 @@ public class VideoStreamController : ControllerBase
{
try
{
- logger.Info("收到初始化自动对焦请求");
+ var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
- var result = await _videoStreamService.InitAutoFocusAsync();
+ var result = await _videoStreamService.InitAutoFocusAsync(boardId);
if (result)
{
@@ -465,9 +318,9 @@ public class VideoStreamController : ControllerBase
{
try
{
- logger.Info("收到执行自动对焦请求");
+ var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
- var result = await _videoStreamService.PerformAutoFocusAsync();
+ var result = await _videoStreamService.PerformAutoFocusAsync(boardId);
if (result)
{
@@ -496,61 +349,4 @@ public class VideoStreamController : ControllerBase
return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}");
}
}
-
- ///
- /// 执行一次自动对焦 (GET方式)
- ///
- /// 对焦结果
- [HttpGet("Focus")]
- [EnableCors("Users")]
- [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
- [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
- [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
- public async Task Focus()
- {
- try
- {
- logger.Info("收到执行一次对焦请求 (GET)");
-
- // 检查摄像头是否已配置
- if (!_videoStreamService.IsCameraConfigured())
- {
- logger.Warn("摄像头未配置,无法执行对焦");
- return TypedResults.BadRequest(new
- {
- success = false,
- message = "摄像头未配置,请先配置摄像头连接",
- timestamp = DateTime.Now
- });
- }
-
- var result = await _videoStreamService.PerformAutoFocusAsync();
-
- if (result)
- {
- logger.Info("对焦执行成功");
- return TypedResults.Ok(new
- {
- success = true,
- message = "对焦执行成功",
- timestamp = DateTime.Now
- });
- }
- else
- {
- logger.Warn("对焦执行失败");
- return TypedResults.BadRequest(new
- {
- success = false,
- message = "对焦执行失败",
- timestamp = DateTime.Now
- });
- }
- }
- catch (Exception ex)
- {
- logger.Error(ex, "执行对焦时发生异常");
- return TypedResults.InternalServerError($"执行对焦失败: {ex.Message}");
- }
- }
}
diff --git a/server/src/Peripherals/CameraClient.cs b/server/src/Peripherals/CameraClient.cs
index c201318..7771d1e 100644
--- a/server/src/Peripherals/CameraClient.cs
+++ b/server/src/Peripherals/CameraClient.cs
@@ -1,6 +1,5 @@
using System.Net;
using DotNext;
-using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.CameraClient;
@@ -16,7 +15,7 @@ static class CameraAddr
public const UInt32 CAMERA_POWER = BASE + 0x10; //[0]: rstn, 0 is reset. [8]: power down, 1 is down.
}
-class Camera
+public class Camera
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -276,7 +275,7 @@ class Camera
{
var currentAddress = (UInt16)(baseAddress + i - 1);
var data = (byte)cmd[i];
-
+
logger.Debug($"ConfigureRegisters: 写入地址=0x{currentAddress:X4}, 数据=0x{data:X2}");
// 准备I2C数据:16位地址 + 8位数据
@@ -322,14 +321,14 @@ class Camera
public async ValueTask> ReadRegister(UInt16 register)
{
var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.taskID, this.timeout);
-
+
// Convert 16-bit register address to byte array
var registerBytes = new byte[] { (byte)(register >> 8), (byte)(register & 0xFF) };
-
+
var ret = await i2c.ReadData(CAM_I2C_ADDR, registerBytes, 1, CAM_PROTO);
if (!ret.IsSuccessful)
return new(ret.Error);
-
+
return new Result(ret.Value[0]);
}
@@ -412,25 +411,25 @@ class Camera
[0x3801, unchecked((byte)(hStart & 0xFF))],
[0x3802, unchecked((byte)((vStart >> 8) & 0xFF))],
[0x3803, unchecked((byte)(vStart & 0xFF))],
-
+
// H_END/V_END
[0x3804, unchecked((byte)((hEnd >> 8) & 0xFF))],
[0x3805, unchecked((byte)(hEnd & 0xFF))],
[0x3806, unchecked((byte)((vEnd >> 8) & 0xFF))],
[0x3807, unchecked((byte)(vEnd & 0xFF))],
-
+
// 输出像素个数
[0x3808, unchecked((byte)((dvpHo >> 8) & 0xFF))],
[0x3809, unchecked((byte)(dvpHo & 0xFF))],
[0x380A, unchecked((byte)((dvpVo >> 8) & 0xFF))],
[0x380B, unchecked((byte)(dvpVo & 0xFF))],
-
+
// 总像素
[0x380C, unchecked((byte)((hts >> 8) & 0xFF))],
[0x380D, unchecked((byte)(hts & 0xFF))],
[0x380E, unchecked((byte)((vts >> 8) & 0xFF))],
[0x380F, unchecked((byte)(vts & 0xFF))],
-
+
// H_OFFSET/V_OFFSET
[0x3810, unchecked((byte)((hOffset >> 8) & 0xFF))],
[0x3811, unchecked((byte)(hOffset & 0xFF))],
@@ -521,7 +520,7 @@ class Camera
hOffset: 16, vOffset: 4,
hWindow: 2624, vWindow: 1456
);
-
+
}
///
@@ -537,7 +536,7 @@ class Camera
hOffset: 16, vOffset: 4,
hWindow: 2624, vWindow: 1456
);
-
+
}
///
@@ -637,7 +636,7 @@ class Camera
[0x3008, 0x42] // 休眠命令
};
- return await ConfigureRegisters(sleepRegisters, customDelayMs: 50);
+ return await ConfigureRegisters(sleepRegisters, customDelayMs: 50);
}
///
@@ -1305,7 +1304,7 @@ class Camera
UInt16 firmwareAddr = 0x8000;
var firmwareCommand = new UInt16[1 + OV5640_AF_FIRMWARE.Length];
firmwareCommand[0] = firmwareAddr;
-
+
// 将固件数据复制到命令数组中
for (int i = 0; i < OV5640_AF_FIRMWARE.Length; i++)
{
@@ -1425,7 +1424,7 @@ class Camera
logger.Error($"自动对焦超时,状态: 0x{readResult.Value:X2}");
return new(new Exception($"自动对焦超时,状态: 0x{readResult.Value:X2}"));
}
-
+
await Task.Delay(100);
}
diff --git a/server/src/Peripherals/DebuggerClient.cs b/server/src/Peripherals/DebuggerClient.cs
index c85da01..609b150 100644
--- a/server/src/Peripherals/DebuggerClient.cs
+++ b/server/src/Peripherals/DebuggerClient.cs
@@ -55,28 +55,28 @@ class DebuggerCmd
public const UInt32 ClearSignal = 0xFFFF_FFFF;
}
-///
+///
/// 信号捕获模式枚举
///
public enum CaptureMode : byte
{
- ///
+ ///
/// 无捕获模式
///
None = 0,
- ///
+ ///
/// 低电平触发模式
///
Logic0 = 1,
- ///
+ ///
/// 高电平触发模式
///
Logic1 = 2,
- ///
+ ///
/// 上升沿触发模式
///
Rise = 3,
- ///
+ ///
/// 下降沿触发模式
///
Fall = 4,
@@ -170,7 +170,7 @@ public class DebuggerClient
/// 操作结果,成功返回状态标志字节,失败返回错误信息
public async ValueTask> ReadFlag()
{
- var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, DebuggerAddr.Signal, this.timeout);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, DebuggerAddr.Signal, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read flag: {ret.Error}");
diff --git a/server/src/Peripherals/I2cClient.cs b/server/src/Peripherals/I2cClient.cs
index 878e891..7ea28f4 100644
--- a/server/src/Peripherals/I2cClient.cs
+++ b/server/src/Peripherals/I2cClient.cs
@@ -8,8 +8,8 @@ static class I2cAddr
const UInt32 Base = 0x6000_0000;
- ///
- /// 0x0000_0000:
+ ///
+ /// 0x0000_0000:
/// [7:0] 本次传输的i2c地址(最高位总为0);
/// [8] 1为读,0为写;
/// [16] 1为SCCB协议,0为I2C协议;
@@ -17,45 +17,45 @@ static class I2cAddr
///
public const UInt32 BaseConfig = Base + 0x0000_0000;
- ///
+ ///
/// 0x0000_0001:
/// [15:0] 本次传输的数据量(以字节为单位,0为传1个字节);
/// [31:16] 若本次传输为读的DUMMY数据量(字节为单位,0为传1个字节)
///
public const UInt32 TranConfig = Base + 0x0000_0001;
- ///
+ ///
/// 0x0000_0002: [0] cmd_done; [8] cmd_error;
///
public const UInt32 Flag = Base + 0x0000_0002;
- ///
+ ///
/// 0x0000_0003: FIFO写入口,仅低8位有效,只写
///
public const UInt32 Write = Base + 0x0000_0003;
- ///
+ ///
/// 0x0000_0004: FIFO读出口,仅低8位有效,只读
///
public const UInt32 Read = Base + 0x0000_0004;
- ///
+ ///
/// 0x0000_0005: [0] FIFO写入口清空;[8] FIFO读出口清空;
///
public const UInt32 Clear = Base + 0x0000_0005;
}
-///
+///
/// [TODO:Enum]
///
public enum I2cProtocol
{
- ///
+ ///
/// [TODO:Enum]
///
I2c = 0,
- ///
+ ///
/// [TODO:Enum]
///
SCCB = 1
@@ -296,7 +296,7 @@ public class I2c
// 读取数据
{
- var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, I2cAddr.Read);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, I2cAddr.Read);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read data from I2C FIFO: {ret.Error}");
diff --git a/server/src/Peripherals/JpegClient.cs b/server/src/Peripherals/JpegClient.cs
new file mode 100644
index 0000000..870c441
--- /dev/null
+++ b/server/src/Peripherals/JpegClient.cs
@@ -0,0 +1,281 @@
+using System.Net;
+using DotNext;
+using Common;
+
+namespace Peripherals.JpegClient;
+
+static class JpegAddr
+{
+ const UInt32 BASE = 0x0000_0000;
+ public const UInt32 ENABLE = BASE + 0x0;
+ public const UInt32 FRAME_NUM = BASE + 0x1;
+ public const UInt32 FRAME_INFO = BASE + 0x2;
+ public const UInt32 FRAME_SAMPLE_RATE = BASE + 0x3;
+ public const UInt32 FRAME_DATA_MAX_POINTER = BASE + 0x4;
+
+ public const UInt32 DDR_FRAME_DATA_ADDR = 0x0000_0000;
+ public const UInt32 DDR_FRAME_DATA_MAX_ADDR = 0x8000_0000;
+}
+
+public class JpegInfo
+{
+ public UInt32 Width { get; set; }
+ public UInt32 Height { get; set; }
+ public UInt32 Size { get; set; }
+
+ public JpegInfo(UInt32 width, UInt32 height, UInt32 size)
+ {
+ Width = width;
+ Height = height;
+ Size = size;
+ }
+
+ public JpegInfo(byte[] data)
+ {
+ if (data.Length < 8)
+ throw new ArgumentException("Invalid data length", nameof(data));
+
+ Width = ((UInt32)(data[5] << 8 + data[6] & 0xF0));
+ Height = ((UInt32)((data[6] & 0x0F) << 4 + data[7]));
+ Size = Number.BytesToUInt32(data, 0, 4).Value;
+ }
+}
+
+public enum JpegSampleRate : UInt32
+{
+ RATE_1_1 = 0b1111_1111_1111_1111_1111_1111_1111_1111,
+ RATE_1_2 = 0b1010_1010_1010_1010_1010_1010_1010_1010,
+ RATE_1_4 = 0b1000_1000_1000_1000_1000_1000_1000_1000,
+ RATE_3_4 = 0b1110_1110_1110_1110_1110_1110_1110_1110,
+ RATE_1_8 = 0b1000_0000_1000_0000_1000_0000_1000_0000,
+ RATE_3_8 = 0b1001_0010_0100_1001_1001_0010_0100_1001,
+ RATE_7_8 = 0b1111_1110_1111_1110_1111_1110_1111_1110,
+ RATE_1_16 = 0b1000_0000_0000_0000_1000_0000_0000_0000,
+ RATE_3_16 = 0b1000_0100_0010_0000_1000_0100_0010_0000,
+ RATE_5_16 = 0b1001_0001_0010_0010_0100_0100_1000_1001,
+ RATE_15_16 = 0b1111_1111_1111_1110_1111_1111_1111_1110,
+ RATE_1_32 = 0b1000_0000_0000_0000_0000_0000_0000_0000,
+ RATE_31_32 = 0b1111_1111_1111_1111_1111_1111_1111_1110,
+}
+
+public class Jpeg
+{
+ private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
+ readonly int timeout = 2000;
+ readonly int taskID;
+ readonly int port;
+ readonly string address;
+ private IPEndPoint ep;
+
+ public Jpeg(string address, int port, int taskID, int timeout = 2000)
+ {
+ if (timeout < 0)
+ throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
+ this.address = address;
+ this.taskID = taskID;
+ this.port = port;
+ this.ep = new IPEndPoint(IPAddress.Parse(address), port);
+ this.timeout = timeout;
+ }
+
+ public async ValueTask SetEnable(bool enable)
+ {
+ var ret = await UDPClientPool.WriteAddr(
+ this.ep, this.taskID, JpegAddr.ENABLE, Convert.ToUInt32(enable), this.timeout);
+ if (!ret.IsSuccessful)
+ {
+ logger.Error($"Failed to set JPEG enable: {ret.Error}");
+ return false;
+ }
+ return ret.Value;
+ }
+
+ public async ValueTask SetSampleRate(uint rate)
+ {
+ var ret = await UDPClientPool.WriteAddr(
+ this.ep, this.taskID, JpegAddr.FRAME_SAMPLE_RATE, rate, this.timeout);
+ if (!ret.IsSuccessful)
+ {
+ logger.Error($"Failed to set JPEG sample rate: {ret.Error}");
+ return false;
+ }
+ return ret.Value;
+ }
+
+ public async ValueTask SetSampleRate(JpegSampleRate rate)
+ {
+ return await SetSampleRate((uint)rate);
+ }
+
+ public async ValueTask GetFrameNumber()
+ {
+ var ret = await UDPClientPool.ReadAddrByte(
+ this.ep, this.taskID, JpegAddr.FRAME_NUM, this.timeout);
+ if (!ret.IsSuccessful)
+ {
+ logger.Error($"Failed to get JPEG frame number: {ret.Error}");
+ return 0;
+ }
+ return Number.BytesToUInt32(ret.Value.Options.Data ?? Array.Empty()).Value;
+ }
+
+ public async ValueTask>> GetFrameInfo(int num)
+ {
+ var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, JpegAddr.FRAME_INFO, num, this.timeout);
+ if (!ret.IsSuccessful)
+ {
+ logger.Error($"Failed to get JPEG frame info: {ret.Error}");
+ return new(null);
+ }
+
+ var data = ret.Value.Options.Data;
+ if (data == null || data.Length == 0)
+ {
+ logger.Error($"Data is null or empty");
+ return new(null);
+ }
+ if (data.Length != num * 2)
+ {
+ logger.Error(
+ $"Data length should be {num * 2} bytes, instead of {data.Length} bytes");
+ return new(null);
+ }
+
+ var infos = new List();
+ for (int i = 0; i < num; i++)
+ {
+ infos.Add(new JpegInfo(data[i..(i + 1)]));
+ }
+ return new(infos);
+ }
+
+ public async ValueTask UpdatePointer(uint cnt)
+ {
+ var ret = await UDPClientPool.WriteAddr(
+ this.ep, this.taskID, JpegAddr.FRAME_DATA_MAX_POINTER, cnt, this.timeout);
+ if (!ret.IsSuccessful)
+ {
+ logger.Error($"Failed to update pointer: {ret.Error}");
+ return false;
+ }
+ return ret.Value;
+ }
+
+ public async ValueTask> GetFrame(uint offset, uint length)
+ {
+ if (!MsgBus.IsRunning)
+ {
+ logger.Error("Message bus is not running");
+ return new(new Exception("Message bus is not running"));
+ }
+ MsgBus.UDPServer.ClearUDPData(this.ep.Address.ToString(), this.ep.Port);
+
+ var firstReadLength = (int)(Math.Min(length, JpegAddr.DDR_FRAME_DATA_MAX_ADDR - offset));
+ var secondReadLength = (int)(length - firstReadLength);
+ var dataBytes = new byte[length];
+
+ {
+ var ret = await UDPClientPool.ReadAddr4Bytes(
+ this.ep, this.taskID, JpegAddr.DDR_FRAME_DATA_ADDR + offset, firstReadLength, this.timeout);
+ if (!ret.IsSuccessful)
+ {
+ logger.Error($"Failed to get JPEG frame data: {ret.Error}");
+ return null;
+ }
+ if (ret.Value.Length != firstReadLength)
+ {
+ logger.Error($"Data length should be {firstReadLength} bytes, instead of {ret.Value.Length} bytes");
+ return null;
+ }
+ Buffer.BlockCopy(ret.Value, 0, dataBytes, 0, firstReadLength);
+ }
+
+ if (secondReadLength > 0)
+ {
+ var ret = await UDPClientPool.ReadAddr4Bytes(
+ this.ep, this.taskID, JpegAddr.DDR_FRAME_DATA_ADDR, secondReadLength, this.timeout);
+ if (!ret.IsSuccessful)
+ {
+ logger.Error($"Failed to get JPEG frame data: {ret.Error}");
+ return null;
+ }
+ if (ret.Value.Length != secondReadLength)
+ {
+ logger.Error($"Data length should be {secondReadLength} bytes, instead of {ret.Value.Length} bytes");
+ return null;
+ }
+ Buffer.BlockCopy(ret.Value, 0, dataBytes, firstReadLength, secondReadLength);
+ }
+
+ return dataBytes;
+ }
+
+ public async ValueTask> GetMultiFrames(uint offset, uint[] sizes)
+ {
+ var frames = new List();
+ for (int i = 0; i < sizes.Length; i++)
+ {
+ var ret = await GetFrame(offset, sizes[i]);
+ if (!ret.IsSuccessful)
+ {
+ logger.Error($"Failed to get JPEG frame {i} data: {ret.Error}");
+ continue;
+ }
+ if (ret.Value == null)
+ {
+ logger.Error($"Frame {i} data is null");
+ continue;
+ }
+ if (ret.Value.Length != sizes[i])
+ {
+ logger.Error(
+ $"Frame {i} data length should be {sizes[i]} bytes, instead of {ret.Value.Length} bytes");
+ continue;
+ }
+
+ frames.Add(ret.Value);
+ offset += sizes[i];
+ }
+
+ {
+ var ret = await UpdatePointer((uint)sizes.Length);
+ if (!ret) logger.Error($"Failed to update pointer");
+ }
+
+ return frames;
+ }
+
+ public async ValueTask?>> GetMultiFrames(uint offset)
+ {
+ if (!MsgBus.IsRunning)
+ {
+ logger.Error("Message bus is not running");
+ return new(new Exception("Message bus is not running"));
+ }
+ MsgBus.UDPServer.ClearUDPData(this.ep.Address.ToString(), this.ep.Port);
+
+ var frameNum = await GetFrameNumber();
+ if (frameNum == 0) return null;
+
+ List? frameSizes = null;
+ {
+ var ret = await GetFrameInfo((int)frameNum);
+ if (!ret.HasValue || ret.Value.Count == 0)
+ {
+ logger.Error($"Failed to get frame info");
+ return null;
+ }
+ frameSizes = ret.Value.Select(x => x.Size).ToList();
+ }
+
+ var frames = await GetMultiFrames(offset, frameSizes.ToArray());
+ if (frames.Count == 0)
+ {
+ logger.Error($"Failed to get frames");
+ return null;
+ }
+
+ return frames;
+ }
+}
diff --git a/server/src/Peripherals/LogicAnalyzerClient.cs b/server/src/Peripherals/LogicAnalyzerClient.cs
index 342ff5d..c6764d9 100644
--- a/server/src/Peripherals/LogicAnalyzerClient.cs
+++ b/server/src/Peripherals/LogicAnalyzerClient.cs
@@ -12,7 +12,7 @@ static class AnalyzerAddr
const UInt32 DMA1_BASE = 0x7000_0000;
const UInt32 DDR_BASE = 0x0000_0000;
- ///
+ ///
/// 0x0000_0000 R/W [ 0] capture on: 置1开始等待捕获,0停止捕获。捕获到信号后该位自动清零。
/// [ 8] capture force: 置1则强制捕获信号,自动置0。
/// [16] capture busy: 1为逻辑分析仪正在捕获信号。
@@ -21,7 +21,7 @@ static class AnalyzerAddr
///
public const UInt32 CAPTURE_MODE = BASE + 0x0000_0000;
- ///
+ ///
/// 0x0000_0001 R/W [1:0] global trig mode: 00: 全局与 (&)
/// 01: 全局或 (|)
/// 10: 全局非与(~&)
@@ -29,7 +29,7 @@ static class AnalyzerAddr
///
public const UInt32 GLOBAL_TRIG_MODE = BASE + 0x0000_0001;
- ///
+ ///
/// 0x0000_0010 - 0x0000_0017 R/W [5:0] 信号M的触发操作符,共8路
/// [5:3] M's Operator: 000 ==
/// 001 !=
@@ -73,7 +73,7 @@ static class AnalyzerAddr
public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014;
public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0100_0000;
- ///
+ ///
/// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储,得到的32位数据中低八位最先捕获,高八位最后捕获。
/// 共1024个地址,每个地址存储4组,深度为4096。
///
@@ -87,53 +87,53 @@ static class AnalyzerAddr
[Flags]
public enum CaptureStatus
{
- ///
+ ///
/// 无状态标志
///
None = 0,
- ///
+ ///
/// 捕获使能位,置1开始等待捕获,0停止捕获。捕获到信号后该位自动清零
///
CaptureOn = 1 << 0, // [0] 捕获使能
- ///
+ ///
/// 强制捕获位,置1则强制捕获信号,自动置0
///
CaptureForce = 1 << 8, // [8] 强制捕获
- ///
+ ///
/// 捕获忙碌位,1为逻辑分析仪正在捕获信号
///
CaptureBusy = 1 << 16, // [16] 捕获进行中
- ///
+ ///
/// 捕获完成位,1为逻辑分析仪内存完整存储了此次捕获的信号
///
CaptureDone = 1 << 24 // [24] 捕获完成
}
-///
+///
/// 全局触发模式枚举,定义多路信号触发条件的逻辑组合方式
///
public enum GlobalCaptureMode
{
- ///
+ ///
/// 全局与模式,所有触发条件都必须满足
///
AND = 0b00,
- ///
+ ///
/// 全局或模式,任一触发条件满足即可
///
OR = 0b01,
- ///
+ ///
/// 全局非与模式,不是所有触发条件都满足
///
NAND = 0b10,
- ///
+ ///
/// 全局非或模式,所有触发条件都不满足
///
NOR = 0b11
@@ -144,32 +144,32 @@ public enum GlobalCaptureMode
///
public enum AnalyzerClockDiv
{
- ///
+ ///
/// 1分频
///
DIV1 = 0x0000_0000,
- ///
+ ///
/// 2分频
///
DIV2 = 0x0000_0001,
- ///
+ ///
/// 4分频
///
DIV4 = 0x0000_0002,
- ///
+ ///
/// 8分频
///
DIV8 = 0x0000_0003,
- ///
+ ///
/// 16分频
///
DIV16 = 0x0000_0004,
- ///
+ ///
/// 32分频
///
DIV32 = 0x0000_0005,
@@ -190,27 +190,27 @@ public enum AnalyzerClockDiv
///
public enum SignalOperator : byte
{
- ///
+ ///
/// 等于操作符
///
Equal = 0b000, // ==
- ///
+ ///
/// 不等于操作符
///
NotEqual = 0b001, // !=
- ///
+ ///
/// 小于操作符
///
LessThan = 0b010, // <
- ///
+ ///
/// 小于等于操作符
///
LessThanOrEqual = 0b011, // <=
- ///
+ ///
/// 大于操作符
///
GreaterThan = 0b100, // >
- ///
+ ///
/// 大于等于操作符
///
GreaterThanOrEqual = 0b101 // >=
@@ -221,35 +221,35 @@ public enum SignalOperator : byte
///
public enum SignalValue : byte
{
- ///
+ ///
/// 逻辑0电平
///
Logic0 = 0b000, // LOGIC 0
- ///
+ ///
/// 逻辑1电平
///
Logic1 = 0b001, // LOGIC 1
- ///
+ ///
/// 不关心该信号状态
///
NotCare = 0b010, // X(not care)
- ///
+ ///
/// 上升沿触发
///
Rise = 0b011, // RISE
- ///
+ ///
/// 下降沿触发
///
Fall = 0b100, // FALL
- ///
+ ///
/// 上升沿或下降沿触发
///
RiseOrFall = 0b101, // RISE OR FALL
- ///
+ ///
/// 信号无变化
///
NoChange = 0b110, // NOCHANGE
- ///
+ ///
/// 特定数值
///
SomeNumber = 0b111 // SOME NUMBER
@@ -260,11 +260,11 @@ public enum SignalValue : byte
///
public enum AnalyzerChannelDiv
{
- ///
+ ///
/// 1路
///
ONE = 0x0000_0000,
- ///
+ ///
/// 2路
///
TWO = 0x0000_0001,
@@ -366,7 +366,7 @@ public class Analyzer
/// 操作结果,成功返回寄存器值,否则返回异常信息
public async ValueTask> ReadCaptureStatus()
{
- var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, this.timeout);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read capture status: {ret.Error}");
diff --git a/server/src/Peripherals/OscilloscopeClient.cs b/server/src/Peripherals/OscilloscopeClient.cs
index 173924e..d3055a1 100644
--- a/server/src/Peripherals/OscilloscopeClient.cs
+++ b/server/src/Peripherals/OscilloscopeClient.cs
@@ -9,57 +9,57 @@ static class OscilloscopeAddr
{
const UInt32 BASE = 0x8000_0000;
- ///
+ ///
/// 0x0000_0000:R/W[0] wave_run 启动捕获/关闭
///
public const UInt32 START_CAPTURE = BASE + 0x0000_0000;
- ///
+ ///
/// 0x0000_0001: R/W[7:0] trig_level 触发电平
///
public const UInt32 TRIG_LEVEL = BASE + 0x0000_0001;
- ///
+ ///
/// 0x0000_0002:R/W[0] trig_edge 触发边沿,0-下降沿,1-上升沿
///
public const UInt32 TRIG_EDGE = BASE + 0x0000_0002;
- ///
+ ///
/// 0x0000_0003: R/W[9:0] h shift 水平偏移量
///
public const UInt32 H_SHIFT = BASE + 0x0000_0003;
- ///
+ ///
/// 0x0000_0004: R/W[9:0] deci rate 抽样率,0—1023
///
public const UInt32 DECI_RATE = BASE + 0x0000_0004;
- ///
+ ///
/// 0x0000_0005:R/W[0] ram refresh RAM刷新
///
public const UInt32 RAM_FRESH = BASE + 0x0000_0005;
- ///
+ ///
/// 0x0000 0006:R[19: 0] ad_freq AD采样频率
///
public const UInt32 AD_FREQ = BASE + 0x0000_0006;
- ///
+ ///
/// Ox0000_0007: R[7:0] ad_vpp AD采样幅度
///
public const UInt32 AD_VPP = BASE + 0x0000_0007;
- ///
+ ///
/// 0x0000_0008: R[7:0] ad max AD采样最大值
///
public const UInt32 AD_MAX = BASE + 0x0000_0008;
- ///
+ ///
/// 0x0000_0009: R[7:0] ad_min AD采样最小值
///
public const UInt32 AD_MIN = BASE + 0x0000_0009;
- ///
+ ///
/// 0x0000_1000-0x0000_13FF:R[7:0] wave_rd_data 共1024个字节
///
public const UInt32 RD_DATA_ADDR = BASE + 0x0000_1000;
@@ -232,7 +232,7 @@ class Oscilloscope
/// 操作结果,成功返回采样频率值,否则返回异常信息
public async ValueTask> GetADFrequency()
{
- var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_FREQ, this.timeout);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_FREQ, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read AD frequency: {ret.Error}");
@@ -255,7 +255,7 @@ class Oscilloscope
/// 操作结果,成功返回采样幅度值,否则返回异常信息
public async ValueTask> GetADVpp()
{
- var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_VPP, this.timeout);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_VPP, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read AD VPP: {ret.Error}");
@@ -275,7 +275,7 @@ class Oscilloscope
/// 操作结果,成功返回采样最大值,否则返回异常信息
public async ValueTask> GetADMax()
{
- var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_MAX, this.timeout);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_MAX, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read AD max: {ret.Error}");
@@ -295,7 +295,7 @@ class Oscilloscope
/// 操作结果,成功返回采样最小值,否则返回异常信息
public async ValueTask> GetADMin()
{
- var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_MIN, this.timeout);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_MIN, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read AD min: {ret.Error}");
diff --git a/server/src/Peripherals/RemoteUpdateClient.cs b/server/src/Peripherals/RemoteUpdateClient.cs
index 296afb8..922f3e2 100644
--- a/server/src/Peripherals/RemoteUpdateClient.cs
+++ b/server/src/Peripherals/RemoteUpdateClient.cs
@@ -7,20 +7,20 @@ static class RemoteUpdaterAddr
{
public const UInt32 Base = 0x20_00_00_00;
- ///
+ ///
/// ADDR: 0X00: 写Flash-读写地址——控制位
/// [31:16]: wr_sector_num
/// [15: 0]: {flash_wr_en,-,-,-, start_wr_sector}
///
public const UInt32 WriteCtrl = Base + 0x00;
- ///
+ ///
/// ADDR: 0X01: 写Flash-只写地址——FIFO入口
/// [31:0]: 写比特流数据入口
///
public const UInt32 WriteFIFO = Base + 0x01;
- ///
+ ///
/// ADDR: 0X02: 写Flash-只读地址——标志位
/// [31:24]: {-, -, -, -, -, -, -, wr_fifo_full}
/// [23:16]: {-, -, -, -, -, -, -, wr_fifo_empty}
@@ -29,14 +29,14 @@ static class RemoteUpdaterAddr
///
public const UInt32 WriteSign = Base + 0x02;
- ///
+ ///
/// ADDR: 0X03: 读Flash-读写地址——控制位1
/// [31:16]: rd_sector_num
/// [15: 0]: {flash_rd_en,-,-,-, start_rd_sub_sector}
///
public const UInt32 ReadCtrl1 = Base + 0x03;
- ///
+ ///
/// ADDR: 0X04: 读Flash-读写地址——控制位2
/// [31:24]: { }
/// [23:16]: {-, -, -, -, -, -,{ bs_crc32_ok }}
@@ -45,19 +45,19 @@ static class RemoteUpdaterAddr
///
public const UInt32 ReadCtrl2 = Base + 0x04;
- ///
+ ///
/// ADDR: 0X05: 读Flash-只读地址——FIFO出口
/// [31:0]: 读比特流数据出口
///
public const UInt32 ReadFIFO = Base + 0x05;
- ///
+ ///
/// ADDR: 0X06: 读Flash-只读地址——CRC校验值
/// [31:0]: CRC校验值 bs_readback_crc
///
public const UInt32 ReadCRC = Base + 0x06;
- ///
+ ///
/// ADDR: 0X07: 读Flash-只读地址——标志位
/// [31:24]: {-, -, -, -, -, -, -, rd_fifo_afull}
/// [23:16]: {-, -, -, -, -, -, -, rd_fifo_empty}
@@ -66,14 +66,14 @@ static class RemoteUpdaterAddr
///
public const UInt32 ReadSign = Base + 0x07;
- ///
+ ///
/// ADDR: 0X08: 热启动开关-读写地址——控制位
/// [31: 8]: hotreset_addr
/// [ 7: 0]: {-, -, -, -, -, -, -, hotreset_en}
///
public const UInt32 HotResetCtrl = Base + 0x08;
- ///
+ ///
/// ADDR: 0X09: 只读地址 版本号
/// [31: 0]: FPGA_VERSION[31:0]
///
@@ -339,7 +339,7 @@ public class RemoteUpdater
}
{
- var ret = await UDPClientPool.ReadAddr(this.ep, 0, RemoteUpdaterAddr.ReadCRC, this.timeout);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, 0, RemoteUpdaterAddr.ReadCRC, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
var bytes = ret.Value.Options.Data;
@@ -543,7 +543,7 @@ public class RemoteUpdater
logger.Trace("Clear udp data finished");
{
- var ret = await UDPClientPool.ReadAddr(this.ep, 0, RemoteUpdaterAddr.Version, this.timeout);
+ var ret = await UDPClientPool.ReadAddrByte(this.ep, 0, RemoteUpdaterAddr.Version, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
var retData = ret.Value.Options.Data;
diff --git a/server/src/Services/HttpHdmiVideoStreamService.cs b/server/src/Services/HttpHdmiVideoStreamService.cs
index a19aacf..d6475cf 100644
--- a/server/src/Services/HttpHdmiVideoStreamService.cs
+++ b/server/src/Services/HttpHdmiVideoStreamService.cs
@@ -23,7 +23,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
public override async Task StartAsync(CancellationToken cancellationToken)
{
_httpListener = new HttpListener();
- _httpListener.Prefixes.Add($"http://*:{_serverPort}/");
+ _httpListener.Prefixes.Add($"http://{Global.localhost}:{_serverPort}/");
_httpListener.Start();
logger.Info($"HDMI Video Stream Service started on port {_serverPort}");
diff --git a/server/src/Services/HttpVideoStreamService.cs b/server/src/Services/HttpVideoStreamService.cs
index 16dbd34..f06967e 100644
--- a/server/src/Services/HttpVideoStreamService.cs
+++ b/server/src/Services/HttpVideoStreamService.cs
@@ -1,6 +1,8 @@
using System.Net;
using System.Text;
-using Peripherals.CameraClient; // 添加摄像头客户端引用
+using System.Collections.Concurrent;
+using DotNext;
+using DotNext.Threading;
#if USB_CAMERA
using OpenCvSharp;
@@ -8,30 +10,47 @@ using OpenCvSharp;
namespace server.Services;
+public class VideoStreamClient
+{
+ public string? ClientId { get; set; } = string.Empty;
+ public int FrameWidth { get; set; }
+ public int FrameHeight { get; set; }
+ public int FrameRate { get; set; }
+ public Peripherals.CameraClient.Camera Camera { get; set; }
+ public CancellationTokenSource CTS { get; set; }
+ public readonly AsyncReaderWriterLock Lock = new();
+
+ public VideoStreamClient(
+ string clientId, int width, int height, Peripherals.CameraClient.Camera camera)
+ {
+ ClientId = clientId;
+ FrameWidth = width;
+ FrameHeight = height;
+ FrameRate = 0;
+ Camera = camera;
+ CTS = new CancellationTokenSource();
+ }
+}
+
///
/// 表示摄像头连接状态信息
///
-public class CameraStatus
+public class VideoEndpoint
{
- ///
- /// 摄像头的IP地址
- ///
- public string Address { get; set; } = string.Empty;
+ public string BoardId { get; set; } = "";
+ public string MjpegUrl { get; set; } = "";
+ public string VideoUrl { get; set; } = "";
+ public string SnapshotUrl { get; set; } = "";
///
- /// 摄像头的端口号
+ /// 视频流的帧率(FPS)
///
- public int Port { get; set; }
+ public int FrameRate { get; set; }
///
- /// 是否已配置摄像头
+ /// 视频分辨率(如 640x480)
///
- public bool IsConfigured { get; set; }
-
- ///
- /// 摄像头连接字符串(IP:端口)
- ///
- public string ConnectionString { get; set; } = string.Empty;
+ public string Resolution { get; set; } = string.Empty;
}
///
@@ -50,29 +69,14 @@ public class ServiceStatus
public int ServerPort { get; set; }
///
- /// 视频流的帧率(FPS)
+ /// 当前连接的客户端端点列表
///
- public int FrameRate { get; set; }
-
- ///
- /// 视频分辨率(如 640x480)
- ///
- public string Resolution { get; set; } = string.Empty;
+ public List ClientEndpoints { get; set; } = new();
///
/// 当前连接的客户端数量
///
- public int ConnectedClients { get; set; }
-
- ///
- /// 当前连接的客户端端点列表
- ///
- public List ClientEndpoints { get; set; } = new();
-
- ///
- /// 摄像头连接状态信息
- ///
- public CameraStatus CameraStatus { get; set; } = new();
+ public int ConnectedClientsNum => ClientEndpoints.Count;
}
///
@@ -82,21 +86,11 @@ public class ServiceStatus
public class HttpVideoStreamService : BackgroundService
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
private HttpListener? _httpListener;
private readonly int _serverPort = 4321;
- private readonly int _frameRate = 30; // 30 FPS
- // 动态分辨率配置
- private int _frameWidth = 640; // 默认640x480
- private int _frameHeight = 480;
- private readonly object _resolutionLock = new object();
-
- // 摄像头客户端
- private Camera? _camera;
- private bool _cameraEnable = false;
- private string _cameraAddress = "192.168.1.100"; // 默认FPGA地址
- private int _cameraPort = 8888; // 默认端口
- private readonly object _cameraLock = new object();
+ private readonly ConcurrentDictionary _clientDict = new();
// USB Camera 相关
#if USB_CAMERA
@@ -105,192 +99,83 @@ public class HttpVideoStreamService : BackgroundService
private readonly object _usbCameraLock = new object();
#endif
- // 模拟 FPGA 图像数据
- private int _frameCounter = 0;
- private readonly List _activeClients = new List();
- private readonly object _clientsLock = new object();
-
- ///
- /// 获取当前连接的客户端数量
- ///
- public int ConnectedClientsCount { get { return _activeClients.Count; } }
-
- ///
- /// 获取服务端口
- ///
- public int ServerPort => _serverPort;
-
- ///
- /// 获取帧宽度
- ///
- public int FrameWidth => _frameWidth;
-
- ///
- /// 获取帧高度
- ///
- public int FrameHeight => _frameHeight;
-
- ///
- /// 获取帧率
- ///
- public int FrameRate => _frameRate;
-
- ///
- /// 获取当前摄像头地址
- ///
- public string CameraAddress { get { return _cameraAddress; } }
-
- ///
- /// 获取当前摄像头端口
- ///
- public int CameraPort { get { return _cameraPort; } }
-
///
/// 初始化 HttpVideoStreamService
///
- public HttpVideoStreamService()
+ public override async Task StartAsync(CancellationToken cancellationToken)
{
- // 延迟初始化摄像头客户端,直到配置完成
- logger.Info("HttpVideoStreamService 初始化完成,默认摄像头地址: {Address}:{Port}", _cameraAddress, _cameraPort);
+ _httpListener = new HttpListener();
+ _httpListener.Prefixes.Add($"http://{Global.localhost}:{_serverPort}/");
+ _httpListener.Start();
+ logger.Info($"Video Stream Service started on port {_serverPort}");
+
+ await base.StartAsync(cancellationToken);
}
///
- /// [TODO:description]
+ /// 停止 HTTP 视频流服务
///
- /// [TODO:parameter]
- /// [TODO:return]
- public async Task SetEnable(bool isEnabled)
+ public override async Task StopAsync(CancellationToken cancellationToken)
{
- if (_camera == null)
+ foreach (var clientKey in _clientDict.Keys)
{
- throw new Exception("Please config camera first");
+ var client = _clientDict[clientKey];
+ client.CTS.Cancel();
+ using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
+ {
+ await client.Camera.EnableHardwareTrans(false);
+ }
}
- _cameraEnable = isEnabled;
- // if (_cameraEnable) await _camera.WakeUp();
- // else await _camera.Sleep();
- await _camera.EnableHardwareTrans(_cameraEnable);
+ _clientDict.Clear();
+ await base.StopAsync(cancellationToken);
}
- ///
- /// 配置摄像头连接参数
- ///
- /// 摄像头IP地址
- /// 摄像头端口
- /// 配置是否成功
- public async Task ConfigureCameraAsync(string address, int port)
+ private Optional TryGetClient(string boardId)
{
- if (string.IsNullOrWhiteSpace(address))
+ if (_clientDict.TryGetValue(boardId, out var client))
{
- logger.Error("摄像头地址不能为空");
- return false;
- }
-
- if (port <= 0 || port > 65535)
- {
- logger.Error("摄像头端口必须在1-65535范围内");
- return false;
- }
-
- try
- {
- lock (_cameraLock)
- {
- // 关闭现有连接
- if (_camera != null)
- {
- logger.Info("关闭现有摄像头连接");
- // Camera doesn't have Dispose method, set to null
- _camera = null;
- }
-
- // 更新配置
- _cameraAddress = address;
- _cameraPort = port;
-
- // 创建新的摄像头客户端
- _camera = new Camera(_cameraAddress, _cameraPort);
-
- logger.Info("摄像头配置已更新: {Address}:{Port}", _cameraAddress, _cameraPort);
- }
-
- // Init Camera
- {
- var ret = await _camera.Init();
- if (!ret.IsSuccessful)
- {
- logger.Error(ret.Error);
- throw ret.Error;
- }
-
- if (!ret.Value)
- {
- logger.Error($"Camera Init Failed!");
- throw new Exception($"Camera Init Failed!");
- }
- }
- return true;
- }
- catch (Exception ex)
- {
- logger.Error(ex, "配置摄像头连接时发生错误");
- return false;
+ return client;
}
+ return null;
}
- ///
- /// 测试摄像头连接
- ///
- /// 连接测试结果
- public async Task<(bool IsSuccess, string Message)> TestCameraConnectionAsync()
+ private async Task GetOrCreateClientAsync(string boardId, int initWidth, int initHeight)
{
- try
+ if (_clientDict.TryGetValue(boardId, out var client))
{
- Camera? testCamera = null;
-
- lock (_cameraLock)
- {
- if (_camera == null)
- {
- return (false, "摄像头未配置");
- }
- testCamera = _camera;
- }
-
- // 尝试读取一帧数据来测试连接
- var result = await testCamera.ReadFrame();
-
- if (result.IsSuccessful)
- {
- logger.Info("摄像头连接测试成功: {Address}:{Port}", _cameraAddress, _cameraPort);
- return (true, "连接成功");
- }
- else
- {
- logger.Warn("摄像头连接测试失败: {Error}", result.Error);
- return (false, result.Error.ToString());
- }
+ // 可在此处做分辨率/Camera等配置更新
+ return client;
}
- catch (Exception ex)
+
+ var db = new Database.AppDataConnection();
+ if (db == null)
{
- logger.Error(ex, "摄像头连接测试出错");
- return (false, ex.Message);
+ logger.Error("Failed to create HdmiIn instance");
+ return null;
}
+
+ var boardRet = db.GetBoardByID(Guid.Parse(boardId));
+ if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
+ {
+ logger.Error($"Failed to get board with ID {boardId}");
+ return null;
+ }
+
+ var board = boardRet.Value.Value;
+
+ var camera = new Peripherals.CameraClient.Camera(board.IpAddr, board.Port);
+ var ret = await camera.Init();
+ if (!ret.IsSuccessful || !ret.Value)
+ {
+ logger.Error("Camera Init Failed!");
+ return null;
+ }
+
+ client = new VideoStreamClient(boardId, initWidth, initHeight, camera);
+ _clientDict[boardId] = client;
+ return client;
}
- ///
- /// 获取摄像头连接状态
- ///
- /// 连接状态信息
- public CameraStatus GetCameraStatus()
- {
- return new CameraStatus
- {
- Address = _cameraAddress,
- Port = _cameraPort,
- IsConfigured = _camera != null,
- ConnectionString = $"{_cameraAddress}:{_cameraPort}"
- };
- }
///
/// 执行 HTTP 视频流服务
@@ -299,106 +184,95 @@ public class HttpVideoStreamService : BackgroundService
/// 任务
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
- try
- {
- logger.Info("启动 HTTP 视频流服务,端口: {Port}", _serverPort);
-
- // 初始化默认摄像头连接
- await ConfigureCameraAsync(_cameraAddress, _cameraPort);
-
- // 创建 HTTP 监听器
- _httpListener = new HttpListener();
- _httpListener.Prefixes.Add($"http://{Global.localhost}:{_serverPort}/");
- _httpListener.Start();
-
- logger.Info("HTTP 视频流服务已启动,监听端口: {Port}", _serverPort);
-
- // 开始接受客户端连接
- _ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken);
-
- // 开始生成视频帧
- while (!stoppingToken.IsCancellationRequested)
- {
- if (_cameraEnable)
- {
- await GenerateVideoFrames(stoppingToken);
- }
- else
- {
- await Task.Delay(500, stoppingToken);
- }
- }
- }
- catch (HttpListenerException ex)
- {
- logger.Error(ex, "HTTP 视频流服务启动失败,请确保您有管理员权限或使用netsh配置URL前缀权限");
- }
- catch (Exception ex)
- {
- logger.Error(ex, "HTTP 视频流服务启动失败");
- }
- }
-
- private async Task AcceptClientsAsync(CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested && _httpListener != null && _httpListener.IsListening)
+ while (!stoppingToken.IsCancellationRequested)
{
+ if (_httpListener == null) continue;
try
{
- // 等待客户端连接
- var context = await _httpListener.GetContextAsync();
- var request = context.Request;
- var response = context.Response;
-
- logger.Info("新HTTP客户端连接: {RemoteEndPoint}", request.RemoteEndPoint);
- // 处理不同的请求路径
- var requestPath = request.Url?.AbsolutePath ?? "/";
-
- if (requestPath == "/video-stream")
+ logger.Debug("Waiting for HTTP request...");
+ var contextTask = _httpListener.GetContextAsync();
+ var completedTask = await Task.WhenAny(contextTask, Task.Delay(-1, stoppingToken));
+ if (completedTask == contextTask)
{
- // MJPEG 流请求(FPGA)
- _ = Task.Run(() => HandleMjpegStreamAsync(response, cancellationToken), cancellationToken);
- }
-#if USB_CAMERA
- else if (requestPath == "/usb-camera")
- {
- // USB Camera MJPEG流请求
- _ = Task.Run(() => HandleUsbCameraStreamAsync(response, cancellationToken), cancellationToken);
- }
-#endif
- else if (requestPath == "/snapshot")
- {
- // 单帧图像请求
- await HandleSnapshotRequestAsync(response, cancellationToken);
- }
- else if (requestPath == "/video-feed.html")
- {
- // HTML页面请求
- await SendVideoHtmlPageAsync(response);
+ var context = contextTask.Result;
+ logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}");
+ if (context != null)
+ _ = HandleRequestAsync(context, stoppingToken);
}
else
{
- // 默认返回简单的HTML页面,提供链接到视频页面
- await SendIndexHtmlPageAsync(response);
+ break;
}
}
- catch (HttpListenerException)
- {
- // HTTP监听器可能已停止
- break;
- }
- catch (ObjectDisposedException)
- {
- // 对象可能已被释放
- break;
- }
catch (Exception ex)
{
- logger.Error(ex, "接受HTTP客户端连接时发生错误");
+ logger.Error(ex, "Error in GetContextAsync");
+ break;
}
}
}
+ private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken)
+ {
+ var path = context.Request.Url?.AbsolutePath ?? "/";
+ var boardId = context.Request.QueryString["board"];
+ var width = int.TryParse(context.Request.QueryString["width"], out var w) ? w : 640;
+ var height = int.TryParse(context.Request.QueryString["height"], out var h) ? h : 480;
+
+ if (string.IsNullOrEmpty(boardId))
+ {
+ await SendErrorAsync(context.Response, "Missing clientId");
+ return;
+ }
+
+ var client = await GetOrCreateClientAsync(boardId, width, height);
+ if (client == null)
+ {
+ await SendErrorAsync(context.Response, "Invalid clientId or camera not available");
+ return;
+ }
+
+ var clientToken = client.CTS.Token;
+ try
+ {
+ logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint);
+
+ if (path == "/video-stream")
+ {
+ // MJPEG 流请求(FPGA)
+ await HandleMjpegStreamAsync(context.Response, client, cancellationToken);
+ }
+#if USB_CAMERA
+ else if (requestPath == "/usb-camera")
+ {
+ // USB Camera MJPEG流请求
+ await HandleUsbCameraStreamAsync(response, cancellationToken);
+ }
+#endif
+ else if (path == "/snapshot")
+ {
+ // 单帧图像请求
+ await HandleSnapshotRequestAsync(context.Response, client, cancellationToken);
+ }
+ else
+ {
+ // 默认返回简单的HTML页面,提供链接到视频页面
+ await SendIndexHtmlPageAsync(context.Response);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "接受HTTP客户端连接时发生错误");
+ }
+ }
+
+ private async Task SendErrorAsync(HttpListenerResponse response, string message)
+ {
+ response.StatusCode = 400;
+ await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
+ response.Close();
+ }
+
// USB Camera MJPEG流处理
#if USB_CAMERA
private async Task HandleUsbCameraStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken)
@@ -480,107 +354,54 @@ public class HttpVideoStreamService : BackgroundService
}
#endif
- private async Task HandleMjpegStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken)
+ private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
- try
+ // 读取 Camera 快照,返回 JPEG
+ var frameResult = await client.Camera.ReadFrame();
+ if (!frameResult.IsSuccessful || frameResult.Value == null)
{
- // 设置MJPEG流的响应头
- response.ContentType = "multipart/x-mixed-replace; boundary=--boundary";
- response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
- response.Headers.Add("Pragma", "no-cache");
- response.Headers.Add("Expires", "0");
-
- // 跟踪活跃的客户端
- lock (_clientsLock)
- {
- _activeClients.Add(response);
- }
-
- logger.Debug("已启动MJPEG流,客户端: {RemoteEndPoint}", response.OutputStream?.GetHashCode() ?? 0);
-
- // 保持连接直到取消或出错
- try
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- await Task.Delay(100, cancellationToken); // 简单的保活循环
- }
- }
- catch (TaskCanceledException)
- {
- // 预期的取消
- }
-
- logger.Debug("MJPEG流已结束,客户端: {ClientId}", response.OutputStream?.GetHashCode() ?? 0);
+ response.StatusCode = 500;
+ await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("Failed to get snapshot"));
+ response.Close();
+ return;
}
- catch (Exception ex)
+ var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80);
+ if (!jpegResult.IsSuccessful)
{
- logger.Error(ex, "处理MJPEG流时出错");
- }
- finally
- {
- lock (_clientsLock)
- {
- _activeClients.Remove(response);
- }
-
- try
- {
- response.Close();
- }
- catch
- {
- // 忽略关闭时的错误
- }
+ response.StatusCode = 500;
+ await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("JPEG conversion failed"));
+ response.Close();
+ return;
}
+ response.ContentType = "image/jpeg";
+ response.ContentLength64 = jpegResult.Value.Length;
+ await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken);
+ response.Close();
}
- private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, CancellationToken cancellationToken)
+ private async Task HandleMjpegStreamAsync(HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
- try
+ response.ContentType = "multipart/x-mixed-replace; boundary=--boundary";
+ response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.Headers.Add("Pragma", "no-cache");
+ response.Headers.Add("Expires", "0");
+
+ while (!cancellationToken.IsCancellationRequested)
{
- // 获取当前帧
- var imageData = await GetFPGAImageData();
+ var frameResult = await client.Camera.ReadFrame();
+ if (!frameResult.IsSuccessful || frameResult.Value == null) continue;
+ var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80);
+ if (!jpegResult.IsSuccessful) continue;
- // 获取当前分辨率
- int currentWidth, currentHeight;
- lock (_resolutionLock)
- {
- currentWidth = _frameWidth;
- currentHeight = _frameHeight;
- }
-
- // 直接使用Common.Image.ConvertRGB24ToJpeg进行转换
- var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, currentWidth, currentHeight, 80);
- if (!jpegResult.IsSuccessful)
- {
- logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
- response.StatusCode = 500;
- response.Close();
- return;
- }
-
- var jpegData = jpegResult.Value;
-
- // 设置响应头
- response.ContentType = "image/jpeg";
- response.ContentLength64 = jpegData.Length;
- response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
-
- // 发送JPEG数据
- await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
+ var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegResult.Value.Length + "\r\n\r\n");
+ await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
+ await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken);
+ await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken);
await response.OutputStream.FlushAsync(cancellationToken);
- logger.Debug("已发送快照图像,大小:{Size} 字节", jpegData.Length);
- }
- catch (Exception ex)
- {
- logger.Error(ex, "处理快照请求时出错");
- }
- finally
- {
- response.Close();
+ await Task.Delay(1000 / client.FrameWidth, cancellationToken);
}
+ response.Close();
}
private async Task SendVideoHtmlPageAsync(HttpListenerResponse response)
@@ -667,147 +488,52 @@ public class HttpVideoStreamService : BackgroundService
response.Close();
}
- private async Task GenerateVideoFrames(CancellationToken cancellationToken)
- {
- var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _frameRate);
- var lastFrameTime = DateTime.UtcNow;
-
- while (!cancellationToken.IsCancellationRequested && _cameraEnable)
- {
- try
- {
- var frameStartTime = DateTime.UtcNow;
-
- // 从 FPGA 获取图像数据
- var imageData = await GetFPGAImageData();
-
- var imageAcquireTime = DateTime.UtcNow;
-
- // 如果有图像数据,立即开始广播(不等待)
- if (imageData != null && imageData.Length > 0)
- {
- // 异步广播帧,不阻塞下一帧的获取
- _ = Task.Run(async () =>
- {
- try
- {
- await BroadcastFrameAsync(imageData, cancellationToken);
- }
- catch (Exception ex)
- {
- logger.Error(ex, "异步广播帧时发生错误");
- }
- }, cancellationToken);
-
- _frameCounter++;
-
- var frameEndTime = DateTime.UtcNow;
- var frameProcessingTime = (frameEndTime - frameStartTime).TotalMilliseconds;
- var imageAcquireElapsed = (imageAcquireTime - frameStartTime).TotalMilliseconds;
-
- if (_frameCounter % 30 == 0) // 每秒记录一次性能信息
- {
- logger.Debug("帧 {FrameNumber} 性能统计 - 图像获取: {AcquireTime:F1}ms, 总处理: {ProcessTime:F1}ms",
- _frameCounter, imageAcquireElapsed, frameProcessingTime);
- }
- }
-
- // 动态调整延迟 - 基于实际处理时间
- var elapsed = (DateTime.UtcNow - lastFrameTime).TotalMilliseconds;
- var targetInterval = frameInterval.TotalMilliseconds;
- var remainingDelay = Math.Max(0, targetInterval - elapsed);
-
- if (remainingDelay > 0)
- {
- await Task.Delay(TimeSpan.FromMilliseconds(remainingDelay), cancellationToken);
- }
-
- lastFrameTime = DateTime.UtcNow;
- }
- catch (OperationCanceledException)
- {
- break;
- }
- catch (Exception ex)
- {
- logger.Error(ex, "生成视频帧时发生错误");
- await Task.Delay(100, cancellationToken); // 减少错误恢复延迟
- }
- }
- }
-
///
/// 从 FPGA 获取图像数据
/// 实际从摄像头读取 RGB565 格式数据并转换为 RGB24
///
- private async Task GetFPGAImageData()
+ private async Task GetFPGAImageData(
+ VideoStreamClient client, CancellationToken cancellationToken = default)
{
- var startTime = DateTime.UtcNow;
- Camera? currentCamera = null;
-
- lock (_cameraLock)
- {
- currentCamera = _camera;
- }
-
- if (currentCamera == null)
- {
- logger.Error("摄像头客户端未初始化");
- return new byte[0];
- }
-
try
{
- // 获取当前分辨率
- int currentWidth, currentHeight;
- lock (_resolutionLock)
+ using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
- currentWidth = _frameWidth;
- currentHeight = _frameHeight;
+ // 从摄像头读取帧数据
+ var readStartTime = DateTime.UtcNow;
+ var result = await client.Camera.ReadFrame();
+ var readEndTime = DateTime.UtcNow;
+ var readTime = (readEndTime - readStartTime).TotalMilliseconds;
+
+ if (!result.IsSuccessful)
+ {
+ logger.Error("读取摄像头帧数据失败: {Error}", result.Error);
+ return new byte[0];
+ }
+
+ var rgb565Data = result.Value;
+
+ // 验证数据长度是否正确
+ if (!Common.Image.ValidateImageDataLength(rgb565Data, client.FrameWidth, client.FrameHeight, 2))
+ {
+ logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
+ client.FrameWidth * client.FrameHeight * 2, rgb565Data.Length);
+ }
+
+ // 将 RGB565 转换为 RGB24
+ var convertStartTime = DateTime.UtcNow;
+ var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, client.FrameWidth, client.FrameHeight, isLittleEndian: false);
+ var convertEndTime = DateTime.UtcNow;
+ var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
+
+ if (!rgb24Result.IsSuccessful)
+ {
+ logger.Error("RGB565转RGB24失败: {Error}", rgb24Result.Error);
+ return new byte[0];
+ }
+
+ return rgb24Result.Value;
}
-
- // 从摄像头读取帧数据
- var readStartTime = DateTime.UtcNow;
- var result = await currentCamera.ReadFrame();
- var readEndTime = DateTime.UtcNow;
- var readTime = (readEndTime - readStartTime).TotalMilliseconds;
-
- if (!result.IsSuccessful)
- {
- logger.Error("读取摄像头帧数据失败: {Error}", result.Error);
- return new byte[0];
- }
-
- var rgb565Data = result.Value;
-
- // 验证数据长度是否正确
- if (!Common.Image.ValidateImageDataLength(rgb565Data, currentWidth, currentHeight, 2))
- {
- logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
- currentWidth * currentHeight * 2, rgb565Data.Length);
- }
-
- // 将 RGB565 转换为 RGB24
- var convertStartTime = DateTime.UtcNow;
- var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, currentWidth, currentHeight, isLittleEndian: false);
- var convertEndTime = DateTime.UtcNow;
- var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
-
- if (!rgb24Result.IsSuccessful)
- {
- logger.Error("RGB565转RGB24失败: {Error}", rgb24Result.Error);
- return new byte[0];
- }
-
- var totalTime = (DateTime.UtcNow - startTime).TotalMilliseconds;
-
- if (_frameCounter % 30 == 0) // 每秒更新一次日志
- {
- logger.Debug("帧 {FrameNumber} 数据获取性能 - 读取: {ReadTime:F1}ms, 转换: {ConvertTime:F1}ms, 总计: {TotalTime:F1}ms, RGB565: {RGB565Size} 字节, RGB24: {RGB24Size} 字节",
- _frameCounter, readTime, convertTime, totalTime, rgb565Data.Length, rgb24Result.Value.Length);
- }
-
- return rgb24Result.Value;
}
catch (Exception ex)
{
@@ -816,282 +542,61 @@ public class HttpVideoStreamService : BackgroundService
}
}
- ///
- /// 向所有连接的客户端广播帧数据
- ///
- private async Task BroadcastFrameAsync(byte[] frameData, CancellationToken cancellationToken)
- {
- if (frameData == null || frameData.Length == 0)
- {
- logger.Warn("尝试广播空帧数据");
- return;
- }
-
- // 获取当前分辨率
- int currentWidth, currentHeight;
- lock (_resolutionLock)
- {
- currentWidth = _frameWidth;
- currentHeight = _frameHeight;
- }
-
- // 直接使用Common.Image.ConvertRGB24ToJpeg进行转换
- var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, currentWidth, currentHeight, 80);
- if (!jpegResult.IsSuccessful)
- {
- logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
- return;
- }
-
- var jpegData = jpegResult.Value;
-
- // 使用Common中的方法准备MJPEG帧数据
- var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
- var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
-
- var clientsToRemove = new List();
- var clientsToProcess = new List();
-
- // 获取当前连接的客户端列表
- lock (_clientsLock)
- {
- clientsToProcess.AddRange(_activeClients);
- }
-
- if (clientsToProcess.Count == 0)
- {
- return; // 没有活跃客户端
- }
-
- // 向每个活跃的客户端并行发送帧
- var sendTasks = clientsToProcess.Select(async client =>
- {
- try
- {
- // 发送帧头部
- await client.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken);
-
- // 发送JPEG数据
- await client.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
-
- // 发送结尾换行符
- await client.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken);
-
- // 确保数据立即发送
- await client.OutputStream.FlushAsync(cancellationToken);
-
- return (client, success: true, error: (Exception?)null);
- }
- catch (Exception ex)
- {
- return (client, success: false, error: ex);
- }
- });
-
- // 等待所有发送任务完成
- var results = await Task.WhenAll(sendTasks);
-
- // 处理发送结果
- foreach (var (client, success, error) in results)
- {
- if (!success)
- {
- logger.Debug("发送帧数据时出错: {Error}", error?.Message ?? "未知错误");
- clientsToRemove.Add(client);
- }
- }
-
- if (_frameCounter % 30 == 0 && clientsToProcess.Count > 0) // 每秒记录一次日志
- {
- logger.Debug("已向 {ClientCount} 个客户端发送第 {FrameNumber} 帧,大小:{Size} 字节",
- clientsToProcess.Count, _frameCounter, jpegData.Length);
- }
-
- // 移除断开连接的客户端
- if (clientsToRemove.Count > 0)
- {
- lock (_clientsLock)
- {
- foreach (var client in clientsToRemove)
- {
- _activeClients.Remove(client);
- try { client.Close(); }
- catch { /* 忽略关闭错误 */ }
- }
- }
-
- logger.Info("已移除 {Count} 个断开连接的客户端,当前连接数: {ActiveCount}",
- clientsToRemove.Count, _activeClients.Count);
- }
- }
-
- ///
- /// 获取连接的客户端端点列表
- ///
- public List GetConnectedClientEndpoints()
- {
- List endpoints = new List();
-
- lock (_clientsLock)
- {
- foreach (var client in _activeClients)
- {
- endpoints.Add($"Client-{client.OutputStream?.GetHashCode() ?? 0}");
- }
- }
-
- return endpoints;
- }
-
- ///
- /// 获取服务状态信息
- ///
- public ServiceStatus GetServiceStatus()
- {
- var cameraStatus = GetCameraStatus();
-
- return new ServiceStatus
- {
- IsRunning = (_httpListener?.IsListening ?? false) && _cameraEnable,
- ServerPort = _serverPort,
- FrameRate = _frameRate,
- Resolution = $"{_frameWidth}x{_frameHeight}",
- ConnectedClients = ConnectedClientsCount,
- ClientEndpoints = GetConnectedClientEndpoints(),
- CameraStatus = cameraStatus
- };
- }
-
-
- ///
- /// 停止 HTTP 视频流服务
- ///
- public override async Task StopAsync(CancellationToken cancellationToken)
- {
- logger.Info("正在停止 HTTP 视频流服务...");
-
- _cameraEnable = false;
-
- if (_httpListener != null && _httpListener.IsListening)
- {
- _httpListener.Stop();
- _httpListener.Close();
- }
-
- // 关闭所有客户端连接
- lock (_clientsLock)
- {
- foreach (var client in _activeClients)
- {
- try { client.Close(); }
- catch { /* 忽略关闭错误 */ }
- }
- _activeClients.Clear();
- }
-
- // 关闭摄像头连接
- lock (_cameraLock)
- {
- _camera = null;
- }
-
- await base.StopAsync(cancellationToken);
-
- logger.Info("HTTP 视频流服务已停止");
- }
-
///
/// 设置视频流分辨率
///
+ /// 板卡ID
/// 宽度
/// 高度
+ /// 超时时间(毫秒)
+ /// 取消令牌
/// 设置结果
- public async Task<(bool IsSuccess, string Message)> SetResolutionAsync(int width, int height)
+ public async Task> SetResolutionAsync(
+ string boardId, int width, int height,
+ int timeout = 100, CancellationToken cancellationToken = default)
{
try
{
- logger.Info($"正在设置视频流分辨率为 {width}x{height}");
+ var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
- Camera? currentCamera = null;
- lock (_cameraLock)
+ using (await client.Lock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout), cancellationToken))
{
- currentCamera = _camera;
- }
+ var currentCamera = client.Camera;
+ if (currentCamera == null)
+ {
+ var message = $"获取摄像头失败";
+ logger.Error(message);
+ return new(new Exception(message));
+ }
- if (currentCamera == null)
- {
- var message = "摄像头未配置,无法设置分辨率";
- logger.Error(message);
- return (false, message);
- }
+ // 设置摄像头分辨率
+ var ret = await currentCamera.ChangeResolution(width, height);
+ if (!ret.IsSuccessful)
+ {
+ var message = $"设置摄像头分辨率失败: {ret.Error}";
+ logger.Error(message);
+ return new(new Exception(message));
+ }
- // 设置摄像头分辨率
- var cameraResult = await currentCamera.ChangeResolution(width, height);
- if (!cameraResult.IsSuccessful)
- {
- var message = $"设置摄像头分辨率失败: {cameraResult.Error}";
- logger.Error(message);
- return (false, message);
- }
+ if (!ret.Value)
+ {
+ logger.Warn($"设置摄像头分辨率失败");
+ return false;
+ }
- // 更新HTTP服务的分辨率配置
- lock (_resolutionLock)
- {
- _frameWidth = width;
- _frameHeight = height;
- }
+ // 更新HTTP服务的分辨率配置
+ client.FrameWidth = width;
+ client.FrameHeight = height;
- var successMessage = $"视频流分辨率已成功设置为 {width}x{height}";
- logger.Info(successMessage);
- return (true, successMessage);
+ logger.Info($"视频流分辨率已成功设置为 {width}x{height}");
+ return true;
+ }
}
catch (Exception ex)
{
var message = $"设置分辨率时发生错误: {ex.Message}";
logger.Error(ex, message);
- return (false, message);
- }
- }
-
- ///
- /// 获取当前分辨率
- ///
- /// 当前分辨率(宽度, 高度)
- public (int Width, int Height) GetCurrentResolution()
- {
- lock (_resolutionLock)
- {
- return (_frameWidth, _frameHeight);
- }
- }
-
- ///
- /// 获取支持的分辨率列表
- ///
- /// 支持的分辨率列表
- public List<(int Width, int Height, string Name)> GetSupportedResolutions()
- {
- return new List<(int, int, string)>
- {
- (640, 480, "640x480 (VGA)"),
- (960, 540, "960x540 (qHD)"),
- (1280, 720, "1280x720 (HD)"),
- (1280, 960, "1280x960 (SXGA)"),
- (1920, 1080, "1920x1080 (Full HD)")
- };
- }
-
- #region 自动对焦功能
-
- ///
- /// 检查摄像头是否已配置
- ///
- /// 是否已配置
- public bool IsCameraConfigured()
- {
- lock (_cameraLock)
- {
- return _camera != null && !string.IsNullOrEmpty(_cameraAddress);
+ return new(new Exception(message));
}
}
@@ -1099,37 +604,33 @@ public class HttpVideoStreamService : BackgroundService
/// 初始化摄像头自动对焦功能
///
/// 初始化结果
- public async Task InitAutoFocusAsync()
+ public async Task InitAutoFocusAsync(
+ string boardId, int timeout = 1000, CancellationToken cancellationToken = default)
{
try
{
- lock (_cameraLock)
+ var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
+
+ using (await client.Lock.AcquireWriteLockAsync(
+ TimeSpan.FromMilliseconds(timeout), cancellationToken))
{
- if (_camera == null)
+ var result = await client.Camera.InitAutoFocus();
+
+ if (result.IsSuccessful && result.Value)
{
- logger.Error("摄像头未配置,无法初始化自动对焦");
+ logger.Info($"Board{boardId}摄像头自动对焦功能初始化成功");
+ return true;
+ }
+ else
+ {
+ logger.Error($"Board{boardId}摄像头自动对焦功能初始化失败: {result.Error?.Message ?? "未知错误"}");
return false;
}
}
-
- logger.Info("开始初始化摄像头自动对焦功能");
-
- var result = await _camera!.InitAutoFocus();
-
- if (result.IsSuccessful && result.Value)
- {
- logger.Info("摄像头自动对焦功能初始化成功");
- return true;
- }
- else
- {
- logger.Error($"摄像头自动对焦功能初始化失败: {result.Error?.Message ?? "未知错误"}");
- return false;
- }
}
catch (Exception ex)
{
- logger.Error(ex, "初始化摄像头自动对焦功能时发生异常");
+ logger.Error(ex, $"Board{boardId}初始化摄像头自动对焦功能时发生异常");
return false;
}
}
@@ -1138,40 +639,108 @@ public class HttpVideoStreamService : BackgroundService
/// 执行摄像头自动对焦
///
/// 对焦结果
- public async Task PerformAutoFocusAsync()
+ public async Task PerformAutoFocusAsync(
+ string boardId, int timeout = 1000, CancellationToken cancellationToken = default)
{
try
{
- lock (_cameraLock)
- {
- if (_camera == null)
- {
- logger.Error("摄像头未配置,无法执行自动对焦");
- return false;
- }
- }
+ var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
- logger.Info("开始执行摄像头自动对焦");
+ logger.Info($"Board{boardId}开始执行摄像头自动对焦");
- var result = await _camera!.PerformAutoFocus();
+ var result = await client.Camera.PerformAutoFocus();
if (result.IsSuccessful && result.Value)
{
- logger.Info("摄像头自动对焦执行成功");
+ logger.Info($"Board{boardId}摄像头自动对焦成功");
return true;
}
else
{
- logger.Error($"摄像头自动对焦执行失败: {result.Error?.Message ?? "未知错误"}");
+ logger.Error($"Board{boardId}摄像头自动对焦执行失败: {result.Error?.Message ?? "未知错误"}");
return false;
}
}
catch (Exception ex)
{
- logger.Error(ex, "执行摄像头自动对焦时发生异常");
+ logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常");
return false;
}
}
- #endregion
+ public VideoEndpoint GetVideoEndpoint(string boardId)
+ {
+ var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
+
+ return new VideoEndpoint
+ {
+ BoardId = boardId,
+ MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={boardId}",
+ VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={boardId}",
+ SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={boardId}",
+ Resolution = $"{client.FrameWidth}x{client.FrameHeight}",
+ FrameRate = client.FrameRate
+ };
+ }
+
+ public List GetAllVideoEndpoints()
+ {
+ var endpoints = new List();
+
+ foreach (var boardId in _clientDict.Keys)
+ endpoints.Add(GetVideoEndpoint(boardId));
+
+ return endpoints;
+ }
+
+ public ServiceStatus GetServiceStatus()
+ {
+ return new ServiceStatus
+ {
+ IsRunning = true,
+ ServerPort = _serverPort,
+ ClientEndpoints = GetAllVideoEndpoints()
+ };
+ }
+
+ public async Task DisableHdmiTransmissionAsync(string boardId)
+ {
+ try
+ {
+ var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
+
+ using (await client.Lock.AcquireWriteLockAsync())
+ {
+ var camera = client.Camera;
+ var disableResult = await camera.EnableHardwareTrans(false);
+ if (disableResult.IsSuccessful && disableResult.Value)
+ logger.Info($"Successfully disabled camera {boardId} hardware transmission");
+ else
+ logger.Error($"Failed to disable camera {boardId} hardware transmission: {disableResult.Error}");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Exception occurred while disabling HDMI transmission for camera {boardId}");
+ }
+ }
+
+ public async ValueTask TestCameraConnection(string boardId)
+ {
+ try
+ {
+ var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
+
+ var imageData = await GetFPGAImageData(client);
+ if (imageData == null || imageData.Length == 0)
+ return false;
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常");
+ return false;
+ }
+ }
}
diff --git a/server/src/Services/ProgressTrackerService.cs b/server/src/Services/ProgressTrackerService.cs
index ebf9982..4fecfca 100644
--- a/server/src/Services/ProgressTrackerService.cs
+++ b/server/src/Services/ProgressTrackerService.cs
@@ -41,7 +41,7 @@ public class ProgressReporter : ProgressInfo, IProgress
private ProgressStatus _status = ProgressStatus.Pending;
private string _errorMessage;
- public string TaskId { get; set; } = new Guid().ToString();
+ public string TaskId { get; set; } = Guid.NewGuid().ToString();
public int ProgressPercent => _progress * 100 / MaxProgress;
public ProgressStatus Status => _status;
public string ErrorMessage => _errorMessage;
diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs
index 3cc45b1..582d7d9 100644
--- a/server/src/UdpClientPool.cs
+++ b/server/src/UdpClientPool.cs
@@ -223,22 +223,28 @@ public class UDPClientPool
/// IP端点(IP地址与端口)
/// 任务ID
/// 设备地址
+ /// 数据长度(0~255)
/// 超时时间(毫秒)
/// 读取结果,包含接收到的数据包
public static async ValueTask> ReadAddr(
- IPEndPoint endPoint, int taskID, uint devAddr, int timeout = 1000)
+ IPEndPoint endPoint, int taskID, uint devAddr, int dataLength, int timeout = 1000)
{
+ if (dataLength <= 0)
+ return new(new ArgumentException("Data length must be greater than 0"));
+
+ if (dataLength > 255)
+ return new(new ArgumentException("Data length must be less than or equal to 255"));
+
var ret = false;
var opts = new SendAddrPackOptions()
{
BurstType = BurstType.FixedBurst,
- BurstLength = 0,
+ BurstLength = ((byte)(dataLength - 1)),
CommandID = Convert.ToByte(taskID),
Address = devAddr,
IsWrite = false,
};
-
// Read Register
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send Address Package Failed!"));
@@ -260,6 +266,20 @@ public class UDPClientPool
return retPack;
}
+ ///
+ /// 读取设备地址数据
+ ///
+ /// IP端点(IP地址与端口)
+ /// 任务ID
+ /// 设备地址
+ /// 超时时间(毫秒)
+ /// 读取结果,包含接收到的数据包
+ public static async ValueTask> ReadAddrByte(
+ IPEndPoint endPoint, int taskID, uint devAddr, int timeout = 1000)
+ {
+ return await ReadAddr(endPoint, taskID, devAddr, 0, timeout);
+ }
+
///
/// 读取设备地址数据并校验结果
///
@@ -271,11 +291,11 @@ public class UDPClientPool
/// 超时时间(毫秒)
/// 校验结果,true表示数据匹配期望值
public static async ValueTask> ReadAddr(
- IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000)
+ IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000)
{
var address = endPoint.Address.ToString();
- var ret = await ReadAddr(endPoint, taskID, devAddr, timeout);
+ var ret = await ReadAddrByte(endPoint, taskID, devAddr, timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value.IsSuccessful)
return new(new Exception($"Read device {address} address {devAddr} failed"));
@@ -324,7 +344,7 @@ public class UDPClientPool
await Task.Delay(waittime);
try
{
- var ret = await ReadAddr(endPoint, taskID, devAddr, Convert.ToInt32(timeleft.TotalMilliseconds));
+ var ret = await ReadAddrByte(endPoint, taskID, devAddr, Convert.ToInt32(timeleft.TotalMilliseconds));
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value.IsSuccessful)
return new(new Exception($"Read device {address} address {devAddr} failed"));
@@ -555,7 +575,7 @@ public class UDPClientPool
var resultData = new List();
for (int i = 0; i < length; i++)
{
- var ret = await ReadAddr(endPoint, taskID, addr[i], timeout);
+ var ret = await ReadAddrByte(endPoint, taskID, addr[i], timeout);
if (!ret.IsSuccessful)
{
logger.Error($"ReadAddrSeq failed at index {i}: {ret.Error}");
diff --git a/src/APIClient.ts b/src/APIClient.ts
index 047d49c..e553bac 100644
--- a/src/APIClient.ts
+++ b/src/APIClient.ts
@@ -185,12 +185,8 @@ export class VideoStreamClient {
return Promise.resolve(null as any);
}
- /**
- * 获取 HTTP 视频流信息
- * @return 流信息
- */
- getStreamInfo( cancelToken?: CancelToken): Promise {
- let url_ = this.baseUrl + "/api/VideoStream/StreamInfo";
+ myEndpoint( cancelToken?: CancelToken): Promise {
+ let url_ = this.baseUrl + "/api/VideoStream/MyEndpoint";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
@@ -209,208 +205,11 @@ export class VideoStreamClient {
throw _error;
}
}).then((_response: AxiosResponse) => {
- return this.processGetStreamInfo(_response);
+ return this.processMyEndpoint(_response);
});
}
- protected processGetStreamInfo(response: AxiosResponse): Promise {
- const status = response.status;
- let _headers: any = {};
- if (response.headers && typeof response.headers === "object") {
- for (const k in response.headers) {
- if (response.headers.hasOwnProperty(k)) {
- _headers[k] = response.headers[k];
- }
- }
- }
- if (status === 200) {
- const _responseText = response.data;
- let result200: any = null;
- let resultData200 = _responseText;
- result200 = StreamInfoResult.fromJS(resultData200);
- return Promise.resolve(result200);
-
- } else if (status === 500) {
- const _responseText = response.data;
- let result500: any = null;
- let resultData500 = _responseText;
- result500 = Exception.fromJS(resultData500);
- return throwException("A server side error occurred.", status, _responseText, _headers, result500);
-
- } else if (status !== 200 && status !== 204) {
- const _responseText = response.data;
- return throwException("An unexpected server error occurred.", status, _responseText, _headers);
- }
- return Promise.resolve(null as any);
- }
-
- /**
- * 配置摄像头连接参数
- * @param config 摄像头配置
- * @return 配置结果
- */
- configureCamera(config: CameraConfigRequest, cancelToken?: CancelToken): Promise {
- let url_ = this.baseUrl + "/api/VideoStream/ConfigureCamera";
- url_ = url_.replace(/[?&]$/, "");
-
- const content_ = JSON.stringify(config);
-
- let options_: AxiosRequestConfig = {
- data: content_,
- method: "POST",
- url: url_,
- headers: {
- "Content-Type": "application/json",
- "Accept": "application/json"
- },
- cancelToken
- };
-
- return this.instance.request(options_).catch((_error: any) => {
- if (isAxiosError(_error) && _error.response) {
- return _error.response;
- } else {
- throw _error;
- }
- }).then((_response: AxiosResponse) => {
- return this.processConfigureCamera(_response);
- });
- }
-
- protected processConfigureCamera(response: AxiosResponse): Promise {
- const status = response.status;
- let _headers: any = {};
- if (response.headers && typeof response.headers === "object") {
- for (const k in response.headers) {
- if (response.headers.hasOwnProperty(k)) {
- _headers[k] = response.headers[k];
- }
- }
- }
- if (status === 200) {
- const _responseText = response.data;
- let result200: any = null;
- let resultData200 = _responseText;
- result200 = resultData200 !== undefined ? resultData200 : null;
-
- return Promise.resolve(result200);
-
- } else if (status === 400) {
- const _responseText = response.data;
- let result400: any = null;
- let resultData400 = _responseText;
- result400 = resultData400 !== undefined ? resultData400 : null;
-
- return throwException("A server side error occurred.", status, _responseText, _headers, result400);
-
- } else if (status === 500) {
- const _responseText = response.data;
- let result500: any = null;
- let resultData500 = _responseText;
- result500 = Exception.fromJS(resultData500);
- return throwException("A server side error occurred.", status, _responseText, _headers, result500);
-
- } else if (status !== 200 && status !== 204) {
- const _responseText = response.data;
- return throwException("An unexpected server error occurred.", status, _responseText, _headers);
- }
- return Promise.resolve(null as any);
- }
-
- /**
- * 获取当前摄像头配置
- * @return 摄像头配置信息
- */
- getCameraConfig( cancelToken?: CancelToken): Promise {
- let url_ = this.baseUrl + "/api/VideoStream/CameraConfig";
- url_ = url_.replace(/[?&]$/, "");
-
- let options_: AxiosRequestConfig = {
- method: "GET",
- url: url_,
- headers: {
- "Accept": "application/json"
- },
- cancelToken
- };
-
- return this.instance.request(options_).catch((_error: any) => {
- if (isAxiosError(_error) && _error.response) {
- return _error.response;
- } else {
- throw _error;
- }
- }).then((_response: AxiosResponse) => {
- return this.processGetCameraConfig(_response);
- });
- }
-
- protected processGetCameraConfig(response: AxiosResponse): Promise {
- const status = response.status;
- let _headers: any = {};
- if (response.headers && typeof response.headers === "object") {
- for (const k in response.headers) {
- if (response.headers.hasOwnProperty(k)) {
- _headers[k] = response.headers[k];
- }
- }
- }
- if (status === 200) {
- const _responseText = response.data;
- let result200: any = null;
- let resultData200 = _responseText;
- result200 = resultData200 !== undefined ? resultData200 : null;
-
- return Promise.resolve(result200);
-
- } else if (status === 500) {
- const _responseText = response.data;
- let result500: any = null;
- let resultData500 = _responseText;
- result500 = Exception.fromJS(resultData500);
- return throwException("A server side error occurred.", status, _responseText, _headers, result500);
-
- } else if (status !== 200 && status !== 204) {
- const _responseText = response.data;
- return throwException("An unexpected server error occurred.", status, _responseText, _headers);
- }
- return Promise.resolve(null as any);
- }
-
- /**
- * 控制 HTTP 视频流服务开关
- * @param enabled (optional) 是否启用服务
- * @return 操作结果
- */
- setEnabled(enabled: boolean | undefined, cancelToken?: CancelToken): Promise {
- let url_ = this.baseUrl + "/api/VideoStream/SetEnabled?";
- if (enabled === null)
- throw new Error("The parameter 'enabled' cannot be null.");
- else if (enabled !== undefined)
- url_ += "enabled=" + encodeURIComponent("" + enabled) + "&";
- url_ = url_.replace(/[?&]$/, "");
-
- let options_: AxiosRequestConfig = {
- method: "POST",
- url: url_,
- headers: {
- "Accept": "application/json"
- },
- cancelToken
- };
-
- return this.instance.request(options_).catch((_error: any) => {
- if (isAxiosError(_error) && _error.response) {
- return _error.response;
- } else {
- throw _error;
- }
- }).then((_response: AxiosResponse) => {
- return this.processSetEnabled(_response);
- });
- }
-
- protected processSetEnabled(response: AxiosResponse): Promise {
+ protected processMyEndpoint(response: AxiosResponse): Promise {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
@@ -502,6 +301,59 @@ export class VideoStreamClient {
return Promise.resolve(null as any);
}
+ disableHdmiTransmission( cancelToken?: CancelToken): Promise {
+ let url_ = this.baseUrl + "/api/VideoStream/DisableTransmission";
+ url_ = url_.replace(/[?&]$/, "");
+
+ let options_: AxiosRequestConfig = {
+ responseType: "blob",
+ method: "POST",
+ url: url_,
+ headers: {
+ "Accept": "application/octet-stream"
+ },
+ cancelToken
+ };
+
+ return this.instance.request(options_).catch((_error: any) => {
+ if (isAxiosError(_error) && _error.response) {
+ return _error.response;
+ } else {
+ throw _error;
+ }
+ }).then((_response: AxiosResponse) => {
+ return this.processDisableHdmiTransmission(_response);
+ });
+ }
+
+ protected processDisableHdmiTransmission(response: AxiosResponse): Promise {
+ const status = response.status;
+ let _headers: any = {};
+ if (response.headers && typeof response.headers === "object") {
+ for (const k in response.headers) {
+ if (response.headers.hasOwnProperty(k)) {
+ _headers[k] = response.headers[k];
+ }
+ }
+ }
+ if (status === 200 || status === 206) {
+ const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined;
+ let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
+ let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
+ if (fileName) {
+ fileName = decodeURIComponent(fileName);
+ } else {
+ fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
+ fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
+ }
+ return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers });
+ } else if (status !== 200 && status !== 204) {
+ const _responseText = response.data;
+ return throwException("An unexpected server error occurred.", status, _responseText, _headers);
+ }
+ return Promise.resolve(null as any);
+ }
+
/**
* 设置视频流分辨率
* @param request 分辨率配置请求
@@ -576,67 +428,6 @@ export class VideoStreamClient {
return Promise.resolve(null as any);
}
- /**
- * 获取当前分辨率
- * @return 当前分辨率信息
- */
- getCurrentResolution( cancelToken?: CancelToken): Promise {
- let url_ = this.baseUrl + "/api/VideoStream/Resolution";
- url_ = url_.replace(/[?&]$/, "");
-
- let options_: AxiosRequestConfig = {
- method: "GET",
- url: url_,
- headers: {
- "Accept": "application/json"
- },
- cancelToken
- };
-
- return this.instance.request(options_).catch((_error: any) => {
- if (isAxiosError(_error) && _error.response) {
- return _error.response;
- } else {
- throw _error;
- }
- }).then((_response: AxiosResponse) => {
- return this.processGetCurrentResolution(_response);
- });
- }
-
- protected processGetCurrentResolution(response: AxiosResponse): Promise {
- const status = response.status;
- let _headers: any = {};
- if (response.headers && typeof response.headers === "object") {
- for (const k in response.headers) {
- if (response.headers.hasOwnProperty(k)) {
- _headers[k] = response.headers[k];
- }
- }
- }
- if (status === 200) {
- const _responseText = response.data;
- let result200: any = null;
- let resultData200 = _responseText;
- result200 = resultData200 !== undefined ? resultData200 : null;
-
- return Promise.resolve(result200);
-
- } else if (status === 500) {
- const _responseText = response.data;
- let result500: any = null;
- let resultData500 = _responseText;
- result500 = resultData500 !== undefined ? resultData500 : null;
-
- return throwException("A server side error occurred.", status, _responseText, _headers, result500);
-
- } else if (status !== 200 && status !== 204) {
- const _responseText = response.data;
- return throwException("An unexpected server error occurred.", status, _responseText, _headers);
- }
- return Promise.resolve(null as any);
- }
-
/**
* 获取支持的分辨率列表
* @return 支持的分辨率列表
@@ -835,75 +626,6 @@ export class VideoStreamClient {
}
return Promise.resolve(null as any);
}
-
- /**
- * 执行一次自动对焦 (GET方式)
- * @return 对焦结果
- */
- focus( cancelToken?: CancelToken): Promise {
- let url_ = this.baseUrl + "/api/VideoStream/Focus";
- url_ = url_.replace(/[?&]$/, "");
-
- let options_: AxiosRequestConfig = {
- method: "GET",
- url: url_,
- headers: {
- "Accept": "application/json"
- },
- cancelToken
- };
-
- return this.instance.request(options_).catch((_error: any) => {
- if (isAxiosError(_error) && _error.response) {
- return _error.response;
- } else {
- throw _error;
- }
- }).then((_response: AxiosResponse) => {
- return this.processFocus(_response);
- });
- }
-
- protected processFocus(response: AxiosResponse): Promise {
- const status = response.status;
- let _headers: any = {};
- if (response.headers && typeof response.headers === "object") {
- for (const k in response.headers) {
- if (response.headers.hasOwnProperty(k)) {
- _headers[k] = response.headers[k];
- }
- }
- }
- if (status === 200) {
- const _responseText = response.data;
- let result200: any = null;
- let resultData200 = _responseText;
- result200 = resultData200 !== undefined ? resultData200 : null;
-
- return Promise.resolve(result200);
-
- } else if (status === 400) {
- const _responseText = response.data;
- let result400: any = null;
- let resultData400 = _responseText;
- result400 = resultData400 !== undefined ? resultData400 : null;
-
- return throwException("A server side error occurred.", status, _responseText, _headers, result400);
-
- } else if (status === 500) {
- const _responseText = response.data;
- let result500: any = null;
- let resultData500 = _responseText;
- result500 = resultData500 !== undefined ? resultData500 : null;
-
- return throwException("A server side error occurred.", status, _responseText, _headers, result500);
-
- } else if (status !== 200 && status !== 204) {
- const _responseText = response.data;
- return throwException("An unexpected server error occurred.", status, _responseText, _headers);
- }
- return Promise.resolve(null as any);
- }
}
export class BsdlParserClient {
@@ -7253,134 +6975,6 @@ export interface IException {
stackTrace?: string | undefined;
}
-/** 视频流信息结构体 */
-export class StreamInfoResult implements IStreamInfoResult {
- /** TODO: */
- frameRate!: number;
- /** TODO: */
- frameWidth!: number;
- /** TODO: */
- frameHeight!: number;
- /** TODO: */
- format!: string;
- /** TODO: */
- htmlUrl!: string;
- /** TODO: */
- mjpegUrl!: string;
- /** TODO: */
- snapshotUrl!: string;
- /** TODO: */
- usbCameraUrl!: string;
-
- constructor(data?: IStreamInfoResult) {
- if (data) {
- for (var property in data) {
- if (data.hasOwnProperty(property))
- (this)[property] = (data)[property];
- }
- }
- }
-
- init(_data?: any) {
- if (_data) {
- this.frameRate = _data["frameRate"];
- this.frameWidth = _data["frameWidth"];
- this.frameHeight = _data["frameHeight"];
- this.format = _data["format"];
- this.htmlUrl = _data["htmlUrl"];
- this.mjpegUrl = _data["mjpegUrl"];
- this.snapshotUrl = _data["snapshotUrl"];
- this.usbCameraUrl = _data["usbCameraUrl"];
- }
- }
-
- static fromJS(data: any): StreamInfoResult {
- data = typeof data === 'object' ? data : {};
- let result = new StreamInfoResult();
- result.init(data);
- return result;
- }
-
- toJSON(data?: any) {
- data = typeof data === 'object' ? data : {};
- data["frameRate"] = this.frameRate;
- data["frameWidth"] = this.frameWidth;
- data["frameHeight"] = this.frameHeight;
- data["format"] = this.format;
- data["htmlUrl"] = this.htmlUrl;
- data["mjpegUrl"] = this.mjpegUrl;
- data["snapshotUrl"] = this.snapshotUrl;
- data["usbCameraUrl"] = this.usbCameraUrl;
- return data;
- }
-}
-
-/** 视频流信息结构体 */
-export interface IStreamInfoResult {
- /** TODO: */
- frameRate: number;
- /** TODO: */
- frameWidth: number;
- /** TODO: */
- frameHeight: number;
- /** TODO: */
- format: string;
- /** TODO: */
- htmlUrl: string;
- /** TODO: */
- mjpegUrl: string;
- /** TODO: */
- snapshotUrl: string;
- /** TODO: */
- usbCameraUrl: string;
-}
-
-/** 摄像头配置请求模型 */
-export class CameraConfigRequest implements ICameraConfigRequest {
- /** 摄像头地址 */
- address!: string;
- /** 摄像头端口 */
- port!: number;
-
- constructor(data?: ICameraConfigRequest) {
- if (data) {
- for (var property in data) {
- if (data.hasOwnProperty(property))
- (this)[property] = (data)[property];
- }
- }
- }
-
- init(_data?: any) {
- if (_data) {
- this.address = _data["address"];
- this.port = _data["port"];
- }
- }
-
- static fromJS(data: any): CameraConfigRequest {
- data = typeof data === 'object' ? data : {};
- let result = new CameraConfigRequest();
- result.init(data);
- return result;
- }
-
- toJSON(data?: any) {
- data = typeof data === 'object' ? data : {};
- data["address"] = this.address;
- data["port"] = this.port;
- return data;
- }
-}
-
-/** 摄像头配置请求模型 */
-export interface ICameraConfigRequest {
- /** 摄像头地址 */
- address: string;
- /** 摄像头端口 */
- port: number;
-}
-
/** 分辨率配置请求模型 */
export class ResolutionConfigRequest implements IResolutionConfigRequest {
/** 宽度 */
diff --git a/src/main.ts b/src/main.ts
deleted file mode 100644
index c962b62..0000000
--- a/src/main.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import './assets/main.css'
-
-import { createApp } from 'vue'
-import { createPinia } from 'pinia'
-
-import App from '@/App.vue'
-import router from './router'
-
-const app = createApp(App).use(router).use(createPinia()).mount('#app')
-