Our simple flashcard app already has some advantages over physical cards. But there are other features we could add. We might want to narrow down the cards we want to study, or sort them into topics, and it would be nice to revisit cards that we got wrong and want to study again. We're also missing a lot of basic UI features like labels and feedback.

In this expanded example, we'll add some new features to make this app a little more advanced, such as:

  • A new JSON format to support multiple "chapters" of a deck
  • A simple menu, allowing us to pick specific parts of our deck to study from
  • New text areas to label the buttons and give the user more directions
  • NeoPixel feedback after a user hits a button (a nice-to-have, since the e-paper screen is sloooow)
  • Automatically add cards the user marks as "forgotten" back into the deck to be studied again.

Download the Software

Download the code by clicking the "Download: Project Zip" and copy the files to the CIRCUITPY flash drive that appears on your computer when you plug your MagTag in to your computer via a known good USB cable.

Here's the program we'll be running on the code.py file in the CIRCUITPY drive.

import time
import json
import terminalio
import digitalio
import random
from adafruit_display_shapes.rect import Rect
from adafruit_magtag.magtag import MagTag
magtag = MagTag()

# ---------------------------------
# Prepare text regions
# ---------------------------------

# Fetch list of chapters
MAX_LLEN = 8
data = {}
with open("deck.json") as fp:
    data = json.load(fp)
chap_list = list(data.keys())
num_chap = len(chap_list)
list_len = min(num_chap,MAX_LLEN)

# Print list of chapters
for i in range(list_len):
    magtag.add_text(
        text_font=terminalio.FONT,
        text_position=(10, 3+(i*10)),
        line_spacing=1.0,
        text_anchor_point=(0, 0), # Top left
        is_data=False,            # Text will be set manually
    )
    if i == 0:
        magtag.set_text("> " + chap_list[i], i, auto_refresh=False)
    else:
        magtag.set_text("  " + chap_list[i], i, auto_refresh=False)

# Add button labels at the bottom of the screen
BUTTON_TEXT_IDX = list_len
magtag.graphics.splash.append(Rect(0, magtag.graphics.display.height - 14,
                                   magtag.graphics.display.width,
                                   magtag.graphics.display.height, fill=0x0))
magtag.add_text(
    text_font=terminalio.FONT,
    text_position=(3, magtag.graphics.display.height - 14),
    text_color=0xFFFFFF,
    line_spacing=1.0,
    text_anchor_point=(0, 0), # Top left
    is_data=False,            # Text will be set manually
)
magtag.set_text("Select        Up          Down        Begin",
                BUTTON_TEXT_IDX, auto_refresh=False)

# Add message label at the top of the screen
MSG_TEXT_IDX = list_len + 1
magtag.add_text(
    text_font=terminalio.FONT,
    text_position=(3, magtag.graphics.display.height - 30),
    line_spacing=1.0,
    text_anchor_point=(0, 0), # Top left
    is_data=False,            # Text will be set manually
)
magtag.set_text("Press Begin to default to all chapters", MSG_TEXT_IDX)

# Empty text region for card displays
CARD_TEXT_IDX = list_len + 2
magtag.add_text(
    text_font="yasashi20.pcf",
    text_position=(
        magtag.graphics.display.width // 2,
        magtag.graphics.display.height // 2,
    ),
    line_spacing=0.85,
    text_anchor_point=(0.5, 0.5),
)

# Button management
curr_btns = [False] * 4
prev_btns = [False] * 4
BTN_A = 0
BTN_B = 1
BTN_C = 2
BTN_D = 3
def update_button(idx, pressed):
    curr_btns[idx] = pressed
    if curr_btns[idx] and not prev_btns[idx]:
        print("Exit menu")
        return True
    prev_btns[idx] = curr_btns[idx]
    return False

# Cursor settings
cursor_pos = 0
list_offset = 0
selected = [False] * num_chap
btn_updated = False

# ---------------------------------
# Program Loop
# ---------------------------------

