MentraOS OEM Display Protocol
This is the firmware-side contract an OEM implements so that MentraOS โ running on the userโs phone โ can drive your glasses. The phone is the brain: it owns all application logic and screen layout and streams your glasses commands that say what to draw. Your glasses are a thin client for the live display, plus a small firmware-resident home screen they render on their own while disconnected. Control messages between the phone and the glasses are Protocol Buffers; audio and images stream as raw binary for throughput. Every BLE packet begins with a 1-byte control header that names the payload type. We assume one thing of your firmware: a render stack capable of the draw primitives below. If you already ship a chunked image transport, you may reuse it for bitmap pixel data rather than implementing a new one.Contents
Foundations- Transport โ BLE GATT, packet types, MTU
- Capability Descriptor โ what the glasses report about themselves
- Fonts โ required glyph coverage, runtime font upload
- Acknowledgements & Forward Compatibility โ
command_result, unknown-message handling
- The Drawing Model โ queue โ atomic
commit;clear, retained elements,update - Display Commands โ text, shapes, bitmaps, commit/clear/update, power, brightness
- The Home Screen โ the offline, firmware-rendered screen
- Input & Sensors โ buttons, head gestures, IMU
- Audio & Microphone โ LC3 mic stream, VAD
- Notifications โ Android (nothing) vs iOS (ANCS relay)
- Device & System โ battery, heartbeat, pairing, reset
- Implementation Notes โ protobuf on the MCU, error handling, timing, security
- What a Complete Device Implements โ the checklist
Foundations
๐ Transport
Communication is standard BLE GATT. The phone is the central; the glasses are the peripheral and send notifications on the RX characteristic.Packet types
Every packetโs first byte is a control header naming the payload:Protobuf control messages
A protobuf packet is0x02 followed by the protobuf-encoded bytes:
PhoneToGlasses or GlassesToPhone (a oneof over every command/event). If the glasses receive a field or message type they do not recognize, they ignore it rather than fail โ this is what lets the protocol grow without breaking older firmware.
MTU
Standard BLE MTU is 23 bytes (20 payload); an extended MTU is negotiated up to 512. Image and audio chunks are sized to fit the negotiated MTU minus their headers; an audio frame must fit a single packet.๐งญ Capability Descriptor
The phone needs each deviceโs parameters to lay out frames correctly: screen size, intensity depth, available fonts, and max image size. The glasses report them in response torequest_glasses_info:
๐ Fonts
The phone wraps and lays out all text, so it must measure each line against the metrics of the font the glasses will actually render. The glasses report their fonts in the capability descriptor; the phone picks afont_code for every text draw and measures against the matching fontโs metrics, which MentraOS holds. An OEM ships at least one font covering the required languages below; the runtime then wraps text identically to what the glasses render.
Required glyph coverage: Latin (incl. extended Latin for European languages such as Spanish, French, German, Italian, Portuguese, Dutch, Polish, Turkish, Vietnamese) and CJK โ Chinese (Simplified and Traditional), Japanese, and Korean.
Recommended: other scripts โ Cyrillic (Russian, Ukrainian), Arabic, Hebrew, Devanagari (Hindi), Bengali, and Thai.
Use the Mentra-provided IBM Plex Sans build, or share your own font (Mentra loads its metrics into the runtime).
Runtime font upload (optional)
Ifsupports_font_upload is true, the phone can push a font to the glasses at runtime. A font is just another binary asset, so it reuses the cached-bitmap transfer path (preload_image) rather than a new one: the glyph-table bytes stream over the 0xB0 channel against a stream_id and are acked with image_transfer_complete, exactly like a preloaded image. The only extra is a control message that says โthe blob arriving on this stream_id is a font โ register it under this font_codeโ:
max_uploaded_fonts slots and addresses it by font_code exactly like a resident font.
โ Acknowledgements & Forward Compatibility
Many commands are fire-and-forget. A single generic result message lets the phone confirm that state-changing commands landed and detect features a given firmware doesnโt implement. EveryPhoneToGlasses carries a msg_id; the glasses echo it in a result:
commit, SetHomeScreen, and asset uploads, where the phone must know the change was persisted โ rather than stay silent. High-frequency, fire-and-forget commands (individual draw_*) need not be acked.
Forward compatibility: unknown fields and message types are ignored, never fatal. When the phone sends a feature the firmware doesnโt implement, the firmware ignores the effect and returns UNSUPPORTED, so the phone can adapt rather than guess.
Display
๐๏ธ The Drawing Model
The phone draws by sending draw commands that queue on the glasses, then acommit that paints them all at once. Nothing appears until commit. Two examples show the whole model.
Draw a fresh frame โ lead with clear, then the ops, then commit:
id when you draw it, then update that id later. No clear, so everything else stays:
- Everything queues; nothing reaches the panel except via
commit.clear_display, everydraw_*, andupdateaccumulate in a pending batch.commitflushes the batch atomically โ the wearer never sees a half-applied batch, and acommitis the only thing that changes the screen. - Draw order is layer order. Commands paint in the order sent; later commands paint over earlier ones. To put text on a filled box, send the rectangle first, the text second.
clear_displayis the only thing that wipes. Queue it to start a fresh frame; it drops the standing scene โ including all retained elements, so theirids become free โ and the commit begins from blank. A batch with noclear_displayamends the standing scene rather than replacing it.- Retained elements and partial repaint. A
draw_*op may carry an optionalid(1-based; absent = unaddressed), making it a retained element the firmware keeps so the phone can change it later withupdate(see Updating one element). Acommitwhose batch is onlyupdates repaints just those elementsโ regions, leaving the rest of the panel untouched โ the flicker-free path for changing one field on a busy screen. Any other batch (aclear_display, or any non-iddraw_*) is a full frame and repaints the whole panel. - Intensity, not RGB. The displays are monochrome. Every drawable carries an
intensityfrom0(off) to15(brightest);0means โnot drawn.โ - Bitmaps composite; unset pixels are transparent. Only lit pixels paint. An unset pixel emits no light, leaves what is beneath untouched, and lets the real world show through โ so a bitmap layers cleanly over text and shapes without a hard erase.
Pacing is the phoneโs job, not the firmwareโs. The firmware does not implement flow control or backpressure on the display channel. The phone paces its own sends and coalesces redundant frames per device; the glasses simply render what arrives.
๐ฅ๏ธ Display Commands
All display commands arePhoneToGlasses protobuf messages, carry a msg_id, and queue into the pending batch โ they take effect on the next commit.
Text
Text is drawn in UTF-8 with(x, y) as the top-left corner of a single line. The phone handles all line breaking and wrapping โ it measures against the font metrics (see Fonts) and sends each line as its own display_text. The glasses never compute multi-line layout.
Shapes
draw_* op carries an optional id (as in display_text above) to retain it as an addressable element. These same op messages are also the building blocks of the stored home screen. The op messages:
id is 1-based; 0 or absent means the op is a one-shot paint, not retained. The firmware can hold up to max_addressable_elements retained elements (see Capability Descriptor); creating more returns NO_MEMORY. The field member is ignored on the live display โ the phone draws live values itself, and the firmware never repaints the live screen on its own. It is honored only inside a stored home-screen frame, where the firmware renders standalone (see The Home Screen).
Commit and clear
Both queue into the pending batch and take effect oncommit (see The Drawing Model).
Updating one element
update replaces the content of a retained element (one whose draw_* carried an id). It queues like any draw op and takes effect on the next commit; a commit whose batch is only updates repaints just those elementsโ regions, so changing one field on a busy screen does not flicker the rest of the panel.
update carries a full replacement op. It is an upsert: if id exists, its content is replaced; if it does not (e.g. the scene was wiped by a clear), the op is created as a new retained element. The replacement op must be the same type as the element it replaces (you cannot turn a DrawText element into a DrawCircle). Its coordinates may differ from the original, so an update can move the element โ the firmware clears the elementโs old region and paints it at the new position, all within the partial repaint.
updates and a single commit โ they all repaint in one atomic frame.
For example, to show an image with a label and then change only the label โ without re-sending the image:
Bitmaps
A bitmap is sent with two messages: a protobufdisplay_image that places it in the draw order and declares its geometry, and the pixel bytes streamed on the binary channel. The bitmap takes its place in the draw order the instant the protobuf message is queued; the pixels are held against its stream_id until commit, where they render as part of the frame.
stream_id: 2 bytes, matching thedisplay_imagemessage.chunk_index: 0โ255.chunk_data: raw pixel bytes, โค MTU โ 4.
status: OK before it sends commit. Multiple bitmaps may be in flight in one frame; stream_id routes each chunk to the right bitmap.
Cached bitmaps
A bitmap can be preloaded once and then drawn by id without re-sending the pixels.preload_image uses the same binary chunking as display_image but stores the image under an image_id instead of drawing it. The imageโs dimensions and encoding live only here, at preload time:
image_transfer_complete against stream_id: "003B". Once cached, draw it by id at a position โ no dimensions, since the firmware already knows the imageโs size from preload (this enqueues into the pending frame like any draw op):
Display power and geometry
Brightness
๐ The Home Screen
While the phone is connected, MentraOS draws every screen โ including any menus or app launchers โ with the ordinary draw commands above, reacting tobutton_events. The firmware has no resident menus or navigation of its own.
The home screen exists for one reason: the glasses must not go dead during a momentary BLE drop. It is a single firmware-resident screen the glasses can render on their own, showing glanceable info, so a disconnect is invisible to the wearer.
Display states
- Idle: the display is off. Nothing is shown until the wearer wakes it (head-up gesture, or a tap โ whatever the device supports).
- On wake while connected: the phone is driving โ it draws whatever it wants.
- On wake while disconnected: the firmware shows the cached home screen.
Uploading the home screen
The phone uploads one home-screen frame, built from the same draw ops as the live display (above). The firmware persists it in flash and re-renders it whenever it needs to show the home screen offline.field placeholder, which is honored here (and only here). A few values change on the glasses while disconnected because only the glasses know them โ and field lets the firmware fill them from its own state as it renders: TIME/DATE from its RTC, BATTERY from its gauge, LINK_STATUS as an offline glyph when the link is down. A DrawText op with field set renders the firmwareโs live value instead of its literal text, and the firmware keeps it current (the clock ticks, the battery updates) for as long as the home screen is shown.
SetHomeScreen is acked with command_result. ids carry no meaning here โ the home screen is a stored frame, not a live scene; to change it, re-upload.
Setting the clock
The firmware ticksTIME/DATE from its own RTC, so it stays correct while disconnected. The phone syncs the clock on connect and periodically (for drift and timezone):
format_24h is a MentraOS user setting; the phone pushes it here and the firmware persists the last value and renders TIME accordingly. The firmware does not expose its own 12/24-hour menu.
Input, Audio & Notifications
๐ฎ Input & Sensors
Buttons
Triggered by a hardware button tap or hold:Head gestures and position
IMU
๐ Audio & Microphone
Audio is a binary stream on the0xA0 channel, controlled by protobuf messages. Frames are LC3, sized to the negotiated MTU; the 1-byte stream_id distinguishes streams (e.g. microphone vs TTS).
Microphone
Voice Activity Detection (VAD)
๐ Notifications
The glasses firmware does very little for notifications. MentraOS owns all notification logic - what to show, how, and when - and renders notifications with the ordinary draw commands described above. The firmware never decides what is notification-worthy and never stores notification UI of its own. Android. The glasses are not involved at all. The phone reads notifications through its own OS-level listener and draws whatever it wants on the glasses. There is nothing to implement on the firmware side. iOS. iOS does not expose notifications to a companion app the way Android does; instead a BLE accessory reads them directly over Appleโs ANCS (Apple Notification Center Service). So on iOS the glasses do one thing: act as an ANCS consumer and relay each notification to the phone.- After bonding, the glasses subscribe to ANCS on the iPhone (the iPhone is the Notification Provider; the glasses are the Notification Consumer). This is standard ANCS โ most BLE SoC SDKs ship an ANCS client.
- ANCS delivers every notification from the iPhone to the accessory; there is no per-app filter in the protocol itself. Each notification carries the source appโs bundle id as an attribute. The glasses do not filter โ they relay everything, and MentraOS applies the userโs per-app preferences on the phone.
- For each notification, the glasses forward it to the phone and otherwise do nothing with it:
Device & Implementation
๐งฐ Device & System
Battery and charging
Heartbeat
The glasses send a periodic ping to verify the link is alive; the phone replies:Pairing, disconnect, restart, factory reset
๐พ Implementation Notes
Protobuf on the MCU
nanopb (~10 KB footprint, static allocation, no dynamic memory) is the recommended library. A minimal receive path:oneof discriminates message type; repeated carries arrays (like missing_chunks).
Error handling
- Invalid message: if protobuf decode fails, ignore the message.
- Unknown fields/messages: ignore (forward compatibility).
- Resource limits: return a
command_resulterror for operations that exceed memory/display limits. - Timeouts: image transfers ~5 s per chunk; command responses ~1 s.
- Connection loss: clean up pending operations and reset state.
Timing and buffers
- Audio: 10 ms LC3 frame intervals; buffer 3โ5 frames for jitter.
- Display: aim for < 50 ms latency for responsive UI.
- IMU streaming: configurable 10โ100 Hz.
- Image chunk buffer: typically 12 chunks (configurable, reported in
Features). - Command queue: support at least 10 pending commands.
Security
- Implement pairing/bonding and encryption at the BLE level.
- Validate all input parameters; enforce maximum sizes for strings and arrays.
- Rate-limit to prevent abuse via excessive commands.
๐ What a Complete Device Implements
- Display & input: the transport, the drawing model (queue โ atomic
commit;clearto start fresh; retainedidโd elements withupdatefor flicker-free partial repaint), draw primitives, image transfer and cached bitmaps, brightness, button/IMU/head events. - Capability descriptor & fonts: report geometry, intensity depth, and the resident fonts so the phone can measure and wrap text.
- Acknowledgements: echo
command_resultfor state-changing commands; honor forward-compatibility (ignore unknowns, returnUNSUPPORTED). - Notifications: on iOS, subscribe to ANCS and relay each notification to the phone as
ancs_notification; on Android, nothing. MentraOS draws notifications itself. - Home screen: accept a phone-uploaded home-screen design (
SetHomeScreen), render it on wake while disconnected, fill theTIME/DATE/BATTERY/LINK_STATUSfields from its own state, and tick the clock fromSyncClock. Display is off when idle.

