diff --git a/components/micropython/usermodule/mp_leds.c b/components/micropython/usermodule/mp_leds.c index f98e8cfb3445e1cd5194b1267261582d3e80243d..ed18607cfefb1c2dd92b0cdc7645d6f9e5110275 100644 --- a/components/micropython/usermodule/mp_leds.c +++ b/components/micropython/usermodule/mp_leds.c @@ -58,6 +58,11 @@ STATIC mp_obj_t mp_leds_get_slew_rate() { STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_leds_get_slew_rate_obj, mp_leds_get_slew_rate); +STATIC mp_obj_t mp_leds_get_steady() { + return mp_obj_new_bool(st3m_leds_get_steady()); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_leds_get_steady_obj, mp_leds_get_steady); + STATIC mp_obj_t mp_led_set_rgb(size_t n_args, const mp_obj_t *args) { uint8_t index = mp_obj_get_int(args[0]); float red = mp_obj_get_float(args[1]); @@ -165,6 +170,7 @@ STATIC const mp_rom_map_elem_t mp_module_leds_globals_table[] = { MP_ROM_PTR(&mp_leds_get_slew_rate_obj) }, { MP_ROM_QSTR(MP_QSTR_set_slew_rate), MP_ROM_PTR(&mp_leds_set_slew_rate_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_steady), MP_ROM_PTR(&mp_leds_get_steady_obj) }, }; STATIC MP_DEFINE_CONST_DICT(mp_module_leds_globals, diff --git a/components/st3m/st3m_leds.c b/components/st3m/st3m_leds.c index 500d508dcba32e066b5e39f949e0da4ea677f08b..aa495b6715aaf0996c58eb430fe2ac7d847aa2fa 100644 --- a/components/st3m/st3m_leds.c +++ b/components/st3m/st3m_leds.c @@ -38,6 +38,7 @@ typedef struct { uint8_t brightness; uint8_t slew_rate; bool auto_update; + bool is_steady; uint8_t timer; st3m_leds_gamma_table_t gamma_red; @@ -67,7 +68,7 @@ static void set_single_led(uint8_t index, st3m_u8_rgb_t c) { static uint16_t led_get_slew(uint16_t old, uint16_t new, uint16_t slew) { new = new << 8; - if (slew == 255) return new; + if (slew == 255 || (old == new)) return new; int16_t bonus = ((int16_t)slew) - 225; slew = 30 + (slew << 2) + ((slew * slew) >> 3); if (bonus > 0) { @@ -105,13 +106,17 @@ void st3m_leds_update_hardware() { if (state.auto_update) leds_update_target(); bool is_different = false; + bool is_steady = true; for (int i = 0; i < 40; i++) { st3m_u8_rgb_t ret = state.target[i]; st3m_u16_rgb_t c; - c.r = led_get_slew(state.slew_output[i].r, ret.r, state.slew_rate); - c.g = led_get_slew(state.slew_output[i].g, ret.g, state.slew_rate); - c.b = led_get_slew(state.slew_output[i].b, ret.b, state.slew_rate); + st3m_u16_rgb_t prev = state.slew_output[i]; + c.r = led_get_slew(prev.r, ret.r, state.slew_rate); + c.g = led_get_slew(prev.g, ret.g, state.slew_rate); + c.b = led_get_slew(prev.b, ret.b, state.slew_rate); + if ((prev.r != c.r) || (prev.g != c.g) || (prev.b != c.b)) + is_steady = false; state.slew_output[i] = c; c.r = ((uint32_t)c.r * state.brightness) >> 8; @@ -129,6 +134,7 @@ void st3m_leds_update_hardware() { is_different = true; } } + state.is_steady = is_steady; UNLOCK; if (is_different || (state.timer > 10)) { @@ -164,6 +170,7 @@ void st3m_leds_set_single_rgb(uint8_t index, float red, float green, state.target_buffer[index].r = (uint8_t)(red * 255); state.target_buffer[index].g = (uint8_t)(green * 255); state.target_buffer[index].b = (uint8_t)(blue * 255); + state.is_steady = false; UNLOCK_INCOMING; } @@ -277,6 +284,13 @@ uint8_t st3m_leds_get_slew_rate() { return res; } +bool st3m_leds_get_steady() { + LOCK; + uint8_t res = state.is_steady; + UNLOCK; + return res; +} + void st3m_leds_set_auto_update(bool on) { LOCK; state.auto_update = on; diff --git a/components/st3m/st3m_leds.h b/components/st3m/st3m_leds.h index ba05cbd0a14cf5ac12a67c55081cc792a0bdc19b..66f762173e1341547d58e069bb41e3f2a98c5c2e 100644 --- a/components/st3m/st3m_leds.h +++ b/components/st3m/st3m_leds.h @@ -44,9 +44,7 @@ uint8_t st3m_leds_get_brightness(); // to 255 to disable. Currently clocks at 50Hz. void st3m_leds_set_slew_rate(uint8_t slew_rate); uint8_t st3m_leds_get_slew_rate(); - -void st3m_leds_set_max_slew_rate(uint8_t slew_rate); -uint8_t st3m_leds_get_max_slew_rate(); +bool st3m_leds_get_steady(); // Update LEDs. Ie., copy the LED state from the first buffer into the second // buffer, effectively scheduling the LED state to be presented to the user. diff --git a/python_payload/apps/appearance/__init__.py b/python_payload/apps/appearance/__init__.py index e9ce5824ef5029f35f0d4c62f90e50b4122546f5..d61e360e5f5411a8b220c7a0104a431ffd151746 100644 --- a/python_payload/apps/appearance/__init__.py +++ b/python_payload/apps/appearance/__init__.py @@ -30,7 +30,6 @@ class App(Application): self.mid_x = 42 self.led_accumulator_ms = 0 self.blueish = False - self.half_time = 600 def draw_widget(self, label): ctx = self.ctx @@ -213,10 +212,6 @@ class App(Application): tmp = 0 elif tmp > 255: tmp = 255 - if tmp < 120: - self.half_time = 600 + (120 - tmp) * (120 - tmp) / 2 - else: - self.half_time = 600 if tmp != settings.num_leds_speed.value: settings.num_leds_speed.set_value(tmp) leds.set_slew_rate(settings.num_leds_speed.value) @@ -237,8 +232,7 @@ class App(Application): def think(self, ins, delta_ms): super().think(ins, delta_ms) self.delta_ms += delta_ms - if self.focused_widget == 2: - self.led_accumulator_ms += delta_ms + if ( self.input.buttons.app.right.pressed or self.input.buttons.app.right.repeated @@ -249,9 +243,13 @@ class App(Application): if self.input.buttons.app.middle.pressed: self.select_pressed = True - while self.led_accumulator_ms > self.half_time: - self.led_accumulator_ms = self.led_accumulator_ms % self.half_time + if self.focused_widget == 3 and leds.get_steady(): + self.led_accumulator_ms += delta_ms + + if self.led_accumulator_ms > 1000: + self.led_accumulator_ms = 0 led_patterns.shift_all_hsv(h=0.8) + leds.update() def on_enter(self, vm): super().on_enter(vm) diff --git a/python_payload/apps/shoegaze/__init__.py b/python_payload/apps/shoegaze/__init__.py index 822383b1c859a46c26c5071c2ddac2acb8306a90..2e90f9330841fba3f258da57198bc3f1dfd44988 100644 --- a/python_payload/apps/shoegaze/__init__.py +++ b/python_payload/apps/shoegaze/__init__.py @@ -3,6 +3,7 @@ from st3m.goose import Dict, Any, List, Optional from st3m.ui.view import View, ViewManager from st3m.input import InputState from ctx import Context +from st3m.ui import colours, led_patterns import json import errno @@ -45,6 +46,8 @@ class ShoegazeApp(Application): self._rand_rot = 0.0 self.delay_on = True self.organ_on = False + self.hue_change = False + self.hue = 0 self._set_chord(3, force_update=True) def _build_synth(self) -> None: @@ -146,11 +149,10 @@ class ShoegazeApp(Application): self._update_connections() def _set_chord(self, i: int, force_update=False) -> None: - hue = int(54 * (i + 0.5)) % 360 if i != self.chord_index or force_update: + self.hue = (54 * (i + 0.5)) * math.tau / 360 self.chord_index = i - leds.set_all_hsv(hue, 1, 0.7) - leds.update() + self.hue_change = True if self.organ_on and self._organ_chords[i] is not None: self.chord = self._organ_chords[i] else: @@ -255,10 +257,22 @@ class ShoegazeApp(Application): self.bass_string.decay = 1000 self.bass_string.signals.trigger.start() + if self.hue_change: + leds.set_slew_rate(min(self.max_slew_rate, 200)) + leds.set_all_rgb(*colours.hsv_to_rgb(self.hue, 1, 0.7)) + leds.update() + self.hue_change = False + elif leds.get_steady(): + leds.set_slew_rate(min(self.max_slew_rate, 50)) + led_patterns.pretty_pattern() + leds.set_all_rgba(*colours.hsv_to_rgb(self.hue, 1, 0.7), 0.75) + leds.update() + def on_enter(self, vm: Optional[ViewManager]) -> None: if self.blm is None: self._build_synth() self.blm.foreground = True + self.max_slew_rate = leds.get_slew_rate() self._set_chord(self.chord_index, force_update=True) def on_exit(self) -> None: diff --git a/python_payload/mypystubs/leds.pyi b/python_payload/mypystubs/leds.pyi index a09c4b634a23fca99bac8041c3d23e5d57aab8b9..9d0acc4a5d44347c904c4d19834a937c715bc572 100644 --- a/python_payload/mypystubs/leds.pyi +++ b/python_payload/mypystubs/leds.pyi @@ -22,6 +22,16 @@ def get_slew_rate() -> int: Get maximum change rate of brightness. See set_slew_rate() """ +def get_steady() -> bool: + """ + Returns true if the LED engine is certain that there is no further color change + until user input occurs, meaning that the last animation has finished and there + is no unprocessed data is queued up. The LED task consumes data at 50Hz. + + If you use this function to time next animation frames be sure to set a maximum + rate so that users with a high slew rate setting will not get strobelighted! + """ + def set_rgb(i: int, r: float, g: float, b: float) -> None: """Set LED i to rgb value r, g, b diff --git a/python_payload/st3m/ui/led_patterns.py b/python_payload/st3m/ui/led_patterns.py index a5aa9ccdb175092793ccdf1e8aa43e060da02435..77dacdd3d2c776d74a2a2f60d31017d93cc5d61d 100644 --- a/python_payload/st3m/ui/led_patterns.py +++ b/python_payload/st3m/ui/led_patterns.py @@ -15,6 +15,19 @@ def _clip(val): return val +def _hue_blend(h1, h2, n): + """ + mixes two hues together while accounting for overflow. + n in range [0..1] blends between [h1..h2] + """ + if abs(h2 - h1) < (math.tau / 2): + return h2 * n + h1 * (1 - n) + elif h2 > h1: + return h2 * n + (h1 + math.tau) * (1 - n) + else: + return (h2 + math.tau) * n + h1 * (1 - n) + + def set_menu_colors(): """ set all LEDs to the configured menu colors if provided in @@ -65,12 +78,7 @@ def pretty_pattern(): 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) + hsv_mixed[0] = _hue_blend(hsv[0], hsv_old[0], k) for h in range(1, 3): hsv_mixed[h] = hsv_old[h] * k + hsv[h] * (1 - k) diff --git a/sim/fakes/leds.py b/sim/fakes/leds.py index 6b268f462005fd535dfd612f164dbf012f6f01e6..bbffc0eee932edeb362427e7844c919d5c5dc630 100644 --- a/sim/fakes/leds.py +++ b/sim/fakes/leds.py @@ -17,6 +17,10 @@ def get_rgb(ix): return 0, 0, 0 +def get_steady(): + return False + + def set_all_rgb(r, g, b): for i in range(40): set_rgb(i, r, g, b)