The Adafruit Circuit Playground Express comes equipped with an analog light sensor, but it can be used for much more than just sensing light or darkness! The light sensor has a similar spectral response to the human eye. Its connected to analog pin A8 and will return a value between 0 and 1023. A normal indoor light level reading is about 300, with higher numbers being brighter.
With a light sensor you can obviously read ambient light, but did you know you could also use it to sense color or to measure your heartbeat? For this tutorial, we will use the light sensor and the buttons to make a color matching ring of NeoPixels!
To use a light sensor to read color you just need a NeoPixel near the light sensor. Lucky for us there is one already there on the Circuit Playground Express! When we see something that’s blue, its because it is reflecting blue light back at us from the full spectrum. If we light up our pixel near the light sensor each primary color and take a reading from each, we can calculate how much of each color was reflected and duplicate it on the NeoPixels. It's not a perfect system, as our NeoPixel cannot create true primary colors, and our NeoPixel ring won’t be able to perfectly duplicate every color we try to match. We can expect to accurately replicate at least 12 colors! Just hold the object that you want to color match very close to the sensor! Press button B to take a reading, Press Button A to clear the NeoPixel ring!
The Code
Let's take a look at the completed code, then break it down into parts.
# Color Matching with the Light Sensor: Adafruit Circuit Playground Express # Created by Stephen @ core-electronics.com.au from adafruit_circuitplayground.express import cpx import time import simpleio cpx.pixels.fill((0, 0, 0)) cpx.pixels.show() while True: # when button b is pressed, detect color and set ring to that color if cpx.button_b: # set all cpx.pixels to off cpx.pixels.fill((0, 0, 0)) time.sleep(.1) # turn pixel 1 Red and read light level, turn off after cpx.pixels[1] = [255, 0, 0] redRaw = cpx.light time.sleep(.1) cpx.pixels.fill((0, 0, 0)) # turn pixel 1 to green and read light leve, turn off after cpx.pixels[1] = [0, 255, 0] greenRaw = cpx.light time.sleep(.1) cpx.pixels.fill((0, 0, 0)) # turn pixel 1 to blue and read light leve, turn off after cpx.pixels[1] = [0, 0, 255] blueRaw = cpx.light time.sleep(.1) cpx.pixels.fill((0, 0, 0)) # determine highest light reading maximum = max(redRaw, greenRaw, blueRaw) minimum = min(redRaw, greenRaw, blueRaw) # map light from between minimum light and maximum reading to 0-255 red = simpleio.map_range(redRaw, minimum, maximum, 0, 255) green = simpleio.map_range(greenRaw, minimum, maximum, 0, 255) blue = simpleio.map_range(blueRaw, minimum, maximum, 0, 255) # this simplifies detected colors into a smaller range, # removes washed out colors that just appear white if red < 30: red = 0 if green < 30: green = 0 if blue < 30: blue = 0 # converts the float values of red green and blue into intergers r = int(red) g = int(green) b = int(blue) # Serial monitor print print("Maximum Light: %i" % (maximum)) print("Minimum Light: %i" % (minimum)) print("Raw RGB: {0} {1} {2}". format(redRaw, greenRaw, blueRaw)) # send data to cpx.pixels, display nothing on bad reading if maximum - minimum <= 30: cpx.pixels.fill((0, 0, 0)) print("No Color Detected!") else: cpx.pixels.fill((r, g, b)) print("Output RGB: {0} {1} {2} ". format(r, g, b)) print() # turn all cpx.pixels off when button A is pressed if cpx.button_a: cpx.pixels.fill((0, 0, 0))
Take the Readings
The first thing that we do in the program is wait for a button B press. When the button is pressed the color sensing process begins. We first set all the pixels off, so any previously displayed colors don’t interfere with the sensing of a new color. There is a small sleep time between each light action just to make sure the NeoPixel has sufficient time to react. We then turn the nearest pixel to the light sensor on red and take a sensor reading while its on. We repeat this for green and blue.
while True: # when button b is pressed, detect color and set ring to that color if cpx.button_b: # set all cpx.pixels to off cpx.pixels.fill((0, 0, 0)) time.sleep(.1) # turn pixel 1 Red and read light level, turn off after cpx.pixels[1] = [255, 0, 0] redRaw = cpx.light time.sleep(.1) cpx.pixels.fill((0, 0, 0)) # turn pixel 1 to green and read light leve, turn off after cpx.pixels[1] = [0, 255, 0] greenRaw = cpx.light time.sleep(.1) cpx.pixels.fill((0, 0, 0)) # turn pixel 1 to blue and read light leve, turn off after cpx.pixels[1] = [0, 0, 255] blueRaw = cpx.light time.sleep(.1) cpx.pixels.fill((0, 0, 0))
Calculate the Colors
This part of the code begins calculating color by first determining what the highest and lowest value read was and storing them as minimum and maximum light level detected. This becomes our range for our raw light input. We then remap the range of detected light levels to a range of 0-255 so it can be used as an output value for our NeoPixels. Making the range this way simplifies the detected colors because the input color will be mapped to 0 and the highest will be mapped to 255. Since we are replicating the colors on NeoPixels that don’t duplicate colors in a way that looks the same to our eyes, this ends up creating a much more satisfying result. To simplify the color reading further we then remove any RGB value under 30. This allows us to display true Red Green and Blue. There is almost always a bit of another color detected, so this makes it display just red, green, or blue when its close to those colors.
# determine highest light reading maximum = max(redRaw, greenRaw, blueRaw) minimum = min(redRaw, greenRaw, blueRaw) # map light from between minimum light and maximum reading to 0-255 red = simpleio.map_range(redRaw, minimum, maximum, 0, 255) green = simpleio.map_range(greenRaw, minimum, maximum, 0, 255) blue = simpleio.map_range(blueRaw, minimum, maximum, 0, 255) # this simplifies detected colors into a smaller range, # removes washed out colors that just appear white if red < 30: red = 0 if green < 30: green = 0 if blue < 30: blue = 0
Output
The readings taken from the light sensor on the Circuit Playground Express are an analog float value, this means that it’s a variable with lots of decimal places. The NeoPixels need an integer (no decimal places). We convert the float values of red, green, and blue to integers and store them as r, g, b.
We then print the readings to the serial monitor. This helps with troubleshooting or if you want to make some changes to the way colors are simplified. Finally, there is some logic to determine if the reading was good or not. If the difference between the minimum and maximum reading is too small, it most likely means that there is nothing above the color sensor. If we have a bad reading, then the lights are left off and “No Color Detected” is printed. If there was a good color sensing, then we set all the pixels to the detected color and print the RGB value.
# converts the float values of red green and blue into intergers r = int(red) g = int(green) b = int(blue) # Serial monitor print print("Maximum Light: %i" % (maximum)) print("Minimum Light: %i" % (minimum)) print("Raw RGB: {0} {1} {2}". format(redRaw, greenRaw, blueRaw)) # send data to cpx.pixels, display nothing on bad reading if maximum - minimum <= 30: cpx.pixels.fill((0, 0, 0)) print("No Color Detected!") else: cpx.pixels.fill((r, g, b)) print("Output RGB: {0} {1} {2} ". format(r, g, b)) print() # turn all cpx.pixels off when button A is pressed if cpx.button_a: cpx.pixels.fill((0, 0, 0))
The readings taken from the light sensor on the Circuit Playground Express are an analog float value, this means that it’s a variable with lots of decimal places. The NeoPixels need an integer (no decimal places). We convert the float values of red, green, and blue to integers and store them as r, g, b.
We then print the readings to the serial monitor. This helps with troubleshooting or if you want to make some changes to the way colors are simplified. Finally, there is some logic to determine if the reading was good or not. If the difference between the minimum and maximum reading is too small, it most likely means that there is nothing above the color sensor. If we have a bad reading, then the lights are left off and “No Color Detected” is printed. If there was a good color sensing, then we set all the pixels to the detected color and print the RGB value.
While color sensing using a light sensor isn’t necessarily the best way to read color, it does allow us to sense color with the Circuit Playground Express with no additional hardware. If you really want to detect color accurately you can use a dedicated IR color sensor. I have found that these read colors very accurately, but I still must simplify the detected colors to replicate them on an LED. Using a light sensor gives just as useable results for this sort of application, you just might need to take a couple readings before the color is just right.
If you want to learn more about the Adafruit Circuit Playground Express and CircuitPython, head over to our Circuit Playground Tutorials for great info on using the various sensors and lights! If you are an educator or new to programming, this same project can be done using MakeCode and we even have a tutorial for that!