From e87cdd45f3fe1e9020a27b645fa628fdba73470a Mon Sep 17 00:00:00 2001
From: fleur <spacecarrot@fleurshax.net>
Date: Mon, 12 Aug 2019 14:39:54 +0000
Subject: [PATCH] feat: Add proper LED module

The new LED modules allow a much broader range of uses of the RGB LEDs.
It also includes smart powersaving and options to set all LEDs at once.
---
 Documentation/conf.py            |   2 +-
 Documentation/pycardium/leds.rst |  40 +----
 epicardium/epicardium.h          | 185 +++++++++++++++++++++--
 epicardium/modules/leds.c        |  85 ++++++++++-
 hw-tests/dual-core/main.c        |  21 +--
 hw-tests/hello-world/main.c      |   2 +-
 lib/card10/leds.c                | 178 +++++++++++++++++++---
 lib/card10/leds.h                |  12 +-
 lib/card10/portexpander.c        |  18 +++
 lib/card10/portexpander.h        |   2 +
 pycardium/meson.build            |   2 +-
 pycardium/modules/leds.c         |  48 ------
 pycardium/modules/py/ledfx.py    |  48 ++++++
 pycardium/modules/py/leds.py     | 225 ++++++++++++++++++++++++++++
 pycardium/modules/py/meson.build |   3 +
 pycardium/modules/py/pride.py    | 130 ++++++++++++++++
 pycardium/modules/qstrdefs.h     |  15 +-
 pycardium/modules/sys_leds.c     | 250 +++++++++++++++++++++++++++++++
 18 files changed, 1125 insertions(+), 141 deletions(-)
 delete mode 100644 pycardium/modules/leds.c
 create mode 100644 pycardium/modules/py/ledfx.py
 create mode 100644 pycardium/modules/py/leds.py
 create mode 100644 pycardium/modules/py/pride.py
 create mode 100644 pycardium/modules/sys_leds.c

