Weatherman: Port to WeMos D1 R2 ESP8266 board

The WEATHERMAN: Web Weather Forecaster original project was built using an Arduino UNO and an Ethernet shield to connect to the web to obtain the forecasted weather information. The need for an Ethernet connection was a severe limitation and the small amount of data RAM of the UNO meant that only three days worth of weather data could be processed…. Bummer!

However, my new WeMos D1 R2 WiFi ESP8266 Development Board has oodles of memory so the Weatherman project was a great way to test how easy it is to port existing the code onto the ESP8266 and to expand the forecasting to the full 7 days.

Et voila!

Weatherman: Port to WeMos ESP8266 WiFi Board

Weatherman: Port to WeMos D1 R2 WiFi ESP8266 Development Board

The native WiFi support in the ESP8266 simplifies the Weatherman code while the additional RAM allows all 7 days of the forecast to be received, processed and displayed.The LCD and LCD I2C libraries ported without any fuss, while most of the display routines and the text-based processing of the API response was retained intact.

On the hardware side, the display is the same 4×20 character LCD fitted with a I²C interface board as in the original project. The breadboard contains the pair of level shifters (using 2N7000 N-channel FETs) for the I²C pins – SDA and SCL – to interface the 3.3V of the WeMos board to the 5V of the display. For this version I decided not to use an RTC (although I could have added it to the I²C interface) as I derived day of the week information from each of the UTC timestamps of each day’s forecast present in the API response. Down the road I’ll add a simple case and shrink the level shifters onto a small “shield” with the display that will plug directly onto the WeMos board.

Here’s the code…

//**************************************************************************************************
//                                 WEB WEATHER FORECAST MONITOR
//                                     ESP8266 WeMos Board
//                                 Adrian Jones, February 2016
//
//**************************************************************************************************

// Build 1
//   r1 150320 - initial build with I2C 4x20 display
//********************************************************************************************
#define build 1
#define revision 1
//********************************************************************************************

#include <ESP8266WiFi.h>
#include <Wire.h> 

#include <LCD.h>                     // Standard lcd library
#include <LiquidCrystal_I2C.h>       // library for I@C interface

#define I2C_ADDR  0x27               // address found from I2C scanner
#define RS_pin    0                  // pin configuration for LCM1602 interface module
#define RW_pin    1
#define EN_pin    2
#define BACK_pin  3
#define D4_pin    4
#define D5_pin    5
#define D6_pin    6
#define D7_pin    7
#define backON    0xFF
#define backOFF   0x00

LiquidCrystal_I2C lcd(I2C_ADDR, EN_pin, RW_pin, RS_pin, D4_pin, D5_pin, D6_pin, D7_pin, BACK_pin, POSITIVE);
//ESP8266 pins for the LCD are SCL D15, SDA D14

// Local WiFi details
const char* ssid     = "Your_WiFi_SSID";     // SSID of local network
const char* password = "Your_WiFi_Password"; // Password on network
WiFiClient client;

// Server API details
char servername[]   = "api.openweathermap.org";                  // name for server
String sendReq  = "/data/2.5/forecast/daily?id=Your_LOCATION&units=metric&cnt=7&APPID=Your_APPID";     // send request
String inString = "";
char inChar;

const char custom[][8] = {                         // custom characters to form large numbers 
     { 0x01, 0x07, 0x0F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F },      // char 1: bottom right triangle
     { 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F },      // char 2: bottom block
     { 0x10, 0x1C, 0x1E, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F },      // char 3: bottom left triangle
     { 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x0F, 0x07, 0x01 },      // char 4: top right triangle
     { 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1E, 0x1C, 0x10 },      // char 5: top left triangle
     { 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x00, 0x00, 0x00 },      // char 6: upper block
     { 0x01, 0x07, 0x0F, 0x1F, 0x00, 0x00, 0x00, 0x00 },      // char 7: top right triangle
     { 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0F, 0x07, 0x01 }       // char 8: lower right triangle
};

