RomaNetworkConfigUIController.cs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. using System.Collections.Generic;
  2. using TMPro;
  3. using UnityEngine;
  4. using UnityEngine.UI;
  5. using LightGlue.Unity.Config;
  6. using LightGlue.Unity.Python;
  7. using System.Collections;
  8. using LightGlue.Unity.Roma.Networking;
  9. namespace LightGlue.Unity.Roma.UI
  10. {
  11. /// <summary>
  12. /// Roma 场景专用网络配置 UI。
  13. /// 目标:
  14. /// - 复用 NetworkConfig 的 JSON 存取(NetworkConfigManager)
  15. /// - 复用本机 IP 获取(NetworkConfig.GetLocalBindIp)
  16. /// - 只管理 Roma 相关端口/参数,降低与旧 LightGlue_Deployment UI 的耦合
  17. /// </summary>
  18. public sealed class RomaNetworkConfigUIController : MonoBehaviour
  19. {
  20. [Header("Config Panel (optional)")]
  21. [Tooltip("Roma 配置面板根节点(用于显示/隐藏整块 UI)。")]
  22. public GameObject configPanelRoot;
  23. [Header("Roma UI Fields")]
  24. [Tooltip("设备IP(从广播发现得到,仅显示/可手动改)")]
  25. public TMP_InputField deviceIpField;
  26. [Tooltip("ESP32 模式下的设备端口(供后续 ImageTransmissionUIController 下发硬件信息)。")]
  27. public TMP_InputField devicePortField;
  28. [Tooltip("本机IP(用于下发给 OrangePi 的 target_ip),不要填 127.0.0.1")]
  29. public TMP_InputField localTargetIpField;
  30. [Tooltip("OrangePi -> Python WiFi 图传端口({wifiPort},建议默认9000)")]
  31. public TMP_InputField romaWifiPortField;
  32. [Tooltip("Python -> Unity 图像转发端口({forwardPort},默认12366)")]
  33. public TMP_InputField romaForwardPortField;
  34. [Tooltip("Unity 结果接收绑定IP({resultIp},默认127.0.0.1)")]
  35. public TMP_InputField resultBindIpField;
  36. [Tooltip("Unity 结果接收端口({resultPort},默认12348)")]
  37. public TMP_InputField resultPortField;
  38. [Tooltip("Unity -> Python 控制端口({controlPort},默认12349)")]
  39. public TMP_InputField controlPortField;
  40. [Tooltip("Roma WiFi 启动参数模板(NetworkConfig.romaWifiPythonScriptArgs,支持占位符)")]
  41. public TMP_InputField romaArgsTemplateField;
  42. [Header("Hardware Mode (optional)")]
  43. [Tooltip("Roma 硬件模式选择(OrangePi/ESP32)。不绑定也可通过配置文件生效。")]
  44. public TMP_Dropdown hardwareModeDropdown;
  45. [Tooltip("按硬件模式切换可编辑性(TMP_InputField.interactable),而不是隐藏 UI。")]
  46. public bool useInteractableToggleByHardwareMode = true;
  47. [Header("Hardware Mode Visibility (optional lists)")]
  48. [Tooltip("ESP32 模式下需要显示的节点列表(会 SetActive(true))。")]
  49. public List<GameObject> showInEsp32Mode = new List<GameObject>();
  50. [Tooltip("OrangePi 模式下需要显示的节点列表(会 SetActive(true))。")]
  51. public List<GameObject> showInOrangePiMode = new List<GameObject>();
  52. [Tooltip("是否在切换模式时,自动把“非本模式列表”的节点隐藏(SetActive(false))。")]
  53. public bool hideOtherModeNodes = true;
  54. [Header("Buttons")]
  55. public Button startListenButton;
  56. public Button stopListenButton;
  57. public Button sendConfigButton;
  58. public Button streamOnButton;
  59. public Button streamOffButton;
  60. public Button fillLocalIpButton;
  61. public Button loadButton;
  62. public Button saveButton;
  63. public Button applyButton;
  64. [Header("Toggles")]
  65. public Toggle autoStartToggle;
  66. public Toggle noDisplayToggle;
  67. [Header("Status")]
  68. public Text statusText;
  69. [Header("Discovery Behavior")]
  70. [Tooltip("首次发现设备IP后是否自动停止监听广播,避免重复触发/刷屏。")]
  71. public bool stopListeningAfterFirstDiscover = true;
  72. [Header("After Config Behavior")]
  73. [Tooltip("发送图传 config 后是否自动启动 Roma Python(调用 RomaManager.StartPython)。")]
  74. public bool autoStartPythonAfterSendConfig = true;
  75. [Tooltip("发送图传 config 后是否自动启动 Unity 侧转发图像接收(12366)。")]
  76. public bool autoStartForwardViewerAfterSendConfig = true;
  77. [Tooltip("发送图传 config 后是否自动启动 Unity 侧结果接收(12348)。")]
  78. public bool autoStartResultReceiverAfterSendConfig = true;
  79. [Header("ESP32 Mode")]
  80. [Tooltip("当 romaHardwareMode=ESP32 时,进入场景是否自动启动 Python/显示/结果接收(无需下发图传配置)。")]
  81. public bool autoStartEsp32PipelineOnStart = true;
  82. [Tooltip("ESP32 模式下:若检测到 Python 已在运行,是否自动确保结果接收器(12348)已启动。")]
  83. public bool esp32AutoEnsureResultReceiverWhenPythonRunning = true;
  84. [Tooltip("ESP32 模式下自动检查间隔(秒)。")]
  85. public float esp32EnsureIntervalSeconds = 0.5f;
  86. [Header("Apply Targets (optional)")]
  87. [Tooltip("RomaManager(跨场景常驻)。为空则自动找 RomaManager.Instance")]
  88. public RomaManager romaManager;
  89. [Tooltip("Roma 用 PythonProcessController(建议挂在 RomaSystem 上)。为空则从 RomaManager 上找。")]
  90. public PythonProcessController romaPythonController;
  91. private NetworkConfig _cfg;
  92. private string _lastDiscoveredDeviceIp;
  93. private string _lastConfigSentKey;
  94. private bool _didInitialSyncDeviceIp;
  95. private bool _ignoreNextWifiConfigSent;
  96. // ESP32 模式下避免重复自动启动
  97. private bool _didAutoStartEsp32;
  98. private float _lastEsp32EnsureTime;
  99. [Header("Apply Behavior")]
  100. [Tooltip("点击 Apply 后,如果 Roma Python 正在运行,是否自动重启以应用新参数/端口。")]
  101. public bool restartPythonAfterApply = true;
  102. [Tooltip("重启 Python 的延迟(秒),给端口释放一点时间。")]
  103. public float restartPythonDelaySeconds = 0.2f;
  104. private void Start()
  105. {
  106. if (romaManager == null)
  107. romaManager = RomaManager.Instance;
  108. if (romaPythonController == null && romaManager != null)
  109. romaPythonController = romaManager.romaPythonController;
  110. LoadFromFile();
  111. SetupUi();
  112. HookEvents();
  113. BindRuntimeEvents();
  114. SyncLastDeviceIpToUi(force: true);
  115. TryAutoStartEsp32Pipeline();
  116. }
  117. private void OnEnable()
  118. {
  119. // 场景切换/对象重新启用时也要保证订阅存在
  120. BindRuntimeEvents();
  121. SyncLastDeviceIpToUi(force: false);
  122. TryAutoStartEsp32Pipeline();
  123. }
  124. private void Update()
  125. {
  126. // 关键修复:你现在遇到的是 Python 已启动,但结果接收器未启动,导致“不点配置就没坐标”
  127. if (!esp32AutoEnsureResultReceiverWhenPythonRunning) return;
  128. if (_cfg == null) return;
  129. if (_cfg.romaHardwareMode != NetworkConfig.RomaHardwareMode.Esp32) return;
  130. if (Time.time - _lastEsp32EnsureTime < esp32EnsureIntervalSeconds) return;
  131. _lastEsp32EnsureTime = Time.time;
  132. if (romaManager == null) romaManager = RomaManager.Instance;
  133. if (romaManager == null) return;
  134. if (romaPythonController == null) romaPythonController = romaManager.romaPythonController;
  135. if (romaPythonController == null || !romaPythonController.IsRunning) return;
  136. // 确保 12348 有监听者
  137. var bridge = FindObjectOfType<LightGlue.Unity.Roma.Bridge.RomaHardwareToPythonBridge>();
  138. if (bridge != null)
  139. {
  140. bridge.EnsureResultReceiverStarted();
  141. }
  142. else
  143. {
  144. romaManager.StartResultReceiver();
  145. }
  146. }
  147. private void OnDisable()
  148. {
  149. // UI 组件禁用时解除订阅,避免重复绑定
  150. if (romaManager != null && romaManager.discovery != null)
  151. romaManager.discovery.OnDeviceIpDiscovered -= OnDeviceIpDiscovered;
  152. if (romaManager != null && romaManager.wifiControl != null)
  153. romaManager.wifiControl.OnConfigSent -= OnWifiConfigSent;
  154. }
  155. private void BindRuntimeEvents()
  156. {
  157. if (romaManager == null)
  158. romaManager = RomaManager.Instance;
  159. if (romaManager == null) return;
  160. // 绑定广播发现:发现到设备IP后写入 UI(但不自动下发图传)
  161. if (romaManager.discovery != null)
  162. {
  163. romaManager.discovery.OnDeviceIpDiscovered -= OnDeviceIpDiscovered;
  164. romaManager.discovery.OnDeviceIpDiscovered += OnDeviceIpDiscovered;
  165. }
  166. // 绑定 WiFi 控制:无论是 UI 点击下发还是“自动下发”,都能触发后续启动流程
  167. if (romaManager.wifiControl != null)
  168. {
  169. romaManager.wifiControl.OnConfigSent -= OnWifiConfigSent;
  170. romaManager.wifiControl.OnConfigSent += OnWifiConfigSent;
  171. }
  172. // 绑定设备信息上报(Python -> Unity, JSON)
  173. if (romaManager.deviceInfoReceiver != null)
  174. {
  175. romaManager.deviceInfoReceiver.OnDeviceInfoUpdated -= OnDeviceInfoUpdatedFromPython;
  176. romaManager.deviceInfoReceiver.OnDeviceInfoUpdated += OnDeviceInfoUpdatedFromPython;
  177. }
  178. }
  179. private void OnDeviceInfoUpdatedFromPython(string ip, int srcPort)
  180. {
  181. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  182. if (_cfg.romaHardwareMode != NetworkConfig.RomaHardwareMode.Esp32) return;
  183. // ESP32 模式下:自动回填 deviceIp(端口保留用户配置的“控制端口”,不强制覆盖)
  184. if (deviceIpField != null && (string.IsNullOrWhiteSpace(deviceIpField.text) || deviceIpField.text.Trim() != ip))
  185. deviceIpField.text = ip;
  186. // devicePortField:仅当为空/无效时填默认 8888(避免覆盖用户手动设置)
  187. if (devicePortField != null)
  188. {
  189. string s = devicePortField.text;
  190. if (string.IsNullOrWhiteSpace(s) || !int.TryParse(s.Trim(), out int p) || p <= 0 || p > 65535)
  191. {
  192. devicePortField.text = "8888";
  193. }
  194. }
  195. if (string.IsNullOrWhiteSpace(_cfg.romaEsp32DeviceIp) || _cfg.romaEsp32DeviceIp != ip)
  196. _cfg.romaEsp32DeviceIp = ip;
  197. if (_cfg.romaEsp32DevicePort <= 0 || _cfg.romaEsp32DevicePort > 65535)
  198. _cfg.romaEsp32DevicePort = 8888;
  199. }
  200. private void SyncLastDeviceIpToUi(bool force)
  201. {
  202. if (!force && _didInitialSyncDeviceIp) return;
  203. if (romaManager == null) romaManager = RomaManager.Instance;
  204. if (romaManager == null || romaManager.discovery == null) return;
  205. string ip = romaManager.discovery.LastDeviceIp;
  206. if (string.IsNullOrWhiteSpace(ip)) return;
  207. if (deviceIpField != null && (force || string.IsNullOrWhiteSpace(deviceIpField.text)))
  208. deviceIpField.text = ip;
  209. // 走同一条逻辑,确保状态文本与“自动停止监听”等行为一致
  210. OnDeviceIpDiscovered(ip);
  211. _didInitialSyncDeviceIp = true;
  212. }
  213. private void SetupUi()
  214. {
  215. if (romaWifiPortField != null) romaWifiPortField.contentType = TMP_InputField.ContentType.IntegerNumber;
  216. if (romaForwardPortField != null) romaForwardPortField.contentType = TMP_InputField.ContentType.IntegerNumber;
  217. if (resultPortField != null) resultPortField.contentType = TMP_InputField.ContentType.IntegerNumber;
  218. if (controlPortField != null) controlPortField.contentType = TMP_InputField.ContentType.IntegerNumber;
  219. if (devicePortField != null) devicePortField.contentType = TMP_InputField.ContentType.IntegerNumber;
  220. if (romaArgsTemplateField != null)
  221. {
  222. romaArgsTemplateField.lineType = TMP_InputField.LineType.MultiLineNewline;
  223. romaArgsTemplateField.textComponent.enableWordWrapping = true;
  224. }
  225. if (hardwareModeDropdown != null)
  226. {
  227. hardwareModeDropdown.ClearOptions();
  228. hardwareModeDropdown.AddOptions(new System.Collections.Generic.List<string> { "OrangePi", "ESP32" });
  229. hardwareModeDropdown.onValueChanged.RemoveListener(OnHardwareModeChanged);
  230. hardwareModeDropdown.onValueChanged.AddListener(OnHardwareModeChanged);
  231. }
  232. UpdateUiFromConfig(force: true);
  233. }
  234. private void OnHardwareModeChanged(int _)
  235. {
  236. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  237. ReadUiIntoConfig();
  238. UpdateHardwareModeInteractable();
  239. UpdateHardwareModeVisibility();
  240. }
  241. private void HookEvents()
  242. {
  243. if (startListenButton != null) startListenButton.onClick.AddListener(StartListening);
  244. if (stopListenButton != null) stopListenButton.onClick.AddListener(StopListening);
  245. if (sendConfigButton != null) sendConfigButton.onClick.AddListener(OnSendConfigClicked);
  246. if (streamOnButton != null) streamOnButton.onClick.AddListener(StreamOn);
  247. if (streamOffButton != null) streamOffButton.onClick.AddListener(StreamOff);
  248. if (fillLocalIpButton != null) fillLocalIpButton.onClick.AddListener(FillLocalIp);
  249. if (loadButton != null) loadButton.onClick.AddListener(LoadFromFile);
  250. if (saveButton != null) saveButton.onClick.AddListener(SaveToFile);
  251. if (applyButton != null) applyButton.onClick.AddListener(ApplyToRuntime);
  252. if (autoStartToggle != null) autoStartToggle.onValueChanged.AddListener(OnAutoStartChanged);
  253. if (noDisplayToggle != null) noDisplayToggle.onValueChanged.AddListener(OnNoDisplayChanged);
  254. }
  255. private void OnSendConfigClicked()
  256. {
  257. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  258. ReadUiIntoConfig();
  259. // ESP32:无需下发图传配置,直接启动/重启 Python 流程
  260. if (_cfg.romaHardwareMode == NetworkConfig.RomaHardwareMode.Esp32)
  261. {
  262. StartEsp32Pipeline();
  263. return;
  264. }
  265. SendConfigToDevice(fromAuto: false);
  266. }
  267. private void OnDeviceIpDiscovered(string ip)
  268. {
  269. if (string.IsNullOrWhiteSpace(ip))
  270. return;
  271. // 去重:同一个 IP 连续广播不重复刷 UI/日志
  272. if (!string.IsNullOrWhiteSpace(_lastDiscoveredDeviceIp) && _lastDiscoveredDeviceIp == ip)
  273. return;
  274. _lastDiscoveredDeviceIp = ip;
  275. if (deviceIpField != null)
  276. deviceIpField.text = ip;
  277. bool autoCfg = false;
  278. bool autoStream = false;
  279. if (romaManager != null && romaManager.wifiControl != null)
  280. {
  281. autoCfg = romaManager.wifiControl.autoConfigAndStartOnDeviceDiscovered;
  282. autoStream = autoCfg; // 该开关语义为 config + stream_on
  283. }
  284. if (autoCfg)
  285. UpdateStatus($"发现设备IP: {ip}(已启用自动下发图传配置{(autoStream ? "+开流" : "")},请查看 [RomaWiFiCtrl] 日志确认发送结果)");
  286. else
  287. UpdateStatus($"发现设备IP: {ip}(未自动下发图传,请点击配置/开启按钮)");
  288. // 自动模式:OrangePi 才需要下发图传配置;ESP32 不走这条路径
  289. if (autoCfg)
  290. {
  291. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  292. // 仅 OrangePi 模式:统一走 SendConfigToDevice(确保使用 localTargetIpField 的“记录值”)
  293. if (_cfg.romaHardwareMode == NetworkConfig.RomaHardwareMode.OrangePi)
  294. {
  295. // 防止 wifiControl 也在 discovery 回调里自动下发造成重复:已在 wifiControl.autoConfigHandledByUi=true 时阻止
  296. SendConfigToDevice(fromAuto: true);
  297. }
  298. }
  299. if (stopListeningAfterFirstDiscover)
  300. {
  301. try
  302. {
  303. romaManager?.discovery?.StopListening();
  304. }
  305. catch
  306. {
  307. // ignore
  308. }
  309. UpdateStatus($"发现设备IP: {ip},已自动停止监听广播");
  310. }
  311. }
  312. public void StartListening()
  313. {
  314. if (romaManager == null) romaManager = RomaManager.Instance;
  315. if (romaManager == null || romaManager.discovery == null)
  316. {
  317. UpdateStatus("未找到 RomaManager 或 discovery,无法开始监听广播");
  318. return;
  319. }
  320. romaManager.discovery.StartListening();
  321. UpdateStatus("已开始监听广播(12345),等待设备IP...");
  322. }
  323. public void StopListening()
  324. {
  325. if (romaManager == null) romaManager = RomaManager.Instance;
  326. romaManager?.discovery?.StopListening();
  327. UpdateStatus("已停止监听广播");
  328. }
  329. public void SendConfigToDevice()
  330. {
  331. SendConfigToDevice(fromAuto: false);
  332. }
  333. private void SendConfigToDevice(bool fromAuto)
  334. {
  335. if (romaManager == null) romaManager = RomaManager.Instance;
  336. if (romaManager == null || romaManager.wifiControl == null)
  337. {
  338. UpdateStatus("未找到 RomaManager 或 wifiControl,无法发送配置");
  339. return;
  340. }
  341. string deviceIp = deviceIpField != null ? deviceIpField.text.Trim() : romaManager.wifiControl.deviceIp;
  342. if (!string.IsNullOrWhiteSpace(deviceIp))
  343. romaManager.wifiControl.deviceIp = deviceIp;
  344. // 无论自动/手动,都以 UI 里“记录的” localTargetIpField 为准
  345. string targetIp = localTargetIpField != null ? localTargetIpField.text.Trim() : string.Empty;
  346. if (string.IsNullOrWhiteSpace(targetIp))
  347. {
  348. // 若用户尚未手动填写,给一个合理默认,避免自动下发时 Validate 直接失败
  349. targetIp = NetworkConfig.GetLocalBindIp();
  350. if (localTargetIpField != null)
  351. localTargetIpField.text = targetIp;
  352. }
  353. if (!string.IsNullOrWhiteSpace(targetIp))
  354. romaManager.wifiControl.targetIp = targetIp;
  355. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  356. ReadUiIntoConfig();
  357. if (_cfg.romaHardwareMode == NetworkConfig.RomaHardwareMode.Esp32)
  358. {
  359. UpdateStatus("当前为 ESP32 模式:无需下发 OrangePi 图传配置。");
  360. StartEsp32Pipeline();
  361. return;
  362. }
  363. romaManager.wifiControl.targetPort = _cfg.romaWifiListenPort;
  364. // 该次下发由 UI 主动触发,避免 OnConfigSent 回调里重复 StartAfterConfig
  365. _ignoreNextWifiConfigSent = true;
  366. romaManager.wifiControl.ConfigStream(romaManager.wifiControl.targetIp, romaManager.wifiControl.targetPort);
  367. UpdateStatus($"已发送图传 config -> {romaManager.wifiControl.deviceIp}:8008, target={romaManager.wifiControl.targetIp}:{romaManager.wifiControl.targetPort}");
  368. StartAfterConfig(
  369. deviceIp: romaManager.wifiControl.deviceIp,
  370. targetIp: romaManager.wifiControl.targetIp,
  371. targetPort: romaManager.wifiControl.targetPort,
  372. fromAutoConfig: fromAuto);
  373. // 若勾选了“自动下发 config+开流”,这里也按设备端期望发 stream_on
  374. if (fromAuto && romaManager.wifiControl.autoConfigAndStartOnDeviceDiscovered)
  375. {
  376. int ms = romaManager.wifiControl.delayMsBetweenConfigAndOn;
  377. if (ms > 0) StartCoroutine(StreamOnAfterDelay(ms / 1000f));
  378. else StreamOn();
  379. }
  380. // 发送配置后:自动启动 Python 接收(手动流程中仅在此处自动启动,避免一进入场景就起进程)
  381. if (romaPythonController == null && romaManager != null)
  382. romaPythonController = romaManager.romaPythonController;
  383. if (romaPythonController != null)
  384. {
  385. romaPythonController.useRomaWifiArgsFromNetworkConfig = true;
  386. romaPythonController.addNoDisplayWhenLaunching = _cfg.pythonNoDisplay;
  387. // 默认切到 Unity bridge 入口脚本(低耦合,不再使用 demo_roma_camera_position_async_wifi.py)
  388. // 仅当当前 scriptPath 为空或仍指向旧 wifi 脚本时才覆盖,避免用户手动指定被强行改掉。
  389. string p = (romaPythonController.scriptPath ?? string.Empty).Replace("\\", "/").ToLowerInvariant();
  390. if (string.IsNullOrWhiteSpace(p) ||
  391. p.EndsWith("demo_roma_camera_position_async_wifi.py") ||
  392. p.EndsWith("/demo_roma_camera_position_async_wifi.py"))
  393. {
  394. romaPythonController.scriptPath = "demo/demo_roma_camera_position_async_unity_bridge.py";
  395. }
  396. }
  397. // 启动动作已统一收敛到 StartAfterConfig()
  398. }
  399. private void OnWifiConfigSent(string targetIp, int targetPort)
  400. {
  401. if (romaManager == null) romaManager = RomaManager.Instance;
  402. if (romaManager == null || romaManager.wifiControl == null) return;
  403. if (_ignoreNextWifiConfigSent)
  404. {
  405. _ignoreNextWifiConfigSent = false;
  406. return;
  407. }
  408. string deviceIp = romaManager.wifiControl.deviceIp;
  409. string key = $"{deviceIp}|{targetIp}|{targetPort}";
  410. if (!string.IsNullOrWhiteSpace(_lastConfigSentKey) && _lastConfigSentKey == key)
  411. return;
  412. _lastConfigSentKey = key;
  413. // 如果是“自动下发”,这里补齐 UI 侧的启动流程与状态提示
  414. if (romaManager.wifiControl.autoConfigAndStartOnDeviceDiscovered)
  415. {
  416. UpdateStatus($"已自动发送图传 config -> {deviceIp}:8008, target={targetIp}:{targetPort}");
  417. StartAfterConfig(deviceIp, targetIp, targetPort, fromAutoConfig: true);
  418. }
  419. }
  420. private void StartAfterConfig(string deviceIp, string targetIp, int targetPort, bool fromAutoConfig)
  421. {
  422. if (romaManager == null) romaManager = RomaManager.Instance;
  423. if (romaManager == null) return;
  424. // 这里负责“真正开启”:Python / 图像显示 / 结果接收
  425. if (autoStartPythonAfterSendConfig)
  426. romaManager.StartPython();
  427. if (autoStartForwardViewerAfterSendConfig)
  428. romaManager.StartForwardViewer();
  429. if (autoStartResultReceiverAfterSendConfig)
  430. {
  431. // 优先用 RomaHardwareToPythonBridge 的结果接收(避免和 RomaManager 抢占 12348)
  432. var bridge = FindObjectOfType<LightGlue.Unity.Roma.Bridge.RomaHardwareToPythonBridge>();
  433. if (bridge != null)
  434. bridge.EnsureResultReceiverStarted();
  435. else
  436. romaManager.StartResultReceiver();
  437. }
  438. if (fromAutoConfig)
  439. Debug.Log($"[RomaNetUI] Auto-start after config: python={(autoStartPythonAfterSendConfig ? 1 : 0)} viewer={(autoStartForwardViewerAfterSendConfig ? 1 : 0)} result={(autoStartResultReceiverAfterSendConfig ? 1 : 0)}");
  440. }
  441. public void StreamOn()
  442. {
  443. if (romaManager == null) romaManager = RomaManager.Instance;
  444. romaManager?.wifiControl?.StartStream();
  445. UpdateStatus("已发送 stream_on");
  446. }
  447. public void StreamOff()
  448. {
  449. if (romaManager == null) romaManager = RomaManager.Instance;
  450. romaManager?.wifiControl?.StopStream();
  451. UpdateStatus("已发送 stream_off");
  452. }
  453. public void FillLocalIp()
  454. {
  455. string ip = NetworkConfig.GetLocalBindIp();
  456. if (localTargetIpField != null) localTargetIpField.text = ip;
  457. UpdateStatus($"本机IP已填充: {ip}");
  458. }
  459. public void LoadFromFile()
  460. {
  461. _cfg = NetworkConfigManager.LoadConfig();
  462. UpdateUiFromConfig(force: true);
  463. UpdateStatus($"Roma 配置已加载: {NetworkConfigManager.GetConfigPath()}");
  464. }
  465. public void SaveToFile()
  466. {
  467. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  468. ReadUiIntoConfig();
  469. NetworkConfigManager.SaveConfig(_cfg);
  470. UpdateStatus($"Roma 配置已保存: {NetworkConfigManager.GetConfigPath()}");
  471. }
  472. public void ApplyToRuntime()
  473. {
  474. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  475. ReadUiIntoConfig();
  476. // 1) Roma WiFi 控制:下发目标IP/端口
  477. if (romaManager == null) romaManager = RomaManager.Instance;
  478. if (romaManager != null && romaManager.wifiControl != null)
  479. {
  480. string targetIp = localTargetIpField != null ? localTargetIpField.text.Trim() : string.Empty;
  481. if (!string.IsNullOrWhiteSpace(targetIp))
  482. romaManager.wifiControl.targetIp = targetIp;
  483. romaManager.wifiControl.targetPort = _cfg.romaWifiListenPort;
  484. }
  485. // 2) Unity 显示端口(Python->Unity)
  486. if (romaManager != null && romaManager.forwardedViewer != null)
  487. {
  488. romaManager.forwardedViewer.forwardPort = _cfg.romaForwardPort;
  489. }
  490. // 3) RomaManager 结果接收端口/IP
  491. if (romaManager != null)
  492. {
  493. romaManager.resultBindIp = _cfg.pythonResultBindIp;
  494. romaManager.resultPort = _cfg.pythonResultPort;
  495. romaManager.maxResultQueueSize = 10;
  496. }
  497. // 4) Python 启动参数:Roma 模板
  498. if (romaPythonController == null && romaManager != null)
  499. romaPythonController = romaManager.romaPythonController;
  500. if (romaPythonController != null)
  501. {
  502. romaPythonController.useRomaWifiArgsFromNetworkConfig = true;
  503. romaPythonController.addNoDisplayWhenLaunching = _cfg.pythonNoDisplay;
  504. if (restartPythonAfterApply && romaPythonController.IsRunning)
  505. {
  506. StartCoroutine(RestartPythonCoroutine());
  507. }
  508. }
  509. // Apply 同时保存到配置文件,确保下次启动保持一致
  510. NetworkConfigManager.SaveConfig(_cfg);
  511. UpdateStatus("Roma 配置已应用到运行时组件(如需持久化请点保存)");
  512. HideConfigPanel();
  513. }
  514. private IEnumerator StreamOnAfterDelay(float seconds)
  515. {
  516. if (seconds > 0f) yield return new WaitForSeconds(seconds);
  517. StreamOn();
  518. }
  519. private IEnumerator RestartPythonCoroutine()
  520. {
  521. if (romaPythonController == null) yield break;
  522. UpdateStatus("检测到 Python 正在运行:将自动重启以应用新配置...");
  523. romaPythonController.StopPython();
  524. if (restartPythonDelaySeconds > 0f)
  525. yield return new WaitForSeconds(restartPythonDelaySeconds);
  526. romaPythonController.StartPython();
  527. }
  528. private void TryAutoStartEsp32Pipeline()
  529. {
  530. if (_didAutoStartEsp32) return;
  531. if (!autoStartEsp32PipelineOnStart) return;
  532. if (_cfg == null) return;
  533. if (_cfg.romaHardwareMode != NetworkConfig.RomaHardwareMode.Esp32) return;
  534. _didAutoStartEsp32 = true;
  535. StartEsp32Pipeline();
  536. }
  537. private void StartEsp32Pipeline()
  538. {
  539. if (romaManager == null) romaManager = RomaManager.Instance;
  540. if (romaManager == null)
  541. {
  542. UpdateStatus("未找到 RomaManager,无法启动 ESP32 流程");
  543. return;
  544. }
  545. if (romaPythonController == null)
  546. romaPythonController = romaManager.romaPythonController;
  547. // ESP32:无下发配置,直接启动 Python / 显示 / 结果接收
  548. if (autoStartPythonAfterSendConfig)
  549. romaManager.StartPython();
  550. if (autoStartForwardViewerAfterSendConfig)
  551. romaManager.StartForwardViewer();
  552. if (autoStartResultReceiverAfterSendConfig)
  553. {
  554. var bridge = FindObjectOfType<LightGlue.Unity.Roma.Bridge.RomaHardwareToPythonBridge>();
  555. if (bridge != null)
  556. bridge.EnsureResultReceiverStarted();
  557. else
  558. romaManager.StartResultReceiver();
  559. }
  560. UpdateStatus("ESP32 模式:已启动 Python/显示/结果接收(无需下发图传配置)");
  561. }
  562. private void OnAutoStartChanged(bool v)
  563. {
  564. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  565. _cfg.autoStartOnPlay = v;
  566. }
  567. private void OnNoDisplayChanged(bool v)
  568. {
  569. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  570. _cfg.pythonNoDisplay = v;
  571. if (romaPythonController != null)
  572. romaPythonController.addNoDisplayWhenLaunching = v;
  573. }
  574. private void UpdateUiFromConfig(bool force)
  575. {
  576. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  577. // localTargetIp:优先使用上次保存的配置;仅当为空时才退回自动获取本机IP
  578. if (localTargetIpField != null && (force || string.IsNullOrWhiteSpace(localTargetIpField.text)))
  579. {
  580. string ip = !string.IsNullOrWhiteSpace(_cfg.romaLocalTargetIp)
  581. ? _cfg.romaLocalTargetIp.Trim()
  582. : NetworkConfig.GetLocalBindIp();
  583. localTargetIpField.text = ip;
  584. }
  585. if (romaWifiPortField != null) romaWifiPortField.text = _cfg.romaWifiListenPort.ToString();
  586. if (romaForwardPortField != null) romaForwardPortField.text = _cfg.romaForwardPort.ToString();
  587. if (resultBindIpField != null) resultBindIpField.text = _cfg.pythonResultBindIp;
  588. if (resultPortField != null) resultPortField.text = _cfg.pythonResultPort.ToString();
  589. if (controlPortField != null) controlPortField.text = _cfg.romaControlPort.ToString();
  590. if (romaArgsTemplateField != null) romaArgsTemplateField.text = _cfg.romaWifiPythonScriptArgs;
  591. if (autoStartToggle != null) autoStartToggle.isOn = _cfg.autoStartOnPlay;
  592. if (noDisplayToggle != null) noDisplayToggle.isOn = _cfg.pythonNoDisplay;
  593. if (hardwareModeDropdown != null)
  594. hardwareModeDropdown.value = (int)_cfg.romaHardwareMode;
  595. // ESP32 设备信息:仅 ESP32 模式需要填写/持久化(字段开放可编辑)
  596. if (deviceIpField != null && (force || string.IsNullOrWhiteSpace(deviceIpField.text)))
  597. deviceIpField.text = _cfg.romaEsp32DeviceIp;
  598. if (devicePortField != null)
  599. devicePortField.text = _cfg.romaEsp32DevicePort.ToString();
  600. UpdateHardwareModeInteractable();
  601. UpdateHardwareModeVisibility();
  602. }
  603. private void ReadUiIntoConfig()
  604. {
  605. if (_cfg == null) _cfg = NetworkConfig.CreateDefault();
  606. if (localTargetIpField != null && !string.IsNullOrWhiteSpace(localTargetIpField.text))
  607. _cfg.romaLocalTargetIp = localTargetIpField.text.Trim();
  608. if (hardwareModeDropdown != null)
  609. {
  610. int v = hardwareModeDropdown.value;
  611. _cfg.romaHardwareMode = (v == 1) ? NetworkConfig.RomaHardwareMode.Esp32 : NetworkConfig.RomaHardwareMode.OrangePi;
  612. }
  613. if (_cfg.romaHardwareMode == NetworkConfig.RomaHardwareMode.Esp32)
  614. {
  615. if (deviceIpField != null && !string.IsNullOrWhiteSpace(deviceIpField.text))
  616. _cfg.romaEsp32DeviceIp = deviceIpField.text.Trim();
  617. _cfg.romaEsp32DevicePort = ParsePortOrKeep(devicePortField, _cfg.romaEsp32DevicePort);
  618. }
  619. _cfg.romaWifiListenPort = ParsePortOrKeep(romaWifiPortField, _cfg.romaWifiListenPort);
  620. _cfg.romaForwardPort = ParsePortOrKeep(romaForwardPortField, _cfg.romaForwardPort);
  621. if (resultBindIpField != null && !string.IsNullOrWhiteSpace(resultBindIpField.text))
  622. _cfg.pythonResultBindIp = resultBindIpField.text.Trim();
  623. _cfg.pythonResultPort = ParsePortOrKeep(resultPortField, _cfg.pythonResultPort);
  624. _cfg.romaControlPort = ParsePortOrKeep(controlPortField, _cfg.romaControlPort);
  625. if (romaArgsTemplateField != null && !string.IsNullOrWhiteSpace(romaArgsTemplateField.text))
  626. _cfg.romaWifiPythonScriptArgs = romaArgsTemplateField.text.Trim();
  627. if (autoStartToggle != null) _cfg.autoStartOnPlay = autoStartToggle.isOn;
  628. if (noDisplayToggle != null) _cfg.pythonNoDisplay = noDisplayToggle.isOn;
  629. }
  630. private void UpdateHardwareModeInteractable()
  631. {
  632. if (_cfg == null) return;
  633. bool isEsp32 = _cfg.romaHardwareMode == NetworkConfig.RomaHardwareMode.Esp32;
  634. if (!useInteractableToggleByHardwareMode)
  635. return;
  636. // ESP32:设备IP/端口可编辑;OrangePi:只读(通常由广播发现/旧流程决定)
  637. if (deviceIpField != null)
  638. deviceIpField.interactable = isEsp32;
  639. if (devicePortField != null)
  640. devicePortField.interactable = isEsp32;
  641. // localTargetIpField 始终可编辑(不随硬件模式置灰)
  642. }
  643. private void UpdateHardwareModeVisibility()
  644. {
  645. if (_cfg == null) return;
  646. bool isEsp32 = _cfg.romaHardwareMode == NetworkConfig.RomaHardwareMode.Esp32;
  647. // 显示本模式列表
  648. List<GameObject> show = isEsp32 ? showInEsp32Mode : showInOrangePiMode;
  649. if (show != null)
  650. {
  651. for (int i = 0; i < show.Count; i++)
  652. {
  653. var go = show[i];
  654. if (go != null) go.SetActive(true);
  655. }
  656. }
  657. // 可选:隐藏另一个模式列表
  658. if (!hideOtherModeNodes) return;
  659. List<GameObject> hide = isEsp32 ? showInOrangePiMode : showInEsp32Mode;
  660. if (hide != null)
  661. {
  662. for (int i = 0; i < hide.Count; i++)
  663. {
  664. var go = hide[i];
  665. if (go != null) go.SetActive(false);
  666. }
  667. }
  668. }
  669. private static int ParsePortOrKeep(TMP_InputField field, int keep)
  670. {
  671. if (field == null) return keep;
  672. string s = field.text;
  673. if (string.IsNullOrWhiteSpace(s)) return keep;
  674. if (!int.TryParse(s.Trim(), out int p)) return keep;
  675. if (p <= 0 || p > 65535) return keep;
  676. return p;
  677. }
  678. private void UpdateStatus(string msg)
  679. {
  680. if (statusText != null) statusText.text = msg;
  681. Debug.Log($"[RomaNetUI] {msg}");
  682. }
  683. /// <summary>
  684. /// 显示配置面板
  685. /// </summary>
  686. public void ShowConfigPanel()
  687. {
  688. if (configPanelRoot != null)
  689. {
  690. configPanelRoot.SetActive(true);
  691. Debug.Log("[RomaNetUI] 配置面板已显示");
  692. }
  693. }
  694. /// <summary>
  695. /// 隐藏配置面板
  696. /// </summary>
  697. public void HideConfigPanel()
  698. {
  699. if (configPanelRoot != null)
  700. {
  701. configPanelRoot.SetActive(false);
  702. Debug.Log("[RomaNetUI] 配置面板已隐藏");
  703. }
  704. }
  705. /// <summary>
  706. /// 切换配置面板显示/隐藏状态
  707. /// </summary>
  708. public void ToggleConfigPanel()
  709. {
  710. if (configPanelRoot != null)
  711. {
  712. bool newState = !configPanelRoot.activeSelf;
  713. configPanelRoot.SetActive(newState);
  714. Debug.Log($"[RomaNetUI] 配置面板已{(newState ? "显示" : "隐藏")}");
  715. }
  716. }
  717. private void OnDestroy()
  718. {
  719. if (romaManager != null && romaManager.discovery != null)
  720. {
  721. romaManager.discovery.OnDeviceIpDiscovered -= OnDeviceIpDiscovered;
  722. }
  723. if (romaManager != null && romaManager.wifiControl != null)
  724. {
  725. romaManager.wifiControl.OnConfigSent -= OnWifiConfigSent;
  726. }
  727. if (romaManager != null && romaManager.deviceInfoReceiver != null)
  728. {
  729. romaManager.deviceInfoReceiver.OnDeviceInfoUpdated -= OnDeviceInfoUpdatedFromPython;
  730. }
  731. }
  732. }
  733. }