├── LICENSE ├── README.md ├── docs ├── images │ ├── clean-dir.png │ ├── edit-configurations.png │ ├── gh-python-examples.png │ ├── micropython-tools.png │ ├── mpremote-mount.png │ ├── mpy_sdcard_log_output.png │ ├── pc-connect.png │ ├── pc-execute.png │ ├── pc-file-system.png │ ├── pc-mp-settings.png │ ├── pc-run-execute.png │ ├── pc-run-upload.png │ ├── pc-run.png │ ├── pc-sources-root.png │ ├── pc-start-dir.png │ ├── pc-upload-project.png │ ├── pycharm-framework.png │ ├── pycharm-install.png │ ├── pycharm-new-project.png │ ├── sublime-print-platform.png │ ├── thonny-board.png │ ├── thonny-file.png │ ├── thonny-new.png │ ├── thonny-run.png │ ├── thonny-shell.png │ ├── thonny-view.png │ └── tmp117-web-server.png ├── linux_setup.md ├── mcu_setup.md └── qwiic_setup.md └── examples ├── README.md ├── mpy_rgb_blink ├── README.md └── mpy_rgb_blink.py ├── mpy_rgb_ramp ├── README.md └── mpy_rgb_ramp.py ├── mpy_sdcard_log ├── README.md └── mpy_sdcard_log.py └── mpy_tmp117_web_server ├── README.md ├── microdot ├── __init__.py ├── helpers.py ├── microdot.py └── websocket.py ├── static ├── index.css ├── index.html └── logo.png ├── tmp117_server_ap.py └── wlan_ap ├── __init__.py ├── config_ap_linux.py └── config_ap_micropython.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 SparkFun Electronics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![SparkFun Python Exampoles](docs/images/gh-python-examples.png "SparkFun Python Examples") 2 | 3 | # SparkFun Python Examples 4 | 5 | ![License](https://img.shields.io/github/license/sparkfun/sparkfun-python) 6 | ![Release](https://img.shields.io/github/v/release/sparkfun/sparkfun-python) 7 | ![Release Date](https://img.shields.io/github/release-date/sparkfun/sparkfun-python) 8 | ![GitHub issues](https://img.shields.io/github/issues/sparkfun/sparkfun-python) 9 | 10 | ## SparkFun Qwiic 11 | 12 | [Using Qwiic in Python](docs/qwiic_setup.md) 13 | 14 | ## Setup SparkFun Boards and Devices 15 | 16 | [Using a Raspberry Pi or NVIDIA Orin](docs/linux_setup.md) 17 | 18 | [Setup and Using a Microcontroller Board](docs/mcu_setup.md) 19 | 20 | ## Examples 21 | 22 | [Available Python Examples](examples/README.md) 23 | -------------------------------------------------------------------------------- /docs/images/clean-dir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/clean-dir.png -------------------------------------------------------------------------------- /docs/images/edit-configurations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/edit-configurations.png -------------------------------------------------------------------------------- /docs/images/gh-python-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/gh-python-examples.png -------------------------------------------------------------------------------- /docs/images/micropython-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/micropython-tools.png -------------------------------------------------------------------------------- /docs/images/mpremote-mount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/mpremote-mount.png -------------------------------------------------------------------------------- /docs/images/mpy_sdcard_log_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/mpy_sdcard_log_output.png -------------------------------------------------------------------------------- /docs/images/pc-connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-connect.png -------------------------------------------------------------------------------- /docs/images/pc-execute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-execute.png -------------------------------------------------------------------------------- /docs/images/pc-file-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-file-system.png -------------------------------------------------------------------------------- /docs/images/pc-mp-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-mp-settings.png -------------------------------------------------------------------------------- /docs/images/pc-run-execute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-run-execute.png -------------------------------------------------------------------------------- /docs/images/pc-run-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-run-upload.png -------------------------------------------------------------------------------- /docs/images/pc-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-run.png -------------------------------------------------------------------------------- /docs/images/pc-sources-root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-sources-root.png -------------------------------------------------------------------------------- /docs/images/pc-start-dir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-start-dir.png -------------------------------------------------------------------------------- /docs/images/pc-upload-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pc-upload-project.png -------------------------------------------------------------------------------- /docs/images/pycharm-framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pycharm-framework.png -------------------------------------------------------------------------------- /docs/images/pycharm-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pycharm-install.png -------------------------------------------------------------------------------- /docs/images/pycharm-new-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/pycharm-new-project.png -------------------------------------------------------------------------------- /docs/images/sublime-print-platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/sublime-print-platform.png -------------------------------------------------------------------------------- /docs/images/thonny-board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/thonny-board.png -------------------------------------------------------------------------------- /docs/images/thonny-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/thonny-file.png -------------------------------------------------------------------------------- /docs/images/thonny-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/thonny-new.png -------------------------------------------------------------------------------- /docs/images/thonny-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/thonny-run.png -------------------------------------------------------------------------------- /docs/images/thonny-shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/thonny-shell.png -------------------------------------------------------------------------------- /docs/images/thonny-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/thonny-view.png -------------------------------------------------------------------------------- /docs/images/tmp117-web-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/docs/images/tmp117-web-server.png -------------------------------------------------------------------------------- /docs/linux_setup.md: -------------------------------------------------------------------------------- 1 | # Using Qwiic with Python on Linux 2 | 3 | ## Contents 4 | * [Supported Platforms](#hardware) 5 | * [Raspberry Pi Setup](#raspberry-pi-setup) 6 | * [Jetson Orin Nano Setup](#jetson-orin-nano-setup) 7 | * [Qwiic Shim Setup](#qwiic-shim-or-qwiic-cable-female-jumper-setup) 8 | * [Installation](#installation) 9 | * [Drivers](#drivers) 10 | 11 | ## Supported Platforms 12 | [Raspberry Pi](https://www.sparkfun.com/raspberry-pi-5-8gb.html) , [NVIDIA Jetson Orin Nano](https://www.sparkfun.com/nvidia-jetson-orin-nano-developer-kit.html) via the [SparkFun Qwiic SHIM](https://www.sparkfun.com/sparkfun-qwiic-shim-for-raspberry-pi.html) 13 | 14 | ## Raspberry Pi Setup 15 | Follow the [instructions here](https://www.raspberrypi.com/software/operating-systems/) to to download a Raspberry PI OS image. It is expected that most/all RaspberryPi OS versions will work with our Qwiic I2C drivers, but testing was done with Kernel v6.6, Debian GNU/Linux 12 (bookworm). Image an SD card with your favorite SD card imager, we recommend the [Raspberry Pi Imager](https://www.raspberrypi.com/software/). 16 | 17 | ## Jetson Orin Nano Setup 18 | Follow the [in-depth instructions here](https://developer.nvidia.com/embedded/learn/get-started-jetson-orin-nano-devkit#intro) to set up your Jetson Orin Nano Developer kit with a JetPack 6.2 (or higher) Linux image. 19 | 20 | ## Qwiic Shim or Qwiic Cable Female Jumper Setup 21 | On either of the above platforms, a pysical hardware interface is required to connect the I2C pins of the board to a qwiic connector on a qwiic device. Connect a [Qwiic Shim](https://www.sparkfun.com/sparkfun-qwiic-shim-for-raspberry-pi.html) or a [Qwiic Cable Female Jumper](https://www.sparkfun.com/flexible-qwiic-cable-female-jumper-4-pin.html) to your board, making sure to connect the correct pins for PWR, GND, SDA, and SCL. See the [instructions here](https://learn.sparkfun.com/tutorials/qwiic-shim-for-raspberry-pi-hookup-guide) for more information. Then connect a qwiic cable to your SparkFun Qwiic Device. 22 | 23 | > [!Warning] 24 | > IMPROPER ORIENTATION OF A QWIIC SHIM CAN SHORT POWER TO GROUND, DAMAGING YOUR BOARD! 25 | 26 | ## Installation 27 | You can install the [qwiic_i2c_py](https://github.com/sparkfun/Qwiic_I2C_Py) package to get Qwiic I2C support for your board. Also check out our comprehensive [qwiic_py](https://github.com/sparkfun/Qwiic_Py) repository. 28 | 29 | The qwiic_i2c_py package is primarily installed using the `pip3` command, downloading the package from the Python Index - "PyPi". 30 | 31 | First, setup a virtual environment from a specific directory using venv: 32 | ```sh 33 | python3 -m venv ~/sparkfun_venv 34 | ``` 35 | You can pass any path instead of ~/sparkfun_venv, just make sure you use the same one for all future steps. For more information on venv [click here](https://docs.python.org/3/library/venv.html). 36 | 37 | Next, install the qwiic package with: 38 | ```sh 39 | ~/sparkfun_venv/bin/pip3 install sparkfun-qwiic-i2c 40 | ``` 41 | Now you should be able to run any example or custom python scripts that have `import qwiic_i2c` by running e.g.: 42 | ```sh 43 | ~/sparkfun_venv/bin/python3 example_script.py 44 | ``` 45 | 46 | To get started with any of the Qwiic Drivers, check out our list of Qwiic Python Driver Repos below and follow the device-specific "PyPi Installation" instructions in your device's repository. 47 | 48 | As an alternative to pip, at you could also manually clone/download the qwiic_i2c_py repository and the repository for your desired driver and then utilize the qwiic files directly. 49 | 50 | ## Drivers 51 | Check out our growing list of Python Drivers: [https://github.com/topics/sparkfun-python](https://github.com/topics/sparkfun-python) 52 | 53 | -------------------------------------------------------------------------------- /docs/mcu_setup.md: -------------------------------------------------------------------------------- 1 | # Setup and Using MicroPython 2 | 3 | ## Contents 4 | * [Supported Platforms](#hardware) 5 | * [Latest MicroPython Firmware Downloads](#latest-micropython-firmware-downloads) 6 | * [RP2 Boards](#rp2-boards) 7 | * [ESP32 Boards](#esp32-boards) 8 | * [Suggested Development Environments](#suggested-development-environments) 9 | * [mpremote](#mpremote-micropython-remote-control) 10 | * [Thonny](#thonny) 11 | * [PyCharm](#pycharm) 12 | * [Drivers](#drivers) 13 | 14 | ## Supported Platforms 15 | [SparkFun Pro Micro RP2350](https://www.sparkfun.com/sparkfun-pro-micro-rp2350.html), [SparkFun IoT RedBoard ESP32](https://www.sparkfun.com/sparkfun-iot-redboard-esp32-micropython-development-board.html), [SparkFun IoT RedBoard RP2350](https://www.sparkfun.com/sparkfun-iot-redboard-rp2350.html) 16 | 17 | And more to come... 18 | 19 | ## Latest MicroPython Firmware Downloads 20 | Get our latest MicroPython firmware for your board from our [MicroPython release page](https://github.com/sparkfun/micropython/releases). Different platforms have different methods of flashing: 21 | 22 | 23 | ### RP2 Boards 24 | While connected to your computer, hold the "boot" button on the RP2 board while you press and release the "reset" button to enter bootloader mode. Your board will appear as a regular drive on your computer that you can add files to. Drag and drop the correct .uf2 file from the most recent release from the link above onto your board and it will reboot, now running MicroPython. 25 | 26 | Connect to it with one of the [suggested development environments](#suggested-development-environments) below. 27 | 28 | ### ESP32 Boards 29 | Download the .zip archive for your board from the release link above and extract it. If you have not already, [download the esptool utility](https://docs.espressif.com/projects/esptool/en/latest/esp32/installation.html). Then, use ```esptool``` to flash your board using the command specified in the README.md contained in the .zip archive you downloaded for your board. Make sure you run the command from within that directory as well. For example, one ESP32 release contains a `bootloader.bin`, `partition-table.bin`, `micropython.bin`, and `README.md`. By reading the `README.md` I see that the command I must run FROM WITHIN THIS EXTRACTED DIRECTORY is: 30 | 31 | ```python -m esptool --chip esp32 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 4MB --flash_freq 40m 0x1000 bootloader.bin 0x8000 partition-table.bin 0x10000 micropython.bin``` 32 | 33 | Connect to it with one of the [suggested development environments](#suggested-development-environments) below. 34 | 35 | ## Suggested Development Environments 36 | 37 | ### mpremote: MicroPython remote control 38 | [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) is a command line utility that provides tons of options for interfacing with a MicroPython board. A simple way to use it is to execute it standalone with no options. If you have installed mpremote you can simply execute ```mpremote``` in a command line to get direct access to the Python REPL on your board. A useful way to navigate the file system from this repl is to execute ```import os``` and then use the `os` methods. For example, ```os.listdir()``` will show everything in the current directory on your MicroPython board. ```os.getcwd()``` will print the name of the current directory and ```os.chdir('dir_name')``` will change the directory. An example of navigating around directories for a user who has installed the [mpy_tmp117_web_server](https://github.com/sparkfun/sparkfun-python/tree/main/examples/mpy_tmp117_web_server) demo from this repository can be seen below. 39 | 40 | ``` 41 | C:\Users\qwiic_guy> mpremote 42 | 43 | Connected to MicroPython at COM14 44 | Use Ctrl-] or Ctrl-x to exit this shell 45 | MicroPython on SparkFun IoT RedBoard RP2350 with RP2350 46 | Type "help()" for more information. 47 | >>> 48 | >>> 49 | >>> import os 50 | >>> os.listdir() 51 | ['lib', 'static', 'tmp117_server_ap.py'] 52 | >>> os.getcwd() 53 | '/' 54 | >>> os.chdir('static') 55 | >>> os.getcwd() 56 | '/static' 57 | >>> os.listdir() 58 | ['index.css', 'index.html', 'logo.png'] 59 | ``` 60 | 61 | Once you have navigated to the directory containing the python script that you want to run, run it with the exec command: 62 | 63 | ``` 64 | >>> exec(open('your_script.py').read()) 65 | ``` 66 | 67 | Other MicroPython development environments like the IDE's below will also provide you with a REPL where you can directly execute MicroPython commands. So skills gained from navigating the REPL directly with `mpremote` will carry over into other environments. 68 | 69 | To get files from your computer onto your micropython board you can use ```mpremote cp``` or install them directly from repositories that support mip installation with ```mpremote mip install github:reponame``` for example, to install our qwiic_i2c_py driver, execute 70 | 71 | ``` 72 | mpremote mip install github:sparkfun/qwiic_i2c_py 73 | ``` 74 | 75 | Let's walk through a quick example where we develop a program on a local code editor and then run it on a MicroPython board. 76 | 77 | We can either develop our files first and then manually copy them over to our board (with `mpremote cp`) each time we want to test them, or we can "mount" a directory such that files are "shared" between the local file system and the MicroPython board. 78 | 79 | Let's explore using [mpremote mount](https://docs.micropython.org/en/latest/reference/mpremote.html#mpremote-command-mount) to map a local directory onto our remote device. First create a new directory named `hello_world` and then open it in your code editor of choice. Now lets add our Python/MicroPython program. Add a file called `print_platform.py` to your hello_world directory. Paste the following code into the file: 80 | 81 | ```python 82 | import sys 83 | 84 | print ("Hello from (Micro)Python! I am running on the following platform:", sys.platform) 85 | ``` 86 | 87 | The `sys` module exists both in Python and MicroPython so this code can run on both, and will let us know if we are successfully running it on an MCU. `sys.platform` will display your computer's OS if we interpret this program with a Python interpreter on your computer (for example on Windows it is `win32` and on linux it is `linux` or `linux2`). However, if we interpret/execute it via MicroPython on your MCU, it will be the MicroPython port representing your MCU (for example for RP2350 it is `rp2` and for ESP32 it is `esp32`). 88 | 89 | Now, my local directory structure in my code editor looks like this: 90 | ![sublime-print-platform](/docs/images/sublime-print-platform.png "sublime-print-platfrom") 91 | 92 | If I run `mpremote` and use `os.listdir` to list the current contents on my board, I see that it is empty: 93 | ``` 94 | C:\Users\awesome_qwiic_user> mpremote 95 | Connected to MicroPython at COM14 96 | Use Ctrl-] or Ctrl-x to exit this shell 97 | 98 | >>> import os 99 | >>> os.listdir() 100 | [] 101 | >>> 102 | ``` 103 | 104 | Now let's mount our directory. Issue `mpremote mount {path to your hello_world directory}`. When we issue the command and again view the contents on our board, we see that our file has appeared! 105 | 106 | ![mpremote-mount](/docs/images/mpremote-mount.png "mpremote-mount") 107 | 108 | Notice that our file `print_platform.py` is now accessible to us from our MicroPython board and how we have automatically been moved into a directory called `/remote` on the remote device that maps to the local `hello_world` directory. 109 | 110 | Now let's run our file using the same `exec(open('print_platform.py').read())` and see what happens. 111 | 112 | ``` 113 | >>> exec(open('print_platform.py').read()) 114 | Hello from (Micro)Python! I am running on the following platform: rp2 115 | ``` 116 | 117 | If all went well, we'll see our hello message and the name of an MCU platform (in this case RP2 for the RP2350). 118 | 119 | You can add any number of files to the `hello_world` directory in a local code-editor, and modify them as you wish and the changes will be reflected "on-the-fly" in your mpremote session. 120 | 121 | ### Thonny 122 | [Thonny](https://thonny.org/) is an IDE that provides a GUI environment with builtin support for MicroPython development. To get started, visit the [Thonny Downloads page](https://thonny.org/) and download the correct version for your operating system. Run the installation/setup program that you just downloaded for Thonny and click through each of the setup pages by accepting the default settings and pressing `Next`. 123 | 124 | Connect your board that already has [MicroPython Firmware installed](#latest-micropython-firmware-downloads) to your computer and then configure your interpreter by clicking the bottom right-hand corner of Thonny. If your serial drivers are up to date and your board has proper MicroPython firmware installed, clicking this interpreter box should show several MicroPython options: 125 | 126 | ![thonny-board](/docs/images/thonny-board.png "Thonny Board") 127 | 128 | Select the version of MicroPython that makes the most sense for your board. Not sure? Select ```MicroPython (generic)```. 129 | 130 | This will connect to your board and show a Python REPL in the "shell" tab. To run a MicroPython program, open it from the ```MicroPython device``` tab. Then press the green arrow (Run Current Script). If you ever want to stop the running program, soft reset your board, or reconnect to your board, click the red stop sign (Stop/Restart backend). 131 | 132 | Let's run the same Python example in Thonny as we did for mpremote. Once connected to your board, via the instructions above, ensure that the `View > Files` option is selected from the Toolbar: 133 | 134 | ![thonny-view](/docs/images/thonny-view.png "thonny-view") 135 | 136 | Now, in the `Files` tab, you should see two filel explorers, one called "This computer" for your local filesystem and one called "MicroPython device" representing the files on your board. Right click in the "MicroPython device" area and select `New file...`. 137 | 138 | ![thonny-new](/docs/images/thonny-new.png "thonny-new") 139 | 140 | Lets call our new file `print_platform.py`. Let's copy and paste the same python code from the `mpremote` section above into our new file and save it. Now it should appear in our `Files > MicroPython device` tab: 141 | 142 | ![thonny-file](/docs/images/thonny-file.png "thonny-file") 143 | 144 | Finally, let's click on the green `Run current script` button and in the `Shell` tab we should see the expected print with a platform that matches the MCU of our MicroPython board: 145 | 146 | ![thonny-run](/docs/images/thonny-run.png "thonny-run") 147 | 148 | ![thonny-shell](/docs/images/thonny-shell.png "thonny-shell") 149 | 150 | Remember that you can also still use the REPL directly from the `Shell` tab and execute MicroPython commands on your board just as if you were using `mpremote`. 151 | 152 | ### PyCharm 153 | 154 | [PyCharm](https://www.jetbrains.com/pycharm/) is a popular and modern Python IDE with plugin support for interfacing with MicroPython boards. PyCharm Professional is a paid version, but we suggest installing the free community version. To get started, visit the [PyCharm Downloads Page](https://www.jetbrains.com/pycharm/download/) and scroll down until you see the "PyCharm Community Addition" section and click the `Download` button. Open the setup executable that you downloaded and configure your installation. We suggest accepting the default installation folder and adding the Desktop Shortcut and Context Menu. 155 | 156 | ![pycharm-install](/docs/images/pycharm-install.png "PyCharm Install") 157 | 158 | Once your install is complete, open PyCharm. Skip any import settings that pop up when you open it for the first time. 159 | 160 | Navigate to Plugins and in the "Marketplace" tab search for "MicroPython Tools". Note: this is a third-party plugin with no explicit support or maintenance from Jetbrain or SparkFun. Older versions of PyCharm contain a JetBrains-supported plugin called simply "MicroPython" but they no longer update it and it cannot run on the latest PyCharm versions. 161 | 162 | ![micropython-tools](/docs/images/micropython-tools.png "MicroPython Tools") 163 | 164 | After installing the plugin, restart your IDE. Then, select the gear icon and choose "Settings" then navigate to Languages & Frameworks and select `MicroPython Tools`. 165 | 166 | ![pycharm-framework](/docs/images/pycharm-framework.png "PyCharm-Framework") 167 | 168 | Select `Enable MicroPython support` and leave the other defaults checked. SparkFun is in the process of getting stubs for SparkFun MicroPython boards added to the official repository, but in the meantime in the `stubs package` field, choose a generic MicroPython stubs package corresponding to your MCU. For an RP2350 or RP2040 board, we suggest the most recent version of `micropython-rp2-stubs_x.xx.x`. For ESP32 boards, we suggest the most recent version of `micropython-esp32-stubs_x.xx.x`. These stubs packages simply provide useful code-completion, highlighting, and warnings for MicroPython development. 169 | 170 | ![pc-mp-settings.png](/docs/images/pc-mp-settings.png "pc-mp-settings") 171 | 172 | 173 | 174 | A good starting place for the use of this plugin is the [MicroPython Tools README](https://github.com/lukaskremla/micropython-tools-jetbrains/blob/main/README.md). Let's create our first project. Click the "+" sign or select `file > New Project...` to create a new project. Let's name our project "hello_world" and accept the default interpreter/environment: 175 | 176 | ![pycharm-new-project.png](/docs/images/pycharm-new-project.png "pycharm-new-project") 177 | 178 | When our new project first opens up, it has our `hello_world` directory as well as several components like `.venv` and `External Libraries` that are helpful when doing regular Python Development. 179 | 180 | ![pc-start-dir.png](/docs/images/pc-start-dir.png "pc-start-dir") 181 | 182 | But we are anything but regular. We will be using the MicroPython running on our MCU to interpret our code, not a Python interpreter installed on your computer. So you can mostly disregard these files. 183 | 184 | Now, right click the `hello_world` directory and select `Mark Directory as > MicroPython Sources Root`. The MicroPython Tools plugin will now map this `hello_world` directory that exists on our computer to the root file system of our MicroPython board when we perform upload commands. 185 | 186 | ![pc-sources-root.png](/docs/images/pc-sources-root.png "pc-sources-root") 187 | 188 | Now lets add our MicroPython program. Right click the `hello_world` directory and select `new > Python File` and add a file called `print_platform.py`. Paste the following code into the file: 189 | 190 | ```python 191 | import sys 192 | 193 | print ("Hello from (Micro)Python! I am running on the following platform:", sys.platform) 194 | ``` 195 | 196 | This is the same code as is discussed in the [mpremote]() section. 197 | 198 | Now lets configure an upload command and a run command. At the top of PyCharm next to the execute and debug buttons, select `Current File > Edit Configurations` 199 | 200 | ![edit-configurations.png](/docs/images/edit-configurations.png "edit-configurations") 201 | 202 | In the Run/Debug Configurations window that pops up, select the "+" sign and then select `MicoPython Tools > Upload Project`. Check the boxes for `Reset on success`, `Switch to REPL tab on success`. Then, click `Apply`. 203 | 204 | ![pc-upload-project.png](/docs/images/pc-upload-project.png "pc-upload-project") 205 | 206 | While we're at it, lets create an execute command. Again, click the "+" sign and select `MicroPython Tools > Execute`. Input the path to our print_platform.py file we have created and check the box for `Switch to REPL tab on success`. Click `Apply` and finally `OK` to save our upload and execute commands. 207 | 208 | ![pc-execute.png](/docs/images/pc-execute.png "pc-execute") 209 | 210 | The upload command will take the directory that is configured as `MicroPython Sources Root` (in our case the hello_world directory) and load that onto the root directory of our MicroPython connected board. The execute command will run whatever file we have selected from the local computer's file system and run it on our board (without explicitly uploading it to the device). 211 | 212 | Now, let's connect to our device! Plug in your board that already has [MicroPython Firmware installed](#latest-micropython-firmware-downloads) and then in the bottom-left of PyCharm, select the MicroPython Tools extension. Select the correct COM port for your board. And then click the plug symbol to connect. 213 | 214 | ![pc-connect.png](/docs/images/pc-connect.png "pc-connect") 215 | 216 | Now that we are connected, let's upload our program. Select our upload configuration from the drop-down at the top of PyCharm and then click the green `Run` arrow. 217 | 218 | ![pc-run-upload.png](/docs/images/pc-run-upload.png "pc-run-upload") 219 | 220 | Select the `File System` tab in the MicroPython tools extension tab and we should now see `print_platform.py` file uploaded to the device! 221 | 222 | ![pc-file-system.png](/docs/images/pc-file-system.png "pc-file-system") 223 | 224 | Finally, let's select our execute configuration and run it as well. 225 | 226 | ![pc-run-execute.png](/docs/images/pc-run-execute.png "pc-run-execute") 227 | 228 | If all goes well, you should see a hello statement printed in the REPL tab of the MicroPython Tools extension. The platform printed should match the MCU of your board and not be your computer's operating system. 229 | 230 | ![pc-run.png](/docs/images/pc-run.png "pc-run") 231 | 232 | Some things to note: 233 | - The execute job we configured uses the version of the file currently in our local `hello_world` directory and runs it directly without uploading it. The upload step is helpful however if we have multiple files that we want to upload at once, for example if the file we are actually running relies on other files. 234 | - Drag and drop is supported by this extension, such that you can simply drag files from the project explorer on the left of PyCharm to the `File System` tab of the extension to copy files from your local computer to your MicroPython board. 235 | - Another alternative to explicitly using an upload configuration is to select a file from the PyCharm project explorer and right click it and select either `Execute file in MicroPython REPL` or `Upload file to MicroPython device`. 236 | - Remember that you can still use the REPL directly from the `REPL` tab and execute MicroPython commands on your board just as if you were using `mpremote`. 237 | 238 | 239 | ### Other Tools 240 | * [VSCode with MicroPico Extension](https://marketplace.visualstudio.com/items?itemName=paulober.pico-w-go): If you happen to be developing on a Raspberry Pi pico platform, this offers a similar experience to Thonny and PyCharm. 241 | * [Arduino Lab For MicroPython](https://labs.arduino.cc/en/labs/micropython): Offers a simple IDE for MicroPython development with a similar look and feel to Arduino IDE. 242 | 243 | ## Drivers 244 | Check out our growing list of Python Drivers: [https://github.com/topics/sparkfun-python](https://github.com/topics/sparkfun-python) 245 | 246 | 247 | -------------------------------------------------------------------------------- /docs/qwiic_setup.md: -------------------------------------------------------------------------------- 1 | # Using the Qwiic Python Drivers 2 | SparkFun offers an ever-growing list of Qwiic Python Drivers that enable interfacing with SparkFun Qwiic devices in Python, MicroPython, and CircuitPython. Generally, each qwiic driver relies on the [qwiic_i2c_py](https://github.com/sparkfun/Qwiic_I2C_Py) driver to provide the cross-platform I2C functions used by each driver. To get started with the Qwiic Python drivers, grab a qwiic connector, your Qwiic device, a supported controller board with MicroPython, CircuitPython, or Linux installed and visit the qwiic__py software repository for your device from the driver list below. 3 | 4 | ## Contents 5 | * [List of Drivers](#list-of-drivers) 6 | * [Qwiic Python Repositories](#qwiic-i2c-repository) 7 | 8 | ## List of Drivers 9 | Check out our growing list of Python Drivers: [https://github.com/topics/sparkfun-python](https://github.com/topics/sparkfun-python) 10 | 11 | Follow the installation instructions for the repository in the list above that corresponds to your Qwiic device. Make sure to follow the instructions for which Python you are using (i.e. Python (Linux), MicroPython, or CircuitPython). 12 | 13 | ## Qwiic I2C Repository 14 | Check out the [qwiic_i2c_py](https://github.com/sparkfun/Qwiic_I2C_Py) directory as well as the [qwiic_py](https://github.com/sparkfun/Qwiic_Py) directory for more information on Qwiic, I2C, and Python. -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Python Examples 2 | 3 | ## Examples 4 | 5 | |Example| Description| 6 | |--|--| 7 | |[RGB LED Blink](mpy_rgb_blink/)| Demonstrates how to control and blink the on-board RGB LED using MicroPython| 8 | |[RGB LED Ramp](mpy_rgb_ramp/)| Demonstrates how to control and change colors on the on-board RGB LED using MicroPython| 9 | |[SD Card Data Logging](mpy_sdcard_log/)| Demonstrates how log collected sensor data to a file on a SD Card and also to the console| 10 | |[TMP117 Web Server](mpy_tmp117_web_server/)| Demonstrates how to configure a WLAN access point web server to publish TMP117 temperatures using MicroPython| 11 | -------------------------------------------------------------------------------- /examples/mpy_rgb_blink/README.md: -------------------------------------------------------------------------------- 1 | # MicroPython RGB LED Blink Example 2 | 3 | The mpy_rgb_blink demo writes to the NeoPixel LED on a MicroPython device (that has the "NEOPIXEL" pin defined). It demonstrates blinking the LED with different colors and fading the brightness of the LED higher and lower. 4 | 5 | ## Contents 6 | 7 | * [Hardware](#hardware) 8 | * [Installation](#installation) 9 | * [Code Explanation](#code-explanation) 10 | 11 | ## Hardware 12 | This example uses the built-in NeoPixel LED present on SparkFun development boards (such as the [IoT RedBoard RP2350](https://www.sparkfun.com/sparkfun-iot-redboard-rp2350.html) and the [IoT RedBoard ESP32 with MicroPython](https://www.sparkfun.com/sparkfun-iot-redboard-esp32-micropython-development-board.html)) and no additional hardware is needed! 13 | 14 | ## Installation 15 | Check out [mcu_setup.md](https://github.com/sparkfun/sparkfun-python/blob/main/docs/mcu_setup.md) to see how to create or copy a new file to a MicroPython device. Add the `mpy_rgb_blink.py` file from this directory to your MicroPython device and run it with your [tool of choice](https://github.com/sparkfun/sparkfun-python/blob/main/docs/mcu_setup.md#suggested-development-environments). 16 | 17 | ## Code Explanation 18 | ### Setting up the NEOPIXEL 19 | 20 | MicroPython has a built-in [`machine`](https://docs.micropython.org/en/latest/library/machine.html) module designed to enable developers to easily control hardware features. The [`machine.Pin`](https://docs.micropython.org/en/latest/library/machine.Pin.html) class is used for controlling hardware pins such as GPIOs. Usually, we pass a pin number when instantiating an instance of the `machine.Pin` class, but we can also pass a string to instantiate a named pin. Board developers can submit a "pins.csv" file to MicroPython to create a list of named pins that can be passed to `machine.Pin()` in place of a pin number. For example, if your are curious check out the [pins.csv file](https://github.com/micropython/micropython/blob/master/ports/rp2/boards/SPARKFUN_IOTREDBOARD_RP2350/pins.csv) for the IoT RedBoard RP2350. A common named pin is "NEOPIXEL" representing a NeoPixel LED (individually addressable RGB LED). Thus, our first step is to create a pin representing our LED by using this name: 21 | 22 | ```python 23 | pin = machine.Pin("NEOPIXEL") 24 | ``` 25 | 26 | The `neopixel` module is another module included in most MicroPython versions and allows us to easily interact with these NeoPixel LEDs. Lets create a [`neopixel.NeoPixel`](https://docs.micropython.org/en/latest/esp8266/tutorial/neopixel.html) object using the pin that we just created. We pass 1 to represent that we will only be interacting with a single LED. 27 | 28 | ```python 29 | led = neopixel.NeoPixel(pin, 1) 30 | ``` 31 | 32 | Our resulting `led` NeoPixel object allows us to write different RGB values to different LEDs. Since we only have one LED, we will only interact with `led[0]`. 33 | 34 | ### Blink Example 35 | Now, let's use the `led` object. 36 | 37 | ```python 38 | def blink_the_led(led, count=30): 39 | led[0] = (0, 0, 0) # LED OFF 40 | led.write() 41 | 42 | for i in range(count): 43 | R = random.randint(0, 180) 44 | G = random.randint(0, 180) 45 | B = random.randint(0, 180) 46 | 47 | led[0] = (R, G, B) # LED ON 48 | led.write() 49 | 50 | time.sleep_ms(BLINK_DELAY) 51 | 52 | led[0] = [0, 0, 0] # off 53 | led.write() 54 | time.sleep_ms(BLINK_DELAY//2) 55 | print(".", end="") 56 | ``` 57 | 58 | Notice how every time that we want to write the LED with a new value, we assign an RGB tuple to `led[0]` and then call ```write()```. To blink the LED with random colors, we simply need to turn the LEDs off by passing a tuple with R=0, G=0, B=0: 59 | 60 | ```python 61 | led[0] = (0, 0, 0) # LED OFF 62 | led.write() 63 | ``` 64 | 65 | Similarly, to assign new arbitrary led values, we can simply write them with: 66 | ```python 67 | led[0] = (R, G, B) # LED ON 68 | led.write() 69 | ``` 70 | 71 | ### Fade Example 72 | 73 | If we keep the ratio between R, G, and B the same, but raise or lower the value for all of them proportionally, we can keep the same color while changing the brightness of our LED. 74 | 75 | ```python 76 | def fade_in_out(led, color, fade_time=1000): 77 | for i in range(0, 256): 78 | led[0] = (int(color[0] * i / 255), int(color[1] 79 | * i / 255), int(color[2] * i / 255)) 80 | led.write() 81 | time.sleep_ms(fade_time // 256) 82 | 83 | for i in range(255, -1, -1): 84 | led[0] = (int(color[0] * i / 255), int(color[1] 85 | * i / 255), int(color[2] * i / 255)) 86 | led.write() 87 | time.sleep_ms(fade_time // 256) 88 | ``` 89 | 90 | This function takes an RGB tuple in `color` and then uses a loop to apply a multiplier to the passed in RGB values to vary their brightness. -------------------------------------------------------------------------------- /examples/mpy_rgb_blink/mpy_rgb_blink.py: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # @file mpy_rgb_blink.py 4 | # @brief This MicroPython file contains functions to control the on-board RGB LED on SparkFun MicroPython 5 | # enabled boards that have a RGB LED. 6 | # 7 | # @details 8 | # This module depends on the available `neopixel` library to control the RGB LED and the on-board 9 | # LED pin defined as "NEOPIXEL" and accessible via the machine module. 10 | # 11 | # @note This code is designed to work with the `neopixel` library and a compatible microcontroller, such as the 12 | # SparkFun IoT RedBoard - ESP32, or the SparkFun IoT RedBoard - RP2350 13 | # 14 | # @author SparkFun Electronics 15 | # @date March 2025 16 | # @copyright Copyright (c) 2024-2025, SparkFun Electronics Inc. 17 | # 18 | # SPDX-License-Identifier: MIT 19 | # @license MIT 20 | # 21 | 22 | import machine 23 | import neopixel 24 | import random 25 | import time 26 | 27 | BLINK_DELAY = 200 # delay in milliseconds 28 | # --------------------------------------------------------------------------------- 29 | 30 | 31 | def fade_in_out(led, color, fade_time=1000): 32 | """ 33 | @brief Fade the LED in and out to a given color. 34 | 35 | @param led The LED object to be controlled. It is expected to be a `neopixel.NeoPixel` object. 36 | @param color The RGB color to fade to. 37 | @param fade_time The time in milliseconds for the fade effect. Default is 1000 ms. 38 | 39 | """ 40 | 41 | # fade in 42 | for i in range(0, 256): 43 | led[0] = (int(color[0] * i / 255), int(color[1] 44 | * i / 255), int(color[2] * i / 255)) 45 | led.write() 46 | time.sleep_ms(fade_time // 256) 47 | 48 | # fade out 49 | for i in range(255, -1, -1): 50 | led[0] = (int(color[0] * i / 255), int(color[1] 51 | * i / 255), int(color[2] * i / 255)) 52 | led.write() 53 | time.sleep_ms(fade_time // 256) 54 | 55 | 56 | def rgb_fade_example(led, count=10): 57 | """ 58 | @brief Fade the LED in and out random color 59 | 60 | @param led The LED object to be controlled. It is expected to be a `neopixel.NeoPixel` object. 61 | @param count The number of times to fade the LED. Default is 1. 62 | 63 | """ 64 | 65 | led[0] = (0, 0, 0) # LED OFF 66 | led.write() 67 | 68 | for i in range(count): 69 | # generate random RGB values - use a lower range to avoid too bright colors 70 | R = random.randint(0, 255) 71 | G = random.randint(0, 255) 72 | B = random.randint(0, 255) 73 | 74 | fade_in_out(led, (R, G, B)) 75 | print(".", end="") 76 | 77 | 78 | def blink_the_led(led, count=30): 79 | """ 80 | @brief Blink the LED with random colors of count times. 81 | 82 | @param led The LED object to be controlled. It is expected to be a `neopixel.NeoPixel` object. 83 | @param count The number of times to blink the LED. Default is 1. 84 | 85 | """ 86 | 87 | led[0] = (0, 0, 0) # LED OFF 88 | led.write() 89 | 90 | for i in range(count): 91 | # generate random RGB values - use a lower range to avoid too bright colors 92 | R = random.randint(0, 180) 93 | G = random.randint(0, 180) 94 | B = random.randint(0, 180) 95 | 96 | led[0] = (R, G, B) # LED ON 97 | led.write() 98 | 99 | time.sleep_ms(BLINK_DELAY) 100 | 101 | # restore the color 102 | led[0] = [0, 0, 0] # off 103 | led.write() 104 | time.sleep_ms(BLINK_DELAY//2) 105 | print(".", end="") 106 | 107 | 108 | # --------------------------------------------------------------------------------- 109 | # rgb_blink_example 110 | 111 | def rgb_blink_example(led, count=20): 112 | """ 113 | @brief Demonstrates LED color blinking using the onboard NeoPixel. 114 | 115 | @details 116 | - Initializes the NeoPixel LED. 117 | - Blinks the LED through a random color sequence. 118 | 119 | """ 120 | # start at LED off 121 | led[0] = (0, 0, 0) 122 | led.write() 123 | 124 | blink_the_led(led, count) 125 | print() 126 | 127 | # --------------------------------------------------------------------------------- 128 | # Run the example when this file is loaded 129 | 130 | 131 | def run(): 132 | 133 | print("-----------------------------------------------------------") 134 | print("Running the SparkFun RGB blink example...") 135 | print("-----------------------------------------------------------") 136 | # the the pin object for the pin defined as "NEOPIXEL" 137 | try: 138 | pin = machine.Pin("NEOPIXEL") 139 | except ValueError: 140 | print( 141 | "Error: The NEOPIXEL pin is not defined. Please check your board configuration.") 142 | exit(0) 143 | 144 | led = neopixel.NeoPixel(pin, 1) # create a NeoPixel object with 1 LED 145 | print("Blink the LED with random colors:") 146 | rgb_blink_example(led) 147 | print("Fade in and out with random colors:") 148 | rgb_fade_example(led) 149 | print("Done!") 150 | 151 | 152 | run() 153 | -------------------------------------------------------------------------------- /examples/mpy_rgb_ramp/README.md: -------------------------------------------------------------------------------- 1 | # MicroPython RGB LED Ramp Example 2 | The mpy_rgb_ramp demo writes to the NeoPixel LED on a MicroPython device (that has the "NEOPIXEL" pin defined). It demonstrates blinking the LED, reading the current LED RGB value, and smoothly transitioning between the current RGB value and a target RGB value. 3 | 4 | ## Contents 5 | 6 | * [Hardware](#hardware) 7 | * [Installation](#installation) 8 | * [Code Explanation](#code-explanation) 9 | 10 | ## Hardware 11 | This example uses the built-in NeoPixel LED present on SparkFun development boards (such as the [IoT RedBoard RP2350](https://www.sparkfun.com/sparkfun-iot-redboard-rp2350.html) and the [IoT RedBoard ESP32 with MicroPython](https://www.sparkfun.com/sparkfun-iot-redboard-esp32-micropython-development-board.html)) and no additional hardware is needed! 12 | 13 | ## Installation 14 | Check out [mcu_setup.md](https://github.com/sparkfun/sparkfun-python/blob/main/docs/mcu_setup.md) to see how to create or copy a new file to a MicroPython device. Add the `mpy_rgb_blink.py` file from this directory to your MicroPython device and run it with your [tool of choice](https://github.com/sparkfun/sparkfun-python/blob/main/docs/mcu_setup.md#suggested-development-environments). 15 | 16 | ## Code Explanation 17 | ### Setting up the NEOPIXEL 18 | 19 | MicroPython has a built-in [`machine`](https://docs.micropython.org/en/latest/library/machine.html) module designed to enable developers to easily control hardware features. The [`machine.Pin`](https://docs.micropython.org/en/latest/library/machine.Pin.html) class is used for controlling hardware pins such as GPIOs. Usually, we pass a pin number when instantiating an instance of the `machine.Pin` class, but we can also pass a string to instantiate a named pin. Board developers can submit a "pins.csv" file to MicroPython to create a list of named pins that can be passed to `machine.Pin()` in place of a pin number. For example, if your are curious check out the [pins.csv file](https://github.com/micropython/micropython/blob/master/ports/rp2/boards/SPARKFUN_IOTREDBOARD_RP2350/pins.csv) for the IoT RedBoard RP2350. A common named pin is "NEOPIXEL" representing a NeoPixel LED (individually addressable RGB LED). Thus, our first step is to create a pin representing our LED by using this name: 20 | 21 | ```python 22 | pin = machine.Pin("NEOPIXEL") 23 | ``` 24 | 25 | The `neopixel` module is another module included in most MicroPython versions and allows us to easily interact with these NeoPixel LEDs. Lets create a [`neopixel.NeoPixel`](https://docs.micropython.org/en/latest/esp8266/tutorial/neopixel.html) object using the pin that we just created. We pass 1 to represent that we will only be interacting with a single LED. 26 | 27 | ```python 28 | led = neopixel.NeoPixel(pin, 1) 29 | ``` 30 | 31 | Our resulting `led` NeoPixel object allows us to write different RGB values to different LEDs. Since we only have one LED, we will only interact with `led[0]`. 32 | 33 | ### Winking the LED 34 | Now, let's use the `led` object. 35 | 36 | ```python 37 | def wink_led(led): 38 | cur_clr = led[0] # Read the current color 39 | 40 | # wink the LED ... off and on three times 41 | for i in range(0, 3): 42 | led[0] = [0, 0, 0] # off 43 | led.write() 44 | 45 | time.sleep_ms(100) 46 | 47 | # restore the color 48 | led[0] = cur_clr 49 | led.write() 50 | time.sleep_ms(100) 51 | ``` 52 | 53 | Notice how every time that we want to write the LED with a new value, we assign an RGB tuple to `led[0]` and then call ```write()```. To blink the LED with random colors, we simply need to turn the LEDs off by passing a tuple with R=0, G=0, B=0: 54 | 55 | ```python 56 | led[0] = (0, 0, 0) # LED OFF 57 | led.write() 58 | ``` 59 | 60 | Similarly, to assign new arbitrary led values, we can simply write them with: 61 | ```python 62 | led[0] = (R, G, B) # LED ON 63 | led.write() 64 | ``` 65 | 66 | We can read this RGB tuple by inspecting `led[0]` 67 | ```python 68 | cur_clr = led[0] # Read the current color 69 | ``` 70 | 71 | Thus by alternating between turning the LED on with its current RGB values and off with 0's, we can create a "winking" effect. 72 | 73 | ### Smoothly Transitioning the LED Color 74 | Now, let's see how to smoothly transition between two colors. 75 | 76 | ```python 77 | def led_transition(led, R, G, B): 78 | # get current led value - which is a tuple 79 | # Note - we convert to a list to support value assignment below. 80 | clrCurrent = list(led[0]) 81 | 82 | # How many increments during the transition 83 | inc = 51 # 255/5 84 | 85 | # how much to change a color component value every increment 86 | rInc = (R - clrCurrent[0]) / inc 87 | gInc = (G - clrCurrent[1]) / inc 88 | bInc = (B - clrCurrent[2]) / inc 89 | 90 | # loop - adjust color during each increment. 91 | for i in range(0, inc): 92 | 93 | # add the desired increment to each color component value. Use round() to convert the float value to an integer 94 | clrCurrent[0] = round(clrCurrent[0] + rInc) 95 | clrCurrent[1] = round(clrCurrent[1] + gInc) 96 | clrCurrent[2] = round(clrCurrent[2] + bInc) 97 | 98 | # set the new LED color and write (enable) it 99 | led[0] = clrCurrent 100 | led.write() 101 | 102 | # indicate process ... add a small delay 103 | print(".", end='') 104 | time.sleep_ms(20) 105 | ``` 106 | 107 | We read our current led value and convert it to a list (because you cannot have their elements directly assigned). Next we calculate the linear relationship between each of our current R, G, and B values and the target value we want them to reach. Finally, we use a loop to gradually change each RGB value. -------------------------------------------------------------------------------- /examples/mpy_rgb_ramp/mpy_rgb_ramp.py: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # @file mpy_rgb_ramp.py 4 | # @brief This MicroPython file contains functions to control the on-board RGB LED on SparkFun MicroPython 5 | # enabled boards that have a RGB LED. 6 | # 7 | # @details 8 | # This module depends on the available `neopixel` library to control the RGB LED and the on-board 9 | # LED pin defined as "NEOPIXEL" and accessible via the machine module. 10 | # 11 | # @note This code is designed to work with the `neopixel` library and a compatible microcontroller, such as the 12 | # SparkFun IoT RedBoard - ESP32, or the SparkFun IoT RedBoard - RP2350 13 | # 14 | # @author SparkFun Electronics 15 | # @date March 2025 16 | # @copyright Copyright (c) 2024-2025, SparkFun Electronics Inc. 17 | # 18 | # SPDX-License-Identifier: MIT 19 | # @license MIT 20 | # 21 | 22 | import machine 23 | import neopixel 24 | import time 25 | 26 | # --------------------------------------------------------------------------------- 27 | # Wink the LED by turning it off and 28 | 29 | 30 | def wink_led(led): 31 | """ 32 | @brief Wink the LED by turning it off and on three times. 33 | 34 | @param led The LED object to be controlled. It is expected to be a `neopixel.NeoPixel` object. 35 | 36 | @details 37 | - Saves the current color of the LED. 38 | - Turns the LED off and on three times with a delay of 100 milliseconds between each state change. 39 | - Restores the LED to its original color after winking. 40 | 41 | """ 42 | 43 | # safe the current color 44 | cur_clr = led[0] 45 | 46 | # wink the LED ... off and on three times 47 | for i in range(0, 3): 48 | led[0] = [0, 0, 0] # off 49 | led.write() 50 | 51 | time.sleep_ms(100) 52 | 53 | # restore the color 54 | led[0] = cur_clr 55 | led.write() 56 | time.sleep_ms(100) 57 | 58 | # --------------------------------------------------------------------------------- 59 | # Transition the current LED value to a given RGB value. 60 | # This function assumes pixel color/channel values are 8 bit (0-255) 61 | # 62 | 63 | 64 | def led_transition(led, R, G, B): 65 | """ 66 | @brief Transition the current LED value to a given RGB value. 67 | 68 | This function assumes pixel color/channel values are 8-bit (0-255). 69 | 70 | @param led The LED object to be controlled. It is expected to be a `neopixel.NeoPixel` object. 71 | @param R The target red color value (0-255). 72 | @param G The target green color value (0-255). 73 | @param B The target blue color value (0-255). 74 | 75 | @details 76 | - Retrieves the current color of the LED 77 | - transitions the current color to the provided color over a series of increments. 78 | - Also outputs a dot for each increment to indicate progress. 79 | 80 | @example 81 | @code 82 | led_transition(led, 255, 0, 0); // Transition to red color 83 | @endcode 84 | """ 85 | 86 | # get current led value - which is a tuple 87 | # Note - we convert to a list to support value assignment below. 88 | clrCurrent = list(led[0]) 89 | 90 | # How many increments during the transition 91 | inc = 51 # 255/5 92 | 93 | # how much to change a color component value every increment 94 | rInc = (R - clrCurrent[0]) / inc 95 | gInc = (G - clrCurrent[1]) / inc 96 | bInc = (B - clrCurrent[2]) / inc 97 | 98 | # loop - adjust color during each increment. 99 | for i in range(0, inc): 100 | 101 | # add the desired increment to each color component value. Use round() to convert the float value to an integer 102 | clrCurrent[0] = round(clrCurrent[0] + rInc) 103 | clrCurrent[1] = round(clrCurrent[1] + gInc) 104 | clrCurrent[2] = round(clrCurrent[2] + bInc) 105 | 106 | # set the new LED color and write (enable) it 107 | led[0] = clrCurrent 108 | led.write() 109 | 110 | # indicate process ... add a small delay 111 | print(".", end='') 112 | time.sleep_ms(20) 113 | 114 | 115 | # --------------------------------------------------------------------------------- 116 | # rgp_ramp_example 117 | 118 | def rgb_ramp_example(): 119 | """ 120 | @brief Demonstrates LED color transitions using the onboard NeoPixel. 121 | 122 | @details 123 | - Initializes the NeoPixel LED. 124 | - Transitions the LED through a series of colors: Blue, Red, Green, Yellow, White, and Off. 125 | - Winks the LED (turns it off and on three times) after each color transition. 126 | 127 | """ 128 | 129 | # the the pin object for the pin defined as "NEOPIXEL" 130 | try: 131 | pin = machine.Pin("NEOPIXEL") 132 | except ValueError: 133 | print( 134 | "Error: The NEOPIXEL pin is not defined. Please check your board configuration.") 135 | return 136 | 137 | led = neopixel.NeoPixel(pin, 1) # create a NeoPixel object with 1 LED 138 | 139 | # start at LED off 140 | led[0] = (0, 0, 0) 141 | led.write() 142 | 143 | print() 144 | print("RGB LED Color Transitions:") 145 | 146 | time.sleep_ms(100) 147 | 148 | # transition through a series of colors 149 | print("\t\t", end='') 150 | led_transition(led, 0, 0, 255) 151 | print(" ") 152 | wink_led(led) 153 | 154 | print("\t\t", end='') 155 | led_transition(led, 255, 0, 0) 156 | print(" ") 157 | wink_led(led) 158 | 159 | print("\t\t", end='') 160 | led_transition(led, 0, 255, 0) 161 | print(" ") 162 | wink_led(led) 163 | 164 | print("\t\t", end='') 165 | led_transition(led, 255, 255, 0) 166 | print(" ") 167 | wink_led(led) 168 | 169 | print("\t", end='') 170 | led_transition(led, 255, 255, 255) 171 | print(" ") 172 | wink_led(led) 173 | 174 | print("\t\t", end='') 175 | led_transition(led, 0, 0, 0) 176 | print(" ") 177 | 178 | # turn off the LED 179 | led[0] = (0, 0, 0) 180 | led.write() 181 | 182 | 183 | def run(): 184 | """ 185 | @brief Run the RGB ramp example. 186 | 187 | @details 188 | - Calls the rgb_ramp_example function to demonstrate LED color transitions. 189 | """ 190 | 191 | print("-----------------------------------------------------------") 192 | print("Running the SparkFun RGB ramp example...") 193 | print("-----------------------------------------------------------") 194 | rgb_ramp_example() 195 | print("Done!") 196 | 197 | 198 | run() 199 | -------------------------------------------------------------------------------- /examples/mpy_sdcard_log/README.md: -------------------------------------------------------------------------------- 1 | # MicroPython - Logging to a SD Card 2 | 3 | Leveraging the flexibility of MicroPython, this example demonstrates how to access a SD Card, collect (simulated) sensor data efficiently and write the information in a JSON format to a file on the the system SD Card as well as print out the results to a connected serial console. 4 | 5 | The key elements shown in this example include: 6 | 7 | - Mounting the on-board SDCard and determining if a SD Card is inserted on the device 8 | - Opening a file for writing on the SD Card 9 | - Implementing a logging loop that orchestrates the recording and output of sensed information. 10 | - Gathering sensor data in a python dictionary and writing this information in a JSON format. 11 | - Detecting a board type at runtime. 12 | 13 | ## Requirements and Setup 14 | 15 | ### Supported Development Boards 16 | 17 | Currently this example is setup to run on the following development boards running MicroPython: 18 | 19 | - [SparkFun IoT RedBoard - RP2350](https://www.sparkfun.com/sparkfun-iot-redboard-rp2350.html) 20 | - [SparkFun IoT RedBoard - ESP32 MicroPython](https://www.sparkfun.com/sparkfun-iot-redboard-esp32-micropython-development-board.html) 21 | - [Teensy 4.1](https://www.sparkfun.com/teensy-4-1.html) 22 | 23 | ### SD Card 24 | 25 | A FAT formatted SD card inserted into the Development Board. 26 | 27 | A great option for this is the [1GB SD Card](https://www.sparkfun.com/microsd-card-1gb-class-4.html) at SparkFun. 28 | 29 | ## Install the Demo 30 | 31 | The demo Python source code [mpy_sdcard_log.py](mpy_sdcard_log.py) must be installed on the development board. This is either peformed using a MicroPython enabled IDE (such as [Thonny](https://thonny.org) or [PyCharm](https://www.jetbrains.com/pycharm/download/?section=mac)). 32 | 33 | > [!NOTE] 34 | > For development boards that access the SDCard via a SPI connection (such as the SparkFun Line of IoT RedBaords), the MicroPython `sdcard` library must be available. If not included in the MicroPython firmware, this library is easily installed using the MicroPython tool [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html). 35 | > 36 | > To install the library, connect your MicroPython development board to your computer and issue the following command in a terminal window: 37 | > 38 | > ```sh 39 | > mpremote mip install sdcard 40 | > ``` 41 | 42 | ## Running the Demo 43 | 44 | Once the required demo files are loaded on the demo board, and a FAT formatted SDCard inserted, the demo is ready to run. To run the demo, connect to the development board and access the command line (often referred to as the [REPL](https://docs.micropython.org/en/latest/esp8266/tutorial/repl.html)). This is either done via an IDE interface, using the `mpremote` command or a Serial Terminal application. 45 | 46 | Once at the REPL command line, the demo runs when the demo python file is loaded. 47 | 48 | ```python 49 | >>> import mpy_sdcard_log 50 | ``` 51 | 52 | When the demo starts, the SD Card is mounted and the output file created. After this is complete, data is logged to the output file and to the console. The following is an example of the output: 53 | 54 | ![Demo Logging Output](../../docs/images/mpy_sdcard_log_output.png) 55 | 56 | Once the demo is complete, and while in the same MicroPython session, it is rerun using the following command: 57 | 58 | ```python 59 | >>>> mpy_sdcard_log.run() 60 | ``` 61 | -------------------------------------------------------------------------------- /examples/mpy_sdcard_log/mpy_sdcard_log.py: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # @file mpy_sdcard_log.py 4 | # @brief This MicroPython file contains functions to manage SD card logging on SparkFun MicroPython 5 | # enabled boards. 6 | # 7 | # @details 8 | # This module provides functionality to mount an SD card, read data from sensors, and log 9 | # the data to a file on the SD card. 10 | # 11 | # @note This code is designed to work with compatible microcontrollers, such as the 12 | # SparkFun IoT RedBoard - ESP32, or the SparkFun IoT RedBoard - RP2350 13 | # 14 | # @author SparkFun Electronics 15 | # @date March 2025 16 | # @copyright Copyright (c) 2024-2025, SparkFun Electronics Inc. 17 | # 18 | # SPDX-License-Identifier: MIT 19 | # @license MIT 20 | # 21 | 22 | # NOTE: This example requires the use of the sd card module for micro python. If not installed, the 23 | # module is installed using the following mpremote command: 24 | # mpremote mip install sdcard 25 | 26 | import os 27 | import uos 28 | import time 29 | import random 30 | import json 31 | 32 | SDCARD_MOUNT_POINT = "/sdcard" 33 | 34 | # global variable to track if the SD card is mounted 35 | _mounted_sd_card = False 36 | 37 | # Define the boards we support with this deme - This is a dictionary the key being 38 | # the board uname.machine value, and value a tuple that contains SPI bus number and CS ping number. 39 | SupportedBoards = { 40 | "SparkFun IoT RedBoard RP2350 with RP2350": (1, 9), 41 | "SparkFun IoT RedBoard ESP32 with ESP32": (2, 5), 42 | "Teensy 4.1 with MIMXRT1062DVJ6A": (-1, -1) 43 | } 44 | 45 | # ------------------------------------------------------------ 46 | 47 | 48 | def mount_sd_card(spi_bus, cs_pin): 49 | 50 | global _mounted_sd_card 51 | try: 52 | import sdcard 53 | except ImportError: 54 | print("[Error] sdcard module not found. Please install it.") 55 | return False 56 | 57 | from machine import Pin, SPI 58 | 59 | # Create the SPI object 60 | spi = SPI(spi_bus, baudrate=1000000, polarity=0, phase=0) 61 | # Create the CS pin object 62 | cs = Pin(cs_pin, Pin.OUT) 63 | 64 | # Create the SD card object 65 | try: 66 | sd = sdcard.SDCard(spi, cs) 67 | except Exception as e: 68 | print("[Error] ", e) 69 | return False 70 | 71 | # Mount the SD card 72 | try: 73 | vfs = uos.VfsFat(sd) 74 | uos.mount(vfs, SDCARD_MOUNT_POINT) 75 | except Exception as e: 76 | print("[Error] Failed to mount the SD Card", e) 77 | return False 78 | 79 | _mounted_sd_card = True 80 | return True 81 | 82 | 83 | def setup_sd_card(): 84 | """ 85 | Mounts an SD card to the filesystem. 86 | 87 | This function checks if the current board is supported, initializes the SPI bus and CS pin, 88 | creates the SD card object, and mounts the SD card to the filesystem. 89 | 90 | Returns: 91 | bool: True if the SD card was successfully mounted, False otherwise. 92 | 93 | """ 94 | 95 | # is this a supported board? 96 | board_name = os.uname().machine 97 | if board_name not in SupportedBoards: 98 | print("This board is not supported") 99 | return False 100 | 101 | # Get the SPI bus and CS pin for this board 102 | spi_bus, cs_pin = SupportedBoards[board_name] 103 | 104 | # do we need to mount the sd card? (Teensy auto mounts) 105 | status = False 106 | if spi_bus != -1: 107 | status = mount_sd_card(spi_bus, cs_pin) 108 | else: 109 | status = True 110 | 111 | if status is True: 112 | print("SD Card mounted successfully") 113 | 114 | return status 115 | 116 | # ------------------------------------------------------------ 117 | 118 | 119 | def read_data(observation): 120 | """ 121 | Simulates the collection of data and adds values to the observation dictionary. 122 | 123 | This function generates simulated data for time, temperature, humidity, and pressure, 124 | and adds these values to the provided observation dictionary. If connecting to an actual 125 | sensor, you would take readings from the sensor and add them to the observation dictionary 126 | within this function. 127 | 128 | Args: 129 | observation (dict): A dictionary to store the simulated sensor data. 130 | 131 | Returns: 132 | bool: Always returns True to indicate successful data collection. 133 | """ 134 | 135 | # Note: If connecting to a actual sensor, you would take readings from the sensor and add them to the 136 | # observation dictionary here. 137 | 138 | # Add the time 139 | observation["time"] = time.time() 140 | 141 | # Add the temperature 142 | observation["temperature"] = random.randrange(-340, 1000)/10 143 | 144 | # Add the humidity 145 | observation["humidity"] = random.randrange(0, 1000)/10. 146 | 147 | # Add the pressure 148 | observation["pressure"] = random.randrange(10, 1000)/10. 149 | 150 | # Success ! 151 | return True 152 | 153 | # ------------------------------------------------------------ 154 | # Setup the log file 155 | # This function opens the log file for writing 156 | # and returns the file object. 157 | # If the file cannot be opened, it returns None. 158 | 159 | 160 | def setup_log_file(filename): 161 | """ 162 | Sets up a log file on the SD card with the given filename. 163 | 164 | This function attempts to open a file on the SD card for writing. If the file 165 | cannot be opened, an error message is printed and None is returned. 166 | 167 | Args: 168 | filename (str): The name of the log file to be created on the SD card. 169 | 170 | Returns: 171 | file object: A file object for the opened log file if successful, or None if an error occurred. 172 | """ 173 | 174 | try: 175 | fs = open(SDCARD_MOUNT_POINT + os.sep + filename, "w") 176 | except Exception as e: 177 | print("[Error] Failed to open log file:", e) 178 | return None 179 | 180 | return fs 181 | 182 | # ------------------------------------------------------------ 183 | 184 | 185 | def log_data(filename, count=30, interval=50, to_console=True): 186 | """ 187 | Logs data to a specified file and optionally prints it to the console. 188 | 189 | Parameters: 190 | filename (str): The name of the file to log data to. 191 | count (int, optional): The number of times to log data. Default is 30. 192 | interval (int, optional): The interval (in milliseconds) between each log entry. Default is 50. 193 | to_console (bool, optional): If True, prints the data to the console. Default is True. 194 | 195 | Returns: 196 | None 197 | """ 198 | 199 | # Create the observation dictionary 200 | observation = {} 201 | 202 | # Open the log file 203 | fs = setup_log_file(filename) 204 | if fs is None: 205 | return 206 | 207 | # Loop for the number of times specified 208 | for i in range(count): 209 | 210 | observation.clear() 211 | observation["iteration"] = i 212 | 213 | # Read the data 214 | read_data(observation) 215 | 216 | # Write the data to the log file 217 | fs.write(json.dumps(observation) + "\n") 218 | 219 | if to_console: 220 | # Print the data to the console 221 | print(json.dumps(observation)) 222 | 223 | # Wait for the specified interval 224 | time.sleep(interval) 225 | 226 | # Close the log file 227 | fs.close() 228 | 229 | # ------------------------------------------------------------ 230 | 231 | 232 | def sdcard_log_example(filename="mpy_sdcard_log.txt", count=20, interval=2): 233 | """ 234 | Logs data to an SD card at specified intervals and prints the data to the console. 235 | 236 | Args: 237 | filename (str): The name of the file to log data to. Defaults to "mpy_sdcard_log.txt". 238 | count (int): The number of iterations to log data for. Defaults to 20. 239 | interval (int): The interval in seconds between each log entry. Defaults to 2. 240 | 241 | Returns: 242 | None 243 | """ 244 | 245 | global _mounted_sd_card 246 | print("Logging to: {filename}, every {interval} seconds for {count} iterations.\n".format( 247 | filename=filename, interval=interval, count=count)) 248 | # Mount the SD card 249 | if not setup_sd_card(): 250 | print("Failed to mount SD card") 251 | return 252 | 253 | print("\nLogging Data to SD Card and Console:") 254 | 255 | # Log the data 256 | log_data(filename, count=count, interval=interval, to_console=True) 257 | 258 | # Unmount the SD card if we need to 259 | if _mounted_sd_card: 260 | uos.umount(SDCARD_MOUNT_POINT) 261 | _mounted_sd_card = False 262 | print("\nSD Card unmounted successfully") 263 | 264 | # ------------------------------------------------------------ 265 | # Run method for the example 266 | 267 | 268 | def run(): 269 | """ 270 | Executes the SparkFun SD card logging example. 271 | 272 | """ 273 | print("-----------------------------------------------------------") 274 | print("Running the SparkFun sd card logging example...") 275 | print("-----------------------------------------------------------") 276 | 277 | sdcard_log_example() 278 | print() 279 | print("Done!") 280 | 281 | 282 | # run the demo/example on load 283 | run() 284 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/README.md: -------------------------------------------------------------------------------- 1 | 2 | ![TMP117-Web-Server](/docs/images/tmp117-web-server.png "TMP117 Web Server") 3 | 4 | # MicroPython TMP117 Web Server Example 5 | The mpy_tmp117_web_server demo application configures a MicroPython device or Raspberry Pi as a wireless access point that publishes TMP117 temperature data. The Microdot python web framework is used to set up a web server on your MicroPython or Python board. Static web elements are served to create a Web UI to display data and deliver commands to the device. 6 | 7 | While this simple demo is for the TMP117 and serves temperature data, use it as a starting point to develop your own web application for interfacing with any of our other [MicroPython supported Qwiic Devices](https://github.com/topics/sparkfun-python)! 8 | 9 | ## Contents 10 | 11 | * [Hardware](#hardware) 12 | * [Installation](#installation) 13 | * [Running the Example](#running-the-example) 14 | * [Using the Webpage](#using-the-webpage) 15 | * [Code Explanation](#code-explanation) 16 | * [References and More](#references-and-more) 17 | 18 | ## Hardware 19 | In order to run this demo you will need: 20 | 21 | * [Qwiic Cable](https://www.sparkfun.com/sparkfun-qwiic-cable-kit.html) 22 | * [TMP117 Temperature Sensor](https://www.sparkfun.com/sparkfun-high-precision-temperature-sensor-tmp117-qwiic.html) 23 | * A client computer, phone, tablet, etc. to view the web app. 24 | * EITHER: 25 | 1) MicroPython capable board with a WLAN interface and a [Qwiic Connector](https://www.sparkfun.com/qwiic) or broken-out I2C pins (we reccomend our [IoT RedBoard ESP32](https://www.sparkfun.com/sparkfun-iot-redboard-esp32-development-board.html) or [IoT RedBoard RP2350](https://www.sparkfun.com/sparkfun-iot-redboard-rp2350.html)). 26 | 27 | OR 28 | 29 | 2) A Raspberry Pi with the [Qwiic Shim](https://www.sparkfun.com/sparkfun-qwiic-shim-for-raspberry-pi.html) or a [Qwiic Cable Female Jumper](https://www.sparkfun.com/flexible-qwiic-cable-female-jumper-4-pin.html). Testing was done with a RaspberryPi 4 Model B Running Kernel v6.6, Debian GNU/Linux 12 (bookworm). 30 | 31 | Connect the TMP117 to your board with your chosen qwiic method and you're ready to go. 32 | 33 | ## Installation 34 | 35 | ### MicroPython 36 | If you are running the demo on a MicroPython board (and you did not purchase one of our boards with MicroPython pre-loaded on the board), first flash your board with MicroPython firmware. See the [most recent release of Sparkfun MicroPython](https://github.com/sparkfun/micropython/releases) and install the .uf2 (for RP2 boards) or .bin files (for ESP32 boards) corresponding to your board. 37 | 38 | Next, add the demo files to your board. You can do this manually, by copying the files from this directory (and from [qwiic_i2c_py](https://github.com/sparkfun/Qwiic_I2C_Py) and [qwiic_tmp117](https://github.com/sparkfun/Qwiic_TMP117_Py)) to your board with mpremote or with Thonny or another IDE. 39 | 40 | In any case, after installing, the file structure on your board should look like this: 41 | ``` 42 | / 43 | | 44 | +--- lib/ 45 | | |--- qwiic_i2c 46 | | |--- __init__.py 47 | | |--- micropython_i2c.py 48 | | `--- i2c_driver.py 49 | | |--- microdot 50 | | |--- __init__.py 51 | | |--- helpers.py 52 | | |--- microdot.py 53 | | `--- websocket.py 54 | | |--- wlan_ap 55 | | |--- __init__.py 56 | | |--- config_ap_micropython.py 57 | | `--- config_ap_linux.py 58 | | |--- qwiic_tmp117.py 59 | | 60 | +--- static/ 61 | | |--- index.css 62 | | |--- index.html 63 | | `--- logo.png 64 | | 65 | `--- tmp117_server_ap.py 66 | ``` 67 | 68 | ### Raspberry Pi 69 | On a Raspberry Pi running Linux, manually copy the files from this directory as well as those from [qwiic_i2c_py](https://github.com/sparkfun/Qwiic_I2C_Py) and [qwiic_tmp117_py](https://github.com/sparkfun/qwiic_tmp117_py) into the same directory. Alternatively, you can set up a virtual environment and install qwiic_i2c_py and qwiic_tmp117_py in the same venv path with pip3 as instructed in the READMEs for [qwiic_i2c_py](https://github.com/sparkfun/Qwiic_I2C_Py?tab=readme-ov-file#python) and [qwiic_tmp117_py](https://github.com/sparkfun/qwiic_tmp117_py/tree/master?tab=readme-ov-file#python). 70 | 71 | If you manually copy the files, your directory structure should look like this: 72 | ``` 73 | /your-directory-name 74 | | 75 | +--- qwiic_i2c 76 | | |--- __init__.py 77 | | |--- micropython_i2c.py 78 | | `--- i2c_driver.py 79 | +--- microdot 80 | | |--- __init__.py 81 | | |--- helpers.py 82 | | |--- microdot.py 83 | | `--- websocket.py 84 | +--- wlan_ap 85 | | |--- __init__.py 86 | | |--- config_ap_micropython.py 87 | | `--- config_ap_linux.py 88 | +--- qwiic_tmp117.py 89 | | 90 | +--- static/ 91 | | |--- index.css 92 | | |--- index.html 93 | | `--- logo.png 94 | | 95 | `--- tmp117_server_ap.py 96 | ``` 97 | 98 | If you used pip3 to install qwiic_i2c and qwiic_tmp117, you won't need them in the local directory as shown above (but you will need to run from the virtual environment where you installed them). 99 | 100 | ## Running the Example 101 | 102 | ### MicroPython 103 | 104 | #### Option 1: Thonny/Other IDE 105 | If using Thonny, connect your board, open the ```tmp117_server_ap.py``` file, and click the green arrow button (run current script). 106 | 107 | #### Option 2: Command Line 108 | If using the command line: 109 | 1) Execute ```mpremote``` to connect to your board 110 | 2) From the REPL, execute the following command to run the example: 111 | ```python 112 | >>> exec(open("tmp117_server_ap.py").read()) 113 | ``` 114 | 115 | ### Raspberry Pi 116 | Run the file with sudo privileges: 117 | ```bash 118 | sudo python3 tmp117_server_ap.py 119 | ``` 120 | 121 | ## Using the Webpage 122 | ### Connecting 123 | When you start the application, it should print the IP address and port that you should use to connect. For example: 124 | 125 | ```Navigate to http://192.168.4.1:5000/ to view the TMP117 temperature readings``` 126 | 127 | The application will broadcast a wireless network called ```iot_redboard_tmp117``` with the password ```thermo_wave2``` (or whatever you have set as ```kApSsid``` or ```kApPass``` in the constants at the top of the tmp117_server_ap.py file). Connect to this wireless network with the WiFi manager on your client device. 128 | 129 | Next, copy and paste (or ctrl+click) the connection link from above into your web browser (or enter it manually in a mobile device). 130 | 131 | ### Thermometer 132 | The Web Page should pop up and display a thermometer, some input boxes, and some indicator LEDs. The thermometer will show the temperature on a scale of 0 to 100 degrees F. Try breathing on your TMP117 to watch the temperature increase. 133 | 134 | ### Limit LEDs and Boxes 135 | To change one of the limit values, enter a number in the corresponding "Set Limit" box and press enter. 136 | After changing one of the values, give several seconds for the server to receive your request, 137 | set the limit on the device, and respond with the value read back from the device to update the "Read Limit" box. 138 | 139 | If the temperature drops below the low limit or above the high limit, triggering an alert on the device, 140 | the corresponding alert LED will turn red. 141 | > [!NOTE] 142 | > This simple version of the webserver is only designed to handle a single connection at a time and should be restarted after a client device disconnects. 143 | 144 | ## Code Explanation 145 | 146 | ### Setting Up WLAN as an Access Point (MicroPython) 147 | ```python 148 | def config_wlan_as_ap(ssid = kDefaultSsid, password = kDefaultPassword): 149 | ap = network.WLAN(network.AP_IF) 150 | ap.active(True) 151 | ap.config(essid=ssid, password=password) 152 | 153 | while ap.active() == False: 154 | pass 155 | 156 | config = ap.ifconfig() 157 | 158 | return str(config[0]) 159 | ``` 160 | 161 | ```tmp117_server_ap.py``` uses the config_wlan_as_ap() function to configure the WLAN as an access point. Notice how simple it is to create the object using the ```network``` module. By passing ```AP_IF``` we choose to set up the WLAN interface 162 | 163 | 164 | See the ```wlan_ap/config_ap_linux.py``` file for the analog for Raspberry Pi. It makes use of [nmcli](https://networkmanager.dev/docs/api/latest/nmcli.html) to configure the Raspberry Pi WLAN0 as an access point. We pass our ssid and password to set up our network credentials. We return our IP so we know where to navigate to view our webpage. 165 | 166 | ### Setting Up the TMP117 167 | First we ensure the TMP117 is properly connected by calling ```tmp117Device.is_connected()```. Then we perform initialization by calling ```tmp117Device.begin()```. Finally, we set up alerts using ```tmp117Device.set_high_limit()```, ```tmp117Device.set_low_limit()```, and ```set_alert_function_mode()```. 168 | 169 | ### The MicroDot Web Server Framework 170 | [Microdot](https://github.com/miguelgrinberg/microdot) is a minimal Python and MicroPython web framework that allows us to quickly make web apps that can run on platforms with limited resources. It is based around the idea of "routes", such that we can call different asynchronous Python functions when we receive client http requests to different paths. See the [Microdot README](https://github.com/miguelgrinberg/microdot/blob/main/README.md) for more information. 171 | 172 | ### Serving Static Web Elements 173 | When a client first connects, it will be to the root path "/" of our web app. We create a route to service this path: 174 | ```python 175 | @app.route('/') 176 | async def index(request): 177 | return send_file('static/index.html') 178 | ``` 179 | 180 | Using the Microdot ```send_file()``` function, we choose display our hompage in ```index.html``` when the user first connects. 181 | 182 | We also want to be able to serve an arbitrary number of static web elements for example, the sparkfun logo as well as some styling using css. For this, we create a route to service the "static" path: 183 | ```python 184 | @app.route('/static/') 185 | async def static(request, path): 186 | if '..' in path: 187 | # directory traversal is not allowed 188 | return 'Not found', 404 189 | return send_file('static/' + path) 190 | ``` 191 | This creates a mapping from client requests to all ```static/``` paths and the files on our server in the "static" folder. 192 | 193 | ### Client-Server Communication with WebSocket 194 | Microdot also allows for easy interfacing with WebSockets. In our ```index.html``` client code, we create a WebSocket at the ```'/temperature``` path. 195 | In our server code, we create a route for this path and specify that it will be a WebSocket using the ```@with_websocket``` decorator. 196 | ```python 197 | @app.route('/temperature') 198 | @with_websocket 199 | async def handle_limits(request, ws): 200 | ``` 201 | 202 | Now we will have access to the `ws` WebSocket access and can send and receive messages between the server and client using it's `send()` and `receive()` methods. 203 | 204 | ### Reading and Publishing Temperature 205 | We can read from the TMP117 using the functions defined in the qwiic_tmp117_py library. Often when conveying data over a WebSocket, the JSON format is used because it keeps our messages organized, and there is library suppport for JSON in most programming langauges. So we store our data in a dictionary and use the ```json.dumps()``` and ```tempSocket.send()``` functions to write it over the WebSocket as a JSON string where it can be caught by the client. 206 | 207 | ```python 208 | async def send_temperature(tempSocket): 209 | while True: 210 | if myTMP117.data_ready(): 211 | 212 | data = {"tempF": 0, "tempC": 0, "limitH": 75, "limitL": 65, "alertH": False, "alertL": False} 213 | data['tempC'] = myTMP117.read_temp_c() 214 | data['tempF'] = myTMP117.read_temp_f() 215 | 216 | if kDoAlerts: 217 | await asyncio.sleep(1.5) 218 | alertFlags = myTMP117.get_high_low_alert() 219 | data['alertL'] = bool(alertFlags[myTMP117.kLowAlertIdx]) 220 | data['alertH'] = bool(alertFlags[myTMP117.kHighAlertIdx]) 221 | data['limitL'] = c_to_f(myTMP117.get_low_limit()) 222 | data['limitH'] = c_to_f(myTMP117.get_high_limit()) 223 | 224 | data = json.dumps(data) 225 | await tempSocket.send(data) 226 | await asyncio.sleep(0.5) 227 | ``` 228 | 229 | This asynchronous task is created by the handle_limits function when a client first connects and creates the websocket. 230 | 231 | ```python 232 | asyncio.create_task(send_temperature(ws)) 233 | ``` 234 | 235 | 236 | 237 | ## References and More 238 | Special thanks to the creators of the MIT-licensed elements below: 239 | * [Miguel Grinberg and Microdot](https://github.com/miguelgrinberg/microdot) for the Microdot Web Framework. 240 | * [Arkellys](https://codepen.io/Arkellys/details/rgpNBK) for the Thermometer HTML/CSS element. 241 | * [Johnny Berkmans](https://codepen.io/berkmansjohnny/details/LzXbPV) for the Indicator LED HTML/CSS elements. 242 | 243 | Find a bug or want a feature? [Let us know here](https://github.com/sparkfun/sparkfun-python/issues). Have fun and happy hacking! 244 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/microdot/__init__.py: -------------------------------------------------------------------------------- 1 | from microdot.microdot import Microdot, Request, Response, abort, redirect, \ 2 | send_file # noqa: F401 3 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/microdot/helpers.py: -------------------------------------------------------------------------------- 1 | try: 2 | from functools import wraps 3 | except ImportError: # pragma: no cover 4 | # MicroPython does not currently implement functools.wraps 5 | def wraps(wrapped): 6 | def _(wrapper): 7 | return wrapper 8 | return _ 9 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/microdot/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. 7 | """ 8 | import asyncio 9 | import io 10 | import json 11 | import time 12 | 13 | try: 14 | from inspect import iscoroutinefunction, iscoroutine 15 | from functools import partial 16 | 17 | async def invoke_handler(handler, *args, **kwargs): 18 | """Invoke a handler and return the result. 19 | 20 | This method runs sync handlers in a thread pool executor. 21 | """ 22 | if iscoroutinefunction(handler): 23 | ret = await handler(*args, **kwargs) 24 | else: 25 | ret = await asyncio.get_running_loop().run_in_executor( 26 | None, partial(handler, *args, **kwargs)) 27 | return ret 28 | except ImportError: # pragma: no cover 29 | def iscoroutine(coro): 30 | return hasattr(coro, 'send') and hasattr(coro, 'throw') 31 | 32 | async def invoke_handler(handler, *args, **kwargs): 33 | """Invoke a handler and return the result. 34 | 35 | This method runs sync handlers in the asyncio thread, which can 36 | potentially cause blocking and performance issues. 37 | """ 38 | ret = handler(*args, **kwargs) 39 | if iscoroutine(ret): 40 | ret = await ret 41 | return ret 42 | 43 | try: 44 | from sys import print_exception 45 | except ImportError: # pragma: no cover 46 | import traceback 47 | 48 | def print_exception(exc): 49 | traceback.print_exc() 50 | 51 | MUTED_SOCKET_ERRORS = [ 52 | 32, # Broken pipe 53 | 54, # Connection reset by peer 54 | 104, # Connection reset by peer 55 | 128, # Operation on closed socket 56 | ] 57 | 58 | 59 | def urldecode_str(s): 60 | s = s.replace('+', ' ') 61 | parts = s.split('%') 62 | if len(parts) == 1: 63 | return s 64 | result = [parts[0]] 65 | for item in parts[1:]: 66 | if item == '': 67 | result.append('%') 68 | else: 69 | code = item[:2] 70 | result.append(chr(int(code, 16))) 71 | result.append(item[2:]) 72 | return ''.join(result) 73 | 74 | 75 | def urldecode_bytes(s): 76 | s = s.replace(b'+', b' ') 77 | parts = s.split(b'%') 78 | if len(parts) == 1: 79 | return s.decode() 80 | result = [parts[0]] 81 | for item in parts[1:]: 82 | if item == b'': 83 | result.append(b'%') 84 | else: 85 | code = item[:2] 86 | result.append(bytes([int(code, 16)])) 87 | result.append(item[2:]) 88 | return b''.join(result).decode() 89 | 90 | 91 | def urlencode(s): 92 | return s.replace('+', '%2B').replace(' ', '+').replace( 93 | '%', '%25').replace('?', '%3F').replace('#', '%23').replace( 94 | '&', '%26').replace('=', '%3D') 95 | 96 | 97 | class NoCaseDict(dict): 98 | """A subclass of dictionary that holds case-insensitive keys. 99 | 100 | :param initial_dict: an initial dictionary of key/value pairs to 101 | initialize this object with. 102 | 103 | Example:: 104 | 105 | >>> d = NoCaseDict() 106 | >>> d['Content-Type'] = 'text/html' 107 | >>> print(d['Content-Type']) 108 | text/html 109 | >>> print(d['content-type']) 110 | text/html 111 | >>> print(d['CONTENT-TYPE']) 112 | text/html 113 | >>> del d['cOnTeNt-TyPe'] 114 | >>> print(d) 115 | {} 116 | """ 117 | def __init__(self, initial_dict=None): 118 | super().__init__(initial_dict or {}) 119 | self.keymap = {k.lower(): k for k in self.keys() if k.lower() != k} 120 | 121 | def __setitem__(self, key, value): 122 | kl = key.lower() 123 | key = self.keymap.get(kl, key) 124 | if kl != key: 125 | self.keymap[kl] = key 126 | super().__setitem__(key, value) 127 | 128 | def __getitem__(self, key): 129 | kl = key.lower() 130 | return super().__getitem__(self.keymap.get(kl, kl)) 131 | 132 | def __delitem__(self, key): 133 | kl = key.lower() 134 | super().__delitem__(self.keymap.get(kl, kl)) 135 | 136 | def __contains__(self, key): 137 | kl = key.lower() 138 | return self.keymap.get(kl, kl) in self.keys() 139 | 140 | def get(self, key, default=None): 141 | kl = key.lower() 142 | return super().get(self.keymap.get(kl, kl), default) 143 | 144 | def update(self, other_dict): 145 | for key, value in other_dict.items(): 146 | self[key] = value 147 | 148 | 149 | def mro(cls): # pragma: no cover 150 | """Return the method resolution order of a class. 151 | 152 | This is a helper function that returns the method resolution order of a 153 | class. It is used by Microdot to find the best error handler to invoke for 154 | the raised exception. 155 | 156 | In CPython, this function returns the ``__mro__`` attribute of the class. 157 | In MicroPython, this function implements a recursive depth-first scanning 158 | of the class hierarchy. 159 | """ 160 | if hasattr(cls, 'mro'): 161 | return cls.__mro__ 162 | 163 | def _mro(cls): 164 | m = [cls] 165 | for base in cls.__bases__: 166 | m += _mro(base) 167 | return m 168 | 169 | mro_list = _mro(cls) 170 | 171 | # If a class appears multiple times (due to multiple inheritance) remove 172 | # all but the last occurence. This matches the method resolution order 173 | # of MicroPython, but not CPython. 174 | mro_pruned = [] 175 | for i in range(len(mro_list)): 176 | base = mro_list.pop(0) 177 | if base not in mro_list: 178 | mro_pruned.append(base) 179 | return mro_pruned 180 | 181 | 182 | class MultiDict(dict): 183 | """A subclass of dictionary that can hold multiple values for the same 184 | key. It is used to hold key/value pairs decoded from query strings and 185 | form submissions. 186 | 187 | :param initial_dict: an initial dictionary of key/value pairs to 188 | initialize this object with. 189 | 190 | Example:: 191 | 192 | >>> d = MultiDict() 193 | >>> d['sort'] = 'name' 194 | >>> d['sort'] = 'email' 195 | >>> print(d['sort']) 196 | 'name' 197 | >>> print(d.getlist('sort')) 198 | ['name', 'email'] 199 | """ 200 | def __init__(self, initial_dict=None): 201 | super().__init__() 202 | if initial_dict: 203 | for key, value in initial_dict.items(): 204 | self[key] = value 205 | 206 | def __setitem__(self, key, value): 207 | if key not in self: 208 | super().__setitem__(key, []) 209 | super().__getitem__(key).append(value) 210 | 211 | def __getitem__(self, key): 212 | return super().__getitem__(key)[0] 213 | 214 | def get(self, key, default=None, type=None): 215 | """Return the value for a given key. 216 | 217 | :param key: The key to retrieve. 218 | :param default: A default value to use if the key does not exist. 219 | :param type: A type conversion callable to apply to the value. 220 | 221 | If the multidict contains more than one value for the requested key, 222 | this method returns the first value only. 223 | 224 | Example:: 225 | 226 | >>> d = MultiDict() 227 | >>> d['age'] = '42' 228 | >>> d.get('age') 229 | '42' 230 | >>> d.get('age', type=int) 231 | 42 232 | >>> d.get('name', default='noname') 233 | 'noname' 234 | """ 235 | if key not in self: 236 | return default 237 | value = self[key] 238 | if type is not None: 239 | value = type(value) 240 | return value 241 | 242 | def getlist(self, key, type=None): 243 | """Return all the values for a given key. 244 | 245 | :param key: The key to retrieve. 246 | :param type: A type conversion callable to apply to the values. 247 | 248 | If the requested key does not exist in the dictionary, this method 249 | returns an empty list. 250 | 251 | Example:: 252 | 253 | >>> d = MultiDict() 254 | >>> d.getlist('items') 255 | [] 256 | >>> d['items'] = '3' 257 | >>> d.getlist('items') 258 | ['3'] 259 | >>> d['items'] = '56' 260 | >>> d.getlist('items') 261 | ['3', '56'] 262 | >>> d.getlist('items', type=int) 263 | [3, 56] 264 | """ 265 | if key not in self: 266 | return [] 267 | values = super().__getitem__(key) 268 | if type is not None: 269 | values = [type(value) for value in values] 270 | return values 271 | 272 | 273 | class AsyncBytesIO: 274 | """An async wrapper for BytesIO.""" 275 | def __init__(self, data): 276 | self.stream = io.BytesIO(data) 277 | 278 | async def read(self, n=-1): 279 | return self.stream.read(n) 280 | 281 | async def readline(self): # pragma: no cover 282 | return self.stream.readline() 283 | 284 | async def readexactly(self, n): # pragma: no cover 285 | return self.stream.read(n) 286 | 287 | async def readuntil(self, separator=b'\n'): # pragma: no cover 288 | return self.stream.readuntil(separator=separator) 289 | 290 | async def awrite(self, data): # pragma: no cover 291 | return self.stream.write(data) 292 | 293 | async def aclose(self): # pragma: no cover 294 | pass 295 | 296 | 297 | class Request: 298 | """An HTTP request.""" 299 | #: Specify the maximum payload size that is accepted. Requests with larger 300 | #: payloads will be rejected with a 413 status code. Applications can 301 | #: change this maximum as necessary. 302 | #: 303 | #: Example:: 304 | #: 305 | #: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed 306 | max_content_length = 16 * 1024 307 | 308 | #: Specify the maximum payload size that can be stored in ``body``. 309 | #: Requests with payloads that are larger than this size and up to 310 | #: ``max_content_length`` bytes will be accepted, but the application will 311 | #: only be able to access the body of the request by reading from 312 | #: ``stream``. Set to 0 if you always access the body as a stream. 313 | #: 314 | #: Example:: 315 | #: 316 | #: Request.max_body_length = 4 * 1024 # up to 4KB bodies read 317 | max_body_length = 16 * 1024 318 | 319 | #: Specify the maximum length allowed for a line in the request. Requests 320 | #: with longer lines will not be correctly interpreted. Applications can 321 | #: change this maximum as necessary. 322 | #: 323 | #: Example:: 324 | #: 325 | #: Request.max_readline = 16 * 1024 # 16KB lines allowed 326 | max_readline = 2 * 1024 327 | 328 | class G: 329 | pass 330 | 331 | def __init__(self, app, client_addr, method, url, http_version, headers, 332 | body=None, stream=None, sock=None, url_prefix='', 333 | subapp=None): 334 | #: The application instance to which this request belongs. 335 | self.app = app 336 | #: The address of the client, as a tuple (host, port). 337 | self.client_addr = client_addr 338 | #: The HTTP method of the request. 339 | self.method = method 340 | #: The request URL, including the path and query string. 341 | self.url = url 342 | #: The URL prefix, if the endpoint comes from a mounted 343 | #: sub-application, or else ''. 344 | self.url_prefix = url_prefix 345 | #: The sub-application instance, or `None` if this isn't a mounted 346 | #: endpoint. 347 | self.subapp = subapp 348 | #: The path portion of the URL. 349 | self.path = url 350 | #: The query string portion of the URL. 351 | self.query_string = None 352 | #: The parsed query string, as a 353 | #: :class:`MultiDict ` object. 354 | self.args = {} 355 | #: A dictionary with the headers included in the request. 356 | self.headers = headers 357 | #: A dictionary with the cookies included in the request. 358 | self.cookies = {} 359 | #: The parsed ``Content-Length`` header. 360 | self.content_length = 0 361 | #: The parsed ``Content-Type`` header. 362 | self.content_type = None 363 | #: A general purpose container for applications to store data during 364 | #: the life of the request. 365 | self.g = Request.G() 366 | 367 | self.http_version = http_version 368 | if '?' in self.path: 369 | self.path, self.query_string = self.path.split('?', 1) 370 | self.args = self._parse_urlencoded(self.query_string) 371 | 372 | if 'Content-Length' in self.headers: 373 | self.content_length = int(self.headers['Content-Length']) 374 | if 'Content-Type' in self.headers: 375 | self.content_type = self.headers['Content-Type'] 376 | if 'Cookie' in self.headers: 377 | for cookie in self.headers['Cookie'].split(';'): 378 | name, value = cookie.strip().split('=', 1) 379 | self.cookies[name] = value 380 | 381 | self._body = body 382 | self.body_used = False 383 | self._stream = stream 384 | self.sock = sock 385 | self._json = None 386 | self._form = None 387 | self.after_request_handlers = [] 388 | 389 | @staticmethod 390 | async def create(app, client_reader, client_writer, client_addr): 391 | """Create a request object. 392 | 393 | :param app: The Microdot application instance. 394 | :param client_reader: An input stream from where the request data can 395 | be read. 396 | :param client_writer: An output stream where the response data can be 397 | written. 398 | :param client_addr: The address of the client, as a tuple. 399 | 400 | This method is a coroutine. It returns a newly created ``Request`` 401 | object. 402 | """ 403 | # request line 404 | line = (await Request._safe_readline(client_reader)).strip().decode() 405 | if not line: # pragma: no cover 406 | return None 407 | method, url, http_version = line.split() 408 | http_version = http_version.split('/', 1)[1] 409 | 410 | # headers 411 | headers = NoCaseDict() 412 | content_length = 0 413 | while True: 414 | line = (await Request._safe_readline( 415 | client_reader)).strip().decode() 416 | if line == '': 417 | break 418 | header, value = line.split(':', 1) 419 | value = value.strip() 420 | headers[header] = value 421 | if header.lower() == 'content-length': 422 | content_length = int(value) 423 | 424 | # body 425 | body = b'' 426 | if content_length and content_length <= Request.max_body_length: 427 | body = await client_reader.readexactly(content_length) 428 | stream = None 429 | else: 430 | body = b'' 431 | stream = client_reader 432 | 433 | return Request(app, client_addr, method, url, http_version, headers, 434 | body=body, stream=stream, 435 | sock=(client_reader, client_writer)) 436 | 437 | def _parse_urlencoded(self, urlencoded): 438 | data = MultiDict() 439 | if len(urlencoded) > 0: # pragma: no branch 440 | if isinstance(urlencoded, str): 441 | for kv in [pair.split('=', 1) 442 | for pair in urlencoded.split('&') if pair]: 443 | data[urldecode_str(kv[0])] = urldecode_str(kv[1]) \ 444 | if len(kv) > 1 else '' 445 | elif isinstance(urlencoded, bytes): # pragma: no branch 446 | for kv in [pair.split(b'=', 1) 447 | for pair in urlencoded.split(b'&') if pair]: 448 | data[urldecode_bytes(kv[0])] = urldecode_bytes(kv[1]) \ 449 | if len(kv) > 1 else b'' 450 | return data 451 | 452 | @property 453 | def body(self): 454 | """The body of the request, as bytes.""" 455 | return self._body 456 | 457 | @property 458 | def stream(self): 459 | """The body of the request, as a bytes stream.""" 460 | if self._stream is None: 461 | self._stream = AsyncBytesIO(self._body) 462 | return self._stream 463 | 464 | @property 465 | def json(self): 466 | """The parsed JSON body, or ``None`` if the request does not have a 467 | JSON body.""" 468 | if self._json is None: 469 | if self.content_type is None: 470 | return None 471 | mime_type = self.content_type.split(';')[0] 472 | if mime_type != 'application/json': 473 | return None 474 | self._json = json.loads(self.body.decode()) 475 | return self._json 476 | 477 | @property 478 | def form(self): 479 | """The parsed form submission body, as a 480 | :class:`MultiDict ` object, or ``None`` if the 481 | request does not have a form submission.""" 482 | if self._form is None: 483 | if self.content_type is None: 484 | return None 485 | mime_type = self.content_type.split(';')[0] 486 | if mime_type != 'application/x-www-form-urlencoded': 487 | return None 488 | self._form = self._parse_urlencoded(self.body) 489 | return self._form 490 | 491 | def after_request(self, f): 492 | """Register a request-specific function to run after the request is 493 | handled. Request-specific after request handlers run at the very end, 494 | after the application's own after request handlers. The function must 495 | take two arguments, the request and response objects. The return value 496 | of the function must be the updated response object. 497 | 498 | Example:: 499 | 500 | @app.route('/') 501 | def index(request): 502 | # register a request-specific after request handler 503 | @req.after_request 504 | def func(request, response): 505 | # ... 506 | return response 507 | 508 | return 'Hello, World!' 509 | 510 | Note that the function is not called if the request handler raises an 511 | exception and an error response is returned instead. 512 | """ 513 | self.after_request_handlers.append(f) 514 | return f 515 | 516 | @staticmethod 517 | async def _safe_readline(stream): 518 | line = (await stream.readline()) 519 | if len(line) > Request.max_readline: 520 | raise ValueError('line too long') 521 | return line 522 | 523 | 524 | class Response: 525 | """An HTTP response class. 526 | 527 | :param body: The body of the response. If a dictionary or list is given, 528 | a JSON formatter is used to generate the body. If a file-like 529 | object or an async generator is given, a streaming response is 530 | used. If a string is given, it is encoded from UTF-8. Else, 531 | the body should be a byte sequence. 532 | :param status_code: The numeric HTTP status code of the response. The 533 | default is 200. 534 | :param headers: A dictionary of headers to include in the response. 535 | :param reason: A custom reason phrase to add after the status code. The 536 | default is "OK" for responses with a 200 status code and 537 | "N/A" for any other status codes. 538 | """ 539 | types_map = { 540 | 'css': 'text/css', 541 | 'gif': 'image/gif', 542 | 'html': 'text/html', 543 | 'jpg': 'image/jpeg', 544 | 'js': 'application/javascript', 545 | 'json': 'application/json', 546 | 'png': 'image/png', 547 | 'txt': 'text/plain', 548 | } 549 | 550 | send_file_buffer_size = 1024 551 | 552 | #: The content type to use for responses that do not explicitly define a 553 | #: ``Content-Type`` header. 554 | default_content_type = 'text/plain' 555 | 556 | #: The default cache control max age used by :meth:`send_file`. A value 557 | #: of ``None`` means that no ``Cache-Control`` header is added. 558 | default_send_file_max_age = None 559 | 560 | #: Special response used to signal that a response does not need to be 561 | #: written to the client. Used to exit WebSocket connections cleanly. 562 | already_handled = None 563 | 564 | def __init__(self, body='', status_code=200, headers=None, reason=None): 565 | if body is None and status_code == 200: 566 | body = '' 567 | status_code = 204 568 | self.status_code = status_code 569 | self.headers = NoCaseDict(headers or {}) 570 | self.reason = reason 571 | if isinstance(body, (dict, list)): 572 | self.body = json.dumps(body).encode() 573 | self.headers['Content-Type'] = 'application/json; charset=UTF-8' 574 | elif isinstance(body, str): 575 | self.body = body.encode() 576 | else: 577 | # this applies to bytes, file-like objects or generators 578 | self.body = body 579 | self.is_head = False 580 | 581 | def set_cookie(self, cookie, value, path=None, domain=None, expires=None, 582 | max_age=None, secure=False, http_only=False, 583 | partitioned=False): 584 | """Add a cookie to the response. 585 | 586 | :param cookie: The cookie's name. 587 | :param value: The cookie's value. 588 | :param path: The cookie's path. 589 | :param domain: The cookie's domain. 590 | :param expires: The cookie expiration time, as a ``datetime`` object 591 | or a correctly formatted string. 592 | :param max_age: The cookie's ``Max-Age`` value. 593 | :param secure: The cookie's ``secure`` flag. 594 | :param http_only: The cookie's ``HttpOnly`` flag. 595 | :param partitioned: Whether the cookie is partitioned. 596 | """ 597 | http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value) 598 | if path: 599 | http_cookie += '; Path=' + path 600 | if domain: 601 | http_cookie += '; Domain=' + domain 602 | if expires: 603 | if isinstance(expires, str): 604 | http_cookie += '; Expires=' + expires 605 | else: # pragma: no cover 606 | http_cookie += '; Expires=' + time.strftime( 607 | '%a, %d %b %Y %H:%M:%S GMT', expires.timetuple()) 608 | if max_age is not None: 609 | http_cookie += '; Max-Age=' + str(max_age) 610 | if secure: 611 | http_cookie += '; Secure' 612 | if http_only: 613 | http_cookie += '; HttpOnly' 614 | if partitioned: 615 | http_cookie += '; Partitioned' 616 | if 'Set-Cookie' in self.headers: 617 | self.headers['Set-Cookie'].append(http_cookie) 618 | else: 619 | self.headers['Set-Cookie'] = [http_cookie] 620 | 621 | def delete_cookie(self, cookie, **kwargs): 622 | """Delete a cookie. 623 | 624 | :param cookie: The cookie's name. 625 | :param kwargs: Any cookie opens and flags supported by 626 | ``set_cookie()`` except ``expires`` and ``max_age``. 627 | """ 628 | self.set_cookie(cookie, '', expires='Thu, 01 Jan 1970 00:00:01 GMT', 629 | max_age=0, **kwargs) 630 | 631 | def complete(self): 632 | if isinstance(self.body, bytes) and \ 633 | 'Content-Length' not in self.headers: 634 | self.headers['Content-Length'] = str(len(self.body)) 635 | if 'Content-Type' not in self.headers: 636 | self.headers['Content-Type'] = self.default_content_type 637 | if 'charset=' not in self.headers['Content-Type']: 638 | self.headers['Content-Type'] += '; charset=UTF-8' 639 | 640 | async def write(self, stream): 641 | self.complete() 642 | 643 | try: 644 | # status code 645 | reason = self.reason if self.reason is not None else \ 646 | ('OK' if self.status_code == 200 else 'N/A') 647 | await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format( 648 | status_code=self.status_code, reason=reason).encode()) 649 | 650 | # headers 651 | for header, value in self.headers.items(): 652 | values = value if isinstance(value, list) else [value] 653 | for value in values: 654 | await stream.awrite('{header}: {value}\r\n'.format( 655 | header=header, value=value).encode()) 656 | await stream.awrite(b'\r\n') 657 | 658 | # body 659 | if not self.is_head: 660 | iter = self.body_iter() 661 | async for body in iter: 662 | if isinstance(body, str): # pragma: no cover 663 | body = body.encode() 664 | try: 665 | await stream.awrite(body) 666 | except OSError as exc: # pragma: no cover 667 | if exc.errno in MUTED_SOCKET_ERRORS or \ 668 | exc.args[0] == 'Connection lost': 669 | if hasattr(iter, 'aclose'): 670 | await iter.aclose() 671 | raise 672 | if hasattr(iter, 'aclose'): # pragma: no branch 673 | await iter.aclose() 674 | 675 | except OSError as exc: # pragma: no cover 676 | if exc.errno in MUTED_SOCKET_ERRORS or \ 677 | exc.args[0] == 'Connection lost': 678 | pass 679 | else: 680 | raise 681 | 682 | def body_iter(self): 683 | if hasattr(self.body, '__anext__'): 684 | # response body is an async generator 685 | return self.body 686 | 687 | response = self 688 | 689 | class iter: 690 | ITER_UNKNOWN = 0 691 | ITER_SYNC_GEN = 1 692 | ITER_FILE_OBJ = 2 693 | ITER_NO_BODY = -1 694 | 695 | def __aiter__(self): 696 | if response.body: 697 | self.i = self.ITER_UNKNOWN # need to determine type 698 | else: 699 | self.i = self.ITER_NO_BODY 700 | return self 701 | 702 | async def __anext__(self): 703 | if self.i == self.ITER_NO_BODY: 704 | await self.aclose() 705 | raise StopAsyncIteration 706 | if self.i == self.ITER_UNKNOWN: 707 | if hasattr(response.body, 'read'): 708 | self.i = self.ITER_FILE_OBJ 709 | elif hasattr(response.body, '__next__'): 710 | self.i = self.ITER_SYNC_GEN 711 | return next(response.body) 712 | else: 713 | self.i = self.ITER_NO_BODY 714 | return response.body 715 | elif self.i == self.ITER_SYNC_GEN: 716 | try: 717 | return next(response.body) 718 | except StopIteration: 719 | await self.aclose() 720 | raise StopAsyncIteration 721 | buf = response.body.read(response.send_file_buffer_size) 722 | if iscoroutine(buf): # pragma: no cover 723 | buf = await buf 724 | if len(buf) < response.send_file_buffer_size: 725 | self.i = self.ITER_NO_BODY 726 | return buf 727 | 728 | async def aclose(self): 729 | if hasattr(response.body, 'close'): 730 | result = response.body.close() 731 | if iscoroutine(result): # pragma: no cover 732 | await result 733 | 734 | return iter() 735 | 736 | @classmethod 737 | def redirect(cls, location, status_code=302): 738 | """Return a redirect response. 739 | 740 | :param location: The URL to redirect to. 741 | :param status_code: The 3xx status code to use for the redirect. The 742 | default is 302. 743 | """ 744 | if '\x0d' in location or '\x0a' in location: 745 | raise ValueError('invalid redirect URL') 746 | return cls(status_code=status_code, headers={'Location': location}) 747 | 748 | @classmethod 749 | def send_file(cls, filename, status_code=200, content_type=None, 750 | stream=None, max_age=None, compressed=False, 751 | file_extension=''): 752 | """Send file contents in a response. 753 | 754 | :param filename: The filename of the file. 755 | :param status_code: The 3xx status code to use for the redirect. The 756 | default is 302. 757 | :param content_type: The ``Content-Type`` header to use in the 758 | response. If omitted, it is generated 759 | automatically from the file extension of the 760 | ``filename`` parameter. 761 | :param stream: A file-like object to read the file contents from. If 762 | a stream is given, the ``filename`` parameter is only 763 | used when generating the ``Content-Type`` header. 764 | :param max_age: The ``Cache-Control`` header's ``max-age`` value in 765 | seconds. If omitted, the value of the 766 | :attr:`Response.default_send_file_max_age` attribute is 767 | used. 768 | :param compressed: Whether the file is compressed. If ``True``, the 769 | ``Content-Encoding`` header is set to ``gzip``. A 770 | string with the header value can also be passed. 771 | Note that when using this option the file must have 772 | been compressed beforehand. This option only sets 773 | the header. 774 | :param file_extension: A file extension to append to the ``filename`` 775 | parameter when opening the file, including the 776 | dot. The extension given here is not considered 777 | when generating the ``Content-Type`` header. 778 | 779 | Security note: The filename is assumed to be trusted. Never pass 780 | filenames provided by the user without validating and sanitizing them 781 | first. 782 | """ 783 | if content_type is None: 784 | if compressed and filename.endswith('.gz'): 785 | ext = filename[:-3].split('.')[-1] 786 | else: 787 | ext = filename.split('.')[-1] 788 | if ext in Response.types_map: 789 | content_type = Response.types_map[ext] 790 | else: 791 | content_type = 'application/octet-stream' 792 | headers = {'Content-Type': content_type} 793 | 794 | if max_age is None: 795 | max_age = cls.default_send_file_max_age 796 | if max_age is not None: 797 | headers['Cache-Control'] = 'max-age={}'.format(max_age) 798 | 799 | if compressed: 800 | headers['Content-Encoding'] = compressed \ 801 | if isinstance(compressed, str) else 'gzip' 802 | 803 | f = stream or open(filename + file_extension, 'rb') 804 | return cls(body=f, status_code=status_code, headers=headers) 805 | 806 | 807 | class URLPattern(): 808 | def __init__(self, url_pattern): 809 | self.url_pattern = url_pattern 810 | self.segments = [] 811 | self.regex = None 812 | pattern = '' 813 | use_regex = False 814 | for segment in url_pattern.lstrip('/').split('/'): 815 | if segment and segment[0] == '<': 816 | if segment[-1] != '>': 817 | raise ValueError('invalid URL pattern') 818 | segment = segment[1:-1] 819 | if ':' in segment: 820 | type_, name = segment.rsplit(':', 1) 821 | else: 822 | type_ = 'string' 823 | name = segment 824 | parser = None 825 | if type_ == 'string': 826 | parser = self._string_segment 827 | pattern += '/([^/]+)' 828 | elif type_ == 'int': 829 | parser = self._int_segment 830 | pattern += '/(-?\\d+)' 831 | elif type_ == 'path': 832 | use_regex = True 833 | pattern += '/(.+)' 834 | elif type_.startswith('re:'): 835 | use_regex = True 836 | pattern += '/({pattern})'.format(pattern=type_[3:]) 837 | else: 838 | raise ValueError('invalid URL segment type') 839 | self.segments.append({'parser': parser, 'name': name, 840 | 'type': type_}) 841 | else: 842 | pattern += '/' + segment 843 | self.segments.append({'parser': self._static_segment(segment)}) 844 | if use_regex: 845 | import re 846 | self.regex = re.compile('^' + pattern + '$') 847 | 848 | def match(self, path): 849 | args = {} 850 | if self.regex: 851 | g = self.regex.match(path) 852 | if not g: 853 | return 854 | i = 1 855 | for segment in self.segments: 856 | if 'name' not in segment: 857 | continue 858 | value = g.group(i) 859 | if segment['type'] == 'int': 860 | value = int(value) 861 | args[segment['name']] = value 862 | i += 1 863 | else: 864 | if len(path) == 0 or path[0] != '/': 865 | return 866 | path = path[1:] 867 | args = {} 868 | for segment in self.segments: 869 | if path is None: 870 | return 871 | arg, path = segment['parser'](path) 872 | if arg is None: 873 | return 874 | if 'name' in segment: 875 | args[segment['name']] = arg 876 | if path is not None: 877 | return 878 | return args 879 | 880 | def _static_segment(self, segment): 881 | def _static(value): 882 | s = value.split('/', 1) 883 | if s[0] == segment: 884 | return '', s[1] if len(s) > 1 else None 885 | return None, None 886 | return _static 887 | 888 | def _string_segment(self, value): 889 | s = value.split('/', 1) 890 | if len(s[0]) == 0: 891 | return None, None 892 | return s[0], s[1] if len(s) > 1 else None 893 | 894 | def _int_segment(self, value): 895 | s = value.split('/', 1) 896 | try: 897 | return int(s[0]), s[1] if len(s) > 1 else None 898 | except ValueError: 899 | return None, None 900 | 901 | def __repr__(self): # pragma: no cover 902 | return 'URLPattern: {}'.format(self.url_pattern) 903 | 904 | 905 | class HTTPException(Exception): 906 | def __init__(self, status_code, reason=None): 907 | self.status_code = status_code 908 | self.reason = reason or str(status_code) + ' error' 909 | 910 | def __repr__(self): # pragma: no cover 911 | return 'HTTPException: {}'.format(self.status_code) 912 | 913 | 914 | class Microdot: 915 | """An HTTP application class. 916 | 917 | This class implements an HTTP application instance and is heavily 918 | influenced by the ``Flask`` class of the Flask framework. It is typically 919 | declared near the start of the main application script. 920 | 921 | Example:: 922 | 923 | from microdot import Microdot 924 | 925 | app = Microdot() 926 | """ 927 | 928 | def __init__(self): 929 | self.url_map = [] 930 | self.before_request_handlers = [] 931 | self.after_request_handlers = [] 932 | self.after_error_request_handlers = [] 933 | self.error_handlers = {} 934 | self.shutdown_requested = False 935 | self.options_handler = self.default_options_handler 936 | self.debug = False 937 | self.server = None 938 | 939 | def route(self, url_pattern, methods=None): 940 | """Decorator that is used to register a function as a request handler 941 | for a given URL. 942 | 943 | :param url_pattern: The URL pattern that will be compared against 944 | incoming requests. 945 | :param methods: The list of HTTP methods to be handled by the 946 | decorated function. If omitted, only ``GET`` requests 947 | are handled. 948 | 949 | The URL pattern can be a static path (for example, ``/users`` or 950 | ``/api/invoices/search``) or a path with dynamic components enclosed 951 | in ``<`` and ``>`` (for example, ``/users/`` or 952 | ``/invoices//products``). Dynamic path components can also 953 | include a type prefix, separated from the name with a colon (for 954 | example, ``/users/``). The type can be ``string`` (the 955 | default), ``int``, ``path`` or ``re:[regular-expression]``. 956 | 957 | The first argument of the decorated function must be 958 | the request object. Any path arguments that are specified in the URL 959 | pattern are passed as keyword arguments. The return value of the 960 | function must be a :class:`Response` instance, or the arguments to 961 | be passed to this class. 962 | 963 | Example:: 964 | 965 | @app.route('/') 966 | def index(request): 967 | return 'Hello, world!' 968 | """ 969 | def decorated(f): 970 | self.url_map.append( 971 | ([m.upper() for m in (methods or ['GET'])], 972 | URLPattern(url_pattern), f, '', None)) 973 | return f 974 | return decorated 975 | 976 | def get(self, url_pattern): 977 | """Decorator that is used to register a function as a ``GET`` request 978 | handler for a given URL. 979 | 980 | :param url_pattern: The URL pattern that will be compared against 981 | incoming requests. 982 | 983 | This decorator can be used as an alias to the ``route`` decorator with 984 | ``methods=['GET']``. 985 | 986 | Example:: 987 | 988 | @app.get('/users/') 989 | def get_user(request, id): 990 | # ... 991 | """ 992 | return self.route(url_pattern, methods=['GET']) 993 | 994 | def post(self, url_pattern): 995 | """Decorator that is used to register a function as a ``POST`` request 996 | handler for a given URL. 997 | 998 | :param url_pattern: The URL pattern that will be compared against 999 | incoming requests. 1000 | 1001 | This decorator can be used as an alias to the``route`` decorator with 1002 | ``methods=['POST']``. 1003 | 1004 | Example:: 1005 | 1006 | @app.post('/users') 1007 | def create_user(request): 1008 | # ... 1009 | """ 1010 | return self.route(url_pattern, methods=['POST']) 1011 | 1012 | def put(self, url_pattern): 1013 | """Decorator that is used to register a function as a ``PUT`` request 1014 | handler for a given URL. 1015 | 1016 | :param url_pattern: The URL pattern that will be compared against 1017 | incoming requests. 1018 | 1019 | This decorator can be used as an alias to the ``route`` decorator with 1020 | ``methods=['PUT']``. 1021 | 1022 | Example:: 1023 | 1024 | @app.put('/users/') 1025 | def edit_user(request, id): 1026 | # ... 1027 | """ 1028 | return self.route(url_pattern, methods=['PUT']) 1029 | 1030 | def patch(self, url_pattern): 1031 | """Decorator that is used to register a function as a ``PATCH`` request 1032 | handler for a given URL. 1033 | 1034 | :param url_pattern: The URL pattern that will be compared against 1035 | incoming requests. 1036 | 1037 | This decorator can be used as an alias to the ``route`` decorator with 1038 | ``methods=['PATCH']``. 1039 | 1040 | Example:: 1041 | 1042 | @app.patch('/users/') 1043 | def edit_user(request, id): 1044 | # ... 1045 | """ 1046 | return self.route(url_pattern, methods=['PATCH']) 1047 | 1048 | def delete(self, url_pattern): 1049 | """Decorator that is used to register a function as a ``DELETE`` 1050 | request handler for a given URL. 1051 | 1052 | :param url_pattern: The URL pattern that will be compared against 1053 | incoming requests. 1054 | 1055 | This decorator can be used as an alias to the ``route`` decorator with 1056 | ``methods=['DELETE']``. 1057 | 1058 | Example:: 1059 | 1060 | @app.delete('/users/') 1061 | def delete_user(request, id): 1062 | # ... 1063 | """ 1064 | return self.route(url_pattern, methods=['DELETE']) 1065 | 1066 | def before_request(self, f): 1067 | """Decorator to register a function to run before each request is 1068 | handled. The decorated function must take a single argument, the 1069 | request object. 1070 | 1071 | Example:: 1072 | 1073 | @app.before_request 1074 | def func(request): 1075 | # ... 1076 | """ 1077 | self.before_request_handlers.append(f) 1078 | return f 1079 | 1080 | def after_request(self, f): 1081 | """Decorator to register a function to run after each request is 1082 | handled. The decorated function must take two arguments, the request 1083 | and response objects. The return value of the function must be an 1084 | updated response object. 1085 | 1086 | Example:: 1087 | 1088 | @app.after_request 1089 | def func(request, response): 1090 | # ... 1091 | return response 1092 | """ 1093 | self.after_request_handlers.append(f) 1094 | return f 1095 | 1096 | def after_error_request(self, f): 1097 | """Decorator to register a function to run after an error response is 1098 | generated. The decorated function must take two arguments, the request 1099 | and response objects. The return value of the function must be an 1100 | updated response object. The handler is invoked for error responses 1101 | generated by Microdot, as well as those returned by application-defined 1102 | error handlers. 1103 | 1104 | Example:: 1105 | 1106 | @app.after_error_request 1107 | def func(request, response): 1108 | # ... 1109 | return response 1110 | """ 1111 | self.after_error_request_handlers.append(f) 1112 | return f 1113 | 1114 | def errorhandler(self, status_code_or_exception_class): 1115 | """Decorator to register a function as an error handler. Error handler 1116 | functions for numeric HTTP status codes must accept a single argument, 1117 | the request object. Error handler functions for Python exceptions 1118 | must accept two arguments, the request object and the exception 1119 | object. 1120 | 1121 | :param status_code_or_exception_class: The numeric HTTP status code or 1122 | Python exception class to 1123 | handle. 1124 | 1125 | Examples:: 1126 | 1127 | @app.errorhandler(404) 1128 | def not_found(request): 1129 | return 'Not found' 1130 | 1131 | @app.errorhandler(RuntimeError) 1132 | def runtime_error(request, exception): 1133 | return 'Runtime error' 1134 | """ 1135 | def decorated(f): 1136 | self.error_handlers[status_code_or_exception_class] = f 1137 | return f 1138 | return decorated 1139 | 1140 | def mount(self, subapp, url_prefix='', local=False): 1141 | """Mount a sub-application, optionally under the given URL prefix. 1142 | 1143 | :param subapp: The sub-application to mount. 1144 | :param url_prefix: The URL prefix to mount the application under. 1145 | :param local: When set to ``True``, the before, after and error request 1146 | handlers only apply to endpoints defined in the 1147 | sub-application. When ``False``, they apply to the entire 1148 | application. The default is ``False``. 1149 | """ 1150 | for methods, pattern, handler, _prefix, _subapp in subapp.url_map: 1151 | self.url_map.append( 1152 | (methods, URLPattern(url_prefix + pattern.url_pattern), 1153 | handler, url_prefix + _prefix, _subapp or subapp)) 1154 | if not local: 1155 | for handler in subapp.before_request_handlers: 1156 | self.before_request_handlers.append(handler) 1157 | subapp.before_request_handlers = [] 1158 | for handler in subapp.after_request_handlers: 1159 | self.after_request_handlers.append(handler) 1160 | subapp.after_request_handlers = [] 1161 | for handler in subapp.after_error_request_handlers: 1162 | self.after_error_request_handlers.append(handler) 1163 | subapp.after_error_request_handlers = [] 1164 | for status_code, handler in subapp.error_handlers.items(): 1165 | self.error_handlers[status_code] = handler 1166 | subapp.error_handlers = {} 1167 | 1168 | @staticmethod 1169 | def abort(status_code, reason=None): 1170 | """Abort the current request and return an error response with the 1171 | given status code. 1172 | 1173 | :param status_code: The numeric status code of the response. 1174 | :param reason: The reason for the response, which is included in the 1175 | response body. 1176 | 1177 | Example:: 1178 | 1179 | from microdot import abort 1180 | 1181 | @app.route('/users/') 1182 | def get_user(id): 1183 | user = get_user_by_id(id) 1184 | if user is None: 1185 | abort(404) 1186 | return user.to_dict() 1187 | """ 1188 | raise HTTPException(status_code, reason) 1189 | 1190 | async def start_server(self, host='0.0.0.0', port=5000, debug=False, 1191 | ssl=None): 1192 | """Start the Microdot web server as a coroutine. This coroutine does 1193 | not normally return, as the server enters an endless listening loop. 1194 | The :func:`shutdown` function provides a method for terminating the 1195 | server gracefully. 1196 | 1197 | :param host: The hostname or IP address of the network interface that 1198 | will be listening for requests. A value of ``'0.0.0.0'`` 1199 | (the default) indicates that the server should listen for 1200 | requests on all the available interfaces, and a value of 1201 | ``127.0.0.1`` indicates that the server should listen 1202 | for requests only on the internal networking interface of 1203 | the host. 1204 | :param port: The port number to listen for requests. The default is 1205 | port 5000. 1206 | :param debug: If ``True``, the server logs debugging information. The 1207 | default is ``False``. 1208 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should 1209 | not use TLS. The default is ``None``. 1210 | 1211 | This method is a coroutine. 1212 | 1213 | Example:: 1214 | 1215 | import asyncio 1216 | from microdot import Microdot 1217 | 1218 | app = Microdot() 1219 | 1220 | @app.route('/') 1221 | async def index(request): 1222 | return 'Hello, world!' 1223 | 1224 | async def main(): 1225 | await app.start_server(debug=True) 1226 | 1227 | asyncio.run(main()) 1228 | """ 1229 | self.debug = debug 1230 | 1231 | async def serve(reader, writer): 1232 | if not hasattr(writer, 'awrite'): # pragma: no cover 1233 | # CPython provides the awrite and aclose methods in 3.8+ 1234 | async def awrite(self, data): 1235 | self.write(data) 1236 | await self.drain() 1237 | 1238 | async def aclose(self): 1239 | self.close() 1240 | await self.wait_closed() 1241 | 1242 | from types import MethodType 1243 | writer.awrite = MethodType(awrite, writer) 1244 | writer.aclose = MethodType(aclose, writer) 1245 | 1246 | await self.handle_request(reader, writer) 1247 | 1248 | if self.debug: # pragma: no cover 1249 | print('Starting async server on {host}:{port}...'.format( 1250 | host=host, port=port)) 1251 | 1252 | try: 1253 | self.server = await asyncio.start_server(serve, host, port, 1254 | ssl=ssl) 1255 | except TypeError: # pragma: no cover 1256 | self.server = await asyncio.start_server(serve, host, port) 1257 | 1258 | while True: 1259 | try: 1260 | if hasattr(self.server, 'serve_forever'): # pragma: no cover 1261 | try: 1262 | await self.server.serve_forever() 1263 | except asyncio.CancelledError: 1264 | pass 1265 | await self.server.wait_closed() 1266 | break 1267 | except AttributeError: # pragma: no cover 1268 | # the task hasn't been initialized in the server object yet 1269 | # wait a bit and try again 1270 | await asyncio.sleep(0.1) 1271 | 1272 | def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None): 1273 | """Start the web server. This function does not normally return, as 1274 | the server enters an endless listening loop. The :func:`shutdown` 1275 | function provides a method for terminating the server gracefully. 1276 | 1277 | :param host: The hostname or IP address of the network interface that 1278 | will be listening for requests. A value of ``'0.0.0.0'`` 1279 | (the default) indicates that the server should listen for 1280 | requests on all the available interfaces, and a value of 1281 | ``127.0.0.1`` indicates that the server should listen 1282 | for requests only on the internal networking interface of 1283 | the host. 1284 | :param port: The port number to listen for requests. The default is 1285 | port 5000. 1286 | :param debug: If ``True``, the server logs debugging information. The 1287 | default is ``False``. 1288 | :param ssl: An ``SSLContext`` instance or ``None`` if the server should 1289 | not use TLS. The default is ``None``. 1290 | 1291 | Example:: 1292 | 1293 | from microdot import Microdot 1294 | 1295 | app = Microdot() 1296 | 1297 | @app.route('/') 1298 | async def index(request): 1299 | return 'Hello, world!' 1300 | 1301 | app.run(debug=True) 1302 | """ 1303 | asyncio.run(self.start_server(host=host, port=port, debug=debug, 1304 | ssl=ssl)) # pragma: no cover 1305 | 1306 | def shutdown(self): 1307 | """Request a server shutdown. The server will then exit its request 1308 | listening loop and the :func:`run` function will return. This function 1309 | can be safely called from a route handler, as it only schedules the 1310 | server to terminate as soon as the request completes. 1311 | 1312 | Example:: 1313 | 1314 | @app.route('/shutdown') 1315 | def shutdown(request): 1316 | request.app.shutdown() 1317 | return 'The server is shutting down...' 1318 | """ 1319 | self.server.close() 1320 | 1321 | def find_route(self, req): 1322 | method = req.method.upper() 1323 | if method == 'OPTIONS' and self.options_handler: 1324 | return self.options_handler(req), '', None 1325 | if method == 'HEAD': 1326 | method = 'GET' 1327 | f = 404 1328 | p = '' 1329 | s = None 1330 | for route_methods, route_pattern, route_handler, url_prefix, subapp \ 1331 | in self.url_map: 1332 | req.url_args = route_pattern.match(req.path) 1333 | if req.url_args is not None: 1334 | p = url_prefix 1335 | s = subapp 1336 | if method in route_methods: 1337 | f = route_handler 1338 | break 1339 | else: 1340 | f = 405 1341 | return f, p, s 1342 | 1343 | def default_options_handler(self, req): 1344 | allow = [] 1345 | for route_methods, route_pattern, _, _, _ in self.url_map: 1346 | if route_pattern.match(req.path) is not None: 1347 | allow.extend(route_methods) 1348 | if 'GET' in allow: 1349 | allow.append('HEAD') 1350 | allow.append('OPTIONS') 1351 | return {'Allow': ', '.join(allow)} 1352 | 1353 | async def handle_request(self, reader, writer): 1354 | req = None 1355 | try: 1356 | req = await Request.create(self, reader, writer, 1357 | writer.get_extra_info('peername')) 1358 | except Exception as exc: # pragma: no cover 1359 | print_exception(exc) 1360 | 1361 | res = await self.dispatch_request(req) 1362 | if res != Response.already_handled: # pragma: no branch 1363 | await res.write(writer) 1364 | try: 1365 | await writer.aclose() 1366 | except OSError as exc: # pragma: no cover 1367 | if exc.errno in MUTED_SOCKET_ERRORS: 1368 | pass 1369 | else: 1370 | raise 1371 | if self.debug and req: # pragma: no cover 1372 | print('{method} {path} {status_code}'.format( 1373 | method=req.method, path=req.path, 1374 | status_code=res.status_code)) 1375 | 1376 | def get_request_handlers(self, req, attr, local_first=True): 1377 | handlers = getattr(self, attr + '_handlers') 1378 | local_handlers = getattr(req.subapp, attr + '_handlers') \ 1379 | if req and req.subapp else [] 1380 | return local_handlers + handlers if local_first \ 1381 | else handlers + local_handlers 1382 | 1383 | async def error_response(self, req, status_code, reason=None): 1384 | if req and req.subapp and status_code in req.subapp.error_handlers: 1385 | return await invoke_handler( 1386 | req.subapp.error_handlers[status_code], req) 1387 | elif status_code in self.error_handlers: 1388 | return await invoke_handler(self.error_handlers[status_code], req) 1389 | return reason or 'N/A', status_code 1390 | 1391 | async def dispatch_request(self, req): 1392 | after_request_handled = False 1393 | if req: 1394 | if req.content_length > req.max_content_length: 1395 | # the request body is larger than allowed 1396 | res = await self.error_response(req, 413, 'Payload too large') 1397 | else: 1398 | # find the route in the app's URL map 1399 | f, req.url_prefix, req.subapp = self.find_route(req) 1400 | 1401 | try: 1402 | res = None 1403 | if callable(f): 1404 | # invoke the before request handlers 1405 | for handler in self.get_request_handlers( 1406 | req, 'before_request', False): 1407 | res = await invoke_handler(handler, req) 1408 | if res: 1409 | break 1410 | 1411 | # invoke the endpoint handler 1412 | if res is None: 1413 | res = await invoke_handler(f, req, **req.url_args) 1414 | 1415 | # process the response 1416 | if isinstance(res, int): 1417 | # an integer response is taken as a status code 1418 | # with an empty body 1419 | res = '', res 1420 | if isinstance(res, tuple): 1421 | # handle a tuple response 1422 | if isinstance(res[0], int): 1423 | # a tuple that starts with an int has an empty 1424 | # body 1425 | res = ('', res[0], 1426 | res[1] if len(res) > 1 else {}) 1427 | body = res[0] 1428 | if isinstance(res[1], int): 1429 | # extract the status code and headers (if 1430 | # available) 1431 | status_code = res[1] 1432 | headers = res[2] if len(res) > 2 else {} 1433 | else: 1434 | # if the status code is missing, assume 200 1435 | status_code = 200 1436 | headers = res[1] 1437 | res = Response(body, status_code, headers) 1438 | elif not isinstance(res, Response): 1439 | # any other response types are wrapped in a 1440 | # Response object 1441 | res = Response(res) 1442 | 1443 | # invoke the after request handlers 1444 | for handler in self.get_request_handlers( 1445 | req, 'after_request', True): 1446 | res = await invoke_handler( 1447 | handler, req, res) or res 1448 | for handler in req.after_request_handlers: 1449 | res = await invoke_handler( 1450 | handler, req, res) or res 1451 | after_request_handled = True 1452 | elif isinstance(f, dict): 1453 | # the response from an OPTIONS request is a dict with 1454 | # headers 1455 | res = Response(headers=f) 1456 | else: 1457 | # if the route is not found, return a 404 or 405 1458 | # response as appropriate 1459 | res = await self.error_response(req, f, 'Not found') 1460 | except HTTPException as exc: 1461 | # an HTTP exception was raised while handling this request 1462 | res = await self.error_response(req, exc.status_code, 1463 | exc.reason) 1464 | except Exception as exc: 1465 | # an unexpected exception was raised while handling this 1466 | # request 1467 | print_exception(exc) 1468 | 1469 | # invoke the error handler for the exception class if one 1470 | # exists 1471 | handler = None 1472 | res = None 1473 | if req.subapp and exc.__class__ in \ 1474 | req.subapp.error_handlers: 1475 | handler = req.subapp.error_handlers[exc.__class__] 1476 | elif exc.__class__ in self.error_handlers: 1477 | handler = self.error_handlers[exc.__class__] 1478 | else: 1479 | # walk up the exception class hierarchy to try to find 1480 | # a handler 1481 | for c in mro(exc.__class__)[1:]: 1482 | if req.subapp and c in req.subapp.error_handlers: 1483 | handler = req.subapp.error_handlers[c] 1484 | break 1485 | elif c in self.error_handlers: 1486 | handler = self.error_handlers[c] 1487 | break 1488 | if handler: 1489 | try: 1490 | res = await invoke_handler(handler, req, exc) 1491 | except Exception as exc2: # pragma: no cover 1492 | print_exception(exc2) 1493 | if res is None: 1494 | # if there is still no response, issue a 500 error 1495 | res = await self.error_response( 1496 | req, 500, 'Internal server error') 1497 | else: 1498 | # if the request could not be parsed, issue a 400 error 1499 | res = await self.error_response(req, 400, 'Bad request') 1500 | if isinstance(res, tuple): 1501 | res = Response(*res) 1502 | elif not isinstance(res, Response): 1503 | res = Response(res) 1504 | if not after_request_handled: 1505 | # if the request did not finish due to an error, invoke the after 1506 | # error request handler 1507 | for handler in self.get_request_handlers( 1508 | req, 'after_error_request', True): 1509 | res = await invoke_handler( 1510 | handler, req, res) or res 1511 | res.is_head = (req and req.method == 'HEAD') 1512 | return res 1513 | 1514 | 1515 | Response.already_handled = Response() 1516 | 1517 | abort = Microdot.abort 1518 | redirect = Response.redirect 1519 | send_file = Response.send_file 1520 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/microdot/websocket.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import hashlib 3 | from microdot import Request, Response 4 | from microdot.microdot import MUTED_SOCKET_ERRORS, print_exception 5 | from microdot.helpers import wraps 6 | 7 | 8 | class WebSocketError(Exception): 9 | """Exception raised when an error occurs in a WebSocket connection.""" 10 | pass 11 | 12 | 13 | class WebSocket: 14 | """A WebSocket connection object. 15 | 16 | An instance of this class is sent to handler functions to manage the 17 | WebSocket connection. 18 | """ 19 | CONT = 0 20 | TEXT = 1 21 | BINARY = 2 22 | CLOSE = 8 23 | PING = 9 24 | PONG = 10 25 | 26 | #: Specify the maximum message size that can be received when calling the 27 | #: ``receive()`` method. Messages with payloads that are larger than this 28 | #: size will be rejected and the connection closed. Set to 0 to disable 29 | #: the size check (be aware of potential security issues if you do this), 30 | #: or to -1 to use the value set in 31 | #: ``Request.max_body_length``. The default is -1. 32 | #: 33 | #: Example:: 34 | #: 35 | #: WebSocket.max_message_length = 4 * 1024 # up to 4KB messages 36 | max_message_length = -1 37 | 38 | def __init__(self, request): 39 | self.request = request 40 | self.closed = False 41 | 42 | async def handshake(self): 43 | response = self._handshake_response() 44 | await self.request.sock[1].awrite( 45 | b'HTTP/1.1 101 Switching Protocols\r\n') 46 | await self.request.sock[1].awrite(b'Upgrade: websocket\r\n') 47 | await self.request.sock[1].awrite(b'Connection: Upgrade\r\n') 48 | await self.request.sock[1].awrite( 49 | b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n') 50 | 51 | async def receive(self): 52 | """Receive a message from the client.""" 53 | while True: 54 | opcode, payload = await self._read_frame() 55 | send_opcode, data = self._process_websocket_frame(opcode, payload) 56 | if send_opcode: # pragma: no cover 57 | await self.send(data, send_opcode) 58 | elif data: # pragma: no branch 59 | return data 60 | 61 | async def send(self, data, opcode=None): 62 | """Send a message to the client. 63 | 64 | :param data: the data to send, given as a string or bytes. 65 | :param opcode: a custom frame opcode to use. If not given, the opcode 66 | is ``TEXT`` or ``BINARY`` depending on the type of the 67 | data. 68 | """ 69 | frame = self._encode_websocket_frame( 70 | opcode or (self.TEXT if isinstance(data, str) else self.BINARY), 71 | data) 72 | await self.request.sock[1].awrite(frame) 73 | 74 | async def close(self): 75 | """Close the websocket connection.""" 76 | if not self.closed: # pragma: no cover 77 | self.closed = True 78 | await self.send(b'', self.CLOSE) 79 | 80 | def _handshake_response(self): 81 | connection = False 82 | upgrade = False 83 | websocket_key = None 84 | for header, value in self.request.headers.items(): 85 | h = header.lower() 86 | if h == 'connection': 87 | connection = True 88 | if 'upgrade' not in value.lower(): 89 | return self.request.app.abort(400) 90 | elif h == 'upgrade': 91 | upgrade = True 92 | if not value.lower() == 'websocket': 93 | return self.request.app.abort(400) 94 | elif h == 'sec-websocket-key': 95 | websocket_key = value 96 | if not connection or not upgrade or not websocket_key: 97 | return self.request.app.abort(400) 98 | d = hashlib.sha1(websocket_key.encode()) 99 | d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11') 100 | return binascii.b2a_base64(d.digest())[:-1] 101 | 102 | @classmethod 103 | def _parse_frame_header(cls, header): 104 | fin = header[0] & 0x80 105 | opcode = header[0] & 0x0f 106 | if fin == 0 or opcode == cls.CONT: # pragma: no cover 107 | raise WebSocketError('Continuation frames not supported') 108 | has_mask = header[1] & 0x80 109 | length = header[1] & 0x7f 110 | if length == 126: 111 | length = -2 112 | elif length == 127: 113 | length = -8 114 | return fin, opcode, has_mask, length 115 | 116 | def _process_websocket_frame(self, opcode, payload): 117 | if opcode == self.TEXT: 118 | payload = payload.decode() 119 | elif opcode == self.BINARY: 120 | pass 121 | elif opcode == self.CLOSE: 122 | raise WebSocketError('Websocket connection closed') 123 | elif opcode == self.PING: 124 | return self.PONG, payload 125 | elif opcode == self.PONG: # pragma: no branch 126 | return None, None 127 | return None, payload 128 | 129 | @classmethod 130 | def _encode_websocket_frame(cls, opcode, payload): 131 | frame = bytearray() 132 | frame.append(0x80 | opcode) 133 | if opcode == cls.TEXT: 134 | payload = payload.encode() 135 | if len(payload) < 126: 136 | frame.append(len(payload)) 137 | elif len(payload) < (1 << 16): 138 | frame.append(126) 139 | frame.extend(len(payload).to_bytes(2, 'big')) 140 | else: 141 | frame.append(127) 142 | frame.extend(len(payload).to_bytes(8, 'big')) 143 | frame.extend(payload) 144 | return frame 145 | 146 | async def _read_frame(self): 147 | header = await self.request.sock[0].read(2) 148 | if len(header) != 2: # pragma: no cover 149 | raise WebSocketError('Websocket connection closed') 150 | fin, opcode, has_mask, length = self._parse_frame_header(header) 151 | if length == -2: 152 | length = await self.request.sock[0].read(2) 153 | length = int.from_bytes(length, 'big') 154 | elif length == -8: 155 | length = await self.request.sock[0].read(8) 156 | length = int.from_bytes(length, 'big') 157 | max_allowed_length = Request.max_body_length \ 158 | if self.max_message_length == -1 else self.max_message_length 159 | if length > max_allowed_length: 160 | raise WebSocketError('Message too large') 161 | if has_mask: # pragma: no cover 162 | mask = await self.request.sock[0].read(4) 163 | payload = await self.request.sock[0].read(length) 164 | if has_mask: # pragma: no cover 165 | payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload)) 166 | return opcode, payload 167 | 168 | 169 | async def websocket_upgrade(request): 170 | """Upgrade a request handler to a websocket connection. 171 | 172 | This function can be called directly inside a route function to process a 173 | WebSocket upgrade handshake, for example after the user's credentials are 174 | verified. The function returns the websocket object:: 175 | 176 | @app.route('/echo') 177 | async def echo(request): 178 | if not authenticate_user(request): 179 | abort(401) 180 | ws = await websocket_upgrade(request) 181 | while True: 182 | message = await ws.receive() 183 | await ws.send(message) 184 | """ 185 | ws = WebSocket(request) 186 | await ws.handshake() 187 | 188 | @request.after_request 189 | async def after_request(request, response): 190 | return Response.already_handled 191 | 192 | return ws 193 | 194 | 195 | def websocket_wrapper(f, upgrade_function): 196 | @wraps(f) 197 | async def wrapper(request, *args, **kwargs): 198 | ws = await upgrade_function(request) 199 | try: 200 | await f(request, ws, *args, **kwargs) 201 | except OSError as exc: 202 | if exc.errno not in MUTED_SOCKET_ERRORS: # pragma: no cover 203 | raise 204 | except WebSocketError: 205 | pass 206 | except Exception as exc: 207 | print_exception(exc) 208 | finally: # pragma: no cover 209 | try: 210 | await ws.close() 211 | except Exception: 212 | pass 213 | return Response.already_handled 214 | return wrapper 215 | 216 | 217 | def with_websocket(f): 218 | """Decorator to make a route a WebSocket endpoint. 219 | 220 | This decorator is used to define a route that accepts websocket 221 | connections. The route then receives a websocket object as a second 222 | argument that it can use to send and receive messages:: 223 | 224 | @app.route('/echo') 225 | @with_websocket 226 | async def echo(request, ws): 227 | while True: 228 | message = await ws.receive() 229 | await ws.send(message) 230 | """ 231 | return websocket_wrapper(f, websocket_upgrade) 232 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/static/index.css: -------------------------------------------------------------------------------- 1 | /* Overall Document Style */ 2 | body { 3 | display: flex; 4 | height: 100vh; 5 | margin: 0; 6 | background: #3d3d44; 7 | font-family: sans-serif; 8 | font-size: 14px; 9 | color: white; 10 | } 11 | 12 | .header img { 13 | float: center; 14 | /* width: 100px; 15 | height: 100px; 16 | background: #555; */ 17 | } 18 | 19 | .header h1 { 20 | position: relative; 21 | top: 18px; 22 | left: 10px; 23 | } 24 | 25 | /* Thermometer element */ 26 | #wrapper { 27 | margin: auto; 28 | /* display: flex; */ 29 | flex-direction: column; 30 | align-items: left; 31 | margin-left: 50px; 32 | } 33 | #thermometer { 34 | width: 25px; 35 | background: #38383f; 36 | height: 240px; 37 | position: relative; 38 | display: inline-block; 39 | border: 9px solid #2a2a2e; 40 | border-radius: 20px; 41 | z-index: 1; 42 | margin-bottom: 50px; 43 | } 44 | #thermometer:before, #thermometer:after { 45 | position: absolute; 46 | content: ""; 47 | border-radius: 50%; 48 | } 49 | #thermometer:before { 50 | width: 100%; 51 | height: 34px; 52 | bottom: 9px; 53 | background: #38383f; 54 | z-index: -1; 55 | } 56 | #thermometer:after { 57 | transform: translateX(-50%); 58 | width: 50px; 59 | height: 50px; 60 | background-color: #3dcadf; 61 | bottom: -41px; 62 | border: 9px solid #2a2a2e; 63 | z-index: -3; 64 | left: 50%; 65 | } 66 | #thermometer #graduations { 67 | height: 59%; 68 | top: 20%; 69 | width: 50%; 70 | } 71 | #thermometer #graduations, #thermometer #graduations:before { 72 | position: absolute; 73 | border-top: 2px solid rgba(0, 0, 0, 0.5); 74 | border-bottom: 2px solid rgba(0, 0, 0, 0.5); 75 | } 76 | #thermometer #graduations:before { 77 | content: ""; 78 | height: 34%; 79 | width: 100%; 80 | top: 32%; 81 | } 82 | #thermometer #temperature { 83 | bottom: 0; 84 | background: linear-gradient(#f17a65, #3dcadf) no-repeat bottom; 85 | width: 100%; 86 | border-radius: 20px; 87 | background-size: 100% 240px; 88 | transition: all 0.2s ease-in-out; 89 | } 90 | #thermometer #temperature, #thermometer #temperature:before, #thermometer #temperature:after { 91 | position: absolute; 92 | } 93 | #thermometer #temperature:before { 94 | content: attr(data-value); 95 | background: rgba(0, 0, 0, 0.7); 96 | color: white; 97 | z-index: 2; 98 | padding: 5px 10px; 99 | border-radius: 5px; 100 | font-size: 1em; 101 | line-height: 1; 102 | transform: translateY(50%); 103 | left: calc(100% + 1em / 1.5); 104 | top: calc(-1em + 5px - 5px * 2); 105 | } 106 | #thermometer #temperature:after { 107 | content: ""; 108 | border-top: 0.4545454545em solid transparent; 109 | border-bottom: 0.4545454545em solid transparent; 110 | border-right: 0.6666666667em solid rgba(0, 0, 0, 0.7); 111 | left: 100%; 112 | top: calc(-1em / 2.2 + 5px); 113 | } 114 | 115 | /* Alert Elements */ 116 | #alerts { 117 | margin-left: 75px; 118 | display: inline-block; 119 | position: absolute; 120 | } 121 | #alerts #lowa{ 122 | display: inline-block; 123 | } 124 | #alerts #higha{ 125 | display: inline-block; 126 | padding-left:50px; 127 | } 128 | .indicator { 129 | display: inline-block; 130 | margin: 15px; 131 | width: 15px; 132 | height: 15px; 133 | border-radius: 50%; 134 | position: relative; 135 | top: 15px; 136 | } 137 | .--success { 138 | background: #00d563; 139 | border: 1px solid #00bc57; 140 | color: #00d563; 141 | animation: blink 3s infinite; 142 | } 143 | .--error { 144 | background: #fd2f51; 145 | border: 1px solid #fd163c; 146 | color: #fd2f51; 147 | animation: blink 3s infinite; 148 | } 149 | input[type="text"] { 150 | margin-top: 50px; 151 | width:100px; 152 | } 153 | 154 | @keyframes blink { 155 | 0% { 156 | box-shadow: 0 0 10px; 157 | } 158 | 50% { 159 | box-shadow: 0 0 30px; 160 | } 161 | 100% { 162 | box-shadow: 0 0 10px; 163 | } 164 | } 165 | 166 | /* Information Elements */ 167 | #log { 168 | margin-top: 35px; 169 | } 170 | 171 | .info { 172 | /* background-color: #e7f3fe; */ 173 | border-left: 6px solid #2196F3; 174 | padding-left: 10px; 175 | } 176 | 177 | .info_button{ 178 | border: none; 179 | background-color: inherit; 180 | font-size: 16px; 181 | cursor: pointer; 182 | display: inline-block; 183 | } 184 | 185 | .hidden { 186 | display:none; 187 | } 188 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SparkFun TMP117 6 | 7 | 8 | 9 |
10 | 11 |

