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

用于远程边缘部署的 IoT 数据中心

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2024年2月9日

CPOL

12分钟阅读

viewsIcon

4326

物联网边缘数据中心专为远程低功耗无人值守应用而设计。物联网设备可以将数据发布到数据中心,数据中心将存储数据,并使其可供以后下载到核心服务器。该设计基于低成本的ESP32平台,并附带SD卡。

引言

在此应用中,使用经济实惠的ESP32平台将数据存储在SD卡上,形成一个“边缘数据中心”。可以通过直接更换SD卡来传输存储的数据,或者可以通过FTP连接定期下载。设计标准包括在可能“离网”的区域进行远程部署,这些区域无法获得稳定的互联网连接,并且可用电源非常有限。

背景

物联网传感器通常部署在低功耗环境中——并且可能放置在非常偏远的位置。收集这些数据并在“边缘”临时存储通常是可取的。

此应用允许收集数据并将其存储在小型边缘数据中心上的SD卡上。该数据中心可以连接到现有的WiFi信号,也可以托管自己的WiFi接入点——允许物联网设备暂时连接并推送数据到物联网边缘数据中心。

服务器设备可以在稍后通过连接到数据中心并使用FTP提取数据供以后使用来收集这些数据。物联网数据中心功耗小于一瓦,因此适合在电池和/或太阳能供电下进行远程操作。

一个示例应用允许将物联网数据中心放置在没有互联网连接且电源限制严格的山顶上。它可能正在定期测量天气参数和/或拍摄照片。稍后,卡车甚至无人机可能会访问该站点并从物联网数据中心上传收集到的数据。

另一个应用可能是地下放置,甚至是漂浮的浮标放置,在那里数据可以被收集并存储在边缘以供最终检索。

设计架构

ESP32平台因其良好的低成本、低功耗和高可用性评分而被选用于此应用。ESP32带有一个SD卡读卡器,可用于为从传感器上传的数据提供存储。在固定的时间间隔,可以检索这些数据并传输到非边缘服务器。

在此设计中,ESP32的WiFi接口用于连接到现有的WiFi网络,或提供一个托管自身WiFi服务的接入点。WiFi接口支持两种服务供传感器用于推送要存储的数据。第一种是实现FTP服务器,允许数据量较大的远程传感器上传数据——例如,远程摄像头可能会定期推送照片。还实现了一个HTTP服务,允许数据速率较低的传感器将数据推送到边缘服务器。

为了进一步的功能,以下内容被考虑用于后续项目

  • 用于从硬接线传感器收集数据进行记录的串行端口数据接口——这可以包括标准的RS232类型信号以及其他串行协议,如1Wire、IIC或SPI。
  • 一个蓝牙网关,允许具有蓝牙或BTLE连接的传感器通过WiFi服务进行发布。
  • 一个LoRa网关,允许具有LoRa连接的传感器通过WiFi服务发布数据。

在所有这些情况下,可以使用具有自身应用的辅助ESP32将数据推送到边缘服务器。甚至有可能单个ESP32可以作为所有这三个网关。这是稍后的一个后续项目。

硬件设计注意事项

对于硬件平台,我选择了Aideepen ESP32-CAM板。该板体积小巧,并配有SD卡读卡器。它还有一个重要功能——有一个用于外部WiFi天线的小型连接器。在某些应用中,这可能很重要,因为增加WiFi信号的范围对于远程传感器或来自现有WiFi信号源的弱信号很重要。

Aideepen ESP32-CAM board

这些板在亚马逊上很容易买到,价格相对较低(2块板20美元)。SD卡读卡器文档支持高达4GB的卡,但我一直成功使用32GB的三星A1 micro-sd卡。

此应用无需对电路板进行任何硬件添加或修改——只需将代码编程到模块中,并确保闪存中有一个有效的配置文件在启动时读取。如果需要,请安装SD卡存储图像。该板配有摄像头,在此应用中未使用。如果愿意,可以将其移除。或者,您可以使用它制作一个远程摄像头,定期拍照并将其上传到这个边缘服务器。有关示例,请参阅本文。

