Skip to content

Instantly share code, notes, and snippets.

@glsubri
Created July 19, 2021 13:57
Show Gist options
  • Save glsubri/ac280b594d0c68b3dc1b3bcfdc7b7b7b to your computer and use it in GitHub Desktop.
Save glsubri/ac280b594d0c68b3dc1b3bcfdc7b7b7b to your computer and use it in GitHub Desktop.
package ch.subri.morninglight.data.api.ble
import android.bluetooth.BluetoothAdapter
import android.util.Log
import ch.subri.morninglight.data.CurrentLightServiceUUID
import ch.subri.morninglight.data.CurrentTimeServiceUUID
import ch.subri.morninglight.data.TAG
import com.juul.kable.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.*
class BLEApiImpl(mcuAddress: MCUAddress) : BLEApi {
private val ioScope = CoroutineScope(Dispatchers.IO)
private val peripheral: Peripheral = ioScope.peripheral(bluetoothDeviceFrom(mcuAddress)) {
onServicesDiscovered {
_state.value = DeviceState.Ready
// Update MCU's time at connection
updateTime()
}
}
private val _state: MutableStateFlow<DeviceState> = MutableStateFlow(DeviceState.Disconnected)
@OptIn(FlowPreview::class)
override val state: StateFlow<DeviceState> = _state.asStateFlow()
init {
ioScope.launch {
peripheral.state
.onEach { _state.value = it.toDeviceState() }
.collect()
}
}
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
override val intensity: Flow<Int> = peripheral
.observe(CurrentLightServiceUUID.intensity)
.map { it[0].toInt() }
.flowOn(Dispatchers.IO)
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
override val activity: Flow<Boolean> = peripheral
.observe(CurrentLightServiceUUID.active)
.map { it.isTurnedOn() }
.flowOn(Dispatchers.IO)
override fun connect() {
ioScope.launch {
try {
peripheral.connect()
} catch (e: ConnectionLostException) {
Log.w(TAG, "e: ${e.message}")
}
}
}
override fun disconnect() {
ioScope.launch { peripheral.disconnect() }
}
override fun reconnect() {
disconnect()
connect()
}
override fun updateTime() {
if (state.value != DeviceState.Ready) {
throw error("Device is not in ready state")
}
val calendar = Calendar.getInstance()
val bytes = timeToBytes(calendar)
ioScope.launch {
peripheral.write(
characteristic = CurrentTimeServiceUUID.time,
data = bytes,
writeType = WriteType.WithResponse,
)
}
}
override fun setIntensity(intensity: Int) {
if (state.value != DeviceState.Ready) {
throw error("Device is not in ready state")
}
val data = byteArrayOf(intensity.toByte())
ioScope.launch {
peripheral.write(
characteristic = CurrentLightServiceUUID.intensity,
data = data,
writeType = WriteType.WithResponse,
)
}
}
override fun setActivity(on: Boolean) {
if (state.value != DeviceState.Ready) {
throw error("Device is not in ready state")
}
val data = byteArrayOf(
if (on) 1.toByte() else 0.toByte()
)
ioScope.launch {
peripheral.write(
characteristic = CurrentLightServiceUUID.active,
data = data,
writeType = WriteType.WithResponse,
)
}
}
override suspend fun getIntensity(): Int {
if (state.value != DeviceState.Ready) {
throw error("Device is not in ready state")
}
val value: ByteArray = peripheral.read(
characteristic = CurrentLightServiceUUID.intensity,
)
return value[0].toInt()
}
override suspend fun getActivity(): Boolean {
if (state.value != DeviceState.Ready) {
throw error("Device is not in ready state")
}
val value: ByteArray = peripheral.read(
characteristic = CurrentLightServiceUUID.active,
)
return value.isTurnedOn()
}
companion object {
/**
* Helper function that returns a given time in the correct BLE exchange format
*
* @param calendar The calendar to translate to byte array
* @return The given time in the correct BLE exchange format
*/
fun timeToBytes(calendar: Calendar): ByteArray {
val year = calendar.get(Calendar.YEAR)
// Calendar.MONTH -> [0-11]
val month = calendar.get(Calendar.MONTH) + 1
val mday = calendar.get(Calendar.DAY_OF_MONTH)
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
// Sunday = 1, Monday = 2, Tuesday = 3
// Wednesday = 4, Thursday = 5, Friday = 6, Saturday = 7
var wday = calendar.get(Calendar.DAY_OF_WEEK) - 1
if (wday == 0) wday = 7 // in BLE 0 means "unknown" and 7 means Sunday
val fraction = 0
val adjustReason = 0
val yearLE0 = year
val yearLE1 = (year shr 8)
val arr = arrayOf(
yearLE0,
yearLE1,
month,
mday,
hour,
minute,
second,
wday,
fraction,
adjustReason,
)
return arr.map { it.toByte() }.toByteArray()
}
}
}
private fun State.toDeviceState(): DeviceState {
return when (this) {
State.Connecting -> DeviceState.Connecting
State.Connected -> DeviceState.Connected
State.Disconnecting -> DeviceState.Disconnected
is State.Disconnected -> DeviceState.Disconnected
}
}
private fun ByteArray.isTurnedOn(): Boolean {
return !this.all { it == 0.toByte() }
}
/**
* Returns a bluetooth device from a mac address
*
* @param macAddress The mac address of the device
*/
private fun bluetoothDeviceFrom(macAddress: String) =
BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress)
package ch.subri.morninglight.data.api.ble
sealed class DeviceState {
object Disconnected : DeviceState()
object Connecting : DeviceState()
object Connected : DeviceState()
object Ready : DeviceState()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment