├── .gitignore ├── LICENSE ├── README.md ├── boot.py ├── main.py ├── microdot.py ├── microdot_asyncio.py ├── microdot_asyncio_websocket.py ├── microdot_utemplate.py ├── microdot_websocket.py ├── robot_car.py ├── static ├── css │ ├── custom.css │ └── entireframework.min.css └── js │ └── custom.js └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micropython-wifi-robot-car 2 | A MicroPython powered wifi robot car using MicroDot 3 | 4 | ![Building MicroPython Wifi Robot Car - Featured Image](https://user-images.githubusercontent.com/69466026/206447986-5ecd29d5-69bc-4346-b91c-cce55fb7e927.jpg) 5 | 6 | ## Write Up 7 | [Building a MicroPython Wifi Robot Car](https://www.donskytech.com/building-a-micropython-wifi-robot-car/) 8 | -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | # boot.py -- run on boot-up 2 | import network, utime 3 | 4 | # Replace the following with your WIFI Credentials 5 | SSID = "" 6 | SSI_PASSWORD = "" 7 | 8 | def do_connect(): 9 | import network 10 | sta_if = network.WLAN(network.STA_IF) 11 | if not sta_if.isconnected(): 12 | print('connecting to network...') 13 | sta_if.active(True) 14 | sta_if.connect(SSID, SSI_PASSWORD) 15 | while not sta_if.isconnected(): 16 | pass 17 | print('Connected! Network config:', sta_if.ifconfig()) 18 | 19 | print("Connecting to your wifi...") 20 | do_connect() 21 | 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from microdot_asyncio import Microdot, Response, send_file 2 | from microdot_asyncio_websocket import with_websocket 3 | from microdot_utemplate import render_template 4 | from robot_car import RobotCar 5 | 6 | app = Microdot() 7 | Response.default_content_type = "text/html" 8 | 9 | # Wifi Robot Car Configuration 10 | MAX_POWER_LEVEL = 65535 # 100% 11 | MEDIUM_POWER_LEVEL = 49151 # 75% 12 | MIN_POWER_LEVEL = 32767 # 50% 13 | 14 | enable_pins = [21, 22] 15 | motor_pins = [18, 5, 33, 25] 16 | 17 | robot_car = RobotCar(enable_pins, motor_pins, MEDIUM_POWER_LEVEL) 18 | 19 | car_commands = { 20 | "forward": robot_car.forward, 21 | "reverse": robot_car.reverse, 22 | "left": robot_car.turnLeft, 23 | "right": robot_car.turnRight, 24 | "stop": robot_car.stop 25 | } 26 | 27 | speed_commands = { 28 | "slow-speed": MIN_POWER_LEVEL, 29 | "normal-speed": MEDIUM_POWER_LEVEL, 30 | "fast-speed": MAX_POWER_LEVEL 31 | } 32 | 33 | 34 | # App Route 35 | @app.route("/") 36 | async def index(request): 37 | return render_template("index.html") 38 | 39 | 40 | @app.route("/ws") 41 | @with_websocket 42 | async def executeCarCommands(request, ws): 43 | while True: 44 | websocket_message = await ws.receive() 45 | print(f"receive websocket message : {websocket_message}") 46 | 47 | if "speed" in websocket_message: 48 | new_speed = speed_commands.get(websocket_message) 49 | robot_car.set_speed(new_speed) 50 | else: 51 | command = car_commands.get(websocket_message) 52 | command() 53 | await ws.send("OK") 54 | 55 | 56 | @app.route("/shutdown") 57 | async def shutdown(request): 58 | request.app.shutdown() 59 | return "The server is shutting down..." 60 | 61 | 62 | @app.route("/static/") 63 | def static(request, path): 64 | if ".." in path: 65 | # directory traversal is not allowed 66 | return "Not found", 404 67 | return send_file("static/" + path) 68 | 69 | 70 | if __name__ == "__main__": 71 | try: 72 | app.run() 73 | except KeyboardInterrupt: 74 | robot_car.cleanUp() 75 | -------------------------------------------------------------------------------- /microdot.py: -------------------------------------------------------------------------------- 1 | """ 2 | microdot 3 | -------- 4 | 5 | The ``microdot`` module defines a few classes that help implement HTTP-based 6 | servers for MicroPython and standard Python, with multithreading support for 7 | Python interpreters that support it. 8 | """ 9 | try: 10 | from sys import print_exception 11 | except ImportError: # pragma: no cover 12 | import traceback 13 | 14 | def print_exception(exc): 15 | traceback.print_exc() 16 | try: 17 | import uerrno as errno 18 | except ImportError: 19 | import errno 20 | 21 | concurrency_mode = 'threaded' 22 | 23 | try: # pragma: no cover 24 | import threading 25 | 26 | def create_thread(f, *args, **kwargs): 27 | # use the threading module 28 | threading.Thread(target=f, args=args, kwargs=kwargs).start() 29 | except ImportError: # pragma: no cover 30 | def create_thread(f, *args, **kwargs): 31 | # no threads available, call function synchronously 32 | f(*args, **kwargs) 33 | 34 | concurrency_mode = 'sync' 35 | 36 | try: 37 | import ujson as json 38 | except ImportError: 39 | import json 40 | 41 | try: 42 | import ure as re 43 | except ImportError: 44 | import re 45 | 46 | try: 47 | import usocket as socket 48 | except ImportError: 49 | try: 50 | import socket 51 | except ImportError: # pragma: no cover 52 | socket = None 53 | 54 | MUTED_SOCKET_ERRORS = [ 55 | 32, # Broken pipe 56 | 54, # Connection reset by peer 57 | 104, # Connection reset by peer 58 | 128, # Operation on closed socket 59 | ] 60 | 61 | 62 | def urldecode_str(s): 63 | s = s.replace('+', ' ') 64 | parts = s.split('%') 65 | if len(parts) == 1: 66 | return s 67 | result = [parts[0]] 68 | for item in parts[1:]: 69 | if item == '': 70 | result.append('%') 71 | else: 72 | code = item[:2] 73 | result.append(chr(int(code, 16))) 74 | result.append(item[2:]) 75 | return ''.join(result) 76 | 77 | 78 | def urldecode_bytes(s): 79 | s = s.replace(b'+', b' ') 80 | parts = s.split(b'%') 81 | if len(parts) == 1: 82 | return s.decode() 83 | result = [parts[0]] 84 | for item in parts[1:]: 85 | if item == b'': 86 | result.append(b'%') 87 | else: 88 | code = item[:2] 89 | result.append(bytes([int(code, 16)])) 90 | result.append(item[2:]) 91 | return b''.join(result).decode() 92 | 93 | 94 | def urlencode(s): 95 | return s.replace('+', '%2B').replace(' ', '+').replace( 96 | '%', '%25').replace('?', '%3F').replace('#', '%23').replace( 97 | '&', '%26').replace('=', '%3D') 98 | 99 | 100 | class NoCaseDict(dict): 101 | """A subclass of dictionary that holds case-insensitive keys. 102 | 103 | :param initial_dict: an initial dictionary of key/value pairs to 104 | initialize this object with. 105 | 106 | Example:: 107 | 108 | >>> d = NoCaseDict() 109 | >>> d['Content-Type'] = 'text/html' 110 | >>> print(d['Content-Type']) 111 | text/html 112 | >>> print(d['content-type']) 113 | text/html 114 | >>> print(d['CONTENT-TYPE']) 115 | text/html 116 | >>> del d['cOnTeNt-TyPe'] 117 | >>> print(d) 118 | {} 119 | """ 120 | def __init__(self, initial_dict=None): 121 | super().__init__(initial_dict or {}) 122 | self.keymap = {k.lower(): k for k in self.keys() if k.lower() != k} 123 | 124 | def __setitem__(self, key, value): 125 | kl = key.lower() 126 | key = self.keymap.get(kl, key) 127 | if kl != key: 128 | self.keymap[kl] = key 129 | super().__setitem__(key, value) 130 | 131 | def __getitem__(self, key): 132 | kl = key.lower() 133 | return super().__getitem__(self.keymap.get(kl, kl)) 134 | 135 | def __delitem__(self, key): 136 | kl = key.lower() 137 | super().__delitem__(self.keymap.get(kl, kl)) 138 | 139 | def __contains__(self, key): 140 | kl = key.lower() 141 | return self.keymap.get(kl, kl) in self.keys() 142 | 143 | def get(self, key, default=None): 144 | kl = key.lower() 145 | return super().get(self.keymap.get(kl, kl), default) 146 | 147 | 148 | def mro(cls): # pragma: no cover 149 | """Return the method resolution order of a class. 150 | 151 | This is a helper function that returns the method resolution order of a 152 | class. It is used by Microdot to find the best error handler to invoke for 153 | the raised exception. 154 | 155 | In CPython, this function returns the ``__mro__`` attribute of the class. 156 | In MicroPython, this function implements a recursive depth-first scanning 157 | of the class hierarchy. 158 | """ 159 | if hasattr(cls, 'mro'): 160 | return cls.__mro__ 161 | 162 | def _mro(cls): 163 | m = [cls] 164 | for base in cls.__bases__: 165 | m += _mro(base) 166 | return m 167 | 168 | mro_list = _mro(cls) 169 | 170 | # If a class appears multiple times (due to multiple inheritance) remove 171 | # all but the last occurence. This matches the method resolution order 172 | # of MicroPython, but not CPython. 173 | mro_pruned = [] 174 | for i in range(len(mro_list)): 175 | base = mro_list.pop(0) 176 | if base not in mro_list: 177 | mro_pruned.append(base) 178 | return mro_pruned 179 | 180 | 181 | class MultiDict(dict): 182 | """A subclass of dictionary that can hold multiple values for the same 183 | key. It is used to hold key/value pairs decoded from query strings and 184 | form submissions. 185 | 186 | :param initial_dict: an initial dictionary of key/value pairs to 187 | initialize this object with. 188 | 189 | Example:: 190 | 191 | >>> d = MultiDict() 192 | >>> d['sort'] = 'name' 193 | >>> d['sort'] = 'email' 194 | >>> print(d['sort']) 195 | 'name' 196 | >>> print(d.getlist('sort')) 197 | ['name', 'email'] 198 | """ 199 | def __init__(self, initial_dict=None): 200 | super().__init__() 201 | if initial_dict: 202 | for key, value in initial_dict.items(): 203 | self[key] = value 204 | 205 | def __setitem__(self, key, value): 206 | if key not in self: 207 | super().__setitem__(key, []) 208 | super().__getitem__(key).append(value) 209 | 210 | def __getitem__(self, key): 211 | return super().__getitem__(key)[0] 212 | 213 | def get(self, key, default=None, type=None): 214 | """Return the value for a given key. 215 | 216 | :param key: The key to retrieve. 217 | :param default: A default value to use if the key does not exist. 218 | :param type: A type conversion callable to apply to the value. 219 | 220 | If the multidict contains more than one value for the requested key, 221 | this method returns the first value only. 222 | 223 | Example:: 224 | 225 | >>> d = MultiDict() 226 | >>> d['age'] = '42' 227 | >>> d.get('age') 228 | '42' 229 | >>> d.get('age', type=int) 230 | 42 231 | >>> d.get('name', default='noname') 232 | 'noname' 233 | """ 234 | if key not in self: 235 | return default 236 | value = self[key] 237 | if type is not None: 238 | value = type(value) 239 | return value 240 | 241 | def getlist(self, key, type=None): 242 | """Return all the values for a given key. 243 | 244 | :param key: The key to retrieve. 245 | :param type: A type conversion callable to apply to the values. 246 | 247 | If the requested key does not exist in the dictionary, this method 248 | returns an empty list. 249 | 250 | Example:: 251 | 252 | >>> d = MultiDict() 253 | >>> d.getlist('items') 254 | [] 255 | >>> d['items'] = '3' 256 | >>> d.getlist('items') 257 | ['3'] 258 | >>> d['items'] = '56' 259 | >>> d.getlist('items') 260 | ['3', '56'] 261 | >>> d.getlist('items', type=int) 262 | [3, 56] 263 | """ 264 | if key not in self: 265 | return [] 266 | values = super().__getitem__(key) 267 | if type is not None: 268 | values = [type(value) for value in values] 269 | return values 270 | 271 | 272 | class Request(): 273 | """An HTTP request.""" 274 | #: Specify the maximum payload size that is accepted. Requests with larger 275 | #: payloads will be rejected with a 413 status code. Applications can 276 | #: change this maximum as necessary. 277 | #: 278 | #: Example:: 279 | #: 280 | #: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed 281 | max_content_length = 16 * 1024 282 | 283 | #: Specify the maximum payload size that can be stored in ``body``. 284 | #: Requests with payloads that are larger than this size and up to 285 | #: ``max_content_length`` bytes will be accepted, but the application will 286 | #: only be able to access the body of the request by reading from 287 | #: ``stream``. Set to 0 if you always access the body as a stream. 288 | #: 289 | #: Example:: 290 | #: 291 | #: Request.max_body_length = 4 * 1024 # up to 4KB bodies read 292 | max_body_length = 16 * 1024 293 | 294 | #: Specify the maximum length allowed for a line in the request. Requests 295 | #: with longer lines will not be correctly interpreted. Applications can 296 | #: change this maximum as necessary. 297 | #: 298 | #: Example:: 299 | #: 300 | #: Request.max_readline = 16 * 1024 # 16KB lines allowed 301 | max_readline = 2 * 1024 302 | 303 | class G: 304 | pass 305 | 306 | def __init__(self, app, client_addr, method, url, http_version, headers, 307 | body=None, stream=None, sock=None): 308 | #: The application instance to which this request belongs. 309 | self.app = app 310 | #: The address of the client, as a tuple (host, port). 311 | self.client_addr = client_addr 312 | #: The HTTP method of the request. 313 | self.method = method 314 | #: The request URL, including the path and query string. 315 | self.url = url 316 | #: The path portion of the URL. 317 | self.path = url 318 | #: The query string portion of the URL. 319 | self.query_string = None 320 | #: The parsed query string, as a 321 | #: :class:`MultiDict ` object. 322 | self.args = {} 323 | #: A dictionary with the headers included in the request. 324 | self.headers = headers 325 | #: A dictionary with the cookies included in the request. 326 | self.cookies = {} 327 | #: The parsed ``Content-Length`` header. 328 | self.content_length = 0 329 | #: The parsed ``Content-Type`` header. 330 | self.content_type = None 331 | #: A general purpose container for applications to store data during 332 | #: the life of the request. 333 | self.g = Request.G() 334 | 335 | self.http_version = http_version 336 | if '?' in self.path: 337 | self.path, self.query_string = self.path.split('?', 1) 338 | self.args = self._parse_urlencoded(self.query_string) 339 | 340 | if 'Content-Length' in self.headers: 341 | self.content_length = int(self.headers['Content-Length']) 342 | if 'Content-Type' in self.headers: 343 | self.content_type = self.headers['Content-Type'] 344 | if 'Cookie' in self.headers: 345 | for cookie in self.headers['Cookie'].split(';'): 346 | name, value = cookie.strip().split('=', 1) 347 | self.cookies[name] = value 348 | 349 | self._body = body 350 | self.body_used = False 351 | self._stream = stream 352 | self.stream_used = False 353 | self.sock = sock 354 | self._json = None 355 | self._form = None 356 | self.after_request_handlers = [] 357 | 358 | @staticmethod 359 | def create(app, client_stream, client_addr, client_sock=None): 360 | """Create a request object. 361 | 362 | 363 | :param app: The Microdot application instance. 364 | :param client_stream: An input stream from where the request data can 365 | be read. 366 | :param client_addr: The address of the client, as a tuple. 367 | :param client_sock: The low-level socket associated with the request. 368 | 369 | This method returns a newly created ``Request`` object. 370 | """ 371 | # request line 372 | line = Request._safe_readline(client_stream).strip().decode() 373 | if not line: 374 | return None 375 | method, url, http_version = line.split() 376 | http_version = http_version.split('/', 1)[1] 377 | 378 | # headers 379 | headers = NoCaseDict() 380 | while True: 381 | line = Request._safe_readline(client_stream).strip().decode() 382 | if line == '': 383 | break 384 | header, value = line.split(':', 1) 385 | value = value.strip() 386 | headers[header] = value 387 | 388 | return Request(app, client_addr, method, url, http_version, headers, 389 | stream=client_stream, sock=client_sock) 390 | 391 | def _parse_urlencoded(self, urlencoded): 392 | data = MultiDict() 393 | if len(urlencoded) > 0: 394 | if isinstance(urlencoded, str): 395 | for k, v in [pair.split('=', 1) 396 | for pair in urlencoded.split('&')]: 397 | data[urldecode_str(k)] = urldecode_str(v) 398 | elif isinstance(urlencoded, bytes): # pragma: no branch 399 | for k, v in [pair.split(b'=', 1) 400 | for pair in urlencoded.split(b'&')]: 401 | data[urldecode_bytes(k)] = urldecode_bytes(v) 402 | return data 403 | 404 | @property 405 | def body(self): 406 | """The body of the request, as bytes.""" 407 | if self.stream_used: 408 | raise RuntimeError('Cannot use both stream and body') 409 | if self._body is None: 410 | self._body = b'' 411 | if self.content_length and \ 412 | self.content_length <= Request.max_body_length: 413 | while len(self._body) < self.content_length: 414 | data = self._stream.read( 415 | self.content_length - len(self._body)) 416 | if len(data) == 0: # pragma: no cover 417 | raise EOFError() 418 | self._body += data 419 | self.body_used = True 420 | return self._body 421 | 422 | @property 423 | def stream(self): 424 | """The input stream, containing the request body.""" 425 | if self.body_used: 426 | raise RuntimeError('Cannot use both stream and body') 427 | self.stream_used = True 428 | return self._stream 429 | 430 | @property 431 | def json(self): 432 | """The parsed JSON body, or ``None`` if the request does not have a 433 | JSON body.""" 434 | if self._json is None: 435 | if self.content_type is None: 436 | return None 437 | mime_type = self.content_type.split(';')[0] 438 | if mime_type != 'application/json': 439 | return None 440 | self._json = json.loads(self.body.decode()) 441 | return self._json 442 | 443 | @property 444 | def form(self): 445 | """The parsed form submission body, as a 446 | :class:`MultiDict ` object, or ``None`` if the 447 | request does not have a form submission.""" 448 | if self._form is None: 449 | if self.content_type is None: 450 | return None 451 | mime_type = self.content_type.split(';')[0] 452 | if mime_type != 'application/x-www-form-urlencoded': 453 | return None 454 | self._form = self._parse_urlencoded(self.body) 455 | return self._form 456 | 457 | def after_request(self, f): 458 | """Register a request-specific function to run after the request is 459 | handled. Request-specific after request handlers run at the very end, 460 | after the application's own after request handlers. The function must 461 | take two arguments, the request and response objects. The return value 462 | of the function must be the updated response object. 463 | 464 | Example:: 465 | 466 | @app.route('/') 467 | def index(request): 468 | # register a request-specific after request handler 469 | @req.after_request 470 | def func(request, response): 471 | # ... 472 | return response 473 | 474 | return 'Hello, World!' 475 | """ 476 | self.after_request_handlers.append(f) 477 | return f 478 | 479 | @staticmethod 480 | def _safe_readline(stream): 481 | line = stream.readline(Request.max_readline + 1) 482 | if len(line) > Request.max_readline: 483 | raise ValueError('line too long') 484 | return line 485 | 486 | 487 | class Response(): 488 | """An HTTP response class. 489 | 490 | :param body: The body of the response. If a dictionary or list is given, 491 | a JSON formatter is used to generate the body. If a file-like 492 | object or a generator is given, a streaming response is used. 493 | If a string is given, it is encoded from UTF-8. Else, the 494 | body should be a byte sequence. 495 | :param status_code: The numeric HTTP status code of the response. The 496 | default is 200. 497 | :param headers: A dictionary of headers to include in the response. 498 | :param reason: A custom reason phrase to add after the status code. The 499 | default is "OK" for responses with a 200 status code and 500 | "N/A" for any other status codes. 501 | """ 502 | types_map = { 503 | 'css': 'text/css', 504 | 'gif': 'image/gif', 505 | 'html': 'text/html', 506 | 'jpg': 'image/jpeg', 507 | 'js': 'application/javascript', 508 | 'json': 'application/json', 509 | 'png': 'image/png', 510 | 'txt': 'text/plain', 511 | } 512 | send_file_buffer_size = 1024 513 | 514 | #: The content type to use for responses that do not explicitly define a 515 | #: ``Content-Type`` header. 516 | default_content_type = 'text/plain' 517 | 518 | #: Special response used to signal that a response does not need to be 519 | #: written to the client. Used to exit WebSocket connections cleanly. 520 | already_handled = None 521 | 522 | def __init__(self, body='', status_code=200, headers=None, reason=None): 523 | if body is None and status_code == 200: 524 | body = '' 525 | status_code = 204 526 | self.status_code = status_code 527 | self.headers = NoCaseDict(headers or {}) 528 | self.reason = reason 529 | if isinstance(body, (dict, list)): 530 | self.body = json.dumps(body).encode() 531 | self.headers['Content-Type'] = 'application/json; charset=UTF-8' 532 | elif isinstance(body, str): 533 | self.body = body.encode() 534 | else: 535 | # this applies to bytes, file-like objects or generators 536 | self.body = body 537 | 538 | def set_cookie(self, cookie, value, path=None, domain=None, expires=None, 539 | max_age=None, secure=False, http_only=False): 540 | """Add a cookie to the response. 541 | 542 | :param cookie: The cookie's name. 543 | :param value: The cookie's value. 544 | :param path: The cookie's path. 545 | :param domain: The cookie's domain. 546 | :param expires: The cookie expiration time, as a ``datetime`` object 547 | or a correctly formatted string. 548 | :param max_age: The cookie's ``Max-Age`` value. 549 | :param secure: The cookie's ``secure`` flag. 550 | :param http_only: The cookie's ``HttpOnly`` flag. 551 | """ 552 | http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value) 553 | if path: 554 | http_cookie += '; Path=' + path 555 | if domain: 556 | http_cookie += '; Domain=' + domain 557 | if expires: 558 | if isinstance(expires, str): 559 | http_cookie += '; Expires=' + expires 560 | else: 561 | http_cookie += '; Expires=' + expires.strftime( 562 | '%a, %d %b %Y %H:%M:%S GMT') 563 | if max_age: 564 | http_cookie += '; Max-Age=' + str(max_age) 565 | if secure: 566 | http_cookie += '; Secure' 567 | if http_only: 568 | http_cookie += '; HttpOnly' 569 | if 'Set-Cookie' in self.headers: 570 | self.headers['Set-Cookie'].append(http_cookie) 571 | else: 572 | self.headers['Set-Cookie'] = [http_cookie] 573 | 574 | def complete(self): 575 | if isinstance(self.body, bytes) and \ 576 | 'Content-Length' not in self.headers: 577 | self.headers['Content-Length'] = str(len(self.body)) 578 | if 'Content-Type' not in self.headers: 579 | self.headers['Content-Type'] = self.default_content_type 580 | if 'charset=' not in self.headers['Content-Type']: 581 | self.headers['Content-Type'] += '; charset=UTF-8' 582 | 583 | def write(self, stream): 584 | self.complete() 585 | 586 | # status code 587 | reason = self.reason if self.reason is not None else \ 588 | ('OK' if self.status_code == 200 else 'N/A') 589 | stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format( 590 | status_code=self.status_code, reason=reason).encode()) 591 | 592 | # headers 593 | for header, value in self.headers.items(): 594 | values = value if isinstance(value, list) else [value] 595 | for value in values: 596 | stream.write('{header}: {value}\r\n'.format( 597 | header=header, value=value).encode()) 598 | stream.write(b'\r\n') 599 | 600 | # body 601 | can_flush = hasattr(stream, 'flush') 602 | try: 603 | for body in self.body_iter(): 604 | if isinstance(body, str): # pragma: no cover 605 | body = body.encode() 606 | stream.write(body) 607 | if can_flush: # pragma: no cover 608 | stream.flush() 609 | except OSError as exc: # pragma: no cover 610 | if exc.errno in MUTED_SOCKET_ERRORS: 611 | pass 612 | else: 613 | raise 614 | 615 | def body_iter(self): 616 | if self.body: 617 | if hasattr(self.body, 'read'): 618 | while True: 619 | buf = self.body.read(self.send_file_buffer_size) 620 | if len(buf): 621 | yield buf 622 | if len(buf) < self.send_file_buffer_size: 623 | break 624 | if hasattr(self.body, 'close'): # pragma: no cover 625 | self.body.close() 626 | elif hasattr(self.body, '__next__'): 627 | yield from self.body 628 | else: 629 | yield self.body 630 | 631 | @classmethod 632 | def redirect(cls, location, status_code=302): 633 | """Return a redirect response. 634 | 635 | :param location: The URL to redirect to. 636 | :param status_code: The 3xx status code to use for the redirect. The 637 | default is 302. 638 | """ 639 | if '\x0d' in location or '\x0a' in location: 640 | raise ValueError('invalid redirect URL') 641 | return cls(status_code=status_code, headers={'Location': location}) 642 | 643 | @classmethod 644 | def send_file(cls, filename, status_code=200, content_type=None): 645 | """Send file contents in a response. 646 | 647 | :param filename: The filename of the file. 648 | :param status_code: The 3xx status code to use for the redirect. The 649 | default is 302. 650 | :param content_type: The ``Content-Type`` header to use in the 651 | response. If omitted, it is generated 652 | automatically from the file extension. 653 | 654 | Security note: The filename is assumed to be trusted. Never pass 655 | filenames provided by the user without validating and sanitizing them 656 | first. 657 | """ 658 | if content_type is None: 659 | ext = filename.split('.')[-1] 660 | if ext in Response.types_map: 661 | content_type = Response.types_map[ext] 662 | else: 663 | content_type = 'application/octet-stream' 664 | f = open(filename, 'rb') 665 | return cls(body=f, status_code=status_code, 666 | headers={'Content-Type': content_type}) 667 | 668 | 669 | class URLPattern(): 670 | def __init__(self, url_pattern): 671 | self.url_pattern = url_pattern 672 | self.pattern = '' 673 | self.args = [] 674 | use_regex = False 675 | for segment in url_pattern.lstrip('/').split('/'): 676 | if segment and segment[0] == '<': 677 | if segment[-1] != '>': 678 | raise ValueError('invalid URL pattern') 679 | segment = segment[1:-1] 680 | if ':' in segment: 681 | type_, name = segment.rsplit(':', 1) 682 | else: 683 | type_ = 'string' 684 | name = segment 685 | if type_ == 'string': 686 | pattern = '[^/]+' 687 | elif type_ == 'int': 688 | pattern = '\\d+' 689 | elif type_ == 'path': 690 | pattern = '.+' 691 | elif type_.startswith('re:'): 692 | pattern = type_[3:] 693 | else: 694 | raise ValueError('invalid URL segment type') 695 | use_regex = True 696 | self.pattern += '/({pattern})'.format(pattern=pattern) 697 | self.args.append({'type': type_, 'name': name}) 698 | else: 699 | self.pattern += '/{segment}'.format(segment=segment) 700 | if use_regex: 701 | self.pattern = re.compile('^' + self.pattern + '$') 702 | 703 | def match(self, path): 704 | if isinstance(self.pattern, str): 705 | if path != self.pattern: 706 | return 707 | return {} 708 | g = self.pattern.match(path) 709 | if not g: 710 | return 711 | args = {} 712 | i = 1 713 | for arg in self.args: 714 | value = g.group(i) 715 | if arg['type'] == 'int': 716 | value = int(value) 717 | args[arg['name']] = value 718 | i += 1 719 | return args 720 | 721 | 722 | class HTTPException(Exception): 723 | def __init__(self, status_code, reason=None): 724 | self.status_code = status_code 725 | self.reason = reason or str(status_code) + ' error' 726 | 727 | def __repr__(self): # pragma: no cover 728 | return 'HTTPException: {}'.format(self.status_code) 729 | 730 | 731 | class Microdot(): 732 | """An HTTP application class. 733 | 734 | This class implements an HTTP application instance and is heavily 735 | influenced by the ``Flask`` class of the Flask framework. It is typically 736 | declared near the start of the main application script. 737 | 738 | Example:: 739 | 740 | from microdot import Microdot 741 | 742 | app = Microdot() 743 | """ 744 | 745 | def __init__(self): 746 | self.url_map = [] 747 | self.before_request_handlers = [] 748 | self.after_request_handlers = [] 749 | self.error_handlers = {} 750 | self.shutdown_requested = False 751 | self.debug = False 752 | self.server = None 753 | 754 | def route(self, url_pattern, methods=None): 755 | """Decorator that is used to register a function as a request handler 756 | for a given URL. 757 | 758 | :param url_pattern: The URL pattern that will be compared against 759 | incoming requests. 760 | :param methods: The list of HTTP methods to be handled by the 761 | decorated function. If omitted, only ``GET`` requests 762 | are handled. 763 | 764 | The URL pattern can be a static path (for example, ``/users`` or 765 | ``/api/invoices/search``) or a path with dynamic components enclosed 766 | in ``<`` and ``>`` (for example, ``/users/`` or 767 | ``/invoices//products``). Dynamic path components can also 768 | include a type prefix, separated from the name with a colon (for 769 | example, ``/users/``). The type can be ``string`` (the 770 | default), ``int``, ``path`` or ``re:[regular-expression]``. 771 | 772 | The first argument of the decorated function must be 773 | the request object. Any path arguments that are specified in the URL 774 | pattern are passed as keyword arguments. The return value of the 775 | function must be a :class:`Response` instance, or the arguments to 776 | be passed to this class. 777 | 778 | Example:: 779 | 780 | @app.route('/') 781 | def index(request): 782 | return 'Hello, world!' 783 | """ 784 | def decorated(f): 785 | self.url_map.append( 786 | (methods or ['GET'], URLPattern(url_pattern), f)) 787 | return f 788 | return decorated 789 | 790 | def get(self, url_pattern): 791 | """Decorator that is used to register a function as a ``GET`` request 792 | handler for a given URL. 793 | 794 | :param url_pattern: The URL pattern that will be compared against 795 | incoming requests. 796 | 797 | This decorator can be used as an alias to the ``route`` decorator with 798 | ``methods=['GET']``. 799 | 800 | Example:: 801 | 802 | @app.get('/users/') 803 | def get_user(request, id): 804 | # ... 805 | """ 806 | return self.route(url_pattern, methods=['GET']) 807 | 808 | def post(self, url_pattern): 809 | """Decorator that is used to register a function as a ``POST`` request 810 | handler for a given URL. 811 | 812 | :param url_pattern: The URL pattern that will be compared against 813 | incoming requests. 814 | 815 | This decorator can be used as an alias to the``route`` decorator with 816 | ``methods=['POST']``. 817 | 818 | Example:: 819 | 820 | @app.post('/users') 821 | def create_user(request): 822 | # ... 823 | """ 824 | return self.route(url_pattern, methods=['POST']) 825 | 826 | def put(self, url_pattern): 827 | """Decorator that is used to register a function as a ``PUT`` request 828 | handler for a given URL. 829 | 830 | :param url_pattern: The URL pattern that will be compared against 831 | incoming requests. 832 | 833 | This decorator can be used as an alias to the ``route`` decorator with 834 | ``methods=['PUT']``. 835 | 836 | Example:: 837 | 838 | @app.put('/users/') 839 | def edit_user(request, id): 840 | # ... 841 | """ 842 | return self.route(url_pattern, methods=['PUT']) 843 | 844 | def patch(self, url_pattern): 845 | """Decorator that is used to register a function as a ``PATCH`` request 846 | handler for a given URL. 847 | 848 | :param url_pattern: The URL pattern that will be compared against 849 | incoming requests. 850 | 851 | This decorator can be used as an alias to the ``route`` decorator with 852 | ``methods=['PATCH']``. 853 | 854 | Example:: 855 | 856 | @app.patch('/users/') 857 | def edit_user(request, id): 858 | # ... 859 | """ 860 | return self.route(url_pattern, methods=['PATCH']) 861 | 862 | def delete(self, url_pattern): 863 | """Decorator that is used to register a function as a ``DELETE`` 864 | request handler for a given URL. 865 | 866 | :param url_pattern: The URL pattern that will be compared against 867 | incoming requests. 868 | 869 | This decorator can be used as an alias to the ``route`` decorator with 870 | ``methods=['DELETE']``. 871 | 872 | Example:: 873 | 874 | @app.delete('/users/') 875 | def delete_user(request, id): 876 | # ... 877 | """ 878 | return self.route(url_pattern, methods=['DELETE']) 879 | 880 | def before_request(self, f): 881 | """Decorator to register a function to run before each request is 882 | handled. The decorated function must take a single argument, the 883 | request object. 884 | 885 | Example:: 886 | 887 | @app.before_request 888 | def func(request): 889 | # ... 890 | """ 891 | self.before_request_handlers.append(f) 892 | return f 893 | 894 | def after_request(self, f): 895 | """Decorator to register a function to run after each request is 896 | handled. The decorated function must take two arguments, the request 897 | and response objects. The return value of the function must be an 898 | updated response object. 899 | 900 | Example:: 901 | 902 | @app.after_request 903 | def func(request, response): 904 | # ... 905 | return response 906 | """ 907 | self.after_request_handlers.append(f) 908 | return f 909 | 910 | def errorhandler(self, status_code_or_exception_class): 911 | """Decorator to register a function as an error handler. Error handler 912 | functions for numeric HTTP status codes must accept a single argument, 913 | the request object. Error handler functions for Python exceptions 914 | must accept two arguments, the request object and the exception 915 | object. 916 | 917 | :param status_code_or_exception_class: The numeric HTTP status code or 918 | Python exception class to 919 | handle. 920 | 921 | Examples:: 922 | 923 | @app.errorhandler(404) 924 | def not_found(request): 925 | return 'Not found' 926 | 927 | @app.errorhandler(RuntimeError) 928 | def runtime_error(request, exception): 929 | return 'Runtime error' 930 | """ 931 | def decorated(f): 932 | self.error_handlers[status_code_or_exception_class] = f 933 | return f 934 | return decorated 935 | 936 | def mount(self, subapp, url_prefix=''): 937 | """Mount a sub-application, optionally under the given URL prefix. 938 | 939 | :param subapp: The sub-application to mount. 940 | :param url_prefix: The URL prefix to mount the application under. 941 | """ 942 | for methods, pattern, handler in subapp.url_map: 943 | self.url_map.append( 944 | (methods, URLPattern(url_prefix + pattern.url_pattern), 945 | handler)) 946 | for handler in subapp.before_request_handlers: 947 | self.before_request_handlers.append(handler) 948 | for handler in subapp.after_request_handlers: 949 | self.after_request_handlers.append(handler) 950 | for status_code, handler in subapp.error_handlers.items(): 951 | self.error_handlers[status_code] = handler 952 | 953 | @staticmethod 954 | def abort(status_code, reason=None): 955 | """Abort the current request and return an error response with the 956 | given status code. 957 | 958 | :param status_code: The numeric status code of the response. 959 | :param reason: The reason for the response, which is included in the 960 | response body. 961 | 962 | Example:: 963 | 964 | from microdot import abort 965 | 966 | @app.route('/users/') 967 | def get_user(id): 968 | user = get_user_by_id(id) 969 | if user is None: 970 | abort(404) 971 | return user.to_dict() 972 | """ 973 | raise HTTPException(status_code, reason) 974 | 975 | def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None): 976 | """Start the web server. This function does not normally return, as 977 | the server enters an endless listening loop. The :func:`shutdown` 978 | function provides a method for terminating the server gracefully. 979 | 980 | :param host: The hostname or IP address of the network interface that 981 | will be listening for requests. A value of ``'0.0.0.0'`` 982 | (the default) indicates that the server should listen for 983 | requests on all the available interfaces, and a value of 984 | ``127.0.0.1`` indicates that the server should listen 985 | for requests only on the internal networking interface of 986 | the host. 987 | :param port: The port number to listen for requests. The default is 988 | port 5000. 989 | :param debug: If ``True``, the server logs debugging information. The 990 | default is ``False``. 991 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should 992 | not use TLS. The default is ``None``. 993 | 994 | Example:: 995 | 996 | from microdot import Microdot 997 | 998 | app = Microdot() 999 | 1000 | @app.route('/') 1001 | def index(): 1002 | return 'Hello, world!' 1003 | 1004 | app.run(debug=True) 1005 | """ 1006 | self.debug = debug 1007 | self.shutdown_requested = False 1008 | 1009 | self.server = socket.socket() 1010 | ai = socket.getaddrinfo(host, port) 1011 | addr = ai[0][-1] 1012 | 1013 | if self.debug: # pragma: no cover 1014 | print('Starting {mode} server on {host}:{port}...'.format( 1015 | mode=concurrency_mode, host=host, port=port)) 1016 | self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 1017 | self.server.bind(addr) 1018 | self.server.listen(5) 1019 | 1020 | if ssl: 1021 | self.server = ssl.wrap_socket(self.server, server_side=True) 1022 | 1023 | while not self.shutdown_requested: 1024 | try: 1025 | sock, addr = self.server.accept() 1026 | except OSError as exc: # pragma: no cover 1027 | if exc.errno == errno.ECONNABORTED: 1028 | break 1029 | else: 1030 | print_exception(exc) 1031 | except Exception as exc: # pragma: no cover 1032 | print_exception(exc) 1033 | else: 1034 | create_thread(self.handle_request, sock, addr) 1035 | 1036 | def shutdown(self): 1037 | """Request a server shutdown. The server will then exit its request 1038 | listening loop and the :func:`run` function will return. This function 1039 | can be safely called from a route handler, as it only schedules the 1040 | server to terminate as soon as the request completes. 1041 | 1042 | Example:: 1043 | 1044 | @app.route('/shutdown') 1045 | def shutdown(request): 1046 | request.app.shutdown() 1047 | return 'The server is shutting down...' 1048 | """ 1049 | self.shutdown_requested = True 1050 | 1051 | def find_route(self, req): 1052 | f = 404 1053 | for route_methods, route_pattern, route_handler in self.url_map: 1054 | req.url_args = route_pattern.match(req.path) 1055 | if req.url_args is not None: 1056 | if req.method in route_methods: 1057 | f = route_handler 1058 | break 1059 | else: 1060 | f = 405 1061 | return f 1062 | 1063 | def handle_request(self, sock, addr): 1064 | if not hasattr(sock, 'readline'): # pragma: no cover 1065 | stream = sock.makefile("rwb") 1066 | else: 1067 | stream = sock 1068 | 1069 | req = None 1070 | res = None 1071 | try: 1072 | req = Request.create(self, stream, addr, sock) 1073 | res = self.dispatch_request(req) 1074 | except Exception as exc: # pragma: no cover 1075 | print_exception(exc) 1076 | try: 1077 | if res and res != Response.already_handled: # pragma: no branch 1078 | res.write(stream) 1079 | stream.close() 1080 | except OSError as exc: # pragma: no cover 1081 | if exc.errno in MUTED_SOCKET_ERRORS: 1082 | pass 1083 | else: 1084 | print_exception(exc) 1085 | except Exception as exc: # pragma: no cover 1086 | print_exception(exc) 1087 | if stream != sock: # pragma: no cover 1088 | sock.close() 1089 | if self.shutdown_requested: # pragma: no cover 1090 | self.server.close() 1091 | if self.debug and req: # pragma: no cover 1092 | print('{method} {path} {status_code}'.format( 1093 | method=req.method, path=req.path, 1094 | status_code=res.status_code)) 1095 | 1096 | def dispatch_request(self, req): 1097 | if req: 1098 | if req.content_length > req.max_content_length: 1099 | if 413 in self.error_handlers: 1100 | res = self.error_handlers[413](req) 1101 | else: 1102 | res = 'Payload too large', 413 1103 | else: 1104 | f = self.find_route(req) 1105 | try: 1106 | res = None 1107 | if callable(f): 1108 | for handler in self.before_request_handlers: 1109 | res = handler(req) 1110 | if res: 1111 | break 1112 | if res is None: 1113 | res = f(req, **req.url_args) 1114 | if isinstance(res, tuple): 1115 | body = res[0] 1116 | if isinstance(res[1], int): 1117 | status_code = res[1] 1118 | headers = res[2] if len(res) > 2 else {} 1119 | else: 1120 | status_code = 200 1121 | headers = res[1] 1122 | res = Response(body, status_code, headers) 1123 | elif not isinstance(res, Response): 1124 | res = Response(res) 1125 | for handler in self.after_request_handlers: 1126 | res = handler(req, res) or res 1127 | for handler in req.after_request_handlers: 1128 | res = handler(req, res) or res 1129 | elif f in self.error_handlers: 1130 | res = self.error_handlers[f](req) 1131 | else: 1132 | res = 'Not found', f 1133 | except HTTPException as exc: 1134 | if exc.status_code in self.error_handlers: 1135 | res = self.error_handlers[exc.status_code](req) 1136 | else: 1137 | res = exc.reason, exc.status_code 1138 | except Exception as exc: 1139 | print_exception(exc) 1140 | exc_class = None 1141 | res = None 1142 | if exc.__class__ in self.error_handlers: 1143 | exc_class = exc.__class__ 1144 | else: 1145 | for c in mro(exc.__class__)[1:]: 1146 | if c in self.error_handlers: 1147 | exc_class = c 1148 | break 1149 | if exc_class: 1150 | try: 1151 | res = self.error_handlers[exc_class](req, exc) 1152 | except Exception as exc2: # pragma: no cover 1153 | print_exception(exc2) 1154 | if res is None: 1155 | if 500 in self.error_handlers: 1156 | res = self.error_handlers[500](req) 1157 | else: 1158 | res = 'Internal server error', 500 1159 | else: 1160 | if 400 in self.error_handlers: 1161 | res = self.error_handlers[400](req) 1162 | else: 1163 | res = 'Bad request', 400 1164 | 1165 | if isinstance(res, tuple): 1166 | res = Response(*res) 1167 | elif not isinstance(res, Response): 1168 | res = Response(res) 1169 | return res 1170 | 1171 | 1172 | abort = Microdot.abort 1173 | Response.already_handled = Response() 1174 | redirect = Response.redirect 1175 | send_file = Response.send_file 1176 | -------------------------------------------------------------------------------- /microdot_asyncio.py: -------------------------------------------------------------------------------- 1 | """ 2 | microdot_asyncio 3 | ---------------- 4 | 5 | The ``microdot_asyncio`` module defines a few classes that help implement 6 | HTTP-based servers for MicroPython and standard Python that use ``asyncio`` 7 | and coroutines. 8 | """ 9 | try: 10 | import uasyncio as asyncio 11 | except ImportError: 12 | import asyncio 13 | 14 | try: 15 | import uio as io 16 | except ImportError: 17 | import io 18 | 19 | from microdot import Microdot as BaseMicrodot 20 | from microdot import mro 21 | from microdot import NoCaseDict 22 | from microdot import Request as BaseRequest 23 | from microdot import Response as BaseResponse 24 | from microdot import print_exception 25 | from microdot import HTTPException 26 | from microdot import MUTED_SOCKET_ERRORS 27 | 28 | 29 | def _iscoroutine(coro): 30 | return hasattr(coro, 'send') and hasattr(coro, 'throw') 31 | 32 | 33 | class _AsyncBytesIO: 34 | def __init__(self, data): 35 | self.stream = io.BytesIO(data) 36 | 37 | async def read(self, n=-1): 38 | return self.stream.read(n) 39 | 40 | async def readline(self): # pragma: no cover 41 | return self.stream.readline() 42 | 43 | async def readexactly(self, n): # pragma: no cover 44 | return self.stream.read(n) 45 | 46 | async def readuntil(self, separator=b'\n'): # pragma: no cover 47 | return self.stream.readuntil(separator=separator) 48 | 49 | async def awrite(self, data): # pragma: no cover 50 | return self.stream.write(data) 51 | 52 | async def aclose(self): # pragma: no cover 53 | pass 54 | 55 | 56 | class Request(BaseRequest): 57 | @staticmethod 58 | async def create(app, client_reader, client_writer, client_addr): 59 | """Create a request object. 60 | 61 | :param app: The Microdot application instance. 62 | :param client_reader: An input stream from where the request data can 63 | be read. 64 | :param client_writer: An output stream where the response data can be 65 | written. 66 | :param client_addr: The address of the client, as a tuple. 67 | 68 | This method is a coroutine. It returns a newly created ``Request`` 69 | object. 70 | """ 71 | # request line 72 | line = (await Request._safe_readline(client_reader)).strip().decode() 73 | if not line: 74 | return None 75 | method, url, http_version = line.split() 76 | http_version = http_version.split('/', 1)[1] 77 | 78 | # headers 79 | headers = NoCaseDict() 80 | content_length = 0 81 | while True: 82 | line = (await Request._safe_readline( 83 | client_reader)).strip().decode() 84 | if line == '': 85 | break 86 | header, value = line.split(':', 1) 87 | value = value.strip() 88 | headers[header] = value 89 | if header.lower() == 'content-length': 90 | content_length = int(value) 91 | 92 | # body 93 | body = b'' 94 | if content_length and content_length <= Request.max_body_length: 95 | body = await client_reader.readexactly(content_length) 96 | stream = None 97 | else: 98 | body = b'' 99 | stream = client_reader 100 | 101 | return Request(app, client_addr, method, url, http_version, headers, 102 | body=body, stream=stream, 103 | sock=(client_reader, client_writer)) 104 | 105 | @property 106 | def stream(self): 107 | if self._stream is None: 108 | self._stream = _AsyncBytesIO(self._body) 109 | return self._stream 110 | 111 | @staticmethod 112 | async def _safe_readline(stream): 113 | line = (await stream.readline()) 114 | if len(line) > Request.max_readline: 115 | raise ValueError('line too long') 116 | return line 117 | 118 | 119 | class Response(BaseResponse): 120 | """An HTTP response class. 121 | 122 | :param body: The body of the response. If a dictionary or list is given, 123 | a JSON formatter is used to generate the body. If a file-like 124 | object or an async generator is given, a streaming response is 125 | used. If a string is given, it is encoded from UTF-8. Else, 126 | the body should be a byte sequence. 127 | :param status_code: The numeric HTTP status code of the response. The 128 | default is 200. 129 | :param headers: A dictionary of headers to include in the response. 130 | :param reason: A custom reason phrase to add after the status code. The 131 | default is "OK" for responses with a 200 status code and 132 | "N/A" for any other status codes. 133 | """ 134 | 135 | async def write(self, stream): 136 | self.complete() 137 | 138 | try: 139 | # status code 140 | reason = self.reason if self.reason is not None else \ 141 | ('OK' if self.status_code == 200 else 'N/A') 142 | await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format( 143 | status_code=self.status_code, reason=reason).encode()) 144 | 145 | # headers 146 | for header, value in self.headers.items(): 147 | values = value if isinstance(value, list) else [value] 148 | for value in values: 149 | await stream.awrite('{header}: {value}\r\n'.format( 150 | header=header, value=value).encode()) 151 | await stream.awrite(b'\r\n') 152 | 153 | # body 154 | async for body in self.body_iter(): 155 | if isinstance(body, str): # pragma: no cover 156 | body = body.encode() 157 | await stream.awrite(body) 158 | except OSError as exc: # pragma: no cover 159 | if exc.errno in MUTED_SOCKET_ERRORS or \ 160 | exc.args[0] == 'Connection lost': 161 | pass 162 | else: 163 | raise 164 | 165 | def body_iter(self): 166 | if hasattr(self.body, '__anext__'): 167 | # response body is an async generator 168 | return self.body 169 | 170 | response = self 171 | 172 | class iter: 173 | def __aiter__(self): 174 | if response.body: 175 | self.i = 0 # need to determine type of response.body 176 | else: 177 | self.i = -1 # no response body 178 | return self 179 | 180 | async def __anext__(self): 181 | if self.i == -1: 182 | raise StopAsyncIteration 183 | if self.i == 0: 184 | if hasattr(response.body, 'read'): 185 | self.i = 2 # response body is a file-like object 186 | elif hasattr(response.body, '__next__'): 187 | self.i = 1 # response body is a sync generator 188 | return next(response.body) 189 | else: 190 | self.i = -1 # response body is a plain string 191 | return response.body 192 | elif self.i == 1: 193 | try: 194 | return next(response.body) 195 | except StopIteration: 196 | raise StopAsyncIteration 197 | buf = response.body.read(response.send_file_buffer_size) 198 | if _iscoroutine(buf): # pragma: no cover 199 | buf = await buf 200 | if len(buf) < response.send_file_buffer_size: 201 | self.i = -1 202 | if hasattr(response.body, 'close'): # pragma: no cover 203 | result = response.body.close() 204 | if _iscoroutine(result): 205 | await result 206 | return buf 207 | 208 | return iter() 209 | 210 | 211 | class Microdot(BaseMicrodot): 212 | async def start_server(self, host='0.0.0.0', port=5000, debug=False, 213 | ssl=None): 214 | """Start the Microdot web server as a coroutine. This coroutine does 215 | not normally return, as the server enters an endless listening loop. 216 | The :func:`shutdown` function provides a method for terminating the 217 | server gracefully. 218 | 219 | :param host: The hostname or IP address of the network interface that 220 | will be listening for requests. A value of ``'0.0.0.0'`` 221 | (the default) indicates that the server should listen for 222 | requests on all the available interfaces, and a value of 223 | ``127.0.0.1`` indicates that the server should listen 224 | for requests only on the internal networking interface of 225 | the host. 226 | :param port: The port number to listen for requests. The default is 227 | port 5000. 228 | :param debug: If ``True``, the server logs debugging information. The 229 | default is ``False``. 230 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should 231 | not use TLS. The default is ``None``. 232 | 233 | This method is a coroutine. 234 | 235 | Example:: 236 | 237 | import asyncio 238 | from microdot_asyncio import Microdot 239 | 240 | app = Microdot() 241 | 242 | @app.route('/') 243 | async def index(): 244 | return 'Hello, world!' 245 | 246 | async def main(): 247 | await app.start_server(debug=True) 248 | 249 | asyncio.run(main()) 250 | """ 251 | self.debug = debug 252 | 253 | async def serve(reader, writer): 254 | if not hasattr(writer, 'awrite'): # pragma: no cover 255 | # CPython provides the awrite and aclose methods in 3.8+ 256 | async def awrite(self, data): 257 | self.write(data) 258 | await self.drain() 259 | 260 | async def aclose(self): 261 | self.close() 262 | await self.wait_closed() 263 | 264 | from types import MethodType 265 | writer.awrite = MethodType(awrite, writer) 266 | writer.aclose = MethodType(aclose, writer) 267 | 268 | await self.handle_request(reader, writer) 269 | 270 | if self.debug: # pragma: no cover 271 | print('Starting async server on {host}:{port}...'.format( 272 | host=host, port=port)) 273 | 274 | try: 275 | self.server = await asyncio.start_server(serve, host, port, 276 | ssl=ssl) 277 | except TypeError: 278 | self.server = await asyncio.start_server(serve, host, port) 279 | 280 | while True: 281 | try: 282 | await self.server.wait_closed() 283 | break 284 | except AttributeError: # pragma: no cover 285 | # the task hasn't been initialized in the server object yet 286 | # wait a bit and try again 287 | await asyncio.sleep(0.1) 288 | 289 | def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None): 290 | """Start the web server. This function does not normally return, as 291 | the server enters an endless listening loop. The :func:`shutdown` 292 | function provides a method for terminating the server gracefully. 293 | 294 | :param host: The hostname or IP address of the network interface that 295 | will be listening for requests. A value of ``'0.0.0.0'`` 296 | (the default) indicates that the server should listen for 297 | requests on all the available interfaces, and a value of 298 | ``127.0.0.1`` indicates that the server should listen 299 | for requests only on the internal networking interface of 300 | the host. 301 | :param port: The port number to listen for requests. The default is 302 | port 5000. 303 | :param debug: If ``True``, the server logs debugging information. The 304 | default is ``False``. 305 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should 306 | not use TLS. The default is ``None``. 307 | 308 | Example:: 309 | 310 | from microdot_asyncio import Microdot 311 | 312 | app = Microdot() 313 | 314 | @app.route('/') 315 | async def index(): 316 | return 'Hello, world!' 317 | 318 | app.run(debug=True) 319 | """ 320 | asyncio.run(self.start_server(host=host, port=port, debug=debug, 321 | ssl=ssl)) 322 | 323 | def shutdown(self): 324 | self.server.close() 325 | 326 | async def handle_request(self, reader, writer): 327 | req = None 328 | try: 329 | req = await Request.create(self, reader, writer, 330 | writer.get_extra_info('peername')) 331 | except Exception as exc: # pragma: no cover 332 | print_exception(exc) 333 | 334 | res = await self.dispatch_request(req) 335 | if res != Response.already_handled: # pragma: no branch 336 | await res.write(writer) 337 | try: 338 | await writer.aclose() 339 | except OSError as exc: # pragma: no cover 340 | if exc.errno in MUTED_SOCKET_ERRORS: 341 | pass 342 | else: 343 | raise 344 | if self.debug and req: # pragma: no cover 345 | print('{method} {path} {status_code}'.format( 346 | method=req.method, path=req.path, 347 | status_code=res.status_code)) 348 | 349 | async def dispatch_request(self, req): 350 | if req: 351 | if req.content_length > req.max_content_length: 352 | if 413 in self.error_handlers: 353 | res = await self._invoke_handler( 354 | self.error_handlers[413], req) 355 | else: 356 | res = 'Payload too large', 413 357 | else: 358 | f = self.find_route(req) 359 | try: 360 | res = None 361 | if callable(f): 362 | for handler in self.before_request_handlers: 363 | res = await self._invoke_handler(handler, req) 364 | if res: 365 | break 366 | if res is None: 367 | res = await self._invoke_handler( 368 | f, req, **req.url_args) 369 | if isinstance(res, tuple): 370 | body = res[0] 371 | if isinstance(res[1], int): 372 | status_code = res[1] 373 | headers = res[2] if len(res) > 2 else {} 374 | else: 375 | status_code = 200 376 | headers = res[1] 377 | res = Response(body, status_code, headers) 378 | elif not isinstance(res, Response): 379 | res = Response(res) 380 | for handler in self.after_request_handlers: 381 | res = await self._invoke_handler( 382 | handler, req, res) or res 383 | for handler in req.after_request_handlers: 384 | res = await handler(req, res) or res 385 | elif f in self.error_handlers: 386 | res = await self._invoke_handler( 387 | self.error_handlers[f], req) 388 | else: 389 | res = 'Not found', f 390 | except HTTPException as exc: 391 | if exc.status_code in self.error_handlers: 392 | res = self.error_handlers[exc.status_code](req) 393 | else: 394 | res = exc.reason, exc.status_code 395 | except Exception as exc: 396 | print_exception(exc) 397 | exc_class = None 398 | res = None 399 | if exc.__class__ in self.error_handlers: 400 | exc_class = exc.__class__ 401 | else: 402 | for c in mro(exc.__class__)[1:]: 403 | if c in self.error_handlers: 404 | exc_class = c 405 | break 406 | if exc_class: 407 | try: 408 | res = await self._invoke_handler( 409 | self.error_handlers[exc_class], req, exc) 410 | except Exception as exc2: # pragma: no cover 411 | print_exception(exc2) 412 | if res is None: 413 | if 500 in self.error_handlers: 414 | res = await self._invoke_handler( 415 | self.error_handlers[500], req) 416 | else: 417 | res = 'Internal server error', 500 418 | else: 419 | if 400 in self.error_handlers: 420 | res = await self._invoke_handler(self.error_handlers[400], req) 421 | else: 422 | res = 'Bad request', 400 423 | if isinstance(res, tuple): 424 | res = Response(*res) 425 | elif not isinstance(res, Response): 426 | res = Response(res) 427 | return res 428 | 429 | async def _invoke_handler(self, f_or_coro, *args, **kwargs): 430 | ret = f_or_coro(*args, **kwargs) 431 | if _iscoroutine(ret): 432 | ret = await ret 433 | return ret 434 | 435 | 436 | abort = Microdot.abort 437 | Response.already_handled = Response() 438 | redirect = Response.redirect 439 | send_file = Response.send_file 440 | -------------------------------------------------------------------------------- /microdot_asyncio_websocket.py: -------------------------------------------------------------------------------- 1 | from microdot_asyncio import Response 2 | from microdot_websocket import WebSocket as BaseWebSocket 3 | 4 | 5 | class WebSocket(BaseWebSocket): 6 | async def handshake(self): 7 | response = self._handshake_response() 8 | await self.request.sock[1].awrite( 9 | b'HTTP/1.1 101 Switching Protocols\r\n') 10 | await self.request.sock[1].awrite(b'Upgrade: websocket\r\n') 11 | await self.request.sock[1].awrite(b'Connection: Upgrade\r\n') 12 | await self.request.sock[1].awrite( 13 | b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n') 14 | 15 | async def receive(self): 16 | while True: 17 | opcode, payload = await self._read_frame() 18 | send_opcode, data = self._process_websocket_frame(opcode, payload) 19 | if send_opcode: # pragma: no cover 20 | await self.send(send_opcode, data) 21 | elif data: # pragma: no branch 22 | return data 23 | 24 | async def send(self, data, opcode=None): 25 | frame = self._encode_websocket_frame( 26 | opcode or (self.TEXT if isinstance(data, str) else self.BINARY), 27 | data) 28 | await self.request.sock[1].awrite(frame) 29 | 30 | async def close(self): 31 | if not self.closed: # pragma: no cover 32 | self.closed = True 33 | await self.send(b'', self.CLOSE) 34 | 35 | async def _read_frame(self): 36 | header = await self.request.sock[0].read(2) 37 | if len(header) != 2: # pragma: no cover 38 | raise OSError(32, 'Websocket connection closed') 39 | fin, opcode, has_mask, length = self._parse_frame_header(header) 40 | if length == -2: 41 | length = await self.request.sock[0].read(2) 42 | length = int.from_bytes(length, 'big') 43 | elif length == -8: 44 | length = await self.request.sock[0].read(8) 45 | length = int.from_bytes(length, 'big') 46 | if has_mask: # pragma: no cover 47 | mask = await self.request.sock[0].read(4) 48 | payload = await self.request.sock[0].read(length) 49 | if has_mask: # pragma: no cover 50 | payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload)) 51 | return opcode, payload 52 | 53 | 54 | async def websocket_upgrade(request): 55 | """Upgrade a request handler to a websocket connection. 56 | 57 | This function can be called directly inside a route function to process a 58 | WebSocket upgrade handshake, for example after the user's credentials are 59 | verified. The function returns the websocket object:: 60 | 61 | @app.route('/echo') 62 | async def echo(request): 63 | if not authenticate_user(request): 64 | abort(401) 65 | ws = await websocket_upgrade(request) 66 | while True: 67 | message = await ws.receive() 68 | await ws.send(message) 69 | """ 70 | ws = WebSocket(request) 71 | await ws.handshake() 72 | 73 | @request.after_request 74 | async def after_request(request, response): 75 | return Response.already_handled 76 | 77 | return ws 78 | 79 | 80 | def with_websocket(f): 81 | """Decorator to make a route a WebSocket endpoint. 82 | 83 | This decorator is used to define a route that accepts websocket 84 | connections. The route then receives a websocket object as a second 85 | argument that it can use to send and receive messages:: 86 | 87 | @app.route('/echo') 88 | @with_websocket 89 | async def echo(request, ws): 90 | while True: 91 | message = await ws.receive() 92 | await ws.send(message) 93 | """ 94 | async def wrapper(request, *args, **kwargs): 95 | ws = await websocket_upgrade(request) 96 | try: 97 | await f(request, ws, *args, **kwargs) 98 | await ws.close() # pragma: no cover 99 | except OSError as exc: 100 | if exc.errno not in [32, 54, 104]: # pragma: no cover 101 | raise 102 | return '' 103 | return wrapper 104 | -------------------------------------------------------------------------------- /microdot_utemplate.py: -------------------------------------------------------------------------------- 1 | from utemplate import recompile 2 | 3 | _loader = None 4 | 5 | 6 | def init_templates(template_dir='templates', loader_class=recompile.Loader): 7 | """Initialize the templating subsystem. 8 | 9 | :param template_dir: the directory where templates are stored. This 10 | argument is optional. The default is to load templates 11 | from a *templates* subdirectory. 12 | :param loader_class: the ``utemplate.Loader`` class to use when loading 13 | templates. This argument is optional. The default is 14 | the ``recompile.Loader`` class, which automatically 15 | recompiles templates when they change. 16 | """ 17 | global _loader 18 | _loader = loader_class(None, template_dir) 19 | 20 | 21 | def render_template(template, *args, **kwargs): 22 | """Render a template. 23 | 24 | :param template: The filename of the template to render, relative to the 25 | configured template directory. 26 | :param args: Positional arguments to be passed to the render engine. 27 | :param kwargs: Keyword arguments to be passed to the render engine. 28 | 29 | The return value is an iterator that returns sections of rendered template. 30 | """ 31 | if _loader is None: # pragma: no cover 32 | init_templates() 33 | render = _loader.load(template) 34 | return render(*args, **kwargs) 35 | -------------------------------------------------------------------------------- /microdot_websocket.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import hashlib 3 | from microdot import Response 4 | 5 | 6 | class WebSocket: 7 | CONT = 0 8 | TEXT = 1 9 | BINARY = 2 10 | CLOSE = 8 11 | PING = 9 12 | PONG = 10 13 | 14 | def __init__(self, request): 15 | self.request = request 16 | self.closed = False 17 | 18 | def handshake(self): 19 | response = self._handshake_response() 20 | self.request.sock.send(b'HTTP/1.1 101 Switching Protocols\r\n') 21 | self.request.sock.send(b'Upgrade: websocket\r\n') 22 | self.request.sock.send(b'Connection: Upgrade\r\n') 23 | self.request.sock.send( 24 | b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n') 25 | 26 | def receive(self): 27 | while True: 28 | opcode, payload = self._read_frame() 29 | send_opcode, data = self._process_websocket_frame(opcode, payload) 30 | if send_opcode: # pragma: no cover 31 | self.send(send_opcode, data) 32 | elif data: # pragma: no branch 33 | return data 34 | 35 | def send(self, data, opcode=None): 36 | frame = self._encode_websocket_frame( 37 | opcode or (self.TEXT if isinstance(data, str) else self.BINARY), 38 | data) 39 | self.request.sock.send(frame) 40 | 41 | def close(self): 42 | if not self.closed: # pragma: no cover 43 | self.closed = True 44 | self.send(b'', self.CLOSE) 45 | 46 | def _handshake_response(self): 47 | connection = False 48 | upgrade = False 49 | websocket_key = None 50 | for header, value in self.request.headers.items(): 51 | h = header.lower() 52 | if h == 'connection': 53 | connection = True 54 | if 'upgrade' not in value.lower(): 55 | return self.request.app.abort(400) 56 | elif h == 'upgrade': 57 | upgrade = True 58 | if not value.lower() == 'websocket': 59 | return self.request.app.abort(400) 60 | elif h == 'sec-websocket-key': 61 | websocket_key = value 62 | if not connection or not upgrade or not websocket_key: 63 | return self.request.app.abort(400) 64 | d = hashlib.sha1(websocket_key.encode()) 65 | d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11') 66 | return binascii.b2a_base64(d.digest())[:-1] 67 | 68 | @classmethod 69 | def _parse_frame_header(cls, header): 70 | fin = header[0] & 0x80 71 | opcode = header[0] & 0x0f 72 | if fin == 0 or opcode == cls.CONT: # pragma: no cover 73 | raise OSError(32, 'Continuation frames not supported') 74 | has_mask = header[1] & 0x80 75 | length = header[1] & 0x7f 76 | if length == 126: 77 | length = -2 78 | elif length == 127: 79 | length = -8 80 | return fin, opcode, has_mask, length 81 | 82 | def _process_websocket_frame(self, opcode, payload): 83 | if opcode == self.TEXT: 84 | payload = payload.decode() 85 | elif opcode == self.BINARY: 86 | pass 87 | elif opcode == self.CLOSE: 88 | raise OSError(32, 'Websocket connection closed') 89 | elif opcode == self.PING: 90 | return self.PONG, payload 91 | elif opcode == self.PONG: # pragma: no branch 92 | return None, None 93 | return None, payload 94 | 95 | @classmethod 96 | def _encode_websocket_frame(cls, opcode, payload): 97 | frame = bytearray() 98 | frame.append(0x80 | opcode) 99 | if opcode == cls.TEXT: 100 | payload = payload.encode() 101 | if len(payload) < 126: 102 | frame.append(len(payload)) 103 | elif len(payload) < (1 << 16): 104 | frame.append(126) 105 | frame.extend(len(payload).to_bytes(2, 'big')) 106 | else: 107 | frame.append(127) 108 | frame.extend(len(payload).to_bytes(8, 'big')) 109 | frame.extend(payload) 110 | return frame 111 | 112 | def _read_frame(self): 113 | header = self.request.sock.recv(2) 114 | if len(header) != 2: # pragma: no cover 115 | raise OSError(32, 'Websocket connection closed') 116 | fin, opcode, has_mask, length = self._parse_frame_header(header) 117 | if length < 0: 118 | length = self.request.sock.recv(-length) 119 | length = int.from_bytes(length, 'big') 120 | if has_mask: # pragma: no cover 121 | mask = self.request.sock.recv(4) 122 | payload = self.request.sock.recv(length) 123 | if has_mask: # pragma: no cover 124 | payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload)) 125 | return opcode, payload 126 | 127 | 128 | def websocket_upgrade(request): 129 | """Upgrade a request handler to a websocket connection. 130 | 131 | This function can be called directly inside a route function to process a 132 | WebSocket upgrade handshake, for example after the user's credentials are 133 | verified. The function returns the websocket object:: 134 | 135 | @app.route('/echo') 136 | def echo(request): 137 | if not authenticate_user(request): 138 | abort(401) 139 | ws = websocket_upgrade(request) 140 | while True: 141 | message = ws.receive() 142 | ws.send(message) 143 | """ 144 | ws = WebSocket(request) 145 | ws.handshake() 146 | 147 | @request.after_request 148 | def after_request(request, response): 149 | return Response.already_handled 150 | 151 | return ws 152 | 153 | 154 | def with_websocket(f): 155 | """Decorator to make a route a WebSocket endpoint. 156 | 157 | This decorator is used to define a route that accepts websocket 158 | connections. The route then receives a websocket object as a second 159 | argument that it can use to send and receive messages:: 160 | 161 | @app.route('/echo') 162 | @with_websocket 163 | def echo(request, ws): 164 | while True: 165 | message = ws.receive() 166 | ws.send(message) 167 | """ 168 | def wrapper(request, *args, **kwargs): 169 | ws = websocket_upgrade(request) 170 | try: 171 | f(request, ws, *args, **kwargs) 172 | ws.close() # pragma: no cover 173 | except OSError as exc: 174 | if exc.errno not in [32, 54, 104]: # pragma: no cover 175 | raise 176 | return '' 177 | return wrapper 178 | -------------------------------------------------------------------------------- /robot_car.py: -------------------------------------------------------------------------------- 1 | from machine import Pin, PWM 2 | 3 | """ 4 | Class to represent our robot car 5 | """ 6 | class RobotCar(): 7 | def __init__(self, enable_pins, motor_pins, speed): 8 | self.right_motor_enable_pin = PWM(Pin(enable_pins[0]), freq=2000) 9 | self.left_motor_enable_pin = PWM(Pin(enable_pins[1]), freq=2000) 10 | 11 | self.right_motor_control_1 = Pin(motor_pins[0], Pin.OUT) 12 | self.right_motor_control_2 = Pin(motor_pins[1], Pin.OUT) 13 | 14 | self.left_motor_control_1 = Pin(motor_pins[2], Pin.OUT) 15 | self.left_motor_control_2 = Pin(motor_pins[3], Pin.OUT) 16 | 17 | self.speed = speed 18 | 19 | def stop(self): 20 | print('Car stopping') 21 | self.right_motor_control_1.value(0) 22 | self.right_motor_control_2.value(0) 23 | self.left_motor_control_1.value(0) 24 | self.left_motor_control_2.value(0) 25 | self.right_motor_enable_pin.duty_u16(0) 26 | self.left_motor_enable_pin.duty_u16(0) 27 | 28 | def forward(self): 29 | print('Move forward') 30 | self.right_motor_enable_pin.duty_u16(self.speed) 31 | self.left_motor_enable_pin.duty_u16(self.speed) 32 | 33 | self.right_motor_control_1.value(1) 34 | self.right_motor_control_2.value(0) 35 | self.left_motor_control_1.value(1) 36 | self.left_motor_control_2.value(0) 37 | 38 | 39 | def reverse(self): 40 | print('Move reverse') 41 | self.right_motor_enable_pin.duty_u16(self.speed) 42 | self.left_motor_enable_pin.duty_u16(self.speed) 43 | 44 | self.right_motor_control_1.value(0) 45 | self.right_motor_control_2.value(1) 46 | self.left_motor_control_1.value(0) 47 | self.left_motor_control_2.value(1) 48 | 49 | def turnLeft(self): 50 | print('Turning Left') 51 | self.right_motor_enable_pin.duty_u16(self.speed) 52 | self.left_motor_enable_pin.duty_u16(self.speed) 53 | 54 | self.right_motor_control_1.value(1) 55 | self.right_motor_control_2.value(0) 56 | self.left_motor_control_1.value(0) 57 | self.left_motor_control_2.value(0) 58 | 59 | def turnRight(self): 60 | print('Turning Right') 61 | self.right_motor_enable_pin.duty_u16(self.speed) 62 | self.left_motor_enable_pin.duty_u16(self.speed) 63 | 64 | self.right_motor_control_1.value(0) 65 | self.right_motor_control_2.value(0) 66 | self.left_motor_control_1.value(1) 67 | self.left_motor_control_2.value(0) 68 | 69 | def set_speed(self, new_speed): 70 | self.speed = new_speed 71 | 72 | def cleanUp(self): 73 | print('Cleaning up pins') 74 | self.right_motor_enable_pin.deinit() 75 | self.left_motor_enable_pin.deinit() 76 | -------------------------------------------------------------------------------- /static/css/custom.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Lato:400,500,600,700&display=swap"); 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | /* box-sizing: border-box; */ 6 | font-family: "Lato", sans-serif; 7 | } 8 | 9 | .hero { 10 | background: #eee; 11 | padding: 20px; 12 | border-radius: 10px; 13 | margin-top: 1em; 14 | text-align: center; 15 | } 16 | 17 | .hero h1 { 18 | margin-top: 0; 19 | margin-bottom: 0.3em; 20 | } 21 | 22 | .c4 { 23 | padding: 10px; 24 | box-sizing: border-box; 25 | } 26 | 27 | .c4 h3 { 28 | margin-top: 0; 29 | } 30 | 31 | .c4 a { 32 | margin-top: 10px; 33 | display: inline-block; 34 | } 35 | /************* 36 | Display Grid 37 | ***************/ 38 | /* .grid-wrapper{ 39 | display: grid; 40 | grid-template-columns: 1fr auto; 41 | column-gap: 2px; 42 | } 43 | 44 | .speed-controls{ 45 | margin-top: 30px; 46 | padding: 10px; 47 | }*/ 48 | 49 | .parent { 50 | display: grid; 51 | padding: 1em; 52 | margin-top: 1em; 53 | } 54 | .speed-settings { 55 | /* width: 360px; */ 56 | font-weight: bold; 57 | font-size: larger; 58 | justify-content: center; 59 | margin: auto auto; 60 | background: #fff; 61 | } 62 | .wrapper { 63 | display: flex; 64 | background: #fff; 65 | height: 100px; 66 | /* width: 360px; */ 67 | align-items: center; 68 | justify-content: center; 69 | border-radius: 5px; 70 | /* padding: 20px 15px; */ 71 | box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.2); 72 | margin: auto; 73 | } 74 | .wrapper .option { 75 | background: #fff; 76 | height: 60%; 77 | width: 100%; 78 | display: flex; 79 | align-items: center; 80 | justify-content: space-evenly; 81 | margin: 0 10px; 82 | border-radius: 5px; 83 | cursor: pointer; 84 | padding: 0 10px; 85 | border: 2px solid lightgrey; 86 | transition: all 0.3s ease; 87 | } 88 | .wrapper .option .dot { 89 | height: 20px; 90 | width: 20px; 91 | background: #d9d9d9; 92 | border-radius: 50%; 93 | position: relative; 94 | } 95 | .wrapper .option .dot::before { 96 | position: absolute; 97 | content: ""; 98 | top: 4px; 99 | left: 4px; 100 | width: 12px; 101 | height: 12px; 102 | background: #0069d9; 103 | border-radius: 50%; 104 | opacity: 0; 105 | transform: scale(1.5); 106 | transition: all 0.3s ease; 107 | } 108 | input[type="radio"] { 109 | display: none; 110 | } 111 | #option-1:checked:checked ~ .option-1, 112 | #option-2:checked:checked ~ .option-2, 113 | #option-3:checked:checked ~ .option-3 { 114 | border-color: #0069d9; 115 | background: #0069d9; 116 | } 117 | #option-1:checked:checked ~ .option-1 .dot, 118 | #option-2:checked:checked ~ .option-2 .dot, 119 | #option-3:checked:checked ~ .option-3 .dot { 120 | background: #fff; 121 | } 122 | #option-1:checked:checked ~ .option-1 .dot::before, 123 | #option-2:checked:checked ~ .option-2 .dot::before, 124 | #option-3:checked:checked ~ .option-3 .dot::before { 125 | opacity: 1; 126 | transform: scale(1); 127 | } 128 | .wrapper .option span { 129 | font-size: 20px; 130 | color: #808080; 131 | } 132 | #option-1:checked:checked ~ .option-1 span, 133 | #option-2:checked:checked ~ .option-2 span, 134 | #option-3:checked:checked ~ .option-3 span { 135 | color: #fff; 136 | } 137 | 138 | 139 | /************** 140 | D-PAD - O-PAD 141 | **************/ 142 | .set { 143 | overflow: hidden; 144 | padding: 30px; 145 | text-align: center; 146 | } 147 | .set .d-pad { 148 | margin-right: 40px; 149 | } 150 | .set .d-pad, 151 | .set .o-pad { 152 | display: inline-block; 153 | } 154 | .set.setbg { 155 | background: #222; 156 | } 157 | .set.setbg2 { 158 | background: #5f9837; 159 | } 160 | .d-pad { 161 | position: relative; 162 | width: 200px; 163 | height: 200px; 164 | border-radius: 48%; 165 | overflow: hidden; 166 | } 167 | .d-pad:before { 168 | content: ''; 169 | position: absolute; 170 | top: 50%; 171 | left: 50%; 172 | border-radius: 5%; 173 | transform: translate(-50%, -50%); 174 | width: 66.6%; 175 | height: 66.6%; 176 | background: #ddd; 177 | } 178 | .d-pad:after { 179 | content: ''; 180 | position: absolute; 181 | display: none; 182 | z-index: 2; 183 | width: 20%; 184 | height: 20%; 185 | top: 50%; 186 | left: 50%; 187 | background: #ddd; 188 | border-radius: 50%; 189 | transform: translate(-50%, -50%); 190 | transition: all 0.25s; 191 | cursor: pointer; 192 | } 193 | .d-pad:hover:after { 194 | width: 30%; 195 | height: 30%; 196 | } 197 | .d-pad a { 198 | display: block; 199 | position: absolute; 200 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 201 | width: 33.3%; 202 | height: 43%; 203 | line-height: 40%; 204 | color: #fff; 205 | background: #ddd; 206 | text-align: center; 207 | } 208 | .d-pad a:hover { 209 | background: #eee; 210 | } 211 | .d-pad a:before { 212 | content: ''; 213 | position: absolute; 214 | width: 0; 215 | height: 0; 216 | border-radius: 5px; 217 | border-style: solid; 218 | transition: all 0.25s; 219 | } 220 | .d-pad a:after { 221 | content: ''; 222 | position: absolute; 223 | width: 102%; 224 | height: 78%; 225 | background: #fff; 226 | border-radius: 20%; 227 | } 228 | .d-pad a.left, 229 | .d-pad a.right { 230 | width: 43%; 231 | height: 33%; 232 | } 233 | .d-pad a.left:after, 234 | .d-pad a.right:after { 235 | width: 78%; 236 | height: 102%; 237 | } 238 | .d-pad a.up { 239 | top: 0; 240 | left: 50%; 241 | transform: translate(-50%, 0); 242 | border-radius: 17% 17% 50% 50%; 243 | } 244 | .d-pad a.up:hover { 245 | background: linear-gradient(0deg, #ddd 0%, #eee 50%); 246 | } 247 | .d-pad a.up:after { 248 | left: 0; 249 | top: 0; 250 | transform: translate(-100%, 0); 251 | border-top-left-radius: 50%; 252 | pointer-events: none; 253 | } 254 | .d-pad a.up:before { 255 | top: 40%; 256 | left: 50%; 257 | transform: translate(-50%, -50%); 258 | border-width: 0 13px 19px 13px; 259 | border-color: transparent transparent #aaa transparent; 260 | } 261 | .d-pad a.up:active:before { 262 | border-bottom-color: #333; 263 | } 264 | .d-pad a.up:hover:before { 265 | top: 35%; 266 | } 267 | .d-pad a.down { 268 | bottom: 0; 269 | left: 50%; 270 | transform: translate(-50%, 0); 271 | border-radius: 50% 50% 17% 17%; 272 | } 273 | .d-pad a.down:hover { 274 | background: linear-gradient(180deg, #ddd 0%, #eee 50%); 275 | } 276 | .d-pad a.down:after { 277 | right: 0; 278 | bottom: 0; 279 | transform: translate(100%, 0); 280 | border-bottom-right-radius: 50%; 281 | pointer-events: none; 282 | } 283 | .d-pad a.down:before { 284 | bottom: 40%; 285 | left: 50%; 286 | transform: translate(-50%, 50%); 287 | border-width: 19px 13px 0px 13px; 288 | border-color: #aaa transparent transparent transparent; 289 | } 290 | .d-pad a.down:active:before { 291 | border-top-color: #333; 292 | } 293 | .d-pad a.down:hover:before { 294 | bottom: 35%; 295 | } 296 | .d-pad a.left { 297 | top: 50%; 298 | left: 0; 299 | transform: translate(0, -50%); 300 | border-radius: 17% 50% 50% 17%; 301 | } 302 | .d-pad a.left:hover { 303 | background: linear-gradient(-90deg, #ddd 0%, #eee 50%); 304 | } 305 | .d-pad a.left:after { 306 | left: 0; 307 | bottom: 0; 308 | transform: translate(0, 100%); 309 | border-bottom-left-radius: 50%; 310 | pointer-events: none; 311 | } 312 | .d-pad a.left:before { 313 | left: 40%; 314 | top: 50%; 315 | transform: translate(-50%, -50%); 316 | border-width: 13px 19px 13px 0; 317 | border-color: transparent #aaa transparent transparent; 318 | } 319 | .d-pad a.left:active:before { 320 | border-right-color: #333; 321 | } 322 | .d-pad a.left:hover:before { 323 | left: 35%; 324 | } 325 | .d-pad a.right { 326 | top: 50%; 327 | right: 0; 328 | transform: translate(0, -50%); 329 | border-radius: 50% 17% 17% 50%; 330 | } 331 | .d-pad a.right:hover { 332 | background: linear-gradient(90deg, #ddd 0%, #eee 50%); 333 | } 334 | .d-pad a.right:after { 335 | right: 0; 336 | top: 0; 337 | transform: translate(0, -100%); 338 | border-top-right-radius: 50%; 339 | pointer-events: none; 340 | } 341 | .d-pad a.right:before { 342 | right: 40%; 343 | top: 50%; 344 | transform: translate(50%, -50%); 345 | border-width: 13px 0 13px 19px; 346 | border-color: transparent transparent transparent #aaa; 347 | } 348 | .d-pad a.right:active:before { 349 | border-left-color: #333; 350 | } 351 | .d-pad a.right:hover:before { 352 | right: 35%; 353 | } 354 | .d-pad.up a.up:before { 355 | border-bottom-color: #333; 356 | } 357 | .d-pad.down a.down:before { 358 | border-top-color: #333; 359 | } 360 | .d-pad.left a.left:before { 361 | border-right-color: #333; 362 | } 363 | .d-pad.right a.right:before { 364 | border-left-color: #333; 365 | } 366 | .o-pad { 367 | position: relative; 368 | background: #ddd; 369 | width: 200px; 370 | height: 200px; 371 | border-radius: 50%; 372 | overflow: hidden; 373 | } 374 | .o-pad:after { 375 | content: ''; 376 | position: absolute; 377 | z-index: 2; 378 | width: 20%; 379 | height: 20%; 380 | top: 50%; 381 | left: 50%; 382 | background: #ddd; 383 | border-radius: 50%; 384 | transform: translate(-50%, -50%); 385 | display: none; 386 | transition: all 0.25s; 387 | cursor: pointer; 388 | } 389 | .o-pad:hover:after { 390 | width: 30%; 391 | height: 30%; 392 | } 393 | .o-pad a { 394 | display: block; 395 | position: absolute; 396 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 397 | width: 50%; 398 | height: 50%; 399 | text-align: center; 400 | transform: rotate(45deg); 401 | border: 1px solid rgba(0, 0, 0, 0.2); 402 | } 403 | .o-pad a:before { 404 | content: ''; 405 | position: absolute; 406 | width: 60%; 407 | height: 60%; 408 | top: 50%; 409 | left: 50%; 410 | background: rgba(255, 255, 255, 0.1); 411 | border-radius: 50%; 412 | transform: translate(-50%, -50%); 413 | transition: all 0.25s; 414 | cursor: pointer; 415 | display: none; 416 | } 417 | .o-pad a:after { 418 | content: ''; 419 | position: absolute; 420 | width: 0; 421 | height: 0; 422 | border-radius: 5px; 423 | border-style: solid; 424 | transform: translate(-50%, -50%) rotate(-45deg); 425 | transition: all 0.25s; 426 | } 427 | .o-pad a.up { 428 | bottom: 50%; 429 | left: 50%; 430 | transform: translate(-50%, -20%) rotate(45deg); 431 | border-top-left-radius: 50%; 432 | z-index: 1; 433 | } 434 | .o-pad a.up:hover { 435 | background: linear-gradient(315deg, rgba(255, 255, 255, 0) 15%, rgba(255, 255, 255, 0.4) 100%); 436 | } 437 | .o-pad a.up:before { 438 | left: 57%; 439 | top: 57%; 440 | } 441 | .o-pad a.up:after { 442 | left: 53%; 443 | top: 53%; 444 | border-width: 0 13px 19px 13px; 445 | border-color: transparent transparent #aaa transparent; 446 | } 447 | .o-pad a.up:active:after { 448 | border-bottom-color: #333; 449 | } 450 | .o-pad a.down { 451 | top: 50%; 452 | left: 50%; 453 | transform: translate(-50%, 20%) rotate(45deg); 454 | border-bottom-right-radius: 50%; 455 | z-index: 1; 456 | } 457 | .o-pad a.down:hover { 458 | background: linear-gradient(135deg, rgba(255, 255, 255, 0) 15%, rgba(255, 255, 255, 0.4) 100%); 459 | } 460 | .o-pad a.down:before { 461 | left: 43%; 462 | top: 43%; 463 | } 464 | .o-pad a.down:after { 465 | left: 47%; 466 | top: 47%; 467 | border-width: 19px 13px 0px 13px; 468 | border-color: #aaa transparent transparent transparent; 469 | } 470 | .o-pad a.down:active:after { 471 | border-top-color: #333; 472 | } 473 | .o-pad a.left { 474 | top: 50%; 475 | right: 50%; 476 | transform: translate(-20%, -50%) rotate(45deg); 477 | border-bottom-left-radius: 50%; 478 | border: none; 479 | } 480 | .o-pad a.left:hover { 481 | background: linear-gradient(225deg, rgba(255, 255, 255, 0) 15%, rgba(255, 255, 255, 0.4) 100%); 482 | } 483 | .o-pad a.left:before { 484 | left: 57%; 485 | top: 43%; 486 | } 487 | .o-pad a.left:after { 488 | left: 53%; 489 | top: 47%; 490 | border-width: 13px 19px 13px 0; 491 | border-color: transparent #aaa transparent transparent; 492 | } 493 | .o-pad a.left:active:after { 494 | border-right-color: #333; 495 | } 496 | .o-pad a.right { 497 | top: 50%; 498 | left: 50%; 499 | transform: translate(20%, -50%) rotate(45deg); 500 | border-top-right-radius: 50%; 501 | border: none; 502 | } 503 | .o-pad a.right:hover { 504 | background: linear-gradient(45deg, rgba(255, 255, 255, 0) 15%, rgba(255, 255, 255, 0.4) 100%); 505 | } 506 | .o-pad a.right:before { 507 | left: 43%; 508 | top: 57%; 509 | } 510 | .o-pad a.right:after { 511 | left: 47%; 512 | top: 53%; 513 | border-width: 13px 0 13px 19px; 514 | border-color: transparent transparent transparent #aaa; 515 | } 516 | .o-pad a.right:active:after { 517 | border-left-color: #333; 518 | } 519 | .o-pad a:hover:after { 520 | left: 50%; 521 | top: 50%; 522 | } 523 | .dark .d-pad a { 524 | border-radius: 35%; 525 | } 526 | .dark .d-pad:before, 527 | .dark .d-pad a { 528 | background: #111; 529 | } 530 | .dark .d-pad a.up:hover { 531 | background: linear-gradient(0deg, #111 0%, #222 50%); 532 | } 533 | .dark .d-pad a.right:hover { 534 | background: linear-gradient(90deg, #111 0%, #222 50%); 535 | } 536 | .dark .d-pad a.down:hover { 537 | background: linear-gradient(180deg, #111 0%, #222 50%); 538 | } 539 | .dark .d-pad a.left:hover { 540 | background: linear-gradient(-90deg, #111 0%, #222 50%); 541 | } 542 | .dark .d-pad a.up:before { 543 | border-bottom-color: rgba(255, 255, 255, 0.9); 544 | } 545 | .dark .d-pad a.right:before { 546 | border-left-color: rgba(255, 255, 255, 0.9); 547 | } 548 | .dark .d-pad a.down:before { 549 | border-top-color: rgba(255, 255, 255, 0.9); 550 | } 551 | .dark .d-pad a.left:before { 552 | border-right-color: rgba(255, 255, 255, 0.9); 553 | } 554 | .dark .d-pad a.up:active:before { 555 | border-bottom-color: #61e22d; 556 | } 557 | .dark .d-pad a.right:active:before { 558 | border-left-color: #61e22d; 559 | } 560 | .dark .d-pad a.down:active:before { 561 | border-top-color: #61e22d; 562 | } 563 | .dark .d-pad a.left:active:before { 564 | border-right-color: #61e22d; 565 | } 566 | .dark .o-pad { 567 | background: #111; 568 | } 569 | .dark .o-pad a { 570 | border-color: rgba(255, 255, 255, 0.4); 571 | } 572 | .dark .o-pad a:before { 573 | display: block; 574 | } 575 | .dark .o-pad:before, 576 | .dark .o-pad a { 577 | background: #111; 578 | } 579 | .dark .o-pad a.up:after { 580 | border-bottom-color: rgba(255, 255, 255, 0.9); 581 | } 582 | .dark .o-pad a.right:after { 583 | border-left-color: rgba(255, 255, 255, 0.9); 584 | } 585 | .dark .o-pad a.down:after { 586 | border-top-color: rgba(255, 255, 255, 0.9); 587 | } 588 | .dark .o-pad a.left:after { 589 | border-right-color: rgba(255, 255, 255, 0.9); 590 | } 591 | .dark .o-pad a.up:active:after { 592 | border-bottom-color: #61e22d; 593 | } 594 | .dark .o-pad a.right:active:after { 595 | border-left-color: #61e22d; 596 | } 597 | .dark .o-pad a.down:active:after { 598 | border-top-color: #61e22d; 599 | } 600 | .dark .o-pad a.left:active:after { 601 | border-right-color: #61e22d; 602 | } 603 | .pink .d-pad:before, 604 | .pink .d-pad a { 605 | background: #ff1285; 606 | } 607 | .pink .d-pad a:after { 608 | border-radius: 30%; 609 | } 610 | .pink .d-pad a.up:hover { 611 | background: linear-gradient(0deg, #ff1285 0%, #f366aa 50%); 612 | } 613 | .pink .d-pad a.right:hover { 614 | background: linear-gradient(90deg, #ff1285 0%, #f366aa 50%); 615 | } 616 | .pink .d-pad a.down:hover { 617 | background: linear-gradient(180deg, #ff1285 0%, #f366aa 50%); 618 | } 619 | .pink .d-pad a.left:hover { 620 | background: linear-gradient(-90deg, #ff1285 0%, #f366aa 50%); 621 | } 622 | .pink .d-pad a.up:before { 623 | border-bottom-color: rgba(255, 255, 255, 0.7); 624 | } 625 | .pink .d-pad a.right:before { 626 | border-left-color: rgba(255, 255, 255, 0.7); 627 | } 628 | .pink .d-pad a.down:before { 629 | border-top-color: rgba(255, 255, 255, 0.7); 630 | } 631 | .pink .d-pad a.left:before { 632 | border-right-color: rgba(255, 255, 255, 0.7); 633 | } 634 | .pink .d-pad a.up:active:before { 635 | border-bottom-color: #ffffff; 636 | } 637 | .pink .d-pad a.right:active:before { 638 | border-left-color: #ffffff; 639 | } 640 | .pink .d-pad a.down:active:before { 641 | border-top-color: #ffffff; 642 | } 643 | .pink .d-pad a.left:active:before { 644 | border-right-color: #ffffff; 645 | } 646 | .pink .o-pad { 647 | background: #ff1285; 648 | } 649 | .pink .o-pad a { 650 | border-color: rgba(255, 255, 255, 0.6); 651 | } 652 | .pink .o-pad:before, 653 | .pink .o-pad a { 654 | background: #ff1285; 655 | } 656 | .pink .o-pad a.up:after { 657 | border-bottom-color: rgba(255, 255, 255, 0.7); 658 | } 659 | .pink .o-pad a.right:after { 660 | border-left-color: rgba(255, 255, 255, 0.7); 661 | } 662 | .pink .o-pad a.down:after { 663 | border-top-color: rgba(255, 255, 255, 0.7); 664 | } 665 | .pink .o-pad a.left:after { 666 | border-right-color: rgba(255, 255, 255, 0.7); 667 | } 668 | .pink .o-pad a.up:active:after { 669 | border-bottom-color: #ffffff; 670 | } 671 | .pink .o-pad a.right:active:after { 672 | border-left-color: #ffffff; 673 | } 674 | .pink .o-pad a.down:active:after { 675 | border-top-color: #ffffff; 676 | } 677 | .pink .o-pad a.left:active:after { 678 | border-right-color: #ffffff; 679 | } 680 | .clear .d-pad { 681 | border-radius: 0; 682 | } 683 | .clear .d-pad a { 684 | border: 1px solid #fff; 685 | } 686 | .clear .d-pad:before, 687 | .clear .d-pad a { 688 | background: none; 689 | } 690 | .clear .d-pad a:after { 691 | display: none; 692 | } 693 | .clear .d-pad a.up:hover { 694 | background: linear-gradient(0deg, #5f9837 0%, #6ea248 50%); 695 | } 696 | .clear .d-pad a.right:hover { 697 | background: linear-gradient(90deg, #5f9837 0%, #6ea248 50%); 698 | } 699 | .clear .d-pad a.down:hover { 700 | background: linear-gradient(180deg, #5f9837 0%, #6ea248 50%); 701 | } 702 | .clear .d-pad a.left:hover { 703 | background: linear-gradient(-90deg, #5f9837 0%, #6ea248 50%); 704 | } 705 | .clear .d-pad a.up:before { 706 | border-bottom-color: #fff; 707 | } 708 | .clear .d-pad a.right:before { 709 | border-left-color: #fff; 710 | } 711 | .clear .d-pad a.down:before { 712 | border-top-color: #fff; 713 | } 714 | .clear .d-pad a.left:before { 715 | border-right-color: #fff; 716 | } 717 | .clear .d-pad a.up:active:before { 718 | border-bottom-color: rgba(0, 0, 0, 0.6); 719 | } 720 | .clear .d-pad a.right:active:before { 721 | border-left-color: rgba(0, 0, 0, 0.6); 722 | } 723 | .clear .d-pad a.down:active:before { 724 | border-top-color: rgba(0, 0, 0, 0.6); 725 | } 726 | .clear .d-pad a.left:active:before { 727 | border-right-color: rgba(0, 0, 0, 0.6); 728 | } 729 | .clear .o-pad { 730 | background: none; 731 | border: 1px solid #fff; 732 | } 733 | .clear .o-pad a { 734 | border-color: #fff; 735 | } 736 | .clear .o-pad:before, 737 | .clear .o-pad a { 738 | background: none; 739 | } 740 | .clear .o-pad a.up:after { 741 | border-bottom-color: #fff; 742 | } 743 | .clear .o-pad a.right:after { 744 | border-left-color: #fff; 745 | } 746 | .clear .o-pad a.down:after { 747 | border-top-color: #fff; 748 | } 749 | .clear .o-pad a.left:after { 750 | border-right-color: #fff; 751 | } 752 | .clear .o-pad a.up:active:after { 753 | border-bottom-color: rgba(0, 0, 0, 0.6); 754 | } 755 | .clear .o-pad a.right:active:after { 756 | border-left-color: rgba(0, 0, 0, 0.6); 757 | } 758 | .clear .o-pad a.down:active:after { 759 | border-top-color: rgba(0, 0, 0, 0.6); 760 | } 761 | .clear .o-pad a.left:active:after { 762 | border-right-color: rgba(0, 0, 0, 0.6); 763 | } 764 | .outline .d-pad { 765 | border-radius: 0; 766 | } 767 | .outline .d-pad a { 768 | border: 1px solid rgba(0, 0, 0, 0.1); 769 | } 770 | .outline .d-pad:after, 771 | .outline .d-pad:before, 772 | .outline .d-pad a { 773 | background: #fff; 774 | } 775 | .outline .d-pad a:after { 776 | display: none; 777 | } 778 | .outline .d-pad a.up:hover { 779 | background: linear-gradient(0deg, #fff 0%, #efefef 50%); 780 | } 781 | .outline .d-pad a.right:hover { 782 | background: linear-gradient(90deg, #fff 0%, #efefef 50%); 783 | } 784 | .outline .d-pad a.down:hover { 785 | background: linear-gradient(180deg, #fff 0%, #efefef 50%); 786 | } 787 | .outline .d-pad a.left:hover { 788 | background: linear-gradient(-90deg, #fff 0%, #efefef 50%); 789 | } 790 | .outline .d-pad a.up:before { 791 | border-bottom-color: rgba(0, 0, 0, 0.1); 792 | } 793 | .outline .d-pad a.right:before { 794 | border-left-color: rgba(0, 0, 0, 0.1); 795 | } 796 | .outline .d-pad a.down:before { 797 | border-top-color: rgba(0, 0, 0, 0.1); 798 | } 799 | .outline .d-pad a.left:before { 800 | border-right-color: rgba(0, 0, 0, 0.1); 801 | } 802 | .outline .o-pad { 803 | background: #fff; 804 | border: 1px solid rgba(0, 0, 0, 0.1); 805 | } 806 | .outline .o-pad a { 807 | border-color: rgba(0, 0, 0, 0.1); 808 | } 809 | .outline .o-pad:after, 810 | .outline .o-pad:before, 811 | .outline .o-pad a { 812 | background: #fff; 813 | } 814 | .outline .o-pad a.up:after { 815 | border-bottom-color: rgba(0, 0, 0, 0.1); 816 | } 817 | .outline .o-pad a.right:after { 818 | border-left-color: rgba(0, 0, 0, 0.1); 819 | } 820 | .outline .o-pad a.down:after { 821 | border-top-color: rgba(0, 0, 0, 0.1); 822 | } 823 | .outline .o-pad a.left:after { 824 | border-right-color: rgba(0, 0, 0, 0.1); 825 | } 826 | .blue .d-pad:before, 827 | .blue .d-pad a { 828 | background: #1843ca; 829 | } 830 | .blue .d-pad:after { 831 | display: block; 832 | background: #ccc; 833 | } 834 | .blue .d-pad a:after { 835 | border-radius: 10%; 836 | } 837 | .blue .d-pad a.up:hover { 838 | background: linear-gradient(0deg, #1843ca 0%, #143cb9 50%); 839 | } 840 | .blue .d-pad a.right:hover { 841 | background: linear-gradient(90deg, #1843ca 0%, #143cb9 50%); 842 | } 843 | .blue .d-pad a.down:hover { 844 | background: linear-gradient(180deg, #1843ca 0%, #143cb9 50%); 845 | } 846 | .blue .d-pad a.left:hover { 847 | background: linear-gradient(-90deg, #1843ca 0%, #143cb9 50%); 848 | } 849 | .blue .d-pad a.up:before { 850 | border-bottom-color: #ccc; 851 | } 852 | .blue .d-pad a.right:before { 853 | border-left-color: #ccc; 854 | } 855 | .blue .d-pad a.down:before { 856 | border-top-color: #ccc; 857 | } 858 | .blue .d-pad a.left:before { 859 | border-right-color: #ccc; 860 | } 861 | .blue .d-pad a.up:active:before { 862 | border-bottom-color: #ffffff; 863 | } 864 | .blue .d-pad a.right:active:before { 865 | border-left-color: #ffffff; 866 | } 867 | .blue .d-pad a.down:active:before { 868 | border-top-color: #ffffff; 869 | } 870 | .blue .d-pad a.left:active:before { 871 | border-right-color: #ffffff; 872 | } 873 | .blue .o-pad { 874 | background: #1843ca; 875 | } 876 | .blue .o-pad a { 877 | border-color: rgba(255, 255, 255, 0.6); 878 | } 879 | .blue .o-pad:before, 880 | .blue .o-pad a { 881 | background: #1843ca; 882 | } 883 | .blue .o-pad:after { 884 | display: block; 885 | background: #ccc; 886 | } 887 | .blue .o-pad a.up:after { 888 | border-bottom-color: #ccc; 889 | } 890 | .blue .o-pad a.right:after { 891 | border-left-color: #ccc; 892 | } 893 | .blue .o-pad a.down:after { 894 | border-top-color: #ccc; 895 | } 896 | .blue .o-pad a.left:after { 897 | border-right-color: #ccc; 898 | } 899 | .blue .o-pad a.up:active:after { 900 | border-bottom-color: #ffffff; 901 | } 902 | .blue .o-pad a.right:active:after { 903 | border-left-color: #ffffff; 904 | } 905 | .blue .o-pad a.down:active:after { 906 | border-top-color: #ffffff; 907 | } 908 | .blue .o-pad a.left:active:after { 909 | border-right-color: #ffffff; 910 | } 911 | .setbg.white .d-pad:before, 912 | .setbg.white .d-pad a { 913 | background: #fff; 914 | } 915 | .setbg.white .d-pad:after { 916 | display: block; 917 | background: rgba(0, 0, 0, 0.1); 918 | } 919 | .setbg.white .d-pad a:after { 920 | border-radius: 40%; 921 | background: #222; 922 | } 923 | .setbg.white .d-pad a.up:hover { 924 | background: #fff; 925 | } 926 | .setbg.white .d-pad a.right:hover { 927 | background: #fff; 928 | } 929 | .setbg.white .d-pad a.down:hover { 930 | background: #fff; 931 | } 932 | .setbg.white .d-pad a.left:hover { 933 | background: #fff; 934 | } 935 | .setbg.white .d-pad a.up:before { 936 | border-bottom-color: #0074D9; 937 | } 938 | .setbg.white .d-pad a.right:before { 939 | border-left-color: #FF851B; 940 | } 941 | .setbg.white .d-pad a.down:before { 942 | border-top-color: #3D9970; 943 | } 944 | .setbg.white .d-pad a.left:before { 945 | border-right-color: #FFDC00; 946 | } 947 | .setbg.white .d-pad a.up:active:before { 948 | border-bottom-color: rgba(0, 0, 0, 0.6); 949 | } 950 | .setbg.white .d-pad a.right:active:before { 951 | border-left-color: rgba(0, 0, 0, 0.6); 952 | } 953 | .setbg.white .d-pad a.down:active:before { 954 | border-top-color: rgba(0, 0, 0, 0.6); 955 | } 956 | .setbg.white .d-pad a.left:active:before { 957 | border-right-color: rgba(0, 0, 0, 0.6); 958 | } 959 | .setbg.white .o-pad { 960 | background: #fff; 961 | } 962 | .setbg.white .o-pad a { 963 | border-color: rgba(255, 255, 255, 0.6); 964 | } 965 | .setbg.white .o-pad:before, 966 | .setbg.white .o-pad a { 967 | background: #fff; 968 | } 969 | .setbg.white .o-pad:after { 970 | display: block; 971 | background: #ccc; 972 | } 973 | .setbg.white .o-pad a.up:after { 974 | border-bottom-color: #2ECC40; 975 | } 976 | .setbg.white .o-pad a.right:after { 977 | border-left-color: #85144b; 978 | } 979 | .setbg.white .o-pad a.down:after { 980 | border-top-color: #7FDBFF; 981 | } 982 | .setbg.white .o-pad a.left:after { 983 | border-right-color: #B10DC9; 984 | } 985 | .setbg.white .o-pad a.up:active:after { 986 | border-bottom-color: rgba(0, 0, 0, 0.6); 987 | } 988 | .setbg.white .o-pad a.right:active:after { 989 | border-left-color: rgba(0, 0, 0, 0.6); 990 | } 991 | .setbg.white .o-pad a.down:active:after { 992 | border-top-color: rgba(0, 0, 0, 0.6); 993 | } 994 | .setbg.white .o-pad a.left:active:after { 995 | border-right-color: rgba(0, 0, 0, 0.6); 996 | } 997 | .wire .d-pad { 998 | overflow: initial; 999 | border: 1px dashed #93b4ff; 1000 | } 1001 | .wire .d-pad:after { 1002 | display: block; 1003 | } 1004 | .wire .d-pad:after, 1005 | .wire .d-pad:before, 1006 | .wire .d-pad a, 1007 | .wire .d-pad a:after { 1008 | background: none; 1009 | border: 1px solid #93b4ff; 1010 | } 1011 | .wire .d-pad a:after { 1012 | border: 1px dashed #93b4ff; 1013 | } 1014 | .wire .d-pad a.up:before { 1015 | border-bottom-color: #93b4ff; 1016 | } 1017 | .wire .d-pad a.right:before { 1018 | border-left-color: #93b4ff; 1019 | } 1020 | .wire .d-pad a.down:before { 1021 | border-top-color: #93b4ff; 1022 | } 1023 | .wire .d-pad a.left:before { 1024 | border-right-color: #93b4ff; 1025 | } 1026 | .wire .d-pad a:hover { 1027 | background: none; 1028 | } 1029 | .wire .o-pad { 1030 | border: 1px dashed #93b4ff; 1031 | background: none; 1032 | overflow: initial; 1033 | } 1034 | .wire .o-pad:after, 1035 | .wire .o-pad a:before { 1036 | display: block; 1037 | } 1038 | .wire .o-pad:after, 1039 | .wire .o-pad:before, 1040 | .wire .o-pad a, 1041 | .wire .o-pad a:before { 1042 | background: none; 1043 | border: 1px solid #93b4ff; 1044 | } 1045 | .wire .o-pad a.up:after { 1046 | border-bottom-color: #93b4ff; 1047 | } 1048 | .wire .o-pad a.right:after { 1049 | border-left-color: #93b4ff; 1050 | } 1051 | .wire .o-pad a.down:after { 1052 | border-top-color: #93b4ff; 1053 | } 1054 | .wire .o-pad a.left:after { 1055 | border-right-color: #93b4ff; 1056 | } 1057 | .wire .o-pad a:hover { 1058 | background: none; 1059 | } 1060 | .d-pad.up a.up:before { 1061 | border-bottom-color: #333; 1062 | } 1063 | .d-pad.down a.down:before { 1064 | border-top-color: #333; 1065 | } 1066 | .d-pad.left a.left:before { 1067 | border-right-color: #333; 1068 | } 1069 | .d-pad.right a.right:before { 1070 | border-left-color: #333; 1071 | } 1072 | .o-pad.up a.up:after { 1073 | border-bottom-color: #333; 1074 | } 1075 | .o-pad.down a.down:after { 1076 | border-top-color: #333; 1077 | } 1078 | .o-pad.left a.left:after { 1079 | border-right-color: #333; 1080 | } 1081 | .o-pad.right a.right:after { 1082 | border-left-color: #333; 1083 | } 1084 | -------------------------------------------------------------------------------- /static/css/entireframework.min.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2014 Owen Versteeg; MIT licensed */body,textarea,input,select{background:0;border-radius:0;font:16px sans-serif;margin:0}.smooth{transition:all .2s}.btn,.nav a{text-decoration:none}.container{margin:0 20px;width:auto}label>*{display:inline}form>*{display:block;margin-bottom:10px}.btn{background:#999;border-radius:6px;border:0;color:#fff;cursor:pointer;display:inline-block;margin:2px 0;padding:12px 30px 14px}.btn:hover{background:#888}.btn:active,.btn:focus{background:#777}.btn-a{background:#0ae}.btn-a:hover{background:#09d}.btn-a:active,.btn-a:focus{background:#08b}.btn-b{background:#3c5}.btn-b:hover{background:#2b4}.btn-b:active,.btn-b:focus{background:#2a4}.btn-c{background:#d33}.btn-c:hover{background:#c22}.btn-c:active,.btn-c:focus{background:#b22}.btn-sm{border-radius:4px;padding:10px 14px 11px}.row{margin:1% 0;overflow:auto}.col{float:left}.table,.c12{width:100%}.c11{width:91.66%}.c10{width:83.33%}.c9{width:75%}.c8{width:66.66%}.c7{width:58.33%}.c6{width:50%}.c5{width:41.66%}.c4{width:33.33%}.c3{width:25%}.c2{width:16.66%}.c1{width:8.33%}h1{font-size:3em}.btn,h2{font-size:2em}.ico{font:33px Arial Unicode MS,Lucida Sans Unicode}.addon,.btn-sm,.nav,textarea,input,select{outline:0;font-size:14px}textarea,input,select{padding:8px;border:1px solid #ccc}textarea:focus,input:focus,select:focus{border-color:#5ab}textarea,input[type=text]{-webkit-appearance:none;width:13em}.addon{padding:8px 12px;box-shadow:0 0 0 1px #ccc}.nav,.nav .current,.nav a:hover{background:#000;color:#fff}.nav{height:24px;padding:11px 0 15px}.nav a{color:#aaa;padding-right:1em;position:relative;top:-1px}.nav .pagename{font-size:22px;top:1px}.btn.btn-close{background:#000;float:right;font-size:25px;margin:-54px 7px;display:none}@media(min-width:1310px){.container{margin:auto;width:1270px}}@media(max-width:870px){.row .col{width:100%}}@media(max-width:500px){.btn.btn-close{display:block}.nav{overflow:hidden}.pagename{margin-top:-11px}.nav:active,.nav:focus{height:auto}.nav div:before{background:#000;border-bottom:10px double;border-top:3px solid;content:'';float:right;height:4px;position:relative;right:3px;top:14px;width:20px}.nav a{padding:.5em 0;display:block;width:50%}}.table th,.table td{padding:.5em;text-align:left}.table tbody>:nth-child(2n-1){background:#ddd}.msg{padding:1.5em;background:#def;border-left:5px solid #59d} -------------------------------------------------------------------------------- /static/js/custom.js: -------------------------------------------------------------------------------- 1 | var targetUrl = `ws://${location.host}/ws`; 2 | var websocket; 3 | window.addEventListener("load", onLoad); 4 | 5 | function onLoad() { 6 | initializeSocket(); 7 | setDefaultSpeed(); 8 | } 9 | 10 | function initializeSocket() { 11 | console.log("Opening WebSocket connection to ESP32 MicroPython Server..."); 12 | websocket = new WebSocket(targetUrl); 13 | websocket.onopen = onOpen; 14 | websocket.onclose = onClose; 15 | websocket.onmessage = onMessage; 16 | } 17 | function onOpen(event) { 18 | console.log("Starting connection to WebSocket server.."); 19 | } 20 | function onClose(event) { 21 | console.log("Closing connection to server.."); 22 | setTimeout(initializeSocket, 2000); 23 | } 24 | function onMessage(event) { 25 | console.log("WebSocket message received:", event); 26 | } 27 | 28 | function sendMessage(message) { 29 | websocket.send(message); 30 | } 31 | 32 | /* 33 | Speed Settings Handler 34 | */ 35 | var speedSettings = document.querySelectorAll( 36 | 'input[type=radio][name="speed-settings"]' 37 | ); 38 | speedSettings.forEach((radio) => 39 | radio.addEventListener("change", () => { 40 | var speedSettings = radio.value; 41 | console.log("Speed Settings :: " + speedSettings); 42 | sendMessage(speedSettings); 43 | }) 44 | ); 45 | 46 | function setDefaultSpeed() { 47 | console.log("Setting default speed to normal.."); 48 | let normalOption = document.getElementById("option-2"); 49 | normalOption.checked = true; 50 | } 51 | 52 | /* 53 | O-Pad/ D-Pad Controller and Javascript Code 54 | */ 55 | // Prevent scrolling on every click! 56 | // super sweet vanilla JS delegated event handling! 57 | document.body.addEventListener("click", function (e) { 58 | if (e.target && e.target.nodeName == "A") { 59 | e.preventDefault(); 60 | } 61 | }); 62 | 63 | function touchStartHandler(event) { 64 | var direction = event.target.dataset.direction; 65 | console.log("Touch Start :: " + direction); 66 | sendMessage(direction); 67 | } 68 | 69 | function touchEndHandler(event) { 70 | const stop_command = "stop"; 71 | var direction = event.target.dataset.direction; 72 | console.log("Touch End :: " + direction); 73 | sendMessage(stop_command); 74 | } 75 | 76 | document.querySelectorAll(".control").forEach((item) => { 77 | item.addEventListener("touchstart", touchStartHandler); 78 | }); 79 | 80 | document.querySelectorAll(".control").forEach((item) => { 81 | item.addEventListener("touchend", touchEndHandler); 82 | }); 83 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MicroPython Wifi Robot Car 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 |
21 |
22 |

MicroPython Wifi Robot Car

23 |
24 | 25 |
26 |
Speed Settings
27 |
28 | 29 | 30 | 31 | 35 | 39 | 43 |
44 | 45 |
46 | 52 | 58 |
59 | 60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | --------------------------------------------------------------------------------