To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.
Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory PyGamer_Thermal_Camera/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.
Your CIRCUITPY drive should now look similar to the following image:
# SPDX-FileCopyrightText: 2020 Jan Goolsbey for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# Thermal_Cam_v32.py
# 2020-01-29 v3.2
# (c) 2020 Jan Goolsbey for Adafruit Industries
import time
import board
import displayio
from simpleio import map_range
from adafruit_display_text.label import Label
from adafruit_bitmap_font import bitmap_font
from adafruit_display_shapes.rect import Rect
import adafruit_amg88xx
from adafruit_pybadger import pybadger as panel
from thermal_cam_converters import celsius_to_fahrenheit, fahrenheit_to_celsius
# Load default alarm and min/max range values list from config file
from thermal_cam_config import ALARM_F, MIN_RANGE_F, MAX_RANGE_F
# Establish panel instance and check for joystick
panel.pixels.brightness = 0.1 # Set NeoPixel brightness
panel.pixels.fill(0) # Clear all NeoPixels
if hasattr(board, "JOYSTICK_X"):
panel.has_joystick = True # PyGamer
else: panel.has_joystick = False # Must be PyBadge
# Establish I2C interface for the AMG8833 Thermal Camera
i2c = board.I2C() # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller
amg8833 = adafruit_amg88xx.AMG88XX(i2c)
# Load the text font from the fonts folder
font = bitmap_font.load_font("/fonts/OpenSans-9.bdf")
# Display splash graphics and play startup tones
splash = displayio.Group()
bitmap = displayio.OnDiskBitmap("/thermal_cam_splash.bmp")
splash.append(displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader))
board.DISPLAY.root_group = splash
time.sleep(0.1) # Allow the splash to display
panel.play_tone(440, 0.1) # A4
panel.play_tone(880, 0.1) # A5
# The image sensor's design-limited temperature range
MIN_SENSOR_C = 0
MAX_SENSOR_C = 80
MIN_SENSOR_F = celsius_to_fahrenheit(MIN_SENSOR_C)
MAX_SENSOR_F = celsius_to_fahrenheit(MAX_SENSOR_C)
# Convert default alarm and min/max range values from config file
ALARM_C = fahrenheit_to_celsius(ALARM_F)
MIN_RANGE_C = fahrenheit_to_celsius(MIN_RANGE_F)
MAX_RANGE_C = fahrenheit_to_celsius(MAX_RANGE_F)
# The board's integral display size
WIDTH = board.DISPLAY.width # 160 for PyGamer and PyBadge
HEIGHT = board.DISPLAY.height # 128 for PyGamer and PyBadge
ELEMENT_SIZE = WIDTH // 10 # Size of element_grid blocks in pixels
# Default colors
BLACK = 0x000000
RED = 0xFF0000
ORANGE = 0xFF8811
YELLOW = 0xFFFF00
GREEN = 0x00FF00
CYAN = 0x00FFFF
BLUE = 0x0000FF
VIOLET = 0x9900FF
WHITE = 0xFFFFFF
GRAY = 0x444455
# Block colors for the thermal image grid
element_color = [GRAY, BLUE, GREEN, YELLOW, ORANGE, RED, VIOLET, WHITE]
# Text colors for on-screen parameters
param_list = [("ALARM", WHITE), ("RANGE", RED), ("RANGE", CYAN)]
### Helpers ###
def element_grid(col0, row0): # Determine display coordinates for column, row
x = int(ELEMENT_SIZE * col0 + 30) # x coord + margin
y = int(ELEMENT_SIZE * row0 + 1) # y coord + margin
return x, y # Return display coordinates
def flash_status(text="", duration=0.05): # Flash status message once
status_label.color = WHITE
status_label.text = text
time.sleep(duration)
status_label.color = BLACK
time.sleep(duration)
return
def update_image_frame(): # Get camera data and display
minimum = MAX_SENSOR_C # Set minimum to sensor's maximum C value
maximum = MIN_SENSOR_C # Set maximum to sensor's minimum C value
min_histo.text = "" # Clear histogram legend
max_histo.text = ""
range_histo.text = ""
sum_bucket = 0 # Clear bucket for building average value
for row1 in range(0, 8): # Parse camera data list and update display
for col1 in range(0, 8):
value = map_range(image[7 - row1][7 - col1],
MIN_SENSOR_C, MAX_SENSOR_C,
MIN_SENSOR_C, MAX_SENSOR_C)
color_index = int(map_range(value, MIN_RANGE_C, MAX_RANGE_C, 0, 7))
image_group[((row1 * 8) + col1) + 1].fill = element_color[color_index]
sum_bucket = sum_bucket + value # Calculate sum for average
minimum = min(value, minimum)
maximum = max(value, maximum)
return minimum, maximum, sum_bucket
def update_histo_frame():
minimum = MAX_SENSOR_C # Set minimum to sensor's maximum C value
maximum = MIN_SENSOR_C # Set maximum to sensor's minimum C value
min_histo.text = str(MIN_RANGE_F) # Display histogram legend
max_histo.text = str(MAX_RANGE_F)
range_histo.text = "-RANGE-"
sum_bucket = 0 # Clear bucket for building average value
histo_bucket = [0, 0, 0, 0, 0, 0, 0, 0] # Clear histogram bucket
for row2 in range(7, -1, -1): # Collect camera data and calculate spectrum
for col2 in range(0, 8):
value = map_range(image[col2][row2],
MIN_SENSOR_C, MAX_SENSOR_C,
MIN_SENSOR_C, MAX_SENSOR_C)
histo_index = int(map_range(value, MIN_RANGE_C, MAX_RANGE_C, 0, 7))
histo_bucket[histo_index] = histo_bucket[histo_index] + 1
sum_bucket = sum_bucket + value # Calculate sum for average
minimum = min(value, minimum)
maximum = max(value, maximum)
for col2 in range(0, 8): # Display histogram
for row2 in range(0, 8):
if histo_bucket[col2] / 8 > 7 - row2:
image_group[((row2 * 8) + col2) + 1].fill = element_color[col2]
else:
image_group[((row2 * 8) + col2) + 1].fill = BLACK
return minimum, maximum, sum_bucket
#pylint: disable=too-many-branches,too-many-statements
def setup_mode(): # Set alarm threshold and minimum/maximum range values
status_label.color = WHITE
status_label.text = "-SET-"
ave_label.color = BLACK # Turn off average label and value display
ave_value.color = BLACK
max_value.text = str(MAX_RANGE_F) # Display maximum range value
min_value.text = str(MIN_RANGE_F) # Display minimum range value
time.sleep(0.8) # Show SET status text before setting parameters
status_label.text = "" # Clear status text
param_index = 0 # Reset index of parameter to set
# Select parameter to set
while not panel.button.start:
while (not panel.button.a) and (not panel.button.start):
up, down = move_buttons(joystick=panel.has_joystick)
if up:
param_index = param_index - 1
if down:
param_index = param_index + 1
param_index = max(0, min(2, param_index))
status_label.text = param_list[param_index][0]
image_group[param_index + 66].color = BLACK
status_label.color = BLACK
time.sleep(0.2)
image_group[param_index + 66].color = param_list[param_index][1]
status_label.color = WHITE
time.sleep(0.2)
if panel.button.a: # Button A pressed
panel.play_tone(1319, 0.030) # E6
while panel.button.a: # wait for button release
pass
# Adjust parameter value
param_value = int(image_group[param_index + 70].text)
while (not panel.button.a) and (not panel.button.start):
up, down = move_buttons(joystick=panel.has_joystick)
if up:
param_value = param_value + 1
if down:
param_value = param_value - 1
param_value = max(MIN_SENSOR_F, min(MAX_SENSOR_F, param_value))
image_group[param_index + 70].text = str(param_value)
image_group[param_index + 70].color = BLACK
status_label.color = BLACK
time.sleep(0.05)
image_group[param_index + 70].color = param_list[param_index][1]
status_label.color = WHITE
time.sleep(0.2)
if panel.button.a: # Button A pressed
panel.play_tone(1319, 0.030) # E6
while panel.button.a: # wait for button release
pass
# Exit setup process
if panel.button.start: # Start button pressed
panel.play_tone(784, 0.030) # G5
while panel.button.start: # wait for button release
pass
status_label.text = "RESUME"
time.sleep(0.5)
status_label.text = ""
# Display average label and value
ave_label.color = YELLOW
ave_value.color = YELLOW
return int(alarm_value.text), int(max_value.text), int(min_value.text)
#pylint: enable=too-many-branches,too-many-statements
def move_buttons(joystick=False): # Read position buttons and joystick
move_u = move_d = False
if joystick: # For PyGamer: interpret joystick as buttons
if panel.joystick[1] < 20000:
move_u = True
elif panel.joystick[1] > 44000:
move_d = True
else: # For PyBadge read the buttons
if panel.button.up:
move_u = True
if panel.button.down:
move_d = True
return move_u, move_d
### Define the display group ###
image_group = displayio.Group()
# Create a background color fill layer; image_group[0]
color_bitmap = displayio.Bitmap(WIDTH, HEIGHT, 1)
color_palette = displayio.Palette(1)
color_palette[0] = BLACK
background = displayio.TileGrid(color_bitmap, pixel_shader=color_palette,
x=0, y=0)
image_group.append(background)
# Define the foundational thermal image element layers; image_group[1:64]
# image_group[#]=(row * 8) + column
for row in range(0, 8):
for col in range(0, 8):
pos_x, pos_y = element_grid(col, row)
element = Rect(x=pos_x, y=pos_y,
width=ELEMENT_SIZE, height=ELEMENT_SIZE,
fill=None, outline=None, stroke=0)
image_group.append(element)
# Define labels and values using element grid coordinates
status_label = Label(font, text="", color=BLACK)
pos_x, pos_y = element_grid(2.5, 4)
status_label.x = pos_x
status_label.y = pos_y
image_group.append(status_label) # image_group[65]
alarm_label = Label(font, text="alm", color=WHITE)
pos_x, pos_y = element_grid(-1.8, 1.5)
alarm_label.x = pos_x
alarm_label.y = pos_y
image_group.append(alarm_label) # image_group[66]
max_label = Label(font, text="max", color=RED)
pos_x, pos_y = element_grid(-1.8, 3.5)
max_label.x = pos_x
max_label.y = pos_y
image_group.append(max_label) # image_group[67]
min_label = Label(font, text="min", color=CYAN)
pos_x, pos_y = element_grid(-1.8, 7.5)
min_label.x = pos_x
min_label.y = pos_y
image_group.append(min_label) # image_group[68]
ave_label = Label(font, text="ave", color=YELLOW)
pos_x, pos_y = element_grid(-1.8, 5.5)
ave_label.x = pos_x
ave_label.y = pos_y
image_group.append(ave_label) # image_group[69]
alarm_value = Label(font, text=str(ALARM_F), color=WHITE)
pos_x, pos_y = element_grid(-1.8, 0.5)
alarm_value.x = pos_x
alarm_value.y = pos_y
image_group.append(alarm_value) # image_group[70]
max_value = Label(font, text=str(MAX_RANGE_F), color=RED)
pos_x, pos_y = element_grid(-1.8, 2.5)
max_value.x = pos_x
max_value.y = pos_y
image_group.append(max_value) # image_group[71]
min_value = Label(font, text=str(MIN_RANGE_F), color=CYAN)
pos_x, pos_y = element_grid(-1.8, 6.5)
min_value.x = pos_x
min_value.y = pos_y
image_group.append(min_value) # image_group[72]
ave_value = Label(font, text="---", color=YELLOW)
pos_x, pos_y = element_grid(-1.8, 4.5)
ave_value.x = pos_x
ave_value.y = pos_y
image_group.append(ave_value) # image_group[73]
min_histo = Label(font, text="", color=CYAN)
pos_x, pos_y = element_grid(0.5, 7.5)
min_histo.x = pos_x
min_histo.y = pos_y
image_group.append(min_histo) # image_group[74]
max_histo = Label(font, text="", color=RED)
pos_x, pos_y = element_grid(6.5, 7.5)
max_histo.x = pos_x
max_histo.y = pos_y
image_group.append(max_histo) # image_group[75]
range_histo = Label(font, text="", color=BLUE)
pos_x, pos_y = element_grid(2.5, 7.5)
range_histo.x = pos_x
range_histo.y = pos_y
image_group.append(range_histo) # image_group[76]
###--- PRIMARY PROCESS SETUP ---###
display_image = True # Image display mode; False for histogram
display_hold = False # Active display mode; True to hold display
display_focus = False # Standard display range; True to focus display range
orig_max_range_f = 0 # There are no initial range values
orig_min_range_f = 0
# Activate display and play welcome tone
board.DISPLAY.root_group = image_group
panel.play_tone(880, 0.1) # A5; ready to start looking
###--- PRIMARY PROCESS LOOP ---###
while True:
if display_hold: # Flash hold status text label
flash_status("-HOLD-")
else:
image = amg8833.pixels # Get camera data list if not in hold mode
status_label.text = "" # Clear hold mode status text label
if display_image: # Image display mode and gather min, max, and sum stats
v_min, v_max, v_sum = update_image_frame()
else: # Histogram display mode and gather min, max, and sum stats
v_min, v_max, v_sum = update_histo_frame()
# Display alarm setting and maximum, minimum, and average stats
alarm_value.text = str(ALARM_F)
max_value.text = str(celsius_to_fahrenheit(v_max))
min_value.text = str(celsius_to_fahrenheit(v_min))
ave_value.text = str(celsius_to_fahrenheit(v_sum // 64))
# Flash first NeoPixel and play alarm notes if alarm threshold is exceeded
# Second alarm note frequency is proportional to value above threshold
if v_max >= ALARM_C:
panel.pixels.fill(RED)
panel.play_tone(880, 0.015) # A5
panel.play_tone(880 + (10 * (v_max - ALARM_C)), 0.015) # A5
panel.pixels.fill(BLACK)
# See if a panel button is pressed
if panel.button.a: # Toggle display hold (shutter = button A)
panel.play_tone(1319, 0.030) # E6
while panel.button.a:
pass # wait for button release
if not display_hold:
display_hold = True
else:
display_hold = False
if panel.button.b: # Toggle image/histogram mode (display mode = button B)
panel.play_tone(659, 0.030) # E5
while panel.button.b:
pass # wait for button release
if display_image:
display_image = False
else:
display_image = True
if panel.button.select: # toggle focus mode (focus mode = select button)
panel.play_tone(698, 0.030) # F5
if display_focus:
display_focus = False # restore previous (original) range values
MIN_RANGE_F = orig_min_range_f
MAX_RANGE_F = orig_max_range_f
# update range min and max values in Celsius
MIN_RANGE_C = fahrenheit_to_celsius(MIN_RANGE_F)
MAX_RANGE_C = fahrenheit_to_celsius(MAX_RANGE_F)
flash_status("ORIG", 0.2)
else:
display_focus = True # set range values to image min/max
orig_min_range_f = MIN_RANGE_F
orig_max_range_f = MAX_RANGE_F
MIN_RANGE_F = celsius_to_fahrenheit(v_min)
MAX_RANGE_F = celsius_to_fahrenheit(v_max)
MIN_RANGE_C = v_min # update range temp in Celsius
MAX_RANGE_C = v_max # update range temp in Celsius
flash_status("FOCUS", 0.2)
while panel.button.select:
pass # wait for button release
if panel.button.start: # activate setup mode (setup mode = start button)
panel.play_tone(784, 0.030) # G5
while panel.button.start:
pass # wait for button release
# Update alarm and range values
ALARM_F, MAX_RANGE_F, MIN_RANGE_F = setup_mode()
ALARM_C = fahrenheit_to_celsius(ALARM_F)
MIN_RANGE_C = fahrenheit_to_celsius(MIN_RANGE_F)
MAX_RANGE_C = fahrenheit_to_celsius(MAX_RANGE_F)
# bottom of primary loop
The Thermal Camera needs some helpers to convert back and forth between Celsius and Fahrenheit units. This file is contained in the project zip folder as thermal_cam_converters.py.
# thermal_cam_converters.py
def celsius_to_fahrenheit(deg_c=None): # convert C to F; round to 1 degree C
return round(((9 / 5) * deg_c) + 32)
def fahrenheit_to_celsius(deg_f=None): # convert F to C; round to 1 degree F
return round((deg_f - 32) * (5 / 9))
Finally, the power-up alarm and temperature display range settings are contained in the thermal_cam_config.py file. All values are in degrees Fahrenheit.
# thermal_cam_config.py ### Alarm and range default values in Farenheit ### ALARM_F = 120 MIN_RANGE_F = 60 MAX_RANGE_F = 120
After copying all the project files to the PyGamer, you'll see the camera's splash graphics, hear a couple of beeps, and the thermal image will appear.
The next section shows the features of the camera and how it operates.
Page last edited January 18, 2025
Text editor powered by tinymce.