while True:

    # ---------------------------------
    # Chapter Select
    # ---------------------------------

    while True:
        if btn_updated:
            # Clear default message only when items are selected
            if any(selected):
                magtag.set_text("", MSG_TEXT_IDX, auto_refresh=False)
            else:
                magtag.set_text("Press Begin to default to all chapters",
                                MSG_TEXT_IDX, auto_refresh=False)

            magtag.peripherals.neopixels.fill((128, 0, 0))
            for i in range(list_len):
                prefix = ""
                if i == cursor_pos:
                    prefix += ">"
                else:
                    prefix += " "
                if selected[i + list_offset]:
                    prefix += "*"
                else:
                    prefix += " "
                magtag.set_text(prefix + chap_list[i+list_offset],
                                i, auto_refresh=False)
            magtag.refresh()
            magtag.peripherals.neopixels.fill((0, 0, 0))
            btn_updated = False
        # UP
        if update_button(BTN_B, magtag.peripherals.button_b_pressed):
            cursor_pos -= 1
            btn_updated = True
        # DOWN
        if update_button(BTN_C, magtag.peripherals.button_c_pressed):
            cursor_pos += 1
            btn_updated = True
        # SELECT
        if update_button(BTN_A, magtag.peripherals.button_a_pressed):
            selected[cursor_pos + list_offset] = not selected[cursor_pos + list_offset]
            btn_updated = True
        # BEGIN
        if update_button(BTN_D, magtag.peripherals.button_d_pressed):
            # if nothing was selected, default to all decks
            magtag.peripherals.neopixels.fill((128, 0, 0))
            if not any(selected):
                selected = [True] * list_len
            break
        # detect if you're past the list bounds
        if cursor_pos == MAX_LLEN:
            cursor_pos = MAX_LLEN - 1
            if (num_chap - list_offset - 1) > MAX_LLEN:
                list_offset += 1

        if cursor_pos == -1:
            cursor_pos = 0
            if list_offset > 0:
                list_offset -= 1

    # ---------------------------------
    # Deck Loop
    # ---------------------------------

    # Clear the menu and message box
    for i in range(list_len):
        magtag.set_text("", i, auto_refresh=False)
    magtag.set_text("", MSG_TEXT_IDX,auto_refresh=False)

    # Grab the cards from the chapters we want, and shuffle them
    cards = []
    for i in range(len(selected)):
        if selected[i]:
            cards.extend(data[chap_list[i]])
    cards = sorted(cards, key=lambda _: random.random())

    # make a separate holding deck for cards the user gets wrong
    forgotten_cards = []

    exit_called = False
    while True:
        for card in cards:
            magtag.set_text("Exit          --          --          Turn Over",
                            BUTTON_TEXT_IDX,auto_refresh=False)
            text = '\n'.join(magtag.wrap_nicely(card[0], 11))
            magtag.set_text(text, CARD_TEXT_IDX)
            magtag.peripherals.neopixels.fill((0, 0, 0))

            while True:
                # EXIT
                if update_button(BTN_A, magtag.peripherals.button_a_pressed):
                    exit_called = True
                    break
                # TURN
                if update_button(BTN_D, magtag.peripherals.button_d_pressed):
                    break
            magtag.peripherals.neopixels.fill((128, 0, 0))
            if exit_called:
                break

            magtag.set_text("Exit          --          Forgot      Good",
                            BUTTON_TEXT_IDX,auto_refresh=False)
            text = '\n'.join(magtag.wrap_nicely(card[1], 11))
            text += '\n'
            text += '\n'.join(magtag.wrap_nicely(card[2], 20))
            magtag.set_text(text, CARD_TEXT_IDX)
            magtag.peripherals.neopixels.fill((0, 0, 0))

            while True:
                # EXIT
                if update_button(BTN_A, magtag.peripherals.button_a_pressed):
                    exit_called = True
                    break
                # FORGOT
                if update_button(BTN_C, magtag.peripherals.button_c_pressed):
                    forgotten_cards.append(card)
                    break
                # GOOD
                if update_button(BTN_D, magtag.peripherals.button_d_pressed):
                    break
            magtag.peripherals.neopixels.fill((128, 0, 0))
            if exit_called:
                break
            # Next card
        # If there were forgotten cards, make them the new deck and restart
        if forgotten_cards:
            cards = forgotten_cards
            forgotten_cards = []
        else:
            break

    # ---------------------------------
    # Complete and Reset
    # ---------------------------------

    # Show completion text if deck was finished
    if not exit_called:
        magtag.set_text("--            --          --          --",
                        BUTTON_TEXT_IDX,auto_refresh=False)
        magtag.set_text("Complete!", CARD_TEXT_IDX)
    else:
        exit_called = False

    # Clear and reprint list of chapters
    magtag.set_text("", CARD_TEXT_IDX, auto_refresh=False)
    for i in range(list_len):
        if i == 0:
            magtag.set_text("> " + chap_list[i], i, auto_refresh=False)
        else:
            magtag.set_text("  " + chap_list[i], i, auto_refresh=False)
    magtag.set_text("Select        Up          Down        Begin",
                    BUTTON_TEXT_IDX, auto_refresh=False)
    magtag.set_text("Press Begin to default to all chapters", MSG_TEXT_IDX)

    # Reset cursor:
    cursor_pos = 0
    list_offset = 0
    selected = [False] * list_len
    btn_updated = False

    # Done resetting, return to chapter selection
    magtag.peripherals.neopixels.fill((0, 0, 0))

