diff --git a/docs/badge/usage.rst b/docs/badge/usage.rst
index e982b59151b0f114a970450ae85cdaf2f880a24c..c4a2de211b76f95fd17c96538d506e598daec092 100644
--- a/docs/badge/usage.rst
+++ b/docs/badge/usage.rst
@@ -171,6 +171,11 @@ IMU Demo
 
 Tilt to accelerate a circle with gravity.
 
+LED Painter
+^^^^^^^^^^^
+
+Use the petals to set a color and the application button to move and lift the brush to paint on the LEDs! On exit the LED data is stored in flash at ``/menu_leds.json``. This file sets the default LED color pattern for the main menus and is not specific to LED Painter, other applications may read and write to it.
+
 Scroll Demo
 ^^^^^^^^^^^
 
diff --git a/python_payload/apps/demo_harmonic/__init__.py b/python_payload/apps/demo_harmonic/__init__.py
index a995239d357617366725f17709ec6316f078e6be..ec52fc126baf066fa6b7e105940a47a9a42e1ab3 100644
--- a/python_payload/apps/demo_harmonic/__init__.py
+++ b/python_payload/apps/demo_harmonic/__init__.py
@@ -617,6 +617,7 @@ class HarmonicApp(Application):
             self.blm.free = True
         self.blm = None
         self._save_settings()
+        super().on_exit()
 
 
 if __name__ == "__main__":
diff --git a/python_payload/apps/led_painter/__init__.py b/python_payload/apps/led_painter/__init__.py
index a51cae1bc4e9445299e59e7f1b87f37075815ed6..5be84a8569c52c5c34a34988f2862ad304bc234b 100644
--- a/python_payload/apps/led_painter/__init__.py
+++ b/python_payload/apps/led_painter/__init__.py
@@ -2,6 +2,8 @@
 import random
 import time
 import math
+import json
+import errno
 
 # flow3r imports
 from st3m.application import Application, ApplicationContext
@@ -27,9 +29,6 @@ class LEDPainter(Application):
     def __init__(self, app_ctx: ApplicationContext) -> None:
         super().__init__(app_ctx)
         self.input = InputController()
-        # self.scroll_R = CapScrollController()
-        # self.scroll_G = CapScrollController()
-        # self.scroll_B = CapScrollController()
         self._cursor = 0
         self._draw = True
         self.STEPS = 30
@@ -58,65 +57,152 @@ class LEDPainter(Application):
             (39, 94),
             (70, 51),
         ]
-        # self.PETAL_POS.reverse()
+        self.PETAL_POS = [
+            tuple([(k - 120) * 1.1 + 120 for k in g]) for g in self.PETAL_POS
+        ]
+
+    def _try_load_settings(self, path):
+        try:
+            with open(path, "r") as f:
+                return json.load(f)
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                raise  # ignore file not found
+
+    def _try_save_settings(self, path, settings):
+        try:
+            with open(path, "w+") as f:
+                f.write(json.dumps(settings))
+                f.close()
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                raise  # ignore file not found
+
+    def _load_settings(self):
+        settings_path = "/flash/menu_leds.json"
+        settings = self._try_load_settings(settings_path)
+        if settings is None:
+            return
+        self.LEDS = settings["leds"]
+        for i in range(40):
+            col = settings["leds"][i]
+            leds.set_rgb(i, col[0], col[1], col[2])
+
+    def _save_settings(self):
+        settings_path = "/flash/menu_leds.json"
+        old_settings = self._try_load_settings(settings_path)
+        file_is_different = False
+        if old_settings is None:
+            file_is_different = True
+        else:
+            try:
+                ref_LEDS = old_settings["leds"]
+                for l in range(40):
+                    if file_is_different:
+                        break
+                    for c in range(3):
+                        if self.LEDS[l][c] != ref_LEDS[l][c]:
+                            file_is_different = True
+            except:
+                file_is_different = True
+
+        if file_is_different:
+            settings = {}
+            settings["leds"] = self.LEDS
+            self._try_save_settings(settings_path, settings)
+
+    def on_enter(self, vm):
+        super().on_enter(vm)
+        self._load_settings()
+
+    def on_exit(self):
+        self._save_settings()
+        super().on_exit()
 
     def draw(self, ctx: Context) -> None:
         ctx.font = ctx.get_font_name(1)
 
         ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
 
+        ctx.rgb(1, 1, 1).rectangle(-31, -31, 62, 62).fill()
         ctx.rgb(self.r / 255, self.g / 255, self.b / 255).rectangle(
             -30, -30, 60, 60
         ).fill()
 
