Installing the Code

Download the project files from the GitHub repo linked to the green button below.

In any of the code boxes below you can get all the files in one step by clicking "Download: Zip".

In addition to the code.py file, this project also requires several supporting folders and files. Create these folders, if you do not have them:

  • asgfiles
  • backgrounds
  • images
  • jobs

You will also need to copy the background files, sample jobs and images, and the config.json and boot.py files from the Github repository. 

The config.json file is used to define which jobs to run and folders to use. Here is the config.json that comes with the project:

{
    "jobs": ["shark.json","adafruit.json"]
    "asgfolder": "/asgfiles"
    "jobfolder": "/jobs"
    "bkfolder": "/backgrounds"
    "imagefolder": "/images"
    "slidefolder": "/asgfiles"
}

The boot.py file allows the project to create autostereogram BMP files and write to the CircuitPython file system by pressing a button at boot time. Normally, CircuitPython can not write to the file system since the host operating system has that access by default. Using this boot file and pressing a button during the boot process reverses this access, giving CircuitPython read/write access and read only access to the host operating system. Rebooting without a button press reverts back CircuitPython's access to read only. You can see which boot mode you are currently using by looking at the boot_out file on the file system. When you press a button during the boot process you will be able to create BMP files and the boot_out file will include this line:

CircuitPython has write access to drive

Here is the boot.py code:

import board
import storage
from analogio import AnalogIn

def read_buttons():
    with AnalogIn(board.A3) as ain:
        reading = ain.value / 65535
        if reading > 0.75:
            return None
        if reading > 0.4:
            return 4
        if reading > 0.25:
            return 3
        if reading > 0.13:
            return 2
        return 1

readonly = True
# if a button is pressed while booting up, CircuitPython can write to the drive
button = read_buttons()
if button != None:
    readonly = False
if readonly:
    print("OS has write access to CircuitPython drive")
else:
    print("CircuitPython has write access to drive")
storage.remount("/", readonly)

The code.py contains the code to create the autostereograms as well as display them on the ePaper shield and save them to a BMP file.

import os
import time
import json
import digitalio
import busio
import board
import displayio
import adafruit_imageload
from analogio import AnalogIn
from adafruit_epd.epd import Adafruit_EPD
from adafruit_epd.il91874 import Adafruit_IL91874

spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
ecs = digitalio.DigitalInOut(board.D10)
dc = digitalio.DigitalInOut(board.D9)
srcs = digitalio.DigitalInOut(board.D8)  # can be None to use internal memory
led = digitalio.DigitalInOut(board.D13)
led.direction = digitalio.Direction.OUTPUT

print("Creating display")
display = Adafruit_IL91874(
    176,
    264,
    spi,  # 2.7" Tri-color display
    cs_pin=ecs,
    dc_pin=dc,
    sramcs_pin=srcs,
    rst_pin=None,
    busy_pin=None,
)

# read buttons from ePaper shield
def read_buttons():
    btn = 1
    with AnalogIn(board.A3) as ain:
        reading = ain.value / 65535
        if reading > 0.75:
            btn = None
        if reading > 0.4:
            btn = 4
        if reading > 0.25:
            btn = 3
        if reading > 0.13:
            btn = 2
        btn = 1
    return btn

