1. 引言

你是否想过让ESP32知道自己在哪里?在室外,GPS模块是首选;但在室内或高楼林立的城市,GPS信号可能很差,这时Wi-Fi网络定位就成了一个低成本、易实现的替代方案。它的原理很简单:ESP32扫描周围的Wi-Fi热点,将热点的MAC地址和信号强度发给云服务商,服务商根据全球Wi-Fi热点数据库估算出设备的大致位置,并以经纬度返回。

2. 准备工作

硬件

  • ESP32开发板

  • Micro-USB数据线

  • 电脑

软件

  • Arduino IDE(已配置好ESP32开发环境

  • 需要安装的库(Arduino IDE会自动提示安装,或手动安装):

    • ArduinoJson(用于解析JSON数据)

获取云服务商API密钥

我们以腾讯位置服务为例(国内访问快,文档齐全):

  1. 访问腾讯位置服务官网,注册并登录。

  2. 进入控制台 → 应用管理 → 我的应用,点击“创建应用”。

  3. 填写应用名称(如“ESP32定位”),应用类型选择“其他”。

  4. 创建成功后,进入应用详情,点击“添加Key”,选择WebService API,输入Key名称,完成创建。

  5. 复制生成的Key(一串字母数字组合),后面代码中需要用到。

注意:腾讯位置服务的网络定位接口是免费的,但有一定调用限额,个人学习完全够用。

3. 硬件连接

无需任何额外连接! ESP32的Wi-Fi模块已集成在板载,我们只需给ESP32供电即可。

4. 编写代码

以下代码已通过测试,你只需要替换其中的Wi-Fi信息和腾讯Key即可。请确保所有引号都是英文半角。

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// ==================== 配置区域 ====================
// 替换成你的Wi-Fi账号和密码(用于让ESP32联网查询)
const char* WIFI_SSID = "wifi名";
const char* WIFI_PASSWORD = "密码";

// 替换成你在腾讯位置服务申请的Key
const String TENCENT_KEY = "你的Key,密钥";
// 设备的唯一ID,可以自定义,例如 "esp32_device_01"
const String DEVICE_ID = "my_esp32_locator";
// ================================================

// 上报的最多Wi-Fi数量
const int MAX_WIFI_REPORT = 8;

// 存储扫描结果的全局变量
int networksFound = 0;
String wifi_macs[MAX_WIFI_REPORT];
int wifi_rssis[MAX_WIFI_REPORT];

void setup() {
  Serial.begin(115200);
  Serial.println("系统启动...");

  // 1. 初始化Wi-Fi,用于扫描和后续联网查询
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);
  Serial.println("开始扫描Wi-Fi热点...");

  // 2. 扫描Wi-Fi热点
  networksFound = WiFi.scanNetworks();
  if (networksFound == 0) {
    Serial.println("未扫描到任何Wi-Fi热点!");
    return;
  } else {
    Serial.printf("扫描到 %d 个热点\n", networksFound);
    // 收集MAC地址和RSSI,并按信号强度排序(只保留最强的若干个)
    // 这是一个简化处理:实际应用中你可以按信号强度排序
    int reportCount = min(networksFound, MAX_WIFI_REPORT);
    for (int i = 0; i < reportCount; i++) {
      wifi_macs[i] = WiFi.BSSIDstr(i);
      wifi_rssis[i] = WiFi.RSSI(i);
      Serial.printf("  %d: MAC=%s, RSSI=%d\n", i, wifi_macs[i].c_str(), wifi_rssis[i]);
    }
  }

  // 3. 连接到Wi-Fi网络(用于调用API)
  Serial.printf("正在连接Wi-Fi (%s)...\n", WIFI_SSID);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi已连接!");

  // 4. 调用定位API
  getLocationFromTencent();

  // 5. 可选:深度睡眠或结束
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  Serial.println("定位完成,系统进入深度睡眠...");
  // 这里可以添加进入睡眠的代码,比如 esp_deep_sleep_start();
}

