- 
                Notifications
    You must be signed in to change notification settings 
- Fork 1.3k
Description
CircuitPython version and board name
Adafruit CircuitPython 10.0.3 on 2025-10-17; Adafruit CircuitPlayground Express with samd21g18Code/REPL
# SPDX-FileCopyrightText: 2025 Tom Hoffman
# SPDX-License-Identifier: MIT
# Modular Playground CircuitPython Euclidian Sequencer
DEV_MODE = True
# We use the gc library to check and manage available RAM during development
if DEV_MODE:
    import gc
    gc.collect()
    print("Starting free bytes (after gc) = " + str(gc.mem_free()))
from micropython import const
import math             # need the floor function
import board            # helps set up pins, etc. on the board
import digitalio        # digital (on/off) output to pins, including board LED.
import neopixel         # controls the RGB LEDs on the board
import usb_midi         # basic MIDI over USB support
if DEV_MODE:
    gc.collect()
    print("Free bytes after imports = " + str(gc.mem_free()))
############
# CONSTANTS
_CLOCK_MSG = const(0b11111000)
_LED_COUNT = const(10)
_CLOCK_LIMIT = const(23)  # for quarter note
_MAX_VELOCITY = const(5)
_DEFAULT_VELOCITY = const(4)
_NOTE_COUNT = const(4)
_NOTES = const((36, 38, 59, 47))
# variables
channel_out = 1
############
# board setup steps
# set up the red LED
a_button = digitalio.DigitalInOut(board.BUTTON_A)
a_button.direction = digitalio.Direction.INPUT
a_button.pull = digitalio.Pull.DOWN
b_button = digitalio.DigitalInOut(board.BUTTON_B)
b_button.direction = digitalio.Direction.INPUT
b_button.pull = digitalio.Pull.DOWN
switch = digitalio.DigitalInOut(board.SLIDE_SWITCH)
switch.direction = digitalio.Direction.INPUT
switch.pull = digitalio.Pull.UP
def switchIsLeft():
    return switch.value
pix = neopixel.NeoPixel(board.NEOPIXEL, 10)
pix.brightness = 0.2
pix.fill((255, 0, 255))
pix.show()
innie = usb_midi.ports[0]
outie = usb_midi.ports[1]
if DEV_MODE:
    gc.collect()
    print("Free bytes after board setup = " + str(gc.mem_free()))
############
# Objects
class EuclidianSequencer(object):
    
    def __init__(self):
        self.steps = 9
        self.triggers = 2
        self.sequence = []
        self.active_step = 0
        self.clock_count = 0
    def update(self):
        '''
        Generates a "Euclidian rhythm" where triggers are
        evenly distributed over a given number of steps.
        Takes in a number of triggers and steps, 
        returns a list of Booleans.
        Based on Jeff Holtzkener's Javascript implementation at
        https://medium.com/code-music-noise/euclidean-rhythms-391d879494df
        '''
        slope = self.triggers / self.steps
        result = []
        previous = None
        for i in range(self.steps):
            # adding 0.0001 to correct for imprecise math in CircuitPython.
            current = math.floor((i * slope) + 0.001) 
            result.append(current != previous)
            previous = current
        self.sequence = result
        return self  # so you can do method chaining.
    def addStep(self):
        if self.steps < 10:
            self.steps += 1
        else:
            self.steps = 1
            self.triggers = 0
            self.active_step = 0
        self.update()
    #def incrementActiveStep(self):
    #    pass
     #   #self.active_step = (self.active_step + 1) % self.steps
    
class SequencerApp(object):
    def __init__(self, sequence):
        self.seq = sequence
        self.a = a_button.value
        self.b = b_button.value
        self.switchIsLeft = switch.value
        self.note_index = 0
        self.velocity_index = 4
        self.started = False
        self.starting_step = 0
    def getRed(self, i):
        if i == self.seq.active_step:
            return 64
        else:
            return 0
    def getGreen(self, i):
        if self.seq.sequence[i]:
            return 48
        else:
            return 0
    def getBlue(self, i):
        # add velocity calculation
        return 16
    def updateSequenceDisplay(self):
        for i in range(_LED_COUNT):
            if i < len(self.seq.sequence):
                pix[i] = (self.getRed(i), self.getGreen(i), self.getBlue(i))
            else:
                pix[i] = (0, 0, 0)
            
    def updateConfigDisplay(self):
        pix.fill((8, 8, 8))
    
    def updateNeoPixels(self):
        if switchIsLeft():
            self.updateConfigDisplay()
        else:
            self.updateSequenceDisplay()
        pix.show
    def addTrigger(self):
        pass
    def checkSwitch(self):
        # Return value indicates if NeoPixels need to be updated.
        if self.switchIsLeft != switch.value:
            self.switchIsLeft = switch.value
            return True
        else:
            return False
    def checkA(self):
        # Return value indicates if NeoPixels need to be updated.
        update = False
        if self.a != a_button.value:
            if a_button.value:
                self.seq.addStep()
                update = True
            self.a = a_button.value
        return update
    def getByte(self):
        raw = innie.read(1)
        if raw != b'':
            # convert the byte type to a number
            return ord(raw)     
        else:
            return None
    
    def get_msg(self):
        b = self.getByte()
        if b is None:
            return None
        else:
            return b    
    def incrementClock(self):
        self.clock_count +=1 
        if self.clock_count >= _CLOCK_LIMIT:
            self.clock_count = 0
            #self.seq.incrementActiveStep()
            #self.updateNeoPixels()
            
    def main(self):
        while True:
            # These method calls change state and return True or False.
            # True indicates we need to call updateNeoPixels()
            if (self.checkSwitch() or self.checkA()):
                self.updateNeoPixels()
            msg = self.get_msg()
            if msg is not None:
                # if there is a message flip the red led
                led.value = not(led.value)
                #if msg == _CLOCK_MSG:
                #    pass
                    #self.incrementClock()
app = SequencerApp(EuclidianSequencer().update())
app.updateNeoPixels()
if DEV_MODE:
    gc.collect()
    print("Free bytes after object def and creation = " + str(gc.mem_free()))
app.main()Behavior
MemoryError: memory allocation failed, allocating 208 bytes
or, commenting out line 211:
Starting free bytes (after gc) = 12420
Free bytes after imports = 11764
Free bytes after board setup = 11428
Free bytes after object def and creation = 10580
Description
Even taking into account that a CPX is very memory constrained with CircuitPython, it has always felt completely arbitrary when one is going to hit a MemoryError. It feels like the amount of memory reported by gc is irrelevant, as I have gotten in the habit of watching gc.mem_free() very closely. I just went from having over 10,000 bytes free according to gc, to failing to start at all with a MemoryError by adding an if statement that increments a variable.
Obviously this is not actually random, but I can't figure out how to predict/mitigate the issue.
This is probably the same issue as: #10359
I accept that it is an edge case, as everyone is moving on to more current microcontrollers or using C if they are trying to milk performance out of older ones, but I had hopes I'd be able to eek at least slightly more complex CircuitPython applications on the CPX with my students.
Is this maybe a stack/heap configuration kinda thing? I don't think it is a recent regression as CircuitPython has always done the to me but I keep hoping I can figure out how to work with it. I am motivated to try to do some more detailed debugging if a dev can point me in the right direction.
Additional information
No response