Initial Medtrum BLEComm

This commit is contained in:
jbr7rr 2023-02-19 20:09:52 +01:00
parent 37a6fa9220
commit b635ad26d8
14 changed files with 759 additions and 5 deletions

View file

@ -24,4 +24,4 @@ dependencies {
implementation project(':core:main')
implementation project(':core:ui')
implementation project(':core:utils')
}
}

View file

@ -218,4 +218,4 @@ class MedtrumPumpPlugin @Inject constructor(
private fun readTBR(): PumpSync.PumpState.TemporaryBasal? {
return pumpSync.expectedPumpState().temporaryBasal // TODO
}
}
}

View file

@ -0,0 +1,36 @@
package info.nightscout.pump.medtrum.comm
import kotlin.experimental.and
import info.nightscout.pump.medtrum.extension.toLong
class ManufacturerData(private val manufacturerDataBytes: ByteArray) {
private var deviceID: Long = 0
private var deviceType = 0
private var version = 0
init {
setData(manufacturerDataBytes)
}
fun setData(inputData: ByteArray) {
var index = 0
val deviceIDBytes: ByteArray = manufacturerDataBytes.copyOfRange(index, index + 4)
deviceID = deviceIDBytes.toLong()
index += 4
deviceType = (manufacturerDataBytes[index] and 0xff.toByte()).toInt()
index += 1
version = (manufacturerDataBytes[index] and 0xff.toByte()).toInt()
}
fun getDeviceID(): Long{
return deviceID
}
fun getDeviceType(): Int {
return deviceType
}
fun getVersion(): Int {
return version
}
}

View file

@ -0,0 +1,79 @@
package info.nightscout.pump.medtrum.comm
import info.nightscout.pump.medtrum.encryption.Crypt
class WriteCommandPackets(private val command: ByteArray) {
val crypt = Crypt()
private val packages = mutableListOf<ByteArray>()
private var index = 0
private var writeCommandIndex = 0
private var allPacketsConsumed = false
init {
setData(command)
}
fun setData(inputData: ByteArray) {
resetPackets()
// PackageIndex: 0 initially, if there are multiple packet, for the first packet it is set to 0 (not included in crc calc but sent in actual header)
var pkgIndex = 0
var header = byteArrayOf(
(inputData.size + 4).toByte(),
inputData[0],
writeCommandIndex.toByte(),
pkgIndex.toByte()
)
var tmp: ByteArray = header + inputData.copyOfRange(1, inputData.size)
var totalCommand: ByteArray = tmp + crypt.calcCrc8(tmp, tmp.size).toByte()
if ((totalCommand.size - header.size) <= 15) {
packages.add(totalCommand + 0.toByte())
} else {
pkgIndex = 1
var remainingCommand = totalCommand.copyOfRange(4, totalCommand.size)
while (remainingCommand.size > 15) {
header[3] = pkgIndex.toByte()
tmp = header + remainingCommand.copyOfRange(0, 15)
packages.add(tmp + crypt.calcCrc8(tmp, tmp.size).toByte())
remainingCommand = remainingCommand.copyOfRange(15, remainingCommand.size)
pkgIndex = (pkgIndex + 1) % 256
}
// Add last package
header[3] = pkgIndex.toByte()
tmp = header + remainingCommand
packages.add(tmp + crypt.calcCrc8(tmp, tmp.size).toByte())
}
writeCommandIndex = (writeCommandIndex % 255) + 1
}
fun allPacketsConsumed(): Boolean {
return allPacketsConsumed
}
fun getNextPacket(): ByteArray? {
var ret: ByteArray? = null
if (index < packages.size) {
ret = packages[index]
index++
}
if (index >= packages.size) {
allPacketsConsumed = true
}
return ret
}
private fun resetPackets() {
packages.clear()
index = 0
allPacketsConsumed = false
}
}

View file

@ -11,4 +11,4 @@ abstract class MedtrumPumpModule {
@ContributesAndroidInjector abstract fun contributesMedtrumPumpFragment(): MedtrumPumpFragment
}
}

View file

@ -0,0 +1,75 @@
package info.nightscout.pump.medtrum.encryption
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toLong
class Crypt {
val RIJNDEAL_S_BOX: IntArray = intArrayOf(99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22)
val RIJNDEAL_INVERSE_S_BOX: IntArray = intArrayOf(82, 9, 106, 213, 48, 54, 165, 56, 191, 64, 163, 158, 129, 243, 215, 251, 124, 227, 57, 130, 155, 47, 255, 135, 52, 142, 67, 68, 196, 222, 233, 203, 84, 123, 148, 50, 166, 194, 35, 61, 238, 76, 149, 11, 66, 250, 195, 78, 8, 46, 161, 102, 40, 217, 36, 178, 118, 91, 162, 73, 109, 139, 209, 37, 114, 248, 246, 100, 134, 104, 152, 22, 212, 164, 92, 204, 93, 101, 182, 146, 108, 112, 72, 80, 253, 237, 185, 218, 94, 21, 70, 87, 167, 141, 157, 132, 144, 216, 171, 0, 140, 188, 211, 10, 247, 228, 88, 5, 184, 179, 69, 6, 208, 44, 30, 143, 202, 63, 15, 2, 193, 175, 189, 3, 1, 19, 138, 107, 58, 145, 17, 65, 79, 103, 220, 234, 151, 242, 207, 206, 240, 180, 230, 115, 150, 172, 116, 34, 231, 173, 53, 133, 226, 249, 55, 232, 28, 117, 223, 110, 71, 241, 26, 113, 29, 41, 197, 137, 111, 183, 98, 14, 170, 24, 190, 27, 252, 86, 62, 75, 198, 210, 121, 32, 154, 219, 192, 254, 120, 205, 90, 244, 31, 221, 168, 51, 136, 7, 199, 49, 177, 18, 16, 89, 39, 128, 236, 95, 96, 81, 127, 169, 25, 181, 74, 13, 45, 229, 122, 159, 147, 201, 156, 239, 160, 224, 59, 77, 174, 42, 245, 176, 200, 235, 187, 60, 131, 83, 153, 97, 23, 43, 4, 126, 186, 119, 214, 38, 225, 105, 20, 99, 85, 33, 12, 125)
val CRC_8_TABLE: IntArray = intArrayOf(0, 155, 173, 54, 193, 90, 108, 247, 25, 130, 180, 47, 216, 67, 117, 238, 50, 169, 159, 4, 243, 104, 94, 197, 43, 176, 134, 29, 234, 113, 71, 220, 100, 255, 201, 82, 165, 62, 8, 147, 125, 230, 208, 75, 188, 39, 17, 138, 86, 205, 251, 96, 151, 12, 58, 161, 79, 212, 226, 121, 142, 21, 35, 184, 200, 83, 101, 254, 9, 146, 164, 63, 209, 74, 124, 231, 16, 139, 189, 38, 250, 97, 87, 204, 59, 160, 150, 13, 227, 120, 78, 213, 34, 185, 143, 20, 172, 55, 1, 154, 109, 246, 192, 91, 181, 46, 24, 131, 116, 239, 217, 66, 158, 5, 51, 168, 95, 196, 242, 105, 135, 28, 42, 177, 70, 221, 235, 112, 11, 144, 166, 61, 202, 81, 103, 252, 18, 137, 191, 36, 211, 72, 126, 229, 57, 162, 148, 15, 248, 99, 85, 206, 32, 187, 141, 22, 225, 122, 76, 215, 111, 244, 194, 89, 174, 53, 3, 152, 118, 237, 219, 64, 183, 44, 26, 129, 93, 198, 240, 107, 156, 7, 49, 170, 68, 223, 233, 114, 133, 30, 40, 179, 195, 88, 110, 245, 2, 153, 175, 52, 218, 65, 119, 236, 27, 128, 182, 45, 241, 106, 92, 199, 48, 171, 157, 6, 232, 115, 69, 222, 41, 178, 132, 31, 167, 60, 10, 145, 102, 253, 203, 80, 190, 37, 19, 136, 127, 228, 210, 73, 149, 14, 56, 163, 84, 207, 249, 98, 140, 23, 33, 186, 77, 214, 224, 123)
val MED_CIPHER: Long = 1344751489
fun keyGen(input: Long): Long {
val key = randomGen(randomGen(MED_CIPHER xor input))
return simpleCrypt(key)
}
fun randomGen(input: Long): Long {
val A = 16807
val Q = 127773
val R = 2836
val tmp1 = input / Q
var ret = (input - (tmp1 * Q)) * A - (tmp1 * R)
if (ret < 0) {
ret += 2147483647L
}
return ret
}
fun calcCrc8(value: ByteArray, size: Int): Int {
var crc8 = 0
for (i in 0 until size) {
crc8 = CRC_8_TABLE[(value[i].toInt() and 255) xor (crc8 and 255)].toInt() and 255
}
return crc8
}
private fun simpleCrypt(inputData: Long): Long {
var temp = inputData xor MED_CIPHER
for (i in 0 until 32) {
temp = changeByTable(rotatoLeft(temp, 32, 1), RIJNDEAL_S_BOX).toLong()
}
return temp
}
fun simpleDecrypt(inputData: Long): Long {
var temp = inputData
for (i in 0 until 32) {
temp = rotatoRight(changeByTable(temp, RIJNDEAL_INVERSE_S_BOX), 32, 1).toLong()
}
return temp xor MED_CIPHER
}
private fun changeByTable(inputData: Long, tableData: IntArray): Long {
val value = inputData.toByteArray(4)
val results = ByteArray(4)
for (i in value.indices) {
var byte = value[i].toInt()
if (byte < 0) {
byte += 256
}
results[i] = tableData[byte].toByte()
}
return results.toLong()
}
private fun rotatoLeft(x: Long, s: Int, n: Int): Long {
return (x shl n) or (x ushr (s - n))
}
private fun rotatoRight(x: Long, s: Int, n: Int): Int {
return (x ushr n or (x shl (s - n))).toInt()
}
}

