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.