Text Editor

Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.

Alternatively, you can use any text editor that saves simple text files.

Code

Click the Download: Project Zip File link below in the code window to get a zip file with all the files needed for the project. Copy code.py from the zip file and place on the CIRCUITPY drive.

You'll also need to place the events.json file at the root level of your CIRCUITPY drive so it can be accessed by the code.

Also copy the whole /bmps directory from the zip file and place and its contents it on the CIRCUITPY drive. These are some sample images you can start with.

Copy the /fonts directory from the zip file and place and its contents it on the CIRCUITPY drive.

Once all the files are on the MagTag CIRCUITPY drive, the directory structure should be the same as the listing below. If not, ensure you've got all the files noted in prior steps.

# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT

# MagTag Showtimes Event Viewer
# Uses the events.json file to display next or current event
# Be sure to put WiFi access point info in secrets.py file to connect

import time
import json
import re
from adafruit_magtag.magtag import MagTag

# You can test by setting a time.struct here, to pretend its a different day
# (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst)
FAKETIME = False  # time.struct_time(2020, 12, 11,     15, 01, 00,    4, 346, -1)

BEEP_ON_EVENTSTART = True   # beep when the event begins?
EVENT_FILE = "events.json"  # file containing events
USE_24HR_TIME = False   # True for 24-hr time on display, false for 12 hour (am/pm) time

magtag = MagTag()
magtag.add_text(
    text_font="/fonts/Arial-Bold-12.pcf",
    text_color=0xFFFFFF,
    text_position=(2, 112),
    text_anchor_point=(0, 0),
)

# According to Python, monday is index 0...this array will help us track it
day_names = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
events = None
with open(EVENT_FILE, 'r') as evfile:
    events = json.load(evfile)

# validate data
for i, event in enumerate(events):
    if not event.get('name'):
        raise RuntimeError("No name in event %d" % i)
    if not event.get('day_of_week') or event['day_of_week'] not in day_names:
        raise RuntimeError("Invalid day of week for event '%s'" % event['name'])
    r = re.compile('[0-2]?[0-9]:[0-5][0-9]')
    if not event.get('start_time') or not r.match(event['start_time']) :
        raise RuntimeError("Invalid start time for event '%s'" % event['name'])
    if not event.get('end_time') or not r.match(event['end_time']) :
        raise RuntimeError("Invalid end time for event '%s'" % event['name'])

print(events)

now = None
if not FAKETIME:
    magtag.network.connect()
    magtag.get_local_time()
    now = time.localtime()
else:
    now = FAKETIME

print("Now: ", now)

# Helper to convert times into am/pm times
def time_format(timestr):
    if USE_24HR_TIME:
        return timestr
    hr, mn = [int(x) for x in timestr.split(":")]
    if hr > 12:
        return "%d:%02d PM" % (hr-12, mn)
    elif hr > 0:
        return "%d:%02d AM" % (hr, mn)
    else:
        return "12:%02d AM" % (mn)

# find next event!
remaining_starttimes = []
remaining_endtimes = []
current_event = None
for event in events:
    days_till_event = (day_names.index(event["day_of_week"]) - now[6] + 7) % 7

    # now figure out minutes until event
    eventstart_hr, eventstart_min = event["start_time"].split(":")
    eventstart_time_in_minutes = int(eventstart_hr) * 60 + int(eventstart_min)
    # we'll also track when it ends
    eventend_hr, eventend_min = event["end_time"].split(":")
    eventend_time_in_minutes = int(eventend_hr) * 60 + int(eventend_min)

    current_time_in_minutes = now[3] * 60 + now[4]
    print(
        "\tEvent start is at",
        eventstart_time_in_minutes,
        "now is",
        current_time_in_minutes,
    )
    minutes_till_eventstart = eventstart_time_in_minutes - current_time_in_minutes
    minutes_till_eventend = eventend_time_in_minutes - current_time_in_minutes

    # add the number of days to minutes:
    minutes_till_eventstart += days_till_event * 24 * 60
    minutes_till_eventend += days_till_event * 24 * 60

    # if time is negative, that means it already happened today, so skip ahead
    if minutes_till_eventstart < 0:
        minutes_till_eventstart += 7 * 24 * 60
    if minutes_till_eventend < 0:
        minutes_till_eventend += 7 * 24 * 60

    print(
        "\t%d minutes till start, %d minutes till end"
        % (minutes_till_eventstart, minutes_till_eventend)
    )
    if (minutes_till_eventstart == 0) or (
            minutes_till_eventend < minutes_till_eventstart
    ):
        current_event = event

    # now we can back-calculate when the event is for our debugging
    days = minutes_till_eventstart // (24 * 60)
    hrs = (minutes_till_eventstart - days * (24 * 60)) // 60
    mins = minutes_till_eventstart % 60
    print(event["name"], "starts in", days, "days", hrs, "hours and", mins, "minutes\n")

    remaining_starttimes.append(minutes_till_eventstart)
    remaining_endtimes.append(minutes_till_eventend)

