|
| 1 | +# ICON monitoring |
| 2 | +# NAME C02 |
| 3 | +# DESC Display data from your SCD41 CO2 sensor! |
| 4 | + |
| 5 | +""" |
| 6 | +Add a SCD41 sensor breakout to Presto to make a handy CO2 detector! |
| 7 | +https://shop.pimoroni.com/products/scd41-co2-sensor-breakout |
| 8 | +Touch and hold the screen to reset the high/low values. |
| 9 | +
|
| 10 | +You will need the osansb.af font saved to your Presto alongside this script. |
| 11 | +""" |
| 12 | + |
| 13 | +import gc |
| 14 | +import time |
| 15 | + |
| 16 | +from presto import Presto |
| 17 | +from pimoroni_i2c import PimoroniI2C |
| 18 | +import breakout_scd41 |
| 19 | +from picovector import Transform, Polygon, PicoVector, ANTIALIAS_X16 |
| 20 | + |
| 21 | +i2c = PimoroniI2C(sda=40, scl=41) |
| 22 | + |
| 23 | +presto = Presto(full_res=True, ambient_light=False) |
| 24 | +display = presto.display |
| 25 | +vector = PicoVector(display) |
| 26 | + |
| 27 | +WIDTH, HEIGHT = display.get_bounds() |
| 28 | + |
| 29 | +t = Transform() |
| 30 | +vector.set_font_letter_spacing(100) |
| 31 | +vector.set_font_word_spacing(100) |
| 32 | +vector.set_font_line_height(110) |
| 33 | +vector.set_transform(t) |
| 34 | + |
| 35 | +BLACK = display.create_pen(0, 0, 0) |
| 36 | +WHITE = display.create_pen(216, 216, 200) |
| 37 | +RED = display.create_pen(127, 0, 0) |
| 38 | +GREEN = display.create_pen(0, 127, 0) |
| 39 | + |
| 40 | +# pick what bits of the colour wheel to use (from 0-360°) |
| 41 | +# https://www.cssscript.com/demo/hsv-hsl-color-wheel-picker-reinvented/ |
| 42 | +CO2_HUE_START = 100 # green |
| 43 | +CO2_HUE_END = 0 # red |
| 44 | +TEMPERATURE_HUE_START = 240 |
| 45 | +TEMPERATURE_HUE_END = 359 |
| 46 | +HUMIDITY_SATURATION_START = 0 |
| 47 | +HUMIDITY_SATURATION_END = 360 |
| 48 | + |
| 49 | +# the range of readings to map to colours (and scale our graphs to) |
| 50 | +# https://www.kane.co.uk/knowledge-centre/what-are-safe-levels-of-co-and-co2-in-rooms |
| 51 | +CO2_MIN = 300 |
| 52 | +CO2_MAX = 2000 |
| 53 | +TEMPERATURE_MIN = 10 |
| 54 | +TEMPERATURE_MAX = 33 |
| 55 | +HUMIDITY_MIN = 10 |
| 56 | +HUMIDITY_MAX = 90 |
| 57 | +# use these to draw indicator lines on the CO2 graph |
| 58 | +OK_CO2_LEVEL = 800 |
| 59 | +BAD_CO2_LEVEL = 1500 |
| 60 | + |
| 61 | +MAX_READINGS = 120 # how many readings to store (and draw on the graph) |
| 62 | + |
| 63 | +# the areas of the screen we want to draw our graphs into |
| 64 | +CO2_GRAPH_TOP = HEIGHT * 0.2 |
| 65 | +CO2_GRAPH_BOTTOM = HEIGHT * 0.75 |
| 66 | +TEMPERATURE_GRAPH_TOP = HEIGHT * 0.75 |
| 67 | +TEMPERATURE_GRAPH_BOTTOM = HEIGHT |
| 68 | +HUMIDITY_GRAPH_TOP = HEIGHT * 0.75 |
| 69 | +HUMIDITY_GRAPH_BOTTOM = HEIGHT |
| 70 | +PADDING = 5 |
| 71 | + |
| 72 | +LINE_THICKNESS = 3 # thickness of indicator and graph lines |
| 73 | + |
| 74 | + |
| 75 | +def graph_polygon(top, bottom, graph_min, graph_max, readings, y_offset=0): |
| 76 | + # function to generate a filled graph polygon and scale it to the available space |
| 77 | + points = [] |
| 78 | + reading_width = (WIDTH - 1) / max((len(readings) - 1), 1) |
| 79 | + for r in range(len(readings)): |
| 80 | + x = round(r * reading_width) |
| 81 | + y = round(bottom + ((readings[r] - graph_min) * (top - bottom) / (graph_max - graph_min))) |
| 82 | + points.append((x, y)) |
| 83 | + points.append((WIDTH - 1, round(bottom))) |
| 84 | + points.append((0, round(bottom))) |
| 85 | + if y_offset != 0: |
| 86 | + for r in range(len(readings)): |
| 87 | + x = round(r * reading_width) |
| 88 | + y = round(bottom + ((readings[r] - graph_min) * (top - bottom) / (graph_max - graph_min)) + y_offset) |
| 89 | + points.append((x, y)) |
| 90 | + points.append((WIDTH - 1, round(bottom))) |
| 91 | + points.append((0, round(bottom))) |
| 92 | + poly = Polygon() |
| 93 | + poly.path(*points) |
| 94 | + return poly |
| 95 | + |
| 96 | + |
| 97 | +def line_polygon(top, bottom, min, max, value): |
| 98 | + # function to generate a straight line on our graph at a specific value |
| 99 | + points = [] |
| 100 | + points.append((0, round(bottom + ((value - min) * (top - bottom) / (max - min))))) |
| 101 | + points.append((WIDTH, round(bottom + ((value - min) * (top - bottom) / (max - min))))) |
| 102 | + points.append((WIDTH, round(bottom + ((value - min) * (top - bottom) / (max - min))) + LINE_THICKNESS)) |
| 103 | + points.append((0, round(bottom + ((value - min) * (top - bottom) / (max - min))) + LINE_THICKNESS)) |
| 104 | + poly = Polygon() |
| 105 | + poly.path(*points) |
| 106 | + return poly |
| 107 | + |
| 108 | + |
| 109 | +highest_co2 = 0.0 |
| 110 | +lowest_co2 = 4000.0 |
| 111 | +co2_readings = [] |
| 112 | +temperature_readings = [] |
| 113 | +humidity_readings = [] |
| 114 | + |
| 115 | +# set up |
| 116 | +vector.set_antialiasing(ANTIALIAS_X16) |
| 117 | +vector.set_font("osansb.af", 50) |
| 118 | +display.set_pen(BLACK) |
| 119 | +display.clear() |
| 120 | +presto.update() |
| 121 | + |
| 122 | +# display a message whilst waiting for the sensor to be ready |
| 123 | +display.set_pen(WHITE) |
| 124 | +vector.text("Waiting for sensor to be ready", 0, 40, max_width=WIDTH) |
| 125 | +presto.update() |
| 126 | + |
| 127 | +try: |
| 128 | + breakout_scd41.init(i2c) |
| 129 | + breakout_scd41.start() |
| 130 | +except RuntimeError as e: |
| 131 | + # display a message if no breakout is found |
| 132 | + print(e) |
| 133 | + display.set_pen(BLACK) |
| 134 | + display.clear() |
| 135 | + display.set_pen(WHITE) |
| 136 | + vector.text("SCD41 breakout not detected :(", 0, 40, max_width=WIDTH) |
| 137 | + vector.text("but you could buy one at pimoroni.com ;)", 0, HEIGHT - 120, max_width=WIDTH) |
| 138 | + presto.update() |
| 139 | + |
| 140 | +while True: |
| 141 | + if presto.touch.state: |
| 142 | + # reset recorded high / low values |
| 143 | + highest_co2 = 0.0 |
| 144 | + lowest_co2 = 4000.0 |
| 145 | + |
| 146 | + if breakout_scd41.ready(): |
| 147 | + # read the sensor |
| 148 | + co2, temperature, humidity = breakout_scd41.measure() |
| 149 | + |
| 150 | + # if lists are empty, populate the list with the current readings |
| 151 | + if len(co2_readings) == 0: |
| 152 | + for _i in range(MAX_READINGS): |
| 153 | + co2_readings.append(co2) |
| 154 | + temperature_readings.append(temperature) |
| 155 | + humidity_readings.append(humidity) |
| 156 | + |
| 157 | + # update highest / lowest values |
| 158 | + if co2 < lowest_co2: |
| 159 | + lowest_co2 = co2 |
| 160 | + if co2 > highest_co2: |
| 161 | + highest_co2 = co2 |
| 162 | + |
| 163 | + # calculates some colours from the sensor readings |
| 164 | + co2_hue = max(0, CO2_HUE_START + ((co2 - CO2_MIN) * (CO2_HUE_END - CO2_HUE_START) / (CO2_MAX - CO2_MIN))) |
| 165 | + temperature_hue = max(0, TEMPERATURE_HUE_START + ((temperature - TEMPERATURE_MIN) * (TEMPERATURE_HUE_END - TEMPERATURE_HUE_START) / (TEMPERATURE_MAX - TEMPERATURE_MIN))) |
| 166 | + humidity_hue = max(0, HUMIDITY_SATURATION_START + ((humidity - HUMIDITY_MIN) * (HUMIDITY_SATURATION_END - HUMIDITY_SATURATION_START) / (HUMIDITY_MAX - HUMIDITY_MIN))) |
| 167 | + |
| 168 | + # keep track of readings in a list (so we can draw the graph) |
| 169 | + co2_readings.append(co2) |
| 170 | + temperature_readings.append(temperature) |
| 171 | + humidity_readings.append(humidity) |
| 172 | + |
| 173 | + # we only need to save a screen's worth of readings, so delete the oldest |
| 174 | + if len(co2_readings) > MAX_READINGS: |
| 175 | + co2_readings.pop(0) |
| 176 | + if len(temperature_readings) > MAX_READINGS: |
| 177 | + temperature_readings.pop(0) |
| 178 | + if len(humidity_readings) > MAX_READINGS: |
| 179 | + humidity_readings.pop(0) |
| 180 | + |
| 181 | + # clear to black |
| 182 | + display.set_pen(BLACK) |
| 183 | + display.clear() |
| 184 | + |
| 185 | + # draw the graphs and text |
| 186 | + # draw the CO2 graph |
| 187 | + display.set_pen(display.create_pen_hsv(co2_hue / 360, 1.0, 1.0)) |
| 188 | + co2_graph = graph_polygon(CO2_GRAPH_TOP, CO2_GRAPH_BOTTOM, CO2_MIN, CO2_MAX, co2_readings) |
| 189 | + vector.draw(co2_graph) |
| 190 | + # draw the CO2 indicator lines |
| 191 | + display.set_pen(RED) |
| 192 | + vector.draw(line_polygon(CO2_GRAPH_TOP, CO2_GRAPH_BOTTOM, CO2_MIN, CO2_MAX, BAD_CO2_LEVEL)) |
| 193 | + display.set_pen(GREEN) |
| 194 | + vector.draw(line_polygon(CO2_GRAPH_TOP, CO2_GRAPH_BOTTOM, CO2_MIN, CO2_MAX, OK_CO2_LEVEL)) |
| 195 | + # draw the CO2 text |
| 196 | + vector.set_font_size(40) |
| 197 | + display.set_pen(WHITE) |
| 198 | + vector.text(f"CO2: {co2:.0f}ppm", PADDING, 25) |
| 199 | + display.set_pen(BLACK) |
| 200 | + vector.set_font_size(30) |
| 201 | + vector.text(f"Low {lowest_co2:.0f}ppm", PADDING, round(TEMPERATURE_GRAPH_TOP) - 12) |
| 202 | + high_text = f"High {highest_co2:.0f}ppm" |
| 203 | + text_width = int(vector.measure_text(high_text)[2]) |
| 204 | + vector.text(high_text, WIDTH - PADDING - text_width, round(TEMPERATURE_GRAPH_TOP) - 12) |
| 205 | + |
| 206 | + # draw the humidity graph |
| 207 | + # here we're using the 'hue' value to affect the saturation (so light blue to dark blue) |
| 208 | + display.set_pen(display.create_pen_hsv(240 / 360, humidity_hue / 360, 1.0)) |
| 209 | + # draw this polygon twice, applying an offset to make a line rather than a filled shape |
| 210 | + humidity_graph = graph_polygon(HUMIDITY_GRAPH_TOP, HUMIDITY_GRAPH_BOTTOM, HUMIDITY_MIN, HUMIDITY_MAX, humidity_readings, LINE_THICKNESS) |
| 211 | + vector.draw(humidity_graph) |
| 212 | + #display.set_pen(WHITE) |
| 213 | + humidity_text = f"{humidity:.0f}% Humidity" |
| 214 | + text_width = int(vector.measure_text(humidity_text)[2]) |
| 215 | + vector.text(humidity_text, WIDTH - PADDING - text_width, int(HUMIDITY_GRAPH_BOTTOM) - 13) |
| 216 | + |
| 217 | + # draw the temperature graph |
| 218 | + display.set_pen(display.create_pen_hsv(temperature_hue / 360, 1.0, 1.0)) |
| 219 | + # draw this polygon twice, applying an offset to make a line rather than a filled shape |
| 220 | + temperature_graph = graph_polygon(TEMPERATURE_GRAPH_TOP, TEMPERATURE_GRAPH_BOTTOM, TEMPERATURE_MIN, TEMPERATURE_MAX, temperature_readings, LINE_THICKNESS) |
| 221 | + vector.draw(temperature_graph) |
| 222 | + #display.set_pen(WHITE) |
| 223 | + vector.text(f"Temp: {temperature:.0f}°C", PADDING, int(TEMPERATURE_GRAPH_BOTTOM) - 13) |
| 224 | + |
| 225 | + # light up the rear leds the same colour as the graph |
| 226 | + for x in range(7): |
| 227 | + presto.set_led_hsv(x, co2_hue / 360, 1.0, 1.0) |
| 228 | + presto.update() |
| 229 | + |
| 230 | + gc.collect() |
| 231 | + time.sleep(0.5) |
0 commit comments