From eb1c1c688f15116f1ff0a5934627238dbb7e9d34 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Mon, 21 Dec 2020 13:48:59 -0800 Subject: [PATCH 1/4] Ported over to use PortalBase --- adafruit_matrixportal/fakerequests.py | 49 --- adafruit_matrixportal/graphics.py | 71 +--- adafruit_matrixportal/matrix.py | 2 +- adafruit_matrixportal/matrixportal.py | 354 +++---------------- adafruit_matrixportal/network.py | 474 +------------------------- adafruit_matrixportal/wifi.py | 24 +- 6 files changed, 88 insertions(+), 886 deletions(-) delete mode 100755 adafruit_matrixportal/fakerequests.py diff --git a/adafruit_matrixportal/fakerequests.py b/adafruit_matrixportal/fakerequests.py deleted file mode 100755 index 8e12a9c..0000000 --- a/adafruit_matrixportal/fakerequests.py +++ /dev/null @@ -1,49 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense -""" -`adafruit_matrixportal.fakerequests` -================================================================================ - -Helper library for the Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. - -* Author(s): Melissa LeBlanc-Williams - -Implementation Notes --------------------- - -**Hardware:** - -* `Adafruit Metro M4 Express AirLift `_ -* `Adafruit RGB Matrix Shield `_ -* `64x32 RGB LED Matrix `_ - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - -""" - -import json - -__version__ = "0.0.0-auto.0" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MatrixPortal.git" - - -class Fake_Requests: - """For faking 'requests' using a local file instead of the network.""" - - def __init__(self, filename): - self._filename = filename - - def json(self): - """json parsed version for local requests.""" - with open(self._filename, "r") as file: - return json.load(file) - - @property - def text(self): - """raw text version for local requests.""" - with open(self._filename, "r") as file: - return file.read() diff --git a/adafruit_matrixportal/graphics.py b/adafruit_matrixportal/graphics.py index affeb9e..8a418b1 100755 --- a/adafruit_matrixportal/graphics.py +++ b/adafruit_matrixportal/graphics.py @@ -5,7 +5,7 @@ `adafruit_matrixportal.graphics` ================================================================================ -Helper library for the Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. +Helper library for the MatrixPortal M4 or Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. * Author(s): Melissa LeBlanc-Williams @@ -14,6 +14,7 @@ **Hardware:** +* `Adafruit MatrixPortal M4 `_ * `Adafruit Metro M4 Express AirLift `_ * `Adafruit RGB Matrix Shield `_ * `64x32 RGB LED Matrix `_ @@ -25,15 +26,14 @@ """ -import gc -import displayio +from adafruit_portalbase.graphics import GraphicsBase from adafruit_matrixportal.matrix import Matrix __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MatrixPortal.git" -class Graphics: +class Graphics(GraphicsBase): """Graphics Helper Class for the MatrixPortal Library :param default_bg: The path to your default background image file or a hex color. @@ -48,7 +48,7 @@ class Graphics: """ - # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements + # pylint: disable=too-few-public-methods def __init__( self, *, @@ -61,7 +61,6 @@ def __init__( debug=False ): - self._debug = debug matrix = Matrix( bit_depth=bit_depth, width=width, @@ -69,61 +68,5 @@ def __init__( alt_addr_pins=alt_addr_pins, color_order=color_order, ) - self.display = matrix.display - - if self._debug: - print("Init display") - self.splash = displayio.Group(max_size=15) - - if self._debug: - print("Init background") - self._bg_group = displayio.Group(max_size=1) - self._bg_file = None - self._default_bg = default_bg - self.splash.append(self._bg_group) - - # set the default background - self.set_background(self._default_bg) - self.display.show(self.splash) - - gc.collect() - - def set_background(self, file_or_color, position=None): - """The background image to a bitmap file. - - :param file_or_color: The filename of the chosen background image, or a hex color. - - """ - print("Set background to ", file_or_color) - while self._bg_group: - self._bg_group.pop() - - if not position: - position = (0, 0) # default in top corner - - if not file_or_color: - return # we're done, no background desired - if self._bg_file: - self._bg_file.close() - if isinstance(file_or_color, str): # its a filenme: - self._bg_file = open(file_or_color, "rb") - background = displayio.OnDiskBitmap(self._bg_file) - self._bg_sprite = displayio.TileGrid( - background, - pixel_shader=displayio.ColorConverter(), - x=position[0], - y=position[1], - ) - elif isinstance(file_or_color, int): - # Make a background color fill - color_bitmap = displayio.Bitmap(self.display.width, self.display.height, 1) - color_palette = displayio.Palette(1) - color_palette[0] = file_or_color - self._bg_sprite = displayio.TileGrid( - color_bitmap, pixel_shader=color_palette, x=position[0], y=position[1], - ) - else: - raise RuntimeError("Unknown type of background") - self._bg_group.append(self._bg_sprite) - self.display.refresh() - gc.collect() + + super().__init__(matrix.display, default_bg=default_bg, debug=debug) diff --git a/adafruit_matrixportal/matrix.py b/adafruit_matrixportal/matrix.py index 99d22f0..7f9728d 100755 --- a/adafruit_matrixportal/matrix.py +++ b/adafruit_matrixportal/matrix.py @@ -5,7 +5,7 @@ `adafruit_matrixportal.matrix` ================================================================================ -Helper library for the Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. +Helper library for the MatrixPortal M4 or Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. * Author(s): Melissa LeBlanc-Williams diff --git a/adafruit_matrixportal/matrixportal.py b/adafruit_matrixportal/matrixportal.py index 0b86d99..2e515b5 100755 --- a/adafruit_matrixportal/matrixportal.py +++ b/adafruit_matrixportal/matrixportal.py @@ -5,7 +5,7 @@ `adafruit_matrixportal.matrixportal` ================================================================================ -Helper library for the Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. +Helper library for the MatrixPortal M4 or Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. * Author(s): Melissa LeBlanc-Williams @@ -14,6 +14,7 @@ **Hardware:** +* `Adafruit MatrixPortal M4 `_ * `Adafruit Metro M4 Express AirLift `_ * `Adafruit RGB Matrix Shield `_ * `64x32 RGB LED Matrix `_ @@ -28,8 +29,7 @@ import gc from time import sleep import terminalio -from adafruit_bitmap_font import bitmap_font -from adafruit_display_text.label import Label +from adafruit_portalbase import PortalBase from adafruit_matrixportal.network import Network from adafruit_matrixportal.graphics import Graphics @@ -37,7 +37,7 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MatrixPortal.git" -class MatrixPortal: +class MatrixPortal(PortalBase): """Class representing the Adafruit RGB Matrix Portal. :param url: The URL of your data source. Defaults to ``None``. @@ -64,7 +64,7 @@ class MatrixPortal: """ - # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements + # pylint: disable=too-many-locals def __init__( self, *, @@ -85,8 +85,7 @@ def __init__( height=32, ): - self._debug = debug - self.graphics = Graphics( + graphics = Graphics( default_bg=default_bg, bit_depth=bit_depth, width=width, @@ -95,9 +94,8 @@ def __init__( color_order=color_order, debug=debug, ) - self.display = self.graphics.display - self.network = Network( + network = Network( status_neopixel=status_neopixel, esp=esp, external_spi=external_spi, @@ -105,31 +103,18 @@ def __init__( debug=debug, ) - self._url = None - self.url = url - self._headers = headers - self._json_path = None - self.json_path = json_path - - self._regexp_path = regexp_path - - self.splash = self.graphics.splash - - # Add any JSON translators - if json_transform: - self.network.add_json_transform(json_transform) + super().__init__( + network, + graphics, + url=url, + headers=headers, + json_path=json_path, + regexp_path=regexp_path, + json_transform=json_transform, + debug=debug, + ) - self._text = [] - self._text_color = [] - self._text_position = [] - self._text_wrap = [] - self._text_maxlen = [] - self._text_transform = [] - self._text_scrolling = [] - self._text_scale = [] self._scrolling_index = None - self._text_font = [] - self._text_line_spacing = [] gc.collect() @@ -145,6 +130,8 @@ def add_text( text_scale=1, scrolling=False, line_spacing=1.25, + text_anchor_point=(0, 0.5), + is_data=True, ): """ Add text labels with settings @@ -163,21 +150,10 @@ def add_text( :param bool scrolling: If true, text is placed offscreen and the scroll() function is used to scroll text on a pixel-by-pixel basis. Multiple text labels with the scrolling set to True will be cycled through. - + :param (float, float) text_anchor_point: Values between 0 and 1 to indicate where the text + position is relative to the label + :param bool is_data: If True, fetch will attempt to update the label """ - if text_font is terminalio.FONT: - self._text_font.append(text_font) - else: - self._text_font.append(bitmap_font.load_font(text_font)) - if not text_wrap: - text_wrap = 0 - if not text_maxlen: - text_maxlen = 0 - if not text_transform: - text_transform = None - if not isinstance(text_scale, (int, float)) or text_scale < 1: - text_scale = 1 - text_scale = round(text_scale) if scrolling: if text_position is None: # Center text if position not specified @@ -185,46 +161,27 @@ def add_text( else: text_position = (self.display.width, text_position[1]) - gc.collect() + index = super().add_text( + text_position=text_position, + text_font=text_font, + text_color=text_color, + text_wrap=text_wrap, + text_maxlen=text_maxlen, + text_transform=text_transform, + text_scale=text_scale, + line_spacing=line_spacing, + text_anchor_point=text_anchor_point, + is_data=is_data, + ) - if self._debug: - print("Init text area") - self._text.append(None) - self._text_color.append(self.html_color_convert(text_color)) - self._text_position.append(text_position) - self._text_wrap.append(text_wrap) - self._text_maxlen.append(text_maxlen) - self._text_transform.append(text_transform) - self._text_scale.append(text_scale) - self._text_scrolling.append(scrolling) - self._text_line_spacing.append(line_spacing) + self._text[index]["scrolling"] = scrolling if scrolling and self._scrolling_index is None: # Not initialized yet self._scrolling_index = self._get_next_scrollable_text_index() - return len(self._text) - 1 - # pylint: enable=too-many-arguments - - @staticmethod - def html_color_convert(color): - """Convert an HTML color code to an integer - - :param color: The color value to be converted - - """ - if isinstance(color, str): - if color[0] == "#": - color = color.lstrip("#") - return int(color, 16) - return color # Return unconverted - - def set_headers(self, headers): - """Set the headers used by fetch(). + return index - :param headers: The new header dictionary - - """ - self._headers = headers + # pylint: enable=too-many-arguments def set_background(self, file_or_color, position=None): """The background image to a bitmap file. @@ -234,84 +191,6 @@ def set_background(self, file_or_color, position=None): """ self.graphics.set_background(file_or_color, position) - def preload_font(self, glyphs=None, index=0): - # pylint: disable=line-too-long - """Preload font. - - :param glyphs: The font glyphs to load. Defaults to ``None``, uses alphanumeric glyphs if - None. - """ - # pylint: enable=line-too-long - if not glyphs: - glyphs = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-!,. \"'?!" - print("Preloading font glyphs:", glyphs) - if self._text_font[index] is not terminalio.FONT: - self._text_font[index].load_glyphs(glyphs) - - def set_text_color(self, color, index=0): - """Update the text color, with indexing into our list of text boxes. - - :param int color: The color value to be used - :param index: Defaults to 0. - - """ - if 0 <= index < len(self._text_color): - self._text_color[index] = self.html_color_convert(color) - if self._text[index] is not None: - self._text[index].color = self._text_color[index] - else: - raise IndexError( - "index {} is out of bounds. Please call add_text() and set_text() first.".format( - index - ) - ) - - def set_text(self, val, index=0): - """Display text, with indexing into our list of text boxes. - - :param str val: The text to be displayed - :param index: Defaults to 0. - - """ - # Make sure at least a single label exists - if not self._text: - self.add_text() - string = str(val) - if self._text_maxlen[index]: - string = string[: self._text_maxlen[index]] - print("text index", self._text[index]) - index_in_splash = None - - if self._text[index] is not None: - if self._debug: - print("Replacing text area with :", string) - index_in_splash = self.splash.index(self._text[index]) - elif self._debug: - print("Creating text area with :", string) - - if len(string) > 0: - self._text[index] = Label( - self._text_font[index], text=string, scale=self._text_scale[index] - ) - self._text[index].color = self._text_color[index] - self._text[index].x = self._text_position[index][0] - self._text[index].y = self._text_position[index][1] - self._text[index].line_spacing = self._text_line_spacing[index] - elif index_in_splash is not None: - self._text[index] = None - - if index_in_splash is not None: - if self._text[index] is not None: - self.splash[index_in_splash] = self._text[index] - else: - del self.splash[index_in_splash] - elif self._text[index] is not None: - self.splash.append(self._text[index]) - - def get_local_time(self, location=None): - """Accessor function for get_local_time()""" - return self.network.get_local_time(location=location) - def _get_next_scrollable_text_index(self): index = self._scrolling_index while True: @@ -319,49 +198,13 @@ def _get_next_scrollable_text_index(self): index = 0 else: index += 1 - if index >= len(self._text_scrolling): + if index >= len(self._text): index = 0 - if self._text_scrolling[index]: + if self._text[index]["scrolling"]: return index if index == self._scrolling_index: return None - def push_to_io(self, feed_key, data): - """Push data to an adafruit.io feed - - :param str feed_key: Name of feed key to push data to. - :param data: data to send to feed - - """ - - self.network.push_to_io(feed_key, data) - - def get_io_data(self, feed_key): - """Return all values from the Adafruit IO Feed Data that matches the feed key - - :param str feed_key: Name of feed key to receive data from. - - """ - - return self.network.get_io_data(feed_key) - - def get_io_feed(self, feed_key, detailed=False): - """Return the Adafruit IO Feed that matches the feed key - - :param str feed_key: Name of feed key to match. - :param bool detailed: Whether to return additional detailed information - - """ - return self.network.get_io_feed(feed_key, detailed) - - def get_io_group(self, group_key): - """Return the Adafruit IO Group that matches the group key - - :param str group_key: Name of group key to match. - - """ - return self.network.get_io_group(group_key) - def scroll(self): """Scroll any text that needs scrolling by a single frame. We also we want to queue up multiple lines one after another. To get @@ -371,15 +214,17 @@ def scroll(self): if self._scrolling_index is None: # Not initialized yet return - self._text[self._scrolling_index].x = self._text[self._scrolling_index].x - 1 + self._text[self._scrolling_index]["label"].x = ( + self._text[self._scrolling_index]["label"].x - 1 + ) line_width = ( - self._text[self._scrolling_index].bounding_box[2] - * self._text_scale[self._scrolling_index] + self._text[self._scrolling_index]["label"].bounding_box[2] + * self._text[self._scrolling_index]["scale"] ) - if self._text[self._scrolling_index].x < -line_width: + if self._text[self._scrolling_index]["label"].x < -line_width: # Find the next line self._scrolling_index = self._get_next_scrollable_text_index() - self._text[self._scrolling_index].x = self.graphics.display.width + self._text[self._scrolling_index]["label"].x = self.graphics.display.width def scroll_text(self, frame_delay=0.02): """Scroll the entire text all the way across. We also @@ -388,11 +233,11 @@ def scroll_text(self, frame_delay=0.02): """ if self._scrolling_index is None: # Not initialized yet return - if self._text[self._scrolling_index] is not None: - self._text[self._scrolling_index].x = self.graphics.display.width + if self._text[self._scrolling_index]["label"] is not None: + self._text[self._scrolling_index]["label"].x = self.graphics.display.width line_width = ( - self._text[self._scrolling_index].bounding_box[2] - * self._text_scale[self._scrolling_index] + self._text[self._scrolling_index]["label"].bounding_box[2] + * self._text[self._scrolling_index]["scale"] ) for _ in range(self.graphics.display.width + line_width + 1): self.scroll() @@ -403,102 +248,3 @@ def scroll_text(self, frame_delay=0.02): self._scrolling_index ) ) - - def fetch(self, refresh_url=None, timeout=10): - """Fetch data from the url we initialized with, perfom any parsing, - and display text or graphics. This function does pretty much everything - Optionally update the URL - """ - if refresh_url: - self._url = refresh_url - values = [] - - values = self.network.fetch_data( - self._url, - headers=self._headers, - json_path=self._json_path, - regexp_path=self._regexp_path, - timeout=timeout, - ) - - # fill out all the text blocks - if self._text: - for i in range(len(self._text)): - string = None - if self._text_transform[i]: - func = self._text_transform[i] - string = func(values[i]) - else: - try: - string = "{:,d}".format(int(values[i])) - except (TypeError, ValueError): - string = values[i] # ok its a string - if self._debug: - print("Drawing text", string) - if self._text_wrap[i]: - if self._debug: - print("Wrapping text") - lines = self.wrap_nicely(string, self._text_wrap[i]) - string = "\n".join(lines) - self.set_text(string, index=i) - if len(values) == 1: - return values[0] - return values - - # return a list of lines with wordwrapping - @staticmethod - def wrap_nicely(string, max_chars): - """A helper that will return a list of lines with word-break wrapping. - - :param str string: The text to be wrapped. - :param int max_chars: The maximum number of characters on a line before wrapping. - - """ - string = string.replace("\n", "").replace("\r", "") # strip confusing newlines - words = string.split(" ") - the_lines = [] - the_line = "" - for w in words: - if len(the_line + " " + w) <= max_chars: - the_line += " " + w - else: - the_lines.append(the_line) - the_line = "" + w - if the_line: # last line remaining - the_lines.append(the_line) - # remove first space from first line: - the_lines[0] = the_lines[0][1:] - return the_lines - - @property - def url(self): - """ - Get or set the URL of your data source. - """ - return self._json_path - - @url.setter - def url(self, value): - self._url = value - if value and not self.network.uselocal: - self.network.connect() - if self._debug: - print("My IP address is", self.network.ip_address) - - @property - def json_path(self): - """ - Get or set the list of json traversal to get data out of. Can be list - of lists for multiple data points. - """ - return self._json_path - - @json_path.setter - def json_path(self, value): - if value: - if isinstance(value[0], (list, tuple)): - self._json_path = value - else: - self._json_path = (value,) - else: - self._json_path = None diff --git a/adafruit_matrixportal/network.py b/adafruit_matrixportal/network.py index cdde99c..de71ee9 100755 --- a/adafruit_matrixportal/network.py +++ b/adafruit_matrixportal/network.py @@ -5,7 +5,7 @@ `adafruit_matrixportal.network` ================================================================================ -Helper library for the Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. +Helper library for the MatrixPortal M4 or Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. * Author(s): Melissa LeBlanc-Williams @@ -14,6 +14,7 @@ **Hardware:** +* `Adafruit MatrixPortal M4 `_ * `Adafruit Metro M4 Express AirLift `_ * `Adafruit RGB Matrix Shield `_ * `64x32 RGB LED Matrix `_ @@ -25,62 +26,15 @@ """ -import os -import time import gc -from micropython import const -import adafruit_esp32spi.adafruit_esp32spi_socket as socket -from adafruit_io.adafruit_io import IO_HTTP, AdafruitIO_RequestError -import adafruit_requests as requests -import supervisor -import rtc +from adafruit_portalbase.network import NetworkBase from adafruit_matrixportal.wifi import WiFi -from adafruit_matrixportal.fakerequests import Fake_Requests - -try: - from secrets import secrets -except ImportError: - print( - """WiFi settings are kept in secrets.py, please add them there! -the secrets dictionary must contain 'ssid' and 'password' at a minimum""" - ) - raise __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MatrixPortal.git" -# pylint: disable=line-too-long -# pylint: disable=too-many-lines -# you'll need to pass in an io username and key -TIME_SERVICE = ( - "https://io.adafruit.com/api/v2/%s/integrations/time/strftime?x-aio-key=%s" -) -# our strftime is %Y-%m-%d %H:%M:%S.%L %j %u %z %Z see http://strftime.net/ for decoding details -# See https://apidock.com/ruby/DateTime/strftime for full options -TIME_SERVICE_STRFTIME = ( - "&fmt=%25Y-%25m-%25d+%25H%3A%25M%3A%25S.%25L+%25j+%25u+%25z+%25Z" -) -LOCALFILE = "local.txt" -# pylint: enable=line-too-long - -STATUS_NO_CONNECTION = (100, 0, 0) -STATUS_CONNECTING = (0, 0, 100) -STATUS_FETCHING = (200, 100, 0) -STATUS_DOWNLOADING = (0, 100, 100) -STATUS_CONNECTED = (0, 100, 0) -STATUS_DATA_RECEIVED = (0, 0, 100) -STATUS_OFF = (0, 0, 0) - -CONTENT_TEXT = const(1) -CONTENT_JSON = const(2) -CONTENT_IMAGE = const(3) - - -class HttpError(Exception): - """HTTP Specific Error""" - -class Network: +class Network(NetworkBase): """Class representing the Adafruit RGB Matrix Portal. :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board @@ -94,7 +48,6 @@ class Network: """ - # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements def __init__( self, *, @@ -104,427 +57,14 @@ def __init__( extract_values=True, debug=False, ): - self._wifi = WiFi( - status_neopixel=status_neopixel, esp=esp, external_spi=external_spi - ) - self._debug = debug - self.json_transform = [] - self._extract_values = extract_values - - try: - os.stat(LOCALFILE) - self.uselocal = True - except OSError: - self.uselocal = False - - requests.set_socket(socket, self._wifi.esp) - - gc.collect() - - def neo_status(self, value): - """The status NeoPixel. - - :param value: The color to change the NeoPixel. - - """ - self._wifi.neo_status(value) - - @staticmethod - def json_traverse(json, path): - """ - Traverse down the specified JSON path and return the value or values - - :param json: JSON data to traverse - :param list path: The path that we want to follow - - """ - value = json - if not isinstance(path, (list, tuple)): - raise ValueError( - "The json_path parameter should be enclosed in a list or tuple." - ) - for x in path: - value = value[x] - gc.collect() - return value - - def add_json_transform(self, json_transform): - """Add a function that is applied to JSON data when data is fetched + wifi = WiFi(status_neopixel=status_neopixel, esp=esp, external_spi=external_spi) - :param json_transform: A function or a list of functions to call with the parsed JSON. - Changes and additions are permitted for the ``dict`` object. - """ - if callable(json_transform): - self.json_transform.append(json_transform) - else: - self.json_transform.extend(filter(callable, json_transform)) - - def get_local_time(self, location=None): - # pylint: disable=line-too-long - """Fetch and "set" the local time of this microcontroller to the local time at the location, using an internet time API. - - :param str location: Your city and country, e.g. ``"New York, US"``. - - """ - # pylint: enable=line-too-long - self.connect() - api_url = None - try: - aio_username = secrets["aio_username"] - aio_key = secrets["aio_key"] - except KeyError: - raise KeyError( - "\n\nOur time service requires a login/password to rate-limit. Please register for a free adafruit.io account and place the user/key in your secrets file under 'aio_username' and 'aio_key'" # pylint: disable=line-too-long - ) from KeyError - - if location is None: - location = secrets.get("timezone") - if location: - print("Getting time for timezone", location) - api_url = (TIME_SERVICE + "&tz=%s") % (aio_username, aio_key, location) - else: # we'll try to figure it out from the IP address - print("Getting time from IP address") - api_url = TIME_SERVICE % (aio_username, aio_key) - api_url += TIME_SERVICE_STRFTIME - try: - response = requests.get(api_url, timeout=10) - if response.status_code != 200: - error_message = ( - "Error connection to Adafruit IO. The response was: " - + response.text - ) - raise ValueError(error_message) - if self._debug: - print("Time request: ", api_url) - print("Time reply: ", response.text) - times = response.text.split(" ") - the_date = times[0] - the_time = times[1] - year_day = int(times[2]) - week_day = int(times[3]) - is_dst = None # no way to know yet - except KeyError: - raise KeyError( - "Was unable to lookup the time, try setting secrets['timezone'] according to http://worldtimeapi.org/timezones" # pylint: disable=line-too-long - ) from KeyError - year, month, mday = [int(x) for x in the_date.split("-")] - the_time = the_time.split(".")[0] - hours, minutes, seconds = [int(x) for x in the_time.split(":")] - now = time.struct_time( - (year, month, mday, hours, minutes, seconds, week_day, year_day, is_dst) + super().__init__( + wifi, extract_values=extract_values, debug=debug, ) - rtc.RTC().datetime = now - # now clean up - response.close() - response = None gc.collect() - def wget(self, url, filename, *, chunk_size=12000): - """Download a url and save to filename location, like the command wget. - - :param url: The URL from which to obtain the data. - :param filename: The name of the file to save the data to. - :param chunk_size: how much data to read/write at a time. - - """ - print("Fetching stream from", url) - - self.neo_status(STATUS_FETCHING) - response = requests.get(url, stream=True) - - headers = {} - for title, content in response.headers.items(): - headers[title.lower()] = content - - if response.status_code == 200: - print("Reply is OK!") - self.neo_status((0, 0, 100)) # green = got data - else: - if self._debug: - if "content-length" in headers: - print("Content-Length: {}".format(int(headers["content-length"]))) - if "date" in headers: - print("Date: {}".format(headers["date"])) - self.neo_status((100, 0, 0)) # red = http error - raise HttpError( - "Code {}: {}".format( - response.status_code, response.reason.decode("utf-8") - ) - ) - - if self._debug: - print(response.headers) - if "content-length" in headers: - content_length = int(headers["content-length"]) - else: - raise RuntimeError("Content-Length missing from headers") - remaining = content_length - print("Saving data to ", filename) - stamp = time.monotonic() - file = open(filename, "wb") - for i in response.iter_content(min(remaining, chunk_size)): # huge chunks! - self.neo_status(STATUS_DOWNLOADING) - remaining -= len(i) - file.write(i) - if self._debug: - print( - "Read %d bytes, %d remaining" - % (content_length - remaining, remaining) - ) - else: - print(".", end="") - if not remaining: - break - self.neo_status(STATUS_FETCHING) - file.close() - - response.close() - stamp = time.monotonic() - stamp - print( - "Created file of %d bytes in %0.1f seconds" % (os.stat(filename)[6], stamp) - ) - self.neo_status(STATUS_OFF) - if not content_length == os.stat(filename)[6]: - raise RuntimeError - - def connect(self): - """ - Connect to WiFi using the settings found in secrets.py - """ - self._wifi.neo_status(STATUS_CONNECTING) - while not self._wifi.esp.is_connected: - # secrets dictionary must contain 'ssid' and 'password' at a minimum - print("Connecting to AP", secrets["ssid"]) - if secrets["ssid"] == "CHANGE ME" or secrets["password"] == "CHANGE ME": - change_me = "\n" + "*" * 45 - change_me += "\nPlease update the 'secrets.py' file on your\n" - change_me += "CIRCUITPY drive to include your local WiFi\n" - change_me += "access point SSID name in 'ssid' and SSID\n" - change_me += "password in 'password'. Then save to reload!\n" - change_me += "*" * 45 - raise OSError(change_me) - self._wifi.neo_status(STATUS_NO_CONNECTION) # red = not connected - try: - self._wifi.esp.connect(secrets) - except RuntimeError as error: - print("Could not connect to internet", error) - print("Retrying in 3 seconds...") - time.sleep(3) - - def _get_io_client(self): - self.connect() - - try: - aio_username = secrets["aio_username"] - aio_key = secrets["aio_key"] - except KeyError: - raise KeyError( - "Adafruit IO secrets are kept in secrets.py, please add them there!\n\n" - ) from KeyError - - return IO_HTTP(aio_username, aio_key, requests) - - def push_to_io(self, feed_key, data): - """Push data to an adafruit.io feed - - :param str feed_key: Name of feed key to push data to. - :param data: data to send to feed - - """ - - io_client = self._get_io_client() - - while True: - try: - feed_id = io_client.get_feed(feed_key) - except AdafruitIO_RequestError: - # If no feed exists, create one - feed_id = io_client.create_new_feed(feed_key) - except RuntimeError as exception: - print("An error occured, retrying! 1 -", exception) - continue - break - - while True: - try: - io_client.send_data(feed_id["key"], data) - except RuntimeError as exception: - print("An error occured, retrying! 2 -", exception) - continue - except NameError as exception: - print(feed_id["key"], data, exception) - continue - break - - def get_io_feed(self, feed_key, detailed=False): - """Return the Adafruit IO Feed that matches the feed key - - :param str feed_key: Name of feed key to match. - :param bool detailed: Whether to return additional detailed information - - """ - io_client = self._get_io_client() - - while True: - try: - return io_client.get_feed(feed_key, detailed=detailed) - except RuntimeError as exception: - print("An error occured, retrying! 1 -", exception) - continue - break - - def get_io_group(self, group_key): - """Return the Adafruit IO Group that matches the group key - - :param str group_key: Name of group key to match. - - """ - io_client = self._get_io_client() - - while True: - try: - return io_client.get_group(group_key) - except RuntimeError as exception: - print("An error occured, retrying! 1 -", exception) - continue - break - - def get_io_data(self, feed_key): - """Return all values from Adafruit IO Feed Data that matches the feed key - - :param str feed_key: Name of feed key to receive data from. - - """ - io_client = self._get_io_client() - - while True: - try: - return io_client.receive_all_data(feed_key) - except RuntimeError as exception: - print("An error occured, retrying! 1 -", exception) - continue - break - - def fetch(self, url, *, headers=None, timeout=10): - """Fetch data from the specified url and return a response object""" - gc.collect() - if self._debug: - print("Free mem: ", gc.mem_free()) # pylint: disable=no-member - - response = None - if self.uselocal: - print("*** USING LOCALFILE FOR DATA - NOT INTERNET!!! ***") - response = Fake_Requests(LOCALFILE) - - if not response: - self.connect() - # great, lets get the data - print("Retrieving data...", end="") - self.neo_status(STATUS_FETCHING) # yellow = fetching data - gc.collect() - response = requests.get(url, headers=headers, timeout=timeout) - gc.collect() - - return response - - def fetch_data( - self, url, *, headers=None, json_path=None, regexp_path=None, timeout=10, - ): - """Fetch data from the specified url and perfom any parsing""" - json_out = None - values = [] - content_type = CONTENT_TEXT - - response = self.fetch(url, headers=headers, timeout=timeout) - - headers = {} - for title, content in response.headers.items(): - headers[title.lower()] = content - gc.collect() - if self._debug: - print("Headers:", headers) - if response.status_code == 200: - print("Reply is OK!") - self.neo_status(STATUS_DATA_RECEIVED) # green = got data - if "content-type" in headers: - if "image/" in headers["content-type"]: - content_type = CONTENT_IMAGE - elif "application/json" in headers["content-type"]: - content_type = CONTENT_JSON - elif "application/javascript" in headers["content-type"]: - content_type = CONTENT_JSON - else: - if self._debug: - if "content-length" in headers: - print("Content-Length: {}".format(int(headers["content-length"]))) - if "date" in headers: - print("Date: {}".format(headers["date"])) - self.neo_status((100, 0, 0)) # red = http error - raise HttpError( - "Code {}: {}".format( - response.status_code, response.reason.decode("utf-8") - ) - ) - - if content_type == CONTENT_JSON and json_path is not None: - if isinstance(json_path, (list, tuple)) and ( - not json_path or not isinstance(json_path[0], (list, tuple)) - ): - json_path = (json_path,) - try: - gc.collect() - json_out = response.json() - if self._debug: - print(json_out) - gc.collect() - except ValueError: # failed to parse? - print("Couldn't parse json: ", response.text) - raise - except MemoryError: - supervisor.reload() - - if regexp_path: - import re # pylint: disable=import-outside-toplevel - - # optional JSON post processing, apply any transformations - # these MAY change/add element - for idx, json_transform in enumerate(self.json_transform): - try: - json_transform(json_out) - except Exception as error: - print("Exception from json_transform: ", idx, error) - raise - - # extract desired text/values from json - if json_out and json_path: - for path in json_path: - try: - values.append(self.json_traverse(json_out, path)) - except KeyError: - print(json_out) - raise - elif content_type == CONTENT_TEXT and regexp_path: - for regexp in regexp_path: - values.append(re.search(regexp, response.text).group(1)) - else: - if json_out: - # No path given, so return JSON as string for compatibility - import json # pylint: disable=import-outside-toplevel - - values = json.dumps(response.json()) - else: - values = response.text - - # we're done with the requests object, lets delete it so we can do more! - json_out = None - response = None - gc.collect() - if self._extract_values and len(values) == 1: - return values[0] - - return values - @property def ip_address(self): """Return the IP Address nicely formatted""" diff --git a/adafruit_matrixportal/wifi.py b/adafruit_matrixportal/wifi.py index ec97b70..e9411a9 100755 --- a/adafruit_matrixportal/wifi.py +++ b/adafruit_matrixportal/wifi.py @@ -5,7 +5,7 @@ `adafruit_matrixportal.wifi` ================================================================================ -Helper library for the Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. +Helper library for the MatrixPortal M4 or Adafruit RGB Matrix Shield + Metro M4 Airlift Lite. * Author(s): Melissa LeBlanc-Williams @@ -14,6 +14,7 @@ **Hardware:** +* `Adafruit MatrixPortal M4 `_ * `Adafruit Metro M4 Express AirLift `_ * `Adafruit RGB Matrix Shield `_ * `64x32 RGB LED Matrix `_ @@ -31,6 +32,8 @@ from digitalio import DigitalInOut import neopixel from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager +import adafruit_esp32spi.adafruit_esp32spi_socket as socket +import adafruit_requests as requests __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MatrixPortal.git" @@ -54,6 +57,7 @@ def __init__(self, *, status_neopixel=None, esp=None, external_spi=None): else: self.neopix = None self.neo_status(0) + self.requests = None if esp: # If there was a passed ESP Object self.esp = esp @@ -72,10 +76,18 @@ def __init__(self, *, status_neopixel=None, esp=None, external_spi=None): spi, esp32_cs, esp32_ready, esp32_reset, esp32_gpio0 ) + requests.set_socket(socket, self.esp) self._manager = None gc.collect() + def connect(self, ssid, password): + """ + Connect to WiFi using the settings found in secrets.py + """ + self.esp.connect({"ssid": ssid, "password": password}) + self.requests = requests + def neo_status(self, value): """The status NeoPixel. @@ -92,3 +104,13 @@ def manager(self, secrets): self.esp, secrets, None ) return self._manager + + @property + def is_connected(self): + """Return whether we are connected.""" + return self.esp.is_connected + + @property + def enabled(self): + """Not currently disablable on the ESP32 Coprocessor""" + return True From 5812775c5e8be2b86e586020360c84bbc6ab0110 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Mon, 21 Dec 2020 13:59:57 -0800 Subject: [PATCH 2/4] Removed fakerequests api doc link --- docs/api.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 363b644..97f7fa8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,8 +1,5 @@ -.. automodule:: adafruit_matrixportal.fakerequests - :members: - .. automodule:: adafruit_matrixportal.matrix :members: From e4bbc3f81e41e04c7ed1e20529bda526dffd2865 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Mon, 21 Dec 2020 14:09:23 -0800 Subject: [PATCH 3/4] Added portalbase as dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d4766b1..ff8852f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ Adafruit-Blinka adafruit-blinka-displayio +adafruit-circuitpython-portalbase adafruit-circuitpython-esp32spi adafruit-circuitpython-bitmap-font adafruit-circuitpython-display-text From ebacf7ecf23ef99f0b11de4a5b5518a5eab372dc Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Mon, 21 Dec 2020 14:14:02 -0800 Subject: [PATCH 4/4] Re-linted --- adafruit_matrixportal/matrixportal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_matrixportal/matrixportal.py b/adafruit_matrixportal/matrixportal.py index 2e515b5..fdb19d5 100755 --- a/adafruit_matrixportal/matrixportal.py +++ b/adafruit_matrixportal/matrixportal.py @@ -118,7 +118,7 @@ def __init__( gc.collect() - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, arguments-differ def add_text( self, text_position=None, @@ -181,7 +181,7 @@ def add_text( return index - # pylint: enable=too-many-arguments + # pylint: enable=too-many-arguments, arguments-differ def set_background(self, file_or_color, position=None): """The background image to a bitmap file.