feat: 新增WiFi OTA升级功能与相关优化
1. 新增多语言文案用于OTA升级提示 2. 优化更新进度显示为整数百分比 3. 调整设备轮询间隔为5秒 4. 新增SSE流式请求处理与OTA数据模型 5. 实现WiFi环境下基于SSE的OTA升级流程 6. 增加蓝牙环境下OTA升级的前置校验与提示
This commit is contained in:
parent
ca042827d6
commit
8568724c3f
@ -1658,6 +1658,11 @@
|
||||
"key": "100332",
|
||||
"zh": "视频加载中,请稍候",
|
||||
"en": "Video loading, please wait"
|
||||
},
|
||||
{
|
||||
"key": "100333",
|
||||
"zh": "设备需WiFi在线方可进行OTA升级",
|
||||
"en": "OTA update requires the device to be online via WiFi"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Kill.Managers
|
||||
@ -365,6 +366,41 @@ namespace Kill.Managers
|
||||
public string version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WiFi OTA push 响应(返回 transferId)
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class OTAPushResponse
|
||||
{
|
||||
public int code;
|
||||
public string message;
|
||||
public OTATransferInfo data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OTA 传输信息(push 返回与 SSE 进度事件共用,后端字段为下划线命名)
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class OTATransferInfo
|
||||
{
|
||||
[JsonProperty("transfer_id")]
|
||||
public string transferId;
|
||||
[JsonProperty("device_id")]
|
||||
public string deviceId;
|
||||
public string filename;
|
||||
[JsonProperty("file_size")]
|
||||
public long fileSize;
|
||||
public int percentage;
|
||||
[JsonProperty("completed_chunks")]
|
||||
public int completedChunks;
|
||||
[JsonProperty("total_chunks")]
|
||||
public int totalChunks;
|
||||
[JsonProperty("chunk_index")]
|
||||
public int chunkIndex;
|
||||
public string direction;
|
||||
public string status;
|
||||
}
|
||||
|
||||
public class MessageData
|
||||
{
|
||||
public string id;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@ -292,6 +293,72 @@ namespace Kill.Network
|
||||
|
||||
#endregion
|
||||
|
||||
#region SSE 流式请求
|
||||
|
||||
/// <summary>
|
||||
/// SSE 流式下载处理器:按事件分隔符 \n\n 解析 data: 行并回调
|
||||
/// </summary>
|
||||
public class SSEDownloadHandler : DownloadHandlerScript
|
||||
{
|
||||
private readonly Action<string> _onEventData;
|
||||
private readonly StringBuilder _buffer = new StringBuilder();
|
||||
|
||||
public SSEDownloadHandler(Action<string> onEventData) : base(new byte[16384])
|
||||
{
|
||||
_onEventData = onEventData;
|
||||
}
|
||||
|
||||
protected override bool ReceiveData(byte[] data, int dataLength)
|
||||
{
|
||||
if (data == null || dataLength == 0) return false;
|
||||
_buffer.Append(Encoding.UTF8.GetString(data, 0, dataLength));
|
||||
string content = _buffer.ToString();
|
||||
int idx;
|
||||
while ((idx = content.IndexOf("\n\n", StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
string evt = content.Substring(0, idx);
|
||||
content = content.Substring(idx + 2);
|
||||
foreach (string line in evt.Split('\n'))
|
||||
{
|
||||
string l = line.Trim();
|
||||
if (l.StartsWith("data:"))
|
||||
{
|
||||
string payload = l.Substring(5).Trim();
|
||||
if (!string.IsNullOrEmpty(payload) && payload != "[DONE]")
|
||||
_onEventData?.Invoke(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 保留未完成的事件块,等待下次数据到达
|
||||
_buffer.Clear();
|
||||
_buffer.Append(content);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发起 SSE 流式请求(长连接)。
|
||||
/// 事件数据通过 onEventData 逐条回调;连接结束(服务端关闭)或出错后协程结束。
|
||||
/// </summary>
|
||||
public IEnumerator StartSSE(string url, Dictionary<string, string> queryParams, Dictionary<string, string> headers, Action<string> onEventData, Action<string> onError)
|
||||
{
|
||||
string fullUrl = GetFullUrl(BuildUrl(url, queryParams));
|
||||
var handler = new SSEDownloadHandler(onEventData);
|
||||
using (UnityWebRequest request = new UnityWebRequest(fullUrl, UnityWebRequest.kHttpVerbGET, handler, null))
|
||||
{
|
||||
AddHeadersToRequest(request, headers);
|
||||
// SSE 为长连接,大文件 OTA 传输耗时长,超时设大防止中途断开
|
||||
request.timeout = 600;
|
||||
yield return request.SendWebRequest();
|
||||
if (request.result != UnityWebRequest.Result.Success && request.result != UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 核心请求方法
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -72,7 +72,7 @@ namespace Kill.UI.Components
|
||||
public void SetPrecent(float p)
|
||||
{
|
||||
float fullP=p*100;
|
||||
precent.text=fullP.ToString("f1")+"%";
|
||||
precent.text=Mathf.RoundToInt(fullP).ToString()+"%";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -7,6 +8,7 @@ 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
|
||||
@ -103,9 +105,20 @@ namespace Kill.UI.Pages
|
||||
isUpdating = true;
|
||||
Screen.sleepTimeout = SleepTimeout.NeverSleep;
|
||||
|
||||
// WiFi 模式:直接 push OTA 指令,设备自行下载固件
|
||||
if (DataManager.Instance.hasWifi&&!DataManager.Instance.hasBluetooth)
|
||||
// 仅蓝牙连接:不支持直接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
|
||||
@ -113,29 +126,45 @@ namespace Kill.UI.Pages
|
||||
ble_mac = DataManager.Instance.selectedDevice.ble_mac,
|
||||
version = otaData.version
|
||||
};
|
||||
var pushResponse = await NetworkCtrl.Instance.Post<NoDataResponse>("/api/v1/ota/push", pushRequest);
|
||||
ResponseCodeHandler.HandleResponse(pushResponse,
|
||||
onSuccess: (data) =>
|
||||
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("100308");
|
||||
ToastUI.Show("100226");
|
||||
DataManager.Instance.selectedDevice.firmware_version = otaData.version;
|
||||
},
|
||||
onError: (code, message) =>
|
||||
// 传输完成后等待 10s 再返回主页(设备升级后需要时间重启)
|
||||
StartCoroutine(DelayedGoHome(10f));
|
||||
}
|
||||
else
|
||||
{
|
||||
ToastUI.Show("100227");
|
||||
Debug.LogError($"WiFi OTA push 失败: {message}");
|
||||
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;
|
||||
}
|
||||
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -186,7 +215,6 @@ namespace Kill.UI.Pages
|
||||
if (success)
|
||||
{
|
||||
DataManager.Instance.selectedDevice.firmware_version = otaData.version;
|
||||
UpdateLoadingUI.Instance.Hide();
|
||||
ToastUI.Show("100226");
|
||||
// OTA 成功后等待 15s 再返回主页(设备升级后需要时间重启)
|
||||
StartCoroutine(DelayedGoHome(15f));
|
||||
@ -215,6 +243,51 @@ namespace Kill.UI.Pages
|
||||
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>
|
||||
|
||||
@ -1714,7 +1714,8 @@ namespace Kill.UI.Pages
|
||||
|
||||
while (!stopPolling && selectedDevice != null)
|
||||
{
|
||||
await System.Threading.Tasks.Task.Delay(10000);
|
||||
// 每 5s 检测一次设备 WiFi 在线状态
|
||||
await System.Threading.Tasks.Task.Delay(5000);
|
||||
|
||||
if (stopPolling || selectedDevice == null)
|
||||
break;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user