feat: Default-Transition, Weckschalter und farbfreier Ausschaltfade

Zwei neue Einstellungen, beide wie fade_in/fade_out ueber
POST /api/v1/device/state les- und schreibbar und sofort wirksam:

- transition_default (Voreinstellung 300 ms) fuellt die Transition fuer
  Farb-, Weiss- und Helligkeitswechsel bei eingeschalteter Lampe, wenn der
  Aufruf kein eigenes "transition" mitbringt. Beim Ein- und Ausschalten
  behalten fade_in/fade_out weiterhin Vorrang.
- wake_on_change (Voreinstellung aus) schaltet die Lampe bei einer Farb-,
  Weiss- oder Helligkeitsaenderung ein, wenn sie aus war. Ein
  ausdrueckliches "on": false im selben Aufruf gewinnt immer.

Beide Felder haengen am Ende von nl_settings_t, damit bestehende Lampen
ueber die Laengentoleranz von read_rec() ihre echten Voreinstellungen
bekommen statt Nullen. nl_ctrl_set_fades() heisst jetzt
nl_ctrl_set_options().

Dazu ein Treiberfehler, der erst an der migrierten GU10 RGB auffiel: der
WY-Zweig von nl_sm2135eh_set() schrieb nur Header und WY-Daten, nie die
RGB-Datenregister. Der Chip behielt also die zuletzt gesetzte Farbe, und
bei einem direkten Wechsel Farbe -> Weiss (ohne Zwischenschritt ueber
dunkel) schaelte sie sich beim Ausblenden wieder frei. Der Header sendet
jetzt die - in Weiss immer nullwertigen - RGB-Daten mit.

