From 0d0451c03e571f2a1392ea4706837496f9e638f5 Mon Sep 17 00:00:00 2001 From: Chris I-B Date: Sat, 20 Sep 2025 15:53:38 -0400 Subject: [PATCH 1/8] Move imports to hopefully lower memory footprint on non-wifi models --- software/firmware/europi.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/software/firmware/europi.py b/software/firmware/europi.py index c6e4a5f42..b345634c2 100644 --- a/software/firmware/europi.py +++ b/software/firmware/europi.py @@ -30,7 +30,6 @@ from europi_version import __version__ -from configuration import ConfigSettings from framebuf import FrameBuffer, MONO_HLSB from europi_config import load_europi_config, MODEL_PICO_2W, MODEL_PICO_W @@ -39,7 +38,6 @@ from europi_log import * from experimental.experimental_config import load_experimental_config -from experimental.wifi import WifiConnection, WifiError if sys.implementation.name == "micropython": @@ -127,6 +125,7 @@ def bootsplash(): # Connect to wifi, if supported if europi_config.PICO_MODEL == MODEL_PICO_W or europi_config.PICO_MODEL == MODEL_PICO_2W: try: + from experimental.wifi import WifiConnection, WifiError oled.centre_text( f"""WiFi connecting {experimental_config.WIFI_SSID} From 68c8f761c8669e6688e84ef3ceda9e2c01d0c0a5 Mon Sep 17 00:00:00 2001 From: Chris I-B Date: Tue, 25 Nov 2025 17:18:34 -0500 Subject: [PATCH 2/8] Linting --- software/firmware/europi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/software/firmware/europi.py b/software/firmware/europi.py index b345634c2..e825f7a01 100644 --- a/software/firmware/europi.py +++ b/software/firmware/europi.py @@ -126,6 +126,7 @@ def bootsplash(): if europi_config.PICO_MODEL == MODEL_PICO_W or europi_config.PICO_MODEL == MODEL_PICO_2W: try: from experimental.wifi import WifiConnection, WifiError + oled.centre_text( f"""WiFi connecting {experimental_config.WIFI_SSID} From 07ed281c22879bb1f1fdc5ae2f7e960f12d46626 Mon Sep 17 00:00:00 2001 From: Chris I-B Date: Thu, 11 Jun 2026 19:00:23 -0400 Subject: [PATCH 3/8] Upgrade micropython version used when building UF2 files. Strip comments and docstrings from bundled files to reduce disk usage --- software/uf2_build/Dockerfile | 5 ++- software/uf2_build/copy_and_compile.sh | 20 ++++++++-- software/uf2_build/strip_python.py | 51 ++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 software/uf2_build/strip_python.py diff --git a/software/uf2_build/Dockerfile b/software/uf2_build/Dockerfile index 8cc017209..f766235e6 100644 --- a/software/uf2_build/Dockerfile +++ b/software/uf2_build/Dockerfile @@ -3,11 +3,11 @@ FROM ubuntu:22.04 ENV TERM=xterm DEBIAN_FRONTEND=noninteractive RUN apt update && apt install -y \ - cmake git-core gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential wget python3 + cmake git-core gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential wget python3 sed WORKDIR / -RUN git clone -b v1.25.0 --depth=1 --recursive https://github.com/micropython/micropython.git +RUN git clone -b v1.28.0 --depth=1 --recursive https://github.com/micropython/micropython.git WORKDIR micropython @@ -25,6 +25,7 @@ BootloaderMenu(EUROPI_SCRIPTS).main()\n\ ' > ports/rp2/modules/main.py COPY software/uf2_build/copy_and_compile.sh / +COPY software/uf2_build/strip_python.py / WORKDIR / diff --git a/software/uf2_build/copy_and_compile.sh b/software/uf2_build/copy_and_compile.sh index 8ab4d0266..4606c8679 100644 --- a/software/uf2_build/copy_and_compile.sh +++ b/software/uf2_build/copy_and_compile.sh @@ -5,10 +5,22 @@ echo "Copying EuroPi firmware and scripts to container..." mkdir /micropython/ports/rp2/modules/contrib mkdir /micropython/ports/rp2/modules/experimental mkdir /micropython/ports/rp2/modules/tools -cp -r europi/software/firmware/*.py /micropython/ports/rp2/modules -cp -r europi/software/firmware/experimental/*.py /micropython/ports/rp2/modules/experimental -cp -r europi/software/firmware/tools/*.py /micropython/ports/rp2/modules/tools -cp -r europi/software/contrib/*.py /micropython/ports/rp2/modules/contrib +for pyfile in $(ls europi/software/firmware/*.py); do + f=$(basename $pyfile) + python3 /strip_python.py "$pyfile" "/micropython/ports/rp2/modules/$f" +done +for pyfile in $(ls europi/software/firmware/experimental/*.py); do + f=$(basename $pyfile) + python3 /strip_python.py "$pyfile" "/micropython/ports/rp2/modules/experimental/$f" +done +for pyfile in $(ls europi/software/firmware/tools/*.py); do + f=$(basename $pyfile) + python3 /strip_python.py "$pyfile" "/micropython/ports/rp2/modules/tools/$f" +done +for pyfile in $(ls europi/software/contrib/*.py); do + f=$(basename $pyfile) + python3 /strip_python.py "$pyfile" "/micropython/ports/rp2/modules/contrib/$f" +done echo "Compiling micropython and firmware modules..." cd /micropython/ports/rp2 diff --git a/software/uf2_build/strip_python.py b/software/uf2_build/strip_python.py new file mode 100644 index 000000000..a8cadcc13 --- /dev/null +++ b/software/uf2_build/strip_python.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Strip comments and docstrings from a file. + +Copied from https://gist.github.com/BroHui/aca2b8e6e6bdf3cb4af4b246c9837fa3 with modifications. +""" + +import sys, token, tokenize + + +def process_file(infile, outfile): + """ + Remove comments and docstrings from one file and write the result to another. + + @param infile The path to the file to process + @param outfile The path to the file we're generating + """ + source = open(infile, "r") + mod = open(outfile, "w") + + prev_toktype = token.INDENT + last_lineno = -1 + last_col = 0 + + tokgen = tokenize.generate_tokens(source.readline) + for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen: + if 0: # Change to if 1 to see the tokens fly by. + print("%10s %-14s %-20r %r" % ( + tokenize.tok_name.get(toktype, toktype), + "%d.%d-%d.%d" % (slineno, scol, elineno, ecol), + ttext, ltext + )) + if slineno > last_lineno: + last_col = 0 + if scol > last_col: + mod.write(" " * (scol - last_col)) + if toktype == token.STRING and prev_toktype == token.INDENT: + # Docstring + mod.write("#--") + elif toktype == tokenize.COMMENT: + # Comment + mod.write("##") + else: + mod.write(ttext) + prev_toktype = toktype + last_col = ecol + last_lineno = elineno + + +if __name__ == '__main__': + process_file(sys.argv[1], sys.argv[2]) From 9d3b4ec99fa7d4f8485ebc0a4ab84a428850077f Mon Sep 17 00:00:00 2001 From: Chris I-B Date: Sun, 28 Jun 2026 15:38:26 -0400 Subject: [PATCH 4/8] Fix quotation marks --- software/uf2_build/strip_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/uf2_build/strip_python.py b/software/uf2_build/strip_python.py index a8cadcc13..97e73cf77 100644 --- a/software/uf2_build/strip_python.py +++ b/software/uf2_build/strip_python.py @@ -47,5 +47,5 @@ def process_file(infile, outfile): last_lineno = elineno -if __name__ == '__main__': +if __name__ == "__main__": process_file(sys.argv[1], sys.argv[2]) From 3f0964d25764881c11cff63b43162049c6d91390 Mon Sep 17 00:00:00 2001 From: Chris I-B Date: Sun, 28 Jun 2026 15:45:52 -0400 Subject: [PATCH 5/8] Linting of the strip_python script --- software/uf2_build/strip_python.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/software/uf2_build/strip_python.py b/software/uf2_build/strip_python.py index 97e73cf77..ed9fdfcec 100644 --- a/software/uf2_build/strip_python.py +++ b/software/uf2_build/strip_python.py @@ -24,12 +24,16 @@ def process_file(infile, outfile): tokgen = tokenize.generate_tokens(source.readline) for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen: - if 0: # Change to if 1 to see the tokens fly by. - print("%10s %-14s %-20r %r" % ( - tokenize.tok_name.get(toktype, toktype), - "%d.%d-%d.%d" % (slineno, scol, elineno, ecol), - ttext, ltext - )) + if 0: # Change to if 1 to see the tokens fly by. + print( + "%10s %-14s %-20r %r" + % ( + tokenize.tok_name.get(toktype, toktype), + "%d.%d-%d.%d" % (slineno, scol, elineno, ecol), + ttext, + ltext, + ) + ) if slineno > last_lineno: last_col = 0 if scol > last_col: From af80c1b473fb844a367d25d754b00724e24a3d10 Mon Sep 17 00:00:00 2001 From: Chris I-B Date: Sat, 4 Jul 2026 13:12:00 -0400 Subject: [PATCH 6/8] Reduce code duplication in copy_and_compile.sh, use bash instead of sh for the entrypoint shell --- software/uf2_build/Dockerfile | 2 +- software/uf2_build/copy_and_compile.sh | 38 ++++++++++++++++---------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/software/uf2_build/Dockerfile b/software/uf2_build/Dockerfile index f766235e6..00e74fe16 100644 --- a/software/uf2_build/Dockerfile +++ b/software/uf2_build/Dockerfile @@ -29,4 +29,4 @@ COPY software/uf2_build/strip_python.py / WORKDIR / -ENTRYPOINT [ "sh", "copy_and_compile.sh" ] +ENTRYPOINT [ "bash", "copy_and_compile.sh" ] diff --git a/software/uf2_build/copy_and_compile.sh b/software/uf2_build/copy_and_compile.sh index 4606c8679..b4920e7bf 100644 --- a/software/uf2_build/copy_and_compile.sh +++ b/software/uf2_build/copy_and_compile.sh @@ -5,21 +5,29 @@ echo "Copying EuroPi firmware and scripts to container..." mkdir /micropython/ports/rp2/modules/contrib mkdir /micropython/ports/rp2/modules/experimental mkdir /micropython/ports/rp2/modules/tools -for pyfile in $(ls europi/software/firmware/*.py); do - f=$(basename $pyfile) - python3 /strip_python.py "$pyfile" "/micropython/ports/rp2/modules/$f" -done -for pyfile in $(ls europi/software/firmware/experimental/*.py); do - f=$(basename $pyfile) - python3 /strip_python.py "$pyfile" "/micropython/ports/rp2/modules/experimental/$f" -done -for pyfile in $(ls europi/software/firmware/tools/*.py); do - f=$(basename $pyfile) - python3 /strip_python.py "$pyfile" "/micropython/ports/rp2/modules/tools/$f" -done -for pyfile in $(ls europi/software/contrib/*.py); do - f=$(basename $pyfile) - python3 /strip_python.py "$pyfile" "/micropython/ports/rp2/modules/contrib/$f" + +# Directories we copy stripped Python files _from_ +SRC_DIRS=(europi/software/firmware +europi/software/firmware/experimental +europi/software/firmware/tools +europi/software/contrib) + +# Directories we copy stripped Python files _to_ +# Order must match SRC_DIRS +DST_DIRS=(/micropython/ports/rp2/modules +/micropython/ports/rp2/modules/experimental +/micropython/ports/rp2/modules/tools +/micropython/ports/rp2/modules/contrib) + +for i in ${!SRC_DIRS[@]}; do + src_dir=${SRC_DIRS[i]} + dst_dir=${DST_DIRS[i]} + + for pyfile in $(ls ${src_dir}/*.py); do + f=$(basename $pyfile) + echo "Stripping $pyfile -> $dst_dir" + python3 /strip_python.py $pyfile ${dst_dir}/$f + done done echo "Compiling micropython and firmware modules..." From 7a49cc86ab3ebd22c25398e1d0f108d20f466c5b Mon Sep 17 00:00:00 2001 From: Chris I-B Date: Sat, 4 Jul 2026 22:36:07 -0400 Subject: [PATCH 7/8] Change the order of the boards, remove stale TODO, add a new TODO --- software/uf2_build/copy_and_compile.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/software/uf2_build/copy_and_compile.sh b/software/uf2_build/copy_and_compile.sh index b4920e7bf..d3fc1b738 100644 --- a/software/uf2_build/copy_and_compile.sh +++ b/software/uf2_build/copy_and_compile.sh @@ -34,8 +34,8 @@ echo "Compiling micropython and firmware modules..." cd /micropython/ports/rp2 # Pico W must be last; we need to remove some code to make it fit -# TODO: add RPI_PICO2_W once it's supported -BOARDS="RPI_PICO RPI_PICO2 RPI_PICO_W RPI_PICO2_W" +# TODO: What do we actually need to remove? +BOARDS="RPI_PICO2 RPI_PICO RPI_PICO2_W RPI_PICO_W" for b in $BOARDS; do echo "make BOARD=$b" From 8799a70de798bed013e47b1cae7b94adc5346d46 Mon Sep 17 00:00:00 2001 From: Chris I-B Date: Sun, 5 Jul 2026 17:45:56 -0400 Subject: [PATCH 8/8] Separate tick into 2 functions; the previous one was too long for the UF2 builder to compile as a native function --- software/contrib/pams.py | 160 +++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 75 deletions(-) diff --git a/software/contrib/pams.py b/software/contrib/pams.py index 28dbc3324..c1a8678ec 100644 --- a/software/contrib/pams.py +++ b/software/contrib/pams.py @@ -1213,81 +1213,7 @@ def tick(self): # reset waves are always low; the clock's stop() function handles triggering them out_volts = 0.0 else: - if self.wave_counter % 2 == 0: - # first half of the swing; if swing < 50% this is short, otherwise long - swing_amt = self.swing.value / 100.0 - else: - # second half of the swing; if swing < 50% this is long, otherwise short - swing_amt = (100 - self.swing.value) / 100.0 - ticks_per_note = round(2 * MasterClock.PPQN / self.real_clock_mod * swing_amt) - if ticks_per_note == 0: - # we're swinging SO HARD that one beat is squashed out of existence! - # move immediately to the other beat - self.e_position = self.e_position + 1 - if self.e_position >= len(self.e_pattern): - self.e_position = 0 - ticks_per_note = round(2 * MasterClock.PPQN / self.real_clock_mod) - - e_step = self.e_pattern[self.e_position] - wave_position = self.clock.elapsed_pulses % ticks_per_note - # are we starting a new repeat of the pattern? - rising_edge = (wave_position == int(self.phase.value * ticks_per_note / 100.0)) and e_step - # determine if we should skip this sample playback - if rising_edge: - self.skip_this_step = random.randint(0, 100) < self.skip.value - self.wave_counter += 1 - - wave_sample = int(e_step) * int (not self.skip_this_step) - if self.wave_shape.value == WAVE_RANDOM: - if rising_edge and not self.skip_this_step: - wave_sample = random.random() * (self.amplitude.value / 100.0) + (self.width.value / 100.0) - else: - wave_sample = self.previous_wave_sample - elif self.wave_shape.value == WAVE_AIN: - if rising_edge and not self.skip_this_step: - wave_sample = CV_INS["AIN"].percent() * self.amplitude.value / 100.0 - else: - wave_sample = self.previous_wave_sample - elif self.wave_shape.value == WAVE_KNOB: - if rising_edge and not self.skip_this_step: - wave_sample = CV_INS["KNOB"].percent() * self.amplitude.value / 100.0 - else: - wave_sample = self.previous_wave_sample - elif self.wave_shape.value == WAVE_SQUARE: - wave_sample = wave_sample * self.square_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) - elif self.wave_shape.value == WAVE_TRIANGLE: - wave_sample = wave_sample * self.triangle_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) - elif self.wave_shape.value == WAVE_SIN: - wave_sample = wave_sample * self.sine_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) - elif self.wave_shape.value == WAVE_ADSR: - wave_sample = wave_sample * self.adsr_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) - elif self.wave_shape.value == WAVE_TURING: - wave_sample = self.turing_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) - else: - wave_sample = 0.0 - - self.previous_wave_sample = wave_sample - out_volts = wave_sample * MAX_OUTPUT_VOLTAGE - - if self.quantizer.mapped_value is not None: - (out_volts, note) = self.quantizer.mapped_value.quantize(out_volts, self.root.value) - - if wave_position == ticks_per_note - 1: - if self.next_e_pattern: - # if we just finished a waveform and we have a new euclidean pattern, start it - # this will always line up with the current beat, but may be rotated relative to - # other patterns currently playing. - # rather than do a lot of math, treat this as a feature that if you change patterns - # while playing, the new pattern starts right away instead of waiting for for the - # end of (a potentially long, slow) pattern to finish - self.e_position = 0 - self.e_pattern = self.next_e_pattern - self.next_e_pattern = None - else: - # if we've reached end of the euclidean pattern start it again - self.e_position = self.e_position + 1 - if self.e_position >= len(self.e_pattern): - self.e_position = 0 + out_volts = self.wave_gen() # If the clock modifier was changed, apply the new value now if self.clock_mod_dirty: @@ -1295,6 +1221,90 @@ def tick(self): self.out_volts = out_volts + @micropython.native + def wave_gen(self): + """Calculates the output voltage for the output channel. + + @return The desired output voltage to be applied + """ + if self.wave_counter % 2 == 0: + # first half of the swing; if swing < 50% this is short, otherwise long + swing_amt = self.swing.value / 100.0 + else: + # second half of the swing; if swing < 50% this is long, otherwise short + swing_amt = (100 - self.swing.value) / 100.0 + ticks_per_note = round(2 * MasterClock.PPQN / self.real_clock_mod * swing_amt) + if ticks_per_note == 0: + # we're swinging SO HARD that one beat is squashed out of existence! + # move immediately to the other beat + self.e_position = self.e_position + 1 + if self.e_position >= len(self.e_pattern): + self.e_position = 0 + ticks_per_note = round(2 * MasterClock.PPQN / self.real_clock_mod) + + e_step = self.e_pattern[self.e_position] + wave_position = self.clock.elapsed_pulses % ticks_per_note + # are we starting a new repeat of the pattern? + rising_edge = (wave_position == int(self.phase.value * ticks_per_note / 100.0)) and e_step + # determine if we should skip this sample playback + if rising_edge: + self.skip_this_step = random.randint(0, 100) < self.skip.value + self.wave_counter += 1 + + wave_sample = int(e_step) * int (not self.skip_this_step) + if self.wave_shape.value == WAVE_RANDOM: + if rising_edge and not self.skip_this_step: + wave_sample = random.random() * (self.amplitude.value / 100.0) + (self.width.value / 100.0) + else: + wave_sample = self.previous_wave_sample + elif self.wave_shape.value == WAVE_AIN: + if rising_edge and not self.skip_this_step: + wave_sample = CV_INS["AIN"].percent() * self.amplitude.value / 100.0 + else: + wave_sample = self.previous_wave_sample + elif self.wave_shape.value == WAVE_KNOB: + if rising_edge and not self.skip_this_step: + wave_sample = CV_INS["KNOB"].percent() * self.amplitude.value / 100.0 + else: + wave_sample = self.previous_wave_sample + elif self.wave_shape.value == WAVE_SQUARE: + wave_sample = wave_sample * self.square_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) + elif self.wave_shape.value == WAVE_TRIANGLE: + wave_sample = wave_sample * self.triangle_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) + elif self.wave_shape.value == WAVE_SIN: + wave_sample = wave_sample * self.sine_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) + elif self.wave_shape.value == WAVE_ADSR: + wave_sample = wave_sample * self.adsr_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) + elif self.wave_shape.value == WAVE_TURING: + wave_sample = self.turing_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0) + else: + wave_sample = 0.0 + + self.previous_wave_sample = wave_sample + out_volts = wave_sample * MAX_OUTPUT_VOLTAGE + + if self.quantizer.mapped_value is not None: + (out_volts, note) = self.quantizer.mapped_value.quantize(out_volts, self.root.value) + + if wave_position == ticks_per_note - 1: + if self.next_e_pattern: + # if we just finished a waveform and we have a new euclidean pattern, start it + # this will always line up with the current beat, but may be rotated relative to + # other patterns currently playing. + # rather than do a lot of math, treat this as a feature that if you change patterns + # while playing, the new pattern starts right away instead of waiting for for the + # end of (a potentially long, slow) pattern to finish + self.e_position = 0 + self.e_pattern = self.next_e_pattern + self.next_e_pattern = None + else: + # if we've reached end of the euclidean pattern start it again + self.e_position = self.e_position + 1 + if self.e_position >= len(self.e_pattern): + self.e_position = 0 + + return out_volts + @micropython.native def apply(self): """Apply the calculated voltage to the output channel