const char bn3[][30] = {                                      // 3-line jumbo numbers
//         0               1               2               3               4              5               6                7               8               9
    { 0x01,0x06,0x03, 0x07,0xFF,0xFE, 0x07,0x06,0x03, 0x07,0x06,0x03, 0xFF,0xFE,0xFF, 0xFF,0x06,0x06, 0x01,0x06,0xFE, 0x06,0x06,0xFF, 0x01,0x06,0x03, 0x01,0x06,0x03 },
    { 0xFF,0xFE,0xFF, 0xFE,0xFF,0xFE, 0x01,0x06,0x06, 0xFE,0x06,0xFF, 0x06,0x06,0xFF, 0x06,0x06,0x03, 0xFF,0x06,0x03, 0xFE,0x01,0x05, 0xFF,0x06,0xFF, 0x06,0x06,0xFF },
    { 0x04,0x02,0x05, 0xFE,0xFF,0xFE, 0xFF,0x02,0x02, 0x08,0x02,0x05, 0xFE,0xFE,0xFF, 0x04,0x02,0x05, 0x04,0x02,0x05, 0xFE,0xFF,0xFE, 0x04,0x02,0x05, 0x08,0x02,0x05 }
};

byte row,nb=0,bc=0,bb[8];

// cardinal directions and days of the week
const char*  cards[] = {"N  ", "NNE", "NE ","ENE","E  ","ESE","SE ","SSE","S  ","SSW","SW ","WSW","W  ","WNW","NW ","NNW"};
const char*  days[] =  {"Sunday", "Monday", "Tuessdy","Wednesday","Thursday","Friday","Saturday"};

int loopTime, loopCount, s;
byte x;
boolean newdata=false;
#define ignore 160                                // number of characters to ignore

// days 
#define sw  D3                                    // switch input pin
boolean swdown=false;
#define today     0                                               
#define numdays   6                                // total number of days
int omode, cmode=today;                            // day mode variables

#define lpDelay     100                            // loop (ms)
#define reDelay     20*1000/lpDelay                // display refresh (s)
#define longDelay   300*1000/lpDelay               // long delay (s)
#define shortDelay    3*1000/lpDelay               // short delay (s)
#define timeout     3000                           // timeout (ms) for receiving server data

float  dt[numdays],da[numdays],ni[numdays],pr[numdays],hu[numdays],ws[numdays],wd[numdays],tx;            // day temp, night temp, pressure, humidity, sind speed and direction
String de[numdays];                               // text-based conditions
#define mps2kph 3.6                               // conversion from mps to kph
#define Jan12016 1451606400                       // 1 Jan 2016 (a Friday)

// serial control
#define sb   Serial.begin(57600)
#define sp   Serial.print
#define spf  Serial.printf
#define spln Serial.println

// lcd control
#define lb   lcd.begin(20, 4)                     // 4x20 character LCD display
#define lcc  lcd.createChar
#define lp   lcd.print
#define ls   lcd.setCursor
#define lw   lcd.write
#define lc   lcd.clear

extern "C" {
#include "user_interface.h"
}


// ********************************************************************************** //
//                                  CONNECTION SUBROUTINES
// ********************************************************************************** //

boolean doConnect() {
  spf("\nConnecting to %s ", ssid );
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); sp("."); }
  sp(" with IP address of "); 
  spln(WiFi.localIP());
  
  if (client.connect(servername, 80)) {         // starts client connection, checks for connection
    spln(F("Connecting to server"));        // if you get a connection, report back via serial:
    client.print(F("GET "));
    client.println(sendReq);
    client.println(F("Host: api.openweathermap.org"));
    client.println(F("Connection: close")); 
    client.println();
    return true;  
  } else {
    spln(F("\nConnection failed"));        // if you didn't get a connection to the server
    return false;
  }
}

// ********************************************************************************** //
//                               GENERAL SUBROUTINES
// ********************************************************************************** //
// getCardinal: converts the headsing (in degrees) to a text-based cardinal heading ("ENE", for example)
String getCardinal( float h ) { 
  h += 11.25; if (h > 360.0) h = h-360.0;
  return cards[(int) (h / 22.5)];
}

float getHumidex(float T, float H) {
  float tmp =  7.5*T/(237.7+T);
  tmp = pow(10, tmp);
  tmp = 6.112*tmp*H/100.0;
  tmp = (T +(5.0/9.0)*(tmp-10.0));
  return ((tmp <T)? T:tmp);
}

float getWindchill(float T, float W) {
  float tmp =  (0.045*(5.2735*sqrt(W)+10.45-0.2778*W)*(T-33)+33);
  return ((tmp > T)? T:tmp);
}

int dayWeek(long UTC) {
  long ndate = ( (UTC - Jan12016)-(5*3600) )/(24*3600);  // set for GMT-5:00
  return int(ndate+5)%7;                            // Jan 1, 2016 was a Friday (day 5)
}


