diff --git a/README.md b/README.md
index d424a5c5729285016dd48f5c59cec84b7f45beea..79d150add3bc01cb515cf9d1d841c3a81fd2641d 100644
--- a/README.md
+++ b/README.md
@@ -66,14 +66,10 @@ Standard ESP-IDF project machinery present and working. You can run `idf.py` fro
 
 ### Building
 
-Prepare build:
+Prepare submodules:
 
 ```
-$ cd micropython/
-$ make -C mpy-cross
-$ cd ports/esp32
-$ make submodules
-$ cd ../../../
+$ make -C micropython/ports/esp32 submodules
 ```
 
 Build normally with idf.py:
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index fd1ee897892b02f1399c1d0e89bc9c87d6cbc306..69168406234e3cd552592766755654d2c4697192 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -138,7 +138,7 @@ set(IDF_COMPONENTS
     xtensa
 )
 
-set(MICROPY_FROZEN_MANIFEST ${MICROPY_PORT_DIR}/boards/manifest.py)
+set(MICROPY_FROZEN_MANIFEST ${PROJECT_DIR}/manifest.py)
 set(MICROPY_CROSS_FLAGS -march=xtensawin)
 
 idf_component_register(
diff --git a/manifest.py b/manifest.py
new file mode 100644
index 0000000000000000000000000000000000000000..db151696369b9099e1fcf9ad030ca9566baea62c
--- /dev/null
+++ b/manifest.py
@@ -0,0 +1,2 @@
+include("micropython/ports/esp32/boards")
+freeze("./python_modules")
diff --git a/python_modules/hello.py b/python_modules/hello.py
new file mode 100644
index 0000000000000000000000000000000000000000..30c2e89ec002d8a8f0dfb9d7011e6f9fa92187a9
--- /dev/null
+++ b/python_modules/hello.py
@@ -0,0 +1,2 @@
+# remove me once we have something meaningful to put here
+print("hello world")
diff --git a/python_payload/main.py b/python_payload/main.py
index 59875e6e9559631f47c7165077068b7f318ec577..92aa3c6cadf4049df6b86f06dae0174c866c64bb 100644
--- a/python_payload/main.py
+++ b/python_payload/main.py
@@ -1,11 +1,33 @@
 from machine import Pin
 from hardware import *
+import utils
 import time
 import cap_touch_demo
 import melodic_demo
-boot = Pin(0, Pin.IN)
-vol_up = Pin(35, Pin.IN, Pin.PULL_UP)
-vol_down = Pin(37, Pin.IN, Pin.PULL_UP)
+
+MODULES = [
+    cap_touch_demo,
+    melodic_demo,
+]
+
+BOOTSEL_PIN = Pin(0, Pin.IN)
+VOL_UP_PIN = Pin(35, Pin.IN, Pin.PULL_UP)
+VOL_DOWN_PIN = Pin(37, Pin.IN, Pin.PULL_UP)
+
+CURRENT_APP_RUN = None
+VOLUME = 0
+
+SELECT_TEXT = [
+    " ##  #### #    ####  ##  ##### #",
+    "#  # #    #    #    #  #   #   #",
+    "#    #    #    #    #      #   #",
+    " ##  #### #    #### #      #   #",
+    "   # #    #    #    #      #   #",
+    "#  # #    #    #    #  #   #    ",
+    " ##  #### #### ####  ##    #   #",
+]
+
+BACKGROUND_COLOR = 0
 
 # pin numbers
 # right side: left 37, down 0, right 35
@@ -13,142 +35,76 @@ vol_down = Pin(37, Pin.IN, Pin.PULL_UP)
 # NOTE: All except for 0 should be initialized with Pin.PULL_UP
 # 0 (bootsel) probably not but idk? never tried
 
-def clear_all_leds():
-    for i in range(40):
-        set_led_rgb(i, 0, 0, 0)
-    update_leds()
-
-select = [\
-[0,1,1,0,0,1,1,1,1,0,1,0,0,0,0,1,1,1,1,0,0,1,1,0,0,1,1,1,1,1,0,1],\
-[1,0,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1,0,0,0,1],\
-[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],\
-[0,1,1,0,0,1,1,1,1,0,1,0,0,0,0,1,1,1,1,0,1,0,0,0,0,0,0,1,0,0,0,1],\
-[0,0,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],\
-[1,0,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1,0,0,0,0],\
-[0,1,1,0,0,1,1,1,1,0,1,1,1,1,0,1,1,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1],\
-]
-
-def draw_text_big(text, x, y):
-    ypos = 120+int(len(text)/2) + int(y)
-    xpos = 120+int(len(text[0])/2) + int(x)
-    for l, line in enumerate(text):
-        for p, pixel in enumerate(line):
-            if(pixel == 1):
-                display_draw_pixel(xpos - 2*p, ypos - 2*l, r)
-                display_draw_pixel(xpos - 2*p, ypos - 2*l-1, b)
-                display_draw_pixel(xpos - 2*p-1, ypos - 2*l, b)
-                display_draw_pixel(xpos - 2*p-1, ypos - 2*l-1, r)
-
-def highlight_bottom_petal(num, r, g, b):
-    start = 4 + 8*num
-    for i in range(7):
-        set_led_rgb(((i+start)%40), r, g, b)
-    update_leds()
-    
-def long_bottom_petal_captouch_blocking(num, ms):
-    if(get_captouch((num*2) + 1) == 1):
-        time.sleep_ms(ms)
-        if(get_captouch((num*2) + 1) == 1):
-            return True
-    return False
-    
-foreground = 0
-volume = 0
-
-def init_menu():
-    pass
-
-def draw_rect(x,y,w,h,col):
-    for j in range(w):
-        for k in range(h):
-            display_draw_pixel(x+j,y+k,col)
-            
-    
-def draw_volume_slider():
-    global volume
-    length = 96 + ((volume - 20) * 1.6)
-    if length > 96:
-        length = 96
-    if length < 0:
-        length = 0
-    length = int(length)
-    draw_rect(70,20,100,10,g)
-    draw_rect(71,21,98,8, 0)
-    draw_rect(72+96-length,22,length,6,g)
-    
-
 def run_menu():
-    global foreground
-    display_fill(background)
-    draw_text_big(select, 0, 0)
-    draw_volume_slider()
+    global CURRENT_APP_RUN
+    display_fill(BACKGROUND_COLOR)
+    utils.draw_text_big(SELECT_TEXT, 0, 0)
+    utils.draw_volume_slider(VOLUME)
     display_update()
 
-    if long_bottom_petal_captouch_blocking(0,20):
-        clear_all_leds()
-        highlight_bottom_petal(0,55,0,0)
-        display_fill(background)
-        display_update()
-        foreground = cap_touch_demo.run
-        time.sleep_ms(100)
-        clear_all_leds()
-        cap_touch_demo.foreground()
-    if long_bottom_petal_captouch_blocking(1,20):
-        clear_all_leds()
-        highlight_bottom_petal(1,55,0,0)
-        display_fill(background)
+    selected_petal = None
+    selected_module = None
+    for petal, module in enumerate(MODULES):
+        if utils.long_bottom_petal_captouch_blocking(petal, 20):
+            selected_petal = petal
+            selected_module = module
+            break
+
+    if selected_petal is not None:
+        utils.clear_all_leds()
+        utils.highlight_bottom_petal(selected_petal, 55, 0, 0)
+        display_fill(BACKGROUND_COLOR)
         display_update()
-        foreground = melodic_demo.run
+        CURRENT_APP_RUN = selected_module.run
         time.sleep_ms(100)
-        clear_all_leds()
-        melodic_demo.foreground()
+        utils.clear_all_leds()
+        selected_module.foreground()
 
 def foreground_menu():
-    clear_all_leds()
-    highlight_bottom_petal(0,0,55,55);
-    highlight_bottom_petal(1,55,0,55);
-    display_fill(background)
-    draw_text_big(select, 0, 0)
+    utils.clear_all_leds()
+    utils.highlight_bottom_petal(0,0,55,55);
+    utils.highlight_bottom_petal(1,55,0,55);
+    display_fill(BACKGROUND_COLOR)
+    utils.draw_text_big(SELECT_TEXT, 0, 0)
     display_update()
 
-background = 0;
-g = 0b0000011111100000;
-r = 0b1111100000000000;
-b = 0b0000000000011111;
-
-time.sleep_ms(5000)
-captouch_autocalib()
-cap_touch_demo.init()
-melodic_demo.init()
-
-init_menu()
-foreground = run_menu
-foreground_menu()
-set_global_volume_dB(volume)
-
 def set_rel_volume(vol):
-    global volume
-    vol += volume
+    global VOLUME
+    vol += VOLUME
     if vol > 20:
         vol = 20
     if vol < -40:
         vol = -40
-    volume = vol
+    VOLUME = vol
     if vol == -40: #mute
         set_global_volume_dB(-90)
     else:
-        set_global_volume_dB(volume)
+        set_global_volume_dB(VOLUME)
     time.sleep_ms(100)
 
-while True:
-    if(boot.value() == 0):
-        if foreground == run_menu:
-            captouch_autocalib()
-        else:
-            foreground = run_menu
-            foreground_menu()
-    if(vol_up.value() == 0):
-        set_rel_volume(+3)
-    if(vol_down.value() == 0):
-        set_rel_volume(-3)
-    foreground()
+def main():
+    global CURRENT_APP_RUN
+    time.sleep_ms(5000)
+    captouch_autocalib()
+
+    for module in MODULES:
+        module.init()
+
+    CURRENT_APP_RUN = run_menu
+    foreground_menu()
+    set_global_volume_dB(VOLUME)
+
+    while True:
+        if(BOOTSEL_PIN.value() == 0):
+            if CURRENT_APP_RUN == run_menu:
+                captouch_autocalib()
+            else:
+                CURRENT_APP_RUN = run_menu
+                foreground_menu()
+        if(VOL_UP_PIN.value() == 0):
+            set_rel_volume(+3)
+        if(VOL_DOWN_PIN.value() == 0):
+            set_rel_volume(-3)
+        CURRENT_APP_RUN()
+
+main()
diff --git a/python_payload/utils.py b/python_payload/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..52b2b00f509c8a66ab0328fe396598dd63d2ef6c
--- /dev/null
+++ b/python_payload/utils.py
@@ -0,0 +1,51 @@
+import time
+from hardware import *
+
+RED = 0b1111100000000000
+GREEN = 0b0000011111100000
+BLUE = 0b0000000000011111
+
+def clear_all_leds():
+    for i in range(40):
+        set_led_rgb(i, 0, 0, 0)
+    update_leds()
+
+def draw_text_big(text, x, y):
+    ypos = 120+int(len(text)/2) + int(y)
+    xpos = 120+int(len(text[0])/2) + int(x)
+    for l, line in enumerate(text):
+        for p, pixel in enumerate(line):
+            if(pixel == '#'):
+                display_draw_pixel(xpos - 2*p, ypos - 2*l, RED)
+                display_draw_pixel(xpos - 2*p, ypos - 2*l-1, BLUE)
+                display_draw_pixel(xpos - 2*p-1, ypos - 2*l, BLUE)
+                display_draw_pixel(xpos - 2*p-1, ypos - 2*l-1, RED)
+
+def highlight_bottom_petal(num, RED, GREEN, BLUE):
+    start = 4 + 8*num
+    for i in range(7):
+        set_led_rgb(((i+start)%40), RED, GREEN, BLUE)
+    update_leds()
+
+def long_bottom_petal_captouch_blocking(num, ms):
+    if(get_captouch((num*2) + 1) == 1):
+        time.sleep_ms(ms)
+        if(get_captouch((num*2) + 1) == 1):
+            return True
+    return False
+
+def draw_rect(x,y,w,h,col):
+    for j in range(w):
+        for k in range(h):
+            display_draw_pixel(x+j,y+k,col)
+
+def draw_volume_slider(volume):
+    length = 96 + ((volume - 20) * 1.6)
+    if length > 96:
+        length = 96
+    if length < 0:
+        length = 0
+    length = int(length)
+    draw_rect(70,20,100,10,GREEN)
+    draw_rect(71,21,98,8, 0)
+    draw_rect(72+96-length,22,length,6,GREEN)