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.
Text editor powered by tinymce.