Add GitHub actions workflow
[EVA-2020-02-2.git] / examples / combined.py
1 #!/usr/bin/env python3
2
3 import time
4 import colorsys
5 import sys
6 import ST7735
7 try:
8 # Transitional fix for breaking change in LTR559
9 from ltr559 import LTR559
10 ltr559 = LTR559()
11 except ImportError:
12 import ltr559
13
14 from bme280 import BME280
15 from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError, SerialTimeoutError
16 from enviroplus import gas
17 from subprocess import PIPE, Popen
18 from PIL import Image
19 from PIL import ImageDraw
20 from PIL import ImageFont
21 from fonts.ttf import RobotoMedium as UserFont
22 import logging
23
24 logging.basicConfig(
25 format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
26 level=logging.INFO,
27 datefmt='%Y-%m-%d %H:%M:%S')
28
29 logging.info("""combined.py - Displays readings from all of Enviro plus' sensors
30
31 Press Ctrl+C to exit!
32
33 """)
34
35 # BME280 temperature/pressure/humidity sensor
36 bme280 = BME280()
37
38 # PMS5003 particulate sensor
39 pms5003 = PMS5003()
40 time.sleep(1.0)
41
42 # Create ST7735 LCD display class
43 st7735 = ST7735.ST7735(
44 port=0,
45 cs=1,
46 dc=9,
47 backlight=12,
48 rotation=270,
49 spi_speed_hz=10000000
50 )
51
52 # Initialize display
53 st7735.begin()
54
55 WIDTH = st7735.width
56 HEIGHT = st7735.height
57
58 # Set up canvas and font
59 img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0))
60 draw = ImageDraw.Draw(img)
61 font_size_small = 10
62 font_size_large = 20
63 font = ImageFont.truetype(UserFont, font_size_large)
64 smallfont = ImageFont.truetype(UserFont, font_size_small)
65 x_offset = 2
66 y_offset = 2
67
68 message = ""
69
70 # The position of the top bar
71 top_pos = 25
72
73 # Create a values dict to store the data
74 variables = ["temperature",
75 "pressure",
76 "humidity",
77 "light",
78 "oxidised",
79 "reduced",
80 "nh3",
81 "pm1",
82 "pm25",
83 "pm10"]
84
85 units = ["C",
86 "hPa",
87 "%",
88 "Lux",
89 "kO",
90 "kO",
91 "kO",
92 "ug/m3",
93 "ug/m3",
94 "ug/m3"]
95
96 # Define your own warning limits
97 # The limits definition follows the order of the variables array
98 # Example limits explanation for temperature:
99 # [4,18,28,35] means
100 # [-273.15 .. 4] -> Dangerously Low
101 # (4 .. 18] -> Low
102 # (18 .. 28] -> Normal
103 # (28 .. 35] -> High
104 # (35 .. MAX] -> Dangerously High
105 # DISCLAIMER: The limits provided here are just examples and come
106 # with NO WARRANTY. The authors of this example code claim
107 # NO RESPONSIBILITY if reliance on the following values or this
108 # code in general leads to ANY DAMAGES or DEATH.
109 limits = [[4, 18, 28, 35],
110 [250, 650, 1013.25, 1015],
111 [20, 30, 60, 70],
112 [-1, -1, 30000, 100000],
113 [-1, -1, 40, 50],
114 [-1, -1, 450, 550],
115 [-1, -1, 200, 300],
116 [-1, -1, 50, 100],
117 [-1, -1, 50, 100],
118 [-1, -1, 50, 100]]
119
120 # RGB palette for values on the combined screen
121 palette = [(0, 0, 255), # Dangerously Low
122 (0, 255, 255), # Low
123 (0, 255, 0), # Normal
124 (255, 255, 0), # High
125 (255, 0, 0)] # Dangerously High
126
127 values = {}
128
129
130 # Displays data and text on the 0.96" LCD
131 def display_text(variable, data, unit):
132 # Maintain length of list
133 values[variable] = values[variable][1:] + [data]
134 # Scale the values for the variable between 0 and 1
135 vmin = min(values[variable])
136 vmax = max(values[variable])
137 colours = [(v - vmin + 1) / (vmax - vmin + 1) for v in values[variable]]
138 # Format the variable name and value
139 message = "{}: {:.1f} {}".format(variable[:4], data, unit)
140 logging.info(message)
141 draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255))
142 for i in range(len(colours)):
143 # Convert the values to colours from red to blue
144 colour = (1.0 - colours[i]) * 0.6
145 r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour, 1.0, 1.0)]
146 # Draw a 1-pixel wide rectangle of colour
147 draw.rectangle((i, top_pos, i + 1, HEIGHT), (r, g, b))
148 # Draw a line graph in black
149 line_y = HEIGHT - (top_pos + (colours[i] * (HEIGHT - top_pos))) + top_pos
150 draw.rectangle((i, line_y, i + 1, line_y + 1), (0, 0, 0))
151 # Write the text at the top in black
152 draw.text((0, 0), message, font=font, fill=(0, 0, 0))
153 st7735.display(img)
154
155
156 # Saves the data to be used in the graphs later and prints to the log
157 def save_data(idx, data):
158 variable = variables[idx]
159 # Maintain length of list
160 values[variable] = values[variable][1:] + [data]
161 unit = units[idx]
162 message = "{}: {:.1f} {}".format(variable[:4], data, unit)
163 logging.info(message)
164
165
166 # Displays all the text on the 0.96" LCD
167 def display_everything():
168 draw.rectangle((0, 0, WIDTH, HEIGHT), (0, 0, 0))
169 column_count = 2
170 row_count = (len(variables) / column_count)
171 for i in range(len(variables)):
172 variable = variables[i]
173 data_value = values[variable][-1]
174 unit = units[i]
175 x = x_offset + ((WIDTH // column_count) * (i // row_count))
176 y = y_offset + ((HEIGHT / row_count) * (i % row_count))
177 message = "{}: {:.1f} {}".format(variable[:4], data_value, unit)
178 lim = limits[i]
179 rgb = palette[0]
180 for j in range(len(lim)):
181 if data_value > lim[j]:
182 rgb = palette[j + 1]
183 draw.text((x, y), message, font=smallfont, fill=rgb)
184 st7735.display(img)
185
186
187 # Get the temperature of the CPU for compensation
188 def get_cpu_temperature():
189 process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True)
190 output, _error = process.communicate()
191 return float(output[output.index('=') + 1:output.rindex("'")])
192
193
194 def main():
195 # Tuning factor for compensation. Decrease this number to adjust the
196 # temperature down, and increase to adjust up
197 factor = 2.25
198
199 cpu_temps = [get_cpu_temperature()] * 5
200
201 delay = 0.5 # Debounce the proximity tap
202 mode = 10 # The starting mode
203 last_page = 0
204
205 for v in variables:
206 values[v] = [1] * WIDTH
207
208 # The main loop
209 try:
210 while True:
211 proximity = ltr559.get_proximity()
212
213 # If the proximity crosses the threshold, toggle the mode
214 if proximity > 1500 and time.time() - last_page > delay:
215 mode += 1
216 mode %= (len(variables) + 1)
217 last_page = time.time()
218
219 # One mode for each variable
220 if mode == 0:
221 # variable = "temperature"
222 unit = "C"
223 cpu_temp = get_cpu_temperature()
224 # Smooth out with some averaging to decrease jitter
225 cpu_temps = cpu_temps[1:] + [cpu_temp]
226 avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
227 raw_temp = bme280.get_temperature()
228 data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
229 display_text(variables[mode], data, unit)
230
231 if mode == 1:
232 # variable = "pressure"
233 unit = "hPa"
234 data = bme280.get_pressure()
235 display_text(variables[mode], data, unit)
236
237 if mode == 2:
238 # variable = "humidity"
239 unit = "%"
240 data = bme280.get_humidity()
241 display_text(variables[mode], data, unit)
242
243 if mode == 3:
244 # variable = "light"
245 unit = "Lux"
246 if proximity < 10:
247 data = ltr559.get_lux()
248 else:
249 data = 1
250 display_text(variables[mode], data, unit)
251
252 if mode == 4:
253 # variable = "oxidised"
254 unit = "kO"
255 data = gas.read_all()
256 data = data.oxidising / 1000
257 display_text(variables[mode], data, unit)
258
259 if mode == 5:
260 # variable = "reduced"
261 unit = "kO"
262 data = gas.read_all()
263 data = data.reducing / 1000
264 display_text(variables[mode], data, unit)
265
266 if mode == 6:
267 # variable = "nh3"
268 unit = "kO"
269 data = gas.read_all()
270 data = data.nh3 / 1000
271 display_text(variables[mode], data, unit)
272
273 if mode == 7:
274 # variable = "pm1"
275 unit = "ug/m3"
276 try:
277 data = pms5003.read()
278 except pmsReadTimeoutError:
279 logging.warning("Failed to read PMS5003")
280 else:
281 data = float(data.pm_ug_per_m3(1.0))
282 display_text(variables[mode], data, unit)
283
284 if mode == 8:
285 # variable = "pm25"
286 unit = "ug/m3"
287 try:
288 data = pms5003.read()
289 except pmsReadTimeoutError:
290 logging.warning("Failed to read PMS5003")
291 else:
292 data = float(data.pm_ug_per_m3(2.5))
293 display_text(variables[mode], data, unit)
294
295 if mode == 9:
296 # variable = "pm10"
297 unit = "ug/m3"
298 try:
299 data = pms5003.read()
300 except pmsReadTimeoutError:
301 logging.warning("Failed to read PMS5003")
302 else:
303 data = float(data.pm_ug_per_m3(10))
304 display_text(variables[mode], data, unit)
305 if mode == 10:
306 # Everything on one screen
307 cpu_temp = get_cpu_temperature()
308 # Smooth out with some averaging to decrease jitter
309 cpu_temps = cpu_temps[1:] + [cpu_temp]
310 avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
311 raw_temp = bme280.get_temperature()
312 raw_data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
313 save_data(0, raw_data)
314 display_everything()
315 raw_data = bme280.get_pressure()
316 save_data(1, raw_data)
317 display_everything()
318 raw_data = bme280.get_humidity()
319 save_data(2, raw_data)
320 if proximity < 10:
321 raw_data = ltr559.get_lux()
322 else:
323 raw_data = 1
324 save_data(3, raw_data)
325 display_everything()
326 gas_data = gas.read_all()
327 save_data(4, gas_data.oxidising / 1000)
328 save_data(5, gas_data.reducing / 1000)
329 save_data(6, gas_data.nh3 / 1000)
330 display_everything()
331 pms_data = None
332 try:
333 pms_data = pms5003.read()
334 except (SerialTimeoutError, pmsReadTimeoutError):
335 logging.warning("Failed to read PMS5003")
336 else:
337 save_data(7, float(pms_data.pm_ug_per_m3(1.0)))
338 save_data(8, float(pms_data.pm_ug_per_m3(2.5)))
339 save_data(9, float(pms_data.pm_ug_per_m3(10)))
340 display_everything()
341
342 # Exit cleanly
343 except KeyboardInterrupt:
344 sys.exit(0)
345
346
347 if __name__ == "__main__":
348 main()