feat&fix: 完善JPEGClient逻辑,前端新增编码器组件
This commit is contained in:
		@@ -258,12 +258,12 @@ public class Image
 | 
				
			|||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// 将原始 JPEG 数据补全 JPEG 头部,生成完整的 JPEG 图片
 | 
					    /// 将原始 JPEG 数据补全 JPEG 头部,生成完整的 JPEG 图片
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    /// <param name="jpegData">原始 JPEG 数据(可能缺少完整头部)</param>
 | 
					    /// <param name="jpegData">原始 JPEG 扫描数据(不含头尾)</param>
 | 
				
			||||||
    /// <param name="width">图像宽度</param>
 | 
					    /// <param name="width">图像宽度</param>
 | 
				
			||||||
    /// <param name="height">图像高度</param>
 | 
					    /// <param name="height">图像高度</param>
 | 
				
			||||||
    /// <param name="quality">JPEG质量(1-100,默认80)</param>
 | 
					    /// <param name="quantizationTable">量化表数组(Y0-Y63, Cb0-Cb63, Cr0-Cr63,共192个值)</param>
 | 
				
			||||||
    /// <returns>完整的 JPEG 图片数据</returns>
 | 
					    /// <returns>完整的 JPEG 图片数据</returns>
 | 
				
			||||||
    public static Result<byte[]> CompleteJpegData(byte[] jpegData, int width, int height, int quality = 80)
 | 
					    public static Result<byte[]> CompleteJpegData(byte[] jpegData, int width, int height, uint[] quantizationTable)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (jpegData == null)
 | 
					        if (jpegData == null)
 | 
				
			||||||
            return new(new ArgumentNullException(nameof(jpegData)));
 | 
					            return new(new ArgumentNullException(nameof(jpegData)));
 | 
				
			||||||
