From 8568724c3fe3d67f5eb330a861b04c0949675033 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9C=E8=99=9E=E6=B8=A0=E6=88=90=E2=80=9D?=
<“yuqucheng2006@qq.com”>
Date: Tue, 25 Aug 2026 14:12:45 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9EWiFi=20OTA=E5=8D=87?=
=?UTF-8?q?=E7=BA=A7=E5=8A=9F=E8=83=BD=E4=B8=8E=E7=9B=B8=E5=85=B3=E4=BC=98?=
=?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 新增多语言文案用于OTA升级提示
2. 优化更新进度显示为整数百分比
3. 调整设备轮询间隔为5秒
4. 新增SSE流式请求处理与OTA数据模型
5. 实现WiFi环境下基于SSE的OTA升级流程
6. 增加蓝牙环境下OTA升级的前置校验与提示
---
Assets/Res/language/language.json | 5 +
Assets/Scripts/Managers/DataBase.cs | 36 +++++++
Assets/Scripts/Network/HttpRequestManager.cs | 67 +++++++++++++
.../Scripts/UI/Components/UpdateLoadingUI.cs | 2 +-
.../DeviceInfoPage/FirmwareUpdatePage.cs | 97 ++++++++++++++++---
.../Scripts/UI/Pages/HomePage/HomePageCtrl.cs | 3 +-
6 files changed, 196 insertions(+), 14 deletions(-)
diff --git a/Assets/Res/language/language.json b/Assets/Res/language/language.json
index 67b724f..b899c54 100644
--- a/Assets/Res/language/language.json
+++ b/Assets/Res/language/language.json
@@ -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"
}
diff --git a/Assets/Scripts/Managers/DataBase.cs b/Assets/Scripts/Managers/DataBase.cs
index 2e80be4..c43e6b8 100644
--- a/Assets/Scripts/Managers/DataBase.cs
+++ b/Assets/Scripts/Managers/DataBase.cs
@@ -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;
}
+ ///
+ /// WiFi OTA push 响应(返回 transferId)
+ ///
+ [System.Serializable]
+ public class OTAPushResponse
+ {
+ public int code;
+ public string message;
+ public OTATransferInfo data;
+ }
+
+ ///
+ /// OTA 传输信息(push 返回与 SSE 进度事件共用,后端字段为下划线命名)
+ ///
+ [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;
diff --git a/Assets/Scripts/Network/HttpRequestManager.cs b/Assets/Scripts/Network/HttpRequestManager.cs
index cf4ef49..02b244d 100644
--- a/Assets/Scripts/Network/HttpRequestManager.cs
+++ b/Assets/Scripts/Network/HttpRequestManager.cs
@@ -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 流式请求
+
+ ///
+ /// SSE 流式下载处理器:按事件分隔符 \n\n 解析 data: 行并回调
+ ///
+ public class SSEDownloadHandler : DownloadHandlerScript
+ {
+ private readonly Action _onEventData;
+ private readonly StringBuilder _buffer = new StringBuilder();
+
+ public SSEDownloadHandler(Action 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;
+ }
+ }
+
+ ///
+ /// 发起 SSE 流式请求(长连接)。
+ /// 事件数据通过 onEventData 逐条回调;连接结束(服务端关闭)或出错后协程结束。
+ ///
+ public IEnumerator StartSSE(string url, Dictionary queryParams, Dictionary headers, Action onEventData, Action 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 核心请求方法
///
diff --git a/Assets/Scripts/UI/Components/UpdateLoadingUI.cs b/Assets/Scripts/UI/Components/UpdateLoadingUI.cs
index 1e9d444..252c65d 100644
--- a/Assets/Scripts/UI/Components/UpdateLoadingUI.cs
+++ b/Assets/Scripts/UI/Components/UpdateLoadingUI.cs
@@ -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()+"%";
}
}
}
diff --git a/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs b/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs
index ba405cf..54ff148 100644
--- a/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs
+++ b/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs
@@ -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("/api/v1/ota/push", pushRequest);
- ResponseCodeHandler.HandleResponse(pushResponse,
- onSuccess: (data) =>
+ var pushResponse = await NetworkCtrl.Instance.Post("/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);
}
+ ///
+ /// 订阅 WiFi OTA 传输进度(SSE),返回是否传输完成
+ ///
+ private Task SubscribeOtaProgress(string transferId, Action onProgress)
+ {
+ var tcs = new TaskCompletionSource();
+ StartCoroutine(SSEProgressCoroutine(transferId, onProgress, success => tcs.TrySetResult(success)));
+ return tcs.Task;
+ }
+
+ ///
+ /// SSE 进度协程:持续接收进度事件,传输完成(status/percentage)或连接结束后结束
+ ///
+ private IEnumerator SSEProgressCoroutine(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();
+ 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);
+ }
+
///
/// 加载固件文件
///
diff --git a/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs b/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs
index 67b1ed4..93aac5b 100644
--- a/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs
+++ b/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs
@@ -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;