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;
}
-
}