Building Stuff - Studium Lentum (Adabox017)

As I mentioned a couple of months ago, I subscribed to Adafruit's Adabox subscription service and I received my first box last week!

The closed box.

Lovely tissue paper

The packaging is very beautiful. The theme of this box is "Good Fortune Ahead," so the art card visible through the letters of the box feature zodiac signs. Inside of the box were the following goodies:

The MagTag is essentially a wifi-enabled board with an e-ink display on the front. There are four LED lights mounted perpendicularly to the display. The preloaded project featured a horoscope program that fetches fortunes from either the internet or a json file.

I decided to try to make a flashcard quiz program out of mine. I originally wanted to make such a program for Japanese, but after playing around with font files for a while, I discovered that the necessary BDF font file for displaying hiragana, katakana, and kanji would exceed the storage available onboard the MagTag. Pocketing that idea for a later project, I switched to making a Latin quiz program instead.

When I was desperately searching for employment 6-7 years ago, I worked on a number of projects to try to show off my tech skills and teaching skills together. One of these was an interactive Latin textbook that I didn't make too much progress on. I copied a couple of the graphics I had made for that and converted them into 4-color greyscale. The site was going to be called "Athena Noctua," and so had the little owl of Minerva as its mascot:

Sample Program Background

Essentially, the user is presented with a word and two choices (A and B). When the choice is made, the screen updates to inform the user whether the answer was correct or not before displaying the next word. Additional grammatical information can be toggled on or off with the "Extra" button. The "Setup" button allows the user to toggle on/off the NeoPixel lights and sound, which provide sensory flair when the answer is graded. They can also select the level the questions should be drawn from (lessons 1 through 40, in groups of 5). These lessons correspond to the vocabulary lists in Wheelock's Latin, 6th edition, which is the book I first took Latin from way back in 2002. I don't imagine the vocabulary lists have changed much in the newer edition of the book, though, so others might find it useful as well. The latin.json file on GitHub currently only has the first few levels; I'll update the file once I finish the complete vocabulary list.

I was able to adapt a lot of the preloaded program for my flashcard quiz. The only portion of the program that really gave me trouble was working with the json file, simply because I've only ever used csv or txt files in the past. The other thing I did was modify the default 18-pixel Arial font file that was included. The bottom of the lowercase "t" was elevated a couple of pixels above the bottoms of the other letters, and it bothered me, so I modified that section of the BDF file to extend the lowercase "t" down to the same ground level as the other letters. The code for the program can be found at GitHub.

import time
import random
import terminalio
import json
from adafruit_magtag.magtag import MagTag

We import the necessary libraries. Then we declare some global variables. I currently have the sound and lights turned off by default, so as to not annoy the rest of the household as I worked through testing.

SHOW_LIGHTS = False
PLAY_SOUNDS = False
MAX_LEVEL =  40
NUMBER_CORRECT = 0
CORRECT_ANSWER = 1
SHOW_EXTRA = True
WHICH_QUESTION = 0

We tell the MagTag to be a MagTag. Cogitat ergo est? After that, there are a number of text blocks defined. Text block 0 (counting as the code counts) is the big-font block, which shows the word being quizzed. 1-4 are the labels for the buttons (typically they say "SETUP," "A," "B," and "EXTRA" in my program). 5 is the optional "extra" grammatical details displayed under the word being quizzed. 6 is choice A for the meaning; 7 is choice B.

# main text, index 0
magtag.add_text(
    text_font ="Arial-Bold-24.bdf",
    text_position=(
        magtag.graphics.display.width // 2,
        5,
    ),
    text_scale = 1,
    line_spacing=1,
    text_anchor_point=(0.5, 0),
)
# button labels, add all 4 in one loop
for x_coord in (5, 92, 166, 220):
    magtag.add_text(
        text_font = "Arial-12.bdf",
        text_position=(x_coord, magtag.graphics.display.height - 3),
        line_spacing=1.0,
        text_anchor_point=(0, 1),
    )