New Deck Format

For our new deck, we want to start organizing cards by "chapter" - like different topics in a language, or different chapters in a textbook. To do this, we use the key-value syntax in JSON.

Instead of being a big list of smaller lists, the top level is now equivalent to a Python Dictionary, using curly brackets {}. Each chapter name has a list of cards associated with it. In our final program, we'll be able to study chapters by themselves or combine them together using the list of chapter names.

{
    "Everyday Phrases":[
        ["You're Welcome", "どういたしまして", "Dō Itashimashite"],
        ["Good Morning", "おはよう ございます", "Ohayō gozaimasu"],
        ["Yes", "はい", "Hai"],
        ["No", "いいえ", "Iie"],
        ["Hello", "こんにちは", "Konnichi wa"],
        ["Please", "おねがい します", "Onegai Shimasu"],
        ["Excuse Me", "すみません", "Sumimasen"],
        ["Thank You", "ありがとう", "Arigatō"],
    ],
    "Days of the Week":[
        ["Monday","げつ ようび","Getsu yōbi"],
        ["Tuesday","か ようび","Ka yōbi"],
        ["Wednesday","すい ようび","Sui yōbi"],
        ["Thursday","もく ようび","Moku yōbi"],
        ["Friday","きん ようび","Kin yōbi"],
        ["Saturday","ど ようび","Do yōbi"],
        ["Sunday","にち ようび","Nichi yōbi"],
    ],
    "Animals":[
        ["Dog","いぬ","Inu"],
        ["Cat","ねこ","Neko"],
        ["Horse","うま","Uma"],
        ["Monkey","さる","Saru"],
        ["Elephant","ぞう","Zō"],
        ["Rabbit","うさぎ","Usagi"],
    ]
}

Program Flow

This program has a couple of different modes, so we'll go over how it works first.

The user starts by seeing a list of the chapters in their deck, along with a line of button labels. Hitting the Begin button right away will simply combine all the cards into one big study session and start up, but they can also pick specific chapters to focus on.

Users can move up and down the list with the arrow buttons, and select different chapters with the select button. Once they've picked the chapters you want to study, the Begin button will start a session for only cards from those chapters.

After a user turns a card over, they can press the "Forgot" button to mark it as incorrect. Forgotten cards automatically get added to the end of the deck, so once they've finished the original set, they'll need to study those cards again.

Once every card has been marked "good", the session is complete! A congratulatory message shows, and the user returns to the menu.

Code Walkthrough

Setup:

Just like the previous version, we start by importing the libraries we need, but note that we're using a new one, Adafruit Shapes, which will be a part of our background display.

import time
import json
import terminalio
import digitalio
import random
from adafruit_display_shapes.rect import Rect
from adafruit_magtag.magtag import MagTag
magtag = MagTag()

The file import section comes with some extra steps.

If the user has a LOT of chapters, we won't be able to fit them all on the screen at once, so we set a maximum number of lists to display at one time with MAX_LLEN. Lists over this number won't be shown unless the list is scrolled down.

We're going to start off by displaying all the chapters in a list, so we extract their names into a variable called chap_list. We also store the total number of chapters with num_chap.

Finally, if the list of chapters is shorter than MAX_LLEN, we won't need to scroll. So we find the actual length of the list we're displaying by comparing the two variables and picking the minimum.

MAX_LLEN = 8
data = {}
with open("deck.json") as fp:
    data = json.load(fp)
chap_list = list(data.keys())
num_chap = len(chap_list)
list_len = min(num_chap,MAX_LLEN)

After importing the deck, we set up the various text regions. We create one for each item in the menu list, and then add on the button labels, background shape, and a special message region for telling the user about the "default" option if they don't actually pick any chapters.

We also create an empty text region that will eventually hold the flashcards themselves. Since the user moves back and forth between the menu selection and the cards, we won't actually delete any objects when we change modes - instead, we'll just fill the menu-specific text fields with empty strings when in flashcard mode, and vice versa for menu mode.

# Print list of chapters
for i in range(list_len):
    magtag.add_text(
        text_font=terminalio.FONT,
        text_position=(10, 3+(i*10)),
        line_spacing=1.0,
        text_anchor_point=(0, 0), # Top left
        is_data=False,            # Text will be set manually
    )
    if i == 0:
        magtag.set_text("> " + chap_list[i], i, auto_refresh=False)
    else:
        magtag.set_text("  " + chap_list[i], i, auto_refresh=False)

# Add button labels at the bottom of the screen
BUTTON_TEXT_IDX = list_len
magtag.graphics.splash.append(Rect(0, magtag.graphics.display.height - 14,
                                   magtag.graphics.display.width,
                                   magtag.graphics.display.height, fill=0x0))
magtag.add_text(
    text_font=terminalio.FONT,
    text_position=(3, magtag.graphics.display.height - 14),
    text_color=0xFFFFFF,
    line_spacing=1.0,
    text_anchor_point=(0, 0), # Top left
    is_data=False,            # Text will be set manually
)
magtag.set_text("Select        Up          Down        Begin", BUTTON_TEXT_IDX, auto_refresh=False)

# Add message label at the top of the screen
MSG_TEXT_IDX = list_len + 1
magtag.add_text(
    text_font=terminalio.FONT,
    text_position=(3, magtag.graphics.display.height - 30),
    line_spacing=1.0,
    text_anchor_point=(0, 0), # Top left
    is_data=False,            # Text will be set manually
)
magtag.set_text("Press Begin to default to all chapters", MSG_TEXT_IDX)

# Empty text region for card displays
CARD_TEXT_IDX = list_len + 2
magtag.add_text(
    text_font="yasashi20.pcf",
    text_position=(
        magtag.graphics.display.width // 2,
        magtag.graphics.display.height // 2,
    ),
    line_spacing=0.85,
    text_anchor_point=(0.5, 0.5),
)

Remember the button code from the simple flashcards example? We don't need to change it much, but we do need to support all 4 buttons rather than just one. So we adapt the old code into a new function that can determine out the button status by index (unfortunately, the magtag library attributes aren't indexable, so you still need to pass those in too).

# Button management
curr_btns = [False] * 4
prev_btns = [False] * 4
BTN_A = 0
BTN_B = 1
BTN_C = 2
BTN_D = 3
def update_button(idx, pressed):
    curr_btns[idx] = pressed
    if curr_btns[idx] and not prev_btns[idx]:
        print("Exit menu")
        return True
    prev_btns[idx] = curr_btns[idx]
    return False

