Dash Button Hacks

Quite some time ago Amazon launched the dash buttons. Amazon intends them to be used for ordering everyday products. Without even knowing the price before automatically finalizing the order! This way I don’t want to use such a button.
In the end a dash button is a relatively cheap WiFi button. Quickly the first users found out how to hack them and use them in different contexts. A dash button can be a doorbell, a phone finder, a tool for doing statistics (work started/stopped, …)  or it could simply switch the light on.
Here is a short description on how to set up a dash button for alternative uses.

Hardware

Raspberry Pi (Zero)
Amazon Dash Button

Dash Button Setup

    • Follow the setup descriptions as described here. The trick is to leave the Amazon app directly after having copied the WiFi credentials to the button.
    • Find out the dash button’s IP and MAC, e.g. by looking at the active devices in your router’s setup.
    • The button will constantly nag in the amazon app about being setup uncompletely. Block the button’s internet access using the setup of the router.

Software

A small Python script on a Raspberry Pi „sniffs“ the local network for packets of all the devices within the network. If the MAC of a dash button is found certain actions can be triggered.

Additionally required packets:

sudo pip install scapy # http://www.secdev.org/projects/scapy/
sudo apt-get install tcpdump
#!/usr/bin/python
from scapy.all import *
import httplib, urllib

def doWhatIWant():
  print "TODO"

def arp_detect(pkt):
  #pkt.show() # debug info
  if pkt[ARP].op == 1: # network request
    mac = pkt[ARP].hwsrc
    mac = mac.lower()
    ip = pkt[ARP].psrc
    print "IP: " + str(ip) + ", MAC: " + str(mac)

  if mac == 'xx:xx:xx:xx:xx:xx': # dash button
    doWhatIWant()
    sleep(5)
    return "dash button detected\n"
  else:
    print "Unknown: " + str(ip) + ", " + str(mac)
    return "Unknown MAC: " + pkt[ARP].hwsrc

try:
  print sniff(prn=arp_detect, filter="arp", store=0)
except KeyboardInterrupt:
  exit()

To run the script automatically after boot simply add a line to /etc/rc.local:

sudo python /home/pi/sniff.py&

Now the dash button is ready to be used for anything else.

To make it a doorbell or a phone finder one could use pushover. The app is installed on a smartphone. That way it is possible to send notifications to this smartphone using the pushover API  using an API and a user key.

Example code

def sendNotification(message):
  conn = httplib.HTTPSConnection("api.pushover.net:443")
  conn.request("POST", "/1/messages.json",
  urllib.urlencode({
  "token": "APItoken",
  "user": "usertoken",
  "message": str(message),
 "sound": "intermission"
 }), { "Content-type": "application/x-www-form-urlencoded" })
 conn.getresponse()

A smart Raspberry Pi Zero DIY Text Clock

So this is the project I had in mind when I was experimenting with a NeoPixel strip on a Raspberry Pi Zero. The original text clock was invented a couple of years ago. With its elegant and timeless (yes, literally) design the QLOCKTWO is simultaneously a beautiful and useful piece of art.
It is not that I exactly needed yet another clock – but I got intrigued and wanted to create my own, smart version of a text clock.

Numerous examples of DIY versions and manuals on how to build a text clock are available on the internet. Some manuals involve soldering a lot of LEDs. I wanted to skip this step and went for a NeoPixel strip. In total I calculated 92 NeoPixels: one for each letter that can be alighted.

My version of the text clock should not only display the time in a unique way but should also indicate something more, it should be smart! This is why I chose a Raspberry Pi Zero instead of a microcontroller as a base. This way I’m able to easily get more information using a Python script along with some ready-made libraries.

My smart text clock indicates whether I have unread emails in my inbox by changing the colour of the LEDs. If desired the smart text clock is also able to indicate the weather developments depending on the outside temperature or any other criteria. The weather data is taken from openweathermap.org as in former projects.

One could also try to indicate whether a train one needs to catch regularly is on time. Or the smart text clock could be used as a traffic monitor for commuters (similar to this project idea).

Updating the smart text clock every 5 minutes should be precise enough for me. It is definitely more precise than a fuzzy clock which indicates bright and dark only.

Hardware

