Merge branch 'mapbox'

This commit is contained in:
Piotr Dec 2024-08-03 02:05:58 +02:00
commit 82a29cb00a
Signed by: stawros
GPG key ID: F89F27AD8F881A91
30 changed files with 2384 additions and 580 deletions

1
.gitattributes vendored
View file

@ -1 +1,2 @@
* text eol=lf * text eol=lf
*.bat text eol=crlf

1
.gitignore vendored
View file

@ -8,3 +8,4 @@
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
__pycache__/

View file

@ -6,16 +6,18 @@ plugins {
android { android {
namespace 'eu.ztsh.garmin' namespace 'eu.ztsh.garmin'
compileSdk 31 compileSdk 34
ndkVersion "23.2.8568313"
defaultConfig { defaultConfig {
applicationId "eu.ztsh.garmin" applicationId "eu.ztsh.garmin"
minSdk 29 minSdk 29
targetSdk 31 targetSdk 34
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "MAPBOX_DOWNLOADS_TOKEN", "\"$MAPBOX_DOWNLOADS_TOKEN\""
} }
buildTypes { buildTypes {
@ -33,27 +35,26 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig = true
} }
} }
ext {
mapboxVersion = '3.2.0'
}
dependencies { dependencies {
implementation "com.mapbox.navigationcore:navigation:$mapboxVersion"
// Notification parser implementation "com.mapbox.navigationcore:ui-maps:$mapboxVersion"
implementation 'com.github.3v1n0.GMapsParser:navparser:0.2.1' implementation "com.mapbox.navigationcore:voice:$mapboxVersion"
// implementation "com.mapbox.navigationcore:tripdata:$mapboxVersion"
// // Bluetooth serial implementation "com.mapbox.navigationcore:ui-components:$mapboxVersion"
// implementation 'com.github.harry1453:android-bluetooth-serial:v1.1' implementation 'androidx.core:core-ktx:1.13.1'
// implementation 'io.reactivex.rxjava2:rxjava:2.1.12' implementation 'androidx.appcompat:appcompat:1.7.0'
// implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.activity:activity-ktx:1.4.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
// testImplementation 'junit:junit:4.13.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' testImplementation 'org.assertj:assertj-core:3.24.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
} }

View file

@ -8,21 +8,15 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" /> 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" /> <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-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/> <uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<queries>
<package android:name="com.google.android.apps.maps" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
</queries> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -32,15 +26,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Garmin" android:theme="@style/Theme.Garmin"
tools:targetApi="31"> tools:targetApi="31">
<service
android:name="me.trevi.navparser.service.NavigationListenerEmitter"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true">

View file

@ -5,12 +5,19 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
import android.util.Log import android.util.Log
import me.trevi.navparser.lib.NavigationData 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.io.IOException
import java.util.* import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.SynchronousQueue import java.util.concurrent.SynchronousQueue
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
class Garmin( class Garmin(
val context: MainActivity, val context: MainActivity,
@ -18,48 +25,202 @@ class Garmin(
val adapter: BluetoothAdapter val adapter: BluetoothAdapter
) { ) {
private lateinit var thread: ConnectThread 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() { fun start() {
thread = ConnectThread() connection = ConnectThread()
thread.start() connection.start()
}
fun send(data: IntArray) { maneuvers = ManeuverProcessingThread()
thread.enqueue(data) maneuvers.start()
}
fun send(data: NavigationData) { trips = TripProgressProcessingThread()
Log.d("SEND_DATA", "$data.remainingDistance.distance") trips.start()
locations = LocationMatcherProcessingThread()
locations.start()
} }
fun close() { fun close() {
thread.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 inner class ConnectThread : Thread() {
private val queue: SynchronousQueue<IntArray> = SynchronousQueue()
private var current: IntArray = intArrayOf()
private val socket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) { private val socket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
context.checkBt() context.checkBt()
device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")) device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
} }
private val queue: SynchronousQueue<IntArray> = SynchronousQueue()
fun enqueue(data: IntArray) {
// queue.clear() // for immediate run
queue.put(data)
}
override fun run() { override fun run() {
// Cancel discovery because it otherwise slows down the connection. // Cancel discovery because it otherwise slows down the connection.
context.checkBt() context.checkBt()
adapter.cancelDiscovery() adapter.cancelDiscovery()
socket?.connect() try {
sleep(3000) socket?.connect()
readAll() context.setConnectionStatus(true)
while (true) { sleep(3000)
queue.poll()?.let { send(it) } 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)
}
} }
} }
@ -72,7 +233,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 buffer = ByteArray(64)
val istr = socket!!.inputStream val istr = socket!!.inputStream
istr!!.let { istr!!.let {
@ -82,20 +251,18 @@ class Garmin(
} }
} }
fun send(data: IntArray) {
sendRaw(prepareData(data))
}
private fun sendRaw(buff: IntArray) { private fun sendRaw(buff: IntArray) {
buff.forEach { socket!!.outputStream.write(it) } buff.forEach { socket!!.outputStream.write(it) }
socket!!.outputStream.flush() socket!!.outputStream.flush()
sleep(2000)
readAll() readAll()
} }
} }
companion object { companion object {
lateinit var instance: Garmin
fun prepareData(input: IntArray): IntArray { fun prepareData(input: IntArray): IntArray {
val n = input.size val n = input.size
var crc = (0xeb + n + n).toUInt() var crc = (0xeb + n + n).toUInt()

View file

@ -2,94 +2,79 @@ package eu.ztsh.garmin
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.CompoundButton import android.view.WindowManager
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.lifecycle.DefaultLifecycleObserver
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
import com.mapbox.navigation.core.lifecycle.requireMapboxNavigation
import eu.ztsh.garmin.databinding.ActivityMainBinding import eu.ztsh.garmin.databinding.ActivityMainBinding
import me.trevi.navparser.lib.NavigationData import eu.ztsh.garmin.mapbox.MapControl
import me.trevi.navparser.service.* import eu.ztsh.garmin.util.PermissionsHelper
import java.io.IOException import java.lang.ref.WeakReference
import java.util.*
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
var navigating: Boolean = false // lateinit var garmin: Garmin
lateinit var garmin: Garmin
private lateinit var binding : ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val navDataModel: NavigationDataModel by viewModels() private lateinit var mapControl: MapControl
private val permissionsHelper = PermissionsHelper(WeakReference(this))
val mapboxNavigation: MapboxNavigation by requireMapboxNavigation(
onResumedObserver = object : DefaultLifecycleObserver, MapboxNavigationObserver {
override fun onAttached(mapboxNavigation: MapboxNavigation) {
mapControl.onAttached(mapboxNavigation)
}
override fun onDetached(mapboxNavigation: MapboxNavigation) {
mapControl.onDetached(mapboxNavigation)
}
},
onInitialize = fun() {
mapControl.initNavigation()
}
)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.mapView
permissionsHelper.checkPermissions {
mapControl = MapControl(this, UI(binding), resources)
mapControl.init()
MapboxNavigationApp.setup(
NavigationOptions.Builder(applicationContext)
.build()
)
}
bluetoothInit() bluetoothInit()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding.enabledSwitch.setOnCheckedChangeListener {
_, isChecked ->
run {
navigating = isChecked
setNavigationData(NavigationData(true))
navDataModel.liveData.observe(this) { setNavigationData(it) }
Log.d(TAG, "OK")
}
}
} }
override fun onStart() {
super.onStart()
startServiceListener() override fun onDestroy() {
checkNotificationsAccess() super.onDestroy()
} mapControl.onDestroy()
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
Log.d(TAG, "Got activity result: $intent, ${intent?.extras}, ${intent?.action}")
when (intent?.action) {
NOTIFICATIONS_ACCESS_RESULT -> {
intent.getBooleanExtra(NOTIFICATIONS_ACCESS_RESULT, false).also {
val notificationAccess = it
if (!notificationAccess) {
Log.e(TAG, "No notification access for ${NavigationListenerEmitter::class.qualifiedName}")
}
}
}
NAVIGATION_DATA_UPDATED -> {
// gotoFragment(R.id.NavigationFragment)
val navData = intent.getParcelableExtra<NavigationData>(NAVIGATION_DATA)
Log.d(TAG, "Got navigation data $navData")
navDataModel.data = navData
}
NAVIGATION_STARTED -> {
Log.d(TAG, "Started")
}
NAVIGATION_STOPPED -> {
Log.d(TAG, "Stopped")
}
}
} }
private fun bluetoothInit() { private fun bluetoothInit() {
@ -108,10 +93,10 @@ class MainActivity : AppCompatActivity() {
val context = this val context = this
pairedDevices?.firstOrNull { device -> pairedDevices?.firstOrNull { device ->
Log.d(TAG, device.name) Log.d(TAG, device.name)
device.name.equals("GARMIN HUD") device.name == "GARMIN HUD"
}?.apply { }?.apply {
garmin = Garmin(context, this, bluetoothAdapter) Garmin.instance = Garmin(context, this, bluetoothAdapter)
garmin.start() Garmin.instance.start()
} }
} }
@ -138,33 +123,19 @@ class MainActivity : AppCompatActivity() {
return true return true
} }
private fun startServiceListener() { fun setConnectionStatus(success: Boolean) {
Log.d(TAG, "Start Service Listener") this.runOnUiThread {
setServiceListenerIntent(createPendingResult(100, Intent(), 0)) if (success) {
} Toast.makeText(this, "Garmin connected", Toast.LENGTH_LONG).show()
} else {
fun stopServiceListener() { // TODO: Make snackbar with reconnect option
Log.d(TAG, "Stopping Service Listener") Toast.makeText(this, "Garmin not connected", Toast.LENGTH_LONG).show()
setServiceListenerIntent(null) }
} }
private fun setServiceListenerIntent(pendingIntent: PendingIntent?) {
startService(serviceIntent(SET_INTENT).putExtra(PENDING_INTENT, pendingIntent))
}
private fun checkNotificationsAccess() {
startService(serviceIntent(CHECK_NOTIFICATIONS_ACCESS))
}
private fun serviceIntent(action: String): Intent {
return Intent(applicationContext, NavigationListenerEmitter::class.java).setAction(action)
}
private fun setNavigationData(navData: NavigationData) {
Log.d(TAG, "Sending $navData")
garmin.send(navData)
} }
companion object { companion object {
private const val TAG = "bt" private const val TAG = "bt"
} }

View file

@ -0,0 +1,59 @@
package eu.ztsh.garmin
import android.content.res.Resources
import com.mapbox.maps.EdgeInsets
import eu.ztsh.garmin.databinding.ActivityMainBinding
class UI(b: ActivityMainBinding) {
companion object {
const val BUTTON_ANIMATION_DURATION = 1500L
private val pixelDensity = Resources.getSystem().displayMetrics.density
val overviewPadding: EdgeInsets by lazy {
EdgeInsets(
140.0 * pixelDensity,
40.0 * pixelDensity,
120.0 * pixelDensity,
40.0 * pixelDensity
)
}
val landscapeOverviewPadding: EdgeInsets by lazy {
EdgeInsets(
30.0 * pixelDensity,
380.0 * pixelDensity,
110.0 * pixelDensity,
20.0 * pixelDensity
)
}
val followingPadding: EdgeInsets by lazy {
EdgeInsets(
180.0 * pixelDensity,
40.0 * pixelDensity,
150.0 * pixelDensity,
40.0 * pixelDensity
)
}
val landscapeFollowingPadding: EdgeInsets by lazy {
EdgeInsets(
30.0 * pixelDensity,
380.0 * pixelDensity,
110.0 * pixelDensity,
40.0 * pixelDensity
)
}
}
val mapView = b.mapView
val maneuverView = b.maneuverView
val tripProgressView = b.tripProgressView
val tripProgressCard = b.tripProgressCard
val recenter = b.recenter
val soundButton = b.soundButton
val routeOverview = b.routeOverview
val stop = b.stop
}

View file

@ -1,310 +0,0 @@
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
}
}

