单片机/MCU论坛
直播中

HonestQiao

8年用户 545经验值
擅长:嵌入式技术
私信 关注

【FireBeetle 2 ESP32-S3开发板体验】基于ESP32S3+SPIFFS+AsyncWebServer+SQLite3的硬件地址归属品牌(厂商)查询工具

在之前的两篇分享中,分别分享了
在Arduino中充分利用FireBeetle 2 ESP32-S3的16MB Flash做SPIFFS在Arduino中使用基于SPIFFS分区的sqlite3嵌入式数据库 ,这篇分享,在以上两篇文章的基础上,再结合AsyncWebServer,基于Arduino环境开发,实现了一个在FireBeetle 2 ESP32-S3开发板的基于ESP32S3+SPIFFS+AsyncWebServer+SQLite3的硬件地址归属品牌(厂商)查询工具,最终具体访问效果界面如下:
image.png

一、规划准备

要完成上面这个查询工具,需要做如下的准备:

  1. 熟练SPIFFS的使用
  2. 在ESP32-S3上建立WebServer
  3. 构建HTML查询页面
  4. 收到查询请求后,从sqlite数据库检索匹配的数据
  5. 提供合适的sqlite数据库,用于硬件地址的匹配检索

第1点,详细阅读之前的第一篇文章即可了解。

第2点,可以试用Arduino中的WebServer扩展库,但使用ESPAsyncWebServer性能好,而且ESP32S3运行效果更好

第3点,使用bootstrap来构建基础也没,简单方便

第4点,详细阅读之前的第二篇文章了解

第5点,查找了不少资料,有提供查询的平台,但是获取数据集,需要收费。不过,最终从IEEE官方网站,获取到了公开的数据,最终生成了需要的sqlite数据库,具体可以查看data · HonestQiao/mac_query_tool_on_esp32 中的oui.db文件,其具体结构如下:

sqlite> .schema oui
CREATE TABLE oui
        (Assignment CHAR(6) PRIMARY KEY     NOT NULL,
        OrganizationName           TEXT    NOT NULL
        );
sqlite>

通常情况下,如果一个mac地址为 ac:bc:34:ce:45:67 ,则其前6位(ac:bc:34)是归属到一个具体的品牌(厂商)的。

所以上述数据表中,Assignment表示这个前六位,OrganizationName则表示品牌(厂商)。

二、SPIFFS分区的文件结构

对应的文件,已经在 data · HonestQiao/mac_query_tool_on_esp32 提供,包括上述的sqlite数据库和网页文件,具体结构如下:image.png

在上述目录中:

  • oui.db:从IEEE获取的数据生成的sqlite数据库
  • www:网页部分的文件

使用 ESP32 Filesystem Uploader 工具上传data目录的数据到FireBeetle 2 ESP32-S3开发板备用。

三、扩展库的安装

在之前两篇文章的基础上,使用库管理工具,安装 ESPAsyncWebServer 即可:
image.png

sqlite3扩展库esp32_arduino_sqlite3_lib,在之前的分享中已经安装过了。

四、代码实现
这篇分享的所有代码和文件,都可以从mac_query_tool_on_esp32: 基于ESP32S3+SPIFFS+AsyncWebServer+SQLite3的硬件地址归属品牌(厂商)查询工具 (gitee.com) 查看。

代码包含两部分,一部分是HTML代码index.html,具体如下:

<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>设备硬件地址品牌(厂商)查询:</title>
    <link rel="stylesheet" href="bootstrap.min.css" />
  </head>
  <body>
    <div class="container">
      <br />
      <h1>设备硬件地址品牌(厂商)查询:</h1>
      <br />
      <form class="form-inline" role="search" method="get" action="/query_db">
        <div class="input-group">
          <input
            type="text"
            class="form-control"
            name="mac"
            placeholder="请输入设备Mac地址"
          />
          &nbsp;&nbsp;&nbsp;&nbsp;
          <span class="input-group-btn">
            <button type="submit" class="btn btn-secondary">搜索</button>
          </span>
        </div>
      </form>
      <br />
      <br />
      {RESULT}
      <br />
      <br />
      <div><hr>Power by Arduino+ESP32+SPIFFS+ESPAsyncWebServer+SQLite3</div>
    </div>
  </body>
</html>

其中:

  • 表单部分,用于输入mac地址,提交后查询。
  • {RESULT},用于查询后输出对应的结果信息

第二部分的代码,是Arduino代码mac_query_tool_on_esp32.ino,参考了sqlite3_webquery示例,但原示例代码使用的是WebServer性能较低,页面也都是手写嵌入到页面显示效果太差,所以使用ESPAsyncWebServer重写,页面也基于bootstrap来编写,查询部分使用IEEE数据集进行查询。

mac_query_tool_on_esp32.ino具体代码如下:

