Glassly Miniapp SDK betaThe SDK is in beta, so its APIs may change before general availability.Developing on Mentra Live? We recommend using the
Glassly Bluetooth SDK.The developer tools are available in the Glassly App under Settings →
Miniapp Developer Settings.Share feedback with an in-app bug report, on
Discord, or by email at
[email protected].
session.display.render() draws to the glasses HUD. Each call describes the
whole frame: everything that should be on screen right now. The phone diffs it
against the previous frame, so elements with a stable id update in place (no
flicker), anything you leave out of the new frame comes off the screen, and
render([]) clears it. Your job is to describe the frame; the phone works out
what changed.
DISPLAY
hardware requirement in your manifest
so the miniapp only installs on glasses that can show anything.
Elements
Every element also takes
rotation/pivot (shapes and SVG) and depth (any
element), described below.
Shapes beyond rect render where capabilities.display.shapes lists the
type (the Even Realities G2 with the Glassly firmware draws all of them on the
glasses); elsewhere they are dropped and reported. color is a gray level
0..15. Points are raw canvas pixels and are not anchored.
Text fonts
style.font picks how text is drawn:
style.fontSize (default 20) sizes font: "mono" text.
Rotation
Geometric shapes andsvg elements take a rotation in clockwise degrees,
unwrapped, so 0 → 360 is a full turn. pivot fixes the centre of rotation
in canvas pixels; it defaults to the target box’s centre. Text and image
elements ignore both.
rotation tweens like any other geometry change, giving a rotating
element a stable id and a transition spins it on the glasses.
Depth
Every element takes an optionaldepth: a stereo disparity in canvas pixels
from -16 to 16. Positive values float the element in front of the rest of the
frame, negative values sink it behind. Devices whose
capabilities.display.depth is true draw the element depth px further right
on the left lens and depth px further left on the right lens; every other
device draws it flat, at the same place. Depth never drops an element.
Depth is a cue, not an order: elements still paint in array order. A card that
should sit on top of running content therefore goes last in the array and
at a positive depth. Because it is only a per-lens offset, a card slides in
over the content with the ordinary transition mechanics:
transition glides toward or away from the viewer:
each lens moves the element the opposite way, so it comes closer without
changing size. Text uses style.font: "builtin" in the example so a moving box
never re-wraps it on the phone.
Transitions
Give an element a stableid and a transition, and geometry changes between
renders animate on devices with capabilities.display.animation:
color and stroke widths tween;
new text or image pixels switch immediately. Devices without animation jump.
Text with a stable id can therefore tick, scroll or fade at tens of updates
per second: re-render with the new string and, optionally, a new box.
Screen sizes
box is {x, y, w, h} in raw device pixels. Coordinates map 1:1 to the
device canvas, with the origin at the top-left:
We recommend designing your layout for 500 × 220. A frame that fits
500 × 220 renders as-is on every positioning device; on the G2’s larger canvas
it sits in the top-left with some margin. Boxes that run past the edge are
clamped, and elements past the device’s budget are dropped tail-first. Both
show up in the render result, so you find out.
Anchoring to a corner
For elements that hug a screen corner (a minimap bottom-right, a status glyph bottom-left), give the box ananchor. x/y then become insets from that
corner, growing inward, and the phone resolves the box against the connected
device’s real canvas. The element lands flush on every resolution with
nothing to clamp:
anchor is one of "top-left" (the default), "top-right",
"bottom-left", "bottom-right". Corners only, there is no center or edge
anchoring, and no scaling.
When you want an exact fit instead (a full-screen caption), compute boxes
from the connected device’s real canvas. session.capabilities.display
carries it, populated on the "ready" event:
Older glasses
Even Realities G1 and Vuzix Z100 show text but can’t place elements at coordinates, and their displays can’t show bitmaps. Render the same scene anyway: the phone converts it for them. Text elements collapse into a single full-view text layout in reading order (top to bottom, then left to right), and image and rect elements are dropped. The result tells you what happened (degraded: true, plus the dropped ids), and
session.capabilities.display.canPosition tells you up front which kind of
device you’re on.
In practice a text-first miniapp works everywhere with zero branching: a
caption app’s single full-canvas text element reads the same on every device.
If your layout leans on positioning or images, like a HUD with a minimap,
check canPosition and render a text-only variant for these devices.
Updating and clearing
Give any element that changes over time a stableid. Re-rendering with the
same id updates that element in place on the glasses, and unchanged elements
never re-cross Bluetooth, so pushing the whole frame on every change is cheap.
To take something off screen, leave it out of the next frame. To clear
everything:
Options and results
render(elements, options?) takes {view, durationMs}. view picks
"main" (default) or "dashboard", and durationMs auto-clears the frame
after that many milliseconds.
Awaiting is opt-in. A plain fire-and-forget call is fine, and the returned
promise never rejects:
Just showing text
The simplest useful frame is one text element covering the whole canvas: a status line, a caption, a “Saved” confirmation.style.breakMode ("character" | "character-no-hyphen" | "word" | "strict-word") controls how the text wraps inside its box.
Helpers
Two optional helpers ship from@glassly/miniapp:
monoTextRows({id, text, box, fontSize}) splits a fixed-pitch block into one
text element per line, each with a stable ${id}-row-N id, using the G2 mono
renderer’s metrics. On G2 custom firmware a line containing unsupported Unicode
then rasterizes only its own row instead of the whole grid. It’s opt-in: one row
costs one text element, so check the device’s text budget first, and include
every returned row (blanks included) in each replacement scene.
new LatestRenderScheduler(() => intervalMs) paces a producer that can generate
frames faster than the glasses can show them. request(fn) keeps at most one
render in flight and one replaceable pending callback, so no scene is built
until it can be sent; clearPending() drops the queued one and dispose()
stops the scheduler. It bounds SDK work, not Bluetooth traffic, and never
cancels work already handed to the phone.
Text measurement and pixel art
@glassly/miniapp/display now exposes the same pure TypeScript text primitives
the host uses. They work in a miniapp background without DOM or native imports:
G1_PROFILE, G1_PROFILE_LEGACY, G2_PROFILE, Z100_PROFILE, or
NEX_PROFILE (Mentra Display) for your target’s text metrics. Read live
capabilities for the canvas size and element budgets, and pass the text box’s
width and line limit explicitly. Profiles do not measure arbitrary browser
fonts or font: "mono" at a custom size.
measureText(text) returns pixel width. wrap() returns lines, truncated,
maxLineWidthPx, totalBytes, and per-line lineMetrics, plus the original
text and chosen break mode. wrapToLines() returns just the lines. Wrapping
also accepts maxBytes; its default break mode is "character-no-hyphen".
Pixel-art rows must have equal widths: . is black, # is full intensity,
and 0–f are gray levels. pixelArtPng(rows) returns base64 PNG data;
pixelArtSize(rows) returns {w, h}. Keep the rows array immutable and reuse
it so encoding can be cached. Image support and element budgets still apply.
The subpath also exports shared scene types such as SceneElementInput,
SceneFrame, and SceneTransition.
Dashboard view
The dashboard view is one overlay slot above retained main content. The dashboard is a role that one installed miniapp holds at a time, the active dashboard. A miniapp must declare manifesttype: "dashboard" to be eligible,
and the wearer picks the active one in the Glassly App under Settings >
Dashboard > Dashboard app (the picker appears once two or more dashboards are
installed). The default is the bundled com.glassly.dashboard, and uninstalling
the active dashboard returns the role to it. Only the active dashboard can take
the slot, and only while session.capabilities?.display?.views includes
"dashboard". Currently this requires connected glasses running Glassly CFW and
the wearer’s dashboard setting enabled. Check capabilities again when they
change. A dashboard that does not hold the role is not started by alwaysOn.
showView("dashboard") takes the slot; showView("main") releases it and
restores retained main content. Both are idempotent and resolve with
{status: "shown" | "unsupported", reason?} rather than rejecting. A
dashboard miniapp that does not hold the role gets
reason: "not the active dashboard"; a miniapp of any other type gets
reason: 'The dashboard view belongs to miniapps of type "dashboard"'. Rendering
with {view: "dashboard"} alone does not acquire the slot.
onViewChange({view: "main", reason: "replaced"}). Other
reasons are "shown", "hidden", and "ineligible". "app_stopped" is
included in the reason type but is not delivered to a stopped miniapp.
onViewChange returns an unsubscribe function and fires only for the active
dashboard. Release temporary gesture claims
when a page loses the view.
Native dashboard
session.dashboard.showNativeDashboard() is a different thing: it brings the
glasses’ own OS-level idle screen to the foreground. On G2 this tears down the
current page, and the display comes back when the wearer dismisses the dashboard
or you render again. It resolves with {status: "shown" | "unsupported", reason?}
and never rejects; "unsupported" means the host refused, for instance because
the native nav UI owns the screen.
session.dashboard.setContent() remains a deferred widget API and has no
visible effect. Use session.display for overlay scenes.
Native navigation UI
Glasses that reportcapabilities.hasNativeNavigation (the G2) have a
firmware-drawn turn-by-turn screen. Three display methods feed it while a trip is
active. Each resolves with {status: "sent" | "unsupported" | "blocked", reason?}
and never rejects; on "unsupported" there is no native nav UI or no active trip,
so fall back to render().
NativeNavInfo field but directionIcon is a pre-formatted display
string the device renders verbatim, so format units and times yourself
(session.preferences carries the user’s unit system and clock).
sendNativeNavMiniMap(data, {rotationDeg?, border?}) pushes the live mini map as
a base64 BMP or PNG; the phone re-encodes and scales it to the device’s slot
(176 × 176 on G2). Draw it north-up with the cursor centred and let the device
rotate it by rotationDeg. sendNativeNavOverviewMap(data) pushes the
full-route overview map (576 × 188 on G2).