View file

@ -0,0 +1,63 @@
package eu.ztsh.garmin.data
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.NavigationSessionState
import com.mapbox.navigation.tripdata.maneuver.model.Lane
import com.mapbox.navigation.tripdata.maneuver.model.Maneuver
import com.mapbox.navigation.tripdata.maneuver.model.PrimaryManeuver
import com.mapbox.navigation.tripdata.maneuver.model.StepDistance
class DataCache {
private var maneuverCache: Maneuver? = null
private var locationCache: LocationMatcherResult? = null
private var session: NavigationSessionState? = null
private var eta: Arrival? = null
// maneuver
fun hasChanged(guidance: Lane?): Boolean {
return guidance != null && maneuverCache.let { it == null || it.laneGuidance != guidance }
}
fun hasChanged(distance: StepDistance): Boolean {
return maneuverCache.let { it == null || it.stepDistance != distance }
}
fun hasChanged(primaryManeuver: PrimaryManeuver): Boolean {
return maneuverCache.let { it == null || it.primary != primaryManeuver }
}
fun update(maneuver: Maneuver) {
maneuverCache = maneuver
}
// eta
fun hasChanged(eta: Arrival): Boolean {
return this.eta.let { it == null || it != eta }
}
fun update(eta: Arrival) {
this.eta = eta
}
// location
fun hasChanged(locationMatcherResult: LocationMatcherResult): Boolean {
return locationCache.let { it == null
|| it.enhancedLocation.speed != locationMatcherResult.enhancedLocation.speed
|| it.speedLimitInfo.speed != locationMatcherResult.speedLimitInfo.speed }
}
fun update(locationMatcherResult: LocationMatcherResult) {
locationCache = locationMatcherResult
}
// session
fun isActive(): Boolean {
return session != null && session is NavigationSessionState.ActiveGuidance
}
fun update(sessionState: NavigationSessionState) {
session = sessionState
}
}

View file

