65.9K
CodeProject 正在变化。 阅读更多。
Home

如何查找任何智能手机的 GPS 位置,然后使其相关

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (120投票s)

2008 年 12 月 22 日

CPOL

10分钟阅读

viewsIcon

809393

downloadIcon

11126

一个逐步教程,介绍如何从任何智能手机获取 GPS,即使没有内置 GPS,然后使位置有用。

目录

引言

前段时间,我在另一篇文章中提到,我将把位置广播的想法发展成一个移动解决方案作为后续。当时的问题是我无法从手机上获取位置数据,也无法使其变得有用。现在时过境迁!在这篇文章中,我将演示如何获取手机的 GPS 坐标…

即使您的手机没有内置 GPS(就像我的)。

为什么位置定位很困难?

手机技术正在以惊人的速度成熟。目前市面上大部分手机都内置了 GPS。尽管如此,“智能”手机的平均价格高达数百美元,而我们大多数人还没有冲动购买一部新手机(尤其是在撰写本文时,经济形势不容乐观)。

GPS 尚未普及是位置感知开发面临的真正难题。目标是获取任何手机的位置,而不仅仅是支持 GPS 的手机。这其中就存在困难。

谷歌能做到,为什么我们不行?

如果您还没有玩过 Google 移动地图,我强烈推荐您尝试一下。它确实是一个方便的小应用程序,尤其是在您出行时需要地图(对我来说,这种情况经常发生)。但是,移动地图在我的手机上却能正常工作,而我的手机没有 GPS。这怎么可能?谷歌通过其移动地图,利用常见的手机数据,找到了在没有 GPS 的情况下确定您位置的方法。这意味着在没有 GPS 的情况下也可以定位。事实上,让我们通过 C# 来拆解和重建谷歌移动地图的工作原理,开始我们的项目。

DeepCast/google_mobile_maps.png

协议分析?

本文不打算详细介绍协议或数据包分析器(我使用的是 Microsoft Network Monitor)。 suffice to say that the first step to deconstructing Google Mobile Maps is to analyze HTTP requests while Mobile Maps is running through ActiveSync.

快速查看移动地图的内部工作原理,会发现几件事。首先,谷歌正在向 http://www.google.com/glm/mmap 发送请求以获取位置数据。我已经仔细研究了它们的 API,但这似乎并不是公开的。这意味着,我们需要自己弄清楚如何打包和发送数据。其次,很明显它们发送了四个关键数据

  • 蜂窝基站 ID
  • 位置区域码 (LAC)
  • 移动网络代码 (MNC)
  • 移动国家代码 (MCC)

这真是个好消息! 实际上,几乎所有手机都能轻松获取这四条数据。Windows Mobile (5 和 6) 有一个称为无线电接口层 (Radio Interface Layer) 的原生 API,可以获取这四条数据。

蜂窝基站数据 - 无线电接口层

我们需要深入研究 Windows Mobile 无线电接口层来获取蜂窝基站数据。别担心,这并没有您想象的那么复杂。另外,您可以在 MSDN 上搜索 RIL 来获取整个 RIL 库的详细信息。它有非常完善的文档。

DeepCast/ril.gif

我们只对 RIL 的蜂窝基站部分感兴趣。我们需要做的第一件事是添加必要的 PInvoke DLL 导入签名,以调用无线电接口层。

[DllImport("ril.dll")] 
private static extern IntPtr RIL_Initialize(uint dwIndex, RILRESULTCALLBACK pfnResult, 
        RILNOTIFYCALLBACK pfnNotify, uint dwNotificationClasses, 
        uint dwParam, out IntPtr lphRil);
[DllImport("ril.dll", EntryPoint = "RIL_GetCellTowerInfo")]
private static extern IntPtr RIL_GetCellTowerInfo(IntPtr hRil);
[DllImport("ril.dll", EntryPoint = "RIL_Hangup")]
private static extern IntPtr RIL_Hangup(IntPtr hRil);
[DllImport("ril.dll")]
private static extern IntPtr RIL_Deinitialize(IntPtr hRil);

ril.dll 中包含的这四个方法是我们访问蜂窝基站数据的入口。通过这些方法以及它们支持的 RILRESULTCALLBACKRILNOTIFYCALLBACK 结构,我们可以轻松地调用 Windows 来获取我们的蜂窝基站数据。