mins_till_next_eventstart = min(remaining_starttimes)
mins_till_next_eventend = min(remaining_endtimes)
next_up = events[remaining_starttimes.index(mins_till_next_eventstart)]

# OK find the one with the smallest minutes remaining
sleep_time = None
if current_event:
    print("Currently: ", current_event)
    magtag.set_background("/bmps/"+current_event["graphic"])
    magtag.set_text("Currently streaming until " + time_format(current_event["end_time"]))
    remaining_starttimes.index(mins_till_next_eventstart)
    if BEEP_ON_EVENTSTART:
        for _ in range(3):
            magtag.peripherals.play_tone(1760, 0.1)
            time.sleep(0.2)
    sleep_time = mins_till_next_eventend + 1
else:
    print("Next up! ", next_up)
    magtag.set_background("/bmps/"+next_up["graphic"])

    string = (
        "Coming up on "
        + next_up["day_of_week"]
        + " at "
        + time_format(next_up["start_time"])
    )
    magtag.set_text(string)
    sleep_time = mins_till_next_eventstart

print("Sleeping for %d minutes" % sleep_time)
time.sleep(2)
magtag.exit_and_deep_sleep(sleep_time * 60)

Events File

In order to make it easier to edit your events, we put all of the event data into a separate .json file that the main code will access.

Here's what the event file looks like:

[
  {
    "name": "JP's Product Pick of the Week",
    "day_of_week": "Tuesday",
    "graphic": "jpp.bmp",
    "start_time": "13:00",
    "end_time": "13:30"
  },
  {
    "name": "3D Hangouts",
    "day_of_week": "Wednesday",
    "graphic": "3dh.bmp",
    "start_time": "8:00",
    "end_time": "9:00"
  },
  {
    "name": "Show & Tell",
    "day_of_week": "Wednesday",
    "graphic": "snt.bmp",
    "start_time": "16:30",
    "end_time": "17:00"
  },
  {
    "name": "Ask An Engineer",
    "day_of_week": "Wednesday",
    "graphic": "aae.bmp",
    "start_time": "17:00",
    "end_time": "18:00"
  },
  {
    "name": "John Park's Workshop",
    "day_of_week": "Thursday",
    "graphic": "jpw.bmp",
    "start_time": "13:00",
    "end_time": "14:00"
  },
  {
    "name": "Scott's Deep Dive",
    "day_of_week": "Friday",
    "graphic": "dds.bmp",
    "start_time": "14:00",
    "end_time": "15:00"
  }
]

How It Works

Libraries

First we import some libraries to help out, including time, json (for parsing our event data from json file format), re (regular expression), and the adafruit_magtag library, which will make it simple to display graphics and text on the MagTag's e-Ink display.

import time
import json
import re
from adafruit_magtag.magtag import MagTag

Faketime

When you're testing code that is based on a date and time it can be a bit inconvenient to wait around for that exact moment when your event should begin! So, we can use a FAKETIME variable when necessary. In normal use, this will be set to False.

The comment shows how to use it when you need to fake things out.

# (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_wday, tm_yday, tm_isdst)
FAKETIME = False  # time.struct_time(2020, 12, 11,     15, 01, 00,    4, 346, -1)

Variables

These other variables are set up for convenience -- you can turn the beep on and off, set the name of the events.json file, and decide if you're going to display 24 hour time or not here.

BEEP_ON_EVENTSTART = True   # beep when the event begins?
EVENT_FILE = "events.json"  # file containing events
USE_24HR_TIME = False   # True for 24-hr time on display, false for 12 hour (am/pm) time

Setup

Next we'll take care of MagTag setup, and set a text object in place.

The day_names and events variables will help us print nice day names and keep track of event state.

# According to Python, monday is index 0...this array will help us track it
day_names = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
events = None

Event File

We'll open the events.json file and then validate the data, printing it to the serial output. Here you can see regular expressions being used to parse out some of the data. It's helpful to watch the serial output while this runs to see how the data is parsed.

with open(EVENT_FILE, 'r') as evfile:
    events = json.load(evfile)

# validate data
for i, event in enumerate(events):
    if not event.get('name'):
        raise RuntimeError("No name in event %d" % i)
    if not event.get('day_of_week') or event['day_of_week'] not in day_names:
        raise RuntimeError("Invalid day of week for event '%s'" % event['name'])
    r = re.compile('[0-2]?[0-9]:[0-5][0-9]')
    if not event.get('start_time') or not r.match(event['start_time']) :
        raise RuntimeError("Invalid start time for event '%s'" % event['name'])
    if not event.get('end_time') or not r.match(event['end_time']) :
        raise RuntimeError("Invalid end time for event '%s'" % event['name'])

