Skip to content

Commit a984f77

Browse files
authored
Merge pull request #17 from ladyada/master
new requests interface
2 parents fb4f900 + 61f3246 commit a984f77

11 files changed

+534
-185
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ install:
2424
- pip install circuitpython-build-tools Sphinx sphinx-rtd-theme
2525
- pip install --force-reinstall pylint==1.9.2
2626
script:
27-
- pylint adafruit_espatcontrol.py
27+
- pylint adafruit_espatcontrol/*.py
2828
- ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace examples/*.py)
2929
- circuitpython-build-bundles --filename_prefix adafruit-circuitpython-espatcontrol --library_location .
3030
- cd docs && sphinx-build -E -W -b html . _build/html && cd ..

adafruit_espatcontrol.py renamed to adafruit_espatcontrol/adafruit_espatcontrol.py

Lines changed: 25 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
# THE SOFTWARE.
2222
"""
23-
`adafruit_espatcontrol`
23+
`adafruit_espatcontrol.adafruit_espatcontrol`
2424
====================================================
2525
2626
Use the ESP AT command sent to communicate with the Interwebs.
@@ -60,20 +60,6 @@ class OKError(Exception):
6060
"""The exception thrown when we didn't get acknowledgement to an AT command"""
6161
pass
6262

63-
class ESP_ATcontrol_socket:
64-
"""A 'socket' compatible interface thru the ESP AT command set"""
65-
def __init__(self, esp):
66-
self._esp = esp
67-
68-
def getaddrinfo(self, host, port, # pylint: disable=too-many-arguments
69-
family=0, socktype=0, proto=0, flags=0): # pylint: disable=unused-argument
70-
"""Given a hostname and a port name, return a 'socket.getaddrinfo'
71-
compatible list of tuples. Honestly, we ignore anything but host & port"""
72-
if not isinstance(port, int):
73-
raise RuntimeError("port must be an integer")
74-
ipaddr = self._esp.nslookup(host)
75-
return [(family, socktype, proto, '', (ipaddr, port))]
76-
7763
class ESP_ATcontrol:
7864
"""A wrapper for AT commands to a connected ESP8266 or ESP32 module to do
7965
some very basic internetting. The ESP module must be pre-programmed with
@@ -147,55 +133,18 @@ def begin(self):
147133
except OKError:
148134
pass #retry
149135

150-
def request_url(self, url, ssl=False, request_type="GET"):
151-
"""Send an HTTP request to the URL. If the URL starts with https://
152-
we will force SSL and use port 443. Otherwise, you can select whether
153-
you want ssl by passing in a flag."""
154-
if url.startswith("https://"):
155-
ssl = True
156-
url = url[8:]
157-
if url.startswith("http://"):
158-
url = url[7:]
159-
domain, path = url.split('/', 1)
160-
path = '/'+path
161-
port = 80
162-
conntype = self.TYPE_TCP
163-
if ssl:
164-
conntype = self.TYPE_SSL
165-
port = 443
166-
if not self.socket_connect(conntype, domain, port, keepalive=10, retries=3):
167-
raise RuntimeError("Failed to connect to host")
168-
request = request_type+" "+path+" HTTP/1.1\r\n"
169-
request += "Host: "+domain+"\r\n"
170-
request += "User-Agent: "+self.USER_AGENT+"\r\n"
171-
request += "\r\n"
172-
try:
173-
self.socket_send(bytes(request, 'utf-8'))
174-
except RuntimeError:
175-
raise
176-
177-
reply = self.socket_receive(timeout=3).split(b'\r\n')
178-
if self._debug:
179-
print(reply)
180-
try:
181-
headerbreak = reply.index(b'')
182-
except ValueError:
183-
raise RuntimeError("Reponse wasn't valid HTML")
184-
header = reply[0:headerbreak]
185-
data = b'\r\n'.join(reply[headerbreak+1:]) # put back the way it was
186-
self.socket_disconnect()
187-
return (header, data)
188-
189136
def connect(self, settings):
190137
"""Repeatedly try to connect to an access point with the details in
191138
the passed in 'settings' dictionary. Be sure 'ssid' and 'password' are
192139
defined in the settings dict! If 'timezone' is set, we'll also configure
193140
SNTP"""
194141
# Connect to WiFi if not already
142+
retries = 3
195143
while True:
196144
try:
197-
if not self._initialized:
145+
if not self._initialized or retries == 0:
198146
self.begin()
147+
retries = 3
199148
AP = self.remote_AP # pylint: disable=invalid-name
200149
print("Connected to", AP[0])
201150
if AP[0] != settings['ssid']:
@@ -210,6 +159,7 @@ def connect(self, settings):
210159
return # yay!
211160
except (RuntimeError, OKError) as exp:
212161
print("Failed to connect, retrying\n", exp)
162+
retries -= 1
213163
continue
214164

215165
# *************************** SOCKET SETUP ****************************
@@ -223,10 +173,6 @@ def cipmux(self):
223173
return int(reply[8:])
224174
raise RuntimeError("Bad response to CIPMUX?")
225175

226-
def socket(self):
227-
"""Create a 'socket' object"""
228-
return ESP_ATcontrol_socket(self)
229-
230176
def socket_connect(self, conntype, remote, remote_port, *, keepalive=10, retries=1):
231177
"""Open a socket. conntype can be TYPE_TCP, TYPE_UDP, or TYPE_SSL. Remote
232178
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):
283229
return True
284230

285231
def socket_receive(self, timeout=5):
286-
# pylint: disable=too-many-nested-blocks
232+
# pylint: disable=too-many-nested-blocks, too-many-branches
287233
"""Check for incoming data over the open socket, returns bytes"""
288234
incoming_bytes = None
289-
bundle = b''
235+
bundle = []
236+
toread = 0
290237
gc.collect()
291238
i = 0 # index into our internal packet
292239
stamp = time.monotonic()
@@ -312,6 +259,9 @@ def socket_receive(self, timeout=5):
312259
except ValueError:
313260
raise RuntimeError("Parsing error during receive", ipd)
314261
i = 0 # reset the input buffer now that we know the size
262+
elif i > 20:
263+
i = 0 # Hmm we somehow didnt get a proper +IPD packet? start over
264+
315265
else:
316266
self.hw_flow(False) # stop the flow
317267
# read as much as we can!
@@ -322,11 +272,22 @@ def socket_receive(self, timeout=5):
322272
if i == incoming_bytes:
323273
#print(self._ipdpacket[0:i])
324274
gc.collect()
325-
bundle += self._ipdpacket[0:i]
275+
bundle.append(self._ipdpacket[0:i])
276+
gc.collect()
326277
i = incoming_bytes = 0
327278
else: # no data waiting
328279
self.hw_flow(True) # start the floooow
329-
return bundle
280+
totalsize = sum([len(x) for x in bundle])
281+
ret = bytearray(totalsize)
282+
i = 0
283+
for x in bundle:
284+
for char in x:
285+
ret[i] = char
286+
i += 1
287+
for x in bundle:
288+
del x
289+
gc.collect()
290+
return ret
330291

331292
def socket_disconnect(self):
332293
"""Close any open socket, if there is one"""
@@ -371,6 +332,7 @@ def is_connected(self):
371332
self.begin()
372333
try:
373334
self.echo(False)
335+
self.baudrate = self.baudrate
374336
stat = self.status
375337
if stat in (self.STATUS_APCONNECTED,
376338
self.STATUS_SOCKETOPEN,
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"""
2+
adapted from https://github.com/micropython/micropython-lib/tree/master/urequests
3+
4+
micropython-lib consists of multiple modules from different sources and
5+
authors. Each module comes under its own licensing terms. Short name of
6+
a license can be found in a file within a module directory (usually
7+
metadata.txt or setup.py). Complete text of each license used is provided
8+
at https://github.com/micropython/micropython-lib/blob/master/LICENSE
9+
10+
author='Paul Sokolovsky'
11+
license='MIT'
12+
"""
13+
14+
# pylint: disable=no-name-in-module
15+
16+
import gc
17+
import adafruit_espatcontrol.adafruit_espatcontrol_socket as socket
18+
19+
_the_interface = None # pylint: disable=invalid-name
20+
def set_interface(iface):
21+
"""Helper to set the global internet interface"""
22+
global _the_interface # pylint: disable=invalid-name, global-statement
23+
_the_interface = iface
24+
socket.set_interface(iface)
25+
26+
class Response:
27+
"""The response from a request, contains all the headers/content"""
28+
headers = {}
29+
encoding = None
30+
31+
def __init__(self, f):
32+
self.raw = f
33+
self.encoding = "utf-8"
34+
self._cached = None
35+
self.status_code = None
36+
self.reason = None
37+
38+
def close(self):
39+
"""Close, delete and collect the response data"""
40+
if self.raw:
41+
self.raw.close()
42+
del self.raw
43+
del self._cached
44+
gc.collect()
45+
46+
@property
47+
def content(self):
48+
"""The HTTP content direct from the socket, as bytes"""
49+
if self._cached is None:
50+
try:
51+
self._cached = self.raw.read()
52+
finally:
53+
self.raw.close()
54+
self.raw = None
55+
return self._cached
56+
57+
@property
58+
def text(self):
59+
"""The HTTP content, encoded into a string according to the HTTP
60+
header encoding"""
61+
return str(self.content, self.encoding)
62+
63+
def json(self):
64+
"""The HTTP content, parsed into a json dictionary"""
65+
import ujson
66+
return ujson.loads(self.content)
67+
68+
69+
# pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
70+
def request(method, url, data=None, json=None, headers=None, stream=None):
71+
"""Perform an HTTP request to the given url which we will parse to determine
72+
whether to use SSL ('https://') or not. We can also send some provided 'data'
73+
or a json dictionary which we will stringify. 'headers' is optional HTTP headers
74+
sent along. 'stream' is unused in this implementation"""
75+
global _the_interface # pylint: disable=global-statement, invalid-name
76+
77+
if not headers:
78+
headers = {}
79+
80+
try:
81+
proto, dummy, host, path = url.split("/", 3)
82+
except ValueError:
83+
proto, dummy, host = url.split("/", 2)
84+
path = ""
85+
if proto == "http:":
86+
port = 80
87+
elif proto == "https:":
88+
port = 443
89+
else:
90+
raise ValueError("Unsupported protocol: " + proto)
91+
92+
if ":" in host:
93+
host, port = host.split(":", 1)
94+
port = int(port)
95+
96+
addr_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0]
97+
sock = socket.socket(addr_info[0], addr_info[1], addr_info[2])
98+
resp = Response(sock) # our response
99+
100+
try:
101+
conntype = _the_interface.TYPE_TCP
102+
if proto == "https:":
103+
conntype = _the_interface.TYPE_SSL
104+
sock.connect(addr_info[-1], conntype)
105+
sock.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
106+
if "Host" not in headers:
107+
sock.write(b"Host: %s\r\n" % host)
108+
if "User-Agent" not in headers:
109+
sock.write(b"User-Agent: Adafruit CircuitPython\r\n")
110+
# Iterate over keys to avoid tuple alloc
111+
for k in headers:
112+
sock.write(k)
113+
sock.write(b": ")
114+
sock.write(headers[k])
115+
sock.write(b"\r\n")
116+
if json is not None:
117+
assert data is None
118+
import ujson
119+
data = ujson.dumps(json)
120+
sock.write(b"Content-Type: application/json\r\n")
121+
if data:
122+
sock.write(b"Content-Length: %d\r\n" % len(data))
123+
sock.write(b"\r\n")
124+
if data:
125+
sock.write(data)
126+
127+
line = sock.readline()
128+
#print(line)
129+
line = line.split(None, 2)
130+
status = int(line[1])
131+
reason = ""
132+
if len(line) > 2:
133+
reason = line[2].rstrip()
134+
while True:
135+
line = sock.readline()
136+
if not line or line == b"\r\n":
137+
break
138+
139+
#print(line)
140+
title, content = line.split(b': ', 1)
141+
if title and content:
142+
title = str(title.lower(), 'utf-8')
143+
content = str(content, 'utf-8')
144+
resp.headers[title] = content
145+
146+
if line.startswith(b"Transfer-Encoding:"):
147+
if b"chunked" in line:
148+
raise ValueError("Unsupported " + line)
149+
elif line.startswith(b"Location:") and not 200 <= status <= 299:
150+
raise NotImplementedError("Redirects not yet supported")
151+
152+
except OSError:
153+
sock.close()
154+
raise
155+
156+
resp.status_code = status
157+
resp.reason = reason
158+
return resp
159+
# pylint: enable=too-many-branches, too-many-statements, unused-argument
160+
# pylint: enable=too-many-arguments, too-many-locals
161+
162+
def head(url, **kw):
163+
"""Send HTTP HEAD request"""
164+
return request("HEAD", url, **kw)
165+
166+
def get(url, **kw):
167+
"""Send HTTP GET request"""
168+
return request("GET", url, **kw)
169+
170+
def post(url, **kw):
171+
"""Send HTTP POST request"""
172+
return request("POST", url, **kw)
173+
174+
def put(url, **kw):
175+
"""Send HTTP PUT request"""
176+
return request("PUT", url, **kw)
177+
178+
def patch(url, **kw):
179+
"""Send HTTP PATCH request"""
180+
return request("PATCH", url, **kw)
181+
182+
def delete(url, **kw):
183+
"""Send HTTP DELETE request"""
184+
return request("DELETE", url, **kw)

0 commit comments

Comments
 (0)