public static CellTower GetCellTowerInfo()
{
    IntPtr radioInterfaceLayerHandle = IntPtr.Zero;
    IntPtr radioResponseHandle = IntPtr.Zero;

    // Initialize the radio layer with a result callback parameter.
    radioResponseHandle = RIL_Initialize(1, new RILRESULTCALLBACK(CellDataCallback), 
        null, 0, 0, out radioInterfaceLayerHandle);

    // The initialize API call will always return 0 if initialization is successful.
    if (radioResponseHandle != IntPtr.Zero)
    { 
        return null;
    }

    // Query for the current tower data.
    radioResponseHandle = RIL_GetCellTowerInfo(radioInterfaceLayerHandle);

    // Wait for cell tower info to be returned since RIL_GetCellTowerInfo invokes the
    // callback method asynchronously.
    waithandle.WaitOne();

    // Release the RIL handle
    RIL_Deinitialize(radioInterfaceLayerHandle);

    // Convert the raw tower data structure data into a CellTower object
    return new CellTower()
    {
        TowerId = Convert.ToInt32(_towerDetails.dwCellID),
        LocationAreaCode = Convert.ToInt32(_towerDetails.dwLocationAreaCode),
        MobileCountryCode = Convert.ToInt32(_towerDetails.dwMobileCountryCode),
        MobileNetworkCode = Convert.ToInt32(_towerDetails.dwMobileNetworkCode),
    };
}

这很容易,多亏了 MSDN!现在,我们可以调用 GetCellTowerInfo(),并将返回一个强类型 CellTower 类,其中包含所有基站的详细信息。我一直 Prefer to work in a strongly typed representation of PInvoke output instead of marshaling pointers.

*注意:此代码在模拟器中无法正常工作,除非您也配置并运行了蜂窝模拟器。我建议使用您的手机而不是模拟器。

蜂窝数据服务

太好了,我们有了蜂窝基站数据,但接下来呢?我们仍然没有 GPS 坐标。我们知道谷歌正在进行这种从蜂窝数据到经纬度的转换,让我们继续深入分析谷歌移动地图。再次查看其内部工作原理,会发现它们正在向上述 URL 发送二进制数据。同样,协议分析超出了本文的范围。您会发现,我们需要将数据打包成一个 55 字节的数组。大部分字节是空的 (0),但我们需要填充一些关键项。下面是创建和填充带有蜂窝基站数据的字节数组的代码

private static byte[] GetFormPostData(int cellTowerId, int mobileCountryCode, 
        int mobileNetworkCode, int locationAreaCode)
{
    byte[] pd = new byte[55];
    pd[1] = 14;     //0x0e;
    pd[16] = 27;    //0x1b;
    pd[47] = 255;   //0xff;
    pd[48] = 255;   //0xff;
    pd[49] = 255;   //0xff;
    pd[50] = 255;   //0xff;

    // GSM uses 4 digits while UTMS used 6 digits (hex)
    pd[28] = ((Int64)cellTowerId > 65536) ? (byte)5 : (byte)3; 

    Shift(pd, 17, mobileNetworkCode);
    Shift(pd, 21, mobileCountryCode);
    Shift(pd, 31, cellTowerId);
    Shift(pd, 35, locationAreaCode);
    Shift(pd, 39, mobileNetworkCode);
    Shift(pd, 43, mobileCountryCode);

    return pd;
}

/// <summary>
/// Shifts specified data in the byte array starting at the specified array index.
/// </summary>
/// <param name="data">The data.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="leftOperand">The left operand.</param>
private static void Shift(byte[] data, int startIndex, int leftOperand)
{
    int rightOperand = 24;

    for (int i = 0; i < 4; i++, rightOperand -= 8)
    {
        data[startIndex++] = (byte)((leftOperand >> rightOperand) & 255);
    }
}

简而言之,我们可以传入四个参数:基站 ID、LAC、MNC 和 MCC,然后得到一个字节数组,我们可以通过 HTTP 发送给谷歌。下面的代码演示了整个 HTTP 请求,它获取我们强类型的蜂窝基站数据(来自上面的 PInvoke)并从谷歌数据库返回经纬度!

internal static GeoLocation GetLocation(CellTower tower)
{
    try
    {
        // Translate cell tower data into http post parameter data
        byte[] formData = GetFormPostData(tower.TowerId,
        tower.MobileCountryCode,
        tower.MobileNetworkCode,
        tower.LocationAreaCode);

        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
            new Uri(Google_Mobile_Service_Uri));
        request.Method = "POST";
        request.ContentLength = formData.Length;
        request.ContentType = "application/binary";

        Stream outputStream = request.GetRequestStream();

        // Write the cell data to the http stream
        outputStream.Write(formData, 0, formData.Length);
        outputStream.Close();

        HttpWebResponse response = (HttpWebResponse)request.GetResponse();

        return ReadResponse(response);
    }
    catch{}

    return GeoLocation.Empty;
}

