E-Ink Weather Display

An E-Ink display is perfect for displaying information that does not change quickly. Displaying weather data and perhaps even a weather forecast is something that usually does not require an update every second.

Where to find weather data

Weather data can be retrieved by openweathermap for example. All it needs is an account. The retrieval of current weather data and a five day / three hour forecast is for free within certain limits. Limit means that the calls per minute of the service may not exceed 60.

Openweathermap provides an API that can be used in various programming languages. In the end an URL is used to fetch the desired data. The format of the result can be selected. The default is the JSON format which can be relatively simple parsed on Arduino using libraries such as ArduinoJson. Still I found the summary of the typical pitfalls helpful.

The same principle can be used with a tailor made outdoor weather sensor in combination with a custom HTTP web server that delivers the requested data in the desired format. But that is a different blog post.

Example URL

To retrieve the weather data from openweathermap a simple URL is required. This URL may contain the city ID which can be found here, the desired unit system and the API key that can be generated after sign up on openweathermap. Other parameters to adjust the resulting weather data can be added optionally.

The weather data for the desired location can be accessed in several ways: by city name, by zip code, by geographic location, … . However, openweathermap recommends to use the ID of the location. This list contains the IDs for the available locations.

This example of an URL will work in a browser as well (using a valid API key!):

http://api.openweathermap.org/data/2.5/weather?id=2643743&units=metric&APPID=YourAPIKey

Example Result in JSON format

Below is an exemplary result string:

{"coord":{"lon":-0.13,"lat":51.51},
"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],
"base":"cmc stations",
"main":{"temp":15.25,"pressure":1017,"humidity":77,"temp_min":13,"temp_max":17},
"wind":{"speed":5.1,"deg":110},
"clouds":{"all":44},
"dt":1464380212,
"sys":{"type":1,"id":5091,"message":0.0065,"country":"GB",
"sunrise":1464321142,
"sunset":1464379442},
"id":2643743,"name":"London",
"cod":200}

How to retrieve the current weather data is explained in more detail on openweathermap/current.

Components used & Wiring

The hardware setup is the same as in the previous blog post.

Software

The Arduino sketch to retrieve and process weather data from openweathermap is based on an example from the ArduinoJson library. This example was extended to retrieve and process the weather data from openweathermap and to display the information on the E-Ink display.

// based on: https://github.com/bblanchon/ArduinoJson/blob/master/examples/JsonHttpClient/JsonHttpClient.ino

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ArduinoJson.h>
#include <epd.h>

const char* ssid = "SSID";
const char* password = "WIFIPASSWORD";

const char* server = "api.openweathermap.org"; // server's address
const int port = 80;
const char* resource = "/data/2.5/weather?id=2643743&units=metric&APPID=YourAPIKey"; // http resource

const unsigned long BAUD_RATE = 115200; // serial connection speed
const unsigned long HTTP_TIMEOUT = 10000; // max respone time from server
const size_t MAX_CONTENT_SIZE = 1024; // max size of the HTTP response

/* example URL
http://api.openweathermap.org/data/2.5/weather?id=2643743&units=metric&APPID=YourAPIKey
example result in JSON format:
{"coord":{"lon":-0.13,"lat":51.51},
"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],
"base":"cmc stations",
"main":{"temp":15.25,"pressure":1017,"humidity":77,"temp_min":13,"temp_max":17},
"wind":{"speed":5.1,"deg":110},
"clouds":{"all":44},
"dt":1464380212,
"sys":{"type":1,"id":5091,"message":0.0065,"country":"GB",
"sunrise":1464321142,
"sunset":1464379442},
"id":2643743,"name":"London",
"cod":200}*/

// weather data type
struct WeatherData {
char cityName[20];
char nowDescription[50];
char temperature[6];
char humidity[3];
char pressure[5];
char iconCode[4];
};

WiFiClient client;
boolean debug=true;

void initSerial();
void initializeEInkDisplay();
void connectWiFi();
bool connect(const char* hostName);
void disconnect();
void wait();
bool sendRequest(const char* host, const char* resource);
bool skipResponseHeaders();
void readReponseContent(char* content, size_t maxSize);
void printWeatherData(const struct WeatherData* weatherData);
bool parseWeatherData(char* content, struct WeatherData* weatherData);

void setSmallText(String text, int x, int y);
void setMediumText(String text, int x, int y);
void setLargeText(String text, int x, int y);

void updateDisplay(struct WeatherData* weatherData);
String selectWeatherIcon(struct WeatherData* weatherData);

void setup() {
initSerial();
initializeEInkDisplay();
connectWiFi();
}

void loop() {
if( connect(server) ) {
if( sendRequest(server, resource) && skipResponseHeaders() ) {

char jsonResult[MAX_CONTENT_SIZE];
readReponseContent(jsonResult, sizeof(jsonResult));

WeatherData weatherData;
if( parseWeatherData(jsonResult, &weatherData) ) {
printWeatherData(&weatherData);
updateDisplay(&weatherData);
}
}
disconnect();
}
wait();
}

void initSerial() {
Serial.begin(BAUD_RATE);
while (!Serial) {
; // wait for serial port to initialize
}
if( debug ) Serial.println("Serial ready");
}

void initializeEInkDisplay() {
epd_init();
epd_wakeup();
epd_set_memory(MEM_TF); // MEM_NAND=internal memory; MEM_TF=sd card
epd_set_color(BLACK, WHITE);
}

void connectWiFi() {
WiFi.mode(WIFI_STA);
// connect to the WiFi network
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
if( debug ) Serial.print(".");
}
if( debug ) {
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
}

bool connect(const char* hostName) {
if( debug ) {
Serial.print("Connect to ");
Serial.println(hostName);
}
bool ok = client.connect(hostName, port);
if( debug ) Serial.println(ok ? "Connected" : "Connection Failed!");
return ok;
}

bool sendRequest(const char* host, const char* resource) {
if( debug ) {
Serial.print("GET ");
Serial.println(resource);
}
client.print("GET ");
client.print(resource);
client.println(" HTTP/1.1");
client.print("Host: ");
client.println(server);
client.println("Connection: close");
client.println();
return true;
}

bool skipResponseHeaders() {
// HTTP headers end with an empty line
char endOfHeaders[] = "\r\n\r\n";
client.setTimeout(HTTP_TIMEOUT);
bool ok = client.find(endOfHeaders);
if (!ok) {
if( debug ) Serial.println("No response or invalid response!");
}
return ok;
}

void readReponseContent(char* content, size_t maxSize) {
size_t length = client.readBytes(content, maxSize);
content[length] = 0;
if( debug ) {
Serial.println("readReponseContent");
Serial.println(content);
}
}

bool parseWeatherData(char* content, struct WeatherData* weatherData) {
// find first and last curly bracket of JSON result
String cStr = String(content);
int firstCurlyBracket = cStr.indexOf('{');
int lastCurlyBracket = cStr.lastIndexOf('}');
String c = cStr.substring(firstCurlyBracket,lastCurlyBracket+1);

/*if( debug ) {
Serial.println("parseWeatherData: ");
Serial.print("content length: ");
Serial.println(c.length());
Serial.println(c.c_str());
}*/

c.toCharArray(content, MAX_CONTENT_SIZE);

/*if( debug ) {
Serial.println("parseWeatherData: ");
Serial.println(content);
}*/

StaticJsonBuffer<MAX_CONTENT_SIZE> jsonBuffer;

// find fields in JSON object
JsonObject& root = jsonBuffer.parseObject(content);
if (!root.success()) {
if( debug ) Serial.println("parsing JSON Object() failed");
return false;
}

strcpy(weatherData->cityName, root["name"]);
strcpy(weatherData->nowDescription, root["weather"][0]["description"]);
strcpy(weatherData->temperature, root["main"]["temp"]);
strcpy(weatherData->humidity, root["main"]["humidity"]);
strcpy(weatherData->pressure, root["main"]["pressure"]);
strcpy(weatherData->iconCode, root["weather"][0]["icon"]);
return true;
}

