├── .gitignore ├── LICENSE.TXT ├── README.md ├── larger-tricks ├── README.md ├── audiomixer_demo.py ├── audiomixer_demo_i2s.py ├── beatfader.py ├── beatfader_i2s.py ├── beatfader_wavs │ ├── amen_22k16b_160bpm.wav │ ├── dnb21580_22k16b_160bpm.wav │ ├── drumloopA_22k16b_160bpm.wav │ └── femvoc_330662_22k16b_160bpm.wav ├── beatslicer_idea.py ├── bmps │ ├── emoji_spritesheet_27x2_28x28-1.bmp │ ├── emoji_spritesheet_27x2_28x28-2.bmp │ ├── emoji_spritesheet_27x2_28x28-3.bmp │ ├── emoji_spritesheet_27x2_28x28.bmp │ ├── emoji_spritesheet_27x7_28x28.bmp │ └── fireworks_spritesheet.bmp ├── bouncy_balls_vectorio.py ├── breakbeat_sampleplayer.py ├── breakbeat_wav │ ├── amen1_22k_s16.wav │ ├── amen2_22k_s16.wav │ ├── amen3_22k_s16.wav │ ├── amen4_22k_s16.wav │ ├── amen5_22k_s16.wav │ ├── amen6_22k_s16.wav │ ├── amen7_22k_s16.wav │ ├── amen8_22k_s16.wav │ ├── amenfull_22k_s16.wav │ └── ohohoh2.wav ├── chikkenplayer.py ├── chikkenplayer_wav │ ├── chikken1_161_22k16b.wav │ ├── chikken2_161_22k16b.wav │ ├── chikken3_161_22k16b.wav │ ├── chikken4_161_22k16b.wav │ ├── chikken5_161_22k16b.wav │ └── chikken6_161_22k16b.wav ├── cpx_midi_controller.py ├── docs │ ├── breakbeat_sampleplayer.fzz │ └── breakbeat_sampleplayer_wiring.png ├── dvdlogo_vectorio_code.py ├── emoji_flipper.py ├── eyeballs │ ├── code.py │ ├── imgs │ │ ├── eye0_ball2.bmp │ │ ├── eye0_iris0.bmp │ │ ├── eyelid_spritesheet.bmp │ │ └── eyelid_spritesheet2.bmp │ ├── qteye.py │ ├── qteye_blink_esp32s3.py │ ├── qteye_blink_qualia.py │ └── qteye_person_sensor.py ├── fireworks_sprites.py ├── i2s_sdcard_pico.py ├── midi_forward.py ├── pidaydrummachine.py ├── pidaydrummachine_wavs │ ├── 544413_punch_22k16b.wav │ ├── 544587_splash1a_22k16b.wav │ ├── 544682_snare_22k16b.wav │ ├── 544684_dubhat1_22k16b.wav │ └── 544694_toggle_22k16b.wav ├── pidaydrummachine_wiring.fzz ├── pidaydrummachine_wiring.png ├── pin_state.py ├── robust_keyboard.py ├── sdcard_max_wavs │ ├── code.py │ └── sine_wavs │ │ ├── 01.wav │ │ ├── 02.wav │ │ ├── 03.wav │ │ ├── 04.wav │ │ ├── 05.wav │ │ ├── 06.wav │ │ ├── 07.wav │ │ ├── 08.wav │ │ ├── 09.wav │ │ ├── 10.wav │ │ ├── 11.wav │ │ ├── 12.wav │ │ ├── 13.wav │ │ ├── 14.wav │ │ ├── 15.wav │ │ ├── 16.wav │ │ ├── 17.wav │ │ ├── 18.wav │ │ ├── 19.wav │ │ ├── 20.wav │ │ ├── 21.wav │ │ ├── 22.wav │ │ ├── 23.wav │ │ ├── 24.wav │ │ ├── 25.wav │ │ └── make_wavs.sh ├── vectorio_rotate_example.py └── wav │ ├── drumsacuff_22k_s16.wav │ ├── laser2.wav │ ├── laser20.wav │ ├── silence-2sec.wav │ ├── snowpeaks_22k_s16.wav │ └── vocalchops476663_22k_128k.mp3 └── synthio └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.mpy 2 | .idea 3 | __pycache__ 4 | _build 5 | *.pyc 6 | .env 7 | bundles 8 | *.DS_Store 9 | .eggs 10 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 Tod Kurt 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # circuitpython-tricks 3 | 4 | A small list of tips & tricks I find myself needing when working with CircuitPython. 5 | I find these examples useful when picking up a new project and I just want some boilerplate to get started. 6 | Also see the [circuitpython-tricks/larger-tricks](larger-tricks) directory for additional ideas. 7 | 8 | An older version of this page is a [Learn Guide on Adafruit](https://learn.adafruit.com/todbot-circuitpython-tricks?view=all) too! 9 | 10 | If you're new to CircuitPython overall, there's no single reference, but: 11 | - [The Python Tutorial](https://docs.python.org/3/tutorial/) on Python.org, 12 | since "CircuitPython is Python" mostly. (approx. Python 3.4) 13 | - [CircuitPython API reference](https://docs.circuitpython.org/en/latest/docs/), particularly the ["Core Modules > Modules" section](https://docs.circuitpython.org/en/latest/shared-bindings/index.html#modules) in the left sidebar 14 | - for compiled-in libraries like `displayio`, `usb`, `audioio`, `ulab.numpy` 15 | - [Pure-Python libraries in Adafruit Library Bundle](https://github.com/adafruit/Adafruit_CircuitPython_Bundle) for [drivers](https://github.com/adafruit/Adafruit_CircuitPython_Bundle/tree/main/libraries/drivers) & [helpers](https://github.com/adafruit/Adafruit_CircuitPython_Bundle/tree/main/libraries/helpers) libraries like `board`, `neopixel` & `ble` 16 | - and [CircuitPython Essentials Learn Guide](https://learn.adafruit.com/circuitpython-essentials) of course 17 | 18 | 19 | Table of Contents 20 | ================= 21 | 22 | But it's probably easiest to do a Cmd-F/Ctrl-F find on keyword of idea you want. 23 | 24 | * [Inputs](#inputs) 25 | * [Read a digital input as a Button](#read-a-digital-input-as-a-button) 26 | * [Read a Potentiometer](#read-a-potentiometer) 27 | * [Read a Touch Pin / Capsense](#read-a-touch-pin--capsense) 28 | * [Read a Rotary Encoder](#read-a-rotary-encoder) 29 | * [Debounce a pin / button](#debounce-a-pin--button) 30 | * [Detect button double-click](#detect-button-double-click) 31 | * [Set up and debounce a list of pins](#set-up-and-debounce-a-list-of-pins) 32 | * [Outputs](#outputs) 33 | * [Output HIGH / LOW on a pin (like an LED)](#output-high--low-on-a-pin-like-an-led) 34 | * [Output Analog value on a DAC pin](#output-analog-value-on-a-dac-pin) 35 | * [Output a "Analog" value on a PWM pin](#output-a-analog-value-on-a-pwm-pin) 36 | * [Control Neopixel / WS2812 LEDs](#control-neopixel--ws2812-leds) 37 | * [Control a servo, with animation list](#control-a-servo-with-animation-list) 38 | * [Neopixels / Dotstars](#neopixels--dotstars) 39 | * [Light each LED in order](#light-each-led-in-order) 40 | * [Moving rainbow on built-in board.NEOPIXEL](#moving-rainbow-on-built-in-boardneopixel) 41 | * [Make moving rainbow gradient across LED strip](#make-moving-rainbow-gradient-across-led-strip) 42 | * [Fade all LEDs by amount for chase effects](#fade-all-leds-by-amount-for-chase-effects) 43 | * [Audio](#audio) 44 | * [Making simple tones](#making-simple-tones) 45 | * [Play a WAV file](#play-a-wav-file) 46 | * [Audio out using PWM](#audio-out-using-pwm) 47 | * [Audio out using DAC](#audio-out-using-dac) 48 | * [Audio out using I2S](#audio-out-using-i2s) 49 | * [Use audiomixer to prevent audio crackles](#use-audiomixer-to-prevent-audio-crackles) 50 | * [Play multiple sounds with audiomixer](#play-multiple-sounds-with-audiomixer) 51 | * [Playing MP3 files](#playing-mp3-files) 52 | * [USB](#usb) 53 | * [Rename CIRCUITPY drive to something new](#rename-circuitpy-drive-to-something-new) 54 | * [Detect if USB is connected or not](#detect-if-usb-is-connected-or-not) 55 | * [Get CIRCUITPY disk size and free space](#get-circuitpy-disk-size-and-free-space) 56 | * [Programmatically reset to UF2 bootloader](#programmatically-reset-to-uf2-bootloader) 57 | * [USB Serial](#usb-serial) 58 | * [Print to USB Serial](#print-to-usb-serial) 59 | * [Read user input from USB Serial, blocking](#read-user-input-from-usb-serial-blocking) 60 | * [Read user input from USB Serial, non-blocking (mostly)](#read-user-input-from-usb-serial-non-blocking-mostly) 61 | * [Read keys from USB Serial](#read-keys-from-usb-serial) 62 | * [Read user input from USB serial, non-blocking](#read-user-input-from-usb-serial-non-blocking) 63 | * [USB MIDI](#usb-midi) 64 | * [Sending MIDI with adafruit_midi](#sending-midi-with-adafruit_midi) 65 | * [Sending MIDI with bytearray](#sending-midi-with-bytearray) 66 | * [MIDI over Serial UART](#midi-over-serial-uart) 67 | * [Receiving MIDI](#receiving-midi) 68 | * [Receiving MIDI USB and MIDI Serial UART together](#receiving-midi-usb-and-midi-serial-uart-together) 69 | * [Enable USB MIDI in boot.py (for ESP32-S2 and STM32F4)](#enable-usb-midi-in-bootpy-for-esp32-s2-and-stm32f4) 70 | * [WiFi / Networking](#wifi--networking) 71 | * [Scan for WiFi Networks, sorted by signal strength](#scan-for-wifi-networks-sorted-by-signal-strength) 72 | * [Join WiFi network with highest signal strength](#join-wifi-network-with-highest-signal-strength) 73 | * [Ping an IP address](#ping-an-ip-address) 74 | * [Get IP address of remote host](#get-ip-address-of-remote-host) 75 | * [Fetch a JSON file](#fetch-a-json-file) 76 | * [Serve a webpage via HTTP](#serve-a-webpage-via-http) 77 | * [Set RTC time from NTP](#set-rtc-time-from-ntp) 78 | * [Set RTC time from time service](#set-rtc-time-from-time-service) 79 | * [What the heck is settings.toml?](#what-the-heck-is-settingstoml) 80 | * [What the heck is secrets.py?](#what-the-heck-is-secretspy) 81 | * [Displays (LCD / OLED / E-Ink) and displayio](#displays-lcd--oled--e-ink-and-displayio) 82 | * [Get default display and change display rotation](#get-default-display-and-change-display-rotation) 83 | * [Display an image](#display-an-image) 84 | * [Display background bitmap](#display-background-bitmap) 85 | * [Image slideshow](#image-slideshow) 86 | * [Dealing with E-Ink "Refresh Too Soon" error](#dealing-with-e-ink-refresh-too-soon-error) 87 | * [Turn off REPL on built-in display](#turn-off-repl-on-built-in-display) 88 | * [I2C](#i2c) 89 | * [Scan I2C bus for devices](#scan-i2c-bus-for-devices) 90 | * [Speed up I2C bus](#speed-up-i2c-bus) 91 | * [Timing](#timing) 92 | * [Measure how long something takes](#measure-how-long-something-takes) 93 | * [More accurate timing with ticks_ms(), like Arduino millis()](#more-accurate-timing-with-ticks_ms-like-arduino-millis) 94 | * [Control garbage collection for reliable timing](#control-garbage-collection-for-reliable-timing) 95 | * [Converting milliseconds to seconds: 0.004 * 1000 != 4, sometimes](#converting-milliseconds-to-seconds-0004--1000--4-sometimes) 96 | * [Board Info](#board-info) 97 | * [Get CPU speed (and set it!)](#get-cpu-speed-and-set-it) 98 | * [Display amount of free RAM](#display-amount-of-free-ram) 99 | * [Show microcontroller.pin to board mappings](#show-microcontrollerpin-to-board-mappings) 100 | * [Determine which board you're on](#determine-which-board-youre-on) 101 | * [Support multiple boards with one code.py](#support-multiple-boards-with-one-codepy) 102 | * [Computery Tasks](#computery-tasks) 103 | * [Formatting strings](#formatting-strings) 104 | * [Formatting strings with f-strings](#formatting-strings-with-f-strings) 105 | * [Using regular expressions to "findall" strings](#using-regular-expressions-to-findall-strings) 106 | * [Make and use a config file](#make-and-use-a-config-file) 107 | * [Run different code.py on startup](#run-different-codepy-on-startup) 108 | * [Coding Techniques](#coding-techniques) 109 | * [Map an input range to an output range](#map-an-input-range-to-an-output-range) 110 | * [Constrain an input to a min/max](#constrain-an-input-to-a-minmax) 111 | * [Turn a momentary value into a toggle](#turn-a-momentary-value-into-a-toggle) 112 | * [Do something every N seconds without sleep()](#do-something-every-n-seconds-without-sleep) 113 | * [System error handling](#system-error-handling) 114 | * [Preventing Ctrl-C from stopping the program](#preventing-ctrl-c-from-stopping-the-program) 115 | * [Prevent auto-reload when CIRCUITPY is touched](#prevent-auto-reload-when-circuitpy-is-touched) 116 | * [Raspberry Pi Pico boot.py Protection](#raspberry-pi-pico-bootpy-protection) 117 | * [Using the REPL](#using-the-repl) 118 | * [Display built-in modules / libraries](#display-built-in-modules--libraries) 119 | * [Turn off built-in display to speed up REPL printing](#turn-off-built-in-display-to-speed-up-repl-printing) 120 | * [Useful REPL one-liners](#useful-repl-one-liners) 121 | * [Python tricks](#python-tricks) 122 | * [Create list with elements all the same value](#create-list-with-elements-all-the-same-value) 123 | * [Convert RGB tuples to int and back again](#convert-rgb-tuples-to-int-and-back-again) 124 | * [Storing multiple values per list entry](#storing-multiple-values-per-list-entry) 125 | * [Python info](#python-info) 126 | * [Display which (not built-in) libraries have been imported](#display-which-not-built-in-libraries-have-been-imported) 127 | * [List names of all global variables](#list-names-of-all-global-variables) 128 | * [Display the running CircuitPython release](#display-the-running-circuitpython-release) 129 | * [Host-side tasks](#host-side-tasks) 130 | * [Installing CircuitPython libraries](#installing-circuitpython-libraries) 131 | * [Installing libraries with circup](#installing-libraries-with-circup) 132 | * [Copying libraries by hand with cp](#copying-libraries-by-hand-with-cp) 133 | * [Preparing images for CircuitPython](#preparing-images-for-circuitpython) 134 | * [Online](#online) 135 | * [Command-line: using ImageMagick](#command-line-using-imagemagick) 136 | * [Command-line: using GraphicsMagick](#command-line-using-graphicsmagick) 137 | * [Making images smaller or for E-Ink displays](#making-images-smaller-or-for-e-ink-displays) 138 | * [NodeJs: using gm](#nodejs-using-gm) 139 | * [Python: using PIL / pillow](#python-using-pil--pillow) 140 | * [Preparing audio files for CircuitPython](#preparing-audio-files-for-circuitpython) 141 | * [WAV files](#wav-files) 142 | * [MP3 files](#mp3-files) 143 | * [Getting sox](#getting-sox) 144 | * [Circup hacks](#circup-hacks) 145 | * [Finding where circup stores its files](#finding-where-circup-stores-its-files) 146 | * [Building CircuitPython](#building-circuitpython) 147 | * [About this guide](#about-this-guide) 148 | 149 | 150 | ## Inputs 151 | 152 | ### Read a digital input as a Button 153 | 154 | ```py 155 | import board 156 | from digitalio import DigitalInOut, Pull 157 | button = DigitalInOut(board.D3) # defaults to input 158 | button.pull = Pull.UP # turn on internal pull-up resistor 159 | print(button.value) # False == pressed 160 | ``` 161 | 162 | Can also do: 163 | 164 | ```py 165 | import time, board, digitalio 166 | button = digitalio.DigitalInOut(board.D3) 167 | button.switch_to_input(digitalio.Pull.UP) 168 | while True: 169 | print("button pressed:", button.value == False) # False == pressed 170 | time.sleep(0.1) 171 | ``` 172 | 173 | But you probably want to use `keypad` to get debouncing and press/release events. 174 | You can use it for a single button! 175 | 176 | ```py 177 | import board, keypad 178 | keys = keypad.Keys((board.D3,), value_when_pressed=False, pull=True) 179 | while True: 180 | if key := keys.events.get(): 181 | if key.pressed: 182 | print("pressed key!") 183 | 184 | ``` 185 | Note: be sure to add the comma when using a single button (e.g. `(board.D3,)`) 186 | 187 | 188 | ### Read a Potentiometer 189 | 190 | ```py 191 | import board 192 | import analogio 193 | potknob = analogio.AnalogIn(board.A1) 194 | position = potknob.value # ranges from 0-65535 195 | pos = potknob.value // 256 # make 0-255 range 196 | ``` 197 | 198 | Note: While `AnalogIn.value` is 16-bit (0-65535) corresponding to 0 V to 3.3V, 199 | the MCU ADCs can have limitations in resolution and voltage range. 200 | This reduces what CircuitPython sees. 201 | For example, the ESP32 ADCs are 12-bit w/ approx 0.1 V to 2.5 V range 202 | (e.g. `value` goes from around 200 to 50,000, in steps of 16) 203 | 204 | ### Read a Touch Pin / Capsense 205 | 206 | ```py 207 | import touchio 208 | import board 209 | touch_pin = touchio.TouchIn(board.GP6) 210 | # on Pico / RP2040, need 1M pull-down on each input 211 | if touch_pin.value: 212 | print("touched!") 213 | ``` 214 | 215 | You can also get an "analog" touch value with `touch_pin.raw_value` to do 216 | basic proximity detection or even [theremin-like behavior](https://gist.github.com/todbot/bb4ec9c509f8c301e4787e5cb26ec870). 217 | 218 | ### Read a Rotary Encoder 219 | 220 | ```py 221 | import board 222 | import rotaryio 223 | encoder = rotaryio.IncrementalEncoder(board.GP0, board.GP1) # must be consecutive on Pico 224 | print(encoder.position) # starts at zero, goes neg or pos 225 | ``` 226 | 227 | ### Debounce a pin / button 228 | 229 | But you probably want to use `keypad` to get debouncing and press/release events. 230 | You can use it for a single button! 231 | 232 | ```py 233 | import board, keypad 234 | keys = keypad.Keys((board.D3,), value_when_pressed=False, pull=True) 235 | while True: 236 | if key := keys.events.get(): 237 | if key.pressed: 238 | print("pressed key!", key.key_number) 239 | if key.released: 240 | print("released key!", key.key_number) 241 | 242 | ``` 243 | Note: be sure to add the comma when using a single button (e.g. `(board.D3,)`) 244 | 245 | If your board doesn't have `keypad`, you can use `adafruit_debouncer` from 246 | the bundle. 247 | 248 | ```py 249 | import board 250 | from digitalio import DigitalInOut, Pull 251 | from adafruit_debouncer import Debouncer 252 | button_in = DigitalInOut(board.D3) # defaults to input 253 | button_in.pull = Pull.UP # turn on internal pull-up resistor 254 | button = Debouncer(button_in) 255 | while True: 256 | button.update() 257 | if button.fell: 258 | print("press!") 259 | if button.rose: 260 | print("release!") 261 | ``` 262 | 263 | Note: Most boards have the native `keypad` module that can do keypad debouncing in a much more 264 | efficient way. See [Set up and debounce a list of pins](#set-up-and-debounce-a-list-of-pins) 265 | 266 | 267 | ### Detect button double-click 268 | 269 | ```py 270 | import board 271 | from digitalio import DigitalInOut, Pull 272 | from adafruit_debouncer import Button 273 | button_in = DigitalInOut(board.D3) # defaults to input 274 | button_in.switch_to_input(Pull.UP) # turn on internal pull-up resistor 275 | button = Button(button_in) 276 | while True: 277 | button.update() 278 | if button.pressed: 279 | print("press!") 280 | if button.released: 281 | print("release!") 282 | if button.short_count > 1: # detect multi-click 283 | print("multi-click: click count:", button.short_count) 284 | ``` 285 | 286 | ### Set up and debounce a list of pins 287 | 288 | If your board's CircuitPython has the `keypad` library (most do), 289 | then I recommend using it. It's not just for key matrixes! And it's more efficient 290 | and, since it's built-in, reduces a library dependency. 291 | 292 | ```py 293 | import board 294 | import keypad 295 | button_pins = (board.GP0, board.GP1, board.GP2, board.GP3, board.GP4) 296 | buttons = keypad.Keys(button_pins, value_when_pressed=False, pull=True) 297 | 298 | while True: 299 | button = buttons.events.get() # see if there are any key events 300 | if button: # there are events! 301 | if button.pressed: 302 | print("button", button.key_number, "pressed!") 303 | if button.released: 304 | print("button", button.key_number, "released!") 305 | ``` 306 | 307 | Otherwise, you can use `adafruit_debouncer`: 308 | 309 | ```py 310 | import board 311 | from digitalio import DigitalInOut, Pull 312 | from adafruit_debouncer import Debouncer 313 | button_pins = (board.GP0, board.GP1, board.GP2, board.GP3, board.GP4) 314 | buttons = [] # will hold list of Debouncer objects 315 | for pin in button_pins: # set up each pin 316 | tmp_pin = DigitalInOut(pin) # defaults to input 317 | tmp_pin.pull = Pull.UP # turn on internal pull-up resistor 318 | buttons.append( Debouncer(tmp_pin) ) 319 | while True: 320 | for i in range(len(buttons)): 321 | buttons[i].update() 322 | if buttons[i].fell: 323 | print("button",i,"pressed!") 324 | if buttons[i].rose: 325 | print("button",i,"released!") 326 | ``` 327 | 328 | And you can use `adafruit_debouncer` on touch pins too: 329 | 330 | ```py 331 | import board, touchio, adafruit_debouncer 332 | touchpad = adafruit_debouncer.Debouncer(touchio.TouchIn(board.GP1)) 333 | while True: 334 | touchpad.update() 335 | if touchpad.rose: print("touched!") 336 | if touchpad.fell: print("released!") 337 | ``` 338 | 339 | 340 | ## Outputs 341 | 342 | ### Output HIGH / LOW on a pin (like an LED) 343 | 344 | ```py 345 | import board 346 | import digitalio 347 | ledpin = digitalio.DigitalInOut(board.D2) 348 | ledpin.direction = digitalio.Direction.OUTPUT 349 | ledpin.value = True 350 | ``` 351 | 352 | Can also do: 353 | ```py 354 | ledpin = digitalio.DigitalInOut(board.D2) 355 | ledpin.switch_to_output(value=True) 356 | ``` 357 | 358 | ### Output Analog value on a DAC pin 359 | 360 | Different boards have DAC on different pins 361 | 362 | ```py 363 | import board 364 | import analogio 365 | dac = analogio.AnalogOut(board.A0) # on Trinket M0 & QT Py 366 | dac.value = 32768 # mid-point of 0-65535 367 | ``` 368 | 369 | ### Output a "Analog" value on a PWM pin 370 | 371 | ```py 372 | import board 373 | import pwmio 374 | out1 = pwmio.PWMOut(board.MOSI, frequency=25000, duty_cycle=0) 375 | out1.duty_cycle = 32768 # mid-point 0-65535 = 50 % duty-cycle 376 | ``` 377 | 378 | ### Control Neopixel / WS2812 LEDs 379 | 380 | ```py 381 | import neopixel 382 | leds = neopixel.NeoPixel(board.NEOPIXEL, 16, brightness=0.2) 383 | leds[0] = 0xff00ff # first LED of 16 defined 384 | leds[0] = (255,0,255) # equivalent 385 | leds.fill( 0x00ff00 ) # set all to green 386 | ``` 387 | 388 | ### Control a servo, with animation list 389 | 390 | ```py 391 | # servo_animation_code.py -- show simple servo animation list 392 | import time, random, board 393 | from pwmio import PWMOut 394 | from adafruit_motor import servo 395 | 396 | # your servo will likely have different min_pulse & max_pulse settings 397 | servoA = servo.Servo(PWMOut(board.RX, frequency=50), min_pulse=500, max_pulse=2250) 398 | 399 | # the animation to play 400 | animation = ( 401 | # (angle, time to stay at that angle) 402 | (0, 2.0), 403 | (90, 2.0), 404 | (120, 2.0), 405 | (180, 2.0) 406 | ) 407 | ani_pos = 0 # where in list to start our animation 408 | 409 | while True: 410 | angle, secs = animation[ ani_pos ] 411 | print("servo moving to", angle, secs) 412 | servoA.angle = angle 413 | time.sleep( secs ) 414 | ani_pos = (ani_pos + 1) % len(animation) # go to next, loop if at end 415 | ``` 416 | 417 | 418 | ## Neopixels / Dotstars 419 | 420 | ### Light each LED in order 421 | 422 | You can access each LED with Python array methods on the `leds` object. 423 | And you can set the LED color with either an RGB tuple (`(255,0,80)`) or an 424 | RGB hex color as a 24-bit number (`0xff0050`) 425 | 426 | ```py 427 | import time, board, neopixel 428 | 429 | led_pin = board.GP5 # which pin the LED strip is on 430 | num_leds = 10 431 | colors = ( (255,0,0), (0,255,0), (0,0,255), 0xffffff, 0x000000 ) 432 | 433 | leds = neopixel.NeoPixel(led_pin, num_leds, brightness=0.1) 434 | 435 | i = 0 436 | while True: 437 | print("led:",i) 438 | for c in colors: 439 | leds[i] = c 440 | time.sleep(0.2) 441 | i = (i+1) % num_leds 442 | ``` 443 | 444 | ### Moving rainbow on built-in `board.NEOPIXEL` 445 | 446 | In CircuitPython 7, the `rainbowio` module has a `colorwheel()` function. 447 | Unfortunately, the `rainbowio` module is not available in all builds. 448 | In CircuitPython 6, `colorwheel()` is a built-in function part of `_pixelbuf` or `adafruit_pypixelbuf`. 449 | 450 | The `colorwheel()` function takes a single value 0-255 hue and returns an `(R,G,B)` tuple 451 | given a single 0-255 hue. It's not a full HSV_to_RGB() function but often all you need 452 | is "hue to RGB", wher you assume saturation=255 and value=255. 453 | It can be used with `neopixel`, `adafruit_dotstar`, or any place you need a (R,G,B) 3-byte tuple. 454 | Here's one way to use it. 455 | 456 | ```py 457 | # CircuitPython 7 with or without rainbowio module 458 | import time, board, neopixel 459 | try: 460 | from rainbowio import colorwheel 461 | except: 462 | def colorwheel(pos): 463 | if pos < 0 or pos > 255: return (0, 0, 0) 464 | if pos < 85: return (255 - pos * 3, pos * 3, 0) 465 | if pos < 170: pos -= 85; return (0, 255 - pos * 3, pos * 3) 466 | pos -= 170; return (pos * 3, 0, 255 - pos * 3) 467 | 468 | led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.4) 469 | while True: 470 | led.fill( colorwheel((time.monotonic()*50)%255) ) 471 | time.sleep(0.05) 472 | ``` 473 | 474 | 475 | ### Make moving rainbow gradient across LED strip 476 | 477 | See [demo of it in this tweet](https://twitter.com/todbot/status/1397992493833097218). 478 | 479 | ```py 480 | import time, board, neopixel, rainbowio 481 | num_leds = 16 482 | leds = neopixel.NeoPixel(board.D2, num_leds, brightness=0.4, auto_write=False ) 483 | delta_hue = 256//num_leds 484 | speed = 10 # higher numbers = faster rainbow spinning 485 | i=0 486 | while True: 487 | for l in range(len(leds)): 488 | leds[l] = rainbowio.colorwheel( int(i*speed + l * delta_hue) % 255 ) 489 | leds.show() # only write to LEDs after updating them all 490 | i = (i+1) % 255 491 | time.sleep(0.05) 492 | ``` 493 | 494 | A shorter version using a Python list comprehension. The `leds[:]` trick is a way to assign 495 | a new list of colors to all the LEDs at once. 496 | 497 | ```py 498 | import supervisor, board, neopixel, rainbowio 499 | num_leds = 16 500 | speed = 10 # lower is faster, higher is slower 501 | leds = neopixel.NeoPixel(board.D2, 16, brightness=0.4) 502 | while True: 503 | t = supervisor.ticks_ms() / speed 504 | leds[:] = [rainbowio.colorwheel( t + i*(255/len(leds)) ) for i in range(len(leds))] 505 | 506 | ``` 507 | 508 | ### Fade all LEDs by amount for chase effects 509 | ```py 510 | import time 511 | import board, neopixel 512 | num_leds = 16 513 | leds = neopixel.NeoPixel(board.D2, num_leds, brightness=0.4, auto_write=False ) 514 | my_color = (55,200,230) 515 | dim_by = 20 # dim amount, higher = shorter tails 516 | pos = 0 517 | while True: 518 | leds[pos] = my_color 519 | leds[:] = [[max(i-dim_by,0) for i in l] for l in leds] # dim all by (dim_by,dim_by,dim_by) 520 | pos = (pos+1) % num_leds # move to next position 521 | leds.show() # only write to LEDs after updating them all 522 | time.sleep(0.05) 523 | ``` 524 | 525 | ## Audio 526 | 527 | If you're used to Arduino, making sound was mostly constrained to simple beeps 528 | using the Arduino `tone()` function. You can do that in CircuitPython too with 529 | `pwmio` and `simpleio`, but CircuitPython can also play WAV and MP3 530 | files and become a [fully-fledged audio synthesizer with `synthio`](https://github.com/todbot/circuitpython-synthio-tricks). 531 | 532 | In CircuitPython, there are multiple core module libraries available to output audio: 533 | 534 | - `pwmio` -- use almost any GPIO pin to output simple beeps, no WAV/MP3/synthio 535 | - `audioio` -- uses built-in DAC to output WAV, MP3, synthio 536 | - `audiopwmio` -- like above, but uses PWM like arduino `analogWrite()`, requires RC filter to convert to analog 537 | - `audiobusio` -- outputs high-quality I2S audio data stream, requires external I2S decoder hardware 538 | 539 | Different devices will have different audio modules available. Generally, the 540 | pattern is: 541 | 542 | - SAMD51 (e.g. "M4" boards) -- `audioio` (DAC) and `audiobusio` (I2S) 543 | - RP2040 (e.g. Pico) -- `audiopwmio` (PWM) and `audiobusio` (I2S) 544 | - ESP32 (e.g. QTPy ESP32) -- `audiobusio` (I2S) only 545 | 546 | To play WAV and MP3 files, they usually must be resaved in a format parsable by CircuitPython, 547 | see [Preparing Audio Files for CircuitPython](#preparing-audio-files-for-circuitpython) 548 | 549 | ### Making simple tones 550 | 551 | For devices that only have `pwmio` capability, you can make simple tones. 552 | The [`simpleio`](https://docs.circuitpython.org/projects/simpleio/en/latest/examples.html#id1) library can be used for this: 553 | 554 | ```py 555 | # a short piezo song using tone() 556 | import time, board, simpleio 557 | while True: 558 | for f in (262, 294, 330, 349, 392, 440, 494, 523): 559 | simpleio.tone(board.A0, f, 0.25) 560 | time.sleep(1) 561 | ``` 562 | 563 | ### Play a WAV file 564 | 565 | WAV files are easiest for CircuitPython to play. 566 | The shortest code to play a WAV file on Pico RP2040 is: 567 | 568 | ```py 569 | import time, board, audiocore, audiopwmio 570 | audio = audiopwmio.PWMAudioOut(board.GP0) 571 | wave = audiocore.WaveFile("laser2.wav") 572 | audio.play(wave) 573 | while True: 574 | pass # wait for audio to finish playing 575 | ``` 576 | Details and other ways below. 577 | 578 | ### Audio out using PWM 579 | 580 | This uses the `audiopwmio` library, only available for RP2040 boards like Raspberry Pi Pico and NRF52840-based boards like Adafruit Feather nRF52840 Express. 581 | On RP2040-based boards, any pin can be PWM Audio pin. 582 | See the [audiopwomio Support Matrix](https://circuitpython.readthedocs.io/en/latest/shared-bindings/support_matrix.html?filter=audiopwmio) for which boards support `audiopwmio`. 583 | 584 | ```py 585 | import time, board 586 | from audiocore import WaveFile 587 | from audiopwmio import PWMAudioOut as AudioOut 588 | wave = WaveFile("laser2.wav") # can also be filehandle from open() 589 | audio = AudioOut(board.GP0) # must be PWM-capable pin 590 | while True: 591 | print("audio is playing:",audio.playing) 592 | if not audio.playing: 593 | audio.play(wave) 594 | wave.sample_rate = int(wave.sample_rate * 0.90) # play 10% slower each time 595 | time.sleep(0.1) 596 | ``` 597 | 598 | Notes: 599 | 600 | - There will be a small *pop* when audio starts playing as the PWM driver 601 | takes the GPIO line from not being driven to being PWM'ed. 602 | There's currently no way around this. If playing multiple WAVs, consider using 603 | [`AudioMixer`](#use-audiomixer-to-prevent-audio-crackles) to keep the audio system 604 | running between WAVs. This way, you'll only have the startup pop. 605 | 606 | - If you want *stereo* output on boards that support it 607 | then you can pass in two pins, like: 608 | `audio = audiopwmio.PWMAudioOut(left_channel=board.GP14, right_channel=board.GP15)` 609 | 610 | - PWM output must be filtered and converted to line-level to be usable. 611 | Use an RC circuit to accomplish this, see [this simple circuit](https://github.com/todbot/circuitpython-tricks/blob/main/larger-tricks/docs/breakbeat_sampleplayer_wiring.png) or [this twitter thread for details](https://twitter.com/todbot/status/1403451581593374720). 612 | 613 | - The `WaveFile()` object can take either a filestream 614 | (the output of `open('filewav','rb')`) or can take a string filename (`wav=WaveFile("laser2.wav")`). 615 | 616 | 617 | ### Audio out using DAC 618 | 619 | Some CircuitPython boards (SAMD51 "M4" & SAMD21 "M0") have built-in DACs that are supported. 620 | The code is the same as above, with just the import line changing. 621 | See the [audioio Support Matrix](https://circuitpython.readthedocs.io/en/latest/shared-bindings/support_matrix.html?filter=audioio) for which boards support `audioio`. 622 | 623 | ```py 624 | import time, board 625 | import audiocore, audioio # DAC 626 | wave_file = open("laser2.wav", "rb") 627 | wave = audiocore.WaveFile(wave_file) 628 | audio = audioio.AudioOut(board.A0) # must be DAC-capable pin, A0 on QTPy Haxpress 629 | while True: 630 | print("audio is playing:",audio.playing) 631 | if not audio.playing: 632 | audio.play(wave) 633 | wave.sample_rate = int(wave.sample_rate * 0.90) # play 10% slower each time 634 | time.sleep(0.1) 635 | ``` 636 | 637 | Note: if you want *stereo* output on boards that support it (SAMD51 "M4" mostly), 638 | then you can pass in two pins, like: 639 | `audio = audioio.AudioOut(left_channel=board.A0, right_channel=board.A1)` 640 | 641 | 642 | ### Audio out using I2S 643 | 644 | Unlike PWM or DAC, most CircuitPython boards support driving an external I2S audio board. 645 | This will also give you higher-quality sound output than DAC or PWM. 646 | See the [audiobusio Support Matrix](https://circuitpython.readthedocs.io/en/latest/shared-bindings/support_matrix.html?filter=audiobusio) for which boards support `audiobusio`. 647 | 648 | ```py 649 | # for e.g. Pico RP2040 pins bit_clock & word_select pins must be adjacent 650 | import board, audiobusio, audiocore 651 | audio = audiobusio.I2SOut(bit_clock=board.GP0, word_select=board.GP1, data=board.GP2) 652 | audio.play( audiocore.WaveFile("laser2.wav") ) 653 | ``` 654 | 655 | ### Use audiomixer to prevent audio crackles 656 | 657 | The default buffer used by the audio system is quite small. 658 | This means you'll hear corrupted audio if CircuitPython is doing anything else 659 | (having CIRCUITPY written to, updating a display). To get around this, you can 660 | use `audiomixer` to make the audio buffer larger. Try `buffer_size=2048` to start. 661 | A larger buffer means a longer lag between when a sound is triggered when its heard. 662 | 663 | AudioMixer is also great if you want to play multiple WAV files at the same time. 664 | 665 | ```py 666 | import time, board 667 | from audiocore import WaveFile 668 | from audioio import AudioOut 669 | import audiomixer 670 | wave = WaveFile("laser2.wav", "rb") 671 | audio = AudioOut(board.A0) # assuming QTPy M0 or Itsy M4 672 | mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1, 673 | bits_per_sample=16, samples_signed=True, buffer_size=2048) 674 | audio.play(mixer) # never touch "audio" after this, use "mixer" 675 | while True: 676 | print("mixer voice is playing:", mixer.voice[0].playing) 677 | if not mixer.voice[0].playing: 678 | time.sleep(1) 679 | print("playing again") 680 | mixer.voice[0].play(wave) 681 | time.sleep(0.1) 682 | ``` 683 | 684 | ### Play multiple sounds with audiomixer 685 | 686 | This example assumes WAVs that are mono 22050 Hz sample rate, w/ signed 16-bit samples. 687 | 688 | ```py 689 | import time, board, audiocore, audiomixer 690 | from audiopwmio import PWMAudioOut as AudioOut 691 | 692 | wav_files = ("loop1.wav", "loop2.wav", "loop3.wav") 693 | wavs = [None] * len(wav_files) # holds the loaded WAVs 694 | 695 | audio = AudioOut(board.GP2) # RP2040 example 696 | mixer = audiomixer.Mixer(voice_count=len(wav_files), sample_rate=22050, channel_count=1, 697 | bits_per_sample=16, samples_signed=True, buffer_size=2048) 698 | audio.play(mixer) # attach mixer to audio playback 699 | 700 | for i in range(len(wav_files)): 701 | print("i:",i) 702 | wavs[i] = audiocore.WaveFile(open(wav_files[i], "rb")) 703 | mixer.voice[i].play( wavs[i], loop=True) # start each one playing 704 | 705 | while True: 706 | print("doing something else while all loops play") 707 | time.sleep(1) 708 | ``` 709 | 710 | Note: M0 boards do not have `audiomixer` 711 | 712 | Note: Number of simultaneous sounds is limited sample rate and flash read speed. 713 | Rules of thumb: 714 | - Built-in flash: 10 22kHz sounds simultanously 715 | - SPI SD cards: 2 22kHz sounds simultaneously 716 | 717 | 718 | Also see the many examples in [larger-tricks](./larger-tricks/). 719 | 720 | ### Playing MP3 files 721 | 722 | Once you have set up audio output (either directly or via AudioMixer), you can play WAVs or 723 | MP3s through it, or play both simultaneously. 724 | 725 | For instance, here's an example that uses an I2SOut to a PCM5102 on a Raspberry Pi Pico RP2040 726 | to simultaneously play both a WAV and an MP3: 727 | 728 | ```py 729 | import board, audiobusio, audiocore, audiomp3 730 | num_voices = 2 731 | 732 | i2s_bclk, i2s_wsel, i2s_data = board.GP9, board.GP10, board.GP11 # BCLK, LCLK, DIN on PCM5102 733 | 734 | audio = audiobusio.I2SOut(bit_clock=i2s_bclk, word_select=i2s_wsel, data=i2s_data) 735 | mixer = audiomixer.Mixer(voice_count=num_voices, sample_rate=22050, channel_count=1, 736 | bits_per_sample=16, samples_signed=True) 737 | audio.play(mixer) # attach mixer to audio playback 738 | 739 | wav_file = "/amen1_22k_s16.wav" # in 'circuitpython-tricks/larger-tricks/breakbeat_wavs' 740 | mp3_file = "/vocalchops476663_22k_128k.mp3" # in 'circuitpython-tricks/larger-tricks/wav' 741 | # https://freesound.org/people/f-r-a-g-i-l-e/sounds/476663/ 742 | 743 | wave = audiocore.WaveFile(open(wav_file, "rb")) 744 | mp3 = audiomp3.MP3Decoder(open(mp3_file, "rb")) 745 | mixer.voice[0].play( wave ) 746 | mixer.voice[1].play( mp3 ) 747 | 748 | while True: 749 | pass # both audio files play 750 | 751 | ``` 752 | 753 | __Note:__ For MP3 files, be aware that since this is doing software MP3 decoding, 754 | you will likely need to re-encode the MP3s to lower bitrate and sample rate 755 | (max 128 kbps and 22,050 Hz) to be playable the lower-end CircuitPython devices 756 | like the Pico / RP2040. 757 | 758 | __Note:__ For MP3 files and setting `loop=True` when playing, there is a small delay 759 | when looping. WAV files loop seemlessly. 760 | 761 | 762 | 763 | An example of boards with `pwmio` but no audio are ESP32-S2-based boards like 764 | [FunHouse](https://www.adafruit.com/product/4985), 765 | where you cannot play WAV files, but you can make beeps. 766 | A larger example is this gist: https://gist.github.com/todbot/f35bb5ceed013a277688b2ca333244d5 767 | 768 | 769 | ## USB 770 | 771 | 772 | ### Rename CIRCUITPY drive to something new 773 | 774 | For instance, if you have multiple of the same device. 775 | The `label` can be up to 11 characters. 776 | This goes in `boot.py` not `code.py` and you must powercycle board. 777 | 778 | ```py 779 | # this goes in boot.py not code.py! 780 | new_name = "TRINKEYPY0" 781 | import storage 782 | storage.remount("/", readonly=False) 783 | m = storage.getmount("/") 784 | m.label = new_name 785 | storage.remount("/", readonly=True) 786 | ``` 787 | 788 | ### Detect if USB is connected or not 789 | 790 | ```py 791 | import supervisor 792 | if supervisor.runtime.usb_connected: 793 | led.value = True # USB 794 | else: 795 | led.value = False # no USB 796 | ``` 797 | 798 | An older way that tries to mount CIRCUITPY read-write and if it fails, USB connected: 799 | 800 | ```py 801 | def is_usb_connected(): 802 | import storage 803 | try: 804 | storage.remount('/', readonly=False) # attempt to mount readwrite 805 | storage.remount('/', readonly=True) # attempt to mount readonly 806 | except RuntimeError as e: 807 | return True 808 | return False 809 | is_usb = "USB" if is_usb_connected() else "NO USB" 810 | print("USB:", is_usb) 811 | ``` 812 | 813 | ### Get CIRCUITPY disk size and free space 814 | ```py 815 | import os 816 | fs_stat = os.statvfs('/') 817 | print("Disk size in MB", fs_stat[0] * fs_stat[2] / 1024 / 1024) 818 | print("Free space in MB", fs_stat[0] * fs_stat[3] / 1024 / 1024) 819 | ``` 820 | 821 | ### Programmatically reset to UF2 bootloader 822 | ```py 823 | import microcontroller 824 | microcontroller.on_next_reset(microcontroller.RunMode.UF2) 825 | microcontroller.reset() 826 | ``` 827 | 828 | Note: in older CircuitPython use `RunMode.BOOTLOADER` and for boards with multiple 829 | bootloaders (like ESP32-S2): 830 | 831 | ```py 832 | import microcontroller 833 | microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER) 834 | microcontroller.reset() 835 | ``` 836 | 837 | 838 | ## USB Serial 839 | 840 | ### Print to USB Serial 841 | 842 | ```py 843 | print("hello there") # prints a newline 844 | print("waiting...", end='') # does not print newline 845 | for i in range(256): print(i, end=', ') # comma-separated numbers 846 | ``` 847 | 848 | 849 | 850 | ### Read user input from USB Serial, blocking 851 | ```py 852 | while True: 853 | print("Type something: ", end='') 854 | my_str = input() # type and press ENTER or RETURN 855 | print("You entered: ", my_str) 856 | ``` 857 | 858 | ### Read user input from USB Serial, non-blocking (mostly) 859 | ```py 860 | import time 861 | import supervisor 862 | print("Type something when you're ready") 863 | last_time = time.monotonic() 864 | while True: 865 | if supervisor.runtime.serial_bytes_available: 866 | my_str = input() 867 | print("You entered:", my_str) 868 | if time.monotonic() - last_time > 1: # every second, print 869 | last_time = time.monotonic() 870 | print(int(last_time),"waiting...") 871 | ``` 872 | 873 | ### Read keys from USB Serial 874 | ```py 875 | import time, sys, supervisor 876 | print("type charactcers") 877 | while True: 878 | n = supervisor.runtime.serial_bytes_available 879 | if n > 0: # we read something! 880 | s = sys.stdin.read(n) # actually read it in 881 | # print both text & hex version of recv'd chars (see control chars!) 882 | print("got:", " ".join("{:s} {:02x}".format(c,ord(c)) for c in s)) 883 | time.sleep(0.01) # do something else 884 | ``` 885 | 886 | ### Read user input from USB serial, non-blocking 887 | ```py 888 | class USBSerialReader: 889 | """ Read a line from USB Serial (up to end_char), non-blocking, with optional echo """ 890 | def __init__(self): 891 | self.s = '' 892 | def read(self,end_char='\n', echo=True): 893 | import sys, supervisor 894 | n = supervisor.runtime.serial_bytes_available 895 | if n > 0: # we got bytes! 896 | s = sys.stdin.read(n) # actually read it in 897 | if echo: sys.stdout.write(s) # echo back to human 898 | self.s = self.s + s # keep building the string up 899 | if s.endswith(end_char): # got our end_char! 900 | rstr = self.s # save for return 901 | self.s = '' # reset str to beginning 902 | return rstr 903 | return None # no end_char yet 904 | 905 | usb_reader = USBSerialReader() 906 | print("type something and press the end_char") 907 | while True: 908 | mystr = usb_reader.read() # read until newline, echo back chars 909 | #mystr = usb_reader.read(end_char='\t', echo=False) # trigger on tab, no echo 910 | if mystr: 911 | print("got:",mystr) 912 | time.sleep(0.01) # do something time critical 913 | ``` 914 | 915 | ## USB MIDI 916 | 917 | CircuitPython can be a MIDI controller, or respond to MIDI! 918 | Adafruit provides an [`adafruit_midi`](https://github.com/adafruit/Adafruit_CircuitPython_MIDI) 919 | class to make things easier, but it's rather complex for how simple MIDI actually is. 920 | 921 | For outputting MIDI, you can opt to deal with raw `bytearray`s, since most MIDI messages 922 | are just 1,2, or 3 bytes long. For reading MIDI, 923 | you may find [Winterbloom's SmolMIDI](https://github.com/wntrblm/Winterbloom_SmolMIDI) to be faster 924 | to parse MIDI messages, since by design it does less. 925 | 926 | ### Sending MIDI with adafruit_midi 927 | 928 | ```py 929 | import usb_midi 930 | import adafruit_midi 931 | from adafruit_midi.note_on import NoteOn 932 | from adafruit_midi.note_off import NoteOff 933 | midi_out_channel = 3 # human version of MIDI out channel (1-16) 934 | midi = adafruit_midi.MIDI( midi_out=usb_midi.ports[1], out_channel=midi_out_channel-1) 935 | 936 | def play_note(note,velocity=127): 937 | midi.send(NoteOn(note, velocity)) # 127 = highest velocity 938 | time.sleep(0.1) 939 | midi.send(NoteOff(note, 0)) # 0 = lowest velocity 940 | ``` 941 | 942 | Note: This pattern works for sending serial (5-pin) MIDI too, see below 943 | 944 | ### Sending MIDI with bytearray 945 | 946 | Sending MIDI with a lower-level `bytearray` is also pretty easy and 947 | could gain some speed for timing-sensitive applications. 948 | This code is equivalent to the above, without `adafruit_midi` 949 | 950 | ```py 951 | import usb_midi 952 | midi_out = usb_midi.ports[1] 953 | midi_out_channel = 3 # MIDI out channel (1-16) 954 | note_on_status = (0x90 | (midi_out_channel-1)) 955 | note_off_status = (0x80 | (midi_out_channel-1)) 956 | 957 | def play_note(note,velocity=127): 958 | midi_out.write( bytearray([note_on_status, note, velocity]) ) 959 | time.sleep(0.1) 960 | midi_out.write( bytearray([note_off_status, note, 0]) ) 961 | ``` 962 | 963 | ### MIDI over Serial UART 964 | 965 | Not exactly USB, but it is MIDI! 966 | Both `adafruit_midi` and the bytearray technique works for Serial MIDI (aka "5-pin MIDI") too. 967 | With a [simple MIDI out circuit](https://learn.adafruit.com/qt-py-rp2040-usb-to-serial-midi-friends/circuit-diagram) 968 | you can control old hardware synths. 969 | 970 | ```py 971 | import busio 972 | midi_out_channel = 3 # MIDI out channel (1-16) 973 | note_on_status = (0x90 | (midi_out_channel-1)) 974 | note_off_status = (0x80 | (midi_out_channel-1)) 975 | # must pick board pins that are UART TX and RX pins 976 | midi_uart = busio.UART(tx=board.GP16, rx=board.GP17, baudrate=31250) 977 | 978 | def play_note(note,velocity=127): 979 | midi_uart.write( bytearray([note_on_status, note, velocity]) ) 980 | time.sleep(0.1) 981 | midi_uart.write( bytearray([note_off_status, note, 0]) ) 982 | ``` 983 | 984 | ### Receiving MIDI 985 | 986 | ```py 987 | import usb_midi # built-in library 988 | import adafruit_midi # install with 'circup install adafruit_midi' 989 | from adafruit_midi.note_on import NoteOn 990 | from adafruit_midi.note_off import NoteOff 991 | 992 | midi_usb = adafruit_midi.MIDI(midi_in=usb_midi.ports[0]) 993 | while True: 994 | msg = midi_usb.receive() 995 | if msg: 996 | if isinstance(msg, NoteOn): 997 | print("usb noteOn:",msg.note, msg.velocity) 998 | elif isinstance(msg, NoteOff): 999 | print("usb noteOff:",msg.note, msg.velocity) 1000 | ``` 1001 | Note with `adafruit_midi` you must `import` each kind of MIDI Message you want to handle. 1002 | 1003 | ### Receiving MIDI USB and MIDI Serial UART together 1004 | 1005 | MIDI is MIDI, so you can use either the `midi_uart` or the `usb_midi.ports[]` created above with `adafruit_midi`. 1006 | Here's an example receiving MIDI from both USB and Serial on a QTPy RP2040. 1007 | Note for receiving serial MIDI, you need an appropriate optoisolator input circuit, 1008 | like [this one for QTPys](https://www.denki-oto.com/store/p74/MICROMIDITRS-USB.html#/) 1009 | or [this one for MacroPad RP2040](https://www.tindie.com/products/todbot/macropadsynthplug-turn-rp2040-into-a-synth/). 1010 | 1011 | ```py 1012 | import board, busio 1013 | import usb_midi # built-in library 1014 | import adafruit_midi # install with 'circup install adafruit_midi' 1015 | from adafruit_midi.note_on import NoteOn 1016 | from adafruit_midi.note_off import NoteOff 1017 | 1018 | uart = busio.UART(tx=board.TX, rx=board.RX, baudrate=31250, timeout=0.001) 1019 | midi_usb = adafruit_midi.MIDI( midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1] ) 1020 | midi_serial = adafruit_midi.MIDI( midi_in=uart, midi_out=uart ) 1021 | 1022 | while True: 1023 | msg = midi_usb.receive() 1024 | if msg: 1025 | if isinstance(msg, NoteOn): 1026 | print("usb noteOn:",msg.note, msg.velocity) 1027 | elif isinstance(msg, NoteOff): 1028 | print("usb noteOff:",msg.note, msg.velocity) 1029 | msg = midi_serial.receive() 1030 | if msg: 1031 | if isinstance(msg, NoteOn): 1032 | print("serial noteOn:",msg.note, msg.velocity) 1033 | elif isinstance(msg, NoteOff): 1034 | print("serial noteOff:",msg.note, msg.velocity) 1035 | ``` 1036 | 1037 | If you don't care about the source of the MIDI messages, you can combine 1038 | the two if blocks using the "walrus operator" (`:=`) 1039 | 1040 | ```py 1041 | while True: 1042 | while msg := midi_usb.receive() or midi_uart.receive(): 1043 | if isinstance(msg, NoteOn) and msg.velocity != 0: 1044 | note_on(msg.note, msg.velocity) 1045 | elif isinstance(msg,NoteOff) or isinstance(msg,NoteOn) and msg.velocity==0: 1046 | note_off(msg.note, msg.velocity) 1047 | ``` 1048 | 1049 | 1050 | ### Enable USB MIDI in boot.py (for ESP32-S2 and STM32F4) 1051 | 1052 | Some CircuitPython devices like ESP32-S2 based ones, do not have enough 1053 | [USB endpoints to enable all USB functions](https://learn.adafruit.com/customizing-usb-devices-in-circuitpython/how-many-usb-devices-can-i-have), so USB MIDI is disabled by default. 1054 | To enable it, the easiest is to disable USB HID (keyboard/mouse) support. 1055 | This must be done in `boot.py` and the board power cycled. 1056 | 1057 | ```py 1058 | # boot.py 1059 | import usb_hid 1060 | import usb_midi 1061 | usb_hid.disable() 1062 | usb_midi.enable() 1063 | print("enabled USB MIDI, disabled USB HID") 1064 | ``` 1065 | 1066 | 1067 | ## WiFi / Networking 1068 | 1069 | ### Scan for WiFi Networks, sorted by signal strength 1070 | 1071 | Note: this is for boards with native WiFi (ESP32) 1072 | 1073 | ```py 1074 | import wifi 1075 | networks = [] 1076 | for network in wifi.radio.start_scanning_networks(): 1077 | networks.append(network) 1078 | wifi.radio.stop_scanning_networks() 1079 | networks = sorted(networks, key=lambda net: net.rssi, reverse=True) 1080 | for network in networks: 1081 | print("ssid:",network.ssid, "rssi:",network.rssi) 1082 | ``` 1083 | 1084 | ### Join WiFi network with highest signal strength 1085 | 1086 | ```py 1087 | import wifi 1088 | 1089 | def join_best_network(good_networks, print_info=False): 1090 | """join best network based on signal strength of scanned nets""" 1091 | networks = [] 1092 | for network in wifi.radio.start_scanning_networks(): 1093 | networks.append(network) 1094 | wifi.radio.stop_scanning_networks() 1095 | networks = sorted(networks, key=lambda net: net.rssi, reverse=True) 1096 | for network in networks: 1097 | if print_info: print("network:",network.ssid) 1098 | if network.ssid in good_networks: 1099 | if print_info: print("connecting to WiFi:", network.ssid) 1100 | try: 1101 | wifi.radio.connect(network.ssid, good_networks[network.ssid]) 1102 | return True 1103 | except ConnectionError as e: 1104 | if print_info: print("connect error:",e) 1105 | return False 1106 | 1107 | good_networks = {"todbot1":"FiOnTheFly", # ssid, password 1108 | "todbot2":"WhyFlyWiFi",} 1109 | connected = join_best_network(good_networks, print_info=True) 1110 | if connected: 1111 | print("connected!") 1112 | 1113 | ``` 1114 | 1115 | ### Ping an IP address 1116 | 1117 | Note: this is for boards with native WiFi (ESP32) 1118 | 1119 | ```py 1120 | import os 1121 | import time 1122 | import wifi 1123 | import ipaddress 1124 | 1125 | ip_to_ping = "1.1.1.1" 1126 | 1127 | wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), 1128 | password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) 1129 | 1130 | print("my IP addr:", wifi.radio.ipv4_address) 1131 | print("pinging ",ip_to_ping) 1132 | ip1 = ipaddress.ip_address(ip_to_ping) 1133 | while True: 1134 | print("ping:", wifi.radio.ping(ip1)) 1135 | time.sleep(1) 1136 | ``` 1137 | 1138 | ### Get IP address of remote host 1139 | 1140 | ```py 1141 | import os, wifi, socketpool 1142 | 1143 | wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), 1144 | password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) 1145 | print("my IP addr:", wifi.radio.ipv4_address) 1146 | 1147 | hostname = "todbot.com" 1148 | 1149 | pool = socketpool.SocketPool(wifi.radio) 1150 | addrinfo = pool.getaddrinfo(host=hostname, port=443) # port is required 1151 | print("addrinfo", addrinfo) 1152 | 1153 | ipaddr = addrinfo[0][4][0] 1154 | 1155 | print(f"'{hostname}' ip address is '{ipaddr}'") 1156 | ``` 1157 | 1158 | 1159 | ### Fetch a JSON file 1160 | 1161 | Note: this is for boards with native WiFi (ESP32) 1162 | 1163 | ```py 1164 | import os 1165 | import time 1166 | import wifi 1167 | import socketpool 1168 | import ssl 1169 | import adafruit_requests 1170 | 1171 | wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), 1172 | password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) 1173 | print("my IP addr:", wifi.radio.ipv4_address) 1174 | pool = socketpool.SocketPool(wifi.radio) 1175 | session = adafruit_requests.Session(pool, ssl.create_default_context()) 1176 | while True: 1177 | response = session.get("https://todbot.com/tst/randcolor.php") 1178 | data = response.json() 1179 | print("data:",data) 1180 | time.sleep(5) 1181 | ``` 1182 | 1183 | 1184 | ### Serve a webpage via HTTP 1185 | 1186 | Note: this is for boards with native WiFi (ESP32) 1187 | 1188 | The [`adafruit_httpserver`](https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer) library 1189 | makes this pretty easy, and has good examples. You can tell it to either `server.serve_forver()` 1190 | and do all your computation in your `@server.route()` functions, or use `server.poll()` inside a while-loop. 1191 | There is also the [Ampule library](https://github.com/deckerego/ampule). 1192 | 1193 | ```py 1194 | import time, os, wifi, socketpool 1195 | from adafruit_httpserver.server import HTTPServer 1196 | from adafruit_httpserver.response import HTTPResponse 1197 | 1198 | my_port = 1234 # set this to your liking 1199 | wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), 1200 | password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) 1201 | server = HTTPServer(socketpool.SocketPool(wifi.radio)) 1202 | 1203 | @server.route("/") # magic that attaches this function to "server" object 1204 | def base(request): 1205 | my_str = f"

