diff --git a/app/build.gradle b/app/build.gradle index cf3f08a..855563f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,6 +16,7 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + buildConfigField "String", "MAPBOX_DOWNLOADS_TOKEN", "\"$MAPBOX_DOWNLOADS_TOKEN\"" } buildTypes { @@ -33,6 +34,7 @@ android { } buildFeatures { viewBinding true + buildConfig = true } } diff --git a/app/src/main/java/eu/ztsh/garmin/Garmin.kt b/app/src/main/java/eu/ztsh/garmin/Garmin.kt index 21071f5..f71221f 100644 --- a/app/src/main/java/eu/ztsh/garmin/Garmin.kt +++ b/app/src/main/java/eu/ztsh/garmin/Garmin.kt @@ -5,8 +5,10 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothSocket import android.util.Log +import com.mapbox.navigation.ui.maneuver.model.Maneuver import java.io.IOException import java.util.* +import java.util.concurrent.SynchronousQueue @SuppressLint("MissingPermission") @@ -16,19 +18,79 @@ class Garmin( val adapter: BluetoothAdapter ) { - private lateinit var thread: ConnectThread + private lateinit var connection: ConnectThread + private lateinit var processing: ProcessingThread + private var stateCache: State = State() fun start() { - thread = ConnectThread() - thread.start() + connection = ConnectThread() + connection.start() } fun close() { - thread.close() + connection.close() + } + + fun process(maneuver: Maneuver) { + processing = ProcessingThread(maneuver) + processing.start() + processing.join() + } + + + + private inner class ProcessingThread(val maneuver: Maneuver) : Thread() { + override fun run() { + send(ManeuverMapper.apply(maneuver)) + } + + fun send(incoming: eu.ztsh.garmin.State) { + if (stateCache.distance != incoming.distance) { + setDistance(incoming) + } + if (stateCache.direction != incoming.direction) { + setDirection(stateCache.direction) + } + stateCache = incoming + } + + private fun setDistance(state: eu.ztsh.garmin.State) { + connection.enqueue(intArrayOf( + 0x03, asDigit(state.distance / 1000), asDigit(state.distance / 100), asDigit(state.distance / 10), + 0x00, asDigit(state.distance), state.unit.data + )) + } + + private fun setDirection(direction: Direction) { + val param1 = when (direction.outAngle) { + OutAngle.LeftDown -> 0x10 + OutAngle.RightDown -> 0x20 + else -> direction.outType.data + } + val param2: Int = if (direction.outType == OutType.RightRoundabout + || direction.outType == OutType.LeftRoundabout) { + if (direction.roundabout == OutAngle.AsDirection) direction.outAngle.data else direction.roundabout.data + } else { + 0x00 + } + val param3: Int = if (direction.outAngle == OutAngle.LeftDown || direction.outAngle == OutAngle.RightDown) 0x00 else direction.outAngle.data + connection.enqueue(intArrayOf(0x01, param1, param2, param3)) + } + + private fun asDigit(n: Int): Int { + if (n == 0) { + return 0 + } + val m = n % 10 + return if (m == 0) 10 else m + } } 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")) @@ -41,10 +103,12 @@ class Garmin( 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)) + while (true) { + val newCurrent = Optional.ofNullable(queue.poll()).orElse(current) + current = newCurrent + send(current) + sleep(900) + } } // Closes the client socket and causes the thread to finish. @@ -56,7 +120,15 @@ class Garmin( } } - fun readAll() { + 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 { @@ -66,10 +138,6 @@ class Garmin( } } - fun send(hex: IntArray) { - sendRaw(prepareData(hex)) - } - private fun sendRaw(buff: IntArray) { buff.forEach { socket!!.outputStream.write(it) } socket!!.outputStream.flush() diff --git a/app/src/main/java/eu/ztsh/garmin/MainActivity.kt b/app/src/main/java/eu/ztsh/garmin/MainActivity.kt index bb0af4c..4d02d83 100644 --- a/app/src/main/java/eu/ztsh/garmin/MainActivity.kt +++ b/app/src/main/java/eu/ztsh/garmin/MainActivity.kt @@ -14,6 +14,15 @@ import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.mapbox.navigation.base.formatter.DistanceFormatterOptions +import com.mapbox.navigation.base.options.NavigationOptions +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import com.mapbox.navigation.core.trip.session.RouteProgressObserver +import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi import eu.ztsh.garmin.databinding.ActivityMainBinding @SuppressLint("MissingPermission") @@ -21,15 +30,66 @@ class MainActivity : AppCompatActivity() { lateinit var garmin: Garmin - private lateinit var binding : ActivityMainBinding + private lateinit var binding: ActivityMainBinding + private val mapboxObserver = MapboxObserver() + + init { + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + MapboxNavigationApp.attach(owner) + MapboxNavigationApp.registerObserver(mapboxObserver) + } + + override fun onPause(owner: LifecycleOwner) { + MapboxNavigationApp.detach(owner) + MapboxNavigationApp.unregisterObserver(mapboxObserver) + } + }) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) -// bluetoothInit() + if (!MapboxNavigationApp.isSetup()) { + MapboxNavigationApp.setup { + NavigationOptions.Builder(applicationContext) + .accessToken(BuildConfig.MAPBOX_DOWNLOADS_TOKEN) + .build() + } + } + MapboxNavigationApp.current()?.startTripSession() + bluetoothInit() } + override fun onStart() { + super.onStart() + MapboxNavigationApp.current()?.registerRouteProgressObserver(routeProgressObserver) + } + override fun onStop() { + super.onStop() + MapboxNavigationApp.current()?.unregisterRouteProgressObserver(routeProgressObserver) + } + + override fun onDestroy() { + super.onDestroy() + MapboxNavigationApp.current()?.stopTripSession() + maneuverApi.cancel() + } + + + // Define distance formatter options + private val distanceFormatter: DistanceFormatterOptions by lazy { + DistanceFormatterOptions.Builder(this).build() + } + // Create an instance of the Maneuver API + private val maneuverApi: MapboxManeuverApi by lazy { + MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatter)) + } + + private val routeProgressObserver = + RouteProgressObserver { routeProgress -> maneuverApi.getManeuvers(routeProgress).value?.apply { garmin.process(this[0]) } } + private fun bluetoothInit() { val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java) val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter diff --git a/app/src/main/java/eu/ztsh/garmin/ManeuverMapper.kt b/app/src/main/java/eu/ztsh/garmin/ManeuverMapper.kt new file mode 100644 index 0000000..3b8b54f --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/ManeuverMapper.kt @@ -0,0 +1,66 @@ +package eu.ztsh.garmin + +import com.mapbox.navigation.ui.maneuver.model.Maneuver + +class ManeuverMapper { + + companion object { + + fun apply(maneuver: Maneuver): State { + val state = State() + maneuver.apply { + this.primary.apply { + state.direction = Direction() + when (this.type) { + "turn" -> { + state.direction.outType = OutType.Lane + state.direction.roundabout = OutAngle.AsDirection + } + + "roundabout" -> { + state.direction.outType = OutType.RightRoundabout + } + + "arrive" -> { + state.flag = true + } + } + when (this.modifier) { + "right" -> { + when (this.type) { + "turn" -> state.direction.outAngle = OutAngle.Right + "roundabout" -> { + when (this.degrees) { + 137.0 -> state.direction.outAngle = OutAngle.EasyRight + } + } + } + } + "left" -> { + when (this.type) { + "turn" -> state.direction.outAngle = OutAngle.Left + } + } + } + } + this.stepDistance.apply { + this.distanceRemaining?.apply { + distanceFormatter.formatDistance(distanceRemaining!!).split(" ").apply { + state.distance = this[0].toInt() + state.unit = when (this[1]) { + "m" -> Unit.Metres + "km" -> Unit.Kilometres + else -> {Unit.Any} + } + + } + } + + } + } + return state + } + + } + +} diff --git a/app/src/main/java/eu/ztsh/garmin/MapboxObserver.kt b/app/src/main/java/eu/ztsh/garmin/MapboxObserver.kt new file mode 100644 index 0000000..35bdabd --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/MapboxObserver.kt @@ -0,0 +1,21 @@ +package eu.ztsh.garmin + +import android.util.Log +import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver + +class MapboxObserver : MapboxNavigationObserver { + + override fun onAttached(mapboxNavigation: MapboxNavigation) { + Log.d(TAG, "Attached") + } + + override fun onDetached(mapboxNavigation: MapboxNavigation) { + Log.d(TAG, "Detached") + } + + companion object { + const val TAG = "MBOXOBS" + } + +} diff --git a/app/src/main/java/eu/ztsh/garmin/Model.kt b/app/src/main/java/eu/ztsh/garmin/Model.kt new file mode 100644 index 0000000..15e77bf --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/Model.kt @@ -0,0 +1,96 @@ +package eu.ztsh.garmin + +enum class OutType(val data: Int) { + + Off(0x00), + Lane(0x01), + LongerLane(0x02), + LeftRoundabout(0x04), + RightRoundabout(0x08), + Flag(0x40), + ArrowOnly(0x80); + +} + + +enum class OutAngle(val data: Int) { + + Down(0x01), + SharpRight(0x02), + Right(0x04), + EasyRight(0x08), + Straight(0x10), + EasyLeft(0x20), + Left(0x40), + SharpLeft(0x80), + LeftDown(0x81), + RightDown(0x82), + AsDirection(0x00) + +} + +enum class Unit(val data: Int) { + + Any(0), + Metres(1), + Kilometres(3), + Miles(5), + Foot(8) + +} + +enum class Lane(val data: Int) { + + DotsRight(0x01), + OuterRight(0x02), + MiddleRight(0x04), + InnerRight(0x08), + InnerLeft(0x10), + MiddleLeft(0x20), + OuterLeft(0x40), + DotsLeft(0x80) + +} + +class State { + + var lineArrows: Int = 0 + var lineOutlines: Int = 0 + var direction = Direction() + var distance: Int = 0 + var unit: Unit = Unit.Any + var speed: Int = 0 + var limit: Int = 0 + var hours: Int = 0 + var minutes: Int = 0 + var traffic: Boolean = false + var flag: Boolean = false + var control: Boolean = false + +} + +class Direction { + var outAngle: OutAngle = OutAngle.AsDirection + var outType: OutType = OutType.Lane + var roundabout: OutAngle = OutAngle.AsDirection + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Direction + + if (outAngle != other.outAngle) return false + if (outType != other.outType) return false + if (roundabout != other.roundabout) return false + + return true + } + + override fun hashCode(): Int { + var result = outAngle.hashCode() + result = 31 * result + outType.hashCode() + result = 31 * result + roundabout.hashCode() + return result + } + +}