Audio Book: Raspberry Pi Zero Internet Radio with pHAT BEAT

A couple of days ago I laid my hands on a pHAT BEAT and two small speakers. Together with a Raspberry Pi Zero (and an Internet connection of course) this makes building an internet radio easily possible. And yes, inspiration for this project was also the Pirate Radio Kit.
The pHAT BEAT comes along with stereo output, an amplifier, a couple of buttons for adjusting the volume, playing/pause, forward/backward and powering off and a number of bright and shiny LEDs. Just the perfect audio hardware component for an internet radio.

Hardware

Raspberry Pi Zero with Micro SD card and up-to-date OS
USB WiFi stick (not needed if a Raspberry Pi Zero W is used)
pHAT BEAT
2
small speakers
some cables
USB power supply

Assemble the hardware as required. This implies some soldering for the headers of the Raspberry Pi Zero and the pHAT BEAT as well as the connections to the speakers. This tutorial is a good guideline to see what to do.

Software

Once the Raspberry Pi Zero is accessible headless in the local WLAN network (see this blog post for setup instructions) install the pHAT BEAT Python library.

Luckily the software for an internet radio project already exists. The setup is really made simple by running the setup script only. The setup script installs the required software and adjusts the whole configuration on the Raspberry Pi Zero. See https://github.com/pimoroni/phat-beat/tree/master/projects/vlc-radio for further reference.

Once the installation is complete, reboot. After reboot the internet radio will be automatically started and will play some example music.

The pHAT BEAT’s buttons directly work with the example projects software. Adjusting the volume or switching between different items on a configurable playlist (see configuration below) is directly possible. Even the off button immediately works: it turns off the radio and fully shuts down the Raspberry Pi Zero.

Configuration

Configure Internet Radio Streams

Collect the URLs of your favourite internet radio streams. Create the file /home/pi/.config/vlc/playlist.m3u . Insert the URLs into the playlist as in this example:

Example playlist.m3u
#EXTM3U
#EXTINF:0,station1
#EXTVLCOPT:network-caching=1000
http://station1.net/.../...
#EXTINF:0,station2
http://station2.com/.../.../mp3/...
#EXTINF:0,station3
http://station3.something/...

Alternatively create a playlist containing the radio stream URLs of your choice in VLC and save the playlist to a file. This file can be copied to the Raspberry Pi Zero to /home/pi/.config/vlc/playlist.m3u.

After reboot the forward/backward buttons of the pHAT BEAT can be used to switch between the different internet radio streams.

Wrapping: The Result

The wrapping was simple in this case: an old book became a nice „audio book“! Similar to my ‚book book shelves‘ an old book is hollowed inside with a sharp knife so the hardware fits in.
Surprisingly well is the sound of the speakers inside the book!
All I need now is to find a way to operate the small buttons of the pHAT BEAT…

Info & Links

https://github.com/pimoroni/phat-beat

 

NeoPixels Strip on Raspberry Pi Zero

Looking into my desk’s drawer I found the remainder of an Adafruit NeoPixel strip I used in another project. And an unused, last years Raspberry Pi Zero. Does that work together? Well, yes, it does! At least after fiddling a bit with hard- and software and circumventing some common traps.

Searching the web I found a tutorial for steering a NeoPixel strip with a first generation Raspberry Pi. Technically it should work with an exemplary of a more recent version, but it did not initially.
Here is the description of how it all worked out in the end:

Hardware

Raspberry Pi Zero with up-to-date Raspbian Jessie Pixel
Mini USB WiFi Adapter (if the brand new Raspberry Pi Zero W is not used)
Raspberry Pi Zero adapter cables + power supply
Adafruit NeoPixel strip
1000 μF capacitor
330 Ω resistor
1N4001 diode
5 V breadboard power supply
breadbord, cables

Connections

  • 5V power supply GND : 1000 μF capacitor (short leg)
  • 5V power supply 5V : 1000 μF capacitor
  • 5V power supply GND : NeoPixel strip GND
  • 5V power supply 5V : NeoPixel strip 5V via 1N4001 diode (side with stripe goes to 5V input of the strip)
  • 5V power supply GND : Raspberry Pi Zero GND (physical pin 6)
  • Raspberry Pi Zero (physical pin 12) : NeoPixel strip data line via 330 Ω resistor

The available pins of the Raspberry Pi Zero are listed here. GPIO #1 correlates to physical pin 12 which is BCM #18. The latter is used in the Python software.

Don’t!

It is not recommended to use the 5V output of the Raspberry Pi Zero directly to power the NeoPixel strip. The pixels might draw too much current and might therefore damage the pin. It would have been way too convenient…so: an additional 5V power supply is strongly recommended.

Software

Running Headless: Setting up WiFi

To run the Raspberry Pi Zero headless (without display), set up the WiFi connection. For this step an HDMI display and a keyboard is required. Open the file

sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

Put the network configuration at the end of the file:

network={
ssid="WiFi network name"
psk="password"
}

Use raspi-config to allow SSH connections and to adjust the Pi’s hostname, the password, the time settings etc. .

SSH to Zero

When attaching the Mini USB WiFi Adapter instead of the keyboard and rebooting the Raspberry Pi Zero the desired WiFi network is used and it is possible to SSH to the Pi Zero. To find the IP adress in the local network check which devices are logged into the network at your routers access point. Or kindly ask your network admin to check. 😉

GPIO Checks

To see the available GPIO pins on the Raspberry Pi Zero run

gpio readall

NeoPixel Python Library

To set up the Python library for driving NeoPixels on a Raspberry follow this tutorial. Jeremy Garff’s Python library for NeoPixels is working like a charm.

Disabling Audio

To be able to use the PWM pins as data pins for the NeoPixel strip I disabled audio by commenting the line

# Enable audio (loads snd_bcm2835)
#dtparam=audio=on  # disable audio for PWM pin usage

in the file /boot/config.txt.