@ -0,0 +1,152 @@
package eu.ztsh.garmin.data
class GarminMapper {
companion object {
fun map(lanes: Lanes): IntArray {
return intArrayOf(0x02, lanes.lanes.lanes.sumOf { it.value }, lanes.outlines.lanes.sumOf { it.value })
}
fun map(direction: Direction): IntArray {
return toDirectionArray(direction.angle, direction.out, direction.roundabout)
}
fun map(distance: Distance): IntArray {
return setDistance(distance.distance, distance.unit)
}
fun map(speed: Speed): IntArray {
return setSpeed(speed.speed, speed.limit)
}
fun setTime(hours: Int, minutes: Int, traffic: Boolean = false, flag: Boolean = false): IntArray {
val trafficChar = asChar(traffic)
val flagChar = asChar(flag)
// TODO: needed? Displaying ETA, not remaining
return if (hours > 99) {
intArrayOf(
0x05,
trafficChar,
asDigit(hours / 1000),
asDigit(hours / 100),
0x00,
asDigit(hours / 10),
asDigit(hours),
0xff,
flagChar
)
} else {
intArrayOf(
0x05,
trafficChar,
asDigit(hours / 10),
asDigit(if (hours == 0) 10 else hours),
0xff,
asDigit(minutes / 10),
asDigit(minutes),
0x00,
flagChar
)
}
}
fun speedControl(state: Boolean): IntArray {
return intArrayOf(0x04, if (state) 0x01 else 0x02)
}
fun cleanDistance(): IntArray {
return intArrayOf(0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
}
private fun toDirectionArray(
angle: OutAngle,
out: OutType = OutType.Lane,
roundabout: OutAngle = OutAngle.AsDirection
): IntArray {
val param1: Int = when (angle) {
OutAngle.LeftDown -> 0x10
OutAngle.RightDown -> 0x20
else -> out.value
}
val param2: Int = if (out == OutType.RightRoundabout
|| out == OutType.LeftRoundabout
) {
if (roundabout == OutAngle.AsDirection) angle.value else roundabout.value
} else {
0x00
}
val param3: Int =
if (angle == OutAngle.LeftDown || angle == OutAngle.RightDown) 0x00 else angle.value
return intArrayOf(0x01, param1, param2, param3)
}
private fun setDistance(distance: Double, unit: Unit = Unit.Any): IntArray {
val isDecimal = (distance * 10).toInt() != (distance.toInt() * 10)
val distanceValue = if (isDecimal) distance * 10 else distance
return intArrayOf(
0x03,
asDigit(distanceValue / 1000), // position 1
asDigit(distanceValue / 100), // position 2
asDigit(distanceValue / 10), // position 3
if (isDecimal) 0xff else 0x00, // comma
asDigit(distanceValue), // position 4
unit.value // unit
)
}
private fun setSpeed(
speed: Int,
limit: Int = 0,
acc: Boolean = false
): IntArray {
// TODO: car connection
val accChar = asChar(acc)
return if (limit > 0) {
intArrayOf(
0x06,
asDigit(speed / 100),
asDigit(speed / 10),
asDigit(if (speed == 0) 10 else speed),
0xff,
asDigit(limit / 100),
asDigit(limit / 10),
asDigit(limit),
asChar(speed > limit),
accChar
)
} else {
intArrayOf(
0x06,
0x00,
0x00,
0x00,
0x00,
asDigit(speed / 100),
asDigit(speed / 10),
asDigit(speed),
asChar(false),
accChar
)
}
}
private fun asDigit(n: Double): Int {
return asDigit(n.toInt())
}
private fun asDigit(n: Int): Int {
if (n == 0) {
return 0
}
val m = n % 10
return if (m == 0) 10 else m
}
private fun asChar(boolean: Boolean): Int {
return if (boolean) 0xff else 0x00
}
}
}

View file

@ -0,0 +1,124 @@
package eu.ztsh.garmin.data
import com.mapbox.navigation.tripdata.maneuver.model.Maneuver
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue
class MapboxMapper {
companion object {
fun asDirection(maneuver: Maneuver): Direction {
val direction = Direction()
maneuver.primary.apply {
when (this.type) {
"roundabout" -> {
direction.out = OutType.RightRoundabout
}
"arrive" -> {
direction.out = OutType.Flag
direction.angle = OutAngle.Right
}
"turn" -> {
when (this.modifier) {
"straight" -> direction.angle = OutAngle.Straight
"slight right" -> direction.angle = OutAngle.EasyRight
"slight left" -> direction.angle = OutAngle.EasyLeft
"sharp right" -> direction.angle = OutAngle.SharpRight
"sharp left" -> direction.angle = OutAngle.SharpLeft
"uturn" -> direction.angle = OutAngle.LeftDown
}
}
}
when (this.modifier) {
"right" -> {
direction.angle = OutAngle.Right
when (this.type) {
"roundabout" -> {
when (this.degrees!!.toInt()) {
in 22..66 -> direction.angle = OutAngle.SharpRight
in 67..111 -> direction.angle = OutAngle.Right
in 112..156 -> direction.angle = OutAngle.EasyRight
in 157..203 -> direction.angle = OutAngle.Straight
in 204..248 -> direction.angle = OutAngle.EasyLeft
in 249..293 -> direction.angle = OutAngle.Left
in 294..338 -> direction.angle = OutAngle.SharpLeft
else -> direction.angle = OutAngle.Down
}
}
"fork", "off ramp" -> {
direction.angle = OutAngle.EasyRight
direction.out = OutType.LongerLane
}
}
}
"left" -> {
direction.angle = OutAngle.Left
when (this.type) {
"fork", "off ramp" -> {
direction.angle = OutAngle.EasyLeft
direction.out = OutType.LongerLane
}
}
}
}
}
return direction
}
fun asDistance(maneuver: Maneuver): Distance {
return maneuver.stepDistance.let { step ->
step.distanceFormatter.formatDistance(step.distanceRemaining!!).split(" ").let {
Distance(
it[0].replace(',', '.').toDouble(),
when (it[1]) {
"m" -> Unit.Metres
"km" -> Unit.Kilometres
else -> Unit.Any
}
)
}
}
}
fun asLanes(maneuver: Maneuver): Lanes {
val laneIterator = Lane.iterator()
val outlines = mutableSetOf<Lane>()
val lanes = mutableSetOf<Lane>()
maneuver.laneGuidance?.apply {
this.allLanes.reversed().let {
it.forEach{ indicator ->
val lane = if (laneIterator.hasNext()) laneIterator.next() else Lane.DotsLeft
if (lane == Lane.DotsLeft) {
outlines.add(Lane.DotsLeft)
} else {
outlines.add(lane)
if (indicator.isActive) {
lanes.add(lane)
}
}
}
}
}
return Lanes(Arrows(lanes), Arrows(outlines))
}
fun asSpeed(locationMatcherResult: LocationMatcherResult): Speed {
return Speed(
locationMatcherResult.enhancedLocation.speed.let { it?.toInt() ?: 0 },
locationMatcherResult.speedLimitInfo.speed.let { it ?: 0 },
)
}
fun asEta(trip: TripProgressUpdateValue): Arrival {
val eta = trip.formatter
.getEstimatedTimeToArrival(trip.estimatedTimeToArrival)
.toString().split(":")
return Arrival(eta[0].toInt(), eta[1].toInt())
}
}
}

View file

@ -0,0 +1,149 @@
package eu.ztsh.garmin.data
enum class OutType(val value: Int) {
Off(0x00),
Lane(0x01),
LongerLane(0x02),
LeftRoundabout(0x04),
RightRoundabout(0x08),
Flag(0x40),
ArrowOnly(0x80);
}
enum class OutAngle(val value: 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 value: Int) {
Any(0),
Metres(1),
Kilometres(3),
Miles(5),
Foot(8)
}
enum class Lane(val value: Int) {
DotsRight(0x01),
OuterRight(0x02),
MiddleRight(0x04),
InnerRight(0x08),
InnerLeft(0x10),
MiddleLeft(0x20),
OuterLeft(0x40),
DotsLeft(0x80);
companion object {
val iterator = {sortedSetOf(OuterRight, MiddleRight, InnerRight, InnerLeft, MiddleLeft, OuterLeft).iterator()}
}
}
class Arrows(val lanes: Set<Lane>) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Arrows
return lanes == other.lanes
}
override fun hashCode(): Int {
return lanes.hashCode()
}
}
class Lanes(val outlines: Arrows, val lanes: Arrows)
class Distance(val distance: Double, val unit: Unit) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Distance
if (distance != other.distance) return false
if (unit != other.unit) return false
return true
}
override fun hashCode(): Int {
var result = distance.hashCode()
result = 31 * result + unit.hashCode()
return result
}
override fun toString(): String {
return "Distance($distance$unit)"
}
}
class Speed(val speed: Int, val limit: Int)
class Arrival(val hours: Int, val minutes: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Arrival
if (hours != other.hours) return false
if (minutes != other.minutes) return false
return true
}
override fun hashCode(): Int {
return 61 * hours + minutes
}
}
class Direction(
var angle: OutAngle = OutAngle.AsDirection,
var out: 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 (angle != other.angle) return false
if (out != other.out) return false
if (roundabout != other.roundabout) return false
return true
}
override fun hashCode(): Int {
var result = angle.hashCode()
result = 31 * result + out.hashCode()
result = 31 * result + roundabout.hashCode()
return result
}
}

View file

@ -0,0 +1,49 @@
package eu.ztsh.garmin.mapbox
import com.mapbox.common.location.Location
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
import eu.ztsh.garmin.Garmin
class LocationObserver(private val mapControl: MapControl) : LocationObserver {
/**
* Gets notified with location updates.
*
* Exposes raw updates coming directly from the location services
* and the updates enhanced by the Navigation SDK (cleaned up and matched to the road).
*/
private var firstLocationUpdateReceived = false
override fun onNewRawLocation(rawLocation: Location) {
// not handled
}
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
val enhancedLocation = locationMatcherResult.enhancedLocation
// update location puck's position on the map
mapControl.navigationLocationProvider.changePosition(
location = enhancedLocation,
keyPoints = locationMatcherResult.keyPoints,
)
// update camera position to account for new location
mapControl.viewportDataSource.onLocationChanged(enhancedLocation)
mapControl.viewportDataSource.evaluate()
Garmin.instance.process(locationMatcherResult)
// if this is the first location update the activity has received,
// it's best to immediately move the camera to the current user location
if (!firstLocationUpdateReceived) {
firstLocationUpdateReceived = true
mapControl.navigationCamera.requestNavigationCameraToOverview(
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
.maxDuration(0) // instant transition
.build()
)
}
}
}

View file

