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