print(events)

Time Check

Next we'll use the MagTag's network connection to check the local time (unless FAKETIME is being used). We also create a time_format() function to help with time formatting.

now = None
if not FAKETIME:
    magtag.network.connect()
    magtag.get_local_time()
    now = time.localtime()
else:
    now = FAKETIME

print("Now: ", now)
def time_format(timestr):
    if USE_24HR_TIME:
        return timestr
    hr, mn = [int(x) for x in timestr.split(":")]
    if hr > 12:
        return "%d:%02d PM" % (hr-12, mn)
    elif hr > 0:
        return "%d:%02d AM" % (hr, mn)
    else:
        return "12:%02d AM" % (mn)

Find Next Event

Now that we know when the events are and what time it is, this block of code determines which event is next.

remaining_starttimes = []
remaining_endtimes = []
current_event = None
for event in events:
    days_till_event = (day_names.index(event["day_of_week"]) - now[6] + 7) % 7

    # now figure out minutes until event
    eventstart_hr, eventstart_min = event["start_time"].split(":")
    eventstart_time_in_minutes = int(eventstart_hr) * 60 + int(eventstart_min)
    # we'll also track when it ends
    eventend_hr, eventend_min = event["end_time"].split(":")
    eventend_time_in_minutes = int(eventend_hr) * 60 + int(eventend_min)

    current_time_in_minutes = now[3] * 60 + now[4]
    print(
        "\tEvent start is at",
        eventstart_time_in_minutes,
        "now is",
        current_time_in_minutes,
    )
    minutes_till_eventstart = eventstart_time_in_minutes - current_time_in_minutes
    minutes_till_eventend = eventend_time_in_minutes - current_time_in_minutes

    # add the number of days to minutes:
    minutes_till_eventstart += days_till_event * 24 * 60
    minutes_till_eventend += days_till_event * 24 * 60

    # if time is negative, that means it already happened today, so skip ahead
    if minutes_till_eventstart < 0:
        minutes_till_eventstart += 7 * 24 * 60
    if minutes_till_eventend < 0:
        minutes_till_eventend += 7 * 24 * 60

    print(
        "\t%d minutes till start, %d minutes till end"
        % (minutes_till_eventstart, minutes_till_eventend)
    )
    if (minutes_till_eventstart == 0) or (
            minutes_till_eventend < minutes_till_eventstart
    ):
        current_event = event

    # now we can back-calculate when the event is for our debugging
    days = minutes_till_eventstart // (24 * 60)
    hrs = (minutes_till_eventstart - days * (24 * 60)) // 60
    mins = minutes_till_eventstart % 60
    print(event["name"], "starts in", days, "days", hrs, "hours and", mins, "minutes\n")

    remaining_starttimes.append(minutes_till_eventstart)
    remaining_endtimes.append(minutes_till_eventend)

mins_till_next_eventstart = min(remaining_starttimes)
mins_till_next_eventend = min(remaining_endtimes)
next_up = events[remaining_starttimes.index(mins_till_next_eventstart)]

Background Images and Text

Whichever event has is coming up the soonest will get its background graphic displayed, as well as the "Next up!" text. If the event is in progress, the text will display that it is "Currently streaming until..." the end time.

sleep_time = None
if current_event:
    print("Currently: ", current_event)
    magtag.set_background("bmps/"+current_event["graphic"])
    magtag.set_text("Currently streaming until " + time_format(current_event["end_time"]))
    remaining_starttimes.index(mins_till_next_eventstart)
    if BEEP_ON_EVENTSTART:
        for _ in range(3):
            magtag.peripherals.play_tone(1760, 0.1)
            time.sleep(0.2)
    sleep_time = mins_till_next_eventend + 1
else:
    print("Next up! ", next_up)
    magtag.set_background("bmps/"+next_up["graphic"])

    string = (
        "Coming up on "
        + next_up["day_of_week"]
        + " at "
        + time_format(next_up["start_time"])
    )
    magtag.set_text(string)

Sleep

In order to save battery power, the MagTag can go into deep sleep mode. The e-Ink display will stay the same, so you'll hardly know! The code will determine how long it should sleep so that it can wake up in time to change the display for the next upcoming event!

sleep_time = mins_till_next_eventstart

print("Sleeping for %d minutes" % sleep_time)
time.sleep(2)
magtag.exit_and_deep_sleep(sleep_time * 60)

Now, you're ready to go, and you'll know when the next show starts!

Or, you can customize the events.json file and graphics to use with your own recurring calendar events!

This guide was first published on Dec 16, 2020. It was last updated on Dec 16, 2020.

This page (Code the MagTag Showtimes Display) was last updated on Mar 03, 2023.

Text editor powered by tinymce.