├── .gitignore ├── LICENSE ├── Multiserver ├── one.jpg ├── test.css ├── test.html ├── three.jpg ├── two.jpg ├── websocket_multi_demo.py └── ws_multiserver.py ├── Poll (ESP32 fix) ├── ws_connection.py └── ws_server.py ├── README.md ├── test.html ├── websocket_demo.py ├── ws_connection.py └── ws_server.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ivan Sevcik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Multiserver/one.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetaRavener/upy-websocket-server/676c24ae196665bee1ae3df41190c66d34027e95/Multiserver/one.jpg -------------------------------------------------------------------------------- /Multiserver/test.css: -------------------------------------------------------------------------------- 1 | #response { 2 | color: blue; 3 | font-weight: bold; 4 | } 5 | 6 | body { 7 | font-size: xx-large 8 | } -------------------------------------------------------------------------------- /Multiserver/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket Client 6 | 7 | 8 | 9 | 10 |
11 | Success Story 12 | Always Test 13 | The Feeling 14 |
15 | 16 | Received:
17 |
18 | (Received content) 19 |
20 | 21 | 22 | 35 | 36 | -------------------------------------------------------------------------------- /Multiserver/three.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetaRavener/upy-websocket-server/676c24ae196665bee1ae3df41190c66d34027e95/Multiserver/three.jpg -------------------------------------------------------------------------------- /Multiserver/two.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetaRavener/upy-websocket-server/676c24ae196665bee1ae3df41190c66d34027e95/Multiserver/two.jpg -------------------------------------------------------------------------------- /Multiserver/websocket_multi_demo.py: -------------------------------------------------------------------------------- 1 | from ws_connection import ClientClosedError 2 | from ws_server import WebSocketClient 3 | from ws_multiserver import WebSocketMultiServer 4 | 5 | 6 | class TestClient(WebSocketClient): 7 | def __init__(self, conn): 8 | super().__init__(conn) 9 | 10 | def process(self): 11 | try: 12 | msg = self.connection.read() 13 | if not msg: 14 | return 15 | msg = msg.decode("utf-8") 16 | items = msg.split(" ") 17 | cmd = items[0] 18 | if cmd == "Hello": 19 | self.connection.write(cmd + " World") 20 | print("Hello World") 21 | except ClientClosedError: 22 | self.connection.close() 23 | 24 | 25 | class TestServer(WebSocketMultiServer): 26 | def __init__(self): 27 | super().__init__("test.html", 10) 28 | 29 | def _make_client(self, conn): 30 | return TestClient(conn) 31 | 32 | 33 | server = TestServer() 34 | server.start() 35 | try: 36 | while True: 37 | server.process_all() 38 | except KeyboardInterrupt: 39 | pass 40 | server.stop() 41 | -------------------------------------------------------------------------------- /Multiserver/ws_multiserver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import websocket_helper 3 | from time import sleep 4 | 5 | from ws_server import WebSocketServer 6 | from ws_connection import WebSocketConnection 7 | 8 | 9 | class WebSocketMultiServer(WebSocketServer): 10 | http_codes = { 11 | 200: "OK", 12 | 404: "Not Found", 13 | 500: "Internal Server Error", 14 | 503: "Service Unavailable" 15 | } 16 | 17 | mime_types = { 18 | "jpg": "image/jpeg", 19 | "jpeg": "image/jpeg", 20 | "png": "image/png", 21 | "gif": "image/gif", 22 | "html": "text/html", 23 | "htm": "text/html", 24 | "css": "text/css", 25 | "js": "application/javascript" 26 | } 27 | 28 | def __init__(self, index_page, max_connections=1): 29 | super().__init__(index_page, max_connections) 30 | dir_idx = index_page.rfind("/") 31 | self._web_dir = index_page[0:dir_idx] if dir_idx > 0 else "/" 32 | 33 | def _accept_conn(self, listen_sock): 34 | cl, remote_addr = self._listen_s.accept() 35 | print("Client connection from:", remote_addr) 36 | 37 | if len(self._clients) >= self._max_connections: 38 | # Maximum connections limit reached 39 | cl.setblocking(True) 40 | self._generate_static_page(cl, 503, "503 Too Many Connections") 41 | return 42 | 43 | requested_file = None 44 | data = cl.recv(64).decode() 45 | if data and "Upgrade: websocket" not in data.split("\r\n") and "GET" == data.split(" ")[0]: 46 | # data should looks like GET /index.html HTTP/1.1\r\nHost: 19" 47 | # requested file is on second position in data, ignore all get parameters after question mark 48 | requested_file = data.split(" ")[1].split("?")[0] 49 | requested_file = self._page if requested_file in [None, "/"] else requested_file 50 | 51 | try: 52 | websocket_helper.server_handshake(cl) 53 | self._clients.append(self._make_client(WebSocketConnection(remote_addr, cl, self.remove_connection))) 54 | except OSError: 55 | if requested_file: 56 | cl.setblocking(True) 57 | self._serve_file(requested_file, cl) 58 | else: 59 | self._generate_static_page(cl, 500, "500 Internal Server Error [2]") 60 | 61 | def _serve_file(self, requested_file, c_socket): 62 | print("### Serving file: {}".format(requested_file)) 63 | try: 64 | # check if file exists in web directory 65 | path = requested_file.split("/") 66 | filename = path[-1] 67 | subdir = "/" + "/".join(path[1:-1]) if len(path) > 2 else "" 68 | 69 | if filename not in os.listdir(self._web_dir + subdir): 70 | self._generate_static_page(c_socket, 404, "404 Not Found") 71 | return 72 | 73 | # Create path based on web root directory 74 | file_path = self._web_dir + requested_file 75 | length = os.stat(file_path)[6] 76 | c_socket.sendall(self._generate_headers(200, file_path, length)) 77 | # Send file by chunks to prevent large memory consumption 78 | chunk_size = 1024 79 | with open(file_path, "rb") as f: 80 | while True: 81 | data = f.read(chunk_size) 82 | c_socket.sendall(data) 83 | if len(data) < chunk_size: 84 | break 85 | sleep(0.1) 86 | c_socket.close() 87 | except OSError: 88 | self._generate_static_page(c_socket, 500, "500 Internal Server Error [2]") 89 | 90 | @staticmethod 91 | def _generate_headers(code, filename=None, length=None): 92 | content_type = "text/html" 93 | 94 | if filename: 95 | ext = filename.split(".")[1] 96 | if ext in WebSocketMultiServer.mime_types: 97 | content_type = WebSocketMultiServer.mime_types[ext] 98 | 99 | # Close connection after completing the request 100 | return "HTTP/1.1 {} {}\n" \ 101 | "Content-Type: {}\n" \ 102 | "Content-Length: {}\n" \ 103 | "Server: ESPServer\n" \ 104 | "Connection: close\n\n".format( 105 | code, WebSocketMultiServer.http_codes[code], content_type, length) 106 | 107 | @staticmethod 108 | def _generate_static_page(sock, code, message): 109 | sock.sendall(WebSocketMultiServer._generate_headers(code)) 110 | sock.sendall("