Whether audio is disabled can be checked using

aplay -l

. If audio is disabled properly the result is an error message („aplay: device_list:268: no soundcards found…“).

Examples

Once the NeoPixel library is set up and the hardware is connected properly run strandtest.py or any other example code from the rpi_ws281x/python/examples section.

Result

That’s it! The NeoPixel strip finally can be driven by a Raspberry Pi Zero.

neopixeltest

While this example is working I definitively have a new project in mind…

Links

https://learn.adafruit.com/neopixels-on-raspberry-pi/overview

https://github.com/jgarff/rpi_ws281x

Solar powered outdoor weather sensor

To be independent of a plug socket for an outdoor weather sensor solar power looks a lot more promising than wind or water for power generation. 😉 Especially since simple solar panels for tinkering are available for little money in the meantime. Only shipping from China usually takes a couple of weeks.

Earlier this year I launched my personal outdoor weather sensor to collect weather data such as temperature, humidity and air pressure. So far this required a plug socket and a 5V power supply. For the future I plan to use a solar panel to load a rechargeable battery which will alternatively power the outdoor weather sensor. Well, at least, when outside temperatures allow. During wintertime the cold temperatures might shorten the life of the rechargeable battery. Therefore, in the cold season the 5V power supply can be used alternatively.

Components used

Outdoor weather sensor
5V solar panel
Lithium battery 3,7 V, 2500 mAh
Lithium battery charger
Step up power supply

Wiring

Starting from the solar panel the wires go to the input of the charger module. The charger is connected to the battery and the step up power supply. The output lines of the step up power supply are soldered to the outdoor weather sensor. The battery should be removable to switch easily to a conventional 5V power supply.

The following sketch roughly illustrates the setup:

sketch

Adjusting the Step Up power supply

The desired output voltage of the step up power supply is 5V in this case. It can be adjusted by turning the small screw while the outgoing lines of the charger modules are connected to a multimeter measuring the voltage.

Notes

To fully charge the rechargeable battery it should be very sunny! Charging takes a couple of hours depending on the size of the battery. Since it is a lithium battery the memory effect known from NiMh or NiCd rechargeable batteries can be neglected.

As soon as the outdoor weather sensor is powered by an electric 5V power supply the battery should be removed! (Mentioned just in case.)

Result

Summers sunlight is optimal for charging a lithium battery and driving the outdoor weather sensor. This works in parallel. Since the outdoor weather sensor will spend most of the time in deep sleep mode the battery should last a while! So far I did not measure how mich power is drawn by awaking the outdoor weather sensor from deep sleep, doing the measurements, sending the measured values via WiFi to the server and going back to sleep. Perhaps later…

Well, now it is time to find a weather-proof box for all of this!

 

 

ESP8266: Switch WiFi Connections using RFID

It took me some time to find an example for using RFID (Radio Frequency IDentification) on an Arduino. RFID is commonly used for identification, tracking, etc. . RFID senders (tags) are so small that they can be implanted. I heard rumours that some humans already started to wear RFID senders under their skin! Brave new world… Imagine to turn on the coffee machine contactless by pointing at it with the index finger. Such a thought gives me the creeps.

However, the example I describe here switches the WiFi connection only depending on the RFID tag discovered. Easily expandable for different purposes!

Components used

Adafruit Feather Huzzah
Monochrome OLED display
RFID-RC522 receiver + matching RFID tags

Wiring

Adafruit Feather Huzzah RFID-RC522 OLED Display
3,3 V 3,3 V  3,3 V
GND GND GND
SDA (4) SDA
SCL (5) SCL
2 (Arduino Uno: 10) SDA
SCK/14 (Arduino Uno: 11) SCK
MO/13  MOSI
MI/12 MISO
16 * (Arduino Uno: 9)  RST

For SDA and Reset two free IO pins should be used on the ESP8266 module. These two pins will be initialized during setup.

The wiring of the RFID receiver to the ESP8266 module is described here as well, including a wiring diagram.

Software

The RFID Arduino library https://github.com/miguelbalboa/rfid can be used. The library can be downloaded from the github repository. The download folder rfid-master should be renamed to rfidmaster and should be copied to the Arduino IDE’s library directory.

During compilation the rfid library issues a warning that it is incompatible with STM32F1 architecture. But this seems to have no influence, the software is working in the end.

Sample Sketch

In this example the WiFi connection will be switched depending on the RFID tag that was recognized. The tags that can be used are hard coded.

#include <ESP8266WiFi.h>  // http://esp8266.github.io/Arduino/versions/2.0.0/doc/libraries.html
#include <WiFiClient.h>   // https://www.arduino.cc/en/Reference/WiFiClient

#include <SPI.h>
#include <MFRC522.h>

// OLED ESP_ssd1306_128x64_I2C
#include <ESP_SSD1306.h>    // Modification of Adafruit_SSD1306 for ESP8266 compatibility
#include <Adafruit_GFX.h>   // Needs a little change in original Adafruit library (See README.txt file)
#include <SPI.h>            // For SPI comm (needed for not getting compile error)
#include <Wire.h>           // For I2C comm, but needed for not getting compile error

boolean debug=true;

// Arduino Uno
//#define SS_PIN 10 // SDA an Pin 10
//#define RST_PIN 9 // RST an Pin 9

// ESP8266 (Adafruit Feather Huzzah)
#define RST_PIN 15 // RST-PIN for RC522 - RFID - SPI - Modul GPIO15
#define SS_PIN  2  // SDA-PIN for RC522 - RFID - SPI - Modul GPIO2 

MFRC522 mfrc522(SS_PIN, RST_PIN); // RFID-Empfänger benennen

const unsigned long BAUD_RATE = 115200;  // serial connection speed
const unsigned long HTTP_TIMEOUT = 10000;   // max respone time from server

void initSerial();
void connectWiFi(const char* ssid, const char* password);
void disconnectWifi();
WiFiClient client;