// ********************************************************************************** //
//                                     SERIAL OUTPUT
// ********************************************************************************** //
void doSerial() {
  if(cmode==0) spln(F("\nToday")); else {spln(); spln(days[dayWeek(dt[cmode])]);} 
  sp(F("Timestamp: \t"));       sp(dt[cmode],0); spln(F(" UTC"));
  sp(F("Temperature: \tHi "));  sp(da[cmode],1); sp(F("C to Lo ")); sp(ni[cmode],1); spln(F("C"));
  sp(F("Humidex:\t"));          sp((getHumidex(da[cmode],hu[cmode])),1); spln(F("C")); 
  sp(F("Humidity:\t"));         sp(hu[cmode],0); spln(F("%"));
  sp(F("Pressure:\t"));         sp(pr[cmode],0); spln(F(" hpa"));
  sp(F("Wind speed:\t"));       sp(ws[cmode]*mps2kph,1); spln(F(" kph"));
  sp(F("Wind Direction:\t"));   sp(wd[cmode],0); sp(F(" deg [")); sp(getCardinal(wd[cmode])); spln(F("]"));
  sp(F("Windchill:\t"));        sp((getWindchill(da[cmode],ws[cmode]*mps2kph)),1); spln(F("C")); 
  sp(F("Conditions:\t"));       spln(de[cmode]);
  spln();
}

// ********************************************************************************** //
//                                     LCD OUTPUT
// ********************************************************************************** //
// printNum3(...): prints 3-character high [digit] to LCD and [left][top] position 
void printNum3(byte digit, byte leftAdjust, byte topAdjust) {
   for(row=0; row<3; row++) {
     ls(leftAdjust,row+topAdjust);               
     for(byte num=digit*3; num <digit*3+3; num++) {
       lw(bn3[row][num]);
     }
   }
}

// doTemp(): Display temperature on the LCD
void doTemp() {
  tx = da[cmode];
  ls(19,1); lp(" ");
  ls(11, 1); lp(F("   "));
  if(tx<0.0) { if(abs(tx)>9) ls(11, 1); else ls(13, 1); lw(0x06); }               // negative reading?
  if(abs(tx) > 9) printNum3(abs(tx)/10, 12, 0);                                   // double digit?
  printNum3(abs(int(tx))%10, 15, 0);                                              // temp units
  ls(19, 2); lp(int(abs(tx)*10)%10);                                              // single decimal place 
  ls(18, 2); lp(F("."));
}


// doLCD(): Display other data on the LCD
void doLCD() {
  lc();                                                                            // clear all first
  ls(0, 0);
  String snum= (String) days[dayWeek(dt[cmode])];
  if(cmode==0) lp(F("Today")); else lp(snum.substring(0,3));
  
  lp(F(" (")); lp(int(ni[cmode])); lp(F(") ")); 
  ls(0, 1); lp(hu[cmode],0); lp(F("% "));   lp(pr[cmode],0); lp(F("mb"));            // humidity & pressure
  ls(0, 2); lp(ws[cmode]*mps2kph,1); lp(F("kph ")); lp(getCardinal(wd[cmode]));              // wind speed and direction
  ls(0, 3); lp(de[cmode].substring(0,20));                                          // write out description (cut to 17 chars)
  ls(18, 0); lw(0xDF);  lp(F("C"));                                                // deg. C
  doTemp();                                                                        // write out current temp / humidex
}

// doSwitch(): handles loops for display updating and switch button to cycle days
void doSwitch() {
  if(loopCount++>reDelay) {
    loopCount=0;
    omode=-1; 
    cmode = today;
  }
  if(digitalRead(sw) && swdown) {                 // button released and previously pressed
    cmode++;
    if(cmode>=numdays) cmode=0;
    omode=-1;
    loopCount=0;
    swdown=false;                                  // reset switch down
  } 
  if(!digitalRead(sw)) swdown=true;                // button pressed
}


