Uma das características mais diferenciadoras do MCU ESP32 face aos demais é o facto dele possuir funcionalidades de comunicação sem fios WiFi e mesmo BLE (Bluetooth Low Energy). Nesta página iremos abordar como as comunicações por WiFi podem ser implementadas em programas MicroPython recorrendo a uma biblioteca chamada network.
O ESP32 pode estabeler o WiFi de duas formas distintas:
- ligando-se a uma rede WiFi existente na sua envolvente
- criando ele próprio essa rede WiFi, funcionando pois como um Access Point (AP)
Vamos começar pelo primeiro ponto, sendo necessário para tal saber o nome (SSID) e password da rede WiFi a que pretendemos ligar o ESP32.
Como em geral esta ligação deverá ocorrer assim que o ESP32 é ligado, vamos colocar o script de ligação no ficheiro boot.py (o script que é executado mesmo antes do main.py):
from mysecrets import ssid, password import time import network wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print('Connecting to network...') wlan.connect(ssid, password) while not wlan.isconnected(): print('.', end='') time.sleep(0.1) print(' Connected!') print('network config:', wlan.ifconfig())
De notar que os dados de acesso à rede WiFi, por questões de sigilo, foram colocados num outro script (mysecrets.py):
ssid = 'mySSID' password = 'myPassword'
Mesmo ainda sem qualquer script main.py é já possível verificar se o nosso ESP32 se liga à rede SSID especificada, pelo impressão do IP que lhe é atribuído, como por exemplo:
Connecting to network... ................ Connected! network config: ('10.0.5.187', '255.255.255.0', '10.0.5.1', '10.0.5.1')
Cliente de serviços REST
Agora que já temos acesso à rede e por isso à Internet vamos aceder a dados disponibilizados por servidores de serviços REST. Estes servidores permitem o acesso a diferentes tipos de dados (geralmente no formato json). Neste exemplo, vamos recorrer a uma das APIs acessíveis em openweathermap.org, para fornecimento de informação meteorológica (Current weather data).
Primeiro é necessário realizar um registo, que nos permite ter acesso a uma chave (API key) que dá acesso aos dados pretendidos. No nosso exemplo, e mais uma vez por questões de sigilo, esta chave será adicionada ao ficheiro mysecrest.py. De notar que o acesso a este serviço não tem qualquer custo desde que o acesso seja moderado (até 60 pedidos por minuto).
Para facilitar o acesso a estes serviços REST, iremos usar uma biblioteca (urequests) que já faz parte da versão 1.18 do firmware do MicroPython.
O código seguinte tira partido desta biblioteca urequests para aceder aos dados meteorológicos para a cidade do Porto.
from time import sleep import urequests from mysecrets import api_key from ujson import dumps def prettify(s): i = 0 for c in s: if c in ['[', '{']: print(c) i += 2 print(i*' ', end='') elif c in [']', '}']: print("") i -= 2 print(i*' ', end='') print(c, end='') elif c == ',': print(c) print((i-1)*' ', end='') else: print(c, end='') print("") while True: location = "Porto" url = "http://api.openweathermap.org/data/2.5/weather?q={0}&units=metric&appid={1}" \ .format(location, api_key) r = urequests.get(url).json() prettify(dumps(r)) print('Estão {0} graus na cidade do {1}, prevendo-se um máximo de {2} graus!' \ .format(r["main"]["temp"], r["name"], r["main"]["temp_max"])) sleep(5)
O resultado (atualizado a cada 5 segundos), é o seguinte:
{ "timezone": 3600, "cod": 200, "dt": 1590414115, "base": "stations", "weather": [ { "id": 800, "icon": "01d", "main": "Clear", "description": "clear sky" } ], "sys": { "country": "PT", "sunrise": 1590383279, "sunset": 1590436511, "id": 6900, "type": 1 }, "name": "Porto", "clouds": { "all": 7 }, "coord": { "lon": -8.61, "lat": 41.15 }, "visibility": 10000, "wind": { "speed": 3.6, "deg": 310 }, "id": 2735943, "main": { "feels_like": 26.7, "pressure": 1023, "temp_min": 22, "humidity": 68, "temp_max": 28.33, "temp": 25.79 } } Estão 25.79 graus na cidade do Porto, prevendo-se um máximo de 28.33 graus!
Este programa pode perfeitamente ser testado no simulador Wokwi. Um exemplo disso mesmo pode ser visto aqui.
De notar que a rede WiFi a usar nesse caso terá obrigatoriamente um SSID igual a Wokwi-GUEST, sem qualquer password!
Servidor de páginas Web
Para criar um servidor de páginas Web no ESP32 será conveniente usar uma biblioteca para o efeito. A biblioteca micropython-aioweb que usaremos neste exemplo recorre ao paradigma da programação assíncrona já abordada anteriormente, bastando incluir o ficheiro web.py no nosso projeto.
Tal como consta do exemplo incluído na descrição da biblioteca, uma página web simples poderia ser servida pelo seguinte programa main.py:
import web import uasyncio as asyncio app = web.App(host='0.0.0.0', port=80) # root route handler @app.route('/') async def handler(r, w): # write http headers w.write(b'HTTP/1.0 200 OK\r\n') w.write(b'Content-Type: text/html; charset=utf-8\r\n') w.write(b'\r\n') # write page body w.write(b'Hello world!') # drain stream buffer await w.drain() # Start event loop and create server task loop = asyncio.get_event_loop() loop.create_task(app.serve()) loop.run_forever()
Contudo no nosso caso pretendemos uma página que interaja com determinados componentes ligados ao ESP32 (concretamente um LED e um botão), pelo que teremos que usar websockets para o efeito (funcionalidade incluída na biblioteca escolhida).
Para além do ficheiro web.py, o programa que apresentamos de seguida é basicamente formado por dois ficheiros:
- main.py – script Python que cria o servidor e interage com o hardware
- index.html – ficheiro HTML que descreve o conteúdo da página web
Os respetivos códigos são mostrados de seguida:
import network import web import uasyncio as asyncio from machine import Pin led = Pin(21, Pin.OUT) led.value(False) but = Pin(23, Pin.IN, Pin.PULL_UP) async def checkbut(): global WS_CLIENTS state = True while True: if but.value() != state: state = not state msg = 'Button ' + ('OFF' if state else 'ON') print(msg) for ws_client in WS_CLIENTS: try: await ws_client.send(msg) except: continue await asyncio.sleep_ms(5) app = web.App(host='0.0.0.0', port=80) # Store current WebSocket clients WS_CLIENTS = set() # root route handler @app.route('/') async def index_handler(r, w): f = open('index.html') w.write(f.read()) f.close() await w.drain() # /ws WebSocket route handler @app.route('/ws') async def ws_handler(r, w): global WS_CLIENTS # upgrade connection to WebSocket ws = await web.WebSocket.upgrade(r, w) r.closed = False # add current client to set WS_CLIENTS.add(ws) while ws.open: # handle ws events evt = await ws.recv() if evt is None or evt['type'] == 'close': ws.open = False elif evt['type'] == 'text': msg = evt['data'] led.value(True if msg == "LED ON" else False) # remove current client from set WS_CLIENTS.discard(ws) # Start event loop and create server task loop = asyncio.get_event_loop() loop.create_task(app.serve()) loop.create_task(checkbut()) loop.run_forever()
<!DOCTYPE html> <html> <head> <title>Button LED webpage</title> </head> <script language="javascript"> function init() { var scheme if (window.location.protocol == 'https:') scheme = 'wss:'; else scheme = 'ws:'; var wsUri = scheme + '//' + window.location.hostname + '/ws'; logMsg("Connecting to " + wsUri + "...") websocket = new WebSocket(wsUri); websocket.onopen = function(evt) { onOpen (evt) }; websocket.onclose = function(evt) { onClose (evt) }; websocket.onmessage = function(evt) { onMessage (evt) }; websocket.onerror = function(evt) { onError (evt) }; } function onOpen(evt) { logMsg("Connected"); } function onClose(evt) { logMsg("Disconnected"); } function onMessage(evt) { logMsg(evt.data); document.getElementById("button").innerHTML = evt.data; } function onError(evt) { logMsg('ERROR: ' + evt.data); } function logMsg(s) { document.getElementById("log").value += s + '\n'; } function ledClick() { if (document.getElementById("led").checked) websocket.send('LED ON'); else websocket.send('LED OFF'); } window.addEventListener("load", init, false); </script> <body> <h1>Button LED webpage</h1> <input type="checkbox" id="led" onclick="ledClick()">LED<br> <p id="button">Button Off</p> <textarea id="log" rows="10" cols="100"></textarea> </body> </html>
Não é intenção desta unidade curricular abordar em profundidade a temática da construção de páginas web nem muito menos abordar o conceito dos Websockets aqui usados. O código apresentado pode contudo constituir um ponto de partida para a análise destas temáticas.
A página Web servida pelo ESP32 pode ser acedida através dum Browser indicando o IP atribuído ao ESP32 (neste caso particular o 10.0.5.137). Nesta página não só é possível controlar o estado dum LED na placa como também receber o estado dum dos seus botões assim que ele muda de estado.
Neste último exemplo o ESP32 está a aceder a uma rede WiFi existente nas suas imediações mas como já foi referido na ausência dessa rede, ele próprio pode criar um Access Point. Para tal o script boot.py tem que ser alterado em conformidade:
import network wlan = network.WLAN(network.AP_IF) wlan.config(essid='ESP32-AP', password='12345678', authmode=network.AUTH_WPA_WPA2_PSK) wlan.config(max_clients=5) wlan.active(True) print('network config:', wlan.ifconfig())
Neste caso é criada uma rede WiFi com o nome ESP32-AP cuja password de acesso é 12345678. Nessa rede, o ESP32-AP fica com o IP 192.168.4.1, pelo que deverá agora ser a esse IP que o browser deverá se ligar (desde que esteja a ser executado num dispositivo ligado a essa mesma rede).
De notar, que caso este servidor seja executado no simulador Wokwi, o nosso browser não consegue aceder nem a essa rede ESP32-AP nem à rede gerada pelo simulador (Wokwi-GUEST), pelo que nesta situação não nos é possível abrir a página Web.
Protocolo MQTT
Uma das desvantagens do método anterior reside no facto de para nos ligarmos à placa temos que saber o seu IP. Mesmo sabendo, estando ela dentro duma rede local, para acedermos a ela será necessário encaminhar o porto usado ao nível do router, o que nem sempre é possível.
Uma outra abordagem passa pelo uso dum protocolo diferente do HTTP (HyperText Transfer Protocol) usado nas páginas Web. Esse outro protocolo chama-se MQTT (Message Queuing Telemetry Transport) e não só é mais leve e por isso mais adequado à troca de pequenas mensagens indicadoras do estado do LED e do botão, como também se baseia num princípio de funcionamento diferente designado de publish-subscribe.
Nesta nova filosofia, deixa de haver um servidor (o ESP32) e o dispositivo que lhe quer aceder (por exemplo o PC) passam a ser ambos clientes ligando-se a um terceiro elemento: o broker que negoceia a troca de mensagens entre eles.
Quem tem algo a publicar (o estado do botão no caso do ESP32, e o estado pretendido para o LED no caso do PC) fá-lo para o broker, subscrevendo cada um deles as mensagens que lhe interessam (o estado do botão no caso do PC, e o estado pretendido para o LED no caso do ESP32).
No nosso caso, vamos recorrer a um broker público HiveMQ, situado no endereço broker.hivemq.com.
No seguinte programa voltamos a ter possibilidade de ligar/desligar o LED assim como saber sempre que o botão mudar o seu estado, mas desta vez usando o protocolo MQTT:
from time import sleep from umqttsimple import MQTTClient import ubinascii from machine import Pin, unique_id, reset but = Pin(23, Pin.IN, Pin.PULL_UP) led = Pin(21, Pin.OUT) led.value(False) mqtt_server = 'broker.hivemq.com' client_id = ubinascii.hexlify(unique_id()) def sub_cb(topic, message): print("Received MQTT message: topic '{0}', value '{1}'" \ .format(topic.decode("utf-8"), message.decode("utf-8"))) if topic == b'edm/led': led.value(1 if message == b'1' else 0) def connect_and_subscribe(): mqtt_client = MQTTClient(client_id, mqtt_server) mqtt_client.set_callback(sub_cb) mqtt_client.connect() topic_sub = 'edm/led' mqtt_client.subscribe(topic_sub) print('Connected to {0} MQTT broker'.format(mqtt_server)) return mqtt_client def restart_and_reconnect(): print('Failed to connect to MQTT broker. Reconnecting...') sleep(10) reset() try: client = connect_and_subscribe() except OSError as e: restart_and_reconnect() state = True def loop(): try: client.check_msg() global state if but.value() != state: state = not state msg = 'Button ' + ('OFF' if state else 'ON') print(msg) client.publish(b'edm/button', b'0' if state else b'1') except OSError as e: restart_and_reconnect() try: while True: loop() except KeyboardInterrupt: pass
De notar a importação do pacote umqttsimple.py que faz uma implementação simplificada do protocolo MQTT, e que por isso tem que ser adicionado ao projeto.
Resta agora ter no PC (ou em outro dispositivo qualquer) um programa cliente que possa publicar/subscrever mensagens MQTT. Dependendo do sistema operativo, deixo aqui as seguintes sugestões:
As seguintes imagens mostram a aplicação MQTTLens a subscrever o estado do botão e publicar o estado pretendido para o LED:
Neste exemplo, volta a ser possível usar o simulador Wokwi, como se pode constatar aqui.