const char* ssid1 = "SSID1";
const char* password1 = "PASSWORD1";
const char* ssid2 = "SSID2";
const char* password2 = "PASSWORD2";

const char* RFIDTAG1 = "IDe7b5f653";
const char* RFIDTAG2 = "ID2e6432";

#define OLED_RESET  16  // Pin 16 -RESET digital signal
ESP_SSD1306 display(OLED_RESET); // FOR I2C

void initOLEDDisplay();
void showConnection(const char* ssid);

void setup() {

  initSerial();
  if( debug ) Serial.println("setup");

  initOLEDDisplay();

  // start SPI connection
  // initialize RFID receiver
  SPI.begin();
  mfrc522.PCD_Init();
}

void loop() {

  // RFID-TAG is close
  if ( ! mfrc522.PICC_IsNewCardPresent() ) {
    return; // gehe weiter...
  }

  // RFID-TAG was detected
  if ( ! mfrc522.PICC_ReadCardSerial()) {
    return; // gehe weiter...
  }

  String tag = "ID";
  for (byte i = 0; i < mfrc522.uid.size; i++) {
    // UID of RFID-TAG is read, consists of 4 single blocks
    tag += String(mfrc522.uid.uidByte[i], HEX);
  }
  if( debug ) {
    Serial.print("ID of RFID-TAG: ");
    Serial.print(tag.c_str());
    Serial.println();
  }

  // decide what to do for a certain tag
  if( tag == RFIDTAG1 ) {
    disconnectWifi();
    connectWiFi(ssid1, password1);
  }
  if( tag == RFIDTAG2 ) {
    disconnectWifi();
    connectWiFi(ssid2, password2);
  }
}

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

void initOLEDDisplay() {
  // SSD1306 Init
  display.begin(SSD1306_SWITCHCAPVCC);  // Switch OLED
  display.clearDisplay();
  // Show image buffer on the display hardware.
  // Since the buffer is intialized with an Adafruit splashscreen
  // internally, this will display the splashscreen.
  display.display();
  delay(2000);

  // Clear the buffer.
  display.clearDisplay();
}

// attempt to connect to WiFi
void connectWiFi(const char* ssid, const char* password) {
  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());
  }
  if( WiFi.status() == WL_CONNECTED ) {
    showConnection(ssid);
  } else {
    showConnection("-");
  }
}

void disconnectWifi() {
  WiFi.disconnect();
}

void showConnection(const char* ssid) {
  display.clearDisplay();

  display.setTextSize(2);
  display.setTextColor(WHITE);

  display.setCursor(0, 0);
  display.println("SSID");

  display.setCursor(0, 15);
  display.println(ssid);

  display.display();
}

This is how the test setup looks like:
RFID switch

ESP8266: Uploading Weather Data to openweathermap.org

A couple of months ago I designed a small weather station to measure weather data such as temperature,  humidity and barometric pressure using an ESP8266 module and a couple of sensors. How this outdoor weather sensor is constructed is described in an older blog post.

openweathermap.org allows to connect a home made weather station to its network of weather stations around the world (currently >40.000). This may help to improve the data which is provided by openweathermap.org.

Uploading measured weather data is easily accomplished by performing an HTTP POST to http://openweathermap.org/data/post using basic authentication.

This manual describes how to upload weather data. Which weather details may be submitted in the post is illustrated in the table of the manual.

Prerequisites

An account for openweathermap.org is required. The username and password must be translatedinto BASE64 encoding. This online tool helps to transfer username:password for openweathermap.org into the required format.

The location of the weather station is required as GPS coordinates. The latitude and the longitude of the location of the weather station can be determined with google maps for example. All it needs is to click on the location on the map. The coordinates will be displayed in a small window below the adress.

Components used

Outdoor weather sensor

Source Code

The Arduino sketch for the outdoor weather sensor may be enhanced. For brevity I concentrate in this example on the additional functionality required to upload the weather data, not on their measurement.

#include <ESP8266WiFi.h>  // http://esp8266.github.io/Arduino/versions/2.0.0/doc/libraries.html
#include <WiFiClient.h>   // https://www.arduino.cc/en/Reference/WiFiClient

boolean debug=true;
// WiFi connection data
const char* ssid = "SSID";
const char* password = "PASSWORD";

const char* server = "openweathermap.org";
const int serverPort = 80;

const unsigned long BAUD_RATE = 115200; // serial connection speed
const unsigned long HTTP_TIMEOUT = 10000;   // max respone time from server

void initSerial();
void connectWiFi();
bool connect(const char* hostName, const int port);
bool sendPost(const char* hostName, float temperature, float humidity, float pressure);
void displayResponse();
void disconnect();
WiFiClient client;

unsigned long previousMillis = 0;
#define INTERVAL_MS 60000

#define CredentialsBase64 "sOmECRYPticStRiNggg" // enter here the BASE64 encoded credentials in the form &amp;amp;amp;lt;username&amp;amp;amp;gt;:&amp;amp;amp;lt;password&amp;amp;amp;gt;
// https://www.base64encode.org/enc/credential/
const String stationName = "MyOwnWeatherStation"; // enter the station name (it will be displayed on openweathermap.org)
// use coordinates from google maps
const String lat = "xx.xxxxx"; // latitude
const String lng = "yy.yyyyy"; // longitude
const String alt = "5"; // altitude of the location in meters without decimals

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

void loop() {
  unsigned long currentMillis = millis();
  // run every minute
  if (currentMillis - previousMillis >= INTERVAL_MS) {
    previousMillis = currentMillis;
    if( debug ) Serial.println("loop: measure weather data");
    float temperature=14.6;
    float humidity=89.1;
    float pressure=1004.6;
    // TODO use measured data from sensors!

    if( connect(server, serverPort) ) {
      if( sendPost(server, temperature, humidity, pressure) ) {
        displayResponse();
      }
    }
    disconnect();
  }
}