private static GeoLocation ReadResponse(HttpWebResponse response)
{
    byte[] responseData = new byte[response.ContentLength];

    int bytesRead = 0;

    // Read the response into the response byte array
    while (bytesRead < responseData.Length)
    {
        bytesRead += response.GetResponseStream()
            .Read(responseData, bytesRead, responseData.Length - bytesRead);
    }

    // Check the response
    if (response.StatusCode == HttpStatusCode.OK)
    {
        int successful = Convert.ToInt32(GetCode(responseData, 3));

        if (successful == 0)
        {
            return new GeoLocation()
            {
                Latitude = GetCode(responseData, 7) / 1000000,
                Longitude = GetCode(responseData, 11) / 1000000
            };
        }
    }

    return GeoLocation.Empty;
}

/// <summary>
/// Gets the latitude or longitude from the byte array.
/// </summary>
/// <param name="data">The byte array.</param>
/// <param name="startIndex">The start index.</param>
/// <returns></returns>
private static double GetCode(byte[] data, int startIndex)
{
    return ((double)((data[startIndex++] << 24) |
        (data[startIndex++] << 16) |
        (data[startIndex++] << 8) |
        (data[startIndex++])));
}

简要介绍蜂窝基站

让我花点时间描述一下刚才发生的事情。我们从操作系统读取了手机的蜂窝基站数据,通过 HTTP 发送给谷歌,然后得到了经纬度。这怎么可能?

美国联邦通信委员会 (FCC) 保留了美国每个蜂窝基站的记录。谷歌并没有做什么(太)神奇的事情。它们只是查找我们提交的基站的详细信息,并以经纬度格式返回基站的已知位置。

然而,谷歌并不是唯一一个拥有基站位置数据库的。 OpenCellID.org 是一个开源项目,旨在绘制所有蜂窝基站及其 GPS 坐标的地图。雅虎也在使用其 ZoneTag 服务进行同样的操作。

为什么费尽周折使用谷歌,如果还有更简单的替代方案?

这是个好问题!简单来说,谷歌的数据库是最全面的。OpenCellID 和 ZoneTag 较新,它们的数据库不完整。即使在我家(一个主要城市郊区)也无法从这些“测试版”服务中获得有效的 GPS 结果……但谷歌在我测试过的所有地方都有效。

如果我的手机有 GPS 怎么办?

当然,蜂窝基站数据不如 GPS 精确。您无法通过蜂窝基站数据判断您离基站是 10 英尺还是 1000 英尺。换句话说,如果 GPS 可用,最好还是使用 GPS,因为它精度高。当然也有例外,比如在室内无法获得 GPS 信号时,您很可能仍然有蜂窝连接。

从兼容的手机获取 GPS 数据比获取蜂窝基站数据更容易。更好的是——微软已经分发了一个库来处理所有繁重的工作。我们只需要订阅微软 GPS 类检测到位置变化时触发的事件。剩下的事情微软已经做好了。

private Gps _gps = new Gps();
private GpsPosition _currentPosition;

public void Start()
{
    _gps.LocationChanged += new 
      Microsoft.Location.LocationChangedEventHandler(_gps_LocationChanged);

    if (!_gps.Opened)
    {
        _gps.Open();
    }
}

private void _gps_LocationChanged(object sender, 
        Microsoft.Location.LocationChangedEventArgs args)
{
    _currentPosition = args.Position;
}

正如 Staples 的人所说,“太容易了。” DeepCast/easy-button.jpg

圣杯?

到目前为止,我们已经建立了两种机制来获取手机的经纬度。我们现在拥有与谷歌地图相媲美的潜力!似乎到了可以跳胜利舞、提交代码、喝一杯蛋酒(在我写作时,距离圣诞节还有 5 天)的时候了。

但是,知道自己的位置有什么用呢?

我通常知道我在哪里,所以知道我的位置对我帮助不大。谷歌移动地图等应用程序已经显示了我的位置,但它们并没有做任何其他事情。我就是觉得文章不能到此结束……还没完。

在我看来,基于位置服务的真正价值在于共享您的位置,而不仅仅是知道您的位置。这是社交网络中必然会发生的下一步。

利用位置信息

我坚信创造力,而且有很多开发者比我更有创造力。我将稍微改变方向,尝试提供创造性的影响,而不是具体的解决方案。为什么不让本文的其余部分成为您的移动位置灵感呢?应该想出下一个关于如何利用您位置的绝妙主意。

我的第一个创意催化剂是更新您的 Yahoo! Fire Eagle 帐户,提供您的经纬度。Fire Eagle 是雅虎的一项新服务,它接收您的位置并将其“广播”给您订阅的应用程序(在其快速增长的列表中)。

实际上,这就是本项目命名为 DeepCast 的原因。我们将位置广播到(潜在的)深度服务和提供商列表。不只是广播…而是深度广播 =)。

