日本語版は README_ja.md をご覧ください。
A Python gateway that bridges Even Realities G2 smart glasses to HTTP, WebSocket, and a local browser UI.
The BLE communication layer is ported from the MentraOS G2.kt implementation.
no cloud, no internet required.
offgrid, offline, Local completion processing.
No custom firmware required. works on official firmware(v2.2.20).
- FastMCP server: agent-friendly MCP tools for display, images, status, touch waits, notifications, menus, and user prompts
- BLE connection management — automatic pairing, reconnection, and heartbeat for left/right lenses
- Fast text path — low-latency full-screen text display via in-place update
- Layout path — multi-element pages with positioned text and image containers
- Image rendering — base64/data-URL images converted to 4-bit BMP, tiled within device constraints
- Differential display updates — unchanged layouts avoid page rebuilds, and unchanged image tiles are not resent
- Microphone control — enable/disable the glasses microphone; audio frames streamed over WebSocket
- WebSocket broadcast — all normalised device events delivered to every connected client
- CORS and API key auth — optional cross-origin access and shared-key protection for HTTP/WebSocket APIs
- Tk GUI — live status window (connection phase, battery, mic, firmware, event log)
- Browser UI — static HTML frontend served from the same process, including a layout composer for sending image + text together
- CLI — scriptable command-line client for text, images, mic control, and event streaming
- Config persistence —
config/gateway.yamlstores the last-connected pair for fast reconnect
- Python 3.11+
- Bluetooth adapter accessible via Bleak
aiohttp
bleak
fastmcp>=3.0
Pillow
PyYAML
Install with:
pip install -r requirements.txtpython gateway_server.pyWith the Tk GUI disabled (headless):
python gateway_server.py --no-guiAdditional options:
| Flag | Description |
|---|---|
--config PATH |
Path to the YAML config file (default: config/gateway.yaml) |
--host HOST |
Override the listen host |
--port PORT |
Override the listen port |
--search-id ID |
Restrict BLE scan to a specific serial prefix |
--no-gui |
Disable the Tk status window |
--mcp / --no-mcp |
Enable or disable the FastMCP HTTP server |
--mcp-host HOST |
Override the FastMCP listen host (default: 127.0.0.1) |
--mcp-port PORT |
Override the FastMCP listen port (default: 8766) |
--mcp-path PATH |
Override the FastMCP HTTP path (default: /mcp) |
--debug-raw-events |
Include glasses.raw_packet events in WebSocket output |
--log-level LEVEL |
Python logging level (default: INFO) |
--clear-saved-addresses |
Clear saved glass addresses on startup and rescan |
--unpair-on-startup |
Attempt OS-level unpair of saved addresses at startup, then rescan |
--image-gamma FLOAT |
Default gamma correction for all images (1.0 = none, <1.0 = brighter; default: 1.0) |
--image-dither |
Enable 4-bit Floyd-Steinberg dithering for all images |
--api-key KEY |
Require this API key for /api/* and WebSocket clients |
--cors-allow-origin ORIGIN |
Enable CORS for an origin; repeat or comma-separate values. Use * for any origin |
--cors-allow-credentials |
Send Access-Control-Allow-Credentials: true with CORS responses |
On first run the gateway scans for a G2 pair, connects, runs the initialisation sequence, and saves the discovered addresses to config/gateway.yaml for fast reconnect on subsequent launches.
- Before starting, make sure the glasses are disconnected from the smartphone side: power off the smartphone, turn Bluetooth fully off, and force-close Even app / MentraOS. If the glasses are still connected to the phone, advertising may not start and the gateway may not detect them.
- You do not need to remove the smartphone-side pairing.
- If the PC and glasses show a pairing request dialog, accept the pairing. Otherwise the connection may be dropped after some time.
- When you want to use the smartphone app again, stop the gateway server, re-enable Bluetooth on the smartphone, and relaunch the app. In some cases you may also need to turn off Bluetooth on the PC or power-cycle the PC once.
- If operation is unstable, try restarting the gateway server, restarting the PC, and restarting the glasses (tap both touch panels 5 times in a row).
Navigate to http://127.0.0.1:8765 for the built-in status and test interface.
The layout composer can send an image and positioned text together without hand-editing JSON.
config/gateway.yaml is created automatically if it does not exist.
server:
host: 0.0.0.0
port: 8765
websocket_path: /ws
static_dir: ui
glass:
search_id: "" # optional serial prefix filter
left_address: "" # populated automatically after first connection
right_address: ""
left_mac_address: ""
right_mac_address: ""
last_serial_number: ""
ble:
scan_timeout_sec: 5
reconnect_interval_sec: 5
heartbeat_interval_sec: 5
ble_packet_gap_ms: 8
text_queue_interval_ms: 100
image_settle_delay_ms: 1000
image_fragment_interval_ms: 200
unpair_on_startup: false
gui:
enabled: true
mcp:
enabled: false
host: 127.0.0.1
port: 8766
path: /mcp
cors:
enabled: false
allow_origins: [] # e.g. ["http://localhost:5173"] or ["*"]
allow_methods: [GET, POST, OPTIONS]
allow_headers: [Content-Type, Authorization, X-API-Key]
allow_credentials: false
max_age: 600
auth:
api_key: "" # blank disables API key authentication
header_name: X-API-Key
query_parameter: api_keyStart the gateway with MCP enabled:
python gateway_server.py --no-gui --mcpMCP clients can connect to http://127.0.0.1:8766/mcp by default. The server exposes tools for common agent workflows:
Keep the MCP listener bound to localhost unless the network and client are trusted. It is an agent-control surface and does not reuse the HTTP API key checks.
display_text: show full-screen text.display_image: show a data URL, base64 image, or local image file path.display_layout: show arbitrary text/image layout JSON.clear_display: clear the display.get_status: read gateway and glasses state.wait_for_touch: wait forsingle_tap,double_tap,swipe_up, orswipe_down.ask_user_on_glasses: display a question and map touch gestures to choices.notify_user_on_glasses: display a temporary notification, then optionally clear it.ask_menu_on_glasses: display a swipe-controlled menu and return the tapped selection.ask_character_on_glasses: display a game-style dialogue UI with a speaker icon (image, short text, or emoji) and swipe-controlled choices.ask_client_user: request structured input from the MCP client UI when elicitation is supported.
Display-backed interaction tools (ask_user_on_glasses, ask_menu_on_glasses, ask_character_on_glasses) automatically clear the glasses display before returning after selection, cancellation, or timeout.
MCP tools that need an active glasses connection wait up to 15 seconds for glasses.ready before failing.
You can also run the MCP bridge as a separate stdio server that controls an already-running gateway over HTTP:
python gateway_server.py --no-gui
fastmcp run gateway_mcp.py:mcpFor a protected gateway, set G2_GATEWAY_API_KEY. To target a non-default gateway URL, set G2_GATEWAY_URL and optionally G2_GATEWAY_WS_PATH.
For the full API reference see API.md.
If auth.api_key or --api-key is set, HTTP clients must send the key in either X-API-Key: <key> or Authorization: Bearer <key>. Browser WebSocket clients can use ?api_key=<key>.
Send text or a layout to the glasses. If the current page already has the same layout structure, the gateway updates text and image contents in place instead of rebuilding the page; unchanged image tiles are skipped.
Fast text (lowest latency):
{ "text": "Hello, world!" }Layout (positioned text and images):
{
"elements": [
{
"type": "text",
"text": "Header",
"x": 0, "y": 0, "width": 576, "height": 50,
"capture_events": true
},
{
"type": "image",
"image_base64": "<base64 or data URL>",
"x": 0, "y": 60, "width": 288, "height": 144
}
]
}Clear display:
{ "clear": true }Response:
{ "accepted": true, "mode": "fast_text", "queued": true }Possible mode values: fast_text, layout, clear.
{ "enabled": true }Synthesise a touch gesture event and broadcast it to all WebSocket clients.
Valid gesture values: single_tap, double_tap, swipe_up, swipe_down.
{ "gesture": "single_tap" }Response:
{ "accepted": true, "gesture": "single_tap" }Returns a snapshot of the server and glasses state:
{
"server": { "host": "0.0.0.0", "port": 8765, ... },
"glasses": {
"phase": "ready",
"ready": true,
"last_serial_number": "G2_...",
"mic_enabled": false,
"target_mic_enabled": false,
"battery_level": 85,
"charging": false,
"firmware_version": "...",
"last_error": "",
"last_gesture": "single_tap",
"display_surface": "app",
"pairing_warning": "",
"left": { "address": "...", "mac_address": "...", "connected": true, "authenticated": true },
"right": { "address": "...", "mac_address": "...", "connected": true, "authenticated": true }
}
}Connect to ws://127.0.0.1:8765/ws.
An initial status.snapshot event is sent on connection, followed by all device events in real time.
{
"seq": 42,
"kind": "glasses.touch",
"timestamp": "2026-05-26T12:34:56.123Z",
"data": { "gesture": "single_tap", "source": 0 }
}| Kind | Description |
|---|---|
status.snapshot |
Full server + glasses state |
connection.state |
BLE phase change |
glasses.touch |
Tap / swipe gesture |
glasses.mic_audio |
Microphone audio frame (base64-encoded) |
glasses.battery |
Battery level and charging state |
glasses.firmware |
Firmware version info |
glasses.authentication |
Per-lens authentication result |
glasses.dashboard |
Dashboard menu app selection |
glasses.raw_packet |
Raw BLE packet (debug mode only) |
system.error |
Connection or internal error |
system.reinitialize |
Post-exit re-initialisation |
python gateway_cli.py [--server URL] [--ws-path PATH] [--api-key KEY] <command>Default server: http://127.0.0.1:8765
| Command | Description |
|---|---|
send-text --text "hello" |
Send fast text to the glasses |
send-image --file img.png [--x N] [--y N] [--width N] [--height N] [--image-gamma FLOAT] [--image-dither] |
Send an image |
send-json --file payload.json [--image-gamma FLOAT] [--image-dither] |
Send a raw display JSON file |
mic --on / mic --off |
Enable or disable the microphone |
status |
Print current gateway status |
events |
Stream all WebSocket events to stdout |
| Constraint | Value |
|---|---|
| Canvas | 576 × 288 px, 4-bit greyscale (16 levels) |
| Max containers per page | 12 total (≤ 8 text/list, ≤ 4 image) |
| Container name max length | 16 characters |
| Image container width | 20 – 288 px |
| Image container height | 20 – 144 px |
| Initial text per container | ≤ 1 000 UTF-8 bytes |
| In-place text update | ≤ 1000 bytes |
Images larger than a single container are tiled automatically. When a later request keeps the same layout structure, the gateway updates image data in place and resends only tiles whose rendered BMP payload changed. Every page must have exactly one event-capturing text/list container; the gateway inserts one automatically when needed.
A self-contained demo that renders a character dialogue screen on the glasses and lets the user navigate a choice menu with swipe gestures.
Layout (576 × 288 canvas):
┌──────────┬─────────────────────────────────────┐
│ icon │ dialogue text │
│ 100×100 │ │
├──────────┴─────────────────────────────────────┤
│ choice list (capture_events=True) │
│ > Talk │
│ Use item │
│ Leave │
└────────────────────────────────────────────────┘
Controls:
| Gesture | Action |
|---|---|
| Swipe up | Move cursor up |
| Swipe down | Move cursor down |
| Single tap | Confirm selection |
Prerequisites: gateway server running (python gateway_server.py)
# Optional: place a custom icon image
cp your_icon.png icon.png
python example_character_game.pyIf icon.png is not present, a simple face icon is generated automatically.
Tap to start / stop recording. Decoded audio is saved as a WAV file in recordings/.
Controls:
| Gesture | Action |
|---|---|
| Single tap | Start recording |
| Single tap | Stop recording and save |
Output: recordings/rec_<timestamp>.wav — 16 kHz, signed 16-bit little-endian, mono PCM
Prerequisites: gateway server running (python gateway_server.py)
LC3 codec setup (liblc3 submodule):
The G2 transmits audio compressed with the LC3 codec. Build the native shared library once before running:
Windows (MSYS2 + MinGW-w64):
# Install GCC inside the MSYS2 MinGW64 shell (one-time):
# pacman -S mingw-w64-x86_64-gcc
$root = "D:/men-g2-ble-gateway/liblc3" # adjust to your path
C:\msys64\usr\bin\bash.exe -c "
gcc -O3 -std=c11 -shared -fPIC \
-I$root/include $root/src/*.c \
-o $root/liblc3.dll -lm"Expected output: liblc3\liblc3.dll
Linux:
cd liblc3
gcc -O3 -std=c11 -shared -fPIC -Iinclude src/*.c -o liblc3.so -lmmacOS:
cd liblc3
gcc -O3 -std=c11 -shared -fPIC -Iinclude src/*.c -o liblc3.dylib -lm
# Apple clang works too; replace gcc with clangpython example_pcm_record.pymentraos/ BLE communication library (ported from G2.kt)
g2/
constants.py UUIDs, command enums, display constraints
crc.py CRC16 (matches Kotlin implementation)
protobuf.py Minimal protobuf writer / reader
transport.py BLE packet framing and reassembly
scan.py BLE device discovery and pairing
render.py Image decode, resize, 4-bit BMP generation
events.py Normalised event types and factory
state.py Runtime connection and page state
client.py High-level async G2 client
protocol/
even_hub.py Page, text, image, heartbeat, audio builders
dev_settings.py Auth, time sync, pipe role, base heartbeat
g2_setting.py Device info request
onboarding.py Onboarding skip
even_ai.py Hey Even toggle
menu.py Dashboard menu (passive only)
calendar.py Calendar service init builder
dashboard.py Dashboard display settings builder
LICENSES/
MentraOS_LICENSE Original MentraOS licence
NOTICE.md Attribution notice
gateway_config.py YAML config load / save
gateway_server.py aiohttp server + Tk GUI entry point
gateway_cli.py CLI client
example_character_game.py Character game UI demo
example_pcm_record.py Tap-to-record microphone demo (LC3 → WAV)
config/gateway.yaml Runtime configuration
ui/ Static browser frontend
This project is released under the terms in LICENSE.
The BLE protocol implementation in mentraos/ is derived from the MentraOS project.
See mentraos/LICENSES/MentraOS_LICENSE and mentraos/LICENSES/NOTICE.md for attribution details.
