ImageTransmissionUIController.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. using UnityEngine;
  2. using UnityEngine.UI;
  3. using TMPro;
  4. using LightGlue.Unity.Config;
  5. using LightGlue.Unity.Bridge;
  6. using LightGlue.Unity.Game;
  7. using LightGlue.Unity.Roma;
  8. using LightGlue.Unity.Roma.Bridge;
  9. using LightGlue.Unity.Sdk.Unity;
  10. using System.Collections;
  11. namespace LightGlue.Unity.UI
  12. {
  13. /// <summary>
  14. /// 图像传输配置UI控制器
  15. /// - 提供分辨率、质量、上报时间间隔、传输开关等控件
  16. /// - 将配置同步到 Bridge:Python 链路(开关/间隔)+ 硬件 0x40 参数下发(由 Bridge 统一管理)
  17. /// </summary>
  18. public class ImageTransmissionUIController : MonoBehaviour
  19. {
  20. private const string KeyPrefix = "LightGlue_ImageTx_";
  21. private const string KeyResolution = KeyPrefix + "Resolution";
  22. private const string KeyQuality = KeyPrefix + "Quality";
  23. private const string KeyInterval = KeyPrefix + "IntervalMs";
  24. private const string KeyEnabled = KeyPrefix + "Enabled";
  25. [Header("UI组件引用")]
  26. [Tooltip("图片分辨率下拉菜单")]
  27. public TMP_Dropdown resolutionDropdown;
  28. [Tooltip("图片质量输入框")]
  29. public TMP_InputField qualityInputField;
  30. [Tooltip("上报时间间隔输入框 (毫秒)")]
  31. public TMP_InputField intervalInputField;
  32. [Tooltip("开启图片传输开关(主要控制 Python 链路与硬件采图开关状态)")]
  33. public Toggle transmissionToggle;
  34. [Tooltip("应用配置按钮(点击时将当前配置一次性下发给硬件)")]
  35. public Button applyConfigButton;
  36. [Tooltip("重置为默认图像配置并立即下发硬件的按钮")]
  37. public Button resetConfigButton;
  38. [Header("配置")]
  39. [Tooltip("图像传输配置")]
  40. public ImageTransmissionConfig config = new ImageTransmissionConfig();
  41. [Header("硬件下发开关")]
  42. [Tooltip("是否启用硬件下发相关功能(0x40)。关闭后会隐藏下发按钮,且不会在启动/配置变更时触发任何硬件下发动作。")]
  43. public bool enableHardwareApply = true;
  44. [Tooltip("关闭硬件下发时,是否隐藏“应用配置/重置并下发”按钮。")]
  45. public bool hideHardwareApplyButtonsWhenDisabled = true;
  46. [Header("Roma: 下发后自动恢复链路")]
  47. [Tooltip("在 Roma(ESP32) 下发 0x40 后,是否自动重启 Roma Python(避免硬件重配置导致画面/结果暂时中断)。")]
  48. public bool restartRomaPythonAfterHardwareApply = true;
  49. [Tooltip("重启 Roma Python 的延迟(秒),给端口/硬件一点恢复时间。")]
  50. public float restartRomaPythonDelaySeconds = 0.25f;
  51. // 启动时记录的“场景默认配置”(来自 Inspector),仅用于 ResetToDefaultAndSave,不受本地记录覆盖影响。
  52. private ImageTransmissionConfig _defaultConfigSnapshot;
  53. [Header("本地记录")]
  54. [Tooltip("启动时是否从本地(PlayerPrefs)加载上次保存的图像传输配置")]
  55. public bool loadFromLocalOnStart = true;
  56. [Header("Bridge连接")]
  57. [Tooltip("Bridge(同步传输配置并下发 0x40 到硬件;可选,未设置时从 LightGlueManager 获取)")]
  58. public HardwareToPythonUdpBridge bridge;
  59. [Tooltip("Roma Bridge(用于 ESP32 模式下发 0x40 到硬件;可选,未设置时从 RomaManager 查找)")]
  60. public RomaHardwareToPythonBridge romaBridge;
  61. [Tooltip("LightGlue SDK 新桥接组件(可选,用于对比旧 Bridge 行为)")]
  62. public LightGlueBridgeBehaviour sdkBridge;
  63. [Header("事件")]
  64. [Tooltip("配置变更时触发的事件")]
  65. public UnityEngine.Events.UnityEvent<ImageTransmissionConfig> onConfigChanged;
  66. private void Start()
  67. {
  68. if (bridge == null && LightGlueManager.Instance != null)
  69. bridge = LightGlueManager.Instance.bridge;
  70. if (romaBridge == null && RomaManager.Instance != null)
  71. romaBridge = FindObjectOfType<RomaHardwareToPythonBridge>();
  72. // sdkBridge 一般通过 Inspector 指定;不自动从 Manager 获取,避免与旧 Bridge 冲突。
  73. // 1) 先记录当前 Inspector 中的 config 作为“默认值快照”(仅 Reset 使用)
  74. _defaultConfigSnapshot = new ImageTransmissionConfig
  75. {
  76. resolution = config.resolution,
  77. quality = config.quality,
  78. reportIntervalMs = config.reportIntervalMs,
  79. enableImageTransmission = config.enableImageTransmission
  80. };
  81. // 2) 再按需从本地记录覆盖运行时 config(不影响默认值快照)
  82. if (loadFromLocalOnStart)
  83. {
  84. LoadFromLocal();
  85. }
  86. InitializeUI();
  87. SetupEventListeners();
  88. // 关闭硬件下发时:隐藏按钮并跳过“启动时自动同步到 Bridge”(避免触发 0x40)
  89. if (!enableHardwareApply)
  90. {
  91. if (hideHardwareApplyButtonsWhenDisabled)
  92. {
  93. if (applyConfigButton != null) applyConfigButton.gameObject.SetActive(false);
  94. if (resetConfigButton != null) resetConfigButton.gameObject.SetActive(false);
  95. }
  96. Debug.Log("[UI] 硬件下发已禁用:将不会在启动/配置变更时触发 0x40 下发。");
  97. return;
  98. }
  99. // 初始化时同步配置到 Bridge(SetTransmissionConfig 内部会立即下发 0x40,保证硬件从第一帧起就用正确参数)
  100. if (bridge != null)
  101. {
  102. bridge.SetTransmissionConfig(config);
  103. bridge.MarkHardwareAutoApplyReady();
  104. }
  105. // 同步到新 SDK Bridge,便于对比测试
  106. if (sdkBridge != null)
  107. {
  108. sdkBridge.SetTransmissionConfig(config);
  109. }
  110. }
  111. /// <summary>
  112. /// 初始化UI组件,从配置加载当前值
  113. /// </summary>
  114. private void InitializeUI()
  115. {
  116. // 初始化分辨率下拉菜单(与硬件分辨率枚举对应)
  117. if (resolutionDropdown != null)
  118. {
  119. resolutionDropdown.ClearOptions();
  120. resolutionDropdown.AddOptions(new System.Collections.Generic.List<string>
  121. {
  122. "QQVGA 160x120",
  123. "QVGA 320x240",
  124. "VGA 640x480"
  125. //"SVGA 800x600",
  126. //"XGA 1024x768",
  127. //"HD 1280x720",
  128. //"SXGA 1280x1024",
  129. //"UXGA 1600x1200"
  130. });
  131. resolutionDropdown.value = (int)config.resolution;
  132. }
  133. // 初始化图片质量输入框(0-63)
  134. if (qualityInputField != null)
  135. {
  136. qualityInputField.text = config.quality.ToString();
  137. qualityInputField.contentType = TMP_InputField.ContentType.IntegerNumber;
  138. qualityInputField.characterLimit = 2; // 0-63,最多2位
  139. }
  140. // 初始化上报时间间隔输入框
  141. if (intervalInputField != null)
  142. {
  143. intervalInputField.text = config.reportIntervalMs.ToString();
  144. intervalInputField.contentType = TMP_InputField.ContentType.IntegerNumber;
  145. }
  146. // 初始化传输开关
  147. if (transmissionToggle != null)
  148. {
  149. transmissionToggle.isOn = config.enableImageTransmission;
  150. }
  151. }
  152. /// <summary>
  153. /// 设置UI事件监听器
  154. /// </summary>
  155. private void SetupEventListeners()
  156. {
  157. // 应用配置按钮
  158. if (applyConfigButton != null)
  159. {
  160. applyConfigButton.onClick.AddListener(OnApplyConfigButtonClicked);
  161. }
  162. if (resetConfigButton != null)
  163. {
  164. resetConfigButton.onClick.AddListener(ResetToDefaultAndSave);
  165. }
  166. // 分辨率下拉菜单变更事件
  167. if (resolutionDropdown != null)
  168. {
  169. resolutionDropdown.onValueChanged.AddListener(OnResolutionChanged);
  170. }
  171. // 图片质量输入框变更事件
  172. if (qualityInputField != null)
  173. {
  174. qualityInputField.onEndEdit.AddListener(OnQualityChanged);
  175. }
  176. // 上报时间间隔输入框变更事件
  177. if (intervalInputField != null)
  178. {
  179. intervalInputField.onEndEdit.AddListener(OnIntervalChanged);
  180. }
  181. // 传输开关变更事件
  182. if (transmissionToggle != null)
  183. {
  184. transmissionToggle.onValueChanged.AddListener(OnTransmissionToggleChanged);
  185. }
  186. }
  187. /// <summary>
  188. /// 分辨率下拉菜单变更处理
  189. /// </summary>
  190. private void OnResolutionChanged(int value)
  191. {
  192. if (value >= 0 && value <= 7)
  193. {
  194. config.resolution = (ImageResolution)value;
  195. NotifyConfigChanged();
  196. SaveToLocal();
  197. Debug.Log($"[UI] 分辨率已更改为: {config.GetResolutionString()}");
  198. }
  199. }
  200. /// <summary>
  201. /// 图片质量输入框变更处理
  202. /// </summary>
  203. private void OnQualityChanged(string value)
  204. {
  205. if (int.TryParse(value, out int quality))
  206. {
  207. // 限制在0-63范围内(与硬件一致)
  208. quality = Mathf.Clamp(quality, 0, 63);
  209. config.quality = quality;
  210. // 如果输入值被修正,更新输入框显示
  211. if (qualityInputField != null && qualityInputField.text != quality.ToString())
  212. {
  213. qualityInputField.text = quality.ToString();
  214. }
  215. NotifyConfigChanged();
  216. SaveToLocal();
  217. Debug.Log($"[UI] 图片质量已更改为: {quality}");
  218. }
  219. else if (!string.IsNullOrEmpty(value))
  220. {
  221. // 输入无效,恢复原值
  222. if (qualityInputField != null)
  223. {
  224. qualityInputField.text = config.quality.ToString();
  225. }
  226. }
  227. }
  228. /// <summary>
  229. /// 上报时间间隔输入框变更处理
  230. /// </summary>
  231. private void OnIntervalChanged(string value)
  232. {
  233. if (int.TryParse(value, out int interval))
  234. {
  235. // 限制最小值为0
  236. interval = Mathf.Max(0, interval);
  237. config.reportIntervalMs = interval;
  238. // 如果输入值被修正,更新输入框显示
  239. if (intervalInputField != null && intervalInputField.text != interval.ToString())
  240. {
  241. intervalInputField.text = interval.ToString();
  242. }
  243. NotifyConfigChanged();
  244. SaveToLocal();
  245. Debug.Log($"[UI] 上报时间间隔已更改为: {interval}ms");
  246. }
  247. else if (!string.IsNullOrEmpty(value))
  248. {
  249. // 输入无效,恢复原值
  250. if (intervalInputField != null)
  251. {
  252. intervalInputField.text = config.reportIntervalMs.ToString();
  253. }
  254. }
  255. }
  256. /// <summary>
  257. /// 传输开关变更处理
  258. /// </summary>
  259. private void OnTransmissionToggleChanged(bool value)
  260. {
  261. config.enableImageTransmission = value;
  262. NotifyConfigChanged();
  263. SaveToLocal();
  264. Debug.Log($"[UI] 图片传输已{(value ? "开启" : "关闭")}");
  265. }
  266. /// <summary>
  267. /// 通知配置已变更
  268. /// </summary>
  269. private void NotifyConfigChanged()
  270. {
  271. onConfigChanged?.Invoke(config);
  272. // 1) 同步到 Python Bridge:用于控制 Unity->Python 转发(toggle + interval等)
  273. if (enableHardwareApply && bridge != null)
  274. {
  275. bridge.SetTransmissionConfig(config);
  276. }
  277. // 1b) 同步到新 SDK Bridge
  278. if (sdkBridge != null)
  279. {
  280. sdkBridge.SetTransmissionConfig(config);
  281. }
  282. // 2) 硬件配置不在每次修改时立刻下发,
  283. // 而是等用户点击“应用配置”按钮时一次性下发。
  284. }
  285. /// <summary>
  286. /// “应用配置”按钮点击:通过 Bridge 将当前配置以 0x40 帧下发给硬件,并同步到 Python 链路
  287. /// </summary>
  288. private void OnApplyConfigButtonClicked()
  289. {
  290. if (!enableHardwareApply)
  291. {
  292. Debug.Log("[UI] 硬件下发已禁用:忽略“应用配置”点击。");
  293. return;
  294. }
  295. if (bridge != null)
  296. {
  297. bridge.SetTransmissionConfig(config);
  298. bridge.ApplyConfig(config);
  299. Debug.Log("[UI] 已将当前图像配置下发给硬件并同步到 Bridge");
  300. }
  301. else if (romaBridge != null)
  302. {
  303. romaBridge.ApplyEsp32HardwareConfig(config);
  304. Debug.Log("[UI][Roma] 已将当前图像配置以 0x40 下发给 ESP32(RomaBridge)");
  305. if (restartRomaPythonAfterHardwareApply)
  306. StartCoroutine(RestartRomaPythonCoroutine());
  307. }
  308. if (sdkBridge != null)
  309. {
  310. sdkBridge.SetTransmissionConfig(config);
  311. sdkBridge.ApplyHardwareConfig(config);
  312. Debug.Log("[UI] 已将当前图像配置下发给硬件并同步到 SDK Bridge");
  313. }
  314. }
  315. /// <summary>
  316. /// 从本地(PlayerPrefs)加载配置;若没有记录则保持当前 Inspector 默认值。
  317. /// </summary>
  318. public void LoadFromLocal()
  319. {
  320. if (PlayerPrefs.HasKey(KeyResolution))
  321. {
  322. int r = PlayerPrefs.GetInt(KeyResolution);
  323. if (r >= 0 && r <= 7)
  324. config.resolution = (ImageResolution)r;
  325. }
  326. if (PlayerPrefs.HasKey(KeyQuality))
  327. {
  328. config.quality = Mathf.Clamp(PlayerPrefs.GetInt(KeyQuality), 0, 63);
  329. }
  330. if (PlayerPrefs.HasKey(KeyInterval))
  331. {
  332. config.reportIntervalMs = Mathf.Max(0, PlayerPrefs.GetInt(KeyInterval));
  333. }
  334. if (PlayerPrefs.HasKey(KeyEnabled))
  335. {
  336. config.enableImageTransmission = PlayerPrefs.GetInt(KeyEnabled) != 0;
  337. }
  338. }
  339. /// <summary>
  340. /// 保存当前配置到本地(PlayerPrefs)。
  341. /// </summary>
  342. public void SaveToLocal()
  343. {
  344. PlayerPrefs.SetInt(KeyResolution, (int)config.resolution);
  345. PlayerPrefs.SetInt(KeyQuality, Mathf.Clamp(config.quality, 0, 63));
  346. PlayerPrefs.SetInt(KeyInterval, Mathf.Max(0, config.reportIntervalMs));
  347. PlayerPrefs.SetInt(KeyEnabled, config.enableImageTransmission ? 1 : 0);
  348. PlayerPrefs.Save();
  349. }
  350. /// <summary>
  351. /// 重置为默认配置:保存到本地,并立刻同步到 Bridge + 下发硬件 0x40(供 Button 调用)。
  352. /// </summary>
  353. public void ResetToDefaultAndSave()
  354. {
  355. // 1) 使用启动时记录的默认值快照重置配置(若快照为空则退回到 ImageTransmissionConfig 默认)
  356. ImageTransmissionConfig resetConfig;
  357. if (_defaultConfigSnapshot != null)
  358. {
  359. resetConfig = new ImageTransmissionConfig
  360. {
  361. resolution = _defaultConfigSnapshot.resolution,
  362. quality = _defaultConfigSnapshot.quality,
  363. reportIntervalMs = _defaultConfigSnapshot.reportIntervalMs,
  364. enableImageTransmission = _defaultConfigSnapshot.enableImageTransmission
  365. };
  366. }
  367. else
  368. {
  369. resetConfig = new ImageTransmissionConfig();
  370. }
  371. // 重置并刷新 UI + 同步到 Bridge(Unity->Python 链路)
  372. SetConfig(resetConfig);
  373. // 2) 保存到本地
  374. SaveToLocal();
  375. // 3) 立刻下发硬件 0x40(只做一次的自动下发逻辑是 Bridge 自己的,这里是“用户点重置”的显式下发)
  376. if (!enableHardwareApply)
  377. {
  378. Debug.Log("[UI] 硬件下发已禁用:已重置并保存本地,但不下发 0x40。");
  379. }
  380. else if (bridge != null)
  381. {
  382. bridge.SetTransmissionConfig(config);
  383. bridge.ApplyConfig(config);
  384. Debug.Log("[UI] 已重置图像配置:已保存本地,并立刻下发 0x40 到硬件 + 同步到 Bridge");
  385. }
  386. else if (romaBridge != null)
  387. {
  388. romaBridge.ApplyEsp32HardwareConfig(config);
  389. Debug.Log("[UI][Roma] 已重置图像配置:已保存本地,并立刻下发 0x40 到 ESP32(RomaBridge)");
  390. if (restartRomaPythonAfterHardwareApply)
  391. StartCoroutine(RestartRomaPythonCoroutine());
  392. }
  393. if (sdkBridge != null)
  394. {
  395. sdkBridge.SetTransmissionConfig(config);
  396. sdkBridge.ApplyHardwareConfig(config);
  397. Debug.Log("[UI] 已重置图像配置:已保存本地,并立刻下发 0x40 到硬件 + 同步到 SDK Bridge");
  398. }
  399. }
  400. /// <summary>
  401. /// 获取当前配置(供外部访问)
  402. /// </summary>
  403. public ImageTransmissionConfig GetConfig()
  404. {
  405. return config;
  406. }
  407. /// <summary>
  408. /// 设置配置(供外部设置)
  409. /// </summary>
  410. public void SetConfig(ImageTransmissionConfig newConfig)
  411. {
  412. if (newConfig == null) return;
  413. config = newConfig;
  414. // 更新UI显示
  415. if (resolutionDropdown != null)
  416. {
  417. resolutionDropdown.value = (int)config.resolution;
  418. }
  419. if (qualityInputField != null)
  420. {
  421. qualityInputField.text = config.quality.ToString();
  422. }
  423. if (intervalInputField != null)
  424. {
  425. intervalInputField.text = config.reportIntervalMs.ToString();
  426. }
  427. if (transmissionToggle != null)
  428. {
  429. transmissionToggle.isOn = config.enableImageTransmission;
  430. }
  431. NotifyConfigChanged();
  432. }
  433. private IEnumerator RestartRomaPythonCoroutine()
  434. {
  435. var romaMgr = RomaManager.Instance;
  436. if (romaMgr == null || romaMgr.romaPythonController == null) yield break;
  437. // 仅在运行中才重启,避免无意义启动
  438. if (!romaMgr.romaPythonController.IsRunning) yield break;
  439. Debug.Log("[UI][Roma] Hardware config applied, restarting Roma Python to recover streaming...");
  440. romaMgr.romaPythonController.StopPython();
  441. if (restartRomaPythonDelaySeconds > 0f)
  442. yield return new WaitForSeconds(restartRomaPythonDelaySeconds);
  443. romaMgr.romaPythonController.StartPython();
  444. }
  445. private void OnDestroy()
  446. {
  447. // 清理事件监听器
  448. if (applyConfigButton != null)
  449. {
  450. applyConfigButton.onClick.RemoveListener(OnApplyConfigButtonClicked);
  451. }
  452. if (resetConfigButton != null)
  453. {
  454. resetConfigButton.onClick.RemoveListener(ResetToDefaultAndSave);
  455. }
  456. if (resolutionDropdown != null)
  457. {
  458. resolutionDropdown.onValueChanged.RemoveListener(OnResolutionChanged);
  459. }
  460. if (qualityInputField != null)
  461. {
  462. qualityInputField.onEndEdit.RemoveListener(OnQualityChanged);
  463. }
  464. if (intervalInputField != null)
  465. {
  466. intervalInputField.onEndEdit.RemoveListener(OnIntervalChanged);
  467. }
  468. if (transmissionToggle != null)
  469. {
  470. transmissionToggle.onValueChanged.RemoveListener(OnTransmissionToggleChanged);
  471. }
  472. }
  473. }
  474. }