- Assistant.processInSession: безымянная сессия получает имя из
первого сообщения (обреза 30 симв.), «беседка» — пустое
- ChatScreen: периодическая перезагрузка подхватывает и список
сессий — шапка обновляет название без ручного переключения
- MainActivity: windowSoftInputMode=adjustResize — окно сжимается,
а не панорамируется вверх при появлении IME
- ChatScreen: Modifier.imePadding() — контент честно ужимается
под высоту клавиатуры
- AndroidManifest: три uses-native-library (libOpenCL/libvndksupport/libcdsprpc)
ВНУТРИ <application> — aapt2 8.9.2 отбрасывает тег на уровне <manifest>
- LocalLlm: enableSpeculativeDecoding=true, GPU/GPU/CPU бэкенды (как у автора)
- DebugHttpServer: HTTP-отладка ассистента без UI
- Log/PhoneApp: мелкие правки
Подтверждено: движок поднимается, локальный чат отвечает на HONOR AXGL024B05001337
Проверено на железе (HONOR FCP-N49): Gemma 4 E2B отвечает
«Здравствуйте! Чем я могу вам помочь с вашими очками Viewmate?»
(15 чанков, 61 символ, ~19с генерации).
- MEDIA_TOOLSET + MediaToolExecutor: search (поиск с лимитом), info (детали), play (проверки+запуск)
- JellyfinMediaSource: обёртка JellyfinClient (search/item) с защитой userId
- agentPlayMedia: проверки (очки на связи, видео скачано на очки, звук на телефоне) → watch с позицией
- PhoneActions.watch(startPositionMs), JellyfinItem.ProductionYear
- скилл app-phone/skills/media.md (frontmatter: name/description)
- MediaToolsTest: 22 кейса — всего 142 теста зелёные
Архитектура (договорено 2026-08-22):
- скиллы (знания) ≠ тулсеты (руки)
- ядро 5 тул: время, show_text, read_skill, enable/disable_toolset
- префиксы тул по набору (media_*)
- активность на сессию, в хиппе, таймаут 10 мин
- enable/disable невидимы (не в истории), скилл исчезает при активном наборе
- бесшовная подмена вызова на активацию
- история не вычищается
- polling-цикл: при status failed/error — retry (до 2 раз)
- логирование каждого retry
- после исчерпания retry — текущее поведение (кнопка 'Перезаказать')
Если при старте очков телефон недоступен, discoverPhoneUrl возвращал
fallback (эмуляторный 10.0.2.2), он запоминался как lastUrl — очки вечно
реконнектили туда. Теперь на устройстве fallback не используется: не
нашли — повторный поиск через 3с (connectLoop рекурсивно). На эмуляторе
fallback остался. Железо: очки сами нашли телефон после его включения.
Телефон качает audio-N.ogg (audioKeys вместо fileKeys, стадия «звук»),
очки получают DownloadFiles только с video.mkv (buildGlassesFileSpecs).
Watch проверяет готовность обоих: телефон — audioReady(), очки — как
было; отказ с подсказкой «Скачать на телефон/очки». Тесты: lib-core 19,
phone 36.
Из pair-view: meta-data com.rayneo.mercury.app=true — очки распознают
приложение как AR (без неё открывалось как обычное). Тема
Theme.ViewMateGlasses (чёрный фон, fullscreen), screenOrientation
landscape, hardwareAccelerated. Плюс фикс краша: prefs читались в
конструкторе Application (до attachBaseContext) — перенесено в onCreate.
Проверено на железе: открывается через лаунчер нормально.
На паузе ExoPlayer не шлёт кадры — поверхность меняла размер/позицию,
но кадр оставался старым (часть видео не видно). requestRedraw():
updateTexImage + drawFrame для всех outputs — вызывается при смене
scale (LaunchedEffect) и в surfaceChanged. Подтверждено на железе.
Binocular-контент растягивался на весь 1280 (fillMaxSize внутри
PositionedBox) — панель оказывалась по центру экрана, половина в каждом
глазу. Теперь явная ширина половины (как у видео-Row) — панель по центру
каждой половины, бинокулярно сливается в одну. ConnectionScreen тоже
обёрнут в Binocular. Квадратик-индикатор убран — процент окрашивается
цветом статуса (зелёный/белый/красный). Скрин: обе половины идентичны.
ConnectionScreen был без фона — светлый MaterialTheme. Теперь
darkColorScheme + чёрный Box-корень вокруг обоих экранов + страховка
background(Color.Black) в ConnectionScreen. Проверено скрином:
все пиксели #000000.
Перенос из pair-view (проверено на железе): RayNeoARSDK V0.1.3 (aar в
libs/, MercurySDK.init в GlassesApp), MainActivity → BaseEventActivity,
templeActionViewModel.state → команды (Click=пауза/плей, SlideForward/
Backward=±30с), dispatchTouchEvent→onTouchEvent + toggleSoundEffect(0..3,
false) — системные щелчки трекпада отключены.
Фиксы по ходу (железо):
- applyMediaCommand(msg, onDone) — sendPlaybackPositionNow после seekTo
(иначе телефон получал позицию ДО seek)
- догонка seekTo через 1с на телефоне (первый seekTo в ogg неточен)
- playbackPosition: isPlaying → playWhenReady — ложное playing=false при
буферизации после seekTo паузило звук на телефоне (~5с «пропажи»);
pauseIfPlaying тоже по playWhenReady
Тесты: glasses 59+5, phone 71. Железо: клик пауза/плей ✓, свайпы ±30с ✓,
звук при seek не пропадает ✓ (подтверждено пользователем)
Проблемы на железе (RayNeo X2 + HONOR): при гашении экрана позиция
ExoPlayer «набегала» (мусор 42 мин), откат не срабатывал (markPosition
сохранял мусор), доставка паузы телефону 5-10с (RayNeo вырубает
WiFi-радио при screen-off, игнорируя WifiLock).
Решение:
- markPosition(positionMs, trusted): защита от мусорных скачков (>30с за
цикл = игнор); seek через трекбар помечает позицию trusted (rollback
откатывает к seek, не к началу)
- rollback при resume: откат к последней достоверной позиции, если
текущая убежала вперёд >5с
- WifiLock (FULL_HIGH_PERF) при просмотре + ensureWifiLock в точках
гашения (страховка; RayNeo игнорирует, но не мешает)
- Watchdog на телефоне: очки молчат >5с (AUDIO_SILENCE_PAUSE_MS) → пауза
звука (гарантированная верхняя граница)
- sendPlaybackPositionNow с повторами (0/2/5с) при включении экрана —
свежая позиция уходит при пробуждении радио
- Диагностика: события пишутся в files/events.log (logcat очков забит
EGL-спамом)
- Тесты: +14 (rollback/скачки/trusted seek)
- Железо подтверждено: позиция после гашения идеальная (продолжение с
места паузы, не в начало), звук пауза ≤5-6с, синк diff < 1000
После рестарта телефона автоподхват создавал звук, но state.currentItem
оставался null — пульт не знал длительность: слайдер скрыт, «сколько
всего» не показывалось. Теперь при успешном подхвате восстанавливаются
currentItem (jellyfin.item по itemId), currentAudioIndex, mode=MOVIE,
playing. Железо: «Акира · 05:21 / 2:04:28» + слайдер на месте.
Сценарий (железо): телефон перезапущен, очки продолжают играть — звук
не создавался (плеер живёт только по watch). Решение:
- PlaybackPosition теперь несёт itemId+audioIndex (дефолты — совместимость)
- очки: MovieController хранит itemId/audioIndex (playVideo/close), шлёт в позиции
- телефон: onPlaybackPosition → autostartAudio: если itemId есть, аудио не
активно и не было неудачи — mirrorByItem → resolveAudioSource → play(source,
positionMs); lastFailed защита от спама каждые 5с
- Тесты: 13 lib-core (сериализация + старый формат), 4 glasses
(contentId/close), 70/0 phone; железо: рестарт телефона → «audio sync»
через ~5с (diff 483), подтверждено пользователем