@@ -271,95 +271,148 @@ public class Image
 | 
				
			|||||||
        if (width <= 0 || height <= 0)
 | 
					        if (width <= 0 || height <= 0)
 | 
				
			||||||
            return new(new ArgumentException("Width and height must be positive"));
 | 
					            return new(new ArgumentException("Width and height must be positive"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (quality < 1 || quality > 100)
 | 
					        if (quantizationTable == null || quantizationTable.Length != 192)
 | 
				
			||||||
            return new(new ArgumentException("Quality must be between 1 and 100"));
 | 
					            return new(new ArgumentException("Quantization table must contain exactly 192 values (64 Y + 64 Cb + 64 Cr)"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // 检查是否已经是完整的 JPEG 文件(以 FFD8 开头,FFD9 结尾)
 | 
					            var jpegBytes = new List<byte>();
 | 
				
			||||||
            if (jpegData.Length >= 4 &&
 | 
					
 | 
				
			||||||
                jpegData[0] == 0xFF && jpegData[1] == 0xD8 &&
 | 
					            // SOI (Start of Image)
 | 
				
			||||||
                jpegData[jpegData.Length - 2] == 0xFF && jpegData[jpegData.Length - 1] == 0xD9)
 | 
					            jpegBytes.AddRange(new byte[] { 0xFF, 0xD8 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // APP0 段
 | 
				
			||||||
 | 
					            jpegBytes.AddRange(new byte[] { 
 | 
				
			||||||
 | 
					                0xFF, 0xE0,  // APP0 marker
 | 
				
			||||||
 | 
					                0x00, 0x10,  // Length (16 bytes)
 | 
				
			||||||
 | 
					                0x4A, 0x46, 0x49, 0x46, 0x00,  // "JFIF\0"
 | 
				
			||||||
 | 
					                0x01, 0x01,  // Version 1.1
 | 
				
			||||||
 | 
					                0x00,        // Units: 0 = no units
 | 
				
			||||||
 | 
					                0x00, 0x01,  // X density (1)
 | 
				
			||||||
 | 
					                0x00, 0x01,  // Y density (1)
 | 
				
			||||||
 | 
					                0x00,        // Thumbnail width
 | 
				
			||||||
 | 
					                0x00         // Thumbnail height
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // DQT (Define Quantization Table) - Y table
 | 
				
			||||||
 | 
					            jpegBytes.AddRange(new byte[] { 0xFF, 0xDB }); // DQT marker
 | 
				
			||||||
 | 
					            jpegBytes.AddRange(new byte[] { 0x00, 0x43 }); // Length (67 bytes)
 | 
				
			||||||
 | 
					            jpegBytes.Add(0x00); // Table ID (0 = Y table)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 添加Y量化表 (quantizationTable[0-63])
 | 
				
			||||||
 | 
					            for (int i = 0; i < 64; i++)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                // 已经是完整的 JPEG 文件,直接返回
 | 
					                jpegBytes.Add((byte)Math.Min(255, quantizationTable[i]));
 | 
				
			||||||
                return jpegData;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // 创建一个临时的 RGB24 图像用于生成 JPEG 头部
 | 
					            // DQT (Define Quantization Table) - CbCr table
 | 
				
			||||||
            using var tempImage = new SixLabors.ImageSharp.Image<Rgb24>(new Configuration
 | 
					            jpegBytes.AddRange(new byte[] { 0xFF, 0xDB }); // DQT marker
 | 
				
			||||||
 | 
					            jpegBytes.AddRange(new byte[] { 0x00, 0x43 }); // Length (67 bytes)
 | 
				
			||||||
 | 
					            jpegBytes.Add(0x01); // Table ID (1 = CbCr table)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 添加Cb量化表 (quantizationTable[64-127]),但这里使用Cr表的数据作为CbCr共用
 | 
				
			||||||
 | 
					            for (int i = 128; i < 192; i++) // 使用Cr量化表 (quantizationTable[128-191])
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
					                jpegBytes.Add((byte)Math.Min(255, quantizationTable[i]));
 | 
				
			||||||
            }, width, height);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // 填充临时图像(使用简单的渐变色作为占位符)
 | 
					 | 
				
			||||||
            for (int y = 0; y < height; y++)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                for (int x = 0; x < width; x++)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    tempImage[x, y] = new Rgb24((byte)(x % 256), (byte)(y % 256), 128);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            using var stream = new MemoryStream();
 | 
					            // SOF0 (Start of Frame)
 | 
				
			||||||
            tempImage.SaveAsJpeg(stream, new JpegEncoder { Quality = quality });
 | 
					            jpegBytes.AddRange(new byte[] { 
 | 
				
			||||||
            var completeJpeg = stream.ToArray();
 | 
					                0xFF, 0xC0,  // SOF0 marker
 | 
				
			||||||
 | 
					                0x00, 0x11,  // Length (17 bytes)
 | 
				
			||||||
 | 
					                0x08,        // Precision (8 bits)
 | 
				
			||||||
 | 
					                (byte)((height >> 8) & 0xFF), (byte)(height & 0xFF),  // Height
 | 
				
			||||||
 | 
					                (byte)((width >> 8) & 0xFF), (byte)(width & 0xFF),   // Width
 | 
				
			||||||
 | 
					                0x03,        // Number of components
 | 
				
			||||||
 | 
					                0x01, 0x11, 0x00,  // Y component
 | 
				
			||||||
 | 
					                0x02, 0x11, 0x01,  // Cb component
 | 
				
			||||||
 | 
					                0x03, 0x11, 0x01   // Cr component
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // 如果原始数据看起来是 JPEG 扫描数据,尝试替换扫描数据部分
 | 
					            // DHT (Define Huffman Table) - DC Y table
 | 
				
			||||||
            if (jpegData.Length > 0)
 | 
					            jpegBytes.AddRange(new byte[] { 
 | 
				
			||||||
            {
 | 
					                0xFF, 0xC4,  // DHT marker
 | 
				
			||||||
                // 查找 JPEG 扫描数据开始位置(SOS 标记 0xFFDA 后)
 | 
					                0x00, 0x1F,  // Length
 | 
				
			||||||
                int sosIndex = -1;
 | 
					                0x00,        // Table class and ID (DC table 0)
 | 
				
			||||||
                for (int i = 0; i < completeJpeg.Length - 1; i++)
 | 
					                // DC Y Huffman table
 | 
				
			||||||
                {
 | 
					                0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
				
			||||||
                    if (completeJpeg[i] == 0xFF && completeJpeg[i + 1] == 0xDA)
 | 
					                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
 | 
				
			||||||
                    {
 | 
					            });
 | 
				
			||||||
                        // 跳过 SOS 段头部,找到实际扫描数据开始位置
 | 
					 | 
				
			||||||
                        i += 2; // 跳过 FF DA
 | 
					 | 
				
			||||||
                        if (i < completeJpeg.Length - 1)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            int segmentLength = (completeJpeg[i] << 8) | completeJpeg[i + 1];
 | 
					 | 
				
			||||||
                            sosIndex = i + segmentLength;
 | 
					 | 
				
			||||||
                            break;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // 查找 EOI 标记位置(0xFFD9)
 | 
					            // DHT (Define Huffman Table) - AC Y table (简化版)
 | 
				
			||||||
                int eoiIndex = -1;
 | 
					            jpegBytes.AddRange(new byte[] { 
 | 
				
			||||||
                for (int i = completeJpeg.Length - 2; i >= 0; i--)
 | 
					                0xFF, 0xC4,  // DHT marker
 | 
				
			||||||
                {
 | 
					                0x00, 0xB5,  // Length
 | 
				
			||||||
                    if (completeJpeg[i] == 0xFF && completeJpeg[i + 1] == 0xD9)
 | 
					                0x10         // Table class and ID (AC table 0)
 | 
				
			||||||
                    {
 | 
					            });
 | 
				
			||||||
                        eoiIndex = i;
 | 
					            
 | 
				
			||||||
                        break;
 | 
					            // AC Y Huffman table数据
 | 
				
			||||||
                    }
 | 
					            jpegBytes.AddRange(new byte[] {
 | 
				
			||||||
                }
 | 
					                0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
 | 
				
			||||||
 | 
					                0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
 | 
				
			||||||
 | 
					                0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
 | 
				
			||||||
 | 
					                0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
 | 
				
			||||||
 | 
					                0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
 | 
				
			||||||
 | 
					                0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
 | 
				
			||||||
 | 
					                0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
 | 
				
			||||||
 | 
					                0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
 | 
				
			||||||
 | 
					                0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
 | 
				
			||||||
 | 
					                0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
 | 
				
			||||||
 | 
					                0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
 | 
				
			||||||
 | 
					                0xF9, 0xFA
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (sosIndex > 0 && eoiIndex > sosIndex)
 | 
					            // DHT (Define Huffman Table) - DC CbCr table
 | 
				
			||||||
                {
 | 
					            jpegBytes.AddRange(new byte[] { 
 | 
				
			||||||
                    // 替换扫描数据部分
 | 
					                0xFF, 0xC4,  // DHT marker
 | 
				
			||||||
                    var headerLength = sosIndex;
 | 
					                0x00, 0x1F,  // Length
 | 
				
			||||||
                    var footerStart = eoiIndex;
 | 
					                0x01,        // Table class and ID (DC table 1)
 | 
				
			||||||
                    var footerLength = completeJpeg.Length - footerStart;
 | 
					                // DC CbCr Huffman table
 | 
				
			||||||
 | 
					                0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
				
			||||||
 | 
					                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    var newJpegLength = headerLength + jpegData.Length + footerLength;
 | 
					            // DHT (Define Huffman Table) - AC CbCr table
 | 
				
			||||||
                    var newJpegData = new byte[newJpegLength];
 | 
					            jpegBytes.AddRange(new byte[] { 
 | 
				
			||||||
 | 
					                0xFF, 0xC4,  // DHT marker
 | 
				
			||||||
 | 
					                0x00, 0xB5,  // Length
 | 
				
			||||||
 | 
					                0x11         // Table class and ID (AC table 1)
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // AC CbCr Huffman table数据(与AC Y table相同)
 | 
				
			||||||
 | 
					            jpegBytes.AddRange(new byte[] {
 | 
				
			||||||
 | 
					                0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
 | 
				
			||||||
 | 
					                0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
 | 
				
			||||||
 | 
					                0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
 | 
				
			||||||
 | 
					                0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
 | 
				
			||||||
 | 
					                0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
 | 
				
			||||||
 | 
					                0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
 | 
				
			||||||
 | 
					                0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
 | 
				
			||||||
 | 
					                0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
 | 
				
			||||||
 | 
					                0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
 | 
				
			||||||
 | 
					                0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
 | 
				
			||||||
 | 
					                0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
 | 
				
			||||||
 | 
					                0xF9, 0xFA
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // 复制头部
 | 
					            // SOS (Start of Scan)
 | 
				
			||||||
                    Array.Copy(completeJpeg, 0, newJpegData, 0, headerLength);
 | 
					            jpegBytes.AddRange(new byte[] { 
 | 
				
			||||||
 | 
					                0xFF, 0xDA,  // SOS marker
 | 
				
			||||||
 | 
					                0x00, 0x0C,  // Length (12 bytes)
 | 
				
			||||||
 | 
					                0x03,        // Number of components
 | 
				
			||||||
 | 
					                0x01, 0x00,  // Y component, DC/AC table
 | 
				
			||||||
 | 
					                0x02, 0x11,  // Cb component, DC/AC table
 | 
				
			||||||
 | 
					                0x03, 0x11,  // Cr component, DC/AC table
 | 
				
			||||||
 | 
					                0x00, 0x3F, 0x00  // Start of spectral, End of spectral, Ah/Al
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // 复制原始扫描数据
 | 
					            // 添加原始 JPEG 扫描数据
 | 
				
			||||||
                    Array.Copy(jpegData, 0, newJpegData, headerLength, jpegData.Length);
 | 
					            jpegBytes.AddRange(jpegData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // 复制尾部
 | 
					            // EOI (End of Image)
 | 
				
			||||||
                    Array.Copy(completeJpeg, footerStart, newJpegData, headerLength + jpegData.Length, footerLength);
 | 
					            jpegBytes.AddRange(new byte[] { 0xFF, 0xD9 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return newJpegData;
 | 
					            return jpegBytes.ToArray();
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // 如果无法智能合并,返回完整的模板 JPEG
 | 
					 | 
				
			||||||
            return completeJpeg;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ namespace Peripherals.JpegClient;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
static class JpegAddr
 | 
					static class JpegAddr
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    const UInt32 BASE = 0x0000_0000;
 | 
					    const UInt32 BASE = 0xA000_0000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
 | 
					    public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
 | 
				
			||||||
    public const UInt32 CAPTURE_WR_CTRL = BASE + 0x1;
 | 
					    public const UInt32 CAPTURE_WR_CTRL = BASE + 0x1;
 | 
				
			||||||
@@ -26,9 +26,11 @@ static class JpegAddr
 | 
				
			|||||||
    public const UInt32 JPEG_FRAME_SAVE_NUM = BASE + 0xC;
 | 
					    public const UInt32 JPEG_FRAME_SAVE_NUM = BASE + 0xC;
 | 
				
			||||||
    public const UInt32 JPEG_FIFO_FRAME_INFO = BASE + 0xD;
 | 
					    public const UInt32 JPEG_FIFO_FRAME_INFO = BASE + 0xD;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public const UInt32 ADDR_HDMI_WD_START = 0x4000_0000;
 | 
					    public const UInt32 JPEG_QUANTIZATION_TABLE = BASE + 0x100;
 | 
				
			||||||
    public const UInt32 ADDR_JPEG_START = 0x8000_0000;
 | 
					
 | 
				
			||||||
    public const UInt32 ADDR_JPEG_END = 0xA000_0000;
 | 
					    public const UInt32 ADDR_HDMI_WD_START = 0x0400_0000;
 | 
				
			||||||
 | 
					    public const UInt32 ADDR_JPEG_START = 0x0800_0000;
 | 
				
			||||||
 | 
					    public const UInt32 ADDR_JPEG_END = 0x09FF_FFFF;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class JpegInfo
 | 
					public class JpegInfo
 | 
				
			||||||
@@ -142,23 +144,33 @@ public class Jpeg
 | 
				
			|||||||
        else return true;
 | 
					        else return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async ValueTask<bool> SetEnable(bool enable)
 | 
					    public async ValueTask<Result<bool>> SetEnable(bool enable)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (enable)
 | 
					        if (enable)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await UDPClientPool.WriteAddrSeq(
 | 
					 | 
				
			||||||
                this.ep,
 | 
					 | 
				
			||||||
                this.taskID,
 | 
					 | 
				
			||||||
                [JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL],
 | 
					 | 
				
			||||||
                [0b11, 0b01],
 | 
					 | 
				
			||||||
                this.timeout
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            if (!ret.IsSuccessful)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                logger.Error($"Failed to set JPEG enable: {ret.Error}");
 | 
					                var ret = await UDPClientPool.WriteAddrSeq(
 | 
				
			||||||
                return false;
 | 
					                    this.ep,
 | 
				
			||||||
 | 
					                    this.taskID,
 | 
				
			||||||
 | 
					                    [JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL],
 | 
				
			||||||
 | 
					                    [0b11, 0b01],
 | 
				
			||||||
 | 
					                    this.timeout
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                if (!ret.IsSuccessful)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    logger.Error($"Failed to set JPEG enable: {ret.Error}");
 | 
				
			||||||
 | 
					                    return ret.Value;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return ret.Value;
 | 
					            {
 | 
				
			||||||
 | 
					                var ret = await AddFrameNum2Process(1);
 | 
				
			||||||
 | 
					                if (!ret)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    logger.Error($"Failed to AddFrameNum2Process: {ret}");
 | 
				
			||||||
 | 
					                    return ret.Value;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -181,7 +193,7 @@ public class Jpeg
 | 
				
			|||||||
    public async ValueTask<Result<bool>> CheckHdmiIsReady()
 | 
					    public async ValueTask<Result<bool>> CheckHdmiIsReady()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var ret = await UDPClientPool.ReadAddrWithWait(
 | 
					        var ret = await UDPClientPool.ReadAddrWithWait(
 | 
				
			||||||
            this.ep, this.taskID, JpegAddr.HDMI_NOT_READY, 0b01, 0b01, 100, this.timeout);
 | 
					            this.ep, this.taskID, JpegAddr.HDMI_NOT_READY, 0b00, 0b01, 100, this.timeout);
 | 
				
			||||||
        if (!ret.IsSuccessful)
 | 
					        if (!ret.IsSuccessful)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.Error($"Failed to check HDMI status: {ret.Error}");
 | 
					            logger.Error($"Failed to check HDMI status: {ret.Error}");
 | 
				
			||||||
@@ -192,8 +204,8 @@ public class Jpeg
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public async ValueTask<Result<(int, int)>> GetHdmiResolution()
 | 
					    public async ValueTask<Result<(int, int)>> GetHdmiResolution()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var ret = await UDPClientPool.ReadAddr(
 | 
					        var ret = await UDPClientPool.ReadAddrByte(
 | 
				
			||||||
            this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, 0, this.timeout);
 | 
					            this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, this.timeout);
 | 
				
			||||||
        if (!ret.IsSuccessful)
 | 
					        if (!ret.IsSuccessful)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.Error($"Failed to get HDMI resolution: {ret.Error}");
 | 
					            logger.Error($"Failed to get HDMI resolution: {ret.Error}");
 | 
				
			||||||
@@ -207,11 +219,13 @@ public class Jpeg
 | 
				
			|||||||
            return new(new Exception("Invalid HDMI resolution data length"));
 | 
					            return new(new Exception("Invalid HDMI resolution data length"));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var width = data[0] | (data[1] << 8);
 | 
					        var width = data[3] | (data[2] << 8);
 | 
				
			||||||
        var height = data[2] | (data[3] << 8);
 | 
					        var height = data[1] | (data[0] << 8);
 | 
				
			||||||
        this.Width = width;
 | 
					        this.Width = width;
 | 
				
			||||||
        this.Height = height;
 | 
					        this.Height = height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        logger.Info($"HDMI resolution: {width}x{height}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return new((width, height));
 | 
					        return new((width, height));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -223,7 +237,22 @@ public class Jpeg
 | 
				
			|||||||
            return new(new ArgumentException("Invalid HDMI resolution"));
 | 
					            return new(new ArgumentException("Invalid HDMI resolution"));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var frameSize = (UInt32)(width * height / 4);
 | 
					        var frameSize = (UInt32)(width * height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var ret = await UDPClientPool.WriteAddr(
 | 
				
			||||||
 | 
					                this.ep, this.taskID, JpegAddr.JPEG_HEIGHT_WIDTH, (uint)((height << 16) + width), this.timeout);
 | 
				
			||||||
 | 
					            if (!ret.IsSuccessful)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error($"Failed to set HDMI output start address: {ret.Error}");
 | 
				
			||||||
 | 
					                return new(ret.Error);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!ret.Value)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error($"Failed to set HDMI output start address");
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await UDPClientPool.WriteAddr(
 | 
					            var ret = await UDPClientPool.WriteAddr(
 | 
				
			||||||
@@ -243,7 +272,7 @@ public class Jpeg
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await UDPClientPool.WriteAddr(
 | 
					            var ret = await UDPClientPool.WriteAddr(
 | 
				
			||||||
                this.ep, this.taskID, JpegAddr.END_WR_ADDR0,
 | 
					                this.ep, this.taskID, JpegAddr.END_WR_ADDR0,
 | 
				
			||||||
                JpegAddr.ADDR_HDMI_WD_START + frameSize, this.timeout);
 | 
					                JpegAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
 | 
				
			||||||
            if (!ret.IsSuccessful)
 | 
					            if (!ret.IsSuccessful)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                logger.Error($"Failed to set HDMI output end address: {ret.Error}");
 | 
					                logger.Error($"Failed to set HDMI output end address: {ret.Error}");
 | 
				
			||||||
@@ -274,7 +303,7 @@ public class Jpeg
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            var ret = await UDPClientPool.WriteAddr(
 | 
					            var ret = await UDPClientPool.WriteAddr(
 | 
				
			||||||
                this.ep, this.taskID, JpegAddr.END_RD_ADDR0,
 | 
					                this.ep, this.taskID, JpegAddr.END_RD_ADDR0,
 | 
				
			||||||
                JpegAddr.ADDR_HDMI_WD_START + frameSize, this.timeout);
 | 
					                JpegAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
 | 
				
			||||||
            if (!ret.IsSuccessful)
 | 
					            if (!ret.IsSuccessful)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                logger.Error($"Failed to set jpeg input end address: {ret.Error}");
 | 
					                logger.Error($"Failed to set jpeg input end address: {ret.Error}");
 | 
				
			||||||
@@ -339,14 +368,39 @@ public class Jpeg
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public async ValueTask<uint> GetFrameNumber()
 | 
					    public async ValueTask<uint> GetFrameNumber()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var ret = await UDPClientPool.ReadAddrByte(
 | 
					        const int maxAttempts = 10;
 | 
				
			||||||
            this.ep, this.taskID, JpegAddr.JPEG_FRAME_SAVE_NUM, this.timeout);
 | 
					        const int delayMs = 5;
 | 
				
			||||||
        if (!ret.IsSuccessful)
 | 
					        
 | 
				
			||||||
 | 
					        for (int attempt = 0; attempt < maxAttempts; attempt++)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.Error($"Failed to get JPEG frame number: {ret.Error}");
 | 
					            var ret = await UDPClientPool.ReadAddrByte(
 | 
				
			||||||
            return 0;
 | 
					                this.ep, this.taskID, JpegAddr.JPEG_FRAME_SAVE_NUM, this.timeout);
 | 
				
			||||||
 | 
					            if (!ret.IsSuccessful)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error($"Failed to get JPEG frame number on attempt {attempt + 1}: {ret.Error}");
 | 
				
			||||||
 | 
					                if (attempt < maxAttempts - 1)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    await Task.Delay(delayMs);
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return 0;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            var frameNumber = Number.BytesToUInt32(ret.Value.Options.Data ?? Array.Empty<byte>()).Value;
 | 
				
			||||||
 | 
					            if (frameNumber != 0)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return frameNumber;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 如果不是最后一次尝试,等待5ms后重试
 | 
				
			||||||
 | 
					            if (attempt < maxAttempts - 1)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                await Task.Delay(delayMs);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return Number.BytesToUInt32(ret.Value.Options.Data ?? Array.Empty<byte>()).Value;
 | 
					        
 | 
				
			||||||
 | 
					        // 所有尝试都失败或返回0
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async ValueTask<Optional<List<JpegInfo>>> GetFrameInfo(int num)
 | 
					    public async ValueTask<Optional<List<JpegInfo>>> GetFrameInfo(int num)
 | 
				
			||||||
@@ -379,14 +433,14 @@ public class Jpeg
 | 
				
			|||||||
        return new(infos);
 | 
					        return new(infos);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async ValueTask<bool> AddFrameNum2Process(uint cnt)
 | 
					    public async ValueTask<Result<bool>> AddFrameNum2Process(uint cnt)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var ret = await UDPClientPool.WriteAddr(
 | 
					        var ret = await UDPClientPool.WriteAddr(
 | 
				
			||||||
            this.ep, this.taskID, JpegAddr.JPEG_ADD_NEED_FRAME_NUM, cnt, this.timeout);
 | 
					            this.ep, this.taskID, JpegAddr.JPEG_ADD_NEED_FRAME_NUM, cnt, this.timeout);
 | 
				
			||||||
        if (!ret.IsSuccessful)
 | 
					        if (!ret.IsSuccessful)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            logger.Error($"Failed to update pointer: {ret.Error}");
 | 
					            logger.Error($"Failed to update pointer: {ret.Error}");
 | 
				
			||||||
            return false;
 | 
					            return ret.Value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return ret.Value;
 | 
					        return ret.Value;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -510,4 +564,45 @@ public class Jpeg
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return frames;
 | 
					        return frames;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async ValueTask<Result<uint[]?>> GetQuantizationTable()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        const int totalQuantValues = 8 * 8 * 3; // Y(64) + Cb(64) + Cr(64) = 192个量化值
 | 
				
			||||||
 | 
					        const int bytesPerValue = 4; // 每个量化值32bit = 4字节
 | 
				
			||||||
 | 
					        const int totalBytes = totalQuantValues * bytesPerValue; // 总共768字节
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var ret = await UDPClientPool.ReadAddr4Bytes(
 | 
				
			||||||
 | 
					                this.ep, this.taskID, JpegAddr.JPEG_QUANTIZATION_TABLE, totalBytes, this.timeout);
 | 
				
			||||||
 | 
					            if (!ret.IsSuccessful)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error($"Failed to read JPEG quantization table: {ret.Error}");
 | 
				
			||||||
 | 
					                return new(ret.Error);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var data = ret.Value;
 | 
				
			||||||
 | 
					            if (data == null || data.Length != totalBytes)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error($"Invalid quantization table data length: expected {totalBytes}, got {data?.Length ?? 0}");
 | 
				
			||||||
 | 
					                return new(new Exception("Invalid quantization table data length"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var quantTable = new uint[totalQuantValues];
 | 
				
			||||||
 | 
					            for (int i = 0; i < totalQuantValues; i++)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // 每32bit为一个量化值,按小端序读取
 | 
				
			||||||
 | 
					                var offset = i * bytesPerValue;
 | 
				
			||||||
 | 
					                quantTable[i] = (uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            logger.Debug($"Successfully read JPEG quantization table with {totalQuantValues} values");
 | 
				
			||||||
 | 
					            return quantTable;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (Exception ex)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            logger.Error(ex, "Exception occurred while reading JPEG quantization table");
 | 
				
			||||||
 | 
					            return new(ex);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -235,7 +235,19 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var jpegData = frameResult.Value[0];
 | 
					            var jpegData = frameResult.Value[0];
 | 
				
			||||||
            var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height);
 | 
					            
 | 
				
			||||||
 | 
					            var quantTableResult = await client.JpegClient.GetQuantizationTable();
 | 
				
			||||||
 | 
					            if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
 | 
				
			||||||
 | 
					                response.StatusCode = 500;
 | 
				
			||||||
 | 
					                var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table");
 | 
				
			||||||
 | 
					                await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
 | 
				
			||||||
 | 
					                response.Close();
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height, quantTableResult.Value);
 | 
				
			||||||
            if (!jpegImage.IsSuccessful)
 | 
					            if (!jpegImage.IsSuccessful)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                logger.Error("JPEG数据补全失败");
 | 
					                logger.Error("JPEG数据补全失败");
 | 
				
			||||||
@@ -280,6 +292,18 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            logger.Debug("开始HDMI MJPEG流传输");
 | 
					            logger.Debug("开始HDMI MJPEG流传输");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var quantTableResult = await client.JpegClient.GetQuantizationTable();
 | 
				
			||||||
 | 
					            if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
 | 
				
			||||||
 | 
					                response.StatusCode = 500;
 | 
				
			||||||
 | 
					                await response.OutputStream.WriteAsync(
 | 
				
			||||||
 | 
					                    System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table"), 0, 0, cancellationToken);
 | 
				
			||||||
 | 
					                response.Close();
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            var quantTable = quantTableResult.Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            int frameCounter = 0;
 | 
					            int frameCounter = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            while (!cancellationToken.IsCancellationRequested)
 | 
					            while (!cancellationToken.IsCancellationRequested)
 | 
				
			||||||
@@ -297,7 +321,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                foreach (var framebytes in frameResult.Value)
 | 
					                foreach (var framebytes in frameResult.Value)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    var jpegImage = Common.Image.CompleteJpegData(framebytes, client.Width, client.Height);
 | 
					                    var jpegImage = Common.Image.CompleteJpegData(framebytes, client.Width, client.Height, quantTable);
 | 
				
			||||||
                    if (!jpegImage.IsSuccessful)
 | 
					                    if (!jpegImage.IsSuccessful)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        logger.Error("JPEG数据不完整");
 | 
					                        logger.Error("JPEG数据不完整");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,7 @@ export interface TemplateConfig {
 | 
				
			|||||||
export const previewSizes: Record<string, number> = {
 | 
					export const previewSizes: Record<string, number> = {
 | 
				
			||||||
  MechanicalButton: 0.4,
 | 
					  MechanicalButton: 0.4,
 | 
				
			||||||
  Switch: 0.35,
 | 
					  Switch: 0.35,
 | 
				
			||||||
 | 
					  EC11RotaryEncoder: 0.4,
 | 
				
			||||||
  Pin: 0.8,
 | 
					  Pin: 0.8,
 | 
				
			||||||
  SMT_LED: 0.7,
 | 
					  SMT_LED: 0.7,
 | 
				
			||||||
  SevenSegmentDisplay: 0.4,
 | 
					  SevenSegmentDisplay: 0.4,
 | 
				
			||||||
@@ -48,6 +49,7 @@ export const previewSizes: Record<string, number> = {
 | 
				
			|||||||
export const availableComponents: ComponentConfig[] = [
 | 
					export const availableComponents: ComponentConfig[] = [
 | 
				
			||||||
  { type: "MechanicalButton", name: "机械按钮" },
 | 
					  { type: "MechanicalButton", name: "机械按钮" },
 | 
				
			||||||
  { type: "Switch", name: "开关" },
 | 
					  { type: "Switch", name: "开关" },
 | 
				
			||||||
 | 
					  { type: "EC11RotaryEncoder", name: "EC11旋转编码器" },
 | 
				
			||||||
  { type: "Pin", name: "引脚" },
 | 
					  { type: "Pin", name: "引脚" },
 | 
				
			||||||
  { type: "SMT_LED", name: "贴片LED" },
 | 
					  { type: "SMT_LED", name: "贴片LED" },
 | 
				
			||||||
  { type: "SevenSegmentDisplay", name: "数码管" },
 | 
					  { type: "SevenSegmentDisplay", name: "数码管" },
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										258
									
								
								src/components/equipments/EC11RotaryEncoder.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								src/components/equipments/EC11RotaryEncoder.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,258 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="ec11-container" :style="{
 | 
				
			||||||
 | 
					    width: width + 'px',
 | 
				
			||||||
 | 
					    height: height + 'px',
 | 
				
			||||||
 | 
					    position: 'relative',
 | 
				
			||||||
 | 
					  }">
 | 
				
			||||||
 | 
					    <svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 100 100"
 | 
				
			||||||
 | 
					      class="ec11-encoder">
 | 
				
			||||||
 | 
					      <defs>
 | 
				
			||||||
 | 
					        <!-- 发光效果滤镜 -->
 | 
				
			||||||
 | 
					        <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
 | 
				
			||||||
 | 
					          <feFlood result="flood" flood-color="#00ff88" flood-opacity="1"></feFlood>
 | 
				
			||||||
 | 
					          <feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
 | 
				
			||||||
 | 
					          <feMorphology in="mask" result="dilated" operator="dilate" radius="1"></feMorphology>
 | 
				
			||||||
 | 
					          <feGaussianBlur in="dilated" stdDeviation="2" result="blur1" />
 | 
				
			||||||
 | 
					          <feGaussianBlur in="dilated" stdDeviation="4" result="blur2" />
 | 
				
			||||||
 | 
					          <feGaussianBlur in="dilated" stdDeviation="8" result="blur3" />
 | 
				
			||||||
 | 
					          <feMerge>
 | 
				
			||||||
 | 
					            <feMergeNode in="blur3" />
 | 
				
			||||||
 | 
					            <feMergeNode in="blur2" />
 | 
				
			||||||
 | 
					            <feMergeNode in="blur1" />
 | 
				
			||||||
 | 
					            <feMergeNode in="SourceGraphic" />
 | 
				
			||||||
 | 
					          </feMerge>
 | 
				
			||||||
 | 
					        </filter>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- 编码器主体渐变 -->
 | 
				
			||||||
 | 
					        <radialGradient id="encoderGradient" cx="50%" cy="30%">
 | 
				
			||||||
 | 
					          <stop offset="0%" stop-color="#666666" />
 | 
				
			||||||
 | 
					          <stop offset="70%" stop-color="#333333" />
 | 
				
			||||||
 | 
					          <stop offset="100%" stop-color="#1a1a1a" />
 | 
				
			||||||
 | 
					        </radialGradient>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- 旋钮渐变 -->
 | 
				
			||||||
 | 
					        <radialGradient id="knobGradient" cx="30%" cy="30%">
 | 
				
			||||||
 | 
					          <stop offset="0%" stop-color="#555555" />
 | 
				
			||||||
 | 
					          <stop offset="70%" stop-color="#222222" />
 | 
				
			||||||
 | 
					          <stop offset="100%" stop-color="#111111" />
 | 
				
			||||||
 | 
					        </radialGradient>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- 按下状态渐变 -->
 | 
				
			||||||
 | 
					        <radialGradient id="knobPressedGradient" cx="50%" cy="50%">
 | 
				
			||||||
 | 
					          <stop offset="0%" stop-color="#333333" />
 | 
				
			||||||
 | 
					          <stop offset="70%" stop-color="#555555" />
 | 
				
			||||||
 | 
					          <stop offset="100%" stop-color="#888888" />
 | 
				
			||||||
 | 
					        </radialGradient>
 | 
				
			||||||
 | 
					      </defs>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 编码器底座 -->
 | 
				
			||||||
 | 
					      <rect x="10" y="30" width="80" height="60" rx="8" ry="8" 
 | 
				
			||||||
 | 
					            fill="#2a2a2a" stroke="#444444" stroke-width="1"/>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 编码器主体外壳 -->
 | 
				
			||||||
 | 
					      <circle cx="50" cy="60" r="32" fill="url(#encoderGradient)" stroke="#555555" stroke-width="1"/>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 编码器接线端子 -->
 | 
				
			||||||
 | 
					      <rect x="5" y="75" width="4" height="8" fill="#c9c9c9" rx="1"/>
 | 
				
			||||||
 | 
					      <rect x="15" y="85" width="4" height="8" fill="#c9c9c9" rx="1"/>
 | 
				
			||||||
 | 
					      <rect x="25" y="85" width="4" height="8" fill="#c9c9c9" rx="1"/>
 | 
				
			||||||
 | 
					      <rect x="81" y="85" width="4" height="8" fill="#c9c9c9" rx="1"/>
 | 
				
			||||||
 | 
					      <rect x="91" y="75" width="4" height="8" fill="#c9c9c9" rx="1"/>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 旋钮 -->
 | 
				
			||||||
 | 
					      <circle cx="50" cy="60" r="22" 
 | 
				
			||||||
 | 
					              :fill="isPressed ? 'url(#knobPressedGradient)' : 'url(#knobGradient)'" 
 | 
				
			||||||
 | 
					              stroke="#666666" stroke-width="1"
 | 
				
			||||||
 | 
					              :transform="`rotate(${rotation/2} 50 60)`"
 | 
				
			||||||
 | 
					              class="interactive"
 | 
				
			||||||
 | 
					              @mousedown="handleMouseDown"
 | 
				
			||||||
 | 
					              @mouseup="handlePress(false)"
 | 
				
			||||||
 | 
					              @mouseleave="handlePress(false)"/>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 旋钮指示器 -->
 | 
				
			||||||
 | 
					      <line x1="50" y1="42" x2="50" y2="48" 
 | 
				
			||||||
 | 
					            stroke="#ffffff" stroke-width="2" stroke-linecap="round"
 | 
				
			||||||
 | 
					            :transform="`rotate(${rotation} 50 60)`"/>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 旋钮上的纹理刻度 -->
 | 
				
			||||||
 | 
					      <g :transform="`rotate(${rotation} 50 60)`">
 | 
				
			||||||
 | 
					        <circle cx="50" cy="60" r="18" fill="none" stroke="#777777" stroke-width="0.5"/>
 | 
				
			||||||
 | 
					        <!-- 刻度线 -->
 | 
				
			||||||
 | 
					        <g v-for="i in 16" :key="i">
 | 
				
			||||||
 | 
					          <line :x1="50 + 16 * Math.cos((i-1) * Math.PI / 8)" 
 | 
				
			||||||
 | 
					                :y1="60 + 16 * Math.sin((i-1) * Math.PI / 8)"
 | 
				
			||||||
 | 
					                :x2="50 + 18 * Math.cos((i-1) * Math.PI / 8)" 
 | 
				
			||||||
 | 
					                :y2="60 + 18 * Math.sin((i-1) * Math.PI / 8)"
 | 
				
			||||||
 | 
					                stroke="#999999" stroke-width="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					      </g>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 编码器编号标签 -->
 | 
				
			||||||
 | 
					      <text x="50" y="15" text-anchor="middle" font-family="Arial" font-size="10" 
 | 
				
			||||||
 | 
					            fill="#cccccc" font-weight="bold">
 | 
				
			||||||
 | 
					        EC11-{{ encoderNumber }}
 | 
				
			||||||
 | 
					      </text>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 状态指示器 -->
 | 
				
			||||||
 | 
					      <circle cx="85" cy="20" r="3" :fill="isPressed ? '#ff4444' : '#444444'" 
 | 
				
			||||||
 | 
					              :filter="isPressed ? 'url(#glow)' : ''" 
 | 
				
			||||||
 | 
					              stroke="#666666" stroke-width="0.5"/>
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { ref, computed } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  size?: number;
 | 
				
			||||||
 | 
					  encoderNumber?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
 | 
					  size: 1,
 | 
				
			||||||
 | 
					  encoderNumber: 1
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 组件状态
 | 
				
			||||||
 | 
					const isPressed = ref(false);
 | 
				
			||||||
 | 
					const rotation = ref(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 拖动状态
 | 
				
			||||||
 | 
					const isDragging = ref(false);
 | 
				
			||||||
 | 
					const dragStartX = ref(0);
 | 
				
			||||||
 | 
					const lastTriggerX = ref(0);
 | 
				
			||||||
 | 
					const dragThreshold = 20; // 每20像素触发一次旋转
 | 
				
			||||||
 | 
					const hasRotated = ref(false); // 标记是否已经发生了旋转
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 计算宽高
 | 
				
			||||||
 | 
					const width = computed(() => 100 * props.size);
 | 
				
			||||||
 | 
					const height = computed(() => 100 * props.size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 定义发出的事件
 | 
				
			||||||
 | 
					const emit = defineEmits(['press', 'release', 'rotate-left', 'rotate-right']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 鼠标按下处理
 | 
				
			||||||
 | 
					function handleMouseDown(event: MouseEvent) {
 | 
				
			||||||
 | 
					  isDragging.value = true;
 | 
				
			||||||
 | 
					  dragStartX.value = event.clientX;
 | 
				
			||||||
 | 
					  lastTriggerX.value = event.clientX;
 | 
				
			||||||
 | 
					  hasRotated.value = false; // 重置旋转标记
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 添加全局鼠标事件监听
 | 
				
			||||||
 | 
					  document.addEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
 | 
					  document.addEventListener('mouseup', handleMouseUp);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 鼠标移动处理
 | 
				
			||||||
 | 
					function handleMouseMove(event: MouseEvent) {
 | 
				
			||||||
 | 
					  if (!isDragging.value) return;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  const currentX = event.clientX;
 | 
				
			||||||
 | 
					  const deltaX = currentX - lastTriggerX.value;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 检查是否达到触发阈值
 | 
				
			||||||
 | 
					  if (Math.abs(deltaX) >= dragThreshold) {
 | 
				
			||||||
 | 
					    hasRotated.value = true; // 标记已经发生旋转
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (deltaX > 0) {
 | 
				
			||||||
 | 
					      // 右拖动 - 右旋转
 | 
				
			||||||
 | 
					      rotation.value += 15;
 | 
				
			||||||
 | 
					      emit('rotate-right', { 
 | 
				
			||||||
 | 
					        encoderNumber: props.encoderNumber
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // 左拖动 - 左旋转
 | 
				
			||||||
 | 
					      rotation.value -= 15;
 | 
				
			||||||
 | 
					      emit('rotate-left', { 
 | 
				
			||||||
 | 
					        encoderNumber: props.encoderNumber
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 更新最后触发位置
 | 
				
			||||||
 | 
					    lastTriggerX.value = currentX;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 保持角度在0-360度范围内
 | 
				
			||||||
 | 
					    rotation.value = rotation.value % 720;
 | 
				
			||||||
 | 
					    if (rotation.value < 0) {
 | 
				
			||||||
 | 
					      rotation.value += 720;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 鼠标松开处理
 | 
				
			||||||
 | 
					function handleMouseUp() {
 | 
				
			||||||
 | 
					  isDragging.value = false;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 只有在没有发生旋转的情况下才识别为按压事件
 | 
				
			||||||
 | 
					  if (!hasRotated.value) {
 | 
				
			||||||
 | 
					    // 触发按压和释放事件(模拟快速按压)
 | 
				
			||||||
 | 
					    handlePress(true);
 | 
				
			||||||
 | 
					    // 使用setTimeout来模拟按压和释放的时序
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      handlePress(false);
 | 
				
			||||||
 | 
					    }, 100);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 移除全局事件监听
 | 
				
			||||||
 | 
					  document.removeEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
 | 
					  document.removeEventListener('mouseup', handleMouseUp);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 按压处理
 | 
				
			||||||
 | 
					function handlePress(pressed: boolean) {
 | 
				
			||||||
 | 
					  if (pressed !== isPressed.value) {
 | 
				
			||||||
 | 
					    isPressed.value = pressed;
 | 
				
			||||||
 | 
					    if (pressed) {
 | 
				
			||||||
 | 
					      emit('press', { encoderNumber: props.encoderNumber });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      emit('release', { encoderNumber: props.encoderNumber });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 暴露组件方法
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
					  press: () => handlePress(true),
 | 
				
			||||||
 | 
					  release: () => handlePress(false),
 | 
				
			||||||
 | 
					  rotateLeft: () => {
 | 
				
			||||||
 | 
					    rotation.value -= 15;
 | 
				
			||||||
 | 
					    emit('rotate-left', { 
 | 
				
			||||||
 | 
					      encoderNumber: props.encoderNumber
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  rotateRight: () => {
 | 
				
			||||||
 | 
					    rotation.value += 15;
 | 
				
			||||||
 | 
					    emit('rotate-right', { 
 | 
				
			||||||
 | 
					      encoderNumber: props.encoderNumber
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  isPressed: () => isPressed.value
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					// 添加一个静态方法来获取默认props
 | 
				
			||||||
 | 
					export function getDefaultProps() {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    size: 1,
 | 
				
			||||||
 | 
					    encoderNumber: 1
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped lang="postcss">
 | 
				
			||||||
 | 
					.ec11-container {
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ec11-encoder {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  overflow: visible;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.interactive {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user