using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using UnityEngine; namespace LightGlue.Unity.Roma.Networking { /// /// 接收 Python 上报的设备信息(JSON over UDP)。 /// 典型用途:ESP32 无广播时,从 Python 收图线程“学到”的 source_ip/source_port 回填到 Unity UI。 /// JSON 示例:{"device_ip":"192.168.0.16","device_port":12346,"ts":1710000000.0} /// public sealed class RomaDeviceInfoReceiver : MonoBehaviour { [Header("Listen")] public string bindIp = "0.0.0.0"; public int port = 12350; public bool autoStartOnEnable = true; [Header("State (readonly)")] [SerializeField] private string latestDeviceIp; [SerializeField] private int latestDevicePort; [SerializeField] private bool hasLatest; public string LatestDeviceIp => latestDeviceIp; public int LatestDevicePort => latestDevicePort; public bool HasLatest => hasLatest; public event Action OnDeviceInfoUpdated; private UdpClient _client; private Thread _thread; private volatile bool _running; private readonly object _lock = new object(); private string _pendingIp; private int _pendingPort; private bool _hasPending; [Serializable] private class DeviceInfoMsg { public string device_ip; public int device_port; public double ts; } private void OnEnable() { if (autoStartOnEnable) StartListening(); } private void OnDisable() { StopListening(); } private void Update() { string ip = null; int p = 0; bool has = false; lock (_lock) { if (_hasPending) { ip = _pendingIp; p = _pendingPort; has = true; _hasPending = false; } } if (!has || string.IsNullOrWhiteSpace(ip)) return; latestDeviceIp = ip; latestDevicePort = p; hasLatest = true; OnDeviceInfoUpdated?.Invoke(ip, p); } public void StartListening() { if (_running) return; try { var ip = IPAddress.Parse(string.IsNullOrWhiteSpace(bindIp) ? "0.0.0.0" : bindIp); _client = new UdpClient(new IPEndPoint(ip, port)); _client.Client.ReceiveTimeout = 200; _running = true; _thread = new Thread(ReceiveLoop) { IsBackground = true, Name = "RomaDeviceInfoReceiver" }; _thread.Start(); Debug.Log($"[RomaDevInfo] Listening on {bindIp}:{port}"); } catch (Exception ex) { Debug.LogWarning($"[RomaDevInfo] Start failed: {ex.Message}"); Cleanup(); } } public void StopListening() { _running = false; try { _client?.Close(); } catch { /* ignore */ } try { _client?.Dispose(); } catch { /* ignore */ } _client = null; if (_thread != null && _thread.IsAlive) { if (!_thread.Join(500)) { try { _thread.Interrupt(); } catch { /* ignore */ } } } _thread = null; } private void ReceiveLoop() { var remote = new IPEndPoint(IPAddress.Any, 0); while (_running) { try { byte[] data = _client.Receive(ref remote); if (data == null || data.Length == 0) continue; string json = Encoding.UTF8.GetString(data); DeviceInfoMsg msg = JsonUtility.FromJson(json); if (msg == null || string.IsNullOrWhiteSpace(msg.device_ip)) continue; int p = msg.device_port; lock (_lock) { _pendingIp = msg.device_ip; _pendingPort = p; _hasPending = true; } } catch (SocketException) { continue; } catch (ObjectDisposedException) { break; } catch (ThreadInterruptedException) { break; } catch (Exception ex) { Debug.LogWarning($"[RomaDevInfo] Receive error: {ex.Message}"); } } } private void Cleanup() { _running = false; try { _client?.Close(); } catch { /* ignore */ } try { _client?.Dispose(); } catch { /* ignore */ } _client = null; } } }