ESP32 as a co-processor(slave)

Posting this here in the event anyone wants to go down the route I needed to. i hope this is allowed, and wish i had a better way to present this.

I have a ton of ESP32’s laying around, so why not use one as a co-processor as both Adafruit & Arduino do. don’t be confused, the ESP32 is a Slave device. the OpenMV Cam H7 is a master.

Nina-FW project here: GitHub - arduino/nina-fw: Firmware for u-blox NINA W102 WiFi/BT module
Adafruit’s ESP32SPI libraries for the circuit python project here: GitHub - adafruit/Adafruit_CircuitPython_ESP32SPI: ESP32 as wifi with SPI interface
Adafruit’s minimqtt library: GitHub - adafruit/Adafruit_CircuitPython_MiniMQTT: MQTT Client Library for CircuitPython

based on their fine work, i present the (ugly, but functional) ports for anyone else in need or interested.

A few things. i did, and learned along the way. hopefully it helps if you try this, its taken me 6 months off and on to nail it all down. my use case was to use a Lolin D32 Pro, so i could offload alot of work i wanted a separate device to do on its own; lights, battery management, charging, and monitoring, wifi, and an LCD.

  1. yes, its ugly. but it’s completely functional
  2. i still can’t get my head around github, so its not posted there.
  3. this isn’t a 1:1 port. a few things had to be done, including:
    a. borrowing code from some of their base libraries and incorporating it and some other modifications because of the minute code differences between micropython and circuit python.
    b. changed some of it to make it non-blocking on wifi manager.
    c. added a few of my own inclusions
    d. i’d strong recommend whenever using the socket to call the “settimeout” function with any value other than the 0 it starts with. else, any internet delay will cause it to raise an exception. its not forgiving.
    e. i renamed the libraries, and as such, their references.
  4. put the libraries on the SD card directly. do not under any circumstance allow openmv’s IDE to transfer them to the SD card. the IDE transfers them fine to the computer, but something it does when transferring them to the SD card breaks them, and nothing works.
  5. i myself cannot get the SPI speed above 19.999999 Mhz. at 20 Mhz, everything goes haywire. I tested this with both a WROOM and a WROVER module with both VSPI and HSPI and regardless of polarity and phase, and it just can’t do 20mhz or higher.
  6. the reset pin needs to be set to “IN” in order to flash the ESP32 if hardwired.
  7. Polarity and phase must both be set to 1. it won’t work under any other combination. i bought an analyzer to check this.
  8. this works with version 1.5 of nina-fw.

If you have any suggestions, please let me know.

without further ado.

ESP32SPI.py

"""
`esp32spi`
================================================================================
CircuitPython driver library for using ESP32 as WiFi co-processor using SPI ported to the
OpenMV Cam.
Original source here: https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI
* Author(s): ladyada
* Forked by: Nezra
Implementation Notes
--------------------
**Hardware:**
ESP32 variant of your choice with the Nina-FW.
OpenMV Cam
"""
import struct
import utime
import time
from micropython import const
from machine import Pin
from digitalio import Direction

__version__ = "0.0.0-auto.0"

# pylint: disable=bad-whitespace
_SET_NET_CMD           = const(0x10)
_SET_PASSPHRASE_CMD    = const(0x11)
_SET_AP_NET_CMD        = const(0x18)
_SET_AP_PASSPHRASE_CMD = const(0x19)
_SET_DEBUG_CMD         = const(0x1A)

_GET_CONN_STATUS_CMD   = const(0x20)
_GET_IPADDR_CMD        = const(0x21)
_GET_MACADDR_CMD       = const(0x22)
_GET_CURR_SSID_CMD     = const(0x23)
_GET_CURR_BSSID_CMD    = const(0x24)
_GET_CURR_RSSI_CMD     = const(0x25)
_GET_CURR_ENCT_CMD     = const(0x26)

_SCAN_NETWORKS         = const(0x27)
_START_SERVER_TCP_CMD  = const(0x28)
_GET_SOCKET_CMD        = const(0x3F)
_GET_STATE_TCP_CMD     = const(0x29)
_DATA_SENT_TCP_CMD     = const(0x2A)
_AVAIL_DATA_TCP_CMD    = const(0x2B)
_GET_DATA_TCP_CMD      = const(0x2C)
_START_CLIENT_TCP_CMD  = const(0x2D)
_STOP_CLIENT_TCP_CMD   = const(0x2E)
_GET_CLIENT_STATE_TCP_CMD = const(0x2F)
_DISCONNECT_CMD        = const(0x30)
_GET_IDX_RSSI_CMD      = const(0x32)
_GET_IDX_ENCT_CMD      = const(0x33)
_REQ_HOST_BY_NAME_CMD  = const(0x34)
_GET_HOST_BY_NAME_CMD  = const(0x35)
_START_SCAN_NETWORKS   = const(0x36)
_GET_FW_VERSION_CMD    = const(0x37)
_GET_TIME              = const(0x3B)
_GET_IDX_BSSID_CMD     = const(0x3C)
_GET_IDX_CHAN_CMD      = const(0x3D)
_PING_CMD              = const(0x3E)

_SEND_DATA_TCP_CMD     = const(0x44)
_GET_DATABUF_TCP_CMD   = const(0x45)
_SET_ENT_IDENT_CMD     = const(0x4A)
_SET_ENT_UNAME_CMD     = const(0x4B)
_SET_ENT_PASSWD_CMD    = const(0x4C)
_SET_ENT_ENABLE_CMD    = const(0x4F)
_SET_CLI_CERT          = const(0x40)
_SET_PK                = const(0x41)

_SET_PIN_MODE_CMD      = const(0x50)
_SET_DIGITAL_WRITE_CMD = const(0x51)
_SET_ANALOG_WRITE_CMD  = const(0x52)
_SET_DIGITAL_READ_CMD  = const(0x53)
_SET_ANALOG_READ_CMD   = const(0x54)

_START_CMD             = const(0xE0)
_END_CMD               = const(0xEE)
_ERR_CMD               = const(0xEF)
_REPLY_FLAG            = const(1<<7)
_CMD_FLAG              = const(0)

SOCKET_CLOSED      = const(0)
SOCKET_LISTEN      = const(1)
SOCKET_SYN_SENT    = const(2)
SOCKET_SYN_RCVD    = const(3)
SOCKET_ESTABLISHED = const(4)
SOCKET_FIN_WAIT_1  = const(5)
SOCKET_FIN_WAIT_2  = const(6)
SOCKET_CLOSE_WAIT  = const(7)
SOCKET_CLOSING     = const(8)
SOCKET_LAST_ACK    = const(9)
SOCKET_TIME_WAIT   = const(10)

WL_NO_SHIELD          = const(0xFF)
WL_NO_MODULE          = const(0xFF)
WL_IDLE_STATUS        = const(0)
WL_NO_SSID_AVAIL      = const(1)
WL_SCAN_COMPLETED     = const(2)
WL_CONNECTED          = const(3)
WL_CONNECT_FAILED     = const(4)
WL_CONNECTION_LOST    = const(5)
WL_DISCONNECTED       = const(6)
WL_AP_LISTENING       = const(7)
WL_AP_CONNECTED       = const(8)
WL_AP_FAILED          = const(9)

ADC_ATTEN_DB_0   = const(0)
ADC_ATTEN_DB_2_5 = const(1)
ADC_ATTEN_DB_6   = const(2)
ADC_ATTEN_DB_11  = const(3)

class SPIDevice:
    def __init__(self, spi, chip_select=None, *,
                 baudrate=100000, polarity=0, phase=0, extra_clocks=0):
        self.spi = spi
        self.baudrate = baudrate
        self.polarity = polarity
        self.phase = phase
        self.extra_clocks = extra_clocks
        self.chip_select = chip_select
        if self.chip_select:
            self.chip_select.high()
    def __enter__(self):
        if self.chip_select:
            self.chip_select.low()
        return self.spi
    def __exit__(self, *exc):
        if self.chip_select:
            self.chip_select.high()
        if self.extra_clocks > 0:
            buf = bytearray(1)
            buf[0] = 0xff
            clocks = self.extra_clocks // 8
            if self.extra_clocks % 8 != 0:
                clocks += 1
            for _ in range(clocks):
                self.spi.write(buf)
        return False

