Skip to main content

使用 XIAO ESP32S3 的地理位置追踪器

市面上的自制追踪器经常面临几个问题。例如,追踪精度差、体积庞大以及对追踪器使用环境要求苛刻。XIAO 凭借其出色的体积控制吸引了众多创客。那么我们能否仅使用 XIAO 来制作一个可以全球定位的追踪器呢?

在本教程中,我们将探索两种更受欢迎的方式来部署 XIAO(不使用 GPS 模块)来创建一个令人惊喜的追踪器。

概述

本文将介绍两种定位方式,一种是从 XIAO 连接的网络 IP 地址获取位置信息来实现定位。另一种是使用 Wi-Fi 定位系统(通常称为 WiPS 或 WFPS)。

  • 方法 1:通过 IP 地址定位

IP 地址定位的原理涉及使用将 IP 地址映射到物理位置的数据库。这个过程通常被称为地理定位。

IP 地址查找涉及使用反向 DNS 查找来检索与 IP 地址关联的域名。然后可以使用域名来识别托管网站或服务的服务器的地理位置。

IP 地址映射涉及使用将 IP 地址映射到物理位置的数据库。该数据库可能包含与每个 IP 地址关联的城市、地区和国家等信息。

IP 地址地理定位的准确性可能因使用的方法和可用数据的质量而有所不同。一般来说,IP 地址地理定位对于固定设备(如台式计算机和服务器)最为准确,这些设备通常与固定的物理位置相关联。移动设备(如智能手机和平板电脑)可能更难准确定位,因为它们可以移动并连接到不同的 Wi-Fi 网络。

  • 方法 2:通过 WFPS 定位

Wi-Fi 定位系统是一种地理定位系统,它使用附近 Wi-Fi 热点和其他无线接入点的特征来发现设备的位置。

它用于卫星导航(如 GPS)由于各种原因(包括室内多径和信号阻塞)不足的地方,或者获取卫星定位需要太长时间的地方。此类系统包括辅助 GPS、通过热点数据库的城市定位服务和室内定位系统。Wi-Fi 定位利用了 21 世纪初城市地区无线接入点的快速增长。

用于无线接入点定位的最常见和最广泛的定位技术基于测量接收信号强度(接收信号强度指示或 RSSI)和"指纹识别"方法。用于地理定位无线接入点的典型参数包括其 SSID 和 MAC 地址。准确性取决于已将位置输入数据库的附近接入点的数量。Wi-Fi 热点数据库通过将移动设备 GPS 位置数据与 Wi-Fi 热点 MAC 地址相关联来填充。可能发生的信号波动会增加用户路径中的错误和不准确性。为了最小化接收信号中的波动,有一些技术可以应用来过滤噪声。

这是 XIAO 能够在不借助 GPS 模块的情况下获得位置的理论基础。我们还将比较通过上述两种方法获得定位的最佳方式,结合使用圆形显示屏,在屏幕上以地图形式显示坐标。以下是目录和论文摘要。

开始使用

要成功完成这个项目,您可能需要使用以下硬件。支持任何 XIAO ESP32。

Seeed Studio XIAO ESP32S3Seeed Studio XIAO ESP32S3 SenseSeeed Studio Round Display for XIAO

除此之外,您还可以额外准备一个小型锂电池、microSD 卡和外壳。组成一个完整的追踪器形式。

使用 XIAO ESP32S3 连接网络并获取公网 IP

tip

如果您不熟悉 XIAO ESP32S3 的网络功能,可以阅读 Seeed Studio XIAO ESP32S3 (Sense) 的 WiFi 使用

在 XIAO ESP32S3 基本使用的教程中,我们已经掌握了使用 XIAO ESP32S3 连接网络的方法。

#include <WiFi.h>

// For network
const char* ssid = "<YOUR_WIFI_SSID_HERE>";
const char* password = "<YOUR_WIFI_PW_HERE>";

void setup() {
Serial.begin(115200);

Serial.print("Try to connect to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED){
Serial.print(".");
}
Serial.println("Wi-Fi Connected!");
}

void loop() {
}

使用 IP 地址查找位置的核心本质是获取 XIAO 的 IP 地址。然后可能很自然地想到我们需要使用 WiFi.localIP() 函数来查找。

