From ef4e2ab0dfbc0887ee911d7a6604b0ae0302d06f Mon Sep 17 00:00:00 2001
From: moon2 <moon2protonmail@protonmail.com>
Date: Fri, 22 Sep 2023 02:04:40 +0000
Subject: [PATCH] leds: added random menu mode

---
 docs/api/led_patterns.rst                  | 26 ++++++
 docs/index.rst                             |  1 +
 python_payload/apps/appearance/__init__.py | 65 ++++++++------
 python_payload/st3m/application.py         |  2 +-
 python_payload/st3m/led_patterns.py        | 32 -------
 python_payload/st3m/run.py                 |  2 +-
 python_payload/st3m/settings.py            | 12 ++-
 python_payload/st3m/ui/led_patterns.py     | 99 ++++++++++++++++++++++
 sim/fakes/leds.py                          |  4 +
 9 files changed, 180 insertions(+), 63 deletions(-)
 create mode 100644 docs/api/led_patterns.rst
 delete mode 100644 python_payload/st3m/led_patterns.py
 create mode 100644 python_payload/st3m/ui/led_patterns.py

diff --git a/docs/api/led_patterns.rst b/docs/api/led_patterns.rst
new file mode 100644
index 0000000000..46173d5013
--- /dev/null
+++ b/docs/api/led_patterns.rst
@@ -0,0 +1,26 @@
+.. py:module:: led_patterns
+
+``st3m.ui.led_patterns`` module
+===============================
+
+None of these functions call ``leds.update()``, to actually see the changes you have to do that yourself!
+
+.. py:function:: highlight_petal_rgb(num : int, r : float, g : float, b : float, num_leds : int = 5) -> None
+
+    Sets the LED closest to the petal and num_leds-1 around it to the given rgb color.
+    If num_leds is uneven the appearance will be symmetric.
+
+.. py:function:: shift_all_hsv(h : float  = 0, s : float = 0, v : float = 0) -> None
+
+    Shifts all LEDs by the given values. Clips effective ``s`` and ``v``.
+
+.. py:function:: pretty_pattern() -> None
+
+    Generates a random pretty pattern and loads it into the LED buffer.
+
+.. py:function:: set_menu_colors() -> None
+
+    If not disabled in settings: Tries to load LED colors from /flash/menu_leds.json. Else, or in
+    case of missing file, call ``pretty_pattern()``. Note: There is no caching, it tries to attempt
+    to read the file every time, if you need something faster use ``leds.get_rgb`` to cache it in the
+    application.
diff --git a/docs/index.rst b/docs/index.rst
index 3fdc936c6d..20ce30f793 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -50,6 +50,7 @@ User manual
    api/ctx.rst
    api/leds.rst
    api/colours.rst
+   api/led_patterns.rst
    api/uos.rst
    api/sys_buttons.rst
    api/sys_display.rst
diff --git a/python_payload/apps/appearance/__init__.py b/python_payload/apps/appearance/__init__.py
index 61514471c1..e9ce5824ef 100644
--- a/python_payload/apps/appearance/__init__.py
+++ b/python_payload/apps/appearance/__init__.py
@@ -3,7 +3,7 @@ import math, random, sys_display
 from st3m import settings
 import leds
 import sys_display
-from st3m.ui import colours
+from st3m.ui import colours, led_patterns
 
 
 class App(Application):
@@ -22,12 +22,12 @@ class App(Application):
         self.angle = 0
         self.focused_widget = 1
         self.active = False
-        self.num_widgets = 4
+        self.num_widgets = 5
         self.overhang = -20
         self.line_height = 24
         self.input.buttons.app.left.repeat_enable(800, 300)
         self.input.buttons.app.right.repeat_enable(800, 300)
-        self.mid_x = 50
+        self.mid_x = 42
         self.led_accumulator_ms = 0
         self.blueish = False
         self.half_time = 600
@@ -107,6 +107,19 @@ class App(Application):
                 ctx.text(choices[a] + " ")
         return no
 
+    def draw_boolean(self, label, value, on_str="on", off_str="off"):
+        ctx = self.ctx
+        self.draw_widget(label)
+        if self.widget_no == self.focused_widget and self.active:
+            value = not value
+            self.active = False
+
+        if value:
+            ctx.text(on_str)
+        else:
+            ctx.text(off_str)
+        return value
+
     def draw_number(self, label, step_size, no, unit=""):
         ctx = self.ctx
         self.draw_widget(label)
