# SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries # # SPDX-License-Identifier: MIT """ PyPortal based alarm clock. Adafruit invests time and resources providing this open source code. Please support Adafruit and open source hardware by purchasing products from Adafruit! Written by Dave Astels for Adafruit Industries Copyright (c) 2019 Adafruit Industries Licensed under the MIT license. All text above must be included in any redistribution. """ # pylint:disable=redefined-outer-name,no-member,global-statement, line-too-long, invalid-name # pylint:disable=no-self-use,too-many-branches,too-many-statements, import-error # pylint:disable=useless-super-delegation, too-many-locals, no-name-in-module import math import time import board from adafruit_bitmap_font import bitmap_font # type: ignore from adafruit_display_text.label import Label # pyright: ignore[reportMissingImports] import adafruit_sgp40 # pyright: ignore[reportMissingImports] import analogio # import displayio import adafruit_logging as logging # pyright: ignore[reportMissingImports] import bitbangio import rtc import adafruit_gps # pyright: ignore[reportMissingImports] import neopixel # pyright: ignore[reportMissingImports] from microcontroller import nvm from adafruit_bme280 import basic as bmE280 # pyright: ignore[reportMissingImports]TZ_CUR = nvm[0] # type: ignore import adafruit_sht31d import adafruit_lps2x from adafruit_pyportal import PyPortal # type: ignore # read time zone if nvm[0] > 23 or nvm[0] < 0: # pyright: ignore[reportOptionalSubscript] TZ_CUR = 4 nvm[0] = 4 # pyright: ignore[reportOptionalSubscript] else: TZ_CUR = nvm[0] # type: ignore #################### # setup hardware # i2c = board.I2C() # i2c = busio.I2C(board.SCL, board.SDA, frequency=100_000) # bitbangio used due to its implementation working better (less i2c errors) i2c = bitbangio.I2C(board.SCL, board.SDA, frequency=100_000) # type: ignore gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # type: ignore # Use I2C interface gps.send_command(b"PMTK314,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") gps.send_command(b"PMTK220,1000") pixels = neopixel.NeoPixel(board.NEOPIXEL, 1) # type: ignore bmE280.sea_level_pressure = 1013.25 # type: ignore outside = adafruit_sht31d.SHT31D(i2c) inside = bmE280.Adafruit_BME280_I2C(i2c) # type: ignore lps = adafruit_lps2x.LPS22(i2c) sgp = adafruit_sgp40.SGP40(i2c) while gps.timestamp_utc is None: print("No time data from GPS yet") pixels[0] = (0, 0, 0) time.sleep(.7) pixels[0] = (128, 0, 0) time.sleep(.7) gps.update() print(gps.readline()) pixels[0] = (0, 0, 0) the_rtc = rtc.RTC() the_rtc.datetime = time.struct_time(gps.timestamp_utc) pyportal = PyPortal() light = analogio.AnalogIn(board.LIGHT) # type: ignore # init logfile # t_stamp = "{0:x}".format(time.time()) DELIMETER = "\t" LOG_FILE = f"/sd/{time.time()}gps.txt" LOG_MODE = "a" with open(LOG_FILE, LOG_MODE, encoding="utf-8") as outfile: # type: ignore print("UTC",DELIMETER,"lat",DELIMETER,"long",DELIMETER,"alt",DELIMETER,"Speed",DELIMETER,"intemp",DELIMETER,"intemp2",DELIMETER,"inh",DELIMETER,"otemp",DELIMETER,"outh",DELIMETER,"barr1",DELIMETER,"barr2",DELIMETER,"airQ", file=outfile) print("envlogfile created") #################### # variables Speed = 0 # alarm support """ alarm_background = 'red_alertl.bmp' alarm_file = 'alarm.wav' alarm_enabled = False alarm_armed = False alarm_interval = 10.0 alarm_hour = 9 alarm_minute = 45 snooze_time = None snooze_interval = 600.0 """ # pressure support # mugsy_background = 'mugsy_backgroundl.bmp' # weather support icon_file = None icon_sprite = None celcius = None # display/data refresh timers refresh_time = None update_time = None weather_refresh = None # The most recently fetched time current_time = None # track whether we're in low light mode low_light = False #################### # Load the fonts print("loading fonts") time_font = bitmap_font.load_font('/fonts/Asimov-104.pcf') time_font.load_glyphs(b'0123456789:%') # pre-load glyphs for fast printing # type: ignore alarm_font = bitmap_font.load_font('/fonts/AsimovNar-45.pcf') alarm_font.load_glyphs(b'0123456789mph.:%') # type: ignore title_font = bitmap_font.load_font('/fonts/Asimov-25.pcf') title_font.load_glyphs(b'INSIDEOUT') # type: ignore temperature_font = bitmap_font.load_font('/fonts/AsimovNar-40.pcf') temperature_font.load_glyphs(b'0123456789INSIDEOUTFft.%') # type: ignore print("fonts loaded") #################### # Set up logging logger = logging.getLogger('alarm_clock') logger.addHandler(logging.FileHandler("/sd/syslogz.txt", "a")) logger.setLevel(logging.INFO) # change as desired # type: ignore print("logger loaded") #################### # Functions # def debug(*args, **kwargs): # lets both print and write to the file # print(*args, **kwargs) # print(*args, **kwargs, file=outfile) # type: ignore def create_text_areas(configs): """Given a list of area specifications, create and return text areas.""" print("creating text areas") text_areas = [] for cfg in configs: textarea = Label(cfg['font'], text=' '*cfg['size']) textarea.x = cfg['x'] textarea.y = cfg['y'] textarea.color = cfg['color'] text_areas.append(textarea) return text_areas def calc_dewpoint(humidity, temp_c): """Calculate dew point given relative humidity and temperature in Celsius.""" a = 17.625 b = 243.04 alpha = math.log(humidity/100.0) + ((a * temp_c) / (b + temp_c)) return (b * alpha) / (a - alpha) def clear_splash(): """clear the screen""" print("clear_splash") for _ in range(len(pyportal.root_group) - 1): pyportal.root_group.pop() def touch_in_button(t, b): """touch""" in_horizontal = b['left'] <= t[0] <= b['right'] in_vertical = b['top'] <= t[1] <= b['bottom'] return in_horizontal and in_vertical touched = False #################### # states class State(object): """State abstract base class""" def __init__(self): pass @property def name(self): """Return the name of teh state""" return '' def tick(self, now): """Handle a tick: one pass through the main loop""" # pylint:disable=unused-argument def touch(self, t, touched): """Handle a touch event. :param (x, y, z) - t: the touch location/strength""" return bool(t) def enter(self): """Just after the state is entered.""" print(State) def exit(self): """Just before the state exits.""" clear_splash() class Time_State(State): """This state manages the primary time display screen/mode""" def __init__(self): super().__init__() self.background_day = 'main_background_dayl.bmp' self.background_night = 'main_background_nightl.bmp' self.refresh_time = None self.update_time = None self.weather_refresh = None text_area_configs = [dict(x=160, y=235, size=5, color=0xFFFFFF, font=time_font), # TIME dict(x=325, y=100, size=5, color=0xE09941, font=alarm_font), # SPEED dict(x=120, y=33, size=16, color=0xCFFC03, font=title_font), # screen heading dict(x=125, y=70, size=6, color=0xFFFFFF, font=temperature_font), # INSIDE temp dict(x=125, y=115, size=6, color=0xFFFFFF, font=temperature_font), # INSIDE humid dict(x=215, y=70, size=6, color=0xFFFFFF, font=temperature_font), # OUTSIDE temp dict(x=215, y=115, size=6, color=0xFFFFFF, font=temperature_font), # OUTSIDE humid dict(x=145, y=170, size=12, color=0x32A89E, font=temperature_font), # Altitude dict(x=310, y=180, size=5, color=0xCFFC03, font=title_font)] # Air Quality self.text_areas = create_text_areas(text_area_configs) # self.weather_icon = displayio.Group() # self.weather_icon.x = 125 # self.weather_icon.y = 25 # self.icon_file = None print("creating buttons") # each button has it's edges as well as the state to transition to when touched self.buttons = [dict(left=0, top=72, right=118, bottom=158, next_state='settings'), dict(left=0, top=208, right=118, bottom=294, next_state='pressurel')] @property def name(self): return 'time' def adjust_backlight_based_on_light(self, force=False): """Check light level. Adjust the backlight and background image if it's dark.""" global low_light if light.value <= 800 and (force or not low_light): pyportal.set_backlight(0.01) pyportal.set_background(self.background_night) low_light = True elif force or (light.value >= 2000 and low_light): pyportal.set_backlight(1.00) pyportal.set_background(self.background_day) low_light = False def tick(self, now): global update_time, outfile # check light level and adjust background & backlight self.adjust_backlight_based_on_light() # only query the online time once per hour (and on first run) if (not self.refresh_time) or ((now - self.refresh_time) > 3600): logger.debug('Fetching time') try: gps.update() the_rtc.datetime = time.struct_time(gps.timestamp_utc) # type: ignore self.refresh_time = now except RuntimeError as e: self.refresh_time = now - 3000 # delay 10 minutes before retrying logger.error('Some error occured, retrying! - %s', str(e)) # only query the weather every 10 minutes (and on first run) # if (not self.weather_refresh) or (now - self.weather_refresh) > 1: print("reading sensors") logger.debug('Fetching weather') try: gps.update() if gps.speed_knots is not None: Speed = '%5d mph' % round(gps.speed_knots * 1.151) # type: ignore else: Speed = "0" self.text_areas[1].text = Speed i1 = (inside.temperature * 9 / 5) + 32 i2 = (lps.temperature * 9 / 5) + 32 intemp = '%3d F' % round(i1) intemp2 = '%3d F' % round(i2) it_text = '%3d F' % round((i1 + i2) / 2) self.text_areas[2].text = "INSIDE OUTSIDE" self.text_areas[3].text = it_text inrh = inside.relative_humidity humi_text = '%3d' % round((calc_dewpoint(inside.relative_humidity, inside.temperature)* 9 / 5) + 32 ) self.text_areas[4].text = humi_text outtemp = outside.temperature ot_text = '%3d F' % round(((outtemp * 9 / 5) + 32)) # type: ignore self.text_areas[5].text = ot_text humo_text = '%3d' % round((calc_dewpoint(outside.relative_humidity, outside.temperature)* 9 / 5) + 32 ) # type: ignore print("humo_text:", (calc_dewpoint(outside.relative_humidity, outside.temperature)* 9 / 5) + 32) self.text_areas[6].text = humo_text VOCIdx = sgp.measure_index(temperature = inside.temperature, relative_humidity = inrh) self.text_areas[8].text = '%3d' % round(VOCIdx) if gps.altitude_m is not None: altftxt = '%6.2d ft' % round(gps.altitude_m * 3.28084, 2) else: altftxt = "0" self.text_areas[7].text = altftxt self.weather_refresh = now t_stamp = time.time() with open(LOG_FILE, LOG_MODE, encoding="utf-8") as outfile: print(t_stamp, DELIMETER, gps.latitude, DELIMETER, gps.longitude, DELIMETER, altftxt, DELIMETER, Speed, DELIMETER, intemp, DELIMETER, intemp2, DELIMETER, humi_text, DELIMETER, ot_text, humo_text, " ", inside.pressure, DELIMETER, lps.pressure, " ", VOCIdx, file=outfile) try: board.DISPLAY.refresh(target_frames_per_second=60) # type: ignore except AttributeError: board.DISPLAY.refresh_soon() # type: ignore board.DISPLAY.wait_for_frame() # type: ignore except RuntimeError as e: self.weather_refresh = now - 540 # delay a minute before retrying logger.error("Some error occured, retrying! - %s", str(e)) if (not update_time) or ((now - update_time) > 30): # Update the time update_time = now current_time = time.localtime() time_string = '%02d:%02d' % (((current_time.tm_hour-TZ_CUR) % 24), current_time.tm_min) self.text_areas[0].text = time_string try: board.DISPLAY.refresh(target_frames_per_second=60) # type: ignore except AttributeError: board.DISPLAY.refresh_soon() # type: ignore board.DISPLAY.wait_for_frame() # type: ignore def touch(self, t, touched): if t and not touched: # only process the initial touch for button_index in enumerate(self.buttons): b = self.buttons[button_index] if touch_in_button(t, b): change_to_state(b['next_state']) # type: ignore break return bool(t) def enter(self): self.adjust_backlight_based_on_light(force=True) for ta in self.text_areas: pyportal.root_group.append(ta) # pyportal.root_group.append(self.weather_icon) try: board.DISPLAY.refresh(target_frames_per_second=60) # type: ignore except AttributeError: board.DISPLAY.refresh_soon() # type: ignore board.DISPLAY.wait_for_frame() # type: ignore def exit(self): super().exit() class Pressure_State(Time_State): """This state records a pressure log """ def __init__(self): super().__init__() text_area_configs = [dict(x=160, y=220, size=3, color=0xFFAAFF, font=time_font)] self.text_areas = create_text_areas(text_area_configs) @property def name(self): return 'pressurel' def tick(self, now): # Once the job is done, go back to the main screen change_to_state('time') # type: ignore def enter(self): global low_light for ta in self.text_areas: pyportal.root_group.append(ta) low_light = False plog_path = "/sd/" # pyportal.set_backlight(1.00) # pyportal.set_background(mugsy_background) lps.data_rate = adafruit_lps2x.Rate.LPS22_RATE_75_HZ # type: ignore gps.update() t_stamp = time.time() PLOG_FILE = f"{t_stamp}pressure.txt" start = time.monotonic_ns() / 1e9 with open((plog_path+PLOG_FILE), LOG_MODE, encoding="utf-8") as pressfile: print("timestamp, latitude, longitude, altitude_m, \n", file=pressfile) print(t_stamp, gps.latitude, gps.longitude, gps.altitude_m, file=pressfile) curr_time = 0 while curr_time < 60: curr_time = (time.monotonic_ns() / 1e9) - start print(curr_time, lps.pressure, file=pressfile) cntdwn = '%3d' % round((60 - curr_time)) self.text_areas[0].text = cntdwn try: board.DISPLAY.refresh(target_frames_per_second=60) # type: ignore except AttributeError: board.DISPLAY.refresh_soon() # type: ignore board.DISPLAY.wait_for_frame() # type: ignore lps.data_rate = adafruit_lps2x.Rate.LPS22_RATE_1_HZ # type: ignore class Setting_State(State): """This state lets the user adjust UTC offset.""" def __init__(self): super().__init__() self.previous_touch = None self.background = 'settings_backgroundl.bmp' text_area_configs = [dict(x=160, y=220, size=5, color=0xFFFFFF, font=time_font)] self.text_areas = create_text_areas(text_area_configs) self.buttons = [dict(left=0, top=40, right=115, bottom=123), # increase dict(left=0, top=130, right=115, bottom=150), # return dict(left=0, top=160, right=115, bottom=295)] # decrease # dict(left=150, top=0, right=300, bottom=320), # hours # dict(left=310, top=0, right=480, bottom=320)] # minutes time_string = '%02d' % TZ_CUR self.text_areas[0].text = time_string @property def name(self): return 'settings' def touch(self, t, touched): global TZ_CUR if t: if touch_in_button(t, self.buttons[0]): # increase offset logger.debug('increase offset') TZ_CUR += 1 time_string = '%02d' % TZ_CUR self.text_areas[0].text = time_string elif touch_in_button(t, self.buttons[1]): # return logger.debug('RETURN touched') nvm[0] = TZ_CUR # type: ignore change_to_state('time') elif touch_in_button(t, self.buttons[2]): # decrease offset logger.debug('decrease offset') TZ_CUR -= 1 time_string = '%02d' % TZ_CUR self.text_areas[0].text = time_string try: board.DISPLAY.refresh(target_frames_per_second=60) # type: ignore except AttributeError: board.DISPLAY.refresh_soon() # type: ignore board.DISPLAY.wait_for_frame() # type: ignore else: self.previous_touch = None return bool(t) def enter(self): pyportal.set_background(self.background) for ta in self.text_areas: pyportal.root_group.append(ta) # if alarm_enabled: # self.text_areas[0].text = '%02d:%02d' % (alarm_hour, alarm_minute) # set time textarea # else: # self.text_areas[0].text = ' ' #################### # State management states = {'time': Time_State(), 'pressurel': Pressure_State(), 'settings': Setting_State()} current_state = None def change_to_state(state_name): """.""" global current_state if current_state: logger.debug('Exiting %s', current_state.name) current_state.exit() current_state = states[state_name] logger.debug('Entering %s', current_state.name) current_state.enter() #################### # And... go clear_splash() change_to_state("time") while True: touched = current_state.touch(pyportal.touchscreen.touch_point, touched) # type: ignore current_state.tick(time.monotonic()) # type: ignore