From c8444d1d4eed6ee4dea314baa643eda732592631 Mon Sep 17 00:00:00 2001 From: alivender <13898766233@163.com> Date: Wed, 20 Aug 2025 01:37:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=A4=96=E8=AE=BEBASE=E5=81=8F=E7=A7=BB=E9=87=8F=EF=BC=9B?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0WS2812=E5=90=8E=E7=AB=AF=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E5=99=A8=EF=BC=9BDSO=E5=AF=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Hubs/WS2812Hub.cs | 138 ++++++++++++++ server/src/Peripherals/HdmiInClient.cs | 48 +++-- server/src/Peripherals/LogicAnalyzerClient.cs | 52 ++++-- server/src/Peripherals/WS2812Client.cs | 168 ++++++++++++++++++ 4 files changed, 370 insertions(+), 36 deletions(-) create mode 100644 server/src/Hubs/WS2812Hub.cs create mode 100644 server/src/Peripherals/WS2812Client.cs diff --git a/server/src/Hubs/WS2812Hub.cs b/server/src/Hubs/WS2812Hub.cs new file mode 100644 index 0000000..bdb08c0 --- /dev/null +++ b/server/src/Hubs/WS2812Hub.cs @@ -0,0 +1,138 @@ +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Cors; +using TypedSignalR.Client; +using Tapper; +using DotNext; +using Peripherals.WS2812Client; +using System.Collections.Concurrent; + +#pragma warning disable 1998 + +namespace server.Hubs; + +[Hub] +public interface IWS2812Hub +{ + Task GetAllLedColors(); + Task GetLedColor(int ledIndex); +} + +[Receiver] +public interface IWS2812Receiver +{ + Task OnReceive(RGBColor[] data); +} + +[TranspilationSource] +public class WS2812TaskStatus +{ + public bool IsRunning { get; set; } = false; +} + +class WS2812ScanTaskInfo +{ + public string BoardID { get; set; } + public string ClientID { get; set; } + public Task? ScanTask { get; set; } + public WS2812Client LedClient { get; set; } + public CancellationTokenSource CTS { get; set; } = new(); + public bool IsRunning { get; set; } = false; + + public WS2812ScanTaskInfo(string boardID, string clientID, WS2812Client client) + { + BoardID = boardID; + ClientID = clientID; + LedClient = client; + } + + public WS2812TaskStatus ToWS2812TaskStatus() + { + return new WS2812TaskStatus + { + IsRunning = IsRunning + }; + } +} + +[Authorize] +[EnableCors("SignalR")] +public class WS2812Hub : Hub, IWS2812Hub +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly IHubContext _hubContext; + private readonly Database.UserManager _userManager = new(); + private ConcurrentDictionary<(string, string), WS2812ScanTaskInfo> _scanTasks = new(); + + public WS2812Hub(IHubContext hubContext) + { + _hubContext = hubContext; + } + + private Optional TryGetBoard() + { + var userName = Context.User?.FindFirstValue(ClaimTypes.Name); + if (string.IsNullOrEmpty(userName)) + { + logger.Error("User name is null or empty"); + return null; + } + var userRet = _userManager.GetUserByName(userName); + if (!userRet.IsSuccessful || !userRet.Value.HasValue) + { + logger.Error($"User '{userName}' not found"); + return null; + } + var user = userRet.Value.Value; + var boardRet = _userManager.GetBoardByID(user.BoardID); + if (!boardRet.IsSuccessful || !boardRet.Value.HasValue) + { + logger.Error($"Board not found"); + return null; + } + return boardRet.Value.Value; + } + + public async Task GetAllLedColors() + { + try + { + var board = TryGetBoard().OrThrow(() => new Exception("Board not found")); + var client = new WS2812Client(board.IpAddr, board.Port, 0); + var result = await client.GetAllLedColors(); + if (!result.IsSuccessful) + { + logger.Error($"GetAllLedColors failed: {result.Error}"); + return null; + } + return result.Value; + } + catch (Exception ex) + { + logger.Error(ex, "Failed to get all LED colors"); + return null; + } + } + + public async Task GetLedColor(int ledIndex) + { + try + { + var board = TryGetBoard().OrThrow(() => new Exception("Board not found")); + var client = new WS2812Client(board.IpAddr, board.Port, 0); + var result = await client.GetLedColor(ledIndex); + if (!result.IsSuccessful) + { + logger.Error($"GetLedColor failed: {result.Error}"); + return null; + } + return result.Value; + } + catch (Exception ex) + { + logger.Error(ex, "Failed to get LED color"); + return null; + } + } +} diff --git a/server/src/Peripherals/HdmiInClient.cs b/server/src/Peripherals/HdmiInClient.cs index 3742f81..e3db675 100644 --- a/server/src/Peripherals/HdmiInClient.cs +++ b/server/src/Peripherals/HdmiInClient.cs @@ -10,11 +10,12 @@ static class HdmiInAddr public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0; - public const UInt32 START_WR_ADDR0 = BASE + 0x2; - public const UInt32 END_WR_ADDR0 = BASE + 0x3; + public const UInt32 START_WR_ADDR0 = BASE + 0x20; + public const UInt32 END_WR_ADDR0 = BASE + 0x21; - public const UInt32 HDMI_NOT_READY = BASE + 0x8; - public const UInt32 HDMI_HEIGHT_WIDTH = BASE + 0x9; + 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; } @@ -31,7 +32,7 @@ public class HdmiIn public int Width { get; private set; } public int Height { get; private set; } - public int FrameLength => Width * Height * 3 / 4; + public int FrameLength => Width * Height / 2; /// /// 初始化HDMI输入客户端 @@ -129,7 +130,7 @@ public class HdmiIn this.taskID, // taskID HdmiInAddr.ADDR_HDMI_WD_START, FrameLength, // 使用当前分辨率的动态大小 - BurstType.FixedBurst, + BurstType.ExtendBurst, this.timeout); if (!result.IsSuccessful) @@ -165,25 +166,22 @@ public class HdmiIn return Optional<(byte[] header, byte[] data, byte[] footer)>.None; } - var rgb24Data = frameResult.Value; + var rgb565Data = frameResult.Value; // 验证数据长度是否正确 (RGB24为每像素2字节) var expectedLength = Width * Height * 2; - if (rgb24Data.Length != expectedLength) + if (rgb565Data.Length != expectedLength) { logger.Warn("HDMI数据长度不匹配,期望: {Expected}, 实际: {Actual}", - expectedLength, rgb24Data.Length); + expectedLength, rgb565Data.Length); } // 将RGB24转换为JPEG(参考Camera版本的处理) - var jpegStartTime = DateTime.UtcNow; - var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Data, Width, Height, 80); - var jpegEndTime = DateTime.UtcNow; - var jpegTime = (jpegEndTime - jpegStartTime).TotalMilliseconds; + var jpegResult = Common.Image.ConvertRGB565ToJpeg(rgb565Data, Width, Height, 80, false); if (!jpegResult.IsSuccessful) { - logger.Error("HDMI RGB24转JPEG失败: {Error}", jpegResult.Error); + logger.Error("HDMI RGB565转JPEG失败: {Error}", jpegResult.Error); return Optional<(byte[] header, byte[] data, byte[] footer)>.None; } @@ -224,8 +222,8 @@ public class HdmiIn return new(new Exception("Invalid HDMI resolution data length")); } - var width = data[3] | (data[2] << 8); - var height = data[1] | (data[0] << 8); + 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; @@ -234,6 +232,7 @@ public class HdmiIn return new((width, height)); } + public async ValueTask> ConnectJpeg2Hdmi(int width, int height) { if (width <= 0 || height <= 0) @@ -242,7 +241,22 @@ public class HdmiIn return new(new ArgumentException("Invalid HDMI resolution")); } - var frameSize = (UInt32)(width * height); + 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( diff --git a/server/src/Peripherals/LogicAnalyzerClient.cs b/server/src/Peripherals/LogicAnalyzerClient.cs index c6764d9..27e04a5 100644 --- a/server/src/Peripherals/LogicAnalyzerClient.cs +++ b/server/src/Peripherals/LogicAnalyzerClient.cs @@ -9,7 +9,7 @@ namespace Peripherals.LogicAnalyzerClient; static class AnalyzerAddr { const UInt32 BASE = 0x9000_0000; - const UInt32 DMA1_BASE = 0x7000_0000; + const UInt32 DMA_BASE = 0xA000_0000; const UInt32 DDR_BASE = 0x0000_0000; /// @@ -68,9 +68,9 @@ static class AnalyzerAddr public const UInt32 PRE_LOAD_NUM_ADDR = BASE + 0x0000_0003; public const UInt32 CAHNNEL_DIV_ADDR = BASE + 0x0000_0004; public const UInt32 CLOCK_DIV_ADDR = BASE + 0x0000_0005; - public const UInt32 DMA1_START_WRITE_ADDR = DMA1_BASE + 0x0000_0012; - public const UInt32 DMA1_END_WRITE_ADDR = DMA1_BASE + 0x0000_0013; - public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014; + public const UInt32 DMA_CAPTURE_RD_CTRL1 = DMA_BASE + 0x1; + public const UInt32 DMA_START_WRITE_ADDR1 = DMA_BASE + 0x22; + public const UInt32 DMA_END_WRITE_ADDR1 = DMA_BASE + 0x23; public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0100_0000; /// @@ -327,20 +327,34 @@ public class Analyzer /// 操作结果,成功返回true,否则返回异常信息 public async ValueTask> SetCaptureMode(bool captureOn, bool force) { - // 构造寄存器值 - UInt32 value = 0; - if (captureOn) value |= 1 << 0; { - var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_CAPTURE_CTRL_ADDR, value, this.timeout); + var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA_CAPTURE_RD_CTRL1, 0x00000000u, this.timeout); if (!ret.IsSuccessful) { - logger.Error($"Failed to set DMA1_CAPTURE_CTRL_ADDR: {ret.Error}"); + logger.Error($"Failed to set DMA_CAPTURE_RD_CTRL to 0: {ret.Error}"); return new(ret.Error); } if (!ret.Value) { - logger.Error("WriteAddr to DMA1_CAPTURE_CTRL_ADDR returned false"); - return new(new Exception("Failed to set DMA1_CAPTURE_CTRL_ADDR")); + logger.Error("WriteAddr to DMA_CAPTURE_RD_CTRL returned false"); + return new(new Exception("Failed to set DMA_CAPTURE_RD_CTRL")); + } + } + await Task.Delay(5); + // 构造寄存器值 + UInt32 value = 0; + if (captureOn) value |= 1 << 0; + { + var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA_CAPTURE_RD_CTRL1, value, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to set DMA_CAPTURE_RD_CTRL: {ret.Error}"); + return new(ret.Error); + } + if (!ret.Value) + { + logger.Error("WriteAddr to DMA_CAPTURE_RD_CTRL returned false"); + return new(new Exception("Failed to set DMA_CAPTURE_RD_CTRL")); } } if (force) value |= 1 << 8; @@ -472,29 +486,29 @@ public class Analyzer } } { - var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_START_WRITE_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR, this.timeout); + var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA_START_WRITE_ADDR1, AnalyzerAddr.STORE_OFFSET_ADDR, this.timeout); if (!ret.IsSuccessful) { - logger.Error($"Failed to set DMA1_START_WRITE_ADDR: {ret.Error}"); + logger.Error($"Failed to set DMA_START_WRITE_ADDR: {ret.Error}"); return new(ret.Error); } if (!ret.Value) { - logger.Error("WriteAddr to DMA1_START_WRITE_ADDR returned false"); - return new(new Exception("Failed to set DMA1_START_WRITE_ADDR")); + logger.Error("WriteAddr to DMA_START_WRITE_ADDR returned false"); + return new(new Exception("Failed to set DMA_START_WRITE_ADDR")); } } { - var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_END_WRITE_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR + (UInt32)(capture_length - 1), this.timeout); + var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA_END_WRITE_ADDR1, AnalyzerAddr.STORE_OFFSET_ADDR + (UInt32)(capture_length - 1), this.timeout); if (!ret.IsSuccessful) { - logger.Error($"Failed to set DMA1_END_WRITE_ADDR: {ret.Error}"); + logger.Error($"Failed to set DMA_END_WRITE_ADDR: {ret.Error}"); return new(ret.Error); } if (!ret.Value) { - logger.Error("WriteAddr to DMA1_END_WRITE_ADDR returned false"); - return new(new Exception("Failed to set DMA1_END_WRITE_ADDR")); + logger.Error("WriteAddr to DMA_END_WRITE_ADDR returned false"); + return new(new Exception("Failed to set DMA_END_WRITE_ADDR")); } } { diff --git a/server/src/Peripherals/WS2812Client.cs b/server/src/Peripherals/WS2812Client.cs new file mode 100644 index 0000000..38a3190 --- /dev/null +++ b/server/src/Peripherals/WS2812Client.cs @@ -0,0 +1,168 @@ +using System.Net; +using DotNext; + +namespace Peripherals.WS2812Client; + +class WS2812Addr +{ + public const UInt32 BASE = 0xB0_00_01_00; + public const int LED_COUNT = 128; +} + +/// +/// RGB颜色结构体,包含红、绿、蓝三个颜色分量 +/// +public struct RGBColor +{ + public byte Red { get; set; } + public byte Green { get; set; } + public byte Blue { get; set; } + + public RGBColor(byte red, byte green, byte blue) + { + Red = red; + Green = green; + Blue = blue; + } + + /// + /// 从32位数据的低24位提取RGB颜色 + /// + /// 32位数据 + /// RGB颜色 + public static RGBColor FromUInt32(UInt32 data) + { + return new RGBColor( + (byte)((data >> 16) & 0xFF), // Red + (byte)((data >> 8) & 0xFF), // Green + (byte)(data & 0xFF) // Blue + ); + } + + /// + /// 转换为32位数据格式 + /// + /// 32位数据 + public UInt32 ToUInt32() + { + return ((UInt32)Red << 16) | ((UInt32)Green << 8) | (UInt32)Blue; + } + + public override string ToString() + { + return $"RGB({Red}, {Green}, {Blue})"; + } +} + +public class WS2812Client +{ + 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 WS2812Client(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; + } + + /// + /// 获取指定灯珠的RGB颜色 + /// + /// 灯珠索引,范围0-127 + /// RGB颜色结果 + public async ValueTask> GetLedColor(int ledIndex) + { + if (ledIndex < 0 || ledIndex >= WS2812Addr.LED_COUNT) + { + return new(new ArgumentOutOfRangeException(nameof(ledIndex), + $"LED index must be between 0 and {WS2812Addr.LED_COUNT - 1}")); + } + + if (MsgBus.IsRunning) + MsgBus.UDPServer.ClearUDPData(this.address, this.taskID); + else + return new(new Exception("Message Bus not work!")); + + var addr = WS2812Addr.BASE + (UInt32)(ledIndex * 4); // 每个地址32位,步长为4字节 + var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, addr, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Get LED {ledIndex} color failed: {ret.Error}"); + return new(ret.Error); + } + var retData = ret.Value.Options.Data; + if (retData is null) + return new(new Exception($"Device {address} receive none")); + if (retData.Length < 4) + { + var error = new Exception($"Invalid data length: expected 4 bytes, got {retData.Length}"); + logger.Error($"Get LED {ledIndex} color failed: {error}"); + return new(error); + } + + var colorData = Convert.ToUInt32(Common.Number.BytesToUInt64(retData).Value); + var color = RGBColor.FromUInt32(colorData); + return new(color); + } + + /// + /// 获取所有灯珠的RGB颜色 + /// + /// 包含所有灯珠颜色的数组 + public async ValueTask> GetAllLedColors() + { + if (MsgBus.IsRunning) + MsgBus.UDPServer.ClearUDPData(this.address, this.taskID); + else + return new(new Exception("Message Bus not work!")); + + try + { + // 一次性读取所有LED数据,每个LED占用4字节,总共128*4=512字节 + var ret = await UDPClientPool.ReadAddr4Bytes(this.ep, this.taskID, WS2812Addr.BASE, WS2812Addr.LED_COUNT, this.timeout); + + if (!ret.IsSuccessful) + { + logger.Error($"Get all LED colors failed: {ret.Error}"); + return new(ret.Error); + } + + var data = ret.Value; + var expectedLength = WS2812Addr.LED_COUNT * 4; // 128 * 4 = 512 bytes + + if (data.Length < expectedLength) + { + var error = new Exception($"Invalid data length: expected {expectedLength} bytes, got {data.Length}"); + logger.Error(error.Message); + return new(error); + } + + var colors = new RGBColor[WS2812Addr.LED_COUNT]; + + for (int i = 0; i < WS2812Addr.LED_COUNT; i++) + { + var offset = i * 4; + // 将4字节数据转换为UInt32 + var colorData = BitConverter.ToUInt32(data, offset); + colors[i] = RGBColor.FromUInt32(colorData); + } + + return new(colors); + } + catch (Exception ex) + { + logger.Error($"Get all LED colors failed: {ex}"); + return new(ex); + } + } +}