INTRO

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

CYCPLUS M1
CYCPLUS M1

Спустя четыре года использования этого устройства разочарования у меня не наступило. Более того, я к нему прикипел. Небольшие недостатки штатного ПО дали мне повод прикоснуться к миру мобильной разработки, немного изучить 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, что является списком тренировок.

Запрос загрузки списка тренировок в Wirechark
Запрос загрузки списка тренировок в Wirechark

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

Передача содержимого filelist.txt в Wirechark
Передача содержимого filelist.txt в Wirechark

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

  1. выполняется запрос файла

  2. в ответ приходит подтверждение готовности передачи

  3. отправляется команда на начало передачи

  4. выполняется передача информации о файле, имя и его размер

  5. передача происходит порциями по 1Kb, которые в свою очередь делится на пять пакетов

  6. прием каждой 1K порции подтверждается клиентом

  7. устройства обмениваются сигналами об окончании передачи

Диаграмма получения файла
Диаграмма получения файла

Немного о коде

Одной из целей этого проекта было попробовать себя в мобильной разработке. Это моя первая программа, за исключением «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 с такой тренировкой. Скорее всего сделаю.

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


  1. Brazil
    29.08.2026 15:39

    Зачем тратить время на чужое если можно сделать своё?
    Вот делаю велокомпьютер - https://habr.com/ru/articles/1018540/
    Плата уже готова. Софт будет открытый. Кроме BLE имее WiFi , синхронизируется с облаками, получить погоду не вопрос, соединится с другими такими же дивайсами тоже.
    Может сам отправить трек в Google Maps и вообще куда угодно.