diff --git a/Documentation/conf.py b/Documentation/conf.py
index 769dbc5d..7dafb184 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -87,7 +87,7 @@ html_context = {
 # }}}
 
 # -- Options for Auto-Doc ---------------------------------------------------- {{{
-autodoc_mock_imports = ["sys_display", "ucollections", "urandom", "utime"]
+autodoc_mock_imports = ["sys_display", "sys_leds", "ucollections", "urandom", "utime"]
 
 autodoc_member_order = "bysource"
 # }}}
diff --git a/Documentation/pycardium/leds.rst b/Documentation/pycardium/leds.rst
index 44af2a6a..e3cae9ab 100644
--- a/Documentation/pycardium/leds.rst
+++ b/Documentation/pycardium/leds.rst
@@ -1,38 +1,8 @@
 ``leds`` - LEDs
 ===============
+The ``leds`` module provides functions to interact with card10's RGB LEDs.
+This is the 11 LEDs above the display and 4 LEDs on the underside of the
+top-board, in the four corners.
 
-.. py:function:: leds.set(led, color)
-
-   Set one of the card10's RGB LEDs to a certain color.
-
-   **Example**:
-
-   .. code-block:: python
-
-      import leds, color
-
-      # Set all of the top LEDs to red
-      for i in range(11):
-         leds.set(i, color.RED)
-
-   :param led:  Which led to set.  0-10 are the leds on the top
-      and 11-14 are the 4 "ambient" leds.
-   :param color:  What color to set the led to.  Should be a
-      :py:class:`color.Color` but any list/tuple with 3 elements
-      will work just as well.
-
-.. py:data:: leds.BOTTOM_RIGHT
-
-   Index of the LED in the bottom right of card10.
-
-.. py:data:: leds.BOTTOM_LEFT
-
-   Index of the LED in the bottom left of card10.
-
-.. py:data:: leds.TOP_RIGHT
-
-   Index of the LED in the top right of card10.
-
-.. py:data:: leds.TOP_LEFT
-
-   Index of the LED in the top left of card10.
+.. automodule:: leds
+    :members:
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 34af173e..2c65e376 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -5,10 +5,12 @@
 #include <errno.h>
 
 #ifndef __SPHINX_DOC
-/* stddef.h is not recognized by hawkmoth for some odd reason */
+/* Some headers are not recognized by hawkmoth for some odd reason */
 #include <stddef.h>
+#include <stdbool.h>
 #else
 typedef unsigned int size_t;
+typedef _Bool bool;
 #endif /* __SPHINX_DOC */
 
 /*
@@ -63,6 +65,18 @@ typedef unsigned int size_t;
 #define API_RTC_SCHEDULE_ALARM     0x51
 
 #define API_LEDS_SET               0x60
+#define API_LEDS_SET_HSV           0x61
+#define API_LEDS_PREP              0x62
+#define API_LEDS_PREP_HSV          0x63
+#define API_LEDS_UPDATE            0x64
+#define API_LEDS_SET_POWERSAVE     0x65
+#define API_LEDS_SET_ROCKET        0x66
+#define API_LEDS_SET_FLASHLIGHT    0x67
+#define API_LEDS_DIM_TOP           0x68
+#define API_LEDS_DIM_BOTTOM        0x69
+#define API_LEDS_SET_ALL           0x6a
+#define API_LEDS_SET_ALL_HSV       0x6b
+#define API_LEDS_SET_GAMMA_TABLE   0x6c
 
 #define API_VIBRA_SET              0x70
 #define API_VIBRA_VIBRATE          0x71
@@ -211,22 +225,169 @@ API_ISR(EPIC_INT_CTRL_C, epic_isr_ctrl_c);
  */
 
 /**
- * Set one of card10's RGB LEDs to a certain color.
+ * Set one of card10's RGB LEDs to a certain color in RGB format.
  *
- * .. warning::
+ * This function is rather slow when setting multiple LEDs, use
+ * :c:func:`leds_set_all` or :c:func:`leds_prep` + :c:func:`leds_update`
+ * instead.
  *
- *    This API function is not yet stable and is this not part of the API
- *    freeze.  Any binary using :c:func:`epic_leds_set` might stop working at
- *    any time.  Once this warning is removed, the function can be considered
- *    stable like the rest of the API.
- *
- * :param led:  Which led to set.  0-10 are the leds on the top and 11-14 are the 4 "ambient" leds.
- * :param r:  Red component of the color.
- * :param g:  Green component of the color.
- * :param b:  Blue component of the color.
+ * :param int led:  Which LED to set.  0-10 are the LEDs on the top and 11-14
+ *    are the 4 "ambient" LEDs.
+ * :param uint8_t r:  Red component of the color.
+ * :param uint8_t g:  Green component of the color.
+ * :param uint8_t b:  Blue component of the color.
  */
 API(API_LEDS_SET, void epic_leds_set(int led, uint8_t r, uint8_t g, uint8_t b));
 
+/**
+ * Set one of card10's RGB LEDs to a certain color in HSV format.
+ *
+ * This function is rather slow when setting multiple LEDs, use
+ * :c:func:`leds_set_all_hsv` or :c:func:`leds_prep_hsv` + :c:func:`leds_update`
+ * instead.
+ *
+ * :param int led:  Which LED to set.  0-10 are the LEDs on the top and 11-14 are the 4 "ambient" LEDs.
+ * :param float h:  Hue component of the color. (0 <= h < 360)
+ * :param float s:  Saturation component of the color. (0 <= s <= 1)
+ * :param float v:  Value/Brightness component of the color. (0 <= v <= 0)
+ */
+API(API_LEDS_SET_HSV, void epic_leds_set_hsv(int led, float h, float s, float v));
+
+/**
+ * Set multiple of card10's RGB LEDs to a certain color in RGB format.
+ *
+ * The first ``len`` leds are set, the remaining ones are not modified.
+ *
+ * :param uint8_t[len][r,g,b] pattern:  Array with RGB Values with 0 <= len <=
+ *    15. 0-10 are the LEDs on the top and 11-14 are the 4 "ambient" LEDs.
+ * :param uint8_t len: Length of 1st dimension of ``pattern``, see above.
+ */
+API(API_LEDS_SET_ALL, void epic_leds_set_all(uint8_t *pattern, uint8_t len));
+
+/**
+ * Set multiple of card10's RGB LEDs to a certain color in HSV format.
+ *
+ * The first ``len`` led are set, the remaining ones are not modified.
+ *
+ * :param uint8_t[len][h,s,v] pattern:  Array of format with HSV Values with 0
+ *    <= len <= 15.  0-10 are the LEDs on the top and 11-14 are the 4 "ambient"
+ *    LEDs. (0 <= h < 360, 0 <= s <= 1, 0 <= v <= 1)
+ * :param uint8_t len: Length of 1st dimension of ``pattern``, see above.
+ */
+API(API_LEDS_SET_ALL_HSV, void epic_leds_set_all_hsv(float *pattern, uint8_t len));
+
+/**
+ * Prepare one of card10's RGB LEDs to be set to a certain color in RGB format.
+ *
+ * Use :c:func:`leds_update` to apply changes.
+ *
+ * :param int led:  Which LED to set.  0-10 are the LEDs on the top and 11-14
+ *    are the 4 "ambient" LEDs.
+ * :param uint8_t r:  Red component of the color.
+ * :param uint8_t g:  Green component of the color.
+ * :param uint8_t b:  Blue component of the color.
+ */
+API(API_LEDS_PREP, void epic_leds_prep(int led, uint8_t r, uint8_t g, uint8_t b));
+
+/**
+ * Prepare one of card10's RGB LEDs to be set to a certain color in HSV format.
+ *
+ * Use :c:func:`leds_update` to apply changes.
+ *
+ * :param int led:  Which LED to set.  0-10 are the LEDs on the top and 11-14
+ *    are the 4 "ambient" LEDs.
+ * :param uint8_t h:  Hue component of the color. (float, 0 <= h < 360)
+ * :param uint8_t s:  Saturation component of the color. (float, 0 <= s <= 1)
+ * :param uint8_t v:  Value/Brightness component of the color. (float, 0 <= v <= 0)
+ */
+API(API_LEDS_PREP_HSV, void epic_leds_prep_hsv(int led, float h, float s, float v));
+
+/**
+ * Set global brightness for top RGB LEDs.
+ *
+ * Aside from PWM, the RGB LEDs' overall brightness can be controlled with a
+ * current limiter independently to achieve a higher resolution at low
+ * brightness which can be set with this function.
+ *
+ * :param uint8_t value:  Global brightness of top LEDs. (1 <= value <= 8, default = 1)
+ */
+API(API_LEDS_DIM_BOTTOM, void epic_leds_dim_bottom(uint8_t value));
+
+/**
+ * Set global brightness for bottom RGB LEDs.
+ *
+ * Aside from PWM, the RGB LEDs' overall brightness can be controlled with a
+ * current limiter independently to achieve a higher resolution at low
+ * brightness which can be set with this function.
+ *
+ * :param uint8_t value:  Global brightness of bottom LEDs. (1 <= value <= 8, default = 8)
+ */
+API(API_LEDS_DIM_TOP, void epic_leds_dim_top(uint8_t value));
+
+/**
+ * Enables or disables powersave mode.
+ *
+ * Even when set to zero, the RGB LEDs still individually consume ~1mA.
+ * Powersave intelligently switches the supply power in groups. This introduces
+ * delays in the magnitude of ~10µs, so it can be disabled for high speed
+ * applications such as POV.
+ *
+ * :param bool eco:  Activates powersave if true, disables it when false. (default = True)
+ */
+API(API_LEDS_SET_POWERSAVE, void epic_leds_set_powersave(bool eco));
+
+/**
+ * Updates the RGB LEDs with changes that have been set with :c:func:`leds_prep`
+ * or :c:func:`leds_prep_hsv`.
+ *
+ * The LEDs can be only updated in bulk, so using this approach instead of
+ * :c:func:`leds_set` or :c:func:`leds_set_hsv` significantly reduces the load
+ * on the corresponding hardware bus.
+ */
+API(API_LEDS_UPDATE, void epic_leds_update(void));
+
+/**
+ * Set the brightness of one of the rocket LEDs.
+ *
+ * :param int led:  Which LED to set.
+ *
+ *    +-------+--------+----------+
+ *    |   ID  | Color  | Location |
+ *    +=======+========+==========+
+ *    | ``0`` | Blue   | Left     |
+ *    +-------+--------+----------+
+ *    | ``1`` | Yellow | Top      |
+ *    +-------+--------+----------+
+ *    | ``2`` | Green  | Right    |
+ *    +-------+--------+----------+
+ * :param uint8_t value:  Brightness of LED (only two brightness levels are
+ *    supported right now).
+ */
+API(API_LEDS_SET_ROCKET, void epic_leds_set_rocket(int led, uint8_t value));
+
+/**
+ * Turn on the bright side LED which can serve as a flashlight if worn on the left wrist or as a rad tattoo illuminator if worn on the right wrist.
+ *
+ *:param bool power:  Side LED on if true.
+ */
+API(API_LEDS_SET_FLASHLIGHT, void epic_set_flashlight(bool power));
+
+/**
+ * Set gamma lookup table for individual rgb channels.
+ *
+ * Since the RGB LEDs' subcolor LEDs have different peak brightness and the
+ * linear scaling introduced by PWM is not desireable for color accurate work,
+ * custom lookup tables for each individual color channel can be loaded into the
+ * Epicardium's memory with this function.
+ *
+ * :param uint8_t rgb_channel:  Color whose gamma table is to be updated, 0->Red, 1->Green, 2->Blue.
+ * :param uint8_t[256] gamma_table: Gamma lookup table. (default = 4th order power function rounded up)
+ */
+API(API_LEDS_SET_GAMMA_TABLE, void epic_leds_set_gamma_table(
+	uint8_t rgb_channel,
+	uint8_t *gamma_table
+));
+
 /**
  * Sensor Data Streams
  * ===================
diff --git a/epicardium/modules/leds.c b/epicardium/modules/leds.c
index c4a3f977..0fa35c16 100644
--- a/epicardium/modules/leds.c
+++ b/epicardium/modules/leds.c
@@ -1,7 +1,88 @@
 #include "leds.h"
+#include "pmic.h"
+//#include "FreeRTOS.h"
+//#include "task.h"
+
+//TODO: create smth like vTaskDelay(pdMS_TO_TICKS(//put ms here)) for us, remove blocking delay from /lib/leds.c to avoid process blocking
 
 void epic_leds_set(int led, uint8_t r, uint8_t g, uint8_t b)
 {
-	leds_set(led, r, g, b);
+	leds_prep(led, r, g, b);
+	leds_update_power();
+	leds_update();
+}
+
+void epic_leds_set_hsv(int led, float h, float s, float v)
+{
+	leds_prep_hsv(led, h, s, v);
+	leds_update_power();
+	leds_update();
+}
+
+void epic_leds_prep(int led, uint8_t r, uint8_t g, uint8_t b)
+{
+	leds_prep(led, r, g, b);
+}
+
+void epic_leds_prep_hsv(int led, float h, float s, float v)
+{
+	leds_prep_hsv(led, h, s, v);
+}
+
+void epic_leds_set_all(uint8_t *pattern_ptr, uint8_t len)
+{
+	uint8_t(*pattern)[3] = (uint8_t(*)[3])pattern_ptr;
+	for (int i = 0; i < len; i++) {
+		leds_prep(i, pattern[i][0], pattern[i][1], pattern[i][2]);
+	}
+	leds_update_power();
+	leds_update();
+}
+
+void epic_leds_set_all_hsv(float *pattern_ptr, uint8_t len)
+{
+	float(*pattern)[3] = (float(*)[3])pattern_ptr;
+	for (int i = 0; i < len; i++) {
+		leds_prep_hsv(i, pattern[i][0], pattern[i][1], pattern[i][2]);
+	}
+	leds_update_power();
 	leds_update();
-}
\ No newline at end of file
+}
+
+void epic_leds_dim_top(uint8_t value)
+{
+	leds_set_dim_top(value);
+	leds_update();
+}
+
+void epic_leds_dim_bottom(uint8_t value)
+{
+	leds_set_dim_bottom(value);
+	leds_update();
+}
+
+void epic_leds_set_rocket(int led, uint8_t value)
+{
+	pmic_set_led(led, value);
+}
+
+void epic_set_flashlight(bool power)
+{
+	leds_flashlight(power);
+}
+
+void epic_leds_update(void)
+{
+	leds_update_power();
+	leds_update();
+}
+
+void epic_leds_set_powersave(bool eco)
+{
+	leds_powersave(eco);
+}
+
+void epic_leds_set_gamma_table(uint8_t rgb_channel, uint8_t gamma_table[256])
+{
+	leds_set_gamma_table(rgb_channel, gamma_table);
+}
diff --git a/hw-tests/dual-core/main.c b/hw-tests/dual-core/main.c
index ca91f9d6..5c279502 100644
--- a/hw-tests/dual-core/main.c
+++ b/hw-tests/dual-core/main.c
@@ -24,31 +24,12 @@ int main(void)
 	Paint_DrawImage(Heart, 0, 0, 160, 80);
 	LCD_Update();
 
-	for (int i = 0; i < 11; i++) {
-		leds_set_dim(i, 1);
-	}
-
-	int h = 0;
-
 	// Release core1
 	core1_start((void *)0x10080000);
+	int h = 0;
 
 	while (1) {
 #define NUM 15
-		for (int i = 0; i < NUM; i++) {
-			if (i < 12) {
-				leds_set_hsv(
-					i,
-					(h + 360 / NUM * i) % 360,
-					1.,
-					1. / 8
-				);
-			} else {
-				leds_set_hsv(
-					i, (h + 360 / NUM * i) % 360, 1., 1.
-				);
-			}
-		}
 
 		leds_update();
 		TMR_Delay(MXC_TMR0, MSEC(10), 0);
diff --git a/hw-tests/hello-world/main.c b/hw-tests/hello-world/main.c
index d5787c09..b797233b 100644
--- a/hw-tests/hello-world/main.c
+++ b/hw-tests/hello-world/main.c
@@ -33,7 +33,7 @@ int main(void)
 	LCD_Update();
 
 	for (int i = 0; i < 11; i++) {
-		leds_set_dim(i, 1);
+		//        leds_set_dim(i, 1);
 	}
 
 	int __attribute__((unused)) h = 0;
diff --git a/lib/card10/leds.c b/lib/card10/leds.c
index c847709a..3f284db4 100644
--- a/lib/card10/leds.c
+++ b/lib/card10/leds.c
@@ -1,10 +1,14 @@
 #include "gpio.h"
 #include "portexpander.h"
-
+#include "max32665.h"
 #include <stdint.h>
 #include <string.h>
-
+#include <stdbool.h>
+#include <stdio.h>
 #define NUM_LEDS 15
+#define DEFAULT_DIM_TOP 1
+#define DEFAULT_DIM_BOTTOM 8
+#define MAX_DIM 8
 
 static const gpio_cfg_t rgb_dat_pin = {
 	PORT_1, PIN_14, GPIO_FUNC_OUT, GPIO_PAD_NONE
@@ -12,7 +16,13 @@ static const gpio_cfg_t rgb_dat_pin = {
 static const gpio_cfg_t rgb_clk_pin = {
 	PORT_1, PIN_15, GPIO_FUNC_OUT, GPIO_PAD_NONE
 };
-static uint8_t leds[NUM_LEDS][4];
+static uint8_t leds[NUM_LEDS][3];
+static uint8_t gamma_table[3][256];
+static uint8_t active_groups;
+static uint8_t bottom_dim; //index 11-14
+static uint8_t top_dim;    //index 0-10
+static bool powersave;
+static long powerup_wait_cycles = 500;
 
 /***** Functions *****/
 // *****************************************************************************
@@ -174,19 +184,45 @@ static void leds_stop(void)
 	shift(0xFF);
 }
 