// send the HTTP POST request to the server
bool sendPost(const char* hostName, float temperature, float humidity, float pressure) {
  if( debug ) {
    Serial.print("POST weather data to");
    Serial.println(hostName);
    Serial.print("t = ");
    Serial.println(temperature);
    Serial.print("h = ");
    Serial.println(humidity);
    Serial.print("p = ");
    Serial.println(pressure);
  }

  // construct packet
  String packet = "";
  packet += "temp=";
  packet += (int)temperature;
  packet += "&humidity=";
  packet += (int)humidity;
  packet += "&pressure=";
  packet += (int)pressure;
  packet += "&lat=";
  packet += lat;
  packet += "&long=";
  packet += lng;
  packet += "&alt=";
  packet += alt;
  packet += "&name=";
  packet += stationName;

  // construct POST request
  String cmd = "POST /data/post HTTP/1.1\n";
  cmd += "Host: ";
  cmd += hostName;
  cmd += "\n";
  cmd += "Content-Type: application/x-www-form-urlencoded\n";
  cmd += "Authorization: Basic ";
  cmd += CredentialsBase64;
  cmd += "\n";
  cmd += "Content-Length: ";
  cmd += packet.length();
  cmd += "\n";
  cmd += "Connection: close\n\n";
  cmd += packet;
  cmd += "\r\n\r\n";

  if( debug ) {
    Serial.print("packet: ");
    Serial.println(packet);
    Serial.print("cmd: ");
    Serial.println(cmd);
  }
  client.println(cmd);
  return true;
}

void displayResponse() {
  client.setTimeout(HTTP_TIMEOUT);
  char reply[400];
  size_t length = client.readBytes(reply, 400);
  reply[length] = 0;
  String replyString = String(reply);

  if( debug ) {
    Serial.print("HTTP response ");
    Serial.println(replyString.c_str());
  }
}

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

// attempt to connect to WiFi
void connectWiFi() {
  WiFi.mode(WIFI_STA);
  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());
  }
}

// open connection to the HTTP server
bool connect(const char* hostName, const int port) {
  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;
}

// close the connection with the HTTP server
void disconnect() {
  if( debug ) Serial.println("Disconnect from HTTP server");
  client.stop();
}

Notes

The HTTP POST to upload weather data boils down to

POST /data/post HTTP/1.1
Host: openweathermap.org
Content-Type: application/x-www-form-urlencoded
Authorization: Basic sOmECRYPticStRiNggg
Content-Length: 80
Connection: close

temp=20&humidity=71&pressure=1011&lat=49.11&long=24.11&alt=200&name=StationName

The content length is the length of the string containing the measured weather data, the coordinates etc. . More weather parameters may be added – everything that is measurable can be interesting for the upload.

How to find the own Weather Station

Finding the data from the own weather station after the upload was a bit tricky. It looks like openweathermap.org is working on improvements for uploading weather data as I conclude from this support answer. At least the documentation for upploading data needs improvements.

The station ID is returned in the HTTP response to the post. This is easy to miss. If the upload of weather data was successful the response should look similar to this:

HTTP response header HTTP/1.1 200 OK
Server: nginx/1.6.2
Date: Sat, 20 Aug 2016 11:37:00 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: close
X-Powered-By: Fat-Free Framework (http://fatfree.sourceforge.net)
Pragma: no-cache
Cache-Control: no-cache, must-revalidate

{"message":"","cod":"200","id":987654321}

The ID can be used to observe the station using the URL http://openweathermap.org/station/987654321 .

To retrieve the weather data from this station in JSON format an HTTP GET request can be performed using this URL in a browser:

http://api.openweathermap.org/data/2.5/station?id=987654321&APPID=<YOURAPPID&gt; .

With some delay the uploaded data becomes visible in JSON format:

{
"station":
{"name":"MyOwnWeatherStation",
"type":5,"status":20,"user_id":0,"id":987654321,
"coord":{"lon":y.yyyy,"lat":xx.xxxx}},
"last":{"main":{"temp":290.15,"humidity":69,"pressure":1030},
"dt":1471791637},
"params":["temp","pressure","humidity"]
}

Inspirational Links

https://github.com/Benjamin3992/OpenWeatherDuino

http://openweathermap.org/stations

ESP8266: Get Date and Time from HTTP Header

Instead of sending NTP packets to remote time server as in a previous blog post the date and the time can also be extracted from any HTTP header returned after an HTTP GET request. Usually the HTTP header contains the date and the time from the router. This way the local network must not be left necessarily. For illustration purposes only a well-known URL is used in this example.

Components used

Adafruit Feather Huzzah
Monochrome OLED display

Wiring

Adafruit Feather Huzzah OLED Display
3,3 V 3,3 V
GND GND
SDA (4)  SDA
SCL (5)  SCL

Software

This Arduino sketch shows

  • how to connect to the WiFi network
  • how to perform an HTTP GET request on an URL (could be an IP address as well)
  • how to extract date and time from the HTTP response header
  • how to display date and time on an OLED display .

Code

#include <EEPROM.h>
#include <ESP8266WiFi.h>    // http://esp8266.github.io/Arduino/versions/2.0.0/doc/libraries.html
#include <WiFiClient.h>     // https://www.arduino.cc/en/Reference/WiFiClient

// OLED ESP_ssd1306_128x64_I2C
#include <ESP_SSD1306.h>    // Modification of Adafruit_SSD1306 for ESP8266 compatibility
#include <Adafruit_GFX.h>   // Needs a little change in original Adafruit library (See README.txt file)
#include <SPI.h>            // For SPI comm (needed for not getting compile error)
#include <Wire.h>           // For I2C comm, but needed for not getting compile error

boolean debug=true;

// WiFi connection data
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

const char* server = "www.google.de"; 	// an example URL
const int serverPort = 80;		// a port number
const char* resource = "/";       	// http resource

const unsigned long BAUD_RATE = 115200;	// serial connection speed
const unsigned long HTTP_TIMEOUT = 10000;   // max respone time from server

void initSerial();
void connectWiFi();
bool connect(const char* hostName, const int port);
void disconnect();
bool sendRequest(const char* host, const char* resource);
WiFiClient client;

#define OLED_RESET  16  // Pin 16 -RESET digital signal
ESP_SSD1306 display(OLED_RESET); // FOR I2C

void initOLEDDisplay();
void showDateAndTime();

bool findDateAndTimeInResponseHeaders();
String extractDayFromDateTimeString(String dateTime);
String extractMonthFromDateTimeString(String dateTime);
String extractYearFromDateTimeString(String dateTime);
String extractHourFromDateTimeString(String dateTime);
String extractMinuteFromDateTimeString(String dateTime);
String translateMonth(String monthStr);
String dateAndTime; // stores date and time from HTTP response header

unsigned long previousMillis = 0;
#define INTERVAL_MS 10000

void setup() {
  if( debug ) Serial.println("setup");
  initSerial();
  initOLEDDisplay();
  connectWiFi();
} // setup()

void loop() {

  unsigned long currentMillis = millis();
  // run every 10 seconds
  if (currentMillis - previousMillis >= INTERVAL_MS) {
    previousMillis = currentMillis;
	if( debug ) Serial.println("loop: get date and time");

	  if( connect(server, serverPort) ) {
		if( sendRequest(server, resource) ) {
		  if( findDateAndTimeInResponseHeaders() ) {
			if( debug ) {
			  Serial.print( "Date and Time from HTTP response header: " );
			  Serial.println( dateAndTime.c_str() );
			}
			showDateAndTime();
		  }
		}
		disconnect();
	  }
  }
} // loop()

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

void initOLEDDisplay() {
  // SSD1306 Init
  display.begin(SSD1306_SWITCHCAPVCC);  // Switch OLED
  display.clearDisplay();
  // Show image buffer on the display hardware.
  // Since the buffer is intialized with an Adafruit splashscreen
  // internally, this will display the splashscreen.
  display.display();
  delay(2000);

  // Clear the buffer.
  display.clearDisplay();
}

// attempt to connect to WiFi
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());
  }
}

// Open connection to the HTTP server
bool connect(const char* hostName, const int port) {
  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;
}
/* example  curl -vv http://www.google.de
* About to connect() to www.google.de port 80 (#0)
*   Trying 172.217.17.227...
* Connected to www.google.de (172.217.17.227) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: www.google.de
> Accept: * / *
*/
/* example response:
< HTTP/1.1 200 OK
< Date: Sun, 29 May 2016 10:00:14 GMT */ // Send the HTTP GET request to the server 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(host);     client.println("Accept: */*");     client.println("Connection: close");     client.println();     return true;  }  bool findDateAndTimeInResponseHeaders() {     // date and time string starts with Date: and ends with GMT     // example: Date: Sun, 29 May 2016 10:00:14 GMT     client.setTimeout(HTTP_TIMEOUT);     char header[85];     size_t length = client.readBytes(header, 85);     header[length] = 0;     String headerString = String(header);     int ds = headerString.indexOf("Date: ");     int de = headerString.indexOf("GMT");     dateAndTime = headerString.substring(ds+6, de);     // date and time: Sun, 29 May 2016 10:00:14     if( debug ) {       Serial.print("HTTP response header ");       Serial.println(headerString.c_str());       Serial.print("index start date ");       Serial.println(ds);       Serial.print("index end time ");       Serial.println(de);       Serial.println(  );       Serial.print("date and time: ");       Serial.println(dateAndTime.c_str());     }    return dateAndTime.length()&amp;gt;15;
}

// Close the connection with the HTTP server
void disconnect() {
  if( debug ) Serial.println("Disconnect from HTTP server");
  client.stop();
}

//-------- time+date code ----------
// example: Sun, 29 May 2016 10:00:14
String extractDayFromDateTimeString(String dateTime) {
  uint8_t firstSpace = dateTime.indexOf(' ');
  String dayStr = dateTime.substring(firstSpace+1, firstSpace+3);
  if( debug ) {
    Serial.print("Day: ");
    Serial.println(dayStr.c_str());
  }
  return dayStr;
}

String extractMonthFromDateTimeString(String dateTime) {
  uint8_t firstSpace = dateTime.indexOf(' ', 7);
  String monthStr = dateTime.substring(firstSpace+1, firstSpace+4);
  if( debug ) {
    Serial.print("Month: ");
    Serial.println(monthStr.c_str());
  }
  return monthStr;
}

String extractYearFromDateTimeString(String dateTime) {
  uint8_t firstSpace = dateTime.indexOf(' ', 10);
  String yearStr = dateTime.substring(firstSpace+1, firstSpace+5);
  if( debug ) {
    Serial.print("Year: ");
    Serial.println(yearStr.c_str());
  }
  return yearStr;
}

String extractHourFromDateTimeString(String dateTime) {
  uint8_t firstColon = dateTime.indexOf(':');
  String hourStr = dateTime.substring(firstColon, firstColon-2);
  if( debug ) {
    Serial.print("Hour (GMT): ");
    Serial.println(hourStr.c_str());
  }
  // adjust GMT time
  int h = hourStr.toInt();
  h += 2; // summertime
  //h += 1; // wintertime
  if( debug ) {
    Serial.print("Hour (adjusted for summertime): ");
    Serial.println(h);
  }
  return String(h);
}

String extractMinuteFromDateTimeString(String dateTime) {
  uint8_t secondColon = dateTime.lastIndexOf(':');
  String minuteStr = dateTime.substring(secondColon, secondColon-2);
  if( debug ) {
    Serial.print("Minute: ");
    Serial.println(minuteStr.c_str());
  }
  return minuteStr;
}

String extractDayFromCalendarDate(String date) {
  String dateStr = String(date);
  uint8_t firstDot = dateStr.indexOf('.');
  String dayStr = dateStr.substring(1, firstDot);
  if( debug ) {
    Serial.print("Day: ");
    Serial.println(dayStr.c_str());
  }
  return dayStr;
}

String translateMonth(String monthStr) {
  if(monthStr.equals(String("Jan"))) return String("01");
  if(monthStr.equals(String("Feb"))) return String("02");
  if(monthStr.equals(String("Mar"))) return String("03");
  if(monthStr.equals(String("Apr"))) return String("04");
  if(monthStr.equals(String("May"))) return String("05");
  if(monthStr.equals(String("Jun"))) return String("06");
  if(monthStr.equals(String("Jul"))) return String("07");
  if(monthStr.equals(String("Aug"))) return String("08");
  if(monthStr.equals(String("Sep"))) return String("09");
  if(monthStr.equals(String("Oct"))) return String("10");
  if(monthStr.equals(String("Nov"))) return String("11");
  if(monthStr.equals(String("Dec"))) return String("12");
}

void showDateAndTime() {
  display.clearDisplay();

  display.setTextSize(2);
  display.setTextColor(WHITE);
  display.setCursor(0, 0);
  String date = extractDayFromDateTimeString(dateAndTime);
  date += ".";
  date += translateMonth(extractMonthFromDateTimeString(dateAndTime));
  date += ".";
  date += extractYearFromDateTimeString(dateAndTime);
  display.println(date.c_str());
  String timeStr = extractHourFromDateTimeString(dateAndTime);
  timeStr += ":";
  timeStr += extractMinuteFromDateTimeString(dateAndTime);
  display.setCursor(30, 35);
  display.println(timeStr.c_str());

  display.display();
}

Results

oled_blog
Date and time extracted from HTTP response header on OLED display

REST asleep – how to construct an HTTP server in Java

REST? What? An HTTP server to „communicate“ with via URLs? Yes, why not. In certain use cases a valid solution for various different tasks. Especially for the idea of displaying more or less ’static‘ information on a display.

In my case such a server handles tasks such as

  • fetching the next x entries in a (google) calendar
  • retrieving and preprocessing actual weather data from openweathermap
  • retrieving and preprocessing the weather forecast for the upcoming hours/days from openweathermap

Other examples of useful information could be financial data such as

  • exchange rates of different currencies or
  • stock market information.

Interesting could also be the information which dustbin should be standing outside next for garbage disposal in the morning.
Perhaps flight information for frequent flyers, train times (and delays) for rail travellers, even momentary fuel prices in the region (if only they were not changing so quickly as in Germany) is helpul.

For sure there are more ideas which data could be retrieved and prepared by a server for display! But back to the server itself.

HTTP Server Construction

An HTTP server in Java is basically a runnable jar file that is launched on a machine within a (local) network. The required libraries include http, httpclient, httpcore, … .

Functionality

When a GET request is received from a client the appropriate routine is launched. For each ‚digestible‘ URL a different Java class handles the request.
In case of fetching the next x entries from a Google calendar the specific Java class handles the authentication, the retrieval and the processing of the calendar entries into the JSON format. In Java this is more simple and faster than computing directly on an Arduino.

The entry point of the server may be constructed like this:

import com.sun.net.httpserver.HttpServer;
...
public static void main(String[] args) {
	try {
	    HttpServer server = HttpServer.create(new InetSocketAddress(12345), 0);
	    server.createContext("/calendar", new RequestCalendarHandler());
	    server.createContext("/weather", new RequestOWMWeatherHandler());
	    server.createContext("/forecast", new RequestOWMWeatherForecastHandler());
	    server.setExecutor(null); // creates a default executor
	    server.start();
	    System.out.println("Server running on host " + server.getAddress().getHostString());
	} catch (IOException e) {
	    System.err.println(e.getMessage());
	}
}

Openweathermap

For weather data/weather forecast retrieval from openweathermap it is not necessary to reimplement the wheel. An excellent Java library already exists: owm-japis. It can be used in a different Java class to process appropriate HTTP GET requests.

The handler for fetching weather data by openweathermap may look similar to this:

