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 programa 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:
from secrets import ssid, password import network def do_connect(): 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(): pass print('network config:', wlan.ifconfig()) do_connect()
De notar que os dados de acesso à rede WiFi, por questões de sigilo, foram colocados num outro script (secrets.py):
ssid = 'mySSID' password = 'myPassword'
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 secrest.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 não fazendo parte do firmware original do MicroPython terá que ser adicionada, usando para isso o “gestor de pacotes” upip. Para isso basta no REPL Micropython executar os seguintes comandos:
import upip upip.install('micropython-urequests')
O pacote fica instalado na pasta /lib, e poderá ser descarregado para o nosso projeto caso façamos o respetivo “Download”.
Importante: Caso ocorra um erro durante o processo de instalação do pacote micropython-urequests há que insistir, ou em alternativa, incluir simplesmente o ficheiro urequests.py no nosso projeto.
O código seguinte tira partido do pacote urequests para aceder aos dados meteorológicos para a cidade do Porto.
from time import sleep import urequests from secrets 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!
Servidor de páginas Web
Para criar um servidor de páginas Web no ESP32 será conveniente usar uma biblioteca para o efeito. Das existentes iremos aqui apresentar a MicroWebSrv2 cujo código pode ser visto aqui.
Ao contrário do pacote usado no exemplo anterior (urequests), a instalação deste novo pacote (MicroWebSrv2), não pode ser realizada com o upip nem tão pouco fazendo a descarga do seu código fonte para dentro do projeto. É que sendo a memória RAM do ESP32 algo limitada (520KB), o pacote é demasiado grande para ser interpretado em runtime pelo MicroPython.
Uma forma de contornar este problema é gerar um novo firmware MicroPython que já integre o pacote MicroWebSrv2. Este procedimento envolve a compilação MicroPython a partir do seu código fonte usando uma toolchain disponibilizada para o efeito pelo fabricante Expressif designado de ESP-IDF. O processo que iremos descrever em seguida (vulgarmente designado cross installing packages with freezing“) foi realizado numa máquina virtual Linux (Ubuntu 18.04) a correr num servidor Amazon AWS.
1º passo – Instalação das ferramentas e do Python 3
sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev sudo apt-get install python3 python3-pip python3-setuptools sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10
2º passo – Instalação da toolchain ESP-IDF
cd ~ mkdir esp cd esp git clone https://github.com/espressif/esp-idf.git cd esp-idf git checkout 463a9d8b7f9af8205222b80707f9bdbba7c530e1 git submodule update --init --recursive ./install.sh export ESPIDF=$HOME/esp/esp-idf source $ESPIDF/export.sh
3º passo – Clonagem do repositório MicroPython
cd ~ git clone https://github.com/micropython/micropython.git cd ~/micropython/mpy-cross make
4º passo – Clonagem do repositório MicroWebSrv2
todo
5º passo – Obtenção do novo firmware
cd ~/micropython/ports/esp32 make submodules make
No final será criado o novo firmware (que iremos aqui partilhar evitando-se assim a realização de todo este processo) que deverá ser colocado no ESP32 usando a ferramenta esptool.py tal como foi aqui descrito.
O programa que apresentamos de seguida é basicamente formado por dois ficheiros:
- main.py – script Python que cria o servidor
- index.html – ficheiro HTML a situar dentro da pasta www a colocar na pasta src e que descreve a página web
Os respetivos códigos são mostrados de seguida:
# pylint: disable=global-statement, unused-argument from MicroWebSrv2 import MicroWebSrv2 from machine import Pin but = Pin(23, Pin.IN, Pin.PULL_UP) led = Pin(21, Pin.OUT) led.value(False) myWebSockets = None def OnWebSocketTextMsg(webSocket, msg): print('Received message: {0}'.format(msg)) led.value(True if msg == "LED ON" else False) def OnWebSocketClosed(webSocket): global myWebSockets myWebSockets = None def OnWebSocketAccepted(microWebSrv2, webSocket): global myWebSockets if myWebSockets is None: print('WebSocket from {0}'.format(webSocket.Request.UserAddress)) myWebSockets = webSocket myWebSockets.OnTextMessage = OnWebSocketTextMsg myWebSockets.OnClosed = OnWebSocketClosed mws2 = MicroWebSrv2() wsMod = MicroWebSrv2.LoadModule('WebSockets') wsMod.OnWebSocketAccepted = OnWebSocketAccepted mws2.SetEmbeddedConfig() mws2.NotFoundURL = '/' mws2.StartManaged() state = True def loop(): global state if but.value() != state: state = not state msg = 'Button ' + ('OFF' if state else 'ON') print(msg) if myWebSockets is not None: myWebSockets.SendTextMessage(msg) try: while mws2.IsRunning: loop() except KeyboardInterrupt: pass mws2.Stop()
<!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; 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 def do_connect(): ap = network.WLAN(network.AP_IF) ap.config(essid='ESP32-AP', authmode=network.AUTH_WPA_WPA2_PSK, password='12345678') ap.config(max_clients=5) ap.active(True) do_connect()
Neste caso é criada uma rede WiFi com o nome ESP32-AP cuja password de acesso é 12345678. O ESP32-AP fica com o IP 192.168.4.1 atribuindo os IPs seguintes ao dispositivos que se liguem a essa rede.
Caso a pasta www, para além de ficheiros html, contenha ficheiros de outro tipo (nomeadamente imagens) será necessário editar o ficheiro de configurações globais pymakr.json, de forma a que a operação de Upload também os inclua.
Por exemplo caso se pretenda incluir um favicon (favicon.ico), para que ele também enviado para o ESP32 será necessário ir a View > Command palette… > Pymakr > Global settings, e editar a linha:
"sync_file_types": "py,txt,log,json,xml,html,js,css,mpy",
para:
"sync_file_types": "py,txt,log,json,xml,html,js,css,mpy,ico",
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 master, e 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, o broker (Mosquito) foi instalado numa máquina virtual Linux (Ubuntu 18.04) a correr num servidor Amazon AWS, cujos dados de acesso são os seguintes:
- mqtt_server = ‘edm2020.ddns.net’
- mqtt_user = ‘edm’
- mqtt_pass = (enviada por email)
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:
# pylint: disable=global-statement, unused-variable from secrets import mqtt_pass from utime 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 = 'edm2020.ddns.net' 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'led': led.value(1 if message == b'1' else 0) def connect_and_subscribe(): mqtt_client = MQTTClient(client_id, mqtt_server, user='edm', password=mqtt_pass) mqtt_client.set_callback(sub_cb) mqtt_client.connect() topic_sub = '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'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 MQTT.fx respetivamente a publicar o estado pretendido para o LED e a subscrever o estado do botão:
Projeto final
Ver aqui.