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.
Installing Project Code
CIRCUITPY drive. Then you need to update code.py with the example script.
Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory MagTag_Flashcards/chapters/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.
CIRCUITPY
# SPDX-FileCopyrightText: 2021 Anne Barela for Adafruit Industries # # SPDX-License-Identifier: MIT 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"], ] }
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))
Page last edited January 21, 2025
Text editor powered by tinymce.