INTRO
Несколько лет назад я стал обладателем отличного велокомпьютера CYCPLUS M1. За свою скромную стоимость он предлагал возможность писать GPS трек и подключить кучу датчиков. Велосипедист я со стажем, и у меня еще не стерлись воспоминания как я прикручивал магнитики к спицам и вымерял расстояние до геркона, для того чтобы заработал велокомпьютер, который умел только измерять скорость расстояние. На фоне этих воспоминаний, то что обещал данный прибор выгладило как волшебство.

Спустя четыре года использования этого устройства разочарования у меня не наступило. Более того, я к нему прикипел. Небольшие недостатки штатного ПО дали мне повод прикоснуться к миру мобильной разработки, немного изучить Kotlin, погрузиться в мир реверс‑инженеринга.
Что пишут в рекламных плакатах
Даже не надеюсь что эту статью прочтет хоть один велосипедист, поэтому расскажу как данное устройство работает.
Компьютер на своем борту имеет:
GPS трекер
альтометр (высота над уровнем моря)
измеритель угла наклона
датчик температуры
К нему можно подключить внешние датчики:
датчик ЧСС (частота сердечных сокращений)
датчик каденса (частота вращения педалей)
измеритель мощности
датчик оборотов колеса (зачем он нужен, если есть GPS? Во‑первых с датчиком точнее, а с учетом невысоких скоростей и высоких требований к точности, это актуально, во‑вторых при попадании в зону глушения GPS статистика по тренировке испорчена не будет).
Что не напишут в рекламных плакатах
Каждый из этих показателей велокомпьютер пишет в свою внутреннюю память в виде файлов Fit (спасибо Garmin за открытый формат).
Загрузить тренировку (файл) с устройства можно с помощью ПО XOSS (на момент покупки его рекомендовал производитель CYCPLUS M1). XOSS подключается к велокомпьютеру по Bluetooth LE, он же Bluetooth Low Energy или BLE. Это такой специализированный Bluetooth для устройств с низким потреблением энергии.
XOSS требует регистрации, работает только в режиме online. Интегрируется с двумя сервисами, Trainingpeaks и Strava. Trainingpeaks является основным и после загрузки тренировки на данный сайт он автоматически (используя привязанную вами учетку) выгружает на Strava.
Повторюсь, XOSS, работет только online, то есть без подключения к Trainingpeaks нельзя скачать данные с велокомпьютера, нельзя просмотреть уже скаченные тренировки (хотя файлы с тренировками лежат в в одной из директорий приложения). XOSS сохраняет на телефоне файлы тренировок, выгружает их на сервер Trainingpeaks, но потом их не использует. Иначе я не могу объяснить странные артефакты, о которых пойдет речь далее.
Тренировки открываются медленно, доходит до десяти секунд. При этом графики открываются некорректно. Тут начинается бред. Вот так выглядит ЧСС одной и той же тренировки в разных приложениях.