/*
 * 基于ESP32S3+SPIFFS+AsyncWebServer+SQLite3的硬件地址归属品牌(厂商)查询工具
 * 作者:HonestQiao(honestqiao@163.com)
 * 日期:2023-08-03
 * 
 * 说明:通过硬件设备的Mac地址,查询其所对应的品牌或者厂商信息
 * 
 * 参考资料:
 * ESP32 简单的WEB Server和GET参数读取示例
 * https://lingshunlab.com/book/esp32/esp32-web-server-and-read-get-parameters
 * 
 * me-no-dev/ESPAsyncWebServer
 * https://github.com/me-no-dev/ESPAsyncWebServer#specifying-cache-control-header
 * 
 * ESP32 Web Server using SPIFFS (SPI Flash File System)
 * https://randomnerdtutorials.com/esp32-web-server-spiffs-spi-flash-file-system/
 * 
 * 
 * esp32_arduino_sqlite3_lib
 * https://github.com/siara-cc/esp32_arduino_sqlite3_lib/tree/master
 * 
 * bootstrap
 * https://code.z01.com/bootstrap/examples/search/
 * 
 * 硬件地址数据集:
 * https://regauth.standards.ieee.org/standards-ra-web/pub/view.html
 */


#include <WiFi.h>
#include <WiFiClient.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebSrv.h>
#include <ESPmDNS.h>
#include <sqlite3.h>
#include <SPI.h>
#include <FS.h>
#include "SPIFFS.h"

const char *ssid = "********";
const char *password = "********";

AsyncWebServer server(80);

const int led = 21;
String html_result = "";

sqlite3 *db1;
int rc;
sqlite3_stmt *res;
int rec_count = 0;
const char *tail;

void notFound(AsyncWebServerRequest *request) {
  request->send(404, "text/plain", "Not found");
}

int openDb(const char *filename, sqlite3 **db) {
  int rc = sqlite3_open(filename, db);
  if (rc) {
    Serial.printf("Can't open database: %s\\\\n", sqlite3_errmsg(*db));
    return rc;
  } else {
    Serial.printf("Opened database successfully\\\\n");
  }
  return rc;
}

