diff --git a/.gitignore b/.gitignore index c83f8b7..2c6ddfd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,18 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + *.mpy .idea __pycache__ _build *.pyc .env +.python-version +build*/ bundles *.DS_Store .eggs dist -**/*.egg-info \ No newline at end of file +**/*.egg-info +.vscode diff --git a/.pylintrc b/.pylintrc index d8f0ee8..54a9d35 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + [MASTER] # A comma-separated list of package or module names from where C extensions may diff --git a/.readthedocs.yml b/.readthedocs.yml index f4243ad..a1e2575 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + python: version: 3 requirements_file: requirements.txt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 134d510..d885b36 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,3 +1,9 @@ + # Adafruit Community Code of Conduct ## Our Pledge @@ -43,7 +49,7 @@ Examples of unacceptable behavior by participants include: The goal of the standards and moderation guidelines outlined here is to build and maintain a respectful community. We ask that you don’t just aim to be -"technically unimpeachable", but rather try to be your best self. +"technically unimpeachable", but rather try to be your best self. We value many things beyond technical expertise, including collaboration and supporting others within our community. Providing a positive experience for @@ -74,9 +80,9 @@ You may report in the following ways: In any situation, you may send an email to . On the Adafruit Discord, you may send an open message from any channel -to all Community Moderators by tagging @community moderators. You may -also send an open message from any channel, or a direct message to -@kattni#1507, @tannewt#4653, @Dan Halbert#1614, @cater#2442, +to all Community Moderators by tagging @community moderators. You may +also send an open message from any channel, or a direct message to +@kattni#1507, @tannewt#4653, @danh#1614, @cater#2442, @sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. Email and direct message reports will be kept confidential. @@ -117,7 +123,7 @@ accordingly. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], +This Code of Conduct is adapted from the [Contributor Covenant], version 1.4, available at , and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). @@ -127,3 +133,5 @@ Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion. + +[Contributor Covenant]: https://www.contributor-covenant.org diff --git a/LICENSE b/LICENSE index e58c11a..aa6d192 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019 Limor Fried for Adafruit Industries +Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000..3f92dfc --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,324 @@ +Creative Commons Attribution 4.0 International Creative Commons Corporation +("Creative Commons") is not a law firm and does not provide legal services +or legal advice. Distribution of Creative Commons public licenses does not +create a lawyer-client or other relationship. Creative Commons makes its licenses +and related information available on an "as-is" basis. Creative Commons gives +no warranties regarding its licenses, any material licensed under their terms +and conditions, or any related information. Creative Commons disclaims all +liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions +that creators and other rights holders may use to share original works of +authorship and other material subject to copyright and certain other rights +specified in the public license below. The following considerations are for +informational purposes only, are not exhaustive, and do not form part of our +licenses. + +Considerations for licensors: Our public licenses are intended for use by +those authorized to give the public permission to use material in ways otherwise +restricted by copyright and certain other rights. Our licenses are irrevocable. +Licensors should read and understand the terms and conditions of the license +they choose before applying it. Licensors should also secure all rights necessary +before applying our licenses so that the public can reuse the material as +expected. Licensors should clearly mark any material not subject to the license. +This includes other CC-licensed material, or material used under an exception +or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors + +Considerations for the public: By using one of our public licenses, a licensor +grants the public permission to use the licensed material under specified +terms and conditions. If the licensor's permission is not necessary for any +reason–for example, because of any applicable exception or limitation to copyright–then +that use is not regulated by the license. Our licenses grant only permissions +under copyright and certain other rights that a licensor has authority to +grant. Use of the licensed material may still be restricted for other reasons, +including because others have copyright or other rights in the material. A +licensor may make special requests, such as asking that all changes be marked +or described. Although not required by our licenses, you are encouraged to +respect those requests where reasonable. More considerations for the public +: wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution +4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to +be bound by the terms and conditions of this Creative Commons Attribution +4.0 International Public License ("Public License"). To the extent this Public +License may be interpreted as a contract, You are granted the Licensed Rights +in consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the Licensor +receives from making the Licensed Material available under these terms and +conditions. + +Section 1 – Definitions. + +a. Adapted Material means material subject to Copyright and Similar Rights +that is derived from or based upon the Licensed Material and in which the +Licensed Material is translated, altered, arranged, transformed, or otherwise +modified in a manner requiring permission under the Copyright and Similar +Rights held by the Licensor. For purposes of this Public License, where the +Licensed Material is a musical work, performance, or sound recording, Adapted +Material is always produced where the Licensed Material is synched in timed +relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright and Similar +Rights in Your contributions to Adapted Material in accordance with the terms +and conditions of this Public License. + +c. Copyright and Similar Rights means copyright and/or similar rights closely +related to copyright including, without limitation, performance, broadcast, +sound recording, and Sui Generis Database Rights, without regard to how the +rights are labeled or categorized. For purposes of this Public License, the +rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +d. Effective Technological Measures means those measures that, in the absence +of proper authority, may not be circumvented under laws fulfilling obligations +under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, +and/or similar international agreements. + +e. Exceptions and Limitations means fair use, fair dealing, and/or any other +exception or limitation to Copyright and Similar Rights that applies to Your +use of the Licensed Material. + +f. Licensed Material means the artistic or literary work, database, or other +material to which the Licensor applied this Public License. + +g. Licensed Rights means the rights granted to You subject to the terms and +conditions of this Public License, which are limited to all Copyright and +Similar Rights that apply to Your use of the Licensed Material and that the +Licensor has authority to license. + +h. Licensor means the individual(s) or entity(ies) granting rights under this +Public License. + +i. Share means to provide material to the public by any means or process that +requires permission under the Licensed Rights, such as reproduction, public +display, public performance, distribution, dissemination, communication, or +importation, and to make material available to the public including in ways +that members of the public may access the material from a place and at a time +individually chosen by them. + +j. Sui Generis Database Rights means rights other than copyright resulting +from Directive 96/9/EC of the European Parliament and of the Council of 11 +March 1996 on the legal protection of databases, as amended and/or succeeded, +as well as other essentially equivalent rights anywhere in the world. + +k. You means the individual or entity exercising the Licensed Rights under +this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + +1. Subject to the terms and conditions of this Public License, the Licensor +hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, +irrevocable license to exercise the Licensed Rights in the Licensed Material +to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + +2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions +and Limitations apply to Your use, this Public License does not apply, and +You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + +4. Media and formats; technical modifications allowed. The Licensor authorizes +You to exercise the Licensed Rights in all media and formats whether now known +or hereafter created, and to make technical modifications necessary to do +so. The Licensor waives and/or agrees not to assert any right or authority +to forbid You from making technical modifications necessary to exercise the +Licensed Rights, including technical modifications necessary to circumvent +Effective Technological Measures. For purposes of this Public License, simply +making modifications authorized by this Section 2(a)(4) never produces Adapted +Material. + + 5. Downstream recipients. + +A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed +Material automatically receives an offer from the Licensor to exercise the +Licensed Rights under the terms and conditions of this Public License. + +B. No downstream restrictions. You may not offer or impose any additional +or different terms or conditions on, or apply any Effective Technological +Measures to, the Licensed Material if doing so restricts exercise of the Licensed +Rights by any recipient of the Licensed Material. + +6. No endorsement. Nothing in this Public License constitutes or may be construed +as permission to assert or imply that You are, or that Your use of the Licensed +Material is, connected with, or sponsored, endorsed, or granted official status +by, the Licensor or others designated to receive attribution as provided in +Section 3(a)(1)(A)(i). + + b. Other rights. + +1. Moral rights, such as the right of integrity, are not licensed under this +Public License, nor are publicity, privacy, and/or other similar personality +rights; however, to the extent possible, the Licensor waives and/or agrees +not to assert any such rights held by the Licensor to the limited extent necessary +to allow You to exercise the Licensed Rights, but not otherwise. + +2. Patent and trademark rights are not licensed under this Public License. + +3. To the extent possible, the Licensor waives any right to collect royalties +from You for the exercise of the Licensed Rights, whether directly or through +a collecting society under any voluntary or waivable statutory or compulsory +licensing scheme. In all other cases the Licensor expressly reserves any right +to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following +conditions. + + a. Attribution. + +1. If You Share the Licensed Material (including in modified form), You must: + +A. retain the following if it is supplied by the Licensor with the Licensed +Material: + +i. identification of the creator(s) of the Licensed Material and any others +designated to receive attribution, in any reasonable manner requested by the +Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + +v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + +B. indicate if You modified the Licensed Material and retain an indication +of any previous modifications; and + +C. indicate the Licensed Material is licensed under this Public License, and +include the text of, or the URI or hyperlink to, this Public License. + +2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner +based on the medium, means, and context in which You Share the Licensed Material. +For example, it may be reasonable to satisfy the conditions by providing a +URI or hyperlink to a resource that includes the required information. + +3. If requested by the Licensor, You must remove any of the information required +by Section 3(a)(1)(A) to the extent reasonably practicable. + +4. If You Share Adapted Material You produce, the Adapter's License You apply +must not prevent recipients of the Adapted Material from complying with this +Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to +Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, +reuse, reproduce, and Share all or a substantial portion of the contents of +the database; + +b. if You include all or a substantial portion of the database contents in +a database in which You have Sui Generis Database Rights, then the database +in which You have Sui Generis Database Rights (but not its individual contents) +is Adapted Material; and + +c. You must comply with the conditions in Section 3(a) if You Share all or +a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not replace +Your obligations under this Public License where the Licensed Rights include +other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. Unless otherwise separately undertaken by the Licensor, to the extent possible, +the Licensor offers the Licensed Material as-is and as-available, and makes +no representations or warranties of any kind concerning the Licensed Material, +whether express, implied, statutory, or other. This includes, without limitation, +warranties of title, merchantability, fitness for a particular purpose, non-infringement, +absence of latent or other defects, accuracy, or the presence or absence of +errors, whether or not known or discoverable. Where disclaimers of warranties +are not allowed in full or in part, this disclaimer may not apply to You. + +b. To the extent possible, in no event will the Licensor be liable to You +on any legal theory (including, without limitation, negligence) or otherwise +for any direct, special, indirect, incidental, consequential, punitive, exemplary, +or other losses, costs, expenses, or damages arising out of this Public License +or use of the Licensed Material, even if the Licensor has been advised of +the possibility of such losses, costs, expenses, or damages. Where a limitation +of liability is not allowed in full or in part, this limitation may not apply +to You. + +c. The disclaimer of warranties and limitation of liability provided above +shall be interpreted in a manner that, to the extent possible, most closely +approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights +licensed here. However, if You fail to comply with this Public License, then +Your rights under this Public License terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under Section +6(a), it reinstates: + +1. automatically as of the date the violation is cured, provided it is cured +within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + +c. For the avoidance of doubt, this Section 6(b) does not affect any right +the Licensor may have to seek remedies for Your violations of this Public +License. + +d. For the avoidance of doubt, the Licensor may also offer the Licensed Material +under separate terms or conditions or stop distributing the Licensed Material +at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or +conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the Licensed +Material not stated herein are separate from and independent of the terms +and conditions of this Public License. + +Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not +be interpreted to, reduce, limit, restrict, or impose conditions on any use +of the Licensed Material that could lawfully be made without permission under +this Public License. + +b. To the extent possible, if any provision of this Public License is deemed +unenforceable, it shall be automatically reformed to the minimum extent necessary +to make it enforceable. If the provision cannot be reformed, it shall be severed +from this Public License without affecting the enforceability of the remaining +terms and conditions. + +c. No term or condition of this Public License will be waived and no failure +to comply consented to unless expressly agreed to by the Licensor. + +d. Nothing in this Public License constitutes or may be interpreted as a limitation +upon, or waiver of, any privileges and immunities that apply to the Licensor +or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative +Commons may elect to apply one of its public licenses to material it publishes +and in those instances will be considered the "Licensor." The text of the +Creative Commons public licenses is dedicated to the public domain under the +CC0 Public Domain Dedication. Except for the limited purpose of indicating +that material is shared under a Creative Commons public license or as otherwise +permitted by the Creative Commons policies published at creativecommons.org/policies, +Creative Commons does not authorize the use of the trademark "Creative Commons" +or any other trademark or logo of Creative Commons without its prior written +consent including, without limitation, in connection with any unauthorized +modifications to any of its public licenses or any other arrangements, understandings, +or agreements concerning use of licensed material. For the avoidance of doubt, +this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..204b93d --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/Unlicense.txt b/LICENSES/Unlicense.txt new file mode 100644 index 0000000..24a8f90 --- /dev/null +++ b/LICENSES/Unlicense.txt @@ -0,0 +1,20 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute +this software, either in source code form or as a compiled binary, for any +purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and +to the detriment of our heirs and successors. We intend this dedication to +be an overt act of relinquishment in perpetuity of all present and future +rights to this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH +THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, +please refer to diff --git a/README.rst.license b/README.rst.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/README.rst.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/adafruit_pyportal.py b/adafruit_pyportal.py deleted file mode 100755 index 33a656d..0000000 --- a/adafruit_pyportal.py +++ /dev/null @@ -1,1225 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2019 Limor Fried for Adafruit Industries, Kevin J. Walters -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -""" -`adafruit_pyportal` -================================================================================ - -CircuitPython driver for Adafruit PyPortal. - - -* Author(s): Limor Fried, Kevin J. Walters, Melissa LeBlanc-Williams - -Implementation Notes --------------------- - -**Hardware:** - -* `Adafruit PyPortal `_ - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - -* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice -""" - -import os -import time -import gc -from micropython import const -import board -import busio -from digitalio import DigitalInOut -import pulseio -import neopixel -from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager -import adafruit_esp32spi.adafruit_esp32spi_socket as socket -from adafruit_bitmap_font import bitmap_font -import adafruit_requests as requests -import storage -import displayio -from adafruit_display_text.label import Label -import terminalio -import audioio -import audiocore -import rtc -import supervisor -from adafruit_io.adafruit_io import IO_HTTP, AdafruitIO_RequestError - -try: - import sdcardio - - NATIVE_SD = True -except ImportError: - import adafruit_sdcard as sdcardio - - NATIVE_SD = False - -if hasattr(board, "TOUCH_XL"): - import adafruit_touchscreen -elif hasattr(board, "BUTTON_CLOCK"): - from adafruit_cursorcontrol.cursorcontrol import Cursor - from adafruit_cursorcontrol.cursorcontrol_cursormanager import CursorManager - - -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_PyPortal.git" - -# pylint: disable=line-too-long -# pylint: disable=too-many-lines -# you'll need to pass in an io username, width, height, format (bit depth), io key, and then url! -IMAGE_CONVERTER_SERVICE = "https://io.adafruit.com/api/v2/%s/integrations/image-formatter?x-aio-key=%s&width=%d&height=%d&output=BMP%d&url=%s" -# 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 - -CONTENT_TEXT = const(1) -CONTENT_JSON = const(2) -CONTENT_IMAGE = const(3) - - -class HttpError(Exception): - """HTTP Specific Error""" - - -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.""" - import json # pylint: disable=import-outside-toplevel - - 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() - - -class PyPortal: - """Class representing the Adafruit PyPortal. - - :param url: The URL of your data source. Defaults to ``None``. - :param headers: The headers for authentication, typically used by Azure API's. - :param json_path: The list of json traversal to get data out of. Can be list of lists for - multiple data points. Defaults to ``None`` to not use json. - :param regexp_path: The list of regexp strings to get data out (use a single regexp group). Can - be list of regexps for multiple data points. Defaults to ``None`` to not - use regexp. - :param convert_image: Determine whether or not to use the AdafruitIO image converter service. - Set as False if your image is already resized. Defaults to True. - :param default_bg: The path to your default background image file or a hex color. - Defaults to 0x000000. - :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board - NeoPixel. Defaults to ``None``, not the status LED - :param str text_font: The path to your font file for your data text display. - :param text_position: The position of your extracted text on the display in an (x, y) tuple. - Can be a list of tuples for when there's a list of json_paths, for example - :param text_color: The color of the text, in 0xRRGGBB format. Can be a list of colors for when - there's multiple texts. Defaults to ``None``. - :param text_wrap: Whether or not to wrap text (for long text data chunks). Defaults to - ``False``, no wrapping. - :param text_maxlen: The max length of the text for text wrapping. Defaults to 0. - :param text_transform: A function that will be called on the text before display - :param int text_scale: The factor to scale the default size of the text by - :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. - :param image_json_path: The JSON traversal path for a background image to display. Defaults to - ``None``. - :param image_resize: What size to resize the image we got from the json_path, make this a tuple - of the width and height you want. Defaults to ``None``. - :param image_position: The position of the image on the display as an (x, y) tuple. Defaults to - ``None``. - :param image_dim_json_path: The JSON traversal path for the original dimensions of image tuple. - Used with fetch(). Defaults to ``None``. - :param success_callback: A function we'll call if you like, when we fetch data successfully. - Defaults to ``None``. - :param str caption_text: The text of your caption, a fixed text not changed by the data we get. - Defaults to ``None``. - :param str caption_font: The path to the font file for your caption. Defaults to ``None``. - :param caption_position: The position of your caption on the display as an (x, y) tuple. - Defaults to ``None``. - :param caption_color: The color of your caption. Must be a hex value, e.g. ``0x808000``. - :param image_url_path: The HTTP traversal path for a background image to display. - Defaults to ``None``. - :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used - before calling the pyportal class. Defaults to ``None``. - :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. - :param debug: Turn on debug print outs. Defaults to False. - - """ - - # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements - def __init__( - self, - *, - url=None, - headers=None, - json_path=None, - regexp_path=None, - convert_image=True, - default_bg=0x000000, - status_neopixel=None, - text_font=terminalio.FONT, - text_position=None, - text_color=0x808080, - text_wrap=False, - text_maxlen=0, - text_transform=None, - text_scale=1, - json_transform=None, - image_json_path=None, - image_resize=None, - image_position=None, - image_dim_json_path=None, - caption_text=None, - caption_font=None, - caption_position=None, - caption_color=0x808080, - image_url_path=None, - success_callback=None, - esp=None, - external_spi=None, - debug=False - ): - - self._debug = debug - self._convert_image = convert_image - - try: - if hasattr(board, "TFT_BACKLIGHT"): - self._backlight = pulseio.PWMOut( - board.TFT_BACKLIGHT - ) # pylint: disable=no-member - elif hasattr(board, "TFT_LITE"): - self._backlight = pulseio.PWMOut( - board.TFT_LITE - ) # pylint: disable=no-member - except ValueError: - self._backlight = None - self.set_backlight(1.0) # turn on backlight - - self._url = url - self._headers = headers - if json_path: - if isinstance(json_path[0], (list, tuple)): - self._json_path = json_path - else: - self._json_path = (json_path,) - else: - self._json_path = None - - self._regexp_path = regexp_path - self._success_callback = success_callback - - if status_neopixel: - self.neopix = neopixel.NeoPixel(status_neopixel, 1, brightness=0.2) - else: - self.neopix = None - self.neo_status(0) - - try: - os.stat(LOCALFILE) - self._uselocal = True - except OSError: - self._uselocal = False - - 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) - - # show thank you and bootup file if available - for bootscreen in ("/thankyou.bmp", "/pyportal_startup.bmp"): - try: - os.stat(bootscreen) - board.DISPLAY.show(self.splash) - for i in range(100, -1, -1): # dim down - self.set_backlight(i / 100) - time.sleep(0.005) - self.set_background(bootscreen) - try: - board.DISPLAY.refresh(target_frames_per_second=60) - except AttributeError: - board.DISPLAY.wait_for_frame() - for i in range(100): # dim up - self.set_backlight(i / 100) - time.sleep(0.005) - time.sleep(2) - except OSError: - pass # they removed it, skip! - - self._speaker_enable = DigitalInOut(board.SPEAKER_ENABLE) - self._speaker_enable.switch_to_output(False) - if hasattr(board, "AUDIO_OUT"): - self.audio = audioio.AudioOut(board.AUDIO_OUT) - elif hasattr(board, "SPEAKER"): - self.audio = audioio.AudioOut(board.SPEAKER) - else: - raise AttributeError("Board does not have a builtin speaker!") - try: - self.play_file("pyportal_startup.wav") - except OSError: - pass # they deleted the file, no biggie! - - if esp: # If there was a passed ESP Object - if self._debug: - print("Passed ESP32 to PyPortal") - self._esp = esp - if external_spi: # If SPI Object Passed - spi = external_spi - else: # Else: Make ESP32 connection - spi = busio.SPI(board.SCK, board.MOSI, board.MISO) - else: - if self._debug: - print("Init ESP32") - esp32_ready = DigitalInOut(board.ESP_BUSY) - esp32_gpio0 = DigitalInOut(board.ESP_GPIO0) - esp32_reset = DigitalInOut(board.ESP_RESET) - esp32_cs = DigitalInOut(board.ESP_CS) - spi = busio.SPI(board.SCK, board.MOSI, board.MISO) - - self._esp = adafruit_esp32spi.ESP_SPIcontrol( - spi, esp32_cs, esp32_ready, esp32_reset, esp32_gpio0 - ) - # self._esp._debug = 1 - for _ in range(3): # retries - try: - print("ESP firmware:", self._esp.firmware_version) - break - except RuntimeError: - print("Retrying ESP32 connection") - time.sleep(1) - self._esp.reset() - else: - raise RuntimeError("Was not able to find ESP32") - requests.set_socket(socket, self._esp) - - if url and not self._uselocal: - self._connect_esp() - - if self._debug: - print("My IP address is", self._esp.pretty_ip(self._esp.ip_address)) - - # set the default background - self.set_background(self._default_bg) - board.DISPLAY.show(self.splash) - - if self._debug: - print("Init SD Card") - sd_cs = board.SD_CS - if not NATIVE_SD: - sd_cs = DigitalInOut(sd_cs) - self._sdcard = None - try: - self._sdcard = sdcardio.SDCard(spi, sd_cs) - vfs = storage.VfsFat(self._sdcard) - storage.mount(vfs, "/sd") - except OSError as error: - print("No SD card found:", error) - - self._qr_group = None - # Tracks whether we've hidden the background when we showed the QR code. - self._qr_only = False - - if self._debug: - print("Init caption") - self._caption = None - if caption_font: - self._caption_font = bitmap_font.load_font(caption_font) - self.set_caption(caption_text, caption_position, caption_color) - - if text_font: - if text_position is not None and isinstance( - text_position[0], (list, tuple) - ): - num = len(text_position) - if not text_wrap: - text_wrap = [0] * num - if not text_maxlen: - text_maxlen = [0] * num - if not text_transform: - text_transform = [None] * num - if not isinstance(text_scale, (list, tuple)): - text_scale = [text_scale] * num - else: - num = 1 - text_position = (text_position,) - text_color = (text_color,) - text_wrap = (text_wrap,) - text_maxlen = (text_maxlen,) - text_transform = (text_transform,) - text_scale = (text_scale,) - self._text = [None] * num - self._text_color = [None] * num - self._text_position = [None] * num - self._text_wrap = [None] * num - self._text_maxlen = [None] * num - self._text_transform = [None] * num - self._text_scale = [None] * num - if text_font is not terminalio.FONT: - self._text_font = bitmap_font.load_font(text_font) - else: - self._text_font = terminalio.FONT - if self._debug: - print("Loading font glyphs") - # self._text_font.load_glyphs(b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - # b'0123456789:/-_,. ') - gc.collect() - - for i in range(num): - if self._debug: - print("Init text area", i) - self._text[i] = None - self._text_color[i] = text_color[i] - self._text_position[i] = text_position[i] - self._text_wrap[i] = text_wrap[i] - self._text_maxlen[i] = text_maxlen[i] - self._text_transform[i] = text_transform[i] - if not isinstance(text_scale[i], (int, float)) or text_scale[i] < 1: - text_scale[i] = 1 - self._text_scale[i] = text_scale[i] - else: - self._text_font = None - self._text = None - - # Add any JSON translators - self._json_transform = [] - if json_transform: - if callable(json_transform): - self._json_transform.append(json_transform) - else: - self._json_transform.extend(filter(callable, json_transform)) - - self._image_json_path = image_json_path - self._image_url_path = image_url_path - self._image_resize = image_resize - self._image_position = image_position - self._image_dim_json_path = image_dim_json_path - if image_json_path or image_url_path: - if self._debug: - print("Init image path") - if not self._image_position: - self._image_position = (0, 0) # default to top corner - if not self._image_resize: - self._image_resize = ( - board.DISPLAY.width, - board.DISPLAY.height, - ) # default to full screen - if hasattr(board, "TOUCH_XL"): - if self._debug: - print("Init touchscreen") - # pylint: disable=no-member - self.touchscreen = adafruit_touchscreen.Touchscreen( - board.TOUCH_XL, - board.TOUCH_XR, - board.TOUCH_YD, - board.TOUCH_YU, - calibration=((5200, 59000), (5800, 57000)), - size=(board.DISPLAY.width, board.DISPLAY.height), - ) - # pylint: enable=no-member - - self.set_backlight(1.0) # turn on backlight - elif hasattr(board, "BUTTON_CLOCK"): - if self._debug: - print("Init cursor") - self.mouse_cursor = Cursor( - board.DISPLAY, display_group=self.splash, cursor_speed=8 - ) - self.mouse_cursor.hide() - self.cursor = CursorManager(self.mouse_cursor) - else: - raise AttributeError( - "PyPortal module requires either a touchscreen or gamepad." - ) - - gc.collect() - - def set_headers(self, headers): - """Set the headers used by fetch(). - - :param headers: The new header dictionary - """ - self._headers = headers - - 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) - try: - self._bg_sprite = displayio.TileGrid( - background, - pixel_shader=displayio.ColorConverter(), - position=position, - ) - except TypeError: - 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( - board.DISPLAY.width, board.DISPLAY.height, 1 - ) - color_palette = displayio.Palette(1) - color_palette[0] = file_or_color - try: - self._bg_sprite = displayio.TileGrid( - color_bitmap, pixel_shader=color_palette, position=(0, 0) - ) - except TypeError: - 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) - try: - board.DISPLAY.refresh(target_frames_per_second=60) - gc.collect() - except AttributeError: - board.DISPLAY.refresh_soon() - gc.collect() - board.DISPLAY.wait_for_frame() - - def set_backlight(self, val): - """Adjust the TFT backlight. - - :param val: The backlight brightness. Use a value between ``0`` and ``1``, where ``0`` is - off, and ``1`` is 100% brightness. - - """ - val = max(0, min(1.0, val)) - if self._backlight: - self._backlight.duty_cycle = int(val * 65535) - else: - board.DISPLAY.auto_brightness = False - board.DISPLAY.brightness = val - - def preload_font(self, glyphs=None): - # 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 and self._text_font is not terminalio.FONT: - self._text_font.load_glyphs(glyphs) - - def set_caption(self, caption_text, caption_position, caption_color): - # pylint: disable=line-too-long - """A caption. Requires setting ``caption_font`` in init! - - :param caption_text: The text of the caption. - :param caption_position: The position of the caption text. - :param caption_color: The color of your caption text. Must be a hex value, e.g. - ``0x808000``. - - """ - # pylint: enable=line-too-long - if self._debug: - print("Setting caption to", caption_text) - - if (not caption_text) or (not self._caption_font) or (not caption_position): - return # nothing to do! - - if self._caption: - self._caption._update_text( # pylint: disable=protected-access - str(caption_text) - ) - try: - board.DISPLAY.refresh(target_frames_per_second=60) - except AttributeError: - board.DISPLAY.refresh_soon() - board.DISPLAY.wait_for_frame() - return - - self._caption = Label(self._caption_font, text=str(caption_text)) - self._caption.x = caption_position[0] - self._caption.y = caption_position[1] - self._caption.color = caption_color - self.splash.append(self._caption) - - 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. - - """ - if self._text_font: - string = str(val) - if self._text_maxlen[index]: - string = string[: self._text_maxlen[index]] - if self._text[index]: - # print("Replacing text area with :", string) - # self._text[index].text = string - # return - try: - text_index = self.splash.index(self._text[index]) - except AttributeError: - for i in range(len(self.splash)): - if self.splash[i] == self._text[index]: - text_index = i - break - - self._text[index] = Label( - self._text_font, 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.splash[text_index] = self._text[index] - return - - if self._text_position[index]: # if we want it placed somewhere... - print("Making text area with string:", string) - self._text[index] = Label( - self._text_font, 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.splash.append(self._text[index]) - - def neo_status(self, value): - """The status NeoPixel. - - :param value: The color to change the NeoPixel. - - """ - if self.neopix: - self.neopix.fill(value) - - def play_file(self, file_name, wait_to_finish=True): - """Play a wav file. - - :param str file_name: The name of the wav file to play on the speaker. - - """ - wavfile = open(file_name, "rb") - wavedata = audiocore.WaveFile(wavfile) - self._speaker_enable.value = True - self.audio.play(wavedata) - if not wait_to_finish: - return - while self.audio.playing: - pass - wavfile.close() - self._speaker_enable.value = False - - @staticmethod - def _json_traverse(json, path): - value = json - for x in path: - value = value[x] - gc.collect() - return value - - 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_esp() - api_url = None - try: - aio_username = secrets["aio_username"] - aio_key = secrets["aio_key"] - except KeyError as error: - 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 error - - location = secrets.get("timezone", location) - 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: - raise RuntimeError(response.text) - 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 as error: - raise KeyError( - "Was unable to lookup the time, try setting secrets['timezone'] according to http://worldtimeapi.org/timezones" # pylint: disable=line-too-long - ) from error - 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) - ) - print(now) - 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((100, 100, 0)) - r = requests.get(url, stream=True) - - headers = {} - for title, content in r.headers.items(): - headers[title.lower()] = content - - if r.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(r.status_code, r.reason.decode("utf-8")) - ) - - if self._debug: - print(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 r.iter_content(min(remaining, chunk_size)): # huge chunks! - self.neo_status((0, 100, 100)) - 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((100, 100, 0)) - file.close() - - r.close() - stamp = time.monotonic() - stamp - print( - "Created file of %d bytes in %0.1f seconds" % (os.stat(filename)[6], stamp) - ) - self.neo_status((0, 0, 0)) - if not content_length == os.stat(filename)[6]: - raise RuntimeError - - def _connect_esp(self): - self.neo_status((0, 0, 100)) - while not self._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.neo_status((100, 0, 0)) # red = not connected - try: - self._esp.connect(secrets) - except RuntimeError as error: - print("Could not connect to internet", error) - print("Retrying in 3 seconds...") - time.sleep(3) - - @staticmethod - def image_converter_url(image_url, width, height, color_depth=16): - """Generate a converted image url from the url passed in, - with the given width and height. aio_username and aio_key must be - set in secrets.""" - try: - aio_username = secrets["aio_username"] - aio_key = secrets["aio_key"] - except KeyError as error: - raise KeyError( - "\n\nOur image converter service require 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 error - - return IMAGE_CONVERTER_SERVICE % ( - aio_username, - aio_key, - width, - height, - color_depth, - image_url, - ) - - def sd_check(self): - """Returns True if there is an SD card preset and False - if there is no SD card. The _sdcard value is set in _init - """ - if self._sdcard: - return True - return False - - def push_to_io(self, feed_key, data): - # pylint: disable=line-too-long - """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 - - """ - # pylint: enable=line-too-long - - try: - aio_username = secrets["aio_username"] - aio_key = secrets["aio_key"] - except KeyError as error: - raise KeyError( - "Adafruit IO secrets are kept in secrets.py, please add them there!\n\n" - ) from error - - wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager( - self._esp, secrets, None - ) - io_client = IO_HTTP(aio_username, aio_key, wifi) - - 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 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 - json_out = None - image_url = None - values = [] - content_type = CONTENT_TEXT - - gc.collect() - if self._debug: - print("Free mem: ", gc.mem_free()) # pylint: disable=no-member - - r = None - if self._uselocal: - print("*** USING LOCALFILE FOR DATA - NOT INTERNET!!! ***") - r = Fake_Requests(LOCALFILE) - - if not r: - self._connect_esp() - # great, lets get the data - print("Retrieving data...", end="") - self.neo_status((100, 100, 0)) # yellow = fetching data - gc.collect() - r = requests.get(self._url, headers=self._headers, timeout=timeout) - headers = {} - for title, content in r.headers.items(): - headers[title.lower()] = content - gc.collect() - if self._debug: - print("Headers:", headers) - if r.status_code == 200: - print("Reply is OK!") - self.neo_status((0, 0, 100)) # 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(r.status_code, r.reason.decode("utf-8")) - ) - - if self._debug and content_type == CONTENT_TEXT: - print(r.text) - - if self._debug: - print("Detected Content Type", content_type) - - if content_type == CONTENT_JSON: - try: - gc.collect() - json_out = r.json() - if self._debug: - print(json_out) - gc.collect() - except ValueError: # failed to parse? - print("Couldn't parse json: ", r.text) - raise - except MemoryError: - supervisor.reload() - - if self._regexp_path: - import re # pylint: disable=import-outside-toplevel - - if self._image_url_path: - image_url = self._image_url_path - - # 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 self._json_path: - for path in self._json_path: - try: - values.append(PyPortal._json_traverse(json_out, path)) - except KeyError: - print(json_out) - raise - elif content_type == CONTENT_TEXT and self._regexp_path: - for regexp in self._regexp_path: - values.append(re.search(regexp, r.text).group(1)) - else: - if content_type == CONTENT_JSON: - # No path given, so return JSON as string for compatibility - import json # pylint: disable=import-outside-toplevel - - values = json.dumps(r.json()) - else: - values = r.text - - if self._image_json_path: - try: - image_url = PyPortal._json_traverse(json_out, self._image_json_path) - except KeyError as error: - print("Error finding image data. '" + error.args[0] + "' not found.") - self.set_background(self._default_bg) - - iwidth = 0 - iheight = 0 - if self._image_dim_json_path: - iwidth = int( - PyPortal._json_traverse(json_out, self._image_dim_json_path[0]) - ) - iheight = int( - PyPortal._json_traverse(json_out, self._image_dim_json_path[1]) - ) - print("image dim:", iwidth, iheight) - - # we're done with the requests object, lets delete it so we can do more! - json_out = None - r = None - gc.collect() - - if image_url: - try: - print("original URL:", image_url) - if self._convert_image: - if iwidth < iheight: - image_url = self.image_converter_url( - image_url, - int( - self._image_resize[1] - * self._image_resize[1] - / self._image_resize[0] - ), - self._image_resize[1], - ) - else: - image_url = self.image_converter_url( - image_url, self._image_resize[0], self._image_resize[1] - ) - - print("convert URL:", image_url) - # convert image to bitmap and cache - # print("**not actually wgetting**") - filename = "/cache.bmp" - chunk_size = 4096 # default chunk size is 12K (for QSPI) - if self._sdcard: - filename = "/sd" + filename - chunk_size = 512 # current bug in big SD writes -> stick to 1 block - try: - self.wget(image_url, filename, chunk_size=chunk_size) - except OSError as error: - raise OSError( - """\n\nNo writable filesystem found for saving datastream. Insert an SD card or set internal filesystem to be unsafe by setting 'disable_concurrent_write_protection' in the mount options in boot.py""" # pylint: disable=line-too-long - ) from error - except RuntimeError as error: - raise RuntimeError("wget didn't write a complete file") from error - if iwidth < iheight: - pwidth = int( - self._image_resize[1] - * self._image_resize[1] - / self._image_resize[0] - ) - self.set_background( - filename, - ( - self._image_position[0] - + int((self._image_resize[0] - pwidth) / 2), - self._image_position[1], - ), - ) - else: - self.set_background(filename, self._image_position) - - except ValueError as error: - print("Error displaying cached image. " + error.args[0]) - self.set_background(self._default_bg) - finally: - image_url = None - gc.collect() - - # if we have a callback registered, call it now - if self._success_callback: - self._success_callback(values) - - # 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 = PyPortal.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 - - def show_QR( - self, qr_data, *, qr_size=1, x=0, y=0, hide_background=False - ): # pylint: disable=invalid-name - """Display a QR code on the TFT - - :param qr_data: The data for the QR code. - :param int qr_size: The scale of the QR code. - :param x: The x position of upper left corner of the QR code on the display. - :param y: The y position of upper left corner of the QR code on the display. - :param hide_background: Show the QR code on a black background if True. - - """ - import adafruit_miniqr # pylint: disable=import-outside-toplevel - - # generate the QR code - qrcode = adafruit_miniqr.QRCode() - qrcode.add_data(qr_data) - qrcode.make() - - # monochrome (2 color) palette - palette = displayio.Palette(2) - palette[0] = 0xFFFFFF - palette[1] = 0x000000 - - # pylint: disable=invalid-name - # bitmap the size of the matrix, plus border, monochrome (2 colors) - qr_bitmap = displayio.Bitmap( - qrcode.matrix.width + 2, qrcode.matrix.height + 2, 2 - ) - for i in range(qr_bitmap.width * qr_bitmap.height): - qr_bitmap[i] = 0 - - # transcribe QR code into bitmap - for xx in range(qrcode.matrix.width): - for yy in range(qrcode.matrix.height): - qr_bitmap[xx + 1, yy + 1] = 1 if qrcode.matrix[xx, yy] else 0 - - # display the QR code - qr_sprite = displayio.TileGrid(qr_bitmap, pixel_shader=palette) - if self._qr_group: - try: - self._qr_group.pop() - except IndexError: # later test if empty - pass - else: - self._qr_group = displayio.Group() - self.splash.append(self._qr_group) - self._qr_group.scale = qr_size - self._qr_group.x = x - self._qr_group.y = y - self._qr_group.append(qr_sprite) - if hide_background: - board.DISPLAY.show(self._qr_group) - self._qr_only = hide_background - - def hide_QR(self): # pylint: disable=invalid-name - """Clear any QR codes that are currently on the screen""" - - if self._qr_only: - board.DISPLAY.show(self.splash) - else: - try: - self._qr_group.pop() - except (IndexError, AttributeError): # later test if empty - pass - - # 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 diff --git a/adafruit_pyportal/__init__.py b/adafruit_pyportal/__init__.py new file mode 100755 index 0000000..bb432dc --- /dev/null +++ b/adafruit_pyportal/__init__.py @@ -0,0 +1,364 @@ +# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense +""" +`adafruit_pyportal` +================================================================================ + +CircuitPython driver for Adafruit PyPortal. + +* Author(s): Limor Fried, Kevin J. Walters, Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit PyPortal `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import os +import gc +import time +import board +import terminalio +import supervisor +from adafruit_portalbase import PortalBase +from adafruit_pyportal.network import Network, CONTENT_JSON, CONTENT_TEXT +from adafruit_pyportal.graphics import Graphics +from adafruit_pyportal.peripherals import Peripherals + +if hasattr(board, "TOUCH_XL"): + import adafruit_touchscreen +elif hasattr(board, "BUTTON_CLOCK"): + from adafruit_cursorcontrol.cursorcontrol import Cursor + from adafruit_cursorcontrol.cursorcontrol_cursormanager import CursorManager + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PyPortal.git" + + +class PyPortal(PortalBase): + """Class representing the Adafruit PyPortal. + + :param url: The URL of your data source. Defaults to ``None``. + :param headers: The headers for authentication, typically used by Azure API's. + :param json_path: The list of json traversal to get data out of. Can be list of lists for + multiple data points. Defaults to ``None`` to not use json. + :param regexp_path: The list of regexp strings to get data out (use a single regexp group). Can + be list of regexps for multiple data points. Defaults to ``None`` to not + use regexp. + :param convert_image: Determine whether or not to use the AdafruitIO image converter service. + Set as False if your image is already resized. Defaults to True. + :param default_bg: The path to your default background image file or a hex color. + Defaults to 0x000000. + :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board + NeoPixel. Defaults to ``None``, not the status LED + :param str text_font: The path to your font file for your data text display. + :param text_position: The position of your extracted text on the display in an (x, y) tuple. + Can be a list of tuples for when there's a list of json_paths, for example + :param text_color: The color of the text, in 0xRRGGBB format. Can be a list of colors for when + there's multiple texts. Defaults to ``None``. + :param text_wrap: Whether or not to wrap text (for long text data chunks). Defaults to + ``False``, no wrapping. + :param text_maxlen: The max length of the text for text wrapping. Defaults to 0. + :param text_transform: A function that will be called on the text before display + :param int text_scale: The factor to scale the default size of the text by + :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. + :param image_json_path: The JSON traversal path for a background image to display. Defaults to + ``None``. + :param image_resize: What size to resize the image we got from the json_path, make this a tuple + of the width and height you want. Defaults to ``None``. + :param image_position: The position of the image on the display as an (x, y) tuple. Defaults to + ``None``. + :param image_dim_json_path: The JSON traversal path for the original dimensions of image tuple. + Used with fetch(). Defaults to ``None``. + :param success_callback: A function we'll call if you like, when we fetch data successfully. + Defaults to ``None``. + :param str caption_text: The text of your caption, a fixed text not changed by the data we get. + Defaults to ``None``. + :param str caption_font: The path to the font file for your caption. Defaults to ``None``. + :param caption_position: The position of your caption on the display as an (x, y) tuple. + Defaults to ``None``. + :param caption_color: The color of your caption. Must be a hex value, e.g. ``0x808000``. + :param image_url_path: The HTTP traversal path for a background image to display. + Defaults to ``None``. + :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used + before calling the pyportal class. Defaults to ``None``. + :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. + :param debug: Turn on debug print outs. Defaults to False. + + """ + + # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements + def __init__( + self, + *, + url=None, + headers=None, + json_path=None, + regexp_path=None, + convert_image=True, + default_bg=0x000000, + status_neopixel=None, + text_font=terminalio.FONT, + text_position=None, + text_color=0x808080, + text_wrap=False, + text_maxlen=0, + text_transform=None, + text_scale=1, + json_transform=None, + image_json_path=None, + image_resize=None, + image_position=None, + image_dim_json_path=None, + caption_text=None, + caption_font=None, + caption_position=None, + caption_color=0x808080, + image_url_path=None, + success_callback=None, + esp=None, + external_spi=None, + debug=False + ): + + graphics = Graphics( + default_bg=default_bg, + debug=debug, + ) + + self._default_bg = default_bg + + if external_spi: # If SPI Object Passed + spi = external_spi + else: # Else: Make ESP32 connection + spi = board.SPI() + + if image_json_path or image_url_path: + if debug: + print("Init image path") + if not image_position: + image_position = (0, 0) # default to top corner + if not image_resize: + image_resize = ( + self.display.width, + self.display.height, + ) # default to full screen + + network = Network( + status_neopixel=status_neopixel, + esp=esp, + external_spi=spi, + extract_values=False, + convert_image=convert_image, + image_url_path=image_url_path, + image_json_path=image_json_path, + image_resize=image_resize, + image_position=image_position, + image_dim_json_path=image_dim_json_path, + debug=debug, + ) + + self.url = url + + super().__init__( + network, + graphics, + url=url, + headers=headers, + json_path=json_path, + regexp_path=regexp_path, + json_transform=json_transform, + success_callback=success_callback, + debug=debug, + ) + + # Convenience Shortcuts for compatibility + self.peripherals = Peripherals( + spi, display=self.display, splash_group=self.splash, debug=debug + ) + self.set_backlight = self.peripherals.set_backlight + self.sd_check = self.peripherals.sd_check + self.play_file = self.peripherals.play_file + + self.image_converter_url = self.network.image_converter_url + self.wget = self.network.wget + # pylint: disable=invalid-name + self.show_QR = self.graphics.qrcode + self.hide_QR = self.graphics.hide_QR + # pylint: enable=invalid-name + + if hasattr(self.peripherals, "touchscreen"): + self.touchscreen = self.peripherals.touchscreen + if hasattr(self.peripherals, "mouse_cursor"): + self.mouse_cursor = self.peripherals.mouse_cursor + if hasattr(self.peripherals, "cursor"): + self.cursor = self.peripherals.cursor + + # show thank you and bootup file if available + for bootscreen in ("/thankyou.bmp", "/pyportal_startup.bmp"): + try: + os.stat(bootscreen) + for i in range(100, -1, -1): # dim down + self.set_backlight(i / 100) + time.sleep(0.005) + self.set_background(bootscreen) + try: + self.display.refresh(target_frames_per_second=60) + except AttributeError: + self.display.wait_for_frame() + for i in range(100): # dim up + self.set_backlight(i / 100) + time.sleep(0.005) + time.sleep(2) + except OSError: + pass # they removed it, skip! + + try: + self.peripherals.play_file("pyportal_startup.wav") + except OSError: + pass # they deleted the file, no biggie! + + if default_bg is not None: + self.graphics.set_background(default_bg) + + if self._debug: + print("Init caption") + if caption_font: + self._caption_font = self._load_font(caption_font) + self.set_caption(caption_text, caption_position, caption_color) + + if text_font: + if text_position is not None and isinstance( + text_position[0], (list, tuple) + ): + num = len(text_position) + if not text_wrap: + text_wrap = [0] * num + if not text_maxlen: + text_maxlen = [0] * num + if not text_transform: + text_transform = [None] * num + if not isinstance(text_scale, (list, tuple)): + text_scale = [text_scale] * num + else: + num = 1 + text_position = (text_position,) + text_color = (text_color,) + text_wrap = (text_wrap,) + text_maxlen = (text_maxlen,) + text_transform = (text_transform,) + text_scale = (text_scale,) + for i in range(num): + self.add_text( + text_position=text_position[i], + text_font=text_font, + text_color=text_color[i], + text_wrap=text_wrap[i], + text_maxlen=text_maxlen[i], + text_transform=text_transform[i], + text_scale=text_scale[i], + ) + else: + self._text_font = None + self._text = None + + gc.collect() + + def set_caption(self, caption_text, caption_position, caption_color): + # pylint: disable=line-too-long + """A caption. Requires setting ``caption_font`` in init! + + :param caption_text: The text of the caption. + :param caption_position: The position of the caption text. + :param caption_color: The color of your caption text. Must be a hex value, e.g. + ``0x808000``. + """ + # pylint: enable=line-too-long + if self._debug: + print("Setting caption to", caption_text) + + if (not caption_text) or (not self._caption_font) or (not caption_position): + return # nothing to do! + + index = self.add_text( + text_position=caption_position, + text_font=self._caption_font, + text_color=caption_color, + is_data=False, + ) + self.set_text(caption_text, 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 + + response = self.network.fetch(self.url, timeout=timeout) + + json_out = None + content_type = self.network.check_response(response) + json_path = self._json_path + + if content_type == CONTENT_JSON: + if json_path is not None: + # Drill down to the json path and set json_out as that node + 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() + + try: + filename, position = self.network.process_image( + json_out, self.peripherals.sd_check() + ) + if filename and position is not None: + self.graphics.set_background(filename, position) + except ValueError as error: + print("Error displaying cached image. " + error.args[0]) + if self._default_bg is not None: + self.graphics.set_background(self._default_bg) + except KeyError as error: + print("Error finding image data. '" + error.args[0] + "' not found.") + self.set_background(self._default_bg) + + if content_type == CONTENT_JSON: + values = self.network.process_json(json_out, json_path) + elif content_type == CONTENT_TEXT: + values = self.network.process_text(response.text, self._regexp_path) + + # if we have a callback registered, call it now + if self._success_callback: + self._success_callback(values) + + self._fill_text_labels(values) + # Clean up + json_out = None + response = None + gc.collect() + + return values diff --git a/adafruit_pyportal/graphics.py b/adafruit_pyportal/graphics.py new file mode 100755 index 0000000..22af94c --- /dev/null +++ b/adafruit_pyportal/graphics.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense +""" +`adafruit_pyportal.graphics` +================================================================================ + +CircuitPython driver for Adafruit PyPortal. + +* Author(s): Limor Fried, Kevin J. Walters, Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit PyPortal `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import board +from adafruit_portalbase.graphics import GraphicsBase + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PyPortal.git" + + +class Graphics(GraphicsBase): + """Graphics Helper Class for the PyPortal Library + + :param default_bg: The path to your default background image file or a hex color. + Defaults to 0x000000. + :param debug: Turn on debug print outs. Defaults to False. + + """ + + # pylint: disable=too-few-public-methods + def __init__(self, *, default_bg=None, debug=False): + + super().__init__(board.DISPLAY, default_bg=default_bg, debug=debug) + # Tracks whether we've hidden the background when we showed the QR code. + self._qr_only = False + + # pylint: disable=arguments-differ + def qrcode(self, qr_data, *, qr_size=1, x=0, y=0, hide_background=False): + """Display a QR code + + :param qr_data: The data for the QR code. + :param int qr_size: The scale of the QR code. + :param x: The x position of upper left corner of the QR code on the display. + :param y: The y position of upper left corner of the QR code on the display. + + """ + super().qrcode( + qr_data, + qr_size=qr_size, + x=x, + y=y, + ) + if hide_background: + self.display.show(self._qr_group) + self._qr_only = hide_background + + # pylint: enable=arguments-differ + + def hide_QR(self): # pylint: disable=invalid-name + """Clear any QR codes that are currently on the screen""" + + if self._qr_only: + self.display.show(self.splash) + else: + try: + self._qr_group.pop() + except (IndexError, AttributeError): # later test if empty + pass diff --git a/adafruit_pyportal/network.py b/adafruit_pyportal/network.py new file mode 100755 index 0000000..9f5a48a --- /dev/null +++ b/adafruit_pyportal/network.py @@ -0,0 +1,198 @@ +# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense +""" +`adafruit_pyportal.network` +================================================================================ + +CircuitPython driver for Adafruit PyPortal. + +* Author(s): Limor Fried, Kevin J. Walters, Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit PyPortal `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import gc + +# pylint: disable=unused-import +from adafruit_portalbase.network import ( + NetworkBase, + secrets, + CONTENT_JSON, + CONTENT_TEXT, +) + +# pylint: enable=unused-import +from adafruit_pyportal.wifi import WiFi + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PyPortal.git" + +# you'll need to pass in an io username, width, height, format (bit depth), io key, and then url! +IMAGE_CONVERTER_SERVICE = ( + "https://io.adafruit.com/api/v2/%s/integrations/image-formatter?" + "x-aio-key=%s&width=%d&height=%d&output=BMP%d&url=%s" +) + + +class Network(NetworkBase): + """Class representing the Adafruit PyPortal. + + :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board + NeoPixel. Defaults to ``None``, not the status LED + :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used + before calling the pyportal class. Defaults to ``None``. + :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. + :param bool extract_values: If true, single-length fetched values are automatically extracted + from lists and tuples. Defaults to ``True``. + :param debug: Turn on debug print outs. Defaults to False. + + """ + + def __init__( + self, + *, + status_neopixel=None, + esp=None, + external_spi=None, + extract_values=True, + debug=False, + convert_image=True, + image_url_path=None, + image_json_path=None, + image_resize=None, + image_position=None, + image_dim_json_path=None, + ): + wifi = WiFi(status_neopixel=status_neopixel, esp=esp, external_spi=external_spi) + + super().__init__( + wifi, + extract_values=extract_values, + debug=debug, + ) + + self._convert_image = convert_image + self._image_json_path = image_json_path + self._image_url_path = image_url_path + self._image_resize = image_resize + self._image_position = image_position + self._image_dim_json_path = image_dim_json_path + + gc.collect() + + @property + def ip_address(self): + """Return the IP Address nicely formatted""" + return self._wifi.esp.pretty_ip(self._wifi.esp.ip_address) + + @staticmethod + def image_converter_url(image_url, width, height, color_depth=16): + """Generate a converted image url from the url passed in, + with the given width and height. aio_username and aio_key must be + set in secrets.""" + try: + aio_username = secrets["aio_username"] + aio_key = secrets["aio_key"] + except KeyError as error: + raise KeyError( + "\n\nOur image converter service require 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 error + + return IMAGE_CONVERTER_SERVICE % ( + aio_username, + aio_key, + width, + height, + color_depth, + image_url, + ) + + # pylint: disable=too-many-branches, too-many-statements + def process_image(self, json_data, sd_card=False): + """ + Process image content + + :param json_data: The JSON data that we can pluck values from + :param bool sd_card: Whether or not we have an SD card inserted + + """ + filename = None + position = None + image_url = None + + if self._image_url_path: + image_url = self._image_url_path + + if self._image_json_path: + image_url = self.json_traverse(json_data, self._image_json_path) + + iwidth = 0 + iheight = 0 + if self._image_dim_json_path: + iwidth = int(self.json_traverse(json_data, self._image_dim_json_path[0])) + iheight = int(self.json_traverse(json_data, self._image_dim_json_path[1])) + print("image dim:", iwidth, iheight) + + if image_url: + print("original URL:", image_url) + if self._convert_image: + if iwidth < iheight: + image_url = self.image_converter_url( + image_url, + int( + self._image_resize[1] + * self._image_resize[1] + / self._image_resize[0] + ), + self._image_resize[1], + ) + else: + image_url = self.image_converter_url( + image_url, self._image_resize[0], self._image_resize[1] + ) + + print("convert URL:", image_url) + # convert image to bitmap and cache + # print("**not actually wgetting**") + filename = "/cache.bmp" + chunk_size = 4096 # default chunk size is 12K (for QSPI) + if sd_card: + filename = "/sd" + filename + chunk_size = 512 # current bug in big SD writes -> stick to 1 block + try: + self.wget(image_url, filename, chunk_size=chunk_size) + except OSError as error: + raise OSError( + """\n\nNo writable filesystem found for saving datastream. Insert an SD card or set internal filesystem to be unsafe by setting 'disable_concurrent_write_protection' in the mount options in boot.py""" # pylint: disable=line-too-long + ) from error + except RuntimeError as error: + raise RuntimeError("wget didn't write a complete file") from error + if iwidth < iheight: + pwidth = int( + self._image_resize[1] + * self._image_resize[1] + / self._image_resize[0] + ) + position = ( + self._image_position[0] + int((self._image_resize[0] - pwidth) / 2), + self._image_position[1], + ) + else: + position = self._image_position + + image_url = None + gc.collect() + + return filename, position diff --git a/adafruit_pyportal/peripherals.py b/adafruit_pyportal/peripherals.py new file mode 100755 index 0000000..88d49c5 --- /dev/null +++ b/adafruit_pyportal/peripherals.py @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_pyportal.peripherals` +================================================================================ + +CircuitPython driver for Adafruit PyPortal. + +* Author(s): Limor Fried, Kevin J. Walters, Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit PyPortal `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import gc +import board +from digitalio import DigitalInOut +import pulseio +import audioio +import audiocore +import storage + +try: + import sdcardio + + NATIVE_SD = True +except ImportError: + import adafruit_sdcard as sdcardio + + NATIVE_SD = False + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PyPortal.git" + + +class Peripherals: + """Peripherals Helper Class for the PyPortal Library""" + + # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements + def __init__(self, spi, display, splash_group, debug=False): + # Speaker Enable + self._speaker_enable = DigitalInOut(board.SPEAKER_ENABLE) + self._speaker_enable.switch_to_output(False) + + self._display = display + + if hasattr(board, "AUDIO_OUT"): + self.audio = audioio.AudioOut(board.AUDIO_OUT) + elif hasattr(board, "SPEAKER"): + self.audio = audioio.AudioOut(board.SPEAKER) + else: + raise AttributeError("Board does not have a builtin speaker!") + + if debug: + print("Init SD Card") + sd_cs = board.SD_CS + if not NATIVE_SD: + sd_cs = DigitalInOut(sd_cs) + self._sdcard = None + + try: + self._sdcard = sdcardio.SDCard(spi, sd_cs) + vfs = storage.VfsFat(self._sdcard) + storage.mount(vfs, "/sd") + except OSError as error: + print("No SD card found:", error) + + try: + if hasattr(board, "TFT_BACKLIGHT"): + self._backlight = pulseio.PWMOut( + board.TFT_BACKLIGHT + ) # pylint: disable=no-member + elif hasattr(board, "TFT_LITE"): + self._backlight = pulseio.PWMOut( + board.TFT_LITE + ) # pylint: disable=no-member + except ValueError: + self._backlight = None + self.set_backlight(1.0) # turn on backlight + # pylint: disable=import-outside-toplevel + if hasattr(board, "TOUCH_XL"): + import adafruit_touchscreen + + if debug: + print("Init touchscreen") + # pylint: disable=no-member + self.touchscreen = adafruit_touchscreen.Touchscreen( + board.TOUCH_XL, + board.TOUCH_XR, + board.TOUCH_YD, + board.TOUCH_YU, + calibration=((5200, 59000), (5800, 57000)), + size=(board.DISPLAY.width, board.DISPLAY.height), + ) + # pylint: enable=no-member + + self.set_backlight(1.0) # turn on backlight + elif hasattr(board, "BUTTON_CLOCK"): + from adafruit_cursorcontrol.cursorcontrol import Cursor + from adafruit_cursorcontrol.cursorcontrol_cursormanager import CursorManager + + if debug: + print("Init cursor") + self.mouse_cursor = Cursor( + board.DISPLAY, display_group=splash_group, cursor_speed=8 + ) + self.mouse_cursor.hide() + self.cursor = CursorManager(self.mouse_cursor) + else: + raise AttributeError( + "PyPortal module requires either a touchscreen or gamepad." + ) + # pylint: enable=import-outside-toplevel + + gc.collect() + + def set_backlight(self, val): + """Adjust the TFT backlight. + + :param val: The backlight brightness. Use a value between ``0`` and ``1``, where ``0`` is + off, and ``1`` is 100% brightness. + + """ + val = max(0, min(1.0, val)) + if self._backlight: + self._backlight.duty_cycle = int(val * 65535) + else: + self._display.auto_brightness = False + self._display.brightness = val + + def play_file(self, file_name, wait_to_finish=True): + """Play a wav file. + + :param str file_name: The name of the wav file to play on the speaker. + + """ + wavfile = open(file_name, "rb") + wavedata = audiocore.WaveFile(wavfile) + self._speaker_enable.value = True + self.audio.play(wavedata) + if not wait_to_finish: + return + while self.audio.playing: + pass + wavfile.close() + self._speaker_enable.value = False + + def sd_check(self): + """Returns True if there is an SD card preset and False + if there is no SD card. The _sdcard value is set in _init + """ + if self._sdcard: + return True + return False + + @property + def speaker_disable(self): + """ + Enable or disable the speaker for power savings + """ + return not self._speaker_enable.value + + @speaker_disable.setter + def speaker_disable(self, value): + self._speaker_enable.value = not value diff --git a/adafruit_pyportal/wifi.py b/adafruit_pyportal/wifi.py new file mode 100755 index 0000000..6c13b9c --- /dev/null +++ b/adafruit_pyportal/wifi.py @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense +""" +`adafruit_pyportal.wifi` +================================================================================ + +CircuitPython driver for Adafruit PyPortal. + +* Author(s): Limor Fried, Kevin J. Walters, Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit PyPortal `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import gc +import board +import busio +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_PyPortal.git" + + +class WiFi: + """Class representing the ESP. + + :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board + NeoPixel. Defaults to ``None``, not the status LED + :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used + before calling the pyportal class. Defaults to ``None``. + :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. + + """ + + def __init__(self, *, status_neopixel=None, esp=None, external_spi=None): + + if status_neopixel: + self.neopix = neopixel.NeoPixel(status_neopixel, 1, brightness=0.2) + else: + self.neopix = None + self.neo_status(0) + self.requests = None + + if external_spi: # If SPI Object Passed + spi = external_spi + else: # Else: Make ESP32 connection + spi = busio.SPI(board.SCK, board.MOSI, board.MISO) + + if esp: # If there was a passed ESP Object + self.esp = esp + else: + esp32_ready = DigitalInOut(board.ESP_BUSY) + esp32_gpio0 = DigitalInOut(board.ESP_GPIO0) + esp32_reset = DigitalInOut(board.ESP_RESET) + esp32_cs = DigitalInOut(board.ESP_CS) + + self.esp = adafruit_esp32spi.ESP_SPIcontrol( + 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. + + :param value: The color to change the NeoPixel. + + """ + if self.neopix: + self.neopix.fill(value) + + def manager(self, secrets): + """Initialize the WiFi Manager if it hasn't been cached and return it""" + if self._manager is None: + self._manager = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager( + 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 diff --git a/docs/_static/favicon.ico.license b/docs/_static/favicon.ico.license new file mode 100644 index 0000000..86a3fbf --- /dev/null +++ b/docs/_static/favicon.ico.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/api.rst.license b/docs/api.rst.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/docs/api.rst.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/docs/conf.py b/docs/conf.py index 4a117be..9792779 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + import os import sys @@ -27,21 +31,13 @@ "pulseio", "audioio", "audiocore", - "displayio", - "neopixel", "microcontroller", - "adafruit_touchscreen", - "adafruit_bitmap_font", - "adafruit_display_text", - "adafruit_esp32spi", "secrets", "adafruit_sdcard", "storage", "sdcardio", "adafruit_io", "adafruit_cursorcontrol", - "adafruit_requests", - "terminalio", ] diff --git a/docs/examples.rst.license b/docs/examples.rst.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/docs/examples.rst.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/docs/index.rst.license b/docs/index.rst.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/docs/index.rst.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/examples/pyportal_simpletest.py b/examples/pyportal_simpletest.py index 14d1514..18b0017 100644 --- a/examples/pyportal_simpletest.py +++ b/examples/pyportal_simpletest.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + # NOTE: Make sure you've created your secrets.py file before running this example # https://learn.adafruit.com/adafruit-pyportal/internet-connect#whats-a-secrets-file-17-2 import board diff --git a/examples/pyportal_startup.wav.license b/examples/pyportal_startup.wav.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/examples/pyportal_startup.wav.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/requirements.txt b/requirements.txt index 7b80f3d..e4b8de3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,13 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + Adafruit-Blinka +adafruit-blinka-displayio +adafruit-circuitpython-portalbase adafruit-circuitpython-busdevice adafruit-circuitpython-touchscreen adafruit-circuitpython-esp32spi adafruit-circuitpython-bitmap-font adafruit-circuitpython-neopixel +adafruit-circuitpython-requests diff --git a/setup.py.disabled b/setup.py.disabled index a80b9bf..4d6d42a 100644 --- a/setup.py.disabled +++ b/setup.py.disabled @@ -1,3 +1,8 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + """ This library is not deployed to PyPI. It is either a board-specific helper library, or does not make sense for use on or is incompatible with single board computers and Linux.