@glassly/engine 0.1.0-dev.59 → 0.1.0-dev.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +5 -1
  2. package/build/facades/ota.d.ts +3 -1
  3. package/build/facades/ota.d.ts.map +1 -1
  4. package/build/facades/ota.js +4 -2
  5. package/build/facades/ota.js.map +1 -1
  6. package/build/generated/releaseMetadata.js +3 -3
  7. package/build/generated/releaseMetadata.js.map +1 -1
  8. package/build/index.d.ts +2 -2
  9. package/build/index.d.ts.map +1 -1
  10. package/build/index.js +2 -2
  11. package/build/index.js.map +1 -1
  12. package/build/react/MentraLiveOtaFlow.d.ts +6 -1
  13. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  14. package/build/react/MentraLiveOtaFlow.js +136 -14
  15. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  16. package/build/react/useMentraLiveOta.d.ts +5 -1
  17. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  18. package/build/react/useMentraLiveOta.js +135 -58
  19. package/build/react/useMentraLiveOta.js.map +1 -1
  20. package/build/services/DisplayProcessor.d.ts +3 -3
  21. package/build/services/DisplayProcessor.js +3 -3
  22. package/build/services/DisplayProcessor.js.map +1 -1
  23. package/build/services/LocalDisplayManager.d.ts +2 -2
  24. package/build/services/LocalDisplayManager.js +2 -2
  25. package/build/services/LocalDisplayManager.js.map +1 -1
  26. package/build/services/OtaInstallCoordinator.d.ts +2 -0
  27. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  28. package/build/services/OtaInstallCoordinator.js +21 -1
  29. package/build/services/OtaInstallCoordinator.js.map +1 -1
  30. package/build/services/PhonePhotoCoordinator.js +1 -1
  31. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  32. package/build/services/StreamLifecycleController.d.ts +4 -0
  33. package/build/services/StreamLifecycleController.d.ts.map +1 -1
  34. package/build/services/StreamLifecycleController.js +4 -0
  35. package/build/services/StreamLifecycleController.js.map +1 -1
  36. package/build/services/otaInstallPolicy.d.ts +4 -0
  37. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  38. package/build/services/otaInstallPolicy.js +4 -0
  39. package/build/services/otaInstallPolicy.js.map +1 -1
  40. package/build/types/applet.d.ts +1 -1
  41. package/build/types/applet.js.map +1 -1
  42. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  43. package/build/types/capabilities/even-realities-g2.js +2 -2
  44. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  45. package/build/types/hardware.d.ts.map +1 -1
  46. package/build/types/hardware.js +1 -0
  47. package/build/types/hardware.js.map +1 -1
  48. package/build/types/index.d.ts +1 -1
  49. package/build/types/index.d.ts.map +1 -1
  50. package/build/types/index.js +1 -1
  51. package/build/types/index.js.map +1 -1
  52. package/build/utils/display/composer/ColumnComposer.d.ts +1 -1
  53. package/build/utils/display/composer/ColumnComposer.js +1 -1
  54. package/build/utils/display/composer/ColumnComposer.js.map +1 -1
  55. package/package.json +8 -7
  56. package/src/facades/ota.ts +4 -2
  57. package/src/generated/releaseMetadata.ts +3 -3
  58. package/src/index.ts +2 -1
  59. package/src/react/MentraLiveOtaFlow.tsx +226 -41
  60. package/src/react/useMentraLiveOta.ts +146 -60
  61. package/src/services/DisplayProcessor.ts +3 -3
  62. package/src/services/LocalDisplayManager.ts +2 -2
  63. package/src/services/OtaInstallCoordinator.ts +21 -0
  64. package/src/services/PhonePhotoCoordinator.ts +1 -1
  65. package/src/services/StreamLifecycleController.ts +4 -0
  66. package/src/services/otaInstallPolicy.ts +4 -0
  67. package/src/types/applet.ts +1 -1
  68. package/src/types/capabilities/even-realities-g2.ts +3 -2
  69. package/src/types/hardware.ts +1 -0
  70. package/src/types/index.ts +1 -1
  71. package/src/utils/display/composer/ColumnComposer.ts +1 -1
@@ -28,7 +28,9 @@ export type MentraLiveOtaFlowPage = "check" | "progress"
28
28
  export type MentraLiveOtaScreen =
29
29
  | "initializing"
30
30
  | "checking"
31
+ | "finishing"
31
32
  | "update_available"