logo SparkFun TMP117 Temperature Readings

12 | 13 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 | 63 |
64 | 65 |
66 | 67 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkfun/sparkfun-python/630c1e09b664b7a156a5af7a2d3e3ff2491a55c8/examples/mpy_tmp117_web_server/static/logo.png -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/tmp117_server_ap.py: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # @file mpy_tmp117_server_ap.py 4 | # @brief This MicroPython file contains a full implementation of a web server that reads 5 | # temperature data from a TMP117 sensor and sends it to a client via a websocket. 6 | # The complementary client code that is served can be found in the static directory. 7 | # 8 | # @details 9 | # This module depends on the qwicc_tmp117 library to control the TMP117 sensor. 10 | # 11 | # @note This code is designed to work with the qwiic_tmp117 library and a compatible microcontroller, such as the 12 | # SparkFun IoT RedBoard - ESP32, or the SparkFun IoT RedBoard - RP2350 13 | # 14 | # @author SparkFun Electronics 15 | # @date March 2025 16 | # @copyright Copyright (c) 2024-2025, SparkFun Electronics Inc. 17 | # 18 | # SPDX-License-Identifier: MIT 19 | # @license MIT 20 | # 21 | 22 | # -------------------- Import the necessary modules -------------------- 23 | from microdot import Microdot, send_file 24 | from microdot.websocket import with_websocket 25 | import json 26 | import asyncio 27 | import wlan_ap 28 | import qwiic_tmp117 29 | 30 | # -------------------- Constants -------------------- 31 | kDoAlerts = True # Set to False to disable checking alerts. This will speed up temperature reads. 32 | kApSsid = "iot_redboard_tmp117" # This will be the SSID of the AP, the "Network Name" that you'll see when you scan for networks on your client device 33 | kApPass = "thermo_wave2" # This will be the password for the AP, that you'll use when you connect to the network from your client device 34 | 35 | # -------------------- Shared Variables -------------------- 36 | # Create instance of our TMP117 device 37 | myTMP117 = qwiic_tmp117.QwiicTMP117() 38 | 39 | # Use the Microdot framework to create a web server 40 | app = Microdot() 41 | 42 | # -------------------- Fahrenheit to Celcius -------------------- 43 | def f_to_c(degreesF): 44 | return (degreesF - 32) * 5/9 45 | 46 | def c_to_f(degreesC): 47 | return (degreesC * 9/5) + 32 48 | 49 | # -------------------- Set up the TMP117 -------------------- 50 | def config_TMP117(tmp117Device, doAlerts): 51 | """ 52 | @brief Function to configure the TMP117 sensor 53 | 54 | @param tmp117Device The QwiicTMP117 object to be configured. 55 | 56 | @details 57 | - This function initializes the TMP117 sensor and sets the high and low temperature limits. 58 | - If doAlerts is set to True, the TMP117 will be set to alert mode. 59 | """ 60 | print("Setting up TMP117") 61 | 62 | # Check if it's connected 63 | if tmp117Device.is_connected() == False: 64 | print("The TMP117 device isn't connected to the system. Please check your connection") 65 | exit() 66 | 67 | print("TMP117 device connected!") 68 | 69 | # Initialize the device 70 | tmp117Device.begin() 71 | 72 | if doAlerts: 73 | tmp117Device.set_high_limit(25.50) 74 | tmp117Device.set_low_limit(25) 75 | 76 | # Set to kAlertMode or kThermMode 77 | tmp117Device.set_alert_function_mode(tmp117Device.kAlertMode) 78 | 79 | print("TMP117 Configured!") 80 | 81 | # -------------------- Asynchronous Microdot Functions -------------------- 82 | @app.route('/') 83 | async def index(request): 84 | """ 85 | @brief Function/Route to the index.html file to serve it as the root page 86 | 87 | @param request The Microdot "Request" object containing details about a client HTTP request. 88 | 89 | @details 90 | - This function is asynchronous and is called when a client requests the root "/" path from our server. 91 | - The requested file is located in the "static" directory. 92 | - The requested file is sent to the client. 93 | """ 94 | return send_file('static/index.html') 95 | 96 | # Create server-side coroutine for websocket to send temperature data to the client 97 | async def send_temperature(tempSocket): 98 | """ 99 | @brief Server-side coroutine for websocket to send temperature data to the client. 100 | 101 | @param tempSocket The Microdot "WebSocket" object containing details about the websocket connection. 102 | 103 | @details 104 | - This coroutine is asynchronous and is started when the client connects to the websocket. 105 | - It sends temperature data to the client every 0.5 seconds. 106 | """ 107 | print("Spawned send_temperature coroutine") 108 | while True: 109 | if myTMP117.data_ready(): 110 | # We'll store all our results in a dictionary so it's easy to dump to JSON 111 | data = {"tempF": 0, "tempC": 0, "limitH": 75, "limitL": 65, "alertH": False, "alertL": False} 112 | data['tempC'] = myTMP117.read_temp_c() 113 | data['tempF'] = myTMP117.read_temp_f() 114 | 115 | if kDoAlerts: 116 | await asyncio.sleep(1.5) # This delay between reads to the config register is necessary. see qwiic_tmp117_ex2 117 | alertFlags = myTMP117.get_high_low_alert() # Returned value is a list containing the two flags 118 | data['alertL'] = bool(alertFlags[myTMP117.kLowAlertIdx]) 119 | data['alertH'] = bool(alertFlags[myTMP117.kHighAlertIdx]) 120 | data['limitL'] = c_to_f(myTMP117.get_low_limit()) 121 | data['limitH'] = c_to_f(myTMP117.get_high_limit()) 122 | 123 | data = json.dumps(data) # Convert to a json string to be parsed by client 124 | await tempSocket.send(data) 125 | await asyncio.sleep(0.5) 126 | 127 | @app.route('/temperature') 128 | @with_websocket 129 | async def handle_limits(request, ws): 130 | """ 131 | @brief Server-side coroutine for websocket to receive changes to the high and low temperature limits from the client 132 | 133 | Since our client code creates a websocket connection to the /temperature route, we'll define our websocket coroutine there 134 | 135 | @param request The Microdot "Request" object containing details about a client HTTP request. 136 | @param ws The Microdot "WebSocket" object containing details about the websocket connection. 137 | 138 | @details 139 | - This function is asynchronous and is called when a client requests a file from the server. 140 | - The requested file is located in the "static" directory. 141 | - The requested file is sent to the client. 142 | """ 143 | print("Spawned handle_limits coroutine") 144 | # We won't start sending data until now, when we know the client has connected to the websocket 145 | # Let's start the send_temperature coroutine and pass it the websocket object 146 | asyncio.create_task(send_temperature(ws)) 147 | while True: 148 | # Lets block here until we receive a message from the client 149 | data = await ws.receive() 150 | print("Received new limit: " + data) 151 | limitJson = json.loads(data) 152 | # Check if the client sent a new high or low limit, and update the TMP117 accordingly 153 | if 'low_input' in limitJson: 154 | toSet = f_to_c(limitJson['low_input']) 155 | print("setting low limit to: " + str(toSet)) 156 | myTMP117.set_low_limit(toSet) 157 | print("New low limit: " + str(myTMP117.get_low_limit())) 158 | if 'high_input' in limitJson: 159 | toSet = f_to_c(limitJson['high_input']) 160 | print("setting high limit to: " + str(toSet)) 161 | myTMP117.set_high_limit(toSet) 162 | print("New high limit: " + str(myTMP117.get_high_limit())) 163 | 164 | @app.route('/static/') 165 | async def static(request, path): 166 | """ 167 | @brief Function/Route to the static folder to serve up the HTML and CSS files 168 | 169 | @param request The Microdot "Request" object containing details about a client HTTP request. 170 | @param path The path to the requested file. 171 | 172 | @details 173 | - This function is asynchronous and is called when a client requests a file from the server. 174 | - The requested file is located in the "static" directory. 175 | - The requested file is sent to the client. 176 | """ 177 | if '..' in path: 178 | # directory traversal is not allowed 179 | return 'Not found', 404 180 | return send_file('static/' + path) 181 | 182 | def run(): 183 | """ 184 | @brief Configure the WLAN, and TMP117 and run the web server 185 | """ 186 | # Set up the AP 187 | print("Formatting WIFI") 188 | accessPointIp = wlan_ap.config_wlan_as_ap(kApSsid, kApPass) 189 | print("WiFi Configured!") 190 | 191 | # Set up the TMP117 192 | config_TMP117(myTMP117, kDoAlerts) 193 | 194 | # Print the IP address of the server, port 5000 is the default port for Microdot 195 | print("\nNavigate to http://" + accessPointIp + ":5000/ to view the TMP117 temperature readings\n") 196 | 197 | # Start the web server 198 | app.run() 199 | 200 | # Finally after we've defined all our functions, we'll call the run function to start the server! 201 | run() 202 | -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/wlan_ap/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # @file wlan_ap/__init__.py 4 | # @brief This Python file impoprts the correct config_wlan_as_ap function based on the platform. 5 | # 6 | # @author SparkFun Electronics 7 | # @date March 2025 8 | # @copyright Copyright (c) 2024-2025, SparkFun Electronics Inc. 9 | # 10 | # SPDX-License-Identifier: MIT 11 | # @license MIT 12 | # 13 | 14 | from sys import platform 15 | 16 | if platform.startswith("linux"): 17 | from .config_ap_linux import config_wlan_as_ap 18 | else: 19 | from .config_ap_micropython import config_wlan_as_ap -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/wlan_ap/config_ap_linux.py: -------------------------------------------------------------------------------- 1 | ## 2 | # @file config_ap_linux.py 3 | # @brief This Python file contains the functions to configure a Raspberry Pi Linux machine as an access point. 4 | # 5 | # @details This file must be run with sudo rights. 6 | # 7 | # @note This code was tested with a RaspberryPi 4 Model B Running Kernel v6.6, Debian GNU/Linux 12 (bookworm) 8 | # 9 | # @author SparkFun Electronics 10 | # @date March 2025 11 | # @copyright Copyright (c) 2024-2025, SparkFun Electronics Inc. 12 | # 13 | # SPDX-License-Identifier: MIT 14 | # @license MIT 15 | # 16 | 17 | # NOTE: 18 | # Import the required modules 19 | import os 20 | 21 | # # Define the default access point settings 22 | kDefaultSsid = "raspberry_pi_tmp117" 23 | kDefaultPassword = "I_Love_Qwiic13" 24 | kDefaultIp = "192.168.4.1/24" # On the raspberry pi we'll configure a static ip address for the access point 25 | kDefaultInterface = "wlan0" 26 | 27 | def add_to_network_manager_conf(): 28 | content = "" 29 | with open("/etc/NetworkManager/NetworkManager.conf", "r") as f: 30 | content = f.read() 31 | 32 | if ("[main]" in content) and ("dns=dnsmasq" not in content): 33 | content = content.replace("[main]", "[main]\ndns=dnsmasq") 34 | elif "[main]" not in content: 35 | content = "[main]\ndns=dnsmasq\n" + content 36 | else: 37 | return 38 | 39 | with open("/etc/NetworkManager/NetworkManager.conf", "w") as f: 40 | f.write(content) 41 | 42 | # Return a string representing the url to access the content 43 | def config_wlan_as_ap(ssid=kDefaultSsid, password=kDefaultPassword, ip=kDefaultIp, interface=kDefaultInterface): 44 | # Install packages that give us the necessary commands for the access point (dnsmasq) 45 | os.system("sudo apt update") 46 | os.system("sudo apt install -y dnsmasq") 47 | 48 | # add dnsmasq to /etc/NetworkManager/NetworkManager.conf 49 | add_to_network_manager_conf() 50 | 51 | # Disable the current instance of dnsmasq such that it doesn't conflict with the one started by network manager 52 | os.system("sudo systemctl disable dnsmasq") 53 | os.system("sudo systemctl stop dnsmasq") 54 | 55 | # Use network manager to create a new access point 56 | os.system(f"nmcli con delete myiot_ap") 57 | os.system(f"nmcli con add type wifi ifname {interface} mode ap con-name myiot_ap ssid {ssid} autoconnect false") 58 | os.system(f"nmcli con modify myiot_ap 802-11-wireless.band bg") 59 | os.system(f"nmcli con modify myiot_ap 802-11-wireless.channel 7") 60 | os.system(f"nmcli con modify myiot_ap wifi-sec.key-mgmt wpa-psk") 61 | os.system(f"nmcli con modify myiot_ap wifi-sec.proto rsn") 62 | os.system(f"nmcli con modify myiot_ap wifi-sec.group ccmp") 63 | os.system(f"nmcli con modify myiot_ap wifi-sec.pairwise ccmp") 64 | os.system(f"nmcli con modify myiot_ap wifi-sec.psk {password}") 65 | os.system(f"nmcli con modify myiot_ap ipv4.method shared ipv4.address {ip}") 66 | os.system(f"nmcli con modify myiot_ap ipv6.method disabled") 67 | os.system(f"nmcli con up myiot_ap") 68 | 69 | return ip.split("/")[0] 70 | 71 | # Now serve our content to the client -------------------------------------------------------------------------------- /examples/mpy_tmp117_web_server/wlan_ap/config_ap_micropython.py: -------------------------------------------------------------------------------- 1 | ## 2 | # @file config_ap_micropython.py 3 | # @brief This MicroPython file contains the functions to configure a supported MicroPython machine as an access point. 4 | # 5 | # @details This code depends on the available MicroPython `network` library to configure an access point. 6 | # 7 | # @note This code is designed to work with the available MicroPython `network` library and a compatible microcontroller, such as the 8 | # SparkFun IoT RedBoard - ESP32, or the SparkFun IoT RedBoard - RP2350 9 | # 10 | # @author SparkFun Electronics 11 | # @date March 2025 12 | # @copyright Copyright (c) 2024-2025, SparkFun Electronics Inc. 13 | # 14 | # SPDX-License-Identifier: MIT 15 | # @license MIT 16 | # 17 | 18 | import network 19 | 20 | # Return a string representing the ip address of the access point 21 | kDefaultSsid = "raspberry_pi_tmp117" 22 | kDefaultPassword = "iot_redboard_tmp117" 23 | 24 | def config_wlan_as_ap(ssid = kDefaultSsid, password = kDefaultPassword): 25 | ap = network.WLAN(network.AP_IF) 26 | ap.active(True) 27 | ap.config(essid=ssid, password=password) 28 | 29 | while ap.active() == False: 30 | pass 31 | 32 | config = ap.ifconfig() 33 | 34 | return str(config[0]) 35 | --------------------------------------------------------------------------------