Receiver

As you can see from the picture, the receiver will be taking the images and temperature readings from the feed and displaying them on the PiTFT via Pygame. The overview from my Raspberry Pi Pygame UI Basics tutorial will get your environment all set up and running sweetly.

Once you've done that follow these steps to add some prerequisites for this project:

sudo pip install adafruit-io
sudo apt-get install python-matplotlib

Now go ahead and grab the project code again as we did on the Sender:

cd ~
git clone https://github.com/jerbly/adaiot.git

There's quite a lot going on in this code so let's walk through it in a few sections.

Setup

In this section we're importing all the required libraries, setting up a few constants and initialising pygame. You may want to change some of the constants:

  • ADAFRUIT_IO_KEY and ADAFRUIT_IO_USERNAME - set these to your secret AIO key and your Adafruit account user name
  • PIC_FEED and TEMP_FEED hold the feed names that must match the Sender
  • LCD_WIDTH and LCD_HEIGHT are used throughout the code, set these to match your screen size
  • LUM_SAMPLE_POS (explained later) is the pixel we will sample for luminance
import time
import base64
import pygame
from pygame.locals import *
import os
from Adafruit_IO import MQTTClient
import feed
import sys
import signal

# Set to your Adafruit IO key & username below.
ADAFRUIT_IO_KEY      = 'SECRET'
ADAFRUIT_IO_USERNAME = 'SECRET'  # See https://accounts.adafruit.com to find your username.

PIC_FEED = 'pic'
TEMP_FEED = 'deck-temp'
OUT_DIR = '/home/pi/adaiot/'
LCD_WIDTH = 320
LCD_HEIGHT = 240
LCD_SIZE = (LCD_WIDTH, LCD_HEIGHT)
LUM_SAMPLE_POS = 30,220

BLACK = 0,0,0
GREEN = 0,255,0
RED = 255,0,0
WHITE = 255,255,255

os.putenv('SDL_FBDEV', '/dev/fb1')
os.putenv('SDL_MOUSEDRV', 'TSLIB')
os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')
pygame.init()
pygame.mouse.set_visible(False)
lcd = pygame.display.set_mode(LCD_SIZE)
lcd.fill(BLACK)
font_big = pygame.font.Font(None, 40)

image_surface = None
text_surface = None
lum = 100
page = 0

MQTT events

Here we set up some event handlers for the MQTT session.

  • connected is called when the connection to the Adafruit servers is established. At this point we subscribe to the two feeds. The Adafruit servers immediately send back the last entry in the feed and then every new entry as it comes in.
  • disconnected is called when ever our connection drops. The underlying MQTT library has a reconnect loop which will kick in and attempt to get you connected again. So we just print a message for debug.
  • message is called whenever a new feed entry comes in.
    • If it's a new image we decode the base64 to a jpeg and then load it back into a pygame surface. Next we grab the colour of a pixel to get its luminance this is so we can determine whether we need a light or dark text colour over the background.
    • If it's a temperature value we choose black or white text colour according to the background luminance and render this to a surface.
def connected(client):
    print 'MQTT Connected'
    client.subscribe(PIC_FEED)
    client.subscribe(TEMP_FEED)

def disconnected(client):
    print 'MQTT Disconnected from Adafruit IO!'

def message(client, feed_id, payload):
    global image_surface, text_surface, lum
    if feed_id == PIC_FEED:
        print 'MQTT received pic'
        fh = open(OUT_DIR+"testjr.jpg", "wb")
        fh.write(payload.decode('base64'))
        fh.close()
        image_surface = pygame.image.load(OUT_DIR+"testjr.jpg")
        col = image_surface.get_at(LUM_SAMPLE_POS)
        lum = (0.299*col.r + 0.587*col.g + 0.114*col.b)
        # IF RESIZE REQUIRED
        #surf = pygame.transform.scale(surf, LCD_SIZE)
    elif feed_id == TEMP_FEED:
        print 'MQTT received temp: {0}'.format(payload)
        if lum < 75:
            col = WHITE
        else:
            col = BLACK
        text_surface = font_big.render(payload+u'\u00B0C', True, col)
    show_dash()

Drawing and Switching pages

show_dash is the main function for rendering the surfaces to the LCD.

  • If we're on page 0 it first paints the image_surface if we have one, otherwise it's just a black fill. Next it positions the text_surface in the bottom left of the screen.
  • If we're on page 1 it paints the temperature graph. (More on this later)

You'll see later how we're using the touchscreen to flip between pages. switch_page simply flips between page 0 and 1.

