├── LICENSE ├── README.md ├── control-components-from-web-server ├── README.MD ├── boot.py ├── main.py ├── microdot.py ├── microdot_asyncio.py ├── microdot_asyncio_websocket.py ├── microdot_utemplate.py ├── microdot_websocket.py ├── rgb_led.py ├── static │ ├── index.css │ └── index.js └── templates │ └── index.html ├── dc-motor-drv8833 ├── README.MD ├── main.py └── robot_car.py ├── ldr-photoresistor ├── README.MD ├── ldr_photoresistor_class.py └── ldr_photoresistor_pico_w.py ├── microdot-dynamic-component-path ├── README.MD ├── boot.py ├── color_service.py ├── colors.json ├── main.py ├── microdot.py ├── microdot_asyncio.py ├── microdot_utemplate.py ├── rgb_led.py ├── static │ └── index.js └── templates │ └── index.html ├── mqtt-bme280-weather-station ├── README.MD ├── bme_module.py ├── boot.py └── main.py ├── rgb-module ├── README.MD ├── basic_rgb_show.py ├── custom_color.py └── rgb_dim_brightness.py ├── umqtt.simple ├── boot.py └── main.py └── websocket_using_microdot ├── README.MD ├── boot.py ├── ldr_photoresistor_module.py ├── main.py ├── microdot.py ├── microdot_asyncio.py ├── microdot_asyncio_websocket.py ├── microdot_utemplate.py ├── microdot_websocket.py ├── static ├── index.css └── index.js └── templates └── index.html /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-raspberry-pi-pico 2 | A repository of my MicroPython projects using my Raspberry Pi Pico board 3 | -------------------------------------------------------------------------------- /control-components-from-web-server/README.MD: -------------------------------------------------------------------------------- 1 | # Write Up 2 | https://www.donskytech.com/raspberry-pi-pico-w-web-server-to-control-components/ 3 | 4 | ![Featured Image - Raspberry Pi Pico W Web Server](https://user-images.githubusercontent.com/69466026/225221186-42d3ba8c-c1d6-4bf2-8b0d-3e8c5b5849e8.jpg) 5 | -------------------------------------------------------------------------------- /control-components-from-web-server/boot.py: -------------------------------------------------------------------------------- 1 | # boot.py -- run on boot-up 2 | import network 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() -------------------------------------------------------------------------------- /control-components-from-web-server/main.py: -------------------------------------------------------------------------------- 1 | from microdot_asyncio import Microdot, Response, send_file 2 | from microdot_utemplate import render_template 3 | from microdot_asyncio_websocket import with_websocket 4 | from rgb_led import RGBLEDModule 5 | import time 6 | import ujson 7 | 8 | # initialize RGB 9 | pwm_pins = [14, 15, 16] 10 | rgb_led = RGBLEDModule(pwm_pins) 11 | # Set default color to 50% each for RGB 12 | rgb_led.set_rgb_color({'blue': '50', 'red': '50', 'green': '50'}) 13 | 14 | # Initialize MicroDot 15 | app = Microdot() 16 | Response.default_content_type = 'text/html' 17 | 18 | # root route 19 | @app.route('/') 20 | async def index(request): 21 | return render_template('index.html') 22 | 23 | # initialize websocket 24 | @app.route('/ws') 25 | @with_websocket 26 | async def read_sensor(request, ws): 27 | while True: 28 | data = await ws.receive() 29 | rgb_color = ujson.loads(data) 30 | rgb_led.set_rgb_color(rgb_color) 31 | await ws.send("OK") 32 | 33 | # Static CSS/JSS 34 | @app.route("/static/") 35 | def static(request, path): 36 | if ".." in path: 37 | # directory traversal is not allowed 38 | return "Not found", 404 39 | return send_file("static/" + path) 40 | 41 | 42 | # shutdown 43 | @app.get('/shutdown') 44 | def shutdown(request): 45 | request.app.shutdown() 46 | return 'The server is shutting down...' 47 | 48 | 49 | if __name__ == "__main__": 50 | try: 51 | app.run() 52 | except KeyboardInterrupt: 53 | rgb_led.deinit_pwms() 54 | pass 55 | -------------------------------------------------------------------------------- /control-components-from-web-server/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 | after_request_handled = False 351 | if req: 352 | if req.content_length > req.max_content_length: 353 | if 413 in self.error_handlers: 354 | res = await self._invoke_handler( 355 | self.error_handlers[413], req) 356 | else: 357 | res = 'Payload too large', 413 358 | else: 359 | f = self.find_route(req) 360 | try: 361 | res = None 362 | if callable(f): 363 | for handler in self.before_request_handlers: 364 | res = await self._invoke_handler(handler, req) 365 | if res: 366 | break 367 | if res is None: 368 | res = await self._invoke_handler( 369 | f, req, **req.url_args) 370 | if isinstance(res, tuple): 371 | body = res[0] 372 | if isinstance(res[1], int): 373 | status_code = res[1] 374 | headers = res[2] if len(res) > 2 else {} 375 | else: 376 | status_code = 200 377 | headers = res[1] 378 | res = Response(body, status_code, headers) 379 | elif not isinstance(res, Response): 380 | res = Response(res) 381 | for handler in self.after_request_handlers: 382 | res = await self._invoke_handler( 383 | handler, req, res) or res 384 | for handler in req.after_request_handlers: 385 | res = await self._invoke_handler( 386 | handler, req, res) or res 387 | after_request_handled = True 388 | elif f in self.error_handlers: 389 | res = await self._invoke_handler( 390 | self.error_handlers[f], req) 391 | else: 392 | res = 'Not found', f 393 | except HTTPException as exc: 394 | if exc.status_code in self.error_handlers: 395 | res = self.error_handlers[exc.status_code](req) 396 | else: 397 | res = exc.reason, exc.status_code 398 | except Exception as exc: 399 | print_exception(exc) 400 | exc_class = None 401 | res = None 402 | if exc.__class__ in self.error_handlers: 403 | exc_class = exc.__class__ 404 | else: 405 | for c in mro(exc.__class__)[1:]: 406 | if c in self.error_handlers: 407 | exc_class = c 408 | break 409 | if exc_class: 410 | try: 411 | res = await self._invoke_handler( 412 | self.error_handlers[exc_class], req, exc) 413 | except Exception as exc2: # pragma: no cover 414 | print_exception(exc2) 415 | if res is None: 416 | if 500 in self.error_handlers: 417 | res = await self._invoke_handler( 418 | self.error_handlers[500], req) 419 | else: 420 | res = 'Internal server error', 500 421 | else: 422 | if 400 in self.error_handlers: 423 | res = await self._invoke_handler(self.error_handlers[400], req) 424 | else: 425 | res = 'Bad request', 400 426 | if isinstance(res, tuple): 427 | res = Response(*res) 428 | elif not isinstance(res, Response): 429 | res = Response(res) 430 | if not after_request_handled: 431 | for handler in self.after_error_request_handlers: 432 | res = await self._invoke_handler( 433 | handler, req, res) or res 434 | return res 435 | 436 | async def _invoke_handler(self, f_or_coro, *args, **kwargs): 437 | ret = f_or_coro(*args, **kwargs) 438 | if _iscoroutine(ret): 439 | ret = await ret 440 | return ret 441 | 442 | 443 | abort = Microdot.abort 444 | Response.already_handled = Response() 445 | redirect = Response.redirect 446 | send_file = Response.send_file 447 | -------------------------------------------------------------------------------- /control-components-from-web-server/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 | -------------------------------------------------------------------------------- /control-components-from-web-server/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 | -------------------------------------------------------------------------------- /control-components-from-web-server/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 | -------------------------------------------------------------------------------- /control-components-from-web-server/rgb_led.py: -------------------------------------------------------------------------------- 1 | import time 2 | from machine import Pin,PWM 3 | 4 | #RGB 5 | RED = 0 6 | GREEN = 1 7 | BLUE = 2 8 | 9 | RED_COLOR = "red" 10 | GREEN_COLOR = "green" 11 | BLUE_COLOR = "blue" 12 | 13 | # Class that wil interface with our RGB Module 14 | class RGBLEDModule: 15 | def __init__(self, pwm_pins): 16 | self.pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])), 17 | PWM(Pin(pwm_pins[BLUE]))] 18 | self.init_pwms() 19 | 20 | # Initialize PWM Pins 21 | def init_pwms(self): 22 | for pwm in self.pwms: 23 | pwm.freq(1000) 24 | 25 | # Deinitialize PWM fins 26 | def deinit_pwms(self): 27 | self.turn_off_rgb() 28 | for pwm in self.pwms: 29 | pwm.deinit() 30 | 31 | # Map RGB values from 0-100 to duty cycle 0-65535 32 | def map_range(self, x, in_min, in_max, out_min, out_max): 33 | return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min 34 | 35 | # Turn off RGB 36 | def turn_off_rgb(self): 37 | self.pwms[RED].duty_u16(0) 38 | self.pwms[GREEN].duty_u16(0) 39 | self.pwms[BLUE].duty_u16(0) 40 | time.sleep(0.01) 41 | 42 | # Set RGB Color 43 | def set_rgb_color(self, rgb_color): 44 | self.turn_off_rgb() 45 | 46 | 47 | self.pwms[RED].duty_u16(self.map_range(int(rgb_color[RED_COLOR]), 0, 100, 0, 65535)) 48 | self.pwms[GREEN].duty_u16(self.map_range(int(rgb_color[GREEN_COLOR]), 0, 100, 0, 65535)) 49 | self.pwms[BLUE].duty_u16(self.map_range(int(rgb_color[BLUE_COLOR]), 0, 100, 0, 65535)) 50 | -------------------------------------------------------------------------------- /control-components-from-web-server/static/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | outline: 0; 5 | } 6 | main { 7 | padding: 2rem; 8 | } 9 | .container { 10 | display: flex; 11 | flex-flow: column nowrap; 12 | align-items: center; 13 | } 14 | .slidecontainer { 15 | display: flex; 16 | flex-flow: column nowrap; 17 | width: 60%; 18 | margin-top: 2rem; 19 | } 20 | .color-label { 21 | font-size: 1.5rem; 22 | font-weight: bold; 23 | } 24 | -------------------------------------------------------------------------------- /control-components-from-web-server/static/index.js: -------------------------------------------------------------------------------- 1 | // Slider 2 | var redSlider = document.querySelector("#redSlider"); 3 | var redValue = document.querySelector("#redValue"); 4 | 5 | var greenSlider = document.querySelector("#greenSlider"); 6 | var greenValue = document.querySelector("#greenValue"); 7 | 8 | var blueSlider = document.querySelector("#blueSlider"); 9 | var blueValue = document.querySelector("#blueValue"); 10 | 11 | redSlider.addEventListener("change", () => { 12 | redValue.textContent = redSlider.value; 13 | sendMessage( 14 | JSON.stringify({ 15 | red: redSlider.value, 16 | green: greenSlider.value, 17 | blue: blueSlider.value, 18 | }) 19 | ); 20 | }); 21 | 22 | greenSlider.addEventListener("change", () => { 23 | greenValue.textContent = greenSlider.value; 24 | sendMessage( 25 | JSON.stringify({ 26 | red: redSlider.value, 27 | green: greenSlider.value, 28 | blue: blueSlider.value, 29 | }) 30 | ); 31 | }); 32 | 33 | blueSlider.addEventListener("change", () => { 34 | blueValue.textContent = blueSlider.value; 35 | sendMessage( 36 | JSON.stringify({ 37 | red: redSlider.value, 38 | green: greenSlider.value, 39 | blue: blueSlider.value, 40 | }) 41 | ); 42 | }); 43 | 44 | // WebSocket support 45 | var targetUrl = `ws://${location.host}/ws`; 46 | var websocket; 47 | window.addEventListener("load", onLoad); 48 | 49 | function onLoad() { 50 | initializeSocket(); 51 | } 52 | 53 | function initializeSocket() { 54 | console.log("Opening WebSocket connection MicroPython Server..."); 55 | websocket = new WebSocket(targetUrl); 56 | websocket.onopen = onOpen; 57 | websocket.onclose = onClose; 58 | websocket.onmessage = onMessage; 59 | } 60 | function onOpen(event) { 61 | console.log("Starting connection to WebSocket server.."); 62 | } 63 | function onClose(event) { 64 | console.log("Closing connection to server.."); 65 | setTimeout(initializeSocket, 2000); 66 | } 67 | function onMessage(event) { 68 | console.log("WebSocket message received:", event); 69 | } 70 | 71 | function sendMessage(message) { 72 | websocket.send(message); 73 | } 74 | -------------------------------------------------------------------------------- /control-components-from-web-server/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Raspberry Pi Pico W - Web Server to control components 8 | 9 | 10 | 11 |
12 |

