From 9f07be90da3e2405ffcd2fa93ead2451574e5086 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 29 Sep 2021 12:16:54 +0100 Subject: [PATCH 1/7] PicoWireless: move HTTP code to ppwhttp library Creates a new ppwhttp library to hide the implementation detail of HTTP clients/servers from the examples. Adds a new example - plasma_ws2812_http.py - showing how to expand rgb_http.py to use a WS2812 pixel strip. Adds "secrets.py" and moves WIFI connection information there. ppwhttp will throw an error if it's missing. --- micropython/examples/.gitignore | 1 + .../examples/pico_wireless/cheerlights.py | 105 +------- .../pico_wireless/plasma_ws2812_http.py | 82 ++++++ micropython/examples/pico_wireless/ppwhttp.py | 240 ++++++++++++++++++ .../examples/pico_wireless/rgb_http.py | 141 +--------- micropython/examples/pico_wireless/secrets.py | 2 + 6 files changed, 349 insertions(+), 222 deletions(-) create mode 100644 micropython/examples/.gitignore create mode 100644 micropython/examples/pico_wireless/plasma_ws2812_http.py create mode 100644 micropython/examples/pico_wireless/ppwhttp.py create mode 100644 micropython/examples/pico_wireless/secrets.py diff --git a/micropython/examples/.gitignore b/micropython/examples/.gitignore new file mode 100644 index 00000000..ef418f5f --- /dev/null +++ b/micropython/examples/.gitignore @@ -0,0 +1 @@ +secrets.py diff --git a/micropython/examples/pico_wireless/cheerlights.py b/micropython/examples/pico_wireless/cheerlights.py index b49482c9..ee198156 100644 --- a/micropython/examples/pico_wireless/cheerlights.py +++ b/micropython/examples/pico_wireless/cheerlights.py @@ -1,106 +1,25 @@ import time -import picowireless from micropython import const -WIFI_SSID = "Your SSID here!" -WIFI_PASS = "Your PSK here!" +try: + import ppwhttp +except ImportError: + raise RuntimeError("Cannot find ppwhttp. Have you copied ppwhttp.py to your Pico?") -CLOUDFLARE_DNS = (1, 1, 1, 1) -GOOGLE_DNS = (8, 8, 8, 8) -USE_DNS = CLOUDFLARE_DNS -TCP_MODE = const(0) -HTTP_REQUEST_DELAY = const(30) -HTTP_PORT = 80 +HTTP_REQUEST_DELAY = const(60) +HTTP_REQUEST_PORT = const(80) HTTP_REQUEST_HOST = "api.thingspeak.com" HTTP_REQUEST_PATH = "/channels/1417/field/2/last.txt" -def connect(host_address, port, client_sock, timeout=1000): - picowireless.client_start(host_address, port, client_sock, TCP_MODE) - - t_start = time.time() - timeout /= 1000.0 - - while time.time() - t_start < timeout: - state = picowireless.get_client_state(client_sock) - if state == 4: - return True - time.sleep(1.0) - - return False - - -def http_request(client_sock, host_address, port, request_host, request_path, handler, timeout=5000): - print("Connecting to {1}.{2}.{3}.{4}:{0}...".format(port, *host_address)) - if not connect(host_address, port, client_sock): - print("Connection failed!") - return False - print("Connected!") - - http_request = """GET {} HTTP/1.1 -Host: {} -Connection: close - -""".format(request_path, request_host).replace("\n", "\r\n") - - picowireless.send_data(client_sock, http_request) - - t_start = time.time() - - while True: - if time.time() - t_start > timeout: - picowireless.client_stop(client_sock) - print("HTTP request to {}:{} timed out...".format(host_address, port)) - return False - - avail_length = picowireless.avail_data(client_sock) - if avail_length > 0: - break - - print("Got response: {} bytes".format(avail_length)) - - response = b"" - - while len(response) < avail_length: - data = picowireless.get_data_buf(client_sock) - response += data - - response = response.decode("utf-8") - - head, body = response.split("\r\n\r\n", 1) - dhead = {} - - for line in head.split("\r\n")[1:]: - key, value = line.split(": ", 1) - dhead[key] = value - - handler(dhead, body) - - picowireless.client_stop(client_sock) - - -picowireless.init() - -print("Connecting to {}...".format(WIFI_SSID)) -picowireless.wifi_set_passphrase(WIFI_SSID, WIFI_PASS) - -while True: - if picowireless.get_connection_status() == 3: - break -print("Connected!") +ppwhttp.start_wifi() +ppwhttp.set_dns(ppwhttp.GOOGLE_DNS) # Get our own local IP! -my_ip = picowireless.get_ip_address() +my_ip = ppwhttp.get_ip_address() print("Local IP: {}.{}.{}.{}".format(*my_ip)) -# Resolve and cache the IP address -picowireless.set_dns(USE_DNS) -http_address = picowireless.get_host_by_name(HTTP_REQUEST_HOST) -print("Resolved {} to {}.{}.{}.{}".format(HTTP_REQUEST_HOST, *http_address)) - -client_sock = picowireless.get_socket() - def handler(head, body): if head["Status"] == "200 OK": @@ -108,12 +27,12 @@ def handler(head, body): r = int(color[0:2], 16) g = int(color[2:4], 16) b = int(color[4:6], 16) - picowireless.set_led(r, g, b) + ppwhttp.set_led(r, g, b) print("Set LED to {} {} {}".format(r, g, b)) else: print("Error: {}".format(head["Status"])) while True: - http_request(client_sock, http_address, HTTP_PORT, HTTP_REQUEST_HOST, HTTP_REQUEST_PATH, handler) - time.sleep(60.0) + ppwhttp.http_request(HTTP_REQUEST_HOST, HTTP_REQUEST_PORT, HTTP_REQUEST_HOST, HTTP_REQUEST_PATH, handler) + time.sleep(HTTP_REQUEST_DELAY) diff --git a/micropython/examples/pico_wireless/plasma_ws2812_http.py b/micropython/examples/pico_wireless/plasma_ws2812_http.py new file mode 100644 index 00000000..e214ba74 --- /dev/null +++ b/micropython/examples/pico_wireless/plasma_ws2812_http.py @@ -0,0 +1,82 @@ +import time +import plasma + +try: + import ppwhttp +except ImportError: + raise RuntimeError("Cannot find ppwhttp. Have you copied ppwhttp.py to your Pico?") + +""" +This example uses the Plasma WS2812 LED library to drive a string of LEDs alongside the built-in RGB LED. +You should wire your LEDs to VBUS/GND and connect the data pin to pin 27 (unused by Pico Wireless). +""" + +NUM_LEDS = 30 # Number of connected LEDs +LED_PIN = 27 # LED data pin (27 is unused by Pico Wireless) +LED_PIO = 0 # Hardware PIO (0 or 1) +LED_SM = 0 # PIO State-Machine (0 to 3) + + +r = 0 +g = 0 +b = 0 + +led_strip = plasma.WS2812(NUM_LEDS, LED_PIO, LED_SM, LED_PIN) + + +# Edit your routes here +# Nothing fancy is supported, just plain ol' URLs and GET/POST methods +@ppwhttp.route("/", methods=["GET", "POST"]) +def get_home(method, url, data=None): + if method == "POST": + global r, g, b + r = int(data.get("r", 0)) + g = int(data.get("g", 0)) + b = int(data.get("b", 0)) + ppwhttp.set_led(r, g, b) + for i in range(NUM_LEDS): + led_strip.set_rgb(i, r, g, b) + print("Set LED to {} {} {}".format(r, g, b)) + + return """
+ + + + +
""".format(r=r, g=g, b=b) + + +@ppwhttp.route("/test", methods="GET") +def get_test(method, url): + return "Hello World!" + + +ppwhttp.start_wifi() + +led_strip.start() + +server_sock = ppwhttp.start_server() +while True: + ppwhttp.handle_http_request(server_sock) + time.sleep(0.01) + + +# Whoa there! Did you know you could run the server polling loop +# on Pico's *other* core!? Here's how: +# +# import _thread +# +# def server_loop_forever(): +# # Start a server and continuously poll for HTTP requests +# server_sock = ppwhttp.start_server() +# while True: +# ppwhttp.handle_http_request(server_sock) +# time.sleep(0.01) +# +# Handle the server polling loop on the other core! +# _thread.start_new_thread(server_loop_forever, ()) +# +# # Your very own main loop for fun and profit! +# while True: +# print("Colour: {} {} {}".format(r, g, b)) +# time.sleep(5.0) diff --git a/micropython/examples/pico_wireless/ppwhttp.py b/micropython/examples/pico_wireless/ppwhttp.py new file mode 100644 index 00000000..2d022e6b --- /dev/null +++ b/micropython/examples/pico_wireless/ppwhttp.py @@ -0,0 +1,240 @@ +"""Pimoroni Pico Wireless HTTP + +A super-simple HTTP server library for Pico Wireless. +""" +import time +import picowireless + +from micropython import const + +try: + from secrets import WIFI_SSID, WIFI_PASS +except ImportError: + WIFI_SSID = None + WIFI_PASS = None + + +TCP_CLOSED = const(0) +TCP_LISTEN = const(1) + +CLOUDFLARE_DNS = (1, 1, 1, 1) +GOOGLE_DNS = (8, 8, 8, 8) + +DEFAULT_HTTP_PORT = const(80) + + +routes = {} +sockets = [] +hosts = {} + + +def set_led(r, g, b): + """Set """ + picowireless.set_led(r, g, b) + + +def get_socket(force_new=False): + global sockets + if force_new or len(sockets) == 0: + socket = picowireless.get_socket() + sockets.append(socket) + return socket + return sockets[0] + + +def get_ip_address(): + return picowireless.get_ip_address() + + +def set_dns(dns): + picowireless.set_dns(dns) + + +def get_host_by_name(hostname, no_cache=False): + # Already an IP + if type(hostname) is tuple and len(hostname) == 4: + return hostname + + # Get from cached hosts + if hostname in hosts and not no_cache: + return hosts[hostname] + + ip = picowireless.get_host_by_name(hostname) + hosts[hostname] = ip + return ip + + +def start_wifi(wifi_ssid=WIFI_SSID, wifi_pass=WIFI_PASS): + if wifi_ssid is None or wifi_pass is None: + raise RuntimeError("WiFi SSID/PASS required. Set them in secrets.py and copy it to your Pico, or pass them as arguments.") + picowireless.init() + + print("Connecting to {}...".format(wifi_ssid)) + picowireless.wifi_set_passphrase(wifi_ssid, wifi_pass) + + while True: + if picowireless.get_connection_status() == 3: + break + print("Connected!") + + +def start_server(http_port=DEFAULT_HTTP_PORT, timeout=5000): + my_ip = picowireless.get_ip_address() + print("Starting server...") + server_sock = picowireless.get_socket() + picowireless.server_start(http_port, server_sock, 0) + + t_start = time.ticks_ms() + + while time.ticks_ms() - t_start < timeout: + state = picowireless.get_server_state(server_sock) + if state == TCP_LISTEN: + print("Server listening on {1}.{2}.{3}.{4}:{0}".format(http_port, *my_ip)) + return server_sock + + return None + + +def connect_to_server(host_address, port, client_sock, timeout=5000): + picowireless.client_start(host_address, port, client_sock, TCP_CLOSED) + + t_start = time.ticks_ms() + + while time.ticks_ms() - t_start < timeout: + state = picowireless.get_client_state(client_sock) + if state == 4: + return True + time.sleep(1.0) + + return False + + +def http_request(host_address, port, request_host, request_path, handler, timeout=5000, client_sock=None): + if client_sock is None: + client_sock = get_socket() + + host_address = get_host_by_name(host_address) + + print("Connecting to {1}.{2}.{3}.{4}:{0}...".format(port, *host_address)) + if not connect_to_server(host_address, port, client_sock): + print("Connection failed!") + return False + print("Connected!") + + http_request = """GET {} HTTP/1.1 +Host: {} +Connection: close + +""".format(request_path, request_host).replace("\n", "\r\n") + + picowireless.send_data(client_sock, http_request) + + t_start = time.ticks_ms() + + while True: + if time.ticks_ms() - t_start > timeout: + picowireless.client_stop(client_sock) + print("HTTP request to {}:{} timed out...".format(host_address, port)) + return False + + avail_length = picowireless.avail_data(client_sock) + if avail_length > 0: + break + + print("Got response: {} bytes".format(avail_length)) + + response = b"" + + while len(response) < avail_length: + data = picowireless.get_data_buf(client_sock) + response += data + + response = response.decode("utf-8") + + head, body = response.split("\r\n\r\n", 1) + dhead = {} + + for line in head.split("\r\n")[1:]: + key, value = line.split(": ", 1) + dhead[key] = value + + handler(dhead, body) + + picowireless.client_stop(client_sock) + + +def handle_http_request(server_sock, timeout=5000): + t_start = time.ticks_ms() + + client_sock = picowireless.avail_server(server_sock) + if client_sock in [server_sock, 255, -1]: + return False + + print("Client connected!") + + avail_length = picowireless.avail_data(client_sock) + if avail_length == 0: + picowireless.client_stop(client_sock) + return False + + request = b"" + + while len(request) < avail_length: + data = picowireless.get_data_buf(client_sock) + request += data + if time.ticks_ms() - t_start > timeout: + print("Client timed out getting data!") + picowireless.client_stop(client_sock) + return False + + request = request.decode("utf-8") + + if len(request) > 0: + head, body = request.split("\r\n\r\n", 1) + dhead = {} + + for line in head.split("\r\n")[1:]: + key, value = line.split(": ", 1) + dhead[key] = value + + method, url, _ = head.split("\r\n", 1)[0].split(" ") + + print("Serving {} on {}...".format(method, url)) + + response = None + + # Dispatch the request to the relevant route + if url in routes and method in routes[url] and callable(routes[url][method]): + if method == "POST": + data = {} + for var in body.split("&"): + key, value = var.split("=") + data[key] = value + response = routes[url][method](method, url, data) + else: + response = routes[url][method](method, url) + + if response is not None: + response = "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: text/html\r\n\r\n".format(len(response)) + response + picowireless.send_data(client_sock, response) + picowireless.client_stop(client_sock) + print("Success! Sending 200 OK") + return True + else: + picowireless.send_data(client_sock, "HTTP/1.1 501 Not Implemented\r\nContent-Length: 19\r\n\r\n501 Not Implemented") + picowireless.client_stop(client_sock) + print("Unhandled Request! Sending 501 OK") + return False + + +def route(url, methods="GET"): + if type(methods) is str: + methods = [methods] + + def decorate(handler): + for method in methods: + if url not in routes: + routes[url] = {} + routes[url][method] = handler + + return decorate diff --git a/micropython/examples/pico_wireless/rgb_http.py b/micropython/examples/pico_wireless/rgb_http.py index 60db461c..1823374f 100644 --- a/micropython/examples/pico_wireless/rgb_http.py +++ b/micropython/examples/pico_wireless/rgb_http.py @@ -1,143 +1,26 @@ import time -import picowireless -from micropython import const -WIFI_SSID = "your SSID here!" -WIFI_PASS = "Your PSK here!" +try: + import ppwhttp +except ImportError: + raise RuntimeError("Cannot find ppwhttp. Have you copied ppwhttp.py to your Pico?") -TCP_CLOSED = 0 -TCP_LISTEN = 1 - -CLOUDFLARE_DNS = (1, 1, 1, 1) -GOOGLE_DNS = (8, 8, 8, 8) - -TCP_MODE = const(0) -HTTP_REQUEST_DELAY = const(30) -HTTP_PORT = 80 - -routes = {} r = 0 g = 0 b = 0 -def start_wifi(): - picowireless.init() - - print("Connecting to {}...".format(WIFI_SSID)) - picowireless.wifi_set_passphrase(WIFI_SSID, WIFI_PASS) - - while True: - if picowireless.get_connection_status() == 3: - break - print("Connected!") - - -def start_server(http_port, timeout=1.0): - my_ip = picowireless.get_ip_address() - print("Starting server...") - server_sock = picowireless.get_socket() - picowireless.server_start(http_port, server_sock, 0) - - t_start = time.time() - - while time.time() - t_start < timeout: - state = picowireless.get_server_state(server_sock) - if state == TCP_LISTEN: - print("Server listening on {1}.{2}.{3}.{4}:{0}".format(http_port, *my_ip)) - return server_sock - - return None - - -def handle_http_request(server_sock, timeout=1.0): - t_start = time.time() - - client_sock = picowireless.avail_server(server_sock) - if client_sock in [server_sock, 255, -1]: - return False - - print("Client connected!") - - avail_length = picowireless.avail_data(client_sock) - if avail_length == 0: - picowireless.client_stop(client_sock) - return False - - request = b"" - - while len(request) < avail_length: - data = picowireless.get_data_buf(client_sock) - request += data - if time.time() - t_start > timeout: - print("Client timed out getting data!") - picowireless.client_stop(client_sock) - return False - - request = request.decode("utf-8") - - if len(request) > 0: - head, body = request.split("\r\n\r\n", 1) - dhead = {} - - for line in head.split("\r\n")[1:]: - key, value = line.split(": ", 1) - dhead[key] = value - - method, url, _ = head.split("\r\n", 1)[0].split(" ") - - print("Serving {} on {}...".format(method, url)) - - response = None - - # Dispatch the request to the relevant route - if url in routes and method in routes[url] and callable(routes[url][method]): - if method == "POST": - data = {} - for var in body.split("&"): - key, value = var.split("=") - data[key] = value - response = routes[url][method](method, url, data) - else: - response = routes[url][method](method, url) - - if response is not None: - response = "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: text/html\r\n\r\n".format(len(response)) + response - picowireless.send_data(client_sock, response) - picowireless.client_stop(client_sock) - print("Success! Sending 200 OK") - return True - else: - picowireless.send_data(client_sock, "HTTP/1.1 501 Not Implemented\r\nContent-Length: 19\r\n\r\n501 Not Implemented") - picowireless.client_stop(client_sock) - print("Unhandled Request! Sending 501 OK") - return False - - -def route(url, methods="GET"): - if type(methods) is str: - methods = [methods] - - def decorate(handler): - for method in methods: - if url not in routes: - routes[url] = {} - routes[url][method] = handler - - return decorate - - # Edit your routes here # Nothing fancy is supported, just plain ol' URLs and GET/POST methods -@route("/", methods=["GET", "POST"]) +@ppwhttp.route("/", methods=["GET", "POST"]) def get_home(method, url, data=None): if method == "POST": global r, g, b r = int(data.get("r", 0)) g = int(data.get("g", 0)) b = int(data.get("b", 0)) - picowireless.set_led(r, g, b) + ppwhttp.set_led(r, g, b) print("Set LED to {} {} {}".format(r, g, b)) return """
@@ -148,16 +31,16 @@ def get_home(method, url, data=None):
""".format(r=r, g=g, b=b) -@route("/test", methods="GET") +@ppwhttp.route("/test", methods="GET") def get_test(method, url): return "Hello World!" -start_wifi() +ppwhttp.start_wifi() -server_sock = start_server(HTTP_PORT) +server_sock = ppwhttp.start_server() while True: - handle_http_request(server_sock) + ppwhttp.handle_http_request(server_sock) time.sleep(0.01) @@ -168,9 +51,9 @@ while True: # # def server_loop_forever(): # # Start a server and continuously poll for HTTP requests -# server_sock = start_server(HTTP_PORT) +# server_sock = ppwhttp.start_server() # while True: -# handle_http_request(server_sock) +# ppwhttp.handle_http_request(server_sock) # time.sleep(0.01) # # Handle the server polling loop on the other core! diff --git a/micropython/examples/pico_wireless/secrets.py b/micropython/examples/pico_wireless/secrets.py new file mode 100644 index 00000000..be62f042 --- /dev/null +++ b/micropython/examples/pico_wireless/secrets.py @@ -0,0 +1,2 @@ +WIFI_SSID = "your SSID here!" +WIFI_PASS = "Your PSK here!" From eb3c8b0ebce863f4bcdace436dc8bb17e3086279 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 29 Sep 2021 12:47:55 +0100 Subject: [PATCH 2/7] PicoWireless: ppwhttp fix to support JSON content type This is a bit of a fudge, and was only tested against the Cheerlights API. Detects JSON content type, parses out the content length and truncates the response body to length. Should probably do this *before* decoding from utf-8. Updates cheerlights.py API example to support XML, JSON and TEXT endpoints. --- .../examples/pico_wireless/cheerlights.py | 24 ++++++++++++++++++- micropython/examples/pico_wireless/ppwhttp.py | 8 +++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/micropython/examples/pico_wireless/cheerlights.py b/micropython/examples/pico_wireless/cheerlights.py index ee198156..2a77a538 100644 --- a/micropython/examples/pico_wireless/cheerlights.py +++ b/micropython/examples/pico_wireless/cheerlights.py @@ -1,4 +1,10 @@ import time +import json +try: + import xmltok # https://pypi.org/project/micropython-xmltok/ + import io +except ImportError: + xmltok = None from micropython import const try: @@ -23,7 +29,23 @@ print("Local IP: {}.{}.{}.{}".format(*my_ip)) def handler(head, body): if head["Status"] == "200 OK": - color = body[1:] + if HTTP_REQUEST_PATH.endswith(".json"): + # Parse as JSON + data = json.loads(body) + color = data['field2'][1:] + + elif HTTP_REQUEST_PATH.endswith(".xml") and xmltok is not None: + # Parse as XML + data = xmltok.tokenize(io.StringIO(body)) + color = xmltok.text_of(data, "field2")[1:] + + elif HTTP_REQUEST_PATH.endswith(".txt"): + # Parse as plain text + color = body[1:] + + else: + print("Unable to parse API response!") + return r = int(color[0:2], 16) g = int(color[2:4], 16) b = int(color[4:6], 16) diff --git a/micropython/examples/pico_wireless/ppwhttp.py b/micropython/examples/pico_wireless/ppwhttp.py index 2d022e6b..0122d42a 100644 --- a/micropython/examples/pico_wireless/ppwhttp.py +++ b/micropython/examples/pico_wireless/ppwhttp.py @@ -158,6 +158,14 @@ Connection: close key, value = line.split(": ", 1) dhead[key] = value + # Fudge to handle JSON data type, which is prefixed with a content length. + # This ignores the charset, if specified, since we've already assumed utf-8 above! + # TODO maybe pay attention to Content-Type charset + if "Content-Type" in dhead and dhead["Content-Type"].startswith("application/json"): + length, body = body.split("\n", 1) + length = int(length, 16) + body = body[:length] + handler(dhead, body) picowireless.client_stop(client_sock) From b92d77a2f979ec02aebfe2820b83c5d6e044e8b5 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 29 Sep 2021 14:09:59 +0100 Subject: [PATCH 3/7] PicoWireless: handle encoding/content type better in ppwhttp Uses the correct? default Content-Type and encoding for HTTP. Parses the Content-Type header *before* decoding the content body. Handles JSON type gracefully. Decodes the response body accoding to the encoding header. --- micropython/examples/pico_wireless/ppwhttp.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/micropython/examples/pico_wireless/ppwhttp.py b/micropython/examples/pico_wireless/ppwhttp.py index 0122d42a..022fa3b7 100644 --- a/micropython/examples/pico_wireless/ppwhttp.py +++ b/micropython/examples/pico_wireless/ppwhttp.py @@ -149,23 +149,33 @@ Connection: close data = picowireless.get_data_buf(client_sock) response += data - response = response.decode("utf-8") - - head, body = response.split("\r\n\r\n", 1) + head, body = response.split(b"\r\n\r\n", 1) + head = head.decode("utf-8") dhead = {} for line in head.split("\r\n")[1:]: key, value = line.split(": ", 1) dhead[key] = value - # Fudge to handle JSON data type, which is prefixed with a content length. - # This ignores the charset, if specified, since we've already assumed utf-8 above! - # TODO maybe pay attention to Content-Type charset - if "Content-Type" in dhead and dhead["Content-Type"].startswith("application/json"): - length, body = body.split("\n", 1) + encoding = "iso-8869-1" + content_type = "application/octet-stream" + + if "Content-Type" in dhead: + ctype = dhead["Content-Type"].split("; ") + content_type = ctype[0].lower() + for c in ctype: + if c.startswith("encoding="): + encoding = c[9:] + + # Handle JSON content type, this is prefixed with a length + # which we'll parse and use to truncate the body + if content_type == "application/json": + length, body = body.split(b"\r\n", 1) length = int(length, 16) body = body[:length] + body = body.decode(encoding) + handler(dhead, body) picowireless.client_stop(client_sock) From 78d50c29867fa2ef644cc62173ad28cc6eff797a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 29 Sep 2021 16:27:02 +0100 Subject: [PATCH 4/7] PicoWireless: ppwhttp add wildcard routes This slightly reckless extension to ppwhttp adds support for wildcard routes, eg: /get_led/ Which will serve URLs in the form: /get_led/10 /get_led/22 etc. The wildcard includes , attempting to match the behaviour of Flask. Only type "int" is supported currently. /get_led/ - would set data["index"] to a string /get_led/ - would attempt to parse the URL part to an int, and would not serve eg: /get_led/hi See plasma_ws2812_http.py for example usage. --- .../pico_wireless/plasma_ws2812_http.py | 43 ++++++++++++- micropython/examples/pico_wireless/ppwhttp.py | 64 +++++++++++++++++-- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/micropython/examples/pico_wireless/plasma_ws2812_http.py b/micropython/examples/pico_wireless/plasma_ws2812_http.py index e214ba74..ff2f3ddb 100644 --- a/micropython/examples/pico_wireless/plasma_ws2812_http.py +++ b/micropython/examples/pico_wireless/plasma_ws2812_http.py @@ -9,6 +9,8 @@ except ImportError: """ This example uses the Plasma WS2812 LED library to drive a string of LEDs alongside the built-in RGB LED. You should wire your LEDs to VBUS/GND and connect the data pin to pin 27 (unused by Pico Wireless). + +Go to: https://
:/set_led/ to set a single LED """ NUM_LEDS = 30 # Number of connected LEDs @@ -27,7 +29,7 @@ led_strip = plasma.WS2812(NUM_LEDS, LED_PIO, LED_SM, LED_PIN) # Edit your routes here # Nothing fancy is supported, just plain ol' URLs and GET/POST methods @ppwhttp.route("/", methods=["GET", "POST"]) -def get_home(method, url, data=None): +def get_home(method, url, data): if method == "POST": global r, g, b r = int(data.get("r", 0)) @@ -46,6 +48,45 @@ def get_home(method, url, data=None): """.format(r=r, g=g, b=b) +# This wildcard route allows us to visit eg `/set_led/` +# to get/set the state of LED +# You should *probably* not modify state with GET, even though you can +# so we use a form and POST to handle changing things. +@ppwhttp.route("/set_led/", methods=["GET", "POST"]) +def set_led(method, url, data): + i = int(data.get("index", 0)) + + if method == "POST": + r = int(data.get("r", 0)) + g = int(data.get("g", 0)) + b = int(data.get("b", 0)) + led_strip.set_rgb(i, r, g, b) + print("Set LED to {} {} {}".format(r, g, b)) + else: + # TODO Fix WS2812 / APA102 get methods to correct for colour order/alignment + r, g, b, w = led_strip.get(i) + r = int(r) + g = int(g) + b = int(b) + + return """LED: {i}
+ + + + +
""".format(i=i, r=r, g=g, b=b) + + +# This wildcard route allows us to visit eg `/get_led/` +# to get the state of LED +@ppwhttp.route("/get_led/", methods="GET") +def get_led(method, url, data): + i = data.get("index", 0) + # TODO Fix WS2812 / APA102 get methods to correct for colour order/alignment + r, g, b, w = led_strip.get(i) + return "LED: {}
R: {:0.0f}
G: {:0.0f}
B: {:0.0f}".format(i, r, g, b) + + @ppwhttp.route("/test", methods="GET") def get_test(method, url): return "Hello World!" diff --git a/micropython/examples/pico_wireless/ppwhttp.py b/micropython/examples/pico_wireless/ppwhttp.py index 022fa3b7..a6ad3c0d 100644 --- a/micropython/examples/pico_wireless/ppwhttp.py +++ b/micropython/examples/pico_wireless/ppwhttp.py @@ -181,6 +181,38 @@ Connection: close picowireless.client_stop(client_sock) +def find_route(route, url, method, data): + if len(url) > 0: + for key, value in route.items(): + if key == url[0]: + return find_route(route[url[0]], url[1:], method, data) + + elif key.startswith("<") and key.endswith(">"): + key = key[1:-1] + if ":" in key: + dtype, key = key.split(":") + else: + dtype = "str" + + if dtype == "int": + try: + data[key] = int(url[0]) + except ValueError: + continue + + else: + data[key] = url[0] + + return find_route(value, url[1:], method, data) + + return None, None + + if method in route: + return route[method], data + + return None, None + + def handle_http_request(server_sock, timeout=5000): t_start = time.ticks_ms() @@ -221,16 +253,24 @@ def handle_http_request(server_sock, timeout=5000): response = None + data = {} + + if url.startswith("/"): + url = url[1:] + url = url.split("/") + handler, data = find_route(routes, url, method, data) + # Dispatch the request to the relevant route - if url in routes and method in routes[url] and callable(routes[url][method]): + if callable(handler): if method == "POST": - data = {} for var in body.split("&"): key, value = var.split("=") data[key] = value - response = routes[url][method](method, url, data) + + if data == {}: + response = handler(method, url) else: - response = routes[url][method](method, url) + response = handler(method, url, data) if response is not None: response = "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: text/html\r\n\r\n".format(len(response)) + response @@ -249,10 +289,20 @@ def route(url, methods="GET"): if type(methods) is str: methods = [methods] + if url.startswith("/"): + url = url[1:] + + url = url.split("/") + def decorate(handler): + route = routes + for part in url: + if part not in route: + route[part] = {} + + route = route[part] + for method in methods: - if url not in routes: - routes[url] = {} - routes[url][method] = handler + route[method] = handler return decorate From 7772959450ec246438b79587c62f818a10d43fbd Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 30 Sep 2021 10:47:30 +0100 Subject: [PATCH 5/7] PicoWireless: add TLS support to ppwhttp * Add a new "connection_mode" argument to http_request. This can be TLS_MODE or TCP_MODE * Fix a bug where assumptions about json parsing don't hold up * Check for TCP_STATE_CLOSED and bail early from connect_to_server --- micropython/examples/pico_wireless/ppwhttp.py | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/micropython/examples/pico_wireless/ppwhttp.py b/micropython/examples/pico_wireless/ppwhttp.py index a6ad3c0d..f662d9db 100644 --- a/micropython/examples/pico_wireless/ppwhttp.py +++ b/micropython/examples/pico_wireless/ppwhttp.py @@ -17,6 +17,24 @@ except ImportError: TCP_CLOSED = const(0) TCP_LISTEN = const(1) +TCP_MODE = const(0) +UDP_MODE = const(1) +TLS_MODE = const(2) +UDP_MULTICAST_MODE = const(3) +TLS_BEARSSL_MODE = const(4) + +TCP_STATE_CLOSED = const(0) +TCP_STATE_LISTEN = const(1) +TCP_STATE_SYN_SENT = const(2) +TCP_STATE_SVN_RCVD = const(3) +TCP_STATE_ESTABLISHED = const(4) +TCP_STATE_FIN_WAIT_1 = const(5) +TCP_STATE_FIN_WAIT_2 = const(6) +TCP_STATE_CLOSE_WAIT = const(7) +TCP_STATE_CLOSING = const(8) +TCP_STATE_LAST_ACK = const(9) +TCP_STATE_TIME_WAIT = const(10) + CLOUDFLARE_DNS = (1, 1, 1, 1) GOOGLE_DNS = (8, 8, 8, 8) @@ -95,31 +113,38 @@ def start_server(http_port=DEFAULT_HTTP_PORT, timeout=5000): return None -def connect_to_server(host_address, port, client_sock, timeout=5000): - picowireless.client_start(host_address, port, client_sock, TCP_CLOSED) +def connect_to_server(host_address, port, client_sock, timeout=5000, connection_mode=TCP_MODE): + if connection_mode in (TLS_MODE, TLS_BEARSSL_MODE): + print("Connecting to {1}:{0}...".format(port, host_address)) + picowireless.client_start(host_address, (0, 0, 0, 0), port, client_sock, connection_mode) + else: + host_address = get_host_by_name(host_address) + print("Connecting to {1}.{2}.{3}.{4}:{0}...".format(port, *host_address)) + picowireless.client_start(host_address, port, client_sock, connection_mode) t_start = time.ticks_ms() while time.ticks_ms() - t_start < timeout: state = picowireless.get_client_state(client_sock) - if state == 4: + if state == TCP_STATE_ESTABLISHED: + print("Connected!") return True - time.sleep(1.0) + if state == TCP_STATE_CLOSED: + print("Connection failed!") + return False + print(state) + time.sleep(0.5) + print("Connection timeout!") return False -def http_request(host_address, port, request_host, request_path, handler, timeout=5000, client_sock=None): +def http_request(host_address, port, request_host, request_path, handler, timeout=5000, client_sock=None, connection_mode=TCP_MODE): if client_sock is None: client_sock = get_socket() - host_address = get_host_by_name(host_address) - - print("Connecting to {1}.{2}.{3}.{4}:{0}...".format(port, *host_address)) - if not connect_to_server(host_address, port, client_sock): - print("Connection failed!") + if not connect_to_server(host_address, port, client_sock, connection_mode=connection_mode): return False - print("Connected!") http_request = """GET {} HTTP/1.1 Host: {} @@ -170,9 +195,10 @@ Connection: close # Handle JSON content type, this is prefixed with a length # which we'll parse and use to truncate the body if content_type == "application/json": - length, body = body.split(b"\r\n", 1) - length = int(length, 16) - body = body[:length] + if not body.startswith(b"{"): + length, body = body.split(b"\r\n", 1) + length = int(length, 16) + body = body[:length] body = body.decode(encoding) From 71058bca1ea7d3dadf6cc20fb542936d354ce49b Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 4 Oct 2021 16:16:19 +0100 Subject: [PATCH 6/7] PicoWireless: use strnlen for fwver and SSIDs Avoid going through std::string and instead uses strnlen to get string length. Prevents extra null chars leaking into the Python string. --- micropython/modules/pico_wireless/pico_wireless.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/micropython/modules/pico_wireless/pico_wireless.cpp b/micropython/modules/pico_wireless/pico_wireless.cpp index 10ec8c44..bcb92f26 100644 --- a/micropython/modules/pico_wireless/pico_wireless.cpp +++ b/micropython/modules/pico_wireless/pico_wireless.cpp @@ -370,8 +370,7 @@ mp_obj_t picowireless_get_ssid_networks(size_t n_args, const mp_obj_t *pos_args, uint8_t network_item = args[ARG_network_item].u_int; const char* ssid = wireless->get_ssid_networks(network_item); if(ssid != nullptr) { - std::string str_ssid(ssid, WL_SSID_MAX_LENGTH); - return mp_obj_new_str(str_ssid.c_str(), str_ssid.length()); + return mp_obj_new_str(ssid, strnlen(ssid, WL_SSID_MAX_LENGTH)); } } else @@ -516,8 +515,7 @@ mp_obj_t picowireless_get_host_by_name(size_t n_args, const mp_obj_t *pos_args, mp_obj_t picowireless_get_fw_version() { if(wireless != nullptr) { const char* fw_ver = wireless->get_fw_version(); - std::string str_fw_ver(fw_ver, WL_FW_VER_LENGTH); - return mp_obj_new_str(str_fw_ver.c_str(), str_fw_ver.length()); + return mp_obj_new_str(fw_ver, strnlen(fw_ver, WL_FW_VER_LENGTH)); } else mp_raise_msg(&mp_type_RuntimeError, NOT_INITIALISED_MSG); From 1bb61b2c52b90a289f191c2eab22f60d1096d6d6 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 4 Oct 2021 16:18:13 +0100 Subject: [PATCH 7/7] PicoWireless: network scan example Basic example to scan SSID networks and list them. --- .../examples/pico_wireless/scan_networks.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 micropython/examples/pico_wireless/scan_networks.py diff --git a/micropython/examples/pico_wireless/scan_networks.py b/micropython/examples/pico_wireless/scan_networks.py new file mode 100644 index 00000000..e120ccc7 --- /dev/null +++ b/micropython/examples/pico_wireless/scan_networks.py @@ -0,0 +1,15 @@ +import picowireless +import time + +picowireless.init() + +picowireless.start_scan_networks() + +while True: + networks = picowireless.get_scan_networks() + print("Found {} network(s)...".format(networks)) + for network in range(networks): + ssid = picowireless.get_ssid_networks(network) + print("{}: {}".format(network, ssid)) + print("\n") + time.sleep(10)