View file

@ -2,4 +2,4 @@ package info.nightscout.pump.medtrum.events
import info.nightscout.rx.events.EventUpdateGui
class EventMedtrumPumpUpdateGui : EventUpdateGui()
class EventMedtrumPumpUpdateGui : EventUpdateGui()

View file

@ -0,0 +1,28 @@
package info.nightscout.pump.medtrum.extension
/** Extensions for different types of conversions needed when doing stuff with bytes */
fun ByteArray.toLong(): Long {
require(this.size <= 8) {
"Array size must be <= 8 for 'toLong' conversion operation"
}
var result = 0L
for (i in this.indices) {
val byte = this[i]
val shifted = (byte.toInt() and 0xFF).toLong() shl 8 * i
result = result or shifted
}
return result
}
fun ByteArray.toInt(): Int {
require(this.size <= 4) {
"Array size must be <= 4 for 'toInt' conversion operation"
}
var result = 0
for (i in this.indices) {
val byte = this[i]
val shifted = (byte.toInt() and 0xFF).toInt() shl 8 * i
result = result or shifted
}
return result
}

View file

@ -0,0 +1,10 @@
package info.nightscout.pump.medtrum.extension
/** Extensions for different types of conversions needed when doing stuff with bytes */
fun Int.toByteArray(byteLength: Int): ByteArray {
val bytes = ByteArray(byteLength)
for (i in 0 until byteLength) {
bytes[i] = (this shr (i * 8) and 0xFF).toByte()
}
return bytes
}

View file

@ -0,0 +1,10 @@
package info.nightscout.pump.medtrum.extension
/** Extensions for different types of conversions needed when doing stuff with bytes */
fun Long.toByteArray(byteLength: Int): ByteArray {
val bytes = ByteArray(byteLength)
for (i in 0 until byteLength) {
bytes[i] = (this shr (i * 8) and 0xFF).toByte()
}
return bytes
}

View file