@ -0,0 +1,158 @@
package eu.ztsh.garmin.mapbox
import android.content.res.Configuration
import android.content.res.Resources
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.maps.ImageHolder
import com.mapbox.maps.plugin.LocationPuck2D
import com.mapbox.maps.plugin.animation.camera
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
import com.mapbox.navigation.ui.maps.camera.NavigationCamera
import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
import com.mapbox.navigation.ui.maps.camera.lifecycle.NavigationBasicGesturesHandler
import com.mapbox.navigation.ui.maps.camera.state.NavigationCameraState
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
import eu.ztsh.garmin.MainActivity
import eu.ztsh.garmin.UI
import eu.ztsh.garmin.mock.ReplayResources
class MapControl(
val context: AppCompatActivity,
val ui: UI,
private val resources: Resources
) : MapboxNavigationObserver {
/**
* Used to execute camera transitions based on the data generated by the [viewportDataSource].
* This includes transitions from route overview to route following and continuously updating the camera as the location changes.
*/
lateinit var navigationCamera: NavigationCamera
/**
* Produces the camera frames based on the location and routing data for the [navigationCamera] to execute.
*/
lateinit var viewportDataSource: MapboxNavigationViewportDataSource
/**
* [NavigationLocationProvider] is a utility class that helps to provide location updates generated by the Navigation SDK
* to the Maps SDK in order to update the user location indicator on the map.
*/
val navigationLocationProvider = NavigationLocationProvider()
val replay = ReplayResources(this)
// Observers
private lateinit var routeControl: RouteControl
private lateinit var voiceControl: VoiceControl
private lateinit var routesObserver: RoutesObserver
private lateinit var locationObserver: LocationObserver
private lateinit var routeProgressObserver: RouteProgressObserver
private lateinit var voiceInstructionsObserver: VoiceInstructionsObserver
fun init() {
viewportDataSource = MapboxNavigationViewportDataSource(ui.mapView.mapboxMap)
if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
viewportDataSource.overviewPadding = UI.landscapeOverviewPadding
} else {
viewportDataSource.overviewPadding = UI.overviewPadding
}
if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
viewportDataSource.followingPadding = UI.landscapeFollowingPadding
} else {
viewportDataSource.followingPadding = UI.followingPadding
}
navigationCamera = NavigationCamera(
ui.mapView.mapboxMap,
ui.mapView.camera,
viewportDataSource
)
// set the animations lifecycle listener to ensure the NavigationCamera stops
// automatically following the user location when the map is interacted with
ui.mapView.camera.addCameraAnimationsLifecycleListener(
NavigationBasicGesturesHandler(navigationCamera)
)
navigationCamera.registerNavigationCameraStateChangeObserver { navigationCameraState ->
// shows/hide the recenter button depending on the camera state
when (navigationCameraState) {
NavigationCameraState.TRANSITION_TO_FOLLOWING,
NavigationCameraState.FOLLOWING -> ui.recenter.visibility = View.INVISIBLE
NavigationCameraState.TRANSITION_TO_OVERVIEW,
NavigationCameraState.OVERVIEW,
NavigationCameraState.IDLE -> ui.recenter.visibility = View.VISIBLE
}
}
routeControl = RouteControl(this, ui, context)
voiceControl = VoiceControl(ui, context)
routesObserver = routeControl.routesObserver
locationObserver = LocationObserver(this)
routeProgressObserver = routeControl.routeProgressObserver
voiceInstructionsObserver = voiceControl.voiceInstructionsObserver
}
fun initNavigation() {
MapboxNavigationApp.setup(
NavigationOptions.Builder(context)
.build()
)
// initialize location puck
ui.mapView.location.apply {
setLocationProvider(navigationLocationProvider)
this.locationPuck = LocationPuck2D(
bearingImage = ImageHolder.Companion.from(
com.mapbox.navigation.ui.maps.R.drawable.mapbox_navigation_puck_icon
)
)
puckBearingEnabled = true
enabled = true
}
replay.replayOriginLocation()
}
fun mapboxNavigation(): MapboxNavigation {
return (context as MainActivity).mapboxNavigation
}
override fun onAttached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.registerRoutesObserver(routesObserver)
mapboxNavigation.registerLocationObserver(locationObserver)
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
replay.onAttached(mapboxNavigation)
}
override fun onDetached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.unregisterRoutesObserver(routesObserver)
mapboxNavigation.unregisterLocationObserver(locationObserver)
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
replay.onDetached(mapboxNavigation)
}
fun onDestroy() {
routeControl.cancel()
voiceControl.cancel()
}
companion object {
const val TAG = "MAPCTRL"
}
}

View file

@ -0,0 +1,309 @@
package eu.ztsh.garmin.mapbox
import android.content.Context
import android.view.View
import android.widget.Toast
import com.mapbox.api.directions.v5.models.Bearing
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.geojson.Point
import com.mapbox.maps.plugin.gestures.gestures
import com.mapbox.navigation.base.TimeFormat
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.route.NavigationRouterCallback
import com.mapbox.navigation.base.route.RouterFailure
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi
import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi
import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter
import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalFormatter
import com.mapbox.navigation.tripdata.progress.model.PercentDistanceTraveledFormatter
import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter
import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter
import com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
import com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView
import com.mapbox.navigation.ui.maps.NavigationStyles
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView
import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
import eu.ztsh.garmin.Garmin
import eu.ztsh.garmin.UI
class RouteControl(private val mapControl: MapControl, ui: UI, private val context: Context) {
/**
* Generates updates for the [MapboxManeuverView] to display the upcoming maneuver instructions
* and remaining distance to the maneuver point.
*/
private lateinit var maneuverApi: MapboxManeuverApi
/**
* Generates updates for the [MapboxTripProgressView] that include remaining time and distance to the destination.
*/
private lateinit var tripProgressApi: MapboxTripProgressApi
/**
* Generates updates for the [routeLineView] with the geometries and properties of the routes that should be drawn on the map.
*/
private lateinit var routeLineApi: MapboxRouteLineApi
/**
* Draws route lines on the map based on the data from the [routeLineApi]
*/
private lateinit var routeLineView: MapboxRouteLineView
/**
* Generates updates for the [routeArrowView] with the geometries and properties of maneuver arrows that should be drawn on the map.
*/
private val routeArrowApi: MapboxRouteArrowApi = MapboxRouteArrowApi()
/**
* Draws maneuver arrows on the map based on the data [routeArrowApi].
*/
private lateinit var routeArrowView: MapboxRouteArrowView
init {
// make sure to use the same DistanceFormatterOptions across different features
val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
maneuverApi = MapboxManeuverApi(
MapboxDistanceFormatter(distanceFormatterOptions)
)
// initialize bottom progress view
tripProgressApi = MapboxTripProgressApi(
TripProgressUpdateFormatter.Builder(context)
.distanceRemainingFormatter(
DistanceRemainingFormatter(distanceFormatterOptions)
)
.timeRemainingFormatter(
TimeRemainingFormatter(context)
)
.percentRouteTraveledFormatter(
PercentDistanceTraveledFormatter()
)
.estimatedTimeToArrivalFormatter(
EstimatedTimeToArrivalFormatter(context, TimeFormat.TWENTY_FOUR_HOURS)
)
.build()
)
// initialize route line, the routeLineBelowLayerId is specified to place
// the route line below road labels layer on the map
// the value of this option will depend on the style that you are using
// and under which layer the route line should be placed on the map layers stack
val mapboxRouteLineViewOptions = MapboxRouteLineViewOptions.Builder(context)
.routeLineBelowLayerId("road-label-navigation")
.build()
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
routeLineView = MapboxRouteLineView(mapboxRouteLineViewOptions)
// initialize maneuver arrow view to draw arrows on the map
val routeArrowOptions = RouteArrowOptions.Builder(context).build()
routeArrowView = MapboxRouteArrowView(routeArrowOptions)
// load map style
ui.mapView.mapboxMap.loadStyle(NavigationStyles.NAVIGATION_DAY_STYLE) {
// Ensure that the route line related layers are present before the route arrow
routeLineView.initializeLayers(it)
// add long click listener that search for a route to the clicked destination
ui.mapView.gestures.addOnMapLongClickListener { point ->
findRoute(point)
true
}
}
// initialize view interactions
ui.stop.setOnClickListener {
clearRouteAndStopNavigation()
}
ui.recenter.setOnClickListener {
mapControl.navigationCamera.requestNavigationCameraToFollowing()
ui.routeOverview.showTextAndExtend(UI.BUTTON_ANIMATION_DURATION)
}
ui.routeOverview.setOnClickListener {
mapControl.navigationCamera.requestNavigationCameraToOverview()
ui.recenter.showTextAndExtend(UI.BUTTON_ANIMATION_DURATION)
}
// set initial sounds button state
ui.soundButton.unmute()
}
/**
* Gets notified with progress along the currently active route.
*/
val routeProgressObserver = RouteProgressObserver { routeProgress ->
// update the camera position to account for the progressed fragment of the route
mapControl.viewportDataSource.onRouteProgressChanged(routeProgress)
mapControl.viewportDataSource.evaluate()
// draw the upcoming maneuver arrow on the map
val style = mapControl.ui.mapView.mapboxMap.style
if (style != null) {
val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
routeArrowView.renderManeuverUpdate(style, maneuverArrowResult)
}
// update top banner with maneuver instructions
val maneuvers = maneuverApi.getManeuvers(routeProgress)
maneuvers.fold(
{ error ->
Toast.makeText(
mapControl.context,
error.errorMessage,
Toast.LENGTH_SHORT
).show()
},
{
mapControl.ui.maneuverView.visibility = View.VISIBLE
mapControl.ui.maneuverView.renderManeuvers(maneuvers)
Garmin.instance.process(it[0])
}
)
// update bottom trip progress summary and send to HUD
tripProgressApi.getTripProgress(routeProgress).let {
mapControl.ui.tripProgressView.render(it)
Garmin.instance.process(it)
}
}
/**
* Gets notified whenever the tracked routes change.
*
* A change can mean:
* - routes get changed with [MapboxNavigation.setNavigationRoutes]
* - routes annotations get refreshed (for example, congestion annotation that indicate the live traffic along the route)
* - driver got off route and a reroute was executed
*/
val routesObserver = RoutesObserver { routeUpdateResult ->
if (routeUpdateResult.navigationRoutes.isNotEmpty()) {
// generate route geometries asynchronously and render them
routeLineApi.setNavigationRoutes(
routeUpdateResult.navigationRoutes
) { value ->
mapControl.ui.mapView.mapboxMap.style?.apply {
routeLineView.renderRouteDrawData(this, value)
}
}
// update the camera position to account for the new route
mapControl.viewportDataSource.onRouteChanged(routeUpdateResult.navigationRoutes.first())
mapControl.viewportDataSource.evaluate()
} else {
// remove the route line and route arrow from the map
val style = mapControl.ui.mapView.mapboxMap.style
if (style != null) {
routeLineApi.clearRouteLine { value ->
routeLineView.renderClearRouteLineValue(
style,
value
)
}
routeArrowView.render(style, routeArrowApi.clearArrows())
}
// remove the route reference from camera position evaluations
mapControl.viewportDataSource.clearRouteData()
mapControl.viewportDataSource.evaluate()
}
}
private fun findRoute(destination: Point) {
val originLocation = mapControl.navigationLocationProvider.lastLocation ?: return
val originPoint = Point.fromLngLat(originLocation.longitude, originLocation.latitude)
// execute a route request
// it's recommended to use the
// applyDefaultNavigationOptions and applyLanguageAndVoiceUnitOptions
// that make sure the route request is optimized
// to allow for support of all of the Navigation SDK features
mapControl.mapboxNavigation().requestRoutes(
RouteOptions.builder()
.applyDefaultNavigationOptions()
.applyLanguageAndVoiceUnitOptions(context)
.coordinatesList(listOf(originPoint, destination))
.apply {
// provide the bearing for the origin of the request to ensure
// that the returned route faces in the direction of the current user movement
originLocation.bearing?.let { bearing ->
bearingsList(
listOf(
Bearing.builder()
.angle(bearing)
.degrees(45.0)
.build(),
null
)
)
}
}
.layersList(listOf(mapControl.mapboxNavigation().getZLevel(), null))
.build(),
object : NavigationRouterCallback {
override fun onCanceled(routeOptions: RouteOptions, routerOrigin: String) {
// no impl
}
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
// no impl
}
override fun onRoutesReady(
routes: List<NavigationRoute>,
routerOrigin: String
) {
setRouteAndStartNavigation(routes)
}
}
)
}
private fun setRouteAndStartNavigation(routes: List<NavigationRoute>) {
// set routes, where the first route in the list is the primary route that
// will be used for active guidance
mapControl.mapboxNavigation().setNavigationRoutes(routes)
// show UI elements
mapControl.ui.soundButton.visibility = View.VISIBLE
mapControl.ui.routeOverview.visibility = View.VISIBLE
mapControl.ui.tripProgressCard.visibility = View.VISIBLE
// move the camera to overview when new route is available
mapControl.navigationCamera.requestNavigationCameraToOverview()
// start simulation
mapControl.replay.startSimulation(routes.first().directionsRoute)
}
private fun clearRouteAndStopNavigation() {
// clear
mapControl.mapboxNavigation().setNavigationRoutes(listOf())
// stop simulation
mapControl.replay.stopSimulation()
// hide UI elements
mapControl.ui.soundButton.visibility = View.INVISIBLE
mapControl.ui.maneuverView.visibility = View.INVISIBLE
mapControl.ui.routeOverview.visibility = View.INVISIBLE
mapControl.ui.tripProgressCard.visibility = View.INVISIBLE
}
fun cancel() {
maneuverApi.cancel()
routeLineApi.cancel()
routeLineView.cancel()
}
}

