Используемые устройства — телефон Samsung Galaxy S8+ (SM-G955FD), часы Galaxy Watch 5 Pro (P0ED)

Я столкнулся с проблемой зависания данных на часах, что стало проблемой, особенно когда речь идет о занятии сопртом. Мне нравится велосипед и во время тренировки удобно отсматривать показатели только одной рукой и управление спортивным режимом (в этом форке он нами так же добавлен в основное приложение) желательно только на часах.

Дополнено включение и настройка спортивного режима не только в app, но и в wear, а так же была исследована проблема залипания данных (когда на часы данные о глюкозе приходят не вместе с обновлением на телефоне, а гораздо позже или периодически например всего несколько раз в час, не стабильно). При этом сейчас данные приходят как правило быстрее чем за 7 минут (на часы). Если быть точным - обновление и показ происходит не позже 7 минут.

Ниже показываю что происходит внутри программы, какие события удалось отследить в логах (некоторые логи были добавлены мной) и как диагностировал проблему.

GitHub и форк


Работа велась в рамках проекта (форка) ? OpenApsAIMI-additional-sport-options: https://github.com/AlexeyDedeshko/OpenApsAIMI-additional-sport-options, изменения уже в master.

Дальнейшая доработка будет в wear-dev.

Особенность форка:

В нем добавлены возможности управления спортивным режимом с часов (да, спортивный режим добавлен и в основное приложение), так же переработан самый простой из имеющихся циферблат для Wear OS - как раз для улучшения стабильности связи. Внутри программы есть несколько циферблатов но мы наладили связь только для CircleWatchface. При выборе на часах он называется "(круглый)".

Цель — минимальная задержка обновления данных.

То есть это отладочный «лайт»-вариант, который позволил проверить стабильность канала данных (DataLayer → RxBus → Watchface). Внимание, на нем пока не отображаются корерктно другие данные кроме глюкозы, работа над этими данными ведется.

Основные понятия

DataLayer


Это канал связи между телефоном и часами на Wear OS. Телефон отправляет события (например, новые показания сахара), а часы принимают их. В логах мы видим работу этого канала с префиксом: WEAR_PIPE ...

onMessageReceived
Метод сервиса DataLayerListenerServiceWear на часах. Срабатывает, когда по DataLayer пришло сообщение с телефона.

Каждый раз, когда телефон отправляет новые данные на часы через DataLayer, на часах срабатывает метод onMessageReceived.

  • «Соседние» записи onMessageReceived — это две подряд идущие такие строки в логе. Между ними обычно проходит ~1 минута (т.к. новые данные глюкозы от сенсора приходят раз в 5 мин, но по DataLayer могут пересылаться чаще). Если же интервал между соседними записями >7 минут → значит, телефон не отправлял новые данные на часы (или часы их не принимали). Это и есть «залипание» на уровне канала. Залипание в реальности вызвано не только этим, но этот момент отработан.

  • onMessageReceived получил данные (например, одно значение сахара). Но нарисовать их на экране он не может напрямую — этим занимается другой компонент (циферблат). Чтобы «раздать» данные всем заинтересованным частям программы, они кладутся на RxBus (в логах WEAR_PIPE postToRxBus type=SingleBg)

    Тут мы видим, какой именно тип события отправлен:

    • SingleBg → одно измерение глюкозы,

    • Status → статус (IOB, BGI, насос),

    • GraphData → пачка точек для графика.

RxBus


Это внутренняя «шина событий» в приложении.

Когда DataLayer получает данные, он не рисует их напрямую, а передаёт их через RxBus.

Так часы могут подписывать разные компоненты на одни и те же данные: и циферблат, и виджеты, и сервисы.

postToRxBus

Наша метка перед тем, как отправить данные во внутреннюю шину событий RxBus.
Это значит: «Данные пришли и теперь я раздаю их всем подписчикам (включая циферблат)».
➝ Лог: WEAR_PIPE postToRxBus type=SingleBg ...

WEAR_FACE Rx SingleBg

Это лог внутри циферблата (класса CircleWatchface). Когда RxBus «выкинул» событие, циферблат и получает данные. Мы логируем момент прихода.

Watchface (циферблат)


Это класс (у нас CircleWatchface), который отрисовывает данные на экране часов.

У него есть метод:

onDraw(Canvas) — собственно момент прорисовки. Каждый раз, когда нужно обновить экран, вызывается этот метод.

