Digital Lutherie ESP32 Micropython - tamlablinz/learn-esp32 GitHub Wiki

Intro

This page serves as tutorial and code repository for those interested in learning how to design musical interfaces with ESP32 boards under micropython. Looking for Arduino code? Please check my other page here for a similar wiki with Arduino.

CircuitPython

In this tutorial CircuitPython will be our programming language. It is a micro version of python, or a micropython wrap with plenty of sensor and hardware libraries already included in the main package for our creative use of microcontrollers and microprocessors.

If you want to know more, visit the CircuitPython Essentials Documentation.

The documentation of the core modules is here: https://docs.circuitpython.org/en/latest/docs/index.html

The reference of each core module can be found here: https://docs.circuitpython.org/en/latest/shared-bindings/index.html#modules

Flashing and Reflashing Circuit Python

First time flashing? Your board needs to be enabled by pressing this combination of two buttons:

  • Press button "0" and do not release

  • Press button "RST" and release

  • Release button "0"

Your board will be now enabled to be flashed with Circuit Python.

If reflashing Circuit Python is needed open this page preferably with Chrome: https://circuitpython.org/downloads and look for your board (usually https://circuitpython.org/board/lolin_s2_mini/ or ). In the following sections links to particular boards we use are provided.

  • connect USB-C cable to the COM port of the ESP32-S3 or S2

  • Run the installer from the website clicking on "Open Installer"

  • In the first pop-up window, click on Connect and select your Serial Port (I selected the wchusb version)

  • Click Next and the process of erasing and flashing the board will begin. The installer first creates a boot drive called S3DKC1BOOT (or S2MINI or similar) but it can only be accessed when the USB cable is at the USB Port labelled "USB" (not at COM). When the installer asks for the S3DKC1BOOT Drive (or S2MINIBOOT) disconnect the cable to the USB Port or just unplug and plug it again, wait a few seconds, and the drive will appear in your Finder or Windows browser. Select it.

  • The installer will install the adafruit libraries and will rename the boot drive as CIRCUITPY. Please select it when a pop-up window requires it.

*Add the WIFI details if wanted and check if the board can be accessed with Thonny.

*If you are flashing many boards, the webpage sometimes needs to be reloaded to find the serial port.

IDE Installation

Install Thonny for your platform: download

Connect the board to the USB labelled port (not COM) and check:

  • you see a new mass storage folder called "CIRCUITPY" at your files browser

  • Download this ZIP file, extract it and copy its contents (and not the zip file) to the folder /CIRCUITPY/lib in the ESP32.

  • at Thonny's preferences interpreter you see the ESP32 port. Also select "CircuitPython (generic)" as your interpreter.

thonny's preferences

Other Libraries

If you are missing some module or external module, or want to install your own libraries, you can download libraries from https://circuitpython.org/libraries (also from the community bundle). You just have to copy the .mpy files to the /lib folder in the mass storage folder CIRCUITPY. You can also read a tutorial about libraries in circuit python: https://learn.adafruit.com/adafruit-pyportal/circuitpython-libraries

Boards Pinouts

We are using two boards:

  1. a ESP32-S3 WROOM-1 N8R2 with two USB-C ports. It has Wi-Fi + Bluetooth LE, 2 MB PSRAM and 8 MB SPI Flash.

  2. a ESP32-S2 WROOM with one microUSB port

  3. a ESP32-S2 with USB-C port

ESP32-S3 N8R2

It is similar to the original Espressif ESP32-S3-DevKit-1

ESP32-S3

Pinout (although this is a different board, their pinouts are the same):

RGB LED Addressable RGB LED, driven by GPIO48.
USB Port ESP32-S3 full-speed USB OTG interface, compliant with the USB 1.1 specification. The interface is used for power supply to the board, for flashing applications to the chip, for communication with the chip using USB 1.1 protocols, as well as for JTAG debugging.

Do Not Use (generally)

gpio.43 Used for USB/Serial U0TXD
gpio.44 Used for USB Serial U0RXD
gpio.19 Used for native USB D-
gpio.20 Used for native USB D+

Strapping Pins

Typically these can be used, but you need to make sure they are not in the wrong state during boot.

gpio.0 Boot Mode. Weak pullup during reset. (Boot Mode 0=Boot from Flash, 1=Download)
gpio.3 JTAG Mode. Weak pull down during reset. (JTAG Config)
gpio.45 SPI voltage. Weak pull down during reset. (SPI Voltage 0=3.3v 1=1.8v)
gpio.46 Boot mode. Weak pull down during reset. (Enabling/Disabling ROM Messages Print During Booting)