View file

@ -0,0 +1,111 @@
package eu.ztsh.garmin.mapbox
import android.content.Context
import com.mapbox.bindgen.Expected
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer
import com.mapbox.navigation.voice.api.MapboxSpeechApi
import com.mapbox.navigation.voice.api.MapboxVoiceInstructionsPlayer
import com.mapbox.navigation.voice.model.SpeechAnnouncement
import com.mapbox.navigation.voice.model.SpeechError
import com.mapbox.navigation.voice.model.SpeechValue
import com.mapbox.navigation.voice.model.SpeechVolume
import eu.ztsh.garmin.UI
import java.util.*
class VoiceControl(private val ui: UI, context: Context) {
/**
* Extracts message that should be communicated to the driver about the upcoming maneuver.
* When possible, downloads a synthesized audio file that can be played back to the driver.
*/
private lateinit var speechApi: MapboxSpeechApi
/**
* Plays the synthesized audio files with upcoming maneuver instructions
* or uses an on-device Text-To-Speech engine to communicate the message to the driver.
* NOTE: do not use lazy initialization for this class since it takes some time to initialize
* the system services required for on-device speech synthesis. With lazy initialization
* there is a high risk that said services will not be available when the first instruction
* has to be played. [MapboxVoiceInstructionsPlayer] should be instantiated in
* `Activity#onCreate`.
*/
private lateinit var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer
init {
speechApi = MapboxSpeechApi(
context,
Locale("pl").language
)
voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer(
context,
Locale("pl").language
)
ui.soundButton.setOnClickListener {
// mute/unmute voice instructions
isVoiceInstructionsMuted = !isVoiceInstructionsMuted
}
}
/**
* Stores and updates the state of whether the voice instructions should be played as they come or muted.
*/
private var isVoiceInstructionsMuted = false
set(value) {
field = value
if (value) {
ui.soundButton.muteAndExtend(UI.BUTTON_ANIMATION_DURATION)
voiceInstructionsPlayer.volume(SpeechVolume(0f))
} else {
ui.soundButton.unmuteAndExtend(UI.BUTTON_ANIMATION_DURATION)
voiceInstructionsPlayer.volume(SpeechVolume(1f))
}
}
/**
* Observes when a new voice instruction should be played.
*/
val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions ->
speechApi.generate(voiceInstructions, speechCallback)
}
/**
* Based on whether the synthesized audio file is available, the callback plays the file
* or uses the fall back which is played back using the on-device Text-To-Speech engine.
*/
private val speechCallback =
MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>> { expected ->
expected.fold(
{ error ->
// play the instruction via fallback text-to-speech engine
voiceInstructionsPlayer.play(
error.fallback,
voiceInstructionsPlayerCallback
)
},
{ value ->
// play the sound file from the external generator
voiceInstructionsPlayer.play(
value.announcement,
voiceInstructionsPlayerCallback
)
}
)
}
/**
* When a synthesized audio file was downloaded, this callback cleans up the disk after it was played.
*/
private val voiceInstructionsPlayerCallback =
MapboxNavigationConsumer<SpeechAnnouncement> { value ->
// remove already consumed file to free-up space
speechApi.clean(value)
}
fun cancel() {
speechApi.cancel()
voiceInstructionsPlayer.shutdown()
}
}

View file

@ -0,0 +1,73 @@
package eu.ztsh.garmin.mock
import com.mapbox.api.directions.v5.models.DirectionsRoute
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.replay.route.ReplayProgressObserver
import com.mapbox.navigation.core.replay.route.ReplayRouteMapper
import eu.ztsh.garmin.mapbox.MapControl
import java.util.*
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class ReplayResources(private val mapControl: MapControl) {
/**
* Debug observer that makes sure the replayer has always an up-to-date information to generate mock updates.
*/
private lateinit var replayProgressObserver: ReplayProgressObserver
/**
* Debug object that converts a route into events that can be replayed to navigate a route.
*/
private val replayRouteMapper = ReplayRouteMapper()
fun replayOriginLocation() {
with(mapControl.mapboxNavigation().mapboxReplayer) {
play()
pushEvents(
listOf(
ReplayRouteMapper.mapToUpdateLocation(
Date().time.toDouble(),
com.mapbox.geojson.Point.fromLngLat(18.531478, 50.088155)
)
)
)
playFirstLocation()
}
}
fun startSimulation(route: DirectionsRoute) {
with(mapControl.mapboxNavigation()) {
mapboxReplayer.stop()
mapboxReplayer.clearEvents()
val replayData = replayRouteMapper.mapDirectionsRouteGeometry(route)
mapboxReplayer.pushEvents(replayData)
mapboxReplayer.seekTo(replayData[0])
mapboxReplayer.play()
}
}
fun stopSimulation() {
with(mapControl.mapboxNavigation()) {
mapboxReplayer.stop()
mapboxReplayer.clearEvents()
}
}
fun onAttached(mapboxNavigation: MapboxNavigation) {
replayProgressObserver = ReplayProgressObserver(mapboxNavigation.mapboxReplayer)
mapboxNavigation.registerRouteProgressObserver(replayProgressObserver)
// Start the trip session to being receiving location updates in free drive
// and later when a route is set also receiving route progress updates.
// In case of `startReplayTripSession`,
// location events are emitted by the `MapboxReplayer`
mapboxNavigation.startReplayTripSession()
}
fun onDetached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver)
mapboxNavigation.mapboxReplayer.finish()
}
}

View file

