sonicfirr 发表于 2022-9-5 09:55

【Beetle ESP32-C3】九、OLED时钟和天气助手逻辑说明(Arduino)

<div class='showpostmsg'><p>&nbsp; &nbsp; &nbsp; &nbsp;本篇测评承接<a href="https://bbs.eeworld.com.cn/thread-1216938-1-1.html" target="_blank">上篇</a>,介绍上篇完成案例的主要业务逻辑。</p>

<h2 ><b>1、时钟功能相关说明</b></h2>

<p >&nbsp; &nbsp; &nbsp; &nbsp;案例利用网络授时,在初始化时配置了ESP32的RTC,之后就可以通过RTC来获取本地时间。其实,这个功能是本人前年开发过的,当时是基于ESP32 WROOM,这次改为C3了。不过当时在下错误的认为其仅是&ldquo;获取网络时间&rdquo;并保存到时间结构体变量struct tm t(案例定义变量名为&ldquo;t&rdquo;),所以完全是画蛇添足的搞了一个每秒种通过代码更新变量t的值,实际上直接访问RTC就可以随时刷新t值。</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;在ESP32-C3的第七篇测评中,本人还延续了这种错误思路,所以编写了相关代码:</p>

<p >&nbsp;</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(&amp;t)实际为读取RTC,被本人错认为“获取网络时间”
void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
    if((t.tm_sec)++ &gt;= 59)timestate = ONEMIN;
}
//-----print localtime per minute &amp; 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(&amp;t)) {
    Serial.println("Failed to obtain time");
    return;
}
Serial.println(&amp;t, "%F %T %A");
}</code></pre>

<p >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;实际上,本人在写第七篇测评时就已经怀疑之前犯的错误了,不过也没有修改(就是懒得),直到完成上篇的OLED时钟案例出现Bug&mdash;&mdash;时间显示有时会变成&ldquo;HH:mm:60&rdquo;,也就是秒钟数显示为60。于是,代码调整为:</p>

<p >&nbsp;</p>

<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
    getLocalTime(&amp;t);
}

//省略其它代码
}</code></pre>

<p >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;本人还尝试了直接在定时器回调中调用getLocalTime(&amp;t),结果系统开始不断重启,并提示重启原因为:&ldquo;rst:0x3 (RTC_SW_SYS_RST)&rdquo;,只能改回使用信号量。</p>

<h2 ><b>2、时钟显示相关说明</b></h2>

<p >&nbsp; &nbsp; &nbsp; &nbsp;开发阶段,本人也是脑子迷糊,把时间显示的代码也放到接收信号量的语句块中,心想&ldquo;一秒钟刷新一次屏幕,没毛病啊&rdquo;。</p>

<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
    getLocalTime(&amp;t);
}

//省略其它代码
}</code></pre>

<p >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;不过这样写也出现了莫名其妙的bug,屏幕时不时就会定住。后来也是琢磨过来&ldquo;当接收不到信号量时,任务本来就会阻塞,刷屏操作没必要放到语句块中&rdquo;,所以语句块中只做时间刷新就好,刷屏代码放到外面就行。</p>

<pre>
<code>void loop() {
//-----update time state per second
  if(xSemaphoreTake(timersem, 0) == pdTRUE) {
    getLocalTime(&amp;t);
  }
//-----refresh screen 暂时省略其它代码
  drawLocalTimePage();

}</code></pre>

