From a95b257b0d0801e863ba672ccb5286f61f518f89 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: Thu, 27 Aug 2026 13:11:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9EOTA=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=E4=B8=8E=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=BC=93=E5=AD=98=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增设备信息页OTA升级进度UI与本地持久化传输状态 2. 实现WiFi模式OTA非全屏进度订阅与重启等待逻辑 3. 新增设备列表本地缓存与加载、清空功能 4. 优化蓝牙管理:移除自动弹窗,改为提示用户手动开启蓝牙 5. 新增网络状态监听与设备在线状态刷新逻辑 6. 新增多语言文案与蓝牙未开启提示资源 7. 优化网络请求处理,新增静默错误提示参数 --- Assets/Res/language/language.json | 15 + Assets/Res/ui/deviceinfopage/固件更新.prefab | 306 +++++++++++++ Assets/Res/ui/deviceinfopage/更新进度.png | Bin 0 -> 53170 bytes .../Res/ui/deviceinfopage/更新进度.png.meta | 127 ++++++ Assets/Scripts/Bluetooth/BluetoothManager.cs | 40 +- Assets/Scripts/Managers/DataManager.cs | 79 +++- Assets/Scripts/Network/ResponseCode.cs | 18 +- .../DeviceInfoPage/FirmwareUpdatePage.cs | 401 ++++++++++++++++-- .../Scripts/UI/Pages/HomePage/HomePageCtrl.cs | 180 +++++++- 9 files changed, 1090 insertions(+), 76 deletions(-) create mode 100644 Assets/Res/ui/deviceinfopage/更新进度.png create mode 100644 Assets/Res/ui/deviceinfopage/更新进度.png.meta diff --git a/Assets/Res/language/language.json b/Assets/Res/language/language.json index b899c54..8cce917 100644 --- a/Assets/Res/language/language.json +++ b/Assets/Res/language/language.json @@ -1134,6 +1134,16 @@ "zh": "固件传输失败,请重试", "en": "Firmware transfer failed. Please try again." }, + { + "key": "ota_update_failed", + "zh": "固件更新失败,请重试", + "en": "Firmware update failed. Please try again." + }, + { + "key": "ota_waiting_reboot", + "zh": "设备更新中...", + "en": "Device updating..." + }, { "key": "100228", "zh": "温度错误:", @@ -1663,6 +1673,11 @@ "key": "100333", "zh": "设备需WiFi在线方可进行OTA升级", "en": "OTA update requires the device to be online via WiFi" + }, + { + "key": "100334", + "zh": "蓝牙未开启,如需使用请打开", + "en": "Bluetooth is off. Turn it on if needed." } diff --git a/Assets/Res/ui/deviceinfopage/固件更新.prefab b/Assets/Res/ui/deviceinfopage/固件更新.prefab index 9acfda1..7b92f0e 100644 --- a/Assets/Res/ui/deviceinfopage/固件更新.prefab +++ b/Assets/Res/ui/deviceinfopage/固件更新.prefab @@ -1,5 +1,139 @@ %YAML 1.1 %TAG !u! tag:unity3d.com,2011: +--- !u!1 &241336363346333283 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6901814223954429598} + - component: {fileID: 7794302058416943133} + - component: {fileID: 469724078145029111} + - component: {fileID: 5745045663283300389} + m_Layer: 5 + m_Name: "\u66F4\u65B0\u8FDB\u5EA6" + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &6901814223954429598 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 241336363346333283} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.0003841, y: 1.0003841, z: 1.0003841} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1682688447280925076} + - {fileID: 1221440869875413360} + m_Father: {fileID: 6542542256051934179} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 120} + m_SizeDelta: {x: -60, y: 90} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &7794302058416943133 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 241336363346333283} + m_CullTransparentMesh: 1 +--- !u!114 &469724078145029111 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 241336363346333283} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.09019608, g: 0.09019608, b: 0.09019608, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: e70cab5c4dbadd748a0e5e007f58bf9d, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 2 +--- !u!114 &5745045663283300389 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 241336363346333283} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 0 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 469724078145029111} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 0} + m_TargetAssemblyTypeName: Kill.UI.Pages.DeviceInfoPage, Assembly-CSharp + m_MethodName: RestoreSettings0 + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 --- !u!1 &312154182751522083 GameObject: m_ObjectHideFlags: 0 @@ -34,6 +168,7 @@ RectTransform: - {fileID: 2568352390314877935} - {fileID: 3959655657766269314} - {fileID: 5168603418929363938} + - {fileID: 6901814223954429598} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -97,6 +232,9 @@ MonoBehaviour: changeLogText: {fileID: 1957090092938833670} backButton: {fileID: 5686861562038373004} updateButton: {fileID: 2226008222436017730} + progressPanel: {fileID: 241336363346333283} + progressBar: {fileID: 4875656352718239638} + progressText: {fileID: 1165127137834906993} --- !u!1 &414212200923698105 GameObject: m_ObjectHideFlags: 0 @@ -852,6 +990,174 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 1 +--- !u!1 &3751683427085812987 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1221440869875413360} + - component: {fileID: 6517004505051842296} + - component: {fileID: 1165127137834906993} + - component: {fileID: 2493490058903432914} + m_Layer: 5 + m_Name: "\u6570\u503C" + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1221440869875413360 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3751683427085812987} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 6901814223954429598} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6517004505051842296 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3751683427085812987} + m_CullTransparentMesh: 1 +--- !u!114 &1165127137834906993 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3751683427085812987} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: f4c31d68e7cd3454685bb4c132a52f58, type: 3} + m_FontSize: 30 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 0% +--- !u!114 &2493490058903432914 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3751683427085812987} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 89f1d49b50a31514787b4e268b1c73bb, type: 3} + m_Name: + m_EditorClassIdentifier: + textKey: 100221 +--- !u!1 &4223866644516014858 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1682688447280925076} + - component: {fileID: 8395495456873232610} + - component: {fileID: 4875656352718239638} + m_Layer: 5 + m_Name: "\u6761" + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1682688447280925076 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4223866644516014858} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 6901814223954429598} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8395495456873232610 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4223866644516014858} + m_CullTransparentMesh: 1 +--- !u!114 &4875656352718239638 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4223866644516014858} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: bc956526bf19a8e4da8da24025fe8240, type: 3} + m_Type: 3 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 0 + m_FillAmount: 0 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 --- !u!1 &4561661931995402753 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Res/ui/deviceinfopage/更新进度.png b/Assets/Res/ui/deviceinfopage/更新进度.png new file mode 100644 index 0000000000000000000000000000000000000000..751a771e82ace21d4088fd2f6105d3ea9ad470bf GIT binary patch literal 53170 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 literal 0 HcmV?d00001 diff --git a/Assets/Res/ui/deviceinfopage/更新进度.png.meta b/Assets/Res/ui/deviceinfopage/更新进度.png.meta new file mode 100644 index 0000000..c072629 --- /dev/null +++ b/Assets/Res/ui/deviceinfopage/更新进度.png.meta @@ -0,0 +1,127 @@ +fileFormatVersion: 2 +guid: bc956526bf19a8e4da8da24025fe8240 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Bluetooth/BluetoothManager.cs b/Assets/Scripts/Bluetooth/BluetoothManager.cs index 395ced0..14f973c 100644 --- a/Assets/Scripts/Bluetooth/BluetoothManager.cs +++ b/Assets/Scripts/Bluetooth/BluetoothManager.cs @@ -243,16 +243,15 @@ namespace Kill.Bluetooth if (!isEnabled) { - Log("蓝牙未开启,使用 Intent 请求用户启用..."); - - using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) - using (AndroidJavaObject currentActivity = unityPlayer.GetStatic("currentActivity")) - { - AndroidJavaObject intent = new AndroidJavaObject("android.content.Intent", "android.bluetooth.adapter.action.REQUEST_ENABLE"); - currentActivity.Call("startActivityForResult", intent, 1); - } - - _enableBtCoroutine = StartCoroutine(WaitForBluetoothEnabled(onBluetoothReady)); + // 不再弹出系统蓝牙开启弹窗(弹窗会打断 App 生命周期,导致返回后页面卡住不刷新), + // 仅提示用户手动去系统设置中开启蓝牙 + Log("蓝牙未开启,仅提示用户手动开启蓝牙"); + ToastUI.Show("100334"); + _enableBtCoroutine = null; + _pendingStartScan = false; + _pendingConnectAddress = null; + OnError?.Invoke("蓝牙未开启"); + OnInitialized?.Invoke(false); } else { @@ -350,20 +349,23 @@ namespace Kill.Bluetooth Log("iOS 蓝牙已开启"); onBluetoothReady?.Invoke(); } - // 4 = PoweredOff (蓝牙关闭) — iOS 系统会自动弹出蓝牙开启对话框,无需手动跳转设置 + // 4 = PoweredOff (蓝牙关闭) — 仅提示用户手动开启,不等待系统弹窗 else if (state == 4) { - Log("iOS 蓝牙未开启,等待系统弹出蓝牙开启提示..."); - // 不调用 ShowIOSTurnOnBluetoothPrompt(),让 iOS 原生弹窗处理 - // 等待蓝牙状态变化(检测焦点变化 + 轮询蓝牙状态) - _enableBtCoroutine = StartCoroutine(WaitForIOSBluetoothEnabled(onBluetoothReady)); + Log("iOS 蓝牙未开启,仅提示用户手动开启蓝牙"); + ToastUI.Show("100334"); + _enableBtCoroutine = null; + OnError?.Invoke("蓝牙未开启"); + OnInitialized?.Invoke(false); } - // 3 = Unauthorized (未授权) — 需要跳转到应用设置页面让用户开启蓝牙权限 + // 3 = Unauthorized (未授权) — 仅提示用户去系统设置中开启蓝牙权限,不自动跳转 else if (state == 3) { - Log("iOS 蓝牙未授权,跳转到应用设置页..."); - ShowIOSTurnOnBluetoothPrompt(); - _enableBtCoroutine = StartCoroutine(WaitForIOSBluetoothEnabled(onBluetoothReady)); + Log("iOS 蓝牙未授权,仅提示用户去系统设置中开启蓝牙权限"); + ToastUI.Show("100334"); + _enableBtCoroutine = null; + OnError?.Invoke("蓝牙未授权"); + OnInitialized?.Invoke(false); } // 2 = Unsupported (不支持蓝牙) else if (state == 2) diff --git a/Assets/Scripts/Managers/DataManager.cs b/Assets/Scripts/Managers/DataManager.cs index 4aa1a35..1fca575 100644 --- a/Assets/Scripts/Managers/DataManager.cs +++ b/Assets/Scripts/Managers/DataManager.cs @@ -53,6 +53,79 @@ namespace Kill.Managers { selectedDeviceMac=PlayerPrefs.GetString("selectedDeviceMac", ""); } + + #region 设备列表本地缓存(断网时使用) + private const string OWNED_DEVICES_KEY = "ownedDevicesCache"; + private const string SHARED_DEVICES_KEY = "sharedDevicesCache"; + + ///