Raspberry Pi Pico W - Web Server to control components

13 |
14 |
15 | 16 | 24 |
25 | 26 | 50 27 |
28 |
29 |
30 | 31 | 39 |
40 | 41 | 50 42 |
43 |
44 |
45 | 46 | 54 |
55 | 56 | 50 57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /dc-motor-drv8833/README.MD: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Pico W Motor Control using the DRV8833 2 | How to add motor control capability to your Raspberry Pi Pico W using the DRV8833 H-Bridge motor controller and MicroPython 3 | 4 | ## Writeup 5 | https://www.donskytech.com/raspberry-pi-pico-motor-control-using-the-drv8833/ 6 | 7 | 8 | ![Featured Image - Raspberry Pi Pico W Motor Control using DRV8833 and MicroPython](https://github.com/donskytech/micropython-raspberry-pi-pico/assets/69466026/3a93002b-8b1f-4592-b988-d7f6c18a2cc3) 9 | -------------------------------------------------------------------------------- /dc-motor-drv8833/main.py: -------------------------------------------------------------------------------- 1 | from robot_car import RobotCar 2 | import utime 3 | 4 | # Pico W GPIO Pin 5 | LEFT_MOTOR_PIN_1 = 16 6 | LEFT_MOTOR_PIN_2 = 17 7 | RIGHT_MOTOR_PIN_1 = 18 8 | RIGHT_MOTOR_PIN_2 = 19 9 | 10 | motor_pins = [LEFT_MOTOR_PIN_1, LEFT_MOTOR_PIN_2, RIGHT_MOTOR_PIN_1, RIGHT_MOTOR_PIN_2] 11 | 12 | # Create an instance of our robot car 13 | robot_car = RobotCar(motor_pins, 20000) 14 | 15 | if __name__ == '__main__': 16 | try: 17 | # Test forward, reverse, stop, turn left and turn right 18 | print("*********Testing forward, reverse and loop*********") 19 | for i in range(2): 20 | print("Moving forward") 21 | robot_car.move_forward() 22 | utime.sleep(2) 23 | print("Moving backward") 24 | robot_car.move_backward() 25 | utime.sleep(2) 26 | print("stop") 27 | robot_car.stop() 28 | utime.sleep(2) 29 | print("turn left") 30 | robot_car.turn_left() 31 | utime.sleep(2) 32 | print("turn right") 33 | robot_car.turn_right() 34 | utime.sleep(2) 35 | 36 | print("*********Testing speed*********") 37 | for i in range(2): 38 | print("Moving at 100% speed") 39 | robot_car.change_speed(100); 40 | robot_car.move_forward() 41 | utime.sleep(2) 42 | 43 | print("Moving at 50% speed") 44 | robot_car.change_speed(50); 45 | robot_car.move_forward() 46 | utime.sleep(2) 47 | 48 | print("Moving at 20% of speed") 49 | robot_car.change_speed(20); 50 | robot_car.move_forward() 51 | utime.sleep(2) 52 | 53 | print("Moving at 0% of speed or the slowest") 54 | robot_car.change_speed(0); 55 | robot_car.move_forward() 56 | utime.sleep(2) 57 | 58 | robot_car.deinit() 59 | 60 | except KeyboardInterrupt: 61 | robot_car.deinit() -------------------------------------------------------------------------------- /dc-motor-drv8833/robot_car.py: -------------------------------------------------------------------------------- 1 | 2 | from machine import Pin 3 | from machine import PWM 4 | import utime 5 | 6 | ''' 7 | Class to represent our robot car 8 | ''' 9 | class RobotCar: 10 | MAX_DUTY_CYCLE = 65535 11 | MIN_DUTY_CYCLE = 0 12 | def __init__(self, motor_pins, frequency=20000): 13 | self.left_motor_pin1 = PWM(Pin(motor_pins[0], mode=Pin.OUT)) 14 | self.left_motor_pin2 = PWM(Pin(motor_pins[1], mode=Pin.OUT)) 15 | self.right_motor_pin1 = PWM(Pin(motor_pins[2], mode=Pin.OUT)) 16 | self.right_motor_pin2 = PWM(Pin(motor_pins[3], mode=Pin.OUT)) 17 | # set PWM frequency 18 | self.left_motor_pin1.freq(frequency) 19 | self.left_motor_pin2.freq(frequency) 20 | self.right_motor_pin1.freq(frequency) 21 | self.right_motor_pin2.freq(frequency) 22 | 23 | self.current_speed = RobotCar.MAX_DUTY_CYCLE 24 | 25 | def move_forward(self): 26 | self.left_motor_pin1.duty_u16(self.current_speed) 27 | self.left_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE) 28 | 29 | self.right_motor_pin1.duty_u16(self.current_speed) 30 | self.right_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE) 31 | 32 | def move_backward(self): 33 | self.left_motor_pin1.duty_u16(RobotCar.MIN_DUTY_CYCLE) 34 | self.left_motor_pin2.duty_u16(self.current_speed) 35 | 36 | self.right_motor_pin1.duty_u16(RobotCar.MIN_DUTY_CYCLE) 37 | self.right_motor_pin2.duty_u16(self.current_speed) 38 | 39 | def turn_left(self): 40 | self.left_motor_pin1.duty_u16(self.current_speed) 41 | self.left_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE) 42 | 43 | self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) 44 | self.right_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) 45 | 46 | def turn_right(self): 47 | self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) 48 | self.left_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) 49 | 50 | self.right_motor_pin1.duty_u16(self.current_speed) 51 | self.right_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE) 52 | 53 | def stop(self): 54 | self.left_motor_pin1.duty_u16(RobotCar.MIN_DUTY_CYCLE) 55 | self.left_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE) 56 | 57 | self.right_motor_pin1.duty_u16(RobotCar.MIN_DUTY_CYCLE) 58 | self.right_motor_pin2.duty_u16(RobotCar.MIN_DUTY_CYCLE) 59 | 60 | ''' Map duty cycle values from 0-100 to duty cycle 40000-65535 ''' 61 | def __map_range(self, x, in_min, in_max, out_min, out_max): 62 | return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min 63 | 64 | ''' new_speed is a value from 0% - 100% ''' 65 | def change_speed(self, new_speed): 66 | new_duty_cycle = self.__map_range(new_speed, 0, 100, 40000, 65535) 67 | self.current_speed = new_duty_cycle 68 | 69 | 70 | def deinit(self): 71 | """deinit PWM Pins""" 72 | print("Deinitializing PWM Pins") 73 | self.stop() 74 | utime.sleep(0.1) 75 | self.left_motor_pin1.deinit() 76 | self.left_motor_pin2.deinit() 77 | self.right_motor_pin1.deinit() 78 | self.right_motor_pin2.deinit() 79 | 80 | -------------------------------------------------------------------------------- /ldr-photoresistor/README.MD: -------------------------------------------------------------------------------- 1 | ## Writeup 2 | https://www.donskytech.com/micropython-read-ldr-or-photoresistor/ 3 | 4 | 5 | ![Featured Image - MicroPython - Read LDR or PhotoResistor](https://user-images.githubusercontent.com/69466026/219956667-82439f39-3c0e-479c-9d7b-3c5836121ed1.jpg) 6 | -------------------------------------------------------------------------------- /ldr-photoresistor/ldr_photoresistor_class.py: -------------------------------------------------------------------------------- 1 | from machine import Pin 2 | import time 3 | 4 | 5 | class LDR: 6 | def __init__(self, pin): 7 | self.ldr_pin = machine.ADC(Pin(pin)) 8 | 9 | def get_raw_value(self): 10 | return self.ldr_pin.read_u16() 11 | 12 | def get_light_percentage(self): 13 | return round(self.get_raw_value()/65535*100,2) 14 | 15 | ldr = LDR(27) 16 | 17 | while True: 18 | print(ldr.get_light_percentage()) 19 | time.sleep(1) -------------------------------------------------------------------------------- /ldr-photoresistor/ldr_photoresistor_pico_w.py: -------------------------------------------------------------------------------- 1 | from machine import Pin 2 | import time 3 | 4 | adc = machine.ADC(Pin(27)) 5 | 6 | while True: 7 | print(adc.read_u16()) 8 | time.sleep(1) -------------------------------------------------------------------------------- /microdot-dynamic-component-path/README.MD: -------------------------------------------------------------------------------- 1 | ![Featured Image - MicroDot Dynamic Content](https://user-images.githubusercontent.com/69466026/217284888-fa3a573b-ebfb-4276-9dac-a58d7007b2f4.png) 2 | 3 | # Write Up: 4 | [MicroDot/MicroPython – Handling dynamic content](https://www.donskytech.com/microdot-micropython-handling-dynamic-content/) 5 | -------------------------------------------------------------------------------- /microdot-dynamic-component-path/boot.py: -------------------------------------------------------------------------------- 1 | # boot.py -- run on boot-up 2 | import network 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() -------------------------------------------------------------------------------- /microdot-dynamic-component-path/color_service.py: -------------------------------------------------------------------------------- 1 | import ujson 2 | 3 | class ReadColorsService(): 4 | def __init__(self): 5 | pass 6 | 7 | def read_colors(self): 8 | with open('colors.json', 'r') as f: 9 | colors_dict = ujson.load(f) 10 | return colors_dict["colors"] 11 | -------------------------------------------------------------------------------- /microdot-dynamic-component-path/colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "name": "red", 5 | "rgb": [255, 0, 0] 6 | }, 7 | { 8 | "name": "green", 9 | "rgb": [0, 255, 0] 10 | }, 11 | { 12 | "name": "blue", 13 | "rgb": [0, 0, 255] 14 | }, 15 | { 16 | "name": "aqua", 17 | "rgb": [0, 255, 255] 18 | }, 19 | { 20 | "name": "fuschia", 21 | "rgb": [255, 0, 255] 22 | }, 23 | { 24 | "name": "white", 25 | "rgb": [255, 255, 255] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /microdot-dynamic-component-path/main.py: -------------------------------------------------------------------------------- 1 | from microdot_asyncio import Microdot, Response, send_file 2 | from microdot_utemplate import render_template 3 | from color_service import ReadColorsService 4 | from rgb_led import RGBLEDModule 5 | 6 | # Initialize MicroDot 7 | app = Microdot() 8 | Response.default_content_type = 'text/html' 9 | 10 | # Read the colors 11 | color_service = ReadColorsService() 12 | led_colors = color_service.read_colors() 13 | 14 | # Set the GPIO pins 15 | rgb_led_module = RGBLEDModule([13 , 14, 15]) 16 | 17 | # root route 18 | @app.route('/') 19 | async def index(request): 20 | return render_template('index.html', colors=led_colors) 21 | 22 | # toggle RGB Module color 23 | @app.route('/toggle-led/') 24 | async def index(request, color): 25 | for led_color in led_colors: 26 | if color == led_color['name']: 27 | rgb_led_module.set_rgb_color(led_color['rgb']) 28 | break 29 | return {"status": "OK"} 30 | 31 | # Static CSS/JSS 32 | @app.route("/static/") 33 | def static(request, path): 34 | if ".." in path: 35 | # directory traversal is not allowed 36 | return "Not found", 404 37 | return send_file("static/" + path) 38 | 39 | # shutdown 40 | @app.get('/shutdown') 41 | def shutdown(request): 42 | rgb_led_module.deinit_pwms() 43 | request.app.shutdown() 44 | return 'The server is shutting down...' 45 | 46 | 47 | if __name__ == "__main__": 48 | try: 49 | app.run() 50 | except KeyboardInterrupt: 51 | rgb_led_module.deinit_pwms() 52 | -------------------------------------------------------------------------------- /microdot-dynamic-component-path/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-dynamic-component-path/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 self._invoke_handler( 385 | handler, req, res) or res 386 | elif f in self.error_handlers: 387 | res = await self._invoke_handler( 388 | self.error_handlers[f], req) 389 | else: 390 | res = 'Not found', f 391 | except HTTPException as exc: 392 | if exc.status_code in self.error_handlers: 393 | res = self.error_handlers[exc.status_code](req) 394 | else: 395 | res = exc.reason, exc.status_code 396 | except Exception as exc: 397 | print_exception(exc) 398 | exc_class = None 399 | res = None 400 | if exc.__class__ in self.error_handlers: 401 | exc_class = exc.__class__ 402 | else: 403 | for c in mro(exc.__class__)[1:]: 404 | if c in self.error_handlers: 405 | exc_class = c 406 | break 407 | if exc_class: 408 | try: 409 | res = await self._invoke_handler( 410 | self.error_handlers[exc_class], req, exc) 411 | except Exception as exc2: # pragma: no cover 412 | print_exception(exc2) 413 | if res is None: 414 | if 500 in self.error_handlers: 415 | res = await self._invoke_handler( 416 | self.error_handlers[500], req) 417 | else: 418 | res = 'Internal server error', 500 419 | else: 420 | if 400 in self.error_handlers: 421 | res = await self._invoke_handler(self.error_handlers[400], req) 422 | else: 423 | res = 'Bad request', 400 424 | if isinstance(res, tuple): 425 | res = Response(*res) 426 | elif not isinstance(res, Response): 427 | res = Response(res) 428 | return res 429 | 430 | async def _invoke_handler(self, f_or_coro, *args, **kwargs): 431 | ret = f_or_coro(*args, **kwargs) 432 | if _iscoroutine(ret): 433 | ret = await ret 434 | return ret 435 | 436 | 437 | abort = Microdot.abort 438 | Response.already_handled = Response() 439 | redirect = Response.redirect 440 | send_file = Response.send_file 441 | -------------------------------------------------------------------------------- /microdot-dynamic-component-path/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-dynamic-component-path/rgb_led.py: -------------------------------------------------------------------------------- 1 | import time 2 | from machine import Pin,PWM 3 | 4 | #RGB 5 | RED = 0 6 | GREEN = 1 7 | BLUE = 2 8 | 9 | # Class that wil interface with our RGB Module 10 | class RGBLEDModule: 11 | def __init__(self, pwm_pins): 12 | self.pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])), 13 | PWM(Pin(pwm_pins[BLUE]))] 14 | self.init_pwms() 15 | 16 | # Initialize PWM Pins 17 | def init_pwms(self): 18 | for pwm in self.pwms: 19 | pwm.freq(1000) 20 | 21 | # Deinitialize PWM fins 22 | def deinit_pwms(self): 23 | self.turn_off_rgb() 24 | for pwm in self.pwms: 25 | pwm.deinit() 26 | 27 | # Map RGB values from 0-255 to duty cycle 0-65535 28 | def map_range(self, x, in_min, in_max, out_min, out_max): 29 | return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min 30 | 31 | # Turn off RGB 32 | def turn_off_rgb(self): 33 | self.pwms[RED].duty_u16(0) 34 | self.pwms[GREEN].duty_u16(0) 35 | self.pwms[BLUE].duty_u16(0) 36 | time.sleep(0.1) 37 | 38 | # Set RGB Color 39 | def set_rgb_color(self, color): 40 | red, green, blue = color 41 | 42 | self.turn_off_rgb() 43 | 44 | self.pwms[RED].duty_u16(self.map_range(red, 0, 255, 0, 65535)) 45 | self.pwms[GREEN].duty_u16(self.map_range(green, 0, 255, 0, 65535)) 46 | self.pwms[BLUE].duty_u16(self.map_range(blue, 0, 255, 0, 65535)) 47 | -------------------------------------------------------------------------------- /microdot-dynamic-component-path/static/index.js: -------------------------------------------------------------------------------- 1 | function toggleLED(color){ 2 | console.log(color); 3 | fetch(`/toggle-led/${color}`) 4 | .then(response => { 5 | console.log(response) 6 | }) 7 | .catch(error => { 8 | console.log(error) 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /microdot-dynamic-component-path/templates/index.html: -------------------------------------------------------------------------------- 1 | {% args colors %} 2 | 3 | 4 | 5 | Microdot Dynamic Component in Path 6 | 7 | 8 |

