Digital Nose Milk Freshness Checker
2024-09-27 | By Adafruit Industries
License: See Original Project Qwiic STEMMA
Courtesy of Adafruit
Guide by Carter Nelson
Overview
The classic method for testing for milk freshness is to give it a quick sniff check. If it smells "bad", then it probably is. This is great, as long as your sense of smell is working. But maybe covid, congenital anosmia, a stuffy cold, or something else has reduced your sense of smell. What then?
In this guide we investigate the potential for using a gas sensor as a way to test for milk freshness. An SGP30 and a CLUE are used to make a little "freshness checker" device. We then use this in a simple experiment to see if it can detect spoiled milk.
This project does NOT require any soldering and is a great little science experiment.
Hardware
Here is a summary of the hardware needed for this project.
This is the battery shown in the guide:
but you can use any other similar lithium-ion battery option:
Lithium-Ion Batteries
Background Information
In this guide we use the phrase "covid" and/or "coronavirus" to refer to the Coronavirus disease 2019 (COVID-19) as caused by the severe acute respiratory syndrome coronavirus 2 (SARS-CoV-2).
Covid Related Loss of Smell
The CDC list "New loss of taste or smell" as one the main symptoms of covid. Sickness related loss of smell is nothing new, even the common cold can cause it to happen. However, due to the global pandemic scope of covid, this issue has received renewed and increased significance. There have been various news stories covering this issue and how it has impacted people's ability to "sniff" for dirty laundry, rotten food, etc.
We thought this issue would make for some great science and wanted to see if electrical sensors could be used. Here we share our results on making a milk freshness tester.
How Milk Spoils
We use the term "bad" to describe "spoiled" milk, but really, it's just nature doing its thing. There are numerous bacteria that love milk. Unfortunately for us, their consumption of milk produces byproducts that we find distasteful and can even be harmful.
Spoilage is also a vague term. As this paper (PDF) puts it:
Milk spoilage is an indefinite term and difficult to measure with accuracy.
This paper summarizes various techniques that have been investigated as a way to detect spoilage, including:
pH
electrical methods
magnetoelastic sensors
gas sensor arrays
infrared spectroscopy
protein count
It is the gas sensor based approach we are interested in here. This is analogous to what we do when we sniff milk to check freshness. Our noses are essentially gas sensors. But if covid (or something else) has knocked out your sense of smell, then your gas sensor is broken. Can we use an electrical gas sensor instead?
Electrical Nose
The general of idea of gas sensor-based milk spoilage detection has been investigated by others.
Evaluation based on change over time
Milk-sense: a volatile sensing system recognises spoilage bacteria and yeasts in milk
Conducting polymer detector for microbial volatiles
There are various gases that are discussed amongst these papers, but a common approach is to use microbial produced volatile organic compounds (VOCs) and carbon dioxide (CO2). Well, the SGP30 from Sensirion , as used on the Adafruit SGP30 STEMMA QT breakout, can detect both of these.
So, can the SPG30 be used to create an "electric nose"? Well, let's science this and find out.
Build the Checker
Let's start by putting together our CLUE Milk Freshness Checker. The CLUE and SGP-30 are the key parts. For the other items, we used some things that can hopefully be found lying around the house. If not, feel free to substitute for whatever is available to achieve the same general arrangement.
CLUE board
SGP-30 sensor
Battery
Stemma QT cable
Popsicle stick
Double sided tape
Rubber band
Put a piece of double-sided tape about the same size as the SGP-30 at one end of the popsicle stick.
Place the SGP-30 firmly onto the double-sided tape and attach the Stemma QT cable.
Use the rubber band to secure the CLUE and battery to the other end of the popsicle stick.
Plug in the Stemma QT cable and battery.
Do NOT let the SGP-30 touch the milk. It is not a moisture proof sensor.
Now move on to setting up the CLUE for CircuitPython usage and loading the code. It comes with some preset values we determined in our testing. We'll cover how you can alter these later.
CircuitPython on CLUE
CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY flash drive to iterate.
The following instructions will show you how to install CircuitPython. If you've already installed CircuitPython but are looking to update it or reinstall it, the same steps work for that as well!
Set up CircuitPython Quick Start!
Follow this quick step-by-step for super-fast Python power :)
Download the latest version of CircuitPython for CLUE from circuitpython.org
Click the link above to download the latest version of CircuitPython for the CLUE.
Download and save it to your desktop (or wherever is handy).
Plug your CLUE into your computer using a known-good USB cable.
A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.
Double-click the Reset button on the top (magenta arrow) on your board, and you will see the NeoPixel RGB LED (green arrow) turn green. If it turns red, check the USB cable, try another USB port, etc. Note: The little red LED next to the USB connector will pulse red. That's ok!
If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!
You will see a new disk drive appear called CLUEBOOT.
Drag the adafruit-circuitpython-clue-etc.uf2 file to CLUEBOOT.
The LED will flash. Then, the CLUEBOOT drive will disappear, and a new disk drive called CIRCUITPY will appear.
If this is the first time, you're installing CircuitPython or you're doing a completely fresh install after erasing the filesystem, you will have two files - boot_out.txt, and code.py, and one folder - lib on your CIRCUITPY drive.
If CircuitPython was already installed, the files present before reloading CircuitPython should still be present on your CIRCUITPY drive. Loading CircuitPython will not create new files if there was already a CircuitPython filesystem present.
That's it, you're done! :)
CLUE CircuitPython Libraries
The CLUE is packed full of features like a display and a ton of sensors. Now that you have CircuitPython installed on your CLUE, you'll need to install a base set of CircuitPython libraries to use the features of the board with CircuitPython.
Follow these steps to get the necessary libraries installed.
Installing CircuitPython Libraries on your CLUE
If you do not already have a lib folder on your CIRCUITPY drive, create one now.
Then, download the CircuitPython library bundle that matches your version of CircuitPython from CircuitPython.org.
Download the latest library bundle from circuitpython.org
The bundle downloads as a .zip file. Extract the file. Open the resulting folder.
Open the lib folder found within.
Once inside, you'll find a lengthy list of folders and .mpy files. To install a CircuitPython library, you drag the file or folder from the bundle lib folder to the lib folder on your CIRCUITPY drive.
Copy the following folders and files from the bundle lib folder to the lib folder on your CIRCUITPY drive:
adafruit_apds9960
adafruit_bmp280.mpy
adafruit_bus_device
adafruit_clue.mpy
adafruit_display_shapes
adafruit_display_text
adafruit_lis3mdl.mpy
adafruit_lsm6ds
adafruit_register
adafruit_sht31d.mpy
adafruit_slideshow.mpy
neopixel.mpy
Your lib folder should look like the image on the left. These libraries will let you run the demos in the CLUE guide.
Code
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
adafruit_sgp30.py
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.
Code
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 code.py 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( mouth_bmp, pixel_shader=mouth_pal, tile_width=40, tile_height=20, width=1, height=1, x=35, y=110, ) msg_font = bitmap_font.load_font("fonts/Alphakind_28.bdf") msg_font.load_glyphs("".join(MESSAGES)) 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") data_font.load_glyphs("eTVOC=12345?") 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() splash.append(background) splash.append(mouth) splash.append(message) splash.append(tvoc) splash.append(eco2) 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) time.sleep(15) # 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: break level += 1 if level <= len(TVOC_LEVELS): message.text = MESSAGES[level] mouth[0] = level else: message.text = "????" time.sleep(1)
Experiment
Here is the simple experiment we ran to determine the specific SPG-30 sensor values used in the code.
The experiment was carried out during February 2021 with the milk shown.
Data was collected February 28, 2021.
Each day, a small amount of milk was poured into a glass and labeled with the current date. This was done for 5 days to produce a series of milk of differing ages.
We used the code to provide a readout of the SGP-30's TVOC and eCO2 - the values shown in the grass at the bottom of the display.
At this point, don't worry about what the cow says.
Do NOT let the SGP-30 touch the milk. It is not a moisture proof sensor.
For each sample, we lowered the sensor into the glass without touching the milk, slowly moved it around, and watched the readings for about 1 minute.
We then wrote down the observed range for each gas reading.
Here is a summary of the readings we obtained:
Now we have values we can use in our code to determine milk freshness. We considered there to be three levels:
GOOD - low readings very close to fresh milk
SUS? - the readings are slightly elevated, milk is sus
BAD! - milk that has obviously gone bad
Based on the values we observed, we decided on TVOC <= 80 as the cutoff for GOOD, TVOC <= 120 as the cutoff for SUS, and anything above that as BAD!
However, different milk may age in different ways. Different environmental conditions, like temperature and humidity, may also affect the readings. Further, your personal level of comfort on what constitutes "bad" milk, may be different than others. Therefore, we suggest you repeat the above experiment.
We suggest you repeat this experiment!
Adjusting the Code
You can easily adjust the code to change the cutoff TVOC levels used for freshness detection. You can even customize what the cow says. Just look for these lines at the top of the code:
# --| User Config |------------------------- TVOC_LEVELS = (80, 120) # set two TVOC levels MESSAGES = ("GOOD", "SUS?", "BAD!") # set three messages (4 char max) # ------------------------------------------
and change as needed. Note there are only two TVOC levels to specify for a total of three freshness levels.
Other Sensor Values
We decided to keep things simple and just use the SGP-30's TVOC reading. But note in the data above that eCO2 also increased with milk age. This production of CO2 is mentioned in the research papers previously cited. Perhaps a more sophisticated approach could be used that take both readings into account? We leave that up to any intrepid scientist out there that wishes to investigate further.
Other Sensors
We tried a few other sensors as well, but they did not seem to have any useful "milk freshness" related signal. At least nothing with as 000good a signal-to-noise ratio as the SGP-30.
Other Uses
We also thought this idea could be used for detecting dirty laundry. However, that did not seem to work - at least not as easy as the approach above for milk. This may be due to the various dyes used to color clothing producing VOCs that mask any similar signal from "dirty" smells.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum