using System; using System.IO; using System.Net; using System.Net.Sockets; using LightGlue.Unity.Config; using LightGlue.Unity.Networking; using LightGlue.Unity.Python; using LightGlue.Unity.Sdk.Core.Networking; using UnityEngine; namespace LightGlue.Unity.Sdk.Core { /// /// SDK 原型版:对外统一的 LightGlue 通信客户端。 /// - 组合 JPEG 接收、Python 结果接收、硬件控制 0x40 发送、Unity->Python 图像发送。 /// - 非 MonoBehaviour,由外部(如 BridgeBehaviour)驱动生命周期和 Tick。 /// public sealed class LightGlueClient : IDisposable { public enum TransferMode { Stdin, Udp } public sealed class Options { // Hardware -> Unity image stream public string HardwareBindIp = "0.0.0.0"; public int HardwarePort = 12346; public float HardwareTimeoutSeconds = 2.0f; public int MaxQueuedFrames = 2; // Unity -> Hardware control (0x40) public string HardwareControlIp = "192.168.0.106"; public int HardwareControlPort = 8888; // Unity -> Python image public TransferMode Mode = TransferMode.Stdin; public string PythonIp = "127.0.0.1"; public int PythonPort = 12347; public PythonProcessController PythonController; // Python -> Unity result public bool EnableResultReceiver = true; public string PythonResultBindIp = "127.0.0.1"; public int PythonResultPort = 12348; public int MaxResultQueueSize = 10; } private readonly Options _options; private CoreUDPJpegReceiver _jpegReceiver; private CoreUDPResultReceiver _resultReceiver; private UdpClient _pythonUdpSender; private IPEndPoint _pythonEndpoint; private UdpClient _hwControlClient; private IPEndPoint _hwControlEndpoint; private PythonProcessController _pythonController; private StreamWriter _stdinWriter; private ImageTransmissionConfig _transmissionConfig; private bool _configEnabled = true; private byte[] _latestJpeg; private readonly object _jpegLock = new object(); private float _lastSendTimeMs; public bool IsRunning { get; private set; } public LightGlueClient(Options options) { _options = options ?? throw new ArgumentNullException(nameof(options)); } /// /// 启动所有内部模块(JPEG 接收、结果接收、硬件控制 UDP、Python 图像发送端)。 /// public void Start() { if (IsRunning) return; // JPEG receiver _jpegReceiver = new CoreUDPJpegReceiver( _options.HardwareBindIp, _options.HardwarePort, _options.HardwareTimeoutSeconds, _options.MaxQueuedFrames); try { _jpegReceiver.Start(); } catch (SocketException ex) { Debug.LogError($"[SDK][Client] Failed to bind JPEG receiver on {_options.HardwareBindIp}:{_options.HardwarePort}: {ex.Message}"); SafeStop(ref _jpegReceiver); throw; } // Unity -> Python sender if (_options.Mode == TransferMode.Stdin) { _pythonController = _options.PythonController; TryConnectStdin(); } else { var ip = IPAddress.Parse(string.IsNullOrWhiteSpace(_options.PythonIp) ? "127.0.0.1" : _options.PythonIp); _pythonEndpoint = new IPEndPoint(ip, _options.PythonPort); _pythonUdpSender = new UdpClient(); Debug.Log($"[SDK][Client] Unity -> Python UDP sender to {_pythonEndpoint.Address}:{_pythonEndpoint.Port}"); } // Hardware control (0x40) try { string ctrlIp = string.IsNullOrWhiteSpace(_options.HardwareControlIp) ? "192.168.0.106" : _options.HardwareControlIp; var ip = IPAddress.Parse(ctrlIp); _hwControlEndpoint = new IPEndPoint(ip, _options.HardwareControlPort); _hwControlClient = new UdpClient(); Debug.Log($"[SDK][Client] Hardware control UDP -> {_hwControlEndpoint.Address}:{_hwControlEndpoint.Port}"); } catch (Exception ex) { Debug.LogError($"[SDK][Client] Hardware control UDP init failed: {ex.Message}"); _hwControlClient = null; } // Result receiver if (_options.EnableResultReceiver) { try { _resultReceiver = new CoreUDPResultReceiver( _options.PythonResultBindIp, _options.PythonResultPort, _options.MaxResultQueueSize); _resultReceiver.Start(); Debug.Log($"[SDK][Client] Result receiver on {_options.PythonResultBindIp}:{_options.PythonResultPort}"); } catch (Exception ex) { Debug.LogError($"[SDK][Client] Result receiver init failed: {ex.Message}"); SafeStop(ref _resultReceiver); _resultReceiver = null; } } IsRunning = true; } public void Stop() { if (!IsRunning) return; SafeStop(ref _jpegReceiver); SafeStop(ref _resultReceiver); try { _hwControlClient?.Close(); } catch { /* ignore */ } _hwControlClient = null; try { _pythonUdpSender?.Close(); } catch { /* ignore */ } _pythonUdpSender = null; _stdinWriter = null; _pythonController = null; IsRunning = false; } private static void SafeStop(ref T disposable) where T : class, IDisposable { if (disposable == null) return; try { disposable.Dispose(); } catch { /* ignore */ } disposable = null; } private void TryConnectStdin() { if (_pythonController == null) { Debug.LogWarning("[SDK][Client] PythonProcessController is null, cannot use stdin mode."); return; } if (_pythonController.IsRunning) { _stdinWriter = _pythonController.StdinWriter; if (_stdinWriter != null) { Debug.Log("[SDK][Client] Connected to Python stdin."); } else { Debug.LogWarning("[SDK][Client] Python stdin writer is null."); } } else { Debug.LogWarning("[SDK][Client] Python process not running, stdin mode will be retried later."); } } /// /// 设置图像传输配置(仅影响 Unity->Python 图像流,不影响硬件 0x40 配置)。 /// public void SetTransmissionConfig(ImageTransmissionConfig config) { _transmissionConfig = config; _configEnabled = config != null && config.enableImageTransmission; } /// /// 下发硬件图像参数(0x40 帧),通常在 UI 点击“应用配置”时调用。 /// public void ApplyHardwareConfig(ImageTransmissionConfig config) { if (_hwControlClient == null || _hwControlEndpoint == null || config == null) { Debug.LogWarning("[SDK][Client] Hardware control not ready or config null, cannot send 0x40 frame."); return; } try { byte[] frame = CoreHardwareControlProtocol.BuildParameterSettingFrame(config); if (frame != null && frame.Length > 0) { _hwControlClient.Send(frame, frame.Length, _hwControlEndpoint); Debug.Log($"[SDK][Client] Sent 0x40 frame -> {_hwControlEndpoint.Address}:{_hwControlEndpoint.Port}"); } } catch (SocketException ex) { Debug.LogWarning($"[SDK][Client] Hardware control UDP send error: {ex.SocketErrorCode} {ex.Message}"); } catch (Exception ex) { Debug.LogWarning($"[SDK][Client] Hardware control send failed: {ex.Message}"); } } /// /// 每帧在 Unity 的 Update 中调用,用于驱动 JPEG 队列消费并发送到 Python。 /// public void Tick() { if (!IsRunning || _jpegReceiver == null) return; // 确保发送端可用 if (_options.Mode == TransferMode.Stdin) { if (_stdinWriter == null) { if (_pythonController != null && _pythonController.IsRunning) { _stdinWriter = _pythonController.StdinWriter; } if (_stdinWriter == null) return; } } else { if (_pythonUdpSender == null || _pythonEndpoint == null) return; } bool shouldTransmit = _configEnabled; if (_transmissionConfig != null) { shouldTransmit = _transmissionConfig.enableImageTransmission; } if (!shouldTransmit) { while (_jpegReceiver.TryDequeueJpeg(out _)) { } return; } float nowMs = Time.time * 1000f; if (_transmissionConfig != null) { float intervalMs = _transmissionConfig.reportIntervalMs; if (intervalMs > 0 && (nowMs - _lastSendTimeMs) < intervalMs) { while (_jpegReceiver.TryDequeueJpeg(out _)) { } return; } } byte[] latest = null; while (_jpegReceiver.TryDequeueJpeg(out var jpeg)) { latest = jpeg; } if (latest == null) return; lock (_jpegLock) { _latestJpeg = latest; } byte[] processed = ProcessImage(latest); try { if (_options.Mode == TransferMode.Stdin) { if (_stdinWriter != null) { _stdinWriter.BaseStream.Write(processed, 0, processed.Length); _stdinWriter.BaseStream.Flush(); _lastSendTimeMs = nowMs; } } else { _pythonUdpSender.Send(processed, processed.Length, _pythonEndpoint); _lastSendTimeMs = nowMs; } } catch (SocketException ex) { Debug.LogWarning($"[SDK][Client] UDP send error: {ex.SocketErrorCode} {ex.Message}"); } catch (IOException ex) { Debug.LogWarning($"[SDK][Client] Stdin write error: {ex.Message}"); _stdinWriter = null; } } private byte[] ProcessImage(byte[] jpegBytes) { if (_transmissionConfig == null) return jpegBytes; // 目前保持与现有 Bridge 一致:不在 Unity 端做 resize / 质量压缩,直接转发。 return jpegBytes; } public byte[] GetLatestJpegCopy() { lock (_jpegLock) { return _latestJpeg != null ? (byte[])_latestJpeg.Clone() : null; } } public bool TryGetLatestResult(out LightGlueResult result) { if (_resultReceiver != null) { return _resultReceiver.TryDequeueResult(out result); } result = default(LightGlueResult); return false; } public void Dispose() { Stop(); } } }