+Xms after invalidate()

Пишется в onDraw() циферблата. Показывает, сколько миллисекунд прошло от invalidate() (запрос перерисовки) до реального вызова onDraw() (отрисовки на экране).

➝ Лог: WEAR_FACE: onDraw(); +5ms after invalidate()

В логах он фиксируется как: WEAR_FACE: onDraw(); +5ms after invalidate()

События SingleBg / Status / GraphData

  • SingleBg — одно измерение уровня сахара (Single Blood Glucose).

    То самое число, которое рисуется крупным шрифтом.

    В логе выглядит так: WEAR_FACE: Rx SingleBg at 123456789ms

  • Status — сопутствующие данные (например, IOB — активный инсулин, BGI, состояние насоса). Рисуются мелким текстом или могут быть отключены.

  • GraphData — пачка точек для графика за последние часы. Мы их в упрощённом циферблате не рисуем, но они тоже приходят.

  • invalidate() → onDraw() — Когда циферблат получает новые данные, он вызывает invalidate(). Это значит: «экран устарел, нужно перерисовать». Дальше система вызывает onDraw(Canvas) — и мы реально рисуем новые цифры.

? Описание процесса обмена данными

Ниже я своими словами описываю процесс обмена данными, описание может быть не точным и приведено чтобы заложить основу "простого понимания" процесса обмена данными. Я буду рад если вы скорректируете меня в комментариях.

Все начинается со "шпиона" в нашем теле - это сенсор глюкозы (в моем случае Dexcom 6 серии). Он получает данные и отправляет в xDrip, которые передает данные в AAPS.
AAPS выступает опытным почтальоном, который оборачивает данные в специальный формат (кладет записи в капсулу) и отправляет на часы с определенной переодичностью.

DataLayer (Layer - это в переводе слой, прослойка или несушка) это как труба в которую и кладется капсула и по этой трубе несется на часы. В логах это "WEAR_PIPE: onMessageReceived".

Капсулу принимает не сразу экран, а она как бы попадает в громкоговоритель на площади, называемый RxBus. RxBus громко читает послание и тогда любой житель (например, циферблат, виджет или другой сервис) может услышать и использовать эти данные. В логах это так: WEAR_FACE: Rx SingleBg at 123456789ms = на площади прозвучало новое значение сахара.

Циферблат - это художник, как только он слышит новое объявление о глюкозе на площади - он отрисовывает его на экране. Логика художника такая, сначала ему говорят - твой рисонок устарел, перерисуй - это команда invalidate(). Потом художник берет кисточку и рисует на холсте (onDraw(Canvas)). В логах WEAR_FACE: onDraw(); +5ms after invalidate().

Если данные дошли до RxBus и циферблата, отрисовка занимает несколько миллисекунд.

Итог: задержка может быть только на этапе пересылки с телефона на часы (казалось мне по началу). Но в итоге она может быть и на этапе отрисовки когда отрисовать не получается ввиду того что андроид не дает перерисовать когда часы спят и нам нужно их будить. Это мы тоже полечили.

Схематично так

[SENSOR]

  └─ измеряет сахар (каждые N минут)

      ▼

[PHONE / МОБ. ПРИЛОЖЕНИЕ]

  └─ формирует событие с данными

      ▼

[DataLayer]  ← «труба» телефон→часы

(в логах: WEAR_PIPE onMessageReceived)

      ▼

[WEAR_PIPE] postToRxBus        <-- отправка события в "шину" RxBus

     v

[RxBus]  ← «громкоговоритель» внутри часов

(в логах на циферблате: WEAR_FACE Rx SingleBg)

      ▼

[Watchface / CircleWatchface]

  ├─ invalidate()  ← «перерисовать!»

  └─ onDraw()      ← «нарисовано»

(в логах: WEAR_FACE onDraw(); +Xms after invalidate())

      ▼

[ЭКРАН ЧАСОВ]  ← видно новое BG + др. данные.

Какие файлы были переработаны


1. Циферблат

? wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt

  • Добавлены подробные логи (Log.d) в методах:

    • при получении событий от RxBus (Rx SingleBg, Rx Status),

    • при вызове invalidate(),

    • при отрисовке onDraw().

  • Добавлены счётчики времени (SystemClock.elapsedRealtime()), чтобы видеть задержку между событиями.

  • Упрощено отображение (SGV, Δ, ago, debug).

2. Сервис приёма данных