Иногда графики в XOSS открываются нормально, но в основном это несколько значений за всю тренировку (а показания с датчиков снимаются раз в секунду!).
Наверно тут уже кто то написал в комментах что у меня для Strav‑ы скорость, а не ЧСС. Дело в том что график ЧСС доступен только для платной подписки и заплатить то мне не жалко, вот только Strava нас не любит и на территории РФ свои сервисы не предоставляет. А я не хочу платить тем, кто не хочет моих денег.
Путь к цифровой свободе
Итак, я поставил перед собой следующие задачи:
реализовать загрузку тренировок самостоятельно
разработать авторские методы анализа данных тренировок
попробовать себя в мобильной разработке
изучить Kotlin
Изучаем подключение к устройствам BLE
Велокомпьютер общается с внешним миром по протоколу BLE. Разъем USB Type‑C на девайсе исключительно для зарядки.
Прежде чем писать код я попробовал изучить устройство. Я использую Ubuntu поэтому, все примеры дальше под него. Нам нужна командная оболочка bluetoothctl (пакет bluez). Разумеется в компьютере должен быть Bluetooth адаптер.
BLE устройства реализуют протокол GATT (Generic Attribute Profile). В обмене учавствуют GATT сервер и GATT клиент. GATT сервер (в нашем случае это велокомпьютер), реализует сервисы (Service), например сервис управления питанием или пульса. Сервисы объединяют одну или несколько характеристик (Characteristic). Характеристика это параметр или переменная которую можно читать или писать, например уровень заряда батареи.
Для адресации сервисов и характеристик используются UUIDs. Их значения стандартизированы, поэтому UUID какой либо характеристики, (например заряда батареи) будет одинаковым для всех устройств. Посмотреть полный перечень можно здесь.
Переходим к практике. Запускаем оболочку, сканируем эфир, находим адрес нашего устройства и подключаемся к нему (описание магии bluetoothctl). Сразу после подключения оболочка выводит полный перечень сервисов и характеристик поддерживаемых устройством (вывод сокращен для упрощения).
bluetoothctl [bluetoothctl]> scan on [CHG] Controller 00:1A:7D:DA:71:12 Discovering: yes [CHG] Device FA:5C:7F:9F:64:0F RSSI: 0xffffffba (-70) [CHG] Device FA:5C:7F:9F:64:0F RSSI: 0xffffffb2 (-78) [bluetoothctl]> scan off [bluetoothctl]> trust FA:5C:7F:9F:64:0F [bluetoothctl]> connect FA:5C:7F:9F:64:0F Attempting to connect to FA:5C:7F:9F:64:0F [CHG] Device FA:5C:7F:9F:64:0F Connected: yes Connection successful [CHG] Device FA:5C:7F:9F:64:0F UUIDs: 6e400001-b5a3-f393-e0a9-e50e24dcca9e [NEW] Primary Service (Handle 0x0001) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service0001 00001800-0000-1000-8000-00805f9b34fb Generic Access Profile [NEW] Characteristic (Handle 0x0002) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service0001/char0002 00002a00-0000-1000-8000-00805f9b34fb Device Name ... [NEW] Primary Service (Handle 0x000e) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service000e 6e400001-b5a3-f393-e0a9-e50e24dcca9e Nordic UART Service [NEW] Characteristic (Handle 0x000f) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service000e/char000f 6e400002-b5a3-f393-e0a9-e50e24dcca9e Nordic UART TX [NEW] Characteristic (Handle 0x0011) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service000e/char0011 6e400003-b5a3-f393-e0a9-e50e24dcca9e Nordic UART RX ... [NEW] Secondary Service (Handle 0x0024) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service0024 0000180f-0000-1000-8000-00805f9b34fb Battery Service [NEW] Characteristic (Handle 0x0025) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service0024/char0025 00002a19-0000-1000-8000-00805f9b34fb Battery Level [NEW] Descriptor (Handle 0x0027) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service0024/char0025/desc0027 00002902-0000-1000-8000-00805f9b34fb Client Characteristic Configuration … [NEW] Primary Service (Handle 0x0028) /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service0028 0000fe59-0000-1000-8000-00805f9b34fb Nordic Semiconductor ASA
Продолжаем эксперементы. Получим общую информацию об устройстве
[M1_5CFA]> info Device FA:5C:7F:9F:64:0F (random) Name: M1_5CFA Alias: M1_5CFA Paired: yes Bonded: yes Trusted: yes Blocked: no Connected: yes LegacyPairing: no CablePairing: no UUID: Generic Access Profile (00001800-0000-1000-8000-00805f9b34fb) UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb) UUID: Device Information (0000180a-0000-1000-8000-00805f9b34fb) UUID: Battery Service (0000180f-0000-1000-8000-00805f9b34fb) UUID: Nordic Semiconductor AS.. (0000fe59-0000-1000-8000-00805f9b34fb) UUID: Nordic UART Service (6e400001-b5a3-f393-e0a9-e50e24dcca9e) AdvertisingFlags: 06 . Battery Percentage: 0x64 (100)
Попробуем считать информацию о заряде батареи. Команда info эту информацию уже показала, но нам нужен не результат, а понять саму механику работы с устройством через BLE. Итак, чтобы считать информацию о заряде нужно обратиться к характеристике 00002a19-0000-1000-8000-00805f9b34fb (Battery Level).
[M1_5CFA]> menu gatt Menu gatt: Available commands: list-attributes [dev/local] List attributes select-attribute <attribute/UUID/local> [attribute/UUID] Select attribute attribute-info [attribute/UUID] Select attribute read [offset] Read attribute value write <data=xx xx ...> [offset] [type] Write attribute value acquire-write Acquire Write file descriptor release-write Release Write file descriptor acquire-notify Acquire Notify file descriptor release-notify Release Notify file descriptor notify <on/off> Notify attribute value clone [dev/attribute/UUID] Clone a device or attribute register-application [UUID ...] Register profile to connect unregister-application Unregister profile register-service [handle] Register application service. unregister-service <UUID/object> Unregister application service register-includes [handle] Register as Included service in. unregister-includes Unregister Included service. register-characteristic <Flags=read,write,notify...> [handle] Register application characteristic unregister-characteristic <UUID/object> Unregister application characteristic register-descriptor <Flags=read,write...> [handle] Register application descriptor unregister-descriptor <UUID/object> Unregister application descriptor back Return to main menu version Display version quit Quit program exit Quit program help Display help about this program export Print environment variables script Run script [M1_5CFA]> select-attribute 00002a19-0000-1000-8000-00805f9b34fb [M1_5CFA:/service0024/char0025]> read Attempting to read /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service0024/char0025 [CHG] Attribute /org/bluez/hci0/dev_FA_5C_7F_9F_64_0F/service0024/char0025 Value: 64 d 64 d
Результат 64, совпадает с тем, что вывела команда info (это 100 в десятичной системе).
Реверс‑инженеринг
Самое сложное было разобрать как загружать файлы с велокомпьютера самому. План следующий:
включить на телефоне запись трафика Bluetooth
запустить загрузку тренировки в XOSS
скачать логи
разобраться с протоколом обмена
Выглядит очень страшно. Покажу только самые интересные моменты.
Переводим телефон в режим отладки, включаем запись трафика Bluetooth и скидываем логи на компьютер, как это делать, поисковики в помощь.
В логах нам нужен файл BT_CHI***.cfa.curf. Разбор его выполнял в Wirechark. Просматриваем обмен телефона с велокомпьютером и бинго!, видим вот такую запись. Это приложение запросило у велокомпьютера передачу filelist.txt, что является списком тренировок.

