diff --git a/examples/gpio/weather/README.md b/examples/gpio/weather/README.md new file mode 100644 index 0000000..27bda00 --- /dev/null +++ b/examples/gpio/weather/README.md @@ -0,0 +1,104 @@ +# Microdot Weather Dashboard + +This example reports the temperature and humidity, both as a web application +and as a JSON API. + +![Weather Dashboard Screenshot](screenshot.png) + +## Requirements + +- A microcontroller that supports MicroPython (e.g. ESP8266, ESP32, Raspberry + Pi Pico W, etc.) +- A DHT22 temperature and humidity sensor +- A breadboard and some jumper wires to create the circuit + +## Circuit + +Install the microconller and the DHT22 sensor on different parts of the +breadboard. Make the following connections with jumper wires: + +- from a microcontroller power pin (3.3V or 5V) to the left pin of the DHT22 + sensor. +- from a microcontroller `GND` pin to the right pin of the DHT22 sensor. +- from any available microcontroller GPIO pin to the middle pin of the DHT22 + sensor. If the DHT22 sensor has 4 pins instead of 3, use the one on the left, + next to the pin receiving power. + +The following diagram shows a possible wiring for this circuit using an ESP8266 +microcontroller and the 4-pin variant of the DHT22. In this diagram the data +pin of the DHT22 sensor is connected to pin `D2` of the ESP8266, which is +assigned to GPIO #4. Note that the location of the pins in the microcontroller +board will vary depending on which microcontroller you use. + +![Circuit diagram](circuit.png) + +## Installation + +Edit *config.py* as follows: + +- Set the `DHT22_PIN` variable to the GPIO pin number connected to the sensor's + data pin. Make sure you consult the documentation for your microcontroller to + learn what number you should use for your chosen GPIO pin. In the example + diagram above, the value should be 4. +- Enter your Wi-Fi SSID name and password in this file. + +Install MicroPython on your microcontroller board following instructions on the +MicroPython website. Then use a tool such as +[rshell](https://github.com/dhylands/rshell) to upload the following files to +the board: + +- *main.py* +- *config.py* +- *index.html* +- *microdot.py* + +You can find *microdot.py* in the *src/microdot* directory of this repository. + +If you are using a low end microcontroller such as the ESP8266, it is quite +possible that the *microdot.py* file will fail to compile due to the +MicroPython compiler needing more RAM than available in the device. In that +case, you can install the `mpy-cross` Python package in your computer (same +version as your MicroPython firmware) and precompile this file. The precompiled +file will have the name *microdot.mpy*. Upload this file and remove +*microdot.py* from the device. + +When the device is restarted after the files were uploaded, it will connect to +Wi-Fi and then start a web server on port 8000. One way to find out which IP +address was assigned to your device is to check your Wi-Fi's router +administration panel. Another option is to connect to the MicroPython REPL with +`rshell` or any other tool that you like, and then press Ctrl-D at the +MicroPython prompt to soft boot the device. The IP address is printed to the +terminal on startup. + +You should not upload other *.py* files that exist in this directory to your +device. These files are used when running with emulated hardware. + +## Trying out the application + +Once the device is running the server, you can connect to it using a web +browser. For example, if your device's Wi-Fi connection was assigned the IP +address 192.168.0.145, type *http://192.168.0.45:8000/* in your browser's +address bar. Note it is *http://* and not *https://*. This example does not use +the TLS/SSL protocol. + +To test the JSON API, you can use `curl` or your favorite HTTP client. The API +endpoint uses the */api* path, with the same URL as the main website. Here is +an example using `curl`: + +```bash +$ curl http://192.168.0.145:8000/api +{"temperature": 21.6, "humidity": 58.9, "time": 1752444652} +``` + +The `temperature` value is given in degrees Celsius. The `humidity` value is +given as a percentage. The `time` value is a UNIX timestamp. + +## Running in Emulation mode + +You can run this application on your computer, directly from this directory. +When used in this way, the DHT22 hardware is emulated, and the temperature and +humidity values are randomly generated. + +The only dependency that is needed for this application to run in emulation +mode is `microdot`, so make sure that is installed, or else add a copy of the +*microdot.py* from the *src/microdot* directory in this folder. diff --git a/examples/gpio/weather/circuit.png b/examples/gpio/weather/circuit.png new file mode 100644 index 0000000..f908b8c Binary files /dev/null and b/examples/gpio/weather/circuit.png differ diff --git a/examples/gpio/weather/config.py b/examples/gpio/weather/config.py new file mode 100644 index 0000000..174934a --- /dev/null +++ b/examples/gpio/weather/config.py @@ -0,0 +1,3 @@ +DHT22_PIN = 4 # GPIO pin for DHT22 sensor +WIFI_ESSID = 'your_wifi_ssid' +WIFI_PASSWORD = 'your_wifi_password' diff --git a/examples/gpio/weather/dht.py b/examples/gpio/weather/dht.py new file mode 100644 index 0000000..3ab2ef0 --- /dev/null +++ b/examples/gpio/weather/dht.py @@ -0,0 +1,26 @@ +""" +DO NOT UPLOAD THIS FILE TO YOUR MICROPYTHON DEVICE + +This module emulates MicroPython's DHT22 driver. It can be used when running +on a system without the DHT22 hardware. + +The temperature and humidity values that are returned are random values. +""" + +from random import random + + +class DHT22: + def __init__(self, pin): + self.pin = pin + + def measure(self): + pass + + def temperature(self): + """Return a random temperature between 10 and 30 degrees Celsius.""" + return random() * 20 + 10 + + def humidity(self): + """Return a random humidity between 30 and 70 percent.""" + return random() * 40 + 30 diff --git a/examples/gpio/weather/index.html b/examples/gpio/weather/index.html new file mode 100644 index 0000000..53bddac --- /dev/null +++ b/examples/gpio/weather/index.html @@ -0,0 +1,169 @@ + + + + Microdot Weather Dashboard + + + + + + +
+

