The moment folks look at this project, they get both nostalgic and confused at the same time. It is, almost, what it appears to be: a floppy disk shaped item ("the save icon" to the young ones) with a color display showing what files are contained within.

With classic floppies, we always wanted to know which files were on a disk - this shows the files rather than having a paper label.

This project is a functional storage device with a display. It is a solid state device with 8MB of storage transferable via USB. You can store your files in style and have an interesting item to show your friends and colleagues (and confuse the kids).

This is a fun no solder project realized with CircuitPython.

Inspiration

The 19A0s (not a typo) was a wild decade for design. Vividly described by Boing Boing and Reddit, the 19A0s saw the creation of fabulous art, culture, and technology which was not present in other decades of the 20th century. 

A Boing Boing article on the technology in the television series Loki illustrates how design and function morph as we look back at the past, at a decade which is now hazy to remember.

lcds___displays_loki.png
"19A0" retro technology in the series Loki (via Boing Boing)

Dana Sibera posts on social media wonderful retro inspired designs. These throwbacks to a bygone era elicit nostalgia yet cannot be pinpointed to any particular time or place.

lcds___displays_sibera.png
Designs by Dana Sibera (via Twitter)

This project draws inspiration from one of Sibera's designs at the bottom of the picture above: Floppy disks with a display built-in to see what files are on the disk. It's a fun thought that this was possible.

There was some technology which had icons, but they were mainly focused on PDA (personal digital assistant) function selection.

Due to this implementation, the case is slightly larger than a 3.5" floppy in length and width and sixteen millimeters thick to accommodate the Adafruit PyPortal display module. One wouldn't want this stuffed into a vintage 3.5" drive, would we (?)

Parts

The Adafruit PyPortal features a beefy SAMD51 processor, a lovely color display and 8MB of flash storage with optional microSD card (not used in this build but extendable by the reader). There is also WiFi capability, again not in use at present.

Front view of a Adafruit PyPortal - CircuitPython Powered Internet Display with a pyportal logo image on the display.
PyPortal, our easy-to-use IoT device that allows you to create all the things for the “Internet of Things” in minutes. Make custom touch screen interface...
$54.95
In Stock

This USB micro B cable is actually reversible like a USB C connector. Adafruit has quite a selection of USB micro B cables, please ensure it has data & power wires and is long enough for your setup. The project (as designed) is powered from USB (no battery), so plan accordingly.

Fully Reversible Pink/Purple USB A to micro B Cable
This cable is not only super-fashionable, with a woven pink and purple Blinka-like pattern, it's also fully reversible! That's right, you will save seconds a day by...
$3.95
In Stock

You can print the floppy case in any color you like, but printing it in beige gives it a retro look. And a grey or silver works well for the window and hub.

CAD Parts List

STL files for 3D printing are oriented to print "as-is" on FDM style machines. Parts are designed to 3D print with PLA filament. Original design source may be downloaded using the links below.

  • Front Cover.stl
  • Back Door.stl
  • Front Door.stl
  • Disk.stl
  • Back Cover.stl
Enclosure designed by Noe Ruiz.

Build Volume

The parts require a 3D printer with a minimum build volume.

  • 95mm (X) x 102mm (Y) x 14mm (Z)

CURA Slicer Settings

The Front Cover part requires support material in order to properly 3D print. Use the following settings for best results.

  • Support Density: 15%
  • Support Z Distance: 0.21mm
  • Enable Support Interface: Yes
  • Support Interface Thickness: 0.4mm
  • Support Interface Density: 75%

Support Blockers in CURA

Use support blockers to reduce the amount of material needed. Apply support blockers to the arrow and two mounting holes of the Front Cover. 

Layer Line Direction

Rotate the Front Cover at 45 degrees to create parallel lines in the supported areas. This will help to create better quality surfaces. Use the Preview tab in CURA slicer to see how the layers will be laid down. Goto layer #9 to see the direction of the lines in the supported areas.

CAD Assembly

The PyPortal is secured to the Front Cover using two sets of machine screws. The bottom standoffs require 2x M2.5 x 10mm screws and nuts. The top standoffs require 2x M3 x 6mm screws. The Front and Back Cover snap fit together.

The Door assembly snap fits onto the top section of the Front and Back covers. The Front and Back Door parts are glued together using adhesives. The disk hub may snap fit into the center hole of the Back Cover or it might need a bit of adhesive also, depending on the print. 

Hardware for PyPortal

Use the following hardware for securing the PyPortal to the case.

  • 2x M2.5 x 10mm long screws
  • 2x M2.5 hex nuts
  • 2x M3 x 6mm long screws

