Купи китайский браслет, разочаруйся в официальном ПО, напиши свое!
Эта история ждала своей публикации больше полугода, за это время многое изменилось, прошивки и ПО обновились и многие мои наработки уже устарели.
Активная работа большого количества компаний в области носимой техники и умных часов не оставляла покоя моей душе. Я видел в носимых устройствах с экраном большой потенциал. Нет, я не говорю о подсчете шагов и других фитнес-штуках, они безусловно классные, но пока кроме банальных «Поздравляем! Вы прошли 4км, сделали 20к+ шагов!» и красивых графиков прогресса и регресса, ничего особенного не придумали.
А вот то, что я могу получать уведомления прямо на дисплей на запястье — это удобно. Если я могу еще и как-то взаимодействовать с ним или с чем-то поблизости нажимая 1-2-3 кнопки — это еще круче.
В очередной раз бороздя просторы aliexpress, я наткнулся на фитнес-браслет iWown i5. Он сразу привлек моё внимание невероятно низкой ценой ( на тот момент около 800р с бесплатной доставкой ) и наличием OLED дисплея. Внимательно почитав описание продавца и отзывы покупателей, я решил заказать сие чудо.
Заявленные характеристики (перевод описания с aliexpress):
Возможности:
и другие «надуманные» плюсы в стиле китайского маркетинга
Меня сильно заинтересовала возможность трекать сон и будить в нужную фазу. Многие мои знакомые покупали недорогие фитнес трекеры именно из-за этой функции и были довольны mi band и тому подобными штуками. Мне в них всегда не хватало экрана, а тут все-в-одном.
В моей работе частенько приходиться разрабатывать простые приложения для Android, я решил, что если мне не хватит функционала родного приложения, напишу своё.
Посылка пришла довольно быстро и я тут же бросился изучать замечательный браслет. После часа игры с приложением Zeroner, которое по инструкции необходимо поставить на свой Android девайс, я понял, что функционал довольно скуден и печален. Zeroner как и все остальные производители делал акцент на подсчет шагов и калорий, выводя красивые графики, имеет функцию поиска телефона (об этом позже расскажу), может оповещать о входящем вызове, о приходе сообщения в facebook и whatsapp и пересылает уведомления с ОДНОГО любого выбранного приложения, которое будет считать как приложение для SMS.
Вибрация у браслета весьма спорная, на форумах пишут что слабовата, некоторые говорят, нормальная. По мне так, можно было бы и по сильнее. У браслета есть реакция на жест «Посмотреть на часы», если посмотреть на браслет как на наручные часы, поднимая руку и сгибая в локте, автоматически включится экран и покажет время или пропущенное уведомление.
В общем, не долго думая, я решил писать своё приложение, с уведомлениями, вибрацией и синхронизацией. Забегу вперед, на это ушло 4 выходных дня и несколько долгих вечеров…
Учитывая что с Bluetooth я не в-синий-зуб-ногой, с дуру решил попытаться перехватить данные, которыми обменивается телефон и браслет. Для этого я полез во вкладку для разработчиков, и включил галку «Включить журнал трансляции HCI Bluetooth». После включения этой опции, весь дамп общения андроида с любыми Bluetooth устройствами складывается в файл /sdcard/Android/data/btsnoop_hci.log (у разных устройств путь может меняться, имя файла вроде всегда одинаковое).
Скачав WireShark я принялся изучать логи общения с браслетом и увидел что-то похожее на это:
Проведя почти два часа, изучая логи, проводя зависимости, гугля в интернете протоколы, я понял, что такой путь не для меня.
Так как мой телефон все-же интерпретировал браслет как обычное BLE устройство и показывал его в разделе подключенных устройств, я решил воспользоваться примерами работы с BLE из Android SDK.
Склонировав репозиторий https://github.com/googlesamples/android-BluetoothLeGatt, натравил Android Studio на пупку с исходниками, собрал и запустил приложение. (Ссылка на описание работы Android SDK с Bluetooth LE)
Получилось как на картинках с гитхаба:
Запустив сканирование, приложение не увидело устройство. Оказалось, что родное приложение подключившись к браслету не давало BLE найти устройство. Все решилось простым удалением Zeroner, можно было просто отключить, но надежнее снести полностью.
И так, Bluetooth LE — это технология которая строится на устройствах с малым потреблением энергии, используется в новомодных датчиках, метках и многих других устройствах. Основой этой технологии служит Generic Attribute Profile (GATT), это Bluetooth профиль, позволяющий обмениваться маленькими порциями данных, «атрибутами». Не буду долго расписывать как это все работает, на хабре и в инете есть куча информации, которую мне также пришлось перерыть в поисках решений.
Я понадеялся, что все нужные мне данные хранятся в характеристиках и дескрипторах браслета, и я смогу получать и записывать данные безо всяких проблем. Я ошибался…
Тестовое BLE приложение показывало мне всего 4 сервиса:
0000180f-0000-1000-8000-00805f9b34fb
00001800-0000-1000-8000-00805f9b34fb
0000ff20-0000-1000-8000-00805f9b34fb
00001801-0000-1000-8000-00805f9b34fb
в них было очень мало характеристик, те, что читались, возвращали пустоту или нули, а писать было бесполезно. Но меня воодушевило то, что я смог подключиться и получить хоть какие-то данные.
Далее, я решил, что в слепую действовать не получиться и решил препарировать приложение Zeroner. Накопав в интернете пару онлайн APK декомпиляторов, я скормил им zeroner.apk и получил на выходе 2 zip архива.
Первый был JADX вариант, а воторой содержал результат работы apktool.
Роясь в исходниках я ужасался китайскому коду (хотя в работе я часто с ним сталкиваюсь в виде бэендов для сайтов и сервисов, но он не перестает удивлять своей извилистостью и изобретательностью, но как ни крути, он ужасно тяжело читается)
После долгих изысканий, я наконец наткнулся на файл WristBandDevice.java, который находился по пути com.kunekt/bluetooth.
В этом классе как раз и скрывалась вся работа с устройством, но опять таки, меня ждала засада.
Как позже выяснилось, в предыдущих прошивках браслета использовалось больше сервисов в характеристик (как я ранее и предполагал), но позже, разработчики оставили всего 2, одна на чтение, вторая на запись. Все команды передаются в одном пакете.
Понять как должен выглядеть пакет оказалось не так просто, я решил четко определиться, чего я хочу от браслета в первую очередь, что бы начать прослеживать вызовы функций. А хотел я, отображать кастомные сообщения на браслете.
Не долго думая, я полез в com.kunekt/receiver/CallReceiver.java, так как входящие вызовы отображались очень стабильно и даже русскими символами, я решил что это отличное начало, учитывая что я уже сталкивался с событием входящих вызовов в Android, представление о том, как это может работать уже было.
Открыв файл я увидел это:
Тут мы явно видим, что существует 2 варианта API и названия у них очень логичные newAPI, а второе соответственно oldAPI. Во всем этом обилии условий, меня заинтересовала только одна, повторяющаяся строка:
WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName.....)
Это было то самое, что я искал. Забегая вперед, скажу, что у iWown есть еще модели i5+ и i6, у них экран больше и соответственно символов помещается больше, для этого и нужны все эти проверки. непонятно почему они не написали класс или что-то вроде того, возможно это шалости декомпелятора, но данный код повторяется во многих местах.
Перейдя к определению этой функции, я увидел это:
Отлично, используется одна и та же функция для отправки текста, просто с разными параметрами. Все функции со словом New — это как раз наш вариант, потому что как выяснилось выше, API у меня new.
Радостно перейдя к определению функции writeAlertNew, я увидел следующее:
Было понятно, что от профита меня отделяет пара функций, которые используются здесь.
writeWristBandDataByte — формирует пакет с сообщением для браслета, интересно, что есть специальная функция form_Header(3, 1), которая формирует заголовок пакета, по которому браслет понимает чего от него хотят. 3 — это номер группы команд, а 1 — это сама команда
Функция простая, скопировал себе в проект без изменений. Следующее было это
NewAgreementBackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, writeData));
Как оказалось, ничего необычного, приложение создает поток, в котором постоянно проверяется очередь пакетов на отправку, если в очереди появляется пакет, поток выполняет запись в заданную характеристику устройства, если пакетов больше чем один, она отправляет их с задержкой в 240 миллисекунд.
Далее шло самое непонятное:
PebbleBitmap.fromString(context, String.valueOf(e), 8, 1).data)
Почему класс называется именно так, непонятно, ведь с Pebble у данного устройства нет ничего общего. Открыв исходник класса я увидел следующее:
После долгого осмысления я пришел к выводу, что fromString создает картинку с буквой используя определенный шрифт (который вшит в приложение), а потом конвертирует пиксели в 0 или 1 в зависимости от заполнения, таким образом, буква О, будет выглядеть примерно так:
00011100
01100011
01100011
01100011
00011100
Не особо вникая в подробности, я скопировал все в свой проект использовав BLE GATT пример от гугла.
И… О чудо!!! Браслет завибрировал! Но вот сообщение не отобразилось, пустая строка и значок входящего вызова.
Оказалось, что куча проверок размеров не спроста, браслет тупо игнорит черезчур длинные сообщения и сообщения, длина которых 11 символов, хотя 12 отображает нормально. Пару часов танцев вокруг этих функций наконец дали результат, я научился отображать и русский и английский текст, а заодно узнал, что в группе сообщений есть несколько режимов работы:
Научив своё приложение пересылать мне уведомления от разных приложений, whatsapp, vk, viber, telegram и других, я решил, что пора научить браслет реагировать на входящие вызовы и уже, в конце-концов, задействовать единственную кнопку для сброса входящих.
Описывать этот процесс не буду, пост и так получился раздутым, скажу лишь, что реагировать на входящие оказалось не сложно, а вот задействовать кнопку — нет.
Все входящие сообщения от браслета, Zeroner перехватывал в специальном классе. входящий пакет имел заголовок группы команд и номер команды, после долгого дебага и тестов я выудил используемые группы, а потом нашел описание в коде Zeroner.
Благодаря этому, я смог реализовать полноценную работу с браслетом. Могу получать данные о шагах, о сне. Могу управлять настройками, ставить будильники. Обозначение байт самого пакета удалось достать из классов, сохраняющих данные в БД, все их я реализовал и у себя.
Немного подумав, я решил, что все это может пригодиться не только мне и написал новое приложение, которое содержит в себе все необходимые данные и функции для работы с браслетом, а так же реализует простой интерфейс для переправки оповещений от любого приложения на браслет.
WiliX iWown for Geek
С тех пор прошло много времени, и у многих после обновления до Android 6, приложение перестало работать. Оно так же не стабильно работает с прошивками браслетов 2-й версии. Но я надеюсь найти время на доработку.
Исходный код выложен на GitHub. Можно форкать и развлекаться как угодно. Все pull-request после review будут приниматься, и после тестов сразу же заливаться на Google Play.
На данный момент приложение умеет:
Реализовано подключение к Google Fit для сохранения данных о тренировках, но, как я не ковырял SDK к Fit, перерыл кучу ссылок и форумов, но так и не понял, как заставить фит отображать данные с кастомных устройств. Непонятно тогда, зачем эта функция вообще есть.
Если кто-то работал с Google Fit, и знает как заставить его использовать данные с кастомного сенсора для отображения графиков, расскажите в коментах или напишите мне, пользователи и я будем очень благодарны!
Так же была идея, подключить браслет к Sleep as Adnroid. Собственно для мониторинга сна и покупался браслет. Но, как оказалось, iWown умеет возвращать только продолжительность фаз сна. То есть уже посчитанные данные с акселерометра.
А Sleep as Android требует голые данные с акселерометра, причем с желательной периодичностью в 10 секунд.
В общем итоге. Приглашаю разработчиков и владельцев поддержать проект своим кодом, советами и чем угодно. Оставляйте pull-requist, делайте issue на Github.
Приложение оказалось очень популярно за рубежом, мне часто пишут иностранцы, просят что-то добавить/исправить/перевести.
Кстати, у iWown i5 есть несколько клонов, со схожими прошивками:
Vidonn X5
Harper BFB-301
Excelvan i5
Google Play — iWown for Geek
Репозиторий на GitHub
Обсуждение на 4pda
P.S. Начиная с 5-й версии, в андроидах появилась дополнительная категория в шторке, которая не отображается на экране блокировке.
Может кто-то подскажет, как перенести моё уведомление в эту категорию? Спасибо!
Эта история ждала своей публикации больше полугода, за это время многое изменилось, прошивки и ПО обновились и многие мои наработки уже устарели.
Предисловие
Активная работа большого количества компаний в области носимой техники и умных часов не оставляла покоя моей душе. Я видел в носимых устройствах с экраном большой потенциал. Нет, я не говорю о подсчете шагов и других фитнес-штуках, они безусловно классные, но пока кроме банальных «Поздравляем! Вы прошли 4км, сделали 20к+ шагов!» и красивых графиков прогресса и регресса, ничего особенного не придумали.
А вот то, что я могу получать уведомления прямо на дисплей на запястье — это удобно. Если я могу еще и как-то взаимодействовать с ним или с чем-то поблизости нажимая 1-2-3 кнопки — это еще круче.
В очередной раз бороздя просторы aliexpress, я наткнулся на фитнес-браслет iWown i5. Он сразу привлек моё внимание невероятно низкой ценой ( на тот момент около 800р с бесплатной доставкой ) и наличием OLED дисплея. Внимательно почитав описание продавца и отзывы покупателей, я решил заказать сие чудо.
Заявленные характеристики (перевод описания с aliexpress):
- Дисплей: OLED
- Батарея: литий-полимерная
- Зарядка: стандартная USB зарядка
- Работа в режиме ожидания: более 72-х часов
- Размеры: 69.1*15.8*11.2mm
- Вес: 18g
- Материал: Ремешок из ABS, стальная застежка
- Водонепроницаемость: IP55
- Рабочая температура: -20 ° C ~ + 45 ° C
- Рабочая температура флеш носителя: -40 ° C ~ + 45 ° C
Возможности:
- Спортивный монитор: все время записывает шаги и движения, пройденное расстояние и сожженные калории, все цифры рассчитываются с учетом Вашего веса и роста.
- Мониторинг качества сна: Пока Вы спите, трекер записывает фазы сна, определяя глубокий и быстрый сон, 8 групп бесшумных будильников позволяют будить Вас не тревожа других членов семьи
- Bluetooth 4.0 low-power беспроводная синхронизация
- Поддержка синхронизации с PC через USB
- Защита IP55: защищает устройство под сильным дождем, но не более
и другие «надуманные» плюсы в стиле китайского маркетинга
Меня сильно заинтересовала возможность трекать сон и будить в нужную фазу. Многие мои знакомые покупали недорогие фитнес трекеры именно из-за этой функции и были довольны mi band и тому подобными штуками. Мне в них всегда не хватало экрана, а тут все-в-одном.
В моей работе частенько приходиться разрабатывать простые приложения для Android, я решил, что если мне не хватит функционала родного приложения, напишу своё.
Посылка пришла довольно быстро и я тут же бросился изучать замечательный браслет. После часа игры с приложением Zeroner, которое по инструкции необходимо поставить на свой Android девайс, я понял, что функционал довольно скуден и печален. Zeroner как и все остальные производители делал акцент на подсчет шагов и калорий, выводя красивые графики, имеет функцию поиска телефона (об этом позже расскажу), может оповещать о входящем вызове, о приходе сообщения в facebook и whatsapp и пересылает уведомления с ОДНОГО любого выбранного приложения, которое будет считать как приложение для SMS.
Вибрация у браслета весьма спорная, на форумах пишут что слабовата, некоторые говорят, нормальная. По мне так, можно было бы и по сильнее. У браслета есть реакция на жест «Посмотреть на часы», если посмотреть на браслет как на наручные часы, поднимая руку и сгибая в локте, автоматически включится экран и покажет время или пропущенное уведомление.
В общем, не долго думая, я решил писать своё приложение, с уведомлениями, вибрацией и синхронизацией. Забегу вперед, на это ушло 4 выходных дня и несколько долгих вечеров…
К делу
Учитывая что с Bluetooth я не в-синий-зуб-ногой, с дуру решил попытаться перехватить данные, которыми обменивается телефон и браслет. Для этого я полез во вкладку для разработчиков, и включил галку «Включить журнал трансляции HCI Bluetooth». После включения этой опции, весь дамп общения андроида с любыми Bluetooth устройствами складывается в файл /sdcard/Android/data/btsnoop_hci.log (у разных устройств путь может меняться, имя файла вроде всегда одинаковое).
Скачав WireShark я принялся изучать логи общения с браслетом и увидел что-то похожее на это:
Проведя почти два часа, изучая логи, проводя зависимости, гугля в интернете протоколы, я понял, что такой путь не для меня.
Так как мой телефон все-же интерпретировал браслет как обычное BLE устройство и показывал его в разделе подключенных устройств, я решил воспользоваться примерами работы с BLE из Android SDK.
Склонировав репозиторий https://github.com/googlesamples/android-BluetoothLeGatt, натравил Android Studio на пупку с исходниками, собрал и запустил приложение. (Ссылка на описание работы Android SDK с Bluetooth LE)
Получилось как на картинках с гитхаба:
Запустив сканирование, приложение не увидело устройство. Оказалось, что родное приложение подключившись к браслету не давало BLE найти устройство. Все решилось простым удалением Zeroner, можно было просто отключить, но надежнее снести полностью.
И так, Bluetooth LE — это технология которая строится на устройствах с малым потреблением энергии, используется в новомодных датчиках, метках и многих других устройствах. Основой этой технологии служит Generic Attribute Profile (GATT), это Bluetooth профиль, позволяющий обмениваться маленькими порциями данных, «атрибутами». Не буду долго расписывать как это все работает, на хабре и в инете есть куча информации, которую мне также пришлось перерыть в поисках решений.
Я понадеялся, что все нужные мне данные хранятся в характеристиках и дескрипторах браслета, и я смогу получать и записывать данные безо всяких проблем. Я ошибался…
Тестовое BLE приложение показывало мне всего 4 сервиса:
0000180f-0000-1000-8000-00805f9b34fb
00001800-0000-1000-8000-00805f9b34fb
0000ff20-0000-1000-8000-00805f9b34fb
00001801-0000-1000-8000-00805f9b34fb
в них было очень мало характеристик, те, что читались, возвращали пустоту или нули, а писать было бесполезно. Но меня воодушевило то, что я смог подключиться и получить хоть какие-то данные.
Далее, я решил, что в слепую действовать не получиться и решил препарировать приложение Zeroner. Накопав в интернете пару онлайн APK декомпиляторов, я скормил им zeroner.apk и получил на выходе 2 zip архива.
Первый был JADX вариант, а воторой содержал результат работы apktool.
Роясь в исходниках я ужасался китайскому коду (хотя в работе я часто с ним сталкиваюсь в виде бэендов для сайтов и сервисов, но он не перестает удивлять своей извилистостью и изобретательностью, но как ни крути, он ужасно тяжело читается)
После долгих изысканий, я наконец наткнулся на файл WristBandDevice.java, который находился по пути com.kunekt/bluetooth.
В этом классе как раз и скрывалась вся работа с устройством, но опять таки, меня ждала засада.
Как позже выяснилось, в предыдущих прошивках браслета использовалось больше сервисов в характеристик (как я ранее и предполагал), но позже, разработчики оставили всего 2, одна на чтение, вторая на запись. Все команды передаются в одном пакете.
Понять как должен выглядеть пакет оказалось не так просто, я решил четко определиться, чего я хочу от браслета в первую очередь, что бы начать прослеживать вызовы функций. А хотел я, отображать кастомные сообщения на браслете.
Не долго думая, я полез в com.kunekt/receiver/CallReceiver.java, так как входящие вызовы отображались очень стабильно и даже русскими символами, я решил что это отличное начало, учитывая что я уже сталкивался с событием входящих вызовов в Android, представление о том, как это может работать уже было.
Открыв файл я увидел это:
Большой кусок китайского кода
public void onReceive(Context context, Intent intent) {
Log.e(this.TAG, "+++ ON RECEIVE +++");
switch (((TelephonyManager) context.getSystemService("phone")).getCallState()) {
case C08571.POSITION_OPEN /*0*/:
if (ZeronerApplication.newAPI) {
BackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, WristBandDevice.getInstance(context).setPhoneStatue()));
}
case BitmapCacheManagementTask.MESSAGE_INIT_DISK_CACHE /*1*/:
incomingNumber = intent.getStringExtra("incoming_number");
Contact contact = getContact(context, incomingNumber);
if (!WristBandDevice.getInstance(context).isConnected() || !ZeronerApplication.phoneAlert) {
return;
}
if (ZeronerApplication.newAPI) {
this.fMdeviceInfo = jsonToFMdeviceInfo(UserConfig.getInstance(context).getDevicesInfo());
if (this.fMdeviceInfo.getModel().indexOf("5+") != -1) {
if (UserConfig.getInstance(context).getFont_lib() == 1 || UserConfig.getInstance(context).getFont_lib() == 2 || UserConfig.getInstance(context).getSysFont().equalsIgnoreCase("en") || UserConfig.getInstance(context).getSysFont().equalsIgnoreCase("es")) {
if (contact.getDisplayName().length() > 11) {
WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName().substring(0, 11));
} else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) {
WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName());
} else {
WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName().substring(0, contact.getDisplayName().length()));
}
} else if (contact.getDisplayName().length() > 11) {
WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, 11));
} else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) {
WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName());
} else {
WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, contact.getDisplayName().length()));
}
} else if (contact.getDisplayName().length() > 11) {
WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, 11));
} else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) {
WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName());
} else {
WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, contact.getDisplayName().length()));
}
} else if (contact.getDisplayName().length() > 11) {
WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName().substring(0, 11));
} else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) {
WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName());
} else {
WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName().substring(0, contact.getDisplayName().length()));
}
case BitmapCacheManagementTask.MESSAGE_FLUSH /*2*/:
if (ZeronerApplication.newAPI) {
BackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, WristBandDevice.getInstance(context).setPhoneStatue()));
}
default:
}
}
Тут мы явно видим, что существует 2 варианта API и названия у них очень логичные newAPI, а второе соответственно oldAPI. Во всем этом обилии условий, меня заинтересовала только одна, повторяющаяся строка:
WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName.....)
Это было то самое, что я искал. Забегая вперед, скажу, что у iWown есть еще модели i5+ и i6, у них экран больше и соответственно символов помещается больше, для этого и нужны все эти проверки. непонятно почему они не написали класс или что-то вроде того, возможно это шалости декомпелятора, но данный код повторяется во многих местах.
Перейдя к определению этой функции, я увидел это:
public void writeWristBandPhoneAlertNew(Context context, String displayName) {
writeAlertNew(context, displayName, 1);
}
public void writeWristBandSmsAlertNew(Context context, String displayName) {
writeAlertNew(context, displayName, 2);
}
Отлично, используется одна и та же функция для отправки текста, просто с разными параметрами. Все функции со словом New — это как раз наш вариант, потому что как выяснилось выше, API у меня new.
Радостно перейдя к определению функции writeAlertNew, я увидел следующее:
private void writeAlertNew(Context context, String displayName, int type) {
ArrayList<Byte> datas = new ArrayList();
datas.add(Byte.valueOf((byte) type));
int i = 0;
while (i < displayName.length()) {
if (displayName.charAt(i) < '@' || (displayName.charAt(i) < '\u0080' && displayName.charAt(i) > '`')) {
char e = displayName.charAt(i);
datas.add(Byte.valueOf((byte) 0));
for (byte valueOf : PebbleBitmap.fromString(context, String.valueOf(e), 8, 1).data) {
datas.add(Byte.valueOf(valueOf));
}
} else {
char c = displayName.charAt(i);
datas.add(Byte.valueOf((byte) 1));
for (byte valueOf2 : PebbleBitmap.fromString(context, String.valueOf(c), 16, 1).data) {
datas.add(Byte.valueOf(valueOf2));
}
}
i++;
}
byte[] data = writeWristBandDataByte(true, form_Header(3, 1), datas);
for (i = 0; i < data.length; i += 20) {
byte[] writeData;
if (i + 20 > data.length) {
writeData = Arrays.copyOfRange(data, i, data.length);
} else {
writeData = Arrays.copyOfRange(data, i, i + 20);
}
NewAgreementBackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, writeData));
}
}
Было понятно, что от профита меня отделяет пара функций, которые используются здесь.
writeWristBandDataByte — формирует пакет с сообщением для браслета, интересно, что есть специальная функция form_Header(3, 1), которая формирует заголовок пакета, по которому браслет понимает чего от него хотят. 3 — это номер группы команд, а 1 — это сама команда
public static byte form_Header(int grp, int cmd) {
return (byte) (((((byte) grp) & 15) << 4) | (((byte) cmd) & 15));
}
Функция простая, скопировал себе в проект без изменений. Следующее было это
NewAgreementBackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, writeData));
Как оказалось, ничего необычного, приложение создает поток, в котором постоянно проверяется очередь пакетов на отправку, если в очереди появляется пакет, поток выполняет запись в заданную характеристику устройства, если пакетов больше чем один, она отправляет их с задержкой в 240 миллисекунд.
Далее шло самое непонятное:
PebbleBitmap.fromString(context, String.valueOf(e), 8, 1).data)
Почему класс называется именно так, непонятно, ведь с Pebble у данного устройства нет ничего общего. Открыв исходник класса я увидел следующее:
Исходник класса PebbleBitmap
public class PebbleBitmap {
public static boolean f1285D;
public final byte[] data;
public final UnsignedInteger flags;
public final short height;
public int index;
public int offset;
public final UnsignedInteger rowLengthBytes;
public final short width;
public final short f1286x;
public final short f1287y;
static {
f1285D = true;
}
private PebbleBitmap(UnsignedInteger _rowLengthBytes, UnsignedInteger _flags, short _x, short _y, short _width, short _height, byte[] _data) {
this.offset = 0;
this.index = 0;
this.rowLengthBytes = _rowLengthBytes;
this.flags = _flags;
this.f1286x = _x;
this.f1287y = _y;
this.width = _width;
this.height = _height;
this.data = _data;
}
public static PebbleBitmap fromString(Context context, String text, int w, int l) {
TextPaint textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setTextSize(16.5f);
if (w == 32) {
textPaint.setTextAlign(Align.CENTER);
}
textPaint.setTypeface(ZeronerApplication.unifont);
StaticLayout sl = new StaticLayout(text, textPaint, w, Alignment.ALIGN_NORMAL, 1.0f, 0.49f, false);
int h = sl.getHeight();
if (h > l * 16) {
h = l * 16;
}
Bitmap newBitmap = Bitmap.createBitmap(w, h, Config.ARGB_8888);
sl.draw(new Canvas(newBitmap));
return fromAndroidBitmap(newBitmap);
}
public static PebbleBitmap fromAndroidBitmap(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int rowLengthBytes = width / 8;
ByteBuffer data = ByteBuffer.allocate(rowLengthBytes * height);
data.order(ByteOrder.LITTLE_ENDIAN);
StringBuffer stringBuffer = new StringBuffer(StatConstants.MTA_COOPERATION_TAG);
for (int y = 0; y < height; y++) {
int[] pixels = new int[width];
bitmap.getPixels(pixels, 0, width * 2, 0, y, width, 1);
stringBuffer = new StringBuffer(StatConstants.MTA_COOPERATION_TAG);
for (int x = 0; x < width; x++) {
if (pixels[x] == 0) {
stringBuffer.append(Constants.VIA_RESULT_SUCCESS);
if (f1285D) {
stringBuffer.append("-");
}
} else {
stringBuffer.append(Constants.VIA_TO_TYPE_QQ_GROUP);
if (f1285D) {
stringBuffer.append("#");
}
}
}
for (int k = 0; k < rowLengthBytes * 8; k += 8) {
ByteBuffer byteBuffer = data;
byteBuffer.put(Byte.valueOf((byte) new BigInteger(stringBuffer.substring(k, k + 8), 2).intValue()).byteValue());
}
if (f1285D) {
stringBuffer.append("\n");
}
Log.i("info", stringBuffer.toString());
}
if (f1285D) {
System.out.println(stringBuffer.toString());
}
if (!(bitmap == null || bitmap.isRecycled())) {
bitmap.recycle();
}
System.gc();
return new PebbleBitmap(UnsignedInteger.fromIntBits(rowLengthBytes), UnsignedInteger.fromIntBits(DfuSettingsConstants.SETTINGS_DEFAULT_MBR_SIZE), (short) 0, (short) 0, (short) width, (short) height, data.array());
}
public static PebbleBitmap fromPng(InputStream paramInputStream) throws IOException {
return fromAndroidBitmap(BitmapFactory.decodeStream(paramInputStream));
}
}
После долгого осмысления я пришел к выводу, что fromString создает картинку с буквой используя определенный шрифт (который вшит в приложение), а потом конвертирует пиксели в 0 или 1 в зависимости от заполнения, таким образом, буква О, будет выглядеть примерно так:
00011100
01100011
01100011
01100011
00011100
Не особо вникая в подробности, я скопировал все в свой проект использовав BLE GATT пример от гугла.
И… О чудо!!! Браслет завибрировал! Но вот сообщение не отобразилось, пустая строка и значок входящего вызова.
Оказалось, что куча проверок размеров не спроста, браслет тупо игнорит черезчур длинные сообщения и сообщения, длина которых 11 символов, хотя 12 отображает нормально. Пару часов танцев вокруг этих функций наконец дали результат, я научился отображать и русский и английский текст, а заодно узнал, что в группе сообщений есть несколько режимов работы:
- Входящий вызов. Отображается трубка, имя звонящего и браслет вибрирует
- Сообщение. Отображается текст и значок конверта. При появлении вибрирует 2 раза
- Облачко. Тоже самое что и 2, только вместо конвертика, иконка облачка
- Ошибка. Тоже что и 2, что только иконка с восклицательным знаком.
Научив своё приложение пересылать мне уведомления от разных приложений, whatsapp, vk, viber, telegram и других, я решил, что пора научить браслет реагировать на входящие вызовы и уже, в конце-концов, задействовать единственную кнопку для сброса входящих.
Описывать этот процесс не буду, пост и так получился раздутым, скажу лишь, что реагировать на входящие оказалось не сложно, а вот задействовать кнопку — нет.
Все входящие сообщения от браслета, Zeroner перехватывал в специальном классе. входящий пакет имел заголовок группы команд и номер команды, после долгого дебага и тестов я выудил используемые группы, а потом нашел описание в коде Zeroner.
Группы и команды браслета
// HEADER GROUPS //
DEVICE = 0
CONFIG = 1
DATALOG = 2
MSG = 3
PHONE_MSG = 4
// CONFIG = 1 ///
CMD_ID_CONFIG_GET_AC = 5
CMD_ID_CONFIG_GET_BLE = 3
CMD_ID_CONFIG_GET_HW_OPTION = 9
CMD_ID_CONFIG_GET_NMA = 7
CMD_ID_CONFIG_GET_TIME = 1
CMD_ID_CONFIG_SET_AC = 4
CMD_ID_CONFIG_SET_BLE = 2
CMD_ID_CONFIG_SET_HW_OPTION = 8
CMD_ID_CONFIG_SET_NMA = 6
CMD_ID_CONFIG_SET_TIME = 0
// DATALOG = 2 //
CMD_ID_DATALOG_CLEAR_ALL = 2
CMD_ID_DATALOG_GET_BODY_PARAM = 1
CMD_ID_DATALOG_SET_BODY_PARAM = 0
CMD_ID_DATALOG_GET_CUR_DAY_DATA = 7
CMD_ID_DATALOG_START_GET_DAY_DATA = 3
CMD_ID_DATALOG_START_GET_MINUTE_DATA = 5
CMD_ID_DATALOG_STOP_GET_DAY_DATA = 4
CMD_ID_DATALOG_STOP_GET_MINUTE_DATA = 6
// DEVICE = 0 //
CMD_ID_DEVICE_GET_BATTERY = 1
CMD_ID_DEVICE_GET_INFORMATION = 0
CMD_ID_DEVICE_RESE = 2
CMD_ID_DEVICE_UPDATE = 3
// MSG = 3 //
CMD_ID_MSG_DOWNLOAD = 1
CMD_ID_MSG_MULTI_DOWNLOAD_CONTINUE = 3
CMD_ID_MSG_MULTI_DOWNLOAD_END = 4
CMD_ID_MSG_MULTI_DOWNLOAD_START = 2
CMD_ID_MSG_UPLOAD = 0
// PHONE_MSG = 4 //
CMD_ID_PHONE_ALERT = 1
CMD_ID_PHONE_PRESSKEY = 0
Благодаря этому, я смог реализовать полноценную работу с браслетом. Могу получать данные о шагах, о сне. Могу управлять настройками, ставить будильники. Обозначение байт самого пакета удалось достать из классов, сохраняющих данные в БД, все их я реализовал и у себя.
В итоге
Немного подумав, я решил, что все это может пригодиться не только мне и написал новое приложение, которое содержит в себе все необходимые данные и функции для работы с браслетом, а так же реализует простой интерфейс для переправки оповещений от любого приложения на браслет.
WiliX iWown for Geek
С тех пор прошло много времени, и у многих после обновления до Android 6, приложение перестало работать. Оно так же не стабильно работает с прошивками браслетов 2-й версии. Но я надеюсь найти время на доработку.
Исходный код выложен на GitHub. Можно форкать и развлекаться как угодно. Все pull-request после review будут приниматься, и после тестов сразу же заливаться на Google Play.
На данный момент приложение умеет:
- Отображать уведомления от любого приложения
- Отображать входящий звонок
- Сбрасывать входящий при нажатии на кнопку
- Искать телефон если он в зоне действия BT
- Управлять настройками браслета
- И некоторые другие мелкие функции
Реализовано подключение к Google Fit для сохранения данных о тренировках, но, как я не ковырял SDK к Fit, перерыл кучу ссылок и форумов, но так и не понял, как заставить фит отображать данные с кастомных устройств. Непонятно тогда, зачем эта функция вообще есть.
Если кто-то работал с Google Fit, и знает как заставить его использовать данные с кастомного сенсора для отображения графиков, расскажите в коментах или напишите мне, пользователи и я будем очень благодарны!
Так же была идея, подключить браслет к Sleep as Adnroid. Собственно для мониторинга сна и покупался браслет. Но, как оказалось, iWown умеет возвращать только продолжительность фаз сна. То есть уже посчитанные данные с акселерометра.
А Sleep as Android требует голые данные с акселерометра, причем с желательной периодичностью в 10 секунд.
В общем итоге. Приглашаю разработчиков и владельцев поддержать проект своим кодом, советами и чем угодно. Оставляйте pull-requist, делайте issue на Github.
Приложение оказалось очень популярно за рубежом, мне часто пишут иностранцы, просят что-то добавить/исправить/перевести.
Кстати, у iWown i5 есть несколько клонов, со схожими прошивками:
Vidonn X5
Harper BFB-301
Excelvan i5
Ссылки
Google Play — iWown for Geek
Репозиторий на GitHub
Обсуждение на 4pda
P.S. Начиная с 5-й версии, в андроидах появилась дополнительная категория в шторке, которая не отображается на экране блокировке.
Может кто-то подскажет, как перенести моё уведомление в эту категорию? Спасибо!