Files
ninlight-w600/src/light/nl_dmx.c
T
Michael Sauer fbdf0c40ef feat(sacn): zweite Slot-Belegung "rgb" fuer pixelorientierte Werkzeuge
LedFx und Verwandte denken in RGB-Pixeln: Ihr e131-Geraet schreibt je Pixel
genau drei Kanaele. Unsere Belegung beginnt mit einem Master-Dimmer, also landet
deren erstes Pixel auf Dimmer, R, G - ein rotes Effektbild (255,0,0) hiesse bei
uns "Dimmer 255, Rot 0, Gruen 0" und damit Dunkelheit. Von Hand eintragen liess
sich eine Lampe schon, sie zeigte aber Unsinn.

Der Dimmer bleibt fuer DMX-Pulte richtig, deshalb eine zweite Belegung statt
einer anderen: layout = "dimmer" (Vorgabe, unveraendert) oder "rgb" (R,G,B, drei
Slots auf jedem Board). Im rgb-Layout ist eine Lampe genau ein Pixel.

Ein Board ohne Farbkanaele bekommt trotzdem ein Farbtripel. Genommen wird der
groesste der drei Werte auf beide Weisskanaele: die Lampe folgt sichtbar der
Helligkeit des Effekts, ohne dass ein Farbmodell in den DMX-Pfad kommt - genau
das wird dort sonst bewusst vermieden.

Die Obergrenze fuer start_slot haengt jetzt am Layout (512 - slots + 1). Ein
Wechsel, der einen bisher zulaessigen start_slot unzulaessig macht, wird
abgelehnt statt still verschoben: eine Lampe, die nach einer Einstellung
woanders im Universe liegt als angegeben, findet niemand wieder.

Das neue Settings-Feld beginnt bei Offset 216, dem sizeof davor, und ist durch
eine eigene Pruefung zur Uebersetzungszeit und einen Migrationstest abgesichert -
derselbe Fallstrick, der beim Schlussreview fuer sacn_enabled gefunden wurde.
2026-09-20 03:35:19 +02:00

156 lines
5.2 KiB
C

