def __init__(self, logger, data_type, lora): """ Schedules events for calculating averages or getting GPS position and also schedules random LoRa messages for each interval :param logger: status logger :type logger: LoggerFactory object :param data_type: "sensors" or "gps" :type data_type: str :param lora: LoRaWan object, False if lora is not enabled :type lora: LoRaWAN object """ # Arguments self.logger = logger self.data_type = data_type self.lora = lora # Attributes if self.data_type == "sensors": self.interval_s = int(float(config.get_config("interval")) * 60) if self.interval_s < 15 * 60: self.logger.warning( "Interval is less than 15 mins - real time transmission is not guaranteed" ) elif self.data_type == "gps": self.interval_s = int( float(config.get_config("GPS_period")) * 3600) self.s_to_next_lora = None self.first_alarm = None self.periodic_alarm = None self.random_alarm = None # Start scheduling events self.start_events()
def setup_new_config(logger): """ Start a WiFi AP that provides a configuration page. The device automatically reboots and applies modifications upon successful configuration. :param logger: status logger :type logger: LoggerFactory """ # Only one of this thread is allowed to run at a time if not wifi_lock.locked(): with wifi_lock: logger.info("New configuration setup started") # Config uses LED colours to indicate the state of the connection - lock is necessary to disable error pings led_lock.acquire(1) unique_id = ubinascii.hexlify(machine.unique_id()).decode() # set pycom up as access point wlan = WLAN(mode=WLAN.AP, ssid=config.get_config("device_name") + unique_id) # Connect to PmSensor using password set by the user wlan.init( mode=WLAN.AP, ssid=config.get_config("device_name") + unique_id, auth=(WLAN.WPA2, config.get_config("password")), channel=1, antenna=WLAN.INT_ANT, ) # Load HTML via entering 192,168.4.10 to browser wlan.ifconfig( id=1, config=("192.168.4.10", "255.255.255.0", "192.168.4.1", "192.168.4.1"), ) logger.info("Access point turned on as {}".format( config.get_config("device_name") + unique_id)) logger.info( "Configuration website can be accessed at 192.168.4.10") address = socket.getaddrinfo( "0.0.0.0", 80)[0][-1] # Accept stations from all addresses sct = socket.socket() # Create socket for communication sct.settimeout(int( float(config.get_config("config_timeout")) * 60)) # session times out after x seconds gc.collect( ) # frees up unused memory if there was a previous connection sct.bind(address) # Bind address to socket sct.listen(1) # Allow one station to connect to socket pycom.rgbled(0x000055) # Blue LED - waiting for connection get_new_config(sct, logger) wlan.deinit() # turn off wifi gc.collect() logger.info("rebooting...") machine.reset()
def pm_thread(sensor_name, status_logger, pins, serial_id): status_logger.debug("Thread {} started".format(sensor_name)) sensor_logger = SensorLogger(sensor_name=sensor_name, terminal_out=True) sensor_type = config.get_config(sensor_name) init_time = config.get_config(sensor_name + "_init") init_count = 0 if sensor_type == "PMS5003": # initialize sensor sensor = Plantower(pins=pins, id=serial_id) # warm up time - readings are not logged while init_count < init_time: try: time.sleep(1) sensor.read() init_count += 1 except PlantowerException as e: status_logger.exception("Failed to read from sensor PMS5003") blink_led((0x550000, 0.4, True)) elif sensor_type == "SPS030": # initialize sensor while True: try: sensor = Sensirion( retries=1, pins=pins, id=serial_id) # automatically starts measurement break except SensirionException as e: status_logger.exception("Failed to read from sensor SPS030") blink_led((0x550000, 0.4, True)) time.sleep(1) # warm up time - readings are not logged while init_count < init_time: try: time.sleep(1) sensor.read() init_count += 1 except SensirionException as e: status_logger.exception("Failed to read from sensor SPS030") blink_led((0x550000, 0.4, True)) # start a periodic timer interrupt to poll readings every second processing_alarm = Timer.Alarm(process_readings, arg=(sensor_type, sensor, sensor_logger, status_logger), s=1, periodic=True)
def get_sensor_averages(logger, lora): """ Gets averages for specific columns of defined sensor data, loads them into a line and appends it to the file which the LoRa thread sends data from. A sensor is defined if it is ticked in the configuration and has data to be sent (has a current.csv file). :type logger: LoggerFactory object (status_logger) :param is_def: Stores which sensors are defined in the form "sensor_name" : True/False :type is_def: dict """ logger.debug("Calculating averages") # get a dictionary of sensors and their status sensors = get_sensors() fmt = get_format(sensors) version = str(config.get_config("version")) timestamp = s.csv_timestamp_template.format( *time.gmtime()) # get current time in desired format minutes = str(minutes_of_the_month()) # get minutes past last midnight try: sensor_averages = {} for sensor_name in [s.TEMP, s.PM1, s.PM2]: if sensors[sensor_name]: sensor_averages.update(calculate_average(sensor_name, logger)) # Append averages to the line to be sent over LoRa according to which sensors are defined. line_to_log = '{}' + fmt + ',' + version + ',' + minutes for sensor_name in [s.TEMP, s.PM1, s.PM2]: if sensors[sensor_name]: line_to_log += ',' + str( config.get_config(sensor_name + "_id")) + ',' + ','.join( sensor_averages[sensor_name + "_avg"]) + ',' + str( sensor_averages[sensor_name + "_count"]) line_to_log += '\n' # Logs line_to_log to archive and places copies into relevant to_send folders log_averages(line_to_log.format(timestamp + ',')) if config.get_config("LORA") == "ON": year_month = timestamp[2:4] + "," + timestamp[5:7] + ',' lora.lora_buffer.write(line_to_log.format(year_month)) # If raw data was processed, saved and dumped, processing files can be deleted path = s.processing_path for sensor_name in [s.TEMP, s.PM1, s.PM2]: if sensors[sensor_name]: filename = sensor_name + '.csv' try: os.remove(path + filename) except Exception as e: logger.exception("Failed to remove " + filename + " in " + path) except Exception as e: logger.exception("Failed to flash averages") blink_led((0x550000, 0.4, True))
def get_sensor_averages(logger, lora): """ Takes the averages of sensor readings and constructs a line to log to the SD card, terminal and lora buffer :param logger: status logger :type logger: LoggerFactory object :param lora: LoRaWAN object, False if lora is not enabled :type lora: LoRaWAN object """ logger.debug("Calculating averages") # get a dictionary of sensors and their status sensors = get_sensors() fmt = get_format(sensors) version = str(config.get_config("fmt_version")) timestamp = s.csv_timestamp_template.format( *time.gmtime()) # get current time in desired format minutes = str(minutes_of_the_month()) # get minutes past last midnight try: sensor_averages = {} for sensor_name in [s.TEMP, s.PM1, s.PM2]: if sensors[sensor_name]: sensor_averages.update(calculate_average(sensor_name, logger)) # Append averages to the line to be sent over LoRa according to which sensors are defined. line_to_log = '{}' + fmt + ',' + version + ',' + minutes for sensor_name in [s.TEMP, s.PM1, s.PM2]: if sensors[sensor_name]: line_to_log += ',' + str( config.get_config(sensor_name + "_id")) + ',' + ','.join( sensor_averages[sensor_name + "_avg"]) + ',' + str( sensor_averages[sensor_name + "_count"]) line_to_log += '\n' # Logs line_to_log to archive and places copies into relevant to_send folders log_averages(line_to_log.format(timestamp + ',')) if lora is not False: year_month = timestamp[2:4] + "," + timestamp[5:7] + ',' lora.lora_buffer.write(line_to_log.format(year_month)) # If raw data was processed, saved and dumped, processing files can be deleted path = s.processing_path for sensor_name in [s.TEMP, s.PM1, s.PM2]: if sensors[sensor_name]: filename = sensor_name + '.csv' try: os.remove(path + filename) except Exception as e: pass except Exception as e: logger.exception("Failed to flash averages") blink_led((0x550000, 0.4, True))
def get_random_time(): """ Get random number of seconds within interval :return: s_to_next_lora :rtype: int """ # get random number of seconds within (interval - lora_timeout) and add one so it cannot be zero s_to_next_lora = int((machine.rng() / (2**24)) * (int(float(config.get_config("interval")) * 60) - int(config.get_config("lora_timeout")))) + 1 return s_to_next_lora
def get_random_time(self): # get random number of seconds within interval, add one so it cannot be zero s_to_next_lora = int( machine.rng() / (2**24) * int(float(config.get_config("interval")) * 60)) + 1 return s_to_next_lora
def get_time(rtc, logger): if gps_lock.locked(): logger.debug("Waiting for other gps thread to finish") with gps_lock: logger.info("Getting UTC datetime via GPS") serial, chrono, indicator_led = gps_init(logger) # initialize serial and timer com_counter = int(chrono.read()) # counter for checking whether gps is connected message = False # no message while terminal is disabled (by default) while True: # data_in = '$GPRMC,085258.000,A,5056.1384,N,00123.1522,W,0.00,159.12,200819,,,A*7E\r\n' data_in = (str(serial.readline()))[1:] if (int(chrono.read()) - com_counter) >= 10: gps_deinit(serial, logger, message, indicator_led) logger.error("GPS enabled, but not connected") return False if data_in[1:4] != "$GP": time.sleep(1) else: for char in data_in: sentence = gps.update(char) if sentence == "GPRMC": com_counter = int(chrono.read()) if gps.valid: # Set current time on pycom - convert seconds (timestamp[2]) from float to int datetime = (int('20' + str(gps.date[2])), gps.date[1], gps.date[0], gps.timestamp[0], gps.timestamp[1], int(gps.timestamp[2]), 0, 0) rtc.init(datetime) # Set current time on RTC module if connected - convert seconds (timestamp[2]) from float to int h_day, h_mnth, h_yr = int(str(gps.date[0]), 16), int(str(gps.date[1]), 16), int(str(gps.date[2]), 16) h_hr, h_min, h_sec = int(str(gps.timestamp[0]), 16), int(str(gps.timestamp[1]), 16), int( str(int(gps.timestamp[2])), 16) try: clock.set_time(h_yr, h_mnth, h_day, h_hr, h_min, h_sec) message = """GPS UTC datetime successfully updated on pycom board GPS UTC datetime successfully updated on RTC module""" except Exception: message = """GPS UTC datetime successfully updated on pycom board Failed to set GPS UTC datetime on the RTC module""" gps_deinit(serial, logger, message, indicator_led) return True # If timeout elapsed exit function or thread if chrono.read() >= int(config.get_config("GPS_timeout")): gps_deinit(serial, logger, message, indicator_led) logger.error("""GPS timeout Check if GPS module is connected Place device under clear sky Increase GPS timeout in configurations""") return False
def periodic_event(self, arg): if self.data_type == "gps": # Get position from gps to be sent over LoRA _thread.start_new_thread(GpsSIM28.get_position, (self.logger, self.lora)) elif self.data_type == "sensors": # Put new averages in lora buffer and also print to terminal. get_sensor_averages(logger=self.logger, sensor_loggers=self.sensor_loggers, lora=self.lora) if self.lora is not False: # Schedule LoRa messages if LoRa is enabled # if device was last transmitting a day or more ago, reset message_count for the day today = time.gmtime() date = str(today[0]) + str(today[1]) + str(today[2]) if self.lora.transmission_date != date: self.lora.message_count = 0 self.lora.transmission_date = date config.save_config({ "message_count": self.lora.message_count, "transmission_date": date }) # send 2, 3 or at most 4 messages per interval based on length of interval lora_slot = int( float(config.get_config("interval")) * 60) // 30 # lora_rate changes for each 30 seconds max_lora_slot = self.lora.message_limit // 96 # max number of msg per interval optimized around 15 min if max_lora_slot < 2: max_lora_slot = 2 if lora_slot < 2: raise Exception("Interval has to be at least a minute") else: lora_rate = lora_slot if lora_slot > max_lora_slot: lora_rate = max_lora_slot waiting = self.lora.lora_buffer.size( lora_rate ) # check number of messages in stack (up to max to send) remaining = self.lora.message_limit - self.lora.message_count # check how many more we can send today if remaining <= 0: self.logger.info("LoRa message limit reached for the day") if remaining - waiting >= 0: count = waiting # if we have more than we want to send, send them all else: count = remaining # if we have less than we want to send, send up to the limit for val in range( count ): # Schedule up to 4 randomly timed messages within interval self.random_alarm = Timer.Alarm(self.random_event, s=get_random_time(), periodic=False) else: raise Exception("Non existent data type")
def get_sensors(): sensors = {s.TEMP: False, s.PM1: False, s.PM2: False} for sensor in sensors: if config.get_config(sensor) != "OFF": sensors[sensor] = True return sensors
def software_update(logger): with wifi_lock: try: logger.info("Over the Air update started") led_lock.acquire(1) # disable all other indicator LEDs pycom.rgbled(0x555500) # Yellow LED # Get credentials from configuration ssid = config.get_config("SSID") password = config.get_config("wifi_password") server_ip = config.get_config("server") port = int(config.get_config("port")) print(ssid, password, server_ip, port) # Perform OTA update ota = WiFiOTA(ssid, password, server_ip, port) # Turn off WiFi to save power w = WLAN() w.deinit() ota.connect() ota.update() time.sleep(5) except Exception as e: logger.exception("Failed to update the device") pycom.rgbled(0x550000) time.sleep(3) finally: # Turn off update mode config.save_configuration({"update": False}) pycom.rgbled(0x000000) led_lock.release() # Reboot the device to apply patches machine.reset()
def get_logging_level(): logging_lvl = config.get_config("logging_lvl") if logging_lvl == "Critical": return CRITICAL elif logging_lvl == "Error": return ERROR elif logging_lvl == "Warning": return WARNING elif logging_lvl == "Info": return INFO elif logging_lvl == "Debug": return DEBUG
def get_sensors(): """ Dictionary of sensors (TEMP, PM1, PM2) and whether they are enabled in the configurations :return: sensors :rtype: dict """ sensors = {s.TEMP: False, s.PM1: False, s.PM2: False} for sensor in sensors: if config.get_config(sensor) != "OFF": sensors[sensor] = True return sensors
def __init__(self, logger, data_type, lora): # Arguments self.logger = logger self.data_type = data_type self.lora = lora # Attributes if self.data_type == "sensors": self.interval_s = int(float(config.get_config("interval")) * 60) if self.interval_s < 15 * 60: self.logger.warning( "Interval is less than 15 mins - real time transmission is not guaranteed" ) elif self.data_type == "gps": self.interval_s = int(float(config.get_config("GPS_freq")) * 3600) self.s_to_next_lora = None self.first_alarm = None self.periodic_alarm = None self.random_alarm = None # Start scheduling events self.start_events()
def __init__(self, logger, data_type, lora, sensor_loggers=[]): """ Schedules events for calculating averages or getting GPS position and also schedules random LoRa messages for each interval :param logger: status logger :type logger: LoggerFactory object :param data_type: "sensors" or "gps" :type data_type: str :param lora: LoRaWan object, False if lora is not enabled :type lora: LoRaWAN object :param sensor_loggers: A list of sensor loggers if data_type is "sensors" :type sensor_loggers: list """ self.logger = logger self.sensor_loggers = sensor_loggers self.data_type = data_type self.lora = lora if self.data_type == "sensors": self.interval_s = int(float(config.get_config("interval")) * 60) if self.interval_s < 15 * 60: self.logger.warning( "Interval is less than 15 mins - real time transmission is not guaranteed" ) elif self.data_type == "gps": self.interval_s = int( float(config.get_config("GPS_period")) * 3600) self.s_to_next_lora = None self.periodic_alarm = None self.random_alarm = None # Start scheduling events - cCalculate time (s) until the first event, and set up an alarm first_event_s = seconds_to_first_event(self.interval_s) self.first_alarm = Timer.Alarm(self.first_event, s=first_event_s, periodic=False)
def __init__(self, sensor_logger, status_logger): self.sensor_logger = sensor_logger self.status_logger = status_logger # Initialise i2c - bus no., type, baudrate, i2c pins self.i2c = I2C(0, I2C.MASTER, baudrate=9600, pins=('P9', 'P10')) self.address = 0x45 # get one sensor reading upon init to catch any errors and calibrate the sensor self.read() # start a periodic timer interrupt to poll readings at a frequency self.processing_alarm = Timer.Alarm( self.process_readings, s=int(config.get_config("TEMP_period")), periodic=True)
def get_logging_level(): """ Get logging level from configurations :return: logging level :rtype: str """ logging_lvl = config.get_config("logging_lvl") if logging_lvl == "Critical": return CRITICAL elif logging_lvl == "Error": return ERROR elif logging_lvl == "Warning": return WARNING elif logging_lvl == "Info": return INFO elif logging_lvl == "Debug": return DEBUG
def __init__(self, sensor_name, terminal_out=True, terminator='\n'): """ :param sensor_name: sensor name :type sensor_name: str :param terminal_out: print output to terminal :type terminal_out: bool :param terminator: end of line character :type terminator: object """ self.terminal_out = terminal_out self.terminator = terminator self.sensor_name = sensor_name self.sensor_type = config.get_config(sensor_name) # Average data that we return if there isn't enough readings to create one self.zeroed_data = list('0' * len(s.lora_sensor_headers[self.sensor_type])) # We use a lock since log_row and get_averages will be called from diff threads self.average_list_lock = _thread.allocate_lock() self.average_list = [] self.init_averages()
def calculate_average(sensor_name, logger): """ Calculates averages for specific columns of sensor data to be sent over LoRa. Sets placeholders if it fails. :param sensor_name: PM1, PM2 or TEMP :type sensor_name: str :param logger: status logger :type logger: LoggerFactory object """ filename = sensor_name + '.csv' sensor_type = config.get_config(sensor_name) sensor_id = str(config.get_config(sensor_name + "_id")) # headers in current file of the sensor according to its type headers = s.headers_dict_v4[sensor_type] # data to send if no readings are available avg_readings_str = list('0' * len(s.lora_sensor_headers[sensor_type])) count = 0 try: with current_lock: # Move sensor_name.csv from current dir to processing dir os.rename(s.current_path + filename, s.processing_path + filename) with open(s.processing_path + filename, 'r') as f: # read all lines from processing lines = f.readlines() lines_lst = [ ] # list of lists that store average sensor readings from specific columns for line in lines: stripped_line = line[:-1] # strip \n stripped_line_lst = str(stripped_line).split( ',') # split string to list at comas named_line = dict(zip( headers, stripped_line_lst)) # assign each value to its header sensor_reading = [] for header in s.lora_sensor_headers[sensor_type]: sensor_reading.append(int(named_line[header])) # Append extra lines here for more readings - update version number and back-end to interpret data lines_lst.append(sensor_reading) count += 1 # Compute averages from sensor_name.csv.processing avg_readings_str = [ str(int(i)) for i in mean_across_arrays(lines_lst) ] # Append content of sensor_name.csv.processing into sensor_name.csv with open( s.archive_path + sensor_name + '_' + sensor_id + '.csv', 'a') as f: for line in lines: f.write(line) except Exception as e: logger.error("No readings from sensor {}".format(sensor_name)) logger.warning("Setting 0 as a place holder") blink_led((0x550000, 0.4, True)) finally: return { sensor_name + "_avg": avg_readings_str, sensor_name + "_count": count }
def get_html_form(): """ Constructs a webpage that includes a form to fill in by the user to acquire new configurations for the device :return: html_form :rtype: str """ if config.get_config("LORA") == "ON": lora_check = " checked" else: lora_check = "" selected_region = { "Europe": "", "Asia": "", "Australia": "", "United States": "" } for option in selected_region: if option == config.get_config("region"): selected_region[option] = " selected" selected_TEMP = {"SHT35": "", "OFF": ""} for option in selected_TEMP: if option == config.get_config(s.TEMP): selected_TEMP[option] = " selected" selected_PM1 = {"PMS5003": "", "SPS030": "", "OFF": ""} for option in selected_PM1: if option == config.get_config(s.PM1): selected_PM1[option] = " selected" selected_PM2 = {"PMS5003": "", "SPS030": "", "OFF": ""} for option in selected_PM2: if option == config.get_config(s.PM2): selected_PM2[option] = " selected" selected_GPS = {"SIM28": "", "OFF": ""} for option in selected_GPS: if option == config.get_config("GPS"): selected_GPS[option] = " selected" selected_logging = { "Critical": "", "Error": "", "Warning": "", "Info": "", "Debug": "" } for level in selected_logging: if level == config.get_config("logging_lvl"): selected_logging[level] = " selected" hardware_test_on = "selected" if config.get_config( "hardware_test").lower() == "yes" else "" hardware_test_off = "selected" if hardware_test_on == "" else "" with open("/flash/lib/setup/form.html", "r") as html_file: html = html_file.read() # This is quite cumbersome but allows for easy insertion into the html. # If you want to add another variable into the HTML form, all you have to do is # refer to it like {this}, and then in the dict below add a line like this: # "this": variable_you_want_to_insert, # Take note that in the html you surround it with {} but you dont in the dict. replacements = { "unique_id": str(config.get_config("device_id")), "device_name": str(config.get_config("device_name")), "password": str(config.get_config("password")), "config_timeout": str(config.get_config("config_timeout")), "device_eui": str(config.get_config("device_eui")), "application_eui": str(config.get_config("application_eui")), "app_key": str(config.get_config("app_key")), "fair_access": str(config.get_config("fair_access")), "air_time": str(config.get_config("air_time")), "SSID": str(config.get_config("SSID")), "wifi_password": str(config.get_config("wifi_password")), "TEMP_id": str(config.get_config("TEMP_id")), "TEMP_period": str(config.get_config("TEMP_period")), "PM1_id": str(config.get_config("PM1_id")), "PM1_init": str(config.get_config("PM1_init")), "interval": str(config.get_config("interval")), "PM2_id": str(config.get_config("PM2_id")), "PM2_init": str(config.get_config("PM2_init")), "GPS_id": str(config.get_config("GPS_id")), "GPS_timeout": str(config.get_config("GPS_timeout")), "GPS_period": str(config.get_config("GPS_period")), "lora_check": lora_check, "eu_selected": str(selected_region["Europe"]), "as_selected": str(selected_region["Asia"]), "au_selected": str(selected_region["Australia"]), "us_selected": str(selected_region["United States"]), "temp_selected_SHT35": str(selected_TEMP["SHT35"]), "temp_selected_off": str(selected_TEMP["OFF"]), "PM1_PMS5003_selected": str(selected_PM1["PMS5003"]), "PM1_SPS030_selected": str(selected_PM1["SPS030"]), "PM1_selected_off": str(selected_PM1["OFF"]), "PM2_PMS5003_selected": str(selected_PM2["PMS5003"]), "PM2_SPS030_selected": str(selected_PM2["SPS030"]), "PM2_selected_off": str(selected_PM2["OFF"]), "gps_selected_SIM28": str(selected_GPS["SIM28"]), "gps_selected_off": str(selected_GPS["OFF"]), "logging_level_selected_critical": str(selected_logging["Critical"]), "logging_level_selected_error": str(selected_logging["Error"]), "logging_level_selected_warning": str(selected_logging["Warning"]), "logging_level_selected_info": str(selected_logging["Info"]), "logging_level_selected_debug": str(selected_logging["Debug"]), "hardware_test_on": hardware_test_on, "hardware_test_off": hardware_test_off, } for where, to_copy in replacements.items(): html = html.replace("{" + where + "}", to_copy) return html
def get_position(logger, lora): if gps_lock.locked(): logger.debug("Waiting for other gps thread to finish") with gps_lock: logger.info("Getting position via GPS") serial, chrono, indicator_led = gps_init(logger) com_counter = int(chrono.read()) # counter for checking whether gps is connected message = False while True: # data_in = '$GPGGA,085259.000,5056.1384,N,00123.1522,W,1,8,1.17,25.1,M,47.6,M,,*7D\r\n' data_in = (str(serial.readline()))[1:] if (int(chrono.read()) - com_counter) >= 10: gps_deinit(serial, logger, message, indicator_led) logger.error("GPS enabled, but not connected") return False if data_in[1:4] != "$GP": time.sleep(1) else: for char in data_in: sentence = gps.update(char) if sentence == "GPGGA": com_counter = int(chrono.read()) # set aim for the quality of the signal based on the time elapsed elapsed = chrono.read() / int(config.get_config("GPS_timeout")) hdop_aim = [1, 1.2, 1.5, 1.8, 2, 2.5, 3, 4, 5, 6, 7] time_limit = [0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9] for index in range(len(time_limit)): if elapsed < time_limit[index]: break # Process data only if quality of signal is great if 0 < gps.hdop <= hdop_aim[index] and gps.satellites_in_use >= 3: latitude = gps.latitude[0] + gps.latitude[1]/60 if gps.latitude[2] == 'S': latitude = -latitude longitude = gps.longitude[0] + gps.longitude[1]/60 if gps.longitude[2] == 'W': longitude = -longitude message = """Successfully acquired location from GPS Satellites used: {} HDOP: {} Latitude: {} Longitude: {} Altitude: {}""".format(gps.satellites_in_use, gps.hdop, latitude, longitude, gps.altitude) # Process GPS location timestamp = s.csv_timestamp_template.format(*time.gmtime()) # get current time in desired format lst_to_log = [timestamp, latitude, longitude, gps.altitude] str_lst_to_log = list(map(str, lst_to_log)) # cast to string line_to_log = ','.join(str_lst_to_log) + '\n' # Print to terminal and log to archive sys.stdout.write(s.GPS + " - " + line_to_log) with open(s.archive_path + s.GPS + '.csv', 'a') as f_archive: f_archive.write(line_to_log) if config.get_config("LORA") == "ON": # get year and month from timestamp year_month = timestamp[2:4] + "," + timestamp[5:7] + ',' # get minutes since start of the month minutes = str(minutes_of_the_month()) # Construct LoRa message line_to_log = year_month + 'G,' + str(config.get_config("version")) + ',' + minutes + ',' \ + str(config.get_config("GPS_id")) + ',' + ','.join(str_lst_to_log[1:]) + '\n' # Logs line_to_log to be sent over lora lora.lora_buffer.write(line_to_log) gps_deinit(serial, logger, message, indicator_led) return True # If timeout elapsed exit function or thread if chrono.read() >= int(config.get_config("GPS_timeout")): gps_deinit(serial, logger, message, indicator_led) logger.error("""GPS timeout Check if GPS module is connected Place device under clear sky Increase GPS timeout in configurations""") return False
def __init__(self, logger): """ Connects to LoRaWAN using OTAA and sets up a ring buffer for storing messages. :param logger: status logger :type logger: LoggerFactory object """ self.logger = logger self.message_limit = int( float(config.get_config("fair_access")) / (float(config.get_config("air_time")) / 1000)) self.transmission_date = config.get_config( "transmission_date") # last date when lora was transmitting today = time.gmtime() date = str(today[0]) + str(today[1]) + str(today[2]) if self.transmission_date == date: # if device was last transmitting today self.message_count = config.get_config( "message_count") # get number of messages sent today else: self.message_count = 0 # if device was last transmitting a day or more ago, reset message_count for the day self.transmission_date = date config.save_config({ "message_count": self.message_count, "transmission_date": date }) regions = { "Europe": LoRa.EU868, "Asia": LoRa.AS923, "Australia": LoRa.AU915, "United States": LoRa.US915 } region = regions[config.get_config("region")] self.lora = LoRa(mode=LoRa.LORAWAN, region=region, adr=True) # create an OTAA authentication parameters app_eui = ubinascii.unhexlify(config.get_config("application_eui")) app_key = ubinascii.unhexlify(config.get_config("app_key")) # join a network using OTAA (Over the Air Activation) self.lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0) # create a LoRa socket self.lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # request acknowledgment of data sent # self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, True) # do not request acknowledgment of data sent self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, False) # sets timeout for sending data self.lora_socket.settimeout( int(config.get_config("lora_timeout")) * 1000) # set up callback for receiving downlink messages self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=self.lora_recv) # initialises circular lora stack to back up data up to about 22.5 days depending on the length of the month self.lora_buffer = RingBuffer(self.logger, s.processing_path, s.lora_file_name, 31 * self.message_limit, 100) try: # this fails if the buffer is empty self.check_date() # remove messages that are over a month old except Exception as e: pass
def software_update(logger): """ Connects to the wlan and fetches updates from a server. After having applied the patches successfully, it reboots the device. :param logger: status logger :type logger: LoggerFactory object """ with wifi_lock: try: logger.info("Over the Air update started") led_lock.acquire(1) # disable all other indicator LEDs pycom.rgbled(0x555500) # Yellow LED # Get credentials from configuration ssid = config.get_config("SSID") password = config.get_config("wifi_password") server_ip = config.get_config("server") port = int(config.get_config("port")) logger.info("SSID: " + str(ssid)) logger.info("server_ip: " + str(server_ip)) logger.info("port: " + str(port)) version = config.get_config("code_version") # Perform OTA update ota = WiFiOTA(logger, ssid, password, server_ip, port, version) # Turn off WiFi to save power w = WLAN() w.deinit() logger.info("connecting...") ota.connect() if ota.update(): new_version = str( ota.get_current_version()) # get updated version config.save_config({"code_version": str(new_version) }) # save new version to config on SD logger.info( "Successfully updated the device from version {} to version {}" .format(version, new_version)) except Exception as e: logger.exception("Failed to update the device") pycom.rgbled(0x550000) # turn LED to RED time.sleep(3) finally: # Turn off update mode config.save_config({"update": False}) # Turn off indicator LED pycom.rgbled(0x000000) led_lock.release() # Reboot the device to apply patches logger.info("rebooting...") machine.reset()
def get_html_form(): """ Constructs a webpage that includes a form to fill in by the user to acquire new configurations for the device :return: html_form :rtype: str """ if config.get_config("LORA") == "ON": lora_check = " checked" else: lora_check = "" selected_region = { "Europe": "", "Asia": "", "Australia": "", "United States": "" } for option in selected_region: if option == config.get_config("region"): selected_region[option] = " selected" selected_TEMP = {"SHT35": "", "OFF": ""} for option in selected_TEMP: if option == config.get_config(s.TEMP): selected_TEMP[option] = " selected" selected_PM1 = {"PMS5003": "", "SPS030": "", "OFF": ""} for option in selected_PM1: if option == config.get_config(s.PM1): selected_PM1[option] = " selected" selected_PM2 = {"PMS5003": "", "SPS030": "", "OFF": ""} for option in selected_PM2: if option == config.get_config(s.PM2): selected_PM2[option] = " selected" selected_GPS = {"SIM28": "", "OFF": ""} for option in selected_GPS: if option == config.get_config("GPS"): selected_GPS[option] = " selected" selected_logging = { "Critical": "", "Error": "", "Warning": "", "Info": "", "Debug": "" } for level in selected_logging: if level == config.get_config("logging_lvl"): selected_logging[level] = " selected" html_form = '''<!DOCTYPE html> <html> <head> <title>PyonAir Configuration</title> <style> body{ background-color: #252525; margin-left: 25px; margin-top: 30px; margin-bottom: 45px; font-family: helvetica; color: white; font-size: 15px; } h1{ margin-bottom: 0px; max-height: 999999px; //disables font boost in android } p{ font-size: 20px; margin-left: 5px; margin-bottom: 0px; margin-top: 2em; max-height: 999999px; //disables font boost in android } hr { display: block; height: 1px; width: 270px; border: 0; border-top: 1px solid #909090; margin-top: 0.1em; margin-left: -5px; } .p_line{ padding-bottom: 0.8em; } form { margin-left: 5px; } label { display: block; margin-top: 1em; max-height: 999999px; //disables font boost in android } .settings{ margin-left: 30px; } .lora_grid1{ display: grid; grid-template-columns: 120px 100px; } .lora_grid2{ display: grid; grid-template-columns: 95px 100px; } .pm_sensors{ margin-left: 15px; display: grid; grid-template-columns: 390px 110px; } .pm_sensors .pm_sensor_label{ margin-top: 1em; } .pm_sensors hr{ width: 360px; } .input_text{ margin-top: 5px; box-sizing: border-box; height: 1.8em; padding: 2px 4px; width: 200px; } .input_checkbox{ margin-top: 0.6em; margin-left: 5px; transform: scale(1.6); } select{ margin-top: 5px; width: 110px; height: 1.8em; } .sensor{ display: grid; grid-template-columns: 150px 110px 110px 110px; margin-left: 10px; margin-top: -0.5em; margin-bottom: 1.8em; } .input_number{ margin-top: 5px; box-sizing: border-box; height: 1.8em; padding: 2px 4px; width: 70px; } .pm_sensors .sensor{ grid-template-columns: 150px 110px 110px; margin-left: -5px; margin-bottom: 0; margin-top: -1.3em; } .grid_item3{ grid-column-start: 2; grid-column-end: 3; grid-row-start: 1; grid-row-end: 4; margin-top: 50%; margin-left: -5px; } .sensor_settings{ width: 530px; } .interval_hr{ width: 380px; } .gps_hr{ width: 480px; } </style> </head> <body> <form class="config_form"> <h1>PyonAir Configuration</h1> <p>General Settings</p> <hr class="p_line"/> <div class="settings"> <label>Unique ID: ''' + str( config.get_config("device_id")) + '''</label> <label for="device_name">Device Name</label> <input class="input_text" id="device_name" name="device_name" type="text" value="''' + str( config.get_config( "device_name")) + '''" required="required" maxlength="32"/> <label for="password">New Password</label> <input class="input_text" id="password" name="password" type="password" value="''' + str( config.get_config("password") ) + '''" required="required" maxlength="32"/> <label for="config_timeout">Config Timeout (m)</label> <input class="input_number" id="config_timeout" name="config_timeout" type="number" value="''' + str( config.get_config("config_timeout") ) + '''" required="required" min="3" max="120" step="0.01"/> </div> <p>LoRaWAN Configuration</p> <hr class="p_line"/> <div class="settings"> <label>Device EUI: ''' + str( config.get_config("device_eui")) + '''</label> <label for="application_eui">Application EUI</label> <input class="input_text" id="application_eui" name="application_eui" type="text" value="''' + str( config.get_config("application_eui") ) + '''" required="required" maxlength="16"/> <label for="app_key">App Key </label> <input class="input_text" id="app_key" name="app_key" type="password" value="''' + str( config.get_config("app_key") ) + '''" required="required" maxlength="32"/> <div class = "lora_grid1"> <div> <label for="fair_access">Fair Access (s)</label> <input class="input_number" id="fair_access" name="fair_access" type="number" value="''' + str( config.get_config("fair_access") ) + '''" required="required" min="0" max="65535"/> </div> <div> <label for="air_time">Air Time (ms)</label> <input class="input_number" id="air_time" name="air_time" type="number" value="''' + str( config.get_config("air_time") ) + '''" required="required" min="0" max="5000"/> </div> </div> <div class = "lora_grid2"> <div> <label for="LORA">On?</label> <input class="input_checkbox" type="checkbox" name="LORA" value="ON"''' + lora_check + '''> </div> <div> <label for="region">Region</label> <select name="region"> <option''' + str( selected_region["Europe"]) + '''>Europe</option> <option''' + str(selected_region["Asia"]) + '''>Asia</option> <option''' + str(selected_region["Australia"] ) + '''>Australia</option> <option''' + str(selected_region["United States"] ) + '''>United States</option> </select> </div> </div> </div> <p>WiFi Configuration</p> <hr class="p_line"/> <div class="settings"> <label for="SSID">SSID</label> <input class="input_text" id="SSID" name="SSID" type="text" value="''' + str( config.get_config("SSID") ) + '''" required="required" maxlength="32"/> <label for="wifi_password">Password</label> <input class="input_text" id="wifi_password" name="wifi_password" type="password" value="''' + str( config.get_config("wifi_password") ) + '''" required="required" maxlength="128"/> </div> <p>Sensor Settings</p> <hr class="p_line sensor_settings"/> <div class="settings"> <label>Temperature and Humidity Sensor</label> <hr class="interval_hr"/> <div class="sensor"> <div> <label for="TEMP">Sensor Type</label> <select name="TEMP"> <option''' + str(selected_TEMP["SHT35"]) + '''>SHT35</option> <option''' + str(selected_TEMP["OFF"]) + '''>OFF</option> </select> </div> <div> <label for="TEMP_id">Sensor ID</label> <input class="input_number" id="TEMP_id" name="TEMP_id" type="number" value="''' + str( config.get_config("TEMP_id") ) + '''" required="required" min="0" max="65535"/> </div> <div> <label for="TEMP_period">Period (s)</label> <input class="input_number" id="TEMP_period" name="TEMP_freq" type="number" value="''' + str( config.get_config("TEMP_period") ) + '''" required="required" min="1" max="120"/> </div> </div> <label>Particulate Matter Sensors</label> <hr class="interval_hr"/> <div class="pm_sensors"> <div class="grid_item1"> <label class="pm_sensor_label">PM Sensor 1</label> <hr/> </div> <div class="sensor grid_item2"> <div> <label for="PM1">Sensor Type</label> <select name="PM1"> <option''' + str(selected_PM1["PMS5003"] ) + '''>PMS5003</option> <option''' + str(selected_PM1["SPS030"] ) + '''>SPS030</option> <option''' + str(selected_PM1["OFF"]) + '''>OFF</option> </select> </div> <div> <label for="PM1_id">Sensor ID</label> <input class="input_number" id="PM1_id" name="PM1_id" type="number" value="''' + str( config.get_config("PM1_id") ) + '''" required="required" min="0" max="65535"/> </div> <div> <label for="PM1_init">Setup Time (s)</label> <input class="input_number" id="PM1_init" name="PM1_init" type="number" value="''' + str( config.get_config("PM1_init") ) + '''" required="required" min="1" max="3600" step="0.01"/> </div> </div> <div class="grid_item3"> <label for="interval">Interval (m)</label> <input class="input_number" id="PM_interval" name="interval" type="number" value="''' + str( config.get_config("interval") ) + '''" required="required" min="1" max="120" step="0.01"/> </div> <div class="grid_item4"> <label class="pm_sensor_label">PM Sensor 2</label> <hr/> </div> <div class="sensor grid_item5"> <div> <label for="PM2">Sensor Type</label> <select name="PM2"> <option''' + str(selected_PM2["PMS5003"] ) + '''>PMS5003</option> <option''' + str(selected_PM2["SPS030"] ) + '''>SPS030</option> <option''' + str(selected_PM2["OFF"]) + '''>OFF</option> </select> </div> <div> <label for="PM2_id">Sensor ID</label> <input class="input_number" id="PM2_id" name="PM2_id" type="number" value="''' + str( config.get_config("PM2_id") ) + '''" required="required" min="0" max="65535"/> </div> <div> <label for="PM2_init">Setup Time (s)</label> <input class="input_number" id="PM2_init" name="PM2_init" type="number" value="''' + str( config.get_config("PM2_init") ) + '''" required="required" min="1" max="3600" step="0.01"/> </div> </div> </div> <label>GPS</label> <hr class="gps_hr"/> <div class="sensor"> <div> <label for="GPS">Sensor Type</label> <select name="GPS"> <option''' + str(selected_GPS["SIM28"]) + '''>SIM28</option> <option''' + str(selected_GPS["OFF"]) + '''>OFF</option> </select> </div> <div> <label for="GPS_id">Sensor ID</label> <input class="input_number" id="GPS_id" name="GPS_id" type="number" value="''' + str( config.get_config("GPS_id") ) + '''" required="required" min="0" max="65535"/> </div> <div> <label for="GPS_timeout">Timeout (m)</label> <input class="input_number" id="GPS_timeout" name="GPS_timeout" type="number" value="''' + str( config.get_config("GPS_timeout") ) + '''" required="required" min="5" max="120" step="0.01"/> </div> <div> <label for="GPS_period">Period (h)</label> <input class="input_number" id="GPS_period" name="GPS_period" type="number" value="''' + str( config.get_config("GPS_period") ) + '''" required="required" min="0.1" max="8760" step="0.01"/> </div> </div> </div> <hr class="p_line sensor_settings"/> <label for="logging_lvl">Select Logging Level</label> <select id="logging_lvl" name="logging_lvl"> <option''' + str(selected_logging["Critical"] ) + '''>Critical</option> <option''' + str(selected_logging["Error"]) + '''>Error</option> <option''' + str(selected_logging["Warning"] ) + '''>Warning</option> <option''' + str(selected_logging["Info"]) + '''>Info</option> <option''' + str(selected_logging["Debug"]) + '''>Debug</option> </select> <br><br> <button type="submit">Save</button> </form> </body> <script> //Source: https://lengstorf.com/get-form-values-as-json/ const isValidElement = element => { return element.name && element.value; }; const isValidValue = element => { return (!['checkbox'].includes(element.type) || element.checked); }; const formToJSON = elements => [].reduce.call(elements, (data, element) => { // Make sure the element has the required properties and should be added. if (isValidElement(element) && isValidValue(element)) { data[element.name] = element.value; } return data; }, {}); const handleFormSubmit = event => { // Stop the form from submitting since we’re handling that with AJAX. event.preventDefault(); // Call our function to get the form data. const data = formToJSON(form.elements); // Use `JSON.stringify()` to make the output valid, human-readable JSON. var json_data = "json_str_begin" + JSON.stringify(data, null, "") + "json_str_end"; json_data.replace(/\\n/g, ''); var date = new Date(); var now = 'time_begin'+date.getUTCFullYear()+':'+(date.getUTCMonth()+1)+':'+date.getUTCDate()+':'+date.getUTCHours()+":"+date.getUTCMinutes()+":"+date.getUTCSeconds()+'time_end'; // ...this is where we’d actually do something with the form data... var xhttp = new XMLHttpRequest(); xhttp.open("POST", "", true); xhttp.send(json_data+now); }; const form = document.getElementsByClassName('config_form')[0]; form.addEventListener('submit', handleFormSubmit); </script> </html>''' return html_form
def start_timer(self): # start a periodic timer interrupt to poll readings at a frequency self.processing_alarm = Timer.Alarm( self.process_readings, s=int(config.get_config("TEMP_period")), periodic=True)
def pm_thread(sensor_name, sensor_logger, status_logger, pins, serial_id): """ Method to run as a thread that reads, processes and logs readings form pm sensors according to their type :param sensor_name: PM1 or PM2 :type sensor_name: str :param status_logger: status logger :type status_logger: LoggerFactory object :param pins: serial bus pins (TX, RX) :type pins: tuple(int, int) :param serial_id: serial bus id (0, 1 or 2) :type serial_id: int """ status_logger.debug("Thread {} started".format(sensor_name)) sensor_type = config.get_config(sensor_name) # sensor_logger = SensorLogger(sensor_name=sensor_name, terminal_out=True) init_time = int(config.get_config(sensor_name + "_init")) init_count = 0 if sensor_type == "PMS5003": # initialise sensor sensor = Plantower(pins=pins, id=serial_id) time.sleep(1) # warm up time - readings are not logged while init_count < init_time: try: time.sleep(1) sensor.read() init_count += 1 except PlantowerException as e: status_logger.exception("Failed to read from sensor PMS5003") blink_led((0x550000, 0.4, True)) elif sensor_type == "SPS030": # initialise sensor try: sensor = Sensirion( retries=1, pins=pins, id=serial_id) # automatically starts measurement except SensirionException as e: status_logger.exception("Failed to read from sensor SPS030") blink_led((0x550000, 0.4, True)) time.sleep(1) # warm up time - readings are not logged while init_count < init_time: try: time.sleep(1) sensor.read() init_count += 1 except SensirionException as e: status_logger.exception("Failed to read from sensor SPS030") blink_led((0x550000, 0.4, True)) # start a periodic timer interrupt to poll readings every second processing_alarm = Timer.Alarm(process_readings, arg=(sensor_type, sensor, sensor_logger, status_logger), s=1, periodic=True)
def new_config(logger, arg): """ Thread that turns on access point on the device to modify configurations. Name of access point: PmSensor Password: pmsensor Enter 192.168.4.10 on device browser to get configuration form. Indicator LEDs: Blue - Access point turned on, not connected, Green - Connected, configuration is open on browser, Red - An error has occured The device automatically reboots and applies modifications upon successful configuration. :param thread_name: Thread id :type thread_name: str :param logger: status logger :type logger: LoggerFactory object :param timeout: timeout (in seconds) for user configuration :type timeout: int """ # Only one of this thread is allowed to run at a time if not wifi_lock.locked(): with wifi_lock: logger.info("New configuration setup started") # Config uses LED colours to indicate the state of the connection - lock is necessary to disable error pings led_lock.acquire(1) # set pycom up as access point wlan = network.WLAN(mode=WLAN.AP, ssid=config.get_config("device_name")) # Connect to PmSensor using password set by the user wlan.init(mode=WLAN.AP, ssid=config.get_config("device_name"), auth=(WLAN.WPA2, config.get_config("password")), channel=1, antenna=WLAN.INT_ANT) # Load HTML via entering 192,168.4.10 to browser wlan.ifconfig(id=1, config=('192.168.4.10', '255.255.255.0', '192.168.4.1', '192.168.4.1')) logger.info('Access point turned on as {}'.format( config.get_config("device_name"))) logger.info( 'Configuration website can be accessed at 192.168.4.10') address = socket.getaddrinfo( '0.0.0.0', 80)[0][-1] # Accept stations from all addresses sct = socket.socket() # Create socket for communication sct.settimeout(config.get_config( "config_timeout")) # session times out after x seconds gc.collect( ) # frees up unused memory if there was a previous connection sct.bind(address) # Bind address to socket sct.listen(1) # Allow one station to connect to socket pycom.rgbled(0x000055) # Blue LED - waiting for connection get_new_config(sct, logger) wlan.deinit() # turn off wifi gc.collect() logger.info('rebooting...') machine.reset()
# ToDo: Update OTA.py so if version is 0.0.0, it backs up all existing files, and adds all files as new. # ToDo: Set code_version to '0.0.0' in default config, and remove the line below config.save_config({"code_version": "0.2.6"}) """SET FORMAT VERSION NUMBER - version number is used to indicate the data format used to decode LoRa messages in the back end. If the structure of the LoRa message is changed during update, increment the version number and add a corresponding decoder to the back-end.""" config.save_config({"fmt_version": 1}) # Override Preferences - DEVELOPER USE ONLY - keep all overwrites here if "debug_config.json" in os.listdir("/flash"): status_logger.warning( "Overriding configuration with the content of debug_config.json") with open("/flash/debug_config.json", "r") as f: config.set_config(ujson.loads(f.read())) status_logger.warning("Configuration changed to: " + str(config.get_config())) # Check if GPS is enabled in configurations gps_on = False if config.get_config("GPS") == "OFF" else True # Get current time rtc = RTC() no_time, update_time_later = initialise_time(rtc, gps_on, status_logger) # Check if device is configured, or SD card has been moved to another device device_id = hexlify(unique_id()).upper().decode("utf-8") if (not config.is_complete(status_logger) or config.get_config("device_id") != device_id): config.reset_configuration(status_logger) # Force user to configure device, then reboot setup_new_config(status_logger)
# Read configuration file to get preferences config.read_configuration() """SET VERSION NUMBER - version number is used to indicate the data format used to decode LoRa messages in the back end. If the structure of the LoRa message is changed during update, increment the version number and add a corresponding decoder to the back-end.""" config.set_config({"version": 1}) # Override Preferences - DEVELOPER USE ONLY - keep all overwrites here if 'debug_config.json' in os.listdir('/flash'): status_logger.warning( "Overriding configuration with the content of debug_config.json") with open('/flash/debug_config.json', 'r') as f: config.set_config(ujson.loads(f.read())) status_logger.warning("Configuration changed to: " + str(config.get_config())) # Initialize PM power circuitry PM_transistor = Pin('P20', mode=Pin.OUT) PM_transistor.value(0) if config.get_config(s.PM1) != "OFF" or config.get_config(s.PM2) != "OFF": PM_transistor.value(1) # Check if GPS is enabled in configurations gps_on = True if config.get_config("GPS") == "OFF": gps_on = False # Get current time rtc = RTC() no_time, update_time_later = initialize_time(rtc, gps_on, status_logger)
def run_test(self): pycom.rgbled(0x66039a) # Purple :] self.logger.info("Running hardware test") active_sensors = get_sensors() active_sensor_names = list(active_sensors.keys()) self.logger.info("Found {} sensors: {}".format( len(active_sensor_names), active_sensor_names)) for sensor_name in active_sensor_names: if active_sensors[sensor_name] == False: self.logger.info( "Skipping test of disabled sensor {}".format(sensor_name)) continue self.logger.info("Testing sensor {}".format(sensor_name)) sensor_type = config.get_config(sensor_name) self.logger.info("Sensor {} found to be of type {}".format( sensor_name, sensor_type)) self.logger.info("Attempting to initialize sensor") try: if sensor_type == "PMS5003": sensor = Plantower( pins=("P3", "P17"), id=1, ) elif sensor_type == "SPS030": sensor = Sensirion(retries=1, pins=("P11", "P18"), id=2) elif sensor_type == "SHT35": sensor = TempSHT35(None, None) else: raise ValueError("Unknown sensor type") except Exception as e: self.logger.exception( "Failed to initialize sensor: {}".format(e)) continue time.sleep(1) init_count = 0 try: init_limit = int(config.get_config(sensor_name + "_init")) except TypeError: # Sensor doesn't have a set init time. init_limit = 5 self.logger.info( "Warming up sensor for {} seconds".format(init_limit)) while init_count < init_limit: try: time.sleep(1) sensor.read() init_count += 1 except Exception as e: self.logger.exception( "Failed to read from sensor: {}".format(e)) pass time.sleep(3) # We can't read the sensor too fast after warmup self.logger.info("Attempting read of data from sensor") try: recv = sensor.read() if recv: recv_str = str(recv) self.logger.info("Read data: {}".format(recv_str)) else: self.logger.info("sensor.read returned no data") except Exception as e: self.logger.exception( "Failed to read from sensor {}".format(sensor_type)) pass self.logger.info("Finished testing sensor {}".format(sensor_name)) self.logger.info("Hardware test finished") pycom.rgbled(0x000000) # Turn off