Eric's Place
Welcome to the DEEP WEB. No... wait... still the regular web.

Let Bluetooth Guide My Bike (Part 2: Android App)

In our previous episode, we laid out the framework for the development of a bicycle navigator, which would point us to where we want to go using data fed from an Android app over Bluetooth Low Energy (BLE). Let’s talk about that app.

Finding a Destination

The first step in figuring out where to go is of course telling the computer where we want to go. We need a way of capturing a typical street address and converting it into a latitude/longitude, with which we can perform our fun calculations. Android has a built-in way of accomplishing this via its Geocoder, but we’re busy cycle-commuters with stuff to do. So how about if it autocompletes the address à la…everything on the internet? Luckily Google provides a way for us to do this too, via their Places API. It’s not free, but Google kindly gives a $200 credit for the use of this API, which is more than enough for a lone developer trying to find his way (literally) around the world. Just add an AutocompleteSupportFragment to the UI, and tell it we want the latitude and longitude:

destinationEditTxt.setPlaceFields(Arrays.asList(Place.Field.ID, Place.Field.NAME, Place.Field.LAT_LNG));

Then set a listener for the result:

 destinationEditTxt.setOnPlaceSelectedListener(new PlaceSelectionListener() {
            @Override
            public void onPlaceSelected(Place place) {
                Log.i(TAG, "Place: " + place.getName() + ", " + place.getId());
                selectedPlace = place;
            }

            @Override
            public void onError(Status status) {
                Log.i(TAG, "An error occurred: " + status);
            }
        });



Pointing the way

Now this is the interesting part: Assuming we know where we are and where we want to be, how do we figure out where to go? I figure there’s two ways to go about this: The phone’s GPS, or its compass. Let’s investigate:

The Compass

What attracted me to the compass at first (just call me the north pole! har har.) was that in theory it should work even when I wasn’t moving, and would use less power than the GPS. The ROTATION_VECTOR sensor provided by Android is the current way of accessing the compass. It reports the yaw, pitch, and roll of the device with respect to magnetic north, but we’re only interested in the yaw since it represents the rotation of the plane parallel to the ground. We convert from radians to degrees, change the scale from 0-360, and also apply some low-pass filtering to avoid spurious changes in value (it just wouldn’t be a sensor without a filter somewhere):

 SensorManager.getRotationMatrixFromVector(rMat, event.values);
                    float newBearing = (float) Math.toDegrees(SensorManager.getOrientation(rMat, orientation)[0]);
                    if (newBearing < 0) {
                        newBearing += 360;
                    }
                    currentCompassBearing = lowPassFilter(newBearing, currentCompassBearing);

And our filter, which adjusts our existing value at a dampened rate controlled by a coefficient:

static private final float FILTER_ALPHA = 0.25f;


protected static float lowPassFilter( float input, float output ) {
    output = output + FILTER_ALPHA * (input - output);

    return output;
}

The only remaining issue is that the compass gives us the angle of our device with respect to North, but we need to know the deflection in degrees between our current orientation and our target destination. Let’s start by annotating a typical example with what we know so far:

Start and Destination
Start, destination and angle

The first step is to place ourselves on the origin, so subtract our destination’s position from our own. Now we can also turn our bearing into a virtual vector (assuming magnitude = 1) using trigonometry to extract its x,y components:

Start, destination and bearing vector
Start, destination and bearing vector

This just becomes a matter of finding the angle between two vectors, which is easy enough. I used the difference of the atan2 of both vectors to find the final angle. The atan2 function is handy because it returns an angle with respect to the X-axis and therefore the result will end up in the right quadrant. But I’m not a math expert - maybe there’s a better method?

Vector2D currPos = new Vector2D(currentLocation.getLatitude(), currentLocation.getLongitude());
Vector2D destVect = new Vector2D(targetLocation.latitude, targetLocation.longitude).subtract(currPos);
Vector2D bearingVect = new Vector2D(Math.sin(Math.toRadians(currentCompassBearing)), Math.cos(Math.toRadians(currentCompassBearing)));

return (float) Math.toDegrees(Math.atan2(destVect.getX(), destVect.getY()) - Math.atan2(bearingVect.getX(), bearingVect.getY()));


Unfortunately, there are two problems: First of all, the phone is going to be in my pocket and might not necessarily be facing forwards. This is a nuisance, yet still a solvable problem. Unfortunately, the second issue is simply insurmountable: The compass in my phone (Pixel 3), to put it bluntly, sucks. By that I mean it generally loses its calibration leading to inaccuracies of 120 degrees or more. I noticed it tends to vary with the orientation of the phone (screen facing down vs screen facing up) until it’s “calibrated”, a process which involves making figure-eights with the phone in midair, like I’m trying to curry favour with the gods of navigation before venturing forth. Definitely not the kind of thing I want to be doing every time I hop on my bike. Let’s try something else.

The GPS

Perhaps the GPS will give us a more reliable way of finding our current bearing. The good news is that the GPS actually gives us a bearing directly as one of its available values - just call Location.getBearing(). Better yet, this bearing is calculated based on displacement, so the direction that the phone is pointing is irrelevant.

But, as always, there’s a catch: You need to be moving to actually measure displacement, so it will be impossible to generate updates while standing still. Unfortunate, but tolerable. All that’s missing is finding the bearing between us and our destination: It turns out Android saves us from linear algebra purgatory with a handy Location.distanceBetween method. Feed it two positions and it spits out 3 floats: An initial bearing, a final bearing, and the distance in meters.

So why do we get two bearings? Well, the world is round (an ellipsoid, for the fancy folk), meaning you always travel in an arc across its surface. The angle with which you start, therefore, is different than the one you’ll finish with. Since we’ll be updating this value continuously and are covering short distances (my bike seat isn’t very comfortable), we can just pick the initial bearing. The code for navigating via GPS is thusly much simpler:

public synchronized float[] getGpsDistanceAndBearingTo(LatLng targetLocation) {
        if (currentLocation == null || !isGpsBearingValid()) {
            return new float[]{INVALID_RESULT,-1,-1,-1};
        }
        float[] distResults = new float[3];
        Location.distanceBetween(currentFilteredLatLng.latitude, currentFilteredLatLng.longitude, targetLocation.latitude, targetLocation.longitude, distResults);

        distResults[0] /= 1000f;
        if (distResults[1] < 0) {
            distResults[1] += 360;
        }

        distResults[1] -= currentLocation.getBearing();
        return distResults;
    }

In order to keep battery drain in check, we can use the FusedLocationProvider with an update interval of 1s. FusedLocationProvder is a new component introduced by Google which will attempt to use both network and GPS data to determine location, which allegedly reduces power consumption. After tweaking the update rate, adding some more filtering (turns out it occasionally gives a completely wrong location), and making sure it doesn’t update below a certain speed, the GPS-based approach produces a reliable way of finding the bearing to the destination. Of course, it also provides the current speed and distance remaining. Success!

Sending the data

The next big part of the Android app is the part that sends the data over BLE. Unfortunately, this part isn’t very interesting: It’s largely an exercise in tedious boilerplate, and there is good sample code available. To keep things simple, let’s use a single GATT characteristic which packs the speed, distance, and angle in an array of 3 floats:

private static byte[] navDtoToByteArray(NavDTO dto) {
        return ByteBuffer.allocate(12).putFloat(dto.distToDest).putFloat(dto.angleToDest).putFloat(dto.speed).array();
}


The rest is probably easier to understand by just seeing the code. Next, we will discuss implementing the microcontroller side.