在前一篇文章 【FireBeetle 2 ESP32-S3开发板体验】在Arduino中充分利用FireBeetle 2 ESP32-S3的16MB Flash做SPIFFS 中,分享了在FireBeetle 2 ESP32-S3开发板上使用SPIFFS分区,接下来,继续分享在Arduino中使用基于SPIFFS分区的sqlite3。
sqlite是一个非常小巧的支持SQL语言的嵌入式数据库,这里的嵌入式,有两重意义。
- 其一,sqlite可以很轻松的,嵌入到其他程序中提供SQL数据库功能,而无需独立的数据库服务端。
- 其二,sqlite占用资源非常低,使用c编写,在嵌入式设备中应用非常方便,广泛应用于嵌入式物联网领域。
sqlite的每个数据库存储在单个存储文件中,底层的存储基于B-tree,所以在小型数据库中,数据的查找、删除、添加速度非常之快。
sqlite的最新版本为sqlite3,要在Arduino-ESP32S3中使用sqlite,使用esp32_arduino_sqlite3_lib即可。
一、安装sqlite3扩展库
在上一篇文章中说过,Arduino IDE安装后,通常有3个目录分别为:
- Arduino IDE 程序目录
- 开发板支持包目录
- 扩展库目录
我使用的是macOS系统,所以以上的目录分别为:
- 开发工具目录:/Applications/Arduino.app
- 开发板支持目录:/Users/HonestQiao/Library/Arduino15
- 扩展库目录:/Users/HonestQiao/Documents/Arduino/libraries
- 插件目录:/Users/HonestQiao/Documents/Arduino/tools
如果是在Windows系统,通常目录如下:
- 开发工具目录:C:\Program Files (x86)\Arduino
- 开发板支持目录:C:\Users\Administrator\AppData\Local\Arduino15
- 扩展库目录:C:\Users\Administrator\Documents\Arduino\libraries
- 插件目录:C:\Users\Administrator\Documents\Arduino\tools
具体目录,需要根据实际情况确定,上面只是通常的情况。
安装sqlite3扩展库,可以用如下方法:
安装完成后,重启Arduino IDE,才文件菜单的示例中,可以看到对应的例子:
二、示例测试
在上述sqlite3扩展库的示例中,就包含了基于spiffs的测试,直接选择sqlite3_spiffs即可,示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>
#include <SPI.h>
#include <FS.h>
#include "SPIFFS.h"
#define FORMAT_SPIFFS_IF_FAILED true
const char* data = "Callback function called";
static int callback(void *data, int argc, char **argv, char **azColName) {
int i;
Serial.printf("%s: ", (const char*)data);
for (i = 0; i<argc; i++){
Serial.printf("%s = %s\\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
Serial.printf("\\n");
return 0;
}
int db_open(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;
}
char *zErrMsg = 0;
int db_exec(sqlite3 *db, const char *sql) {
Serial.println(sql);
long start = micros();
int rc = sqlite3_exec(db, sql, callback, (void*)data, &zErrMsg);
if (rc != SQLITE_OK) {
Serial.printf("SQL error: %s\\n", zErrMsg);
sqlite3_free(zErrMsg);
} else {
Serial.printf("Operation done successfully\\n");
}
Serial.print(F("Time taken:"));
Serial.println(micros()-start);
return rc;
}
void setup() {
Serial.begin(115200);
sqlite3 *db1;
sqlite3 *db2;
int rc;
if (!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) {
Serial.println("Failed to mount file system");
return;
}
File root = SPIFFS.open("/");
if (!root) {
Serial.println("- failed to open directory");
return;
}
if (!root.isDirectory()) {
Serial.println(" - not a directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print(" DIR : ");
Serial.println(file.name());
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print("\\tSIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
SPIFFS.remove("/test1.db");
SPIFFS.remove("/test2.db");
sqlite3_initialize();
if (db_open("/spiffs/test1.db", &db1))
return;
if (db_open("/spiffs/test2.db", &db2))
return;
rc = db_exec(db1, "CREATE TABLE test1 (id INTEGER, content);");
if (rc != SQLITE_OK) {
sqlite3_close(db1);
sqlite3_close(db2);
return;
}
rc = db_exec(db2, "CREATE TABLE test2 (id INTEGER, content);");
if (rc != SQLITE_OK) {
sqlite3_close(db1);
sqlite3_close(db2);
return;
}
rc = db_exec(db1, "INSERT INTO test1 VALUES (1, 'Hello, World from test1');");
if (rc != SQLITE_OK) {
sqlite3_close(db1);
sqlite3_close(db2);
return;
}
rc = db_exec(db2, "INSERT INTO test2 VALUES (1, 'Hello, World from test2');");
if (rc != SQLITE_OK) {
sqlite3_close(db1);
sqlite3_close(db2);
return;
}
rc = db_exec(db1, "SELECT * FROM test1");
if (rc != SQLITE_OK) {
sqlite3_close(db1);
sqlite3_close(db2);
return;
}
rc = db_exec(db2, "SELECT * FROM test2");
if (rc != SQLITE_OK) {
sqlite3_close(db1);
sqlite3_close(db2);
return;
}
sqlite3_close(db1);
sqlite3_close(db2);
}
void loop() {
}
上述代码演示了简单的打开sqlite3数据库,新建数据表,新增数据,以及查询数据的方式。
编译代码下到到FireBeetle 2 ESP32-S3开发板运行后,输出如下:
三、使用sqlite3进行日志记录
完成上一步,就可以在FireBeetle 2 ESP32-S3开发板上顺利使用sqlite3数据库了。
下面,我们再使用sqlite3,做一个简单的日志记录的功能。
首先,我们定义一个log数据表:
sqlite> .schema log
CREATE TABLE log (id INTEGER PRIMARY KEY, info TEXT);
sqlite>
这个表有两个字段,其中主键id为自增字段,info用于存储信息。
然后使用下面的代码进行测试:
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>
#include <SPI.h>
#include <FS.h>
#include "SPIFFS.h"
sqlite3 *db1;
int rc;
int rc_count;
sqlite3_stmt *res;
const char *tail;
String sql;
const char* data = "Callback function called";
static int callback(void *data, int argc, char **argv, char **azColName) {
int i;
Serial.printf("%s: ", (const char*)data);
for (i = 0; i < argc; i++) {
Serial.printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
Serial.printf("\n");
return 0;
}
int db_open(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;
}
char *zErrMsg = 0;
int db_exec(sqlite3 *db, const char *sql) {
Serial.println(sql);
long start = micros();
int rc = sqlite3_exec(db, sql, callback, (void*)data, &zErrMsg);
if (rc != SQLITE_OK) {
Serial.printf("SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
} else {
Serial.printf("Operation done successfully\n");
}
Serial.print(F("Time taken:"));
Serial.println(micros() - start);
return rc;
}
void setup() {
Serial.begin(115200);
randomSeed(analogRead(A0));
if (!SPIFFS.begin(true)) {
Serial.println("Failed to mount file system");
return;
}
// list SPIFFS contents
File root = SPIFFS.open("/");
if (!root) {
Serial.println("- failed to open directory");
return;
}
if (!root.isDirectory()) {
Serial.println(" - not a directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print(" DIR : ");
Serial.println(file.name());
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print("\tSIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
// SPIFFS.remove("/log.db");
// SPIFFS.remove("log.db-journal");
sqlite3_initialize();
if (db_open("/spiffs/log.db", &db1))
return;
Serial.println("Check table log");
rc = db_exec(db1, "SELECT 1 FROM log LIMIT 1");
if (rc != SQLITE_OK) {
// 数据表不存在,需要创建
Serial.println("Create table log");
rc = db_exec(db1, "CREATE TABLE log (id INTEGER PRIMARY KEY, info TEXT)");
if (rc != SQLITE_OK) {
// 创建失败
Serial.println("Create table failed");
sqlite3_close(db1);
return;
}
String sql = "INSERT INTO log (info) VALUES(";
sql += "'frist log'";
sql += ");";
db_exec(db1, sql.c_str());
}
Serial.println("Count table log");
sql = "SELECT COUNT(*) FROM log";
rc = sqlite3_prepare_v2(db1, sql.c_str(), -1, &res, &tail);
if (rc != SQLITE_OK) {
sqlite3_close(db1);
return;
}
rc_count = 0;
while (sqlite3_step(res) == SQLITE_ROW) {
rc_count = sqlite3_column_int(res, 0);
Serial.print("Current log record lines: ");
Serial.println(rc_count);
}
sqlite3_finalize(res);
Serial.print("Check max id: ");
sql = "SELECT MAX(id) FROM log";
rc = sqlite3_prepare_v2(db1, sql.c_str(), -1, &res, &tail);
if (rc != SQLITE_OK) {
sqlite3_close(db1);
return;
}
int id_max = 0;
while (sqlite3_step(res) == SQLITE_ROW) {
id_max = sqlite3_column_int(res, 0);
Serial.print("Max id: ");
Serial.println(id_max);
}
sqlite3_finalize(res);
if(rc_count>100) {
String sql = "DELETE FROM log WHERE id<=";
sql += id_max-100;
db_exec(db1, sql.c_str());
}
Serial.println("Last 20 logs: ");
sql = "SELECT * FROM log ORDER BY id DESC LIMIT 20";
rc = sqlite3_prepare_v2(db1, sql.c_str(), -1, &res, &tail);
if (rc != SQLITE_OK) {
sqlite3_close(db1);
return;
}
while (sqlite3_step(res) == SQLITE_ROW) {
Serial.print(sqlite3_column_int(res, 0));
Serial.print(", ");
Serial.println((const char *) sqlite3_column_text(res, 1));
}
sqlite3_finalize(res);
}
void loop() {
delay(10000);
String sql = "INSERT INTO log (info) VALUES(";
sql += "'test log, random=";
sql += random(10000);
sql += "')";
db_exec(db1, sql.c_str());
}
上述代码应用了sqlite3_spiffs示例代码的部分,添加了日志数据库处理的部分,具体逻辑如下:
- 日志数据库为SPIFFS分区下的log.db
- 通过db_exec()执行SQL查询:SELECT 1 FROM log LIMIT 1
- 如果查询成功,说明日志数据表已经创建;
- 如果查询失败,则创建日志数据表,并使用db_exec()执行插入一条"frist log"日志
- 然后统计日志的条数,以及获取日志数据的最大id值
- 如果日志条数大于100条,则自动删除旧的日志,仅保留最后100条
- 取出最新20条日志显示
- 循环loop中的处理:
- 每隔10秒,新增一条日志(含随机数)
以上代码每次重启后,都会清理旧的日志;实际上,我们也可以在loop()循环中,适当的时候检查日志条数,及时清理。
编译代码下载,第二启动后,将会出现如下信息:
从上图可以看到,我们的日志成功写入了,并且输出了之前保存的最后的日志信息。
另外,在上述代码中,使用了随机数,具体调用包括:
- randomSeed(analogRead(A0)):重置随机数生成器,用A0的输入值作为种子
- random(10000):在0~10000生成随机整数
四、总结
以上的分享,只是sqlite3在FireBeetle 2 ESP32-S3的简单使用,在实际使用中,可以有很多用法。例如,你可以用它来记录传感器的数据,进行进一步的分析。
想要进一步了解使用sqlite的相关信息,可以查看下面的链接:
siara-cc/esp32_arduino_sqlite3_lib: Sqlite3 Arduino library for ESP32 (github.com)
SQLite 教程 | 菜鸟教程 (runoob.com)