Install Disk to Back Cover

Position the disk part over the center hole of the back cover. Firmly press the disk into the hole to snap fit into place.

Screws for PyPortal

Orient the PyPortal with the Front Cover in preparation for installation. 

The M3 screws will be used on the upper set of standoffs while the M2.5 screws will be used on the lower set.

Secure PyPortal to Front Cover

Place the PyPortal over the Front Cover with the mounting holes lined up with the standoffs.

Insert and fasten the M3 screws to the upper set of standoffs until the screw head are flush with the mounting tabs.

Install M2.5 Hardware

Insert an M2.5 x 10mm long screw through the holes on the Front Cover. The screws should pass-through.

Flip the Front Cover and begin installing an M2.5 hex nut onto the thread of the M2.5 screw.

Secure M2.5 Hardware

Use a screwdriver to fasten the M2.5 screw to the hex nut. 

Secured PyPortal

Repeat the process for the second set of M2.5 hardware.

Installing Front to Back

Orient the front and back covers in preparation for installing together.

Join the front and back covers by snap fitting them together.

Install Front Door

Orient the front door with the case so that the nub is lined up with the slot on the front cover.

Insert the front door at an angle to snap fit into place. Slide the front door to lock it into position.

Install Back Door

The back door can be attached to either the front door or back cover using adhesives such as super glue or double-sided tape.

 

Final Build

Connect the PyPortal to a 5V USB power supply to power it on. Congratulations on your build!

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY "flash" drive to iterate.

The following instructions will show you how to install CircuitPython. If you've already installed CircuitPython but are looking to update it or reinstall it, the same steps work for that as well!

Set up CircuitPython Quick Start!

Follow this quick step-by-step for super-fast Python power :)

Click the link above to download the latest version of CircuitPython for the PyPortal.

Download and save it to your desktop (or wherever is handy).

Plug your PyPortal into your computer using a known-good USB cable.

A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.

Double-click the Reset button on the top in the middle (magenta arrow) on your board, and you will see the NeoPixel RGB LED (green arrow) turn green. If it turns red, check the USB cable, try another USB port, etc. Note: The little red LED next to the USB connector will pulse red. That's ok!

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

You will see a new disk drive appear called PORTALBOOT.

Drag the adafruit-circuitpython-pyportal-<whatever>.uf2 file to PORTALBOOT.

The LED will flash. Then, the PORTALBOOT drive will disappear and a new disk drive called CIRCUITPY will appear.

If you haven't added any code to your board, the only file that will be present is boot_out.txt. This is absolutely normal! It's time for you to add your code.py and get started!

That's it, you're done! :)

PyPortal Default Files

Click below to download a zip of the files that shipped on the PyPortal or PyPortal Pynt.

It is suggested you use the latest version of CircuitPython available (8.x at the time of writing this guide) for best results.

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

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries, and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.

Drag the contents of the uncompressed bundle directory onto your PyPortal CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.

Files in the bundle:

# SPDX-FileCopyrightText: 2023 Anne Barela for Adafruit Industries
#
# SPDX-License-Identifier: MIT
#
# Faux Floppy Disk with LCD Screen
# Display file icons on screen

import os
import time
import board
import displayio
import adafruit_imageload
import terminalio
import adafruit_touchscreen
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect

# Get a dictionary of filenames at the passed base directory
#    each entry is a tuple (filename, bool) where bool = true
#    means the filename is a directory, else false.
def get_files(base):
    files = os.listdir(base)
    file_names = []
    for isdir, filetext in enumerate(files):
        if not filetext.startswith("."):
            if filetext not in ('boot_out.txt', 'System Volume Information'):
                stats = os.stat(base + filetext)
                isdir = stats[0] & 0x4000
                if isdir:
                    file_names.append((filetext, True))
                else:
                    file_names.append((filetext, False))
    return file_names

def get_touch(screen):
    p = None
    while p is None:
        time.sleep(0.05)
        p = screen.touch_point
    return p[0]

# Icon Positions
ICONSIZE = 48
SPACING = 18
LEFTSPACE = 38
TOPSPACE = 10
TEXTSPACE = 10
ICONSACROSS = 4
ICONSDOWN = 3
PAGEMAXFILES = ICONSACROSS * ICONSDOWN  # For the chosen display, this is the
#                                     maximum number of file icons that will fit
#                                     on the display at once (display dependent)
# File Types
BLANK = 0
FILE = 1
DIR = 2
BMP = 3
WAV = 4
PY = 5
RIGHT = 6
LEFT = 7