" + message + "

") 111 | sleep(0.1) 112 | sock.close() 113 | -------------------------------------------------------------------------------- /Poll (ESP32 fix)/ws_connection.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from websocket import websocket 3 | import uselect 4 | 5 | 6 | class ClientClosedError(Exception): 7 | pass 8 | 9 | 10 | class WebSocketConnection: 11 | def __init__(self, addr, s, close_callback): 12 | self.client_close = False 13 | self._need_check = False 14 | 15 | self.address = addr 16 | self.socket = s 17 | self.ws = websocket(s, True) 18 | self.poll = uselect.poll() 19 | self.close_callback = close_callback 20 | 21 | self.socket.setblocking(False) 22 | self.poll.register(self.socket, uselect.POLLIN) 23 | 24 | def read(self): 25 | poll_events = self.poll.poll(0) 26 | 27 | if not poll_events: 28 | return 29 | 30 | # Check the flag for connection hung up 31 | if poll_events[0][1] & uselect.POLLHUP: 32 | self.client_close = True 33 | 34 | msg_bytes = None 35 | try: 36 | msg_bytes = self.ws.read() 37 | except OSError: 38 | self.client_close = True 39 | 40 | # If no bytes => connection closed. See the link below. 41 | # http://stefan.buettcher.org/cs/conn_closed.html 42 | if not msg_bytes or self.client_close: 43 | raise ClientClosedError() 44 | 45 | return msg_bytes 46 | 47 | def write(self, msg): 48 | try: 49 | self.ws.write(msg) 50 | except OSError: 51 | self.client_close = True 52 | 53 | def is_closed(self): 54 | return self.socket is None 55 | 56 | def close(self): 57 | print("Closing connection.") 58 | self.poll.unregister(self.socket) 59 | self.socket.close() 60 | self.socket = None 61 | self.ws = None 62 | if self.close_callback: 63 | self.close_callback(self) 64 | -------------------------------------------------------------------------------- /Poll (ESP32 fix)/ws_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import network 4 | import websocket_helper 5 | import uselect 6 | from time import sleep 7 | from ws_connection import WebSocketConnection, ClientClosedError 8 | 9 | 10 | class WebSocketClient: 11 | def __init__(self, conn): 12 | self.connection = conn 13 | 14 | def process(self): 15 | pass 16 | 17 | 18 | class WebSocketServer: 19 | def __init__(self, page, max_connections=1): 20 | self._listen_s = None 21 | self._listen_poll = None 22 | self._clients = [] 23 | self._max_connections = max_connections 24 | self._page = page 25 | 26 | def _setup_conn(self, port): 27 | self._listen_s = socket.socket() 28 | self._listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 29 | self._listen_poll = uselect.poll() 30 | 31 | ai = socket.getaddrinfo("0.0.0.0", port) 32 | addr = ai[0][4] 33 | 34 | self._listen_s.bind(addr) 35 | self._listen_s.listen(1) 36 | self._listen_poll.register(self._listen_s) 37 | for i in (network.AP_IF, network.STA_IF): 38 | iface = network.WLAN(i) 39 | if iface.active(): 40 | print("WebSocket started on ws://%s:%d" % (iface.ifconfig()[0], port)) 41 | 42 | def _check_new_connections(self, accept_handler): 43 | poll_events = self._listen_poll.poll(0) 44 | if not poll_events: 45 | return 46 | 47 | if poll_events[0][1] & uselect.POLLIN: 48 | accept_handler() 49 | 50 | def _accept_conn(self): 51 | cl, remote_addr = self._listen_s.accept() 52 | print("Client connection from:", remote_addr) 53 | 54 | if len(self._clients) >= self._max_connections: 55 | # Maximum connections limit reached 56 | cl.setblocking(True) 57 | cl.sendall("HTTP/1.1 503 Too many connections\n\n") 58 | cl.sendall("\n") 59 | #TODO: Make sure the data is sent before closing 60 | sleep(0.1) 61 | cl.close() 62 | return 63 | 64 | try: 65 | websocket_helper.server_handshake(cl) 66 | except OSError: 67 | # Not a websocket connection, serve webpage 68 | self._serve_page(cl) 69 | return 70 | 71 | self._clients.append(self._make_client(WebSocketConnection(remote_addr, cl, self.remove_connection))) 72 | 73 | def _make_client(self, conn): 74 | return WebSocketClient(conn) 75 | 76 | def _serve_page(self, sock): 77 | try: 78 | sock.sendall('HTTP/1.1 200 OK\nConnection: close\nServer: WebSocket Server\nContent-Type: text/html\n') 79 | length = os.stat(self._page)[6] 80 | sock.sendall('Content-Length: {}\n\n'.format(length)) 81 | # Process page by lines to avoid large strings 82 | with open(self._page, 'r') as f: 83 | for line in f: 84 | sock.sendall(line) 85 | except OSError: 86 | # Error while serving webpage 87 | pass 88 | sock.close() 89 | 90 | def stop(self): 91 | if self._listen_poll: 92 | self._listen_poll.unregister(self._listen_s) 93 | self._listen_poll = None 94 | if self._listen_s: 95 | self._listen_s.close() 96 | self._listen_s = None 97 | 98 | for client in self._clients: 99 | client.connection.close() 100 | print("Stopped WebSocket server.") 101 | 102 | def start(self, port=80): 103 | if self._listen_s: 104 | self.stop() 105 | self._setup_conn(port) 106 | print("Started WebSocket server.") 107 | 108 | def process_all(self): 109 | self._check_new_connections(self._accept_conn) 110 | 111 | for client in self._clients: 112 | client.process() 113 | 114 | def remove_connection(self, conn): 115 | for client in self._clients: 116 | if client.connection is conn: 117 | self._clients.remove(client) 118 | return 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # upy-websocket-server 2 | Micropython (ESP8266) websocket server implementation. 3 | 4 | Upload all scripts and HTML page to device and execute the `websocket_demo.py` script. 5 | 6 | When client connects to the device, `test.html` is served to him, which in turn makes websocket connection to the device and greets it with `Hello`. The device acknowledges this and replies with ` World` appended, which makes client display `Hello World`. 7 | 8 | New implementations can be made by subclassing `WebSocketClient` and `WebSocketServer` as shown in `websocket_demo.py`. 9 | 10 | ### ESP32 users 11 | Because `setsockopt` can't be used on ESP32 to install event handlers, new implementation using `Poll` class has been made and is available in separate folder. From user perspective, both implementation behave the same. The folder contains only files that are different so don't forget to place other files from root folder on your ESP. 12 | 13 | Also, if you're having problems importing `websocket` class, make sure that it's available in your firmware using following commands. `websocket` class should be listed in the output. 14 | ``` 15 | import websocket # This is importing module, not class 16 | dir(websocket) # List everything that module supports 17 | ``` 18 | 19 | Micropython [revision 0d5bccad](https://github.com/micropython/micropython/commit/0d5bccad) is confirmed to be working correctly. 20 | Thanks for this goes to [@plugowski](https://github.com/plugowski). 21 | 22 | ### Support for multiple files 23 | It is recommended to keep the page served by server simple as the transfer speed is quite slow. Due to this, the basic implementation supports serving only one HTML page and websocket communication. However, if you really need to use multiple files (images, etc.) you can find extended implementation in `Multiserver` folder. You only need `ws_multiserver.py` file. The rest of files are for demo purposes. If you want to try it out, copy all files from the mentioned folder and run `websocket_multi_demo.py`. The demo page consists of 3 images and CSS file which are all served along the HTML page and websocket communication that happens after content is loaded. 24 | 25 | 26 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket Client 6 | 7 | 8 | 9 | Received:
10 |
11 | (Received content) 12 |
13 | 14 | 25 | 26 | -------------------------------------------------------------------------------- /websocket_demo.py: -------------------------------------------------------------------------------- 1 | from ws_connection import ClientClosedError 2 | from ws_server import WebSocketServer, WebSocketClient 3 | 4 | 5 | class TestClient(WebSocketClient): 6 | def __init__(self, conn): 7 | super().__init__(conn) 8 | 9 | def process(self): 10 | try: 11 | msg = self.connection.read() 12 | if not msg: 13 | return 14 | msg = msg.decode("utf-8") 15 | items = msg.split(" ") 16 | cmd = items[0] 17 | if cmd == "Hello": 18 | self.connection.write(cmd + " World") 19 | print("Hello World") 20 | except ClientClosedError: 21 | self.connection.close() 22 | 23 | 24 | class TestServer(WebSocketServer): 25 | def __init__(self): 26 | super().__init__("test.html", 2) 27 | 28 | def _make_client(self, conn): 29 | return TestClient(conn) 30 | 31 | 32 | server = TestServer() 33 | server.start() 34 | try: 35 | while True: 36 | server.process_all() 37 | except KeyboardInterrupt: 38 | pass 39 | server.stop() 40 | -------------------------------------------------------------------------------- /ws_connection.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from websocket import websocket 3 | 4 | 5 | class ClientClosedError(Exception): 6 | pass 7 | 8 | 9 | class WebSocketConnection: 10 | def __init__(self, addr, s, close_callback): 11 | self.client_close = False 12 | self._need_check = False 13 | 14 | self.address = addr 15 | self.socket = s 16 | self.ws = websocket(s, True) 17 | self.close_callback = close_callback 18 | 19 | s.setblocking(False) 20 | s.setsockopt(socket.SOL_SOCKET, 20, self.notify) 21 | 22 | def notify(self, s): 23 | self._need_check = True 24 | 25 | def read(self): 26 | if self._need_check: 27 | self._check_socket_state() 28 | 29 | msg_bytes = None 30 | try: 31 | msg_bytes = self.ws.read() 32 | except OSError: 33 | self.client_close = True 34 | 35 | if not msg_bytes and self.client_close: 36 | raise ClientClosedError() 37 | 38 | return msg_bytes 39 | 40 | def write(self, msg): 41 | try: 42 | self.ws.write(msg) 43 | except OSError: 44 | self.client_close = True 45 | 46 | def _check_socket_state(self): 47 | self._need_check = False 48 | sock_str = str(self.socket) 49 | state_str = sock_str.split(" ")[1] 50 | state = int(state_str.split("=")[1]) 51 | 52 | if state == 4: 53 | self.client_close = True 54 | 55 | def is_closed(self): 56 | return self.socket is None 57 | 58 | def close(self): 59 | print("Closing connection.") 60 | self.socket.setsockopt(socket.SOL_SOCKET, 20, None) 61 | self.socket.close() 62 | self.socket = None 63 | self.ws = None 64 | if self.close_callback: 65 | self.close_callback(self) 66 | -------------------------------------------------------------------------------- /ws_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import network 4 | import websocket_helper 5 | from time import sleep 6 | from ws_connection import WebSocketConnection, ClientClosedError 7 | 8 | 9 | class WebSocketClient: 10 | def __init__(self, conn): 11 | self.connection = conn 12 | 13 | def process(self): 14 | pass 15 | 16 | 17 | class WebSocketServer: 18 | def __init__(self, page, max_connections=1): 19 | self._listen_s = None 20 | self._clients = [] 21 | self._max_connections = max_connections 22 | self._page = page 23 | 24 | def _setup_conn(self, port, accept_handler): 25 | self._listen_s = socket.socket() 26 | self._listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 27 | 28 | ai = socket.getaddrinfo("0.0.0.0", port) 29 | addr = ai[0][4] 30 | 31 | self._listen_s.bind(addr) 32 | self._listen_s.listen(1) 33 | if accept_handler: 34 | self._listen_s.setsockopt(socket.SOL_SOCKET, 20, accept_handler) 35 | for i in (network.AP_IF, network.STA_IF): 36 | iface = network.WLAN(i) 37 | if iface.active(): 38 | print("WebSocket started on ws://%s:%d" % (iface.ifconfig()[0], port)) 39 | 40 | def _accept_conn(self, listen_sock): 41 | cl, remote_addr = listen_sock.accept() 42 | print("Client connection from:", remote_addr) 43 | 44 | if len(self._clients) >= self._max_connections: 45 | # Maximum connections limit reached 46 | cl.setblocking(True) 47 | cl.sendall("HTTP/1.1 503 Too many connections\n\n") 48 | cl.sendall("\n") 49 | #TODO: Make sure the data is sent before closing 50 | sleep(0.1) 51 | cl.close() 52 | return 53 | 54 | try: 55 | websocket_helper.server_handshake(cl) 56 | except OSError: 57 | # Not a websocket connection, serve webpage 58 | self._serve_page(cl) 59 | return 60 | 61 | self._clients.append(self._make_client(WebSocketConnection(remote_addr, cl, self.remove_connection))) 62 | 63 | def _make_client(self, conn): 64 | return WebSocketClient(conn) 65 | 66 | def _serve_page(self, sock): 67 | try: 68 | sock.sendall('HTTP/1.1 200 OK\nConnection: close\nServer: WebSocket Server\nContent-Type: text/html\n') 69 | length = os.stat(self._page)[6] 70 | sock.sendall('Content-Length: {}\n\n'.format(length)) 71 | # Process page by lines to avoid large strings 72 | with open(self._page, 'r') as f: 73 | for line in f: 74 | sock.sendall(line) 75 | except OSError: 76 | # Error while serving webpage 77 | pass 78 | sock.close() 79 | 80 | def stop(self): 81 | if self._listen_s: 82 | self._listen_s.close() 83 | self._listen_s = None 84 | for client in self._clients: 85 | client.connection.close() 86 | print("Stopped WebSocket server.") 87 | 88 | def start(self, port=80): 89 | if self._listen_s: 90 | self.stop() 91 | self._setup_conn(port, self._accept_conn) 92 | print("Started WebSocket server.") 93 | 94 | def process_all(self): 95 | for client in self._clients: 96 | client.process() 97 | 98 | def remove_connection(self, conn): 99 | for client in self._clients: 100 | if client.connection is conn: 101 | self._clients.remove(client) 102 | return 103 | --------------------------------------------------------------------------------