Which LED?

9 |
10 | {% for color in colors %} 11 |
12 | 13 | 14 |
15 | {% endfor %} 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /mqtt-bme280-weather-station/README.MD: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mqtt-bme280-weather-station/bme_module.py: -------------------------------------------------------------------------------- 1 | import machine 2 | import bme280 3 | import math 4 | 5 | class BME280Module: 6 | SEA_LEVEL_PRESSURE_HPA = 1013.25 7 | def __init__(self, id, scl_pin, sda_pin): 8 | self.i2c = machine.I2C(id=id, scl=machine.Pin(scl_pin), sda=machine.Pin(sda_pin), freq=400000) 9 | self.bme = bme280.BME280(i2c=self.i2c) 10 | 11 | def get_sensor_readings(self): 12 | (temperature, pressure, humidity) = self.bme.values 13 | temperature_val = float(temperature[:len(temperature) - 1]) 14 | humidity_val = float(humidity[:len(humidity) - 1]) 15 | pressure_val = float(pressure[:len(pressure) - 3]) 16 | 17 | # Altitude calculation 18 | altitude_val = 44330 * (1.0 - math.pow(pressure_val / BME280Module.SEA_LEVEL_PRESSURE_HPA, 0.1903)) 19 | 20 | return (temperature_val, pressure_val, humidity_val, altitude_val) 21 | 22 | 23 | -------------------------------------------------------------------------------- /mqtt-bme280-weather-station/boot.py: -------------------------------------------------------------------------------- 1 | # boot.py -- run on boot-up 2 | import network, utime, machine 3 | 4 | # Replace the following with your WIFI Credentials 5 | SSID = "" 6 | SSID_PASSWORD = "" 7 | 8 | 9 | def do_connect(): 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, SSID_PASSWORD) 15 | while not sta_if.isconnected(): 16 | print("Attempting to connect....") 17 | utime.sleep(1) 18 | print('Connected! Network config:', sta_if.ifconfig()) 19 | 20 | print("Connecting to your wifi...") 21 | do_connect() -------------------------------------------------------------------------------- /mqtt-bme280-weather-station/main.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ubinascii 3 | from umqtt.simple import MQTTClient 4 | import machine 5 | import random 6 | from bme_module import BME280Module 7 | import ujson 8 | 9 | 10 | # Default MQTT_BROKER to connect to 11 | MQTT_BROKER = "192.168.100.22" 12 | CLIENT_ID = ubinascii.hexlify(machine.unique_id()) 13 | SUBSCRIBE_TOPIC = b"led" 14 | PUBLISH_TOPIC = b"sensorReadings" 15 | 16 | # Publish MQTT messages after every set timeout 17 | last_publish = time.time() 18 | publish_interval = 5 19 | 20 | # Pin assignment 21 | I2C_ID = 0 22 | SCL_PIN = 1 23 | SDA_PIN = 0 24 | bme_module = BME280Module(I2C_ID,SCL_PIN,SDA_PIN) 25 | 26 | # Received messages from subscriptions will be delivered to this callback 27 | def sub_cb(topic, msg): 28 | print((topic, msg)) 29 | if msg.decode() == "ON": 30 | led.value(1) 31 | else: 32 | led.value(0) 33 | 34 | # Reset the device in case of error 35 | def reset(): 36 | print("Resetting...") 37 | time.sleep(5) 38 | machine.reset() 39 | 40 | # Read the BMP/BME280 readings 41 | def get_temperature_reading(): 42 | return bme_module.get_sensor_readings() 43 | 44 | # Main program 45 | def main(): 46 | print(f"Begin connection with MQTT Broker :: {MQTT_BROKER}") 47 | mqttClient = MQTTClient(CLIENT_ID, MQTT_BROKER, keepalive=60) 48 | mqttClient.set_callback(sub_cb) 49 | mqttClient.connect() 50 | mqttClient.subscribe(SUBSCRIBE_TOPIC) 51 | print(f"Connected to MQTT Broker :: {MQTT_BROKER}, and waiting for callback function to be called!") 52 | while True: 53 | # Non-blocking wait for message 54 | mqttClient.check_msg() 55 | global last_publish 56 | current_time = time.time() 57 | if (current_time - last_publish) >= publish_interval: 58 | temperature, pressure, humidity, altitude = get_temperature_reading() 59 | readings = {"temperature": temperature, "pressure": pressure,"humidity": humidity, "altitude": altitude} 60 | 61 | mqttClient.publish(PUBLISH_TOPIC, ujson.dumps(readings).encode()) 62 | last_publish = current_time 63 | time.sleep(1) 64 | 65 | 66 | if __name__ == "__main__": 67 | while True: 68 | try: 69 | main() 70 | except OSError as e: 71 | print("Error: " + str(e)) 72 | reset() 73 | -------------------------------------------------------------------------------- /rgb-module/README.MD: -------------------------------------------------------------------------------- 1 | 2 | ![Featured Image - MicroPython RGB LED](https://user-images.githubusercontent.com/69466026/217149072-c33ea59b-d03b-4679-a452-adcd083b70e6.jpg) 3 | 4 | # Writeup 5 | [Control RGB LED using MicroPython](https://www.donskytech.com/control-rgb-led-using-micropython/). 6 | -------------------------------------------------------------------------------- /rgb-module/basic_rgb_show.py: -------------------------------------------------------------------------------- 1 | import time 2 | from machine import Pin,PWM 3 | 4 | # RGB 5 | RED = 0 6 | GREEN = 1 7 | BLUE = 2 8 | 9 | # Declare pins 10 | pwm_pins = [13,14,15] 11 | # Setup pins for PWM 12 | pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])), 13 | PWM(Pin(pwm_pins[BLUE]))] 14 | # Set pwm frequency 15 | [pwm.freq(1000) for pwm in pwms] 16 | 17 | # Deinitialize PWM on all pins 18 | def deinit_pwm_pins(): 19 | pwms[RED].deinit() 20 | pwms[GREEN].deinit() 21 | pwms[BLUE].deinit() 22 | 23 | # main function 24 | def main(): 25 | while True: 26 | # Display RED 27 | pwms[RED].duty_u16(65535) 28 | pwms[GREEN].duty_u16(0) 29 | pwms[BLUE].duty_u16(0) 30 | time.sleep(1) 31 | 32 | # Display GREEN 33 | pwms[RED].duty_u16(0) 34 | pwms[GREEN].duty_u16(65535) 35 | pwms[BLUE].duty_u16(0) 36 | time.sleep(1) 37 | 38 | # Display BLUE 39 | pwms[RED].duty_u16(0) 40 | pwms[GREEN].duty_u16(0) 41 | pwms[BLUE].duty_u16(65535) 42 | time.sleep(1) 43 | 44 | if __name__ == "__main__": 45 | try: 46 | main() 47 | except KeyboardInterrupt: 48 | deinit_pwm_pins() 49 | 50 | -------------------------------------------------------------------------------- /rgb-module/custom_color.py: -------------------------------------------------------------------------------- 1 | import time 2 | from machine import Pin,PWM 3 | 4 | #RGB 5 | RED = 0 6 | GREEN = 1 7 | BLUE = 2 8 | 9 | # Declare pins 10 | pwm_pins = [13,14,15] 11 | # Setup pins for PWM 12 | pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])), 13 | PWM(Pin(pwm_pins[BLUE]))] 14 | # Set pwm frequency 15 | [pwm.freq(1000) for pwm in pwms] 16 | 17 | # Colors that we are going to display 18 | colors = {"fuschia": (255, 0, 255), "yellow": (255,255,0), "aqua": (0, 255, 255) 19 | , "orange": (230, 138 , 0), "white": (255, 255 , 255)} 20 | 21 | def map_range(x, in_min, in_max, out_min, out_max): 22 | return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min 23 | 24 | def turn_off_rgb(): 25 | pwms[RED].duty_u16(0) 26 | pwms[GREEN].duty_u16(0) 27 | pwms[BLUE].duty_u16(0) 28 | time.sleep(0.1) 29 | 30 | # Deinitialize PWM on all pins 31 | def deinit_pwm_pins(): 32 | pwms[RED].deinit() 33 | pwms[GREEN].deinit() 34 | pwms[BLUE].deinit() 35 | 36 | # main function 37 | def main(): 38 | while True: 39 | for key, color in colors.items(): 40 | # Turn off each RGB 41 | turn_off_rgb() 42 | 43 | print(f"Displaying Color:: {key}") 44 | red, green, blue = color 45 | 46 | pwms[RED].duty_u16(map_range(red, 0, 255, 0, 65535)) 47 | pwms[GREEN].duty_u16(map_range(green, 0, 255, 0, 65535)) 48 | pwms[BLUE].duty_u16(map_range(blue, 0, 255, 0, 65535)) 49 | time.sleep(2) 50 | 51 | 52 | if __name__ == "__main__": 53 | try: 54 | main() 55 | except KeyboardInterrupt: 56 | deinit_pwm_pins() 57 | 58 | -------------------------------------------------------------------------------- /rgb-module/rgb_dim_brightness.py: -------------------------------------------------------------------------------- 1 | import time 2 | from machine import Pin,PWM 3 | 4 | # RGB 5 | RED = 0 6 | GREEN = 1 7 | BLUE = 2 8 | 9 | # Declare pins 10 | pwm_pins = [13,14,15] 11 | # Setup pins for PWM 12 | pwms = [PWM(Pin(pwm_pins[RED])),PWM(Pin(pwm_pins[GREEN])), 13 | PWM(Pin(pwm_pins[BLUE]))] 14 | # Set pwm frequency 15 | [pwm.freq(1000) for pwm in pwms] 16 | 17 | def turn_off_rgb(): 18 | pwms[RED].duty_u16(0) 19 | pwms[GREEN].duty_u16(0) 20 | pwms[BLUE].duty_u16(0) 21 | time.sleep(0.1) 22 | 23 | # Deinitialize PWM on all pins 24 | def deinit_pwm_pins(): 25 | pwms[RED].deinit() 26 | pwms[GREEN].deinit() 27 | pwms[BLUE].deinit() 28 | 29 | # main function 30 | def main(): 31 | while True: 32 | for pwm in pwms: 33 | # Turn off each RGB 34 | turn_off_rgb() 35 | 36 | for duty_value in range(0, 65535, 16): 37 | pwm.duty_u16(duty_value) 38 | time.sleep(0.001) 39 | 40 | if __name__ == "__main__": 41 | try: 42 | main() 43 | except KeyboardInterrupt: 44 | deinit_pwm_pins() -------------------------------------------------------------------------------- /umqtt.simple/boot.py: -------------------------------------------------------------------------------- 1 | # boot.py -- run on boot-up 2 | import network, utime, machine 3 | 4 | # Replace the following with your WIFI Credentials 5 | SSID = "" 6 | SSID_PASSWORD = "" 7 | 8 | 9 | def do_connect(): 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, SSID_PASSWORD) 15 | while not sta_if.isconnected(): 16 | print("Attempting to connect....") 17 | utime.sleep(1) 18 | print('Connected! Network config:', sta_if.ifconfig()) 19 | 20 | print("Connecting to your wifi...") 21 | do_connect() 22 | -------------------------------------------------------------------------------- /umqtt.simple/main.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ubinascii 3 | from umqtt.simple import MQTTClient 4 | import machine 5 | import random 6 | 7 | # Default MQTT_BROKER to connect to 8 | MQTT_BROKER = "192.168.100.22" 9 | CLIENT_ID = ubinascii.hexlify(machine.unique_id()) 10 | SUBSCRIBE_TOPIC = b"led" 11 | PUBLISH_TOPIC = b"temperature" 12 | 13 | # Setup built in PICO LED as Output 14 | led = machine.Pin("LED",machine.Pin.OUT) 15 | 16 | # Publish MQTT messages after every set timeout 17 | last_publish = time.time() 18 | publish_interval = 5 19 | 20 | # Received messages from subscriptions will be delivered to this callback 21 | def sub_cb(topic, msg): 22 | print((topic, msg)) 23 | if msg.decode() == "ON": 24 | led.value(1) 25 | else: 26 | led.value(0) 27 | 28 | 29 | def reset(): 30 | print("Resetting...") 31 | time.sleep(5) 32 | machine.reset() 33 | 34 | # Generate dummy random temperature readings 35 | def get_temperature_reading(): 36 | return random.randint(20, 50) 37 | 38 | def main(): 39 | print(f"Begin connection with MQTT Broker :: {MQTT_BROKER}") 40 | mqttClient = MQTTClient(CLIENT_ID, MQTT_BROKER, keepalive=60) 41 | mqttClient.set_callback(sub_cb) 42 | mqttClient.connect() 43 | mqttClient.subscribe(SUBSCRIBE_TOPIC) 44 | print(f"Connected to MQTT Broker :: {MQTT_BROKER}, and waiting for callback function to be called!") 45 | while True: 46 | # Non-blocking wait for message 47 | mqttClient.check_msg() 48 | global last_publish 49 | if (time.time() - last_publish) >= publish_interval: 50 | random_temp = get_temperature_reading() 51 | mqttClient.publish(PUBLISH_TOPIC, str(random_temp).encode()) 52 | last_publish = time.time() 53 | time.sleep(1) 54 | 55 | 56 | if __name__ == "__main__": 57 | while True: 58 | try: 59 | main() 60 | except OSError as e: 61 | print("Error: " + str(e)) 62 | reset() 63 | 64 | -------------------------------------------------------------------------------- /websocket_using_microdot/README.MD: -------------------------------------------------------------------------------- 1 | # Write Up 2 | https://www.donskytech.com/using-websocket-in-micropython-a-practical-example/ 3 | 4 | ![Featured Image - Using WebSocket in MicroPython - A Practical Example](https://user-images.githubusercontent.com/69466026/220387186-31b15ce8-5893-47ef-b01f-f56d896247f7.jpg) 5 | -------------------------------------------------------------------------------- /websocket_using_microdot/boot.py: -------------------------------------------------------------------------------- 1 | # boot.py -- run on boot-up 2 | import network 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() -------------------------------------------------------------------------------- /websocket_using_microdot/ldr_photoresistor_module.py: -------------------------------------------------------------------------------- 1 | import machine 2 | 3 | class LDR: 4 | def __init__(self, pin): 5 | self.ldr_pin = machine.ADC(machine.Pin(pin)) 6 | 7 | def get_raw_value(self): 8 | return self.ldr_pin.read_u16() 9 | 10 | def get_light_percentage(self): 11 | return round(self.get_raw_value()/65535*100,2) 12 | 13 | -------------------------------------------------------------------------------- /websocket_using_microdot/main.py: -------------------------------------------------------------------------------- 1 | from microdot_asyncio import Microdot, Response, send_file 2 | from microdot_utemplate import render_template 3 | from microdot_asyncio_websocket import with_websocket 4 | from ldr_photoresistor_module import LDR 5 | import time 6 | 7 | # Initialize MicroDot 8 | app = Microdot() 9 | Response.default_content_type = 'text/html' 10 | 11 | # LDR module 12 | ldr = LDR(27) 13 | 14 | # root route 15 | @app.route('/') 16 | async def index(request): 17 | return render_template('index.html') 18 | 19 | 20 | @app.route('/ws') 21 | @with_websocket 22 | async def read_sensor(request, ws): 23 | while True: 24 | # data = await ws.receive() 25 | time.sleep(.1) 26 | await ws.send(str(ldr.get_light_percentage())) 27 | 28 | # Static CSS/JSS 29 | @app.route("/static/") 30 | def static(request, path): 31 | if ".." in path: 32 | # directory traversal is not allowed 33 | return "Not found", 404 34 | return send_file("static/" + path) 35 | 36 | 37 | # shutdown 38 | @app.get('/shutdown') 39 | def shutdown(request): 40 | request.app.shutdown() 41 | return 'The server is shutting down...' 42 | 43 | 44 | if __name__ == "__main__": 45 | try: 46 | app.run() 47 | except KeyboardInterrupt: 48 | pass 49 | -------------------------------------------------------------------------------- /websocket_using_microdot/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 | after_request_handled = False 351 | if req: 352 | if req.content_length > req.max_content_length: 353 | if 413 in self.error_handlers: 354 | res = await self._invoke_handler( 355 | self.error_handlers[413], req) 356 | else: 357 | res = 'Payload too large', 413 358 | else: 359 | f = self.find_route(req) 360 | try: 361 | res = None 362 | if callable(f): 363 | for handler in self.before_request_handlers: 364 | res = await self._invoke_handler(handler, req) 365 | if res: 366 | break 367 | if res is None: 368 | res = await self._invoke_handler( 369 | f, req, **req.url_args) 370 | if isinstance(res, tuple): 371 | body = res[0] 372 | if isinstance(res[1], int): 373 | status_code = res[1] 374 | headers = res[2] if len(res) > 2 else {} 375 | else: 376 | status_code = 200 377 | headers = res[1] 378 | res = Response(body, status_code, headers) 379 | elif not isinstance(res, Response): 380 | res = Response(res) 381 | for handler in self.after_request_handlers: 382 | res = await self._invoke_handler( 383 | handler, req, res) or res 384 | for handler in req.after_request_handlers: 385 | res = await self._invoke_handler( 386 | handler, req, res) or res 387 | after_request_handled = True 388 | elif f in self.error_handlers: 389 | res = await self._invoke_handler( 390 | self.error_handlers[f], req) 391 | else: 392 | res = 'Not found', f 393 | except HTTPException as exc: 394 | if exc.status_code in self.error_handlers: 395 | res = self.error_handlers[exc.status_code](req) 396 | else: 397 | res = exc.reason, exc.status_code 398 | except Exception as exc: 399 | print_exception(exc) 400 | exc_class = None 401 | res = None 402 | if exc.__class__ in self.error_handlers: 403 | exc_class = exc.__class__ 404 | else: 405 | for c in mro(exc.__class__)[1:]: 406 | if c in self.error_handlers: 407 | exc_class = c 408 | break 409 | if exc_class: 410 | try: 411 | res = await self._invoke_handler( 412 | self.error_handlers[exc_class], req, exc) 413 | except Exception as exc2: # pragma: no cover 414 | print_exception(exc2) 415 | if res is None: 416 | if 500 in self.error_handlers: 417 | res = await self._invoke_handler( 418 | self.error_handlers[500], req) 419 | else: 420 | res = 'Internal server error', 500 421 | else: 422 | if 400 in self.error_handlers: 423 | res = await self._invoke_handler(self.error_handlers[400], req) 424 | else: 425 | res = 'Bad request', 400 426 | if isinstance(res, tuple): 427 | res = Response(*res) 428 | elif not isinstance(res, Response): 429 | res = Response(res) 430 | if not after_request_handled: 431 | for handler in self.after_error_request_handlers: 432 | res = await self._invoke_handler( 433 | handler, req, res) or res 434 | return res 435 | 436 | async def _invoke_handler(self, f_or_coro, *args, **kwargs): 437 | ret = f_or_coro(*args, **kwargs) 438 | if _iscoroutine(ret): 439 | ret = await ret 440 | return ret 441 | 442 | 443 | abort = Microdot.abort 444 | Response.already_handled = Response() 445 | redirect = Response.redirect 446 | send_file = Response.send_file 447 | -------------------------------------------------------------------------------- /websocket_using_microdot/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 | -------------------------------------------------------------------------------- /websocket_using_microdot/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 | -------------------------------------------------------------------------------- /websocket_using_microdot/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 | -------------------------------------------------------------------------------- /websocket_using_microdot/static/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-white: #fff; 3 | --color-dark-variant: #f3f5f8; 4 | --border-radius-1: 0.4rem; 5 | } 6 | 7 | * { 8 | margin: 0; 9 | padding: 0; 10 | outline: 0; 11 | } 12 | 13 | body { 14 | width: 100vw; 15 | height: 100vh; 16 | overflow-x: hidden; 17 | background: var(--color-dark-variant); 18 | } 19 | h1 { 20 | margin-top: 0.4rem; 21 | margin-left: 1.6rem; 22 | } 23 | 24 | .container { 25 | display: grid; 26 | width: 96%; 27 | margin: 0 auto; 28 | gap: 1.8rem; 29 | grid-template-columns: 14rem auto; 30 | } 31 | aside { 32 | margin-top: 1.4rem; 33 | height: 100vh; 34 | background: var(--color-white); 35 | border-radius: var(--border-radius-1); 36 | padding: 0.4rem; 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | justify-content: center; 41 | } 42 | .values { 43 | text-align: center; 44 | resize: none; 45 | height: 100%; 46 | font-size: 1.5rem; 47 | font-weight: bold; 48 | } 49 | 50 | main { 51 | margin-top: 1.4rem; 52 | background: var(--color-white); 53 | padding: 0.4rem; 54 | /* border: 1px solid red; */ 55 | text-align: center; 56 | } 57 | 58 | .wrapper { 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | } 63 | -------------------------------------------------------------------------------- /websocket_using_microdot/static/index.js: -------------------------------------------------------------------------------- 1 | const sensorValues = document.querySelector("#sensor-values"); 2 | 3 | const sensorData = []; 4 | 5 | /* 6 | Plotly.js graph and chart setup code 7 | */ 8 | var sensorChartDiv = document.getElementById("sensor-chart"); 9 | 10 | // History Data 11 | var sensorTrace = { 12 | x: [], 13 | y: [], 14 | name: "LDR/Photoresistor", 15 | mode: "lines+markers", 16 | type: "line", 17 | }; 18 | 19 | var sensorLayout = { 20 | autosize: false, 21 | width: 800, 22 | height: 500, 23 | colorway: ["#05AD86"], 24 | margin: { t: 40, b: 40, l: 80, r: 80, pad: 0 }, 25 | xaxis: { 26 | gridwidth: "2", 27 | autorange: true, 28 | }, 29 | yaxis: { 30 | gridwidth: "2", 31 | autorange: true, 32 | }, 33 | }; 34 | var config = { responsive: true }; 35 | 36 | Plotly.newPlot(sensorChartDiv, [sensorTrace], sensorLayout, config); 37 | 38 | // Will hold the sensor reads 39 | let newSensorXArray = []; 40 | let newSensorYArray = []; 41 | 42 | // The maximum number of data points displayed on our scatter/line graph 43 | let MAX_GRAPH_POINTS = 50; 44 | let ctr = 0; 45 | 46 | function updateChart(sensorRead) { 47 | if (newSensorXArray.length >= MAX_GRAPH_POINTS) { 48 | newSensorXArray.shift(); 49 | } 50 | if (newSensorYArray.length >= MAX_GRAPH_POINTS) { 51 | newSensorYArray.shift(); 52 | } 53 | newSensorXArray.push(ctr++); 54 | newSensorYArray.push(sensorRead); 55 | 56 | var data_update = { 57 | x: [newSensorXArray], 58 | y: [newSensorYArray], 59 | }; 60 | 61 | Plotly.update(sensorChartDiv, data_update); 62 | } 63 | 64 | // WebSocket support 65 | var targetUrl = `ws://${location.host}/ws`; 66 | var websocket; 67 | window.addEventListener("load", onLoad); 68 | 69 | function onLoad() { 70 | initializeSocket(); 71 | } 72 | 73 | function initializeSocket() { 74 | console.log("Opening WebSocket connection MicroPython Server..."); 75 | websocket = new WebSocket(targetUrl); 76 | websocket.onopen = onOpen; 77 | websocket.onclose = onClose; 78 | websocket.onmessage = onMessage; 79 | } 80 | function onOpen(event) { 81 | console.log("Starting connection to WebSocket server.."); 82 | } 83 | function onClose(event) { 84 | console.log("Closing connection to server.."); 85 | setTimeout(initializeSocket, 2000); 86 | } 87 | function onMessage(event) { 88 | console.log("WebSocket message received:", event); 89 | updateValues(event.data); 90 | updateChart(event.data); 91 | } 92 | 93 | function sendMessage(message) { 94 | websocket.send(message); 95 | } 96 | 97 | function updateValues(data) { 98 | sensorData.unshift(data); 99 | if (sensorData.length > 20) sensorData.pop(); 100 | sensorValues.value = sensorData.join("\r\n"); 101 | } 102 | -------------------------------------------------------------------------------- /websocket_using_microdot/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MicroPython WebSocket 8 | 9 | 10 | 11 | 12 | 13 |

MicroPython WebSocket

14 |
15 | 25 |
26 |

LDR/Photoresistor

27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | --------------------------------------------------------------------------------