Reflashing this ESP32-S3

If reflashing is needed (only admin) open this page with chrome: https://circuitpython.org/board/espressif_esp32s3_devkitc_1_n8r2/

ESP32-S2 MINI (WEMOS, LOLIN, HEILEGE, etc)

ESP32-S2FN4R2 WiFi SoC, 4 MB Flash (embedded) and 2 MB PSRAM (embedded)

Circut Python installation: https://circuitpython.org/board/lolin_s2_mini/

ESP32-S2

ESP32- S2 board by Olimex

ESP32-S2 pinout

Circuit Python Install with https://circuitpython.org/board/lilygo_ttgo_t8_esp32_s2_wroom/

RGB LED: board.IO18

Behavior

  • boot.py (if it exists) runs only once on start up before workflows are initialized. This lays the ground work for configuring USB at startup rather than it being fixed. Since serial is not available, output is written to boot_out.txt.

  • code.py (or main.py) is run after every reload until it finishes or is interrupted. After it is done running, the vm and hardware is reinitialized.

Pure Data Examples

A few pure data examples to practice mapping can be downloaded from here: https://github.com/tamlablinz/learn-esp32/tree/master/pd-patches

If you need to install Pure Data: https://puredata.info/downloads/pure-data

Code

Specifics for ESP32

There are plenty specificities of Circuit Python for ESP32. Take a look: https://learn.adafruit.com/circuitpython-with-esp32-quick-start/

Discover the name of your pins and your available modules

This is how you will call them in CircuitPython

import board
dir(board)

The pin names available through board are not the same as the pins labelled on the microcontroller itself. The board pin names are aliases to the microcontroller pin names. If you look at the datasheet of your microcontroller, you'll likely find a pinout with a series of pin names, such as "PA18" or "GPIO5". If you want to get to the actual microcontroller pin name in CircuitPython:

import microcontroller
dir(microcontroller.pin)

Finally, get the list of available modules:

help("modules")

Blinking LED (S2 board)

Switch onboard LED (pin 15) on and off.

import time
import board
from digitalio import DigitalInOut, Direction

led = DigitalInOut(board.IO15)    #reference to pin GPIO15 in S2 mini
led.direction = Direction.OUTPUT  #use it as output

while True:
    led.value = True   #on
    time.sleep(1.5)	
    led.value = False  #off
    time.sleep(1.5)

Blinking RGB LED (only S3 board)

The RGB LED is accessible on pin 48, and it is programmable through the neopixel library. More info about neopixel usage can be found here.

import board
import neopixel
import time
from rainbowio import colorwheel

#print(board.NEOPIXEL)

led = neopixel.NeoPixel(board.NEOPIXEL, 1)  # for S3 boards
#led = neopixel.NeoPixel(board.IO18, 1) # for S2 boards only

led.brightness = 0.3

while True:
    led[0] = (255, 0, 0)
    time.sleep(0.5)
    led[0] = (0, 255, 0)
    time.sleep(0.5)
    led[0] = (0, 0, 255)
    time.sleep(0.5)

Capacitive Touch

Read and detect capacitive touch on one pin (here IO4)

# SPDX-FileCopyrightText: 2018 Kattni Rembor for Adafruit Industries
#
# SPDX-License-Identifier: MIT

#CircuitPython Essentials Capacitive Touch example
import time
import board
import touchio

touch_pad = board.IO4

touch = touchio.TouchIn(touch_pad)
touch.threshold = 20000

while True:
    print(touch.raw_value)
    if touch.value:
        print("Touched!")
        
    time.sleep(0.05)

Analog Input test

Connect a potentiometer to IO14.

import time
import board
from analogio import AnalogIn

analog_in = AnalogIn(board.IO14)

def get_voltage(pin):
    return (pin.value * 3.3) / 65536  # max voltage and digital value

while True:
    print((analog_in.value,get_voltage(analog_in)))
    time.sleep(0.1)

Connecting Buttons

import time
import board
import digitalio

#buttons definition
button1 = digitalio.DigitalInOut(board.IO13)
button1.switch_to_input(pull=digitalio.Pull.UP)

while True:
    print(button1.value)
    time.sleep(0.1)

Connect to Wifi

import ipaddress
import wifi

ssid="xxxxx"
passwd="xxxxxxx"

print('Hello World!')

for network in wifi.radio.start_scanning_networks():
    print(network, network.ssid, network.channel)
wifi.radio.stop_scanning_networks()

