[经验分享] 【DigiKey创意大赛】中考倒计时摆件 作品展示

aramy   2023-12-9 09:04 楼主
中考倒计时摆件
作者:aramy
一、作品简介
中考倒计时摆件。孩子今年初三了,一直很努力。为了考试理想的高中,打算做个中考倒计时摆件,让孩子督促一下自己的学习。
作为一个摆件,主要功能分三个部分。
214924wgns2hmbskgzlgly.png
214924ne1q1zlxdx24lhqh.png
214924x1zn6xfw8wfpwvfi.png

1、中考倒计时。显示距离中考的天数。并展示学习分解目标和提醒。
2、天气信息展示。展示日期信息和天气信息。
3、通过互联网给孩子留言,做一些生活方面的提醒功能。
二、系统框图
214924pbl1sl3itzatsizt.png
硬件主控使用CoreS3,使用现成的模块会美观很多(自己做的总是不美观,而且容易乱)。最初有考虑过使用墨水屏,但是作为家里用的摆件,可以外接供电,彩色屏幕展示信息也更好看,最终选择了CoreS3。CoreS3是M5Stack开发套件系列的第三代主机,其核心主控采用ESP32-S3方案,双核Xtensa LX7处理器,主频240MHz,自带WiFi功能,板载16MFLASH和8M-PSRAM。正面搭载一块2.0寸电容触摸IPS屏,面板采用高强度玻璃材质。这里我使用CoreS3进行联网,交互和数据展示。
额外添加的传感器:VL53L3CX,一个激光测距传感器。用来感知摆件前方是否有物体(是否有人在跟前)。CoreS3自身有带接近传感器(LTR-553ALS-WA),不过这个接近传感器是使用红外来检测物体的,感知距离比较近,而我希望检测摆件前方人的距离,范围应该在20cm~200cm内,所以使用激光测距传感器。
交互消息使用物联网来传递消息信息。物联网使用的是百度的免费物联网。
软件使用vscode+platformio进行开发,用了Arduino。按功能分为三个部分。
214924r6lyl3ode909gxx3.png
主线程在初始化完成各项设备后,进入LVGL的循环,负责联网展示各类信息和用户交互。处理天气线程,负责定时访问“免费天气信息”网站,获得实时的天气信息。处理消息线程,负责维护MQTT物联网的连接,当物联网有消息更新时,就获取消息并写入SD卡。由主线程读取SD卡文件内容,并展示。这样做降低了LVGL和mqtt消息之间的耦合。另外还有个进程负责不停地读取激光测距模块的数据,用来感知与人的距离。
三、各部分功能说明

GUI Guider 使用
图形界面使用lvgl。新学lvgl,还很不熟悉,这里就找了个lvgl的绘制工具GUI Guider来绘制主界面。

214924anpn8z8gngppat5q.png
主界面由一个tabview构成,分了三个子页面:天气、学习、消息。
214924mt7pgtavsapqzca8.png
214924g542eecq8qcm8cl5.png
在GUI Guider中进行界面编辑后,可以直接生成C语言的代码。界面中展示的图片资源,使用了两种方式。
第一种:使用GUI Guider导入图片资源,然后生成图片对应的C文件。
214924na45b7q5b9b7xkhs.png
第二种:将图片保存到SD卡上,在程序运行时动态读取图片进行展示。

214924wqvjoob5pux5qqbs.png
字体使用了simsun字体,在https://gitee.com/feng_xingkai/chinese下载了常用汉字制作了汉字的字体。这样就可以在GUI Guider里使用汉字了。
连接WIFI与更新Rtc时间
系统初始化时,就需要进行连接WIFI,访问互联网。这里使用了CoreS3官方例程中提供的联网代码。
#include "wifi_info.h"
#include <esp_sntp.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
/**
 * Task: monitor the WiFi connection and keep it alive!
 *
 * When a WiFi connection is established, this task will check it every 10 seconds
 * to make sure it's still alive.
 *
 * If not, a reconnect is attempted. If this fails to finish within the timeout,
 * the ESP32 will wait for it to recover and try again.
 */