# Use the builtin display
display = board.DISPLAY
WIDTH = board.DISPLAY.width
HEIGHT = board.DISPLAY.height
ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR,
                                      board.TOUCH_YD, board.TOUCH_YU,
                                      calibration=((5200, 59000), (5800, 57000)),
                                      size=(WIDTH, HEIGHT))

# Create base display group
displaygroup = displayio.Group()

# Load the bitmap (this is the "spritesheet")
sprite_sheet, palette = adafruit_imageload.load("/icons.bmp")

background = Rect(0, 0, WIDTH - 1, HEIGHT - 1, fill=0x000000)
displaygroup.append(background)

# Create enough sprites & labels for the icons that will fit on screen
sprites = []
labels = []
for _ in range(PAGEMAXFILES):
    sprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
                                width=1, height=1, tile_height=48,
                                tile_width=48,)
    sprites.append(sprite)  # Append the sprite to the sprite array
    displaygroup.append(sprite)
    filelabel = label.Label(terminalio.FONT, color=0xFFFFFF)
    labels.append(filelabel)
    displaygroup.append(filelabel)

# Make the more files and less files icons (> <)
moresprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
                                width=1, height=1, tile_height=48,
                                tile_width=48,)
displaygroup.append(moresprite)
moresprite.x = WIDTH - ICONSIZE + TEXTSPACE
moresprite.y = int((HEIGHT - ICONSIZE) / 2)
lesssprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
                                width=1, height=1, tile_height=48,
                                tile_width=48,)
displaygroup.append(lesssprite)
lesssprite.x = -10
lesssprite.y = int((HEIGHT - ICONSIZE) / 2)

display.show(displaygroup)

filecount = 0
xpos = LEFTSPACE
ypos = TOPSPACE

displaybase = "/"  # Get file names in base directory
filenames = get_files(displaybase)

currentfile = 0  # Which file is being processed in all files
spot = 0  # Which spot on the screen is getting a file icon
PAGE = 1  # Which page of icons is displayed on screen, 1 is first

while True:
    if currentfile < len(filenames) and spot < PAGEMAXFILES:
        filename, dirfile = filenames[currentfile]
        if dirfile:
            filetype = DIR
        elif filename.endswith(".bmp"):
            filetype = BMP
        elif filename.endswith(".wav"):
            filetype = WAV
        elif filename.endswith(".py"):
            filetype = PY
        else:
            filetype = FILE
        # Set icon location information and icon type
        sprites[spot].x = xpos
        sprites[spot].y = ypos
        sprites[spot][0] = filetype
        #
        # Set filename
        labels[spot].x = xpos
        labels[spot].y = ypos + ICONSIZE + TEXTSPACE
        # The next line gets the filename without the extension, first 11 chars
        labels[spot].text = filename.rsplit('.', 1)[0][0:10]

    currentpage = PAGE

    # Pagination Handling
    if spot >= PAGEMAXFILES - 1:
        if currentfile < (len(filenames) + 1):
            # Need to display the greater than touch sprite
            moresprite[0] = RIGHT
        else:
            # Blank out more and extra icon spaces
            moresprite[0] = BLANK
        if PAGE > 1:  # Need to display the less than touch sprite
            lesssprite[0] = LEFT
        else:
            lesssprite[0] = BLANK

        # Time to check for user touch of screen (BLOCKING)
        touch_x = get_touch(ts)
        print("Touch Registered ")
        # Check if touch_x is around the LEFT or RIGHT arrow
        currentpage = PAGE
        if touch_x >= int(WIDTH - ICONSIZE):    # > Touched
            if moresprite[0] != BLANK:          # Ensure there are more
                if spot == (PAGEMAXFILES - 1):  # Page full
                    if currentfile < (len(filenames)):  # and more files
                        PAGE = PAGE + 1         # Increment page
        if touch_x <= ICONSIZE:                 # < Touched
            if PAGE > 1:
                PAGE = PAGE - 1                 # Decrement page
            else:
                lesssprite[0] = BLANK        # Not show < for first page
        print("Page ", PAGE)
    # Icon Positioning

    if PAGE != currentpage:  # We have a page change
        # Reset icon locations to upper left
        xpos = LEFTSPACE
        ypos = TOPSPACE
        spot = 0
        if currentpage > PAGE:
            # Decrement files by a page (current page & previous page)
            currentfile = currentfile - (PAGEMAXFILES * 2) + 1
        else:
            # Forward go to the next file
            currentfile = currentfile + 1
    else:
        currentfile += 1             # Increment file counter
        spot += 1                    # Increment icon space counter
        if spot == PAGEMAXFILES:     # Last page ended with
            print("hit")
        # calculate next icon location
        if spot % ICONSACROSS:       # not at end of icon row
            xpos += SPACING + ICONSIZE
        else:                        # start new icon row
            ypos += ICONSIZE + SPACING + TEXTSPACE
            xpos = LEFTSPACE
    # End If Changed Page
    # Blank out rest if needed
    if currentfile == len(filenames):
        for i in range(spot, PAGEMAXFILES):
            sprites[i][0] = BLANK
            labels[i].text = " "
