adafruit / Adafruit_CircuitPython_PyPortal

CircuitPython driver for Adafruit PyPortal.
MIT License
46 stars 57 forks source link

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

Closed uxp closed 3 years ago

uxp commented 3 years ago

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.

uxp commented 3 years ago

An interesting observation I just made: Swapping the library for the source .py version on my PyPortal device does not exhibit this issue. Keeping the "compiled" .mpy version of the library does show this issue.

I've also tried various firmware versions, such as adafruit-circuitpython-pyportal-en_US-7.0.0-alpha.1 with the library as taken from adafruit-circuitpython-bundle-7.x-mpy-20210410.zip, as well as adafruit-circuitpython-pyportal-en_US-6.2.0 with the libraries taken from adafruit-circuitpython-bundle-6.x-mpy-20210409.zip, which all exhibit this issue.

Maybe this isn't as clear as waiting for the network to initialize, as I'm a bit more confused why the source version behaves differently now.

Craigxyzzy commented 3 years ago

Issue resolved: forums.adafruit.com/viewtopic.php?t=177959 forums.adafruit.com/viewtopic.php?t=177980

makermelissa commented 3 years ago

Fixed via #109. Thank you @Neradoc