class ESP_SPIcontrol:
    """A class that will talk to an ESP32 module programmed with special firmware
    that lets it act as a fast an efficient WiFi co-processor"""
    TCP_MODE = const(0)
    UDP_MODE = const(1)
    TLS_MODE = const(2)

    def __init__(self, spi, cs_pin, ready_pin, reset_pin, gpio0_pin=None, *, debug=False):
        self._debug = debug
        self.set_psk = False
        self.set_crt = False
        self._buffer = bytearray(10)
        self._pbuf = bytearray(1)
        self._sendbuf = bytearray(256)
        self._socknum_ll = [[0]]

        self._spi_device = SPIDevice(spi, cs_pin)#, baudrate=8000000)
        self._cs = cs_pin
        self._ready = ready_pin
        self._reset = reset_pin
        self._gpio0 = gpio0_pin
        #self._cs.direction = Direction.OUTPUT
        #self._ready.direction = Direction.INPUT
        #self._reset.direction = Direction.OUTPUT
        #if self._gpio0:
            #self._gpio0.direction = Direction.INPUT
        self.reset()

    def reset(self):
        """Hard reset the ESP32 using the reset pin"""
        if self._debug:
            print("Reset ESP32")
        self._cs.high()
        self._reset.low()
        utime.sleep_ms(100)
        self._reset.high()
        utime.sleep(1)

    def _wait_for_ready(self):
        """Wait until the ready pin goes low"""
        if self._debug:
            print("Wait for ESP32 ready", end='')
            print(self._ready.value())
        times = time.ticks()
        while (time.ticks() - times) < 10000:
            if not self._ready.value():
                break
            if self._debug:
                print('.', end='')
                utime.sleep_ms(50)
        else:
            raise RuntimeError("ESP32 not responding")
        if self._debug:
            print()

    def _send_command(self, cmd, params=None, *, param_len_16=False):
        """Send over a command with a list of parameters"""
        if not params:
            params = ()

        packet_len = 4
        for i, param in enumerate(params):
            packet_len += len(param)
            packet_len += 1
            if param_len_16:
                packet_len += 1
        while packet_len % 4 != 0:
            packet_len += 1
        if packet_len > len(self._sendbuf):
            self._sendbuf = bytearray(packet_len)

        self._sendbuf[0] = _START_CMD
        self._sendbuf[1] = cmd & ~_REPLY_FLAG
        self._sendbuf[2] = len(params)

        # handle parameters here
        ptr = 3
        for i, param in enumerate(params):
            if self._debug:
                print("\tSending param %d is %d bytes long" % (i, len(param)))
            if param_len_16:
                self._sendbuf[ptr] = (len(param) >> 8) & 0xFF
                ptr += 1
            self._sendbuf[ptr] = len(param) & 0xFF
            ptr += 1
            for j, par in enumerate(param):
                self._sendbuf[ptr+j] = par
            ptr += len(param)
        self._sendbuf[ptr] = _END_CMD

        self._wait_for_ready()
        with self._spi_device as spi:
            times = time.ticks()
            while (time.ticks() - times) < 1000:
                if self._ready.value():
                    break
            else:
                raise RuntimeError("ESP32 timed out on SPI select")
            spi.write(self._sendbuf) #, start=0, end=packet_len)
            if self._debug:
                print("Wrote: ", [hex(b) for b in self._sendbuf[0:packet_len]])

    def _read_byte(self, spi):
        """Read one byte from SPI"""
        spi.readinto(self._pbuf)
        if self._debug:
            print("Read:", hex(self._pbuf[0]))
        return self._pbuf[0]

    def _read_bytes(self, spi, buffer, start=0, end=None):
        """Read many bytes from SPI"""
        if not end:
            end = len(buffer)
        spi.readinto(buffer) #, start=start,end=end)
        if self._debug:
            print("\t\tRead:", [hex(i) for i in buffer])

    def _wait_spi_char(self, spi, desired):
        """Read a byte with a time-out, and if we get it, check that its what we expect"""
        times = time.ticks()
        while (time.ticks() - times) < 100:
            r = self._read_byte(spi)
            if r == _ERR_CMD:
                raise RuntimeError("Error response to command")
            if r == desired:
                return True
        raise RuntimeError("Timed out waiting for SPI char")

    def _check_data(self, spi, desired):
        """Read a byte and verify its the value we want"""
        r = self._read_byte(spi)

        if r != desired:
            raise RuntimeError("Expected %02X but got %02X" % (desired, r))

    def _wait_response_cmd(self, cmd, num_responses=None, *, param_len_16=False):
        """Wait for ready, then parse the response"""
        self._wait_for_ready()

        responses = []
        with self._spi_device as spi:
            times = time.ticks()
            while (time.ticks() - times) < 1000:
                if self._ready.value():
                    break
            else:
                raise RuntimeError("ESP32 timed out on SPI select")

            self._wait_spi_char(spi, _START_CMD)
            self._check_data(spi, cmd | _REPLY_FLAG)
            if num_responses is not None:
                self._check_data(spi, num_responses)
            else:
                num_responses = self._read_byte(spi)
            for num in range(num_responses):
                param_len = self._read_byte(spi)
                if param_len_16:
                    param_len <<= 8
                    param_len |= self._read_byte(spi)
                if self._debug:
                    print("\tParameter %d length is %d" % (num, param_len))
                response = bytearray(param_len)
                self._read_bytes(spi, response)
                responses.append(response)
            self._check_data(spi, _END_CMD)

        if self._debug:
            print("Read %d: " % len(responses[0]), responses)
        return responses

    def _send_command_get_response(self, cmd, params=None, *,
                                   reply_params=1, sent_param_len_16=False,
                                   recv_param_len_16=False):
        """Send a high level SPI command, wait and return the response"""
        self._send_command(cmd, params, param_len_16=sent_param_len_16)
        return self._wait_response_cmd(cmd, reply_params, param_len_16=recv_param_len_16)

    @property
    def status(self):
        """The status of the ESP32 WiFi core. Can be WL_NO_SHIELD or WL_NO_MODULE
        (not found), WL_IDLE_STATUS, WL_NO_SSID_AVAIL, WL_SCAN_COMPLETED,
        WL_CONNECTED, WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED,
        WL_AP_LISTENING, WL_AP_CONNECTED, WL_AP_FAILED"""
        if self._debug:
            print("Connection status")
        resp = self._send_command_get_response(_GET_CONN_STATUS_CMD)
        if self._debug:
            print("Conn status:", resp[0][0])
        return resp[0][0]

    @property
    def firmware_version(self):
        """A string of the firmware version on the ESP32"""
        if self._debug:
            print("Firmware version")
        resp = self._send_command_get_response(_GET_FW_VERSION_CMD)
        return resp[0]

    @property
    def MAC_address(self):
        """A bytearray containing the MAC address of the ESP32"""
        if self._debug:
            print("MAC address")
        resp = self._send_command_get_response(_GET_MACADDR_CMD, [b'\xFF'])
        return resp[0]

    def start_scan_networks(self):
        """Begin a scan of visible access points. Follow up with a call
        to 'get_scan_networks' for response"""
        if self._debug:
            print("Start scan")
        resp = self._send_command_get_response(_START_SCAN_NETWORKS)
        if resp[0][0] != 1:
            raise RuntimeError("Failed to start AP scan")

    def get_scan_networks(self):
        """The results of the latest SSID scan. Returns a list of dictionaries with
        'ssid', 'rssi' and 'encryption' entries, one for each AP found"""
        self._send_command(_SCAN_NETWORKS)
        names = self._wait_response_cmd(_SCAN_NETWORKS)
        APs = []
        for i, name in enumerate(names):
            a_p = {'ssid': name}
            rssi = self._send_command_get_response(_GET_IDX_RSSI_CMD, ((i,),))[0]
            a_p['rssi'] = struct.unpack('<i', rssi)[0]
            encr = self._send_command_get_response(_GET_IDX_ENCT_CMD, ((i,),))[0]
            a_p['encryption'] = encr[0]
            bssid = self._send_command_get_response(_GET_IDX_BSSID_CMD, ((i,),))[0]
            a_p['bssid'] = bssid
            chan = self._send_command_get_response(_GET_IDX_CHAN_CMD, ((i,),))[0]
            a_p['channel'] = chan[0]
            APs.append(a_p)
        return APs

    def scan_networks(self):
        """Scan for visible access points, returns a list of access point details.
         Returns a list of dictionaries with 'ssid', 'rssi' and 'encryption' entries,
         one for each AP found"""
        self.start_scan_networks()
        for _ in range(10):
            utime.sleep(2)
            APs = self.get_scan_networks()
            if APs:
                return APs
        return None

    def wifi_set_network(self, ssid):
        """Tells the ESP32 to set the access point to the given ssid"""
        resp = self._send_command_get_response(_SET_NET_CMD, [ssid])
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set network")

    def wifi_set_passphrase(self, ssid, passphrase):
        """Sets the desired access point ssid and passphrase"""
        resp = self._send_command_get_response(_SET_PASSPHRASE_CMD, [ssid, passphrase])
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set passphrase")

    def wifi_set_entidentity(self, ident):
        """Sets the WPA2 Enterprise anonymous identity"""
        resp = self._send_command_get_response(_SET_ENT_IDENT_CMD, [ident])
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set enterprise anonymous identity")

    def wifi_set_entusername(self, username):
        """Sets the desired WPA2 Enterprise username"""
        resp = self._send_command_get_response(_SET_ENT_UNAME_CMD, [username])
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set enterprise username")

    def wifi_set_entpassword(self, password):
        """Sets the desired WPA2 Enterprise password"""
        resp = self._send_command_get_response(_SET_ENT_PASSWD_CMD, [password])
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set enterprise password")

    def wifi_set_entenable(self):
        """Enables WPA2 Enterprise mode"""
        resp = self._send_command_get_response(_SET_ENT_ENABLE_CMD)
        if resp[0][0] != 1:
            raise RuntimeError("Failed to enable enterprise mode")

    def _wifi_set_ap_network(self, ssid, channel):
        """Creates an Access point with SSID and Channel"""
        resp = self._send_command_get_response(_SET_AP_NET_CMD, [ssid, channel])
        if resp[0][0] != 1:
            raise RuntimeError("Failed to setup AP network")

    def _wifi_set_ap_passphrase(self, ssid, passphrase, channel):
        """Creates an Access point with SSID, passphrase, and Channel"""
        resp = self._send_command_get_response(_SET_AP_PASSPHRASE_CMD, [ssid, passphrase, channel])
        if resp[0][0] != 1:
            raise RuntimeError("Failed to setup AP password")

    @property
    def ssid(self):
        """The name of the access point we're connected to"""
        resp = self._send_command_get_response(_GET_CURR_SSID_CMD, [b'\xFF'])
        return resp[0]

    @property
    def bssid(self):
        """The MAC-formatted service set ID of the access point we're connected to"""
        resp = self._send_command_get_response(_GET_CURR_BSSID_CMD, [b'\xFF'])
        return resp[0]

    @property
    def rssi(self):
        """The receiving signal strength indicator for the access point we're
        connected to"""
        resp = self._send_command_get_response(_GET_CURR_RSSI_CMD, [b'\xFF'])
        return struct.unpack('<i', resp[0])[0]

    @property
    def network_data(self):
        """A dictionary containing current connection details such as the 'ip_addr',
        'netmask' and 'gateway'"""
        resp = self._send_command_get_response(_GET_IPADDR_CMD, [b'\xFF'], reply_params=3)
        return {'ip_addr': resp[0], 'netmask': resp[1], 'gateway': resp[2]}

    @property
    def ip_address(self):
        """Our local IP address"""
        return self.network_data['ip_addr']

    @property
    def is_connected(self):
        """Whether the ESP32 is connected to an access point"""
        try:
            return self.status == WL_CONNECTED
        except RuntimeError:
            self.reset()
            return False

    @property
    def ap_listening(self):
        """Returns if the ESP32 is in access point mode and is listening for connections"""
        try:
            return self.status == WL_AP_LISTENING
        except RuntimeError:
            self.reset()
            return False

    def connect(self, secrets):
        """Connect to an access point using a secrets dictionary
        that contains a 'ssid' and 'password' entry"""
        self.connect_AP(secrets['ssid'], secrets['password'])

    def connect_AP(self, ssid, password, timeout_s=10):
        """
        Connect to an access point with given name and password.
        Will wait until specified timeout seconds and return on success
        or raise an exception on failure.
        :param ssid: the SSID to connect to
        :param passphrase: the password of the access point
        :param timeout_s: number of seconds until we time out and fail to create AP
        """
        if self._debug:
            print("Connect to AP", ssid, password)
        if isinstance(ssid, str):
            ssid = bytes(ssid, 'utf-8')
        if password:
            if isinstance(password, str):
                password = bytes(password, 'utf-8')
            self.wifi_set_passphrase(ssid, password)
        else:
            self.wifi_set_network(ssid)
        times = time.ticks()
        while (time.ticks() - times) < (timeout_s*1000):
            stat = self.status
            if stat == WL_CONNECTED:
                return stat
            utime.sleep_ms(50)
        if stat in (WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED):
            raise RuntimeError("Failed to connect to ssid", ssid)
        if stat == WL_NO_SSID_AVAIL:
            raise RuntimeError("No such ssid", ssid)
        raise RuntimeError("Unknown error 0x%02X" % stat)

    def create_AP(self, ssid, password, channel=1, timeout=10): # pylint: disable=invalid-name
        """
        Create an access point with the given name, password, and channel.
        Will wait until specified timeout seconds and return on success
        or raise an exception on failure.
        :param str ssid: the SSID of the created Access Point. Must be less than 32 chars.
        :param str password: the password of the created Access Point. Must be 8-63 chars.
        :param int channel: channel of created Access Point (1 - 14).
        :param int timeout: number of seconds until we time out and fail to create AP
        """
        if len(ssid) > 32:
            raise RuntimeError("ssid must be no more than 32 characters")
        if password and (len(password) < 8 or len(password) > 64):
            raise RuntimeError("password must be 8 - 63 characters")
        if channel < 1 or channel > 14:
            raise RuntimeError("channel must be between 1 and 14")

        if isinstance(channel, int):
            channel = bytes(channel)
        if isinstance(ssid, str):
            ssid = bytes(ssid, 'utf-8')
        if password:
            if isinstance(password, str):
                password = bytes(password, 'utf-8')
            self._wifi_set_ap_passphrase(ssid, password, channel)
        else:
            self._wifi_set_ap_network(ssid, channel)

        times = time.ticks()
        while (time.ticks() - times) < (timeout*1000):  # wait up to timeout
            stat = self.status
            if stat == WL_AP_LISTENING:
                return stat
            utime.sleep_ms(50)
        if stat == WL_AP_FAILED:
            raise RuntimeError("Failed to create AP", ssid)
        raise RuntimeError("Unknown error 0x%02x" % stat)

    def pretty_ip(self, ip):
        """Converts a bytearray IP address to a dotted-quad string for printing"""
        return "%d.%d.%d.%d" % (ip[0], ip[1], ip[2], ip[3])

    def unpretty_ip(self, ip):
        """Converts a dotted-quad string to a bytearray IP address"""
        octets = [int(x) for x in ip.split('.')]
        return bytes(octets)

    def get_host_by_name(self, hostname):
        """Convert a hostname to a packed 4-byte IP address. Returns
        a 4 bytearray"""
        if self._debug:
            print("*** Get host by name")
        if isinstance(hostname, str):
            hostname = bytes(hostname, 'utf-8')
        resp = self._send_command_get_response(_REQ_HOST_BY_NAME_CMD, (hostname,))
        if resp[0][0] != 1:
            raise RuntimeError("Failed to request hostname")
        resp = self._send_command_get_response(_GET_HOST_BY_NAME_CMD)
        return resp[0]

    def ping(self, dest, ttl=250):
        """Ping a destination IP address or hostname, with a max time-to-live
        (ttl). Returns a millisecond timing value"""
        if isinstance(dest, str):
            dest = self.get_host_by_name(dest)
        ttl = max(0, min(ttl, 255))
        resp = self._send_command_get_response(_PING_CMD, (dest, (ttl,)))
        return struct.unpack('<H', resp[0])[0]

    def get_socket(self):
        """Request a socket from the ESP32, will allocate and return a number that
        can then be passed to the other socket commands"""
        if self._debug:
            print("*** Get socket")
        resp = self._send_command_get_response(_GET_SOCKET_CMD)
        resp = resp[0][0]
        if resp == 255:
            raise RuntimeError("No sockets available")
        if self._debug:
            print("Allocated socket %d" % resp)
        return resp

    def socket_open(self, socket_num, dest, port, conn_mode=TCP_MODE):
        """Open a socket to a destination IP address or hostname
        using the ESP32's internal reference number. By default we use
        'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE
        (dest must be hostname for TLS_MODE!)"""
        self._socknum_ll[0][0] = socket_num
        if self._debug:
            print("*** Open socket")
        port_param = struct.pack('>H', port)
        if isinstance(dest, str):
            dest = bytes(dest, 'utf-8')
            resp = self._send_command_get_response(_START_CLIENT_TCP_CMD,
                                                   (dest, b'\x00\x00\x00\x00',
                                                    port_param,
                                                    self._socknum_ll[0],
                                                    (conn_mode,)))
        else:
            resp = self._send_command_get_response(_START_CLIENT_TCP_CMD,
                                                   (dest, port_param,
                                                    self._socknum_ll[0],
                                                    (conn_mode,)))
        if resp[0][0] != 1:
            raise RuntimeError("Could not connect to remote server")

    def socket_status(self, socket_num):
        """Get the socket connection status, can be SOCKET_CLOSED, SOCKET_LISTEN,
        SOCKET_SYN_SENT, SOCKET_SYN_RCVD, SOCKET_ESTABLISHED, SOCKET_FIN_WAIT_1,
        SOCKET_FIN_WAIT_2, SOCKET_CLOSE_WAIT, SOCKET_CLOSING, SOCKET_LAST_ACK, or
        SOCKET_TIME_WAIT"""
        self._socknum_ll[0][0] = socket_num
        resp = self._send_command_get_response(_GET_CLIENT_STATE_TCP_CMD, self._socknum_ll)
        return resp[0][0]

    def socket_connected(self, socket_num):
        """Test if a socket is connected to the destination, returns boolean true/false"""
        return self.socket_status(socket_num) == SOCKET_ESTABLISHED

    def socket_write(self, socket_num, buffer):
        """Write the bytearray buffer to a socket"""
        if self._debug:
            print("Writing:", buffer)
        self._socknum_ll[0][0] = socket_num
        sent = 0
        for chunk in range((len(buffer) // 64)+1):
            resp = self._send_command_get_response(_SEND_DATA_TCP_CMD,
                                                   (self._socknum_ll[0],
                                                    memoryview(buffer)[(chunk*64):((chunk+1)*64)]),
                                                   sent_param_len_16=True)
            sent += resp[0][0]

        if sent != len(buffer):
            raise RuntimeError("Failed to send %d bytes (sent %d)" % (len(buffer), sent))

        resp = self._send_command_get_response(_DATA_SENT_TCP_CMD, self._socknum_ll)
        if resp[0][0] != 1:
            raise RuntimeError("Failed to verify data sent")

    def socket_available(self, socket_num):
        """Determine how many bytes are waiting to be read on the socket"""
        self._socknum_ll[0][0] = socket_num
        resp = self._send_command_get_response(_AVAIL_DATA_TCP_CMD, self._socknum_ll)
        reply = struct.unpack('<H', resp[0])[0]
        if self._debug:
            print("ESPSocket: %d bytes available" % reply)
        return reply

    def socket_read(self, socket_num, size):
        """Read up to 'size' bytes from the socket number. Returns a bytearray"""
        if self._debug:
            print("Reading %d bytes from ESP socket with status %d" %
                  (size, self.socket_status(socket_num)))
        self._socknum_ll[0][0] = socket_num
        resp = self._send_command_get_response(_GET_DATABUF_TCP_CMD,
                                               (self._socknum_ll[0],
                                                (size & 0xFF, (size >> 8) & 0xFF)),
                                               sent_param_len_16=True,
                                               recv_param_len_16=True)
        return bytes(resp[0])

    def socket_connect(self, socket_num, dest, port, conn_mode=TCP_MODE):
        """Open and verify we connected a socket to a destination IP address or hostname
        using the ESP32's internal reference number. By default we use
        'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE (dest must
        be hostname for TLS_MODE!)"""
        if self._debug:
            print("*** Socket connect mode", conn_mode)
        self.socket_open(socket_num, dest, port, conn_mode=conn_mode)
        times = time.ticks()
        while (time.ticks() - times) < 3000:
            if self.socket_connected(socket_num):
                return True
            time.sleep_ms(10)
        raise RuntimeError("Failed to establish connection")

    def socket_close(self, socket_num):
        """Close a socket using the ESP32's internal reference number"""
        if self._debug:
            print("*** Closing socket %d" % socket_num)
        self._socknum_ll[0][0] = socket_num
        resp = self._send_command_get_response(_STOP_CLIENT_TCP_CMD, self._socknum_ll)
        if resp[0][0] != 1:
            raise RuntimeError("Failed to close socket")

    def start_server(self, port, socket_num, conn_mode=TCP_MODE, ip=None): # pylint: disable=invalid-name
        """Opens a server on the specified port, using the ESP32's internal reference number"""
        if self._debug:
            print("*** starting server")
        self._socknum_ll[0][0] = socket_num
        params = [struct.pack('>H', port), self._socknum_ll[0], (conn_mode,)]
        if ip:
            params.insert(0, ip)
        resp = self._send_command_get_response(_START_SERVER_TCP_CMD, params)

        if resp[0][0] != 1:
            raise RuntimeError("Could not start server")

    def server_state(self, socket_num):
        """Get the state of the ESP32's internal reference server socket number"""
        self._socknum_ll[0][0] = socket_num
        resp = self._send_command_get_response(_GET_STATE_TCP_CMD, self._socknum_ll)
        return resp[0][0]


    def set_esp_debug(self, enabled):
        """Enable/disable debug mode on the ESP32. Debug messages will be
        written to the ESP32's UART."""
        resp = self._send_command_get_response(_SET_DEBUG_CMD, ((bool(enabled),),))
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set debug mode")

    def set_pin_mode(self, pin, mode):
        """
        Set the io mode for a GPIO pin.
        :param int pin: ESP32 GPIO pin to set.
        :param value: direction for pin, digitalio.Direction or integer (0=input, 1=output).
        """
        pin_mode = mode
        resp = self._send_command_get_response(_SET_PIN_MODE_CMD,
                                               ((pin,), (pin_mode,)))
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set pin mode")

    def set_digital_write(self, pin, value):
        """
        Set the digital output value of pin.
        :param int pin: ESP32 GPIO pin to write to.
        :param bool value: Value for the pin.
        """
        resp = self._send_command_get_response(_SET_DIGITAL_WRITE_CMD,
                                               ((pin,), (value,)))
        if resp[0][0] != 1:
            raise RuntimeError("Failed to write to pin")

    def set_analog_write(self, pin, analog_value):
        """
        Set the analog output value of pin, using PWM.
        :param int pin: ESP32 GPIO pin to write to.
        :param float value: 0=off 1.0=full on
        """
        value = int(255 * analog_value)
        resp = self._send_command_get_response(_SET_ANALOG_WRITE_CMD,
                                               ((pin,), (value,)))
        if resp[0][0] != 1:
            raise RuntimeError("Failed to write to pin")

    def set_digital_read(self, pin):
        """
        Get the digital input value of pin. Returns the boolean value of the pin.
        :param int pin: ESP32 GPIO pin to read from.
        """
        # Verify nina-fw => 1.5.0
        fw_semver_maj = bytes(self.firmware_version).decode("utf-8")[2]
        assert int(fw_semver_maj) >= 5, "Please update nina-fw to 1.5.0 or above."

        resp = self._send_command_get_response(_SET_DIGITAL_READ_CMD,
                                               ((pin,),))[0]
        if resp[0] == 0:
            return False
        elif resp[0] == 1:
            return True
        else:
            raise ValueError("_SET_DIGITAL_READ response error: response is not boolean", resp[0])

    def set_analog_read(self, pin, atten=ADC_ATTEN_DB_11):
        """
        Get the analog input value of pin. Returns an int between 0 and 65536.
        :param int pin: ESP32 GPIO pin to read from.
        :param int atten: attenuation constant
        """
        # Verify nina-fw => 1.5.0
        fw_semver_maj = bytes(self.firmware_version).decode("utf-8")[2]
        assert int(fw_semver_maj) >= 5, "Please update nina-fw to 1.5.0 or above."

        resp = self._send_command_get_response(_SET_ANALOG_READ_CMD,
                                           ((pin,), (atten,)))
        resp_analog = struct.unpack('<i', resp[0])
        if resp_analog[0] < 0:
            raise ValueError("_SET_ANALOG_READ parameter error: invalid pin", resp_analog[0])
        if self._debug:
            print(resp, resp_analog, resp_analog[0], 16 * resp_analog[0])
        return 16 * resp_analog[0]

    def get_time(self):
        """The current unix timestamp"""
        if self.status == WL_CONNECTED:
            resp = self._send_command_get_response(_GET_TIME)
            resp_time = struct.unpack('<i', resp[0])
            if resp_time == (0,):
                raise ValueError("_GET_TIME returned 0")
            return resp_time
        if self.status in (WL_AP_LISTENING, WL_AP_CONNECTED):
            raise RuntimeError("Cannot obtain NTP while in AP mode, must be connected to internet")
        raise RuntimeError("Must be connected to WiFi before obtaining NTP.")

    def set_certificate(self, client_certificate):
        """Sets client certificate. Must be called
        BEFORE a network connection is established.
        :param str client_certificate: User-provided .PEM certificate up to 1300 bytes.
        """
        if self._debug:
            print("** Setting client certificate")
        if self.status == WL_CONNECTED:
            raise RuntimeError("set_certificate must be called BEFORE a connection is established.")
        if isinstance(client_certificate, str):
            client_certificate = bytes(client_certificate, 'utf-8')
        if "-----BEGIN CERTIFICATE" not in client_certificate:
            raise TypeError(".PEM must start with -----BEGIN CERTIFICATE")
        assert len(client_certificate) < 1300, ".PEM must be less than 1300 bytes."
        resp = self._send_command_get_response(_SET_CLI_CERT, (client_certificate,))
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set client certificate")
        self.set_crt = True
        return resp[0]

    def set_private_key(self, private_key):
        """Sets private key. Must be called
        BEFORE a network connection is established.
        :param str private_key: User-provided .PEM file up to 1700 bytes.
        """
        if self._debug:
            print("** Setting client's private key.")
        if self.status == WL_CONNECTED:
            raise RuntimeError("set_private_key must be called BEFORE a connection is established.")
        if isinstance(private_key, str):
            private_key = bytes(private_key, 'utf-8')
        if "-----BEGIN RSA" not in private_key:
            raise TypeError(".PEM must start with -----BEGIN RSA")
        assert len(private_key) < 1700, ".PEM must be less than 1700 bytes."
        resp = self._send_command_get_response(_SET_PK, (private_key,))
        if resp[0][0] != 1:
            raise RuntimeError("Failed to set private key.")
        self.set_psk = True
        return resp[0]

Part 2.


ESP32SPI_requests.py

# The MIT License (MIT)
#
# Copyright (c) 2019 ladyada 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
# 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_requests`
================================================================================

A requests-like library for web interfacing

*** Openmv Variant ***


* Author(s): ladyada, Paul Sokolovsky
* Forked by: Nezra

Implementation Notes
--------------------

Adapted from https://github.com/micropython/micropython-lib/tree/master/urequests

micropython-lib consists of multiple modules from different sources and
authors. Each module comes under its own licensing terms. Short name of
a license can be found in a file within a module directory (usually
metadata.txt or setup.py). Complete text of each license used is provided
at https://github.com/micropython/micropython-lib/blob/master/LICENSE

author='Paul Sokolovsky'
license='MIT'

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

"""

import gc

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Requests.git"

_the_interface = None  # pylint: disable=invalid-name
_the_sock = None  # pylint: disable=invalid-name


def set_socket(sock, iface=None):
    """Helper to set the global socket and optionally set the global network interface.
    :param sock: socket object.
    :param iface: internet interface object

    """
    global _the_sock  # pylint: disable=invalid-name, global-statement
    _the_sock = sock
    if iface:
        global _the_interface # pylint: disable=invalid-name, global-statement
        _the_interface = iface
        _the_sock.set_interface(iface)

class Response:
    """The response from a request, contains all the headers/content"""

    encoding = None

    def __init__(self, sock):
        self.socket = sock
        self.encoding = "utf-8"
        self._cached = None
        self.status_code = None
        self.reason = None
        self._read_so_far = 0
        self.headers = {}

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def close(self):
        """Close, delete and collect the response data"""
        if self.socket:
            self.socket.close()
            del self.socket
        del self._cached
        gc.collect()

    @property
    def content(self):
        """The HTTP content direct from the socket, as bytes"""
        # print(self.headers)
        try:
            content_length = int(self.headers["content-length"])
        except KeyError:
            content_length = 0
        # print("Content length:", content_length)
        if self._cached is None:
            try:
                self._cached = self.socket.recv(content_length)
            finally:
                self.socket.close()
                self.socket = None
        # print("Buffer length:", len(self._cached))
        return self._cached

    @property
    def text(self):
        """The HTTP content, encoded into a string according to the HTTP
        header encoding"""
        return str(self.content, self.encoding)

    def json(self):
        """The HTTP content, parsed into a json dictionary"""
        try:
            import json as json_module
        except ImportError:
            import ujson as json_module
        return json_module.loads(self.content)

    def iter_content(self, chunk_size=1, decode_unicode=False):
        """An iterator that will stream data by only reading 'chunk_size'
        bytes and yielding them, when we can't buffer the whole datastream"""
        if decode_unicode:
            raise NotImplementedError("Unicode not supported")

        while True:
            chunk = self.socket.recv(chunk_size)
            if chunk:
                yield chunk
            else:
                return


# pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
def request(method, url, data=None, json=None, headers=None, stream=False, timeout=1):
    """Perform an HTTP request to the given url which we will parse to determine
    whether to use SSL ('https://') or not. We can also send some provided 'data'
    or a json dictionary which we will stringify. 'headers' is optional HTTP headers
    sent along. 'stream' will determine if we buffer everything, or whether to only
    read only when requested
    """
    global _the_interface  # pylint: disable=global-statement, invalid-name
    global _the_sock  # pylint: disable=global-statement, invalid-name

    if not headers:
        headers = {}

    try:
        proto, dummy, host, path = url.split("/", 3)
        # replace spaces in path
        path = path.replace(" ", "%20")
    except ValueError:
        proto, dummy, host = url.split("/", 2)
        path = ""
    if proto == "http:":
        port = 80
    elif proto == "https:":
        port = 443
    else:
        raise ValueError("Unsupported protocol: " + proto)

    if ":" in host:
        host, port = host.split(":", 1)
        port = int(port)

    addr_info = _the_sock.getaddrinfo(host, port, 0, _the_sock.SOCK_STREAM)[0]
    sock = _the_sock.socket(addr_info[0], addr_info[1], addr_info[2])
    resp = Response(sock)  # our response

    sock.settimeout(timeout)  # socket read timeout

    try:
        if proto == "https:":
            conntype = _the_interface.TLS_MODE
            sock.connect(
                (host, port), conntype
            )  # for SSL we need to know the host name
        else:
            conntype = _the_interface.TCP_MODE
            sock.connect(addr_info[-1], conntype)
        sock.send(b"%s /%s HTTP/1.0\r\n" % (bytes(method, "utf-8"), bytes(path, "utf-8")))
        if "Host" not in headers:
            sock.send(b"Host: %s\r\n" % bytes(host, "utf-8"))
        if "User-Agent" not in headers:
            sock.send(b"User-Agent: Adafruit CircuitPython\r\n")
        # Iterate over keys to avoid tuple alloc
        for k in headers:
            sock.send(k.encode())
            sock.send(b": ")
            sock.send(headers[k].encode())
            sock.send(b"\r\n")
        if json is not None:
            assert data is None
            try:
                import json as json_module
            except ImportError:
                import ujson as json_module
            data = json_module.dumps(json)
            sock.send(b"Content-Type: application/json\r\n")
        if data:
            sock.send(b"Content-Length: %d\r\n" % len(data))
        sock.send(b"\r\n")
        if data:
            sock.send(bytes(data, "utf-8"))

        line = sock.readline()
        # print(line)
        line = line.split(None, 2)
        status = int(line[1])
        reason = ""
        if len(line) > 2:
            reason = line[2].rstrip()
        resp.headers = parse_headers(sock)
        if resp.headers.get("transfer-encoding"):
            if "chunked" in resp.headers.get("transfer-encoding"):
                raise ValueError("Unsupported " + resp.headers.get("transfer-encoding"))
        elif resp.headers.get("location") and not 200 <= status <= 299:
            raise NotImplementedError("Redirects not yet supported")

    except:
        sock.close()
        raise

    resp.status_code = status
    resp.reason = reason
    return resp

def parse_headers(sock):
    """
    Parses the header portion of an HTTP request/response from the socket.
    Expects first line of HTTP request/response to have been read already
    return: header dictionary
    rtype: Dict
    """
    headers = {}
    while True:
        line = sock.readline()
        if not line or line == b"\r\n":
            break

        #print("**line: ", line)
        title, content = line.split(b': ', 1)
        if title and content:
            title = str(title.lower(), 'utf-8')
            content = str(content, 'utf-8')
            headers[title] = content
    return headers

def head(url, **kw):
    """Send HTTP HEAD request"""
    return request("HEAD", url, **kw)


def get(url, **kw):
    """Send HTTP GET request"""
    return request("GET", url, **kw)


def post(url, **kw):
    """Send HTTP POST request"""
    return request("POST", url, **kw)


def put(url, **kw):
    """Send HTTP PUT request"""
    return request("PUT", url, **kw)


def patch(url, **kw):
    """Send HTTP PATCH request"""
    return request("PATCH", url, **kw)


def delete(url, **kw):
    """Send HTTP DELETE request"""
    return request("DELETE", url, **kw)

ESP32SPI_socket.py

# The MIT License (MIT)
#
# Copyright (c) 2019 ladyada 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
# 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_esp32spi_socket`
================================================================================

A socket compatible interface thru the ESP SPI command set for the Openmv Cam

* Author(s): ladyada
* Forked By: Nezra
"""


import time
import gc
from micropython import const
import esp32spi

_the_interface = None   # pylint: disable=invalid-name
def set_interface(iface):
    """Helper to set the global internet interface"""
    global _the_interface   # pylint: disable=global-statement, invalid-name
    _the_interface = iface

SOCK_STREAM = const(1)
AF_INET = const(2)
NO_SOCKET_AVAIL = const(255)

MAX_PACKET = const(4000)

# pylint: disable=too-many-arguments, unused-argument
def getaddrinfo(host, port, family=0, socktype=0, proto=0, flags=0):
    """Given a hostname and a port name, return a 'socket.getaddrinfo'
    compatible list of tuples. Honestly, we ignore anything but host & port"""
    if not isinstance(port, int):
        raise RuntimeError("Port must be an integer")
    ipaddr = _the_interface.get_host_by_name(host)
    return [(AF_INET, socktype, proto, '', (ipaddr, port))]
# pylint: enable=too-many-arguments, unused-argument

# pylint: disable=unused-argument, redefined-builtin, invalid-name
class socket:
    """A simplified implementation of the Python 'socket' class, for connecting
    through an interface to a remote device"""
    def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None, socknum=None):
        if family != AF_INET:
            raise RuntimeError("Only AF_INET family supported")
        if type != SOCK_STREAM:
            raise RuntimeError("Only SOCK_STREAM type supported")
        self._buffer = b''
        self._socknum = socknum if socknum else _the_interface.get_socket()
        self.settimeout(1)

    def connect(self, address, conntype=None):
        """Connect the socket to the 'address' (which can be 32bit packed IP or
        a hostname string). 'conntype' is an extra that may indicate SSL or not,
        depending on the underlying interface"""
        host, port = address
        if conntype is None:
            conntype = _the_interface.TCP_MODE
        if not _the_interface.socket_connect(self._socknum, host, port, conn_mode=conntype):
            raise RuntimeError("Failed to connect to host", host)
        self._buffer = b''

    def send(self, data):         # pylint: disable=no-self-use
        """Send some data to the socket"""
        _the_interface.socket_write(self._socknum, data)
        gc.collect()

    def write(self, data):         # pylint: disable=no-self-use
        """Sends data to the socket.
        NOTE: This method is deprecated and will be removed.
        """
        self.send(data)

    def readline(self):
        """Attempt to return as many bytes as we can up to but not including '\r\n'"""
        #print("Socket readline")
        stamp = time.ticks()
        while b'\r\n' not in self._buffer:
            # there's no line already in there, read some more
            avail = self.available()
            if avail:
                self._buffer += _the_interface.socket_read(self._socknum, avail)

            elif self._timeout > 0 and time.ticks() - stamp > self._timeout:
                print("time elapsed: ",(time.ticks()-stamp),self._timeout)
                self.close()  # Make sure to close socket so that we don't exhaust sockets.
                #gc.collect()
                #return "failed to get full response"
                raise RuntimeError("Didn't receive full response, failing out")
        firstline, self._buffer = self._buffer.split(b'\r\n', 1)
        gc.collect()
        return firstline

    def recv(self, bufsize=0):
        """Read up to 'size' bytes from the socket, this may be buffered internally!
        If 'size' isnt specified, return everything in the buffer."""
        #print("Socket read", size)
        if bufsize == 0:   # read as much as we can at the moment
            while True:
                avail = self.available()
                if avail:
                    self._buffer += _the_interface.socket_read(self._socknum, avail)
                else:
                    break
            gc.collect()
            ret = self._buffer
            self._buffer = b''
            gc.collect()
            return ret
        stamp = time.ticks()

        to_read = bufsize - len(self._buffer)
        received = []
        while to_read > 0:
            #print("Bytes to read:", to_read)
            avail = self.available()
            if avail:
                stamp = time.ticks()
                recv = _the_interface.socket_read(self._socknum, min(to_read, avail))
                received.append(recv)
                to_read -= len(recv)
                gc.collect()
            if self._timeout > 0 and time.ticks() - stamp > self._timeout:
                break
        #print(received)
        self._buffer += b''.join(received)

        ret = None
        if len(self._buffer) == bufsize:
            ret = self._buffer
            self._buffer = b''
        else:
            ret = self._buffer[:bufsize]
            self._buffer = self._buffer[bufsize:]
        gc.collect()
        return ret

    def read(self, size=0):
        """Read up to 'size' bytes from the socket, this may be buffered internally!
        If 'size' isnt specified, return everything in the buffer.
        NOTE: This method is deprecated and will be removed.
        """
        return self.recv(size)

    def settimeout(self, value):
        """Set the read timeout for sockets, if value is 0 it will block"""
        self._timeout = value*1000

    def available(self):
        """Returns how many bytes of data are available to be read (up to the MAX_PACKET length)"""
        if self.socknum != NO_SOCKET_AVAIL:
            return min(_the_interface.socket_available(self._socknum), MAX_PACKET)
        return 0

    def connected(self):
        """Whether or not we are connected to the socket"""
        if self.socknum == NO_SOCKET_AVAIL:
            return False
        elif self.available():
            return True
        else:
            status = _the_interface.socket_status(self.socknum)
            result = status not in (esp32spi.SOCKET_LISTEN,
                                    esp32spi.SOCKET_CLOSED,
                                    esp32spi.SOCKET_FIN_WAIT_1,
                                    esp32spi.SOCKET_FIN_WAIT_2,
                                    esp32spi.SOCKET_TIME_WAIT,
                                    esp32spi.SOCKET_SYN_SENT,
                                    esp32spi.SOCKET_SYN_RCVD,
                                    esp32spi.SOCKET_CLOSE_WAIT)
            if not result:
                self.close()
                self._socknum = NO_SOCKET_AVAIL
            return result

    @property
    def socknum(self):
        """The socket number"""
        return self._socknum

    def close(self):
        """Close the socket, after reading whatever remains"""
        _the_interface.socket_close(self._socknum)
# pylint: enable=unused-argument, redefined-builtin, invalid-name

Part 3.

ESP32SPI_wsgiserver.py

# The MIT License (MIT)
#
# Copyright (c) 2019 Matt Costi 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
# 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_esp32spi_wsgiserver`
================================================================================

A simple WSGI (Web Server Gateway Interface) server that interfaces with the ESP32 over SPI.
Opens a specified port on the ESP32 to listen for incoming HTTP Requests and
Accepts an Application object that must be callable, which gets called
whenever a new HTTP Request has been received.

The Application MUST accept 2 ordered parameters:
    1. environ object (incoming request data)
    2. start_response function. Must be called before the Application
        callable returns, in order to set the response status and headers.

The Application MUST return a single string in a list,
which is the response data

Requires update_poll being called in the applications main event loop.

For more details about Python WSGI see:
https://www.python.org/dev/peps/pep-0333/

For the OpenMV Cam

* Author(s): Matt Costi
* Forked by: Nezra
"""
# pylint: disable=no-name-in-module

import io
import gc
from micropython import const
import esp32spi_socket as socket
from esp32spi_requests import parse_headers

_the_interface = None   # pylint: disable=invalid-name
def set_interface(iface):
    """Helper to set the global internet interface"""
    global _the_interface   # pylint: disable=global-statement, invalid-name
    _the_interface = iface
    socket.set_interface(iface)

NO_SOCK_AVAIL = const(255)

# pylint: disable=invalid-name
class WSGIServer:
    """
    A simple server that implements the WSGI interface
    """

    def __init__(self, port=80, debug=False, application=None):
        self.application = application
        self.port = port
        self._server_sock = socket.socket(socknum=NO_SOCK_AVAIL)
        self._client_sock = socket.socket(socknum=NO_SOCK_AVAIL)
        self._debug = debug

        self._response_status = None
        self._response_headers = []

    def start(self):
        """
        starts the server and begins listening for incoming connections.
        Call update_poll in the main loop for the application callable to be
        invoked on receiving an incoming request.
        """
        self._server_sock = socket.socket()
        _the_interface.start_server(self.port, self._server_sock.socknum)
        if self._debug:
            ip = _the_interface.pretty_ip(_the_interface.ip_address)
            print("Server available at {0}:{1}".format(ip, self.port))
            print("Sever status: ", _the_interface.get_server_state(self._server_sock.socknum))

    def update_poll(self):
        """
        Call this method inside your main event loop to get the server
        check for new incoming client requests. When a request comes in,
        the application callable will be invoked.
        """
        self.client_available()
        if (self._client_sock and self._client_sock.available()):
            environ = self._get_environ(self._client_sock)
            result = self.application(environ, self._start_response)
            self.finish_response(result)

    def finish_response(self, result):
        """
        Called after the application callbile returns result data to respond with.
        Creates the HTTP Response payload from the response_headers and results data,
        and sends it back to client.

        :param string result: the data string to send back in the response to the client.
        """
        try:
            response = "HTTP/1.1 {0}\r\n".format(self._response_status)
            for header in self._response_headers:
                response += "{0}: {1}\r\n".format(*header)
            response += "\r\n"
            self._client_sock.send(response.encode("utf-8"))
            for data in result:
                if isinstance(data, bytes):
                    self._client_sock.send(data)
                if isinstance(data, str):    # new
                    self._client_sock.send(data.encode("utf-8")) #new
                else:
                    self._client_sock.send(data) # moved .encode above
            gc.collect()
        finally:
            print("closing")
            self._client_sock.close()

    def client_available(self):
        """
        returns a client socket connection if available.
        Otherwise, returns None
        :return: the client
        :rtype: Socket
        """
        sock = None
        if self._server_sock.socknum != NO_SOCK_AVAIL:
            if self._client_sock.socknum != NO_SOCK_AVAIL:
                # check previous received client socket
                if self._debug > 2:
                    print("checking if last client sock still valid")
                if self._client_sock.connected() and self._client_sock.available():
                    sock = self._client_sock
            if not sock:
                # check for new client sock
                if self._debug > 2:
                    print("checking for new client sock")
                client_sock_num = _the_interface.socket_available(self._server_sock.socknum)
                sock = socket.socket(socknum=client_sock_num)
        else:
            print("Server has not been started, cannot check for clients!")

        if sock and sock.socknum != NO_SOCK_AVAIL:
            if self._debug > 2:
                print("client sock num is: ", sock.socknum)
            self._client_sock = sock
            return self._client_sock

        return None

    def _start_response(self, status, response_headers):
        """
        The application callable will be given this method as the second param
        This is to be called before the application callable returns, to signify
        the response can be started with the given status and headers.

        :param string status: a status string including the code and reason. ex: "200 OK"
        :param list response_headers: a list of tuples to represent the headers.
            ex ("header-name", "header value")
        """
        self._response_status = status
        self._response_headers = [("Server", "esp32WSGIServer")] + response_headers

    def _get_environ(self, client):
        """
        The application callable will be given the resulting environ dictionary.
        It contains metadata about the incoming request and the request body ("wsgi.input")

        :param Socket client: socket to read the request from
        """
        env = {}
        line = str(client.readline(), "utf-8")
        (method, path, ver) = line.rstrip("\r\n").split(None, 2)

        env["wsgi.version"] = (1, 0)
        env["wsgi.url_scheme"] = "http"
        env["wsgi.multithread"] = False
        env["wsgi.multiprocess"] = False
        env["wsgi.run_once"] = False

        env["REQUEST_METHOD"] = method
        env["SCRIPT_NAME"] = ""
        env["SERVER_NAME"] = _the_interface.pretty_ip(_the_interface.ip_address)
        env["SERVER_PROTOCOL"] = ver
        env["SERVER_PORT"] = self.port
        if path.find("?") >= 0:
            env["PATH_INFO"] = path.split("?")[0]
            env["QUERY_STRING"] = path.split("?")[1]
        else:
            env["PATH_INFO"] = path

        headers = parse_headers(client)
        if "content-type" in headers:
            env["CONTENT_TYPE"] = headers.get("content-type")
        if "content-length" in headers:
            env["CONTENT_LENGTH"] = headers.get("content-length")
            body = client.read(int(env["CONTENT_LENGTH"]))
            env["wsgi.input"] = io.StringIO(body)
        else:
            body = client.read()
            env["wsgi.input"] = io.StringIO(body)
        for name, value in headers.items():
            key = "HTTP_" + name.replace('-', '_').upper()
            if key in env:
                value = "{0},{1}".format(env[key], value)
            env[key] = value

        return env

miniMQTT.py

# The MIT License (MIT)
#
# Copyright (c) 2019 Brent Rubell for Adafruit Industries
#
# Original Work Copyright (c) 2016 Paul Sokolovsky, uMQTT
# Modified Work Copyright (c) 2019 Bradley Beach, esp32spi_mqtt
#
# 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_minimqtt`
================================================================================

MQTT Library for OpenMV.

* Author(s): Brent Rubell
* Forked by : Nezra

Implementation Notes
--------------------

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

"""
import struct
import time, utime
from random import randint
import machine
from micropython import const
#import adafruit_logging as logging

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT.git"

# Client-specific variables
MQTT_MSG_MAX_SZ = const(268435455)
MQTT_MSG_SZ_LIM = const(10000000)
MQTT_TOPIC_LENGTH_LIMIT = const(65535)
MQTT_TCP_PORT = const(1883)
MQTT_TLS_PORT = const(8883)
TCP_MODE = const(0)
TLS_MODE = const(2)

# MQTT Commands
MQTT_PINGREQ = b'\xc0\0'
MQTT_PINGRESP = const(0xd0)
MQTT_SUB = b'\x82'
MQTT_UNSUB = b'\xA2'
MQTT_PUB = bytearray(b'\x30\0')
# Variable CONNECT header [MQTT 3.1.2]
MQTT_VAR_HEADER = bytearray(b"\x04MQTT\x04\x02\0\0")
MQTT_DISCONNECT = b'\xe0\0'

CONNACK_ERRORS = {const(0x01) : 'Connection Refused - Incorrect Protocol Version',
                  const(0x02) : 'Connection Refused - ID Rejected',
                  const(0x03) : 'Connection Refused - Server unavailable',
                  const(0x04) : 'Connection Refused - Incorrect username/password',
                  const(0x05) : 'Connection Refused - Unauthorized'}

class MMQTTException(Exception):
    """MiniMQTT Exception class."""
    # pylint: disable=unnecessary-pass
    #pass

class MQTT:
    """MQTT Client for CircuitPython
    :param socket: Socket object for provided network interface
    :param str broker: MQTT Broker URL or IP Address.
    :param int port: Optional port definition, defaults to 8883.
    :param str username: Username for broker authentication.
    :param str password: Password for broker authentication.
    :param network_manager: NetworkManager object, such as WiFiManager from ESPSPI_WiFiManager.
    :param str client_id: Optional client identifier, defaults to a unique, generated string.
    :param bool is_ssl: Sets a secure or insecure connection with the broker.
    :param bool log: Attaches a logger to the MQTT client, defaults to logging level INFO.
    :param int keep_alive: KeepAlive interval between the broker and the MiniMQTT client.
    """
    # pylint: disable=too-many-arguments,too-many-instance-attributes, not-callable, invalid-name, no-member
    def __init__(self, socket, broker, port=None, username=None,
                 password=None, network_manager=None, client_id=None,
                 is_ssl=True, log=False, keep_alive=60):
        # network management
        self._socket = socket
        network_manager_type = str(type(network_manager))
        if 'ESPSPI_WiFiManager' in network_manager_type:
            self._wifi = network_manager
        else:
            raise TypeError("This library requires a NetworkManager object.")
        # broker
        try: # set broker IP
            self.broker = self._wifi.esp.unpretty_ip(broker)
        except ValueError: # set broker URL
            self.broker = broker
        # port/ssl
        self.port = MQTT_TCP_PORT
        if is_ssl:
            self.port = MQTT_TLS_PORT
        if port is not None:
            self.port = port
        # session identifiers
        self.user = username
        # [MQTT-3.1.3.5]
        self.password = password
        if self.password is not None and len(password.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT:
            raise MMQTTException('Password length is too large.')
        if client_id is not None:
            # user-defined client_id MAY allow client_id's > 23 bytes or
            # non-alpha-numeric characters
            self.client_id = client_id
        else:
            # assign a unique client_id
            self.client_id = 'esp32-{0}'.format(randint(10000, 99999))
            # generated client_id's enforce spec.'s length rules
            if len(self.client_id) > 23 or not self.client_id:
                raise ValueError('MQTT Client ID must be between 1 and 23 bytes')
        self.keep_alive = keep_alive
        self.user_data = None
        self.logger = None
#        if log is True:
#            self.logger = logging.getLogger('log')
#            self.logger.setLevel(logging.INFO)
        self._sock = None
        self._is_connected = False
        self._msg_size_lim = MQTT_MSG_SZ_LIM
        self._pid = 0
        self._timestamp = 0
        # List of subscribed topics, used for tracking
        self._subscribed_topics = []
        # Server callbacks
        self.on_message = None
        self.on_connect = None
        self.on_disconnect = None
        self.on_publish = None
        self.on_subscribe = None
        self.on_unsubscribe = None
        self.last_will()

    def __enter__(self):
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        self.deinit()

    def deinit(self):
        """De-initializes the MQTT client and disconnects from
        the mqtt broker.
        """
        self.disconnect()

    def last_will(self, topic=None, message=None, qos=0, retain=False):
        """Sets the last will and testament properties. MUST be called before connect().
        :param str topic: MQTT Broker topic.
        :param str message: Last will disconnection message.
        :param int qos: Quality of Service level.
        :param bool retain: Specifies if the message is to be retained when it is published.
        """
        if self._is_connected:
            raise MMQTTException('Last Will should be defined before connect() is called.')
        if qos < 0 or qos > 2:
            raise MMQTTException("Invalid QoS level,  must be between 0 and 2.")
        if self.logger is not None:
            self.logger.debug('Setting last will properties')
        self._lw_qos = qos
        self._lw_topic = topic
        self._lw_msg = message
        self._lw_retain = retain

    # pylint: disable=too-many-branches, too-many-statements
    def connect(self, clean_session=True):
        """Initiates connection with the MQTT Broker.
        :param bool clean_session: Establishes a persistent session.
        """
        self._set_interface()
        if self.logger is not None:
            self.logger.debug('Creating new socket')
        self._sock = self._socket.socket()
        self._sock.settimeout(10)
        if self.port == 8883:
            try:
                if self.logger is not None:
                    self.logger.debug('Attempting to establish secure MQTT connection...')
                self._sock.connect((self.broker, self.port), TLS_MODE)
            except RuntimeError:
                raise MMQTTException("Invalid broker address defined.")
        else:
            if isinstance(self.broker, str):
                addr = self._socket.getaddrinfo(self.broker, self.port)[0][-1]
            else:
                addr = (self.broker, self.port)
            try:
                if self.logger is not None:
                    self.logger.debug('Attempting to establish insecure MQTT connection...')
                #self._sock.connect((self.broker, self.port), TCP_MODE)
                self._sock.connect(addr, TCP_MODE)
            except RuntimeError as e:
                raise MMQTTException("Invalid broker address defined.", e)

        # Fixed Header
        fixed_header = bytearray()
        fixed_header.append(0x10)

        # Variable Header
        var_header = MQTT_VAR_HEADER
        var_header[6] = clean_session << 1

        # Set up variable header and remaining_length
        remaining_length = 12 + len(self.client_id)
        if self.user is not None:
            remaining_length += 2 + len(self.user) + 2 + len(self.password)
            var_header[6] |= 0xC0
        if self.keep_alive:
            assert self.keep_alive < MQTT_TOPIC_LENGTH_LIMIT
            var_header[7] |= self.keep_alive >> 8
            var_header[8] |= self.keep_alive & 0x00FF
        if self._lw_topic:
            remaining_length += 2 + len(self._lw_topic) + 2 + len(self._lw_msg)
            var_header[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3
            var_header[6] |= self._lw_retain << 5

        # Remaining length calculation
        large_rel_length = False
        if remaining_length > 0x7f:
            large_rel_length = True
            # Calculate Remaining Length [2.2.3]
            while remaining_length > 0:
                encoded_byte = remaining_length % 0x80
                remaining_length = remaining_length // 0x80
                # if there is more data to encode, set the top bit of the byte
                if remaining_length > 0:
                    encoded_byte |= 0x80
                fixed_header.append(encoded_byte)
        if large_rel_length:
            fixed_header.append(0x00)
        else:
            fixed_header.append(remaining_length)
            fixed_header.append(0x00)

        if self.logger is not None:
            self.logger.debug('Sending CONNECT to broker')
            self.logger.debug('Fixed Header: {}\nVariable Header: {}'.format(fixed_header,
                                                                             var_header))
        self._sock.send(fixed_header)
        self._sock.send(var_header)
        # [MQTT-3.1.3-4]
        self._send_str(self.client_id)
        if self._lw_topic:
            # [MQTT-3.1.3-11]
            self._send_str(self._lw_topic)
            self._send_str(self._lw_msg)
        if self.user is None:
            self.user = None
        else:
            self._send_str(self.user)
            self._send_str(self.password)
        if self.logger is not None:
            self.logger.debug('Receiving CONNACK packet from broker')
        while True:
            op = self._wait_for_msg()
            if op == 32:
                rc = self._sock.recv(3)
                assert rc[0] == 0x02
                if rc[2] != 0x00:
                    raise MMQTTException(CONNACK_ERRORS[rc[2]])
                self._is_connected = True
                result = rc[0] & 1
                if self.on_connect is not None:
                    self.on_connect(self, self.user_data, result, rc[2])
                return result

    def disconnect(self):
        """Disconnects the MiniMQTT client from the MQTT broker.
        """
        self.is_connected()
        if self.logger is not None:
            self.logger.debug('Sending DISCONNECT packet to broker')
        self._sock.send(MQTT_DISCONNECT)
        if self.logger is not None:
            self.logger.debug('Closing socket')
        self._sock.close()
        self._is_connected = False
        self._subscribed_topics = None
        if self.on_disconnect is not None:
            self.on_disconnect(self, self.user_data, 0)

    def ping(self):
        """Pings the MQTT Broker to confirm if the broker is alive or if
        there is an active network connection.
        """
        self.is_connected()
        if self.logger is not None:
            self.logger.debug('Sending PINGREQ')
        self._sock.send(MQTT_PINGREQ)
        if self.logger is not None:
            self.logger.debug('Checking PINGRESP')
        while True:
            op = self._wait_for_msg(0.5)
            if op == 208:
                ping_resp = self._sock.recv(2)
                if ping_resp[0] != 0x00:
                    raise MMQTTException('PINGRESP not returned from broker.')
            return

    # pylint: disable=too-many-branches, too-many-statements
    def publish(self, topic, msg, retain=False, qos=0):
        """Publishes a message to a topic provided.
        :param str topic: Unique topic identifier.
        :param str msg: Data to send to the broker.
        :param int msg: Data to send to the broker.
        :param float msg: Data to send to the broker.
        :param bool retain: Whether the message is saved by the broker.
        :param int qos: Quality of Service level for the message.

        Example of sending an integer, 3, to the broker on topic 'piVal'.
        .. code-block:: python

            mqtt_client.publish('topics/piVal', 3)

        Example of sending a float, 3.14, to the broker on topic 'piVal'.
        .. code-block:: python

            mqtt_client.publish('topics/piVal', 3.14)

        Example of sending a string, 'threepointonefour', to the broker on topic piVal.
        .. code-block:: python

            mqtt_client.publish('topics/piVal', 'threepointonefour')

        """
        self.is_connected()
        self._check_topic(topic)
        if '+' in topic or '#' in topic:
            raise MMQTTException('Publish topic can not contain wildcards.')
        # check msg/qos kwargs
        if msg is None:
            raise MMQTTException('Message can not be None.')
        elif isinstance(msg, (int, float)):
            msg = str(msg).encode('ascii')
        elif isinstance(msg, str):
            msg = str(msg).encode('utf-8')
        else:
            raise MMQTTException('Invalid message data type.')
        if len(msg) > MQTT_MSG_MAX_SZ:
            raise MMQTTException('Message size larger than %db.'%MQTT_MSG_MAX_SZ)
        self._check_qos(qos)
        pkt = MQTT_PUB
        pkt[0] |= qos << 1 | retain
        sz = 2 + len(topic) + len(msg)
        if qos > 0:
            sz += 2
        assert sz < 2097152
        i = 1
        while sz > 0x7f:
            pkt[i] = (sz & 0x7f) | 0x80
            sz >>= 7
            i += 1
        pkt[i] = sz
        if self.logger is not None:
            self.logger.debug('Sending PUBLISH\nTopic: {0}\nMsg: {1}\
                                \nQoS: {2}\nRetain? {3}'.format(topic, msg, qos, retain))
        self._sock.send(pkt)
        self._send_str(topic)
        if qos == 0:
            if self.on_publish is not None:
                self.on_publish(self, self.user_data, topic, self._pid)
        if qos > 0:
            self._pid += 1
            pid = self._pid
            struct.pack_into("!H", pkt, 0, pid)
            self._sock.send(pkt)
            if self.on_publish is not None:
                self.on_publish(self, self.user_data, topic, pid)
        if self.logger is not None:
            self.logger.debug('Sending PUBACK')
        self._sock.send(msg)
        if qos == 1:
            while True:
                op = self._wait_for_msg()
                if op == 0x40:
                    sz = self._sock.recv(1)
                    assert sz == b"\x02"
                    rcv_pid = self._sock.recv(2)
                    rcv_pid = rcv_pid[0] << 0x08 | rcv_pid[1]
                    if pid == rcv_pid:
                        if self.on_publish is not None:
                            self.on_publish(self, self.user_data, topic, rcv_pid)
                        return
        elif qos == 2:
            assert 0
            if self.on_publish is not None:
                self.on_publish(self, self.user_data, topic, rcv_pid)

    def subscribe(self, topic, qos=0):
        """Subscribes to a topic on the MQTT Broker.
        This method can subscribe to one topics or multiple topics.
        :param str topic: Unique MQTT topic identifier.
        :param int qos: Quality of Service level for the topic, defaults to zero.
        :param tuple topic: Tuple containing topic identifier strings and qos level integers.
        :param list topic: List of tuples containing topic identifier strings and qos.

        Example of subscribing a topic string.
        .. code-block:: python

            mqtt_client.subscribe('topics/ledState')

        Example of subscribing to a topic and setting the qos level to 1.
        .. code-block:: python

            mqtt_client.subscribe('topics/ledState', 1)

        Example of subscribing to topic string and setting qos level to 1, as a tuple.
        .. code-block:: python

            mqtt_client.subscribe(('topics/ledState', 1))

        Example of subscribing to multiple topics with different qos levels.
        .. code-block:: python

            mqtt_client.subscribe([('topics/ledState', 1), ('topics/servoAngle', 0)])

        """
        self.is_connected()
        topics = None
        if isinstance(topic, tuple):
            topic, qos = topic
            self._check_topic(topic)
            self._check_qos(qos)
        if isinstance(topic, str):
            self._check_topic(topic)
            self._check_qos(qos)
            topics = [(topic, qos)]
        if isinstance(topic, list):
            topics = []
            for t, q in topic:
                self._check_qos(q)
                self._check_topic(t)
                topics.append((t, q))
        # Assemble packet
        packet_length = 2 + (2 * len(topics)) + (1 * len(topics))
        packet_length += sum(len(topic) for topic, qos in topics)
        packet_length_byte = packet_length.to_bytes(1, 'big')
        self._pid += 1
        packet_id_bytes = self._pid.to_bytes(2, 'big')
        # Packet with variable and fixed headers
        packet = MQTT_SUB + packet_length_byte + packet_id_bytes
        # attaching topic and QOS level to the packet
        for t, q in topics:
            topic_size = len(t).to_bytes(2, 'big')
            qos_byte = q.to_bytes(1, 'big')
            packet += topic_size + t + qos_byte
        if self.logger is not None:
            for t, q in topics:
                self.logger.debug('SUBSCRIBING to topic {0} with QoS {1}'.format(t, q))
#        print('SUBSCRIBING to topic {0} with QoS {1}'.format(t, q))
        self._sock.send(packet)
        while True:
            op = self._wait_for_msg()
            if op == 0x90:
                rc = self._sock.recv(4)
                assert rc[1] == packet[2] and rc[2] == packet[3]
                if rc[3] == 0x80:
                    raise MMQTTException('SUBACK Failure!')
                for t, q in topics:
                    if self.on_subscribe is not None:
                        self.on_subscribe(self, self.user_data, t, q)
                    self._subscribed_topics.append(t)
                return

    def unsubscribe(self, topic):
        """Unsubscribes from a MQTT topic.
        :param str topic: Unique MQTT topic identifier.
        :param list topic: List of tuples containing topic identifier strings.

        Example of unsubscribing from a topic string.
        .. code-block:: python

            mqtt_client.unsubscribe('topics/ledState')

        Example of unsubscribing from multiple topics.
        .. code-block:: python

            mqtt_client.unsubscribe([('topics/ledState'), ('topics/servoAngle')])

        """
        topics = None
        if isinstance(topic, str):
            self._check_topic(topic)
            topics = [(topic)]
        if isinstance(topic, list):
            topics = []
            for t in topic:
                self._check_topic(t)
                topics.append((t))
        for t in topics:
            if t not in self._subscribed_topics:
                raise MMQTTException('Topic must be subscribed to before attempting unsubscribe.')
        # Assemble packet
        packet_length = 2 + (2 * len(topics))
        packet_length += sum(len(topic) for topic in topics)
        packet_length_byte = packet_length.to_bytes(1, 'big')
        self._pid += 1
        packet_id_bytes = self._pid.to_bytes(2, 'big')
        packet = MQTT_UNSUB + packet_length_byte + packet_id_bytes
        for t in topics:
            topic_size = len(t).to_bytes(2, 'big')
            packet += topic_size + t
        if self.logger is not None:
            for t in topics:
                self.logger.debug('UNSUBSCRIBING from topic {0}.'.format(t))
        self._sock.send(packet)
        if self.logger is not None:
            self.logger.debug('Waiting for UNSUBACK...')
        while True:
            op = self._wait_for_msg()
            if op == 176:
                return_code = self._sock.recv(3)
                assert return_code[0] == 0x02
                # [MQTT-3.32]
                assert return_code[1] == packet_id_bytes[0] and return_code[2] == packet_id_bytes[1]
                for t in topics:
                    if self.on_unsubscribe is not None:
                        self.on_unsubscribe(self, self.user_data, t, self._pid)
                    self._subscribed_topics.remove(t)
                return

    @property
    def is_wifi_connected(self):
        """Returns if the ESP module is connected to
        an access point, resets module if False"""
        if self._wifi:
            return self._wifi.esp.is_connected
        raise MMQTTException("MiniMQTT Client does not use a WiFi NetworkManager.")

    # pylint: disable=line-too-long, protected-access
    @property
    def is_sock_connected(self):
        """Returns if the socket is connected."""
        return self.is_wifi_connected and self._sock and self._wifi.esp.socket_connected(self._sock._socknum)

    def reconnect_socket(self):
        """Re-establishes the socket's connection with the MQTT broker.
        """
        try:
            if self.logger is not None:
                self.logger.debug("Attempting to reconnect with MQTT Broker...")
            self.reconnect()
        except RuntimeError as err:
            if self.logger is not None:
                self.logger.debug('Failed to reconnect with MQTT Broker, retrying...', err)
            utime.sleep(1)
            self.reconnect_socket()

    def reconnect_wifi(self):
        """Reconnects to WiFi Access Point and socket, if disconnected.
        """
        while not self.is_wifi_connected:
            try:
                if self.logger is not None:
                    self.logger.debug('Connecting to WiFi AP...')
                self._wifi.connect()
            except (RuntimeError, ValueError):
                if self.logger is not None:
                    self.logger.debug('Failed to reset WiFi module, retrying...')
                time.sleep(1)
        # we just reconnected, is the socket still connected?
        if not self.is_sock_connected:
            self.reconnect_socket()

    def reconnect(self, resub_topics=True):
        """Attempts to reconnect to the MQTT broker.
        :param bool resub_topics: Resubscribe to previously subscribed topics.
        """
        if self.logger is not None:
            self.logger.debug('Attempting to reconnect with MQTT broker')
        self.connect()
        if self.logger is not None:
            self.logger.debug('Reconnected with broker')
        if resub_topics:
            if self.logger is not None:
                self.logger.debug('Attempting to resubscribe to previously subscribed topics.')
            while self._subscribed_topics:
                feed = self._subscribed_topics.pop()
                self.subscribe(feed)

    def loop_forever(self):
        """Starts a blocking message loop. Use this
        method if you want to run a program forever.
        Code below a call to this method will NOT execute.
        Network reconnection is handled within this call.

        """
        while True:
            # Check WiFi and socket status
            if self.is_sock_connected:
                try:
                    self.loop()
                except (RuntimeError, ValueError):
                    if self._wifi:
                        # Reconnect the WiFi module and the socket
                        self.reconnect_wifi()
                    continue

    def loop(self):
        """Non-blocking message loop. Use this method to
        check incoming subscription messages.

        This method does NOT handle networking or
        network hardware management, use loop_forever
        or handle in code instead.
        """
        if self._timestamp == 0:
            self._timestamp = time.ticks()
        current_time = time.ticks()
        if current_time - self._timestamp >= (self.keep_alive*1000):
            # Handle KeepAlive by expecting a PINGREQ/PINGRESP from the server
            if self.logger is not None:
                self.logger.debug('KeepAlive period elapsed - requesting a PINGRESP from the server...')
            self.ping()
            self._timestamp = 0
        self._sock.settimeout(0.1)
        return self._wait_for_msg()

    def _wait_for_msg(self, timeout=30):
        """Reads and processes network events.
        Returns response code if successful.
        """
        res = self._sock.recv(1)
        self._sock.settimeout(timeout)
        if res in [None, b""]:
            return None
        if res == MQTT_PINGRESP:
            sz = self._sock.recv(1)[0]
            assert sz == 0
            return None
        if res[0] & 0xf0 != 0x30:
            return res[0]
        sz = self._recv_len()
        topic_len = self._sock.recv(2)
        topic_len = (topic_len[0] << 8) | topic_len[1]
        topic = self._sock.recv(topic_len)
        topic = str(topic, 'utf-8')
        sz -= topic_len + 2
        if res[0] & 0x06:
            pid = self._sock.recv(2)
            pid = pid[0] << 0x08 | pid[1]
            sz -= 0x02
        msg = self._sock.recv(sz)
        if self.on_message is not None:
            self.on_message(self, topic, str(msg, 'utf-8'))
        if res[0] & 0x06 == 0x02:
            pkt = bytearray(b"\x40\x02\0\0")
            struct.pack_into("!H", pkt, 2, pid)
            self._sock.send(pkt)
        elif res[0] & 6 == 4:
            assert 0
        return res[0]

    def _recv_len(self):
        n = 0
        sh = 0
        while True:
            b = self._sock.recv(1)[0]
            n |= (b & 0x7f) << sh
            if not b & 0x80:
                return n
            sh += 7

    def _send_str(self, string):
        """Packs and encodes a string to a socket.
        :param str string: String to write to the socket.
        """
        self._sock.send(struct.pack("!H", len(string)))
        if isinstance(string, str):
            self._sock.send(str.encode(string, 'utf-8'))
        else:
            self._sock.send(string)

    @staticmethod
    def _check_topic(topic):
        """Checks if topic provided is a valid mqtt topic.
        :param str topic: Topic identifier
        """
        if topic is None:
            raise MMQTTException('Topic may not be NoneType')
        # [MQTT-4.7.3-1]
        elif not topic:
            raise MMQTTException('Topic may not be empty.')
        # [MQTT-4.7.3-3]
        elif len(topic.encode('utf-8')) > MQTT_TOPIC_LENGTH_LIMIT:
            raise MMQTTException('Topic length is too large.')

    @staticmethod
    def _check_qos(qos_level):
        """Validates the quality of service level.
        :param int qos_level: Desired QoS level.
        """
        if isinstance(qos_level, int):
            if qos_level < 0 or qos_level > 2:
                raise MMQTTException('QoS must be between 1 and 2.')
        else:
            raise MMQTTException('QoS must be an integer.')

    def _set_interface(self):
        """Sets a desired network hardware interface.
        The network hardware must be set in init
        prior to calling this method.
        """
        if self._wifi:
            self._socket.set_interface(self._wifi.esp)
        else:
            raise TypeError('Network Manager Required.')

    def is_connected(self):
        """Returns MQTT client session status as True if connected, raises
        a MMQTTException if False.
        """
        if self._sock is None or self._is_connected is False:
            raise MMQTTException("MiniMQTT is not connected.")
        return self._is_connected

    @property
    def mqtt_msg(self):
        """Returns maximum MQTT payload and topic size."""
        return self._msg_size_lim, MQTT_TOPIC_LENGTH_LIMIT

    @mqtt_msg.setter
    def mqtt_msg(self, msg_size):
        """Sets the maximum MQTT message payload size.
        :param int msg_size: Maximum MQTT payload size.
        """
        if msg_size < MQTT_MSG_MAX_SZ:
            self._msg_size_lim = msg_size

    # Logging
    def attach_logger(self, logger_name='log'):
        """Initializes and attaches a logger to the MQTTClient.
        :param str logger_name: Name of the logger instance
        """
        #self.logger = logging.getLogger(logger_name)
        #self.logger.setLevel(logging.INFO)

    def set_logger_level(self, log_level):
        """Sets the level of the logger, if defined during init.
        :param string log_level: Level of logging to output to the REPL.
        """
#        if self.logger is None:
#            raise MMQTTException('No logger attached - did you create it during initialization?')
#        if log_level == 'DEBUG':
#            self.logger.setLevel(logging.DEBUG)
#        elif log_level == 'INFO':
#            self.logger.setLevel(logging.INFO)
#        elif log_level == 'WARNING':
#            self.logger.setLevel(logging.WARNING)
#        elif log_level == 'ERROR':
#            self.logger.setLevel(logging.CRITICIAL)
#        else:
#            raise MMQTTException('Incorrect logging level provided!')

PWMOut.py

# The MIT License (MIT)
#
# Copyright (c) 2019 Brent Rubell 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
# 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.
"""
`PWMOut`
==============================
PWMOut CircuitPython API for ESP32SPI forked to Openmv.

* Author(s): Brent Rubell
* Forked by: Nezra
"""

class PWMOut():
    """
    Implementation of CircuitPython PWMOut for ESP32SPI.

    :param int esp_pin: Valid ESP32 GPIO Pin, predefined in ESP32_GPIO_PINS.
    :param ESP_SPIcontrol esp: The ESP object we are using.
    :param int duty_cycle: The fraction of each pulse which is high, 16-bit.
    :param int frequency: The target frequency in Hertz (32-bit).
    :param bool variable_frequency: True if the frequency will change over time.
    """
    ESP32_PWM_PINS = set([0, 1, 2, 4, 5,
                          12, 13, 14, 15,
                          16, 17, 18, 19,
                          21, 22, 23, 25,
                          26, 27, 32, 33])
    def __init__(self, esp, pwm_pin, *, frequency=500, duty_cycle=0, variable_frequency=False):
        if pwm_pin in self.ESP32_PWM_PINS:
            self._pwm_pin = pwm_pin
        else:
            raise AttributeError("Pin %d is not a valid ESP32 GPIO Pin."%pwm_pin)
        self._esp = esp
        self._duty_cycle = duty_cycle
        self._freq = frequency
        self._var_freq = variable_frequency

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.deinit()

    def deinit(self):
        """De-initalize the PWMOut object."""
        self._duty_cycle = 0
        self._freq = 0
        self._pwm_pin = None

    def _is_deinited(self):
        """Checks if PWMOut object has been previously de-initalized"""
        if self._pwm_pin is None:
            raise ValueError("PWMOut Object has been deinitialized and can no longer "
                             "be used. Create a new PWMOut object.")

    @property
    def duty_cycle(self):
        """Returns the PWMOut object's duty cycle as a
        ratio from 0.0 to 1.0."""
        self._is_deinited()
        return self._duty_cycle

    @duty_cycle.setter
    def duty_cycle(self, duty_cycle):
        """Sets the PWMOut duty cycle.
        :param float duty_cycle: Between 0.0 (low) and 1.0 (high).
        :param int duty_cycle: Between 0 (low) and 1 (high).
        """
        self._is_deinited()
        if not isinstance(duty_cycle, (int, float)):
            raise TypeError("Invalid duty_cycle, should be int or float.")
        duty_cycle /= 65535.0
        if not 0.0 <= duty_cycle <= 1.0:
            raise ValueError("Invalid duty_cycle, should be between 0.0 and 1.0")
        self._esp.set_analog_write(self._pwm_pin, duty_cycle)

    @property
    def frequency(self):
        """Returns the PWMOut object's frequency value."""
        self._is_deinited()
        return self._freq

    @frequency.setter
    def frequency(self, freq):
        """Sets the PWMOut object's frequency value.
        :param int freq: 32-bit value that dictates the PWM frequency in Hertz.
        NOTE: Only writeable when constructed with variable_Frequency=True.
        """
        self._is_deinited()
        self._freq = freq
        raise NotImplementedError("PWMOut Frequency not implemented in ESP32SPI")

secrets.py

# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it
# which would be not great. So, instead, keep it all in this one file and
# keep it a secret.

secrets = {
    'ssid' : '',     # Keep the two '' quotes around the name
    'password' : '',   # Keep the two '' quotes around password
    'timezone' : "America/New_York",  # http://worldtimeapi.org/timezones
    'aio_username' : 'YOUR_ADAFRUIT_ACCOUNT_USERNAME',
    'aio_key' : 'YOUR_ADAFRUITIO_KEY',
    'broker' : '', #mqtt broker
    'port' : 1883, #mqtt port, integer. no quotes needed
    'user' : '', #mqtt username
    'pass' : '', #mqtt password
    }

and finally, a simple example for wifi. i have a few more if anyone is interested.

#openmv required libraries
import utime
from pyb import Pin, SPI

#replacement adafruit libraries
import esp32spi_wifimanager
import esp32spi
import esp32spi_socket as socket
import esp32spi_requests as requests

# 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

esp32_cs = Pin("P3", Pin.OUT_OD)
esp32_ready = Pin("P7", Pin.IN)
esp32_reset = Pin("P8", Pin.OUT_PP)
#mosi -> mosi
#miso -> miso
#ss -> ss
#sclk -> sck

print("ESP32 SPI webclient test")

spi = SPI(2,SPI.MASTER,baudrate=19999999,polarity=1,phase=1)
esp = esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, debug=False)

wifi = esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets)
print("connecting to AP")
wifi.connect()

requests.set_socket(socket, esp)


TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html"
JSON_URL = "http://api.coindesk.com/v1/bpi/currentprice/USD.json"

if esp.status == esp32spi.WL_IDLE_STATUS:
    print("ESP32 found and in idle mode")

try:
    print("Firmware vers.", esp.firmware_version)
    print("MAC addr:", [hex(i) for i in esp.MAC_address])

    for ap in esp.scan_networks():
        print("%s\tRSSI: %d" % (str(ap['ssid'], 'utf-8'), ap['rssi']))

    print("Connecting to AP...")

    print("Connected to", str(esp.ssid, 'utf-8'), "\tRSSI:", esp.rssi)
    print("My IP address is", esp.pretty_ip(esp.ip_address))
    print("IP lookup adafruit.com: %s" % esp.pretty_ip(esp.get_host_by_name("adafruit.com")))
    print("Ping google.com: %d ms" % esp.ping("google.com"))

    print("Fetching text from", TEXT_URL)
    r = requests.get(TEXT_URL)
    print('-'*40)
    print(r.text)
    print('-'*40)
    r.close()

    print()
    print("Fetching json from", JSON_URL)
    r = requests.get(JSON_URL)
    print('-'*40)
    print(r.json())
    print('-'*40)
    r.close()

    print("Done!")

except (ValueError, RuntimeError) as e:
    print("Failed to update server, restarting ESP32\n", e)
    wifi.reset()

ESP32SPI_wifimanager.py attached. can’t include it via code as the forums hate it for some reason.

if i can ever get around to setting up an environment to compile my own firmware, i’ll get to work on the C driver for micropython/openmv; but this at least allows anyone who wants to drag and drop code for it to just work to do so without custom firmwares.

Let me know if this proves useful to anyone!
esp32spi_wifimanager.py (9.1 KB)

Hi, can you just post everything in a zip file? That would be easier for folks to use.

Also, please give me a list of things to fix. I’ll add them to github issues. I will be going back to firmware development soon after doing some work on the store.