@glassly/engine 0.3.3-dev.621 → 0.3.3-dev.623

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.
@@ -1 +1 @@
1
- {"version":3,"file":"effectiveCapabilities.d.ts","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAqB,WAAW,EAAwB,KAAK,YAAY,EAAC,MAAM,UAAU,CAAA;AAOjG,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CAI3D;AAMD,iGAAiG;AACjG,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAgCzE"}
1
+ {"version":3,"file":"effectiveCapabilities.d.ts","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"AAoBA,OAAO,EAEL,WAAW,EAGX,KAAK,YAAY,EAClB,MAAM,UAAU,CAAA;AAQjB,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CAI3D;AAMD,iGAAiG;AACjG,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAEzE"}
@@ -11,11 +11,16 @@
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 { ALL_DISPLAY_SHAPES, getModelCapabilities } from "../types";
21
+ import { ALL_DISPLAY_SHAPES, getModelCapabilities, } from "../types";
18
22
  import { resolveNativeNavigation } from "./nativeNavigationGate";
23
+ import { input2dFrom } from "./settingsBridge";
19
24
  const CFW_MAX_TEXT_ELEMENTS = 24;
20
25
  const CFW_MAX_IMAGE_ELEMENTS = 16;
21
26
  const CFW_VECTOR_MAX_ELEMENTS = 120;
@@ -33,6 +38,9 @@ export function runtimeVolumeGate() {
33
38
  return useGlassesStore.getState().volumeGate === true;
34
39
  }
35
40
  export function getEffectiveCapabilities(model) {
41
+ return withControllerInput(glassesCapabilities(model));
42
+ }
43
+ function glassesCapabilities(model) {
36
44
  const base = getModelCapabilities(model);
37
45
  // Only models with a Glassly CFW publish a canvas or a volume gate; never
38
46
  // let a stale value reshape another model.
@@ -61,6 +69,36 @@ export function getEffectiveCapabilities(model) {
61
69
  },
62
70
  };
63
71
  }
72
+ /** The Neural Band's thumb swipes, pinches and dial. */
73
+ const NEURAL_BAND_SURFACE = {
74
+ type: "swipe2d",
75
+ events: [
76
+ "TAP",
77
+ "DOUBLE_TAP",
78
+ "PRESS_HOLD",
79
+ "SWIPE_UP",
80
+ "SWIPE_DOWN",
81
+ "SWIPE_LEFT",
82
+ "SWIPE_RIGHT",
83
+ "DIAL_UP",
84
+ "DIAL_DOWN",
85
+ ],
86
+ };
87
+ function withControllerInput(caps) {
88
+ const controller = useSettingsStore.getState().getSetting(SETTINGS.default_controller.key);
89
+ if (!input2dFrom(useGlassesStore.getState(), controller))
90
+ return caps;
91
+ const buttons = caps.button?.buttons ?? [];
92
+ return {
93
+ ...caps,
94
+ hasButton: true,
95
+ button: {
96
+ ...caps.button,
97
+ count: (caps.button?.count ?? buttons.length) + 1,
98
+ buttons: [...buttons, NEURAL_BAND_SURFACE],
99
+ },
100
+ };
101
+ }
64
102
  function withVolumeGate(base) {
65
103
  return { ...base, microphone: { ...(base.microphone ?? {}), volumeGate: true } };
66
104
  }