对于电源,这些板需要5V电源——可以直接从USB端口供电,也可以从用于为手机充电的一些常见充电宝供电。

据我所知,这个ESP32-CAM板设计经常被低成本制造商克隆——所以请注意,其中一些制造商可能存在质量问题。我从亚马逊购买了两块上述电路板——幸运的是,它们都工作正常。

软件架构

该软件使用了几个组件,包括FTP服务器和HTTP服务器。感谢Rui Santos在Random Nerd Tutorials为ESP32平台所做的出色工作!

在构建此应用时,使用了以下现有组件

  • ESP32Time - 时钟和日历模块
  • NTPClient - 从NTP服务器检索网络日期和时间
  • FTPServer - 进行了少量修改以增加功能
  • WiFiClient - 实现一个极简的HTTP服务器,用于状态和发布传感器数据

大部分代码用于设置这些服务并使用它们。

配置文件

应用程序启动时,它将从SD卡读取一个名为config.ini的文件。此文件包含边缘服务器正确运行所需的信息。它是一个简单的文本文件,行格式为Key=Value。

您应该准备配置文件,并在ESP32通电之前将其放在SD卡根文件夹中。

此文件中所需的信息定义了WiFi配置和FTP服务器配置。配置文件最少包含5行。它区分大小写,请注意!

SSID=wifissid

PASSWORD=wifipassword

MODE=xxx   AP or STATION

MODE=AP

AP表示边缘服务器将自身配置为WiFi接入点。这意味着其他设备可以直接连接到ESP32进行通信。SSIDPASSWORD在此模式下用于远程设备连接。在此模式下,ESP32的地址将是192.168.4.1。远程传感器需要配置为在此地址联系边缘服务器以上传数据。

MODE=STATION 

STATION表示边缘服务器连接到具有SSIDPASSWORD的现有WiFi网络。在此模式下,ESP32将由它连接的AP分配自己的IP地址。任何传感器都需要使用此IP地址发送数据。

FTPUSER=ftpusernaee

FTPPASSWORD=ftppassword

这两行确定了传感器为了访问FTP服务器上传数据而需要配置的用户名和密码。发布数据的传感器不需要配置这些信息。

SSID=iothub1445
PASSWORD=soWhat1445
FTPUSER=iothub
FTPPASSWORD=soWhat1445pwd
MODE=AP 

错误通知

某些可能遇到的错误——尤其是在启动过程中——将导致边缘服务器无法完全启动并投入运行。为了报告这些错误,将使用板载LED闪烁错误代码。

ESP32将闪烁“SOS”然后闪烁一次或多次。SOS后面的闪烁次数是错误代码。您可能看到的错误代码是

  • 1次闪烁 - 由于某种原因无法访问SD卡。请确保其格式化为FAT32,并且根文件夹中有一个包含上述5行的config.ini文件。您可以使用Windows记事本创建文件并将其保存为config.ini。
  • 2次闪烁 - 无法连接到WiFi——可能是信号太弱,或者config.ini文件中的SSIDPASSWORD不正确。

日志文件

来自传感器的数据将存储在SD卡上。对于FTP上传,每个传感器或传感器组可以上传到自己的日志文件,或者所有传感器都可以上传到自己的日志文件。FTP服务器支持APPEND模式,因此可以将传感器数据追加到特定的日志文件中(这样,每次上传就不必是单独的文件,尽管如果这符合您的需求,也可以这样做)。

对于HTTP POST服务上传,一行将被追加到一个公共日志文件——根文件夹中的iotdata.log。传感器数据应包含采集数据的传感器ID,这一点很明智。POST的数据会带时间戳。可以使用FTP服务器检索此文件。通常,在远程FTP客户端连接以收集数据时,它会获取相应的数据文件(包括iotdata.log),并在成功上传后,可以使用FTP删除文件,以避免在远程服务器上重复传感器数据。

除了内部日志文件外,还有一个关于物联网数据中心活动的日志文件——它被称为iothub.log。此文件将包含指示重要事件(如启动、WiFi状态、NTP状态等)的条目。

设置日期和时间

物联网数据中心将为POST服务行中的记录数据添加时间戳,并将上传文件的文件名添加上传时间戳。但是,它必须以某种方式获知正确的日期和时间才能形成准确的时间戳。

