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
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
- 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
importboardfromdigitalioimportDigitalInOut, Pullbutton=DigitalInOut(board.D3) # defaults to inputbutton.pull=Pull.UP# turn on internal pull-up resistorprint(button.value) # False == pressedCan also do:
importtime, board, digitaliobutton=digitalio.DigitalInOut(board.D3)
button.switch_to_input(digitalio.Pull.UP)
whileTrue:
print("button pressed:", button.value==False) # False == pressedtime.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!
importboard, keypadkeys=keypad.Keys((board.D3,), value_when_pressed=False, pull=True)
whileTrue:
ifkey:=keys.events.get():
ifkey.pressed:
print("pressed key!")Note: be sure to add the comma when using a single button (e.g. (board.D3,))
importboardimportanalogiopotknob=analogio.AnalogIn(board.A1)
position=potknob.value# ranges from 0-65535pos=potknob.value//256# make 0-255 rangeNote: 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)
importtouchioimportboardtouch_pin=touchio.TouchIn(board.GP6)
# on Pico / RP2040, need 1M pull-down on each inputiftouch_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.
importboardimportrotaryioencoder=rotaryio.IncrementalEncoder(board.GP0, board.GP1) # must be consecutive on Picoprint(encoder.position) # starts at zero, goes neg or posBut you probably want to use keypad to get debouncing and press/release events.
You can use it for a single button!
importboard, keypadkeys=keypad.Keys((board.D3,), value_when_pressed=False, pull=True)
whileTrue:
ifkey:=keys.events.get():
ifkey.pressed:
print("pressed key!", key.key_number)
ifkey.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.
importboardfromdigitalioimportDigitalInOut, Pullfromadafruit_debouncerimportDebouncerbutton_in=DigitalInOut(board.D3) # defaults to inputbutton_in.pull=Pull.UP# turn on internal pull-up resistorbutton=Debouncer(button_in)
whileTrue:
button.update()
ifbutton.fell:
print("press!")
ifbutton.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
importboardfromdigitalioimportDigitalInOut, Pullfromadafruit_debouncerimportButtonbutton_in=DigitalInOut(board.D3) # defaults to inputbutton_in.switch_to_input(Pull.UP) # turn on internal pull-up resistorbutton=Button(button_in)
whileTrue:
button.update()
ifbutton.pressed:
print("press!")
ifbutton.released:
print("release!")
ifbutton.short_count>1: # detect multi-clickprint("multi-click: click count:", button.short_count)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.
importboardimportkeypadbutton_pins= (board.GP0, board.GP1, board.GP2, board.GP3, board.GP4)
buttons=keypad.Keys(button_pins, value_when_pressed=False, pull=True)
whileTrue:
button=buttons.events.get() # see if there are any key eventsifbutton: # there are events!ifbutton.pressed:
print("button", button.key_number, "pressed!")
ifbutton.released:
print("button", button.key_number, "released!")Otherwise, you can use adafruit_debouncer:
importboardfromdigitalioimportDigitalInOut, Pullfromadafruit_debouncerimportDebouncerbutton_pins= (board.GP0, board.GP1, board.GP2, board.GP3, board.GP4)
buttons= [] # will hold list of Debouncer objectsforpininbutton_pins: # set up each pintmp_pin=DigitalInOut(pin) # defaults to inputtmp_pin.pull=Pull.UP# turn on internal pull-up resistorbuttons.append( Debouncer(tmp_pin) )
whileTrue:
foriinrange(len(buttons)):
buttons[i].update()
ifbuttons[i].fell:
print("button",i,"pressed!")
ifbuttons[i].rose:
print("button",i,"released!")And you can use adafruit_debouncer on touch pins too:
importboard, touchio, adafruit_debouncertouchpad=adafruit_debouncer.Debouncer(touchio.TouchIn(board.GP1))
whileTrue:
touchpad.update()
iftouchpad.rose: print("touched!")
iftouchpad.fell: print("released!")importboardimportdigitalioledpin=digitalio.DigitalInOut(board.D2)
ledpin.direction=digitalio.Direction.OUTPUTledpin.value=TrueCan also do:
ledpin=digitalio.DigitalInOut(board.D2)
ledpin.switch_to_output(value=True)Different boards have DAC on different pins
importboardimportanalogiodac=analogio.AnalogOut(board.A0) # on Trinket M0 & QT Pydac.value=32768# mid-point of 0-65535importboardimportpwmioout1=pwmio.PWMOut(board.MOSI, frequency=25000, duty_cycle=0)
out1.duty_cycle=32768# mid-point 0-65535 = 50 % duty-cycleimportneopixelleds=neopixel.NeoPixel(board.NEOPIXEL, 16, brightness=0.2)
leds[0] =0xff00ff# first LED of 16 definedleds[0] = (255,0,255) # equivalentleds.fill( 0x00ff00 ) # set all to green# servo_animation_code.py -- show simple servo animation listimporttime, random, boardfrompwmioimportPWMOutfromadafruit_motorimportservo# your servo will likely have different min_pulse & max_pulse settingsservoA=servo.Servo(PWMOut(board.RX, frequency=50), min_pulse=500, max_pulse=2250)
# the animation to playanimation= (
# (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 animationwhileTrue:
angle, secs=animation[ ani_pos ]
print("servo moving to", angle, secs)
servoA.angle=angletime.sleep( secs )
ani_pos= (ani_pos+1) %len(animation) # go to next, loop if at endYou 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)
importtime, board, neopixelled_pin=board.GP5# which pin the LED strip is onnum_leds=10colors= ( (255,0,0), (0,255,0), (0,0,255), 0xffffff, 0x000000 )
leds=neopixel.NeoPixel(led_pin, num_leds, brightness=0.1)
i=0whileTrue:
print("led:",i)
forcincolors:
leds[i] =ctime.sleep(0.2)
i= (i+1) %num_ledsIn 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 moduleimporttime, board, neopixeltry:
fromrainbowioimportcolorwheelexcept:
defcolorwheel(pos):
ifpos<0orpos>255: return (0, 0, 0)
ifpos<85: return (255-pos*3, pos*3, 0)
ifpos<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)
whileTrue:
led.fill( colorwheel((time.monotonic()*50)%255) )
time.sleep(0.05)importtime, board, neopixel, rainbowionum_leds=16leds=neopixel.NeoPixel(board.D2, num_leds, brightness=0.4, auto_write=False )
delta_hue=256//num_ledsspeed=10# higher numbers = faster rainbow spinningi=0whileTrue:
forlinrange(len(leds)):
leds[l] =rainbowio.colorwheel( int(i*speed+l*delta_hue) %255 )
leds.show() # only write to LEDs after updating them alli= (i+1) %255time.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.
importsupervisor, board, neopixel, rainbowionum_leds=16speed=10# lower is faster, higher is slowerleds=neopixel.NeoPixel(board.D2, 16, brightness=0.4)
whileTrue:
t=supervisor.ticks_ms() /speedleds[:] = [rainbowio.colorwheel( t+i*(255/len(leds)) ) foriinrange(len(leds))]importtimeimportboard, neopixelnum_leds=16leds=neopixel.NeoPixel(board.D2, num_leds, brightness=0.4, auto_write=False )
my_color= (55,200,230)
dim_by=20# dim amount, higher = shorter tailspos=0whileTrue:
leds[pos] =my_colorleds[:] = [[max(i-dim_by,0) foriinl] forlinleds] # dim all by (dim_by,dim_by,dim_by)pos= (pos+1) %num_leds# move to next positionleds.show() # only write to LEDs after updating them alltime.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 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
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()importtime, board, simpleiowhileTrue:
forfin (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:
importtime, board, audiocore, audiopwmioaudio=audiopwmio.PWMAudioOut(board.GP0)
wave=audiocore.WaveFile("laser2.wav")
audio.play(wave)
whileTrue:
pass# wait for audio to finish playingDetails 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.
importtime, boardfromaudiocoreimportWaveFilefromaudiopwmioimportPWMAudioOutasAudioOutwave=WaveFile("laser2.wav") # can also be filehandle from open()audio=AudioOut(board.GP0) # must be PWM-capable pinwhileTrue:
print("audio is playing:",audio.playing)
ifnotaudio.playing:
audio.play(wave)
wave.sample_rate=int(wave.sample_rate*0.90) # play 10% slower each timetime.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")).
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.
importtime, boardimportaudiocore, audioio# DACwave_file=open("laser2.wav", "rb")
wave=audiocore.WaveFile(wave_file)
audio=audioio.AudioOut(board.A0) # must be DAC-capable pin, A0 on QTPy HaxpresswhileTrue:
print("audio is playing:",audio.playing)
ifnotaudio.playing:
audio.play(wave)
wave.sample_rate=int(wave.sample_rate*0.90) # play 10% slower each timetime.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 adjacentimportboard, audiobusio, audiocoreaudio=audiobusio.I2SOut(bit_clock=board.GP0, word_select=board.GP1, data=board.GP2)
audio.play( audiocore.WaveFile("laser2.wav") )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.
importtime, boardfromaudiocoreimportWaveFilefromaudioioimportAudioOutimportaudiomixerwave=WaveFile("laser2.wav", "rb")
audio=AudioOut(board.A0) # assuming QTPy M0 or Itsy M4mixer=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"whileTrue:
print("mixer voice is playing:", mixer.voice[0].playing)
ifnotmixer.voice[0].playing:
time.sleep(1)
print("playing again")
mixer.voice[0].play(wave)
time.sleep(0.1)This example assumes WAVs that are mono 22050 Hz sample rate, w/ signed 16-bit samples.
importtime, board, audiocore, audiomixerfromaudiopwmioimportPWMAudioOutasAudioOutwav_files= ("loop1.wav", "loop2.wav", "loop3.wav")
wavs= [None] *len(wav_files) # holds the loaded WAVsaudio=AudioOut(board.GP2) # RP2040 examplemixer=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 playbackforiinrange(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 playingwhileTrue:
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.
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:
importboard, audiobusio, audiocore, audiomp3num_voices=2i2s_bclk, i2s_wsel, i2s_data=board.GP9, board.GP10, board.GP11# BCLK, LCLK, DIN on PCM5102audio=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 playbackwav_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 )
whileTrue:
pass# both audio files playNote: 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"importstoragestorage.remount("/", readonly=False)
m=storage.getmount("/")
m.label=new_namestorage.remount("/", readonly=True)importsupervisorifsupervisor.runtime.usb_connected:
led.value=True# USBelse:
led.value=False# no USBAn older way that tries to mount CIRCUITPY read-write and if it fails, USB connected:
defis_usb_connected():
importstoragetry:
storage.remount('/', readonly=False) # attempt to mount readwritestorage.remount('/', readonly=True) # attempt to mount readonlyexceptRuntimeErrorase:
returnTruereturnFalseis_usb="USB"ifis_usb_connected() else"NO USB"print("USB:", is_usb)importosfs_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)importmicrocontrollermicrocontroller.on_next_reset(microcontroller.RunMode.UF2)
microcontroller.reset()Note: in older CircuitPython use RunMode.BOOTLOADER and for boards with multiple
bootloaders (like ESP32-S2):
importmicrocontrollermicrocontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
microcontroller.reset()print("hello there") # prints a newlineprint("waiting...", end='') # does not print newlineforiinrange(256): print(i, end=', ') # comma-separated numberswhileTrue:
print("Type something: ", end='')
my_str=input() # type and press ENTER or RETURNprint("You entered: ", my_str)importtimeimportsupervisorprint("Type something when you're ready")
last_time=time.monotonic()
whileTrue:
ifsupervisor.runtime.serial_bytes_available:
my_str=input()
print("You entered:", my_str)
iftime.monotonic() -last_time>1: # every second, printlast_time=time.monotonic()
print(int(last_time),"waiting...")importtime, sys, supervisorprint("type charactcers")
whileTrue:
n=supervisor.runtime.serial_bytes_availableifn>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)) forcins))
time.sleep(0.01) # do something elseclassUSBSerialReader:
""" Read a line from USB Serial (up to end_char), non-blocking, with optional echo """def__init__(self):
self.s=''defread(self,end_char='\n', echo=True):
importsys, supervisorn=supervisor.runtime.serial_bytes_availableifn>0: # we got bytes!s=sys.stdin.read(n) # actually read it inifecho: sys.stdout.write(s) # echo back to humanself.s=self.s+s# keep building the string upifs.endswith(end_char): # got our end_char!rstr=self.s# save for returnself.s=''# reset str to beginningreturnrstrreturnNone# no end_char yetusb_reader=USBSerialReader()
print("type something and press the end_char")
whileTrue:
mystr=usb_reader.read() # read until newline, echo back chars#mystr = usb_reader.read(end_char='\t', echo=False) # trigger on tab, no echoifmystr:
print("got:",mystr)
time.sleep(0.01) # do something time criticalCircuitPython 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.
importtimeimportusb_hidfromadafruit_hid.keyboardimportKeyboardfromadafruit_hid.keycodeimportKeycodefromadafruit_hid.mouseimportMousekbd=Keyboard(usb_hid.devices)
mouse=Mouse(usb_hid.devices)
whileTrue:
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 righttime.sleep(1)
print("moving left")
kbd.send(Keycode.B) # types "b" (sends press() & release_all())mouse.move(x=-50, y=0) # moves mouse slightly lefttime.sleep(1)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.
importusb_midiimportadafruit_midifromadafruit_midi.note_onimportNoteOnfromadafruit_midi.note_offimportNoteOffmidi_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)
defplay_note(note,velocity=127):
midi.send(NoteOn(note, velocity)) # 127 = highest velocitytime.sleep(0.1)
midi.send(NoteOff(note, 0)) # 0 = lowest velocityNote: This pattern works for sending serial (5-pin) MIDI too, see below
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
importusb_midimidi_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))
defplay_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.
importbusiomidi_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 pinsmidi_uart=busio.UART(tx=board.GP16, rx=board.GP17, baudrate=31250)
defplay_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]) )importusb_midi# built-in libraryimportadafruit_midi# install with 'circup install adafruit_midi'fromadafruit_midi.note_onimportNoteOnfromadafruit_midi.note_offimportNoteOffmidi_usb=adafruit_midi.MIDI(midi_in=usb_midi.ports[0])
whileTrue:
msg=midi_usb.receive()
ifmsg:
ifisinstance(msg, NoteOn):
print("usb noteOn:",msg.note, msg.velocity)
elifisinstance(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.
importboard, busioimportusb_midi# built-in libraryimportadafruit_midi# install with 'circup install adafruit_midi'fromadafruit_midi.note_onimportNoteOnfromadafruit_midi.note_offimportNoteOffuart=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 )
whileTrue:
msg=midi_usb.receive()
ifmsg:
ifisinstance(msg, NoteOn):
print("usb noteOn:",msg.note, msg.velocity)
elifisinstance(msg, NoteOff):
print("usb noteOff:",msg.note, msg.velocity)
msg=midi_serial.receive()
ifmsg:
ifisinstance(msg, NoteOn):
print("serial noteOn:",msg.note, msg.velocity)
elifisinstance(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" (:=)
whileTrue:
whilemsg:=midi_usb.receive() ormidi_uart.receive():
ifisinstance(msg, NoteOn) andmsg.velocity!=0:
note_on(msg.note, msg.velocity)
elifisinstance(msg,NoteOff) orisinstance(msg,NoteOn) andmsg.velocity==0:
note_off(msg.note, msg.velocity)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.pyimportusb_hidimportusb_midiusb_hid.disable()
usb_midi.enable()
print("enabled USB MIDI, disabled USB HID")Note: this is for boards with native WiFi (ESP32)
importwifinetworks= []
fornetworkinwifi.radio.start_scanning_networks():
networks.append(network)
wifi.radio.stop_scanning_networks()
networks=sorted(networks, key=lambdanet: net.rssi, reverse=True)
fornetworkinnetworks:
print("ssid:",network.ssid, "rssi:",network.rssi)importwifidefjoin_best_network(good_networks, print_info=False):
"""join best network based on signal strength of scanned nets"""networks= []
fornetworkinwifi.radio.start_scanning_networks():
networks.append(network)
wifi.radio.stop_scanning_networks()
networks=sorted(networks, key=lambdanet: net.rssi, reverse=True)
fornetworkinnetworks:
ifprint_info: print("network:",network.ssid)
ifnetwork.ssidingood_networks:
ifprint_info: print("connecting to WiFi:", network.ssid)
try:
wifi.radio.connect(network.ssid, good_networks[network.ssid])
returnTrueexceptConnectionErrorase:
ifprint_info: print("connect error:",e)
returnFalsegood_networks= {"todbot1":"FiOnTheFly", # ssid, password"todbot2":"WhyFlyWiFi",}
connected=join_best_network(good_networks, print_info=True)
ifconnected:
print("connected!")Note: this is for boards with native WiFi (ESP32)
importosimporttimeimportwifiimportipaddressip_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)
whileTrue:
print("ping:", wifi.radio.ping(ip1))
time.sleep(1)importos, wifi, socketpoolwifi.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 requiredprint("addrinfo", addrinfo)
ipaddr=addrinfo[0][4][0]
print(f"'{hostname}' ip address is '{ipaddr}'")Note: this is for boards with native WiFi (ESP32)
importosimporttimeimportwifiimportsocketpoolimportsslimportadafruit_requestswifi.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())
whileTrue:
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.
# based on https://docs.circuitpython.org/projects/httpserver/en/latest/starting_methods.htmlimportsocketpoolimportwififromadafruit_httpserverimportRequest, Response, ServerWIFI_SSID="..."WIFI_PASSWORD="..."MY_PORT=8080print(f"Connecting to {WIFI_SSID}...")
wifi.radio.connect(WIFI_SSID, WIFI_PASSWORD)
MY_IP_ADDR=str(wifi.radio.ipv4_address)
print(f"Connected to {WIFI_SSID}")
print(f"My ip address: {MY_IP_ADDR}")
pool=socketpool.SocketPool(wifi.radio)
server=Server(pool, root_path="/static", debug=True)
@server.route("/")defbase(request: Request):
returnResponse(request, "Hello from the CircuitPython HTTP Server!")
server.serve_forever(host=MY_IP_ADDR, port=MY_PORT)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.htmlimporttime, os, rtcimportsocketpool, wifiimportadafruit_ntpmy_tz_offset=-7# PDTwifi.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.datetimewhileTrue:
print("current datetime:", time.localtime())
time.sleep(5)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.
importtime, os, rtcimportwifi, ssl, socketpoolimportadafruit_requestswifi.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*=-1unixtime=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 itwhileTrue:
print("current datetime: ", time.localtime()) # time.* now reflects current local timetime.sleep(5)Also see this more concise version from @deilers78.
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.tomlCIRCUITPY_WIFI_SSID="PrettyFlyForAWiFi"CIRCUITPY_WIFI_PASSWORD="mysecretpassword"# code.pyimportos, wifiprint("connecting...")
wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'),
password=os.getenv('CIRCUITPY_WIFI_PASSWORD'))
print("my IP addr:", wifi.radio.ipv4_address)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.
importboarddisplay=board.DISPLAYprint(display.rotation) # print current rotationdisplay.rotation=0# valid values 0,90,180,270Using displayio.OnDiskBitmap
CircuitPython has a built-in BMP parser called displayio.OnDiskBitmap:
The images should be in non-compressed, palettized BMP3 format.
(how to make BMP3 images)
importboard, displayiodisplay=board.DISPLAYmaingroup=displayio.Group() # everything goes in maingroupdisplay.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 imageUsing adafruit_imageload
You can also use the adafruit_imageload library that supports slightly more kinds of BMP files,
(but should still be palettized BMP3 format
as well as palettized PNG and GIF files. Which file format to choose?
- BMP images are larger but faster to load
- PNG images are about 2x smaller than BMP and almost as fast to load
- GIF images are a little bigger than PNG but much slower to load
importboard, displayioimportadafruit_imageloaddisplay=board.DISPLAYmaingroup=displayio.Group() # everything goes in maingroupdisplay.root_group=maingroup# set the root group to displaybitmap, palette=adafruit_imageload.load("my_image.png")
image=displayio.TileGrid(bitmap, pixel_shader=palette)
maingroup.append(image) # shows the imageHow displayio is structured
CircuitPython's displayio library works like:
- an image
Bitmapand itsPalettegoes inside aTileGrid - a
TileGridgoes inside aGroup - a
Groupis shown on aDisplay.
So if you want to address individual pixels, first create a Palette and a Bitmap.
Assign colors to the Palette positions. Create a TileGrid with that Bitmap and Palette.
Then add the TileGrid to a Group. Finally the Group can be shown on a display.
When you set bitmap[ x,y ] = number you are setting that pixel at x,y to the color at palette
position number.
Useful for display a solid background color that can be quickly changed.
importtime, board, displayiodisplay=board.DISPLAY# get default display (FunHouse,Pygamer,etc)maingroup=displayio.Group() # Create a main group to hold everythingdisplay.root_group=maingroup# put it on the display# make bitmap that spans entire display, with 3 colorsbackground=displayio.Bitmap(display.width, display.height, 3)
# make a 3 color palette to matchmypal=displayio.Palette(3)
mypal[0] =0x000000# set colors (black)mypal[1] =0x999900# dark yellowmypal[2] =0x009999# dark cyan# Put background into main group, using palette to map palette ids to colorsmaingroup.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:
importboard, displayio, vectoriodisplay=board.DISPLAY# built-in displaymaingroup=displayio.Group() # a main group that holds everythingdisplay.root_group=maingroup# put maingroup on the displaymypal=displayio.Palette(1)
mypal[0] =0x999900background=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:
importboard, displayiofromadafruit_display_shapes.rectimportRectdisplay=board.DISPLAYmaingroup=displayio.Group() # a main group that holds everythingdisplay.root_group=maingroup# add it to displaybackground=Rect(0,0, display.width, display.height, fill=0x000000 ) # background colormaingroup.append(background)importtime, board, displayioimportadafruit_imageloaddisplay=board.DISPLAY# get display object (built-in on some boards)screen=displayio.Group() # main group that holds all on-screen contentdisplay.root_group=screen# add it to displayfile_names= [ '/images/cat1.bmp', '/images/cat2.bmp' ] # list of filenamesscreen.append(displayio.Group()) # placeholder, will be replaced w/ screen[0] belowwhileTrue:
forfnameinfile_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
E-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.
importtime, board, displayio, terminaliofromadafruit_display_textimportlabelmylabel=label.Label(terminalio.FONT, text="demo", x=20,y=20,
background_color=0x000000, color=0xffffff )
display=board.DISPLAY# e.g. for MagTagdisplay.root_group=mylabelwhileTrue:
ifdisplay.time_to_refresh==0:
display.refresh()
mylabel.text=str(time.monotonic())
time.sleep(0.1)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.pyimportboardboard.DISPLAY.root_group=Nonefrom: CircuitPython I2C Guide: Find Your Sensor
importboardi2c=board.I2C() # or busio.I2C(pin_scl,pin_sda)whilenoti2c.try_lock(): passprint("I2C addresses found:", [hex(device_address)
fordevice_addressini2c.scan()])
i2c.unlock()One liner to copy-n-paste into REPL for quicky I2C scan:
importboard; i2c=board.I2C(); i2c.try_lock(); [hex(a) foraini2c.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.
importboardimportbusio# 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)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:
importtimestart_time=time.monotonic()
# put thing you want to measure here, like:importneopixelstop_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.
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.
importsupervisorstart_msecs=supervisor.ticks_ms()
importneopixelstop_msecs=supervisor.ticks_ms()
print("elapsed time = ", (stop_msecs-start_msecs)/1000)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.
importgcfromadafruit_ticksimportticks_ms, ticks_add, ticks_lessgc.collect() # collect any garbage before we...gc.disable() # disable automatic garbage collectiondmillis=10# how many millis between explicit gcdeadline=ticks_add(ticks_ms(), dmillis)
whileTrue:
now=ticks_ms()
ifticks_diff(now, deadline) >=0:
deadline=ticks_add(now, dmillis)
gc.collect() # explicitly run a garbage collection cycle# do timing-sensitive thing hereWhen doing timing-sensitive tasks in CircuitPython, you may have code that looks like this (say from the above):
fromadafruit_ticksimportticks_ms, ticks_add, ticks_lesscheck_secs=0.004# check_secs is seconds between checkscheck_millis=check_secs*1000# convert to millisdeadline=ticks_add(ticks_ms(), check_millis)
whileTrue:
now=ticks_ms()
ifticks_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.
importmicrocontrollerprint("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.
importmicrocontrollermicrocontroller.cpu.frequency=250_000_000# run at 250 MHz instead of 125 MHzfrom: https://learn.adafruit.com/welcome-to-circuitpython/frequently-asked-questions
importgcprint( gc.mem_free() )from https://gist.github.com/anecdata/1c345cb2d137776d76b97a5d5678dc97
importmicrocontrollerimportboardforpinindir(microcontroller.pin):
ifisinstance(getattr(microcontroller.pin, pin), microcontroller.Pin):
print("".join(("microcontroller.pin.", pin, "\t")), end=" ")
foraliasindir(board):
ifgetattr(board, alias) isgetattr(microcontroller.pin, pin):
print("".join(("", "board.", alias)), end=" ")
print()importosprint(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
importboardprint(board.board_id)
'raspberry_pi_pico'To get the chip family:
importosprint(os.uname().sysname)
'ESP32S2'importosboard_type=os.uname().machineif'QT Py M0'inboard_type:
tft_clk=board.SCKtft_mosi=board.MOSIspi=busio.SPI(clock=tft_clk, MOSI=tft_mosi)
elif'ItsyBitsy M4'inboard_type:
tft_clk=board.SCKtft_mosi=board.MOSIspi=busio.SPI(clock=tft_clk, MOSI=tft_mosi)
elif'Pico'inboard_type:
tft_clk=board.GP10# must be a SPI CLKtft_mosi=board.GP11# must be a SPI TXspi=busio.SPI(clock=tft_clk, MOSI=tft_mosi)
else:
print("unsupported board", board_type)name="John"fav_color=0x003366body_temp=98.65fav_number=123print("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'(doesn't work on 'small' CircuitPythons like QTPy M0)
name="John"fav_color=0xff3366body_temp=98.65fav_number=123print(f"name:{name} color:{fav_color:06x} temp:{body_temp:2.1f} num:{fav_number}")
# 'name:John color:ff3366 temp:98.6 num:123'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().
importredeffind_all(regex, some_str):
matches= []
whilem:=regex.search(some_str):
matches.append( m.groups() )
some_str=some_str[ m.end(): ] # get past matchreturnmatchesmy_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)# my_config.pyconfig= {
"username": "Grogu Djarin",
"password": "ig88rules",
"secret_key": "3a3d9bfaf05835df69713c470427fe35"
}
# code.pyfrommy_configimportconfigprint("secret:", config['secret_key'])
# 'secret: 3a3d9bfaf05835df69713c470427fe35'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.
importtimeimportmicrocontrollerstartup_mode=microcontroller.nvm[0]
ifstartup_mode==1:
importcode1# runs code in `code1.py`ifstartup_mode==2:
importcode2# runs code in `code2.py`# otherwise runs 'code.py`whileTrue:
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:
importsupervisorsupervisor.set_next_code_file('code_awesome.py')
# and then if you want to run it now, trigger a reloadsupervisor.reload()# simple range mapper, like Arduino map()defmap_range(s, a1, a2, b1, b2):
returnb1+ ((s-a1) * (b2-b1) / (a2-a1))
# example: map 0-0123 value to 0.0-1.0 valueval=768outval=map_range( val, 0,1023, 0.0,1.0 )
# outval = 0.75The 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-255outval=min(max(val, 0), 255)
# constrain a value to be 0-255 integeroutval=int(min(max(val, 0), 255))
# constrain a value to be -1 to +1outval=min(max(val, -1), 1)importtouchioimportboardtouch_pin=touchio.TouchIn(board.GP6)
last_touch_val=False# holds last measurementtoggle_value=False# holds state of toggle switchwhileTrue:
touch_val=touch_pin.valueiftouch_val!=last_touch_val:
iftouch_val:
toggle_value=nottoggle_value# flip toggleprint("toggle!", toggle_value)
last_touch_val=touch_valAlso known as "blink-without-delay"
importtimelast_time1=time.monotonic() # holds when we did something #1last_time2=time.monotonic() # holds when we did something #2whileTrue:
iftime.monotonic() -last_time1>2.0: # every 2 secondslast_time1=time.monotonic() # save when we do the thingprint("hello!") # do thing #1iftime.monotonic() -last_time2>5.0: # every 5 secondslast_time2=time.monotonic() # save when we do the thingprint("world!") # do thing #2Note: 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.
whileTrue:
try:
print("Doing something important...")
time.sleep(0.1)
exceptKeyboardInterrupt:
print("Nice try, human! Not quitting.")Also useful for graceful shutdown (turning off neopixels, say) on Ctrl-C.
importtime, randomimportboard, neopixel, rainbowioleds=neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.4 )
whileTrue:
try:
rgb=rainbowio.colorwheel(int(time.monotonic()*75) %255)
leds.fill(rgb)
time.sleep(0.05)
exceptKeyboardInterrupt:
print("shutting down nicely...")
leds.fill(0)
break# gets us out of the while TrueNormally, 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.
importsupervisorsupervisor.runtime.autoreload=False# CirPy 8 and above#supervisor.disable_autoreload() # CirPy 7 and belowTo trigger a reload, do a Ctrl-C + Ctrl-D in the REPL or reset your board.
Also 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)importboardimporttimefromdigitalioimportDigitalInOut,Pullled=DigitalInOut(board.LED)
led.switch_to_output()
safe=DigitalInOut(board.GP14) # <-- choose your button pinsafe.switch_to_input(Pull.UP)
defreset_on_pin():
ifsafe.valueisFalse:
importmicrocontrollermicrocontroller.on_next_reset(microcontroller.RunMode.SAFE_MODE)
microcontroller.reset()
led.value=Falseforxinrange(16):
reset_on_pin()
led.value=notled.value# toggle LED on/off as noticetime.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
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.
importboard, displayioboard.DISPLAY.root_group=None# turn off REPL printingboard.DISPLAY.root_group=displayio.CIRCUITPYTHON_TERMINAL# turn back on REPL printingIn CircuitPython 8.x, you could do the below. In 9.x, the root_group is read-only
after it's been assigned.
importboardboard.DISPLAY.root_group.hidden=Trueboard.DISPLAY.root_group.hidden=False# to turn it back on(yes, semicolons are legal in Python)
# get into Safe Mode if you have REPL accessimportmicrocontroller; microcontroller.on_next_reset(microcontroller.RunMode.SAFE_MODE); microcontroller.reset()
# load common libraries (for later REPL experiments)importtime, board, analogio, touchio; fromdigitalioimportDigitalInOut,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)importboard; dir(board)
# print out microcontroller pins (chip pins, not the same as board pins)importmicrocontroller; dir(microcontroller.pin)
# release configured / built-in displayimportdisplayio; displayio.release_displays()
# turn off auto-reload when CIRCUITPY drive is touchedimportsupervisor; supervisor.runtime.autoreload=False# test neopixel strip, make them all purpleimportboard, neopixel; leds=neopixel.NeoPixel(board.GP3, 8, brightness=0.2); leds.fill(0xff00ff)
leds.deinit() # releases pin# scan I2C busimportboard; i2c=board.I2C(); i2c.try_lock(); [hex(a) foraini2c.scan()]; i2c.unlock()These are general Python tips that may be useful in CircuitPython.
blank_array= [0] *50# creats 50-element list of zerosThanks to @Neradoc for this tip:
rgb_tuple= (255, 0, 128)
rgb_int=int.from_bytes(rgb_tuple, byteorder='big')
rgb_int=0xFF0080rgb_tuple2=tuple((rgb_int).to_bytes(3,"big"))
rgb_tuple2==rgb_tupleCreate 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"),
)
foriinrange(len(mycolors)):
(val, name) =mycolors[i]
print("my color ", name, "has the value", val)How to get information about Python inside of CircuitPython.
importsysprint(sys.modules.keys())
# 'dict_keys([])'importboardimportneopixelimportadafruit_dotstarprint(sys.modules.keys())
prints"dict_keys(['neopixel', 'adafruit_dotstar'])"a=123b='hello there'my_globals=sorted(dir)
print(my_globals)
# prints "['__name__', 'a', 'b']"if'a'inmy_globals:
print("you have a variable named 'a'!")
if'c'inmy_globals:
print("you have a variable named 'c'!")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.
importosprint(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.
The below examples are for MacOS / Linux. Similar commands are used for Windows
circup can be used to easily install and update modules
$ pip3 install --user circup
$ circup install adafruit_midi
$ circup update # updates all modulesFreshly 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.txtAnd updating circup when a new version of CircuitPython comes out:
$ pip3 install --upgrade circupTo 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/libNote: 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:
- The built-in
displayio.OnDiskBitmap() - The library
adafruit_imageload
displayio.OnDisBitmap():
- can load "indexed" (aka "palette") non-compressed BMP3 images
- doesn't load image into RAM (great for TileGrids)
adafruit_imageload
- can load BMP3 images with RLE compression
- loads entire image into RAM (thus you may run out of memory)
- an load palette PNG images and GIF images
- PNG image loading is almost as fast as BMP and uses 1/2 the disk space
- GIF loading is very slow
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:
- https://learn.adafruit.com/creating-your-first-tilemap-game-with-circuitpython/indexed-bmp-graphics
- https://learn.adafruit.com/preparing-graphics-for-e-ink-displays
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.
ImageMagick 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.bmpYou can also use this technique to create reduced-color palette PNGs:
convert myimage.jpg -resize 240x240 -type palette -colors 64 myimage.pngGraphicsMagick 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.bmpTo 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:
vargm=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');});The Python Image Library (PIL) fork pillow
seems to work the best. It's unclear how to toggle compression.
fromPILimportImagesize= (240, 240)
num_colors=64img=Image.open('myimage.jpg')
img=img.resize(size)
newimg=img.convert(mode='P', colors=num_colors)
newimg.save('myimage_for_cirpy.bmp')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:
- PCM 16-bit signed PCM
- Mono (but stereo will work if using I2S or SAMD51)
- 22050 Hz sample rate
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.wavMP3 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:
- 128 kbps data rate CBR or lower
- 22050 Hz sample rate or lower
- Mono
In sox, you can do this conversion with:
sox loop.mp3 -c 1 -r 22050 -C 128 loop_22k_128kbps.mp3To get sox on various platforms:
- Linux:
sudo apt install sox libsox-fmt-mp3 - macOS:
brew install sox - Windows: Use installer at http://sox.sourceforge.net/
Some audio Learn Guide links:
- https://learn.adafruit.com/circuitpython-essentials/circuitpython-audio-out#play-a-wave-file-2994862-6
- https://learn.adafruit.com/adafruit-wave-shield-audio-shield-for-arduino/convert-files
circup
is a great tool to help you install CircuitPython libraries. Think of it like pip or npm for CircuitPython.
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_dirIf 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:
- Adding "Frozen" Modules (libraries built-in to the firmware)
- Setting different SPI flash chips (if your custom board uses a different kind of flash)
- Adding a new board to CircuitPython
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-crossAnd 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_esp32s3Note, 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.