Merge branch 'master' into enviro-non-plus
[EVA-2020-02-2.git] / examples / combined.py
1 #!/usr/bin/env python3
2
3 import time
4 import colorsys
5 import os
6 import sys
7 import ST7735
8 try:
9 # Transitional fix for breaking change in LTR559
10 from ltr559 import LTR559
11 ltr559 = LTR559()
12 except ImportError:
13 import ltr559
14
15 from bme280 import BME280
16 from pms5003 import PMS5003, ReadTimeoutError as pmsReadTimeoutError
17 from enviroplus import gas
18 from subprocess import PIPE, Popen
19 from PIL import Image
20 from PIL import ImageDraw
21 from PIL import ImageFont
22 from fonts.ttf import RobotoMedium as UserFont
23 import logging
24
25 logging.basicConfig(
26 format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
27 level=logging.INFO,
28 datefmt='%Y-%m-%d %H:%M:%S')
29
30 logging.info("""all-in-one.py - Displays readings from all of Enviro plus' sensors
31
32 Press Ctrl+C to exit!
33
34 """)
35
36 # BME280 temperature/pressure/humidity sensor
37 bme280 = BME280()
38
39 # PMS5003 particulate sensor
40 pms5003 = PMS5003()
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 colours = [(v - min(values[variable]) + 1) / (max(values[variable])
136 - min(values[variable]) + 1) for v in values[variable]]
137 # Format the variable name and value
138 message = "{}: {:.1f} {}".format(variable[:4], data, unit)
139 logging.info(message)
140 draw.rectangle((0, 0, WIDTH, HEIGHT), (255, 255, 255))
141 for i in range(len(colours)):
142 # Convert the values to colours from red to blue
143 colour = (1.0 - colours[i]) * 0.6
144 r, g, b = [int(x * 255.0) for x in colorsys.hsv_to_rgb(colour,
145 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)))\
150 + top_pos
151 draw.rectangle((i, line_y, i+1, line_y+1), (0, 0, 0))
152 # Write the text at the top in black
153 draw.text((0, 0), message, font=font, fill=(0, 0, 0))
154 st7735.display(img)
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
188 # Get the temperature of the CPU for compensation
189 def get_cpu_temperature():
190 process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE, universal_newlines=True)
191 output, _error = process.communicate()
192 return float(output[output.index('=') + 1:output.rindex("'")])
193
194
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 light = 1
205
206 for v in variables:
207 values[v] = [1] * WIDTH
208
209 # The main loop
210 try:
211 while True:
212 proximity = ltr559.get_proximity()
213
214 # If the proximity crosses the threshold, toggle the mode
215 if proximity > 1500 and time.time() - last_page > delay:
216 mode += 1
217 mode %= (len(variables)+1)
218 last_page = time.time()
219
220 # One mode for each variable
221 if mode == 0:
222 # variable = "temperature"
223 unit = "C"
224 cpu_temp = get_cpu_temperature()
225 # Smooth out with some averaging to decrease jitter
226 cpu_temps = cpu_temps[1:] + [cpu_temp]
227 avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
228 raw_temp = bme280.get_temperature()
229 data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
230 display_text(variables[mode], data, unit)
231
232 if mode == 1:
233 # variable = "pressure"
234 unit = "hPa"
235 data = bme280.get_pressure()
236 display_text(variables[mode], data, unit)
237
238 if mode == 2:
239 # variable = "humidity"
240 unit = "%"
241 data = bme280.get_humidity()
242 display_text(variables[mode], data, unit)
243
244 if mode == 3:
245 # variable = "light"
246 unit = "Lux"
247 if proximity < 10:
248 data = ltr559.get_lux()
249 else:
250 data = 1
251 display_text(variables[mode], data, unit)
252
253 if mode == 4:
254 # variable = "oxidised"
255 unit = "kO"
256 data = gas.read_all()
257 data = data.oxidising / 1000
258 display_text(variables[mode], data, unit)
259
260 if mode == 5:
261 # variable = "reduced"
262 unit = "kO"
263 data = gas.read_all()
264 data = data.reducing / 1000
265 display_text(variables[mode], data, unit)
266
267 if mode == 6:
268 # variable = "nh3"
269 unit = "kO"
270 data = gas.read_all()
271 data = data.nh3 / 1000
272 display_text(variables[mode], data, unit)
273
274 if mode == 7:
275 # variable = "pm1"
276 unit = "ug/m3"
277 try:
278 data = pms5003.read()
279 except pmsReadTimeoutError:
280 logging.warn("Failed to read PMS5003")
281 else:
282 data = float(data.pm_ug_per_m3(1.0))
283 display_text(variables[mode], data, unit)
284
285 if mode == 8:
286 # variable = "pm25"
287 unit = "ug/m3"
288 try:
289 data = pms5003.read()
290 except pmsReadTimeoutError:
291 logging.warn("Failed to read PMS5003")
292 else:
293 data = float(data.pm_ug_per_m3(2.5))
294 display_text(variables[mode], data, unit)
295
296 if mode == 9:
297 # variable = "pm10"
298 unit = "ug/m3"
299 try:
300 data = pms5003.read()
301 except pmsReadTimeoutError:
302 logging.warn("Failed to read PMS5003")
303 else:
304 data = float(data.pm_ug_per_m3(10))
305 display_text(variables[mode], data, unit)
306 if mode == 10:
307 # Everything on one screen
308 cpu_temp = get_cpu_temperature()
309 # Smooth out with some averaging to decrease jitter
310 cpu_temps = cpu_temps[1:] + [cpu_temp]
311 avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps))
312 raw_temp = bme280.get_temperature()
313 raw_data = raw_temp - ((avg_cpu_temp - raw_temp) / factor)
314 save_data(0, raw_data)
315 display_everything()
316 raw_data = bme280.get_pressure()
317 save_data(1, raw_data)
318 display_everything()
319 raw_data = bme280.get_humidity()
320 save_data(2, raw_data)
321 if proximity < 10:
322 raw_data = ltr559.get_lux()
323 else:
324 raw_data = 1
325 save_data(3, raw_data)
326 display_everything()
327 gas_data = gas.read_all()
328 save_data(4, gas_data.oxidising / 1000)
329 save_data(5, gas_data.reducing / 1000)
330 save_data(6, gas_data.nh3 / 1000)
331 display_everything()
332 pms_data = None
333 try:
334 pms_data = pms5003.read()
335 except pmsReadTimeoutError:
336 logging.warn("Failed to read PMS5003")
337 else:
338 save_data(7, float(pms_data.pm_ug_per_m3(1.0)))
339 save_data(8, float(pms_data.pm_ug_per_m3(2.5)))
340 save_data(9, float(pms_data.pm_ug_per_m3(10)))
341 display_everything()
342
343
344
345 # Exit cleanly
346 except KeyboardInterrupt:
347 sys.exit(0)