В ответ девайс начинает передавать содержимое filelist.txt, которое состоит из списка названий файлов Fit и их размерами.

Дальше пошла монотонная работа в попытках разобраться в протоколе. Достаточно быстро стало понятно что для передачи файлов используется протокол XModem. Все файлы, список тренировок (filelist.txt) и файлы тренировок(yyyyMMddhhmmss.fit) передаются одинаковым образом:
выполняется запрос файла
в ответ приходит подтверждение готовности передачи
отправляется команда на начало передачи
выполняется передача информации о файле, имя и его размер
передача происходит порциями по 1Kb, которые в свою очередь делится на пять пакетов
прием каждой 1K порции подтверждается клиентом
устройства обмениваются сигналами об окончании передачи

Немного о коде
Одной из целей этого проекта было попробовать себя в мобильной разработке. Это моя первая программа, за исключением «Hello World», под Android и вообще на Kotlin. Отмечу только основные моменты.
Самой кропотливой частью была реализация обмена в велокомпьютером по BLE. В качестве библиотеки для работы с BLE я использовал no.nordicsemi. Название сервиса «Nordic Semiconductor ASA», которое вернула команда bluetoothctl намекало, что все должно получиться. Я не стал искать готовую библиотеку под XModem, поскольку протокол обмена простой, и разбираться чужом коде, в попытках его запустить мне казалось будет сложнее. Поэтому протокол загрузки файлов я реализовал сам, опираясь на записанный трафик.
Когда я говорю что протокол простой, это не значит что у меня это легко и быстро получилось. Продвигался медленными шагами разбираясь с каждым нюансом обмена. В итоге получился «поток», который передает команды и «поток», принимающий данные и небольшая «машина состояний» которая переключается в зависимости от информации в этих потоках.
UI делал с использованием Jetpack Compose. Очень понравилась его гибкость, особенно когда захотел самостоятельно написать Chart‑ы, об этом ниже. Исходный код на githab.com. cycoffline.apk.zip это готовый apk.
Описание работы программы
Тренировки можно добавлять двумя способами, добавляя файлы формата Fit и синхронизируясь по BLE с велокомпьютером. Общая информация о тренировке сохраняется в локальной БД, а сам файл в папке приложения, в дальнейшем файл используется для отображения информации о тренировке. Загрузка по BLE выполняется в ForegroundService. Можно выбирать несколько файлов, они будут добавлены в очередь загрузки.

