Simple way to display a variable on Unicorn Hat HD

I’ll need to learn what placeholders are within strings before I can proceed.

That’s not a programming term. The code takes a list and displays each item inside that list on the HAT. I said you should use list[0] = (whatever your air quality value is) to set that. list[0] tells the Pi to replace the first item in the list with your air quality value.

Unfortunately, when we declared the list we said list = []. That’s an empty list with no items in it. This is why you got an error saying “list assignment index out of range”: it couldn’t replace item 0 in list with a new value because when we created list we didn’t put any items into it, so item 0 doesn’t exist.

So, when we create list we need to make sure it has a single item in it already. It doesn’t matter what you put in there because it will immediately be overwritten with the first air quality value, that’s why I was calling it a placeholder. You should be able to put any old string into the list when you first declare it.

So you could use lines = ["placeholder string"] as I suggested previously, or really lines = ["anything at all, it doesn't matter"]

1 Like

The latest code produces the error below, any ideas, I feel so close! :)

Traceback (most recent call last):
File “air2.py”, line 144, in
lines[0] = ["{2:.2f}".format(air_quality_score)]
IndexError: tuple index out of range


import bme680
import random
import blinkt
import time
import colorsys
from sys import exit
import unicornhathd

try:
    from PIL import Image, ImageDraw, ImageFont
except ImportError:
    exit("This script requires the pillow module\nInstall with: sudo pip install pillow")

blinkt.set_clear_on_exit()

print("""Estimate indoor air quality

Runs the sensor for a burn-in period, then uses a 
combination of relative humidity and gas resistance
to estimate indoor air quality as a percentage.

Press Ctrl+C to exit

""")

sensor = bme680.BME680()

lines = [("placeholder")]

FONT = ("/usr/share/fonts/truetype/freefont/FreeSansBold.ttf", 12)

def show_text(lines):
    colours = [tuple([int(n * 255) for n in colorsys.hsv_to_rgb(x/float(len(lines)), 1.0, 1.0)]) for x in range(len(lines))]
    FONT = ("/usr/share/fonts/truetype/freefont/FreeSansBold.ttf", 12)
    width, height = unicornhathd.get_shape()
    text_x = width
    text_y = 2
    font_file, font_size = FONT
    font = ImageFont.truetype(font_file, font_size)
    text_width, text_height = width, 0
    for line in lines:
        w, h = font.getsize(line)
        text_width += w + width
        text_height = max(text_height,h)
    text_width += width + text_x + 1
    image = Image.new("RGB", (text_width,max(16, text_height)), (0,0,0))
    draw = ImageDraw.Draw(image)
    offset_left = 0
    for index, line in enumerate(lines):
        draw.text((text_x + offset_left, text_y), line, colours[index], font=font)
        offset_left += font.getsize(line)[0] + width
    for scroll in range(text_width - width):
        for x in range(width):
                for y in range(height):
                        pixel = image.getpixel((x+scroll, y))
                        r, g, b = [int(n) for n in pixel]
                        unicornhathd.set_pixel(width-1-x, y, r, g, b)
        unicornhathd.show()
        time.sleep(0.01)

# These oversampling settings can be tweaked to 
# change the balance between accuracy and noise in
# the data.

sensor.set_humidity_oversample(bme680.OS_2X)
sensor.set_pressure_oversample(bme680.OS_4X)
sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)
sensor.set_gas_status(bme680.ENABLE_GAS_MEAS)

sensor.set_gas_heater_temperature(320)
sensor.set_gas_heater_duration(150)
sensor.select_gas_heater_profile(0)

# start_time and curr_time ensure that the 
# burn_in_time (in seconds) is kept track of.

start_time = time.time()
curr_time = time.time()
burn_in_time = 3 #should be 300 which is 300/60=5 minutes

burn_in_data = []

try:
    # Collect gas resistance burn-in values, then use the average
    # of the last 50 values to set the upper limit for calculating
    # gas_baseline.
    print("Collecting gas resistance burn-in data for 5 mins\n")
    

    while curr_time - start_time < burn_in_time:
        curr_time = time.time()
        if sensor.get_sensor_data() and sensor.data.heat_stable:
            gas = sensor.data.gas_resistance
            burn_in_data.append(gas)
            print("Gas: {0} Ohms".format(gas))
            blinkt.clear()
            blinkt.set_pixel(3, 255, 0, 0.1)
            blinkt.set_pixel(4, 255, 0, 0.1)
            blinkt.show()
            time.sleep(1)

    gas_baseline = sum(burn_in_data[-50:]) / 50.0
    

    # Set the humidity baseline to 40%, an optimal indoor humidity.
    hum_baseline = 40.0

    # This sets the balance between humidity and gas reading in the 
    # calculation of air_quality_score (25:75, humidity:gas)
    hum_weighting = 0.25

    print("Gas baseline: {0} Ohms, humidity baseline: {1:.2f} %RH\n".format(gas_baseline, hum_baseline))

    while True:
        if sensor.get_sensor_data() and sensor.data.heat_stable:
            gas = sensor.data.gas_resistance
            gas_offset = gas_baseline - gas

            hum = sensor.data.humidity
            hum_offset = hum - hum_baseline

            # Calculate hum_score as the distance from the hum_baseline.
            if hum_offset > 0:
                hum_score = (100 - hum_baseline - hum_offset) / (100 - hum_baseline) * (hum_weighting * 100)

            else:
                hum_score = (hum_baseline + hum_offset) / hum_baseline * (hum_weighting * 100)

            # Calculate gas_score as the distance from the gas_baseline.
            if gas_offset > 0:
                gas_score = (gas / gas_baseline) * (100 - (hum_weighting * 100))

            else:
                gas_score = 100 - (hum_weighting * 100)

            # Calculate air_quality_score. 
            air_quality_score = hum_score + gas_score

            print("Gas: {0:.2f} Ohms,humidity: {1:.2f} %RH,air quality: {2:.2f}".format(gas, hum, air_quality_score))
            
            lines[0] = ["{2:.2f}".format(air_quality_score)] 
            
            show_text(lines) #display the air quality score on the unicorn HAT HD
            
            
            
            if air_quality_score >= 85:
		blinkt.clear()
		blinkt.set_all(0, 255, 0, 0.1)
		blinkt.show()
	    elif air_quality_score > 70 and air_quality_score < 85:
		blinkt.clear()
		blinkt.set_all(255, 255, 0, 0.1)
		blinkt.show()
	    else:
		blinkt.clear()
		blinkt.set_all(255, 0, 0, 0.1)
		blinkt.show()

            time.sleep(0.02)

except KeyboardInterrupt:
    pass

I’m a little shakey with the string formatting module, but I don’t think that 2 is meant to be there before the colon. I think it’s meant to look more like lines[0] = ["{:.2f}".format(air_quality_score)]

1 Like

Corrected but now i get this,

Traceback (most recent call last):
File “air2.py”, line 146, in
show_text(lines) #display the air quality score on the unicorn HAT HD
File “air2.py”, line 44, in show_text
w, h = font.getsize(line)
File “/usr/lib/python2.7/dist-packages/PIL/ImageFont.py”, line 140, in getsize
size, offset = self.font.getsize(text)
TypeError: expected string

Its killing me this!

ps. Thanks for your help

Yeah, sorry this is taking so much back and forth. This would be so much easier if I had a BME680 to run it with (I’m the kind of person who makes loads of mistakes and does a lot of bugfixing…).

I think I’ve found a goof: we’re not just giving lines[0] a string, we’re giving it a string inside another list.

Change:
lines[0] = ["{:.2f}".format(air_quality_score)]
to:
lines[0] = "{:.2f}".format(air_quality_score)

(Note the lack of the second pair of square brackets).

1 Like

Thank you very much, I really appreciate your patience. That works a treat!

Wahey! I’m glad it’s working. Like I said there is other stuff in that function which should probably be tidied up but as long as it’s working that’s the important thing.

At some point I want to sit down and figure out how Phil has got the colours working so that would be controllable as well, I might end up using this myself for something.

Thanks again, here’s what it looks like.

That looks great, but I think you can work in a few more LEDs ;-)

I like shiny things! The Blinkt go Red, Amber, Green depending on the air quality rating too!

You should get a Microdot pHAT and graph it over time!

The wife will kill me if I spend anything else at Pimoroni 😆😆😆

But you’re doing it because you’re concerned about the effects of air quality on her health, right?!

1 Like

Anyway I could get a copy of your code? I’m looking to get a jump start on a similar project. Thanks

1 Like

I know this is an old thread but I found something I consider very interesting.
I just got a Unicorn Mini and was playing around with the example code.
If I use
text = time.strftime("It's %A %B %-d %-I:%M %p")
I get “It’s Tuesday June 16 5.21 PM” displayed in the scrolling message.

And

        T = bme280.get_temperature() 
        T = round(T)

        text = ("and the Temperature is %sc") % (T)

gets me a “and the Temperature is 27c” message. =)

Here is code I’m using to display the Day, date, Time, and Temperature, Humidity, and Pressure from a BME289.

#!/usr/bin/env python3
import sys
import os
import time, datetime
import RPi.GPIO as GPIO

from PIL import Image, ImageDraw, ImageFont
from unicornhatmini import UnicornHATMini

try:
    from smbus2 import SMBus
except ImportError:
    from smbus import SMBus
from bme280 import BME280

bus = SMBus(1)
bme280 = BME280(i2c_dev=bus)

unicornhatmini = UnicornHATMini()

