Tuesday, 17 August 2010

Satellite Status on Android 1.5 and beyond

Long time no change, but recently I had some time to redo the YGPS app using the official APIs.

You might remember from the previous posts that I employed a trick to get to the satellite status. Since API 3 this trick is not required any more and indeed the security hole that allowed me to access this information has been plugged.

So now I am just using API 3, which is the base for the long out Cupcake Android release. The code will also work on subsequent Android releases, such as Eclair and Froyo.

Now, the API boasts a call to a GpsStatus object, which contains a list of the status of all visible satellites. It is accessed from the LocationManager and once you got it I can iterate over the satellites to get their status and draw them on a sky map, like so:


float scale = radius / 90.0f;
if (lp.isProviderEnabled(LocationManager.GPS_PROVIDER)){
gpsStatus = this.lp.getGpsStatus(gpsStatus);

float mX;
float mY;

for (GpsSatellite s: gpsStatus.getSatellites()){
float theta = - (s.getAzimuth() + 90);
float rad = (float) (theta * Math.PI/180.0f);
mX = (float)Math.cos(rad);
mY = -(float)Math.sin(rad);
float elevation = s.getElevation() - 90.0f;
if (elevation > 90 || s.getAzimuth() < 0 || s.getPrn() < 0){
continue;
}
float a = elevation * scale;

int x = (int)Math.round(centerX + (mX * a) - mBitmapAdjustment);
int y = (int)Math.round(centerY + (mY * a) - mBitmapAdjustment);
if (s.usedInFix()){
canvas.drawBitmap(mSatelliteBitmapUsed, x, y, gridPaint);
} else {
if (gpsStatus.getTimeToFirstFix() > 0){
canvas.drawBitmap(mSatelliteBitmapUnused, x, y, gridPaint);
} else {
canvas.drawBitmap(mSatelliteBitmapNoFix, x, y, gridPaint);
}
}
canvas.drawText(new Integer(s.getPrn()).toString(),x, y, textPaint);
}
}


As usual you can find the full code here.

Wednesday, 4 February 2009

Priming the Openmoko U-Blox GPS Chip

I just wrote a little companion application to the YGPS Satellites app, or indeed any other application that uses the GPS chip on Openmoko. This new app speeds up the time-to-fix by communicating with the UBlox Online Assist server to download ephemeris data via the internet rather than waiting for the same information to be available via the GPS feeds. For me it speeds up the time-to-fix to a few seconds in some cases, less than a minute in most, while without it it can take up to 10 minutes to get a fix (has something to do with my desk being indoors, only facing a window).

The technical details for the U-Blox Online Assist feature can be found in detail on the Openmoko GPS pages.

To use the application you will have to get a free account with UBlox, because the requests require a user name and password. To obtain an account, send an empty email to agps-account@u-blox.com. I received my account password soon later by (apparently) automated email.

The app itself is headless, i.e. has no user-interface as such and is not installed to be launched by a user on its own. It consists of a BroadcastReceiver that waits for messages from other applications (such as my updated YGPSSats.apk) and then tries to interact with the UBlox server and primes the chip. It communicates back to the user with a few toasts. A preferences dialog allows the user to enter his Ublox Account details.

An updated version of my YGPSSats.apk checks whether the YGPSUblogs.apk is installed and then gives two additional menu options, one for setting the account details and an approximate location, the other for priming the chip. The updated YGPSSats also conveys (onStop()) the last known location to the ublox app, so that the next time the location details are as accurate as possible. (If you travelled far without the app on, set the location manually via settings).

The most likely error the app encounters is not having internet access, but sometimes there seems to be garbled responses. If you get an error and you believe you have internet access, simply try again. There seems to be no harm in priming a chip that already has a lock-on.

Installation


You will need the new version of my YGPSSats.apk, which contains the interface code to the ublox app. Users on phones other than Openmoko Freerunner will not see the new menu options. See below for installation hints if you are on the Koolu-RC3 image.

The you will need the YGPSUBlox.apk itself. This app will not show up in the launcher, but once it is installed you will get new menu options on the YGPS Satellites app.



Current limitations and work-arounds


Entering text into preferences dialog


