Add weather dashboard example (#303)
This commit is contained in:
104
examples/gpio/weather/README.md
Normal file
104
examples/gpio/weather/README.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Microdot Weather Dashboard
|
||||
|
||||
This example reports the temperature and humidity, both as a web application
|
||||
and as a JSON API.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
BIN
examples/gpio/weather/circuit.png
Normal file
BIN
examples/gpio/weather/circuit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
3
examples/gpio/weather/config.py
Normal file
3
examples/gpio/weather/config.py
Normal file
@@ -0,0 +1,3 @@
|
||||
DHT22_PIN = 4 # GPIO pin for DHT22 sensor
|
||||
WIFI_ESSID = 'your_wifi_ssid'
|
||||
WIFI_PASSWORD = 'your_wifi_password'
|
||||
26
examples/gpio/weather/dht.py
Normal file
26
examples/gpio/weather/dht.py
Normal file
@@ -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
|
||||
169
examples/gpio/weather/index.html
Normal file
169
examples/gpio/weather/index.html
Normal file
@@ -0,0 +1,169 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Microdot Weather Dashboard</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gauge.js/1.3.9/gauge.min.js" integrity="sha512-/gkYCBz4KVyJb3Shz6Z1kKu9Za5EdInNezzsm2O/DPvAYhCeIOounTzi7yuIF526z3rNZfIDxcx+rJAD07p8aA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<style>
|
||||
html, body {
|
||||
height: 95%;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
#container {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
h1, p {
|
||||
text-align: center;
|
||||
}
|
||||
table {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
table h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
table p {
|
||||
margin: 0;
|
||||
}
|
||||
#temperature, #humidity {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
aspect-ratio: 2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<h1>Microdot Weather Dashboard</h1>
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td>
|
||||
<canvas id="temperature" width="400" height="200"></canvas>
|
||||
</td>
|
||||
<td>
|
||||
<canvas id="humidity" width="400" height="200"></canvas>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Temperature</p>
|
||||
<h1><span id="temperature-text">??</span>°C</h1>
|
||||
</td>
|
||||
<td>
|
||||
<p>Humidity</p>
|
||||
<h1><span id="humidity-text">??</span>%</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<p><i>Last updated: <span id="time-text">...</span></i></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
// create the temperature gauge
|
||||
let temperatureGauge = new Gauge(document.getElementById('temperature')).setOptions({
|
||||
angle: 0,
|
||||
lineWidth: 0.3,
|
||||
radiusScale: 1,
|
||||
pointer: {
|
||||
length: 0.6,
|
||||
strokeWidth: 0.035,
|
||||
color: '#000000',
|
||||
},
|
||||
limitMax: false,
|
||||
limitMin: false,
|
||||
highDpiSupport: true,
|
||||
staticLabels: {
|
||||
font: "14px sans-serif",
|
||||
labels: [-30, -20, -10, 0, 10, 20, 30, 40, 50],
|
||||
color: "#000000",
|
||||
fractionDigits: 0,
|
||||
},
|
||||
staticZones: [
|
||||
{strokeStyle: "#85a6e8", min: -30, max: 0},
|
||||
{strokeStyle: "#a5dde8", min: 0, max: 10},
|
||||
{strokeStyle: "#a5e8a6", min: 10, max: 20},
|
||||
{strokeStyle: "#e8d8a5", min: 20, max: 30},
|
||||
{strokeStyle: "#e8a8a5", min: 30, max: 50},
|
||||
],
|
||||
renderTicks: {
|
||||
divisions: 8,
|
||||
divWidth: 1.1,
|
||||
divLength: 0.7,
|
||||
divColor: '#333333',
|
||||
subDivisions: 4,
|
||||
subLength: 0.3,
|
||||
subWidth: 0.6,
|
||||
subColor: '#666666'
|
||||
}
|
||||
});
|
||||
temperatureGauge.maxValue = 50;
|
||||
temperatureGauge.setMinValue(-30);
|
||||
temperatureGauge.animationSpeed = 36;
|
||||
temperatureGauge.set(0);
|
||||
|
||||
let humidityGauge = new Gauge(document.getElementById('humidity')).setOptions({
|
||||
angle: 0,
|
||||
lineWidth: 0.3,
|
||||
radiusScale: 1,
|
||||
pointer: {
|
||||
length: 0.6,
|
||||
strokeWidth: 0.035,
|
||||
color: '#000000',
|
||||
},
|
||||
limitMax: false,
|
||||
limitMin: false,
|
||||
highDpiSupport: true,
|
||||
staticLabels: {
|
||||
font: "14px sans-serif",
|
||||
labels: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
||||
color: "#000000",
|
||||
fractionDigits: 0,
|
||||
},
|
||||
staticZones: [
|
||||
{strokeStyle: "#85a6e8", min: 0, max: 40},
|
||||
{strokeStyle: "#a5e8a6", min: 40, max: 70},
|
||||
{strokeStyle: "#e8a8a5", min: 70, max: 100},
|
||||
],
|
||||
renderTicks: {
|
||||
divisions: 10,
|
||||
divWidth: 1.1,
|
||||
divLength: 0.7,
|
||||
divColor: '#333333',
|
||||
subDivisions: 4,
|
||||
subLength: 0.3,
|
||||
subWidth: 0.6,
|
||||
subColor: '#666666'
|
||||
}
|
||||
});
|
||||
humidityGauge.maxValue = 100;
|
||||
humidityGauge.setMinValue(0);
|
||||
humidityGauge.animationSpeed = 36;
|
||||
humidityGauge.set(0);
|
||||
|
||||
async function update() {
|
||||
const response = await fetch('/api');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
temperatureGauge.set(data.temperature);
|
||||
humidityGauge.set(data.humidity);
|
||||
document.getElementById('temperature-text').textContent = data.temperature;
|
||||
document.getElementById('humidity-text').textContent = data.humidity;
|
||||
document.getElementById('time-text').textContent = new Date(data.time * 1000).toLocaleString();
|
||||
}
|
||||
setTimeout(update, 60000); // refresh every minute
|
||||
}
|
||||
update();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
12
examples/gpio/weather/machine.py
Normal file
12
examples/gpio/weather/machine.py
Normal file
@@ -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
|
||||
112
examples/gpio/weather/main.py
Normal file
112
examples/gpio/weather/main.py
Normal file
@@ -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())
|
||||
31
examples/gpio/weather/network.py
Normal file
31
examples/gpio/weather/network.py
Normal file
@@ -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
|
||||
BIN
examples/gpio/weather/screenshot.png
Normal file
BIN
examples/gpio/weather/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Reference in New Issue
Block a user