ble: start implementing message splitting and sending

This commit is contained in:
Andrei Vereha 2021-02-25 19:03:38 +01:00
parent 5b128e6def
commit 08ff02dd4f
21 changed files with 316 additions and 29 deletions

View file

@ -12,6 +12,9 @@ import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callback
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandHello
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.*
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk.LTKExchanger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan.PodScanner
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.status.ConnectionStatus
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.pod.command.base.Command
@ -34,16 +37,21 @@ class OmnipodDashBleManagerImpl @Inject constructor(private val context: Context
val podScanner = PodScanner(aapsLogger, bluetoothAdapter)
val podAddress = podScanner.scanForPod(PodScanner.SCAN_FOR_SERVICE_UUID, PodScanner.POD_ID_NOT_ACTIVATED).scanResult.device.address
// For tests: this.podAddress = "B8:27:EB:1D:7E:BB";
connect(podAddress)
val bleIO = connect(podAddress)
val msgIO = MessageIO(aapsLogger, bleIO)
val ltkExchanger = LTKExchanger(aapsLogger, msgIO)
val ltk = ltkExchanger.negociateLTKAndNonce()
aapsLogger.info(LTag.PUMPCOMM, "Got LTK and Nonce Prefix: ${ltk}")
}
@Throws(FailedToConnectException::class, CouldNotSendBleException::class, InterruptedException::class, BleIOBusyException::class, TimeoutException::class, CouldNotConfirmWriteException::class, CouldNotEnableNotifications::class, DescriptorNotFoundException::class, CouldNotConfirmDescriptorWriteException::class)
private fun connect(podAddress: String) {
private fun connect(podAddress: String): BleIO {
// TODO: locking?
val podDevice = bluetoothAdapter.getRemoteDevice(podAddress)
val incomingPackets: Map<CharacteristicType, BlockingQueue<ByteArray>> =
mapOf(CharacteristicType.CMD to LinkedBlockingDeque(),
CharacteristicType.DATA to LinkedBlockingDeque());
CharacteristicType.DATA to LinkedBlockingDeque())
val bleCommCallbacks = BleCommCallbacks(aapsLogger, incomingPackets)
aapsLogger.debug(LTag.PUMPBTCOMM, "Connecting to $podAddress")
var autoConnect = true
@ -65,6 +73,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(private val context: Context
aapsLogger.debug(LTag.PUMPBTCOMM, "Saying hello to the pod")
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandHello(CONTROLLER_ID).data)
bleIO.readyToRead()
return bleIO
}
override fun sendCommand(cmd: Command): Response {
@ -84,6 +93,7 @@ class OmnipodDashBleManagerImpl @Inject constructor(private val context: Context
}
companion object {
private const val CONNECT_TIMEOUT_MS = 5000
private const val CONTROLLER_ID = 4242 // TODO read from preferences or somewhere else.
}

View file

@ -4,9 +4,11 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CharacteristicNotFoundException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.ServiceNotFoundException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import java.math.BigInteger
import java.util.*

View file

@ -7,8 +7,8 @@ import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothProfile
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.CharacteristicType.Companion.byValue
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType.Companion.byValue
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotConfirmDescriptorWriteException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.CouldNotConfirmWriteException
import java.util.concurrent.BlockingQueue

View file

@ -1,17 +1,33 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
abstract class BleCommand {
open class BleCommand(val data: ByteArray) {
val data: ByteArray
constructor(type: BleCommandType) : this(byteArrayOf(type.value)) {}
constructor(type: BleCommandType) {
data = byteArrayOf(type.value)
constructor(type: BleCommandType, payload: ByteArray): this(
byteArrayOf(type.value) + payload
) {}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BleCommand) return false
if (!data.contentEquals(other.data)) return false
return true
}
constructor(type: BleCommandType, payload: ByteArray) {
val n = payload.size + 1
data = ByteArray(n)
data[0] = type.value
System.arraycopy(payload, 0, data, 1, payload.size)
override fun hashCode(): Int {
return data.contentHashCode()
}
}
}
class BleCommandRTS(): BleCommand(BleCommandType.RTS) {}
class BleCommandCTS(): BleCommand(BleCommandType.CTS) {}
class BleCommandAbort(): BleCommand(BleCommandType.ABORT) {}
class BleCommandSuccess(): BleCommand(BleCommandType.SUCCESS) {}
class BleCommandFail(): BleCommand(BleCommandType.FAIL) {}

View file

@ -0,0 +1,4 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command
class BleCommandNack {
}

View file

@ -1,5 +1,5 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
class CouldNotEnableNotifications(cmd: CharacteristicType) : Exception(cmd.value)

View file

@ -0,0 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommand
import java.lang.Exception
class UnexpectedCommandException(val cmd: BleCommand): Exception("Unexpected command: ${cmd}") {
}

View file

@ -5,7 +5,6 @@ import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.callbacks.BleCommCallbacks
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.*
import java.util.concurrent.BlockingQueue

View file

@ -1,4 +1,4 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
import java.math.BigInteger
import java.util.*

View file

@ -0,0 +1,16 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.BlePacket
import java.io.ByteArrayOutputStream
class PayloadJoiner() {
private val payload = ByteArrayOutputStream()
fun accumulate(packet: BlePacket) {
}
fun bytes(): ByteArray {
return ByteArray(0);
}
}

View file

@ -0,0 +1,69 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.BlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.FirstBlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.LastBlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.LastOptionalPlusOneBlePacket
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet.MiddleBlePacket
import java.lang.Integer.min
import java.util.zip.CRC32
internal class PayloadSplitter(private val payload: ByteArray) {
fun splitInPackets(): List<BlePacket> {
val ret = ArrayList<BlePacket>()
val crc32 = payload.crc32();
if (payload.size <= 18) {
val end = min(14, payload.size)
ret.add(FirstBlePacket(
totalFragments = 0,
payload = payload.copyOfRange(0, end),
size = payload.size.toByte(),
crc32 = crc32,
))
if (payload.size > 14) {
ret.add(LastOptionalPlusOneBlePacket(
index = 1,
payload = payload.copyOfRange(end, payload.size),
))
}
return ret;
}
val middleFragments = (payload.size-18)/19
val rest = ((payload.size - middleFragments.toInt() * 19) - 18).toByte()
ret.add(FirstBlePacket(
totalFragments = (middleFragments + 1).toByte(),
payload = payload.copyOfRange(0, 18),
))
for( i in 1..middleFragments ) {
val p = if (i ==1 ) {
payload.copyOfRange(18,37)
}else {
payload.copyOfRange((i-1)*19+18, (i-1)*19+18+19)
}
ret.add(MiddleBlePacket(
index = i.toByte(),
payload = p,
))
}
val end = min(14, rest.toInt())
ret.add(LastBlePacket(
index = (middleFragments+1).toByte(),
size = rest,
payload = payload.copyOfRange(middleFragments*19+18,middleFragments*19+18+end),
crc32 = crc32,
))
if (rest > 14) {
ret.add(LastOptionalPlusOneBlePacket(
index = (middleFragments+2).toByte(),
payload = payload.copyOfRange(middleFragments*19+18+14, payload.size),
))
}
return ret;
}
}
internal fun ByteArray.crc32(): Long {
val crc = CRC32()
crc.update(this)
return crc.value
}

View file

@ -0,0 +1,12 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleIO
data class LTK(val ltk: ByteArray, val noncePrefix: ByteArray) {
init{
require(ltk.size == 16)
require(noncePrefix.size == 16)
}
}

View file

@ -0,0 +1,23 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.ltk
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.Address
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.MessageIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message.PairMessage
import info.nightscout.androidaps.utils.extensions.hexStringToByteArray
internal class LTKExchanger(private val aapsLogger: AAPSLogger,private val msgIO: MessageIO) {
fun negociateLTKAndNonce(): LTK? {
val msg = PairMessage(
destination = Address(byteArrayOf(1,2,3,4)),
source = Address(byteArrayOf(5,6,7,8)),
payload = "5350313d0004024200032c5350323d000bffc32dbd20030e01000016".hexStringToByteArray(),
sequenceNumber = 1,
)
msgIO.sendMesssage(msg)
return null
}
}

View file

@ -0,0 +1,7 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
data class Address(val address: ByteArray) {
init {
require(address.size == 4)
}
}

View file

@ -0,0 +1,23 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
abstract class Message(
val type: MessageType,
val source: Address,
val destination: Address,
val payload: ByteArray,
val sequenceNumber: Byte,
val ack: Boolean = false,
val ackNumber: Byte = 0.toByte(),
val eqos: Short = 0.toShort(), // TODO: understand
val priority: Boolean = false,
val lastMessage: Boolean= false,
val gateway: Boolean = false,
val sas: Boolean = false, // TODO: understand
val tfs: Boolean = false, // TODO: understand
val version: Short = 0.toShort(),
) {
fun asByteArray(): ByteArray {
return payload; // TODO implement
}
}

View file

@ -0,0 +1,43 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.BleManager
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommand
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandCTS
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandHello
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.command.BleCommandRTS
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.UnexpectedCommandException
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.BleIO
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.CharacteristicType
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.io.PayloadSplitter
class MessageIO(private val aapsLogger: AAPSLogger, private val bleIO: BleIO) {
fun sendMesssage(msg: Message) {
bleIO.flushIncomingQueues();
bleIO.sendAndConfirmPacket(CharacteristicType.CMD, BleCommandRTS().data)
val expectCTS = bleIO.receivePacket(CharacteristicType.CMD)
if (BleCommand(expectCTS) != BleCommandCTS()) {
throw UnexpectedCommandException(BleCommand(expectCTS))
}
val payload = msg.asByteArray()
val splitter = PayloadSplitter(payload)
val packets = splitter.splitInPackets()
for (packet in packets) {
bleIO.sendAndConfirmPacket(CharacteristicType.DATA, packet.asByteArray())
}
// TODO: peek for NACKs
val expectSuccess = bleIO.receivePacket(CharacteristicType.CMD)
if (BleCommand(expectSuccess) != BleCommandCTS()) {
throw UnexpectedCommandException(BleCommand(expectSuccess))
}
// TODO: handle NACKS/FAILS/etc
bleIO.flushIncomingQueues();
}
fun receiveMessage(): Message? {
// do the RTS/CTS/data/success dance
return null
}
}

View file

@ -0,0 +1,14 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
enum class MessageType(val value: Byte) {
CLEAR(0),
ENCRYPTED(1),
SESSION_ESTABLISHMENT(2),
PAIRING(3);
companion object {
fun byValue(value: Byte): MessageType =
MessageType.values().firstOrNull() {it.value == value}
?: throw IllegalArgumentException("Unknown MessageType: $value")
}
}

View file

@ -0,0 +1,8 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.message
class PairMessage(source: Address, destination: Address, payload: ByteArray, sequenceNumber: Byte
) : Message(
type=MessageType.PAIRING, source, destination, payload, sequenceNumber,
) {
}

View file

@ -1,3 +1,34 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.packet
class BlePacket
sealed class BlePacket {
abstract fun asByteArray(): ByteArray
}
data class FirstBlePacket(val totalFragments: Byte, val payload: ByteArray, val size: Byte = 0, val crc32: Long? = null) : BlePacket() {
override fun asByteArray(): ByteArray {
TODO("Not yet implemented")
}
}
data class MiddleBlePacket(val index: Byte, val payload: ByteArray) : BlePacket() {
override fun asByteArray(): ByteArray {
TODO("Not yet implemented")
}
}
data class LastBlePacket(val index: Byte, val size: Byte, val payload: ByteArray, val crc32: Long) : BlePacket() {
override fun asByteArray(): ByteArray {
TODO("Not yet implemented")
}
}
data class LastOptionalPlusOneBlePacket(val index: Byte, val payload: ByteArray) : BlePacket() {
override fun asByteArray(): ByteArray {
TODO("Not yet implemented")
}
}

View file

@ -1,33 +1,34 @@
package info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.scan
import android.bluetooth.le.ScanRecord
import android.bluetooth.le.ScanResult
import android.os.ParcelUuid
import info.nightscout.androidaps.plugins.pump.omnipod.dash.driver.comm.exceptions.DiscoveredInvalidPodException
class BleDiscoveredDevice(val scanResult: ScanResult, private val podId: Long) {
class BleDiscoveredDevice(val scanResult: ScanResult, val scanRecord: ScanRecord, private val podId: Long) {
private val sequenceNo: Int
private val lotNo: Long
@Throws(DiscoveredInvalidPodException::class)
private fun validateServiceUUIDs() {
val scanRecord = scanResult.scanRecord
?: throw DiscoveredInvalidPodException("Scan record is null");
val serviceUuids = scanRecord.serviceUuids
if (serviceUuids.size != 9) {
throw DiscoveredInvalidPodException("Expected 9 service UUIDs, got" + serviceUuids.size, serviceUuids)
}
if (extractUUID16(serviceUuids[0]) != MAIN_SERVICE_UUID) {
// this is the service that we filtered for
throw DiscoveredInvalidPodException("The first exposed service UUID should be 4024, got " + extractUUID16(serviceUuids[0]), serviceUuids)
}
// TODO understand what is serviceUUIDs[1]. 0x2470. Alarms?
if (extractUUID16(serviceUuids[2]) != "000a") {
if (extractUUID16(serviceUuids[2]) != UNKNOWN_THIRD_SERVICE_UUID) {
// constant?
throw DiscoveredInvalidPodException("The third exposed service UUID should be 000a, got " + serviceUuids[2], serviceUuids)
}
}
@Throws(DiscoveredInvalidPodException::class)
private fun validatePodId() {
val scanRecord = scanResult.scanRecord
val serviceUUIDs = scanRecord.serviceUuids
@ -39,7 +40,6 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val podId: Long) {
}
private fun parseLotNo(): Long {
val scanRecord = scanResult.scanRecord
val serviceUUIDs = scanRecord.serviceUuids
val lotSeq = extractUUID16(serviceUUIDs[5]) +
extractUUID16(serviceUUIDs[6]) +
@ -48,7 +48,6 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val podId: Long) {
}
private fun parseSeqNo(): Int {
val scanRecord = scanResult.scanRecord
val serviceUUIDs = scanRecord.serviceUuids
val lotSeq = extractUUID16(serviceUUIDs[7]) +
extractUUID16(serviceUUIDs[8])
@ -57,8 +56,9 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val podId: Long) {
override fun toString(): String {
return "BleDiscoveredDevice{" +
"scanResult=" + scanResult +
"scanRecord=" + scanRecord +
", podID=" + podId +
"scanResult=" + scanResult +
", sequenceNo=" + sequenceNo +
", lotNo=" + lotNo +
'}'
@ -66,6 +66,7 @@ class BleDiscoveredDevice(val scanResult: ScanResult, private val podId: Long) {
companion object {
const val MAIN_SERVICE_UUID = "4024";
const val UNKNOWN_THIRD_SERVICE_UUID = "000a" // FIXME: why is this 000a?
private fun extractUUID16(uuid: ParcelUuid): String {
return uuid.toString().substring(4, 8)
}

View file

@ -33,9 +33,11 @@ class ScanCollector(private val logger: AAPSLogger, private val podID: Long) : S
logger.debug(LTag.PUMPBTCOMM, "ScanCollector looking for podID: $podID")
for (result in found.values) {
try {
val device = BleDiscoveredDevice(result, podID)
ret.add(device)
logger.debug(LTag.PUMPBTCOMM, "ScanCollector found: " + result.toString() + "Pod ID: " + podID)
result.scanRecord?.let {
val device = BleDiscoveredDevice(result, result.scanRecord, podID)
ret.add(device)
logger.debug(LTag.PUMPBTCOMM, "ScanCollector found: " + result.toString() + "Pod ID: " + podID)
}
} catch (e: DiscoveredInvalidPodException) {
logger.debug(LTag.PUMPBTCOMM, "ScanCollector: pod not matching$e")
// this is not the POD we are looking for