然而,实际上,路由器会为 XIAO 分配一个内部 IP 地址,类似于 192.168.xxx.xxx,这无法查询位置信息。我们需要的是公网 IP。所以我们需要使用以下方法。

// Get local IP address
IPAddress publicIP;
if (WiFi.hostByName("ip-api.com", publicIP)) {
Serial.print("Public IP address: ");
Serial.println(publicIP);
} else {
Serial.println("Failed to get public IP address");
return;
}

然后我们可以得到以下效果,这是第一步。

使用 ipstack 平台获取位置坐标

使用 IP 地址进行定位通常需要一些公共服务器库信息。我们可以借助一些平台的 API 接口来获取这些服务器信息。例如,在本教程中,我们将使用 ipstack 平台。

ipstack 提供了一个强大的实时 IP 地理位置 API,能够查找准确的位置数据并评估来自风险 IP 地址的安全威胁。结果在毫秒内以 JSON 或 XML 格式提供。

ipstack 提供免费/付费搜索服务,价格表 可以在下图中找到。在这个示例中,我们只是展示示例,使用**免费服务(100 次查询/月)**就足够了。

步骤 1. 获取 ipstack API 访问密钥

如果这是您第一次使用 ipstack,那么您需要注册一个新账户

注册并登录后,您将能够看到您的 API 密钥,请复制并保存在安全的地方,我们稍后会使用它。

步骤 2. 学习如何使用 ipstack API

ipstack 提供了详细的文档,解释如何使用 ipstack API。

非常简单,对吧?只需发送服务器地址 + IP 地址 + API 密钥

接下来我们需要知道 ipstack 会返回给我们什么样的 JSON 消息,并提取我们需要的信息,如城市、国家和经纬度。

