This review is a continuation of the previous one , introducing the main business logic of the case completed in the previous one.
1. Clock function instructions
The case uses network timing, and configures the ESP32 RTC during initialization, and then the local time can be obtained through the RTC. In fact, this function was developed by me two years ago, based on ESP32 WROOM at that time, and this time it was changed to C3. However, at that time, I mistakenly thought that it was just "obtaining network time" and saving it to the time structure variable struct tm t (the case defines the variable name as "t"), so it was completely superfluous to create a code that updates the value of variable t every second. In fact, the value of t can be refreshed at any time by directly accessing the RTC.
In the seventh review of ESP32-C3, I continued this wrong idea, so I wrote the relevant 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");
}
In fact, I suspected the mistake I made when I wrote the seventh review, but I didn't modify it (I was just too lazy) until I finished the OLED clock case in the previous article. A bug appeared - the time display sometimes turned into "HH:mm:60", that is, the seconds were displayed as 60. So, the code was adjusted to:
void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
getLocalTime(&t);
}
//省略其它代码
}
I also tried to call getLocalTime(&t) directly in the timer callback. As a result, the system started to restart continuously and prompted the reason for the restart: "rst:0x3 (RTC_SW_SYS_RST)". I had to change back to using the semaphore.
2. Clock display related instructions
During the development stage, I was also confused and put the time display code into the statement block that receives the semaphore, thinking, "There's nothing wrong with refreshing the screen once a second."
void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
getLocalTime(&t);
}
//省略其它代码
}
However, this also caused some inexplicable bugs, and the screen would freeze from time to time. Later, I figured out that "when the signal is not received, the task will be blocked, and there is no need to put the screen refresh operation in the statement block", so only the time refresh is done in the statement block, and the screen refresh code can be put outside.
void loop() {
//-----update time state per second
if(xSemaphoreTake(timersem, 0) == pdTRUE) {
getLocalTime(&t);
}
//-----refresh screen 暂时省略其它代码
drawLocalTimePage();
}
drawLocalTimePage() is a time display function written by myself. I feel that there is still plenty of space on the screen. AHT10 is also plugged into the breadboard, so the remaining screen space displays the sensor temperature and humidity.
In the time display screen, the first line (the comment line1 in the following code) is the "HH:MM:SS" time display and the week abbreviation - that is, the statement: "u8g2.print(&t, "%H:%M:%S %a");", struct tm has defined formatting characters in C++, which can be found at: https://baike.baidu.com/item/strftime/9569073?fr=aladdin . The font selected in the first line is u8g2_font_ncenB14_tr, and its specific pixel value is not found. Combining the font name and visual inspection, I guess it is 7x14. The time + week has a total of 14 characters (two spaces in the middle), which is 98 pixels horizontally. However, it looks beautiful when set to display at the top, so I didn't modify it.
The second line (the commented line2 in the following code) is the "YYYY-mm-dd" date display, with a total of 10 characters, using the font u8g2_font_8x13_mf (the character pixels should be 8*13 according to the name), occupying 80 pixels horizontally, and centered with 24 pixels left and right (OLED screen is 128*64), so the display coordinates are set to start from [24, 18] - the first line height of 14 pixels plus the line spacing is set to a y coordinate of 18.
The third and fourth lines show humidity and temperature. Taking humidity as an example, "Humi: " has six characters (plus a space), the humidity value occupies six characters (converted by dtostrf() function, the length is limited to 6), followed by the unit "% rH" occupies four characters, which is exactly 16 characters (the font is still u8g2_font_8x13_mf). Therefore, the y coordinate of the humidity line is 32, and the y coordinate of the temperature line is 48.
In addition, the temperature and humidity values are floating point numbers. If you use u8g2.print() to output, there will be a bug. It seems that the Printable interface of ESP32-Arduino (a class in the framework) has not been able to handle floating point numbers. Therefore, dtostrf() is used here for conversion, and then drawStr() is used for display. However, the temperature unit "°" is a UTF character, and it can only be successfully displayed using u8g2.print().
//-----draw local time function
void drawLocalTimePage(void) {
char str[6] = {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();
}
3. Weather display instructions
The weather display is a separate screen, which is based on the u8g2 case "buffer/weather". Some font libraries provided by u8g2 contain weather icons, and the case provides five examples of "sunny, cloudy, overcast, rainy, and thunderous". I also couldn't find other introductions on the Internet, so this example only uses the above five icons.
//u8g2案例,设置固定字体,并给编号67,就可以显示“雷”图标
//本人随意修改67这个编号值,还找到了齿轮和五角星图标
u8g2.setFont(u8g2_font_open_iconic_embedded_6x_t);
u8g2.drawGlyph(x, y, 67);
In addition to displaying the weather icon and temperature value, the u8g2 weather example also displays a running string in the last line. In this example, the last line is used to display the city name, that is, "Tianjin".
In this example, the real-time weather information is requested from Xinzhi Weather through the custom getWeather() function. Because the screen refresh logic is to ensure the correct time display, that is, refresh once every 1s, and the weather is only requested once every 5 minutes, global variables are defined to store the weather number (defined on the Xinzhi website) and temperature value.
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("[HTTP] GET... code: %d\n", httpCode);
if(httpCode == HTTP_CODE_OK) {
String payload = http.getString();
Serial.println(payload);
//parse response string to 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[0];
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("[HTTP] GET... failed, error: %s\n",
http.errorToString(httpCode).c_str());
}
http.end(); //end http connectiong
}
}
Xinzhi Weather has numbered various weather conditions (please see: https://seniverse.yuque.com/books/share/e52aa43f-8fe9-4ffa-860d-96c0f3cf1c49/yev2c3 ). The case only references five icons of u8g2, so a judgment is made on the parsed Xinzhi Weather status code (code global variable) to determine the icon style to be displayed. Weather numbers above 21 will display a "five-pointed star".
Figure 9-1 Screenshot of the Xinzhi Weather Phenomenon Code Comparison Table page
//-----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();
}
Correspondingly, the code in loop() is:
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();
}
}
//暂时省略其它代码
}
4. Instructions for turning pages using buttons
The last step is to turn the pages by pressing the button. Here we start to consider using key interrupts, but restarts occur from time to time after the interrupt is triggered. I personally guess that there may be a conflict between the ISR and the semaphore, so the page turning judgment logic is placed in loop().
First, an enumeration type is defined to represent different display pages. If you want to add more display pages later, you can expand the enumeration members.
Next, define a global variable for recording the page number and initialize it to "TIMEPAGE", which means that the clock screen will be displayed by default after booting up.
//-----show page enum
typedef enum {
TIMEPAGE = 0,
WEATHERPAGE,
} AITA_PAGE_INDEX;
//-----global variable of page No
int pagestate = TIMEPAGE;
Then, define the key IO in the macro - use IO7 in this example, and initialize IO.
#define KEY 7
void setup() {
//-----initialize BSP
Serial.begin(115200);
pinMode(KEY, INPUT_PULLUP);
//省略其它代码
}
Finally, due to the weak key interrupt, the key judgment can only be performed in loop().
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++;
}
}
}