-
-
Save glsubri/ac280b594d0c68b3dc1b3bcfdc7b7b7b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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