//*****************************************************************************************//
//                                      Initial Setup
//*****************************************************************************************//
void setup() {
  sb;                                             // Open serial port
  delay(100);
  sb;                                             // seems to work better this way
  spln();
  spln(F("********************************************"));
  spln(F("WEB WEATHER FORECAST MONITOR by Adrian Jones"));
  sp(F("Build ")); sp(build); sp(F(".")); spln(revision);
  sp(F("Heap Size: \t"));    sp(system_get_free_heap_size()/1024); spln(F("KB"));
  sp(F("Boot Version: \t")); spln(system_get_boot_version());
  sp(F("CPU Speed: \t"));    sp(system_get_cpu_freq()); spln(F("MHz"));
  spln(F("*******************************************"));
  
  lb;
  for (nb=0; nb<8; nb++ ) {                     // create 8 custom characters for big numbers
    for (bc=0; bc<8; bc++) bb[bc]= custom[nb][bc];
    lcc( nb+1, bb );
  }
  lc();
  ls(0, 0); lp(F("WEB WEATHER STATION")); 
  ls(4, 1); lp(F("Version ")); lp(build); lp(F(".")); lp(revision); lp(F(" ")); 
  ls(2, 3); lp(F("... waiting ..."));
  
  pinMode(sw, INPUT);                         // switch input
  loopTime = 0;                               // initially
  loopCount = 0;
  cmode=today;
}

//*****************************************************************************************//
//                                      MAIN LOOP
//*****************************************************************************************//

void loop() { 
  lcd.setBacklight(backON);
  if(--loopTime < 0) {                              // if ready, do main loop to get weather forecast data from server       
    loopTime = doConnect()? longDelay: shortDelay;  // connect to server. If OK, long delay, else try again shortly
    inString = "";
    s=0;
    long timenow = millis();                        // set timeout counter
    while( (timenow+timeout) > millis() ) {
      boolean done=false;
      while(client.connected() && !client.available()) delay(1); //waits for data
      while (client.available()) {                  // while there are some... 
        inChar = client.read();                     // read incoming bytes if available
        if(s++ >ignore && inChar!=char(0x22) && inChar!=char(0x5B) && inChar!=char(0x5D) && inChar!=char(0x7B) && inChar!=char(0x7D) && !done) inString += inChar;    // ignore first chars and remove quotation marks["]
        if( !done && inString.length()>1400 ) { inString +="\n"; done=true; }  // if string length has sufficient days, you're done
      }
    }

    if(inString.length()>300) {                     // Parse new input string
      sp("Data = "); spln(inString);
      s=0; for(x=0;x<numdays;x++) { s=inString.indexOf("dt:",s)+3;        dt[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }  // date code (UTC timestamp)
      s=0; for(x=0;x<numdays;x++) { s=inString.indexOf("max:",s)+4;       da[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }  // daytime temperature (C)
      s=0; for(x=0;x<numdays;x++) { s=inString.indexOf("min:",s)+4;       ni[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }  // nighttime temperature (C)
      s=0; for(x=0;x<numdays;x++) { s=inString.indexOf("pressure:",s)+9;  pr[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }  // pressure (mb)
      s=0; for(x=0;x<numdays;x++) { s=inString.indexOf("humidity:",s)+9;  hu[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }  // humidity (%)
      s=0; for(x=0;x<numdays;x++) { s=inString.indexOf("speed:", s)+6;    ws[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }  // wind speed (mps)
      s=0; for(x=0;x<numdays;x++) { s=inString.indexOf("deg:", s)+4;      wd[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }  // wind direction (deg.)
      s=0; for(x=0;x<numdays;x++) { s=inString.indexOf("cription:", s)+9; de[x]=inString.substring(s,(inString.indexOf(',',s)));  }           // conditions text
      newdata=true;                                 // set new data flag
      cmode=today;                                  // start with todays information
    }
    if (!client.connected()) { 
      spln(F("Disconnecting from server")); 
      client.stop();                                // if the server's disconnected, stop the client
    } 
  }
// end of main loop to get weather forecast data from server
  if(omode != cmode || newdata) { 
    doSerial();                           // write out to serial port
    doLCD();                              // write out onto LCD
    omode=cmode;                          // set old mode  
    newdata=false;                        // reset new data flag
  }
  doSwitch();                             // check switch
  delay(lpDelay);                         // wait for loop delay
}

Overall, the port was very straight-forward with no major issues found.

Now, I have 7 days of weather forecast anywhere where there is a WiFi signal!

If only it would shovel snow!

3 thoughts on “Weatherman: Port to WeMos D1 R2 ESP8266 board

  1. THE

    Hi Adrian!
    Great work from your side!
    It worked with minor modifications (different i2c adress of the lcd) like a charm!
    I will use this project (at least parts of it) to teach kids in Germany.
    And I can make my mom a nice birthday present, when I finished the translation.
    Thanks from me.
    Timo
    PS: One question: Does your wemos weatherman also stop syncing and does not react when you press the switch?

    Reply

Come on... leave a comment...