1. 新增多语言文案用于OTA升级提示 2. 优化更新进度显示为整数百分比 3. 调整设备轮询间隔为5秒 4. 新增SSE流式请求处理与OTA数据模型 5. 实现WiFi环境下基于SSE的OTA升级流程 6. 增加蓝牙环境下OTA升级的前置校验与提示
357 lines
14 KiB
C#
357 lines
14 KiB
C#
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;
|
||
OTAData otaData;
|
||
void Start()
|
||
{
|
||
UIManager.Instance.RegisterBackAction(Back);
|
||
backButton.onClick.RemoveAllListeners();
|
||
backButton.onClick.AddListener(Back);
|
||
updateButton.onClick.RemoveAllListeners();
|
||
updateButton.onClick.AddListener(OnUpdateButtonClick);
|
||
updateButton.gameObject.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;
|
||
}
|
||
}
|
||
public void Back()
|
||
{
|
||
UIManager.Instance.RegisterBackAction(GetComponentInParent<DeviceInfoPage>().Back);
|
||
Destroy(gameObject);
|
||
Screen.sleepTimeout = SleepTimeout.SystemSetting;
|
||
}
|
||
private bool isUpdating; // OTA升级进行中(防连点重入)
|
||
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)
|
||
{
|
||
ToastUI.Show("100294");
|
||
UpdateLoadingUI.Instance.Show();
|
||
UpdateLoadingUI.Instance.SetPrecent(0);
|
||
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);
|
||
if (pushResponse.IsSuccess && pushResponse.Data != null && pushResponse.Data.data != null && !string.IsNullOrEmpty(pushResponse.Data.data.transferId))
|
||
{
|
||
string transferId = pushResponse.Data.data.transferId;
|
||
Debug.Log($"WiFi OTA push 成功,transferId: {transferId}");
|
||
// SSE 订阅传输进度,传输完成返回 true
|
||
bool success = await SubscribeOtaProgress(transferId, p => UpdateLoadingUI.Instance.SetPrecent(p / 100f));
|
||
if (success)
|
||
{
|
||
ToastUI.Show("100226");
|
||
DataManager.Instance.selectedDevice.firmware_version = otaData.version;
|
||
// 传输完成后等待 10s 再返回主页(设备升级后需要时间重启)
|
||
StartCoroutine(DelayedGoHome(10f));
|
||
}
|
||
else
|
||
{
|
||
ToastUI.Show("100227");
|
||
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ToastUI.Show("100227");
|
||
Debug.LogError($"WiFi OTA push 失败: {pushResponse.ErrorMessage ?? pushResponse.RawResponse}");
|
||
UpdateLoadingUI.Instance.Hide();
|
||
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
||
}
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogError($"WiFi OTA push 异常: {ex.Message}");
|
||
UpdateLoadingUI.Instance.Hide();
|
||
ToastUI.Show("100227");
|
||
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
||
}
|
||
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 传输期间清空返回事件,防止用户触发返回中断升级;传输结束恢复
|
||
var otaBackAction = UIManager.Instance.GetBackAction();
|
||
UIManager.Instance.ClearBackAction();
|
||
StartCoroutine(OTAManager.Instance.PerformFullUpgrade(_selectedFirmwareData, firmwareVersion, (success) =>
|
||
{
|
||
Loom.QueueOnMainThread(() =>
|
||
{
|
||
isUpdating = false;
|
||
// 恢复返回事件
|
||
if (otaBackAction != null)
|
||
UIManager.Instance.RegisterBackAction(otaBackAction);
|
||
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),返回是否传输完成
|
||
/// </summary>
|
||
private Task<bool> SubscribeOtaProgress(string transferId, Action<int> onProgress)
|
||
{
|
||
var tcs = new TaskCompletionSource<bool>();
|
||
StartCoroutine(SSEProgressCoroutine(transferId, onProgress, success => tcs.TrySetResult(success)));
|
||
return tcs.Task;
|
||
}
|
||
|
||
/// <summary>
|
||
/// SSE 进度协程:持续接收进度事件,传输完成(status/percentage)或连接结束后结束
|
||
/// </summary>
|
||
private IEnumerator SSEProgressCoroutine(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();
|
||
if (info.percentage >= 100 || status.Contains("completed") || status.Contains("success") || status.Contains("done"))
|
||
{
|
||
transferFinished = true;
|
||
}
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogError($"OTA 进度解析失败: {ex.Message}");
|
||
}
|
||
},
|
||
err =>
|
||
{
|
||
Debug.LogError($"OTA 进度连接错误: {err}");
|
||
});
|
||
// SSE 连接结束(服务端关闭或出错),以最后收到的进度状态判定是否完成
|
||
onDone?.Invoke(transferFinished);
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
}
|
||
}
|