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

ESP32 DIY GPS 跟踪器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2023年11月22日

CPOL

15分钟阅读

viewsIcon

30590

DIY GPS 位置追踪器

引言

本文介绍如何使用 ESP32 和 GPS 模块构建一个追踪设备。GPS 模块配有天线,可以通过 GPS、Glonass、Galileo 和 Beidou 等卫星确定位置。ESP32 为模块供电并从中接收位置信息。

该设备每分钟将位置记录到闪存中的位置日志文件中。每个日志条目包含日期和时间戳,以及模块报告的纬度、经度和速度。

当追踪器位于预设 Wi-Fi 接入点的范围内时,可以定期查看或上传位置日志文件以进行存储或进一步处理。

背景

目前有多种基于太空的轨道导航系统可供合适的接收器使用,以确定其位置。这些系统中最先的是 Navstar 或全球定位系统 (GPS)。首批测试卫星于 1978 年发射。最初用于军事应用,但在 1983 年,罗纳德·里根总统指示将该系统开放用于商业和娱乐用途。

随后,苏联在 20 世纪 80 年代开发并开始发射自己的系统,称为 Glonass。到 1995 年,Glonass 卫星星座已建成并投入使用。

之后,欧盟 (Galileo) 和中国 (Beidou) 分别开发了后续的卫星导航系统。此外,日本和印度开发的卫星也为局部区域提供了增强的覆盖范围和精度。

总而言之,这些系统现在被称为全球导航卫星系统 (GNSS)。各种组成系统可以被一些导航接收器协同使用。

2023 年,市面上有相对便宜的 GNSS 接收器,它们能够利用多重信号,在卫星信号可用的任何地方提供快速、可靠且准确的位置数据访问。

GNSS 模块

本项目中,我使用了一个亚马逊上的模块,以 2 个接收器模块带天线的套装形式出售,价格不到 16 美元。虽然亚马逊上没有制造商的直接数据,但这些似乎是中国杭州中科微电子的 CASIC 多模卫星导航接收器 (ATGM3 ???)。这并不算太关键,因为该模块默认支持标准的通信(如下定义)。

GNSS 模块通信 (NMEA)

由于 GNSS 接收器产生的数据量适中(每秒 ASCII 字符大约 100-200 个),历史上接收器通常通过 RS232 串行连接进行连接——这是一个自 20 世纪 60 年代以来一直在使用的通信系统。在这些模块上,4800 或 9600 的波特率相当常见。

最初,GPS 功能被安装在大型船舶和大型飞机上,以提供位置、速度和方向的备份确定。到 20 世纪 90 年代末,手持 GPS 设备开始上市,价格在 100-200 美元之间。当然,如今大多数手机、汽车、无人机,甚至自行车和滑板车都拥有自己的 GPS 接收器。

基于这段历史,自 1992 年国家海洋电子协会发布 NMEA 0183 标准以来,GNSS 的通信已实现标准化。该标准定义了接收器传输的信息语句,其中包含有关接收器位置和其他操作参数的信息,例如其天线可见的卫星等。当前版本是 2008 年发布的 4.00。

NMEA 0183 语句

所有语句本质上都是接收器发送的 ASCII(7 位)文本行。每个语句以 $ 字符开头,以 <CR><LF> 结尾。每个语句都有一个 8 位校验和,以提供通信的置信度。每个语句都以一个 3-5 个字母(通常是 5 个)的代码开头,该代码定义了正在传输的语句类型。值以数字或字符串的形式传输,由逗号分隔——如果将它们视为文件,则本质上是逗号分隔值 (CSV) 格式。事实上,您可以使用 Excel 将 NMEA 语句文件导入为 CSV 格式文档。

这是一个示例文本

$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45
  • $ 开头
  • <CR><LF> 结尾
  • 语句类型为 GPRMC
  • 语句校验和位于末尾 *45 - 表示十六进制 45
  • 语句参数以逗号分隔
    • 092751.000 - 表示 UTC0 时间为 09:27:51.000 秒
    • A 表示接收器认为它已根据可见卫星确定了一个有效位置
    • 5321.6802,N 表示纬度为北纬 53 度,21.6802 分
    • 00630.3371,W 表示经度为西经 006 度,30.3371 分
    • 0.06 是速度(单位为节或海里/小时,1.15078 英里/小时,1.852 千米/小时)
    • 31.66 是相对于真北的运动方向
    • 280511 是日期(2011 年 5 月 28 日)

这个语句包含了日期和时间戳,以及纬度、经度、速度和方向。

