├── dev ├── __init__.py ├── apps │ ├── __init__.py │ ├── demo │ │ ├── __init__.py │ │ ├── main.py │ │ └── ws.py │ └── echoserver │ │ ├── __init__.py │ │ └── main.py ├── app.json ├── network.json ├── certs │ ├── server.csr │ ├── server.crt │ ├── client.key │ └── server.key ├── main.py ├── controllers.py └── www │ ├── page.htm │ └── jquery-3.5.1.min.js ├── tests ├── __init__.py ├── pytest.ini ├── TESTING.rst ├── test_handhake.py └── test_protocol.py ├── .gitignore ├── scripts ├── requirements.txt ├── server-gen.sh ├── echo_client.py ├── device_server.py └── README.rst ├── .gitmodules ├── examples ├── websocket │ ├── main.py │ ├── runme.py │ └── test.py └── ws_client_dupterm │ └── notes ├── test └── sslclient.py ├── WEBSOCKETS.rst ├── HTTP.rst └── README.rst /dev/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev/apps/demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev/apps/echoserver/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode = strict 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | certs/* 2 | __pycache__/ 3 | .coverage 4 | 5 | utils.py 6 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | cryptography 3 | pytest 4 | pytest-pudb 5 | pytest-cov 6 | pytest-asyncio 7 | -------------------------------------------------------------------------------- /scripts/server-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/bin/openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes 4 | -------------------------------------------------------------------------------- /tests/TESTING.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | Right now, run ``pytest`` in this directory, not the top level. 5 | 6 | I have to wrangle some paths in pytest as the modules are not to 7 | be installed when using (macro)python. 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dev/http"] 2 | path = dev/http 3 | url = git@github.com:marcidy/micropython-uasyncio-http 4 | [submodule "dev/websockets"] 5 | path = dev/websockets 6 | url = git@github.com:marcidy/micropython-uasyncio-websockets 7 | -------------------------------------------------------------------------------- /examples/websocket/main.py: -------------------------------------------------------------------------------- 1 | from websockets.test import client_test 2 | import uasyncio 3 | import time 4 | 5 | 6 | while not net.isconnect(): 7 | time.sleep_ms(200) 8 | 9 | loop = uasyncio.get_event_loop() 10 | loop.run_until_complete(client_test()) 11 | -------------------------------------------------------------------------------- /dev/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "demo", 3 | "echoserver": { 4 | "listen_addr": "0.0.0.0", 5 | "listen_port": 7777 6 | }, 7 | "demo": { 8 | "http": { 9 | "ip": "0.0.0.0", 10 | "port": 80 11 | }, 12 | "websocket": { 13 | "ip": "0.0.0.0", 14 | "port": 7777 15 | }, 16 | "home": { 17 | "ip": "192.168.3.1", 18 | "port": 7770 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/websocket/runme.py: -------------------------------------------------------------------------------- 1 | from websockets.test import add_client 2 | from websockets.ssl_server import serve 3 | import uasyncio 4 | import network 5 | import webrepl 6 | 7 | 8 | net = network.WLAN(network.STA_IF) 9 | net.active(1) 10 | net.connect("titopuente", "testmatt") 11 | webrepl.start(password="test") 12 | 13 | def main(): 14 | wss_server = serve(add_client, "0.0.0.0", 7777) 15 | loop = uasyncio.get_event_loop() 16 | loop.run_until_complete(wss_server) 17 | loop.run_forever() 18 | -------------------------------------------------------------------------------- /dev/network.json: -------------------------------------------------------------------------------- 1 | { 2 | "device": { 3 | "iam": "Device_1", 4 | "dev": "ESP32" 5 | }, 6 | "ap": { 7 | "essid": "upy-ap-test", 8 | "authmode": 3, 9 | "password": "testislonger", 10 | "hidden": false 11 | }, 12 | "sta": { 13 | "ssid": "titopuente", 14 | "pass": "testmatt" 15 | }, 16 | "wsserver": { 17 | "ip": "192.168.3.1", 18 | "host": "mattarcidy.com", 19 | "port": 7777, 20 | "path": "TS001" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/sslclient.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import websockets 4 | import json 5 | 6 | 7 | async def hello(): 8 | uri = "wss://192.168.3.116:7777/TESTABC" 9 | async with websockets.connect(uri) as websocket: 10 | await websocket.send(json.dumps(['GET', 'wifi', [""]])) 11 | async for msg in websocket: 12 | print(msg) 13 | 14 | 15 | logger = logging.getLogger('websockets.client') 16 | logger.setLevel(logging.DEBUG) 17 | logger.addHandler(logging.StreamHandler()) 18 | loop = asyncio.get_event_loop() 19 | loop.run_until_complete(hello()) 20 | -------------------------------------------------------------------------------- /dev/certs/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBmzCCAQQCAQAwWzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx 3 | ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLMTky 4 | LjE2OC40LjEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAL0jq7CCi3NKemAU 5 | ziI27F7JDz7Pho7Os5wAQR+MXafaV8tQSQZ3Izi4vvmqDKmoafprw8NJV+lOdDbW 6 | RF8p5KAR9nOP2Em5wXPH7RU5wwjTcWRpr1UnbrQ08EnhB7iiBGQ7YPYLH3R/6LSF 7 | bEI8skNBgbw+u9Dl6XkJ2JTE5zHNAgMBAAGgADANBgkqhkiG9w0BAQsFAAOBgQBG 8 | leNF4BruHCLnTvRfqkilAdq3QlC45JgyJHuN3CYrmWtEXKMerR0imvFIMfZDgAme 9 | at18uw9ZltojmjQH2Ow/Fzs0pqSJX+7aD8eltxmNt1TNBkB7LokjkJ259QWZfvFr 10 | b4UnVhMxivEkRLZ22uUZDtKjlEscWobMr6yKsOzflQ== 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /dev/apps/echoserver/main.py: -------------------------------------------------------------------------------- 1 | import uasyncio 2 | from controllers import init 3 | from apps.utils import load_app_cfg 4 | from websockets.server import serve 5 | 6 | 7 | async def on_connect(ws, path): 8 | print("Client connection to {}".format(path)) 9 | 10 | async for msg in ws: 11 | await ws.send(msg) 12 | 13 | 14 | def app_main(args): 15 | init() 16 | config = load_app_cfg()['echoserver'] 17 | listen_addr = config['listen_addr'] 18 | listen_port = config['listen_port'] 19 | print("starting echo server on {}:{}".format(listen_addr, listen_port)) 20 | uasyncio.create_task(serve(on_connect, listen_addr, listen_port)) 21 | loop = uasyncio.get_event_loop() 22 | loop.run_forever() 23 | -------------------------------------------------------------------------------- /dev/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIB8zCCAVwCFGqHtDsZanbuJdP/cNf5h487c7zdMA0GCSqGSIb3DQEBCwUAMBYx 3 | FDASBgNVBAMMCzE5Mi4xNjguMy4xMB4XDTIyMDUyNDIzNTE0OFoXDTIzMDUyNDIz 4 | NTE0OFowWzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV 5 | BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLMTkyLjE2OC40 6 | LjEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAL0jq7CCi3NKemAUziI27F7J 7 | Dz7Pho7Os5wAQR+MXafaV8tQSQZ3Izi4vvmqDKmoafprw8NJV+lOdDbWRF8p5KAR 8 | 9nOP2Em5wXPH7RU5wwjTcWRpr1UnbrQ08EnhB7iiBGQ7YPYLH3R/6LSFbEI8skNB 9 | gbw+u9Dl6XkJ2JTE5zHNAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAnpmce13kl1gS 10 | HNQdzs2J2SYHuz4cWjXGUj+wbkJ6nLx2fVsZ7aIyjuBbSHylIYP83jMXCQx0Jj96 11 | md/KOFZixdeuxY5NEB7Hs8M9aaEyCOMc6qP10/nQ8uRMsXbS+i37ZS7CRSIJ1/18 12 | i35NQ1GwoexVhxsXUF/3Wrl0qeud5wE= 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /examples/websocket/test.py: -------------------------------------------------------------------------------- 1 | import uasyncio as asyncio 2 | from websockets.client import connect 3 | # from websockets.server import serve 4 | 5 | 6 | async def client_test(): 7 | ws = await connect("wss://192.168.3.116:7777/test") 8 | if not ws: 9 | print("connection failed") 10 | return 11 | print("sending") 12 | await ws.send("This is a story") 13 | print("sent") 14 | async for msg in ws: 15 | print(msg) 16 | await ws.wait_closed() 17 | 18 | 19 | async def add_client(ws, path): 20 | print("Connection on {}".format(path)) 21 | 22 | try: 23 | await ws.send("adding client to {}".format(path)) 24 | async for msg in ws: 25 | print(msg) 26 | await ws.send(msg) 27 | finally: 28 | print("Disconnected") 29 | 30 | 31 | # ws_server = serve(add_client, "0.0.0.0", 7777) 32 | # loop = asyncio.get_event_loop() 33 | # loop.run_until_complete(ws_server) 34 | # loop.run_forever() 35 | -------------------------------------------------------------------------------- /dev/certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAPJKNNv2pS81pUmH 3 | CFUbrLLPEEFfRocPdEk9RfNDTzbP+araEUf+oWjiwhql+RCYM8rzJcxL3GTBzEVi 4 | Swx4NiiXx7QT8BfifDA7OJkTZmh9AG1Uig2Nvh+HZzZ++OSK+FVBcC81vyRlNa9R 5 | zOm2l3pM1/rAoJIhP2iCOBO3w7ynAgMBAAECgYEAwujBRyZOUgEU22Z+ZIDj1+BN 6 | ElnD0ciz4mshR4WhRtXd4fyVJFaJoGeygF9+UkQufhhGEuf//yoL2tEs0HYwEbz6 7 | rF9QEIsnhTY5gr5c/CanvEI1xA6z45moTwwdTp2/EKOsQKz1Fet65g2GuQ2Ly8iV 8 | sxoQivGqTSRKN3k/dUECQQD6gB3z6S09eypJbIHtXbWaCxDsbjrwHb7cVIVJmaVr 9 | bc86OdP/uB9hKD52maSI+P6yr76mLYdAzg2JTV5z1MSHAkEA95vxlBAQxVVRODd3 10 | PVfIKU3i9bPFvGrDiJVDyDfKQTef0ryceeW76OZoYYLrz8mL/4m8ItKKXQpHR4Za 11 | /Wdu4QJBAPWekcALrvRNxBTidDNOYzZ8C0gIfXnbcL2Rkm+sW+qObVbmRNzEqOAX 12 | GcgotpAntXV3pTRECA6e+97ZIffpdBkCQQCYLEHCwkDcVfvNdeEVVR6Rq+lhIXPT 13 | wTWcekenBAqMHDhgFkSAcd0TXI8n5oMwN2iPysFSEVyxo4B9B6hh9jBhAkAQrPPd 14 | LLKhj1ZjKm3sZo9cGUW0CFE2KE565gEY9YEnJiDW0w/u6nlBGPWJ4K3BNzp4SKdy 15 | 12qc0cn1PHxK8wbc 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /dev/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL0jq7CCi3NKemAU 3 | ziI27F7JDz7Pho7Os5wAQR+MXafaV8tQSQZ3Izi4vvmqDKmoafprw8NJV+lOdDbW 4 | RF8p5KAR9nOP2Em5wXPH7RU5wwjTcWRpr1UnbrQ08EnhB7iiBGQ7YPYLH3R/6LSF 5 | bEI8skNBgbw+u9Dl6XkJ2JTE5zHNAgMBAAECgYA5eXxzv8e2ehxj1A6rsPr09q4O 6 | TSu7e65yqlUfzBytpBjnrHb0YwGt+930qir85zOFKHgtgL0ZJYEFJOlT7bwOLoSD 7 | +XORRdENiXVfHKh1Komn6E2n87siTOTvqcx3p5r3hZR8wl+BKXgIv9xrsZl6z3as 8 | +Tvrj4rVIpA4Ek7Y1QJBAOB6jvNQnDsK2a0NghXP9DmlQQaWfHDmmTrhb5HicW02 9 | ucadcyKHLnMFXweT//rIVbcVekdmg8Rv6cwkK0jiEzsCQQDXssAQ70BDpF6bbwZu 10 | 6RGoOPxL85PiY/vD792uNDWbemronAsUWT2RDoxlIs79XuMNVAVlcoXDJ8UIVzHo 11 | N+6XAkAtws5Ba71ti9i0HnzWVX5EAhwva54Spe+2wR4tbywQR4e3pYFDGKuvZvjo 12 | YpNcXFqc6BP1WkCiWu4eX4EzamLhAkEAmZ2SL/1UVVgwkJ4nhMG0c4vyEt1sSuVO 13 | HqMry68fJpWuoe7P7TQJJs+nqTd7FvOG3K0kErXXEb+3EVOYFXwQDQJAC+0xhBq5 14 | MH+/9gAQNCjAE4LE05cdPbSqrH9p8sSFjZb59NRAYIaCw2q2u96iVbrS108faGN1 15 | wsydnYEznKaOVw== 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /dev/apps/demo/main.py: -------------------------------------------------------------------------------- 1 | import uasyncio 2 | from controllers import init 3 | from apps.utils import load_app_cfg 4 | from http.web import server as http_server, route 5 | from websockets.server import serve as websocket_server 6 | from apps.demo.ws import add_client, call_home 7 | 8 | 9 | def app_main(args): 10 | init() 11 | route(b'/', '/www/page.htm') 12 | route(b'/static/jquery.js', '/www/jquery-3.5.1.min.js') 13 | config = load_app_cfg()['demo'] 14 | uasyncio.create_task( 15 | uasyncio.start_server( 16 | http_server, 17 | config['http']['ip'], 18 | config['http']['port'])) 19 | uasyncio.create_task( 20 | websocket_server( 21 | add_client, 22 | config['websocket']['ip'], 23 | config['websocket']['port'])) 24 | uasyncio.create_task( 25 | call_home( 26 | config['home']['ip'], 27 | config['home']['port'])) 28 | 29 | loop = uasyncio.get_event_loop() 30 | loop.run_forever() 31 | -------------------------------------------------------------------------------- /dev/main.py: -------------------------------------------------------------------------------- 1 | from machine import reset 2 | import sys 3 | from apps.utils import ( 4 | load_app_cfg, 5 | recovery, 6 | ) 7 | 8 | 9 | def app_main(args): 10 | ''' a 'default' app_main function which is called if the import from apps 11 | fails ''' 12 | recovery() 13 | 14 | # try to load application configuration and load target app 15 | try: 16 | app_cfg = load_app_cfg() 17 | if not app_cfg: 18 | raise ValueError("No app config") 19 | app = app_cfg.get('app') 20 | if not app: 21 | raise ValueError("No app defined in app config") 22 | modline = "from apps.{}.main import app_main".format(app) 23 | exec(modline) 24 | except Exception as e: 25 | # don't fail here, app_main is still runnable and will launch recovery 26 | sys.print_exception(e) 27 | 28 | 29 | try: 30 | app_main(None) 31 | except KeyboardInterrupt: 32 | sys.exit() 33 | except Exception as e: 34 | sys.print_exception(e) 35 | recovery() 36 | except BaseException as e: 37 | sys.print_exception(e) 38 | recovery() 39 | -------------------------------------------------------------------------------- /scripts/echo_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import websockets 4 | from concurrent.futures import ThreadPoolExecutor 5 | import pudb 6 | import time 7 | 8 | 9 | async def ainput(prompt=">>> "): 10 | with ThreadPoolExecutor(1, "AsyncInput") as executor: 11 | return await asyncio.get_event_loop().run_in_executor(executor, input, prompt) 12 | 13 | 14 | async def reader(ws, prompt=">>> "): 15 | while True: 16 | data = await ainput(prompt) 17 | await ws.send(data) 18 | 19 | 20 | async def client(uri): 21 | ws = await websockets.connect(uri, ping_interval=5) 22 | st = time.time() 23 | 24 | reader_rask = asyncio.create_task(reader(ws)) 25 | 26 | try: 27 | async for msg in ws: 28 | print(msg) 29 | except Exception as e: 30 | et = time.time() 31 | print(f"Duration: {et - st}") 32 | raise e 33 | 34 | 35 | if __name__ == "__main__": 36 | 37 | uri = input("websocket server uri [ws://192.168.4.1:7777]: ") 38 | if not uri: 39 | uri = "ws://192.168.4.1:7777" 40 | logger = logging.getLogger('websockets.client') 41 | logger.setLevel(logging.DEBUG) 42 | logger.addHandler(logging.StreamHandler()) 43 | 44 | loop = asyncio.new_event_loop() 45 | loop.run_until_complete(client(uri)) 46 | -------------------------------------------------------------------------------- /WEBSOCKETS.rst: -------------------------------------------------------------------------------- 1 | Micropython uasycio websockets 2 | ============================== 3 | A client/server websockets implementation 4 | 5 | Examples 6 | -------- 7 | see http://github.com/marcidy/micropython-uasyncio-webexample for both the client and server in action and how to use them in web pages as well as with python `websockets`. That repository also has unit tesets for the protocol. 8 | 9 | protocol.py 10 | ----------- 11 | The heart of the websocket is the protocol, developed by http://github.com/danni/uwebsockets. 12 | 13 | The protocol has been updated to take fragmentation into account and checked against python `websocket` 10.3. 14 | 15 | client.py 16 | --------- 17 | `client.connect(uri)` creates a socket, connectes to `uri` and attempts to upgrade the connection to a WebSocket. 18 | 19 | The returned WebSocketClient should then be run as:: 20 | 21 | ws = await websocket.client.connect('ws://server.tld:7777/path') 22 | 23 | async for msg in ws: 24 | print(msg) 25 | 26 | server.py 27 | --------- 28 | `server.connect(client_callback, host, port)` is run as a uasyncio task, which runs `uasyncio.start_server` with a modified first argument which 29 | performs the server handshake and calls `client_callback` on the resulting WebSocket as `uasyncio.create_task(client_callback(ws, path))` 30 | 31 | note: this call is not compatible with websockets 10.3 where `path` is an attribute on the websocket object. A future update will address this. 32 | -------------------------------------------------------------------------------- /dev/controllers.py: -------------------------------------------------------------------------------- 1 | import network 2 | import json 3 | import time 4 | 5 | 6 | net = network.WLAN(network.STA_IF) 7 | ap = network.WLAN(network.AP_IF) 8 | config = {} 9 | 10 | 11 | def load_config(): 12 | with open("/network.json", 'r') as f: 13 | data = json.load(f) 14 | 15 | if data: 16 | config.update(**data) 17 | 18 | load_config() # need it done on import 19 | 20 | 21 | class FakeInterface: 22 | 23 | def __init__(self): 24 | self.sent = 0 25 | self.received = 0 26 | self.to_send = [] 27 | self.to_rec = [] 28 | 29 | def send(self, msg): 30 | self.to_send.append(msg) 31 | 32 | def recv(self): 33 | if self.to_rec: 34 | return self.to_rec.pop() 35 | 36 | def run(self): 37 | while self.to_send: 38 | msg = self.to_send.pop(0) 39 | print(msg) 40 | self.to_rec.append(msg.upper()) 41 | 42 | 43 | fake_interface = FakeInterface() 44 | 45 | 46 | def init_sta(): 47 | ssid = config['sta'].get('ssid') 48 | pwrd = config['sta'].get('pass') 49 | 50 | if not ssid: 51 | raise ValueError("No SSID in network.json") 52 | 53 | net.active(1) 54 | if not net.status() in [ 55 | network.STAT_IDLE, 56 | network.STAT_GOT_IP,]: 57 | net.disconnect() 58 | 59 | try: 60 | net.connect(ssid, pwrd) 61 | except OSError: 62 | net.disconnect() 63 | net.connect() 64 | 65 | start_time = time.time() 66 | while (not net.isconnected()): 67 | if (time.time() - start_time > 30): 68 | print("Connect failed") 69 | return False 70 | time.sleep(.010) 71 | 72 | return True 73 | 74 | 75 | def init_ap(): 76 | 77 | ap_config = config.get('ap') 78 | if not ap_config: 79 | return True 80 | 81 | ap.active(1) 82 | ap.config(**ap_config) 83 | return True 84 | 85 | 86 | def init_webrepl(): 87 | import webrepl 88 | webrepl.start(password="test") 89 | 90 | 91 | def init(): 92 | load_config() 93 | init_sta() 94 | init_ap() 95 | init_webrepl() 96 | -------------------------------------------------------------------------------- /scripts/device_server.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pathlib 3 | import logging 4 | import logging.handlers 5 | import asyncio 6 | from collections import defaultdict 7 | import json 8 | import ssl 9 | import websockets 10 | 11 | 12 | def log_setup(): 13 | handler = logging.handlers.TimedRotatingFileHandler( 14 | "device_server.log", when='D') 15 | formatter = logging.Formatter( 16 | '%(asctime)s device_server [%(process)d]: %(message)s', 17 | '%b %d %H:%M:%S') 18 | formatter.converter = time.gmtime 19 | handler.setFormatter(formatter) 20 | logger = logging.getLogger() 21 | logger.addHandler(handler) 22 | logger.setLevel(logging.INFO) 23 | 24 | 25 | devices = {} 26 | pages = defaultdict(set) 27 | 28 | 29 | async def add_page(websocket, path): 30 | print(f"Page connected to {path}") 31 | pages[path].add(websocket) 32 | try: 33 | async for msg in websocket: 34 | logging.info( "< " + path + "/ " + msg) 35 | if path in devices: 36 | await devices[path].send(msg) 37 | finally: 38 | pages[path].remove(websocket) 39 | print("Page Left") 40 | 41 | 42 | async def add_device(websocket, path): 43 | print(f"Device connected: {path}") 44 | if path in devices: 45 | ws = devices.pop(path) 46 | await ws.close() 47 | devices[path] = websocket 48 | 49 | try: 50 | async for msg in websocket: 51 | logging.info("> " + path + "/ " + msg) 52 | if len(pages[path]) > 0: 53 | asyncio.gather(*[pg.send(msg) for pg in pages[path]]) 54 | finally: 55 | if path in devices: 56 | devices.pop(path) 57 | await websocket.close() 58 | 59 | 60 | page_server = websockets.serve(add_page, '127.0.0.1', 7771, compression=None) 61 | device_server = websockets.serve(add_device, '0.0.0.0', 7770, compression=None) 62 | 63 | 64 | if __name__ == "__main__": 65 | log_setup() 66 | logging.info('Device Server restarted') 67 | loop = asyncio.new_event_loop() 68 | loop.run_until_complete(device_server) 69 | loop.run_until_complete(page_server) 70 | loop.run_forever() 71 | -------------------------------------------------------------------------------- /HTTP.rst: -------------------------------------------------------------------------------- 1 | Tiny Asyncio HTTP server for micropython 2 | ======================================== 3 | 4 | Who DOESN'T need to serve some files over http on their micropython device? 5 | 6 | This does it with asyncio and with extremely limited functionality. 7 | 8 | The goal here is not to be fully featured, but to aid in settings up basic services on a device 9 | which can then host small things like configuring a wifi connection, basic diagnostics, etc. 10 | 11 | Example 12 | ------- 13 | If you want to see it in action, it's part of a worked example here: 14 | http://github.com/marcidy/micropython-uasyncio-webexample. 15 | 16 | http/web.py 17 | ----------- 18 | `server(reader, writer)` is used with `uasyncio.start_server(server, host, port)` and is 19 | called on new connections. 20 | 21 | The server is really chatty at the moment with lots of print statements so you can see what's 22 | going on. I'll quiet those with a debug flag at some point. 23 | 24 | The `route(location, resource)` function is used to match the requested route (say 'static/jquery-min.js`) 25 | with the resource on the filesytem that will be served (say '/www/jquery-3.5.1.js'). `route` is synchronous, 26 | but turns the resource into an async coroutine with the `pre_route` function. This is a bit convoluted, 27 | but will allow expansion over time. The result of calling `route` is that the module dict `routes` is 28 | population with pairs like:: 29 | 30 | routes = { 31 | b'/': b'/www/page.htm', 32 | b'/style.css': b'/www/style.css', 33 | b'/static/jquery-min.js': b'/www/jquery-3.5.1.js' 34 | } 35 | 36 | Note that the entries are all decoded strings (b""). This works better with the raw data coming over 37 | the socket. Note we don't serve right off the filesystem. This lets you control what can be accessed. 38 | 39 | `send_file(writer, file)` serves the item located at `file` via a chunked transfer. This can be html, js, 40 | anything. It's quite simple, however. It's sufficient to send over an html page and the jquery library 41 | and do some fancy stuff client side instead of server side. 42 | 43 | Enhancements welcome, but we're trying to keep it small, so it realy ought to be something you can't do 44 | but is realy useful. 45 | 46 | Or ever better, make it modular to add handlers on the fly and have a repo of handlers. 47 | -------------------------------------------------------------------------------- /examples/ws_client_dupterm/notes: -------------------------------------------------------------------------------- 1 | Who calls these functions? 2 | uio.IOBase gives read, write, and ioctl. How about close? 3 | note that read maps to readinto 4 | 5 | extmod/uos_dupterm.c 6 | 7 | - mp_uos_dupterm(size_t n_args, const mp_obj_t *args) 8 | - if 2 args, 2nd args is index, otherwise index is 0 9 | - is index < 0 or index >= MICROPY_PY_OS_DUPTERM 10 | - invalid index 11 | - MICROPY_PY_OS_DUPTERM must be default terminal 12 | - get previous dupterm object at index 13 | - if it's NULL, return None 14 | - if 15 | - first arg is None, set dupter_obj[idx] to NULL 16 | - else 17 | - check if object has read, write and ioctl 18 | - set dupterm object at index to first arg 19 | - return previous object from that index 20 | 21 | - mp_uos_dupter_tx_strn(const char *str, size_t len) 22 | - called in: 23 | - ports/esp32/mphalport.c:mp_hal_stdout_tx_strn 24 | 25 | - loop over all indices in dupter_objs 26 | - if object at index is NULL, continue 27 | 28 | - if built with MICROPY_PY_UOS_DUPTERM_BULTIN_STREAM 29 | - gets stream pointer for dupterm_object at index 30 | - write the string 31 | - else 32 | - create nlr_buf_t # some kind of execution context? 33 | - if nlr_push(&nlr) == 0 # i assume successful push 34 | - call write 35 | - call nlr_pop() # pop out of the context? 36 | - else 37 | - deactivate the object at index, something wrong with writing string 38 | 39 | - mp_uos_dupterm_rx_chr(void) 40 | - called in: ports/esp32/mouos.c:os_dupterm_notify 41 | 42 | - for each object in dupterm_objs, reads 1 byte 43 | - note that mp_stream->read maps to readinto 44 | 45 | - mp_uos_dupterm_poll(uintptr_t poll_flags) 46 | - for each object in dupterm_objs: 47 | - if null, continue 48 | - if it's a builtin stream 49 | - call ioctl(obj, MP_STREAM_POLL, poll_flags, errcode) 50 | - else 51 | - same but with nlr_buf context 52 | 53 | - mp_uos_deactivate(size_t dupterm_idx, const char *msg, mp_obj_t exc) 54 | - if an exception is passed in, print it out 55 | - call close on the object, ignoring errors from the close operation 56 | 57 | ports/esp32/moduos.c 58 | 59 | - os_dupterm_notify(mp_obj_t obj_in) 60 | - calls mp_uos_dupterm_rx_chr, which returns bytes, until bytes < 0 61 | - puts byte into the stdin ringbuffer 62 | 63 | 64 | so if I manage the call to dupterm_notify instead of using it as a call back, i can control when the 65 | websocket.read is called. 66 | 67 | Just need to make websocket.read pop chars / bytes or something 68 | -------------------------------------------------------------------------------- /scripts/README.rst: -------------------------------------------------------------------------------- 1 | Scripts 2 | ======= 3 | Helpers scripts I run on my debian machine. 4 | 5 | requirements.py 6 | --------------- 7 | 8 | Python requirements to run the scripts 9 | 10 | echo_client.py 11 | -------------- 12 | A simple websockets based client. Use this with the "echoserver" example. 13 | 14 | It asks for the uri to connect to the echo server as "ws://:" and will attempt 15 | to connect. 16 | 17 | Is set to log in debug mode so you'll see pings and pongs. 18 | 19 | e.g. if the device has ip 192.168.1.114 and the echoserver port is set to 7890: 20 | usage:: 21 | 22 | $ python echo_client.py 23 | websocket server uri [ws://192.168.4.1:7777]: ws://192.168.1.114:7890 24 | = connection is CONNECTING 25 | > GET / HTTP/1.1 26 | > Host: 192.168.1.114:7890 27 | > Upgrade: websocket 28 | > Connection: Upgrade 29 | > Sec-WebSocket-Key: FgkVdWpmh9iuwtjQxyF/eA== 30 | > Sec-WebSocket-Version: 13 31 | > Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits 32 | > User-Agent: Python/3.10 websockets/10.1 33 | < HTTP/1.1 101 Switching Protocols 34 | < Upgrade: websocket 35 | < Connection: Upgrade 36 | < Sec-WebSocket-Accept: q8bxu70GcvzYecW7xN1CVC9zyNc= 37 | < Server: Micropython 38 | = connection is OPEN 39 | >>> % sending keepalive ping 40 | > PING a2 8b 96 01 [binary, 4 bytes] 41 | < PONG a2 8b 96 01 [binary, 4 bytes] 42 | % received keepalive pong 43 | % sending keepalive ping 44 | > PING 3c cd 30 c7 [binary, 4 bytes] 45 | < PONG 3c cd 30 c7 [binary, 4 bytes] 46 | % received keepalive pong 47 | 48 | 49 | The ping and pong will continue, this shows that the websocket server is running well, 50 | responding to ping requests. 51 | 52 | 53 | device_server.py 54 | ---------------- 55 | Basically a pipe for devices to chat with webpages. 56 | 57 | run the server like:: 58 | 59 | $ python device_server.py 60 | 61 | it will block, which is fine, you can make it into a service if you want. It creates 62 | a log file. 63 | 64 | It's confgured to run a "device" server on port 7770 and a "page" server on 7771. Note 65 | that it's listening on all interfaces, so "as-is" don't run this anywhere public. 66 | 67 | Assuming the laptop, desktop, raspberry pi, or whatever it runs on is on the same 68 | network as the micropython device, point the device to this server by editing "app.json". 69 | 70 | When things are working well, the device will connect with the name configured in 71 | "network.json":: 72 | 73 | Device connected: /Device_1 74 | 75 | When the device is connected to the device server, you can point a web page to the 76 | page server, and they will interact. Multiple pages can be connected to a single 77 | device. 78 | 79 | 80 | device_server.log 81 | ----------------- 82 | Not included, but is created by the device_server. You can watch the messages being 83 | exchanged by inspecting this log file. 84 | 85 | 86 | server-gen.sh 87 | ------------- 88 | creating keys for the eventual use of tls, which is currently blocked by memory 89 | consumption issues. 90 | -------------------------------------------------------------------------------- /tests/test_handhake.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, "../dev/") # shadow websockets from [v]env 3 | 4 | from unittest.mock import patch, MagicMock 5 | import pytest 6 | from websockets.client import connect as client_connect 7 | from websockets.server import ( 8 | serve, 9 | make_respkey, 10 | connect as server_connect, 11 | ) 12 | from websockets.protocol import Websocket 13 | from .utils import Stream 14 | 15 | 16 | @pytest.mark.asyncio 17 | @patch('websockets.client.random') 18 | @patch('websockets.client.asyncio') 19 | async def test_websockets_client_connect(asio, random): 20 | 21 | def getrandbits(x): 22 | return 0xA0 23 | 24 | random.getrandbits = getrandbits 25 | 26 | respkey = b"poops" 27 | 28 | server = "thisisnotreal.tld:12345" 29 | path = "weeeee" 30 | 31 | handshake = ( 32 | b"HTTP/1.1 101 Switching Protocols\r\n" 33 | b"Upgrade: websocket\r\n" 34 | b"Connection: Upgrade\r\n" 35 | b"Sec-WebSocket-Accept: " + respkey + b"\r\n" 36 | b"Server: Micropython\r\n" 37 | b"\r\n" 38 | ) 39 | 40 | response = ( 41 | f"GET /{path} HTTP/1.1\r\n".encode('utf-8') + 42 | f"Host: {server}\r\n".encode('utf-8') + 43 | b"Connection: Upgrade\r\n" 44 | b"Upgrade: websocket\r\n" 45 | b"Sec-WebSocket-Key: oKCgoKCgoKCgoKCgoKCgoA==\r\n" 46 | b"Sec-WebSocket-Version: 13\r\n" 47 | b"Origin: http://localhost\r\n" 48 | b"\r\n" 49 | ) 50 | 51 | s = Stream(handshake) 52 | 53 | async def gen_stream(a, b): 54 | return (s, s) 55 | 56 | asio.open_connection = gen_stream 57 | 58 | ws = await client_connect(f"ws://{server}/{path}") 59 | 60 | assert ws.is_client 61 | assert s.sent_buf == response 62 | 63 | 64 | @pytest.mark.asyncio 65 | @patch('websockets.client.random') 66 | async def test_websockets_server_handshake(random): 67 | 68 | def getrandbits(x): 69 | return 0xA0 70 | 71 | random.getrandbits = getrandbits 72 | path = "psyche" 73 | server = "whateveriwant.neato" 74 | 75 | handshake = ( 76 | f"GET /{path} HTTP/1.1\r\n".encode('utf-8') + 77 | f"Host: {server}\r\n".encode('utf-8') + 78 | b"Connection: Upgrade\r\n" 79 | b"Upgrade: websocket\r\n" 80 | b"Sec-WebSocket-Key: oKCgoKCgoKCgoKCgoKCgoA==\r\n" 81 | b"Sec-WebSocket-Version: 13\r\n" 82 | b"Origin: http://localhost\r\n" 83 | b"\r\n" 84 | ) 85 | 86 | response = ( 87 | b"HTTP/1.1 101 Switching Protocols\r\n" 88 | b"Upgrade: websocket\r\n" 89 | b"Connection: Upgrade\r\n" 90 | b"Sec-WebSocket-Accept: L6fPoOJVlqyp7GKQkh0bqGA3nGw=\r\n" 91 | b"Server: Micropython\r\n" 92 | b"\r\n" 93 | ) 94 | s = Stream(handshake) 95 | 96 | async def on_connect(ws, path): 97 | pass 98 | 99 | await server_connect(s, s, on_connect) 100 | 101 | assert s.sent_buf == response 102 | 103 | 104 | @pytest.mark.asyncio 105 | @patch('websockets.client.random') 106 | @patch('websockets.client.asyncio') 107 | async def test_websocket_server_nokey(asio, random): 108 | 109 | def getrandbits(x): 110 | return 0xA0 111 | 112 | random.getrandbits = getrandbits 113 | 114 | server = "thisisnotreal.tld:12345" 115 | path = "path" 116 | 117 | handshake = ( 118 | f"GET /{path} HTTP/1.1\r\n".encode('utf-8') + 119 | f"Host: {server}\r\n".encode('utf-8') + 120 | b"Connection: Upgrade\r\n" 121 | b"Upgrade: websocket\r\n" 122 | b"Sec-WebSocket-Version: 13\r\n" 123 | b"Origin: http://localhost\r\n" 124 | b"\r\n" 125 | ) 126 | 127 | s = Stream(handshake) 128 | 129 | async def on_connect(ws, path): 130 | pass 131 | 132 | await server_connect(s, s, on_connect) 133 | 134 | assert s.closed 135 | assert s.did_wait_closed 136 | 137 | 138 | @pytest.mark.skip("donno how to test that 3 line function lmao, pls hlp") 139 | @pytest.mark.asyncio 140 | async def test_websocket_server_serve(): 141 | 142 | out = {} 143 | 144 | async def on_connect(ws, path): 145 | out['ws'] = ws 146 | out['path'] = path 147 | 148 | host = "localhost" 149 | port = 9878 150 | path = "whatever" 151 | 152 | await serve(on_connect, host, port) 153 | 154 | s = Stream() 155 | async def gen_stream(a, b): 156 | return (s, s) 157 | 158 | wsc = await client_connect(f"ws://{host}:{port}/{path}") 159 | -------------------------------------------------------------------------------- /dev/apps/demo/ws.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import uasyncio 3 | import json 4 | import time 5 | from controllers import ( 6 | config, 7 | net, 8 | ap, 9 | fake_interface, 10 | ) 11 | from websockets import client 12 | 13 | 14 | cid = 0 15 | clients = set() 16 | tasks = {} 17 | 18 | iam = config['device']['iam'] 19 | dev = config['device']['dev'] 20 | 21 | state = { 22 | "iam": iam, 23 | "dev": dev, 24 | "fake_interface/running": False, 25 | } 26 | 27 | 28 | async def send(data): 29 | # print(data) 30 | await uasyncio.gather(*[cl.send(data) for cl in clients]) 31 | 32 | 33 | async def wifi(ws): 34 | await ws.send('{"info": "Querying networking..."}') 35 | if ap: 36 | await ws.send('{{"wifi/ap/active": "{}"}}'.format(ap.active())) 37 | await ws.send('{{"wifi/ap/ssid": "{}"}}'.format(ap.config('essid'))) 38 | else: 39 | await ws.send('{{"wifi/ap/active": "{}"}}'.format(False)) 40 | if net: 41 | await ws.send('{{"wifi/net/active": "{}"}}'.format(net.active())) 42 | mac = (("{:02X}:"*6)[:-1]).format(*net.config('mac')) 43 | await ws.send('{{"wifi/net/mac": "{}"}}'.format(mac)) 44 | await ws.send('{{"wifi/net/ap": "{}"}}'.format(net.config('essid'))) 45 | connected = net.isconnected() 46 | await ws.send('{{"wifi/net/connected": "{}"}}'.format(connected)) 47 | if connected: 48 | ip, mask, gw, dns = net.ifconfig() 49 | await ws.send('{{"wifi/net/ip": "{}"}}'.format(ip)) 50 | await ws.send('{{"wifi/net/gw": "{}"}}'.format(gw)) 51 | await ws.send('{{"wifi/net/dns": "{}"}}'.format(dns)) 52 | 53 | else: 54 | await ws.send('{{"wifi/net/active": "{}"}}'.format(False)) 55 | await ws.send('{"info": "Done"}') 56 | 57 | 58 | async def scan_new_network(sta_ssid): 59 | if not net.active(): 60 | net.active(1) 61 | 62 | for an_ap in net.scan(): 63 | if an_ap[0] == sta_ssid.encode('ascii'): 64 | return True 65 | return False 66 | 67 | 68 | async def watch_connection(): 69 | start_time = time.time() 70 | while time.time() - start_time < 10: 71 | if net.isconnected(): 72 | return True 73 | await uasyncio.sleep_ms(500) 74 | return False 75 | 76 | 77 | async def test_new_sta(args=None): 78 | if not args or len(args) < 1 or len(args) > 2: 79 | await send('{"error": "Bad args for wifi, need at least 1, no more than 2"}') 80 | return 81 | 82 | ssid = args[0] 83 | wifi_pass = None 84 | if len(args) == 2: 85 | wifi_pass = args[1] 86 | if wifi_pass == "": 87 | wifi_pass = None 88 | 89 | await send('{"info": "Starting network scan"}') 90 | scan_result = await scan_new_network(ssid) 91 | 92 | if scan_result: 93 | await send('{"info": "Network Found"}') 94 | await send('{"info": "Attempting Connection"}') 95 | net.disconnect() 96 | try: 97 | if wifi_pass: 98 | net.connect(ssid, wifi_pass) 99 | else: 100 | net.connect(ssid) 101 | except Exception: 102 | net.connect() 103 | return 104 | else: 105 | await send('{"wifi/test/result": "Fail"}') 106 | await send('{{"info": "Network {} Not Found"}}'.format(ssid)) 107 | return 108 | 109 | con_ssid = net.config("essid") 110 | success = "Success" if con_ssid == ssid else "Fail" 111 | if net.isconnected(): 112 | await send('{{"wifi/test/result": "{}"}}'.format(success)) 113 | ip, mask, gw, dns = net.ifconfig() 114 | await send('{{"wifi/sta/new_test": "{}"}}'.format(True)) 115 | await send('{{"wifi/sta/connected": "{}"}}'.format(True)) 116 | await send('{{"wifi/sta/ip": "{}"}}'.format(ip)) 117 | await send('{{"wifi/sta/gw": "{}"}}'.format(gw)) 118 | await send('{{"wifi/sta/dns": "{}"}}'.format(dns)) 119 | else: 120 | net.connect() 121 | 122 | 123 | async def send_message(args=None): 124 | if not args or len(args) > 1: 125 | await send('{"error": "Bad args for Send Message"}') 126 | return 127 | 128 | if 'fake_interface' not in tasks: 129 | await send('{"error": "Fake Interface not running"}') 130 | 131 | fake_interface.send(args[0]) 132 | 133 | 134 | async def run_fake_interface(): 135 | while True: 136 | fake_interface.run() 137 | while fake_interface.to_rec: 138 | msg = fake_interface.recv() 139 | await send('{{"fake_interface/msg": "{}"}}'.format(msg)) 140 | await uasyncio.sleep_ms(250) 141 | 142 | 143 | async def start_fake_interface(args=None): 144 | if 'fake_interface' in tasks: 145 | return 146 | 147 | tasks['fake_interface'] = uasyncio.create_task(run_fake_interface()) 148 | state['fake_interface/running'] = True 149 | 150 | 151 | async def stop_fake_interface(args=None): 152 | if 'fake_interface' not in tasks: 153 | return 154 | else: 155 | task = tasks.pop('fake_interface') 156 | task.cancel() 157 | state['fake_interface/running'] = False 158 | 159 | 160 | get_router = { 161 | 'wifi': wifi, 162 | } 163 | 164 | cmd_router = { 165 | 'test_new_sta': test_new_sta, 166 | 'send_message': send_message, 167 | 'start_interface': start_fake_interface, 168 | 'stop_interface': stop_fake_interface, 169 | } 170 | 171 | 172 | async def process(ws, data): 173 | print("Handling: {}".format(data)) 174 | gc.collect() 175 | try: 176 | cmd, item, args = json.loads(data) 177 | except Exception as e: 178 | gc.collect() 179 | await ws.send('{"error": "Malformed Request"}') 180 | await ws.send(json.dumps({'error': data})) 181 | await ws.send('{{"error": "{}"}}'.format(e)) 182 | gc.collect() 183 | return 184 | 185 | if cmd == "GET": 186 | if item in get_router: 187 | await get_router[item](ws) 188 | if cmd == "CMD": 189 | if item in cmd_router: 190 | if args: 191 | await cmd_router[item](args) 192 | else: 193 | await cmd_router[item]() 194 | gc.collect() 195 | 196 | 197 | async def register(ws): 198 | print("register") 199 | clients.add(ws) 200 | 201 | 202 | async def unregister(ws): 203 | print("unregister") 204 | clients.remove(ws) 205 | 206 | 207 | async def add_client(ws, path): 208 | print("Client Connection to {}".format(path)) 209 | await register(ws) 210 | 211 | try: 212 | await send(json.dumps(state)) 213 | async for msg in ws: 214 | await process(ws, msg) 215 | await send(json.dumps(state)) 216 | finally: 217 | await unregister(ws) 218 | if not net.isconnected(): 219 | net.connect() 220 | 221 | 222 | async def call_home(ip_addr, port): 223 | uri = "ws://{}:{}/{}".format(ip_addr, port, iam) 224 | connected = False 225 | while True: 226 | if not connected: 227 | try: 228 | ws = await client.connect(uri) 229 | if ws: 230 | connected = True 231 | uasyncio.create_task(add_client(ws, "/" + iam)) 232 | except Exception: 233 | connected = False 234 | else: 235 | connected = ws.open 236 | await uasyncio.sleep(5) 237 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | This repository has been reworked to be an example of how to use tools from my other repositories. 4 | 5 | I'm not realy clear the best way to package this stuff for micropython but also have a repo full 6 | of documentation, examples, and tests. This repo may be that. I'll stick the actual tools in 7 | other, small repos that just have the micropython focussed packages. Comments welcome. 8 | 9 | 10 | Note about TLS 11 | -------------- 12 | TLS support is currently blocked in micropython due to memory constraints. I have begun work 13 | on these tools to support it at least client side and made some attempts for server side support 14 | but until the memory issues are resolved in micropython, the support won't be complete. 15 | 16 | Requirements 17 | ------------ 18 | You'll need the packages from https://github.com/marcidy/micropython-uasyncio-websockets 19 | and https://github.com/marcidy/micropython-uasyncio-http. I install them at the root 20 | of the filesystem (i.e. as /http and /websockets) but as long as they are in your 21 | import path on the device, they ought to work. 22 | 23 | note: I've added thses as submodules so cloning this repositroy and using 24 | ``git submodure init && git submodule update --recursive`` ought to work. 25 | 26 | 27 | /dev 28 | ---- 29 | The /dev directory holds a working example. Nothing *has* to be done this way, I just chose 30 | these methods as examples. My goal is that this directory represents the root of the device 31 | for this example. 32 | 33 | JSON config files 34 | ----------------- 35 | 36 | Just an example of how to manange configuraiton with json files. I have used btree 37 | key/value dbs for the same purpose with success, and NVS partitions are also 38 | candidates. I'll add examples for those down the road. 39 | 40 | networking credentials and locations 41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 42 | These settings are used to identify the device, start an access point, and connect to a 43 | local access point as a wifi station. Angle brackets mean fill this information in. 44 | Feel free to change anything. This file is checked on boot in all cases. 45 | 46 | Look in ``controllers.py`` to see how these settings are consumed. 47 | 48 | network.json:: 49 | 50 | { 51 | "device": { 52 | "iam": "Device_1", 53 | "dev": "ESP32" 54 | }, 55 | "ap": { 56 | "essid": "", 57 | "authmode": 3, 58 | "password": "", 59 | "hidden": false 60 | }, 61 | "sta": { 62 | "ssid": "", 63 | "pass": "" 64 | }, 65 | "wsserver": { 66 | "ip": "192.168.3.1", 67 | "host": 'server.tld' 68 | "port": 7777, 69 | "path": "TS001" 70 | } 71 | } 72 | 73 | 74 | 75 | application configuration 76 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 77 | The default application to load and application settings. The only settings 78 | that need customization are the "home" ip and port in the demo to point to the 79 | machine running the device_server. 80 | 81 | app.json:: 82 | 83 | { 84 | "app": "demo", 85 | "echoserver": { 86 | "listen_addr": "0.0.0.0", 87 | "listen_port": 7777 88 | }, 89 | "demo": { 90 | "http": { 91 | "ip": "0.0.0.0", 92 | "port": 80 93 | }, 94 | "websocket": { 95 | "ip": "0.0.0.0", 96 | "port": 7777 97 | }, 98 | "home": { 99 | "ip": "192.168.3.1", 100 | "port": 7770 101 | } 102 | } 103 | } 104 | 105 | 106 | The value associated with "app" will be loaded by the application loading in 107 | ``main.py``. This value must exists as a key in the file, whose values represt 108 | configuration for that app. 109 | 110 | /certs 111 | ------ 112 | Ignore this directory for now, will hold keys and things for tls implementation 113 | 114 | /www 115 | ---- 116 | web root for html requests to the http server. The demo page is in there and uses 117 | jquery as an example of how to self-contain some helpful js for a nice intro page. 118 | 119 | /controllers.py 120 | --------------- 121 | There are some "important" object instances and initialization routines in here 122 | which are used elsewhere, like the network station and access point interfaces, and a 123 | "fake_interface" which is used in the demo page. 124 | 125 | The fake_interface takes a string and upper cases it. 126 | 127 | This is done via websocket connection between the loaded page and the device, and 128 | represents a way to interact between the device and the page. This could be an 129 | interface to additional hardare, for example. 130 | 131 | The networking interface initialization "works" with micropython v1.19.1. Using 132 | soft-resets (ctrl-D) can cause some errors to be thrown but the initial connection 133 | should be robust. Monitoring the connection is not implemented. 134 | 135 | The ``init()`` function is called on boot to connect the interfaces. 136 | 137 | The ``recovery()`` function is called when booting fails or the application exits 138 | with an unhandled exception. 139 | 140 | /main.py 141 | -------- 142 | main.py does a lot of things differently from how standard python is taught. This 143 | is because it's more systems programming than application programming. 144 | 145 | A default ``app_main(args)`` is defined whose purpose is to run if the import 146 | of the desired application's ``app_main`` fails. All applications (in this 147 | scheme anyways) have the following structure:: 148 | 149 | apps..main.app_main 150 | 151 | where is the application name in app.json. 152 | 153 | main.py trys to load app.json and read the application name:: 154 | 155 | try: 156 | app_cfg = load_app_cfg() 157 | if not app_cfg: 158 | raise ValueError("No app config") 159 | app = app_cfg.get('app') 160 | if not app: 161 | raise ValueError("No app defined in app config") 162 | 163 | 164 | Since we're trying to load an application by variable, the import line is 165 | constructured and run through "exec()":: 166 | 167 | modline = "from apps.{}.main import app_main".format(app) 168 | exec(modline) 169 | 170 | Exceptions aren't handled, just printed. This is becuase there's a severe 171 | unexpected error: the app we want to load isn't loading. 172 | 173 | This is why ``app_main`` was defined. If the app loaded, ``app_main`` would point 174 | to the application we want to run. Since it wasn't loaded, it defaults to run 175 | the ``recovery()`` function as defined:: 176 | 177 | def app_main(args): 178 | ''' a 'default' app_main function which is called if the import from apps 179 | fails ''' 180 | recovery() 181 | 182 | Now app_main is run:: 183 | 184 | try: 185 | app_main(None) 186 | except KeyboardInterrupt: 187 | sys.exit() 188 | except Exception as e: 189 | sys.print_exception(e) 190 | recovery() 191 | except BaseException as e: 192 | sys.print_exception(e) 193 | recovery() 194 | 195 | In this case, a KayboardInterrupt will drop to the shell, while the other two main 196 | classes of exceptions will cause ``recovery()`` to run. 197 | 198 | The application loader does not know or care about the application. The application 199 | ought to handle it's own exceptions. If an excepetion is raised to here, the best 200 | we can do is try to put the device into a recoverable state. 201 | 202 | /apps 203 | ----- 204 | 205 | The applications we intent to run, synced with app.json. 206 | 207 | 208 | /apps/utils.py 209 | ^^^^^^^^^^^^^^ 210 | Some helpers, like what to do for recovery and loading config files only once. 211 | 212 | /apps/echoserver 213 | ^^^^^^^^^^^^^^^^ 214 | Reads the configuration and launches a websocket server which repeats back to what you send. 215 | 216 | Useful for testing as it's simple. Use ``scripts/echo_client.py`` to interact with it from a 217 | different machine on the same network. Make sure the server ip and port match in both. 218 | 219 | /apps/demo 220 | ^^^^^^^^^^ 221 | The main dealy. The device will run a http serer and a websocket server, and will launch a 222 | websocket client attempting to contact the device_server. Run the device_server in the 223 | /scripts directory. 224 | 225 | If you connect to the device access point, or are on the same network as the device, navigate 226 | your web browser to it's ip address:: 227 | 228 | http://192.168.4.1 229 | or 230 | http://192.168.1.100 # or whatever it's ip address is on your network 231 | 232 | If everything is working, you should be greeted with a page which shows you information 233 | about the device and has a card for Fake Interface Example. 234 | 235 | Start the fake interface via the button. 236 | Verify it's running. 237 | Send it a message. 238 | Read the glorified, all capitalized message, fully processed on the device. 239 | 240 | Troubleshooting 241 | --------------- 242 | Oof, sorry you are here. 243 | 244 | There's a lot of output on the device side, there might be helpful information there. 245 | 246 | edit "app.json" so that "app" is now "echoserver" and upload that change to the device and 247 | reboot. Run the echo_client.py in scripts and verify the device and computer are talking 248 | to each other. 249 | 250 | In when running the "demo" app, you can connect to the device as an AP, try that, might be 251 | easier than dealing with all the intermediate networking issues which can arise. 252 | 253 | -------------------------------------------------------------------------------- /dev/www/page.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 62 | 168 | 169 | 170 | Device Configuration 171 |
172 |
Log
173 |
174 |
175 |
176 |
177 |
Device Information
178 |
179 | 180 | 181 | 182 |
Device ID
Device Type
183 |
184 |
185 |
186 |
Wifi Configuration
187 |
188 |
189 |
Access Point
190 |
191 | 192 | 193 | 194 |
Enabled
SSID
195 |
196 |
197 |
198 |
Station
199 |
200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 |
Enabled
MAC
Connected
Connected to AP
IP
Gateway
DNS
209 |
210 |
211 |
212 |
Stored Settings
213 |
214 | 215 | 216 | 217 | 218 | 219 |
Stored SSID:
New SSID:
Password:
Last Test Result:
220 | 221 | 222 |
223 |
224 |
225 |
226 |
227 |
Hardware Drivers
228 |
229 |
230 |
231 |
Fake Interface Example
232 |
233 | 234 | 235 |
Running
236 | 237 |
238 |
Send Message
239 |
240 |
241 | 242 |
243 |
244 |
245 |
Received Messages
246 |
247 |
248 |
249 |
250 |
251 |
252 | 253 | 319 | 320 | 321 | -------------------------------------------------------------------------------- /tests/test_protocol.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, "../dev") # shadow official websockets 3 | 4 | from unittest.mock import patch 5 | import pytest 6 | from websockets.protocol import( 7 | urlparse, 8 | Websocket, 9 | OP_CONT, 10 | OP_TEXT, 11 | OP_BYTES, 12 | OP_CLOSE, 13 | OP_PING, 14 | OP_PONG, 15 | ) 16 | from .utils import Stream 17 | 18 | 19 | def print_frame(frame): 20 | print("") 21 | for i, b in enumerate(frame): 22 | if ((i+1) % 4) != 0: 23 | print(f"{b:08b} ", end="") 24 | else: 25 | print(f"{b:08b}") 26 | 27 | 28 | # class Stream: 29 | # ''' Mimics an uasyncio.stream.Stream which underlies websockets''' 30 | # def __init__(self, 31 | # read_buf=b'', 32 | # write_buf=b'', 33 | # throw=None, 34 | # throw_on_byte=0, 35 | # ): 36 | # self.read_buf = read_buf 37 | # self.write_buf = write_buf 38 | # self.sent_buf = b'' 39 | # self.throw_on_byte = throw_on_byte 40 | # self.throw = throw 41 | # self.bytes_read = 0 42 | # 43 | # async def readexactly(self, n): 44 | # if self.throw: 45 | # if (self.bytes_read + n >= self.throw_on_byte): 46 | # raise self.throw 47 | # 48 | # if n > len(self.read_buf): 49 | # raise ValueError 50 | # 51 | # out = self.read_buf[0:n] 52 | # self.read_buf = self.read_buf[n:] 53 | # return out 54 | # 55 | # async def drain(self): 56 | # self.sent_buf += self.write_buf 57 | # self.write_buf = b'' 58 | # 59 | # def write(self, in_buf): 60 | # self.write_buf += in_buf 61 | # 62 | # async def wait_closed(self): 63 | # if self.throw: 64 | # raise self.throw 65 | 66 | 67 | def test_urlparse(): 68 | # Note: Shouldn't site.tld:port, site.tld, and site.tld/ all have the path 69 | # "/"? 70 | tcs = ( 71 | ( 72 | "ws://localhost", 73 | ("ws", "localhost", 80, None) 74 | ), 75 | ( 76 | "ws://localhost.com:12345/", 77 | ("ws", "localhost.com", 12345, None) 78 | ), 79 | ( 80 | "wss://localhost.com:12345/test", 81 | ("wss", "localhost.com", 12345, "/test") 82 | ), 83 | ) 84 | 85 | for tc, result in tcs: 86 | uri = urlparse(tc) 87 | 88 | assert uri.proto == result[0] 89 | assert uri.hostname == result[1] 90 | assert uri.port == result[2] 91 | assert uri.path == result[3] 92 | 93 | 94 | def test_websocket_create(): 95 | s = Stream() 96 | ws = Websocket(s) 97 | 98 | assert ws.open 99 | assert not ws.fragment 100 | assert ws._stream == s 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_websocket_read(): 105 | fin = 0x80 106 | opcode = OP_TEXT 107 | mask = 0x00 108 | in_data = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05] 109 | length = len(in_data) 110 | 111 | buf = bytes([ 112 | fin | opcode, 113 | mask | length, 114 | *in_data 115 | ]) 116 | 117 | s = Stream(buf) 118 | ws = Websocket(s) 119 | 120 | fin, opcode, data = await ws.read_frame() 121 | 122 | assert fin 123 | assert opcode == 0x01 124 | assert data == bytes(in_data) 125 | assert not len(s.read_buf) 126 | 127 | 128 | @pytest.mark.asyncio 129 | async def test_websocket_masked_hello(): 130 | fin = 0x80 131 | opcode = OP_TEXT 132 | mask = 0x80 133 | mask_data = b"\x00\x00\x00\x00" 134 | in_data = b"Hello!" 135 | length = len(in_data) 136 | 137 | in_buf = bytes([ 138 | fin | opcode, 139 | mask | length, 140 | *mask_data, 141 | *in_data 142 | ]) 143 | 144 | s = Stream(in_buf) 145 | ws = Websocket(s) 146 | 147 | fin, opcode, data = await ws.read_frame() 148 | 149 | assert fin 150 | assert opcode == OP_TEXT 151 | assert data == b"Hello!" 152 | 153 | 154 | @pytest.mark.asyncio 155 | async def test_websocket_magic_length_126(): 156 | ''' Extended payload length case 126, with an ineffective mask (0000) ''' 157 | fin = 0x80 158 | opcode = OP_TEXT 159 | mask = 0x80 160 | mask_data = b'\x00' * 4 161 | magic_length = 126 162 | in_data = b"g" * 255 # need this in 2 bytes, network byte order 163 | actual_length = len(in_data).to_bytes(2, 'big') 164 | 165 | in_buf = bytes([ 166 | fin | opcode, 167 | mask | magic_length, 168 | *actual_length, 169 | *mask_data, 170 | *in_data 171 | ]) 172 | 173 | s = Stream(in_buf) 174 | ws = Websocket(s) 175 | 176 | fin, opcode, data = await ws.read_frame() 177 | 178 | assert fin 179 | assert opcode == OP_TEXT 180 | assert data == in_data 181 | 182 | 183 | @pytest.mark.asyncio 184 | async def test_websocket_magic_length_127(): 185 | '''Extended payload length contniued without mask''' 186 | fin = 0x80 187 | opcode = OP_BYTES 188 | mask = 0x00 189 | magic_length = 127 190 | in_data = b"\x0c" * 1024 191 | actual_length = len(in_data).to_bytes(8, 'big') 192 | 193 | in_buf = bytes([ 194 | fin | opcode, 195 | mask | magic_length, 196 | *actual_length, 197 | *in_data, 198 | ]) 199 | 200 | s = Stream(in_buf) 201 | ws = Websocket(s) 202 | 203 | fin, opcode, data = await ws.read_frame() 204 | 205 | assert fin 206 | assert opcode == OP_BYTES 207 | assert data == in_data 208 | 209 | 210 | # Examples from IETF RFC 6455 sec 5.7 211 | 212 | @pytest.mark.asyncio 213 | async def test_websocket_single_frame_unmasked_text_message(): 214 | in_buf = bytes([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) 215 | s = Stream(in_buf) 216 | ws = Websocket(s) 217 | 218 | fin, opcode, data = await ws.read_frame() 219 | 220 | assert fin 221 | assert opcode == OP_TEXT 222 | assert data == b"Hello" 223 | 224 | 225 | @pytest.mark.asyncio 226 | async def test_websocket_single_frame_masked_test_message(): 227 | in_buf = bytes([ 228 | 0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58 229 | ]) 230 | s = Stream(in_buf) 231 | ws = Websocket(s) 232 | 233 | fin, opcode, data = await ws.read_frame() 234 | 235 | assert fin 236 | assert opcode == OP_TEXT 237 | assert data == b"Hello" 238 | 239 | 240 | @pytest.mark.asyncio 241 | async def test_websocket_fragmented_unmasked_text_message(): 242 | in_buf1 = bytes([ 0x01, 0x03, 0x48, 0x65, 0x6c ]) 243 | in_buf2 = bytes([ 0x80, 0x02, 0x6c, 0x6f ]) 244 | 245 | s = Stream(in_buf1) 246 | ws = Websocket(s) 247 | 248 | fin1, opcode1, data1 = await ws.read_frame() 249 | s.read_buf = in_buf2 250 | fin2, opcode2, data2 = await ws.read_frame() 251 | 252 | assert not fin1 253 | assert opcode1 == OP_TEXT 254 | assert data1 == b'Hel' 255 | assert fin2 256 | assert opcode2 == OP_CONT 257 | assert data2 == b'lo' 258 | 259 | 260 | @pytest.mark.asyncio 261 | async def test_websocket_ping_request(): 262 | ''' Note this won't send a pong, just testing the input side ''' 263 | in_buf = bytes([ 0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f ]) 264 | 265 | s = Stream(in_buf) 266 | ws = Websocket(s) 267 | 268 | fin, opcode, data = await ws.read_frame() 269 | 270 | assert fin 271 | assert OP_PING 272 | assert data == b"Hello" 273 | 274 | in_buf = bytes([ 275 | 0x8a, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58, ]) 276 | 277 | s = Stream(in_buf) 278 | ws = Websocket(s) 279 | 280 | fin, opcode, data = await ws.read_frame() 281 | 282 | assert fin 283 | assert OP_PING 284 | assert data == b"Hello" 285 | 286 | 287 | @pytest.mark.asyncio 288 | async def test_websocket_long_binary_message(): 289 | in_data = b"\x01" * 256 290 | in_buf = bytes([0x82, 0x7e, 0x01, 0x00, *in_data]) 291 | 292 | s = Stream(in_buf) 293 | ws = Websocket(s) 294 | 295 | fin, opcode, data = await ws.read_frame() 296 | 297 | assert fin 298 | assert opcode == OP_BYTES 299 | assert data == in_data 300 | 301 | 302 | @pytest.mark.asyncio 303 | async def test_websocket_really_long_binary_message(): 304 | in_data = b"\xA5" * 65536 305 | in_buf = bytes([ 306 | 0x82, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 307 | *in_data 308 | ]) 309 | 310 | s = Stream(in_buf) 311 | ws = Websocket(s) 312 | 313 | fin, opcode, data = await ws.read_frame() 314 | 315 | assert fin 316 | assert opcode == OP_BYTES 317 | assert data == in_data 318 | 319 | 320 | @pytest.mark.asyncio 321 | async def test_websocket_throw_memory_error_in_read_frame(): 322 | in_buf = bytes([ 323 | 0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58 324 | ]) 325 | 326 | s = Stream(in_buf, throw=MemoryError, throw_on_byte=5) 327 | ws = Websocket(s) 328 | 329 | fin, opcode, data = await ws.read_frame() 330 | 331 | assert fin 332 | assert opcode == OP_CLOSE 333 | assert not data 334 | 335 | 336 | @pytest.mark.asyncio 337 | async def test_websocket_throw_other_error_in_read_frame(): 338 | in_buf = bytes([ 339 | 0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58 340 | ]) 341 | 342 | s = Stream(in_buf, throw=ValueError) 343 | ws = Websocket(s) 344 | 345 | with pytest.raises(ValueError): 346 | fin, opcode, data = await ws.read_frame() 347 | 348 | 349 | @pytest.mark.asyncio 350 | async def test_websocket_write_client_text(): 351 | in_buf = bytes([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) 352 | 353 | s = Stream() 354 | ws = Websocket(s) 355 | 356 | await ws.send('Hello') 357 | assert s.sent_buf == in_buf 358 | 359 | 360 | @pytest.mark.asyncio 361 | async def test_websocket_write_client_bytes(): 362 | out_buf = bytes([0x82, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) 363 | 364 | s = Stream() 365 | ws = Websocket(s) 366 | 367 | await ws.send(b'Hello') 368 | assert s.sent_buf == out_buf 369 | 370 | 371 | @pytest.mark.asyncio 372 | @patch('websockets.protocol.random') 373 | async def test_websocket_write_masked(random): 374 | 375 | # mask uses random bits, but we aren't testing random, just masking 376 | fin = 0x80 377 | not_rand = int(b'A55AE8E8', 16) 378 | masked = 0x80 379 | length = 0x05 380 | mask = not_rand.to_bytes(4, 'big') 381 | def getrandbits(x): 382 | if x == 32: 383 | return not_rand 384 | else: 385 | raise ValueError() 386 | 387 | random.getrandbits = getrandbits 388 | 389 | # expectation 390 | out_buf = bytes([ 391 | fin | OP_TEXT, 392 | masked | length, 393 | *mask, 394 | 0xED, 0x3F, 0x84, 0x84, 0xCA # masked Hello 395 | ]) 396 | s = Stream() 397 | ws = Websocket(s) 398 | ws.is_client = True # client triggers masking 399 | 400 | await ws.send('Hello') 401 | 402 | assert s.sent_buf == out_buf 403 | 404 | 405 | @pytest.mark.asyncio 406 | async def test_websocket_write_magic_lenght_126(): 407 | in_data = b'f'*255 408 | fin = 0x80 409 | opcode = OP_BYTES 410 | mask = 0x00 411 | actual_length = len(in_data).to_bytes(2, 'big') 412 | 413 | out_buf = bytes([ 414 | fin | opcode, 415 | mask | 126, 416 | *actual_length, 417 | *in_data, 418 | ]) 419 | 420 | s = Stream() 421 | ws = Websocket(s) 422 | 423 | await ws.send(in_data) 424 | 425 | assert s.sent_buf == out_buf 426 | 427 | 428 | @pytest.mark.asyncio 429 | async def test_websocket_write_magic_length_127(): 430 | in_data = "x" * 70000 431 | fin = 0x80 432 | opcode = OP_TEXT 433 | mask = 0x00 434 | actual_length = len(in_data).to_bytes(8, 'big') 435 | 436 | out_buf = bytes([ 437 | fin | opcode, 438 | mask | 127, 439 | *actual_length, 440 | *(in_data.encode('utf-8')), 441 | ]) 442 | 443 | s = Stream() 444 | ws = Websocket(s) 445 | 446 | await ws.send(in_data) 447 | 448 | assert s.sent_buf == out_buf 449 | 450 | 451 | # Websocket.protocol.recv are very similar to read_frame based testcases 452 | 453 | 454 | @pytest.mark.asyncio 455 | async def test_websocket_recv_text(): 456 | fin = 0x80 457 | opcode = OP_TEXT 458 | mask = 0x00 459 | in_data = b'Hello' 460 | length = len(in_data) 461 | 462 | buf = bytes([ 463 | fin | opcode, 464 | mask | length, 465 | *in_data, 466 | ]) 467 | 468 | s = Stream(buf) 469 | ws = Websocket(s) 470 | 471 | data = await ws.recv() 472 | 473 | assert data == in_data.decode('utf-8') 474 | 475 | 476 | @pytest.mark.asyncio 477 | async def test_websocket_recv_bytes(): 478 | fin = 0x80 479 | opcode = OP_BYTES 480 | mask = 0x00 481 | in_data = b'Hello' 482 | length = len(in_data) 483 | 484 | buf = bytes([ 485 | fin | opcode, 486 | mask | length, 487 | *in_data, 488 | ]) 489 | 490 | s = Stream(buf) 491 | ws = Websocket(s) 492 | 493 | data = await ws.recv() 494 | 495 | assert data == in_data 496 | 497 | 498 | @pytest.mark.asyncio 499 | async def test_websocket_recv_close(): 500 | fin = 0x80 501 | opcode = OP_CLOSE 502 | mask = 0x00 503 | length = 0 504 | 505 | buf = bytes([ 506 | fin | opcode, 507 | mask | length, 508 | ]) 509 | 510 | s = Stream(buf) 511 | ws = Websocket(s) 512 | 513 | data = await ws.recv() 514 | 515 | assert not ws.open 516 | assert s.sent_buf == b'\x88\x02\x03\xe8' 517 | 518 | 519 | @pytest.mark.asyncio 520 | async def test_websocket_recv_ping(): 521 | fin = 0x80 522 | opcode = OP_PING 523 | mask = 0x00 524 | in_data = b'weeeeee' 525 | length = len(in_data) 526 | 527 | buf = bytes([ 528 | fin | opcode, 529 | mask | length, 530 | *in_data, 531 | ]) 532 | 533 | s = Stream(buf) 534 | ws = Websocket(s) 535 | 536 | data = await ws.recv() 537 | 538 | assert s.sent_buf == bytes([ 539 | 0x80 | OP_PONG, 540 | mask | length, 541 | *in_data]) 542 | 543 | 544 | @pytest.mark.asyncio 545 | async def test_websocket_recv_pong(): 546 | ''' testing recv doesn't raise, PONGs have no real reqs''' 547 | fin = 0x80 548 | opcode = OP_PONG 549 | mask = 0x00 550 | in_data = b'whatever' 551 | length = len(in_data) 552 | 553 | buf = bytes([ 554 | fin | opcode, 555 | mask | length, 556 | *in_data, 557 | ]) 558 | 559 | s = Stream(buf) 560 | ws = Websocket(s) 561 | 562 | await ws.recv() 563 | 564 | 565 | @pytest.mark.asyncio 566 | async def test_websocket_recv_cont(): 567 | fin1 = 0x00 568 | opcode1 = OP_TEXT 569 | mask1 = 0x00 570 | in_data1 = b"I'm a cont" 571 | length1 = len(in_data1) 572 | 573 | fin2 = 0x80 574 | opcode2 = OP_CONT 575 | mask2 = 0x00 576 | in_data2 = b"inuation frame" 577 | length2 = len(in_data2) 578 | 579 | buf = bytes([ 580 | fin1 | opcode1, 581 | mask1 | length1, 582 | *in_data1, 583 | fin2 | opcode2, 584 | mask2 | length2, 585 | *in_data2, 586 | ]) 587 | 588 | s = Stream(buf) 589 | ws = Websocket(s) 590 | 591 | out = await ws.recv() 592 | 593 | assert out == (in_data1 + in_data2).decode('utf-8') 594 | 595 | 596 | @pytest.mark.asyncio 597 | async def test_websocket_raises_on_bad_opcode(): 598 | fin = 0x80 599 | opcode = 255 600 | mask = 0x00 601 | in_data = b'blah' 602 | length = len(in_data) 603 | 604 | buf = bytes([ 605 | fin | opcode, 606 | mask | length, 607 | *in_data, 608 | ]) 609 | 610 | s = Stream(buf) 611 | ws = Websocket(s) 612 | 613 | with pytest.raises(ValueError) as exc_info: 614 | await ws.recv() 615 | 616 | assert exc_info.value.args[0] == opcode 617 | 618 | 619 | @pytest.mark.asyncio 620 | async def test_websocket_asyncio_interfaces(): 621 | test_msgs = [ 622 | 'hello', 623 | 'how are you', 624 | 'find thanks', 625 | 'goodbye'] 626 | 627 | s = Stream() 628 | ws = Websocket(s) 629 | # borrow send ot create frames for messages 630 | for msg in test_msgs: 631 | await ws.send(msg) 632 | 633 | s.read_buf, s.sent_buf = s.sent_buf, b'' 634 | 635 | out = [] 636 | 637 | async for msg in ws: 638 | out.append(msg) 639 | 640 | assert out == test_msgs 641 | 642 | @pytest.mark.asyncio 643 | async def test_websocket_context_manager_interface(): 644 | test_msgs = [ 645 | 'hello', 646 | 'how are you', 647 | 'find thanks', 648 | 'goodbye'] 649 | 650 | s = Stream() 651 | ws = Websocket(s) 652 | 653 | for msg in test_msgs: 654 | await ws.send(msg) 655 | 656 | s.read_buf, s.sent_buf = s.sent_buf, b'' 657 | out = [] 658 | 659 | with ws as f: 660 | async for msg in ws: 661 | out.append(msg) 662 | 663 | assert not ws.open 664 | assert test_msgs == out 665 | 666 | 667 | @pytest.mark.asyncio 668 | async def test_websocket_send_wrong_object(): 669 | o = object() 670 | s = Stream() 671 | ws = Websocket(s) 672 | 673 | with pytest.raises(TypeError): 674 | await ws.send(o) 675 | 676 | -------------------------------------------------------------------------------- /dev/www/jquery-3.5.1.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0