import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import net.aksingh.owmjapis.CurrentWeather;
import net.aksingh.owmjapis.OpenWeatherMap;
import net.aksingh.owmjapis.OpenWeatherMap.Units;
...
static class RequestOWMWeatherHandler implements HttpHandler {
	@Override
	public void handle(HttpExchange http) throws IOException {
		System.out.println("URI received: " + http.getRequestURI().toString());

	String[] request = http.getRequestURI().toString().split("/");
	if( request.length&gt;1 ) {
		// Handle read requests
		if( request[1].equals("weather") ) {
		try{
			OpenWeatherMap owm = new OpenWeatherMap("APIKEY");
			owm.setUnits(Units.METRIC);

			// getting current weather data for the location
			CurrentWeather cwd = owm.currentWeatherByCityCode(CITYCODE_OWM);

			// checking data retrieval was successful or not
			if (cwd.isValid()) {
				DateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy");
				Date now = cwd.getDateTime();
				String dateNow = dateFormatter.format(now);
				DateFormat timeFormatter = new SimpleDateFormat("HH:mm");
				String timeNow = timeFormatter.format(now);

				System.out.println(cwd.getRawResponse());
				System.out.println("Post weather result in JSON format (raw)");
				// wrap parcels of byte size 4096
				int BUFFER_SIZE = 4096;
				http.sendResponseHeaders(200, 0);
				try (BufferedOutputStream out = new BufferedOutputStream(http.getResponseBody())) {
					try (ByteArrayInputStream bis = new ByteArrayInputStream(response.getBytes())) {
						byte [] buffer = new byte [BUFFER_SIZE];
						int count ;
						while ((count = bis.read(buffer)) != -1) {
							out.write(buffer, 0, count);
						}
					}
				}
			}
		} catch (Exception e) {
			System.err.println(e.getMessage());
		}
	}
}

Client to Server

A client requests data from the HTTP server by a GET request. An example URL to fetch the weather data looks like this:
http://localhost:12345/weather
Such an URL consisting of hostname:port can be tested in a browser on the machine where the server is running. Otherwise an IP or hostname must replace localhost.

Server to Clients

As soon as the HTTP server received a request it analyses the URL and launches the appropriate handler. In the example above the weather data will be retrieved from openweathermap (API key and location ID required).

How to read from a google calendar in Java is explained on google’s developer pages. Authentication for the desired calendar must be set up in advance following the manual.

The results can be preprocessed into a desired format to be posted for the client.
A common format is the JSON format. Data in this format can be read and processed further by a client.

Example Client: Arduino / ESP8266

The client I use is an Adafruit Huzzah microcontroller with an ESP8266 WiFi chip.
(Just in case: the hardware setup, the pin changes in the epd library and the code to parse weather data are described in previous blog posts) .
The source code in this blog post illustrates how to act as an HTTP client. To decode a reply in JSON format the library ArduinoJson is used.
Any display can be attached to the microcontroller to show the results from the HTTP GET request to the HTTP server. At the moment I strongly prefer an e-Ink display. 😉

calendar_currentweather

Links

https://developers.google.com/google-apps/calendar/quickstart/java

http://openweathermap.org/api

https://bitbucket.org/akapribot/owm-japis/overview

https://github.com/bblanchon/ArduinoJson

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

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

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

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

 

IoT: Collecting Weather Data

Collecting data from temperature, humidity and barometric sensors with an
Arduino is compelling when observing the weather. The idea is to create an outdoor sensor module. This outdoor module sends the measured data via WiFi to a server which stores the measurement data in a local database.

Components used

ESP-ADC DIL 18 with ESP8266 module
BMP085 Barometric Pressure & Temp Sensor
DHT22 Temperature and Humidity Sensor
220 uF 16V capacitor
Single color LED
220; 4,7k, 10k resistors
wires, switches, breadboard, breadboard power supply, 3,3 V FTDI programmer

Make it happen

Wiring

wiring of esp8266, dht22, bmp085

 

ESP8266 Parts Notes
GND GND DHT22 / GND BMP085 / GND ESP-ADC / LED – via 220 resistor
VCC 3,3 V VCC DHT22 / VCC BMP085 / VCC ESP8266
Reset switch 1 / GPIO_16 Connection to GPIO_16 required for deep sleep mode of ESP8266
GPIO_0 switch 2
GPIO_4 SDA BMP085 On ESP8266 I2C is implemented in software, could be soft-wired to any other GPIO, default for I2C clock is GPIO_4
GPIO_5 SDL BMP085 On ESP8266 I2C is implemented in software, could be soft-wired to any other GPIO, default for I2C data is GPIO_5
GPIO_7 Pin 2 DHT22, pull to 3,3 V VCC via 4,7 k resistor
GPIO_13 LED +
GPIO_16 RST ESP8266
CHPD 3,3 V VCC via 10k resistor

The capacitor is used to stabilize the electric power supply for the two sensors and the ESP8266. This is especially useful when the ESP8266 sets up a WiFi connection and its power consumption increases.

Uploading an Arduino Sketch to the ESP8266

Wiring

  • Adjust the FTDI programmer for 3,3 V logic
  • Connect the FTDI programmer’s RX and TX lines to the ESP8266.
  • Connect the FTDI programmer’s GND line to GND of the breadboard power supply.

Arduino IDE settings

Adjust the upload settings in the Arduino IDE:

Arduino IDE upload settings

Uploading

To upload an Arduino sketch to the ESP8266 module push the two buttons. Release the switch connected to Reset approx. a second before the switch connected to GPIO_0 and trigger the upload in the Arduino IDE. The timing is relevant. It may happen that this procedure has to be repeated several times until the upload succeeds.

After the successful upload of the sketch the output in the Arduino IDE looks similar to this:

...
upload message on success:
setting serial port timeouts to 1000 ms
 espcomm_send_command: receiving 2 bytes of data
 writing flash
............................................................................................................................................................................................................................
starting app without reboot
 espcomm_send_command: sending command header
 espcomm_send_command: sending command payload
 espcomm_send_command: receiving 2 bytes of data
closing bootloader
 flush start
 setting serial port timeouts to 1 ms
 setting serial port timeouts to 1000 ms
 flush complete

Sketch

Installing Adafruit Libraries

The Arduino sketch based on the examples for the unified BMP085 and DHT libraries.
The required libraries for Adafruid BMP085 Unified, Sensor and DHT Unified can be installed for the Arduino IDE via Sketch > Libraries > Manage Libraries… .

Some Code

#include <ESP8266WiFi.h> // http://esp8266.github.io/Arduino/versions/2.0.0/doc/libraries.html
#include <WiFiClient.h>

#include <Adafruit_Sensor.h>
#include <Adafruit_BMP085_U.h> // https://github.com/adafruit/Adafruit_BMP085_Unified

#include <DHT.h>
#include <DHT_U.h> // https://github.com/adafruit/Adafruit_DHT_Unified
#define DHTTYPE DHT22
#define DHTPIN 7

// WiFi network credentials
const char* ssid = "<SSID>";
const char* password = "<PASSWD>";

// IP / port weather station server
#define IPWS "192.168.145.244"
#define PORT 20016

// times for deep sleep mode
unsigned long MIN_1 = 60000000;
unsigned long MIN_15 = 900000000;
unsigned long MIN_30 = 1800000000;
unsigned long S_30 = 30000000;
unsigned long DEEP_SLEEP = MIN_30;
RFMode MODE = WAKE_RF_DEFAULT;

// Initialize DHT sensor 
// NOTE: For working with a faster than ATmega328p 16 MHz Arduino chip, like an ESP8266,
// you need to increase the threshold for cycle counts considered a 1 or 0.
// You can do this by passing a 3rd parameter for this threshold. It's a bit
// of fiddling to find the right value, but in general the faster the CPU the
// higher the value. The default for a 16mhz AVR is a value of 6. For an
// Arduino Due that runs at 84mhz a value of 30 works.
// This is for the ESP8266 processor on ESP-01 
DHT_Unified dht(DHTPIN, DHTTYPE, 11); // 11 works fine for ESP8266

Adafruit_BMP085_Unified bmp;

#define STATUSLED 13

boolean debugging = false; // enable / disable debug output

String float2String(float value) {
  char v[10];
  dtostrf(value,4,1,v);
  return String(v);
}

String readSensors() {
  if( debugging ) Serial.println("Read sensor data...");
  String str = "";
  String str2Send = "wData;";

  if( debugging ) Serial.println("DHT22");
  sensors_event_t event;
  float temperature = 0.0;
  dht.temperature().getEvent(&event);
  temperature = event.temperature;
  if( isnan(temperature) ) {
    if( debugging ) Serial.println("Error reading temperature!");
  } else {
    if( debugging ) {
      Serial.print("Temperature: ");
      Serial.print(temperature);
      Serial.println(" *C");
    }
 }
 // Get humidity event and print its value.
 dht.humidity().getEvent(&event);
 float humidity = 0.0;
 humidity = event.relative_humidity;
 if( isnan(humidity) ) {
   if( debugging ) Serial.println("Error reading humidity!");
 } else {
   if( debugging ) {
     Serial.print("Humidity: ");
     Serial.print(humidity);
     Serial.println("%");
   }
 }

 float temperature2 = 0.0;
 bmp.getTemperature(&temperature2);
 str = "Temperature: " + float2String(temperature2) + " *C\n";
 str += "Humidity: " + float2String(humidity) +" %\n";

 float pressure = 0.0;
 bmp.getPressure(&pressure);
 pressure = pressure/100.0;
 if( debugging ) {
   Serial.print("Pressure: ");
   Serial.print(pressure);
   Serial.println(" hPa");
 }
 str += "Pressure: " + float2String(pressure) + " hPa\n";

 if( debugging ) {
   Serial.print("Collected sensor data: ");
   Serial.println(str);
 } 
 str2Send += temperature;
 str2Send += ";";
 str2Send += humidity;
 str2Send += ";";
 str2Send += pressure;
 str2Send += "\0";

 if( debugging ) {
   Serial.print("Sensor data for sending to server: ");
   Serial.println(str2Send);
 }
 return str2Send;
}

// expected string: e.g. weatherdata;14.3;60.7;1018.0
void sendSensorData(String str2Send) {
 WiFiClient client;
 client.connect(IPWS, PORT);
 if( debugging ) {
   Serial.print("Sending: ");
   Serial.println(str2Send);
 }
 client.write(str2Send.c_str(), str2Send.length());
 if( client.connected() ) {
   client.stop();
 }
}

void setup(void) {
 // You can open the Arduino IDE Serial Monitor window to see what the code is doing
 Serial.begin(115200); // Serial connection from ESP-01 via 3.3v console cable

 dht.begin(); // initialize temperature sensor

 if (!bmp.begin()) {
   if( debugging ) Serial.println("Could not find a valid BMP085 sensor, check wiring!");
   while (1) {}
 }
 
 // set as station
 WiFi.mode(WIFI_STA);
 
 // connect to WiFi network
 WiFi.begin(ssid, password);
 if( debugging ) Serial.print("\n\r \n\rWorking to connect");

 // wait for connection
 while (WiFi.status() != WL_CONNECTED) {
   delay(500);
   if( debugging ) Serial.print(".");
 }

 pinMode(STATUSLED, OUTPUT);
 digitalWrite(STATUSLED, LOW);
} // setup()

void loop(void) {

 // read data from sensors
 String str = readSensors();

 if( debugging ) {
   Serial.print("Sending sensor data to server: ");
   Serial.println(str);
 }
 sendSensorData(str);

 // indicate data acquisition and sending
 digitalWrite(STATUSLED, HIGH);
 delay(1000);

 // enter deep sleep mode for x minutes
 // GPIO16 needs to be tied to RST to wake ESP8266 from deep sleep mode
 // http://russ.russmathis.com/esp8266-power-modes/
 //ESP.deepSleep(DEEP_SLEEP, MODE);
 // 30000000 ms = 30 s
 ESP.deepSleep(MIN_30, WAKE_RF_DEFAULT);
} 

Output

In debug mode the output on the serial console is similar to this:

Working to connect….
Read sensor data…
DHT22
Temperature: 22.10 *C
Humidity: 53.60%
BMP085
Temperature: 22.10 *C
Pressure: 1009.2 hPa
Collected sensor data:
Temperature: 22.10 *C
Humidity: 53.60%
Pressure: 1009.2 hPa
Sending sensor data to server: …

Notes

After measurement the ESP8266 establishes a WiFi connection. The collected data is sent as a string to a dedicated server of the desired IP within the local network.
Another possibility would be to send the data to thingspeak.com for quick visualization.

Deep sleep – saving power

After having sent the measurement data the ESP8266 enters „deep sleep“ mode until it is woken up again after the desired time. For this feature GPIO_16 has to be wired with Reset on the ESP8266.

Configuration

The ESP8266 module should be configured as a a station, not as an accesspoint (WIFI_AP). In this setup the module is used for sending data only.

Barometric sensor

Usually the barometric pressure is the ‚raw‘ value. It is calculated to match the barometric pressure at sea level. Depending on the actual level the measured value differs from the barometric values close to the location. In this case the measured barometric pressure needs to be adapted.

Temperature sensor data

Small deviations of the measured temperature are possible between the two sensors. The causes for such deviations are usually production tolerances of the sensor’s chips or even a close local heat source.