星座指示器

5 个字母的语句(如上文的 GPRMC)的前两个字母指示了用于确定所报告位置的星座类型。大致上,GP 用于 GPS,GL 用于 Glonass,BD 用于 Beidou,GN 用于通过多个系统数据确定的位置。

SO

  • GPRMC 表示 GPS 解决方案
  • GLRMC 表示 Glonass 解决方案
  • BDRMC 表示 Beidou 解决方案
  • GNRMC 表示多星座解决方案

还有许多其他 NMEA 语句可用。您可以查阅您所用 GNSS 接收器的文档,了解支持的语句。您也可以查阅 NMEA 0183 规范,了解已定义的语句(尽管特定的 GNSS 接收器可能不会生成所有语句)。Arduino 文档中对最常见的语句有很好的描述——有关详细信息,请参阅此链接

硬件组件

本系统的硬件包含两个模块。这些模块可以在亚马逊上购买,价格约为 25-30 美元。

处理器板是标准的 ESP32 板(在亚马逊上搜索 ESP WROOM)。亚马逊上有许多此类板,价格在 10 美元或以下。这些板具有内置的 Wi-Fi 和蓝牙功能。本项目将使用 WROOM 平台的以下子系统。然而,大多数 ESP32 板都支持下面显示的必需功能。

  • 当然是处理器——32 位指令集,浮点运算,RAM,RTC,闪存,由 Arduino IDE 支持
  • 板载闪存,以及可选的 SD 卡槽用于 SD 卡
  • Wi-Fi 功能
  • 用于连接 GNSS 模块的串行端口
  • 用于为 GNSS 模块供电的 3.3V(或 5V)输出

Teyleten GNSS Module

该项目还将使用 GNSS 模块,它是一个小型板,包含 GNSS 接收器本身以及内置或外置天线。本项目我从亚马逊订购了两个 GNSS 接收器模块

特利腾 ATGM336H GPS+BDS 双模模块 飞行控制 卫星定位导航仪

该模块工作在 3.3V,以 9600 波特率输出 TTL-RS232 串行信号。我们将此信号连接到 ESP32,以接收模块发送的 NMEA 数据。

ESP32 板和 GNSS 模块之间需要连接四根线

  • 接地(黑色)连接到 ESP32 GND
  • 3.3 Vdc(红色)连接到 ESP32 3.3V
  • GNSS TX 数据(绿色)连接到 ESP32 GPIO16 RX 数据(ESP 从模块读取数据)
  • GNSS RX 数据(蓝色)连接到 ESP32 GPIO17 TX 数据(ESP 向模块发送数据)

ESP32 and GNSS module connections

软件代码组件

软件是使用 **Arduino IDE** 和 ESP32 板支持包开发的。如果您不熟悉,网上有很多关于安装 Arduino IDE 和 ExpressIF ESP32 板支持包的资源。

首次启动并打开项目时,请确保在 Arduino IDE 中选择以下选项。

  • 在 **工具** 菜单下,连接 ESP32 到 PC 并使用 USB 数据线后,选择用于编程 ESP32 板的 COM 端口。在 Windows 中,如果您不确定,可以使用 Windows 设备管理器查找 COM 端口。
  • 在 **工具** 菜单下,选择板,在 ESP32 Arduino 下,选择您正在使用的板——如果您不确定,很可能是 ESP32 Wroom Module。
  • 在 **工具** 菜单下,选择分区方案,选择 Default 4mb with spiffs

软件组件

软件使用的以下组件——由源代码或板支持包提供的外部库提供。

  • WiFi.h - ESP32 的基本 Wi-Fi 支持组件
  • WiFiUdp.h - ESP32 的 TCP UDP 通信组件,用于 NTP
  • NTPClient.h - ESP32 的网络时间协议组件
  • SPI.h - ESP32 的 SPI 通信组件
  • FS.h - ESP32 文件系统组件
  • SPIFFS.h - ESP32 SPI 闪存文件系统组件
  • SD_MMC.h - ESP32 SD 卡支持(如果使用 SD 卡)
  • ESP32Time.h - ESP32 实时时钟组件
  • ESPTelnet.h - ESP32 Telnet 服务器组件
  • ESP32_FTPClient.h - ESP32 FTP 客户端组件
  • GPS 服务 - 用于服务连接到 GNSS 模块的串行端口的代码
  • 文件系统服务 - 支持读取、写入、创建、删除、列出文件的代码
  • Telnet 服务 - 支持简单 Telnet shell 以访问日志记录器的代码
  • 日志服务 - 将数据记录到文件的代码——包括来自 GNSS 模块的位置数据
  • 配置文件服务 - 读取配置文件以控制 Wi-Fi 访问、FTP 访问等的代码
  • Wi-Fi 服务 - 连接/断开/重新连接 Wi-Fi 的代码
  • NTP 服务 - 读取网络时间并设置内部实时时钟 (RTC) 的代码 (ESP32Time)
  • 设置服务 - 初始化 ESP32 硬件、Wi-Fi、串行端口等的代码
  • 主循环服务 - 永无止境的日志记录器运行代码——读取和记录 GNSS 数据、处理 Telnet 等。