-void leds_set_dim(uint8_t led, uint8_t dim)
+static uint8_t led_to_dim_value(uint8_t led)
+{
+	return (led < 11) ? top_dim : bottom_dim;
+}
+
+void leds_set_dim_top(uint8_t value)
+{
+	top_dim = (value > MAX_DIM) ? MAX_DIM : value;
+}
+
+void leds_set_dim_bottom(uint8_t value)
 {
-	leds[led][3] = dim;
+	bottom_dim = (value > MAX_DIM) ? MAX_DIM : value;
 }
 
-void leds_set(uint8_t led, uint8_t r, uint8_t g, uint8_t b)
+void leds_prep(uint8_t led, uint8_t r, uint8_t g, uint8_t b)
 {
 	leds[led][0] = r;
 	leds[led][1] = g;
 	leds[led][2] = b;
 }
 
-void leds_set_hsv(uint8_t led, float h, float s, float v)
+#if 0
+//don't use, is buggy
+void leds_set_autodim(uint8_t led, uint8_t r, uint8_t g, uint8_t b)
+{
+    if(led==NUM_LEDS){
+        leds_set(led,r,g,b);
+        return;
+    }
+    leds[led][3] = max(r,max(g,b));
+    float gain = (float)255/leds[led][3]; //might cause rounding->overflow errors might debug later idk~
+    leds[led][0] = (uint8_t)(r*gain);
+    leds[led][1] = (uint8_t)(g*gain);
+    leds[led][2] = (uint8_t)(b*gain);
+}
+#endif
+
+void leds_prep_hsv(uint8_t led, float h, float s, float v)
 {
 	hsv in       = { h, s, v };
 	rgb out      = hsv2rgb(in);
@@ -195,15 +231,121 @@ void leds_set_hsv(uint8_t led, float h, float s, float v)
 	leds[led][2] = out.b * 255;
 }
 