-        if (self.r == 0) and (self.g == 0) and (self.b == 0):
-            ctx.font_size = 20
-            ctx.move_to(-30, 0).rgb(255, 255, 255).text("BLACK")
+        # if (self.r == 0) and (self.g == 0) and (self.b == 0):
+        #    ctx.move_to(0, 0).rgb(255, 255, 255).text("BLACK")
+
+        ctx.font_size = 16
+
+        ctx.text_align = ctx.LEFT
+        ctx.move_to(39, 5 - 17)
+        ctx.rgb(1, 0, 0).text(str(self.r))
+        ctx.move_to(39, 5)
+        ctx.rgb(0, 1, 0).text(str(self.g))
+        ctx.move_to(39, 5 + 17)
+        ctx.rgb(0, 0, 1).text(str(self.b))
 
+        ctx.rgb(0.7, 0.7, 0.7)
+        text_shift = -20
+        ctx.text_align = ctx.RIGHT
+        ctx.move_to(text_shift - 5, -62).text("brush:")
+        ctx.move_to(text_shift - 5, -42).text("<-/->:")
+
+        ctx.text_align = ctx.LEFT
         if self._draw:
             self.LEDS[self._cursor] = [self.r, self.g, self.b]
-            ctx.font_size = 14
-            ctx.move_to(-80, -40).rgb(255, 255, 255).text("(Center L) Brush Down")
+            ctx.move_to(text_shift, -60).text("down")
+            ctx.move_to(text_shift, -44).text("draw")
             for i in range(len(self.LEDS)):
                 leds.set_rgb(i, self.LEDS[i][0], self.LEDS[i][1], self.LEDS[i][2])
 
         else:
-            ctx.font_size = 14
-            ctx.move_to(-80, -40).rgb(255, 255, 255).text("(Center L) Brush Up")
+            ctx.move_to(text_shift, -60).text("up")
+            ctx.move_to(text_shift, -44).text("move")
             for i in range(len(self.LEDS)):
                 leds.set_rgb(i, self.LEDS[i][0], self.LEDS[i][1], self.LEDS[i][2])
             leds.set_rgb(self._cursor, 255, 255, 255)
             if (self.r == 255) and (self.g == 255) and (self.b == 255):
                 leds.set_rgb(self._cursor, 0, 0, 255)
 
-        leds.update()
-        off_x = 130
-        off_y = 110
+        ctx.text_align = ctx.CENTER
         ctx.font_size = 20
+
+        leds.update()
+        off_x = 120
+        off_y = 120
         ctx.move_to(self.PETAL_POS[0][0] - off_x, self.PETAL_POS[0][1] - off_y).rgb(
-            255, 255, 255
+            1, 0, 0
         ).text("R+")
         ctx.move_to(self.PETAL_POS[1][0] - off_x, self.PETAL_POS[1][1] - off_y).rgb(
-            255, 255, 255
+            1, 0, 0
         ).text("R-")
         ctx.move_to(self.PETAL_POS[2][0] - off_x, self.PETAL_POS[2][1] - off_y).rgb(
-            255, 255, 255
+            0, 1, 0
         ).text("G+")
         ctx.move_to(self.PETAL_POS[3][0] - off_x, self.PETAL_POS[3][1] - off_y).rgb(
-            255, 255, 255
+            0, 1, 0
         ).text("G-")
         ctx.move_to(self.PETAL_POS[4][0] - off_x, self.PETAL_POS[4][1] - off_y).rgb(
-            255, 255, 255
+            0, 0, 1
         ).text("B+")
         ctx.move_to(self.PETAL_POS[5][0] - off_x, self.PETAL_POS[5][1] - off_y).rgb(
-            255, 255, 255
+            0, 0, 1
         ).text("B-")
-        ctx.move_to(self.PETAL_POS[6][0] - off_x, self.PETAL_POS[6][1] - off_y).rgb(
-            255, 255, 255
-        ).text("B")
-        ctx.move_to(self.PETAL_POS[7][0] - off_x, self.PETAL_POS[7][1] - off_y).rgb(
-            255, 255, 255
-        ).text("W")
+
+        ctx.font_size = 16
+        pos_x = self.PETAL_POS[6][0] - off_x
+        pos_y = self.PETAL_POS[6][1] - off_y
+        ctx.move_to(pos_x, pos_y).rgb(255, 255, 255)
+        ctx.rectangle(pos_x - 19, pos_y - 18, 38, 26).stroke()
+        ctx.text("BLK")
+
+        pos_x = self.PETAL_POS[7][0] - off_x
+        pos_y = self.PETAL_POS[7][1] - off_y
+        ctx.move_to(pos_x, pos_y).rgb(255, 255, 255)
+        ctx.rectangle(pos_x - 19, pos_y - 18, 38, 26).fill()
+        ctx.rgb(0, 0, 0)
+        ctx.text("WHT")
 
     def think(self, ins: InputState, delta_ms: int) -> None:
         super().think(ins, delta_ms)
