Garmin connection (DIRTY)
This commit is contained in:
parent
78922546ab
commit
0c6e305aea
10 changed files with 638 additions and 59 deletions
|
@ -1,16 +1,17 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
id "de.mannodermaus.android-junit5"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'eu.ztsh.garmin'
|
namespace 'eu.ztsh.garmin'
|
||||||
compileSdk 33
|
compileSdk 31
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.ztsh.garmin"
|
applicationId "eu.ztsh.garmin"
|
||||||
minSdk 29
|
minSdk 29
|
||||||
targetSdk 33
|
targetSdk 31
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
||||||
|
@ -36,17 +37,18 @@ dependencies {
|
||||||
|
|
||||||
// Notification parser
|
// Notification parser
|
||||||
implementation 'com.github.3v1n0.GMapsParser:navparser:0.2.1'
|
implementation 'com.github.3v1n0.GMapsParser:navparser:0.2.1'
|
||||||
|
//
|
||||||
// Bluetooth serial
|
// // Bluetooth serial
|
||||||
implementation 'com.github.harry1453:android-bluetooth-serial:v1.1'
|
// implementation 'com.github.harry1453:android-bluetooth-serial:v1.1'
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
|
// implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
// implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
// testImplementation 'junit:junit:4.13.2'
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,24 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<!-- Needed only if your app looks for Bluetooth devices.
|
||||||
|
If your app doesn't use Bluetooth scan results to derive physical
|
||||||
|
location information, you can strongly assert that your app
|
||||||
|
doesn't derive physical location. -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||||
|
|
||||||
|
<!-- Needed only if your app communicates with already-paired Bluetooth
|
||||||
|
devices. -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
|
112
app/src/main/java/eu/ztsh/garmin/Garmin.kt
Normal file
112
app/src/main/java/eu/ztsh/garmin/Garmin.kt
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package eu.ztsh.garmin
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
class Garmin(
|
||||||
|
val context: MainActivity,
|
||||||
|
val device: BluetoothDevice,
|
||||||
|
val adapter: BluetoothAdapter
|
||||||
|
) {
|
||||||
|
|
||||||
|
private lateinit var thread: ConnectThread
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
thread = ConnectThread()
|
||||||
|
thread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
thread.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ConnectThread : Thread() {
|
||||||
|
|
||||||
|
private val socket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
context.checkBt()
|
||||||
|
device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
// Cancel discovery because it otherwise slows down the connection.
|
||||||
|
context.checkBt()
|
||||||
|
adapter.cancelDiscovery()
|
||||||
|
socket?.connect()
|
||||||
|
sleep(3000)
|
||||||
|
readAll()
|
||||||
|
send(intArrayOf(1, 1, 0, 16))
|
||||||
|
send(intArrayOf(3, 0, 1, 10, 0, 10, 3))
|
||||||
|
send(intArrayOf(3, 0, 0, 9, 0, 9, 3))
|
||||||
|
send(intArrayOf(0x04, 0x01))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the client socket and causes the thread to finish.
|
||||||
|
fun close() {
|
||||||
|
try {
|
||||||
|
socket?.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Could not close the client socket", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readAll() {
|
||||||
|
val buffer = ByteArray(64)
|
||||||
|
val istr = socket!!.inputStream
|
||||||
|
istr!!.let {
|
||||||
|
if (it.available() > 0) {
|
||||||
|
it.read(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(hex: IntArray) {
|
||||||
|
sendRaw(prepareData(hex))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendRaw(buff: IntArray) {
|
||||||
|
buff.forEach { socket!!.outputStream.write(it) }
|
||||||
|
socket!!.outputStream.flush()
|
||||||
|
sleep(2000)
|
||||||
|
readAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun prepareData(input: IntArray): IntArray {
|
||||||
|
val n = input.size
|
||||||
|
var crc = (0xeb + n + n).toUInt()
|
||||||
|
val chars = ArrayList<Int>()
|
||||||
|
chars.add(0x10)
|
||||||
|
chars.add(0x7b)
|
||||||
|
chars.add((n + 0x06))
|
||||||
|
if (n == 0xa)
|
||||||
|
chars.add(0x10)
|
||||||
|
chars.add(n)
|
||||||
|
chars.add(0x00)
|
||||||
|
chars.add(0x00)
|
||||||
|
chars.add(0x00)
|
||||||
|
chars.add(0x55)
|
||||||
|
chars.add(0x15)
|
||||||
|
for (char in input) {
|
||||||
|
crc = (crc + char.toUInt())
|
||||||
|
chars.add(char)
|
||||||
|
if (char == 0x10)
|
||||||
|
chars.add(0x10)
|
||||||
|
}
|
||||||
|
chars.add((-(crc.toInt()) and 0xff))
|
||||||
|
chars.add(0x10)
|
||||||
|
chars.add(0x03)
|
||||||
|
return chars.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val TAG = "GARMIN"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,125 @@
|
||||||
package eu.ztsh.garmin
|
package eu.ztsh.garmin
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
lateinit var garmin: Garmin
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
bluetoothInit()
|
||||||
|
}
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
// bluetoothSerial.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// bluetoothSerial.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bluetoothInit() {
|
||||||
|
val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
|
||||||
|
val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
|
||||||
|
?: // Device doesn't support Bluetooth
|
||||||
|
throw Exception()
|
||||||
|
if (!bluetoothAdapter.isEnabled) {
|
||||||
|
// TODO: Start intent
|
||||||
|
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
|
registerForActivityResult(StartActivityForResult(), ActivityResultCallback { })
|
||||||
|
}
|
||||||
|
checkBt()
|
||||||
|
|
||||||
|
val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter.bondedDevices
|
||||||
|
val context = this
|
||||||
|
pairedDevices?.firstOrNull { device ->
|
||||||
|
Log.d(TAG, device.name)
|
||||||
|
device.name.equals("GARMIN HUD")
|
||||||
|
}?.apply {
|
||||||
|
garmin = Garmin(context, this, bluetoothAdapter)
|
||||||
|
garmin.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkBt(): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
|
||||||
|
checkBt(Manifest.permission.BLUETOOTH_SCAN)
|
||||||
|
checkBt(Manifest.permission.BLUETOOTH_CONNECT)
|
||||||
|
} else {
|
||||||
|
checkBt(Manifest.permission.BLUETOOTH)
|
||||||
|
checkBt(Manifest.permission.BLUETOOTH_ADMIN)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkBt(permission: String): Boolean {
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
permission
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
ActivityCompat.requestPermissions(this, arrayOf(permission), 1)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ConnectThread(val device: BluetoothDevice, val adapter: BluetoothAdapter) : Thread() {
|
||||||
|
|
||||||
|
private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
checkBt()
|
||||||
|
// device.createInsecureRfcommSocketToServiceRecord(UUID.fromString("7d00d7f5-921b-450c-8eda-26e1d4a15c61"))
|
||||||
|
device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
// Cancel discovery because it otherwise slows down the connection.
|
||||||
|
checkBt()
|
||||||
|
adapter.cancelDiscovery()
|
||||||
|
|
||||||
|
mmSocket?.let { socket ->
|
||||||
|
// Connect to the remote device through the socket. This call blocks
|
||||||
|
// until it succeeds or throws an exception.
|
||||||
|
socket.connect()
|
||||||
|
|
||||||
|
// The connection attempt succeeded. Perform work associated with
|
||||||
|
// the connection in a separate thread.
|
||||||
|
// manageMyConnectedSocket(socket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Closes the client socket and causes the thread to finish.
|
||||||
|
fun cancel() {
|
||||||
|
try {
|
||||||
|
mmSocket?.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(Companion.TAG, "Could not close the client socket", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "bt"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
310
app/src/main/java/eu/ztsh/garmin/bt/BluetoothSerial.kt
Normal file
310
app/src/main/java/eu/ztsh/garmin/bt/BluetoothSerial.kt
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
package eu.ztsh.garmin.bt
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.AsyncTask
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import eu.ztsh.garmin.MainActivity
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class BluetoothSerial(var context: MainActivity, var messageHandler: MessageHandler, devicePrefix: String) {
|
||||||
|
var connected = false
|
||||||
|
var bluetoothDevice: BluetoothDevice? = null
|
||||||
|
var serialSocket: BluetoothSocket? = null
|
||||||
|
var serialInputStream: InputStream? = null
|
||||||
|
var serialOutputStream: OutputStream? = null
|
||||||
|
var serialReader: SerialReader? = null
|
||||||
|
var connectionTask: AsyncTask<Void?, Void?, BluetoothDevice?>? = null
|
||||||
|
var devicePrefix: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for discount message from bluetooth system and restablishing a connection
|
||||||
|
*/
|
||||||
|
private val bluetoothReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val action = intent.action
|
||||||
|
val eventDevice = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||||
|
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action) {
|
||||||
|
if (bluetoothDevice != null && bluetoothDevice == eventDevice) {
|
||||||
|
Log.i(BMX_BLUETOOTH, "Received bluetooth disconnect notice")
|
||||||
|
|
||||||
|
//clean up any streams
|
||||||
|
close()
|
||||||
|
|
||||||
|
//reestablish connect
|
||||||
|
connect()
|
||||||
|
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(BLUETOOTH_DISCONNECTED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.devicePrefix = devicePrefix.uppercase(Locale.getDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPause() {
|
||||||
|
context.unregisterReceiver(bluetoothReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResume() {
|
||||||
|
//listen for bluetooth disconnect
|
||||||
|
val disconnectIntent = IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||||
|
context.registerReceiver(bluetoothReceiver, disconnectIntent)
|
||||||
|
|
||||||
|
//reestablishes a connection is one doesn't exist
|
||||||
|
if (!connected) {
|
||||||
|
connect()
|
||||||
|
} else {
|
||||||
|
val intent = Intent(BLUETOOTH_CONNECTED)
|
||||||
|
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the bluetooth serial connections, uses the LocalBroadcastManager when
|
||||||
|
* connection is established
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun connect() {
|
||||||
|
if (connected) {
|
||||||
|
Log.e(BMX_BLUETOOTH, "Connection request while already connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (connectionTask != null && connectionTask!!.status == AsyncTask.Status.RUNNING) {
|
||||||
|
Log.e(BMX_BLUETOOTH, "Connection request while attempting connection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
||||||
|
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
context.checkBt()
|
||||||
|
val pairedDevices: List<BluetoothDevice> = ArrayList(bluetoothAdapter.getBondedDevices())
|
||||||
|
if (pairedDevices.isNotEmpty()) {
|
||||||
|
bluetoothAdapter.cancelDiscovery()
|
||||||
|
/**
|
||||||
|
* AsyncTask to handle the establishing of a bluetooth connection
|
||||||
|
*/
|
||||||
|
// connectionTask = object : AsyncTask<Void?, Void?, BluetoothDevice?>() {
|
||||||
|
// var MAX_ATTEMPTS = 30
|
||||||
|
// var attemptCounter = 0
|
||||||
|
// protected override fun doInBackground(vararg params: Void): BluetoothDevice? {
|
||||||
|
// while (!isCancelled) { //need to kill without calling onCancel
|
||||||
|
// for (device in pairedDevices) {
|
||||||
|
// if (device.getName().uppercase(Locale.getDefault()).startsWith(devicePrefix)) {
|
||||||
|
// Log.i(
|
||||||
|
// BMX_BLUETOOTH,
|
||||||
|
// attemptCounter.toString() + ": Attempting connection to " + device.getName()
|
||||||
|
// )
|
||||||
|
// try {
|
||||||
|
// serialSocket = try {
|
||||||
|
// // Standard SerialPortService ID
|
||||||
|
// val uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
|
||||||
|
// device.createRfcommSocketToServiceRecord(uuid)
|
||||||
|
// } catch (ce: Exception) {
|
||||||
|
// connectViaReflection(device)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// //setup the connect streams
|
||||||
|
// serialSocket!!.connect()
|
||||||
|
// serialInputStream = serialSocket!!.inputStream
|
||||||
|
// serialOutputStream = serialSocket!!.outputStream
|
||||||
|
// connected = true
|
||||||
|
// Log.i(BMX_BLUETOOTH, "Connected to " + device.getName())
|
||||||
|
// return device
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// serialSocket = null
|
||||||
|
// serialInputStream = null
|
||||||
|
// serialOutputStream = null
|
||||||
|
// Log.i(BMX_BLUETOOTH, e.message!!)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// try {
|
||||||
|
// attemptCounter++
|
||||||
|
// if (attemptCounter > MAX_ATTEMPTS) cancel(false) else Thread.sleep(1000)
|
||||||
|
// } catch (e: InterruptedException) {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Log.i(BMX_BLUETOOTH, "Stopping connection attempts")
|
||||||
|
// val intent = Intent(BLUETOOTH_FAILED)
|
||||||
|
// LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onPostExecute(result: BluetoothDevice?) {
|
||||||
|
// super.onPostExecute(result)
|
||||||
|
// bluetoothDevice = result
|
||||||
|
//
|
||||||
|
// //start thread responsible for reading from inputstream
|
||||||
|
// serialReader = SerialReader()
|
||||||
|
// serialReader!!.start()
|
||||||
|
//
|
||||||
|
// //send connection message
|
||||||
|
// val intent = Intent(BLUETOOTH_CONNECTED)
|
||||||
|
// LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// connectionTask.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// see: http://stackoverflow.com/questions/3397071/service-discovery-failed-exception-using-bluetooth-on-android
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun connectViaReflection(device: BluetoothDevice): BluetoothSocket {
|
||||||
|
val m = device.javaClass.getMethod(
|
||||||
|
"createRfcommSocket", *arrayOf<Class<*>?>(
|
||||||
|
Int::class.javaPrimitiveType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return m.invoke(device, 1) as BluetoothSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun available(): Int {
|
||||||
|
if (connected) return serialInputStream!!.available()
|
||||||
|
throw RuntimeException("Connection lost, reconnecting now.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun read(): Int {
|
||||||
|
if (connected) return serialInputStream!!.read()
|
||||||
|
throw RuntimeException("Connection lost, reconnecting now.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun read(buffer: ByteArray?): Int {
|
||||||
|
if (connected) return serialInputStream!!.read(buffer)
|
||||||
|
throw RuntimeException("Connection lost, reconnecting now.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun read(buffer: ByteArray?, byteOffset: Int, byteCount: Int): Int {
|
||||||
|
if (connected) return serialInputStream!!.read(buffer, byteOffset, byteCount)
|
||||||
|
throw RuntimeException("Connection lost, reconnecting now.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun write(buffer: ByteArray?) {
|
||||||
|
if (connected) serialOutputStream!!.write(buffer)
|
||||||
|
throw RuntimeException("Connection lost, reconnecting now.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun write(oneByte: Int) {
|
||||||
|
if (connected) serialOutputStream!!.write(oneByte)
|
||||||
|
throw RuntimeException("Connection lost, reconnecting now.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun write(buffer: ByteArray?, offset: Int, count: Int) {
|
||||||
|
serialOutputStream!!.write(buffer, offset, count)
|
||||||
|
throw RuntimeException("Connection lost, reconnecting now.")
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class SerialReader : Thread() {
|
||||||
|
var buffer = ByteArray(Companion.MAX_BYTES)
|
||||||
|
var bufferSize = 0
|
||||||
|
override fun run() {
|
||||||
|
Log.i("serialReader", "Starting serial loop")
|
||||||
|
while (!isInterrupted) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* check for some bytes, or still bytes still left in
|
||||||
|
* buffer
|
||||||
|
*/
|
||||||
|
if (available() > 0) {
|
||||||
|
val newBytes = read(buffer, bufferSize, Companion.MAX_BYTES - bufferSize)
|
||||||
|
if (newBytes > 0) bufferSize += newBytes
|
||||||
|
Log.d(BMX_BLUETOOTH, "read $newBytes")
|
||||||
|
}
|
||||||
|
if (bufferSize > 0) {
|
||||||
|
val read = messageHandler.read(bufferSize, buffer)
|
||||||
|
|
||||||
|
// shift unread data to start of buffer
|
||||||
|
if (read > 0) {
|
||||||
|
var index = 0
|
||||||
|
for (i in read until bufferSize) {
|
||||||
|
buffer[index++] = buffer[i]
|
||||||
|
}
|
||||||
|
bufferSize = index
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
sleep(10)
|
||||||
|
} catch (ie: InterruptedException) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(BMX_BLUETOOTH, "Error reading serial data", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(BMX_BLUETOOTH, "Shutting serial loop")
|
||||||
|
}
|
||||||
|
|
||||||
|
// companion object {
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads from the serial buffer, processing any available messages. Must return the number of bytes
|
||||||
|
* consumer from the buffer
|
||||||
|
*
|
||||||
|
* @author jpetrocik
|
||||||
|
*/
|
||||||
|
fun interface MessageHandler {
|
||||||
|
fun read(bufferSize: Int, buffer: ByteArray?): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
connected = false
|
||||||
|
if (connectionTask != null) {
|
||||||
|
connectionTask!!.cancel(false)
|
||||||
|
}
|
||||||
|
if (serialReader != null) {
|
||||||
|
serialReader!!.interrupt()
|
||||||
|
try {
|
||||||
|
serialReader!!.join(1000)
|
||||||
|
} catch (ie: InterruptedException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
serialInputStream!!.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(BMX_BLUETOOTH, "Failed releasing inputstream connection")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
serialOutputStream!!.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(BMX_BLUETOOTH, "Failed releasing outputstream connection")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
serialSocket!!.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(BMX_BLUETOOTH, "Failed closing socket")
|
||||||
|
}
|
||||||
|
Log.i(BMX_BLUETOOTH, "Released bluetooth connections")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BMX_BLUETOOTH = "BMXBluetooth"
|
||||||
|
var BLUETOOTH_CONNECTED = "bluetooth-connection-started"
|
||||||
|
var BLUETOOTH_DISCONNECTED = "bluetooth-connection-lost"
|
||||||
|
var BLUETOOTH_FAILED = "bluetooth-connection-failed"
|
||||||
|
private const val MAX_BYTES = 125
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package eu.ztsh.garmin
|
package eu.ztsh.garmin
|
||||||
|
|
||||||
import org.junit.Test
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
|
22
app/src/test/java/eu/ztsh/garmin/GarminTest.kt
Normal file
22
app/src/test/java/eu/ztsh/garmin/GarminTest.kt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package eu.ztsh.garmin
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class GarminTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun arayCreationTest() {
|
||||||
|
arrayTest(intArrayOf(0x04, 0x01), intArrayOf(16, 123, 8, 2, 0, 0, 0, 85, 21, 4, 1, 12, 16, 3))
|
||||||
|
arrayTest(
|
||||||
|
intArrayOf(3, 0, 0, 9, 0, 9, 3),
|
||||||
|
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 0, 9, 0, 9, 3, 0xef, 16, 3)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun arrayTest(input: IntArray, expected: IntArray) {
|
||||||
|
val output = Garmin.prepareData(input)
|
||||||
|
assertArrayEquals(expected, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,11 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
buildscript {
|
||||||
|
dependencies {
|
||||||
|
classpath("de.mannodermaus.gradle.plugins:android-junit5:1.8.2.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '7.4.1' apply true
|
id 'com.android.application' version '7.4.1' apply false
|
||||||
id 'com.android.library' version '7.4.1' apply false
|
id 'com.android.library' version '7.4.1' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
|
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,19 @@
|
||||||
# Project-wide Gradle settings.
|
## For more details on how to configure your build environment visit
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
#
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||||
|
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
|
#
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
#Thu Jun 22 21:51:56 CEST 2023
|
||||||
# Android operating system, and which are packaged with your app's APK
|
android.enableJetifier=true
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
|
||||||
kotlin.code.style=official
|
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding\=UTF-8
|
||||||
|
org.gradle.unsafe.configuration-cache=true
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue