@glassly/engine 0.3.5-beta.710 → 0.3.5-beta.760
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 +4 -1
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +5 -1
- package/build/engine.js.map +1 -1
- package/build/facades/ai.d.ts +3 -3
- package/build/facades/ai.d.ts.map +1 -1
- package/build/facades/ai.js +3 -3
- package/build/facades/ai.js.map +1 -1
- package/build/facades/glasses.d.ts +5 -1
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +5 -0
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +2 -1
- package/build/services/GestureDotsService.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts +6 -4
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +76 -37
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +12 -4
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +21 -7
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalLlmModelManager.d.ts +3 -1
- package/build/services/LocalLlmModelManager.d.ts.map +1 -1
- package/build/services/LocalLlmModelManager.js +13 -5
- package/build/services/LocalLlmModelManager.js.map +1 -1
- package/build/services/LocalLlmPrompt.d.ts +10 -9
- package/build/services/LocalLlmPrompt.d.ts.map +1 -1
- package/build/services/LocalLlmPrompt.js +43 -32
- package/build/services/LocalLlmPrompt.js.map +1 -1
- package/build/services/LocalLlmProvider.d.ts.map +1 -1
- package/build/services/LocalLlmProvider.js +39 -20
- package/build/services/LocalLlmProvider.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +22 -4
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +108 -134
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappExitGesture.d.ts +6 -3
- package/build/services/MiniappExitGesture.d.ts.map +1 -1
- package/build/services/MiniappExitGesture.js +4 -4
- package/build/services/MiniappExitGesture.js.map +1 -1
- package/build/services/MiniappWidgetMode.d.ts +6 -4
- package/build/services/MiniappWidgetMode.d.ts.map +1 -1
- package/build/services/MiniappWidgetMode.js +7 -6
- package/build/services/MiniappWidgetMode.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +3 -1
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/actionInvoke.d.ts +37 -0
- package/build/services/actionInvoke.d.ts.map +1 -0
- package/build/services/actionInvoke.js +89 -0
- package/build/services/actionInvoke.js.map +1 -0
- package/build/services/appleWatch/AppleWatchLink.d.ts +42 -0
- package/build/services/appleWatch/AppleWatchLink.d.ts.map +1 -0
- package/build/services/appleWatch/AppleWatchLink.js +259 -0
- package/build/services/appleWatch/AppleWatchLink.js.map +1 -0
- package/build/services/controllers.d.ts.map +1 -1
- package/build/services/controllers.js +16 -0
- package/build/services/controllers.js.map +1 -1
- package/build/services/settingsBridge.d.ts +4 -4
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +9 -4
- package/build/services/settingsBridge.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +13 -11
- package/build/stores/apps.js.map +1 -1
- package/build/stores/bluetoothSettingKeys.d.ts +2 -0
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +17 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +32 -2
- package/build/stores/settings.js.map +1 -1
- package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
- package/build/types/capabilities/rayneo-io.js +2 -0
- package/build/types/capabilities/rayneo-io.js.map +1 -1
- package/build/types/enums.d.ts +2 -1
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -0
- package/build/types/enums.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.map +1 -1
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +20 -1
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/display/scene/types.d.ts +1 -1
- package/build/utils/display/scene/types.d.ts.map +1 -1
- package/build/utils/display/scene/types.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +5 -1
- package/src/facades/ai.ts +3 -3
- package/src/facades/glasses.ts +7 -0
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/GestureDotsService.ts +2 -1
- package/src/services/GlassesTouchInput.ts +81 -34
- package/src/services/LocalDisplayManager.ts +22 -8
- package/src/services/LocalLlmModelManager.ts +16 -6
- package/src/services/LocalLlmPrompt.ts +56 -37
- package/src/services/LocalLlmProvider.ts +49 -22
- package/src/services/LocalMiniappRuntime.ts +140 -134
- package/src/services/MiniappExitGesture.ts +8 -5
- package/src/services/MiniappWidgetMode.ts +9 -7
- package/src/services/SceneRenderer.ts +3 -1
- package/src/services/actionInvoke.ts +105 -0
- package/src/services/appleWatch/AppleWatchLink.ts +297 -0
- package/src/services/controllers.ts +17 -0
- package/src/services/settingsBridge.ts +9 -4
- package/src/stores/apps.ts +12 -10
- package/src/stores/bluetoothSettingKeys.ts +20 -0
- package/src/stores/settings.ts +34 -2
- package/src/types/capabilities/rayneo-io.ts +2 -0
- package/src/types/enums.ts +1 -0
- package/src/utils/display/scene/index.ts +1 -0
- package/src/utils/display/scene/process.ts +17 -1
- package/src/utils/display/scene/types.ts +1 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Apple Watch remote's phone half: the watch app's status context (glasses, the miniapp in
|
|
3
|
+
* front, launchable miniapps, whether Assist takes questions, the mirror setting), mirror frames
|
|
4
|
+
* while the setting is on, and the commands the watch sends back. Native adds whether the active
|
|
5
|
+
* watch is paired and keeps the newest frame for when the watch app opens. Gestures ride
|
|
6
|
+
* `touch_event` like any controller's.
|
|
7
|
+
*/
|
|
8
|
+
import type {AppleWatchCommandEvent} from "@glassly/bluetooth-sdk"
|
|
9
|
+
import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
10
|
+
import type {MirrorSnapshot} from "@glassly/miniapp"
|
|
11
|
+
import {MiniappStreamType} from "@glassly/miniapp"
|
|
12
|
+
import {Platform} from "react-native"
|
|
13
|
+
|
|
14
|
+
import {useAppStatusStore} from "../../stores/apps"
|
|
15
|
+
import {useGlassesStore} from "../../stores/glasses"
|
|
16
|
+
import {SETTINGS, useSettingsStore} from "../../stores/settings"
|
|
17
|
+
import type {ClientApp} from "../../types"
|
|
18
|
+
import {BgTimer} from "../../utils/timers"
|
|
19
|
+
import {isGlassesConnected} from "../GlassesReadiness"
|
|
20
|
+
import localMiniappRuntime from "../LocalMiniappRuntime"
|
|
21
|
+
import {mirrorService} from "../MirrorService"
|
|
22
|
+
import {keyEventsForTextChange} from "../neuralBand/NeuralBandInput"
|
|
23
|
+
|
|
24
|
+
export const ASSIST_PACKAGE = "com.glassly.assistant"
|
|
25
|
+
export const ASSIST_ACTION = "ask"
|
|
26
|
+
/** WatchConnectivity messages top out near 64 KB; frames past this are skipped, not truncated. */
|
|
27
|
+
const MAX_FRAME_BYTES = 48_000
|
|
28
|
+
const MAX_IMAGE_CHARS = 16_000
|
|
29
|
+
const MAX_APPS = 24
|
|
30
|
+
const STATE_DEBOUNCE_MS = 250
|
|
31
|
+
const MIRROR_INTERVAL_MS = 500
|
|
32
|
+
|
|
33
|
+
export interface WatchStateInput {
|
|
34
|
+
glasses: {connected: boolean; model: string; battery: number}
|
|
35
|
+
apps: readonly ClientApp[]
|
|
36
|
+
assist: boolean
|
|
37
|
+
mirror: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Miniapps the watch can open: shown, compatible, and drawing on the glasses. */
|
|
41
|
+
export function launchableApps(apps: readonly ClientApp[]): {packageName: string; name: string}[] {
|
|
42
|
+
return apps
|
|
43
|
+
.filter((app) => app.type === "standard" && !app.hidden && app.compatibility?.isCompatible !== false)
|
|
44
|
+
.filter((app) => app.packageName && !app.packageName.includes("@empty"))
|
|
45
|
+
.slice(0, MAX_APPS)
|
|
46
|
+
.map((app) => ({packageName: app.packageName, name: app.name}))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function watchState({glasses, apps, assist, mirror}: WatchStateInput): Record<string, unknown> {
|
|
50
|
+
const front = apps.find((app) => app.foregrounded && app.running && app.type === "standard")
|
|
51
|
+
const state: Record<string, unknown> = {
|
|
52
|
+
glasses: {connected: glasses.connected, name: glasses.model || "Glasses", battery: glasses.battery},
|
|
53
|
+
apps: launchableApps(apps),
|
|
54
|
+
assist,
|
|
55
|
+
mirror,
|
|
56
|
+
}
|
|
57
|
+
if (front) state.app = {packageName: front.packageName, name: front.name}
|
|
58
|
+
return state
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type WatchElement = Record<string, string | number | boolean | number[]>
|
|
62
|
+
|
|
63
|
+
const level = (color: unknown, fallback = 15) =>
|
|
64
|
+
typeof color === "number" ? Math.max(0, Math.min(15, color)) : fallback
|
|
65
|
+
|
|
66
|
+
/** The glasses frame in the watch's drawing vocabulary; null when there is nothing to draw. */
|
|
67
|
+
export function mirrorFrame(snapshot: MirrorSnapshot): Record<string, unknown> | null {
|
|
68
|
+
const canvas = snapshot.canvas
|
|
69
|
+
if (!canvas) return null
|
|
70
|
+
const content = snapshot.content
|
|
71
|
+
if (content.kind === "bitmap") {
|
|
72
|
+
if (content.data.length > MAX_FRAME_BYTES) return null
|
|
73
|
+
return {width: canvas.width, height: canvas.height, bitmap: content.data, elements: []}
|
|
74
|
+
}
|
|
75
|
+
if (content.kind !== "scene") return null
|
|
76
|
+
const elements: WatchElement[] = []
|
|
77
|
+
for (const element of content.elements) {
|
|
78
|
+
const {x, y, w, h} = element.box
|
|
79
|
+
const style = element.style ?? {}
|
|
80
|
+
switch (element.type) {
|
|
81
|
+
case "text":
|
|
82
|
+
if (!element.text) break
|
|
83
|
+
elements.push({
|
|
84
|
+
type: "text",
|
|
85
|
+
x,
|
|
86
|
+
y,
|
|
87
|
+
w,
|
|
88
|
+
h,
|
|
89
|
+
text: element.text,
|
|
90
|
+
size: style.fontSize ?? 20,
|
|
91
|
+
color: level(style.color),
|
|
92
|
+
})
|
|
93
|
+
break
|
|
94
|
+
case "rect":
|
|
95
|
+
elements.push({
|
|
96
|
+
type: "rect",
|
|
97
|
+
x,
|
|
98
|
+
y,
|
|
99
|
+
w,
|
|
100
|
+
h,
|
|
101
|
+
fill: style.fill === true,
|
|
102
|
+
border: style.border ?? 1,
|
|
103
|
+
radius: style.radius ?? 0,
|
|
104
|
+
color: level(style.color),
|
|
105
|
+
})
|
|
106
|
+
break
|
|
107
|
+
case "circle":
|
|
108
|
+
elements.push({
|
|
109
|
+
type: "circle",
|
|
110
|
+
x,
|
|
111
|
+
y,
|
|
112
|
+
w,
|
|
113
|
+
h,
|
|
114
|
+
fill: style.fill === true,
|
|
115
|
+
border: style.border ?? 1,
|
|
116
|
+
color: level(style.color),
|
|
117
|
+
})
|
|
118
|
+
break
|
|
119
|
+
case "line":
|
|
120
|
+
case "triangle":
|
|
121
|
+
case "quad": {
|
|
122
|
+
const points = (element.points ?? []).flatMap((point) => [point.x, point.y])
|
|
123
|
+
if (points.length < 4) break
|
|
124
|
+
const closed = element.type !== "line"
|
|
125
|
+
elements.push({
|
|
126
|
+
type: "line",
|
|
127
|
+
points,
|
|
128
|
+
width: style.width ?? style.border ?? 1,
|
|
129
|
+
color: level(style.color),
|
|
130
|
+
closed,
|
|
131
|
+
fill: closed && style.fill === true,
|
|
132
|
+
})
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
case "image":
|
|
136
|
+
if (element.data && element.data.length <= MAX_IMAGE_CHARS)
|
|
137
|
+
elements.push({type: "image", x, y, w, h, data: element.data})
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const frame = {width: canvas.width, height: canvas.height, elements}
|
|
142
|
+
return JSON.stringify(frame).length > MAX_FRAME_BYTES ? null : frame
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface AppleWatchCommandDeps {
|
|
146
|
+
apps: () => readonly ClientApp[]
|
|
147
|
+
start: (app: ClientApp) => Promise<unknown>
|
|
148
|
+
stop: (packageName: string) => Promise<unknown>
|
|
149
|
+
ask: (query: string) => Promise<unknown>
|
|
150
|
+
type: (stream: string, payload: unknown) => void
|
|
151
|
+
now: () => number
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const defaultDeps: AppleWatchCommandDeps = {
|
|
155
|
+
apps: () => useAppStatusStore.getState().apps,
|
|
156
|
+
start: (app) => useAppStatusStore.getState().start(app),
|
|
157
|
+
stop: (packageName) => useAppStatusStore.getState().stop(packageName),
|
|
158
|
+
ask: (query) => localMiniappRuntime.invokeActionFromHost(ASSIST_PACKAGE, ASSIST_ACTION, {query}),
|
|
159
|
+
type: (stream, payload) => localMiniappRuntime.forwardEvent(stream, payload),
|
|
160
|
+
now: () => Date.now(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function handleAppleWatchCommand(
|
|
164
|
+
event: AppleWatchCommandEvent,
|
|
165
|
+
deps: AppleWatchCommandDeps = defaultDeps,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
switch (event.command) {
|
|
168
|
+
case "launch": {
|
|
169
|
+
const app = launchableApps(deps.apps()).some((entry) => entry.packageName === event.packageName)
|
|
170
|
+
? deps.apps().find((entry) => entry.packageName === event.packageName)
|
|
171
|
+
: undefined
|
|
172
|
+
if (!app) {
|
|
173
|
+
console.warn(`AppleWatch: launch of unknown miniapp ${event.packageName}`)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
await deps.start(app)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
case "stop": {
|
|
180
|
+
const front = deps.apps().find((app) => app.foregrounded && app.running && app.type === "standard")
|
|
181
|
+
if (front) await deps.stop(front.packageName)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
case "assist": {
|
|
185
|
+
const query = event.text.trim()
|
|
186
|
+
if (query) await deps.ask(query)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
case "text":
|
|
190
|
+
for (const key of keyEventsForTextChange("", event.text, deps.now()))
|
|
191
|
+
deps.type(MiniappStreamType.KEYBOARD_EVENT, key)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let stops: (() => void)[] = []
|
|
197
|
+
let stopMirror: (() => void) | null = null
|
|
198
|
+
|
|
199
|
+
function currentState(): Record<string, unknown> {
|
|
200
|
+
const glasses = useGlassesStore.getState()
|
|
201
|
+
return watchState({
|
|
202
|
+
glasses: {
|
|
203
|
+
connected: isGlassesConnected(glasses.connection),
|
|
204
|
+
model: glasses.deviceModel,
|
|
205
|
+
battery: glasses.batteryLevel,
|
|
206
|
+
},
|
|
207
|
+
apps: useAppStatusStore.getState().apps,
|
|
208
|
+
assist: localMiniappRuntime.hostActionAvailable(ASSIST_PACKAGE, ASSIST_ACTION),
|
|
209
|
+
mirror: useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function syncMirror(enabled: boolean): void {
|
|
214
|
+
if (!enabled) {
|
|
215
|
+
if (stopMirror) {
|
|
216
|
+
stopMirror()
|
|
217
|
+
stopMirror = null
|
|
218
|
+
void BluetoothSdk.setWatchMirror(null).catch(() => {})
|
|
219
|
+
}
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (stopMirror) return
|
|
223
|
+
let lastSentAt = 0
|
|
224
|
+
let pending: MirrorSnapshot | null = null
|
|
225
|
+
let timer: ReturnType<typeof BgTimer.setTimeout> | null = null
|
|
226
|
+
const send = (snapshot: MirrorSnapshot) => {
|
|
227
|
+
lastSentAt = Date.now()
|
|
228
|
+
void BluetoothSdk.setWatchMirror(mirrorFrame(snapshot)).catch((err) =>
|
|
229
|
+
console.warn("AppleWatch: mirror push failed:", err),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
const unsubscribe = mirrorService.subscribe((snapshot) => {
|
|
233
|
+
const wait = lastSentAt + MIRROR_INTERVAL_MS - Date.now()
|
|
234
|
+
if (wait <= 0 && !timer) {
|
|
235
|
+
send(snapshot)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
pending = snapshot
|
|
239
|
+
timer ??= BgTimer.setTimeout(
|
|
240
|
+
() => {
|
|
241
|
+
timer = null
|
|
242
|
+
if (pending) send(pending)
|
|
243
|
+
pending = null
|
|
244
|
+
},
|
|
245
|
+
Math.max(wait, 0),
|
|
246
|
+
)
|
|
247
|
+
})
|
|
248
|
+
stopMirror = () => {
|
|
249
|
+
unsubscribe()
|
|
250
|
+
if (timer) BgTimer.clearTimeout(timer)
|
|
251
|
+
timer = null
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function startAppleWatchLink(): void {
|
|
256
|
+
if (Platform.OS !== "ios" || stops.length > 0) return
|
|
257
|
+
let lastKey = ""
|
|
258
|
+
let timer: ReturnType<typeof BgTimer.setTimeout> | null = null
|
|
259
|
+
const push = () => {
|
|
260
|
+
timer = null
|
|
261
|
+
const state = currentState()
|
|
262
|
+
const key = JSON.stringify(state)
|
|
263
|
+
if (key === lastKey) return
|
|
264
|
+
lastKey = key
|
|
265
|
+
void BluetoothSdk.setWatchState(state).catch((err) => console.warn("AppleWatch: state push failed:", err))
|
|
266
|
+
}
|
|
267
|
+
const schedule = () => {
|
|
268
|
+
timer ??= BgTimer.setTimeout(push, STATE_DEBOUNCE_MS)
|
|
269
|
+
}
|
|
270
|
+
const mirrorEnabled = () => useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true
|
|
271
|
+
const commands = BluetoothSdk.addListener("apple_watch_command", (event) => {
|
|
272
|
+
handleAppleWatchCommand(event).catch((err) => console.warn(`AppleWatch: ${event.command} failed:`, err))
|
|
273
|
+
})
|
|
274
|
+
stops = [
|
|
275
|
+
() => commands.remove(),
|
|
276
|
+
useGlassesStore.subscribe(schedule),
|
|
277
|
+
useAppStatusStore.subscribe(schedule),
|
|
278
|
+
useSettingsStore.subscribe(
|
|
279
|
+
(s) => s.getSetting(SETTINGS.apple_watch_mirror.key),
|
|
280
|
+
() => {
|
|
281
|
+
syncMirror(mirrorEnabled())
|
|
282
|
+
schedule()
|
|
283
|
+
},
|
|
284
|
+
),
|
|
285
|
+
() => {
|
|
286
|
+
if (timer) BgTimer.clearTimeout(timer)
|
|
287
|
+
timer = null
|
|
288
|
+
},
|
|
289
|
+
]
|
|
290
|
+
syncMirror(mirrorEnabled())
|
|
291
|
+
schedule()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function stopAppleWatchLink(): void {
|
|
295
|
+
stops.splice(0).forEach((stop) => stop())
|
|
296
|
+
syncMirror(false)
|
|
297
|
+
}
|
|
@@ -26,9 +26,26 @@ const NEURAL_BAND_SURFACE: InputSurface = {
|
|
|
26
26
|
],
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** The Apple Watch trackpad's taps and four-way swipes, and the Digital Crown as the dial. */
|
|
30
|
+
const APPLE_WATCH_SURFACE: InputSurface = {
|
|
31
|
+
type: "swipe2d",
|
|
32
|
+
events: [
|
|
33
|
+
"TAP",
|
|
34
|
+
"DOUBLE_TAP",
|
|
35
|
+
"PRESS_HOLD",
|
|
36
|
+
"SWIPE_UP",
|
|
37
|
+
"SWIPE_DOWN",
|
|
38
|
+
"SWIPE_LEFT",
|
|
39
|
+
"SWIPE_RIGHT",
|
|
40
|
+
"DIAL_UP",
|
|
41
|
+
"DIAL_DOWN",
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
// The R1 swipes up/down like the temples it stands in for, so it adds nothing.
|
|
30
46
|
const INPUT_SURFACES: Readonly<Record<string, readonly InputSurface[]>> = {
|
|
31
47
|
[ControllerTypes.NEURAL_BAND]: [NEURAL_BAND_SURFACE],
|
|
48
|
+
[ControllerTypes.APPLE_WATCH]: [APPLE_WATCH_SURFACE],
|
|
32
49
|
[ControllerTypes.R1]: [],
|
|
33
50
|
}
|
|
34
51
|
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* be tested without the native import surface the runtime pulls in.
|
|
7
7
|
*
|
|
8
8
|
* A dashboard miniapp is the usual consumer: it edits the notification,
|
|
9
|
-
* glasses-menu
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* that in its storage.
|
|
9
|
+
* glasses-menu, display-position, brightness and microphone keys the phone's
|
|
10
|
+
* settings screens do, through the store, so persistence, server sync and the
|
|
11
|
+
* Bluetooth sync behave as if a phone screen wrote them. A dashboard's own
|
|
12
|
+
* look is not here; it keeps that in its storage.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type {ControllerStatus, PairedController} from "@glassly/bluetooth-sdk"
|
|
@@ -28,6 +28,11 @@ export const SETTINGS_BRIDGE_KEYS: Readonly<Record<string, SettingsBridgeAccess>
|
|
|
28
28
|
// Where the glasses draw; each model's steps are its display.positionRange.
|
|
29
29
|
dashboard_depth: "write",
|
|
30
30
|
dashboard_height: "write",
|
|
31
|
+
// Device settings the dashboard's glasses Settings page edits.
|
|
32
|
+
brightness: "write",
|
|
33
|
+
auto_brightness: "write",
|
|
34
|
+
// Per paired model: the store resolves the indexed key.
|
|
35
|
+
preferred_mic: "write",
|
|
31
36
|
twelve_hour_time: "write",
|
|
32
37
|
menu_apps: "write",
|
|
33
38
|
notifications_enabled: "write",
|
package/src/stores/apps.ts
CHANGED
|
@@ -256,6 +256,7 @@ function projectApps(previousState: AppStatusState, localApps: ClientApp[]): Cli
|
|
|
256
256
|
// across both passes.
|
|
257
257
|
foregrounded: app.packageName === previousState.foregroundedPackage,
|
|
258
258
|
paused: app.local && app.running && isMiniappsPaused(),
|
|
259
|
+
widgetOnly: !!app.local && app.running && miniappWidgetMode.isWidgetOnly(app.packageName),
|
|
259
260
|
}
|
|
260
261
|
const prev = previousByPackage.get(app.packageName)
|
|
261
262
|
return prev && shallowEqualApp(prev, next) ? prev : next
|
|
@@ -362,6 +363,11 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
362
363
|
return false
|
|
363
364
|
}
|
|
364
365
|
|
|
366
|
+
// An app running only for its widget is promoted before the host hook:
|
|
367
|
+
// the hook may foreground it, and setForeground sends a widget-only app
|
|
368
|
+
// back here.
|
|
369
|
+
if (app.widgetOnly) miniappWidgetMode.setWidgetOnly(packageName, false)
|
|
370
|
+
|
|
365
371
|
// Host navigation / open-animation seam. Runs on EVERY accepted open —
|
|
366
372
|
// including re-opening an already-running app — so the animation always
|
|
367
373
|
// plays (BuiltInMiniappCatalog.navigateForApp: overlay apps setForeground
|
|
@@ -374,13 +380,12 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
374
380
|
|
|
375
381
|
// If the tapped app is already running, this is a re-open: the nav/foreground
|
|
376
382
|
// above already drove the animation, and there's nothing to spawn. Short-
|
|
377
|
-
// circuit before the spawn pipeline.
|
|
378
|
-
//
|
|
383
|
+
// circuit before the spawn pipeline. A promoted app keeps its context but
|
|
384
|
+
// still takes the foreground below.
|
|
379
385
|
if (app.running && !app.widgetOnly) {
|
|
380
386
|
saveLastOpenTime(packageName)
|
|
381
387
|
return true
|
|
382
388
|
}
|
|
383
|
-
if (app.widgetOnly) miniappWidgetMode.setWidgetOnly(packageName, false)
|
|
384
389
|
|
|
385
390
|
// Only-one rules: one standard app in the foreground, one dashboard
|
|
386
391
|
// drawing the overlay. Starting another stops the one running. Widget-only
|
|
@@ -495,13 +500,10 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
|
|
|
495
500
|
apps: s.apps.map((a) => ({...a, foregrounded: a.packageName === packageName})),
|
|
496
501
|
}))
|
|
497
502
|
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
// if (!app.running) {
|
|
503
|
-
// get().start(app)
|
|
504
|
-
// }
|
|
503
|
+
// The wearer is in this app now: one running only for its widget becomes
|
|
504
|
+
// a normal run. start() promotes it, stops the standard peer and keeps the
|
|
505
|
+
// context. Fire-and-forget after the flag flip so it doesn't block paint.
|
|
506
|
+
if (app.running && app.widgetOnly) void get().start(app, {skipNavigation: true})
|
|
505
507
|
},
|
|
506
508
|
|
|
507
509
|
clearForeground: () => {
|
|
@@ -35,3 +35,23 @@ function isMentraLiveDevice(model: string | undefined): boolean {
|
|
|
35
35
|
export function getBluetoothSettingKeysForDevice(deviceModel: string | undefined, fullKeys: string[]): string[] {
|
|
36
36
|
return isMentraLiveDevice(deviceModel) ? MENTRA_LIVE_SETTING_KEYS : fullKeys
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Glasses Menu entries the firmware's own menu cannot open: Settings is the
|
|
41
|
+
* dashboard's glasses page, which only the dashboard's Apps menu shows.
|
|
42
|
+
*/
|
|
43
|
+
const NATIVE_MENU_EXCLUDED_PACKAGES = new Set(["com.glassly.settings"])
|
|
44
|
+
const nativeMenuAppsCache = new WeakMap<object, unknown[]>()
|
|
45
|
+
|
|
46
|
+
/** `menu_apps` as the glasses get it; the same array in gives the same array out, so the change-push stays quiet. */
|
|
47
|
+
export function nativeMenuApps(value: unknown): unknown {
|
|
48
|
+
if (!Array.isArray(value)) return value
|
|
49
|
+
let filtered = nativeMenuAppsCache.get(value)
|
|
50
|
+
if (!filtered) {
|
|
51
|
+
filtered = value.filter(
|
|
52
|
+
(item) => !NATIVE_MENU_EXCLUDED_PACKAGES.has((item as {packageName?: string} | null)?.packageName ?? ""),
|
|
53
|
+
)
|
|
54
|
+
nativeMenuAppsCache.set(value, filtered)
|
|
55
|
+
}
|
|
56
|
+
return filtered
|
|
57
|
+
}
|
package/src/stores/settings.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {create} from "zustand"
|
|
|
4
4
|
import {subscribeWithSelector} from "zustand/middleware"
|
|
5
5
|
|
|
6
6
|
import {storage} from "../utils/storage"
|
|
7
|
-
import {getBluetoothSettingKeysForDevice} from "./bluetoothSettingKeys"
|
|
7
|
+
import {getBluetoothSettingKeysForDevice, nativeMenuApps} from "./bluetoothSettingKeys"
|
|
8
8
|
import {useGlassesStore} from "./glasses"
|
|
9
9
|
|
|
10
10
|
export {MENTRA_LIVE_SETTING_KEYS, getBluetoothSettingKeysForDevice} from "./bluetoothSettingKeys"
|
|
@@ -831,6 +831,15 @@ export const SETTINGS: Record<string, Setting> = {
|
|
|
831
831
|
saveOnServer: false,
|
|
832
832
|
persist: true,
|
|
833
833
|
},
|
|
834
|
+
// Apple Watch: draw the glasses' current frame behind the watch trackpad. Frames
|
|
835
|
+
// cross WatchConnectivity while the watch app is up, so it stays off unless asked for.
|
|
836
|
+
apple_watch_mirror: {
|
|
837
|
+
key: "apple_watch_mirror",
|
|
838
|
+
defaultValue: () => false,
|
|
839
|
+
writable: true,
|
|
840
|
+
saveOnServer: false,
|
|
841
|
+
persist: true,
|
|
842
|
+
},
|
|
834
843
|
// Maximum rate at which display updates leave the phone for the glasses.
|
|
835
844
|
// The Bluetooth SDK paces every scene frame and legacy layout to this,
|
|
836
845
|
// coalescing faster updates last-wins; clears bypass it. No effect on G2
|
|
@@ -1577,6 +1586,29 @@ export const useSettingsStore = create<SettingsState>()(
|
|
|
1577
1586
|
// desired value was applied so a transient failure retries on boot.
|
|
1578
1587
|
if (migrationSucceeded) storage.save(CAMERA_FOV_MIGRATION_KEY, true)
|
|
1579
1588
|
}
|
|
1589
|
+
|
|
1590
|
+
// Settings joined the Glasses Menu (the dashboard's glasses Settings
|
|
1591
|
+
// page): add it once, last, to a menu the wearer already saved. An
|
|
1592
|
+
// unsaved menu gets it from the default; a full one (10) is left alone.
|
|
1593
|
+
const MENU_SETTINGS_MIGRATION_KEY = "migration:menu_apps_settings_v1"
|
|
1594
|
+
const menuSettingsMigrationDone = storage.load<boolean>(MENU_SETTINGS_MIGRATION_KEY)
|
|
1595
|
+
if (menuSettingsMigrationDone.is_error() || !menuSettingsMigrationDone.value) {
|
|
1596
|
+
const menu = get().getSetting(SETTINGS.menu_apps.key) as Array<{packageName?: string}> | null
|
|
1597
|
+
let menuMigrated = true
|
|
1598
|
+
if (
|
|
1599
|
+
Array.isArray(menu) &&
|
|
1600
|
+
menu.length < 10 &&
|
|
1601
|
+
!menu.some((item) => item?.packageName === "com.glassly.settings")
|
|
1602
|
+
) {
|
|
1603
|
+
const next = [...menu, {packageName: "com.glassly.settings", name: "Settings"}]
|
|
1604
|
+
const result = await get().setSetting(SETTINGS.menu_apps.key, next, true)
|
|
1605
|
+
if (result.is_error()) {
|
|
1606
|
+
console.log("SETTINGS: menu_apps Settings migration failed:", result.error)
|
|
1607
|
+
menuMigrated = false
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (menuMigrated) storage.save(MENU_SETTINGS_MIGRATION_KEY, true)
|
|
1611
|
+
}
|
|
1580
1612
|
})
|
|
1581
1613
|
loadAllSettingsInFlight = inFlight
|
|
1582
1614
|
void Promise.resolve(inFlight).then((result) => {
|
|
@@ -1598,7 +1630,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|
|
1598
1630
|
if (key === SETTINGS.core_token.key && (typeof value !== "string" || value.trim().length === 0)) {
|
|
1599
1631
|
continue
|
|
1600
1632
|
}
|
|
1601
|
-
bluetoothSettings[key] = value
|
|
1633
|
+
bluetoothSettings[key] = key === SETTINGS.menu_apps.key ? nativeMenuApps(value) : value
|
|
1602
1634
|
}
|
|
1603
1635
|
return bluetoothSettings
|
|
1604
1636
|
},
|
|
@@ -32,6 +32,8 @@ export const rayneoIO: Capabilities = {
|
|
|
32
32
|
fieldOfView: {horizontal: 30},
|
|
33
33
|
maxTextLines: 5,
|
|
34
34
|
adjustBrightness: true,
|
|
35
|
+
// Both drivers map these settings onto the launcher's three heights and two distances.
|
|
36
|
+
positionRange: {height: {min: 0, max: 8}, depth: {min: 1, max: 3}},
|
|
35
37
|
|
|
36
38
|
// Scene display API — the Glassly CFW page canvas.
|
|
37
39
|
width: 540,
|
package/src/types/enums.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
ScenePoint,
|
|
22
22
|
SceneTextStyle,
|
|
23
23
|
SceneTransition,
|
|
24
|
+
SceneTransitionEdge,
|
|
24
25
|
} from "./types"
|
|
25
26
|
import {MAX_SCENE_DEPTH, POINT_SHAPE_ARITY, POINT_SHAPE_TYPES, elementContentHash} from "./types"
|
|
26
27
|
|
|
@@ -66,11 +67,24 @@ function pointsBox(points: ScenePoint[]): SceneBox {
|
|
|
66
67
|
return {x, y, w: Math.max(...xs) - x + 1, h: Math.max(...ys) - y + 1}
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function validEdge(e: unknown): SceneTransitionEdge | undefined {
|
|
71
|
+
if (!e || typeof e !== "object") return undefined
|
|
72
|
+
const dx = Math.round(Number((e as SceneTransitionEdge).dx ?? 0))
|
|
73
|
+
const dy = Math.round(Number((e as SceneTransitionEdge).dy ?? 0))
|
|
74
|
+
if (!isFinite(dx) || !isFinite(dy) || Math.abs(dx) > 1024 || Math.abs(dy) > 1024) return undefined
|
|
75
|
+
return {dx, dy}
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
function validTransition(t: unknown): SceneTransition | undefined {
|
|
70
79
|
if (!t || typeof t !== "object") return undefined
|
|
71
80
|
const {durationMs, easing} = t as SceneTransition
|
|
72
81
|
if (!isFinite(durationMs) || durationMs <= 0) return undefined
|
|
73
|
-
|
|
82
|
+
const out: SceneTransition = easing ? {durationMs, easing} : {durationMs}
|
|
83
|
+
const enter = validEdge((t as SceneTransition).enter)
|
|
84
|
+
const exit = validEdge((t as SceneTransition).exit)
|
|
85
|
+
if (enter) out.enter = enter
|
|
86
|
+
if (exit) out.exit = exit
|
|
87
|
+
return out
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
function boxShrunk(orig: SceneBox, clamped: SceneBox): boolean {
|
|
@@ -175,6 +189,8 @@ export function processScene(
|
|
|
175
189
|
|
|
176
190
|
for (const {el, index, box, points} of valid) {
|
|
177
191
|
const transition = validTransition((el as {transition?: unknown}).transition)
|
|
192
|
+
// Leaving elements only exist to tween away; a device that jumps never shows them.
|
|
193
|
+
if (transition?.exit && !caps.animation) continue
|
|
178
194
|
const transform: {rotation?: number; pivot?: ScenePoint} = {}
|
|
179
195
|
if (el.rotation !== undefined || el.pivot !== undefined) {
|
|
180
196
|
const angle = el.rotation ?? 0
|