The current Android version on Openmoko might not allow you to enter text into a preferences dialog. If this is the case run:
 adb pull /data/data/com.yunnanexplorer.android.gps.openmoko/shared_prefs/com.yunnanexplorer.android.gps.openmoko_preferences.xml prefs.xml

Then edit the prefs.xml file to add the following two lines:
<string name="ublox_password">your password here</string>
<string name="ublox_username">your email account name here</string>

The upload the prefs.xml file to your device with
adb push prefs.xml /data/data/com.yunnanexplorer.android.gps.openmoko/shared_prefs/com.yunnanexplorer.android.gps.openmoko_preferences.xml

When first starting you should also supply your approximate location and an accuracy estimate. The YGPS Satellites application automatically
saves your last location to these fields, so once you have a fix when running YGPS Satellites, the next time you start up (and have not moved to far
from your last location, this information should be good enough. If you start up your GPS at a distant point, you should either set the new location
or just wait for a natural GPS fix.

Installation on Koolu Beta-3


The new Koolu-beta 3 image has the YGPS app installed in the systems folder, meaning that it cannot be uninstalled or upgraded by a user.
To change this mount the rootfs in read/write-mode and remove the app from the system folder

adb shell
mount -o remount,rw rootfs /
cd /system/app
rm YGPSSats.apk

The application should then be automatically removed and can thus be reinstalled















Sunday, 1 February 2009

Visualizing the GPS Satellite Status

In a previous blog I mentioned that J Larimer J Larimer posted a way on how to get to the satellite status information that is captured by Android from the NMEA GSV messages.

I now put a little app together that visualises the satellite status in a way some GPS units do this. This is how it looks like on my OpenMoko:


On the OpenMoko this APK will run if a patch I posted on the Koolu Android Forum is applied to the Android Openmoko build as otherwise the GPS GSV NMEA messages are not parsed at all.

I do not have a G1 phone, but J Latimer's post suggested that the messages are correctly parsed on the device and that the updates are propagated through the Android stack. I would love to hear from someone if this works for you on the G1. Maybe even send a screenshot.

The APK for this app can be downloaded from here and the source is here.

Friday, 9 January 2009

Android GPS Internals: Satellite Status Updates

Trudging through the Android source code one will find that in addition to the location updates, the location library also provides updates on the state of satellites. But this functionality is hidden from developers, unless one delves into a bit of hacking as demonstrated here.

First of all a quick recap of how the GPS functionality works. On the G1 phone there is a library libgps.so which provides the interface to the GPS hardware. The code for this is not open source and is not available. However, what we do know is that this library communicates with Android over a set of callbacks, which are defined in
hardware/libhardware/include/hardware/gps.h.

In the emulator this hardware functionality is replicated in gps_qemu.cpp and can parse two of the many GPS NMEA sentences which are then sent on to trigger location updates. This allows users to trigger updates through the geo nmea messages mentioned in the documentation. However, satellite status updates are not part of this and any such message is just eaten.

However, changing this is not that difficult: it requires adding code to parse NMEA GPGSV messages in the qemu code and triggering the status update callback. I made an inital attempt at these changes for my OpenMoko Freerunner, which I posted in the Koolu Forums.

Thursday, 13 November 2008

Getting USGS Earthquake Data into Android

This is the second instalment on how to display USGS Earthquake data on Android. The last post dealt with displaying the data, this post with getting it.

Here we use the USGS RSS feed containing the last day or so of earthquakes, which is available here. This is a standard XML document, which I decided to parse and store in a content provider, which hides a SQLite Database. I do not show the content provider here, but it stores some text, the magnitude and the location. I decided not to update the database, but to wipe it clean every time. While it is possible to add to existing earthquake data, care must be taken, because the USGS updates data on earthquakes it has reported on earlier, so for some quakes an insert is required, for others an update. Here I want to get only the latest data, so I can afford to wipe the data.

I decided to implement it as a service that schedules a task using the Java Timer class. My runnable gets the feed, parses it and builds up info on the earthquakes. This is then, in a separate step, inserted into the database, where it is picked up from the Overlay as explained in the last post. The parser is simply a SAX parser that implements a little state machine and accumulates all the earthquake info from the feed (some questions to USGS: why not put magnitude and time stamp into a more readable format in their own elements? I am pretty sure most people want this.)

The schedule task looks like this:

private class Task extends TimerTask {
URL url;
SAXParser sp;
XMLReader xmlReader;
USGSHandler handler;
public Task() {
handler = new USGSHandler();
try {
url = new URL("http://earthquake.usgs.gov/eqcenter/catalogs/1day-M2.5.xml");
SAXParserFactory spf = SAXParserFactory.newInstance();
sp = spf.newSAXParser();
xmlReader = sp.getXMLReader();
xmlReader.setContentHandler(handler);
xmlReader.setErrorHandler(handler);
} catch (IOException e){
Log.e(TAG, e.toString());
} catch (SAXException e){
Log.e(TAG, e.toString());
} catch (ParserConfigurationException e){
Log.e(TAG, e.toString());
}
}
@Override
public void run() {
Log.e(TAG, "refreshing earthquakes");
handler.reset();
try {
xmlReader.parse(new InputSource(url.openStream()));
} catch (IOException e) {
Log.e(TAG, e.toString());
} catch (SAXException e) {
Log.e(TAG, e.toString());
}
List quakes = handler.getQuakes();
getContentResolver().delete(EarthquakeContentProvider.EarthquakeData.CONTENT_URI, "1 = 1", null);
for (Earthquake e : quakes){
ContentValues values = new ContentValues();
values.put(EarthquakeContentProvider.EarthquakeData.TEXT, e.mText);
values.put(EarthquakeContentProvider.EarthquakeData.LON, Math.round(e.mLon * 1.e6));
values.put(EarthquakeContentProvider.EarthquakeData.LAT, Math.round(e.mLat * 1e6));
values.put(EarthquakeContentProvider.EarthquakeData.MAGNITUDE, e.mMagnitude);
getContentResolver().insert(EarthquakeContentProvider.EarthquakeData.CONTENT_URI, values);

}
}

}

As usual lousy error handling.

Displaying USGS Earthquake Data on Android


The United States Geological Survey (USGS) has a helpful RSS feed for earthquakes all around the globe. You can find the various feeds here. Here I illustrate how to load the data and display them as an overlay on Google maps on Android.

The principal way I have chosen is to have a service running in the background that periodically gets the latest earthquakes and stores them in a ContentProvider. The map overlay queries the ContentProvider for the earthquakes for the given map extent.

We start off with the display. The display is an overlay that redefines the draw() method. There are two steps: first to get all the earthquakes from the content provider, secondly drawing them (with a bit of hint on the magnitude).

Finding all earthquakes is easy, bar the pain of accommodating that map views can span the dateline, where the query to the content provider gets a bit more difficult. First I thought that simply changing the where clause according to the left hand side longitude being smaller than the right hand side longitude was sufficient, but then it turned out that one can zoom out so much that the right hand side longitude is larger than the left hand side, but the map still contains the dateline. I handle this case by checking if the longitude span is larger than 360 degrees. That means that earthquakes regardless of longitude need to be returned. (The same does not apply to latitude: Google maps always just cuts off at 80 degrees and does not wrap around. (For engineers of the Google phone the world seems to be a wrap-around band. They seem to smoke weird stuff down in California, but I guess it makes their life a bit easier.)

Once I have all the earthquakes, I iterate through the list and display them according to magnitude: one degree of magnitude gets one circle and using the zoom-level I scale this a little bit, so that the concentric circles get smaller if one zooms out, resembling a bit an impact zone.

This is the code in all its glory:

import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.Log;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;

class EarthquakeOverlay extends Overlay {

public final static String TAG = "EarthquakeOverlay";

EarthquakeOverlay(){}

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);
GeoPoint[] cornerCoords = MapUtils.getCornerCoordinates(mapView.getProjection(), canvas);
String selection;
if (mapView.getLongitudeSpan() >= 360 * 1e6){
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString();
} else if (cornerCoords[0].getLongitudeE6() < cornerCoords[1].getLongitudeE6()){
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LON + " > " + ((Integer)cornerCoords[0].getLongitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LON + " < " + ((Integer)cornerCoords[1].getLongitudeE6()).toString();
} else {
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString() + " and ( " +
EarthquakeContentProvider.EarthquakeData.LON + " > " + ((Integer)cornerCoords[0].getLongitudeE6()).toString() + " or " +
EarthquakeContentProvider.EarthquakeData.LON + " < " + ((Integer)cornerCoords[1].getLongitudeE6()).toString() + ")";
}

Projection p = mapView.getProjection();
Cursor cursor = null;
Paint paint = new Paint();
paint.setStrokeWidth(0);
paint.setStyle(Paint.Style.STROKE);
try {
cursor = mapView.getContext().getContentResolver().query(EarthquakeContentProvider.EarthquakeData.CONTENT_URI, null, selection, null, null);
if (cursor != null) {
cursor.moveToFirst();
while (!cursor.isAfterLast()){
GeoPoint gp = MapUtils.geopointFromLatLongE6(cursor.getInt(EarthquakeContentProvider.EarthquakeData.LAT_CN), cursor.getInt(EarthquakeContentProvider.EarthquakeData.LON_CN));
Point point = p.toPixels(gp, null);
Float magnitude = cursor.getFloat(EarthquakeContentProvider.EarthquakeData.MAGNITUDE_CN);
canvas.drawPoint(point.x, point.y, paint);
int zoomLevel = mapView.getZoomLevel() + 1;
for (int i = 0; i < magnitude; i++){
canvas.drawCircle(point.x, point.y, i * zoomLevel, paint);
}
cursor.moveToNext(); }
}
} finally {
if (cursor != null){
cursor.close();
}
}
}
}

How to get the data from USGS follows in one of the next posts...

Monday, 3 November 2008

An Editable Waypoint Layer

A GPS without waypoints is like an Android without Activities, so here I illustrate what needs to be done to add an editable waypoint layer to Android.

First of all we need a waypoint content provider, which is just a wrapper around a database table. I am not planning to elaborate on this here as it is just another content provider.

We start with the map. The idea is just to tap on the map at the location where we want to position the waypoint. However, this conflicts with the usual use of touch to pan the map around. I worked around this by having a button on the map that puts the map in different modes. One is the panning mode, another the editing mode for (right now just) adding waypoints. At startup the map is in panning mode. Once a user clicks the button it toggles into editing mode and the next touch will create a waypoint and start the waypoint editor. This is how this looks:

This is all easily done within the onClick() method on the map:

protected synchronized void setDrawingMode(boolean drawing){
Button button = (Button)findViewById(R.id.modeButton);
if (!drawing){
drawingMode = false;
button.setText("Add Waypoint");
} else {
drawingMode = true;
button.setText("Pan");
}
}

@Override
public void onClick(View v) {
setDrawingMode(!drawingMode);
}


Now my onTouch() method distinguishes between the drawing and panning mode and when in editing mode takes the current location and call the waypoint editor (which is hard-coded here, would be better with intent negotation):

public boolean onTouch(View v, MotionEvent event){
if (drawingMode){
setDrawingMode(false);
MapView mv = (MapView)v;
Projection p = mv.getProjection();
GeoPoint point = p.fromPixels(Math.round(event.getX()), Math.round(event.getY()));
ComponentName comp = new ComponentName(this.getPackageName(), WaypointEditor.class.getName());
Intent intent = new Intent().setComponent(comp);
intent.putExtra(WaypointEditor.LAT, point.getLatitudeE6());
intent.putExtra(WaypointEditor.LON, point.getLongitudeE6());
startActivity(intent);
return true;
} else {
return false;
}
}

You might have noticed that I set the drawing mode to false as soon as the onTouch() call happens. This is because quite often I get more than one touch event from what my clumsy fingers believe is a single tap. We catch only the first, the last ones will be translated into panning events again.

I will not show the waypoint editor here, but waypoints are shown through an ItemizedOverlay, more or less as it comes out of the box. But waypoints should be editable as well, so I override the onTap() method on the ItemizedOverlay:

@Override
protected boolean onTap(int index) {
Waypoint i = items.get(index);
ComponentName comp = new ComponentName(WaypointEditor.class.getPackage().getName(), WaypointEditor.class.getName());
Intent intent = new Intent().setComponent(comp);
intent.putExtra(WaypointEditor.ID, i.id());
context.startActivity(intent);
return true;
}