#pylint: disable-msg=too-many-nested-blocks
#pylint: disable-msg=too-many-branches
#pylint: disable-msg=too-many-statements
# display bitmap file
def display_bitmap(epd, filename):
    try:
        f = open("/" + filename, "rb")
        print("File opened")
        if f.read(2) != b"BM":  # check signature
            raise BMPError("Not BitMap file")
        read_le(f.read(4)) # File size
        f.read(4)  # Read & ignore creator bytes
        bmpImageoffset = read_le(f.read(4))  # Start of image data
        headerSize = read_le(f.read(4))
        bmpWidth = read_le(f.read(4))
        # convert width to 8 pixels per byte width
        bmpWidth = (bmpWidth + 7) // 8
        bmpHeight = read_le(f.read(4))
        # convert unsigned int to signed int in case there is a negative height
        if bmpHeight > 0x7FFFFFFF:
            bmpHeight = bmpHeight - 4294967296
        flip = True
        if bmpHeight < 0:
            bmpHeight = abs(bmpHeight)
            flip = False
        print(
            "Image offset: %d\nHeader size: %d"
            % (bmpImageoffset, headerSize)
        )
        print("Width: %d\nHeight: %d" % (bmpWidth, bmpHeight))
        if read_le(f.read(2)) != 1:
            raise BMPError("Not singleplane")
        if read_le(f.read(2)) != 1: # bits per pixel
            raise BMPError("Not 1-bit")
        if read_le(f.read(4)) != 0:
            raise BMPError("Compressed file not supported")
        read_le(4)  # SizeImage
        read_le(4)  # biXPelsPerMeter
        read_le(4)  # biYPelsPerMeter
        read_le(4)  # biClrUsed
        read_le(4)  # biClrImportant
        blkpixel = 1
        if read_le(4) != 0:
            blkpixel = 0
        print("black pixel is ", blkpixel)
        print("Image OK! Drawing...")
        rowSize = (bmpWidth + 3) & ~3  # 32-bit line boundary
        for row in range(bmpHeight):  # For each scanline...
            # blink the LED
            if row % 2 == 0:
                led.value = False
            else:
                led.value = True
            if flip:  # Bitmap is stored bottom-to-top order (normal BMP)
                f.seek(bmpImageoffset + (bmpHeight - 1 - row) * rowSize)
            else:  # Bitmap is stored top-to-bottom
                f.seek(bmpImageoffset + row * rowSize)

            rowdata = f.read(bmpWidth)
            for col in range(bmpWidth):
                for b in range(8):
                    if (rowdata[col] & (0x80 >> b) != 0 and blkpixel == 0) or (
                            rowdata[col] & (0x80 >> b) == 0 and blkpixel == 1):
                        epd.pixel(col * 8 + b, row, Adafruit_EPD.BLACK)
    except (ValueError) as e:
        display_message("Error: " + e.args[0])
    except BMPError:
        display_message("Error: unsupported BMP file " + filename)
    except OSError:
        display_message("Error: Couldn't open file " + filename)
    finally:
        f.close()
    print("Finished drawing")
    return


def read_le(s):
    result = 0
    shift = 0
    for byte in bytearray(s):
        result += byte << shift
        shift += 8
    return result


class BMPError(Exception):
    pass


# alternate bitmap display method using imageload library
def display_bitmap_alternate(epd, filename):
    image, _ = adafruit_imageload.load(
        filename, bitmap=displayio.Bitmap, palette=displayio.Palette
    )
    for y in range(display.height):
        # blink the LED
        if y % 2 == 0:
            led.value = True
        else:
            led.value = False
        for x in range(display.width):
            if image[x, y] == 0:
                epd.pixel(x, y, Adafruit_EPD.BLACK)
    return


# display message both on the screen and the serial port
def display_message(message):
    print(message)
    display.rotation = 1
    display.fill_rect(0, 10, 264, 20, Adafruit_EPD.WHITE)
    display.text(message, 10, 10, Adafruit_EPD.BLACK)
    display.display()
    return


# slide show routine
def show_files():
    try:
        filelist = os.listdir(config["slidefolder"])
        display.rotation = 1
        led.value = True
        while True:
            for file in filelist:
                starttime = time.monotonic()
                display.fill(Adafruit_EPD.WHITE)
                print("displaying file", config["slidefolder"] + "/" + file)
                display_bitmap(display, config["slidefolder"] + "/" + file)
                endtime = time.monotonic()
                minutes = (endtime - starttime) // 60
                seconds = int(endtime - starttime) - minutes * 60
                print("update time:", minutes, "minutes", seconds, "seconds")
                print("updating display")
                display.display()
                print("done")
                time.sleep(5)
    except OSError:
        display_message("Error: Couldn't display file " + file)
    led.value = False
    return

