From 8294688e379f7e1edf120aa7fdecfb8f23462017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20von=20B=C3=BClow?= <48244604+rvbuelow@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:26:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(board):=20ledshield=5Frgb=20=E2=80=94=20Mo?= =?UTF-8?q?ntana=20Black=20RGB-LED-Shield=20(analoge=20RGB-Streifen)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Viertes Analog-Profil neben den SM2135EH-Lampen: PWM-Kanaele R=PB13 (ch1), G=PB16 (ch2), B=PB15 (ch3) bei 800 Hz, PIN-belegt aus der Original-Firmware (qcl_build_config_lamp.h, HARDWARE_MOBL_LED_SHIELD). Kein CW/WW-Zweig: ct_min == ct_max ist das Erkennungszeichen fuer 'weiss = RGB-Mischung', nl_color.c gibt in diesem Fall alle drei Farbkanalgleich. Neues Bauziel in allen Skripten; test_release zaehlt Dateizahlen jetzt aus TARGETS statt hart 8/16. --- CMakeLists.txt | 2 +- src/board/nl_board.c | 8 ++++++++ src/light/nl_color.c | 5 +++++ test/test_board.c | 24 +++++++++++++++++------- test/test_color.c | 17 +++++++++++++++++ tools/build_all.sh | 2 +- tools/build_nocreds.sh | 4 ++-- tools/release.sh | 4 ++-- tools/tests/test_release.sh | 10 +++++----- 9 files changed, 58 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bae47fb..aa5242f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() -set(NINLIGHT_DEVICES evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw) +set(NINLIGHT_DEVICES evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw ledshield_rgb) set(NINLIGHT_DEVICE "" CACHE STRING "Target device") if(NOT NINLIGHT_DEVICE IN_LIST NINLIGHT_DEVICES) message(FATAL_ERROR "Set -DNINLIGHT_DEVICE=") diff --git a/src/board/nl_board.c b/src/board/nl_board.c index 526cd2a..e93b94f 100644 --- a/src/board/nl_board.c +++ b/src/board/nl_board.c @@ -31,6 +31,14 @@ const nl_board_t nl_boards[] = { * PB16=WW, PB15=CW - verify on a series bulb. */ {"g95_cw", "Klyqa G95 Vintage CW/WW", NL_DRV_PWM, false, 2000, 5000, 600, {NONE, NONE, NONE, CH(1, NL_PB(13)), CH(3, NL_PB(15))}, NO_SM}, + /* Montana Black LED Shield (Klyqa LED-Strippen-Steuergeraet, + product_id @qcx.montana.black.ledshield.rgb): analog RGB-Streifen an + lowseitigen MOSFETs, kein CW/WW-Zweig. PIN-Kanaele und 800 Hz aus der + Original-Firmware (fw-w600 qcl_build_config_lamp.h, HARDWARE_MOBL_LED_SHIELD); + dort invertiert init mit invertiertem Duty, effektive Wirkung wie hier + active-high. ct_min == ct_max markiert "weiss = RGB-Kombination". */ + {"ledshield_rgb", "Montana Black RGB LED Shield", NL_DRV_PWM, true, 4000, 4000, 800, + {CH(1, NL_PB(13)), CH(2, NL_PB(16)), CH(3, NL_PB(15)), NONE, NONE}, NO_SM}, }; const size_t nl_boards_count = sizeof(nl_boards) / sizeof(nl_boards[0]); diff --git a/src/light/nl_color.c b/src/light/nl_color.c index e559a3a..28b524e 100644 --- a/src/light/nl_color.c +++ b/src/light/nl_color.c @@ -62,6 +62,11 @@ void nl_visual_to_channels(const nl_visual_t *v, uint16_t ct_min, uint16_t ct_ma /* 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 if (ct_min == ct_max) { + /* RGB-only board (ledshield_rgb): no white channels wired, ct range + * collapsed to a point. White shows as an equal RGB mix. */ + uint8_t w = to8(level); + out[NL_CH_R] = out[NL_CH_G] = out[NL_CH_B] = w ? w : 1; } else { uint32_t cw = level * nl_ct_cw_fraction(v->ct, ct_min, ct_max) / 65535u; uint32_t ww = level - cw; diff --git a/test/test_board.c b/test/test_board.c index 939db9b..c6c6ab2 100644 --- a/test/test_board.c +++ b/test/test_board.c @@ -5,12 +5,12 @@ void setUp(void) {} void tearDown(void) {} -static const char *IDS[] = {"evk", "e27_rgb", "e14_rgb", "gu10_rgb", "e27_cw", "e14_cw", "gu10_cw", "g95_cw"}; +static const char *IDS[] = {"evk", "e27_rgb", "e14_rgb", "gu10_rgb", "e27_cw", "e14_cw", "gu10_cw", "g95_cw", "ledshield_rgb"}; static void test_all_devices_present(void) { - TEST_ASSERT_EQUAL_UINT(8, nl_boards_count); - for (size_t i = 0; i < 8; i++) { + TEST_ASSERT_EQUAL_UINT(9, nl_boards_count); + for (size_t i = 0; i < 9; i++) { const nl_board_t *b = nl_board_find(IDS[i]); TEST_ASSERT_NOT_NULL_MESSAGE(b, IDS[i]); TEST_ASSERT_EQUAL_STRING(IDS[i], b->id); @@ -34,9 +34,12 @@ static void test_rgb_flags_and_ct_ranges(void) 5000 K - hard-coded per board rather than derived, because "warm variant" is a property of that one product, not of a class of them. */ bool g95 = strcmp(b->id, "g95_cw") == 0; + /* ledshield_rgb has no white LEDs at all: ct_min == ct_max is the + sentinel "white mode renders as an RGB mix" (see nl_color.c). */ + bool shield = strcmp(b->id, "ledshield_rgb") == 0; TEST_ASSERT_EQUAL_MESSAGE(rgb, b->has_rgb, b->id); - TEST_ASSERT_EQUAL_UINT16_MESSAGE(g95 ? 2000 : 2700, b->ct_min, b->id); - TEST_ASSERT_EQUAL_UINT16_MESSAGE(g95 ? 5000 : 6500, b->ct_max, b->id); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(shield ? 4000 : (g95 ? 2000 : 2700), b->ct_min, b->id); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(shield ? 4000 : (g95 ? 5000 : 6500), b->ct_max, b->id); } } @@ -47,8 +50,15 @@ static void test_pwm_boards_consistent(void) if (b->driver != NL_DRV_PWM) continue; TEST_ASSERT_TRUE(b->pwm_freq_hz > 0); - TEST_ASSERT_TRUE(b->pwm[NL_CH_CW].pwm_ch >= 0); - TEST_ASSERT_TRUE(b->pwm[NL_CH_WW].pwm_ch >= 0); + if (b->ct_min == b->ct_max) { + /* RGB-only board: white is an RGB mix, no white channels wired. */ + TEST_ASSERT_TRUE(b->has_rgb); + TEST_ASSERT_EQUAL(-1, b->pwm[NL_CH_CW].pwm_ch); + TEST_ASSERT_EQUAL(-1, b->pwm[NL_CH_WW].pwm_ch); + } else { + TEST_ASSERT_TRUE(b->pwm[NL_CH_CW].pwm_ch >= 0); + TEST_ASSERT_TRUE(b->pwm[NL_CH_WW].pwm_ch >= 0); + } for (int c = NL_CH_R; c <= NL_CH_B; c++) TEST_ASSERT_EQUAL_MESSAGE(b->has_rgb, b->pwm[c].pwm_ch >= 0, b->id); for (int c = 0; c < NL_CH_COUNT; c++) { diff --git a/test/test_color.c b/test/test_color.c index 6074f3a..cf2cca2 100644 --- a/test/test_color.c +++ b/test/test_color.c @@ -88,6 +88,22 @@ static void test_one_percent_is_visible(void) TEST_ASSERT_EQUAL_INT(1, ch[NL_CH_CW] + ch[NL_CH_WW]); } +static void test_rgb_only_white_mode(void) +{ + uint8_t ch[NL_CH_COUNT]; + /* ct_min == ct_max: RGB-only board, white = equal RGB mix, no division. */ + nl_visual_t v = vis(10000, NL_MODE_WHITE, 0, 0, 0, 4000); + nl_visual_to_channels(&v, 4000, 4000, ch); + TEST_ASSERT_EQUAL_UINT8(255, ch[NL_CH_R]); + TEST_ASSERT_EQUAL_UINT8(255, ch[NL_CH_G]); + TEST_ASSERT_EQUAL_UINT8(255, ch[NL_CH_B]); + TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_CW]); + TEST_ASSERT_EQUAL_UINT8(0, ch[NL_CH_WW]); + v.bri = 100; /* 1 % stays visible */ + nl_visual_to_channels(&v, 4000, 4000, ch); + TEST_ASSERT_EQUAL_UINT8(1, ch[NL_CH_R]); +} + static void test_zero_brightness_is_dark(void) { uint8_t ch[NL_CH_COUNT]; @@ -107,6 +123,7 @@ int main(void) RUN_TEST(test_white_mix_keeps_total); RUN_TEST(test_color_mode); RUN_TEST(test_one_percent_is_visible); + RUN_TEST(test_rgb_only_white_mode); RUN_TEST(test_zero_brightness_is_dark); return UNITY_END(); } diff --git a/tools/build_all.sh b/tools/build_all.sh index a74cf56..8148243 100755 --- a/tools/build_all.sh +++ b/tools/build_all.sh @@ -3,7 +3,7 @@ set -euo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) OUT=${BUILD_ROOT:-$ROOT/build} -DEVICES=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw) +DEVICES=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw ledshield_rgb) for d in "${DEVICES[@]}"; do echo "== $d" cmake -S "$ROOT" -B "$OUT/$d" -G Ninja \ diff --git a/tools/build_nocreds.sh b/tools/build_nocreds.sh index 7eb6434..22ad3cd 100755 --- a/tools/build_nocreds.sh +++ b/tools/build_nocreds.sh @@ -2,7 +2,7 @@ # Baut Firmwareabzuege OHNE einkompilierte WLAN-Zugangsdaten: die Lampen starten damit # im Onboarding-AP (ninlight-), statt sich in ein fremdes WLAN einzuwaehlen. # -# Aufruf: tools/build_nocreds.sh [ziel ...] (ohne Argumente: alle acht Ziele) +# Aufruf: tools/build_nocreds.sh [ziel ...] (ohne Argumente: alle Ziele) # # Ergebnis: je Ziel ein Build-Verzeichnis unter $NINLIGHT_NOCREDS_BUILD_REL (Vorgabe # "build-nocreds", relativ zum Repo-Wurzelverzeichnis) mit den ueblichen Artefakten @@ -22,7 +22,7 @@ ROOT=$(cd "$(dirname "$0")/.." && pwd) DOCKER=${NINLIGHT_DOCKER:-$ROOT/tools/docker.sh} BUILD_REL=${NINLIGHT_NOCREDS_BUILD_REL:-build-nocreds} -DEVICES=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw) +DEVICES=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw ledshield_rgb) if [ $# -gt 0 ]; then TARGETS=("$@") else diff --git a/tools/release.sh b/tools/release.sh index 1e958da..ad3716c 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -14,7 +14,7 @@ set -euo pipefail VERSION=${1:?Aufruf: tools/release.sh } ROOT=$(cd "$(dirname "$0")/.." && pwd) -TARGETS=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw) +TARGETS=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw ledshield_rgb) BUILD_REL=${NINLIGHT_NOCREDS_BUILD_REL:-build-nocreds} BUILD_DIR="$ROOT/$BUILD_REL" NAME="ninlight-$VERSION" @@ -196,7 +196,7 @@ diesem Archiv sind **ohne einkompilierte WLAN-Zugangsdaten** gebaut: eine damit Lampe startet im Onboarding-Zugangspunkt \`ninlight-\` (Passwort \`ninlight\`) und wird mit \`onboard.sh\` ins WLAN gebracht. -Ziele: evk, e27_rgb, e14_rgb, gu10_rgb, e27_cw, e14_cw, gu10_cw, g95_cw. +Ziele: evk, e27_rgb, e14_rgb, gu10_rgb, e27_cw, e14_cw, gu10_cw, g95_cw, ledshield_rgb. ## Was liegt wo diff --git a/tools/tests/test_release.sh b/tools/tests/test_release.sh index fe1ebf5..9388846 100755 --- a/tools/tests/test_release.sh +++ b/tools/tests/test_release.sh @@ -41,7 +41,7 @@ FAILED=0 pass() { echo "ok - $1"; } fail() { echo "FEHLER - $1" >&2; FAILED=1; } -TARGETS=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw) +TARGETS=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw ledshield_rgb) # Wegwerf-Zugangsdaten - niemals die echten aus dem Repo. SECRET_SSID="TestWLAN-4b7f-geheim" @@ -233,9 +233,9 @@ if [ -x "$A/onboard.sh" ]; then pass "onboard.sh ist ausfuehrbar"; else fail "on N_OTA=$(find "$A/ota" -type f | wc -l | tr -d ' ') N_FLASH=$(find "$A/flash" -type f | wc -l | tr -d ' ') N_MIG=$(find "$A/migrate" -type f | wc -l | tr -d ' ') -[ "$N_OTA" = "8" ] && pass "ota/ enthaelt 8 Dateien" || fail "ota/ enthaelt $N_OTA statt 8" -[ "$N_FLASH" = "8" ] && pass "flash/ enthaelt 8 Dateien" || fail "flash/ enthaelt $N_FLASH statt 8" -[ "$N_MIG" = "16" ] && pass "migrate/ enthaelt 16 Dateien" || fail "migrate/ enthaelt $N_MIG statt 16" +[ "$N_OTA" = "${#TARGETS[@]}" ] && pass "ota/ enthaelt ${#TARGETS[@]} Dateien" || fail "ota/ enthaelt $N_OTA statt ${#TARGETS[@]}" +[ "$N_FLASH" = "${#TARGETS[@]}" ] && pass "flash/ enthaelt ${#TARGETS[@]} Dateien" || fail "flash/ enthaelt $N_FLASH statt ${#TARGETS[@]}" +[ "$N_MIG" = "$(( 2 * ${#TARGETS[@]} ))" ] && pass "migrate/ enthaelt $(( 2 * ${#TARGETS[@]} )) Dateien" || fail "migrate/ enthaelt $N_MIG statt $(( 2 * ${#TARGETS[@]} ))" MISS="" for t in "${TARGETS[@]}"; do [ -f "$A/ota/ninlight_${t}_ota.img" ] || MISS="$MISS ota/$t" @@ -373,7 +373,7 @@ EXTRACT="$TMPROOT/x4"; mkdir -p "$EXTRACT" tar xzf "$SANDBOX/dist/ninlight-0.9.9.tar.gz" -C "$EXTRACT" A4="$EXTRACT/ninlight-0.9.9" if [ ! -d "$A4/migrate" ]; then pass "migrate/ liegt nicht vor"; else fail "migrate/ trotzdem vorhanden"; fi -if [ -d "$A4/ota" ] && [ "$(find "$A4/ota" -type f | wc -l | tr -d ' ')" = "8" ]; then +if [ -d "$A4/ota" ] && [ "$(find "$A4/ota" -type f | wc -l | tr -d ' ')" = "${#TARGETS[@]}" ]; then pass "ota/ trotzdem vollstaendig" else fail "ota/ unvollstaendig"