# 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, MdnsScanner - `pw.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) | ## Что осталось на утро 1. **Реальный проброс SurfaceTexture клиент → сервис.** Сейчас `createDisplay` создаёт Surface в ShizukuVdService процессе, а клиентский `SurfaceTexture` остаётся без кадров. Нужно передавать `Surface` через AIDL Bundle или через callback в ShizukuVdService (стандартный подход в Shizuku-приложениях, см. `IIdProvider`-style). 2. **AudioPlaybackCapture (Android 12+).** Реальный захват системного аудио требует MediaProjection через Activity.onActivityResult. Сейчас сервис стартует в MIC-режиме. 3. **InjectInputEvent — verify on device.** Код написан, но не тестировался вживую на устройстве. 4. **TaskStackListener для обнаружения краша гостя.** Каркас в SPEC, реализация не доделана. ## Сборка ```bash ./gradlew :app-glasses-vm:assembleDebug ./gradlew :app-phone-vm:assembleDebug ``` ## Установка ```bash adb -s install -r app-glasses-vm/build/outputs/apk/debug/app-glasses-vm-debug.apk adb -s 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`, но **дефолтным он не назначается автоматически** — это отдельная команда, ставится вручную. Назначить нашим приложением (выполнять осознанно; пока не сделано): ```bash adb -s shell cmd package set-home-activity pw.binom.rayneovm.glasses/.MainActivity ``` Вернуть штатный лаунчер RayNeo обратно: ```bash adb -s shell cmd package set-home-activity com.ffalconxr.mercury.launcher/home.MainActivity ``` > Компонент записывается через `/` (package/class), а не через `/.` — именно так его принимает `set-home-activity` на этой прошивке. Проверить, кто сейчас дефолтный HOME: ```bash adb -s shell cmd package resolve-activity -a android.intent.action.MAIN -c android.intent.category.HOME ``` Показать всех кандидатов в HOME: ```bash adb -s 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 (проверено) 1. Запустить `GlassesVmApp` на очках → VmService стартует, `netstat -tlnp` показывает `*:8080`. 2. Запустить `PhoneVmApp` на телефоне → mDNS ищет `_rayneo-vm._tcp`, ловит сервис очков. 3. На телефоне: тап на 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) ## Лицензия Внутренний проект.