+static bool is_led_on(uint8_t led) // scheduled to be on after next update
+{
+	if (!led_to_dim_value(led)) {
+		return false;
+	}
+	for (int i = 0; i < 3; i++) {
+		if (leds[led][i] != 0) {
+			return true;
+		}
+	}
+	return false;
+}
+
+static uint8_t led_to_group(uint8_t led)
+{
+	if (led == 14) {
+		return 1;
+	} else if (led >= 11) {
+		return 2;
+	}
+	return 3;
+}
+
+static uint8_t
+check_privilege(void) //returns number of hierarchical groups with power
+{
+	for (int i = 0; i < NUM_LEDS; i++) {
+		if (is_led_on(i)) {
+			return led_to_group(i);
+		}
+	}
+	return 0;
+}
+
+static uint8_t power_pin_conversion(uint8_t group)
+{
+	if (group == 2) {
+		return 1;
+	}
+	if (group == 1) {
+		return 2;
+	}
+	return 0;
+}
+
+static void power_all(void)
+{
+	for (int i = 0; i < 3; i++) {
+		portexpander_prep(i, 0);
+	}
+	portexpander_update();
+}
+
+void leds_update_power(void)
+{
+	if (!powersave) {
+		return;
+	}
+	uint8_t new_groups =
+		check_privilege(); //there must be a prettier way to do this but meh
+	if (new_groups == active_groups) {
+		return;
+	}
+	for (int i = 0; i < 3; i++) {
+		if (i < new_groups) {
+			portexpander_prep(power_pin_conversion(i), 0);
+		} else {
+			portexpander_prep(power_pin_conversion(i), 1);
+		}
+	}
+	portexpander_update();
+	if (active_groups < new_groups) {
+		for (int i = 0; i < powerup_wait_cycles; i++) {
+			__NOP();
+		}
+	}
+	active_groups = new_groups;
+}
+
+void leds_powersave(bool eco)
+{
+	powersave = eco;
+	if (!powersave) {
+		power_all();
+	} else {
+		leds_update_power();
+	}
+}
+
 void leds_update(void)
 {
 	leds_start();
 	for (int i = NUM_LEDS - 1; i >= 0; i--) {
-		leds_shift(leds[i][0], leds[i][1], leds[i][2], leds[i][3]);
+		leds_shift(
+			gamma_table[0][leds[i][0]],
+			gamma_table[1][leds[i][1]],
+			gamma_table[2][leds[i][2]],
+			led_to_dim_value(i)
+		);
 	}
 	leds_stop();
 }
 
+void leds_flashlight(bool power)
+{
+	portexpander_set(7, (power) ? 0 : 1);
+}
+
+void leds_set_gamma_table(uint8_t rgb_channel, uint8_t table[256])
+{
+	for (int i = 0; i < 256; i++) {
+		gamma_table[rgb_channel][i] = table[i];
+	}
+}
+
 void leds_init(void)
 {
 	GPIO_Config(&rgb_clk_pin);
@@ -214,17 +356,19 @@ void leds_init(void)
 
 	memset(leds, 0, sizeof(leds));
 
+	powersave  = TRUE;
+	top_dim    = DEFAULT_DIM_TOP;
+	bottom_dim = DEFAULT_DIM_BOTTOM;
 	for (int i = 0; i < NUM_LEDS; i++) {
-		leds[i][3] = 8;
+		for (int j = 0; j < 3; j++) {
+			leds[i][j] = 0;
+		}
 	}
-
-	if (portexpander_detected()) {
-		// Turn on LEDs
-		// TODO: only turn on LEDs if value != 0,0,0 && dim > 0
-		portexpander_set(0, 0);
-		portexpander_set(1, 0);
-		portexpander_set(2, 0);
+	for (int i = 0; i < 256; i++) {
+		for (int j = 0; j < 3; j++) {
+			int k             = (i * (1 + i) + 255) >> 8;
+			gamma_table[j][i] = (k * (k + 1) + 255) >> 8;
+		}
 	}
-
 	leds_update();
 }
diff --git a/lib/card10/leds.h b/lib/card10/leds.h
index 7b4dc91a..8977daa7 100644
--- a/lib/card10/leds.h
+++ b/lib/card10/leds.h
@@ -1,11 +1,17 @@
 #ifndef LED_H
 
 #include <stdint.h>
+#include <stdbool.h>
 
-void leds_set_dim(uint8_t led, uint8_t dim);
-void leds_set(uint8_t led, uint8_t r, uint8_t g, uint8_t b);
-void leds_set_hsv(uint8_t led, float h, float s, float v);
+void leds_set_dim_top(uint8_t value);
+void leds_set_dim_bottom(uint8_t value);
+void leds_prep(uint8_t led, uint8_t r, uint8_t g, uint8_t b);
+void leds_prep_hsv(uint8_t led, float h, float s, float v);
+void leds_update_power(void);
 void leds_update(void);
 void leds_init(void);
+void leds_powersave(bool eco);
+void leds_flashlight(bool power);
+void leds_set_gamma_table(uint8_t rgb_channel, uint8_t table[256]);
 
 #endif
diff --git a/lib/card10/portexpander.c b/lib/card10/portexpander.c
index 6ae8cd93..c6a8c302 100644
--- a/lib/card10/portexpander.c
+++ b/lib/card10/portexpander.c
@@ -106,6 +106,24 @@ void portexpander_set(uint8_t pin, uint8_t value)
 	}
 }
 
+void portexpander_prep(uint8_t pin, uint8_t value)
+{
+	if (pin < 8) {
+		if (value) {
+			output_state |= (1 << pin);
+		} else {
+			output_state &= ~(1 << pin);
+		}
+	}
+}
+
+void portexpander_update(void)
+{
+	if (detected) {
+		portexpander_write(PE_C_OUTPUT_PORT, output_state);
+	}
+}
+
 void portexpander_set_mask(uint8_t mask, uint8_t values)
 {
 	if (detected) {
diff --git a/lib/card10/portexpander.h b/lib/card10/portexpander.h
index 24e4e5ff..86614bdd 100644
--- a/lib/card10/portexpander.h
+++ b/lib/card10/portexpander.h
@@ -8,6 +8,8 @@ void portexpander_init(void);
 uint8_t portexpander_get(void);
 void portexpander_set(uint8_t pin, uint8_t value);
 void portexpander_set_mask(uint8_t mask, uint8_t values);
+void portexpander_prep(uint8_t pin, uint8_t value);
+void portexpander_update(void);
 bool portexpander_detected(void);
 
 #endif
diff --git a/pycardium/meson.build b/pycardium/meson.build
index 810a6845..0f460274 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -4,7 +4,7 @@ modsrc = files(
   'modules/fat_file.c',
   'modules/fat_reader_import.c',
   'modules/interrupt.c',
-  'modules/leds.c',
+  'modules/sys_leds.c',
   'modules/light_sensor.c',
   'modules/sys_display.c',
   'modules/utime.c',
diff --git a/pycardium/modules/leds.c b/pycardium/modules/leds.c
deleted file mode 100644
index 7428069c..00000000
--- a/pycardium/modules/leds.c
+++ /dev/null
@@ -1,48 +0,0 @@
-#include "epicardium.h"
-
-#include "py/obj.h"
-#include "py/objlist.h"
-#include "py/runtime.h"
-
-static mp_obj_t mp_leds_set(mp_obj_t led_in, mp_obj_t color_in)
-{
-	int led = mp_obj_get_int(led_in);
-
-	if (mp_obj_get_int(mp_obj_len(color_in)) < 3) {
-		mp_raise_ValueError("color must have 3 elements");
-	}
-
-	uint8_t red = mp_obj_get_int(
-		mp_obj_subscr(color_in, mp_obj_new_int(0), MP_OBJ_SENTINEL)
-	);
-	uint8_t green = mp_obj_get_int(
-		mp_obj_subscr(color_in, mp_obj_new_int(1), MP_OBJ_SENTINEL)
-	);
-	uint8_t blue = mp_obj_get_int(
-		mp_obj_subscr(color_in, mp_obj_new_int(2), MP_OBJ_SENTINEL)
-	);
-
-	epic_leds_set(led, red, green, blue);
-
-	return mp_const_none;
-}
-static MP_DEFINE_CONST_FUN_OBJ_2(leds_set_obj, mp_leds_set);
-
-static const mp_rom_map_elem_t leds_module_globals_table[] = {
-	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_leds) },
-	{ MP_ROM_QSTR(MP_QSTR_set), MP_ROM_PTR(&leds_set_obj) },
-	{ MP_ROM_QSTR(MP_QSTR_BOTTOM_RIGHT), MP_OBJ_NEW_SMALL_INT(11) },
-	{ MP_ROM_QSTR(MP_QSTR_BOTTOM_LEFT), MP_OBJ_NEW_SMALL_INT(12) },
-	{ MP_ROM_QSTR(MP_QSTR_TOP_RIGHT), MP_OBJ_NEW_SMALL_INT(13) },
-	{ MP_ROM_QSTR(MP_QSTR_TOP_LEFT), MP_OBJ_NEW_SMALL_INT(14) },
-};
-static MP_DEFINE_CONST_DICT(leds_module_globals, leds_module_globals_table);
-
-const mp_obj_module_t leds_module = {
-	.base    = { &mp_type_module },
-	.globals = (mp_obj_dict_t *)&leds_module_globals,
-};
-
-/* Register the module to make it available in Python */
-/* clang-format off */
-MP_REGISTER_MODULE(MP_QSTR_leds, leds_module, MODULE_LEDS_ENABLED);
diff --git a/pycardium/modules/py/ledfx.py b/pycardium/modules/py/ledfx.py
new file mode 100644
index 00000000..2181f19c
--- /dev/null
+++ b/pycardium/modules/py/ledfx.py
@@ -0,0 +1,48 @@
+import leds, utime, math
+
+
+def col_cor(colors, brightness=1, gamma=1):
+    return [
+        [int(255 * brightness * math.pow((y / 255.0), gamma)) for y in x]
+        for x in colors
+    ]
+
+
+def halo(colors):
+    used_leds = len(colors)
+    colors += [[0, 0, 0]] * (11 - used_leds)
+    colors += [colors[used_leds - 1]] + [colors[0]] * 2 + [colors[used_leds - 1]]
+    return colors
+
+
+def kitt(
+    cycles=100,
+    delay=80,
+    power=10,
+    minimum=0.3,
+    rgb=[255, 0, 0],
+    spectrum=[],
+    halo=False,
+):
+
+    kitt_table = [((-math.cos(math.pi * (x / 10.0))) + 1) / 2.0 for x in range(21)]
+    kitt_table = [math.pow(x, power) * (1 - minimum) + minimum for x in kitt_table]
+
+    for i in range(cycles):
+        j = i % 20
+        if j > 10:
+            j = 20 - j
+        if spectrum == []:
+            used_leds = 11
+            output = [[int(x * y) for y in rgb] for x in kitt_table[j : (j + 11)]]
+        else:
+            used_leds = len(spectrum)
+            output = [
+                [int(y * kitt_table[j + x]) for y in spectrum[x]]
+                for x in range(used_leds)
+            ]
+        if halo:
+            halo(output)
+        leds.set_all(output)
+        utime.sleep_ms(delay)
+    leds.clear()
diff --git a/pycardium/modules/py/leds.py b/pycardium/modules/py/leds.py
new file mode 100644
index 00000000..05038e84
--- /dev/null
+++ b/pycardium/modules/py/leds.py
@@ -0,0 +1,225 @@
+import sys_leds
+import math
+
+
+def update():
+    """
+    Updates the RGB LEDs.
+
+    This will apply changes that have been set with :func:`leds.prep` or
+    :func:`leds.prep_hsv`.  The LEDs can be only updated in bulk, so using this
+    approach instead of :func:`leds.set` or :func:`leds.set_hsv` significantly
+    reduces the load on the corresponding hardware bus.
+    """
+    sys_leds.update()
+
+
+def clear():
+    """
+    Turns all LEDs off.
+
+    Does **not** reactivate powersave if it has been deactivated, in which case
+    ~15mA will be wasted.
+    """
+    values = [[0, 0, 0] for x in range(15)]
+    sys_leds.set_all(values)
+
+
+def flashlight(on):
+    """
+    Turn on the bright side LED.
+
+    This LED can serve as a flashlight if worn on the left wrist or as a rad
+    tattoo illuminator if worn on the right wrist.
+
+    :param bool on:  Side LED on if true.
+    """
+    sys_leds.set_flashlight(on)
+
+
+def dim_top(value):
+    """
+    Set global brightness for top RGB LEDs.
+
+    :param int value: Brightness. Default = 1, range = 1...8
+    """
+    sys_leds.dim_top(value)
+
+
+def dim_bottom(value):
+    """
+    Set global brightness for bottom RGB LEDs.
+
+    :param int value: Brightness. Default = 8, range = 1...8
+    """
+    sys_leds.dim_bottom(value)
+
+
+def rocket(led, value):
+    """
+    Set brightness of one of the rocket LEDs.
+
+    :param int led: Choose your rocket!
+
+       +-------+--------+----------+
+       |   ID  | Color  | Location |
+       +=======+========+==========+
+       | ``0`` | Blue   | Left     |
+       +-------+--------+----------+
+       | ``1`` | Yellow | Top      |
+       +-------+--------+----------+
+       | ``2`` | Green  | Right    |
+       +-------+--------+----------+
+    :param int value:  Brightness of LED (only two brightness levels are
+       supported right now).
+    """
+    sys_leds.set_rocket(led, value)
+
+
+def prep(led, color):
+    """
+    Prepare am RGB LED to be set to an RGB value.
+
+    Changes are applied upon calling :func:`leds.update`. This is faster than
+    individual :func:`leds.set` or :func:`leds.set_hsv` calls in case of
+    multiple changes.
+
+    :param int led: Which LED to prepare. 0-10 are the LEDs on the top and
+       11-14 are the 4 "ambient" LEDs
+    :param [r,g,b] color: RGB triplet
+    """
+    sys_leds.prep(led, color)
+
+
+def prep_hsv(led, color):
+    """
+    Prepare an RGB LED to be set to an HSV value.
+
+    Changes are applied upon calling :func:`leds.update`. This is faster than
+    individual :func:`leds.set` or :func:`leds.set_hsv` calls in case of
+    multiple changes.
+
+    :param int led: Which LED to prepare. 0-10 are the LEDs on the top and
+       11-14 are the 4 "ambient" LEDs
+    :param [h,s,v] color: HSV triplet
+    """
+    sys_leds.prep_hsv(led, color)
+
+
+def set(led, color):
+    """
+    Set an RGB LED to an RGB value.
+
+    :param int led: Which LED to set. 0-10 are the LEDs on the top and 11-14
+       are the 4 "ambient" LEDs
+    :param [r,g,b] color: RGB triplet
+    """
+    sys_leds.set(led, color)
+
+
+def set_hsv(led, color):
+    """
+    Prepare an RGB LED to be set to an HSV value.
+
+    :param int led: Which LED to set. 0-10 are the LEDs on the top and 11-14
+       are the 4 "ambient" LEDs
+    :param [h,s,v] color: HSV triplet
+    """
+    sys_leds.set_hsv(led, color)
+
+
+def set_all(colors):
+    """
+    Set multiple RGB LEDs to RGB values.
+
+    Filling starts at LED0 ascending.
+
+    **Example**:
+
+    .. code-block:: python
+
+       import leds, color
+
+       # 1st red, 2nd green & 3rd blue:
+       leds.set_all([color.RED, color.GREEN, color.BLUE])
+
+    :param colors: List of RGB triplets
+    """
+    sys_leds.set_all(colors)
+
+
+def set_all_hsv(colors):
+    """
+    Set multiple RGB LEDs to HSV values.
+
+    Filling starts at LED0 ascending.
+
+    :param colors: List of HSV triplets
+    """
+    sys_leds.set_all_hsv(colors)
+
+
+def gay(value=0.5):
+    """
+    Gamma Adjust Yassistant. Prints a rainbow.
+
+    Recommended calibration prodecure:
+
+    .. code-block:: python
+
+       import leds
+
+       leds.gay(1)
+       # adjust gain for uniform brightness
+       leds.gamma_rgb(channel=..., gain=...)
+
+       leds.gay(0.5)
+       # adjust power~4 for uniform brightness
+       leds.gamma_rgb(channel=..., power=...)
+
+    :param value: Brightness. Default = 0.5
+    """
+    values = [[((x * 360.0) / 11), 1.0, value] for x in range(11)]
+    sys_leds.set_all_hsv(values)
+
+
+def powersave(eco=True):
+    """
+    Enable or disable powersave mode.
+
+    Even when set to zero, the RGB LEDs still individually consume ~1mA.
+    Powersave intelligently switches the supply power in groups. This
+    introduces delays in the magnitude of ~10us, so it can be disabled for high
+    speed applications such as POV.
+
+    :param bool eco:  Activates powersave if ``True``, disables it when ``False``.
+    """
+    sys_leds.set_powersave(eco)
+
+
+def gamma(power=4.0):
+    """
+    Applies same power function gamma correction to all RGB channels.
+
+    :param float power: Exponent of power function.
+    """
+    table = [int(math.ceil(math.pow((x / 255.0), power) * 255)) for x in range(256)]
+    for i in range(3):
+        sys_leds.set_gamma(i, table)
+    sys_leds.update()
+
+
+def gamma_rgb(channel, power=4.0, gain=1.0):
+    """
+    Applies power function gamma correction with optional amplification to a single RGB channel.
+
+    :param int channel: RGB channel to be adjusted. 0->Red, 1->Green, 2->Blue.
+    :param float power: Exponent of power function.
+    :param float gain: Amplification of channel. Values above 1.0 might cause
+       overflow.
+    """
+    table = [
+        int(math.ceil(math.pow((x / 255.0), power) * gain * 255)) for x in range(256)
+    ]
+    sys_leds.set_gamma(channel, table)
+    sys_leds.update()
diff --git a/pycardium/modules/py/meson.build b/pycardium/modules/py/meson.build
index eab92f95..7b1b9768 100644
--- a/pycardium/modules/py/meson.build
+++ b/pycardium/modules/py/meson.build
@@ -2,6 +2,9 @@ python_modules = files(
   'color.py',
   'htmlcolor.py',
   'display.py',
+  'leds.py',
+  'pride.py',
+  'ledfx.py',
 )
 
 frozen_modules = mpy_cross.process(python_modules)
diff --git a/pycardium/modules/py/pride.py b/pycardium/modules/py/pride.py
new file mode 100644
index 00000000..d45d675d
--- /dev/null
+++ b/pycardium/modules/py/pride.py
@@ -0,0 +1,130 @@
+flags = {}
+flags["rainbow"] = [
+    [255, 0, 24],
+    [255, 165, 44],
+    [255, 255, 65],
+    [0, 128, 24],
+    [0, 0, 249],
+    [134, 0, 125],
+]
+flags["trans"] = [
+    [85, 205, 252],
+    [247, 168, 184],
+    [255, 255, 255],
+    [247, 168, 184],
+    [85, 205, 252],
+]
+flags["bi"] = [[214, 2, 112], [155, 79, 150], [0, 56, 168]]
+flags["ace"] = [[1, 1, 1], [164, 164, 164], [255, 255, 255], [150, 0, 150]]
+flags["greyace"] = [
+    [150, 0, 150],
+    [164, 164, 164],
+    [255, 255, 255],
+    [164, 164, 164],
+    [150, 0, 150],
+]
+flags["aro"] = [
+    [61, 165, 66],
+    [167, 211, 121],
+    [255, 255, 255],
+    [169, 169, 169],
+    [1, 1, 1],
+]
+flags["greyaro"] = [
+    [61, 165, 66],
+    [164, 164, 164],
+    [255, 255, 255],
+    [164, 164, 164],
+    [61, 165, 66],
+]
+flags["pan"] = [[255, 27, 141], [255, 218, 0], [27, 179, 255]]
+flags["inter"] = [[255, 218, 0], [122, 0, 172]]
+flags["genderqueer"] = [[201, 138, 255], [255, 255, 255], [80, 150, 85]]
+flags["lesbian"] = [
+    [139, 60, 105],
+    [171, 99, 143],
+    [187, 127, 179],
+    [255, 255, 255],
+    [228, 172, 207],
+    [214, 113, 113],
+    [134, 70, 70],
+]
+flags["nonbinary"] = [[255, 244, 51], [255, 255, 255], [155, 89, 208], [0, 0, 0]]
+flags["genderfluid"] = [
+    [254, 117, 161],
+    [255, 255, 255],
+    [189, 22, 213],
+    [0, 0, 0],
+    [50, 61, 187],
+]
+flags["agender"] = [
+    [0, 0, 0],
+    [150, 150, 150],
+    [255, 255, 255],
+    [182, 245, 131],
+    [255, 255, 255],
+    [150, 150, 150],
+    [0, 0, 0],
+]
+flags["poly"] = [
+    [0, 0, 255],
+    [0, 0, 255],
+    [0, 0, 255],
+    [255, 0, 0],
+    [255, 0, 0],
+    [255, 255, 0],
+    [255, 0, 0],
+    [255, 0, 0],
+    [0, 0, 0],
+    [0, 0, 0],
+    [0, 0, 0],
+]
+import leds, display, math, utime, ledfx
+
+
+def expand(colors, cutoff=12):
+    output = []
+    if len(colors) != 7 or cutoff > 14:
+        leds_per_color = int(cutoff / len(colors))
+        for i in colors:
+            output += [i] * leds_per_color
+    else:
+        for j, i in enumerate(colors):
+            output += [i] * (1 + (j % 2 == 1))
+    return output
+
+
+def show_leds(flag="rainbow", brightness=0.5, gamma=1, cutoff=12, halo=True):
+    colors = ledfx.col_cor(flags[flag], brightness, gamma)
+    output = expand(colors, cutoff)[::-1][0:11]
+    if halo:
+        output = ledfx.halo(output)
+    leds.clear()
+    leds.set_all(output)
+
+
+def show_display(flag="rainbow", brightness=1, gamma=1):
+    colors = ledfx.col_cor(flags[flag], brightness, gamma)
+    colors = expand(colors, 160)
+    with display.open() as disp:
+        disp.clear()
+        for line, color in enumerate(colors):
+            disp.line(line, 0, line, 80, col=color)
+        disp.update()
+        disp.close()
+
+
+def get(flag="rainbow"):
+    return flags[flag]
+
+
+def demo(s_delay=2):
+    for i in flags:
+        print(i)
+        show_leds(flag=i)
+        show_display(flag=i)
+        utime.sleep_ms(s_delay * 1000)
+    leds.clear()
+    with display.open() as disp:
+        disp.clear().update()
+        disp.close()
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index dedc57c0..1c399060 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -5,7 +5,20 @@
 #endif
 
 /* leds */
-Q(leds)
+Q(sys_leds)
+Q(update)
+Q(set)
+Q(set_hsv)
+Q(prep)
+Q(prep_hsv)
+Q(set_all)
+Q(set_all_hsv)
+Q(set_flashlight)
+Q(set_rocket)
+Q(set_powersave)
+Q(set_gamma)
+Q(dim_top)
+Q(dim_bottom)
 Q(BOTTOM_LEFT)
 Q(BOTTOM_RIGHT)
 Q(TOP_LEFT)
diff --git a/pycardium/modules/sys_leds.c b/pycardium/modules/sys_leds.c
new file mode 100644
index 00000000..55c99ba3
--- /dev/null
+++ b/pycardium/modules/sys_leds.c
@@ -0,0 +1,250 @@
+#include "py/obj.h"
+#include "py/objlist.h"
+#include "py/runtime.h"
+#include <stdio.h>
+
+#include "epicardium.h"
+
+static mp_obj_t mp_leds_set(mp_obj_t led_in, mp_obj_t color_in)
+{
+	int led = mp_obj_get_int(led_in);
+
+	if (mp_obj_get_int(mp_obj_len(color_in)) < 3) {
+		mp_raise_ValueError("color must have 3 elements");
+	}
+
+	uint8_t red = mp_obj_get_int(
+		mp_obj_subscr(color_in, mp_obj_new_int(0), MP_OBJ_SENTINEL)
+	);
+	uint8_t green = mp_obj_get_int(
+		mp_obj_subscr(color_in, mp_obj_new_int(1), MP_OBJ_SENTINEL)
+	);
+	uint8_t blue = mp_obj_get_int(
+		mp_obj_subscr(color_in, mp_obj_new_int(2), MP_OBJ_SENTINEL)
+	);
+
+	epic_leds_set(led, red, green, blue);
+
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(leds_set_obj, mp_leds_set);
+
+static mp_obj_t mp_leds_set_hsv(mp_obj_t led_in, mp_obj_t color_in)
+{
+	int led = mp_obj_get_int(led_in);
+
+	if (mp_obj_get_int(mp_obj_len(color_in)) < 3) {
+		mp_raise_ValueError("color must have 3 elements");
+	}
+
+	float h = mp_obj_get_float(
+		mp_obj_subscr(color_in, mp_obj_new_int(0), MP_OBJ_SENTINEL)
+	);
+	float s = mp_obj_get_float(
+		mp_obj_subscr(color_in, mp_obj_new_int(1), MP_OBJ_SENTINEL)
+	);
+	float v = mp_obj_get_float(
+		mp_obj_subscr(color_in, mp_obj_new_int(2), MP_OBJ_SENTINEL)
+	);
+
+	epic_leds_set_hsv(led, h, s, v);
+
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(leds_set_hsv_obj, mp_leds_set_hsv);
+
+static mp_obj_t mp_leds_prep(mp_obj_t led_in, mp_obj_t color_in)
+{
+	int led = mp_obj_get_int(led_in);
+
+	if (mp_obj_get_int(mp_obj_len(color_in)) < 3) {
+		mp_raise_ValueError("color must have 3 elements");
+	}
+
+	uint8_t red = mp_obj_get_int(
+		mp_obj_subscr(color_in, mp_obj_new_int(0), MP_OBJ_SENTINEL)
+	);
+	uint8_t green = mp_obj_get_int(
+		mp_obj_subscr(color_in, mp_obj_new_int(1), MP_OBJ_SENTINEL)
+	);
+	uint8_t blue = mp_obj_get_int(
+		mp_obj_subscr(color_in, mp_obj_new_int(2), MP_OBJ_SENTINEL)
+	);
+
+	epic_leds_prep(led, red, green, blue);
+
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(leds_prep_obj, mp_leds_prep);
+
+static mp_obj_t mp_leds_prep_hsv(mp_obj_t led_in, mp_obj_t color_in)
+{
+	int led = mp_obj_get_int(led_in);
+
+	if (mp_obj_get_int(mp_obj_len(color_in)) < 3) {
+		mp_raise_ValueError("color must have 3 elements");
+	}
+
+	float h = mp_obj_get_float(
+		mp_obj_subscr(color_in, mp_obj_new_int(0), MP_OBJ_SENTINEL)
+	);
+	float s = mp_obj_get_float(
+		mp_obj_subscr(color_in, mp_obj_new_int(1), MP_OBJ_SENTINEL)
+	);
+	float v = mp_obj_get_float(
+		mp_obj_subscr(color_in, mp_obj_new_int(2), MP_OBJ_SENTINEL)
+	);
+
+	epic_leds_prep_hsv(led, h, s, v);
+
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(leds_prep_hsv_obj, mp_leds_prep_hsv);
+
+static mp_obj_t mp_leds_set_all(mp_obj_t color_in)
+{
+	uint8_t len = mp_obj_get_int(mp_obj_len(color_in));
+	uint8_t pattern[len][3];
+	for (int i = 0; i < len; i++) {
+		mp_obj_t color = mp_obj_subscr(
+			color_in, mp_obj_new_int(i), MP_OBJ_SENTINEL
+		);
+		pattern[i][0] = mp_obj_get_int(mp_obj_subscr(
+			color, mp_obj_new_int(0), MP_OBJ_SENTINEL)
+		);
+		pattern[i][1] = mp_obj_get_int(mp_obj_subscr(
+			color, mp_obj_new_int(1), MP_OBJ_SENTINEL)
+		);
+		pattern[i][2] = mp_obj_get_int(mp_obj_subscr(
+			color, mp_obj_new_int(2), MP_OBJ_SENTINEL)
+		);
+	}
+	epic_leds_set_all((uint8_t *)pattern, len);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(leds_set_all_obj, mp_leds_set_all);
+
+static mp_obj_t mp_leds_set_all_hsv(mp_obj_t color_in)
+{
+	uint8_t len = mp_obj_get_int(mp_obj_len(color_in));
+	float pattern[len][3];
+	for (int i = 0; i < len; i++) {
+		mp_obj_t color = mp_obj_subscr(
+			color_in, mp_obj_new_int(i), MP_OBJ_SENTINEL
+		);
+		pattern[i][0] = mp_obj_get_float(mp_obj_subscr(
+			color, mp_obj_new_int(0), MP_OBJ_SENTINEL)
+		);
+		pattern[i][1] = mp_obj_get_float(mp_obj_subscr(
+			color, mp_obj_new_int(1), MP_OBJ_SENTINEL)
+		);
+		pattern[i][2] = mp_obj_get_float(mp_obj_subscr(
+			color, mp_obj_new_int(2), MP_OBJ_SENTINEL)
+		);
+	}
+	epic_leds_set_all_hsv((float *)pattern, len);
+
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(leds_set_all_hsv_obj, mp_leds_set_all_hsv);
+
+static mp_obj_t mp_leds_set_flashlight(mp_obj_t on_in)
+{
+	int on = mp_obj_get_int(on_in);
+	epic_set_flashlight(on);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(
+	leds_set_flashlight_obj, mp_leds_set_flashlight
+);
+
+static mp_obj_t mp_leds_set_rocket(mp_obj_t led_in, mp_obj_t value_in)
+{
+	int led   = mp_obj_get_int(led_in);
+	int value = mp_obj_get_int(value_in);
+	epic_leds_set_rocket(led, value);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(leds_set_rocket_obj, mp_leds_set_rocket);
+
+static mp_obj_t mp_leds_dim_top(mp_obj_t dim_in)
+{
+	int dim = mp_obj_get_int(dim_in);
+	epic_leds_dim_top(dim);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(leds_dim_top_obj, mp_leds_dim_top);
+
+static mp_obj_t mp_leds_dim_bottom(mp_obj_t dim_in)
+{
+	int dim = mp_obj_get_int(dim_in);
+	epic_leds_dim_bottom(dim);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(leds_dim_bottom_obj, mp_leds_dim_bottom);
+
+static mp_obj_t mp_leds_update()
+{
+	epic_leds_update();
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_0(leds_update_obj, mp_leds_update);
+
+static mp_obj_t mp_leds_set_powersave(mp_obj_t eco_in)
+{
+	int eco = mp_obj_get_int(eco_in);
+	epic_leds_set_powersave(eco);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_1(leds_set_powersave_obj, mp_leds_set_powersave);
+
+static mp_obj_t
+mp_leds_set_gamma(mp_obj_t rgb_channel_in, mp_obj_t gamma_table_in)
+{
+	int rgb_channel = mp_obj_get_int(rgb_channel_in);
+	if (mp_obj_get_int(mp_obj_len(gamma_table_in)) != 256) {
+		mp_raise_ValueError("table must have 256 elements");
+	}
+	uint8_t gamma_table[256];
+	for (int i = 0; i < 256; i++) {
+		gamma_table[i] = mp_obj_get_int(mp_obj_subscr(
+			gamma_table_in, mp_obj_new_int(i), MP_OBJ_SENTINEL)
+		);
+	}
+	epic_leds_set_gamma_table(rgb_channel, gamma_table);
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(leds_set_gamma_obj, mp_leds_set_gamma);
+
+static const mp_rom_map_elem_t leds_module_globals_table[] = {
+	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_leds) },
+	{ MP_ROM_QSTR(MP_QSTR_set), MP_ROM_PTR(&leds_set_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_hsv), MP_ROM_PTR(&leds_set_hsv_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_prep), MP_ROM_PTR(&leds_prep_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_prep_hsv), MP_ROM_PTR(&leds_prep_hsv_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_all), MP_ROM_PTR(&leds_set_all_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_all_hsv), MP_ROM_PTR(&leds_set_all_hsv_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_rocket), MP_ROM_PTR(&leds_set_rocket_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_flashlight),
+	  MP_ROM_PTR(&leds_set_flashlight_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&leds_update_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_powersave),
+	  MP_ROM_PTR(&leds_set_powersave_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_gamma), MP_ROM_PTR(&leds_set_gamma_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_dim_top), MP_ROM_PTR(&leds_dim_top_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_dim_bottom), MP_ROM_PTR(&leds_dim_bottom_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_BOTTOM_RIGHT), MP_OBJ_NEW_SMALL_INT(11) },
+	{ MP_ROM_QSTR(MP_QSTR_BOTTOM_LEFT), MP_OBJ_NEW_SMALL_INT(12) },
+	{ MP_ROM_QSTR(MP_QSTR_TOP_RIGHT), MP_OBJ_NEW_SMALL_INT(13) },
+	{ MP_ROM_QSTR(MP_QSTR_TOP_LEFT), MP_OBJ_NEW_SMALL_INT(14) },
+};
+static MP_DEFINE_CONST_DICT(leds_module_globals, leds_module_globals_table);
+
+const mp_obj_module_t leds_module = {
+	.base    = { &mp_type_module },
+	.globals = (mp_obj_dict_t *)&leds_module_globals,
+};
+
+/* Register the module to make it available in Python */
+/* clang-format off */
+MP_REGISTER_MODULE(MP_QSTR_sys_leds, leds_module, MODULE_LEDS_ENABLED);
-- 
GitLab