void loop() {
  // 本示例只在setup中运行一次,loop保持为空
}

void getLocationFromTencent() {
  HTTPClient http;
  String url = "https://apis.map.qq.com/ws/location/v1/network";

  // 构造JSON请求体
  DynamicJsonDocument doc(2048);
  doc["key"] = TENCENT_KEY;
  doc["device_id"] = DEVICE_ID;

  JsonArray wifiList = doc.createNestedArray("wifiinfo");
  int reportCount = min(networksFound, MAX_WIFI_REPORT);
  for (int i = 0; i < reportCount; i++) {
    JsonObject wifi = wifiList.createNestedObject();
    wifi["mac"] = wifi_macs[i];
    wifi["rssi"] = wifi_rssis[i];
  }

  String requestBody;
  serializeJson(doc, requestBody);
  Serial.println("请求数据: " + requestBody);

  http.begin(url);
  http.addHeader("Content-Type", "application/json");
  http.setTimeout(15000); // 设置超时时间15秒

  int httpCode = http.POST(requestBody);

  if (httpCode > 0) {
    String response = http.getString();
    Serial.printf("HTTP响应码: %d\n", httpCode);
    Serial.println("响应内容: " + response);

    // 解析返回的JSON
    parseResponse(response);
  } else {
    Serial.printf("HTTP请求失败,错误: %s\n", http.errorToString(httpCode).c_str());
  }

  http.end();
}

void parseResponse(String response) {
  DynamicJsonDocument doc(1024);
  DeserializationError error = deserializeJson(doc, response);

  if (error) {
    Serial.print("JSON解析失败: ");
    Serial.println(error.c_str());
    return;
  }

  int status = doc["status"];
  if (status == 0) {
    float lat = doc["result"]["location"]["lat"];
    float lng = doc["result"]["location"]["lng"];
    float accuracy = doc["result"]["accuracy"]; // 定位精度半径,单位:米

    Serial.println("========== 定位成功 ==========");
    Serial.print("纬度: "); Serial.println(lat, 6);
    Serial.print("经度: "); Serial.println(lng, 6);
    Serial.print("精度范围: "); Serial.print(accuracy); Serial.println(" 米");
    Serial.println("==============================");
  } else {
    String message = doc["message"];
    Serial.printf("定位失败,错误码: %d, 信息: %s\n", status, message.c_str());
  }
}

5. 代码解释

5.1 Wi-Fi扫描

  • WiFi.scanNetworks() 扫描周围的Wi-Fi热点,返回找到的数量。

  • WiFi.BSSIDstr(i) 获取第i个热点的MAC地址,格式如"xx:xx:xx:xx:xx:xx"。

  • WiFi.RSSI(i) 获取信号强度(负值,越接近0信号越强)。

  • 我们只取信号最强的8个热点上报,因为太多也无益,且API有长度限制。

5.2 连接互联网

  • 使用你自己的Wi-Fi账号让ESP32连上网,这样才能访问腾讯的API。

5.3 构建请求

  • 使用ArduinoJson库构造一个JSON对象,包含key、device_id和wifiinfo数组。

  • wifiinfo数组中每个元素包含macrssi

5.4 发送HTTPS POST请求

  • 请求地址:https://apis.map.qq.com/ws/location/v1/network

  • 设置Content-Type为application/json,发送JSON数据。

5.5 解析响应

  • 返回的JSON中包含status(0表示成功)、result.location.lat/lng(经纬度)、accuracy(精度半径,单位米)。

  • 我们将其打印到串口。

