From 4f7bc8c62ee248361eb0f1f93edb484a0341855b Mon Sep 17 00:00:00 2001 From: nfa Date: Thu, 23 Dec 2021 14:17:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20'src/lib'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/adafruit_dht.mpy | Bin 0 -> 3935 bytes src/lib/adafruit_requests.py | 669 +++++++++++++++++++++++++++++++++++ 2 files changed, 669 insertions(+) create mode 100644 src/lib/adafruit_dht.mpy create mode 100644 src/lib/adafruit_requests.py diff --git a/src/lib/adafruit_dht.mpy b/src/lib/adafruit_dht.mpy new file mode 100644 index 0000000000000000000000000000000000000000..fc561b32d76d7d099563a978174bc1b36d4b4ee5 GIT binary patch literal 3935 zcmb_f+iw%u89$#dNlc<{61-+hoM1Z>=R$BCvyNG=2}zbUgkadvB8BdF;yJcQ*kgBQ z3}mG$V;0nkZmaD-(AT~?H`td-Eth3qSKA6&(XO--53MMxQX%zi`?iv{h@0sNW6QlcRkpBGZU+y5}TfHY604@Od zI|$q$=$;`+K$Zh(2)H4L=$>KljDTkp683Bmq+#HOK^g&W1f)^mMj_J8jlr?Ei0&C@ z7x#iR4%|3IdXYUB-KmHBlBDkvN!Y%MIrYeV6!aNS2)KQ)i*OU*OM)j1qKymV(&ouP zA5wNMgOuHqHK{E0EO#8v7sOJI9(_X)g|t$VG+xYVq2j7jP!z#@Iy5Oyjfb-&3wdfQ zsMSUF*)=C+Bu&W0LfByY>K5>5G z{rUKrc9gEtxZGZ3zfz%2n|#aR&Un69P_%hPDJVM^3mTF+ny0cxMT6(uADRk-$>dP=^R_fRiGEtI1Y@vK2_AsWO7I=fE5U=%QwbhokHfH42_6B@QSc6d zNdEF=tXr@2AAu>C?)`x5a*A*f7#-`=z2mq#0B-$kuV}9Xk0Eso2BslRBA7Wf+gttE z#xiJf(!m!cc?Yj4Qii`GNLm-46I6{)3Ar2SF%+0l~lH8!Eq>LCj@I_*9-!l3gG|#Bky9 zstXO&w7Sn?le4HuvR1dDO{5HOrV(oY9NougNytebGHsY?p_=83U4%FYanCYkX7o1# zfnhGNH}LLhR-W&5-{0Jb`tr{W`N3BCo7+$CZGU;c{A6K>7MIu|vFQdCv^G06aiUq$VPFAvv1J33(QbWx;3vdjoJJ3u zn^S*VTVJ+hotcTP`Zt$+c+V1tdIX%Q92Jy0*``pnl&4KFueJ#_M`^KsstUa}p;)A{ zSm(AE3s{fSjp&+TO*_J&Nc7soP8-8gV3E1%Lv+>Y*|DL$^O4cO$-yhrCkICd*Cxl` zKGxzl?=ZUP00QmE-^rb`~e@GSIh1pA@MZ`}==Io18!>(ehx&n}_!?)Bbh zcYk2qJ@!4~9fAG}Nc^zceZ7~+SFs&>q=#{g?i=mXw>SFx;c={2N9b%~B-q=StR}Hr z&v)4#PW^Gs``!{cwpZlNxON7r^WW9eS zpSnN#c1^UQ#40wwKy1$S9Gz*2uvu6CM}#eXo0Y3Z-)=i|ekpN&3EQ{X$eovkT*({) zjOf0^Xa#{QU%;|akfl^ZtTVo7k!o~(+xdh8A(?wsk2W@{uQKst0IVUqg|z2|tM(j~ zjW3oC^O8L;$+l`zrtRU-q=~T$y+%5YUt}2jjo!ZE^NpTc2pk_inK>0d9Ul+==)_zm zml&H3?9D917vn>SwriN12SNYS6xK8|zKrpF{ZXu~T<2iJ(eLPYjWph&R|et?Qqgu? zEwU9xdrO3y4`bML3PxeP2vwe2Pt>?J){4xcAYiKB*P|X*uvm@{%)-{KTc7+hZAU14 z@+8czO?|zF)gT73PJLVV-`&3Ezk_hX<I zaYui!tzW-j^PiaQAfMm<^vQ$hi?!SC^4hcQ>;A{4qIRazrv7lML*I1k;vG{Kh8z4o4^GQFy>S$JkDFhF#jE6Hq>HKxCp7?$#P+yu_cRo`sy z7FkxJxuZ84y`u_0HGsn&HY!;yC`MaN7fP}??n`B9>XPq@g#9j~j?MVem=L~|QaVi) z-xZL+a2hyNOlUCrY9r5i?a11&vydouer#UB>QRl8ZSU z1*DZ6_2Dt0`Pd$_fm48mtMT9IO>o6=g&tHyy={Z%7x z^`vFswfw87&wT`ja3*&UaIqouR~@Vqb;EA5zGpY--EfybPHd(f|7}oo;zUK2hCBje x#@N8dvgTo|W@?t1+GQ~{`yHl6BCl&|B=TCOa?Pe<)ohsBz2W|8_Wxw*e*ktu+cW?G literal 0 HcmV?d00001 diff --git a/src/lib/adafruit_requests.py b/src/lib/adafruit_requests.py new file mode 100644 index 0000000..ce7fbc4 --- /dev/null +++ b/src/lib/adafruit_requests.py @@ -0,0 +1,669 @@ +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Requests.git" + +import errno + +# CircuitPython 6.0 does not have the bytearray.split method. +# This function emulates buf.split(needle)[0], which is the functionality +# required. +def _buffer_split0(buf, needle): + index = buf.find(needle) + if index == -1: + return buf + return buf[:index] + + +class _RawResponse: + def __init__(self, response): + self._response = response + + def read(self, size=-1): + """Read as much as available or up to size and return it in a byte string. + + Do NOT use this unless you really need to. Reusing memory with `readinto` is much better. + """ + if size == -1: + return self._response.content + return self._response.socket.recv(size) + + def readinto(self, buf): + """Read as much as available into buf or until it is full. Returns the number of bytes read + into buf.""" + return self._response._readinto(buf) # pylint: disable=protected-access + + +class _SendFailed(Exception): + """Custom exception to abort sending a request.""" + + +class OutOfRetries(Exception): + """Raised when requests has retried to make a request unsuccessfully.""" + + +class Response: + """The response from a request, contains all the headers/content""" + + # pylint: disable=too-many-instance-attributes + + encoding = None + + def __init__(self, sock, session=None): + self.socket = sock + self.encoding = "utf-8" + self._cached = None + self._headers = {} + + # _start_index and _receive_buffer are used when parsing headers. + # _receive_buffer will grow by 32 bytes everytime it is too small. + self._received_length = 0 + self._receive_buffer = bytearray(32) + self._remaining = None + self._chunked = False + + self._backwards_compatible = not hasattr(sock, "recv_into") + + http = self._readto(b" ") + if not http: + if session: + session._close_socket(self.socket) + else: + self.socket.close() + raise RuntimeError("Unable to read HTTP response.") + self.status_code = int(bytes(self._readto(b" "))) + self.reason = self._readto(b"\r\n") + self._parse_headers() + self._raw = None + self._session = session + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def _recv_into(self, buf, size=0): + if self._backwards_compatible: + size = len(buf) if size == 0 else size + b = self.socket.recv(size) + read_size = len(b) + buf[:read_size] = b + return read_size + return self.socket.recv_into(buf, size) + + @staticmethod + def _find(buf, needle, start, end): + if hasattr(buf, "find"): + return buf.find(needle, start, end) + result = -1 + i = start + while i < end: + j = 0 + while j < len(needle) and i + j < end and buf[i + j] == needle[j]: + j += 1 + if j == len(needle): + result = i + break + i += 1 + + return result + + def _readto(self, first, second=b""): + buf = self._receive_buffer + end = self._received_length + while True: + firsti = self._find(buf, first, 0, end) + secondi = -1 + if second: + secondi = self._find(buf, second, 0, end) + + i = -1 + needle_len = 0 + if firsti >= 0: + i = firsti + needle_len = len(first) + if secondi >= 0 and (firsti < 0 or secondi < firsti): + i = secondi + needle_len = len(second) + if i >= 0: + result = buf[:i] + new_start = i + needle_len + + if i + needle_len <= end: + new_end = end - new_start + buf[:new_end] = buf[new_start:end] + self._received_length = new_end + return result + + # Not found so load more. + + # If our buffer is full, then make it bigger to load more. + if end == len(buf): + new_size = len(buf) + 32 + new_buf = bytearray(new_size) + new_buf[: len(buf)] = buf + buf = new_buf + self._receive_buffer = buf + + read = self._recv_into(memoryview(buf)[end:]) + if read == 0: + self._received_length = 0 + return buf[:end] + end += read + + return b"" + + def _read_from_buffer(self, buf=None, nbytes=None): + if self._received_length == 0: + return 0 + read = self._received_length + if nbytes < read: + read = nbytes + membuf = memoryview(self._receive_buffer) + if buf: + buf[:read] = membuf[:read] + if read < self._received_length: + new_end = self._received_length - read + self._receive_buffer[:new_end] = membuf[read : self._received_length] + self._received_length = new_end + else: + self._received_length = 0 + return read + + def _readinto(self, buf): + if not self.socket: + raise RuntimeError( + "Newer Response closed this one. Use Responses immediately." + ) + + if not self._remaining: + # Consume the chunk header if need be. + if self._chunked: + # Consume trailing \r\n for chunks 2+ + if self._remaining == 0: + self._throw_away(2) + chunk_header = _buffer_split0(self._readto(b"\r\n"), b";") + http_chunk_size = int(bytes(chunk_header), 16) + if http_chunk_size == 0: + self._chunked = False + self._parse_headers() + return 0 + self._remaining = http_chunk_size + else: + return 0 + + nbytes = len(buf) + if nbytes > self._remaining: + nbytes = self._remaining + + read = self._read_from_buffer(buf, nbytes) + if read == 0: + read = self._recv_into(buf, nbytes) + self._remaining -= read + + return read + + def _throw_away(self, nbytes): + nbytes -= self._read_from_buffer(nbytes=nbytes) + + buf = self._receive_buffer + for _ in range(nbytes // len(buf)): + self._recv_into(buf) + remaining = nbytes % len(buf) + if remaining: + self._recv_into(buf, remaining) + + def close(self): + """Drain the remaining ESP socket buffers. We assume we already got what we wanted.""" + if not self.socket: + return + # Make sure we've read all of our response. + if self._cached is None: + if self._remaining and self._remaining > 0: + self._throw_away(self._remaining) + elif self._chunked: + while True: + chunk_header = _buffer_split0(self._readto(b"\r\n"), b";") + chunk_size = int(bytes(chunk_header), 16) + if chunk_size == 0: + break + self._throw_away(chunk_size + 2) + self._parse_headers() + if self._session: + self._session._free_socket(self.socket) # pylint: disable=protected-access + else: + self.socket.close() + self.socket = None + + def _parse_headers(self): + """ + Parses the header portion of an HTTP request/response from the socket. + Expects first line of HTTP request/response to have been read already. + """ + while True: + title = self._readto(b": ", b"\r\n") + if not title: + break + + content = self._readto(b"\r\n") + if title and content: + title = str(title, "utf-8") + content = str(content, "utf-8") + # Check len first so we can skip the .lower allocation most of the time. + if ( + len(title) == len("content-length") + and title.lower() == "content-length" + ): + self._remaining = int(content) + if ( + len(title) == len("transfer-encoding") + and title.lower() == "transfer-encoding" + ): + self._chunked = content.strip().lower() == "chunked" + self._headers[title] = content + + @property + def headers(self): + """ + The response headers. Does not include headers from the trailer until + the content has been read. + """ + return self._headers + + @property + def content(self): + """The HTTP content direct from the socket, as bytes""" + if self._cached is not None: + if isinstance(self._cached, bytes): + return self._cached + raise RuntimeError("Cannot access content after getting text or json") + + self._cached = b"".join(self.iter_content(chunk_size=32)) + return self._cached + + @property + def text(self): + """The HTTP content, encoded into a string according to the HTTP + header encoding""" + if self._cached is not None: + if isinstance(self._cached, str): + return self._cached + raise RuntimeError("Cannot access text after getting content or json") + self._cached = str(self.content, self.encoding) + return self._cached + + def json(self): + """The HTTP content, parsed into a json dictionary""" + # pylint: disable=import-outside-toplevel + import json + + # The cached JSON will be a list or dictionary. + if self._cached: + if isinstance(self._cached, (list, dict)): + return self._cached + raise RuntimeError("Cannot access json after getting text or content") + if not self._raw: + self._raw = _RawResponse(self) + + try: + obj = json.load(self._raw) + except OSError: + # <5.3.1 doesn't piecemeal load json from any object with readinto so load the whole + # string. + obj = json.loads(self._raw.read()) + if not self._cached: + self._cached = obj + self.close() + return obj + + 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") + + b = bytearray(chunk_size) + while True: + size = self._readinto(b) + if size == 0: + break + if size < chunk_size: + chunk = bytes(memoryview(b)[:size]) + else: + chunk = bytes(b) + yield chunk + self.close() + + +class Session: + """HTTP session that shares sockets and ssl context.""" + + def __init__(self, socket_pool, ssl_context=None): + self._socket_pool = socket_pool + self._ssl_context = ssl_context + # Hang onto open sockets so that we can reuse them. + self._open_sockets = {} + self._socket_free = {} + self._last_response = None + + def _free_socket(self, socket): + if socket not in self._open_sockets.values(): + raise RuntimeError("Socket not from session") + self._socket_free[socket] = True + + def _close_socket(self, sock): + sock.close() + del self._socket_free[sock] + key = None + for k in self._open_sockets: + if self._open_sockets[k] == sock: + key = k + break + if key: + del self._open_sockets[key] + + def _free_sockets(self): + free_sockets = [] + for sock in self._socket_free: + if self._socket_free[sock]: + free_sockets.append(sock) + for sock in free_sockets: + self._close_socket(sock) + + def _get_socket(self, host, port, proto, *, timeout=1): + # pylint: disable=too-many-branches + key = (host, port, proto) + if key in self._open_sockets: + sock = self._open_sockets[key] + if self._socket_free[sock]: + self._socket_free[sock] = False + return sock + if proto == "https:" and not self._ssl_context: + raise RuntimeError( + "ssl_context must be set before using adafruit_requests for https" + ) + addr_info = self._socket_pool.getaddrinfo( + host, port, 0, self._socket_pool.SOCK_STREAM + )[0] + retry_count = 0 + sock = None + while retry_count < 5 and sock is None: + if retry_count > 0: + if any(self._socket_free.items()): + self._free_sockets() + else: + raise RuntimeError("Sending request failed") + retry_count += 1 + + try: + sock = self._socket_pool.socket( + addr_info[0], addr_info[1], addr_info[2] + ) + except OSError: + continue + except RuntimeError: + continue + + connect_host = addr_info[-1][0] + if proto == "https:": + sock = self._ssl_context.wrap_socket(sock, server_hostname=host) + connect_host = host + sock.settimeout(timeout) # socket read timeout + + try: + sock.connect((connect_host, port)) + except MemoryError: + sock.close() + sock = None + except OSError: + sock.close() + sock = None + + if sock is None: + raise RuntimeError("Repeated socket failures") + + self._open_sockets[key] = sock + self._socket_free[sock] = False + return sock + + @staticmethod + def _send(socket, data): + total_sent = 0 + while total_sent < len(data): + # ESP32SPI sockets raise a RuntimeError when unable to send. + try: + sent = socket.send(data[total_sent:]) + except RuntimeError: + sent = 0 + if sent is None: + sent = len(data) + if sent == 0: + raise _SendFailed() + total_sent += sent + + def _send_request(self, socket, host, method, path, headers, data, json): + # pylint: disable=too-many-arguments + self._send(socket, bytes(method, "utf-8")) + self._send(socket, b" /") + self._send(socket, bytes(path, "utf-8")) + self._send(socket, b" HTTP/1.1\r\n") + if "Host" not in headers: + self._send(socket, b"Host: ") + self._send(socket, bytes(host, "utf-8")) + self._send(socket, b"\r\n") + if "User-Agent" not in headers: + self._send(socket, b"User-Agent: Adafruit CircuitPython\r\n") + # Iterate over keys to avoid tuple alloc + for k in headers: + self._send(socket, k.encode()) + self._send(socket, b": ") + self._send(socket, headers[k].encode()) + self._send(socket, b"\r\n") + if json is not None: + assert data is None + # pylint: disable=import-outside-toplevel + try: + import json as json_module + except ImportError: + import ujson as json_module + data = json_module.dumps(json) + self._send(socket, b"Content-Type: application/json\r\n") + if data: + if isinstance(data, dict): + self._send( + socket, b"Content-Type: application/x-www-form-urlencoded\r\n" + ) + _post_data = "" + for k in data: + _post_data = "{}&{}={}".format(_post_data, k, data[k]) + data = _post_data[1:] + self._send(socket, b"Content-Length: %d\r\n" % len(data)) + self._send(socket, b"\r\n") + if data: + if isinstance(data, bytearray): + self._send(socket, bytes(data)) + else: + self._send(socket, bytes(data, "utf-8")) + + # pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals + def request( + self, method, url, data=None, json=None, headers=None, stream=False, timeout=60 + ): + """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 + """ + 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) + + if self._last_response: + self._last_response.close() + self._last_response = None + + # We may fail to send the request if the socket we got is closed already. So, try a second + # time in that case. + retry_count = 0 + while retry_count < 2: + retry_count += 1 + socket = self._get_socket(host, port, proto, timeout=timeout) + ok = True + try: + self._send_request(socket, host, method, path, headers, data, json) + except (_SendFailed, OSError): + ok = False + if ok: + # Read the H of "HTTP/1.1" to make sure the socket is alive. send can appear to work + # even when the socket is closed. + if hasattr(socket, "recv"): + result = socket.recv(1) + else: + result = bytearray(1) + try: + socket.recv_into(result) + except OSError: + pass + if result == b"H": + # Things seem to be ok so break with socket set. + break + self._close_socket(socket) + socket = None + + if not socket: + raise OutOfRetries("Repeated socket failures") + + resp = Response(socket, self) # our response + if "location" in resp.headers and 300 <= resp.status_code <= 399: + raise NotImplementedError("Redirects not yet supported") + + self._last_response = resp + return resp + + def head(self, url, **kw): + """Send HTTP HEAD request""" + return self.request("HEAD", url, **kw) + + def get(self, url, **kw): + """Send HTTP GET request""" + return self.request("GET", url, **kw) + + def post(self, url, **kw): + """Send HTTP POST request""" + return self.request("POST", url, **kw) + + def put(self, url, **kw): + """Send HTTP PUT request""" + return self.request("PUT", url, **kw) + + def patch(self, url, **kw): + """Send HTTP PATCH request""" + return self.request("PATCH", url, **kw) + + def delete(self, url, **kw): + """Send HTTP DELETE request""" + return self.request("DELETE", url, **kw) + + +# Backwards compatible API: + +_default_session = None # pylint: disable=invalid-name + + +class _FakeSSLSocket: + def __init__(self, socket, tls_mode): + self._socket = socket + self._mode = tls_mode + self.settimeout = socket.settimeout + self.send = socket.send + self.recv = socket.recv + self.close = socket.close + + def connect(self, address): + """connect wrapper to add non-standard mode parameter""" + try: + return self._socket.connect(address, self._mode) + except RuntimeError as error: + raise OSError(errno.ENOMEM) from error + + +class _FakeSSLContext: + def __init__(self, iface): + self._iface = iface + + def wrap_socket(self, socket, server_hostname=None): + """Return the same socket""" + # pylint: disable=unused-argument + return _FakeSSLSocket(socket, self._iface.TLS_MODE) + + +def set_socket(sock, iface=None): + """Legacy API for setting the socket and network interface. Use a `Session` instead.""" + global _default_session # pylint: disable=global-statement,invalid-name + if not iface: + # pylint: disable=protected-access + _default_session = Session(sock, _FakeSSLContext(sock._the_interface)) + else: + _default_session = Session(sock, _FakeSSLContext(iface)) + sock.set_interface(iface) + + +def request(method, url, data=None, json=None, headers=None, stream=False, timeout=0.1): + """Send HTTP request""" + # pylint: disable=too-many-arguments + _default_session.request( + method, + url, + data=data, + json=json, + headers=headers, + stream=stream, + timeout=timeout, + ) + + +def head(url, **kw): + """Send HTTP HEAD request""" + return _default_session.request("HEAD", url, **kw) + + +def get(url, **kw): + """Send HTTP GET request""" + return _default_session.request("GET", url, **kw) + + +def post(url, **kw): + """Send HTTP POST request""" + return _default_session.request("POST", url, **kw) + + +def put(url, **kw): + """Send HTTP PUT request""" + return _default_session.request("PUT", url, **kw) + + +def patch(url, **kw): + """Send HTTP PATCH request""" + return _default_session.request("PATCH", url, **kw) + + +def delete(url, **kw): + """Send HTTP DELETE request""" + return _default_session.request("DELETE", url, **kw)