Location

In the code.py file (which you will have renamed from electioncal.py), you can change the location for which you want to display the election data in this lines:

Download: file
# Change this to your state and county, replacing spaces for underscores and in lowercase
STATE="new_york"
COUNTY="new_york"

Adafruit IO Time Server

In order to get the precise time, our project will query the Adafruit IO Internet of Things service for the time. Adafruit IO is absolutely free to use, but you'll need to log in with your Adafruit account to use it. If you don't already have an Adafruit login, create one here.

If you haven't used Adafruit IO before, check out this guide for more info.

Once you have logged into your account, there are two pieces of information you'll need to place in your secrets.py file: Adafruit IO username, and Adafruit IO key. Head to io.adafruit.com and simply click the View AIO Key link on the left hand side of the Adafruit IO page to get this information.

Then, add them to the secrets.py file like this:

Download: file
secrets = {
    'ssid' : 'your_wifi_ssid',
    'password' : 'your_wifi_password',
    'aio_username' : '_your_aio_username_',
    'aio_key' : '_your_big_huge_super_long_aio_key_'
}

CircuitPython Code

In the embedded code element below, click on the Download: Project Zip link, and save the .zip archive file to your computer.

Then, decompress the .zip file, it will unpack to a folder named PyPortal_Electioncal_US.

Copy the contents of the PyPortal_Electioncal_US directory to your PyPortal's CIRCUITPY drive, and then be sure to rename the electioncal.py file to code.py so it will automatically run when the PyPortal restarts.

This is what the final contents of the CIRCUITPY drive will look like:

import sys
import time
import board
from adafruit_pyportal import PyPortal
cwd = ("/"+__file__).rsplit('/', 1)[0] # the current working directory (where this file is)
sys.path.append(cwd)
import electioncal_graphics  # pylint: disable=wrong-import-position

# Get wifi details and more from a secrets.py file
try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise

# Change this to your state and county, replacing spaces for underscores and in lowercase
STATE="new_york"
COUNTY="new_york"

DATA_SOURCE = "https://electioncal.us/en/" + STATE +"/" + COUNTY + "/voter.json"
DATA_LOCATION = []

# Initialize the pyportal object and let us know what data to fetch and where
# to display it
pyportal = PyPortal(url=DATA_SOURCE,
                    json_path=DATA_LOCATION,
                    status_neopixel=board.NEOPIXEL,
                    default_bg=0x000000)


gfx = electioncal_graphics.Electioncal_Graphics(pyportal.splash, am_pm=True)
display_refresh = None
while True:
    # only query the online time once per hour (and on first run)
    if (not display_refresh) or (time.monotonic() - display_refresh) > 3600:
        try:
            print("Getting time from internet!")
            pyportal.get_local_time()
            display_refresh = time.monotonic()
        except RuntimeError as e:
            print("Some error occured, retrying! -", e)
            continue

        try:
            value = pyportal.fetch()
            #print("Response is", value)
            gfx.load_data(value)
        except RuntimeError as e:
            print("Some error occured, retrying! -", e)
            continue
    try:
        gfx.elections_cycle()
    except RuntimeError as e:
        print("Some error ocurred, retrying! -", e)
        continue
If you run into any errors, such as "ImportError: no module named `adafruit_display_text.label`" be sure to update your libraries to the latest release bundle!

How It Works

The PyPortal Electioncal US has a few steps it takes to provide you with the information desired. It has a boot-up screen, a nice simple background, and multiple fonts for displaying the election dates.

PyPortal Constructor

When setting up the pyportal constructor, we are providing it with these things:

  • url to query
  • json_path to traverse and find the key:value pairs we need
  • default_bg default background color

Background

First, it displays a bitmap graphic as the screen's startup background while getting ready to download the Election dates. This is a 320 x 240 pixel RGB 16-bit raster graphic in .bmp format.

Time

Next, the program connects through the WiFi to get the local time via the adafruit.io server, for which we will display today's date at the bottom of the screen, to give you better context of the election dates.

API Query and JSON

At the top of this page, you configured:

DATA_SOURCE = "https://electioncal.us/en/" + STATE +"/" + COUNTY + "/voter.json"

This, with the STATE and COUNTY you defined, will turn into something like (New York County, New York State in this example):

https://electioncal.us/en/new_york/new_york/voter.json

When this query is complete, it returns a JSON file that looks like this:

Download: file
{
  "version": 0,
  "name": null,
  "description": null,
  "dates": 
  	[
      {
        "date": "2020-04-28", 
        "name": "Presidential Primary",
        "original_date": "2020-04-28", 
        "state": "New York", 
        "county": "New York County",
        "key": "20200428", 
        "type": "election"
      }, 
      {
        "state": "New York", 
        "county": "New York County", 
        "name": "Mail voter registration",
        "type": "reminder",
        "election_key": "20200623",
        "subtype": "registration.received_by",
        "date": "2020-05-26", 
        "postmark_too_late": true,
        "deadline_date": "2020-06-03", 
        "explanation": "Voter registration must be received by June 3rd."
      }
    ]
}

Here is the same file beautified with the Firefox browser's built in tools (You can also use online code "beautifiers" such as https://codebeautify.org/jsonviewer or http://jsonviewer.stack.hu) :

Fetch

With the PyPortal set up, we can then use pyportal.fetch() and then from the screen object we call load_data() to load the JSON data to a way that is easy to pass through all the dates.

All of the heavy lifting of parsing that data and displaying it as text or bitmaps is done in the electioncal_graphics.py code.

JSON Traversal

The JSON file is formatted in a way that makes it easy to traverse the hierarchy and parse the data. In it, you'll see keys, such as state, county, name, type and date, and their respective values. So, here are some key : value pairs we care about:

  • state: "New York"
  • county: "New York County"
  • name: "Presidential Primary"
  • date: "2020-04-28"

In order to fetch this data from the file, we need to be able to describe their locations in the file hierarchically. This is helpful, for example, in differentiating between the 'name' value that you have in the Presidential Primary and the 'name' of the Mail voter registration. To avoid name clashing, we rely on JSON traversal.

In the electioncal_graphics.py file, you'll see how this is done. For example, the first date is found in  this hierarchy of the JSON file: ["dates"][0]["date"].

In this case the number 0 is quite important, as it has the sub-tree that is relevant for the first, date, but if you change it to a number such as 1 or 2, you will print the second and third date. Please note that the counter starts at zero.

Depending of the STATE and COUNTY you configured, you will have several election dates, so the program then tries to replace this number by [i], so that we can display not one date but many, and cycle them through the screen.

Font

The data is displayed as text created with bitmapped fonts to overlay on top of the background. The fonts used here are bitmap fonts made from the Arial typeface. You can learn more about converting type in this guide.

Graphics

Let's have a look at how the electioncal_graphics.py code places the elements on screen. Below, we can see the icon and text that are displayed. The items in quotes are the key names from the JSON file, and their values are what we see displayed using the CircuitPython label from the displayio library.

import time
import json
import displayio
from adafruit_display_text.label import Label
from adafruit_bitmap_font import bitmap_font

cwd = ("/"+__file__).rsplit('/', 1)[0] # the current working directory (where this file is)

small_font = cwd+"/fonts/Arial-12.bdf"
medium_font = cwd+"/fonts/Arial-16.bdf"
large_font = cwd+"/fonts/Arial-Bold-24.bdf"

