circuitpython-tricks
Some CircuitPython tricks, mostly reminders to myself
git clone https://github.com/todbot/circuitpython-tricks.gittodbot/circuitpython-trickscircuitpython-tricks
A small list of tips & tricks I find myself needing when working with CircuitPython. I find these examples useful when picking up a new project and I just want some boilerplate to get started. Also see the circuitpython-tricks/larger-tricks directory for additional ideas.
An older version of this page is a Learn Guide on Adafruit too!
If you're new to CircuitPython overall, there's no single reference, but:
- The Python Tutorial on Python.org, since "CircuitPython is Python" mostly. (approx. Python 3.4)
- CircuitPython API reference, particularly the "Core Modules > Modules" section in the left sidebar
- for compiled-in libraries like
displayio,usb,audioio,ulab.numpy
- for compiled-in libraries like
- Pure-Python libraries in Adafruit Library Bundle for drivers & helpers libraries like
board,neopixel&ble - and CircuitPython Essentials Learn Guide of course
Table of Contents
But it's probably easiest to do a Cmd-F/Ctrl-F find on keyword of idea you want.
- Inputs
- Outputs
- Neopixels / Dotstars
- Audio
- USB
- USB Serial
- USB Keyboard & Mouse
- USB MIDI
- WiFi / Networking
- Scan for WiFi Networks, sorted by signal strength
- Join WiFi network with highest signal strength
- Ping an IP address
- Get IP address of remote host
- Fetch a JSON file
- Serve a webpage via HTTP
- Set RTC time from NTP
- Set RTC time from time service
- What the heck is settings.toml?
- What the heck is secrets.py?
- Displays (LCD / OLED / E-Ink) and displayio
- I2C
- Timing
- Board Info
- Computery Tasks
- Coding Techniques
- System error handling
- Using the REPL
- Python tricks
- Python info
- Host-side tasks
- About this guide
Inputs
Read a digital input as a Button
import board from digitalio import DigitalInOut, Pull button = DigitalInOut(board.D3) # defaults to input button.pull = Pull.UP # turn on internal pull-up resistor print(button.value) # False == pressed
Can also do:
import time, board, digitalio
button = digitalio.DigitalInOut(board.D3)
button.switch_to_input(digitalio.Pull.UP)
while True:
print("button pressed:", button.value == False) # False == pressed
time.sleep(0.1)
But you probably want to use keypad to get debouncing and press/release events.
You can use it for a single button!
import board, keypad
keys = keypad.Keys((board.D3,), value_when_pressed=False, pull=True)
while True:
if key := keys.events.get():
if key.pressed:
print("pressed key!")
Note: be sure to add the comma when using a single button (e.g. (board.D3,))
Read a Potentiometer
import board import analogio potknob = analogio.AnalogIn(board.A1) position = potknob.value # ranges from 0-65535 pos = potknob.value // 256 # make 0-255 range
Note: While AnalogIn.value is 16-bit (0-65535) corresponding to 0 V to 3.3V,
the MCU ADCs can have limitations in resolution and voltage range.
This reduces what CircuitPython sees.
For example, the ESP32 ADCs are 12-bit w/ approx 0.1 V to 2.5 V range
(e.g. value goes from around 200 to 50,000, in steps of 16)
Read a Touch Pin / Capsense
import touchio
import board
touch_pin = touchio.TouchIn(board.GP6)
# on Pico / RP2040, need 1M pull-down on each input
if touch_pin.value:
print("touched!")
You can also get an "analog" touch value with touch_pin.raw_value to do
basic proximity detection or even theremin-like behavior.
Read a Rotary Encoder
import board import rotaryio encoder = rotaryio.IncrementalEncoder(board.GP0, board.GP1) # must be consecutive on Pico print(encoder.position) # starts at zero, goes neg or pos
Debounce a pin / button
But you probably want to use keypad to get debouncing and press/release events.
You can use it for a single button!
import board, keypad
keys = keypad.Keys((board.D3,), value_when_pressed=False, pull=True)
while True:
if key := keys.events.get():
if key.pressed:
print("pressed key!", key.key_number)
if key.released:
print("released key!", key.key_number)
Note: be sure to add the comma when using a single button (e.g. (board.D3,))
If your board doesn't have keypad, you can use adafruit_debouncer from
the bundle.
import board
from digitalio import DigitalInOut, Pull
from adafruit_debouncer import Debouncer
button_in = DigitalInOut(board.D3) # defaults to input
button_in.pull = Pull.UP # turn on internal pull-up resistor
button = Debouncer(button_in)
while True:
button.update()
if button.fell:
print("press!")
if button.rose:
print("release!")
Note: Most boards have the native keypad module that can do keypad debouncing in a much more
efficient way. See Set up and debounce a list of pins
Detect button double-click
import board
from digitalio import DigitalInOut, Pull
from adafruit_debouncer import Button
button_in = DigitalInOut(board.D3) # defaults to input
button_in.switch_to_input(Pull.UP) # turn on internal pull-up resistor
button = Button(button_in)
while True:
button.update()
if button.pressed:
print("press!")
if button.released:
print("release!")
if button.short_count > 1: # detect multi-click
print("multi-click: click count:", button.short_count)
Set up and debounce a list of pins
If your board's CircuitPython has the keypad library (most do),
then I recommend using it. It's not just for key matrixes! And it's more efficient
and, since it's built-in, reduces a library dependency.
import board
import keypad
button_pins = (board.GP0, board.GP1, board.GP2, board.GP3, board.GP4)
buttons = keypad.Keys(button_pins, value_when_pressed=False, pull=True)
while True:
button = buttons.events.get() # see if there are any key events
if button: # there are events!
if button.pressed:
print("button", button.key_number, "pressed!")
if button.released:
print("button", button.key_number, "released!")
Otherwise, you can use adafruit_debouncer:
import board
from digitalio import DigitalInOut, Pull
from adafruit_debouncer import Debouncer
button_pins = (board.GP0, board.GP1, board.GP2, board.GP3, board.GP4)
buttons = [] # will hold list of Debouncer objects
for pin in button_pins: # set up each pin
tmp_pin = DigitalInOut(pin) # defaults to input
tmp_pin.pull = Pull.UP # turn on internal pull-up resistor
buttons.append( Debouncer(tmp_pin) )
while True:
for i in range(len(buttons)):
buttons[i].update()
if buttons[i].fell:
print("button",i,"pressed!")
if buttons[i].rose:
print("button",i,"released!")
And you can use adafruit_debouncer on touch pins too:
import board, touchio, adafruit_debouncer
touchpad = adafruit_debouncer.Debouncer(touchio.TouchIn(board.GP1))
while True:
touchpad.update()
if touchpad.rose: print("touched!")
if touchpad.fell: print("released!")
Outputs
Output HIGH / LOW on a pin (like an LED)
import board import digitalio ledpin = digitalio.DigitalInOut(board.D2) ledpin.direction = digitalio.Direction.OUTPUT ledpin.value = True
Can also do:
ledpin = digitalio.DigitalInOut(board.D2) ledpin.switch_to_output(value=True)
Output Analog value on a DAC pin
Different boards have DAC on different pins
import board import analogio dac = analogio.AnalogOut(board.A0) # on Trinket M0 & QT Py dac.value = 32768 # mid-point of 0-65535
Output a "Analog" value on a PWM pin
import board import pwmio out1 = pwmio.PWMOut(board.MOSI, frequency=25000, duty_cycle=0) out1.duty_cycle = 32768 # mid-point 0-65535 = 50 % duty-cycle
Control Neopixel / WS2812 LEDs
import neopixel leds = neopixel.NeoPixel(board.NEOPIXEL, 16, brightness=0.2) leds[0] = 0xff00ff # first LED of 16 defined leds[0] = (255,0,255) # equivalent leds.fill( 0x00ff00 ) # set all to green
Control a servo, with animation list
# servo_animation_code.py -- show simple servo animation list
import time, random, board
from pwmio import PWMOut
from adafruit_motor import servo
# your servo will likely have different min_pulse & max_pulse settings
servoA = servo.Servo(PWMOut(board.RX, frequency=50), min_pulse=500, max_pulse=2250)
# the animation to play
animation = (
# (angle, time to stay at that angle)
(0, 2.0),
(90, 2.0),
(120, 2.0),
(180, 2.0)
)
ani_pos = 0 # where in list to start our animation
while True:
angle, secs = animation[ ani_pos ]
print("servo moving to", angle, secs)
servoA.angle = angle
time.sleep( secs )
ani_pos = (ani_pos + 1) % len(animation) # go to next, loop if at end
Neopixels / Dotstars
Light each LED in order
You can access each LED with Python array methods on the leds object.
And you can set the LED color with either an RGB tuple ((255,0,80)) or an
RGB hex color as a 24-bit number (0xff0050)
import time, board, neopixel
led_pin = board.GP5 # which pin the LED strip is on
num_leds = 10
colors = ( (255,0,0), (0,255,0), (0,0,255), 0xffffff, 0x000000 )
leds = neopixel.NeoPixel(led_pin, num_leds, brightness=0.1)
i = 0
while True:
print("led:",i)
for c in colors:
leds[i] = c
time.sleep(0.2)
i = (i+1) % num_leds
Moving rainbow on built-in board.NEOPIXEL
In CircuitPython 7, the rainbowio module has a colorwheel() function.
Unfortunately, the rainbowio module is not available in all builds.
In CircuitPython 6, colorwheel() is a built-in function part of _pixelbuf or adafruit_pypixelbuf.
The colorwheel() function takes a single value 0-255 hue and returns an (R,G,B) tuple
given a single 0-255 hue. It's not a full HSV_to_RGB() function but often all you need
is "hue to RGB", wher you assume saturation=255 and value=255.
It can be used with neopixel, adafruit_dotstar, or any place you need a (R,G,B) 3-byte tuple.
Here's one way to use it.
# CircuitPython 7 with or without rainbowio module
import time, board, neopixel
try:
from rainbowio import colorwheel
except:
def colorwheel(pos):
if pos < 0 or pos > 255: return (0, 0, 0)
if pos < 85: return (255 - pos * 3, pos * 3, 0)
if pos < 170: pos -= 85; return (0, 255 - pos * 3, pos * 3)
pos -= 170; return (pos * 3, 0, 255 - pos * 3)
led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.4)
while True:
led.fill( colorwheel((time.monotonic()*50)%255) )
time.sleep(0.05)
Make moving rainbow gradient across LED strip
import time, board, neopixel, rainbowio
num_leds = 16
leds = neopixel.NeoPixel(board.D2, num_leds, brightness=0.4, auto_write=False )
delta_hue = 256//num_leds
speed = 10 # higher numbers = faster rainbow spinning
i=0
while True:
for l in range(len(leds)):
leds[l] = rainbowio.colorwheel( int(i*speed + l * delta_hue) % 255 )
leds.show() # only write to LEDs after updating them all
i = (i+1) % 255
time.sleep(0.05)
A shorter version using a Python list comprehension. The leds[:] trick is a way to assign
a new list of colors to all the LEDs at once.
import supervisor, board, neopixel, rainbowio num_leds = 16 speed = 10 # lower is faster, higher is slower leds = neopixel.NeoPixel(board.D2, 16, brightness=0.4) while True: t = supervisor.ticks_ms() / speed leds[:] = [rainbowio.colorwheel( t + i*(255/len(leds)) ) for i in range(len(leds))]
Fade all LEDs by amount for chase effects
import time import board, neopixel num_leds = 16 leds = neopixel.NeoPixel(board.D2, num_leds, brightness=0.4, auto_write=False ) my_color = (55,200,230) dim_by = 20 # dim amount, higher = shorter tails pos = 0 while True: leds[pos] = my_color leds[:] = [[max(i-dim_by,0) for i in l] for l in leds] # dim all by (dim_by,dim_by,dim_by) pos = (pos+1) % num_leds # move to next position leds.show() # only write to LEDs after updating them all time.sleep(0.05)
Audio
If you're used to Arduino, making sound was mostly constrained to simple beeps
using the Arduino tone() function. You can do that in CircuitPython too with
pwmio and simpleio, but CircuitPython can also play WAV and MP3
files and become a fully-fledged audio synthesizer with synthio.
In CircuitPython, there are multiple core module libraries available to output audio:
pwmio-- use almost any GPIO pin to output simple beeps, no WAV/MP3/synthioaudioio-- uses built-in DAC to output WAV, MP3, synthioaudiopwmio-- like above, but uses PWM like arduinoanalogWrite(), requires RC filter to convert to analogaudiobusio-- outputs high-quality I2S audio data stream, requires external I2S decoder hardware
Different devices will have different audio modules available. Generally, the pattern is:
- SAMD51 (e.g. "M4" boards) --
audioio(DAC) andaudiobusio(I2S) - RP2040 (e.g. Pico) --
audiopwmio(PWM) andaudiobusio(I2S) - ESP32 (e.g. QTPy ESP32) --
audiobusio(I2S) only
To play WAV and MP3 files, they usually must be resaved in a format parsable by CircuitPython, see Preparing Audio Files for CircuitPython
Making simple tones
For devices that only have pwmio capability, you can make simple tones.
The simpleio library can be used for this:
# a short piezo song using tone()
import time, board, simpleio
while True:
for f in (262, 294, 330, 349, 392, 440, 494, 523):
simpleio.tone(board.A0, f, 0.25)
time.sleep(1)
Play a WAV file
WAV files are easiest for CircuitPython to play. The shortest code to play a WAV file on Pico RP2040 is:
import time, board, audiocore, audiopwmio
audio = audiopwmio.PWMAudioOut(board.GP0)
wave = audiocore.WaveFile("laser2.wav")
audio.play(wave)
while True:
pass # wait for audio to finish playing
Details and other ways below.
Audio out using PWM
This uses the audiopwmio library, only available for RP2040 boards like Raspberry Pi Pico and NRF52840-based boards like Adafruit Feather nRF52840 Express.
On RP2040-based boards, any pin can be PWM Audio pin.
See the audiopwomio Support Matrix for which boards support audiopwmio.
import time, board
from audiocore import WaveFile
from audiopwmio import PWMAudioOut as AudioOut
wave = WaveFile("laser2.wav") # can also be filehandle from open()
audio = AudioOut(board.GP0) # must be PWM-capable pin
while True:
print("audio is playing:",audio.playing)
if not audio.playing:
audio.play(wave)
wave.sample_rate = int(wave.sample_rate * 0.90) # play 10% slower each time
time.sleep(0.1)
Notes:
-
There will be a small pop when audio starts playing as the PWM driver takes the GPIO line from not being driven to being PWM'ed. There's currently no way around this. If playing multiple WAVs, consider using
AudioMixerto keep the audio system running between WAVs. This way, you'll only have the startup pop. -
If you want stereo output on boards that support it then you can pass in two pins, like:
audio = audiopwmio.PWMAudioOut(left_channel=board.GP14, right_channel=board.GP15) -
PWM output must be filtered and converted to line-level to be usable. Use an RC circuit to accomplish this, see this simple circuit or this twitter thread for details.
-
The
WaveFile()object can take either a filestream (the output ofopen('filewav','rb')) or can take a string filename (wav=WaveFile("laser2.wav")).
Audio out using DAC
Some CircuitPython boards (SAMD51 "M4" & SAMD21 "M0") have built-in DACs that are supported.
The code is the same as above, with just the import line changing.
See the audioio Support Matrix for which boards support audioio.
import time, board
import audiocore, audioio # DAC
wave_file = open("laser2.wav", "rb")
wave = audiocore.WaveFile(wave_file)
audio = audioio.AudioOut(board.A0) # must be DAC-capable pin, A0 on QTPy Haxpress
while True:
print("audio is playing:",audio.playing)
if not audio.playing:
audio.play(wave)
wave.sample_rate = int(wave.sample_rate * 0.90) # play 10% slower each time
time.sleep(0.1)
Note: if you want stereo output on boards that support it (SAMD51 "M4" mostly),
then you can pass in two pins, like:
audio = audioio.AudioOut(left_channel=board.A0, right_channel=board.A1)
Audio out using I2S
Unlike PWM or DAC, most CircuitPython boards support driving an external I2S audio board.
This will also give you higher-quality sound output than DAC or PWM.
See the audiobusio Support Matrix for which boards support audiobusio.
# for e.g. Pico RP2040 pins bit_clock & word_select pins must be adjacent
import board, audiobusio, audiocore
audio = audiobusio.I2SOut(bit_clock=board.GP0, word_select=board.GP1, data=board.GP2)
audio.play( audiocore.WaveFile("laser2.wav") )
Use audiomixer to prevent audio crackles
The default buffer used by the audio system is quite small.
This means you'll hear corrupted audio if CircuitPython is doing anything else
(having CIRCUITPY written to, updating a display). To get around this, you can
use audiomixer to make the audio buffer larger. Try buffer_size=2048 to start.
A larger buffer means a longer lag between when a sound is triggered when its heard.
AudioMixer is also great if you want to play multiple WAV files at the same time.
import time, board
from audiocore import WaveFile
from audioio import AudioOut
import audiomixer
wave = WaveFile("laser2.wav", "rb")
audio = AudioOut(board.A0) # assuming QTPy M0 or Itsy M4
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=2048)
audio.play(mixer) # never touch "audio" after this, use "mixer"
while True:
print("mixer voice is playing:", mixer.voice[0].playing)
if not mixer.voice[0].playing:
time.sleep(1)
print("playing again")
mixer.voice[0].play(wave)
time.sleep(0.1)
Play multiple sounds with audiomixer
This example assumes WAVs that are mono 22050 Hz sample rate, w/ signed 16-bit samples.
import time, board, audiocore, audiomixer
from audiopwmio import PWMAudioOut as AudioOut
wav_files = ("loop1.wav", "loop2.wav", "loop3.wav")
wavs = [None] * len(wav_files) # holds the loaded WAVs
audio = AudioOut(board.GP2) # RP2040 example
mixer = audiomixer.Mixer(voice_count=len(wav_files), sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=2048)
audio.play(mixer) # attach mixer to audio playback
for i in range(len(wav_files)):
print("i:",i)
wavs[i] = audiocore.WaveFile(open(wav_files[i], "rb"))
mixer.voice[i].play( wavs[i], loop=True) # start each one playing
while True:
print("doing something else while all loops play")
time.sleep(1)
Note: M0 boards do not have audiomixer
Note: Number of simultaneous sounds is limited sample rate and flash read speed. Rules of thumb:
- Built-in flash: 10 22kHz sounds simultanously
- SPI SD cards: 2 22kHz sounds simultaneously
Also see the many examples in larger-tricks.
Playing MP3 files
Once you have set up audio output (either directly or via AudioMixer), you can play WAVs or MP3s through it, or play both simultaneously.
For instance, here's an example that uses an I2SOut to a PCM5102 on a Raspberry Pi Pico RP2040 to simultaneously play both a WAV and an MP3:
import board, audiobusio, audiocore, audiomp3
num_voices = 2
i2s_bclk, i2s_wsel, i2s_data = board.GP9, board.GP10, board.GP11 # BCLK, LCLK, DIN on PCM5102
audio = audiobusio.I2SOut(bit_clock=i2s_bclk, word_select=i2s_wsel, data=i2s_data)
mixer = audiomixer.Mixer(voice_count=num_voices, sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True)
audio.play(mixer) # attach mixer to audio playback
wav_file = "/amen1_22k_s16.wav" # in 'circuitpython-tricks/larger-tricks/breakbeat_wavs'
mp3_file = "/vocalchops476663_22k_128k.mp3" # in 'circuitpython-tricks/larger-tricks/wav'
# https://freesound.org/people/f-r-a-g-i-l-e/sounds/476663/
wave = audiocore.WaveFile(open(wav_file, "rb"))
mp3 = audiomp3.MP3Decoder(open(mp3_file, "rb"))
mixer.voice[0].play( wave )
mixer.voice[1].play( mp3 )
while True:
pass # both audio files play
Note: For MP3 files, be aware that since this is doing software MP3 decoding, you will likely need to re-encode the MP3s to lower bitrate and sample rate (max 128 kbps and 22,050 Hz) to be playable the lower-end CircuitPython devices like the Pico / RP2040.
Note: For MP3 files and setting loop=True when playing, there is a small delay
when looping. WAV files loop seemlessly.
An example of boards with pwmio but no audio are ESP32-S2-based boards like
FunHouse,
where you cannot play WAV files, but you can make beeps.
A larger example is this gist: https://gist.github.com/todbot/f35bb5ceed013a277688b2ca333244d5
USB
Rename CIRCUITPY drive to something new
For instance, if you have multiple of the same device.
The label can be up to 11 characters.
This goes in boot.py not code.py and you must powercycle board.
# this goes in boot.py not code.py!
new_name = "TRINKEYPY0"
import storage
storage.remount("/", readonly=False)
m = storage.getmount("/")
m.label = new_name
storage.remount("/", readonly=True)
Detect if USB is connected or not
import supervisor if supervisor.runtime.usb_connected: led.value = True # USB else: led.value = False # no USB
An older way that tries to mount CIRCUITPY read-write and if it fails, USB connected:
def is_usb_connected():
import storage
try:
storage.remount('/', readonly=False) # attempt to mount readwrite
storage.remount('/', readonly=True) # attempt to mount readonly
except RuntimeError as e:
return True
return False
is_usb = "USB" if is_usb_connected() else "NO USB"
print("USB:", is_usb)
Get CIRCUITPY disk size and free space
import os
fs_stat = os.statvfs('/')
print("Disk size in MB", fs_stat[0] * fs_stat[2] / 1024 / 1024)
print("Free space in MB", fs_stat[0] * fs_stat[3] / 1024 / 1024)
Programmatically reset to UF2 bootloader
import microcontroller microcontroller.on_next_reset(microcontroller.RunMode.UF2) microcontroller.reset()
Note: in older CircuitPython use RunMode.BOOTLOADER and for boards with multiple
bootloaders (like ESP32-S2):
import microcontroller microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER) microcontroller.reset()
USB Serial
Print to USB Serial
print("hello there") # prints a newline
print("waiting...", end='') # does not print newline
for i in range(256): print(i, end=', ') # comma-separated numbers
Read user input from USB Serial, blocking
while True:
print("Type something: ", end='')
my_str = input() # type and press ENTER or RETURN
print("You entered: ", my_str)
Read user input from USB Serial, non-blocking (mostly)
import time
import supervisor
print("Type something when you're ready")
last_time = time.monotonic()
while True:
if supervisor.runtime.serial_bytes_available:
my_str = input()
print("You entered:", my_str)
if time.monotonic() - last_time > 1: # every second, print
last_time = time.monotonic()
print(int(last_time),"waiting...")
Read keys from USB Serial
import time, sys, supervisor
print("type charactcers")
while True:
n = supervisor.runtime.serial_bytes_available
if n > 0: # we read something!
s = sys.stdin.read(n) # actually read it in
# print both text & hex version of recv'd chars (see control chars!)
print("got:", " ".join("{:s} {:02x}".format(c,ord(c)) for c in s))
time.sleep(0.01) # do something else
Read user input from USB serial, non-blocking
class USBSerialReader:
""" Read a line from USB Serial (up to end_char), non-blocking, with optional echo """
def __init__(self):
self.s = ''
def read(self,end_char='\n', echo=True):
import sys, supervisor
n = supervisor.runtime.serial_bytes_available
if n > 0: # we got bytes!
s = sys.stdin.read(n) # actually read it in
if echo: sys.stdout.write(s) # echo back to human
self.s = self.s + s # keep building the string up
if s.endswith(end_char): # got our end_char!
rstr = self.s # save for return
self.s = '' # reset str to beginning
return rstr
return None # no end_char yet
usb_reader = USBSerialReader()
print("type something and press the end_char")
while True:
mystr = usb_reader.read() # read until newline, echo back chars
#mystr = usb_reader.read(end_char='\t', echo=False) # trigger on tab, no echo
if mystr:
print("got:",mystr)
time.sleep(0.01) # do something time critical
USB Keyboard & Mouse
CircuitPython comes set up to be a USB keyboard and mouse.
Many more details in CircuitPython Essentials but the basics are below. The adafruit_hid library needs
to be installed from the the bundle.
I do circup install adafruit_hid in a terminal.
Sending keystrokes and mouse moves
import time
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.mouse import Mouse
kbd = Keyboard(usb_hid.devices)
mouse = Mouse(usb_hid.devices)
while True:
time.sleep(1)
print("moving right")
kbd.send(Keycode.A) # types "a" (sends press() & release_all())
mouse.move(x=50, y=0) # moves mouse slightly right
time.sleep(1)
print("moving left")
kbd.send(Keycode.B) # types "b" (sends press() & release_all())
mouse.move(x=-50, y=0) # moves mouse slightly left
time.sleep(1)
USB MIDI
CircuitPython can be a MIDI controller, or respond to MIDI!
Adafruit provides an adafruit_midi
class to make things easier, but it's rather complex for how simple MIDI actually is.
For outputting MIDI, you can opt to deal with raw bytearrays, since most MIDI messages
are just 1,2, or 3 bytes long. For reading MIDI,
you may find TMIDI or
SmolMIDI to be faster
to parse MIDI messages, since by design it does less.
Sending MIDI with adafruit_midi
import usb_midi
import adafruit_midi
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
midi_out_channel = 3 # human version of MIDI out channel (1-16)
midi = adafruit_midi.MIDI( midi_out=usb_midi.ports[1], out_channel=midi_out_channel-1)
def play_note(note,velocity=127):
midi.send(NoteOn(note, velocity)) # 127 = highest velocity
time.sleep(0.1)
midi.send(NoteOff(note, 0)) # 0 = lowest velocity
Note: This pattern works for sending serial (5-pin) MIDI too, see below
Sending MIDI with bytearray
Sending MIDI with a lower-level bytearray is also pretty easy and
could gain some speed for timing-sensitive applications.
This code is equivalent to the above, without adafruit_midi
import usb_midi
midi_out = usb_midi.ports[1]
midi_out_channel = 3 # MIDI out channel (1-16)
note_on_status = (0x90 | (midi_out_channel-1))
note_off_status = (0x80 | (midi_out_channel-1))
def play_note(note,velocity=127):
midi_out.write( bytearray([note_on_status, note, velocity]) )
time.sleep(0.1)
midi_out.write( bytearray([note_off_status, note, 0]) )
MIDI over Serial UART
Not exactly USB, but it is MIDI!
Both adafruit_midi and the bytearray technique works for Serial MIDI (aka "5-pin MIDI") too.
With a simple MIDI out circuit
you can control old hardware synths.
import busio
midi_out_channel = 3 # MIDI out channel (1-16)
note_on_status = (0x90 | (midi_out_channel-1))
note_off_status = (0x80 | (midi_out_channel-1))
# must pick board pins that are UART TX and RX pins
midi_uart = busio.UART(tx=board.GP16, rx=board.GP17, baudrate=31250)
def play_note(note,velocity=127):
midi_uart.write( bytearray([note_on_status, note, velocity]) )
time.sleep(0.1)
midi_uart.write( bytearray([note_off_status, note, 0]) )
Receiving MIDI
import usb_midi # built-in library
import adafruit_midi # install with 'circup install adafruit_midi'
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
midi_usb = adafruit_midi.MIDI(midi_in=usb_midi.ports[0])
while True:
msg = midi_usb.receive()
if msg:
if isinstance(msg, NoteOn):
print("usb noteOn:",msg.note, msg.velocity)
elif isinstance(msg, NoteOff):
print("usb noteOff:",msg.note, msg.velocity)
Note with adafruit_midi you must import each kind of MIDI Message you want to handle.
Receiving MIDI USB and MIDI Serial UART together
MIDI is MIDI, so you can use either the midi_uart or the usb_midi.ports[] created above with adafruit_midi.
Here's an example receiving MIDI from both USB and Serial on a QTPy RP2040.
Note for receiving serial MIDI, you need an appropriate optoisolator input circuit,
like this one for QTPys
or this one for MacroPad RP2040.
import board, busio
import usb_midi # built-in library
import adafruit_midi # install with 'circup install adafruit_midi'
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
uart = busio.UART(tx=board.TX, rx=board.RX, baudrate=31250, timeout=0.001)
midi_usb = adafruit_midi.MIDI( midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1] )
midi_serial = adafruit_midi.MIDI( midi_in=uart, midi_out=uart )
while True:
msg = midi_usb.receive()
if msg:
if isinstance(msg, NoteOn):
print("usb noteOn:",msg.note, msg.velocity)
elif isinstance(msg, NoteOff):
print("usb noteOff:",msg.note, msg.velocity)
msg = midi_serial.receive()
if msg:
if isinstance(msg, NoteOn):
print("serial noteOn:",msg.note, msg.velocity)
elif isinstance(msg, NoteOff):
print("serial noteOff:",msg.note, msg.velocity)
If you don't care about the source of the MIDI messages, you can combine
the two if blocks using the "walrus operator" (:=)
while True:
while msg := midi_usb.receive() or midi_uart.receive():
if isinstance(msg, NoteOn) and msg.velocity != 0:
note_on(msg.note, msg.velocity)
elif isinstance(msg,NoteOff) or isinstance(msg,NoteOn) and msg.velocity==0:
note_off(msg.note, msg.velocity)
Enable USB MIDI in boot.py (for ESP32-S2 and STM32F4)
Some CircuitPython devices like ESP32-S2 based ones, do not have enough
USB endpoints to enable all USB functions, so USB MIDI is disabled by default.
To enable it, the easiest is to disable USB HID (keyboard/mouse) support.
This must be done in boot.py and the board power cycled.
# boot.py
import usb_hid
import usb_midi
usb_hid.disable()
usb_midi.enable()
print("enabled USB MIDI, disabled USB HID")
WiFi / Networking
Scan for WiFi Networks, sorted by signal strength
Note: this is for boards with native WiFi (ESP32)
import wifi
networks = []
for network in wifi.radio.start_scanning_networks():
networks.append(network)
wifi.radio.stop_scanning_networks()
networks = sorted(networks, key=lambda net: net.rssi, reverse=True)
for network in networks:
print("ssid:",network.ssid, "rssi:",network.rssi)
Join WiFi network with highest signal strength
import wifi
def join_best_network(good_networks, print_info=False):
"""join best network based on signal strength of scanned nets"""
networks = []
for network in wifi.radio.start_scanning_networks():
networks.append(network)
wifi.radio.stop_scanning_networks()
networks = sorted(networks, key=lambda net: net.rssi, reverse=True)
for network in networks:
if print_info: print("network:",network.ssid)
if network.ssid in good_networks:
if print_info: print("connecting to WiFi:", network.ssid)
try:
wifi.radio.connect(network.ssid, good_networks[network.ssid])
return True
except ConnectionError as e:
if print_info: print("connect error:",e)
return False
good_networks = {"todbot1":"FiOnTheFly", # ssid, password
"todbot2":"WhyFlyWiFi",}
connected = join_best_network(good_networks, print_info=True)
if connected:
print("connected!")
Ping an IP address
Note: this is for boards with native WiFi (ESP32)
import os
import time
import wifi
import ipaddress
ip_to_ping = "1.1.1.1"
wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'),
password=os.getenv('CIRCUITPY_WIFI_PASSWORD'))
print("my IP addr:", wifi.radio.ipv4_address)
print("pinging ",ip_to_ping)
ip1 = ipaddress.ip_address(ip_to_ping)
while True:
print("ping:", wifi.radio.ping(ip1))
time.sleep(1)
Get IP address of remote host
import os, wifi, socketpool
wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'),
password=os.getenv('CIRCUITPY_WIFI_PASSWORD'))
print("my IP addr:", wifi.radio.ipv4_address)
hostname = "todbot.com"
pool = socketpool.SocketPool(wifi.radio)
addrinfo = pool.getaddrinfo(host=hostname, port=443) # port is required
print("addrinfo", addrinfo)
ipaddr = addrinfo[0][4][0]
print(f"'{hostname}' ip address is '{ipaddr}'")
Fetch a JSON file
Note: this is for boards with native WiFi (ESP32)
import os
import time
import wifi
import socketpool
import ssl
import adafruit_requests
wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'),
password=os.getenv('CIRCUITPY_WIFI_PASSWORD'))
print("my IP addr:", wifi.radio.ipv4_address)
pool = socketpool.SocketPool(wifi.radio)
session = adafruit_requests.Session(pool, ssl.create_default_context())
while True:
response = session.get("https://todbot.com/tst/randcolor.php")
data = response.json()
print("data:",data)
time.sleep(5)
Serve a webpage via HTTP
Note: this is for boards with native WiFi (ESP32)
The adafruit_httpserver library
makes this pretty easy, and has good examples. You can tell it to either server.serve_forver()
and do all your computation in your @server.route() functions, or use server.poll() inside a while-loop.
There is also the Ampule library.
# based on https://docs.circuitpython.org/projects/httpserver/en/latest/starting_methods.html import socketpool import wifi from adafruit_httpserver import Request, Response, Server WIFI_SSID="..." WIFI_PASSWORD="..." MY_PORT=
more like this
RaspPi-Pico-Examples-Fedora
Compiling Raspberry Pi Pico RP2040 / Pico2 RP2350 C/C++ programs on Fedora