feat: 新增OTA升级相关功能与设备列表缓存优化
1. 新增设备信息页OTA升级进度UI与本地持久化传输状态 2. 实现WiFi模式OTA非全屏进度订阅与重启等待逻辑 3. 新增设备列表本地缓存与加载、清空功能 4. 优化蓝牙管理:移除自动弹窗,改为提示用户手动开启蓝牙 5. 新增网络状态监听与设备在线状态刷新逻辑 6. 新增多语言文案与蓝牙未开启提示资源 7. 优化网络请求处理,新增静默错误提示参数
This commit is contained in:
parent
77b29b4f15
commit
a95b257b0d
@ -1134,6 +1134,16 @@
|
|||||||
"zh": "固件传输失败,请重试",
|
"zh": "固件传输失败,请重试",
|
||||||
"en": "Firmware transfer failed. Please try again."
|
"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",
|
"key": "100228",
|
||||||
"zh": "温度错误:",
|
"zh": "温度错误:",
|
||||||
@ -1663,6 +1673,11 @@
|
|||||||
"key": "100333",
|
"key": "100333",
|
||||||
"zh": "设备需WiFi在线方可进行OTA升级",
|
"zh": "设备需WiFi在线方可进行OTA升级",
|
||||||
"en": "OTA update requires the device to be online via WiFi"
|
"en": "OTA update requires the device to be online via WiFi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "100334",
|
||||||
|
"zh": "蓝牙未开启,如需使用请打开",
|
||||||
|
"en": "Bluetooth is off. Turn it on if needed."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,139 @@
|
|||||||
%YAML 1.1
|
%YAML 1.1
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
%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
|
--- !u!1 &312154182751522083
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@ -34,6 +168,7 @@ RectTransform:
|
|||||||
- {fileID: 2568352390314877935}
|
- {fileID: 2568352390314877935}
|
||||||
- {fileID: 3959655657766269314}
|
- {fileID: 3959655657766269314}
|
||||||
- {fileID: 5168603418929363938}
|
- {fileID: 5168603418929363938}
|
||||||
|
- {fileID: 6901814223954429598}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
@ -97,6 +232,9 @@ MonoBehaviour:
|
|||||||
changeLogText: {fileID: 1957090092938833670}
|
changeLogText: {fileID: 1957090092938833670}
|
||||||
backButton: {fileID: 5686861562038373004}
|
backButton: {fileID: 5686861562038373004}
|
||||||
updateButton: {fileID: 2226008222436017730}
|
updateButton: {fileID: 2226008222436017730}
|
||||||
|
progressPanel: {fileID: 241336363346333283}
|
||||||
|
progressBar: {fileID: 4875656352718239638}
|
||||||
|
progressText: {fileID: 1165127137834906993}
|
||||||
--- !u!1 &414212200923698105
|
--- !u!1 &414212200923698105
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@ -852,6 +990,174 @@ MonoBehaviour:
|
|||||||
m_FillOrigin: 0
|
m_FillOrigin: 0
|
||||||
m_UseSpriteMesh: 0
|
m_UseSpriteMesh: 0
|
||||||
m_PixelsPerUnitMultiplier: 1
|
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
|
--- !u!1 &4561661931995402753
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
BIN
Assets/Res/ui/deviceinfopage/更新进度.png
Normal file
BIN
Assets/Res/ui/deviceinfopage/更新进度.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
127
Assets/Res/ui/deviceinfopage/更新进度.png.meta
Normal file
127
Assets/Res/ui/deviceinfopage/更新进度.png.meta
Normal file
@ -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:
|
||||||
@ -243,16 +243,15 @@ namespace Kill.Bluetooth
|
|||||||
|
|
||||||
if (!isEnabled)
|
if (!isEnabled)
|
||||||
{
|
{
|
||||||
Log("蓝牙未开启,使用 Intent 请求用户启用...");
|
// 不再弹出系统蓝牙开启弹窗(弹窗会打断 App 生命周期,导致返回后页面卡住不刷新),
|
||||||
|
// 仅提示用户手动去系统设置中开启蓝牙
|
||||||
using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
|
Log("蓝牙未开启,仅提示用户手动开启蓝牙");
|
||||||
using (AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
|
ToastUI.Show("100334");
|
||||||
{
|
_enableBtCoroutine = null;
|
||||||
AndroidJavaObject intent = new AndroidJavaObject("android.content.Intent", "android.bluetooth.adapter.action.REQUEST_ENABLE");
|
_pendingStartScan = false;
|
||||||
currentActivity.Call("startActivityForResult", intent, 1);
|
_pendingConnectAddress = null;
|
||||||
}
|
OnError?.Invoke("蓝牙未开启");
|
||||||
|
OnInitialized?.Invoke(false);
|
||||||
_enableBtCoroutine = StartCoroutine(WaitForBluetoothEnabled(onBluetoothReady));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -350,20 +349,23 @@ namespace Kill.Bluetooth
|
|||||||
Log("iOS 蓝牙已开启");
|
Log("iOS 蓝牙已开启");
|
||||||
onBluetoothReady?.Invoke();
|
onBluetoothReady?.Invoke();
|
||||||
}
|
}
|
||||||
// 4 = PoweredOff (蓝牙关闭) — iOS 系统会自动弹出蓝牙开启对话框,无需手动跳转设置
|
// 4 = PoweredOff (蓝牙关闭) — 仅提示用户手动开启,不等待系统弹窗
|
||||||
else if (state == 4)
|
else if (state == 4)
|
||||||
{
|
{
|
||||||
Log("iOS 蓝牙未开启,等待系统弹出蓝牙开启提示...");
|
Log("iOS 蓝牙未开启,仅提示用户手动开启蓝牙");
|
||||||
// 不调用 ShowIOSTurnOnBluetoothPrompt(),让 iOS 原生弹窗处理
|
ToastUI.Show("100334");
|
||||||
// 等待蓝牙状态变化(检测焦点变化 + 轮询蓝牙状态)
|
_enableBtCoroutine = null;
|
||||||
_enableBtCoroutine = StartCoroutine(WaitForIOSBluetoothEnabled(onBluetoothReady));
|
OnError?.Invoke("蓝牙未开启");
|
||||||
|
OnInitialized?.Invoke(false);
|
||||||
}
|
}
|
||||||
// 3 = Unauthorized (未授权) — 需要跳转到应用设置页面让用户开启蓝牙权限
|
// 3 = Unauthorized (未授权) — 仅提示用户去系统设置中开启蓝牙权限,不自动跳转
|
||||||
else if (state == 3)
|
else if (state == 3)
|
||||||
{
|
{
|
||||||
Log("iOS 蓝牙未授权,跳转到应用设置页...");
|
Log("iOS 蓝牙未授权,仅提示用户去系统设置中开启蓝牙权限");
|
||||||
ShowIOSTurnOnBluetoothPrompt();
|
ToastUI.Show("100334");
|
||||||
_enableBtCoroutine = StartCoroutine(WaitForIOSBluetoothEnabled(onBluetoothReady));
|
_enableBtCoroutine = null;
|
||||||
|
OnError?.Invoke("蓝牙未授权");
|
||||||
|
OnInitialized?.Invoke(false);
|
||||||
}
|
}
|
||||||
// 2 = Unsupported (不支持蓝牙)
|
// 2 = Unsupported (不支持蓝牙)
|
||||||
else if (state == 2)
|
else if (state == 2)
|
||||||
|
|||||||
@ -53,6 +53,79 @@ namespace Kill.Managers
|
|||||||
{
|
{
|
||||||
selectedDeviceMac=PlayerPrefs.GetString("selectedDeviceMac", "");
|
selectedDeviceMac=PlayerPrefs.GetString("selectedDeviceMac", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region 设备列表本地缓存(断网时使用)
|
||||||
|
private const string OWNED_DEVICES_KEY = "ownedDevicesCache";
|
||||||
|
private const string SHARED_DEVICES_KEY = "sharedDevicesCache";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将当前 OwnedDevices/SharedDevices 持久化到本地(按当前登录用户隔离)
|
||||||
|
/// </summary>
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从本地恢复 OwnedDevices/SharedDevices(返回是否成功恢复)
|
||||||
|
/// </summary>
|
||||||
|
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<DeviceInfo>() : JsonUtility.FromJson<DeviceInfoListWrapper>(ownedJson)?.ToList() ?? new List<DeviceInfo>();
|
||||||
|
SharedDevices = string.IsNullOrEmpty(sharedJson) ? new List<DeviceInfo>() : JsonUtility.FromJson<DeviceInfoListWrapper>(sharedJson)?.ToList() ?? new List<DeviceInfo>();
|
||||||
|
return OwnedDevices.Count > 0 || SharedDevices.Count > 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[DataManager] 读取设备列表缓存失败: {ex.Message}");
|
||||||
|
OwnedDevices = new List<DeviceInfo>();
|
||||||
|
SharedDevices = new List<DeviceInfo>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空本地缓存的设备列表(登出时调用)
|
||||||
|
/// </summary>
|
||||||
|
public void ClearDeviceLists()
|
||||||
|
{
|
||||||
|
string userId = userInfo?.id ?? "";
|
||||||
|
PlayerPrefs.DeleteKey(OWNED_DEVICES_KEY + "_" + userId);
|
||||||
|
PlayerPrefs.DeleteKey(SHARED_DEVICES_KEY + "_" + userId);
|
||||||
|
PlayerPrefs.Save();
|
||||||
|
OwnedDevices = new List<DeviceInfo>();
|
||||||
|
SharedDevices = new List<DeviceInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
private class DeviceInfoListWrapper
|
||||||
|
{
|
||||||
|
public List<DeviceInfo> items = new List<DeviceInfo>();
|
||||||
|
public DeviceInfoListWrapper() { }
|
||||||
|
public DeviceInfoListWrapper(List<DeviceInfo> source)
|
||||||
|
{
|
||||||
|
if (source != null) items.AddRange(source);
|
||||||
|
}
|
||||||
|
public List<DeviceInfo> ToList() => new List<DeviceInfo>(items);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
public void InitUser()
|
public void InitUser()
|
||||||
{
|
{
|
||||||
token = PlayerPrefs.GetString("token", "");
|
token = PlayerPrefs.GetString("token", "");
|
||||||
@ -124,7 +197,8 @@ namespace Kill.Managers
|
|||||||
onError: (code, msg) =>
|
onError: (code, msg) =>
|
||||||
{
|
{
|
||||||
Debug.LogError($"[DataManager] 获取设备配置失败: {msg}");
|
Debug.LogError($"[DataManager] 获取设备配置失败: {msg}");
|
||||||
}
|
},
|
||||||
|
silentErrorTip: true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +238,8 @@ namespace Kill.Managers
|
|||||||
onError: (code, msg) =>
|
onError: (code, msg) =>
|
||||||
{
|
{
|
||||||
Debug.LogError($"[DataManager] 修改设备配置失败: {msg}");
|
Debug.LogError($"[DataManager] 修改设备配置失败: {msg}");
|
||||||
}
|
},
|
||||||
|
silentErrorTip: true
|
||||||
);
|
);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|||||||
@ -93,21 +93,23 @@ namespace Kill.Network
|
|||||||
/// <param name="code">输出状态码</param>
|
/// <param name="code">输出状态码</param>
|
||||||
/// <param name="message">输出消息</param>
|
/// <param name="message">输出消息</param>
|
||||||
/// <returns>是否已全局处理</returns>
|
/// <returns>是否已全局处理</returns>
|
||||||
public static bool CheckGlobalHandle<T>(HttpResponse<T> response, out int code, out string message)
|
public static bool CheckGlobalHandle<T>(HttpResponse<T> response, out int code, out string message, bool silentErrorTip = false)
|
||||||
{
|
{
|
||||||
code = -1;
|
code = -1;
|
||||||
message = null;
|
message = null;
|
||||||
// 空响应(网络异常)
|
// 空响应(网络异常)
|
||||||
if (response == null)
|
if (response == null)
|
||||||
{
|
{
|
||||||
ShowErrorTip("100036");
|
message = "网络异常";
|
||||||
|
if (!silentErrorTip) ShowErrorTip("100036");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP错误(网络层错误)
|
// HTTP错误(网络层错误)
|
||||||
if (!response.IsSuccess)
|
if (!response.IsSuccess)
|
||||||
{
|
{
|
||||||
ShowErrorTip("100036");
|
message = string.IsNullOrEmpty(response.ErrorMessage) ? "网络异常" : response.ErrorMessage;
|
||||||
|
if (!silentErrorTip) ShowErrorTip("100036");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,13 +134,19 @@ namespace Kill.Network
|
|||||||
/// <param name="response">HTTP响应</param>
|
/// <param name="response">HTTP响应</param>
|
||||||
/// <param name="onSuccess">成功回调(code=200)</param>
|
/// <param name="onSuccess">成功回调(code=200)</param>
|
||||||
/// <param name="onError">其他错误回调(可选,不传则自动提示)</param>
|
/// <param name="onError">其他错误回调(可选,不传则自动提示)</param>
|
||||||
public static void HandleResponse<T>(HttpResponse<T> response, Action<T> onSuccess, Action<int, string> onError = null)
|
/// <param name="silentErrorTip">true 时静默处理网络错误(不弹100036),仅触发 onError</param>
|
||||||
|
public static void HandleResponse<T>(HttpResponse<T> response, Action<T> onSuccess, Action<int, string> onError = null, bool silentErrorTip = false)
|
||||||
{
|
{
|
||||||
// 全局处理(网络错误、601等)
|
// 全局处理(网络错误、601等)
|
||||||
int code;
|
int code;
|
||||||
string message;
|
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; // 已被全局处理
|
return; // 已被全局处理
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,22 @@ namespace Kill.UI.Pages
|
|||||||
public Text changeLogText;
|
public Text changeLogText;
|
||||||
public Button backButton;
|
public Button backButton;
|
||||||
public Button updateButton;
|
public Button updateButton;
|
||||||
|
// WiFi OTA 页内进度条(非全屏,不阻塞操作)
|
||||||
|
public GameObject progressPanel;
|
||||||
|
public Image progressBar; // Fill Method = Horizontal, Fill Origin = Left
|
||||||
|
public Text progressText;
|
||||||
OTAData otaData;
|
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()
|
void Start()
|
||||||
{
|
{
|
||||||
UIManager.Instance.RegisterBackAction(Back);
|
UIManager.Instance.RegisterBackAction(Back);
|
||||||
@ -30,6 +45,7 @@ namespace Kill.UI.Pages
|
|||||||
updateButton.onClick.RemoveAllListeners();
|
updateButton.onClick.RemoveAllListeners();
|
||||||
updateButton.onClick.AddListener(OnUpdateButtonClick);
|
updateButton.onClick.AddListener(OnUpdateButtonClick);
|
||||||
updateButton.gameObject.SetActive(false);
|
updateButton.gameObject.SetActive(false);
|
||||||
|
if (progressPanel != null) progressPanel.SetActive(false);
|
||||||
GetOtaInfo();
|
GetOtaInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +106,9 @@ namespace Kill.UI.Pages
|
|||||||
versionText = latestTip.transform.Find("value").GetComponent<Text>();
|
versionText = latestTip.transform.Find("value").GetComponent<Text>();
|
||||||
versionText.text = otaData.version;
|
versionText.text = otaData.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进入页面:若本地有未结束的 transferId,则恢复订阅进度(非全屏,不阻塞)
|
||||||
|
TryResumeWifiOta();
|
||||||
}
|
}
|
||||||
public void Back()
|
public void Back()
|
||||||
{
|
{
|
||||||
@ -97,7 +116,6 @@ namespace Kill.UI.Pages
|
|||||||
Destroy(gameObject);
|
Destroy(gameObject);
|
||||||
Screen.sleepTimeout = SleepTimeout.SystemSetting;
|
Screen.sleepTimeout = SleepTimeout.SystemSetting;
|
||||||
}
|
}
|
||||||
private bool isUpdating; // OTA升级进行中(防连点重入)
|
|
||||||
public async void OnUpdateButtonClick()
|
public async void OnUpdateButtonClick()
|
||||||
{
|
{
|
||||||
// 防连点:升级流程进行中忽略重复触发,避免并发启动两个OTA
|
// 防连点:升级流程进行中忽略重复触发,避免并发启动两个OTA
|
||||||
@ -113,12 +131,9 @@ namespace Kill.UI.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WiFi 模式:push OTA 指令,SSE 订阅传输进度
|
// WiFi 模式:push OTA 指令,SSE 订阅传输进度(非全屏,不阻塞操作,可关闭页面后下次进入继续订阅)
|
||||||
if (DataManager.Instance.hasWifi)
|
if (DataManager.Instance.hasWifi)
|
||||||
{
|
{
|
||||||
ToastUI.Show("100294");
|
|
||||||
UpdateLoadingUI.Instance.Show();
|
|
||||||
UpdateLoadingUI.Instance.SetPrecent(0);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pushRequest = new OTAPushRequest
|
var pushRequest = new OTAPushRequest
|
||||||
@ -127,39 +142,38 @@ namespace Kill.UI.Pages
|
|||||||
version = otaData.version
|
version = otaData.version
|
||||||
};
|
};
|
||||||
var pushResponse = await NetworkCtrl.Instance.Post<OTAPushResponse>("/api/v1/ota/push", pushRequest);
|
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))
|
// 提取 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}");
|
Debug.Log($"WiFi OTA push 成功,transferId: {transferId}");
|
||||||
// SSE 订阅传输进度,传输完成返回 true
|
// 持久化 transferId,订阅后台进行,不阻塞页面操作
|
||||||
bool success = await SubscribeOtaProgress(transferId, p => UpdateLoadingUI.Instance.SetPrecent(p / 100f));
|
SaveTransferId(transferId, otaData.version, "pushing");
|
||||||
if (success)
|
StartWifiOtaSubscription(transferId);
|
||||||
{
|
|
||||||
ToastUI.Show("100226");
|
|
||||||
DataManager.Instance.selectedDevice.firmware_version = otaData.version;
|
|
||||||
// 传输完成后等待 10s 再返回主页(设备升级后需要时间重启)
|
|
||||||
StartCoroutine(DelayedGoHome(10f));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ToastUI.Show("100227");
|
|
||||||
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ToastUI.Show("100227");
|
ToastUI.Show("100227");
|
||||||
Debug.LogError($"WiFi OTA push 失败: {pushResponse.ErrorMessage ?? pushResponse.RawResponse}");
|
Debug.LogError($"WiFi OTA push 失败: {pushResponse.ErrorMessage ?? pushResponse.RawResponse}");
|
||||||
UpdateLoadingUI.Instance.Hide();
|
|
||||||
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (System.Exception ex)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogError($"WiFi OTA push 异常: {ex.Message}");
|
Debug.LogError($"WiFi OTA push 异常: {ex.Message}");
|
||||||
UpdateLoadingUI.Instance.Hide();
|
|
||||||
ToastUI.Show("100227");
|
ToastUI.Show("100227");
|
||||||
UIManager.Instance.OpenMainPage(UIManager.PageName.homePage);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -201,17 +215,13 @@ namespace Kill.UI.Pages
|
|||||||
UpdateLoadingUI.Instance.Show();
|
UpdateLoadingUI.Instance.Show();
|
||||||
uint firmwareVersion = ParseFirmwareVersion(otaData.version);
|
uint firmwareVersion = ParseFirmwareVersion(otaData.version);
|
||||||
OTAManager.Instance.OnTransferProgress += OnOTATransferProgress;
|
OTAManager.Instance.OnTransferProgress += OnOTATransferProgress;
|
||||||
// OTA 传输期间清空返回事件,防止用户触发返回中断升级;传输结束恢复
|
// OTA 传输期间(进度显示)清空返回事件,返回键走两次返回退出App,防止误返回中断升级(成败都会返回主页)
|
||||||
var otaBackAction = UIManager.Instance.GetBackAction();
|
|
||||||
UIManager.Instance.ClearBackAction();
|
UIManager.Instance.ClearBackAction();
|
||||||
StartCoroutine(OTAManager.Instance.PerformFullUpgrade(_selectedFirmwareData, firmwareVersion, (success) =>
|
StartCoroutine(OTAManager.Instance.PerformFullUpgrade(_selectedFirmwareData, firmwareVersion, (success) =>
|
||||||
{
|
{
|
||||||
Loom.QueueOnMainThread(() =>
|
Loom.QueueOnMainThread(() =>
|
||||||
{
|
{
|
||||||
isUpdating = false;
|
isUpdating = false;
|
||||||
// 恢复返回事件
|
|
||||||
if (otaBackAction != null)
|
|
||||||
UIManager.Instance.RegisterBackAction(otaBackAction);
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
DataManager.Instance.selectedDevice.firmware_version = otaData.version;
|
DataManager.Instance.selectedDevice.firmware_version = otaData.version;
|
||||||
@ -244,19 +254,318 @@ namespace Kill.UI.Pages
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 订阅 WiFi OTA 传输进度(SSE),返回是否传输完成
|
/// 订阅 WiFi OTA 传输进度(SSE),返回是否传输完成(仅供兼容旧调用,BLE 仍走全屏 OTAManager)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Task<bool> SubscribeOtaProgress(string transferId, Action<int> onProgress)
|
private Task<bool> SubscribeOtaProgress(string transferId, Action<int> onProgress)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
StartCoroutine(SSEProgressCoroutine(transferId, onProgress, success => tcs.TrySetResult(success)));
|
StartCoroutine(SSELegacyCoroutine(transferId, onProgress, success => tcs.TrySetResult(success)));
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
/// <summary>
|
||||||
/// SSE 进度协程:持续接收进度事件,传输完成(status/percentage)或连接结束后结束
|
/// 保存当前设备的 OTA transferId(与设备mac绑定,切换设备不影响)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerator SSEProgressCoroutine(string transferId, Action<int> onProgress, Action<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取当前设备的 OTA transferId(空表示无)
|
||||||
|
/// </summary>
|
||||||
|
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>
|
||||||
|
/// 传输完成后等待设备重启+重连:轮询设备列表直到当前设备以新版本号上线,否则按持久化的绝对截止时间超时退出。
|
||||||
|
/// 期间保持 transferId 持久化、保留页内进度条显示「升级中」文案,重新进入页面也能继续等待(截止时间不会重置)。
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
// 已经传输完成等待设备重启:直接走轮询分支,不重新订阅 SSE
|
||||||
|
if (savedStatus == "waiting_reboot")
|
||||||
|
{
|
||||||
|
ShowProgressPanelWaitingForDevice();
|
||||||
|
StartCoroutine(WaitDeviceRebootAndFinish(savedId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ShowProgressPanel(0);
|
||||||
|
StartCoroutine(SubscribeAndHandle(savedId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 仅订阅进度协程,传出是否完成(不弹出后续处理)
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator SubscribeOtaProgressCoroutine(string transferId, Action<int> onProgress, Action<bool> onDone)
|
||||||
{
|
{
|
||||||
bool transferFinished = false;
|
bool transferFinished = false;
|
||||||
string sseUrl = $"/api/v1/ota/transfer/{transferId}/progress";
|
string sseUrl = $"/api/v1/ota/transfer/{transferId}/progress";
|
||||||
@ -270,7 +579,8 @@ namespace Kill.UI.Pages
|
|||||||
if (info == null) return;
|
if (info == null) return;
|
||||||
onProgress?.Invoke(info.percentage);
|
onProgress?.Invoke(info.percentage);
|
||||||
string status = (info.status ?? "").ToLower();
|
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;
|
transferFinished = true;
|
||||||
}
|
}
|
||||||
@ -284,10 +594,29 @@ namespace Kill.UI.Pages
|
|||||||
{
|
{
|
||||||
Debug.LogError($"OTA 进度连接错误: {err}");
|
Debug.LogError($"OTA 进度连接错误: {err}");
|
||||||
});
|
});
|
||||||
// SSE 连接结束(服务端关闭或出错),以最后收到的进度状态判定是否完成
|
|
||||||
onDone?.Invoke(transferFinished);
|
onDone?.Invoke(transferFinished);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 加载固件文件
|
/// 加载固件文件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -79,7 +79,7 @@ namespace Kill.UI.Pages
|
|||||||
// 订阅蓝牙事件
|
// 订阅蓝牙事件
|
||||||
SubscribeBluetoothEvents();
|
SubscribeBluetoothEvents();
|
||||||
|
|
||||||
// 获取设备列表
|
// 获取设备列表(失败时使用上一次缓存的列表,继续后续流程)
|
||||||
await FetchDeviceList();
|
await FetchDeviceList();
|
||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
@ -129,6 +129,13 @@ namespace Kill.UI.Pages
|
|||||||
BluetoothManager.Instance.OnStartScan+=deviceState.OnStartScan;
|
BluetoothManager.Instance.OnStartScan+=deviceState.OnStartScan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 网络状态:断网立刻把 WiFi 设备标离线,联网后异步重新确认
|
||||||
|
if (NetworkStateManager.Instance != null)
|
||||||
|
{
|
||||||
|
NetworkStateManager.Instance.OnNetworkDisconnected += OnNetworkDisconnectedHandler;
|
||||||
|
NetworkStateManager.Instance.OnNetworkConnected += OnNetworkConnectedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
if (BLECommunicationManager.Instance != null)
|
if (BLECommunicationManager.Instance != null)
|
||||||
{
|
{
|
||||||
// 订阅用户登录结果事件
|
// 订阅用户登录结果事件
|
||||||
@ -159,6 +166,12 @@ namespace Kill.UI.Pages
|
|||||||
BluetoothManager.Instance.OnStartScan-=deviceState.OnStartScan;
|
BluetoothManager.Instance.OnStartScan-=deviceState.OnStartScan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (NetworkStateManager.Instance != null)
|
||||||
|
{
|
||||||
|
NetworkStateManager.Instance.OnNetworkDisconnected -= OnNetworkDisconnectedHandler;
|
||||||
|
NetworkStateManager.Instance.OnNetworkConnected -= OnNetworkConnectedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
if (BLECommunicationManager.Instance != null)
|
if (BLECommunicationManager.Instance != null)
|
||||||
{
|
{
|
||||||
// 取消订阅用户登录结果事件
|
// 取消订阅用户登录结果事件
|
||||||
@ -460,6 +473,33 @@ namespace Kill.UI.Pages
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region 网络状态事件处理
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 网络断开:立刻把 WiFi 设备标离线并刷新 UI
|
||||||
|
/// </summary>
|
||||||
|
private void OnNetworkDisconnectedHandler(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Debug.Log("[HomePageCtrl] 网络断开,立即刷新设备WiFi为未连接");
|
||||||
|
hasWifi = false;
|
||||||
|
DataManager.Instance.hasWifi = false;
|
||||||
|
pendingUIUpdates.Add(() =>
|
||||||
|
{
|
||||||
|
deviceState?.UpdateDeviceState(hasBluetooth, hasWifi);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 网络恢复:异步重新查询设备在线状态(局域网通但设备可能下线)
|
||||||
|
/// </summary>
|
||||||
|
private void OnNetworkConnectedHandler(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Debug.Log("[HomePageCtrl] 网络恢复,重新查询设备在线状态");
|
||||||
|
RefreshWifiOnlineStatusAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region 设备状态回调
|
#region 设备状态回调
|
||||||
|
|
||||||
// 当前工作模式
|
// 当前工作模式
|
||||||
@ -823,7 +863,7 @@ namespace Kill.UI.Pages
|
|||||||
public async Task Init()
|
public async Task Init()
|
||||||
{
|
{
|
||||||
LoadingUI.Show();
|
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);
|
noDevicePage.SetActive(true);
|
||||||
normalPage.SetActive(false);
|
normalPage.SetActive(false);
|
||||||
@ -880,12 +920,15 @@ namespace Kill.UI.Pages
|
|||||||
}
|
}
|
||||||
selectDeviceButton.GetComponentInChildren<Text>().text = selectedDevice.device_name;
|
selectDeviceButton.GetComponentInChildren<Text>().text = selectedDevice.device_name;
|
||||||
selectDeviceButton.gameObject.SetActive(true);
|
selectDeviceButton.gameObject.SetActive(true);
|
||||||
hasWifi=await GetDeviceOnlineStatus();
|
// 蓝牙:直接触发连接,状态以连接回调为准(不阻塞)
|
||||||
DataManager.Instance.hasWifi=hasWifi;
|
|
||||||
|
|
||||||
hasBluetooth = BluetoothManager.Instance.ChangeAimMac(selectedDevice.ble_mac);
|
hasBluetooth = BluetoothManager.Instance.ChangeAimMac(selectedDevice.ble_mac);
|
||||||
DataManager.Instance.hasBluetooth=hasBluetooth;
|
DataManager.Instance.hasBluetooth=hasBluetooth;
|
||||||
|
|
||||||
|
// WiFi:不阻塞主线,异步短超时查询;进入页面先按未知/未连接显示,断网时立刻离线,命中在线再补做初始化
|
||||||
|
hasWifi = false;
|
||||||
|
DataManager.Instance.hasWifi = false;
|
||||||
|
RefreshWifiOnlineStatusAsync();
|
||||||
|
|
||||||
// 先初始化所有 UI 为未连接状态
|
// 先初始化所有 UI 为未连接状态
|
||||||
ResetAllDeviceUI();
|
ResetAllDeviceUI();
|
||||||
|
|
||||||
@ -969,8 +1012,6 @@ namespace Kill.UI.Pages
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task FetchDeviceList()
|
public async Task FetchDeviceList()
|
||||||
{
|
{
|
||||||
LoadingUI.Show();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Debug.Log("[HomePageCtrl] 开始获取设备列表...");
|
Debug.Log("[HomePageCtrl] 开始获取设备列表...");
|
||||||
@ -980,15 +1021,20 @@ namespace Kill.UI.Pages
|
|||||||
if (string.IsNullOrEmpty(userId))
|
if (string.IsNullOrEmpty(userId))
|
||||||
{
|
{
|
||||||
Debug.LogError("[HomePageCtrl] 获取设备列表失败: 用户未登录");
|
Debug.LogError("[HomePageCtrl] 获取设备列表失败: 用户未登录");
|
||||||
LoadingUI.Hide();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 断网/接口失败兜底:先尝试从 PlayerPrefs 读取上次缓存(同步赋值给当前页字段,供随后 Init 渲染)
|
||||||
|
if (OwnedDevices.Count == 0 && SharedDevices.Count == 0 && DataManager.Instance.LoadDeviceLists())
|
||||||
|
{
|
||||||
|
OwnedDevices = new List<Kill.Managers.DeviceInfo>(DataManager.Instance.OwnedDevices);
|
||||||
|
SharedDevices = new List<Kill.Managers.DeviceInfo>(DataManager.Instance.SharedDevices);
|
||||||
|
Debug.Log($"[HomePageCtrl] 使用本地缓存设备列表: 自有={OwnedDevices.Count}, 共享={SharedDevices.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
string url = $"/api/v1/device/user/list?user_id={userId}";
|
string url = $"/api/v1/device/user/list?user_id={userId}";
|
||||||
var response = await NetworkCtrl.Instance.Get<DeviceListResponse>(url);
|
var response = await NetworkCtrl.Instance.Get<DeviceListResponse>(url);
|
||||||
|
|
||||||
LoadingUI.Hide();
|
|
||||||
|
|
||||||
ResponseCodeHandler.HandleResponse(response,
|
ResponseCodeHandler.HandleResponse(response,
|
||||||
onSuccess: (data) =>
|
onSuccess: (data) =>
|
||||||
{
|
{
|
||||||
@ -1016,17 +1062,48 @@ namespace Kill.UI.Pages
|
|||||||
|
|
||||||
Debug.Log($"[HomePageCtrl] 获取设备列表成功: 自有设备={OwnedDevices.Count}, 共享设备={SharedDevices.Count}");
|
Debug.Log($"[HomePageCtrl] 获取设备列表成功: 自有设备={OwnedDevices.Count}, 共享设备={SharedDevices.Count}");
|
||||||
DataManager.Instance.userInfo.device_count = OwnedDevices.Count + SharedDevices.Count;
|
DataManager.Instance.userInfo.device_count = OwnedDevices.Count + SharedDevices.Count;
|
||||||
|
// 同步到 DataManager 并持久化到本地(断网时使用)
|
||||||
|
DataManager.Instance.OwnedDevices = new List<Kill.Managers.DeviceInfo>(OwnedDevices);
|
||||||
|
DataManager.Instance.SharedDevices = new List<Kill.Managers.DeviceInfo>(SharedDevices);
|
||||||
|
DataManager.Instance.SaveDeviceLists();
|
||||||
|
// 设备列表更新后,刷新主页选中设备与 UI(不阻塞初次进入)
|
||||||
|
Loom.QueueOnMainThread(() => RefreshAfterDeviceListUpdated());
|
||||||
},
|
},
|
||||||
onError: null
|
onError: null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoadingUI.Hide();
|
|
||||||
Debug.LogError($"[HomePageCtrl] 获取设备列表异常: {ex.Message}");
|
Debug.LogError($"[HomePageCtrl] 获取设备列表异常: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设备列表更新后:重新解析 selectedDevice 并刷新主页相关 UI(不重复 LoadingUI)
|
||||||
|
/// </summary>
|
||||||
|
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>().text = selectedDevice.device_name;
|
||||||
|
selectDeviceButton.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
DataManager.Instance.OwnedDevices = OwnedDevices;
|
||||||
|
DataManager.Instance.SharedDevices = SharedDevices;
|
||||||
|
}
|
||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
// 执行所有挂起的UI更新
|
// 执行所有挂起的UI更新
|
||||||
@ -1683,7 +1760,8 @@ namespace Kill.UI.Pages
|
|||||||
|
|
||||||
var response = await NetworkCtrl.Instance.Get<DeviceOnlineResponse>(
|
var response = await NetworkCtrl.Instance.Get<DeviceOnlineResponse>(
|
||||||
"/api/v1/device/online",
|
"/api/v1/device/online",
|
||||||
new Dictionary<string, string> { { "mac", selectedDevice.ble_mac } }
|
new Dictionary<string, string> { { "mac", selectedDevice.ble_mac } },
|
||||||
|
new HttpRequestConfig { Timeout = 5, MaxRetries = 1, RetryDelayMs = 500, UseExponentialBackoff = false }
|
||||||
);
|
);
|
||||||
|
|
||||||
ResponseCodeHandler.HandleResponse(response,
|
ResponseCodeHandler.HandleResponse(response,
|
||||||
@ -1697,12 +1775,78 @@ namespace Kill.UI.Pages
|
|||||||
{
|
{
|
||||||
Debug.LogError($"[HomePageCtrl] 查询设备在线状态失败: {msg}");
|
Debug.LogError($"[HomePageCtrl] 查询设备在线状态失败: {msg}");
|
||||||
tcs.TrySetResult(false);
|
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;
|
return await tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非阻塞刷新 WiFi 在线状态:失败立即标记未连接并刷新 UI;后台轮询会持续覆盖
|
||||||
|
/// 若从离线变为在线且蓝牙未连接,补做 WiFi 分支的设备状态初始化(拉取 config/schedule)
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WiFi-only 分支的设备状态初始化(拉取配置/定时任务),与 Init() 中对应分支保持一致
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 启动设备在线状态轮询(始终运行,后台检测WiFi状态变化)
|
/// 启动设备在线状态轮询(始终运行,后台检测WiFi状态变化)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1739,6 +1883,14 @@ namespace Kill.UI.Pages
|
|||||||
deviceState?.UpdateDeviceState(hasBluetooth, hasWifi);
|
deviceState?.UpdateDeviceState(hasBluetooth, hasWifi);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 服务器超时/网络异常:无论初始 wasOnline 状态,都强制刷新 UI 一次以确保图标同步
|
||||||
|
pendingUIUpdates.Add(() =>
|
||||||
|
{
|
||||||
|
deviceState?.UpdateDeviceState(hasBluetooth, hasWifi);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (online && !wasOnline && !hasBluetooth)
|
else if (online && !wasOnline && !hasBluetooth)
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user