├── .gitignore ├── PiM25.py ├── PiM25_Box.py ├── README.md ├── Roboto-Regular.ttf ├── logo.png ├── oledyaml.yaml └── pim25b.bmp /.gitignore: -------------------------------------------------------------------------------- 1 | README_v001.md 2 | PiM25_v001.py 3 | PiM25_box_v001.py 4 | -------------------------------------------------------------------------------- /PiM25.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # SUGGESTED CONNECTIONS, but you can of course do it differenlty! 5 | ############################################################## 6 | # Raspberry Pi 3 GPIO Pinout; Corner --> # 7 | # (pin 1) | (pin 2) # 8 | # OLED/GPS Vcc +3.3V | +5.0V GPS NEO6 Vcc # 9 | # OLED SDA GPIO 2 | +5.0V PM25 G5 pin 1 Vcc b# 10 | # OLED SCL GPIO 3 | GND PM25 G5 pin 2 GND o# 11 | # GPIO 4 | UART TX # 12 | # OLED/Gas GND GND | UART RX # 13 | # GPIO 17 | GPIO 18 GPS NEO6 pin 5 TX # 14 | # GPIO 27 | GND GPS NEO6 GND # 15 | # GPIO 22 | GPIO 23 # 16 | #r MCP3008 Vcc/Vref +3.3V | GPIO 24 PM25 G5 pin 5 TX g# 17 | # GPIO 10 | GND DHT22 GND g# 18 | # GPIO 9 | GPIO 25 DHT22 DATA b# # 19 | # GPIO 11 | GPIO 8 DHT22 POWER p# 20 | # GND | GPIO 7 # 21 | # Reserved | Reserved # 22 | # GPIO 5 | GND # 23 | #b MCP3008 CLK GPIO 6 | GPIO 12 # 24 | #g MCP3008 MISO GPIO 13 | GND (GPS GND) # 25 | #y MCP3008 MOSI GPIO 19 | GPIO 16 (GPS TX) # 26 | #o MCP3008 CSbar GPIO 26 | GPIO 20 # 27 | #brMCP3008 GND/GND GND | GPIO 21 # 28 | # (pin 39) | (pin 40) # 29 | ############################################################## 30 | 31 | 32 | import pigpio, smbus 33 | import atexit 34 | import re, commands 35 | import psutil # http://psutil.readthedocs.io/en/latest/ 36 | 37 | import yaml 38 | 39 | import time, datetime 40 | import numpy as np 41 | import paho.mqtt.client as mqtt 42 | import json 43 | 44 | from binascii import hexlify 45 | from PIL import Image, ImageDraw, ImageFont 46 | import matplotlib.pyplot as plt 47 | 48 | import logging # heyhere 49 | 50 | class BOX(object): 51 | 52 | devkind = "BOX" 53 | 54 | def __init__(self, name, use_WiFi=False, use_SMBus=False, 55 | use_pigpio=False): 56 | 57 | self.name = name 58 | 59 | # Create and configure logger 60 | LOG_FORMAT = "%(levelname)s %(asctime)s - %(message)s" 61 | logging.basicConfig(filename = "PiM25box_logging.log", 62 | level = logging.DEBUG, 63 | format = LOG_FORMAT, 64 | filemode = 'w') 65 | 66 | self.logger = logging.getLogger() 67 | 68 | self.logger.info("box '{}' __init__".format(self.name)) 69 | 70 | self.instance_things = locals() 71 | donts = ('self', 'name') 72 | for dont in donts: 73 | try: 74 | self.instance_things.pop(dont) 75 | except: 76 | pass 77 | 78 | self.name = name 79 | self.use_WiFi = use_WiFi 80 | self.use_SMBus = use_SMBus 81 | self.use_pigpio = use_pigpio 82 | 83 | self.devices = [] 84 | self.LOG_devices = [] 85 | self.LASS_devices = [] 86 | 87 | self.mac_address = None 88 | self.get_mac_address() 89 | 90 | if self.use_WiFi: 91 | self._get_nWiFi() 92 | self.WiFi_setstatus('on') 93 | self.logger.info("WiFi_setstatus('on')") 94 | else: 95 | pass 96 | 97 | print ' testing self.use_pigpio: ', self.use_pigpio 98 | if self.use_pigpio: 99 | print ' it was True' 100 | self.make_a_pi() 101 | self.logger.info("make_a_pi()") 102 | else: 103 | print ' it was False' 104 | self.pi = None 105 | self.pigpiod_process = None 106 | 107 | if self.use_SMBus: 108 | self.bus = smbus.SMBus(1) 109 | self.logger.info("smbus.SMBus(1)") 110 | else: 111 | self.bus = None 112 | 113 | def __repr__(self): 114 | return ('{self.__class__.__name__}({self.name})' 115 | .format(self=self)) 116 | 117 | def clear_all_device_datadicts(self): 118 | for device in self.devices: 119 | device.datadict = dict() # easiest way to clear! 120 | 121 | def read_all_devices(self): 122 | for device in self.devices: 123 | device.read() # each device repopulates its datadict 124 | 125 | def get_system_timedate_dict(self): 126 | 127 | sysnow = datetime.datetime.now() 128 | sysnow_str = sysnow.strftime("%Y-%m-%d %H:%M:%S") 129 | sysdate_str = sysnow_str[:10] 130 | systime_str = sysnow_str[11:] 131 | sysmicroseconds_str = str(sysnow.microsecond) 132 | 133 | systimedatedict = dict() 134 | 135 | systimedatedict['sysnow_str'] = sysnow_str 136 | systimedatedict['timestr'] = systime_str 137 | systimedatedict['datestr'] = sysdate_str 138 | systimedatedict['tickstr'] = sysmicroseconds_str 139 | 140 | return systimedatedict 141 | 142 | def make_a_pi(self): 143 | 144 | status, process = commands.getstatusoutput('sudo pidof pigpiod') # check it 145 | 146 | if status: # it wasn't running, so start it 147 | print "pigpiod was not running" 148 | self.logger.info("pigpiod was not running") 149 | commands.getstatusoutput('sudo pigpiod') # start it 150 | time.sleep(0.5) 151 | status, process = commands.getstatusoutput('sudo pidof pigpiod') # check it again 152 | 153 | if not status: # if it worked, i.e. if it's running... 154 | self.pigpiod_process = process 155 | print "pigpiod is running, process ID is: ", self.pigpiod_process 156 | self.logger.info("pigpiod is running, process ID is: {}".format(self.pigpiod_process)) 157 | 158 | try: 159 | self.pi = pigpio.pi() # local GPIO only 160 | print "pi is instantiated successfully" 161 | self.logger.info("pi is instantiated successfully") 162 | except Exception, e: 163 | str_e = str(e) 164 | print "problem instantiating pi, the exception message is: ", str_e 165 | self.logger.warning("problem instantiating pi: {}".format(str_e)) 166 | self.start_pigpiod_exception = str_e 167 | 168 | # METHODS that involve WiFi 169 | 170 | def WiFi_setstatus(self, on_or_off): 171 | if type(on_or_off) is str: 172 | if on_or_off.lower() == 'on': 173 | self.WiFi_on() 174 | elif on_or_off.lower() == 'off': 175 | self.WiFi_off() 176 | else: 177 | print "WiFi_onoff unrecognized string" 178 | self.logger.warning("WiFi_onoff unrecognized string: {}".format(on_or_off)) 179 | else: 180 | if on_or_off: 181 | self.WiFi_on() 182 | else: 183 | self.WiFi_off() 184 | 185 | def get_WiFi_is_on(self): 186 | WiFi_is_on = None 187 | try: 188 | stat, isblocked = commands.getstatusoutput("sudo rfkill list " + 189 | str(self.nWiFi) + 190 | " | grep Soft | awk '{print $3}'") 191 | if isblocked == 'yes': 192 | WiFi_is_on = False 193 | print "WiFi is off" 194 | self.logger.info("WiFi is off") 195 | elif isblocked == 'no': 196 | WiFi_is_on = True 197 | print "WiFi is on" 198 | self.logger.info("WiFi is on") 199 | else: 200 | print "can't tell if WiFi is on or off" 201 | except: 202 | print "problem checking WiFi status" 203 | self.logger.warning("problem checking WiFi status") 204 | 205 | return WiFi_is_on 206 | 207 | def WiFi_on(self): 208 | stat, out = commands.getstatusoutput("sudo rfkill unblock " + str(self.nWiFi)) 209 | if stat: 210 | print "problem turning WiFi on" , stat, out 211 | self.logger.warning("problem turning WiFi on") 212 | 213 | def WiFi_off(self): 214 | 215 | stat, out = commands.getstatusoutput("sudo rfkill block " + str(self.nWiFi)) 216 | if stat: 217 | print "problem turning WiFi off" 218 | self.logger.warning("problem turning WiFi off") 219 | 220 | def _get_nWiFi(self): 221 | 222 | stat, out = commands.getstatusoutput("sudo rfkill list | grep phy0 | awk '{print $1}'") 223 | try: 224 | self.nWiFi = int(out.replace(':', '')) # confirm by checking that it can be an integer 225 | self.logger.info("nWiFi: {}".format(self.nWiFi)) # heyhere 226 | except: 227 | # print "there was an exception! " 228 | self.logger.warning("there was a problem checking nWiFi!") # heyhere 229 | self.nWiFi = None 230 | 231 | # METHODS that involve ntpdate 232 | 233 | def _do_ntpdate(self): 234 | stat, out = commands.getstatusoutput("sudo ntpdate") 235 | # needs work! 236 | return stat, out 237 | 238 | # METHODS that involve MAC address 239 | 240 | def get_mac_address(self): 241 | 242 | # https://stackoverflow.com/questions/159137/getting-mac-address 243 | # Nice: https://forums.hak5.org/topic/20372-python-script-to-get-mac-address/ 244 | # Wow! https://stackoverflow.com/questions/24196932/how-can-i-get-the-ip-address-of-eth0-in-python 245 | # also https://stackoverflow.com/questions/159137/getting-mac-address 246 | 247 | ifconfig = commands.getoutput("ifconfig eth0 " + 248 | " | grep HWaddr | " + 249 | "awk '{print $5}'") 250 | # print ' ifconfig: ', ifconfig 251 | self.logger.info("ifconfig: {}".format(ifconfig)) 252 | 253 | if type(ifconfig) is str: 254 | possible_mac = ifconfig.replace(':','') # alternate 255 | if len(possible_mac) == 12: 256 | self.mac_address = possible_mac 257 | self.logger.info("self.mac_address: {}".format(possible_mac)) 258 | 259 | # METHODS that involve system status 260 | 261 | def _get_some_system_info_lines(self): 262 | info_lines = ['', '--------', '--------'] 263 | things = ('uname -a', 'lsb_release -a', 'df -h', 'free', 264 | 'vcgencmd measure_temp') 265 | for thing in things: 266 | err, msg = commands.getstatusoutput(thing) 267 | if not err: 268 | info_lines += ['COMMAND: ' + thing + 269 | ' returns: ' + msg, '--------'] 270 | else: 271 | info_lines += ['COMMAND: ' + thing + 272 | ' returns: error', '--------'] 273 | info_lines += ['--------'] 274 | 275 | return info_lines 276 | 277 | def print_some_system_info_lines(self): 278 | info_lines = _get_some_system_info_lines() 279 | for line in info_lines: 280 | print line 281 | 282 | 283 | def get_system_datetime(self): 284 | 285 | sysnow = datetime.datetime.now() 286 | sysnow_str = sysnow.strftime("%Y-%m-%d %H:%M:%S") 287 | sysdate_str = sysnow_str[:10] 288 | systime_str = sysnow_str[11:] 289 | sysmicroseconds_str = str(sysnow.microsecond) 290 | 291 | return sysnow_str 292 | 293 | def show_CPU_temp(self): 294 | temp = None 295 | err, msg = commands.getstatusoutput('vcgencmd measure_temp') 296 | #if not err: 297 | #m = re.search(r'-?\d+\.?\d*', msg) 298 | #try: 299 | #temp = float(m.group()) 300 | #except: 301 | #pass 302 | # return temp 303 | return msg 304 | 305 | def add_device(self, device): 306 | if type(device.name) is not str: 307 | print "device not added, type(device.name) is not str: {}".format(device.name) 308 | self.logger.error("device not added, type(device.name) is not str: {}".format(device.name)) # heyhere 309 | elif len(device.name) == 0: 310 | print "device not added, zero-length name not allowed" 311 | self.logger.error("device not added, zero-length name not allowed") # heyhere 312 | elif self.get_device(device.name) is not None: 313 | print "device with name '{}' not added, that name is already present".format(device.name) 314 | self.logger.error("device with name '{}' not added, that name is already present".format(device.name)) # heyhere 315 | else: 316 | self.devices.append(device) 317 | print "device with unique name '{}' successfully added".format(device.name) 318 | self.logger.info("device with unique name '{}' successfully added".format(device.name)) # heyhere 319 | 320 | 321 | def new_G5bb(self, name, DATA=None, collect_time=None): 322 | g5 = G5bb(box=self, name=name, DATA=DATA, collect_time=collect_time) 323 | self.logger.info("new_G5bb: '{}'".format(g5.name)) 324 | return g5 325 | 326 | def new_GPSbb(self, name, DATA=None, collect_time=None): 327 | gps = GPSbb(box=self, name=name, DATA=DATA, collect_time=collect_time) 328 | self.logger.info("new_GPSbb: '{}'".format(gps.name)) 329 | return gps 330 | 331 | def new_DHT22bb(self, name, DATA=None, POWER=None): 332 | dht = DHT22bb(box=self, name=name, DATA=DATA, POWER=POWER) 333 | self.logger.info("new_DHT22bb: '{}'".format(dht.name)) 334 | return dht 335 | 336 | def new_MCP3008bb(self, name, CSbar=None, MISO=None, 337 | MOSI=None, SCLK=None, Vref=None): 338 | 339 | mcp3008 = MCP3008bb(box=self, name=name, CSbar=CSbar, 340 | MISO=MISO, MOSI=MOSI, SCLK=SCLK, Vref=Vref) 341 | self.logger.info("new_MCP3008bb: '{}'".format(mcp3008.name)) 342 | return mcp3008 343 | 344 | def new_MOS_gas_sensor(self, name, ADC=None, channel=None, 345 | Rseries=None, Calibrationdata=None, 346 | use_loglog=None, gasname=None, 347 | atlimitsisokay=None): 348 | 349 | gas_sensor = MOS_gas_sensor(box=self, name=name, 350 | ADC=ADC, channel=channel, Rseries=Rseries, 351 | Calibrationdata=Calibrationdata, 352 | use_loglog=use_loglog, gasname=gasname, 353 | atlimitsisokay=atlimitsisokay) 354 | self.logger.info("new_MOS_gas_sensor: '{}'".format(gas_sensor.name)) 355 | return gas_sensor 356 | 357 | def new_OLEDi2c(self, name): 358 | oled = OLEDi2c(box=self, name=name) 359 | self.logger.info("new_OLEDi2c: '{}'".format(oled.name)) 360 | return oled 361 | 362 | def new_Dummy(self, name, dummydatadict=None): 363 | dummy = Dummy(box=self, name=name, dummydatadict=dummydatadict) 364 | self.logger.info("new_Dummy: '{}'".format(dummy.name)) 365 | return dummy 366 | 367 | def get_device(self, devname): 368 | 369 | try: 370 | device = [d for d in self.devices if d.name == devname][0] 371 | except: 372 | device = None 373 | 374 | return device 375 | 376 | def new_LASS(self, name=None): 377 | """wrapper to make instantiation 'look nicer'""" 378 | lass = LASS(self, name) 379 | self.LASS_devices.append(lass) 380 | self.logger.info("new_LASS: '{}'".format(lass.name)) 381 | return lass 382 | 383 | def new_LOG(self, filename='deleteme.txt', name=None): 384 | """wrapper to make instantiation 'look nicer'""" 385 | log = LOG(self, filename, name) 386 | self.logger.info("new_LOG: '{}' filename: '{}'".format(log.name, log.filename)) 387 | return log 388 | 389 | 390 | class LASS(object): 391 | def __init__(self, box, name=None): 392 | 393 | self.box = box 394 | self.name = name 395 | self.devkind = 'LASS' 396 | 397 | self.box.logger.info("LASS '{}' __init__".format(self.name)) 398 | 399 | self.mac_address = box.mac_address 400 | 401 | self.last_system_info = None # double check this should be here 402 | 403 | self.devices = [] 404 | 405 | # six static box 406 | self.app = 'PiM25' 407 | self.ver_app = '0.1.0' 408 | self.device = 'PiM25Box ' + name 409 | self.device_id = None # replace with MAC yes mac YES mac yes! 410 | self.ver_format = 3 # what does this mean? always 3 411 | self.fmt_opt = 1 # (0) default (real GPS) (1) gps information invalid always 0 or 1 412 | 413 | self.battery_level_static = 100.0 414 | self.battery_mode_static = 1.0 415 | self.motion_speed_static = 0.0 416 | self.CPU_utilization_static = 0.0 # Hey link this up 417 | 418 | self.sequence_number = 1 419 | 420 | #self.static_lat = None 421 | #self.static_lon = None 422 | #self.static_alt = None 423 | 424 | self.gps_lat = None 425 | self.gps_lon = None 426 | self.gps_alt = None 427 | 428 | self.gps_fix = 0 429 | self.gps_num = 0 430 | 431 | # MQTT 432 | self.client = mqtt.Client() 433 | self.host = 'gpssensor.ddns.net' 434 | self.topic = 'LASS/Test/PM25' 435 | self.client.connect(self.host, 1883, 60) 436 | 437 | 438 | def __repr__(self): 439 | return ('{self.__class__.__name__}({self.name})' 440 | .format(self=self)) 441 | 442 | def set_static_location(self, latlon=tuple, alt=None): 443 | 444 | self.static_latlon = latlon 445 | self.static_alt = alt 446 | if type(latlon) == tuple and len(latlon) >= 2: 447 | if all([type(x) in (float, int) for x in latlon[:2]]): 448 | self.static_lat = latlon[0] 449 | self.static_lon = latlon[1] 450 | print "static latitude and longitude set." 451 | self.box.logger.info("set static latitude: {} longitude: {}" 452 | .format(self.static_lat, self.static_lon)) 453 | elif all([type(x) is str for x in latlon[:2]]): 454 | try: 455 | lat, lon = [float(x) for x in latlon[:2]] 456 | self.static_lat = lat 457 | self.static_lon = lon 458 | print "static latitude and longitude set." 459 | self.box.logger.info("set static latitude: {} longitude: {}" 460 | .format(self.static_lat, self.static_lon)) 461 | except: 462 | print "static latitude and longitude set has failed!" 463 | self.box.logger.error("static latitude and longitude set has failed!") 464 | pass 465 | 466 | if type(alt) is float: 467 | self.static_alt = alt 468 | print "static altitude set." 469 | self.box.logger.info("sstatic altitude set: {}".format(self.static_alt)) 470 | 471 | def set_sources(self, humsrc=None, tempsrc=None, pm25src=None, 472 | pm1src=None, pm10src=None, timedatesrc=None, 473 | GPSsrc=None, gassensors=None): 474 | 475 | self.source_dict = dict() 476 | 477 | self._lookup = {'humidity':{'DHT22':'s_h0', 'HTS221':'s_h1', 'SHT31':'s_h2', 478 | 'HTU21D':'s_h3', 'BME280':'s_h4', 'SHT25':'s_h5', 'G5':'s_h6', 479 | 'other':'s_h9'}, 480 | 'temperature':{'DHT22':'s_t0', 'HTS221':'s_t1', 'SHT31':'s_t2', 481 | 'HTU21D':'s_t3', 'BME280':'s_t4', 'SHT25':'s_t5', 'G5':'s_t6', 482 | 'other':'s_t9'}, 483 | 'PM25':{'G5':'s_d0', 'Panasonic':'s_d3', 'other':'s_d7'}, 484 | 'PM1' :{'G5':'s_d1', 'other':'s_d8'}, 485 | 'PM10':{'G5':'s_d2', 'other':'s_d9'}} 486 | 487 | self._gaslookup = {'NH3':'s_g0', 'CO':'s_g1', 'NO2':'s_g2', 'C3H8':'s_g5', 488 | 'C4H10':'s_g4', 'CH4':'s_g5', 'H2':'s_g6', 489 | 'C2H5OH':'s_g7', 'CO2':'s_g8', 'TVOC':'s_gg'} 490 | 491 | if humsrc: 492 | param, source = 'humidity', humsrc 493 | self.source_dict[self._lookup[param][source.devkind]] = (source, param) 494 | self.box.logger.info("LASS {}: humsrc: {} param: {}" 495 | .format(self.name, source, param)) 496 | 497 | if tempsrc: 498 | param, source = 'temperature', tempsrc 499 | self.source_dict[self._lookup[param][source.devkind]] = (source, param) 500 | self.box.logger.info("LASS {}: tempsrc: {} param: {}" 501 | .format(self.name, source, param)) 502 | 503 | if pm25src: 504 | param, source = 'PM25', pm25src 505 | self.source_dict[self._lookup[param][source.devkind]] = (source, param) 506 | self.box.logger.info("LASS {}: pm25src: {} param: {}" 507 | .format(self.name, source, param)) 508 | 509 | if pm1src: 510 | param, source = 'PM1', pm1src 511 | self.source_dict[self._lookup[param][source.devkind]] = (source, param) 512 | self.box.logger.info("LASS {}: pm1src: {} param: {}" 513 | .format(self.name, source, param)) 514 | 515 | if pm10src: 516 | param, source = 'PM10', pm10src 517 | self.source_dict[self._lookup[param][source.devkind]] = (source, param) 518 | self.box.logger.info("LASS {}: pm10src: {} param: {}" 519 | .format(self.name, source, param)) 520 | 521 | if not gassensors: 522 | gassensors = [] 523 | 524 | for sensor in gassensors: 525 | param, source, gasname = 'ppm', sensor, sensor.devkind 526 | self.source_dict[self._gaslookup[gasname]] = (source, param) 527 | self.box.logger.info("LASS {}: gas sensor: {} param: {}, gasname: {}" 528 | .format(self.name, source, param, gasname)) 529 | 530 | if type(GPSsrc) is str and GPSsrc.lower() == 'static': 531 | self.source_dict['gps_lat'] = ('static', 'static_lat') 532 | self.source_dict['gps_lon'] = ('static', 'static_lon') 533 | self.source_dict['gps_alt'] = ('static', 'static_alt') 534 | self.source_dict['gps_fix'] = ('static', 'static_fix') 535 | self.source_dict['gps_num'] = ('static', 'static_num') 536 | self.fmt_opt = 1 # (0) default (real GPS) (1) gps information invalid always 0 or 1 537 | self.box.logger.info("LASS {}: GPSsrc: {}:" 538 | .format(self.name, 'static')) 539 | 540 | else: 541 | self.source_dict['gps_lat'] = (GPSsrc, 'latitude' ) 542 | self.source_dict['gps_lon'] = (GPSsrc, 'longitude') 543 | self.source_dict['gps_alt'] = (GPSsrc, 'altitude' ) 544 | self.source_dict['gps_fix'] = (GPSsrc, 'fix' ) 545 | self.source_dict['gps_num'] = (GPSsrc, 'satnum' ) 546 | self.source_dict['gps_num'] = (GPSsrc, 'satnum' ) 547 | self.fmt_opt = 0 # (0) default (real GPS) (1) gps information invalid always 0 or 1 548 | self.box.logger.info("LASS {}: GPSsrc: {}:" 549 | .format(self.name, GPSsrc)) 550 | 551 | if type(timedatesrc) is str and timedatesrc.lower() == 'system': 552 | self.source_dict['time'] = ('system', 'timestr') 553 | self.source_dict['date'] = ('system', 'datestr') 554 | self.source_dict['ticks'] = ('system', 'tickstr') 555 | self.box.logger.info("LASS {}: timedatesrc: {}:" 556 | .format(self.name, 'system')) 557 | else: 558 | self.source_dict['time'] = (timedatesrc, 'timestr') 559 | self.source_dict['date'] = (timedatesrc, 'datestr') 560 | self.source_dict['ticks'] = ('system', 'tickstr') 561 | self.box.logger.info("LASS {}: timedatesrc: {}:" 562 | .format(self.name, timedatesrc)) 563 | 564 | # 's_gx' g0, g1, g2, g3, g4, g5, g6 g7, g8, gg 565 | # 's_gx' NH3, CO, NO2, C3H8, C4H10, CH4, H2, C2H5OH, SenseAir S8 CO2, TVOC 566 | # 's_hx' h0, h1, h2, h3, h4, h5, 567 | # 's_hx' DHT22, HTS221, SHT31, HTU21D, BME280, SHT25 568 | # 's_tx' s_t0, s_t1, s_t2, s_t3, s_t4, s_t5 569 | # 's_tx' DHT22, HTS221, SHT31, HTU21D, BME280, SHT25 570 | # 's_dx' d0, d1, d2 d3 571 | # 's_dx' PM2.5, PM10, PM1, Panasonic 572 | 573 | def build_entry(self): 574 | 575 | self.LASS_data = [] 576 | 577 | # static box information 578 | self.LASS_data.append('ver_format=' + str(self.ver_format)) 579 | self.LASS_data.append('fmt_opt=' + str(self.fmt_opt)) 580 | self.LASS_data.append('app=' + str(self.app)) 581 | self.LASS_data.append('ver_app=' + str(self.ver_app)) 582 | self.LASS_data.append('device_id=' + str(self.device_id)) 583 | self.LASS_data.append('device=' + str(self.device)) 584 | 585 | systdd = self.box.get_system_timedate_dict() 586 | 587 | for key, (source, param) in self.source_dict.items(): 588 | if (key in ('time', 'date', 'ticks') and source == 'system'): 589 | thing = systdd[param] 590 | if type(thing) is str and len(thing) >=8: 591 | self.LASS_data.append(key + '=' + systdd[param]) 592 | elif ('gps' in key and source == 'static'): 593 | thing = getattr(self, param) 594 | if thing != None: 595 | self.LASS_data.append(key + '=' + str(thing) ) 596 | else: 597 | try: 598 | thing = source.datadict[param] 599 | if thing != None: 600 | self.LASS_data.append(key + '=' + str(thing)) 601 | except: 602 | pass 603 | 604 | # https://www.saltycrane.com/blog/2008/11/python-datetime-time-conversions/ 605 | 606 | # sequence number 607 | self.LASS_data.append('s_0=' + str(self.sequence_number)) 608 | self.sequence_number += 1 609 | 610 | # battery level 611 | self.LASS_data.append('s_1=' + str(self.battery_level_static)) 612 | 613 | # battery mode 614 | self.LASS_data.append('s_2=' + str(self.battery_mode_static)) 615 | 616 | # motion speed 617 | self.LASS_data.append('s_3=' + str(self.motion_speed_static)) 618 | 619 | # CPU utilization 620 | # http://psutil.readthedocs.io/en/latest/ 621 | self.CPU_utilization = psutil.cpu_percent() 622 | self.LASS_data.append('s_4=' + str(self.CPU_utilization)) 623 | 624 | self._generate_LASS_string() 625 | self.send_to_LASS() 626 | 627 | def _generate_LASS_string(self): 628 | self.LASS_string = '|'.join([''] + self.LASS_data + ['']) 629 | self.box.logger.info("LASS_string '{}'".format(self.LASS_string)) 630 | return self.LASS_string 631 | 632 | def send_to_LASS(self): 633 | print "==============================" 634 | print time.ctime(), self.LASS_string 635 | self.box.logger.info("send_to_LASS: {}".format(self.LASS_string)) 636 | self.client.publish(self.topic, "%s" % ( self.LASS_string )) 637 | print "==============================" 638 | # return self.LASS_string 639 | pass 640 | 641 | def build_and_send_to_LASS(self): 642 | self.build_entry() 643 | self.send_to_LASS() 644 | return self.LASS_string 645 | 646 | # ['ver_format', 'fmt_opt', 'app', 'ver_app', 'device_id', 'tick', 647 | # 'date', 'time', 'device', 's_0', 's_1', 's_2', 's_3', 's_d0', 648 | # 's_t0', 's_h0', 'gps_lat', 'gps_lon', 'gps_fix', 'gps_num', 649 | # 'gps_alt'] 650 | 651 | # time hh:mm:ss 652 | # date yyyy-mm-dd 653 | 654 | # 's_0' # sequence number 655 | # 's_1' # battery level 656 | # 's_2' # battery mode vs charging 657 | # 's_3' # motion speed 658 | # 's_4' # CPU utiliation 659 | 660 | # 's_bx' # barometer b0, b1, b2 = Grove, BMP180, BME280 661 | # 's_dx' # dust d0, d1, d2 is PM2.5, PM10, PM1, d3 Panasonic 662 | # 's_gx' # gas g0, g1, g2, g3, g4, g5, g6 g7, g8, gg 663 | # 's_gx' # gas NH3, CO, NO2, C3H8, C4H10, CH4, H2, C2H5OH, SenseAir S8 CO2, TVOC 664 | # 's_hx' # h0, h1, h2, h3, h4, h5, DHT22, HTS221, SHT31, HTU21D, BME280, SHT25 665 | 666 | # 's_Ix' # light 667 | # 's_nx' # radiation 668 | # 's_ox' # other, misc 669 | # 's_px' # rain 670 | # 's_sx' # sound 671 | # 's_tx' # temperature, t0, t1, t2, t3, t4, t5 is DHT22, HTS221, SHT31, HTU21D, BME280, SHT25 672 | # 's_wx' # winds w0, w1 speed, direction 673 | # 's_rx' # rainfall r10, r60 is 10 and 60 minutes 674 | 675 | 676 | class LOG(object): 677 | 678 | def __init__(self, box, filename, name=None): 679 | 680 | self.box = box 681 | self.name = name 682 | self.filename = filename 683 | self.devices = [] 684 | 685 | self.box.logger.info("LOG '{}' __init__ logfilename: '{}'" 686 | .format(self.name, self.filename)) 687 | 688 | self.t_previous_sysinfo = None 689 | 690 | headerlines = [] 691 | headerlines.append('New log file, filename = ' + str(self.filename)) 692 | headerlines.append('New log file, log name = ' + str(self.name)) 693 | 694 | datetimedict = self.box.get_system_timedate_dict() 695 | 696 | headerlines.append('Time = ' + datetimedict['timestr']) 697 | headerlines.append('Date = ' + datetimedict['datestr']) 698 | headerlines.append('box name = ' + str(self.box.name)) 699 | try: 700 | headerlines.append('box MAC address = ' + 701 | str(self.box.macaddress)) 702 | except: 703 | pass 704 | 705 | with open(filename, 'w') as outfile: 706 | outfile.writelines([line + '\n' for line in headerlines]) 707 | 708 | def __repr__(self): 709 | return ('{self.__class__.__name__}({self.name})' 710 | .format(self=self)) 711 | 712 | def configure(self, logconfigure_dict): 713 | 714 | for device, datakeys in logconfigure_dict.items(): 715 | if device in self.box.devices: 716 | self.devices.append((device, datakeys)) 717 | # does not test if datakeys are there, device may be flexible. 718 | else: 719 | print "Not added. This is not in box.devices" 720 | 721 | def build_entry(self, sysinfo_interval=None): 722 | 723 | self.datadict = dict() 724 | 725 | self.datadict['buildtime'] = self.box.get_system_datetime() 726 | 727 | try: 728 | time_since = time.time() - self.t_previous_sysinfo 729 | except: 730 | time_since = None 731 | 732 | if ((time_since == None) or 733 | (time_since > sysinfo_interval) or 734 | (sysinfo_interval<=0)): 735 | 736 | sysinfolines = self.box._get_some_system_info_lines() 737 | 738 | self.datadict['sysinfolines'] = sysinfolines 739 | 740 | self.t_previous_sysinfo = time.time() 741 | 742 | for device, datakeys in self.devices: 743 | 744 | devdict = dict() 745 | 746 | self.datadict[device.name] = devdict 747 | 748 | for dk in datakeys: 749 | try: 750 | devdict[dk] = device.datadict[dk] 751 | except: 752 | pass 753 | 754 | def save_entry(self): 755 | 756 | lines = [] 757 | try: 758 | lines += self.datadict.pop('sysinfolines') 759 | except: 760 | pass 761 | for key, info in self.datadict.items(): 762 | lines += [key] 763 | if type(info) is list: 764 | lines += info 765 | elif type(info) is str: 766 | lines += [info] 767 | elif type(info) is dict: 768 | for datakey, data in info.items(): 769 | lines += [' datakey: ' + datakey + ' = ' + str(data)] 770 | 771 | with open(self.filename, 'a') as outfile: # note, append!!! 772 | outfile.writelines([line + '\n' for line in lines]) 773 | 774 | self.log_entry_lines = lines # save for debugging 775 | 776 | def build_and_save_entry(self, sysinfo_interval=None): 777 | 778 | self.build_entry(sysinfo_interval=sysinfo_interval) 779 | self.save_entry() 780 | 781 | 782 | class GPIO_DEVICE(object): 783 | 784 | def __init__(self, box, name=None): 785 | 786 | self.box = box 787 | self.pi = self.box.pi 788 | self.bus = self.box.bus 789 | self.name = name 790 | self.datadict = dict() 791 | self.statistics = {'nreads':0, 'ngoodreads':0, 792 | 'nbadreads':0} # minimal each may have more 793 | self.last_twenty_stats = [] 794 | 795 | self.box.add_device(self) 796 | 797 | donts = ('self', 'name', 'box') 798 | for dont in donts: 799 | try: 800 | self.instance_things.pop(dont) 801 | except: 802 | pass 803 | 804 | def __repr__(self): 805 | return ('{self.__class__.__name__}({self.name})' 806 | .format(self=self)) 807 | 808 | def get_my_current_instance_info(self): 809 | 810 | current_info = dict() 811 | for key in self.instance_things: 812 | current_info[key] = getattr(self, key) 813 | return current_info 814 | 815 | def get_my_original_instance_info(self): 816 | 817 | original_info = self.instance_things.copy() 818 | 819 | return original_info 820 | 821 | def _last_twenty_increment(self): 822 | 823 | self.last_twenty_stats = ([self.last_read_is_good] + 824 | self.last_twenty_stats[:19]) 825 | 826 | 827 | class Dummy(GPIO_DEVICE): 828 | 829 | devkind = 'Dummy' 830 | 831 | def __init__(self, box, name, dummydatadict=None): 832 | 833 | self.instance_things = locals() 834 | 835 | GPIO_DEVICE.__init__(self, box, name) 836 | 837 | self.box.logger.info("Dummy '{}' __init__".format(self.name)) 838 | 839 | if type(dummydatadict) == dict: 840 | self.datadict.update(dummydatadict) 841 | 842 | def read(self): # doesn't really read anything 843 | 844 | self.last_read_is_good = False 845 | self.statistics['nreads'] += 1 846 | 847 | self.datadict['read_time'] = time.time() 848 | 849 | self.last_read_is_good = True # it's a dummy! 850 | if self.last_read_is_good: 851 | self.statistics['ngoodreads'] += 1 852 | else: 853 | self.statistics['nbadreads'] += 1 854 | 855 | self._last_twenty_increment() 856 | 857 | 858 | class G5bb(GPIO_DEVICE): 859 | 860 | devkind = "G5" 861 | 862 | def __init__(self, box, name, DATA, collect_time=None): 863 | 864 | self.instance_things = locals() 865 | 866 | GPIO_DEVICE.__init__(self, box, name) 867 | 868 | self.box.logger.info("G5bb '{}' __init__".format(self.name)) 869 | 870 | if collect_time == None: 871 | collect_time = 3.0 872 | 873 | self.collect_time = collect_time 874 | self.DATA = DATA 875 | self.baud = 9600 876 | self.key = '424d' 877 | 878 | def read(self): 879 | 880 | self.datadict = dict() # assures the old dict has been cleared. 881 | 882 | self.last_read_is_good = False 883 | self.statistics['nreads'] += 1 884 | 885 | try: 886 | self.pi.bb_serial_read_close(self.DATA) 887 | except: 888 | pass 889 | 890 | self.pi.bb_serial_read_open(self.DATA, self.baud) 891 | time.sleep(self.collect_time) 892 | 893 | self.datadict['start_read_time'] = time.time() 894 | size, data = self.pi.bb_serial_read(self.DATA) 895 | #data_hexlified = hexlify(data) 896 | 897 | try: 898 | import struct 899 | except ImportError: 900 | import ustruct as struct 901 | 902 | buffer = [] 903 | data = list(data) 904 | buffer += data 905 | while buffer and buffer[0] != 0x42: 906 | buffer.pop(0) 907 | 908 | if len(buffer) > 200: 909 | buffer = [] # avoid an overrun if all bad data 910 | if buffer[1] != 0x4d: 911 | buffer.pop(0) 912 | 913 | check = sum(buffer[0:30]) 914 | chksum = buffer[30]*256 + buffer[31] 915 | 916 | if check == chksum: 917 | PM1 = buffer[10]*256 + buffer[11] 918 | PM25 = buffer[12]*256 + buffer[13] 919 | PM10 = buffer[14]*256 + buffer[15] 920 | 921 | try: 922 | temperature = int(buffer[24]*256 + buffer[25])/10 923 | humidity = int(buffer[26]*256 + buffer[27])/10 924 | self.datadict['humidity'] = humidity 925 | self.datadict['temperature'] = temperature 926 | except: 927 | pass 928 | 929 | self.datadict['PM1'] = PM1 930 | self.datadict['PM25'] = PM25 931 | self.datadict['PM10'] = PM10 932 | self.last_read_is_good = True 933 | 934 | if self.last_read_is_good: 935 | self.statistics['ngoodreads'] += 1 936 | else: 937 | self.statistics['nbadreads'] += 1 938 | 939 | self._last_twenty_increment() 940 | 941 | 942 | class GPSbb(GPIO_DEVICE): 943 | 944 | devkind = "GPS" # "u-blox NEO 6, 7" 945 | 946 | def __init__(self, box, name, DATA, collect_time=None): 947 | 948 | self.instance_things = locals() 949 | 950 | GPIO_DEVICE.__init__(self, box, name) 951 | 952 | self.box.logger.info("GPSbb '{}' __init__".format(self.name)) 953 | 954 | if collect_time == None: 955 | collect_time = 3.0 956 | 957 | self.DATA = DATA 958 | 959 | # okdict = {'GNGGA':15, 'GNRMC':13, 'GNGLL':8} 960 | # latlonstartdict = {'GNGGA':2, ' GNRMC':3, 'GNGLL':1} 961 | # timestartdict = {'GNGGA':1, ' GNRMC':1, 'GNGLL':5} 962 | 963 | self.sentence_type = "$GNGGA" 964 | self.ok_length = 15 965 | self.speed_sentence_type = "$GNVTG" # speed 966 | self.speed_ok_length = 10 967 | self.satpos_sentence_type = "$GPGSV" # satellite positions 968 | 969 | self.DATA = DATA 970 | self.baud = 9600 971 | 972 | if collect_time == None: 973 | collect_time = 3. 974 | self.collect_time = collect_time 975 | 976 | def _read_chunk(self): 977 | 978 | try: 979 | self.status = self.pi.bb_serial_read_close(self.DATA) 980 | except: 981 | self.status = None 982 | pass 983 | 984 | self.status = self.pi.bb_serial_read_open(self.DATA, self.baud) 985 | 986 | time.sleep(self.collect_time) 987 | 988 | size, data = self.pi.bb_serial_read(self.DATA) 989 | lines = ''.join(chr(x) for x in data) 990 | self.status = self.pi.bb_serial_read_close(self.DATA) 991 | 992 | return lines.splitlines() 993 | 994 | def _get_degs(self, string, hemisphere): 995 | 996 | A, B = string.split('.') 997 | 998 | mins = float(A[-2:]) + float('0.' + B) 999 | degs = float(A[:-2]) 1000 | 1001 | degrees = degs + mins/60. 1002 | 1003 | if hemisphere in ('S', 'W'): 1004 | degrees *= -1. 1005 | 1006 | return degrees 1007 | 1008 | def read(self): 1009 | 1010 | self.datadict = dict() # assures the old dict has been cleared. 1011 | 1012 | self.last_read_is_good = False 1013 | self.statistics['nreads'] += 1 1014 | 1015 | self.datadict['start_read_time'] = time.time() 1016 | all_lines = self._read_chunk() 1017 | self.datadict['stop_read_time'] = time.time() 1018 | 1019 | gprmc = [s for s in all_lines if "$GPRMC" in s] 1020 | 1021 | if gprmc is not None: 1022 | print "---Parsing GPRMC---", gprmc[0] 1023 | gdata = gprmc[0].split(",") 1024 | 1025 | status = gdata[1] 1026 | latitude = gdata[3] #latitude 1027 | dir_lat = gdata[4] #latitude direction N/S 1028 | longitute = gdata[5] #longitute 1029 | dir_lon = gdata[6] #longitude direction E/W 1030 | speed = gdata[7] #Speed in knots 1031 | trCourse = gdata[8] #True course 1032 | try: 1033 | receive_t = gdata[1][0:2] + ":" + gdata[1][2:4] + ":" + gdata[1][4:6] 1034 | except ValueError: 1035 | pass 1036 | 1037 | try: 1038 | receive_d = gdata[9][0:2] + "/" + gdata[9][2:4] + "/" + gdata[9][4:6] 1039 | except ValueError: 1040 | pass 1041 | 1042 | try: 1043 | self.datadict['latitude'] = float(latitude) 1044 | except ValueError: 1045 | self.datadict['latitude'] = 0.0 1046 | 1047 | try: 1048 | self.datadict['longitude'] = float(longitute) or 0.0 1049 | except ValueError: 1050 | self.datadict['longitude'] = 0.0 1051 | 1052 | self.datadict['dir_lat'] = dir_lat 1053 | self.datadict['dir_lon'] = dir_lon 1054 | #self.datadict['n_sats'] = n_sats 1055 | #self.datadict['coor_time_string'] = coor_time_string 1056 | self.datadict['speed'] = speed 1057 | self.datadict['fix'] = 1 1058 | self.last_read_is_good = True 1059 | print "time : %s, latitude : %s(%s), longitude : %s(%s), speed : %s, True Course : %s, Date : %s" % (receive_t, latitude , dir_lat, longitute, dir_lon, speed, trCourse, receive_d) 1060 | else: 1061 | self.datadict['fix'] = 0 1062 | 1063 | #self.datadict['satdict'] = satdict # very inclusive! 1064 | 1065 | 1066 | class DHT22bb(GPIO_DEVICE): 1067 | 1068 | devkind = "DHT22" 1069 | 1070 | def __init__(self, box, name=None, DATA=None, POWER=None): 1071 | 1072 | self.instance_things = locals() 1073 | 1074 | GPIO_DEVICE.__init__(self, box, name) 1075 | 1076 | self.box.logger.info("DHT22bb '{}' __init__".format(self.name)) 1077 | 1078 | self.DATA = DATA 1079 | self.POWER = POWER 1080 | 1081 | # Following based on https://github.com/joan2937/pigpio/blob/master/EXAMPLES/Python/DHT22_AM2302_SENSOR/DHT22.py 1082 | 1083 | self.diffs = [] # time differences (tics = microseconds) 1084 | 1085 | self.pi.write(self.POWER, 1) # it takes about 2 seconds to activate, should sleep 1086 | 1087 | atexit.register(self.cancel) # Cancel watchdog on exit 1088 | 1089 | self.high_tick = 0 1090 | self.bit = 40 1091 | self.tick_threshold = 50 # data is 1 or 0 1092 | 1093 | self.pi.set_pull_up_down(self.DATA, pigpio.PUD_OFF) 1094 | 1095 | self.pi.set_watchdog(self.DATA, 0) # Kill any existing watchdogs on the pin 1096 | 1097 | # Set the callback on the pin now, You'll start the watchdog later 1098 | self.cb = self.pi.callback(self.DATA, pigpio.EITHER_EDGE, 1099 | self._cb2) 1100 | 1101 | def _cb2(self, gpio, level, tick): 1102 | 1103 | diff = pigpio.tickDiff(self.high_tick, tick) 1104 | 1105 | if level == 0: 1106 | 1107 | self.diffs.append(diff) 1108 | 1109 | if self.bit >= 40: # Message complete. 1110 | self.bit = 40 1111 | 1112 | elif self.bit == 39: # 40th bit received. 1113 | 1114 | self.pi.set_watchdog(self.DATA, 0) # deactivate watchdog 1115 | 1116 | self.bit += 1 1117 | 1118 | elif level == 1: 1119 | 1120 | self.high_tick = tick 1121 | 1122 | if diff > 250000: 1123 | 1124 | self.bit = -2 1125 | 1126 | else: # level == pigpio.TIMEOUT: 1127 | 1128 | self.pi.set_watchdog(self.DATA, 0) # deactivate watchdog 1129 | 1130 | def read(self): 1131 | 1132 | self.last_read_is_good = False 1133 | self.statistics['nreads'] += 1 1134 | 1135 | self.datadict = dict() # clear old data 1136 | self.diffs = [] # clear old data 1137 | 1138 | self.datadict['trigger_time'] = time.time() 1139 | self.pi.write(self.DATA, pigpio.LOW) 1140 | 1141 | time.sleep(0.017) # 17 ms 1142 | 1143 | self.pi.set_mode(self.DATA, pigpio.INPUT) 1144 | 1145 | self.pi.set_watchdog(self.DATA, 200) 1146 | 1147 | time.sleep(0.2) 1148 | 1149 | self.datadict['read_time'] = time.time() 1150 | 1151 | diffs_length = len(self.diffs) 1152 | 1153 | self.datadict['diffs'] = self.diffs 1154 | self.datadict['diffs_length'] = diffs_length 1155 | 1156 | if diffs_length == 43: 1157 | 1158 | five = [self.diffs[3+8*i : 3+8*(i+1)] for i in range(5)] 1159 | 1160 | values = [] 1161 | for thing in five: 1162 | 1163 | value = 0 1164 | 1165 | for diff in thing: 1166 | value = (value<<1) + int(diff>self.tick_threshold) 1167 | 1168 | values.append(value) 1169 | 1170 | HH, HL, TH, TL, check_sum = values 1171 | 1172 | four_sum = sum(values[:4]) 1173 | 1174 | okay = four_sum == check_sum 1175 | 1176 | self.datadict['values'] = values 1177 | self.datadict['checksum_okay'] = okay 1178 | 1179 | if okay: 1180 | 1181 | humidity = HH + 0.1*HL 1182 | 1183 | if TH & 128: # temperature is negative 1184 | sign = -1. 1185 | TH = TH & 127 1186 | else: 1187 | sign = +1. 1188 | 1189 | temperature = sign * (TH + 0.1*TL) 1190 | 1191 | self.datadict['humidity'] = humidity 1192 | self.datadict['temperature'] = temperature 1193 | self.last_read_is_good = True 1194 | 1195 | else: 1196 | # print "diffs_length is not 43, it's ", diffs_length 1197 | pass 1198 | 1199 | if self.last_read_is_good: 1200 | self.statistics['ngoodreads'] += 1 1201 | else: 1202 | self.statistics['nbadreads'] += 1 1203 | 1204 | self._last_twenty_increment() 1205 | 1206 | # CANCEL THE WATCHDOG ON EXIT! 1207 | def cancel(self): 1208 | """Cancel the DHT22 sensor.""" 1209 | 1210 | self.pi.set_watchdog(self.DATA, 0) 1211 | 1212 | if self.cb != None: 1213 | self.cb.cancel() 1214 | self.cb = None 1215 | 1216 | 1217 | class OLEDi2c(GPIO_DEVICE): 1218 | 1219 | devkind = "OLED" 1220 | 1221 | def __init__(self, box, name=None): 1222 | 1223 | self.instance_things = locals() 1224 | 1225 | GPIO_DEVICE.__init__(self, box, name) 1226 | 1227 | self.box.logger.info("OLEDi2c '{}' __init__".format(self.name)) 1228 | 1229 | self.screens = [] 1230 | self.screendict = dict() 1231 | 1232 | self.cmdmode = 0x00 1233 | self.datamode = 0x40 1234 | 1235 | # for generating bytes to transfer to display 1236 | self.npages = 8 1237 | self.nsegs = 128 1238 | 1239 | # for generating images using PIL 1240 | self.nx = self.nsegs 1241 | self.ny = 8 * self.npages 1242 | self.nxy = (self.nx, self.ny) 1243 | 1244 | self.array = (np.zeros(self.nx*self.ny, 1245 | dtype=int).reshape(self.ny, self.nx)) 1246 | 1247 | # Constants 1248 | self.SSD1306_I2C_ADDRESS = 0x3C # 011110+SA0+RW - 0x3C or 0x3D 1249 | self.SSD1306_SETCONTRAST = 0x81 1250 | self.SSD1306_DISPLAYALLON_RESUME = 0xA4 1251 | self.SSD1306_DISPLAYALLON = 0xA5 1252 | self.SSD1306_NORMALDISPLAY = 0xA6 1253 | self.SSD1306_INVERTDISPLAY = 0xA7 1254 | self.SSD1306_DISPLAYOFF = 0xAE 1255 | self.SSD1306_DISPLAYON = 0xAF 1256 | self.SSD1306_SETDISPLAYOFFSET = 0xD3 1257 | self.SSD1306_SETCOMPINS = 0xDA 1258 | self.SSD1306_SETVCOMDETECT = 0xDB 1259 | self.SSD1306_SETDISPLAYCLOCKDIV = 0xD5 1260 | self.SSD1306_SETPRECHARGE = 0xD9 1261 | self.SSD1306_SETMULTIPLEX = 0xA8 1262 | self.SSD1306_SETLOWCOLUMN = 0x00 1263 | self.SSD1306_SETHIGHCOLUMN = 0x10 1264 | self.SSD1306_SETSTARTLINE = 0x40 1265 | self.SSD1306_MEMORYMODE = 0x20 1266 | self.SSD1306_COLUMNADDR = 0x21 1267 | self.SSD1306_PAGEADDR = 0x22 1268 | self.SSD1306_COMSCANINC = 0xC0 1269 | self.SSD1306_COMSCANDEC = 0xC8 1270 | self.SSD1306_SEGREMAP = 0xA0 1271 | self.SSD1306_CHARGEPUMP = 0x8D 1272 | self.SSD1306_EXTERNALVCC = 0x1 1273 | self.SSD1306_SWITCHCAPVCC = 0x2 1274 | 1275 | # Scrolling constants 1276 | self.SSD1306_ACTIVATE_SCROLL = 0x2F 1277 | self.SSD1306_DEACTIVATE_SCROLL = 0x2E 1278 | self.SSD1306_SET_VERTICAL_SCROLL_AREA = 0xA3 1279 | self.SSD1306_RIGHT_HORIZONTAL_SCROLL = 0x26 1280 | self.SSD1306_LEFT_HORIZONTAL_SCROLL = 0x27 1281 | self.SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL = 0x29 1282 | self.SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL = 0x2A 1283 | 1284 | # and also 1285 | self.ADDR = self.SSD1306_I2C_ADDRESS # = 0x3C # 011110+SA0+RW - 0x3C or 0x3D 1286 | 1287 | def YAMLsetup(self, fname): 1288 | 1289 | try: 1290 | with open(fname, 'r') as infile: 1291 | self.yamldict = yaml.load(infile) 1292 | except: 1293 | print "yaml read failed for some reason" 1294 | 1295 | try: 1296 | for sname, sdef in self.yamldict.items(): 1297 | screen = self.new_screen(sname) 1298 | if 'fields' in sdef: 1299 | for fname, fdef in sdef['fields'].items(): 1300 | xy0 = fdef['xy0'] 1301 | args = fdef['args'] 1302 | infopairs = args['info'] 1303 | newpairs = [] 1304 | for devname, key in infopairs: 1305 | devicx = self.box.get_device(devname) 1306 | newpairs.append([devicx, key]) 1307 | args['info'] = newpairs 1308 | 1309 | screen.new_field(fname, xy0, **args) 1310 | 1311 | print "successfull oled yaml setup!" 1312 | 1313 | for s in self.screens: 1314 | print s 1315 | for f in s.fields: 1316 | print ' ', f 1317 | 1318 | return self.yamldict 1319 | 1320 | except: 1321 | print "yaml setup could not be implemented successfully for some reason" 1322 | pass 1323 | 1324 | def new_screen(self, name): 1325 | s = SCREEN(self.nxy, name) 1326 | self.add_screen(s) 1327 | return s 1328 | 1329 | def add_screen(self, s): 1330 | self.screens.append(s) 1331 | self.screendict[s.name] = s 1332 | return s 1333 | 1334 | def initiate(self): 1335 | 1336 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_DISPLAYOFF) # 0xAE 1337 | 1338 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SETDISPLAYCLOCKDIV) # 0xD5 1339 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0x80) # the suggested ratio 0x80 1340 | 1341 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SETMULTIPLEX) # 0xA8 1342 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0x3F) 1343 | 1344 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SETDISPLAYOFFSET) # 0xD3 1345 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0x0) # no offset 1346 | 1347 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SETSTARTLINE | 0x0) # line #0 1348 | 1349 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_CHARGEPUMP) # 0x8D 1350 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0x14) # not external Vcc 1351 | 1352 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_MEMORYMODE) # 0x20 1353 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0x00) # 0x00 acts like ks0108 1354 | 1355 | # HEY I AM NOT SURE ABOUT THIS ONE! 1356 | # We'd like VERTICAL addressing mode 1357 | # which should be 0x01?? 1358 | 1359 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SEGREMAP | 0x1) 1360 | 1361 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_COMSCANDEC) 1362 | 1363 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SETCOMPINS) # 0xDA 1364 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0x12) 1365 | 1366 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SETCONTRAST) # 0x81 1367 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0xCF) # not external Vcc 1368 | 1369 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SETPRECHARGE) # 0xd9 1370 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0xF1) # not external Vcc 1371 | 1372 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_SETVCOMDETECT) # 0xDB 1373 | self.bus.write_byte_data(self.ADDR, self.cmdmode, 0x40) 1374 | 1375 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_DISPLAYALLON_RESUME) # 0xA4 1376 | 1377 | self.bus.write_byte_data(self.ADDR, self.cmdmode, self.SSD1306_NORMALDISPLAY) # 0xA6 1378 | 1379 | print "OLED inited!" 1380 | 1381 | def display_off(self): 1382 | 1383 | self.initiate() 1384 | 1385 | time.sleep(0.2) 1386 | 1387 | for thing in (self.SSD1306_DISPLAYOFF, ): 1388 | 1389 | self.bus.write_byte_data(self.ADDR, self.cmdmode, thing) 1390 | 1391 | time.sleep(0.2) 1392 | 1393 | def display_on(self): 1394 | 1395 | self.initiate() 1396 | 1397 | time.sleep(0.2) 1398 | 1399 | for thing in (self.SSD1306_DISPLAYON, self.SSD1306_DISPLAYALLON, 1400 | self.SSD1306_DISPLAYALLON_RESUME): 1401 | 1402 | self.bus.write_byte_data(self.ADDR, self.cmdmode, thing) 1403 | 1404 | time.sleep(0.2) 1405 | 1406 | def set_contrast(self, contrast): 1407 | 1408 | # contrast = 128 # would be 50% for example 1409 | self.bus.write_byte_data(self.ADDR, 0x00, self.SSD1306_SETCONTRAST) 1410 | self.bus.write_byte_data(self.ADDR, 0x00, contrast) 1411 | 1412 | def show_black(self): 1413 | self.array[:] = 0 1414 | self.show_array() 1415 | 1416 | def show_white(self): 1417 | self.array[:] = 1 1418 | self.show_array() 1419 | 1420 | def show_gray(self): 1421 | self.array[:] = 0 1422 | self.array[0::2, 0::2] = 1 1423 | self.array[1::2, 1::2] = 1 1424 | self.show_array() 1425 | 1426 | def show_array(self): 1427 | self.pages = self.array.reshape(self.npages, 8, -1) # [::-1] # invert it bitwise or this way 1428 | self.bytelist = self._pages_to_bytes(self.pages) 1429 | 1430 | 1431 | if 1 == 1: # Hey! # Hey! double-check other works also! 1432 | # Write buffer data. 1433 | for byte in self.bytelist: 1434 | self.bus.write_byte_data(self.ADDR, self.datamode, byte) 1435 | 1436 | else: 1437 | # Write buffer data. 1438 | for i in range(0, len(self.bytelist), 31): 1439 | bus.write_i2c_block_data(self.ADDR, self.datamode, self.bytelist[i:i+31]) 1440 | 1441 | def _pages_to_bytes(self, Zpages): 1442 | twos = 2**np.arange(8)[:, None] 1443 | Zbinpages = [] 1444 | for page in Zpages: 1445 | bytez = (page.astype(bool)*twos).sum(axis=0) 1446 | Zbinpages.append(bytez.tolist()) 1447 | 1448 | allbytez = sum(Zbinpages, []) 1449 | return allbytez 1450 | 1451 | def get_screen(self, scrn): # heytoday this is updated! 1452 | 1453 | if scrn in self.screens: 1454 | sc = scrn 1455 | else: 1456 | try: 1457 | sc = [s for s in self.screens if 1458 | s.name == scrn][0] 1459 | except: 1460 | print " oh, I couldn't find s.name = ", scrn 1461 | print " all of the names are: ", [s.name for s in self.screens] 1462 | sc = None 1463 | 1464 | return sc 1465 | 1466 | def show_screen(self, showscreen): # heytoday this is updated! 1467 | 1468 | if showscreen not in self.screens: # screen object 1469 | showscreen = self.get_screen(showscreen) # screen name search 1470 | 1471 | if showscreen: 1472 | self.array = showscreen.array.copy() 1473 | self.show_array() 1474 | else: 1475 | print "screen not found" 1476 | return showscreen 1477 | 1478 | def preview_screen(self, previewscreen): # heytoday this is updated! 1479 | 1480 | if previewscreen not in self.screens: # screen object 1481 | previewscreen = self.get_screen(previewscreen) # screen name search 1482 | 1483 | if previewscreen: 1484 | screen.preview_me() 1485 | 1486 | def preview_me(self): 1487 | 1488 | plt.figure() 1489 | plt.imshow(self.array, cmap='gray', interpolation='nearest') 1490 | plt.show() 1491 | 1492 | def update_and_show_screen(self, showscreen): # heytoday this is updated! 1493 | 1494 | showscreen = self.get_screen(showscreen) 1495 | 1496 | if showscreen: 1497 | showscreen.update() 1498 | self.show_screen(showscreen) 1499 | 1500 | def array_stats(self): 1501 | 1502 | print "self.array.min(), self.array.max(): ", self.array.min(), self.array.max() 1503 | print "self.array.shape, self.array.dtype: ", self.array.shape, self.array.dtype 1504 | 1505 | def _embed(self, small_array, big_array, big_index): 1506 | """Overwrites values in big_array starting at big_index with those in small_array""" 1507 | slices = [np.s_[i:i+j] for i,j in zip(big_index, small_array.shape)] 1508 | big_array[slices] = small_array 1509 | try: 1510 | big_array[slices] = small_array 1511 | except: 1512 | print "field embed failed" 1513 | 1514 | def show_image(self, filename, resize_method, conversion_method, threshold=None): 1515 | 1516 | self.img0 = Image.open(filename) 1517 | w0, h0 = self.img0.size 1518 | 1519 | self.box.logger.info("showimage opened size w0, h0 {}".format((w0, h0))) 1520 | print "showimage opened size w0, h0 {}".format((w0, h0)) 1521 | 1522 | if resize_method == 'stretch': 1523 | 1524 | new_size = (self.nx, self.ny) 1525 | 1526 | self.imgr = self.img0.resize(new_size) 1527 | 1528 | self.box.logger.info("showimage new_size stretch {}".format(new_size)) 1529 | print "showimage new_size stretch {}".format(new_size) 1530 | 1531 | elif resize_method == 'fit': 1532 | 1533 | wscale = float(self.nx)/float(w0) 1534 | hscale = float(self.ny)/float(h0) 1535 | 1536 | if hscale <= wscale: 1537 | print "hscale <= wscale" 1538 | new_size = (int(w0*hscale), self.ny) 1539 | else: 1540 | print "hscale > wscale" 1541 | new_size = (self.nx, int(h0*hscale)) 1542 | 1543 | self.imgr = self.img0.resize(new_size) 1544 | 1545 | self.box.logger.info("showimage new_size fit {}".format(new_size)) 1546 | print "showimage new_size fit {}".format(new_size) 1547 | 1548 | else: 1549 | 1550 | self.imgr = None 1551 | 1552 | self.box.logger.error("showimage new_size failed") 1553 | print "showimage new_size failed" 1554 | 1555 | if self.imgr: 1556 | 1557 | if conversion_method in ('default', 'dither'): 1558 | 1559 | self.imgc = self.imgr.convert("1") 1560 | 1561 | self.box.logger.info("showimage convert default".format(new_size)) 1562 | print "showimage convert default".format(new_size) 1563 | 1564 | elif conversion_method in ('threshold', ): 1565 | 1566 | self.imgr8 = self.imgr.convert("L") 1567 | self.imgc = self.imgr8.point(lambda x: 0 if x < threshold else 1, mode='1') 1568 | 1569 | self.box.logger.info("showimage convert threshold".format(new_size)) 1570 | print "showimage convert threshold".format(new_size) 1571 | 1572 | else: 1573 | 1574 | imgc = None 1575 | 1576 | self.box.logger.error("showimage conversion failed") 1577 | print "showimage conversion failed" 1578 | 1579 | if self.imgc: 1580 | 1581 | wni, hni = self.imgc.size 1582 | array = np.array(list(self.imgc.getdata())).reshape(hni, wni) 1583 | 1584 | bigarray = np.zeros(self.ny*self.nx, dtype=int).reshape(self.ny, self.nx) 1585 | 1586 | sa0, sa1 = array.shape 1587 | sb0, sb1 = bigarray.shape 1588 | 1589 | hoff = max(0, (sb0-sa0)/2-1) 1590 | woff = max(0, (sb1-sa1)/2-1) 1591 | print "hoff = ", hoff 1592 | print "woff = ", woff 1593 | 1594 | self._embed(array, bigarray, (hoff, woff)) 1595 | 1596 | print "bigarray.shape: ", bigarray.shape 1597 | arrayf = bigarray.astype(float) 1598 | arrayf = arrayf / arrayf.max() 1599 | 1600 | self.array = bigarray.copy() 1601 | 1602 | self.show_array() 1603 | # plt.figure() 1604 | # plt.imshow(arrayf) 1605 | # plt.show() 1606 | 1607 | 1608 | 1609 | class MCP3008bb(GPIO_DEVICE): 1610 | 1611 | devkind="MCP3008" 1612 | 1613 | def __init__(self, box, name=None, CSbar=None, MISO=None, 1614 | MOSI=None, SCLK=None, SPI_baud=None, Vref=None): 1615 | 1616 | self.instance_things = locals() 1617 | 1618 | GPIO_DEVICE.__init__(self, box, name) 1619 | 1620 | self.box.logger.info("MCP3008bb '{}' __init__".format(self.name)) 1621 | 1622 | self.CSbar = CSbar 1623 | self.MISO = MISO 1624 | self.MOSI = MOSI 1625 | self.SCLK = SCLK 1626 | if SPI_baud == None: 1627 | SPI_baud = 10000 1628 | self.SPI_baud = SPI_baud 1629 | self.SPI_MODE = 1 # this is important! 1630 | self.Vref = Vref 1631 | 1632 | self.nbits = 10 # MCP3008 is always 10 bits?? this may change 1633 | 1634 | try: 1635 | self.pi.bb_spi_close(self.CSbar) 1636 | except: 1637 | pass 1638 | 1639 | print " #### Okay, here we go! " 1640 | print "self.CSbar: ", self.CSbar 1641 | print "self.MISO: ", self.MISO 1642 | print "self.MOSI: ", self.MOSI 1643 | print "self.SCLK: ", self.SCLK 1644 | print "self.SPI_baud: ", self.SPI_baud 1645 | print "self.SPI_MODE: ", self.SPI_MODE 1646 | 1647 | self.pi.bb_spi_open(self.CSbar, self.MISO, self.MOSI, 1648 | self.SCLK, self.SPI_baud, self.SPI_MODE) 1649 | 1650 | def _digitize_one_channel(self, n_channel, clear_datadict=False): 1651 | 1652 | if clear_datadict: 1653 | self.datadict = dict() 1654 | 1655 | # NOTE check if iterable, or an integer 1656 | 1657 | three_bytes = [1, (8+n_channel)<<4, 0] 1658 | 1659 | ct, data = self.pi.bb_spi_xfer(self.CSbar, three_bytes) 1660 | 1661 | adc_convert_time = time.time() 1662 | 1663 | counts = ((data[1]<<8) | data[2]) & 0x3FF 1664 | 1665 | self.datadict['adc_convert_time'] = adc_convert_time 1666 | self.datadict['channel ' + str(n_channel) + ' counts'] = counts 1667 | 1668 | return counts, adc_convert_time 1669 | 1670 | def measure_one_voltage(self, channel, clear_datadict=False): 1671 | 1672 | self.last_read_is_good = False 1673 | self.statistics['nreads'] += 1 1674 | 1675 | adc_value, adc_convert_time = self._digitize_one_channel(channel, clear_datadict) 1676 | 1677 | voltage = self.Vref * float(adc_value) / (2**self.nbits - 1.) 1678 | 1679 | self.datadict['channel ' + str(channel) + ' voltage'] = voltage 1680 | 1681 | self.last_read_is_good = True 1682 | 1683 | if self.last_read_is_good: 1684 | self.statistics['ngoodreads'] += 1 1685 | else: 1686 | self.statistics['nbadreads'] += 1 1687 | 1688 | self._last_twenty_increment() 1689 | 1690 | return voltage, adc_value, adc_convert_time 1691 | 1692 | 1693 | class MOS_gas_sensor(GPIO_DEVICE): 1694 | 1695 | devkind="MOS_gas_sensor" 1696 | 1697 | def __init__(self, box, name, ADC, channel, Rseries, 1698 | Calibrationdata, 1699 | use_loglog = False, gasname=None, 1700 | atlimitsisokay = False): 1701 | 1702 | self.instance_things = locals() 1703 | 1704 | GPIO_DEVICE.__init__(self, box, name) 1705 | 1706 | self.box.logger.info("MOS_gas_sensor '{}' __init__".format(self.name)) 1707 | 1708 | if ADC not in self.box.devices: 1709 | ADC = self.box.get_device(ADC) 1710 | 1711 | self.ADC = ADC 1712 | self.Vref = 3.3 1713 | self.channel = channel 1714 | self.Rseries = Rseries 1715 | self.Calibrationdata = Calibrationdata 1716 | self.use_loglog = use_loglog 1717 | self.gasname = gasname 1718 | self.atlimitsisokay = atlimitsisokay 1719 | 1720 | Resistdata, ppmdata = zip(*self.Calibrationdata) 1721 | 1722 | self.ppmdata = ppmdata 1723 | self.Resistdata = Resistdata 1724 | 1725 | self.use_loglog = use_loglog 1726 | 1727 | self.ppmdata = ppmdata 1728 | self.Resistdata = Resistdata 1729 | 1730 | def read(self): 1731 | 1732 | self.last_read_is_good = False 1733 | self.statistics['nreads'] += 1 1734 | 1735 | self.datadict = dict() # clear old data 1736 | 1737 | # get data 1738 | v, adc_val, adc_time = self.ADC.measure_one_voltage(self.channel, 1739 | clear_datadict=False) # make ADC measurement 1740 | if v < 0.1 or v > self.Vref - 0.1: 1741 | v = None # don't continue with pathaological voltage 1742 | 1743 | try: 1744 | R_sensor = self.Rseries*(self.Vref/v - 1.) 1745 | except: 1746 | R_sensor = None # don't continue with pathaological resistance 1747 | 1748 | try: 1749 | ppm = np.interp(R_sensor, self.Resistdata, self.ppmdata) 1750 | except: 1751 | ppm = None # don't continue with pathaological interpolation 1752 | 1753 | if self.atlimitsisokay: 1754 | if ppm <= 0 or ppm < min(self.ppmdata) or ppm > max(self.ppmdata): 1755 | ppm = None 1756 | else: 1757 | if ppm <= 0 or ppm <= min(self.ppmdata) or ppm >= max(self.ppmdata): 1758 | ppm = None 1759 | 1760 | if ppm != None: 1761 | ppm = int(ppm) 1762 | 1763 | self.datadict['read_time'] = time.time() 1764 | self.datadict['ppm'] = ppm 1765 | self.datadict['R_sensor'] = R_sensor 1766 | self.datadict['voltage'] = v 1767 | self.datadict['adc_value'] = adc_val 1768 | self.datadict['adc_convert_time'] = adc_time 1769 | 1770 | if ppm != None: 1771 | self.last_read_is_good = True 1772 | 1773 | if self.last_read_is_good: 1774 | self.statistics['ngoodreads'] += 1 1775 | else: 1776 | self.statistics['nbadreads'] += 1 1777 | 1778 | self._last_twenty_increment() 1779 | 1780 | 1781 | class SCREEN(object): 1782 | 1783 | devkind = "SCREEN" 1784 | 1785 | def __init__(self, nxy, name=None): 1786 | 1787 | self.name = name 1788 | self.fields = dict() 1789 | self.nxy = nxy 1790 | self.nx, self.ny = self.nxy 1791 | 1792 | def __repr__(self): 1793 | return ('{self.__class__.__name__}({self.name})' 1794 | .format(self=self)) 1795 | 1796 | def new_field(self, name, xy0, wh=None, 1797 | fmt=None, fontdef=None, fontsize=None, 1798 | threshold=None, info=None): 1799 | 1800 | f = FIELD(name, wh, fmt, fontdef, fontsize, threshold, info) 1801 | self.add_field(f, xy0) 1802 | return f 1803 | 1804 | def add_field(self, f, xy0): 1805 | self.fields[f] = xy0 1806 | f.screens.append(self) 1807 | return f 1808 | 1809 | def get_field(self, fieldname): 1810 | 1811 | try: 1812 | field = [f for f in self.fields if f.name == fieldname][0] 1813 | except: 1814 | print " oh, I couldn't find f.name = ", fieldname 1815 | print " all of the names are: ", [f.name for f in self.fields] 1816 | field = None 1817 | 1818 | return field 1819 | 1820 | def _embed(self, small_array, big_array, big_index): 1821 | """Overwrites values in big_array starting at big_index with those in small_array""" 1822 | slices = [np.s_[i:i+j] for i,j in zip(big_index, small_array.shape)] 1823 | big_array[slices] = small_array 1824 | try: 1825 | big_array[slices] = small_array 1826 | except: 1827 | print "field embed failed" 1828 | 1829 | def _update_array(self): 1830 | 1831 | self.array = np.zeros(self.nx * self.ny, dtype=int).reshape(self.ny, self.nx) 1832 | 1833 | for field, xy0 in self.fields.items(): 1834 | 1835 | self._embed(field.array, self.array, xy0[::-1]) 1836 | 1837 | def update(self): # heytoday this is updated! 1838 | 1839 | for field in self.fields: 1840 | 1841 | field.update() 1842 | 1843 | self._update_array() 1844 | 1845 | def preview_me(self): 1846 | 1847 | plt.figure() 1848 | plt.imshow(self.array, cmap='gray', interpolation='nearest') 1849 | plt.show() 1850 | 1851 | def update_and_preview_me(self): # heytoday this is updated! 1852 | 1853 | self.update() 1854 | self.preview_me() 1855 | 1856 | 1857 | def show_image(self, filename, resize_method, conversion_method, threshold=None): 1858 | 1859 | img0 = Image.open(filename) 1860 | w0, h0 = img0.width, img0.height 1861 | 1862 | self.box.logger.info("showimage opened size w0, h0 {}".format((w0, h0))) 1863 | print "showimage opened size w0, h0 {}".format((w0, h0)) 1864 | 1865 | if resize_method == 'stretch': 1866 | 1867 | new_size = (self.w, self.h) 1868 | 1869 | imgr = img0.resize(new_size) 1870 | 1871 | self.box.logger.info("showimage new_size stretch {}".format(new_size)) 1872 | print "showimage new_size stretch {}".format(new_size) 1873 | 1874 | elif resize_method == 'fit': 1875 | 1876 | wscale = float(self.nx)/float(w0) 1877 | hscale = float(self.ny)/float(h0) 1878 | 1879 | if hscale <= wscale: 1880 | print "hscale <= wscale" 1881 | new_size = (int(w0*hscale), self.ny) 1882 | else: 1883 | print "hscale > wscale" 1884 | new_size = (self.nx, int(h0*hscale)) 1885 | 1886 | imgr = img0.resize(new_size) 1887 | 1888 | self.box.logger.info("showimage new_size fit {}".format(new_size)) 1889 | print "showimage new_size fit {}".format(new_size) 1890 | 1891 | else: 1892 | 1893 | imgr = None 1894 | 1895 | self.box.logger.error("showimage new_size failed") 1896 | print "showimage new_size failed" 1897 | 1898 | if imgr: 1899 | 1900 | if conversion_method in ('default', 'dither'): 1901 | 1902 | imgc = imgr.convert("1") 1903 | 1904 | self.box.logger.info("showimage convert default".format(new_size)) 1905 | print "showimage convert default".format(new_size) 1906 | 1907 | elif conversion_method in ('threshold', ): 1908 | 1909 | imgr8 = imgr.convert("L") 1910 | imgc = imgr8.point(lambda x: 0 if x < threshold else 1, mode='1') 1911 | 1912 | self.box.logger.info("showimage convert threshold".format(new_size)) 1913 | print "showimage convert threshold".format(new_size) 1914 | 1915 | else: 1916 | 1917 | imgc = None 1918 | 1919 | self.box.logger.error("showimage conversion failed") 1920 | print "showimage conversion failed" 1921 | 1922 | if imgc: 1923 | 1924 | wni, hni = imgc.width, imgc.height 1925 | array = np.array(list(imgc.getdata())).reshape(hni, wni) 1926 | 1927 | bigarray = np.zeros(self.h*self.w, dtype=int).reshape(self.h, self.w) 1928 | 1929 | sa0, sa1 = array.shape 1930 | sb0, sb1 = bigarray.shape 1931 | 1932 | hoff = max(0, (sb0-sa0)/2-1) 1933 | woff = max(0, (sb1-sa1)/2-1) 1934 | print "hoff = ", hoff 1935 | print "woff = ", woff 1936 | 1937 | _embed(array, bigarray, (hoff, woff)) 1938 | 1939 | print "bigarray.shape: ", bigarray.shape 1940 | arrayf = bigarray.astype(float) 1941 | arrayf = arrayf / arrayf.max() 1942 | plt.figure() 1943 | plt.imshow(arrayf) 1944 | plt.show() 1945 | 1946 | 1947 | 1948 | class FIELD(object): 1949 | 1950 | devkind = "FIELD" 1951 | 1952 | def __init__(self, name, wh=None, 1953 | fmt=None, fontdef=None, fontsize=None, 1954 | threshold=None, info=None): 1955 | 1956 | self.name = name 1957 | self.wh = wh 1958 | 1959 | self.screens = [] 1960 | 1961 | try: 1962 | self.w, self.h = self.wh[:2] 1963 | except: 1964 | pass 1965 | 1966 | self.fmt = fmt 1967 | 1968 | self.fontdef = fontdef 1969 | 1970 | if type(fontsize) is int: 1971 | self.fontsize = fontsize 1972 | else: 1973 | self.fontsize = 14 1974 | 1975 | if self.fontdef == 'default': 1976 | self.font = ImageFont.load_default() 1977 | elif type(self.fontdef) is str: 1978 | if self.fontdef[-4:].lower() == '.ttf': 1979 | self.font = ImageFont.truetype(self.fontdef, 1980 | self.fontsize) 1981 | else: 1982 | print "fontdef problem!" 1983 | 1984 | self.threshold = threshold 1985 | self.info = info 1986 | 1987 | def __repr__(self): 1988 | return ('{self.__class__.__name__}({self.name})' 1989 | .format(self=self)) 1990 | 1991 | def update(self): 1992 | 1993 | self._update_string() 1994 | self._generate_array() 1995 | 1996 | def _get_pairs(self, fmt): 1997 | pairs =[(m.start(), m.end()) for m in re.finditer('{([^{)]*)}', fmt)] 1998 | return pairs 1999 | 2000 | def _stringit(self, fmt, data): 2001 | xchar = '##' 2002 | xchar_alt = u'\u25A1\u25A0' 2003 | pairs = self._get_pairs(fmt) 2004 | if len(pairs) == 0: 2005 | s = fmt.format() 2006 | else: 2007 | keepers = [fmt[:pairs[0][0]]] 2008 | for i in range(len(pairs)-1): 2009 | keepers.append(fmt[pairs[i][1]:pairs[i+1][0]]) 2010 | keepers.append(fmt[pairs[-1][1]:]) 2011 | zingers = [fmt[p[0]:p[1]] for p in pairs] 2012 | newdata = [] 2013 | newfmt = keepers[0] 2014 | for d, z, k in zip(data, zingers, keepers[1:]): 2015 | if d == None: 2016 | newfmt += xchar + k 2017 | else: 2018 | newdata.append(d) 2019 | newfmt += z + k 2020 | s = newfmt.format(*newdata) 2021 | return s 2022 | 2023 | def _update_string(self): 2024 | self.string = '' 2025 | self.values = [] 2026 | for device, key in self.info: 2027 | try: 2028 | dev = device # self.box.get_device(device) 2029 | value = dev.datadict[key] 2030 | self.values.append(value) 2031 | except: 2032 | self.values.append(None) 2033 | 2034 | self.string = self._stringit(self.fmt, self.values) 2035 | 2036 | # self.string = self.fmt.format(*self.values) 2037 | 2038 | def _generate_array(self): 2039 | # print " trying to generate my array! ", self.name 2040 | if type(self.threshold) in (int, float): 2041 | self.imageRGB = Image.new('RGB', (self.w, self.h)) 2042 | self.draw = ImageDraw.Draw(self.imageRGB) 2043 | 2044 | self.draw.text((0,0), self.string, font=self.font, 2045 | fill=(255, 255, 255, 255)) # R, G, B alpha 2046 | 2047 | self.image8bit = self.imageRGB.convert("L") 2048 | self.image1bit = self.image8bit.point(lambda x: 0 if x < self.threshold else 1, mode='1') 2049 | self.image = self.image1bit 2050 | self.arrayRGB = np.array(list(self.imageRGB.getdata())).reshape(self.h, self.w, 3) 2051 | self.array8bit = np.array(list(self.image8bit.getdata())).reshape(self.h, self.w) 2052 | self.array1bit = np.array(list(self.image1bit.getdata())).reshape(self.h, self.w) 2053 | else: 2054 | self.image = Image.new('1', (self.w, self.h)) 2055 | self.draw = ImageDraw.Draw(self.image) 2056 | 2057 | self.draw.text((0,0), self.string, font=self.font, fill=255) 2058 | 2059 | self.ww, self.hh = self.image.size 2060 | 2061 | self.array = np.array(list(self.image.getdata())).reshape(self.hh, -1) 2062 | 2063 | def preview_me(self): 2064 | 2065 | plt.figure() 2066 | plt.imshow(self.array, cmap='gray', interpolation='nearest') 2067 | plt.show() 2068 | 2069 | def update_and_preview_me(self): 2070 | 2071 | self.update() 2072 | self.preview_me() 2073 | 2074 | 2075 | def PiM25YAMLreader(fname): 2076 | 2077 | with open(fname, 'r') as infile: 2078 | d = yaml.load(infile) 2079 | 2080 | nboxes = len(d) 2081 | 2082 | boxes = [] 2083 | 2084 | for i, (boxname, boxdef) in enumerate(d.items()): 2085 | 2086 | boxargs = boxdef['args'] 2087 | 2088 | print ' BOXARGS: ', boxargs 2089 | 2090 | box = BOX(boxname, **boxargs) 2091 | 2092 | box.logger.info("BOX '{}' instantiated from YAML file '{}'".format(box.name, fname)) 2093 | 2094 | boxes.append(box) 2095 | 2096 | GPIO_devices = boxdef['GPIO devices'] 2097 | print "Box GPIO devices: ", GPIO_devices.keys() 2098 | 2099 | for j, (devname, devdef) in enumerate(GPIO_devices.items()): 2100 | use_method = devdef['method'] 2101 | method = getattr(box, use_method) 2102 | if 'args' in devdef: 2103 | devargs = devdef['args'] 2104 | device = method(devname, **devargs) 2105 | else: 2106 | device = method(devname) 2107 | if 'oled' in device.devkind.lower() and 'screens' in devdef: 2108 | for sname, sdef in devdef['screens'].items(): 2109 | screen = device.new_screen(sname) 2110 | if 'fields' in sdef: 2111 | for fname, fdef in sdef['fields'].items(): 2112 | xy0 = fdef['xy0'] 2113 | args = fdef['args'] 2114 | infopairs = args['info'] 2115 | newpairs = [] 2116 | for devname, key in infopairs: 2117 | devicx = box.get_device(devname) 2118 | newpairs.append([devicx, key]) 2119 | args['info'] = newpairs 2120 | 2121 | screen.new_field(fname, xy0, **args) 2122 | LASS_devices = boxdef['LASS devices'] 2123 | print "Box LASS devices: ", LASS_devices.keys() 2124 | for LASSname, LASSdevice in LASS_devices.items(): 2125 | newLASS = box.new_LASS(LASSname) 2126 | newLASS.set_static_location(**LASSdevice['static location']) 2127 | sourcedict = LASSdevice['sources'] 2128 | newsourcedict = dict() 2129 | for key, src in sourcedict.items(): 2130 | if key == 'gassensors': 2131 | print 'gassensors = ', src 2132 | for gs in src: 2133 | source = box.get_device(gs) 2134 | print ' zzzzzz: ', source.name 2135 | else: 2136 | if src not in ('static', 'system'): 2137 | source = box.get_device(src) 2138 | else: 2139 | source = src 2140 | newsourcedict[key] = source 2141 | newLASS.set_sources(**newsourcedict) 2142 | 2143 | return boxes 2144 | 2145 | 2146 | -------------------------------------------------------------------------------- /PiM25_Box.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # SUGGESTED CONNECTIONS, but you can of course do it differenlty! 5 | ############################################################## 6 | # Raspberry Pi 3 GPIO Pinout; Corner --> # 7 | # (pin 1) | (pin 2) # 8 | # OLED/GPS Vcc +3.3V | +5.0V GPS NEO6 Vcc # 9 | # OLED SDA GPIO 2 | +5.0V PM25 G5 pin 1 Vcc b# 10 | # OLED SCL GPIO 3 | GND PM25 G5 pin 2 GND o# 11 | # GPIO 4 | UART TX # 12 | # OLED/Gas GND GND | UART RX # 13 | # GPIO 17 | GPIO 18 GPS NEO6 pin 5 TX # 14 | # GPIO 27 | GND GPS NEO6 GND # 15 | # GPIO 22 | GPIO 23 # 16 | #r MCP3008 Vcc/Vref +3.3V | GPIO 24 PM25 G5 pin 5 TX g# 17 | # GPIO 10 | GND DHT22 GND g# 18 | # GPIO 9 | GPIO 25 DHT22 DATA b# # 19 | # GPIO 11 | GPIO 8 DHT22 POWER p# 20 | # GND | GPIO 7 # 21 | # Reserved | Reserved # 22 | # GPIO 5 | GND # 23 | #b MCP3008 CLK GPIO 6 | GPIO 12 # 24 | #g MCP3008 MISO GPIO 13 | GND (GPS GND) # 25 | #y MCP3008 MOSI GPIO 19 | GPIO 16 (GPS TX) # 26 | #o MCP3008 CSbar GPIO 26 | GPIO 20 # 27 | #brMCP3008 GND/GND GND | GPIO 21 # 28 | # (pin 39) | (pin 40) # 29 | ############################################################## 30 | 31 | from PiM25 import BOX 32 | import time 33 | 34 | # make a box 35 | box = BOX('my box', use_WiFi=False, 36 | use_SMBus=True, use_pigpio=True) 37 | 38 | dht = box.new_DHT22bb('my dht', DATA=26, POWER=19) 39 | g5 = box.new_G5bb('my g5', DATA=24, collect_time = 3.0) 40 | #oled = box.new_OLEDi2c('my oled') 41 | #adc = box.new_MCP3008bb('my adc', MISO=22, MOSI=27, 42 | # CSbar=17, SCLK=10, Vref=3.3) 43 | 44 | # if you don't have MOS gas sensors, just use a resistor divider to make a voltage for the ADC 45 | #CO2 = box.new_MOS_gas_sensor('my CO2', ADC=adc, channel=5, 46 | # Rseries=1000, 47 | # Calibrationdata=[[100, 10000], [1000, 1000], [10000, 100]], 48 | # use_loglog=False, gasname='CO2', 49 | # atlimitsisokay=True) 50 | #CO = box.new_MOS_gas_sensor('my CO', ADC=adc, channel=6, 51 | # Rseries=2000, 52 | # Calibrationdata=[[100, 10000], [1000, 1000], [10000, 100]], 53 | # use_loglog=False, gasname='CO', 54 | # atlimitsisokay=True) 55 | 56 | mylogconfig = {dht:['temperature', 'humidity'], 57 | g5:['PM25', 'PM1', 'PM10'], } 58 | 59 | mylog = box.new_LOG('mylog.txt', 'mylog') 60 | mylog.configure(mylogconfig) 61 | 62 | lass = box.new_LASS('mylass') 63 | timedate = box.new_Dummy('sys timedate') 64 | gps = box.new_GPSbb('my gps', DATA=18) 65 | lass.set_sources(humsrc=g5, tempsrc=g5, 66 | pm25src=g5, pm1src=g5, pm10src=g5, 67 | GPSsrc=gps, 68 | timedatesrc='system') 69 | timedate = box.new_Dummy('sys timedate') 70 | 71 | readables = [d for d in box.devices if hasattr(d, 'read')] 72 | 73 | for d in readables: 74 | d.read() 75 | print d, 'read is good: ', d.last_read_is_good 76 | 77 | 78 | #oled.YAMLsetup('oledyaml.yaml') 79 | #oled.initiate() 80 | #oled.display_on() 81 | #for thing in ('show_white', 'show_black', 'show_gray'): 82 | # getattr(oled, thing)() 83 | 84 | while True: 85 | print "loop!" 86 | #oled.show_image('pim25b.bmp', resize_method='fit', 87 | # conversion_method='threshold', threshold=60) 88 | for d in readables: 89 | d.read() 90 | systimedic = box.get_system_timedate_dict() 91 | timedate.datadict.update(systimedic) 92 | 93 | mylog.build_and_save_entry(sysinfo_interval=10) 94 | 95 | lass.build_entry() 96 | 97 | for cycle in range(2): 98 | # for s in oled.screens: 99 | # s.update() 100 | # oled.show_screen(s) 101 | time.sleep(1) 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PiM25 2 | An easy-to-use Python script for connecting a Raspberry Pi-based PM2.5 monitor to the LASS system. 3 | 4 | While a stand-alone AirBox is cool, using a Raspberry Pi to power it is a bit of overkill for something that you put and forget. A simple, reliable, industrial micro-controller (Arduino or other) is more desirable for a put-and-forget airbox that you install and leave somewhere. 5 | 6 | The goal here is to find a way to take full advantage of the Raspberry Pi’s capability in order to do something better. 7 | 8 | Raspberry pi has several important advantages, and PiM25 software’s goal is to make all of these advantages available 9 | 10 | You can: 11 | * Have multiple, virtual boxes 12 | * Compare different sensors head-to-head in real time. 13 | * Do local logging, or electronic reporting of sensors beyond those needed for a standard AirBox report. They allow for many, and for mobile and battery operated support (battery level, battery charge/discharge, motion speed, CPU usage percent) as well as additional sensors beyond (PM1, 2.5, 10, temp, humid) like 14 | * add toxic and irritating gasses 15 | * light levels 16 | * radiation 17 | * rain and precipitation 18 | * barometric pressure 19 | * loud noises 20 | * wind speed and direction 21 | 22 | Multiple, virtual LASS boxes to support multiple sensor sets in the same location 23 | 24 | How to “build a box”. 25 | 26 | There are a few ways. 27 | 28 | * You can write a standard, simple python script. Make a box instance, then make sensor instances from it. 29 | 30 | * You can do it interactively from a Python prompt. Similarly to the script, make a box instance, then make sensor instances from it. 31 | 32 | * Generate the whole thing predefined in a .yaml dictionary. Use PiM25.PiM25YAMLreader(‘filename.yaml’) 33 | 34 | You can also combine any or all of the above! 35 | 36 | # install required package and python module 37 | ```shell 38 | $ sudo apt-get update 39 | $ sudo apt-get install -y python-dev python-pip python-matplotlib mosquitto mosquitto-clients 40 | $ sudo pip install numpy paho-mqtt psutil ymlconfig pynmea2 requests 41 | $ sudo pip3 install paho-mqtt 42 | ``` 43 | 44 | Once you are done, you can SAVE your configuration as a .yaml file using PiM25.PiM25YAMLwriter(‘filename.yaml’) That way you can “rebuild” your box the next time you start your project, exactly as it existed when you were last developing it. 45 | 46 | METHOD 1: Python script. 47 | 48 | from PiM25_UPDATED import BOX 49 | import time 50 | from itertools import cycle 51 | import matplotlib.pyplot as plt 52 | 53 | # make a box 54 | box = BOX('my box', use_WiFi=True, 55 | use_SMBus=True, use_pigpio=True) 56 | 57 | # add your sensors 58 | dht = box.new_DHT22bb('my dht', DATA=17, POWER=27) 59 | 60 | g3 = box.new_G3bb('my dht', DATA=24, collect_time=3.0) 61 | 62 | adc = box.new_MCP3008bbspi('my adc',CSbar=26, MISO=13, 63 | MOSI=19, SCLK=6, Vref=3.3 ) 64 | 65 | CO = box.new_MOS_gas_sensor('my CO sensor', ADC='my adc', channel=1, 66 | Rseries=1000, 67 | Calibrationdata=[[10,5000],[100,2200],[1000,777]], 68 | logCalibrationdata=[[10,5000],[100,2200],[1000,777]], 69 | use_loglog=False, gasname='CO') 70 | 71 | 72 | # add your oled screen setup 73 | 74 | oled = box.new_OLEDi2c('my oled') 75 | 76 | s1 = oled.new_screen('T and H') 77 | 78 | s1.new_field('temp', [2, 5], wh=[120, 24], 79 | fmt='T(C) {0:.1f}', fontdef='Arial Unicode.ttf', 80 | fontsize=20, info=[[dht, 'temperature']]) 81 | s1.new_field('humid', [2, 35], wh=[120, 24], 82 | fmt='H(%) {0:.1f}', fontdef='Arial Unicode.ttf', 83 | fontsize=20, info=[[dht, 'humidity']]) 84 | 85 | s2 = oled.new_screen('particles') 86 | 87 | s2.new_field('PMbig', [2, 5], wh=[120, 24], 88 | fmt='PM25 {0:.0f}', fontdef='Arial Unicode.ttf', 89 | fontsize=20, info=((g3, 'PM25'),)) 90 | s2.new_field('PMsmall', [2, 35], wh=[120, 24], 91 | fmt='PM1 {0:.0f}, PM10 {0:.0f}', fontdef='default', 92 | fontsize=20, info=((g3, 'PM1'), (g3, 'PM10'))) 93 | 94 | 95 | s3 = oled.new_screen('gasses') 96 | 97 | s3.new_field('COa', [2, 30], wh=[120, 24], 98 | fmt='CO (ppm) {0:.1f}', fontdef='Arial Unicode.ttf', 99 | fontsize=18, info=((CO, 'ppm'),)) 100 | 101 | 102 | 103 | oled.initiate() 104 | oled.display_on() 105 | for thing in ('show_white', 'show_black', 'show_gray'): 106 | getattr(oled, thing)() 107 | 108 | # Define your loop for operation 109 | 110 | wait = 7. # seconds 111 | 112 | # Go! 113 | while True: 114 | 115 | oled.show_gray() 116 | 117 | dht.read() 118 | g3.read() 119 | adc.digitize_one_channel(1) 120 | CO.read() 121 | 122 | # or just use box.read_all() 123 | # lass.build_and_send_to_LASS() 124 | # print lass.LASS_string 125 | 126 | for s in oled.screens: 127 | s.update_all() 128 | 129 | tnext = time.time() + wait 130 | 131 | screenz = cycle(oled.screens) 132 | 133 | while time.time() < tnext: 134 | s = screenz.next() 135 | oled.show_screen(s) 136 | time.sleep(1.5) 137 | 138 | 139 | 140 | METHOD 2: Python interactive. 141 | should be similar or identical, just typed line-by-line 142 | 143 | 144 | METHOD 3: .yaml definition. 145 | 146 | my box: 147 | args: {'use_WiFi': True, 'use_pigpio': True, 'use_SMBus': True} 148 | LASS devices: 149 | my LASS: 150 | static location: {'latlon':[25.03366, 121.564841], 'alt':550.0} 151 | sources: {'humsrc':'my dht', 'tempsrc':'my dht', 'pm1src':'my gthree', 'pm25src':'my gthree', 'pm10src':'my gthree', 152 | 'timedatesrc':'system', 'GPSsrc':'static', 'gassensors':[]} 153 | GPIO devices: 154 | my dht: 155 | method: new_DHT22bb 156 | args: {'DATA':17, 'POWER':27} 157 | my gthree: 158 | method: new_G3bb 159 | args: {'DATA':24, 'collect_time':3.0} 160 | my oled: 161 | method: new_OLEDi2c 162 | screens: 163 | T and H: 164 | fields: 165 | temp: 166 | args: {'wh':[120, 24], 167 | 'fmt':'T(C) {0:.1f}', 'fontdef':'Arial Unicode.ttf', 168 | 'fontsize':20, 'info':[['my dht', 'temperature']]} 169 | xy0: [2, 5] 170 | humid: 171 | args: {'wh':[120, 24], 172 | 'fmt':'H(%) {0:.1f}', 'fontdef':'Arial Unicode.ttf', 173 | 'fontsize':20, 'info':[['my dht', 'humidity']]} 174 | xy0: [2, 35] 175 | particles: 176 | fields: 177 | PMbig: 178 | args: {'wh':[120, 24], 179 | 'fmt':'PM25 {0:.0f}', 'fontdef':'Arial Unicode.ttf', 180 | 'fontsize':20, 'info':[['my gthree', 'PM25']]} 181 | xy0: [2, 5] 182 | PMsmall: 183 | args: {'wh':[120, 24], 184 | 'fmt':'PM1 {0:.0f}, PM10 {0:.0f}', 'fontdef':'default', 185 | 'info':[['my gthree', 'PM1'], ['my gthree', 'PM10']]} 186 | xy0: [2, 35] 187 | 188 | 189 | And the python to run it is currently like this. In the next version, it will be completely automated 190 | 191 | boxes = reader('mybox_documentation.yaml') 192 | 193 | box = boxes[0] 194 | lass = box.LASS_devices[0] 195 | log = box.LOG_devices[0] 196 | 197 | box.get_device('my oled').initiate() 198 | box.get_device('my oled').display_on() 199 | box.get_device('my oled').show_gray() 200 | 201 | # Define your loop for operation 202 | 203 | wait = 10. # seconds 204 | 205 | # Go! 206 | while True: 207 | 208 | box.read_all() 209 | lass.build_and_send_to_LASS() 210 | # print lass.LASS_string 211 | log.build_and_save_entry() 212 | 213 | for s in oled.screens(): 214 | s.update_all() 215 | 216 | tnext = time.time() + wait 217 | 218 | screenz = cycle(box.screens) 219 | 220 | while time.time() < tnext: 221 | s = next(screenz) 222 | oled.show_screen(s) 223 | time.sleep(1) 224 | 225 | ---- 226 | 227 | Here is a quick review of python methods: 228 | 229 | BOX 230 | .clear_all_device_datadicts() 231 | .read_all_devices() 232 | .get_system_timedate_dict() 233 | .make_a_pi() 234 | .WiFi_setstatus() 235 | .get_WiFi_is_on() 236 | .WiFi_on() 237 | .WiFi_off() 238 | .get_mac_address() 239 | .print_some_system_info() 240 | .get_system_datetime() 241 | .show_CPU_temp() 242 | .get_system_datetime() 243 | .add_device(dev) 244 | 245 | # some wrappers….. 246 | .new_Dummy() 247 | .new_DHT22bb() 248 | .new_G3bb() 249 | .new_GPSbb() 250 | .new_MCP3008bbspi() 251 | .new_MOS_gas_sensor() 252 | .new_OLEDi2c() 253 | .new_LASS() 254 | .new_LOG() 255 | .get_system_datetime() 256 | 257 | LASS 258 | .set_static_location(latlon, alt) 259 | .set_sources(self, humsrc=None, tempsrc=None, pm25src=None, 260 | pm1src=None, pm10src=None, timedatesrc=None, 261 | GPSsrc=None, gassensors=None) 262 | .build_entry() 263 | .send_to_LASS() 264 | .view_LASS_entry() 265 | 266 | LOG 267 | .configure() 268 | .build_entry() 269 | .save_entry() 270 | 271 | GPIO_DEVICE 272 | .get_my_current_instance_info() 273 | .get_my_original_instance_info() 274 | Dummy 275 | .read() 276 | DHT22bb 277 | .read() 278 | .cancel() 279 | MCP3008bbSPI 280 | .digitize_one_channel() 281 | .measure_one_voltage() 282 | MOS_gas_sensor_ppm() 283 | .read() 284 | MOS_gas_sensor 285 | .read() 286 | RTCi2c 287 | .read() 288 | .set_time() 289 | G3bb 290 | .read() 291 | GPSbb 292 | .read() 293 | OLEDi2c 294 | .new_screen() 295 | .add_screen(screen object) 296 | .initiate() 297 | .display_on() 298 | .set_contrast() 299 | .show_black() 300 | .show_white() 301 | .show_gray() 302 | .show_screen(screen object) 303 | .update_all_and_show_screen(screen object) 304 | .show_array() 305 | .preview_me() 306 | SCREEN 307 | .__init__() 308 | .__repr__() 309 | .new_field() 310 | .add_field() 311 | .update_all() 312 | .preview_me() 313 | .update_all_and_preview_me() 314 | FIELD 315 | .__init__() 316 | .__repr__() 317 | .update() 318 | .preview_me() 319 | -------------------------------------------------------------------------------- /Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi-tw/PiM25/a17701f267b0a9565b6232cc904f3692ef0658cb/Roboto-Regular.ttf -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi-tw/PiM25/a17701f267b0a9565b6232cc904f3692ef0658cb/logo.png -------------------------------------------------------------------------------- /oledyaml.yaml: -------------------------------------------------------------------------------- 1 | TandH: 2 | fields: 3 | temp: 4 | args: {'wh':[120, 30], 5 | 'fmt':'{:0.1f}°C', 'fontdef':'Roboto-Regular.ttf', 6 | 'fontsize':28, 'info':[['my dht', 'temperature']]} 7 | xy0: [2, 1] 8 | humid: 9 | args: {'wh':[120, 30], 10 | 'fmt':'{:0.0f}%', 'fontdef':'Roboto-Regular.ttf', 11 | 'fontsize':28, 'info':[['my dht', 'humidity']]} 12 | xy0: [2, 33] 13 | particles: 14 | fields: 15 | PMbigtext: 16 | args: {'wh':[60, 20], 17 | 'fmt':'PM2.5', 'fontdef':'Roboto-Regular.ttf', 18 | 'fontsize':18, 'threshold':30, 'info':[['my g3', 'PM25']]} 19 | xy0: [1, 7] 20 | PMbigvalue: 21 | args: {'wh':[60, 30], 22 | 'fmt':'{:d}', 'fontdef':'Roboto-Regular.ttf', 23 | 'fontsize':24, 'threshold':30, 'info':[['my g3', 'PM25']]} 24 | xy0: [65, 1] 25 | PMonetext: 26 | args: {'wh':[60, 18], 27 | 'fmt':'PM1', 'fontdef':'Roboto-Regular.ttf', 28 | 'fontsize':14, 'info':[['my g3', 'PM1']]} 29 | xy0: [2, 27] 30 | PMonevalue: 31 | args: {'wh':[56, 18], 32 | 'fmt':'{:d}', 'fontdef':'Roboto-Regular.ttf', 33 | 'fontsize':14, 'info':[['my g3', 'PM1']]} 34 | xy0: [69, 27] 35 | PMtentext: 36 | args: {'wh':[60, 18], 37 | 'fmt':'PM10', 'fontdef':'Roboto-Regular.ttf', 38 | 'fontsize':14, 'info':[['my g3', 'PM10']]} 39 | xy0: [2, 44] 40 | PMtenvalue: 41 | args: {'wh':[56, 18], 42 | 'fmt':'{:d}', 'fontdef':'Roboto-Regular.ttf', 43 | 'fontsize':14, 'info':[['my g3', 'PM10']]} 44 | xy0: [69, 44] 45 | GPS: 46 | fields: 47 | latlabel: 48 | args: {'wh':[40, 16], 49 | 'fmt':'Lat', 'fontdef':'Roboto-Regular.ttf', 50 | 'fontsize':15, 'threshold':30, 'info':[['my gps', 'latitude']]} 51 | xy0: [1, 0] 52 | latvalue: 53 | args: {'wh':[80, 16], 54 | 'fmt':'{:0.5f}', 'fontdef':'Roboto-Regular.ttf', 55 | 'fontsize':15, 'threshold':30, 'info':[['my gps', 'latitude']]} 56 | xy0: [45, 1] 57 | lonlabel: 58 | args: {'wh':[40, 16], 59 | 'fmt':'Lon', 'fontdef':'Roboto-Regular.ttf', 60 | 'fontsize':15, 'threshold':30, 'info':[['my gps', 'latitude']]} 61 | xy0: [1, 16] 62 | lonvalue: 63 | args: {'wh':[80, 16], 64 | 'fmt':'{:0.5f}', 'fontdef':'Roboto-Regular.ttf', 65 | 'fontsize':15, 'threshold':30, 'info':[['my gps', 'longitude']]} 66 | xy0: [45, 16] 67 | time: 68 | args: {'wh':[104, 16], 69 | 'fmt':'{}', 'fontdef':'Roboto-Regular.ttf', 70 | 'fontsize':15, 'threshold':30, 'info':[['sys timedate', 'timestr']]} 71 | xy0: [20, 32] 72 | date: 73 | args: {'wh':[104, 16], 74 | 'fmt':'{}', 'fontdef':'Roboto-Regular.ttf', 75 | 'fontsize':15, 'threshold':30, 'info':[['sys timedate', 'datestr']]} 76 | xy0: [20, 48] 77 | 78 | -------------------------------------------------------------------------------- /pim25b.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypi-tw/PiM25/a17701f267b0a9565b6232cc904f3692ef0658cb/pim25b.bmp --------------------------------------------------------------------------------