@ -0,0 +1,50 @@
package eu.ztsh.garmin.util
import android.app.Activity
import android.widget.Toast
import com.mapbox.android.core.permissions.PermissionsListener
import com.mapbox.android.core.permissions.PermissionsManager
import java.lang.ref.WeakReference
class PermissionsHelper(val activityRef: WeakReference<Activity>) {
private lateinit var permissionsManager: PermissionsManager
fun checkPermissions(onMapReady: () -> Unit) {
activityRef.get()?.let { activity: Activity ->
if (PermissionsManager.areLocationPermissionsGranted(activity)) {
onMapReady()
} else {
permissionsManager = PermissionsManager(object : PermissionsListener {
override fun onExplanationNeeded(permissionsToExplain: List<String>) {
activityRef.get()?.let {
Toast.makeText(
it, "You need to accept location permissions.",
Toast.LENGTH_SHORT
).show()
}
}
override fun onPermissionResult(granted: Boolean) {
activityRef.get()?.let {
if (granted) {
onMapReady()
} else {
it.finish()
}
}
}
})
permissionsManager.requestLocationPermissions(activity)
}
}
}
fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}

View file

@ -1,19 +1,80 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="match_parent">
android:layout_width="match_parent"
android:layout_height="match_parent" <com.mapbox.maps.MapView
tools:context=".MainActivity"> android:id="@+id/mapView"
<androidx.appcompat.widget.SwitchCompat android:layout_width="0dp"
android:text="Enabled" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/tripProgressCard"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:cardElevation="8dp"
app:cardUseCompatPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView
android:id="@+id/tripProgressView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/stop"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="12dp"
app:srcCompat="@android:drawable/ic_delete" />
</androidx.cardview.widget.CardView>
<com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
android:id="@+id/maneuverView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.mapbox.navigation.ui.components.voice.view.MapboxSoundButton
android:id="@+id/soundButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/enabled_switch" android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginEnd="16dp"
app:layout_constraintTop_toTopOf="parent" android:visibility="invisible"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintTop_toBottomOf="@id/maneuverView" />
</androidx.constraintlayout.widget.ConstraintLayout> <com.mapbox.navigation.ui.components.maps.camera.view.MapboxRouteOverviewButton
android:id="@+id/routeOverview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/soundButton" />
<com.mapbox.navigation.ui.components.maps.camera.view.MapboxRecenterButton
android:id="@+id/recenter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/routeOverview" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item> <item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>

View file

@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">Garmin</string> <string name="app_name">Garmin</string>
</resources> <string name="mapbox_access_token">pk.eyJ1IjoibWFwcy16dHNoIiwiYSI6ImNsbDl4YXU4cjA3eW8zcXMzbXdjYjNsN3oifQ.kbDCjthamXvXX_pAdsq3hQ</string>
</resources>

View file

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item> <item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>

View file

@ -0,0 +1,275 @@
package eu.ztsh.garmin.data
import eu.ztsh.garmin.Garmin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class GarminMapperTest {
@Test
fun linesTest() {
linesTest(
setOf(Lane.DotsLeft),
setOf(),
intArrayOf(2, 128, 0),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 128, 0, 141, 16, 3)
)
linesTest(
Lane.OuterRight,
Lane.OuterLeft,
intArrayOf(2, 2, 64),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 2, 64, 203, 16, 3)
)
linesTest(
Lane.MiddleRight,
Lane.MiddleLeft,
intArrayOf(2, 4, 32),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 4, 32, 233, 16, 3)
)
linesTest(
Lane.InnerRight,
Lane.InnerLeft,
intArrayOf(2, 8, 16),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 8, 16, 16, 245, 16, 3)
)
linesTest(
Lane.InnerLeft,
Lane.InnerRight,
intArrayOf(2, 16, 8),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 16, 16, 8, 245, 16, 3)
)
linesTest(
Lane.MiddleLeft,
Lane.MiddleRight,
intArrayOf(2, 32, 4),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 32, 4, 233, 16, 3)
)
linesTest(
Lane.OuterLeft,
Lane.OuterRight,
intArrayOf(2, 64, 2),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 64, 2, 203, 16, 3)
)
linesTest(
setOf(Lane.DotsRight),
setOf(Lane.OuterRight, Lane.MiddleRight, Lane.InnerRight, Lane.InnerLeft, Lane.MiddleLeft, Lane.OuterLeft),
intArrayOf(2, 1, 126),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 1, 126, 142, 16, 3)
)
linesTest(
setOf(),
setOf(),
intArrayOf(2, 0, 0),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 0, 0, 13, 16, 3)
)
}
@Test
fun directionTest() {
directionTest(
OutAngle.RightDown,
intArrayOf(1, 32, 0, 0),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 32, 0, 0, 236, 16, 3)
)
directionTest(
OutAngle.SharpRight,
intArrayOf(1, 1, 0, 2),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 2, 9, 16, 3)
)
directionTest(
OutAngle.Right,
intArrayOf(1, 1, 0, 4),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 4, 7, 16, 3)
)
directionTest(
OutAngle.EasyRight,
intArrayOf(1, 1, 0, 8),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 8, 3, 16, 3)
)
directionTest(
OutAngle.Straight,
intArrayOf(1, 1, 0, 16),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 16, 16, 251, 16, 3)
)
directionTest(
OutAngle.EasyLeft,
intArrayOf(1, 1, 0, 32),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 32, 235, 16, 3)
)
directionTest(
OutAngle.Left,
intArrayOf(1, 1, 0, 64),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 64, 203, 16, 3)
)
directionTest(
OutAngle.SharpLeft,
intArrayOf(1, 1, 0, 128),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 128, 139, 16, 3)
)
directionTest(
OutAngle.LeftDown,
intArrayOf(1, 16, 0, 0),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 16, 16, 0, 0, 252, 16, 3)
)
directionTest(
OutAngle.Down,
intArrayOf(1, 1, 0, 1),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 1, 10, 16, 3)
)
directionTest(
OutAngle.SharpRight, OutType.LongerLane,
intArrayOf(1, 2, 0, 2),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 2, 8, 16, 3)
)
directionTest(
OutAngle.Right, OutType.LongerLane,
intArrayOf(1, 2, 0, 4),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 4, 6, 16, 3)
)
directionTest(
OutAngle.EasyRight, OutType.LongerLane,
intArrayOf(1, 2, 0, 8),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 8, 2, 16, 3)
)
directionTest(
OutAngle.Straight, OutType.LongerLane,
intArrayOf(1, 2, 0, 16),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 16, 16, 250, 16, 3)
)
directionTest(
OutAngle.EasyLeft, OutType.LongerLane,
intArrayOf(1, 2, 0, 32),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 32, 234, 16, 3)
)
directionTest(
OutAngle.Left, OutType.LongerLane,
intArrayOf(1, 2, 0, 64),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 64, 202, 16, 3)
)
directionTest(
OutAngle.SharpLeft, OutType.LongerLane,
intArrayOf(1, 2, 0, 128),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 128, 138, 16, 3)
)
directionTest(
OutAngle.SharpRight, OutType.RightRoundabout,
intArrayOf(1, 8, 2, 2),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 2, 2, 0, 16, 3)
)
directionTest(
OutAngle.Right, OutType.RightRoundabout,
intArrayOf(1, 8, 4, 4),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 4, 4, 252, 16, 3)
)
directionTest(
OutAngle.EasyRight, OutType.RightRoundabout,
intArrayOf(1, 8, 8, 8),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 8, 8, 244, 16, 3)
)
directionTest(
OutAngle.Straight, OutType.RightRoundabout,
intArrayOf(1, 8, 16, 16),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 16, 16, 16, 16, 228, 16, 3)
)
directionTest(
OutAngle.EasyLeft, OutType.RightRoundabout,
intArrayOf(1, 8, 32, 32),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 32, 32, 196, 16, 3)
)
directionTest(
OutAngle.Left, OutType.RightRoundabout,
intArrayOf(1, 8, 64, 64),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 64, 64, 132, 16, 3)
)
directionTest(
OutAngle.SharpLeft, OutType.RightRoundabout,
intArrayOf(1, 8, 128, 128),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 128, 128, 4, 16, 3)
)
directionTest(
OutAngle.Left, OutType.Flag,
intArrayOf(1, 64, 0, 64),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 64, 0, 64, 140, 16, 3)
)
directionTest(
OutAngle.Right, OutType.Flag,
intArrayOf(1, 64, 0, 4),
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 64, 0, 4, 200, 16, 3)
)
}
@Test
fun distanceTest() {
// TODO
intArrayOf(3, 0, 9, 9, 0, 9, 0)
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 0, 233, 16, 3)
intArrayOf(3, 0, 9, 9, 0, 9, 3)
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 3, 230, 16, 3)
intArrayOf(3, 0, 9, 9, 0, 9, 1)
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 1, 232, 16, 3)
intArrayOf(3, 0, 9, 9, 0, 9, 8)
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 8, 225, 16, 3)
intArrayOf(3, 0, 9, 9, 0, 9, 5)
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 5, 228, 16, 3)
}
@Test
fun speedTest() {
// TODO
intArrayOf(7, 1)
intArrayOf(16, 123, 8, 2, 0, 0, 0, 85, 21, 7, 1, 9, 16, 3)
intArrayOf(6, 0, 0, 0, 0, 0, 5, 10, 0, 0)
intArrayOf(16, 123, 16, 16, 10, 0, 0, 0, 85, 21, 6, 0, 0, 0, 0, 0, 5, 10, 0, 0, 236, 16, 3)
intArrayOf(6, 0, 5, 10, 255, 1, 10, 10, 0, 0)
intArrayOf(16, 123, 16, 16, 10, 0, 0, 0, 85, 21, 6, 0, 5, 10, 255, 1, 10, 10, 0, 0, 216, 16, 3)
intArrayOf(6, 1, 5, 10, 255, 1, 10, 10, 255, 0)
intArrayOf(16, 123, 16, 16, 10, 0, 0, 0, 85, 21, 6, 1, 5, 10, 255, 1, 10, 10, 255, 0, 216, 16, 3)
intArrayOf(6, 0, 5, 10, 255, 1, 10, 10, 0, 255)
intArrayOf(16, 123, 16, 16, 10, 0, 0, 0, 85, 21, 6, 0, 5, 10, 255, 1, 10, 10, 0, 255, 217, 16, 3)
}
@Test
fun timeTest() {
// TODO
intArrayOf(5, 0, 2, 2, 255, 2, 2, 0, 0)
intArrayOf(16, 123, 15, 9, 0, 0, 0, 85, 21, 5, 0, 2, 2, 255, 2, 2, 0, 0, 247, 16, 3)
intArrayOf(5, 255, 2, 2, 255, 2, 2, 0, 0)
intArrayOf(16, 123, 15, 9, 0, 0, 0, 85, 21, 5, 255, 2, 2, 255, 2, 2, 0, 0, 248, 16, 3)
intArrayOf(5, 0, 2, 2, 255, 2, 2, 0, 255)
intArrayOf(16, 123, 15, 9, 0, 0, 0, 85, 21, 5, 0, 2, 2, 255, 2, 2, 0, 255, 248, 16, 3)
}
@Test
fun controlTest() {
// TODO
intArrayOf(4, 1)
intArrayOf(16, 123, 8, 2, 0, 0, 0, 85, 21, 4, 1, 12, 16, 3)
intArrayOf(4, 0)
intArrayOf(16, 123, 8, 2, 0, 0, 0, 85, 21, 4, 0, 13, 16, 3)
}
private fun linesTest(outlines: Lane, arrows: Lane, expectedRaw: IntArray, expectedBoxed: IntArray) {
linesTest(setOf(outlines), setOf(arrows), expectedRaw, expectedBoxed)
}
private fun linesTest(outlines: Set<Lane>, arrows: Set<Lane>, expectedRaw: IntArray, expectedBoxed: IntArray) {
val lanes = Lanes(Arrows(arrows), Arrows(outlines))
makeAssertions(GarminMapper.map(lanes), expectedRaw, expectedBoxed)
}
private fun directionTest(outAngle: OutAngle, expectedRaw: IntArray, expectedBoxed: IntArray) {
directionTest(outAngle, OutType.Lane, expectedRaw, expectedBoxed)
}
private fun directionTest(outAngle: OutAngle, outType: OutType, expectedRaw: IntArray, expectedBoxed: IntArray) {
val direction = Direction(outAngle, outType)
makeAssertions(GarminMapper.map(direction), expectedRaw, expectedBoxed)
}
private fun makeAssertions(resultRaw: IntArray, expectedRaw: IntArray, expectedBoxed: IntArray) {
assertThat(resultRaw).containsExactly(expectedRaw.toTypedArray())
val resultBoxed = Garmin.prepareData(resultRaw)
assertThat(resultBoxed).containsExactly(expectedBoxed.toTypedArray())
}
}