void printWeatherData(const struct WeatherData* weatherData) {
if( debug ) {
Serial.print("city name = ");
Serial.println(weatherData->cityName);
Serial.print("temperature = ");
Serial.print(weatherData->temperature);
Serial.println(" *C");
Serial.print("humidity = ");
Serial.print(weatherData->humidity);
Serial.println(" %");
Serial.print("pressure = ");
Serial.print(weatherData->pressure);
Serial.println(" hPa");
}
}

void disconnect() {
if( debug ) Serial.println("Disconnect from HTTP server");
client.stop();
}

void wait() {
if( debug ) Serial.println("Wait 30 seconds");
delay(30000);
}

//-------- e-ink display code ----------
void setSmallText(String text, int x, int y) {
epd_set_ch_font(GBK32);
epd_set_en_font(ASCII32);
epd_disp_string(text.c_str(), x, y);
}

void setMediumText(String text, int x, int y) {
epd_set_ch_font(GBK48);
epd_set_en_font(ASCII48);
epd_disp_string(text.c_str(), x, y);
}

void setLargeText(String text, int x, int y) {
epd_set_ch_font(GBK64);
epd_set_en_font(ASCII64);
epd_disp_string(text.c_str(), x, y);
}

void updateDisplay(struct WeatherData* weatherData) {
epd_clear();
int distStart=50;
setLargeText(String(weatherData->cityName), 50, distStart);
distStart += 100;
epd_draw_line(10, distStart, 500, distStart); // horizontal line
distStart += 20;
setMediumText(String(weatherData->nowDescription), 50, distStart);
distStart += 60;
epd_draw_line(10, distStart, 500, distStart); // horizontal line

distStart += 50;
setMediumText(String("temperature: ") + String(weatherData->temperature) + String(" *C"), 50, distStart);
distStart += 60;
setMediumText(String("humidity: ") + String(weatherData->humidity) + String(" %"), 50, distStart);
distStart += 60;
setMediumText(String("pressure: ") + String(weatherData->pressure) + String(" hPa"), 50, distStart);
distStart += 60;

epd_draw_line(10, distStart, 500, distStart); // horizontal line

epd_draw_line(510, 10, 510, 590); // vertical line

// display weather icon
String weatherIcon = selectWeatherIcon(weatherData);
epd_disp_bitmap(weatherIcon.c_str(), 560, 100);

epd_udpate();
}

String selectWeatherIcon(struct WeatherData* weatherData) {
String iconCode = String(weatherData->iconCode);
if( debug ) {
Serial.print("selectWeatherIcon: ");
Serial.println(iconCode.c_str());
}
if( iconCode.startsWith("01") ){
if( debug ) Serial.println("SUN.BMP");
return String("SUN.BMP");
} else if( iconCode.startsWith("02") ){
if( debug ) Serial.println("CLOUDY.BMP");
return String("CLOUDY.BMP");
} else if( iconCode.startsWith("03") ){
if( debug ) Serial.println("CLOUD.BMP");
return String("CLOUD.BMP");
} else if( iconCode.startsWith("04") ){
if( debug ) Serial.println("CLOUD.BMP");
return String("CLOUD.BMP");
} else if( iconCode.startsWith("09") ){
if( debug ) Serial.println("RAIN.BMP");
return String("RAIN.BMP");
} else if( iconCode.startsWith("10") ){
if( debug ) Serial.println("UNSETTLED.BMP");
return String("UNSET.BMP");
} else if( iconCode.startsWith("11") ){
if( debug ) Serial.println("THUNDER.BMP");
return String("THUND.BMP");
} else if( iconCode.startsWith("23") ){
if( debug ) Serial.println("SNOW.BMP");
return String("SNOW.BMP");
} else {
if( debug ) Serial.println("UNSETTLED.BMP");
return String("UNSET.BMP");
}
}

Beautifying

