GithubHelp home page GithubHelp logo

adafruit_circuitpython_pyportal's Introduction

Introduction

Documentation Status Discord Build Status Code Style: Black

CircuitPython driver for Adafruit PyPortal.

Dependencies

This driver depends on:

Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading the Adafruit library and driver bundle.

Usage Example

See examples/pyportal_simpletest.py

Documentation

API documentation for this library can be found on Read the Docs.

For information on building library documentation, please check out this guide.

Contributing

Contributions are welcome! Please read our Code of Conduct before contributing to help this project stay welcoming.

adafruit_circuitpython_pyportal's People

Contributors

brentru avatar caternuson avatar cjsieh avatar cogliano avatar dastels avatar dhalbert avatar evaherrada avatar foamyguy avatar jasonlshelton avatar jepler avatar jerryneedell avatar jfurcean avatar joebaird avatar jposada202020 avatar justmobilize avatar kattni avatar kevinjwalters avatar ladyada avatar makermelissa avatar martymcguire avatar mikerenfro avatar sconaway avatar seantibor avatar siddacious avatar sommersoft avatar tannewt avatar tekktrik avatar theelectricmayhem avatar virgilvox avatar wifijt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

adafruit_circuitpython_pyportal's Issues

PyPortal class requires the 'esp' parameter, which defaults to 'None'

I'm opening this as an issue before attempting a fix.

tl;dr: the object PyPortal doesnt properly initialize the network before attempting to use it. Should this be fixed?

I've been going through some of the examples on the Adafruit Learn site with the PyPortal and I keep on running into an issue resulting in a stacktrace when instantiating a PyPortal object like this:

Traceback (most recent call last):
  File "code.py", line 12, in <module>
  File "adafruit_pyportal/__init__.py", line 152, in __init__
  File "adafruit_pyportal/network.py", line 90, in __init__
  File "adafruit_pyportal/wifi.py", line 77, in __init__
AttributeError: 'NoneType' object has no attribute 'is_connected'

Taken from https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/master/PyPortal_Quotes/quote.py

This can be attributed to the line if self.esp.is_connected: in the constructor of WiFi:

    def __init__(self, *, status_neopixel=None, esp=None, external_spi=None):
        [...omitted...]

        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)
        if self.esp.is_connected:
            self.requests = requests

which is called from Network's constructor:

    def __init__(
        self,
        *,
        status_neopixel=None,
        esp=None,
        external_spi=None,
        [...omitted...]
    ):
        wifi = WiFi(status_neopixel=status_neopixel, esp=esp, external_spi=external_spi)