# End while

Coding the application on the Adafruit PyPortal with CircuitPython provides a clean, high level method of creating a user interface which can be adapted for other displays of varying sizes and types.

If a greyscale display, smaller (or larger) display or possibly an eInk device was desired, the application only would need a few changes (if there is capacitive touch, a bit more coding to use buttons if not).

Code Overview

The code starts off importing a number of CircuitPython libraries:

import os
import time
import board
import displayio
import adafruit_imageload
import terminalio
import adafruit_touchscreen
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect

A function, get_files(), gets a dictionary of files for the passed-in director name. For this demo, the base directory "/" is used. If the code is modified to do subdirectories, etc. the function only needs the correct path. The return is a dictionary of filename, boolean tuples. If the boolean is True, the filename refers to a directory. 

# Get a dictionary of filenames at the passed base directory
#    each entry is a tuple (filename, bool) where bool = true
#    means the filename is a directory, else false.
def get_files(base):
    files = os.listdir(base)
    file_names = []
    for j, filetext in enumerate(files):
        if not filetext.startswith("."):
            if filetext not in ('boot_out.txt', 'System Volume Information'):
                stats = os.stat(base + filetext)
                isdir = stats[0] & 0x4000
                if isdir:
                    file_names.append((filetext, True))
                else:
                    file_names.append((filetext, False))
    return filenames

And then a function to wait and get a touchscreen touch and return the x coordinate only (all that is needed):

def get_touch(screen)
    p = None
    while p is None:
        time.sleep(0.05)
        p = screen.touch_point
    return p[0]

The values which get the file icons spaced out appropriately. The values are for PyPortal, but if you want to use a different display, you can change the values here rather than "magic numbers" scattered throughout the code.

# Icon Positions
ICONSIZE = 48
SPACING = 18
LEFTSPACE = 38
TOPSPACE = 10
TEXTSPACE = 10
ICONSACROSS = 4
ICONSDOWN = 3
PAGEMAXFILES = ICONSACROSS * ICONSDOWN  # For the chosen display, this is the
#                                     maximum number of file icons that will fit
#                                     on the display at once (display dependent)

There is blank icon, followed by five defined file icons,  and the left & right arrows for pagination. If more icons are wanted, add them after the last file icon (currently PY).

# File Types
BLANK = 0
FILE = 1
DIR = 2
BMP = 3
WAV = 4
PY = 5
RIGHT = 6
LEFT = 7

The display is then set up. The code uses the CircuitPython displayio framework. The display touchscreen is set up for use.

# Use the builtin display
display = board.DISPLAY
WIDTH = board.DISPLAY.width
HEIGHT = board.DISPLAY.height
ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR,
                                      board.TOUCH_YD, board.TOUCH_YU,
                                      calibration=((5200, 59000), (5800, 57000)),
                                      size=(WIDTH, HEIGHT))

# Create base display group
displaygroup = displayio.Group()

Now to load the icons:

# Load the bitmap (this is the "spritesheet")
sprite_sheet, palette = adafruit_imageload.load("/icons.bmp")

This code sets up all the icon positions based on the constants defined earlier:

sprites = []
labels = []
for _ in range(PAGEMAXFILES):
    sprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
                                width=1, height=1, tile_height=48,
                                tile_width=48,)
    sprites.append(sprite)  # Append the sprite to the sprite array
    displaygroup.append(sprite)
    filelabel = label.Label(terminalio.FONT, color=0xFFFFFF)
    labels.append(filelabel)
    displaygroup.append(filelabel)

# Make the more files and less files icons (> <)
moresprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
                                width=1, height=1, tile_height=48,
                                tile_width=48,)
displaygroup.append(moresprite)
moresprite.x = WIDTH - ICONSIZE + TEXTSPACE
moresprite.y = int((HEIGHT - ICONSIZE) / 2)
lesssprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
                                width=1, height=1, tile_height=48,
                                tile_width=48,)
