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.
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:
-
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.
-
a ESP32-S2 WROOM with one microUSB port
-
a ESP32-S2 with USB-C port
ESP32-S3 N8R2
It is similar to the original Espressif ESP32-S3-DevKit-1
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
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)