在之前的两篇分享中,分别分享了
在Arduino中充分利用FireBeetle 2 ESP32-S3的16MB Flash做SPIFFS和在Arduino中使用基于SPIFFS分区的sqlite3嵌入式数据库 ,这篇分享,在以上两篇文章的基础上,再结合AsyncWebServer,基于Arduino环境开发,实现了一个在FireBeetle 2 ESP32-S3开发板的基于ESP32S3+SPIFFS+AsyncWebServer+SQLite3的硬件地址归属品牌(厂商)查询工具,最终具体访问效果界面如下:
一、规划准备
要完成上面这个查询工具,需要做如下的准备:
- 熟练SPIFFS的使用
- 在ESP32-S3上建立WebServer
- 构建HTML查询页面
- 收到查询请求后,从sqlite数据库检索匹配的数据
- 提供合适的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数据库和网页文件,具体结构如下:
在上述目录中:
- oui.db:从IEEE获取的数据生成的sqlite数据库
- www:网页部分的文件
使用 ESP32 Filesystem Uploader 工具上传data目录的数据到FireBeetle 2 ESP32-S3开发板备用。
三、扩展库的安装
在之前两篇文章的基础上,使用库管理工具,安装 ESPAsyncWebServer 即可:
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地址"
/>
<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具体代码如下:
#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数据库进行匹配,根据匹配结果,输出对应的页面
在上述代码中,有一些点要注意:
- 预读index.html的内容到内存中,以便于每次请求查询时,根据结果动态输出数据,提高性能,毕竟每次从SPIFFS读取还是需要时间的。
- 使用server.serveStatic()来设置bootstrap.min.css和bootstrap.min.css.map请求的输出,并设置其在客户端浏览器的缓存时间为一天(86400)
编译烧录到开发板以后,通过串口监视器查看输出信息:
然后在其他电脑或者手机上,访问上面显示的ip地址,进入查询页面:
现在,就可以把你的电脑或者手机的mac地址输入查询,看看是否返回正确。输入的Mac地址,可以是 ac:bc:34:ce:45:67 ,也可以使ac-bc-34-ce-45-67,大小写不限,因为代码中已经做了对应的处理。
注意,要输入真实的硬件设备mac地址,否则是没有结果的。
五、总结
这篇分享,把多种功能给整合到了一起综合运用,实现这个小小的查询工具,得益于FireBeetle 2 ESP32-S3开发板的强劲性能,运行非常流畅。
这样做,有什么用吗?
重点在这里,后续的分享,将会揭秘。