Garmin/app/src/main/java/eu/ztsh/garmin/Garmin.kt
2024-08-03 00:20:40 +02:00

295 lines
8.9 KiB
Kotlin

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 com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.tripdata.maneuver.model.Maneuver
import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue
import eu.ztsh.garmin.data.Arrows
import eu.ztsh.garmin.data.DataCache
import eu.ztsh.garmin.data.GarminMapper
import eu.ztsh.garmin.data.Lanes
import eu.ztsh.garmin.data.MapboxMapper
import java.io.IOException
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.SynchronousQueue
@SuppressLint("MissingPermission")
class Garmin(
val context: MainActivity,
val device: BluetoothDevice,
val adapter: BluetoothAdapter
) {
private lateinit var connection: ConnectThread
private lateinit var maneuvers: ManeuverProcessingThread
private lateinit var trips: TripProgressProcessingThread
private lateinit var locations: LocationMatcherProcessingThread
private val cache = DataCache()
private val processingPool = Executors.newFixedThreadPool(8)
fun start() {
connection = ConnectThread()
connection.start()
maneuvers = ManeuverProcessingThread()
maneuvers.start()
trips = TripProgressProcessingThread()
trips.start()
locations = LocationMatcherProcessingThread()
locations.start()
}
fun close() {
connection.close()
maneuvers.interrupt()
maneuvers.join(0)
trips.interrupt()
trips.join(0)
locations.interrupt()
locations.join(0)
processingPool.shutdown()
}
fun process(maneuver: Maneuver) {
processingPool.submit{maneuvers.enqueue(maneuver)}
}
fun process(tripProgressUpdateValue: TripProgressUpdateValue) {
processingPool.submit{trips.enqueue(tripProgressUpdateValue)}
}
fun process(locationMatcherResult: LocationMatcherResult) {
processingPool.submit{locations.enqueue(locationMatcherResult)}
}
private inner class LocationMatcherProcessingThread: ProcessingThread<LocationMatcherResult>() {
override fun mapAndSend(maybeItem: LocationMatcherResult?): LocationMatcherResult? {
if (maybeItem != null && cache.hasChanged(maybeItem)) {
send(GarminMapper.map(MapboxMapper.asSpeed(maybeItem)))
return maybeItem
}
return null
}
override fun updateCache(item: LocationMatcherResult) {
cache.update(item)
}
}
private inner class TripProgressProcessingThread : ProcessingThread<TripProgressUpdateValue>() {
override fun mapAndSend(maybeItem: TripProgressUpdateValue?): TripProgressUpdateValue? {
if (maybeItem != null) {
// it is much simplier to parse and compare model object
val value = MapboxMapper.asEta(maybeItem)
if (cache.hasChanged(value)) {
// TODO: traffic
send(GarminMapper.setTime(value.hours, value.minutes))
cache.update(value)
}
}
return null
}
override fun updateCache(item: TripProgressUpdateValue) {
// won't be used
}
}
private inner class ManeuverProcessingThread : ProcessingThread<Maneuver>() {
override fun mapAndSend(maybeItem: Maneuver?): Maneuver? {
if (maybeItem != null) {
Log.d(TAG, "mapAndSend (${currentThread().name}): got new")
var changed = false
if (cache.hasChanged(maybeItem.primary)) {
changed = true
Log.d(TAG, "mapAndSend: primary")
send(GarminMapper.map(MapboxMapper.asDirection(maybeItem)))
if (maybeItem.laneGuidance == null) {
send(GarminMapper.map(Lanes(Arrows(setOf()), Arrows(setOf()))))
}
}
if (cache.hasChanged(maybeItem.laneGuidance)) {
changed = true
Log.d(TAG, "mapAndSend: lanes")
send(GarminMapper.map(MapboxMapper.asLanes(maybeItem)))
}
if (cache.hasChanged(maybeItem.stepDistance)) {
changed = true
Log.d(TAG, "mapAndSend: stepDistance")
send(GarminMapper.map(MapboxMapper.asDistance(maybeItem)))
}
if (changed) {
return maybeItem
}
}
return null
}
override fun updateCache(item: Maneuver) {
cache.update(item)
}
}
private abstract inner class ProcessingThread<T> : Thread() {
private val queue: SynchronousQueue<T> = SynchronousQueue()
private var stop: Boolean = false
abstract fun mapAndSend(maybeItem: T?): T?
abstract fun updateCache(item: T)
fun send(data: IntArray) {
connection.enqueue(data)
}
fun enqueue(item: T) {
queue.put(item)
}
override fun run() {
while (!stop) {
val maybeItem = queue.poll()
val item = mapAndSend(maybeItem)
if (item != null) {
Log.d(TAG, "run: Cache updated")
updateCache(item)
}
}
}
override fun interrupt() {
stop = true
super.interrupt()
}
}
private inner class ConnectThread : Thread() {
private val queue: SynchronousQueue<IntArray> = SynchronousQueue()
private var current: IntArray = intArrayOf()
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()
try {
socket?.connect()
context.setConnectionStatus(true)
sleep(3000)
readAll()
send(intArrayOf(0x07, 0x01)) // Set GPS to true
while (true) {
val newCurrent = queue.poll()
if (newCurrent == null) {
Log.d(TAG, "run (${currentThread().name}): Sleep...")
sleep(250)
} else {
current = newCurrent
}
send(current)
}
} catch (e: IOException) {
Log.d(TAG, "Not connected", e)
context.setConnectionStatus(false)
while (true) {
// Just dequeue
// TODO: Add option to reconnect
queue.poll()
sleep(900)
}
}
}
// 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 enqueue(data: IntArray) {
queue.put(data)
}
private fun send(data: IntArray) {
sendRaw(prepareData(data))
}
private fun readAll() {
val buffer = ByteArray(64)
val istr = socket!!.inputStream
istr!!.let {
if (it.available() > 0) {
it.read(buffer)
}
}
}
private fun sendRaw(buff: IntArray) {
buff.forEach { socket!!.outputStream.write(it) }
socket!!.outputStream.flush()
readAll()
}
}
companion object {
lateinit var instance: Garmin
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"
}
}