The hardware list of the last blog entry can be extended by the picture frame which is often used for DIY text clocks. A suitable one is sold by a well-known swedish furniture store.
Additionally some paper is useful for dispersing the light from the LEDs behind the letters.

Adhesive foil with precisely cut letters can be put on the glass to match the LEDs from the strip. Here I had professional help by friends owning a cutting plotter.

The LED strip is cut and soldered together appropriately to match the letters positions. The strip is glued with its adhesive back to the picture frame’s back plate.

The LEDs are separated by a grid behind the glass. It is printed with a 3D printer. This grid helps to avoid interferences between the different letters.

A piece of transparent paper between the glass and the grid is the possibility to make the letters look smooth. If it was missing the LEDs were directly visible. A bit of diffusion makes it look better…

Software

A straightforward python script is run automatically every five minutes. First the current time is determined. The time is translated into words with a five minute precision.

The words are mapped to the LED indices from the NeoPixel strip. These are the ones to alight to display the time.

Colour Definition

To determine which colour to use for the alighted LEDs some (optional) checks are built-in:

  • Approximately every hour the weather data is fetched from openweathermap.org using the python owm library. The temperature is extracted along with the weather code. The results are used for defining the colour of the LEDs. Other parameters can be taken into account as well.
  • The number of unread emails is checked using the Python imap library. If the number is greater than zero the LED color is changed.

During night time the brightness of the LEDs is lowered. That way the smart text clock serves as a convenient night light as well.

Source Code


#!/usr/bin/python
# -*- coding: cp1252 -*-

import time

import imaplib

import pyowm
import json
import pprint

from neopixel import *

########################CONFIG############################

OWM_APYKEY='get one from openweathermap.org'
OWM_ID = an ID number

# file to store weather state
fileName="/home/pi/textClockWeatherState.txt"

EMAIL_NAME = "user@googlemail.com"
EMAIL_PASS = "password"

# LED strip configuration:
LED_COUNT = 92 # Number of LED pixels.
LED_PIN = 18 # GPIO pin connected to the pixels (must support PWM!).
LED_FREQ_HZ = 800000 # LED signal frequency in hertz (usually 800khz)
LED_DMA = 5 # DMA channel to use for generating signal (try 5)
LED_BRIGHTNESS = 128 # Set to 0 for darkest and 255 for brightest
LED_INVERT = False # True to invert the signal (when using NPN transistor level shift)

######################END#CONFIG##########################

_start = "IT IS "
_end = " O\'CLOCK"
_numbers = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'TEN', 'ELEVEN', 'TWELVE')
_past = ' PAST '
_to = ' TO '
_fivepast = 'FIVE PAST '
_tenpast = 'TEN PAST '
_aquarter = 'A QUARTER '
_twenty = ' TWENTY'
_twentyfive = ' TWENTYFIVE'
_half = ' HALF'
_fiveto = 'FIVE TO '
_tento = 'TEN TO '

'''
I T L I S A S T I M E 0,1, 2,3
A C Q U A R T E R D C 4, 5,6,7,8,9,10,11
T W E N T Y F I V E X 12,13,14,15,16,17, 18,19,20,21
H A L F B T E N F T O 22,23,24,25, 26,27,28, 29,30
P A S T E R U N I N E 31,32,33,34, 35,36,37,38
O N E S I X T H R E E 39,40,41, 42,43,44, 45,46,47,48,49
F O U R F I V E T W O 50,51,52,53, 54,55,56,57, 58,59,60
E I G H T E L E V E N 61,62,63,64,65, 66,67,68,69,70,71
S E V E N T W E L V E 72,73,74,75,76, 77,78,79,80,81,82
T E N S E O C L O C K 83,84,85
'''
# map time to precise LED indices
_timeLightMap = {
'IT IS ' : (0,1,2,3),
' HALF' : (22,23,24,25),
' PAST ' : (31,32,33,34),
' TO ' : (29,30),
'FIVE PAST ' : (18,19,20,21, 31,32,33,34),
'TEN PAST ' : (26,27,28, 31,32,33,34),
'A QUARTER ' : (4, 5,6,7,8,9,10,11),
' TWENTY' : (12,13,14,15,16,17),
' TWENTYFIVE' : (12,13,14,15,16,17, 18,19,20,21),
' HALF PAST ' : (22,23,24,25, 31,32,33,34),
' TWENTYFIVE TO ' : (12,13,14,15,16,17, 18,19,20,21, 29,30),
' TWENTY TO ' : (12,13,14,15,16,17, 29,30),
'TEN TO ' : (26,27,28, 29,30),
'FIVE TO ' : (18,19,20,21, 29,30),
'ONE' : (39,40,41),
'TWO' : (58,59,60),
'THREE' : (45,46,47,48,49),
'FOUR' : (50,51,52,53),
'FIVE' : (54,55,56,57),
'SIX' : (42,43,44),
'SEVEN' : (72,73,74,75,76),
'EIGHT' : (61,62,63,64,65),
'NINE' : (35,36,37,38),
'TEN' : (83,84,85),
'ELEVEN' : (66,67,68,69,70,71),
'TWELVE' : (77,78,79,80,81,82),
' O\'CLOCK' : (0,1,2,3)
}

class SmartTextClock():

def run(self):
print "A SMART TEXT CLOCK"

def check_googlemail(self, login, password):
# if new mail return # emails
obj = imaplib.IMAP4_SSL('imap.gmail.com','993')
obj.login(login, password)
obj.select()
nofUnreadMessages = len(obj.search(None, 'UnSeen')[1][0].split())
print "Unread emails: " + str(nofUnreadMessages)
return nofUnreadMessages

def clock(self):
t = time.strftime("%H:%M")
print t
return t

def translateHour(self, hour, offset):
if hour == '00' or hour == '12':
if offset == True:
return _numbers[0]
else:
return _numbers[11]
if hour == '1' or hour == '13':
if offset == True:
return _numbers[1]
else:
return _numbers[0]
if hour == '2' or hour == '14':
if offset == True:
return _numbers[2]
else:
return _numbers[1]
if hour == '3' or hour == '15':
if offset == True:
return _numbers[3]
else:
return _numbers[2]
if hour == '4' or hour == '16':
if offset == True:
return _numbers[4]
else:
return _numbers[3]
if hour == '5' or hour == '17':
if offset == True:
return _numbers[5]
else:
return _numbers[4]
if hour == '6' or hour == '18':
if offset == True:
return _numbers[6]
else:
return _numbers[5]
if hour == '7' or hour == '19':
if offset == True:
return _numbers[7]
else:
return _numbers[6]
if hour == '8' or hour == '20':
if offset == True:
return _numbers[8]
else:
return _numbers[7]
if hour == '9' or hour == '21':
if offset == True:
return _numbers[9]
else:
return _numbers[8]
if hour == '10' or hour == '22':
if offset == True:
return _numbers[10]
else:
return _numbers[9]
if hour == '11' or hour == '23':
if offset == True:
return _numbers[11]
else:
return _numbers[10]
return ''

# time format: HH:mm
def translateTime(self, time):
t = time.split(':', 1)
print t
h = str(t[0])
m = str(t[1])
print h + ":" + m

indices = (1,2)

if float(m) >= 0.0 and float(m) <= 2.5:
#IT IS X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[self.translateHour(h, False)] + _timeLightMap[_end]
if float(m) > 2.5 and float(m) <= 7.5:
#IT IS FIVE PAST X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_fivepast] + _timeLightMap[self.translateHour(h, False)] + _timeLightMap[_end]
if float(m) > 7.5 and float(m) <= 12.5:
#IT IS TEN PAST X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_tenpast] + _timeLightMap[self.translateHour(h, False)] + _timeLightMap[_end]
if float(m) > 12.5 and float(m) <= 17.5:
#IT IS A QUARTER PAST X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_aquarter] + _timeLightMap[_past] + _timeLightMap[self.translateHour(h, False)] + _timeLightMap[_end]
if float(m) > 17.5 and float(m) <= 22.5:
#IT IS TWENTY PAST X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_twenty] + _timeLightMap[_past] + _timeLightMap[self.translateHour(h, False)] + _timeLightMap[_end]
if float(m) > 22.5 and float(m) <= 27.5:
#IT IS TWENTYFIVE PAST X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_twentyfive] + _timeLightMap[_past] + _timeLightMap[self.translateHour(h, False)] + _timeLightMap[_end]
if float(m) > 27.5 and float(m) <= 32.5:
#IT IS HALF PAST X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_half] + _timeLightMap[_past] + _timeLightMap[self.translateHour(h, False)] + _timeLightMap[_end]
if float(m) > 32.5 and float(m) <= 37.5:
#IT IS TWENTYFIVE TO X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_twentyfive] + _timeLightMap[_to] + _timeLightMap[self.translateHour(h, True)] + _timeLightMap[_end]
if float(m) > 37.5 and float(m) <= 42.5:
#IT IS TWENTY TO X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_twenty] + _timeLightMap[_to] + _timeLightMap[self.translateHour(h, True)] +_timeLightMap[_end]
if float(m) > 42.5 and float(m) <= 47.5:
#IT IS A QUARTER TO X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_aquarter] + _timeLightMap[_to] + _timeLightMap[self.translateHour(h, True)] + _timeLightMap[_end]
if float(m) > 47.5 and float(m) <= 52.5:
#IT IS TEN TO X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_tento] + _timeLightMap[self.translateHour(h, True)] + _timeLightMap[_end]
if float(m) > 52.5 and float(m) <= 57.5:
#IT IS FIVE TO X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[_fiveto] + _timeLightMap[self.translateHour(h, True)] + _timeLightMap[_end]
if float(m) > 57.5 and float(m) <= 59.9:
#IT IS TO X O'CLOCK
indices = _timeLightMap[_start] + _timeLightMap[self.translateHour(h, True)] + _timeLightMap[_end]
return indices

def selectColor(self, weatherCondition):
# Email: Lime Green 50-205-50
# http://www.tayloredmktg.com/rgb/
color = Color(255, 222, 173)
# weatherCondition = 'fair' # 'good', 'fair, 'bad'
if weatherCondition == 'fair':
color = Color(255, 222, 173) # Navajo White 255-222-173 # Lemon Chiffon 255-250-205
if weatherCondition == 'good':
color = Color(255, 127, 80) # Coral 255-127-80 # Light Salmon 255-160-122
if weatherCondition == 'bad':
color = Color(70, 130, 180) # Steel Blue 70-130-180
return color

def getWeatherFromOWM(self):
owm = pyowm.OWM(OWM_APYKEY, version='2.5')
# Search for current weather
print "Weather @ID"
obs = owm.weather_at_id(OWM_ID)
w1 = obs.get_weather()
print(w1)
print w1.get_status()

weatherCondition = 'fair' # 'good', 'fair, 'bad'

# get general meaning for weather codes https://openweathermap.org/weather-conditions
weatherCode = w1.get_weather_code()
print weatherCode
print w1.get_sunset_time('iso')
temperature = w1.get_temperature('celsius')['temp']
print str(temperature) + " C"

# simple: judge weather on temperature
if temperature<=10.0:
weatherCondition = 'bad'
if temperature>10.0 and temperature<20.0:
weatherCondition = 'fair'
if temperature>=20.0 and temperature<35.0:
weatherCondition = 'good'
if temperature>=35.0:
weatherCondition = 'bad'

return weatherCondition

def readSavedWeatherCondition(self):
weatherCondition = 'fair' # default
try:
print 'Read file ' + fileName
target = open(fileName, 'r')
weatherCondition = target.read()
print weatherCondition
target.close()
except IOError:
print fileName + " does not exist yet. Creating a default file."
self.saveWeatherConditionToFile(weatherCondition)
pass
return weatherCondition

def saveWeatherConditionToFile(self, weatherCondition):
try:
print 'Write file ' + fileName
target = open(fileName, 'w')
target.write(weatherCondition)
target.close()
except:
print 'File ' + fileName + ' could not be written.'

# NEOPIXEL: GRB
#strip.setPixelColor(i, Color(0,0,120)) # B
#strip.setPixelColor(i, Color(0,120,0)) # R
#strip.setPixelColor(i, Color(120,0,0)) # G
def alight(self, LEDindices, color):
print 'Alight indices ' + str(LEDindices)
for i in LEDindices:
strip.setPixelColor(i, color)
strip.show()

if __name__ == "__main__":
app = SmartTextClock()
app.run()