@@ -171,7 +184,21 @@ class App(Application):
         self.draw_bg()
 
         tmp = self.draw_number(
-            "led brightness", 5, int(settings.num_leds_brightness.value)
+            "display brightness",
+            5,
+            int(settings.num_display_brightness.value),
+            unit="%",
+        )
+        if tmp < 5:
+            tmp = 5
+        if tmp > 100:
+            tmp = 100
+        if tmp != settings.num_display_brightness.value:
+            settings.num_display_brightness.set_value(tmp)
+            sys_display.set_backlight(settings.num_display_brightness.value)
+
+        tmp = self.draw_number(
+            "LED brightness", 5, int(settings.num_leds_brightness.value)
         )
         if tmp < 5:
             tmp = 5
@@ -181,7 +208,7 @@ class App(Application):
             settings.num_leds_brightness.set_value(tmp)
             leds.set_brightness(settings.num_leds_brightness.value)
 
-        tmp = self.draw_number("led speed", 5, int(settings.num_leds_speed.value))
+        tmp = self.draw_number("LED speed", 5, int(settings.num_leds_speed.value))
         if tmp < 0:
             tmp = 0
         elif tmp > 255:
@@ -194,19 +221,13 @@ class App(Application):
             settings.num_leds_speed.set_value(tmp)
             leds.set_slew_rate(settings.num_leds_speed.value)
 
