From 178ac0de67a671ca3d7b9af924f3b3559d3c0d8a Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Thu, 3 Jul 2025 15:47:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=86=E6=91=84=E5=83=8F=E5=A4=B4?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E4=BB=8E=E7=94=9F=E6=88=90=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=94=B9=E4=B8=BA=E8=AF=BB=E5=8F=96=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Common.cs | 339 ++++++++++++++++++ server/src/Peripherals/CameraClient.cs | 66 ++++ server/src/Services/HttpVideoStreamService.cs | 108 +++--- server/src/UdpClientPool.cs | 150 ++++++-- 4 files changed, 583 insertions(+), 80 deletions(-) diff --git a/server/src/Common.cs b/server/src/Common.cs index fbc2170..ee7cd7a 100644 --- a/server/src/Common.cs +++ b/server/src/Common.cs @@ -1,5 +1,9 @@ using System.Collections; using DotNext; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; +using System.Text; namespace Common { @@ -389,4 +393,339 @@ namespace Common } + /// + /// 图像处理工具 + /// + public class Image + { + /// + /// 将 RGB565 格式转换为 RGB24 格式 + /// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节) + /// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节) + /// + /// RGB565格式的原始数据 + /// 图像宽度 + /// 图像高度 + /// 是否为小端序(默认为true) + /// RGB24格式的转换后数据 + public static Result ConvertRGB565ToRGB24(byte[] rgb565Data, int width, int height, bool isLittleEndian = true) + { + if (rgb565Data == null) + return new(new ArgumentNullException(nameof(rgb565Data))); + + if (width <= 0 || height <= 0) + return new(new ArgumentException("Width and height must be positive")); + + // 计算像素数量 + var expectedPixelCount = width * height; + var actualPixelCount = rgb565Data.Length / 2; + + if (actualPixelCount < expectedPixelCount) + { + return new(new ArgumentException( + $"RGB565 data length insufficient. Expected: {expectedPixelCount * 2} bytes, Actual: {rgb565Data.Length} bytes")); + } + + try + { + var pixelCount = Math.Min(actualPixelCount, expectedPixelCount); + var rgb24Data = new byte[pixelCount * 3]; + + for (int i = 0; i < pixelCount; i++) + { + // 读取 RGB565 数据 + var rgb565Index = i * 2; + if (rgb565Index + 1 >= rgb565Data.Length) break; + + // 组合成16位值 + UInt16 rgb565; + if (isLittleEndian) + { + rgb565 = (UInt16)(rgb565Data[rgb565Index] | (rgb565Data[rgb565Index + 1] << 8)); + } + else + { + rgb565 = (UInt16)((rgb565Data[rgb565Index] << 8) | rgb565Data[rgb565Index + 1]); + } + + // 提取各颜色分量 + var r5 = (rgb565 >> 11) & 0x1F; // 高5位为红色 + var g6 = (rgb565 >> 5) & 0x3F; // 中间6位为绿色 + var b5 = rgb565 & 0x1F; // 低5位为蓝色 + + // 转换为8位颜色值 + var r8 = (byte)((r5 * 255) / 31); // 5位扩展到8位 + var g8 = (byte)((g6 * 255) / 63); // 6位扩展到8位 + var b8 = (byte)((b5 * 255) / 31); // 5位扩展到8位 + + // 存储到 RGB24 数组 + var rgb24Index = i * 3; + rgb24Data[rgb24Index] = r8; // R + rgb24Data[rgb24Index + 1] = g8; // G + rgb24Data[rgb24Index + 2] = b8; // B + } + + return rgb24Data; + } + catch (Exception ex) + { + return new(ex); + } + } + + /// + /// 将 RGB24 格式转换为 RGB565 格式 + /// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节) + /// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节) + /// + /// RGB24格式的原始数据 + /// 图像宽度 + /// 图像高度 + /// 是否为小端序(默认为true) + /// RGB565格式的转换后数据 + public static Result ConvertRGB24ToRGB565(byte[] rgb24Data, int width, int height, bool isLittleEndian = true) + { + if (rgb24Data == null) + return new(new ArgumentNullException(nameof(rgb24Data))); + + if (width <= 0 || height <= 0) + return new(new ArgumentException("Width and height must be positive")); + + var expectedPixelCount = width * height; + var actualPixelCount = rgb24Data.Length / 3; + + if (actualPixelCount < expectedPixelCount) + { + return new(new ArgumentException( + $"RGB24 data length insufficient. Expected: {expectedPixelCount * 3} bytes, Actual: {rgb24Data.Length} bytes")); + } + + try + { + var pixelCount = Math.Min(actualPixelCount, expectedPixelCount); + var rgb565Data = new byte[pixelCount * 2]; + + for (int i = 0; i < pixelCount; i++) + { + var rgb24Index = i * 3; + if (rgb24Index + 2 >= rgb24Data.Length) break; + + // 读取 RGB24 数据 + var r8 = rgb24Data[rgb24Index]; + var g8 = rgb24Data[rgb24Index + 1]; + var b8 = rgb24Data[rgb24Index + 2]; + + // 转换为5位、6位、5位 + var r5 = (UInt16)((r8 * 31) / 255); + var g6 = (UInt16)((g8 * 63) / 255); + var b5 = (UInt16)((b8 * 31) / 255); + + // 组合成16位值 + var rgb565 = (UInt16)((r5 << 11) | (g6 << 5) | b5); + + // 存储到 RGB565 数组 + var rgb565Index = i * 2; + if (isLittleEndian) + { + rgb565Data[rgb565Index] = (byte)(rgb565 & 0xFF); + rgb565Data[rgb565Index + 1] = (byte)(rgb565 >> 8); + } + else + { + rgb565Data[rgb565Index] = (byte)(rgb565 >> 8); + rgb565Data[rgb565Index + 1] = (byte)(rgb565 & 0xFF); + } + } + + return rgb565Data; + } + catch (Exception ex) + { + return new(ex); + } + } + + /// + /// 将 RGB24 数据转换为 JPEG 格式 + /// + /// RGB24格式的图像数据 + /// 图像宽度 + /// 图像高度 + /// JPEG质量(1-100,默认80) + /// JPEG格式的字节数组 + public static Result ConvertRGB24ToJpeg(byte[] rgb24Data, int width, int height, int quality = 80) + { + if (rgb24Data == null) + return new(new ArgumentNullException(nameof(rgb24Data))); + + if (width <= 0 || height <= 0) + return new(new ArgumentException("Width and height must be positive")); + + if (quality < 1 || quality > 100) + return new(new ArgumentException("Quality must be between 1 and 100")); + + var expectedDataLength = width * height * 3; + if (rgb24Data.Length < expectedDataLength) + { + return new(new ArgumentException( + $"RGB24 data length insufficient. Expected: {expectedDataLength} bytes, Actual: {rgb24Data.Length} bytes")); + } + + try + { + using var image = new SixLabors.ImageSharp.Image(width, height); + + // 将 RGB 数据复制到 ImageSharp 图像 + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int index = (y * width + x) * 3; + if (index + 2 < rgb24Data.Length) + { + var pixel = new Rgb24(rgb24Data[index], rgb24Data[index + 1], rgb24Data[index + 2]); + image[x, y] = pixel; + } + } + } + + using var stream = new MemoryStream(); + image.SaveAsJpeg(stream, new JpegEncoder { Quality = quality }); + return stream.ToArray(); + } + catch (Exception ex) + { + return new(ex); + } + } + + /// + /// 将 RGB565 数据直接转换为 JPEG 格式 + /// + /// RGB565格式的图像数据 + /// 图像宽度 + /// 图像高度 + /// JPEG质量(1-100,默认80) + /// 是否为小端序(默认为true) + /// JPEG格式的字节数组 + public static Result ConvertRGB565ToJpeg(byte[] rgb565Data, int width, int height, int quality = 80, bool isLittleEndian = true) + { + // 先转换为RGB24 + var rgb24Result = ConvertRGB565ToRGB24(rgb565Data, width, height, isLittleEndian); + if (!rgb24Result.IsSuccessful) + { + return new(rgb24Result.Error); + } + + // 再转换为JPEG + return ConvertRGB24ToJpeg(rgb24Result.Value, width, height, quality); + } + + /// + /// 创建 MJPEG 帧头部 + /// + /// 帧数据长度 + /// 边界字符串(默认为"--boundary") + /// MJPEG帧头部字节数组 + public static byte[] CreateMjpegFrameHeader(int frameDataLength, string boundary = "--boundary") + { + var header = $"{boundary}\r\nContent-Type: image/jpeg\r\nContent-Length: {frameDataLength}\r\n\r\n"; + return Encoding.ASCII.GetBytes(header); + } + + /// + /// 创建 MJPEG 帧尾部 + /// + /// MJPEG帧尾部字节数组 + public static byte[] CreateMjpegFrameFooter() + { + return Encoding.ASCII.GetBytes("\r\n"); + } + + /// + /// 创建完整的 MJPEG 帧数据 + /// + /// JPEG数据 + /// 边界字符串(默认为"--boundary") + /// 完整的MJPEG帧数据 + public static Result CreateMjpegFrame(byte[] jpegData, string boundary = "--boundary") + { + if (jpegData == null) + return new(new ArgumentNullException(nameof(jpegData))); + + try + { + var header = CreateMjpegFrameHeader(jpegData.Length, boundary); + var footer = CreateMjpegFrameFooter(); + + var totalLength = header.Length + jpegData.Length + footer.Length; + var frameData = new byte[totalLength]; + + var offset = 0; + Array.Copy(header, 0, frameData, offset, header.Length); + offset += header.Length; + + Array.Copy(jpegData, 0, frameData, offset, jpegData.Length); + offset += jpegData.Length; + + Array.Copy(footer, 0, frameData, offset, footer.Length); + + return frameData; + } + catch (Exception ex) + { + return new(ex); + } + } + + /// + /// 验证图像数据长度是否正确 + /// + /// 图像数据 + /// 图像宽度 + /// 图像高度 + /// 每像素字节数 + /// 验证结果 + public static bool ValidateImageDataLength(byte[] data, int width, int height, int bytesPerPixel) + { + if (data == null || width <= 0 || height <= 0 || bytesPerPixel <= 0) + return false; + + var expectedLength = width * height * bytesPerPixel; + return data.Length >= expectedLength; + } + + /// + /// 获取图像格式信息 + /// + /// 图像格式枚举 + /// 格式信息 + public static ImageFormatInfo GetImageFormatInfo(ImageFormat format) + { + return format switch + { + ImageFormat.RGB565 => new ImageFormatInfo("RGB565", 2, "16-bit RGB format (5R+6G+5B)"), + ImageFormat.RGB24 => new ImageFormatInfo("RGB24", 3, "24-bit RGB format (8R+8G+8B)"), + ImageFormat.RGBA32 => new ImageFormatInfo("RGBA32", 4, "32-bit RGBA format (8R+8G+8B+8A)"), + ImageFormat.Grayscale8 => new ImageFormatInfo("Grayscale8", 1, "8-bit grayscale format"), + _ => new ImageFormatInfo("Unknown", 0, "Unknown image format") + }; + } + } + + /// + /// 图像格式枚举 + /// + public enum ImageFormat + { + RGB565, + RGB24, + RGBA32, + Grayscale8 + } + + /// + /// 图像格式信息 + /// + public record ImageFormatInfo(string Name, int BytesPerPixel, string Description); } diff --git a/server/src/Peripherals/CameraClient.cs b/server/src/Peripherals/CameraClient.cs index e69de29..ef65c6b 100644 --- a/server/src/Peripherals/CameraClient.cs +++ b/server/src/Peripherals/CameraClient.cs @@ -0,0 +1,66 @@ +using System.Net; +using DotNext; + +namespace Peripherals.CameraClient; + +static class CameraAddr +{ + public const UInt32 Base = 0x0000_0000; + public const UInt32 FrameLength = 0x25800; +} + +class Camera +{ + private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + readonly int timeout = 2000; + + readonly int port; + readonly string address; + private IPEndPoint ep; + + /// + /// 初始化摄像头客户端 + /// + /// 摄像头设备IP地址 + /// 摄像头设备端口 + /// 超时时间(毫秒) + public Camera(string address, int port, int timeout = 2000) + { + 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.timeout = timeout; + } + + /// + /// 读取一帧图像数据 + /// + /// 包含图像数据的字节数组 + public async ValueTask> ReadFrame() + { + // 清除UDP服务器接收缓冲区 + await MsgBus.UDPServer.ClearUDPData(this.address, 3); + + logger.Trace($"Clear up udp server {this.address} receive data"); + + // 使用UDPClientPool读取图像帧数据 + var result = await UDPClientPool.ReadAddrBytes( + this.ep, + 3, // taskID + CameraAddr.Base, + (int)CameraAddr.FrameLength, + this.timeout); + + if (!result.IsSuccessful) + { + logger.Error($"Failed to read frame from camera {this.address}:{this.port}, error: {result.Error}"); + return new(result.Error); + } + + logger.Debug($"Successfully read frame from camera {this.address}:{this.port}, data length: {result.Value.Length} bytes"); + return result.Value; + } +} diff --git a/server/src/Services/HttpVideoStreamService.cs b/server/src/Services/HttpVideoStreamService.cs index c35fcc6..99bbbb3 100644 --- a/server/src/Services/HttpVideoStreamService.cs +++ b/server/src/Services/HttpVideoStreamService.cs @@ -3,8 +3,10 @@ using System.Text; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.PixelFormats; +using Peripherals.CameraClient; // 添加摄像头客户端引用 namespace server.Services; + /// /// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页 /// 简化版本实现,先建立基础框架 @@ -18,6 +20,11 @@ public class HttpVideoStreamService : BackgroundService private readonly int _frameWidth = 640; private readonly int _frameHeight = 480; + // 摄像头客户端 + private Camera? _camera; + private readonly string _cameraAddress = "192.168.1.100"; // 根据实际FPGA地址配置 + private readonly int _cameraPort = 8888; // 根据实际端口配置 + // 模拟 FPGA 图像数据 private int _frameCounter = 0; private readonly List _activeClients = new List(); @@ -62,6 +69,8 @@ public class HttpVideoStreamService : BackgroundService /// public HttpVideoStreamService() { + // 初始化摄像头客户端 + _camera = new Camera(_cameraAddress, _cameraPort); } /// @@ -353,57 +362,70 @@ public class HttpVideoStreamService : BackgroundService } /// - /// 模拟从 FPGA 获取图像数据的函数 - /// 实际实现时,这里应该通过 UDP 连接读取 FPGA 特定地址范围的数据 + /// 从 FPGA 获取图像数据 + /// 实际从摄像头读取 RGB565 格式数据并转换为 RGB24 /// private async Task GetFPGAImageData() { - // 模拟异步 FPGA 数据读取 - await Task.Delay(1); - - // 简化的模拟图像数据生成 - var random = new Random(_frameCounter); - var imageData = new byte[_frameWidth * _frameHeight * 3]; // RGB24 格式 - - // 生成简单的彩色噪声图案 - for (int i = 0; i < imageData.Length; i += 3) + if (_camera == null) { - // 基于帧计数器和位置生成颜色 - var baseColor = (_frameCounter + i / 3) % 256; - imageData[i] = (byte)((baseColor + random.Next(0, 50)) % 256); // R - imageData[i + 1] = (byte)((baseColor * 2 + random.Next(0, 50)) % 256); // G - imageData[i + 2] = (byte)((baseColor * 3 + random.Next(0, 50)) % 256); // B + logger.Error("摄像头客户端未初始化"); + return new byte[0]; } - if (_frameCounter % 30 == 0) // 每秒更新一次日志 + try { - logger.Debug("生成第 {FrameNumber} 帧", _frameCounter); - } + // 从摄像头读取帧数据 + var result = await _camera.ReadFrame(); + + if (!result.IsSuccessful) + { + logger.Error("读取摄像头帧数据失败: {Error}", result.Error); + return new byte[0]; + } - return imageData; + var rgb565Data = result.Value; + + // 验证数据长度是否正确 + if (!Common.Image.ValidateImageDataLength(rgb565Data, _frameWidth, _frameHeight, 2)) + { + logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}", + _frameWidth * _frameHeight * 2, rgb565Data.Length); + } + + // 将 RGB565 转换为 RGB24 + var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, _frameWidth, _frameHeight); + if (!rgb24Result.IsSuccessful) + { + logger.Error("RGB565转RGB24失败: {Error}", rgb24Result.Error); + return new byte[0]; + } + + if (_frameCounter % 30 == 0) // 每秒更新一次日志 + { + logger.Debug("成功获取第 {FrameNumber} 帧,RGB565大小: {RGB565Size} 字节, RGB24大小: {RGB24Size} 字节", + _frameCounter, rgb565Data.Length, rgb24Result.Value.Length); + } + + return rgb24Result.Value; + } + catch (Exception ex) + { + logger.Error(ex, "获取FPGA图像数据时发生错误"); + return new byte[0]; + } } - /// - /// 将 RGB 图像数据转换为 JPEG 格式 - /// - private byte[] ConvertToJpeg(byte[] rgbData) + private async Task ConvertToJpeg(byte[] rgbData) { - using var image = new Image(_frameWidth, _frameHeight); - - // 将 RGB 数据复制到 ImageSharp 图像 - for (int y = 0; y < _frameHeight; y++) + var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgbData, _frameWidth, _frameHeight, 80); + if (!jpegResult.IsSuccessful) { - for (int x = 0; x < _frameWidth; x++) - { - int index = (y * _frameWidth + x) * 3; - var pixel = new Rgb24(rgbData[index], rgbData[index + 1], rgbData[index + 2]); - image[x, y] = pixel; - } + logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error); + return new byte[0]; } - - using var stream = new MemoryStream(); - image.SaveAsJpeg(stream, new JpegEncoder { Quality = 80 }); - return stream.ToArray(); + + return jpegResult.Value; } /// @@ -417,9 +439,9 @@ public class HttpVideoStreamService : BackgroundService return; } - // 准备MJPEG帧数据 - var mjpegFrameHeader = $"--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: {frameData.Length}\r\n\r\n"; - var headerBytes = Encoding.ASCII.GetBytes(mjpegFrameHeader); + // 使用Common中的方法准备MJPEG帧数据 + var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(frameData.Length); + var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter(); var clientsToRemove = new List(); var clientsToProcess = new List(); @@ -441,13 +463,13 @@ public class HttpVideoStreamService : BackgroundService try { // 发送帧头部 - await client.OutputStream.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken); + await client.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken); // 发送JPEG数据 await client.OutputStream.WriteAsync(frameData, 0, frameData.Length, cancellationToken); // 发送结尾换行符 - await client.OutputStream.WriteAsync(Encoding.ASCII.GetBytes("\r\n"), 0, 2, cancellationToken); + await client.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken); // 确保数据立即发送 await client.OutputStream.FlushAsync(cancellationToken); diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index b881d5a..57e923a 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -177,13 +177,13 @@ public class UDPClientPool } /// - /// [TODO:description] + /// 读取设备地址数据 /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] + /// IP端点(IP地址与端口) + /// 任务ID + /// 设备地址 + /// 超时时间(毫秒) + /// 读取结果,包含接收到的数据包 public static async ValueTask> ReadAddr( IPEndPoint endPoint, int taskID, uint devAddr, int timeout = 1000) { @@ -218,15 +218,15 @@ public class UDPClientPool } /// - /// [TODO:description] + /// 读取设备地址数据并校验结果 /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] + /// IP端点(IP地址与端口) + /// 任务ID + /// 设备地址 + /// 期望的结果值 + /// 结果掩码,用于位校验 + /// 超时时间(毫秒) + /// 校验结果,true表示数据匹配期望值 public static async ValueTask> ReadAddr( IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000) { @@ -257,15 +257,15 @@ public class UDPClientPool } /// - /// [TODO:description] + /// 读取设备地址数据并等待直到结果匹配或超时 /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] + /// IP端点(IP地址与端口) + /// 任务ID + /// 设备地址 + /// 期望的结果值 + /// 结果掩码,用于位校验 + /// 超时时间(毫秒) + /// 校验结果,true表示在超时前数据匹配期望值 public static async ValueTask> ReadAddrWithWait( IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000) { @@ -305,16 +305,93 @@ public class UDPClientPool return false; } + /// + /// 从设备地址读取字节数组数据(支持大数据量分段传输) + /// + /// IP端点(IP地址与端口) + /// 任务ID + /// 设备地址 + /// 要读取的数据长度(字节) + /// 超时时间(毫秒) + /// 读取结果,包含接收到的字节数组 + public static async ValueTask> ReadAddrBytes( + IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000) + { + var ret = false; + var opts = new SendAddrPackOptions(); + var resultData = new List(); + + opts.BurstType = BurstType.FixedBurst; + opts.CommandID = Convert.ToByte(taskID); + opts.Address = devAddr; + opts.IsWrite = false; + + // Check Msg Bus + if (!MsgBus.IsRunning) + return new(new Exception("Message bus not working!")); + + // Calculate read times and segments + var maxBytesPerRead = 256 * (32 / 8); // 1024 bytes per read + var hasRest = dataLength % maxBytesPerRead != 0; + var readTimes = hasRest ? + dataLength / maxBytesPerRead + 1 : + dataLength / maxBytesPerRead; + + for (var i = 0; i < readTimes; i++) + { + // Calculate current segment size + var isLastSegment = i == readTimes - 1; + var currentSegmentSize = isLastSegment && hasRest ? + dataLength % maxBytesPerRead : + maxBytesPerRead; + + // Set burst length (in 32-bit words) + opts.BurstLength = (byte)(currentSegmentSize / 4 - 1); + + // Update address for current segment + opts.Address = devAddr + (uint)(i * maxBytesPerRead); + + // Send read address package + ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts)); + if (!ret) return new(new Exception($"Send address package failed at segment {i}!")); + + // Wait for data response + var retPack = await MsgBus.UDPServer.WaitForDataAsync( + endPoint.Address.ToString(), taskID, endPoint.Port, timeout); + if (!retPack.IsSuccessful) return new(retPack.Error); + + if (!retPack.Value.IsSuccessful) + return new(new Exception($"Read address package failed at segment {i}")); + + var retPackOpts = retPack.Value.Options; + if (retPackOpts.Data is null) + return new(new Exception($"Data is null at segment {i}, package: {retPackOpts.ToString()}")); + + // Validate received data length + if (retPackOpts.Data.Length != currentSegmentSize) + return new(new Exception($"Expected {currentSegmentSize} bytes but received {retPackOpts.Data.Length} bytes at segment {i}")); + + // Add received data to result + resultData.AddRange(retPackOpts.Data); + } + + // Validate total data length + if (resultData.Count != dataLength) + return new(new Exception($"Expected total {dataLength} bytes but received {resultData.Count} bytes")); + + return resultData.ToArray(); + } + /// - /// [TODO:description] + /// 向设备地址写入32位数据 /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] + /// IP端点(IP地址与端口) + /// 任务ID + /// 设备地址 + /// 要写入的32位数据 + /// 超时时间(毫秒) + /// 写入结果,true表示写入成功 public static async ValueTask> WriteAddr( IPEndPoint endPoint, int taskID, UInt32 devAddr, UInt32 data, int timeout = 1000) { @@ -348,14 +425,14 @@ public class UDPClientPool } /// - /// [TODO:description] + /// 向设备地址写入字节数组数据(支持大数据量分段传输) /// - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:parameter] - /// [TODO:return] + /// IP端点(IP地址与端口) + /// 任务ID + /// 设备地址 + /// 要写入的字节数组 + /// 超时时间(毫秒) + /// 写入结果,true表示写入成功 public static async ValueTask> WriteAddr( IPEndPoint endPoint, int taskID, UInt32 devAddr, byte[] dataArray, int timeout = 1000) { @@ -404,5 +481,4 @@ public class UDPClientPool return true; } - }