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:
displayio
, usb
, audioio
, ulab.numpy
board
, neopixel
& ble
But it's probably easiest to do a Cmd-F/Ctrl-F find on keyword of idea you want.
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,)
)
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)
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.
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
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
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!")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-65535Output 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-cycleControl 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 greenControl 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
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_ledsMoving 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)
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 arduino analogWrite()
, requires RC filter to convert to analogaudiobusio
-- outputs high-quality I2S audio data stream, requires external I2S decoder hardwareDifferent devices will have different audio modules available. Generally, the pattern is:
audioio
(DAC) and audiobusio
(I2S)audiopwmio
(PWM) and audiobusio
(I2S)audiobusio
(I2S) onlyTo play WAV and MP3 files, they usually must be resaved in a format parsable by CircuitPython, see Preparing Audio Files for CircuitPython
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)
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.
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 AudioMixer
to 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 of open('filewav','rb')
) or can take a string filename (wav=WaveFile("laser2.wav")
).
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)
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:
Also see the many examples in larger-tricks.
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
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()
print("hello there") # prints a newline print("waiting...", end='') # does not print newline for i in range(256): print(i, end=', ') # comma-separated numbersRead 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 elseRead 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
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 bytearray
s, since most MIDI messages are just 1,2, or 3 bytes long. For reading MIDI, you may find Winterbloom's SmolMIDI to be faster to parse MIDI messages, since by design it does less.
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 bytearraySending 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]) )
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]) )
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.
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")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!")
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}'")
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)
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.
import time, os, wifi, socketpool from adafruit_httpserver.server import HTTPServer from adafruit_httpserver.response import HTTPResponse my_port = 1234 # set this to your liking wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) server = HTTPServer(socketpool.SocketPool(wifi.radio)) @server.route("/") # magic that attaches this function to "server" object def base(request): my_str = f"<html><body><h1> Hello! Current time.monotonic is {time.monotonic()}</h1></body></html>" return HTTPResponse(body=my_str, content_type="text/html") # or for static content: return HTTPResponse(filename="/index.html") print(f"Listening on http://{wifi.radio.ipv4_address}:{my_port}") server.serve_forever(str(wifi.radio.ipv4_address), port=my_port) # never returns
Note: this is for boards with native WiFi (ESP32)
Note: You need to set my_tz_offset
to match your region
# copied from: # https://docs.circuitpython.org/projects/ntp/en/latest/examples.html import time, os, rtc import socketpool, wifi import adafruit_ntp my_tz_offset = -7 # PDT wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) print("Connected, getting NTP time") pool = socketpool.SocketPool(wifi.radio) ntp = adafruit_ntp.NTP(pool, tz_offset=my_tz_offset) rtc.RTC().datetime = ntp.datetime while True: print("current datetime:", time.localtime()) time.sleep(5)Set RTC time from time service
Note: this is for boards with native WiFi (ESP32)
This uses the awesome and free WorldTimeAPI.org site, and this example will fetch the current local time (including timezone and UTC offset) based on the geolocated IP address of your device.
import time, os, rtc import wifi, ssl, socketpool import adafruit_requests wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) print("Connected, getting WorldTimeAPI time") pool = socketpool.SocketPool(wifi.radio) request = adafruit_requests.Session(pool, ssl.create_default_context()) print("Getting current time:") response = request.get("http://worldtimeapi.org/api/ip") time_data = response.json() tz_hour_offset = int(time_data['utc_offset'][0:3]) tz_min_offset = int(time_data['utc_offset'][4:6]) if (tz_hour_offset < 0): tz_min_offset *= -1 unixtime = int(time_data['unixtime'] + (tz_hour_offset * 60 * 60)) + (tz_min_offset * 60) print(time_data) print("URL time: ", response.headers['date']) rtc.RTC().datetime = time.localtime( unixtime ) # create time struct and set RTC with it while True: print("current datetime: ", time.localtime()) # time.* now reflects current local time time.sleep(5)
Also see this more concise version from @deilers78.
What the heck issettings.toml
?
It's a config file that lives next to your code.py
and is used to store WiFi credentials and other global settings. It is also used (invisibly) by many Adafruit libraries that do WiFi. You can use it (as in the examples above) without those libraries. The settings names used by CircuitPython are documented in CircuitPython Web Workflow.
Note: You can use any variable names for your WiFI credentials (a common pair is WIFI_SSID
and WIFI_PASSWORD
), but if you use the CIRCUITPY_WIFI_*
names that will also start up the Web Workflow
You use it like this for basic WiFi connectivity:
# settings.toml CIRCUITPY_WIFI_SSID = "PrettyFlyForAWiFi" CIRCUITPY_WIFI_PASSWORD = "mysecretpassword" # code.py import os, wifi print("connecting...") wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) print("my IP addr:", wifi.radio.ipv4_address)What the heck is
secrets.py
?
It's an older version of the settings.toml
idea. You may see older code that uses it.
displayio is the native system-level driver for displays in CircuitPython. Several CircuitPython boards (FunHouse, MagTag, PyGamer, CLUE) have displayio
-based displays and a built-in board.DISPLAY
object that is preconfigured for that display. Or, you can add your own I2C or SPI display.
Boards like FunHouse, MagTag, PyGamer, CLUE have built-in displays. display.rotation
works with all displays, not just built-in ones.
import board display = board.DISPLAY print(display.rotation) # print current rotation display.rotation = 0 # valid values 0,90,180,270
Using displayio.OnDiskBitmap
CircuitPython has a built-in BMP parser called displayio.OnDiskBitmap
: The images should be in non-compressed, paletized BMP3 format. (how to make BMP3 images)
import board, displayio display = board.DISPLAY maingroup = displayio.Group() # everything goes in maingroup display.root_group = maingroup # show our maingroup (clears the screen) bitmap = displayio.OnDiskBitmap(open("my_image.bmp", "rb")) image = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader) maingroup.append(image) # shows the image
Using adafruit_imageload
You can also use the adafruit_imageload
library that supports slightly more kinds of BMP files, (but should still be paletized BMP3 format as well as paletized PNG and GIF files. Which file format to choose?
import board, displayio import adafruit_imageload display = board.DISPLAY maingroup = displayio.Group() # everything goes in maingroup display.root_group = maingroup # set the root group to display bitmap, palette = adafruit_imageload.load("my_image.png") image = displayio.TileGrid(bitmap, pixel_shader=palette) maingroup.append(image) # shows the image
How displayio
is structured
CircuitPython's displayio
library works like:
Bitmap
(and its Palette
) goes inside a TileGrid
TileGrid
goes inside a Group
Group
is shown on a Display
.Useful for display a solid background color that can be quickly changed.
import time, board, displayio display = board.DISPLAY # get default display (FunHouse,Pygamer,etc) maingroup = displayio.Group() # Create a main group to hold everything display.root_group = maingroup # put it on the display # make bitmap that spans entire display, with 3 colors background = displayio.Bitmap(display.width, display.height, 3) # make a 3 color palette to match mypal = displayio.Palette(3) mypal[0] = 0x000000 # set colors (black) mypal[1] = 0x999900 # dark yellow mypal[2] = 0x009999 # dark cyan # Put background into main group, using palette to map palette ids to colors maingroup.append(displayio.TileGrid(background, pixel_shader=mypal)) time.sleep(2) background.fill(2) # change background to dark cyan (mypal[2]) time.sleep(2) background.fill(1) # change background to dark yellow (mypal[1])
Another way is to use vectorio
:
import board, displayio, vectorio display = board.DISPLAY # built-in display maingroup = displayio.Group() # a main group that holds everything display.root_group = maingroup # put maingroup on the display mypal = displayio.Palette(1) mypal[0] = 0x999900 background = vectorio.Rectangle(pixel_shader=mypal, width=display.width, height=display.height, x=0, y=0) maingroup.append(background)
Or can also use adafruit_display_shapes
:
import board, displayio from adafruit_display_shapes.rect import Rect display = board.DISPLAY maingroup = displayio.Group() # a main group that holds everything display.root_group = maingroup # add it to display background = Rect(0,0, display.width, display.height, fill=0x000000 ) # background color maingroup.append(background)
import time, board, displayio import adafruit_imageload display = board.DISPLAY # get display object (built-in on some boards) screen = displayio.Group() # main group that holds all on-screen content display.root_group = screen # add it to display file_names = [ '/images/cat1.bmp', '/images/cat2.bmp' ] # list of filenames screen.append(displayio.Group()) # placeholder, will be replaced w/ screen[0] below while True: for fname in file_names: image, palette = adafruit_imageload.load(fname) screen[0] = displayio.TileGrid(image, pixel_shader=palette) time.sleep(1)
Note: Images must be in palettized BMP3 format. For more details, see Preparing images for CircuitPython
Dealing with E-Ink "Refresh Too Soon" errorE-Ink displays are damaged if refreshed too frequently. CircuitPython enforces this, but also provides display.time_to_refresh
, the number of seconds you need to wait before the display can be refreshed. One solution is to sleep a little longer than that and you'll never get the error. Another would be to wait for time_to_refresh
to go to zero, as show below.
import time, board, displayio, terminalio from adafruit_display_text import label mylabel = label.Label(terminalio.FONT, text="demo", x=20,y=20, background_color=0x000000, color=0xffffff ) display = board.DISPLAY # e.g. for MagTag display.root_group = mylabel while True: if display.time_to_refresh == 0: display.refresh() mylabel.text = str(time.monotonic()) time.sleep(0.1)Turn off REPL on built-in display
If you have a board with a built-in display (like Feather TFT, Cardputer, FunHouse, etc), CircuitPython will set up the display for you and print the REPL to it. But if you want a more polished look for your project, you can turn off the REPL from printing on the built-in display by putting this at at the top of both your boot.py
and code.py
# put at top of both boot.py & code.py import board board.DISPLAY.root_group = None
from: CircuitPython I2C Guide: Find Your Sensor
import board i2c = board.I2C() # or busio.I2C(pin_scl,pin_sda) while not i2c.try_lock(): pass print("I2C addresses found:", [hex(device_address) for device_address in i2c.scan()]) i2c.unlock()
One liner to copy-n-paste into REPL for quicky I2C scan:
import board; i2c=board.I2C(); i2c.try_lock(); [hex(a) for a in i2c.scan()]; i2c.unlock()
CircuitPython defaults to 100 kHz I2C bus speed. This will work for all devices, but some devices can go faster. Common faster speeds are 200 kHz and 400 kHz.
import board import busio # instead of doing # i2c = board.I2C() i2c = busio.I2C( board.SCL, board.SDA, frequency=200_000) # then do something with 'i2c' object as before, like: oled = adafruit_ssd1306.SSD1306_I2C(width=128, height=32, i2c=i2c)Measure how long something takes
Generally use time.monotonic()
to get the current "uptime" of a board in fractional seconds. So to measure the duration it takes CircuitPython to do something like:
import time start_time = time.monotonic() # put thing you want to measure here, like: import neopixel stop_time = time.monotonic() print("elapsed time = ", stop_time - start_time)
Note that on the "small" versions of CircuitPython in the QT Py M0, Trinket M0, etc., the floating point value of seconds will become less accurate as uptime increases.
More accurate timing withticks_ms()
, like Arduino millis()
If you want something more like Arduino's millis()
function, the supervisor.ticks_ms()
function returns an integer, not a floating point value. It is more useful for sub-second timing tasks and you can still convert it to floating-point seconds for human consumption.
import supervisor start_msecs = supervisor.ticks_ms() import neopixel stop_msecs = supervisor.ticks_ms() print("elapsed time = ", (stop_msecs - start_msecs)/1000)Control garbage collection for reliable timing
The CircuitPython garbage collector makes it so you don't have to deal with memory allocations like you do in languages like C/C++. But when it runs, it can pause your program for tens of milliseconds in some cases. For timing-sensitive applications, you can exhibit some control over the garbage collector so it only runs when you want it (like in the "shadow" after a timing-critical event)
Here's one way to do this.
import gc from adafruit_ticks import ticks_ms, ticks_add, ticks_less gc.collect() # collect any garbage before we... gc.disable() # disable automatic garbage collection dmillis = 10 # how many millis between explicit gc deadline = ticks_add(ticks_ms(), dmillis) while True: now = ticks_ms() if ticks_diff(now, deadline) >= 0: deadline = ticks_add(now, dmillis) gc.collect() # explicitly run a garbage collection cycle # do timing-sensitive thing hereConverting milliseconds to seconds: 0.004 * 1000 != 4, sometimes
When doing timing-sensitive tasks in CircuitPython, you may have code that looks like this (say from the above):
from adafruit_ticks import ticks_ms, ticks_add, ticks_less check_secs = 0.004 # check_secs is seconds between checks check_millis = check_secs * 1000 # convert to millis deadline = ticks_add(ticks_ms(), check_millis) while True: now = ticks_ms() if ticks_less(now,deadline) >= 0: deadline = ticks_add(now, check_millis) do_periodic_task() # do timing-critical thing every 'check_secs'
This seems more accurate than using time.monotonic()
since it's using the millisecond-accurate supervisor.ticks_ms
property, the timing resolution of CircuitPython. This seems to work, until you pass in check_secs = 0.004
, because the ticks_*()
functions expect an integer and int(0.004*1000) = 3
. If you were using the above code to output an accurate timing signal, it's now going to be 25% off from what you expect. This is ultimately because CircuitPython has reduced floating point precision (30-bit instead of 32-bit) (further discusion here). In short: stick to integer milliseconds.
CircuitPython provides a way to get the microcontroller's CPU frequency with microcontroller.cpu.frequency
.
import microcontroller print("CPU speed:", microcontroller.cpu.frequency)
On some chips (most notably Pico P2040), you can also set this value. Overclock your Pico! It's safe to double the RP2040 speed, Raspberry Pi officially supports up to 200 MHz. CircuitPython will adjust its internal timings, but you should do this change before creating any peripheral objects like UARTs or displays.
import microcontroller microcontroller.cpu.frequency = 250_000_000 # run at 250 MHz instead of 125 MHzDisplay amount of free RAM
from: https://learn.adafruit.com/welcome-to-circuitpython/frequently-asked-questions
import gc print( gc.mem_free() )Show microcontroller.pin to board mappings
from https://gist.github.com/anecdata/1c345cb2d137776d76b97a5d5678dc97
import microcontroller import board for pin in dir(microcontroller.pin): if isinstance(getattr(microcontroller.pin, pin), microcontroller.Pin): print("".join(("microcontroller.pin.", pin, "\t")), end=" ") for alias in dir(board): if getattr(board, alias) is getattr(microcontroller.pin, pin): print("".join(("", "board.", alias)), end=" ") print()Determine which board you're on
import os print(os.uname().machine) 'Adafruit ItsyBitsy M4 Express with samd51g19'
Another way is the board.board_id
. This is the "port" name used when compiling CircuitPython. To find a list of valid board IDs, you can look in the circuitpython core repo inside of: "ports/[some_port]/boards/". i.e. for espressif boards find the list of directories in: ports/espressif/boards/. The "board_id is used as the argument for circuitpython_setboard
in circuitpython-stubs
import board print(board.board_id) 'raspberry_pi_pico'
To get the chip family:
import os print(os.uname().sysname) 'ESP32S2'Support multiple boards with one
code.py
import os board_type = os.uname().machine if 'QT Py M0' in board_type: tft_clk = board.SCK tft_mosi = board.MOSI spi = busio.SPI(clock=tft_clk, MOSI=tft_mosi) elif 'ItsyBitsy M4' in board_type: tft_clk = board.SCK tft_mosi = board.MOSI spi = busio.SPI(clock=tft_clk, MOSI=tft_mosi) elif 'Pico' in board_type: tft_clk = board.GP10 # must be a SPI CLK tft_mosi= board.GP11 # must be a SPI TX spi = busio.SPI(clock=tft_clk, MOSI=tft_mosi) else: print("unsupported board", board_type)
name = "John" fav_color = 0x003366 body_temp = 98.65 fav_number = 123 print("name:%s color:%06x temp:%2.1f num:%d" % (name,fav_color,body_temp,fav_number)) # 'name:John color:ff3366 temp:98.6 num:123'Formatting strings with f-strings
(doesn't work on 'small' CircuitPythons like QTPy M0)
name = "John" fav_color = 0xff3366 body_temp = 98.65 fav_number = 123 print(f"name:{name} color:{fav_color:06x} temp:{body_temp:2.1f} num:{fav_number}") # 'name:John color:ff3366 temp:98.6 num:123'Using regular expressions to "findall" strings
Regular expressions are a really powerful way to match information in and parse data from strings. While CircuitPython has a version of the re
regex module you may know from desktop Python, it is very limited. Specifcally it doesn't have the very useful re.findall()
function. Below is a semi-replacement for findall()
.
import re def find_all(regex, some_str): matches = [] while m := regex.search(some_str): matches.append( m.groups() ) some_str = some_str[ m.end(): ] # get past match return matches my_str = "<thing>thing1 I want</thing> <thing>thing2 I want</thing> <thing>thing3 I want</thing>" regex1 = re.compile('<thing.*?>(.*?)<\/thing>') my_matches = find_all( regex1, my_str ) print("matches:", my_matches)Make and use a config file
# my_config.py config = { "username": "Grogu Djarin", "password": "ig88rules", "secret_key": "3a3d9bfaf05835df69713c470427fe35" } # code.py from my_config import config print("secret:", config['secret_key']) # 'secret: 3a3d9bfaf05835df69713c470427fe35'Run different
code.py
on startup
Use microcontroller.nvm
to store persistent state across resets or between boot.py
and code.py
, and declare that the first byte of nvm
will be the startup_mode
. Now if you create multiple code.py files (say) code1.py
, code2.py
, etc. you can switch between them based on startup_mode
.
import time import microcontroller startup_mode = microcontroller.nvm[0] if startup_mode == 1: import code1 # runs code in `code1.py` if startup_mode == 2: import code2 # runs code in `code2.py` # otherwise runs 'code.py` while True: print("main code.py") time.sleep(1)
Note: in CircuitPyton 7+ you can use supervisor.set_next_code_file()
to change which .py file is run on startup. This changes only what happens on reload, not hardware reset or powerup. Using it would look like:
import supervisor supervisor.set_next_code_file('code_awesome.py') # and then if you want to run it now, trigger a reload supervisor.reload()Map an input range to an output range
# simple range mapper, like Arduino map() def map_range(s, a1, a2, b1, b2): return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) # example: map 0-0123 value to 0.0-1.0 value val = 768 outval = map_range( val, 0,1023, 0.0,1.0 ) # outval = 0.75Constrain an input to a min/max
The Python built-in min()
and max()
functions can be used together to make something like Arduino's constrain()
to clamp an input between two values.
# constrain a value to be 0-255 outval = min(max(val, 0), 255) # constrain a value to be 0-255 integer outval = int(min(max(val, 0), 255)) # constrain a value to be -1 to +1 outval = min(max(val, -1), 1)Turn a momentary value into a toggle
import touchio import board touch_pin = touchio.TouchIn(board.GP6) last_touch_val = False # holds last measurement toggle_value = False # holds state of toggle switch while True: touch_val = touch_pin.value if touch_val != last_touch_val: if touch_val: toggle_value = not toggle_value # flip toggle print("toggle!", toggle_value) last_touch_val = touch_valDo something every N seconds without
sleep()
Also known as "blink-without-delay"
import time last_time1 = time.monotonic() # holds when we did something #1 last_time2 = time.monotonic() # holds when we did something #2 while True: if time.monotonic() - last_time1 > 2.0: # every 2 seconds last_time1 = time.monotonic() # save when we do the thing print("hello!") # do thing #1 if time.monotonic() - last_time2 > 5.0: # every 5 seconds last_time2 = time.monotonic() # save when we do the thing print("world!") # do thing #2
Note: a more accurate of this uses ticks_ms()
and maybe turning off gc / garbage collection.
Put a try
/except KeyboardInterrupt
to catch the Ctrl-C on the inside of your main loop.
while True: try: print("Doing something important...") time.sleep(0.1) except KeyboardInterrupt: print("Nice try, human! Not quitting.")
Also useful for graceful shutdown (turning off neopixels, say) on Ctrl-C.
import time, random import board, neopixel, rainbowio leds = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.4 ) while True: try: rgb = rainbowio.colorwheel(int(time.monotonic()*75) % 255) leds.fill(rgb) time.sleep(0.05) except KeyboardInterrupt: print("shutting down nicely...") leds.fill(0) break # gets us out of the while TruePrevent auto-reload when CIRCUITPY is touched
Normally, CircuitPython restarts anytime the CIRCUITPY drive is written to. This is great normally, but is frustrating if you want your code to keep running, and you want to control exactly when a restart happens.
import supervisor supervisor.runtime.autoreload = False # CirPy 8 and above #supervisor.disable_autoreload() # CirPy 7 and below
To trigger a reload, do a Ctrl-C + Ctrl-D in the REPL or reset your board.
Raspberry Pi Pico boot.py ProtectionAlso works on other RP2040-based boards like QTPy RP2040. From https://gist.github.com/Neradoc/8056725be1c209475fd09ffc37c9fad4
Also see getting into Safe Mode with a REPL one-liner.
# Copy this as 'boot.py' in your Pico's CIRCUITPY drive # Useful in case Pico locks up (which it's done a few times on me) import board import time from digitalio import DigitalInOut,Pull led = DigitalInOut(board.LED) led.switch_to_output() safe = DigitalInOut(board.GP14) # <-- choose your button pin safe.switch_to_input(Pull.UP) def reset_on_pin(): if safe.value is False: import microcontroller microcontroller.on_next_reset(microcontroller.RunMode.SAFE_MODE) microcontroller.reset() led.value = False for x in range(16): reset_on_pin() led.value = not led.value # toggle LED on/off as notice time.sleep(0.1)
The "serial" REPL is the most useful diagnostic tools in CircuitPython. Always have it open when saving your code to see any errors. If you use a separate terminal program instead of an IDE, I recommend tio
.
Adafruit CircuitPython 6.2.0-beta.2 on 2021-02-11; Adafruit Trinket M0 with samd21e18
>>> help("modules")
__main__ digitalio pulseio supervisor
analogio gc pwmio sys
array math random time
board microcontroller rotaryio touchio
builtins micropython rtc usb_hid
busio neopixel_write storage usb_midi
collections os struct
Plus any modules on the filesystem
Turn off built-in display to speed up REPL printing
By default CircuitPython will echo the REPL to the display of those boards with built-in displays. This can slow down the REPL. So one way to speed the REPL up is to hide the displayio.Group
that contains all the REPL output.
import board, displayio board.DISPLAY.root_group = None # turn off REPL printing board.DISPLAY.root_group = displayio.CIRCUITPYTHON_TERMINAL # turn back on REPL printing
In CircuitPython 8.x, you could do the below. In 9.x, the root_group
is read-only after it's been assigned.
import board board.DISPLAY.root_group.hidden = True board.DISPLAY.root_group.hidden = False # to turn it back on
(yes, semicolons are legal in Python)
# get into Safe Mode if you have REPL access import microcontroller; microcontroller.on_next_reset(microcontroller.RunMode.SAFE_MODE); microcontroller.reset() # load common libraries (for later REPL experiments) import time, board, analogio, touchio; from digitalio import DigitalInOut,Pull # create a pin and set a pin LOW (if you've done the above) pin = DigitalInOut(board.GP0); pin.switch_to_output(value=False) # print out board pins and objects (like `I2C`, `STEMMA_I2C`, `DISPLAY`, if present) import board; dir(board) # print out microcontroller pins (chip pins, not the same as board pins) import microcontroller; dir(microcontroller.pin) # release configured / built-in display import displayio; displayio.release_displays() # turn off auto-reload when CIRCUITPY drive is touched import supervisor; supervisor.runtime.autoreload = False # test neopixel strip, make them all purple import board, neopixel; leds = neopixel.NeoPixel(board.GP3, 8, brightness=0.2); leds.fill(0xff00ff) leds.deinit() # releases pin # scan I2C bus import board; i2c=board.I2C(); i2c.try_lock(); [hex(a) for a in i2c.scan()]; i2c.unlock()
These are general Python tips that may be useful in CircuitPython.
Create list with elements all the same valueblank_array = [0] * 50 # creats 50-element list of zerosConvert RGB tuples to int and back again
Thanks to @Neradoc for this tip:
rgb_tuple = (255, 0, 128) rgb_int = int.from_bytes(rgb_tuple, byteorder='big') rgb_int = 0xFF0080 rgb_tuple2 = tuple((rgb_int).to_bytes(3,"big")) rgb_tuple2 == rgb_tupleStoring multiple values per list entry
Create simple data structures as config to control your program. Unlike Arduino, you can store multiple values per list/array entry.
mycolors = ( # color val, name (0x0000FF, "blue"), (0x00FFFF, "cyan"), (0xFF00FF, "purple"), ) for i in range(len(mycolors)): (val, name) = mycolors[i] print("my color ", name, "has the value", val)
How to get information about Python inside of CircuitPython.
Display which (not built-in) libraries have been importedimport sys print(sys.modules.keys()) # 'dict_keys([])' import board import neopixel import adafruit_dotstar print(sys.modules.keys()) prints "dict_keys(['neopixel', 'adafruit_dotstar'])"List names of all global variables
a = 123 b = 'hello there' my_globals = sorted(dir) print(my_globals) # prints "['__name__', 'a', 'b']" if 'a' in my_globals: print("you have a variable named 'a'!") if 'c' in my_globals: print("you have a variable named 'c'!")Display the running CircuitPython release
With an established serial connection, press Ctrl+c
:
Adafruit CircuitPython 7.1.1 on 2022-01-14; S2Pico with ESP32S2-S2FN4R2 >>>
Without connection or code running, check the boot_out.txt
file in your CIRCUITPY drive.
import os print(os.uname().release) '7.1.1' print(os.uname().version) '7.1.1 on 2022-01-14'
Things you might need to do on your computer when using CircuitPython.
Installing CircuitPython librariesThe below examples are for MacOS / Linux. Similar commands are used for Windows
Installing libraries withcircup
circup
can be used to easily install and update modules
$ pip3 install --user circup $ circup install adafruit_midi $ circup update # updates all modules
Freshly update all modules to latest version (e.g. when going from CP 6 -> CP 7) (This is needed because circup update
doesn't actually seem to work reliably)
circup freeze > mymodules.txt rm -rf /Volumes/CIRCUITPY/lib/* circup install -r mymodules.txt
And updating circup when a new version of CircuitPython comes out:
$ pip3 install --upgrade circupCopying libraries by hand with
cp
To install libraries by hand from the CircuitPython Library Bundle or from the CircuitPython Community Bundle (which circup doesn't support), get the bundle, unzip it and then use cp -rX
.
cp -rX bundle_folder/lib/adafruit_midi /Volumes/CIRCUITPY/lib
Note: on limited-memory boards like Trinkey, Trinket, QTPy, you must use the -X
option on MacOS to save space. You may also need to omit unused parts of some libraries (e.g. adafruit_midi/system_exclusive
is not needed if just sending MIDI notes)
There's two ways to load images for use with displayio
:
displayio.OnDiskBitmap()
adafruit_imageload
displayio.OnDisBitmap()
:
adafruit_imageload
To make images load faster generally, you can reduce the number of colors in the image. The maximum number of colors is 256, but try reducing colors to 64 or even 2 if it's a black-n-white image.
Some existing Learn Guides:
And here's some ways to do the conversions.
Most online image converters do not create BMPs in the proper format: BMP3, non-compressed, up to 256 colors in an 8-bit palette.
However @Neradoc found the site convert2bmp will work when you set "Color:" mode to "8 (Indexed)". Some initial tests show this works! I'd recommend also trying out one of the following techniques too to have finer control.
The site https://cancerberosgx.github.io/magic/playground/ lets you use any of the ImageMagick commands below to convert images. It's really handy if you can't install ImageMagick locally.
Command-line: using ImageMagickImageMagick is a command-line image manipulation tool. With it, you can convert any image format to BMP3 format. The main ImageMagick CLI command is convert
.
convert myimage.jpg -resize 240x240 -type palette -colors 64 -compress None BMP3:myimage_for_cirpy.bmp
You can also use this technique to create reduced-color palette PNGs:
convert myimage.jpg -resize 240x240 -type palette -colors 64 myimage.pngCommand-line: using GraphicsMagick
GraphicsMagick is a slimmer, lower-requirement clone of ImageMagick. All GraphicsMagick commands are accessed via the gm
CLI command.
gm convert myimage.jpg -resize 240x240 -colors 64 -type palette -compress None BMP3:myimage_for_cirpy.bmpMaking images smaller or for E-Ink displays
To make images smaller (and load faster), reduce number of colors from 256. If your image is a monochrome (or for use with E-Ink displays like MagTag), use 2 colors. The "-dither" options are really helpful for monochrome:
convert cat.jpg -dither FloydSteinberg -colors 2 -type palette BMP3:cat.bmp
There is a nice wrapper around GraphicsMagick / Imagemagick with the gm library
. A small NodeJs program to convert images could look like this:
var gm = require('gm'); gm('myimage.jpg') .resize(240, 240) .colors(64) .type("palette") .compress("None") .write('BMP3:myimage_for_cirpy.bmp', function (err) { if (!err) console.log('done1'); });Python: using PIL / pillow
The Python Image Library (PIL) fork pillow
seems to work the best. It's unclear how to toggle compression.
from PIL import Image size = (240, 240) num_colors = 64 img = Image.open('myimage.jpg') img = img.resize(size) newimg = img.convert(mode='P', colors=num_colors) newimg.save('myimage_for_cirpy.bmp')Preparing audio files for CircuitPython
CircuitPython can play both WAV files and MP3 files, but there are specific variants of these files that will work better, as some options require much more processor demand. WAV files are much easier to play but take up more disk space.
For WAV files, I've found the best trade-off in quality / flash-space / compatibility to be:
And remember that these settings must match how you're setting up the audiomixer
object. So for the above settings, you'd create an audiomixer.Mixer
like:
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1, bits_per_sample=16, samples_signed=True)
To convert WAVs for CircuitPython, I like to use Audacity or the sox
command-line tool. Sox can convert just about any audio to the correct WAV format:
sox loop.mp3 -b 16 -c 1 -r 22050 loop.wav
MP3 files require a lot more CPU to decode so in general you will want to re-encode MP3s to be a lower bit-rate and lower sample-rate. These settings seem to work okay on an lower-end chip like the Pico /RP2040:
In sox
, you can do this conversion with:
sox loop.mp3 -c 1 -r 22050 -C 128 loop_22k_128kbps.mp3
To get sox
on various platforms:
sudo apt install sox libsox-fmt-mp3
brew install sox
Some audio Learn Guide links:
circup
is a great tool to help you install CircuitPython libraries. Think of it like pip
or npm
for CircuitPython.
circup
stores its files
Instead of downloading the bundles by hand, circup
has it already downloaded an unzipped. Here's how to find that directory:
circup_dir=`python3 -c 'import appdirs; print(appdirs.user_data_dir(appname="circup", appauthor="adafruit"))'` ls $circup_dir
If you want to build CircuitPython yourself, you can! It's not too bad. There's a very good "Building CircuitPython" Learn Guide that I refer to all the time, since it goes through the main reasons why you might want to build your own version of CircuitPython, including:
But if you just want a quick list of the commands to use to build, here's what I use (as of Jun 2024) to build CircuitPython for rp2040.
git clone https://github.com/todbot/circuitpython circuitpython-todbot cd circuitpython-todbot pip3 install --upgrade -r requirements-dev.txt # do occasionally, after 'git pull' pip3 install --upgrade -r requirements-doc.txt # do occasionally, after 'git pull' cd ports/raspberrypi make fetch-port-submodules # takes a long time the first time ran, do after 'git pull' too make BOARD=raspberry_pi_pico # or other board name listed in ports/raspberrypi/boards/ # make -C ../../mpy-cross # if you need mpy-cross
And for Espressif / ESP32 builds:
git clone https://github.com/todbot/circuitpython circuitpython-todbot cd circuitpython-todbot pip3 install --upgrade -r requirements-dev.txt # do occasionally, after 'git pull' pip3 install --upgrade -r requirements-doc.txt # do occasionally, after 'git pull' cd ports/espressif make fetch-port-submodules # takes a long time the first time ran, do after 'git pull' too ./esp-idf/install.sh . ./esp-idf/export.sh pip3 install --upgrade -r requirements-dev.txt # because now we're using a new python pip3 install --upgrade -r requirements-doc.txt # because now we're using a new python make BOARD=adafruit_qtpy_esp32s3
Note, this assumes you've already installed the system-level prerequisites. On MacOS, this is what I do to get those:
brew install git git-lfs python3 gettext uncrustify cmake brew install gcc-arm-embedded # (the cask, not 'arm-none-eabi-gcc')
Many of the code snippets are not considered "well-formatted" by Python linters. This guide favors brevity over strict style adherence.
The precursor to this guide is QTPy Tricks, which has similar but different (and still valid) fun things to do in CircuitPython.
This guide is the result of trying to learn Python via CircuitPython and from very enlightening discussions with John Edgar Park. I have a good background in other languages, but mostly C/C++, and have taught classes in Arduino for several years. This informs how I've been thinking about things in this guide.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4