Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # 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, global-variable-not-assigned
- # pylint:disable=useless-super-delegation, too-many-locals, no-name-in-module, C0200
- 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]
- import adafruit_sht31d # type: ignore
- import adafruit_lps2x # type: ignore
- 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
- print("i2c")
- gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # type: ignore # Use I2C interface
- print("gps")
- 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)
- print("sht31d")
- inside = bmE280.Adafruit_BME280_I2C(i2c) # type: ignore
- print("bme280")
- lps = adafruit_lps2x.LPS22(i2c)
- print("lps")
- sgp = adafruit_sgp40.SGP40(i2c)
- print("sgp40")
- gps.update()
- 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
- nobacklight = False
- # init logfile
- # t_stamp = "{0:x}".format(time.time())
- DELIMETER = "^"
- LOG_FILE = f"/sd/{time.time()}gps.txt"
- LOG_MODE = "a"
- outfile = open(LOG_FILE, LOG_MODE, encoding="utf-8") # 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
- # 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
- oldbacklight = 1
- ####################
- # 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"""
- print(f"touch_in_button: t={t}, b={b}")
- 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 = None
- self.background_night = None
- self.refresh_time = None
- self.update_time = None
- self.weather_refresh = None
- text_area_configs = [dict(x=50, 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=10, size=16, color=0xCFFC03, font=title_font), # screen heading
- dict(x=125, y=48, size=6, color=0xFFFFFF, font=temperature_font), # INSIDE temp
- dict(x=125, y=93, size=6, color=0x4F0066, font=temperature_font), # INSIDE humid
- dict(x=215, y=48, size=6, color=0xFFFFFF, font=temperature_font), # OUTSIDE temp
- dict(x=215, y=93, size=6, color=0x4F0066, 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=380, top=0, right=479, bottom=158, next_state='backlight'),
- dict(left=0, top=0, 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, nobacklight
- if light.value <= 800 and (force or not low_light) and nobacklight is False:
- pyportal.set_backlight(0.01)
- pyportal.set_background(self.background_night)
- low_light = True
- elif (force or (light.value >= 2000 and low_light)) and nobacklight is False:
- pyportal.set_backlight(1.00)
- pyportal.set_background(self.background_day)
- low_light = False
- def tick(self, now):
- global update_time, outfile # type: ignore
- # 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) > 300):
- 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 = round(gps.speed_knots * 1.151,0) # type: ignore
- speed_str = f"{Speed:5} mph"
- else:
- Speed = 0
- speed_str = f"{Speed:5d} mph"
- self.text_areas[1].text = speed_str
- i1 = (inside.temperature * 9 / 5) + 32
- i2 = (lps.temperature * 9 / 5) + 32
- intemp = f"{round(i1)} F"
- intemp2 = f"{round(i2):3d} F"
- it_text = f"{round((i1 + i2) / 2):3d} F"
- self.text_areas[2].text = "INSIDE OUTSIDE"
- self.text_areas[3].text = it_text
- inrh = inside.relative_humidity
- humi_text = f"{round((calc_dewpoint(inside.relative_humidity, inside.temperature)* 9 / 5) + 32 ):2d}"
- self.text_areas[4].text = humi_text
- outtemp = outside.temperature
- ot_text = f"{round(((outtemp * 9 / 5) + 32)):3d} F" # type: ignore
- self.text_areas[5].text = ot_text
- humo_text = f"{round((calc_dewpoint(outside.relative_humidity, outside.temperature)* 9 / 5) + 32 ):2d}" # 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 = f"{round(VOCIdx):3d} voc"
- if gps.altitude_m is not None:
- altftxt = f"{round(gps.altitude_m * 3.28084, 0):6} ft"
- else:
- altftxt = "0"
- self.text_areas[7].text = altftxt
- self.weather_refresh = now
- t_stamp = time.time()
- print(t_stamp, DELIMETER,
- gps.latitude, DELIMETER,
- gps.longitude, DELIMETER,
- altftxt, DELIMETER,
- Speed, DELIMETER,
- intemp, DELIMETER,
- intemp2, DELIMETER,
- humi_text, DELIMETER,
- ot_text, DELIMETER,
- humo_text, DELIMETER,
- inside.pressure, DELIMETER,
- lps.pressure, DELIMETER,
- 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 = f"{((current_time.tm_hour-TZ_CUR) % 24):02d}:{current_time.tm_min:02d}"
- 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 range(len(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):
- 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(time.time(), 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 = f"{round((60 - curr_time)):2d}"
- 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=0, right=200, bottom=123), # increase
- dict(left=0, top=130, right=200, bottom=180), # return
- dict(left=0, top=190, right=200, bottom=320)] # decrease
- # dict(left=150, top=0, right=300, bottom=320), # hours
- # dict(left=310, top=0, right=480, bottom=320)] # minutes
- time_string = f"{TZ_CUR:2d}"
- 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 = f"{TZ_CUR:2d}"
- 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 = f"{TZ_CUR:2d}"
- 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 = ' '
- class Backlight_Power(Time_State):
- """This state toggles backlight """
- def __init__(self):
- super().__init__()
- @property
- def name(self):
- return 'backlight'
- 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 nobacklight, oldbacklight
- if board.DISPLAY.brightness < 0:
- oldbacklight = board.DISPLAY.brightness
- board.DISPLAY.brightness = 0
- nobacklight = True
- return()
- board.DISPLAY.brightness =oldbacklight
- nobacklight = False
- # pyportal.set_background(mugsy_background)
- ####################
- # State management
- states = {'time': Time_State(),
- # 'pressurel': Pressure_State(),
- 'settings': Setting_State(),
- 'backlight': Backlight_Power()}
- 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")
- try:
- while True:
- touched = current_state.touch(pyportal.touchscreen.touch_point, touched) # type: ignore
- current_state.tick(time.monotonic()) # type: ignore
- finally:
- outfile.close()
Advertisement
Add Comment
Please, Sign In to add comment