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;
}
}
}