Update on 17th November 2021: Added a second example using the wifi
library.
Introduction
Recently I’ve been using the Raspberry Pi Pico to record data and send it to me over a WiFi network. Although there are many ways to do this, receiving the results by email had a certain retro appeal. The Pico doesn’t have native WiFi support, so I used an ESP32 as a WiFi coprocessor, and drove it all from Adafruit’s CircuitPython stack.
The data are collected on my local LAN, where I already run an open (to the LAN) SMTP1 server. I hoped that there would already be SMTP support in one of the CircuitPython libraries, but I couldn’t find it. Happily though for this very simple case (no authentication, no fancy 8-bit extensions), it was easy to roll my own.
Hello World
Annoyingly the wifi
library used for ports with native WiFi support
and the adafruit_esp32spi
library used for ESP32 WiFi coprocessors
have different APIs.
wifi
version
Most CircuitPython boards with integrated WiFi support seem to use the wifi
library for low-level stuff and socketpool
above it. I tested the code
below on a Tiny S22 ESP32-S2 board.
import board
import time
import alarm
import digitalio
import wifi
import socketpool
from secrets import secrets
def wifi_connect():
print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])
print("Available WiFi networks:")
for network in wifi.radio.start_scanning_networks():
print("\t%s\t\tRSSI: %d\tChannel: %d" % (str(network.ssid, "utf-8"),
network.rssi, network.channel))
wifi.radio.stop_scanning_networks()
print("Connecting to AP...")
wifi.radio.connect(secrets["ssid"], secrets["password"])
network = wifi.radio.ap_info
print("Connected to {} via {}, RSSI = {}".format(
network.ssid, network.authmode, network.rssi))
print("My IP address is", str(wifi.radio.ipv4_address))
def mail_open_socket():
server = secrets["smtp_server"]
print("Connecting to mail server ", server)
pool = socketpool.SocketPool(wifi.radio)
sock = pool.socket()
addr = (server, 25)
sock.connect(addr)
return sock
def mail_rxtx(s, msg):
if msg is not None:
print('> ' + msg)
s.send(msg.encode('ascii') + b'\n')
buff_size = 1024
buff = bytearray(buff_size)
s.recv_into(buff)
x = buff.decode('ascii')
print('< ' + x.rstrip())
return x
def mail_send(m_subj, m_msg):
m_from = secrets["mail_from"]
m_to = secrets["mail_to"]
s = mail_open_socket()
mail_rxtx(s, None)
mail_rxtx(s, "HELO pico")
mail_rxtx(s, "MAIL FROM:{}".format(m_from))
mail_rxtx(s, "RCPT TO:{}".format(m_to))
mail_rxtx(s, "DATA")
mail_rxtx(s, "From: {}\nTo: {}\nSubject: {}\n\n{}\n.".format(m_from, m_to, m_subj, m_msg))
print("connect to wifi")
wifi_connect()
time.sleep(sleep_time)
print("Send email")
m_subj = "Hello World!"
m_msg = "Your text goes here"
mail_send(m_subj, m_msg)
ESP32SPI
version
The main program owes much to an Adafruit socket example3, though the pins have been changed to suit a Pimoroni Wireless Pack and Raspberry Pi Pico.
import board
import busio
from digitalio import DigitalInOut
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
from adafruit_esp32spi import adafruit_esp32spi
from secrets import secrets
def rxtx(s, msg):
if msg is not None:
print('> ' + msg)
s.send(msg.encode('ascii') + b'\n')
x = s.recv(1024).decode('ascii')
print('< ' + x.rstrip())
return x
print("ESP32 SMTP Client")
esp32_cs = DigitalInOut(board.GP7)
esp32_ready = DigitalInOut(board.GP10)
esp32_reset = DigitalInOut(board.GP11)
spi = busio.SPI(board.GP18, board.GP19, board.GP16)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
while not esp.is_connected:
try:
print("Connecting to AP...")
esp.connect_AP(secrets["ssid"], secrets["password"])
except RuntimeError as e:
print("could not connect to AP, retrying: ", e)
continue
print("Connected to", str(esp.ssid, "utf-8"), "\tRSSI:", esp.rssi)
print("My IP address is", esp.pretty_ip(esp.ip_address))
socket.set_interface(esp)
socketaddr = socket.getaddrinfo(secrets["smtp_server"], 25)[0][4]
s = socket.socket()
s.settimeout(10)
print("Connecting to mail server")
s.connect(socketaddr)
m_from = secrets["mail_from"]
m_to = secrets["mail_to"]
m_subj = "Hello World!"
m_msg = "Your text goes here"
rxtx(s, None)
rxtx(s, "HELO pico")
rxtx(s, "MAIL FROM:{}".format(m_from))
rxtx(s, "RCPT TO:{}".format(m_to))
rxtx(s, "DATA")
rxtx(s, "From: {}\nTo: {}\nSubject: {}\n\n{}\n.".format(m_from, m_to, m_subj, m_msg))
Common code
Besides the usual WiFi information in the secrets.py
file, you
also need to define the mail server and a couple of addresses.
secrets = {
'ssid' : 'XXXXX',
'password' : 'XXXXXXXXXXXXXX,
'smtp_server': '192.168.1.1',
'mail_from': '<foo@wibble.com>',
'mail_to': '<bar@wibble.com>'
}
Conclusion
This email recipe won’t work if you want to use a server with access control, or if you want to send attachements. For simple tasks though it works well. It is nice to write code against an API which is forty years old4, but still works.
References
- 1. https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
- 2. https://unexpectedmaker.com/tinys2
- 3. https://github.com/adafruit/Adafruit_CircuitPython_ESP32SPI/blob/main/examples/esp32spi_tcp_client.py
- 4. https://datatracker.ietf.org/doc/html/rfc821