diff --git a/python_payload/apps/led_painter/__init__.py b/python_payload/apps/led_painter/__init__.py
index 3c55c2b375d83a147406b5d9d46323d991401375..09ca2b03ab031adb960e2a4d1e1fc401616cae01 100644
--- a/python_payload/apps/led_painter/__init__.py
+++ b/python_payload/apps/led_painter/__init__.py
@@ -26,6 +26,18 @@ log.info("hello led painter")
 
 
 class LEDPainter(Application):
+    def get_help(self):
+        help_text = (
+            "use petals 0 to 5 to set rgb values or petals "
+            "6 or 7 for shortcuts to black and white.\n\n"
+            "use the app button to move cw/ccw through "
+            "the LEDs by pressing left/right and enable "
+            "or disable drawing to LEDs by pressing down.\n\n"
+            "your pattern is saved when you exit and can be "
+            'set as a "LED wallpaper" in settings->appearance.'
+        )
+        return help_text
+
     def __init__(self, app_ctx: ApplicationContext) -> None:
         super().__init__(app_ctx)
         self.input = InputController()
diff --git a/python_payload/st3m/ui/elements/overlays.py b/python_payload/st3m/ui/elements/overlays.py
index d9ce9960ae50724f9737acdb582a524a92d4a2e4..264a8a8784cb4643d8da3509cec4f31522f1833e 100644
--- a/python_payload/st3m/ui/elements/overlays.py
+++ b/python_payload/st3m/ui/elements/overlays.py
@@ -15,6 +15,7 @@ import st3m.power
 from ctx import Context
 import st3m.wifi
 from st3m.application import viewmanager_is_in_application
+from st3m.ui.help import Help
 
 import math
 import audio
@@ -147,6 +148,9 @@ class Compositor(Responder):
         self._volume = OverlayVolume(self.input)
         self.add_overlay(self._volume)
 
+    def get_help(self):
+        return self.main.get_help()
+
     def _enabled_overlays(self) -> List[Responder]:
         res: List[Responder] = []
         for kind in _all_kinds:
@@ -380,18 +384,19 @@ class OverlaySystemMenu(Overlay):
         self.exit_app = methodprovider.exit_app
         self.close_menu = methodprovider.close_system_menu
         self.go_home = methodprovider.go_home
+        self.get_help = methodprovider.get_help
         self._menu_pos = 0
         self._os_menu_entries = [
             "resume",
             "go home",
+            "help",
             # "mixer",
-            # "help",
         ]
         self._app_menu_entries = [
             "resume",
             "exit app",
+            "help",
             # "mixer",
-            # "help",
         ]
         self.active = False
         self.sub = None
@@ -422,6 +427,8 @@ class OverlaySystemMenu(Overlay):
         if self.input.buttons.app.middle.released:
             if self._menu_pos == 0:
                 self.close_menu()
+            elif self._menu_pos == 2:
+                self.sub = Help(self.input, self.get_help())
             elif self.in_app:
                 if self._menu_pos == 1:
                     self.exit_app()
