WeatherSense 9001® Arduino-ESP32 Weather Station – The Code – (Part 2 of 5)

The project code is written in C++ for the ESP32 in the Arduino IDE.

We will dispense with the library #include clutter and focus on key code areas since the full code base is available on GitHub, for both the CrowPanel 7.0″ and 5.0″ models:
WeatherSense 9001® Arduino-ESP32 Weather Station (for CrowPanel 7.0″)
WeatherSense 9001® Arduino-ESP32 Weather Station (for CrowPanel 5.0″)

Projects, videos and instructional blog posts like this take a lot of effort to research and produce. I have over 100 hours in this project so far. If this has been helpful or interesting, please consider a donation to help offset the extensive time and effort I put into these projects. Your support for small creators like me is greatly appreciated. Also, it has been shown that donating will allow much positive karma, fortune and mojo to come your way.

Buy Me A Coffee

WiFi credentials are defined in a separate include file named “secrets.h”. Static IP address, subnet, gateway and primary DNS are assigned.

// WiFi Credentials / IP Setup 
const char* ssid = SECRET_WIFISSID;
const char* password = SECRET_WIFIPASS;
IPAddress ip(192, 168, 20, 21);
IPAddress gateway(192, 168, 20, 1);
IPAddress subnet(255, 255, 255, 0);
IPAddress dns(192, 168, 20, 1); //primaryDNS

Time zone parameters are set for my location, Eastern Standard Time and Eastern Daylight Saving Time. The venerable Timezone library by JChristensen is used.

// US Eastern Time Zone (New York, Detroit)
TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240};  // Eastern Daylight Time = UTC - 4 hours
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300};   // Eastern Standard Time = UTC - 5 hours
Timezone usET(usEDT, usEST);

A data structure is defined to hold the readings returned from each sensor, including sensor name, temperature and humidity with minimum and maximum values for alerts, last update date and time, sensor battery voltage and WiFi signal strength.

// Define structure to hold readings returned for a sensor
struct readings{
  char desc[10];
	float temp;
  float tempalert_min;
  float tempalert_max;
	float hum;
  float humalert_min;
  float humalert_max;
  float pres;
	char upd[30];
	float bat;
	float wifi;
};

Sensor IDs are defined in the “secrets.h” include file. Threshold values for sensor low battery level and low WiFi signal strength are hard coded.

// Constants for sensors
const char *cchrSensor1 = SECRET_SP_SENSOR1_ID;
const char *cchrSensor2 = SECRET_SP_SENSOR2_ID;
const char *cchrSensor3 = SECRET_SP_SENSOR3_ID;
const float cfltLowBatteryThreshold = 2.3;
const float cfltLowWifiThreshold = -85.0;

Constants are defined for the various refresh intervals.

auth_minuteshow often to refresh SensorPush API authorization
refresh_minuteshow often to read sensor data
retry_minuteshow often to retry on bad sensor read or WiFi signal loss
pressupd_minuteshow often to update the barometric pressure chart values
const int auth_minutes = 23;      //refresh interval (in minutes) for SensorPush API authentication refresh call
const int refresh_minutes = 10;   //refresh interval (in minutes) for SensorPush API calls to retrieve sensor readings
const int retry_minutes = 1;      //retry interval (in minutes) for bad SensorPush API read
const int pressupd_minutes = 30;  //refresh interval (in minutes) for pressure chart update

ThingSpeak API key credentials are defined in “secrets.h” include file

//Constants for ThingSpeak
const unsigned long glngThingSpeakChannel = SECRET_TS_CHANNEL_ID;
const char * gchrThingSpeakWriteAPIKey = SECRET_TS_WRITE_APIKEY;

The API_Post() function is the main generic helper function used to call and SensorPush API via http POST with passed arguments for URL, headers, body and call type. Each API call is proceeded by a check for WiFi connection. If no connection, function BadRead (described below) is called and a WiFi retry/reconnect process is begun. The function call to the API can be of various types, as defined by ENUM calltype:

