Купи китайский браслет, разочаруйся в официальном ПО, напиши свое!




Эта история ждала своей публикации больше полугода, за это время многое изменилось, прошивки и ПО обновились и многие мои наработки уже устарели.

Предисловие


Активная работа большого количества компаний в области носимой техники и умных часов не оставляла покоя моей душе. Я видел в носимых устройствах с экраном большой потенциал. Нет, я не говорю о подсчете шагов и других фитнес-штуках, они безусловно классные, но пока кроме банальных «Поздравляем! Вы прошли 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 отображает нормально. Пару часов танцев вокруг этих функций наконец дали результат, я научился отображать и русский и английский текст, а заодно узнал, что в группе сообщений есть несколько режимов работы:
  1. Входящий вызов. Отображается трубка, имя звонящего и браслет вибрирует
  2. Сообщение. Отображается текст и значок конверта. При появлении вибрирует 2 раза
  3. Облачко. Тоже самое что и 2, только вместо конвертика, иконка облачка
  4. Ошибка. Тоже что и 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-й версии, в андроидах появилась дополнительная категория в шторке, которая не отображается на экране блокировке.
Может кто-то подскажет, как перенести моё уведомление в эту категорию? Спасибо!

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


  1. lanseg
    29.02.2016 11:55

    Надо будет реверс-инженирить emotiv insight — вот уж у чего софт ужасный, а api мутное.


  1. tmin10
    29.02.2016 12:05

    Отличная статья! У меня у самого был Vidonn X5, но к сожалению, вместо заявленных 4х дней работы он работал всего 7 часов и использовать его по назначению было нереально. Думаю подпаять новый АКБ и повесить коту на ошейник, чтобы мониторить его активность. Пока в поисках подходящего аккумулятора.


    1. Wilix
      29.02.2016 12:12

      Видимо в браслете попался битый аккум. На али есть недорогие и маленькие аккумуляторы. Заказал себе, но не удалось воспользоваться. Валяются уже больше года. Боюсь как-бы не по портились


      1. tmin10
        29.02.2016 12:27

        Для квадрокоптеров полно батареек, надо будет разобрать и посмотреть, какой там вольтаж. Боюсь только при разборке придётся USB отломать...


        1. CRImier
          29.02.2016 13:11
          +3

          Вольтаж будет 3.7 в любом случае, не беспокойтесь.


    1. gregox
      29.02.2016 17:10

      Я вешал на кота Xiaomi Mi Band, безрезультатно. Кот двигается слишком плавно, с иной амплитудой, и браслет считает только прыжки, и те далеко не все.


      1. tmin10
        29.02.2016 17:15

        Я вроде бы на своего вешал X5, и браслет что-то показывал. Надо будет повторить эксперимент на все 7 часов работы батарейки.

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


        1. Wilix
          29.02.2016 20:25

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


  1. f1ac
    29.02.2016 13:22

    Хех, так же пытался написать приложение к своему безымянному браслету, но споткнулся об зашифрованное оригинальное приложение.


  1. av0000
    29.02.2016 13:54

    Годно!

    А у меня с iWown for Geek не сложилось.
    Тоже польстился на экранчик после miBand и взял подешёвке i5. Вдумчиво полистав 4pda, с маркета был установлен iWown for Geek и, вроде бы, надо начать радоваться.

    Но оказалось, что будильник работает только, когда есть связь браслета с телефоном (он её регулярно терял), и батарея съелась за полдня вместо двух. Стал смотреть, кто виноват — синезуб. Создалось ощущение, что он не по BLE, а по обычному каналу общался...

    В итоге поставил "родной" zeroner и мирюсь с косяками будильников-в-рабочие-дни, зато батарейка живёт нормально и будильник будит независимо от отвала связи с телефоном (раз в неделю перестаёт ловиться до перезагрузки телефона)


    1. Wilix
      29.02.2016 14:11

      Боюсь, я где-то в приложении ошибся. Надо бы его оптимизировать. Для поддержания связи с браслетом, с некоторым интервалом идет запрос состояния батареи.
      Родной будильник поддерживается в коде. Для него просто нет интерфейса. Постараюсь добавить в будущем.
      По поводу связи, такое ощущение, что в прошивках самого браслета работа BLE происходит по разному, на старой прошивке у меня чаще отваливался браслет чем в новой. Но в новой, браслет начинает видится сразу, только если, несколько раз нажать на кнопку, прокрутив от экрана часов до них же. Появляется значок спутниковой тарелки


      1. av0000
        29.02.2016 14:22

        У меня были мысли взять код с гитхаба и поковыряться, но как-то всё не до того.
        А значок тарелки, как я понял из зеронера, это поиск связи, а не её наличие. Будем пробовать ещё...


        1. Wilix
          29.02.2016 14:39

          Именно. Значок обозначает режим поиска, в этот момент моё приложение успевает подключиться к браслету и потом связь держится какое-то время. Видимо, потом браслет засыпает так, что связь теряется до тех пор, пока не загорится экран или не будет нажата кнопка.

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


          1. av0000
            29.02.2016 14:51

            во-во, со второй-то версией и мне пришло. Типа, с самой свежей — на конец декабря где-то. Видимо, что-то поменяли.

            Кстати, а победить пробелы в русском шрифте удалось? Раз уж там всё равно генерится картинка перед отправкой, может сразу поправить работу с двух-байтными символами? Чую, тут, как в старых mp3 плеерах — нули из utf-8 выводятся пробелами


            1. Wilix
              29.02.2016 15:05

              С картинкой оказалось все не так просто. У браслета есть 2 режима приёма изображения:
              1 — Single. Используется в моем приложении, генерируется строка в виде 0101 и к ней потом лепится картинка конверта.
              2 — Large. Так и не получилось завести, но по видимому, есть возможность загнать туда изображение на весь экран.

              В первом варианте я пытался менять шрифт, в исходниках кстати осталось парочку, которые неплохо отображались. Пытался менять размер картинки, что бы больше символов уместилось, но что-то все так нестабильно работало, просто ужас. А учитывая, что для тестов приходилось постоянно обновлять приложение на телефоне — эксперименты были крайне неудобными и долгими.

              Возможно, стоит написать тест приложение, в котором можно было бы сразу менять необходимые параметры. Может тогда добьюсь чего-то адекватного.

              Но второй режим не оставляет покоя. Если действительно можно запихнуть картинку на весь экран — это просто круто, можно и свои иконки рисовать и шрифт любого размера сделать и еще много всего :)


  1. tmpuser
    29.02.2016 15:06

    Насчёт вопроса: нужно указать минимальный приоритете в флаге (MIN)
    notification.priority = Notification.PRIORITY_MIN;
    Тут и тут подробнее.


    1. Wilix
      29.02.2016 15:06

      Кажется Ваши ссылки не вставились. Можете продублировать?


    1. Wilix
      29.02.2016 15:12

      Спасибо! Помогло!
      У меня кстати стояло Notification.PRIORITY_LOW. Я был близок :)


  1. dimabudnikov
    29.02.2016 16:23

    по MiBand есть новости, как-то можно расширить приложение?


    1. tmpuser
      29.02.2016 18:02

      В Маркете есть отличные платные приложения Mi Band Tools и Mi Band Notify с почти идентичным функционалом.
      Очень гибкая настройка уведомлений, предупреждение о малой активности, виджет с зарядом батареи и количеством шагов и другое.

      Есть готовая библиотека для управления браслетом:
      https://github.com/betomaluje/Mi-Band


      1. dimabudnikov
        29.02.2016 19:48

        забыл уточнить что для iOs


        1. tmpuser
          01.03.2016 10:09

          В Cydia поищите Mi Band Utilty или Mi Band Alert, они реализуют уведомления на браслете.


      1. vlivyur
        01.03.2016 14:28

        У меня mi band notify слегка быстрее сажает батарейку что у телефона, что у браслета (этому почти в два раза). Сравниваю с оригинальным mi fit в которое были добавлены уведомления (но оно сломалось после какого-то обновления)


    1. tmpuser
      29.02.2016 18:06

      Забыл, ещё на 4PDA есть кое-какие дополнения к приложению:
      http://4pda.ru/forum/index.php?showtopic=596501


  1. Salagin
    29.02.2016 20:18

    Отлична история! Раз уж пошла такая тема, может кто-то пульсометры ковырял? К сожалению, жаба душит покупать готовый и исследовать. Самый главный вопрос, как и в каком формате они передают данные по блутусу? Некоторые фитнес-программы позволяют подключать сторонние пульсометры по синезубу, есть навязчивая идея сделать свой, но с передачей данных пульса пока полная неясность.


    1. Wilix
      29.02.2016 20:27

      Я думаю, что пульсометры передают в уже обработанном виде, то есть число ударов в минуту.


      1. Salagin
        01.03.2016 05:52

        Проблема в том, что общение bt-девайса и телефона происходит совсем не просто, я по наивности надеялся, что bt-девайс просто кидает данные в эфир, а телефон их принимает. На деле, увидев bt-девайс, телефон затем ждет, что ему придет модель, производитель, пульс и заряд батареи в определенном формате. Как прикинутся пульсометром, пока одна мысль — исследовать файл фитнес программы. Хотя, возможно все-таки проще купить готовый браслет-пульсометр и не маяться.


    1. AlterMax
      01.03.2016 11:26

      Ковырял китайский датчик пульса CooSpo H6 BLE. Там все стандартно и довольно просто, с помощью примера android-BluetoothLeGatt можно быстро получить все необходимые данные, они читаются из характеристик, а после подписки на событие приходят автоматически с интервалом около 2 секунд в виде готовых данных в заданном стандартом формате.
      На стандарт можно взглянуть например здесь…


      1. AlterMax
        01.03.2016 11:33

        1. Salagin
          01.03.2016 13:29

          Спасибо за ссылку, почитаю. Мне нужно, чтобы дуина прикинулась пульсометром.


          1. Wilix
            01.03.2016 14:35

            Вот еще ссылка на спецификацию. Что бы Ваше устройство выглядело пульсометром, нужно реализовать описанные характертстики и сервисы. Если приложение ищет именно стандартный профиль, то должно работать.
            https://www.bluetooth.com/specifications/assigned-numbers/Health-Device-Profile