RomaHardwareToPythonBridge.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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. ReferenceImageResizeService.EnsureInitialized();
  93. referenceCaptureWidth = ReferenceImageResizeService.Width;
  94. referenceCaptureHeight = ReferenceImageResizeService.Height;
  95. ReferenceImageResizeService.OnResizeChanged += OnResizeChanged;
  96. if (autoLoadNetworkConfig)
  97. LoadNetworkConfig();
  98. if (enablePythonControl)
  99. InitPythonControl();
  100. if (enableUnityGameViewReference)
  101. InitUnityRefSender();
  102. if (enableResultReceiver && autoStartResultReceiverOnEnable)
  103. StartResultReceiver();
  104. }
  105. private void OnDisable()
  106. {
  107. StopResultReceiver();
  108. try { _pyControlClient?.Close(); } catch { /* ignore */ }
  109. _pyControlClient = null;
  110. try { _unityRefSender?.Close(); } catch { /* ignore */ }
  111. _unityRefSender = null;
  112. try { _esp32ControlClient?.Close(); } catch { /* ignore */ }
  113. _esp32ControlClient = null;
  114. ReferenceImageResizeService.OnResizeChanged -= OnResizeChanged;
  115. }
  116. private void OnResizeChanged(int w, int h)
  117. {
  118. referenceCaptureWidth = w;
  119. referenceCaptureHeight = h;
  120. }
  121. private void LoadNetworkConfig()
  122. {
  123. try
  124. {
  125. NetworkConfig cfg = NetworkConfigManager.LoadConfig();
  126. if (cfg == null || !cfg.Validate()) return;
  127. pythonIp = cfg.pythonIp;
  128. pythonResultBindIp = cfg.pythonResultBindIp;
  129. pythonResultPort = cfg.pythonResultPort;
  130. // Roma WiFi 端口:用于下发给硬件的 target_port(Python监听硬件图像)
  131. if (wifiControl != null)
  132. {
  133. wifiControl.targetPort = cfg.romaWifiListenPort;
  134. }
  135. // Roma forward viewer 端口
  136. if (forwardedViewer != null)
  137. forwardedViewer.forwardPort = cfg.romaForwardPort;
  138. // Roma 控制端口
  139. pythonControlPort = cfg.romaControlPort;
  140. // UnityRefPort(默认复用 pythonPort=12347)
  141. unityRefPort = cfg.pythonPort;
  142. // ESP32 控制端点(仅用于 0x40 下发)
  143. esp32DeviceIp = cfg.romaEsp32DeviceIp;
  144. esp32DevicePort = cfg.romaEsp32DevicePort;
  145. InitEsp32Control();
  146. }
  147. catch (Exception ex)
  148. {
  149. Debug.LogWarning($"[RomaBridge] Load config failed: {ex.Message}");
  150. }
  151. }
  152. private void InitEsp32Control()
  153. {
  154. if (!enableEsp32HardwareControl) return;
  155. if (string.IsNullOrWhiteSpace(esp32DeviceIp)) return;
  156. if (esp32DevicePort <= 0 || esp32DevicePort > 65535) return;
  157. try
  158. {
  159. _esp32ControlEndpoint = new IPEndPoint(IPAddress.Parse(esp32DeviceIp.Trim()), esp32DevicePort);
  160. _esp32ControlClient ??= new UdpClient();
  161. }
  162. catch (Exception ex)
  163. {
  164. Debug.LogWarning($"[RomaBridge] Init ESP32 control failed: {ex.Message}");
  165. _esp32ControlEndpoint = null;
  166. try { _esp32ControlClient?.Close(); } catch { /* ignore */ }
  167. _esp32ControlClient = null;
  168. }
  169. }
  170. /// <summary>
  171. /// 供 UI 调用:将 ImageTransmissionConfig 以 0x40 参数帧下发给 ESP32。
  172. /// </summary>
  173. public void ApplyEsp32HardwareConfig(ImageTransmissionConfig config)
  174. {
  175. if (!enableEsp32HardwareControl)
  176. {
  177. Debug.LogWarning("[RomaBridge] ESP32 hardware control disabled, skip 0x40 apply.");
  178. return;
  179. }
  180. if (_esp32ControlClient == null || _esp32ControlEndpoint == null)
  181. InitEsp32Control();
  182. if (_esp32ControlClient == null || _esp32ControlEndpoint == null)
  183. {
  184. Debug.LogWarning("[RomaBridge] ESP32 control endpoint not ready (device ip/port empty?), cannot send 0x40.");
  185. return;
  186. }
  187. try
  188. {
  189. byte[] frame = CoreHardwareControlProtocol.BuildParameterSettingFrame(config);
  190. if (frame == null || frame.Length == 0) return;
  191. _esp32ControlClient.Send(frame, frame.Length, _esp32ControlEndpoint);
  192. Debug.Log($"[RomaBridge] Sent 0x40 frame -> {_esp32ControlEndpoint.Address}:{_esp32ControlEndpoint.Port}");
  193. }
  194. catch (Exception ex)
  195. {
  196. Debug.LogWarning($"[RomaBridge] Send 0x40 to ESP32 failed: {ex.Message}");
  197. }
  198. }
  199. private void InitPythonControl()
  200. {
  201. try
  202. {
  203. string ipStr = string.IsNullOrWhiteSpace(pythonIp) ? "127.0.0.1" : pythonIp;
  204. _pyControlEndpoint = new IPEndPoint(IPAddress.Parse(ipStr), pythonControlPort);
  205. _pyControlClient = new UdpClient();
  206. }
  207. catch (Exception ex)
  208. {
  209. Debug.LogError($"[RomaBridge] Init python control failed: {ex.Message}");
  210. _pyControlClient = null;
  211. }
  212. }
  213. private void InitUnityRefSender()
  214. {
  215. try
  216. {
  217. string ipStr = string.IsNullOrWhiteSpace(pythonIp) ? "127.0.0.1" : pythonIp;
  218. _unityRefEndpoint = new IPEndPoint(IPAddress.Parse(ipStr), unityRefPort);
  219. _unityRefSender = new UdpClient();
  220. }
  221. catch (Exception ex)
  222. {
  223. Debug.LogError($"[RomaBridge] Init unity ref sender failed: {ex.Message}");
  224. _unityRefSender = null;
  225. }
  226. }
  227. public void StartPython()
  228. {
  229. if (pythonProcessController == null) return;
  230. pythonProcessController.StartPython();
  231. }
  232. public void StopPython()
  233. {
  234. if (pythonProcessController == null) return;
  235. pythonProcessController.StopPython();
  236. }
  237. public void StartForwardViewer()
  238. {
  239. forwardedViewer?.StartReceiver();
  240. }
  241. public void StopForwardViewer()
  242. {
  243. forwardedViewer?.StopReceiver();
  244. }
  245. public void SendControlCommand(char cmd)
  246. {
  247. if (!enablePythonControl || _pyControlClient == null || _pyControlEndpoint == null)
  248. return;
  249. try
  250. {
  251. byte[] payload = { (byte)cmd };
  252. _pyControlClient.Send(payload, payload.Length, _pyControlEndpoint);
  253. }
  254. catch (Exception ex)
  255. {
  256. Debug.LogWarning($"[RomaBridge] Send control '{cmd}' failed: {ex.Message}");
  257. }
  258. }
  259. public void SendWifiConfigToHardware(string targetIp, int targetPort)
  260. {
  261. if (wifiControl == null) return;
  262. wifiControl.ConfigStream(targetIp, targetPort);
  263. }
  264. public void StreamOn()
  265. {
  266. wifiControl?.StartStream();
  267. }
  268. public void StreamOff()
  269. {
  270. wifiControl?.StopStream();
  271. }
  272. private void StartResultReceiver()
  273. {
  274. if (_resultReceiver != null) return;
  275. try
  276. {
  277. _resultReceiver = new UDPResultReceiver(pythonResultBindIp, pythonResultPort, maxResultQueueSize);
  278. _resultReceiver.Start();
  279. }
  280. catch (Exception ex)
  281. {
  282. Debug.LogError($"[RomaBridge] Start result receiver failed: {ex.Message}");
  283. _resultReceiver = null;
  284. }
  285. }
  286. private void StopResultReceiver()
  287. {
  288. try { _resultReceiver?.Stop(); } catch { /* ignore */ }
  289. _resultReceiver = null;
  290. _hasLatest = false;
  291. }
  292. public void EnsureResultReceiverStarted()
  293. {
  294. if (!enableResultReceiver) return;
  295. if (_resultReceiver != null) return;
  296. StartResultReceiver();
  297. }
  298. public bool TryGetLatestResult(out LightGlueResult result)
  299. {
  300. result = default;
  301. if (_resultReceiver == null) return false;
  302. bool any = false;
  303. while (_resultReceiver.TryDequeueResult(out var r))
  304. {
  305. result = r;
  306. any = true;
  307. }
  308. return any;
  309. }
  310. private void Update()
  311. {
  312. // drain results and publish
  313. if (_resultReceiver != null)
  314. {
  315. bool any = false;
  316. LightGlueResult last = default;
  317. while (_resultReceiver.TryDequeueResult(out var r))
  318. {
  319. last = r;
  320. any = true;
  321. }
  322. if (any)
  323. {
  324. _latest = last;
  325. _hasLatest = true;
  326. OnResultUpdated?.Invoke(last);
  327. if (logResults && (Time.unscaledTime - _lastResultLogTime) >= logResultIntervalSeconds)
  328. {
  329. _lastResultLogTime = Time.unscaledTime;
  330. 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}");
  331. }
  332. }
  333. }
  334. if (!enablePythonControl) return;
  335. if (referenceSource == ReferenceSource.ScreenCapture || referenceSource == ReferenceSource.GameView)
  336. {
  337. float now = Time.unscaledTime;
  338. if (now - _lastReferenceRefreshTime >= referenceRefreshIntervalSeconds)
  339. {
  340. _lastReferenceRefreshTime = now;
  341. if (referenceSource == ReferenceSource.ScreenCapture)
  342. {
  343. SendControlCommand('s');
  344. }
  345. else
  346. {
  347. // GameView: send JPEG to python unity_ref_port, then trigger 'r'
  348. if (enableUnityGameViewReference)
  349. {
  350. byte[] jpeg = CaptureGameViewToJpeg();
  351. if (jpeg != null && jpeg.Length > 0)
  352. {
  353. SendUnityRefJpeg(jpeg);
  354. SendControlCommand('r');
  355. }
  356. }
  357. }
  358. }
  359. }
  360. }
  361. private void SendUnityRefJpeg(byte[] jpeg)
  362. {
  363. if (_unityRefSender == null || _unityRefEndpoint == null) return;
  364. try
  365. {
  366. // 单包 UDP:要求 jpeg 长度 < 65507
  367. _unityRefSender.Send(jpeg, jpeg.Length, _unityRefEndpoint);
  368. }
  369. catch (Exception ex)
  370. {
  371. Debug.LogWarning($"[RomaBridge] Send UnityRef JPEG failed: {ex.Message}");
  372. }
  373. }
  374. private byte[] CaptureGameViewToJpeg()
  375. {
  376. Camera cam = gameViewCamera != null ? gameViewCamera : Camera.main;
  377. if (cam == null) return null;
  378. int w = Mathf.Clamp(referenceCaptureWidth, 64, 4096);
  379. int h = Mathf.Clamp(referenceCaptureHeight, 64, 4096);
  380. RenderTexture rt = RenderTexture.GetTemporary(w, h, 24, RenderTextureFormat.ARGB32);
  381. RenderTexture prevTarget = cam.targetTexture;
  382. RenderTexture prevActive = RenderTexture.active;
  383. cam.targetTexture = rt;
  384. cam.Render();
  385. RenderTexture.active = rt;
  386. Texture2D tex = new Texture2D(w, h, TextureFormat.RGB24, false);
  387. tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
  388. tex.Apply();
  389. cam.targetTexture = prevTarget;
  390. RenderTexture.active = prevActive;
  391. RenderTexture.ReleaseTemporary(rt);
  392. int quality = Mathf.Clamp(referenceJpegQuality, 1, 100);
  393. byte[] jpeg = tex.EncodeToJPG(quality);
  394. Destroy(tex);
  395. return jpeg;
  396. }
  397. }
  398. }