@glassly/engine 0.3.8-dev.1014 → 0.3.8-dev.1016
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/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/GlassesSensorService.js +1 -1
- package/build/services/GlassesSensorService.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 +50 -0
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/neuralBand/HandwritingGate.d.ts +38 -0
- package/build/services/neuralBand/HandwritingGate.d.ts.map +1 -0
- package/build/services/neuralBand/HandwritingGate.js +89 -0
- package/build/services/neuralBand/HandwritingGate.js.map +1 -0
- package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -1
- package/build/services/neuralBand/NeuralBandSettingsSync.js +30 -3
- package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +10 -0
- package/build/stores/settings.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/GlassesSensorService.ts +1 -1
- package/src/services/LocalMiniappRuntime.ts +49 -0
- package/src/services/neuralBand/HandwritingGate.ts +111 -0
- package/src/services/neuralBand/NeuralBandSettingsSync.ts +34 -3
- package/src/stores/settings.ts +10 -0
|
@@ -673,6 +673,9 @@ class LocalMiniappRuntime {
|
|
|
673
673
|
private connectedApps: Map<string, ConnectedMiniapp> = new Map()
|
|
674
674
|
/** Claimed gestures (CLAIM_GESTURES): a stack per gesture, the most recent claimer holding it. */
|
|
675
675
|
private gestureClaims: GestureClaimState = emptyGestureClaims()
|
|
676
|
+
/** Apps with a text field open on the glasses (TEXT_FIELD). */
|
|
677
|
+
private textFieldApps = new Set<string>()
|
|
678
|
+
private textFieldListeners = new Set<(open: boolean) => void>()
|
|
676
679
|
// The last main-view frame each app asked to render. Widget mode refuses
|
|
677
680
|
// frames and demotion clears them, and an app that has drawn once may not
|
|
678
681
|
// draw again on its own, so promotion replays this.
|
|
@@ -1295,6 +1298,7 @@ class LocalMiniappRuntime {
|
|
|
1295
1298
|
}
|
|
1296
1299
|
// Its gesture claims go back to whoever claimed them before.
|
|
1297
1300
|
this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName))
|
|
1301
|
+
this.setTextFieldOpen(packageName, false)
|
|
1298
1302
|
this.retainedRenders.delete(packageName)
|
|
1299
1303
|
|
|
1300
1304
|
// Warm-up is a request-owned camera lease. Cancel it even if the app record was already
|
|
@@ -1478,6 +1482,9 @@ class LocalMiniappRuntime {
|
|
|
1478
1482
|
case MiniappRequestType.CLAIM_GESTURES:
|
|
1479
1483
|
this.handleClaimGestures(packageName, payload, requestId)
|
|
1480
1484
|
break
|
|
1485
|
+
case MiniappRequestType.TEXT_FIELD:
|
|
1486
|
+
this.handleTextField(packageName, payload, requestId)
|
|
1487
|
+
break
|
|
1481
1488
|
case MiniappRequestType.DISPLAY:
|
|
1482
1489
|
this.handleDisplay(packageName, payload, requestId)
|
|
1483
1490
|
break
|
|
@@ -2214,6 +2221,48 @@ class LocalMiniappRuntime {
|
|
|
2214
2221
|
}
|
|
2215
2222
|
}
|
|
2216
2223
|
|
|
2224
|
+
/**
|
|
2225
|
+
* TEXT_FIELD (session.input.setTextFieldOpen): the app has a text field on
|
|
2226
|
+
* the glasses, or no longer does. Text entry that costs power to leave on
|
|
2227
|
+
* (Neural Band handwriting) runs while any app has one. Cleared on app stop.
|
|
2228
|
+
*/
|
|
2229
|
+
private handleTextField(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
2230
|
+
if (typeof payload.open !== "boolean") {
|
|
2231
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2232
|
+
code: MiniappErrorCode.INVALID_ARGUMENT,
|
|
2233
|
+
message: "Text field open must be a boolean",
|
|
2234
|
+
})
|
|
2235
|
+
return
|
|
2236
|
+
}
|
|
2237
|
+
if (this.connectedApps.has(packageName)) this.setTextFieldOpen(packageName, payload.open)
|
|
2238
|
+
this.sendResult(packageName, requestId, true)
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
private setTextFieldOpen(packageName: string, open: boolean): void {
|
|
2242
|
+
const before = this.textFieldApps.size > 0
|
|
2243
|
+
if (open) this.textFieldApps.add(packageName)
|
|
2244
|
+
else this.textFieldApps.delete(packageName)
|
|
2245
|
+
const after = this.textFieldApps.size > 0
|
|
2246
|
+
if (before === after) return
|
|
2247
|
+
for (const cb of this.textFieldListeners) {
|
|
2248
|
+
try {
|
|
2249
|
+
cb(after)
|
|
2250
|
+
} catch (err) {
|
|
2251
|
+
console.warn(`${LOG_TAG}: text field listener failed:`, err)
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
/** True while any running miniapp has a text field open on the glasses. */
|
|
2257
|
+
public hasOpenTextField(): boolean {
|
|
2258
|
+
return this.textFieldApps.size > 0
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
public onTextFieldChange(cb: (open: boolean) => void): () => void {
|
|
2262
|
+
this.textFieldListeners.add(cb)
|
|
2263
|
+
return () => this.textFieldListeners.delete(cb)
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2217
2266
|
/** The app holding `gesture` (the most recent claimer), or null when it is unclaimed. */
|
|
2218
2267
|
public gestureClaimant(gesture: string): string | null {
|
|
2219
2268
|
const claimant = gestureClaimant(this.gestureClaims, gesture)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides when each connected Meta Neural Band runs its handwriting model. The
|
|
3
|
+
* model streams to the phone for as long as it runs, written on or not, so it
|
|
4
|
+
* runs only while it can type somewhere: the wearer has handwriting enabled
|
|
5
|
+
* and a miniapp has a text field open on the glasses. It outlives the last
|
|
6
|
+
* field by OFF_DELAY_MS, so moving between fields doesn't restart it. A session
|
|
7
|
+
* the band ends on its own (timeout, refused setting, silence) isn't retried
|
|
8
|
+
* until no field is open, so a band that can't run the model isn't asked again
|
|
9
|
+
* and again.
|
|
10
|
+
*/
|
|
11
|
+
import type {NeuralBandHandwritingStatus} from "./NeuralBandInput"
|
|
12
|
+
|
|
13
|
+
type Phase = NeuralBandHandwritingStatus["phase"]
|
|
14
|
+
|
|
15
|
+
export interface HandwritingGateDeps {
|
|
16
|
+
enabled: () => boolean
|
|
17
|
+
textFieldOpen: () => boolean
|
|
18
|
+
setHandwriting: (id: string, enabled: boolean) => Promise<void>
|
|
19
|
+
setTimeout: (fn: () => void, ms: number) => unknown
|
|
20
|
+
clearTimeout: (handle: unknown) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Band {
|
|
24
|
+
/** What we last asked this band for. */
|
|
25
|
+
requested: boolean
|
|
26
|
+
failed: boolean
|
|
27
|
+
phase: Phase
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const OFF_DELAY_MS = 5_000
|
|
31
|
+
const ACTIVE = new Set<Phase>(["preparing", "ready", "restoring"])
|
|
32
|
+
|
|
33
|
+
export class HandwritingGate {
|
|
34
|
+
private bands = new Map<string, Band>()
|
|
35
|
+
private offTimer: unknown = null
|
|
36
|
+
|
|
37
|
+
constructor(private readonly deps: HandwritingGateDeps) {}
|
|
38
|
+
|
|
39
|
+
/** The connected bands. A band keeps nothing across connections, so a reconnect starts fresh. */
|
|
40
|
+
setBands(ids: string[]): void {
|
|
41
|
+
for (const id of [...this.bands.keys()]) if (!ids.includes(id)) this.bands.delete(id)
|
|
42
|
+
for (const id of ids) if (!this.bands.has(id)) this.bands.set(id, {requested: false, failed: false, phase: "idle"})
|
|
43
|
+
this.update()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A band reported its handwriting phase. */
|
|
47
|
+
status(id: string, phase: Phase): void {
|
|
48
|
+
const band = this.bands.get(id)
|
|
49
|
+
if (!band) return
|
|
50
|
+
band.phase = phase
|
|
51
|
+
if (phase === "finished" && band.requested) {
|
|
52
|
+
band.requested = false
|
|
53
|
+
band.failed = true
|
|
54
|
+
}
|
|
55
|
+
this.update()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Re-evaluate after the setting or the open text fields changed. */
|
|
59
|
+
update(): void {
|
|
60
|
+
const enabled = this.deps.enabled()
|
|
61
|
+
if (enabled && this.deps.textFieldOpen()) {
|
|
62
|
+
this.clearOffTimer()
|
|
63
|
+
for (const [id, band] of this.bands) {
|
|
64
|
+
// A session still winding down (or one we didn't start) has to finish first.
|
|
65
|
+
if (band.requested || band.failed || ACTIVE.has(band.phase)) continue
|
|
66
|
+
this.request(id, band, true)
|
|
67
|
+
}
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
for (const band of this.bands.values()) band.failed = false
|
|
71
|
+
if (![...this.bands.values()].some((band) => band.requested)) {
|
|
72
|
+
this.clearOffTimer()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (!enabled) {
|
|
76
|
+
this.clearOffTimer()
|
|
77
|
+
this.stopAll()
|
|
78
|
+
} else if (this.offTimer === null) {
|
|
79
|
+
this.offTimer = this.deps.setTimeout(() => {
|
|
80
|
+
this.offTimer = null
|
|
81
|
+
if (!this.deps.textFieldOpen() || !this.deps.enabled()) this.stopAll()
|
|
82
|
+
}, OFF_DELAY_MS)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
dispose(): void {
|
|
87
|
+
this.clearOffTimer()
|
|
88
|
+
this.bands.clear()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private stopAll(): void {
|
|
92
|
+
for (const [id, band] of this.bands) if (band.requested) this.request(id, band, false)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private request(id: string, band: Band, enabled: boolean): void {
|
|
96
|
+
band.requested = enabled
|
|
97
|
+
this.deps.setHandwriting(id, enabled).catch((err) => {
|
|
98
|
+
console.warn(`HandwritingGate: turning handwriting ${enabled ? "on" : "off"} failed:`, err)
|
|
99
|
+
if (enabled && this.bands.get(id) === band && band.requested) {
|
|
100
|
+
band.requested = false
|
|
101
|
+
band.failed = true
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private clearOffTimer(): void {
|
|
107
|
+
if (this.offTimer === null) return
|
|
108
|
+
this.deps.clearTimeout(this.offTimer)
|
|
109
|
+
this.offTimer = null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pushes the Neural Band user settings (air mouse, wearing hand) to
|
|
3
|
-
* connected band: to each band as it connects, and to all of them
|
|
4
|
-
*
|
|
2
|
+
* Pushes the Neural Band user settings (air mouse, wearing hand, vibration) to
|
|
3
|
+
* every connected band: to each band as it connects, and to all of them
|
|
4
|
+
* whenever a setting changes. The native side keeps nothing across
|
|
5
|
+
* connections. Handwriting is gated, not pushed: HandwritingGate runs it while
|
|
6
|
+
* the setting is on and a miniapp has a text field open.
|
|
5
7
|
*/
|
|
6
8
|
import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
7
9
|
|
|
8
10
|
import {useGlassesStore} from "../../stores/glasses"
|
|
9
11
|
import {SETTINGS, useSettingsStore} from "../../stores/settings"
|
|
10
12
|
import {ControllerTypes} from "../../types/enums"
|
|
13
|
+
import {BgTimer} from "../../utils/timers"
|
|
11
14
|
import {readyControllers} from "../controllers"
|
|
15
|
+
import localMiniappRuntime from "../LocalMiniappRuntime"
|
|
16
|
+
import {HandwritingGate} from "./HandwritingGate"
|
|
17
|
+
import neuralBandInput from "./NeuralBandInput"
|
|
12
18
|
|
|
13
19
|
let unsubscribers: (() => void)[] = []
|
|
20
|
+
let handwriting: HandwritingGate | null = null
|
|
14
21
|
|
|
15
22
|
function connectedBandIds(): string[] {
|
|
16
23
|
return readyControllers(useGlassesStore.getState().controllers)
|
|
@@ -39,10 +46,32 @@ function pushVibration(id: string): void {
|
|
|
39
46
|
)
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
function startHandwritingGate(): (() => void)[] {
|
|
50
|
+
const gate = new HandwritingGate({
|
|
51
|
+
enabled: () => useSettingsStore.getState().getSetting(SETTINGS.neural_band_handwriting.key) !== false,
|
|
52
|
+
textFieldOpen: () => localMiniappRuntime.hasOpenTextField(),
|
|
53
|
+
setHandwriting: (id, enabled) => BluetoothSdk.neuralBandSetHandwriting(id, enabled),
|
|
54
|
+
setTimeout: (fn, ms) => BgTimer.setTimeout(fn, ms),
|
|
55
|
+
clearTimeout: (handle) => BgTimer.clearTimeout(handle as number),
|
|
56
|
+
})
|
|
57
|
+
handwriting = gate
|
|
58
|
+
gate.setBands(connectedBandIds())
|
|
59
|
+
return [
|
|
60
|
+
useSettingsStore.subscribe(
|
|
61
|
+
(s) => s.getSetting(SETTINGS.neural_band_handwriting.key),
|
|
62
|
+
() => gate.update(),
|
|
63
|
+
),
|
|
64
|
+
localMiniappRuntime.onTextFieldChange(() => gate.update()),
|
|
65
|
+
neuralBandInput.onHandwritingStatus(({id, status}) => gate.status(id, status.phase)),
|
|
66
|
+
() => gate.dispose(),
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
export function startNeuralBandSettingsSync(): void {
|
|
43
71
|
if (unsubscribers.length > 0) return
|
|
44
72
|
let connected = new Set(connectedBandIds())
|
|
45
73
|
unsubscribers = [
|
|
74
|
+
...startHandwritingGate(),
|
|
46
75
|
useGlassesStore.subscribe(
|
|
47
76
|
() => connectedBandIds().sort().join(","),
|
|
48
77
|
() => {
|
|
@@ -54,6 +83,7 @@ export function startNeuralBandSettingsSync(): void {
|
|
|
54
83
|
pushVibration(id)
|
|
55
84
|
}
|
|
56
85
|
connected = next
|
|
86
|
+
handwriting?.setBands([...next])
|
|
57
87
|
},
|
|
58
88
|
),
|
|
59
89
|
useSettingsStore.subscribe(
|
|
@@ -74,4 +104,5 @@ export function startNeuralBandSettingsSync(): void {
|
|
|
74
104
|
export function stopNeuralBandSettingsSync(): void {
|
|
75
105
|
for (const unsubscribe of unsubscribers) unsubscribe()
|
|
76
106
|
unsubscribers = []
|
|
107
|
+
handwriting = null
|
|
77
108
|
}
|
package/src/stores/settings.ts
CHANGED
|
@@ -848,6 +848,16 @@ export const SETTINGS: Record<string, Setting> = {
|
|
|
848
848
|
saveOnServer: false,
|
|
849
849
|
persist: true,
|
|
850
850
|
},
|
|
851
|
+
// Meta Neural Band handwriting typing into miniapps. Enabled means the band runs
|
|
852
|
+
// its handwriting model while a miniapp has a text field open on the glasses
|
|
853
|
+
// (HandwritingGate); the model streams the whole time it runs, so never otherwise.
|
|
854
|
+
neural_band_handwriting: {
|
|
855
|
+
key: "neural_band_handwriting",
|
|
856
|
+
defaultValue: () => true,
|
|
857
|
+
writable: true,
|
|
858
|
+
saveOnServer: false,
|
|
859
|
+
persist: true,
|
|
860
|
+
},
|
|
851
861
|
// The band's own buzz on each gesture (its haptics pattern 1; pattern 2 is silent).
|
|
852
862
|
neural_band_vibration: {
|
|
853
863
|
key: "neural_band_vibration",
|