print("joining network...")
print(wifi.radio.connect(ssid=ssid,password=passwd))
# the above gives "ConnectionError: Unknown failure" if ssid/passwd is wrong

print("my IP addr:", wifi.radio.ipv4_address)

Create Access Point

More info on the wifi module: https://docs.circuitpython.org/en/latest/shared-bindings/wifi/index.html# or https://learn.adafruit.com/pico-w-wifi-with-circuitpython/pico-w-basic-wifi-test

# import wifi module
import wifi

# set access point credentials
ap_ssid = "myAP"
ap_password = "password123"

# You may also need to enable the wifi radio with wifi.radio.enabled(true)

# configure access point
wifi.radio.start_ap(ssid=ap_ssid, password=ap_password)

"""
start_ap arguments include: ssid, password, channel, authmode, and max_connections
"""

# print access point settings
print("Access point created with SSID: {}, password: {}".format(ap_ssid, ap_password))

# print IP address
print("My IP address is", wifi.radio.ipv4_address)

OSC communication as access point

# import wifi module
import wifi
import time
import os
import socketpool
import microosc
import board
import touchio

# set access point credentials
ap_ssid = "myAP2"
ap_password = "password123"

# configure access point
wifi.radio.start_ap(ssid=ap_ssid, password=ap_password)

# print IP address
print("AP active: ", wifi.radio.ap_active)
print("Access point IP: ", wifi.radio.ipv4_address_ap) # esp32: 192.168.4.1

#Configure OSC
socket_pool = socketpool.SocketPool(wifi.radio)
osc_client = microosc.OSCClient(socket_pool, "192.168.4.2", 5000) # connected laptop: 192.168.4.2
msg = microosc.OscMsg( "/capacitive", [0,], ("f",) )

#capacitive touch pin definition
touch_pad = board.IO4
touch = touchio.TouchIn(touch_pad)
touch.threshold = 20000


while True:
    try:
        msg.args[0] = touch.raw_value
        osc_client.send(msg)
        time.sleep(0.1)

    except Exception as e:  #in case there is no client connected
        print("Error: (no client connected)", e)
        time.sleep(0.1)





OSC communication with external router

OSC requires the library microosc (https://circuitpython-microosc.readthedocs.io/en/latest/), which can be downloaded with the Circuit Python community bundle (https://circuitpython.org/libraries). We have also included it into our particular lib.zip bundle.

"""send capacitive touch value with OSC, assumes native `wifi` support"""

import time
import os
import wifi
import socketpool

import microosc

import board
import touchio


#capacitive touch pin definition
touch_pad = board.IO4
touch = touchio.TouchIn(touch_pad)
touch.threshold = 20000

#Connect to the network
wifi.radio.connect("xxxxx","xxxxxxxx")
print("my ip address:", wifi.radio.ipv4_address)

socket_pool = socketpool.SocketPool(wifi.radio)
osc_client = microosc.OSCClient(socket_pool, "192.168.1.34", 5000)

#message definition
msg = microosc.OscMsg( "/capacitive", [0,], ("f",) ) 
#msg = microosc.OscMsg( "/capacitive", [0.99, 3, ], ("f", "i", ) ) #for more


while True:
   
    msg.args[0] = touch.raw_value
    osc_client.send(msg)
    time.sleep(0.1)
    

MIDI Communication

ESP S2 and S3 models provide USB Host capabilities. Therefore it is possible to implement the USB MIDI protocol (https://docs.circuitpython.org/en/latest/shared-bindings/usb_midi/index.html).

The general information about circuit python and USB can be found here: https://learn.adafruit.com/customizing-usb-devices-in-circuitpython/circuitpy-midi-serial

An important detail with USB MIDI is that it is only possible to configure USB devices in the boot.py file. If you try to configure or change the USB device after boot.py you will get an error. For this reason, it is mandatory to have a boot.py file (create this file if necessary) with the following:

import usb_midi
import usb_hid

usb_hid.disable()
usb_midi.enable()

Everytime you change the boot.py it is necessary to hard reset the board by presing RST button.

MIDI: sensor value to Control Change

Once you have the boot.py file as it is explained in the previous section, you can create another .py file with:

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# simple_test
# modified by enrique tomas

import time
import random
import usb_midi
import board
import touchio
import adafruit_midi
from adafruit_midi.control_change import ControlChange


#set MIDI ports
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)


# Prepare capacitive touch input
touch_pad = board.IO4
touch = touchio.TouchIn(touch_pad)

while True:
    # note how a list of messages can be used
    print(int(touch.raw_value/512))
    midi.send(ControlChange(3,int(touch.raw_value/512))) #65536 ->128
    time.sleep(0.1)

MIDI Capacitive Value with Calibration Routine

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# simple_test
# modified by enrique tomas

import time
import random
import usb_midi
import board
import touchio

import adafruit_midi
from adafruit_midi.control_change import ControlChange


#set MIDI ports
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)