diff --git a/python_payload/st3m/ui/help.py b/python_payload/st3m/ui/help.py
new file mode 100644
index 0000000000000000000000000000000000000000..a0c882400924e1e2487896a6488945b61309fec7
--- /dev/null
+++ b/python_payload/st3m/ui/help.py
@@ -0,0 +1,55 @@
+from st3m import Responder
+from st3m.utils import wrap_text
+
+
+class Help(Responder):
+    def __init__(self, inputcontroller, help_text):
+        self.input = inputcontroller
+        self.help_text = help_text
+        self.override_os_button_back = False
+        self.line_height = 16
+        self.line_width = 190
+        self.line_height_steps = 3
+        self.x = -self.line_width / 2
+        self.y = -80
+        self.lines = None
+
+    def think(self, ins, delta_ms):
+        if self.lines is None:
+            return
+        ud_dir = self.input.buttons.app.right.pressed
+        ud_dir -= self.input.buttons.app.left.pressed
+        self.y += -ud_dir * self.line_height * self.line_height_steps
+        if self.y > -80:
+            self.y = -80
+        min_y = -80 - self.line_height * max(len(self.lines) - 8, 0)
+        if self.y < min_y:
+            self.y = min_y
+
+    def draw(self, ctx):
+        ctx.rgb(0, 0, 0)
+        ctx.rectangle(-120, -120, 240, 240).fill()
+        ctx.rgb(0x81 / 255, 0xCD / 255, 0xC6 / 255)
+        ctx.move_to(0, self.y)
+        ctx.text_align = ctx.CENTER
+        if not (self.y < -120):
+            ctx.font_size = 24
+            ctx.text("~ help ~")
+        ctx.font_size = 16
+        offset = self.line_height
+        if not isinstance(self.help_text, str):
+            offset += self.line_height
+            ctx.move_to(0, self.y + offset)
+            ctx.text("no help found :/")
+            return
+        ctx.text_align = ctx.LEFT
+        if self.lines is None:
+            self.lines = wrap_text(self.help_text, self.line_width, ctx)
+        for line in self.lines:
+            offset += self.line_height
+            if (self.y + offset) < (-120):
+                continue
+            elif (self.y + offset) > (120 + self.line_height):
+                break
+            ctx.move_to(self.x, self.y + offset)
+            ctx.text(line)
diff --git a/python_payload/st3m/ui/view.py b/python_payload/st3m/ui/view.py
index 0247cb8c58ae21bd68aeecbbdcfcdeb150c188e7..83c52afa920509a0e6c1270019ed319dffd867ce 100644
--- a/python_payload/st3m/ui/view.py
+++ b/python_payload/st3m/ui/view.py
@@ -51,6 +51,15 @@ class View(Responder):
         """
         return False
 
+    def get_help(self) -> Optional[str]:
+        """
+        Returns help string to be displayed by the system menu. May be
+        dynamic/context-dependent. May in the future be also used
+        to return more complex helpy objects, so ideally check the
+        output with isinstance(ret, str).
+        """
+        return None
+
 
 class BaseView(View):
     """
@@ -262,6 +271,11 @@ class ViewManager(Responder):
             return False
         return self._incoming.override_os_button_back
 
+    def get_help(self):
+        if self._incoming is None:
+            return None
+        return self._incoming.get_help()
+
     def exit_view(self):
         if not self._history and self._debug:
             utime.sleep(0.5)
diff --git a/python_payload/st3m/utils.py b/python_payload/st3m/utils.py
index f16295eb23ccd960bfcf2d090b254f8b3dd3e090..4e350a546a49fe185aa0eee82a75f613b3e41aa3 100644
--- a/python_payload/st3m/utils.py
+++ b/python_payload/st3m/utils.py
@@ -152,3 +152,49 @@ def is_simulator() -> bool:
 
 
 tau = math.pi * 2
+
+
+def wrap_text(text, line_width, ctx=None):
+    """
+    wraps text to stay below a certain line width and returns
+    the wrapped lines as a list without newline characters.
+
+    if no ctx is passed the line width is interpreted as number
+    of characters, else it is the rasterized width in pixels.
+    the latter mode uses ctx.text_width() to determine width so
+    do set your ctx up in the way you'll render the result before
+    calling this.
+
+    may infinite-loop if not a single character fits the line
+    or similar edge cases. we didn't spend a lot of time on
+    this.
+    """
+    wrapped_lines = []
+    if ctx is None:
+        len_fun = len
+    else:
+        len_fun = ctx.text_width
+    for line in text.split("\n"):
+        if len_fun(line) <= line_width:
+            wrapped_lines += [line]
+        else:
+            line_words = line.split(" ")
+            subline_words = []
+            while line_words:
+                subline_words.append(line_words.pop(0))
+                if len_fun(" ".join(subline_words)) > line_width:
+                    if len(subline_words) == 1:
+                        split_word = subline_words[0]
+                        for x in range(len(split_word)):
+                            if len_fun(split_word[:x]) > line_width:
+                                break
+                        x -= 1
+                        subline_words[0] = split_word[:x]
+                        line_words.insert(0, split_word[x:])
+                    else:
+                        line_words.insert(0, subline_words.pop())
+                    wrapped_lines.append(" ".join(subline_words))
+                    subline_words = []
+            if subline_words:
+                wrapped_lines.append(" ".join(subline_words))
+    return wrapped_lines