2026-08-25 14:12:45 +08:00
|
|
|
|
using System;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
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;
|
2026-08-25 14:12:45 +08:00
|
|
|
|
using Newtonsoft.Json;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
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;
|
2026-08-27 13:11:55 +08:00
|
|
|
|
// WiFi OTA 页内进度条(非全屏,不阻塞操作)
|
|
|
|
|
|
public GameObject progressPanel;
|
|
|
|
|
|
public Image progressBar; // Fill Method = Horizontal, Fill Origin = Left
|
|
|
|
|
|
public Text progressText;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
OTAData otaData;
|
2026-08-27 13:11:55 +08:00
|
|
|
|
// 当前设备持久化的 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升级进行中(防连点重入)
|
2026-06-08 08:55:10 +08:00
|
|
|
|
void Start()
|
|
|
|
|
|
{
|
|
|
|
|
|
UIManager.Instance.RegisterBackAction(Back);
|
|
|
|
|
|
backButton.onClick.RemoveAllListeners();
|
|
|
|
|
|
backButton.onClick.AddListener(Back);
|
|
|
|
|
|
updateButton.onClick.RemoveAllListeners();
|
|
|
|
|
|
updateButton.onClick.AddListener(OnUpdateButtonClick);
|
|
|
|
|
|
updateButton.gameObject.SetActive(false);
|
2026-08-27 13:11:55 +08:00
|
|
|
|
if (progressPanel != null) progressPanel.SetActive(false);
|
2026-06-08 08:55:10 +08:00
|
|
|
|
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;
|
2026-07-10 16:11:09 +08:00
|
|
|
|
int index=1;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
for(int i=nowVersionParts.Length-1;i>=0;i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
nowversionNum += int.Parse(nowVersionParts[i]) * 10*index++;
|
|
|
|
|
|
}
|
2026-07-10 16:11:09 +08:00
|
|
|
|
index = 1;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-08-27 13:11:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 进入页面:若本地有未结束的 transferId,则恢复订阅进度(非全屏,不阻塞)
|
|
|
|
|
|
TryResumeWifiOta();
|
2026-06-08 08:55:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
public void Back()
|
|
|
|
|
|
{
|
|
|
|
|
|
UIManager.Instance.RegisterBackAction(GetComponentInParent<DeviceInfoPage>().Back);
|
|
|
|
|
|
Destroy(gameObject);
|
2026-06-22 14:57:16 +08:00
|
|
|
|
Screen.sleepTimeout = SleepTimeout.SystemSetting;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
}
|
2026-08-27 14:18:31 +08:00
|
|
|
|
public void OnUpdateButtonClick()
|
2026-06-08 08:55:10 +08:00
|
|
|
|
{
|
2026-08-14 08:35:38 +08:00
|
|
|
|
// 防连点:升级流程进行中忽略重复触发,避免并发启动两个OTA
|
|
|
|
|
|
if (isUpdating) return;
|
|
|
|
|
|
isUpdating = true;
|
2026-08-27 14:18:31 +08:00
|
|
|
|
// 立刻隐藏更新按钮,防止连点期间按钮仍可点
|
|
|
|
|
|
updateButton.gameObject.SetActive(false);
|
2026-06-22 14:57:16 +08:00
|
|
|
|
Screen.sleepTimeout = SleepTimeout.NeverSleep;
|
2026-08-27 14:18:31 +08:00
|
|
|
|
OnUpdateButtonClickAsync();
|
|
|
|
|
|
}
|
2026-07-14 17:05:14 +08:00
|
|
|
|
|
2026-08-27 14:18:31 +08:00
|
|
|
|
private async void OnUpdateButtonClickAsync()
|
|
|
|
|
|
{
|
2026-08-25 14:12:45 +08:00
|
|
|
|
// 仅蓝牙连接:不支持直接OTA,提示先配置WiFi
|
|
|
|
|
|
if (DataManager.Instance.hasBluetooth && !DataManager.Instance.hasWifi)
|
2026-07-14 17:05:14 +08:00
|
|
|
|
{
|
2026-08-25 14:12:45 +08:00
|
|
|
|
ToastUI.Show("100333");
|
|
|
|
|
|
isUpdating = false;
|
2026-08-27 14:18:31 +08:00
|
|
|
|
updateButton.gameObject.SetActive(true);
|
2026-08-25 14:12:45 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 13:11:55 +08:00
|
|
|
|
// WiFi 模式:push OTA 指令,SSE 订阅传输进度(非全屏,不阻塞操作,可关闭页面后下次进入继续订阅)
|
2026-08-25 14:12:45 +08:00
|
|
|
|
if (DataManager.Instance.hasWifi)
|
|
|
|
|
|
{
|
2026-08-14 08:35:38 +08:00
|
|
|
|
try
|
2026-07-14 17:05:14 +08:00
|
|
|
|
{
|
2026-08-14 08:35:38 +08:00
|
|
|
|
var pushRequest = new OTAPushRequest
|
2026-07-14 17:05:14 +08:00
|
|
|
|
{
|
2026-08-14 08:35:38 +08:00
|
|
|
|
ble_mac = DataManager.Instance.selectedDevice.ble_mac,
|
|
|
|
|
|
version = otaData.version
|
|
|
|
|
|
};
|
2026-08-25 14:12:45 +08:00
|
|
|
|
var pushResponse = await NetworkCtrl.Instance.Post<OTAPushResponse>("/api/v1/ota/push", pushRequest);
|
2026-08-27 13:11:55 +08:00
|
|
|
|
// 提取 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))
|
2026-08-25 14:12:45 +08:00
|
|
|
|
{
|
2026-08-27 13:11:55 +08:00
|
|
|
|
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)
|
2026-08-14 08:35:38 +08:00
|
|
|
|
{
|
2026-08-27 13:11:55 +08:00
|
|
|
|
transferId = match.Groups[1].Value;
|
|
|
|
|
|
Debug.Log($"WiFi OTA push 返回 code=608(已有OTA进行中),从message提取 transferId: {transferId}");
|
2026-08-14 08:35:38 +08:00
|
|
|
|
}
|
2026-08-25 14:12:45 +08:00
|
|
|
|
}
|
2026-08-27 13:11:55 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(transferId))
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.Log($"WiFi OTA push 成功,transferId: {transferId}");
|
|
|
|
|
|
// 持久化 transferId,订阅后台进行,不阻塞页面操作
|
|
|
|
|
|
SaveTransferId(transferId, otaData.version, "pushing");
|
|
|
|
|
|
StartWifiOtaSubscription(transferId);
|
|
|
|
|
|
}
|
2026-08-25 14:12:45 +08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
ToastUI.Show("100227");
|
|
|
|
|
|
Debug.LogError($"WiFi OTA push 失败: {pushResponse.ErrorMessage ?? pushResponse.RawResponse}");
|
|
|
|
|
|
}
|
2026-08-14 08:35:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (System.Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"WiFi OTA push 异常: {ex.Message}");
|
2026-08-25 14:12:45 +08:00
|
|
|
|
ToastUI.Show("100227");
|
2026-08-14 08:35:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
isUpdating = false;
|
|
|
|
|
|
}
|
2026-07-14 17:05:14 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BLE 模式:下载固件 → 校验 → 蓝牙传输
|
2026-06-22 14:57:16 +08:00
|
|
|
|
ToastUI.Show("100294");
|
|
|
|
|
|
UpdateLoadingUI.Instance.Show();
|
2026-06-08 08:55:10 +08:00
|
|
|
|
string savePath = Application.persistentDataPath + "/ota.bin";
|
2026-07-14 17:05:14 +08:00
|
|
|
|
var downloadResult = await NetworkCtrl.Instance.DownloadFile(otaData.download_url, savePath, UpdateLoadingUI.Instance.GetDownloadProcess);
|
2026-06-22 14:57:16 +08:00
|
|
|
|
UpdateLoadingUI.Instance.Hide();
|
2026-07-14 17:05:14 +08:00
|
|
|
|
if (!downloadResult.IsSuccess)
|
2026-06-08 08:55:10 +08:00
|
|
|
|
{
|
|
|
|
|
|
ToastUI.Show("100224");
|
2026-08-14 08:35:38 +08:00
|
|
|
|
isUpdating = false;
|
2026-08-27 14:18:31 +08:00
|
|
|
|
updateButton.gameObject.SetActive(true);
|
2026-06-08 08:55:10 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-14 17:05:14 +08:00
|
|
|
|
string md5 = NetworkCtrl.Instance.CalculateMD5(savePath);
|
|
|
|
|
|
if (md5 != otaData.md5)
|
2026-06-08 08:55:10 +08:00
|
|
|
|
{
|
|
|
|
|
|
ToastUI.Show("100223");
|
|
|
|
|
|
Debug.LogError("下载的文件MD5校验失败");
|
|
|
|
|
|
File.Delete(savePath);
|
2026-08-14 08:35:38 +08:00
|
|
|
|
isUpdating = false;
|
2026-08-27 14:18:31 +08:00
|
|
|
|
updateButton.gameObject.SetActive(true);
|
2026-06-08 08:55:10 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
LoadFirmwareFile(savePath);
|
2026-08-14 08:35:38 +08:00
|
|
|
|
if (_selectedFirmwareData == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
isUpdating = false;
|
2026-08-27 14:18:31 +08:00
|
|
|
|
updateButton.gameObject.SetActive(true);
|
2026-08-14 08:35:38 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-08 08:55:10 +08:00
|
|
|
|
ToastUI.Show("100225");
|
2026-07-14 17:05:14 +08:00
|
|
|
|
|
2026-06-22 14:57:16 +08:00
|
|
|
|
UpdateLoadingUI.Instance.SetPrecent(0);
|
|
|
|
|
|
UpdateLoadingUI.Instance.Show();
|
|
|
|
|
|
uint firmwareVersion = ParseFirmwareVersion(otaData.version);
|
2026-07-14 17:05:14 +08:00
|
|
|
|
OTAManager.Instance.OnTransferProgress += OnOTATransferProgress;
|
2026-08-27 13:11:55 +08:00
|
|
|
|
// OTA 传输期间(进度显示)清空返回事件,返回键走两次返回退出App,防止误返回中断升级(成败都会返回主页)
|
2026-08-14 11:24:48 +08:00
|
|
|
|
UIManager.Instance.ClearBackAction();
|
2026-06-08 08:55:10 +08:00
|
|
|
|
StartCoroutine(OTAManager.Instance.PerformFullUpgrade(_selectedFirmwareData, firmwareVersion, (success) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Loom.QueueOnMainThread(() =>
|
|
|
|
|
|
{
|
2026-08-14 08:35:38 +08:00
|
|
|
|
isUpdating = false;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
if (success)
|
|
|
|
|
|
{
|
2026-07-14 17:05:14 +08:00
|
|
|
|
DataManager.Instance.selectedDevice.firmware_version = otaData.version;
|
2026-08-24 11:09:22 +08:00
|
|
|
|
ToastUI.Show("100226");
|
|
|
|
|
|
// OTA 成功后等待 15s 再返回主页(设备升级后需要时间重启)
|
|
|
|
|
|
StartCoroutine(DelayedGoHome(15f));
|
2026-06-08 08:55:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
ToastUI.Show("100227");
|
2026-08-24 11:09:22 +08:00
|
|
|
|
UpdateLoadingUI.Instance.Hide();
|
|
|
|
|
|
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
2026-06-08 08:55:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选中的固件文件数据
|
|
|
|
|
|
private byte[] _selectedFirmwareData;
|
|
|
|
|
|
private string _selectedFirmwarePath;
|
|
|
|
|
|
|
2026-08-24 11:09:22 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 延迟指定时间后返回主页
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerator DelayedGoHome(float delay)
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return new WaitForSeconds(delay);
|
|
|
|
|
|
UpdateLoadingUI.Instance.Hide();
|
|
|
|
|
|
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-25 14:12:45 +08:00
|
|
|
|
/// <summary>
|
2026-08-27 13:11:55 +08:00
|
|
|
|
/// 订阅 WiFi OTA 传输进度(SSE),返回是否传输完成(仅供兼容旧调用,BLE 仍走全屏 OTAManager)
|
2026-08-25 14:12:45 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Task<bool> SubscribeOtaProgress(string transferId, Action<int> onProgress)
|
|
|
|
|
|
{
|
|
|
|
|
|
var tcs = new TaskCompletionSource<bool>();
|
2026-08-27 13:11:55 +08:00
|
|
|
|
StartCoroutine(SSELegacyCoroutine(transferId, onProgress, success => tcs.TrySetResult(success)));
|
2026-08-25 14:12:45 +08:00
|
|
|
|
return tcs.Task;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 13:11:55 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-25 14:12:45 +08:00
|
|
|
|
/// <summary>
|
2026-08-27 13:11:55 +08:00
|
|
|
|
/// 读取当前设备的 OTA transferId(空表示无)
|
2026-08-25 14:12:45 +08:00
|
|
|
|
/// </summary>
|
2026-08-27 13:11:55 +08:00
|
|
|
|
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>
|
2026-08-27 14:18:31 +08:00
|
|
|
|
/// 传输完成后的过渡:等3s → 提示 100226 → 返回主页。
|
|
|
|
|
|
/// 设备重启/版本号刷新由后台下次进入页面或下次 OTA 拉取时再处理。
|
2026-08-27 13:11:55 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerator WaitDeviceRebootAndFinish(string transferId)
|
|
|
|
|
|
{
|
|
|
|
|
|
ShowProgressPanelWaitingForDevice();
|
2026-08-27 14:18:31 +08:00
|
|
|
|
// 等3s 提示传输完成(给用户缓冲),然后返回主页
|
|
|
|
|
|
yield return new WaitForSeconds(3f);
|
2026-08-27 13:11:55 +08:00
|
|
|
|
if (gameObject == null) yield break;
|
2026-08-27 14:18:31 +08:00
|
|
|
|
// 仅当持久化的还是同一个 transferId 时再继续(用户可能已点过别的)
|
2026-08-27 13:11:55 +08:00
|
|
|
|
if (LoadTransferId() != transferId) yield break;
|
2026-08-27 14:18:31 +08:00
|
|
|
|
ToastUI.Show("100226");
|
|
|
|
|
|
// 返回主页:transferId 保留,下次进入页面仍能识别为「传输已完成、待重启确认」
|
|
|
|
|
|
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
2026-08-27 13:11:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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);
|
2026-08-27 14:18:31 +08:00
|
|
|
|
// 已经传输完成等待设备重启:直接清理状态、恢复更新按钮(实际设备升级由下次进入主页/下次 OTA 拉取时确认)
|
2026-08-27 13:11:55 +08:00
|
|
|
|
if (savedStatus == "waiting_reboot")
|
|
|
|
|
|
{
|
2026-08-27 14:18:31 +08:00
|
|
|
|
ClearTransferId();
|
|
|
|
|
|
HideProgressPanel();
|
|
|
|
|
|
updateButton.gameObject.SetActive(true);
|
2026-08-27 13:11:55 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
ShowProgressPanel(0);
|
|
|
|
|
|
StartCoroutine(SubscribeAndHandle(savedId));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 仅订阅进度协程,传出是否完成(不弹出后续处理)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerator SubscribeOtaProgressCoroutine(string transferId, Action<int> onProgress, Action<bool> onDone)
|
2026-08-25 14:12:45 +08:00
|
|
|
|
{
|
|
|
|
|
|
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();
|
2026-08-27 13:11:55 +08:00
|
|
|
|
Debug.Log($"OTA 进度: {status}, {info.percentage}%");
|
|
|
|
|
|
if (info.percentage >= 100 || status.Contains("completed"))
|
2026-08-25 14:12:45 +08:00
|
|
|
|
{
|
|
|
|
|
|
transferFinished = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (System.Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"OTA 进度解析失败: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
err =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"OTA 进度连接错误: {err}");
|
|
|
|
|
|
});
|
|
|
|
|
|
onDone?.Invoke(transferFinished);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 13:11:55 +08:00
|
|
|
|
/// <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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 08:55:10 +08:00
|
|
|
|
/// <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 &&
|
2026-08-19 09:47:44 +08:00
|
|
|
|
uint.TryParse(parts[0], out uint v0) &&
|
|
|
|
|
|
uint.TryParse(parts[1], out uint v1) &&
|
|
|
|
|
|
uint.TryParse(parts[2], out uint v2))
|
2026-06-08 08:55:10 +08:00
|
|
|
|
{
|
2026-08-19 09:47:44 +08:00
|
|
|
|
// 版本号格式:主版本.次版本.修订号(可含第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;
|
2026-06-08 08:55:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
|
|
|
|
|
|
// 默认返回版本号 1
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-06-22 14:57:16 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// OTA传输进度回调
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void OnOTATransferProgress(int current, int total, int retryCount, int maxRetries)
|
|
|
|
|
|
{
|
|
|
|
|
|
UpdateLoadingUI.Instance.SetPrecent((float)current/ total);
|
|
|
|
|
|
}
|
2026-06-08 08:55:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|