【Beetle ESP32-C3】九、OLED时钟和天气助手逻辑说明(Arduino)
<div class='showpostmsg'><p> 本篇测评承接<a href="https://bbs.eeworld.com.cn/thread-1216938-1-1.html" target="_blank">上篇</a>,介绍上篇完成案例的主要业务逻辑。</p><h2 ><b>1、时钟功能相关说明</b></h2>
<p > 案例利用网络授时,在初始化时配置了ESP32的RTC,之后就可以通过RTC来获取本地时间。其实,这个功能是本人前年开发过的,当时是基于ESP32 WROOM,这次改为C3了。不过当时在下错误的认为其仅是“获取网络时间”并保存到时间结构体变量struct tm t(案例定义变量名为“t”),所以完全是画蛇添足的搞了一个每秒种通过代码更新变量t的值,实际上直接访问RTC就可以随时刷新t值。</p>
<p > 在ESP32-C3的第七篇测评中,本人还延续了这种错误思路,所以编写了相关代码:</p>
<p > </p>
<pre>
<code>//两个宏定义用来标志“是否在一分钟内”
#define INMIN 0x60 //in 60s flag
#define ONEMIN 0x61 //one minute flag
//省略其它代码
//循环逻辑中,接收到信号量,说明1s到了,将t.tm_sec更新(即时间结构体秒钟数成员)
//如果t.tm_sec超过59则表示1分钟到了,标志ONEMIN即“不在一分钟内了”
//然后再次printLocalTime()
//其中的调用getLocalTime(&t)实际为读取RTC,被本人错认为“获取网络时间”
void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
if((t.tm_sec)++ >= 59)timestate = ONEMIN;
}
//-----print localtime per minute & weather info per 5 minutes
if(timestate == ONEMIN) {
timestate = INMIN;
if(WiFi.status() == WL_CONNECTED) {
printLocalTime();
if(t.tm_min%5 == 0) getWeather();
}
}
}
//省略其它代码
//定义一个1s定时器,这是定时器回调产生信号量timersem
void IRAM_ATTR onTimer() {
xSemaphoreGiveFromISR(timersem, NULL);
}
//省略其它代码
//-----print local time function
void printLocalTime() {
if(!getLocalTime(&t)) {
Serial.println("Failed to obtain time");
return;
}
Serial.println(&t, "%F %T %A");
}</code></pre>
<p > </p>
<p > 实际上,本人在写第七篇测评时就已经怀疑之前犯的错误了,不过也没有修改(就是懒得),直到完成上篇的OLED时钟案例出现Bug——时间显示有时会变成“HH:mm:60”,也就是秒钟数显示为60。于是,代码调整为:</p>
<p > </p>
<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
getLocalTime(&t);
}
//省略其它代码
}</code></pre>
<p > </p>
<p > 本人还尝试了直接在定时器回调中调用getLocalTime(&t),结果系统开始不断重启,并提示重启原因为:“rst:0x3 (RTC_SW_SYS_RST)”,只能改回使用信号量。</p>
<h2 ><b>2、时钟显示相关说明</b></h2>
<p > 开发阶段,本人也是脑子迷糊,把时间显示的代码也放到接收信号量的语句块中,心想“一秒钟刷新一次屏幕,没毛病啊”。</p>
<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
getLocalTime(&t);
}
//省略其它代码
}</code></pre>
<p > </p>
<p > 不过这样写也出现了莫名其妙的bug,屏幕时不时就会定住。后来也是琢磨过来“当接收不到信号量时,任务本来就会阻塞,刷屏操作没必要放到语句块中”,所以语句块中只做时间刷新就好,刷屏代码放到外面就行。</p>
<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
getLocalTime(&t);
}
//-----refresh screen 暂时省略其它代码
drawLocalTimePage();
}</code></pre>
<p > </p>
<p > drawLocalTimePage()是自己编写的显示时间函数,感觉屏幕还有富裕空间,面包板上还插接着AHT10,所以剩余屏幕空间显示传感器温湿度。</p>
<p > 时间显示画面中,第一行(下述代码注释line1处)是“HH:MM:SS”时间显示和星期缩写——即语句:“u8g2.print(&t, "%H:%M:%S %a");”,struct tm在C++中有定义好的格式化字符,具体可以参考:<a href="https://baike.baidu.com/item/strftime/9569073?fr=aladdin"><u>https://baike.baidu.com/item/strftime/9569073?fr=aladdin</u></a> 。第一行选择字体是u8g2_font_ncenB14_tr,其具体像素值没找到,结合字体名和目测,本人猜想是7x14的。时间+星期共14个字符(中间两个空格),横向也就是98个像素点,不过设置顶格显示,感觉也挺美观,就没有再修改。</p>
<p > 第二行(下述代码注释line2处)是“YYYY-mm-dd”日期显示,共10个字符,采用字体u8g2_font_8x13_mf(看名字字符像素应该是8*13的),横向占据80个像素,居中显示左右留空24像素(OLED屏128*64),所以设置显示坐标从开始——第一行高度14像素加上行间距定为y坐标18。</p>
<p > 第三、四行显示湿度和温度,以湿度为例,“Humi: ”共六字符(加一个空格),湿度值占六字符(通过dtostrf()函数转换字符串,限定长度为6),后接单位“% rH”占据四字符,这样正好16字符(字体还是u8g2_font_8x13_mf)。因此,顶格显示,湿度行y坐标为32,温度行y坐标为48。</p>
<p > 另外,温湿度值是浮点数,如果使用u8g2.print()输出会出现bug,好像是ESP32-Arduino的Printable接口(框架中的一个类)一直处理不了浮点数,所以这里使用dtostrf()做转换,再用drawStr()显示。不过温度单位“°”是UTF字符,只能用u8g2.print()显示才会成功。</p>
<pre>
<code>//-----draw local time function
void drawLocalTimePage(void) {
char str = {0};
u8g2.clearBuffer();
//line1. set time & weekday
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.setCursor(0, 0); //第一行字从屏幕左上顶点开始
u8g2.print(&t, "%H:%M:%S%a");
//line2. set date
u8g2.setCursor(24, 18);
u8g2.setFont(u8g2_font_8x13_mf);
u8g2.print(&t, "%Y-%m-%d");
//line3. set aht10 humidity
u8g2.drawStr(0, 32, "Humi: ");
memset(str, 0, 6);
dtostrf(aht_humi.relative_humidity, 6, 2, str);
u8g2.drawStr(48, 32, str);
u8g2.drawStr(96,32, "% rH");
//line4. set aht10 temperature
u8g2.drawStr(0, 48, "Temp: ");
memset(str, 0, 6);
dtostrf(aht_temp.temperature, 6, 2, str);
u8g2.drawStr(48, 48, str);
u8g2.setCursor(96, 48);
u8g2.print("°C");
//draw screen
u8g2.sendBuffer();
}</code></pre>
<p > </p>
<h2 ><b>3、天气显示相关说明</b></h2>
<p > 天气显示单独为一个画面,借鉴了u8g2案例“buffer/weather”。u8g2提供的一些字体库中包含天气图标,案例中提供了“晴、多云、阴、雨、雷”五个例子。本人也是在网上没有找到其它介绍,因此本例也只用了上述五个图标。</p>
<pre>
<code>//u8g2案例,设置固定字体,并给编号67,就可以显示“雷”图标
//本人随意修改67这个编号值,还找到了齿轮和五角星图标
u8g2.setFont(u8g2_font_open_iconic_embedded_6x_t);
u8g2.drawGlyph(x, y, 67);</code></pre>
<p > </p>
<p > u8g2的天气案例除了显示天气图标和温度值,还在最后一行显示流水字串,本例将最后一行用于显示城市名——即“Tianjin”。</p>
<p > 本例先通过自定义的getWeather()函数从心知天气请求到实时天气信息,因为刷屏逻辑为了保证时间显示正确,也就是1s刷新一次,而天气每正5分钟点时才请求一次,所以定义了全局变量用来存储天气编号(心知网站定义)、气温值。</p>
<p > </p>
<pre>
<code>int code = 0; //weather code
int degree = 0; //weather temperature
//-----request now weather function
void getWeather() {
if((WiFi.status() == WL_CONNECTED)) {
HTTPClient http; //create HTTPClient instance
http.begin(url); //begin HTTP request
int httpCode = http.GET(); //get response code - normal:200
if(httpCode > 0) {
Serial.printf(" GET... code: %d\n", httpCode);
if(httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(payload);
//parse response stringto DynamicJsonDocument instance
deserializeJson(doc, payload);
//get top level Json-Object
JsonObject obj = doc.as<JsonObject>();
//get "results" property then parse the value to Json-Array
JsonVariant resultsvar = obj["results"];
JsonArray resultsarr = resultsvar.as<JsonArray>();
//get array index 0 which is now weather info
JsonVariant resultselementvar = resultsarr;
JsonObject resultselementobj = resultselementvar.as<JsonObject>();
/* get values of city, temp, code, update */
//get city
JsonVariant namevar = resultselementobj["location"]["name"];
String namestr = namevar.as<String>();
Serial.println(namestr);
//get temperature,变量degree是全局的
JsonVariant temperaturevar = resultselementobj["now"]["temperature"];
String temperaturestr = temperaturevar.as<String>();
degree = temperaturevar.as<int>();
Serial.println(temperaturestr);
//get weather code,变量code是全局的
JsonVariant codevar = resultselementobj["now"]["code"];
code = codevar.as<int>();
Serial.println(code);
//get last_update time
JsonVariant last_updatevar = resultselementobj["last_update"];
String last_updatestr = last_updatevar.as<String>();
Serial.println(last_updatestr);
}
} else {
Serial.printf(" GET... failed, error: %s\n",
http.errorToString(httpCode).c_str());
}
http.end(); //end http connectiong
}
}</code></pre>
<p > </p>
<p > 心知天气对各种天气状态进行了编号(请查看:<a href="https://seniverse.yuque.com/books/share/e52aa43f-8fe9-4ffa-860d-96c0f3cf1c49/yev2c3"><u>https://seniverse.yuque.com/books/share/e52aa43f-8fe9-4ffa-860d-96c0f3cf1c49/yev2c3</u></a> ),案例只引用了u8g2的五种图标,因而对解析到的心知天气状态码(code全局量)做了判断,以决定要显示的图标样式,天气编号在21以上的就显示一个“五角星”了。</p>
<p > </p>
<p class="imagemiddle" style="text-align: center;"></p>
<p align="center" >图9-1 心知天气现象代码对照表页面截图</p>
<p align="center" > </p>
<pre>
<code>//-----set weather icon function
void drawWeatherSymbol(u8g2_uint_t x, u8g2_uint_t y, uint8_t symbol) {
if(symbol>=0 && symbol<4) { //Sun
u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
u8g2.drawGlyph(x, y, 69);
} else if(symbol>=4 && symbol<9) {//Cloudy
u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
u8g2.drawGlyph(x, y, 65);
} else if(symbol==9) { //Overcast
u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
u8g2.drawGlyph(x, y, 64);
} else if(symbol>=10 && symbol<13) {//Thunder
u8g2.setFont(u8g2_font_open_iconic_embedded_6x_t);
u8g2.drawGlyph(x, y, 67);
} else if(symbol>=13 && symbol<21) {//Rain
u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
u8g2.drawGlyph(x, y, 67);
} else { //Others show star
u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
u8g2.drawGlyph(x, y, 68);
}
}
//-----set weather temperature degree function
void drawWeatherDegree(uint8_t symbol, int degree) {
drawWeatherSymbol(0, 0, symbol);
u8g2.setFont(u8g2_font_logisoso32_tf);
u8g2.setCursor(48+6, 10);
u8g2.print(degree);
u8g2.print("°C"); //requires enableUTF8Print()
}
//-----draw now weather function
void drawNowWeatherPage(int code, int temp) {
u8g2.clearBuffer();
drawWeatherDegree(code, temp);
u8g2.setCursor(32, 48);
u8g2.setFont(u8g2_font_8x13_mf);
u8g2.print("Tianjin");
u8g2.sendBuffer();
}</code></pre>
<p > </p>
<p > 对应的,loop()中的代码就是:</p>
<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
getLocalTime(&t);
}
//-----refresh screen
if(pagestate == TIMEPAGE)
drawLocalTimePage();
else if(pagestate == WEATHERPAGE)
drawNowWeatherPage(code, degree);
//-----update weather info per 5 minutes 5分钟检测一次温湿度和请求一次天气
if(t.tm_min%5==0 && t.tm_sec==0) {
getAHT10();
if(WiFi.status() == WL_CONNECTED) {
getWeather();
}
}
//暂时省略其它代码
}</code></pre>
<p > </p>
<h2 ><b>4、按键翻页相关说明</b></h2>
<p > 最后就是按键翻页了,这里开始考虑使用按键中断,但是触发中断后也不时出现重启,个人猜测可能是ISR和信号量之间有冲突,所以将翻页判断逻辑放到了loop()中。</p>
<p > 首先,定义了一个枚举类型,表示不同的显示页,后续如果还想增加显示页面,可以再扩展枚举成员。</p>
<p > 接着,就是定义记录页面编号的全局量,并初始化为“TIMEPAGE”,也就是开机后默认显示时钟画面。</p>
<pre>
<code>//-----show page enum
typedef enum {
TIMEPAGE = 0,
WEATHERPAGE,
} AITA_PAGE_INDEX;
//-----global variable of page No
int pagestate = TIMEPAGE;</code></pre>
<p > </p>
<p > 然后,就是宏定义按键IO——本例用IO7,并作IO初始化。</p>
<pre>
<code>#define KEY 7
void setup() {
//-----initialize BSP
Serial.begin(115200);
pinMode(KEY, INPUT_PULLUP);
//省略其它代码
}</code></pre>
<p > </p>
<p > 最后,由于按键中断的不给力,只能在loop()中进行按键判断了。</p>
<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
getLocalTime(&t);
}
//-----refresh screen
if(pagestate == TIMEPAGE)
drawLocalTimePage();
else if(pagestate == WEATHERPAGE)
drawNowWeatherPage(code, degree);
//-----update weather info per 5 minutes
if(t.tm_min%5==0 && t.tm_sec==0) {
getAHT10();
if(WiFi.status() == WL_CONNECTED) {
getWeather();
}
}
//-----read KEY 按键判断逻辑,采用枚举在另赠页面时可以不修改此处的逻辑
if(digitalRead(KEY) == 0) {
delay(30);
if(digitalRead(KEY) == 0) {
if(pagestate == WEATHERPAGE) pagestate = TIMEPAGE;
else pagestate++;
}
}
}</code></pre>
<p > </p>
</div><script> var loginstr = '<div class="locked">查看本帖全部内容,请<a href="javascript:;" style="color:#e60000" class="loginf">登录</a>或者<a href="https://bbs.eeworld.com.cn/member.php?mod=register_eeworld.php&action=wechat" style="color:#e60000" target="_blank">注册</a></div>';
if(parseInt(discuz_uid)==0){
(function($){
var postHeight = getTextHeight(400);
$(".showpostmsg").html($(".showpostmsg").html());
$(".showpostmsg").after(loginstr);
$(".showpostmsg").css({height:postHeight,overflow:"hidden"});
})(jQuery);
} </script><script type="text/javascript">(function(d,c){var a=d.createElement("script"),m=d.getElementsByTagName("script"),eewurl="//counter.eeworld.com.cn/pv/count/";a.src=eewurl+c;m.parentNode.insertBefore(a,m)})(document,523)</script> <p>楼主的U8g2用得非常好,建议除上图片+视频,让大家一睹为快!</p>
lugl4313820 发表于 2022-9-5 12:03
楼主的U8g2用得非常好,建议除上图片+视频,让大家一睹为快!
<p>好的,我再努力</p>
<p>想看看效果,有视频的话就好了~</p>
页:
[1]