@glassly/engine 0.3.2-dev.586 → 0.3.2-dev.596

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.
Files changed (70) hide show
  1. package/build/engine.d.ts +14 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +4 -0
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/glasses.d.ts +20 -0
  6. package/build/facades/glasses.d.ts.map +1 -1
  7. package/build/facades/glasses.js +21 -0
  8. package/build/facades/glasses.js.map +1 -1
  9. package/build/facades/pairing.js +1 -1
  10. package/build/facades/pairing.js.map +1 -1
  11. package/build/generated/releaseMetadata.js +3 -3
  12. package/build/generated/releaseMetadata.js.map +1 -1
  13. package/build/index.d.ts +2 -0
  14. package/build/index.d.ts.map +1 -1
  15. package/build/index.js +2 -0
  16. package/build/index.js.map +1 -1
  17. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  18. package/build/services/DeviceEventRouter.js +5 -0
  19. package/build/services/DeviceEventRouter.js.map +1 -1
  20. package/build/services/GestureDotsService.d.ts +9 -9
  21. package/build/services/GestureDotsService.d.ts.map +1 -1
  22. package/build/services/GestureDotsService.js +75 -50
  23. package/build/services/GestureDotsService.js.map +1 -1
  24. package/build/services/GlassesTouchInput.d.ts +1 -1
  25. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  26. package/build/services/GlassesTouchInput.js +7 -4
  27. package/build/services/GlassesTouchInput.js.map +1 -1
  28. package/build/services/gestureDotGlyphs.d.ts +1 -1
  29. package/build/services/gestureDotGlyphs.d.ts.map +1 -1
  30. package/build/services/gestureDotGlyphs.js +5 -0
  31. package/build/services/gestureDotGlyphs.js.map +1 -1
  32. package/build/services/neuralBand/MetaAuth.d.ts +31 -0
  33. package/build/services/neuralBand/MetaAuth.d.ts.map +1 -0
  34. package/build/services/neuralBand/MetaAuth.js +161 -0
  35. package/build/services/neuralBand/MetaAuth.js.map +1 -0
  36. package/build/services/neuralBand/NeuralBandInput.d.ts +50 -0
  37. package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -0
  38. package/build/services/neuralBand/NeuralBandInput.js +114 -0
  39. package/build/services/neuralBand/NeuralBandInput.js.map +1 -0
  40. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts +3 -0
  41. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -0
  42. package/build/services/neuralBand/NeuralBandSettingsSync.js +52 -0
  43. package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -0
  44. package/build/services/neuralBand/sha256.d.ts +7 -0
  45. package/build/services/neuralBand/sha256.d.ts.map +1 -0
  46. package/build/services/neuralBand/sha256.js +68 -0
  47. package/build/services/neuralBand/sha256.js.map +1 -0
  48. package/build/stores/settings.d.ts.map +1 -1
  49. package/build/stores/settings.js +18 -0
  50. package/build/stores/settings.js.map +1 -1
  51. package/build/types/enums.d.ts +2 -1
  52. package/build/types/enums.d.ts.map +1 -1
  53. package/build/types/enums.js +1 -0
  54. package/build/types/enums.js.map +1 -1
  55. package/package.json +7 -7
  56. package/src/engine.ts +4 -0
  57. package/src/facades/glasses.ts +27 -0
  58. package/src/facades/pairing.ts +1 -1
  59. package/src/generated/releaseMetadata.ts +3 -3
  60. package/src/index.ts +14 -0
  61. package/src/services/DeviceEventRouter.ts +5 -0
  62. package/src/services/GestureDotsService.ts +72 -49
  63. package/src/services/GlassesTouchInput.ts +13 -5
  64. package/src/services/gestureDotGlyphs.ts +6 -1
  65. package/src/services/neuralBand/MetaAuth.ts +192 -0
  66. package/src/services/neuralBand/NeuralBandInput.ts +148 -0
  67. package/src/services/neuralBand/NeuralBandSettingsSync.ts +63 -0
  68. package/src/services/neuralBand/sha256.ts +70 -0
  69. package/src/stores/settings.ts +18 -0
  70. package/src/types/enums.ts +1 -0
@@ -17,5 +17,6 @@ export var DeviceTypes;
17
17
  export var ControllerTypes;
