using UnityEngine; using UnityEngine.UI; using TMPro; using LightGlue.Unity.Config; using LightGlue.Unity.Bridge; using LightGlue.Unity.Game; using LightGlue.Unity.Roma; using LightGlue.Unity.Roma.Bridge; using LightGlue.Unity.Sdk.Unity; using System.Collections; namespace LightGlue.Unity.UI { /// /// 图像传输配置UI控制器 /// - 提供分辨率、质量、上报时间间隔、传输开关等控件 /// - 将配置同步到 Bridge:Python 链路(开关/间隔)+ 硬件 0x40 参数下发(由 Bridge 统一管理) /// public class ImageTransmissionUIController : MonoBehaviour { private const string KeyPrefix = "LightGlue_ImageTx_"; private const string KeyResolution = KeyPrefix + "Resolution"; private const string KeyQuality = KeyPrefix + "Quality"; private const string KeyInterval = KeyPrefix + "IntervalMs"; private const string KeyEnabled = KeyPrefix + "Enabled"; [Header("UI组件引用")] [Tooltip("图片分辨率下拉菜单")] public TMP_Dropdown resolutionDropdown; [Tooltip("图片质量输入框")] public TMP_InputField qualityInputField; [Tooltip("上报时间间隔输入框 (毫秒)")] public TMP_InputField intervalInputField; [Tooltip("开启图片传输开关(主要控制 Python 链路与硬件采图开关状态)")] public Toggle transmissionToggle; [Tooltip("应用配置按钮(点击时将当前配置一次性下发给硬件)")] public Button applyConfigButton; [Tooltip("重置为默认图像配置并立即下发硬件的按钮")] public Button resetConfigButton; [Header("配置")] [Tooltip("图像传输配置")] public ImageTransmissionConfig config = new ImageTransmissionConfig(); [Header("硬件下发开关")] [Tooltip("是否启用硬件下发相关功能(0x40)。关闭后会隐藏下发按钮,且不会在启动/配置变更时触发任何硬件下发动作。")] public bool enableHardwareApply = true; [Tooltip("关闭硬件下发时,是否隐藏“应用配置/重置并下发”按钮。")] public bool hideHardwareApplyButtonsWhenDisabled = true; [Header("Roma: 下发后自动恢复链路")] [Tooltip("在 Roma(ESP32) 下发 0x40 后,是否自动重启 Roma Python(避免硬件重配置导致画面/结果暂时中断)。")] public bool restartRomaPythonAfterHardwareApply = true; [Tooltip("重启 Roma Python 的延迟(秒),给端口/硬件一点恢复时间。")] public float restartRomaPythonDelaySeconds = 0.25f; // 启动时记录的“场景默认配置”(来自 Inspector),仅用于 ResetToDefaultAndSave,不受本地记录覆盖影响。 private ImageTransmissionConfig _defaultConfigSnapshot; [Header("本地记录")] [Tooltip("启动时是否从本地(PlayerPrefs)加载上次保存的图像传输配置")] public bool loadFromLocalOnStart = true; [Header("Bridge连接")] [Tooltip("Bridge(同步传输配置并下发 0x40 到硬件;可选,未设置时从 LightGlueManager 获取)")] public HardwareToPythonUdpBridge bridge; [Tooltip("Roma Bridge(用于 ESP32 模式下发 0x40 到硬件;可选,未设置时从 RomaManager 查找)")] public RomaHardwareToPythonBridge romaBridge; [Tooltip("LightGlue SDK 新桥接组件(可选,用于对比旧 Bridge 行为)")] public LightGlueBridgeBehaviour sdkBridge; [Header("事件")] [Tooltip("配置变更时触发的事件")] public UnityEngine.Events.UnityEvent onConfigChanged; private void Start() { if (bridge == null && LightGlueManager.Instance != null) bridge = LightGlueManager.Instance.bridge; if (romaBridge == null && RomaManager.Instance != null) romaBridge = FindObjectOfType(); // sdkBridge 一般通过 Inspector 指定;不自动从 Manager 获取,避免与旧 Bridge 冲突。 // 1) 先记录当前 Inspector 中的 config 作为“默认值快照”(仅 Reset 使用) _defaultConfigSnapshot = new ImageTransmissionConfig { resolution = config.resolution, quality = config.quality, reportIntervalMs = config.reportIntervalMs, enableImageTransmission = config.enableImageTransmission }; // 2) 再按需从本地记录覆盖运行时 config(不影响默认值快照) if (loadFromLocalOnStart) { LoadFromLocal(); } InitializeUI(); SetupEventListeners(); // 关闭硬件下发时:隐藏按钮并跳过“启动时自动同步到 Bridge”(避免触发 0x40) if (!enableHardwareApply) { if (hideHardwareApplyButtonsWhenDisabled) { if (applyConfigButton != null) applyConfigButton.gameObject.SetActive(false); if (resetConfigButton != null) resetConfigButton.gameObject.SetActive(false); } Debug.Log("[UI] 硬件下发已禁用:将不会在启动/配置变更时触发 0x40 下发。"); return; } // 初始化时同步配置到 Bridge(SetTransmissionConfig 内部会立即下发 0x40,保证硬件从第一帧起就用正确参数) if (bridge != null) { bridge.SetTransmissionConfig(config); bridge.MarkHardwareAutoApplyReady(); } // 同步到新 SDK Bridge,便于对比测试 if (sdkBridge != null) { sdkBridge.SetTransmissionConfig(config); } } /// /// 初始化UI组件,从配置加载当前值 /// private void InitializeUI() { // 初始化分辨率下拉菜单(与硬件分辨率枚举对应) if (resolutionDropdown != null) { resolutionDropdown.ClearOptions(); resolutionDropdown.AddOptions(new System.Collections.Generic.List { "QQVGA 160x120", "QVGA 320x240", "VGA 640x480" //"SVGA 800x600", //"XGA 1024x768", //"HD 1280x720", //"SXGA 1280x1024", //"UXGA 1600x1200" }); resolutionDropdown.value = (int)config.resolution; } // 初始化图片质量输入框(0-63) if (qualityInputField != null) { qualityInputField.text = config.quality.ToString(); qualityInputField.contentType = TMP_InputField.ContentType.IntegerNumber; qualityInputField.characterLimit = 2; // 0-63,最多2位 } // 初始化上报时间间隔输入框 if (intervalInputField != null) { intervalInputField.text = config.reportIntervalMs.ToString(); intervalInputField.contentType = TMP_InputField.ContentType.IntegerNumber; } // 初始化传输开关 if (transmissionToggle != null) { transmissionToggle.isOn = config.enableImageTransmission; } } /// /// 设置UI事件监听器 /// private void SetupEventListeners() { // 应用配置按钮 if (applyConfigButton != null) { applyConfigButton.onClick.AddListener(OnApplyConfigButtonClicked); } if (resetConfigButton != null) { resetConfigButton.onClick.AddListener(ResetToDefaultAndSave); } // 分辨率下拉菜单变更事件 if (resolutionDropdown != null) { resolutionDropdown.onValueChanged.AddListener(OnResolutionChanged); } // 图片质量输入框变更事件 if (qualityInputField != null) { qualityInputField.onEndEdit.AddListener(OnQualityChanged); } // 上报时间间隔输入框变更事件 if (intervalInputField != null) { intervalInputField.onEndEdit.AddListener(OnIntervalChanged); } // 传输开关变更事件 if (transmissionToggle != null) { transmissionToggle.onValueChanged.AddListener(OnTransmissionToggleChanged); } } /// /// 分辨率下拉菜单变更处理 /// private void OnResolutionChanged(int value) { if (value >= 0 && value <= 7) { config.resolution = (ImageResolution)value; NotifyConfigChanged(); SaveToLocal(); Debug.Log($"[UI] 分辨率已更改为: {config.GetResolutionString()}"); } } /// /// 图片质量输入框变更处理 /// private void OnQualityChanged(string value) { if (int.TryParse(value, out int quality)) { // 限制在0-63范围内(与硬件一致) quality = Mathf.Clamp(quality, 0, 63); config.quality = quality; // 如果输入值被修正,更新输入框显示 if (qualityInputField != null && qualityInputField.text != quality.ToString()) { qualityInputField.text = quality.ToString(); } NotifyConfigChanged(); SaveToLocal(); Debug.Log($"[UI] 图片质量已更改为: {quality}"); } else if (!string.IsNullOrEmpty(value)) { // 输入无效,恢复原值 if (qualityInputField != null) { qualityInputField.text = config.quality.ToString(); } } } /// /// 上报时间间隔输入框变更处理 /// private void OnIntervalChanged(string value) { if (int.TryParse(value, out int interval)) { // 限制最小值为0 interval = Mathf.Max(0, interval); config.reportIntervalMs = interval; // 如果输入值被修正,更新输入框显示 if (intervalInputField != null && intervalInputField.text != interval.ToString()) { intervalInputField.text = interval.ToString(); } NotifyConfigChanged(); SaveToLocal(); Debug.Log($"[UI] 上报时间间隔已更改为: {interval}ms"); } else if (!string.IsNullOrEmpty(value)) { // 输入无效,恢复原值 if (intervalInputField != null) { intervalInputField.text = config.reportIntervalMs.ToString(); } } } /// /// 传输开关变更处理 /// private void OnTransmissionToggleChanged(bool value) { config.enableImageTransmission = value; NotifyConfigChanged(); SaveToLocal(); Debug.Log($"[UI] 图片传输已{(value ? "开启" : "关闭")}"); } /// /// 通知配置已变更 /// private void NotifyConfigChanged() { onConfigChanged?.Invoke(config); // 1) 同步到 Python Bridge:用于控制 Unity->Python 转发(toggle + interval等) if (enableHardwareApply && bridge != null) { bridge.SetTransmissionConfig(config); } // 1b) 同步到新 SDK Bridge if (sdkBridge != null) { sdkBridge.SetTransmissionConfig(config); } // 2) 硬件配置不在每次修改时立刻下发, // 而是等用户点击“应用配置”按钮时一次性下发。 } /// /// “应用配置”按钮点击:通过 Bridge 将当前配置以 0x40 帧下发给硬件,并同步到 Python 链路 /// private void OnApplyConfigButtonClicked() { if (!enableHardwareApply) { Debug.Log("[UI] 硬件下发已禁用:忽略“应用配置”点击。"); return; } if (bridge != null) { bridge.SetTransmissionConfig(config); bridge.ApplyConfig(config); Debug.Log("[UI] 已将当前图像配置下发给硬件并同步到 Bridge"); } else if (romaBridge != null) { romaBridge.ApplyEsp32HardwareConfig(config); Debug.Log("[UI][Roma] 已将当前图像配置以 0x40 下发给 ESP32(RomaBridge)"); if (restartRomaPythonAfterHardwareApply) StartCoroutine(RestartRomaPythonCoroutine()); } if (sdkBridge != null) { sdkBridge.SetTransmissionConfig(config); sdkBridge.ApplyHardwareConfig(config); Debug.Log("[UI] 已将当前图像配置下发给硬件并同步到 SDK Bridge"); } } /// /// 从本地(PlayerPrefs)加载配置;若没有记录则保持当前 Inspector 默认值。 /// public void LoadFromLocal() { if (PlayerPrefs.HasKey(KeyResolution)) { int r = PlayerPrefs.GetInt(KeyResolution); if (r >= 0 && r <= 7) config.resolution = (ImageResolution)r; } if (PlayerPrefs.HasKey(KeyQuality)) { config.quality = Mathf.Clamp(PlayerPrefs.GetInt(KeyQuality), 0, 63); } if (PlayerPrefs.HasKey(KeyInterval)) { config.reportIntervalMs = Mathf.Max(0, PlayerPrefs.GetInt(KeyInterval)); } if (PlayerPrefs.HasKey(KeyEnabled)) { config.enableImageTransmission = PlayerPrefs.GetInt(KeyEnabled) != 0; } } /// /// 保存当前配置到本地(PlayerPrefs)。 /// public void SaveToLocal() { PlayerPrefs.SetInt(KeyResolution, (int)config.resolution); PlayerPrefs.SetInt(KeyQuality, Mathf.Clamp(config.quality, 0, 63)); PlayerPrefs.SetInt(KeyInterval, Mathf.Max(0, config.reportIntervalMs)); PlayerPrefs.SetInt(KeyEnabled, config.enableImageTransmission ? 1 : 0); PlayerPrefs.Save(); } /// /// 重置为默认配置:保存到本地,并立刻同步到 Bridge + 下发硬件 0x40(供 Button 调用)。 /// public void ResetToDefaultAndSave() { // 1) 使用启动时记录的默认值快照重置配置(若快照为空则退回到 ImageTransmissionConfig 默认) ImageTransmissionConfig resetConfig; if (_defaultConfigSnapshot != null) { resetConfig = new ImageTransmissionConfig { resolution = _defaultConfigSnapshot.resolution, quality = _defaultConfigSnapshot.quality, reportIntervalMs = _defaultConfigSnapshot.reportIntervalMs, enableImageTransmission = _defaultConfigSnapshot.enableImageTransmission }; } else { resetConfig = new ImageTransmissionConfig(); } // 重置并刷新 UI + 同步到 Bridge(Unity->Python 链路) SetConfig(resetConfig); // 2) 保存到本地 SaveToLocal(); // 3) 立刻下发硬件 0x40(只做一次的自动下发逻辑是 Bridge 自己的,这里是“用户点重置”的显式下发) if (!enableHardwareApply) { Debug.Log("[UI] 硬件下发已禁用:已重置并保存本地,但不下发 0x40。"); } else if (bridge != null) { bridge.SetTransmissionConfig(config); bridge.ApplyConfig(config); Debug.Log("[UI] 已重置图像配置:已保存本地,并立刻下发 0x40 到硬件 + 同步到 Bridge"); } else if (romaBridge != null) { romaBridge.ApplyEsp32HardwareConfig(config); Debug.Log("[UI][Roma] 已重置图像配置:已保存本地,并立刻下发 0x40 到 ESP32(RomaBridge)"); if (restartRomaPythonAfterHardwareApply) StartCoroutine(RestartRomaPythonCoroutine()); } if (sdkBridge != null) { sdkBridge.SetTransmissionConfig(config); sdkBridge.ApplyHardwareConfig(config); Debug.Log("[UI] 已重置图像配置:已保存本地,并立刻下发 0x40 到硬件 + 同步到 SDK Bridge"); } } /// /// 获取当前配置(供外部访问) /// public ImageTransmissionConfig GetConfig() { return config; } /// /// 设置配置(供外部设置) /// public void SetConfig(ImageTransmissionConfig newConfig) { if (newConfig == null) return; config = newConfig; // 更新UI显示 if (resolutionDropdown != null) { resolutionDropdown.value = (int)config.resolution; } if (qualityInputField != null) { qualityInputField.text = config.quality.ToString(); } if (intervalInputField != null) { intervalInputField.text = config.reportIntervalMs.ToString(); } if (transmissionToggle != null) { transmissionToggle.isOn = config.enableImageTransmission; } NotifyConfigChanged(); } private IEnumerator RestartRomaPythonCoroutine() { var romaMgr = RomaManager.Instance; if (romaMgr == null || romaMgr.romaPythonController == null) yield break; // 仅在运行中才重启,避免无意义启动 if (!romaMgr.romaPythonController.IsRunning) yield break; Debug.Log("[UI][Roma] Hardware config applied, restarting Roma Python to recover streaming..."); romaMgr.romaPythonController.StopPython(); if (restartRomaPythonDelaySeconds > 0f) yield return new WaitForSeconds(restartRomaPythonDelaySeconds); romaMgr.romaPythonController.StartPython(); } private void OnDestroy() { // 清理事件监听器 if (applyConfigButton != null) { applyConfigButton.onClick.RemoveListener(OnApplyConfigButtonClicked); } if (resetConfigButton != null) { resetConfigButton.onClick.RemoveListener(ResetToDefaultAndSave); } if (resolutionDropdown != null) { resolutionDropdown.onValueChanged.RemoveListener(OnResolutionChanged); } if (qualityInputField != null) { qualityInputField.onEndEdit.RemoveListener(OnQualityChanged); } if (intervalInputField != null) { intervalInputField.onEndEdit.RemoveListener(OnIntervalChanged); } if (transmissionToggle != null) { transmissionToggle.onValueChanged.RemoveListener(OnTransmissionToggleChanged); } } } }