which is called from the PyPortal constructor:


    # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements
    def __init__(
        self,
        [...omitted...]
        esp=None,
        external_spi=None,
        [...omitted...]
    ):

        [...omitted...]

        if external_spi:  # If SPI Object Passed
            spi = external_spi
        else:  # Else: Make ESP32 connection
            spi = board.SPI()

        [...omitted...]

        network = Network(
            status_neopixel=status_neopixel,
            esp=esp,
            external_spi=spi,

So, the ultimate reason for the error is because the default constructor for PyPortal propagates a None value for the esp parameter into Network, which propagates it to WiFi, which seemingly instantiates it correctly, but doesnt give time for the esp object to properly connect to the network.

An additional Learn article, https://learn.adafruit.com/adafruit-pyportal/internet-connect, lists a more complete example of connecting to a wireless network for the PyPortal product, which is copied here:

requests.set_socket(socket, esp)

if esp.status == adafruit_esp32spi.WL_IDLE_STATUS:
    print("ESP32 found and in idle mode")
print("Firmware vers.", esp.firmware_version)
print("MAC addr:", [hex(i) for i in esp.MAC_address])
 
for ap in esp.scan_networks():
    print("\t%s\t\tRSSI: %d" % (str(ap["ssid"], "utf-8"), ap["rssi"]))
 
print("Connecting to AP...")
while not esp.is_connected:
    try:
        esp.connect_AP(secrets["ssid"], secrets["password"])
    except RuntimeError as e:
        print("could not connect to AP, retrying: ", e)
        continue
print("Connected to", str(esp.ssid, "utf-8"), "\tRSSI:", esp.rssi)
print("My IP address is", esp.pretty_ip(esp.ip_address))

which simply checks for the ESP's status and will attempt to connect to the SSID with the given secrets as per the standard secrets.py file containing the secrets dictionary.

There seems to be two possible solutions here:

  1. Update the WiFi class' constructor to wait for the network before continuing as per the PyPortal Learn guide on connecting to a network
  2. Update the PyPortal constructor arguments to require a manually configured esp object that has already been configured and connected to the network.

The first seems more user friendly and would allow people to continue to use the vast amounts of example code that already construct a PyPortal object without setting up a network connection, though the second option appears like a less invasive approach, though would continue to make all the example code on the Learn site and github broken by default.

Feature request: Allow json_transforms prior to json_path or image_url_path usage.

I have experimented with initializing a PyPortal object using json_transform=(myTransform),. Based on debug print statements, the transform function I wrote seems to be called after the .fetch() method has completed (or at least at the very end of the .fetch() method). I want to be able to edit (transform) the JSON/dict immediately upon it's creation -- before the image_json_path (or the json_path) is put to use.

For example, I'm trying to adapt the https://learn.adafruit.com/cleveland-museum-of-art-pyportal-frame project to the Art Institute of Chicago's Public API. The Cleveland project uses image_json_path to locate a .JPG file to display. For the Art Institute of Chicago, the .JPG location can only be found by concatenating the values from two separate key:value pairs and a couple of string constants.

I.e., my json_transform would

json_out["joinedKeys"] = json_out["config"]["iiif_url"] + "/" + json_out["data"][0]["imageID"] + "/full/!320,240/0/default.jpg"

That would leave me free to initialize the PyPortal object with

image_json_path = ["joindKeys"],

I can imagine other use cases for accessing or modifying the JSON/dict data immediately after its creation.

Thank you

AttributeError: 'Display' object has no attribute 'refresh_soon'

Hello. I apologize if this issue has already been reported or fixed. I'm just getting started with the PyPortal and I'm getting the following error when trying to upgrade to CircuitPython 5.0.0.

Set background to  /pyportal_startup.bmp
Traceback (most recent call last):
  File "code.py", line 23, in <module>
  File "adafruit_pyportal.py", line 214, in __init__
  File "adafruit_pyportal.py", line 207, in __init__
  File "adafruit_pyportal.py", line 386, in set_background
AttributeError: 'Display' object has no attribute 'refresh_soon'

This is a brand new PyPython that I ordered from DigiKey, and I downloaded the latest CircuitPython .UF2 here https://circuitpython.org/board/pyportal/

Is there a way to edit the adafruit_pyportal.py to remove the references to refresh_soon?

In doing some research, I see in the release notes that the refresh_soon option was removed in version 5.0.0. (https://github.com/adafruit/circuitpython/releases/tag/5.0.0)

For now I'll roll back to the version that came pre-installed on my device. But I'd like to install the latest version.

Thanks for your time and consideration.

Andy

Default param for text_font

Currently text_font on PyPortal class is set to None. Is there a chance we could utilize termalio font for a default font?

Feature suggestion: allow new headers to be passed to PyPortal.fetch, just as a new URL can be

PyPortal.fetch can be used to get data from different URLs simply by passing a new URL when it is called. However this cannot be done for headers. headers cannot currently be passed in a call to that function. Instead is uses the information, if any, set when the instance of PyPortal was first initialized.

I suggest adding the ability to pass a new header as a named argument in the call, so that different URL's with different headers can be called more easily. I think this would be an easy change. If the parameter is passed in, then it just replaces _headers in the network call.

Add new feature to allow changes/additions to the fetched and parsed JSON dict

It's useful in some cases to be able to do some post processing on the returned JSON object. This could be achieved with an optional argument called something like json_transforms allowing the PyPortal object to be created with a function or a list of functions which run over the JSON dict object from the parsed HTTP response body. These could be allowed to modify or add values to the dict which could then be picked up by the usual image_json_path mechanism.

For the case of the NASA viewer code (Adafruit Learn: PyPortal NASA Image of the Day Viewer) as there is no support for video at the moment it would be useful to be able to deal with the odd occasion when this returns a YouTube video URL and turn that into a URL with a single still image. For discussion on the problem, see Adafruit Forums: Not getting actual images from NASA Image Viewer (pyportal)

wait_for_frame fails with CP 5.x

Does this call just have to be removed?

https://github.com/adafruit/Adafruit_CircuitPython_PyPortal/blob/master/adafruit_pyportal.py#L553

It seems to work OK if I delete it -- will thst cause any problems for CP 4.x?

Press any key to enter the REPL. Use CTRL-D to reload.
Adafruit CircuitPython 5.0.0-alpha.4-72-g7f1bc48a8 on 2019-09-20; Adafruit PyPortal with samd51j20
>>> 
>>> import pyportal_simpletest
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pyportal_simpletest.py", line 10, in <module>
  File "adafruit_pyportal.py", line 245, in __init__
  File "adafruit_pyportal.py", line 243, in __init__
  File "adafruit_pyportal.py", line 553, in play_file
AttributeError: 'Display' object has no attribute 'wait_for_frame'
>>> 
>>> 

Should be removed from PyPi

This repo currently has a setup.py and the library is distributed via PyPi for installation via pip. But the setup.py could be disabled and it removed from PyPi since it is not meant to be used with SoC / Blinka based devices.

SCK in use error

Using the current release of this library testing the simpletest script in the examples dir and this learn guide: https://learn.adafruit.com/electronic-history-of-the-day-with-pyportal/overview

I am always getting this error when attempting to run the either code:

code.py output:
Traceback (most recent call last):
  File "code.py", line 14, in <module>
  File "adafruit_pyportal/__init__.py", line 152, in __init__
  File "adafruit_pyportal/network.py", line 95, in __init__
  File "adafruit_portalbase/wifi_coprocessor.py", line 66, in __init__
ValueError: SCK in use

If I revert back to release 5.2.0 and the previous release of portalbase I do not get this error any more.

Building locally has issues on a macOS machine

On a macOS machine with Xcode 10.2.1 installed, attempting to build from source by following the instructions on https://github.com/adafruit/Adafruit_CircuitPython_PyPortal#building-locally results in the following error:

Generating build/genhdr/mpversion.h
GEN build/genhdr/qstr.i.last
QSTR updated
GEN build/genhdr/qstrdefs.generated.h
../py/nlrx64.c:44:9: error: macro expansion producing 'defined' has undefined behavior [-Werror,-Wexpansion-to-defined]
    #if NLR_OS_WINDOWS
        ^
../py/nlrx64.c:37:25: note: expanded from macro 'NLR_OS_WINDOWS'
#define NLR_OS_WINDOWS (defined(_WIN32) || defined(__CYGWIN__))
                        ^
../py/nlrx64.c:44:9: error: macro expansion producing 'defined' has undefined behavior [-Werror,-Wexpansion-to-defined]
../py/nlrx64.c:37:44: note: expanded from macro 'NLR_OS_WINDOWS'
#define NLR_OS_WINDOWS (defined(_WIN32) || defined(__CYGWIN__))
                                           ^
../py/nlrx64.c:112:9: error: macro expansion producing 'defined' has undefined behavior [-Werror,-Wexpansion-to-defined]
    #if NLR_OS_WINDOWS
        ^
../py/nlrx64.c:37:25: note: expanded from macro 'NLR_OS_WINDOWS'
#define NLR_OS_WINDOWS (defined(_WIN32) || defined(__CYGWIN__))
                        ^
../py/nlrx64.c:112:9: error: macro expansion producing 'defined' has undefined behavior [-Werror,-Wexpansion-to-defined]
../py/nlrx64.c:37:44: note: expanded from macro 'NLR_OS_WINDOWS'
#define NLR_OS_WINDOWS (defined(_WIN32) || defined(__CYGWIN__))
                                           ^
4 errors generated.
make: *** [build/py/nlrx64.o] Error 1

If you look at ./build_deps/circuitpython/py/nlrx64.c, on line 37 it has:

#define NLR_OS_WINDOWS (defined(_WIN32) || defined(__CYGWIN__))

On a Mac without Cygwin installed, neither _WIN32 nor CYGWIN will be defined, which leads to the "macro expansion producing 'defined' has undefined behavior" error, failing the build.

If I replace the offending define with #define NLR_OS_WINDOWS 0, everything builds successfully.

However, if I try to install the compiled adafruit_pyportal.mpy file onto my PyPortal device using the newly built adafruit-circuitpython-pyportal-4.x-mpy-3.0.4 bundle, I get the incompatible .mpy error on soft boot. This doesn't happen with the same bundle version as downloaded from the official release.

Thoughts?

I copied pyportal_simpletest.py to cord.py and got an error.

When I copied the code of pyportal_simpletest.py to code.py and made it work, I got the following error.

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Set background to  /pyportal_startup.bmp
ESP firmware: bytearray(b'1.2.2\x00')
Connecting to AP home-AP-n-24
Set background to  0
No SD card found: timeout waiting for v2 card
Traceback (most recent call last):
  File "code.py", line 10, in <module>
  File "adafruit_pyportal.py", line 378, in __init__
TypeError: 'NoneType' object is not subscriptable



Press any key to enter the REPL. Use CTRL-D to reload.

How can I resolve this error?
I'm not familiar with python so I don't know if it's a bug.
I want you to help me.
The version of CircuitPython is 5.3.1.
Nice to meet you.

integration with Adafruit_CircuitPython_MiniMQTT is too fragile

When using pyportal's get_local_time() (and likely fetch()), there
seems to be incompatibilities with MiniMQTT. In the example code.py:

https://github.com/flavio-fernandes/pyportal_station/blob/41af3587d39d01f4ccfd93b41ed8c7ab9ec8986c/code.py#L240-L264

I see that calling pyportal.get_local_time() after client.loop() causes
the error below. My current suspicion is that when pyportal code
does "self._connect_esp()", it rubs MiniMQTT in a bad way. Any attempts
MQTT make to write (ping or publish) after pyportal.get_local_time() no
longer work.

48129.0: DEBUG - Sending PINGREQ
48129.1: DEBUG - Checking PINGRESP
.Sun, 3/Jan/2021 8:50 pm
...Sun, 3/Jan/2021 8:50 pm
..48189.3: DEBUG - KeepAlive period elapsed -                                    requesting a PINGRESP from the server...
48189.3: DEBUG - Sending PINGREQ
48189.3: DEBUG - Checking PINGRESP
.Sun, 3/Jan/2021 8:51 pm
...Sun, 3/Jan/2021 8:51 pm
.20:51:46 Interval localtime triggered
Getting time for timezone America/New_York
struct_time(tm_year=2021, tm_mon=1, tm_mday=3, tm_hour=20, tm_min=51, tm_sec=55, tm_wday=7, tm_yday=3, tm_isdst=None)
.48249.6: DEBUG - KeepAlive period elapsed -                                    requesting a PINGRESP from the server...
48249.6: DEBUG - Sending PINGREQ
48249.6: DEBUG - Checking PINGRESP
.Sun, 3/Jan/2021 8:52 pm
.Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
.Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
.Sun, 3/Jan/2021 8:52 pm
Failed mqtt client loop: Error response to command
.Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
Failed mqtt client loop: Error response to command
.Failed mqtt client loop: Error response to command
48312.3: DEBUG - KeepAlive period elapsed -                                    requesting a PINGRESP from the server...
48312.3: DEBUG - Sending PINGREQ
Failed mqtt client loop: Error response to command
20:53:6 Interval send_status triggered
48321.3: DEBUG - Sending PUBLISH
Topic: /pyportalkitchen/temperature
Msg: b'74.8062'                                
QoS: 0
Retain? False
Error in send_status, retrying in 10s - Failed to send 2 bytes (sent 0)
48321.3: DEBUG - KeepAlive period elapsed -                                    requesting a PINGRESP from the server...
48321.3: DEBUG - Sending PINGREQ
Failed mqtt client loop: Failed to send 2 bytes (sent 0)
.Sun, 3/Jan/2021 8:53 pm
.48324.3: DEBUG - KeepAlive period elapsed -                                    requesting a PINGRESP from the server...
48324.3: DEBUG - Sending PINGREQ
Failed mqtt client loop: Failed to send 2 bytes (sent 0)

push_to_io

with the version 6 its a first that I can reach the image converter from Vienna.
thx a lot to all of you working on requests and PyPortal

But here:

pyportal.push_to_io goes straight in a loop with the message:

An error occured, retrying! 1 - Sending request failed
An error occured, retrying! 1 - Sending request failed
An error occured, retrying! 1 - Sending request failed
An error occured, retrying! 1 - Sending request failed

Herbert

Timezones cannot be an integer

This is mainly a problem because the ESP32SPI secrets file uses the timezone as an integer which is causing PyPortal to crash. I noticed this when trying to use the OpenWeather example. Changing it to a string fixed it and I have submitted a PR to change it to a string by default, but it would still be nice to be able to use numbers.

Improve error handling for HTTP response errors and exception handling for JSON parse parsing in fetch()

fetch makes an HTTP request but does not currently check the HTTP status code. This could be a 5xx error and there's no specific handling for this.

Fifth post on Adafruit Forums: Adafruit CircuitPython and MicroPython: NASA image partially blank on PyPortal has an example of the current error handling too. There is no specific check for an appropriate Content-Type for json, the code optimstically tries to parse the response body and at the moment the exception handling does not cover the KeyError case:

Retrieving data...Reply is OK!
{'code': 500, 'msg': 'Internal Service Error', 'service_version': 'v1'}
Traceback (most recent call last):
File "code.py", line 38, in <module>
File "code.py", line 35, in <module>
File "adafruit_pyportal.py", line 696, in fetch
File "adafruit_pyportal.py", line 693, in fetch
File "adafruit_pyportal.py", line 517, in _json_traverse
KeyError: title

Possible that this line was intended to be only printed for debug too? A useful enhancement would be to print the HTTP status code and if present the response Content-Length:

print("Reply is OK!")

4xx errors are also worth considering particularly because broken servers may 404 requests and buggy servers occasionally respond with a 400, etc. 418 handling could also be considered.

Handling of failures of the adafruit.io services like image converter could also be handled better. I believe that's the cause of KeyError: content-length as discussed in Adafruit Forums: Adafruit CircuitPython and MicroPython: KeyError: content-length.

Handle failure from image converter service or other HttpErrors in `wget()`

If wget() in PortalBase (https://github.com/adafruit/Adafruit_CircuitPython_PortalBase/blob/7a3277af2efb804c7ecf82d8bc87eb95c22dee2e/adafruit_portalbase/network.py#L319) gets some kind of HTTP error, it will raise adafruit_portalbase.network.HttpError, which is a subclass of Exception`.

This exception is not handled when wget() is called here:

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:
.

A scenario where this happened is described in the forums here: the Cleveland Museum of Art may return image URL's that return 404's. These cause the image converter service to return a 422.

I'm thinking that maybe PortalBase should return an easier-to-catch exception, or PyPortal should catch that HttpError and turn it into something else.

PyPortal - Demo code: ValueError: syntax error in JSON (after 72 hours)

I believe I've encountered an error in the PyPoral Library that supports the code.py that retrieves Adafruit famous quotations. After an estimated 72 hours of "burn-in", I found the screen in text mode with a stacktrace-looking stream of text:
rieving data...Reply is OK! (beginning of that has scrolled off screen)
Couldn't parse JSON
The actual "text" value appears truncated, and there is no attribution.
The mentioned locations are:
File code.py line 33 in
File code.py line 30 in
File adafruit_pyportal.py line 661 in fetch
File adafruit_pyportal.py line 657 in fetch
File adafruit_esp32spi/adafruit_esp32spi_requests.py line 110 in json
ValueError: syntax error in JSON

Code done running.Waiting for reload.

PyPortal_Crash_IMG_20190328_221459

wget Does not check for validity of headers prior to use

In the event of an HTTP error, such as polling Adafruit IO api too often, the statement content_length = int(r.headers['content-length']) at line 638 is not valid which causes an unhandled exception.

Traceback (most recent call last):
File "code.py", line 33, in
File "code.py", line 29, in
File "adafruit_pyportal.py", line 845, in fetch
File "adafruit_pyportal.py", line 842, in fetch
File "adafruit_pyportal.py", line 838, in fetch
File "adafruit_pyportal.py", line 832, in fetch
File "adafruit_pyportal.py", line 638, in wget
KeyError: content-length
Code done running. Waiting for reload.
Auto-reload is on. Simply save files over USB to run them or enter REPL to disab
le.
Press any key to enter the REPL. Use CTRL-D to reload.

PyPortal mqtt integration with UI example

Hello all!

For a few months, I've been trying to integrate mqtt with the PyPortal UI example (https://learn.adafruit.com/making-a-pyportal-user-interface-displayio). @makermelissa and @brubell have been wonderful in getting mqtt to work on the PyPortal. Believing I had a clear path, the issue is that the workaround code on this page (https://forums.adafruit.com/viewtopic.php?f=56&t=177198&start=15) does not use the pyportal = PyPortal() protocol. I thought that solved my problem, but when I integrate that into the UI example, it fails because the UI does use pyportal = PyPortal().

On the page, Make It Your Own (https://learn.adafruit.com/making-a-pyportal-user-interface-displayio/make-it-your-own), it challenges us to integrate the mqtt into the UI... which is where I started on this long journey.

I don't know if there are plans to remedy this or if it's better to pull that Make It Your Own page down. I would LOVE to see this resolved though, as my project was designed to rely on this. Thank you for your time and effort and please contact me with any questions.

It would be nice to have a few more examples

Right now the only example is simpletest which (rightly so) only scratches the surface of what this library can do.

It would be great to get a few more examples built up that show the most basic ways to handle and show text, JSON data, and images on the screen.

I can work on adding some of these, but this could also server as a good first issue if anyone is interested.

recent update breaks PyPortal

It looks like #45 breaks this library on the PyPortal! It imports cursorcontrol which imports gamepadshift which is not supported on the PyPortal.

Press any key to enter the REPL. Use CTRL-D to reload.
Adafruit CircuitPython 5.0.0-alpha.0-45-gd99d3bd47-dirty on 2019-07-25; Adafruit PyPortal with samd51j20
>>> 
>>> 
>>> 
>>> import adafruit_pyportal
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "adafruit_pyportal.py", line 60, in <module>
  File "adafruit_cursorcontrol/cursorcontrol_cursormanager.py", line 33, in <module>
ImportError: no module named 'gamepadshift'
>>> 

Allow resizing images to keep full image instead of cropping

Add options to process_image so images can be added on top of background rather than just cropped. I think now that we have bitmap tools, this may be possible, though the images downloaded via the image formatter service are being loaded as ondiskbitmaps to conserve memory and bitmap tools requires the bitmaps to fully be loaded into memory.

Another option (and perhaps it already exists since there doesn't appear to be documentation) is to add a parameter to the image formatter service to let it know what color of background we would like and that would solve the issue.

See https://forums.adafruit.com/viewtopic.php?p=1014450 for more info.

show_QR method call fails

I'm trying to show a QR code on the screen when a touch is detected. Perhaps I'm doing something wrong?

Here's my code:

while True:
    p = ts.touch_point
    if p:
        print('{} was pressed'.format(p))
        light_on_time = time.monotonic()
        if not backlight:
            backlight = 1.0
            pyportal.set_backlight(1.0)
        elif p[1] < 240//3 * 2:
            current_image += 1
            if current_image == len(images):
                current_image = 0
            pyportal.set_background(cwd+images[current_image][0], images[current_image][1]) 
        else:
            pyportal.show_QR(b'http://bit.ly/2Y6zPu7')
    if backlight and (time.monotonic() - light_on_time) > backlight_timeout_seconds:
        backlight = False
        pyportal.set_backlight(0)

    time.sleep(0.05)

I am getting the following error message

Traceback (most recent call last):
  File "code.py", line 51, in <module>
  File "adafruit_pyportal.py", line 845, in show_QR
ValueError: pixel value requires too many bits

text_wrap passed to wrap_text_to_lines instead of text_maxlen

It appears that the text_wrap value is being passed to the underlying library instead of the value of text_maxlen. When omitting text_maxlen and setting text_wrap as if it was maxlen, the library functions as expected.

I'm unsure if this is an undocumented change in behavior or a bug.

From the docs:

text_wrap โ€“ Whether or not to wrap text (for long text data chunks). Defaults to False, no wrapping.
text_maxlen โ€“ The max length of the text for text wrapping. Defaults to 0.

sample code:

pyportal = PyPortal(
    url=DATA_SOURCE,
    json_path=(CARD_NAME, CARD_COST, CARD_TYPE, CARD_TEXT),
    status_neopixel=board.NEOPIXEL,
    text_position=((175, 10), (175, 20), (175, 30), (175, 60)),
    text_color=(0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF),
    text_maxlen=(24, 24, 24, 24),
    text_wrap=True,
    image_json_path=CARD_IMAGE,
    image_resize=(172, 240),
    image_position=(0, 0),
    debug=True
)

With text_maxlen as a list and text_wrap as boolean, a TypeError results

code.py output:
Init display
Init background
Init image path
Init SD Card
Init touchscreen
Init caption
Setting caption to None
Traceback (most recent call last):
  File "code.py", line 29, in <module>
  File "adafruit_pyportal/__init__.py", line 263, in __init__
TypeError: 'bool' object isn't subscriptable

Code done running.

setting text_wrap=(True, True, True, True), results in a ValueError:
Notice the "Wrapping text with length of True" before the trace.

code.py output:
Init display
Init background
Init image path
Init SD Card
Init touchscreen
Init caption
Setting caption to None
Init text area
Init text area
Init text area
Init text area
Connecting to AP <snip>
Retrieving data...Headers: <snip>
Reply is OK!
<snip>
original URL: <snip>
convert URL: https://io.adafruit.com/api/v2/<snip>/integrations/image-formatter?x-aio-key=<snip>
Fetching stream from https://io.adafruit.com/api/v2/<snip>/integrations/image-formatter?x-aio-key=<snip>
Reply is OK!
<snip>
Saving data to  /sd/cache.bmp
Read 41 bytes, 82657 remaining
<snip>
Created file of 82698 bytes in 6.7 seconds
Wrapping text with length of True
Traceback (most recent call last):
  File "code.py", line 38, in <module>
  File "adafruit_pyportal/__init__.py", line 355, in fetch
  File "adafruit_portalbase/__init__.py", line 442, in _fill_text_labels
  File "adafruit_portalbase/__init__.py", line 393, in _fetch_set_text
  File "adafruit_portalbase/__init__.py", line 238, in set_text
  File "adafruit_portalbase/__init__.py", line 137, in wrap_nicely
  File "adafruit_display_text/__init__.py", line 140, in wrap_text_to_lines
  File "adafruit_display_text/__init__.py", line 128, in chunks
ValueError: zero step

This code, although not matching the documentation, works as expected. text_maxlen is omitted and text_wrap is a list of integers, not boolean.

pyportal = PyPortal(
    url=DATA_SOURCE,
    json_path=(CARD_NAME, CARD_COST, CARD_TYPE, CARD_TEXT),
    status_neopixel=board.NEOPIXEL,
    text_position=((175, 10), (175, 20), (175, 30), (175, 60)),
    text_color=(0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF),
    text_wrap=(24, 24, 24, 24),
    image_json_path=CARD_IMAGE,
    image_resize=(172, 240),
    image_position=(0, 0),
    debug=True
)

PyPortal.set_caption() always adds a new caption with no way to remove older ones?

I am trying to update an older PyPortal-based project, and running into a set_caption issue. It used to be that there was a single label for the caption, and its text was set during PyPortal() construction, and then could be updated later using set_caption.

Here is an example project Daily UV Index PyPortal Display that calls pyportal.set_caption() on every update, which is the same basic pattern that I've been using:
https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/9f6d3ea8f8b16d12c6fd3b89cd7ce5bbf0c015ec/PyPortal_UV_Index/code.py#L100-L102

Code with the bug

Today's implementation of PyPortal.set_caption always calls PortalBase.add_text(), and therefore it adds a new label every time:

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)

Here's a link to the add_text implementation

Old Implementation

Looking into the source history, prior to #99, the caption label was created if needed, otherwise it was updated:

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)

This is the behavior I was used to, and is still how I'd expect it to work based on today's documentation

Workaround

I think I can workaround this by ignoring the PyPortal caption, and just managing the label myself?

Fix

I'm not sure how to approach fixing this. The interaction with PortalBase makes it a little complicated. I cloned the repo with the ambition of opening a PR, but I'm not especially familiar with how other folks are using PortalBase / PyPortal.

  • could PyPortal simply remember the index of the caption? if so, how does it move the label / change the color if needed?
  • should there be a way to remove labels in PortalBase, which PyPortal could use to remove / re-create the caption when needed?
  • would remembering the caption label index be fragile - and prone to break if/when the fetched data needs more / fewer labels?
  • should the Adafruit_Learning_System_Guides code be updated to use PyPortal class / set_caption differently?

Set a single pixel

I'd love way to set a single pixel. Is there an existing way to do this?

Problems with adafruit.io image conversion calls (no response and no timeout, short data)

@dhalbert requested that I add this here. I'm experiencing intermittent hangs on my office door sign code, and at least one of those hangs happened while communicating with the adafruit.io image conversion service. The PyPortal never showed any evidence of a response, and no timeouts were triggered.

A second issue with the conversion service has symptoms of the fetch() call not downloading all the BMP data. The content-length header is correct, but only part of the data is received and written to the SD card.

get_local_time is expected to raise RuntimeError when it fails

Currently, adafruit_pyportal::get_local_time() raises the exception
ValueError if the REST call returns anything other than a 200:

@@ -722,7 +722,7 @@ class PyPortal:
         try:
             response = requests.get(api_url, timeout=10)
             if response.status_code != 200:
                raise ValueError(response.text)

That is okay, except that all the examples that use that method
expect RuntimeError exception in order to handle it gracefully:

$ grep -n 'pyportal.get_local_time' -C 4 PyPortal_Alarm_Clock/code.py
237-        # only query the online time once per hour (and on first run)
238-        if (not self.refresh_time) or ((now - self.refresh_time) > 3600):
239-            logger.debug('Fetching time')
240-            try:
241:                pyportal.get_local_time(location=secrets['timezone'])
242-                self.refresh_time = now
243-            except RuntimeError as e:
244-                self.refresh_time = now - 3000   # delay 10 minutes before retrying
245-                logger.error('Some error occured, retrying! - %s', str(e))

All these example do similar handling:
Adafruit_Learning_System_Guides.git

$ grep -lr 'pyportal.get_local_time' *
PyPortal_Alarm_Clock/code.py
PyPortal_CircuitPython_2020/code.py
PyPortal_Electioncal_US/electioncal.py
PyPortal_EventCountdown/eventcountdown.py
PyPortal_EventCountup/eventcountup.py
PyPortal_Halloween_Countdown/code.py
PyPortal_ISS_Tracker/pp_iss_tracker.py
PyPortal_MogwaiClock/mogwai.py
PyPortal_OpenWeather/openweather.py
PyPortal_Quarantine_Clock/code.py
PyPortal_Quarantine_Clock/month_clock.py
PyPortal_Tides/pp_tides_graphical.py
PyPortal_Tides/pp_tides.py
PyPortal_Tides/admiralty_tides.py
PyPortal_Tides/admiralty_tides_graphical.py
PyPortal_Titano_Weather_Station/code.py
PyPortal_Wakeup_Light/wake_up_light.py
PyPortal_WeeklyCountdown/weeklycountdown.py
PyPortal_on_this_day/code.py

I would think that we should either update all the examples, or
make adafruit_pyportal::get_local_time() return RuntimeError

Demos failing to display images on latest release

This was reported on Discord, and has been verified.

With the latest release of PyPortal, if you have a demo that displays an image with or without something (fonts) over it, it will not display. The code will run, and if there are serial prints in the code, those will display on the screen's terminal output.

If you switch to 1.0.3 release, the demos work. I verified this with JP's quote book demo, and the person on Discord verified it with their version of the CuteFuzz demo.

If no JSON Path is provided it gets confused and errors

From @joelguth in #90 (See #90 (comment))

Hmmm... That PR seems to have resolved the issue with that particular example, but as I'm playing around with more examples I'm still seeing some issues related to what appears to be that same area of code in the PyPortal library.

Here's another example with the Weather Station example here.

import time
from calendar import alarms
from calendar import timers
import board
import displayio
from digitalio import DigitalInOut, Direction, Pull
from adafruit_button import Button
from adafruit_pyportal import PyPortal
import openweather_graphics  # pylint: disable=wrong-import-position

# Get wifi details and more from a secrets.py file
try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise

# Use cityname, country code where countrycode is ISO3166 format.
# E.g. "New York, US" or "London, GB"
LOCATION = secrets['location']

# Set up where we'll be fetching data from
DATA_SOURCE = "http://api.openweathermap.org/data/2.5/weather?q="+LOCATION
DATA_SOURCE += "&appid="+secrets['openweather_token']
# You'll need to get a token from openweather.org, looks like 'b6907d289e10d714a6e88b30761fae22'
DATA_LOCATION = []

# Initialize the pyportal object and let us know what data to fetch and where
# to display it
pyportal = PyPortal(url=DATA_SOURCE,
                    json_path=DATA_LOCATION,
                    status_neopixel=board.NEOPIXEL,
                    default_bg=0x000000,
                    debug=True)

display = board.DISPLAY

#  the alarm sound file locations
alarm_sound_trash = "/sounds/trash.wav"
alarm_sound_bed = "/sounds/sleep.wav"
alarm_sound_eat = "/sounds/eat.wav"

#  the alarm sounds in an array that matches the order of the gfx & alarm check-ins
alarm_sounds = [alarm_sound_trash, alarm_sound_bed,
                alarm_sound_eat, alarm_sound_eat, alarm_sound_eat]

#  setting up the bitmaps for the alarms

#  sleep alarm
sleep_bitmap = displayio.OnDiskBitmap(open("/sleepBMP.bmp", "rb"))
sleep_tilegrid = displayio.TileGrid(sleep_bitmap, pixel_shader=displayio.ColorConverter())
group_bed = displayio.Group()
group_bed.append(sleep_tilegrid)

#  trash alarm
trash_bitmap = displayio.OnDiskBitmap(open("/trashBMP.bmp", "rb"))
trash_tilegrid = displayio.TileGrid(trash_bitmap, pixel_shader=displayio.ColorConverter())
group_trash = displayio.Group()
group_trash.append(trash_tilegrid)

#  meal alarm
eat_bitmap = displayio.OnDiskBitmap(open("/eatBMP.bmp", "rb"))
eat_tilegrid = displayio.TileGrid(eat_bitmap, pixel_shader=displayio.ColorConverter())
group_eat = displayio.Group()
group_eat.append(eat_tilegrid)

#  snooze touch screen buttons
#  one for each alarm bitmap
snooze_controls = [
    {'label': "snooze_trash", 'pos': (4, 222), 'size': (236, 90), 'color': None},
    {'label': "snooze_bed", 'pos': (4, 222), 'size': (236, 90), 'color': None},
    {'label': "snooze_eat", 'pos': (4, 222), 'size': (236, 90), 'color': None},
    ]

#  setting up the snooze buttons as buttons
snooze_buttons = []
for s in snooze_controls:
    snooze_button = Button(x=s['pos'][0], y=s['pos'][1],
                           width=s['size'][0], height=s['size'][1],
                           style=Button.RECT,
                           fill_color=s['color'], outline_color=None,
                           name=s['label'])
    snooze_buttons.append(snooze_button)

#  dismiss touch screen buttons
#  one for each alarm bitmap
dismiss_controls = [
    {'label': "dismiss_trash", 'pos': (245, 222), 'size': (230, 90), 'color': None},
    {'label': "dismiss_bed", 'pos': (245, 222), 'size': (230, 90), 'color': None},
    {'label': "dismiss_eat", 'pos': (245, 222), 'size': (230, 90), 'color': None},
    ]

#  setting up the dismiss buttons as buttons
dismiss_buttons = []
for d in dismiss_controls:
    dismiss_button = Button(x=d['pos'][0], y=d['pos'][1],
                            width=d['size'][0], height=d['size'][1],
                            style=Button.RECT,
                            fill_color=d['color'], outline_color=None,
                            name=d['label'])
    dismiss_buttons.append(dismiss_button)

#  adding the touch screen buttons to the different alarm gfx groups
group_trash.append(snooze_buttons[0].group)
group_trash.append(dismiss_buttons[0].group)
group_bed.append(snooze_buttons[1].group)
group_bed.append(dismiss_buttons[1].group)
group_eat.append(snooze_buttons[2].group)
group_eat.append(dismiss_buttons[2].group)

#  setting up the hardware snooze/dismiss buttons
switch_snooze = DigitalInOut(board.D3)
switch_snooze.direction = Direction.INPUT
switch_snooze.pull = Pull.UP

switch_dismiss = DigitalInOut(board.D4)
switch_dismiss.direction = Direction.INPUT
switch_dismiss.pull = Pull.UP

#  grabbing the alarm times from the calendar file
#  'None' is the placeholder for trash, which is weekly rather than daily
alarm_checks = [None, alarms['bed'],alarms['breakfast'],alarms['lunch'],alarms['dinner']]
#  all of the alarm graphics
alarm_gfx = [group_trash, group_bed, group_eat, group_eat, group_eat]

#  allows for the openweather_graphics to show
gfx = openweather_graphics.OpenWeather_Graphics(pyportal.splash, am_pm=True, celsius=False)

#  state machines
localtile_refresh = None
weather_refresh = None
dismissed = None
touched = None
start = None
alarm = None
snoozed = None
touch_button_snooze = None
touch_button_dismiss = None
phys_dismiss = None
phys_snooze = None
mode = 0
button_mode = 0

#  weekday array
weekday = ["Mon.", "Tues.", "Wed.", "Thurs.", "Fri.", "Sat.", "Sun."]

#  weekly alarm setup. checks for weekday and time
weekly_alarms = [alarms['trash']]
weekly_day = [alarms['trash'][0]]
weekly_time = [alarms['trash'][1]]

while True:
    # while esp.is_connected:
    # only query the online time once per hour (and on first run)
    if (not localtile_refresh) or (time.monotonic() - localtile_refresh) > 3600:
        try:
            print("Getting time from internet!")
            pyportal.get_local_time()
            localtile_refresh = time.monotonic()
        except RuntimeError as e:
            print("Some error occured, retrying! -", e)
            continue

    if not alarm:
    # only query the weather every 10 minutes (and on first run)
    #  only updates if an alarm is not active
        if (not weather_refresh) or (time.monotonic() - weather_refresh) > 600:
            try:
                value = pyportal.fetch()
                print("Response is", value)
                gfx.display_weather(value)
                weather_refresh = time.monotonic()
            except RuntimeError as e:
                print("Some error occured, retrying! -", e)
                continue
    #  updates time to check alarms
    #  checks every 30 seconds
    #  identical to def(update_time) in openweather_graphics.py
    if (not start) or (time.monotonic() - start) > 30:
        #  grabs all the time data
        clock = time.localtime()
        date = clock[2]
        hour = clock[3]
        minute = clock[4]
        day = clock[6]
        today = weekday[day]
        format_str = "%d:%02d"
        date_format_str = " %d, %d"
        if hour >= 12:
            hour -= 12
            format_str = format_str+" PM"
        else:
            format_str = format_str+" AM"
        if hour == 0:
            hour = 12
        #  formats date display
        today_str = today
        time_str = format_str % (hour, minute)
        #  checks for weekly alarms
        for i in weekly_alarms:
            w = weekly_alarms.index(i)
            if time_str == weekly_time[w] and today == weekly_day[w]:
                print("trash time")
                alarm = True
                if alarm and not dismissed and not snoozed:
                    display.show(alarm_gfx[w])
                    pyportal.play_file(alarm_sounds[w])
                mode = w
                print("mode is:", mode)
        #  checks for daily alarms
        for i in alarm_checks:
            a = alarm_checks.index(i)
            if time_str == alarm_checks[a]:
                alarm = True
                if alarm and not dismissed and not snoozed:
                    display.show(alarm_gfx[a])
                    pyportal.play_file(alarm_sounds[a])
                mode = a
                print(mode)
        #  calls update_time() from openweather_graphics to update
        #  clock display
        gfx.update_time()
        gfx.update_date()
        #  resets time counter
        start = time.monotonic()

    #  allows for the touchscreen buttons to work
    if mode > 1:
        button_mode = 2
    else:
        button_mode = mode
        #  print("button mode is", button_mode)

    #  hardware snooze/dismiss button setup
    if switch_dismiss.value and phys_dismiss:
        phys_dismiss = False
    if switch_snooze.value and phys_snooze:
        phys_snooze = False
    if not switch_dismiss.value and not phys_dismiss:
        phys_dismiss = True
        print("pressed dismiss button")
        dismissed = True
        alarm = False
        display.show(pyportal.splash)
        touched = time.monotonic()
        mode = mode
    if not switch_snooze.value and not phys_snooze:
        phys_snooze = True
        print("pressed snooze button")
        display.show(pyportal.splash)
        snoozed = True
        alarm = False
        touched = time.monotonic()
        mode = mode

    #  touchscreen button setup
    touch = pyportal.touchscreen.touch_point
    if not touch and touch_button_snooze:
        touch_button_snooze = False
    if not touch and touch_button_dismiss:
        touch_button_dismiss = False
    if touch:
        if snooze_buttons[button_mode].contains(touch) and not touch_button_snooze:
            print("Touched snooze")
            display.show(pyportal.splash)
            touch_button_snooze = True
            snoozed = True
            alarm = False
            touched = time.monotonic()
            mode = mode
        if dismiss_buttons[button_mode].contains(touch) and not touch_button_dismiss:
            print("Touched dismiss")
            dismissed = True
            alarm = False
            display.show(pyportal.splash)
            touch_button_dismiss = True
            touched = time.monotonic()
            mode = mode

    #  this is a little delay so that the dismissed state
    #  doesn't collide with the alarm if it's dismissed
    #  during the same time that the alarm activates
    if (not touched) or (time.monotonic() - touched) > 70:
        dismissed = False
    #  snooze portion
    #  pulls snooze_time from calendar and then when it's up
    #  splashes the snoozed alarm's graphic, plays the alarm sound and goes back into
    #  alarm state
    if (snoozed) and (time.monotonic() - touched) > timers['snooze_time']:
        print("snooze over")
        snoozed = False
        alarm = True
        mode = mode
        display.show(alarm_gfx[mode])
        pyportal.play_file(alarm_sounds[mode])
        print(mode)

This time around I'm getting the error below:

Socket missing recv_into. Using more memory to be compatible
Time request:  https://io.adafruit.com/api/v2/*redacted*/integrations/time/strftime?x-aio-key=*redacted*=America/Chicago&fmt=%25Y-%25m-%25d+%25H%3A%25M%3A%25S.%25L+%25j+%25u+%25z+%25Z
Time reply:  2020-11-03 18:57:13.093 308 2 -0600 CST
struct_time(tm_year=2020, tm_mon=11, tm_mday=3, tm_hour=18, tm_min=57, tm_sec=13, tm_wday=2, tm_yday=308, tm_isdst=None)
Free mem:  48256
Retrieving data...Socket missing recv_into. Using more memory to be compatible
Headers: {'access-control-allow-credentials': 'true', 'server': 'openresty', 'content-type': 'application/json; charset=utf-8', 'connection': 'keep-alive', 'content-length': '466', 'access-control-allow-origin': '*', 'date': 'Wed, 04 Nov 2020 00:57:24 GMT', 'x-cache-key': '/data/2.5/weather?q=chicago,%20us', 'access-control-allow-methods': 'GET, POST'}
Reply is OK!
Detected Content Type 2
{'timezone': -21600, 'sys': {'type': 1, 'sunrise': 1604406361, 'country': 'US', 'id': 4861, 'sunset': 1604443335}, 'base': 'stations', 'main': {'temp_min': 288.71, 'pressure': 1017, 'feels_like': 285.01, 'humidity': 34, 'temp_max': 291.15, 'temp': 289.76}, 'visibility': 10000, 'id': 4887398, 'clouds': {'all': 1}, 'coord': {'lon': -87.65, 'lat': 41.85}, 'name': 'Chicago', 'cod': 200, 'weather': [{'id': 800, 'icon': '01n', 'main': 'Clear', 'description': 'clear sky'}], 'dt': 1604451238, 'wind': {'speed': 4.1, 'deg': 200}}
Some error occured, retrying! - Cannot access text after getting content or json

Similar to before, if I use the pyportal library from Oct 10th i'm not experiencing any issues. It's as if the content type is not properly being parsed or identified?

Let me know if you'd like me to open a different Issue for this.

Thanks and Sorry for breaking things!

pyportal_simpletest - TFT in use

the pyportal_simpletest.py fails - with CP 5.0 at least

Press any key to enter the REPL. Use CTRL-D to reload.
Adafruit CircuitPython 5.0.0-alpha.0-45-gd99d3bd47 on 2019-07-28; Adafruit PyPortal with samd51j20
>>> 
>>> 
>>> import pyportal_simpletest
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "pyportal_simpletest.py", line 10, in <module>
 File "adafruit_pyportal.py", line 172, in __init__
ValueError: TFT_BACKLIGHT in use
>>> 

get_local_time doesn't properly check the http response status code before attempting to parse the time

response = requests.get(api_url)

If the request is an error, such as 404 for User not found (example url), then this code will attempt to parse the response into a datetime anyway.

It should makes sure the status code is 200 before attempting to parse. If it's not, it should throw an exception and include the response text in the exception to help with debugging. As it stands, this will raise a very unhelpful error:

Traceback (most recent call last):
  File "code.py", line 44, in <module>
  File "adafruit_pyportal.py", line 629, in get_local_time
  File "adafruit_pyportal.py", line 625, in get_local_time
ValueError: invalid syntax for integer with base 10 

This would be an excellent first-time issue for someone.

Missing Type Annotations

There are missing type annotations for some functions in this library.

The typing module does not exist on CircuitPython devices so the import needs to be wrapped in try/except to catch the error for missing import. There is an example of how that is done here:

try:
    from typing import List, Tuple
except ImportError:
    pass

Once imported the typing annotations for the argument type(s), and return type(s) can be added to the function signature. Here is an example of a function that has had this done already:

def wrap_text_to_pixels(
    string: str, max_width: int, font=None, indent0: str = "", indent1: str = ""
) -> List[str]:

If you are new to Git or Github we have a guide about contributing to our projects here: https://learn.adafruit.com/contribute-to-circuitpython-with-git-and-github

There is also a guide that covers our CI utilities and how to run them locally to ensure they will pass in Github Actions here: https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/check-your-code In particular the pages: Sharing docs on ReadTheDocs and Check your code with pre-commit contain the tools to install and commands to run locally to run the checks.

If you are attempting to resolve this issue and need help, you can post a comment on this issue and tag both @FoamyGuy and @kattni or reach out to us on Discord: https://adafru.it/discord in the #circuitpython-dev channel.

The following locations are reported by mypy to be missing type annotations:

  • adafruit_pyportal/network.py:75
  • adafruit_pyportal/network.py:117
  • adafruit_pyportal/network.py:139
  • adafruit_pyportal/peripherals.py:52
  • adafruit_pyportal/peripherals.py:129
  • adafruit_pyportal/peripherals.py:143
  • adafruit_pyportal/peripherals.py:176
  • adafruit_pyportal/graphics.py:43
  • adafruit_pyportal/graphics.py:50
  • adafruit_pyportal/__init__.py:95
  • adafruit_pyportal/__init__.py:273
  • adafruit_pyportal/__init__.py:297

Support multiple SSID and multiple passwords

This is easily added by detecting if the SSID and password are not strings and iterating over them.

ATMakersBill: Hi folks, is there a way to list multiple SSIDs in the secrets.py file for the PyPortal and other WiFi boards?
anecdata: I think secrets.py is just a convenient place to put private stuff. Could make WiFi credentials an iterable, and randomize or cycle through in your code.
ATMakersBill: The PyPortal class has code for managing the secrets file - I'm asking if that has any ability to use multiples.
kjw: @ATMakersBill What's the use case for that? Portability between different places?
ATMakersBill: yes
cater: i think it's pretty hard wired: https://github.com/adafruit/Adafruit_CircuitPython_PyPortal/blob/master/adafruit_pyportal.py#L633

A second use-case is to deal with changing passwords, two passwords for one SSID allows for a device to work near-seamlessly across password changes.

Actually this probably belongs in adafruit_esp32spi.ESP_SPIcontrol.connect() over in https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/blob/master/adafruit_esp32spi/adafruit_esp32spi.py but will leave that discussion for those in the know.

play_file() with wait_to_finish=False does not play

In the currently released version of this library calling pyportal.peripherals.play_file("somefile.wav", wait_to_finish=False) results in no audio playing.

Full reproducer script:

import board
from adafruit_pyportal import PyPortal

# Create the PyPortal object
pyportal = PyPortal()

# Set display to show REPL
board.DISPLAY.show(None)

pyportal.peripherals.play_file("piano2.wav", wait_to_finish=False)

while True:
    pass

Running the same code on version 6.0.0 of this library does play the wav file.

I think the change to use with context processor is what is causing it not to play the file when wait_to_finish is False

In this section of code it returns in the case of False for wait_to_finish the return causes it to close the with context which closes the file and causes it not to be able to be played.

with open(file_name, "rb") as wavfile:
wavedata = audiocore.WaveFile(wavfile)
self._speaker_enable.value = True
self.audio.play(wavedata)
if not wait_to_finish:
return
while self.audio.playing:
pass
self._speaker_enable.value = False

Unless there is some way to make the lifetime of the with block continue to remain open after the play_file() function returns I think we would need to remove the usage of the with. We could store the reference to the open file on self and close it on subsequent calls before opening a new one.

PyPortal Titano Latest Library Issues

There appears to be something that changed in the October 13th release of the CircuitPython library (adafruit-circuitpython-bundle-6.x-mpy-20201013) that breaks the PyPortal Titano. I have tried both 5.x and 6.x paths and the issue appears on both.

I consistently get the following error when experimenting with various projects:

TypeError: 'NoneType' object is not subscriptable

For Example, on the PyPortal_Bitcoin project here i get:

Traceback (most recent call last):
  File "code.py", line 42, in <module>
  File "code.py", line 39, in <module>
  File "adafruit_pyportal.py", line 991, in fetch
  File "adafruit_pyportal.py", line 988, in fetch
  File "adafruit_pyportal.py", line 686, in _json_traverse
TypeError: 'NoneType' object is not subscriptable

If i download any CircuitPython release before the October 13th release I don't have any issues (i.e. October 10th).

Let me know if there's anything additional I can do to test, or any additional debugging I can enable to help track down the issue.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.