【DigiKey创意大赛】中考倒计时摆件 作品展示
[复制链接]
本帖最后由 aramy 于 2023-12-11 11:19 编辑
中考倒计时摆件
作者:aramy
一、作品简介
中考倒计时摆件。孩子今年初三了,一直很努力。为了考试理想的高中,打算做个中考倒计时摆件,让孩子督促一下自己的学习。
作为一个摆件,主要功能分三个部分。
1、中考倒计时。显示距离中考的天数。并展示学习分解目标和提醒。
2、天气信息展示。展示日期信息和天气信息。
3、通过互联网给孩子留言,做一些生活方面的提醒功能。
二、系统框图
硬件主控使用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。按功能分为三个部分。
主线程在初始化完成各项设备后,进入LVGL的循环,负责联网展示各类信息和用户交互。处理天气线程,负责定时访问“免费天气信息”网站,获得实时的天气信息。处理消息线程,负责维护MQTT物联网的连接,当物联网有消息更新时,就获取消息并写入SD卡。由主线程读取SD卡文件内容,并展示。这样做降低了LVGL和mqtt消息之间的耦合。另外还有个进程负责不停地读取激光测距模块的数据,用来感知与人的距离。
三、各部分功能说明
GUI Guider 使用
图形界面使用lvgl。新学lvgl,还很不熟悉,这里就找了个lvgl的绘制工具GUI Guider来绘制主界面。
主界面由一个tabview构成,分了三个子页面:天气、学习、消息。
在GUI Guider中进行界面编辑后,可以直接生成C语言的代码。界面中展示的图片资源,使用了两种方式。
第一种:使用GUI Guider导入图片资源,然后生成图片对应的C文件。
第二种:将图片保存到SD卡上,在程序运行时动态读取图片进行展示。
字体使用了simsun字体,在 下载了常用汉字制作了汉字的字体。这样就可以在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_())
四、源代码
六、项目总结
很幸运能参加“智造万物,快乐不停”,感谢德捷为我们提供了这样一次精心设计的学习活动,让我有机会接触CoreS3优秀的模块。知易行难,真正自己去做一个项目,还是困难重重,CoreS3上还集成了很多外设,还没能玩起来,接下来还需要逐一学习。在完成项目过程中结识到一群志同道合的技术爱好者,在各位老师帮助下解决各种问题,就是学习快乐所在!
|