# Extra
magtag.add_text(
    text_font = "Arial-12.bdf",
    text_position=(
        magtag.graphics.display.width // 2,
        38,
    ),
    text_scale = 1,
    line_spacing=1,
    text_anchor_point=(0.5, 0),
)
# Answer A
magtag.add_text(
    text_font = "Arial-18.bdf",
    text_position=(
        magtag.graphics.display.width // 2,
        56,
    ),
    text_scale = 1,
    line_spacing=1,
    text_anchor_point=(0.5, 0),
)
# Answer B
magtag.add_text(
    text_font = "Arial-18.bdf",
    text_position=(
        magtag.graphics.display.width // 2,
        83,
    ),
    text_scale = 1,
    line_spacing=1,
    text_anchor_point=(0.5, 0),
)

Now we read in the vocabulary json file:

quizQuestions = json.loads(open("latin.json").read())

The next section is a custom function that displays the next question. It randomly selects a vocabulary word from our json data, making sure that the level of the word does not exceed the current limit (either declared above or changed by the user). A wrong answer is randomly selected as well (checking to make sure that the wrong answer is not the same as the right answer, because, boy, wouldn't that be confusing!) and the program then randomly chooses whether the correct answer will appear as A or B on the quiz screen.

def displayNextQuestion():
    global MAX_LEVEL
    global CORRECT_ANSWER
    global WHICH_QUESTION
    global SHOW_EXTRA
    magtag.set_background("background_menu.bmp")
    magtag.set_text("SETUP", 1, False)
    magtag.set_text("A", 2, False)
    magtag.set_text("B", 3, False)
    magtag.set_text("EXTRA", 4, False)
    questionLevel = 41
    while questionLevel > MAX_LEVEL:
        WHICH_QUESTION = random.randint(0, len(quizQuestions)-1)
        questionLevel = quizQuestions[WHICH_QUESTION]['lesson']
    magtag.set_text(quizQuestions[WHICH_QUESTION]['question'], 0, False)
    CORRECT_ANSWER = random.randint(1,2)
    wrongAnswer = quizQuestions[WHICH_QUESTION]['answer']
    while wrongAnswer == quizQuestions[WHICH_QUESTION]['answer']:
        whichWrong = random.randint(0, len(quizQuestions)-1)
        wrongAnswer = quizQuestions[whichWrong]['answer']
    if SHOW_EXTRA == True:
        magtag.set_text(quizQuestions[WHICH_QUESTION]['parts'], 5, False)
    if SHOW_EXTRA == False:
        magtag.set_text("", 5, False)
    if CORRECT_ANSWER == 1:
        magtag.set_text('A: ' + quizQuestions[WHICH_QUESTION]['answer'], 6, False)
        magtag.set_text('B: ' + wrongAnswer, 7, False)
    if CORRECT_ANSWER == 2:
        magtag.set_text('A: ' + wrongAnswer, 6, False)
        magtag.set_text('B: ' + quizQuestions[WHICH_QUESTION]['answer'], 7, False)
    magtag.refresh()

This next bit is what first shows up when the MagTag is activated or reset. The title of the program is briefly shown.

magtag.peripherals.neopixels.brightness = 0.1
magtag.set_background("background.bmp")
magtag.set_text("STUDIUM\n LENTUM")
magtag.refresh()
if SHOW_LIGHTS == True:
    magtag.peripherals.neopixels.fill(0xFF4500)
if PLAY_SOUNDS == True:
    song = ((262, 2),(262, 2),(349,6))
    for notepair in song:
        magtag.peripherals.play_tone(notepair[0], notepair[1] * 0.2)
if SHOW_LIGHTS == True:
    time.sleep(0.5)
    magtag.peripherals.neopixels.fill(0xffffff)
else:
    magtag.peripherals.neopixels.fill(0x000000)

The following function defines what happens if the user chooses to go to the "Setup" screen. It gives them the option of turning the lights on/off, of turning the sound on/off, of changing the max flashcard level, or of going back to the quiz.

def changeSettings():
        # Setup Screen
        magtag.set_text('Setup', 0, False)
        magtag.set_text('   1', 1, False)
        magtag.set_text('2', 2, False)
        magtag.set_text('3', 3, False)
        magtag.set_text('BACK', 4, False)
        magtag.set_text(' 1: Light \n 2: Sound \n 3: Level', 5, False)
        magtag.set_text('', 6, False)
        magtag.set_text('', 7, True)
        while magtag.peripherals.buttons[0].value and magtag.peripherals.buttons[1].value and magtag.peripherals.buttons[2].value and magtag.peripherals.buttons[3].value:
            time.sleep(0.1)
        if not magtag.peripherals.buttons[0].value:
            changeLights()
        if not magtag.peripherals.buttons[1].value:
            changeSound()
        if not magtag.peripherals.buttons[2].value:
            changeLevel()
        if not magtag.peripherals.buttons[3].value:
            displayNextQuestion()

If they selected to change the level, they are taken to this screen, where the two center buttons allow the level to be raised or lowered (in increments of 5 - simply because waiting for the MagTag to refresh on each level from 1 to 40 would try anyone's patience).

def changeLevel():
    # Level
    global MAX_LEVEL
    newMaxLevel = MAX_LEVEL
    magtag.set_text('Max Level',0,False)
    magtag.set_text('SAVE',1,False)
    if MAX_LEVEL > 1:
        magtag.set_text('v',2,False)
    else:
        magtag.set_text('',2,False)
    if MAX_LEVEL < 41:
        magtag.set_text('^',3,False)
    else:
        magtag.set_text('',3,False)
    magtag.set_text('(1 - 40)', 5, False)
    magtag.set_text('Current: ' + str(newMaxLevel), 6, True)
    keepLooking = True
    while keepLooking:
        if not magtag.peripherals.buttons[0].value:
            MAX_LEVEL = newMaxLevel
            keepLooking = False
        if not magtag.peripherals.buttons[1].value:
            if newMaxLevel > 1:
                newMaxLevel = newMaxLevel - 5
                if newMaxLevel <= 5:
                    newMaxLevel = 5
                    magtag.set_text('',2,False)
                else:
                    magtag.set_text('v',2,False)
                if newMaxLevel >= 40:
                    newMaxLevel = 40
                    magtag.set_text('',3,False)
                else:
                    magtag.set_text('^',3,False)
                magtag.set_text('Current: ' + str(newMaxLevel), 6, True)
        if not magtag.peripherals.buttons[2].value:
            if newMaxLevel < MAX_LEVEL:
                newMaxLevel = newMaxLevel + 5
                if newMaxLevel <= 5:
                    newMaxLevel = 5
                    magtag.set_text('',2,False)
                else:
                    magtag.set_text('v',2,False)
                if newMaxLevel >= 40:
                    newMaxLevel = 40
                    magtag.set_text('',3,False)
                else:
                    magtag.set_text('^',3,False)
                magtag.set_text('Current: ' + str(newMaxLevel), 6, True)
        if not magtag.peripherals.buttons[3].value:
            changeSettings()
        time.sleep(0.1)
    displayNextQuestion()

This function defines the sound on/off toggle screen:

def changeSound():
    # Sound
    global PLAY_SOUNDS
    magtag.set_text('Sounds',0,False)
    magtag.set_text('CHANGE',1,False)
    magtag.set_text('',2,False)
    magtag.set_text('',3,False)
    if PLAY_SOUNDS == True:
        magtag.set_text('Sounds are on.',5,True)
    else:
        magtag.set_text('Sounds are off.',5,True)
    while magtag.peripherals.buttons[0].value and magtag.peripherals.buttons[3].value:
        time.sleep(0.1)
    if not magtag.peripherals.buttons[0].value:
        if PLAY_SOUNDS == True:
            PLAY_SOUNDS = False
        else:
            PLAY_SOUNDS = True
            song = ((262, 2),(262, 2),(349,6))
            for notepair in song:
                magtag.peripherals.play_tone(notepair[0], notepair[1] * 0.2)
        displayNextQuestion()
    if not magtag.peripherals.buttons[3].value:
        changeSettings()

This function defines the lights on/off toggle screen:

def changeLights():
    # Light
    global SHOW_LIGHTS
    magtag.set_text('Lights',0,False)
    magtag.set_text('CHANGE',1,False)
    magtag.set_text('',2,False)
    magtag.set_text('',3,False)
    if SHOW_LIGHTS == True:
        magtag.set_text('Lights are on.',5,True)
    else:
        magtag.set_text('Lights are off.',5,True)
    while magtag.peripherals.buttons[0].value and magtag.peripherals.buttons[3].value:
        time.sleep(0.1)
    if not magtag.peripherals.buttons[0].value:
        if SHOW_LIGHTS == True:
            SHOW_LIGHTS = False
            magtag.peripherals.neopixels.fill(0x000000)
        else:
            SHOW_LIGHTS = True
            magtag.peripherals.neopixels.fill(0xffffff)
        displayNextQuestion()
    if not magtag.peripherals.buttons[3].value:
        changeSettings()

And, finally, the first question is displayed and the program enters its happy infinite loop waiting for buttons to be pressed. If the first button (0) is pressed, the "Setup" screen is displayed. If the fourth button (3) is pressed, the grammatical information is turned on or off. If either of the two center buttons (1 = "A" and 2 = "B") are pressed, the answer is graded and the user is presented with either a "Correct" screen (with optional green lights and ascending tune of victory) or a "Wrong" screen (with optional red lights and descending tune of disappointment). The correct screen also displays the number of questions answered correctly in a row - don't break that winning streak! After a brief delay, the next question is then displayed and our loop begins again.

#FirstQuestion
displayNextQuestion()
while True:
    if not magtag.peripherals.buttons[0].value:
        changeSettings()
    if not magtag.peripherals.buttons[3].value:
        if SHOW_EXTRA == True:
            SHOW_EXTRA = False
            magtag.set_text('', 5, True)
        else:
            SHOW_EXTRA = True
            magtag.set_text(quizQuestions[WHICH_QUESTION]['parts'], 5, True)
    if not magtag.peripherals.buttons[2].value:
        if CORRECT_ANSWER == 2:
            NUMBER_CORRECT = NUMBER_CORRECT + 1
            magtag.set_background("owl.bmp")
            magtag.set_text('CORRECT!', 0, False)
            magtag.set_text('', 1, False)
            magtag.set_text('Current Streak: ' + str(NUMBER_CORRECT), 2, False)
            magtag.set_text('', 3, False)
            magtag.set_text('', 4, False)
            magtag.set_text('', 5, False)
            magtag.set_text(quizQuestions[WHICH_QUESTION]['question'], 6, False)
            magtag.set_text(quizQuestions[WHICH_QUESTION]['answer'], 7, True)
            if SHOW_LIGHTS == True:
                magtag.peripherals.neopixels.fill(0x002700)
            if PLAY_SOUNDS == True:
                song = ((262, 2),(262, 2),(349,6))
                for notepair in song:
                    magtag.peripherals.play_tone(notepair[0], notepair[1] * 0.2)
            if SHOW_LIGHTS == True:
                time.sleep(0.5)
                magtag.peripherals.neopixels.fill(0xffffff)
            else:
                magtag.peripherals.neopixels.fill(0x000000)
        else:
            magtag.set_background("owl.bmp")
            magtag.set_text('WRONG!', 0, False)
            magtag.set_text('', 1, False)
            magtag.set_text('', 2, False)
            magtag.set_text('', 3, False)
            magtag.set_text('', 4, False)
            magtag.set_text('', 5, False)
            magtag.set_text(quizQuestions[WHICH_QUESTION]['question'], 6, False)
            magtag.set_text(quizQuestions[WHICH_QUESTION]['answer'], 7, True)
            if SHOW_LIGHTS == True:
                magtag.peripherals.neopixels.fill(0x7f0000)
            if PLAY_SOUNDS == True:
                song = ((392, 2),(330, 2),(262,6))
                for notepair in song:
                    magtag.peripherals.play_tone(notepair[0], notepair[1] * 0.2)
            if SHOW_LIGHTS == True:
                time.sleep(0.5)
                magtag.peripherals.neopixels.fill(0xffffff)
            else:
                magtag.peripherals.neopixels.fill(0x000000)
            NUMBER_CORRECT = 0
        time.sleep(0.5)
        displayNextQuestion()
    if not magtag.peripherals.buttons[1].value:
        if CORRECT_ANSWER == 1:
            NUMBER_CORRECT = NUMBER_CORRECT + 1
            magtag.set_background("owl.bmp")
            magtag.set_text('CORRECT!', 0, False)
            magtag.set_text('', 1, False)
            magtag.set_text('Current Streak: ' + str(NUMBER_CORRECT), 2, False)
            magtag.set_text('', 3, False)
            magtag.set_text('', 4, False)
            magtag.set_text('', 5, False)
            magtag.set_text(quizQuestions[WHICH_QUESTION]['question'], 6, False)
            magtag.set_text(quizQuestions[WHICH_QUESTION]['answer'], 7, True)
            if SHOW_LIGHTS == True:
                magtag.peripherals.neopixels.fill(0x002700)
            if PLAY_SOUNDS == True:
                song = ((262, 2),(262, 2),(349,6))
                for notepair in song:
                    magtag.peripherals.play_tone(notepair[0], notepair[1] * 0.2)
            if SHOW_LIGHTS == True:
                time.sleep(0.5)
                magtag.peripherals.neopixels.fill(0xffffff)
            else:
                magtag.peripherals.neopixels.fill(0x000000)
        else:
            magtag.set_background("owl.bmp")
            magtag.set_text('WRONG!', 0, False)
            magtag.set_text('', 1, False)
            magtag.set_text('', 2, False)
            magtag.set_text('', 3, False)
            magtag.set_text('', 4, False)
            magtag.set_text('', 5, False)
            magtag.set_text(quizQuestions[WHICH_QUESTION]['question'], 6, False)
            magtag.set_text(quizQuestions[WHICH_QUESTION]['answer'], 7, True)
            if SHOW_LIGHTS == True:
                magtag.peripherals.neopixels.fill(0x7f0000)
            if PLAY_SOUNDS == True:
                song = ((392, 2),(330, 2),(262,6))
                for notepair in song:
                    magtag.peripherals.play_tone(notepair[0], notepair[1] * 0.2)
            if SHOW_LIGHTS == True:
                time.sleep(0.5)
                magtag.peripherals.neopixels.fill(0xffffff)
            else:
                magtag.peripherals.neopixels.fill(0x000000)
            NUMBER_CORRECT = 0
        time.sleep(0.5)
        displayNextQuestion()
    time.sleep(0.1)

After I finished the programming (I am still working on the vocabulary json list - as of writing this, I have only made it through 8 lessons of Wheelocks!), I assembled the acrylic cloud faceplate set, hooked up the battery, and attached the magnetic feet. It's cute and functional and ready to help with mental retention!

This was a very quick project - I essentially completed it in a day - but it was fun and I really like the end result. There is a lot of debate among the language learning community around flashcards (spaced repetition vs cramming) and question format (self-grading vs multiple choice vs fill-in); I tend to think any amount of exposure is better than no exposure at all, and I know that I can't trust me to grade myself. The idea behind this project is that the MagTag would be placed on the refrigerator or in some other high-trafficked area of the house, and one would do a question or two as it was encountered throughout the day. I haven't looked at Latin in a few years, and I don't like the idea of just allowing my brain to throw out something I had devoted four college semesters toward (I read half of the Aeneid in Latin back in the day!). Maybe these brief but frequent encounters (with sensory feedback!) will help stimulate those neural connections just enough to keep them around. It was also pretty fun watching my kiddo (with zero exposure to Latin) guessing the answers to some of them and hearing her work through figuring out which answer to try.

Now I just need to figure out what I'm going to do with my long string of Neopixels.