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