killapp/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs
“虞渠成” b43121d2ec refactor(FirmwareUpdatePage): 重构固件升级流程逻辑
1. 拆分同步和异步点击处理方法,优化防连点逻辑
2. 简化等待设备重启的流程,不再轮询设备版本
3. 调整升级状态的清理和恢复时机
2026-08-27 14:18:31 +08:00

647 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 transferIdWiFi 模式),空表示无进行中的传输
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<OTAResponse>("/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<Text>();
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<Text>();
versionText.text = otaData.version;
}
// 进入页面:若本地有未结束的 transferId则恢复订阅进度非全屏不阻塞
TryResumeWifiOta();
}
public void Back()
{
UIManager.Instance.RegisterBackAction(GetComponentInParent<DeviceInfoPage>().Back);
Destroy(gameObject);
Screen.sleepTimeout = SleepTimeout.SystemSetting;
}
public void OnUpdateButtonClick()
{
// 防连点升级流程进行中忽略重复触发避免并发启动两个OTA
if (isUpdating) return;
isUpdating = true;
// 立刻隐藏更新按钮,防止连点期间按钮仍可点
updateButton.gameObject.SetActive(false);
Screen.sleepTimeout = SleepTimeout.NeverSleep;
OnUpdateButtonClickAsync();
}
private async void OnUpdateButtonClickAsync()
{
// 仅蓝牙连接不支持直接OTA提示先配置WiFi
if (DataManager.Instance.hasBluetooth && !DataManager.Instance.hasWifi)
{
ToastUI.Show("100333");
isUpdating = false;
updateButton.gameObject.SetActive(true);
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<OTAPushResponse>("/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;
updateButton.gameObject.SetActive(true);
return;
}
string md5 = NetworkCtrl.Instance.CalculateMD5(savePath);
if (md5 != otaData.md5)
{
ToastUI.Show("100223");
Debug.LogError("下载的文件MD5校验失败");
File.Delete(savePath);
isUpdating = false;
updateButton.gameObject.SetActive(true);
return;
}
LoadFirmwareFile(savePath);
if (_selectedFirmwareData == null)
{
isUpdating = false;
updateButton.gameObject.SetActive(true);
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;
/// <summary>
/// 延迟指定时间后返回主页
/// </summary>
private IEnumerator DelayedGoHome(float delay)
{
yield return new WaitForSeconds(delay);
UpdateLoadingUI.Instance.Hide();
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
}
/// <summary>
/// 订阅 WiFi OTA 传输进度SSE返回是否传输完成仅供兼容旧调用BLE 仍走全屏 OTAManager
/// </summary>
private Task<bool> SubscribeOtaProgress(string transferId, Action<int> onProgress)
{
var tcs = new TaskCompletionSource<bool>();
StartCoroutine(SSELegacyCoroutine(transferId, onProgress, success => tcs.TrySetResult(success)));
return tcs.Task;
}
private IEnumerator SSELegacyCoroutine(string transferId, Action<int> onProgress, Action<bool> onDone)
{
bool finished = false;
yield return SubscribeOtaProgressCoroutine(transferId, onProgress, done => finished = done);
onDone?.Invoke(finished);
}
/// <summary>
/// 保存当前设备的 OTA transferId与设备mac绑定切换设备不影响
/// </summary>
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;
}
/// <summary>
/// 设置等待设备重启的绝对截止时间戳Unix 秒)。退出页面再回来仍按此截止时间判定,不会重置。
/// </summary>
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;
}
/// <summary>
/// 读取当前设备的 OTA transferId空表示无
/// </summary>
private string LoadTransferId()
{
string mac = DataManager.Instance.selectedDevice?.ble_mac ?? "";
return PlayerPrefs.GetString(GetTransferIdKey(mac), "");
}
/// <summary>
/// 清除当前设备的 OTA transferId
/// </summary>
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;
/// <summary>
/// 启动 WiFi OTA 订阅:仅更新页内小进度条,不阻塞页面,可关闭/重入
/// </summary>
private void StartWifiOtaSubscription(string transferId)
{
ShowProgressPanel(0);
updateButton.gameObject.SetActive(false); // 升级中禁用更新按钮
StartCoroutine(SubscribeAndHandle(transferId));
}
/// <summary>
/// 订阅协程:完成/失败/连接结束统一处理(不强制回主页)
/// </summary>
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);
}
}
/// <summary>
/// 传输完成后的过渡等3s → 提示 100226 → 返回主页。
/// 设备重启/版本号刷新由后台下次进入页面或下次 OTA 拉取时再处理。
/// </summary>
private IEnumerator WaitDeviceRebootAndFinish(string transferId)
{
ShowProgressPanelWaitingForDevice();
// 等3s 提示传输完成(给用户缓冲),然后返回主页
yield return new WaitForSeconds(3f);
if (gameObject == null) yield break;
// 仅当持久化的还是同一个 transferId 时再继续(用户可能已点过别的)
if (LoadTransferId() != transferId) yield break;
ToastUI.Show("100226");
// 返回主页transferId 保留,下次进入页面仍能识别为「传输已完成、待重启确认」
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
}
/// <summary>
/// 拉取当前设备的最新 firmware_version仅赋值给回调返回空表示失败/离线)
/// </summary>
private IEnumerator FetchDeviceFirmwareVersion(Action<string> onResult)
{
string result = null;
var task = FetchDeviceFirmwareVersionAsync(v => result = v);
while (!task.IsCompleted) yield return null;
onResult?.Invoke(result);
}
private async Task FetchDeviceFirmwareVersionAsync(Action<string> 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<DeviceListResponse>(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);
}
}
/// <summary>
/// 比较版本号a >= b 返回 true按 "." 分段整数比较)
/// </summary>
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;
}
/// <summary>
/// 进度条切换到「升级中...」状态fill 保持满,文字提示)
/// </summary>
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");
}
/// <summary>
/// 进入页面时:若本地存在 transferId则恢复订阅并显示进度条
/// </summary>
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);
// 已经传输完成等待设备重启:直接清理状态、恢复更新按钮(实际设备升级由下次进入主页/下次 OTA 拉取时确认)
if (savedStatus == "waiting_reboot")
{
ClearTransferId();
HideProgressPanel();
updateButton.gameObject.SetActive(true);
return;
}
ShowProgressPanel(0);
StartCoroutine(SubscribeAndHandle(savedId));
}
/// <summary>
/// 仅订阅进度协程,传出是否完成(不弹出后续处理)
/// </summary>
private IEnumerator SubscribeOtaProgressCoroutine(string transferId, Action<int> onProgress, Action<bool> onDone)
{
bool transferFinished = false;
string sseUrl = $"/api/v1/ota/transfer/{transferId}/progress";
var queryParams = new Dictionary<string, string> { { "token", DataManager.Instance.token } };
yield return HttpRequestManager.Instance.StartSSE(sseUrl, queryParams, null,
data =>
{
try
{
var info = JsonConvert.DeserializeObject<OTATransferInfo>(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);
}
/// <summary>
/// 显示页内进度条并更新百分比
/// </summary>
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}%";
}
/// <summary>
/// 隐藏页内进度条
/// </summary>
private void HideProgressPanel()
{
if (progressBar != null) progressBar.fillAmount = 0f;
if (progressText != null) progressText.text = "0%";
if (progressPanel != null) progressPanel.SetActive(false);
}
/// <summary>
/// 加载固件文件
/// </summary>
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();
}
}
/// <summary>
/// 解析固件版本号
/// </summary>
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;
}
/// <summary>
/// OTA传输进度回调
/// </summary>
private void OnOTATransferProgress(int current, int total, int retryCount, int maxRetries)
{
UpdateLoadingUI.Instance.SetPrecent((float)current/ total);
}
}
}