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() { 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() { 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() { 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 : Thread() { private val queue: SynchronousQueue = 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 = 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() 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" } }