<p >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;drawLocalTimePage()是自己编写的显示时间函数,感觉屏幕还有富裕空间,面包板上还插接着AHT10,所以剩余屏幕空间显示传感器温湿度。</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;时间显示画面中,第一行(下述代码注释line1处)是&ldquo;HH:MM:SS&rdquo;时间显示和星期缩写&mdash;&mdash;即语句:&ldquo;u8g2.print(&amp;t, &quot;%H:%M:%S &nbsp;%a&quot;);&rdquo;,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>&nbsp;。第一行选择字体是u8g2_font_ncenB14_tr,其具体像素值没找到,结合字体名和目测,本人猜想是7x14的。时间+星期共14个字符(中间两个空格),横向也就是98个像素点,不过设置顶格显示,感觉也挺美观,就没有再修改。</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;第二行(下述代码注释line2处)是&ldquo;YYYY-mm-dd&rdquo;日期显示,共10个字符,采用字体u8g2_font_8x13_mf(看名字字符像素应该是8*13的),横向占据80个像素,居中显示左右留空24像素(OLED屏128*64),所以设置显示坐标从开始&mdash;&mdash;第一行高度14像素加上行间距定为y坐标18。</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;第三、四行显示湿度和温度,以湿度为例,&ldquo;Humi: &rdquo;共六字符(加一个空格),湿度值占六字符(通过dtostrf()函数转换字符串,限定长度为6),后接单位&ldquo;% rH&rdquo;占据四字符,这样正好16字符(字体还是u8g2_font_8x13_mf)。因此,顶格显示,湿度行y坐标为32,温度行y坐标为48。</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;另外,温湿度值是浮点数,如果使用u8g2.print()输出会出现bug,好像是ESP32-Arduino的Printable接口(框架中的一个类)一直处理不了浮点数,所以这里使用dtostrf()做转换,再用drawStr()显示。不过温度单位&ldquo;&deg;&rdquo;是UTF字符,只能用u8g2.print()显示才会成功。</p>

