- 2023-10-21
-
发表了主题帖:
【得捷电子Follow me第2期】Adafruit ESP32-S3 TFT Feather网络应用开发【修改版】
# 基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践
视频链接: http://training.eeworld.com.cn/video/37657
源码下载:http://download.eeworld.com.cn/detail/%E5%9B%A7%E5%A4%A7%E5%A4%A7%E7%8E%8B/629208
## 项目介绍
该项目是基于Adafruit ESP32-S3 TFT Feather开发板的一个简单的示例,通过该示例可以学习到如何使用Adafruit ESP32-S3 TFT Feather开发板的WiFi、GPIO控制及以Neopixel LED控制等功能。
该项目完成了"Follow me活动”第2期的任务1、任务2、任务3以及任务4的分任务1。
## 功能介绍
* 基于LVGL的多页面图形界面
* 内置 OPPOSans 字体,支持大部分常见汉字及符号的显示
* WiFi热点网页配网
* 日期时间显示,支持网络时钟同步
* 通过网络获取天气信息
* 板载NeoPixel LED控制,呼吸灯闪烁,支持通过按键更换闪烁颜色
在尚未进行配网时启动板卡会自动启动AP热点,通过手机连接该热点会启动CaptivePortal网页引导进行配网,配网成功后会自动关闭AP热点并连接到指定的WiFi SSID。
配网完成后首页会显示已连接的WiFi信息以及IP地址,屏幕上方状态栏会显示日期、时间以及天气信息。
点击屏幕左侧的Boot0按钮可切换显示页面,第二个页面是LED控制页面。进入LED控制页面会自动点亮Neopixel LED呼吸灯,在该页面长按Boot0可控制板载的Neopixel LED的颜色切换,颜色可在红绿蓝三种颜色之间进行切换,离开此页面会自动熄灭Neopixel LED。
内置AP默认连接信息:
* SSID: https://hessian.cn/followme2
* 密码: followme2
> 连接信息可通过menuconfig进行修改
*Captive Portal 配网界面效果*
## Follow me活动介绍
"Follow me活动”是DigiKey联合EEWORLD发起的为期一年的“跟技术大咖学技术,完成任务返现”活动。2023年共有4期,每3个月技术大咖推荐可玩性与可学性较强的开发板/仪器套件,带着大家实际操作。
* [活动首页](https://www.eeworld.com.cn/huodong/digikey_follow_me/)
### 第2期活动任务要求与完成效果演示
#### 任务1:控制屏幕显示中文(必做任务)
> 完成屏幕的控制,并且能显示中文
##### 主界面展示
在这张图片上可以看到我驱动屏幕显示了包括天气、日期、以及网络信息等,其中包含了中文字体。

#### 任务2:网络功能使用(必做任务)
> 完成网络功能的使用,能够创建热点和连接到WiFi
在这个项目这里我做的是基于CaptivePortal实现的web配网,在未配网的情况下启动开发板会进入AP模式同时启动webserver提供CaptivePortal服务。启动开发板后根据屏幕提示用手机连接到指定的AP SSID即可自动进入项目的配网界面。通过配网界面选择需要连接的网络,输入相应的网络密码即可完成网络配置。



#### 任务3:控制WS2812B(必做任务)
> 使用按键控制板载Neopixel LED的显示和颜色切换


#### 任务4:从下方5个分任务中选择1个感兴趣的完成即可(必做任务)
> 分任务1:日历&时钟——完成一个可通过互联网更新的万年历时钟,并显示当地的天气信息
主界面上方有三个区域,分别展示了时钟(通过互联网更新的万年历时钟)、当地天气信息、当前联网状态。

## Adafruit ESP32-S3 TFT Feather简介
Adafruit ESP32-S3 TFT Feather是由开源硬件行业知名公司Adafruit出品的一款富有特色的开源硬件,开发板使用乐鑫ESP32-S3芯片,支持WiFi和蓝牙能,自带高清TFT彩色显示屏。
* [Adafruit ESP32-S3 TFT Feather 产品信息](https://www.adafruit.com/product/5483)
* [Adafruit ESP32-S3 TFT Feather 使用指南](https://learn.adafruit.com/adafruit-esp32-s3-tft-feather)
* [Adafruit ESP32-S3 TFT Feather PCB文件](https://github.com/adafruit/Adafruit-ESP32-S3-TFT-Feather-PCB)
## 程序实现
该项目使用C语言基于ESP-IDF进行开发。项目实现参考了ESP-Box-Lite的代码,自行封装了BSP组件,主要封装了板载TFT LCD(1.14" ST7789)的初始化。
目前该组件已上传至Github: [BSP: Adafruit ESP32-S3 TFT-Feather](https://github.com/HessianZ/bsp_adafruit_esp32_feather)
**项目依赖**
```yaml
## IDF Component Manager Manifest File
dependencies:
idf: ">=5.1"
lvgl/lvgl: "8.3.8"
espressif/esp_lvgl_port: "1.2.0"
espressif/qrcode: ^0.1.0
espressif/json_generator: ^1
espressif/json_parser: =1.0.0
espressif/led_strip: "^2.4.3"
```

**目录结构**
**分区表**
```csv
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
# Name, Type, SubType, Offset, Size, Flags
sec_cert, data, , 0xd000, 0x3000,
nvs, data, nvs, , 0x6000,
otadata, data, ota, , 0x2000,
fctry, data, nvs, , 0x6000,
ota_0, app, ota_0, , 3M,
storage, data, spiffs, , 500K,
```
### 联网功能处理
```c
esp_err_t app_wifi_start(void)
{
ESP_LOGD(TAG, "app_wifi_start() ENTER");
wifi_init_sta();
// 检查是否已配网成功
esp_err_t err = wifi_prov_mgr_is_provisioned(&provisioned);
ESP_ERROR_CHECK(err);
ESP_LOGD(TAG, "isProvisioned %d", provisioned);
// 未配网启动AP,否则启动STA
if (!provisioned) {
// 启动AP
wifi_start_softap();
// 启动CaptivePortal配网服务
start_captive_portal();
// 启动DNS服务器,并重定向所有域名查询到AP的IP地址
dns_server_config_t config = DNS_SERVER_CONFIG_SINGLE("*" /* all A queries */, "WIFI_AP_DEF" /* softAP netif ID */);
dns_server = start_dns_server(&config);
} else {
// 获取网络配置(已配网会自动从NVS加载)
wifi_config_t config;
esp_wifi_get_config(WIFI_IF_STA, &config);
ESP_LOGD(TAG, "WIFI_IF_STA SSID %s / Password: %s", config.sta.ssid, config.sta.password);
// 启动STA
wifi_start_sta();
};
// 阻塞等待网络连接就绪
xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_EVENT, false, true, portMAX_DELAY);
ESP_LOGD(TAG, "app_wifi_start() WAIT_WIFI_CONNECT ---> OK");
// 启动网络时间同步客户端开始同步时间
ESP_LOGD(TAG, "app_wifi_start() APP SNTP INIT");
app_sntp_init();
return ESP_OK;
}
```
### Captive Portal
Captive Portal 中文通常译作“强制主页”或“强制登录门户”。是一个登录Web页面,通常由网络运营商或网关在用户能够正常访问互联网之前拦截用户的请求并将一个强制登录或认证主页呈现(通常是通过浏览器)给用户。该页面可能要求用户输入认证信息、支付、接受某些条款或者其他用户授权等,随后用户才能被授权访问互联网。该技术广泛用于移动和个人宽带服务,包括有线电视、商业WiFi、家庭热点等,也可用于访问企业和住宅区有线网络。
在这个项目中,CaptivePortal是以ESP官方的CaptivePortal demo为基础进行实现的,为了简化开发过程,CaptivePortal中的web静态文件存储到了SPIFF中。
因为Adafruit ESP32-S3 TFT Feathe只搭载了4MB的flash,空间比较有限,我只给SPIFFS分了500K的空间,因此为了平衡体验与空间利用率,WEB 端没有采用像Bootstrap这样的前端框架,而是采用milligram.css + zepto.js 的实现,二者均是针对轻量化进行设计。
**加载SPIFFS文件逻辑**
```c
// HTTP Error (404) Handler - Redirects all requests to the root page
esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
{
char filename[HTTPD_MAX_URI_LEN+10];
sprintf(filename, CONFIG_BSP_SPIFFS_MOUNT_POINT "%s", req->uri);
return send_file_response(req, filename);
}
httpd_handle_t start_captive_portal(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_open_sockets = CONFIG_LWIP_MAX_SOCKETS - 3;
config.lru_purge_enable = true;
config.max_resp_headers = 20;
// Start the httpd server
ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
if (httpd_start(&server, &config) == ESP_OK) {
// Set URI handlers
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(server, &root_action);
httpd_register_uri_handler(server, &generate204_action);
httpd_register_uri_handler(server, &generate_204_action);
httpd_register_uri_handler(server, &config_action);
httpd_register_uri_handler(server, &save_action);
httpd_register_uri_handler(server, &wifi_scan_action);
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
}
return server;
}
static esp_err_t send_file_response(httpd_req_t *req, char* filename)
{
esp_err_t ret = ESP_OK;
FILE *fp = NULL;
char *buf = NULL;
size_t read_len = 0;
fp = fopen(filename, "rb");
ESP_GOTO_ON_FALSE(fp != NULL, ESP_ERR_NOT_FOUND, err, TAG, "Failed to open file %s", filename);
buf = calloc(HTML_BUF_SIZE, sizeof(char));
if (NULL == buf) {
ESP_LOGE(TAG, "Failed to allocate memory for buf");
return ESP_ERR_NO_MEM;
}
if (file_ext_cmp(filename, "js")) {
httpd_resp_set_type(req, "text/javascript");
} else if (file_ext_cmp(filename, "css")) {
httpd_resp_set_type(req, "text/css");
} else {
httpd_resp_set_type(req, "text/html");
}
while ((read_len = fread(buf, sizeof(char), HTML_BUF_SIZE, fp)) > 0) {
httpd_resp_send_chunk(req, buf, read_len);
}
ESP_GOTO_ON_FALSE(feof(fp), ESP_FAIL, err, TAG, "Failed to read file %s error: %d", filename, ferror(fp));
// chunks send finish
httpd_resp_send_chunk(req, NULL, 0);
err:
if (fp != NULL) {
fclose(fp);
}
if (buf != NULL) {
free(buf);
}
if (ret == ESP_ERR_NOT_FOUND) {
ret = httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
}
return ret;
}
```
### 中文字体处理
LVGL内置的CJK宋体对一些符号和汉字的支持也不够,经常有吞字的情况。于是需要自行增加一个字体,通过查询字符表之后用以下命令转成c代码就可以嵌入到我们的工程里。
```bash
lv_font_conv --font ./OPPOSans_L.ttf -r 0x20-0x7F -r 0x2100-0x214F -r 0x3000-0x303F -r 0x4E00-0x9FFF -r 0xFE50-0xFE6F --size 16 --format lvgl --bpp 4 --no-compress -o ~/workspace/esp/esp-followme2/main/gui/font/font_OPPOSans_L_16.c
```
这里我用的是OPPOSans字体16像素大小,指定了常用的ASCII字符、中英文符号和中文字符等范围,足以满足日常文本内容显示的需求。
* 字符范围查询:https://jrgraphix.net/r/Unicode/
* LVGL在线图标转换:https://lvgl.io/tools/imageconverter
* LVGL字体转换程序:https://github.com/lvgl/lv_font_conv
使用方法
```c
LV_FONT_DECLARE(font_OPPOSans_L_16);
lv_obj_t * lab_net_state = lv_label_create(parent);
lv_label_set_recolor(lab_net_state, true);
lv_obj_set_style_text_font(lab_net_state, font_OPPOSans_L_16, LV_PART_MAIN);
```
### NeoPixel LED
开发板上搭载了一颗WS2812可寻址LED。在ESP-IDF环境中,我们可以使用官方组件库中的led_strip进行操作,免去自行编写驱动的过程。
led_strip组件: https://components.espressif.com/components/espressif/led_strip
这里要注意,这个LED的电源是直接由GPIO34提供的,使用前需要先拉高GPIO34进行供电。
```c
#define NEOPIX_GPIO GPIO_NUM_33
#define NEOPIX_PWR_GPIO GPIO_NUM_34
static inline void led_breath(uint32_t *color)
{
if (led_dir) {
(*color) ++;
if (*color == 0xff) {
led_dir = false;
}
} else {
(*color) --;
if (*color == 0) {
led_dir = true;
}
}
}
static void update_led_task(void *arg)
{
switch (led_color) {
case 0:
led_breath(&r);
break;
case 1:
led_breath(&g);
break;
case 2:
led_breath(&b);
break;
}
led_strip_set_pixel(led_strip, 0, r, g, b);
led_strip_refresh(led_strip);
}
static void page_led_init(void)
{
esp_err_t err;
if (!gpio_initialized) {
gpio_config_t io_conf = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL
- 2023-10-19
-
发表了日志:
【得捷电子Follow me第2期】Adafruit ESP32-S3 TFT Feather网络应用开发实践[修改版]
- 2023-09-28
-
回复了主题帖:
【得捷电子Follow me第2期】基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践
小默叔叔 发表于 2023-9-25 18:56
esptool.py v4.7.dev1
Serial port /dev/ttyACM0
Connecting...
Chip is ESP32-S3 (QFN56) (revision v0 ...
这个我也没遇到过,确定进了下载模式吧?不行换个机器换个环境试试
- 2023-09-14
-
加入了学习《基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践》,观看 基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践
- 2023-09-13
-
上传了资料:
基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践项目源代码
-
发表了主题帖:
【得捷电子Follow me第2期】基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践
本帖最后由 囧大大王 于 2023-9-13 16:24 编辑
# 基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践
视频链接: http://training.eeworld.com.cn/video/37657
源码下载:http://download.eeworld.com.cn/detail/%E5%9B%A7%E5%A4%A7%E5%A4%A7%E7%8E%8B/629208
## 项目介绍
该项目是基于Adafruit ESP32-S3 TFT Feather开发板的一个简单的示例,通过该示例可以学习到如何使用Adafruit ESP32-S3 TFT Feather开发板的WiFi、GPIO控制及以Neopixel LED控制等功能。
该项目完成了"Follow me活动”第2期的任务1、任务2、任务3以及任务4的分任务1。
## 功能介绍
* 基于LVGL的多页面图形界面
* 内置 OPPOSans 字体,支持大部分常见汉字及符号的显示
* WiFi热点网页配网
* 日期时间显示,支持网络时钟同步
* 通过网络获取天气信息
* 板载NeoPixel LED控制,呼吸灯闪烁,支持通过按键更换闪烁颜色
在尚未进行配网时启动板卡会自动启动AP热点,通过手机连接该热点会启动CaptivePortal网页引导进行配网,配网成功后会自动关闭AP热点并连接到指定的WiFi SSID。
配网完成后首页会显示已连接的WiFi信息以及IP地址,屏幕上方状态栏会显示日期、时间以及天气信息。
点击屏幕左侧的Boot0按钮可切换显示页面,第二个页面是LED控制页面。进入LED控制页面会自动点亮Neopixel LED呼吸灯,在该页面长按Boot0可控制板载的Neopixel LED的颜色切换,颜色可在红绿蓝三种颜色之间进行切换,离开此页面会自动熄灭Neopixel LED。
内置AP默认连接信息:
* SSID: https://hessian.cn/followme2
* 密码: followme2
> 连接信息可通过menuconfig进行修改
*Captive Portal 配网界面效果*
## Follow me活动介绍
"Follow me活动”是DigiKey联合EEWORLD发起的为期一年的“跟技术大咖学技术,完成任务返现”活动。2023年共有4期,每3个月技术大咖推荐可玩性与可学性较强的开发板/仪器套件,带着大家实际操作。
* [活动首页](https://www.eeworld.com.cn/huodong/digikey_follow_me/)
### 第2期活动任务要求
#### 任务1:控制屏幕显示中文(必做任务)
> 完成屏幕的控制,并且能显示中文
#### 任务2:网络功能使用(必做任务)
> 完成网络功能的使用,能够创建热点和连接到WiFi
#### 任务3:控制WS2812B(必做任务)
> 使用按键控制板载Neopixel LED的显示和颜色切换
#### 任务4:从下方5个分任务中选择1个感兴趣的完成即可(必做任务)
> 分任务1:日历&时钟——完成一个可通过互联网更新的万年历时钟,并显示当地的天气信息
>
> 分任务2:WS2812B效果控制——完成一个Neopixel(12灯珠或以上)控制器,通过按键和屏幕切换展示效果
>
> 分任务3:数据检测与记录——按一定时间间隔连续记录温度/亮度信息,保存到SD卡,并可以通过按键调用查看之前的信息,并在屏幕上绘图
>
> 分任务4:音乐播放功能——实现音乐播放器功能,可以在屏幕上显示列表信息和音乐信息,使用按键进行切换,使用扬声器进行播放
>
> 分任务5:AI功能应用——结合运动传感器,完成手势识别功能,至少要识别三种手势(如水平左右、前后、垂直上下、水平画圈、垂直画圈,或者更复杂手势
#### 任务5:通过网络控制WS2812B(可选任务,非必做)
> 结合123,在手机上通过网络控制板载Neopixel LED的显示和颜色切换,屏幕同步显示状态
## Adafruit ESP32-S3 TFT Feather简介
Adafruit ESP32-S3 TFT Feather是由开源硬件行业知名公司Adafruit出品的一款富有特色的开源硬件,开发板使用乐鑫ESP32-S3芯片,支持WiFi和蓝牙能,自带高清TFT彩色显示屏。
* [Adafruit ESP32-S3 TFT Feather 产品信息](https://www.adafruit.com/product/5483)
* [Adafruit ESP32-S3 TFT Feather 使用指南](https://learn.adafruit.com/adafruit-esp32-s3-tft-feather)
* [Adafruit ESP32-S3 TFT Feather PCB文件](https://github.com/adafruit/Adafruit-ESP32-S3-TFT-Feather-PCB)
## 程序实现
该项目使用C语言基于ESP-IDF进行开发。项目实现参考了ESP-Box-Lite的代码,自行封装了BSP组件,主要封装了板载TFT LCD(1.14" ST7789)的初始化。
目前该组件已上传至Github: [BSP: Adafruit ESP32-S3 TFT-Feather](https://github.com/HessianZ/bsp_adafruit_esp32_feather)
**项目依赖**
```yaml
## IDF Component Manager Manifest File
dependencies:
idf: ">=5.1"
lvgl/lvgl: "8.3.8"
espressif/esp_lvgl_port: "1.2.0"
espressif/qrcode: ^0.1.0
espressif/json_generator: ^1
espressif/json_parser: =1.0.0
espressif/led_strip: "^2.4.3"
```
**目录结构**
**分区表**
```csv
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
# Name, Type, SubType, Offset, Size, Flags
sec_cert, data, , 0xd000, 0x3000,
nvs, data, nvs, , 0x6000,
otadata, data, ota, , 0x2000,
fctry, data, nvs, , 0x6000,
ota_0, app, ota_0, , 3M,
storage, data, spiffs, , 500K,
```
### 联网功能处理
```c
esp_err_t app_wifi_start(void)
{
ESP_LOGD(TAG, "app_wifi_start() ENTER");
wifi_init_sta();
// 检查是否已配网成功
esp_err_t err = wifi_prov_mgr_is_provisioned(&provisioned);
ESP_ERROR_CHECK(err);
ESP_LOGD(TAG, "isProvisioned %d", provisioned);
// 未配网启动AP,否则启动STA
if (!provisioned) {
// 启动AP
wifi_start_softap();
// 启动CaptivePortal配网服务
start_captive_portal();
// 启动DNS服务器,并重定向所有域名查询到AP的IP地址
dns_server_config_t config = DNS_SERVER_CONFIG_SINGLE("*" /* all A queries */, "WIFI_AP_DEF" /* softAP netif ID */);
dns_server = start_dns_server(&config);
} else {
// 获取网络配置(已配网会自动从NVS加载)
wifi_config_t config;
esp_wifi_get_config(WIFI_IF_STA, &config);
ESP_LOGD(TAG, "WIFI_IF_STA SSID %s / Password: %s", config.sta.ssid, config.sta.password);
// 启动STA
wifi_start_sta();
};
// 阻塞等待网络连接就绪
xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_EVENT, false, true, portMAX_DELAY);
ESP_LOGD(TAG, "app_wifi_start() WAIT_WIFI_CONNECT ---> OK");
// 启动网络时间同步客户端开始同步时间
ESP_LOGD(TAG, "app_wifi_start() APP SNTP INIT");
app_sntp_init();
return ESP_OK;
}
```
### Captive Portal
Captive Portal 中文通常译作“强制主页”或“强制登录门户”。是一个登录Web页面,通常由网络运营商或网关在用户能够正常访问互联网之前拦截用户的请求并将一个强制登录或认证主页呈现(通常是通过浏览器)给用户。该页面可能要求用户输入认证信息、支付、接受某些条款或者其他用户授权等,随后用户才能被授权访问互联网。该技术广泛用于移动和个人宽带服务,包括有线电视、商业WiFi、家庭热点等,也可用于访问企业和住宅区有线网络。
在这个项目中,CaptivePortal是以ESP官方的CaptivePortal demo为基础进行实现的,为了简化开发过程,CaptivePortal中的web静态文件存储到了SPIFF中。
因为Adafruit ESP32-S3 TFT Feathe只搭载了4MB的flash,空间比较有限,我只给SPIFFS分了500K的空间,因此为了平衡体验与空间利用率,WEB 端没有采用像Bootstrap这样的前端框架,而是采用milligram.css + zepto.js 的实现,二者均是针对轻量化进行设计。
**加载SPIFFS文件逻辑**
```c
// HTTP Error (404) Handler - Redirects all requests to the root page
esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
{
char filename[HTTPD_MAX_URI_LEN+10];
sprintf(filename, CONFIG_BSP_SPIFFS_MOUNT_POINT "%s", req->uri);
return send_file_response(req, filename);
}
httpd_handle_t start_captive_portal(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_open_sockets = CONFIG_LWIP_MAX_SOCKETS - 3;
config.lru_purge_enable = true;
config.max_resp_headers = 20;
// Start the httpd server
ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
if (httpd_start(&server, &config) == ESP_OK) {
// Set URI handlers
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(server, &root_action);
httpd_register_uri_handler(server, &generate204_action);
httpd_register_uri_handler(server, &generate_204_action);
httpd_register_uri_handler(server, &config_action);
httpd_register_uri_handler(server, &save_action);
httpd_register_uri_handler(server, &wifi_scan_action);
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
}
return server;
}
static esp_err_t send_file_response(httpd_req_t *req, char* filename)
{
esp_err_t ret = ESP_OK;
FILE *fp = NULL;
char *buf = NULL;
size_t read_len = 0;
fp = fopen(filename, "rb");
ESP_GOTO_ON_FALSE(fp != NULL, ESP_ERR_NOT_FOUND, err, TAG, "Failed to open file %s", filename);
buf = calloc(HTML_BUF_SIZE, sizeof(char));
if (NULL == buf) {
ESP_LOGE(TAG, "Failed to allocate memory for buf");
return ESP_ERR_NO_MEM;
}
if (file_ext_cmp(filename, "js")) {
httpd_resp_set_type(req, "text/javascript");
} else if (file_ext_cmp(filename, "css")) {
httpd_resp_set_type(req, "text/css");
} else {
httpd_resp_set_type(req, "text/html");
}
while ((read_len = fread(buf, sizeof(char), HTML_BUF_SIZE, fp)) > 0) {
httpd_resp_send_chunk(req, buf, read_len);
}
ESP_GOTO_ON_FALSE(feof(fp), ESP_FAIL, err, TAG, "Failed to read file %s error: %d", filename, ferror(fp));
// chunks send finish
httpd_resp_send_chunk(req, NULL, 0);
err:
if (fp != NULL) {
fclose(fp);
}
if (buf != NULL) {
free(buf);
}
if (ret == ESP_ERR_NOT_FOUND) {
ret = httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
}
return ret;
}
```
### 中文字体处理
LVGL内置的CJK宋体对一些符号和汉字的支持也不够,经常有吞字的情况。于是需要自行增加一个字体,通过查询字符表之后用以下命令转成c代码就可以嵌入到我们的工程里。
```bash
lv_font_conv --font ./OPPOSans_L.ttf -r 0x20-0x7F -r 0x2100-0x214F -r 0x3000-0x303F -r 0x4E00-0x9FFF -r 0xFE50-0xFE6F --size 16 --format lvgl --bpp 4 --no-compress -o ~/workspace/esp/esp-followme2/main/gui/font/font_OPPOSans_L_16.c
```
这里我用的是OPPOSans字体16像素大小,指定了常用的ASCII字符、中英文符号和中文字符等范围,足以满足日常文本内容显示的需求。
* 字符范围查询:https://jrgraphix.net/r/Unicode/
* LVGL在线图标转换:https://lvgl.io/tools/imageconverter
* LVGL字体转换程序:https://github.com/lvgl/lv_font_conv
使用方法
```c
LV_FONT_DECLARE(font_OPPOSans_L_16);
lv_obj_t * lab_net_state = lv_label_create(parent);
lv_label_set_recolor(lab_net_state, true);
lv_obj_set_style_text_font(lab_net_state, font_OPPOSans_L_16, LV_PART_MAIN);
```
### NeoPixel LED
开发板上搭载了一颗WS2812可寻址LED。在ESP-IDF环境中,我们可以使用官方组件库中的led_strip进行操作,免去自行编写驱动的过程。
led_strip组件: https://components.espressif.com/components/espressif/led_strip
这里要注意,这个LED的电源是直接由GPIO34提供的,使用前需要先拉高GPIO34进行供电。
```c
#define NEOPIX_GPIO GPIO_NUM_33
#define NEOPIX_PWR_GPIO GPIO_NUM_34
static inline void led_breath(uint32_t *color)
{
if (led_dir) {
(*color) ++;
if (*color == 0xff) {
led_dir = false;
}
} else {
(*color) --;
if (*color == 0) {
led_dir = true;
}
}
}
static void update_led_task(void *arg)
{
switch (led_color) {
case 0:
led_breath(&r);
break;
case 1:
led_breath(&g);
break;
case 2:
led_breath(&b);
break;
}
led_strip_set_pixel(led_strip, 0, r, g, b);
led_strip_refresh(led_strip);
}
static void page_led_init(void)
{
esp_err_t err;
if (!gpio_initialized) {
gpio_config_t io_conf = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL
-
发布了课程:
基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践
- 2023-08-25
-
回复了主题帖:
报销返现通知|得捷电子 Follow me 第一期活动
完结撒花,感谢eeworld,感谢DigiKey,感谢CC*V~
- 2023-06-22
-
回复了主题帖:
【得捷电子Follow me第1期】囧大大王的卫星定位音乐盒
sipower 发表于 2023-6-22 11:01
厉害的大王
哈哈 大佬过奖过奖
-
发表了主题帖:
【得捷电子Follow me第1期】囧大大王的卫星定位音乐盒
[localvideo]e331e2ee36c09f2f0247485e6b78e2c7[/localvideo]
本项目是为参加得捷电子Follow me第1期(http://www.eeworld.com.cn/huodong/digikey_follow_me/)活动所作。
该项目是做了一个卫星定位音乐盒,音乐盒为什么需要卫星定位呢?我也不知道啊。
这个项目主要还是为了学习如何使用MicroPython在RP PicoW上进行开发,所以会尽量的去使用项目所提供的各种外设,毕竟是要先完成任务嘛。
该项目主要亮点为使用了asyncio进行异步开发,可实现屏幕刷新、web服务以及定位数据的并发进行。
下面简单介绍这个项目的几个功能,主要分成这几个部分。
一、固定频率的LED闪烁(300ms间隔,本来还打算做个摩斯码闪个ILOVEU来的);
这个功能比较简单,直接上代码。
二、显示欢迎屏幕(SplashScreen)
程序启动后会驱动OLED屏幕在屏幕上显示欢迎信息Hello, EEWorld。
我这里还用了mpremote执行mip安装了ssd1306库,怎么我感觉应该是有内置的呢,奇怪。
三、简谱音乐播放功能,对输入的简谱自动转为声音频率使用PWM驱动蜂鸣器进行音乐播放;
程序实现了一个简单的简谱到声音频率的转换程序,只需要设置好简谱和播放速度即可简单的播放简谱音乐。
四、中文歌词显示,程序内置了《粉刷匠》的歌词字模,在播放音乐的过程中会自动逐字显示歌词;
这个功能比较麻烦一点,MicroPython没有内置中文字库,想要显示中文都会比较麻烦,我这里用了一个比较粗暴但是省事的办法。生成字模后内嵌到代码里,以bitmap的形式进行文字绘制。
字模转换用的是PC2LCD2002。
生成字模后简单处理一下以二维数组的形式嵌入到python代码里。
绘制方法
五、Air530 GNSS定位数据读取与解析,项目启动并联网后会自动读取GNSS模块的数据,并解析的全局变量供其他模块使用;
GNSS模块提供了UART接口可以发送命令和接收数据,为了保证读取、显示和web请求的处理可以并发进行,这里用了asyncio进行了处理,对uart库使用了asyncio.StreamReader进行读取操作。
# GNSS
GNSS_DEBUG = False
class GnssMsg:
talkerId = ''
msgId = ''
data = ''
checksum = ''
def __init__(self, dataStr):
self.talkerId = dataStr[1:3]
self.msgId = dataStr[3:6]
starPos = dataStr.rfind('*')
self.checksum = dataStr[starPos+1:]
self.data = dataStr[7:starPos]
def _parseGSV(self):
part = self.data.split(',')
satelliteCount = int(part[2])
satellites = []
if satelliteCount > 0:
for i in range(3, min(satelliteCount, 4)*4, 4):
try:
satellites.append({
'satelliteId': part[i],
'elevation': int(part[i+1].strip() or 0), # 仰角
'azimuth': int(part[i+2].strip() or 0), # 方位角
'snr': int(part[i+3].strip() or 0), # 00-99 dB (null when not tracking)
})
except IndexError:
break
return {
'msgCount': int(part[0]),
'msgId': int(part[1]),
'satelliteCount': satelliteCount,
'satellites': satellites,
}
def _parseGLL(self):
part = self.data.split(',')
return {
'latitude': part[0],
'northSouth': part[1],
'longitude': part[2],
'westEast': part[3],
}
def _parseZDA(self):
part = self.data.split(',')
fields = [
'utcTime',
'day',
'month',
'year',
'localZoneDescription',
'LocalZoneMinutesDescription',
]
return dict(zip(fields, part))
def _parseRMC(self):
part = self.data.split(',')
fields = [
'utcTime',
'validity', # validity - A-ok, V-invalid
'latitude',
'northSouth',
'longitude',
'westEast',
'speed', # Speed over ground in knots
'trueCourse', # Track made good in degrees True
'dateStamp', # UT date
'variation', # Magnetic variation degrees (Easterly var. subtracts from true course)
'variationDir', # Magnetic variation deg direction
]
return dict(zip(fields, part))
def _parseGGA(self):
part = self.data.split(',')
fields = [
'utcTime',
'latitude',
'northSouth',
'longitude',
'westEast',
'gpsQualityIndicator',
'satellites',
'hdop',
'altitude',
'altitudeUnit',
'geoidalSeparation',
'geoidalSeparationUnit',
'dgpsReferenceStationId',
]
return dict(zip(fields, part))
def parse(self):
if GNSS_DEBUG:
print("Parse" + self.msgId, self.data)
try:
func = getattr(self, "_parse" + self.msgId)
return func()
# except AttributeError:
except:
return None
class Gnss:
uart = 0
location = 0
satelliteCount = 0
time = 0
utcTime = 0
def __init__(self):
uart = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1), timeout=3000)
self.uart = asyncio.StreamReader(uart)
self.location = 0
async def command(self, cmd):
return self.uart.write(bytes(cmd + "\r\n", 'utf-8'))
async def read(self):
data = await self.uart.readline()
try:
if data is not None:
return GnssMsg(data.decode('utf-8'))
print('GNSS: No data')
return None
except:
print('GNSS: Malformed GNSS data', data)
return None
showData = {
"ALT": "--.-M",
"LAT": "---",
"LNG": "---",
"TIME": "00:00:00",
"DATE": "2023-06-20",
}
gnssData = {
}
async def readGnssData(gnss):
gsv = None
while True:
msg = await gnss.read()
if msg is None:
continue
# BD = 北斗
# GP = GPS
# GN = GNSS,全球导航卫星系统
if msg.talkerId == 'BD' or msg.talkerId == 'GP' or msg.talkerId == 'GN':
g = msg.parse()
if g is None:
continue
if msg.talkerId not in gnssData:
gnssData[msg.talkerId] = {}
if msg.msgId == 'GSV':
if g["msgId"] == 1:
gsv = g
elif g["msgId"] == g["msgCount"]:
gnssData[msg.talkerId][msg.msgId] = gsv
if GNSS_DEBUG:
print('GSV', gsv)
elif gsv is not None:
gsv["satellites"] += g["satellites"]
elif msg.msgId == 'ZDA':
gnssData[msg.talkerId][msg.msgId] = g
if 'year' not in g or g['year'] == '':
continue
showData['DATE'] = "%s-%s-%s" % (g['year'], g['month'], g['day'])
showData['TIME'] = "%s:%s:%s" % (g['utcTime'][0:2], g['utcTime'][2:4], g['utcTime'][4:6])
elif msg.msgId == 'GGA' or msg.msgId == 'GLL' or msg.msgId == 'RMC':
gnssData[msg.talkerId][msg.msgId] = g
if GNSS_DEBUG:
print(msg.talkerId, msg.msgId, g)
# invalid RMC data
if 'validity' in g and g['validity'] != 'A':
continue
showData['LAT'] = g['latitude'] or '---'
showData['LNG'] = g['longitude'] or '---'
if 'altitude' in g:
showData['ALT'] = g['altitude'] + g['altitudeUnit']
if 'utcTime' in g and g['utcTime'] != '':
showData['TIME'] = "%d:%s:%s" % (((int(g['utcTime'][0:2])+8)) % 24, g['utcTime'][2:4], g['utcTime'][4:6])
else:
gnssData[msg.talkerId][msg.msgId] = g
if GNSS_DEBUG:
print(msg.talkerId, msg.msgId, g)
六、系统信息显示,在OLED屏幕上以100fps的频率(设定频率)刷新并显示GNSS定位数据、时间和当前IP地址;这里使用了NTP协议获取网络时间。
NTP是网络时间协议,协议非常简单。MicroPython内置了NTPclient支持,通过PICOW的联网功能我们可以轻松的实现网络时间的同步。这里截取代码片段以演示NTP时间同步的使用方法。
七、基于Asyncio的WEB服务器,提供基于网页的GNSS信息显示和地图定位功能。
这里没有贴html1变量的内容,因为贴上来会被WAF拦截。。。有兴趣的小伙伴自己下载源码看就好了
async def requestHandle(reader, writer):
print("Client connected")
request_line = await reader.readline()
# We are not interested in HTTP request headers, skip them
while await reader.readline() != b"\r\n":
pass
startLine = request_line.decode('utf-8').split(' ')
method = startLine[0]
uri = startLine[1]
#print(method, uri)
try:
if method.startswith('GET'):
status = '200 OK'
contentType = 'text/html;Charset=UTF-8'
response = ''
if uri == '/':
rows = ['<tr><th>%s</th><td id="val%s">%s</td></tr>' % (k, k, showData[k]) for k in showData]
response = html1 % '\n'.join(rows)
elif uri == '/api/gnss':
contentType = 'application/json;Charset=UTF-8'
response = json.dumps(gnssData)
#ret = coordTransform_utils.ddmmtoddd(11608.37174, 2417.98904)
#print(ret)
#ret = coordTransform_utils.wgs84_to_gcj02(float(ret[0]), float(ret[1]))
#print(ret)
else:
status = '404 NotFound'
response = '404'
else:
status = '404 NotFound'
response = '404'
writer.write('HTTP/1.0 %s\r\nContent-type: %s\r\n\r\n' % (status, contentType))
writer.write(response)
except Exception as inst:
writer.write('HTTP/1.0 500 InternalServerError\r\nContent-type: text/html;Charset=UTF-8\r\n\r\n')
writer.write("<html><body><h1>Internal Server Error</h1><pre>%s\n%s\n%s</pre></html></body>" % (type(inst), inst.args, inst))
print(type(inst)) # the exception type
print(inst.args) # arguments stored in .args
print(inst) # __str__ allows args to be printed directly, but may be overridden in exception subclasses
finally:
try:
await writer.drain()
await writer.wait_closed()
print("Client disconnected")
except:
pass
对本活动的心得体会(包括意见或建议)
啊这。。。完成任务已经很难了,下次要求再简单一点就更好了。
源代码下载:
外设文档
Seeed Groove生态系统说明 https://wiki.seeedstudio.com/Grove_System/
扩展板GrooveShieldForPiPico https://wiki.seeedstudio.com/Grove-Starter-Kit-for-Raspberry-Pi-Pico/
Air530GPS模块 https://wiki.seeedstudio.com/Grove-GPS-Air530/
SSD1315 OLED模块 https://wiki.seeedstudio.com/Grove-OLED-Display-0.96-SSD1315/
工具链接
MicroPython固件下载 https://micropython.org/download/
合宙在线GPS定位纠偏 https://www.openluat.com/GPS-Offset.html
MU Editor https://codewith.mu/
参考资料
MicroPython官方文档 https://docs.micropython.org/en/latest/library/index.html
《micropython esp8266+ssd1306(OLED) 显示中文(示例)》 https://developer.aliyun.com/article/658192
NMEA 协议定义 http://aprs.gids.nl/nmea/
《GPS坐标系转换》 https://www.jianshu.com/p/24702227eb5e
MicroPython在线文档 https://docs.micropython.org/en/latest/
蜂鸣器音符频率参考 http://bbs.eeworld.com.cn/thread-1247195-1-1.html
- 2023-06-20
-
加入了学习《Digi-Key: Follow Me 系列(1) 直播回放及答疑记录》,观看 现场答疑记录
-
加入了学习《Digi-Key: Follow Me 系列(1) 直播回放及答疑记录》,观看 Raspberry Pi Pico W 使用入门