As the final part of the setup process, we need some miscellaneous variables like the cursor location, the scrolling offset for long lists, what list items have been selected, and whether any buttons have been updated. 

cursor_pos = 0
list_offset = 0
selected = [False] * num_chap
btn_updated = False

Chapter Selection:

Now we can get started with the main program loop. The first screen the user sees is the chapter selection, where they can move a cursor up and down to select chapters. We've already printed out all the text on this screen in the setup stage, so this is basically just a big loop to read buttons.

We have four different button detectors. The Up and Down buttons change the cursor position, Select changes the status of the chapter in the Selected array, and Begin signals to exit the loop and start studying flashcards (combining all of the chapters, if nothing was selected).

# UP
if update_button(BTN_B, magtag.peripherals.button_b_pressed):
    cursor_pos -= 1
    btn_updated = True
# DOWN
if update_button(BTN_C, magtag.peripherals.button_c_pressed):
    cursor_pos += 1
    btn_updated = True
# SELECT
if update_button(BTN_A, magtag.peripherals.button_a_pressed):
    selected[cursor_pos + list_offset] = not selected[cursor_pos + list_offset]
    btn_updated = True
# BEGIN
if update_button(BTN_D, magtag.peripherals.button_d_pressed):
    # if nothing was selected, default to all decks
    magtag.peripherals.neopixels.fill((128, 0, 0))
    if not any(selected):
        selected = [True] * list_len
    break

If a button gets pressed, the program goes over the list of text areas and makes any required changes, like moving the cursor, adding * asterisks to selected chapters, and setting the current user message, all before refreshing the e-paper.

if btn_updated:
    # Clear default message only when items are selected
    if any(selected):
        magtag.set_text("", MSG_TEXT_IDX, auto_refresh=False)
    else:
        magtag.set_text("Press Begin to default to all chapters",
                        MSG_TEXT_IDX, auto_refresh=False)

    magtag.peripherals.neopixels.fill((128, 0, 0))
    for i in range(list_len):
        prefix = ""
        if i == cursor_pos:
            prefix += ">"
        else:
            prefix += " "
        if selected[i + list_offset]:
            prefix += "*"
        else:
            prefix += " "
        magtag.set_text(prefix + chap_list[i+list_offset],
                        i, auto_refresh=False)
    magtag.refresh()
    magtag.peripherals.neopixels.fill((0, 0, 0))
    btn_updated = False

What's with the neopixel code? Since the e-paper updates slowly in comparison to how quickly we can push buttons, it's nice to give the user a little feedback that they've actually started an action.

So every time a button is pressed, we turn on the neopixels, and once the e-paper is finished updating and the buttons are ready to be pushed again, we turn it off. 

magtag.peripherals.neopixels.fill((128, 0, 0))
magtag.peripherals.neopixels.fill((0, 0, 0))

Finally, if the chapter list is over the maximum list length, the user can scroll. This will only happen when the cursor is at the very end or very beginning of the list, and it doesn't affect things like chapter selection, which is the same no matter how the list is offset.

if cursor_pos == MAX_LLEN:
    cursor_pos = MAX_LLEN - 1
    if (num_chap - list_offset - 1) > MAX_LLEN:
        list_offset += 1

if cursor_pos == -1:
    cursor_pos = 0
    if list_offset > 0:
        list_offset -= 1

Flashcard Session:

Once a user picks a chapter and hits begin, they move into the deck loop. This is where flashcards get displayed, and it's similar to the basic example earlier in this chapter, with a few additions. First, we clear all the text from the menu mode:

# Clear the menu and message box
for i in range(list_len):
    magtag.set_text("", i, auto_refresh=False)
magtag.set_text("", MSG_TEXT_IDX,auto_refresh=False)

Then, we create a list of cards for this specific session, by combining all the cards from the chapters that were selected in the menu, and shuffling them.

