using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Kill.Bluetooth; using Kill.Core; using Kill.Managers; using Kill.Network; using Kill.UI.Components; using Newtonsoft.Json; using UnityEngine; using UnityEngine.UI; namespace Kill.UI.Pages { public class FirmwareUpdatePage : MonoBehaviour { public GameObject latestTip; public GameObject getNewVersionTip; public Text versionText; public Text changeLogText; public Button backButton; public Button updateButton; // WiFi OTA 页内进度条(非全屏,不阻塞操作) public GameObject progressPanel; public Image progressBar; // Fill Method = Horizontal, Fill Origin = Left public Text progressText; OTAData otaData; // 当前设备持久化的 OTA transferId(WiFi 模式),空表示无进行中的传输 private const string OTA_TRANSFER_ID_KEY = "otaTransferId"; private const string OTA_TRANSFER_VER_KEY = "otaTransferVer"; private const string OTA_TRANSFER_STATUS_KEY = "otaTransferStatus"; private const string OTA_TRANSFER_DEADLINE_KEY = "otaTransferDeadline"; // 等设备重启的绝对截止时间戳(Unix 秒) // 等设备重启的总时长(秒);持久化截止时间,避免退出/重进页面时计时重置 private const int OTA_WAIT_DEADLINE_SECONDS = 30; // status: "pushing"=传输中, "waiting_reboot"=传输完成等待设备重启/重连 private string currentTransferId; private string currentTransferStatus; private bool isUpdating; // OTA升级进行中(防连点重入) void Start() { UIManager.Instance.RegisterBackAction(Back); backButton.onClick.RemoveAllListeners(); backButton.onClick.AddListener(Back); updateButton.onClick.RemoveAllListeners(); updateButton.onClick.AddListener(OnUpdateButtonClick); updateButton.gameObject.SetActive(false); if (progressPanel != null) progressPanel.SetActive(false); GetOtaInfo(); } public async void GetOtaInfo() { LoadingUI.Show(); var response = await NetworkCtrl.Instance.Get("/api/v1/ota/latest"); LoadingUI.Hide(); ResponseCodeHandler.HandleResponse(response, onSuccess: (data) => { otaData = data.data; Debug.Log($"加载OTA信息成功,版本: {otaData.version}"); InitOtaUI(); }, onError:(code,message)=> { Debug.LogError($"加载OTA信息失败: {message}"); } ); } public void InitOtaUI() { string nowVersion = DataManager.Instance.selectedDevice.firmware_version; string[] versionParts = otaData.version.Split('.'); string[] nowVersionParts = nowVersion.Split('.'); int nowversionNum = 0; int versionNum = 0; int index=1; for(int i=nowVersionParts.Length-1;i>=0;i--) { nowversionNum += int.Parse(nowVersionParts[i]) * 10*index++; } index = 1; for (int i = versionParts.Length - 1; i >= 0; i--) { versionNum += int.Parse(versionParts[i]) * 10 * index++; } Debug.Log($"当前版本: {nowVersion} ({nowversionNum}), 最新版本: {otaData.version} ({versionNum})"); bool hasNewVersion = versionNum > nowversionNum; if(hasNewVersion) { latestTip.SetActive(false); getNewVersionTip.SetActive(true); changeLogText.gameObject.SetActive(true); versionText=getNewVersionTip.transform.Find("value").GetComponent(); versionText.text = otaData.version; changeLogText.text = otaData.release_notes; updateButton.gameObject.SetActive(true); } else { latestTip.SetActive(true); getNewVersionTip.SetActive(false); updateButton.gameObject.SetActive(false); changeLogText.gameObject.SetActive(false); versionText = latestTip.transform.Find("value").GetComponent(); versionText.text = otaData.version; } // 进入页面:若本地有未结束的 transferId,则恢复订阅进度(非全屏,不阻塞) TryResumeWifiOta(); } public void Back() { UIManager.Instance.RegisterBackAction(GetComponentInParent().Back); Destroy(gameObject); Screen.sleepTimeout = SleepTimeout.SystemSetting; } public async void OnUpdateButtonClick() { // 防连点:升级流程进行中忽略重复触发,避免并发启动两个OTA if (isUpdating) return; isUpdating = true; Screen.sleepTimeout = SleepTimeout.NeverSleep; // 仅蓝牙连接:不支持直接OTA,提示先配置WiFi if (DataManager.Instance.hasBluetooth && !DataManager.Instance.hasWifi) { ToastUI.Show("100333"); isUpdating = false; return; } // WiFi 模式:push OTA 指令,SSE 订阅传输进度(非全屏,不阻塞操作,可关闭页面后下次进入继续订阅) if (DataManager.Instance.hasWifi) { try { var pushRequest = new OTAPushRequest { ble_mac = DataManager.Instance.selectedDevice.ble_mac, version = otaData.version }; var pushResponse = await NetworkCtrl.Instance.Post("/api/v1/ota/push", pushRequest); // 提取 transferId:优先从 data.transferId,否则在 code=608(已有OTA进行中)时从 message 解析 "transferId=xxx" string transferId = null; if (pushResponse.Data != null && pushResponse.Data.data != null && !string.IsNullOrEmpty(pushResponse.Data.data.transferId)) { transferId = pushResponse.Data.data.transferId; } else if (pushResponse.Data != null && pushResponse.Data.code == 608 && !string.IsNullOrEmpty(pushResponse.Data.message)) { var match = System.Text.RegularExpressions.Regex.Match(pushResponse.Data.message, @"transferId\s*=\s*([A-Za-z0-9_\-]+)"); if (match.Success) { transferId = match.Groups[1].Value; Debug.Log($"WiFi OTA push 返回 code=608(已有OTA进行中),从message提取 transferId: {transferId}"); } } if (!string.IsNullOrEmpty(transferId)) { Debug.Log($"WiFi OTA push 成功,transferId: {transferId}"); // 持久化 transferId,订阅后台进行,不阻塞页面操作 SaveTransferId(transferId, otaData.version, "pushing"); StartWifiOtaSubscription(transferId); } else { ToastUI.Show("100227"); Debug.LogError($"WiFi OTA push 失败: {pushResponse.ErrorMessage ?? pushResponse.RawResponse}"); } } catch (System.Exception ex) { Debug.LogError($"WiFi OTA push 异常: {ex.Message}"); ToastUI.Show("100227"); } finally { isUpdating = false; } return; } // BLE 模式:下载固件 → 校验 → 蓝牙传输 ToastUI.Show("100294"); UpdateLoadingUI.Instance.Show(); string savePath = Application.persistentDataPath + "/ota.bin"; var downloadResult = await NetworkCtrl.Instance.DownloadFile(otaData.download_url, savePath, UpdateLoadingUI.Instance.GetDownloadProcess); UpdateLoadingUI.Instance.Hide(); if (!downloadResult.IsSuccess) { ToastUI.Show("100224"); isUpdating = false; return; } string md5 = NetworkCtrl.Instance.CalculateMD5(savePath); if (md5 != otaData.md5) { ToastUI.Show("100223"); Debug.LogError("下载的文件MD5校验失败"); File.Delete(savePath); isUpdating = false; return; } LoadFirmwareFile(savePath); if (_selectedFirmwareData == null) { isUpdating = false; return; } ToastUI.Show("100225"); UpdateLoadingUI.Instance.SetPrecent(0); UpdateLoadingUI.Instance.Show(); uint firmwareVersion = ParseFirmwareVersion(otaData.version); OTAManager.Instance.OnTransferProgress += OnOTATransferProgress; // OTA 传输期间(进度显示)清空返回事件,返回键走两次返回退出App,防止误返回中断升级(成败都会返回主页) UIManager.Instance.ClearBackAction(); StartCoroutine(OTAManager.Instance.PerformFullUpgrade(_selectedFirmwareData, firmwareVersion, (success) => { Loom.QueueOnMainThread(() => { isUpdating = false; if (success) { DataManager.Instance.selectedDevice.firmware_version = otaData.version; ToastUI.Show("100226"); // OTA 成功后等待 15s 再返回主页(设备升级后需要时间重启) StartCoroutine(DelayedGoHome(15f)); } else { ToastUI.Show("100227"); UpdateLoadingUI.Instance.Hide(); UIManager.Instance.OpenMainPage(UIManager.PageName.homePage); } }); })); } // 选中的固件文件数据 private byte[] _selectedFirmwareData; private string _selectedFirmwarePath; /// /// 延迟指定时间后返回主页 /// private IEnumerator DelayedGoHome(float delay) { yield return new WaitForSeconds(delay); UpdateLoadingUI.Instance.Hide(); UIManager.Instance.OpenMainPage(UIManager.PageName.homePage); } /// /// 订阅 WiFi OTA 传输进度(SSE),返回是否传输完成(仅供兼容旧调用,BLE 仍走全屏 OTAManager) /// private Task SubscribeOtaProgress(string transferId, Action onProgress) { var tcs = new TaskCompletionSource(); StartCoroutine(SSELegacyCoroutine(transferId, onProgress, success => tcs.TrySetResult(success))); return tcs.Task; } private IEnumerator SSELegacyCoroutine(string transferId, Action onProgress, Action onDone) { bool finished = false; yield return SubscribeOtaProgressCoroutine(transferId, onProgress, done => finished = done); onDone?.Invoke(finished); } /// /// 保存当前设备的 OTA transferId(与设备mac绑定,切换设备不影响) /// private void SaveTransferId(string transferId, string version, string status) { string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; PlayerPrefs.SetString(GetTransferIdKey(mac), transferId); PlayerPrefs.SetString(GetTransferVerKey(mac), version ?? ""); PlayerPrefs.SetString(GetTransferStatusKey(mac), status ?? ""); PlayerPrefs.Save(); currentTransferId = transferId; currentTransferStatus = status; } private void UpdateTransferStatus(string status) { string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; PlayerPrefs.SetString(GetTransferStatusKey(mac), status ?? ""); PlayerPrefs.Save(); currentTransferStatus = status; } /// /// 设置等待设备重启的绝对截止时间戳(Unix 秒)。退出页面再回来仍按此截止时间判定,不会重置。 /// private void SetWaitDeadline() { string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; long deadline = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + OTA_WAIT_DEADLINE_SECONDS; PlayerPrefs.SetString(GetTransferDeadlineKey(mac), deadline.ToString()); PlayerPrefs.Save(); } private long LoadWaitDeadline() { string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; string v = PlayerPrefs.GetString(GetTransferDeadlineKey(mac), ""); if (long.TryParse(v, out long d)) return d; return 0; } /// /// 读取当前设备的 OTA transferId(空表示无) /// private string LoadTransferId() { string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; return PlayerPrefs.GetString(GetTransferIdKey(mac), ""); } /// /// 清除当前设备的 OTA transferId /// private void ClearTransferId() { string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; PlayerPrefs.DeleteKey(GetTransferIdKey(mac)); PlayerPrefs.DeleteKey(GetTransferVerKey(mac)); PlayerPrefs.DeleteKey(GetTransferStatusKey(mac)); PlayerPrefs.DeleteKey(GetTransferDeadlineKey(mac)); PlayerPrefs.Save(); currentTransferId = null; currentTransferStatus = null; } private string GetTransferIdKey(string mac) => OTA_TRANSFER_ID_KEY + "_" + mac; private string GetTransferVerKey(string mac) => OTA_TRANSFER_VER_KEY + "_" + mac; private string GetTransferStatusKey(string mac) => OTA_TRANSFER_STATUS_KEY + "_" + mac; private string GetTransferDeadlineKey(string mac) => OTA_TRANSFER_DEADLINE_KEY + "_" + mac; /// /// 启动 WiFi OTA 订阅:仅更新页内小进度条,不阻塞页面,可关闭/重入 /// private void StartWifiOtaSubscription(string transferId) { ShowProgressPanel(0); updateButton.gameObject.SetActive(false); // 升级中禁用更新按钮 StartCoroutine(SubscribeAndHandle(transferId)); } /// /// 订阅协程:完成/失败/连接结束统一处理(不强制回主页) /// private IEnumerator SubscribeAndHandle(string transferId) { bool finished = false; // SSE 回调本身在主线程,直接同步赋值即可(不再绕 Loom,否则 yield 之后判断时 finished 仍是旧值) yield return SubscribeOtaProgressCoroutine(transferId, p => ShowProgressPanel(p), done => finished = done); // 协程结束:根据是否完成做后续 if (finished) { ShowProgressPanel(100); // 传输完成 ≠ 设备升级完成:等待设备重启/重连,并以新版本号上线后再清理 transferId、刷新 UI UpdateTransferStatus("waiting_reboot"); // 仅在尚未设置过截止时间时才设置(避免重复订阅重置倒计时) if (LoadWaitDeadline() <= 0) SetWaitDeadline(); StartCoroutine(WaitDeviceRebootAndFinish(transferId)); } else { // 中途失败/中断 ToastUI.Show("100227"); ClearTransferId(); HideProgressPanel(); updateButton.gameObject.SetActive(true); } } /// /// 传输完成后等待设备重启+重连:轮询设备列表直到当前设备以新版本号上线,否则按持久化的绝对截止时间超时退出。 /// 期间保持 transferId 持久化、保留页内进度条显示「升级中」文案,重新进入页面也能继续等待(截止时间不会重置)。 /// private IEnumerator WaitDeviceRebootAndFinish(string transferId) { const float pollInterval = 3f; // 轮询间隔 long deadline = LoadWaitDeadline(); // 兜底:若没设置过截止时间,立即设置一个 if (deadline <= 0) { SetWaitDeadline(); deadline = LoadWaitDeadline(); } // 已超过绝对截止时间:直接走超时分支并清掉 transferId,避免每次进入都重复提示 if (DateTimeOffset.UtcNow.ToUnixTimeSeconds() >= deadline) { ClearTransferId(); HideProgressPanel(); updateButton.gameObject.SetActive(true); ToastUI.Show("ota_update_failed"); yield break; } ShowProgressPanelWaitingForDevice(); ToastUI.Show("100226"); // 传输完成提示 string targetVersion = otaData != null ? otaData.version : ""; bool rebooted = false; while (DateTimeOffset.UtcNow.ToUnixTimeSeconds() < deadline) { yield return new WaitForSeconds(pollInterval); if (gameObject == null) yield break; // 页面已销毁 // 仅当持久化的还是同一个 transferId 时才继续等待(用户重新点击/切换设备会清掉) if (LoadTransferId() != transferId) yield break; string nowVersion = null; yield return FetchDeviceFirmwareVersion(r => nowVersion = r); if (!string.IsNullOrEmpty(nowVersion) && IsVersionGE(nowVersion, targetVersion)) { rebooted = true; break; } // 期间截止时间可能被重置(用户重新点击升级等),以最新值为准 long currentDeadline = LoadWaitDeadline(); if (currentDeadline > 0 && currentDeadline != deadline) deadline = currentDeadline; } if (gameObject == null) yield break; if (LoadTransferId() != transferId) yield break; if (rebooted) { // 设备已用新版本上线,更新本地版本并清理状态 if (DataManager.Instance.selectedDevice != null && !string.IsNullOrEmpty(targetVersion)) { DataManager.Instance.selectedDevice.firmware_version = targetVersion; } ClearTransferId(); HideProgressPanel(); InitOtaUI(); // 重新判断:已是最新 / 又有新版本 } else { // 超时:升级失败,提示并清理 transferId(避免下次进入页面再触发) ToastUI.Show("ota_update_failed"); ClearTransferId(); HideProgressPanel(); updateButton.gameObject.SetActive(true); } } /// /// 拉取当前设备的最新 firmware_version(仅赋值给回调,返回空表示失败/离线) /// private IEnumerator FetchDeviceFirmwareVersion(Action onResult) { string result = null; var task = FetchDeviceFirmwareVersionAsync(v => result = v); while (!task.IsCompleted) yield return null; onResult?.Invoke(result); } private async Task FetchDeviceFirmwareVersionAsync(Action onResult) { try { if (DataManager.Instance.userInfo == null || string.IsNullOrEmpty(DataManager.Instance.userInfo.id)) { onResult?.Invoke(null); return; } string url = $"/api/v1/device/user/list?user_id={DataManager.Instance.userInfo.id}"; var response = await NetworkCtrl.Instance.Get(url); if (response == null || !response.IsSuccess || response.Data == null || response.Data.data == null) { onResult?.Invoke(null); return; } string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; var list = response.Data.data.owned_devices; if (list != null) { foreach (var d in list) { if (d != null && d.ble_mac == mac) { onResult?.Invoke(d.firmware_version); return; } } } var shared = response.Data.data.shared_devices; if (shared != null) { foreach (var d in shared) { if (d != null && d.ble_mac == mac) { onResult?.Invoke(d.firmware_version); return; } } } onResult?.Invoke(null); } catch (System.Exception ex) { Debug.LogError($"查询设备版本失败: {ex.Message}"); onResult?.Invoke(null); } } /// /// 比较版本号:a >= b 返回 true(按 "." 分段整数比较) /// private bool IsVersionGE(string a, string b) { if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false; string[] ap = a.Split('.'); string[] bp = b.Split('.'); int len = Mathf.Max(ap.Length, bp.Length); for (int i = 0; i < len; i++) { int av = 0, bv = 0; if (i < ap.Length) int.TryParse(ap[i], out av); if (i < bp.Length) int.TryParse(bp[i], out bv); if (av > bv) return true; if (av < bv) return false; } return true; } /// /// 进度条切换到「升级中...」状态(fill 保持满,文字提示) /// private void ShowProgressPanelWaitingForDevice() { if (progressPanel != null) progressPanel.SetActive(true); if (progressBar != null) progressBar.fillAmount = 1f; if (progressText != null && LanguageManager.Instance != null) progressText.text = LanguageManager.Instance.GetLanguage("ota_waiting_reboot"); } /// /// 进入页面时:若本地存在 transferId,则恢复订阅并显示进度条 /// private void TryResumeWifiOta() { string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; string savedId = LoadTransferId(); string savedStatus = PlayerPrefs.GetString(GetTransferStatusKey(mac), ""); if (string.IsNullOrEmpty(savedId)) return; currentTransferId = savedId; currentTransferStatus = savedStatus; updateButton.gameObject.SetActive(false); // 已经传输完成等待设备重启:直接走轮询分支,不重新订阅 SSE if (savedStatus == "waiting_reboot") { ShowProgressPanelWaitingForDevice(); StartCoroutine(WaitDeviceRebootAndFinish(savedId)); return; } ShowProgressPanel(0); StartCoroutine(SubscribeAndHandle(savedId)); } /// /// 仅订阅进度协程,传出是否完成(不弹出后续处理) /// private IEnumerator SubscribeOtaProgressCoroutine(string transferId, Action onProgress, Action onDone) { bool transferFinished = false; string sseUrl = $"/api/v1/ota/transfer/{transferId}/progress"; var queryParams = new Dictionary { { "token", DataManager.Instance.token } }; yield return HttpRequestManager.Instance.StartSSE(sseUrl, queryParams, null, data => { try { var info = JsonConvert.DeserializeObject(data); if (info == null) return; onProgress?.Invoke(info.percentage); string status = (info.status ?? "").ToLower(); Debug.Log($"OTA 进度: {status}, {info.percentage}%"); if (info.percentage >= 100 || status.Contains("completed")) { transferFinished = true; } } catch (System.Exception ex) { Debug.LogError($"OTA 进度解析失败: {ex.Message}"); } }, err => { Debug.LogError($"OTA 进度连接错误: {err}"); }); onDone?.Invoke(transferFinished); } /// /// 显示页内进度条并更新百分比 /// private void ShowProgressPanel(int percent) { if (progressPanel != null) progressPanel.SetActive(true); if (progressBar != null) progressBar.fillAmount = Mathf.Clamp01(percent / 100f); if (progressText != null) progressText.text = $"{percent}%"; } /// /// 隐藏页内进度条 /// private void HideProgressPanel() { if (progressBar != null) progressBar.fillAmount = 0f; if (progressText != null) progressText.text = "0%"; if (progressPanel != null) progressPanel.SetActive(false); } /// /// 加载固件文件 /// private void LoadFirmwareFile(string filePath) { try { // 获取文件大小 FileInfo fileInfo = new FileInfo(filePath); long fileSize = fileInfo.Length; // 读取文件 _selectedFirmwareData = File.ReadAllBytes(filePath); _selectedFirmwarePath = filePath; string fileName = Path.GetFileName(filePath); } catch (System.Exception) { ToastUI.Show("100224"); LoadingUI.Hide(); } } /// /// 解析固件版本号 /// private uint ParseFirmwareVersion(string version) { try { string[] parts = version.Split('.'); if (parts.Length >= 3 && uint.TryParse(parts[0], out uint v0) && uint.TryParse(parts[1], out uint v1) && uint.TryParse(parts[2], out uint v2)) { // 版本号格式:主版本.次版本.修订号(可含第4段) // 按每段2位十进制压缩,与设备端编码一致(如 00.00.01.05 -> 105) uint v3 = 0; if (parts.Length >= 4) uint.TryParse(parts[3], out v3); return v0 * 1000000 + v1 * 10000 + v2 * 100 + v3; } } catch { } // 默认返回版本号 1 return 1; } /// /// OTA传输进度回调 /// private void OnOTATransferProgress(int current, int total, int retryCount, int maxRetries) { UpdateLoadingUI.Instance.SetPrecent((float)current/ total); } } }