AdbClient wraps the bundled libadb.so (useLegacyPackaging -> exec from nativeLibraryDir, HOME/TMPDIR overridden, adb key injected into BuildConfig from ~/.android at build time and never committed). ShizukuProvisioner: on each BLE+WiFi connect it enables ADB-over-WiFi via Mercury, connects with mDNS fallback, pushes the Shizuku starter and runs it; runs once per BLE session, does not relaunch GlassesApp when the WS channel is already up, and exposes runShell() for arbitrary commands. DebugReceiver (DUMP) + am start --ez provision for diagnostics. Protocol: ShizukuStatus(running/granted) + RequestShizukuStatus; glasses push it on connect and on Shizuku state change, using a passive probe() that never shows a permission dialog. Glasses kill-guard now recreates its UserService when the Shizuku binder dies instead of spamming DeadObjectException. Mercury: brightness disables auto-brightness first (was silently ignored), BLE auto-connect loop retries every 6s (fixes 'searches then stops'). New phone section 'Экран очков' with wake/sleep via adb (input keyevent 224/223).
RayNeo VM
Приложение для запуска Android-приложений (например, Zona) на очках RayNeo X3 Pro через виртуальный дисплей.
Архитектура
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ Phone (телефон) │ │ Glasses (RayNeo X3 Pro) │
│ │ ◀──── WebSocket /mDNS ───▶ │ │
│ app-phone-vm │ │ app-glasses-vm │
│ VmPhoneController │ │ VmService (foreground) │
│ TouchpadScreen │ │ WebSocket server :8080 │
│ ZonaProfile │ │ mDNS publisher │
│ AudioPlaybackManager │ │ BinocularRendererView │
│ │ │ │
│ │ │ ┌─────────────────────┐ │
│ │ │ │ ShizukuVdService │ │
│ │ │ │ process: vm_vd │ │
│ │ │ │ (UID 2000, shell) │ │
│ │ │ └─────────────────────┘ │
└─────────────────────────┘ │ │ │ │
│ ┌─────┘ └──────┐ │
│ ▼ ▼ │
│ VirtualDisplay IActivityTask │
│ (640x480) Manager │
│ │ │
│ ▼ │
│ free.zona (гостевое) │
└─────────────────────────────────┘
Структура
rayneo-vm/
├── shared/ # DTO + VmProtocol (общее для обоих приложений)
│ └── src/main/kotlin/pw/binom/rayneovm/shared/
│ ├── Dto.kt # VmMessage sealed, TouchEvent, KeyEvent, ...
│ ├── VmProtocol.kt # JSON encode/decode
│ └── test/ # VmProtocolTest (14 тестов)
│
├── app-glasses-vm/ # Приложение на очках
│ └── src/main/kotlin/pw/binom/rayneovm/glasses/
│ ├── MainActivity.kt # Compose host
│ ├── VmService.kt # Foreground service, WebSocket server
│ ├── VmShizukuService.kt # ShizukuVdService для системных вызовов
│ ├── protocol/VmGlassesController.kt
│ ├── vm/GlassesScreenState.kt
│ ├── shizuku/VirtualDisplayFrameSource.kt # SurfaceTexture ↔ VirtualDisplay
│ ├── audio/AudioCaptureService.kt # Режим 2: захват системного аудио
│ └── audio/AudioStreamer.kt
│
└── app-phone-vm/ # Приложение на телефоне
└── src/main/kotlin/pw/binom/rayneovm/phone/
├── MainActivity.kt # Compose host + NavGraph
├── PhoneVmApp.kt # Application
├── protocol/VmPhoneController.kt # Auto-reconnect + mDNS
├── ui/HomeScreen.kt
├── ui/GlassesStatusScreen.kt
├── ui/AppsListScreen.kt
├── ui/ZonaProfile.kt # Touchpad + media keys + keyboard
├── ui/TouchpadScreen.kt
└── audio/AudioPlaybackManager.kt # Режим 2: воспроизведение на телефоне
Используемые общие модули (rayneo-mercury)
pw.binom.mercury:rayneo-mercury-transport:0.1.0-SNAPSHOT— generic Transport / ServerTransport, WifiTransport, BtTransport, MdnsScannerpw.binom.mercury:rayneo-mercury-binocular-renderer:0.1.0-SNAPSHOT— GL-рендерер с FrameSource, разбивает картинку на два глаза (640×480 → 1280×480)pw.binom.mercury:rayneo-mercury-shizuku-vd:0.1.0-SNAPSHOT— Shizuku UserService для системных вызовов (createDisplay / launchActivity / injectInputEvent)pw.binom.mercury:library:0.1.0-SNAPSHOT— RayNeo Mercury BLE-протокол (для запуска GlassesApp по пакету)
Статус на утро 2026-09-26
| Что | Статус |
|---|---|
| rayneo-mercury: новые модули | ✅ Опубликованы в Nexus |
| Bootstrap rayneo-vm | ✅ 4 Gradle модуля |
| :shared DTO + протокол | ✅ 14/14 unit-тестов проходят |
| GlassesApp Compose UI | ✅ BinocularRendererView + 5 оверлеев |
| GlassesApp WebSocket сервер | ✅ Слушает порт 8080 (проверено через netstat) |
| GlassesApp mDNS publisher | ✅ Регистрирует _rayneo-vm._tcp |
| GlassesApp ShizukuVdService | ✅ createDisplay / launchActivity / injectInputEvent через reflection |
| PhoneApp VmPhoneController | ✅ mDNS + auto-reconnect + exponential backoff |
| PhoneApp TouchpadScreen | ✅ Drag/tap → TouchEvent (640×480) |
| PhoneApp KeyboardDialog | ✅ Char → KEYCODE_* |
| PhoneApp ZonaProfile | ✅ Touchpad + media keys + back + keyboard |
| PhoneApp AudioPlaybackManager | ✅ AudioTrack + WiFi/Wake lock |
| AudioCaptureService (MIC fallback) | ✅ Запускается, реальный AudioPlaybackCapture — TODO (нужен MediaProjection) |
Что осталось на утро
- Реальный проброс SurfaceTexture клиент → сервис. Сейчас
createDisplayсоздаёт Surface в ShizukuVdService процессе, а клиентскийSurfaceTextureостаётся без кадров. Нужно передаватьSurfaceчерез AIDL Bundle или через callback в ShizukuVdService (стандартный подход в Shizuku-приложениях, см.IIdProvider-style). - AudioPlaybackCapture (Android 12+). Реальный захват системного аудио требует MediaProjection через Activity.onActivityResult. Сейчас сервис стартует в MIC-режиме.
- InjectInputEvent — verify on device. Код написан, но не тестировался вживую на устройстве.
- TaskStackListener для обнаружения краша гостя. Каркас в SPEC, реализация не доделана.
Сборка
./gradlew :app-glasses-vm:assembleDebug
./gradlew :app-phone-vm:assembleDebug
Установка
adb -s <glasses-serial> install -r app-glasses-vm/build/outputs/apk/debug/app-glasses-vm-debug.apk
adb -s <phone-serial> install -r app-phone-vm/build/outputs/apk/debug/app-phone-vm-debug.apk
GlassesApp как Home (лаунчер)
На RayNeo X3 Pro BOOT_COMPLETED ненадёжен, поэтому штатный автозапуск — сделать GlassesApp домашним экраном: система поднимает HOME при каждой загрузке. В манифесте у MainActivity уже есть intent-filter MAIN + HOME + DEFAULT, но дефолтным он не назначается автоматически — это отдельная команда, ставится вручную.
Назначить нашим приложением (выполнять осознанно; пока не сделано):
adb -s <glasses-serial> shell cmd package set-home-activity pw.binom.rayneovm.glasses/.MainActivity
Вернуть штатный лаунчер RayNeo обратно:
adb -s <glasses-serial> shell cmd package set-home-activity com.ffalconxr.mercury.launcher/home.MainActivity
Компонент записывается через
/(package/class), а не через/.— именно так его принимаетset-home-activityна этой прошивке.
Проверить, кто сейчас дефолтный HOME:
adb -s <glasses-serial> shell cmd package resolve-activity -a android.intent.action.MAIN -c android.intent.category.HOME
Показать всех кандидатов в HOME:
adb -s <glasses-serial> shell cmd package query-activities -a android.intent.action.MAIN -c android.intent.category.HOME
Аварийный выход, если домашний экран вообще перестал открываться (например, после pm disable-user не того пакета): переключиться на другой зарегистрированный HOME временно тем же set-home-activity (у нас в системе есть com.teslacoilsw.launcher, Nova) либо снять отключение через cmd package enable-user.
Smoke-test (проверено)
- Запустить
GlassesVmAppна очках → VmService стартует,netstat -tlnpпоказывает*:8080. - Запустить
PhoneVmAppна телефоне → mDNS ищет_rayneo-vm._tcp, ловит сервис очков. - На телефоне: тап на Zona →
LaunchAppCommand("free.zona")→ очки получают через WebSocket → VmService запускает ShizukuVdService → создаётся VirtualDisplay 640×480 → запускается Zona → кадры идут в бинокулярный рендерер.
Конфигурация
Версии в gradle/libs.versions.toml:
- Kotlin 2.4.20, AGP 9.4.1, Gradle 9.x
- Compose BOM 2026.09.00
- Ktor 3.6.0
- Shizuku API 13.1.5
- compileSdk 37, minSdk 32 (glasses), 32 (phone)
Лицензия
Внутренний проект.