# 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