@glassly/engine 0.3.2-dev.617 → 0.3.3-beta.633
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/facades/glasses.d.ts +1 -0
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +1 -0
- 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 +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/services/GestureDotsService.d.ts +2 -0
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +25 -9
- package/build/services/GestureDotsService.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/LocalMiniappRuntime.d.ts +1 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +14 -8
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappIcons.d.ts +16 -0
- package/build/services/MiniappIcons.d.ts.map +1 -0
- package/build/services/MiniappIcons.js +54 -0
- package/build/services/MiniappIcons.js.map +1 -0
- package/build/services/effectiveCapabilities.d.ts +2 -0
- package/build/services/effectiveCapabilities.d.ts.map +1 -1
- package/build/services/effectiveCapabilities.js +54 -6
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +1 -0
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +38 -0
- package/build/stores/settings.js.map +1 -1
- package/build/types/hardware.d.ts +1 -0
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js.map +1 -1
- package/package.json +8 -7
- package/src/facades/glasses.ts +1 -0
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +1 -0
- package/src/services/GestureDotsService.ts +20 -7
- package/src/services/GlassesTouchInput.ts +4 -2
- package/src/services/LocalMiniappRuntime.ts +26 -13
- package/src/services/MiniappIcons.ts +50 -0
- package/src/services/effectiveCapabilities.ts +64 -5
- package/src/stores/glasses.ts +1 -0
- package/src/stores/settings.ts +38 -0
- package/src/types/hardware.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3-beta.633",
|
|
4
4
|
"description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,17 +97,17 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@glassly/bluetooth-sdk": "0.3.
|
|
101
|
-
"@glassly/cloud-client": "0.3.
|
|
102
|
-
"@glassly/cloud-protocol": "0.3.
|
|
103
|
-
"@glassly/crust": "0.3.
|
|
104
|
-
"@glassly/miniapp": "0.3.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.3.3-beta.633",
|
|
101
|
+
"@glassly/cloud-client": "0.3.3-beta.633",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.3-beta.633",
|
|
103
|
+
"@glassly/crust": "0.3.3-beta.633",
|
|
104
|
+
"@glassly/miniapp": "0.3.3-beta.633",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"react-native-marked": "8.1.1",
|
|
108
108
|
"semver": "^7.7.4",
|
|
109
109
|
"typesafe-ts": "^1.7.1",
|
|
110
|
-
"@glassly/llm-providers": "0.3.
|
|
110
|
+
"@glassly/llm-providers": "0.3.3-beta.633"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@glassly/llm-providers": "workspace:*",
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
"react-native-reanimated": ">=4.2.1 <5",
|
|
133
133
|
"react-native-worklets": ">=0.7.4 <1",
|
|
134
134
|
"expo-file-system": ">=55.0.0",
|
|
135
|
+
"expo-image-manipulator": ">=55.0.0",
|
|
135
136
|
"react-native-zip-archive": "7.1.1",
|
|
136
137
|
"zustand": "*",
|
|
137
138
|
"expo-battery": ">=55.0.0",
|
package/src/facades/glasses.ts
CHANGED
|
@@ -95,6 +95,7 @@ function projectInfo() {
|
|
|
95
95
|
rightMac: s.rightMacAddress,
|
|
96
96
|
sceneCanvasWidth: s.sceneCanvasWidth,
|
|
97
97
|
sceneCanvasHeight: s.sceneCanvasHeight,
|
|
98
|
+
volumeGate: s.volumeGate === true,
|
|
98
99
|
// Copy: the snapshot must not hand callers a mutable reference into the store.
|
|
99
100
|
wifi: {...s.wifi},
|
|
100
101
|
}
|
|
@@ -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.3",
|
|
15
|
+
"releaseIdentity": "0.3.3-beta.633",
|
|
16
|
+
"releaseSetId": "glassly-0.3.3-beta.633",
|
|
17
|
+
"sourceCommit": "34d49f6f3585d9f0e8e2d28802ae2b70666507a1",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ export type {
|
|
|
45
45
|
// imperative access goes through engine.settings.
|
|
46
46
|
export {SETTINGS, useSetting, AI_DATA_CONSENT_VERSION} from "./stores/settings"
|
|
47
47
|
export {isCfwFirmwareVersion, isCustomDashboardEligible} from "./services/pages/pageEligibility"
|
|
48
|
+
export {getEffectiveCapabilities} from "./services/effectiveCapabilities"
|
|
48
49
|
|
|
49
50
|
// AI tier registry: the curated per-tier model dropdown options and helpers
|
|
50
51
|
// the Settings > AI Models screen renders. Resolution itself runs inside
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* glasses when a touch gesture arrives, while nothing else is on the display:
|
|
4
4
|
* a filled circle for a single tap, two for a double tap, an arrow for a swipe
|
|
5
5
|
* (cleared after a short pulse), a bullseye for a recognized long press.
|
|
6
|
-
*
|
|
6
|
+
* Controllers that report press edges (touch_down … tap_end) show the press
|
|
7
|
+
* itself instead: a dot while pressed, the bullseye once held, cleared at
|
|
8
|
+
* release. The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from
|
|
7
9
|
* the CFW dashboard service; it renders through
|
|
8
10
|
* the ordinary scene pipeline, so it works on any positioning device (stock
|
|
9
11
|
* G2 included), and is gated by the gesture_dots debug setting.
|
|
@@ -24,6 +26,8 @@ import sceneRenderer from "./SceneRenderer"
|
|
|
24
26
|
const DOTS_APP_ID = "system.gesture-dots"
|
|
25
27
|
/** How long a gesture stays on the indicator. */
|
|
26
28
|
const GESTURE_FLASH_MS = 700
|
|
29
|
+
/** A press-edge device's tap and hold reports trail its last edge by its double-tap window. */
|
|
30
|
+
const EDGE_REPORT_GRACE_MS = 1000
|
|
27
31
|
const MARGIN_X = 16
|
|
28
32
|
const MARGIN_TOP = 10
|
|
29
33
|
/** Gestures that flash a glyph. */
|
|
@@ -45,6 +49,8 @@ class GestureDotsService {
|
|
|
45
49
|
private flashTimer: number | null = null
|
|
46
50
|
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
47
51
|
private presses = new Map<number, {raw: boolean; glyph: GestureDotGlyph}>()
|
|
52
|
+
/** When each finger last sent a press edge (touch_down, or tap_end after one). */
|
|
53
|
+
private lastEdgeAt = new Map<number, number>()
|
|
48
54
|
/** Avoid sending identical images for successive phases of the same press. */
|
|
49
55
|
private renderedGlyph: GestureDotGlyph | null = null
|
|
50
56
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
@@ -76,6 +82,7 @@ class GestureDotsService {
|
|
|
76
82
|
for (const unsub of this.unsubs) unsub()
|
|
77
83
|
this.unsubs = []
|
|
78
84
|
this.relinquish()
|
|
85
|
+
this.lastEdgeAt.clear()
|
|
79
86
|
this.started = false
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -124,9 +131,12 @@ class GestureDotsService {
|
|
|
124
131
|
const finger = fingerIndex ?? 0
|
|
125
132
|
const press = this.presses.get(finger)
|
|
126
133
|
if (gestureName === "touch_down" || gestureName === "tap_start") {
|
|
134
|
+
if (gestureName === "touch_down") this.lastEdgeAt.set(finger, Date.now())
|
|
127
135
|
if (press) {
|
|
128
|
-
// Both reports describe one press
|
|
136
|
+
// Both reports describe one press: after touch_down, tap_start means it is held.
|
|
129
137
|
if (gestureName === "touch_down") press.raw = true
|
|
138
|
+
else if (press.raw) press.glyph = "hold"
|
|
139
|
+
this.render()
|
|
130
140
|
return
|
|
131
141
|
}
|
|
132
142
|
this.clearFlash()
|
|
@@ -137,21 +147,24 @@ class GestureDotsService {
|
|
|
137
147
|
if (gestureName === "tap_end") {
|
|
138
148
|
if (!press) return
|
|
139
149
|
this.presses.delete(finger)
|
|
140
|
-
|
|
141
|
-
// Keep the glyph lit across that gap so one pinch never flashes twice.
|
|
142
|
-
if (press.raw && this.presses.size === 0) this.startFlash(press.glyph)
|
|
150
|
+
if (press.raw) this.lastEdgeAt.set(finger, Date.now())
|
|
143
151
|
this.render()
|
|
144
152
|
return
|
|
145
153
|
}
|
|
146
154
|
const shape = FLASH_GLYPHS[gestureName]
|
|
147
155
|
const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape
|
|
148
|
-
|
|
156
|
+
// The press already showed its own tap and hold; the device's report of
|
|
157
|
+
// them follows after its double-tap window, often after release.
|
|
158
|
+
const shownByPress = gestureName === "single_tap" || gestureName === "long_press"
|
|
159
|
+
if (press?.raw && (!glyph || shownByPress)) {
|
|
149
160
|
// Pinch-and-turn reports do not release a finger. A recognized hold
|
|
150
161
|
// stays visible until tap_end instead of expiring halfway through it.
|
|
151
|
-
if (
|
|
162
|
+
if (gestureName === "long_press") press.glyph = "hold"
|
|
152
163
|
this.render()
|
|
153
164
|
return
|
|
154
165
|
}
|
|
166
|
+
const lastEdgeAt = this.lastEdgeAt.get(finger)
|
|
167
|
+
if (!press && shownByPress && lastEdgeAt !== undefined && Date.now() - lastEdgeAt < EDGE_REPORT_GRACE_MS) return
|
|
155
168
|
// Completed taps/swipes become one continuous pulse, even if the device
|
|
156
169
|
// omits tap_end. Older tap_start-only devices retain the same fallback.
|
|
157
170
|
this.presses.delete(finger)
|
|
@@ -7,6 +7,7 @@ import {useGlassesStore} from "../stores/glasses"
|
|
|
7
7
|
import {BgTimer} from "../utils/timers"
|
|
8
8
|
import dashboardOpenTrace from "./debug/DashboardOpenTrace"
|
|
9
9
|
import gestureDotsService from "./GestureDotsService"
|
|
10
|
+
import {isGlassesConnected} from "./GlassesReadiness"
|
|
10
11
|
import localDisplayManager from "./LocalDisplayManager"
|
|
11
12
|
import localMiniappRuntime from "./LocalMiniappRuntime"
|
|
12
13
|
import {MiniappExitGesture} from "./MiniappExitGesture"
|
|
@@ -92,8 +93,9 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
92
93
|
// Nobody claimed a double tap: the glasses' own dashboard. On eligible
|
|
93
94
|
// Glassly CFW glasses the dashboard miniapp claims it while it runs
|
|
94
95
|
// (ADR-0006); stock firmware keeps its native dashboard (ADR-0002). The
|
|
95
|
-
// modal nav UI owns the screen; showDashboard would fight it.
|
|
96
|
-
|
|
96
|
+
// modal nav UI owns the screen; showDashboard would fight it. Without
|
|
97
|
+
// glasses there is no dashboard to open, and the mirror keeps its frame.
|
|
98
|
+
if (osDoubleTap && !nativeNavigationService.isActive && isGlassesConnected(useGlassesStore.getState().connection)) {
|
|
97
99
|
useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
|
|
98
100
|
void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
|
|
99
101
|
console.error("DeviceEventRouter: showDashboard failed:", err)
|
|
@@ -72,6 +72,7 @@ import overlayPages from "./pages/OverlayPages"
|
|
|
72
72
|
import dashboardOpenTrace from "./debug/DashboardOpenTrace"
|
|
73
73
|
import {isAppForegrounded, isPhoneActive, onAppForegroundChange, onPhoneActivityChange} from "./PhoneActivity"
|
|
74
74
|
import {installedNotificationApps} from "./PhoneNotificationsSync"
|
|
75
|
+
import {MiniappIcons} from "./MiniappIcons"
|
|
75
76
|
import {
|
|
76
77
|
DERIVED_SETTINGS_KEYS,
|
|
77
78
|
SETTINGS_BRIDGE_KEYS,
|
|
@@ -1719,7 +1720,7 @@ class LocalMiniappRuntime {
|
|
|
1719
1720
|
// Inter-miniapp interop (lifecycle is SYSTEM-only; actions are open,
|
|
1720
1721
|
// gated per-action by the target's declared visibility)
|
|
1721
1722
|
case MiniappRequestType.MINIAPPS_LIST:
|
|
1722
|
-
this.handleMiniappsList(packageName, payload, requestId)
|
|
1723
|
+
void this.handleMiniappsList(packageName, payload, requestId)
|
|
1723
1724
|
break
|
|
1724
1725
|
case MiniappRequestType.MINIAPPS_START:
|
|
1725
1726
|
void this.handleMiniappsStart(packageName, payload, requestId)
|
|
@@ -5330,12 +5331,14 @@ class LocalMiniappRuntime {
|
|
|
5330
5331
|
if (this.capabilitiesWired) return
|
|
5331
5332
|
this.capabilitiesWired = true
|
|
5332
5333
|
const broadcast = () => this.broadcastCapabilities()
|
|
5333
|
-
// Connection, model and firmware also decide the dashboard view's eligibility
|
|
5334
|
+
// Connection, model and firmware also decide the dashboard view's eligibility;
|
|
5335
|
+
// a connected Neural Band adds its swipe2d surface to `button`.
|
|
5334
5336
|
useGlassesStore.subscribe(
|
|
5335
5337
|
(s) =>
|
|
5336
|
-
`${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}`,
|
|
5338
|
+
`${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}:${s.controllerConnected}`,
|
|
5337
5339
|
broadcast,
|
|
5338
5340
|
)
|
|
5341
|
+
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.default_controller.key), broadcast)
|
|
5339
5342
|
// The debug knob flips hasNativeNavigation on custom firmware.
|
|
5340
5343
|
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast)
|
|
5341
5344
|
// The dashboard master switch flips the dashboard view.
|
|
@@ -5902,18 +5905,28 @@ class LocalMiniappRuntime {
|
|
|
5902
5905
|
}
|
|
5903
5906
|
}
|
|
5904
5907
|
|
|
5905
|
-
private
|
|
5908
|
+
private miniappIcons = new MiniappIcons()
|
|
5909
|
+
|
|
5910
|
+
private async handleMiniappsList(
|
|
5911
|
+
packageName: string,
|
|
5912
|
+
payload: Record<string, unknown>,
|
|
5913
|
+
requestId?: string,
|
|
5914
|
+
): Promise<void> {
|
|
5906
5915
|
if (!this.requireMiniappsPermission(packageName, requestId, "list")) return
|
|
5907
5916
|
const includeIncompatible = payload.includeIncompatible === true
|
|
5908
|
-
const
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
.map((
|
|
5917
|
+
const apps = this.interopApps().filter(
|
|
5918
|
+
(a) =>
|
|
5919
|
+
a.packageName &&
|
|
5920
|
+
a.packageName !== packageName &&
|
|
5921
|
+
!a.packageName.includes("@empty") &&
|
|
5922
|
+
(includeIncompatible || a.compatibility?.isCompatible !== false),
|
|
5923
|
+
)
|
|
5924
|
+
const infos = await Promise.all(
|
|
5925
|
+
apps.map(async (app) => ({
|
|
5926
|
+
...this.buildMiniappInfo(app),
|
|
5927
|
+
...(payload.includeIcons === true ? {icon: await this.miniappIcons.read(app)} : {}),
|
|
5928
|
+
})),
|
|
5929
|
+
)
|
|
5917
5930
|
this.sendResult(packageName, requestId, true, infos)
|
|
5918
5931
|
this.auditInterop({caller: packageName, op: "list", ok: true})
|
|
5919
5932
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {File} from "expo-file-system"
|
|
2
|
+
import {ImageManipulator, SaveFormat} from "expo-image-manipulator"
|
|
3
|
+
|
|
4
|
+
type IconApp = {packageName: string; version?: string; logoUrl: string}
|
|
5
|
+
|
|
6
|
+
/** Small thumbnails keep the inter-miniapp bridge and every grid frame lightweight. */
|
|
7
|
+
async function thumbnail(uri: string): Promise<string | undefined> {
|
|
8
|
+
const context = ImageManipulator.manipulate(uri)
|
|
9
|
+
let image: Awaited<ReturnType<typeof context.renderAsync>> | undefined
|
|
10
|
+
let savedUri: string | undefined
|
|
11
|
+
try {
|
|
12
|
+
context.resize({width: 32, height: 32})
|
|
13
|
+
image = await context.renderAsync()
|
|
14
|
+
const saved = await image.saveAsync({format: SaveFormat.PNG, base64: true})
|
|
15
|
+
savedUri = saved.uri
|
|
16
|
+
return saved.base64
|
|
17
|
+
} finally {
|
|
18
|
+
image?.release()
|
|
19
|
+
context.release()
|
|
20
|
+
if (savedUri) {
|
|
21
|
+
try {
|
|
22
|
+
new File(savedUri).delete()
|
|
23
|
+
} catch {
|
|
24
|
+
/* Already removed by cache cleanup. */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Cache by installed version and icon URL; failed icons never prevent listing apps. */
|
|
31
|
+
export class MiniappIcons {
|
|
32
|
+
private cache = new Map<string, {key: string; value: Promise<string | undefined>}>()
|
|
33
|
+
|
|
34
|
+
constructor(private readonly load = thumbnail) {}
|
|
35
|
+
|
|
36
|
+
read(app: IconApp): Promise<string | undefined> {
|
|
37
|
+
if (!app.logoUrl) return Promise.resolve(undefined)
|
|
38
|
+
const key = JSON.stringify([app.version, app.logoUrl])
|
|
39
|
+
const cached = this.cache.get(app.packageName)
|
|
40
|
+
if (cached?.key === key) return cached.value
|
|
41
|
+
const value = this.load(app.logoUrl).catch(() => {
|
|
42
|
+
if (this.cache.get(app.packageName)?.value === value) this.cache.delete(app.packageName)
|
|
43
|
+
return undefined
|
|
44
|
+
})
|
|
45
|
+
this.cache.set(app.packageName, {key, value})
|
|
46
|
+
// Bound memory even if apps are repeatedly installed and removed.
|
|
47
|
+
if (this.cache.size > 64) this.cache.delete(this.cache.keys().next().value!)
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -11,11 +11,22 @@
|
|
|
11
11
|
* lifted alongside the canvas, bounded only to keep frames sane. Firmware
|
|
12
12
|
* that draws vector shapes keeps 128 scene slots, so shape-heavy frames
|
|
13
13
|
* (a game board, a chart) get a larger shared text/shape budget.
|
|
14
|
+
*
|
|
15
|
+
* A connected controller that swipes in two dimensions (the Meta Neural Band)
|
|
16
|
+
* adds its surface to `button.buttons` as a `swipe2d` entry, so a miniapp can
|
|
17
|
+
* tell four-way swipes from the temples' and the R1 ring's up/down.
|
|
14
18
|
*/
|
|
15
19
|
import {useGlassesStore} from "../stores/glasses"
|
|
16
20
|
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
17
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ALL_DISPLAY_SHAPES,
|
|
23
|
+
DeviceTypes,
|
|
24
|
+
getModelCapabilities,
|
|
25
|
+
type ButtonCapabilities,
|
|
26
|
+
type Capabilities,
|
|
27
|
+
} from "../types"
|
|
18
28
|
import {resolveNativeNavigation} from "./nativeNavigationGate"
|
|
29
|
+
import {input2dFrom} from "./settingsBridge"
|
|
19
30
|
|
|
20
31
|
const CFW_MAX_TEXT_ELEMENTS = 24
|
|
21
32
|
const CFW_MAX_IMAGE_ELEMENTS = 16
|
|
@@ -36,17 +47,28 @@ function forceNativeNavigationOnCfw(): boolean {
|
|
|
36
47
|
return useSettingsStore.getState().getSetting(SETTINGS.cfw_native_navigation.key) === true
|
|
37
48
|
}
|
|
38
49
|
|
|
50
|
+
/** The connected firmware gates microphone streaming on volume (G2 Glassly CFW revision 40+). */
|
|
51
|
+
export function runtimeVolumeGate(): boolean {
|
|
52
|
+
return useGlassesStore.getState().volumeGate === true
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
export function getEffectiveCapabilities(model: DeviceTypes): Capabilities {
|
|
56
|
+
return withControllerInput(glassesCapabilities(model))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function glassesCapabilities(model: DeviceTypes): Capabilities {
|
|
40
60
|
const base = getModelCapabilities(model)
|
|
41
|
-
// Only models with a Glassly CFW publish a canvas
|
|
42
|
-
// reshape another model.
|
|
61
|
+
// Only models with a Glassly CFW publish a canvas or a volume gate; never
|
|
62
|
+
// let a stale value reshape another model.
|
|
43
63
|
const canvas = base.hasCustomFirmware ? runtimeSceneCanvas() : null
|
|
44
|
-
|
|
64
|
+
const volumeGate = base.hasCustomFirmware && base.hasMicrophone && runtimeVolumeGate()
|
|
65
|
+
if (!canvas) return volumeGate ? withVolumeGate(base) : base
|
|
66
|
+
if (!base.display) return volumeGate ? withVolumeGate(base) : base
|
|
45
67
|
// Glassly CFW supports the complete vector contract, including SVG and rotation.
|
|
46
68
|
// Upstream g2flash publishes a raster canvas with sceneVector=false.
|
|
47
69
|
const vector = useGlassesStore.getState().sceneVector === true
|
|
48
70
|
return {
|
|
49
|
-
...base,
|
|
71
|
+
...(volumeGate ? withVolumeGate(base) : base),
|
|
50
72
|
hasNativeNavigation: resolveNativeNavigation(base.hasNativeNavigation, true, forceNativeNavigationOnCfw()),
|
|
51
73
|
display: {
|
|
52
74
|
...base.display,
|
|
@@ -67,3 +89,40 @@ export function getEffectiveCapabilities(model: DeviceTypes): Capabilities {
|
|
|
67
89
|
},
|
|
68
90
|
}
|
|
69
91
|
}
|
|
92
|
+
|
|
93
|
+
type InputSurface = NonNullable<ButtonCapabilities["buttons"]>[number]
|
|
94
|
+
|
|
95
|
+
/** The Neural Band's thumb swipes, pinches and dial. */
|
|
96
|
+
const NEURAL_BAND_SURFACE: InputSurface = {
|
|
97
|
+
type: "swipe2d",
|
|
98
|
+
events: [
|
|
99
|
+
"TAP",
|
|
100
|
+
"DOUBLE_TAP",
|
|
101
|
+
"PRESS_HOLD",
|
|
102
|
+
"SWIPE_UP",
|
|
103
|
+
"SWIPE_DOWN",
|
|
104
|
+
"SWIPE_LEFT",
|
|
105
|
+
"SWIPE_RIGHT",
|
|
106
|
+
"DIAL_UP",
|
|
107
|
+
"DIAL_DOWN",
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function withControllerInput(caps: Capabilities): Capabilities {
|
|
112
|
+
const controller = useSettingsStore.getState().getSetting(SETTINGS.default_controller.key)
|
|
113
|
+
if (!input2dFrom(useGlassesStore.getState(), controller)) return caps
|
|
114
|
+
const buttons = caps.button?.buttons ?? []
|
|
115
|
+
return {
|
|
116
|
+
...caps,
|
|
117
|
+
hasButton: true,
|
|
118
|
+
button: {
|
|
119
|
+
...caps.button,
|
|
120
|
+
count: (caps.button?.count ?? buttons.length) + 1,
|
|
121
|
+
buttons: [...buttons, NEURAL_BAND_SURFACE],
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function withVolumeGate(base: Capabilities): Capabilities {
|
|
127
|
+
return {...base, microphone: {...(base.microphone ?? {}), volumeGate: true}}
|
|
128
|
+
}
|
package/src/stores/glasses.ts
CHANGED
package/src/stores/settings.ts
CHANGED
|
@@ -611,6 +611,40 @@ export const SETTINGS: Record<string, Setting> = {
|
|
|
611
611
|
saveOnServer: true,
|
|
612
612
|
persist: true,
|
|
613
613
|
},
|
|
614
|
+
// G2 Glassly CFW microphone volume gate (revision 40+): the temples stop
|
|
615
|
+
// streaming audio while nothing reaches the threshold. Off until the user
|
|
616
|
+
// turns it on; shown only when the connected firmware reports the capability.
|
|
617
|
+
volume_gate_enabled: {
|
|
618
|
+
key: "volume_gate_enabled",
|
|
619
|
+
defaultValue: () => false,
|
|
620
|
+
writable: true,
|
|
621
|
+
saveOnServer: true,
|
|
622
|
+
persist: true,
|
|
623
|
+
},
|
|
624
|
+
// dBFS RMS a 50 ms chunk must reach to count as sound.
|
|
625
|
+
volume_gate_threshold_db: {
|
|
626
|
+
key: "volume_gate_threshold_db",
|
|
627
|
+
defaultValue: () => -45,
|
|
628
|
+
writable: true,
|
|
629
|
+
saveOnServer: true,
|
|
630
|
+
persist: true,
|
|
631
|
+
},
|
|
632
|
+
// Quiet time before the gate closes.
|
|
633
|
+
volume_gate_hold_ms: {
|
|
634
|
+
key: "volume_gate_hold_ms",
|
|
635
|
+
defaultValue: () => 3000,
|
|
636
|
+
writable: true,
|
|
637
|
+
saveOnServer: true,
|
|
638
|
+
persist: true,
|
|
639
|
+
},
|
|
640
|
+
// Minimum time the gate stays open once sound opens it.
|
|
641
|
+
volume_gate_min_open_ms: {
|
|
642
|
+
key: "volume_gate_min_open_ms",
|
|
643
|
+
defaultValue: () => 2000,
|
|
644
|
+
writable: true,
|
|
645
|
+
saveOnServer: true,
|
|
646
|
+
persist: true,
|
|
647
|
+
},
|
|
614
648
|
always_on_status_bar: {
|
|
615
649
|
key: "always_on_status_bar",
|
|
616
650
|
defaultValue: () => false,
|
|
@@ -1108,6 +1142,10 @@ export const BLUETOOTH_SETTING_KEYS: string[] = [
|
|
|
1108
1142
|
SETTINGS.power_saving_mode.key,
|
|
1109
1143
|
SETTINGS.voice_activity_detection_enabled.key,
|
|
1110
1144
|
SETTINGS.loudness_gate_enabled.key,
|
|
1145
|
+
SETTINGS.volume_gate_enabled.key,
|
|
1146
|
+
SETTINGS.volume_gate_threshold_db.key,
|
|
1147
|
+
SETTINGS.volume_gate_hold_ms.key,
|
|
1148
|
+
SETTINGS.volume_gate_min_open_ms.key,
|
|
1111
1149
|
SETTINGS.lc3_frame_size.key,
|
|
1112
1150
|
SETTINGS.preferred_mic.key,
|
|
1113
1151
|
SETTINGS.screen_disabled.key,
|
package/src/types/hardware.ts
CHANGED
|
@@ -95,6 +95,10 @@ export const ALL_DISPLAY_SHAPES: DisplayShape[] = ["rect", "circle", "arc", "pie
|
|
|
95
95
|
export interface MicrophoneCapabilities {
|
|
96
96
|
count?: number
|
|
97
97
|
hasVAD?: boolean // Voice Activity Detection
|
|
98
|
+
// The connected firmware can stop streaming audio below a volume floor
|
|
99
|
+
// (G2 Glassly CFW revision 40+). Never set on a model profile: it is
|
|
100
|
+
// reported at runtime and layered on by getEffectiveCapabilities.
|
|
101
|
+
volumeGate?: boolean
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
/**
|