View file

@ -5,7 +5,7 @@ buildscript {
} }
} }
plugins { plugins {
id 'com.android.application' version '7.4.1' apply false id 'com.android.application' version '8.2.0' apply false
id 'com.android.library' version '7.4.1' apply false id 'com.android.library' version '8.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false id 'org.jetbrains.kotlin.android' version '1.9.24' apply false
} }

View file

@ -11,7 +11,7 @@
# 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
#Thu Jun 22 21:51:56 CEST 2023 #Thu Jun 22 21:51:56 CEST 2023
android.enableJetifier=true android.enableJetifier=false
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.useAndroidX=true android.useAndroidX=true
kotlin.code.style=official kotlin.code.style=official

View file

@ -1,6 +1,6 @@
#Thu Jun 22 21:00:17 CEST 2023 #Thu Jun 22 21:00:17 CEST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

178
gradlew.bat vendored
View file

@ -1,89 +1,89 @@
@rem @rem
@rem Copyright 2015 the original author or authors. @rem Copyright 2015 the original author or authors.
@rem @rem
@rem Licensed under the Apache License, Version 2.0 (the "License"); @rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License. @rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at @rem You may obtain a copy of the License at
@rem @rem
@rem https://www.apache.org/licenses/LICENSE-2.0 @rem https://www.apache.org/licenses/LICENSE-2.0
@rem @rem
@rem Unless required by applicable law or agreed to in writing, software @rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS, @rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter. @rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. echo.
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. echo location of your Java installation.
goto fail goto fail
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. echo.
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. echo location of your Java installation.
goto fail goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if "%ERRORLEVEL%"=="0" goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1 exit /b 1
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal
:omega :omega

200
python/main.py Normal file
View file

