@glassly/engine
v0.3.6
Published
Glassly Engine — miniapp registry, WebView bridge, and miniapp store
Maintainers
Readme
@glassly/engine
Glassly Engine — the on-device miniapp library.
Installation
npm install @glassly/enginePeer packages: the engine's
@glassly/*peer dependencies (crust,cloud-client,cloud-protocol,miniapp) must be available to your package manager. Until every peer is published to npm, consume the engine from this monorepo's workspace (as the example OEM app does).
Module format: the published entry points target React Native / Metro consumers (the
react-nativeexports condition). Loading@glassly/enginefrom plain Node (require/ESM) is not supported at 0.1.x — thedefaultcondition's build output is ESM with extensionless imports and will not resolve under Node's loader.
Entry points
The package exposes its Engine entry points and a supported Bluetooth SDK
facade (declared in package.json exports,
with the react-native condition pointing at src/ so Metro, tsc and jest
resolve live TypeScript source):
@glassly/engine(main,src/index.ts) — the OEM-facing surface: theenginenamespace (configure/start/stop+ typed domain facades), contract/read-model types, and pure helpers host UI renders with (decideReconnect,deriveDisplayState, theuseApps-style hooks, OTA policy constants, hardware capability tables,BgTimer). Judgment rule: read models, commands, pure functions and types are main; anything that mutates runtime state or exposes a store/service is not.@glassly/engine/ota(src/react/index.ts) — the complete Mentra Live OTA controller plus its stock full-screen renderer.useMentraLiveOtaowns the check, hotspot or Wi-Fi install, APK/MTK/BES progress, reboot, retry, and final verification behavior.MentraLiveOtaFlowrenders that same public hook.@glassly/engine/bluetooth-sdk(src/bluetooth-sdk/index.ts) — the complete public Bluetooth SDK surface, re-exported without a wrapper so it has the same singleton identity as@glassly/bluetooth-sdk. The supported SDKreact,types,photo-receiver,ota-transport, anddebugsubpaths are mirrored below this path. The SDK'sinternalentrypoint is deliberately not exposed.
See cloud/docs/issues/020-glasses-status-boundary/integration-review.md
§D for the burn-down plan.
This module owns the pieces of miniapp logic and handling that aren't tied to the rest of the manager app: the WebView message bus, the in-memory running registry, and the JS globals that we inject into every miniapp WebView.
The goal is for all miniapp logic to live here over time. Today the move is
incremental — only the self-contained services have moved. Cross-cutting
services (LocalMiniappRuntime, MantleManager, Composer install pipeline) still
live under mobile/src/ because they reach back into the manager's stores and
sockets.
Public surface
import {engine, decideDevLaunchRoute} from "@glassly/engine"
import BluetoothSdk from "@glassly/engine/bluetooth-sdk"decideDevLaunchRoute— pre-flight a dev URL'sminiapp.jsonto decide whether to mount live or take the user to the offline screen.
Mentra Live OTA flow
Bluetooth-only hosts can render the OTA flow without configuring or starting the authenticated cloud connection or miniapp runtime:
import {MentraLiveOtaFlow} from "@glassly/engine/ota"
;<MentraLiveOtaFlow onFinished={() => setShowOta(false)} onOpenWifiSetup={() => setShowWifiSetup(true)} />The component starts only the glasses-status and OTA projections. A host that
already called engine.start() should pass initializeRuntime={false}. Show
the flow after a Mentra Live connects. Hosts must provide onOpenWifiSetup;
the flow invokes it only for older glasses that cannot provide the OTA hotspot
transport.
Apps that need different pages can render the same controller directly:
import {useMentraLiveOta} from "@glassly/engine/ota"
const ota = useMentraLiveOta({onFinished, onOpenWifiSetup})
// Render ota.state.screen and invoke ota.install(), retryInstall(), finish(), etc.On up_to_date, use ota.state.completedUpdate to distinguish a completed
update session from a standalone check. releaseTransition is optional release
metadata and is not a completion marker.
Customize presentation only. Engine must remain responsible for OTA ordering, hotspot staging, restart recovery, retries, and verification. The canonical stock and custom integration guide is Update Mentra Live.
Released Engine packages contain a literal immutable OTA manifest URL and
SHA-256 generated by coordinated release CI. Modern-glasses manifest selection
is developer override, host-app release pin, then Engine release pin. It does
not derive a URL from the Bluetooth SDK version or fall through to a
glasses-reported or mutable production URL. An unpinned source build has OTA
disabled unless an Expo/React Native host explicitly sets
EXPO_PUBLIC_ASG_OTA_VERSION_URL; native source hosts use the Bluetooth SDK's
debug setOtaVersionUrl surface. Pre-39 glasses retain their separate legacy
path because those clients ignore the URL sent by the phone.
Phone notifications
engine.phoneNotifications owns the notification policy for every host.
Capture (Android's listener, or glasses that read the phone's stream) is never
gated by it. Presentation has one owner per event, from
presentationOwner(hostActive):
viewer— the engine's notifications page, on connected Glassly CFW glassesfirmware— glasses whose firmware presents notifications (hasNativeNotifications); on Android the host hands the event over withuploadToGlasses(event), on iOS the glasses already have ithost— the host's own card and speech, while its surface (Notify) runsnone— notifications are off, or nothing is there to show them
Miniapp forwarding is not presentation and stays independent.
Four settings drive firmware and viewer alike: notifications_enabled,
notifications_popups, notifications_popup_seconds (0 = the presenter's
default) and notifications_hold_during_apps, plus the shared
notifications_blocklist and the phone-activity gate
notifications_ignore_while_active. PhoneNotificationsSync folds them into
one GlassesNotificationConfig and pushes it through the Bluetooth SDK on
every change and every connection; the pure mapping lives in
services/notifications/notificationPolicy.ts. captureAuthorization()
tells the settings UI whether the glasses may read the phone's notifications
(not_needed where the phone captures); popupSecondsOptions() lists the
durations the connected firmware accepts.
Imports
Inside mobile/modules/engine/src/, use relative paths (./services/...,
../utils/...). The mobile app's @/* alias is not configured here — there
is no build-time path rewriter for this module.
Testing
Run the suite with bun run test (from this directory). It executes
scripts/test.sh, which runs each test file in its own bun process — do
not replace it with a single bun test src.
Why: bun's mock.module patches one process-wide module registry with live
ESM bindings, last write wins. Several suites mock the same specifiers
("@glassly/bluetooth-sdk/internal" alone is mocked by audioTestMocks.ts,
PhonePhotoCoordinator.test.ts, and others), so in a shared process one
file's mock clobbers another's and suites that pass alone fail in the
combined run. Per-file processes give every suite an isolated registry.
The same rule applies when writing tests: it is fine to mock.module any
specifier your suite needs, but never rely on a mock installed by a
different test file — each file must set up everything it imports.