@ -0,0 +1,399 @@
package info.nightscout.pump.medtrum.services
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.bluetooth.le.ScanFilter
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.SystemClock
import androidx.core.app.ActivityCompat
import dagger.android.HasAndroidInjector
import info.nightscout.core.ui.toast.ToastUtils
import info.nightscout.core.utils.notify
import info.nightscout.core.utils.waitMillis
import info.nightscout.interfaces.notifications.Notification
import info.nightscout.interfaces.pump.PumpSync
import info.nightscout.interfaces.ui.UiInteraction
import info.nightscout.pump.medtrum.extension.toByteArray
import info.nightscout.pump.medtrum.extension.toInt
import info.nightscout.pump.medtrum.encryption.Crypt
import info.nightscout.pump.medtrum.comm.WriteCommandPackets
import info.nightscout.pump.medtrum.comm.ManufacturerData
import info.nightscout.rx.bus.RxBus
import info.nightscout.rx.events.EventDismissNotification
import info.nightscout.rx.events.EventPumpStatusChanged
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
import info.nightscout.shared.utils.DateUtil
import java.util.UUID
import java.util.Arrays
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BLEComm @Inject internal constructor(
private val injector: HasAndroidInjector,
private val aapsLogger: AAPSLogger,
private val rh: ResourceHelper,
private val context: Context,
private val rxBus: RxBus,
private val sp: SP,
private val pumpSync: PumpSync,
private val dateUtil: DateUtil,
private val uiInteraction: UiInteraction
) {
companion object {
private const val WRITE_DELAY_MILLIS: Long = 50
private const val SERVICE_UUID = "669A9001-0008-968F-E311-6050405558B3"
private const val READ_UUID = "669a9120-0008-968f-e311-6050405558b3"
private const val WRITE_UUID = "669a9101-0008-968f-e311-6050405558b"
private const val CHARACTERISTIC_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb"
private const val MANUFACTURER_ID = 18305
private const val COMMAND_AUTH_REQ: Byte = 5
}
private val mBluetoothAdapter: BluetoothAdapter? get() = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
private var mBluetoothGatt: BluetoothGatt? = null
private val mCrypt = Crypt()
var isConnected = false
var isConnecting = false
private var uartWrite: BluetoothGattCharacteristic? = null
private var deviceID: Long = 0
/** Connect flow: 1. Start scanning for our device (SN entered in settings) */
@SuppressLint("MissingPermission")
@Synchronized
fun startScan(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED
) {
ToastUtils.errorToast(context, context.getString(info.nightscout.core.ui.R.string.need_connect_permission))
aapsLogger.error(LTag.PUMPBTCOMM, "missing permissions")
return false
}
aapsLogger.debug(LTag.PUMPBTCOMM, "Start scan!!")
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val filters = mutableListOf<ScanFilter>()
if (deviceID == 0.toLong()) deviceID = rh.gs(info.nightscout.pump.medtrum.R.string.key_snInput).toLong(radix = 16)
isConnected = false
// TODO: Maybe replace this by (or add) a isScanning parameter?
isConnecting = true
// Find our Medtrum Device!
filters.add(
ScanFilter.Builder().setDeviceName("MT").build()
)
// TODO Check if we need to add MAC for reconnects? Not sure if otherwise we can find the device
mBluetoothAdapter?.bluetoothLeScanner?.startScan(filters, settings, mScanCallback)
return true
}
@SuppressLint("MissingPermission")
@Synchronized
fun stopScan() {
mBluetoothAdapter?.bluetoothLeScanner?.stopScan(mScanCallback)
}
/** Connect flow: 2. When device is found this is called by onScanResult() */
@SuppressLint("MissingPermission")
@Synchronized
fun connect(device: BluetoothDevice) {
mBluetoothGatt =
device.connectGatt(context, false, mGattCallback, BluetoothDevice.TRANSPORT_LE)
}
@SuppressLint("MissingPermission")
@Synchronized
fun disconnect() {
mBluetoothGatt?.disconnect()
mBluetoothGatt = null
}
@Synchronized
fun stopConnecting() {
isConnecting = false
}
@SuppressLint("MissingPermission")
@Synchronized fun close() {
aapsLogger.debug(LTag.PUMPBTCOMM, "BluetoothAdapter close")
mBluetoothGatt?.close()
mBluetoothGatt = null
}
/** Scan callback */
private val mScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
aapsLogger.debug(LTag.PUMPBTCOMM, "OnScanResult!" + result)
super.onScanResult(callbackType, result)
val manufacturerData =
result.scanRecord?.getManufacturerSpecificData(MANUFACTURER_ID)
?.let { ManufacturerData(it) }
aapsLogger.debug(LTag.PUMPBTCOMM, "Found DeviceID: " + manufacturerData?.getDeviceID())
if (manufacturerData?.getDeviceID() == deviceID) {
stopScan()
connect(result.device)
}
}
override fun onScanFailed(errorCode: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Scan FAILED!")
}
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
private val mGattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
onConnectionStateChangeSynchronized(gatt, status, newState) // call it synchronized
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onServicesDiscovered")
if (status == BluetoothGatt.GATT_SUCCESS) {
findCharacteristic()
}
}
override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicRead status = " + status)
if (status == BluetoothGatt.GATT_SUCCESS) {
readDataParsing(characteristic.value)
}
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicChanged")
readDataParsing(characteristic.value)
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicWrite status = " + status)
// TODO (note also queue, note that in danars there is no response, so check where to handle multiple packets)
// aapsLogger.debug(LTag.PUMPBTCOMM, "onCharacteristicWrite: " + DanaRS_Packet.toHexString(characteristic.value))
// Thread {
// synchronized(mSendQueue) {
// // after message sent, check if there is the rest of the message waiting and send it
// if (mSendQueue.size > 0) {
// val bytes = mSendQueue[0]
// mSendQueue.removeAt(0)
// writeCharacteristicNoResponse(uartWriteBTGattChar, bytes)
// }
// }
// }.start()
}
override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
super.onDescriptorWrite(gatt, descriptor, status)
aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorWrite " + status)
if (status == BluetoothGatt.GATT_SUCCESS) {
readDescriptor(descriptor)
}
}
/** Connect flow: 5. Notifications enabled read descriptor to verify and start auth process*/
override fun onDescriptorRead(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorRead(gatt, descriptor, status)
aapsLogger.debug(LTag.PUMPBTCOMM, "onDescriptorRead status: " + status)
if (status == BluetoothGatt.GATT_SUCCESS) {
checkDescriptor(descriptor)
}
}
@SuppressLint("MissingPermission")
@Synchronized
private fun readDescriptor(descriptor: BluetoothGattDescriptor?) {
aapsLogger.debug(LTag.PUMPBTCOMM, "readDescriptor")
if (mBluetoothAdapter == null || mBluetoothGatt == null || descriptor == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return
}
mBluetoothGatt?.readDescriptor(descriptor)
}
private fun checkDescriptor(descriptor: BluetoothGattDescriptor) {
aapsLogger.debug(LTag.PUMPBTCOMM, "checkDescriptor")
val service = getGattService()
if (mBluetoothAdapter == null || mBluetoothGatt == null || service == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return
}
if (descriptor.value.toInt() > 0) {
var notificationEnabled = true
val characteristics = service.characteristics
for (j in 0 until characteristics.size) {
val configDescriptor =
characteristics[j].getDescriptor(UUID.fromString(CHARACTERISTIC_CONFIG_UUID))
if (configDescriptor.value == null || configDescriptor.value.toInt() <= 0) {
notificationEnabled = false
}
}
if (notificationEnabled) {
aapsLogger.debug(LTag.PUMPBTCOMM, "Notifications enabled!")
authorize()
}
}
}
@Suppress("DEPRECATION")
@SuppressLint("MissingPermission")
@Synchronized
private fun setCharacteristicNotification(characteristic: BluetoothGattCharacteristic?, enabled: Boolean) {
aapsLogger.debug(LTag.PUMPBTCOMM, "setCharacteristicNotification")
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return
}
mBluetoothGatt?.setCharacteristicNotification(characteristic, enabled)
characteristic?.getDescriptor(UUID.fromString(CHARACTERISTIC_CONFIG_UUID))?.let {
if (characteristic.properties and 0x10 > 0) {
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
mBluetoothGatt?.writeDescriptor(it)
} else if (characteristic.properties and 0x20 > 0) {
it.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
mBluetoothGatt?.writeDescriptor(it)
} else {
}
}
}
/** Connect flow: 3. When we are connected discover services*/
@SuppressLint("MissingPermission")
@Synchronized
private fun onConnectionStateChangeSynchronized(gatt: BluetoothGatt, status: Int, newState: Int) {
aapsLogger.debug(LTag.PUMPBTCOMM, "onConnectionStateChange newState: " + newState + " status: " + status)
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
close()
isConnected = false
isConnecting = false
rxBus.send(EventPumpStatusChanged(EventPumpStatusChanged.Status.DISCONNECTED))
aapsLogger.debug(LTag.PUMPBTCOMM, "Device was disconnected " + gatt.device.name) //Device was disconnected
disconnect()
startScan()
}
}
private fun readDataParsing(receivedData: ByteArray) {
aapsLogger.debug(LTag.PUMPBTCOMM, "<<<readDataParsing>>> " + Arrays.toString(receivedData))
// TODO
/** Connect flow: 6. Authorized */ // TODO place this at the correct place
}
private fun authorize() {
aapsLogger.debug(LTag.PUMPBTCOMM, "Start auth!")
val role = 2 // Fixed to 2 for pump
val key = mCrypt.keyGen(deviceID)
val commandData = byteArrayOf(COMMAND_AUTH_REQ) + byteArrayOf(role.toByte()) + 0.toByteArray(4) + key.toByteArray(4)
sendMessage(commandData)
}
@Suppress("DEPRECATION")
@SuppressLint("MissingPermission")
@Synchronized
private fun sendMessage(message: ByteArray) {
// TODO: Handle packages which consist of multiple, Create a queue of packages
aapsLogger.debug(LTag.PUMPBTCOMM, "sendMessage message = " + Arrays.toString(message))
val writePacket = WriteCommandPackets(message)
val value: ByteArray? = writePacket.getNextPacket()
// TODO: queue
writeCharacteristic(uartWriteBTGattChar, value)
}
private fun getGattService(): BluetoothGattService? {
aapsLogger.debug(LTag.PUMPBTCOMM, "getGattService")
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return null
}
return mBluetoothGatt?.getService(UUID.fromString(SERVICE_UUID))
}
private fun getGattCharacteristic(uuid: UUID): BluetoothGattCharacteristic? {
aapsLogger.debug(LTag.PUMPBTCOMM, "getGattCharacteristic $uuid")
val service = getGattService()
if (mBluetoothAdapter == null || mBluetoothGatt == null || service == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return null
}
return service.getCharacteristic(uuid)
}
@Suppress("DEPRECATION")
@SuppressLint("MissingPermission")
@Synchronized
private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, data: ByteArray?) {
Thread(Runnable {
SystemClock.sleep(WRITE_DELAY_MILLIS)
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
aapsLogger.error("BluetoothAdapter not initialized_ERROR")
isConnecting = false
isConnected = false
return@Runnable
}
characteristic.value = data
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
aapsLogger.debug("writeCharacteristic:" + Arrays.toString(data))
mBluetoothGatt?.writeCharacteristic(characteristic)
}).start()
SystemClock.sleep(WRITE_DELAY_MILLIS)
}
private val uartWriteBTGattChar: BluetoothGattCharacteristic
get() = uartWrite
?: BluetoothGattCharacteristic(UUID.fromString(WRITE_UUID), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT, 0).also { uartWrite = it }
/** Connect flow: 4. When services are discovered find characteristics and set notifications*/
private fun findCharacteristic() {
val gattService = getGattService() ?: return
val gattCharacteristics = gattService.characteristics
for (gattCharacteristic in gattCharacteristics) {
setCharacteristicNotification(gattCharacteristic, true)
}
}
}
}