def show_dash():
    if page == 0:
        if image_surface:
            lcd.blit(image_surface, (0,0))
        else:
            lcd.fill(BLACK)
        if text_surface:
            rect = text_surface.get_rect()
            rect.x = 10
            rect.y = LCD_HEIGHT-rect.height-2
            lcd.blit(text_surface, rect)
    elif page == 1:
        feed_surface = pygame.image.load(OUT_DIR+"temps.png")
        lcd.blit(feed_surface, (0,0))

    pygame.display.update()

def switch_page():
    global page
    if page == 1:
        page = 0
    else:
        page += 1

    show_dash()

Connecting and looping

In this last section there are three significant things happening:

  • First we set up the signal handling so we can close the program cleanly. There are multiple threads in this program so we need to handle ctrl+c and kill signals to gracefully stop.
  • Second we set up and connect to Adafruit.io over MQTT
  • Finally the main loop. Here we're doing to key things:
    • We check for MOUSEBUTTONUP events so we can call switch_page if the screen is touched.
    • Next, we use a counter to trigger a secondary thread every 5 minutes. This other thread creates the temperature graph explained in the next section.
def signal_handler(signal, frame):
    print 'Received signal - exitting'
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

# Create an MQTT client instance.
client = MQTTClient(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY)

# Setup the callback functions defined above.
client.on_connect    = connected
client.on_disconnect = disconnected
client.on_message    = message

# Connect to the Adafruit IO server.
print 'Attempting to connect MQTT...'
client.connect()

client.loop_background()
chart_counter = 0
while True:
    for event in pygame.event.get():
        if event.type is MOUSEBUTTONUP:
            switch_page()
    time.sleep(0.1)
    # Create a new chart approx every 5 mins
    chart_counter += 1
    if chart_counter == 3000:
        chart_counter = 0
        feed.ChartThread(ADAFRUIT_IO_KEY, TEMP_FEED, OUT_DIR).start()

Temperature Graph

feed.py defines a separate thread to be called periodically to construct the temperature graph. This is done in a separate thread to not spoil the responsiveness of the touchscreen to flip between pages. As you will see it's quite a heavy operation to gather the data and generate the graph.

After connecting to Adafruit.io using the REST client we make a call to retrieve the data from the temperature feed. This uses the Data Retrieval API to get all the data from a feed. At the moment there's no way to restrict what's returned, you get everything. So we have to filter the data to only grab entries from the last 24 hours.

Each data object as it comes in actually has a number of elements to it. Here's what one entry actually looks like:

Data(created_epoch=1459912484.47247, created_at=u'2016-04-06T03:14:44.472Z', updated_at=u'2016-04-06T03:14:44.472Z', value=u'-0.875', completed_at=None, feed_id=514989, expiration=None, position=None, id=348921349)

We use the created_epoch to determine if the value was sent in the last 24 hours and as the x axis on the graph, dates. The temperature values for the y axis are stored in the temps list.

Finally these two lists are passed to Matplotlib to render a graph as an image. I'm certainly no expert with this graphing library but it does the trick. Getting it to create you an image the correct size is a black art!

from Adafruit_IO import Client
import datetime
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.dates import date2num

import threading

class ChartThread(threading.Thread):
    def __init__(self, client_key, feed_name, out_dir):
        threading.Thread.__init__(self)
        self._client_key = client_key
        self._feed_name = feed_name
        self._out_dir = out_dir

    def run(self):
        print "ChartThread connecting"
        aio = Client(self._client_key)
    
        print "ChartThread fetching data"
        data = aio.data(self._feed_name)
    
        today = datetime.datetime.now()
        one_day = datetime.timedelta(days=1)
        yesterday = today - one_day
    
        dates = []
        temps = []
    
        print "ChartThread treating data"
        for d in data:
            ts = datetime.datetime.fromtimestamp(d.created_epoch)
            if ts > yesterday:
                dates.append(ts)
                temps.append(d.value)
    
        print "ChartThread plotting"
        dates = date2num(dates)
    
        fig = plt.figure()
        fig.set_size_inches(4, 3)
        plt.subplots_adjust(left=0.0, right=0.925, bottom=0.0, top=0.948)
        ax = fig.add_subplot(111)
        ax.plot_date(dates, temps, '-')
        ax.axes.get_xaxis().set_visible(False)
        plt.savefig(self._out_dir+'temps.png', dpi = 80, bbox_inches='tight', pad_inches = 0)
        plt.close(fig)
        print "ChartThread done"

That's it! Remember to run the dashboard as root:

sudo python dash.py
Last updated on 2016-04-09 at 01.40.28 PM Published on 2016-04-12 at 01.12.03 PM