Fruit Jam Pong Tutorial: 1. Bootstrap
I’ll be honest, I got tired of writing the same code over and over again. Plus, I wanted a place to compile the most up-to-date bare essentials for a well-written Fruit Jam application. Thus, Fruit_Jam_Application was born! This template repository serves as a place to quickly start up your own code repository and support the following goodies:
- Display and audio configuration with Fruit Jam OS launcher config support
- USB Host mouse and keyboard support
- Multi-tasking with
asyncio
(not required, but recommended) - Fruit Jam OS metadata and icon
- Project Bundle build script with GitHub Build CI and Release workflows
Plus, I threw in a little “Hello, World!” to make sure everything is working.
Using the Bootstrap Template
To use this public template repository, you will need to fork it onto your own GitHub account. If you don’t have one… make one! By forking this repository into your account, it essentially makes it available (in its current state) within your account when you create a new repository. And if I ever make major changes to the template, you can update your forked repository by using the “Sync Changes” feature within GitHub.
Now, you can create a new repository for your application using the template (more info). Make sure to name your repository something like “Fruit_Jam_…” to make it clear that it’s a Fruit Jam compatible program. In my case, I used “Fruit_Jam_Pong”.
Alternatively, you can download the latest version of the template available here and build on top of it on your local machine. This may actually be recommended if your just getting started and aren’t ready to start using git repositories.
Preparing Your Application
README
If you are using GitHub to create your application, the first thing people will see when checking out your code is the README.md
file (more info). It’s pretty important that it has the correct information on it. So, let’s get started by opening that file and changing the title and description to better match your program.
Metadata
In a similar vein, the metadata for Fruit Jam OS is stored within metadata.json
. You’ll want to open this file up in your code editor and change the "title"
value to your application’s name, in my case “Pong”.
It will also display the icon for the application according to the icon.bmp
file which supports a resolution of 64x64 pixels. Feel free to edit this to your hearts desire to display how you would like within the Fruit Jam OS launcher.
Install dependencies
The base bootstrap application does require a handful of libraries to help it manage hardware resources among other things. In order to get them installed, we will need circup
, a super handy tool for managing Adafruit and Community CircuitPython libraries on your device. Follow this guide to get it set up on your computer.
Once you’ve done that and ensured that your Fruit Jam is connected and mounted to your computer, you should be able to navigate to the path of your code files within your terminal and run the following command:
circup install -r requirements.txt
This will install the following library dependencies to your device:
adafruit_display_text
- Writes text to the displayadafruit_fruitjam
- Manages Fruit Jam hardware and configures display outputadafruit_pathlib
- File path calculations (not too important, but it helps us load libraries when installed within Fruit Jam OS)adafruit_usb_host_mouse
- Driver for USB Boot-compatible Mouse withdisplayio
supportasyncio
- Cooperative multi-tasking (very helpful when managing program logic and USB devices)
By the time we’re done, we’ll be adding a few more libraries to this list, but this is enough to get us started.
Run code.py
Ready to get your Fruit Jam running? All you need to for now is copy over the code.py
into your CIRCUITPY
drive and replace the existing file. If all goes well, you should see “Hello, World!” in the center of your display and if you plug in a USB mouse, a mouse cursor should display which you can move around. Nothing too special, but pretty cool nonetheless!
If you’re using Thonny, you can also evaluate the code directly by copying and pasting it into the file editor or opening the existing
code.py
file and hitting the “Run current script” button. You’ll also be able to see the serial debug output over REPL.
Auto-Reload
We also have a file called boot.py
. You may have noticed that your device automatically reloaded when code.py
was replaced. This is called “auto-reload” where CircuitPython automagically determines when to restart your code file based on USB operations.
Depending on your host computer and a number of other variables, this might just get in the way. If you start to notice that your program just randomly restarts from time to time, you may have to turn auto-reload off! The provided boot.py
file runs once on start up and disables this feature. Just copy it over to your device and perform a hard reset (either hitting the reset button or switch the power switch on and off) for it to start working.
However, if you plan to manage updates to your code either by editing the file in place on your computer or copying it over, you’ll want to ignore this step and keep auto-reload on.
How it Works
I’ll do my best here to quickly glance over the important bits of the bootstrap code, but feel free to skip ahead if this isn’t your cup of tea.
Dynamic Module Loading
# load included modules if we aren't installed on the root path
if len(__file__.split("/")[:-1]) > 1:
import adafruit_pathlib as pathlib
if (modules_directory := pathlib.Path("/".join(__file__.split("/")[:-1])) / "lib").exists():
import sys
sys.path.append(str(modules_directory.absolute()))
Right at the beginning, we get started with lovely bit of code. Normally, CircuitPython searches for libraries in /
and /lib
. If our program is copied into a subfolder of CIRCUITPY
or on an sd card and the libraries haven’t been merged into /lib
, it will likely crash. This snippet attempts to add the local lib
folder into sys.path
which is used for looking for modules. So, if someone downloads your program, extracts it, and installs it directly into /apps/Fruit_Jam_...
or /sd/apps/Fruit_Jam_...
for use with Fruit Jam OS, it should work without a hitch!
Launcher Config
# get Fruit Jam OS config if available
try:
import launcher_config
config = launcher_config.LauncherConfig()
except ImportError:
config = None
Fruit Jam OS includes the file /launcher_config.py
which is used to read settings from launcher.conf.json
(which can be installed in /
, /saves
, or /sd
). These settings are used to set up the display resolution, audio output destination, and volume among other things. If the module can’t be loaded, that likely means we aren’t using Fruit Jam OS. You’ll see later on where we refer to this file or use a default value if it isn’t found.
Display Configuration
# setup display
displayio.release_displays()
try:
adafruit_fruitjam.peripherals.request_display_config() # user display configuration
except ValueError: # invalid user config or no user config provided
adafruit_fruitjam.peripherals.request_display_config(720, 400) # default display size
display = supervisor.runtime.display
Since we installed the board-specific version of CircuitPython, it knows how our Fruit Jam’s hardware is configured and goes ahead and outputs video via the HDMI display (notice how REPL worked on monitor earlier). This is great, but it might not be exactly how we need it for our program.
We start out with displayio.release_displays()
which deactivates the REPL terminal. Then, we try to use the user display resolution configuration if its available (more info). If this doesn’t work, we try to use our preferred resolution, 720x400 in this case (see this documentation for supported resolutions). Then, we create a local reference to the system’s display output, supervisor.runtime.display
for future use.
As you’ll soon see, we’re actually going to modify this portion of our program quite a bit in the next section to better suit our needs.
Hardware Peripherals & Audio
# setup audio, buttons, and neopixels
peripherals = adafruit_fruitjam.peripherals.Peripherals(
safe_volume_limit=(config.audio_volume_override_danger if config is not None else 12),
)
# user-defined audio output and volume
if config is not None:
peripherals.audio_output = config.audio_output
peripherals.volume = config.audio_volume
else:
peripherals.audio_output = "headphone"
peripherals.volume = 12
The adafruit_fruitjam.peripherals.Peripherals
(documentation) handles essentially all of our hardware resources. This is great, because it acts like an abstraction layer so that we can focus more on writing fun programs!
The only configuration we need to do is set up a few audio settings so that they work with our Fruit Jam OS launcher configuration that we talked about earlier. You can change the defaults here if you’d prefer to use different settings (ie: "speaker"
for audio output).
Drawing Text
# create root group
root_group = displayio.Group()
display.root_group = root_group
# example text
root_group.append(Label(
font=FONT, text="Hello, World!",
anchor_point=(.5, .5),
anchored_position=(display.width//2, display.height//2),
))
First of all, this tutorial is going to require some knowledge of the core module,
displayio
, since it is going to be used to generate all of our graphics. You may need to glance over some displayio guides to build up a foundation of knowledge on this topic in case I’m moving too fast.
In order to drive a display in CircuitPython with displayio
, we’ll need to set up a root group which all of our elements will be contained within. Simple stuff!
Then, I’m using the adafruit_display_text
library (documentation) to draw some basic text centered in the middle of the display and adding that to our group.
Mouse Control Task
# mouse control
async def mouse_task() -> None:
while True:
if (mouse := adafruit_usb_host_mouse.find_and_init_boot_mouse("bitmaps/cursor.bmp")) is not None:
mouse.x = display.width // 2
mouse.y = display.height // 2
root_group.append(mouse.tilegrid)
timeouts = 0
previous_pressed_btns = []
while timeouts < 9999:
pressed_btns = mouse.update()
if pressed_btns is None:
timeouts += 1
else:
timeouts = 0
if "left" in pressed_btns and (previous_pressed_btns is None or "left" not in previous_pressed_btns):
pass
previous_pressed_btns = pressed_btns
await asyncio.sleep(1/30)
root_group.remove(mouse.tilegrid)
await asyncio.sleep(1)
Here comes our first
asyncio
task. I’m going to be usingasyncio
extensively in this tutorial. You may want to take a look at the guide in case you want more detail on how this system works.
Directly talking to USB devices can be quite the chore. Instead of doing it all ourselves, we’re going to take advantage of the adafruit_usb_host_mouse
library. We’ll use the method adafruit_usb_host_mouse.find_and_init_boot_mouse
to search if a mouse is connected once every second. Once a compatible mouse is found, we center the cursor and add the provided displayio.TileGrid
to our root group.
After that, we start up another loop which will keep track of the mouse activity to decide whether it has been disconnected. If not, it can process user input. This happens 30 times every second. You’ll see this number often as a typical polling rate in our application as it is the maximum frame rate we are shooting for.
There are a few pending updates to this library which might result in this control task changing slightly in the future.
Keyboard Control Task
async def keyboard_task() -> None:
# flush input buffer
while supervisor.runtime.serial_bytes_available:
sys.stdin.read(1)
while True:
while (c := supervisor.runtime.serial_bytes_available) > 0:
key = sys.stdin.read(c)
if key == "\x1b": # escape
peripherals.deinit()
supervisor.reload()
await asyncio.sleep(1/30)
USB keyboard support is going to work a little differently than other USB devices. The CircuitPython core is already designed to handle keyboard input internally. Rather than try to take over on our side, we’re just going to tap into that feed using sys.stdin
. This also allows control of our application over serial which can be useful in some scenarios.
At the moment, we’re not doing much except for checking for the escape key which we’ll use to reload the device. By the way, most special keys will start with "\x1b..."
. We’ll run across that later when dealing with arrow keys.
Main Loop
async def main() -> None:
await asyncio.gather(
asyncio.create_task(mouse_task()),
asyncio.create_task(keyboard_task()),
)
try:
asyncio.run(main())
except KeyboardInterrupt:
peripherals.deinit()
In order to run these tasks concurrently, we’ll need to create a main loop and use the asyncio.gather
method to combine our mouse and keyboard tasks. We’ll also check for KeyboardInterrupt
here (Ctrl+C) as another method of exiting from the application.
Any time we exit out of our code, we want to call
peripherals.deinit()
to release all of the hardware resources controlled byadafruit_fruitjam.peripherals.Peripherals
. This includes buttons, neopixels, and audio.
Final Code
Although we didn’t do too much to it, your code should now look something like this:
Next Steps
Enough of this bootstrap nonsense. Let’s get to writing code! Move forward to the next section, Graphics!