// 联网成功后自动校时
WiFiUDP ntpUDP;                                                        //创建UDP实例
NTPClient timeClient(ntpUDP, "asia.pool.ntp.org", 60 * 60 * 8, 60000); // NTC
void init_wifi_ntp(void)
{
    auto cfg = M5.config();
    cfg.external_rtc = true; // default=false. use Unit RTC.
    M5.begin(cfg);
    if (!M5.Rtc.isEnabled()) //开启RTC芯片,BM8563
    {
        Serial.println("RTC not found.");
        for (;;)
        {
            vTaskDelay(500);
        }
    }
    Serial.println("RTC found.");
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD); //连接wifi
    // uint8_t times=0;
    while (WiFi.status() != WL_CONNECTED)
    {
        Serial.print('*');
        delay(1200);
        // times++;
        // if(times>10) break;
    }
    Serial.println("\r\n WiFi Connected.");
    if (timeClient.update())            //网络校时成功,就更新 RTC芯片时间
    {
        Serial.print("ntp time: ");
        // Serial.println(timeClient.getFormattedTime());
        // configTzTime(NTP_TIMEZONE, NTP1, NTP2, NTP3);
        unsigned long epochTime = timeClient.getEpochTime();
        // time_t t = time(nullptr);          // Advance one second.
        // Serial.print(asctime(gmtime((time_t *)&epochTime))); //默认打印格式:Mon Oct 25 11:13:29 2021
        M5.Rtc.setDateTime(gmtime((time_t *)&epochTime));
    }
    delay(500);
    auto dt = M5.Rtc.getDateTime();
    static constexpr const char *const wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"};
    Serial.printf("RTC   UTC  :%04d/%02d/%02d (%s)  %02d:%02d:%02d\r\n", dt.date.year, dt.date.month, dt.date.date, wd[dt.date.weekDay], dt.time.hours, dt.time.minutes, dt.time.seconds);
}

 

