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
andADAFRUIT_IO_USERNAME
- set these to your secret AIO key and your Adafruit account user name -
PIC_FEED
andTEMP_FEED
hold the feed names that must match the Sender -
LCD_WIDTH
andLCD_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 thetext_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 callswitch_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