Let's take a look at the code to see what everything is doing, and so that you can calibrate your artwork to work in your location.
There are several important changes to make, so don't skip this section!
What follows are small sections of code from the full program. To download the whole thing (and associated fonts, libraries, etc.), use the “Download Project Bundle” button on the prior page. Then return here to see what different parts of the code do.
First, we import all the necessary libraries.
import time import board import neopixel from adafruit_clue import clue import adafruit_fancyled.adafruit_fancyled as fancy import displayio from adafruit_display_text import label from adafruit_bitmap_font import bitmap_font
The next few sections are where we'll do most of our customization.
num_leds = 79 # number of LEDs in your strip timeToCheck = 100 # set the amount of time between sensor checks. 7800 is approx. 1 hour
Change num_leds
to reflect the number of LEDs in your strip. Mine has 79 lights.
If you have fewer than 79 lights this will break the code, temporarily!
Don't worry! We'll fix it in the Mapping Colors to Pixels section below.
The timeToCheck
variable allows you to set the length of time between sensor checks. This isn't counting seconds, it's counting up each time the code runs. I used a stopwatch to determine that my CLUE board counts to around 7800 every hour.
In general, barometers should be checked every hour to three hours to read a meaningful change in pressure. For testing purposes you can set it to around 20 - 30 to read the sensor and update the strip every few seconds. For actual usage, I am only checking data every 3 hours so I've set this to 23400 (7800 x 3).
Barometer or Thermometer?
The CLUE's onboard barometric pressure sensor also has a temperature sensor built in. That means it's easy peezy to change this project into a thermometer instead of a barometer just by changing a couple lines of code.
I've included sample code for both types of project. Comment out the Barometer section and uncomment the Thermometer section if you'd prefer to build a temperature visualizer.
Setting Threshold Numbers
This section is also where you customize the thresholds for color visualization. Since barometers vary so much by where they're located, the best thing to do is to take a few readings at your location to find low and high pressure, and adjust these numbers as needed. My house is at around 1300 feet of elevation in a moderate weather zone in Northern California. I took a reading on a few nice sunny days to get a high threshold (around 972), and on a day when a thunderstorm was predicted to get a low threshold (around 945), then divided up my numbers into an even spread.
A low desert in Arizona or an island in the middle of the ocean would have pretty different thresholds. Barometers get less reliable as elevation goes up, and above 5000 feet they're pretty useless, so if you live in the mountains, make a thermometer instead.
The units for pressure are in hectopascals (hPa) or for temperature are in C.
# Barometer or Thermometer? Uncomment the section you want to use # BAROMETER RANGES (hPa) # set desired reading range -- the NeoPixel palette choice will be determined by these thresholds deviceType = 0 min_reading = 965 med_reading = 965 high_reading= 970 max_reading = 975 """ # THERMOMETER RANGES (C) # set desired temperature range -- the NeoPixel palette choice will be determined by these thresholds deviceType = 1 min_reading = 25 med_reading = 27 high_reading= 31 max_reading = 33 """ #get an initial sensor reading if deviceType ==0: reading = clue.pressure else: reading = clue.temperature
Screen Images
Next we set up and draw the .bmp images. We've included a submarine image for when the pressure is sinking, and a hot air balloon image for when the pressure is rising. You can add your own custom images if you'd like. Make the images 240x240 pixels, and save as .bmp with 16 bits. It's probably easiest to name them rising.bmp and sinking.bmp -- that way you don't need to update the code with different filenames, you can just replace our files with yours and it should work fine.
clue.display.brightness = 0.8 clue_display = displayio.Group() # draw the rising image # CircuitPython 6 & 7 compatible rising_file = open("rising.bmp", "rb") rising_bmp = displayio.OnDiskBitmap(rising_file) rising_sprite = displayio.TileGrid(rising_bmp, pixel_shader=getattr(rising_bmp, 'pixel_shader', displayio.ColorConverter())) # # CircuitPython 7+ compatible # rising_bmp = displayio.OnDiskBitmap("rising.bmp") # rising_sprite = displayio.TileGrid(rising_bmp, pixel_shader=rising_bmp.pixel_shader) clue_display.append(rising_sprite) # draw the sinking image # CircuitPython 6 & 7 compatible sinking_file = open("sinking.bmp", "rb") sinking_bmp = displayio.OnDiskBitmap(sinking_file) sinking_sprite = displayio.TileGrid(sinking_bmp, pixel_shader=getattr(sinking_bmp, 'pixel_shader', displayio.ColorConverter())) # # CircuitPython 7+ compatible # sinking_bmp = displayio.OnDiskBitmap("sinking.bmp") # sinking_sprite = displayio.TileGrid(sinking_bmp, pixel_shader=sinking_bmp.pixel_shader)
Further down, we set up the images and the text using the display.io library. This is where we can customize the color and position of the text readouts that tell us the current data.
Change the position of the text by changing reading_label.x
and reading_label.y
in each text section.
# Create text # first create the group text_group = displayio.Group() # Make a label reading_font = bitmap_font.load_font("/font/RacingSansOne-Regular-29.bdf") reading_font.load_glyphs("0123456789ADSWabcdefghijklmnopqrstuvwxyz:!".encode('utf-8')) reading_label = label.Label(reading_font, color=0xffffff) reading_label.x = 10 reading_label.y = 24 text_group.append(reading_label) reading2_label = label.Label(reading_font, color=0xdaf5f4) reading2_label.x = 10 reading2_label.y = 54 text_group.append(reading2_label) reading3_label = label.Label(reading_font, color=0x4f3ab1) reading3_label.x = 10 reading3_label.y = 84 text_group.append(reading3_label) timer_label = label.Label(reading_font, color=0x072170) timer_label.x = 10 timer_label.y = 114 text_group.append(timer_label)
Customizing Color Palettes
The next section is where we set up our color palettes. I'm using hex codes for each color, but you can also use CRGB values or CHSV, or a variety of other options. Check out the FancyLED guide for more info about how this works.
If you don't care how it works and just want to change the colors, that's okay too! Use an online hex code picker like this one. You can put as few or as many different colors in each palette as you'd like. Just select your color, copy the 6-digit hex code at the top of the screen, and place it into the code, copying the current format: i.e. for pure green, the hex code is #00FF00, so you'd use 0x00ff00
to express green.
These palettes are set up to give a few different shades of each color in the rainbow. The ice palette is mainly deep blues and purples -- I was trying to get the strip to express a color as close to blacklight / uv as possible to make the uv pigments in the moss really pop.
# Define color Palettes waterPalette = [ 0x00d9ff, 0x006f82, 0x43bfb9, 0x0066ff] icePalette = [ 0x8080FF, 0x8080FF, 0x8080FF, 0x0000FF, 0xC88AFF] sunPalette = [ 0xffaa00, 0xffdd00, 0x7d5b06, 0xfffca8] firePalette = [ 0xff0000, 0xff5500, 0x8a3104, 0xffaa00 ] forestPalette = [ 0xccffa8, 0x69f505, 0x05f551, 0x2c8247] #set up default initial palettes, just for startup palette = forestPalette palette2 = waterPalette palette3 = icePalette
The next section sets up the LED strip on pin A4, which corresponds to #2 on the CLUE board. We set brightness to 1.0 which is fully bright, since we will be controlling the brightness through FancyLED.
If you want to make the whole thing dimmer overall, you can lower the brightness here. Otherwise, just choose darker colors for your palettes (closer to the black side of the color layout) to make each individual color darker.
# Declare a NeoPixel object on pin A4 with num_leds pixels, no auto-write. # Set brightness to max because we'll be using FancyLED's brightness control. pixels = neopixel.NeoPixel(board.A4, num_leds, brightness=1.0, auto_write=False)
Main Code Loop
That's it for setup. The main code loop starts next, with while true:
Buttons
The first thing I've done is set up toggles for the two buttons. Button A will turn the NeoPixels and the CLUE screen on and off each time it's pressed. Button B turns only the screen on and off, leaving the pixels on.
I've done this by setting the brightness of the pixels and / or the screen to 0, so they don't "forget" what they were showing when you turn them back up again.
Assigning Color Palettes
The next section correlates the palettes we set up with the pressure or temperature thresholds we made way back at the beginning of the code. Choose which palette you'd like to associate with which pressure reading in this section.
#assign color palette to NeoPixel section 1 based on the current reading reading if reading1 < min_reading: palette = firePalette elif reading1 > min_reading and reading1 < med_reading: palette = sunPalette elif reading1 > med_reading and reading1 < high_reading: palette = forestPalette elif reading1 > high_reading and reading1 < max_reading: palette = waterPalette else: palette = icePalette
Mapping Colors to Pixels
Here is where we assign specific colors to show in the area of the artwork we want. I've set up 9 different "zones" around my frame, so that I can create a reflected gradient layout to visualize the colors. This is maybe a leeeetle bit unnecessarily complex, but, that's how you can tell it's Art.
Go find the numbers you wrote down while you were reading the How it Works page, and plug them into the range()
numbers in the code.
Use the ending number from one zone as the starting number for the next zone, i.e.:
for i in range (0, 16): ...
for i in range (16, 23): ...
for i in range (23, 31): ...
# Map colors to pixels. Adjust range numbers to light up specific pixels. This configuration # maps to a reflected gradient, with pixel 0 in the upper left corner # Load each pixel's color from the palette using an offset, run it # through the gamma function, pack RGB value and assign to pixel. for i in range(23, 31): #center right -- present moment color = fancy.palette_lookup(palette, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack() for i in range(63, 71): #center left -- present moment color = fancy.palette_lookup(palette, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack() for i in range(16, 23): #top mid right -- 1 cycle ago color = fancy.palette_lookup(palette2, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack() for i in range(71, 78): #top mid left -- 1 cycle ago color = fancy.palette_lookup(palette2, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack() for i in range(31, 38): #bottom mid right -- 1 cycle ago color = fancy.palette_lookup(palette2, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack() for i in range(56, 63): #bottom mid left -- 1 cycle ago color = fancy.palette_lookup(palette2, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack() for i in range(0, 16): #top right -- 2 cycles ago color = fancy.palette_lookup(palette3, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack() for i in range(77, 79): #top left -- 2 cycles ago color = fancy.palette_lookup(palette3, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack() for i in range(38, 56): #bottom -- 2 cycles ago color = fancy.palette_lookup(palette3, offset + i / num_leds) color = fancy.gamma_adjust(color, brightness=0.25) pixels[i] = color.pack()
Reading the Sensor & Showing the Data
Finally, we write the sensor readings to the screen, then check to see if it's time to read the sensor again. If it is, we do so, and update all the reading variables: reading2
(data from the last check) becomes reading3
(data from two checks ago), reading (current data) becomes reading2
, and a new sensor reading is taken to replace the current data.
reading_label.text = "Now {:.1f}".format(reading1) reading2_label.text = "Last {:.1f}".format(reading2) reading3_label.text = "Prev {:.1f}".format(reading3) timer_label.text = "{}".format(counter) clue.display.root_group = clue_display # Is it time to update? if counter > timeToCheck: #This moves the current data to the "1 hour old" section of pixels and the "1 hour old" data #to the "2 hours old" section of pixels palette3 = palette2 palette2 = palette reading3 = reading2 reading2 = reading1 reading1 = reading # take a new sensor reading and reset the counter if deviceType == 0: reading = clue.pressure else: reading = clue.temperature counter = 0 # if reading is rising, show rising image and position text at the bottom if reading1 > reading2: sinking_sprite.x = 300 reading_label.y = 134 reading2_label.y = 164 reading3_label.y = 194 timer_label.y = 224 # if reading is falling, show sinking image and position text at the top elif reading2 < reading3: #reading is falling sinking_sprite.x = 0 reading_label.y = 24 reading2_label.y = 54 reading3_label.y = 84 timer_label.y = 114 # otherwise keep counting up else: counter = counter + 1
Page last edited March 08, 2024
Text editor powered by tinymce.