Commit | Line | Data |
---|---|---|
20442c9a | 1 | #!/usr/bin/env python3 |
71dc2962 K |
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 | |
20442c9a | 22 | from fonts.ttf import RobotoMedium as UserFont |
71dc2962 K |
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() | |
7c3404f8 | 41 | time.sleep(1.0) |
71dc2962 K |
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) | |
20442c9a | 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) | |
71dc2962 K |
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) | |
20442c9a | 172 | for i in range(len(variables)): |
71dc2962 K |
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] | |
20442c9a | 181 | for j in range(len(lim)): |
71dc2962 K |
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 | |
20442c9a | 198 | factor = 2.25 |
71dc2962 K |
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) |