<pre>
<code>//-----draw local time function
void drawLocalTimePage(void) {
char str = {0};
u8g2.clearBuffer();
//line1. set time &amp; weekday
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.setCursor(0, 0);         //第一行字从屏幕左上顶点开始
u8g2.print(&amp;t, "%H:%M:%S%a");
//line2. set date
u8g2.setCursor(24, 18);
u8g2.setFont(u8g2_font_8x13_mf);
u8g2.print(&amp;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 >&nbsp;</p>

<h2 ><b>3、天气显示相关说明</b></h2>

<p >&nbsp; &nbsp; &nbsp; &nbsp;天气显示单独为一个画面,借鉴了u8g2案例&ldquo;buffer/weather&rdquo;。u8g2提供的一些字体库中包含天气图标,案例中提供了&ldquo;晴、多云、阴、雨、雷&rdquo;五个例子。本人也是在网上没有找到其它介绍,因此本例也只用了上述五个图标。</p>

<pre>
<code>//u8g2案例,设置固定字体,并给编号67,就可以显示“雷”图标
//本人随意修改67这个编号值,还找到了齿轮和五角星图标
u8g2.setFont(u8g2_font_open_iconic_embedded_6x_t);
u8g2.drawGlyph(x, y, 67);</code></pre>

<p >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;u8g2的天气案例除了显示天气图标和温度值,还在最后一行显示流水字串,本例将最后一行用于显示城市名&mdash;&mdash;即&ldquo;Tianjin&rdquo;。</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;本例先通过自定义的getWeather()函数从心知天气请求到实时天气信息,因为刷屏逻辑为了保证时间显示正确,也就是1s刷新一次,而天气每正5分钟点时才请求一次,所以定义了全局变量用来存储天气编号(心知网站定义)、气温值。</p>

<p >&nbsp;</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 &gt; 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&lt;JsonObject&gt;();
      //get "results" property then parse the value to Json-Array
      JsonVariant resultsvar = obj["results"];
      JsonArray resultsarr = resultsvar.as&lt;JsonArray&gt;();
      //get array index 0 which is now weather info
      JsonVariant resultselementvar = resultsarr;
      JsonObject resultselementobj = resultselementvar.as&lt;JsonObject&gt;();
      /* get values of city, temp, code, update */
      //get city
      JsonVariant namevar = resultselementobj["location"]["name"];
      String namestr = namevar.as&lt;String&gt;();
      Serial.println(namestr);
      //get temperature,变量degree是全局的
      JsonVariant temperaturevar = resultselementobj["now"]["temperature"];
      String temperaturestr = temperaturevar.as&lt;String&gt;();
      degree = temperaturevar.as&lt;int&gt;();
      Serial.println(temperaturestr);
      //get weather code,变量code是全局的
      JsonVariant codevar = resultselementobj["now"]["code"];
      code = codevar.as&lt;int&gt;();
      Serial.println(code);
      //get last_update time
      JsonVariant last_updatevar = resultselementobj["last_update"];
      String last_updatestr = last_updatevar.as&lt;String&gt;();
      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 >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;心知天气对各种天气状态进行了编号(请查看:<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>&nbsp;),案例只引用了u8g2的五种图标,因而对解析到的心知天气状态码(code全局量)做了判断,以决定要显示的图标样式,天气编号在21以上的就显示一个&ldquo;五角星&rdquo;了。</p>

<p >&nbsp;</p>

<p class="imagemiddle" style="text-align: center;"></p>

<p align="center" >图9-1 心知天气现象代码对照表页面截图</p>

<p align="center" >&nbsp;</p>

<pre>
<code>//-----set weather icon function
void drawWeatherSymbol(u8g2_uint_t x, u8g2_uint_t y, uint8_t symbol) {
if(symbol&gt;=0 &amp;&amp; symbol&lt;4) {         //Sun
    u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
    u8g2.drawGlyph(x, y, 69);
} else if(symbol&gt;=4 &amp;&amp; symbol&lt;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&gt;=10 &amp;&amp; symbol&lt;13) {//Thunder
    u8g2.setFont(u8g2_font_open_iconic_embedded_6x_t);
    u8g2.drawGlyph(x, y, 67);
} else if(symbol&gt;=13 &amp;&amp; symbol&lt;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 >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; 对应的,loop()中的代码就是:</p>

<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
    getLocalTime(&amp;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 &amp;&amp; t.tm_sec==0) {
    getAHT10();
    if(WiFi.status() == WL_CONNECTED) {
      getWeather();
    }
}
//暂时省略其它代码
}</code></pre>

<p >&nbsp;</p>

<h2 ><b>4、按键翻页相关说明</b></h2>

<p >&nbsp; &nbsp; &nbsp; &nbsp;最后就是按键翻页了,这里开始考虑使用按键中断,但是触发中断后也不时出现重启,个人猜测可能是ISR和信号量之间有冲突,所以将翻页判断逻辑放到了loop()中。</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;首先,定义了一个枚举类型,表示不同的显示页,后续如果还想增加显示页面,可以再扩展枚举成员。</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;接着,就是定义记录页面编号的全局量,并初始化为&ldquo;TIMEPAGE&rdquo;,也就是开机后默认显示时钟画面。</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 >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;然后,就是宏定义按键IO&mdash;&mdash;本例用IO7,并作IO初始化。</p>

<pre>
<code>#define KEY             7


void setup() {
//-----initialize BSP
  Serial.begin(115200);
  pinMode(KEY, INPUT_PULLUP);

//省略其它代码

}</code></pre>

<p >&nbsp;</p>

<p >&nbsp; &nbsp; &nbsp; &nbsp;最后,由于按键中断的不给力,只能在loop()中进行按键判断了。</p>

<pre>
<code>void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
    getLocalTime(&amp;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 &amp;&amp; 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 >&nbsp;</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>

lugl4313820 发表于 2022-9-5 12:03

<p>楼主的U8g2用得非常好,建议除上图片+视频,让大家一睹为快!</p>

sonicfirr 发表于 2022-9-5 12:15

lugl4313820 发表于 2022-9-5 12:03
楼主的U8g2用得非常好,建议除上图片+视频,让大家一睹为快!

<p>好的,我再努力</p>

wangerxian 发表于 2022-9-6 13:11

<p>想看看效果,有视频的话就好了~</p>
页: [1]
查看完整版本: 【Beetle ESP32-C3】九、OLED时钟和天气助手逻辑说明(Arduino)