Fire Eagle

Fire Eagle 使用 Oauth 作为其授权平台。我对 Oauth 的所有细节并不熟悉。我所知道的是,在任何应用程序可以进行更新之前,都需要一个手动授权过程。

Oauth 身份验证的第一步是生成一个请求令牌和密钥对。

DeepCast/dc2.png

然后,您可以通过(桌面浏览器)访问授权 URL。

DeepCast/dc3.png

您必须手动确认应用程序可以更新您的个人资料。

DeepCast/yfe.png

最后,您必须将请求令牌和密钥换成授权令牌和密钥。这些是您每次应用程序尝试更新您的位置时需要保存和使用的值。

DeepCast/dc4.png

现在您拥有了一个授权令牌和密钥对,您可以随时更新 Fire Eagle。Fire Eagle 将把您的新位置广播给您订阅的底层应用程序。

Twitter

我从来都不是 Twitter 的忠实粉丝。它需要太多的工作才能获得太少的信息(仅为个人观点……)。但是,如果 Twitter 能够告诉我人们在哪里呢?!

如果我们能发布一条包含指向显示您位置的谷歌地图链接的推文呢?这似乎有一定用处。

DeepCast/dc5.png

在更新 Twitter 方面几乎没有什么可以描述的。它就像发起一个 HTTP 请求一样简单。唯一的困难是添加超链接到消息。Twitter 使用一种 URL 编码形式,该形式会截断大多数查询字符串。我们将生成一个 TinyUrl 来解决这个问题。

以下是更新 Twitter 的方法

internal override void Update(GeoLocation location)
{
    if (!string.IsNullOrEmpty(Settings.TwitterEmail) && 
        !string.IsNullOrEmpty(Settings.TwitterPassword))
    {
        string googleMapLink = string.Format(Google_Map_Uri, 
                               location.Latitude, location.Longitude);
        string tilyMapLink = TinyUrlService.GetTinyUrl(googleMapLink);
        string place = string.Format("I'm here: {0} - 'L:{1}, {2}'", 
            new object[] { tilyMapLink, location.Latitude, location.Longitude });

        string data = string.Format("status={0}", place);

        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Twitter_Api_Uri);
        request.Credentials = new NetworkCredential(Settings.TwitterEmail, 
                                  Settings.TwitterPassword);
        request.ContentType = "application/x-www-form-urlencoded";
        request.Method = "POST";
        request.AllowWriteStreamBuffering = true;

        byte[] bytes = Encoding.UTF8.GetBytes(data);

        request.ContentLength = bytes.Length;

        using (Stream requestStream = request.GetRequestStream())
        {
            requestStream.Write(bytes, 0, bytes.Length);
            requestStream.Flush();
            requestStream.Close();

            using (WebResponse response = request.GetResponse())
            {
                using (StreamReader reader = 
                    new StreamReader(response.GetResponseStream()))
                {
                    reader.ReadToEnd();
                }
            }
        }
    }
}

DeepCast/twt.png

请注意,有一个位置约定,“L: lat, lon”,它具有更深层的目的。TwitterMap.com 的人可以解析这种格式。再次,我们试图让尽可能多的受众能够利用位置信息。

内部服务

当然,共享您的位置并不是使信息有用的唯一方法。该项目已以一种方式构建,使得位置既可以用于内部,也可以用于外部。例如,主页利用了外部服务使用的相同位置。主页上的地图只是一个浏览器控件,其 URL 被设置为雅虎地图的图像。简单明了,这正是我想要的!

DeepCast/dc1.png

您的服务就在这里

没错——轮到您了。项目的大部分内容已经为您搭建好了框架,以便您添加自己的位置感知服务。如果您读到了这里,您可能对位置服务感兴趣,所以请随意修改项目并开始编码吧!我鼓励所有读者将此项目作为通往下一个伟大想法的垫脚石。我能想到的一些位置用途包括

  • 通过 Web 服务跟踪您的行程。创建一个后端网站,显示您的行程地图 - 何时何地。绘制您旅行过的历史轨迹。
  • 跟踪您的孩子,以便您随时知道他们在哪里。
  • 为您的照片添加地理标签。例如,Flickr 支持地理标记图像。
  • 记录您的车队在哪里(例如,FedEx 送货员)。
  • 地理博客。
  • 更新您的 Facebook/MySpace/社交网站上的位置。
  • 说真的,现在轮到您了!

感谢您的阅读,祝您编码愉快!

历史

  • 2008 年 12 月 22 日 - 文章提交
  • 2009 年 2 月 8 日 - 整合了所有用户提交的错误修复,修复了 Twitter HTTP 417 异常
    • 非常感谢 pejonsson 的建议!
© . All rights reserved.