? wear/src/main/kotlin/app/aaps/wear/watchfaces/CircleWatchface.kt

  • Логирование добавлено в:

    • onMessageReceived() — чтобы зафиксировать момент, когда данные пришли от телефона.

    • перед rxBus.send() — чтобы отметить передачу события дальше в приложение.

  • Введён префикс WEAR_PIPE в логах, чтобы отделять уровень «канала данных» от уровня «отрисовки».

  • Усилен keepAlive/watchdog, чтобы контролировать связь (пинг каждые 30 секунд, проверка обрыва >120s).

Добавлен Partial wakeLog который будет экран когда приходят новые данные, иначе во сне они не перерисовываются (Partial WakeLock держит CPU бодрым, но экран не включает.)

Что показал анализ логов

  • Циферблат обновляется мгновенно (p50 ~5 мс, p90 ~10 мс).

  • Иногда есть редкие всплески (до секунд), но это единичные случаи.

  • Интервалы прихода SingleBg — в среднем ~54 секунды, максимальный разрыв в логе — ~4 минуты (меньше «критических» 7 минут).

  • Настоящая причина зависаний — не отрисовка, а то, что событие SingleBg не всегда сразу приходит от телефона на часы.

Итог

На всём пути разработки удалось пройти несколько этапов диагностики и улучшений:

  1. Начало проблемы

    • На часах Galaxy Watch 5 Pro наблюдались зависания: данные сахара могли не обновляться до 30 минут.

    • Первые логи показали, что циферблат CircleWatchface отрисовывает данные мгновенно, но новые значения (SingleBg) не всегда приходят на часы.

    • Вывод: проблема не в логике циферблата, а в канале связи DataLayer ↔ телефон.

  2. Разделение уровней логами

    • Добавили префиксы логов WEAR_PIPE (канал данных) и WEAR_FACE (отрисовка).

    • Это позволило точно понимать, где пропадают данные:

      • если нет WEAR_PIPE onMessageReceived → телефон не прислал данные, сбой DataLayer;

      • если Rx SingleBg дошёл → циферблат перерисует экран за миллисекунды.

  3. Редкие провалы

    • В логах фиксировались разрывы по 3–5 минут.

    • Причина: телефон → часы иногда не отправляли пакет вовремя (особенность канала DataLayer).

    • Но после получения данных отрисовка всегда шла быстро (onDraw() < 10 мс).

  4. Основная находка

    • Внимательное исследование показало, что при приглушённом или спящем экране сами часы не вызывают отрисовку.

    • В логах это выглядело как пачка onDraw() вызовов разом, когда пользователь «будил» экран.

    • Таким образом, задержка часто была не в DataLayer, а в поведении системы при ambient-режиме.

  5. Принятые меры

    • В CircleWatchface внедрён wakeLock на входящих событиях — теперь процессор часов кратко «будится», даже если экран спит.

    • Добавлены логи с метками времени, чтобы видеть задержку между событием и invalidate/onDraw.

    • Усилен watchdog в DataLayerListenerServiceWear (ping каждые 30 сек, реакция на обрывы).

Финальный результат

  • Циферблат CircleWatchface работает корректно и отрисовывает данные мгновенно.

  • Редкие задержки изначально были вызваны каналом DataLayer ↔ телефон, но позже выяснилось, что большую роль играет сон/ambient часов, где система откладывает отрисовку.

  • Благодаря wakeLock и дополнительным логам данные теперь обновляются и отображаются на часах с минимальными задержками даже в спящем режиме.

Группа для обратной связи и опыта использования здесь https://t.me/+e1X1IQa9LFkzMmE6

Комментарии (0)


  1. APSik Автор
    15.09.2025 19:39

    Оказывается было еще несколько доработок которые пришлось сделать для обеспечения стабильной связи
    Все они после финального теста будут в мастере


  1. APSik Автор
    15.09.2025 19:39

    в итоге залипания продолжились и пришлось немного доработать но сейчас все работает стабильно 

    сделал сервис публикации снапшотов (WearSnapshotService + WearSnapshotPublisher), добавили debug-ресивер ForceSnapshotReceiver, чтобы можно было руками пнуть и проверить отправку, подчистил манифесты (убрал дубли FileProvider, вынес debug-штуки в app/src/debug/), навел порядок с логами, чтобы было видно именно задержки/отправку.

    Изменения уже в мастере, дальнейшие доработки будут в wear-dev