当连接wifi成功后,就访问ntp服务器,校准时间。CoreS3安装了一颗BM8563时钟芯片,在完成ntp校时后,就将时间写入rtc芯片。不过这里有点搞不明白的是这颗rtc芯片,没有额外的供电,断电后时间信息就丢失了,不知道为啥不做个给Rtc芯片一直供电的电路。
获取天气信息
天气信息我用的是http://www.yiketianqi.com/。这里可以获得每5分钟更新的实时天气预报。通过get方法访问本地的URL,获得到天气信息的json字符串。然后进行解析拿到准确的天气信息。使用一个freetos任务来执行获取天气信息。实际操作过程中发现有一定比例访问失败的情况。当访问失败时则10秒后重试,成功则6分钟后再更新。
#include "weather.h"
#include <HTTPClient.h>
#include "lcd_lvgl.h"
/*
{"cityid":"101281601","date":"2023-11-29","week":"星期三","update_time":"08:04","city":"东莞","cityEn":"dongguan","country":"中国","countryEn":"China","wea":"阴","wea_img":"yin","tem":"19.6","tem1":"23",
"tem2":"19","win":"北风","win_speed":"1级","win_meter":"3km\/h","humidity":"76%","visibility":"22km","pressure":"1012","air":"61","air_pm25":"42","air_level":"良","air_tips":"各类人群可多参加户外活动,多呼吸一下清新的空气。",
"alarm":{"alarm_type":"","alarm_level":"","alarm_title":"","alarm_content":""},"rain_pcpn":"0","uvIndex":"4","uvDescription":"中等","wea_day":"阴","wea_day_img":"yin","wea_night":"阴",
"wea_night_img":"yin","sunrise":"06:46","sunset":"17:38","aqi":{"update_time":"06:54","air":"61","air_level":"良","air_tips":"各类人群可多参加户外活动,多呼吸一下清新的空气。",
"pm25":"42","pm25_desc":"良","pm10":"72","pm10_desc":"良","o3":"62","o3_desc":"","no2":"28","no2_desc":"","so2":"8","so2_desc":"","co":"0.9","co_desc":"","kouzhao":"不用佩戴口罩",
"yundong":"适宜运动","waichu":"适宜外出","kaichuang":"适宜开窗","jinghuaqi":"不需要打开"}}
*/
HTTPClient http; // 声明HTTPClient对象
DynamicJsonDocument doc(2048);
struct WeatherInfo weather = {0};
void getWeather()
{
  weather = {0};
  http.begin(WEATHERURL);    // 准备启用连接
  int httpCode = http.GET(); // 发起GET请求
  if (httpCode > 0) // 如果状态码大于0说明请求过程无异常
  {
    if (httpCode == HTTP_CODE_OK) // 请求被服务器正常响应,等同于httpCode == 200
    {
      String payload = http.getString(); // 读取服务器返回的响应正文数据
                                         // 如果正文数据很多该方法会占用很大的内存
      // Serial.println(payload);
      weather.succ = 1;
      deserializeJson(doc, payload);
      String temp_str;
      temp_str = doc["humidity"].as<String>(); // 湿度
      temp_str.replace("%", "");               // 去除尾部的 % 号
      weather.humidity = temp_str.toInt();
      // Serial.println(wea.humidity);
      temp_str = doc["tem"].as<String>(); // 温度
      weather.temperature = int(temp_str.toFloat() + 0.5);
      temp_str = doc["tem1"].as<String>(); // 最高温度
      weather.maxTemp = temp_str.toInt();
      temp_str = doc["tem2"].as<String>(); // 最高温度
      weather.minTemp = temp_str.toInt();
      temp_str = doc["wea"].as<String>(); // 天气
      Serial.print(" wea :");
      Serial.print(temp_str);
      Serial.print(" len= ");
      Serial.print(temp_str.length());
      strncpy(weather.wea, temp_str.c_str(), temp_str.length());
      temp_str = doc["wea_img"].as<String>(); // 天气图标
      // Serial.print(" wea img:");
      // Serial.print(temp_str);
      // Serial.print(" wae_img len ");
      // Serial.print(temp_str.length());
      memset(weather.wea_img, 0, sizeof(weather.wea_img));
      // strncpy(weather.wea_img, temp_str.c_str(), temp_str.length());
      sprintf(weather.wea_img, "S:/%s.png", temp_str);
      sniprintf(weather.wea_msg, 100, "最低气温%d℃,最高气温%d℃,\n空气质量%s,紫外线指数:%s.", weather.minTemp, weather.maxTemp, doc["air_level"].as<String>(), doc["uvDescription"].as<String>());
      // Serial.print("[");
      // Serial.print(weather.wea);
      // Serial.print("]    ");
      // Serial.print(weather.air_level);
      // Serial.print("    ");
      Serial.print("    ");
      Serial.print(weather.temperature);
      Serial.print("    ");
      // Serial.println(weather.maxTemp);
      // Serial.println(weather.wea_msg);
      Serial.println(payload);
    }
  }
  else
  {
    weather.succ = 0;
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
  }
  http.end(); // 结束当前连接
}
void taskFlushWeather(void *parameter)
{
  weather.succ = false;
  uint8_t flag = 0;
  while (1)
  {
    // Serial.println("weather process!");
    if (WiFi.status() == WL_CONNECTED)
    {
      if (weather.succ == 0)
        getWeather(); //获取天气信息
      else if (flag > 60)
      {
        flag = 0;
        getWeather();
      }
    }
    flag++;
    vTaskDelay(10 * 1000); //任务延时调度
  }
  vTaskDelete(NULL); //删除自身函数
}

 

使用物联网mqtt传递消息
摆件的消息使用百度的物联网来实现。这样无论身处何地,都可以通过互联网给摆件发送消息了。CoreS3使用单独的任务负责接收mqtt消息,当收到消息后,就将消息内容写入SD卡对应的文件中。Lvgl则负责定时检查是否有消息更新,一旦有更新,则从文件中读取消息内容,并展示。有效地降低了程序的耦合。
#include "mqtt.h"
#include <PubSubClient.h>
#include <HTTPClient.h>
#include "lcd_lvgl.h"
const char *MQTT_SERVER = "afgsmmu.iot.gz.baidubce.com";
const int MQTT_PORT = 1883;
const char *MQTT_USRNAME = "thingidp@afgsmmu|AICAM|0|MD5";
const char *MQTT_PASSWD = "6c792ea3b7f084f7a1e30da894898d67";
const char *TOPIC = "$iot/AICAM/user/ismask";
const char *CLIENT_ID = "AICAM"; //当前设备的clientid标志
WiFiClient espClient;
PubSubClient client(espClient);
DynamicJsonDocument mqdoc(1024);
static char msgbuf[1024]; //用来缓冲消息信息
static uint16_t pos;
void callback(char *topic, byte *payload, unsigned int length)
{
    char filename[20];
    String msg;
    // Serial.print("Message arrived [");
    // Serial.print(topic); // 打印主题信息
    // Serial.print("] ");
    // Serial.println();
    deserializeJson(mqdoc, payload);
    msg = mqdoc["msg"].as<String>();
    // Serial.print(msg);
    // Serial.print("  ");
    // Serial.print(doc["num"].as<int>());
    // Serial.print(" = ");
    // Serial.println(mqdoc["msg"].as<String>());
    if (mqdoc["num"].as<int>() == 1)
    { //收到第一条消息时,初始化内存
        memset(msgbuf, 0, 1024);
        pos = 0;
    }
    strcpy(msgbuf + pos, msg.c_str());
    pos = msg.length();
    if (mqdoc["num"].as<int>() == mqdoc["all"].as<int>())
    { //收到最后一条消息时,需要写文件
        sprintf(filename,"S:/%s.txt",mqdoc["topic"].as<String>());
        // Serial.print("write_file ");
        // Serial.print(filename);
        // Serial.print("    [");
        // Serial.print(msgbuf);
        // Serial.println("]");        
        saveMsg(filename, msgbuf);
        pos=0;        
    }
    // //收到mqtt消息后,保存到SD卡中,保存后 lvgl 进行展示
    // sprintf(filename, "S:%s.txt", doc["topic"].as<String>());
    // Serial.println(filename);
    // saveMsg(filename, doc["msg"].as<String>());
}
void reconnect()
{
    while (!client.connected())
    {
        Serial.print("Attempting MQTT connection...");
        if (client.connect(CLIENT_ID, MQTT_USRNAME, MQTT_PASSWD))
        {
            Serial.println("connected");
            // 连接成功时订阅主题
            client.subscribe(TOPIC);
        }
        else
        {
            Serial.print("failed, rc=");
            Serial.print(client.state());
            Serial.println(" try again in 5 seconds");
            vTaskDelay(5 * 1000);
        }
    }
}
void taskMqttMsg(void *parameter)
{
    while (WiFi.status() != WL_CONNECTED)
    {
        vTaskDelay(1000);
    }
    client.setServer(MQTT_SERVER, MQTT_PORT); //设定MQTT服务器与使用的端口,1883是默认的MQTT端口
    client.setCallback(callback);             //设定回调方式,当ESP8266收到订阅消息时会调用此方法
    while (1)
    {
        if (WiFi.status() == WL_CONNECTED)
        {
            if (!client.connected())
            {
                reconnect();
            }
            client.loop();
        }
        vTaskDelay(1000); //任务延时调度
    }
    vTaskDelete(NULL); //删除自身函数
}

 

这里遇到个问题,不知道是不是mqtt的限制,一条消息不能够太长,否则接收不到。这里采取了对消息进行拆分处理,将一条消息,拆成多条,并逐一接收,最后拼接合并。
通过传感器测量距离
摆件按计划是摆放在桌子上,想获得桌子前方是否坐人,用来决定展示内容(未来还想扩展更多功能)。这里启动一个获取VL53L3C激光传感器距离的任务。可以实时获取桌子前200cm内人的距离。
//获取激光传感器得到的距离
void getDistinct(void *parameter)
{
  VL53LX_MultiRangingData_t MultiRangingData;
  VL53LX_MultiRangingData_t *pMultiRangingData = &MultiRangingData;
  uint8_t NewDataReady = 0;
  int no_of_object_found = 0, j;
  char report[64];
  int status;
  while (1)
  {
    status = sensor_vl53lx_sat.VL53LX_GetMultiRangingData(pMultiRangingData);
    no_of_object_found = pMultiRangingData->NumberOfObjectsFound;
    snprintf(report, sizeof(report), "VL53LX Satellite: Count=%d, #Objs=%1d \n", pMultiRangingData->StreamCount, no_of_object_found);
    // Serial.print(report);
    if (no_of_object_found == 0)
      distinct = 0;
    for (j = 0; j < no_of_object_found; j++)
    {
      // Serial.print(j);
      // Serial.print("    status=");
      // Serial.print(pMultiRangingData->RangeData[j].RangeStatus);
      // Serial.print(", D=");
      // Serial.print(pMultiRangingData->RangeData[j].RangeMilliMeter);
      // Serial.print("mm");
      // Serial.print(", Signal=");
      // Serial.print((float)pMultiRangingData->RangeData[j].SignalRateRtnMegaCps / 65536.0);
      // Serial.print(" Mcps, Ambient=");
      // Serial.print((float)pMultiRangingData->RangeData[j].AmbientRateRtnMegaCps / 65536.0);
      // Serial.println(" Mcps");
      distinct = pMultiRangingData->RangeData[j].RangeMilliMeter;
    }
    // Serial.println("");
    if (status == 0)
    {
      status = sensor_vl53lx_sat.VL53LX_ClearInterruptAndStartMeasurement();
    }
    vTaskDelay(1000); //任务延时调度
  }
  vTaskDelete(NULL); //删除自身函数
}

 