#include "light/nl_dmx.h"
#include <string.h>
#include "light/nl_engine.h"
static const nl_board_t *s_board;
static uint8_t s_slots; /* 3 oder 6 */
static nl_dmx_cfg_t s_cfg;
static bool s_streaming; /* es fliessen Pakete */
static bool s_override; /* die Engine haelt unsere Kanaele */
static uint8_t s_prio;
static uint8_t s_seq;
static uint32_t s_last_ms;
void nl_dmx_init(const nl_board_t *board)
{
s_board = board;
s_cfg.start_slot = 1;
s_cfg.hold = false;
s_cfg.release_fade_ms = 0;
s_cfg.rgb_layout = false;
s_slots = nl_board_dmx_slots(board, false);
s_streaming = false;
s_override = false;
s_prio = 0;
s_seq = 0;
s_last_ms = 0;
}
void nl_dmx_configure(const nl_dmx_cfg_t *cfg)
{
s_cfg = *cfg;
/* Sicherheitsnetz: start_slot=0 wuerde unten vor den Puffer lesen. Die REST-Schicht
* validiert 1..512, aber dieses Modul soll dafuer nicht auf einen Aufrufer angewiesen
* sein, den es nicht sehen kann. */
if (s_cfg.start_slot < 1u)
s_cfg.start_slot = 1u;
/* Die Slotzahl haengt am Layout, nicht nur am Board - hier nachziehen, sonst liest
* nl_dmx_feed() beim Umschalten die falsche Anzahl. */
s_slots = nl_board_dmx_slots(s_board, s_cfg.rgb_layout);
}
bool nl_dmx_active(void) { return s_override; }
void nl_dmx_release(void)
{
if (s_override)
nl_engine_override_clear(s_cfg.release_fade_ms);
s_override = false;
s_streaming = false;
s_prio = 0;
}
/* Der Strom ist zu Ende - entweder durch das terminated-Bit oder durch Timeout. Im
* "release"-Modus geben wir den Override frei und blenden zurueck zum Zustand von
* nl_ctrl; im "hold"-Modus bleiben die zuletzt gesetzten Kanalwerte einfach stehen. */
static void stream_ended(void)
{
s_streaming = false;
s_prio = 0;
if (!s_cfg.hold && s_override) {
nl_engine_override_clear(s_cfg.release_fade_ms);
s_override = false;
}
}
static uint8_t scale(uint8_t v, uint8_t dim)
{
return (uint8_t)(((uint16_t)v * (uint16_t)dim + 127u) / 255u);
}
void nl_dmx_feed(const nl_e131_packet_t *p, uint32_t now_ms)
{
const uint8_t *s;
uint8_t ch[NL_CH_COUNT];
uint8_t dim;
if (s_board == NULL)
return;
/* Reicht das Paket bis zu unserem letzten Slot? Ein halb belegter Satz ergaebe auf den
* restlichen Kanaelen willkuerliche Werte - also gar nicht erst anwenden. */
if ((uint32_t)s_cfg.start_slot + s_slots - 1u > (uint32_t)p->slot_count)
return;
if (s_streaming) {
if (p->priority < s_prio)
return;
if (p->priority == s_prio) {
/* E1.31-2018 Section 6.7.2: verwerfen, wenn die Differenz <= 0 und > -20 ist.
* Das Fenster ist bei -20 exklusiv - eine Sequenznummer genau 20 hinter der
* zuletzt angenommenen gilt bereits wieder als neu und wird angenommen. Nicht
* "korrigieren": ein Reviewer hat genau das schon einmal versucht. */
int8_t d = (int8_t)(p->sequence - s_seq);
if (d <= 0 && d > -NL_DMX_SEQ_WINDOW)
return; /* veraltet oder Wiederholung */
}
/* p->priority > s_prio: eine neue, hoeherpriore Quelle loest die laufende ab. Sie
* fuehrt ihre eigene Sequenzzaehlung - ein Abgleich gegen s_seq der abgeloesten
* Quelle waere Zufall und koennte die neue Quelle bis zu NL_DMX_SEQ_WINDOW Pakete
* lang verwerfen. Also: wie das erste Paket eines frischen Streams behandeln. */
}
s_prio = p->priority;
s_seq = p->sequence;
s_last_ms = now_ms;
if (p->terminated) { /* die Quelle sagt selbst, dass sie fertig ist */
stream_ended();
return;
}
s_streaming = true;
s = p->slots + (s_cfg.start_slot - 1u);
memset(ch, 0, sizeof ch);
if (s_cfg.rgb_layout) {
/* Drei Kanaele, kein Dimmer: so schreiben pixelorientierte Werkzeuge (LedFx und
* Verwandte) je Pixel genau R,G,B. Eine Lampe ist damit ein Pixel. */
if (s_board->has_rgb) {
ch[NL_CH_R] = s[0];
ch[NL_CH_G] = s[1];
ch[NL_CH_B] = s[2];
} else {
/* Ein Board ohne Farbkanaele bekommt trotzdem ein Farbtripel. Genommen wird der
* groesste der drei Werte auf beide Weisskanaele: die Lampe folgt damit sichtbar
* der Helligkeit des Effekts, ohne dass ein Farbmodell in den DMX-Pfad kommt -
* genau das wird hier sonst bewusst vermieden. */
uint8_t v = s[0] > s[1] ? s[0] : s[1];
if (s[2] > v)
v = s[2];
ch[NL_CH_CW] = v;
ch[NL_CH_WW] = v;
}
} else {
dim = s[0];
if (s_board->has_rgb) {
ch[NL_CH_R] = scale(s[1], dim);
ch[NL_CH_G] = scale(s[2], dim);
ch[NL_CH_B] = scale(s[3], dim);
ch[NL_CH_CW] = scale(s[4], dim);
ch[NL_CH_WW] = scale(s[5], dim);
} else {
ch[NL_CH_CW] = scale(s[1], dim);
ch[NL_CH_WW] = scale(s[2], dim);
}
}
nl_engine_override(ch);
s_override = true;
}
void nl_dmx_tick(uint32_t now_ms)
{
if (!s_streaming)
return;
if (now_ms - s_last_ms >= NL_DMX_TIMEOUT_MS)
stream_ended();
}