diff --git a/app/build.gradle b/app/build.gradle index 2008533..8eb3c08 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,6 +7,7 @@ plugins { android { namespace 'eu.ztsh.garmin' compileSdk 34 + ndkVersion "23.2.8568313" defaultConfig { applicationId "eu.ztsh.garmin" @@ -36,19 +37,24 @@ android { viewBinding true buildConfig = true } - buildToolsVersion '35.0.0' +} +ext { + mapboxVersion = '3.2.0' } dependencies { - - implementation "com.mapbox.navigation:ui-dropin:2.20.0" - 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' + implementation "com.mapbox.navigationcore:navigation:$mapboxVersion" + implementation "com.mapbox.navigationcore:ui-maps:$mapboxVersion" + implementation "com.mapbox.navigationcore:voice:$mapboxVersion" + implementation "com.mapbox.navigationcore:tripdata:$mapboxVersion" + implementation "com.mapbox.navigationcore:ui-components:$mapboxVersion" + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' testImplementation 'org.assertj:assertj-core:3.24.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - 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' } diff --git a/app/src/main/java/eu/ztsh/garmin/Garmin.kt b/app/src/main/java/eu/ztsh/garmin/Garmin.kt index cee7a83..aaf8f0c 100644 --- a/app/src/main/java/eu/ztsh/garmin/Garmin.kt +++ b/app/src/main/java/eu/ztsh/garmin/Garmin.kt @@ -6,13 +6,16 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothSocket import android.util.Log import com.mapbox.navigation.core.trip.session.LocationMatcherResult -import com.mapbox.navigation.core.trip.session.NavigationSessionState -import com.mapbox.navigation.ui.maneuver.model.Maneuver +import com.mapbox.navigation.tripdata.maneuver.model.Maneuver +import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue +import eu.ztsh.garmin.data.Arrows import eu.ztsh.garmin.data.DataCache import eu.ztsh.garmin.data.GarminMapper +import eu.ztsh.garmin.data.Lanes import eu.ztsh.garmin.data.MapboxMapper import java.io.IOException import java.util.* +import java.util.concurrent.Executors import java.util.concurrent.SynchronousQueue @SuppressLint("MissingPermission") @@ -23,68 +26,160 @@ class Garmin( ) { private lateinit var connection: ConnectThread - private lateinit var processing: ProcessingThread + private lateinit var maneuvers: ManeuverProcessingThread + private lateinit var trips: TripProgressProcessingThread + private lateinit var locations: LocationMatcherProcessingThread private val cache = DataCache() + private val processingPool = Executors.newFixedThreadPool(8) + fun start() { connection = ConnectThread() connection.start() + + maneuvers = ManeuverProcessingThread() + maneuvers.start() + + trips = TripProgressProcessingThread() + trips.start() + + locations = LocationMatcherProcessingThread() + locations.start() } fun close() { connection.close() + + maneuvers.interrupt() + maneuvers.join(0) + + trips.interrupt() + trips.join(0) + + locations.interrupt() + locations.join(0) + + processingPool.shutdown() } fun process(maneuver: Maneuver) { - processing = ManeuverProcessingThread(maneuver) - processing.start() - processing.join() + processingPool.submit{maneuvers.enqueue(maneuver)} } - fun process(location: LocationMatcherResult) { - processing = LocationProcessingThread(location) - processing.start() - processing.join() + fun process(tripProgressUpdateValue: TripProgressUpdateValue) { + processingPool.submit{trips.enqueue(tripProgressUpdateValue)} } - fun process(navigationSessionState: NavigationSessionState) { - cache.update(navigationSessionState) + fun process(locationMatcherResult: LocationMatcherResult) { + processingPool.submit{locations.enqueue(locationMatcherResult)} } - private inner class ManeuverProcessingThread(val maneuver: Maneuver) : ProcessingThread() { + private inner class LocationMatcherProcessingThread: ProcessingThread() { + + override fun mapAndSend(maybeItem: LocationMatcherResult?): LocationMatcherResult? { + if (maybeItem != null && cache.hasChanged(maybeItem)) { + send(GarminMapper.map(MapboxMapper.asSpeed(maybeItem))) + return maybeItem + } + return null + } + + override fun updateCache(item: LocationMatcherResult) { + cache.update(item) + } + + } + + private inner class TripProgressProcessingThread : ProcessingThread() { + + override fun mapAndSend(maybeItem: TripProgressUpdateValue?): TripProgressUpdateValue? { + if (maybeItem != null) { + // it is much simplier to parse and compare model object + val value = MapboxMapper.asEta(maybeItem) + if (cache.hasChanged(value)) { + // TODO: traffic + send(GarminMapper.setTime(value.hours, value.minutes)) + cache.update(value) + } + } + return null + } + + override fun updateCache(item: TripProgressUpdateValue) { + // won't be used + } + + } + + private inner class ManeuverProcessingThread : ProcessingThread() { + + override fun mapAndSend(maybeItem: Maneuver?): Maneuver? { + if (maybeItem != null) { + Log.d(TAG, "mapAndSend (${currentThread().name}): got new") + var changed = false + if (cache.hasChanged(maybeItem.primary)) { + changed = true + Log.d(TAG, "mapAndSend: primary") + send(GarminMapper.map(MapboxMapper.asDirection(maybeItem))) + if (maybeItem.laneGuidance == null) { + send(GarminMapper.map(Lanes(Arrows(setOf()), Arrows(setOf())))) + } + } + if (cache.hasChanged(maybeItem.laneGuidance)) { + changed = true + Log.d(TAG, "mapAndSend: lanes") + send(GarminMapper.map(MapboxMapper.asLanes(maybeItem))) + } + if (cache.hasChanged(maybeItem.stepDistance)) { + changed = true + Log.d(TAG, "mapAndSend: stepDistance") + send(GarminMapper.map(MapboxMapper.asDistance(maybeItem))) + } + if (changed) { + return maybeItem + } + } + return null + } + + override fun updateCache(item: Maneuver) { + cache.update(item) + } + + } + + private abstract inner class ProcessingThread : Thread() { + + private val queue: SynchronousQueue = SynchronousQueue() + private var stop: Boolean = false + + abstract fun mapAndSend(maybeItem: T?): T? + + abstract fun updateCache(item: T) + + fun send(data: IntArray) { + connection.enqueue(data) + } + + fun enqueue(item: T) { + queue.put(item) + } override fun run() { - if (cache.hasChanged(maneuver)) { - cache.update(maneuver) - send(MapboxMapper.apply(maneuver)) + while (!stop) { + val maybeItem = queue.poll() + val item = mapAndSend(maybeItem) + if (item != null) { + Log.d(TAG, "run: Cache updated") + updateCache(item) + } } } - } - - private inner class LocationProcessingThread(val location: LocationMatcherResult) : ProcessingThread() { - - override fun run() { - if (cache.hasChanged(location)) { - cache.update(location) - send(MapboxMapper.apply(location)) - } + override fun interrupt() { + stop = true + super.interrupt() } - - } - - private open inner class ProcessingThread : Thread() { - - fun send(incoming: eu.ztsh.garmin.data.State) { - if (cache.hasChanged(incoming.distance)) { - connection.enqueue(GarminMapper.setDistance(incoming)) - } - if (cache.hasChanged(incoming.direction)) { - connection.enqueue(GarminMapper.setDirection(incoming)) - } - cache.update(incoming) - } - } private inner class ConnectThread : Thread() { @@ -106,10 +201,12 @@ class Garmin( context.setConnectionStatus(true) sleep(3000) readAll() + send(intArrayOf(0x07, 0x01)) // Set GPS to true while (true) { val newCurrent = queue.poll() if (newCurrent == null) { - sleep(500) + Log.d(TAG, "run (${currentThread().name}): Sleep...") + sleep(250) } else { current = newCurrent } @@ -157,13 +254,15 @@ class Garmin( private fun sendRaw(buff: IntArray) { buff.forEach { socket!!.outputStream.write(it) } socket!!.outputStream.flush() - sleep(2000) readAll() } } companion object { + + lateinit var instance: Garmin + fun prepareData(input: IntArray): IntArray { val n = input.size var crc = (0xeb + n + n).toUInt() diff --git a/app/src/main/java/eu/ztsh/garmin/MainActivity.kt b/app/src/main/java/eu/ztsh/garmin/MainActivity.kt index 4371bde..bb84358 100644 --- a/app/src/main/java/eu/ztsh/garmin/MainActivity.kt +++ b/app/src/main/java/eu/ztsh/garmin/MainActivity.kt @@ -5,57 +5,76 @@ import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager -import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.util.Log +import android.view.WindowManager import android.widget.Toast import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import com.mapbox.navigation.base.formatter.DistanceFormatterOptions import com.mapbox.navigation.base.options.NavigationOptions -import com.mapbox.navigation.base.trip.model.RouteProgress -import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter +import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp -import com.mapbox.navigation.core.trip.session.RouteProgressObserver -import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi +import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver +import com.mapbox.navigation.core.lifecycle.requireMapboxNavigation import eu.ztsh.garmin.databinding.ActivityMainBinding +import eu.ztsh.garmin.mapbox.MapControl +import eu.ztsh.garmin.util.PermissionsHelper +import java.lang.ref.WeakReference @SuppressLint("MissingPermission") class MainActivity : AppCompatActivity() { - lateinit var garmin: Garmin +// lateinit var garmin: Garmin private lateinit var binding: ActivityMainBinding - private val mapboxToolbox = MapboxToolbox(lifecycle, this) + 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?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - mapboxToolbox.onCreate() + binding.mapView + permissionsHelper.checkPermissions { + mapControl = MapControl(this, UI(binding), resources) + mapControl.init() + + MapboxNavigationApp.setup( + NavigationOptions.Builder(applicationContext) + .build() + ) + } bluetoothInit() + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } - override fun onStart() { - super.onStart() - mapboxToolbox.onStart() - } - - override fun onStop() { - super.onStop() - mapboxToolbox.onStop() - } override fun onDestroy() { super.onDestroy() - mapboxToolbox.onDestroy() + mapControl.onDestroy() + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } private fun bluetoothInit() { @@ -74,10 +93,10 @@ class MainActivity : AppCompatActivity() { val context = this pairedDevices?.firstOrNull { device -> Log.d(TAG, device.name) - device.name.equals("GARMIN HUD") + device.name == "GARMIN HUD" }?.apply { - garmin = Garmin(context, this, bluetoothAdapter) - garmin.start() + Garmin.instance = Garmin(context, this, bluetoothAdapter) + Garmin.instance.start() } } diff --git a/app/src/main/java/eu/ztsh/garmin/MapboxObserver.kt b/app/src/main/java/eu/ztsh/garmin/MapboxObserver.kt deleted file mode 100644 index 35bdabd..0000000 --- a/app/src/main/java/eu/ztsh/garmin/MapboxObserver.kt +++ /dev/null @@ -1,21 +0,0 @@ -package eu.ztsh.garmin - -import android.util.Log -import com.mapbox.navigation.core.MapboxNavigation -import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver - -class MapboxObserver : MapboxNavigationObserver { - - override fun onAttached(mapboxNavigation: MapboxNavigation) { - Log.d(TAG, "Attached") - } - - override fun onDetached(mapboxNavigation: MapboxNavigation) { - Log.d(TAG, "Detached") - } - - companion object { - const val TAG = "MBOXOBS" - } - -} diff --git a/app/src/main/java/eu/ztsh/garmin/MapboxToolbox.kt b/app/src/main/java/eu/ztsh/garmin/MapboxToolbox.kt deleted file mode 100644 index 5dabf1e..0000000 --- a/app/src/main/java/eu/ztsh/garmin/MapboxToolbox.kt +++ /dev/null @@ -1,96 +0,0 @@ -package eu.ztsh.garmin - -import android.annotation.SuppressLint -import android.location.Location -import android.util.Log -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import com.mapbox.navigation.base.formatter.DistanceFormatterOptions -import com.mapbox.navigation.base.options.NavigationOptions -import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter -import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp -import com.mapbox.navigation.core.trip.session.LocationMatcherResult -import com.mapbox.navigation.core.trip.session.LocationObserver -import com.mapbox.navigation.core.trip.session.NavigationSessionState -import com.mapbox.navigation.core.trip.session.NavigationSessionStateObserver -import com.mapbox.navigation.core.trip.session.RouteProgressObserver -import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi - -@SuppressLint("MissingPermission") -class MapboxToolbox(lifecycle: Lifecycle, private val context: MainActivity) { - - private val mapboxObserver = MapboxObserver() - - init { - lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - MapboxNavigationApp.attach(owner) - MapboxNavigationApp.registerObserver(mapboxObserver) - } - - override fun onPause(owner: LifecycleOwner) { - MapboxNavigationApp.detach(owner) - MapboxNavigationApp.unregisterObserver(mapboxObserver) - } - }) - } - - fun onCreate() { - if (!MapboxNavigationApp.isSetup()) { - MapboxNavigationApp.setup { - NavigationOptions.Builder(context) - .accessToken(BuildConfig.MAPBOX_DOWNLOADS_TOKEN) - .build() - } - } - MapboxNavigationApp.current()?.startTripSession() - } - - fun onStart() { - MapboxNavigationApp.current()?.registerRouteProgressObserver(routeProgressObserver) - MapboxNavigationApp.current()?.registerLocationObserver(locationObserver) - MapboxNavigationApp.current()?.registerNavigationSessionStateObserver(navigationStateObserver) - } - - fun onStop() { - MapboxNavigationApp.current()?.unregisterRouteProgressObserver(routeProgressObserver) - MapboxNavigationApp.current()?.unregisterLocationObserver(locationObserver) - MapboxNavigationApp.current()?.unregisterNavigationSessionStateObserver(navigationStateObserver) - } - - fun onDestroy() { - MapboxNavigationApp.current()?.stopTripSession() - maneuverApi.cancel() - } - - // Define distance formatter options - private val distanceFormatter: DistanceFormatterOptions by lazy { - DistanceFormatterOptions.Builder(context).build() - } - // Create an instance of the Maneuver API - private val maneuverApi: MapboxManeuverApi by lazy { - MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatter)) - } - - private val routeProgressObserver = - RouteProgressObserver { routeProgress -> - maneuverApi.getManeuvers(routeProgress).value?.apply { - context.garmin.process( - this[0] - ) - } - } - - private val locationObserver = object : LocationObserver { - override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) { - context.garmin.process(locationMatcherResult) - } - - override fun onNewRawLocation(rawLocation: Location) { - } - } - - private val navigationStateObserver = NavigationSessionStateObserver { context.garmin.process(it) } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/ztsh/garmin/UI.kt b/app/src/main/java/eu/ztsh/garmin/UI.kt new file mode 100644 index 0000000..c27faff --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/UI.kt @@ -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 + +} \ No newline at end of file diff --git a/app/src/main/java/eu/ztsh/garmin/data/DataCache.kt b/app/src/main/java/eu/ztsh/garmin/data/DataCache.kt index 6b5622b..3e61688 100644 --- a/app/src/main/java/eu/ztsh/garmin/data/DataCache.kt +++ b/app/src/main/java/eu/ztsh/garmin/data/DataCache.kt @@ -2,74 +2,49 @@ 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.ui.maneuver.model.Maneuver +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 val stateCache: State = State() private var maneuverCache: Maneuver? = null private var locationCache: LocationMatcherResult? = null private var session: NavigationSessionState? = null - - // state - fun hasChanged(lanes: Lanes?): Boolean { - return stateCache.lineArrows == null || stateCache.lineArrows != lanes - } - - fun hasChanged(outlines: Outlines?): Boolean { - return stateCache.lineOutlines == null || stateCache.lineOutlines != outlines - } - - fun hasChanged(distance: Distance?): Boolean { - return stateCache.distance == null || stateCache.distance != distance - } - - fun hasChanged(direction: Direction?): Boolean { - return stateCache.direction == null || stateCache.direction != direction - } - - fun hasChanged(speed: Speed?): Boolean { - return stateCache.speed == null || stateCache.speed != speed - } - - fun hasChanged(arrival: Arrival?): Boolean { - return stateCache.arrival == null || stateCache.arrival != arrival - } - - // Merge states - fun update(state: State) { - if (state.lineArrows != null) { - stateCache.lineArrows = state.lineArrows - } - if (state.lineOutlines != null) { - state.lineOutlines = state.lineOutlines - } - if (state.direction != null) { - stateCache.direction = state.direction - } - if (state.distance != null) { - stateCache.distance = state.distance - } - if (state.speed != null) { - stateCache.speed = state.speed - } - if (state.arrival != null) { - stateCache.arrival = state.arrival - } - } + private var eta: Arrival? = null // maneuver - fun hasChanged(maneuver: Maneuver): Boolean { - return maneuverCache == null || maneuverCache!! != 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 == null || locationCache!! != locationMatcherResult + return locationCache.let { it == null + || it.enhancedLocation.speed != locationMatcherResult.enhancedLocation.speed + || it.speedLimitInfo.speed != locationMatcherResult.speedLimitInfo.speed } } fun update(locationMatcherResult: LocationMatcherResult) { diff --git a/app/src/main/java/eu/ztsh/garmin/data/GarminMapper.kt b/app/src/main/java/eu/ztsh/garmin/data/GarminMapper.kt index 44182c8..b398381 100644 --- a/app/src/main/java/eu/ztsh/garmin/data/GarminMapper.kt +++ b/app/src/main/java/eu/ztsh/garmin/data/GarminMapper.kt @@ -4,29 +4,26 @@ class GarminMapper { companion object { - fun setLines(state: State): IntArray { - return intArrayOf(0x02, state.lineOutlines.sumOf { it.value }, state.lineArrows.sumOf { it.value }) + fun map(lanes: Lanes): IntArray { + return intArrayOf(0x02, lanes.lanes.lanes.sumOf { it.value }, lanes.outlines.lanes.sumOf { it.value }) } - fun setDirection(state: State): IntArray { - return setDirection(state.direction.angle, state.direction.out, state.direction.roundabout) + fun map(direction: Direction): IntArray { + return toDirectionArray(direction.angle, direction.out, direction.roundabout) } - fun setDistance(state: State): IntArray { - return setDistance(state.distance, state.unit) + fun map(distance: Distance): IntArray { + return setDistance(distance.distance, distance.unit) } - fun setSpeed(state: State): IntArray { - return setSpeed(state.speed, state.limit, state.speed > state.limit) - } - - fun setSpeedFreeRide(state: State): Pair { - return Pair(setDistance(state.speed), setSpeed(state.limit, limitWarning = state.speed > state.limit)) + 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, @@ -44,7 +41,7 @@ class GarminMapper { 0x05, trafficChar, asDigit(hours / 10), - asDigit(hours), + asDigit(if (hours == 0) 10 else hours), 0xff, asDigit(minutes / 10), asDigit(minutes), @@ -54,20 +51,15 @@ class GarminMapper { } } - fun setSpeedControl(state: State): IntArray { - return intArrayOf(0x04, if (state.control) 0x01 else 0x02) - } - - fun setCompass(state: State): IntArray { - // TODO: Implement - return setDirection(OutAngle.Straight, OutType.ArrowOnly) + 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 setDirection( + private fun toDirectionArray( angle: OutAngle, out: OutType = OutType.Lane, roundabout: OutAngle = OutAngle.AsDirection @@ -89,33 +81,38 @@ class GarminMapper { return intArrayOf(0x01, param1, param2, param3) } - private fun setDistance(distance: Int, unit: Unit = Unit.Any): IntArray { + 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(distance / 1000), asDigit(distance / 100), asDigit(distance / 10), - 0x00, asDigit(distance), unit.value + 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, - limitWarning: Boolean = false, acc: Boolean = false ): IntArray { // TODO: car connection val accChar = asChar(acc) - val limitWarningChar = asChar(limitWarning) return if (limit > 0) { intArrayOf( 0x06, asDigit(speed / 100), asDigit(speed / 10), - asDigit(speed), + asDigit(if (speed == 0) 10 else speed), 0xff, asDigit(limit / 100), asDigit(limit / 10), asDigit(limit), - limitWarningChar, + asChar(speed > limit), accChar ) } else { @@ -128,12 +125,16 @@ class GarminMapper { asDigit(speed / 100), asDigit(speed / 10), asDigit(speed), - limitWarningChar, + asChar(false), accChar ) } } + private fun asDigit(n: Double): Int { + return asDigit(n.toInt()) + } + private fun asDigit(n: Int): Int { if (n == 0) { return 0 diff --git a/app/src/main/java/eu/ztsh/garmin/data/MapboxMapper.kt b/app/src/main/java/eu/ztsh/garmin/data/MapboxMapper.kt index bc9b986..445cb40 100644 --- a/app/src/main/java/eu/ztsh/garmin/data/MapboxMapper.kt +++ b/app/src/main/java/eu/ztsh/garmin/data/MapboxMapper.kt @@ -1,74 +1,122 @@ 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.ui.maneuver.model.Maneuver +import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue class MapboxMapper { companion object { - - fun apply(maneuver: Maneuver): State { - val state = State() - maneuver.apply { - this.primary.apply { - state.direction = Direction() - when (this.type) { - "roundabout" -> { - state.direction!!.out = OutType.RightRoundabout - } - - "arrive" -> { - state.flag = true - } + + fun asDirection(maneuver: Maneuver): Direction { + val direction = Direction() + maneuver.primary.apply { + when (this.type) { + "roundabout" -> { + direction.out = OutType.RightRoundabout } - when (this.modifier) { - "right" -> { - when (this.type) { - "turn" -> state.direction!!.angle = OutAngle.Right - "roundabout" -> { - when (this.degrees) { - 137.0 -> state.direction!!.angle = OutAngle.EasyRight - 180.0 -> state.direction!!.angle = OutAngle.Straight - } - } - } - } - - "left" -> { - when (this.type) { - "turn" -> state.direction!!.angle = OutAngle.Left - } + "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 } } } - this.stepDistance.apply { - this.distanceRemaining?.apply { - distanceFormatter.formatDistance(distanceRemaining!!).split(" ").apply { - state.distance = Distance( - this[0].replace(',', '.').toDouble().toInt(), - when (this[1]) { - "m" -> Unit.Metres - "km" -> Unit.Kilometres - else -> Unit.Any + 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 + } } } - } - this.laneGuidance?.apply { - this.allLanes.apply { - println() + + "left" -> { + direction.angle = OutAngle.Left + when (this.type) { + "fork", "off ramp" -> { + direction.angle = OutAngle.EasyLeft + direction.out = OutType.LongerLane + } + } } } } - return state + return direction } - fun apply(locationMatcherResult: LocationMatcherResult): State { - val state = State() - // TODO: speed, limit, location?, bearing - return state + 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() + val lanes = mutableSetOf() + 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()) } } diff --git a/app/src/main/java/eu/ztsh/garmin/data/Model.kt b/app/src/main/java/eu/ztsh/garmin/data/Model.kt index 146d4ed..5b76a83 100644 --- a/app/src/main/java/eu/ztsh/garmin/data/Model.kt +++ b/app/src/main/java/eu/ztsh/garmin/data/Model.kt @@ -12,7 +12,6 @@ enum class OutType(val value: Int) { } - enum class OutAngle(val value: Int) { Down(0x01), @@ -48,11 +47,15 @@ enum class Lane(val value: Int) { InnerLeft(0x10), MiddleLeft(0x20), OuterLeft(0x40), - DotsLeft(0x80) + DotsLeft(0x80); + + companion object { + val iterator = {sortedSetOf(OuterRight, MiddleRight, InnerRight, InnerLeft, MiddleLeft, OuterLeft).iterator()} + } } -open class Arrows(val lanes: List) { +class Arrows(val lanes: Set) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -68,11 +71,9 @@ open class Arrows(val lanes: List) { } } -class Lanes(lanes: List) : Arrows(lanes) +class Lanes(val outlines: Arrows, val lanes: Arrows) -class Outlines(lanes: List) : Arrows(lanes) - -class Distance(val distance: Int, val unit: Unit) { +class Distance(val distance: Double, val unit: Unit) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -87,37 +88,44 @@ class Distance(val distance: Int, val unit: Unit) { } override fun hashCode(): Int { - var result = distance + 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) +class Arrival(val hours: Int, val minutes: Int) { -class State { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false - var lineArrows: Lanes? = null - var lineOutlines: Outlines? = null - var direction : Direction? = null - var distance: Distance? = null - var speed: Speed? = null - var arrival: Arrival? = null - // TODO: Bearing - // TODO: support - var traffic: Boolean? = null - var flag: Boolean? = null - var control: Boolean? = null + 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 +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 diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt new file mode 100644 index 0000000..8dfefde --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt @@ -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() + ) + } + } + +} diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt new file mode 100644 index 0000000..5ae5688 --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt @@ -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" + + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt new file mode 100644 index 0000000..a59a899 --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt @@ -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, routeOptions: RouteOptions) { + // no impl + } + + override fun onRoutesReady( + routes: List, + routerOrigin: String + ) { + setRouteAndStartNavigation(routes) + } + } + ) + } + + private fun setRouteAndStartNavigation(routes: List) { + // 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() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt b/app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt new file mode 100644 index 0000000..aa9578f --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt @@ -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 -> + 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 { value -> + // remove already consumed file to free-up space + speechApi.clean(value) + } + + fun cancel() { + speechApi.cancel() + voiceInstructionsPlayer.shutdown() + + } + +} diff --git a/app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt b/app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt new file mode 100644 index 0000000..b25e579 --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt @@ -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() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/ztsh/garmin/util/PermissionsHelper.kt b/app/src/main/java/eu/ztsh/garmin/util/PermissionsHelper.kt new file mode 100644 index 0000000..573e457 --- /dev/null +++ b/app/src/main/java/eu/ztsh/garmin/util/PermissionsHelper.kt @@ -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) { + 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) { + 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, + grantResults: IntArray + ) { + permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8177dab..cb6c22d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,9 +1,80 @@ - + - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/eu/ztsh/garmin/data/GarminMapperTest.kt b/app/src/test/java/eu/ztsh/garmin/data/GarminMapperTest.kt index f0f1269..8ae4482 100644 --- a/app/src/test/java/eu/ztsh/garmin/data/GarminMapperTest.kt +++ b/app/src/test/java/eu/ztsh/garmin/data/GarminMapperTest.kt @@ -9,8 +9,8 @@ class GarminMapperTest { @Test fun linesTest() { linesTest( - listOf(Lane.DotsLeft), - listOf(), + setOf(Lane.DotsLeft), + setOf(), intArrayOf(2, 128, 0), intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 128, 0, 141, 16, 3) ) @@ -51,14 +51,14 @@ class GarminMapperTest { intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 64, 2, 203, 16, 3) ) linesTest( - listOf(Lane.DotsRight), - listOf(Lane.OuterRight, Lane.MiddleRight, Lane.InnerRight, Lane.InnerLeft, Lane.MiddleLeft, Lane.OuterLeft), + 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( - listOf(), - listOf(), + setOf(), + setOf(), intArrayOf(2, 0, 0), intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 0, 0, 13, 16, 3) ) @@ -249,14 +249,12 @@ class GarminMapperTest { } private fun linesTest(outlines: Lane, arrows: Lane, expectedRaw: IntArray, expectedBoxed: IntArray) { - linesTest(listOf(outlines), listOf(arrows), expectedRaw, expectedBoxed) + linesTest(setOf(outlines), setOf(arrows), expectedRaw, expectedBoxed) } - private fun linesTest(outlines: List, arrows: List, expectedRaw: IntArray, expectedBoxed: IntArray) { - val state = State() - state.lineOutlines = outlines - state.lineArrows = arrows - makeAssertions(GarminMapper.setLines(state), expectedRaw, expectedBoxed) + private fun linesTest(outlines: Set, arrows: Set, 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) { @@ -264,10 +262,8 @@ class GarminMapperTest { } private fun directionTest(outAngle: OutAngle, outType: OutType, expectedRaw: IntArray, expectedBoxed: IntArray) { - val state = State() - state.direction.angle = outAngle - state.direction.out = outType - makeAssertions(GarminMapper.setDirection(state), expectedRaw, expectedBoxed) + val direction = Direction(outAngle, outType) + makeAssertions(GarminMapper.map(direction), expectedRaw, expectedBoxed) } private fun makeAssertions(resultRaw: IntArray, expectedRaw: IntArray, expectedBoxed: IntArray) { diff --git a/build.gradle b/build.gradle index 2084b5d..87ef816 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } } plugins { - id 'com.android.application' version '7.4.1' apply false - id 'com.android.library' version '7.4.1' apply false + id 'com.android.application' version '8.2.0' apply false + id 'com.android.library' version '8.2.0' apply false id 'org.jetbrains.kotlin.android' version '1.9.24' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 89142c4..b2dda6b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 22 21:00:17 CEST 2023 distributionBase=GRADLE_USER_HOME 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 zipStorePath=wrapper/dists diff --git a/python/main.py b/python/main.py index 73a55ab..4d002dd 100644 --- a/python/main.py +++ b/python/main.py @@ -63,7 +63,8 @@ class Controller: def clear(self): self.send_hud([0x03, 0, 0, 0, 0x00, 0, 0]) - def _as_dgt(self, n: int) -> int: + def _as_dgt(self, number: float) -> int: + n = int(number) if n == 0: return 0 n %= 10 @@ -89,9 +90,17 @@ class Controller: arr = [0x01, param_1, param_2, param_3] self.send_hud(arr) - def set_distance(self, distance, unit: Unit = Unit.Any): - arr = [0x03, self._as_dgt(distance // 1000), self._as_dgt(distance // 100), self._as_dgt(distance // 10), 0x00, - self._as_dgt(distance), unit.value] + 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): diff --git a/python/test.py b/python/test.py index 68addc9..c21a9e0 100644 --- a/python/test.py +++ b/python/test.py @@ -1,4 +1,4 @@ -from main import * +from main import Controller, Lane, OutType, OutAngle, Unit from time import sleep interval = 0.2 @@ -32,7 +32,6 @@ def lines(controller: Controller): def direction(controller: Controller): print("Direction") - controller.set_direction(OutAngle.RightDown) controller.set_direction(OutAngle.SharpRight) controller.set_direction(OutAngle.Right) controller.set_direction(OutAngle.EasyRight) @@ -41,6 +40,7 @@ def direction(controller: Controller): 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) @@ -49,6 +49,12 @@ def direction(controller: Controller): 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) @@ -56,18 +62,18 @@ def direction(controller: Controller): 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.Left, OutType.Flag) - controller.set_direction(OutAngle.Right, OutType.Flag) + controller.set_direction(OutAngle.Down, OutType.RightRoundabout) def distance(controller: Controller): print("Distance") - controller.set_distance(999) - controller.set_distance(999, Unit.Kilometres) + 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) - pass def speed(controller: Controller): @@ -77,7 +83,6 @@ def speed(controller: Controller): controller.set_speed(50, 100) controller.set_speed(150, 100) controller.set_speed(50, 100, True) - pass def time(controller: Controller): @@ -85,7 +90,6 @@ def time(controller: Controller): controller.set_time(22, 22) controller.set_time(22, 22, traffic=True) controller.set_time(22, 22, flag=True) - pass def control(controller: Controller): @@ -104,8 +108,34 @@ def compass(controller: Controller): controller.set_compass(247.5) controller.set_compass(292.5) controller.set_compass(337.5) - pass + + +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__': - instance = Controller('/dev/rfcomm0') + import os + name = '/dev/rfcomm0' if os.name != 'nt' else 'COM8' + instance = Controller(name) + suite(instance)