To get a quick overview off the weather or the forecast an icon does the trick better than pure text. One can design own weather icons or search thenounproject for beautiful examples. The icons have to be uploaded first to the micro SD card of the E-Ink display in the appropriate format. The manufacturer of the display explains in his wiki how to prepare the images and how to upload them on the micro SD card.

The weather icon codes returned by openweathermap are listed in this table. To display the appropriate image the codes only need to be translated.

The result can look like this:

e-ink weather display

e-ink weather display

In the end this weather display hack is a prototype which can be easily extended.

Links

http://openweathermap.org/api

http://openweathermap.org/weather-conditions

https://github.com/bblanchon/ArduinoJson

https://thenounproject.com/

http://www.waveshare.com/4.3inch-e-paper.htm

http://www.waveshare.com/wiki/4.3inch_e-Paper

Advertisements

Network Time synchronized Clock with E-Ink Display

In my previus post I connected an E-Ink display to an Arduino Uno.
Since it is possible to drive the E-Ink display with 3,3 V logic it is easily connected to an ESP8266 module. This time I used an Adafruit Huzzah ESP8266 breakout.

The very first test of this module is a typical example: a simple digital clock. Every minute an NTP packet will be requested by a time server. A well known one is time.nist.gov, but some WiFi routers run time servers too. The time returned will be in UTC format.
The UTC time will be adjusted to the local time using the TimeZone library. This way it is easily possible to define daylight saving time rules for the different time zones.

Components used

Adafruit Huzzah ESP8266 breakout
Waveshare 4,3″inch e-Paper 800×600
some cables
FTDI programmer

Wiring

e-Ink Display Adafruit Huzzah
VCC (red) 3,3 V
GND (black) GND
DOUT (white) RX
DIN (green) TX
WAKE_UP (yellow) 12
RST (blue) 14

Software

Two small changes are required in the epd library’s cpp file to use different pins on the Adafruit Huzzah for the reset and wake up lines of the E-Ink display:

//const int wake_up = 2;// Arduino Uno/Pro Mini
//const int reset = 3; // Arduino Uno/Pro Mini
const int wake_up = 12; // Adafruit huzzah
const int reset = 14; // Adafruit huzzah

The sketch is based on the example showing time synchronization with an NTP time source from Arduino’s Time library.

To upload the sketch to the Adafruit Huzzah hold the GPIO0 button, push the reset button, release the reset button and at last the GPIO0 button. This procedure brings the Adafruit Huzzah into bootloader mode which allows to upload sketches using the Arduino IDE.

/*
 * Simple digital clock with e-Ink display.
 * The time in UTC format is adjusted to local time
 * with the TimeZone library.
 */

#include <ESP8266WiFi.h> // https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi
#include <WiFiUdp.h> // https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi
#include <epd.h> // demo in http://www.waveshare.com/wiki/File:4.3inch-e-Paper-Code.7z
#include <TimeLib.h> // https://github.com/PaulStoffregen/Time
#include <Timezone.h> // https://github.com/JChristensen/Timezone

boolean debug = false;
char ssid[] = "SSID";
char pass[] = "WIFIPASSWORD";

unsigned int localPort = 8989; // local port to listen for UDP packets

const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets

// UDP instance to send and receive packets via UDP
WiFiUDP udp;

// Don't hardwire the IP address or we won't get the benefits of the pool.
// Lookup the IP address for the host name instead.
IPAddress timeServerIP; // NTP server address
const char* ntpServerName = "time.nist.gov"; // or any other NTP server

// Central European Time (Paris, Berlin)
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time
Timezone CE(CEST, CET);
TimeChangeRule *tcr; // pointer to the time change rule, used to get the TZ abbrev

time_t prevDisplay = 0; // last time the digital clock was displayed

// function declarations
void setMediumText(String text, int x, int y);
void setLargeText(String text, int x, int y);
unsigned long sendNTPpacket(IPAddress& address);
time_t getNtpTime();
String digits(int number);
String getTime(time_t dt);
String getDate(time_t dt);

