296 lines
9.8 KiB
C#
296 lines
9.8 KiB
C#
using System.Net;
|
||
using DotNext;
|
||
using WebProtocol;
|
||
|
||
namespace Peripherals.HdmiInClient;
|
||
|
||
static class HdmiInAddr
|
||
{
|
||
public const UInt32 BASE = 0xA000_0000;
|
||
|
||
public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
|
||
|
||
public const UInt32 START_WR_ADDR0 = BASE + 0x20;
|
||
public const UInt32 END_WR_ADDR0 = BASE + 0x21;
|
||
|
||
public const UInt32 HDMI_NOT_READY = BASE + 0x26;
|
||
public const UInt32 HDMI_HEIGHT_WIDTH = BASE + 0x27;
|
||
public const UInt32 CAPTURE_HEIGHT_WIDTH = BASE + 0x28;
|
||
|
||
public const UInt32 ADDR_HDMI_WD_START = 0x0400_0000;
|
||
}
|
||
|
||
public class HdmiIn
|
||
{
|
||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||
|
||
readonly int timeout = 500;
|
||
readonly int taskID;
|
||
readonly int port;
|
||
readonly string address;
|
||
private IPEndPoint ep;
|
||
|
||
public int Width { get; private set; }
|
||
public int Height { get; private set; }
|
||
public int FrameLength => Width * Height / 2;
|
||
|
||
/// <summary>
|
||
/// 初始化HDMI输入客户端
|
||
/// </summary>
|
||
/// <param name="address">HDMI输入设备IP地址</param>
|
||
/// <param name="port">HDMI输入设备端口</param>
|
||
/// <param name="taskID">任务ID</param>
|
||
/// <param name="timeout">超时时间(毫秒)</param>
|
||
public HdmiIn(string address, int port, int taskID, int timeout = 500)
|
||
{
|
||
if (timeout < 0)
|
||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||
this.address = address;
|
||
this.port = port;
|
||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||
this.taskID = taskID;
|
||
this.timeout = timeout;
|
||
}
|
||
|
||
public async ValueTask<Result<bool>> Init(bool enable = true)
|
||
{
|
||
{
|
||
var ret = await CheckHdmiIsReady();
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to check HDMI ready: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
if (!ret.Value)
|
||
{
|
||
logger.Error("HDMI not ready");
|
||
return new(false);
|
||
}
|
||
}
|
||
|
||
int width = -1, height = -1;
|
||
{
|
||
var ret = await GetHdmiResolution();
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
(width, height) = ret.Value;
|
||
}
|
||
|
||
{
|
||
var ret = await ConnectJpeg2Hdmi(width, height);
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to connect JPEG to HDMI: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
if (!ret.Value)
|
||
{
|
||
logger.Error("Failed to connect JPEG to HDMI");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (enable) return await SetTransEnable(true);
|
||
else return true;
|
||
}
|
||
|
||
public async ValueTask<Result<bool>> SetTransEnable(bool isEnable)
|
||
{
|
||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.CAPTURE_RD_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
if (!ret.Value)
|
||
{
|
||
logger.Error($"HdmiIn_CTRL write returned false for HdmiIn at {this.address}:{this.port}");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取一帧图像数据
|
||
/// </summary>
|
||
/// <returns>包含图像数据的字节数组</returns>
|
||
public async ValueTask<Result<byte[]>> ReadFrame()
|
||
{
|
||
// 只在第一次或出错时清除UDP缓冲区,避免每帧都清除造成延迟
|
||
// MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||
|
||
logger.Trace($"Reading frame from HdmiIn {this.address}");
|
||
|
||
// 使用UDPClientPool读取图像帧数据
|
||
var result = await UDPClientPool.ReadAddr4BytesAsync(
|
||
this.ep,
|
||
this.taskID, // taskID
|
||
HdmiInAddr.ADDR_HDMI_WD_START,
|
||
FrameLength, // 使用当前分辨率的动态大小
|
||
BurstType.ExtendBurst,
|
||
this.timeout);
|
||
|
||
if (!result.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to read frame from HdmiIn {this.address}:{this.port}, error: {result.Error}");
|
||
// 读取失败时清除缓冲区,为下次读取做准备
|
||
try
|
||
{
|
||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Warn($"Failed to clear UDP data after read error: {ex.Message}");
|
||
}
|
||
return new(result.Error);
|
||
}
|
||
|
||
logger.Trace($"Successfully read frame from HdmiIn {this.address}:{this.port}, data length: {result.Value.Length} bytes");
|
||
return result.Value;
|
||
}
|
||
|
||
public async ValueTask<Optional<(byte[] header, byte[] data, byte[] footer)>> GetMJpegFrame()
|
||
{
|
||
// 从HDMI读取RGB24数据
|
||
var readStartTime = DateTime.UtcNow;
|
||
var frameResult = await ReadFrame();
|
||
var readEndTime = DateTime.UtcNow;
|
||
var readTime = (readEndTime - readStartTime).TotalMilliseconds;
|
||
|
||
if (!frameResult.IsSuccessful || frameResult.Value == null)
|
||
{
|
||
logger.Warn("HDMI帧读取失败或为空");
|
||
return Optional<(byte[] header, byte[] data, byte[] footer)>.None;
|
||
}
|
||
|
||
var rgb565Data = frameResult.Value;
|
||
|
||
// 验证数据长度是否正确 (RGB24为每像素2字节)
|
||
var expectedLength = Width * Height * 2;
|
||
if (rgb565Data.Length != expectedLength)
|
||
{
|
||
logger.Warn("HDMI数据长度不匹配,期望: {Expected}, 实际: {Actual}",
|
||
expectedLength, rgb565Data.Length);
|
||
}
|
||
|
||
// 将RGB24转换为JPEG(参考Camera版本的处理)
|
||
var jpegResult = Common.Image.ConvertRGB565ToJpeg(rgb565Data, Width, Height, 80, false);
|
||
|
||
if (!jpegResult.IsSuccessful)
|
||
{
|
||
logger.Error("HDMI RGB565转JPEG失败: {Error}", jpegResult.Error);
|
||
return Optional<(byte[] header, byte[] data, byte[] footer)>.None;
|
||
}
|
||
|
||
var jpegData = jpegResult.Value;
|
||
|
||
var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
|
||
var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
|
||
|
||
return (mjpegFrameHeader, jpegData, mjpegFrameFooter);
|
||
}
|
||
|
||
public async ValueTask<Result<bool>> CheckHdmiIsReady()
|
||
{
|
||
var ret = await UDPClientPool.ReadAddrWithWait(
|
||
this.ep, this.taskID, HdmiInAddr.HDMI_NOT_READY, 0b00, 0b01, 100, this.timeout);
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to check HDMI status: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
return ret.Value;
|
||
}
|
||
|
||
public async ValueTask<Result<(int, int)>> GetHdmiResolution()
|
||
{
|
||
var ret = await UDPClientPool.ReadAddrByte(
|
||
this.ep, this.taskID, HdmiInAddr.HDMI_HEIGHT_WIDTH, this.timeout);
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
|
||
var data = ret.Value.Options.Data;
|
||
if (data == null || data.Length != 4)
|
||
{
|
||
logger.Error($"Invalid HDMI resolution data length: {data?.Length ?? 0}");
|
||
return new(new Exception("Invalid HDMI resolution data length"));
|
||
}
|
||
|
||
var width = (data[3] | (data[2] << 8)) - 1 - (((data[3] | (data[2] << 8)) - 1)%2);
|
||
var height = (data[1] | (data[0] << 8)) - 1 - (((data[1] | (data[0] << 8)) - 1)%2);
|
||
this.Width = width;
|
||
this.Height = height;
|
||
|
||
logger.Info($"HDMI resolution: {width}x{height}");
|
||
|
||
return new((width, height));
|
||
}
|
||
|
||
|
||
public async ValueTask<Result<bool>> ConnectJpeg2Hdmi(int width, int height)
|
||
{
|
||
if (width <= 0 || height <= 0)
|
||
{
|
||
logger.Error($"Invalid HDMI resolution: {width}x{height}");
|
||
return new(new ArgumentException("Invalid HDMI resolution"));
|
||
}
|
||
|
||
var frameSize = (UInt32)(width * height) / 2;
|
||
|
||
{
|
||
var ret = await UDPClientPool.WriteAddr(
|
||
this.ep, this.taskID, HdmiInAddr.CAPTURE_HEIGHT_WIDTH, (uint)((height << 16) + width), this.timeout);
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to set CAPTURE_HEIGHT_WIDTH: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
if (!ret.Value)
|
||
{
|
||
logger.Error($"Failed to set HDMI output start address");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
{
|
||
var ret = await UDPClientPool.WriteAddr(
|
||
this.ep, this.taskID, HdmiInAddr.START_WR_ADDR0, HdmiInAddr.ADDR_HDMI_WD_START, this.timeout);
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to set HDMI output start address: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
if (!ret.Value)
|
||
{
|
||
logger.Error($"Failed to set HDMI output start address");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
{
|
||
var ret = await UDPClientPool.WriteAddr(
|
||
this.ep, this.taskID, HdmiInAddr.END_WR_ADDR0,
|
||
HdmiInAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
|
||
if (!ret.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to set HDMI output end address: {ret.Error}");
|
||
return new(ret.Error);
|
||
}
|
||
if (!ret.Value)
|
||
{
|
||
logger.Error($"Failed to set HDMI output address");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
}
|