看起来很多,确实不少——所以我们先从一个宏观的视角开始,这样可能会更清楚。

宏观视角

软件主要负责启动、初始化所有内容,然后监听来自 GNSS 模块的数据。特定的 GNSS 模块数据(位置和时间戳)被记录到板上的一个文件中(SPI 闪存或 SD 卡)。

要读取这些数据,用户可以通过 Telnet 连接,并将日志数据通过 FTP 上传到远程服务器。

就是这样……现在进入细节

配置

为了正常运行,日志记录器需要能够连接到 Wi-Fi。它还需要知道要使用哪个 FTP 服务器来上传数据,以及该服务器的登录凭据。

它还需要知道日志记录器运行的时区以及与 GNSS 接收器通信的波特率。

为了存储这些参数,因为它们会根据您的实现和需求而变化,所以有一个名为 config.ini 的文件存储在 ESP32 的闪存文件系统中。

这是一个示例配置文件

// GPS Monitor Config File, Nov 10, 2023, DeanG
WIFISSID=MyWiFi
WIFIPASSWORD=mySecretPassword
TZOFFSETSEC=-28800
FTPSERVER=192.168.4.44
FTPUSER=pi
FTPPASSWORD=raspberry
FTPFOLDER=/media/pi/Seagate2TB/FTP
BAUDRATE=9600

此文件存储在闪存文件系统中,名为 config.ini。将在启动时读取。

如果找不到 config.ini 文件,日志记录器将无法运行——所以请确保正确放置它!

Telnet 界面

日志记录器包含一个 Telnet 服务器,能够接受来自外部计算机通过 Wi-Fi 接口的 Telnet 连接请求。当日志记录器连接到 Wi-Fi 时,您需要检查 Wi-Fi 路由器以了解分配给它的地址。例如,可能是 192.168.5.5。您也可以查看 Arduino IDE 的串口监视器(工具,串口监视器)来查看分配的地址,然后重置 ESP32 模块,观察启动后 5-10 秒内应该出现的 Wi-Fi 连接消息。

在 Windows 中,如果安装了,您可以使用内置的 Telnet 客户端。或者,您可以使用 PuTTY 程序选择一个 telnet(其他,非 SSH)端口 22 连接。 https://www.putty.org

Telnet 界面接受几个简单的命令。不要期望这里有普通 shell!文件系统是扁平的(没有文件夹),通常只包含三个文件

  • config.ini
  • location.log
  • event.log

以下命令可用

  • ls
  • cat /file.1
  • cp /file.1 /file.2
  • rm /file.1
  • off
  • ftp

ls 命令将列出文件系统上的不同文件。

cat 命令将显示一个文件的内容。

cp 命令将文件复制到另一个文件。

rm 命令将删除一个文件。

on 命令将打开 GNSS 接收数据的日志记录到 Telnet 接口。

off 命令将关闭 GNSS 接收数据的日志记录到 Telnet 接口。

ftp 命令将尝试将 location.log 文件发送到配置文件中定义的服务器。

在正常操作中,您将通过 Telnet 连接到日志记录器,然后使用 ftp 命令将位置数据发送到服务器。一旦确认信息已在服务器上正确接收,您将使用 rm 命令删除 /location.log 文件。删除此文件将释放空间以供将来记录位置使用。

日志文件

文件系统将包含一个名为 location.log 的文件,其中将在每分钟操作中记录 GNSS 信息(NMEA $GPRMC 语句)。这导致每小时约 4KB 的数据被记录到文件中(每天 < 100KB)。对于带有外部 SD 卡的系统,如果插入足够大的卡,日志文件大小可能超过 1GB。对于内部文件系统,限制约为 1.2 到 1.4MB——或 12-14 天。这意味着使用内部闪存文件实现时,数据必须每隔一两周下载一次。如果数据存储在 SD 卡上,两次上传之间的时间间隔可能会长得多。

