@glassly/miniapp 0.1.0-dev.99 → 0.2.8-dev.445

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 (250) hide show
  1. package/README.md +218 -73
  2. package/dist/background/index.d.ts +14 -8
  3. package/dist/background/index.d.ts.map +1 -1
  4. package/dist/background/index.js +4 -0
  5. package/dist/background/index.js.map +1 -1
  6. package/dist/background/register.d.ts +1 -1
  7. package/dist/background/register.js +1 -1
  8. package/dist/display/index.d.ts +23 -0
  9. package/dist/display/index.d.ts.map +1 -0
  10. package/dist/display/index.js +17 -0
  11. package/dist/display/index.js.map +1 -0
  12. package/dist/display/measurer/TextMeasurer.d.ts +160 -0
  13. package/dist/display/measurer/TextMeasurer.d.ts.map +1 -0
  14. package/dist/display/measurer/TextMeasurer.js +272 -0
  15. package/dist/display/measurer/TextMeasurer.js.map +1 -0
  16. package/dist/display/measurer/index.d.ts +10 -0
  17. package/dist/display/measurer/index.d.ts.map +1 -0
  18. package/dist/display/measurer/index.js +11 -0
  19. package/dist/display/measurer/index.js.map +1 -0
  20. package/dist/display/measurer/script-detection.d.ts +53 -0
  21. package/dist/display/measurer/script-detection.d.ts.map +1 -0
  22. package/dist/display/measurer/script-detection.js +184 -0
  23. package/dist/display/measurer/script-detection.js.map +1 -0
  24. package/dist/display/pixelArt.d.ts +22 -0
  25. package/dist/display/pixelArt.d.ts.map +1 -0
  26. package/dist/display/pixelArt.js +126 -0
  27. package/dist/display/pixelArt.js.map +1 -0
  28. package/dist/display/profiles/g1.d.ts +33 -0
  29. package/dist/display/profiles/g1.d.ts.map +1 -0
  30. package/dist/display/profiles/g1.js +214 -0
  31. package/dist/display/profiles/g1.js.map +1 -0
  32. package/dist/display/profiles/g2.d.ts +13 -0
  33. package/dist/display/profiles/g2.d.ts.map +1 -0
  34. package/dist/display/profiles/g2.js +34 -0
  35. package/dist/display/profiles/g2.js.map +1 -0
  36. package/dist/display/profiles/index.d.ts +12 -0
  37. package/dist/display/profiles/index.d.ts.map +1 -0
  38. package/dist/display/profiles/index.js +15 -0
  39. package/dist/display/profiles/index.js.map +1 -0
  40. package/dist/display/profiles/nex.d.ts +10 -0
  41. package/dist/display/profiles/nex.d.ts.map +1 -0
  42. package/dist/display/profiles/nex.js +150 -0
  43. package/dist/display/profiles/nex.js.map +1 -0
  44. package/dist/display/profiles/types.d.ts +103 -0
  45. package/dist/display/profiles/types.d.ts.map +1 -0
  46. package/dist/display/profiles/types.js +2 -0
  47. package/dist/display/profiles/types.js.map +1 -0
  48. package/dist/display/profiles/z100.d.ts +33 -0
  49. package/dist/display/profiles/z100.d.ts.map +1 -0
  50. package/dist/display/profiles/z100.js +182 -0
  51. package/dist/display/profiles/z100.js.map +1 -0
  52. package/dist/display/scene/types.d.ts +292 -0
  53. package/dist/display/scene/types.d.ts.map +1 -0
  54. package/dist/display/scene/types.js +0 -0
  55. package/dist/display/scene/types.js.map +1 -0
  56. package/dist/display/wrapper/TextWrapper.d.ts +102 -0
  57. package/dist/display/wrapper/TextWrapper.d.ts.map +1 -0
  58. package/dist/display/wrapper/TextWrapper.js +469 -0
  59. package/dist/display/wrapper/TextWrapper.js.map +1 -0
  60. package/dist/display/wrapper/index.d.ts +12 -0
  61. package/dist/display/wrapper/index.d.ts.map +1 -0
  62. package/dist/display/wrapper/index.js +12 -0
  63. package/dist/display/wrapper/index.js.map +1 -0
  64. package/dist/display/wrapper/types.d.ts +75 -0
  65. package/dist/display/wrapper/types.d.ts.map +1 -0
  66. package/dist/display/wrapper/types.js +11 -0
  67. package/dist/display/wrapper/types.js.map +1 -0
  68. package/dist/globals.d.ts +5 -5
  69. package/dist/globals.d.ts.map +1 -1
  70. package/dist/globals.js +1 -1
  71. package/dist/globals.js.map +1 -1
  72. package/dist/index.d.ts +16 -10
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +3 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/knownApps.d.ts +26 -0
  77. package/dist/knownApps.d.ts.map +1 -0
  78. package/dist/knownApps.js +140 -0
  79. package/dist/knownApps.js.map +1 -0
  80. package/dist/mirror/canvas.d.ts +22 -0
  81. package/dist/mirror/canvas.d.ts.map +1 -0
  82. package/dist/mirror/canvas.js +311 -0
  83. package/dist/mirror/canvas.js.map +1 -0
  84. package/dist/mirror/geometry.d.ts +69 -0
  85. package/dist/mirror/geometry.d.ts.map +1 -0
  86. package/dist/mirror/geometry.js +75 -0
  87. package/dist/mirror/geometry.js.map +1 -0
  88. package/dist/mirror/index.d.ts +3 -0
  89. package/dist/mirror/index.d.ts.map +1 -0
  90. package/dist/mirror/index.js +2 -0
  91. package/dist/mirror/index.js.map +1 -0
  92. package/dist/mirror/types.d.ts +40 -0
  93. package/dist/mirror/types.d.ts.map +1 -0
  94. package/dist/mirror/types.js +2 -0
  95. package/dist/mirror/types.js.map +1 -0
  96. package/dist/modules/auth.d.ts +1 -1
  97. package/dist/modules/auth.d.ts.map +1 -1
  98. package/dist/modules/auth.js +2 -2
  99. package/dist/modules/auth.js.map +1 -1
  100. package/dist/modules/compass.d.ts +47 -0
  101. package/dist/modules/compass.d.ts.map +1 -0
  102. package/dist/modules/compass.js +39 -0
  103. package/dist/modules/compass.js.map +1 -0
  104. package/dist/modules/display.d.ts +189 -12
  105. package/dist/modules/display.d.ts.map +1 -1
  106. package/dist/modules/display.js +40 -3
  107. package/dist/modules/display.js.map +1 -1
  108. package/dist/modules/events.d.ts +30 -3
  109. package/dist/modules/events.d.ts.map +1 -1
  110. package/dist/modules/events.js.map +1 -1
  111. package/dist/modules/input.d.ts +34 -11
  112. package/dist/modules/input.d.ts.map +1 -1
  113. package/dist/modules/input.js +35 -11
  114. package/dist/modules/input.js.map +1 -1
  115. package/dist/modules/latest-render-scheduler.d.ts +19 -0
  116. package/dist/modules/latest-render-scheduler.d.ts.map +1 -0
  117. package/dist/modules/latest-render-scheduler.js +54 -0
  118. package/dist/modules/latest-render-scheduler.js.map +1 -0
  119. package/dist/modules/lightSensor.d.ts +44 -0
  120. package/dist/modules/lightSensor.d.ts.map +1 -0
  121. package/dist/modules/lightSensor.js +33 -0
  122. package/dist/modules/lightSensor.js.map +1 -0
  123. package/dist/modules/mic.d.ts +3 -2
  124. package/dist/modules/mic.d.ts.map +1 -1
  125. package/dist/modules/mic.js +3 -2
  126. package/dist/modules/mic.js.map +1 -1
  127. package/dist/modules/miniapps.d.ts +2 -0
  128. package/dist/modules/miniapps.d.ts.map +1 -1
  129. package/dist/modules/miniapps.js.map +1 -1
  130. package/dist/modules/mirror.d.ts +16 -0
  131. package/dist/modules/mirror.d.ts.map +1 -0
  132. package/dist/modules/mirror.js +40 -0
  133. package/dist/modules/mirror.js.map +1 -0
  134. package/dist/modules/mono-text-rows.d.ts +20 -0
  135. package/dist/modules/mono-text-rows.d.ts.map +1 -0
  136. package/dist/modules/mono-text-rows.js +26 -0
  137. package/dist/modules/mono-text-rows.js.map +1 -0
  138. package/dist/modules/navigation.d.ts.map +1 -1
  139. package/dist/modules/navigation.js +21 -14
  140. package/dist/modules/navigation.js.map +1 -1
  141. package/dist/modules/phone.d.ts +26 -1
  142. package/dist/modules/phone.d.ts.map +1 -1
  143. package/dist/modules/phone.js +28 -0
  144. package/dist/modules/phone.js.map +1 -1
  145. package/dist/modules/settings.d.ts +45 -0
  146. package/dist/modules/settings.d.ts.map +1 -0
  147. package/dist/modules/settings.js +54 -0
  148. package/dist/modules/settings.js.map +1 -0
  149. package/dist/modules/system.d.ts +21 -0
  150. package/dist/modules/system.d.ts.map +1 -1
  151. package/dist/modules/system.js +12 -0
  152. package/dist/modules/system.js.map +1 -1
  153. package/dist/modules/transcription.d.ts +3 -2
  154. package/dist/modules/transcription.d.ts.map +1 -1
  155. package/dist/modules/transcription.js +3 -2
  156. package/dist/modules/transcription.js.map +1 -1
  157. package/dist/protocol.d.ts +59 -5
  158. package/dist/protocol.d.ts.map +1 -1
  159. package/dist/protocol.js +60 -5
  160. package/dist/protocol.js.map +1 -1
  161. package/dist/react/MirrorCanvas.d.ts +10 -0
  162. package/dist/react/MirrorCanvas.d.ts.map +1 -0
  163. package/dist/react/MirrorCanvas.js +23 -0
  164. package/dist/react/MirrorCanvas.js.map +1 -0
  165. package/dist/react/index.d.ts +2 -0
  166. package/dist/react/index.d.ts.map +1 -1
  167. package/dist/react/index.js +1 -0
  168. package/dist/react/index.js.map +1 -1
  169. package/dist/react/useColorScheme.d.ts +2 -3
  170. package/dist/react/useColorScheme.d.ts.map +1 -1
  171. package/dist/react/useColorScheme.js +15 -8
  172. package/dist/react/useColorScheme.js.map +1 -1
  173. package/dist/react/useRpc.js +2 -2
  174. package/dist/react/useRpc.js.map +1 -1
  175. package/dist/react/useSafeArea.d.ts.map +1 -1
  176. package/dist/react/useSafeArea.js +2 -2
  177. package/dist/react/useSafeArea.js.map +1 -1
  178. package/dist/session.d.ts +50 -4
  179. package/dist/session.d.ts.map +1 -1
  180. package/dist/session.js +35 -6
  181. package/dist/session.js.map +1 -1
  182. package/dist/transport/auto.d.ts +2 -2
  183. package/dist/transport/auto.js +2 -2
  184. package/dist/transport/dispatch.d.ts +1 -1
  185. package/dist/transport/dispatch.js +1 -1
  186. package/dist/transport/mock.d.ts +4 -4
  187. package/dist/transport/mock.d.ts.map +1 -1
  188. package/dist/transport/mock.js +31 -7
  189. package/dist/transport/mock.js.map +1 -1
  190. package/dist/ui/index.d.ts +2 -0
  191. package/dist/ui/index.d.ts.map +1 -1
  192. package/dist/ui/index.js +1 -0
  193. package/dist/ui/index.js.map +1 -1
  194. package/package.json +15 -3
  195. package/src/background/index.ts +26 -4
  196. package/src/background/register.ts +1 -1
  197. package/src/display/index.ts +83 -0
  198. package/src/display/measurer/TextMeasurer.ts +346 -0
  199. package/src/display/measurer/index.ts +21 -0
  200. package/src/display/measurer/script-detection.ts +218 -0
  201. package/src/display/pixelArt.ts +141 -0
  202. package/src/display/profiles/g1.ts +237 -0
  203. package/src/display/profiles/g2.ts +39 -0
  204. package/src/display/profiles/index.ts +28 -0
  205. package/src/display/profiles/nex.ts +166 -0
  206. package/src/display/profiles/types.ts +132 -0
  207. package/src/display/profiles/z100.ts +198 -0
  208. package/src/display/scene/types.ts +335 -0
  209. package/src/display/wrapper/TextWrapper.ts +546 -0
  210. package/src/display/wrapper/index.ts +15 -0
  211. package/src/display/wrapper/types.ts +102 -0
  212. package/src/globals.ts +5 -5
  213. package/src/index.ts +34 -6
  214. package/src/knownApps.ts +170 -0
  215. package/src/mirror/canvas.ts +322 -0
  216. package/src/mirror/geometry.ts +128 -0
  217. package/src/mirror/index.ts +2 -0
  218. package/src/mirror/types.ts +38 -0
  219. package/src/modules/auth.ts +2 -2
  220. package/src/modules/compass.ts +69 -0
  221. package/src/modules/display.ts +219 -16
  222. package/src/modules/events.ts +26 -2
  223. package/src/modules/input.ts +44 -12
  224. package/src/modules/latest-render-scheduler.ts +55 -0
  225. package/src/modules/lightSensor.ts +61 -0
  226. package/src/modules/mic.ts +3 -2
  227. package/src/modules/miniapps.ts +2 -0
  228. package/src/modules/mirror.ts +42 -0
  229. package/src/modules/mono-text-rows.ts +30 -0
  230. package/src/modules/navigation.ts +20 -14
  231. package/src/modules/phone.ts +40 -1
  232. package/src/modules/settings.ts +64 -0
  233. package/src/modules/system.ts +30 -1
  234. package/src/modules/transcription.ts +3 -2
  235. package/src/protocol.ts +65 -5
  236. package/src/react/MirrorCanvas.tsx +35 -0
  237. package/src/react/index.ts +3 -0
  238. package/src/react/useColorScheme.ts +17 -8
  239. package/src/react/useRpc.ts +2 -2
  240. package/src/react/useSafeArea.ts +2 -2
  241. package/src/session.ts +78 -8
  242. package/src/transport/auto.ts +2 -2
  243. package/src/transport/dispatch.ts +1 -1
  244. package/src/transport/mock.ts +42 -11
  245. package/src/ui/index.ts +3 -0
  246. package/dist/modules/heading.d.ts +0 -16
  247. package/dist/modules/heading.d.ts.map +0 -1
  248. package/dist/modules/heading.js +0 -20
  249. package/dist/modules/heading.js.map +0 -1
  250. package/src/modules/heading.ts +0 -23
