@glassly/engine 0.3.3-dev.670 → 0.3.4-dev.675
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/build/engine.d.ts +38 -14
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +3 -0
- package/build/engine.js.map +1 -1
- package/build/facades/ai.d.ts +32 -0
- package/build/facades/ai.d.ts.map +1 -0
- package/build/facades/ai.js +38 -0
- package/build/facades/ai.js.map +1 -0
- package/build/facades/glasses.d.ts +30 -24
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +49 -30
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/glassesSettings.d.ts.map +1 -1
- package/build/facades/glassesSettings.js +1 -0
- package/build/facades/glassesSettings.js.map +1 -1
- package/build/facades/pairing.d.ts +6 -5
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +14 -14
- package/build/facades/pairing.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +5 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +9 -1
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/GestureDotsService.d.ts +4 -3
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +17 -12
- package/build/services/GestureDotsService.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +11 -9
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LlmRouting.d.ts +35 -0
- package/build/services/LlmRouting.d.ts.map +1 -0
- package/build/services/LlmRouting.js +25 -0
- package/build/services/LlmRouting.js.map +1 -0
- package/build/services/LocalLlmModelManager.d.ts +79 -0
- package/build/services/LocalLlmModelManager.d.ts.map +1 -0
- package/build/services/LocalLlmModelManager.js +278 -0
- package/build/services/LocalLlmModelManager.js.map +1 -0
- package/build/services/LocalLlmPrompt.d.ts +90 -0
- package/build/services/LocalLlmPrompt.d.ts.map +1 -0
- package/build/services/LocalLlmPrompt.js +189 -0
- package/build/services/LocalLlmPrompt.js.map +1 -0
- package/build/services/LocalLlmProvider.d.ts +35 -0
- package/build/services/LocalLlmProvider.d.ts.map +1 -0
- package/build/services/LocalLlmProvider.js +218 -0
- package/build/services/LocalLlmProvider.js.map +1 -0
- package/build/services/LocalMiniappRuntime.d.ts +9 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +91 -17
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/STTModelManager.d.ts +25 -2
- package/build/services/STTModelManager.d.ts.map +1 -1
- package/build/services/STTModelManager.js +137 -6
- package/build/services/STTModelManager.js.map +1 -1
- package/build/services/controllers.d.ts +21 -0
- package/build/services/controllers.d.ts.map +1 -0
- package/build/services/controllers.js +62 -0
- package/build/services/controllers.js.map +1 -0
- package/build/services/effectiveCapabilities.d.ts.map +1 -1
- package/build/services/effectiveCapabilities.js +18 -24
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/build/services/neuralBand/NeuralBandInput.d.ts +17 -6
- package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -1
- package/build/services/neuralBand/NeuralBandInput.js +40 -23
- 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 +23 -26
- package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -1
- package/build/services/settingsBridge.d.ts +26 -18
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +36 -15
- package/build/services/settingsBridge.js.map +1 -1
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +3 -1
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/core.d.ts.map +1 -1
- package/build/stores/core.js +0 -1
- package/build/stores/core.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +2 -6
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +28 -2
- package/build/stores/settings.js.map +1 -1
- package/package.json +12 -8
- package/src/engine.ts +3 -0
- package/src/facades/ai.ts +43 -0
- package/src/facades/glasses.ts +60 -31
- package/src/facades/glassesSettings.ts +1 -0
- package/src/facades/pairing.ts +18 -14
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +16 -2
- package/src/services/DeviceEventRouter.ts +8 -1
- package/src/services/GestureDotsService.ts +20 -14
- package/src/services/GlassesTouchInput.ts +11 -9
- package/src/services/LlmRouting.ts +49 -0
- package/src/services/LocalLlmModelManager.ts +327 -0
- package/src/services/LocalLlmPrompt.ts +246 -0
- package/src/services/LocalLlmProvider.ts +290 -0
- package/src/services/LocalMiniappRuntime.ts +137 -21
- package/src/services/STTModelManager.ts +140 -8
- package/src/services/controllers.ts +77 -0
- package/src/services/effectiveCapabilities.ts +16 -32
- package/src/services/neuralBand/NeuralBandInput.ts +50 -26
- package/src/services/neuralBand/NeuralBandSettingsSync.ts +28 -26
- package/src/services/settingsBridge.ts +60 -22
- package/src/stores/bluetoothSettingKeys.ts +3 -1
- package/src/stores/core.ts +0 -1
- package/src/stores/glasses.ts +2 -6
- package/src/stores/settings.ts +28 -2
package/src/facades/glasses.ts
CHANGED
|
@@ -14,9 +14,11 @@ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
|
14
14
|
import type {
|
|
15
15
|
ButtonPressEvent,
|
|
16
16
|
ConnectOptions,
|
|
17
|
+
ControllerStatus,
|
|
17
18
|
Device,
|
|
18
19
|
KeyboardAdvertEvent,
|
|
19
20
|
KeyboardPairingEvent,
|
|
21
|
+
PairedController,
|
|
20
22
|
TouchEvent,
|
|
21
23
|
} from "@glassly/bluetooth-sdk"
|
|
22
24
|
import {useGlassesStore} from "../stores/glasses"
|
|
@@ -26,9 +28,11 @@ import micStateCoordinator from "../services/MicStateCoordinator"
|
|
|
26
28
|
import {projectPairingIdentity} from "../services/PairingIdentity"
|
|
27
29
|
import {isGlassesConnected, isGlassesReady} from "../services/GlassesReadiness"
|
|
28
30
|
import {pushAllBluetoothSettings} from "../services/GlassesSettingsSync"
|
|
31
|
+
import {isControllerReady, pairedControllersFrom, readyControllers} from "../services/controllers"
|
|
29
32
|
import neuralBandInput, {
|
|
30
33
|
type NeuralBandClaimStatus,
|
|
31
34
|
type NeuralBandHandwritingStatus,
|
|
35
|
+
type NeuralBandHandwritingUpdate,
|
|
32
36
|
} from "../services/neuralBand/NeuralBandInput"
|
|
33
37
|
import {recordSupportProfileConnectionFailure} from "../services/SupportProfileSync"
|
|
34
38
|
import {getModelCapabilities, type DeviceTypes} from "../types"
|
|
@@ -55,14 +59,17 @@ function projectStatus() {
|
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
function projectControllerList(): PairedController[] {
|
|
63
|
+
return pairedControllersFrom(useSettingsStore.getState().getSetting(SETTINGS.controllers.key))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Copies: snapshots must not hand callers mutable references into the store.
|
|
67
|
+
function projectControllerStatuses(): Record<string, ControllerStatus> {
|
|
68
|
+
const statuses: Record<string, ControllerStatus> = {}
|
|
69
|
+
for (const [id, status] of Object.entries(useGlassesStore.getState().controllers ?? {})) {
|
|
70
|
+
statuses[id] = {...status}
|
|
65
71
|
}
|
|
72
|
+
return statuses
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
function projectKeyboardStatus() {
|
|
@@ -107,7 +114,6 @@ function projectInfo() {
|
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
export type GlassesStatusSnapshot = ReturnType<typeof projectStatus>
|
|
110
|
-
export type ControllerStatusSnapshot = ReturnType<typeof projectControllerStatus>
|
|
111
117
|
export type KeyboardStatusSnapshot = ReturnType<typeof projectKeyboardStatus>
|
|
112
118
|
export type GlassesInfoSnapshot = ReturnType<typeof projectInfo>
|
|
113
119
|
|
|
@@ -259,45 +265,68 @@ export const glasses = {
|
|
|
259
265
|
},
|
|
260
266
|
},
|
|
261
267
|
|
|
262
|
-
// ---
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const snap = projectControllerStatus()
|
|
272
|
-
const key = JSON.stringify(snap)
|
|
268
|
+
// --- controllers (input accessories linked to the phone), by registry id ---
|
|
269
|
+
controllers: {
|
|
270
|
+
/** Paired controllers in pairing order. */
|
|
271
|
+
list: (): PairedController[] => projectControllerList(),
|
|
272
|
+
onList: (cb: (controllers: PairedController[]) => void): (() => void) => {
|
|
273
|
+
let last = JSON.stringify(projectControllerList())
|
|
274
|
+
return useSettingsStore.subscribe(() => {
|
|
275
|
+
const list = projectControllerList()
|
|
276
|
+
const key = JSON.stringify(list)
|
|
273
277
|
if (key === last) return
|
|
274
278
|
last = key
|
|
275
|
-
cb(
|
|
279
|
+
cb(list)
|
|
276
280
|
})
|
|
277
281
|
},
|
|
282
|
+
/** Live driver statuses by id; a paired controller without an entry is idle. */
|
|
283
|
+
statuses: (): Record<string, ControllerStatus> => projectControllerStatuses(),
|
|
284
|
+
status: (id: string): ControllerStatus | undefined => projectControllerStatuses()[id],
|
|
285
|
+
onStatus: (cb: (statuses: Record<string, ControllerStatus>) => void): (() => void) =>
|
|
286
|
+
useGlassesStore.subscribe(
|
|
287
|
+
(s) => s.controllers,
|
|
288
|
+
() => cb(projectControllerStatuses()),
|
|
289
|
+
),
|
|
290
|
+
/** Controllers delivering input (linked, route or claim done). */
|
|
291
|
+
connected: (): ControllerStatus[] => readyControllers(projectControllerStatuses()),
|
|
292
|
+
isConnected: (model?: string): boolean =>
|
|
293
|
+
Object.values(useGlassesStore.getState().controllers ?? {}).some(
|
|
294
|
+
(status) => isControllerReady(status) && (model === undefined || status.model === model),
|
|
295
|
+
),
|
|
296
|
+
connect: (id: string): Promise<void> => BluetoothSdk.connectController(id),
|
|
297
|
+
/** Reconnect every paired controller that is not connected. */
|
|
298
|
+
connectAll: (): Promise<void> => BluetoothSdk.connectControllers(),
|
|
299
|
+
/** Drop an in-flight controller scan or pairing; paired records are untouched. */
|
|
300
|
+
cancelAttempt: (): Promise<void> => BluetoothSdk.cancelControllerAttempt(),
|
|
301
|
+
/** Disconnect; the controller stays paired. */
|
|
302
|
+
disconnect: (id: string): Promise<void> => BluetoothSdk.disconnectController(id),
|
|
303
|
+
forget: async (id: string): Promise<void> => {
|
|
304
|
+
await BluetoothSdk.forgetController(id)
|
|
305
|
+
neuralBandInput.forget(id)
|
|
306
|
+
},
|
|
278
307
|
},
|
|
279
308
|
|
|
280
|
-
// --- Meta Neural Band (a controller with a claim step and its own features) ---
|
|
309
|
+
// --- Meta Neural Band (a controller with a claim step and its own features), by controller id ---
|
|
281
310
|
neuralBand: {
|
|
282
|
-
/** Store the Meta account session the one-time band claim uses. */
|
|
311
|
+
/** Store the Meta account session the one-time band claim uses (account-wide). */
|
|
283
312
|
setMetaSession: (accessToken: string, userId: string): Promise<void> =>
|
|
284
313
|
BluetoothSdk.neuralBandSetMetaSession(accessToken, userId),
|
|
285
314
|
clearMetaSession: (): Promise<void> => BluetoothSdk.neuralBandClearMetaSession(),
|
|
286
315
|
hasMetaSession: (): Promise<boolean> => BluetoothSdk.neuralBandHasMetaSession(),
|
|
287
|
-
/**
|
|
288
|
-
isClaimed: (): Promise<boolean> => BluetoothSdk.neuralBandIsClaimed(),
|
|
289
|
-
/** The latest claim progress, kept across screens. */
|
|
290
|
-
claimStatus: (): NeuralBandClaimStatus => neuralBandInput.claimStatus(),
|
|
316
|
+
/** This band's identity is stored on this phone, so it reconnects without a claim. */
|
|
317
|
+
isClaimed: (id: string): Promise<boolean> => BluetoothSdk.neuralBandIsClaimed(id),
|
|
318
|
+
/** The latest claim progress of any band (with its `controllerId`), kept across screens. */
|
|
319
|
+
claimStatus: (): NeuralBandClaimStatus => ({...neuralBandInput.claimStatus()}),
|
|
291
320
|
resetClaim: (): void => neuralBandInput.resetClaim(),
|
|
292
321
|
onClaimStatus: (cb: (status: NeuralBandClaimStatus) => void): (() => void) => neuralBandInput.onClaimStatus(cb),
|
|
293
322
|
/** The band's handwriting model; writing arrives on the miniapp keyboard stream. */
|
|
294
|
-
setHandwriting: (enabled: boolean): Promise<void> => BluetoothSdk.neuralBandSetHandwriting(enabled),
|
|
295
|
-
handwritingStatus: (): NeuralBandHandwritingStatus => neuralBandInput.handwritingStatus(),
|
|
296
|
-
onHandwritingStatus: (cb: (
|
|
323
|
+
setHandwriting: (id: string, enabled: boolean): Promise<void> => BluetoothSdk.neuralBandSetHandwriting(id, enabled),
|
|
324
|
+
handwritingStatus: (id: string): NeuralBandHandwritingStatus => ({...neuralBandInput.handwritingStatus(id)}),
|
|
325
|
+
onHandwritingStatus: (cb: (update: NeuralBandHandwritingUpdate) => void): (() => void) =>
|
|
297
326
|
neuralBandInput.onHandwritingStatus(cb),
|
|
298
327
|
/** The band's air mouse; movement reaches miniapps on the pointer stream. */
|
|
299
|
-
setPointer: (enabled: boolean): Promise<void> => BluetoothSdk.neuralBandSetPointer(enabled),
|
|
300
|
-
setHand: (hand: "left" | "right"): Promise<void> => BluetoothSdk.neuralBandSetHand(hand),
|
|
328
|
+
setPointer: (id: string, enabled: boolean): Promise<void> => BluetoothSdk.neuralBandSetPointer(id, enabled),
|
|
329
|
+
setHand: (id: string, hand: "left" | "right"): Promise<void> => BluetoothSdk.neuralBandSetHand(id, hand),
|
|
301
330
|
},
|
|
302
331
|
|
|
303
332
|
// --- audio (glasses media-volume, own-app playback hint, live mic level) ---
|
|
@@ -23,6 +23,7 @@ const INTERNAL_KEYS = new Set<string>([
|
|
|
23
23
|
SETTINGS.device_name.key,
|
|
24
24
|
SETTINGS.device_address.key,
|
|
25
25
|
SETTINGS.project_name.key,
|
|
26
|
+
SETTINGS.controllers.key,
|
|
26
27
|
SETTINGS.pending_controller.key,
|
|
27
28
|
SETTINGS.default_controller.key,
|
|
28
29
|
SETTINGS.controller_device_name.key,
|
package/src/facades/pairing.ts
CHANGED
|
@@ -13,7 +13,7 @@ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
|
13
13
|
import type {ConnectOptions, Device, DeviceModel, PairFailureEvent, GlassesNotReadyEvent} from "@glassly/bluetooth-sdk"
|
|
14
14
|
import {useCoreStore} from "../stores/core"
|
|
15
15
|
import {useGlassesStore} from "../stores/glasses"
|
|
16
|
-
import {
|
|
16
|
+
import {useSettingsStore} from "../stores/settings"
|
|
17
17
|
import {hasDefaultDevice} from "../services/DeviceStoreHydration"
|
|
18
18
|
import {
|
|
19
19
|
markPendingSelection,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type AutomaticReportSubmissionStatus,
|
|
29
29
|
} from "../services/AutomaticReportResult"
|
|
30
30
|
import {pushAllBluetoothSettings} from "../services/GlassesSettingsSync"
|
|
31
|
-
import {
|
|
31
|
+
import {isControllerModel, isControllerReady} from "../services/controllers"
|
|
32
32
|
import {submitAutomaticReport} from "./reports"
|
|
33
33
|
|
|
34
34
|
import {BgTimer} from "../utils/timers"
|
|
@@ -63,12 +63,15 @@ function projectTargetReady(options: Pick<PairingReadyWaitOptions, "deviceModel"
|
|
|
63
63
|
const {deviceModel, deviceName} = options
|
|
64
64
|
const glasses = useGlassesStore.getState()
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
// A controller target is ready once some driver of that model delivers input;
|
|
67
|
+
// the scan's device name is the R1's ring id or the band's Bluetooth name.
|
|
68
|
+
if (isControllerModel(deviceModel)) {
|
|
69
|
+
return Object.values(glasses.controllers ?? {}).some(
|
|
70
|
+
(status) =>
|
|
71
|
+
status.model === deviceModel &&
|
|
72
|
+
isControllerReady(status) &&
|
|
73
|
+
(!deviceName || status.name === deviceName || status.id === deviceName),
|
|
74
|
+
)
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
if (!isGlassesReady(glasses.connection)) return false
|
|
@@ -283,17 +286,12 @@ export const pairing = {
|
|
|
283
286
|
* host renders it as a finish-pairing affordance until pairing succeeds. */
|
|
284
287
|
markPendingSelection: (model: string) => markPendingSelection(model),
|
|
285
288
|
|
|
286
|
-
/** Start scanning for nearby glasses
|
|
289
|
+
/** Start scanning for nearby glasses or controllers of `model`. Results land on `searchResults()`/`onFound()`. */
|
|
287
290
|
scan: (model: DeviceModel): Promise<void> => BluetoothSdk.startScan(model),
|
|
288
291
|
/** Whether a scan is currently in progress. */
|
|
289
292
|
scanning: (): boolean => useCoreStore.getState().searching,
|
|
290
293
|
/** Subscribe to scan-in-progress changes; fires only when it changes. Returns an unsubscribe. */
|
|
291
294
|
onScanning: (cb: (scanning: boolean) => void): (() => void) => useCoreStore.subscribe((s) => s.searching, cb),
|
|
292
|
-
/** Whether a controller scan is currently in progress. */
|
|
293
|
-
scanningController: (): boolean => useCoreStore.getState().searchingController,
|
|
294
|
-
/** Subscribe to controller-scan-in-progress changes; fires only when it changes. Returns an unsubscribe. */
|
|
295
|
-
onScanningController: (cb: (scanning: boolean) => void): (() => void) =>
|
|
296
|
-
useCoreStore.subscribe((s) => s.searchingController, cb),
|
|
297
295
|
/** Whether the phone has another Bluetooth (audio) device connected — the BT Classic pairing hint. */
|
|
298
296
|
otherBtConnected: (): boolean => useCoreStore.getState().otherBtConnected,
|
|
299
297
|
/** Subscribe to other-Bluetooth-device-connected changes; fires only when it changes. Returns an unsubscribe. */
|
|
@@ -394,6 +392,12 @@ export const pairing = {
|
|
|
394
392
|
await BluetoothSdk.forget()
|
|
395
393
|
},
|
|
396
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Abandon an in-flight controller pairing (back-out, failure retry). Only the
|
|
397
|
+
* attempt's driver is dropped; paired controllers and the glasses are untouched.
|
|
398
|
+
*/
|
|
399
|
+
abandonControllerAttempt: (): Promise<void> => BluetoothSdk.cancelControllerAttempt(),
|
|
400
|
+
|
|
397
401
|
/** Subscribe to pairing failures; returns an unsubscribe. */
|
|
398
402
|
onPairFailure: (cb: (event: PairFailureEvent) => void): (() => void) => {
|
|
399
403
|
const sub = BluetoothSdk.addListener("pair_failure", cb)
|
|
@@ -11,10 +11,10 @@ export interface EngineReleaseMetadata {
|
|
|
11
11
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
|
-
"familyBaseVersion": "0.3.
|
|
15
|
-
"releaseIdentity": "0.3.
|
|
16
|
-
"releaseSetId": "glassly-0.3.
|
|
17
|
-
"sourceCommit": "
|
|
14
|
+
"familyBaseVersion": "0.3.4",
|
|
15
|
+
"releaseIdentity": "0.3.4-dev.675",
|
|
16
|
+
"releaseSetId": "glassly-0.3.4-dev.675",
|
|
17
|
+
"sourceCommit": "9f5acf0ba693d861ad1e1a038b8dc565b1fc4d86",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -46,12 +46,22 @@ export type {
|
|
|
46
46
|
export {SETTINGS, useSetting, AI_DATA_CONSENT_VERSION} from "./stores/settings"
|
|
47
47
|
export {isCfwFirmwareVersion, isCustomDashboardEligible} from "./services/pages/pageEligibility"
|
|
48
48
|
export {getEffectiveCapabilities} from "./services/effectiveCapabilities"
|
|
49
|
+
export {isControllerModel, isControllerReady} from "./services/controllers"
|
|
49
50
|
|
|
50
51
|
// AI tier registry: the curated per-tier model dropdown options and helpers
|
|
51
52
|
// the Settings > AI Models screen renders. Resolution itself runs inside
|
|
52
53
|
// LocalMiniappRuntime; the screen only reads/writes the ai_model_* settings.
|
|
53
54
|
export {AI_TIERS, TIER_MODEL_OPTIONS, parseTierSetting} from "./services/AiTierResolution"
|
|
54
55
|
export type {AiTier, AiTierProvider, TierModelOption} from "./services/AiTierResolution"
|
|
56
|
+
// On-device LLM: the model catalog and download status the same screen
|
|
57
|
+
// renders for the phone-side model (engine.ai.local).
|
|
58
|
+
export type {
|
|
59
|
+
LocalLlmDownloadStage,
|
|
60
|
+
LocalLlmDownloadStatus,
|
|
61
|
+
LocalLlmModelConfig,
|
|
62
|
+
LocalLlmModelId,
|
|
63
|
+
} from "./services/LocalLlmModelManager"
|
|
64
|
+
export {DEFAULT_LOCAL_LLM_MODEL, isLocalLlmModelId} from "./services/LocalLlmModelManager"
|
|
55
65
|
|
|
56
66
|
// Shared user memory timeline (engine.memory / Settings > Memory). Entry and
|
|
57
67
|
// session shapes are canonical in @glassly/miniapp (the session.memory
|
|
@@ -157,7 +167,7 @@ export type {
|
|
|
157
167
|
|
|
158
168
|
// Bluetooth SDK event types host UI subscribes to via engine facades (the
|
|
159
169
|
// BluetoothSdk singleton passthrough itself is internal).
|
|
160
|
-
export type {PairFailureEvent, GlassesNotReadyEvent} from "@glassly/bluetooth-sdk"
|
|
170
|
+
export type {PairFailureEvent, GlassesNotReadyEvent, PairedController, ControllerStatus} from "@glassly/bluetooth-sdk"
|
|
161
171
|
|
|
162
172
|
// Cloud connection status enum (read model; the connection store is internal).
|
|
163
173
|
export {WebSocketStatus} from "./stores/connection"
|
|
@@ -215,7 +225,11 @@ export {
|
|
|
215
225
|
type MetaSession,
|
|
216
226
|
type MetaSsoTokens,
|
|
217
227
|
} from "./services/neuralBand/MetaAuth"
|
|
218
|
-
export type {
|
|
228
|
+
export type {
|
|
229
|
+
NeuralBandClaimStatus,
|
|
230
|
+
NeuralBandHandwritingStatus,
|
|
231
|
+
NeuralBandHandwritingUpdate,
|
|
232
|
+
} from "./services/neuralBand/NeuralBandInput"
|
|
219
233
|
|
|
220
234
|
// Hardware types and capability profiles.
|
|
221
235
|
export {
|
|
@@ -40,6 +40,13 @@ import dashboardOpenTrace from "./debug/DashboardOpenTrace"
|
|
|
40
40
|
|
|
41
41
|
let subs: Array<{remove: () => void}> = []
|
|
42
42
|
|
|
43
|
+
// Array/object echoes (the controller registry) arrive as fresh objects.
|
|
44
|
+
function sameSettingValue(current: unknown, next: unknown): boolean {
|
|
45
|
+
if (current === next) return true
|
|
46
|
+
if (typeof current !== "object" || typeof next !== "object" || current === null || next === null) return false
|
|
47
|
+
return JSON.stringify(current) === JSON.stringify(next)
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
export function startDeviceEventRouter(): void {
|
|
44
51
|
if (subs.length) return
|
|
45
52
|
|
|
@@ -173,7 +180,7 @@ export function startDeviceEventRouter(): void {
|
|
|
173
180
|
// Damp the relay: native re-echoes some settings unconditionally (e.g.
|
|
174
181
|
// the identity block at every handleDeviceReady) — a same-value echo
|
|
175
182
|
// must not become a store write (persistence churn + change-push noise).
|
|
176
|
-
if (settings.getSetting(event.key)
|
|
183
|
+
if (!sameSettingValue(settings.getSetting(event.key), event.value)) {
|
|
177
184
|
await settings.setSetting(event.key, event.value)
|
|
178
185
|
}
|
|
179
186
|
// Two-phase identity: a promoted default retires the pending selection
|
|
@@ -53,14 +53,18 @@ function fingerGlyph(glyph: GestureDotGlyph, finger: number): GestureDotGlyph {
|
|
|
53
53
|
return finger === 1 ? (MIDDLE_FINGER_GLYPHS[glyph] ?? glyph) : glyph
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function fingerKey(controllerId: string | undefined, finger: number): string {
|
|
57
|
+
return `${controllerId ?? ""}:${finger}`
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
class GestureDotsService {
|
|
57
61
|
private started = false
|
|
58
62
|
private flash: GestureDotGlyph | null = null
|
|
59
63
|
private flashTimer: number | null = null
|
|
60
|
-
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
61
|
-
private presses = new Map<
|
|
64
|
+
/** Raw controller holds survive hold recognition/dial events until their own release. Keyed by fingerKey. */
|
|
65
|
+
private presses = new Map<string, {raw: boolean; glyph: GestureDotGlyph}>()
|
|
62
66
|
/** When each finger last sent a press edge (touch_down, or tap_end after one). */
|
|
63
|
-
private lastEdgeAt = new Map<
|
|
67
|
+
private lastEdgeAt = new Map<string, number>()
|
|
64
68
|
/** Avoid sending identical images for successive phases of the same press. */
|
|
65
69
|
private renderedGlyph: GestureDotGlyph | null = null
|
|
66
70
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
@@ -120,15 +124,16 @@ class GestureDotsService {
|
|
|
120
124
|
* Flash a gesture on the indicator. Other gesture names are ignored.
|
|
121
125
|
* Claimed gestures cancel any earlier press/flash without starting a new
|
|
122
126
|
* one: the claimant may also consume the release edge. `fingerIndex` is the
|
|
123
|
-
* Neural Band's finger on its pinches and press edges (1 = middle)
|
|
127
|
+
* Neural Band's finger on its pinches and press edges (1 = middle);
|
|
128
|
+
* `controllerId` keeps two controllers' fingers apart.
|
|
124
129
|
*/
|
|
125
|
-
noteGesture(gestureName: string, claimed = false, fingerIndex?: number): void {
|
|
130
|
+
noteGesture(gestureName: string, claimed = false, fingerIndex?: number, controllerId?: string): void {
|
|
126
131
|
if (!this.started || !this.enabled()) return
|
|
127
|
-
this.applyGesture(gestureName, claimed, fingerIndex)
|
|
132
|
+
this.applyGesture(gestureName, claimed, fingerIndex, controllerId)
|
|
128
133
|
this.trace("input", {gestureName, claimed})
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
private applyGesture(gestureName: string, claimed: boolean, fingerIndex?: number): void {
|
|
136
|
+
private applyGesture(gestureName: string, claimed: boolean, fingerIndex?: number, controllerId?: string): void {
|
|
132
137
|
if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null) {
|
|
133
138
|
this.relinquish()
|
|
134
139
|
return
|
|
@@ -139,9 +144,10 @@ class GestureDotsService {
|
|
|
139
144
|
return
|
|
140
145
|
}
|
|
141
146
|
const finger = fingerIndex ?? 0
|
|
142
|
-
const
|
|
147
|
+
const key = fingerKey(controllerId, finger)
|
|
148
|
+
const press = this.presses.get(key)
|
|
143
149
|
if (gestureName === "touch_down" || gestureName === "tap_start") {
|
|
144
|
-
if (gestureName === "touch_down") this.lastEdgeAt.set(
|
|
150
|
+
if (gestureName === "touch_down") this.lastEdgeAt.set(key, Date.now())
|
|
145
151
|
if (press) {
|
|
146
152
|
// Both reports describe one press: after touch_down, tap_start means it is held.
|
|
147
153
|
if (gestureName === "touch_down") press.raw = true
|
|
@@ -150,14 +156,14 @@ class GestureDotsService {
|
|
|
150
156
|
return
|
|
151
157
|
}
|
|
152
158
|
this.clearFlash()
|
|
153
|
-
this.presses.set(
|
|
159
|
+
this.presses.set(key, {raw: gestureName === "touch_down", glyph: fingerGlyph("dot", finger)})
|
|
154
160
|
this.render()
|
|
155
161
|
return
|
|
156
162
|
}
|
|
157
163
|
if (gestureName === "tap_end") {
|
|
158
164
|
if (!press) return
|
|
159
|
-
this.presses.delete(
|
|
160
|
-
if (press.raw) this.lastEdgeAt.set(
|
|
165
|
+
this.presses.delete(key)
|
|
166
|
+
if (press.raw) this.lastEdgeAt.set(key, Date.now())
|
|
161
167
|
this.render()
|
|
162
168
|
return
|
|
163
169
|
}
|
|
@@ -173,11 +179,11 @@ class GestureDotsService {
|
|
|
173
179
|
this.render()
|
|
174
180
|
return
|
|
175
181
|
}
|
|
176
|
-
const lastEdgeAt = this.lastEdgeAt.get(
|
|
182
|
+
const lastEdgeAt = this.lastEdgeAt.get(key)
|
|
177
183
|
if (!press && shownByPress && lastEdgeAt !== undefined && Date.now() - lastEdgeAt < EDGE_REPORT_GRACE_MS) return
|
|
178
184
|
// Completed taps/swipes become one continuous pulse, even if the device
|
|
179
185
|
// omits tap_end. Older tap_start-only devices retain the same fallback.
|
|
180
|
-
this.presses.delete(
|
|
186
|
+
this.presses.delete(key)
|
|
181
187
|
if (glyph) this.startFlash(glyph)
|
|
182
188
|
this.render()
|
|
183
189
|
}
|
|
@@ -28,16 +28,18 @@ const miniappExitGesture = new MiniappExitGesture({
|
|
|
28
28
|
// Raw ring reports include every scroll step, whether the CFW forwards them
|
|
29
29
|
// (deviceModel G2) or the ring sends them to the phone itself (deviceModel R1);
|
|
30
30
|
// both tag source 2. Rate-limit scroll actions separately from press/release
|
|
31
|
-
// recognition, using native receipt timestamps so a JS backlog does
|
|
32
|
-
// collapse otherwise separate gestures.
|
|
31
|
+
// recognition, per ring, using native receipt timestamps so a JS backlog does
|
|
32
|
+
// not collapse otherwise separate gestures.
|
|
33
33
|
const RING_SCROLL_INTERVAL_MS = 100
|
|
34
|
-
|
|
34
|
+
const lastRingScrollAt = new Map<string, number>()
|
|
35
35
|
|
|
36
36
|
function shouldForwardScroll(event: TouchEvent): boolean {
|
|
37
37
|
if (event.source !== 2 || (event.gestureName !== "swipe_up" && event.gestureName !== "swipe_down")) return true
|
|
38
|
-
const
|
|
38
|
+
const ring = event.controllerId ?? `source:${event.source}`
|
|
39
|
+
const last = lastRingScrollAt.get(ring)
|
|
40
|
+
const elapsed = last === undefined ? null : event.timestamp - last
|
|
39
41
|
if (elapsed !== null && elapsed >= 0 && elapsed < RING_SCROLL_INTERVAL_MS) return false
|
|
40
|
-
lastRingScrollAt
|
|
42
|
+
lastRingScrollAt.set(ring, event.timestamp)
|
|
41
43
|
return true
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -72,7 +74,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
72
74
|
!localMiniappRuntime.hasGestureClaim(visible, gesture) &&
|
|
73
75
|
miniappExitGesture.tryExitVisibleMiniapp()
|
|
74
76
|
) {
|
|
75
|
-
gestureDotsService.noteGesture(gesture, true)
|
|
77
|
+
gestureDotsService.noteGesture(gesture, true, event.index, event.controllerId)
|
|
76
78
|
return
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -83,7 +85,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
83
85
|
dashboardOpenTrace.trigger(osHeadUp ? "head-up" : "double-tap", event.timestamp)
|
|
84
86
|
}
|
|
85
87
|
// The press may have started before this app claimed its release edge.
|
|
86
|
-
gestureDotsService.noteGesture(gesture, true, event.index)
|
|
88
|
+
gestureDotsService.noteGesture(gesture, true, event.index, event.controllerId)
|
|
87
89
|
if (!shouldForwardScroll(event)) return
|
|
88
90
|
localMiniappRuntime.forwardEvent("touch_event", event, undefined, claimant)
|
|
89
91
|
return
|
|
@@ -92,7 +94,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
92
94
|
// Gesture-dots debug indicator (any device, gated by its setting). It
|
|
93
95
|
// only paints while the screen is otherwise empty. Process even suppressed
|
|
94
96
|
// scroll repeats here so their completion still clears a held dot.
|
|
95
|
-
gestureDotsService.noteGesture(gesture, false, event.index)
|
|
97
|
+
gestureDotsService.noteGesture(gesture, false, event.index, event.controllerId)
|
|
96
98
|
|
|
97
99
|
if (!shouldForwardScroll(event)) return
|
|
98
100
|
localMiniappRuntime.forwardEvent("touch_event", event)
|
|
@@ -116,7 +118,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
116
118
|
|
|
117
119
|
export function resetTouchInput(): void {
|
|
118
120
|
miniappExitGesture.reset()
|
|
119
|
-
lastRingScrollAt
|
|
121
|
+
lastRingScrollAt.clear()
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
/** Phone mirror input uses the same dashboard/miniapp policy as real glasses. */
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LlmRouting — pure decision logic for where one session.ai.complete call
|
|
3
|
+
* runs: the PRIMARY path (vendor-direct on a user key, or the cloud relay) or
|
|
4
|
+
* the on-device model. Same shape as TranscriptionRouting.ts so the rules are
|
|
5
|
+
* unit-testable; LocalMiniappRuntime supplies the inputs and does the work.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface LlmRouteInputs {
|
|
9
|
+
/** Request asked for on-device only (`forceLocal: true`). */
|
|
10
|
+
forceLocal: boolean
|
|
11
|
+
/** Request accepts on-device when the primary is down (`localFallback: true`). */
|
|
12
|
+
localFallback: boolean
|
|
13
|
+
/**
|
|
14
|
+
* Whether the primary path is currently believed usable: a saved user key
|
|
15
|
+
* for the resolved provider, or cloud connected + not disabled in Privacy
|
|
16
|
+
* + monthly AI credit not exhausted.
|
|
17
|
+
*/
|
|
18
|
+
primaryUp: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* - "local": run on-device, never touch the primary.
|
|
23
|
+
* - "primary": primary only; failures surface as today.
|
|
24
|
+
* - "primary_then_local": try the primary, and on any failure serve the same
|
|
25
|
+
* request on-device.
|
|
26
|
+
*/
|
|
27
|
+
export type LlmRoute = "local" | "primary" | "primary_then_local"
|
|
28
|
+
|
|
29
|
+
export function decideLlmRoute({forceLocal, localFallback, primaryUp}: LlmRouteInputs): LlmRoute {
|
|
30
|
+
if (forceLocal) return "local"
|
|
31
|
+
if (!localFallback) return "primary"
|
|
32
|
+
return primaryUp ? "primary_then_local" : "local"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Why a request landed on-device; reported in logs and the result's `model` line. */
|
|
36
|
+
export type LocalLlmReason = "force_local" | "cloud_disconnected" | "cloud_disabled" | "llm_quota" | "primary_failed"
|
|
37
|
+
|
|
38
|
+
export function localLlmReason(inputs: {
|
|
39
|
+
forceLocal: boolean
|
|
40
|
+
cloudDisabled: boolean
|
|
41
|
+
cloudConnected: boolean
|
|
42
|
+
quotaBlocked: boolean
|
|
43
|
+
}): LocalLlmReason {
|
|
44
|
+
if (inputs.forceLocal) return "force_local"
|
|
45
|
+
if (inputs.cloudDisabled) return "cloud_disabled"
|
|
46
|
+
if (inputs.quotaBlocked) return "llm_quota"
|
|
47
|
+
if (!inputs.cloudConnected) return "cloud_disconnected"
|
|
48
|
+
return "primary_failed"
|
|
49
|
+
}
|