Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Droidcon London - Hardware Development With KMP...

Avatar for Enrique Ramirez Enrique Ramirez
October 31, 2024
11

Droidcon London - Hardware Development With KMP and Compose Multiplatform

Avatar for Enrique Ramirez

Enrique Ramirez

October 31, 2024
Tweet

Transcript

  1. Matrix8 Requirements • Switch multiple guitar pedals and its order

    instantly • Display which pedals are selected and switched on • Wireless communication for a companion app
  2. I/O Boards • Mono audio I/O board • JST connectors

    • 6.35mm, 1/4’’ jack connectors
  3. 3D Printing • Helps keep your prototype tidy. • Help

    you visualise your project as a whole. • Helps fi nding issues and iterating on them. • You can use online services. • It has a learning curve.
  4. The Raspberry PI A Swiss Army knife • Small form

    factor computer • Con fi gurable I/O • Integrated wireless comms * • OS Customisation • Good Community support • Display capabilities • Open Source friendly • Wide range of variants
  5. Pi4J Project I/O Java Library for the Pi • Compatible

    with multiple boards • Very fl exible • Support for I²C, SPI, GPIO, UART, PWM • Kotlin DSL
  6. I²C Inter-Integrated Circuit • 2 wires required for multiple devices

    in the bus • 7 bit addressing, up to 128 target devices • Bidirectional communication • Multiple speed modes: 100kbps, 400kbps, 1Mbps, 1.7Mbps, 3.4Mbps, 5Mbps
  7. Example ON/OFF | AX3 | AX2 | AX1 | AX0

    | AY2 | AY1 | AY0 8Bit Word ON X2 -> Y4 1 0 0 1 0 1 0 0 ON 2 4
  8. override suspend fun sendData(commandValue: List<Byte>) { pi4j { i2c(1, ADG2188_DEVICE_ADDRESS)

    {
 id("ADG2188")
 linuxFsI2CProvider() }.use {
 commandValue.forEach {
 adg2188.write(it)
 }
 } }
 }
 

  9. private data class AddressByte(
 val enabled: Boolean,
 val x: Int,


    val y: Int ) {
 fun toByte(): Byte {
 val enableBit = if (enabled) 1 else 0
 return (((enableBit shl 7) or (x.xAddressOffset() shl 3)) or y).toByte()
 }
 
 private fun Int.xAddressOffset(): Int {
 return if (this < 6) return this else (this + 2)
 }
 }

  10. Current State Di ff State Δ New state Di ff

    State Δ List of I2c commands
  11. private fun getMatrix(pedals: List<Pedal>): Array<BooleanArray> { val enabledPedals = pedals.filter

    { it.enabled } val matrix = Array(8) { BooleanArray(8) } if (enabledPedals.isEmpty()) { matrix[0][0] = true return matrix } enabledPedals.forEachIndexed { index, pedal -> when (index) { 0 -> { matrix[0][pedal.ioChannel] = true // If only 1 pedal in the list, output index should be 0 (output) val outputIndex = enabledPedals.getOrNull(1)?.ioChannel ?: 0 matrix[pedal.ioChannel][outputIndex] = true } enabledPedals.lastIndex -> matrix[pedal.ioChannel][0] = true else -> matrix[pedal.ioChannel][enabledPedals[index + 1].ioChannel] = true } } return matrix }
  12. private fun getAddressByteList( oldMatrix: Array<BooleanArray>, newMatrix: Array<BooleanArray> ): List<AddressByte> {

    val addressesPairList = mutableListOf<AddressByte>() for (x in oldMatrix.indices) { for (y in oldMatrix[x].indices) { if (oldMatrix[x][y] != newMatrix[x][y]) { addressesPairList.add( AddressByte( enabled = newMatrix[x][y], x = x, y = y, ) ) } } } return addressesPairList.toList() }
  13. GPIO General Purpose Input/Output • Dedicated logic pins • Con

    fi gurable to read or write. • Ideal for simple applications. (Button read, LED switching, …)
  14. companion object { private const val DIGITAL_INPUT_PIN_SOUND_A: Int = 7

    // GPIO 4 private const val DIGITAL_INPUT_PIN_SOUND_B: Int = 29 // GPIO 5 } fun setButtonListener(onButtonPressed: (matrixButton: MatrixButton) -> Unit) { pi4j { matrix8Button(DIGITAL_INPUT_PIN_SOUND_A) { onButtonPressed(MatrixButton.SOUND_A) } matrix8Button(DIGITAL_INPUT_PIN_SOUND_B) { onButtonPressed(MatrixButton.SOUND_B) } } } private fun Context.matrix8Button( pin: Int onButtonPressed: () -> Unit, ) { digitalInput(pin) { pull(PullResistance.PULL_DOWN) debounce(1000L) piGpioProvider() }.onHigh { onButtonPressed() } }
  15. companion object { private const val DIGITAL_OUTPUT_PIN_SOUND_A: Int = 26

    // GPIO 6 private const val DIGITAL_OUTPUT_PIN_SOUND_B: Int = 31 // GPIO 7 } fun setLED(button: MatrixButton, on: Boolean) { pi4j { val pin = when (button) { MatrixButton.SOUND_A -> DIGITAL_OUTPUT_PIN_SOUND_A MatrixButton.SOUND_B -> DIGITAL_OUTPUT_PIN_SOUND_B } val digitalState = if(on) DigitalState.HIGH else DigitalState.LOW digitalOutput(pin) { initial(digitalState) shutdown(digitalState) piGpioProvider() } } }
  16. Architecture Presentation UI UseCases I²C Gateway Switcher State Matrix8ViewModel Matrix8Screen

    GetMatrix8UseCase
 SwitchPedalUseCase ADG2188 Driver GPIO Gateway LEDs, FootSwitch
  17. Companion app communication Options • TCP/IP based • Wi fi

    Direct • HTTP Server • Websockets Server • MQTT • Bluetooth • Bluetooth Classic • Bluetooth low energy
  18. BLE Java Library • Simple interface • Service and characteristic

    creation capabilities • Not maintained (last change 6 years ago) • Easy to modify to extend capabilities • No maven repository (Requires checkout and jar generation) https://github.com/tongo/ble-java V1
  19. BLE Kotlin Library • Simple interface • Kotlin DSL •

    Hosted in Maven Central • Linux Bluetooth Stack only (Multiplatform in Future) https://github.com/kikermo/ble-server BLE-SERVER V2
  20. bleServer { serverName = DEVICE_NAME bluezServerConnector() primaryService { uuid =

    UUID.fromString(SERVICE_UUID) name = SERVICE_NAME characteristic { uuid = UUID.fromString(CHARACTERISTIC_UUID_PRESETS) name = CHARACTERISTIC_NAME_PRESETS writeAccess = AccessType.Write { presetByteArray -> val presetIndex = presetByteArray.first().toInt() // 00-A, 01-B, 02-C, 03-D, XX-A presetStateFlow.value = initialPresets.getOrNull(presetIndex) ?: initialPresets.first() } readAccess = AccessType.Read notifyAccess = AccessType.Notify valueChangingAction { action -> CoroutineScope(Dispatchers.IO).launch { presetStateFlow.collectLatest { action(it.toCharacteristicValue()) } } } } } }
  21. characteristic { uuid = UUID.fromString(CHARACTERISTIC_UUID_PEDALS) name = CHARACTERISTIC_NAME_PEDALS writeAccess =

    AccessType.Write { pedalsByteArray -> println("Bytes ${pedalsByteArray.joinToString { it.toString() }}") presetStateFlow.value = presetStateFlow.value.copy(pedals = pedalsByteArray.toPedalList()) } readAccess = AccessType.Read notifyAccess = AccessType.Notify valueChangingAction { action -> CoroutineScope(Dispatchers.IO).launch { presetStateFlow.map { it.pedals }.collectLatest { action(getCharacteristicValue(it)) } } } }
  22. Architecture Presentation UI UseCases I²C Gateway BLE Service Switcher State

    Matrix8ViewModel Matrix8Screen GetMatrix8UseCase
 SwitchPedalUseCase Matrix8 State Characteristic ADG2188 Driver GPIO Gateway LEDs, FootSwitch
  23. Wi-fi Direct AKA Wi fi P2P • Useful for device

    setup • No need of network to operate • Android friendly • No direct way to setup via Kotlin 😢
  24. Http Server Ktor • Kotlin library • O ffi cially

    supported • Multiple engines to choose from: Jetty, Netty, Tomcat, CIO, ServletApplicationEngine • WebSocket capabilities
  25. fun Application.configureRouting() { routing { staticResources("static", "static") route("/matrix8") { get(“/pedals”)

    { call.respond(pedals) } post("/pedals") { try { val pedals = call.receive<List<Pedal>>() setPedals(pedals) call.respond(HttpStatusCode.NoContent) } catch (ex: IllegalStateException) { call.respond(HttpStatusCode.BadRequest) } catch (ex: JsonConvertException) { call.respond(HttpStatusCode.BadRequest) } } } } }
  26. Future of Matrix8 • KMP Companion app (desktop/mobile) • Edit

    mode • MIDI port • Use the audio input of the Pi (e.g. Guitar Tuner) • CI/CD with distributable updates