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