diff --git a/bdf/trend_icons.bdf b/bdf/trend_icons.bdf new file mode 100644 index 0000000..b8dd3e4 --- /dev/null +++ b/bdf/trend_icons.bdf @@ -0,0 +1,54 @@ +STARTFONT 2.1 +FONT -Trend Icon Set +SIZE 20 75 75 +FONTBOUNDINGBOX 39 31 -11 -6 +STARTPROPERTIES 40 +FONT_ASCENT 17 +FONT_DESCENT 4 +UNDERLINE_POSITION -1 +UNDERLINE_THICKNESS 2 +X_HEIGHT 10 +CAP_HEIGHT 14 +DEFAULT_CHAR 1 +RAW_ASCENT 800 +RAW_DESCENT 200 +NORM_SPACE 6 +RELATIVE_WEIGHT 70 +RELATIVE_SETWIDTH 50 +SUPERSCRIPT_X 0 +SUPERSCRIPT_Y 7 +SUPERSCRIPT_SIZE 12 +SUBSCRIPT_X 0 +SUBSCRIPT_Y 1 +SUBSCRIPT_SIZE 12 +AVG_LOWERCASE_WIDTH 109 +AVG_UPPERCASE_WIDTH 142 +ENDPROPERTIES +CHARS 1300 +STARTCHAR NULL +ENCODING 0 +SWIDTH 0 0 +DWIDTH 0 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR comma +ENCODING 44 +SWIDTH 333 0 +DWIDTH 5 0 +BBX 3 4 1 -2 +BITMAP +40 +A0 +ENDCHAR +STARTCHAR period +ENCODING 46 +SWIDTH 333 0 +DWIDTH 5 0 +BBX 3 4 1 -2 +BITMAP +A0 +40 +ENDCHAR +ENDFONT \ No newline at end of file diff --git a/bmp/weather-icons.bmp b/bmp/weather-icons.bmp new file mode 100644 index 0000000..95be1fe Binary files /dev/null and b/bmp/weather-icons.bmp differ diff --git a/code.py b/code.py index f90b869..b60f70d 100644 --- a/code.py +++ b/code.py @@ -4,12 +4,16 @@ import busio from digitalio import DigitalInOut, Pull import neopixel + from adafruit_matrixportal.matrix import Matrix from adafruit_esp32spi import adafruit_esp32spi from adafruit_esp32spi import adafruit_esp32spi_wifimanager + import json import display_manager +print("All imports loaded | Available memory: {} bytes".format(gc.mem_free())) + # --- CONSTANTS SETUP --- try: @@ -22,6 +26,17 @@ station_code = secrets["station_code"] historical_trains = [None, None] +# weather data dict +weather_data = {} +# daily highest temperature +# max_temp, day of the year +highest_temp = [None,None] +# daily lowest temperature +# min_temp, day of the year +lowest_temp = [None, None] +# current temp (for historical) +current_temp = [] + # --- DISPLAY SETUP --- # MATRIX DISPLAY MANAGER @@ -110,11 +125,117 @@ def get_trains(StationCode, historical_trains): pass return trains +# queries Openweather API to return a dict with current and 3 hr forecast weather data +# input is latitude and longitude coordinates for weather location +def get_weather(weather_data): + try: + # query Openweather for weather at location defined by input lat, long + base_URL = 'https://api.openweathermap.org/data/3.0/onecall?' + latitude = secrets['dc coords x'] + longitude = secrets['dc coords y'] + units = 'imperial' + api_key = secrets['openweather api key'] + exclude = 'minutely,alerts' + response = wifi.get(base_URL + +'lat='+latitude + +'&lon='+longitude + +'&exclude='+exclude + +'&units='+units + +'&appid='+api_key + ) + weather_json = response.json() + del response + + # insert icon and current weather data into dict + weather_data["icon"] = weather_json["current"]["weather"][0]["icon"] + weather_data["current_temp"] = weather_json["current"]["temp"] + weather_data["current_feels_like"] = weather_json["current"]["feels_like"] + # insert daily forecast min and max temperature into dict + weather_data["daily_temp_min"] = weather_json["daily"][0]["temp"]["min"] + weather_data["daily_temp_max"] = weather_json["daily"][0]["temp"]["max"] + # insert next hour + 1 forecast temperature and feels like into dict + weather_data["hourly_next_temp"] = weather_json["hourly"][2]["temp"] + weather_data["hourly_feels_like"] = weather_json["hourly"][2]["feels_like"] + + # set daily highest temperature + global highest_temp + global current_time + # if daily highest temperature hasn't been set or is from a previous day + if highest_temp[0] is None or highest_temp[1] != current_time.tm_wday: + highest_temp[0] = weather_data["daily_temp_max"] + highest_temp[1] = current_time.tm_wday + print("Daily highest temp set to {}".format(highest_temp[0])) + # if stored highest temp is less than new highest temp + elif highest_temp[0] < weather_data["daily_temp_max"]: + highest_temp[0] = weather_data["daily_temp_max"] + print("Daily highest temp set to {}".format(highest_temp[0])) + # if stored highest temp is greater than new highest temp + elif highest_temp[0] > weather_data["daily_temp_max"]: + weather_data["daily_temp_max"] = highest_temp[0] + print("Daily highest temp pulled from historical data") + + # set daily lowest temperature + global lowest_temp + # if daily lowest temperature hasn't been set or is from a previous day + if lowest_temp[0] is None or lowest_temp[1] != current_time.tm_wday: + lowest_temp[0] = weather_data["daily_temp_min"] + lowest_temp[1] = current_time.tm_wday + print("Daily lowest temp set to {}".format(lowest_temp[0])) + # if daily lowest temp is greater than new lowest temp + elif lowest_temp[0] > weather_data["daily_temp_min"]: + lowest_temp[0] = weather_data["daily_temp_min"] + print("Daily lowest temp set to {}".format(lowest_temp[0])) + # if daily lowest temp is less than new lowest temp + elif lowest_temp[0] < weather_data["daily_temp_min"]: + weather_data["daily_temp_min"] = lowest_temp[0] + print("Daily lowest temp pulled from historical data") + + # add current temp to historical array + global current_temp + current_temp.append(weather_data["current_temp"]) + # clean up response + del weather_json + + # return dict with relevant data + return True + + except Exception as e: + print("Failed to get data, retrying\n", e) + wifi.reset() + +# --- TIME MGMT FUNCTIONS --- + +def check_time(): + base_url = "http://io.adafruit.com/api/v2/time/seconds" + try: + response = wifi.get(base_url) + epoch_time = int(response.text) + del response + + global current_time + current_time = time.localtime(epoch_time) + except Exception as e: + print(e) + wifi.reset() + # --- OPERATING LOOP ------------------------------------------ loop_counter=1 +last_weather_check=None last_train_check=None while True: + check_time() + + # fetch weather data on start and recurring (default: 10 minutes) + if last_weather_check is None or time.monotonic() > last_weather_check + 60 * 10: + weather = get_weather(weather_data) + if weather: + last_weather_check = time.monotonic() + print("weather updated") + # update weather display component + display_manager.update_weather(weather_data) + else:pass + # update train data (default: 15 seconds) if last_train_check is None or time.monotonic() > last_train_check + 15: trains = get_trains(station_code, historical_trains) diff --git a/display_manager.py b/display_manager.py index 626b359..3cceca5 100644 --- a/display_manager.py +++ b/display_manager.py @@ -1,4 +1,6 @@ # Display Manager +# Structure and some code from Weather Display Matrix project: +#https://learn.adafruit.com/weather-display-matrix/code-the-weather-display-matrix import time import displayio @@ -6,6 +8,15 @@ from adafruit_display_text.label import Label from adafruit_bitmap_font import bitmap_font +cwd = ("/" + __file__).rsplit("/", 1)[0] + +icon_spritesheet = cwd + "/bmp/weather-icons.bmp" +icon_width = 16 +icon_height = 16 + +# custom font for temperature trend indicators +symbol_font = bitmap_font.load_font("/bdf/trend_icons.bdf") + # custom colors hex codes metro_orange=0xf06a37 metro_red=0xda1b30 @@ -22,30 +33,92 @@ def __init__( self.root_group = displayio.Group() self.root_group.append(self) + # create parent weather group for weather display groups + self._weather_group = displayio.Group() + self._weather_group.hidden = False + self.append(self._weather_group) + + # create current weather icon group + self._icon_group = displayio.Group(x=4, y=2) + self._weather_group.append(self._icon_group) + + # create current temperature group + self._current_temp_group = displayio.Group() + self._weather_group.append(self._current_temp_group) + + # create temperature trend symbol group + self._temp_trend_group = displayio.Group() + self._weather_group.append(self._temp_trend_group) + + # create min max temperature group + self._min_max_temp_group = displayio.Group() + self._weather_group.append(self._min_max_temp_group) + # create parent train group for train Labels self._train_board_group = displayio.Group() self._train_board_group.hidden = False self.append(self._train_board_group) # set default column and row measurements - self.col1=16 - self.col2=96 + self.col1=4 + self.col2=28 + self.col25=52 + self.col3=108 self.row1=8 self.row2=24 + # Load the icon sprite sheet + icons = displayio.OnDiskBitmap(open(icon_spritesheet, "rb")) + self._icon_sprite = displayio.TileGrid( + icons, + pixel_shader=getattr(icons, 'pixel_shader', displayio.ColorConverter()), + tile_width=icon_width, + tile_height=icon_height + ) + + # set current temperature text + # left-middle column, top row + self.temp_text = Label(terminalio.FONT) + self.temp_text.x = self.col2 - 4 + self.temp_text.y = self.row1 + self.temp_text.color = 0xFFFFFF + self._current_temp_group.append(self.temp_text) + + # set current temperature trend icon + self.temp_trend_icon = Label(font=symbol_font) + self.temp_trend_icon.x = self.col2 + 8 + self.temp_trend_icon.y = self.row1 - 7 + self._temp_trend_group.append(self.temp_trend_icon) + + # set daily minimum temperature text + # left column, bottom row + self.min_temp_text = Label(terminalio.FONT) + self.min_temp_text.x = self.col1 + 2 + self.min_temp_text.y = self.row2 + self.min_temp_text.color = 0x1e81b0 + self._min_max_temp_group.append(self.min_temp_text) + + # set daily maximum temperature text + # left-middle column, bottom row + self.max_temp_text = Label(terminalio.FONT) + self.max_temp_text.x = self.col2 - 4 + self.max_temp_text.y = self.row2 + self.max_temp_text.color = metro_red + self._min_max_temp_group.append(self.max_temp_text) + # set top row of train destination text # right-middle column, top row self.top_row_train_text = Label(terminalio.FONT) - self.top_row_train_text.x = self.col1 + self.top_row_train_text.x = self.col25 self.top_row_train_text.y = self.row1 self.top_row_train_text.color = metro_orange - self.top_row_train_text.text = "Shady Grove" + self.top_row_train_text.text = "Shady Gr" self._train_board_group.append(self.top_row_train_text) # set top row of train time to arrival text # right column, top row self.top_row_train_min = Label(terminalio.FONT) - self.top_row_train_min.x = self.col2 + self.top_row_train_min.x = self.col3 self.top_row_train_min.y = self.row1 self.top_row_train_min.color = metro_orange self.top_row_train_min.text = "0" @@ -54,7 +127,7 @@ def __init__( # set bottom row of train destination text # right-middle column, bottom row self.bottom_row_train_text = Label(terminalio.FONT) - self.bottom_row_train_text.x = self.col1 + self.bottom_row_train_text.x = self.col25 self.bottom_row_train_text.y = self.row2 self.bottom_row_train_text.color = metro_orange self.bottom_row_train_text.text = "Glenmont" @@ -63,12 +136,37 @@ def __init__( # set bottom row of train time to arrival text # right column, bottom row self.bottom_row_train_min = Label(terminalio.FONT) - self.bottom_row_train_min.x = self.col2 + self.bottom_row_train_min.x = self.col3 self.bottom_row_train_min.y = self.row2 self.bottom_row_train_min.color = metro_orange self.bottom_row_train_min.text = "0" self._train_board_group.append(self.bottom_row_train_min) + # default icon set to none + self.set_icon(None) + + def set_icon(self, icon_name): + """Use icon_name to get the position of the sprite and update + the current icon. + :param icon_name: The icon name returned by openweathermap + Format is always 2 numbers followed by 'd' or 'n' as the 3rd character + """ + icon_map = ("01", "02", "03", "04", "09", "10", "11", "13", "50") + if self._icon_group: + self._icon_group.pop() + if icon_name is not None: + row = None + for index, icon in enumerate(icon_map): + if icon == icon_name[0:2]: + row = index + break + column = 0 + if icon_name[2] == "n": + column = 1 + if row is not None: + self._icon_sprite[0] = (row * 2) + column + self._icon_group.append(self._icon_sprite) + # helper function to assign color to minutes labels def get_minutes_color(self, minutes): try: @@ -80,12 +178,48 @@ def get_minutes_color(self, minutes): print("Value Error: {}".format(e)) return metro_orange + # update temperature text, trend, and max/min + # input is a weather dict + def update_weather(self, weather): + if weather: + # set the icon + self.set_icon(weather["icon"]) + + # set the temperature + self.temp_text.text = "%d" % weather["current_temp"] + self.min_temp_text.text = "%d" % weather["daily_temp_min"] + self.max_temp_text.text = "%d" % weather["daily_temp_max"] + + # set temperature trend + # if the temperature change is more than 1 degree + temp_diff_default = 1 + temp_diff = weather["hourly_next_temp"] - weather["current_temp"] + if temp_diff > 0 and temp_diff > temp_diff_default: + # comma is increase arrow + self.temp_trend_icon.text = "," + self.temp_trend_icon.color = metro_red + self.temp_trend_icon.y = self.row1 - 6 + self._temp_trend_group.hidden = False + + elif temp_diff < 0 and abs(temp_diff) > temp_diff_default: + # period is decrease arrow + self.temp_trend_icon.text = "." + self.temp_trend_icon.color = 0x1e81b0 + self.temp_trend_icon.y = self.row1 - 6 + self._temp_trend_group.hidden = False + + else: + self._temp_trend_group.hidden = True + else: + self.temp_text.text = "..." + # update train destination text and time to arrival # input is a list of train objects and display config integer + # TODO abstract default and error handling to support any station def assign_trains(self, trains, historical_trains): try: if trains[0] is not None: - self.top_row_train_text.text = trains[0].destination_name + self.top_row_train_text.text = trains[0].destination # if train isn't Shady Grove, set train text color to white if trains[0].destination_code is not "A15": @@ -99,14 +233,14 @@ def assign_trains(self, trains, historical_trains): # no A train data elif historical_trains[0] is not None: - self.top_row_train_text.text = historical_trains[0].destination_name + self.top_row_train_text.text = historical_trains[0].destination self.top_row_train_min.text = historical_trains[0].minutes self.top_row_train_min.color = 0xFFFFFF else: self.top_row_train_min.text = "NULL" if trains[1] is not None: - self.bottom_row_train_text.text = trains[1].destination_name + self.bottom_row_train_text.text = trains[1].destination # if train isn't Glenmont, set train text color to white if trains[1].destination_code is not "B11": @@ -120,7 +254,7 @@ def assign_trains(self, trains, historical_trains): # no B train data elif historical_trains[1] is not None: - self.bottom_row_train_text.text = historical_trains[1].destination_name + self.bottom_row_train_text.text = historical_trains[1].destination self.bottom_row_train_min.text = historical_trains[1].minutes self.bottom_row_train_min.color = 0xFFFFFF else: