Android, Rx и Kotlin или как заставить сжиматься клешню из Lego. Часть 1 +19


Привет, любители Habr! По счастливой случайности в августе 2018 года мне посчастливилось вместе с моим товарищем(kirillskiy) начать работать над потрясающим по своей интересности проектом. И вот, днем мы были обычными программистами, а ночью с?у?п?е?р?г?е?р?о?я?м?и? снова программистами, которые бьются над вопросами распознавания движений для людей имеющих ограничения функциональности своих конечностей, естественно этим могли бы пользоваться и здоровые люди, используя подобную технологию самыми разными способами.

В этой статье, Кирилл в общих чертах рассказывает о проекте, я же расскажу подробнее и затрону тему андроида в нем.
Расскажу вначале о проекте целиком, что мы напридумывали и как захотели реализовать это:

1) Была выбрана ЭМГ( Электромиография — регистрация электрической активности мышц) как способ получения данных( о, да, данных будет много). Впервые данный способ был применен в 1907 году, поэтому мы шли проторенной дорогой.

2) Нашли 8-ми канальный ЭМГ датчик работающий по bluetooth(даже имеющий свой API, которое в итоге, оказалось абсолютно бесполезным, ибо пришлось самостоятельно коннектиться как BT устройству. Спасибо хоть спецификацию написали)

3) Мы решили, что работать все будет так:

  • режим тренировки. Одеваем датчик на предплечье, выбираем тип движения, который будем тренировать. Например… «сгибание кисти». и начинаем тернировку(раз 12 повторяем движение). Полученные в этот момент данные, мы сохраним и отправим потом на сервер, где обучим нейросеточку(спокойно, про это тоже расскажу)
  • режим распознавания непосредственно движения. Данные снятые в процессе движения сопоставляются уже с моделью, поолученной в результате обучения нейросетки. По результатам мы уже получем «СГИБАНИЕ КИСТИ», например.
  • режим движения. Надо согласно определенному типу движения, что-то заставить перемещаться. Например манипулятор собранный на кухне из конструктора(ппц, какого дорогого) известного датского производителя.

4) Пункт Android. Я Android developer — и грех было этим не воспользоваться. Андроид делает у нас вот что:

  • находит все доступные BT устройства
  • подключается к датчику
  • отрисовывает график, основываясь на данных снятых с датчиков(8 каналов, частота 200гц). 8 красивейших, разноцветных кривых.
  • реализует режим тренировки (выбор типа обучаемого движения, кнопка старта обучения, кнопка отправки данных)
  • реализует клиент-серверное взаимодействие. Надобно отправить данные на сервер, чтоб обучалась нейросетка
  • реализует соединение и взаимодействие с Raspberry PI 3B, к которой припаяны моторы, приподящие в движение манипулятор.

5) Raspberry PI 3B. именно на малину мы поставили Android Things, а потом на ней подняли BT server, который принимает сообщения от Android устройства и двигает соответствующие моторы, приводящие в движение супер-клешню из LEGO.

6) Сервер. Разворачивается Докером локально на компе. Принимает высылаемые вашим устройством данные, обучает нейросеть, возвращает модель.

Часть номер 1. Android. В этот раз рассматрим подкапотное пространство проекта, касающееся Android до момента отправки данных на сервер.

Он называется NUKLEOS (https://github.com/cyber-punk-me/nukleos)
Стэк:

— Kotlin
— MVP
— Dagger2
— Retrofit2
— RxKotlin, RxAndroid

для Raspberry:

-Android Things

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

Приложение состоит из одной БоттомНавигейшн стайл активити и 4х фрагментов:
Первый из них — «Список всех доступных BT устройств»

Мы выбрали 8ми канальный BT датчик, у которого был свой API для работы с BT. К сожалению api оказался абсолютно бесполезным, ибо он сразу предлагал определить один из 6(вроде) типов движения, но точность распознавания была 80% — а это никуда не годится. Ну и нам были нужны фактические данные. Значение изменения биоэлектрических потенциалов, возникающих в мышцах человека при возбуждении мышечного волокона. А для этого надо было работать с этим датчиком напрямую. Создатели оставили описание протокола работы с ним, поэтому ковыряться пришлось не так долго. пример работы с голыми BT устройствами могу описать в отдельной статье, если будет интересно, но вкратце выглядит это так:

class BluetoothConnector(val context: Context) {

    private val mBTLowEnergyScanner by lazy {
        (context.getSystemService(Activity.BLUETOOTH_SERVICE) as BluetoothManager)
                .adapter.bluetoothLeScanner
    }

    private var mBluetoothScanCallback: BluetoothScanCallback? = null
    // scan.

    fun startBluetoothScan(serviceUUID: UUID?) = Flowable.create<BluetoothDevice>({
        mBluetoothScanCallback = BluetoothScanCallback(it)
        if (serviceUUID == null) {
            mBTLowEnergyScanner.startScan(mBluetoothScanCallback)
        } else {
            mBTLowEnergyScanner.startScan(
                    arrayListOf(ScanFilter.Builder().setServiceUuid(ParcelUuid(serviceUUID)).build()),
                    ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(),
                    mBluetoothScanCallback)
        }
    }, BackpressureStrategy.BUFFER).apply {
        doOnCancel { mBTLowEnergyScanner.stopScan(mBluetoothScanCallback) }
    }

    // scan with timeout
    fun startBluetoothScan(interval: Long, timeUnit: TimeUnit, serviceUUID: UUID? = null) = startBluetoothScan(serviceUUID).takeUntil(Flowable.timer(interval, timeUnit))

    inner class BluetoothScanCallback(private val emitter: FlowableEmitter<BluetoothDevice>) : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            result?.let {
                it.device.apply { emitter.onNext(this) }
            }
        }

        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            emitter.onError(RuntimeException())
        }
    }
}

Аккуратно оборачиваем стандартный BT service в RX и получаем меньше боли.

Далее, запускаем сканирование, и благодаря rx на подписке формируем список всех устройств, напихивая их в RecyclerView:

mFindSubscription = mFindFlowable
                        ?.subscribeOn(Schedulers.io())
                        ?.observeOn(AndroidSchedulers.mainThread())
                        ?.subscribe({
                            if (it !in mBluetoothStuffManager.foundBTDevicesList) {
                                addSensorToList(SensorStuff(it.name, it.address))
                                mBluetoothStuffManager.foundBTDevicesList.add(it)
                            }
                        }, {
                            hideFindLoader()
                            showFindError()
                            if (mBluetoothStuffManager.foundBTDevicesList.isEmpty()) {
                                showEmptyListText()
                            }
                        }, {
                            hideFindLoader()
                            showFindSuccess()
                            if (mBluetoothStuffManager.foundBTDevicesList.isEmpty()) {
                                showEmptyListText()
                            }
                        })       

Выбрав одно из устройств, выбираем его, и переходим на следующий экран:
«Настройки датчика»

Подключаемся к нему и запускаем стриминг данных датчика, пользуясь заранее подготовленными нами командами. Благо, что протокол работы с этим устройством создателями датчика описан:

object CommandList {

    //Stop the streaming
    fun stopStreaming(): Command {
        val command_data = 0x01.toByte()
        val payload_data = 3.toByte()
        val emg_mode = 0x00.toByte()
        val imu_mode = 0x00.toByte()
        val class_mode = 0x00.toByte()
        return byteArrayOf(command_data, payload_data, emg_mode, imu_mode, class_mode)
    }

    // Start streaming (with filter)
    fun emgFilteredOnly(): Command {
        val command_data = 0x01.toByte()
        val payload_data = 3.toByte()
        val emg_mode = 0x02.toByte()
        val imu_mode = 0x00.toByte()
        val class_mode = 0x00.toByte()
        return byteArrayOf(command_data, payload_data, emg_mode, imu_mode, class_mode)
    }
    .....

Работа с устройством также заботливо обернута в rx, чтобы работать без боли.

Датчики возвращают БайтАррэи естесственно, и надо было запилить конвертор, частота работы датчиков 200ГЦ… если будет интересно, могу описать подробно(ну или код посмотрите), но в итоге мы работаем с достаточно большим кол-вом данных таким образом:

1 — Нам надо отрисовывать кривые каждого из датчиков. Разумеется, что отрисовывать АБСОЛЮТНО все данные нет смысла, ибо на мобильном устройстве глазу нет смысла рассматривать 200 изменений в секунду на каждом датчике. Поэтому мы будем брать не все.

2 — Нам надо работать со всем объемом данных, если это процесс обучения или распознавания.

для этих нужд RX — идеально подходит со всеми своими фильтрами.

Графики пришлось сделать свои. Кому интересно — посмотрите PowerfullChartsView в папке views.

А теперь немного видео:


На видео вы увидите, как Кирилл работает с системой в целом. На видео ведется работа с моделью. Но модель находится на сервере. В дальнейшем она конечно же будет находиться на девайсе, что существенно ускорит отклик)

Пишите, какие аспекты интересны, какие рассказать подробнее. Естественно, мы работаем над проектом и открыты к вашим предложениям.

Весь проект на github тут




К сожалению, не доступен сервер mySQL