# Prepare capacitive touch input
touch_pad = board.IO4
touch = touchio.TouchIn(touch_pad)



# Phase de calibration
print("Calibration : touch the pad during 5 seconds.")
min_raw = 65535
max_raw = 0

start_time = time.monotonic()
calibration_duration = 5  # durée en secondes

# Fonction pour normaliser les données
def get_normalized_value(touch):
    raw_value = touch.raw_value
    print("raw value ",raw_value)
    constrained_value = max(min_raw, min(raw_value, max_raw))
    print("normalized value ",int((constrained_value - min_raw) * 127 / (max_raw - min_raw)))
    return int((constrained_value - min_raw) * 127 / (max_raw - min_raw))

while time.monotonic() - start_time < calibration_duration:
    raw_value = touch.raw_value
    min_raw = min(min_raw, raw_value)
    max_raw = max(max_raw, raw_value)
    print(f"Calibration running... Min: {min_raw}, Max: {max_raw}")
    time.sleep(0.1)

print(f"Calibration finished. Min: {min_raw}, Max: {max_raw}")


while True:

    touch1_value = get_normalized_value(touch)
       
    print("value1", int(touch1_value/512))
    midi.send(ControlChange(3,int(touch1_value))) #65536 ->128
    time.sleep(0.1)

MIDI note on/off test

(do not forget the boot.py file)

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# simple_test
# modified by enrique tomas

import time
import random
import usb_midi

import adafruit_midi
from adafruit_midi.control_change import ControlChange
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn

#set MIDI ports
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)
print("Midi test: send a note on/off")

# Convert channel numbers at the presentation layer to the ones musicians use
print("Default output channel:", midi.out_channel + 1)
print("Listening on input channel:", midi.in_channel + 1)

while True:
    midi.send(NoteOn(44, 120))  # G sharp 2nd octave
    time.sleep(0.5)
    # note how a list of messages can be used
    midi.send(NoteOff("G#2", 120))
    time.sleep(0.5)

More information about usb_midi can be found here. More information about adafruit_midi can be found here.

Exercise 1: Program a monophonic midi instrument which triggers a random MIDI note after touching a pin



Play in loop MIDI notes from a list

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# simple_test
# modified by enrique tomas

import time
import random
import usb_midi

import adafruit_midi
from adafruit_midi.control_change import ControlChange
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn

#set MIDI ports
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)
print("Midi test: send a note on/off")

# Convert channel numbers at the presentation layer to the ones musicians use
print("Default output channel:", midi.out_channel + 1)
print("Listening on input channel:", midi.in_channel + 1)

notes_list = [60, 62, 63, 65, 67, 68, 70] #a scale 

i = 0 #index to read the list
while True:
    midi.send(NoteOn(notes_list[i], 120))  # G sharp 2nd octave
    time.sleep(0.5)
    # note how a list of messages can be used
    midi.send(NoteOff(notes_list[i], 120))
    time.sleep(0.5)
    i = (i + 1) % 7  # run from 0 to 6
    print(i)

MIDI Control Change with Capacitive Touch

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# simple_test
# modified by enrique tomas

import time
import random
import usb_midi

import board
import touchio

import adafruit_midi
from adafruit_midi.control_change import ControlChange
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn

#set MIDI ports
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)
print("Midi test: send a note on/off")

# Convert channel numbers at the presentation layer to the ones musicians use
print("Default output channel:", midi.out_channel + 1)
print("Listening on input channel:", midi.in_channel + 1)

# Prepare capacitive touch input
touch_pad = board.IO4
touch = touchio.TouchIn(touch_pad)

while True:
    # note how a list of messages can be used
    print(int(touch.raw_value/512))
    midi.send(ControlChange(3,int(touch.raw_value/512))) #65536 ->128
    time.sleep(0.05)

## Non-blocking LED blink (S2 board)

import time
import board
from digitalio import DigitalInOut, Direction

# How long we want the LED to stay on
BLINK_ON_DURATION = 0.5
# How long we want the LED to stay off
BLINK_OFF_DURATION = 0.5
# When we last changed the LED state
LAST_BLINK_TIME = -1

led = DigitalInOut(board.IO15)    #reference to pin GPIO15 in S2 mini
led.direction = Direction.OUTPUT  #use it as output