# check for unread emails
unreadEmails = app.check_googlemail(EMAIL_NAME, EMAIL_PASS)

# get time and determine LED indices
time = app.clock()
indices = app.translateTime(time)
print "Indices " + str(indices)

# investigate weather data
weatherCondition = app.readSavedWeatherCondition()

# update weather data every hour
theTime = time.split(":")
# this range should be met from time to time
if int(theTime[1])>=0 and int(theTime[1])<=7:
weatherCondition = app.getWeatherFromOWM()
app.saveWeatherConditionToFile(weatherCondition)

# create NeoPixel object with appropriate configuration
strip = Adafruit_NeoPixel(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS)
# intialize the library (must be called once before other functions)
strip.begin()

# low light during 19-8 o'clock
if(8 < int(theTime[0]) > 19):
strip.setBrightness(200)
else:
strip.setBrightness(50)

stripColor = Color(120,120,120)

# select color depending on weather condition
if weatherCondition == 'bad':
stripColor = Color(0, 120, 0)
if weatherCondition == 'fair':
stripColor = Color(120, 120, 120)
if weatherCondition == 'good':
stripColor = Color(0,0,120)

if unreadEmails > 0:
stripColor = Color(205,50,50)

app.alight(indices, stripColor)

 

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

 

Making a Raspberry Pi speak: Alexa

Speaking with devices (and making them answer or do something) seems to be a trend of the time. Some up-to-date smartphones and tablets allow allow to use speech to trigger internet searches, to write short messages or e-mails (sometimes with funny results), to ask for something in the region, to turn the light of the smartphone on, … .

In addition to voice control on smartphones, well-known companies started to launch devices to enable voice control @home. However, the commercial solutions perform speech recognition on their own servers. AFAIK due to computing power requirements of the AI behind.
In this case one has to live with the fact that a constant internet connection is inevitable and that own voice samples are uploaded somewhere else for analysis.

Still, speech control can be extremely useful. My favourite example for illustration is setting a timer while being busy with something else.
Though, in a smart home there are many more applications for speech control: light, heating, media, … . Even for elderly, handicapped or visually impaired controlling everyday procedures by the own voice can be a huge advantage in the daily life.

Open Source Solution: Jasper

The open source solution for voice control, Jasper, offers the possibility to work offline, but the setup of the software is not trivial. It looks like the manual is outdated. Some experimental, but required libraries are not to be found easily anymore. This is why I turned to the API of a commercial solution to play with speech recognition on my Raspberry Pi 3.

At the moment speech recognition devices such as Amazon’s Alexa are not sold everywhere yet. It is possible to order them in Europe, but they are not shipped yet. As rumour has it: regions in which stronger accents are spoken are served first. 🙂

Amazon’s Alexa

The voice service that is used by Amazon’s Alexa devices can be relatively easy tested on a Raspberry Pi 3. Since a couple of weeks wake word detection in this solution is possible on the Raspberry Pi 3 as well.

Hardware

Raspberry Pi 3 (incl. power supply, display, keyboard and mouse for setup)

USB microphone

Non-bluetooth speaker

Software

Amazon Developer Account Settings

An Amazon developer account is required for using the voice service. The registration is free. After the registration an Alexa device has to be created along with security and web settings. On this page the required steps are explained. Save the client ID and secret for later.

Raspberry Pi

This github project contains the required installation software for download:

git clone https://github.com/alexa/alexa-avs-sample-app.git

The setup of the software is performed running the automated_install shell script. It has to be completed with the product name, client ID and secret. The script guides through the configuration and setup.

After successful installation the companion service, the AVS client and the desired wake word agent have to be launched in three separate terminals.

The AVS client requires authorization by signing in using the Amazon developer account. On request the default browser is opened and Alexa is ready to listen in after the confirmation.

Playing around

On the Raspberry Pi Alexa starts to listen more closely either on the push of a button or by hearing the wake word ‚Alexa‘. It confirms with a sound that it is listening. The next spoken words (shoud be english) are going to be analyzed. A longer break between words marks the end of the sentence.
Alexa’s answers are returned quickly! Out of the box it is possible to ask for the current weather at a specific location, to ask for a joke, to convert unities, to look up something in wikipedia, etc . Alexa can be connected to a calendar, it can calculate and it knows its „birthday“ (being the day it was first sold). That’s not all…