18
18
  (function (ControllerTypes) {
19
19
  ControllerTypes["R1"] = "Even Realities R1";
20
+ ControllerTypes["NEURAL_BAND"] = "Meta Neural Band";
20
21
  })(ControllerTypes || (ControllerTypes = {}));
21
22
  //# sourceMappingURL=enums.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"enums.js","sourceRoot":"","sources":["../../src/types/enums.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,wBAAwB,EAAC,MAAM,2BAA2B,CAAA;AAEhF,MAAM,CAAN,IAAY,WAaX;AAbD,WAAY,WAAW;IACrB,4BAAa,CAAA;IACb,8CAA+B,CAAA;IAC/B,4BAAa,CAAA;IACb,uCAAwB,CAAA;IACxB,uCAAwB,CAAA;IACxB,mCAAoB,CAAA;IACpB,qCAAsB,CAAA;IACtB,kCAAmB,CAAA;IACnB,qCAAsB,CAAA;IACtB,4BAAa,CAAA;IACb,wCAAyB,CAAA;IACzB,sCAAuB,CAAA;AACzB,CAAC,EAbW,WAAW,KAAX,WAAW,QAatB;AAED,MAAM,CAAN,IAAY,eAEX;AAFD,WAAY,eAAe;IACzB,2CAAwB,CAAA;AAC1B,CAAC,EAFW,eAAe,KAAf,eAAe,QAE1B","sourcesContent":["export {HardwareType, HardwareRequirementLevel} from \"@glassly/miniapp/hardware\"\n\nexport enum DeviceTypes {\n NONE = \"None\",\n SIMULATED = \"Simulated Glasses\",\n AR99 = \"AR99\",\n G1 = \"Even Realities G1\",\n G2 = \"Even Realities G2\",\n LIVE = \"Mentra Live\",\n MACH1 = \"Mentra Mach1\",\n Z100 = \"Vuzix Z100\",\n NEX = \"Mentra Display\",\n NIMO = \"NIMO\",\n FRAME = \"Brilliant Frame\",\n RAYNEO_IO = \"RayNeo iO\",\n}\n\nexport enum ControllerTypes {\n R1 = \"Even Realities R1\",\n}\n"]}
