295 lines
8.9 KiB
Kotlin
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"
|
|
}
|
|
}
|