Android, Rx y Kotlin, o cómo hacer que una garra de Lego se encoja. Parte 1

¡Hola, amantes de Habr! Por una coincidencia afortunada, en agosto de 2018, tuve la suerte de comenzar a trabajar con mi amigo ( kirillskiy ) en un proyecto que fue increíble en su interés. Y así, durante el día éramos programadores ordinarios, y por la noche volvimos a ser programadores que luchan con problemas de reconocer movimientos para personas con funcionalidad limitada de sus extremidades, las personas naturalmente saludables podrían usar esto, utilizando tecnología similar en una variedad de formas.

En este artículo , Cyril en términos generales habla sobre el proyecto, pero contaré con más detalle y tocaré el tema de Android en él.
Primero, le contaré sobre todo el proyecto, lo que se nos ocurrió y cómo queríamos implementar esto:

1) Se eligió EMG (Electromiografía: registrar la actividad eléctrica de los músculos) como una forma de obtener datos (oh, sí, habrá muchos datos). Por primera vez, este método se aplicó en 1907, así que caminamos por los caminos trillados.

2) Encontramos un sensor EMG de 8 canales que funciona en bluetooth (incluso teniendo su propia API, que al final resultó ser absolutamente inútil, porque tuve que conectarme como un dispositivo BT. Gracias al menos escribimos una especificación)

3) Decidimos que todo funcionaría así:

  • modo de entrenamiento Vestimos el sensor en el antebrazo, seleccionamos el tipo de movimiento que entrenaremos. Por ejemplo ... "doblar el pincel". e inicie el entrenamiento (repita el movimiento 12 veces). Guardaremos los datos recibidos en este momento y luego los enviaremos al servidor, donde entrenaremos la red neuronal (con calma, también te contaré sobre esto)
  • Modo de reconocimiento de movimiento directo. Los datos tomados durante el movimiento se comparan con el modelo obtenido como resultado del entrenamiento de la red neuronal. Según los resultados, ya obtendremos un "CEPILLO DE CURVADO", por ejemplo.
  • modo de conducción Según un cierto tipo de movimiento, se debe hacer que algo se mueva. Por ejemplo, un manipulador ensamblado en la cocina de un diseñador (PPC, qué caro) de un famoso fabricante danés.

4) artículo de Android. Soy un desarrollador de Android, y fue un pecado no usarlo. Android hace aquí con nosotros:

  • encuentra todos los dispositivos BT disponibles
  • se conecta al sensor
  • dibuja un gráfico basado en los datos tomados de los sensores (8 canales, frecuencia 200Hz). 8 curvas hermosas y coloridas.
  • implementa el modo de entrenamiento (selección del tipo de movimiento entrenado, botón de inicio de entrenamiento, botón de envío de datos)
  • implementa la interacción cliente-servidor. Es necesario enviar datos al servidor para que la red neuronal esté capacitada.
  • implementa conexión e interacción con Raspberry PI 3B, a la que se sueldan los motores, que mueven el manipulador en movimiento.

5) Frambuesa PI 3B. pusimos Android Things en frambuesa, y luego levantamos el servidor BT, que recibe mensajes de un dispositivo Android y mueve los motores correspondientes, lo que puso en marcha una súper garra de LEGO.

6) Servidor. Docker lo implementa localmente en una computadora. Recibe datos enviados por su dispositivo, enseña una red neuronal y devuelve un modelo.

Número de pieza 1. Android. Esta vez consideraremos la campana del proyecto con respecto a Android hasta que los datos se envíen al servidor.

Se llama NUKLEOS (https://github.com/cyber-punk-me/nukleos)
Pila:

- Kotlin
- MVP
- Daga2
- Retrofit2
- RxKotlin, RxAndroid

para frambuesa:

Cosas de Android

En el trabajo, no me dejan jugar con la arquitectura, pero finalmente tuve la oportunidad de jugar con un juguete viejo llamado MVP.

La aplicación consta de un estilo de actividad de navegación inferior y 4 fragmentos:
El primero es "Lista de todos los dispositivos BT disponibles"

Elegimos un sensor BT de 8 canales, que tenía su propia API para trabajar con BT. Desafortunadamente, la API resultó ser absolutamente inútil, porque inmediatamente sugirió definir uno de los 6 (como) tipos de movimiento, pero la precisión de reconocimiento fue del 80%, y eso no fue bueno. Bueno, necesitábamos datos reales. El valor de los cambios en los potenciales bioeléctricos que ocurren en los músculos humanos tras la excitación de una fibra muscular. Y para esto era necesario trabajar con este sensor directamente. Los creadores dejaron una descripción del protocolo para trabajar con él, por lo que tuvieron que cavar por poco tiempo. Puedo describir un ejemplo de trabajo con dispositivos BT desnudos en un artículo separado, si es interesante, pero en pocas palabras se ve así:

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()) } } } 

Envuelva cuidadosamente el servicio BT estándar en RX y obtenga menos dolor.

A continuación, ejecute el escaneo y, gracias a rx en la suscripción, creamos una lista de todos los dispositivos rellenándolos en 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() } }) 

Al seleccionar uno de los dispositivos, selecciónelo y vaya a la siguiente pantalla:
"Configuración del sensor"

Nos conectamos a él y comenzamos a transmitir los datos del sensor utilizando los comandos que hemos preparado de antemano. Afortunadamente, se describe el protocolo para trabajar con este dispositivo por parte de los creadores del sensor:

 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) } ..... 

Trabajar con el dispositivo también se envuelve cuidadosamente en rx para trabajar sin dolor.

Los sensores devolvieron Byte Arrays de forma natural, y fue necesario cortar el convertidor, la frecuencia de los sensores era de 200 Hz ... si es interesante, puedo describirlo en detalle (bueno, o mirar el código), pero al final trabajamos con una cantidad suficientemente grande de datos de esta manera:

1 - Necesitamos dibujar las curvas de cada uno de los sensores. Por supuesto, no tiene sentido representar ABSOLUTAMENTE todos los datos, porque en el dispositivo móvil no tiene sentido que el ojo examine 200 cambios por segundo en cada sensor. Por lo tanto, no tomaremos todo.

2 - Necesitamos trabajar con toda la cantidad de datos, si se trata de un proceso de aprendizaje o reconocimiento.

Para estas necesidades, el RX es ideal para todos sus filtros.

Se deben hacer gráficos. A quién le importa: mira PowerfullChartsView en la carpeta de vistas.

Y ahora algunos videos:


En el video verá cómo funciona Cyril con el sistema en su conjunto. El video está trabajando con el modelo. Pero el modelo está en el servidor. En el futuro, por supuesto, estará en el dispositivo, lo que acelerará significativamente la respuesta)

Escriba qué aspectos son interesantes, cuáles contar con más detalle. Naturalmente, estamos trabajando en un proyecto y estamos abiertos a sus sugerencias.

Todo el proyecto Github está aquí.

Source: https://habr.com/ru/post/438034/


All Articles