From dd5a95bfb93fedc69d24590f11f25ec429a6318a Mon Sep 17 00:00:00 2001 From: Malcolm McKellips Date: Thu, 20 Mar 2025 09:35:14 -0600 Subject: [PATCH 1/2] Add comments for the tmp117 web server python code --- .../mpy_tmp117_web_server/tmp117_server_ap.py | 149 ++++++++++++------ 1 file changed, 104 insertions(+), 45 deletions(-) diff --git a/examples/mpy_tmp117_web_server/tmp117_server_ap.py b/examples/mpy_tmp117_web_server/tmp117_server_ap.py index 2b83f16..af1b192 100644 --- a/examples/mpy_tmp117_web_server/tmp117_server_ap.py +++ b/examples/mpy_tmp117_web_server/tmp117_server_ap.py @@ -1,6 +1,6 @@ ## -# @file mpy_rgb_ramp.py +# @file mpy_tmp117_server_ap.py # @brief This MicroPython file contains a full implementation of a web server that reads # temperature data from a TMP117 sensor and sends it to a client via a websocket. # The complementary client code that is served can be found in the static directory. @@ -19,6 +19,7 @@ # @license MIT # +# -------------------- Import the necessary modules -------------------- from microdot import Microdot, send_file from microdot.websocket import with_websocket import json @@ -26,62 +27,85 @@ import wlan_ap import qwiic_tmp117 -# defines +# -------------------- Constants -------------------- kDoAlerts = True # Set to False to disable checking alerts. This will speed up temperature reads. -kApSsid = "iot_redboard_tmp117" -kApPass = "thermo_wave2" +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 +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 -# fahrenheit to celcius +# -------------------- Shared Variables -------------------- +# Create instance of our TMP117 device +myTMP117 = qwiic_tmp117.QwiicTMP117() + +# Use the Microdot framework to create a web server +app = Microdot() + +# -------------------- Fahrenheit to Celcius -------------------- def f_to_c(degreesF): return (degreesF - 32) * 5/9 def c_to_f(degreesC): return (degreesC * 9/5) + 32 -# Set up the AP -print("Formatting WIFI") -accessPointIp = wlan_ap.config_wlan_as_ap(kApSsid, kApPass) -print("WiFi Configured!") -# print("Active config: ", config) - -# Set up the TMP117 -print("Setting up TMP117") -# Create instance of device -myTMP117 = qwiic_tmp117.QwiicTMP117() +# -------------------- Set up the TMP117 -------------------- +def config_TMP117(tmp117Device, doAlerts): + """ + @brief Function to configure the TMP117 sensor -# Check if it's connected -if myTMP117.is_connected() == False: - print("The TMP117 device isn't connected to the system. Please check your connection") - exit() + @param tmp117Device The QwiicTMP117 object to be configured. -print("TMP117 device connected!") - -# Initialize the device -myTMP117.begin() + @details + - This function initializes the TMP117 sensor and sets the high and low temperature limits. + - If doAlerts is set to True, the TMP117 will be set to alert mode. + """ + print("Setting up TMP117") + # Create instance of device + tmp117Device = qwiic_tmp117.QwiicTMP117() -if kDoAlerts: - myTMP117.set_high_limit(25.50) - myTMP117.set_low_limit(25) + # Check if it's connected + if tmp117Device.is_connected() == False: + print("The TMP117 device isn't connected to the system. Please check your connection") + exit() - # Set to kAlertMode or kThermMode - myTMP117.set_alert_function_mode(myTMP117.kAlertMode) + print("TMP117 device connected!") + + # Initialize the device + tmp117Device.begin() -print("TMP117 Configured!") + if doAlerts: + tmp117Device.set_high_limit(25.50) + tmp117Device.set_low_limit(25) -print("\nNavigate to http://" + accessPointIp + ":5000/ to view the TMP117 temperature readings\n") + # Set to kAlertMode or kThermMode + tmp117Device.set_alert_function_mode(tmp117Device.kAlertMode) -# Use the Microdot framework to create a web server -app = Microdot() + print("TMP117 Configured!") -# Route our root page to the index.html file +# -------------------- Asynchronous Microdot Functions -------------------- @app.route('/') async def index(request): + """ + @brief Function/Route to the index.html file to serve it as the root page + + @param request The Microdot "Request" object containing details about a client HTTP request. + + @details + - This function is asynchronous and is called when a client requests the root "/" path from our server. + - The requested file is located in the "static" directory. + - The requested file is sent to the client. + """ return send_file('static/index.html') - -gTempSocket = None # Create server-side coroutine for websocket to send temperature data to the client -async def send_temperature(): +async def send_temperature(tempSocket): + """ + @brief Server-side coroutine for websocket to send temperature data to the client. + + @param tempSocket The Microdot "WebSocket" object containing details about the websocket connection. + + @details + - This coroutine is asynchronous and is started when the client connects to the websocket. + - It sends temperature data to the client every 0.5 seconds. + """ print("Spawned send_temperature coroutine") while True: if myTMP117.data_ready(): @@ -99,23 +123,35 @@ async def send_temperature(): data['limitH'] = c_to_f(myTMP117.get_high_limit()) data = json.dumps(data) # Convert to a json string to be parsed by client - await gTempSocket.send(data) + await tempSocket.send(data) await asyncio.sleep(0.5) -# Create server-side coroutine for websocket to receive changes to the high and low temperature limits from the client -# Since our client code creates a websocket connection to the /temperature route, we'll define our websocket coroutine there @app.route('/temperature') @with_websocket async def handle_limits(request, ws): + """ + @brief Server-side coroutine for websocket to receive changes to the high and low temperature limits from the client + + Since our client code creates a websocket connection to the /temperature route, we'll define our websocket coroutine there + + @param request The Microdot "Request" object containing details about a client HTTP request. + @param ws The Microdot "WebSocket" object containing details about the websocket connection. + + @details + - This function is asynchronous and is called when a client requests a file from the server. + - The requested file is located in the "static" directory. + - The requested file is sent to the client. + """ print("Spawned handle_limits coroutine") - global gTempSocket - gTempSocket = ws # Let's save the websocket object so we can send data to it from our send_temperature coroutine # We won't start sending data until now, when we know the client has connected to the websocket - asyncio.create_task(send_temperature()) + # Let's start the send_temperature coroutine and pass it the websocket object + asyncio.create_task(send_temperature(ws)) while True: + # Lets block here until we receive a message from the client data = await ws.receive() print("Received new limit: " + data) limitJson = json.loads(data) + # Check if the client sent a new high or low limit, and update the TMP117 accordingly if 'low_input' in limitJson: toSet = f_to_c(limitJson['low_input']) print("setting low limit to: " + str(toSet)) @@ -127,9 +163,19 @@ async def handle_limits(request, ws): myTMP117.set_high_limit(toSet) print("New high limit: " + str(myTMP117.get_high_limit())) -# Route to the static folder to serve up the HTML and CSS files @app.route('/static/') async def static(request, path): + """ + @brief Function/Route to the static folder to serve up the HTML and CSS files + + @param request The Microdot "Request" object containing details about a client HTTP request. + @param path The path to the requested file. + + @details + - This function is asynchronous and is called when a client requests a file from the server. + - The requested file is located in the "static" directory. + - The requested file is sent to the client. + """ if '..' in path: # directory traversal is not allowed return 'Not found', 404 @@ -137,8 +183,21 @@ async def static(request, path): def run(): """ - @brief Run the web server - """ + @brief Configure the WLAN, and TMP117 and run the web server + """ + # Set up the AP + print("Formatting WIFI") + accessPointIp = wlan_ap.config_wlan_as_ap(kApSsid, kApPass) + print("WiFi Configured!") + + # Set up the TMP117 + config_TMP117(myTMP117, kDoAlerts) + + # Print the IP address of the server, port 5000 is the default port for Microdot + print("\nNavigate to http://" + accessPointIp + ":5000/ to view the TMP117 temperature readings\n") + + # Start the web server app.run() +# Finally after we've defined all our functions, we'll call the run function to start the server! run() From 845f5dedf07c8aa16ed56ca0ee76a88ad46cf8a9 Mon Sep 17 00:00:00 2001 From: Malcolm McKellips Date: Thu, 20 Mar 2025 14:31:03 -0600 Subject: [PATCH 2/2] Add documentation for TMP117 Web Demo --- docs/images/tmp117-web-server.png | Bin 0 -> 51882 bytes examples/mpy_tmp117_web_server/README.md | 235 +++++++++++++++++- .../mpy_tmp117_web_server/tmp117_server_ap.py | 2 - 3 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 docs/images/tmp117-web-server.png diff --git a/docs/images/tmp117-web-server.png b/docs/images/tmp117-web-server.png new file mode 100644 index 0000000000000000000000000000000000000000..83866df77dbc1ba884e86d551152837b7ba1304c GIT binary patch literal 51882 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�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 TXIl(Nz~6fr73oUJPeK0&Efbh* literal 0 HcmV?d00001 diff --git a/examples/mpy_tmp117_web_server/README.md b/examples/mpy_tmp117_web_server/README.md index 5ca30ed..efda36f 100644 --- a/examples/mpy_tmp117_web_server/README.md +++ b/examples/mpy_tmp117_web_server/README.md @@ -1,24 +1,245 @@ + +![TMP117-Web-Server](/docs/images/tmp117-web-server.png "TMP117 Web Server") + # MicroPython TMP117 Web Server Example +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. + +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)! + +## Contents + +* [Hardware](#hardware) +* [Installation](#installation) +* [Running the Example](#running-the-example) +* [Using the Webpage](#using-the-webpage) +* [Code Explanation](#code-explanation) +* [References and More](#references-and-more) + +## Hardware +In order to run this demo you will need: + +* [Qwiic Cable](https://www.sparkfun.com/sparkfun-qwiic-cable-kit.html) +* [TMP117 Temperature Sensor](https://www.sparkfun.com/sparkfun-high-precision-temperature-sensor-tmp117-qwiic.html) +* A client computer, phone, tablet, etc. to view the web app. +* EITHER: + 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)). + + OR + + 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). + +Connect the TMP117 to your board with your chosen qwiic method and you're ready to go. + +## Installation + +### MicroPython +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. -## Overview +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. Alternatively, you can issue the below command to automatically install the files to the correct location on your board: -## Hardware Hookup +```mpremote mip install github:sparkfun/sparkfun-python``` -## Install +In either case, after installing, the file structure on your board should look like this: +``` +/ + | + +--- lib/ + | |--- qwiic_i2c + | |--- __init__.py + | |--- micropython_i2c.py + | `--- i2c_driver.py + | |--- microdot + | |--- __init__.py + | |--- helpers.py + | |--- microdot.py + | `--- websocket.py + | |--- wlan_ap + | |--- __init__.py + | |--- config_ap_micropython.py + | `--- config_ap_linux.py + | |--- qwiic_tmp117.py + | + +--- static/ + | |--- index.css + | |--- index.html + | `--- logo.png + | + `--- tmp117_server_ap.py +``` + +### Raspberry Pi +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). + +If you manually copy the files, your directory structure should look like this: +``` +/your-directory-name + | + +--- qwiic_i2c + | |--- __init__.py + | |--- micropython_i2c.py + | `--- i2c_driver.py + +--- microdot + | |--- __init__.py + | |--- helpers.py + | |--- microdot.py + | `--- websocket.py + +--- wlan_ap + | |--- __init__.py + | |--- config_ap_micropython.py + | `--- config_ap_linux.py + +--- qwiic_tmp117.py + | + +--- static/ + | |--- index.css + | |--- index.html + | `--- logo.png + | + `--- tmp117_server_ap.py +``` + +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). ## Running the Example -## In-Depth Code Explanation -### Setting Up WLAN as an Access Point +### MicroPython + +#### Option 1: Thonny/Other IDE +If using Thonny, connect your board, open the ```tmp117_server_ap.py``` file, and click the green arrow button (run current script). + +#### Option 2: Command Line +If using the command line: +1) Execute ```mpremote``` to connect to your board +2) From the REPL, execute the following command to run the example: +```python +>>> exec(open("tmp117_server_ap.py").read()) +``` + +### Raspberry Pi +Run the file with sudo privileges: +```bash +sudo python3 tmp117_server_ap.py +``` + +## Using the Webpage +### Connecting +When you start the application, it should print the IP address and port that you should use to connect. For example: + +```Navigate to http://192.168.4.1:5000/ to view the TMP117 temperature readings``` + +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. + +Next, copy and paste (or ctrl+click) the connection link from above into your web browser (or enter it manually in a mobile device). + +### Thermometer +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. + +### Limit LEDs and Boxes +To change one of the limit values, enter a number in the corresponding "Set Limit" box and press enter. +After changing one of the values, give several seconds for the server to receive your request, +set the limit on the device, and respond with the value read back from the device to update the "Read Limit" box. + +If the temperature drops below the low limit or above the high limit, triggering an alert on the device, +the corresponding alert LED will turn red. +> [!NOTE] +> 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. + +## Code Explanation + +### Setting Up WLAN as an Access Point (MicroPython) +```python +def config_wlan_as_ap(ssid = kDefaultSsid, password = kDefaultPassword): + ap = network.WLAN(network.AP_IF) + ap.active(True) + ap.config(essid=ssid, password=password) + + while ap.active() == False: + pass + + config = ap.ifconfig() + + return str(config[0]) +``` + +```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 + + +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. ### Setting Up the TMP117 +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()```. ### The MicroDot Web Server Framework +[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. ### Serving Static Web Elements +When a client first connects, it will be to the root path "/" of our web app. We create a route to service this path: +```python +@app.route('/') +async def index(request): + return send_file('static/index.html') +``` + +Using the Microdot ```send_file()``` function, we choose display our hompage in ```index.html``` when the user first connects. -### Client-Server Communication with Websocket +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: +```python +@app.route('/static/') +async def static(request, path): + if '..' in path: + # directory traversal is not allowed + return 'Not found', 404 + return send_file('static/' + path) +``` +This creates a mapping from client requests to all ```static/``` paths and the files on our server in the "static" folder. + +### Client-Server Communication with WebSocket +Microdot also allows for easy interfacing with WebSockets. In our ```index.html``` client code, we create a WebSocket at the ```'/temperature``` path. +In our server code, we create a route for this path and specify that it will be a WebSocket using the ```@with_websocket``` decorator. +```python +@app.route('/temperature') +@with_websocket +async def handle_limits(request, ws): +``` + +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. ### Reading and Publishing Temperature +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. + +```python +async def send_temperature(tempSocket): + while True: + if myTMP117.data_ready(): + + data = {"tempF": 0, "tempC": 0, "limitH": 75, "limitL": 65, "alertH": False, "alertL": False} + data['tempC'] = myTMP117.read_temp_c() + data['tempF'] = myTMP117.read_temp_f() + + if kDoAlerts: + await asyncio.sleep(1.5) + alertFlags = myTMP117.get_high_low_alert() + data['alertL'] = bool(alertFlags[myTMP117.kLowAlertIdx]) + data['alertH'] = bool(alertFlags[myTMP117.kHighAlertIdx]) + data['limitL'] = c_to_f(myTMP117.get_low_limit()) + data['limitH'] = c_to_f(myTMP117.get_high_limit()) + + data = json.dumps(data) + await tempSocket.send(data) + await asyncio.sleep(0.5) +``` + +This asynchronous task is created by the handle_limits function when a client first connects and creates the websocket. + +```python +asyncio.create_task(send_temperature(ws)) +``` + + + +## References and More +Special thanks to the creators of the MIT-licensed elements below: +* [Miguel Grinberg and Microdot](https://github.com/miguelgrinberg/microdot) for the Microdot Web Framework. +* [Arkellys](https://codepen.io/Arkellys/details/rgpNBK) for the Thermometer HTML/CSS element. +* [Johnny Berkmans](https://codepen.io/berkmansjohnny/details/LzXbPV) for the Indicator LED HTML/CSS elements. -## References and More \ No newline at end of file +Find a bug or want a feature? [Let us know here](https://github.com/sparkfun/sparkfun-python/issues). Have fun and happy hacking! \ No newline at end of file diff --git a/examples/mpy_tmp117_web_server/tmp117_server_ap.py b/examples/mpy_tmp117_web_server/tmp117_server_ap.py index af1b192..f5e05fc 100644 --- a/examples/mpy_tmp117_web_server/tmp117_server_ap.py +++ b/examples/mpy_tmp117_web_server/tmp117_server_ap.py @@ -58,8 +58,6 @@ def config_TMP117(tmp117Device, doAlerts): - If doAlerts is set to True, the TMP117 will be set to alert mode. """ print("Setting up TMP117") - # Create instance of device - tmp117Device = qwiic_tmp117.QwiicTMP117() # Check if it's connected if tmp117Device.is_connected() == False: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy