diff --git a/.travis.yml b/.travis.yml index 57b831a..5087119 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ install: - pip install circuitpython-build-tools Sphinx sphinx-rtd-theme - pip install --force-reinstall pylint==1.9.2 script: -- pylint adafruit_espatcontrol.py +- pylint adafruit_espatcontrol/*.py - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py) - circuitpython-build-bundles --filename_prefix adafruit-circuitpython-espatcontrol --library_location . - cd docs && sphinx-build -E -W -b html . _build/html && cd .. diff --git a/adafruit_espatcontrol.py b/adafruit_espatcontrol/adafruit_espatcontrol.py similarity index 90% rename from adafruit_espatcontrol.py rename to adafruit_espatcontrol/adafruit_espatcontrol.py index 3f61c6f..d3dc291 100644 --- a/adafruit_espatcontrol.py +++ b/adafruit_espatcontrol/adafruit_espatcontrol.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -`adafruit_espatcontrol` +`adafruit_espatcontrol.adafruit_espatcontrol` ==================================================== Use the ESP AT command sent to communicate with the Interwebs. @@ -60,20 +60,6 @@ class OKError(Exception): """The exception thrown when we didn't get acknowledgement to an AT command""" pass -class ESP_ATcontrol_socket: - """A 'socket' compatible interface thru the ESP AT command set""" - def __init__(self, esp): - self._esp = esp - - def getaddrinfo(self, host, port, # pylint: disable=too-many-arguments - family=0, socktype=0, proto=0, flags=0): # pylint: disable=unused-argument - """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 = self._esp.nslookup(host) - return [(family, socktype, proto, '', (ipaddr, port))] - class ESP_ATcontrol: """A wrapper for AT commands to a connected ESP8266 or ESP32 module to do some very basic internetting. The ESP module must be pre-programmed with @@ -147,55 +133,18 @@ def begin(self): except OKError: pass #retry - def request_url(self, url, ssl=False, request_type="GET"): - """Send an HTTP request to the URL. If the URL starts with https:// - we will force SSL and use port 443. Otherwise, you can select whether - you want ssl by passing in a flag.""" - if url.startswith("https://"): - ssl = True - url = url[8:] - if url.startswith("http://"): - url = url[7:] - domain, path = url.split('/', 1) - path = '/'+path - port = 80 - conntype = self.TYPE_TCP - if ssl: - conntype = self.TYPE_SSL - port = 443 - if not self.socket_connect(conntype, domain, port, keepalive=10, retries=3): - raise RuntimeError("Failed to connect to host") - request = request_type+" "+path+" HTTP/1.1\r\n" - request += "Host: "+domain+"\r\n" - request += "User-Agent: "+self.USER_AGENT+"\r\n" - request += "\r\n" - try: - self.socket_send(bytes(request, 'utf-8')) - except RuntimeError: - raise - - reply = self.socket_receive(timeout=3).split(b'\r\n') - if self._debug: - print(reply) - try: - headerbreak = reply.index(b'') - except ValueError: - raise RuntimeError("Reponse wasn't valid HTML") - header = reply[0:headerbreak] - data = b'\r\n'.join(reply[headerbreak+1:]) # put back the way it was - self.socket_disconnect() - return (header, data) - def connect(self, settings): """Repeatedly try to connect to an access point with the details in the passed in 'settings' dictionary. Be sure 'ssid' and 'password' are defined in the settings dict! If 'timezone' is set, we'll also configure SNTP""" # Connect to WiFi if not already + retries = 3 while True: try: - if not self._initialized: + if not self._initialized or retries == 0: self.begin() + retries = 3 AP = self.remote_AP # pylint: disable=invalid-name print("Connected to", AP[0]) if AP[0] != settings['ssid']: @@ -210,6 +159,7 @@ def connect(self, settings): return # yay! except (RuntimeError, OKError) as exp: print("Failed to connect, retrying\n", exp) + retries -= 1 continue # *************************** SOCKET SETUP **************************** @@ -223,10 +173,6 @@ def cipmux(self): return int(reply[8:]) raise RuntimeError("Bad response to CIPMUX?") - def socket(self): - """Create a 'socket' object""" - return ESP_ATcontrol_socket(self) - def socket_connect(self, conntype, remote, remote_port, *, keepalive=10, retries=1): """Open a socket. conntype can be TYPE_TCP, TYPE_UDP, or TYPE_SSL. Remote can be an IP address or DNS (we'll do the lookup for you. Remote port @@ -283,10 +229,11 @@ def socket_send(self, buffer, timeout=1): return True def socket_receive(self, timeout=5): - # pylint: disable=too-many-nested-blocks + # pylint: disable=too-many-nested-blocks, too-many-branches """Check for incoming data over the open socket, returns bytes""" incoming_bytes = None - bundle = b'' + bundle = [] + toread = 0 gc.collect() i = 0 # index into our internal packet stamp = time.monotonic() @@ -312,6 +259,9 @@ def socket_receive(self, timeout=5): except ValueError: raise RuntimeError("Parsing error during receive", ipd) i = 0 # reset the input buffer now that we know the size + elif i > 20: + i = 0 # Hmm we somehow didnt get a proper +IPD packet? start over + else: self.hw_flow(False) # stop the flow # read as much as we can! @@ -322,11 +272,22 @@ def socket_receive(self, timeout=5): if i == incoming_bytes: #print(self._ipdpacket[0:i]) gc.collect() - bundle += self._ipdpacket[0:i] + bundle.append(self._ipdpacket[0:i]) + gc.collect() i = incoming_bytes = 0 else: # no data waiting self.hw_flow(True) # start the floooow - return bundle + totalsize = sum([len(x) for x in bundle]) + ret = bytearray(totalsize) + i = 0 + for x in bundle: + for char in x: + ret[i] = char + i += 1 + for x in bundle: + del x + gc.collect() + return ret def socket_disconnect(self): """Close any open socket, if there is one""" @@ -371,6 +332,7 @@ def is_connected(self): self.begin() try: self.echo(False) + self.baudrate = self.baudrate stat = self.status if stat in (self.STATUS_APCONNECTED, self.STATUS_SOCKETOPEN, diff --git a/adafruit_espatcontrol/adafruit_espatcontrol_requests.py b/adafruit_espatcontrol/adafruit_espatcontrol_requests.py new file mode 100644 index 0000000..7f9381c --- /dev/null +++ b/adafruit_espatcontrol/adafruit_espatcontrol_requests.py @@ -0,0 +1,184 @@ +""" +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' +""" + +# pylint: disable=no-name-in-module + +import gc +import adafruit_espatcontrol.adafruit_espatcontrol_socket as socket + +_the_interface = None # pylint: disable=invalid-name +def set_interface(iface): + """Helper to set the global internet interface""" + global _the_interface # pylint: disable=invalid-name, global-statement + _the_interface = iface + socket.set_interface(iface) + +class Response: + """The response from a request, contains all the headers/content""" + headers = {} + encoding = None + + def __init__(self, f): + self.raw = f + self.encoding = "utf-8" + self._cached = None + self.status_code = None + self.reason = None + + def close(self): + """Close, delete and collect the response data""" + if self.raw: + self.raw.close() + del self.raw + del self._cached + gc.collect() + + @property + def content(self): + """The HTTP content direct from the socket, as bytes""" + if self._cached is None: + try: + self._cached = self.raw.read() + finally: + self.raw.close() + self.raw = None + 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""" + import ujson + return ujson.loads(self.content) + + +# 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=None): + """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' is unused in this implementation""" + global _the_interface # pylint: disable=global-statement, invalid-name + + if not headers: + headers = {} + + try: + proto, dummy, host, path = url.split("/", 3) + 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 = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] + sock = socket.socket(addr_info[0], addr_info[1], addr_info[2]) + resp = Response(sock) # our response + + try: + conntype = _the_interface.TYPE_TCP + if proto == "https:": + conntype = _the_interface.TYPE_SSL + sock.connect(addr_info[-1], conntype) + sock.write(b"%s /%s HTTP/1.0\r\n" % (method, path)) + if "Host" not in headers: + sock.write(b"Host: %s\r\n" % host) + if "User-Agent" not in headers: + sock.write(b"User-Agent: Adafruit CircuitPython\r\n") + # Iterate over keys to avoid tuple alloc + for k in headers: + sock.write(k) + sock.write(b": ") + sock.write(headers[k]) + sock.write(b"\r\n") + if json is not None: + assert data is None + import ujson + data = ujson.dumps(json) + sock.write(b"Content-Type: application/json\r\n") + if data: + sock.write(b"Content-Length: %d\r\n" % len(data)) + sock.write(b"\r\n") + if data: + sock.write(data) + + line = sock.readline() + #print(line) + line = line.split(None, 2) + status = int(line[1]) + reason = "" + if len(line) > 2: + reason = line[2].rstrip() + while True: + line = sock.readline() + if not line or line == b"\r\n": + break + + #print(line) + title, content = line.split(b': ', 1) + if title and content: + title = str(title.lower(), 'utf-8') + content = str(content, 'utf-8') + resp.headers[title] = content + + if line.startswith(b"Transfer-Encoding:"): + if b"chunked" in line: + raise ValueError("Unsupported " + line) + elif line.startswith(b"Location:") and not 200 <= status <= 299: + raise NotImplementedError("Redirects not yet supported") + + except OSError: + sock.close() + raise + + resp.status_code = status + resp.reason = reason + return resp +# pylint: enable=too-many-branches, too-many-statements, unused-argument +# pylint: enable=too-many-arguments, too-many-locals + +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) diff --git a/adafruit_espatcontrol/adafruit_espatcontrol_socket.py b/adafruit_espatcontrol/adafruit_espatcontrol_socket.py new file mode 100644 index 0000000..cecbb79 --- /dev/null +++ b/adafruit_espatcontrol/adafruit_espatcontrol_socket.py @@ -0,0 +1,73 @@ +"""A 'socket' compatible interface thru the ESP AT command set""" +from micropython import const + +_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) + +# 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.nslookup(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): + 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'' + + def connect(self, address, conntype=None): + """Connect the socket to the 'address' (which should be dotted quad IP). 'conntype' + is an extra that may indicate SSL or not, depending on the underlying interface""" + host, port = address + if not _the_interface.socket_connect(conntype, host, port, keepalive=10, retries=3): + raise RuntimeError("Failed to connect to host", host) + self._buffer = b'' + + def write(self, data): # pylint: disable=no-self-use + """Send some data to the socket""" + _the_interface.socket_send(data) + + def readline(self): + """Attempt to return as many bytes as we can up to but not including '\r\n'""" + if b'\r\n' not in self._buffer: + # there's no line already in there, read some more + self._buffer = self._buffer + _the_interface.socket_receive(timeout=3) + #print(self._buffer) + firstline, self._buffer = self._buffer.split(b'\r\n', 1) + return firstline + + def read(self, num=0): + """Read up to 'num' bytes from the socket, this may be buffered internally! + If 'num' isnt specified, return everything in the buffer.""" + if num == 0: + # read as much as we can + ret = self._buffer + _the_interface.socket_receive(timeout=1) + self._buffer = b'' + else: + ret = self._buffer[:num] + self._buffer = self._buffer[num:] + return ret + + def close(self): + """Close the socket, after reading whatever remains""" + # read whatever's left + self._buffer = self._buffer + _the_interface.socket_receive(timeout=1) + _the_interface.socket_disconnect() +# pylint: enable=unused-argument, redefined-builtin, invalid-name diff --git a/docs/api.rst b/docs/api.rst index eab3b28..659ccdf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,5 +4,11 @@ .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. use this format as the module name: "adafruit_foo.foo" -.. automodule:: adafruit_espatcontrol +.. automodule:: adafruit_espatcontrol.adafruit_espatcontrol + :members: + +.. automodule:: adafruit_espatcontrol.adafruit_espatcontrol_requests + :members: + +.. automodule:: adafruit_espatcontrol.adafruit_espatcontrol_socket :members: diff --git a/examples/espatcontrol_cheerlights.py b/examples/espatcontrol_cheerlights.py index b37a61f..fd308a7 100644 --- a/examples/espatcontrol_cheerlights.py +++ b/examples/espatcontrol_cheerlights.py @@ -8,12 +8,10 @@ import busio from digitalio import DigitalInOut import adafruit_espatcontrol +import adafruit_espatcontrol_requests as requests import neopixel -import ujson import adafruit_fancyled.adafruit_fancyled as fancy - - # Get wifi details and more from a settings.py file try: from settings import settings @@ -38,6 +36,8 @@ rts_pin=rtspin, debug=True) esp.hard_reset() +requests.set_interface(esp) + # neopixels pixels = neopixel.NeoPixel(board.A1, 16, brightness=0.3) pixels.fill(0) @@ -49,19 +49,6 @@ the_time = None times = 0 -def get_value(response, location): - """Extract a value from a json object, based on the path in 'location'""" - try: - print("Parsing JSON response...", end='') - json = ujson.loads(response) - print("parsed OK!") - for x in location: - json = json[x] - return json - except ValueError: - print("Failed to parse json, retrying") - return None - while True: try: while not esp.is_connected: @@ -72,25 +59,22 @@ def get_value(response, location): # great, lets get the data print("Retrieving data source...", end='') builtin[0] = (100, 100, 0) - header, body = esp.request_url(DATA_SOURCE) + r = requests.get(DATA_SOURCE) builtin[0] = (0, 0, 100) print("Reply is OK!") except (RuntimeError, adafruit_espatcontrol.OKError) as e: print("Failed to get data, retrying\n", e) continue - print('-'*40, "Size: ", len(body)) - print(str(body, 'utf-8')) + print('-'*40,) + print("Headers: ", r.headers) + print("Text:", r.text) print('-'*40) # For mystery reasons, there's two numbers before and after the json data - lines = body.split(b'\r\n') # so split into lines - try: - value = get_value(lines[1], DATA_LOCATION) # an get the middle chunk - builtin[0] = (100, 100, 100) - if not value: - continue - print(times, the_time, "value:", value) - except IndexError as e: - print("IndexError processing Body, retrying\n", e) + value = r.json() + for x in DATA_LOCATION: + value = value[x] + builtin[0] = (100, 100, 100) + if not value: continue if last_value != value: color = int(value[1:],16) @@ -104,7 +88,7 @@ def get_value(response, location): times += 1 # normally we wouldn't have to do this, but we get bad fragments - header = body = None + r = None gc.collect() print(gc.mem_free()) # pylint: disable=no-member time.sleep(TIME_BETWEEN_QUERY) diff --git a/examples/espatcontrol_countviewer.py b/examples/espatcontrol_countviewer.py index 25e886d..7b372bd 100644 --- a/examples/espatcontrol_countviewer.py +++ b/examples/espatcontrol_countviewer.py @@ -9,9 +9,9 @@ import busio from digitalio import DigitalInOut import adafruit_espatcontrol +import adafruit_espatcontrol_requests as requests from adafruit_ht16k33 import segments import neopixel -import ujson # Get wifi details and more from a settings.py file try: @@ -68,6 +68,8 @@ rts_pin=rtspin, debug=True) esp.hard_reset() +requests.set_interface(esp) + # Create the I2C interface. i2c = busio.I2C(board.SCL, board.SDA) # Attach a 7 segment display and display -'s so we know its not live yet @@ -105,38 +107,29 @@ def chime_light(): pixels.fill((i, i, i)) pixels.fill(0) -def get_value(response, location): - """Extract a value from a json object, based on the path in 'location'""" - try: - print("Parsing JSON response...", end='') - json = ujson.loads(response) - print("parsed OK!") - for x in location: - json = json[x] - return json - except ValueError: - print("Failed to parse json, retrying") - return None - while True: try: while not esp.is_connected: # settings dictionary must contain 'ssid' and 'password' at a minimum esp.connect(settings) - # great, lets get the data - # get the time + the_time = esp.sntp_time + # great, lets get the data print("Retrieving data source...", end='') - header, body = esp.request_url(DATA_SOURCE) + r = requests.get(DATA_SOURCE) print("Reply is OK!") except (RuntimeError, adafruit_espatcontrol.OKError) as e: print("Failed to get data, retrying\n", e) continue - #print('-'*40, "Size: ", len(body)) - #print(str(body, 'utf-8')) + #print('-'*40,) + #print("Headers: ", r.headers) + #print("Text:", r.text) #print('-'*40) - value = get_value(body, DATA_LOCATION) + + value = r.json() + for x in DATA_LOCATION: + value = value[x] if not value: continue print(times, the_time, "value:", value) @@ -146,8 +139,9 @@ def get_value(response, location): chime_light() # animate the neopixels last_value = value times += 1 + # normally we wouldn't have to do this, but we get bad fragments - header = body = None + r = value = None gc.collect() print(gc.mem_free()) # pylint: disable=no-member time.sleep(TIME_BETWEEN_QUERY) diff --git a/examples/espatcontrol_post.py b/examples/espatcontrol_post.py deleted file mode 100644 index e73171e..0000000 --- a/examples/espatcontrol_post.py +++ /dev/null @@ -1,62 +0,0 @@ -import time -import board -import busio -from digitalio import DigitalInOut -import adafruit_espatcontrol - - -# Get wifi details and more from a settings.py file -try: - from settings import settings -except ImportError: - print("WiFi settings are kept in settings.py, please add them there!") - raise - - - -URL = "https://io.adafruit.com/api/v2/webhooks/feed/"+settings['aio_feed_webhook']+"?value=" - -# With a Metro or Feather M4 -resetpin = DigitalInOut(board.D5) -rtspin = DigitalInOut(board.D9) -uart = busio.UART(board.TX, board.RX, timeout=0.1) - -# With a Particle Argon -""" -RX = board.ESP_TX -TX = board.ESP_RX -resetpin = DigitalInOut(board.ESP_WIFI_EN) -rtspin = DigitalInOut(board.ESP_CTS) -uart = busio.UART(TX, RX, timeout=0.1) -esp_boot = DigitalInOut(board.ESP_BOOT_MODE) -from digitalio import Direction -esp_boot.direction = Direction.OUTPUT -esp_boot.value = True -""" - -print("Post to a URL", URL) - -esp = adafruit_espatcontrol.ESP_ATcontrol(uart, 115200, reset_pin=resetpin, - run_baudrate = 115200, rts_pin=rtspin, debug=True) -print("Resetting ESP module") -esp.hard_reset() -print("Connected to AT software version", esp.get_version()) - -counter = 0 -while True: - try: - # Connect to WiFi if not already - while not esp.is_connected: - print("Connecting...") - esp.connect(settings) - print("Connected to", esp.remote_AP) - # great, lets get the data - print("Posting request URL...", end='') - header, body = esp.request_url(URL+str(counter), request_type = "POST") - counter = counter + 1 - print("OK") - except (RuntimeError, adafruit_espatcontrol.OKError) as e: - print("Failed to get data, retrying\n", e) - continue - header = body = None - time.sleep(15) diff --git a/examples/espatcontrol_quoteEPD.py b/examples/espatcontrol_quoteEPD.py new file mode 100644 index 0000000..523d994 --- /dev/null +++ b/examples/espatcontrol_quoteEPD.py @@ -0,0 +1,205 @@ +""" +This example will access an API, grab a number like hackaday skulls, github +stars, price of bitcoin, twitter followers... if you can find something that +spits out JSON data, we can display it! +""" + +import gc +import time +import board +import busio +from digitalio import DigitalInOut +from Adafruit_CircuitPython_ESP_ATcontrol import adafruit_espatcontrol +import ujson +from adafruit_epd.epd import Adafruit_EPD +from adafruit_epd.il0373 import Adafruit_IL0373 + + +# Get wifi details and more from a settings.py file +try: + from settings import settings +except ImportError: + print("WiFi settings are kept in settings.py, please add them there!") + raise + +# CONFIGURATION +TIME_BETWEEN_QUERY = 0 # in seconds +DATA_SOURCE = "https://www.adafruit.com/api/quotes.php" + + +# create the spi device and pins we will need +spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) +epd_cs = DigitalInOut(board.D10) +epd_dc = DigitalInOut(board.D11) +epd_rst = DigitalInOut(board.D12) +# give them all to our driver +display = Adafruit_IL0373(104, 212, spi, + cs_pin=epd_cs, dc_pin=epd_dc, sramcs_pin=None, + rst_pin=epd_rst, busy_pin=None) +display.rotation = 3 + +uart = busio.UART(board.TX, board.RX, timeout=0.1) +resetpin = DigitalInOut(board.D5) +rtspin = DigitalInOut(board.D6) + +# Create the connection to the co-processor and reset +esp = adafruit_espatcontrol.ESP_ATcontrol(uart, 115200, run_baudrate=115200, + reset_pin=resetpin, + rts_pin=rtspin, debug=True) +esp.hard_reset() + +# Extract a value from a json string +def get_value(response, location): + """Extract a value from a json object, based on the path in 'location'""" + try: + print("Parsing JSON response...", end='') + json = ujson.loads(response) + print("parsed OK!") + for x in location: + json = json[x] + return json + except ValueError: + print("Failed to parse json, retrying") + return None + +# return a list of lines with wordwrapping +def wrap_nicely(string, max_chars): + words = string.split(' ') + the_lines = [] + the_line = "" + for w in words: + if len(the_line+' '+w) <= max_chars: + the_line += ' '+w + else: + the_lines.append(line) + the_line = ''+w + if the_line: # last line remaining + the_lines.append(line) + return the_lines + +def read_le(s): + # as of this writting, int.from_bytes does not have LE support, DIY! + result = 0 + shift = 0 + for byte in bytearray(s): + result += byte << shift + shift += 8 + return result + +class BMPError(Exception): + pass + +def draw_bmp(filename, x, y): # pylint: disable=too-many-locals, too-many-branches + try: + with open("/" + filename, "rb") as f: + print("File opened") + if f.read(2) != b'BM': # check signature + raise BMPError("Not BitMap file") + + bmpFileSize = read_le(f.read(4)) + f.read(4) # Read & ignore creator bytes + + bmpImageoffset = read_le(f.read(4)) # Start of image data + headerSize = read_le(f.read(4)) + bmpWidth = read_le(f.read(4)) + bmpHeight = read_le(f.read(4)) + flip = True + + print("Size: %d\nImage offset: %d\nHeader size: %d" % + (bmpFileSize, bmpImageoffset, headerSize)) + print("Width: %d\nHeight: %d" % (bmpWidth, bmpHeight)) + + if read_le(f.read(2)) != 1: + raise BMPError("Not singleplane") + bmpDepth = read_le(f.read(2)) # bits per pixel + print("Bit depth: %d" % (bmpDepth)) + if bmpDepth != 24: + raise BMPError("Not 24-bit") + if read_le(f.read(2)) != 0: + raise BMPError("Compressed file") + + print("Image OK! Drawing...") + + rowSize = (bmpWidth * 3 + 3) & ~3 # 32-bit line boundary + + for row in range(bmpHeight): # For each scanline... + if flip: # Bitmap is stored bottom-to-top order (normal BMP) + pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize + else: # Bitmap is stored top-to-bottom + pos = bmpImageoffset + row * rowSize + + # print ("seek to %d" % pos) + f.seek(pos) + for col in range(bmpWidth): + b, g, r = bytearray(f.read(3)) # BMP files store RGB in BGR + color = None + if r < 0x80 and g < 0x80 and b < 0x80: + color = Adafruit_EPD.BLACK + elif r >= 0x80 and g >= 0x80 and b >= 0x80: + color = Adafruit_EPD.WHITE + elif r >= 0x80: + color = Adafruit_EPD.RED + display.pixel(x+row, y+col, color) + + except OSError as e: + if e.args[0] == 28: + raise OSError("OS Error 28 0.25") + else: + raise OSError("OS Error 0.5") + except BMPError as e: + print("Failed to parse BMP: " + e.args[0]) + +quote = "Eternal vigilance is not only the price of liberty; eternal vigilance is the price of human decency" # pylint: disable=line-too-long +author = "Aldous Huxley" +lines = wrap_nicely(str(quote, 'utf-8'), (210 - 50)//6) +print(lines) +start_x = 10 +start_y = 10 +display.fill(Adafruit_EPD.WHITE) +draw_bmp("lilblinka.bmp", display.width - 75, display.height - 80) +for i,line in enumerate(lines): + display.text(line, start_x, start_y+i*10, Adafruit_EPD.BLACK) +display.text(author, 10, 100-20, Adafruit_EPD.RED) + +display.display() + + +while True: + try: + while not esp.is_connected: + # settings dictionary must contain 'ssid' and 'password' at a minimum + esp.connect(settings) + # great, lets get the data + + print("Retrieving data source...", end='') + header, body = esp.request_url(DATA_SOURCE) + print("Reply is OK!") + except (RuntimeError, adafruit_espatcontrol.OKError) as e: + print("Failed to get data, retrying\n", e) + continue + + body = body.split(b'\n')[1] # unclear why but there's extra data at beginning/end + print('-'*40, "Size: ", len(body)) + print(str(body, 'utf-8')) + print('-'*40) + quote = get_value(body, [0, "text"]) + author = get_value(body, [0, "author"]) + if not quote or not author: + continue + print(quote, author) + + lines = wrap_nicely(str(quote, 'utf-8'), (display.width - 50)//6) + start_x = 10 + start_y = 10 + display.fill(Adafruit_EPD.WHITE) + draw_bmp("lilblinka.bmp", display.width - 75, display.height - 80) + for i,line in enumerate(lines): + display.text(line, start_x, start_y+i*10, Adafruit_EPD.BLACK) + display.text(author, 10, display.height-20, Adafruit_EPD.RED) + display.display() + + # normally we wouldn't have to do this, but we get bad fragments + header = body = None + gc.collect() + print(gc.mem_free()) # pylint: disable=no-member + time.sleep(TIME_BETWEEN_QUERY) diff --git a/examples/espatcontrol_webclient.py b/examples/espatcontrol_webclient.py index 44bc02d..5aebd7b 100644 --- a/examples/espatcontrol_webclient.py +++ b/examples/espatcontrol_webclient.py @@ -3,6 +3,7 @@ import busio from digitalio import DigitalInOut import adafruit_espatcontrol +import adafruit_espatcontrol_requests as requests # Get wifi details and more from a settings.py file try: @@ -22,6 +23,8 @@ print("Resetting ESP module") esp.hard_reset() +requests.set_interface(esp) + while True: try: print("Checking connection...") @@ -30,12 +33,12 @@ esp.connect(settings) # great, lets get the data print("Retrieving URL...", end='') - header, body = esp.request_url(URL) - print("OK") - - print('-'*40) - print(str(body, 'utf-8')) - print('-'*40) + r = requests.get(URL) + print("Status:", r.status_code) + print("Content type:", r.headers['content-type']) + print("Content size:", r.headers['content-length']) + print("Encoding:", r.encoding) + print("Text:", r.text) time.sleep(60) except (RuntimeError, adafruit_espatcontrol.OKError) as e: diff --git a/setup.py b/setup.py index c09fab2..af9e2b6 100644 --- a/setup.py +++ b/setup.py @@ -56,5 +56,5 @@ # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). - py_modules=['adafruit_espatcontrol'], -) \ No newline at end of file + packages=['adafruit_espatcontrol'], +)