ledState = True

while True:
    # Store the current time to refer to later.
    now = time.monotonic()
    if not ledState:
        # Is it time to turn on?
        if now >= LAST_BLINK_TIME + BLINK_OFF_DURATION:
            led.value = True
            ledState = True
            LAST_BLINK_TIME = time.monotonic()
    if ledState:
        # Is it time to turn off?
        if now >= LAST_BLINK_TIME + BLINK_ON_DURATION:
            led.value = False
            ledState = False
            LAST_BLINK_TIME = time.monotonic()

## Non-blocking Blinking RGB LED

# SPDX-FileCopyrightText: 2020 FoamyGuy for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# modified by enrique tomas

"""
Using time.monotonic() to blink the built-in LED.
"""
import time
import digitalio
import board
import neopixel

# How long we want the LED to stay on
BLINK_ON_DURATION = 0.5
# How long we want the LED to stay off
BLINK_OFF_DURATION = 0.5
# When we last changed the LED state
LAST_BLINK_TIME = -1

print(board.NEOPIXEL)

led = neopixel.NeoPixel(board.NEOPIXEL, 1)
led.brightness = 0.3
ledState = True

while True:
    # Store the current time to refer to later.
    now = time.monotonic()
    if not ledState:
        # Is it time to turn on?
        if now >= LAST_BLINK_TIME + BLINK_OFF_DURATION:
            led[0] = (255, 0, 0)
            ledState = True
            LAST_BLINK_TIME = now
    if ledState:
        # Is it time to turn off?
        if now >= LAST_BLINK_TIME + BLINK_ON_DURATION:
            led[0] = (0, 0, 0)
            ledState = False
            LAST_BLINK_TIME = now

Non-blocking MIDI player

# SPDX-FileCopyrightText: 2020 FoamyGuy for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# modified by enrique tomas


import time
import usb_midi

import adafruit_midi
from adafruit_midi.control_change import ControlChange
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn

# How long we want the note to stay on and off
NOTE_ON_DURATION = 1.5
NOTE_OFF_DURATION = 1.5

# Variable to store when we last played the last note
LAST_NOTE_TIME = -1


#set MIDI ports
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)


playing = False

while True:
    # Store the current time to refer to later.
    now = time.monotonic()
    
    if not playing:
        # Is it time to Note on?
        if now >= LAST_NOTE_TIME + NOTE_OFF_DURATION:
            midi.send(NoteOn(44, 120))
            print("midi Note On")
            playing = True
            LAST_NOTE_TIME = now      
        
        
    if playing:
        # Is it time to Note off?
        if now > LAST_NOTE_TIME + NOTE_ON_DURATION:
            midi.send(NoteOff(44, 120))
            print("midi Note Off")
            playing = False
            LAST_NOTE_TIME = now



Exercise 2: Play a list of MIDI notes in loop using the non-blocking method and modify their pitch with capacitive touch



Assignment:

Program a NON-BLOCKING 16 step sequencer with three analog controllable inputs (potentiometers, capacitive touch, LDR, etc): tempo, timbre and volume AND use the RGB LED as indicator: sync blink to tempo per step. Add two buttons to start and stop the sequencer. By default the sequencer should not play at startup.

If possible -> explore more possibilities: arpegios, modulations, chords, etc. Add more and more controls.... an enclosure etc :)

Piezo Sensor Trigger with FFT

Connect a Piezo disc to your Analog Input and GND, using a 1 MOhm resistor between both pins.

import array
import board
from analogio import AnalogIn
import neopixel
import time
from ulab import numpy as np

#led 
led = neopixel.NeoPixel(board.IO18, 1) # for S2 boards only
#led = neopixel.NeoPixel(board.NEOPIXEL, 1)  # for S3 boards
led.brightness = 0.3

analog_in = AnalogIn(board.IO17) #piezo pin

fft_size = 8
#array for the piezo readings
samples_bit = array.array('H', [0] * (fft_size+3))

print("begin_________________")
# Main Loop
while True:
    #read the piezo
    for x in range(fft_size):
        samples_bit[x] = analog_in.value
            
    #put readings in numpy array and calculate FFT   
    samples = np.array(samples_bit[3:])
    real,b = np.fft.fft(samples)    
    print(real)  #print fft real part
    
    if real[0] > 5000.0 : #check over a threshold the first partial
        led[0] = (255, 0, 0)
        print("ON___________________________")
    else:
        led[0] = (0, 0, 0)
        
    # to see elapsed time
    print("time ", time.monotonic_ns())

ESPNOW

"ESPnow" is much more reliable than simple "OSC" through Wifi but it requires two ESP boards: one to transmit and another one to receive. More information here: https://docs.circuitpython.org/en/latest/shared-bindings/espnow/index.html

Transmitter code -> the message is built using a struct which has to have a match in size and combination of types to the struct expected in the receiver. In this case we transmit an int, a float and two chars (1, 1.1, "hi")

import espnow
import time
import struct

#create ESPNOW instance
e = espnow.ESPNow()
peeradd = bytearray([0x48, 0x27, 0xe2, 0x59, 0x52, 0xe0])  ## Receiver MAC Address
peer = espnow.Peer(peeradd)
e.peers.append(peer)

while True:
    # the struct we transmit in the message is:
    #if2s -> int, float, 2 x chars -> check other types here
    # https://docs.python.org/3/library/struct.html
    var = struct.pack('if2s',1,1.1,"hi")
    e.send(var)
    
    time.sleep(1)

Receiver code -> receives the message and unpacks it with the known types of the struct (it has to match the sender struct)

import espnow
import wifi
import struct
import usb_midi

import adafruit_midi
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn

#set MIDI ports
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)
print("Midi test: send a note on/off")

print("ESP NOW receiver running ")
print("Receiver MAC addr:", [hex(i) for i in wifi.radio.mac_address]) 

e = espnow.ESPNow()

while True:
    if e:
        packet = e.read()
        var = packet.msg
        # the struct we expect in the message is:
        #if2s -> int, float, 2 x chars -> check other types here
        # https://docs.python.org/3/library/struct.html
        s = struct.unpack('if2s', var) 
        print("received: ", s, "from :", packet.mac)
        midi.send(NoteOn(44, 120))  # G sharp 2nd octave
        time.sleep(0.5)
        # note how a list of messages can be used
        midi.send(NoteOff("G#2", 120))
        time.sleep(0.5)


Waveforms

Simple sawtooth to the DAC1 output (s2 mini)

import board
from analogio import AnalogOut

analog_out = AnalogOut(board.IO17)

while True:
    # Count up from 0 to 65535, with 64 increment
    # which ends up corresponding to the DAC's 10-bit range
    for i in range(0, 65535, 64):
        analog_out.value = i

Having fun with chiptunes:

import board
from analogio import AnalogOut

analog_out = AnalogOut(board.IO17)

while True:
    # Count up from 0 to 65535, with 64 increment
    # which ends up corresponding to the DAC's 10-bit range
    for t in range(0,20):
        for i in range(0, 65535, 512):
            analog_out.value = i
    for t in range(0,20):
        for i in range(0, 65535, 256):
            analog_out.value = i
    for t in range(0,20):
        for i in range(0, 65535, 512):
            analog_out.value = i

A synth: https://learn.adafruit.com/cpx-midi-controller/basic-synthesizer

More sensors

MPU-6050 Accel and Gyro

Copy the adafruit_mpu6050 library to the lib folder.Connect the sensor to 3V, GND, SCL and SDA. Most ESP32s allow using any pin as SCL and SDL. In this case we use SCL->IO9 and SDA->IO8.

If you want to know more about how circuit python deals with I2C you can visit: https://docs.circuitpython.org/en/latest/shared-bindings/busio/#busio.I2C and https://learn.adafruit.com/circuitpython-basics-i2c-and-spi/i2c-devices

import time
import busio
import board
import adafruit_mpu6050

i2c = busio.I2C(board.IO9, board.IO8)

mpu = adafruit_mpu6050.MPU6050(i2c)

while True:
    print("Acceleration: X:%.2f, Y: %.2f, Z: %.2f m/s^2" % (mpu.acceleration))
    print("Gyro X:%.2f, Y: %.2f, Z: %.2f rad/s" % (mpu.gyro))
    print("Temperature: %.2f C" % mpu.temperature)
    print("")
    time.sleep(1)

calculate inclination angles between X and Y

import time
import busio
import board
from math import atan2, degrees
import adafruit_mpu6050

i2c = busio.I2C(board.IO9, board.IO8)

sensor = adafruit_mpu6050.MPU6050(i2c)

def vector_2_degrees(x, y,l=False):
    if l:
        angle = degrees(atan2(y, x))
    else:
        angle = degrees(atan2(y, x))
    if angle < 0:
        angle += 360
    return angle

def get_inclination(_sensor):
    x, y, z = _sensor.acceleration
    return vector_2_degrees(x, z,True), vector_2_degrees(y, z), int(x),int(y),int(z)