cards = []
for i in range(len(selected)):
    if selected[i]:
        cards.extend(data[chap_list[i]])
cards = sorted(cards, key=lambda _: random.random())

In python, you can't add to a list while you're iterating through it. Since we want to keep extending the deck with cards that the user forgot, we'll create a temporary holding list called "forgotten_cards", and add it on later. We'll also create a variable to detect whether the user wants to give up and go back to the menu.

forgotten_cards = []
exit_called = False

Then, we enter the card loop. This is almost the same as the simple example. The only differences are that the user can exit the loop using the Exit button, or add cards to the forgotten_cards list with the Forget button. If there are any cards in forgotten_cards once the loop is finished, it'll restart the loop with those cards as the new deck, over and over until the user has gotten them all correct.

while True:
    for card in cards:
        magtag.set_text("Exit          --          --          Turn Over",
                        BUTTON_TEXT_IDX,auto_refresh=False)
        text = '\n'.join(magtag.wrap_nicely(card[0], 11))
        magtag.set_text(text, CARD_TEXT_IDX)
        magtag.peripherals.neopixels.fill((0, 0, 0))

        while True:
            # EXIT
            if update_button(BTN_A, magtag.peripherals.button_a_pressed):
                exit_called = True
                break
            # TURN
            if update_button(BTN_D, magtag.peripherals.button_d_pressed):
                break
        magtag.peripherals.neopixels.fill((128, 0, 0))
        if exit_called:
            break

        magtag.set_text("Exit          --          Forgot      Good",
                        BUTTON_TEXT_IDX,auto_refresh=False)
        text = '\n'.join(magtag.wrap_nicely(card[1], 11))
        text += '\n'
        text += '\n'.join(magtag.wrap_nicely(card[2], 20))
        magtag.set_text(text, CARD_TEXT_IDX)
        magtag.peripherals.neopixels.fill((0, 0, 0))

        while True:
            # EXIT
            if update_button(BTN_A, magtag.peripherals.button_a_pressed):
                exit_called = True
                break
            # FORGOT
            if update_button(BTN_C, magtag.peripherals.button_c_pressed):
                forgotten_cards.append(card)
                break
            # GOOD
            if update_button(BTN_D, magtag.peripherals.button_d_pressed):
                break
        magtag.peripherals.neopixels.fill((128, 0, 0))
        if exit_called:
            break
        # Next card
    # If there were forgotten cards, make them the new deck and restart
    if forgotten_cards:
        cards = forgotten_cards
        forgotten_cards = []
    else:
        break

Wrapping up:

Once the user has finished a study session, all that's left is to clean up the screen and reset everything back to how it started. We'll send them a message if they completed the deck (rather than exiting), and turn off any LEDs or variables that might have been set.

# Show completion text if deck was finished
if not exit_called:
    magtag.set_text("--            --          --          --",
                    BUTTON_TEXT_IDX,auto_refresh=False)
    magtag.set_text("Complete!", CARD_TEXT_IDX)
else:
    exit_called = False

# Clear and reprint list of chapters
magtag.set_text("", CARD_TEXT_IDX, auto_refresh=False)
for i in range(list_len):
    if i == 0:
        magtag.set_text("> " + chap_list[i], i, auto_refresh=False)
    else:
        magtag.set_text("  " + chap_list[i], i, auto_refresh=False)
magtag.set_text("Select        Up          Down        Begin",
                BUTTON_TEXT_IDX, auto_refresh=False)
magtag.set_text("Press Begin to default to all chapters", MSG_TEXT_IDX)

# Reset cursor:
cursor_pos = 0
list_offset = 0
selected = [False] * list_len
btn_updated = False

# Done resetting, return to chapter selection
magtag.peripherals.neopixels.fill((0, 0, 0))

This guide was first published on Jan 06, 2021. It was last updated on Jan 06, 2021.

This page (Complicated Flashcards) was last updated on Feb 07, 2021.

Text editor powered by tinymce.