STATION模式下,物联网数据中心将尝试自动从互联网上的NTP(网络时间协议)服务器获取日期和时间。如果物联网数据中心无法访问NTP服务器,则此操作将失败。

在AP模式下,物联网数据中心未连接到互联网,因此无法使用NTP服务器。

为了允许准确的时间戳——实现了一个特殊的POST服务命令来设置日期和时间。通常,这将在物联网数据中心首次通电时执行一次。之后,它将能够跟踪时间——尽管该时钟的准确性取决于ESP32的内部时钟,因此可能会随着时间的推移而漂移。

物联网数据中心内部时钟可以通过发送一个以波浪号(~)字符开头,后跟14位数字(无空格或分隔符)的特殊POST服务命令来设置。格式为yyyymmddhhmmss,例如,2024年2月6日下午2:34:56为20240206143456。这看起来像一个发送到物联网数据中心HTTP服务器(默认端口80)的HTTP命令,格式如下:

GET /?LOG=~20240206143456 HTTP/1.1

使用HTTP服务器记录数据

数据速率较低的传感器可以向HTTP服务器(默认端口80)发送一条消息,将一行数据发送到iotdata.log文件。其形式如下:

GET /?LOG=my%20temperature%20is%2056%20degrees

请注意,除字母(A-Z,a-z)或数字(0-9)字符之外的任何字符都必须以传统的URL格式进行“十六进制转义”——例如,%20是空格字符,因此上述命令将记录

我的温度是56度

到iotdata.log文件。

这是一个HTTP GET命令,其中参数LOG设置为远程传感器想要发送到物联网数据中心ioddata.log文件的行。此行将被追加到日志文件并带时间戳(使用物联网数据中心的日期/时间),因此它看起来像这样:

2024/02/06,14:34:56,my temperature is 56 degrees

网页

当浏览器请求物联网数据中心的首页时——例如,在地址栏中输入http://192.168.5.3(此示例为STATION模式,或http://192.168.4.1为AP模式)——物联网数据中心将提供一个网页,显示一些内部信息,如SD卡利用率以及物联网数据中心正在维护的当前日期和时间。

Example web page served by the IoT Hub

启动任务

首次接通电源时,ESP32将启动物联网数据中心应用程序。首先执行的是设置所使用的硬件和通信功能。

