This is a simple IoT toggle switch demo application for Zephyr on the Adafruit Feather TFT ESP32-S3 board. It works with Adafruit IO.
Features:
- LVGL graphics for network status and toggle switch widget
- MQTT networking over Wifi
- TLSv1.2 with "DigiCert Global Root G2" CA cert for Adafruit IO
- Provisioning over USB serial (credentials saved to NVM flash)
- Boot button starts network connection
- Boot button controls MQTT toggle switch once connected
To prepare for building this project:
-
To run Zephyr's
west
commandline tool, you need to set up a Zephyr project, including a Python virtual environment and the zephyr git repo. In my examples, the zephyr project directory is~/code/zephyr-workspace
. -
To build for ESP32-S3, you need to install the Zephyr SDK including the
xtensa-espressif_esp32s3_zephyr-elf
toolchain. The basic getting started guide instructions install all the toolchains. You only need to pay attention to specific toolchains if you want to do a custom install to save disk space and bandwidth. -
The first time you run
west flash
on a board that had CircuitPython installed, you may need to activate the board's ESP32-S3 built in bootloader using the button sequence: hold BOOT, press and release RESET, release BOOT. Once you have installed the Zephyr bootloader,west flash
should work without needing to press any buttons. -
To build with wifi support, you will need to fetch the
hal_espressif
blobs if you have not already done so:west blobs fetch hal_espressif
-
Clone this repository into your Zephyr project directory. For example:
cd ~/code/zephyr-workspace git clone https://github.com/samblenny/zphqst-03.git
To learn more about setting up a Zephyr project directory, you can read the Zephyr Project Getting Started Guide.
The directions here were written and tested for a terminal shell on Debian 12 Linux. Probably they will work about the same on recent versions of Ubuntu. For other operating systems, you may need to adapt the instructions to suit your local setup.
To get started, you need to activate your Python venv that contains west
and
change into the zphqst-03
directory within your Zephyr project directory.
For example:
$ cd ~/code/zephyr-workspace
$ source .venv/bin/activate
(.venv) $ cd zphqst-03
The examples below use make
in a terminal on Debian 12 to run commands for
make targets defined in the zphqst-03 Makefile. Using make
avoids a lot of typing that would otherwise be required to provide commandline
options to west
.
To build and flash the IoT toggle switch app:
make clean
make app
make flash
When you first install the app, in order to connect to the network, you must
first provision the board with Wifi and MQTT login credentials. To do that,
you connect by USB serial to the Zephyr shell and use the settings
command.
Depending on how you've used your Feather TFT board previously, it might already have data written to the settings partition used by Zephyr's Non-Volatile Storage (NVS) subsystem. In that case, you'll need to erase the partition before writing the settings.
You can access the Zephyr shell with the command:
make monitor
The make monitor
command runs west espressif monitor
(keyboard shortcut to
exit the serial monitor is Ctrl+]). If you prefer a different serial
monitor program, like tio
or screen
, you could try one of these:
tio /dev/ttyACM0
screen -fn /dev/ttyACM0 115200
Once you are in the Zephyr shell, you should see the uart:~$
prompt. If not,
press the Enter key on your keyboard a time or two. At the prompt, to
provision your network credentials, you can write values to NVM flash using the
settings
shell command provided by Zephyr's
Settings
subsystem.
The Zephyr Settings subsystem is a key-value store. For this application, I've configured it to use the Non-Volatile Storage (NVS) subsystem for persistent storage. These three settings keys store the network provisioning details for Wifi and MQTT:
Key | Description |
---|---|
zq3/ssid | Wifi ssid (use quotes if it has spaces) |
zq3/psk | Wifi WPA2-PSK passphrase (use quotes if it has spaces) |
zq3/url | MQTT broker url: mqtt[s]://<user>:<pass>@<hostname>/<topic> |
Here is an example provisioning for a private test network with a local MQTT broker listening on port 1883 of 192.168.0.100, with no encryption and anonymous connections enabled (username and password can be blank):
uart:~$ settings write string zq3/ssid MySSID
uart:~$ settings write string zq3/psk "my wifi passphrase"
uart:~$ settings write string zq3/url mqtt://:@192.168.0.100/test
This second example is for an authenticated TLSv1.2 connection to Adafruit IO.
Note how there's an "s" in mqtts://
, the username is 'User', the API key is
key
, and the topic is User/f/test
:
uart:~$ settings write string zq3/ssid MySSID
uart:~$ settings write string zq3/psk "my wifi passphrase"
uart:~$ settings write string zq3/url mqtts://User:[email protected]/User/f/test
If you try writing the settings and get an error, check the section below about erasing the NVM flash partition.
Once you have written the settings, you need to load them from NVM flash. You
can do this by either resetting the board (settings are loaded at boot) or by
running the aio reload
Zephyr shell command.
Once the settings are loaded, you should see a "Press BOOT button to connect" message on the Feather TFT's screen.
Press the Feather TFT's Boot button. You should see a "Connecting..." message. Once wifi is up, the wifi icon in the statusbar (top right) should turn from gray to green. When MQTT connects, you should see a large toggle switch widget in the center of the screen.
If there are Wifi or MQTT connection errors, you should see an error message on the Feather TFT's screen. To troubleshoot the problem, it's best to connect to the serial shell so you can see more detailed error messages.
Troubleshooting Checklist:
-
Is your Wifi router working? Can you connect to it with another device?
-
Does your Wifi router use WPA2-PSK? If you need to use a different type of authentication or encryption, you will need to change the code as it is hardcoded for WPA2-PSK.
-
Does your Wifi use a captive portal? In that case, it won't work.
-
Are the wifi SSID and PSK passphrase settings correct? You can check this in the serial shell with:
uart:~$ settings read zq3/ssid 00000000: 4d 79 53 53 49 44 00 |MySSID. | uart:~$ settings read zq3/psk 00000000: 6d 79 20 77 69 66 69 20 70 61 73 73 70 68 72 61 |my wifi passphra| 00000010: 73 65 00 |se. |
-
Is your MQTT broker working? You can check this with the
mosquitto_pub
andmosquitto_sub
MQTT command line tools for Linux. (check the following sections for more details on using mosquitto as a local MQTT broker). -
Is the MQTT settings URL correct? The URL format is meant to match the format of
mosquitto_pub -L <URL> ...
andmosquitto_sub -L <URL> ...
(except this app always requires the:
and@
). -
Is your account being throttled by the MQTT broker because of exceeding rate limits? For example, if you use Adafruit IO, you can read about throttling in the Adafruit IO MQTT API web docs.
The settings API uses the NVM backend with the storage
partition that is
defined as one of Espressif's default partitions in
zephyr/dts/common/espressif/partitions_0x0_amp_4M.dtsi
. My app's
Devicetree configuration gives this partition the label of "settings".
Depending on how you used your Feather TFT ESP32-S3 board previously, there may be existing data in the storage partition. In that case, you might need to erase it.
One option to erase the NVM partition would be to erase all the flash with the
Adafruit ESPTool
ESP32 web flasher tool, then re-program the bootloader and firmware using
west flash
.
You could also use Zephyr's flash_map list
shell command to find the
partition labeled "settings" and determine its start offset and size. In the
example below, those are 0x3b0000 and 0x30000. Then, you could use the
flash erase
shell command to erase the flash blocks for that partition.
For example:
uart:~$ flash_map list
ID | Device | Device Name | Label | Offset | Size
----------------------------------------------------------------------------------
0 0x3c0a91e8 flash-controller@60002000 mcuboot 0x0 0x20000
1 0x3c0a91e8 flash-controller@60002000 image-0 0x20000 0x150000
2 0x3c0a91e8 flash-controller@60002000 image-1 0x170000 0x150000
3 0x3c0a91e8 flash-controller@60002000 image-0-appcpu 0x2c0000 0x70000
4 0x3c0a91e8 flash-controller@60002000 image-1-appcpu 0x330000 0x70000
5 0x3c0a91e8 flash-controller@60002000 image-0-lpcore 0x3a0000 0x8000
6 0x3c0a91e8 flash-controller@60002000 image-1-lpcore 0x3a8000 0x8000
7 0x3c0a91e8 flash-controller@60002000 settings 0x3b0000 0x30000
8 0x3c0a91e8 flash-controller@60002000 image-scratch 0x3e0000 0x1f000
9 0x3c0a91e8 flash-controller@60002000 coredump 0x3ff000 0x1000
uart:~$
uart:~$ flash erase flash-controller@60002000 0x3b0000 0x30000
Erase success.
uart:~$ settings list
uart:~$
If you want to explore the various flash related shell commands, you can try reading their help messages:
uart:~$ flash_map -h
uart:~$ flash -h
uart:~$ settings -h
To see the Kconfig options that enable those shell commands, check out app/prj.conf.
Mosquitto is the name of an open source project that provides an MQTT broker and MQTT client programs to publish and subscribe from the command line.
For developing an MQTT application on Zephyr, it can be helpful to run your own MQTT broker locally on a private network (Raspberry Pi, Debian box, or whatever).
Writing the Zephyr code for an MQTT client application involves many steps. Attempting to do all of that at once is challenging. You might find it easier to start with basic unencrypted (non-TLS) MQTT connections on a private network, then incrementally add encryption and authentication support.
CAUTION: Using this type of configuration on public wifi is not safe. In the example here, I'm using a private Ethernet LAN (no internet gateway) with a dedicated Wifi AP that I use for testing.
This is how I set up a Debian box with the mosquitto
MQTT broker for
unencrypted and unauthenticated access on my wifi test network:
Install packages and configure the mosquitto server for manual start:
$ sudo apt install mosquitto mosquitto-clients
$ sudo systemctl stop mosquitto
$ sudo systemctl disable mosquitto
Check the IP address assigned to my wifi interface with hostname -I
. In this
case, I have a IP 10.0.x.x address for the private test LAN and a 192.168.0.x
address for the main internet connected Wifi router:
$ hostname -I
10.0.0.10 192.168.0.100
Reconfigure mosquitto
MQTT broker to listen only on the private LAN:
$ cat <<EOF | sudo tee /etc/mosquitto/conf.d/LAN-listener.conf
persistence false
allow_anonymous true
listener 1883 10.0.0.10
EOF
$ sudo systemctl start mosquitto
On Debian, if you install the mosquitto-clents
package, you can publish and
subscribe to MQTT topics from the command line. For example, assuming you were
running an MQTT broker listening on IP address 192.168.0.100, you could start
two terminal windows (or use tmux
), and do this:
Terminal 1 (use Ctrl-C to disconnect mosquitto_sub
when done):
$ mosquitto_sub --debug -v -L mqtt://192.168.0.100:1883/test
Client (null) sending CONNECT
Client (null) received CONNACK (0)
Client (null) sending SUBSCRIBE (Mid: 1, Topic: test, QoS: 0, Options: 0x00)
Client (null) received SUBACK
Subscribed (mid: 1): 0
Client (null) received PUBLISH (d0, q0, r0, m0, 'test', ... (11 bytes))
test hello world
^CClient (null) sending DISCONNECT
Terminal 2:
$ mosquitto_pub --debug -L mqtt://192.168.0.100:1883/test -m "hello world"
Client (null) sending CONNECT
Client (null) received CONNACK (0)
Client (null) sending PUBLISH (d0, q0, r0, m1, 'test', ... (11 bytes))
Client (null) sending DISCONNECT
If you wanted to test an authenticated and TLS encrypted connection to the
Adafruit IO MQTT broker, you could do something like this (replacing $USER
and $KEY
with your AIO username and API key):
Terminal 1:
$ mosquitto_sub -L mqtts://$USER:[email protected]:8883/$USER/f/test
Terminal 2:
$ mosquitto_pub -L mqtts://$USER:[email protected]:8883/$USER/f/test -m 1
To upgrade your local mosquitto MQTT broker to TLS, first you need to make
sure you have the openssl
command line tool installed. Try openssl version
from a terminal. If that doesn't work, you can install openssl with:
sudo apt install openssl
Once you have openssl, you'll need to make a CA certificate and private key, then use those to sign a server key, then install those in the mosquitto configuration directory.
-
Change to a temporary working directory
mkdir ~/ca-cert cd ~/ca-cert
-
Create a Certificate Authority (CA) private key and certificate file
openssl req -newkey rsa:2048 -noenc -x509 -days 730 -extensions v3_ca \ -subj "/C=US/O=MyCA/CN=My Self-Signed CA" -keyout ca.key -out ca.crt
CAUTION: The mbed TLS certificate parser appears to care about the contents of the subject fields and perhaps the validity period. If your connection closes mysteriously and you see a
-0x2180
error code from the mbed TLS debug logging, it may be unhappy with the subject or days. -
Create a server private key and Certificate Signing Request (CSR). The stuff with
$(hostname -I|awk '{print $1}')
is a way to automatically fill in the first hostname reported byhostname -I
. You could just type in10.0.0.10
or whatever instead if you wanted.openssl req -newkey rsa:2048 -noenc \ -subj "/CN=$(hostname -I|awk '{print $1}')" \ -keyout server.key -out server.csr
-
Use your CA's certificate and private key to sign the CSR
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -copy_extensions copy -days 365 -out server.crt
-
Check contents of the PEM files
openssl x509 -in ca.crt -text -noout openssl req -in server.csr -text -noout openssl x509 -in server.crt -text -noout
-
Move the files into the /etc/mosquitto configuration directory and change their permissions. The point of this is to make the certificates publicly visible so you can use them with
mosquitto_pub
, etc. while the private keys can only be used root or the mosquitto server.cd ~/ca-certs chmod 600 ca.* server.* sudo mv ca.* server.* /etc/mosquitto/ca_certificates/ cd /etc/mosquitto/ca_certificates/ sudo chown root:root ca.* server.* sudo chmod 600 ca.* server.* sudo chmod 644 *.crt sudo chown root:mosquitto server.key sudo chmod 640 server.key
The end result should have permissions that look like this:
$ ls -l /etc/mosquitto/ca_certificates/ total 28 -rw-r--r-- 1 root root 1135 Mar 22 12:00 ca.crt -rw------- 1 root root 1704 Mar 22 12:00 ca.key -rw------- 1 root root 41 Mar 22 12:00 ca.srl -rw-r--r-- 1 root root 73 Sep 30 2023 README -rw-r--r-- 1 root root 1131 Mar 22 12:00 server.crt -rw------- 1 root root 944 Mar 22 12:00 server.csr -rw-r----- 1 root mosquitto 1704 Mar 22 12:00 server.key
-
Modify mosquitto config to use TLS (the
$(hostname -I|awk '{print $1}')
thing evaluates to the first IP address returned byhostname -I
, in the case of this example, that would be10.0.0.10
)cat <<EOF | sudo tee /etc/mosquitto/conf.d/LAN-listener.conf # To read the log, do: sudo tail -f /var/log/mosquitto/mosquitto.log log_type all per_listener_settings true listener 1883 $(hostname -I|awk '{print $1}') allow_anonymous true persistence false listener 8883 $(hostname -I|awk '{print $1}') allow_anonymous true persistence false certfile /etc/mosquitto/ca_certificates/server.crt keyfile /etc/mosquitto/ca_certificates/server.key EOF
-
Restart mosquitto
sudo systemctl restart mosquitto
If all that worked, you can use mosquitto_sub
and mosquito_pub
over TLS by
changing to -L mqtts://
(note the "s") and adding a --cafile ...
option.
For example to start a subscriber:
mosquitto_sub --cafile /etc/mosquitto/ca_certificates/ca.crt \
-L mqtts://10.0.3.17/test
And to publish:
mosquitto_pub --cafile /etc/mosquitto/ca_certificates/ca.crt \
-L mqtts://10.0.3.17/test -m 1
To check the Adafruit IO certificate chain with the openssl
command line tool
from a terminal shell on Debian 12:
openssl s_client -showcerts -connect io.adafruit.com:8883
The output is long, but the main relevant points are:
- First cert:
O = Adafruit Industries LLC, CN = *.adafruit.com
witha:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
andNotAfter: Aug 2 23:59:59 2025 GMT
- Second cert:
OU = www.digicert.com, CN = GeoTrust TLS RSA CA G1
witha:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
andNotAfter: Nov 2 12:23:37 2027 GMT
- Third cert:
OU = www.digicert.com, CN = DigiCert Global Root G2
- Negotiated connection used
TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
To match the signature and stream ciphers used by the openssl check, mbed TLS would need to be configured to support:
- Signature:
RSA-SHA256
with 2048-bit RSA key - Cipher:
TLS_AES_256_GCM_SHA384
- TLS Version: TLSv1.3
- CA Cert:
DigiCert Global Root G2
or perhapsGeoTrust TLS RSA CA G1
Pull request boards: adafruit: add initial support for esp32s3 feather # 75844 of the zephyr-rtos/zephyr GitHub repo was merged on March 14, 2025, adding official support for the Adafruit Feather ESP32-S3 and Feather TFT ESP32-S3 boards. I hadn't previously noticed that PR or the related issue, Add Support for Adafruit ESP32-S3 and ESP32-S2 Feather Boards # 68512.
After reviewing the new board defs, I decided to continue using my board def in this repo, mainly because it configures the ST7789V display driver a bit differently (hardware rotation, gamma curve, etc).
Files in this repo use a mix of Apache-2.0 and/or MIT licensing. Check SPDX header comments for details.