void setup ( void ) {
  pinMode(led, OUTPUT);
  digitalWrite(led, 0);
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }

  Serial.println ( "" );
  Serial.print ( "Connected to " );
  Serial.println ( ssid );
  Serial.print ( "IP address: " );
  Serial.println ( WiFi.localIP() );

  if ( MDNS.begin ( "esp32" ) ) {
    Serial.println ( "MDNS responder started" );
  }

  if (!SPIFFS.begin(true)) {
    Serial.println("Failed to mount file system");
    return;
  }

  Serial.println("Read result.html from file system");
  File file = SPIFFS.open("/www/index.html", "r");
  if (!file) {
    Serial.println("Failed to open file for reading");
    return;
  }

  while (file.available()) {
    html_result += char(file.read());
  }
  file.close();

  Serial.print("Length of result.html: ");
  Serial.println(html_result.length());

  sqlite3_initialize();

  // Open database
  if (openDb("/spiffs/oui.db", &db1))
    return;

  server.serveStatic("/bootstrap.min.css", SPIFFS, "/www/bootstrap.min.css").setCacheControl("max-age=86400");
  server.serveStatic("/bootstrap.min.css.map", SPIFFS, "/www/bootstrap.min.css.map").setCacheControl("max-age=86400");

  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
    String html_result_resp = html_result;
    html_result_resp.replace("{RESULT}", "<br/><br/>");
    request->send(200, "text/html", html_result_resp);
  });

  server.on("/query_db", HTTP_GET, [](AsyncWebServerRequest * request) {
    digitalWrite ( led, 1 );
    String inMacAddress = "";
    if (request->hasParam("mac")) {
      inMacAddress = request->getParam("mac")->value();
      Serial.print("query for ");
      Serial.println(inMacAddress);
    } else {
      String resp = "query is empty: ";
      String html_result_resp = html_result;
      html_result_resp.replace("{RESULT}", resp);
      request->send(200, "text/html", html_result_resp);
      Serial.println(resp.c_str());
      digitalWrite ( led, 0 );
      return;
    }

    String sAssignment = inMacAddress;
    for (int i = 0; i < sAssignment.length() - 1; ++i) {
      char c = sAssignment.charAt(i);
      if (c == ':' || c == '-') {
        sAssignment.remove(i, 1);
      }
    }

    if (sAssignment.length() < 6) {
      String resp = "query is less than 6 valid chars: ";
      String html_result_resp = html_result;
      html_result_resp.replace("{RESULT}", resp);
      request->send(200, "text/html", html_result_resp);
      Serial.println(resp.c_str());
      digitalWrite ( led, 0 );
      return;
    }
    sAssignment = sAssignment.substring(0, 6);
    sAssignment.toUpperCase();

    String sql = "Select count(*) from oui where Assignment = '";
    sql += sAssignment;
    sql += "'";
    Serial.print("SQL: ");
    Serial.println(sql.c_str());
    rc = sqlite3_prepare_v2(db1, sql.c_str(), -1, &res, &tail);
    if (rc != SQLITE_OK) {
      String resp = "Failed to fetch data: ";
      resp += sqlite3_errmsg(db1);
      String html_result_resp = html_result;
      html_result_resp.replace("{RESULT}", resp);
      request->send(200, "text/html", html_result_resp);
      Serial.println(resp.c_str());
      digitalWrite ( led, 0 );
      return;
    }
    while (sqlite3_step(res) == SQLITE_ROW) {
      rec_count = sqlite3_column_int(res, 0);
      if (rec_count > 5000) {
        String resp = "Too many records: ";
        resp += rec_count;
        String html_result_resp = html_result;
        html_result_resp.replace("{RESULT}", resp);
        request->send(200, "text/html", html_result_resp);
        Serial.println(resp.c_str());
        sqlite3_finalize(res);

        digitalWrite ( led, 0 );
        return;
      }
    }
    sqlite3_finalize(res);

    sql = "Select Assignment,OrganizationName from oui where Assignment = '";
    sql += sAssignment;
    sql += "'";
    rc = sqlite3_prepare_v2(db1, sql.c_str(), -1, &res, &tail);
    if (rc != SQLITE_OK) {
      String resp = "Failed to fetch data: ";
      resp += sqlite3_errmsg(db1);
      String html_result_resp = html_result;
      html_result_resp.replace("{RESULT}", resp);
      request->send(200, "text/html", html_result_resp);
      Serial.println(resp.c_str());
      digitalWrite ( led, 0 );
      return;
    }

    rec_count = 0;
    String resp = "<table cellspacing='1' cellpadding='1' border='1'><tr><td>Mac Address</td><td>Assignment</td><td>Organization Name</td></tr>";
    while (sqlite3_step(res) == SQLITE_ROW) {
      resp += "<tr><td>";
      resp += inMacAddress;
      resp += "</td><td>";
      resp += (const char *) sqlite3_column_text(res, 0);
      resp += "</td><td>";
      resp += (const char *) sqlite3_column_text(res, 1);
      resp += "</td></tr>";
      rec_count++;
    }
    resp += "</table><br><br>Number of records: ";
    resp += rec_count;

    String html_result_resp = html_result;
    html_result_resp.replace("{RESULT}", resp);
    request->send(200, "text/html", html_result_resp);
    sqlite3_finalize(res);
    digitalWrite ( led, 0 );
  } );

  server.onNotFound(notFound);
  server.begin();

  Serial.println ( "HTTP server started" );
}

void loop ( void ) {

}

上述代码的具体逻辑如下:

  • 初始化LED、串口、WiFI、SPIFFS
  • 预读取index.html内容
  • sqlite3数据库调用初始化
  • 注册web请求网址,包括首页、css文件、查询页面
  • 收到/query_db查询请求后,根据输入的信息,到sqlite3数据库进行匹配,根据匹配结果,输出对应的页面

在上述代码中,有一些点要注意:

  1. 预读index.html的内容到内存中,以便于每次请求查询时,根据结果动态输出数据,提高性能,毕竟每次从SPIFFS读取还是需要时间的。
  2. 使用server.serveStatic()来设置bootstrap.min.css和bootstrap.min.css.map请求的输出,并设置其在客户端浏览器的缓存时间为一天(86400)

编译烧录到开发板以后,通过串口监视器查看输出信息:
image.png

然后在其他电脑或者手机上,访问上面显示的ip地址,进入查询页面:
image.png

现在,就可以把你的电脑或者手机的mac地址输入查询,看看是否返回正确。输入的Mac地址,可以是 ac:bc:34:ce:45:67 ,也可以使ac-bc-34-ce-45-67,大小写不限,因为代码中已经做了对应的处理。
image.png

注意,要输入真实的硬件设备mac地址,否则是没有结果的。

五、总结

这篇分享,把多种功能给整合到了一起综合运用,实现这个小小的查询工具,得益于FireBeetle 2 ESP32-S3开发板的强劲性能,运行非常流畅。

这样做,有什么用吗?
image.png

重点在这里,后续的分享,将会揭秘。

回帖(1)

sipower

2023-8-3 23:31:26
这个nb
作者不会是想用wifi扫描周边设备,然后统计设备品牌吧
1 1 举报
  • HonestQiao: 你能不能不要这么聪明ε(┬┬﹏┬┬)3

更多回帖

发帖
×
20
完善资料,
赚取积分