class OpenWeather_Graphics(displayio.Group):
    def __init__(self, root_group, *, am_pm=True, celsius=True):
        super().__init__(max_size=2)
        self.am_pm = am_pm
        self.celsius = celsius

        root_group.append(self)
        self._icon_group = displayio.Group(max_size=1)
        self.append(self._icon_group)
        self._text_group = displayio.Group(max_size=5)
        self.append(self._text_group)

        self._icon_sprite = None
        self._icon_file = None
        self.set_icon(cwd+"/weather_background.bmp")

        self.small_font = bitmap_font.load_font(small_font)
        self.medium_font = bitmap_font.load_font(medium_font)
        self.large_font = bitmap_font.load_font(large_font)
        glyphs = b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.: '
        self.small_font.load_glyphs(glyphs)
        self.medium_font.load_glyphs(glyphs)
        self.large_font.load_glyphs(glyphs)
        self.large_font.load_glyphs(('°',))  # a non-ascii character we need for sure
        self.city_text = None

        self.time_text = Label(self.medium_font, max_glyphs=8)
        self.time_text.x = 200
        self.time_text.y = 12
        self.time_text.color = 0xFFFFFF
        self._text_group.append(self.time_text)

        self.temp_text = Label(self.large_font, max_glyphs=6)
        self.temp_text.x = 200
        self.temp_text.y = 195
        self.temp_text.color = 0xFFFFFF
        self._text_group.append(self.temp_text)

        self.main_text = Label(self.large_font, max_glyphs=20)
        self.main_text.x = 10
        self.main_text.y = 195
        self.main_text.color = 0xFFFFFF
        self._text_group.append(self.main_text)

        self.description_text = Label(self.small_font, max_glyphs=60)
        self.description_text.x = 10
        self.description_text.y = 225
        self.description_text.color = 0xFFFFFF
        self._text_group.append(self.description_text)

    def display_weather(self, weather):
        weather = json.loads(weather)

        # set the icon/background
        weather_icon = weather['weather'][0]['icon']
        self.set_icon(cwd+"/icons/"+weather_icon+".bmp")

        city_name =  weather['name'] + ", " + weather['sys']['country']
        print(city_name)
        if not self.city_text:
            self.city_text = Label(self.medium_font, text=city_name)
            self.city_text.x = 10
            self.city_text.y = 12
            self.city_text.color = 0xFFFFFF
            self._text_group.append(self.city_text)

        self.update_time()

        main_text = weather['weather'][0]['main']
        print(main_text)
        self.main_text.text = main_text

        temperature = weather['main']['temp'] - 273.15 # its...in kelvin
        print(temperature)
        if self.celsius:
            self.temp_text.text = "%d °C" % temperature
        else:
            self.temp_text.text = "%d °F" % ((temperature * 9 / 5) + 32)

        description = weather['weather'][0]['description']
        description = description[0].upper() + description[1:]
        print(description)
        self.description_text.text = description
        # "thunderstorm with heavy drizzle"

    def update_time(self):
        """Fetch the time.localtime(), parse it out and update the display text"""
        now = time.localtime()
        hour = now[3]
        minute = now[4]
        format_str = "%d:%02d"
        if self.am_pm:
            if hour >= 12:
                hour -= 12
                format_str = format_str+" PM"
            else:
                format_str = format_str+" AM"
            if hour == 0:
                hour = 12
        time_str = format_str % (hour, minute)
        print(time_str)
        self.time_text.text = time_str

    def set_icon(self, filename):
        """The background image to a bitmap file.

        :param filename: The filename of the chosen icon

        """
        print("Set icon to ", filename)
        if self._icon_group:
            self._icon_group.pop()

        if not filename:
            return  # we're done, no icon desired
        if self._icon_file:
            self._icon_file.close()
        self._icon_file = open(filename, "rb")
        icon = displayio.OnDiskBitmap(self._icon_file)
        try:
            self._icon_sprite = displayio.TileGrid(icon,
                                                   pixel_shader=displayio.ColorConverter())
        except TypeError:
            self._icon_sprite = displayio.TileGrid(icon,
                                                   pixel_shader=displayio.ColorConverter(),
                                                   position=(0,0))
        self._icon_group.append(self._icon_sprite)

Text Position

Depending on the design of your background bitmap and the length of the text you're displaying, you may want to reposition the text and caption.

The PyPortal's display is 320 pixels wide and 240 pixels high. In order to refer to those positions on the screen, we use an x/y coordinate system, where x is horizontal and y is vertical.

The origin of this coordinate system is the upper left corner. This means that a pixel placed at the upper left corner would be (0,0) and the lower right corner would be (320, 240).

So, if you wanted to move the subscriber count text to the right and up closer to the top, your code may look like this for that part of the pyportal constructor: text_position=(250, 10)

Text Color

Another way to customize your display is to adjust the color of the text. The line text_color=0xFFFFFF in the constructor shows how. You will need to use the hexadecimal value for any color you want to display.

You can use something like https://htmlcolorcodes.com/ to pick your color and then copy the hex value, in this example it would be 0x0ED9EE

Now, we get ready to assemble the case on the next page.

This guide was first published on Jul 07, 2020. It was last updated on Jul 07, 2020.
This page (Code PyPortal with CircuitPython) was last updated on Sep 16, 2020.