AUTHObtain SensorPush API authorization
ACCESSObtain SensorPush API access token
SAMPLERead sensor temperature and humidity data samples from SensorPush API
SENSORRead sensor battery voltage, WiFi signal strength, and alert minimums and maximums from SensorPush API
THINGSPEAKPush data values to ThingSpeak IoT platform API
API_Post call types
// Function to call and API via http POST with passed arguments for URL, headers, body and call type
void API_Post(char *pchrURL, char *pchrHeader_ContentType, char *pchrHeader_Accept, char *pchrHeader_Value1, char *pchrHeader_Authorization, calltype penuCallType)
{

  if (WiFi.status() != WL_CONNECTED) 
  {
    BadRead("Wifi error");
    unsigned long currentMillis_Wifi = millis();
    // if WiFi is down, try reconnecting every interval_Wifi milliseconds
    if (currentMillis_Wifi - previousMillis_Wifi >= interval_Wifi) 
    {
      Serial.print(millis());
      Serial.println(" -> Reconnecting to WiFi...");

Function BadRead() accepts an error message string to display on the status bar at the bottom of the LCD display. The color of the status bar is set to alert colors while the error remains active. Errors can be for various reasons, including loss of WiFi connection, API offline or some other type of bad data read.

//Function to report error message to serial monitor and display when a bad API read is encountered
void BadRead(char *pchrErrorDesignator)
{
  gblnGoodRead = false;
  Serial.println(pchrErrorDesignator);
  SetAlertColors(ui_Status1);
  lv_label_set_text(ui_Status1, pchrErrorDesignator);
  if (gintResponseCode == 400)
    {

Function SP_Authentication() handles SensorPush API Authorization by attempting to log in using a valid email/password (read from secrets.h include file) to receive an API authorization code. The API authorization code is then used to retrieve an API access token that will be used for subsequent API calls for sensor data. The ArduinoJSON library is used to serialize and deserialize data to and from the various API calls.

// SensorPush Authorization
// Log in using a valid email/password to receive an authorization code
// Use authorization code to retrieve access token that will be used for subsequent API calls for sensor data
void SP_Authentication()
{
  gblnGoodRead = true;

  char chrURL[100] = "https://api.sensorpush.com/api/v1/oauth/authorize";
  char chrHeader_ContentType[25] = "application/json";
  char chrHeader_Accept[25] = "application/json";
  char chrHeader_Authorization[1000] = "";
  char chrHeader_Value1[100] = "";
  sprintf(chrHeader_Value1, "{\"email\":\"%s\",\"password\":\"%s\"}",SECRET_SP_LOGINID,SECRET_SP_PASS);

  API_Post(chrURL,chrHeader_ContentType,chrHeader_Accept,chrHeader_Value1,chrHeader_Authorization,calltype::AUTH);

Functions UpdateDisplay_Float() and UpdateDisplay_TimeStamp() update the LCD UI display fields for float and string values respectively for actual values read from sensor.

// Update display with passed arguments for a FLOAT field 
void UpdateDisplay_Float(float pfltValue, lv_obj_t *puiControlName, int pintSensorNumber, int pintInitialLength, int pintMinimumLength, int pintDecimalPlaces, char *pchrFormat)
{
  char chrValue[pintInitialLength];
  char chrValueFinal[pintInitialLength+strlen(pchrFormat)-2];   //dynamically set final display string length to be value plus NULL terminator minus %s designator
  dtostrf(pfltValue, pintMinimumLength, pintDecimalPlaces, chrValue);   //convert float to string, min len, decimal places
  sprintf(chrValueFinal, pchrFormat, chrValue);  
  Serial.println(chrValueFinal);
  lv_label_set_text(puiControlName, chrValueFinal);

  //Decrement sensor number to account for 0-based array
  pintSensorNumber--;

The real meat of processing code is in the functions my_timer_auth(), my_timer_samples(), and my_timer_pressupd(). These timer functions are defined in standard Arduino function setup() with their respective timers and intervals. Then every defined time interval for each timer, a system interrupt is generated and the respective function listed above is called.

For example, every 10 minutes the my_timer_samples() code is called to kick off reading the sensor data samples.

Note that the timers are set up using the included lvgl graphics library function lv_timer_create() and initiated with related function lv_timer_ready().

my_timer_auth() simply calls SP_Authentication() as explained above on a defined time interval as required by the SensorPush API to keep the connection active.

my_timer_samples() is mostly the meat and traffic cop of the project, assuring authentication to the SensorPush API before calling the API again for the retrieve the actual data sample readings on a defined timer interval. It then calls the functions above to update all the fields on the LCD display and setting alert colors if maximum and minimum thresholds are exceeded. Finally, it writes the values to the channel on the ThingSpeak platform via API.

my_timer_pressupd() updates the barometric pressure chart values on a defined timer interval.


The Arduino Stack Size definition seems to be a tricky one. The line of code to override the default stack size (8K) must exist directly before the standard Arduino setup() function. While the default stack size of 8K is typically acceptable, after adding many libraries and code calls, runtime crashes occurred repeatedly with error messages leading to a need to increase stack size.

A command has been added to the Arduino standard loop() function to list the free stack and heap size on each iteration. The author found it to be somewhat sorcery to get the correct stack size value that will work. Setting it too high can be as bad as too low.

A happy medium was found using the 11K value shown below, but this needs modified as code or libraries change.

It should be noted that repeated running of this code over time produces no measurable memory leaks.

// This sets Arduino Stack Size - comment this line to use default 8K stack size
SET_LOOP_TASK_STACK_SIZE(11*1024); // 11K

The standard Arduino function setup() runs one-time code needed for CrowPanel GPIO hardware and display initialization as well as the SquareLine Studio-created UI. The WiFi network and client is initialized.

void setup()
{
  Serial.begin(9600);
  delay(1000);

  if (llLogLevel == MAX) {Serial.println("WeatheratorCrow7");} else {Serial.println("WC7");}

  //GPIO init
  #if defined (CrowPanel_50) || defined (CrowPanel_70)
    pinMode(38, OUTPUT);
    digitalWrite(38, LOW);
    pinMode(17, OUTPUT);
    digitalWrite(17, LOW);

LVGL-based timers are initialized in setup() to start on a defined interval for API authorization keep-alive and API data sample reads.

  // Set up timer to call my_timer_auth method every timer increment
  //Refresh of API access token required only every 60 minutes
  timerauth = lv_timer_create(my_timer_auth, authinterval , NULL);

  // Set up timer to call my_timer_samples method every timer increment
  timersamples = lv_timer_create(my_timer_samples, refreshinterval,  NULL);
  
  // Set up timer to call my_timer_pressupd method every timer increment
  timerpressupd = lv_timer_create(my_timer_pressupd, pressupdinterval,  NULL);
  

Lastly, loop() is the standard Arduino loop repeat function. It’s main purpose is to refresh the main timer handler to check the timers for interval triggers. loop() also contains a display refresh delay of 5 ms as required by the CrowPanel for proper operation without hang or crash.

Finally, there is code to retry data sample reads on a defined interval in case of a bad API read or lost WiFi connection.

void loop()
{
    if (gblnFirstRun){
      Serial.printf("Stack:%d,Heap:%lu\n", uxTaskGetStackHighWaterMark(NULL), (unsigned long)ESP.getFreeHeap());
      gblnFirstRun = false;
    }
    
    unsigned long currentMillis = millis();
    if ((!gblnGoodRead) && (currentMillis - previousMillis >= retryinterval)) 
    {
      previousMillis = currentMillis;
      lv_timer_ready(timersamples);
    }
    
    lv_timer_handler();
    delay(5);  //required for display refresh
}

One final note is that the Arduino-ESP32 .ino code is slightly different for the CrowPanel 7.0″ vs the CrowPanel 5.0″. These relate to constants being set to 5.0 vs 7.0 so the correct parameters are set for display initialization.

Also due to hardware differences, the PCA9775 Arduino library is required for the 7.0″ display but not the 5.0″. Be sure to download and manually copy to the Arduino libraries folder the provided PCA9775 library from my GitHub project link above for the 7.0″ display. Installing other versions of this library found online do not seem to work.