GPIO.setmode(GPIO.BCM)  
GPIO.setwarnings(False)
GPIO.setup(5, GPIO.IN, pull_up_down = GPIO.PUD_UP)  # A
GPIO.setup(6, GPIO.IN, pull_up_down = GPIO.PUD_UP)  # B
GPIO.setup(16, GPIO.IN, pull_up_down = GPIO.PUD_UP) # X

# button_map
#  5: "A",
#  6: "B",
# 16: "X",
# 24: "Y"}

X = 0
M = 0

def Dim(channel):  
    unicornhatmini.set_brightness(0.5)

def Bright(channel):  
    unicornhatmini.set_brightness(1.0)

def Shutdown(channel):  
    global X
    X = 1

GPIO.add_event_detect(5, GPIO.FALLING, callback = Dim, bouncetime = 2000)
GPIO.add_event_detect(6, GPIO.FALLING, callback = Bright, bouncetime = 2000)
GPIO.add_event_detect(16, GPIO.FALLING, callback = Shutdown, bouncetime = 2000)


rotation = 0
if len(sys.argv) > 1:
    try:
        rotation = int(sys.argv[1])
    except ValueError:
        print("Usage: {} <rotation>".format(sys.argv[0]))
        sys.exit(1)

unicornhatmini.set_rotation(rotation)
display_width, display_height = unicornhatmini.get_shape()

unicornhatmini.set_brightness(0.5)

font = ImageFont.truetype("/home/pi/5x7.ttf", 8)

offset_x = 0

while True:

    if offset_x == 0 and M == 0:
        text = time.strftime("It's %A %B %-d %-I:%M %p")
        text_width, text_height = font.getsize(text)
        image = Image.new('P', (text_width + display_width + display_width, display_height), 0)
        draw = ImageDraw.Draw(image)
        draw.text((display_width, -1), text, font=font, fill=255)
        r, g, b = (0, 255, 255)

    elif offset_x == 0 and M == 1:
        T = bme280.get_temperature() 
        T = round(T)
          
        if T <= 0: 
            r, g, b = [0, 0, 255]    # Blue
        elif T > 0 and T < 13:
            r, g, b, = [255, 255, 0] # Yellow
        elif T >= 13 and T < 25:
            r, g, b = [0, 255, 0]    # Green
        elif T >= 25 and T < 30:
            r, g, b = [255, 50, 0]  # Orange
        elif T >= 30:
            r, g, b = [255, 0, 0]    # Red

        text = ("and the Temperature is %sc") % (T)
        text_width, text_height = font.getsize(text)
        image = Image.new('P', (text_width + display_width + display_width, display_height), 0)
        draw = ImageDraw.Draw(image)
        draw.text((display_width, -1), text, font=font, fill=255)

    elif offset_x == 0 and M == 2:
        H = bme280.get_humidity() 
        H = round(H)
        
        if H < 30:
            r, g, b = [255, 0, 0]    # Red
        elif H >= 30 and H <= 60:
            r, g, b = [0, 255, 0]    # Green
        elif H > 60 and H < 80:
            r, g, b = [255, 255, 0]  # Yellow
        elif H >= 80:
            r, g, b = [255, 0, 0]    # Red

        text = ("with %s%% Humidity") % (H)
        text_width, text_height = font.getsize(text)
        image = Image.new('P', (text_width + display_width + display_width, display_height), 0)
        draw = ImageDraw.Draw(image)
        draw.text((display_width, -1), text, font=font, fill=255)

    elif offset_x == 0 and M == 3:
        p = bme280.get_pressure() 
        P = round(p)
            
        if P > 0 and P < 982:             # Very Low
            r, g, b = [255, 0, 0]         # Red
        elif P >= 982 and P < 1004:       # Low
            r, g, b = [255, 255, 0]       # Yellow
        elif P >= 1004 and P < 1026:      # Mid Range
            r, g, b = [0, 255, 0]         # Green
        elif P >= 1026 and P < 1048:      # High
            r, g, b = [0, 0, 255]         # Blue
        elif P >= 1048:                   # Very High
            r, g, b = [255, 50, 0]        # Orange

        text = ("@ %smb Pressure") % (P)
        text_width, text_height = font.getsize(text)
        image = Image.new('P', (text_width + display_width + display_width, display_height), 0)
        draw = ImageDraw.Draw(image)
        draw.text((display_width, -1), text, font=font, fill=255)

    else:
        
        for y in range(display_height):
            for x in range(display_width):
                if image.getpixel((x + offset_x, y)) == 255:
                    unicornhatmini.set_pixel(x, y, r, g, b)
                else:
                    unicornhatmini.set_pixel(x, y, 0, 0, 0)

    unicornhatmini.show()
    time.sleep(0.05)
                    

    offset_x += 1
    if offset_x + display_width > image.size[0]:
        offset_x = 0
        M = M + 1

    if M == 4:
        M = 0
 
    if X == 1:
        unicornhatmini.set_all(0, 0, 0)
        unicornhatmini.show()
        os.system("sudo shutdown now -P")
        time.sleep(30)