基于Adafruit ESP32-S3 TFT Feather的网络应用开发实践

image

视频链接: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 配网界面效果 image

Follow me活动介绍

"Follow me活动”是DigiKey联合EEWORLD发起的为期一年的“跟技术大咖学技术,完成任务返现”活动。2023年共有4期,每3个月技术大咖推荐可玩性与可学性较强的开发板/仪器套件,带着大家实际操作。

第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简介

image Adafruit ESP32-S3 TFT Feather是由开源硬件行业知名公司Adafruit出品的一款富有特色的开源硬件,开发板使用乐鑫ESP32-S3芯片,支持WiFi和蓝牙能,自带高清TFT彩色显示屏。

程序实现

该项目使用C语言基于ESP-IDF进行开发。项目实现参考了ESP-Box-Lite的代码,自行封装了BSP组件,主要封装了板载TFT LCD(1.14" ST7789)的初始化。 目前该组件已上传至Github: BSP: Adafruit ESP32-S3 TFT-Feather

项目依赖

## 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"

目录结构 image

分区表

# 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,

联网功能处理

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文件逻辑

// 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代码就可以嵌入到我们的工程里。

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

使用方法

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进行供电。


#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<<NEOPIX_PWR_GPIO),
                .intr_type = GPIO_INTR_DISABLE,
                .pull_down_en = 0,
                .pull_up_en = 0,
        };
        err = gpio_config(&io_conf);
        if (err != ESP_OK) {
            ESP_LOGE(TAG, "configure GPIO for NEOPIX_PWR failed");
        }
        gpio_initialized = true;
    }

    err = gpio_set_level(NEOPIX_PWR_GPIO, 1);

    if (err != ESP_OK) {
        ESP_LOGE(TAG, "gpio_set_level GPIO for NEOPIX_PWR failed");
    }

    ESP_LOGI(TAG, "NEOPIX_PWR ON");


/* LED strip initialization with the GPIO and pixels number*/
    led_strip_config_t strip_config = {
            .strip_gpio_num = NEOPIX_GPIO, // The GPIO that connected to the LED strip's data line
            .max_leds = 1, // The number of LEDs in the strip,
            .led_pixel_format = LED_PIXEL_FORMAT_GRB, // Pixel format of your LED strip
            .led_model = LED_MODEL_WS2812, // LED strip model
            .flags.invert_out = false, // whether to invert the output signal (useful when your hardware has a level inverter)
    };

    led_strip_rmt_config_t rmt_config = {
            .clk_src = RMT_CLK_SRC_DEFAULT, // different clock source can lead to different power consumption
            .resolution_hz = 10 * 1000 * 1000, // 10MHz
            .flags.with_dma = false, // whether to enable the DMA feature
    };

    ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));


    const esp_timer_create_args_t periodic_timer_args = {
            .callback = &update_led_task,
            /* name is optional, but may help identify the timer when debugging */
            .name = "periodic"
    };

    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
}

// LED 控制页面渲染逻辑
void page_led_render(lv_obj_t *parent)
{
    ESP_LOGD(TAG, "render start");

    if (parent == NULL) {
        return;
    }

    page_led_init();

    lv_obj_t *lab_text = lv_label_create(parent);
    lv_label_set_recolor(lab_text, true);
    lv_obj_set_style_text_font(lab_text, main_font, LV_PART_MAIN);
    lv_obj_set_align(lab_text, LV_ALIGN_CENTER);
    lv_label_set_text_fmt(lab_text, "#F2C161 LED控制#\n#CCCCCC 长按Boot0切换LED颜色#");

    // 开始呼吸灯 1000us = 1ms
    ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 1000));

    ESP_LOGD(TAG, "render end");
}


参考资料

  • FreeRTOS官方文档:https://www.freertos.org/zh-cn-cmn-s/freertos-core/overview.html
  • ESP-IDF官方文档:https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/getstarted/index.html
  • Unicode字符范围表:https://jrgraphix.net/r/Unicode/

Read: 14

彻底删除Docker Desktop for Mac

Docker Desktop for Mac 4.6 增加了一个新特性VirtioFS(戳这里看Docker官方Blog介绍),虽然截止目前(4.7版本)为止还是实验性的(Experimental),但是98%的文件读写性能提升还是太有诱惑力了。

昨天手欠开了一下想尝试一番,结果点击Apply之后就一直转圈圈,等了2个钟头之后我放弃了。然后重启Docker,发现无法启动,这下就懵了。

