RomaHardwareToPythonBridge.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. using System;
  2. using System.IO;
  3. using System.Net;
  4. using System.Net.Sockets;
  5. using LightGlue.Unity.Config;
  6. using LightGlue.Unity.Networking;
  7. using LightGlue.Unity.Python;
  8. using LightGlue.Unity.Sdk.Core.Networking;
  9. using UnityEngine;
  10. namespace LightGlue.Unity.Roma.Bridge
  11. {
  12. /// <summary>
  13. /// Roma 版本的桥接层:目标是最大化复用旧 HardwareToPythonUdpBridge 的“能力形态”,但核心差异是:
  14. /// - 硬件图像不再先到 Unity;Python 直接监听硬件 FrameHeader 图传端口(例如 9000)
  15. ///
  16. /// 保留/提供的能力:
  17. /// - Unity -> Hardware:通过 RomaWifiCameraControlClient 发送 config/stream_on/stream_off
  18. /// - Unity -> Python:控制指令 n/s/r(pythonControlPort=12349)
  19. /// - Unity -> Python:可选发送 Unity GameView JPEG(单包 UDP,unityRefPort=12347),用于 r 参考图更新
  20. /// - Python -> Unity:结果接收 UDPResultReceiver(12348)
  21. /// - Python 进程:PythonProcessController 启动/停止
  22. /// - Unity 显示:可选启动 RomaForwardedImageViewer(12366)
  23. /// </summary>
  24. public sealed class RomaHardwareToPythonBridge : MonoBehaviour
  25. {
  26. [Header("配置管理")]
  27. public bool autoLoadNetworkConfig = true;
  28. [Header("Discovery + WiFi Control (Unity -> Hardware)")]
  29. public RomaDeviceDiscoveryListener discovery;
  30. public RomaWifiCameraControlClient wifiControl;
  31. [Header("ESP32 Hardware Control (Unity -> ESP32, 0x40)")]
  32. [Tooltip("是否启用 ESP32 硬件 0x40 参数下发(供 ImageTransmissionUIController 调用)。")]
  33. public bool enableEsp32HardwareControl = true;
  34. [Tooltip("ESP32 设备IP(用于发送 0x40 参数帧)。默认从 NetworkConfig.romaEsp32DeviceIp 加载。")]
  35. public string esp32DeviceIp = "";
  36. [Tooltip("ESP32 设备端口(用于发送 0x40 参数帧),默认 8888。")]
  37. public int esp32DevicePort = 8888;
  38. [Header("Python Process")]
  39. public PythonProcessController pythonProcessController;
  40. [Header("Python Control (Unity -> Python)")]
  41. public bool enablePythonControl = true;
  42. public string pythonIp = "127.0.0.1";
  43. public int pythonControlPort = 12349;
  44. [Header("Unity GameView Reference (optional)")]
  45. public bool enableUnityGameViewReference = true;
  46. public int unityRefPort = 12347;
  47. public Camera gameViewCamera;
  48. public int referenceCaptureWidth = 320;
  49. public int referenceCaptureHeight = 240;
  50. [Range(1, 100)] public int referenceJpegQuality = 80;
  51. [Header("Reference Source")]
  52. public ReferenceSource referenceSource = ReferenceSource.CameraOnly;
  53. [Range(0.01f, 60f)] public float referenceRefreshIntervalSeconds = 5f;
  54. [Header("Python Result Receiver (Python -> Unity)")]
  55. public bool enableResultReceiver = true;
  56. [Tooltip("是否在 OnEnable 自动启动结果接收(手动流程建议关闭,避免端口冲突)。")]
  57. public bool autoStartResultReceiverOnEnable = false;
  58. public string pythonResultBindIp = "127.0.0.1";
  59. public int pythonResultPort = 12348;
  60. public int maxResultQueueSize = 10;
  61. [Header("Debug")]
  62. [Tooltip("收到结果时是否打印日志(限流)。")]
  63. public bool logResults = true;
  64. [Min(0.1f)]
  65. public float logResultIntervalSeconds = 1.0f;
  66. [Header("Forwarded Image Viewer (Python -> Unity image)")]
  67. public RomaForwardedImageViewer forwardedViewer;
  68. public enum ReferenceSource
  69. {
  70. CameraOnly, // 仅手动触发 n
  71. ScreenCapture, // 周期性触发 s(Python 截屏)
  72. GameView // 周期性发送 GameView JPEG + 触发 r
  73. }
  74. private UDPResultReceiver _resultReceiver;
  75. public event Action<LightGlueResult> OnResultUpdated;
  76. private LightGlueResult _latest;
  77. private bool _hasLatest;
  78. public bool HasLatestResult => _hasLatest;
  79. public LightGlueResult LatestResult => _latest;
  80. private UdpClient _pyControlClient;
  81. private IPEndPoint _pyControlEndpoint;
  82. private UdpClient _unityRefSender;
  83. private IPEndPoint _unityRefEndpoint;
  84. private UdpClient _esp32ControlClient;
  85. private IPEndPoint _esp32ControlEndpoint;
  86. private float _lastReferenceRefreshTime;
  87. private float _lastResultLogTime;
  88. private void OnEnable()
  89. {
  90. Application.runInBackground = true;
  91. _lastReferenceRefreshTime = Time.unscaledTime;
  92. if (autoLoadNetworkConfig)
  93. LoadNetworkConfig();
  94. if (enablePythonControl)
  95. InitPythonControl();
  96. if (enableUnityGameViewReference)
  97. InitUnityRefSender();
  98. if (enableResultReceiver && autoStartResultReceiverOnEnable)
  99. StartResultReceiver();
  100. }
  101. private void OnDisable()
  102. {
  103. StopResultReceiver();
  104. try { _pyControlClient?.Close(); } catch { /* ignore */ }
  105. _pyControlClient = null;
  106. try { _unityRefSender?.Close(); } catch { /* ignore */ }
  107. _unityRefSender = null;
  108. try { _esp32ControlClient?.Close(); } catch { /* ignore */ }
  109. _esp32ControlClient = null;
  110. }
  111. private void LoadNetworkConfig()
  112. {
  113. try
  114. {
  115. NetworkConfig cfg = NetworkConfigManager.LoadConfig();
  116. if (cfg == null || !cfg.Validate()) return;
  117. pythonIp = cfg.pythonIp;
  118. pythonResultBindIp = cfg.pythonResultBindIp;
  119. pythonResultPort = cfg.pythonResultPort;
  120. // Roma WiFi 端口:用于下发给硬件的 target_port(Python监听硬件图像)
  121. if (wifiControl != null)
  122. {
  123. wifiControl.targetPort = cfg.romaWifiListenPort;
  124. }
  125. // Roma forward viewer 端口
  126. if (forwardedViewer != null)
  127. forwardedViewer.forwardPort = cfg.romaForwardPort;
  128. // Roma 控制端口
  129. pythonControlPort = cfg.romaControlPort;
  130. // UnityRefPort(默认复用 pythonPort=12347)
  131. unityRefPort = cfg.pythonPort;
  132. // ESP32 控制端点(仅用于 0x40 下发)
  133. esp32DeviceIp = cfg.romaEsp32DeviceIp;
  134. esp32DevicePort = cfg.romaEsp32DevicePort;
  135. InitEsp32Control();
  136. }
  137. catch (Exception ex)
  138. {
  139. Debug.LogWarning($"[RomaBridge] Load config failed: {ex.Message}");
  140. }
  141. }
  142. private void InitEsp32Control()
  143. {
  144. if (!enableEsp32HardwareControl) return;
  145. if (string.IsNullOrWhiteSpace(esp32DeviceIp)) return;
  146. if (esp32DevicePort <= 0 || esp32DevicePort > 65535) return;
  147. try
  148. {
  149. _esp32ControlEndpoint = new IPEndPoint(IPAddress.Parse(esp32DeviceIp.Trim()), esp32DevicePort);
  150. _esp32ControlClient ??= new UdpClient();
  151. }
  152. catch (Exception ex)
  153. {
  154. Debug.LogWarning($"[RomaBridge] Init ESP32 control failed: {ex.Message}");
  155. _esp32ControlEndpoint = null;
  156. try { _esp32ControlClient?.Close(); } catch { /* ignore */ }
  157. _esp32ControlClient = null;
  158. }
  159. }
  160. /// <summary>
  161. /// 供 UI 调用:将 ImageTransmissionConfig 以 0x40 参数帧下发给 ESP32。
  162. /// </summary>
  163. public void ApplyEsp32HardwareConfig(ImageTransmissionConfig config)
  164. {
  165. if (!enableEsp32HardwareControl)
  166. {
  167. Debug.LogWarning("[RomaBridge] ESP32 hardware control disabled, skip 0x40 apply.");
  168. return;
  169. }
  170. if (_esp32ControlClient == null || _esp32ControlEndpoint == null)
  171. InitEsp32Control();
  172. if (_esp32ControlClient == null || _esp32ControlEndpoint == null)
  173. {
  174. Debug.LogWarning("[RomaBridge] ESP32 control endpoint not ready (device ip/port empty?), cannot send 0x40.");
  175. return;
  176. }
  177. try
  178. {
  179. byte[] frame = CoreHardwareControlProtocol.BuildParameterSettingFrame(config);
  180. if (frame == null || frame.Length == 0) return;
  181. _esp32ControlClient.Send(frame, frame.Length, _esp32ControlEndpoint);
  182. Debug.Log($"[RomaBridge] Sent 0x40 frame -> {_esp32ControlEndpoint.Address}:{_esp32ControlEndpoint.Port}");
  183. }
  184. catch (Exception ex)
  185. {
  186. Debug.LogWarning($"[RomaBridge] Send 0x40 to ESP32 failed: {ex.Message}");
  187. }
  188. }
  189. private void InitPythonControl()
  190. {
  191. try
  192. {
  193. string ipStr = string.IsNullOrWhiteSpace(pythonIp) ? "127.0.0.1" : pythonIp;
  194. _pyControlEndpoint = new IPEndPoint(IPAddress.Parse(ipStr), pythonControlPort);
  195. _pyControlClient = new UdpClient();
  196. }
  197. catch (Exception ex)
  198. {
  199. Debug.LogError($"[RomaBridge] Init python control failed: {ex.Message}");
  200. _pyControlClient = null;
  201. }
  202. }
  203. private void InitUnityRefSender()
  204. {
  205. try
  206. {
  207. string ipStr = string.IsNullOrWhiteSpace(pythonIp) ? "127.0.0.1" : pythonIp;
  208. _unityRefEndpoint = new IPEndPoint(IPAddress.Parse(ipStr), unityRefPort);
  209. _unityRefSender = new UdpClient();
  210. }
  211. catch (Exception ex)
  212. {
  213. Debug.LogError($"[RomaBridge] Init unity ref sender failed: {ex.Message}");
  214. _unityRefSender = null;
  215. }
  216. }
  217. public void StartPython()
  218. {
  219. if (pythonProcessController == null) return;
  220. pythonProcessController.StartPython();
  221. }
  222. public void StopPython()
  223. {
  224. if (pythonProcessController == null) return;
  225. pythonProcessController.StopPython();
  226. }
  227. public void StartForwardViewer()
  228. {
  229. forwardedViewer?.StartReceiver();
  230. }
  231. public void StopForwardViewer()
  232. {
  233. forwardedViewer?.StopReceiver();
  234. }
  235. public void SendControlCommand(char cmd)
  236. {
  237. if (!enablePythonControl || _pyControlClient == null || _pyControlEndpoint == null)
  238. return;
  239. try
  240. {
  241. byte[] payload = { (byte)cmd };
  242. _pyControlClient.Send(payload, payload.Length, _pyControlEndpoint);
  243. }
  244. catch (Exception ex)
  245. {
  246. Debug.LogWarning($"[RomaBridge] Send control '{cmd}' failed: {ex.Message}");
  247. }
  248. }
  249. public void SendWifiConfigToHardware(string targetIp, int targetPort)
  250. {
  251. if (wifiControl == null) return;
  252. wifiControl.ConfigStream(targetIp, targetPort);
  253. }
  254. public void StreamOn()
  255. {
  256. wifiControl?.StartStream();
  257. }
  258. public void StreamOff()
  259. {
  260. wifiControl?.StopStream();
  261. }
  262. private void StartResultReceiver()
  263. {
  264. if (_resultReceiver != null) return;
  265. try
  266. {
  267. _resultReceiver = new UDPResultReceiver(pythonResultBindIp, pythonResultPort, maxResultQueueSize);
  268. _resultReceiver.Start();
  269. }
  270. catch (Exception ex)
  271. {
  272. Debug.LogError($"[RomaBridge] Start result receiver failed: {ex.Message}");
  273. _resultReceiver = null;
  274. }
  275. }
  276. private void StopResultReceiver()
  277. {
  278. try { _resultReceiver?.Stop(); } catch { /* ignore */ }
  279. _resultReceiver = null;
  280. _hasLatest = false;
  281. }
  282. public void EnsureResultReceiverStarted()
  283. {
  284. if (!enableResultReceiver) return;
  285. if (_resultReceiver != null) return;
  286. StartResultReceiver();
  287. }
  288. public bool TryGetLatestResult(out LightGlueResult result)
  289. {
  290. result = default;
  291. if (_resultReceiver == null) return false;
  292. bool any = false;
  293. while (_resultReceiver.TryDequeueResult(out var r))
  294. {
  295. result = r;
  296. any = true;
  297. }
  298. return any;
  299. }
  300. private void Update()
  301. {
  302. // drain results and publish
  303. if (_resultReceiver != null)
  304. {
  305. bool any = false;
  306. LightGlueResult last = default;
  307. while (_resultReceiver.TryDequeueResult(out var r))
  308. {
  309. last = r;
  310. any = true;
  311. }
  312. if (any)
  313. {
  314. _latest = last;
  315. _hasLatest = true;
  316. OnResultUpdated?.Invoke(last);
  317. if (logResults && (Time.unscaledTime - _lastResultLogTime) >= logResultIntervalSeconds)
  318. {
  319. _lastResultLogTime = Time.unscaledTime;
  320. Debug.Log($"[RomaBridge] Result: valid={(_latest.IsValid ? 1 : 0)} matches={_latest.NumMatches} inliers={_latest.InliersRatio:F3} x={_latest.CameraPosition.x:F1} y={_latest.CameraPosition.y:F1}");
  321. }
  322. }
  323. }
  324. if (!enablePythonControl) return;
  325. if (referenceSource == ReferenceSource.ScreenCapture || referenceSource == ReferenceSource.GameView)
  326. {
  327. float now = Time.unscaledTime;
  328. if (now - _lastReferenceRefreshTime >= referenceRefreshIntervalSeconds)
  329. {
  330. _lastReferenceRefreshTime = now;
  331. if (referenceSource == ReferenceSource.ScreenCapture)
  332. {
  333. SendControlCommand('s');
  334. }
  335. else
  336. {
  337. // GameView: send JPEG to python unity_ref_port, then trigger 'r'
  338. if (enableUnityGameViewReference)
  339. {
  340. byte[] jpeg = CaptureGameViewToJpeg();
  341. if (jpeg != null && jpeg.Length > 0)
  342. {
  343. SendUnityRefJpeg(jpeg);
  344. SendControlCommand('r');
  345. }
  346. }
  347. }
  348. }
  349. }
  350. }
  351. private void SendUnityRefJpeg(byte[] jpeg)
  352. {
  353. if (_unityRefSender == null || _unityRefEndpoint == null) return;
  354. try
  355. {
  356. // 单包 UDP:要求 jpeg 长度 < 65507
  357. _unityRefSender.Send(jpeg, jpeg.Length, _unityRefEndpoint);
  358. }
  359. catch (Exception ex)
  360. {
  361. Debug.LogWarning($"[RomaBridge] Send UnityRef JPEG failed: {ex.Message}");
  362. }
  363. }
  364. private byte[] CaptureGameViewToJpeg()
  365. {
  366. Camera cam = gameViewCamera != null ? gameViewCamera : Camera.main;
  367. if (cam == null) return null;
  368. int w = Mathf.Clamp(referenceCaptureWidth, 64, 4096);
  369. int h = Mathf.Clamp(referenceCaptureHeight, 64, 4096);
  370. RenderTexture rt = RenderTexture.GetTemporary(w, h, 24, RenderTextureFormat.ARGB32);
  371. RenderTexture prevTarget = cam.targetTexture;
  372. RenderTexture prevActive = RenderTexture.active;
  373. cam.targetTexture = rt;
  374. cam.Render();
  375. RenderTexture.active = rt;
  376. Texture2D tex = new Texture2D(w, h, TextureFormat.RGB24, false);
  377. tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
  378. tex.Apply();
  379. cam.targetTexture = prevTarget;
  380. RenderTexture.active = prevActive;
  381. RenderTexture.ReleaseTemporary(rt);
  382. int quality = Mathf.Clamp(referenceJpegQuality, 1, 100);
  383. byte[] jpeg = tex.EncodeToJPG(quality);
  384. Destroy(tex);
  385. return jpeg;
  386. }
  387. }
  388. }