@ -0,0 +1,200 @@
import time
from enum import Enum
import serial
from numpy import uint8
gps = False
class OutType(Enum):
Off = 0x00
Lane = 0x01
LongerLane = 0x02
LeftRoundabout = 0x04
RightRoundabout = 0x08
Flag = 0x40
ArrowOnly = 0x80
class OutAngle(Enum):
Down = 0x01
SharpRight = 0x02
Right = 0x04
EasyRight = 0x08
Straight = 0x10
EasyLeft = 0x20
Left = 0x40
SharpLeft = 0x80
LeftDown = 0x81
RightDown = 0x82
AsDirection = 0x00
class Unit(Enum):
Any = 0
Metres = 1
Kilometres = 3
Miles = 5
Foot = 8
class Lane(Enum):
DotsRight = 0x01
OuterRight = 0x02
MiddleRight = 0x04
InnerRight = 0x08
InnerLeft = 0x10
MiddleLeft = 0x20
OuterLeft = 0x40
DotsLeft = 0x80
class Controller:
def __init__(self, device: str = None):
self.gps = False
self.debug = device is None
if not self.debug:
self.ser = serial.Serial(device, 9600)
time.sleep(2)
print(self.ser.read_all())
def clear(self):
self.send_hud([0x03, 0, 0, 0, 0x00, 0, 0])
def _as_dgt(self, number: float) -> int:
n = int(number)
if n == 0:
return 0
n %= 10
return 10 if n == 0 else n
def set_lines(self, outlines: list, arrows: list):
self.send_hud([0x02, sum([lane.value for lane in outlines]), sum([lane.value for lane in arrows])])
def set_direction(self, angle: OutAngle, out: OutType = OutType.Lane, roundabout: OutAngle = OutAngle.AsDirection):
if angle == OutAngle.LeftDown:
param_1 = 0x10
elif angle == OutAngle.RightDown:
param_1 = 0x20
else:
param_1 = out.value
if out == OutType.RightRoundabout or out == OutType.LeftRoundabout:
param_2 = angle.value if roundabout == OutAngle.AsDirection else roundabout.value
else:
param_2 = 0x00
param_3 = 0x00 if angle == OutAngle.LeftDown or angle == OutAngle.RightDown else angle.value
arr = [0x01, param_1, param_2, param_3]
self.send_hud(arr)
def set_distance(self, distance: float, unit: Unit = Unit.Any):
is_float = int(distance * 10) == int(distance) * 10
if not is_float:
distance = distance * 10
arr = [0x03,
self._as_dgt(distance // 1000),
self._as_dgt(distance // 100),
self._as_dgt(distance // 10),
0x00 if is_float else 0xff,
self._as_dgt(distance),
unit.value]
self.send_hud(arr)
def set_speed(self, speed: int, limit: int = 0, acc: bool = False):
acc_char = 0xff if acc else 0x00
if not self.gps:
self.set_distance(speed)
self.send_hud([0x06, 0, 0, 0, 0, 0, 0, 0, 0, acc_char])
return
if limit > 0:
arr = [0x06, self._as_dgt(speed // 100), self._as_dgt(speed // 10), self._as_dgt(speed), 0xff,
self._as_dgt(limit // 100), self._as_dgt(limit // 10), self._as_dgt(limit),
0xff if speed > limit else 0x00, acc_char]
else:
arr = [0x06, 0x00, 0x00, 0x00, 0x00, self._as_dgt(speed // 100), self._as_dgt(speed // 10),
self._as_dgt(speed), 0x00, acc_char]
self.send_hud(arr)
def set_time(self, hours: int, minutes: int, traffic: bool = False, flag: bool = False):
traffic_char = 0xff if traffic else 0x00
flag_char = 0xff if flag else 0x00
if hours > 99:
arr = [0x05, traffic_char, self._as_dgt(hours // 1000), self._as_dgt(hours // 100), 0x00,
self._as_dgt(hours // 10), self._as_dgt(hours), 0xff, flag_char]
else:
arr = [0x05, traffic_char, self._as_dgt(hours // 10), self._as_dgt(hours), 0xff,
self._as_dgt(minutes // 10), self._as_dgt(minutes), 0x00, flag_char]
self.send_hud(arr)
def set_gps(self, state: bool):
if state:
self.send_hud([0x07, 0x01])
self.gps = state
def set_speed_control(self, on: bool = True):
self.send_hud([0x04, 0x01 if on else 0x00])
def set_compass(self, direction: float):
if direction > 337.5 or direction <= 22.5:
self.set_direction(OutAngle.Straight, OutType.ArrowOnly)
elif 22.5 < direction <= 67.5:
self.set_direction(OutAngle.EasyRight, OutType.ArrowOnly)
elif 67.5 < direction <= 112.5:
self.set_direction(OutAngle.Right, OutType.ArrowOnly)
elif 112.5 < direction <= 157.5:
self.set_direction(OutAngle.SharpRight, OutType.ArrowOnly)
elif 157.5 < direction <= 202.5:
self.set_direction(OutAngle.Down, OutType.ArrowOnly)
elif 202.5 < direction <= 247.5:
self.set_direction(OutAngle.SharpLeft, OutType.ArrowOnly)
elif 247.5 < direction <= 292.5:
self.set_direction(OutAngle.Left, OutType.ArrowOnly)
elif 292.5 < direction <= 337.5:
self.set_direction(OutAngle.EasyLeft, OutType.ArrowOnly)
self.clear()
def send_hud(self, buf: list):
print("buf", buf)
n = len(buf)
chars = []
crc = uint8(0xeb + n + n)
chars.append(0x10)
chars.append(0x7b)
chars.append(n + 6)
if n == 0xa:
chars.append(0x10)
chars.append(n)
chars.append(0x00)
chars.append(0x00)
chars.append(0x00)
chars.append(0x55)
chars.append(0x15)
for char in buf:
crc = uint8(crc + char)
chars.append(char)
if char == 0x10:
chars.append(0x10)
chars.append((-crc) & 0xff)
chars.append(0x10)
chars.append(0x03)
self.send_packet(chars)
def send_packet(self, buff):
encoded = [bytes(chr(char), 'raw_unicode_escape') for char in buff]
if self.debug:
print("raw", buff)
print("enc", encoded)
else:
for char in buff:
self.ser.write(bytes(chr(char), 'raw_unicode_escape'))
time.sleep(0.2)
print(self.ser.read_all())
if __name__ == '__main__':
import sys
device = sys.argv[1] if len(sys.argv) > 1 else None
c = Controller(device)

141
python/test.py Normal file
View file

@ -0,0 +1,141 @@
from main import Controller, Lane, OutType, OutAngle, Unit
from time import sleep
interval = 0.2
def suite(controller: Controller):
lines(controller)
direction(controller)
distance(controller)
speed(controller)
time(controller)
control(controller)
compass(controller)
def lines(controller: Controller):
print("Lines")
controller.set_lines([Lane.DotsLeft], [])
controller.set_lines([Lane.OuterRight], [Lane.OuterLeft])
controller.set_lines([Lane.MiddleRight], [Lane.MiddleLeft])
controller.set_lines([Lane.InnerRight], [Lane.InnerLeft])
controller.set_lines([Lane.InnerLeft], [Lane.InnerRight])
controller.set_lines([Lane.MiddleLeft], [Lane.MiddleRight])
controller.set_lines([Lane.OuterLeft], [Lane.OuterRight])
controller.set_lines(
[Lane.DotsRight],
[Lane.OuterRight, Lane.MiddleRight, Lane.InnerRight, Lane.InnerLeft, Lane.MiddleLeft, Lane.OuterLeft]
)
controller.set_lines([], [])
def direction(controller: Controller):
print("Direction")
controller.set_direction(OutAngle.SharpRight)
controller.set_direction(OutAngle.Right)
controller.set_direction(OutAngle.EasyRight)
controller.set_direction(OutAngle.Straight)
controller.set_direction(OutAngle.EasyLeft)
controller.set_direction(OutAngle.Left)
controller.set_direction(OutAngle.SharpLeft)
controller.set_direction(OutAngle.LeftDown)
controller.set_direction(OutAngle.RightDown)
controller.set_direction(OutAngle.Down)
controller.set_direction(OutAngle.SharpRight, OutType.LongerLane)
controller.set_direction(OutAngle.Right, OutType.LongerLane)
controller.set_direction(OutAngle.EasyRight, OutType.LongerLane)
controller.set_direction(OutAngle.Straight, OutType.LongerLane)
controller.set_direction(OutAngle.EasyLeft, OutType.LongerLane)
controller.set_direction(OutAngle.Left, OutType.LongerLane)
controller.set_direction(OutAngle.SharpLeft, OutType.LongerLane)
roundabout(controller)
controller.set_direction(OutAngle.Left, OutType.Flag)
controller.set_direction(OutAngle.Right, OutType.Flag)
def roundabout(controller: Controller):
controller.set_direction(OutAngle.SharpRight, OutType.RightRoundabout)
controller.set_direction(OutAngle.Right, OutType.RightRoundabout)
controller.set_direction(OutAngle.EasyRight, OutType.RightRoundabout)
controller.set_direction(OutAngle.Straight, OutType.RightRoundabout)
controller.set_direction(OutAngle.EasyLeft, OutType.RightRoundabout)
controller.set_direction(OutAngle.Left, OutType.RightRoundabout)
controller.set_direction(OutAngle.SharpLeft, OutType.RightRoundabout)
controller.set_direction(OutAngle.Down, OutType.RightRoundabout)
def distance(controller: Controller):
print("Distance")
controller.set_distance(555.5)
controller.set_distance(6666)
controller.set_distance(777.7)
controller.set_distance(888, Unit.Kilometres)
controller.set_distance(999, Unit.Metres)
controller.set_distance(999, Unit.Foot)
controller.set_distance(999, Unit.Miles)
def speed(controller: Controller):
print("Speed")
controller.set_gps(True)
controller.set_speed(50)
controller.set_speed(50, 100)
controller.set_speed(150, 100)
controller.set_speed(50, 100, True)
def time(controller: Controller):
print("Time")
controller.set_time(22, 22)
controller.set_time(22, 22, traffic=True)
controller.set_time(22, 22, flag=True)
def control(controller: Controller):
print("Speed Control")
controller.set_speed_control()
controller.set_speed_control(False)
def compass(controller: Controller):
print("Compass")
controller.set_compass(22.5)
controller.set_compass(67.5)
controller.set_compass(112.5)
controller.set_compass(157.5)
controller.set_compass(202.5)
controller.set_compass(247.5)
controller.set_compass(292.5)
controller.set_compass(337.5)
def route(controller: Controller):
print("Route")
controller.set_direction(OutAngle.Left)
controller.set_gps(True)
controller.set_speed(50, 50)
controller.set_distance(1.2, Unit.Kilometres)
sleep(1)
controller.set_distance(1.1, Unit.Kilometres)
sleep(1)
controller.set_distance(1, Unit.Kilometres)
sleep(1)
remaining = 900
while remaining > 0:
controller.set_distance(remaining, Unit.Metres)
sleep(1)
remaining -= 100
controller.set_direction(OutAngle.Right, OutType.Flag)
remaining = 900
while remaining > 0:
controller.set_distance(remaining, Unit.Metres)
sleep(1)
remaining -= 100
if __name__ == '__main__':
import os
name = '/dev/rfcomm0' if os.name != 'nt' else 'COM8'
instance = Controller(name)
suite(instance)

View file

@ -13,6 +13,19 @@ dependencyResolutionManagement {
maven { maven {
url 'https://jitpack.io' url 'https://jitpack.io'
} }
maven {
url 'https://api.mapbox.com/downloads/v2/releases/maven'
authentication {
basic(BasicAuthentication)
}
credentials {
// Do not change the username below.
// This should always be `mapbox` (not your username).
username = "mapbox"
// Use the secret token you stored in gradle.properties as the password
password = MAPBOX_DOWNLOADS_TOKEN
}
}
} }
} }
rootProject.name = "Garmin" rootProject.name = "Garmin"