View file

@ -34,4 +34,4 @@ open class TestBase {
@Suppress("Unchecked_Cast")
fun <T> uninitialized(): T = null as T
}
}

View file

@ -0,0 +1,90 @@
package info.nightscout.pump.medtrum.comm
import org.junit.jupiter.api.Test
import org.junit.Assert.*
class WriteCommandPacketsTest {
@Test
fun Given14LongCommandExpectOnePacket() {
val input = byteArrayOf(5, 2, 0, 0, 0, 0, -21, 57, -122, -56)
val expect = byteArrayOf(14, 5, 0, 0, 2, 0, 0, 0, 0, -21, 57, -122, -56, -93, 0)
val cmdPackets = WriteCommandPackets(input)
val output = cmdPackets.getNextPacket()
assertEquals(expect.contentToString(), output.contentToString())
}
@Test
fun Given41LongCommandExpectThreePackets() {
val input = byteArrayOf(18, 0, 12, 0, 3, 0, 1, 30, 32, 3, 16, 14, 0, 0, 1, 7, 0, -96, 2, -16, 96, 2, 104, 33, 2, -32, -31, 1, -64, 3, 2, -20, 36, 2, 100, -123, 2)
val expect1 = byteArrayOf(41, 18, 0, 1, 0, 12, 0, 3, 0, 1, 30, 32, 3, 16, 14, 0, 0, 1, 7, -121)
val expect2 = byteArrayOf(41, 18, 0, 2, 0, -96, 2, -16, 96, 2, 104, 33, 2, -32, -31, 1, -64, 3, 2, -3)
val expect3 = byteArrayOf(41, 18, 0, 3, -20, 36, 2, 100, -123, 2, -125, -89)
val cmdPackets = WriteCommandPackets(input)
val output1 = cmdPackets.getNextPacket()
val output2 = cmdPackets.getNextPacket()
val output3 = cmdPackets.getNextPacket()
val output4 = cmdPackets.getNextPacket()
assertEquals(expect1.contentToString(), output1.contentToString())
assertEquals(expect2.contentToString(), output2.contentToString())
assertEquals(expect3.contentToString(), output3.contentToString())
assertNull(output4)
assertEquals(true, cmdPackets.allPacketsConsumed())
}
@Test
fun Given2CommandsExpectWriteIndexInHeaderIncrease() {
val input1 = byteArrayOf(66)
val input2 = byteArrayOf(99)
val expect1 = byteArrayOf(5, 66, 0, 0, -25, 0)
val expect2 = byteArrayOf(5, 99, 1, 0, 64, 0)
val cmdPackets = WriteCommandPackets(input1)
val output1 = cmdPackets.getNextPacket()
cmdPackets.setData(input2)
val output2 = cmdPackets.getNextPacket()
assertEquals(expect1.contentToString(), output1.contentToString())
assertEquals(expect2.contentToString(), output2.contentToString())
}
@Test
fun GivenWriteIndexOverflowExpectWriteIndex1() {
val input1 = byteArrayOf(55)
val input2 = byteArrayOf(66)
val input3 = byteArrayOf(99)
val expect1 = byteArrayOf(5, 55, -2, 0, -19, 0)
val expect2 = byteArrayOf(5, 66, -1, 0, 86, 0)
val expect3 = byteArrayOf(5, 99, 1, 0, 64, 0)
val cmdPackets = WriteCommandPackets(byteArrayOf(0.toByte()))
// All this stuff to set the private field ^^
val writeCommandIndex = WriteCommandPackets::class.java.getDeclaredField("writeCommandIndex")
writeCommandIndex.isAccessible = true
writeCommandIndex.setInt(cmdPackets, 254)
cmdPackets.setData(input1)
val output1 = cmdPackets.getNextPacket()
cmdPackets.setData(input2)
val output2 = cmdPackets.getNextPacket()
cmdPackets.setData(input3)
val output3 = cmdPackets.getNextPacket()
assertEquals(expect1.contentToString(), output1.contentToString())
assertEquals(expect2.contentToString(), output2.contentToString())
assertEquals(expect3.contentToString(), output3.contentToString())
}
}

View file

@ -0,0 +1,27 @@
package info.nightscout.pump.medtrum.encryption
import org.junit.jupiter.api.Test
import org.junit.Assert.*
class CryptTest {
@Test
fun GivenSNExpectKey() {
val crypt = Crypt()
val input: Long = 2859923929
val expect: Long = 3364239851
val output: Long = crypt.keyGen(input)
assertEquals(expect, output)
}
@Test
fun GivenSNExpectReal() {
val crypt = Crypt()
val input: Long = 2859923929
val expect: Long = 126009121
val output: Long = crypt.simpleDecrypt(input)
assertEquals(expect, output)
}
}