Compare commits
4 Commits
23236b22bd
...
443aea5e3e
Author | SHA1 | Date |
---|---|---|
|
443aea5e3e | |
|
67bdec8570 | |
|
1af3fa3a8f | |
|
dd7efe3c84 |
|
@ -1,731 +0,0 @@
|
|||
using System.Collections;
|
||||
using DotNext;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Text;
|
||||
|
||||
namespace Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 数字处理工具
|
||||
/// </summary>
|
||||
public class Number
|
||||
{
|
||||
private static readonly byte[] BitReverseTable = new byte[] {
|
||||
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
|
||||
0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
|
||||
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
|
||||
0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
|
||||
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
|
||||
0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
|
||||
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
|
||||
0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
|
||||
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
|
||||
0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
|
||||
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
|
||||
0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
|
||||
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
|
||||
0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
|
||||
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
|
||||
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
|
||||
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
|
||||
0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
|
||||
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
|
||||
0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
|
||||
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
|
||||
0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
|
||||
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
|
||||
0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
|
||||
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
|
||||
0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
|
||||
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
|
||||
0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
|
||||
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
|
||||
0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
|
||||
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
|
||||
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 整数转成二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="num">整数</param>
|
||||
/// <param name="length">整数长度</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>二进制字节数组</returns>
|
||||
public static Result<byte[]> NumberToBytes(ulong num, uint length, bool isLowNumHigh = false)
|
||||
{
|
||||
if (length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(length)
|
||||
));
|
||||
}
|
||||
|
||||
var arr = new byte[length];
|
||||
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
arr[length - 1 - i] = Convert.ToByte((num >> (i << 3)) & (0xFF));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
arr[i] = Convert.ToByte((num >> ((int)(length - 1 - i) << 3)) & (0xFF));
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二进制字节数组转成64bits整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<UInt64> BytesToUInt64(byte[] bytes, bool isLowNumHigh = false)
|
||||
{
|
||||
if (bytes.Length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
UInt64 num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
try
|
||||
{
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt64((UInt64)bytes[len - 1 - i] << (i << 3));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt64((UInt64)bytes[i] << ((int)(len - 1 - i) << 3));
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二进制字节数组转成32bits整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<UInt32> BytesToUInt32(byte[] bytes, bool isLowNumHigh = false)
|
||||
{
|
||||
if (bytes.Length > 4)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 4 bytes(32 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
UInt32 num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
try
|
||||
{
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt32((UInt32)bytes[len - 1 - i] << (i << 3));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt32((UInt32)bytes[i] << ((int)(len - 1 - i) << 3));
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="uintArray">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static Result<byte[]> UInt32ArrayToBytes(UInt32[] uintArray)
|
||||
{
|
||||
byte[] byteArray = new byte[uintArray.Length * 4];
|
||||
try
|
||||
{
|
||||
Buffer.BlockCopy(uintArray, 0, byteArray, 0, uintArray.Length * 4);
|
||||
return byteArray;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成二进制字节
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的二进制字节数组</returns>
|
||||
public static Result<byte[]> MultiBitsToBytes(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
return NumberToBytes(MultiBitsToNumber(bits1, bits1Len, bits2, bits2Len).Value,
|
||||
(bits1Len + bits2Len) % 8 != 0 ? (bits1Len + bits2Len) / 8 + 1 : (bits1Len + bits2Len) / 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成整型
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的整型值</returns>
|
||||
public static Result<ulong> MultiBitsToNumber(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
|
||||
|
||||
ulong num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成整型
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的整型值</returns>
|
||||
public static Result<uint> MultiBitsToNumber(uint bits1, uint bits1Len, uint bits2, uint bits2Len)
|
||||
{
|
||||
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
|
||||
|
||||
uint num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特位检查
|
||||
/// </summary>
|
||||
/// <param name="srcBits">源比特值</param>
|
||||
/// <param name="dstBits">目标比特值</param>
|
||||
/// <param name="mask">掩码(默认为全1)</param>
|
||||
/// <returns>检查结果(是否匹配)</returns>
|
||||
public static bool BitsCheck(ulong srcBits, ulong dstBits, ulong mask = 0xFFFF_FFFF_FFFF_FFFF)
|
||||
{
|
||||
return (srcBits & mask) == dstBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特位检查
|
||||
/// </summary>
|
||||
/// <param name="srcBits">源比特值</param>
|
||||
/// <param name="dstBits">目标比特值</param>
|
||||
/// <param name="mask">掩码(默认为全1)</param>
|
||||
/// <returns>检查结果(是否匹配)</returns>
|
||||
public static bool BitsCheck(uint srcBits, uint dstBits, uint mask = 0xFFFF_FFFF)
|
||||
{
|
||||
return (srcBits & mask) == dstBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取整型对应位置的比特
|
||||
/// </summary>
|
||||
/// <param name="srcBits">整型数字</param>
|
||||
/// <param name="location">位置</param>
|
||||
/// <returns>比特</returns>
|
||||
public static Result<bool> ToBit(UInt32 srcBits, int location)
|
||||
{
|
||||
if (location < 0)
|
||||
return new(new ArgumentException(
|
||||
"Location can't be negetive", nameof(location)));
|
||||
|
||||
return ((srcBits >> location) & ((UInt32)0b1)) == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将BitArray转化为32bits无符号整型
|
||||
/// </summary>
|
||||
/// <param name="bits">BitArray比特数组</param>
|
||||
/// <returns>32bits无符号整型</returns>
|
||||
public static Result<UInt32> BitsToNumber(BitArray bits)
|
||||
{
|
||||
if (bits.Length > 32)
|
||||
throw new ArgumentException("Argument length shall be at most 32 bits.");
|
||||
|
||||
var array = new UInt32[1];
|
||||
bits.CopyTo(array, 0);
|
||||
return array[0];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 字符串转二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="str">输入的字符串</param>
|
||||
/// <param name="numBase">进制(默认为16进制)</param>
|
||||
/// <returns>转换后的二进制字节数组</returns>
|
||||
public static byte[] StringToBytes(string str, int numBase = 16)
|
||||
{
|
||||
var len = str.Length;
|
||||
var bytesLen = len / 2;
|
||||
var bytes = new byte[bytesLen];
|
||||
|
||||
for (var i = 0; i < bytesLen; i++)
|
||||
{
|
||||
bytes[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节数组中的子数组
|
||||
/// </summary>
|
||||
/// <param name="srcBytes">源字节数组</param>
|
||||
/// <param name="distance">子数组的长度(反转的步长)</param>
|
||||
/// <returns>反转后的字节数组</returns>
|
||||
public static Result<byte[]> ReverseBytes(byte[] srcBytes, int distance)
|
||||
{
|
||||
if (distance <= 0)
|
||||
return new(new ArgumentException("Distance can't be negetive", nameof(distance)));
|
||||
|
||||
var srcBytesLen = srcBytes.Length;
|
||||
if (distance > srcBytesLen)
|
||||
return new(new ArgumentException(
|
||||
"Distance is larger than bytesArray", nameof(distance)));
|
||||
if (srcBytesLen % distance != 0)
|
||||
return new(new ArgumentException(
|
||||
"The length of bytes can't be divided by distance without reminder", nameof(distance)));
|
||||
|
||||
var dstBytes = new byte[srcBytesLen];
|
||||
var buffer = new byte[distance];
|
||||
|
||||
for (int i = 0; i < srcBytesLen; i += distance)
|
||||
{
|
||||
var end = i + distance;
|
||||
buffer = srcBytes[i..end];
|
||||
Array.Reverse(buffer);
|
||||
Array.Copy(buffer, 0, dstBytes, i, distance);
|
||||
}
|
||||
|
||||
return dstBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节内比特顺序(使用查找表的方法)
|
||||
/// </summary>
|
||||
/// <param name="srcByte">字节</param>
|
||||
/// <returns>反转后的字节</returns>
|
||||
public static byte ReverseBits(byte srcByte)
|
||||
{
|
||||
return BitReverseTable[srcByte];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节数组的字节内比特顺序(使用查找表的方法)
|
||||
/// </summary>
|
||||
/// <param name="srcBytes">字节数组</param>
|
||||
/// <returns>反转后的字节字节数组</returns>
|
||||
public static byte[] ReverseBits(byte[] srcBytes)
|
||||
{
|
||||
var bytesLen = srcBytes.Length;
|
||||
var dstBytes = new byte[bytesLen];
|
||||
for (int i = 0; i < bytesLen; i++)
|
||||
{
|
||||
dstBytes[i] = BitReverseTable[srcBytes[i]];
|
||||
}
|
||||
return dstBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字符串处理工具
|
||||
/// </summary>
|
||||
public class String
|
||||
{
|
||||
/// <summary>
|
||||
/// 反转字符串
|
||||
/// </summary>
|
||||
/// <param name="s">输入的字符串</param>
|
||||
/// <returns>反转后的字符串</returns>
|
||||
public static string Reverse(string s)
|
||||
{
|
||||
char[] charArray = s.ToCharArray();
|
||||
Array.Reverse(charArray);
|
||||
return new string(charArray);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像处理工具
|
||||
/// </summary>
|
||||
public class Image
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 RGB565 格式转换为 RGB24 格式
|
||||
/// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节)
|
||||
/// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节)
|
||||
/// </summary>
|
||||
/// <param name="rgb565Data">RGB565格式的原始数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>RGB24格式的转换后数据</returns>
|
||||
public static Result<byte[]> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB24 格式转换为 RGB565 格式
|
||||
/// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节)
|
||||
/// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节)
|
||||
/// </summary>
|
||||
/// <param name="rgb24Data">RGB24格式的原始数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>RGB565格式的转换后数据</returns>
|
||||
public static Result<byte[]> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB24 数据转换为 JPEG 格式
|
||||
/// </summary>
|
||||
/// <param name="rgb24Data">RGB24格式的图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="quality">JPEG质量(1-100,默认80)</param>
|
||||
/// <returns>JPEG格式的字节数组</returns>
|
||||
public static Result<byte[]> 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<Rgb24>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB565 数据直接转换为 JPEG 格式
|
||||
/// </summary>
|
||||
/// <param name="rgb565Data">RGB565格式的图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="quality">JPEG质量(1-100,默认80)</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>JPEG格式的字节数组</returns>
|
||||
public static Result<byte[]> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 MJPEG 帧头部
|
||||
/// </summary>
|
||||
/// <param name="frameDataLength">帧数据长度</param>
|
||||
/// <param name="boundary">边界字符串(默认为"--boundary")</param>
|
||||
/// <returns>MJPEG帧头部字节数组</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 MJPEG 帧尾部
|
||||
/// </summary>
|
||||
/// <returns>MJPEG帧尾部字节数组</returns>
|
||||
public static byte[] CreateMjpegFrameFooter()
|
||||
{
|
||||
return Encoding.ASCII.GetBytes("\r\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建完整的 MJPEG 帧数据
|
||||
/// </summary>
|
||||
/// <param name="jpegData">JPEG数据</param>
|
||||
/// <param name="boundary">边界字符串(默认为"--boundary")</param>
|
||||
/// <returns>完整的MJPEG帧数据</returns>
|
||||
public static Result<byte[]> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证图像数据长度是否正确
|
||||
/// </summary>
|
||||
/// <param name="data">图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="bytesPerPixel">每像素字节数</param>
|
||||
/// <returns>验证结果</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取图像格式信息
|
||||
/// </summary>
|
||||
/// <param name="format">图像格式枚举</param>
|
||||
/// <returns>格式信息</returns>
|
||||
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")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像格式枚举
|
||||
/// </summary>
|
||||
public enum ImageFormat
|
||||
{
|
||||
RGB565,
|
||||
RGB24,
|
||||
RGBA32,
|
||||
Grayscale8
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像格式信息
|
||||
/// </summary>
|
||||
public record ImageFormatInfo(string Name, int BytesPerPixel, string Description);
|
||||
}
|
|
@ -0,0 +1,358 @@
|
|||
using System.Text;
|
||||
using DotNext;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Common;
|
||||
|
||||
/// <summary>
|
||||
/// 图像处理工具
|
||||
/// </summary>
|
||||
public class Image
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 RGB565 格式转换为 RGB24 格式
|
||||
/// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节)
|
||||
/// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节)
|
||||
/// </summary>
|
||||
/// <param name="rgb565Data">RGB565格式的原始数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>RGB24格式的转换后数据</returns>
|
||||
public static Result<byte[]> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB24 格式转换为 RGB565 格式
|
||||
/// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节)
|
||||
/// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节)
|
||||
/// </summary>
|
||||
/// <param name="rgb24Data">RGB24格式的原始数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>RGB565格式的转换后数据</returns>
|
||||
public static Result<byte[]> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB24 数据转换为 JPEG 格式
|
||||
/// </summary>
|
||||
/// <param name="rgb24Data">RGB24格式的图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="quality">JPEG质量(1-100,默认80)</param>
|
||||
/// <returns>JPEG格式的字节数组</returns>
|
||||
public static Result<byte[]> 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<Rgb24>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB565 数据直接转换为 JPEG 格式
|
||||
/// </summary>
|
||||
/// <param name="rgb565Data">RGB565格式的图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="quality">JPEG质量(1-100,默认80)</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>JPEG格式的字节数组</returns>
|
||||
public static Result<byte[]> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 MJPEG 帧头部
|
||||
/// </summary>
|
||||
/// <param name="frameDataLength">帧数据长度</param>
|
||||
/// <param name="boundary">边界字符串(默认为"--boundary")</param>
|
||||
/// <returns>MJPEG帧头部字节数组</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 MJPEG 帧尾部
|
||||
/// </summary>
|
||||
/// <returns>MJPEG帧尾部字节数组</returns>
|
||||
public static byte[] CreateMjpegFrameFooter()
|
||||
{
|
||||
return Encoding.ASCII.GetBytes("\r\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建完整的 MJPEG 帧数据
|
||||
/// </summary>
|
||||
/// <param name="jpegData">JPEG数据</param>
|
||||
/// <param name="boundary">边界字符串(默认为"--boundary")</param>
|
||||
/// <returns>完整的MJPEG帧数据</returns>
|
||||
public static Result<byte[]> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证图像数据长度是否正确
|
||||
/// </summary>
|
||||
/// <param name="data">图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="bytesPerPixel">每像素字节数</param>
|
||||
/// <returns>验证结果</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取图像格式信息
|
||||
/// </summary>
|
||||
/// <param name="format">图像格式枚举</param>
|
||||
/// <returns>格式信息</returns>
|
||||
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")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像格式枚举
|
||||
/// </summary>
|
||||
public enum ImageFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// RGB565
|
||||
/// </summary>
|
||||
RGB565,
|
||||
|
||||
/// <summary>
|
||||
/// RGB888 / RGB24
|
||||
/// </summary>
|
||||
RGB24,
|
||||
|
||||
/// <summary>
|
||||
/// RGBA8888 / RGBA32
|
||||
/// </summary>
|
||||
RGBA32,
|
||||
|
||||
/// <summary>
|
||||
/// 灰度图
|
||||
/// </summary>
|
||||
Grayscale8
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像格式信息
|
||||
/// </summary>
|
||||
public record ImageFormatInfo(string Name, int BytesPerPixel, string Description);
|
|
@ -0,0 +1,370 @@
|
|||
using System.Collections;
|
||||
using DotNext;
|
||||
|
||||
namespace Common;
|
||||
/// <summary>
|
||||
/// 数字处理工具
|
||||
/// </summary>
|
||||
public class Number
|
||||
{
|
||||
private static readonly byte[] BitReverseTable = new byte[] {
|
||||
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
|
||||
0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
|
||||
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
|
||||
0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
|
||||
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
|
||||
0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
|
||||
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
|
||||
0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
|
||||
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
|
||||
0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
|
||||
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
|
||||
0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
|
||||
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
|
||||
0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
|
||||
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
|
||||
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
|
||||
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
|
||||
0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
|
||||
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
|
||||
0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
|
||||
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
|
||||
0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
|
||||
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
|
||||
0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
|
||||
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
|
||||
0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
|
||||
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
|
||||
0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
|
||||
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
|
||||
0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
|
||||
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
|
||||
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 整数转成二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="num">整数</param>
|
||||
/// <param name="length">整数长度</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>二进制字节数组</returns>
|
||||
public static Result<byte[]> NumberToBytes(ulong num, uint length, bool isLowNumHigh = false)
|
||||
{
|
||||
if (length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(length)
|
||||
));
|
||||
}
|
||||
|
||||
var arr = new byte[length];
|
||||
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
arr[length - 1 - i] = Convert.ToByte((num >> (i << 3)) & (0xFF));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
arr[i] = Convert.ToByte((num >> ((int)(length - 1 - i) << 3)) & (0xFF));
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二进制字节数组转成64bits整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<UInt64> BytesToUInt64(byte[] bytes, bool isLowNumHigh = false)
|
||||
{
|
||||
if (bytes.Length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
UInt64 num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
try
|
||||
{
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt64((UInt64)bytes[len - 1 - i] << (i << 3));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt64((UInt64)bytes[i] << ((int)(len - 1 - i) << 3));
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二进制字节数组转成32bits整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<UInt32> BytesToUInt32(byte[] bytes, bool isLowNumHigh = false)
|
||||
{
|
||||
if (bytes.Length > 4)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 4 bytes(32 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
UInt32 num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
try
|
||||
{
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt32((UInt32)bytes[len - 1 - i] << (i << 3));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt32((UInt32)bytes[i] << ((int)(len - 1 - i) << 3));
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="uintArray">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static Result<byte[]> UInt32ArrayToBytes(UInt32[] uintArray)
|
||||
{
|
||||
byte[] byteArray = new byte[uintArray.Length * 4];
|
||||
try
|
||||
{
|
||||
Buffer.BlockCopy(uintArray, 0, byteArray, 0, uintArray.Length * 4);
|
||||
return byteArray;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成二进制字节
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的二进制字节数组</returns>
|
||||
public static Result<byte[]> MultiBitsToBytes(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
return NumberToBytes(MultiBitsToNumber(bits1, bits1Len, bits2, bits2Len).Value,
|
||||
(bits1Len + bits2Len) % 8 != 0 ? (bits1Len + bits2Len) / 8 + 1 : (bits1Len + bits2Len) / 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成整型
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的整型值</returns>
|
||||
public static Result<ulong> MultiBitsToNumber(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
|
||||
|
||||
ulong num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成整型
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的整型值</returns>
|
||||
public static Result<uint> MultiBitsToNumber(uint bits1, uint bits1Len, uint bits2, uint bits2Len)
|
||||
{
|
||||
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
|
||||
|
||||
uint num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特位检查
|
||||
/// </summary>
|
||||
/// <param name="srcBits">源比特值</param>
|
||||
/// <param name="dstBits">目标比特值</param>
|
||||
/// <param name="mask">掩码(默认为全1)</param>
|
||||
/// <returns>检查结果(是否匹配)</returns>
|
||||
public static bool BitsCheck(ulong srcBits, ulong dstBits, ulong mask = 0xFFFF_FFFF_FFFF_FFFF)
|
||||
{
|
||||
return (srcBits & mask) == dstBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特位检查
|
||||
/// </summary>
|
||||
/// <param name="srcBits">源比特值</param>
|
||||
/// <param name="dstBits">目标比特值</param>
|
||||
/// <param name="mask">掩码(默认为全1)</param>
|
||||
/// <returns>检查结果(是否匹配)</returns>
|
||||
public static bool BitsCheck(uint srcBits, uint dstBits, uint mask = 0xFFFF_FFFF)
|
||||
{
|
||||
return (srcBits & mask) == dstBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取整型对应位置的比特
|
||||
/// </summary>
|
||||
/// <param name="srcBits">整型数字</param>
|
||||
/// <param name="location">位置</param>
|
||||
/// <returns>比特</returns>
|
||||
public static Result<bool> ToBit(UInt32 srcBits, int location)
|
||||
{
|
||||
if (location < 0)
|
||||
return new(new ArgumentException(
|
||||
"Location can't be negetive", nameof(location)));
|
||||
|
||||
return ((srcBits >> location) & ((UInt32)0b1)) == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将BitArray转化为32bits无符号整型
|
||||
/// </summary>
|
||||
/// <param name="bits">BitArray比特数组</param>
|
||||
/// <returns>32bits无符号整型</returns>
|
||||
public static Result<UInt32> BitsToNumber(BitArray bits)
|
||||
{
|
||||
if (bits.Length > 32)
|
||||
throw new ArgumentException("Argument length shall be at most 32 bits.");
|
||||
|
||||
var array = new UInt32[1];
|
||||
bits.CopyTo(array, 0);
|
||||
return array[0];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 字符串转二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="str">输入的字符串</param>
|
||||
/// <param name="numBase">进制(默认为16进制)</param>
|
||||
/// <returns>转换后的二进制字节数组</returns>
|
||||
public static byte[] StringToBytes(string str, int numBase = 16)
|
||||
{
|
||||
var len = str.Length;
|
||||
var bytesLen = len / 2;
|
||||
var bytes = new byte[bytesLen];
|
||||
|
||||
for (var i = 0; i < bytesLen; i++)
|
||||
{
|
||||
bytes[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节数组中的子数组
|
||||
/// </summary>
|
||||
/// <param name="srcBytes">源字节数组</param>
|
||||
/// <param name="distance">子数组的长度(反转的步长)</param>
|
||||
/// <returns>反转后的字节数组</returns>
|
||||
public static Result<byte[]> ReverseBytes(byte[] srcBytes, int distance)
|
||||
{
|
||||
if (distance <= 0)
|
||||
return new(new ArgumentException("Distance can't be negetive", nameof(distance)));
|
||||
|
||||
var srcBytesLen = srcBytes.Length;
|
||||
if (distance > srcBytesLen)
|
||||
return new(new ArgumentException(
|
||||
"Distance is larger than bytesArray", nameof(distance)));
|
||||
if (srcBytesLen % distance != 0)
|
||||
return new(new ArgumentException(
|
||||
"The length of bytes can't be divided by distance without reminder", nameof(distance)));
|
||||
|
||||
var dstBytes = new byte[srcBytesLen];
|
||||
var buffer = new byte[distance];
|
||||
|
||||
for (int i = 0; i < srcBytesLen; i += distance)
|
||||
{
|
||||
var end = i + distance;
|
||||
buffer = srcBytes[i..end];
|
||||
Array.Reverse(buffer);
|
||||
Array.Copy(buffer, 0, dstBytes, i, distance);
|
||||
}
|
||||
|
||||
return dstBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节内比特顺序(使用查找表的方法)
|
||||
/// </summary>
|
||||
/// <param name="srcByte">字节</param>
|
||||
/// <returns>反转后的字节</returns>
|
||||
public static byte ReverseBits(byte srcByte)
|
||||
{
|
||||
return BitReverseTable[srcByte];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节数组的字节内比特顺序(使用查找表的方法)
|
||||
/// </summary>
|
||||
/// <param name="srcBytes">字节数组</param>
|
||||
/// <returns>反转后的字节字节数组</returns>
|
||||
public static byte[] ReverseBits(byte[] srcBytes)
|
||||
{
|
||||
var bytesLen = srcBytes.Length;
|
||||
var dstBytes = new byte[bytesLen];
|
||||
for (int i = 0; i < bytesLen; i++)
|
||||
{
|
||||
dstBytes[i] = BitReverseTable[srcBytes[i]];
|
||||
}
|
||||
return dstBytes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
using System.Collections.Concurrent;
|
||||
using DotNext;
|
||||
|
||||
namespace Common;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public class SemaphorePool
|
||||
{
|
||||
private SemaphoreSlim semaphore;
|
||||
private ConcurrentQueue<int> queue;
|
||||
private int beginNum;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public int RemainingCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public int MaxCount { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="initialCount">[TODO:parameter]</param>
|
||||
/// <param name="beginNum">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public SemaphorePool(int initialCount, int beginNum = 0)
|
||||
{
|
||||
semaphore = new SemaphoreSlim(initialCount);
|
||||
queue = new ConcurrentQueue<int>();
|
||||
this.beginNum = beginNum;
|
||||
this.RemainingCount = initialCount;
|
||||
this.MaxCount = initialCount;
|
||||
for (int i = 0; i < initialCount; i++)
|
||||
{
|
||||
queue.Enqueue(beginNum + i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="initialCount">[TODO:parameter]</param>
|
||||
/// <param name="maxCount">[TODO:parameter]</param>
|
||||
/// <param name="beginNum">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public SemaphorePool(int initialCount, int maxCount, int beginNum = 0)
|
||||
{
|
||||
semaphore = new SemaphoreSlim(initialCount, maxCount);
|
||||
queue = new ConcurrentQueue<int>();
|
||||
this.beginNum = beginNum;
|
||||
this.RemainingCount = initialCount;
|
||||
this.MaxCount = maxCount;
|
||||
for (int i = 0; i < initialCount; i++)
|
||||
{
|
||||
queue.Enqueue(beginNum + i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public Result<int> Wait()
|
||||
{
|
||||
semaphore.Wait();
|
||||
|
||||
int pop;
|
||||
if (queue.TryDequeue(out pop))
|
||||
{
|
||||
return pop;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(new Exception($"TODO"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public async ValueTask<Result<int>> WaitAsync()
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
|
||||
int pop;
|
||||
if (queue.TryDequeue(out pop))
|
||||
{
|
||||
return pop;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(new Exception($"TODO"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public void Release()
|
||||
{
|
||||
semaphore.Release();
|
||||
queue.Clear();
|
||||
for (int i = 0; i < MaxCount; i++)
|
||||
{
|
||||
queue.Enqueue(beginNum + i);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
namespace Common;
|
||||
|
||||
/// <summary>
|
||||
/// 字符串处理工具
|
||||
/// </summary>
|
||||
public class String
|
||||
{
|
||||
/// <summary>
|
||||
/// 反转字符串
|
||||
/// </summary>
|
||||
/// <param name="s">输入的字符串</param>
|
||||
/// <returns>反转后的字符串</returns>
|
||||
public static string Reverse(string s)
|
||||
{
|
||||
char[] charArray = s.ToCharArray();
|
||||
Array.Reverse(charArray);
|
||||
return new string(charArray);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,11 @@ public class TutorialController : ControllerBase
|
|||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="environment">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public TutorialController(IWebHostEnvironment environment)
|
||||
{
|
||||
_environment = environment;
|
||||
|
|
|
@ -109,6 +109,7 @@ public class UDPController : ControllerBase
|
|||
/// 获取指定IP地址接收的数据列表
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
[HttpGet("GetRecvDataArray")]
|
||||
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// 视频流控制器,支持动态配置摄像头连接
|
||||
|
@ -17,10 +17,16 @@ public class VideoStreamController : ControllerBase
|
|||
/// </summary>
|
||||
public class CameraConfigRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 摄像头地址
|
||||
/// </summary>
|
||||
[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; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头端口
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")]
|
||||
public int Port { get; set; }
|
||||
|
@ -54,21 +60,11 @@ public class VideoStreamController : ControllerBase
|
|||
var status = _videoStreamService.GetServiceStatus();
|
||||
|
||||
// 转换为小写首字母的JSON属性(符合前端惯例)
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
isRunning = true, // HTTP视频流服务作为后台服务始终运行
|
||||
serverPort = _videoStreamService.ServerPort,
|
||||
streamUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
|
||||
mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
|
||||
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
|
||||
connectedClients = _videoStreamService.ConnectedClientsCount,
|
||||
clientEndpoints = _videoStreamService.GetConnectedClientEndpoints(),
|
||||
cameraStatus = _videoStreamService.GetCameraStatus()
|
||||
});
|
||||
return TypedResults.Ok(status);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取 HTTP 视频流服务状态失败");
|
||||
logger.Error(ex, "获取 HTTP 视频流服务状态失败");
|
||||
return TypedResults.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
@ -95,13 +91,11 @@ public class VideoStreamController : ControllerBase
|
|||
htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
|
||||
mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
|
||||
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
|
||||
cameraAddress = _videoStreamService.CameraAddress,
|
||||
cameraPort = _videoStreamService.CameraPort
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取 HTTP 视频流信息失败");
|
||||
logger.Error(ex, "获取 HTTP 视频流信息失败");
|
||||
return TypedResults.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +117,7 @@ public class VideoStreamController : ControllerBase
|
|||
logger.Info("配置摄像头连接: {Address}:{Port}", config.Address, config.Port);
|
||||
|
||||
var success = await _videoStreamService.ConfigureCameraAsync(config.Address, config.Port);
|
||||
|
||||
|
||||
if (success)
|
||||
{
|
||||
return TypedResults.Ok(new
|
||||
|
@ -166,7 +160,7 @@ public class VideoStreamController : ControllerBase
|
|||
{
|
||||
logger.Info("获取摄像头配置");
|
||||
var cameraStatus = _videoStreamService.GetCameraStatus();
|
||||
|
||||
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
address = _videoStreamService.CameraAddress,
|
||||
|
@ -183,35 +177,23 @@ public class VideoStreamController : ControllerBase
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试摄像头连接
|
||||
/// 控制 HTTP 视频流服务开关
|
||||
/// </summary>
|
||||
/// <returns>连接测试结果</returns>
|
||||
[HttpPost("TestCameraConnection")]
|
||||
/// <param name="enabled">是否启用服务</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetEnabled")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> TestCameraConnection()
|
||||
public IResult SetEnabled([FromQuery] bool enabled)
|
||||
{
|
||||
try
|
||||
logger.Info("设置视频流服务开关: {Enabled}", enabled);
|
||||
_videoStreamService.Enabled = enabled;
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
logger.Info("测试摄像头连接");
|
||||
|
||||
var (isSuccess, message) = await _videoStreamService.TestCameraConnectionAsync();
|
||||
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = isSuccess,
|
||||
message = message,
|
||||
cameraAddress = _videoStreamService.CameraAddress,
|
||||
cameraPort = _videoStreamService.CameraPort,
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "测试摄像头连接失败");
|
||||
return TypedResults.InternalServerError(ex.Message);
|
||||
}
|
||||
success = true,
|
||||
enabled = _videoStreamService.Enabled
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -229,16 +211,29 @@ public class VideoStreamController : ControllerBase
|
|||
logger.Info("测试 HTTP 视频流连接");
|
||||
|
||||
// 尝试通过HTTP请求检查视频流服务是否可访问
|
||||
bool isConnected = false;
|
||||
using (var httpClient = new HttpClient())
|
||||
{
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间
|
||||
var response = await httpClient.GetAsync($"http://localhost:{_videoStreamService.ServerPort}/");
|
||||
|
||||
// 只要能连接上就认为成功,不管返回状态
|
||||
bool isConnected = response.IsSuccessStatusCode;
|
||||
|
||||
return TypedResults.Ok(isConnected);
|
||||
isConnected = response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
logger.Info("测试摄像头连接");
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -35,6 +35,25 @@ class Camera
|
|||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public async ValueTask<Result<bool>> Init()
|
||||
{
|
||||
var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.timeout);
|
||||
var ret = await i2c.WriteData(0x78, new byte[] { 0x30, 0x08, 0x02 }, Peripherals.I2cClient.I2cProtocol.I2c);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"I2C write failed during camera initialization for {this.address}:{this.port}, error: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error($"I2C write returned unsuccessful result during camera initialization for {this.address}:{this.port}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取一帧图像数据
|
||||
/// </summary>
|
||||
|
@ -47,7 +66,7 @@ class Camera
|
|||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
|
||||
// 使用UDPClientPool读取图像帧数据
|
||||
var result = await UDPClientPool.ReadAddrBytes(
|
||||
var result = await UDPClientPool.ReadAddr4Bytes(
|
||||
this.ep,
|
||||
2, // taskID
|
||||
CameraAddr.Base,
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.I2cClient;
|
||||
|
||||
static class I2cAddr
|
||||
{
|
||||
|
||||
const UInt32 Base = 0x6000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0000:
|
||||
/// [7:0] 本次传输的i2c地址(最高位总为0);
|
||||
/// [8] 1为读,0为写;
|
||||
/// [16] 1为SCCB协议,0为I2C协议;
|
||||
/// [24] 1为开启本次传输,自动置零
|
||||
/// </summary>
|
||||
public const UInt32 BaseConfig = Base + 0x0000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0001:
|
||||
/// [15:0] 本次传输的数据量(以字节为单位,0为传1个字节);
|
||||
/// [31:16] 若本次传输为读的DUMMY数据量(字节为单位,0为传1个字节)
|
||||
/// </summary>
|
||||
public const UInt32 TranConfig = Base + 0x0000_0001;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0002: [0] cmd_done; [8] cmd_error;
|
||||
/// </summary>
|
||||
public const UInt32 Flag = Base + 0x0000_0002;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0003: FIFO写入口,仅低8位有效,只写
|
||||
/// </summary>
|
||||
public const UInt32 Write = Base + 0x0000_0003;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0004: FIFO读出口,仅低8位有效,只读
|
||||
/// </summary>
|
||||
public const UInt32 Read = Base + 0x0000_0003;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0005: [0] FIFO写入口清空;[8] FIFO读出口清空;
|
||||
/// </summary>
|
||||
public const UInt32 Clear = Base + 0x0000_0003;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:Enum]
|
||||
/// </summary>
|
||||
public enum I2cProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// [TODO:Enum]
|
||||
/// </summary>
|
||||
I2c = 0,
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:Enum]
|
||||
/// </summary>
|
||||
SCCB = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public class I2c
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 2000;
|
||||
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public I2c(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向指定I2C设备写入数据
|
||||
/// </summary>
|
||||
/// <param name="devAddr">I2C设备地址</param>
|
||||
/// <param name="data">要写入的数据</param>
|
||||
/// <param name="proto">I2C协议类型</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> WriteData(UInt32 devAddr, byte[] data, I2cProtocol proto)
|
||||
{
|
||||
if (data.Length > 0x0000_FFFF)
|
||||
{
|
||||
logger.Error($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF");
|
||||
return new(new ArgumentException($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF"));
|
||||
}
|
||||
|
||||
// 清除UDP服务器接收缓冲区
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address, 2);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
|
||||
// 写入数据到I2C FIFO写入口
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 2, I2cAddr.Write, data);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to write data to I2C FIFO: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to I2C FIFO returned false");
|
||||
return new(new Exception("Failed to write data to I2C FIFO"));
|
||||
}
|
||||
}
|
||||
|
||||
// 配置本次传输数据量
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 2, I2cAddr.TranConfig, ((uint)(data.Length - 1)));
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to configure transfer length: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to TranConfig returned false");
|
||||
return new(new Exception("Failed to configure transfer length"));
|
||||
}
|
||||
}
|
||||
|
||||
// 配置I2C地址、协议及启动传输
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, 2, I2cAddr.BaseConfig, (devAddr << 1) | (((uint)proto) << 16) | (1 << 24));
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to configure I2C address/protocol/start: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to BaseConfig returned false");
|
||||
return new(new Exception("Failed to configure I2C address/protocol/start"));
|
||||
}
|
||||
}
|
||||
|
||||
// 等待I2C命令完成
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 2, I2cAddr.Flag, 0x0000_0001, 0xFFFF_FFFF);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to wait for I2C command completion: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("ReadAddrWithWait for I2C command completion returned false");
|
||||
return new(new Exception("I2C command did not complete successfully"));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定I2C设备读取数据
|
||||
/// </summary>
|
||||
/// <param name="devAddr">I2C设备地址</param>
|
||||
/// <param name="length">要读取的数据长度</param>
|
||||
/// <param name="proto">I2C协议类型</param>
|
||||
/// <returns>操作结果,成功返回读取到的数据,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<byte[]>> ReadData(UInt32 devAddr, int length, I2cProtocol proto)
|
||||
{
|
||||
if (length <= 0 || length > 0x0000_FFFF)
|
||||
{
|
||||
logger.Error($"Read length {length} is invalid or exceeds maximum allowed 0x0000_FFFF");
|
||||
return new(new ArgumentException($"Read length {length} is invalid or exceeds maximum allowed 0x0000_FFFF"));
|
||||
}
|
||||
|
||||
// 清除UDP服务器接收缓冲区
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address, 2);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
|
||||
// 配置本次传输数据量
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 2, I2cAddr.TranConfig, ((uint)(length - 1)));
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to configure transfer length: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to TranConfig returned false");
|
||||
return new(new Exception("Failed to configure transfer length"));
|
||||
}
|
||||
}
|
||||
|
||||
// 配置I2C地址、协议及启动传输(读操作)
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, 2, I2cAddr.BaseConfig, (devAddr << 1) | (1 << 8) | (((uint)proto) << 16) | (1 << 24));
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to configure I2C address/protocol/start: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to BaseConfig returned false");
|
||||
return new(new Exception("Failed to configure I2C address/protocol/start"));
|
||||
}
|
||||
}
|
||||
|
||||
// 等待I2C命令完成
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 2, I2cAddr.Flag, 0x0000_0001, 0xFFFF_FFFF);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to wait for I2C command completion: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("ReadAddrWithWait for I2C command completion returned false");
|
||||
return new(new Exception("I2C command did not complete successfully"));
|
||||
}
|
||||
}
|
||||
|
||||
// 读取数据
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, 2, I2cAddr.Read, length);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read data from I2C FIFO: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length != length)
|
||||
{
|
||||
logger.Error($"ReadAddr returned unexpected data length: {ret.Value.Options.Data?.Length ?? 0}");
|
||||
return new(new Exception("Failed to read expected amount of data from I2C FIFO"));
|
||||
}
|
||||
|
||||
return ret.Value.Options.Data;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ using System.Collections;
|
|||
using System.Net;
|
||||
using DotNext;
|
||||
using Newtonsoft.Json;
|
||||
using server;
|
||||
using WebProtocol;
|
||||
|
||||
namespace Peripherals.JtagClient;
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.OscilloscopeClient;
|
||||
|
||||
static class OscilloscopeAddr
|
||||
{
|
||||
public const UInt32 Base = 0x0000_0000;
|
||||
}
|
||||
|
||||
class Oscilloscope
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 2000;
|
||||
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化示波器客户端
|
||||
/// </summary>
|
||||
/// <param name="address">示波器设备IP地址</param>
|
||||
/// <param name="port">示波器设备端口</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
public Oscilloscope(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;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,73 @@ using Peripherals.CameraClient; // 添加摄像头客户端引用
|
|||
|
||||
namespace server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 表示摄像头连接状态信息
|
||||
/// </summary>
|
||||
public class CameraStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 摄像头的IP地址
|
||||
/// </summary>
|
||||
public string Address { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头的端口号
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已配置摄像头
|
||||
/// </summary>
|
||||
public bool IsConfigured { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头连接字符串(IP:端口)
|
||||
/// </summary>
|
||||
public string ConnectionString { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示视频流服务的运行状态
|
||||
/// </summary>
|
||||
public class ServiceStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务是否正在运行
|
||||
/// </summary>
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务监听的端口号
|
||||
/// </summary>
|
||||
public int ServerPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 视频流的帧率(FPS)
|
||||
/// </summary>
|
||||
public int FrameRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 视频分辨率(如 640x480)
|
||||
/// </summary>
|
||||
public string Resolution { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 当前连接的客户端数量
|
||||
/// </summary>
|
||||
public int ConnectedClients { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前连接的客户端端点列表
|
||||
/// </summary>
|
||||
public List<string> ClientEndpoints { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头连接状态信息
|
||||
/// </summary>
|
||||
public CameraStatus CameraStatus { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
|
||||
/// 支持动态配置摄像头地址和端口
|
||||
|
@ -28,6 +95,11 @@ public class HttpVideoStreamService : BackgroundService
|
|||
private readonly List<HttpListenerResponse> _activeClients = new List<HttpListenerResponse>();
|
||||
private readonly object _clientsLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 获取 / 设置视频流服务是否启用
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前连接的客户端数量
|
||||
/// </summary>
|
||||
|
@ -94,7 +166,7 @@ public class HttpVideoStreamService : BackgroundService
|
|||
|
||||
try
|
||||
{
|
||||
await Task.Run(() =>
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
lock (_cameraLock)
|
||||
{
|
||||
|
@ -122,6 +194,22 @@ public class HttpVideoStreamService : BackgroundService
|
|||
|
||||
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;
|
||||
}
|
||||
|
@ -176,18 +264,15 @@ public class HttpVideoStreamService : BackgroundService
|
|||
/// 获取摄像头连接状态
|
||||
/// </summary>
|
||||
/// <returns>连接状态信息</returns>
|
||||
public object GetCameraStatus()
|
||||
public CameraStatus GetCameraStatus()
|
||||
{
|
||||
lock (_cameraLock)
|
||||
return new CameraStatus
|
||||
{
|
||||
return new
|
||||
{
|
||||
Address = _cameraAddress,
|
||||
Port = _cameraPort,
|
||||
IsConfigured = _camera != null,
|
||||
ConnectionString = $"{_cameraAddress}:{_cameraPort}"
|
||||
};
|
||||
}
|
||||
Address = _cameraAddress,
|
||||
Port = _cameraPort,
|
||||
IsConfigured = _camera != null,
|
||||
ConnectionString = $"{_cameraAddress}:{_cameraPort}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -215,7 +300,17 @@ public class HttpVideoStreamService : BackgroundService
|
|||
_ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken);
|
||||
|
||||
// 开始生成视频帧
|
||||
await GenerateVideoFrames(stoppingToken);
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
await GenerateVideoFrames(stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(500, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpListenerException ex)
|
||||
{
|
||||
|
@ -660,13 +755,13 @@ public class HttpVideoStreamService : BackgroundService
|
|||
/// <summary>
|
||||
/// 获取服务状态信息
|
||||
/// </summary>
|
||||
public object GetServiceStatus()
|
||||
public ServiceStatus GetServiceStatus()
|
||||
{
|
||||
var cameraStatus = GetCameraStatus();
|
||||
|
||||
return new
|
||||
return new ServiceStatus
|
||||
{
|
||||
IsRunning = _httpListener?.IsListening ?? false,
|
||||
IsRunning = (_httpListener?.IsListening ?? false) && Enabled,
|
||||
ServerPort = _serverPort,
|
||||
FrameRate = _frameRate,
|
||||
Resolution = $"{_frameWidth}x{_frameHeight}",
|
||||
|
@ -683,6 +778,8 @@ public class HttpVideoStreamService : BackgroundService
|
|||
{
|
||||
logger.Info("正在停止 HTTP 视频流服务...");
|
||||
|
||||
Enabled = false;
|
||||
|
||||
if (_httpListener != null && _httpListener.IsListening)
|
||||
{
|
||||
_httpListener.Stop();
|
||||
|
|
|
@ -311,10 +311,10 @@ public class UDPClientPool
|
|||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="dataLength">要读取的数据长度(字节)</param>
|
||||
/// <param name="dataLength">要读取的数据长度(4字节)</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>读取结果,包含接收到的字节数组</returns>
|
||||
public static async ValueTask<Result<byte[]>> ReadAddrBytes(
|
||||
public static async ValueTask<Result<byte[]>> ReadAddr4Bytes(
|
||||
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000)
|
||||
{
|
||||
var ret = false;
|
||||
|
@ -330,7 +330,7 @@ public class UDPClientPool
|
|||
return new(new Exception("Message bus not working!"));
|
||||
|
||||
// Calculate read times and segments
|
||||
var max4BytesPerRead = 0x80; // 1024 bytes per read
|
||||
var max4BytesPerRead = 0x80; // 512 bytes per read
|
||||
var rest4Bytes = dataLength % max4BytesPerRead;
|
||||
var readTimes = (rest4Bytes != 0) ?
|
||||
(dataLength / max4BytesPerRead + 1) :
|
||||
|
|
|
@ -72,7 +72,9 @@ public class UDPServer
|
|||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private static Dictionary<string, Queue<UDPData>> udpData = new Dictionary<string, Queue<UDPData>>();
|
||||
private Dictionary<string, Queue<UDPData>> udpData = new Dictionary<string, Queue<UDPData>>();
|
||||
|
||||
private Semaphore taskPool = new Semaphore(3, 3);
|
||||
|
||||
private int listenPort;
|
||||
private UdpClient listener;
|
||||
|
|
|
@ -216,11 +216,16 @@ export class VideoStreamClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* 测试摄像头连接
|
||||
* @return 连接测试结果
|
||||
* 控制 HTTP 视频流服务开关
|
||||
* @param enabled (optional) 是否启用服务
|
||||
* @return 操作结果
|
||||
*/
|
||||
testCameraConnection(): Promise<any> {
|
||||
let url_ = this.baseUrl + "/api/VideoStream/TestCameraConnection";
|
||||
setEnabled(enabled: boolean | undefined): Promise<any> {
|
||||
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_: RequestInit = {
|
||||
|
@ -231,11 +236,11 @@ export class VideoStreamClient {
|
|||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processTestCameraConnection(_response);
|
||||
return this.processSetEnabled(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processTestCameraConnection(response: Response): Promise<any> {
|
||||
protected processSetEnabled(response: Response): Promise<any> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
|
@ -2541,4 +2546,4 @@ function throwException(message: string, status: number, response: string, heade
|
|||
throw result;
|
||||
else
|
||||
throw new ApiException(message, status, response, headers, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,69 +149,40 @@
|
|||
</svg>
|
||||
摄像头配置
|
||||
</h3>
|
||||
<div class="flex flex-row justify-between items-center gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">IP地址</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="cameraConfig.address"
|
||||
placeholder="例如: 192.168.1.100"
|
||||
class="input input-bordered input-sm"
|
||||
|
||||
<div class="flex flex-row justify-around gap-4">
|
||||
<div class="grow">
|
||||
<IpInputField
|
||||
v-model="tempCameraConfig.address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">端口号</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model="cameraConfig.port"
|
||||
placeholder="例如: 8080"
|
||||
class="input input-bordered input-sm"
|
||||
|
||||
<div class="grow">
|
||||
<PortInputField
|
||||
v-model="tempCameraConfig.port"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="confirmCameraConfig"
|
||||
:disabled="configuring"
|
||||
class="btn btn-ghost"
|
||||
@click="resetCameraConfig"
|
||||
:disabled="isDefaultCamera"
|
||||
>
|
||||
<svg
|
||||
v-if="configuring"
|
||||
class="animate-spin h-4 w-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{{ configuring ? "配置中..." : "确认" }}
|
||||
<RotateCcw class="w-4 h-4" />
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="confirmCameraConfig"
|
||||
:disabled="!isValidCameraConfig || !hasChangesCamera"
|
||||
:class="{ loading: configuring }"
|
||||
>
|
||||
<Save class="w-4 h-4" v-if="!configuring" />
|
||||
{{ configuring ? "配置中..." : "保存配置" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -540,8 +511,15 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { ref, computed, reactive, watch, onMounted, onUnmounted } from "vue";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Save,
|
||||
RotateCcw,
|
||||
} from "lucide-vue-next";
|
||||
import { VideoStreamClient, CameraConfigRequest } from "@/APIClient";
|
||||
import { IpInputField, PortInputField } from "@/components/InputField";
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false);
|
||||
|
@ -573,12 +551,92 @@ const streamInfo = ref({
|
|||
snapshotUrl: "",
|
||||
});
|
||||
|
||||
// 摄像头配置
|
||||
const cameraConfig = ref({
|
||||
// 摄像头配置类型定义
|
||||
const cameraConfigSchema = z.object({
|
||||
address: z
|
||||
.string()
|
||||
.ip({ version: "v4", message: "请输入有效的IPv4地址" })
|
||||
.min(1, "请输入IP地址"),
|
||||
port: z
|
||||
.number()
|
||||
.int("端口必须是整数")
|
||||
.min(1, "端口必须大于0")
|
||||
.max(65535, "端口必须小于等于65535"),
|
||||
});
|
||||
|
||||
type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||
|
||||
// 默认摄像头配置
|
||||
const defaultCameraConfig: CameraConfig = {
|
||||
address: "192.168.1.100",
|
||||
port: 8080,
|
||||
};
|
||||
|
||||
// 使用 VueUse 存储摄像头配置
|
||||
const cameraConfig = useStorage<CameraConfig>(
|
||||
"camera-config",
|
||||
defaultCameraConfig,
|
||||
localStorage,
|
||||
{
|
||||
serializer: {
|
||||
read: (value: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
const result = cameraConfigSchema.safeParse(parsed);
|
||||
return result.success ? result.data : defaultCameraConfig;
|
||||
} catch {
|
||||
return defaultCameraConfig;
|
||||
}
|
||||
},
|
||||
write: (value: CameraConfig) => JSON.stringify(value),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// 临时摄像头配置(用于编辑)
|
||||
const tempCameraConfig = reactive<CameraConfig>({
|
||||
address: cameraConfig.value.address,
|
||||
port: cameraConfig.value.port,
|
||||
});
|
||||
|
||||
// 摄像头配置验证
|
||||
const isValidCameraConfig = computed(() => {
|
||||
return tempCameraConfig.address && tempCameraConfig.port &&
|
||||
tempCameraConfig.port >= 1 && tempCameraConfig.port <= 65535 &&
|
||||
/^(\d{1,3}\.){3}\d{1,3}$/.test(tempCameraConfig.address);
|
||||
});
|
||||
|
||||
// 检查摄像头配置是否有更改
|
||||
const hasChangesCamera = computed(() => {
|
||||
return (
|
||||
tempCameraConfig.address !== cameraConfig.value.address ||
|
||||
tempCameraConfig.port !== cameraConfig.value.port
|
||||
);
|
||||
});
|
||||
|
||||
const isDefaultCamera = computed(() => {
|
||||
return (
|
||||
defaultCameraConfig.address === tempCameraConfig.address &&
|
||||
defaultCameraConfig.port === tempCameraConfig.port
|
||||
);
|
||||
});
|
||||
|
||||
// 重置摄像头配置
|
||||
const resetCameraConfig = () => {
|
||||
tempCameraConfig.address = defaultCameraConfig.address;
|
||||
tempCameraConfig.port = defaultCameraConfig.port;
|
||||
};
|
||||
|
||||
// 监听存储的摄像头配置变化,同步到临时配置
|
||||
watch(
|
||||
cameraConfig,
|
||||
(newConfig) => {
|
||||
tempCameraConfig.address = newConfig.address;
|
||||
tempCameraConfig.port = newConfig.port;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const currentVideoSource = ref("");
|
||||
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
|
||||
|
||||
|
@ -605,8 +663,11 @@ const loadCameraConfig = async () => {
|
|||
|
||||
const config = await videoClient.getCameraConfig();
|
||||
if (config && config.address && config.port) {
|
||||
cameraConfig.value.address = config.address;
|
||||
cameraConfig.value.port = config.port;
|
||||
// 更新存储的配置
|
||||
cameraConfig.value = {
|
||||
address: config.address,
|
||||
port: config.port,
|
||||
};
|
||||
addLog("success", `摄像头配置加载成功: ${config.address}:${config.port}`);
|
||||
} else {
|
||||
addLog("warning", "未找到保存的摄像头配置,使用默认值");
|
||||
|
@ -619,20 +680,30 @@ const loadCameraConfig = async () => {
|
|||
|
||||
// 确认摄像头配置
|
||||
const confirmCameraConfig = async () => {
|
||||
if (!cameraConfig.value.address || !cameraConfig.value.port) {
|
||||
addLog("error", "请填写完整的摄像头IP地址和端口号");
|
||||
return;
|
||||
}
|
||||
if (!isValidCameraConfig.value) return;
|
||||
|
||||
configuring.value = true;
|
||||
try {
|
||||
addLog(
|
||||
"info",
|
||||
`正在配置摄像头: ${cameraConfig.value.address}:${cameraConfig.value.port}`,
|
||||
`正在配置摄像头: ${tempCameraConfig.address}:${tempCameraConfig.port}`,
|
||||
);
|
||||
|
||||
// 模拟保存延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// 保存配置
|
||||
cameraConfig.value = {
|
||||
address: tempCameraConfig.address,
|
||||
port: tempCameraConfig.port,
|
||||
};
|
||||
|
||||
// 使用新配置调用API
|
||||
const result = await videoClient.configureCamera(
|
||||
CameraConfigRequest.fromJS(cameraConfig.value),
|
||||
CameraConfigRequest.fromJS({
|
||||
address: tempCameraConfig.address,
|
||||
port: tempCameraConfig.port,
|
||||
}),
|
||||
);
|
||||
|
||||
if (result) {
|
||||
|
@ -650,35 +721,6 @@ const confirmCameraConfig = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// 测试摄像头连接
|
||||
const testCameraConnection = async () => {
|
||||
if (!cameraConfig.value.address || !cameraConfig.value.port) {
|
||||
addLog("error", "请先配置摄像头IP地址和端口号");
|
||||
return;
|
||||
}
|
||||
|
||||
testingCamera.value = true;
|
||||
try {
|
||||
addLog(
|
||||
"info",
|
||||
`正在测试摄像头连接: ${cameraConfig.value.address}:${cameraConfig.value.port}`,
|
||||
);
|
||||
|
||||
const result = await videoClient.testCameraConnection();
|
||||
|
||||
if (result && result.success) {
|
||||
addLog("success", "摄像头连接测试成功");
|
||||
} else {
|
||||
addLog("error", `摄像头连接测试失败: ${result?.message || "未知错误"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
addLog("error", `摄像头连接测试失败: ${error}`);
|
||||
console.error("摄像头连接测试失败:", error);
|
||||
} finally {
|
||||
testingCamera.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: Date) => {
|
||||
return time.toLocaleTimeString();
|
||||
|
@ -823,6 +865,7 @@ const startStream = async () => {
|
|||
try {
|
||||
addLog("info", "正在启动视频流...");
|
||||
videoStatus.value = "正在连接视频流...";
|
||||
videoClient.setEnabled(true);
|
||||
|
||||
// 刷新状态
|
||||
await refreshStatus();
|
||||
|
@ -846,6 +889,7 @@ const startStream = async () => {
|
|||
const stopStream = () => {
|
||||
try {
|
||||
addLog("info", "正在停止视频流...");
|
||||
videoClient.setEnabled(false);
|
||||
|
||||
// 清除视频源
|
||||
currentVideoSource.value = "";
|
||||
|
|
Loading…
Reference in New Issue