LVGL更新信息
为Lvgl有两个定时任务,一个是GUI Guider自动生成的用来更新时间的定时任务。在这个任务中可以添加获取rtc时间、日期等信息,让界面与实际时间同步。另外一个定时任务,用来更新天气、消息等信息。当天气发生变化,就从SD卡中读取对应天气的图片,并进行展示。当消息信息有更新时,就从SD卡读取消息内容。使用信号量来防止死锁。
上位机:发送mqtt消息
上位机是用来在电脑上发送mqtt消息的。使用了QT+Python的方式编写。上位机会做几个事情:1 将消息中的全角数字、符号改为半角(制作的字库中没有包括全角数字和符号)。2 给消息加入换行信息。Lvgl消息框使用了滚动显示的方法,加入适当的回车,方便信息展示。3 解决长消息下位机无法接收问题。将单个消息拆成多条物联网信息,编号后转成json字符串发送。
 
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @FileName :MqttMainWin.py
# @time :2023/11/30 17:07
# @author :aramy
import sys
import threading
import json
from PyQt5 import QtWidgets
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QWidget

from QTUI.mqttsendui import Ui_MqttSend
from unit.mqtt_subprocess import MqttMsg
from unit.stringQB import stringpartQ2B, formatMsg

MSGLENG=30
class MainWidget(QWidget):
def __init__(self, parent=None):
super(MainWidget, self).__init__(parent)
self.ui = Ui_MqttSend()
self.ui.setupUi(self)
# self.ui.pushButtonSend.setEnabled(False)

# 开启接收mqtt的线程
self.mqttMsgThread = MqttMsg()
self.mqttMsgThread.start()

@pyqtSlot () #
def on_pushButtonSend_clicked(self):
sendjson = {}
# print('主线程 按钮按下:', threading.currentThread())
# print(self.ui.textEditMsg.toPlainText())
sendmsg = formatMsg(stringpartQ2B(self.ui.textEditMsg.toPlainText()))
# 给消息添加题头
if self.ui.radioButtonStudy.isChecked():
sendjson['topic'] = "study"
elif self.ui.radioButtonMsg1.isChecked():
sendjson['topic'] = "message1"
elif self.ui.radioButtonMsg2.isChecked():
sendjson['topic'] = "message2"

# 分解消息长度,控制每条数据长度
msglen=len(sendmsg)
sendjson['all'] = int(msglen/MSGLENG)+1
for i in range(0,int(msglen/MSGLENG)+1):
sendjson['num'] = i+1
sendjson['msg'] = sendmsg[i*MSGLENG:(i+1)*MSGLENG]
print(sendjson)
self.mqttMsgThread.lock.acquire()
self.mqttMsgThread.message = json.dumps(sendjson)
self.mqttMsgThread.lock.release()
self.mqttMsgThread.send_message()


if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
appWindow = MainWidget()
appWindow.show()
sys.exit(app.exec_())

四、源代码

desk_display.zip (91.99 MB)
(下载次数: 1, 2023-12-8 22:52 上传)


上位机.zip (16.82 KB)
(下载次数: 0, 2023-12-8 22:51 上传)


五、演示视频


 

六、项目总结
很幸运能参加“智造万物,快乐不停”,感谢德捷为我们提供了这样一次精心设计的学习活动,让我有机会接触CoreS3优秀的模块。知易行难,真正自己去做一个项目,还是困难重重,CoreS3上还集成了很多外设,还没能玩起来,接下来还需要逐一学习。在完成项目过程中结识到一群志同道合的技术爱好者,在各位老师帮助下解决各种问题,就是学习快乐所在!
本帖最后由 aramy 于 2023-12-11 11:19 编辑

回复评论 (1)

效果相当不错,不过要不要试着弄个输入系统,这样就可以自定义提示信息了

在爱好的道路上不断前进,在生活的迷雾中播撒光引
点赞  2023-12-10 09:29
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复