Here's how to load the code and assets as well as some additional libraries you'll need.

Additional Libraries

In addition to the main libraries needed for CLUE support covered in the previous section, you'll also need to install the libraries listed here.

You can download the latest library bundle from the CircuitPython webpage.

Make sure you have a copy of these in your CIRCUITPY/lib folder.

  • adafruit_bitmap_font
  • adafruit_imageload

Here's a summary:

Freshness Indication

The code below comes with preset values for TVOC_LEVELS used to determine milk freshness. In the next section we discuss the experiment we ran to determine these values as well as how you can alter them. Once set, the CLUE display will indicate milk "freshness" as follows:

  • GOOD + smiling cow = the TVOC levels are very low, so milk should be good.
  • SUS? + confused cow = the TVOC levels are high enough that the milk may be starting to turn.
  • BAD! + frowning cow = the TVOC level are high enough the milk should be considered bad.


Use the Project ZIP link below to download the code as well as the bitmaps and font files used in a single zip file.

Drag the entire bmps and fonts folders to your CIRCUITPY folder. These assets will live in those subfolders.

Save the code listing as into your CIRCUITPY folder so it will run automatically when powered up.

# SPDX-FileCopyrightText: 2021 Carter Nelson for Adafruit Industries
# SPDX-License-Identifier: MIT

import time
import board
import displayio
import adafruit_sgp30
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
import adafruit_imageload
from adafruit_clue import clue

# --| User Config |-------------------------
TVOC_LEVELS = (80, 120)  # set two TVOC levels
MESSAGES = ("GOOD", "SUS?", "BAD!")  # set three messages (4 char max)
# ------------------------------------------

# setup UI
cow_bmp, cow_pal = adafruit_imageload.load("bmps/milk_bg.bmp")
background = displayio.TileGrid(cow_bmp, pixel_shader=cow_pal)

mouth_bmp, mouth_pal = adafruit_imageload.load("bmps/mouth_sheet.bmp")
mouth = displayio.TileGrid(

msg_font = bitmap_font.load_font("fonts/Alphakind_28.bdf")
message = label.Label(msg_font, text="WAIT", color=0x000000)
message.anchor_point = (0.5, 0.5)
message.anchored_position = (172, 38)

data_font = bitmap_font.load_font("fonts/F25_Bank_Printer_Bold_12.bdf")
tvoc = label.Label(data_font, text="TVOC=?????", color=0x000000)
tvoc.anchor_point = (0, 1)
tvoc.anchored_position = (5, 235)

eco2 = label.Label(data_font, text="eCO2=?????", color=0x000000)
eco2.anchor_point = (0, 1)
eco2.anchored_position = (130, 235)

splash = displayio.Group()
clue.display.root_group = splash

# setup SGP30 and wait for initial warm up
i2c = board.I2C()  # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C()  # For using the built-in STEMMA QT connector on a microcontroller
sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c)

# loop forever
while True:
    eCO2, TVOC = sgp30.iaq_measure()

    tvoc.text = "TVOC={:5d}".format(TVOC)
    eco2.text = "eCO2={:5d}".format(eCO2)

    level = 0
    for thresh in TVOC_LEVELS:
        if TVOC <= thresh:
        level += 1

    if level <= len(TVOC_LEVELS):
        message.text = MESSAGES[level]
        mouth[0] = level
        message.text = "????"


This guide was first published on Mar 03, 2021. It was last updated on Feb 09, 2024.

This page (Code) was last updated on Feb 09, 2024.

Text editor powered by tinymce.