您将在下面的setup()函数中看到它执行以下操作:

  • 初始化串行端口并输出登录消息
  • 初始化信号LED,以便我们可以输出任何错误消息
  • 挂载SD卡——如果失败——我们将停止并闪烁错误消息
  • 读取配置文件(config.ini
  • 初始化实时时钟(RTC)
  • 如果配置文件指示AP模式,则启动ESP32 WiFi作为接入点
  • 如果模式为STATION,则连接到WiFi信号并尝试从NTP获取日期和时间
  • 初始化并启动Web服务器
  • 初始化并启动FTP服务器
//-----------------------------------------------------
// Setup - called once on boot-up
void setup() 
{
  char buf[32];
  
  Serial.begin(115200);
  Serial.println(SIGNON);

  pinMode(SIGNALLED,OUTPUT);
  
  // Mount SD card file system
  if(!SD_MMC.begin()) 
  {
    PRINTLN("SD_MMC Card Mount Failed");
    blink(ERR_NOCARD);
  }
  else
  {
    uint8_t cardType = SD_MMC.cardType();
  
    if(cardType == CARD_NONE) 
    {
      PRINTLN("No SD card attached");
      blink(ERR_NOCARD);
    }
    else
    {
      PRINT("Mounted SD_MMC card successfully, type is ");

      if(cardType == CARD_MMC){
        PRINTLN("MMC");
      } else if(cardType == CARD_SD){
        PRINTLN("SDSC");
      } else if(cardType == CARD_SDHC){
        PRINTLN("SDHC");
      } else {
        PRINTLN("UNKNOWN CARD TYPE");
      }
    }
  }

  // read configuration file
  readKey(CONFIGFN, "SSID=", ssid, 63);
  readKey(CONFIGFN, "PASSWORD=", password, 63);  
  readKey(CONFIGFN, "FTPUSER=", ftpuser, 63);
  readKey(CONFIGFN, "FTPPASSWORD=", ftppwd, 63);
  readKey(CONFIGFN, "MODE=", buf, 31); // MODE=AP or MODE=STA, station mode by default
  isApMode = (strcmp(buf,"AP") == 0);
  Serial.println(isApMode ? "AP MODE" : "STATION MODE");  

  // initialize the RTC and read network time if possible
  rtc.setTime(12,0,0,1,1,2024); // default time 12:00:00 1/1/2024

  // set up WiFi in AP or STATION mode
  if (isApMode)
  {
    // Access point mode
    WiFi.softAP(ssid, password);
    IPAddress IP = WiFi.softAPIP();
    Serial.print("Access Point IP address: ");
    Serial.println(IP);
  }
  else
  {
    // WiFi station mode - connect to an access point
    // on first boot, try to connect to WiFi and get time/date from NTP
    connectToWiFi();
    if (!connectToWiFi) blink(ERR_NOWIFI);
    needNtp = true;
    getNtpTime(); // get NTP time if possible
  }

  PRINTLN("Starting Web service");
  server.begin(); // start web server
  
  PRINT("Initializing FTP server - user "); 
  PRINT(ftpuser);
  PRINT(" password ");
  PRINTLN(ftppwd);
  ftpSrv.begin(ftpuser,ftppwd); // start FTP server
}

操作任务

在启动任务完成后,loop()函数将重复执行,直到断电。在此函数中,我们放置操作任务——主要是服务通信任务。

代码将无限循环执行以下操作:

  • 处理任何连接的FTP客户端,传输文件等
  • 处理任何连接的HTTP客户端,读取客户端发送的请求,如果有一个请求要将数据发布到日志文件或设置日期和时间,则处理该请求。

我们需要小心地处理这一点,以便如果HTTP请求和FTP请求同时出现,我们不会忽略一个并处理另一个。这是一种简单的分时系统,以确保我们为两种通信方式都分配时间。通常,这些不会同时发生——但可能会发生某个传感器尝试发布数据,而另一个传感器或远程用户同时连接到FTP的情况。

//-----------------------------------------------------
// loop() - called forever after setup() is called
void loop() 
{
  for (;;)
  {
    // sort of a crude round-robin scheduler
    ftpSrv.handleFTP();

    if (webClientConnected)
    {
      if (httpScanCounter++ >= MAXHTTPSCANCOUNTER)
      {
        webClientConnected = false;
        if (client)
        {
          client.stop();
        }
        Serial.println("\nForced client disconnect");
      }
      // a client is connected, try to read the request line
      // request line will be something like this:
      // GET /?var1=val1 HTTP/1.1
      //    GET /?LOG=123%20ABC HTTP/1.1
      //   ?var1=val1&var2=val2 ,,,
      //   values have embedded hex for some characters like  "1%202" means "1 2"
      //   basically any char that isn't alpha/numeric is %xx
      //
      else if (!client)
      {
        webClientConnected = false;
        client.stop(); // client unexpectedly disconnected
        //Serial.println("\nunexpected client disconnect");
      }
      else if (client.available())
      {
        char c = client.read();
        //Serial.write(c); // echo for debugging
        if (c == '\n')
        {
          //Serial.println();
          processHeader(httpHeader);
          replyWithHomePage(client);
          client.flush();
          client.stop();
          webClientConnected = false;
          //Serial.println("Web client disco");
        }
        else if (httpHeaderPtr < (HTTPHEADERMAXLEN-1))
        {
          httpHeader[httpHeaderPtr++] = c;
          httpHeader[httpHeaderPtr] = '\0';
        }
      }
    }
    else
    {
      // at the present time no client is connected,
      // see if one is knocking on our door
      client = server.available();
      if (client) 
      {
        webClientConnected = true;
        httpHeader[0] = '\0';
        httpHeaderPtr = 0;
        httpScanCounter = 0;
        Serial.println("Web client connected");
      }
    }
  }
}

历史

  • V1.0 2024年2月2日 - 初始版本
© . All rights reserved.