@glassly/engine 0.3.7 → 0.3.8-beta.1013
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.
- package/README.md +4 -0
- package/build/engine.d.ts +18 -2
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -4
- package/build/engine.js.map +1 -1
- package/build/facades/ai.d.ts +3 -0
- package/build/facades/ai.d.ts.map +1 -1
- package/build/facades/ai.js +3 -0
- package/build/facades/ai.js.map +1 -1
- package/build/facades/backup.d.ts +14 -0
- package/build/facades/backup.d.ts.map +1 -0
- package/build/facades/backup.js +22 -0
- package/build/facades/backup.js.map +1 -0
- package/build/facades/glasses.d.ts +13 -5
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +17 -4
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/services/AppRegistry.d.ts +2 -1
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +38 -8
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AppStateBackup.d.ts +98 -0
- package/build/services/AppStateBackup.d.ts.map +1 -0
- package/build/services/AppStateBackup.js +224 -0
- package/build/services/AppStateBackup.js.map +1 -0
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +5 -12
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CloudClientService.d.ts +3 -1
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +42 -2
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +1 -0
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/DisplayDiagnostics.d.ts.map +1 -1
- package/build/services/DisplayDiagnostics.js +4 -2
- package/build/services/DisplayDiagnostics.js.map +1 -1
- package/build/services/DisplayProcessor.d.ts +2 -0
- package/build/services/DisplayProcessor.d.ts.map +1 -1
- package/build/services/DisplayProcessor.js +15 -1
- package/build/services/DisplayProcessor.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +4 -2
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/JSCrashloopReportService.d.ts.map +1 -1
- package/build/services/JSCrashloopReportService.js +5 -0
- package/build/services/JSCrashloopReportService.js.map +1 -1
- package/build/services/JSRouter.d.ts +11 -3
- package/build/services/JSRouter.d.ts.map +1 -1
- package/build/services/JSRouter.js +36 -6
- package/build/services/JSRouter.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +5 -2
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +13 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +61 -22
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.js +2 -0
- package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
- package/build/services/LocalTranslationService.d.ts +2 -0
- package/build/services/LocalTranslationService.d.ts.map +1 -1
- package/build/services/LocalTranslationService.js +22 -0
- package/build/services/LocalTranslationService.js.map +1 -1
- package/build/services/MiniappEngine.d.ts +9 -0
- package/build/services/MiniappEngine.d.ts.map +1 -1
- package/build/services/MiniappEngine.js +60 -27
- package/build/services/MiniappEngine.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts +2 -0
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +6 -0
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/NavigationService.d.ts +1 -0
- package/build/services/NavigationService.d.ts.map +1 -1
- package/build/services/NavigationService.js +20 -6
- package/build/services/NavigationService.js.map +1 -1
- package/build/services/OfflineActivity.d.ts +54 -0
- package/build/services/OfflineActivity.d.ts.map +1 -0
- package/build/services/OfflineActivity.js +86 -0
- package/build/services/OfflineActivity.js.map +1 -0
- package/build/services/PhoneCalendarService.d.ts.map +1 -1
- package/build/services/PhoneCalendarService.js +8 -0
- package/build/services/PhoneCalendarService.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +7 -3
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/STTModelManager.d.ts +1 -0
- package/build/services/STTModelManager.d.ts.map +1 -1
- package/build/services/STTModelManager.js +6 -0
- package/build/services/STTModelManager.js.map +1 -1
- package/build/services/SceneRenderer.js +1 -1
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/appStateBackupFile.d.ts +32 -0
- package/build/services/appStateBackupFile.d.ts.map +1 -0
- package/build/services/appStateBackupFile.js +135 -0
- package/build/services/appStateBackupFile.js.map +1 -0
- package/build/services/controllers.d.ts.map +1 -1
- package/build/services/controllers.js +13 -3
- package/build/services/controllers.js.map +1 -1
- package/build/services/effectiveCapabilities.js +6 -1
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/build/services/neuralBand/NeuralBandInput.d.ts +12 -1
- package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -1
- package/build/services/neuralBand/NeuralBandInput.js +27 -0
- package/build/services/neuralBand/NeuralBandInput.js.map +1 -1
- package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -1
- package/build/services/neuralBand/NeuralBandSettingsSync.js +6 -0
- package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -1
- package/build/services/pages/pageEligibility.d.ts.map +1 -1
- package/build/services/pages/pageEligibility.js +8 -1
- package/build/services/pages/pageEligibility.js.map +1 -1
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +1 -0
- package/build/services/settingsBridge.js.map +1 -1
- package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
- package/build/services/watch/WatchLink.d.ts.map +1 -0
- package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
- package/build/services/watch/WatchLink.js.map +1 -0
- package/build/stores/appVisibility.d.ts.map +1 -1
- package/build/stores/appVisibility.js +0 -1
- package/build/stores/appVisibility.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +22 -10
- package/build/stores/apps.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +71 -6
- package/build/stores/settings.js.map +1 -1
- package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g1.js +3 -0
- package/build/types/capabilities/even-realities-g1.js.map +1 -1
- package/build/types/capabilities/memomind-one.d.ts +3 -0
- package/build/types/capabilities/memomind-one.d.ts.map +1 -0
- package/build/types/capabilities/memomind-one.js +42 -0
- package/build/types/capabilities/memomind-one.js.map +1 -0
- package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
- package/build/types/capabilities/rayneo-io.js +3 -2
- package/build/types/capabilities/rayneo-io.js.map +1 -1
- package/build/types/enums.d.ts +8 -2
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +6 -0
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +3 -1
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +3 -1
- package/build/types/hardware.js.map +1 -1
- package/build/utils/display/index.d.ts +2 -1
- package/build/utils/display/index.d.ts.map +1 -1
- package/build/utils/display/index.js +3 -2
- package/build/utils/display/index.js.map +1 -1
- package/build/utils/display/profiles/index.d.ts +2 -1
- package/build/utils/display/profiles/index.d.ts.map +1 -1
- package/build/utils/display/profiles/index.js +3 -2
- package/build/utils/display/profiles/index.js.map +1 -1
- package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
- package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
- package/build/utils/display/profiles/rayneo-io.js +1 -1
- package/build/utils/display/profiles/rayneo-io.js.map +1 -1
- package/build/utils/display/scene/index.d.ts +1 -1
- package/build/utils/display/scene/index.d.ts.map +1 -1
- package/build/utils/display/scene/index.js +1 -1
- package/build/utils/display/scene/index.js.map +1 -1
- package/build/utils/display/scene/process.d.ts +6 -0
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +22 -7
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/miniappGlobals.d.ts +13 -17
- package/build/utils/miniappGlobals.d.ts.map +1 -1
- package/build/utils/miniappGlobals.js +24 -56
- package/build/utils/miniappGlobals.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +7 -4
- package/src/facades/ai.ts +4 -0
- package/src/facades/backup.ts +28 -0
- package/src/facades/glasses.ts +22 -5
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +10 -0
- package/src/services/AppRegistry.ts +36 -9
- package/src/services/AppStateBackup.ts +283 -0
- package/src/services/BlobStore.ts +5 -11
- package/src/services/CloudClientService.ts +43 -4
- package/src/services/DeviceEventRouter.ts +1 -0
- package/src/services/DisplayDiagnostics.ts +4 -2
- package/src/services/DisplayProcessor.ts +20 -8
- package/src/services/GlassesTouchInput.ts +6 -2
- package/src/services/JSCrashloopReportService.ts +5 -0
- package/src/services/JSRouter.ts +37 -6
- package/src/services/LocalDisplayManager.ts +5 -2
- package/src/services/LocalMiniappRuntime.ts +61 -22
- package/src/services/LocalSttFallbackCoordinator.ts +4 -0
- package/src/services/LocalTranslationService.ts +20 -0
- package/src/services/MiniappEngine.ts +62 -27
- package/src/services/MiniappLauncher.ts +7 -0
- package/src/services/NavigationService.ts +28 -8
- package/src/services/OfflineActivity.ts +131 -0
- package/src/services/PhoneCalendarService.ts +7 -0
- package/src/services/PhoneNotificationsSync.ts +8 -3
- package/src/services/STTModelManager.ts +6 -0
- package/src/services/SceneRenderer.ts +1 -1
- package/src/services/appStateBackupFile.ts +171 -0
- package/src/services/controllers.ts +14 -3
- package/src/services/effectiveCapabilities.ts +6 -1
- package/src/services/neuralBand/NeuralBandInput.ts +37 -1
- package/src/services/neuralBand/NeuralBandSettingsSync.ts +12 -0
- package/src/services/pages/pageEligibility.ts +9 -1
- package/src/services/settingsBridge.ts +1 -0
- package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
- package/src/stores/appVisibility.ts +0 -1
- package/src/stores/apps.ts +22 -11
- package/src/stores/settings.ts +78 -10
- package/src/types/capabilities/even-realities-g1.ts +4 -0
- package/src/types/capabilities/memomind-one.ts +43 -0
- package/src/types/capabilities/rayneo-io.ts +3 -2
- package/src/types/enums.ts +6 -0
- package/src/types/hardware.ts +14 -1
- package/src/utils/display/index.ts +4 -2
- package/src/utils/display/profiles/index.ts +9 -2
- package/src/utils/display/profiles/rayneo-io.ts +6 -1
- package/src/utils/display/scene/index.ts +1 -1
- package/src/utils/display/scene/process.ts +22 -7
- package/src/utils/miniappGlobals.ts +32 -64
- package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
- package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -64,11 +64,20 @@ export type {
|
|
|
64
64
|
LocalLlmModelId,
|
|
65
65
|
} from "./services/LocalLlmModelManager"
|
|
66
66
|
export {DEFAULT_LOCAL_LLM_MODEL, isLocalLlmModelId} from "./services/LocalLlmModelManager"
|
|
67
|
+
export type {
|
|
68
|
+
OfflineActivity,
|
|
69
|
+
OfflineActivityReason,
|
|
70
|
+
OfflineLlmActivity,
|
|
71
|
+
OfflineTranscriptionActivity,
|
|
72
|
+
OfflineTranslationActivity,
|
|
73
|
+
OfflineTranslationPair,
|
|
74
|
+
} from "./services/OfflineActivity"
|
|
67
75
|
|
|
68
76
|
// Shared user memory timeline (engine.memory / Settings > Memory). Entry and
|
|
69
77
|
// session shapes are canonical in @glassly/miniapp (the session.memory
|
|
70
78
|
// surface); the stats read model is engine-side.
|
|
71
79
|
export type {MemoryStats} from "./services/MemoryStore"
|
|
80
|
+
export type {AppStateExportResult, AppStateRestoreOptions, AppStateRestoreResult} from "./services/appStateBackupFile"
|
|
72
81
|
export type {SpeakerProfile} from "./services/SpeakerProfileStore"
|
|
73
82
|
export type {EnrollOutcome} from "./facades/speakers"
|
|
74
83
|
export {MENTRA_LIVE_SETTING_KEYS, getBluetoothSettingKeysForDevice} from "./stores/bluetoothSettingKeys"
|
|
@@ -230,6 +239,7 @@ export {
|
|
|
230
239
|
} from "./services/neuralBand/MetaAuth"
|
|
231
240
|
export type {
|
|
232
241
|
NeuralBandClaimStatus,
|
|
242
|
+
NeuralBandFirmwareStatus,
|
|
233
243
|
NeuralBandHandwritingStatus,
|
|
234
244
|
NeuralBandHandwritingUpdate,
|
|
235
245
|
} from "./services/neuralBand/NeuralBandInput"
|
|
@@ -658,7 +658,8 @@ class AppRegistry {
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
-
|
|
661
|
+
/** Undefined when no bundle is on disk (offline built-ins, HTTP-direct dev apps). */
|
|
662
|
+
public async getActiveVersion(packageName: string): Promise<string | undefined> {
|
|
662
663
|
let versions = this.getInstalledVersions(packageName)
|
|
663
664
|
// Treat MMKV as a hint, not authority. A stored version may have been
|
|
664
665
|
// GC'd off disk without this pointer being updated.
|
|
@@ -677,8 +678,10 @@ class AppRegistry {
|
|
|
677
678
|
}
|
|
678
679
|
versions = versions.filter((v) => semver.valid(v))
|
|
679
680
|
versions.sort((a, b) => semver.rcompare(a, b))
|
|
680
|
-
|
|
681
|
-
|
|
681
|
+
const latest = versions[0] as string | undefined
|
|
682
|
+
// Never persist undefined: MMKV.set throws on it.
|
|
683
|
+
if (latest) this.setActiveVersion(packageName, latest)
|
|
684
|
+
return latest
|
|
682
685
|
}
|
|
683
686
|
|
|
684
687
|
public setActiveVersion(packageName: string, version: string): Result<void, Error> {
|
|
@@ -727,7 +730,13 @@ class AppRegistry {
|
|
|
727
730
|
const installed = diskApps.filter(
|
|
728
731
|
(app) => !offlinePackages.has(app.packageName) && !devPackages.has(app.packageName),
|
|
729
732
|
)
|
|
730
|
-
|
|
733
|
+
// The last switcher-card capture comes back from disk, so a relaunched
|
|
734
|
+
// app's card is not blank until it is minimized again.
|
|
735
|
+
const withScreenshot = (app: ClientApp): ClientApp => ({
|
|
736
|
+
...app,
|
|
737
|
+
screenshot: app.screenshot ?? getLocalAppScreenshot(app.packageName),
|
|
738
|
+
})
|
|
739
|
+
return [...installed.map(withScreenshot), ...dev.map(withScreenshot), ...offline]
|
|
731
740
|
}
|
|
732
741
|
|
|
733
742
|
public async getInstalledMiniapps(): Promise<ClientApp[]> {
|
|
@@ -747,6 +756,8 @@ class AppRegistry {
|
|
|
747
756
|
const out: ClientApp[] = []
|
|
748
757
|
for (const lmaInfo of installedInfo) {
|
|
749
758
|
const versionString = await this.getActiveVersion(lmaInfo.packageName)
|
|
759
|
+
// A package dir with no usable bundle (e.g. only a half-written version) has nothing to run.
|
|
760
|
+
if (!versionString) continue
|
|
750
761
|
const versionInfo = lmaInfo.versions[versionString]
|
|
751
762
|
|
|
752
763
|
const manifest = this.getMiniappManifest(lmaInfo.packageName, versionString) as {
|
|
@@ -963,6 +974,19 @@ export function saveWidgetExpandChoice(packageName: string, widgetId: string, ex
|
|
|
963
974
|
storage.save(`${packageName}_widget_expand_${widgetId}`, expandOnTap)
|
|
964
975
|
}
|
|
965
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Where a screenshot captured under an earlier install now lives: iOS gives
|
|
979
|
+
* every install a new container, so a stored absolute file URI goes stale
|
|
980
|
+
* while the file itself is still in the documents directory.
|
|
981
|
+
*/
|
|
982
|
+
function rerootDocumentUri(uri: string): string | null {
|
|
983
|
+
const marker = "/Documents/"
|
|
984
|
+
const at = uri.indexOf(marker)
|
|
985
|
+
if (at < 0) return null
|
|
986
|
+
const rerooted = new File(Paths.document, uri.slice(at + marker.length)).uri
|
|
987
|
+
return rerooted === uri ? null : rerooted
|
|
988
|
+
}
|
|
989
|
+
|
|
966
990
|
export function getLocalAppScreenshot(packageName: string): string | undefined {
|
|
967
991
|
const key = `${packageName}_screenshot`
|
|
968
992
|
const res = storage.load<string>(key)
|
|
@@ -971,14 +995,17 @@ export function getLocalAppScreenshot(packageName: string): string | undefined {
|
|
|
971
995
|
const uri = res.value
|
|
972
996
|
if (uri.startsWith("file://")) {
|
|
973
997
|
try {
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
998
|
+
if (new File(uri).exists) return uri
|
|
999
|
+
const rerooted = rerootDocumentUri(uri)
|
|
1000
|
+
if (rerooted && new File(rerooted).exists) {
|
|
1001
|
+
storage.save(key, rerooted)
|
|
1002
|
+
return rerooted
|
|
977
1003
|
}
|
|
978
1004
|
} catch {
|
|
979
|
-
|
|
980
|
-
return undefined
|
|
1005
|
+
// fall through: the stored uri is unusable
|
|
981
1006
|
}
|
|
1007
|
+
storage.remove(key)
|
|
1008
|
+
return undefined
|
|
982
1009
|
}
|
|
983
1010
|
|
|
984
1011
|
return uri
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App state backups (Settings > Profile > Backup): one JSON file carrying the
|
|
3
|
+
* system settings, every miniapp's SimpleStorage, the installed miniapp list
|
|
4
|
+
* with each app's home flags, and the memory timeline.
|
|
5
|
+
*
|
|
6
|
+
* It exists so a logout or reinstall doesn't mean signing back into every
|
|
7
|
+
* miniapp, which is why the file holds live credentials (miniapp tokens,
|
|
8
|
+
* provider API keys) as plain JSON. Pairing identity, cloud endpoints, and
|
|
9
|
+
* consent/login state stay out: they belong to the install and the account,
|
|
10
|
+
* not to the user's preferences.
|
|
11
|
+
*
|
|
12
|
+
* Storage keys are written without their user segment, so a backup restores
|
|
13
|
+
* into whatever namespace the phone has now (account or `local_`). Restore
|
|
14
|
+
* overwrites key by key and leaves keys the backup doesn't carry alone.
|
|
15
|
+
*
|
|
16
|
+
* Pure: the MMKV view, settings registry, and memory snapshot are passed in by
|
|
17
|
+
* appStateBackupFile.ts.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {MemoryBackup} from "@glassly/miniapp"
|
|
21
|
+
|
|
22
|
+
/** Key-value surface (MMKV in production, values already JSON-decoded). */
|
|
23
|
+
export interface AppStateBackupBackend {
|
|
24
|
+
get(key: string): unknown | null
|
|
25
|
+
set(key: string, value: unknown): void
|
|
26
|
+
keys(): string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The fields of a SETTINGS descriptor that decide whether it is backed up. */
|
|
30
|
+
export interface BackupSettingDescriptor {
|
|
31
|
+
key: string
|
|
32
|
+
persist: boolean
|
|
33
|
+
writable: boolean
|
|
34
|
+
nativeAuthoritative?: true
|
|
35
|
+
resetOnBuildEnvChange?: true
|
|
36
|
+
indexer?: (key: string) => string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AppStateBackupMiniapp {
|
|
40
|
+
/** Set when the miniapp was installed at export. */
|
|
41
|
+
installed?: {version: string; source?: string}
|
|
42
|
+
/** SimpleStorage key → stored value. */
|
|
43
|
+
storage: Record<string, unknown>
|
|
44
|
+
/** Home flags by suffix: `hidden`, `always_on`, `widget_expand_<widgetId>`. */
|
|
45
|
+
prefs: Record<string, unknown>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AppStateBackup {
|
|
49
|
+
format: "glassly-app-state"
|
|
50
|
+
version: 1
|
|
51
|
+
/** Epoch ms the backup was written. */
|
|
52
|
+
exportedAt: number
|
|
53
|
+
/** Stored setting values; indexed settings keep their `key:index` storage key. */
|
|
54
|
+
settings: Record<string, unknown>
|
|
55
|
+
miniapps: Record<string, AppStateBackupMiniapp>
|
|
56
|
+
memory: MemoryBackup | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Thrown for a file that isn't an app state backup this build can read. */
|
|
60
|
+
export class AppStateBackupError extends Error {
|
|
61
|
+
constructor(message: string) {
|
|
62
|
+
super(message)
|
|
63
|
+
this.name = "AppStateBackupError"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const STORAGE_ROOT = "localstorage_"
|
|
68
|
+
const PREF_SUFFIXES = ["hidden", "always_on"] as const
|
|
69
|
+
const WIDGET_EXPAND_PREFIX = "widget_expand_"
|
|
70
|
+
// Same rule as the miniapp.json schema; keeps a hand-edited file from writing keys no app reads.
|
|
71
|
+
const PACKAGE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Persisted, writable settings that are still not the user's to carry between
|
|
75
|
+
* installs: account and consent state, the paired devices, and caches.
|
|
76
|
+
*/
|
|
77
|
+
const EXCLUDED_SETTINGS = new Set([
|
|
78
|
+
"auth_email",
|
|
79
|
+
"disable_cloud",
|
|
80
|
+
"ai_data_consent_version",
|
|
81
|
+
"ai_data_consent_agreed",
|
|
82
|
+
"login_skipped",
|
|
83
|
+
"has_paired_glasses",
|
|
84
|
+
"keyboard_name",
|
|
85
|
+
"cached_required_version",
|
|
86
|
+
"offline_camera_running",
|
|
87
|
+
"offline_translation_running",
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
export function isBackupSetting(setting: BackupSettingDescriptor): boolean {
|
|
91
|
+
return (
|
|
92
|
+
setting.persist &&
|
|
93
|
+
setting.writable &&
|
|
94
|
+
!setting.nativeAuthoritative &&
|
|
95
|
+
!setting.resetOnBuildEnvChange &&
|
|
96
|
+
!EXCLUDED_SETTINGS.has(setting.key)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CollectAppStateInput {
|
|
101
|
+
backend: AppStateBackupBackend
|
|
102
|
+
userId: string
|
|
103
|
+
settings: BackupSettingDescriptor[]
|
|
104
|
+
installed: Array<{packageName: string; version: string; source?: string}>
|
|
105
|
+
memory: MemoryBackup | null
|
|
106
|
+
now: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function collectAppStateBackup(input: CollectAppStateInput): AppStateBackup {
|
|
110
|
+
const {backend, settings} = input
|
|
111
|
+
const keys = backend.keys()
|
|
112
|
+
|
|
113
|
+
const settingValues: Record<string, unknown> = {}
|
|
114
|
+
for (const setting of settings) {
|
|
115
|
+
if (!isBackupSetting(setting)) continue
|
|
116
|
+
if (setting.indexer) {
|
|
117
|
+
for (const key of keys) {
|
|
118
|
+
if (!key.startsWith(`${setting.key}:`)) continue
|
|
119
|
+
const value = backend.get(key)
|
|
120
|
+
if (value != null) settingValues[key] = value
|
|
121
|
+
}
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
const value = backend.get(setting.key)
|
|
125
|
+
if (value != null) settingValues[setting.key] = value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const miniapps: Record<string, AppStateBackupMiniapp> = {}
|
|
129
|
+
const entry = (packageName: string) => (miniapps[packageName] ??= {storage: {}, prefs: {}})
|
|
130
|
+
|
|
131
|
+
for (const app of input.installed) {
|
|
132
|
+
const record = entry(app.packageName)
|
|
133
|
+
record.installed = app.source ? {version: app.version, source: app.source} : {version: app.version}
|
|
134
|
+
for (const suffix of PREF_SUFFIXES) {
|
|
135
|
+
const value = backend.get(`${app.packageName}_${suffix}`)
|
|
136
|
+
if (value != null) record.prefs[suffix] = value
|
|
137
|
+
}
|
|
138
|
+
const expandPrefix = `${app.packageName}_${WIDGET_EXPAND_PREFIX}`
|
|
139
|
+
for (const key of keys) {
|
|
140
|
+
if (!key.startsWith(expandPrefix)) continue
|
|
141
|
+
const value = backend.get(key)
|
|
142
|
+
if (value != null) record.prefs[key.slice(app.packageName.length + 1)] = value
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Longest first, so a package whose name extends another's still claims its own keys.
|
|
147
|
+
const known = input.installed.map((app) => app.packageName).sort((a, b) => b.length - a.length)
|
|
148
|
+
const prefix = `${STORAGE_ROOT}${input.userId}_`
|
|
149
|
+
for (const key of keys) {
|
|
150
|
+
if (!key.startsWith(prefix)) continue
|
|
151
|
+
const split = splitStorageKey(key.slice(prefix.length), known)
|
|
152
|
+
if (!split) continue
|
|
153
|
+
const value = backend.get(key)
|
|
154
|
+
if (value != null) entry(split.packageName).storage[split.key] = value
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
format: "glassly-app-state",
|
|
159
|
+
version: 1,
|
|
160
|
+
exportedAt: input.now,
|
|
161
|
+
settings: settingValues,
|
|
162
|
+
miniapps,
|
|
163
|
+
memory: input.memory,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** `{packageName}_{key}` → its parts. Package names may contain `_`, so known names win over the first `_`. */
|
|
168
|
+
function splitStorageKey(rest: string, known: string[]): {packageName: string; key: string} | null {
|
|
169
|
+
const packageName = known.find((name) => rest.startsWith(`${name}_`)) ?? rest.slice(0, rest.indexOf("_"))
|
|
170
|
+
const key = rest.slice(packageName.length + 1)
|
|
171
|
+
if (!packageName || !key) return null
|
|
172
|
+
return {packageName, key}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parseAppStateBackup(data: unknown): AppStateBackup {
|
|
176
|
+
if (!isRecord(data) || data.format !== "glassly-app-state") {
|
|
177
|
+
throw new AppStateBackupError("Not a Glassly app state backup")
|
|
178
|
+
}
|
|
179
|
+
if (typeof data.version !== "number" || data.version > 1) {
|
|
180
|
+
throw new AppStateBackupError(`Unsupported app state backup version ${String(data.version)}`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const settings = isRecord(data.settings) ? data.settings : {}
|
|
184
|
+
const miniapps: Record<string, AppStateBackupMiniapp> = {}
|
|
185
|
+
if (isRecord(data.miniapps)) {
|
|
186
|
+
for (const [packageName, raw] of Object.entries(data.miniapps)) {
|
|
187
|
+
if (!PACKAGE_NAME_RE.test(packageName) || !isRecord(raw)) continue
|
|
188
|
+
const record: AppStateBackupMiniapp = {
|
|
189
|
+
storage: isRecord(raw.storage) ? raw.storage : {},
|
|
190
|
+
prefs: isRecord(raw.prefs) ? raw.prefs : {},
|
|
191
|
+
}
|
|
192
|
+
if (isRecord(raw.installed) && typeof raw.installed.version === "string") {
|
|
193
|
+
record.installed = {version: raw.installed.version}
|
|
194
|
+
if (typeof raw.installed.source === "string") record.installed.source = raw.installed.source
|
|
195
|
+
}
|
|
196
|
+
miniapps[packageName] = record
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let memory: MemoryBackup | null = null
|
|
201
|
+
if (data.memory != null) {
|
|
202
|
+
if (!isRecord(data.memory) || data.memory.format !== "glassly-memory") {
|
|
203
|
+
throw new AppStateBackupError("The backup's memory section is not a memory backup")
|
|
204
|
+
}
|
|
205
|
+
memory = data.memory as unknown as MemoryBackup
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {format: "glassly-app-state", version: 1, exportedAt: Number(data.exportedAt) || 0, settings, miniapps, memory}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The backup's settings this build will restore, split by how they're written:
|
|
213
|
+
* `plain` through the settings store, `indexed` (`key:index`) straight to storage.
|
|
214
|
+
* Keys this build doesn't know, or doesn't back up, are dropped.
|
|
215
|
+
*/
|
|
216
|
+
export function restorableSettings(
|
|
217
|
+
backup: AppStateBackup,
|
|
218
|
+
settings: BackupSettingDescriptor[],
|
|
219
|
+
): {plain: Record<string, unknown>; indexed: Record<string, unknown>} {
|
|
220
|
+
const byKey = new Map(settings.map((setting) => [setting.key, setting]))
|
|
221
|
+
const plain: Record<string, unknown> = {}
|
|
222
|
+
const indexed: Record<string, unknown> = {}
|
|
223
|
+
for (const [key, value] of Object.entries(backup.settings)) {
|
|
224
|
+
if (value == null) continue
|
|
225
|
+
const colon = key.indexOf(":")
|
|
226
|
+
const setting = byKey.get(colon < 0 ? key : key.slice(0, colon))
|
|
227
|
+
if (!setting || !isBackupSetting(setting)) continue
|
|
228
|
+
if (colon < 0 && !setting.indexer) plain[key] = value
|
|
229
|
+
else if (colon > 0 && setting.indexer) indexed[key] = value
|
|
230
|
+
}
|
|
231
|
+
return {plain, indexed}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Miniapps the backup has storage for. */
|
|
235
|
+
export function miniappsWithStorage(backup: AppStateBackup): string[] {
|
|
236
|
+
return Object.entries(backup.miniapps)
|
|
237
|
+
.filter(([, record]) => Object.entries(record.storage).some(([key, value]) => key && value != null))
|
|
238
|
+
.map(([packageName]) => packageName)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Write every miniapp's storage into `userId`'s namespace. Returns the packages that got keys. */
|
|
242
|
+
export function restoreMiniappStorage(
|
|
243
|
+
backend: AppStateBackupBackend,
|
|
244
|
+
userId: string,
|
|
245
|
+
backup: AppStateBackup,
|
|
246
|
+
): {packages: string[]; keys: number} {
|
|
247
|
+
const packages: string[] = []
|
|
248
|
+
let keys = 0
|
|
249
|
+
for (const [packageName, record] of Object.entries(backup.miniapps)) {
|
|
250
|
+
let wrote = 0
|
|
251
|
+
for (const [key, value] of Object.entries(record.storage)) {
|
|
252
|
+
if (!key || value == null) continue
|
|
253
|
+
backend.set(`${STORAGE_ROOT}${userId}_${packageName}_${key}`, value)
|
|
254
|
+
wrote++
|
|
255
|
+
}
|
|
256
|
+
if (wrote > 0) packages.push(packageName)
|
|
257
|
+
keys += wrote
|
|
258
|
+
}
|
|
259
|
+
return {packages, keys}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface MiniappPrefs {
|
|
263
|
+
hidden?: boolean
|
|
264
|
+
alwaysOn?: boolean
|
|
265
|
+
widgetExpand: Record<string, boolean>
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A miniapp's home flags from the backup, with anything that isn't a boolean dropped. */
|
|
269
|
+
export function miniappPrefs(record: AppStateBackupMiniapp): MiniappPrefs {
|
|
270
|
+
const prefs: MiniappPrefs = {widgetExpand: {}}
|
|
271
|
+
if (typeof record.prefs.hidden === "boolean") prefs.hidden = record.prefs.hidden
|
|
272
|
+
if (typeof record.prefs.always_on === "boolean") prefs.alwaysOn = record.prefs.always_on
|
|
273
|
+
for (const [key, value] of Object.entries(record.prefs)) {
|
|
274
|
+
if (!key.startsWith(WIDGET_EXPAND_PREFIX) || typeof value !== "boolean") continue
|
|
275
|
+
const widgetId = key.slice(WIDGET_EXPAND_PREFIX.length)
|
|
276
|
+
if (widgetId) prefs.widgetExpand[widgetId] = value
|
|
277
|
+
}
|
|
278
|
+
return prefs
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
282
|
+
return value != null && typeof value === "object" && !Array.isArray(value)
|
|
283
|
+
}
|
|
@@ -504,20 +504,14 @@ export class BlobStore {
|
|
|
504
504
|
const key = (typeof payload.key === "string" && payload.key) || this.makeId()
|
|
505
505
|
const mimeFilter = payload.mimeType as string | undefined
|
|
506
506
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// Picker dismissed / cancelled (or unavailable) → resolve to null, not an error.
|
|
512
|
-
console.log(`${LOG_TAG}: blob import picker dismissed`, err)
|
|
513
|
-
this.hooks.sendResult(packageName, requestId, true, null)
|
|
514
|
-
return
|
|
515
|
-
}
|
|
516
|
-
const file = Array.isArray(picked) ? picked[0] : picked
|
|
517
|
-
if (!file) {
|
|
507
|
+
// The options form resolves {canceled, result} and never throws; the deprecated
|
|
508
|
+
// (initialUri, mimeType) form resolves a bare File only when a mime type is passed.
|
|
509
|
+
const picked = await File.pickFileAsync(mimeFilter ? {mimeTypes: mimeFilter} : {})
|
|
510
|
+
if (picked.canceled) {
|
|
518
511
|
this.hooks.sendResult(packageName, requestId, true, null)
|
|
519
512
|
return
|
|
520
513
|
}
|
|
514
|
+
const file = picked.result
|
|
521
515
|
try {
|
|
522
516
|
const size = file.size ?? 0
|
|
523
517
|
if (this.overQuota(packageName, key, size)) {
|
|
@@ -19,9 +19,10 @@ import type {
|
|
|
19
19
|
RuntimeSnapshot,
|
|
20
20
|
StoreCatalogResponse,
|
|
21
21
|
StoreListing,
|
|
22
|
+
StoreReleasesResponse,
|
|
22
23
|
} from "@glassly/cloud-client/react-native"
|
|
23
24
|
import type {SubjectTokenType} from "@glassly/cloud-client"
|
|
24
|
-
import {Platform} from "react-native"
|
|
25
|
+
import {AppState, Platform} from "react-native"
|
|
25
26
|
import type {AudioSubscription, QuotaStatusPayload, TranscriptionData, TranslationData} from "@glassly/cloud-protocol"
|
|
26
27
|
|
|
27
28
|
import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
@@ -70,6 +71,8 @@ const LOCAL_AUTH_PORT = 3002
|
|
|
70
71
|
|
|
71
72
|
let client: CloudClient | null = null
|
|
72
73
|
let connected = false
|
|
74
|
+
/** The cloud closed us for a newer socket; the client stays down until reconnect(). */
|
|
75
|
+
let superseded = false
|
|
73
76
|
let persistentFailureTimer: ReturnType<typeof BgTimer.setTimeout> | null = null
|
|
74
77
|
let persistentFailureNotified = false
|
|
75
78
|
let audioSubscriptions: AudioSubscription[] = []
|
|
@@ -84,6 +87,7 @@ let authStateUnsubscribe: (() => void) | null = null
|
|
|
84
87
|
let localDevRuntimeToken: {runtimeUrl: string; token: string; expiresAtMs: number} | null = null
|
|
85
88
|
let lc3FrameSizeUnsubscribe: (() => void) | null = null
|
|
86
89
|
let disableCloudUnsubscribe: (() => void) | null = null
|
|
90
|
+
let foregroundUnsubscribe: (() => void) | null = null
|
|
87
91
|
let coreTokenSyncPromise: Promise<string> | null = null
|
|
88
92
|
/**
|
|
89
93
|
* A device-local namespace (`local_…`) whose data still needs to be adopted
|
|
@@ -511,6 +515,26 @@ function stopLc3FrameSizeWatcher(): void {
|
|
|
511
515
|
lc3FrameSizeUnsubscribe = null
|
|
512
516
|
}
|
|
513
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Reclaim a superseded session only on foreground: the user picked this phone
|
|
520
|
+
* up, and a reclaim on any other signal would fight the newer socket.
|
|
521
|
+
*/
|
|
522
|
+
function startForegroundWatcher(): void {
|
|
523
|
+
if (foregroundUnsubscribe) return
|
|
524
|
+
|
|
525
|
+
const subscription = AppState.addEventListener("change", (state) => {
|
|
526
|
+
if (state !== "active" || !superseded || !client) return
|
|
527
|
+
console.log(`${LOG_TAG}: foregrounded after supersede; reconnecting runtime`)
|
|
528
|
+
cloudClientService.reconnect()
|
|
529
|
+
})
|
|
530
|
+
foregroundUnsubscribe = () => subscription.remove()
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function stopForegroundWatcher(): void {
|
|
534
|
+
foregroundUnsubscribe?.()
|
|
535
|
+
foregroundUnsubscribe = null
|
|
536
|
+
}
|
|
537
|
+
|
|
514
538
|
/**
|
|
515
539
|
* Apply a mid-session flip of the Privacy toggle without a restart: turning it
|
|
516
540
|
* on tears the live client down, turning it back off reconnects. Installed by
|
|
@@ -596,6 +620,7 @@ function construct(): void {
|
|
|
596
620
|
c.runtime.onConnected(() => {
|
|
597
621
|
if (c !== client) return
|
|
598
622
|
connected = true
|
|
623
|
+
superseded = false
|
|
599
624
|
console.log(`${LOG_TAG}: runtime connected`)
|
|
600
625
|
console.log(`${LOG_TAG}: debug: ws-session-debug connected`)
|
|
601
626
|
// Cloud is up — cancel any pending persistent-failure alarm and re-arm for the
|
|
@@ -613,10 +638,9 @@ function construct(): void {
|
|
|
613
638
|
c.runtime.onDisconnected((info) => {
|
|
614
639
|
if (c !== client) return
|
|
615
640
|
connected = false
|
|
641
|
+
superseded = /superseded by newer session/i.test(info.reason)
|
|
616
642
|
console.log(`${LOG_TAG}: runtime disconnected (${info.reason})`)
|
|
617
|
-
console.log(
|
|
618
|
-
`${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${/superseded by newer session/i.test(info.reason)}`,
|
|
619
|
-
)
|
|
643
|
+
console.log(`${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${superseded}`)
|
|
620
644
|
// Arm the persistent-failure alarm once; if we're still down when it fires, raise
|
|
621
645
|
// the notification. A reconnect within the window cancels it (onConnected above).
|
|
622
646
|
if (!persistentFailureTimer && !persistentFailureNotified) {
|
|
@@ -659,6 +683,7 @@ function construct(): void {
|
|
|
659
683
|
)
|
|
660
684
|
|
|
661
685
|
startLc3FrameSizeWatcher()
|
|
686
|
+
startForegroundWatcher()
|
|
662
687
|
}
|
|
663
688
|
|
|
664
689
|
/**
|
|
@@ -720,6 +745,7 @@ export const cloudClientService = {
|
|
|
720
745
|
client = null
|
|
721
746
|
localDevRuntimeToken = null
|
|
722
747
|
connected = false
|
|
748
|
+
superseded = false
|
|
723
749
|
resetRuntimeStatus()
|
|
724
750
|
if (wasConnected) notifyConnectionListeners(false)
|
|
725
751
|
|
|
@@ -753,6 +779,13 @@ export const cloudClientService = {
|
|
|
753
779
|
return c.core.store.get(packageName)
|
|
754
780
|
},
|
|
755
781
|
|
|
782
|
+
async getStoreReleases(packageName: string): Promise<StoreReleasesResponse> {
|
|
783
|
+
if (!client) this.init()
|
|
784
|
+
const c = client
|
|
785
|
+
if (!c?.core) throw new Error("cloud client core is unavailable")
|
|
786
|
+
return c.core.store.releases(packageName)
|
|
787
|
+
},
|
|
788
|
+
|
|
756
789
|
/** Bytes of a store icon or screenshot; these routes need the user Bearer. */
|
|
757
790
|
async fetchStoreAsset(url: string): Promise<Uint8Array> {
|
|
758
791
|
if (!client) this.init()
|
|
@@ -835,11 +868,13 @@ export const cloudClientService = {
|
|
|
835
868
|
console.warn(`${LOG_TAG}: stop close() failed: ${(err as Error)?.message ?? err}`)
|
|
836
869
|
}
|
|
837
870
|
stopLc3FrameSizeWatcher()
|
|
871
|
+
stopForegroundWatcher()
|
|
838
872
|
stopAuthWatch()
|
|
839
873
|
clearRuntimeEventSubscriptions()
|
|
840
874
|
clearPersistentFailureAlarm()
|
|
841
875
|
const wasConnected = connected
|
|
842
876
|
client = null
|
|
877
|
+
superseded = false
|
|
843
878
|
// stop() is part of the logout lifecycle and runs before the host emits
|
|
844
879
|
// SIGNED_OUT. Remove the persisted namespace owner here so a later account
|
|
845
880
|
// can never inherit it. A force-stop/process kill does not call stop(), so
|
|
@@ -855,6 +890,10 @@ export const cloudClientService = {
|
|
|
855
890
|
shutdown(): void {
|
|
856
891
|
this.stop()
|
|
857
892
|
stopDisableCloudWatcher()
|
|
893
|
+
// The next engine.start() is configured afresh; an override left by a
|
|
894
|
+
// reconnect() in this process would otherwise outrank the new config
|
|
895
|
+
// and keep the client on the previous backend.
|
|
896
|
+
endpointsOverride = null
|
|
858
897
|
},
|
|
859
898
|
|
|
860
899
|
/** Device-side managed photo: presign now, deliver bytes, await ready. */
|
|
@@ -245,6 +245,7 @@ export function startDeviceEventRouter(): void {
|
|
|
245
245
|
subs.push(BluetoothSdk.addListener("neural_band_claim", (event) => neuralBandInput.handleClaim(event)))
|
|
246
246
|
subs.push(BluetoothSdk.addListener("neural_band_handwriting", (event) => neuralBandInput.handleHandwriting(event)))
|
|
247
247
|
subs.push(BluetoothSdk.addListener("neural_band_pointer", (event) => neuralBandInput.handlePointer(event)))
|
|
248
|
+
subs.push(BluetoothSdk.addListener("neural_band_firmware", (event) => neuralBandInput.handleFirmware(event)))
|
|
248
249
|
subs.push({
|
|
249
250
|
remove: useGlassesStore.subscribe(
|
|
250
251
|
(s) => s.keyboardConnected === true,
|
|
@@ -4,6 +4,7 @@ import type {DisplayDiagnostic} from "@glassly/miniapp"
|
|
|
4
4
|
// messages, packet bytes, or arbitrary application content to another miniapp.
|
|
5
5
|
const fields = new Set([
|
|
6
6
|
"v",
|
|
7
|
+
"device",
|
|
7
8
|
"run",
|
|
8
9
|
"id",
|
|
9
10
|
"t",
|
|
@@ -72,10 +73,11 @@ export function collectDisplayDiagnostics(
|
|
|
72
73
|
const seen = new Set<string>()
|
|
73
74
|
for (let i = entries.length - 1; i >= 0 && records.length < 180; i--) {
|
|
74
75
|
const message = entries[i].message
|
|
75
|
-
const
|
|
76
|
+
const prefix = message.includes("MEMOMINDPERF ") ? "MEMOMINDPERF " : "G2PERF "
|
|
77
|
+
const offset = message.indexOf(prefix)
|
|
76
78
|
if (offset < 0 || message.length > 8192) continue
|
|
77
79
|
try {
|
|
78
|
-
const raw = JSON.parse(message.slice(offset +
|
|
80
|
+
const raw = JSON.parse(message.slice(offset + prefix.length))
|
|
79
81
|
if (
|
|
80
82
|
!raw ||
|
|
81
83
|
raw.v !== 1 ||
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
Z100_PROFILE,
|
|
21
21
|
NEX_PROFILE,
|
|
22
22
|
RAYNEO_IO_PROFILE,
|
|
23
|
+
MEMOMIND_ONE_PROFILE,
|
|
24
|
+
RAYNEO_IO_CFW_PROFILE,
|
|
23
25
|
TextMeasurer,
|
|
24
26
|
TextWrapper,
|
|
25
27
|
ColumnComposer,
|
|
@@ -42,12 +44,7 @@ import {isGlassesConnected} from "./GlassesReadiness"
|
|
|
42
44
|
* Display event types that we process
|
|
43
45
|
*/
|
|
44
46
|
export type DisplayLayoutType =
|
|
45
|
-
| "
|
|
46
|
-
| "text_line"
|
|
47
|
-
| "text_rows"
|
|
48
|
-
| "reference_card"
|
|
49
|
-
| "double_text_wall"
|
|
50
|
-
| "bitmap_view"
|
|
47
|
+
"text_wall" | "text_line" | "text_rows" | "reference_card" | "double_text_wall" | "bitmap_view"
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
50
|
* Raw display event from the cloud/WebSocket
|
|
@@ -208,6 +205,16 @@ const DEVICE_PROFILES: Partial<Record<DeviceTypes, DisplayProfile>> = {
|
|
|
208
205
|
[DeviceTypes.NEX]: NEX_PROFILE,
|
|
209
206
|
[DeviceTypes.MACH1]: Z100_PROFILE, // Mach1 uses same hardware as Vuzix Z100
|
|
210
207
|
[DeviceTypes.RAYNEO_IO]: RAYNEO_IO_PROFILE,
|
|
208
|
+
[DeviceTypes.MEMOMIND_ONE]: MEMOMIND_ONE_PROFILE,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Scene text profiles for models whose Glassly CFW draws text with a font of
|
|
213
|
+
* its own, used while the CFW publishes a scene canvas.
|
|
214
|
+
*/
|
|
215
|
+
const CFW_SCENE_PROFILES: Partial<Record<DeviceTypes, DisplayProfile>> = {
|
|
216
|
+
[DeviceTypes.RAYNEO_IO]: RAYNEO_IO_CFW_PROFILE,
|
|
217
|
+
[DeviceTypes.MEMOMIND_ONE]: MEMOMIND_ONE_PROFILE,
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
// =============================================================================
|
|
@@ -275,8 +282,7 @@ export class DisplayProcessor {
|
|
|
275
282
|
// Seed the device profile from the saved default wearable, read off the engine
|
|
276
283
|
// settings store directly (was a host settings hook).
|
|
277
284
|
const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable) as
|
|
278
|
-
|
|
|
279
|
-
| undefined
|
|
285
|
+
string | undefined
|
|
280
286
|
if (defaultWearable) {
|
|
281
287
|
this.setDeviceModel(defaultWearable)
|
|
282
288
|
console.log(`DISPLAY_PROCESSOR: Initialized DisplayProcessor with default wearable: ${defaultWearable}`)
|
|
@@ -380,6 +386,12 @@ export class DisplayProcessor {
|
|
|
380
386
|
return this.profile
|
|
381
387
|
}
|
|
382
388
|
|
|
389
|
+
/** Profile scene text is wrapped with: the CFW's own while its scene page is up, else the model's. */
|
|
390
|
+
public getSceneProfile(): DisplayProfile {
|
|
391
|
+
const cfw = CFW_SCENE_PROFILES[this.deviceModel]
|
|
392
|
+
return cfw && (useGlassesStore.getState().sceneCanvasWidth ?? 0) > 0 ? cfw : this.profile
|
|
393
|
+
}
|
|
394
|
+
|
|
383
395
|
// ===========================================================================
|
|
384
396
|
// Display Event Processing
|
|
385
397
|
// ===========================================================================
|
|
@@ -110,7 +110,7 @@ function routeTouchEvent(event: TouchEvent): void {
|
|
|
110
110
|
const doubleTap = gesture === "double_tap"
|
|
111
111
|
// The Neural Band's middle finger (index 1) shares the tap names. Either
|
|
112
112
|
// finger leaves the app on screen; the firmware dashboard is the index finger's.
|
|
113
|
-
const osDoubleTap = doubleTap && event.index
|
|
113
|
+
const osDoubleTap = doubleTap && !(event.source === 3 && event.index === 1)
|
|
114
114
|
const osHeadUp = gesture === "head_up"
|
|
115
115
|
|
|
116
116
|
if (doubleTap) {
|
|
@@ -156,7 +156,11 @@ function routeTouchEvent(event: TouchEvent): void {
|
|
|
156
156
|
// pagesEligible: connected Glassly CFW glasses, whose firmware leaves the
|
|
157
157
|
// dashboard to the phone; other firmware opens its own on a lift.
|
|
158
158
|
const opensNativeDashboard = osDoubleTap || (osHeadUp && pagesEligible())
|
|
159
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
opensNativeDashboard &&
|
|
161
|
+
!nativeNavigationService.isActive &&
|
|
162
|
+
isGlassesConnected(useGlassesStore.getState().connection)
|
|
163
|
+
) {
|
|
160
164
|
useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
|
|
161
165
|
void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
|
|
162
166
|
console.error("DeviceEventRouter: showDashboard failed:", err)
|