WiFi

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.117', '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).

ssid = 'mySSID'
password = 'myPassword'

api_key = 'myOpenWeatherMapAPIkey'

Para facilitar o acesso a estes serviços REST, iremos usar uma biblioteca (urequests) que já faz parte 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()

Quando o programa é executado é possível constatar em que IP o servidor se encontra (neste exemplo em 10.0.5.117):

Connecting to network...
................ Connected!
network config: ('10.0.5.117', '255.255.255.0', '10.0.5.1', '10.0.5.1')

Pelo que se em seguida acedermos a partir de um browser a esse IP obteremos a página Web que apresenta a mensagem “Hello world!”:

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:

  • index.html – ficheiro HTML que descreve o conteúdo da página web
  • main.py – script Python que cria o servidor e interage com o hardware, e que a seguir se apresenta:
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()

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.117). 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 obrigatoriamente 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 lhe acedermos 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.

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:

A imagem seguinte mostra o processo de criação da conexão ao broker público HiveMQ. De notar que, tratando-se duma aplicação Web a ligação faz-se por Websocket (ws://) no porto 8000 como se pode ver aqui.

A imagem seguinte mostra como se pode publicar uma mensagem para ligar o LED (em baixo), e como uma vez subscrito o tópico edm/button é possível receber as mensagens com o estado do botão:

Neste exemplo, volta a ser possível usar o simulador Wokwi, como se pode constatar aqui.