Download the Project Bundle
Your project will use a specific set of CircuitPython libraries and .py files. 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 board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import board
import supervisor
import terminalio
from adafruit_fruitjam import FruitJam
from adafruit_fruitjam.peripherals import request_display_config
BG_COLOR = 0x0000FF
# use built-in system font
OVERLAY_FONT = terminalio.FONT
# or un-comment to use a custom font. Fill in the path to your font file.
# OVERLAY_FONT = "Free_Mono_10.pcf"
request_display_config(320, 240)
display = supervisor.runtime.display
FETCH_DELAY = 12
# Set up where we'll be fetching data from
DATA_SOURCE = "https://www.adafruit.com/api/products/6358"
TITLE_LOCATION = ["product_name"]
SALE_PRICE_LOCATION = ["product_sale_price"]
REG_PRICE_LOCATION = ["product_price"]
STOCK_LOCATION = ["product_stock"]
config_index = 0
TEXT_POSITIONS = [
{
"title_anchor_point": (0, 0),
"title_anchored_position": (4, 205 + 4),
"stock_anchor_point": (1.0, 1.0),
"stock_anchored_position": (display.width - 4, display.height - 4),
"price_anchor_point": (1.0, 0),
"price_anchored_position": (display.width - 4, 200),
"outline_size": 1,
"outline_color": 0x000000,
},
{
"title_anchor_point": (0, 0),
"title_anchored_position": (4, 205 + 4),
"stock_anchor_point": (1.0, 1.0),
"stock_anchored_position": (display.width - 4, display.height - 4),
"price_anchor_point": (1.0, 0),
"price_anchored_position": (display.width - 4, 200),
"outline_size": 0,
"outline_color": 0x000000,
},
{
"title_anchor_point": (0, 0),
"title_anchored_position": (4, 4),
"stock_anchor_point": (1.0, 0),
"stock_anchored_position": (display.width - 4, 26),
"price_anchor_point": (1.0, 0),
"price_anchored_position": (display.width - 4, 4),
"outline_size": 0,
"outline_color": 0x000000,
},
{
"title_anchor_point": (0, 0),
"title_anchored_position": (4, 4),
"stock_anchor_point": (1.0, 0),
"stock_anchored_position": (display.width - 4, 26),
"price_anchor_point": (1.0, 0),
"price_anchored_position": (display.width - 4, 4),
"outline_size": 1,
"outline_color": 0x000000,
},
{
"title_anchor_point": (0, 0),
"title_anchored_position": (4, -104),
"stock_anchor_point": (1.0, 0),
"stock_anchored_position": (display.width - 4, -126),
"price_anchor_point": (1.0, 0),
"price_anchored_position": (display.width - 4, -104),
"outline_size": 1,
"outline_color": 0x000000,
},
]
def apply_hotkey_visuals(index):
config = TEXT_POSITIONS[index]
fruitjam.text_fields[0]["anchor_point"] = config["title_anchor_point"]
fruitjam.text_fields[0]["position"] = config["title_anchored_position"]
fruitjam.text_fields[0]["outline_color"] = config["outline_color"]
fruitjam.text_fields[0]["outline_size"] = config["outline_size"]
if fruitjam.text_fields[0]["label"] is not None:
fruitjam.text_fields[0]["label"].anchor_point = config["title_anchor_point"]
fruitjam.text_fields[0]["label"].anchored_position = config[
"title_anchored_position"
]
fruitjam.text_fields[0]["label"].outline_size = config["outline_size"]
fruitjam.text_fields[0]["label"].outline_color = config["outline_color"]
fruitjam.text_fields[1]["anchor_point"] = config["stock_anchor_point"]
fruitjam.text_fields[1]["position"] = config["stock_anchored_position"]
fruitjam.text_fields[1]["outline_color"] = config["outline_color"]
fruitjam.text_fields[1]["outline_size"] = config["outline_size"]
if fruitjam.text_fields[1]["label"] is not None:
fruitjam.text_fields[1]["label"].anchor_point = config["stock_anchor_point"]
fruitjam.text_fields[1]["label"].anchored_position = config[
"stock_anchored_position"
]
fruitjam.text_fields[1]["label"].outline_size = config["outline_size"]
fruitjam.text_fields[1]["label"].outline_color = config["outline_color"]
fruitjam.text_fields[2]["anchor_point"] = config["price_anchor_point"]
fruitjam.text_fields[2]["position"] = config["price_anchored_position"]
fruitjam.text_fields[2]["outline_color"] = config["outline_color"]
fruitjam.text_fields[2]["outline_size"] = config["outline_size"]
if fruitjam.text_fields[2]["label"] is not None:
fruitjam.text_fields[2]["label"].anchor_point = config["price_anchor_point"]
fruitjam.text_fields[2]["label"].anchored_position = config[
"price_anchored_position"
]
fruitjam.text_fields[2]["label"].outline_size = config["outline_size"]
fruitjam.text_fields[2]["label"].outline_color = config["outline_color"]
def format_data(json_data):
if "product_sale_price" in json_data:
json_data["product_sale_price"] = f'${json_data["product_sale_price"]}'
else:
json_data["product_sale_price"] = f'${json_data["product_price"]}'
json_data["product_stock"] = f'Stock: {json_data["product_stock"]}'
# the current working directory (where this file is)
cwd = ("/" + __file__).rsplit("/", 1)[0]
fruitjam = FruitJam(
url=DATA_SOURCE,
json_path=(TITLE_LOCATION, STOCK_LOCATION, SALE_PRICE_LOCATION),
status_neopixel=board.NEOPIXEL,
default_bg=BG_COLOR,
json_transform=[format_data],
debug=True,
)
fruitjam.remove_all_text()
fruitjam.add_text(
text_font=OVERLAY_FONT, text_wrap=35, text_maxlen=180, text_color=0xFFFFFF, outline_size=1
) # title
fruitjam.add_text(
text_font=OVERLAY_FONT, text_wrap=0, text_maxlen=30, text_color=0xFFFFFF, outline_size=1
) # stock
fruitjam.add_text(
text_font=OVERLAY_FONT, text_wrap=0, text_maxlen=30, text_color=0xFFFFFF, outline_size=1
) # price
apply_hotkey_visuals(config_index)
fruitjam.neopixels.brightness = 0.1
fetch_success = False
data_values = None
last_fetch_time = 0
while not fetch_success:
try:
data_values = fruitjam.fetch()
fruitjam._text[2]["label"].scale = 2 # pylint: disable=protected-access
fetch_success = True
last_fetch_time = time.monotonic()
except ValueError as e:
# no value found for product_sale_price
print("product_sale_price not found. Using regular price.")
print(fruitjam.json_path)
fruitjam.json_path = (TITLE_LOCATION, STOCK_LOCATION, ["product_price"])
print(fruitjam.json_path)
time.sleep(5)
except (RuntimeError, ConnectionError, OSError) as e:
print("Some error occured, retrying! -", e)
time.sleep(5)
old_btn1 = False
old_btn2 = False
old_btn3 = False
while True:
btn1_pressed = fruitjam.button1
if btn1_pressed and not old_btn1:
config_index = (config_index + 1) % len(TEXT_POSITIONS)
apply_hotkey_visuals(config_index)
now = time.monotonic()
if last_fetch_time + FETCH_DELAY <= now:
try:
data_values = fruitjam.fetch()
print(f"type: {fruitjam.text_fields[1]['label']}")
fruitjam._text[2]["label"].scale = 2 # pylint: disable=protected-access
fetch_success = True
last_fetch_time = time.monotonic()
except (ValueError, RuntimeError, ConnectionError, OSError) as e:
print("Some error occured, retrying! -", e)
old_btn1 = btn1_pressed
This project creates a live product information overlay that fetches data from Adafruit's website and displays it on your HDMI monitor via the RP2350's HSTX DVI output. Below breaks down how each section works:
Display Setup and Configuration
The code starts by requesting a 320x240 display configuration through the FruitJam peripheral system. This sets up the HSTX DVI video output to your HDMI display. The supervisor.runtime.display gives us access to CircuitPython's built-in display driver that's already configured for DVI output.
from adafruit_fruitjam.peripherals import request_display_config BG_COLOR = 0x0000FF request_display_config(320, 240) display = supervisor.runtime.display
Data Source Configuration
These variables define what data to fetch and where to find it in the JSON response. The lists contain JSONPath expressions. In this case, simple field names that tell the FruitJam library exactly which values to extract from Adafruit's product API.
DATA_SOURCE = "https://www.adafruit.com/api/products/6358" TITLE_LOCATION = ["product_name"] SALE_PRICE_LOCATION = ["product_sale_price"] REG_PRICE_LOCATION = ["product_price"] STOCK_LOCATION = ["product_stock"]
Visual Layout Presets
The TEXT_POSITIONS array contains five different overlay configurations. Each preset defines:
- anchor_point: How text is aligned (0,0 = top-left, 1,1 = bottom-right)
- anchored_position: Pixel coordinates for placement
- outline_size/color: Text outline styling for readability over any background
For example, the first preset places the title at the bottom-left with a black outline, while preset 3 moves everything to the top of the screen. This gives you different overlay styles depending on your content.
Dynamic Layout
This function updates both the internal FruitJam configuration and any existing on-screen labels when switching between visual presets. It handles both the data structure and the actual DisplayIO label objects.
def apply_hotkey_visuals(index):
config = TEXT_POSITIONS[index]
fruitjam.text_fields[0]["anchor_point"] = config["title_anchor_point"]
# ... applies config to all three text fields
Data Formatting
Before displaying the data, this function adds proper formatting - dollar signs for prices and "Stock:" labels. It also handles the case where a product isn't on sale by using the regular price instead.
def format_data(json_data):
if "product_sale_price" in json_data:
json_data["product_sale_price"] = f'${json_data["product_sale_price"]}'
else:
json_data["product_sale_price"] = f'${json_data["product_price"]}'
json_data["product_stock"] = f'Stock: {json_data["product_stock"]}'
FruitJam Library Initialization
The FruitJam library handles all the networking, JSON parsing, and display management. It's configured with:
- The API endpoint to fetch from
- Which JSON fields to extract
- A NeoPixel for network status indication
- The background color for the display
- The data formatting function to apply before display
fruitjam = FruitJam(
url=DATA_SOURCE,
json_path=(TITLE_LOCATION, STOCK_LOCATION, SALE_PRICE_LOCATION),
status_neopixel=board.NEOPIXEL,
default_bg=BG_COLOR,
json_transform=[format_data],
debug=True,
)
Text Field Configuration
This sets up three text fields with different properties. The title field has text wrapping enabled (35 characters per line) and can hold longer text, while the stock and price fields are shorter with no wrapping.
The text positions and sizes are optimized for the default typeface, but you could pick your own using the fruitjam.add_text(text_font=xxxx) call.
fruitjam.remove_all_text() fruitjam.add_text(text_wrap=35, text_maxlen=180, text_color=0xFFFFFF, outline_size=1) # title fruitjam.add_text(text_wrap=0, text_maxlen=30, text_color=0xFFFFFF, outline_size=1) # stock fruitjam.add_text(text_wrap=0, text_maxlen=30, text_color=0xFFFFFF, outline_size=1) # price
Network Error Handling
The main loop handles two key functions: button debouncing for layout switching and automatic data refresh every 12 seconds. The button press cycles through the five visual presets, while the timer ensures your product information stays current.
while True:
btn1_pressed = fruitjam.button1
if btn1_pressed and not old_btn1:
config_index = (config_index + 1) % len(TEXT_POSITIONS)
apply_hotkey_visuals(config_index)
now = time.monotonic()
if last_fetch_time + FETCH_DELAY <= now:
# Refresh data every 12 seconds
DVI Video Output
While not explicitly shown in this code, the magic happens through CircuitPython's built-in support for the RP2350's HSTX peripheral. When you call request_display_config(), it configures the HSTX pins for DVI output, and supervisor.runtime.display provides a standard DisplayIO interface that automatically sends your graphics to the HDMI display. The FruitJam library uses this display object to render text overlays with hardware acceleration.
Page last edited September 22, 2025
Text editor powered by tinymce.