void setup() {
  // initialize e-ink display
  epd_init();
  epd_wakeup();
  epd_set_memory(MEM_NAND);
  epd_set_color(BLACK, WHITE);

  if( debug ) {
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);
  }
  // connect to the WiFi network
  WiFi.begin(ssid, pass);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    if( debug ) Serial.print(".");
  }
  if( debug ) {
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    Serial.println("Starting UDP");
  }
  udp.begin(localPort);

  if( debug ) {
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    Serial.println("waiting for sync");
  }
 
  setSyncProvider(getNtpTime);
  setSyncInterval(60); // set the number of seconds between re-sync
} //setup()

void loop() {
  if (timeStatus() != timeNotSet) {
    // update the display only if time has changed
    if (now() != prevDisplay) {
      epd_clear();
      prevDisplay = now();
      // adjust daylight saving time
      time_t dt = CE.toLocal(prevDisplay, &tcr);
      // print time
      setLargeText(getTime(dt), 315, 100 );
      // print date
      setMediumText(getDate(dt), 270, 250 );
      epd_udpate();
      delay(60000); // wait a minute
    }
  }
} // loop()

//-------- NTP code ----------
// send an NTP request to the time server at the given address
unsigned long sendNTPpacket(IPAddress& address) {
  if( debug ) Serial.println("Sending NTP packet...");
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011; // LI, Version, Mode
  packetBuffer[1] = 0; // Stratum, or type of clock
  packetBuffer[2] = 6; // Polling Interval
  packetBuffer[3] = 0xEC; // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  udp.beginPacket(address, 123); //NTP requests are to port 123
  udp.write(packetBuffer, NTP_PACKET_SIZE);
  udp.endPacket();
} // sendNTPpacket()

time_t getNtpTime() {
  IPAddress ntpServerIP; // NTP server's ip address

  while (udp.parsePacket() > 0) ; // discard any previously received packets
    if( debug ) Serial.println("Transmit NTP Request");
    // get a random server from the pool
    WiFi.hostByName(ntpServerName, ntpServerIP);
    if( debug ) {
      Serial.print(ntpServerName);
      Serial.print(": ");
      Serial.println(ntpServerIP);
  }
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 = (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - (2208988800UL+0 * SECS_PER_HOUR);
    }
  }
  if( debug ) Serial.println("No NTP Response");
  return 0; // return 0 if unable to get the time
} // getNtpTime()

//-------- e-ink display code ----------
void setMediumText(String text, int x, int y) {
  epd_set_ch_font(GBK48);
  epd_set_en_font(ASCII48);
  epd_disp_string(text.c_str(), x, y);
} // setMediumText()

void setLargeText(String text, int x, int y) {
  epd_set_ch_font(GBK64);
  epd_set_en_font(ASCII64);
  epd_disp_string(text.c_str(), x, y);
} // setLargeText()

//-------- time+date display code ----------
String digits(int number) {
  // utility for digital clock display: leading 0
  String nr = String("");
  if (number < 10) {
    if( debug ) Serial.print('0');
    nr = "0";
  }
  if( debug ) Serial.print(number);
  nr += String(number);
  return nr;
} // digits()

String getTime(time_t dt) {
  String time_clock = String("");
  time_clock = digits(hour(dt));
  time_clock += " : ";
  time_clock += digits(minute(dt));
  //time_clock += " : ";
  //time_clock += digits(second(dt));
  return time_clock;
} // getTime()

String getDate(time_t dt) {
  String date_clock = String("");
  date_clock = digits(day(dt));
  date_clock += ".";
  date_clock += digits(month(dt));
  date_clock += ".";
  date_clock += year();
  return date_clock;
} // getDate()

Result

The final result of this test looks like this:

simple digital clock on e-ink display

Sources

https://learn.adafruit.com/adafruit-huzzah-esp8266-breakout/overview
http://www.waveshare.com/wiki/4.3inch_e-Paper
https://github.com/PaulStoffregen/Time
https://github.com/JChristensen/Timezone