feat: Licht-Engine mit Transitionen, Moduswechsel und Effekten

This commit is contained in:
Michael Sauer
2026-09-11 13:11:23 +02:00
parent 42eeb0e0eb
commit 73a7f31cb6
5 changed files with 400 additions and 1 deletions
+2 -1
View File
@@ -31,7 +31,8 @@ set(NL_FW_SOURCES
src/sys/fwup_hook_default.c
src/sys/sdk_stubs.c
src/board/nl_board.c
src/light/nl_color.c)
src/light/nl_color.c
src/light/nl_engine.c)
set(NL_FW ninlight_${NINLIGHT_DEVICE})
add_executable(${NL_FW} ${NL_SDK_BOOT_SOURCES} ${NL_FW_SOURCES} ${NL_SDK_OBJECTS})
+178
View File
@@ -0,0 +1,178 @@
#include "light/nl_engine.h"
#include "light/nl_color.h"
typedef struct {
nl_visual_t from;
nl_visual_t to;
uint32_t dur;
uint32_t elapsed;
} seg_t;
static nl_engine_cfg_t s_cfg;
static nl_visual_t s_cur;
static seg_t s_seg[2];
static uint8_t s_nseg;
static uint8_t s_iseg;
static uint8_t s_blink_left; /* remaining blink phases; even = on, odd = off */
static uint32_t s_blink_t;
static bool s_pulse;
static uint32_t s_pulse_t;
static nl_visual_t to_visual(const nl_light_state_t *s)
{
nl_visual_t v;
v.bri = s->on ? (uint16_t)(s->brightness * 100u) : 0;
v.mode = s->mode;
v.r = s->r;
v.g = s->g;
v.b = s->b;
v.ct = s->ct;
return v;
}
static int32_t lerp(int32_t a, int32_t b, uint32_t e, uint32_t d)
{
return a + (int32_t)(((int64_t)(b - a) * (int64_t)e) / (int64_t)d);
}
static void push(const nl_visual_t *from, const nl_visual_t *to, uint32_t dur)
{
seg_t *s = &s_seg[s_nseg++];
s->from = *from;
s->to = *to;
s->dur = dur;
s->elapsed = 0;
}
static void emit(void)
{
uint8_t ch[NL_CH_COUNT];
nl_visual_t v;
if (s_blink_left) {
v.bri = (s_blink_left % 2 == 0) ? NL_BRI_SCALE : 0;
v.mode = NL_MODE_WHITE;
v.r = v.g = v.b = 0;
v.ct = s_cfg.ct_min;
} else if (s_pulse) {
uint32_t half = NL_PULSE_PERIOD_MS / 2;
uint32_t ph = s_pulse_t % NL_PULSE_PERIOD_MS;
uint32_t tri = ph < half ? ph : NL_PULSE_PERIOD_MS - ph;
v.bri = (uint16_t)(1000u + tri * 5000u / half); /* 10 % .. 60 % */
v.mode = NL_MODE_WHITE;
v.r = v.g = v.b = 0;
v.ct = s_cfg.ct_min;
} else {
v = s_cur;
}
nl_visual_to_channels(&v, s_cfg.ct_min, s_cfg.ct_max, ch);
s_cfg.output(ch, s_cfg.ctx);
}
void nl_engine_init(const nl_engine_cfg_t *cfg)
{
s_cfg = *cfg;
s_cur.bri = 0;
s_cur.mode = NL_MODE_WHITE;
s_cur.r = s_cur.g = s_cur.b = 255;
s_cur.ct = cfg->ct_min;
s_nseg = s_iseg = 0;
s_blink_left = 0;
s_blink_t = 0;
s_pulse = false;
s_pulse_t = 0;
emit();
}
void nl_engine_set(const nl_light_state_t *target, uint32_t transition_ms)
{
nl_visual_t tgt = to_visual(target);
s_nseg = 0;
s_iseg = 0;
if (transition_ms == 0) {
s_cur = tgt;
emit();
return;
}
if (s_cur.bri == 0) {
nl_visual_t start = tgt;
start.bri = 0;
push(&start, &tgt, transition_ms);
} else if (tgt.bri == 0) {
nl_visual_t dark = s_cur;
dark.bri = 0;
push(&s_cur, &dark, transition_ms);
push(&tgt, &tgt, 0);
} else if (tgt.mode != s_cur.mode) {
nl_visual_t a = s_cur;
nl_visual_t b = tgt;
a.bri = 0;
b.bri = 0;
push(&s_cur, &a, transition_ms / 2);
push(&b, &tgt, transition_ms - transition_ms / 2);
} else {
push(&s_cur, &tgt, transition_ms);
}
}
void nl_engine_tick(uint32_t elapsed_ms)
{
uint32_t left = elapsed_ms;
while (s_iseg < s_nseg) {
seg_t *s = &s_seg[s_iseg];
if (s->elapsed + left >= s->dur) {
left -= s->dur - s->elapsed;
s->elapsed = s->dur;
s_cur = s->to;
s_iseg++;
continue;
}
s->elapsed += left;
left = 0;
s_cur.bri = (uint16_t)lerp(s->from.bri, s->to.bri, s->elapsed, s->dur);
s_cur.r = (uint8_t)lerp(s->from.r, s->to.r, s->elapsed, s->dur);
s_cur.g = (uint8_t)lerp(s->from.g, s->to.g, s->elapsed, s->dur);
s_cur.b = (uint8_t)lerp(s->from.b, s->to.b, s->elapsed, s->dur);
s_cur.ct = (uint16_t)lerp(s->from.ct, s->to.ct, s->elapsed, s->dur);
s_cur.mode = s->to.mode;
break;
}
if (s_blink_left) {
s_blink_t += elapsed_ms;
while (s_blink_left && s_blink_t >= NL_BLINK_PHASE_MS) {
s_blink_t -= NL_BLINK_PHASE_MS;
s_blink_left--;
}
}
if (s_pulse)
s_pulse_t += elapsed_ms;
emit();
}
bool nl_engine_idle(void)
{
return s_iseg >= s_nseg && !s_blink_left && !s_pulse;
}
void nl_engine_current(nl_visual_t *out)
{
*out = s_cur;
}
void nl_engine_blink(uint8_t count)
{
s_blink_left = (uint8_t)(count * 2u);
s_blink_t = 0;
emit();
}
void nl_engine_pulse(bool enable)
{
s_pulse = enable;
s_pulse_t = 0;
emit();
}
+31
View File
@@ -0,0 +1,31 @@
#ifndef NL_ENGINE_H
#define NL_ENGINE_H
#include <stdbool.h>
#include <stdint.h>
#include "board/nl_board.h"
#include "light/nl_light_types.h"
#define NL_BLINK_PHASE_MS 300u
#define NL_PULSE_PERIOD_MS 3000u
typedef void (*nl_output_fn)(const uint8_t ch[NL_CH_COUNT], void *ctx);
typedef struct {
uint16_t ct_min;
uint16_t ct_max;
nl_output_fn output;
void *ctx;
} nl_engine_cfg_t;
/* Not thread safe: callers serialise access. */
void nl_engine_init(const nl_engine_cfg_t *cfg);
void nl_engine_set(const nl_light_state_t *target, uint32_t transition_ms);
void nl_engine_tick(uint32_t elapsed_ms);
bool nl_engine_idle(void);
void nl_engine_current(nl_visual_t *out);
void nl_engine_blink(uint8_t count);
void nl_engine_pulse(bool enable);
#endif
+1
View File
@@ -26,3 +26,4 @@ nl_test(test_sha256 test_sha256.c ${NL_SRC}/util/nl_sha256.c)
nl_test(test_crc test_crc.c ${NL_SRC}/util/nl_crc.c)
nl_test(test_board test_board.c ${NL_SRC}/board/nl_board.c)
nl_test(test_color test_color.c ${NL_SRC}/light/nl_color.c)
nl_test(test_engine test_engine.c ${NL_SRC}/light/nl_engine.c ${NL_SRC}/light/nl_color.c)
+188
View File
@@ -0,0 +1,188 @@
#include <string.h>
#include "unity.h"
#include "light/nl_engine.h"
static uint8_t last[NL_CH_COUNT];
static int calls;
static void out(const uint8_t ch[NL_CH_COUNT], void *ctx)
{
(void)ctx;
memcpy(last, ch, NL_CH_COUNT);
calls++;
}
static nl_light_state_t st(bool on, uint8_t bri, nl_light_mode_t m, uint8_t r, uint8_t g, uint8_t b, uint16_t ct)
{
nl_light_state_t s = {on, bri, m, r, g, b, ct};
return s;
}
static nl_visual_t cur(void)
{
nl_visual_t v;
nl_engine_current(&v);
return v;
}
static int sum(void)
{
int s = 0;
for (int i = 0; i < NL_CH_COUNT; i++)
s += last[i];
return s;
}
void setUp(void)
{
nl_engine_cfg_t cfg = {2700, 6500, out, NULL};
memset(last, 0xAA, sizeof last);
calls = 0;
nl_engine_init(&cfg);
}
void tearDown(void) {}
static void test_init_is_dark_and_idle(void)
{
TEST_ASSERT_TRUE(calls >= 1);
TEST_ASSERT_EQUAL_INT(0, sum());
TEST_ASSERT_EQUAL_UINT16(0, cur().bri);
TEST_ASSERT_TRUE(nl_engine_idle());
}
static void test_instant_set(void)
{
nl_light_state_t s = st(true, 100, NL_MODE_WHITE, 0, 0, 0, 2700);
nl_engine_set(&s, 0);
TEST_ASSERT_EQUAL_UINT8(255, last[NL_CH_WW]);
TEST_ASSERT_EQUAL_UINT8(0, last[NL_CH_CW]);
TEST_ASSERT_TRUE(nl_engine_idle());
}
static void test_fade_in_is_linear_in_brightness(void)
{
nl_light_state_t s = st(true, 100, NL_MODE_WHITE, 0, 0, 0, 2700);
nl_engine_set(&s, 1000);
nl_engine_tick(500);
TEST_ASSERT_EQUAL_UINT16(5000, cur().bri);
TEST_ASSERT_FALSE(nl_engine_idle());
nl_engine_tick(500);
TEST_ASSERT_EQUAL_UINT16(10000, cur().bri);
TEST_ASSERT_TRUE(nl_engine_idle());
TEST_ASSERT_EQUAL_UINT8(255, last[NL_CH_WW]);
}
static void test_fade_out_keeps_current_color(void)
{
nl_light_state_t red = st(true, 100, NL_MODE_COLOR, 255, 0, 0, 2700);
nl_light_state_t off = st(false, 100, NL_MODE_WHITE, 0, 0, 0, 4000);
nl_engine_set(&red, 0);
nl_engine_set(&off, 1000);
nl_engine_tick(500);
TEST_ASSERT_EQUAL(NL_MODE_COLOR, cur().mode);
TEST_ASSERT_EQUAL_UINT16(5000, cur().bri);
TEST_ASSERT_TRUE(last[NL_CH_R] > 0);
nl_engine_tick(500);
TEST_ASSERT_EQUAL_INT(0, sum());
TEST_ASSERT_TRUE(nl_engine_idle());
TEST_ASSERT_EQUAL(NL_MODE_WHITE, cur().mode); /* target values applied at the end */
}
static void test_mode_switch_passes_through_dark(void)
{
nl_light_state_t red = st(true, 100, NL_MODE_COLOR, 255, 0, 0, 2700);
nl_light_state_t white = st(true, 100, NL_MODE_WHITE, 0, 0, 0, 4000);
nl_engine_set(&red, 0);
nl_engine_set(&white, 1000);
nl_engine_tick(250);
TEST_ASSERT_EQUAL(NL_MODE_COLOR, cur().mode);
TEST_ASSERT_EQUAL_UINT16(5000, cur().bri);
nl_engine_tick(250);
TEST_ASSERT_EQUAL_UINT16(0, cur().bri);
TEST_ASSERT_EQUAL_INT(0, sum());
nl_engine_tick(250);
TEST_ASSERT_EQUAL(NL_MODE_WHITE, cur().mode);
TEST_ASSERT_EQUAL_UINT16(5000, cur().bri);
nl_engine_tick(250);
TEST_ASSERT_EQUAL_UINT16(10000, cur().bri);
TEST_ASSERT_TRUE(nl_engine_idle());
}
static void test_leftover_time_carries_into_next_segment(void)
{
nl_light_state_t red = st(true, 100, NL_MODE_COLOR, 255, 0, 0, 2700);
nl_light_state_t white = st(true, 100, NL_MODE_WHITE, 0, 0, 0, 4000);
nl_engine_set(&red, 0);
nl_engine_set(&white, 1000);
nl_engine_tick(750);
TEST_ASSERT_EQUAL(NL_MODE_WHITE, cur().mode);
TEST_ASSERT_EQUAL_UINT16(5000, cur().bri);
}
static void test_new_target_starts_from_current_value(void)
{
nl_light_state_t full = st(true, 100, NL_MODE_WHITE, 0, 0, 0, 2700);
nl_light_state_t dim = st(true, 20, NL_MODE_WHITE, 0, 0, 0, 2700);
nl_engine_set(&full, 1000);
nl_engine_tick(500);
nl_engine_set(&dim, 1000);
nl_engine_tick(500);
TEST_ASSERT_EQUAL_UINT16(3500, cur().bri);
}
static void test_color_interpolates(void)
{
nl_light_state_t red = st(true, 100, NL_MODE_COLOR, 255, 0, 0, 2700);
nl_light_state_t blue = st(true, 100, NL_MODE_COLOR, 0, 0, 255, 2700);
nl_engine_set(&red, 0);
nl_engine_set(&blue, 1000);
nl_engine_tick(500);
TEST_ASSERT_INT_WITHIN(1, 128, cur().r);
TEST_ASSERT_INT_WITHIN(1, 127, cur().b);
}
static void test_blink_overrides_and_restores(void)
{
nl_engine_blink(2);
TEST_ASSERT_EQUAL_UINT8(255, last[NL_CH_WW]);
TEST_ASSERT_FALSE(nl_engine_idle());
nl_engine_tick(300);
TEST_ASSERT_EQUAL_INT(0, sum());
nl_engine_tick(300);
TEST_ASSERT_EQUAL_UINT8(255, last[NL_CH_WW]);
nl_engine_tick(300);
TEST_ASSERT_EQUAL_INT(0, sum());
nl_engine_tick(300);
TEST_ASSERT_EQUAL_INT(0, sum()); /* back to the (dark) state */
TEST_ASSERT_TRUE(nl_engine_idle());
}
static void test_pulse(void)
{
nl_engine_pulse(true);
uint8_t low = last[NL_CH_WW];
TEST_ASSERT_TRUE(low > 0);
nl_engine_tick(1500);
TEST_ASSERT_TRUE(last[NL_CH_WW] > low);
TEST_ASSERT_FALSE(nl_engine_idle());
nl_engine_pulse(false);
TEST_ASSERT_EQUAL_INT(0, sum());
TEST_ASSERT_TRUE(nl_engine_idle());
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_init_is_dark_and_idle);
RUN_TEST(test_instant_set);
RUN_TEST(test_fade_in_is_linear_in_brightness);
RUN_TEST(test_fade_out_keeps_current_color);
RUN_TEST(test_mode_switch_passes_through_dark);
RUN_TEST(test_leftover_time_carries_into_next_segment);
RUN_TEST(test_new_target_starts_from_current_value);
RUN_TEST(test_color_interpolates);
RUN_TEST(test_blink_overrides_and_restores);
RUN_TEST(test_pulse);
return UNITY_END();
}