33
+ | "battery_required"
32
34
  | "wifi_required"
33
35
  | "up_to_date"
34
36
  | "dev_build"
@@ -69,6 +71,7 @@ export type MentraLiveOtaReleaseTransition = {
69
71
  export type MentraLiveOtaState = {
70
72
  screen: MentraLiveOtaScreen
71
73
  connected: boolean
74
+ batteryLevel: number | null
72
75
  transport: MentraLiveOtaTransport | null
73
76
  updateRequired: boolean
74
77
  versionChange: boolean
@@ -94,6 +97,8 @@ export type MentraLiveOtaState = {
94
97
  canDiscard: boolean
95
98
  canOpenWifiSetup: boolean
96
99
  continueDisabled: boolean
100
+ /** True only after an approved update session reaches a final no-update check. */
101
+ completedUpdate: boolean
97
102
  /** Release labels for the offered or just-completed coordinated update. */
98
103
  releaseTransition: MentraLiveOtaReleaseTransition | null
99
104
  /** Release notes crossed by this update, newest first. Populated on completion. */
@@ -128,6 +133,7 @@ type CheckState = "checking" | "update_available" | "no_update" | "dev_build" |
128
133
 
129
134
  const AUTO_CHAIN_NETWORK_RETRY_DELAY_MS = 5000
130
135
  const AUTO_CHAIN_COMPLETE_DELAY_MS = 750
136
+ export const MINIMUM_OTA_BATTERY_LEVEL = 25
131
137
 
132
138
  function releaseRangeTargetVersion(result: OtaCheckCurrentGlassesResult): string | null {
133
139
  return result.releaseVersion ?? result.updateInfo?.versionName ?? result.latestVersionInfo?.versionName ?? null
@@ -163,10 +169,10 @@ function installProgress(snapshot: OtaInstallSnapshot): number | null {
163
169
  const isDownload = otaStatus?.phase === "download"
164
170
  const totalSteps = otaStatus?.totalSteps ?? 1
165
171
  const rawPercent = isDownload
166
- ? otaStatus?.stepPercent ?? 0
172
+ ? (otaStatus?.stepPercent ?? 0)
167
173
  : totalSteps >= 2
168
- ? otaStatus?.overallPercent ?? 0
169
- : otaStatus?.stepPercent ?? 0
174
+ ? (otaStatus?.overallPercent ?? 0)
175
+ : (otaStatus?.stepPercent ?? 0)
170
176
  return Math.min(Math.max(rawPercent, snapshot.mtkInstallStallSimulatedPercent ?? 0, 0), 100)
171
177
  }
172
178
 
@@ -215,7 +221,9 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
215
221
  const [completedReleaseTransition, setCompletedReleaseTransition] = useState<MentraLiveOtaReleaseTransition | null>(
216
222
  null,
217
223
  )
224
+ const [completedUpdate, setCompletedUpdate] = useState(false)
218
225
  const [completedChangelogs, setCompletedChangelogs] = useState<ReleaseChangelog[]>([])
226
+ const [batteryBlocked, setBatteryBlocked] = useState(false)
219
227
  const [checkGeneration, setCheckGeneration] = useState(0)
220
228
  const performCheckGenerationRef = useRef(0)
221
229
  const activeCheckKeyRef = useRef<string | null>(null)
@@ -249,6 +257,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
249
257
  const returnToCheck = useCallback(() => {
250
258
  installActionPendingRef.current = false
251
259
  autoChainAdvancedRef.current = false
260
+ setCheckState("checking")
252
261
  setPage("check")
253
262
  setCheckGeneration((generation) => generation + 1)
254
263
  }, [])
@@ -259,6 +268,28 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
259
268
  setPage("progress")
260
269
  }, [])
261
270
 
271
+ const continueApprovedChain = useCallback(
272
+ (result: OtaCheckCurrentGlassesResult): boolean => {
273
+ if (installActionPendingRef.current || !isOtaAutoChainActive() || !result.updateInfo) return false
274
+ const snapshot = ota.snapshot()
275
+ if (!snapshot.wifiStatusKnown || (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1)) return false
276
+
277
+ const admission = tryAdvanceOtaAutoChain(
278
+ otaAutoChainFingerprint(result),
279
+ result.updateInfo.isDowngrade === true,
280
+ releaseRangeTargetVersion(result),
281
+ result.releaseVersion,
282
+ )
283
+ if (!admission.advance) return false
284
+
285
+ checkCompletedRef.current = true
286
+ ota.installSession.prepare(result)
287
+ navigateToProgress()
288
+ return true
289
+ },
290
+ [navigateToProgress],
291
+ )
292
+
262
293
  useEffect(() => {
263
294
  if (!runtimeReady || page !== "check") return
264
295
  const MIN_DISPLAY_TIME_MS = 1100
@@ -279,18 +310,18 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
279
310
 
280
311
  const performCheck = async () => {
281
312
  if (checkCompletedRef.current) return
313
+ if (isOtaAutoChainActive() && !otaSnapshot.ready) {
314
+ const remainingMs = otaAutoChainReconnectWaitRemaining()
315
+ if (remainingMs === null) return
316
+ reconnectTimeout = setTimeout(() => {
317
+ if (cancelled || myGeneration !== performCheckGenerationRef.current || ota.snapshot().ready) return
318
+ stopOtaAutoChain()
319
+ checkCompletedRef.current = true
320
+ setCheckState("error")
321
+ }, remainingMs)
322
+ return
323
+ }
282
324
  if (!otaSnapshot.connected) {
283
- if (isOtaAutoChainActive()) {
284
- const remainingMs = otaAutoChainReconnectWaitRemaining()
285
- if (remainingMs === null) return
286
- reconnectTimeout = setTimeout(() => {
287
- if (cancelled || myGeneration !== performCheckGenerationRef.current || ota.snapshot().connected) return
288
- stopOtaAutoChain()
289
- checkCompletedRef.current = true
290
- setCheckState("error")
291
- }, remainingMs)
292
- return
293
- }
294
325
  if (checkStartedRef.current) {
295
326
  stopOtaAutoChain()
296
327
  checkCompletedRef.current = true
@@ -364,34 +395,21 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
364
395
  const fromVersion = currentReleaseVersion(ota.snapshot().appVersion)
365
396
  setOfferedReleaseTransition(result.releaseVersion ? {fromVersion, toVersion: result.releaseVersion} : null)
366
397
  }
367
- if (isOtaAutoChainActive()) {
368
- const snapshot = ota.snapshot()
369
- if (!snapshot.wifiStatusKnown) return
370
- if (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1) {
371
- stopOtaAutoChain()
372
- } else {
373
- const admission = tryAdvanceOtaAutoChain(
374
- fingerprint,
375
- result.updateInfo.isDowngrade === true,
376
- releaseRangeTargetVersion(result),
377
- result.releaseVersion,
378
- )
379
- if (admission.advance) {
380
- checkCompletedRef.current = true
381
- ota.installSession.prepare(result)
382
- navigateToProgress()
383
- return
384
- }
385
- }
398
+ if (isOtaAutoChainActive() && !ota.snapshot().wifiStatusKnown) {
399
+ checkCompletedRef.current = true
400
+ return
386
401
  }
402
+ if (continueApprovedChain(result)) return
387
403
  checkCompletedRef.current = true
388
404
  setCheckState("update_available")
389
405
  return
390
406
  }
391
- if (isOtaAutoChainActive()) {
407
+ const completedApprovedSession = isOtaAutoChainActive()
408
+ if (completedApprovedSession) {
392
409
  setCompletedChangelogs(releaseChangelogsForActiveChain())
393
410
  setCompletedReleaseTransition(releaseTransitionFromRange(otaAutoChainReleaseRange()))
394
411
  }
412
+ setCompletedUpdate(completedApprovedSession)
395
413
  stopOtaAutoChain()
396
414
  checkCompletedRef.current = true
397
415
  ota.clearUpdateAvailable()
@@ -412,7 +430,42 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
412
430
  cancelled = true
413
431
  if (reconnectTimeout) clearTimeout(reconnectTimeout)
414
432
  }
415
- }, [checkGeneration, navigateToProgress, otaSnapshot.connected, otaSnapshot.wifiStatusKnown, page, runtimeReady])
433
+ }, [
434
+ checkGeneration,
435
+ continueApprovedChain,
436
+ otaSnapshot.connected,
437
+ otaSnapshot.ready,
438
+ otaSnapshot.wifiStatusKnown,
439
+ page,
440
+ runtimeReady,
441
+ ])
442
+
443
+ useEffect(() => {
444
+ if (
445
+ !runtimeReady ||
446
+ page !== "check" ||
447
+ (checkState !== "checking" && checkState !== "update_available") ||
448
+ !otaSnapshot.wifiStatusKnown ||
449
+ !isOtaAutoChainActive()
450
+ ) {
451
+ return
452
+ }
453
+ const result = selectedCheckResultRef.current
454
+ if (!result?.updateAvailable || !result.updateInfo) return
455
+ if (!otaSnapshot.wifiConnected && otaSnapshot.hotspotOtaVersion !== 1) {
456
+ setCheckState("update_available")
457
+ return
458
+ }
459
+ continueApprovedChain(result)
460
+ }, [
461
+ checkState,
462
+ continueApprovedChain,
463
+ otaSnapshot.hotspotOtaVersion,
464
+ otaSnapshot.wifiConnected,
465
+ otaSnapshot.wifiStatusKnown,
466
+ page,
467
+ runtimeReady,
468
+ ])
416
469
 
417
470
  useEffect(() => {
418
471
  if (!runtimeReady || page !== "progress") return
@@ -457,8 +510,10 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
457
510
  }, [installSnapshot.connected, installSnapshot.displayState, page, returnToCheck])
458
511
 
459
512
  const check = useCallback(() => {
513
+ setBatteryBlocked(false)
460
514
  setOfferedReleaseTransition(null)
461
515
  setCompletedReleaseTransition(null)
516
+ setCompletedUpdate(false)
462
517
  setCompletedChangelogs([])
463
518
  setPage("check")
464
519
  setCheckState("checking")
@@ -474,6 +529,10 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
474
529
  return
475
530
  }
476
531
  const snapshot = ota.snapshot()
532
+ if (snapshot.batteryLevel !== null && snapshot.batteryLevel < MINIMUM_OTA_BATTERY_LEVEL) {
533
+ setBatteryBlocked(true)
534
+ return
535
+ }
477
536
  if (!snapshot.wifiStatusKnown) {
478
537
  check()
479
538
  return
@@ -483,6 +542,8 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
483
542
  return
484
543
  }
485
544
  ota.installSession.prepare(result)
545
+ setBatteryBlocked(false)
546
+ setCompletedUpdate(false)
486
547
  setCompletedChangelogs([])
487
548
  if (updateFingerprint) {
488
549
  const fromVersion = offeredReleaseTransition
@@ -497,6 +558,15 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
497
558
  navigateToProgress()
498
559
  }, [check, isVersionChange, navigateToProgress, offeredReleaseTransition, page, updateFingerprint])
499
560
 
561
+ useEffect(() => {
562
+ if (
563
+ batteryBlocked &&
564
+ (otaSnapshot.batteryLevel === null || otaSnapshot.batteryLevel >= MINIMUM_OTA_BATTERY_LEVEL)
565
+ ) {
566
+ setBatteryBlocked(false)
567
+ }
568
+ }, [batteryBlocked, otaSnapshot.batteryLevel])
569
+
500
570
  const retryInstall = useCallback(() => {
501
571
  if (page !== "progress") return
502
572
  onFirmwareRestartingChangeRef.current?.(false, true)
@@ -508,7 +578,11 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
508
578
  onFinishedRef.current?.()
509
579
  return
510
580
  }
511
- if (installSnapshot.displayState === "restarting") stopOtaAutoChain()
581
+ const restartStillInProgress =
582
+ installSnapshot.displayState === "restarting" ||
583
+ installSnapshot.versionChangePhase === "restarting" ||
584
+ installSnapshot.versionChangePhase === "verifying"
585
+ if (restartStillInProgress) return
512
586
  const requiresGlassesReboot = shouldRequireGlassesRebootForBesFailure(
513
587
  installSnapshot.otaStatus,
514
588
  installSnapshot.otaProgress,
@@ -534,10 +608,12 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
534
608
  const state = useMemo<MentraLiveOtaState>(() => {
535
609
  const hotspotSupported = otaSnapshot.hotspotOtaVersion === 1
536
610
  const canInstall = otaSnapshot.wifiStatusKnown && (otaSnapshot.wifiConnected || hotspotSupported)
611
+ const autoChainActive = isOtaAutoChainActive()
537
612
  if (!runtimeReady) {
538
613
  return {
539
614
  screen: "initializing",
540
615
  connected: otaSnapshot.connected,
616
+ batteryLevel: otaSnapshot.batteryLevel,
541
617
  transport: null,
542
618
  updateRequired: isUpdateRequired,
543
619
  versionChange: isVersionChange,
@@ -563,6 +639,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
563
639
  canDiscard: false,
564
640
  canOpenWifiSetup: false,
565
641
  continueDisabled: false,
642
+ completedUpdate: false,
566
643
  releaseTransition: null,
567
644
  changelogs: [],
568
645
  }
@@ -570,29 +647,27 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
570
647
 
571
648
  if (page === "check") {
572
649
  const wifiRequired = otaSnapshot.wifiStatusKnown && !otaSnapshot.wifiConnected && !hotspotSupported
573
- const screen: MentraLiveOtaScreen =
574
- checkState === "checking"
575
- ? "checking"
576
- : checkState === "update_available"
577
- ? wifiRequired
578
- ? "wifi_required"
579
- : "update_available"
580
- : checkState === "no_update"
581
- ? "up_to_date"
582
- : checkState === "dev_build"
583
- ? "dev_build"
584
- : errorKind === "pin_unavailable"
585
- ? "update_info_unavailable"
586
- : "check_failed"
587
- const error: MentraLiveOtaError | null =
588
- screen === "check_failed"
589
- ? {code: "check_failed", message: "Couldn't check for updates. Please check your connection and try again."}
590
- : screen === "update_info_unavailable"
591
- ? {code: "update_info_unavailable", message: "Update information for this app version is unavailable."}
592
- : null
650
+ let screen: MentraLiveOtaScreen
651
+ if (checkState === "checking") screen = autoChainActive ? "finishing" : "checking"
652
+ else if (checkState === "update_available") {
653
+ screen = wifiRequired ? "wifi_required" : batteryBlocked ? "battery_required" : "update_available"
654
+ } else if (checkState === "no_update") screen = "up_to_date"
655
+ else if (checkState === "dev_build") screen = "dev_build"
656
+ else screen = errorKind === "pin_unavailable" ? "update_info_unavailable" : "check_failed"
657
+
658
+ let error: MentraLiveOtaError | null = null
659
+ if (screen === "check_failed") {
660
+ error = {
661
+ code: "check_failed",
662
+ message: "Couldn't check for updates. Please check your connection and try again.",
663
+ }
664
+ } else if (screen === "update_info_unavailable") {
665
+ error = {code: "update_info_unavailable", message: "Update information for this app version is unavailable."}
666
+ }
593
667
  return {
594
668
  screen,
595
669
  connected: otaSnapshot.connected,
670
+ batteryLevel: otaSnapshot.batteryLevel,
596
671
  transport: otaSnapshot.wifiConnected ? "wifi" : hotspotSupported ? "hotspot" : null,
597
672
  updateRequired: isUpdateRequired,
598
673
  versionChange: isVersionChange,
@@ -614,13 +689,19 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
614
689
  canInstall: screen === "update_available" && canInstall,
615
690
  canRetry: screen === "check_failed",
616
691
  canFinish: screen === "up_to_date" || screen === "dev_build" || screen === "update_info_unavailable",
617
- canDismiss: (screen === "update_available" || screen === "wifi_required") && !isUpdateRequired,
692
+ canDismiss:
693
+ (screen === "update_available" || screen === "wifi_required" || screen === "battery_required") &&
694
+ !isUpdateRequired &&
695
+ !autoChainActive,
618
696
  canDiscard: false,
619
697
  canOpenWifiSetup: screen === "wifi_required",
620
698
  continueDisabled: false,
699
+ completedUpdate: screen === "up_to_date" && completedUpdate,
621
700
  releaseTransition:
622
701
  screen === "update_available" || screen === "wifi_required"
623
- ? offeredReleaseTransition
702
+ ? autoChainActive
703
+ ? releaseTransitionFromRange(otaAutoChainReleaseRange())
704
+ : offeredReleaseTransition
624
705
  : screen === "up_to_date"
625
706
  ? completedReleaseTransition
626
707
  : null,
@@ -641,7 +722,8 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
641
722
  const displayedError = requiresGlassesReboot
642
723
  ? BES_INSTALL_RESTART_MESSAGE
643
724
  : installSnapshot.errorMsg || getOtaErrorMessage(installSnapshot.otaStatus?.error)
644
- const screen = progressScreen(installSnapshot)
725
+ const progressState = progressScreen(installSnapshot)
726
+ const screen = progressState === "complete" && autoChainActive ? "finishing" : progressState
645
727
  const error =
646
728
  screen === "failed"
647
729
  ? {
@@ -654,6 +736,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
654
736
  return {
655
737
  screen,
656
738
  connected: installSnapshot.connected,
739
+ batteryLevel: otaSnapshot.batteryLevel,
657
740
  transport: installSnapshot.transport,
658
741
  updateRequired: isUpdateRequired,
659
742
  versionChange: installSnapshot.isVersionChange,
@@ -677,18 +760,21 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
677
760
  error,
678
761
  canInstall: false,
679
762
  canRetry: screen === "failed" && !requiresGlassesReboot,
680
- canFinish: screen === "complete" || screen === "restarting" || (screen === "failed" && requiresGlassesReboot),
763
+ canFinish: screen === "complete" || (screen === "failed" && requiresGlassesReboot),
681
764
  canDismiss: false,
682
765
  canDiscard: screen === "disconnected",
683
766
  canOpenWifiSetup: screen === "failed" && showChangeWifi,
684
767
  continueDisabled: installSnapshot.continueButtonDisabled,
768
+ completedUpdate: false,
685
769
  releaseTransition: releaseTransitionFromRange(otaAutoChainReleaseRange()),
686
770
  changelogs,
687
771
  }
688
772
  }, [
689
773
  checkState,
774
+ batteryBlocked,
690
775
  completedChangelogs,
691
776
  completedReleaseTransition,
777
+ completedUpdate,
692
778
  errorKind,
693
779
  firmwareRestarting,
694
780
  installSnapshot,
@@ -2,15 +2,15 @@
2
2
  * DisplayProcessor
3
3
  *
4
4
  * Processes display events with pixel-accurate text wrapping using the same
5
- * display-utils library as the cloud. This ensures the mobile preview matches
6
- * exactly what is shown on the glasses.
5
+ * Engine display utilities. This ensures the mobile preview matches exactly
6
+ * what is shown on the glasses.
7
7
  *
8
8
  * Key responsibilities:
9
9
  * 1. Intercept display events before sending to native SGC
10
10
  * 2. Wrap text using the correct device profile (G1, etc.)
11
11
  * 3. Ensure GlassesDisplayMirror shows exactly what glasses will show
12
12
  *
13
- * @see cloud/issues/026-mobile-display-processor for design docs
13
+ * @see notes/miniapp-display-api-redesign.md for the current display design
14
14
  */
15
15
 
16
16
  import {
@@ -2,8 +2,8 @@
2
2
  * LocalDisplayManager
3
3
  *
4
4
  * Phone-side arbiter for display requests originating from LOCAL miniapps
5
- * (the @glassly/miniapp SDK). Mirrors the user-facing behavior of the cloud
6
- * DisplayManager for the features that matter on the phone:
5
+ * (the @glassly/miniapp SDK). Preserves the retired cloud display manager's
6
+ * user-facing behavior for the features that matter on the phone:
7
7
  *
8
8
  * - boot message ("Starting <AppName>…") with a bounded window
9
9
  * - durationMs auto-clear
@@ -26,10 +26,12 @@ import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
26
26
  import {deriveDisplayState, type DisplayState} from "./otaDisplayState"
27
27
  import {
28
28
  BES_CONTINUE_LOCKOUT_MS,
29
+ BES_RESTART_TIMEOUT_MS,
29
30
  DOWNLOAD_STUCK_TIMEOUT_MS,
30
31
  GLOBAL_OTA_TIMEOUT_MS,
31
32
  LEGACY_APK_COMPLETION_SETTLE_MS,
32
33
  LEGACY_BES_CONTINUE_LOCKOUT_MS,
34
+ LEGACY_BES_RESTART_TIMEOUT_MS,
33
35
  LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS,
34
36
  LEGACY_GLOBAL_OTA_TIMEOUT_MS,
35
37
  LEGACY_MTK_INSTALL_TIMEOUT_MS,
@@ -245,6 +247,7 @@ class OtaInstallCoordinator {
245
247
  private pingInterval: ReturnType<typeof setInterval> | null = null
246
248
  private queryReplyTimeout: ReturnType<typeof setTimeout> | null = null
247
249
  private continueLockoutTimer: ReturnType<typeof setTimeout> | null = null
250
+ private restartRecoveryTimeout: ReturnType<typeof setTimeout> | null = null
248
251
  private legacyApkSettleTimer: ReturnType<typeof setTimeout> | null = null
249
252
  private mtkStallDetectTimer: ReturnType<typeof setTimeout> | null = null
250
253
  private mtkSimTickTimer: ReturnType<typeof setInterval> | null = null
@@ -1000,12 +1003,22 @@ class OtaInstallCoordinator {
1000
1003
  // (15s unified, 35s for legacy-shaped sessions — WP 8C-f).
1001
1004
  if (displayStateChanged && displayState === "restarting") {
1002
1005
  this.clearContinueLockoutTimer()
1006
+ this.clearRestartRecoveryTimeout()
1003
1007
  const lockoutMs = legacySession ? LEGACY_BES_CONTINUE_LOCKOUT_MS : BES_CONTINUE_LOCKOUT_MS
1008
+ const restartTimeoutMs = legacySession ? LEGACY_BES_RESTART_TIMEOUT_MS : BES_RESTART_TIMEOUT_MS
1004
1009
  this.setContinueButtonDisabled(true)
1005
1010
  this.continueLockoutTimer = setTimeout(() => {
1006
1011
  this.continueLockoutTimer = null
1007
1012
  this.setContinueButtonDisabled(false)
1008
1013
  }, lockoutMs)
1014
+ this.restartRecoveryTimeout = setTimeout(() => {
1015
+ this.restartRecoveryTimeout = null
1016
+ if (this.computeDisplayStateNow() !== "restarting") return
1017
+ console.log(`[OTA_PROGRESS] watchdog: glasses did not finish restarting in ${restartTimeoutMs}ms`)
1018
+ this.setErrorMsg(OtaProgressMessages.restartTimeout)
1019
+ }, restartTimeoutMs)
1020
+ } else if (displayStateChanged) {
1021
+ this.clearRestartRecoveryTimeout()
1009
1022
  }
1010
1023
 
1011
1024
  // Legacy MTK install stall simulation (WP 8C-e): display-only. The simulation
@@ -1668,6 +1681,13 @@ class OtaInstallCoordinator {
1668
1681
  }
1669
1682
  }
1670
1683
 
1684
+ private clearRestartRecoveryTimeout(): void {
1685
+ if (this.restartRecoveryTimeout) {
1686
+ clearTimeout(this.restartRecoveryTimeout)
1687
+ this.restartRecoveryTimeout = null
1688
+ }
1689
+ }
1690
+
1671
1691
  private clearLegacyApkSettleTimer(): void {
1672
1692
  if (this.legacyApkSettleTimer) {
1673
1693
  clearTimeout(this.legacyApkSettleTimer)
@@ -1706,6 +1726,7 @@ class OtaInstallCoordinator {
1706
1726
  this.clearPingInterval()
1707
1727
  this.clearLegacyApkSettleTimer()
1708
1728
  this.clearMtkSimulationTimers()
1729
+ this.clearRestartRecoveryTimeout()
1709
1730
  }
1710
1731
  }
1711
1732
 
@@ -166,7 +166,7 @@ export class PhonePhotoCoordinator {
166
166
  // to time out. Fail fast with a typed error.
167
167
  //
168
168
  // We DON'T pre-check `hasCamera` here — the canonical capability data
169
- // lives in `getModelCapabilities(deviceModel)` in the engine types and
169
+ // lives in `getModelCapabilities(deviceModel)` from @glassly/engine and
170
170
  // pulling that into this file would add a cross-package import. If a
171
171
  // cameraless device receives the BLE photo command, the glasses-side
172
172
  // handler will return a photo_response error within ~1s and the gated
@@ -2,6 +2,10 @@
2
2
  * Drives RTMP stream liveliness checks via a keep-alive heartbeat:
3
3
  * `setActive(true)` starts the timer; each tick sends a keep-alive with a
4
4
  * fresh ackId; `maxMissedAcks` consecutive timeouts fire `onTimeout`.
5
+ *
6
+ * Phone-owned successor to the retired cloud stream lifecycle controller.
7
+ * It uses the local `LifecycleLogger` interface because pino is not a phone
8
+ * dependency.
5
9
  */
6
10
 
7
11
  export interface LifecycleLogger {
@@ -32,6 +32,8 @@ export const PING_INTERVAL_MS = 10_000
32
32
  export const QUERY_REPLY_TIMEOUT_MS = 6000
33
33
  /** 15s lockout on the Continue button after a BES restart, to prevent an accidental tap. */
34
34
  export const BES_CONTINUE_LOCKOUT_MS = 15_000
35
+ /** Fail closed if a BES reboot never reconnects; normal restarts complete in a fraction of this window. */
36
+ export const BES_RESTART_TIMEOUT_MS = 120_000
35
37
 
36
38
  // --- Legacy old-build (< MINIMUM_OTA_STATUS_BUILD) OTA policy (WP 8C) ---
37
39
  //
@@ -51,6 +53,7 @@ export const LEGACY_MTK_INSTALL_TIMEOUT_MS = MTK_INSTALL_TIMEOUT_MS + LEGACY_EXT
51
53
  export const LEGACY_GLOBAL_OTA_TIMEOUT_MS = GLOBAL_OTA_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS
52
54
  export const LEGACY_PING_INTERVAL_MS = PING_INTERVAL_MS + LEGACY_EXTRA_TIMEOUT_MS
53
55
  export const LEGACY_BES_CONTINUE_LOCKOUT_MS = BES_CONTINUE_LOCKOUT_MS + LEGACY_EXTRA_TIMEOUT_MS
56
+ export const LEGACY_BES_RESTART_TIMEOUT_MS = BES_RESTART_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS
54
57
  /**
55
58
  * After a legacy apk `install FINISHED` observed in-flight, the legacy screen held the
56
59
  * completed state for 12s (+20s pad) so the ASG process restart settles (and fresh
@@ -122,6 +125,7 @@ export function selectOtaProtocolProfile(
122
125
  export const OtaProgressMessages = {
123
126
  stalledOrStuck: "Update may have failed. Ensure glasses have internet access and try again.",
124
127
  globalTimeout: "Update took too long. Please try again.",
128
+ restartTimeout: "Glasses did not finish restarting. Restart them before trying the update again.",
125
129
  sendOtaStartFailed: "Failed to communicate with glasses.",
126
130
  hotspotArtifactDownloadFailed: "Could not download the update to your phone. Check phone internet and try again.",
127
131
  hotspotArtifactVerifyFailed: "The update package could not be verified. Please try again later.",
@@ -1,6 +1,6 @@
1
1
  import type {ReactNode} from "react"
2
2
  /**
3
- * App/Applet types for client interfaces
3
+ * App and miniapp types owned by @glassly/engine.
4
4
  */
5
5
 
6
6
 
@@ -85,6 +85,9 @@ export const evenRealitiesG2: Capabilities = {
85
85
  // WiFi capabilities - G2 does not support WiFi
86
86
  hasWifi: false,
87
87
 
88
+ // OTA capabilities - G2 firmware updates are not driven by the ASG OTA flow
89
+ hasOta: false,
90
+
88
91
  // Dashboard - G2 renders Even Realities' native dashboard in firmware, so
89
92
  // Glassly does not manage the dashboard or expose dashboard settings for it
90
93
  hasNativeDashboard: true,
@@ -93,6 +96,4 @@ export const evenRealitiesG2: Capabilities = {
93
96
  // 576×188 overview map); magnetometer streams only while it is active
94
97
  hasNativeNavigation: true,
95
98
 
96
- // OTA capabilities - G2 firmware updates are not driven by the ASG OTA flow
97
- hasOta: false,
98
99
  }
@@ -189,6 +189,7 @@ export const HARDWARE_CAPABILITIES: Record<string, Capabilities> = {
189
189
  [mentraLive.modelName]: mentraLive,
190
190
  [simulatedGlasses.modelName]: simulatedGlasses,
191
191
  [vuzixZ100.modelName]: vuzixZ100,
192
+ [nimo.modelName]: nimo,
192
193
  [DeviceTypes.MACH1]: vuzixZ100, // Mach1 uses same Vuzix Ultralite hardware as Z100
193
194
  [nimo.modelName]: nimo,
194
195
  [DeviceTypes.AR99]: {
@@ -25,9 +25,9 @@ export {
25
25
  evenRealitiesG1,
26
26
  evenRealitiesG2,
27
27
  mentraLive,
28
+ nimo,
28
29
  vuzixZ100,
29
30
  mentraDisplay,
30
- nimo,
31
31
  } from "./hardware"
32
32
 
33
33
  // Applet types
@@ -11,7 +11,7 @@
11
11
  * 2. Merges columns line-by-line with pixel-precise space padding
12
12
  * 3. Returns a single pre-composed string ready for native to chunk & send
13
13
  *
14
- * @see cloud/issues/026-mobile-display-processor for design docs
14
+ * @see notes/miniapp-display-api-redesign.md for the current display design
15
15
  */
16
16
 
17
17
  import type {DisplayProfile} from "../profiles/types"