Introduction
Where am I (Wami) is a Windows Mobile application suite that tells you where you are in the middle of a trip, without GPS and without connecting to the internet, using cell broadcast and cell tower information and a pre-recorded route file instead. It works by recording cell broadcast and tower information along a route into a route file and then using the current cell broadcast/tower information to index into the route file and find out the relative location within the route.
Motivation
I regularly travel to my hometown and I usually take the night train, preferring to sleep during the 9-10 hour journey. If I woke up during the journey, I had no way of knowing where I was and how long it would take to reach my hometown - I'd have to wait for the next major junction to find that out. Wami was written to solve that problem. With my train route recorded, I can now know my location and the estimated time of arrival at my destination with just a glance at my mobile phone. Along the way, I also added the ability to automatically respond to SMSes from configured numbers with the location and ETA information, and the ability to sound an alarm when I'm close to the destination.
Using Wami
The Wami suite has three executables.
- RouteLogger - Runs on the phone and records all cell broadcast and tower information into a route file. Closing the app saves the recorded route into a location that will be probed by Wami.
- Wami - Main application which reads in the route file recorded by
RouteLogger
and shows you the current location and ETA at destination. When launched, Wami presents you with a list of route files recorded by Route Logger (from Application Data\wami\Routes) from which you can choose the appropriate route. Wami then loads the route file and shows the following screen:
- RouteEditor - Desktop application that lets you edit route files. The best way to edit the route files generated by
RouteLogger
is to:
- Clear All existing location groups.
- Add new location group, with a meaningful name.
- Expand the range of cell towers covered by the location group, by clicking on Expand Right (Alt+r) or Expand Left (Alt+e).
- Keep adding new location groups, until you have covered all locations.
- Save the modifications.
If you are taking a trip for the first time, launch RouteLogger
and keep it running until you reach your destination. RouteLogger
uses text that is broadcast by your cellphone provider to associate a name to a cell tower, and that might not always be accurate or useful. RouteEditor
lets you edit the route file to rename and group locations into more meaningful location groups.
Next time onwards, launch Wami and load the appropriate route file, that's all there is to it. You don't even need to have cell broadcast turned on for Wami to work, it can use the cell tower information in conjunction with the information in the route file to give you location names.
How It Works
The heart of the Wami suite is wamilib.dll, an assembly that houses all the core data structures and classes in the suite.
LocationChangeNotifier
This class triggers location change events, with the current cell broadcast text and the cell tower id values as event parameters. It does this by:
- Subscribing to the
Changed
event of the PhoneCellBroadcast
system state.
cellBroadcastChanged = new SystemState(SystemProperty.PhoneCellBroadcast, true);
cellBroadcastChanged.Changed +=
new ChangeEventHandler(cellBroadcastChanged_Changed);
- Running a timer that periodically gets cell tower information from the Radio Interface Layer (RIL). There is no managed interface to the RIL, no nice event that we can subscribe to. We'll have to P/Invoke the functions ourselves, and run a timer to know if the cell tower changed.
private string GetCellTowerId()
{
IntPtr handleToRIL = IntPtr.Zero;
try
{
var result = RIL_Initialize(1, new RILRESULTCALLBACK(RILResultCallback),
null, 0, 0, out handleToRIL);
if (result != IntPtr.Zero || handleToRIL == IntPtr.Zero)
return "";
result = RIL_GetCellTowerInfo(handleToRIL);
if (result.ToInt32() >= 0)
{
cellTowerDataReturned.WaitOne();
}
return currentCellId;
}
finally
{
if (handleToRIL != IntPtr.Zero)
{
RIL_Deinitialize(handleToRIL);
}
}
}
The code is this long because the RIL_GetCellTowerInfo
method is asynchronous - the OS calls back using the RILResultCallback
delegate passed to it in the RIL_Initialize
method. I tried to optimize this by calling RIL_Initialize
only once and storing the returned handle, but that caused weird issues like the callback occasionally not running and causing an infinite wait on the WaitOne
call.
RouteTracker
RouteTracker
subscribes to the raw notifications from LocationChangeNotifier
and translates it into a higher level LocationChanged
event, using the information from the route file to look up Location
objects for the given cell broadcast text and tower ids. It also keeps track of the current location along the route.
private void ProcessCellLocationChange(string newLocationName, string cellTowerId)
{
newLocationName = newLocationName.Trim();
currentLocationName = newLocationName;
if (currentRoute != null)
{
Location location =
GetLocationForNameAndCellTowerId(newLocationName, cellTowerId);
if (location != null)
{
ProcessKnownLocation(location);
}
else
{
ProcessUnknownLocation(newLocationName);
}
}
}
RouteManager, RoutePoint and Route
RouteManager
acts as a single point interface for loading and saving routes. A Route
is a collection of RoutePoint
s, with each route point representing a distinct location along the route. The RoutePoint
also carries the time taken to reach the current route point from the previous route point, so finding the time to destination becomes the simple matter of adding all such time spans from the current point to the destination point.
Location and LocationGroups
A LocationGroup
is simply a bunch of locations grouped under a recognizable name. For e.g. locations like San Francisco, Fremont and Sacramento can be grouped under California. LocationGroup
s exist because any decent city, at least in India, has multiple cell towers. Those towers will obviously have different cell ids/broadcast text, and it helps to organize them into a single identifiable entity, say Chennai.
Auto SMS Response
The Auto SMS response feature lets people chosen by you know your whereabouts. The Configure Auto SMS allows you to add contacts and specify a specific message. Wami will respond with your location and ETA to destination only if an incoming SMS message is from one of your chosen contacts and the message text matches the configured message. The SMSHandler
class handles response to SMS requests. It uses the MessageInterceptor
class from the Compact Framework to listen for incoming messages with the same body text as the configured message.
private void InitializeInterceptor()
{
if (interceptor == null)
{
interceptor = new MessageInterceptor(InterceptionAction.NotifyAndDelete, true);
}
else
{
interceptor.Dispose();
interceptor = new MessageInterceptor(InterceptionAction.NotifyAndDelete, true);
}
if (currentConfiguredMessage != null)
{
interceptor.MessageCondition = new MessageCondition
(MessageProperty.Body, MessagePropertyComparisonType.Equal,
currentConfiguredMessage, false)
}
if (interceptor != null)
interceptor.MessageReceived += new MessageInterceptorEventHandler
(interceptor_MessageReceived);
}
The MessageInterceptor
unfortunately does not provide a way to look for specific contacts, so that's handled by NotificationManager
. NotificationManager
maintains a list of triggers and the corresponding notifications - receiving an SMS with the configured body text is a trigger, and that trigger causes it to execute the corresponding notification action, i.e. replying to the SMS with the current tracking information.
private void ProcessTrigger(NotificationTrigger trigger)
{
List<string> usersToNotify = new List<string>();
foreach (var pair in userPredicateMap)
{
if (pair.Value(trigger))
{
usersToNotify.Add(pair.Key);
}
}
if (usersToNotify.Count == 0)
return;
var message = GetMessage();
if (message != null)
notifier.Notify(usersToNotify.ToArray(), message);
}
Location Based Alarm
The CustomSoundPlayer
class is used to play sound files on the device. It P/Invokes methods on aygshell.dll to asynchronously start and stop playing sounds.
public static class CustomSoundPlayer
{
static IntPtr soundHandle;
static object lockObject = new object();
public static event EventHandler<eventargs> PlayingSound;
public static event EventHandler<eventargs> StoppedPlaying;
public static void PlaySound(string path)
{
lock (lockObject)
{
if (soundHandle != IntPtr.Zero)
return;
SndOpen(path, ref soundHandle);
SndPlayAsync(soundHandle, 0x1);
}
if (PlayingSound != null)
PlayingSound(new object(), new EventArgs());
}
public static void StopPlaying()
{
lock (lockObject)
{
if (soundHandle == IntPtr.Zero)
return;
SndStop((int)SndScope.Process, IntPtr.Zero);
SndClose(soundHandle);
soundHandle = IntPtr.Zero;
}
if (StoppedPlaying != null)
StoppedPlaying(new object(), new EventArgs());
}
[DllImport("aygshell.dll")]
internal static extern uint SndOpen(string file, ref IntPtr phSound);
[DllImport("aygshell.dll")]
internal static extern uint SndPlayAsync(IntPtr hSound, uint flags);
[DllImport("aygshell.dll")]
internal static extern uint SndStop(int soundScope, IntPtr hSound);
[DllImport("aygshell.dll")]
internal static extern uint SndClose(IntPtr hSound);
}</eventargs></eventargs>
The LocationAndETAWatcher
class subscribes to LocationChanged
and TimeChanged
methods of RouteTracker
and provides the ability to execute single shot actions when the appropriate location/time change occurs. The location based alarm feature works by hooking up CustomSoundPlayer
's PlaySound
to LocationAndETAWatcher
. The user can configure Wami to sound an alarm if:
- Time to destination becomes lesser than a configured value.
- Current location group becomes equal to a configured value.
LocationAndETAWatcher
can watch both location changes and ETA changes, so the UI simply creates appropriate predicates for the above two conditions and sets up LocationAndETAWatcher
to execute CustomSoundPlayer
's PlaySound
method.
watcher.Initialize(routeTracker);
if (!string.IsNullOrEmpty(s.SoundPlayLocationGroupName))
{
watcher.AddSingleShotActionForLocationChange
(locationGroup => locationGroup.Name == s.SoundPlayLocationGroupName,
() => PlaySound(s.SoundFilePath));
}
var timeSpanToDestinationConfiguration = s.TimeSpanToDestination;
if (timeSpanToDestinationConfiguration != TimeSpan.Zero)
{
watcher.AddSingleShotActionForETA
(liveTimeSpanToDestination => liveTimeSpanToDestination <=
timeSpanToDestinationConfiguration, () => PlaySound(s.SoundFilePath));
}
Challenges
I have to say that writing a UI application for mobile devices is much more difficult than writing one for a desktop. Besides the obvious limitation in space and availability of controls, there's also orientation and wildly different aspect ratios to consider. Not something I enjoyed doing. The other hard thing is unit testing of the code. Without mocking, I'd be broke by now, having sent thousands of SMS messages from my phone to test the Auto SMS response feature:). I mocked everything I could, trying to avoid finding bugs when running on the phone. Cellular Emulator helped a great deal too.
References
History
- 3:48 PM 1/14/2009 - Initial submission
- 11:26 AM 1/15/2009 - Updated references and resized class diagram to avoid horizontal scrolling