我在哪里?






4.76/5 (30投票s)
一个无需GPS和互联网,在没有GPS和互联网的情况下进行路线位置跟踪的Windows Mobile应用程序。
 
 
引言
“我在哪里”(Wami)是一套Windows Mobile应用程序套件,它可以在旅途中,无需GPS和互联网连接,利用小区广播和小区基站信息以及预先记录的路线文件来告诉你所在位置。它的工作原理是沿途记录小区广播和基站信息到一个路线文件中,然后利用当前的小区广播/基站信息在路线文件中进行索引,找出路线中的相对位置。
动机
我经常回老家,通常会乘坐夜间火车,喜欢在9-10小时的旅程中睡觉。如果在旅途中醒来,我无法知道自己身在何处以及离老家还有多长时间——我必须等到下一个主要枢纽才能知道。Wami就是为了解决这个问题而开发的。现在,通过记录我的火车路线,我只需看一眼手机就能知道我的位置和预计到达目的地的时间。在此过程中,我还添加了自动回复配置号码的短信功能,提供位置和预计到达时间信息,以及在接近目的地时发出警报的功能。
使用Wami
Wami 套件包含三个可执行文件。
- RouteLogger - 在手机上运行,并将所有小区广播和基站信息记录到一个路线文件中。关闭应用程序会将被记录的路线保存到Wami将要探测的位置。  
- Wami - 主应用程序,读取由RouteLogger记录的路线文件,并显示当前位置和预计到达目的地的时间。启动时,Wami会显示由Route Logger记录的路线文件列表(来自Application Data\wami\Routes),您可以从中选择合适的路线。然后Wami加载路线文件并显示以下屏幕。  
- RouteEditor - 桌面应用程序,用于编辑路线文件。编辑RouteLogger生成的路线文件的最佳方法是:- 清除所有现有位置组。
- 添加新的位置组,并赋予有意义的名称。
- 通过点击“向右扩展”(Alt+r)或“向左扩展”(Alt+e)来扩展位置组覆盖的基站范围。
- 继续添加新的位置组,直到覆盖所有位置。
- 保存修改。
 

如果你是第一次旅行,请启动RouteLogger并保持运行,直到到达目的地。RouteLogger使用手机运营商广播的文本将名称与基站关联,但这可能并不总是准确或有用的。RouteEditor允许你编辑路线文件,以重命名位置并将其分组为更有意义的位置组。
从下次开始,启动Wami并加载相应的路线文件,就这么简单。Wami甚至不需要开启小区广播也能工作,它可以结合路线文件中的信息和基站信息来为您提供位置名称。
工作原理
Wami 套件的核心是 wamilib.dll,这是一个包含套件中所有核心数据结构和类的程序集。
 
 
LocationChangeNotifier
此类触发位置更改事件,将当前小区广播文本和基站ID值作为事件参数。它通过以下方式实现:
- 订阅PhoneCellBroadcast系统状态的Changed事件。cellBroadcastChanged = new SystemState(SystemProperty.PhoneCellBroadcast, true); cellBroadcastChanged.Changed += new ChangeEventHandler(cellBroadcastChanged_Changed); 
- 运行一个定时器,定期从无线接口层(RIL)获取基站信息。RIL没有托管接口,也没有我们可以订阅的良好事件。我们必须自己P/Invoke这些函数,并运行一个定时器来判断基站是否发生变化。
 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 the RIL call succeeds, wait for the result, otherwise don't bother. if (result.ToInt32() >= 0) { cellTowerDataReturned.WaitOne(); } return currentCellId; } finally { if (handleToRIL != IntPtr.Zero) { RIL_Deinitialize(handleToRIL); } } }代码之所以这么长,是因为 RIL_GetCellTowerInfo方法是异步的——操作系统通过RIL_Initialize方法中传入的RILResultCallback委托进行回调。我尝试通过只调用一次RIL_Initialize并存储返回的句柄来优化此过程,但这导致了一些奇怪的问题,例如回调偶尔不运行并导致WaitOne调用无限等待。
RouteTracker
RouteTracker订阅来自LocationChangeNotifier的原始通知,并使用路线文件中的信息查找给定小区广播文本和基站ID的Location对象,将其转换为更高级别的LocationChanged事件。它还跟踪路线上的当前位置。
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和Route
RouteManager作为加载和保存路线的单点接口。Route是RoutePoint的集合,每个路线点代表路线上的一个独特位置。RoutePoint还携带了从上一个路线点到达当前路线点所需的时间,因此查找到达目的地的时间就变成了从当前点到目的地点的所有此类时间跨度相加的简单事情。
地点和地点组
一个LocationGroup(位置组)仅仅是一堆以可识别名称分组的地点。例如,像旧金山、弗里蒙特和萨克拉门托这样的地点可以归类到加利福尼亚。存在LocationGroups是因为任何像样的城市,至少在印度,都有多个基站。这些基站显然会有不同的基站ID/广播文本,将它们组织成一个单一的可识别实体,比如金奈,会很有帮助。
自动短信回复
自动短信回复功能让您选择的人知道您的行踪。配置自动短信允许您添加联系人并指定特定消息。Wami只会在收到来自您选择的联系人的短信且消息文本与配置的消息匹配时,才会回复您的位置和预计到达目的地的时间。SMSHandler类处理对短信请求的回复。它使用Compact Framework中的MessageInterceptor类监听与配置消息正文文本相同的传入消息。
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);
}
不幸的是,MessageInterceptor没有提供查找特定联系人的方法,所以这由NotificationManager处理。NotificationManager维护一个触发器列表和相应的通知——接收到带有配置正文文本的短信是一个触发器,该触发器使其执行相应的通知动作,即回复短信并附带当前跟踪信息。
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);
}
基于位置的闹钟
CustomSoundPlayer类用于在设备上播放声音文件。它通过 P/Invoke 调用 aygshell.dll 中的方法来异步启动和停止播放声音。
public static class CustomSoundPlayer
{
        static IntPtr soundHandle;
        static object lockObject = new object();
        public static event EventHandler PlayingSound;
        public static event EventHandler 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());
        }
        // Not threadsafe
        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);
}  
LocationAndETAWatcher类订阅RouteTracker的LocationChanged和TimeChanged方法,并提供在适当的位置/时间发生变化时执行单次操作的能力。基于位置的闹钟功能通过将CustomSoundPlayer的PlaySound连接到LocationAndETAWatcher来实现。用户可以配置Wami在以下情况下发出警报:
- 到达目的地的时间小于配置值。
- 当前位置组等于配置值。
LocationAndETAWatcher可以同时观察位置变化和预计到达时间变化,因此UI只需为上述两个条件创建适当的谓词,并设置LocationAndETAWatcher来执行CustomSoundPlayer的PlaySound方法。
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));
}
挑战
我不得不说,为移动设备编写UI应用程序比为桌面编写要困难得多。除了空间和控件可用性的明显限制外,还需要考虑方向和截然不同的宽高比。这不是我喜欢做的事情。另一个困难的事情是对代码进行单元测试。如果没有模拟,我现在就已经破产了,为了测试自动短信回复功能,我从手机上发送了数千条短信:)。我尽可能地模拟了一切,试图避免在手机上运行时发现错误。蜂窝模拟器也帮了大忙。
参考文献
历史
- 2009年1月14日下午3:48 - 初次提交
- 2009年1月15日上午11:26 - 更新引用并调整类图大小以避免水平滚动