{
"ip": "134.201.250.155",
"hostname": "134.201.250.155",
"type": "ipv4",
"continent_code": "NA",
"continent_name": "North America",
"country_code": "US",
"country_name": "United States",
"region_code": "CA",
"region_name": "California",
"city": "Los Angeles",
"zip": "90013",
"latitude": 34.0453,
"longitude": -118.2413,
"location": {
"geoname_id": 5368361,
"capital": "Washington D.C.",
"languages": [
{
"code": "en",
...

然后,我们只需要借助 ArduinoJSON 库来提取我们需要的信息。

步骤 3. 通过 http 服务获取 IP 地址的坐标

总结一下,我们首先安装 ArduinoJSON 库。它可以直接从 Arduino IDE 中搜索并下载。

然后我们编写 getLocation() 函数,用于获取 ipstack 返回的国家、城市和经纬度信息,并将它们打印出来。

// For ipstack
const char* IPStack_key = "<YOUR_API_KEY_HERE>";
String ip_address;

// Obtain the approximate coordinate position based on the current IP address of XIAO.
bool getLocation(){
// Make HTTP request to IPStack API
HTTPClient http;
String url = "http://api.ipstack.com/" + String(ip_address) + "?access_key=" + String(IPStack_key);
Serial.println("Requesting URL: " + url);
http.begin(url);
int httpCode = http.GET();

// Parse JSON response
if (httpCode == 200) {
String payload = http.getString();
Serial.println("Response payload: " + payload);
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
String country_name = doc["country_name"].as<String>();
String region_name = doc["region_name"].as<String>();
String city = doc["city"].as<String>();
latitude = doc["latitude"].as<double>();
longitude = doc["longitude"].as<double>();
Serial.println("Country: " + country_name);
Serial.println("Region: " + region_name);
Serial.println("City: " + city);
Serial.println("Latitude: " + String(latitude));
Serial.println("Longitude: " + String(longitude));
http.end(); // Close connection
return true;
} else {
Serial.println("HTTP error code: " + String(httpCode));
http.end(); // Close connection
return false;
}
}

在上述程序中,请将 ipstack API 密钥替换为您自己的。

接下来,我们可以看看通过 IP 地址定位的准确性如何。下面地图上的红色标记点是通过 IP 地址确定的我所在的确切位置。红线的另一端是我实际所在的位置。它们之间相差 2.4 公里。

可以看出,这种定位方式的误差在公里范围内,这远远达不到我们对追踪器的期望。

通过 HTTPS 服务从 Google Maps 下载静态图像

纬度和经度坐标在我们看来并不直观。即使它们包含有关国家和城市的信息。所以我们想知道是否可以在地图上标记这些纬度和经度坐标并在屏幕上显示它们。然后我们找到了 Google Cloud 的地图服务。

在我们开始之前,我认为了解 Google Maps 服务的定价 对您决定是否继续进行很重要。

如果您是首次注册用户,您将获得 $300 免费额度。这里我们主要使用 Maps Static API,每 1000 次调用的费用为 $2.00

步骤 4. 设置您的 Google Cloud 项目 并完成后续的设置说明

步骤 5. 启用 Google Maps API

您需要一个 google API 密钥来验证 Google API。导航到 Google Developers Console 以启用 GeoLocation API。没有此 API 密钥,您将收到错误响应。

一旦您获得了 API,请将其保存在安全的地方,我们将在后续的编程步骤中使用它。

note

如果您对当前使用 API 的环境有担忧,您可以开启对 API 调用的限制,以避免因盗用而产生额外费用。开启某些限制可能需要对您的程序进行更改。

在圆形显示屏上显示位置地图

tip

如果您是第一次使用 XIAO 的圆形显示屏,那么您可能需要参考这里的 Wiki 来为圆形屏幕配置您的 Arduino 环境。

步骤 6. 学习如何调用 Google Cloud Static Maps API

点击这里阅读 Google Cloud Static Maps API 的文档。

文档给出了使用 API 的示例代码如下:

https://maps.googleapis.com/maps/api/staticmap?center=Brooklyn+Bridge,New+York,NY&zoom=13&size=600x300&maptype=roadmap
&markers=color:blue%7Clabel:S%7C40.702147,-74.015794&markers=color:green%7Clabel:G%7C40.711614,-74.012318
&markers=color:red%7Clabel:C%7C40.718217,-73.998284
&key=YOUR_API_KEY&signature=YOUR_SIGNATURE

Maps Static API URL 必须采用以下形式:

https://maps.googleapis.com/maps/api/staticmap?parameters

Maps Static API 使用以下 URL 参数定义地图图像:

  • center(如果不存在标记则为必需)定义地图的中心,与地图所有边缘等距。此参数接受位置作为逗号分隔的 {latitude,longitude} 对(例如 "40.714728,-73.998672")或字符串地址(例如 "city hall, new york, ny"),用于标识地球表面上的唯一位置。
  • zoom(如果不存在标记则为必需)定义地图的缩放级别,这决定了地图的放大级别。此参数接受与所需区域缩放级别对应的数值。
  • size(必需)定义地图图像的矩形尺寸。此参数接受形式为 {horizontal_value}x{vertical_value} 的字符串。
  • maptype(可选)定义要构建的地图类型。有几种可能的 maptype 值,包括 roadmap、satellite、hybrid 和 terrain。
  • markers(可选)定义一个或多个标记以在指定位置附加到图像。此参数接受单个标记定义,参数由管道字符 (|) 分隔。只要它们表现出相同的样式,多个标记可以放置在同一个 markers 参数中;您可以通过添加额外的 markers 参数来添加不同样式的额外标记。请注意,如果您为地图提供标记,则不需要指定(通常必需的)center 和 zoom 参数。
  • key(必需)允许您在 Google Cloud Console 中监控应用程序的 API 使用情况,并确保 Google 在必要时可以联系您的应用程序。
tip

上面只显示了最基本的参数,如果您需要自定义此静态地图,您可以点击**这里**阅读完整的参数列表。

总之,我们可以拼接起来形成一个完整的发送字符串。

// For google static maps
const char * host = "maps.googleapis.com";
const String defaultPath = "/maps/api/staticmap?center=";
const String Googlemaps_key = "<YOUR_API_KEY_HERE>";
int zoomLevel = 14;
double latitude;
double longitude;

// Stitching to form commands sent to Google Maps
String getPath(){
String newPath = defaultPath;
newPath += latitude;
newPath += ",";
newPath += longitude;
newPath += "&zoom=";
newPath += String(zoomLevel);
newPath += "&size=240x240";
newPath += "&maptype=roadmap";
newPath += "&markers=size:tiny%7Ccolor:red%7C";
newPath += latitude;
newPath += ",";
newPath += longitude;
newPath += "&format=jpg-baseline";
newPath += "&key=";
newPath += Googlemaps_key;
Serial.println(newPath);
return newPath;
}

请将上面的代码替换为您自己的 Google Cloud Maps API。

步骤 7. 通过 HTTPS 获取返回的图像并写入 microSD 卡

我们需要一个足够大的存储介质来保存返回的静态图像,以便在屏幕显示程序中读取它们。Round Display 恰好支持 microSD 卡。

// 来自 Google Cloud Services 的坐标静态图像
bool getStaticMapImage(const char *host, const char *path, String fileName){
int contentLength = 0;
int httpCode;

WiFiClientSecure client;

client.setCACert(GlobalSignCA);
client.connect(host, 443);

Serial.printf("Trying: %s:443...", host);

if(!client.connected()){
client.stop();
Serial.printf("*** Can't connect. ***\n-------\n");
return false;
}

Serial.println("HTTPS Connected!");
client.print("GET ");
client.print(path);
client.print(" HTTP/1.0\r\nHost: ");
client.print(host);
client.print("\r\nUser-Agent: ESP32S3\r\n");
client.print("\r\n");

while(client.connected()){
String header = client.readStringUntil('\n');
if(header.startsWith(F("HTTP/1."))){
httpCode = header.substring(9, 12).toInt();
if(httpCode != 200){
client.stop();
return false;
}
}
if(header.startsWith(F("Content-Length: "))){
contentLength = header.substring(15).toInt();
}
if(header == F("\r")){
break;
}

}
if(!(contentLength > 0)){
client.stop();
return false;
}
fs::File f = SD.open(fileName, "w");
if(!f){
Serial.println(F("FILE OPEN FAILED"));
client.stop();
return false;
}
int remaining = contentLength;
int received;
uint8_t buff[512] = {0};
while(client.available() && remaining > 0){
received = client.readBytes(buff, ((remaining > sizeof(buff)) ? sizeof(buff) : remaining));
f.write(buff, received);
if(remaining > 0){
remaining -= received;
}
yield();
}
f.close();
client.stop();
Serial.println("DOWNLOAD END");
return (remaining == 0 ? true : false);
}

步骤 8. 在 Round Display 上显示 JPEG 图像

一般来说,Round Display 支持的 TFT 图形库只支持显示 BMP 格式的图像,如果我们需要显示其他格式的图像,需要使用一些第三方库,这里我们使用 TJpg_Decoder 库。

请将此库下载为 zip 文件并添加到您的 Arduino 环境中。


我们通过参考仓库提供的示例程序来重写我们的程序:

// 下一个函数将在解码 jpeg 文件期间被调用,以
// 将每个块渲染到 TFT。如果您使用不同的 TFT 库
// 您需要调整此函数以适应。
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap)
{
// 停止进一步解码,因为图像超出了屏幕底部
if ( y >= tft.height() ) return 0;

// 此函数将在 TFT 边界处自动裁剪图像块渲染
tft.pushImage(x, y, w, h, bitmap);

// 返回 1 以解码下一个块
return 1;
}

void setup() {
// 初始化 TFT
tft.init();
tft.setRotation(2);
tft.fillScreen(TFT_BLACK);
tft.setSwapBytes(true); // 我们需要交换颜色字节(字节序)

// 在 TFT 之前初始化 SD
if (!SD.begin(SD_CS)) {
Serial.println(F("SD.begin failed!"));
return;
}
Serial.println("\r\nInitialisation done.");

// jpeg 图像可以按 1、2、4 或 8 的因子缩放
TJpgDec.setJpgScale(1);

// 解码器必须给出上面渲染函数的确切名称
TJpgDec.setCallback(tft_output);

if(WiFi.status() == WL_CONNECTED){
if(getLocation() && getStaticMapImage(host, getPath().c_str(), mapFile)){
TJpgDec.drawSdJpg(0, 0, mapFile);
}
}
}

此项目的完整程序可以在此处找到。


执行程序,您可以看到串行监视器的输出。

屏幕还会显示与您的IP地址对应位置的图片。

使用WFPS方法进行定位

正如我们在前面的步骤中比较的那样,使用IP地址进行定位的准确性确实很差。所以接下来,让我们使用WFPS方法改进程序,看看准确性是否有变化。

当然,这个算法对我们来说很难实现,我们仍然依赖于Google Maps服务中的地理位置API

地理位置API是一个服务,它接受一个HTTPS请求,包含移动客户端可以检测到的蜂窝塔和WiFi接入点。它返回纬度/经度坐标和一个半径,表示每个有效输入结果的准确性。

在社区中,**gmag11**和他们的团队编写了可以直接调用此服务的库。我们可以在这里直接使用它。


同时,您还需要QuickDebug库用于调试消息。


接下来,我们只需要修改getLocation()函数。

//For google geolocation
WifiLocation location (Googlemaps_key);

// Set time via NTP, as required for x.509 validation
void setClock () {
configTime (0, 0, "pool.ntp.org", "time.nist.gov");

Serial.print ("Waiting for NTP time sync: ");
time_t now = time (nullptr);
while (now < 8 * 3600 * 2) {
delay (500);
Serial.print (".");
now = time (nullptr);
}
struct tm timeinfo;
gmtime_r (&now, &timeinfo);
Serial.print ("\n");
Serial.print ("Current time: ");
Serial.print (asctime (&timeinfo));
}

// Get the exact coordinates of XIAO by WiFi location method
void getLocation(){
setClock();
location_t loc = location.getGeoFromWiFi();

Serial.println("Location request data");
Serial.println(location.getSurroundingWiFiJson()+"\n");
Serial.println ("Location: " + String (loc.lat, 7) + "," + String (loc.lon, 7));
latitude = loc.lat;
longitude = loc.lon;
Serial.println ("Accuracy: " + String (loc.accuracy));
Serial.println ("Result: " + location.wlStatusStr (location.getStatus ()));
}

让我们看看通过WFPS方法获得的坐标与实际位置有何不同。

位置偏差已经在1公里左右!这个性能甚至比一些GPS模块还要好。

最新位置的实时更新

最后一步,让我们完善这个全球定位追踪器。让它实现自动地图刷新功能。

tip

使用此程序时请估算您的 Google Cloud 服务费用消耗,否则频繁的 API 调用可能导致高额账单!

void loop() {
// Make sure you pay attention to the number of API calls! This could cost you extra spending!

if(WiFi.status() == WL_CONNECTED){
getLocation();
if(latitude != last_latitude || longitude != last_longitude){ // Update of the location image is performed only when the location is updated
last_latitude = latitude;
last_longitude = longitude;
if(getStaticMapImage(host, getPath().c_str(), mapFile)){
TJpgDec.drawSdJpg(0, 0, mapFile);
}
}
}
delay(10000);
}

在主循环中,我们将每 10 秒获取一次周围网络并更新当前位置坐标。如果返回的位置坐标与上次不同,则会重新下载地图并在屏幕上刷新。

配合我们的 3D 打印外壳,看起来真的像一个追踪器!

最后,通过 WFPS 方法获取定位的完整程序代码可在下方按钮获取。

资源

故障排除

Q1: 为什么我使用 WiFi.hostByName() 函数无法获取准确的 IP 地址?

当使用 WiFi.hostByName() 函数查询路由器的公网 IP 地址时,请确保您的路由器能够被 DNS 解析器解析为相应的 IP 地址。如果您的路由器使用 ISP 提供的 DNS 服务器,您可能会遇到 DNS 解析失败的情况。在这种情况下,您可以尝试使用替代的 DNS 服务器,例如 Google 的公共 DNS 服务器 8.8.8.8 或 8.8.4.4。

如果您仍然无法查询到正确的公网 IP 地址,可能是由于网络连接问题或其他网络配置问题。您可以尝试以下方法来解决问题:

  1. 尝试替代的公网 IP 地址查询服务:如果您使用 api.ipify.org 服务查询公网 IP 地址仍然无法获取正确的 IP 地址,您可以尝试使用替代的公网 IP 地址查询服务,例如 ip-api.com 或 whatismyip.com。

  2. 检查路由器配置:检查您的路由器配置,确保 NAT 和端口转发功能已正确配置且未阻止对外部网络的访问。您还可以尝试在路由器上启用 UPnP 功能以自动配置端口转发。

  3. 重启路由器和 ESP32S3 设备:有时,重启路由器和 ESP32S3 设备可以解决网络连接和配置问题。

如果这仍然无法解决问题,我们建议使用第二种方法或在路由器查询公网 IP 后直接为 XIAO 分配一个值。

技术支持与产品讨论

感谢您选择我们的产品!我们在这里为您提供不同的支持,以确保您使用我们产品的体验尽可能顺畅。我们提供多种沟通渠道,以满足不同的偏好和需求。

Loading Comments...