Skip to content

Commit 5f753c9

Browse files
authored
Merge pull request #24 from MichaelBell/examples/co2
SCD41 CO2 sensor example
2 parents 5636cf8 + 3a3c3ea commit 5f753c9

2 files changed

Lines changed: 231 additions & 0 deletions

File tree

examples/co2.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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)

examples/osansb.af

7.08 KB
Binary file not shown.

0 commit comments

Comments
 (0)