diff --git a/python_payload/st3m/application.py b/python_payload/st3m/application.py
index 33217cb18ca1f4f6770f93b59f175bc023e0a6dc..3a5354fb610710152a5dbc6ff275623a841c6b9c 100644
--- a/python_payload/st3m/application.py
+++ b/python_payload/st3m/application.py
@@ -11,6 +11,8 @@ from st3m.goose import Optional, List, Dict
 from st3m.logging import Log
 from st3m import settings
 from ctx import Context
+from st3m import led_patterns
+import leds
 
 import toml
 import os
@@ -65,6 +67,8 @@ class Application(BaseView):
             st3m.wifi.setup_wifi()
         elif self._wifi_preference is False:
             st3m.wifi.disable()
+        leds.set_auto_update(0)
+        leds.set_slew_rate(255)
         super().on_enter(vm)
 
     def on_exit(self) -> None:
@@ -74,10 +78,14 @@ class Application(BaseView):
         if fully_exiting and self._wifi_preference is not None:
             st3m.wifi._onoff_wifi_update()
         super().on_exit()
-        # set the default graphics mode, this is a no-op if
-        # it is already set
         if fully_exiting:
+            # set the default graphics mode, this is a no-op if
+            # it is already set
             sys_display.set_mode(0)
+            # read menu led config from flash and set it
+            led_patterns.set_menu_colors()
+            leds.set_slew_rate(10)
+            leds.set_auto_update(1)
 
     def think(self, ins: InputState, delta_ms: int) -> None:
         super().think(ins, delta_ms)
diff --git a/python_payload/st3m/led_patterns.py b/python_payload/st3m/led_patterns.py
new file mode 100644
index 0000000000000000000000000000000000000000..72adec34735b24ed8711c83e965bafc3c4db9150
--- /dev/null
+++ b/python_payload/st3m/led_patterns.py
@@ -0,0 +1,32 @@
+import leds
+import json
+
+
+def set_menu_colors():
+    """
+    set all LEDs to the configured menu colors if provided in
+    /flash/menu_leds.json. leds.update() must be called externally.
+    """
+    path = "/flash/menu_leds.json"
+    try:
+        with open(path, "r") as f:
+            settings = json.load(f)
+    except OSError:
+        leds.set_all_rgb(0, 0, 0)
+        return
+    for i in range(40):
+        col = settings["leds"][i]
+        leds.set_rgb(i, col[0], col[1], col[2])
+
+
+def highlight_petal(num, r, g, b, num_leds=5):
+    """
+    Sets the LED closest to the petal and num_leds-1 around it to
+    the color. If num_leds is uneven the appearance will be symmetric.
+    leds.update() must be called externally.
+    """
+    num = num % 10
+    if num_leds < 0:
+        num_leds = 0
+    for i in range(num_leds):
+        leds.set_rgb((num * 4 + i - num_leds // 2) % 40, r, g, b)
diff --git a/python_payload/st3m/run.py b/python_payload/st3m/run.py
index d74ea7e0e005d1ea87864376515955ab913da896..e2b1fd61830a6648d7295096ede9ee2aa4c50913 100644
--- a/python_payload/st3m/run.py
+++ b/python_payload/st3m/run.py
@@ -19,6 +19,7 @@ from st3m.application import (
 )
 from st3m.about import About
 from st3m import settings_menu as settings, logging, processors, wifi
+from st3m import led_patterns
 
 import captouch, audio, leds, gc, sys_buttons, sys_display, sys_mode
 import os
@@ -158,6 +159,12 @@ def run_main() -> None:
     bundles = BundleManager()
     bundles.update()
 
+    leds.set_rgb(0, 0, 0, 0)
+    leds.update()
+    leds.set_slew_rate(2)
+    leds.set_auto_update(1)
+    led_patterns.set_menu_colors()
+
     try:
         network.hostname(
             settings.str_hostname.value if settings.str_hostname.value else "flow3r"
diff --git a/sim/fakes/leds.py b/sim/fakes/leds.py
index 7dd26ef90dd948b2f1cb731dc27b7c72ef83814a..6df4deb57d7c42ccf4769186be3c927a29a55a46 100644
--- a/sim/fakes/leds.py
+++ b/sim/fakes/leds.py
@@ -43,3 +43,7 @@ def set_slew_rate(b: int):
 def update():
     _sim.leds_update()
     _sim.render_gui_lazy()
+
+
+def set_auto_update(b: int):
+    pass