feat: Farbrechnung mit CIE-Helligkeit und CW/WW-Mischung
This commit is contained in:
+2
-1
@@ -30,7 +30,8 @@ set(NL_FW_SOURCES
|
|||||||
src/util/nl_crc.c
|
src/util/nl_crc.c
|
||||||
src/sys/fwup_hook_default.c
|
src/sys/fwup_hook_default.c
|
||||||
src/sys/sdk_stubs.c
|
src/sys/sdk_stubs.c
|
||||||
src/board/nl_board.c)
|
src/board/nl_board.c
|
||||||
|
src/light/nl_color.c)
|
||||||
|
|
||||||
set(NL_FW ninlight_${NINLIGHT_DEVICE})
|
set(NL_FW ninlight_${NINLIGHT_DEVICE})
|
||||||
add_executable(${NL_FW} ${NL_SDK_BOOT_SOURCES} ${NL_FW_SOURCES} ${NL_SDK_OBJECTS})
|
add_executable(${NL_FW} ${NL_SDK_BOOT_SOURCES} ${NL_FW_SOURCES} ${NL_SDK_OBJECTS})
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#include "light/nl_color.h"
|
||||||
|
|
||||||
|
uint16_t nl_cie_level(uint16_t bri)
|
||||||
|
{
|
||||||
|
float lightness, y;
|
||||||
|
uint32_t v;
|
||||||
|
|
||||||
|
if (bri == 0)
|
||||||
|
return 0;
|
||||||
|
if (bri >= NL_BRI_SCALE)
|
||||||
|
return 65535;
|
||||||
|
lightness = (float)bri / 100.0f; /* 0..100 */
|
||||||
|
if (lightness <= 8.0f) {
|
||||||
|
y = lightness / 903.3f;
|
||||||
|
} else {
|
||||||
|
float t = (lightness + 16.0f) / 116.0f;
|
||||||
|
y = t * t * t;
|
||||||
|
}
|
||||||
|
v = (uint32_t)(y * 65535.0f + 0.5f);
|
||||||
|
return (uint16_t)(v == 0 ? 1 : (v > 65535 ? 65535 : v));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t nl_ct_cw_fraction(uint16_t ct, uint16_t ct_min, uint16_t ct_max)
|
||||||
|
{
|
||||||
|
uint32_t m, mw, mc;
|
||||||
|
|
||||||
|
if (ct <= ct_min)
|
||||||
|
return 0;
|
||||||
|
if (ct >= ct_max)
|
||||||
|
return 65535;
|
||||||
|
m = 1000000u / ct;
|
||||||
|
mw = 1000000u / ct_min;
|
||||||
|
mc = 1000000u / ct_max;
|
||||||
|
return (uint16_t)(((mw - m) * 65535u) / (mw - mc));
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t to8(uint32_t lin)
|
||||||
|
{
|
||||||
|
return (uint8_t)((lin * 255u + 32767u) / 65535u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nl_visual_to_channels(const nl_visual_t *v, uint16_t ct_min, uint16_t ct_max, uint8_t out[NL_CH_COUNT])
|
||||||
|
{
|
||||||
|
uint32_t level = nl_cie_level(v->bri);
|
||||||
|
|
||||||
|
for (int i = 0; i < NL_CH_COUNT; i++)
|
||||||
|
out[i] = 0;
|
||||||
|
if (level == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (v->mode == NL_MODE_COLOR) {
|
||||||
|
uint32_t lin[3];
|
||||||
|
int maxi = 0;
|
||||||
|
lin[0] = level * v->r / 255u;
|
||||||
|
lin[1] = level * v->g / 255u;
|
||||||
|
lin[2] = level * v->b / 255u;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
out[NL_CH_R + i] = to8(lin[i]);
|
||||||
|
if (lin[i] > lin[maxi])
|
||||||
|
maxi = i;
|
||||||
|
}
|
||||||
|
/* keep the lowest brightness step visible */
|
||||||
|
if (!out[NL_CH_R] && !out[NL_CH_G] && !out[NL_CH_B] && lin[maxi] > 0)
|
||||||
|
out[NL_CH_R + maxi] = 1;
|
||||||
|
} else {
|
||||||
|
uint32_t cw = level * nl_ct_cw_fraction(v->ct, ct_min, ct_max) / 65535u;
|
||||||
|
uint32_t ww = level - cw;
|
||||||
|
out[NL_CH_CW] = to8(cw);
|
||||||
|
out[NL_CH_WW] = to8(ww);
|
||||||
|
if (!out[NL_CH_CW] && !out[NL_CH_WW])
|
||||||
|
out[cw >= ww ? NL_CH_CW : NL_CH_WW] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef NL_COLOR_H
|
||||||
|
#define NL_COLOR_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "board/nl_board.h"
|
||||||
|
#include "light/nl_light_types.h"
|
||||||
|
|
||||||
|
/* Perceived brightness (0..NL_BRI_SCALE) to linear light (0..65535), CIE 1931 lightness.
|
||||||
|
* Every bri > 0 yields at least 1. */
|
||||||
|
uint16_t nl_cie_level(uint16_t bri);
|
||||||
|
|
||||||
|
/* Share of cold white (0..65535) for ct, linear in mired between ct_min and ct_max. */
|
||||||
|
uint16_t nl_ct_cw_fraction(uint16_t ct, uint16_t ct_min, uint16_t ct_max);
|
||||||
|
|
||||||
|
/* 8 bit channel values for the LED drivers. In white mode cw + ww equals the brightness. */
|
||||||
|
void nl_visual_to_channels(const nl_visual_t *v, uint16_t ct_min, uint16_t ct_max, uint8_t out[NL_CH_COUNT]);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef NL_LIGHT_TYPES_H
|
||||||
|
#define NL_LIGHT_TYPES_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define NL_BRI_SCALE 10000u /* visual brightness fixed point: 10000 = 100.00 % */
|
||||||
|
|
||||||
|
typedef enum { NL_MODE_COLOR = 0, NL_MODE_WHITE = 1 } nl_light_mode_t;
|
||||||
|
typedef enum { NL_POB_LAST = 0, NL_POB_ON = 1, NL_POB_OFF = 2 } nl_pob_t;
|
||||||
|
|
||||||
|
/* Light state as seen by the API (brightness in percent). */
|
||||||
|
typedef struct {
|
||||||
|
bool on;
|
||||||
|
uint8_t brightness; /* 1..100 */
|
||||||
|
nl_light_mode_t mode;
|
||||||
|
uint8_t r, g, b;
|
||||||
|
uint16_t ct; /* Kelvin */
|
||||||
|
} nl_light_state_t;
|
||||||
|
|
||||||
|
/* What is currently visible; bri = 0 means dark. */
|
||||||
|
typedef struct {
|
||||||
|
uint16_t bri; /* 0..NL_BRI_SCALE */
|
||||||
|
nl_light_mode_t mode;
|
||||||
|
uint8_t r, g, b;
|
||||||
|
uint16_t ct;
|
||||||
|
} nl_visual_t;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -25,3 +25,4 @@ endfunction()
|
|||||||
nl_test(test_sha256 test_sha256.c ${NL_SRC}/util/nl_sha256.c)
|
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_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_board test_board.c ${NL_SRC}/board/nl_board.c)
|
||||||
|
nl_test(test_color test_color.c ${NL_SRC}/light/nl_color.c)
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#include "unity.h"
|
||||||
|
#include "light/nl_color.h"
|
||||||
|
|
||||||
|
void setUp(void) {}
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
static nl_visual_t vis(uint16_t bri, nl_light_mode_t mode, uint8_t r, uint8_t g, uint8_t b, uint16_t ct)
|
||||||
|
{
|
||||||
|
nl_visual_t v = {bri, mode, r, g, b, ct};
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_cie_endpoints(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(0, nl_cie_level(0));
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(65535, nl_cie_level(10000));
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(65535, nl_cie_level(12000));
|
||||||
|
TEST_ASSERT_UINT16_WITHIN(1, 73, nl_cie_level(100)); /* 1 % -> L=1 -> Y=1/903.3 */
|
||||||
|
TEST_ASSERT_UINT16_WITHIN(3, 12071, nl_cie_level(5000)); /* 50 % -> ((66/116)^3) */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_cie_monotonic(void)
|
||||||
|
{
|
||||||
|
uint16_t prev = 0;
|
||||||
|
for (uint16_t b = 1; b <= 10000; b++) {
|
||||||
|
uint16_t v = nl_cie_level(b);
|
||||||
|
TEST_ASSERT_TRUE(v >= prev);
|
||||||
|
TEST_ASSERT_TRUE(v > 0);
|
||||||
|
prev = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_ct_fraction(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(0, nl_ct_cw_fraction(2700, 2700, 6500));
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(0, nl_ct_cw_fraction(2000, 2700, 6500));
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(65535, nl_ct_cw_fraction(6500, 2700, 6500));
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(65535, nl_ct_cw_fraction(9000, 2700, 6500));
|
||||||
|
/* mired midpoint of 2700 K (370.4) and 6500 K (153.8) is 262.1 mired = 3815 K */
|
||||||
|
TEST_ASSERT_UINT16_WITHIN(400, 32768, nl_ct_cw_fraction(3815, 2700, 6500));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_white_endpoints(void)
|
||||||
|
{
|
||||||
|
uint8_t ch[NL_CH_COUNT];
|
||||||
|
nl_visual_t v = vis(10000, NL_MODE_WHITE, 0, 0, 0, 2700);
|
||||||
|
nl_visual_to_channels(&v, 2700, 6500, ch);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(255, ch[NL_CH_WW]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_CW]);
|
||||||
|
v.ct = 6500;
|
||||||
|
nl_visual_to_channels(&v, 2700, 6500, ch);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(255, ch[NL_CH_CW]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_WW]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_white_mix_keeps_total(void)
|
||||||
|
{
|
||||||
|
uint8_t ch[NL_CH_COUNT];
|
||||||
|
nl_visual_t v = vis(10000, NL_MODE_WHITE, 0, 0, 0, 3815);
|
||||||
|
nl_visual_to_channels(&v, 2700, 6500, ch);
|
||||||
|
TEST_ASSERT_UINT8_WITHIN(3, 128, ch[NL_CH_CW]);
|
||||||
|
TEST_ASSERT_INT_WITHIN(1, 255, ch[NL_CH_CW] + ch[NL_CH_WW]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_R]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_G]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_B]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_color_mode(void)
|
||||||
|
{
|
||||||
|
uint8_t ch[NL_CH_COUNT];
|
||||||
|
nl_visual_t v = vis(10000, NL_MODE_COLOR, 255, 120, 0, 2700);
|
||||||
|
nl_visual_to_channels(&v, 2700, 6500, ch);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(255, ch[NL_CH_R]);
|
||||||
|
TEST_ASSERT_UINT8_WITHIN(1, 120, ch[NL_CH_G]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_B]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_CW]);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_WW]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_one_percent_is_visible(void)
|
||||||
|
{
|
||||||
|
uint8_t ch[NL_CH_COUNT];
|
||||||
|
nl_visual_t v = vis(100, NL_MODE_COLOR, 255, 0, 0, 2700);
|
||||||
|
nl_visual_to_channels(&v, 2700, 6500, ch);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1, ch[NL_CH_R]);
|
||||||
|
v = vis(100, NL_MODE_WHITE, 0, 0, 0, 3000);
|
||||||
|
nl_visual_to_channels(&v, 2700, 6500, ch);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, ch[NL_CH_CW] + ch[NL_CH_WW]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_zero_brightness_is_dark(void)
|
||||||
|
{
|
||||||
|
uint8_t ch[NL_CH_COUNT];
|
||||||
|
nl_visual_t v = vis(0, NL_MODE_COLOR, 255, 255, 255, 2700);
|
||||||
|
nl_visual_to_channels(&v, 2700, 6500, ch);
|
||||||
|
for (int i = 0; i < NL_CH_COUNT; i++)
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, ch[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_cie_endpoints);
|
||||||
|
RUN_TEST(test_cie_monotonic);
|
||||||
|
RUN_TEST(test_ct_fraction);
|
||||||
|
RUN_TEST(test_white_endpoints);
|
||||||
|
RUN_TEST(test_white_mix_keeps_total);
|
||||||
|
RUN_TEST(test_color_mode);
|
||||||
|
RUN_TEST(test_one_percent_is_visible);
|
||||||
|
RUN_TEST(test_zero_brightness_is_dark);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user