Разработку я начал давно и за это время перепробовал несколько библиотек для построения графиков. Ничего меня не устроило, и итоге решил написать свою библиотеку. Вдохновлялся я проектом ehsannarmani.github.io. Самая большая претензия к нему, это то что индикаторы он отображает не по центру линий сетки.
Я реализовал два варианта отображения графиков, с отображением пройденного расстояния или времени по оси x. При отображении времени можно скрыть паузы. Паузы учитываются при формировании сетки, и расстояние между линиями уменьшается на величину паузы.

Интересен еще вот такой вариант отображения статистики о тренировке. Классический вариант отображения с распределением ЧСС по зонам слева, справа распределение ЧСС по времени нахождения на каждом из значений ЧСС. Я применил данный тип графиков к анализу скорости и каденса. Нив одном фитнес приложении я такого не встречал.

В целом больше никаких особенностей нет, в остальном программа похожа на многие другие фитнес приложения.
Планы
На данный момент я единственный разработчик и пользователь этого проекта.
Из функций которые я хочу добавить но пока не знаю как, это поддержка погоды. Направление ветра во время тренировок очень важно, можно накладывать скорость ветра на записанный трек и учитывать какой объем тренировки выполнялся с попутным и встречным направлением ветра. Нигде этого не видел. Но сервисы, которые предоставляют архив метеоинформации, только платные и цены очень неприличные для моего скромного проекта. В общем думаю. Как вариант можно запрашивать текущую погоду на конец тренировки на каком‑нибудь сервисе, вот только тренировка может длиться несколько часов и охватывать большую площадь, а синхронизировать тренировку сразу после ее окончания, пока погода не поменялась. Так что точность будет посредственная и сам подход некрасивый. Пока над этом думаю.
Добавить поддержку других велокомпьютеров. Добавление тренировок в виде Fit файлов, записанных другими устройствами я пробовал, везде есть небольшие нюансы, но скажем так, должно работать или нужно будет чуть‑чуть доделать. С загрузкой тренировок с устройств сложнее, нужен доступ к самому устройству. Есть в доступе у знакомых пару девайсов, сейчас сезон закончится, возьму покопаться.
Адаптировать программу под другие виды физической активности пока не планирую.
Привязка к навигации, скажем к google map. Скорее всего сделаю, но не в ближайшее время.
Добавление других датчиков, например датчика мощности. У меня его нет, приобретать в ближайшее время не планирую, добавить можно чисто из спортивного интереса взяв у кого‑нибудь fit с такой тренировкой. Скорее всего сделаю.
Brazil
Зачем тратить время на чужое если можно сделать своё?
Вот делаю велокомпьютер - https://habr.com/ru/articles/1018540/
Плата уже готова. Софт будет открытый. Кроме BLE имее WiFi , синхронизируется с облаками, получить погоду не вопрос, соединится с другими такими же дивайсами тоже.
Может сам отправить трек в Google Maps и вообще куда угодно.