Microdot Weather Dashboard

+ + + + + + + + + + + + +
+ + + +
+

Temperature

+

??°C

+
+

Humidity

+

??%

+
+

Last updated: ...

+
+
+ + + + diff --git a/examples/gpio/weather/machine.py b/examples/gpio/weather/machine.py new file mode 100644 index 0000000..9c6a246 --- /dev/null +++ b/examples/gpio/weather/machine.py @@ -0,0 +1,12 @@ +""" +DO NOT UPLOAD THIS FILE TO YOUR MICROPYTHON DEVICE + +This module emulates parts of MicroPython's `machine` module, to enable to run +MicroPython applications on UNIX, Mac or Windows systems without dedicated +hardware. +""" + + +class Pin: + def __init__(self, pin): + self.pin = pin diff --git a/examples/gpio/weather/main.py b/examples/gpio/weather/main.py new file mode 100644 index 0000000..855771f --- /dev/null +++ b/examples/gpio/weather/main.py @@ -0,0 +1,112 @@ +import asyncio +import dht +import gc +import machine +import network +import socket +import time + +import config +from microdot import Microdot, send_file + +app = Microdot() +current_temperature = None +current_humidity = None +current_time = None + + +def wifi_connect(): + """Connect to the configured Wi-Fi network. + Returns the IP address of the connected interface. + """ + ap_if = network.WLAN(network.AP_IF) + ap_if.active(False) + + sta_if = network.WLAN(network.STA_IF) + if not sta_if.isconnected(): + print('connecting to network...') + sta_if.active(True) + sta_if.connect(config.WIFI_ESSID, config.WIFI_PASSWORD) + for i in range(20): + if sta_if.isconnected(): + break + time.sleep(1) + if not sta_if.isconnected(): + raise RuntimeError('Could not connect to network') + return sta_if.ifconfig()[0] + + +def get_current_time(): + """Return the current Unix time. + Note that because many microcontrollers do not have a clock, this function + makes a call to an NTP server to obtain the current time. A Wi-Fi + connection needs to be in place before calling this function. + """ + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(5) + s.sendto(b'\x1b' + 47 * b'\0', + socket.getaddrinfo('pool.ntp.org', 123)[0][4]) + msg, _ = s.recvfrom(1024) + return ((msg[40] << 24) | (msg[41] << 16) | (msg[42] << 8) | msg[43]) - \ + 2208988800 + + +def get_current_weather(): + """Read the temperature and humidity from the DHT22 sensor. + Returns them as a tuple. The returned temperature is in degrees Celcius. + The humidity is a 0-100 percentage. + """ + d = dht.DHT22(machine.Pin(config.DHT22_PIN)) + d.measure() + return d.temperature(), d.humidity() + + +async def refresh_weather(): + """Background task that updates the temperature and humidity. + This task is designed to run in the background. It connects to the DHT22 + temperature and humidity sensor once per minute and stores the updated + readings in global variables. + """ + global current_temperature + global current_humidity + global current_time + + while True: + try: + t = get_current_time() + temp, hum = get_current_weather() + except asyncio.CancelledError: + raise + except Exception as error: + print(f'Could not obtain weather, error: {error}') + else: + current_time = t + current_temperature = int(temp * 10) / 10 + current_humidity = int(hum * 10) / 10 + gc.collect() + await asyncio.sleep(60) + + +@app.route('/') +async def index(request): + return send_file('index.html') + + +@app.route('/api') +async def api(request): + return { + 'temperature': current_temperature, + 'humidity': current_humidity, + 'time': current_time, + } + + +async def start(): + ip = wifi_connect() + print(f'Starting server at http://{ip}:8000...') + bgtask = asyncio.create_task(refresh_weather()) + server = asyncio.create_task(app.start_server(port=8000)) + await asyncio.gather(server, bgtask) + + +asyncio.run(start()) diff --git a/examples/gpio/weather/network.py b/examples/gpio/weather/network.py new file mode 100644 index 0000000..711ebbb --- /dev/null +++ b/examples/gpio/weather/network.py @@ -0,0 +1,31 @@ +""" +DO NOT UPLOAD THIS FILE TO YOUR MICROPYTHON DEVICE + +This module emulates parts of MicroPython's `network` module, in particular +those related to establishing a Wi-Fi connection. This enables to run +MicroPython applications on UNIX, Mac or Windows systems without dedicated +hardware. + +Note that no connections are attempted. The assumption is that the system is +already connected. The "127.0.0.1" address is always returned. +""" + +AP_IF = 1 +STA_IF = 2 + + +class WLAN: + def __init__(self, network): + self.network = network + + def isconnected(self): + return True + + def ifconfig(self): + return ('127.0.0.1', 'n/a', 'n/a', 'n/a') + + def connect(self): + pass + + def active(self, active=None): + pass diff --git a/examples/gpio/weather/screenshot.png b/examples/gpio/weather/screenshot.png new file mode 100644 index 0000000..08a032d Binary files /dev/null and b/examples/gpio/weather/screenshot.png differ