From fdfc5729ecf064e758c95ad60a8ca519ed8dec7a Mon Sep 17 00:00:00 2001 From: alivender <13898766233@163.com> Date: Sun, 17 Aug 2025 14:55:38 +0800 Subject: [PATCH] =?UTF-8?q?feat&fix:=20=E5=AE=8C=E5=96=84JPEGClient?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=89=8D=E7=AB=AF=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E5=99=A8=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Common/Image.cs | 203 +++++++++----- server/src/Peripherals/JpegClient.cs | 159 ++++++++--- .../Services/HttpHdmiVideoStreamService.cs | 28 +- src/components/LabCanvas/index.ts | 2 + .../equipments/EC11RotaryEncoder.vue | 258 ++++++++++++++++++ 5 files changed, 541 insertions(+), 109 deletions(-) create mode 100644 src/components/equipments/EC11RotaryEncoder.vue diff --git a/server/src/Common/Image.cs b/server/src/Common/Image.cs index 65cd5ef..f650d14 100644 --- a/server/src/Common/Image.cs +++ b/server/src/Common/Image.cs @@ -258,12 +258,12 @@ public class Image /// /// 将原始 JPEG 数据补全 JPEG 头部,生成完整的 JPEG 图片 /// - /// 原始 JPEG 数据(可能缺少完整头部) + /// 原始 JPEG 扫描数据(不含头尾) /// 图像宽度 /// 图像高度 - /// JPEG质量(1-100,默认80) + /// 量化表数组(Y0-Y63, Cb0-Cb63, Cr0-Cr63,共192个值) /// 完整的 JPEG 图片数据 - public static Result CompleteJpegData(byte[] jpegData, int width, int height, int quality = 80) + public static Result CompleteJpegData(byte[] jpegData, int width, int height, uint[] quantizationTable) { if (jpegData == null) return new(new ArgumentNullException(nameof(jpegData))); @@ -271,95 +271,148 @@ public class Image 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")); + if (quantizationTable == null || quantizationTable.Length != 192) + return new(new ArgumentException("Quantization table must contain exactly 192 values (64 Y + 64 Cb + 64 Cr)")); try { - // 检查是否已经是完整的 JPEG 文件(以 FFD8 开头,FFD9 结尾) - if (jpegData.Length >= 4 && - jpegData[0] == 0xFF && jpegData[1] == 0xD8 && - jpegData[jpegData.Length - 2] == 0xFF && jpegData[jpegData.Length - 1] == 0xD9) + var jpegBytes = new List(); + + // SOI (Start of Image) + 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 文件,直接返回 - return jpegData; + jpegBytes.Add((byte)Math.Min(255, quantizationTable[i])); } - // 创建一个临时的 RGB24 图像用于生成 JPEG 头部 - using var tempImage = new SixLabors.ImageSharp.Image(new Configuration + // DQT (Define Quantization Table) - CbCr table + 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]) { - - }, 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); - } + jpegBytes.Add((byte)Math.Min(255, quantizationTable[i])); } - using var stream = new MemoryStream(); - tempImage.SaveAsJpeg(stream, new JpegEncoder { Quality = quality }); - var completeJpeg = stream.ToArray(); + // SOF0 (Start of Frame) + jpegBytes.AddRange(new byte[] { + 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 扫描数据,尝试替换扫描数据部分 - if (jpegData.Length > 0) - { - // 查找 JPEG 扫描数据开始位置(SOS 标记 0xFFDA 后) - int sosIndex = -1; - for (int i = 0; i < completeJpeg.Length - 1; i++) - { - if (completeJpeg[i] == 0xFF && completeJpeg[i + 1] == 0xDA) - { - // 跳过 SOS 段头部,找到实际扫描数据开始位置 - i += 2; // 跳过 FF DA - if (i < completeJpeg.Length - 1) - { - int segmentLength = (completeJpeg[i] << 8) | completeJpeg[i + 1]; - sosIndex = i + segmentLength; - break; - } - } - } + // DHT (Define Huffman Table) - DC Y table + jpegBytes.AddRange(new byte[] { + 0xFF, 0xC4, // DHT marker + 0x00, 0x1F, // Length + 0x00, // Table class and ID (DC table 0) + // DC Y 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 + }); - // 查找 EOI 标记位置(0xFFD9) - int eoiIndex = -1; - for (int i = completeJpeg.Length - 2; i >= 0; i--) - { - if (completeJpeg[i] == 0xFF && completeJpeg[i + 1] == 0xD9) - { - eoiIndex = i; - break; - } - } + // DHT (Define Huffman Table) - AC Y table (简化版) + jpegBytes.AddRange(new byte[] { + 0xFF, 0xC4, // DHT marker + 0x00, 0xB5, // Length + 0x10 // Table class and ID (AC table 0) + }); + + // 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) - { - // 替换扫描数据部分 - var headerLength = sosIndex; - var footerStart = eoiIndex; - var footerLength = completeJpeg.Length - footerStart; + // DHT (Define Huffman Table) - DC CbCr table + jpegBytes.AddRange(new byte[] { + 0xFF, 0xC4, // DHT marker + 0x00, 0x1F, // Length + 0x01, // Table class and ID (DC table 1) + // 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; - var newJpegData = new byte[newJpegLength]; + // DHT (Define Huffman Table) - AC CbCr table + 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 + }); - // 复制头部 - Array.Copy(completeJpeg, 0, newJpegData, 0, headerLength); + // SOS (Start of Scan) + 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 + }); - // 复制原始扫描数据 - Array.Copy(jpegData, 0, newJpegData, headerLength, jpegData.Length); + // 添加原始 JPEG 扫描数据 + jpegBytes.AddRange(jpegData); - // 复制尾部 - Array.Copy(completeJpeg, footerStart, newJpegData, headerLength + jpegData.Length, footerLength); + // EOI (End of Image) + jpegBytes.AddRange(new byte[] { 0xFF, 0xD9 }); - return newJpegData; - } - } - - // 如果无法智能合并,返回完整的模板 JPEG - return completeJpeg; + return jpegBytes.ToArray(); } catch (Exception ex) { diff --git a/server/src/Peripherals/JpegClient.cs b/server/src/Peripherals/JpegClient.cs index 1a32c3c..a3250b1 100644 --- a/server/src/Peripherals/JpegClient.cs +++ b/server/src/Peripherals/JpegClient.cs @@ -6,7 +6,7 @@ namespace Peripherals.JpegClient; 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_WR_CTRL = BASE + 0x1; @@ -26,9 +26,11 @@ static class JpegAddr public const UInt32 JPEG_FRAME_SAVE_NUM = BASE + 0xC; public const UInt32 JPEG_FIFO_FRAME_INFO = BASE + 0xD; - public const UInt32 ADDR_HDMI_WD_START = 0x4000_0000; - public const UInt32 ADDR_JPEG_START = 0x8000_0000; - public const UInt32 ADDR_JPEG_END = 0xA000_0000; + public const UInt32 JPEG_QUANTIZATION_TABLE = BASE + 0x100; + + 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 @@ -142,23 +144,33 @@ public class Jpeg else return true; } - public async ValueTask SetEnable(bool enable) + public async ValueTask> SetEnable(bool 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}"); - return false; + 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}"); + 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 { @@ -181,7 +193,7 @@ public class Jpeg public async ValueTask> CheckHdmiIsReady() { 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) { logger.Error($"Failed to check HDMI status: {ret.Error}"); @@ -192,8 +204,8 @@ public class Jpeg public async ValueTask> GetHdmiResolution() { - var ret = await UDPClientPool.ReadAddr( - this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, 0, this.timeout); + var ret = await UDPClientPool.ReadAddrByte( + this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, this.timeout); if (!ret.IsSuccessful) { 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")); } - var width = data[0] | (data[1] << 8); - var height = data[2] | (data[3] << 8); + var width = data[3] | (data[2] << 8); + var height = data[1] | (data[0] << 8); this.Width = width; this.Height = height; + logger.Info($"HDMI resolution: {width}x{height}"); + return new((width, height)); } @@ -223,7 +237,22 @@ public class Jpeg 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( @@ -243,7 +272,7 @@ public class Jpeg { var ret = await UDPClientPool.WriteAddr( 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) { logger.Error($"Failed to set HDMI output end address: {ret.Error}"); @@ -274,7 +303,7 @@ public class Jpeg { var ret = await UDPClientPool.WriteAddr( 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) { logger.Error($"Failed to set jpeg input end address: {ret.Error}"); @@ -339,14 +368,39 @@ public class Jpeg public async ValueTask GetFrameNumber() { - var ret = await UDPClientPool.ReadAddrByte( - this.ep, this.taskID, JpegAddr.JPEG_FRAME_SAVE_NUM, this.timeout); - if (!ret.IsSuccessful) + const int maxAttempts = 10; + const int delayMs = 5; + + for (int attempt = 0; attempt < maxAttempts; attempt++) { - logger.Error($"Failed to get JPEG frame number: {ret.Error}"); - return 0; + var ret = await UDPClientPool.ReadAddrByte( + 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()).Value; + if (frameNumber != 0) + { + return frameNumber; + } + + // 如果不是最后一次尝试,等待5ms后重试 + if (attempt < maxAttempts - 1) + { + await Task.Delay(delayMs); + } } - return Number.BytesToUInt32(ret.Value.Options.Data ?? Array.Empty()).Value; + + // 所有尝试都失败或返回0 + return 0; } public async ValueTask>> GetFrameInfo(int num) @@ -379,14 +433,14 @@ public class Jpeg return new(infos); } - public async ValueTask AddFrameNum2Process(uint cnt) + public async ValueTask> AddFrameNum2Process(uint cnt) { var ret = await UDPClientPool.WriteAddr( this.ep, this.taskID, JpegAddr.JPEG_ADD_NEED_FRAME_NUM, cnt, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to update pointer: {ret.Error}"); - return false; + return ret.Value; } return ret.Value; } @@ -510,4 +564,45 @@ public class Jpeg return frames; } + + public async ValueTask> 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); + } + } } diff --git a/server/src/Services/HttpHdmiVideoStreamService.cs b/server/src/Services/HttpHdmiVideoStreamService.cs index 98d839f..2ef4f73 100644 --- a/server/src/Services/HttpHdmiVideoStreamService.cs +++ b/server/src/Services/HttpHdmiVideoStreamService.cs @@ -235,7 +235,19 @@ public class HttpHdmiVideoStreamService : BackgroundService } 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) { logger.Error("JPEG数据补全失败"); @@ -280,6 +292,18 @@ public class HttpHdmiVideoStreamService : BackgroundService 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; while (!cancellationToken.IsCancellationRequested) @@ -297,7 +321,7 @@ public class HttpHdmiVideoStreamService : BackgroundService 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) { logger.Error("JPEG数据不完整"); diff --git a/src/components/LabCanvas/index.ts b/src/components/LabCanvas/index.ts index 8ccb84f..e5ea0b4 100644 --- a/src/components/LabCanvas/index.ts +++ b/src/components/LabCanvas/index.ts @@ -29,6 +29,7 @@ export interface TemplateConfig { export const previewSizes: Record = { MechanicalButton: 0.4, Switch: 0.35, + EC11RotaryEncoder: 0.4, Pin: 0.8, SMT_LED: 0.7, SevenSegmentDisplay: 0.4, @@ -48,6 +49,7 @@ export const previewSizes: Record = { export const availableComponents: ComponentConfig[] = [ { type: "MechanicalButton", name: "机械按钮" }, { type: "Switch", name: "开关" }, + { type: "EC11RotaryEncoder", name: "EC11旋转编码器" }, { type: "Pin", name: "引脚" }, { type: "SMT_LED", name: "贴片LED" }, { type: "SevenSegmentDisplay", name: "数码管" }, diff --git a/src/components/equipments/EC11RotaryEncoder.vue b/src/components/equipments/EC11RotaryEncoder.vue new file mode 100644 index 0000000..f5d7d9a --- /dev/null +++ b/src/components/equipments/EC11RotaryEncoder.vue @@ -0,0 +1,258 @@ + + + + + + +