1
+ {"version":3,"file":"enums.js","sourceRoot":"","sources":["../../src/types/enums.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,wBAAwB,EAAC,MAAM,2BAA2B,CAAA;AAEhF,MAAM,CAAN,IAAY,WAaX;AAbD,WAAY,WAAW;IACrB,4BAAa,CAAA;IACb,8CAA+B,CAAA;IAC/B,4BAAa,CAAA;IACb,uCAAwB,CAAA;IACxB,uCAAwB,CAAA;IACxB,mCAAoB,CAAA;IACpB,qCAAsB,CAAA;IACtB,kCAAmB,CAAA;IACnB,qCAAsB,CAAA;IACtB,4BAAa,CAAA;IACb,wCAAyB,CAAA;IACzB,sCAAuB,CAAA;AACzB,CAAC,EAbW,WAAW,KAAX,WAAW,QAatB;AAED,MAAM,CAAN,IAAY,eAGX;AAHD,WAAY,eAAe;IACzB,2CAAwB,CAAA;IACxB,mDAAgC,CAAA;AAClC,CAAC,EAHW,eAAe,KAAf,eAAe,QAG1B","sourcesContent":["export {HardwareType, HardwareRequirementLevel} from \"@glassly/miniapp/hardware\"\n\nexport enum DeviceTypes {\n NONE = \"None\",\n SIMULATED = \"Simulated Glasses\",\n AR99 = \"AR99\",\n G1 = \"Even Realities G1\",\n G2 = \"Even Realities G2\",\n LIVE = \"Mentra Live\",\n MACH1 = \"Mentra Mach1\",\n Z100 = \"Vuzix Z100\",\n NEX = \"Mentra Display\",\n NIMO = \"NIMO\",\n FRAME = \"Brilliant Frame\",\n RAYNEO_IO = \"RayNeo iO\",\n}\n\nexport enum ControllerTypes {\n R1 = \"Even Realities R1\",\n NEURAL_BAND = \"Meta Neural Band\",\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.2-dev.586",
3
+ "version": "0.3.2-dev.596",
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.2-dev.586",
101
- "@glassly/cloud-client": "0.3.2-dev.586",
102
- "@glassly/cloud-protocol": "0.3.2-dev.586",
103
- "@glassly/crust": "0.3.2-dev.586",
104
- "@glassly/miniapp": "0.3.2-dev.586",
100
+ "@glassly/bluetooth-sdk": "0.3.2-dev.596",
101
+ "@glassly/cloud-client": "0.3.2-dev.596",
102
+ "@glassly/cloud-protocol": "0.3.2-dev.596",
103
+ "@glassly/crust": "0.3.2-dev.596",
104
+ "@glassly/miniapp": "0.3.2-dev.596",
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.2-dev.586"
110
+ "@glassly/llm-providers": "0.3.2-dev.596"
111
111
  },
112
112
  "devDependencies": {
113
113
  "@glassly/llm-providers": "workspace:*",
package/src/engine.ts CHANGED
@@ -23,6 +23,7 @@ import {startDeviceEventRouter, stopDeviceEventRouter} from "./services/DeviceEv
23
23
  import {startPhoneActivityTracking, stopPhoneActivityTracking} from "./services/PhoneActivity"
24
24
  import {startPhoneNotificationsSync, stopPhoneNotificationsSync} from "./services/PhoneNotificationsSync"
25
25
  import {startSpeakerIdSync, stopSpeakerIdSync} from "./services/SpeakerIdSync"
26
+ import {startNeuralBandSettingsSync, stopNeuralBandSettingsSync} from "./services/neuralBand/NeuralBandSettingsSync"
26
27
  import {startCaptionsTesterReportService, stopCaptionsTesterReportService} from "./services/CaptionsTesterReportService"
27
28
  import {startJSCrashloopReportService, stopJSCrashloopReportService} from "./services/JSCrashloopReportService"
28
29
  import {ensureMiniappEngine, stopMiniappEngine} from "./services/MiniappEngine"
@@ -133,6 +134,8 @@ export const engine = {
133
134
  // profile centroids to the native SpeakerIdEngine, and start the resolver
134
135
  // that annotates cloud transcripts with resolved speaker identities.
135
136
  startSpeakerIdSync()
137
+ // Meta Neural Band: push the air mouse and wearing-hand settings to the band on connect.
138
+ startNeuralBandSettingsSync()
136
139
  // Android internal/e2e: laptop captions tester can broadcast a failure intent;
137
140
  // engine owns turning that into a Cloud report.
138
141
  startCaptionsTesterReportService()
@@ -176,6 +179,7 @@ export const engine = {
176
179
  await safely("phone notifications sync", stopPhoneNotificationsSync)
177
180
  await safely("phone activity tracking", stopPhoneActivityTracking)
178
181
  await safely("speaker id sync", stopSpeakerIdSync)
182
+ await safely("neural band settings sync", stopNeuralBandSettingsSync)
179
183
  await safely("captions tester report service", stopCaptionsTesterReportService)
180
184
  await safely("js crashloop report service", stopJSCrashloopReportService)
181
185
  await safely("miniapp engine", stopMiniappEngine)
@@ -25,6 +25,10 @@ import {hasDefaultDevice} from "../services/DeviceStoreHydration"
25
25
  import {projectPairingIdentity} from "../services/PairingIdentity"
26
26
  import {isGlassesConnected, isGlassesReady} from "../services/GlassesReadiness"
27
27
  import {pushAllBluetoothSettings} from "../services/GlassesSettingsSync"
28
+ import neuralBandInput, {
29
+ type NeuralBandClaimStatus,
30
+ type NeuralBandHandwritingStatus,
31
+ } from "../services/neuralBand/NeuralBandInput"
28
32
  import {recordSupportProfileConnectionFailure} from "../services/SupportProfileSync"
29
33
  import {getModelCapabilities, type DeviceTypes} from "../types"
30
34
  import {glassesWifi} from "./glassesWifi"
@@ -267,6 +271,29 @@ export const glasses = {
267
271
  },
268
272
  },
269
273
 
274
+ // --- Meta Neural Band (a controller with a claim step and its own features) ---
275
+ neuralBand: {
276
+ /** Store the Meta account session the one-time band claim uses. */
277
+ setMetaSession: (accessToken: string, userId: string): Promise<void> =>
278
+ BluetoothSdk.neuralBandSetMetaSession(accessToken, userId),
279
+ clearMetaSession: (): Promise<void> => BluetoothSdk.neuralBandClearMetaSession(),
280
+ hasMetaSession: (): Promise<boolean> => BluetoothSdk.neuralBandHasMetaSession(),
281
+ /** A band identity is stored on this phone, so the remembered band connects without a claim. */
282
+ isClaimed: (): Promise<boolean> => BluetoothSdk.neuralBandIsClaimed(),
283
+ /** The latest claim progress, kept across screens. */
284
+ claimStatus: (): NeuralBandClaimStatus => neuralBandInput.claimStatus(),
285
+ resetClaim: (): void => neuralBandInput.resetClaim(),
286
+ onClaimStatus: (cb: (status: NeuralBandClaimStatus) => void): (() => void) => neuralBandInput.onClaimStatus(cb),
287
+ /** The band's handwriting model; writing arrives on the miniapp keyboard stream. */
288
+ setHandwriting: (enabled: boolean): Promise<void> => BluetoothSdk.neuralBandSetHandwriting(enabled),
289
+ handwritingStatus: (): NeuralBandHandwritingStatus => neuralBandInput.handwritingStatus(),
290
+ onHandwritingStatus: (cb: (status: NeuralBandHandwritingStatus) => void): (() => void) =>
291
+ neuralBandInput.onHandwritingStatus(cb),
292
+ /** The band's air mouse; movement reaches miniapps on the pointer stream. */
293
+ setPointer: (enabled: boolean): Promise<void> => BluetoothSdk.neuralBandSetPointer(enabled),
294
+ setHand: (hand: "left" | "right"): Promise<void> => BluetoothSdk.neuralBandSetHand(hand),
295
+ },
296
+
270
297
  // --- audio (glasses media-volume + own-app playback hint) ---
271
298
  audio: {
272
299
  /** Read the glasses' current media volume. */
@@ -63,7 +63,7 @@ function projectTargetReady(options: Pick<PairingReadyWaitOptions, "deviceModel"
63
63
  const {deviceModel, deviceName} = options
64
64
  const glasses = useGlassesStore.getState()
65
65
 
66
- if (deviceModel === ControllerTypes.R1) {
66
+ if (deviceModel !== undefined && (Object.values(ControllerTypes) as string[]).includes(deviceModel)) {
67
67
  if (!glasses.controllerConnected || !glasses.controllerFullyBooted) return false
68
68
  const settings = useSettingsStore.getState()
69
69
  const pairedModel = settings.getSetting(SETTINGS.default_controller.key)
@@ -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.2",
15
- "releaseIdentity": "0.3.2-dev.586",
16
- "releaseSetId": "glassly-0.3.2-dev.586",
17
- "sourceCommit": "d6fda6d4ab62929d5449b0fee51cb3ac22e92be5",
15
+ "releaseIdentity": "0.3.2-dev.596",
16
+ "releaseSetId": "glassly-0.3.2-dev.596",
17
+ "sourceCommit": "4bbd4945a62883e5f384569b0f5c56a51e5c81cd",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
package/src/index.ts CHANGED
@@ -201,6 +201,20 @@ export {
201
201
  } from "./utils/devMiniappLaunch"
202
202
  export {HardwareCompatibility, type CompatibilityResult} from "./utils/hardware"
203
203
  export {BgTimer, throttle, debounce} from "./utils/timers"
204
+ // Meta Neural Band: the account sign-in a claim needs (the WebView lives in the host).
205
+ export {
206
+ MetaAuthError,
207
+ authEntryUrl,
208
+ callbackMatches,
209
+ callbackQuery,
210
+ decryptBlob,
211
+ isLoginCallback,
212
+ login,
213
+ tokensQuery,
214
+ type MetaSession,
215
+ type MetaSsoTokens,
216
+ } from "./services/neuralBand/MetaAuth"
217
+ export type {NeuralBandClaimStatus, NeuralBandHandwritingStatus} from "./services/neuralBand/NeuralBandInput"
204
218
 
205
219
  // Hardware types and capability profiles.
206
220
  export {
@@ -35,6 +35,7 @@ import {retirePendingSelectionOnPromotion} from "./PairingIdentity"
35
35
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
36
36
  import {asgCameraApi} from "./asg/asgCameraApi"
37
37
  import {dispatchTouchEvent, resetTouchInput} from "./GlassesTouchInput"
38
+ import neuralBandInput from "./neuralBand/NeuralBandInput"
38
39
  import dashboardOpenTrace from "./debug/DashboardOpenTrace"
39
40
 
40
41
  let subs: Array<{remove: () => void}> = []
@@ -233,6 +234,10 @@ export function startDeviceEventRouter(): void {
233
234
  // Physical keyboard bridged through G2 Glassly CFW: raw HID transitions →
234
235
  // layout-translated keyboard_event stream (with synthesized auto-repeat).
235
236
  subs.push(BluetoothSdk.addListener("keyboard_key", (event) => glassesKeyboardInput.handle(event)))
237
+ // Meta Neural Band: its gestures ride touch_event above; these are its own streams.
238
+ subs.push(BluetoothSdk.addListener("neural_band_claim", (event) => neuralBandInput.handleClaim(event)))
239
+ subs.push(BluetoothSdk.addListener("neural_band_handwriting", (event) => neuralBandInput.handleHandwriting(event)))
240
+ subs.push(BluetoothSdk.addListener("neural_band_pointer", (event) => neuralBandInput.handlePointer(event)))
236
241
  subs.push({
237
242
  remove: useGlassesStore.subscribe(
238
243
  (s) => s.keyboardConnected === true,
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Gesture dots — a debug indicator that flashes a small glyph top-right on the
3
3
  * glasses when a touch gesture arrives, while nothing else is on the display:
4
- * a filled circle for a single tap, an arrow for a swipe (cleared after a
5
- * short pulse), a bullseye while a CFW tap-then-hold is held. Extracted from
4
+ * a filled circle for a single tap, two for a double tap, an arrow for a swipe
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
7
  * the CFW dashboard service; it renders through
7
8
  * the ordinary scene pipeline, so it works on any positioning device (stock
8
9
  * G2 included), and is gated by the gesture_dots debug setting.
@@ -25,20 +26,27 @@ const DOTS_APP_ID = "system.gesture-dots"
25
26
  const GESTURE_FLASH_MS = 700
26
27
  const MARGIN_X = 16
27
28
  const MARGIN_TOP = 10
28
- // Off: tap_then_hold opens the side menu now.
29
- const HOLD_GLYPH_ENABLED = false
29
+ /** Gestures that flash a glyph. */
30
+ const FLASH_GLYPHS: Record<string, GestureDotGlyph> = {
31
+ single_tap: "dot",
32
+ double_tap: "double",
33
+ long_press: "hold",
34
+ swipe_up: "up",
35
+ swipe_down: "down",
36
+ swipe_left: "left",
37
+ swipe_right: "right",
38
+ }
39
+ /** The band's middle finger draws the filled shapes hollow. */
40
+ const MIDDLE_FINGER_GLYPHS: Partial<Record<GestureDotGlyph, GestureDotGlyph>> = {dot: "ring", double: "doubleRing"}
30
41
 
31
42
  class GestureDotsService {
32
43
  private started = false
33
44
  private flash: GestureDotGlyph | null = null
34
45
  private flashTimer: number | null = null
35
- /** Finger currently on the touch bar (tap_start..tap_end; the CFW forwards holds as these edges). */
36
- private held = false
37
- /**
38
- * The CFW's tap_then_hold fired during this press (discrete recognition
39
- * event; the tap_end that follows on release is what clears it).
40
- */
41
- private holdRecognized = false
46
+ /** Raw controller holds survive hold recognition/dial events until their own release. */
47
+ private presses = new Map<number, {raw: boolean; glyph: GestureDotGlyph}>()
48
+ /** Avoid sending identical images for successive phases of the same press. */
49
+ private renderedGlyph: GestureDotGlyph | null = null
42
50
  /** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
43
51
  private visible = false
44
52
  private unsubs: Array<() => void> = []
@@ -75,14 +83,14 @@ class GestureDotsService {
75
83
  if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)
76
84
  this.flashTimer = null
77
85
  this.flash = null
78
- this.held = false
79
- this.holdRecognized = false
86
+ this.presses.clear()
80
87
  }
81
88
 
82
89
  /** Another surface owns the display; forget the indicator without painting over it. */
83
90
  private relinquish(): void {
84
91
  this.resetGesture()
85
92
  this.visible = false
93
+ this.renderedGlyph = null
86
94
  sceneRenderer.clearApp(DOTS_APP_ID)
87
95
  this.trace("relinquish")
88
96
  }
@@ -94,15 +102,16 @@ class GestureDotsService {
94
102
  /**
95
103
  * Flash a gesture on the indicator. Other gesture names are ignored.
96
104
  * Claimed gestures cancel any earlier press/flash without starting a new
97
- * one: the claimant may also consume the release edge.
105
+ * one: the claimant may also consume the release edge. `fingerIndex` is the
106
+ * Neural Band's finger on its pinches and press edges (1 = middle).
98
107
  */
99
- noteGesture(gestureName: string, claimed = false): void {
108
+ noteGesture(gestureName: string, claimed = false, fingerIndex?: number): void {
100
109
  if (!this.started || !this.enabled()) return
101
- this.applyGesture(gestureName, claimed)
110
+ this.applyGesture(gestureName, claimed, fingerIndex)
102
111
  this.trace("input", {gestureName, claimed})
103
112
  }
104
113
 
105
- private applyGesture(gestureName: string, claimed: boolean): void {
114
+ private applyGesture(gestureName: string, claimed: boolean, fingerIndex?: number): void {
106
115
  if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null) {
107
116
  this.relinquish()
108
117
  return
@@ -112,49 +121,59 @@ class GestureDotsService {
112
121
  this.render()
113
122
  return
114
123
  }
115
- // Tap edges show a steady circle for as long as the finger is down (on
116
- // CFW these arrive for holds: ET_LONG/ET_REL forwarded as
117
- // tap_start/tap_end), with no timer — tap_end clears it. The ring's
118
- // touch_down is the same finger-down edge; the ring's tap_end releases it.
119
- if (gestureName === "tap_start" || gestureName === "touch_down" || gestureName === "tap_end") {
120
- const held = gestureName !== "tap_end"
121
- if (this.held === held) return
122
- this.held = held
123
- if (!held) this.holdRecognized = false
124
+ const finger = fingerIndex ?? 0
125
+ const press = this.presses.get(finger)
126
+ if (gestureName === "touch_down" || gestureName === "tap_start") {
127
+ if (press) {
128
+ // Both reports describe one press. Never restart its visual feedback.
129
+ if (gestureName === "touch_down") press.raw = true
130
+ return
131
+ }
132
+ this.clearFlash()
133
+ this.presses.set(finger, {raw: gestureName === "touch_down", glyph: finger === 1 ? "ring" : "dot"})
124
134
  this.render()
125
135
  return
126
136
  }
127
- if (gestureName === "tap_then_hold" && HOLD_GLYPH_ENABLED) {
128
- this.held = true
129
- this.holdRecognized = true
137
+ if (gestureName === "tap_end") {
138
+ if (!press) return
139
+ 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)
130
143
  this.render()
131
144
  return
132
145
  }
133
- // Any other gesture implies the finger is up, even if tap_end was lost.
134
- const released = this.held
135
- this.held = false
136
- this.holdRecognized = false
137
- const flash: GestureDotGlyph | null =
138
- gestureName === "single_tap"
139
- ? "dot"
140
- : gestureName === "swipe_up"
141
- ? "up"
142
- : gestureName === "swipe_down"
143
- ? "down"
144
- : null
145
- if (!flash) {
146
- if (released) this.render()
146
+ const shape = FLASH_GLYPHS[gestureName]
147
+ const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape
148
+ if (press?.raw && (!glyph || gestureName === "long_press")) {
149
+ // Pinch-and-turn reports do not release a finger. A recognized hold
150
+ // stays visible until tap_end instead of expiring halfway through it.
151
+ if (glyph) press.glyph = glyph
152
+ this.render()
147
153
  return
148
154
  }
149
- this.flash = flash
155
+ // Completed taps/swipes become one continuous pulse, even if the device
156
+ // omits tap_end. Older tap_start-only devices retain the same fallback.
157
+ this.presses.delete(finger)
158
+ if (glyph) this.startFlash(glyph)
159
+ this.render()
160
+ }
161
+
162
+ private clearFlash(): void {
150
163
  if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)
164
+ this.flashTimer = null
165
+ this.flash = null
166
+ }
167
+
168
+ private startFlash(glyph: GestureDotGlyph): void {
169
+ this.clearFlash()
170
+ this.flash = glyph
151
171
  this.flashTimer = BgTimer.setTimeout(() => {
152
172
  this.flashTimer = null
153
173
  this.flash = null
154
174
  this.render()
155
175
  this.trace("flash-ended")
156
176
  }, GESTURE_FLASH_MS)
157
- this.render()
158
177
  }
159
178
 
160
179
  /** Debug setting only; no payloads or retained history. Correlate seq with G2PERF. */
@@ -165,7 +184,7 @@ class GestureDotsService {
165
184
  JSON.stringify({
166
185
  t: Date.now(),
167
186
  reason,
168
- held: this.held,
187
+ held: this.presses.size > 0,
169
188
  flash: this.flash,
170
189
  overlay: useDisplayStore.getState().overlayOwner,
171
190
  active: localDisplayManager.hasActiveDisplay(),
@@ -188,15 +207,18 @@ class GestureDotsService {
188
207
  private render(): void {
189
208
  if (!this.started) return
190
209
  if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null) return
191
- const shown = this.flash ?? (this.held ? (this.holdRecognized ? "hold" : "dot") : null)
210
+ const shown = Array.from(this.presses.values()).at(-1)?.glyph ?? this.flash
192
211
  if (!this.enabled()) {
193
212
  if (this.visible) {
194
213
  this.visible = false
214
+ this.renderedGlyph = null
195
215
  sceneRenderer.emitScene(DOTS_APP_ID, "main", [])
196
216
  }
197
217
  return
198
218
  }
199
219
  if (!shown && !this.visible) return
220
+ const glyph = shown ?? "blank"
221
+ if (glyph === this.renderedGlyph) return
200
222
  const elements: SceneElementInput[] = [
201
223
  {
202
224
  type: "image",
@@ -208,13 +230,14 @@ class GestureDotsService {
208
230
  h: GESTURE_DOT_GLYPH_SIZE,
209
231
  anchor: "top-right",
210
232
  },
211
- data: GESTURE_DOT_GLYPHS[shown ?? "blank"],
233
+ data: GESTURE_DOT_GLYPHS[glyph],
212
234
  },
213
235
  ]
214
236
  this.visible = true
237
+ this.renderedGlyph = glyph
215
238
  const result = sceneRenderer.emitScene(DOTS_APP_ID, "main", elements)
216
239
  this.trace("render", {
217
- glyph: shown ?? "blank",
240
+ glyph,
218
241
  ...(result?.kind === "scene" ? {seq: result.seq, sent: result.sent} : {}),
219
242
  })
220
243
  }
@@ -50,8 +50,11 @@ function shouldForwardScroll(event: TouchEvent): boolean {
50
50
  export function dispatchTouchEvent(event: TouchEvent): void {
51
51
  const gesture = event.gestureName
52
52
  const claimant = localMiniappRuntime.gestureClaimant(gesture)
53
+ // The Neural Band's middle finger (index 1) shares the tap names but is left
54
+ // to miniapps: the engine's own double-tap behaviors are the index finger's.
55
+ const osDoubleTap = gesture === "double_tap" && event.index !== 1
53
56
 
54
- if (gesture === "double_tap") {
57
+ if (osDoubleTap) {
55
58
  // A visible main miniapp opts out of OS exit by registering double tap (the
56
59
  // terminal's back-to-menu). The latest claimant still receives it. Otherwise
57
60
  // the tap means "leave the app". The controller shows the bounded OS
@@ -72,7 +75,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
72
75
  // An overlay app claims the double tap on an empty screen to open the dashboard.
73
76
  if (gesture === "double_tap" && isOverlayApp(claimant)) dashboardOpenTrace.trigger("double-tap", event.timestamp)
74
77
  // The press may have started before this app claimed its release edge.
75
- gestureDotsService.noteGesture(gesture, true)
78
+ gestureDotsService.noteGesture(gesture, true, event.index)
76
79
  if (!shouldForwardScroll(event)) return
77
80
  localMiniappRuntime.forwardEvent("touch_event", event, undefined, claimant)
78
81
  return
@@ -81,7 +84,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
81
84
  // Gesture-dots debug indicator (any device, gated by its setting). It
82
85
  // only paints while the screen is otherwise empty. Process even suppressed
83
86
  // scroll repeats here so their completion still clears a held dot.
84
- gestureDotsService.noteGesture(gesture)
87
+ gestureDotsService.noteGesture(gesture, false, event.index)
85
88
 
86
89
  if (!shouldForwardScroll(event)) return
87
90
  localMiniappRuntime.forwardEvent("touch_event", event)
@@ -90,7 +93,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
90
93
  // Glassly CFW glasses the dashboard miniapp claims it while it runs
91
94
  // (ADR-0006); stock firmware keeps its native dashboard (ADR-0002). The
92
95
  // modal nav UI owns the screen; showDashboard would fight it.
93
- if (gesture === "double_tap" && !nativeNavigationService.isActive) {
96
+ if (osDoubleTap && !nativeNavigationService.isActive) {
94
97
  useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
95
98
  void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
96
99
  console.error("DeviceEventRouter: showDashboard failed:", err)
@@ -113,7 +116,12 @@ export function sendGesture(
113
116
  | "tap_then_hold"
114
117
  | "touch_down"
115
118
  | "swipe_up"
116
- | "swipe_down",
119
+ | "swipe_down"
120
+ | "swipe_left"
121
+ | "swipe_right"
122
+ | "long_press"
123
+ | "dial_up"
124
+ | "dial_down",
117
125
  ): void {
118
126
  const model = useGlassesStore.getState().deviceModel
119
127
  dispatchTouchEvent({
@@ -4,12 +4,17 @@
4
4
  */
5
5
  export const GESTURE_DOT_GLYPH_SIZE = 24
6
6
 
7
- export type GestureDotGlyph = "dot" | "up" | "down" | "hold" | "blank"
7
+ export type GestureDotGlyph = "dot" | "double" | "ring" | "doubleRing" | "up" | "down" | "left" | "right" | "hold" | "blank"
8
8
 
9
9
  export const GESTURE_DOT_GLYPHS: Record<GestureDotGlyph, string> = {
10
10
  dot: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAALklEQVR4nM2QsQ0AAAjC/P/pmrhqZ+xIF6DqBQyWbwO3gduAmKjQuj7QL/ETEzQmMptl7Q9NBAAAAABJRU5ErkJggg==",
11
+ double: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAG0lEQVR4nGNgGD7gPxjgYkN4yOyBlcDp3GEBAB3sd4lPaQxDAAAAAElFTkSuQmCC",
12
+ ring: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAANElEQVR4nGNgGBTgPxjgEseUgQphyCD4qDL/kSX+Y9WApoWqEjgsx+lc3B7EHSS4A3EgAAC+cEu1JTNREQAAAABJRU5ErkJggg==",
13
+ doubleRing: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAIklEQVR4nGNgGD7gPxjgYkN4yGwGmBA6SV0JHJbjdO6wAADBMlep8zl7TAAAAABJRU5ErkJggg==",
11
14
  up: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAMElEQVR4nGNgoCv4////f1zi2GT+/4ciTHE4RhdHIlDFUUhkcTQKQx9WBw8PCVoBAIcbOcf3xeeBAAAAAElFTkSuQmCC",
12
15
  down: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAALUlEQVR4nGNgoC/4/3+4SkBFUCkkNjKJIoMg0GRgGEMGgrC4CQSwuxanR2gEAO1/Ocfgp8uvAAAAAElFTkSuQmCC",
16
+ left: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAI0lEQVR4nGNgoCv4j0sch8RAiTP8RwPESOAwajDI4JKgGQAAKO05x7DqJxIAAAAASUVORK5CYII=",
17
+ right: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAIElEQVR4nGNgoDf4j1NiMMn8RwOEJYizZKDEcQc7bQAAS7w5x+ro2lsAAAAASUVORK5CYII=",
13
18
  hold: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAR0lEQVR4nGNgIAj+gwF2UUy5//+xyyDzcLGRedjMhTEwXAKl0Z0Lof7DKbgMhBpQCZzOxedBHEGCMxAJBTXhKGRAA9hFMQEANr7jHQU9fO0AAAAASUVORK5CYII=",
14
19
  blank: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAD0lEQVR4nGNgGAWjgPoAAAJYAAGgxYwDAAAAAElFTkSuQmCC",
15
20
  }