Die Bruno-Collection zeigt Transition jetzt zusammen mit einem Farb- und
einem Weisswechsel im selben Aufruf und hat einen Request fuer die
beiden neuen Einstellungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Michael Sauer
2026-09-13 02:56:56 +02:00
co-authored by Claude Opus 5
parent 62a2b43175
commit 00d5a4c532
13 changed files with 256 additions and 19 deletions
+25 -4
View File
@@ -96,7 +96,7 @@ items:
followRedirects: true followRedirects: true
maxRedirects: 5 maxRedirects: 5
- info: - info:
name: Color Red name: Color Red + Transition
type: http type: http
seq: 5 seq: 5
http: http:
@@ -110,7 +110,8 @@ items:
"r": 255, "r": 255,
"g": 0, "g": 0,
"b": 0 "b": 0
} },
"transition": 1500
} }
settings: settings:
encodeUrl: true encodeUrl: true
@@ -118,7 +119,7 @@ items:
followRedirects: true followRedirects: true
maxRedirects: 5 maxRedirects: 5
- info: - info:
name: Color Temp 2700 name: Color Temp 2700 + Transition
type: http type: http
seq: 6 seq: 6
http: http:
@@ -128,7 +129,8 @@ items:
type: json type: json
data: |- data: |-
{ {
"color_temp": 2700 "color_temp": 2700,
"transition": 1500
} }
settings: settings:
encodeUrl: true encodeUrl: true
@@ -192,6 +194,25 @@ items:
timeout: 0 timeout: 0
followRedirects: true followRedirects: true
maxRedirects: 5 maxRedirects: 5
- info:
name: Default Transition + Wake Switch
type: http
seq: 10
http:
method: POST
url: '{{device_address}}:{{device_port}}/api/v1/device/state'
body:
type: json
data: |-
{
"transition_default": 300,
"wake_on_change": false
}
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5
- info: - info:
name: System name: System
type: folder type: folder
+2 -1
View File
@@ -202,7 +202,8 @@ static void app_task(void *arg)
after_ota); after_ota);
nl_app_lock(); nl_app_lock();
nl_engine_init(&cfg); nl_engine_init(&cfg);
nl_ctrl_init(board, s_settings.fade_in_ms, s_settings.fade_out_ms); nl_ctrl_init(board, s_settings.fade_in_ms, s_settings.fade_out_ms, s_settings.transition_ms,
s_settings.wake_on_change != 0);
nl_ctrl_boot(&initial); nl_ctrl_boot(&initial);
nl_app_unlock(); nl_app_unlock();
+2
View File
@@ -47,6 +47,8 @@ void nl_settings_defaults(nl_settings_t *s)
s->power_on_behaviour = NL_POB_LAST; s->power_on_behaviour = NL_POB_LAST;
s->fade_in_ms = 500; s->fade_in_ms = 500;
s->fade_out_ms = 500; s->fade_out_ms = 500;
s->transition_ms = 300;
s->wake_on_change = 0;
} }
void nl_settings_init(uint32_t addr_a, uint32_t addr_b) void nl_settings_init(uint32_t addr_a, uint32_t addr_b)
+9
View File
@@ -19,6 +19,15 @@ typedef struct {
uint16_t fade_in_ms; uint16_t fade_in_ms;
uint16_t fade_out_ms; uint16_t fade_out_ms;
uint8_t ota_pending; /* set before an OTA; the next boot restores the last state */ uint8_t ota_pending; /* set before an OTA; the next boot restores the last state */
/* Appended fields: read_rec() in nl_settings.c pre-fills the struct with
* nl_settings_defaults() and only overwrites the bytes actually stored, so records
* written by an older firmware pick up the real default here, not zero. Keep adding new
* fields at the end for that to keep working. */
uint16_t transition_ms; /* default transition (ms) for a color/brightness/CT change
* while the lamp stays on; fade_in/fade_out still win when
* switching on or off. */
uint8_t wake_on_change; /* if set, such a change also switches the lamp on when it was
* off, unless the same call says "on": false. */
} nl_settings_t; } nl_settings_t;
void nl_settings_defaults(nl_settings_t *s); void nl_settings_defaults(nl_settings_t *s);
+8 -2
View File
@@ -88,9 +88,15 @@ void nl_sm2135eh_set(const uint8_t ch[NL_CH_COUNT])
uint8_t d[6] = {SM_ADDR_CURRENT, s_current, chan, ch[NL_CH_B], ch[NL_CH_G], ch[NL_CH_R]}; uint8_t d[6] = {SM_ADDR_CURRENT, s_current, chan, ch[NL_CH_B], ch[NL_CH_G], ch[NL_CH_R]};
sm_send(d, 6); sm_send(d, 6);
} else { } else {
uint8_t h[3] = {SM_ADDR_CURRENT, s_current, SM_CHANNEL_WY}; /* The RGB data registers are written along with the channel select, even though they
are always zero here: the chip keeps whatever colour was last written, and a direct
colour -> white change never passes through the all-dark frame above. The stale
colour then bleeds through as soon as the white dims - visible as a coloured
fade-out on a migrated GU10 RGB, 2026-09-13. */
uint8_t h[6] = {SM_ADDR_CURRENT, s_current, SM_CHANNEL_WY,
ch[NL_CH_B], ch[NL_CH_G], ch[NL_CH_R]};
uint8_t d[3] = {SM_ADDR_WY, ch[NL_CH_WW], ch[NL_CH_CW]}; uint8_t d[3] = {SM_ADDR_WY, ch[NL_CH_WW], ch[NL_CH_CW]};
sm_send(h, 3); sm_send(h, 6);
sm_send(d, 3); sm_send(d, 3);
} }
} }
+17 -4
View File
@@ -7,23 +7,29 @@ static const nl_board_t *s_board;
static nl_light_state_t s_state; static nl_light_state_t s_state;
static uint16_t s_fade_in; static uint16_t s_fade_in;
static uint16_t s_fade_out; static uint16_t s_fade_out;
static uint16_t s_transition;
static bool s_wake_on_change;
static bool s_dirty; static bool s_dirty;
static uint32_t s_changed_at; static uint32_t s_changed_at;
void nl_ctrl_init(const nl_board_t *board, uint16_t fade_in_ms, uint16_t fade_out_ms) void nl_ctrl_init(const nl_board_t *board, uint16_t fade_in_ms, uint16_t fade_out_ms,
uint16_t transition_ms, bool wake_on_change)
{ {
s_board = board; s_board = board;
s_state = nl_boot_default_state(board); s_state = nl_boot_default_state(board);
s_state.on = false; s_state.on = false;
s_dirty = false; s_dirty = false;
s_changed_at = 0; s_changed_at = 0;
nl_ctrl_set_fades(fade_in_ms, fade_out_ms); nl_ctrl_set_options(fade_in_ms, fade_out_ms, transition_ms, wake_on_change);
} }
void nl_ctrl_set_fades(uint16_t fade_in_ms, uint16_t fade_out_ms) void nl_ctrl_set_options(uint16_t fade_in_ms, uint16_t fade_out_ms, uint16_t transition_ms,
bool wake_on_change)
{ {
s_fade_in = fade_in_ms > NL_MAX_FADE_MS ? NL_MAX_FADE_MS : fade_in_ms; s_fade_in = fade_in_ms > NL_MAX_FADE_MS ? NL_MAX_FADE_MS : fade_in_ms;
s_fade_out = fade_out_ms > NL_MAX_FADE_MS ? NL_MAX_FADE_MS : fade_out_ms; s_fade_out = fade_out_ms > NL_MAX_FADE_MS ? NL_MAX_FADE_MS : fade_out_ms;
s_transition = transition_ms > NL_MAX_TRANSITION_MS ? NL_MAX_TRANSITION_MS : transition_ms;
s_wake_on_change = wake_on_change;
} }
void nl_ctrl_boot(const nl_light_state_t *initial) void nl_ctrl_boot(const nl_light_state_t *initial)
@@ -62,6 +68,13 @@ nl_ctrl_err_t nl_ctrl_apply(const nl_light_change_t *c, uint32_t now_ms)
n.mode = NL_MODE_WHITE; n.mode = NL_MODE_WHITE;
} }
/* Weckschalter: a color/CT/brightness change arriving while the lamp is off switches it
* on, unless this same call also says "on": false explicitly - that always wins. Applied
* before the duration below so an off->on transition here uses fade_in like any other. */
if (s_wake_on_change && !s_state.on && !n.on && !(c->has_on && !c->on) &&
(c->has_color || c->has_ct || c->has_brightness))
n.on = true;
if (c->has_transition) if (c->has_transition)
dur = c->transition_ms; dur = c->transition_ms;
else if (!s_state.on && n.on) else if (!s_state.on && n.on)
@@ -69,7 +82,7 @@ nl_ctrl_err_t nl_ctrl_apply(const nl_light_change_t *c, uint32_t now_ms)
else if (s_state.on && !n.on) else if (s_state.on && !n.on)
dur = s_fade_out; dur = s_fade_out;
else else
dur = 0; dur = s_transition;
s_state = n; s_state = n;
nl_engine_set(&s_state, dur); nl_engine_set(&s_state, dur);
+11 -3
View File
@@ -32,9 +32,17 @@ typedef enum {
NL_CTRL_ERR_TRANSITION NL_CTRL_ERR_TRANSITION
} nl_ctrl_err_t; } nl_ctrl_err_t;
/* The engine must be initialised before. Not thread safe: callers serialise access. */ /* The engine must be initialised before. Not thread safe: callers serialise access.
void nl_ctrl_init(const nl_board_t *board, uint16_t fade_in_ms, uint16_t fade_out_ms); *
void nl_ctrl_set_fades(uint16_t fade_in_ms, uint16_t fade_out_ms); * transition_ms: default transition applied to a color/brightness/CT change while the lamp
* stays on and the caller did not send its own "transition" (fade_in/fade_out still take
* precedence when the lamp is switching on or off).
* wake_on_change: if true, a color/CT/brightness change arriving while the lamp is off
* switches it on (using fade_in), unless the same call also says "on": false. */
void nl_ctrl_init(const nl_board_t *board, uint16_t fade_in_ms, uint16_t fade_out_ms,
uint16_t transition_ms, bool wake_on_change);
void nl_ctrl_set_options(uint16_t fade_in_ms, uint16_t fade_out_ms, uint16_t transition_ms,
bool wake_on_change);
void nl_ctrl_boot(const nl_light_state_t *initial); void nl_ctrl_boot(const nl_light_state_t *initial);
nl_ctrl_err_t nl_ctrl_apply(const nl_light_change_t *c, uint32_t now_ms); nl_ctrl_err_t nl_ctrl_apply(const nl_light_change_t *c, uint32_t now_ms);
void nl_ctrl_get(nl_light_state_t *out); void nl_ctrl_get(nl_light_state_t *out);
+16
View File
@@ -115,6 +115,11 @@ static cJSON *state_json(const nl_api_ctx_t *ctx)
POB_NAMES[s.power_on_behaviour <= NL_POB_OFF ? s.power_on_behaviour : NL_POB_LAST]); POB_NAMES[s.power_on_behaviour <= NL_POB_OFF ? s.power_on_behaviour : NL_POB_LAST]);
cJSON_AddNumberToObject(o, "fade_in", s.fade_in_ms); cJSON_AddNumberToObject(o, "fade_in", s.fade_in_ms);
cJSON_AddNumberToObject(o, "fade_out", s.fade_out_ms); cJSON_AddNumberToObject(o, "fade_out", s.fade_out_ms);
/* Named "transition_default", not "transition": the latter is already the per-call field
* above the settings section of this same request/response and a second field with the
* same key in one JSON object would collide with it. */
cJSON_AddNumberToObject(o, "transition_default", s.transition_ms);
cJSON_AddBoolToObject(o, "wake_on_change", s.wake_on_change != 0);
cap = cJSON_AddObjectToObject(o, "capabilities"); cap = cJSON_AddObjectToObject(o, "capabilities");
cJSON_AddBoolToObject(cap, "color", ctx->board->has_rgb); cJSON_AddBoolToObject(cap, "color", ctx->board->has_rgb);
ct[0] = ctx->board->ct_min; ct[0] = ctx->board->ct_min;
@@ -193,6 +198,17 @@ static void handle_post_state(const nl_api_ctx_t *ctx, const nl_http_request_t *
case 1: s.fade_out_ms = (uint16_t)v; settings = true; break; case 1: s.fade_out_ms = (uint16_t)v; settings = true; break;
default: break; default: break;
} }
switch (get_int(j, "transition_default", 0, NL_MAX_TRANSITION_MS, &v)) {
case -1: err = "invalid transition_default"; goto out;
case 1: s.transition_ms = (uint16_t)v; settings = true; break;
default: break;
}
it = cJSON_GetObjectItemCaseSensitive(j, "wake_on_change");
if (it) {
if (!cJSON_IsBool(it)) { err = "invalid wake_on_change"; goto out; }
s.wake_on_change = cJSON_IsTrue(it) ? 1u : 0u;
settings = true;
}
if (light) { if (light) {
switch (ctx->apply(&ch)) { switch (ctx->apply(&ch)) {
+1 -1
View File
@@ -58,7 +58,7 @@ static int save_settings(const nl_settings_t *s)
memcpy(merged.wifi_pass, cur->wifi_pass, sizeof merged.wifi_pass); memcpy(merged.wifi_pass, cur->wifi_pass, sizeof merged.wifi_pass);
*cur = merged; *cur = merged;
} }
nl_ctrl_set_fades(cur->fade_in_ms, cur->fade_out_ms); nl_ctrl_set_options(cur->fade_in_ms, cur->fade_out_ms, cur->transition_ms, cur->wake_on_change != 0);
memcpy(name, cur->name, sizeof name); memcpy(name, cur->name, sizeof name);
nl_app_unlock(); nl_app_unlock();
if (name_changed) if (name_changed)
+1 -1
View File
@@ -78,7 +78,7 @@ static void exec(const nl_cmd_t *c)
nl_app_lock(); nl_app_lock();
cfg->fade_in_ms = (uint16_t)c->arg[0]; cfg->fade_in_ms = (uint16_t)c->arg[0];
cfg->fade_out_ms = (uint16_t)c->arg[1]; cfg->fade_out_ms = (uint16_t)c->arg[1];
nl_ctrl_set_fades(cfg->fade_in_ms, cfg->fade_out_ms); nl_ctrl_set_options(cfg->fade_in_ms, cfg->fade_out_ms, cfg->transition_ms, cfg->wake_on_change != 0);
nl_app_unlock(); nl_app_unlock();
printf(nl_app_save_settings() == 0 ? "ok\r\n" : "error: save failed\r\n"); printf(nl_app_save_settings() == 0 ? "ok\r\n" : "error: save failed\r\n");
print_state(); print_state();
+33
View File
@@ -55,6 +55,8 @@ static void test_get_state(void)
TEST_ASSERT_EQUAL_STRING("last", cJSON_GetStringValue(field("power_on_behaviour"))); TEST_ASSERT_EQUAL_STRING("last", cJSON_GetStringValue(field("power_on_behaviour")));
TEST_ASSERT_EQUAL_INT(700, field("fade_in")->valueint); TEST_ASSERT_EQUAL_INT(700, field("fade_in")->valueint);
TEST_ASSERT_EQUAL_INT(500, field("fade_out")->valueint); TEST_ASSERT_EQUAL_INT(500, field("fade_out")->valueint);
TEST_ASSERT_EQUAL_INT(300, field("transition_default")->valueint);
TEST_ASSERT_TRUE(cJSON_IsFalse(field("wake_on_change")));
cap = field("capabilities"); cap = field("capabilities");
TEST_ASSERT_TRUE(cJSON_IsTrue(cJSON_GetObjectItem(cap, "color"))); TEST_ASSERT_TRUE(cJSON_IsTrue(cJSON_GetObjectItem(cap, "color")));
TEST_ASSERT_EQUAL_INT(2700, cJSON_GetArrayItem(cJSON_GetObjectItem(cap, "color_temp"), 0)->valueint); TEST_ASSERT_EQUAL_INT(2700, cJSON_GetArrayItem(cJSON_GetObjectItem(cap, "color_temp"), 0)->valueint);
@@ -114,6 +116,8 @@ static void test_post_validation(void)
{"{\"power_on_behaviour\":\"maybe\"}", "invalid power_on_behaviour"}, {"{\"power_on_behaviour\":\"maybe\"}", "invalid power_on_behaviour"},
{"{\"fade_in\":10001}", "invalid fade_in"}, {"{\"fade_in\":10001}", "invalid fade_in"},
{"{\"fade_out\":-1}", "invalid fade_out"}, {"{\"fade_out\":-1}", "invalid fade_out"},
{"{\"transition_default\":60001}", "invalid transition_default"},
{"{\"wake_on_change\":\"ja\"}", "invalid wake_on_change"},
}; };
for (size_t i = 0; i < sizeof CASES / sizeof CASES[0]; i++) { for (size_t i = 0; i < sizeof CASES / sizeof CASES[0]; i++) {
call(NL_HTTP_POST, "/api/v1/device/state", NULL, CASES[i].body); call(NL_HTTP_POST, "/api/v1/device/state", NULL, CASES[i].body);
@@ -151,6 +155,33 @@ static void test_post_settings_fields(void)
TEST_ASSERT_EQUAL_STRING("on", cJSON_GetStringValue(field("power_on_behaviour"))); TEST_ASSERT_EQUAL_STRING("on", cJSON_GetStringValue(field("power_on_behaviour")));
} }
static void test_post_transition_default_and_wake(void)
{
call(NL_HTTP_POST, "/api/v1/device/state", NULL,
"{\"transition_default\":1200,\"wake_on_change\":true}");
TEST_ASSERT_EQUAL_INT(200, resp.status);
TEST_ASSERT_EQUAL_INT(1, fake_save_calls);
TEST_ASSERT_EQUAL_UINT16(1200, fake_settings.transition_ms);
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.wake_on_change);
TEST_ASSERT_EQUAL_INT(1200, field("transition_default")->valueint);
TEST_ASSERT_TRUE(cJSON_IsTrue(field("wake_on_change")));
}
/* A colour and a per-call transition travel in the same request - the light change is
applied, and nothing is treated as a settings write. */
static void test_post_color_with_transition(void)
{
call(NL_HTTP_POST, "/api/v1/device/state", NULL,
"{\"color\":{\"r\":255,\"g\":0,\"b\":0},\"transition\":2000}");
TEST_ASSERT_EQUAL_INT(200, resp.status);
TEST_ASSERT_EQUAL_INT(1, fake_apply_calls);
TEST_ASSERT_EQUAL_INT(0, fake_save_calls);
TEST_ASSERT_TRUE(fake_last_change.has_transition);
TEST_ASSERT_EQUAL_UINT32(2000, fake_last_change.transition_ms);
TEST_ASSERT_TRUE(fake_last_change.has_color);
TEST_ASSERT_EQUAL_UINT8(255, fake_last_change.r);
}
static void test_post_save_failure(void) static void test_post_save_failure(void)
{ {
fake_save_result = -1; fake_save_result = -1;
@@ -364,6 +395,8 @@ int main(void)
RUN_TEST(test_post_validation); RUN_TEST(test_post_validation);
RUN_TEST(test_post_control_errors); RUN_TEST(test_post_control_errors);
RUN_TEST(test_post_settings_fields); RUN_TEST(test_post_settings_fields);
RUN_TEST(test_post_transition_default_and_wake);
RUN_TEST(test_post_color_with_transition);
RUN_TEST(test_post_save_failure); RUN_TEST(test_post_save_failure);
RUN_TEST(test_auth_all_protects_device_state); RUN_TEST(test_auth_all_protects_device_state);
RUN_TEST(test_auth_too_long_is_unauthenticated); RUN_TEST(test_auth_too_long_is_unauthenticated);
+115 -3
View File
@@ -10,18 +10,24 @@ static void out(const uint8_t ch[NL_CH_COUNT], void *ctx)
(void)ctx; (void)ctx;
} }
static void start(const char *board_id, bool on) static void start_opts(const char *board_id, bool on, uint16_t transition_ms, bool wake)
{ {
const nl_board_t *b = nl_board_find(board_id); const nl_board_t *b = nl_board_find(board_id);
nl_engine_cfg_t cfg = {b->ct_min, b->ct_max, out, NULL}; nl_engine_cfg_t cfg = {b->ct_min, b->ct_max, out, NULL};
nl_engine_init(&cfg); nl_engine_init(&cfg);
nl_ctrl_init(b, 500, 800); nl_ctrl_init(b, 500, 800, transition_ms, wake);
nl_light_state_t s = nl_boot_default_state(b); nl_light_state_t s = nl_boot_default_state(b);
s.on = on; s.on = on;
nl_ctrl_boot(&s); nl_ctrl_boot(&s);
nl_engine_tick(10000); /* finish a possible boot fade */ nl_engine_tick(10000); /* finish a possible boot fade */
} }
/* Most tests want the historical behaviour: no default transition, no wake switch. */
static void start(const char *board_id, bool on)
{
start_opts(board_id, on, 0, false);
}
static uint16_t bri(void) static uint16_t bri(void)
{ {
nl_visual_t v; nl_visual_t v;
@@ -188,13 +194,112 @@ static void test_boot_fades_in_when_on(void)
const nl_board_t *b = nl_board_find("evk"); const nl_board_t *b = nl_board_find("evk");
nl_engine_cfg_t cfg = {b->ct_min, b->ct_max, out, NULL}; nl_engine_cfg_t cfg = {b->ct_min, b->ct_max, out, NULL};
nl_engine_init(&cfg); nl_engine_init(&cfg);
nl_ctrl_init(b, 500, 800); nl_ctrl_init(b, 500, 800, 0, false);
nl_light_state_t s = nl_boot_default_state(b); nl_light_state_t s = nl_boot_default_state(b);
nl_ctrl_boot(&s); nl_ctrl_boot(&s);
nl_engine_tick(250); nl_engine_tick(250);
TEST_ASSERT_EQUAL_UINT16(5000, bri()); TEST_ASSERT_EQUAL_UINT16(5000, bri());
} }
static void test_default_transition_applies_while_on(void)
{
start_opts("evk", true, 400, false);
nl_light_change_t c = chg();
c.has_brightness = true;
c.brightness = 50;
nl_ctrl_apply(&c, 0);
TEST_ASSERT_EQUAL_UINT16(10000, bri()); /* not instant any more */
nl_engine_tick(200);
TEST_ASSERT_EQUAL_UINT16(7500, bri());
nl_engine_tick(200);
TEST_ASSERT_EQUAL_UINT16(5000, bri());
}
static void test_explicit_transition_beats_default(void)
{
start_opts("evk", true, 5000, false);
nl_light_change_t c = chg();
c.has_brightness = true;
c.brightness = 50;
c.has_transition = true;
c.transition_ms = 100;
nl_ctrl_apply(&c, 0);
nl_engine_tick(100);
TEST_ASSERT_EQUAL_UINT16(5000, bri());
}
static void test_fades_beat_default_transition_when_switching(void)
{
start_opts("evk", false, 5000, false);
nl_light_change_t c = chg();
c.has_on = true;
c.on = true;
nl_ctrl_apply(&c, 0);
nl_engine_tick(500); /* fade_in, not the 5 s default */
TEST_ASSERT_EQUAL_UINT16(10000, bri());
c = chg();
c.has_on = true;
c.on = false;
nl_ctrl_apply(&c, 1000);
nl_engine_tick(800); /* fade_out */
TEST_ASSERT_EQUAL_UINT16(0, bri());
}
static void test_wake_switch_off_leaves_lamp_off(void)
{
start_opts("evk", false, 300, false);
nl_light_change_t c = chg();
c.has_ct = true;
c.ct = 3000;
nl_ctrl_apply(&c, 0);
nl_light_state_t s;
nl_ctrl_get(&s);
TEST_ASSERT_FALSE(s.on);
}
static void test_wake_switch_turns_lamp_on_with_fade_in(void)
{
start_opts("evk", false, 300, true);
nl_light_change_t c = chg();
c.has_ct = true;
c.ct = 3000;
nl_ctrl_apply(&c, 0);
nl_light_state_t s;
nl_ctrl_get(&s);
TEST_ASSERT_TRUE(s.on);
nl_engine_tick(250); /* fade_in = 500, not the 300 ms default transition */
TEST_ASSERT_EQUAL_UINT16(5000, bri());
}
static void test_wake_switch_also_reacts_to_brightness(void)
{
start_opts("evk", false, 300, true);
nl_light_change_t c = chg();
c.has_brightness = true;
c.brightness = 40;
nl_ctrl_apply(&c, 0);
nl_light_state_t s;
nl_ctrl_get(&s);
TEST_ASSERT_TRUE(s.on);
TEST_ASSERT_EQUAL_UINT8(40, s.brightness);
}
static void test_explicit_off_beats_wake_switch(void)
{
start_opts("evk", false, 300, true);
nl_light_change_t c = chg();
c.has_on = true;
c.on = false;
c.has_brightness = true;
c.brightness = 40;
nl_ctrl_apply(&c, 0);
nl_light_state_t s;
nl_ctrl_get(&s);
TEST_ASSERT_FALSE(s.on);
TEST_ASSERT_EQUAL_UINT8(40, s.brightness);
}
int main(void) int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
@@ -209,5 +314,12 @@ int main(void)
RUN_TEST(test_persist_debounce); RUN_TEST(test_persist_debounce);
RUN_TEST(test_change_during_write_stays_dirty); RUN_TEST(test_change_during_write_stays_dirty);
RUN_TEST(test_boot_fades_in_when_on); RUN_TEST(test_boot_fades_in_when_on);
RUN_TEST(test_default_transition_applies_while_on);
RUN_TEST(test_explicit_transition_beats_default);
RUN_TEST(test_fades_beat_default_transition_when_switching);
RUN_TEST(test_wake_switch_off_leaves_lamp_off);
RUN_TEST(test_wake_switch_turns_lamp_on_with_fade_in);
RUN_TEST(test_wake_switch_also_reacts_to_brightness);
RUN_TEST(test_explicit_off_beats_wake_switch);
return UNITY_END(); return UNITY_END();
} }
+16
View File
@@ -42,6 +42,8 @@ static void test_defaults(void)
TEST_ASSERT_EQUAL_UINT8(NL_POB_LAST, s.power_on_behaviour); TEST_ASSERT_EQUAL_UINT8(NL_POB_LAST, s.power_on_behaviour);
TEST_ASSERT_EQUAL_UINT16(500, s.fade_in_ms); TEST_ASSERT_EQUAL_UINT16(500, s.fade_in_ms);
TEST_ASSERT_EQUAL_UINT16(500, s.fade_out_ms); TEST_ASSERT_EQUAL_UINT16(500, s.fade_out_ms);
TEST_ASSERT_EQUAL_UINT16(300, s.transition_ms);
TEST_ASSERT_EQUAL_UINT8(0, s.wake_on_change);
} }
static void test_save_and_load(void) static void test_save_and_load(void)
@@ -108,6 +110,19 @@ static void test_load_accepts_shorter_record(void)
TEST_ASSERT_EQUAL_UINT16(500, out.fade_in_ms); TEST_ASSERT_EQUAL_UINT16(500, out.fade_in_ms);
TEST_ASSERT_EQUAL_UINT16(500, out.fade_out_ms); TEST_ASSERT_EQUAL_UINT16(500, out.fade_out_ms);
TEST_ASSERT_EQUAL_UINT8(0, out.ota_pending); TEST_ASSERT_EQUAL_UINT8(0, out.ota_pending);
TEST_ASSERT_EQUAL_UINT16(300, out.transition_ms);
TEST_ASSERT_EQUAL_UINT8(0, out.wake_on_change);
}
static void test_transition_and_wake_roundtrip(void)
{
nl_settings_t in = sample("neu"), out;
in.transition_ms = 1500;
in.wake_on_change = 1;
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
TEST_ASSERT_TRUE(nl_settings_load(&out));
TEST_ASSERT_EQUAL_UINT16(1500, out.transition_ms);
TEST_ASSERT_EQUAL_UINT8(1, out.wake_on_change);
} }
static void test_load_rejects_longer_record(void) static void test_load_rejects_longer_record(void)
@@ -171,6 +186,7 @@ int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
RUN_TEST(test_defaults); RUN_TEST(test_defaults);
RUN_TEST(test_transition_and_wake_roundtrip);
RUN_TEST(test_save_and_load); RUN_TEST(test_save_and_load);
RUN_TEST(test_alternates_slots); RUN_TEST(test_alternates_slots);
RUN_TEST(test_power_loss_keeps_previous); RUN_TEST(test_power_loss_keeps_previous);