x,y,z=0,0,0


while True:
    
    turn, ac,x,y,z = get_inclination(sensor)
    turn=int(turn)
    ac=int(ac)
    
    print(turn,ac,x,y,z,sep=" : ")
    time.sleep(0.1)

Orientation in MIDI

import time
import busio
import board
from math import atan2, degrees
import adafruit_mpu6050

import usb_midi
import board
import adafruit_midi
from adafruit_midi.control_change import ControlChange

#set MIDI ports
print(usb_midi.ports)
midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)

i2c = busio.I2C(board.IO9, board.IO8)

sensor = adafruit_mpu6050.MPU6050(i2c)

def vector_2_degrees(x, y,l=False):
    if l:
        angle = degrees(atan2(y, x))
    else:
        angle = degrees(atan2(y, x))
    if angle < 0:
        angle += 360
    return angle

def get_inclination(_sensor):
    x, y, z = _sensor.acceleration
    return vector_2_degrees(x, z,True), vector_2_degrees(y, z), int(x),int(y),int(z)


x,y,z=0,0,0


while True:
    
    turn, ac,x,y,z = get_inclination(sensor)
    turn=int(turn/360*128)
    ac=int(ac)
    
    print(turn,ac,x,y,z,sep=" : ")
    midi.send(ControlChange(3,int(turn))) #65536 ->128
    time.sleep(0.1)

VL53LOx distance sensor

Copy the adafruit_vl53l0x library to the lib folder. Connect the sensor to 3V, GND, SCL and SDA. Most ESP32s allow using any pin as SCL and SDL. In this case we use SCL->IO9 and SDA->IO8.

More information about this library can be found here: https://github.com/adafruit/Adafruit_CircuitPython_VL53L0X


import time

import board
import busio

import adafruit_vl53l0x

# Initialize I2C bus and sensor.
i2c = busio.I2C(board.IO9, board.IO8)
vl53 = adafruit_vl53l0x.VL53L0X(i2c)

# Optionally adjust the measurement timing budget to change speed and accuracy.
# See the example here for more details:
#   https://github.com/pololu/vl53l0x-arduino/blob/master/examples/Single/Single.ino
# For example a higher speed but less accurate timing budget of 20ms:
# vl53.measurement_timing_budget = 20000
# Or a slower but more accurate timing budget of 200ms:
# vl53.measurement_timing_budget = 200000
# The default timing budget is 33ms, a good compromise of speed and accuracy.

# Main loop will read the range and print it every second.
while True:
    print("Range: {0}mm".format(vl53.range))
    time.sleep(1.0)

Website Reading

import time
import ipaddress
import wifi
import socketpool
import ssl
import adafruit_requests
import adafruit_requests as requests

ssid="xxxxx"
passwd="xxxxxx"

print('Hello World!')

for network in wifi.radio.start_scanning_networks():
    print(network, network.ssid, network.channel)
wifi.radio.stop_scanning_networks()

print("joining network...")
print(wifi.radio.connect(ssid=ssid,password=passwd))
# the above gives "ConnectionError: Unknown failure" if ssid/passwd is wrong

print("my IP addr:", wifi.radio.ipv4_address)

print("pinging 1.1.1.1...")
ip1 = ipaddress.ip_address("1.1.1.1")
print("ip1:",ip1)
print("ping:", wifi.radio.ping(ip1))


pool = socketpool.SocketPool(wifi.radio)
request = adafruit_requests.Session(pool, ssl.create_default_context())

print("Fetching wifitest.adafruit.com...");
response = request.get("http://wifitest.adafruit.com/testwifi/index.html")
print(response.status_code)
print(response.text)

print("Fetching http://www.elpais.com");
response = request.get("http://www.elpais.com")
print(response.status_code)
#print(response.json())
print(response.text[:2500])

Internet of Things

Connect to Adafruit IO via MQTT

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT

import os
import time
import ssl
import socketpool
import wifi
import adafruit_minimqtt.adafruit_minimqtt as MQTT

# Add settings.toml to your filesystem CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD keys
# with your WiFi credentials. DO NOT share that file or commit it into Git or other
# source control.

# Set your Adafruit IO Username, Key and Port in settings.toml
# (visit io.adafruit.com if you need to create an account,
# or if you need your Adafruit IO key.)
aio_username = os.getenv("aio_username")
aio_key = os.getenv("aio_key")

print(f"Connecting to {os.getenv('CIRCUITPY_WIFI_SSID')}")
wifi.radio.connect(
    os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}!")