+ /// 将当前 OwnedDevices/SharedDevices 持久化到本地(按当前登录用户隔离) + /// + public void SaveDeviceLists() + { + try + { + string userId = userInfo?.id ?? ""; + string ownedJson = JsonUtility.ToJson(OwnedDevices != null ? new DeviceInfoListWrapper(OwnedDevices) : new DeviceInfoListWrapper()); + string sharedJson = JsonUtility.ToJson(SharedDevices != null ? new DeviceInfoListWrapper(SharedDevices) : new DeviceInfoListWrapper()); + PlayerPrefs.SetString(OWNED_DEVICES_KEY + "_" + userId, ownedJson); + PlayerPrefs.SetString(SHARED_DEVICES_KEY + "_" + userId, sharedJson); + PlayerPrefs.Save(); + } + catch (Exception ex) + { + Debug.LogError($"[DataManager] 保存设备列表缓存失败: {ex.Message}"); + } + } + + /// + /// 从本地恢复 OwnedDevices/SharedDevices(返回是否成功恢复) + /// + public bool LoadDeviceLists() + { + try + { + string userId = userInfo?.id ?? ""; + string ownedJson = PlayerPrefs.GetString(OWNED_DEVICES_KEY + "_" + userId, ""); + string sharedJson = PlayerPrefs.GetString(SHARED_DEVICES_KEY + "_" + userId, ""); + OwnedDevices = string.IsNullOrEmpty(ownedJson) ? new List() : JsonUtility.FromJson(ownedJson)?.ToList() ?? new List(); + SharedDevices = string.IsNullOrEmpty(sharedJson) ? new List() : JsonUtility.FromJson(sharedJson)?.ToList() ?? new List(); + return OwnedDevices.Count > 0 || SharedDevices.Count > 0; + } + catch (Exception ex) + { + Debug.LogError($"[DataManager] 读取设备列表缓存失败: {ex.Message}"); + OwnedDevices = new List(); + SharedDevices = new List(); + return false; + } + } + + /// + /// 清空本地缓存的设备列表(登出时调用) + /// + public void ClearDeviceLists() + { + string userId = userInfo?.id ?? ""; + PlayerPrefs.DeleteKey(OWNED_DEVICES_KEY + "_" + userId); + PlayerPrefs.DeleteKey(SHARED_DEVICES_KEY + "_" + userId); + PlayerPrefs.Save(); + OwnedDevices = new List(); + SharedDevices = new List(); + } + + [Serializable] + private class DeviceInfoListWrapper + { + public List items = new List(); + public DeviceInfoListWrapper() { } + public DeviceInfoListWrapper(List source) + { + if (source != null) items.AddRange(source); + } + public List ToList() => new List(items); + } + #endregion public void InitUser() { token = PlayerPrefs.GetString("token", ""); @@ -124,7 +197,8 @@ namespace Kill.Managers onError: (code, msg) => { Debug.LogError($"[DataManager] 获取设备配置失败: {msg}"); - } + }, + silentErrorTip: true ); } @@ -164,7 +238,8 @@ namespace Kill.Managers onError: (code, msg) => { Debug.LogError($"[DataManager] 修改设备配置失败: {msg}"); - } + }, + silentErrorTip: true ); return success; diff --git a/Assets/Scripts/Network/ResponseCode.cs b/Assets/Scripts/Network/ResponseCode.cs index 2d720c7..1cabffb 100644 --- a/Assets/Scripts/Network/ResponseCode.cs +++ b/Assets/Scripts/Network/ResponseCode.cs @@ -93,21 +93,23 @@ namespace Kill.Network /// 输出状态码 /// 输出消息 /// 是否已全局处理 - public static bool CheckGlobalHandle(HttpResponse response, out int code, out string message) + public static bool CheckGlobalHandle(HttpResponse response, out int code, out string message, bool silentErrorTip = false) { code = -1; message = null; // 空响应(网络异常) if (response == null) { - ShowErrorTip("100036"); + message = "网络异常"; + if (!silentErrorTip) ShowErrorTip("100036"); return true; } // HTTP错误(网络层错误) if (!response.IsSuccess) { - ShowErrorTip("100036"); + message = string.IsNullOrEmpty(response.ErrorMessage) ? "网络异常" : response.ErrorMessage; + if (!silentErrorTip) ShowErrorTip("100036"); return true; } @@ -132,13 +134,19 @@ namespace Kill.Network /// HTTP响应 /// 成功回调(code=200) /// 其他错误回调(可选,不传则自动提示) - public static void HandleResponse(HttpResponse response, Action onSuccess, Action onError = null) + /// true 时静默处理网络错误(不弹100036),仅触发 onError + public static void HandleResponse(HttpResponse response, Action onSuccess, Action onError = null, bool silentErrorTip = false) { // 全局处理(网络错误、601等) int code; string message; - if (CheckGlobalHandle(response, out code, out message)) + if (CheckGlobalHandle(response, out code, out message, silentErrorTip)) { + // silentErrorTip=true 时若调用方提供了 onError,仍触发以便业务继续 + if (silentErrorTip && onError != null) + { + onError.Invoke(code, message ?? "网络异常"); + } return; // 已被全局处理 } diff --git a/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs b/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs index 54ff148..05b33ca 100644 --- a/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs +++ b/Assets/Scripts/UI/Pages/DeviceInfoPage/FirmwareUpdatePage.cs @@ -21,7 +21,22 @@ namespace Kill.UI.Pages 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 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升级进行中(防连点重入) void Start() { UIManager.Instance.RegisterBackAction(Back); @@ -30,6 +45,7 @@ namespace Kill.UI.Pages updateButton.onClick.RemoveAllListeners(); updateButton.onClick.AddListener(OnUpdateButtonClick); updateButton.gameObject.SetActive(false); + if (progressPanel != null) progressPanel.SetActive(false); GetOtaInfo(); } @@ -90,6 +106,9 @@ namespace Kill.UI.Pages versionText = latestTip.transform.Find("value").GetComponent(); versionText.text = otaData.version; } + + // 进入页面:若本地有未结束的 transferId,则恢复订阅进度(非全屏,不阻塞) + TryResumeWifiOta(); } public void Back() { @@ -97,7 +116,6 @@ namespace Kill.UI.Pages Destroy(gameObject); Screen.sleepTimeout = SleepTimeout.SystemSetting; } - private bool isUpdating; // OTA升级进行中(防连点重入) public async void OnUpdateButtonClick() { // 防连点:升级流程进行中忽略重复触发,避免并发启动两个OTA @@ -113,12 +131,9 @@ namespace Kill.UI.Pages return; } - // WiFi 模式:push OTA 指令,SSE 订阅传输进度 + // WiFi 模式:push OTA 指令,SSE 订阅传输进度(非全屏,不阻塞操作,可关闭页面后下次进入继续订阅) if (DataManager.Instance.hasWifi) { - ToastUI.Show("100294"); - UpdateLoadingUI.Instance.Show(); - UpdateLoadingUI.Instance.SetPrecent(0); try { var pushRequest = new OTAPushRequest @@ -127,39 +142,38 @@ namespace Kill.UI.Pages version = otaData.version }; 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)) + // 提取 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)) { - 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); - } + // 持久化 transferId,订阅后台进行,不阻塞页面操作 + SaveTransferId(transferId, otaData.version, "pushing"); + StartWifiOtaSubscription(transferId); } 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 { @@ -201,17 +215,13 @@ namespace Kill.UI.Pages UpdateLoadingUI.Instance.Show(); uint firmwareVersion = ParseFirmwareVersion(otaData.version); OTAManager.Instance.OnTransferProgress += OnOTATransferProgress; - // OTA 传输期间清空返回事件,防止用户触发返回中断升级;传输结束恢复 - var otaBackAction = UIManager.Instance.GetBackAction(); + // OTA 传输期间(进度显示)清空返回事件,返回键走两次返回退出App,防止误返回中断升级(成败都会返回主页) 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; @@ -244,19 +254,318 @@ namespace Kill.UI.Pages } /// - /// 订阅 WiFi OTA 传输进度(SSE),返回是否传输完成 + /// 订阅 WiFi OTA 传输进度(SSE),返回是否传输完成(仅供兼容旧调用,BLE 仍走全屏 OTAManager) /// private Task SubscribeOtaProgress(string transferId, Action onProgress) { var tcs = new TaskCompletionSource(); - StartCoroutine(SSEProgressCoroutine(transferId, onProgress, success => tcs.TrySetResult(success))); + StartCoroutine(SSELegacyCoroutine(transferId, onProgress, success => tcs.TrySetResult(success))); return tcs.Task; } + private IEnumerator SSELegacyCoroutine(string transferId, Action onProgress, Action onDone) + { + bool finished = false; + yield return SubscribeOtaProgressCoroutine(transferId, onProgress, done => finished = done); + onDone?.Invoke(finished); + } + /// - /// SSE 进度协程:持续接收进度事件,传输完成(status/percentage)或连接结束后结束 + /// 保存当前设备的 OTA transferId(与设备mac绑定,切换设备不影响) /// - private IEnumerator SSEProgressCoroutine(string transferId, Action onProgress, Action onDone) + 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; + } + + /// + /// 设置等待设备重启的绝对截止时间戳(Unix 秒)。退出页面再回来仍按此截止时间判定,不会重置。 + /// + 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; + } + + /// + /// 读取当前设备的 OTA transferId(空表示无) + /// + private string LoadTransferId() + { + string mac = DataManager.Instance.selectedDevice?.ble_mac ?? ""; + return PlayerPrefs.GetString(GetTransferIdKey(mac), ""); + } + + /// + /// 清除当前设备的 OTA transferId + /// + 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; + + /// + /// 启动 WiFi OTA 订阅:仅更新页内小进度条,不阻塞页面,可关闭/重入 + /// + private void StartWifiOtaSubscription(string transferId) + { + ShowProgressPanel(0); + updateButton.gameObject.SetActive(false); // 升级中禁用更新按钮 + StartCoroutine(SubscribeAndHandle(transferId)); + } + + /// + /// 订阅协程:完成/失败/连接结束统一处理(不强制回主页) + /// + 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); + } + } + + /// + /// 传输完成后等待设备重启+重连:轮询设备列表直到当前设备以新版本号上线,否则按持久化的绝对截止时间超时退出。 + /// 期间保持 transferId 持久化、保留页内进度条显示「升级中」文案,重新进入页面也能继续等待(截止时间不会重置)。 + /// + private IEnumerator WaitDeviceRebootAndFinish(string transferId) + { + const float pollInterval = 3f; // 轮询间隔 + long deadline = LoadWaitDeadline(); + // 兜底:若没设置过截止时间,立即设置一个 + if (deadline <= 0) + { + SetWaitDeadline(); + deadline = LoadWaitDeadline(); + } + // 已超过绝对截止时间:直接走超时分支并清掉 transferId,避免每次进入都重复提示 + if (DateTimeOffset.UtcNow.ToUnixTimeSeconds() >= deadline) + { + ClearTransferId(); + HideProgressPanel(); + updateButton.gameObject.SetActive(true); + ToastUI.Show("ota_update_failed"); + yield break; + } + ShowProgressPanelWaitingForDevice(); + ToastUI.Show("100226"); // 传输完成提示 + string targetVersion = otaData != null ? otaData.version : ""; + bool rebooted = false; + while (DateTimeOffset.UtcNow.ToUnixTimeSeconds() < deadline) + { + yield return new WaitForSeconds(pollInterval); + if (gameObject == null) yield break; // 页面已销毁 + // 仅当持久化的还是同一个 transferId 时才继续等待(用户重新点击/切换设备会清掉) + if (LoadTransferId() != transferId) yield break; + string nowVersion = null; + yield return FetchDeviceFirmwareVersion(r => nowVersion = r); + if (!string.IsNullOrEmpty(nowVersion) && IsVersionGE(nowVersion, targetVersion)) + { + rebooted = true; + break; + } + // 期间截止时间可能被重置(用户重新点击升级等),以最新值为准 + long currentDeadline = LoadWaitDeadline(); + if (currentDeadline > 0 && currentDeadline != deadline) deadline = currentDeadline; + } + if (gameObject == null) yield break; + if (LoadTransferId() != transferId) yield break; + if (rebooted) + { + // 设备已用新版本上线,更新本地版本并清理状态 + if (DataManager.Instance.selectedDevice != null && !string.IsNullOrEmpty(targetVersion)) + { + DataManager.Instance.selectedDevice.firmware_version = targetVersion; + } + ClearTransferId(); + HideProgressPanel(); + InitOtaUI(); // 重新判断:已是最新 / 又有新版本 + } + else + { + // 超时:升级失败,提示并清理 transferId(避免下次进入页面再触发) + ToastUI.Show("ota_update_failed"); + ClearTransferId(); + HideProgressPanel(); + updateButton.gameObject.SetActive(true); + } + } + + /// + /// 拉取当前设备的最新 firmware_version(仅赋值给回调,返回空表示失败/离线) + /// + private IEnumerator FetchDeviceFirmwareVersion(Action onResult) + { + string result = null; + var task = FetchDeviceFirmwareVersionAsync(v => result = v); + while (!task.IsCompleted) yield return null; + onResult?.Invoke(result); + } + + private async Task FetchDeviceFirmwareVersionAsync(Action 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(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); + } + } + + /// + /// 比较版本号:a >= b 返回 true(按 "." 分段整数比较) + /// + 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; + } + + /// + /// 进度条切换到「升级中...」状态(fill 保持满,文字提示) + /// + 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"); + } + + /// + /// 进入页面时:若本地存在 transferId,则恢复订阅并显示进度条 + /// + 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); + // 已经传输完成等待设备重启:直接走轮询分支,不重新订阅 SSE + if (savedStatus == "waiting_reboot") + { + ShowProgressPanelWaitingForDevice(); + StartCoroutine(WaitDeviceRebootAndFinish(savedId)); + return; + } + ShowProgressPanel(0); + StartCoroutine(SubscribeAndHandle(savedId)); + } + + /// + /// 仅订阅进度协程,传出是否完成(不弹出后续处理) + /// + private IEnumerator SubscribeOtaProgressCoroutine(string transferId, Action onProgress, Action onDone) { bool transferFinished = false; string sseUrl = $"/api/v1/ota/transfer/{transferId}/progress"; @@ -270,7 +579,8 @@ namespace Kill.UI.Pages 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")) + Debug.Log($"OTA 进度: {status}, {info.percentage}%"); + if (info.percentage >= 100 || status.Contains("completed")) { transferFinished = true; } @@ -284,10 +594,29 @@ namespace Kill.UI.Pages { Debug.LogError($"OTA 进度连接错误: {err}"); }); - // SSE 连接结束(服务端关闭或出错),以最后收到的进度状态判定是否完成 onDone?.Invoke(transferFinished); } + /// + /// 显示页内进度条并更新百分比 + /// + 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}%"; + } + + /// + /// 隐藏页内进度条 + /// + private void HideProgressPanel() + { + if (progressBar != null) progressBar.fillAmount = 0f; + if (progressText != null) progressText.text = "0%"; + if (progressPanel != null) progressPanel.SetActive(false); + } + /// /// 加载固件文件 /// diff --git a/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs b/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs index 93aac5b..8fd566e 100644 --- a/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs +++ b/Assets/Scripts/UI/Pages/HomePage/HomePageCtrl.cs @@ -79,7 +79,7 @@ namespace Kill.UI.Pages // 订阅蓝牙事件 SubscribeBluetoothEvents(); - // 获取设备列表 + // 获取设备列表(失败时使用上一次缓存的列表,继续后续流程) await FetchDeviceList(); Init(); } @@ -129,6 +129,13 @@ namespace Kill.UI.Pages BluetoothManager.Instance.OnStartScan+=deviceState.OnStartScan; } + // 网络状态:断网立刻把 WiFi 设备标离线,联网后异步重新确认 + if (NetworkStateManager.Instance != null) + { + NetworkStateManager.Instance.OnNetworkDisconnected += OnNetworkDisconnectedHandler; + NetworkStateManager.Instance.OnNetworkConnected += OnNetworkConnectedHandler; + } + if (BLECommunicationManager.Instance != null) { // 订阅用户登录结果事件 @@ -159,6 +166,12 @@ namespace Kill.UI.Pages BluetoothManager.Instance.OnStartScan-=deviceState.OnStartScan; } + if (NetworkStateManager.Instance != null) + { + NetworkStateManager.Instance.OnNetworkDisconnected -= OnNetworkDisconnectedHandler; + NetworkStateManager.Instance.OnNetworkConnected -= OnNetworkConnectedHandler; + } + if (BLECommunicationManager.Instance != null) { // 取消订阅用户登录结果事件 @@ -460,6 +473,33 @@ namespace Kill.UI.Pages #endregion + #region 网络状态事件处理 + + /// + /// 网络断开:立刻把 WiFi 设备标离线并刷新 UI + /// + private void OnNetworkDisconnectedHandler(object sender, EventArgs e) + { + Debug.Log("[HomePageCtrl] 网络断开,立即刷新设备WiFi为未连接"); + hasWifi = false; + DataManager.Instance.hasWifi = false; + pendingUIUpdates.Add(() => + { + deviceState?.UpdateDeviceState(hasBluetooth, hasWifi); + }); + } + + /// + /// 网络恢复:异步重新查询设备在线状态(局域网通但设备可能下线) + /// + private void OnNetworkConnectedHandler(object sender, EventArgs e) + { + Debug.Log("[HomePageCtrl] 网络恢复,重新查询设备在线状态"); + RefreshWifiOnlineStatusAsync(); + } + + #endregion + #region 设备状态回调 // 当前工作模式 @@ -823,7 +863,7 @@ namespace Kill.UI.Pages public async Task Init() { LoadingUI.Show(); - if (DataManager.Instance.userInfo.device_count == 0) + if (DataManager.Instance.userInfo.device_count == 0 || (OwnedDevices.Count == 0 && SharedDevices.Count == 0)) { noDevicePage.SetActive(true); normalPage.SetActive(false); @@ -880,12 +920,15 @@ namespace Kill.UI.Pages } selectDeviceButton.GetComponentInChildren().text = selectedDevice.device_name; selectDeviceButton.gameObject.SetActive(true); - hasWifi=await GetDeviceOnlineStatus(); - DataManager.Instance.hasWifi=hasWifi; - + // 蓝牙:直接触发连接,状态以连接回调为准(不阻塞) hasBluetooth = BluetoothManager.Instance.ChangeAimMac(selectedDevice.ble_mac); DataManager.Instance.hasBluetooth=hasBluetooth; - + + // WiFi:不阻塞主线,异步短超时查询;进入页面先按未知/未连接显示,断网时立刻离线,命中在线再补做初始化 + hasWifi = false; + DataManager.Instance.hasWifi = false; + RefreshWifiOnlineStatusAsync(); + // 先初始化所有 UI 为未连接状态 ResetAllDeviceUI(); @@ -969,8 +1012,6 @@ namespace Kill.UI.Pages /// public async Task FetchDeviceList() { - LoadingUI.Show(); - try { Debug.Log("[HomePageCtrl] 开始获取设备列表..."); @@ -980,15 +1021,20 @@ namespace Kill.UI.Pages if (string.IsNullOrEmpty(userId)) { Debug.LogError("[HomePageCtrl] 获取设备列表失败: 用户未登录"); - LoadingUI.Hide(); return; } + // 断网/接口失败兜底:先尝试从 PlayerPrefs 读取上次缓存(同步赋值给当前页字段,供随后 Init 渲染) + if (OwnedDevices.Count == 0 && SharedDevices.Count == 0 && DataManager.Instance.LoadDeviceLists()) + { + OwnedDevices = new List(DataManager.Instance.OwnedDevices); + SharedDevices = new List(DataManager.Instance.SharedDevices); + Debug.Log($"[HomePageCtrl] 使用本地缓存设备列表: 自有={OwnedDevices.Count}, 共享={SharedDevices.Count}"); + } + string url = $"/api/v1/device/user/list?user_id={userId}"; var response = await NetworkCtrl.Instance.Get(url); - LoadingUI.Hide(); - ResponseCodeHandler.HandleResponse(response, onSuccess: (data) => { @@ -1016,17 +1062,48 @@ namespace Kill.UI.Pages Debug.Log($"[HomePageCtrl] 获取设备列表成功: 自有设备={OwnedDevices.Count}, 共享设备={SharedDevices.Count}"); DataManager.Instance.userInfo.device_count = OwnedDevices.Count + SharedDevices.Count; + // 同步到 DataManager 并持久化到本地(断网时使用) + DataManager.Instance.OwnedDevices = new List(OwnedDevices); + DataManager.Instance.SharedDevices = new List(SharedDevices); + DataManager.Instance.SaveDeviceLists(); + // 设备列表更新后,刷新主页选中设备与 UI(不阻塞初次进入) + Loom.QueueOnMainThread(() => RefreshAfterDeviceListUpdated()); }, onError: null ); } catch (Exception ex) { - LoadingUI.Hide(); Debug.LogError($"[HomePageCtrl] 获取设备列表异常: {ex.Message}"); } } + /// + /// 设备列表更新后:重新解析 selectedDevice 并刷新主页相关 UI(不重复 LoadingUI) + /// + private void RefreshAfterDeviceListUpdated() + { + if (DataManager.Instance == null) return; + // 若初次进入时无设备列表而走了「无设备」分支,列表就绪后再切回正常页 + if (OwnedDevices.Count > 0 || SharedDevices.Count > 0) + { + noDevicePage.SetActive(false); + normalPage.SetActive(true); + } + // 重新定位 selectedDevice + if (DataManager.Instance.selectedDevice == null && (OwnedDevices.Count > 0 || SharedDevices.Count > 0)) + { + selectedDevice = OwnedDevices.Count > 0 ? OwnedDevices[0] : SharedDevices[0]; + DataManager.Instance.selectedDeviceMac = selectedDevice.ble_mac; + DataManager.Instance.SavaSelectedDeviceMac(selectedDevice.ble_mac); + DataManager.Instance.selectedDevice = selectedDevice; + selectDeviceButton.GetComponentInChildren().text = selectedDevice.device_name; + selectDeviceButton.gameObject.SetActive(true); + } + DataManager.Instance.OwnedDevices = OwnedDevices; + DataManager.Instance.SharedDevices = SharedDevices; + } + void Update() { // 执行所有挂起的UI更新 @@ -1683,7 +1760,8 @@ namespace Kill.UI.Pages var response = await NetworkCtrl.Instance.Get( "/api/v1/device/online", - new Dictionary { { "mac", selectedDevice.ble_mac } } + new Dictionary { { "mac", selectedDevice.ble_mac } }, + new HttpRequestConfig { Timeout = 5, MaxRetries = 1, RetryDelayMs = 500, UseExponentialBackoff = false } ); ResponseCodeHandler.HandleResponse(response, @@ -1697,12 +1775,78 @@ namespace Kill.UI.Pages { Debug.LogError($"[HomePageCtrl] 查询设备在线状态失败: {msg}"); tcs.TrySetResult(false); - } + }, + silentErrorTip: true ); + // 兜底:silentErrorTip=true 时 CheckGlobalHandle 会直接 return,导致 onError 永远不被调, + // 这里加超时与兜底,避免 tcs 永久 hang(否则后台轮询将停在这一帧) + if (!tcs.Task.IsCompleted) + { + Debug.LogWarning("[HomePageCtrl] 设备在线状态查询未返回结果,兜底设为未连接"); + tcs.TrySetResult(false); + } + return await tcs.Task; } + /// + /// 非阻塞刷新 WiFi 在线状态:失败立即标记未连接并刷新 UI;后台轮询会持续覆盖 + /// 若从离线变为在线且蓝牙未连接,补做 WiFi 分支的设备状态初始化(拉取 config/schedule) + /// + public async void RefreshWifiOnlineStatusAsync() + { + if (selectedDevice == null || string.IsNullOrEmpty(selectedDevice.ble_mac)) return; + bool wasOnline = hasWifi; + bool online; + try + { + online = await GetDeviceOnlineStatus(); + } + catch (System.Exception ex) + { + Debug.LogError($"[HomePageCtrl] 刷新WiFi在线状态异常: {ex.Message}"); + online = false; + } + if (this == null || selectedDevice == null) return; + // 失败(online=false)一律立即刷为未连接,避免 UI 残留已连接态 + hasWifi = online; + DataManager.Instance.hasWifi = online; + pendingUIUpdates.Add(() => + { + deviceState?.UpdateDeviceState(hasBluetooth, hasWifi); + }); + if (!online) + { + Debug.Log("[HomePageCtrl] WiFi在线状态获取失败,已将设备刷新为未连接"); + return; + } + // 由离线变为在线且蓝牙未连接:补做 WiFi 分支的设备状态初始化 + if (!wasOnline && !hasBluetooth) + { + Debug.Log("[HomePageCtrl] WiFi 由离线变为在线,补做设备状态初始化"); + _ = InitWifiOnlyDeviceStateAsync(); + } + } + + /// + /// WiFi-only 分支的设备状态初始化(拉取配置/定时任务),与 Init() 中对应分支保持一致 + /// + private async Task InitWifiOnlyDeviceStateAsync() + { + if (selectedDevice == null) return; + deviceCtrl?.InitDeviceControl(false); + await DataManager.Instance.GetDeviceConfig(selectedDevice.ble_mac); + await DataManager.Instance.GetScheduleTasks(selectedDevice.ble_mac); + // 期间蓝牙可能已连接,避免用 WiFi 数据覆盖 BLE 数据 + if (!hasBluetooth) + { + scheduleSettingButton.interactable = true; + moreSettingButton.interactable = true; + InitDeviceUIFromConfig(); + } + } + /// /// 启动设备在线状态轮询(始终运行,后台检测WiFi状态变化) /// @@ -1739,6 +1883,14 @@ namespace Kill.UI.Pages deviceState?.UpdateDeviceState(hasBluetooth, hasWifi); }); } + else + { + // 服务器超时/网络异常:无论初始 wasOnline 状态,都强制刷新 UI 一次以确保图标同步 + pendingUIUpdates.Add(() => + { + deviceState?.UpdateDeviceState(hasBluetooth, hasWifi); + }); + } } else if (online && !wasOnline && !hasBluetooth) {