displaygroup.append(lesssprite)
lesssprite.x = -10
lesssprite.y = int((HEIGHT - ICONSIZE) / 2)

display.show(displaygroup)

The code iterates through all the filenames, displaying them in sequential icon locations. The icons are labeled with the first 10 characters of the filename (without the extension). This can be changed, there isn't a lot of room.

filename, dirfile = filenames[currentfile]
        if dirfile:
            filetype = DIR
        elif filename.endswith(".bmp"):
            filetype = BMP
        elif filename.endswith(".wav"):
            filetype = WAV
        elif filename.endswith(".py"):
            filetype = PY
        else:
            filetype = FILE
        # Set icon location information and icon type
        sprites[spot].x = xpos
        sprites[spot].y = ypos
        sprites[spot][0] = filetype
        #
        # Set filename
        labels[spot].x = xpos
        labels[spot].y = ypos + ICONSIZE + TEXTSPACE
        # The next line gets the filename without the extension, first 11 chars
        labels[spot].text = filename.rsplit('.', 1)[0][0:10]

The next chunk of code is lengthy, handling the pagination. Putting the < and > icons on screen as necessary, check for a touch on the screen, and changing pages (clearing any icons as needed). It's page oriented, there is not smooth scrolling or any dragging a finger to see files.

touch_x = get_touch(ts)
        print("Touch Registered ")
        # Check if touch_x is around the LEFT or RIGHT arrow
        currentpage = PAGE
        if touch_x >= int(WIDTH - ICONSIZE):    # > Touched
            if moresprite[0] != BLANK:          # Ensure there are more
                if spot == (PAGEMAXFILES - 1):  # Page full
                    if currentfile < (len(filenames)):  # and more files
                        PAGE = PAGE + 1         # Increment page
        if touch_x <= ICONSIZE:                 # < Touched
            if PAGE > 1:
                PAGE = PAGE - 1                 # Decrement page
            else:
                lesssprite[0] = BLANK        # Not show < for first page
        print("Page ", PAGE)
    # Icon Positioning

    if PAGE != currentpage:  # We have a page change
        # Reset icon locations to upper left
        xpos = LEFTSPACE
        ypos = TOPSPACE
        spot = 0
        if currentpage > PAGE:
            # Decrement files by a page (current page & previous page)
            currentfile = currentfile - (PAGEMAXFILES * 2) + 1
        else:
            # Forward go to the next file
            currentfile = currentfile + 1
    else:
        currentfile += 1             # Increment file counter
        spot += 1                    # Increment icon space counter
        if spot == PAGEMAXFILES:     # Last page ended with 
            print("hit")
        # calculate next icon location
        if spot % ICONSACROSS:       # not at end of icon row
            xpos += SPACING + ICONSIZE
        else:                        # start new icon row
            ypos += ICONSIZE + SPACING + TEXTSPACE
            xpos = LEFTSPACE
    # End If Changed Page
    # Blank out rest if needed
    if currentfile == len(filenames):
        for i in range(spot, PAGEMAXFILES):
            sprites[i][0] = BLANK
            labels[i].text = " "
# End while

When the code is loaded on the PyPortal, it will have the necessary files to run the application. This will leave almost 8 megabytes of flash memory for your own files. As floppies are not high storage devices, this is not either (this holds about eight times the amount of data a 3.5" floppy did back at the turn of the century).

Whenever the project is plugged into a USB cable (via power source or to a computer), it will display the first twelve files in the root directory.

Directories are displayed (but one cannot drill into subdirectories by touch in this implementation, although it's possible for the user to look to add that capability to the code).

To see more than twelve files, there is a ">" icon on the middle right of the screen (if there are more than 12 root directory files) to scroll to the next screenful of file icons. When on the second page and any subsequent pages, a "<" icon will be displayed to move back pages. These movement icons disappear as appropriate. You may want to use a fingernail to click the < and > icons.

The file icons are stored in icons.bmp, a Windows bitmap format easily read by microcontrollers. In the current implementation, they are:

  • Blank
  • Generic File
  • Directory/Folder
  • Bitmap Graphics File
  • WAV file (sound)
  • Python (.py) File
  • Next Page ">"
  • Previous Page "<"

Additional types of file icons may be added with appropriate changes to the code to select the correct icon for the file type of the file at hand (described in the Code Overview).

If files are added or deleted, the CircuitPython app restarts - this is normal file system behavior for CircuitPython.

Clicking on an icon is not currently implemented. One could attempt to view images, play sound, and print out text files. An advanced application, but certainly possible.

This guide was first published on Feb 22, 2023. It was last updated on Feb 22, 2023.