Advertisement
aalh

gpstravellogging

Dec 7th, 2023
761
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.08 KB | Source Code | 0 0
  1. # SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries
  2. #
  3. # SPDX-License-Identifier: MIT
  4.  
  5. """
  6. PyPortal based alarm clock.
  7.  
  8. Adafruit invests time and resources providing this open source code.
  9. Please support Adafruit and open source hardware by purchasing
  10. products from Adafruit!
  11.  
  12. Written by Dave Astels for Adafruit Industries
  13. Copyright (c) 2019 Adafruit Industries
  14. Licensed under the MIT license.
  15.  
  16. All text above must be included in any redistribution.
  17. """
  18.  
  19. # pylint:disable=redefined-outer-name,no-member,global-statement
  20. # pylint:disable=no-self-use,too-many-branches,too-many-statements
  21. # pylint:disable=useless-super-delegation, too-many-locals
  22.  
  23. import time
  24. import board
  25. from adafruit_pyportal import PyPortal
  26. from adafruit_bitmap_font import bitmap_font
  27. from adafruit_display_text.label import Label
  28.  
  29. import analogio
  30. import displayio
  31. import adafruit_logging as logging
  32. import busio
  33. import rtc
  34. import adafruit_gps
  35. import neopixel
  36. from microcontroller import nvm
  37. from adafruit_bme280 import basic as bmE280
  38. import adafruit_ahtx0 as ahT20
  39. # init logfile
  40.  
  41. LOG_FILE = "/sd/gps.txt"
  42. LOG_MODE = "a"
  43.  
  44. # read time zone
  45. tz_cur = nvm[0]
  46. ####################
  47. # setup hardware
  48. i2c = busio.I2C(board.SCL, board.SDA, frequency=200_000)
  49. gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False)  # Use I2C interface
  50.  
  51. gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")
  52. gps.send_command(b"PMTK220,1000")
  53.  
  54. pixels = neopixel.NeoPixel(board.NEOPIXEL, 1)
  55. bmE280.sea_level_pressure = 1013.25
  56. outside = ahT20.AHTx0(i2c)
  57. inside = bmE280.Adafruit_BME280_I2C(i2c)
  58.  
  59. while gps.timestamp_utc is None:
  60.     print("No time data from GPS yet")
  61.     pixels[0] = (0, 0, 0)
  62.     time.sleep(.7)
  63.     pixels[0] = (128, 0, 0)
  64.     time.sleep(.7)
  65.     gps.update()
  66. pixels[0] = (0, 0, 0)
  67. the_rtc = rtc.RTC()
  68. the_rtc.datetime = time.struct_time(gps.timestamp_utc)
  69.  
  70. pyportal = PyPortal()
  71.  
  72. light = analogio.AnalogIn(board.LIGHT)
  73.  
  74. ####################
  75. # variables
  76. speed = 0
  77. # alarm support
  78.  
  79. alarm_background = 'red_alertl.bmp'
  80. alarm_file = 'alarm.wav'
  81. alarm_enabled = False
  82. alarm_armed = False
  83. alarm_interval = 10.0
  84. alarm_hour = 9
  85. alarm_minute = 45
  86. snooze_time = None
  87. snooze_interval = 600.0
  88.  
  89. # pressure support
  90. mugsy_background = 'mugsy_backgroundl.bmp'
  91.  
  92. # weather support
  93.  
  94. icon_file = None
  95. icon_sprite = None
  96. celcius = None
  97.  
  98.  
  99. # display/data refresh timers
  100.  
  101. refresh_time = None
  102. update_time = None
  103. weather_refresh = None
  104.  
  105. # The most recently fetched time
  106. current_time = None
  107.  
  108. # track whether we're in low light mode
  109.  
  110. low_light = False
  111.  
  112.  
  113. ####################
  114. # Load the fonts
  115.  
  116. time_font = bitmap_font.load_font('/fonts/Anton-Regular-104.bdf')
  117. time_font.load_glyphs(b'0123456789:')  # pre-load glyphs for fast printing
  118.  
  119. alarm_font = bitmap_font.load_font('/fonts/Helvetica-Bold-36.bdf')
  120. alarm_font.load_glyphs(b'0123456789:')
  121.  
  122. temperature_font = bitmap_font.load_font('/fonts/Arial-16.bdf')
  123. temperature_font.load_glyphs(b'0123456789CF')
  124.  
  125. ####################
  126. # Set up logging
  127.  
  128. logger = logging.getLogger('alarm_clock')
  129. logger.addHandler(logging.FileHandler("/sd/test.txt", "a"))
  130. logger.setLevel(logging.ERROR)            # change as desired
  131.  
  132. ####################
  133. # Functions
  134. def debug(*args, **kwargs):
  135.     # lets both print and write to the file
  136.     print(*args, **kwargs)
  137.     print(*args, **kwargs, file=outfile)
  138.  
  139. def debugp(*args, **kwargs):
  140.     # lets both print and write to the file
  141.     print(*args, **kwargs)
  142.     print(*args, **kwargs, file=pressfile)
  143.  
  144. def create_text_areas(configs):
  145.     """Given a list of area specifications, create and return test areas."""
  146.     text_areas = []
  147.     for cfg in configs:
  148.         textarea = Label(cfg['font'], text=' '*cfg['size'])
  149.         textarea.x = cfg['x']
  150.         textarea.y = cfg['y']
  151.         textarea.color = cfg['color']
  152.         text_areas.append(textarea)
  153.     return text_areas
  154.  
  155.  
  156. def clear_splash():
  157.     for _ in range(len(pyportal.splash) - 1):
  158.         pyportal.splash.pop()
  159.  
  160.  
  161. def touch_in_button(t, b):
  162.     in_horizontal = b['left'] <= t[0] <= b['right']
  163.     in_vertical = b['top'] <= t[1] <= b['bottom']
  164.     return in_horizontal and in_vertical
  165.  
  166.  
  167. touched = False
  168.  
  169. ####################
  170. # states
  171.  
  172. class State(object):
  173.     """State abstract base class"""
  174.  
  175.     def __init__(self):
  176.         pass
  177.  
  178.     @property
  179.     def name(self):
  180.         """Return the name of teh state"""
  181.         return ''
  182.  
  183.     def tick(self, now):
  184.         """Handle a tick: one pass through the main loop"""
  185.         pass
  186.  
  187.     # pylint:disable=unused-argument
  188.     def touch(self, t, touched):
  189.  
  190.         """Handle a touch event.
  191.        :param (x, y, z) - t: the touch location/strength"""
  192.         return bool(t)
  193.  
  194.     def enter(self):
  195.         """Just after the state is entered."""
  196.         pass
  197.  
  198.     def exit(self):
  199.         """Just before the state exits."""
  200.         clear_splash()
  201.  
  202. class Time_State(State):
  203.     """This state manages the primary time display screen/mode"""
  204.  
  205.     def __init__(self):
  206.         super().__init__()
  207.         self.background_day = 'main_background_dayl.bmp'
  208.         self.background_night = 'main_background_nightl.bmp'
  209.         self.refresh_time = None
  210.         self.update_time = None
  211.         self.weather_refresh = None
  212.         text_area_configs = [dict(x=160, y=220, size=4, color=0xFFFFFF, font=time_font),
  213.                              dict(x=290, y=100, size=4, color=0xFF0000, font=alarm_font),
  214.                              dict(x=125, y=90, size=6, color=0xFFFFFF, font=temperature_font),
  215.                              dict(x=125, y=125, size=6, color=0xFFFFFF, font=temperature_font),
  216.                              dict(x=200, y=90, size=6, color=0xFFFFFF, font=temperature_font),
  217.                              dict(x=200, y=125, size=6, color=0xFFFFFF, font=temperature_font),
  218.                              dict(x=145, y=150, size=6, color=0x00FFFF, font=temperature_font)]
  219.         self.text_areas = create_text_areas(text_area_configs)
  220.         self.weather_icon = displayio.Group()
  221.         self.weather_icon.x = 125
  222.         self.weather_icon.y = 25
  223.         self.icon_file = None
  224.  
  225.         # each button has it's edges as well as the state to transition to when touched
  226.         self.buttons = [dict(left=0, top=72, right=118, bottom=158, next_state='settings'),
  227.                         dict(left=0, top=208, right=118, bottom=294, next_state='pressurel')]
  228.  
  229.     @property
  230.     def name(self):
  231.         return 'time'
  232.  
  233.     def adjust_backlight_based_on_light(self, force=False):
  234.         """Check light level. Adjust the backlight and background image if it's dark."""
  235.         global low_light
  236.         if light.value <= 800 and (force or not low_light):
  237.             pyportal.set_backlight(0.01)
  238.             pyportal.set_background(self.background_night)
  239.             low_light = True
  240.         elif force or (light.value >= 2000 and low_light):
  241.             pyportal.set_backlight(1.00)
  242.             pyportal.set_background(self.background_day)
  243.             low_light = False
  244.  
  245.     def tick(self, now):
  246.         global alarm_armed, snooze_time, update_time, current_time, time_string, tz_cur, outfile, t_stamp
  247.  
  248.         # check light level and adjust background & backlight
  249.         self.adjust_backlight_based_on_light()
  250.  
  251.         # only query the online time once per hour (and on first run)
  252.         if (not self.refresh_time) or ((now - self.refresh_time) > 3600):
  253.             logger.debug('Fetching time')
  254.             try:
  255.                 gps.update()
  256.                 the_rtc.datetime = time.struct_time(gps.timestamp_utc)
  257.                 self.refresh_time = now
  258.             except RuntimeError as e:
  259.                 self.refresh_time = now - 3000   # delay 10 minutes before retrying
  260.                 logger.error('Some error occured, retrying! - %s', str(e))
  261.  
  262.         # only query the weather every 10 minutes (and on first run)
  263.         if (not self.weather_refresh) or (now - self.weather_refresh) > 4:
  264.             logger.debug('Fetching weather')
  265.             try:
  266.                 gps.update()
  267.                 if gps.speed_knots is not None:
  268.                     speed = '%5d mph' % round(gps.speed_knots * 1.151)
  269.                 else:
  270.                     speed = "0"
  271.                 self.text_areas[1].text = speed
  272.                 intemp = inside.temperature
  273.                 it_text = '%3d F' % round(((intemp * 9 / 5) + 32))
  274.                 self.text_areas[2].text = it_text
  275.                 inrh = inside.relative_humidity
  276.                 humi_text = '%3d ' % round(inrh)
  277.                 self.text_areas[3].text = humi_text
  278.                 outtemp = outside.temperature
  279.                 ot_text = '%3d F' % round(((outtemp * 9 / 5) + 32))
  280.                 self.text_areas[4].text = ot_text
  281.                 outrh = outside.relative_humidity
  282.                 humo_text = '%3d ' % round(outrh)
  283.                 self.text_areas[5].text = humo_text
  284.                 if gps.altitude_m is not None:
  285.                     altm = '%5d M' % round(gps.altitude_m)
  286.                 else:
  287.                     altm = "0"
  288.                 self.text_areas[6].text = altm
  289.                 self.weather_refresh = now
  290.                 t_stamp = time.mktime(gps.timestamp_utc)
  291.                 with open(LOG_FILE, LOG_MODE) as outfile:
  292.                     debug(t_stamp, gps.latitude, gps.longitude, gps.altitude_m, speed,
  293.                           " : ",  intemp, inrh, outtemp, outrh, inside.pressure)
  294.  
  295.                 try:
  296.                     board.DISPLAY.refresh(target_frames_per_second=60)
  297.                 except AttributeError:
  298.                     board.DISPLAY.refresh_soon()
  299.                     board.DISPLAY.wait_for_frame()
  300.  
  301.             except RuntimeError as e:
  302.                 self.weather_refresh = now - 540   # delay a minute before retrying
  303.                 logger.error("Some error occured, retrying! - %s", str(e))
  304.  
  305.         if (not update_time) or ((now - update_time) > 30):
  306.             # Update the time
  307.             update_time = now
  308.             current_time = time.localtime()
  309.             time_string = '%02d:%02d' % (((current_time.tm_hour-tz_cur) % 24), current_time.tm_min)
  310.             self.text_areas[0].text = time_string
  311.  
  312.             try:
  313.                 board.DISPLAY.refresh(target_frames_per_second=60)
  314.             except AttributeError:
  315.                 board.DISPLAY.refresh_soon()
  316.                 board.DISPLAY.wait_for_frame()
  317.  
  318.     def touch(self, t, touched):
  319.         if t and not touched:             # only process the initial touch
  320.             for button_index in range(len(self.buttons)):
  321.                 b = self.buttons[button_index]
  322.                 if touch_in_button(t, b):
  323.                     change_to_state(b['next_state'])
  324.                     break
  325.         return bool(t)
  326.  
  327.     def enter(self):
  328.         self.adjust_backlight_based_on_light(force=True)
  329.         for ta in self.text_areas:
  330.             pyportal.splash.append(ta)
  331.         pyportal.splash.append(self.weather_icon)
  332.         if snooze_time:
  333.             # CircuitPython 7+ compatible
  334.             icon = displayio.OnDiskBitmap("/icons/zzz.bmp")
  335.             icon_sprite = displayio.TileGrid(icon, pixel_shader=icon.pixel_shader)
  336.  
  337.             self.snooze_icon.append(icon_sprite)
  338.             pyportal.splash.append(self.snooze_icon)
  339.  
  340.         try:
  341.             board.DISPLAY.refresh(target_frames_per_second=60)
  342.         except AttributeError:
  343.             board.DISPLAY.refresh_soon()
  344.             board.DISPLAY.wait_for_frame()
  345.  
  346.     def exit(self):
  347.         super().exit()
  348.         for _ in range(len(self.snooze_icon)):
  349.             self.snooze_icon.pop()
  350.  
  351. class Pressure_State(Time_State):
  352.     """This state records a pressure log """
  353.  
  354.     def __init__(self):
  355.         super().__init__()
  356.  
  357.     @property
  358.     def name(self):
  359.         return 'pressurel'
  360.  
  361.     def tick(self, now):
  362.         # Once the job is done, go back to the main screen
  363.         change_to_state('time')
  364.  
  365.     def enter(self):
  366.         global low_light, pressfile, PLOG_FILE, LOG_MODE, current_time
  367.         low_light = False
  368.         stamped = False
  369.         plog_path = "/sd/"
  370.         pyportal.set_backlight(1.00)
  371.         pyportal.set_background(mugsy_background)
  372.         try:
  373.             board.DISPLAY.refresh(target_frames_per_second=60)
  374.         except AttributeError:
  375.             board.DISPLAY.refresh_soon()
  376.             board.DISPLAY.wait_for_frame()
  377.         gps.update()
  378.         t_stamp = time.mktime(gps.timestamp_utc)
  379.         PLOG_FILE = f"{t_stamp}pressure.txt"
  380.         loop = 1200
  381.         timep = time.monotonic_ns()
  382.         while loop > 0:
  383.             with open((plog_path+PLOG_FILE), LOG_MODE) as pressfile:
  384.                 if stamped is False:
  385.                     debugp(t_stamp, gps.latitude, gps.longitude, gps.altitude_m)
  386.                     stamped = True
  387.                 debugp(((time.monotonic_ns() - timep) / 1000000000), inside.pressure)
  388.             loop -= 1
  389.  
  390. class Setting_State(State):
  391.     """This state lets the user enable/disable the alarm and set its time.
  392.    Swiping up/down adjusts the hours & miniutes separately."""
  393.  
  394.     def __init__(self):
  395.         super().__init__()
  396.         self.previous_touch = None
  397.         self.background = 'settings_backgroundl.bmp'
  398.         text_area_configs = [dict(x=160, y=220, size=4, color=0xFFFFFF, font=time_font)]
  399.  
  400.         self.text_areas = create_text_areas(text_area_configs)
  401.         self.buttons = [dict(left=0, top=40, right=115, bottom=123),    # on
  402.                         dict(left=0, top=130, right=115, bottom=200),   # return
  403.                         dict(left=0, top=208, right=100, bottom=295),  # off
  404.                         dict(left=150, top=0, right=300, bottom=320),  # hours
  405.                         dict(left=310, top=0, right=480, bottom=320)]   # minutes
  406.  
  407.     @property
  408.     def name(self):
  409.         return 'settings'
  410.  
  411.     def touch(self, t, touched):
  412.         global alarm_hour, alarm_minute, alarm_enabled, time_string, tz_cur
  413.         if t:
  414.             if touch_in_button(t, self.buttons[0]):   # increase offset
  415.                 logger.debug('increase offset')
  416.                 tz_cur += 1
  417.                 current_time = time.localtime()
  418.                 time_string = '%02d:%02d' % (((current_time.tm_hour-tz_cur) % 24), current_time.tm_min)
  419.                 self.text_areas[0].text = time_string
  420.             elif touch_in_button(t, self.buttons[1]):   # return
  421.                 logger.debug('RETURN touched')
  422.                 nvm[0] = tz_cur
  423.                 change_to_state('time')
  424.             elif touch_in_button(t, self.buttons[2]):  # decrease offset
  425.                 logger.debug('decrease offset')
  426.                 tz_cur -= 1
  427.                 current_time = time.localtime()
  428.                 time_string = '%02d:%02d' % (((current_time.tm_hour-tz_cur) % 24), current_time.tm_min)
  429.                 self.text_areas[0].text = time_string
  430.             try:
  431.                 board.DISPLAY.refresh(target_frames_per_second=60)
  432.             except AttributeError:
  433.                 board.DISPLAY.refresh_soon()
  434.                 board.DISPLAY.wait_for_frame()
  435.  
  436.         else:
  437.             self.previous_touch = None
  438.         return bool(t)
  439.  
  440.     def enter(self):
  441.         global snooze_time
  442.         snooze_time = None
  443.  
  444.         pyportal.set_background(self.background)
  445.         for ta in self.text_areas:
  446.             pyportal.splash.append(ta)
  447.         if alarm_enabled:
  448.             self.text_areas[0].text = '%02d:%02d' % (alarm_hour, alarm_minute)  # set time textarea
  449.         else:
  450.             self.text_areas[0].text = '     '
  451.  
  452.  
  453. ####################
  454. # State management
  455.  
  456. states = {'time': Time_State(),
  457.           'pressurel': Pressure_State(),
  458.  
  459.           'settings': Setting_State()}
  460.  
  461. current_state = None
  462.  
  463.  
  464. def change_to_state(state_name):
  465.     global current_state
  466.     if current_state:
  467.         logger.debug('Exiting %s', current_state.name)
  468.         current_state.exit()
  469.     current_state = states[state_name]
  470.     logger.debug('Entering %s', current_state.name)
  471.     current_state.enter()
  472.  
  473. ####################
  474. # And... go
  475.  
  476. clear_splash()
  477. change_to_state("time")
  478.  
  479. while True:
  480.     touched = current_state.touch(pyportal.touchscreen.touch_point, touched)
  481.     current_state.tick(time.monotonic())
  482.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement