[Digi-Jet Follow me Issue 3] + Multifunctional desktop ornaments based on lvgl graphics library driver (complete project)
[Copy link]
This post was last edited by genvex on 2023-12-15 20:02
This project will include the following:
Task 1: Install and use Micropython---(Task 1)
Micropython is really good.
Task 2: Porting lvgl based on SSD1306 ---- (Task 2)
Using lvgl to drive oled_ssd1036 is very smooth.
Task 3: Music Album --- (Task 3)
Play midi while flipping through the photo album.
Task 4: Apple-style clock --- (Task 4)
Network synchronization time to ensure no delay
Task 5: Weather Forecast Station----(Task 4)
Weather forecast station + gif animation.
5. Acceleration data collection---(Task 5)
Additional Content
1. The temperature and humidity monitor
is a high-imitation temperature and humidity sensor, and the data is also written to the SD card synchronously.
2. Sound spectrum analysis,
sound fast Fourier analysis, sound visualization
3. LVGL watch dial
High-end, classy dial.
1. Install and use Micropython---(Task 1)
The Arduino environment will come with the esptool tool for burning. Put the bin file into the same directory as the esptool tool where the Arduino is located:
esptool.exe --chip esp32c3 --port COM71 erase_flash
Run the cmd command line in the esptool directory
And run the following command:
esptool.exe --chip esp32c3 --port COM71 --baud 921600 --before default_reset --after hard_reset --no-stub write_flash --flash_mode dio --flash_freq 80m 0x0 ESP32_GENERIC_C3-20231005-v1.21.0.bin
Among them, COM71 needs to be replaced with the serial port number actually used by the computer, and ESP32_GENERIC_C3-20231005-v1.21.0.bin needs to be replaced with the actual MicroPython firmware file name used.
Open XiaoC3 in Thonny.
Next, we directly use Thonny to connect ESP32C3 to run the Hello World program. We can see the contents of the fresh and hot firmware and see that MicorPython has been making progress. Let's congratulate it and give it a kiss. Then it's 88.
2. Porting lvgl based on SSD1306 ---- (Task 2)
Xiao-expansion-board has expanded multiple grove interfaces and is also equipped with an entry-level ssd1306_oled. According to the general practice, ssd1306 should be driven by an ancient power-consuming artifact such as u8g2. However, with the emergence of many high-performance embedded development boards, the speed of graphics driver updates and iterations has also accelerated, and there are more options for driving ssd1306. We deserve the best. In this project, LovyanGFX graphics driver library is used to drive ssd1306. LovyanGFX library is inspired by TFT_eSPI and deeply modified. It is the favorite of many mass makers. It can not only drive common LCD screens, but also support some oled screens. Using this library, the simplest entry-level ssd1306 oled screen can also share the drawing functions that only LCD screens have. The emergence of LVGL (Light and Versatile Graphics Library) provides a more elegant solution for embedded development in the presentation of final products. Compared with traditional graphics libraries, LVGL is more suitable for embedded systems. It has certain advantages in performance, resource usage and flexibility. The biggest highlight of this project is the use of lvgl to drive ssd1306. Because lvgl has very superior cross-platform portability, the application of ssd1306 will be even more exciting.
{ //进行总线控制的设置。
auto cfg = _bus_instance.config(); // 获取总线设置的结构。
// I2C设置
cfg.i2c_port = 0; // (0 or 1)
cfg.freq_write = 400000; // 写速
cfg.freq_read = 400000; // 收速
cfg.pin_sda = 6; // SDA引脚编号
cfg.pin_scl = 7; // SCL引脚编号
cfg.i2c_addr = 0x3C; // I2C地址
_bus_instance.config(cfg); // 将设定值从吃谧芟呱稀
_panel_instance.setBus(&_bus_instance); // 把总线设置在面板上。
}
Lvgl driver's core image brushing function
static void lvgl_begin(void)
{
// #define DISP_BUF_SIZE (LVGL_HOR_RES * 100)
lcd.init();
lcd.setRotation(2);
// lcd.setBrightness(100);
lv_init();
static lv_disp_draw_buf_t draw_buf;
size_t DISP_BUF_SIZE = sizeof(lv_color_t) * (LVGL_HOR_RES * 20); // best operation.
// static lv_color_t *buf1 = (lv_color_t *)heap_caps_malloc(
// DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
// static lv_color_t *buf2 = (lv_color_t *)heap_caps_malloc(
// DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
static lv_color_t *buf1 = (lv_color_t *)heap_caps_malloc( // most fast operation.
DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
static lv_color_t *buf2 = (lv_color_t *)heap_caps_malloc(
DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_BUF_SIZE); /*Initialize the display buffer*/
/*-----------------------------------
* Register the display in LVGL
*----------------------------------*/
static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
/*Set up the functions to access to your display*/
/*Set the resolution of the display*/
disp_drv.hor_res = LVGL_HOR_RES;
disp_drv.ver_res = LVGL_VER_RES;
/*Used to copy the buffer's content to the display*/
disp_drv.flush_cb = my_disp_flush;
/*Set a display buffer*/
disp_drv.draw_buf = &draw_buf;
/*Finally register the driver*/
oled = lv_disp_drv_register(&disp_drv);
}
After the successful port, the benkmark test driver ran with an excellent score of 90fps.
3. Music Album---(Task 3)
A MIDI music decoding library was customized to achieve midi music freedom, background timer switching, and long-press button switching music, which greatly enriched the playability of the device. The song "My Motherland and I" allowed me to enjoy an immersive patriotic education and cleansed my manic mind.
void MonitorModel::play(int tempo, uint8_t looping, const char *song) // the key function here.
{
static unsigned long music_tick = millis(); // caculate the time.
if (song && this->song != song) // the most import change here.
{
this->setSong(song);
}
if (this->paused)
{ // check if stop by the control
return;
}
if (millis() - music_tick >= tempo) // 结束的时候 超过了 1节拍的时长。
{ // where dose the tempo come from. how low will it play. the tempo is fixed, by the song.
// if (this->p && *(this->p))
if (this->p)
{
char note[4];
sscanf(this->p, "%[^,]", note); // difficult point here.
// Serial.println(note);
this->playNote(note);
this->p = strchr(this->p, ','); // the pointer arrives at the next","
if (this->p)
{ // not empty(still has "," in the sorn list) ,will keep going to next tone.
++(this->p); // next char of the ",",the real freq index string.
}
}
else // outer judgement.
{
if (looping)
{ // repeat the song or not !
this->p = this->song;
}
else
{
this->stopped = 1; // move on till the end of the song
// noTone(this->pinNumber);
speaker.mute();
Serial.println("End");
}
}
music_tick = millis();
}
}
The key function of playing music plays music at a specified rhythm and supports pause, song switching (long press the user button) and loop playback. In order to achieve non-blocking playback (non-blocking means that you can work and slack off at the same time), the function records the current time to check whether it is necessary to switch to a new tone. The most important function is to decode the midi audio data sequence.
The data format of Midi tracks:
const char *my_people_my_country = "#,#,A#5,A#5,A#5,A#5,C6,C6,C6,...#”
Each note is separated by a comma, and then the function determines whether a beat point has been reached based on the set beat time (the duration of the tone). If a beat point has been reached, the function parses the notes in the string and plays the corresponding notes.
Note recognition uses a map container to quickly query the corresponding note frequency value based on the note string. See the source code for the specific implementation method.
std::map<String, int> tones = {
{"C0", 16}, {"C#0", 17}, {"D0", 18},,,,{"A#9", 14917}, {"B9", 15804}};
The logic of parsing the notes in the string is that the function will move the pointer to the next comma position to prepare to play the next note. If there is still a comma, the function will continue to play the next note. If no comma is detected, it indicates that the playback is over. After the song is finished, it will check whether to loop it.
The playback logic of this set is very clever and interesting. It uses containers to implement MIDI note decoding. It is a feasible solution for MIDI music playback. According to the freeRTOS features of esp32, it is also possible to continue to develop the function of playing different audios at the same time.
4. Apple Style Clock --- (Task 4)
After booting up, the computer can be connected to the network to synchronize the local time. The Apple-style clock graphical interface can be easily realized through lvgl's simple code line drawing, rounded rectangle, and label functions. It can be said that it is a must-learn operation for beginners of lvgl. The time refresh and the jumping colon as the second hand indication are completed by two timers, so that the clock can run independently without being disturbed by other programs.
void TimerController::onViewLoad() {
// model.init();
// view.create();
view.load(); // setup the lvgl display enviroment.
updateTimer = lv_timer_create( // start the timers.
[](lv_timer_t* timer) {
TimerController* controller = (TimerController *)timer->user_data;
controller->update(); // get the time
},
1000,
this
);
tickTimer = lv_timer_create(
[](lv_timer_t* timer) {
TimerController* controller = (TimerController *)timer->user_data;
controller->updateSegLabel(); // show the second flashing effect.
},
1000,
this
);
}
5. Weather Forecast Station----(Task 4)
A development board that cannot implement a clock and weather forecast is not a good development board
------Lu Xun
The json format data is obtained from the toy weather information service site provided by Amap, and then parsed using the ArduinoJson library to extract key information.
{"status":"1","count":"1","info":"OK","infocode":"10000","lives":[{"province":"广东","city":"佛山市","adcode":"440600","weather":"阴","temperature":"20","winddirection":"北","windpower":"≤3","humidity":"75","reporttime":"2023-11-29 09:00:59","temperature_float":"20.0","humidity_float":"75.0"}]}
void WeatherModel::getWeather()
{
if (WiFi.status() != WL_CONNECTED)
{
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.println("Connecting to WiFi...");
}
}
// 定义一个全局变量作为连接成功的标志
bool connection_success = false;
Serial.println("Connected to WiFi");
HTTPClient http;
http.begin("https://restapi.amap.com/v3/weather/weatherInfo?city=" + cityCode + "key=" + userKey);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK)
{
String payload = http.getString();
Serial.println(payload);
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
this->live = doc["lives"][0];
this->temperature = live["temperature_float"].as<String>();
this->humidity = live["humidity"].as<String>();
this->city = live["city"].as<String>();
this->weather = live["weather"].as<String>();
this->winddirection = live["winddirection"].as<String>();
this->windpower = live["windpower"].as<String>();
// // Serial.print("城市:");
// Serial.println(live["city"].as<String>());
// Serial.print("天气:");
// Serial.println(live["weather"].as<String>());
// Serial.print("温度:");
// Serial.println(live["temperature"].as<String>());
// Serial.print("湿度:");
// Serial.println(live["humidity"].as<String>());
}
else
{
Serial.printf("HTTP request failed with error %d\n", httpCode);
}
http.end();
// WiFi.disconnect();
}
The weather data is displayed in the form of text scrolling upwards, with a small gif animation placed next to the text, which adds a lot of vitality to the entire page (everyone is tired of seeing astronauts). The advantage of using gif in LVGL is that the animation does not need to be converted frame by frame in advance, and a direct gif conversion is sufficient. In addition, it has many built-in animation processes, making the page more vivid.
5. Acceleration data collection---(Task 5)
Implement a simple acceleration data collection interface, which includes a chart and a label. First, create a root object and set it, including size, position and style. Create a chart object to display the data of the acceleration sensor, and provide the layout and style settings of the chart and label.
void MpuView::create()
{
/** screen */
lv_obj_t *root = lv_obj_create(NULL);
// lv_obj_add_style(ui.root, &style_scr, 0);
// lv_obj_set_flex_flow(root, LV_FLEX_FLOW_ROW); // arrange as row.
lv_obj_clear_flag(root, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_size(root, LV_HOR_RES, LV_VER_RES);
lv_obj_center(root);
lv_obj_set_style_pad_all(root, 0, 0);
lv_obj_set_style_bg_color(root, lv_color_black(), 0);
lv_obj_set_style_bg_opa(root, LV_OPA_100, 0);
ui.root = root;
// lv_obj_set_flex_grow(ui.city, 1);
static lv_style_t style;
lv_style_init(&style);
/*Make a gradient*/
lv_style_set_bg_color(&style, lv_color_black());
// lv_style_set_border_opa(&style, LV_OPA_100);
lv_style_set_bg_opa(&style, LV_OPA_100);
lv_obj_t *chart = lv_chart_create(root);
// lv_obj_remove_style_all(game_station);
lv_obj_set_size(chart, LV_PCT(100), LV_PCT(80));
lv_obj_align(chart, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_add_style(chart, &style, 0);
// lv_obj_move_background(game_station);
// lv_obj_clear_flag(chart, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
// lv_obj_set_style_pad_all(chart, 0, 0);
lv_obj_set_style_border_color(chart, lv_color_black(), 0);
lv_obj_set_style_radius(chart, 0, 0);
lv_chart_set_axis_tick(chart, LV_CHART_AXIS_PRIMARY_Y, 5, 3, 5, 2, true, 10);
lv_chart_set_point_count(chart, 100);
lv_chart_set_div_line_count(chart, 0, 0);
ui.chart = chart;
lv_chart_set_type(chart, LV_CHART_TYPE_LINE); /*Show lines and points too*/
lv_obj_set_style_size(chart, 0, LV_PART_INDICATOR);
/*Add two data series*/
ser1 = lv_chart_add_series(chart, lv_color_white(), LV_CHART_AXIS_PRIMARY_Y);
ser2 = lv_chart_add_series(chart, lv_color_white(), LV_CHART_AXIS_SECONDARY_Y);
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, -200,200);
lv_chart_set_range(chart,LV_CHART_AXIS_SECONDARY_Y,-200,200);
lv_obj_t *labelaccX = lv_label_create(root);
lv_obj_set_size(labelaccX, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_label_set_text_fmt(labelaccX, "X:%.2f,Y:%.2f", 0.25, 0.25);
lv_obj_align(labelaccX, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_set_style_text_color(labelaccX, lv_color_white(), 0);
lv_obj_set_style_text_font(labelaccX, &lv_font_montserrat_16, 0);
ui.labelaccX = labelaccX;
}
The sensor used is the MPU6886 accelerometer
Additional Content
1. Temperature and humidity monitor
Initialize the timer when the view loads, and set the timer's callback function to perform related operations regularly. These include refreshing data, updating environmental information, and writing data to the SD card. The updateTimer timer triggers the callback function every 100 milliseconds to refresh the data displayed on the screen. The updateTH timer triggers the callback function every 500 milliseconds to update environmental data. The dataWriteTimer timer triggers the callback function every 1000 milliseconds to write data to the SD card.
void EnviromentController::onViewLoad() {
// model.init(); // will affect the program, if it load many times.
// view.create();
view.load();
updateTimer = lv_timer_create(
[](lv_timer_t* timer) {
EnviromentController* controller = (EnviromentController *)timer->user_data;
controller->flashData();
},
100,
this
);
updateTH = lv_timer_create(
[](lv_timer_t* timer) {
EnviromentController* controller = (EnviromentController *)timer->user_data;
controller->model.dataUpdate();
},
500,
this
);
}
Environmental Monitoring Program Framework Diagram
The temperature and humidity data are successfully written to the SD card.
(2) Sound visualization
The main content of sound visualization is to decompose the sound signal into various frequency components, obtain a floating-point array through Fourier transform calculation, and then update the value and peak of the bar chart according to the conditional judgment. The first-order lag smoothing process is used to make the change of the bar chart more stable. The key link of drawing graphics is to draw the spectrum object as a container. The drawing does not use the conventional preset drawing function of lvgl, but uses the underlying drawing method.
static void spectrum_draw_event_cb(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_DRAW_POST) {
lv_obj_t *obj = lv_event_get_target(e);
lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e);
lv_draw_rect_dsc_t draw_rect_dsc;
lv_draw_rect_dsc_init(&draw_rect_dsc);
draw_rect_dsc.bg_opa = LV_OPA_100;
draw_rect_dsc.bg_color = lv_color_white();
lv_draw_line_dsc_t draw_line_dsc;
lv_draw_line_dsc_init(&draw_line_dsc);
draw_line_dsc.width = 2;
draw_line_dsc.color = lv_color_white();
for (int i = 0; i < SAMPLE_SIZE; i++) {
lv_area_t _rect;
_rect.x1 = i * x_step;
_rect.x2 = i * x_step + 3;
_rect.y1 = CENTER_Y - int(bar_chart[i] / 2);
_rect.y2 = CENTER_Y + int(bar_chart[i] / 2);
lv_draw_rect(draw_ctx, &draw_rect_dsc, &_rect);
lv_point_t above_line[2];
/* upside line always 2 px above the bar */
above_line[0].x = i * x_step;
above_line[0].y = CENTER_Y - int(bar_chart_peaks[i] / 2) - 2;
above_line[1].x = i * x_step + 3;
above_line[1].y = CENTER_Y - int(bar_chart_peaks[i] / 2) - 2;
lv_draw_line(draw_ctx, &draw_line_dsc, &above_line[0],
&above_line[1]);
lv_point_t blow_line[2];
/* under line always 2 px below the bar */
blow_line[0].x = i * x_step;
blow_line[0].y = CENTER_Y + int(bar_chart_peaks[i] / 2) + 2;
blow_line[1].x = i * x_step + 3;
blow_line[1].y = CENTER_Y + int(bar_chart_peaks[i] / 2) + 2;
lv_draw_line(draw_ctx, &draw_line_dsc, &blow_line[0],
&blow_line[1]);
}
}
}
We use an amplitude update function (bar_value_update) to handle the work of adapting the spectrum and screen size. It accepts a floating-point array mag calculated by Fourier transform, loops through every two elements of the mag array, calculates their average value ave, and compares it with the window height to get the value of the bar chart. The overall logic is to calculate the weighted average, and then update the value and peak of the bar chart based on the conditional judgment, and uses first-order lag smoothing to make the change of the bar chart more stable. The key link in drawing graphics is to draw the spectrum object as a container (spectrum_draw_event_cb). The drawing does not use the preset drawing function of lvgl, but uses the underlying drawing method. The drawing actually occurs in the LV_EVENT_DRAW_POST (after the drawing is completed) event, which will draw the spectrum object.
The whole process uses the mechanisms of drawing rectangles (lv_draw_rect) and drawing lines (lv_draw_line). The rectangle represents the instantaneous spectrum intensity, and the line is used to represent the hysteresis response of the spectrum peak. Since the width of the line is 2 pixels, it also looks like a small cuboid. It draws rectangles and two lines by looping through an array of spectrum analysis result data SAMPLE_SIZE, where bar_chart and bar_chart_peaks are data arrays used to determine the height of the bar chart. The bar chart is quickly drawn on the spectrum_obj object, and is updated in real time through the height data of the bar chart. Even on the black and white Oled screen, it can respond in time and produce good dynamic effects.
Music analyzed by FFT is like a colorful musical painting. It decomposes the originally complex sound signal into various frequency components. The music presents a delicate and orderly spectrum. Its internal structure and emotional depth are clearly analyzed. The song is decomposed into thousands of subtle sound colors. Each spectrum point is like a window to the soul of music, allowing various tones, rhythms and emotions in the music to be presented in a delicate and profound way, so that people can seem to listen to the light jumping between the notes and feel the rhythm and charm of the soul of music. We can feel the jumping of notes, the ups and downs of the melody, and the overall charm of the music, interweaving an intoxicating musical picture. We can see the unique relationship between notes and scales, and feel the infinite possibilities hidden in the music, as if opening the magic box of music, allowing us to glimpse the beauty hidden in the music.
(3) Round screen function display
The lvgl driver and watch function of the round screen have been completed, and the effect is amazing.
void showCurrentTime(lv_timer_t *timer)
{
struct tm info;
getLocalTime(&info);
lv_img_set_angle(img_hour, (info.tm_hour * 300 + info.tm_min * 5) % 3600);
lv_img_set_angle(img_minute, info.tm_min * 60 + info.tm_sec);
lv_img_set_angle(img_second, info.tm_sec * 60);
#if smooth
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, img_second);
lv_anim_set_exec_cb(&a, sec_img_cb);
lv_anim_set_values(&a, (info.tm_sec * 60) % 3600,
(info.tm_sec + 1) * 60);
lv_anim_set_time(&a, 1000);
lv_anim_start(&a);
#endif
}
void setup()
{
Serial.begin(115200); // prepare for possible serial debug
Serial.println("XIAO round screen - LVGL_Arduino");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.println("Connecting to WiFi...");
}
configTime(GMT_OFFSET_SEC, DAY_LIGHT_OFFSET_SEC, NTP_SERVER1, NTP_SERVER2);
struct tm timeinfo;
if (!getLocalTime(&timeinfo))
{
Serial.println("No time available (yet)");
return;
}
Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
lv_init();
lv_xiao_disp_init();
lv_xiao_touch_init();
tf.init();
// tf.readFile("/file.txt");
// lv_fs_if_init();
lv_fs_test();
lv_obj_t *gif = lv_gif_create(lv_scr_act());
lv_gif_set_src(gif, &fighters);
lv_obj_center(gif);
lv_obj_t *connecting = lv_label_create(gif);
lv_label_set_text(connecting, "Connecting to WiFi...");
lv_obj_center(connecting);
long n = 3000;
while (n--)
{
lv_timer_handler();
}
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(10);
lv_timer_handler();
Serial.println("Connecting to WiFi...");
}
bg = lv_img_create(lv_scr_act());
lv_img_set_src(bg, &watch_bg);
lv_img_set_zoom(bg, 310);
lv_obj_center(bg);
img_hour = lv_img_create(bg);
lv_img_set_src(img_hour, &hour);
lv_obj_set_align(img_hour, LV_ALIGN_CENTER);
lv_img_set_zoom(img_hour, 310);
lv_img_set_antialias(img_hour, true);
img_minute = lv_img_create(bg);
lv_img_set_src(img_minute, &minute);
lv_img_set_zoom(img_minute, 310);
lv_obj_set_align(img_minute, LV_ALIGN_CENTER);
// lv_img_set_antialias(img_minute,true);
img_second = lv_img_create(bg);
lv_img_set_src(img_second, &second);
lv_img_set_zoom(img_second, 310);
lv_obj_set_align(img_second, LV_ALIGN_CENTER);
struct tm info;
getLocalTime(&info);
lv_img_set_angle(img_hour, (info.tm_hour * 300 + info.tm_min * 5) % 3600);
lv_img_set_angle(img_minute, info.tm_min * 60 + info.tm_sec);
lv_img_set_angle(img_second, info.tm_sec * 60);
timer1 = lv_timer_create(showCurrentTime, 1000, NULL);
}
Summary of the full text:
The innovations and highlights of this article include:
(1) Use the LovyanGFX graphics driver library to drive SSD1306, implement non-blocking audio playback, and write temperature and humidity data to the SD card.
(2) The implementation of sound visualization and light intensity monitoring system makes the equipment more practical and interesting.
(3) The music album function is realized by customizing the MIDI music decoding library, which realizes functions such as background timer switching and long press button to switch music, enriching the playability of the device.
(4) Implementation of the weather forecast station. The json format data is obtained from the weather information service site provided by Amap, and then parsed using the ArduinoJson library to extract key information, thus realizing the scrolling playback of the weather forecast for the day.
(The problem that also arises is that you are too into the game, causing the big roll game to not start working, which may lead to a crash!!!)
(Code is available at your own convenience)
Main task source code
Sound spectrum analysis
Round dial
References:
https://micropython.org/download/ESP32_GENERIC_C3/
https://wiki.seeedstudio.com/get_start_round_display/
https://www.eeworld.com.cn/huodong/digikey_follow_me/
|