Hello! Current time.monotonic is {time.monotonic()}

" 1206 | return HTTPResponse(body=my_str, content_type="text/html") 1207 | # or for static content: return HTTPResponse(filename="/index.html") 1208 | 1209 | print(f"Listening on http://{wifi.radio.ipv4_address}:{my_port}") 1210 | server.serve_forever(str(wifi.radio.ipv4_address), port=my_port) # never returns 1211 | 1212 | ``` 1213 | 1214 | 1215 | ### Set RTC time from NTP 1216 | 1217 | Note: this is for boards with native WiFi (ESP32) 1218 | 1219 | Note: You need to set `my_tz_offset` to match your region 1220 | 1221 | ```py 1222 | # copied from: 1223 | # https://docs.circuitpython.org/projects/ntp/en/latest/examples.html 1224 | import time, os, rtc 1225 | import socketpool, wifi 1226 | import adafruit_ntp 1227 | 1228 | my_tz_offset = -7 # PDT 1229 | 1230 | wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), 1231 | password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) 1232 | print("Connected, getting NTP time") 1233 | pool = socketpool.SocketPool(wifi.radio) 1234 | ntp = adafruit_ntp.NTP(pool, tz_offset=my_tz_offset) 1235 | 1236 | rtc.RTC().datetime = ntp.datetime 1237 | 1238 | while True: 1239 | print("current datetime:", time.localtime()) 1240 | time.sleep(5) 1241 | ``` 1242 | 1243 | ### Set RTC time from time service 1244 | 1245 | Note: this is for boards with native WiFi (ESP32) 1246 | 1247 | This uses the awesome and free [WorldTimeAPI.org site](http://worldtimeapi.org/pages/examples), 1248 | and this example will fetch the current local time (including timezone and UTC offset) 1249 | based on the geolocated IP address of your device. 1250 | 1251 | ```py 1252 | import time, os, rtc 1253 | import wifi, ssl, socketpool 1254 | import adafruit_requests 1255 | 1256 | wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), 1257 | password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) 1258 | print("Connected, getting WorldTimeAPI time") 1259 | pool = socketpool.SocketPool(wifi.radio) 1260 | request = adafruit_requests.Session(pool, ssl.create_default_context()) 1261 | 1262 | print("Getting current time:") 1263 | response = request.get("http://worldtimeapi.org/api/ip") 1264 | time_data = response.json() 1265 | tz_hour_offset = int(time_data['utc_offset'][0:3]) 1266 | tz_min_offset = int(time_data['utc_offset'][4:6]) 1267 | if (tz_hour_offset < 0): 1268 | tz_min_offset *= -1 1269 | unixtime = int(time_data['unixtime'] + (tz_hour_offset * 60 * 60)) + (tz_min_offset * 60) 1270 | 1271 | print(time_data) 1272 | print("URL time: ", response.headers['date']) 1273 | 1274 | rtc.RTC().datetime = time.localtime( unixtime ) # create time struct and set RTC with it 1275 | 1276 | while True: 1277 | print("current datetime: ", time.localtime()) # time.* now reflects current local time 1278 | time.sleep(5) 1279 | ``` 1280 | 1281 | Also see [this more concise version from @deilers78](https://github.com/todbot/circuitpython-tricks/issues/14#issuecomment-1489181920). 1282 | 1283 | 1284 | ### What the heck is `settings.toml`? 1285 | 1286 | It's a config file that lives next to your `code.py` and is used to store 1287 | WiFi credentials and other global settings. It is also used (invisibly) 1288 | by many Adafruit libraries that do WiFi. 1289 | You can use it (as in the examples above) without those libraries. 1290 | The settings names used by CircuitPython are documented in 1291 | [CircuitPython Web Workflow](https://docs.circuitpython.org/en/latest/docs/workflows.html#web). 1292 | 1293 | Note: You can use any variable names for your WiFI credentials 1294 | (a common pair is `WIFI_SSID` and `WIFI_PASSWORD`), but if you use the 1295 | `CIRCUITPY_WIFI_*` names that will also start up the 1296 | [Web Workflow](https://docs.circuitpython.org/en/latest/docs/workflows.html#web) 1297 | 1298 | 1299 | You use it like this for basic WiFi connectivity: 1300 | 1301 | ```py 1302 | # settings.toml 1303 | CIRCUITPY_WIFI_SSID = "PrettyFlyForAWiFi" 1304 | CIRCUITPY_WIFI_PASSWORD = "mysecretpassword" 1305 | 1306 | # code.py 1307 | import os, wifi 1308 | print("connecting...") 1309 | wifi.radio.connect(ssid=os.getenv('CIRCUITPY_WIFI_SSID'), 1310 | password=os.getenv('CIRCUITPY_WIFI_PASSWORD')) 1311 | print("my IP addr:", wifi.radio.ipv4_address) 1312 | 1313 | ``` 1314 | 1315 | 1316 | ### What the heck is `secrets.py`? 1317 | It's an older version of the `settings.toml` idea. 1318 | You may see older code that uses it. 1319 | 1320 | 1321 | ## Displays (LCD / OLED / E-Ink) and displayio 1322 | 1323 | [displayio](https://circuitpython.readthedocs.io/en/latest/shared-bindings/displayio/#) 1324 | is the native system-level driver for displays in CircuitPython. Several CircuitPython boards 1325 | (FunHouse, MagTag, PyGamer, CLUE) have `displayio`-based displays and a 1326 | built-in `board.DISPLAY` object that is preconfigured for that display. 1327 | Or, you can add your own [I2C](https://circuitpython.readthedocs.io/en/latest/shared-bindings/displayio/#displayio.I2CDisplay) or [SPI](https://circuitpython.readthedocs.io/en/latest/shared-bindings/displayio/#displayio.FourWire) display. 1328 | 1329 | ### Get default display and change display rotation 1330 | 1331 | Boards like FunHouse, MagTag, PyGamer, CLUE have built-in displays. 1332 | `display.rotation` works with all displays, not just built-in ones. 1333 | 1334 | ```py 1335 | import board 1336 | display = board.DISPLAY 1337 | print(display.rotation) # print current rotation 1338 | display.rotation = 0 # valid values 0,90,180,270 1339 | ``` 1340 | 1341 | ### Display an image 1342 | 1343 | __Using `displayio.OnDiskBitmap`__ 1344 | 1345 | CircuitPython has a built-in BMP parser called `displayio.OnDiskBitmap`: 1346 | The images should be in non-compressed, paletized BMP3 format. 1347 | ([how to make BMP3 images](#preparing-images-for-circuitpython)) 1348 | 1349 | ```py 1350 | import board, displayio 1351 | display = board.DISPLAY 1352 | 1353 | maingroup = displayio.Group() # everything goes in maingroup 1354 | display.root_group = maingroup # show our maingroup (clears the screen) 1355 | 1356 | bitmap = displayio.OnDiskBitmap(open("my_image.bmp", "rb")) 1357 | image = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader) 1358 | maingroup.append(image) # shows the image 1359 | ``` 1360 | 1361 | __Using `adafruit_imageload`__ 1362 | 1363 | You can also use the `adafruit_imageload` library that supports slightly more kinds of BMP files, 1364 | (but should still be [paletized BMP3 format](#preparing-images-for-circuitpython) 1365 | as well as paletized PNG and GIF files. Which file format to choose? 1366 | * BMP images are larger but faster to load 1367 | * PNG images are about 2x smaller than BMP and almost as fast to load 1368 | * GIF images are a little bigger than PNG but *much* slower to load 1369 | 1370 | 1371 | ```py 1372 | import board, displayio 1373 | import adafruit_imageload 1374 | display = board.DISPLAY 1375 | maingroup = displayio.Group() # everything goes in maingroup 1376 | display.root_group = maingroup # set the root group to display 1377 | bitmap, palette = adafruit_imageload.load("my_image.png") 1378 | image = displayio.TileGrid(bitmap, pixel_shader=palette) 1379 | maingroup.append(image) # shows the image 1380 | ``` 1381 | 1382 | __How `displayio` is structured__ 1383 | 1384 | CircuitPython's `displayio` library works like: 1385 | - an image `Bitmap` (and its `Palette`) goes inside a `TileGrid` 1386 | - a `TileGrid` goes inside a `Group` 1387 | - a `Group` is shown on a `Display`. 1388 | 1389 | 1390 | 1391 | ### Display background bitmap 1392 | 1393 | Useful for display a solid background color that can be quickly changed. 1394 | 1395 | ```py 1396 | import time, board, displayio 1397 | display = board.DISPLAY # get default display (FunHouse,Pygamer,etc) 1398 | maingroup = displayio.Group() # Create a main group to hold everything 1399 | display.root_group = maingroup # put it on the display 1400 | 1401 | # make bitmap that spans entire display, with 3 colors 1402 | background = displayio.Bitmap(display.width, display.height, 3) 1403 | 1404 | # make a 3 color palette to match 1405 | mypal = displayio.Palette(3) 1406 | mypal[0] = 0x000000 # set colors (black) 1407 | mypal[1] = 0x999900 # dark yellow 1408 | mypal[2] = 0x009999 # dark cyan 1409 | 1410 | # Put background into main group, using palette to map palette ids to colors 1411 | maingroup.append(displayio.TileGrid(background, pixel_shader=mypal)) 1412 | 1413 | time.sleep(2) 1414 | background.fill(2) # change background to dark cyan (mypal[2]) 1415 | time.sleep(2) 1416 | background.fill(1) # change background to dark yellow (mypal[1]) 1417 | ``` 1418 | 1419 | Another way is to use 1420 | [`vectorio`](https://docs.circuitpython.org/en/latest/shared-bindings/vectorio/index.html): 1421 | 1422 | ```py 1423 | import board, displayio, vectorio 1424 | 1425 | display = board.DISPLAY # built-in display 1426 | maingroup = displayio.Group() # a main group that holds everything 1427 | display.root_group = maingroup # put maingroup on the display 1428 | 1429 | mypal = displayio.Palette(1) 1430 | mypal[0] = 0x999900 1431 | background = vectorio.Rectangle(pixel_shader=mypal, width=display.width, height=display.height, x=0, y=0) 1432 | maingroup.append(background) 1433 | ``` 1434 | 1435 | Or can also use 1436 | [`adafruit_display_shapes`](https://docs.circuitpython.org/projects/display-shapes/en/latest/index.html): 1437 | 1438 | ```py 1439 | import board, displayio 1440 | from adafruit_display_shapes.rect import Rect 1441 | 1442 | display = board.DISPLAY 1443 | maingroup = displayio.Group() # a main group that holds everything 1444 | display.root_group = maingroup # add it to display 1445 | 1446 | background = Rect(0,0, display.width, display.height, fill=0x000000 ) # background color 1447 | maingroup.append(background) 1448 | ``` 1449 | 1450 | ### Image slideshow 1451 | 1452 | ```py 1453 | import time, board, displayio 1454 | import adafruit_imageload 1455 | 1456 | display = board.DISPLAY # get display object (built-in on some boards) 1457 | screen = displayio.Group() # main group that holds all on-screen content 1458 | display.root_group = screen # add it to display 1459 | 1460 | file_names = [ '/images/cat1.bmp', '/images/cat2.bmp' ] # list of filenames 1461 | 1462 | screen.append(displayio.Group()) # placeholder, will be replaced w/ screen[0] below 1463 | while True: 1464 | for fname in file_names: 1465 | image, palette = adafruit_imageload.load(fname) 1466 | screen[0] = displayio.TileGrid(image, pixel_shader=palette) 1467 | time.sleep(1) 1468 | ``` 1469 | 1470 | __Note:__ Images must be in palettized BMP3 format. 1471 | For more details, see [Preparing images for CircuitPython](#preparing-images-for-circuitpython) 1472 | 1473 | 1474 | ### Dealing with E-Ink "Refresh Too Soon" error 1475 | 1476 | E-Ink displays are damaged if refreshed too frequently. 1477 | CircuitPython enforces this, but also provides `display.time_to_refresh`, 1478 | the number of seconds you need to wait before the display can be refreshed. 1479 | One solution is to sleep a little longer than that and you'll never get the error. 1480 | Another would be to wait for `time_to_refresh` to go to zero, as show below. 1481 | 1482 | ```py 1483 | import time, board, displayio, terminalio 1484 | from adafruit_display_text import label 1485 | mylabel = label.Label(terminalio.FONT, text="demo", x=20,y=20, 1486 | background_color=0x000000, color=0xffffff ) 1487 | display = board.DISPLAY # e.g. for MagTag 1488 | display.root_group = mylabel 1489 | while True: 1490 | if display.time_to_refresh == 0: 1491 | display.refresh() 1492 | mylabel.text = str(time.monotonic()) 1493 | time.sleep(0.1) 1494 | ``` 1495 | 1496 | ### Turn off REPL on built-in display 1497 | 1498 | If you have a board with a built-in display (like Feather TFT, Cardputer, FunHouse, etc), 1499 | CircuitPython will set up the display for you and print the REPL to it. 1500 | But if you want a more polished look for your project, you can turn off the REPL 1501 | from printing on the built-in display by putting this at at the top of both 1502 | your `boot.py` and `code.py` 1503 | 1504 | ```py 1505 | # put at top of both boot.py & code.py 1506 | import board 1507 | board.DISPLAY.root_group = None 1508 | ``` 1509 | 1510 | 1511 | ## I2C 1512 | 1513 | ### Scan I2C bus for devices 1514 | from: 1515 | [CircuitPython I2C Guide: Find Your Sensor](https://learn.adafruit.com/circuitpython-essentials/circuitpython-i2c#find-your-sensor-2985153-11) 1516 | 1517 | ```py 1518 | import board 1519 | i2c = board.I2C() # or busio.I2C(pin_scl,pin_sda) 1520 | while not i2c.try_lock(): pass 1521 | print("I2C addresses found:", [hex(device_address) 1522 | for device_address in i2c.scan()]) 1523 | i2c.unlock() 1524 | ``` 1525 | 1526 | One liner to copy-n-paste into REPL for quicky I2C scan: 1527 | 1528 | ```py 1529 | import board; i2c=board.I2C(); i2c.try_lock(); [hex(a) for a in i2c.scan()]; i2c.unlock() 1530 | ``` 1531 | 1532 | ### Speed up I2C bus 1533 | 1534 | CircuitPython defaults to 100 kHz I2C bus speed. This will work for all devices, 1535 | but some devices can go faster. Common faster speeds are 200 kHz and 400 kHz. 1536 | 1537 | ```py 1538 | import board 1539 | import busio 1540 | # instead of doing 1541 | # i2c = board.I2C() 1542 | i2c = busio.I2C( board.SCL, board.SDA, frequency=200_000) 1543 | # then do something with 'i2c' object as before, like: 1544 | oled = adafruit_ssd1306.SSD1306_I2C(width=128, height=32, i2c=i2c) 1545 | ``` 1546 | 1547 | ## Timing 1548 | 1549 | ### Measure how long something takes 1550 | 1551 | Generally use `time.monotonic()` to get the current "uptime" of a board in fractional seconds. 1552 | So to measure the duration it takes CircuitPython to do something like: 1553 | 1554 | ```py 1555 | import time 1556 | start_time = time.monotonic() 1557 | # put thing you want to measure here, like: 1558 | import neopixel 1559 | stop_time = time.monotonic() 1560 | print("elapsed time = ", stop_time - start_time) 1561 | ``` 1562 | 1563 | Note that on the "small" versions of CircuitPython in the QT Py M0, Trinket M0, etc., 1564 | the floating point value of seconds will become less accurate as uptime increases. 1565 | 1566 | ### More accurate timing with `ticks_ms()`, like Arduino `millis()` 1567 | 1568 | If you want something more like Arduino's `millis()` function, the `supervisor.ticks_ms()` 1569 | function returns an integer, not a floating point value. It is more useful for sub-second 1570 | timing tasks and you can still convert it to floating-point seconds for human consumption. 1571 | 1572 | ```py 1573 | import supervisor 1574 | start_msecs = supervisor.ticks_ms() 1575 | import neopixel 1576 | stop_msecs = supervisor.ticks_ms() 1577 | print("elapsed time = ", (stop_msecs - start_msecs)/1000) 1578 | ``` 1579 | 1580 | ### Control garbage collection for reliable timing 1581 | 1582 | The CircuitPython garbage collector makes it so you don't have to deal with 1583 | memory allocations like you do in languages like C/C++. But when it runs, it can pause your 1584 | program for tens of milliseconds in some cases. For timing-sensitive applications, 1585 | you can exhibit some control over the garbage collector so it only runs when 1586 | you want it (like in the "shadow" after a timing-critical event) 1587 | 1588 | Here's one way to do this. 1589 | ```py 1590 | import gc 1591 | from adafruit_ticks import ticks_ms, ticks_add, ticks_less 1592 | gc.collect() # collect any garbage before we... 1593 | gc.disable() # disable automatic garbage collection 1594 | dmillis = 10 # how many millis between explicit gc 1595 | deadline = ticks_add(ticks_ms(), dmillis) 1596 | while True: 1597 | now = ticks_ms() 1598 | if ticks_diff(now, deadline) >= 0: 1599 | deadline = ticks_add(now, dmillis) 1600 | gc.collect() # explicitly run a garbage collection cycle 1601 | # do timing-sensitive thing here 1602 | ``` 1603 | 1604 | ### Converting milliseconds to seconds: 0.004 * 1000 != 4, sometimes 1605 | 1606 | When doing timing-sensitive tasks in CircuitPython, you may have code that looks like this (say from the above): 1607 | ```py 1608 | from adafruit_ticks import ticks_ms, ticks_add, ticks_less 1609 | 1610 | check_secs = 0.004 # check_secs is seconds between checks 1611 | check_millis = check_secs * 1000 # convert to millis 1612 | deadline = ticks_add(ticks_ms(), check_millis) 1613 | while True: 1614 | now = ticks_ms() 1615 | if ticks_less(now,deadline) >= 0: 1616 | deadline = ticks_add(now, check_millis) 1617 | do_periodic_task() # do timing-critical thing every 'check_secs' 1618 | ``` 1619 | This seems more accurate than using `time.monotonic()` since it's using the millisecond-accurate `supervisor.ticks_ms` property, the timing resolution of CircuitPython. 1620 | 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 1621 | 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)](https://learn.adafruit.com/welcome-to-circuitpython/frequently-asked-questions#faq-3129274) ([further discusion here](https://github.com/adafruit/circuitpython/issues/9237)). 1622 | In short: stick to integer milliseconds. 1623 | 1624 | 1625 | ## Board Info 1626 | 1627 | ### Get CPU speed (and set it!) 1628 | 1629 | CircuitPython provides a way to get the microcontroller's CPU frequency with 1630 | [`microcontroller.cpu.frequency`](https://docs.circuitpython.org/en/latest/shared-bindings/microcontroller/index.html#microcontroller.Processor.frequency). 1631 | 1632 | ```py 1633 | import microcontroller 1634 | print("CPU speed:", microcontroller.cpu.frequency) 1635 | ``` 1636 | 1637 | On some chips (most notably Pico P2040), you can also **set** this value. Overclock your Pico! 1638 | It's safe to double the RP2040 speed, Raspberry Pi officially supports up to 200 MHz. 1639 | CircuitPython will adjust its internal timings, but you should 1640 | do this change before creating any peripheral objects like UARTs or displays. 1641 | 1642 | ```py 1643 | import microcontroller 1644 | microcontroller.cpu.frequency = 250_000_000 # run at 250 MHz instead of 125 MHz 1645 | ``` 1646 | 1647 | 1648 | ### Display amount of free RAM 1649 | 1650 | from: https://learn.adafruit.com/welcome-to-circuitpython/frequently-asked-questions 1651 | ```py 1652 | import gc 1653 | print( gc.mem_free() ) 1654 | ``` 1655 | 1656 | ### Show microcontroller.pin to board mappings 1657 | from https://gist.github.com/anecdata/1c345cb2d137776d76b97a5d5678dc97 1658 | ```py 1659 | 1660 | import microcontroller 1661 | import board 1662 | 1663 | for pin in dir(microcontroller.pin): 1664 | if isinstance(getattr(microcontroller.pin, pin), microcontroller.Pin): 1665 | print("".join(("microcontroller.pin.", pin, "\t")), end=" ") 1666 | for alias in dir(board): 1667 | if getattr(board, alias) is getattr(microcontroller.pin, pin): 1668 | print("".join(("", "board.", alias)), end=" ") 1669 | print() 1670 | ``` 1671 | 1672 | ### Determine which board you're on 1673 | 1674 | ```py 1675 | import os 1676 | print(os.uname().machine) 1677 | 'Adafruit ItsyBitsy M4 Express with samd51g19' 1678 | ``` 1679 | 1680 | Another way is the `board.board_id`. This is the "port" name used when 1681 | compiling CircuitPython. To find a list of valid board IDs, 1682 | you can look in the circuitpython core repo inside of: "ports/[some_port]/boards/". 1683 | i.e. for espressif boards find the list of directories in: 1684 | [ports/espressif/boards/](https://github.com/adafruit/circuitpython/tree/main/ports/espressif/boards) 1685 | The "board_id is used as the argument for 1686 | `circuitpython_setboard` in [`circuitpython-stubs`](https://pypi.org/project/circuitpython-stubs/) 1687 | 1688 | ```py 1689 | import board 1690 | print(board.board_id) 1691 | 'raspberry_pi_pico' 1692 | ``` 1693 | 1694 | To get the chip family: 1695 | 1696 | ```py 1697 | import os 1698 | print(os.uname().sysname) 1699 | 'ESP32S2' 1700 | ``` 1701 | 1702 | ### Support multiple boards with one `code.py` 1703 | ```py 1704 | import os 1705 | board_type = os.uname().machine 1706 | if 'QT Py M0' in board_type: 1707 | tft_clk = board.SCK 1708 | tft_mosi = board.MOSI 1709 | spi = busio.SPI(clock=tft_clk, MOSI=tft_mosi) 1710 | elif 'ItsyBitsy M4' in board_type: 1711 | tft_clk = board.SCK 1712 | tft_mosi = board.MOSI 1713 | spi = busio.SPI(clock=tft_clk, MOSI=tft_mosi) 1714 | elif 'Pico' in board_type: 1715 | tft_clk = board.GP10 # must be a SPI CLK 1716 | tft_mosi= board.GP11 # must be a SPI TX 1717 | spi = busio.SPI(clock=tft_clk, MOSI=tft_mosi) 1718 | else: 1719 | print("unsupported board", board_type) 1720 | ``` 1721 | 1722 | 1723 | ## Computery Tasks 1724 | 1725 | ### Formatting strings 1726 | ```py 1727 | name = "John" 1728 | fav_color = 0x003366 1729 | body_temp = 98.65 1730 | fav_number = 123 1731 | print("name:%s color:%06x temp:%2.1f num:%d" % (name,fav_color,body_temp,fav_number)) 1732 | # 'name:John color:ff3366 temp:98.6 num:123' 1733 | ``` 1734 | 1735 | ### Formatting strings with f-strings 1736 | (doesn't work on 'small' CircuitPythons like QTPy M0) 1737 | 1738 | ```py 1739 | name = "John" 1740 | fav_color = 0xff3366 1741 | body_temp = 98.65 1742 | fav_number = 123 1743 | print(f"name:{name} color:{fav_color:06x} temp:{body_temp:2.1f} num:{fav_number}") 1744 | # 'name:John color:ff3366 temp:98.6 num:123' 1745 | ``` 1746 | 1747 | ### Using regular expressions to "findall" strings 1748 | 1749 | Regular expressions are a really powerful way to match information in and parse data 1750 | from strings. While CircuitPython has a version of the `re` regex module you may know 1751 | from desktop Python, it is very limited. Specifcally it doesn't have the very useful 1752 | `re.findall()` function. Below is a semi-replacement for `findall()`. 1753 | 1754 | ```py 1755 | import re 1756 | def find_all(regex, some_str): 1757 | matches = [] 1758 | while m := regex.search(some_str): 1759 | matches.append( m.groups() ) 1760 | some_str = some_str[ m.end(): ] # get past match 1761 | return matches 1762 | 1763 | my_str = "thing1 I want thing2 I want thing3 I want" 1764 | regex1 = re.compile('(.*?)<\/thing>') 1765 | my_matches = find_all( regex1, my_str ) 1766 | print("matches:", my_matches) 1767 | 1768 | ``` 1769 | 1770 | 1771 | ### Make and use a config file 1772 | 1773 | ```py 1774 | # my_config.py 1775 | config = { 1776 | "username": "Grogu Djarin", 1777 | "password": "ig88rules", 1778 | "secret_key": "3a3d9bfaf05835df69713c470427fe35" 1779 | } 1780 | 1781 | # code.py 1782 | from my_config import config 1783 | print("secret:", config['secret_key']) 1784 | # 'secret: 3a3d9bfaf05835df69713c470427fe35' 1785 | ``` 1786 | 1787 | ### Run different `code.py` on startup 1788 | 1789 | Use `microcontroller.nvm` to store persistent state across 1790 | resets or between `boot.py` and `code.py`, and declare that 1791 | the first byte of `nvm` will be the `startup_mode`. 1792 | Now if you create multiple code.py files (say) `code1.py`, `code2.py`, etc. 1793 | you can switch between them based on `startup_mode`. 1794 | 1795 | ```py 1796 | import time 1797 | import microcontroller 1798 | startup_mode = microcontroller.nvm[0] 1799 | if startup_mode == 1: 1800 | import code1 # runs code in `code1.py` 1801 | if startup_mode == 2: 1802 | import code2 # runs code in `code2.py` 1803 | # otherwise runs 'code.py` 1804 | while True: 1805 | print("main code.py") 1806 | time.sleep(1) 1807 | 1808 | ``` 1809 | 1810 | **Note:** in CircuitPyton 7+ you can use [`supervisor.set_next_code_file()`](https://circuitpython.readthedocs.io/en/latest/shared-bindings/supervisor/index.html#supervisor.set_next_code_file) 1811 | to change which .py file is run on startup. 1812 | This changes only what happens on reload, not hardware reset or powerup. 1813 | Using it would look like: 1814 | ```py 1815 | import supervisor 1816 | supervisor.set_next_code_file('code_awesome.py') 1817 | # and then if you want to run it now, trigger a reload 1818 | supervisor.reload() 1819 | ``` 1820 | 1821 | ## Coding Techniques 1822 | 1823 | ### Map an input range to an output range 1824 | 1825 | ```py 1826 | # simple range mapper, like Arduino map() 1827 | def map_range(s, a1, a2, b1, b2): 1828 | return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) 1829 | 1830 | # example: map 0-0123 value to 0.0-1.0 value 1831 | val = 768 1832 | outval = map_range( val, 0,1023, 0.0,1.0 ) 1833 | # outval = 0.75 1834 | ``` 1835 | 1836 | ### Constrain an input to a min/max 1837 | The Python built-in `min()` and `max()` functions can be used together 1838 | to make something like Arduino's `constrain()` to clamp an input between two values. 1839 | 1840 | ```py 1841 | # constrain a value to be 0-255 1842 | outval = min(max(val, 0), 255) 1843 | # constrain a value to be 0-255 integer 1844 | outval = int(min(max(val, 0), 255)) 1845 | # constrain a value to be -1 to +1 1846 | outval = min(max(val, -1), 1) 1847 | ``` 1848 | 1849 | ### Turn a momentary value into a toggle 1850 | 1851 | ```py 1852 | import touchio 1853 | import board 1854 | 1855 | touch_pin = touchio.TouchIn(board.GP6) 1856 | last_touch_val = False # holds last measurement 1857 | toggle_value = False # holds state of toggle switch 1858 | 1859 | while True: 1860 | touch_val = touch_pin.value 1861 | if touch_val != last_touch_val: 1862 | if touch_val: 1863 | toggle_value = not toggle_value # flip toggle 1864 | print("toggle!", toggle_value) 1865 | last_touch_val = touch_val 1866 | ``` 1867 | 1868 | ### Do something every N seconds without `sleep()` 1869 | 1870 | Also known as "blink-without-delay" 1871 | 1872 | ```py 1873 | import time 1874 | last_time1 = time.monotonic() # holds when we did something #1 1875 | last_time2 = time.monotonic() # holds when we did something #2 1876 | while True: 1877 | if time.monotonic() - last_time1 > 2.0: # every 2 seconds 1878 | last_time1 = time.monotonic() # save when we do the thing 1879 | print("hello!") # do thing #1 1880 | if time.monotonic() - last_time2 > 5.0: # every 5 seconds 1881 | last_time2 = time.monotonic() # save when we do the thing 1882 | print("world!") # do thing #2 1883 | 1884 | ``` 1885 | 1886 | Note: a more accurate of this [uses `ticks_ms()`](#more-accurate-timing-with-ticks_ms-like-arduino-millis) 1887 | and maybe turning off gc / garbage collection. 1888 | 1889 | 1890 | ## System error handling 1891 | 1892 | ### Preventing Ctrl-C from stopping the program 1893 | 1894 | Put a `try`/`except KeyboardInterrupt` to catch the Ctrl-C 1895 | on the inside of your main loop. 1896 | 1897 | ```py 1898 | while True: 1899 | try: 1900 | print("Doing something important...") 1901 | time.sleep(0.1) 1902 | except KeyboardInterrupt: 1903 | print("Nice try, human! Not quitting.") 1904 | ``` 1905 | 1906 | Also useful for graceful shutdown (turning off neopixels, say) on Ctrl-C. 1907 | 1908 | ```py 1909 | import time, random 1910 | import board, neopixel, rainbowio 1911 | leds = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.4 ) 1912 | while True: 1913 | try: 1914 | rgb = rainbowio.colorwheel(int(time.monotonic()*75) % 255) 1915 | leds.fill(rgb) 1916 | time.sleep(0.05) 1917 | except KeyboardInterrupt: 1918 | print("shutting down nicely...") 1919 | leds.fill(0) 1920 | break # gets us out of the while True 1921 | ``` 1922 | 1923 | ### Prevent auto-reload when CIRCUITPY is touched 1924 | 1925 | Normally, CircuitPython restarts anytime the CIRCUITPY drive is written to. 1926 | This is great normally, but is frustrating if you want your code to keep running, 1927 | and you want to control exactly when a restart happens. 1928 | 1929 | ```py 1930 | import supervisor 1931 | supervisor.runtime.autoreload = False # CirPy 8 and above 1932 | #supervisor.disable_autoreload() # CirPy 7 and below 1933 | ``` 1934 | 1935 | To trigger a reload, do a Ctrl-C + Ctrl-D in the REPL or reset your board. 1936 | 1937 | ### Raspberry Pi Pico boot.py Protection 1938 | 1939 | Also works on other RP2040-based boards like QTPy RP2040. 1940 | From https://gist.github.com/Neradoc/8056725be1c209475fd09ffc37c9fad4 1941 | 1942 | Also see [getting into Safe Mode with a REPL one-liner](#useful-repl-one-liners). 1943 | 1944 | ```py 1945 | # Copy this as 'boot.py' in your Pico's CIRCUITPY drive 1946 | # Useful in case Pico locks up (which it's done a few times on me) 1947 | import board 1948 | import time 1949 | from digitalio import DigitalInOut,Pull 1950 | 1951 | led = DigitalInOut(board.LED) 1952 | led.switch_to_output() 1953 | 1954 | safe = DigitalInOut(board.GP14) # <-- choose your button pin 1955 | safe.switch_to_input(Pull.UP) 1956 | 1957 | def reset_on_pin(): 1958 | if safe.value is False: 1959 | import microcontroller 1960 | microcontroller.on_next_reset(microcontroller.RunMode.SAFE_MODE) 1961 | microcontroller.reset() 1962 | 1963 | led.value = False 1964 | for x in range(16): 1965 | reset_on_pin() 1966 | led.value = not led.value # toggle LED on/off as notice 1967 | time.sleep(0.1) 1968 | 1969 | ``` 1970 | 1971 | 1972 | ## Using the REPL 1973 | 1974 | The "serial" REPL is the most useful diagnostic tools in CircuitPython. 1975 | Always have it open when saving your code to see any errors. 1976 | If you use a separate terminal program instead of an IDE, I recommend [`tio`](https://github.com/tio/tio). 1977 | 1978 | ### Display built-in modules / libraries 1979 | ``` 1980 | Adafruit CircuitPython 6.2.0-beta.2 on 2021-02-11; Adafruit Trinket M0 with samd21e18 1981 | >>> help("modules") 1982 | __main__ digitalio pulseio supervisor 1983 | analogio gc pwmio sys 1984 | array math random time 1985 | board microcontroller rotaryio touchio 1986 | builtins micropython rtc usb_hid 1987 | busio neopixel_write storage usb_midi 1988 | collections os struct 1989 | Plus any modules on the filesystem 1990 | ``` 1991 | 1992 | ### Turn off built-in display to speed up REPL printing 1993 | 1994 | By default CircuitPython will echo the REPL to the display of those boards with built-in displays. 1995 | This can slow down the REPL. So one way to speed the REPL up is to hide the `displayio.Group` that 1996 | contains all the REPL output. 1997 | 1998 | ```py 1999 | import board, displayio 2000 | board.DISPLAY.root_group = None # turn off REPL printing 2001 | board.DISPLAY.root_group = displayio.CIRCUITPYTHON_TERMINAL # turn back on REPL printing 2002 | ``` 2003 | 2004 | In CircuitPython 8.x, you could do the below. In 9.x, the `root_group` is read-only 2005 | after it's been assigned. 2006 | 2007 | ```py 2008 | import board 2009 | board.DISPLAY.root_group.hidden = True 2010 | board.DISPLAY.root_group.hidden = False # to turn it back on 2011 | ``` 2012 | 2013 | ### Useful REPL one-liners 2014 | 2015 | (yes, semicolons are legal in Python) 2016 | 2017 | ```py 2018 | # get into Safe Mode if you have REPL access 2019 | import microcontroller; microcontroller.on_next_reset(microcontroller.RunMode.SAFE_MODE); microcontroller.reset() 2020 | 2021 | # load common libraries (for later REPL experiments) 2022 | import time, board, analogio, touchio; from digitalio import DigitalInOut,Pull 2023 | 2024 | # create a pin and set a pin LOW (if you've done the above) 2025 | pin = DigitalInOut(board.GP0); pin.switch_to_output(value=False) 2026 | 2027 | # print out board pins and objects (like `I2C`, `STEMMA_I2C`, `DISPLAY`, if present) 2028 | import board; dir(board) 2029 | 2030 | # print out microcontroller pins (chip pins, not the same as board pins) 2031 | import microcontroller; dir(microcontroller.pin) 2032 | 2033 | # release configured / built-in display 2034 | import displayio; displayio.release_displays() 2035 | 2036 | # turn off auto-reload when CIRCUITPY drive is touched 2037 | import supervisor; supervisor.runtime.autoreload = False 2038 | 2039 | # test neopixel strip, make them all purple 2040 | import board, neopixel; leds = neopixel.NeoPixel(board.GP3, 8, brightness=0.2); leds.fill(0xff00ff) 2041 | leds.deinit() # releases pin 2042 | 2043 | # scan I2C bus 2044 | import board; i2c=board.I2C(); i2c.try_lock(); [hex(a) for a in i2c.scan()]; i2c.unlock() 2045 | 2046 | ``` 2047 | 2048 | 2049 | ## Python tricks 2050 | 2051 | These are general Python tips that may be useful in CircuitPython. 2052 | 2053 | ### Create list with elements all the same value 2054 | 2055 | ```py 2056 | blank_array = [0] * 50 # creats 50-element list of zeros 2057 | ``` 2058 | 2059 | ### Convert RGB tuples to int and back again 2060 | 2061 | Thanks to @Neradoc for this tip: 2062 | 2063 | ```py 2064 | rgb_tuple = (255, 0, 128) 2065 | rgb_int = int.from_bytes(rgb_tuple, byteorder='big') 2066 | 2067 | rgb_int = 0xFF0080 2068 | rgb_tuple2 = tuple((rgb_int).to_bytes(3,"big")) 2069 | 2070 | rgb_tuple2 == rgb_tuple 2071 | ``` 2072 | 2073 | ### Storing multiple values per list entry 2074 | 2075 | Create simple data structures as config to control your program. 2076 | Unlike Arduino, you can store multiple values per list/array entry. 2077 | 2078 | ```py 2079 | mycolors = ( 2080 | # color val, name 2081 | (0x0000FF, "blue"), 2082 | (0x00FFFF, "cyan"), 2083 | (0xFF00FF, "purple"), 2084 | ) 2085 | for i in range(len(mycolors)): 2086 | (val, name) = mycolors[i] 2087 | print("my color ", name, "has the value", val) 2088 | ``` 2089 | 2090 | 2091 | ## Python info 2092 | 2093 | How to get information about Python inside of CircuitPython. 2094 | 2095 | ### Display which (not built-in) libraries have been imported 2096 | ```py 2097 | import sys 2098 | print(sys.modules.keys()) 2099 | # 'dict_keys([])' 2100 | import board 2101 | import neopixel 2102 | import adafruit_dotstar 2103 | print(sys.modules.keys()) 2104 | prints "dict_keys(['neopixel', 'adafruit_dotstar'])" 2105 | ``` 2106 | 2107 | ### List names of all global variables 2108 | ```py 2109 | a = 123 2110 | b = 'hello there' 2111 | my_globals = sorted(dir) 2112 | print(my_globals) 2113 | # prints "['__name__', 'a', 'b']" 2114 | if 'a' in my_globals: 2115 | print("you have a variable named 'a'!") 2116 | if 'c' in my_globals: 2117 | print("you have a variable named 'c'!") 2118 | ``` 2119 | 2120 | ### Display the running CircuitPython release 2121 | 2122 | With an established serial connection, press `Ctrl+c`: 2123 | 2124 | ```sh 2125 | Adafruit CircuitPython 7.1.1 on 2022-01-14; S2Pico with ESP32S2-S2FN4R2 2126 | >>> 2127 | ``` 2128 | 2129 | Without connection or code running, check the `boot_out.txt` file in your CIRCUITPY drive. 2130 | 2131 | ```py 2132 | import os 2133 | print(os.uname().release) 2134 | '7.1.1' 2135 | print(os.uname().version) 2136 | '7.1.1 on 2022-01-14' 2137 | ``` 2138 | 2139 | ## Host-side tasks 2140 | 2141 | Things you might need to do on your computer when using CircuitPython. 2142 | 2143 | ### Installing CircuitPython libraries 2144 | 2145 | The below examples are for MacOS / Linux. Similar commands are used for Windows 2146 | 2147 | #### Installing libraries with `circup` 2148 | 2149 | `circup` can be used to easily install and update modules 2150 | 2151 | ```sh 2152 | $ pip3 install --user circup 2153 | $ circup install adafruit_midi 2154 | $ circup update # updates all modules 2155 | ``` 2156 | 2157 | Freshly update all modules to latest version (e.g. when going from CP 6 -> CP 7) 2158 | (This is needed because `circup update` doesn't actually seem to work reliably) 2159 | 2160 | ```sh 2161 | circup freeze > mymodules.txt 2162 | rm -rf /Volumes/CIRCUITPY/lib/* 2163 | circup install -r mymodules.txt 2164 | ``` 2165 | 2166 | 2167 | And updating circup when a new version of CircuitPython comes out: 2168 | ```sh 2169 | $ pip3 install --upgrade circup 2170 | ``` 2171 | 2172 | 2173 | #### Copying libraries by hand with `cp` 2174 | 2175 | To install libraries by hand from the 2176 | [CircuitPython Library Bundle](https://circuitpython.org/libraries) 2177 | or from the [CircuitPython Community Bundle](https://github.com/adafruit/CircuitPython_Community_Bundle/releases) (which circup doesn't support), get the bundle, unzip it and then use `cp -rX`. 2178 | 2179 | ```sh 2180 | cp -rX bundle_folder/lib/adafruit_midi /Volumes/CIRCUITPY/lib 2181 | ``` 2182 | 2183 | **Note:** on limited-memory boards like Trinkey, Trinket, QTPy, you must use the `-X` option on MacOS 2184 | 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) 2185 | 2186 | 2187 | ### Preparing images for CircuitPython 2188 | 2189 | There's two ways to load images for use with `displayio`: 2190 | * The built-in [`displayio.OnDiskBitmap()`](https://docs.circuitpython.org/en/latest/shared-bindings/displayio/#displayio.OnDiskBitmap) 2191 | * The library [`adafruit_imageload`](https://docs.circuitpython.org/projects/imageload/en/latest/index.html) 2192 | 2193 | `displayio.OnDisBitmap()`: 2194 | * can load "indexed" (aka "palette") non-compressed BMP3 images 2195 | * doesn't load image into RAM (great for TileGrids) 2196 | 2197 | `adafruit_imageload` 2198 | * can load BMP3 images with RLE compression 2199 | * loads entire image into RAM (thus you may run out of memory) 2200 | * an load palette PNG images and GIF images 2201 | * PNG image loading is almost as fast as BMP and uses 1/2 the disk space 2202 | * GIF loading is very slow 2203 | 2204 | To make images load faster generally, you can reduce the number of colors in the image. 2205 | The maximum number of colors is 256, but try reducing colors to 64 or even 2 if 2206 | it's a black-n-white image. 2207 | 2208 | Some existing Learn Guides: 2209 | - https://learn.adafruit.com/creating-your-first-tilemap-game-with-circuitpython/indexed-bmp-graphics 2210 | - https://learn.adafruit.com/preparing-graphics-for-e-ink-displays 2211 | 2212 | And here's some ways to do the conversions. 2213 | 2214 | 2215 | #### Online 2216 | 2217 | Most online image converters do not create BMPs in the proper format: 2218 | BMP3, non-compressed, up to 256 colors in an 8-bit palette. 2219 | 2220 | However @Neradoc found the site [convert2bmp](https://online-converting.com/image/convert2bmp/) 2221 | will work when you set "Color:" mode to "8 (Indexed)". Some initial tests show this works! 2222 | I'd recommend also trying out one of the following techniques too to have finer control. 2223 | 2224 | The site https://cancerberosgx.github.io/magic/playground/ lets you 2225 | use any of the ImageMagick commands below to convert images. It's really handy if you can't 2226 | install ImageMagick locally. 2227 | 2228 | 2229 | #### Command-line: using ImageMagick 2230 | 2231 | [ImageMagick](https://imagemagick.org/) is a command-line image manipulation tool. With it, 2232 | you can convert any image format to BMP3 format. The main ImageMagick CLI command is `convert`. 2233 | 2234 | ```sh 2235 | convert myimage.jpg -resize 240x240 -type palette -colors 64 -compress None BMP3:myimage_for_cirpy.bmp 2236 | ``` 2237 | 2238 | You can also use this technique to create reduced-color palette PNGs: 2239 | 2240 | ```sh 2241 | convert myimage.jpg -resize 240x240 -type palette -colors 64 myimage.png 2242 | ``` 2243 | 2244 | #### Command-line: using GraphicsMagick 2245 | 2246 | [GraphicsMagick](http://www.graphicsmagick.org/) is a slimmer, lower-requirement 2247 | clone of ImageMagick. All GraphicsMagick commands are accessed via the `gm` CLI command. 2248 | 2249 | ```sh 2250 | gm convert myimage.jpg -resize 240x240 -colors 64 -type palette -compress None BMP3:myimage_for_cirpy.bmp 2251 | ``` 2252 | 2253 | #### Making images smaller or for E-Ink displays 2254 | 2255 | To make images smaller (and load faster), reduce number of colors from 256. 2256 | If your image is a monochrome (or for use with E-Ink displays like MagTag), use 2 colors. 2257 | The ["-dither" options](https://legacy.imagemagick.org/Usage/quantize/#colors) 2258 | are really helpful for monochrome: 2259 | ``` 2260 | convert cat.jpg -dither FloydSteinberg -colors 2 -type palette BMP3:cat.bmp 2261 | ``` 2262 | 2263 | 2264 | #### NodeJs: using gm 2265 | 2266 | There is a nice wrapper around GraphicsMagick / Imagemagick with the [`gm library`](https://github.com/aheckmann/gm). 2267 | A small NodeJs program to convert images could look like this: 2268 | 2269 | ```js 2270 | var gm = require('gm'); 2271 | gm('myimage.jpg') 2272 | .resize(240, 240) 2273 | .colors(64) 2274 | .type("palette") 2275 | .compress("None") 2276 | .write('BMP3:myimage_for_cirpy.bmp', function (err) { 2277 | if (!err) console.log('done1'); 2278 | }); 2279 | ``` 2280 | 2281 | #### Python: using PIL / pillow 2282 | 2283 | The [Python Image Library (PIL) fork `pillow`](https://pillow.readthedocs.io/en/stable/index.html) 2284 | seems to work the best. It's unclear how to toggle compression. 2285 | 2286 | ```py 2287 | from PIL import Image 2288 | size = (240, 240) 2289 | num_colors = 64 2290 | img = Image.open('myimage.jpg') 2291 | img = img.resize(size) 2292 | newimg = img.convert(mode='P', colors=num_colors) 2293 | newimg.save('myimage_for_cirpy.bmp') 2294 | ``` 2295 | 2296 | ### Preparing audio files for CircuitPython 2297 | 2298 | CircuitPython can play both WAV files and MP3 files, but there are specific 2299 | variants of these files that will work better, as some options require much more 2300 | processor demand. WAV files are much easier to play but take up more disk space. 2301 | 2302 | #### WAV files 2303 | 2304 | For WAV files, I've found the best trade-off in quality / flash-space / compatibility to be: 2305 | 2306 | - PCM 16-bit signed PCM 2307 | - Mono (but stereo will work if using I2S or SAMD51) 2308 | - 22050 Hz sample rate 2309 | 2310 | And remember that these settings must match how you're setting up the `audiomixer` object. 2311 | So for the above settings, you'd create an `audiomixer.Mixer` like: 2312 | 2313 | ```py 2314 | mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1, 2315 | bits_per_sample=16, samples_signed=True) 2316 | ``` 2317 | 2318 | To convert WAVs for CircuitPython, I like to use Audacity or the `sox` command-line tool. 2319 | Sox can convert just about any audio to the correct WAV format: 2320 | 2321 | ```sh 2322 | sox loop.mp3 -b 16 -c 1 -r 22050 loop.wav 2323 | ``` 2324 | 2325 | #### MP3 files 2326 | 2327 | MP3 files require a lot more CPU to decode so in general you will want to 2328 | re-encode MP3s to be a lower bit-rate and lower sample-rate. These settings 2329 | seem to work okay on an lower-end chip like the Pico /RP2040: 2330 | 2331 | * 128 kbps data rate CBR or lower 2332 | * 22050 Hz sample rate or lower 2333 | * Mono 2334 | 2335 | In `sox`, you can do this conversion with: 2336 | 2337 | ```sh 2338 | sox loop.mp3 -c 1 -r 22050 -C 128 loop_22k_128kbps.mp3 2339 | ``` 2340 | 2341 | 2342 | #### Getting sox 2343 | 2344 | To get `sox` on various platforms: 2345 | - Linux: `sudo apt install sox libsox-fmt-mp3` 2346 | - macOS: `brew install sox` 2347 | - Windows: Use installer at http://sox.sourceforge.net/ 2348 | 2349 | Some audio Learn Guide links: 2350 | - https://learn.adafruit.com/circuitpython-essentials/circuitpython-audio-out#play-a-wave-file-2994862-6 2351 | - https://learn.adafruit.com/adafruit-wave-shield-audio-shield-for-arduino/convert-files 2352 | 2353 | 2354 | ### Circup hacks 2355 | 2356 | [`circup`](https://learn.adafruit.com/keep-your-circuitpython-libraries-on-devices-up-to-date-with-circup?view=all) 2357 | is a great tool to help you install CircuitPython libraries. Think of it like `pip` or `npm` for CircuitPython. 2358 | 2359 | #### Finding where `circup` stores its files 2360 | 2361 | Instead of downloading the bundles by hand, `circup` has it already downloaded an unzipped. 2362 | Here's how to find that directory: 2363 | 2364 | ```sh 2365 | circup_dir=`python3 -c 'import appdirs; print(appdirs.user_data_dir(appname="circup", appauthor="adafruit"))'` 2366 | ls $circup_dir 2367 | ``` 2368 | 2369 | ### Building CircuitPython 2370 | 2371 | If you want to build CircuitPython yourself, you can! It's not too bad. 2372 | There's a very good ["Building CircuitPython" Learn Guide](https://learn.adafruit.com/building-circuitpython/build-circuitpython) that I refer to all the time, since it goes through the main reasons 2373 | why you might want to build your own version of CircuitPython, including: 2374 | - Adding "Frozen" Modules (libraries built-in to the firmware) 2375 | - Setting different SPI flash chips (if your custom board uses a different kind of flash) 2376 | - Adding a new board to CircuitPython 2377 | 2378 | But if you just want a quick list of the commands to use to build, here's what I use 2379 | (as of Jun 2024) to build CircuitPython for rp2040. 2380 | 2381 | ```sh 2382 | git clone https://github.com/todbot/circuitpython circuitpython-todbot 2383 | cd circuitpython-todbot 2384 | pip3 install --upgrade -r requirements-dev.txt # do occasionally, after 'git pull' 2385 | pip3 install --upgrade -r requirements-doc.txt # do occasionally, after 'git pull' 2386 | cd ports/raspberrypi 2387 | make fetch-port-submodules # takes a long time the first time ran, do after 'git pull' too 2388 | make BOARD=raspberry_pi_pico # or other board name listed in ports/raspberrypi/boards/ 2389 | # make -C ../../mpy-cross # if you need mpy-cross 2390 | ``` 2391 | 2392 | And for Espressif / ESP32 builds: 2393 | 2394 | ```sh 2395 | git clone https://github.com/todbot/circuitpython circuitpython-todbot 2396 | cd circuitpython-todbot 2397 | pip3 install --upgrade -r requirements-dev.txt # do occasionally, after 'git pull' 2398 | pip3 install --upgrade -r requirements-doc.txt # do occasionally, after 'git pull' 2399 | cd ports/espressif 2400 | make fetch-port-submodules # takes a long time the first time ran, do after 'git pull' too 2401 | ./esp-idf/install.sh 2402 | . ./esp-idf/export.sh 2403 | pip3 install --upgrade -r requirements-dev.txt # because now we're using a new python 2404 | pip3 install --upgrade -r requirements-doc.txt # because now we're using a new python 2405 | make BOARD=adafruit_qtpy_esp32s3 2406 | ``` 2407 | 2408 | Note, this assumes you've [already installed the system-level prerequisites](https://learn.adafruit.com/building-circuitpython/introduction). 2409 | On MacOS, this is what I do to get those: 2410 | 2411 | ```sh 2412 | brew install git git-lfs python3 gettext uncrustify cmake 2413 | brew install gcc-arm-embedded # (the cask, not 'arm-none-eabi-gcc') 2414 | ``` 2415 | 2416 | ## About this guide 2417 | 2418 | * Many of the code snippets are not considered "well-formatted" 2419 | by Python linters. This guide favors brevity over strict style adherence. 2420 | 2421 | * The precursor to this guide is [QTPy Tricks](https://github.com/todbot/qtpy-tricks), 2422 | which has similar but different (and still valid) fun things to do in CircuitPython. 2423 | 2424 | * This guide is the result of trying to learn Python via CircuitPython and 2425 | from very enlightening discussions with John Edgar Park. I have a good background in 2426 | other languages, but mostly C/C++, and have 2427 | [taught classes in Arduino](https://todbot.com/blog/bionicarduino/) for several years. 2428 | This informs how I've been thinking about things in this guide. 2429 | -------------------------------------------------------------------------------- /larger-tricks/README.md: -------------------------------------------------------------------------------- 1 | 2 | # circuitpython-tricks / larger-tricks 3 | 4 | These are larger tricks that are too big to fit in the 5 | [circuitpython-tricks](https://github.com/todbot/circuitpython-tricks) page. 6 | 7 | This directory contains a fairly un-ordered list, but here are the main tricks. 8 | 9 | 10 | ## Display and images 11 | 12 | * **emoji_flipper** - demonstrate how to use `displayio` and spritesheets 13 | 14 | * **fireworks_sprites** - demonstrate using `displayio` spritesheets to do animation and simple phyics engine 15 | 16 | * **dvdlogo_vectorio** - demonstrate `vectorio` to display bouncing DVD logo 17 | 18 | * **vectorio_rotate_example** - some `vectorio` tools to rotate & scale vectorio point sets 19 | 20 | 21 | ## Audio playing 22 | 23 | * **audiomixer_demo** - show how to use `audiomixer` to play multiple WAVs at different volumes 24 | 25 | * **pidaydrummachine** - show use of `keypad` and `audiomixer` to make a drum machine 26 | 27 | * **beatfader** - using `audiomixer` to fade between different WAVs 28 | 29 | * **beatfader_i2s** - using I2S DAC and `audiomixer` to fade between different WAVs 30 | 31 | * **i2s_sdcard_pico** - how to set up I2S audio out and SD card reading on Raspberry Pi Pico RP2040 32 | 33 | 34 | ## MIDI 35 | 36 | * **midi_forward** - simple USB MIDI to Serial MIDI forwarder using `adafruit_midi` and `usb_midi` 37 | 38 | 39 | ## USB 40 | 41 | * **robost_keyboard** - demonstrate how to deal with USB disconnect for battery-powered USB HID keyboard devices 42 | -------------------------------------------------------------------------------- /larger-tricks/audiomixer_demo.py: -------------------------------------------------------------------------------- 1 | # audiomixer_demo.py -- show how to fade up and down playing loops 2 | # note this causes glitches and crashes on RP2040 3 | # 9 Feb 2022 - @todbot / Tod Kurt 4 | 5 | import time 6 | import board 7 | import audiocore 8 | import audiomixer 9 | #from audiopwmio import PWMAudioOut as AudioOut # for RP2040 etc 10 | from audioio import AudioOut as AudioOut # for SAMD51/M4 etc 11 | 12 | num_voices = 2 13 | 14 | # audio pin is almost any pin on RP2040, let's do A0 (RP2040 GPIO226) or RX (RP2040 GPIO1) 15 | # audio pin is A0 on SAMD51 (Trelllis M4, Itsy M4, etc) 16 | audio = AudioOut(board.A0) 17 | mixer = audiomixer.Mixer(voice_count=num_voices, sample_rate=22050, channel_count=1, 18 | bits_per_sample=16, samples_signed=True) 19 | # attach mixer to audio playback 20 | audio.play(mixer) 21 | 22 | mixer.voice[0].level = 1.0 23 | mixer.voice[1].level = 0.0 24 | 25 | # start drum loop playing 26 | wave0 = audiocore.WaveFile(open("wav/drumsacuff_22k_s16.wav","rb")) 27 | mixer.voice[0].play( wave0, loop=True ) 28 | 29 | # start synth loop playing 30 | wave1 = audiocore.WaveFile(open("wav/snowpeaks_22k_s16.wav","rb")) 31 | mixer.voice[1].play( wave1, loop=True ) 32 | 33 | time.sleep(1.0) # let drums play a bit 34 | 35 | # fade each channel up and down 36 | up_down_inc = 0.01 37 | while True: 38 | mixer.voice[1].level = min(max(mixer.voice[1].level + up_down_inc, 0), 1) 39 | mixer.voice[0].level = min(max(mixer.voice[0].level - up_down_inc, 0), 1) 40 | if mixer.voice[0].level == 0 or mixer.voice[1].level == 0: 41 | up_down_inc = -up_down_inc 42 | time.sleep(0.1) 43 | 44 | -------------------------------------------------------------------------------- /larger-tricks/audiomixer_demo_i2s.py: -------------------------------------------------------------------------------- 1 | # audiomixer_demo_i2s.py -- show how to fade up and down playing loops 2 | # Code based on that of https://github.com/todbot/circuitpython-tricks/blob/main/larger-tricks/audiomixer_demo.py 3 | # where you can also find the WAV files used 4 | # 30 Nov 2022 - @todbot / Tod Kurt 5 | 6 | import time 7 | import board 8 | import audiocore 9 | import audiomixer 10 | import audiobusio 11 | 12 | i2s_bclk = board.GP9 # BCK on PCM5102 I2S DAC (SCK pin to Gnd) 13 | i2s_wsel = board.GP10 # LCLK on PCM5102 14 | i2s_data = board.GP11 # DIN on PCM5102 15 | num_voices = 2 16 | 17 | audio = audiobusio.I2SOut(bit_clock=i2s_bclk, word_select=i2s_wsel, data=i2s_data) 18 | 19 | mixer = audiomixer.Mixer(voice_count=num_voices, sample_rate=22050, channel_count=1, 20 | bits_per_sample=16, samples_signed=True) 21 | audio.play(mixer) # attach mixer to audio playback 22 | 23 | print("audio is now playing") 24 | 25 | # set some initial levels 26 | mixer.voice[0].level = 1.0 # drums on 27 | mixer.voice[1].level = 0.0 # synth off 28 | 29 | # load wav and start drum loop playing 30 | wave0 = audiocore.WaveFile(open("wav/drumsacuff_22k_s16.wav","rb")) 31 | mixer.voice[0].play( wave0, loop=True ) 32 | 33 | # load wav and start synth loop playing 34 | wave1 = audiocore.WaveFile(open("wav/snowpeaks_22k_s16.wav","rb")) 35 | mixer.voice[1].play( wave1, loop=True ) 36 | 37 | time.sleep(1.0) # let drums play a bit 38 | 39 | # fade each channel up and down 40 | up_down_inc = 0.01 41 | while True: 42 | print("voice 0 level=%1.2f" % mixer.voice[0].level) 43 | mixer.voice[1].level = min(max(mixer.voice[1].level + up_down_inc, 0), 1) 44 | mixer.voice[0].level = min(max(mixer.voice[0].level - up_down_inc, 0), 1) 45 | if mixer.voice[0].level == 0 or mixer.voice[1].level == 0: 46 | up_down_inc = -up_down_inc 47 | time.sleep(0.1) 48 | -------------------------------------------------------------------------------- /larger-tricks/beatfader.py: -------------------------------------------------------------------------------- 1 | # beatfader.py -- fade between different drum loops 2 | # 18 Feb 2022 - @todbot / Tod Kurt 3 | # 4 | # Tested on Raspberry Pi Pico, uses PWM Audio 5 | # 6 | # Copy this file to CIRCUITPY/code.py 7 | # Copy the "beatfader_wavs" directory to CIRCUITPY 8 | # 9 | # Note: CircuitPython will hang or freeze sometimes with audiopwmio 10 | # especially if USB access is happening (e.g. saving files) 11 | # To minimize this, stop the program (Ctrl-C in REPL) before 12 | # saving. 13 | # 14 | # Circuit as in ./docs/breakbeat_sampleplayer.fzz 15 | # 16 | 17 | import time 18 | import board 19 | import analogio 20 | import audiocore 21 | import audiomixer 22 | from audiopwmio import PWMAudioOut as AudioOut 23 | 24 | wav_files = ( 25 | # filename, loop? 26 | ('beatfader_wavs/amen_22k16b_160bpm.wav', True), 27 | ('beatfader_wavs/dnb21580_22k16b_160bpm.wav', True), 28 | ('beatfader_wavs/drumloopA_22k16b_160bpm.wav', True), 29 | ('beatfader_wavs/amen_22k16b_160bpm.wav', True), 30 | ) 31 | 32 | potknob = analogio.AnalogIn(board.A2) 33 | 34 | time.sleep(3) # helps prevent CirPy from crashing from USB + audio 35 | 36 | audio = AudioOut(board.GP1) 37 | mixer = audiomixer.Mixer(voice_count=len(wav_files), sample_rate=22050, channel_count=1, 38 | bits_per_sample=16, samples_signed=True) 39 | audio.play(mixer) # attach mixer to audio playback 40 | 41 | for i in range(len(wav_files)): 42 | (wav_file, loopit) = wav_files[i] 43 | wave = audiocore.WaveFile(open(wav_file,"rb")) 44 | mixer.voice[i].level = 0.0 # start quiet 45 | mixer.voice[i].play(wave, loop=loopit) # start playing 46 | 47 | cnt = len(wav_files) 48 | n = cnt * 0.8 # amount of overlap 49 | m = 1/(cnt-1) # size of slice 50 | 51 | while True: 52 | potval = potknob.value 53 | frac = potknob.value / 65535 54 | for i in range(cnt): 55 | l = min(max( 1 - (n * (frac - m*i))**2, 0), 1) 56 | mixer.voice[i].level = l 57 | print("%1.2f" % l, end=" ") 58 | print("%0.2f", frac) 59 | time.sleep(0.05) 60 | -------------------------------------------------------------------------------- /larger-tricks/beatfader_i2s.py: -------------------------------------------------------------------------------- 1 | # beatfader_i2s.py -- fade between different drum loops 2 | # 13 Dec 2022 - @todbot / Tod Kurt 3 | # 4 | # Tested on Lolin S2 Mini ESP32-S2 https://circuitpython.org/board/lolin_s2_mini/ 5 | # 6 | # Copy this file to CIRCUITPY/code.py 7 | # Copy the "beatfader_wavs" dir to CIRCUITPY 8 | # 9 | # Wiring is: 10 | # - Hook a PCM5102 I2S DAC to S2 Mini like: 11 | # - IO39 - BCK on PCM5102 12 | # - IO37 - LCLK on PCM5102 13 | # - IO35 - DIN on PCM5102 14 | # - Hook up a potentiometer knob to IO3 15 | # - That's it! 16 | # 17 | 18 | import time 19 | import board 20 | import analogio 21 | import audiocore, audiomixer, audiobusio 22 | 23 | wav_files = ( 24 | # filename, loop? 25 | ('beatfader_wavs/amen_22k16b_160bpm.wav', True), 26 | ('beatfader_wavs/dnb21580_22k16b_160bpm.wav', True), 27 | ('beatfader_wavs/drumloopA_22k16b_160bpm.wav', True), 28 | ('beatfader_wavs/amen_22k16b_160bpm.wav', True), 29 | ) 30 | num_voices = len(wav_files) 31 | 32 | potknob = analogio.AnalogIn(board.IO3) 33 | 34 | i2s_bclk = board.IO39 # BCK on PCM5102 I2S DAC (SCK pin to Gnd) 35 | i2s_wsel = board.IO37 # LCLK on PCM5102 36 | i2s_data = board.IO35 # DIN on PCM5102 37 | 38 | audio = audiobusio.I2SOut(bit_clock=i2s_bclk, word_select=i2s_wsel, data=i2s_data) 39 | 40 | mixer = audiomixer.Mixer(voice_count=num_voices, sample_rate=22050, channel_count=1, 41 | bits_per_sample=16, samples_signed=True) 42 | audio.play(mixer) # attach mixer to audio playback 43 | 44 | for i in range(num_voices): 45 | (wav_file, loopit) = wav_files[i] 46 | wave = audiocore.WaveFile(open(wav_file,"rb")) 47 | mixer.voice[i].level = 0.0 # turn down volume 48 | mixer.voice[i].play(wave, loop=loopit) # start wav playing 49 | 50 | n = num_voices * 0.8 # amount of overlap between samples 51 | m = 1/(num_voices-1) # size of slice 52 | 53 | while True: 54 | potval = potknob.value 55 | frac = potknob.value / 65535 56 | for i in range(num_voices): 57 | l = min(max( 1 - (n * (frac - m*i))**2, 0), 1) # fancy math to for nice mixing 58 | mixer.voice[i].level = l 59 | print("%1.2f" % l, end=" ") 60 | print("%0.2f", frac) 61 | time.sleep(0.05) 62 | -------------------------------------------------------------------------------- /larger-tricks/beatfader_wavs/amen_22k16b_160bpm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/beatfader_wavs/amen_22k16b_160bpm.wav -------------------------------------------------------------------------------- /larger-tricks/beatfader_wavs/dnb21580_22k16b_160bpm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/beatfader_wavs/dnb21580_22k16b_160bpm.wav -------------------------------------------------------------------------------- /larger-tricks/beatfader_wavs/drumloopA_22k16b_160bpm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/beatfader_wavs/drumloopA_22k16b_160bpm.wav -------------------------------------------------------------------------------- /larger-tricks/beatfader_wavs/femvoc_330662_22k16b_160bpm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/beatfader_wavs/femvoc_330662_22k16b_160bpm.wav -------------------------------------------------------------------------------- /larger-tricks/beatslicer_idea.py: -------------------------------------------------------------------------------- 1 | # beatslicer_idea.py -- ideas on doing beatslicer on Trellis M4 2 | # 24 Feb 2022 - @todbot / Tod Kurt 3 | 4 | import time 5 | import board 6 | import audiocore 7 | import audiomixer 8 | from audioio import AudioOut as AudioOut 9 | import adafruit_trellism4 10 | 11 | time.sleep(2) # to give us time for when it crashes 12 | 13 | # loop files must be tempo-sync'd! 14 | # loops from https://github.com/todbot/circuitpython-tricks/tree/main/larger-tricks/beatfader_wavs 15 | loop_files = ( 16 | "wav/amen_22k16b_160bpm.wav", # mono 160 bpm 17 | "wav/dnb21580_22k16b_160bpm.wav", # mono 160 bpm 18 | "wav/drumloopA_22k16b_160bpm.wav", # mono 160 bpm 19 | "wav/femvoc_330662_22k16b_160bpm.wav", # mono 160 bpm 20 | ) 21 | bpm = 160.00 22 | 23 | num_loops = len(loop_files) 24 | num_slices = 8 25 | 26 | millis_per_beat = 60_000 / bpm 27 | millis_per_measure = millis_per_beat * num_slices 28 | 29 | # 30 | loop_slices = [[False] * num_loops for i in range(num_slices)] 31 | 32 | beat_pos = 0 33 | last_beat_millis = 0 34 | current_press = set() 35 | 36 | trellis = adafruit_trellism4.TrellisM4Express(rotation=0) 37 | 38 | # audio pin is A0 and A1 on SAMD51 (Trelllis M4, Itsy M4, etc) 39 | audio = AudioOut(board.A0) 40 | #audio = AudioOut(left_channel=board.A1, right_channel=board.A0) 41 | mixer = audiomixer.Mixer(voice_count=num_loops, sample_rate=22050, channel_count=1, 42 | bits_per_sample=16, samples_signed=True) 43 | audio.play(mixer) # attach mixer to audio playback 44 | 45 | def start_loops(): 46 | # start drum loop playing, but silently 47 | for i in range(num_loops): 48 | wave = audiocore.WaveFile(open(loop_files[i],"rb")) 49 | mixer.voice[i].play( wave, loop=True ) 50 | mixer.voice[i].level = 0.0 51 | 52 | def millis(): return time.monotonic()*1000 # I like millis 53 | 54 | while True: 55 | # handle keypresses 56 | pressed = set(trellis.pressed_keys) 57 | for press in pressed - current_press: # I think I understand this now 58 | x, y = press 59 | if not loop_slices[x][y]: 60 | loop_slices[x][y] = True 61 | trellis.pixels[x,y] = 0xff00ff # more responsive UI 62 | else: 63 | loop_slices[x][y] = False 64 | trellis.pixels[x,y] = 0x000000 # more responsive UI 65 | current_press = pressed 66 | 67 | now = millis() 68 | if now - last_beat_millis >= millis_per_beat : 69 | last_beat_millis = now 70 | print("beat:", beat_pos) 71 | 72 | # if we're at the top, restart all loops so we stay in sync 73 | if beat_pos == 0: 74 | start_loops() 75 | 76 | # update mixer volumes 77 | for i in range(num_loops): # rows 78 | if loop_slices[ beat_pos ][i]: 79 | mixer.voice[i].level = 1.0 80 | else: 81 | mixer.voice[i].level = 0.0 82 | 83 | # update LEDs 84 | # show which slices are selected 85 | for x in range(num_slices): 86 | for y in range(num_loops): 87 | if loop_slices[x][y]: 88 | trellis.pixels[x,y] = 0xff00ff 89 | else: 90 | trellis.pixels[x,y] = 0x000000 91 | # set beat marker 92 | for i in range(num_loops): 93 | trellis.pixels[beat_pos,i] = 0x333333 94 | 95 | # go to next slice 96 | beat_pos = (beat_pos+1) % num_slices 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /larger-tricks/bmps/emoji_spritesheet_27x2_28x28-1.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/bmps/emoji_spritesheet_27x2_28x28-1.bmp -------------------------------------------------------------------------------- /larger-tricks/bmps/emoji_spritesheet_27x2_28x28-2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/bmps/emoji_spritesheet_27x2_28x28-2.bmp -------------------------------------------------------------------------------- /larger-tricks/bmps/emoji_spritesheet_27x2_28x28-3.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/bmps/emoji_spritesheet_27x2_28x28-3.bmp -------------------------------------------------------------------------------- /larger-tricks/bmps/emoji_spritesheet_27x2_28x28.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/bmps/emoji_spritesheet_27x2_28x28.bmp -------------------------------------------------------------------------------- /larger-tricks/bmps/emoji_spritesheet_27x7_28x28.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/bmps/emoji_spritesheet_27x7_28x28.bmp -------------------------------------------------------------------------------- /larger-tricks/bmps/fireworks_spritesheet.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/bmps/fireworks_spritesheet.bmp -------------------------------------------------------------------------------- /larger-tricks/bouncy_balls_vectorio.py: -------------------------------------------------------------------------------- 1 | # bouncy_balls_vectorio.py - use displayio to make simple bouncy balls 2 | # 12 Nov 2024 - @todbot / Tod Kurt. Based off bouncy_balls1.py 3 | # video demo at https://gist.github.com/todbot/d216cdfd0c13774c713482395429da16 4 | # works on any CircuitPython device with a display, just create a 'display' 5 | import time, random 6 | import board, busio, displayio, i2cdisplaybus 7 | import vectorio 8 | import adafruit_displayio_ssd1306 9 | 10 | # configuration options 11 | num_balls = 12 12 | ball_size = 5 13 | rand_vx, rand_vy = 2.5, 2 14 | scl_pin, sda_pin = board.GP15, board.GP14 # pins your display is on 15 | dw,dh = 128,64 # or whatever your display is 16 | 17 | # set up the display (change for your setup) 18 | displayio.release_displays() 19 | disp_i2c = busio.I2C(scl=scl_pin, sda=sda_pin, frequency=400_000) 20 | display_bus = i2cdisplaybus.I2CDisplayBus(disp_i2c, device_address=0x3C) # or 0x3D 21 | display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=dw, height=dh, rotation=180) 22 | 23 | # set up the displayio objects that go on the display 24 | maingroup = displayio.Group() 25 | display.root_group = maingroup 26 | 27 | # holder of x,y position and vx,vy velocity of a ball 28 | # and the update method to update the position based on velocity 29 | class Ball: 30 | def __init__(self,x,y, vx,vy, shape): 31 | self.x, self.y, self.vx, self.vy = x,y,vx,vy 32 | self.shape = shape 33 | def update(self): 34 | self.x = self.x + self.vx # update ball position 35 | self.y = self.y + self.vy # update ball position 36 | if self.x <= 0 or self.x > display.width: 37 | self.vx = -self.vx # bounce! 38 | if self.y <= 0 or self.y > display.height: 39 | self.vy = -self.vy # bounce! 40 | self.shape.x = int(self.x) 41 | self.shape.y = int(self.y) 42 | 43 | ballgroup = displayio.Group() # group of ballshapes (will fill out later) 44 | maingroup.append(ballgroup) # add ballshapes to screen 45 | pal = displayio.Palette(1) 46 | pal[0] = 0xffffff # only two colors in OLEDland: white & black 47 | 48 | balls = [] 49 | for i in range(num_balls): 50 | # start in middle of screen 51 | x, y = display.width//2, display.height//2 52 | # random initial velocity 53 | vx, vy = random.uniform(-rand_vx,rand_vx), random.uniform(-rand_vy,rand_vy) 54 | ballshape = vectorio.Circle(pixel_shader=pal, radius=ball_size, x=x, y=y) 55 | balls.append( Ball(x,y, vx,vy, ballshape) ) # our perfect math balls 56 | ballgroup.append(ballshape) # our actual drawn ball shapes 57 | 58 | while True: 59 | for ball in balls: 60 | ball.update() 61 | time.sleep(0.01) 62 | 63 | -------------------------------------------------------------------------------- /larger-tricks/breakbeat_sampleplayer.py: -------------------------------------------------------------------------------- 1 | # breakbeat_sampleplayer.py -- play samples 2 | # 3 | # 8 Feb 2022 - @todbot / Tod Kurt 4 | # 5 | # Part of circuitpython-tricks: https://github.com/todbot/circuitpython-tricks 6 | # 7 | # For wiring diagram, see: 8 | # ./docs/breakbeat_sampleplayer_wiring.png 9 | # 10 | # Convert samples for use with SoX, like: 11 | # sox loop.mp3 -b 16 -c 1 -r 22050 loop.wav 12 | # 13 | # Or copy "breakbeat_wavs" folder as CIRCUITPY/wav 14 | # 15 | 16 | import random 17 | import time 18 | import board 19 | import supervisor 20 | 21 | import audiocore 22 | import audiomixer 23 | from audiopwmio import PWMAudioOut as AudioOut 24 | 25 | import neopixel 26 | 27 | leds = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) 28 | 29 | #def millis(): return supervisor.ticks_ms() # I like millis 30 | def millis(): return time.monotonic()*1000 # I like millis 31 | 32 | wav_files = ( 33 | # filename, loop? 34 | ('wav/amenfull_22k_s16.wav', True), # 137.72 bpm 35 | ('wav/amen2_22k_s16.wav', False), 36 | ('wav/amen3_22k_s16.wav', False), 37 | ('wav/amen4_22k_s16.wav', False), 38 | ('wav/amen5_22k_s16.wav', False), 39 | ('wav/amen6_22k_s16.wav', False), 40 | ('wav/amen7_22k_s16.wav', False), 41 | ('wav/amen8_22k_s16.wav', False), 42 | ('wav/ohohoh2.wav', False), 43 | ) 44 | 45 | # audio pin is RX (pin 1) (RP2040 GPIO1) 46 | audio = AudioOut(board.RX) 47 | mixer = audiomixer.Mixer(voice_count=len(wav_files), sample_rate=22050, channel_count=1, 48 | bits_per_sample=16, samples_signed=True) 49 | # attach mixer to audio playback 50 | audio.play(mixer) 51 | 52 | bpm = 137.72 53 | beat_count = 4 54 | millis_per_beat = 60_000 / bpm 55 | millis_per_measure = millis_per_beat * beat_count 56 | last_millis = 0 57 | last_led_millis = 0 58 | 59 | wav_num = 0 60 | while True: 61 | leds[0] = 0x000000 62 | time.sleep(0.0001) 63 | now = millis() 64 | # a little blinky 65 | if now - last_led_millis >= millis_per_beat : 66 | last_led_millis = now 67 | leds[0] = 0x00ff00 68 | 69 | if now - last_millis >= millis_per_measure : 70 | last_millis = now 71 | print("playing wav", wav_num) 72 | leds[0] = 0xff00ff 73 | voice = mixer.voice[wav_num] 74 | (wav_file,loopit) = wav_files[wav_num] 75 | wave = audiocore.WaveFile(open(wav_file,"rb")) 76 | voice.play(wave,loop=loopit) 77 | # pick random for next time 78 | wav_num = random.randint(0,len(wav_files)-1) 79 | 80 | 81 | -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amen1_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amen1_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amen2_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amen2_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amen3_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amen3_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amen4_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amen4_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amen5_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amen5_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amen6_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amen6_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amen7_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amen7_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amen8_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amen8_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/amenfull_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/amenfull_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/breakbeat_wav/ohohoh2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/breakbeat_wav/ohohoh2.wav -------------------------------------------------------------------------------- /larger-tricks/chikkenplayer.py: -------------------------------------------------------------------------------- 1 | # chikkenplayer.py - StreetChicken Remixer by todbot 2 | # 21 Mar 2021 - @todbot / Tod Kurt, from @jedgarpark idea 3 | # 4 | import time 5 | import board 6 | import analogio 7 | import keypad 8 | import audiocore 9 | import audiomixer 10 | from audiopwmio import PWMAudioOut as AudioOut 11 | 12 | time.sleep(5) # wait a bit to reduce noise from CIRCUITPY access 13 | 14 | wav_files = ( 15 | "wav/chikken1_161_22k16b.wav", # slowed down & hacked StreetChicken 16 | "wav/chikken2_161_22k16b.wav", # fat distorty bassline 17 | "wav/chikken3_161_22k16b.wav", # reversed & slowed & warped StreetChicken 18 | "wav/chikken4_161_22k16b.wav", # 808 drum pattern (kick + snare only) 19 | "wav/chikken5_161_22k16b.wav", # chunky guitar power chords 20 | "wav/chikken6_161_22k16b.wav", # ethereal space arpeggios 21 | ) 22 | 23 | # knob to adjust loop levels 24 | knob = analogio.AnalogIn(board.A3) 25 | 26 | # what keys to use 27 | keys = keypad.Keys((board.RX, board.SCK, board.MISO, board.MOSI, board.SCL, board.SDA), 28 | value_when_pressed=False, pull=True) 29 | # audio output 30 | audio = AudioOut(board.TX) # board.A0 for SAMD chips, any for rp2040 pico 31 | mixer = audiomixer.Mixer(voice_count=len(wav_files), sample_rate=22050, channel_count=1, 32 | bits_per_sample=16, samples_signed=True) 33 | audio.play(mixer) # attach mixer to audio playback, play with mixer.voice[n].play() 34 | 35 | # start loops playing, but silently 36 | for i in range(len(wav_files)): 37 | wave = audiocore.WaveFile(open(wav_files[i],"rb")) 38 | mixer.voice[i].play(wave, loop=True) 39 | mixer.voice[i].level = 0.0 40 | 41 | print("chikkenplayer ready") 42 | 43 | edit_voices = [False] * len(wav_files) 44 | pickup_voices = [False] * len(wav_files) 45 | 46 | while True: 47 | event = keys.events.get() 48 | if event: 49 | voice_num = event.key_number 50 | if event.pressed: 51 | edit_voices[voice_num] = True 52 | if event.released: 53 | edit_voices[voice_num] = False 54 | pickup_voices[voice_num] = False 55 | 56 | for i in range(len(wav_files)): 57 | new_val = knob.value 58 | if edit_voices[i]: # only edit voices with pressed buttons 59 | if pickup_voices[i]: # have we crossed old value? 60 | mixer.voice[i].level = new_val / 65535 # convert to 0-1.0 61 | else: 62 | old_val = int(mixer.voice[i].level * 65535) # convert 0-1 to 0-65535 63 | if abs(new_val - old_val) < 100: # if we get close to old value, 64 | pickup_voices[i] = True # flip the pickup switch until key release 65 | -------------------------------------------------------------------------------- /larger-tricks/chikkenplayer_wav/chikken1_161_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/chikkenplayer_wav/chikken1_161_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/chikkenplayer_wav/chikken2_161_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/chikkenplayer_wav/chikken2_161_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/chikkenplayer_wav/chikken3_161_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/chikkenplayer_wav/chikken3_161_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/chikkenplayer_wav/chikken4_161_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/chikkenplayer_wav/chikken4_161_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/chikkenplayer_wav/chikken5_161_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/chikkenplayer_wav/chikken5_161_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/chikkenplayer_wav/chikken6_161_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/chikkenplayer_wav/chikken6_161_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/cpx_midi_controller.py: -------------------------------------------------------------------------------- 1 | # quick-n-dirty MIDI controller with Circuit Playground Express 2 | # 30 Apr 2024 - @todbot / Tod Kurt 3 | # Works on Circuit Playground Express in CircuitPython 9, 4 | # but will also work on also any board with touchio, like Trinket M0 or QTPy M0 5 | # Uses the minimal number of libraries to save RAM on the tiny M0 in the CPX 6 | # (e.g. cannot load `adafruit_circuitpython`, `adafruit_midi`, and `adafruit_debouncer` on CPX) 7 | 8 | import board 9 | import touchio 10 | import usb_midi 11 | import neopixel 12 | 13 | # which MIDI notes to send 14 | midi_notenums = (40, 41, 42, 43) 15 | 16 | # which pins for each of the above notes 17 | touch_pins = (board.A1, board.A2, board.A3, board.A4) 18 | 19 | # which MIDI channel to transmit on 20 | midi_out_channel = 1 21 | 22 | # set up the status byte constants for sending MIDI later 23 | note_on_status = (0x90 | (midi_out_channel-1)) 24 | note_off_status = (0x80 | (midi_out_channel-1)) 25 | 26 | # get a MIDI out port 27 | midi_out = usb_midi.ports[1] 28 | # make ready the neopixels so we can blink when a pad is touched 29 | leds = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.1) 30 | 31 | # set up the touch pins 32 | touchins = [] 33 | for i, pin in enumerate(touch_pins): 34 | touchins.append( touchio.TouchIn(pin) ) 35 | last_touch = [False] * len(touchins) 36 | 37 | print("Welcome! Touch a pad!") 38 | while True: 39 | for i, touchin in enumerate(touchins): 40 | touch = touchin.value # get current touch value 41 | if touch != last_touch[i]: # was there a change in touch? 42 | last_touch[i] = touch # save for next time 43 | notenum = midi_notenums[i] # get MIDI note for this pad 44 | if touch : # pressed! 45 | print("touch!", i) 46 | midi_out.write(bytearray([note_on_status, notenum, 127])) 47 | leds.fill(0x330044) 48 | else: 49 | print("release!",i) 50 | midi_out.write(bytearray([note_off_status, notenum, 0])) 51 | leds.fill(0) 52 | -------------------------------------------------------------------------------- /larger-tricks/docs/breakbeat_sampleplayer.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/docs/breakbeat_sampleplayer.fzz -------------------------------------------------------------------------------- /larger-tricks/docs/breakbeat_sampleplayer_wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/docs/breakbeat_sampleplayer_wiring.png -------------------------------------------------------------------------------- /larger-tricks/dvdlogo_vectorio_code.py: -------------------------------------------------------------------------------- 1 | # dvdlogo_vectorio_code.py - Bouncing DVD logo in CircuitPython vectorio 2 | # 25 Mar 2022 - @todbot / Tod Kurt 3 | # a vectorio rethink of https://github.com/todbot/circuitpython_screensaver 4 | # video in action: https://twitter.com/todbot/status/1507516909528358916?s=21 5 | # use https://shinao.github.io/PathToPoints/ to get a list of points 6 | # then https://gist.github.com/todbot/05d8e64ba8c6a7a584c1956c817a8779 to normalize those points (see below) 7 | 8 | import time 9 | import board 10 | import displayio 11 | import vectorio 12 | import rainbowio 13 | 14 | class DVDLogo: 15 | def make_logo(width, height, color=0xFFFFFF, x=0, y=0): 16 | """ make a vectorio based DVD logo """ 17 | pal = displayio.Palette(1) 18 | pal[0] = color 19 | points0 = [] 20 | for i in range(len(DVDLogo.path0)): 21 | (dx,dy) = DVDLogo.path0[i] 22 | dx = int(dx * width) 23 | dy = int(dy * height) 24 | points0.append( (dx,dy) ) 25 | dvdlogo0 = vectorio.Polygon(pixel_shader=pal, points=points0, x=x, y=y) 26 | 27 | points1 = [] 28 | for i in range(len(DVDLogo.path1)): 29 | (dx,dy) = DVDLogo.path1[i] 30 | dx = int(dx * width) 31 | dy = int(dy * height) 32 | points1.append( (dx,dy) ) 33 | dvdlogo1 = vectorio.Polygon(pixel_shader=pal, points=points1, x=x, y=y) 34 | 35 | dvdlogo = displayio.Group() 36 | dvdlogo.append(dvdlogo0) 37 | dvdlogo.append(dvdlogo1) 38 | return dvdlogo 39 | 40 | def change_color(logogroup, color): 41 | """ change color of logo, convenience function """ 42 | logogroup[0].pixel_shader[0] = color 43 | 44 | # vector paths from SVG at https://upload.wikimedia.org/wikipedia/commons/9/9b/DVD_logo.svg 45 | # normalized to 1.0 on both x & y, original aspect ratio is 2:1 46 | path0 = ( 47 | (0.580, 0.214), (0.550, 0.297), (0.521, 0.382), (0.509, 0.325), (0.494, 0.225), (0.479, 0.125), (0.455, 0.050), (0.406, 0.050), (0.358, 0.050), (0.309, 0.050), (0.260, 0.050), (0.211, 0.050), (0.162, 0.050), (0.114, 0.050), (0.098, 0.141), (0.132, 0.167), (0.181, 0.167), (0.230, 0.168), (0.276, 0.197), (0.301, 0.282), (0.285, 0.380), (0.246, 0.440), (0.198, 0.463), (0.160, 0.448), (0.171, 0.345), (0.182, 0.243), (0.152, 0.210), (0.103, 0.210), (0.083, 0.288), (0.072, 0.391), (0.061, 0.493), (0.057, 0.583), (0.106, 0.583), (0.155, 0.583), (0.203, 0.581), (0.252, 0.566), (0.298, 0.532), (0.340, 0.481), (0.376, 0.409), (0.398, 0.316), (0.397, 0.212), (0.403, 0.214), (0.419, 0.314), (0.435, 0.413), (0.451, 0.512), (0.467, 0.612), (0.483, 0.703), (0.515, 0.624), (0.547, 0.544), (0.579, 0.465), (0.611, 0.385), (0.643, 0.306), (0.675, 0.226), (0.712, 0.168), (0.760, 0.167), (0.809, 0.167), (0.857, 0.179), (0.896, 0.238), (0.897, 0.340), (0.865, 0.419), (0.820, 0.457), (0.772, 0.465), (0.766, 0.392), (0.777, 0.289), (0.774, 0.210), (0.725, 0.210), (0.688, 0.241), (0.677, 0.344), (0.666, 0.446), (0.655, 0.549), (0.684, 0.583), (0.733, 0.583), (0.782, 0.583), (0.830, 0.575), (0.877, 0.550), (0.922, 0.507), (0.961, 0.445), (0.990, 0.361), (1.000, 0.259), (0.981, 0.163), (0.943, 0.098), (0.898, 0.063), (0.849, 0.050), (0.800, 0.050), (0.752, 0.050), (0.703, 0.050), (0.654, 0.050), (0.619, 0.112), (0.588, 0.194), ) 48 | path1 = ( 49 | (0.485, 0.709), (0.436, 0.710), (0.387, 0.712), (0.339, 0.716), (0.290, 0.722), (0.241, 0.730), (0.193, 0.741), (0.145, 0.756), (0.097, 0.775), (0.050, 0.805), (0.031, 0.877), (0.075, 0.919), (0.123, 0.942), (0.171, 0.958), (0.220, 0.970), (0.268, 0.980), (0.317, 0.987), (0.366, 0.992), (0.414, 0.995), (0.463, 0.997), (0.512, 0.996), (0.561, 0.995), (0.6909, 0.991), (0.658, 0.986), (0.707, 0.979), (0.755, 0.969), (0.804, 0.957), (0.852, 0.940), (0.899, 0.916), (0.943, 0.870), (0.916, 0.802), (0.869, 0.773), (0.821, 0.754), (0.773, 0.740), (0.725, 0.730), (0.676, 0.722), (0.627, 0.716), (0.578, 0.712), (0.530, 0.709), (0.465, 0.904), (0.416, 0.897), (0.369, 0.871), (0.397, 0.820), (0.446, 0.808), (0.494, 0.809), (0.543, 0.821), (0.565, 0.874), (0.518, 0.898), (0.469, 0.904), ) 50 | 51 | 52 | display = board.DISPLAY 53 | 54 | maingroup = displayio.Group() 55 | display.root_group = maingroup # put main group on display, everything goes in maingroup 56 | 57 | dvdlogo_w = 80 58 | dvdlogo_h = 40 59 | x = 0 60 | y = 0 61 | xvel = 1.9 62 | yvel = 1.4 63 | 64 | dvdlogo = DVDLogo.make_logo( dvdlogo_w, dvdlogo_h, color=0xFF00FF, x=x,y=0 ) 65 | maingroup.append(dvdlogo) # put the logo on the display via the main group 66 | 67 | while True: 68 | x = x + xvel 69 | y = y + yvel 70 | # if we hit an edge, bounce & change color! 71 | if x < 0 or x > (display.width - dvdlogo_w): 72 | xvel = -xvel 73 | DVDLogo.change_color(dvdlogo, rainbowio.colorwheel(time.monotonic()*50)) 74 | if y < 0 or y > (display.height - dvdlogo_h): 75 | yvel = -yvel 76 | DVDLogo.change_color(dvdlogo, rainbowio.colorwheel(time.monotonic()*50)) 77 | 78 | dvdlogo.x = int(x) 79 | dvdlogo.y = int(y) 80 | 81 | time.sleep(0.01) 82 | -------------------------------------------------------------------------------- /larger-tricks/emoji_flipper.py: -------------------------------------------------------------------------------- 1 | # emoji_flipper.py - show a bunch of emojis from a sprite sheet 2 | # 23 Jun 2022 - @todbot / Tod Kurt 3 | import time 4 | import board 5 | import displayio 6 | import adafruit_imageload 7 | 8 | sprite_fname = "emoji_spritesheet_27x7_28x28.bmp" 9 | sprite_cnt = 27*7 10 | sprite_w,sprite_h = 28,28 11 | 12 | sprite_sheet,sprite_palette = adafruit_imageload.load(sprite_fname) 13 | #sprite_sheet = displayio.OnDiskBitmap(open(sprite_fname, "rb")) 14 | #sprite_palette = sprite_sheet.pixel_shader 15 | sprite_palette.make_transparent(0) # make background color transparent 16 | sprite = displayio.TileGrid(sprite_sheet, pixel_shader=sprite_palette, 17 | width = 1, height = 1, 18 | tile_width = sprite_w, tile_height = sprite_h) 19 | display = board.DISPLAY # our board has built-in display 20 | 21 | maingroup = displayio.Group(scale=4) # make 4x big 22 | maingroup.append(sprite) 23 | display..root_group = maingroup 24 | 25 | sprite_index = 0 # where in the sprite sheet we currently are 26 | while True: 27 | sprite[0] = sprite_index 28 | sprite_index = (sprite_index + 1) % sprite_cnt 29 | time.sleep(0.1) 30 | -------------------------------------------------------------------------------- /larger-tricks/eyeballs/code.py: -------------------------------------------------------------------------------- 1 | # gc9a01_multi_eyeball_code.py -- 2 | # 14 Oct 2022 - @todbot / Tod Kurt 3 | # Part of circuitpython-tricks/larger-tricks 4 | # Requires rebuilt CircuitPython with CIRCUITPY_DISPLAY_LIMIT set to 3 5 | # see: https://todbot.com/blog/2022/05/19/multiple-displays-in-circuitpython-compiling-custom-circuitpython/ 6 | 7 | import time, math, random 8 | import board, busio 9 | import displayio 10 | import adafruit_imageload 11 | import gc9a01 12 | 13 | displayio.release_displays() 14 | 15 | dw, dh = 240,240 # display dimensions 16 | 17 | eyeball_bitmap, eyeball_pal = adafruit_imageload.load("imgs/eye0_ball2.bmp") 18 | iris_bitmap, iris_pal = adafruit_imageload.load("imgs/eye0_iris0.bmp") 19 | iris_pal.make_transparent(0) 20 | 21 | iris_w, iris_h = iris_bitmap.width, iris_bitmap.height # iris is normally 110x110 22 | iris_cx, iris_cy = dw//2 - iris_w//2, dh//2 - iris_h//2 23 | 24 | tft0_clk = board.GP18 25 | tft0_mosi = board.GP19 26 | 27 | tft1_clk = board.GP10 28 | tft1_mosi = board.GP11 29 | 30 | tft_L0_rst = board.GP21 31 | tft_L0_dc = board.GP22 32 | tft_L0_cs = board.GP20 33 | 34 | tft_R0_rst = board.GP26 35 | tft_R0_dc = board.GP27 36 | tft_R0_cs = board.GP28 37 | 38 | tft_L1_rst = board.GP14 39 | tft_L1_dc = board.GP12 40 | tft_L1_cs = board.GP13 41 | 42 | tft_R1_rst = board.GP3 43 | tft_R1_dc = board.GP4 44 | tft_R1_cs = board.GP5 45 | 46 | spi0 = busio.SPI(clock=tft0_clk, MOSI=tft0_mosi) 47 | spi1 = busio.SPI(clock=tft1_clk, MOSI=tft1_mosi) 48 | 49 | def eye_init(spi, dc, cs, rst, rot): 50 | display_bus = displayio.FourWire(spi, command=dc, chip_select=cs, reset=rst) 51 | display = gc9a01.GC9A01(display_bus, width=dw, height=dh, rotation=rot) 52 | main = displayio.Group() 53 | display.root_group = main 54 | eyeball = displayio.TileGrid(eyeball_bitmap, pixel_shader=eyeball_pal) 55 | iris = displayio.TileGrid(iris_bitmap, pixel_shader=iris_pal, x = iris_cx, y = iris_cy ) 56 | main.append(eyeball) 57 | main.append(iris) 58 | return (display, eyeball, iris) 59 | 60 | 61 | (display_L0, eyeball_L0, iris_L0) = eye_init( spi0, tft_L0_dc, tft_L0_cs, tft_L0_rst, rot=0) 62 | (display_R0, eyeball_R0, iris_R0) = eye_init( spi0, tft_R0_dc, tft_R0_cs, tft_R0_rst, rot=0) 63 | (display_L1, eyeball_L1, iris_L1) = eye_init( spi1, tft_L1_dc, tft_L1_cs, tft_L1_rst, rot=0) 64 | # can't do four yet 65 | #(display_R1, eyeball_R1, iris_R1) = eye_init( spi1, tft_R1_dc, tft_R1_cs, tft_R1_rst, rot=0) 66 | 67 | the_eyes = [ 68 | # x,y, tx,ty, next_time 69 | [display_L0, eyeball_L0, iris_L0, iris_cx, iris_cy, 0, 0, 0 ], 70 | [display_R0, eyeball_R0, iris_R0, iris_cx, iris_cy, 0, 0, 0 ], 71 | [display_L1, eyeball_L1, iris_L1, iris_cx, iris_cy, 0, 0, 0 ], 72 | #[display_R1, eyeball_R1, iris_R1, iris_cx, iris_cy, 0, 0, 0 ], # can't do four yet 73 | ] 74 | 75 | 76 | r = 17 # allowable deviation from center for iris 77 | 78 | while True: 79 | for i in range(len(the_eyes)): 80 | (display, eyeball, iris, x,y, tx,ty, next_time) = the_eyes[i] 81 | x = x * 0.5 + tx * 0.5 # "easing" 82 | y = y * 0.5 + ty * 0.5 83 | iris.x = int( x ) 84 | iris.y = int( y ) 85 | the_eyes[i][3] = x 86 | the_eyes[i][4] = y 87 | if time.monotonic() > next_time: 88 | next_time = time.monotonic() + random.uniform(0,2) 89 | tx = iris_cx + random.uniform(-r,r) 90 | ty = iris_cy + random.uniform(-r,r) 91 | the_eyes[i][5] = tx 92 | the_eyes[i][6] = ty 93 | the_eyes[i][7] = next_time # FIXME 94 | print("change!") 95 | #display.refresh( target_frames_per_second=20 ) 96 | display.refresh() 97 | -------------------------------------------------------------------------------- /larger-tricks/eyeballs/imgs/eye0_ball2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/eyeballs/imgs/eye0_ball2.bmp -------------------------------------------------------------------------------- /larger-tricks/eyeballs/imgs/eye0_iris0.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/eyeballs/imgs/eye0_iris0.bmp -------------------------------------------------------------------------------- /larger-tricks/eyeballs/imgs/eyelid_spritesheet.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/eyeballs/imgs/eyelid_spritesheet.bmp -------------------------------------------------------------------------------- /larger-tricks/eyeballs/imgs/eyelid_spritesheet2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/eyeballs/imgs/eyelid_spritesheet2.bmp -------------------------------------------------------------------------------- /larger-tricks/eyeballs/qteye.py: -------------------------------------------------------------------------------- 1 | # qteye.py - a stand-alone GC9A01 round LCD "eye" on a QTPy 2 | # 23 Oct 2022 - @todbot / Tod Kurt 3 | # Part of circuitpython-tricks/larger-tricks/eyeballs 4 | # also see: https://todbot.com/blog/2022/05/19/multiple-displays-in-circuitpython-compiling-custom-circuitpython/ 5 | 6 | import time, math, random 7 | import board, busio 8 | import displayio 9 | import adafruit_imageload 10 | import gc9a01 11 | 12 | displayio.release_displays() 13 | 14 | dw, dh = 240,240 # display dimensions 15 | 16 | # load our eye and iris bitmaps 17 | eyeball_bitmap, eyeball_pal = adafruit_imageload.load("imgs/eye0_ball2.bmp") 18 | iris_bitmap, iris_pal = adafruit_imageload.load("imgs/eye0_iris0.bmp") 19 | iris_pal.make_transparent(0) 20 | 21 | # compute or declare some useful info about the eyes 22 | iris_w, iris_h = iris_bitmap.width, iris_bitmap.height # iris is normally 110x110 23 | iris_cx, iris_cy = dw//2 - iris_w//2, dh//2 - iris_h//2 24 | r = 20 # allowable deviation from center for iris 25 | 26 | # wiring for QT Py, should work on any QT Py or XIAO board 27 | tft0_clk = board.SCK 28 | tft0_mosi = board.MOSI 29 | 30 | tft_L0_rst = board.MISO 31 | tft_L0_dc = board.RX 32 | tft_L0_cs = board.TX 33 | 34 | spi0 = busio.SPI(clock=tft0_clk, MOSI=tft0_mosi) 35 | 36 | # class to help us track eye info (not needed for this use exactly, but I find it interesting) 37 | class Eye: 38 | def __init__(self, spi, dc, cs, rst, rot=0, eye_speed=0.25, twitch=2): 39 | display_bus = displayio.FourWire(spi, command=dc, chip_select=cs, reset=rst) 40 | display = gc9a01.GC9A01(display_bus, width=dw, height=dh, rotation=rot) 41 | main = displayio.Group() 42 | display.root_group = main 43 | self.display = display 44 | self.eyeball = displayio.TileGrid(eyeball_bitmap, pixel_shader=eyeball_pal) 45 | self.iris = displayio.TileGrid(iris_bitmap, pixel_shader=iris_pal, x=iris_cx,y=iris_cy) 46 | main.append(self.eyeball) 47 | main.append(self.iris) 48 | self.x, self.y = iris_cx, iris_cy 49 | self.tx, self.ty = self.x, self.y 50 | self.next_time = time.monotonic() 51 | self.eye_speed = eye_speed 52 | self.twitch = twitch 53 | 54 | def update(self): 55 | self.x = self.x * (1-self.eye_speed) + self.tx * self.eye_speed # "easing" 56 | self.y = self.y * (1-self.eye_speed) + self.ty * self.eye_speed 57 | self.iris.x = int( self.x ) 58 | self.iris.y = int( self.y ) 59 | if time.monotonic() > self.next_time: 60 | t = random.uniform(0.25,self.twitch) 61 | self.next_time = time.monotonic() + t 62 | self.tx = iris_cx + random.uniform(-r,r) 63 | self.ty = iris_cy + random.uniform(-r,r) 64 | self.display.refresh() 65 | 66 | # a list of all the eyes, in this case, only one 67 | the_eyes = [ 68 | Eye( spi0, tft_L0_dc, tft_L0_cs, tft_L0_rst, rot=0), 69 | ] 70 | 71 | while True: 72 | for eye in the_eyes: 73 | eye.update() 74 | -------------------------------------------------------------------------------- /larger-tricks/eyeballs/qteye_blink_esp32s3.py: -------------------------------------------------------------------------------- 1 | # qteye_blink_esp32s3.py - a stand-alone GC9A01 round LCD "eye" on a QTPy ESP32-S3 2 | # 16 Oct 2023 - @todbot / Tod Kurt 3 | # Part of circuitpython-tricks/larger-tricks/eyeballs 4 | # also see: https://todbot.com/blog/2022/05/19/multiple-displays-in-circuitpython-compiling-custom-circuitpython/ 5 | 6 | import time, math, random 7 | import board, busio 8 | import displayio 9 | import adafruit_imageload 10 | import gc9a01 11 | 12 | # config: behaviors 13 | eye_twitch_time = 2 # bigger is less twitchy 14 | eye_twitch_amount = 20 # allowable deviation from center for iris 15 | eye_blink_time = 0.8 # bigger is slower 16 | eye_rotation = 0 # 0 or 180, don't do 90 or 270 because too slow 17 | 18 | # config: wiring for QT Py, should work on any QT Py or XIAO board, but ESP32-S3 is fastest 19 | tft0_clk = board.SCK 20 | tft0_mosi = board.MOSI 21 | 22 | tft_L0_rst = board.MISO 23 | tft_L0_dc = board.RX 24 | tft_L0_cs = board.TX 25 | 26 | displayio.release_displays() 27 | 28 | dw, dh = 240, 240 # display dimensions 29 | 30 | # load our eye and iris bitmaps 31 | ## static so load from disk (also can't have it it RAM and eyelids in RAM too) 32 | eyeball_bitmap = displayio.OnDiskBitmap(open("/imgs/eye0_ball2.bmp", "rb")) 33 | eyeball_pal = eyeball_bitmap.pixel_shader 34 | ## moves around, so load into RAM 35 | iris_bitmap, iris_pal = adafruit_imageload.load("imgs/eye0_iris0.bmp") 36 | iris_pal.make_transparent(0) 37 | ## also moves, so load into RAM (hopefully) 38 | try: 39 | eyelid_bitmap, eyelid_pal = adafruit_imageload.load("/imgs/eyelid_spritesheet2.bmp") 40 | except Exception as e: 41 | print("couldn't load",e) 42 | eyelid_bitmap = displayio.OnDiskBitmap(open("/imgs/eyelid_spritesheet2.bmp", "rb")) 43 | eyelid_pal = eyelid_bitmap.pixel_shader 44 | eyelid_sprite_cnt = eyelid_bitmap.width // dw # should be 16 45 | eyelid_pal.make_transparent(1) 46 | 47 | # compute or declare some useful info about the eyes 48 | iris_w, iris_h = iris_bitmap.width, iris_bitmap.height # iris is normally 110x110 49 | iris_cx, iris_cy = dw//2 - iris_w//2, dh//2 - iris_h//2 50 | 51 | 52 | spi0 = busio.SPI(clock=tft0_clk, MOSI=tft0_mosi) 53 | 54 | # class to help us track eye info (not needed for this use exactly, but I find it interesting) 55 | class Eye: 56 | """ 57 | global variables used: 58 | - iris_cx, iris_cy 59 | - eye_twitch_amount 60 | - eye_twitch_time 61 | - eye_blink_time 62 | """ 63 | def __init__(self, spi, dc, cs, rst, rot=0, eye_speed=0.25): 64 | # ends up being 80 MHz on ESP32-S3, nice 65 | display_bus = displayio.FourWire(spi, command=dc, chip_select=cs, reset=rst, baudrate=64_000_000) 66 | display = gc9a01.GC9A01(display_bus, width=dw, height=dh, rotation=rot) 67 | display.auto_refresh = False 68 | main = displayio.Group() 69 | display.root_group = main 70 | self.display = display 71 | self.eyeball = displayio.TileGrid(eyeball_bitmap, pixel_shader=eyeball_pal) 72 | self.iris = displayio.TileGrid(iris_bitmap, pixel_shader=iris_pal, x=iris_cx,y=iris_cy) 73 | self.lids = displayio.TileGrid(eyelid_bitmap, pixel_shader=eyelid_pal, x=0, y=0, tile_width=dw, tile_height=dh) 74 | main.append(self.eyeball) 75 | main.append(self.iris) 76 | main.append(self.lids) 77 | self.x, self.y = iris_cx, iris_cy 78 | self.tx, self.ty = self.x, self.y 79 | self.next_time = 0 80 | self.eye_speed = eye_speed 81 | self.lidpos = 0 82 | self.lidpos_inc = 1 83 | self.lid_next_time = 0 84 | 85 | def update(self): 86 | # make the eye twitch around 87 | self.x = self.x * (1-self.eye_speed) + self.tx * self.eye_speed # "easing" 88 | self.y = self.y * (1-self.eye_speed) + self.ty * self.eye_speed 89 | self.iris.x = int( self.x ) 90 | self.iris.y = int( self.y ) # + 10 # have it look down a bit FIXME 91 | if time.monotonic() > self.next_time: 92 | # pick a new "target" for the eye to look at 93 | t = random.uniform(0.25, eye_twitch_time) 94 | self.next_time = time.monotonic() + t 95 | self.tx = iris_cx + random.uniform(-eye_twitch_amount,eye_twitch_amount) 96 | self.ty = iris_cy + random.uniform(-eye_twitch_amount,eye_twitch_amount) 97 | # elif to minimize display changes per update 98 | elif time.monotonic() > self.lid_next_time: 99 | # make the eye blink its eyelids 100 | self.lid_next_time = time.monotonic() + random.uniform( eye_blink_time*0.5, eye_blink_time*1.5) 101 | #self.lid_next_time = time.monotonic() + 100 102 | self.lidpos = self.lidpos + self.lidpos_inc 103 | self.lids[0] = self.lidpos 104 | if self.lidpos == 0 or self.lidpos == eyelid_sprite_cnt-1: 105 | self.lidpos_inc *= -1 # change direction 106 | 107 | self.display.refresh() 108 | 109 | # a list of all the eyes, in this case, only one 110 | the_eyes = [ 111 | Eye( spi0, tft_L0_dc, tft_L0_cs, tft_L0_rst, rot=eye_rotation), 112 | ] 113 | 114 | while True: 115 | for eye in the_eyes: 116 | eye.update() 117 | -------------------------------------------------------------------------------- /larger-tricks/eyeballs/qteye_blink_qualia.py: -------------------------------------------------------------------------------- 1 | # qteye_blink_qualia.py - a stand-alone round LCD "eye" on a ESP32-S3 Qualia board 2 | # 16 Oct 2023 - @todbot / Tod Kurt 3 | # Part of circuitpython-tricks/larger-tricks/eyeballs 4 | # also see: https://todbot.com/blog/2022/05/19/multiple-displays-in-circuitpython-compiling-custom-circuitpython/ 5 | 6 | import time, math, random 7 | import board, busio 8 | import displayio 9 | import adafruit_imageload 10 | 11 | # config: behaviors 12 | eye_twitch_time = 2 # bigger is less twitchy 13 | eye_twitch_amount = 20 # allowable deviation from center for iris 14 | eye_blink_time = 1.8 # bigger is slower 15 | eye_rotation = 00 # 0 or 180, don't do 90 or 270 because too slow (on gc9a01) 16 | 17 | 18 | # config: wiring for QT Py, should work on any QT Py or XIAO board, but ESP32-S3 is fastest 19 | # import gc9a01 20 | # spi0 = busio.SPI(clock=tft0_clk, MOSI=tft0_mosi) 21 | # tft0_clk = board.SCK 22 | # tft0_mosi = board.MOSI 23 | # tft_L0_rst = board.MISO 24 | # tft_L0_dc = board.RX 25 | # tft_L0_cs = board.TX 26 | # display_bus = displayio.FourWire(spi, command=dc, chip_select=cs, reset=rst, baudrate=64_000_000) 27 | # display = gc9a01.GC9A01(display_bus, width=dw, height=dh, rotation=rot) 28 | # display.auto_refresh = False 29 | # dw, dh = 240, 240 # display dimensions 30 | 31 | # config: for Qualia ESP32S3 board and 480x480 round display 32 | # followed the instructions here: 33 | # https://learn.adafruit.com/adafruit-qualia-esp32-s3-for-rgb666-displays/circuitpython-display-setup 34 | # and cribbed from @dexter/@rsbohn's work: 35 | # https://gist.github.com/rsbohn/26a8e69c8fe80112a24e7de09177e8d9 36 | import dotclockframebuffer 37 | from framebufferio import FramebufferDisplay 38 | 39 | init_sequence_TL021WVC02 = bytes(( 40 | b'\xff\x05w\x01\x00\x00\x10' 41 | b'\xc0\x02;\x00' 42 | b'\xc1\x02\x0b\x02' 43 | b'\xc2\x02\x00\x02' 44 | b'\xcc\x01\x10' 45 | b'\xcd\x01\x08' 46 | b'\xb0\x10\x02\x13\x1b\r\x10\x05\x08\x07\x07$\x04\x11\x0e,3\x1d' 47 | b'\xb1\x10\x05\x13\x1b\r\x11\x05\x08\x07\x07$\x04\x11\x0e,3\x1d' 48 | b'\xff\x05w\x01\x00\x00\x11' 49 | b'\xb0\x01]' 50 | b'\xb1\x01C' 51 | b'\xb2\x01\x81' 52 | b'\xb3\x01\x80' 53 | b'\xb5\x01C' 54 | b'\xb7\x01\x85' 55 | b'\xb8\x01 ' 56 | b'\xc1\x01x' 57 | b'\xc2\x01x' 58 | b'\xd0\x01\x88' 59 | b'\xe0\x03\x00\x00\x02' 60 | b'\xe1\x0b\x03\xa0\x00\x00\x04\xa0\x00\x00\x00 ' 61 | b'\xe2\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 62 | b'\xe3\x04\x00\x00\x11\x00' 63 | b'\xe4\x02"\x00' 64 | b'\xe5\x10\x05\xec\xa0\xa0\x07\xee\xa0\xa0\x00\x00\x00\x00\x00\x00\x00\x00' 65 | b'\xe6\x04\x00\x00\x11\x00' 66 | b'\xe7\x02"\x00' 67 | b'\xe8\x10\x06\xed\xa0\xa0\x08\xef\xa0\xa0\x00\x00\x00\x00\x00\x00\x00\x00' 68 | b'\xeb\x07\x00\x00@@\x00\x00\x00' 69 | b'\xed\x10\xff\xff\xff\xba\n\xbfE\xff\xffT\xfb\xa0\xab\xff\xff\xff' 70 | b'\xef\x06\x10\r\x04\x08?\x1f' 71 | b'\xff\x05w\x01\x00\x00\x13' 72 | b'\xef\x01\x08' 73 | b'\xff\x05w\x01\x00\x00\x00' 74 | b'6\x01\x00' 75 | b':\x01`' 76 | b'\x11\x80d' 77 | b')\x802' 78 | )) 79 | 80 | # I'm just guessing at all of this 81 | tft_timings = { 82 | "frequency": 6_500_000, 83 | "width": 480, 84 | "height": 480, 85 | "hsync_pulse_width": 20, 86 | "hsync_front_porch": 40, 87 | "hsync_back_porch": 40, 88 | "vsync_pulse_width": 10, 89 | "vsync_front_porch": 40, 90 | "vsync_back_porch": 40, 91 | "hsync_idle_low": False, 92 | "vsync_idle_low": False, 93 | "de_idle_high": False, 94 | "pclk_active_high": False, 95 | "pclk_idle_high": False, 96 | } 97 | 98 | displayio.release_displays() 99 | board.I2C().deinit() 100 | i2c = busio.I2C(board.SCL, board.SDA, frequency=400_000) 101 | dotclockframebuffer.ioexpander_send_init_sequence( 102 | i2c, init_sequence_TL021WVC02, **(dict(board.TFT_IO_EXPANDER))) 103 | i2c.deinit() 104 | 105 | fb = dotclockframebuffer.DotClockFramebuffer( 106 | **(dict(board.TFT_PINS)), **tft_timings) 107 | display = FramebufferDisplay(fb, rotation=eye_rotation) 108 | display.auto_refresh=False 109 | 110 | dw_real, dh_real = 480, 480 111 | dw, dh = 240,240 # for emulation with 240x240 displays 112 | 113 | # load our eye and iris bitmaps 114 | ## static so load from disk (also can't have it it RAM and eyelids in RAM too) 115 | eyeball_bitmap = displayio.OnDiskBitmap(open("/imgs/eye0_ball2.bmp", "rb")) 116 | eyeball_pal = eyeball_bitmap.pixel_shader 117 | ## moves around, so load into RAM 118 | iris_bitmap, iris_pal = adafruit_imageload.load("imgs/eye0_iris0.bmp") 119 | iris_pal.make_transparent(0) 120 | ## also moves, so load into RAM (hopefully) 121 | try: 122 | eyelid_bitmap, eyelid_pal = adafruit_imageload.load("/imgs/eyelid_spritesheet2.bmp") 123 | except Exception as e: 124 | print("couldn't load",e) 125 | eyelid_bitmap = displayio.OnDiskBitmap(open("/imgs/eyelid_spritesheet2.bmp", "rb")) 126 | eyelid_pal = eyelid_bitmap.pixel_shader 127 | eyelid_sprite_cnt = eyelid_bitmap.width // dw # should be 16 128 | eyelid_pal.make_transparent(1) 129 | 130 | # compute or declare some useful info about the eyes 131 | iris_w, iris_h = iris_bitmap.width, iris_bitmap.height # iris is normally 110x110 132 | iris_cx, iris_cy = dw//2 - iris_w//2, dh//2 - iris_h//2 133 | 134 | 135 | # class to help us track eye info (not needed for this use exactly, but I find it interesting) 136 | class Eye: 137 | """ 138 | global variables used: 139 | - iris_cx, iris_cy 140 | - eye_twitch_amount 141 | - eye_twitch_time 142 | - eye_blink_time 143 | """ 144 | def __init__(self, display, eye_speed=0.25): 145 | # ends up being 80 MHz on ESP32-S3, nice 146 | main = displayio.Group(scale=2) 147 | display.root_group = main 148 | self.display = display 149 | self.eyeball = displayio.TileGrid(eyeball_bitmap, pixel_shader=eyeball_pal) 150 | self.iris = displayio.TileGrid(iris_bitmap, pixel_shader=iris_pal, x=iris_cx,y=iris_cy) 151 | self.lids = displayio.TileGrid(eyelid_bitmap, pixel_shader=eyelid_pal, x=0, y=0, tile_width=dw, tile_height=dw) 152 | main.append(self.eyeball) 153 | main.append(self.iris) 154 | main.append(self.lids) 155 | self.x, self.y = iris_cx, iris_cy 156 | self.tx, self.ty = self.x, self.y 157 | self.next_time = 0 158 | self.eye_speed = eye_speed 159 | self.lidpos = 0 160 | self.lidpos_inc = 1 161 | self.lid_next_time = 0 162 | 163 | def update(self): 164 | # make the eye twitch around 165 | self.x = self.x * (1-self.eye_speed) + self.tx * self.eye_speed # "easing" 166 | self.y = self.y * (1-self.eye_speed) + self.ty * self.eye_speed 167 | self.iris.x = int( self.x ) 168 | self.iris.y = int( self.y ) # + 10 # have it look down a bit FIXME 169 | if time.monotonic() > self.next_time: 170 | # pick a new "target" for the eye to look at 171 | t = random.uniform(0.25, eye_twitch_time) 172 | self.next_time = time.monotonic() + t 173 | self.tx = iris_cx + random.uniform(-eye_twitch_amount,eye_twitch_amount) 174 | self.ty = iris_cy + random.uniform(-eye_twitch_amount,eye_twitch_amount) 175 | # elif to minimize display changes per update 176 | elif time.monotonic() > self.lid_next_time: 177 | # make the eye blink its eyelids 178 | self.lid_next_time = time.monotonic() + random.uniform( eye_blink_time*0.5, eye_blink_time*1.5) 179 | self.lidpos = self.lidpos + self.lidpos_inc 180 | self.lids[0] = self.lidpos 181 | if self.lidpos == 0 or self.lidpos == eyelid_sprite_cnt-1: 182 | self.lidpos_inc *= -1 # change direction 183 | 184 | self.display.refresh() 185 | 186 | 187 | 188 | # a list of all the eyes, in this case, only one 189 | the_eyes = [ 190 | Eye( display ), 191 | ] 192 | 193 | while True: 194 | for eye in the_eyes: 195 | eye.update() 196 | -------------------------------------------------------------------------------- /larger-tricks/eyeballs/qteye_person_sensor.py: -------------------------------------------------------------------------------- 1 | # qteye_person_sensor.py -- quick hacking of Person Sensor by Useful Sensors onto qteye 2 | # 23 Oct 2022 - @todbot / Tod Kurt 3 | # Part of circuitpython-tricks/larger-tricks/eyeballs 4 | # For more information on Person Sensor: 5 | # https://github.com/usefulsensors/person_sensor_docs/blob/main/README.md 6 | # https://github.com/usefulsensors/person_sensor_screen_lock 7 | # https://www.hackster.io/petewarden/auto-lock-your-laptop-screen-with-a-person-sensor-7e0a35 8 | 9 | import time, math, random, struct 10 | import board, busio 11 | import displayio 12 | import adafruit_imageload 13 | import gc9a01 14 | 15 | displayio.release_displays() 16 | 17 | debug_face = True 18 | 19 | dw, dh = 240,240 # display dimensions 20 | 21 | eyeball_bitmap, eyeball_pal = adafruit_imageload.load("imgs/eye0_ball2.bmp") 22 | iris_bitmap, iris_pal = adafruit_imageload.load("imgs/eye0_iris0.bmp") 23 | iris_pal.make_transparent(0) 24 | 25 | iris_w, iris_h = iris_bitmap.width, iris_bitmap.height # iris is normally 110x110 26 | iris_cx, iris_cy = dw//2 - iris_w//2, dh//2 - iris_h//2 27 | r = 20 # allowable deviation from center for iris 28 | 29 | tft0_clk = board.SCK 30 | tft0_mosi = board.MOSI 31 | 32 | tft_L0_rst = board.MISO 33 | tft_L0_dc = board.RX 34 | tft_L0_cs = board.TX 35 | 36 | spi0 = busio.SPI(clock=tft0_clk, MOSI=tft0_mosi) 37 | 38 | class Eye: 39 | def __init__(self, spi, dc, cs, rst, rot=0, eye_speed=0.25, twitch=2): 40 | display_bus = displayio.FourWire(spi, command=dc, chip_select=cs, reset=rst) 41 | display = gc9a01.GC9A01(display_bus, width=dw, height=dh, rotation=rot) 42 | main = displayio.Group() 43 | display.root_group = main 44 | self.display = display 45 | self.eyeball = displayio.TileGrid(eyeball_bitmap, pixel_shader=eyeball_pal) 46 | self.iris = displayio.TileGrid(iris_bitmap, pixel_shader=iris_pal, x = iris_cx, y = iris_cy ) 47 | main.append(self.eyeball) 48 | main.append(self.iris) 49 | self.x, self.y = iris_cx, iris_cy 50 | self.tx, self.ty = self.x, self.y 51 | self.next_time = time.monotonic() 52 | self.eye_speed = eye_speed 53 | self.twitch = twitch 54 | 55 | def update(self, do_random=False, ntx=None, nty=None): 56 | self.x = self.x * (1-self.eye_speed) + self.tx * self.eye_speed # "easing" 57 | self.y = self.y * (1-self.eye_speed) + self.ty * self.eye_speed 58 | self.iris.x = int( self.x ) 59 | self.iris.y = int( self.y ) 60 | 61 | if ntx is not None and nty is not None: 62 | self.tx = iris_cx + ntx 63 | self.ty = iris_cx + nty 64 | else: 65 | if do_random and time.monotonic() > self.next_time: 66 | t = random.uniform(0.25,self.twitch) 67 | self.next_time = time.monotonic() + t 68 | self.tx = iris_cx + random.uniform(-r,r) 69 | self.ty = iris_cy + random.uniform(-r,r) 70 | print("change!",t ) 71 | self.display.refresh() 72 | 73 | the_eye = Eye( spi0, tft_L0_dc, tft_L0_cs, tft_L0_rst, rot=0) 74 | 75 | 76 | i2c = board.STEMMA_I2C() 77 | 78 | # Wait until we can access the bus. 79 | while not i2c.try_lock(): 80 | pass 81 | 82 | last_person_sensor_time = 0 83 | # Keep looping and reading the person sensor results. 84 | def get_faces(): 85 | global last_person_sensor_time 86 | 87 | # The person sensor has the I2C ID of hex 62, or decimal 98. 88 | PERSON_SENSOR_I2C_ADDRESS = 0x62 89 | 90 | # We will be reading raw bytes over I2C, and we'll need to decode them into 91 | # data structures. These strings define the format used for the decoding, and 92 | # are derived from the layouts defined in the developer guide. 93 | PERSON_SENSOR_I2C_HEADER_FORMAT = "BBH" 94 | PERSON_SENSOR_I2C_HEADER_BYTE_COUNT = struct.calcsize( 95 | PERSON_SENSOR_I2C_HEADER_FORMAT) 96 | 97 | PERSON_SENSOR_FACE_FORMAT = "BBBBBBbB" 98 | PERSON_SENSOR_FACE_BYTE_COUNT = struct.calcsize(PERSON_SENSOR_FACE_FORMAT) 99 | 100 | PERSON_SENSOR_FACE_MAX = 4 101 | PERSON_SENSOR_RESULT_FORMAT = PERSON_SENSOR_I2C_HEADER_FORMAT + \ 102 | "B" + PERSON_SENSOR_FACE_FORMAT * PERSON_SENSOR_FACE_MAX + "H" 103 | PERSON_SENSOR_RESULT_BYTE_COUNT = struct.calcsize(PERSON_SENSOR_RESULT_FORMAT) 104 | 105 | # How long to pause between sensor polls. 106 | PERSON_SENSOR_DELAY = 0.3 107 | 108 | # How large a face needs to be to count. 109 | MAIN_FACE_MIN_WIDTH = 16 # was 32 110 | MAIN_FACE_MIN_HEIGHT = 16 111 | 112 | if time.monotonic() - last_person_sensor_time < PERSON_SENSOR_DELAY: 113 | return [] 114 | last_person_sensor_time = time.monotonic() 115 | 116 | read_data = bytearray(PERSON_SENSOR_RESULT_BYTE_COUNT) 117 | i2c.readfrom_into(PERSON_SENSOR_I2C_ADDRESS, read_data) 118 | 119 | offset = 0 120 | (pad1, pad2, payload_bytes) = struct.unpack_from( 121 | PERSON_SENSOR_I2C_HEADER_FORMAT, read_data, offset) 122 | offset = offset + PERSON_SENSOR_I2C_HEADER_BYTE_COUNT 123 | 124 | (num_faces) = struct.unpack_from("B", read_data, offset) 125 | num_faces = int(num_faces[0]) 126 | offset = offset + 1 127 | 128 | faces = [] 129 | for i in range(num_faces): 130 | (box_confidence, box_left, box_top, box_right, box_bottom, id_confidence, id, 131 | is_facing) = struct.unpack_from(PERSON_SENSOR_FACE_FORMAT, read_data, offset) 132 | offset = offset + PERSON_SENSOR_FACE_BYTE_COUNT 133 | face = { 134 | "box_confidence": box_confidence, 135 | "box_left": box_left, 136 | "box_top": box_top, 137 | "box_right": box_right, 138 | "box_bottom": box_bottom, 139 | "id_confidence": id_confidence, 140 | "id": id, 141 | "is_facing": is_facing, 142 | } 143 | faces.append(face) 144 | checksum = struct.unpack_from("H", read_data, offset) 145 | 146 | has_main_face = False 147 | has_lookie_loo = False 148 | for face in faces: 149 | width = face["box_right"] - face["box_left"] 150 | height = face["box_bottom"] - face["box_top"] 151 | big_enough_face = ( 152 | width > MAIN_FACE_MIN_WIDTH and height > MAIN_FACE_MIN_HEIGHT) 153 | if big_enough_face: 154 | if not has_main_face: 155 | has_main_face = True 156 | else: 157 | if face["is_facing"] and face["box_confidence"] > 90: 158 | has_lookie_loo = True 159 | 160 | if debug_face: print("faces:",faces) 161 | return faces 162 | 163 | def map_range(s, a1, a2, b1, b2): 164 | return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) 165 | 166 | while True: 167 | faces = [] 168 | faces = get_faces() 169 | 170 | facex, facey = None,None 171 | if len(faces) > 0: 172 | facex0 = (faces[0]['box_right'] - faces[0]['box_left']) // 2 + faces[0]['box_left'] 173 | facex = map_range(facex0, 0,255, 30,-30) 174 | facey = 0 175 | print("facex: ",facex0,facex) 176 | 177 | the_eye.update( ntx=facex, nty=facey ) 178 | -------------------------------------------------------------------------------- /larger-tricks/fireworks_sprites.py: -------------------------------------------------------------------------------- 1 | # fireworks_sprites.py - show a bunch of fireworks 2 | # the "fireworks_spritesheet.bmp' file can be found in the "bmps" directory 3 | # more info and video demo at this gist: 4 | # https://gist.github.com/todbot/f5a013918c208451424cea9d701f99c5 5 | # 4 Jul 2022 - @todbot / Tod Kurt 6 | import time, random 7 | import board 8 | import displayio 9 | import rainbowio 10 | import adafruit_imageload 11 | 12 | fireworks_count = 5 13 | sprite_fname = "fireworks_spritesheet.bmp" 14 | sprite_count = 30*1 15 | sprite_w,sprite_h = 64,64 16 | 17 | display = board.DISPLAY # our board has built-in display 18 | dw,dh = display.width, display.height # convenience values 19 | 20 | sprite_sheet, sprite_palette = adafruit_imageload.load(sprite_fname) 21 | 22 | maingroup = displayio.Group() # all fireworks go on 'maingroup' 23 | display.root_group = maingroup 24 | 25 | def random_xy(): 26 | return random.randint(0, dw-sprite_w), random.randint(dh//2, dh//2+20) 27 | 28 | def copy_palette(p): # this is fastest way to copy a palette it seems 29 | new_p = displayio.Palette(len(p)) 30 | for i in range(len(p)): new_p[i] = p[i] 31 | return new_p 32 | 33 | def new_colors(p): # first two colors after bg seem to be the "tails" 34 | p[1] = rainbowio.colorwheel( random.randint(0,255) ) 35 | p[2] = rainbowio.colorwheel( random.randint(0,255) ) 36 | 37 | 38 | # holds list of sprites,sprite_index,x,y,launch_time 39 | fireworks = [(None,0,0,0, 0) ] * fireworks_count 40 | # make our fireworks 41 | for i in range(len(fireworks)): 42 | pal = copy_palette(sprite_palette) 43 | pal.make_transparent(0) # make background color transparent 44 | x,y = random_xy() 45 | start_index = random.randint(0,sprite_count-1) # pick random point in fireworks life to start 46 | fwsprite = displayio.TileGrid(sprite_sheet, pixel_shader=pal, 47 | width = 1, height = 1, 48 | tile_width = sprite_w, tile_height = sprite_h) 49 | fireworks[i]= (fwsprite, start_index, x, y, pal) # add sprite, and initial index, x,y, palette 50 | maingroup.append(fwsprite) 51 | 52 | while True: 53 | for i in range(len(fireworks)): 54 | (sprite, sprite_index, x,y, palette) = fireworks[i] # get firework state 55 | # update firework state 56 | launching = sprite_index == 0 and random.random() < 0.9 57 | if launching: 58 | y = y - 3 # move towards top of screen 59 | else: 60 | sprite_index = sprite_index + 1 # explode! 61 | # firework finished 62 | if sprite_index == sprite_count: 63 | sprite_index = 0 # firework is reborn, make a new x,y for it 64 | x,y = random_xy() #random.randint(dw//4, 3*dw//4), random.randint(dh//2,dh//2+20) 65 | new_colors(palette) 66 | # update firework on screen 67 | sprite[0] = sprite_index # set next sprite in animation 68 | sprite.x, sprite.y = x,y # set its x,y 69 | # save the firework state 70 | fireworks[i] = (sprite, sprite_index, x,y, palette) 71 | time.sleep(0.008) # determines our framerate 72 | -------------------------------------------------------------------------------- /larger-tricks/i2s_sdcard_pico.py: -------------------------------------------------------------------------------- 1 | # i2s_sdcard_pico.py -- I2S Audio from SD Card on RP2040 Pico 2 | # 20 May 2022 - @todbot / Tod Kurt 3 | 4 | import time 5 | import board, busio 6 | import audiocore, audiomixer, audiobusio 7 | import sdcardio, storage, os 8 | 9 | # pin definitions 10 | i2s_bclk = board.GP9 # BCK on PCM5102 (connect PCM5102 SCK pin to Gnd) 11 | i2s_wsel = board.GP10 # LCK on PCM5102 12 | i2s_data = board.GP11 # DIN on PCM5102 13 | sd_mosi = board.GP19 14 | sd_sck = board.GP18 15 | sd_miso = board.GP16 16 | sd_cs = board.GP17 17 | 18 | # sd card setup 19 | sd_spi = busio.SPI(clock=sd_sck, MOSI=sd_mosi, MISO=sd_miso) 20 | sdcard = sdcardio.SDCard(sd_spi, sd_cs) 21 | vfs = storage.VfsFat(sdcard) 22 | storage.mount(vfs, "/sd") 23 | 24 | # audio setup 25 | audio = audiobusio. I2SOut(bit_clock=i2s_bclk, word_select=i2s_wsel, data=i2s_data) 26 | mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1, 27 | bits_per_sample=16, samples_signed=True) 28 | audio.play(mixer) # attach mixer to audio playback 29 | 30 | # find all WAV files on SD card 31 | wav_fnames =[] 32 | for filename in os.listdir('/sd'): 33 | if filename.lower().endswith('.wav') and not filename.startswith('.'): 34 | wav_fnames.append("/sd/"+filename) 35 | wav_fnames.sort() # sort alphanumerically for mixtape numbered order 36 | 37 | print("found WAVs:") 38 | for fname in wav_fnames: 39 | print(" ", fname) 40 | 41 | while True: 42 | # play WAV file one after the other 43 | for fname in wav_fnames: 44 | print("playing WAV", fname) 45 | wave = audiocore.WaveFile(open(fname, "rb")) 46 | mixer.voice[0].play(wave, loop=False ) 47 | time.sleep(3) # let WAV play a bit 48 | mixer.voice[0].stop() 49 | -------------------------------------------------------------------------------- /larger-tricks/midi_forward.py: -------------------------------------------------------------------------------- 1 | # 2 | # midi_forward.py -- forward MIDI between MIDI USB and MIDI serial 3 | # 4 | # 8 Jan 2022 - @todbot / Tod Kurt 5 | # 6 | 7 | import time 8 | import board 9 | import busio 10 | import usb_midi 11 | import adafruit_midi 12 | 13 | print("Hello World!") 14 | 15 | midi_usb = adafruit_midi.MIDI( midi_in=usb_midi.ports[0], 16 | midi_out=usb_midi.ports[1] ) 17 | 18 | class MIDI_Forward: 19 | def __init__(self, serial_tx_pin=board.TX, serial_rx_pin=board.RX, timeout=0.01): 20 | uart = busio.UART(tx=serial_tx_pin, rx=serial_rx_pin, 21 | baudrate=31250, timeout=timeout) 22 | self.midiusb_in = usb_midi.ports[0] 23 | self.midiusb_out = usb_midi.ports[1] 24 | self.midiser_in = uart 25 | self.midiser_out = uart 26 | 27 | def forward(self): 28 | bytes_usb = self.midiusb_in.read(30) # 30 from adafruit_midi 29 | if bytes_usb: 30 | self.midiser_out.write(bytes_usb, len(bytes_usb)) 31 | bytes_ser = self.midiser_in.read(30) 32 | if bytes_ser: 33 | selfmidiusb_out.write(bytes_ser, len(bytes_ser)) 34 | 35 | # can specify 'serial_tx_pin=' and 'serial_rx_pin=', or accept defaults 36 | midi_forward = MIDI_Forward() 37 | 38 | while True: 39 | midi_forward.forward() 40 | -------------------------------------------------------------------------------- /larger-tricks/pidaydrummachine.py: -------------------------------------------------------------------------------- 1 | # pidaydrummachine.py - Pi Day Drum Machine 2 | # 14 Mar 2021 - @todbot / Tod Kurt 3 | # samples from: https://freesound.org/people/BaDoink/packs/30682/ 4 | # 5 | import time 6 | import board 7 | import keypad 8 | import audiocore 9 | import audiomixer 10 | from audiopwmio import PWMAudioOut as AudioOut 11 | 12 | time.sleep(3) # wait a bit to reduce noise from CIRCUITPY access 13 | 14 | wav_files = ( 15 | "wav/544413_punch_22k16b.wav", 16 | "wav/544682_snare_22k16b.wav", 17 | "wav/544684_dubhat1_22k16b.wav", 18 | "wav/544694_toggle_22k16b.wav", 19 | "wav/544587_splash1a_22k16b.wav", 20 | ) 21 | 22 | # what keys to use as our drum machine 23 | keys = keypad.Keys((board.GP16, board.GP17, board.GP18, board.GP19, board.GP20), 24 | value_when_pressed=False, pull=True) 25 | 26 | audio = AudioOut(board.GP15) 27 | mixer = audiomixer.Mixer(voice_count=len(wav_files), sample_rate=22050, channel_count=1, 28 | bits_per_sample=16, samples_signed=True) 29 | audio.play(mixer) # attach mixer to audio playback, play with mixer.voice[n].play() 30 | 31 | while True: 32 | event = keys.events.get() 33 | if event and event.pressed: 34 | n = event.key_number 35 | mixer.voice[n].play( audiocore.WaveFile(open(wav_files[n],"rb")) ) 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /larger-tricks/pidaydrummachine_wavs/544413_punch_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/pidaydrummachine_wavs/544413_punch_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/pidaydrummachine_wavs/544587_splash1a_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/pidaydrummachine_wavs/544587_splash1a_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/pidaydrummachine_wavs/544682_snare_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/pidaydrummachine_wavs/544682_snare_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/pidaydrummachine_wavs/544684_dubhat1_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/pidaydrummachine_wavs/544684_dubhat1_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/pidaydrummachine_wavs/544694_toggle_22k16b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/pidaydrummachine_wavs/544694_toggle_22k16b.wav -------------------------------------------------------------------------------- /larger-tricks/pidaydrummachine_wiring.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/pidaydrummachine_wiring.fzz -------------------------------------------------------------------------------- /larger-tricks/pidaydrummachine_wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/pidaydrummachine_wiring.png -------------------------------------------------------------------------------- /larger-tricks/pin_state.py: -------------------------------------------------------------------------------- 1 | # pin_state.py -- look at state of all pins in `board` 2 | # 6 Jul 2022 - @todbot / Tod Kurt 3 | # 4 | import time, board, microcontroller, digitalio 5 | 6 | def get_pin_states(): 7 | pin_states = {} 8 | for name in sorted(dir(board)): 9 | maybe_pin = getattr(board, name) 10 | if isinstance(maybe_pin, microcontroller.Pin): 11 | try: 12 | test_pin = digitalio.DigitalInOut( maybe_pin ) 13 | test_pin.direction = digitalio.Direction.INPUT 14 | pin_states[ name ] = test_pin.value 15 | except: 16 | pin_states[ name ] = None # in use 17 | 18 | return pin_states 19 | 20 | while True: 21 | pin_states = get_pin_states() 22 | for name,value in pin_states.items(): 23 | print(f"{name:<15} = {value}") 24 | time.sleep(1) 25 | -------------------------------------------------------------------------------- /larger-tricks/robust_keyboard.py: -------------------------------------------------------------------------------- 1 | # robust_keyboard_code.py -- demonstrate how to deal with USB disconnect on startup and while running 2 | # 27 Jul 2022 - @todbot / Tod Kurt 3 | 4 | import supervisor 5 | import time 6 | import board 7 | import digitalio 8 | import usb_hid 9 | from adafruit_hid.keyboard import Keyboard 10 | from adafruit_hid.keycode import Keycode 11 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 12 | 13 | def usb_connected(): return supervisor.runtime.usb_connected 14 | 15 | if usb_connected(): # only attempt keyboard creation if USB 16 | keyboard = Keyboard(usb_hid.devices) 17 | keyboard_layout = KeyboardLayoutUS(keyboard) 18 | 19 | # make up some buttons. This is for a FunHouse, but could be any keys 20 | buttons = [] 21 | for pin in (board.BUTTON_UP, board.BUTTON_SELECT, board.BUTTON_DOWN): 22 | switch = digitalio.DigitalInOut(pin) 23 | switch.pull = digitalio.Pull.DOWN # defaults to input 24 | buttons.append(switch) 25 | 26 | # what keycodes to send for each button 27 | keys = [Keycode.A, Keycode.B, Keycode.C] 28 | 29 | while True: 30 | print("usb:", usb_connected(), "buttons:", [b.value for b in buttons] ) 31 | for i in range(len(buttons)): 32 | if buttons[i].value: 33 | if usb_connected(): 34 | try: 35 | keyboard.send( keys[i] ) 36 | except OSError: # also if USB disconnected 37 | pass 38 | 39 | time.sleep(0.1) 40 | -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/code.py: -------------------------------------------------------------------------------- 1 | # sdcard_max_wavs_code.py -- Find out how many WAVs can play at once from SD card 2 | # 30 Jul 2023 - @todbot / Tod Kurt 3 | # 4 | # Plays I2S Audio from SD Card. Finds all WAVs in dir, 5 | # plays one after the other until CircuitPython crashes 6 | # On RP2040 Pico, maximum number of usable WAVs is about 10, with crashing at 14 7 | # Be sure to use fast SD card 8 | # To use: 9 | # - Copy this file to CIRCUITPY drive 10 | # - Copy the "sine_wavs" folder to an SD card. It contains 25 WAV files of different pitched sine waves 11 | # - Adjust the pin definitions below to match your board 12 | # - Plug SD card into your setup 13 | # - Start up CircuitPython board, watch the REPL and listen to the audio output to see when things crash 14 | # 15 | 16 | import time 17 | import board, busio 18 | import audiocore, audiomixer, audiobusio 19 | import sdcardio, storage, os 20 | 21 | wavdir="/sd/sine_wavs" # where we put our WAV files (without the "SD" 22 | 23 | # pin definitions 24 | i2s_bclk = board.GP9 # BCK on PCM5102 (be sure to connect PCM5102 SCK pin to Gnd) 25 | i2s_wsel = board.GP10 # LCK on PCM5102 26 | i2s_data = board.GP11 # DIN on PCM5102 27 | sd_mosi = board.GP19 28 | sd_sck = board.GP18 29 | sd_miso = board.GP16 30 | sd_cs = board.GP17 31 | 32 | # sd card setup 33 | sd_spi = busio.SPI(clock=sd_sck, MOSI=sd_mosi, MISO=sd_miso) 34 | sdcard = sdcardio.SDCard(sd_spi, sd_cs, baudrate=32_000_000) 35 | vfs = storage.VfsFat(sdcard) 36 | storage.mount(vfs, "/sd") 37 | 38 | # find all WAV files on SD card 39 | wav_fnames =[] 40 | for filename in os.listdir(wavdir): 41 | if filename.lower().endswith('.wav') and not filename.startswith('.'): 42 | wav_fnames.append(wavdir+"/"+filename) 43 | 44 | wav_fnames.sort() # sort alphanumerically for mixtape numbered order 45 | print("found WAVs:") 46 | for fname in wav_fnames: 47 | print(" ", fname) 48 | 49 | # audio setup, voice count is max number of WAVs found 50 | audio = audiobusio. I2SOut(bit_clock=i2s_bclk, word_select=i2s_wsel, data=i2s_data) 51 | mixer = audiomixer.Mixer(voice_count=len(wav_fnames), sample_rate=22050, channel_count=1, 52 | bits_per_sample=16, samples_signed=True, buffer_size=1024) 53 | audio.play(mixer) # attach mixer to audio playback 54 | 55 | wavs = [] 56 | for i in range(len(wav_fnames)): 57 | fname = wav_fnames[i] 58 | print('opening',fname) 59 | wave = audiocore.WaveFile(wav_fnames[i]) #, bytearray(1024)) # added max buffer, doesn't help on RP2040 60 | wavs.append(wave) 61 | 62 | print("spi:", sd_spi.frequency) # should be about 32 MHz 63 | 64 | # play WAV file one after the other 65 | while True: 66 | i=0 67 | for i in range(len(wav_fnames)): 68 | print("playing WAV", wav_fnames[i]) 69 | mixer.voice[i].play(wavs[i], loop=True ) 70 | time.sleep(2) # let WAV play a bit 71 | -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/01.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/02.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/03.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/03.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/04.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/04.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/05.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/05.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/06.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/06.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/07.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/07.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/08.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/08.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/09.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/09.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/10.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/10.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/11.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/11.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/12.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/12.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/13.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/13.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/14.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/14.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/15.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/15.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/16.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/17.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/17.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/18.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/18.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/19.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/19.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/20.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/20.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/21.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/21.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/22.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/22.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/23.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/23.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/24.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/24.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/25.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/sdcard_max_wavs/sine_wavs/25.wav -------------------------------------------------------------------------------- /larger-tricks/sdcard_max_wavs/sine_wavs/make_wavs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # make a bunch of test waves 4 | NOTE_START=32 5 | NOTE_END=60 6 | WAVE_TYPE=sin 7 | 8 | for n in $(seq $NOTE_START $NOTE_END) ; do 9 | freq=$(( (440.0/32.0) * (2**(($n-9.0)/12.0)) )) # midi to hz 10 | fn=`printf "%03d.wav" ${n}` 11 | printf "note:%2d freq:%4.1f fn:%s\n" $n $freq $fn 12 | sox -V -r 22050 -n -b 16 -c 1 $fn synth 1 $WAVE_TYPE $freq vol -10dB 13 | done 14 | -------------------------------------------------------------------------------- /larger-tricks/vectorio_rotate_example.py: -------------------------------------------------------------------------------- 1 | # vectorio_rotate_example.py 2 | # 21 Jul 2022 - @todbot / Tod Kurt 3 | # Rotation of a vectorio polygon 4 | 5 | import time 6 | import board 7 | import displayio 8 | import vectorio 9 | import math 10 | 11 | display = board.DISPLAY # assume we have built-in display 12 | 13 | #path0 = [(-20.0,-20.0), (-20.0,20.0), (20.0,20.0), (20.0,-20.0)] 14 | path0_w = 50.000 15 | path0_h = 50.000 16 | path0 = [(26.000,1.003),(26.775,5.948),(27.548,10.879),(28.321,15.813),(31.002,13.640),(34.145,9.746),(37.280,5.861),(37.399,7.204),(35.601,11.864),(33.800,16.532),(34.700,18.496),(39.374,16.693),(44.044,14.891),(46.777,14.205),(42.882,17.349),(38.986,20.492),(35.380,23.553),(40.324,24.328),(45.260,25.101),(50.196,25.874),(46.855,26.649),(41.914,27.423),(36.972,28.198),(37.724,30.489),(41.619,33.633),(45.514,36.776),(45.548,37.689),(40.877,35.887),(36.226,34.093),(33.213,33.946),(35.016,38.619),(36.814,43.281),(38.300,47.402),(35.161,43.513),(32.021,39.623),(28.877,35.726),(27.799,39.514),(27.025,44.458),(26.251,49.400),(25.476,47.654),(24.703,42.719),(23.928,37.776),(22.016,37.098),(18.881,40.982),(15.738,44.877),(14.020,46.302),(15.821,41.634),(17.616,36.982),(18.802,32.924),(14.141,34.722),(9.477,36.522),(4.812,38.322),(7.861,35.666),(11.740,32.536),(15.629,29.397),(13.275,27.923),(8.334,27.149),(3.396,26.375),(3.535,25.603),(8.472,24.829),(13.411,24.056),(15.539,22.530),(11.641,19.384),(7.745,16.240),(4.933,13.725),(9.602,15.526),(14.266,17.326),(18.937,19.128),(17.570,14.898),(15.772,10.238),(13.970,5.569),(15.813,7.216),(18.954,11.108),(22.102,15.009),(23.951,14.082),(24.724,9.142),(25.497,4.212),] 17 | 18 | shape0_w = 50 # not necessarily min/max of path 19 | shape0_h = 50 20 | shape0_color = 0xff00ff # preferred color 21 | 22 | ### vectorio_tools 23 | 24 | def rotate_points(pts, a): 25 | """Rotate a list of points pts by angle a around origin""" 26 | sa, ca = math.sin(a), math.cos(a) # do this computation only once 27 | return [ (p[0]*ca - p[1]*sa, p[1]*ca + p[0]*sa) for p in pts ] # p[0]=x, p[1]=y 28 | 29 | def recenter_points(pts, c=(0,0)): 30 | """Center points around new origin c""" 31 | return [(p[0] - c[0], p[1] - c[1]) for p in pts] 32 | 33 | def rescale_points(pts, orig_w, orig_h, new_w, new_h): 34 | """Rescale point path to new w,h""" 35 | return [ (new_w * p[0] / orig_w, new_h * p[1] / orig_h ) for p in pts] 36 | 37 | def int_points(pts): 38 | """Convert array of flooat points to int, for vectorio""" 39 | return [(int(p[0]),int(p[1])) for p in pts] 40 | 41 | ### 42 | 43 | maingroup = displayio.Group() 44 | display.root_group = maingroup # put main group on display, everything goes in maingroup 45 | 46 | path0 = recenter_points(path0, c=(path0_w//2, path0_h//2)) 47 | 48 | # create vectorio shape, put in a group, put that on maingroup 49 | pal = displayio.Palette(1) 50 | pal[0] = shape0_color 51 | shape0 = vectorio.Polygon(pixel_shader=pal, points=int_points(path0)) 52 | shapeg = displayio.Group() #scale=1, x = 0, y=0) 53 | shapeg.append(shape0) 54 | maingroup.append(shapeg) 55 | 56 | x,y = 80, 80 # starting location for our shape 57 | theta = 0 58 | xvel, yvel = 1.8, 1.0 # xy velocity 59 | theta_vel = 0.04 60 | 61 | last_time = 0 62 | while True: 63 | 64 | elapsed_time = time.monotonic() 65 | 66 | # rotate about shape origin 67 | shape0.points = int_points( rotate_points(path0, theta) ) 68 | 69 | elapsed_time = time.monotonic() - elapsed_time 70 | 71 | if time.monotonic() - last_time > 0.5: 72 | last_time = time.monotonic() 73 | print("elapsed millis %d" % (elapsed_time * 1000)) 74 | 75 | # update position 76 | x,y = x + xvel, y + yvel 77 | theta = theta + theta_vel 78 | shapeg.x = int(x) 79 | shapeg.y = int(y) 80 | 81 | # bounce on screen edge hit 82 | if x < shape0_w//2 or x > (display.width - shape0_w//2): 83 | xvel = -xvel 84 | if y < shape0_h//2 or y > (display.height - shape0_h//2): 85 | yvel = -yvel 86 | 87 | time.sleep(0.05) 88 | -------------------------------------------------------------------------------- /larger-tricks/wav/drumsacuff_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/wav/drumsacuff_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/wav/laser2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/wav/laser2.wav -------------------------------------------------------------------------------- /larger-tricks/wav/laser20.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/wav/laser20.wav -------------------------------------------------------------------------------- /larger-tricks/wav/silence-2sec.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/wav/silence-2sec.wav -------------------------------------------------------------------------------- /larger-tricks/wav/snowpeaks_22k_s16.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/wav/snowpeaks_22k_s16.wav -------------------------------------------------------------------------------- /larger-tricks/wav/vocalchops476663_22k_128k.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython-tricks/0f3023a762e59c159049a795172146eaadbc0642/larger-tricks/wav/vocalchops476663_22k_128k.mp3 -------------------------------------------------------------------------------- /synthio/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Synthio Tricks 4 | =============== 5 | 6 | `synthio-tricks` has it's own repository now! 7 | 8 | [https://github.com/todbot/circuitpython-synthio-tricks](https://github.com/todbot/circuitpython-synthio-tricks) 9 | --------------------------------------------------------------------------------