Surprising was my low-cost microphone in combination with Alexa. The first tests on various operating systems were devastating: I had to speak from a distance of 1 cm to be heard at all. Independent of the recording settings. I thought it is also some kind of safety precaution if I had to be close to the microphone to use speech recognition …but Alexa immediately worked from a distance of 2 m as well. It felt a bit slower, though, but still, it worked…

When I played the video recorded of my running system telling a joke it just started itself again when hearing the wake word from the video! It has already been shown that infinite loops of voice control can be set up easily: https://www.youtube.com/watch?v=ZfCfTYZJWtI . Alexa might also react on its wake word spoken on TV as recently learnt from the Verge’s doll house article!

Alexa is extensible with custom skills for own applications. Perhaps this is the thing to try next.

 

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

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

 

E-Ink Display driven by Arduino

Recently I discovered an 4,3 “ E-Ink display (Waveshare 4,3″inch e-Paper 800×600) that can be used with an Arduino. E-Ink displays (or E-Paper) are low-energy displays that keep what was printed even when the power is turned off. This feature makes E-Ink displays not only perfect for E-Readers, but also interesting for IoT projects. Such a display could show information that does not require animations (E-Ink displays take their time to update).
For IoT projects I can immediately think of examples such as displaying weather data (and the time), daily appointments from an electronic calendar, cooking recipes… .

Hardware

Waveshare 4,3″inch e-Paper 800×600
Arduino (Uno)
some cables

Wiring

e-Ink Display Arduino
VCC 5V
GND GND
DOUT RX
DIN TX
WAKE_UP D2
RST A3

Software

In this post I discovered the epd library for usage in Arduino sketches. Originally this library was found here and repackaged. This library can be directly imported into the Arduino IDE.

Arduino Sketch

This sketch is based on the example from here. It was immediateley working with little changes only.
To upload the sketch the RX/TX lines have to be removed from the Arduino. If the RX/TX lines are put back the Arduino can be reset to refresh the display.

#include <epd.h>

void fillPage(void) {
  char buff[] = {'G', 'B', 'K', '3', '2', ':', ' ', 0xc4, 0xe3, 0xba, 0xc3, 0xca, 0xc0, 0xbd, 0xe7, 0};

  epd_set_color(BLACK, WHITE);  // text=black, background=white
 
  epd_clear();

  // setting the font type and size
  epd_set_ch_font(GBK32);
  epd_set_en_font(ASCII32);

  epd_disp_string(buff, 0, 0);
  epd_disp_string("Test of the length of characters that can be printed in a line #1", 0, 0);
  epd_disp_string("Test of the length of characters that can be printed in a line #2", 0, 50);
  epd_disp_string("Test of the length of characters that can be printed in a line #3", 0, 100);
  
  // increasing the font type and size
  epd_set_ch_font(GBK48);
  epd_set_en_font(ASCII48);

  buff[3] = '4';
  buff[4] = '8';

  // print larger text
  epd_disp_string(buff, -10, -10);
  epd_disp_string("thisisyetanotherblog.wordpress.com", 25, 200);

  epd_set_ch_font(GBK64);
  epd_set_en_font(ASCII64);

  buff[3] = '6';
  buff[4] = '4';

  // drawing some forms
  epd_draw_circle(80, 350, 55);
  epd_draw_triangle(100, 450, 170, 400, 250, 450);
  epd_fill_rect(400, 500, 335, 470);
  epd_draw_line(100, 550, 700, 550);
 
  // update the display
  epd_udpate();
}

void setup(void) {
 // initialize the display
 epd_init();
 epd_wakeup();
 epd_set_memory(MEM_NAND);
}

void loop(void) {
 fillPage(); // fill the display once
 while (1) {} // do nothing more in the loop
}

Result

This is a base to start from with the next IoT project… Hopefully E-Ink displays become more affordable sooner than later!

e-ink display test, powered off

Sources

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

http://www.jarzebski.pl/arduino/komponenty/e-papier-waveshare-4-3.html
http://www.zoobab.com/waveshare-e-paper-and-arduino
http://zoobab.wdfiles.com/local–files/waveshare-e-paper-and-arduino/ArduinoEpd.zip

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.