6. 上传与测试

  1. 在Arduino IDE中,将代码中的WIFI_SSIDWIFI_PASSWORDTENCENT_KEY替换为真实值。

  2. 选择正确的开发板(如“ESP32 Dev Module”)和端口。

  3. 点击“上传”按钮,等待编译并烧录完成。

  4. 打开串口监视器(波特率115200),你会看到如下输出:

    系统启动...
    开始扫描Wi-Fi热点...
    扫描到 10 个热点
      0: MAC=xxxx, RSSI=-51
      1: MAC=xxxx, RSSI=-55
      2: MAC=xxxx, RSSI=-59
      3: MAC=xxxx, RSSI=-68
      4: MAC=xxxx, RSSI=-72
      5: MAC=xxxx, RSSI=-73
      6: MAC=xxxx, RSSI=-77
      7: MAC=xxxx, RSSI=-90
    正在连接Wi-Fi (xxxxx)...
    .....
    Wi-Fi已连接!
    请求数据: {"key":"HZ7BZ-CRJEL-YHPPV-MABFI-7TYYH-LAFTQ","device_id":"my_esp32_locator","wifiinfo":[{"mac":"1C:E6:39:BB:C0:EC","rssi":-51},{"mac":"78:C1:A7:CA:D3:59","rssi":-55},{"mac":"94:BE:09:74:AF:1D","rssi":-59},{"mac":"FC:D7:33:97:2B:38","rssi":-68},{"mac":"82:16:0F:FA:62:13","rssi":-72},{"mac":"82:16:0F:FA:62:21","rssi":-73},{"mac":"5C:54:6D:F4:E5:F0","rssi":-77},{"mac":"48:3F:E9:63:A7:88","rssi":-90}]}
    HTTP响应码: 200
    响应内容: {"status":0,"message":"Success","request_id":"xxxx","result":{"address":"江苏省苏州市XXXXX","location":{"latitude":xx.xxxxxx,"longitude":xxx.xxxxxx,"altitude":0,"accuracy":150},"ad_info":{"adcode":"xxxxx","nation":"中国","province":"江苏省","city":"苏州市","district":"xxx"}}}
    
    

7. 结果分析

得到了一个经纬度坐标和一个精度值(accuracy)。精度值表示估算位置有68%的概率落在以该点为圆心、精度值为半径的圆内。在本例中,精度为150米。

你可以将这个坐标与手机指南针或地图App上的实际位置对比,可能会发现几十米到几百米的偏差。这是正常现象,原因包括:

  • Wi-Fi定位依赖服务商的数据库,数据库可能不完整或不精确。

  • 你周围Wi-Fi热点稀疏,导致估算误差大。

  • 信号波动影响RSSI值,从而影响定位结果。

8. 常见问题及解决

Q1: 扫描不到任何Wi-Fi

A: 检查ESP32天线是否正常,尝试将ESP32放在路由器附近。如果还是扫描不到,可能是硬件问题。

Q2: HTTP请求失败,返回-1或其它错误码

A: 检查Wi-Fi连接是否成功,API密钥是否正确,网络是否通畅。可以尝试ping一下腾讯的API域名(apis.map.qq.com)测试网络。

Q3: 返回错误码“320”或“311”

A: 腾讯API错误码:

  • 320:缺少key参数 → 检查TENCENT_KEY是否已定义。

  • 311:key格式错误 → 确认Key是否正确复制。

  • 其他错误码可参考腾讯官方文档

Q4: 定位精度很差,偏差几公里

A: Wi-Fi定位的精度受环境制约。如果周围Wi-Fi热点很少,服务商可能会根据你的IP地址进行粗略定位,误差可能达数公里。建议在Wi-Fi密集的市区测试。

9. 扩展应用

有了经纬度,你可以做很多有趣的事:

  • OLED显示:将坐标显示在0.96寸OLED屏上。

  • 云端上传:通过MQTT或HTTP将坐标发送到自己的服务器,结合地图API(如高德、百度)实现实时追踪。

  • 地理围栏:判断设备是否进入或离开某个区域,触发报警。

  • 结合GPS:当GPS信号弱时自动切换到Wi-Fi定位。

10. 总结

本教程带你用ESP32和腾讯位置服务实现了Wi-Fi网络定位,无需额外硬件,成本低,适合室内外粗略定位。虽然精度无法与GPS媲美,但在许多物联网场景中已足够。你可以根据自己的需求调整代码,进一步开发出更多有趣的应用。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