闪存文件系统

闪存文件系统可以存在于内部 SPI 闪存(大小限制约为 1.4MB)上,也可以存储在标准的 SD 存储卡上,如果您的 ESP32 支持的话。(有些 ESP32 板内置了 SD 卡读卡器插槽。或者,您可以购买一个带有 SD 卡读卡器的小电路板,通过几根线连接到 ESP32。

软件启动流程

有一个名为 setup() 的函数,它是所有 Arduino 草图的标准部分,包含在源代码中。此函数在 ESP32 启动时调用一次。它的任务是配置应用程序使用的硬件模块并初始化任何软件组件。

对于此应用程序,setup() 函数需要执行以下操作:

  • 初始化内置串行端口并打印启动消息
  • 初始化 GPIO2 上的内置 ESP32 LED,用于一些状态指示
  • 启动闪存文件系统(SD 或内置 SPI 闪存文件系统 - SPIFFS)
  • 初始化实时时钟(启动时会设置为某个默认日期,然后在 NTP 服务完成后更新)
  • 初始化事件日志服务(记录一些重要的事件和错误以用于诊断目的——如果一切正常,您可以忽略这个名为 event.log 的文件)
  • 读取配置文件(config.ini
  • 初始化调度器服务
  • 发起 Wi-Fi 连接
  • 初始化 GNSS 日志服务(location.log
  • 初始化接收来自 GNSS 模块数据的串行端口
  • 初始化一个监视内置诊断端口输入的 خدمة(您可以通过此端口输入 shell 命令,也可以通过 Telnet 输入)

这是设置代码

//----------------------------------------------------------------------------
// setup() - runs one time when the ESP32 boots up
//----------------------------------------------------------------------------
void setup() 
{
  ntpDone = false;
  // Set up the serial port for diagnostic purposes
  Serial.begin(115200);
  // output a signon message to diagnostic port
  Serial.println(SIGNON);

  pinMode(LEDPIN,OUTPUT); // init LED comfort pin

  //----------- initialize the file system ---------------
  // can be either on an SD card or use the built-in flash
  // with the SPI flash file service (SPIFFS)
  // If we can't connect to the file system, the boot-up
  // fails and we can't really go operational.
    
#ifdef WANTSD_MMC
  // SD card setup
  if(!SD_MMC.begin()) {
    Serial.println("SD Card Mount Failed");
    for (;;);
  }
  uint8_t cardType = SD_MMC.cardType();

  if(cardType == CARD_NONE){
    Serial.println("No SD card attached");
    for (;;);
  }

  Serial.print("SD Card Type: ");
  if(cardType == CARD_MMC){
    Serial.println("MMC");
  } else if(cardType == CARD_SD){
    Serial.println("SDSC");
  } else if(cardType == CARD_SDHC){
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }

  uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);
#endif
#ifdef WANTSPIFFS
  // Initialize SPIFFS (file system)
  if(!SPIFFS.begin(true))
  {
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  else
  {
    Serial.println("SPIFFS mounted");
  }
#endif

  // initialize the RTC, uses timer 0
  rtc.setTime(00,00,00, 1, 1, 2023); // default time 00:00:00 1/1/2023

  logInit(EVENTFN, true);
  
  listDir(fileSystem, "/", 1); // for dev purposes, show the file system on boot up

  // read config file
  if (!readConfigFile(CONFIGFN))
  {
    logMessage("Unable to read config file");
  }

  schedulerInit(); // initialize the scheduler used by the loop() function

  // initiate a WIFI connect
  wifiConnect();

  gpsLogInit(); 

  // Set up serial port for connection to GPS module
  gpsInit(baudRate); 

  rmcbuf[0] = '\0';
  
  setupTelnetDone = false;
  sioInit();  // diagnostic serial port input service
  
  //log_d("Total heap: %d", ESP.getHeapSize());
  //log_d("Free heap: %d", ESP.getFreeHeap());
  //("Total PSRAM: %d", ESP.getPsramSize());
  //log_d("Free PSRAM: %d", ESP.getFreePsram());
  
  Serial.print("\n>");  // initial serial prompt
}

软件运行模式

软件的运行模式全部在标准的 Arduino 函数 loop() 中实现。在启动过程中调用 setup() 函数后,loop() 函数会永远重复调用。在此函数中实现了应用程序的主要运行模式。

对于此应用程序,使用 RTC 实现了一个简单的调度器,将需要执行的不同活动根据活动执行频率分组。这使我们能够进行一些每秒执行一次的活动,一些每分钟执行一次,一些每小时执行一次,以及一些每天执行一次的活动。

此外,ESP32 的所有剩余时间都用于执行“高频”活动或任务,例如服务 Telnet、从 GNSS 模块读取数据等。

您将在下面的代码中看到这些不同的任务按照频率进行了划分。

  • 高频任务
    • gpsService() - 从 GNSS 模块读取任何字符。如果有一整行可用,则处理该行
    • telnet.loop() - 处理可能到达的任何 Telnet 数据
  • 每秒任务
    • wifiService() - 处理与 Wi-Fi 连接、断开连接和重新连接相关的逻辑,并带有适当的超时;在首次连接 Wi-Fi 后,启动 NTP 请求以获取当前时间。Wi-Fi 连接后也启动 Telnet 服务。
    • ntpService() - 处理与 Wi-Fi 上与 NTP 服务器通信以获取网络时间并更新实时时钟相关的逻辑
    • sioService() - 处理在诊断串行端口输入的任何字符——用于诊断目的,可以通过此端口输入 shell 命令
  • 每分钟任务
    • 每分钟一次,将最近接收到的 GNSS 位置数据记录到 location.log 文件
  • 每小时任务
    • 每小时一次,将 GNSS 位置数据刷新到 location.log 文件
  • 每天任务
    • 每天一次,启动新的 NTP 请求,以确保实时时钟保持最新

这是 loop() 函数的源代码

//----------------------------------------------------------------------------
// Main repetitive tasks go here. This is called over and over endlessly
// once setup() has completed.
//----------------------------------------------------------------------------
void loop() 
{
  // This is sort of a poor-person's operating system - scheduling tasks
  // at periodic intervals.

  //----------------------------
  // high rate tasks here
  //----------------------------
  char* line = gpsService();
  if (line != NULL)
  {
    if (gpsSerialEcho) Serial.println(line);
    if (gpsTelnetEcho && telnetConnected) telnet.println(line);
    // $GxRMC
    //Serial.print(line[0]); Serial.print(line[1]); 
    //Serial.print(line[3]); Serial.print(line[4]); Serial.println(line[5]);    
    if ((line[1] == 'G') &&
        (line[3] == 'R') &&
        (line[4] == 'M') &&
        (line[5] == 'C'))  strcpy(rmcbuf,line); // save for minute by minute logging
  }
  telnet.loop(); // process any telnet traffic
    
  //----------------------------
  // tasks executed once per second
  //----------------------------
  if (secondDetector())
  {
    wifiService(); // service the wifi connection controller
    
    if (wifiIsConnected() && !ntpStarted() && 
            (ntpAttempts == 0)) ntpStart(); // first NTP request
    ntpService();
    if (!ntpDone && ntpComplete())
    {
      logMessage("NTP (bootup) completed");
      ntpDone = true;
    }

    if (wifiIsConnected() && !setupTelnetDone)
    {
      setupTelnetDone = true;
      setupTelnet();
    }

    char* sioinputline = sioService();
    if (sioinputline != NULL) handleShellCommand(String(sioinputline));
  }

  //----------------------------
  // tasks executed once per minute
  //----------------------------
  if (minuteDetector())
  {
    gpsLogLine(rmcbuf); // log position if available once per minute
  }

  //----------------------------
  // tasks executed once per hour
  //----------------------------
  if (hourDetector())
  {
    gpsLogFlush(); // flush log hourly
  }

  //----------------------------
  // tasks executed once per day
  //----------------------------
  if (dayDetector())
  {
    ntpStart(); // dayly, get an NTP update
  }
}

关于软件的顶层视图,差不多就到这里了。

源代码包含以下文件

  • gpsLogger.ino - 主应用程序的源代码
  • FileSystemService.h - 文件系统操作(如读写文件)的源代码
  • NTPService.h - 顺序进行 NTP 客户端请求以获取时间的源代码
  • WiFiService.h - 连接、断开和重新连接 Wi-Fi AP 的源代码
  • sioService.h - 从诊断串行端口读取数据行的源代码
  • SchedulerService.h - 将任务划分为每秒、每分钟等的源代码

希望您能从中获得乐趣——还有很多想法可以添加到软件中,以增强它以满足特定的应用需求。

完整源代码在 Github 上,地址为 deangi/GpsLogger。

https://github.com/deangi/GpsLogger

历史

  • 版本 1.2,2023 年 11 月 - 初始运行版本
© . All rights reserved.