using System; using System.IO; using System.Net; using System.Net.Sockets; using LightGlue.Unity.Networking; using LightGlue.Unity.Config; using LightGlue.Unity.Python; using UnityEngine; namespace LightGlue.Unity.Bridge { /// /// Bridge: 统一管理硬件与 Python 的通信。 /// - 硬件 -> Unity:UDP JPEG 接收(bindIp:hardwarePort) /// - Unity -> 硬件:0x40 参数设置帧(分辨率/质量/间隔/开关) /// - Unity -> Python:图像数据(stdin 或 UDP) /// - Python -> Unity:算法结果 UDP 接收(pythonResultBindIp:pythonResultPort) /// public sealed class HardwareToPythonUdpBridge : MonoBehaviour { [Header("配置管理")] [Tooltip("是否从NetworkConfigManager自动加载配置(启动时)")] public bool autoLoadNetworkConfig = true; // ---------- 硬件 -> Unity:JPEG 接收 ---------- [Header("Hardware UDP (input: hardware -> Unity)")] [Tooltip("Local bind IP for hardware stream, e.g. 192.168.0.105 or 0.0.0.0")] public string hardwareBindIp = "192.168.0.105"; [Tooltip("UDP port where hardware sends JPEG packets.")] public int hardwarePort = 12346; [Tooltip("Timeout to reset incomplete JPEG (seconds).")] public float hardwareTimeoutSeconds = 2.0f; // ---------- 硬件控制:Unity -> 硬件(0x40 参数设置) ---------- [Header("Hardware Control UDP (Unity -> Hardware)")] [Tooltip("硬件设备 IP(如 ESP32S3),用于发送 0x40 参数设置帧")] public string hardwareControlIp = "192.168.0.106"; [Tooltip("硬件控制端口(硬件接收 0x40 的端口,如 8888)")] public int hardwareControlPort = 8888; [Header("Hardware Auto Apply (one-shot)")] [Tooltip("当检测到硬件开始发送 JPEG 帧时,是否自动将当前 transmissionConfig 以 0x40 下发给硬件(仅一次)。")] public bool autoApplyHardwareConfigOnFirstFrame = true; [Header("Python Output Mode")] [Tooltip("传输模式:Stdin(直接进程通信,推荐)或 UDP(传统模式)")] public TransferMode transferMode = TransferMode.Stdin; [Header("Python UDP (legacy mode: Unity -> Python)")] [Tooltip("Python process IP (normally localhost). Only used in UDP mode.")] public string pythonIp = "127.0.0.1"; [Tooltip("Python UDP port to receive JPEG from Unity. Only used in UDP mode.")] public int pythonPort = 12347; [Header("Python Control (Unity -> Python control commands)")] [Tooltip("是否启用向Python发送控制指令(例如刷新参考图),需要Python脚本开启 --control_port")] public bool enablePythonControl = true; [Tooltip("Python 控制端口(需与 demo_lightglue_camera_position_async.py 的 --control_port 一致,例如 12349)")] public int pythonControlPort = 12349; [Tooltip("用于触发“刷新参考图”指令的按键(等价于 Python 端按下 n 键)")] public KeyCode refreshReferenceKey = KeyCode.N; [Header("基准图刷新方式与频率(由 Unity 触发)")] [Tooltip("基准图来源:仅摄像头(N键) / Python截屏 / Unity游戏画面")] public ReferenceSource referenceSource = ReferenceSource.CameraOnly; [Tooltip("基准图刷新间隔(秒),仅当来源为 Python截屏 或 Unity游戏画面 时生效")] [Range(0.01f, 60f)] public float referenceRefreshIntervalSeconds = 5f; [Tooltip("Unity游戏画面作为基准图时,用于截图的相机(空则使用 Camera.main)")] public Camera gameViewCamera; [Tooltip("基准图截图宽度(与 Python --resize 一致,如 640)")] public int referenceCaptureWidth = 640; [Tooltip("基准图截图高度(与 Python --resize 一致,如 480)")] public int referenceCaptureHeight = 480; [Header("Python Process (stdin mode: Unity -> Python)")] [Tooltip("Python进程控制器(用于stdin模式,直接传递图片数据)")] public PythonProcessController pythonProcessController; [Tooltip("为 true 时:收到首帧图像后再启动 Python(需 PythonProcessController.startOnFirstImageReceived 同时为 true),避免无图时启动算法。")] public bool startPythonOnFirstImageReceived = true; [Tooltip("Max queued frames in receiver (latest N only).")] public int maxQueuedFrames = 2; [Header("Python Result Receiver (Python -> Unity)")] [Tooltip("是否启用Python结果接收(接收算法计算结果)")] public bool enableResultReceiver = true; [Tooltip("Python结果接收端口(用于接收Python发送的算法结果,默认12348)")] public int pythonResultPort = 12348; [Tooltip("Python结果接收绑定IP(默认127.0.0.1)")] public string pythonResultBindIp = "127.0.0.1"; [Tooltip("结果队列最大大小(只保留最新N个结果)")] public int maxResultQueueSize = 10; [Header("Image Transmission Config")] [Tooltip("图像传输配置(运行时唯一来源:可由 LightGlueManager.defaultTransmissionConfig 或 ImageTransmissionUIController 写入)")] public ImageTransmissionConfig transmissionConfig; public enum TransferMode { Stdin, // 通过stdin直接传递(推荐,无需UDP服务器) Udp // 通过UDP传递(传统模式) } /// 基准图来源:仅 N 键摄像头帧 / Python 截屏 / Unity 游戏画面 public enum ReferenceSource { CameraOnly, // 仅通过 N 键将当前摄像头帧设为基准图 ScreenCapture, // Python 端截取整屏作为基准图(按刷新频率发 s) GameView // Unity 将当前游戏画面发给 Python 作为基准图(按刷新频率发 r+图像) } private UDPJpegReceiver _receiver; private UdpClient _udpSender; // Unity->Python UDP 模式 private IPEndPoint _pythonEndpoint; private StreamWriter _stdinWriter; // Stdin 模式 private UDPResultReceiver _resultReceiver; // Python->Unity 结果接收 // 硬件控制:Unity -> 硬件 0x40 参数设置(与 JPEG 接收/转发独立) private UdpClient _hwControlClient; private IPEndPoint _hwControlEndpoint; // Python 控制:Unity -> Python 控制指令(刷新参考图) private UdpClient _pyControlClient; private IPEndPoint _pyControlEndpoint; // 基准图刷新(仅 GameView / ScreenCapture 时按间隔触发) private float _lastReferenceRefreshTime; private byte[] _pendingReferenceImageBytes; // 传输控制 private float _lastSendTime; private bool _configEnabled = true; // 最新图片缓存(供Viewer使用) private byte[] _latestJpeg; private object _latestJpegLock = new object(); private bool _autoHardwareConfigApplied; private bool _autoHardwareConfigReady; private bool _pythonStartedByFirstFrame; /// /// 获取最新的JPEG图片数据(供Viewer等组件使用) /// public byte[] GetLatestJpeg() { lock (_latestJpegLock) { return _latestJpeg != null ? (byte[])_latestJpeg.Clone() : null; } } /// /// 获取接收器(供Viewer共享使用) /// public UDPJpegReceiver Receiver => _receiver; /// /// 获取结果接收器(供其他组件使用) /// public UDPResultReceiver ResultReceiver => _resultReceiver; /// /// 尝试获取最新的算法结果(非阻塞) /// 每帧会排空结果队列并只使用最后一次取到的结果,避免队列积压导致显示旧位置(延迟移动)。 /// public bool TryGetLatestResult(out LightGlueResult result) { result = default(LightGlueResult); if (_resultReceiver == null) return false; bool any = false; while (_resultReceiver.TryDequeueResult(out var r)) { result = r; any = true; } return any; } private void Start() { // 自动加载网络配置 if (autoLoadNetworkConfig) { LoadNetworkConfig(); } // 在Start中初始化,确保所有组件都已启用 // 如果使用stdin模式且Python进程控制器已配置但未运行,尝试启动 if (transferMode == TransferMode.Stdin && pythonProcessController != null && !pythonProcessController.IsRunning) { if (pythonProcessController.autoStartOnEnable) { // 如果配置了自动启动,但还没启动,可能是启动顺序问题,延迟一点再检查 StartCoroutine(DelayedStdinCheck()); } else { Debug.LogWarning("[Bridge] Python process controller is set but not running. " + "Please ensure 'Auto Start On Enable' is enabled or manually start Python process."); } } } /// /// 从NetworkConfigManager加载网络配置 /// private void LoadNetworkConfig() { try { NetworkConfig config = NetworkConfigManager.LoadConfig(); if (config != null && config.Validate()) { hardwareBindIp = config.hardwareBindIp; hardwarePort = config.hardwarePort; hardwareTimeoutSeconds = config.hardwareTimeoutSeconds; pythonIp = config.pythonIp; pythonPort = config.pythonPort; pythonResultBindIp = config.pythonResultBindIp; pythonResultPort = config.pythonResultPort; hardwareControlIp = config.hardwareControlIp; hardwareControlPort = config.hardwareControlPort; Debug.Log($"[Bridge] 已从配置文件加载网络配置: hardwareBindIp={hardwareBindIp}, hardwarePort={hardwarePort}, pythonIp={pythonIp}, pythonPort={pythonPort}, pythonResultPort={pythonResultPort}, hardwareControlIp={hardwareControlIp}, hardwareControlPort={hardwareControlPort}"); } } catch (System.Exception ex) { Debug.LogWarning($"[Bridge] 加载网络配置失败,使用Inspector中的默认值: {ex.Message}"); } } private System.Collections.IEnumerator DelayedStdinCheck() { // 等待一帧,让Python进程有时间启动 yield return null; TryConnectStdin(); } private void OnEnable() { // 游戏全屏时保持收发 UDP/进程通信,避免因失去焦点被系统节流导致 Python 端卡顿 Application.runInBackground = true; _lastReferenceRefreshTime = Time.time; _autoHardwareConfigApplied = false; _autoHardwareConfigReady = false; _autoHardwareConfigApplied = false; // 创建 receiver 前先加载网络配置,保证首次运行与“应用配置重启”使用同一绑定地址,避免预览在重启后因 IP 不一致断流 if (autoLoadNetworkConfig) { LoadNetworkConfig(); } // 参考图 resize:与 Python --resize 一致 ReferenceImageResizeService.EnsureInitialized(); referenceCaptureWidth = ReferenceImageResizeService.Width; referenceCaptureHeight = ReferenceImageResizeService.Height; ReferenceImageResizeService.OnResizeChanged -= OnResizeChanged; ReferenceImageResizeService.OnResizeChanged += OnResizeChanged; // Hardware -> Unity receiver(端口占用时捕获异常,避免崩溃) _receiver = new UDPJpegReceiver(hardwareBindIp, hardwarePort, hardwareTimeoutSeconds, maxQueuedFrames); try { _receiver.Start(); } catch (SocketException ex) { Debug.LogError($"[Bridge] 无法绑定硬件 JPEG 接收端口 {hardwareBindIp}:{hardwarePort},可能已被占用或存在重复的 LightGlueSystem。请确保场景中只通过 GameManager 创建一份预制,且未手动放置重复对象。\n{ex.Message}"); try { _receiver?.Stop(); } catch { /* ignore */ } _receiver = null; } // Unity -> Python sender (根据模式选择) if (transferMode == TransferMode.Stdin) { TryConnectStdin(); } else { // UDP模式(传统) var ip = IPAddress.Parse(string.IsNullOrWhiteSpace(pythonIp) ? "127.0.0.1" : pythonIp); _pythonEndpoint = new IPEndPoint(ip, pythonPort); _udpSender = new UdpClient(); Debug.Log($"[Bridge] Hardware -> Unity listening on {hardwareBindIp}:{hardwarePort}"); Debug.Log($"[Bridge] Unity -> Python: UDP mode sending to {_pythonEndpoint.Address}:{_pythonEndpoint.Port}"); } // Unity -> 硬件:0x40 参数设置 UDP 客户端(与 JPEG 接收/转发独立) try { string ctrlIp = string.IsNullOrWhiteSpace(hardwareControlIp) ? "192.168.0.106" : hardwareControlIp; var ip = IPAddress.Parse(ctrlIp); _hwControlEndpoint = new IPEndPoint(ip, hardwareControlPort); _hwControlClient = new UdpClient(); Debug.Log($"[Bridge] 硬件控制 UDP 已初始化 -> {_hwControlEndpoint.Address}:{_hwControlEndpoint.Port}(发送 0x40 参数设置到硬件)"); } catch (Exception ex) { Debug.LogError($"[Bridge] 硬件控制 UDP 初始化失败: {ex.Message}"); _hwControlClient = null; } // Unity -> Python 控制:简单控制指令(例如刷新参考图) if (enablePythonControl) { try { string targetIp = string.IsNullOrWhiteSpace(pythonIp) ? "127.0.0.1" : pythonIp; var ip = IPAddress.Parse(targetIp); _pyControlEndpoint = new IPEndPoint(ip, pythonControlPort); _pyControlClient = new UdpClient(); Debug.Log($"[Bridge] Python 控制 UDP 已初始化 -> {_pyControlEndpoint.Address}:{_pyControlEndpoint.Port}(发送刷新参考图等控制指令)"); } catch (Exception ex) { Debug.LogError($"[Bridge] Python 控制 UDP 初始化失败: {ex.Message}"); _pyControlClient = null; } } // Python -> Unity result receiver if (enableResultReceiver) { try { _resultReceiver = new UDPResultReceiver(pythonResultBindIp, pythonResultPort, maxResultQueueSize); _resultReceiver.Start(); Debug.Log($"[Bridge] Python -> Unity: Result receiver started on {pythonResultBindIp}:{pythonResultPort}"); } catch (Exception ex) { // 端口占用或绑定失败时给出明确提示,避免与其它进程(例如 YejiDemo)抢占同一结果端口。 Debug.LogError($"[Bridge] Failed to start result receiver on {pythonResultBindIp}:{pythonResultPort}. " + "The UDP port may already be in use by another process. " + $"Exception: {ex.Message}"); try { _resultReceiver?.Stop(); } catch { // ignore } _resultReceiver = null; } } } private void OnDestroy() { ReferenceImageResizeService.OnResizeChanged -= OnResizeChanged; } private void OnResizeChanged(int w, int h) { referenceCaptureWidth = w; referenceCaptureHeight = h; } private void TryConnectStdin() { // Stdin模式:从Python进程控制器获取stdin写入流 if (pythonProcessController != null) { if (pythonProcessController.IsRunning) { _stdinWriter = pythonProcessController.StdinWriter; if (_stdinWriter != null) { Debug.Log($"[Bridge] Hardware -> Unity listening on {hardwareBindIp}:{hardwarePort}"); Debug.Log($"[Bridge] Unity -> Python: Stdin mode connected (direct process communication)"); } else { Debug.LogWarning("[Bridge] Python process stdin not available. Make sure Python process is started."); } } else { Debug.LogWarning("[Bridge] Python process controller is set but Python process is not running. " + "Will retry in Update(). Make sure 'Auto Start On Enable' is enabled in PythonProcessController."); } } else { Debug.LogWarning("[Bridge] Python process controller not set. Please assign PythonProcessController component in Inspector."); } } private void OnDisable() { try { _receiver?.Stop(); } catch { /* ignore */ } _receiver = null; try { _resultReceiver?.Stop(); } catch { /* ignore */ } _resultReceiver = null; try { _hwControlClient?.Close(); } catch { /* ignore */ } _hwControlClient = null; try { _pyControlClient?.Close(); } catch { /* ignore */ } _pyControlClient = null; _stdinWriter = null; try { _udpSender?.Close(); } catch { /* ignore */ } _udpSender = null; _pythonStartedByFirstFrame = false; ReferenceImageResizeService.OnResizeChanged -= OnResizeChanged; } /// /// 收到首帧图像时调用:若启用「首帧后再启动 Python」则启动一次,之后不再重复。 /// private void TryStartPythonOnFirstFrame() { if (_pythonStartedByFirstFrame) return; if (!startPythonOnFirstImageReceived || pythonProcessController == null || pythonProcessController.IsRunning) return; if (pythonProcessController.startOnFirstImageReceived) { pythonProcessController.StartPython(); _pythonStartedByFirstFrame = true; Debug.Log("[Bridge] 已收到首帧图像,启动 Python 进程。"); } } // ---------- 硬件控制:下发 0x40 参数设置(供 ImageTransmissionUIController 等调用) ---------- /// /// 将图像传输配置以 0x40 协议帧下发给硬件(分辨率/质量/间隔/开关)。 /// 调用时机:用户点击“应用配置”等。 /// public void ApplyConfig(ImageTransmissionConfig config) { if (_hwControlClient == null || _hwControlEndpoint == null || config == null) { Debug.LogWarning("[Bridge] 硬件控制未就绪或 config 为空,无法发送 0x40"); return; } try { byte[] frame = BuildParameterSettingFrame(config); if (frame != null && frame.Length > 0) { _hwControlClient.Send(frame, frame.Length, _hwControlEndpoint); Debug.Log($"[Bridge] 已发送 0x40 参数帧 -> {_hwControlEndpoint.Address}:{_hwControlEndpoint.Port}, enable={config.enableImageTransmission}, res=0x{config.GetHardwareResolutionCode():X2}, quality=0x{config.GetHardwareQuality():X2}, interval={config.reportIntervalMs}ms"); } } catch (SocketException ex) { Debug.LogWarning($"[Bridge] 硬件控制 UDP 发送异常: {ex.SocketErrorCode} {ex.Message}"); } catch (Exception ex) { Debug.LogWarning($"[Bridge] 硬件控制发送失败: {ex.Message}"); } } /// /// 构建 0x40 参数设置帧(报头 0x7B + 类型 0x40 + 长度 + 总包/分包 + 标志 0xFF + 数据 20 字节 + 校验 + 报尾 0x7D) /// private static byte[] BuildParameterSettingFrame(ImageTransmissionConfig config) { if (config == null) return Array.Empty(); const byte HEADER = 0x7B; const byte PROTOCOL_TYPE = 0x40; const byte TOTAL_PACKETS = 1; const byte PACKET_INDEX = 1; const byte FLAG = 0xFF; const byte FOOTER = 0x7D; const int DATA_SIZE = 20; byte[] data = new byte[DATA_SIZE]; data[0] = (byte)(config.enableImageTransmission ? 1 : 0); data[1] = 0; data[2] = config.GetHardwareResolutionCode(); data[3] = config.GetHardwareQuality(); data[4] = (byte)Mathf.Clamp(config.reportIntervalMs, 0, 255); ushort dataLength = (ushort)DATA_SIZE; byte[] frame = new byte[29]; int offset = 0; frame[offset++] = HEADER; frame[offset++] = PROTOCOL_TYPE; frame[offset++] = (byte)(dataLength & 0xFF); frame[offset++] = (byte)((dataLength >> 8) & 0xFF); frame[offset++] = TOTAL_PACKETS; frame[offset++] = PACKET_INDEX; frame[offset++] = FLAG; Buffer.BlockCopy(data, 0, frame, offset, data.Length); offset += data.Length; byte checksum = 0; for (int i = 1; i < offset; i++) checksum = (byte)(checksum + frame[i]); frame[offset++] = checksum; frame[offset++] = FOOTER; return frame; } private void Update() { if (_receiver == null) return; // 处理来自 Unity 的刷新参考图:N 键 或 按间隔发送 s/r if (enablePythonControl && _pyControlClient != null && _pyControlEndpoint != null) { if (Input.GetKeyDown(refreshReferenceKey)) { SendControlCommand('n'); } else if (referenceSource == ReferenceSource.ScreenCapture || referenceSource == ReferenceSource.GameView) { float now = Time.time; if (now - _lastReferenceRefreshTime >= referenceRefreshIntervalSeconds) { _lastReferenceRefreshTime = now; if (referenceSource == ReferenceSource.ScreenCapture) { SendControlCommand('s'); } else { byte[] gameViewJpeg = CaptureGameViewToJpeg(); if (gameViewJpeg != null && gameViewJpeg.Length > 0) { _pendingReferenceImageBytes = gameViewJpeg; SendControlCommand('r'); } } } } } // 检查发送端是否可用(根据模式) if (transferMode == TransferMode.Stdin) { if (_stdinWriter == null) { // 尝试重新获取stdin写入流(如果Python进程刚启动) if (pythonProcessController != null && pythonProcessController.IsRunning) { _stdinWriter = pythonProcessController.StdinWriter; } if (_stdinWriter == null) return; } } else { if (_udpSender == null) return; } // 检查配置是否启用传输 bool shouldTransmit = _configEnabled; if (transmissionConfig != null) { shouldTransmit = transmissionConfig.enableImageTransmission; } if (!shouldTransmit) { byte[] last = null; while (_receiver.TryDequeueJpeg(out var j)) { last = j; } if (last != null) { lock (_latestJpegLock) { _latestJpeg = last; } TryStartPythonOnFirstFrame(); TryAutoApplyHardwareConfigOnce(); } return; } float currentTime = Time.time * 1000f; bool hasPendingReference = _pendingReferenceImageBytes != null; if (!hasPendingReference && transmissionConfig != null) { float intervalMs = transmissionConfig.reportIntervalMs; if (intervalMs > 0 && (currentTime - _lastSendTime) < intervalMs) { byte[] last = null; while (_receiver.TryDequeueJpeg(out var j)) { last = j; } if (last != null) { lock (_latestJpegLock) { _latestJpeg = last; } TryStartPythonOnFirstFrame(); TryAutoApplyHardwareConfigOnce(); } return; } } byte[] toSend = null; if (_pendingReferenceImageBytes != null) { toSend = _pendingReferenceImageBytes; _pendingReferenceImageBytes = null; } else { byte[] latest = null; while (_receiver.TryDequeueJpeg(out var jpeg)) latest = jpeg; if (latest == null) return; TryStartPythonOnFirstFrame(); TryAutoApplyHardwareConfigOnce(); toSend = latest; lock (_latestJpegLock) { _latestJpeg = latest; } } byte[] processedImage = ProcessImage(toSend); // 仅在 Python 已就绪接收时发送,避免 TensorRT 编译期间发图导致 UDP 缓冲堆积、后续解码损坏 if (pythonProcessController != null && !pythonProcessController.IsReadyToReceiveFrames) return; try { if (transferMode == TransferMode.Stdin) { if (_stdinWriter != null) { _stdinWriter.BaseStream.Write(processedImage, 0, processedImage.Length); _stdinWriter.BaseStream.Flush(); _lastSendTime = currentTime; } } else { _udpSender.Send(processedImage, processedImage.Length, _pythonEndpoint); _lastSendTime = currentTime; } } catch (SocketException ex) { Debug.LogWarning($"[Bridge] UDP send error: {ex.SocketErrorCode} {ex.Message}"); } catch (IOException ex) { Debug.LogWarning($"[Bridge] Stdin write error: {ex.Message}"); _stdinWriter = null; } } /// /// 标记“自动下发硬件图像配置”的前置条件已就绪(由 ImageTransmissionUIController 初始化完成后调用)。 /// public void MarkHardwareAutoApplyReady() { _autoHardwareConfigReady = true; } private void TryAutoApplyHardwareConfigOnce() { if (_autoHardwareConfigApplied) return; if (!autoApplyHardwareConfigOnFirstFrame) return; if (!_autoHardwareConfigReady) return; if (transmissionConfig == null) return; if (_hwControlClient == null || _hwControlEndpoint == null) return; // 注意:ApplyConfig 内部会做一次基本校验并捕获异常;此处只要硬件控制已就绪就认为可尝试下发。 ApplyConfig(transmissionConfig); _autoHardwareConfigApplied = true; Debug.Log("[Bridge] 已在检测到硬件首帧后自动下发一次 0x40 图像配置。"); } /// /// 根据配置处理图像(分辨率调整和质量压缩) /// 注意:Unity的ImageConversion.EncodeToJPG需要Texture2D,这里先返回原始数据 /// 如果需要真正的resize和quality控制,需要额外的图像处理库 /// private byte[] ProcessImage(byte[] jpegBytes) { if (transmissionConfig == null) return jpegBytes; // TODO: 实现图像resize和quality压缩 // Unity原生不支持JPEG的resize和quality调整,需要: // 1. 使用Texture2D.LoadImage解码JPEG // 2. 使用Texture2D.GetPixels/SetPixels进行resize // 3. 使用ImageConversion.EncodeToJPG进行质量压缩 // 但这个过程可能较慢,建议在Python端处理 // 当前实现:直接返回原始数据,配置参数可用于Python端处理 // 如果需要Unity端处理,可以在这里添加图像处理逻辑 return jpegBytes; } /// /// 设置传输配置(供UI控制器调用) /// public void SetTransmissionConfig(ImageTransmissionConfig config) { transmissionConfig = config; _configEnabled = config != null && config.enableImageTransmission; Debug.Log($"[Bridge] 传输配置已更新: 分辨率={config?.GetResolutionString()}, 质量={config?.quality}, 间隔={config?.reportIntervalMs}ms, 启用={_configEnabled}"); // 配置一旦设置就立即下发 0x40,避免硬件用默认参数发图导致首段画面不完整、闪烁,直到用户手动点「应用」才正常 if (config != null && _hwControlClient != null && _hwControlEndpoint != null) { ApplyConfig(config); } } private void SendControlCommand(char cmd) { if (_pyControlClient == null || _pyControlEndpoint == null) return; try { byte[] payload = { (byte)cmd }; _pyControlClient.Send(payload, payload.Length, _pyControlEndpoint); } catch (Exception ex) { Debug.LogWarning($"[Bridge] Python 控制指令发送失败: {ex.Message}"); } } private byte[] CaptureGameViewToJpeg() { Camera cam = gameViewCamera != null ? gameViewCamera : Camera.main; if (cam == null) return null; int w = Mathf.Clamp(referenceCaptureWidth, 64, 4096); int h = Mathf.Clamp(referenceCaptureHeight, 64, 4096); RenderTexture rt = RenderTexture.GetTemporary(w, h, 24, RenderTextureFormat.ARGB32); RenderTexture prevTarget = cam.targetTexture; RenderTexture prevActive = RenderTexture.active; cam.targetTexture = rt; cam.Render(); RenderTexture.active = rt; Texture2D tex = new Texture2D(w, h, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, w, h), 0, 0); tex.Apply(); cam.targetTexture = prevTarget; RenderTexture.active = prevActive; RenderTexture.ReleaseTemporary(rt); int quality = transmissionConfig != null ? Mathf.Clamp(transmissionConfig.quality, 1, 100) : 80; byte[] jpeg = tex.EncodeToJPG(quality); Destroy(tex); return jpeg; } } }