### Feeds ###

# Setup a feed named 'photocell' for publishing to a feed
photocell_feed = aio_username + "/feeds/photocell"

# Setup a feed named 'onoff' for subscribing to changes
onoff_feed = aio_username + "/feeds/onoff"

### Code ###


# Define callback methods which are called when events occur
# pylint: disable=unused-argument, redefined-outer-name
def connected(client, userdata, flags, rc):
    # This function will be called when the client is connected
    # successfully to the broker.
    print(f"Connected to Adafruit IO! Listening for topic changes on {onoff_feed}")
    # Subscribe to all changes on the onoff_feed.
    client.subscribe(onoff_feed)


def disconnected(client, userdata, rc):
    # This method is called when the client is disconnected
    print("Disconnected from Adafruit IO!")


def message(client, topic, message):
    # This method is called when a topic the client is subscribed to
    # has a new message.
    print(f"New message on topic {topic}: {message}")


# Create a socket pool
pool = socketpool.SocketPool(wifi.radio)
ssl_context = ssl.create_default_context()

# If you need to use certificate/key pair authentication (e.g. X.509), you can load them in the
# ssl context by uncommenting the lines below and adding the following keys to the "secrets"
# dictionary in your secrets.py file:
# "device_cert_path" - Path to the Device Certificate
# "device_key_path" - Path to the RSA Private Key
# ssl_context.load_cert_chain(
#     certfile=secrets["device_cert_path"], keyfile=secrets["device_key_path"]
# )

# Set up a MiniMQTT Client
mqtt_client = MQTT.MQTT(
    broker="io.adafruit.com",
    port=1883,
    username=aio_username,
    password=aio_key,
    socket_pool=pool,
    ssl_context=ssl_context,
)

# Setup the callback methods above
mqtt_client.on_connect = connected
mqtt_client.on_disconnect = disconnected
mqtt_client.on_message = message

# Connect the client to the MQTT broker.
print("Connecting to Adafruit IO...")
mqtt_client.connect()

photocell_val = 0
while True:
    # Poll the message queue
    mqtt_client.loop()

    # Send a new message
    print(f"Sending photocell value: {photocell_val}...")
    mqtt_client.publish(photocell_feed, photocell_val)
    print("Sent!")
    photocell_val += 10
    time.sleep(5)

Stepper Motor Control

Use a 28BYJ-48 stepper motor (2048 steps, max 50RPM) with an ESP32 and ULN2003 driver

import time
import board
import digitalio

## Define PINS
pin1 = digitalio.DigitalInOut(board.IO21)
pin2 = digitalio.DigitalInOut(board.IO18)
pin3 = digitalio.DigitalInOut(board.IO16)
pin4 = digitalio.DigitalInOut(board.IO17)
pin1.direction = digitalio.Direction.OUTPUT
pin2.direction = digitalio.Direction.OUTPUT
pin3.direction = digitalio.Direction.OUTPUT
pin4.direction = digitalio.Direction.OUTPUT

#Define steps per rotation
stepsperrot = 2048

button1 = digitalio.DigitalInOut(board.IO4)


def statefromsteppin(pin, step):
    #   step 1 2 3 4
    # pin 1  1 1 0 0
    # pin 2  0 1 1 0
    # pin 3  0 0 1 1
    # pin 4  1 0 0 1
    #
    Offset = [1, 0, 3, 2]
    Timing = [0, 1, 1, 0]
    TPOS = ((step - 1) % 4 + Offset[pin - 1]) % 4
    if Timing[TPOS] == 1:
        return True
    else:
        return False

def rotatestepsatrpm(steps, rpm):
    if steps >= 1:
        for i in range(1, steps + 1):
            firepins(i)
            time.sleep(60 / (rpm * stepsperrot))
    if steps <= 1:
        for i in range(-1, steps - 1, -1):
            firepins(i)
            time.sleep(60 / (rpm * stepsperrot))

def firepins(nextstep):
    print("Step:{}".format(nextstep))
    pin1.value = statefromsteppin(1, nextstep)
    pin2.value = statefromsteppin(2, nextstep)
    pin3.value = statefromsteppin(3, nextstep)
    pin4.value = statefromsteppin(4, nextstep)
    
# Rotate Stepper 1 full turn in each direction, pausing 30 sec between directions
forwardRPM=40
backwardsRPM=30

rotatestepsatrpm(stepsperrot, forwardRPM)
time.sleep(2)
rotatestepsatrpm(-1 * stepsperrot, backwardsRPM)
time.sleep(2)