-        tmp = self.draw_number(
-            "display brightness",
-            5,
-            int(settings.num_display_brightness.value),
-            unit="%",
+        tmp = self.draw_boolean(
+            "menu LEDs", settings.onoff_leds_random_menu.value, "random", "user"
         )
-        if tmp < 5:
-            tmp = 5
-        if tmp > 100:
-            tmp = 100
-        if tmp != settings.num_display_brightness.value:
-            settings.num_display_brightness.set_value(tmp)
-            sys_display.set_backlight(settings.num_display_brightness.value)
+        if tmp != settings.onoff_leds_random_menu.value:
+            settings.onoff_leds_random_menu.set_value(tmp)
+            led_patterns.pretty_pattern()
+            leds.update()
 
         self.delta_ms = 0
         self.select_pressed = False
@@ -230,15 +251,7 @@ class App(Application):
 
         while self.led_accumulator_ms > self.half_time:
             self.led_accumulator_ms = self.led_accumulator_ms % self.half_time
-            self.leds_shift_hue(0.8)
-
-    def leds_shift_hue(self, val):
-        for i in range(40):
-            rgb = leds.get_rgb(i)
-            h, s, v = colours.rgb_to_hsv(*rgb)
-            h += val
-            leds.set_rgb(i, *colours.hsv_to_rgb(h, s, v))
-        leds.update()
+            led_patterns.shift_all_hsv(h=0.8)
 
     def on_enter(self, vm):
         super().on_enter(vm)
diff --git a/python_payload/st3m/application.py b/python_payload/st3m/application.py
index da27f383be..6f38e9dabe 100644
--- a/python_payload/st3m/application.py
+++ b/python_payload/st3m/application.py
@@ -10,7 +10,7 @@ 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
+from st3m.ui import led_patterns
 import leds
 
 import toml
diff --git a/python_payload/st3m/led_patterns.py b/python_payload/st3m/led_patterns.py
deleted file mode 100644
index 72adec3473..0000000000
--- a/python_payload/st3m/led_patterns.py
+++ /dev/null
@@ -1,32 +0,0 @@
-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 45bb77b743..c98f213116 100644
--- a/python_payload/st3m/run.py
+++ b/python_payload/st3m/run.py
@@ -20,7 +20,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
+from st3m.ui import led_patterns
 import st3m.wifi
 
 import captouch, audio, leds, gc, sys_buttons, sys_display, sys_mode, media
diff --git a/python_payload/st3m/settings.py b/python_payload/st3m/settings.py
index d814843dfe..4cda284985 100644
--- a/python_payload/st3m/settings.py
+++ b/python_payload/st3m/settings.py
@@ -241,11 +241,16 @@ num_speaker_max_db = StringTunable(
 )
 
 num_display_brightness = StringTunable(
-    "Display Brightness", "system.brightness.display", 100
+    "Display Brightness", "system.appearance.display_brightness", 100
+)
+num_leds_brightness = StringTunable(
+    "LED Brightness", "system.appearance.leds_brightness", 70
 )
-num_leds_brightness = StringTunable("LED Brightness", "system.brightness.leds", 70)
 
-num_leds_speed = StringTunable("LED speed", "system.brightness.leds_speed", 235)
+num_leds_speed = StringTunable("LED Speed", "system.appearance.leds_speed", 235)
+onoff_leds_random_menu = OnOffTunable(
+    "Random Menu LEDs", "system.appearance.leds_random_menu", False
+)
 
 # List of all settings to be loaded/saved
 load_save_settings: List[UnaryTunable] = [
@@ -273,6 +278,7 @@ load_save_settings: List[UnaryTunable] = [
     num_display_brightness,
     num_leds_brightness,
     num_leds_speed,
+    onoff_leds_random_menu,
 ]
 
 
diff --git a/python_payload/st3m/ui/led_patterns.py b/python_payload/st3m/ui/led_patterns.py
new file mode 100644
index 0000000000..a5aa9ccdb1
--- /dev/null
+++ b/python_payload/st3m/ui/led_patterns.py
@@ -0,0 +1,99 @@
+import leds
+import json
+import math
+import random
+
+from st3m.ui import colours
+from st3m.settings import onoff_leds_random_menu
+
+
+def _clip(val):
+    if val > 1.0:
+        return 1.0
+    if val < 0.0:
+        return 1.0
+    return val
+
+
+def set_menu_colors():
+    """
+    set all LEDs to the configured menu colors if provided in
+    /flash/menu_leds.json and settings.onoff_leds_random_menu
+    is false, else calls pretty_pattern.
+    leds.update() must be called externally.
+    """
+    if onoff_leds_random_menu.value:
+        pretty_pattern()
+        return
+
+    path = "/flash/menu_leds.json"
+    try:
+        with open(path, "r") as f:
+            settings = json.load(f)
+    except OSError:
+        pretty_pattern()
+        return
+    for i in range(40):
+        col = settings["leds"][i]
+        leds.set_rgb(i, col[0], col[1], col[2])
+
+
+def pretty_pattern():
+    """
+    generates a pretty random pattern.
+    leds.update() must be called externally.
+    """
+    hsv = [0.0, 0.0, 0.0]
+    hsv[0] = random.random() * math.tau
+    hsv[1] = random.random() * 0.3 + 0.7
+    hsv[2] = random.random() * 0.3 + 0.7
+    start = int(random.random() * 40)
+    for i in range(48):
+        hsv[0] += (random.random() - 0.5) * 2
+        for j in range(1, 3):
+            hsv[j] += (random.random() - 0.5) / 2
+            # asymmetric clipping: draw it to bright colors
+            if hsv[j] < 0.7:
+                hsv[j] = 0.7 + (0.7 - hsv[j]) / 2
+            if hsv[j] > 1:
+                hsv[j] = 1
+
+        g = (i + start) % 40
+        if i < 40:
+            leds.set_rgb(g, *colours.hsv_to_rgb(*hsv))
+        else:
+            hsv_old = colours.rgb_to_hsv(*leds.get_rgb(g))
+            hsv_mixed = [0.0, 0.0, 0.0]
+            k = (i - 39) / 8
+            if abs(hsv[0] - hsv_old[0]) < math.tau / 2:
+                hsv_mixed[0] = hsv_old[0] * k + hsv[0] * (1 - k)
+            elif hsv[0] > hsv_old[0]:
+                hsv_mixed[0] = (hsv_old[0] + math.tau) * k + hsv[0] * (1 - k)
+            else:
+                hsv_mixed[0] = hsv_old[0] * k + (hsv[0] + math.tau) * (1 - k)
+
+            for h in range(1, 3):
+                hsv_mixed[h] = hsv_old[h] * k + hsv[h] * (1 - k)
+            leds.set_rgb(j, *colours.hsv_to_rgb(*hsv_mixed))
+
+
+def shift_all_hsv(h=0, s=0, v=0):
+    for i in range(40):
+        hue, sat, val = colours.rgb_to_hsv(*leds.get_rgb(i))
+        hue += h
+        sat = _clip(sat + s)
+        val = _clip(val + v)
+        leds.set_rgb(i, *colours.hsv_to_rgb(hue, sat, val))
+
+
+def highlight_petal_rgb(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/sim/fakes/leds.py b/sim/fakes/leds.py
index c7144899bc..6b268f4620 100644
--- a/sim/fakes/leds.py
+++ b/sim/fakes/leds.py
@@ -13,6 +13,10 @@ def set_rgb(ix, r, g, b):
     _sim.set_led_rgb(ix, r, g, b)
 
 
+def get_rgb(ix):
+    return 0, 0, 0
+
+
 def set_all_rgb(r, g, b):
     for i in range(40):
         set_rgb(i, r, g, b)
-- 
GitLab