@@ -17,36 +17,68 @@
17
17
 
18
18
  import {MiniappRequestType, MiniappStreamType} from "../protocol"
19
19
  import {MiniappSession} from "../session"
20
- import type {ButtonPressData, TouchData, UnsubscribeFn} from "./events"
20
+ import type {ButtonPressData, KeyboardEventData, TouchData, UnsubscribeFn} from "./events"
21
+
22
+ export interface GestureClaimChange {
23
+ gesture: string
24
+ /** True when this app now holds the gesture, false when another app took it. */
25
+ held: boolean
26
+ }
21
27
 
22
28
  export class InputModule {
23
29
  constructor(private readonly session: MiniappSession) {}
24
30
 
25
31
  /**
26
32
  * Declare the gestures this miniapp is handling itself, replacing any
27
- * previous claim set. While a claimed gesture arrives and this app is
28
- * FOREGROUND on the glasses, the host skips its default behavior for it —
29
- * today that is `double_tap`, which otherwise opens the dashboard (or
30
- * toggles head-up). The events themselves still arrive via `onTouch`
31
- * either way; claiming only suppresses the system default.
33
+ * previous claim set in the selected scope. Claims stack: the most recent app to claim a gesture
34
+ * holds it and receives it alone through `onTouch`, and the host skips its
35
+ * own default for it (a double tap otherwise leaves the visible miniapp or
36
+ * opens the glasses' dashboard). When the holder releases the gesture, or
37
+ * stops, the previous claimer holds it again; `onClaimChange` reports both
38
+ * sides of every handoff.
32
39
  *
33
- * Pass `[]` to release every claim. Claims are cleared automatically when
34
- * the miniapp stops, and are inert while the app is backgrounded — a
35
- * background app never hijacks a system gesture.
40
+ * Pass `[]` to release that scope. Omit `scope` for the default registration.
41
+ * Separate scopes preserve persistent shortcuts while a temporary page claims
42
+ * input: releasing the page scope restores the previous receiver without
43
+ * re-registering the shortcuts. All scopes are cleared automatically when
44
+ * the miniapp stops. One exception stands above the claims: an unclaimed
45
+ * double tap on a miniapp whose frame is visibly on the glasses leaves that
46
+ * app, so claim `double_tap` yourself while you are on screen if you use it.
36
47
  *
37
48
  * session.input.setClaimedGestures(["double_tap"]) // in-app back gesture
38
- * session.input.setClaimedGestures([]) // back to system default
49
+ * session.input.setClaimedGestures([]) // hand it back
39
50
  */
40
- setClaimedGestures(gestures: string[]): Promise<void> {
51
+ setClaimedGestures(gestures: string[], options?: {scope?: string}): Promise<void> {
41
52
  return this.session
42
- .sendRequest<void>({type: MiniappRequestType.CLAIM_GESTURES, gestures})
53
+ .sendRequest<void>({
54
+ type: MiniappRequestType.CLAIM_GESTURES,
55
+ gestures,
56
+ ...(options?.scope === undefined ? {} : {scope: options.scope}),
57
+ })
43
58
  .then(() => undefined)
44
59
  }
45
60
 
61
+ /** This app gained or lost a gesture it claimed (another app claimed it, or released it back). */
62
+ onClaimChange(handler: (change: GestureClaimChange) => void): UnsubscribeFn {
63
+ return this.session._subscribe(MiniappStreamType.GESTURE_CLAIM, handler as (data: unknown) => void)
64
+ }
65
+
46
66
  onButtonPress(handler: (data: ButtonPressData) => void): UnsubscribeFn {
47
67
  return this.session._subscribe(MiniappStreamType.BUTTON_PRESS, handler as (data: unknown) => void)
48
68
  }
49
69
 
70
+ /**
71
+ * Subscribe to a physical keyboard paired to the glasses (G2 Glassly CFW
72
+ * revision 39+; nothing arrives on other hardware). Every subscriber
73
+ * receives every key, like button presses: check `phase`, use `text` to
74
+ * type printable characters and `key`/`modifiers` for commands.
75
+ *
76
+ * session.input.onKey((e) => { if (e.phase === "down" && e.text) type(e.text) })
77
+ */
78
+ onKey(handler: (data: KeyboardEventData) => void): UnsubscribeFn {
79
+ return this.session._subscribe(MiniappStreamType.KEYBOARD_EVENT, handler as (data: unknown) => void)
80
+ }
81
+
50
82
  /**
51
83
  * Subscribe to touch events.
52
84
  *
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Opt-in producer pacing for any miniapp. One render promise in flight and
3
+ * one replaceable pending callback; no scene is built until it can be sent.
4
+ * This bounds SDK work, not BLE packets, and does not cancel native work.
5
+ */
6
+ export class LatestRenderScheduler {
7
+ private pending: (() => Promise<void>) | undefined
8
+ private timer: ReturnType<typeof setTimeout> | undefined
9
+ private busy = false
10
+ private disposed = false
11
+ private nextAt = 0
12
+
13
+ constructor(private readonly intervalMs: () => number) {}
14
+
15
+ request(render: () => Promise<void>): void {
16
+ if (this.disposed) return
17
+ this.pending = render
18
+ this.pump()
19
+ }
20
+
21
+ clearPending(): void {
22
+ this.pending = undefined
23
+ if (this.timer !== undefined) clearTimeout(this.timer)
24
+ this.timer = undefined
25
+ }
26
+
27
+ dispose(): void {
28
+ this.disposed = true
29
+ this.clearPending()
30
+ }
31
+
32
+ private pump(): void {
33
+ if (this.disposed || this.busy || !this.pending || this.timer !== undefined) return
34
+ const delay = this.nextAt - Date.now()
35
+ if (delay > 0) {
36
+ this.timer = setTimeout(() => {
37
+ this.timer = undefined
38
+ this.pump()
39
+ }, delay)
40
+ return
41
+ }
42
+ const render = this.pending
43
+ this.pending = undefined
44
+ this.busy = true
45
+ this.nextAt = Date.now() + Math.max(0, this.intervalMs())
46
+ // Callers handle/report errors; still release the scheduler after failure.
47
+ void Promise.resolve()
48
+ .then(() => (this.disposed ? undefined : render()))
49
+ .catch(() => {})
50
+ .finally(() => {
51
+ this.busy = false
52
+ this.pump()
53
+ })
54
+ }
55
+ }
@@ -0,0 +1,61 @@
1
+ /** Ambient light sensor exposed by G2 custom firmware. Subscribing does not start hardware. */
2
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
3
+ import type {MiniappSession} from "../session"
4
+ import type {UnsubscribeFn} from "./events"
5
+
6
+ export interface LightSensorOptions {
7
+ /** 100..5000 ms, default 500. */
8
+ intervalMs?: number
9
+ /** Change threshold in firmware ALS units, 0..65535, default 1. */
10
+ minDelta?: number
11
+ /** Report unchanged readings every 0..65535 ms; 0 disables, default 5000. */
12
+ heartbeatMs?: number
13
+ }
14
+ export interface LightSensorData {
15
+ reason: "query" | "started" | "poll" | "stopped"
16
+ opened: boolean
17
+ passive: boolean
18
+ autoBrightness: boolean
19
+ readSucceeded: boolean
20
+ status: number
21
+ /** Stock calibrated ALS units (approximately tenths of lux), not a lux measurement. */
22
+ value: number
23
+ peak: number
24
+ targetLevel: number
25
+ brightness: number
26
+ gear: number
27
+ scaleQ10: number
28
+ /** Firmware uptime, wraps at 2^32 ms. */
29
+ sampleTimestampMs: number
30
+ /** Phone reception time, Unix milliseconds. */
31
+ timestamp: number
32
+ }
33
+
34
+ export class LightSensorModule {
35
+ constructor(private readonly session: MiniappSession) {}
36
+ onUpdate(handler: (data: LightSensorData) => void): UnsubscribeFn {
37
+ return this.session._subscribe(MiniappStreamType.LIGHT_SENSOR, handler as (data: unknown) => void)
38
+ }
39
+ /** Request the cached driver reading; delivered by onUpdate, without opening the sensor. */
40
+ query(): Promise<void> {
41
+ return this.control("query")
42
+ }
43
+ /** Passive polling temporarily suspends stock automatic brightness adjustments.
44
+ * Navigation pauses polling; stopping or closing the miniapp restores stock behavior.
45
+ * Polling also stops when the CFW display lease is released or expires.
46
+ */
47
+ start(options: LightSensorOptions = {}): Promise<void> {
48
+ return this.control("start", options)
49
+ }
50
+ stop(): Promise<void> {
51
+ return this.control("stop")
52
+ }
53
+ private control(operation: string, options: LightSensorOptions = {}): Promise<void> {
54
+ return this.session.sendRequest({
55
+ type: MiniappRequestType.GLASSES_SENSOR_CONTROL,
56
+ sensor: "light",
57
+ operation,
58
+ options,
59
+ })
60
+ }
61
+ }
@@ -11,8 +11,9 @@
11
11
  * Audio *output* (TTS, file playback) lives on `session.speaker`.
12
12
  *
13
13
  * MICROPHONE permission must be declared in miniapp.json for any of these
14
- * subscriptions to succeed; the phone runtime rejects with
15
- * PERMISSION_NOT_DECLARED otherwise.
14
+ * subscriptions to succeed. Otherwise the phone runtime drops them silently:
15
+ * SUBSCRIBE carries no requestId, so the PERMISSION_NOT_DECLARED result is
16
+ * discarded and handlers just never fire.
16
17
  */
17
18
 
18
19
  import {MiniappRequestType, MiniappStreamType} from "../protocol"
@@ -42,6 +42,8 @@ export interface MiniappInfo {
42
42
  description?: string
43
43
  version: string
44
44
  running: boolean
45
+ /** The wearer keeps the app off the home grid; menus should leave it out too. Absent on older hosts. */
46
+ hidden?: boolean
45
47
  compatibility: MiniappCompatibility
46
48
  /** Declared actions (empty if the miniapp declares none). */
47
49
  actions: MiniappActionInfo[]
@@ -0,0 +1,42 @@
1
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
2
+ import type {MiniappSession} from "../session"
3
+ import type {UnsubscribeFn} from "./events"
4
+ import type {MirrorSnapshot} from "../mirror/types"
5
+
6
+ /** Read the globally visible, intended glasses display. Requires DISPLAY_MIRROR. */
7
+ export class MirrorModule {
8
+ private latest: MirrorSnapshot | null = null
9
+ private listeners = 0
10
+
11
+ constructor(private readonly session: MiniappSession) {}
12
+
13
+ get hasPermission(): boolean {
14
+ return this.session._hasManifestPermission("DISPLAY_MIRROR")
15
+ }
16
+
17
+ /** Query the host's current canvas. This is a preview, not hardware readback. */
18
+ getCurrent(): Promise<MirrorSnapshot> {
19
+ return this.session.sendRequest<MirrorSnapshot>({type: MiniappRequestType.MIRROR_GET})
20
+ }
21
+
22
+ /** Receive an initial snapshot and subsequent changes. Dispose when the preview closes. */
23
+ onUpdate(handler: (snapshot: MirrorSnapshot) => void): UnsubscribeFn {
24
+ this.listeners++
25
+ let active = true
26
+ const off = this.session._subscribe(MiniappStreamType.MIRROR_UPDATE, (data) => {
27
+ this.latest = data as MirrorSnapshot
28
+ handler(this.latest)
29
+ })
30
+ const cached = this.latest
31
+ if (cached)
32
+ queueMicrotask(() => {
33
+ if (active && this.latest === cached) handler(cached)
34
+ })
35
+ return () => {
36
+ if (!active) return
37
+ active = false
38
+ off()
39
+ if (--this.listeners === 0) this.latest = null
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,30 @@
1
+ import type {RenderElement} from "./display"
2
+
3
+ /**
4
+ * Split a fixed-pitch grid into stable row elements. On G2 CFW, unsupported
5
+ * Unicode then rasterizes only its row instead of the entire grid. This is
6
+ * opt-in: callers must check the device's text-element budget. It uses the
7
+ * G2 mono renderer's 2px inset and rounded 1.2x line pitch, not stock metrics.
8
+ * Include every returned row in each replacement scene (including blanks).
9
+ */
10
+ export function monoTextRows(options: {
11
+ id: string
12
+ text: string
13
+ box: {x: number; y: number; w: number; h: number}
14
+ fontSize: number
15
+ }): RenderElement[] {
16
+ const {id, text, box, fontSize} = options
17
+ if (![box.x, box.y, box.w, box.h, fontSize].every(Number.isFinite) || fontSize <= 0) return []
18
+ if (box.w <= 2 || box.h <= 2) return []
19
+ const pitch = Math.max(1, Math.round(fontSize * 1.2))
20
+ return text
21
+ .split("\n")
22
+ .slice(0, Math.ceil((box.h - 2) / pitch))
23
+ .map((line, index) => ({
24
+ type: "text",
25
+ id: `${id}-row-${index}`,
26
+ text: line,
27
+ box: {x: box.x, y: box.y + index * pitch, w: box.w, h: Math.min(pitch + 2, box.h - index * pitch)},
28
+ style: {font: "mono", fontSize},
29
+ }))
30
+ }
@@ -342,21 +342,27 @@ export class NavigationModule {
342
342
  }
343
343
  const restRoute = restResult.routes[0]
344
344
 
345
- const nativeResult = await this.session.sendRequest<{ok: boolean; error?: string}>({
346
- type: MiniappRequestType.NAVIGATION_START,
347
- // Keep lat/lng on the wire for hosts not yet upgraded to read `stops`.
348
- lat: stops[0]?.lat,
349
- lng: stops[0]?.lng,
350
- stops,
351
- mode,
352
- avoid: opts.avoid,
353
- simulate: opts.simulate ?? false,
354
- speedMultiplier: opts.speedMultiplier ?? 1,
355
- missedTurnRerouteMeters: opts.missedTurnRerouteMeters,
356
- })
345
+ let nativeResult: {ok: boolean; error?: string}
346
+ try {
347
+ nativeResult = await this.session.sendRequest<{ok: boolean; error?: string}>({
348
+ type: MiniappRequestType.NAVIGATION_START,
349
+ // Keep lat/lng on the wire for hosts not yet upgraded to read `stops`.
350
+ lat: stops[0]?.lat,
351
+ lng: stops[0]?.lng,
352
+ stops,
353
+ mode,
354
+ avoid: opts.avoid,
355
+ simulate: opts.simulate ?? false,
356
+ speedMultiplier: opts.speedMultiplier ?? 1,
357
+ missedTurnRerouteMeters: opts.missedTurnRerouteMeters,
358
+ })
359
+ } catch (err) {
360
+ // Wire-level failure (host threw, timed out, disconnected). Honor the
361
+ // never-throws contract and release the pivot subscriptions.
362
+ return failStart(err instanceof Error ? err.message : String(err))
363
+ }
357
364
  if (!nativeResult.ok) {
358
- this._detachPivotTracking()
359
- return nativeResult
365
+ return failStart(nativeResult.error ?? "navigation.start: host refused the trip")
360
366
  }
361
367
 
362
368
  // Dispatch the REST route through the native route channel so subscribers see step roads now.
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * session.phone.notifications.{on, hasPermission, stop}
7
7
  * session.phone.calendar.{listEvents, hasPermission}
8
+ * session.phone.heading.{onUpdate, hasPermission}
8
9
  * session.phone.onBattery(...) // stays flat
9
10
  *
10
11
  * Imperative phone-OS calls (share, openUrl, copyToClipboard, download) live
@@ -18,7 +19,7 @@
18
19
 
19
20
  import {MiniappRequestType, MiniappStreamType} from "../protocol"
20
21
  import {MiniappSession} from "../session"
21
- import type {BatteryData, NotificationDismissedData, PhoneNotificationData, UnsubscribeFn} from "./events"
22
+ import type {BatteryData, HeadingData, NotificationDismissedData, PhoneNotificationData, UnsubscribeFn} from "./events"
22
23
 
23
24
  class TrackedSubs {
24
25
  private readonly unsubs = new Set<UnsubscribeFn>()
@@ -71,6 +72,27 @@ export class PhoneNotificationsModule extends TrackedSubs {
71
72
  get hasPermission(): boolean {
72
73
  return this.session._hasManifestPermission("READ_NOTIFICATIONS")
73
74
  }
75
+
76
+ /**
77
+ * Apps that post phone notifications, for a per-app filter UI. Android
78
+ * lists installed apps with their icons (base64 PNG); iOS has no cross-app
79
+ * lookup, so it lists a seeded catalog plus the apps the glasses reported
80
+ * posting, without icons. SYSTEM-only.
81
+ */
82
+ async installedApps(): Promise<NotificationAppInfo[]> {
83
+ const result = await this.session.sendRequest<NotificationAppInfo[]>({
84
+ type: MiniappRequestType.NOTIFICATION_APPS_LIST,
85
+ })
86
+ return result ?? []
87
+ }
88
+ }
89
+
90
+ export interface NotificationAppInfo {
91
+ /** Bundle id (iOS) or package name (Android). */
92
+ packageName: string
93
+ appName: string
94
+ /** Base64 PNG, Android only. */
95
+ icon: string | null
74
96
  }
75
97
 
76
98
  export interface CalendarListOptions {
@@ -136,13 +158,30 @@ function normalizeCalendarDate(value: string | Date | undefined, field: string):
136
158
  return date.toISOString()
137
159
  }
138
160
 
161
+ /** Phone compass (magnetometer / sensor fusion). For the glasses' own heading use `session.compass`. */
162
+ export class PhoneHeadingModule {
163
+ constructor(private readonly session: MiniappSession) {}
164
+
165
+ /** True iff `LOCATION` is declared in the miniapp's manifest. */
166
+ get hasPermission(): boolean {
167
+ return this.session._hasManifestPermission("LOCATION")
168
+ }
169
+
170
+ /** Continuous phone heading updates; the phone compass starts on the first subscriber. */
171
+ onUpdate(handler: (data: HeadingData) => void): UnsubscribeFn {
172
+ return this.session._subscribe(MiniappStreamType.HEADING_UPDATE, handler as (data: unknown) => void)
173
+ }
174
+ }
175
+
139
176
  export class PhoneModule {
140
177
  public readonly notifications: PhoneNotificationsModule
141
178
  public readonly calendar: PhoneCalendarModule
179
+ public readonly heading: PhoneHeadingModule
142
180
 
143
181
  constructor(private readonly session: MiniappSession) {
144
182
  this.notifications = new PhoneNotificationsModule(session)
145
183
  this.calendar = new PhoneCalendarModule(session)
184
+ this.heading = new PhoneHeadingModule(session)
146
185
  }
147
186
 
148
187
  /**
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @fileoverview SettingsModule — `session.settings`, the engine's settings
3
+ * store behind an allowlist. SYSTEM-only: every call rejects with
4
+ * NOT_PERMITTED unless the caller is a system app. The dashboard miniapp is
5
+ * the first consumer: it reads and edits the same dashboard, notification
6
+ * and glasses-menu keys the phone's settings screens used to, so firmware
7
+ * sync and the host's notification policy keep one source of truth.
8
+ *
9
+ * Keys are the engine's setting keys (e.g. `custom_dashboard_layout`,
10
+ * `notifications_popups`, `menu_apps`); values are their JSON values. The
11
+ * host also serves a few derived read-only keys (`phone_active`,
12
+ * `foreground_app_on_screen`, `ring_battery`) that the engine computes
13
+ * rather than stores. `set` on a read-only or unknown key rejects.
14
+ */
15
+
16
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
17
+ import {MiniappSession} from "../session"
18
+ import type {UnsubscribeFn} from "./events"
19
+
20
+ export interface SettingsChange {
21
+ key: string
22
+ value: unknown
23
+ }
24
+
25
+ export class SettingsModule {
26
+ constructor(private readonly session: MiniappSession) {}
27
+
28
+ /** One allowlisted key's value; undefined when the key is unknown. SYSTEM-only. */
29
+ async get<T = unknown>(key: string): Promise<T | undefined> {
30
+ const values = await this.getAll([key])
31
+ return values[key] as T | undefined
32
+ }
33
+
34
+ /**
35
+ * Every allowlisted key's value, or just `keys`. Unknown keys are left out
36
+ * rather than rejected, so a newer miniapp on an older host still reads
37
+ * the keys the host knows. SYSTEM-only.
38
+ */
39
+ async getAll(keys?: string[]): Promise<Record<string, unknown>> {
40
+ const result = await this.session.sendRequest<{values?: Record<string, unknown>}>({
41
+ type: MiniappRequestType.SETTINGS_GET,
42
+ ...(keys ? {keys} : {}),
43
+ })
44
+ return result?.values ?? {}
45
+ }
46
+
47
+ /**
48
+ * Write a writable key through the engine's settings store: persistence,
49
+ * server sync and the Bluetooth sync of glasses settings behave as if a
50
+ * phone screen wrote it. SYSTEM-only.
51
+ */
52
+ async set(key: string, value: unknown): Promise<void> {
53
+ await this.session.sendRequest<void>({type: MiniappRequestType.SETTINGS_SET, key, value})
54
+ }
55
+
56
+ /**
57
+ * An allowlisted key changed (including the derived read-only keys). On
58
+ * subscribe the host replays every key's current value, so a mirror can
59
+ * be built from this stream alone. SYSTEM-only; other apps never hear it.
60
+ */
61
+ onChange(handler: (change: SettingsChange) => void): UnsubscribeFn {
62
+ return this.session._subscribe(MiniappStreamType.SETTINGS_CHANGED, handler as (data: unknown) => void)
63
+ }
64
+ }
@@ -46,6 +46,22 @@ export interface ScanQrOptions {
46
46
 
47
47
  export type ScanQrResult = {data: string; cancelled?: false} | {cancelled: true; data?: undefined}
48
48
 
49
+ /** Native G2 performance counters only; never display text or packet contents. */
50
+ export interface DisplayDiagnostic {
51
+ v: 1
52
+ run: string
53
+ id: number
54
+ t: number
55
+ type: "sample" | "stall" | "session"
56
+ [key: string]: string | number | boolean
57
+ }
58
+
59
+ export interface DisplayDiagnosticsResult {
60
+ enabled: boolean
61
+ generatedAt: number
62
+ records: DisplayDiagnostic[]
63
+ }
64
+
49
65
  export class SystemModule {
50
66
  constructor(private readonly session: MiniappSession) {}
51
67
 
@@ -66,6 +82,19 @@ export class SystemModule {
66
82
  })
67
83
  }
68
84
 
85
+ /**
86
+ * Read recent native G2 diagnostics (bounded to 180 records / 3 minutes).
87
+ * Requires the phone's Debug Console setting. Empty on unsupported devices;
88
+ * submitted bytes are not over-the-air throughput or measured display FPS.
89
+ * Poll no faster than once per second. Older hosts reject this request.
90
+ */
91
+ async getDisplayDiagnostics(): Promise<DisplayDiagnosticsResult> {
92
+ const result = await this.session.sendRequest<DisplayDiagnosticsResult>({
93
+ type: MiniappRequestType.DISPLAY_DIAGNOSTICS,
94
+ })
95
+ return result ?? {enabled: false, generatedAt: Date.now(), records: []}
96
+ }
97
+
69
98
  /** Copy text to the system clipboard. */
70
99
  async copyToClipboard(text: string): Promise<void> {
71
100
  await this.session.sendRequest<void>({
@@ -108,6 +137,6 @@ export class SystemModule {
108
137
  },
109
138
  {timeoutMs: 0},
110
139
  )
111
- return result ?? { cancelled: true }
140
+ return result ?? {cancelled: true}
112
141
  }
113
142
  }
@@ -14,8 +14,9 @@
14
14
  * module still exposes lower-level audio chunks + VAD; this module is for
15
15
  * "give me text from speech."
16
16
  *
17
- * MICROPHONE permission must be declared in miniapp.json. The phone runtime
18
- * rejects with PERMISSION_NOT_DECLARED otherwise.
17
+ * MICROPHONE permission must be declared in miniapp.json. Otherwise the phone
18
+ * runtime drops the subscription silently: SUBSCRIBE carries no requestId, so
19
+ * the PERMISSION_NOT_DECLARED result is discarded and handlers just never fire.
19
20
  */
20
21
 
21
22
  import {MiniappRequestType, MiniappStreamType} from "../protocol"