Docker Desktop stopped

通过查看日志发现com.docker.virtualization报错:Invalid virtual machine configuration.

无奈之下只好选择重新安装。

结果不管是删除后重新安装还是降级到4.5,还是一样的问题。始终无法正常启动,猜测应该是有配置没删干净。期间尝试各种姿势删除Docker,包括官方提供的命令行删除也没有起作用。

最后经过一番查找分析,得出以下命令:

sudo rm -rf /Applications/Docker.app 
sudo rm -rf /private/var/folders/0y/frd89c5s3yx4pg6fjbn9d8j00000gn/C/com.electron.dockerdesktop 
rm -rf ~/Library/Containers/com.docker.docker 
rm -rf ~/Library/Application\ Support/Docker\ Desktop 
rm -rf ~/Library/Saved\ Application\ State/com.electron.dockerdesktop.savedState 
rm -rf ~/Library/HTTPStorages/com.docker.docker 
rm -rf ~/Library/Caches/com.docker.docker 
rm -rf ~/Library/Group\ Containers/group.com.docker 
rm -rf ~/Library/Application\ Support/com.bugsnag.Bugsnag/com.docker.docker

注意第二行的路径需要自己查找一下,中间部分会有区别。

Read: 820

虚拟机硬盘空间增加方法QEMU(QCOW) LVM

# 先停止虚拟机
virsh stop test-server-1
# 修改磁盘镜像大小
qemu-img resize test-server-data +100G
# 进入虚拟机后pvresize
pvresize -v /dev/vdb
# 扩展lv空间
lvextend -l +100%FREE /dev/mapper/vgdata-data
# 调整文件系统大小,这里是ext4的文件系统,用resize2fs,在此之前还需要先执行e2fsck
e2fsck -f /dev/mapper/vgdata-data
resize2fs /dev/mapper/vgdata-data
# 最后挂载磁盘
mount -a

完成

Read: 195

改hosts不生效?教你清理Chrome的DNS缓存

在进行web开发的时候,我们经常会修改hosts文件进行测试,但是偶尔会发现改了hosts文件并不能立刻生效。这是由于浏览器自身对DNS(域名指向)是有进行缓存的,除了缓存之外,由于HTTP1.1支持连接复用,如果之前打开过这个页面,那么即使清理了DNS缓存也会因为复用连接再继续连接到旧的域名指向地址。如果出现连接被复用的情况就需要手动关闭活跃连接了。

查看实际连接地址

查看实际实际连接地址可以通过开发工具的网络面板进行查看。

清理缓存方法

1、在地址栏输入:chrome://net-internals

2、在DNS选项卡下,点击“Clear host cache”,清空缓存;

关闭活跃连接方法

1、在地址栏输入:chrome://net-internals;

2、在Sockets选项卡下,关闭活跃的连接;

再附赠一个清除MacOS DNS缓存的方法

sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

上面三招下来还不行,再来捶我。

Read: 419

Nuxt不停机部署指南

在我们的服务器环境中有不少Nuxt应用,但是项目的配置都有问题,会导致项目部署重启的时候会不可用,提示 502 Bad Gateway。而且启动服务的时候总会有奇怪的错误信息,而真正的错误日志又看不到。

下面直接讲正确部署与重启的姿势是怎样。

配置服务

首先我们需要在工程根目录创建一个PM2环境配置文件 ecosystem.config.js

module.exports = {
apps: [
{
name: '项目名称', // 如 ks.mbachina.com
exec_mode: 'cluster', // 启动模式,cluster为集群,无需修改
instances: 'max', // // 实例数量,max为CPU核心数,无需修改
script: './node_modules/nuxt/bin/nuxt.js', // 无需修改
args: 'start' // 启动参数,通常无需修改
}
]
}

构建工程

这里没什么特别的,本地通过 npm run build 或者通过Jenkins进行构建部署到服务器。

需要注意的点是需要将上面的 ecosystem.config.js 部署到服务器上的项目根目录。

启动服务(创建服务)

cd 工程目录
pm2 start

执行上面的命令之后就会自动创建出pm2服务了。

重启服务(不停机重启)

pm2 reload $项目名称

将上面命令的 $项目名称 替换为在ecosystem.config.js中配置的实际项目名称即可。

注意这里用的是reload命令,不要用restart命令。

再次提示,更新代码后只需要执行reload命令即可,不需要先stop、delete再start。

参考:https://nuxtjs.org/docs/2.x/deployment/deployment-pm2

Read: 163