#pylint: disable-msg=too-many-branches
#pylint: disable-msg=too-many-statements
#pylint: disable-msg=too-many-locals
# run specified job
def run_job(jobfile):
    try:
        print("running job " + jobfile)
        fpr = open(config["jobfolder"] + "/" + jobfile, mode="r")
        job = json.load(fpr)
        fpr.close()
        print("image: ", job["image"])
        starttime = time.monotonic()
        pixelsize = job["bkpixelsize"]
        whitepct = job["bkratio"]
        panelcount = 6
        led.value = True

        display.rotation = 1
        display.fill(Adafruit_EPD.WHITE)
        print("ePaper display size:", display.width, display.height)
        print(config["imagefolder"] + "/" + job["image"])
        image, _ = adafruit_imageload.load(
            config["imagefolder"] + "/" + job["image"],
            bitmap=displayio.Bitmap,
            palette=displayio.Palette,
        )
        inv = False
        print("image size:", image.width, image.height)
        if image[0] == 1:
            inv = True
            print("using inv image")
        panelwidth = display.width // panelcount
        createfile = True
        try:
            out = open(config["asgfolder"] + "/asg" + job["image"], mode="wb")
            print("writing to file asg" + job["image"])
        except OSError:
            # readonly filesystem, do not create file
            createfile = False
        if createfile:  # == True
            # BMP files are all the same dimensions, just different bitmaps,
            #   writing hardcoded headers here
            # write file header (14 bytes)
            out.write(
                bytearray(
                    [
                        0x42, 0x4D, 0xFE, 0x18, 0, 0, 0, 0, 0, 0, 0x3E, 0, 0, 0
                    ]
                )
            )
            # write image header (40 bytes)
            out.write(
                bytearray(
                    [
                        0x28, 0, 0, 0, 0x8, 0x1, 0, 0, 0x50, 0xFF,
                        0xFF, 0xFF, 0x1, 0, 0x1, 0, 0, 0, 0, 0,
                        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                    ]
                )
            )
            # write 2 item color table (8 bytes)
            out.write(bytearray([0xFF, 0xFF, 0xFF, 0, 0, 0, 0, 0]))
        with open(
            config["bkfolder"] + "/background-" + str(whitepct)
            + "-" + str(pixelsize) + ".dat", "rb") as fp:
            bkdata = fp.read()
            canvas = list(bkdata)
            for y in range(0, display.height):
                # blink the LED
                if y % 2 == 0:
                    led.value = True
                else:
                    led.value = False
                tcanvas = [0 for i in range(display.width + panelwidth)]
                tpanel = [0 for i in range(panelwidth)]
                for x in range(panelwidth):
                    bytepos = ((x % panelwidth) // 8) + (
                        y // pixelsize * (panelwidth + 7) // 8
                    )
                    bitpos = x % 8
                    pixel = canvas[bytepos] & 1 << (bitpos)
                    if pixel != 0:
                        tpanel[x] = 1
                for x in range(display.width + panelwidth):
                    pixel = tpanel[x % panelwidth]
                    if pixel != 0:
                        tcanvas[x] = 1
                for x in range(0, display.width):
                    if (x % panelwidth) == 0 and x > 0:
                        for x2 in range(x, x + panelwidth):
                            tcanvas[x2] = tcanvas[x2 - panelwidth]
                        for x2 in range(panelwidth):
                            tpanel[x2] = tcanvas[x + x2 - panelwidth]
                    offset = 0
                    if (x >= 22 and
                            x < (image.width + panelwidth // 2)
                            and y < image.height):
                        if (
                                (image[x - panelwidth // 2, y] != 0 and not inv
                                ) or (
                                    image[x - panelwidth // 2, y] == 0 and inv)
                            ):
                            # offset = 4
                            if job["imagegrayscale"] == 0:
                                offset = job["imageheight"]
                            else:
                                offset = (
                                    image[x - panelwidth // 2, y]
                                    * job["grayscalecolors"]
                                    // 255
                                )
                    if offset != 0:
                        for x2 in range(x, display.width, panelwidth):
                            tcanvas[x2] = tcanvas[x2 + offset]
                    for x3 in range(0, display.width):
                        # write line to eink display
                        if tcanvas[x3] != 0:
                            display.pixel(x3, y, Adafruit_EPD.BLACK)
                        # else:
                        #    display.pixel(x,y,Adafruit_EPD.WHITE)
                    if createfile:
                        count = 0
                        for x4 in range(0, display.width + 7, 8):
                            value = 0
                            for b in range(8):
                                value |= (tcanvas[x4 + b] << 7) >> b
                            out.write(bytes([value]))
                            count += 1
                        # add padding to end of line
                        padding = (4 - (count % 4)) % 4
                        for x4 in range(padding):
                            out.write(bytes([0]))
        if createfile:
            out.close()
        endtime = time.monotonic()
        minutes = (endtime - starttime) // 60
        seconds = int(endtime - starttime) - minutes * 60
        print("completion time:", minutes, "minutes", seconds, "seconds")
        print("updating display")
        display.display()
        print("done")
    except (ValueError) as e:
        display_message("Error: " + e.args[0])
    led.value = False
    return

# main routine
display.fill(Adafruit_EPD.WHITE)
with open("/config.json") as fpx:
    config = json.load(fpx)
    fpx.close()
print("waiting for button press")
while True:
    button = read_buttons()
    if button:
        # button pressed, waiting for button release
        while read_buttons():
            time.sleep(0.1)
    if not button:
        continue
    print("Button #%d pressed" % button)
    if button == 1:
        for jobname in config["jobs"]:
            run_job(jobname)
    if button == 2:
        show_files()
    time.sleep(0.01)
This guide was first published on Aug 21, 2019. It was last updated on Aug 21, 2019. This page (Installing the Code) was last updated on Feb 05, 2020.