@@ -1 +1 @@
1
- {"version":3,"file":"effectiveCapabilities.js","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Effective device capabilities — the static model profile plus what the\n * connected firmware reports at runtime. The runtime override is the scene\n * canvas: Glassly CFW on the G2 rasterizes scenes phone-side straight into\n * the 640×480 panel, so the drawable canvas grows past stock's 576×288; on\n * the RayNeo iO it opens a 540×180 page that stock firmware has no\n * equivalent of. Any model whose profile declares `hasCustomFirmware` reads it.\n * Native publishes the canvas into the glasses status (0 = none). Because\n * that path is phone-side rasterization, the stock firmware's container\n * pools (6 text / 4 image) don't bound a CFW frame either — element caps are\n * lifted alongside the canvas, bounded only to keep frames sane. Firmware\n * that draws vector shapes keeps 128 scene slots, so shape-heavy frames\n * (a game board, a chart) get a larger shared text/shape budget.\n */\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {ALL_DISPLAY_SHAPES, DeviceTypes, getModelCapabilities, type Capabilities} from \"../types\"\nimport {resolveNativeNavigation} from \"./nativeNavigationGate\"\n\nconst CFW_MAX_TEXT_ELEMENTS = 24\nconst CFW_MAX_IMAGE_ELEMENTS = 16\nconst CFW_VECTOR_MAX_ELEMENTS = 120\n\nexport interface SceneCanvasSize {\n width: number\n height: number\n}\n\nexport function runtimeSceneCanvas(): SceneCanvasSize | null {\n const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()\n if (!sceneCanvasWidth || !sceneCanvasHeight || sceneCanvasWidth <= 0 || sceneCanvasHeight <= 0) return null\n return {width: sceneCanvasWidth, height: sceneCanvasHeight}\n}\n\nfunction forceNativeNavigationOnCfw(): boolean {\n return useSettingsStore.getState().getSetting(SETTINGS.cfw_native_navigation.key) === true\n}\n\n/** The connected firmware gates microphone streaming on volume (G2 Glassly CFW revision 40+). */\nexport function runtimeVolumeGate(): boolean {\n return useGlassesStore.getState().volumeGate === true\n}\n\nexport function getEffectiveCapabilities(model: DeviceTypes): Capabilities {\n const base = getModelCapabilities(model)\n // Only models with a Glassly CFW publish a canvas or a volume gate; never\n // let a stale value reshape another model.\n const canvas = base.hasCustomFirmware ? runtimeSceneCanvas() : null\n const volumeGate = base.hasCustomFirmware && base.hasMicrophone && runtimeVolumeGate()\n if (!canvas) return volumeGate ? withVolumeGate(base) : base\n if (!base.display) return volumeGate ? withVolumeGate(base) : base\n // Glassly CFW supports the complete vector contract, including SVG and rotation.\n // Upstream g2flash publishes a raster canvas with sceneVector=false.\n const vector = useGlassesStore.getState().sceneVector === true\n return {\n ...(volumeGate ? withVolumeGate(base) : base),\n hasNativeNavigation: resolveNativeNavigation(base.hasNativeNavigation, true, forceNativeNavigationOnCfw()),\n display: {\n ...base.display,\n width: canvas.width,\n height: canvas.height,\n maxTextElements: Math.max(\n base.display.maxTextElements ?? 0,\n vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_TEXT_ELEMENTS,\n ),\n // Vector firmware draws an image as one cached slot, the same as a shape.\n maxImageElements: Math.max(\n base.display.maxImageElements ?? 0,\n vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_IMAGE_ELEMENTS,\n ),\n ...(vector\n ? {shapes: [...ALL_DISPLAY_SHAPES, \"svg\" as const], animation: true, rotation: true, depth: true}\n : {}),\n },\n }\n}\n\nfunction withVolumeGate(base: Capabilities): Capabilities {\n return {...base, microphone: {...(base.microphone ?? {}), volumeGate: true}}\n}\n"]}
1
+ {"version":3,"file":"effectiveCapabilities.js","sourceRoot":"","sources":["../../src/services/effectiveCapabilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Effective device capabilities — the static model profile plus what the\n * connected firmware reports at runtime. The runtime override is the scene\n * canvas: Glassly CFW on the G2 rasterizes scenes phone-side straight into\n * the 640×480 panel, so the drawable canvas grows past stock's 576×288; on\n * the RayNeo iO it opens a 540×180 page that stock firmware has no\n * equivalent of. Any model whose profile declares `hasCustomFirmware` reads it.\n * Native publishes the canvas into the glasses status (0 = none). Because\n * that path is phone-side rasterization, the stock firmware's container\n * pools (6 text / 4 image) don't bound a CFW frame either — element caps are\n * lifted alongside the canvas, bounded only to keep frames sane. Firmware\n * that draws vector shapes keeps 128 scene slots, so shape-heavy frames\n * (a game board, a chart) get a larger shared text/shape budget.\n *\n * A connected controller that swipes in two dimensions (the Meta Neural Band)\n * adds its surface to `button.buttons` as a `swipe2d` entry, so a miniapp can\n * tell four-way swipes from the temples' and the R1 ring's up/down.\n */\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {\n ALL_DISPLAY_SHAPES,\n DeviceTypes,\n getModelCapabilities,\n type ButtonCapabilities,\n type Capabilities,\n} from \"../types\"\nimport {resolveNativeNavigation} from \"./nativeNavigationGate\"\nimport {input2dFrom} from \"./settingsBridge\"\n\nconst CFW_MAX_TEXT_ELEMENTS = 24\nconst CFW_MAX_IMAGE_ELEMENTS = 16\nconst CFW_VECTOR_MAX_ELEMENTS = 120\n\nexport interface SceneCanvasSize {\n width: number\n height: number\n}\n\nexport function runtimeSceneCanvas(): SceneCanvasSize | null {\n const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()\n if (!sceneCanvasWidth || !sceneCanvasHeight || sceneCanvasWidth <= 0 || sceneCanvasHeight <= 0) return null\n return {width: sceneCanvasWidth, height: sceneCanvasHeight}\n}\n\nfunction forceNativeNavigationOnCfw(): boolean {\n return useSettingsStore.getState().getSetting(SETTINGS.cfw_native_navigation.key) === true\n}\n\n/** The connected firmware gates microphone streaming on volume (G2 Glassly CFW revision 40+). */\nexport function runtimeVolumeGate(): boolean {\n return useGlassesStore.getState().volumeGate === true\n}\n\nexport function getEffectiveCapabilities(model: DeviceTypes): Capabilities {\n return withControllerInput(glassesCapabilities(model))\n}\n\nfunction glassesCapabilities(model: DeviceTypes): Capabilities {\n const base = getModelCapabilities(model)\n // Only models with a Glassly CFW publish a canvas or a volume gate; never\n // let a stale value reshape another model.\n const canvas = base.hasCustomFirmware ? runtimeSceneCanvas() : null\n const volumeGate = base.hasCustomFirmware && base.hasMicrophone && runtimeVolumeGate()\n if (!canvas) return volumeGate ? withVolumeGate(base) : base\n if (!base.display) return volumeGate ? withVolumeGate(base) : base\n // Glassly CFW supports the complete vector contract, including SVG and rotation.\n // Upstream g2flash publishes a raster canvas with sceneVector=false.\n const vector = useGlassesStore.getState().sceneVector === true\n return {\n ...(volumeGate ? withVolumeGate(base) : base),\n hasNativeNavigation: resolveNativeNavigation(base.hasNativeNavigation, true, forceNativeNavigationOnCfw()),\n display: {\n ...base.display,\n width: canvas.width,\n height: canvas.height,\n maxTextElements: Math.max(\n base.display.maxTextElements ?? 0,\n vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_TEXT_ELEMENTS,\n ),\n // Vector firmware draws an image as one cached slot, the same as a shape.\n maxImageElements: Math.max(\n base.display.maxImageElements ?? 0,\n vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_IMAGE_ELEMENTS,\n ),\n ...(vector\n ? {shapes: [...ALL_DISPLAY_SHAPES, \"svg\" as const], animation: true, rotation: true, depth: true}\n : {}),\n },\n }\n}\n\ntype InputSurface = NonNullable<ButtonCapabilities[\"buttons\"]>[number]\n\n/** The Neural Band's thumb swipes, pinches and dial. */\nconst NEURAL_BAND_SURFACE: InputSurface = {\n type: \"swipe2d\",\n events: [\n \"TAP\",\n \"DOUBLE_TAP\",\n \"PRESS_HOLD\",\n \"SWIPE_UP\",\n \"SWIPE_DOWN\",\n \"SWIPE_LEFT\",\n \"SWIPE_RIGHT\",\n \"DIAL_UP\",\n \"DIAL_DOWN\",\n ],\n}\n\nfunction withControllerInput(caps: Capabilities): Capabilities {\n const controller = useSettingsStore.getState().getSetting(SETTINGS.default_controller.key)\n if (!input2dFrom(useGlassesStore.getState(), controller)) return caps\n const buttons = caps.button?.buttons ?? []\n return {\n ...caps,\n hasButton: true,\n button: {\n ...caps.button,\n count: (caps.button?.count ?? buttons.length) + 1,\n buttons: [...buttons, NEURAL_BAND_SURFACE],\n },\n }\n}\n\nfunction withVolumeGate(base: Capabilities): Capabilities {\n return {...base, microphone: {...(base.microphone ?? {}), volumeGate: true}}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.3-dev.621",
3
+ "version": "0.3.3-dev.623",
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.3-dev.621",
101
- "@glassly/cloud-client": "0.3.3-dev.621",
102
- "@glassly/cloud-protocol": "0.3.3-dev.621",
103
- "@glassly/crust": "0.3.3-dev.621",
104
- "@glassly/miniapp": "0.3.3-dev.621",
100
+ "@glassly/bluetooth-sdk": "0.3.3-dev.623",
101
+ "@glassly/cloud-client": "0.3.3-dev.623",
102
+ "@glassly/cloud-protocol": "0.3.3-dev.623",
103
+ "@glassly/crust": "0.3.3-dev.623",
104
+ "@glassly/miniapp": "0.3.3-dev.623",
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.3-dev.621"
110
+ "@glassly/llm-providers": "0.3.3-dev.623"
111
111
  },
112
112
  "devDependencies": {
113
113
  "@glassly/llm-providers": "workspace:*",
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "0.3.3",
15
- "releaseIdentity": "0.3.3-dev.621",
16
- "releaseSetId": "glassly-0.3.3-dev.621",
17
- "sourceCommit": "ff7024d16c4a0a99136ab16fd1f13a743d25ac30",
15
+ "releaseIdentity": "0.3.3-dev.623",
16
+ "releaseSetId": "glassly-0.3.3-dev.623",
17
+ "sourceCommit": "6eb106fb7c84c6b17cd3d46557f75bb81a23f191",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
@@ -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
- * The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from
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. Never restart its visual feedback.
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
- // The band's resolved tap arrives after release (double-tap window).
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
- if (press?.raw && (!glyph || gestureName === "long_press")) {
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 (glyph) press.glyph = glyph
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
- if (osDoubleTap && !nativeNavigationService.isActive) {
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)
@@ -5330,12 +5330,14 @@ class LocalMiniappRuntime {
5330
5330
  if (this.capabilitiesWired) return
5331
5331
  this.capabilitiesWired = true
5332
5332
  const broadcast = () => this.broadcastCapabilities()
5333
- // Connection, model and firmware also decide the dashboard view's eligibility.
5333
+ // Connection, model and firmware also decide the dashboard view's eligibility;
5334
+ // a connected Neural Band adds its swipe2d surface to `button`.
5334
5335
  useGlassesStore.subscribe(
5335
5336
  (s) =>
5336
- `${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}`,
5337
+ `${s.sceneCanvasWidth ?? 0}x${s.sceneCanvasHeight ?? 0}:${s.sceneVector === true}:${s.firmwareVersion}:${s.connection.state}:${s.deviceModel}:${s.controllerConnected}`,
5337
5338
  broadcast,
5338
5339
  )
5340
+ useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.default_controller.key), broadcast)
5339
5341
  // The debug knob flips hasNativeNavigation on custom firmware.
5340
5342
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast)
5341
5343
  // The dashboard master switch flips the dashboard view.
@@ -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 {ALL_DISPLAY_SHAPES, DeviceTypes, getModelCapabilities, type Capabilities} from "../types"
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
@@ -42,6 +53,10 @@ export function runtimeVolumeGate(): boolean {
42
53
  }
43
54
 
44
55
  export function getEffectiveCapabilities(model: DeviceTypes): Capabilities {
56
+ return withControllerInput(glassesCapabilities(model))
57
+ }
58
+
59
+ function glassesCapabilities(model: DeviceTypes): Capabilities {
45
60
  const base = getModelCapabilities(model)
46
61
  // Only models with a Glassly CFW publish a canvas or a volume gate; never
47
62
  // let a stale value reshape another model.
@@ -75,6 +90,39 @@ export function getEffectiveCapabilities(model: DeviceTypes): Capabilities {
75
90
  }
76
91
  }
77
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
+
78
126
  function withVolumeGate(base: Capabilities): Capabilities {
79
127
  return {...base, microphone: {...(base.microphone ?? {}), volumeGate: true}}
80
128
  }