16/09/2026, 21:05:14
Salut,
j'ai testé le code KNX avec un bandeau LED RGB 24V. Le bandeau est branché en 24V et GND avec des MOSFETs et ça marche super bien, tout comme le contrôle depuis le serveur web.
En raison de la longueur du code, je le publie en deux parties.
j'ai testé le code KNX avec un bandeau LED RGB 24V. Le bandeau est branché en 24V et GND avec des MOSFETs et ça marche super bien, tout comme le contrôle depuis le serveur web.
En raison de la longueur du code, je le publie en deux parties.
Code :
#include <WiFi.h>
#include <WiFiUdp.h>
#include <WebServer.h>
#include <Preferences.h>
#include <ESPmDNS.h>
#include <time.h>
const char* projectName = "KNX RGB Controller";
const char* WIFI_SSID = "WLAN SSID";
const char* WIFI_PASSWORD = "WLAN PASSWORT";
const char* HOSTNAME = "led-dimmer";
const char* NTP_SERVER = "de.pool.ntp.org";
const char* TZ_INFO = "CET-1CEST,M3.5.0,M10.5.0/3";
const char* knxGatewayIp = "IP von KNX gateway";
const uint16_t knxPort = 3671;
const uint16_t localPort = 3671;
WiFiUDP udp;
WebServer server(80);
Preferences preferences;
uint8_t knxChannelId = 0;
uint8_t txSeqNum = 0;
bool isKnxConnected = false;
unsigned long lastHeartbeat = 0;
const unsigned long HEARTBEAT_INTERVAL = 60000;
const unsigned long ACK_TIMEOUT = 1000;
#define KNX_LOG_SIZE 10
struct KnxLogEntry {
char timeStr[24];
uint16_t ga;
uint8_t data[16];
uint8_t len;
};
KnxLogEntry knxLog[KNX_LOG_SIZE];
int knxLogNext = 0;
int knxLogFilled = 0;
const uint8_t PIN_RED = 4;
const uint8_t PIN_GREEN = 5;
const uint8_t PIN_BLUE = 6;
const uint32_t PWM_FREQUENCY = 4000;
const uint8_t PWM_RESOLUTION = 10;
const uint32_t PWM_MAX = (1UL << PWM_RESOLUTION) - 1;
bool ledOn = true; // Ruban LED marche/arrêt global
int targetR = 255, targetG = 255, targetB = 255;
int targetMaster = 100; // Luminosité principale en % (0-100)
int activeEffect = 0; // 0 = couleur manuelle, 1-24 = programmes
int effectSpeed = 50; // 1-100 (interne), l'adresse de groupe KNX utilise 0-255
int fxR = 255, fxG = 0, fxB = 0; // Couleur pour les programmes
float currentR = 0.0f, currentG = 0.0f, currentB = 0.0f;
float startR = 0.0f, startG = 0.0f, startB = 0.0f;
uint32_t fadeDuration = 300;
unsigned long fadeStartTime = 0;
bool isFading = false;
unsigned long lastEffectStep = 0;
int effectStep = 0;
bool effectState = false;
int colorTempPercent = 50; // 0-100% (Changement de température de couleur/blanc, terrasse)
int hueDegrees = 0; // 0-359° (Changement de teinte HSV, terrasse)
const uint8_t NUM_EFFECTS = 24; // Programmes 1-24 (0 = couleur manuelle)
const char* effectNames[NUM_EFFECTS + 1] = {
"Manuelle Farbe",
"Fade Pulse", "Fast Pulse", "Knight Rider", "Stroboskop",
"Blitz-Strobo", "Atmung", "Kerzenflackern", "Martinshorn",
"Gewitter-Blitze", "Feuerflackern", "Random Takt", "Smooth Waves",
"Dimmer Cycle", "Sparkle / Funken", "Dual Flash", "Herzschlag",
"Rainbow Loop", "Rainbow Chase", "Party Strobo", "Disko Random",
"Aurora Ozean", "Sunset Fire", "Neon Party", "Polizei Blau/Rot"
};
const char* gaLedOnOff = "1/0/1"; // DPT1 - 0=Éteint, 1=Allumé
const char* gaEffectStep = "3/0/0"; // DPT1 - 0=Programme précédent, 1=Programme suivant
const char* gaEffectSet = "3/0/2"; // DPT5 - Définir directement le programme (0-24)
const char* gaColorRGB = "3/0/3"; // DPT232.600 - Couleur RGB 3 octets
const char* gaBrightness = "3/0/4"; // DPT5 - Luminosité principale 0-255 (-> 0-100%)
const char* gaSpeed = "3/0/5"; // DPT5 - Vitesse de l'effet 0-255 (-> 1-100%)
const char* gaColorTempStep = "1/6/5"; // DPT3 (4 Bit) - Modifier la température de couleur/blanc relativement (Bouton)
const char* gaColorTempStatus = "10/0/27"; // DPT5.001 - État température de couleur, pourcentage 0-100%
const char* gaHueStep = "1/6/6"; // DPT3 (4 Bit) - Modifier la teinte HSV (H) relativement (Bouton)
const char* gaHueStatus = "10/0/28"; // DPT5.003 - État teinte (H), angle 0-360°
uint16_t gaLedOnOffParsed, gaEffectStepParsed, gaEffectSetParsed, gaColorRGBParsed, gaBrightnessParsed, gaSpeedParsed;
uint16_t gaColorTempStepParsed, gaHueStepParsed;
const char* knxStatusToString(uint8_t status) {
switch (status) {
case 0x00: return "E_NO_ERROR";
case 0x01: return "E_HOST_PROTOCOL_TYPE";
case 0x02: return "E_VERSION_NOT_SUPPORTED";
case 0x04: return "E_SEQUENCE_NUMBER (Sequenznummer falsch/ausser Sync)";
case 0x21: return "E_CONNECTION_ID (unbekannte Channel ID)";
case 0x22: return "E_CONNECTION_TYPE";
case 0x23: return "E_CONNECTION_OPTION";
case 0x24: return "E_NO_MORE_CONNECTIONS (Gateway voll)";
case 0x26: return "E_DATA_CONNECTION";
case 0x27: return "E_KNX_CONNECTION";
case 0x29: return "E_TUNNELING_LAYER (nicht unterstuetzt)";
default: return "Unbekannter Statuscode";
}
}
String getFormattedDateTime() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo, 100)) {
return "Zeit nicht synchronisiert";
}
char buf[32];
strftime(buf, sizeof(buf), "%d.%m.%Y %H:%M:%S", &timeinfo);
return String(buf);
}
unsigned long getEpochTime() {
time_t now;
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return 0;
time(&now);
return now;
}
uint16_t parseGroupAddress(const String& gaStr) {
int firstSlash = gaStr.indexOf('/');
int secondSlash = gaStr.indexOf('/', firstSlash + 1);
if (firstSlash == -1 || secondSlash == -1) return 0;
uint8_t main = gaStr.substring(0, firstSlash).toInt();
uint8_t middle = gaStr.substring(firstSlash + 1, secondSlash).toInt();
uint8_t sub = gaStr.substring(secondSlash + 1).toInt();
return ((main & 0x1F) << 11) | ((middle & 0x07) << 8) | (sub & 0xFF);
}
String gaToString(uint16_t ga) {
uint8_t main = (ga >> 11) & 0x1F;
uint8_t middle = (ga >> 8) & 0x07;
uint8_t sub = ga & 0xFF;
return String(main) + "/" + String(middle) + "/" + String(sub);
}
uint16_t floatToDpt9(float val) {
int sign = (val < 0) ? 1 : 0;
float v = (sign == 1) ? -val : val;
int exp = 0;
int mantissa = (int)(v * 100.0f);
while (mantissa > 2047) {
mantissa >>= 1;
exp++;
}
if (sign == 1) {
mantissa = -mantissa;
mantissa &= 0x07FF;
}
return (sign << 15) | ((exp & 0x0F) << 11) | (mantissa & 0x07FF);
}
float dpt9ToFloat(uint8_t hi, uint8_t lo) {
uint16_t raw = ((uint16_t)hi << 8) | lo;
int sign = (raw >> 15) & 0x01;
int exp = (raw >> 11) & 0x0F;
int mantissa11 = raw & 0x07FF;
int M = mantissa11 - (sign ? 2048 : 0);
return 0.01f * M * (float)(1 << exp);
}
void floatToDpt14Bytes(float val, uint8_t* out4) {
uint8_t* p = (uint8_t*)&val;
out4[0] = p[3];
out4[1] = p[2];
out4[2] = p[1];
out4[3] = p[0];
}
float dpt14ToFloat(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) {
uint8_t bytes[4] = { b3, b2, b1, b0 };
float f;
memcpy(&f, bytes, 4);
return f;
}
String apciTypeStr(uint8_t apciByte) {
switch (apciByte & 0xC0) {
case 0x00: return "Read";
case 0x40: return "Response";
case 0x80: return "Write";
default: return "?";
}
}
String interpretKnxData(uint8_t* data, uint8_t len) {
String prefix = "[" + apciTypeStr(data[0]) + "] ";
if (len == 1) {
uint8_t raw = data[0] & 0x0F;
return prefix + "1/2/4-Bit: 0x" + String(raw, HEX) + " (DPT1/2/3)";
}
else if (len == 2) {
return prefix + "DPT5: " + String(data[1]) + " (0..255)";
}
else if (len == 3) {
float f = dpt9ToFloat(data[1], data[2]);
return prefix + "DPT9: " + String(f, 2);
}
else if (len == 4) {
uint8_t h = data[1] & 0x1F, mnt = data[2] & 0x3F, s = data[3] & 0x3F;
uint8_t day = data[1] & 0x1F, month = data[2] & 0x0F, year = data[3] & 0x7F;
char buf[110];
snprintf(buf, sizeof(buf), "DPT10 Zeit: %02d:%02d:%02d / DPT11 Datum: %02d.%02d.20%02d / DPT232 RGB: #%02X%02X%02X",
h, mnt, s, day, month, year, data[1], data[2], data[3]);
return prefix + String(buf);
}
else if (len == 5) {
float f = dpt14ToFloat(data[1], data[2], data[3], data[4]);
return prefix + "DPT14: " + String(f, 3);
}
else if (len == 15) {
char txt[15];
for (int i = 0; i < 14; i++) {
uint8_t c = data[1 + i];
txt[i] = (c >= 32 && c < 127) ? (char)c : (c == 0 ? 0 : '.');
}
txt[14] = 0;
return prefix + "DPT16: \"" + String(txt) + "\"";
}
return prefix + "Unbekannt (" + String(len) + " Byte)";
}
void addKnxLogEntry(uint16_t ga, uint8_t* rxBuf, uint8_t npduLen) {
KnxLogEntry &e = knxLog[knxLogNext];
String t = getFormattedDateTime();
strncpy(e.timeStr, t.c_str(), sizeof(e.timeStr) - 1);
e.timeStr[sizeof(e.timeStr) - 1] = 0;
e.ga = ga;
uint8_t copyLen = npduLen;
if (copyLen > sizeof(e.data)) copyLen = sizeof(e.data);
memcpy(e.data, &rxBuf[20], copyLen);
e.len = copyLen;
knxLogNext = (knxLogNext + 1) % KNX_LOG_SIZE;
if (knxLogFilled < KNX_LOG_SIZE) knxLogFilled++;
}
void hsvToRgb(uint16_t h, uint8_t s, uint8_t v, int &r, int &g, int &b) {
unsigned char region, remainder, p, q, t;
if (s == 0) { r = g = b = v; return; }
region = h / 43;
remainder = (h - (region * 43)) * 6;
p = (v * (255 - s)) >> 8;
q = (v * (255 - ((s * remainder) >> 8))) >> 8;
t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8;
switch (region) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
default: r = v; g = p; b = q; break;
}
}
void saveRGB() {
preferences.putInt("r", targetR);
preferences.putInt("g", targetG);
preferences.putInt("b", targetB);
preferences.putInt("m", targetMaster);
preferences.putInt("speed", effectSpeed);
preferences.putInt("fx", activeEffect);
preferences.putInt("fxr", fxR);
preferences.putInt("fxg", fxG);
preferences.putInt("fxb", fxB);
preferences.putBool("on", ledOn);
preferences.putInt("ctemp", colorTempPercent);
preferences.putInt("hue", hueDegrees);
}
void loadSettings() {
targetR = preferences.getInt("r", 255);
targetG = preferences.getInt("g", 255);
targetB = preferences.getInt("b", 255);
targetMaster = preferences.getInt("m", 100);
effectSpeed = preferences.getInt("speed", 50);
activeEffect = preferences.getInt("fx", 0);
fxR = preferences.getInt("fxr", 255);
fxG = preferences.getInt("fxg", 0);
fxB = preferences.getInt("fxb", 0);
ledOn = preferences.getBool("on", true);
colorTempPercent = preferences.getInt("ctemp", 50);
hueDegrees = preferences.getInt("hue", 0);
currentR = (targetR * targetMaster) / 100.0f;
currentG = (targetG * targetMaster) / 100.0f;
currentB = (targetB * targetMaster) / 100.0f;
}
void writePWMHardware(float r, float g, float b) {
uint32_t dutyR = map((long)(r * 100), 0, 25500, 0, PWM_MAX);
uint32_t dutyG = map((long)(g * 100), 0, 25500, 0, PWM_MAX);
uint32_t dutyB = map((long)(b * 100), 0, 25500, 0, PWM_MAX);
ledcWrite(PIN_RED, dutyR);
ledcWrite(PIN_GREEN, dutyG);
ledcWrite(PIN_BLUE, dutyB);
}
void setDirectRGB(int r, int g, int b) {
float masterFactor = targetMaster / 100.0f;
currentR = constrain(r, 0, 255) * masterFactor;
currentG = constrain(g, 0, 255) * masterFactor;
currentB = constrain(b, 0, 255) * masterFactor;
writePWMHardware(currentR, currentG, currentB);
}
void startRGBFade(int r, int g, int b, int m, uint32_t duration = 300) {
targetR = constrain(r, 0, 255);
targetG = constrain(g, 0, 255);
targetB = constrain(b, 0, 255);
targetMaster = constrain(m, 0, 100);
startR = currentR;
startG = currentG;
startB = currentB;
fadeDuration = duration;
fadeStartTime = millis();
isFading = true;
}
void updateFade() {
if (!isFading) return;
unsigned long elapsed = millis() - fadeStartTime;
float masterFactor = targetMaster / 100.0f;
float finalTargetR = targetR * masterFactor;
float finalTargetG = targetG * masterFactor;
float finalTargetB = targetB * masterFactor;
if (elapsed >= fadeDuration) {
currentR = finalTargetR;
currentG = finalTargetG;
currentB = finalTargetB;
writePWMHardware(currentR, currentG, currentB);
isFading = false;
} else {
float progress = (float)elapsed / (float)fadeDuration;
currentR = startR + (finalTargetR - startR) * progress;
currentG = startG + (finalTargetG - startG) * progress;
currentB = startB + (finalTargetB - startB) * progress;
writePWMHardware(currentR, currentG, currentB);
}
}
void processEffects() {
if (activeEffect == 0) return;
unsigned long now = millis();
int interval = map(effectSpeed, 1, 100, 400, 5);
if (now - lastEffectStep < interval) return;
lastEffectStep = now;
int r = 0, g = 0, b = 0;
switch (activeEffect) {
case 1: // Fade Pulse
effectStep = (effectStep + 4) % 360;
{
float factor = (sin(effectStep * 0.0174533f) + 1.0f) / 2.0f;
setDirectRGB(fxR * factor, fxG * factor, fxB * factor);
}
break;
case 2: // Fast Pulse
effectStep = (effectStep + 15) % 360;
{
float factor = (sin(effectStep * 0.0174533f) + 1.0f) / 2.0f;
setDirectRGB(fxR * factor, fxG * factor, fxB * factor);
}
break;
case 3: // Knight Rider
effectStep = (effectStep + 1) % 20;
{
float val = (effectStep < 10) ? (effectStep / 10.0f) : ((20 - effectStep) / 10.0f);
setDirectRGB(fxR * val, fxG * val, fxB * val);
}
break;
case 4: // Stroboskop
effectState = !effectState;
if (effectState) { setDirectRGB(fxR, fxG, fxB); }
else { setDirectRGB(0, 0, 0); }
break;
case 5: // Blitz Strobo
effectStep = (effectStep + 1) % 4;
if (effectStep == 0) { setDirectRGB(fxR, fxG, fxB); }
else { setDirectRGB(0, 0, 0); }
break;
case 6: // Atmung
effectStep = (effectStep + 2) % 360;
{
float factor = (sin(effectStep * 0.0174533f) + 1.0f) / 2.0f;
setDirectRGB(fxR * factor, fxG * factor, fxB * factor);
}
break;
case 7: // Kerzenflackern
{
float factor = random(40, 100) / 100.0f;
setDirectRGB(fxR * factor, fxG * factor, fxB * factor);
}
break;
case 8: // Martinshorn (Signal)
effectStep = (effectStep + 1) % 4;
if (effectStep == 0 || effectStep == 1) setDirectRGB(fxR, fxG, fxB);
else setDirectRGB(fxR * 0.05, fxG * 0.05, fxB * 0.05);
break;
case 9: // Gewitter Blitze
if (random(0, 10) > 7) {
setDirectRGB(fxR, fxG, fxB);
} else {
setDirectRGB(fxR * 0.05, fxG * 0.05, fxB * 0.05);
}
break;
case 10: // Feuerflackern
{
float factor = random(20, 100) / 100.0f;
setDirectRGB(fxR * factor, fxG * factor, fxB * factor);
}
break;
case 11: // Random Takt
{
float factor = random(10, 100) / 100.0f;
setDirectRGB(fxR * factor, fxG * factor, fxB * factor);
}
break;
case 12: // Smooth Waves
effectStep = (effectStep + 2) % 360;
{
float factor = (sin(effectStep * 0.0174533f) + 1.0f) / 2.0f;
setDirectRGB(fxR * factor, fxG * factor, fxB * factor);
}
break;
case 13: // Dimmer Cycle
effectStep = (effectStep + 5) % 100;
{
float factor = effectStep / 100.0f;
setDirectRGB(fxR * factor, fxG * factor, fxB * factor);
}
break;
case 14: // Sparkle / Funken
if (random(0, 5) == 0) setDirectRGB(fxR, fxG, fxB);
else setDirectRGB(fxR * 0.1, fxG * 0.1, fxB * 0.1);
break;
case 15: // Dual Flash
effectStep = (effectStep + 1) % 6;
if (effectStep == 0 || effectStep == 2) setDirectRGB(fxR, fxG, fxB);
else setDirectRGB(0, 0, 0);
break;
case 16: // Herzschlag
effectStep = (effectStep + 1) % 8;
if (effectStep == 0 || effectStep == 2) setDirectRGB(fxR, fxG, fxB);
else setDirectRGB(fxR * 0.05, fxG * 0.05, fxB * 0.05);
break;
case 17: // Rainbow Loop
effectStep = (effectStep + 2) % 256;
hsvToRgb(effectStep, 255, 255, r, g, b);
setDirectRGB(r, g, b);
break;
case 18: // Rainbow Chase
effectStep = (effectStep + 8) % 256;
hsvToRgb(effectStep, 255, 255, r, g, b);
setDirectRGB(r, g, b);
break;
case 19: // Party Strobo
effectState = !effectState;
if (effectState) { setDirectRGB(random(0,256), random(0,256), random(0,256)); }
else { setDirectRGB(0, 0, 0); }
break;
case 20: // Disko Random
setDirectRGB(random(0,256), random(0,256), random(0,256));
break;
case 21: // Aurora Ozean
effectStep = (effectStep + 2) % 256;
r = 0;
g = map(sin(effectStep * 0.05), -1, 1, 100, 255);
b = map(cos(effectStep * 0.05), -1, 1, 150, 255);
setDirectRGB(r, g, b);
break;
case 22: // Sunset Fire
effectStep = (effectStep + 1) % 256;
hsvToRgb((effectStep % 45), 255, 255, r, g, b);
setDirectRGB(r, g, b);
break;
case 23: // Neon Party
effectStep = (effectStep + 1) % 3;
if (effectStep == 0) setDirectRGB(255, 0, 128);
else if (effectStep == 1) setDirectRGB(0, 255, 255);
else setDirectRGB(128, 0, 255);
break;
case 24: // Polizei Blau/Rot
effectStep = (effectStep + 1) % 6;
if (effectStep == 0 || effectStep == 1) setDirectRGB(0, 0, 255);
else if (effectStep == 3 || effectStep == 4) setDirectRGB(255, 0, 0);
else setDirectRGB(0, 0, 0);
break;
}
}
bool sendKnxValue(String groupAddress, String dptType, String rawValue);
void applyOnOff(bool on, bool fromKnx = false) {
ledOn = on;
if (on && activeEffect == 0) {
startRGBFade(targetR, targetG, targetB, targetMaster);
}
saveRGB();
if (!fromKnx) sendKnxValue(gaLedOnOff, "DPT1", on ? "1" : "0");
}
void applyEffectStep(bool up, bool fromKnx = false) {
activeEffect = (activeEffect + (up ? 1 : (int)NUM_EFFECTS)) % (NUM_EFFECTS + 1);
effectStep = 0; effectState = false; isFading = false;
saveRGB();
if (!fromKnx) sendKnxValue(gaEffectStep, "DPT1", up ? "1" : "0");
}
void applyEffectSet(int e, bool fromKnx = false) {
if (e < 0 || e > NUM_EFFECTS) return;
activeEffect = e;
effectStep = 0; effectState = false; isFading = false;
saveRGB();
if (!fromKnx) sendKnxValue(gaEffectSet, "DPT5", String(e));
}
void applyColor(int r, int g, int b, bool fromKnx = false) {
r = constrain(r, 0, 255); g = constrain(g, 0, 255); b = constrain(b, 0, 255);
fxR = r; fxG = g; fxB = b;
if (activeEffect == 0) {
startRGBFade(r, g, b, targetMaster);
}
saveRGB();
if (!fromKnx) sendKnxValue(gaColorRGB, "DPT232", String(r) + "," + String(g) + "," + String(b));
}
void applyMasterPercent(int percent, bool fromKnx = false) {
percent = constrain(percent, 0, 100);
targetMaster = percent;
if (activeEffect == 0 && !isFading) setDirectRGB(targetR, targetG, targetB);
saveRGB();
if (!fromKnx) sendKnxValue(gaBrightness, "DPT5", String(map(percent, 0, 100, 0, 255)));
}
void applySpeedPercent(int percent, bool fromKnx = false) {
percent = constrain(percent, 1, 100);
effectSpeed = percent;
saveRGB();
if (!fromKnx) sendKnxValue(gaSpeed, "DPT5", String(map(percent, 1, 100, 0, 255)));
}
bool colorTempDimActive = false;
bool colorTempDimUp = false;
unsigned long colorTempLastStep = 0;
unsigned long colorTempLastKnxSend = 0;
const unsigned long COLORTEMP_STEP_INTERVAL_MS = 40;
const unsigned long DIM_STATUS_INTERVAL_MS = 300;
bool hueDimActive = false;
bool hueDimUp = false;
unsigned long hueLastStep = 0;
unsigned long hueLastKnxSend = 0;
const unsigned long HUE_STEP_INTERVAL_MS = 15;
void updateColorTempColor() {
float t = colorTempPercent / 100.0f;
int r = round(255 + (200 - 255) * t);
int g = round(150 + (220 - 150) * t);
int b = round(60 + (255 - 60) * t);
fxR = r; fxG = g; fxB = b;
if (activeEffect == 0) {
targetR = r; targetG = g; targetB = b;
startRGBFade(r, g, b, targetMaster, 80);
}
}
void sendColorTempStatus() {
saveRGB();
sendKnxValue(gaColorTempStatus, "DPT5", String(map(colorTempPercent, 0, 100, 0, 255)));
}
void updateHueColor() {
uint8_t hue8 = (uint8_t)round(hueDegrees / 360.0f * 255.0f);
int r, g, b;
hsvToRgb(hue8, 255, 255, r, g, b);
fxR = r; fxG = g; fxB = b;
if (activeEffect == 0) {
targetR = r; targetG = g; targetB = b;
startRGBFade(r, g, b, targetMaster, 80);
}
}
void sendHueStatus() {
saveRGB();
uint8_t hue8 = (uint8_t)round(hueDegrees / 360.0f * 255.0f);
sendKnxValue(gaHueStatus, "DPT5", String(hue8));
}
void startColorTempDim(bool up) {
colorTempDimActive = true;
colorTempDimUp = up;
colorTempLastStep = millis();
colorTempLastKnxSend = millis();
}
void stopColorTempDim() {
if (!colorTempDimActive) return;
colorTempDimActive = false;
sendColorTempStatus();
}
void startHueDim(bool up) {
hueDimActive = true;
hueDimUp = up;
hueLastStep = millis();
hueLastKnxSend = millis();
}
void stopHueDim() {
if (!hueDimActive) return;
hueDimActive = false;
sendHueStatus();
}
void processColorTempDim() {
if (!colorTempDimActive) return;
unsigned long now = millis();
if (now - colorTempLastStep < COLORTEMP_STEP_INTERVAL_MS) return;
colorTempLastStep = now;
int next = colorTempPercent + (colorTempDimUp ? 1 : -1);
bool atLimit = false;
if (next <= 0) { next = 0; atLimit = true; }
if (next >= 100) { next = 100; atLimit = true; }
colorTempPercent = next;
updateColorTempColor();
if (atLimit) {
colorTempDimActive = false;
sendColorTempStatus();
} else if (now - colorTempLastKnxSend >= DIM_STATUS_INTERVAL_MS) {
sendColorTempStatus();
colorTempLastKnxSend = now;
}
}
void processHueDim() {
if (!hueDimActive) return;
unsigned long now = millis();
if (now - hueLastStep < HUE_STEP_INTERVAL_MS) return;
hueLastStep = now;
hueDegrees = (hueDegrees + (hueDimUp ? 1 : -1) + 360) % 360;
updateHueColor();
if (now - hueLastKnxSend >= DIM_STATUS_INTERVAL_MS) {
sendHueStatus();
hueLastKnxSend = now;
}
}
void applyKnxTelegram(uint16_t ga, uint8_t* rxBuf, uint8_t npduLen) {
if (ga == gaLedOnOffParsed && npduLen == 1) {
applyOnOff(rxBuf[20] & 0x01, true);
}
else if (ga == gaEffectStepParsed && npduLen == 1) {
applyEffectStep(rxBuf[20] & 0x01, true);
}
else if (ga == gaEffectSetParsed && npduLen == 2) {
applyEffectSet(rxBuf[21], true);
}
else if (ga == gaColorRGBParsed && npduLen == 4) {
applyColor(rxBuf[21], rxBuf[22], rxBuf[23], true);
}
else if (ga == gaBrightnessParsed && npduLen == 2) {
applyMasterPercent(map(rxBuf[21], 0, 255, 0, 100), true);
}
else if (ga == gaSpeedParsed && npduLen == 2) {
applySpeedPercent(map(rxBuf[21], 0, 255, 1, 100), true);
}
else if (ga == gaColorTempStepParsed && npduLen == 1) {
uint8_t raw = rxBuf[20] & 0x0F;
uint8_t stepCode = raw & 0x07;
if (stepCode == 0) stopColorTempDim();
else startColorTempDim((raw & 0x08) != 0);
}
else if (ga == gaHueStepParsed && npduLen == 1) {
uint8_t raw = rxBuf[20] & 0x0F;
uint8_t stepCode = raw & 0x07;
if (stepCode == 0) stopHueDim();
else startHueDim((raw & 0x08) != 0);
}
}
uint16_t processIncomingPacket(uint8_t* rxBuf, int len) {
if (len < 6) return 0;
uint16_t serviceType = (rxBuf[2] << 8) | rxBuf[3];
switch (serviceType) {
case 0x0420:
if (len >= 21) {
uint8_t rxChannel = rxBuf[7];
uint8_t rxSeqNum = rxBuf[8];
uint16_t ga = ((uint16_t)rxBuf[16] << 8) | rxBuf[17];
uint8_t npduLen = rxBuf[18];
if (rxChannel == knxChannelId) {
addKnxLogEntry(ga, rxBuf, npduLen);
applyKnxTelegram(ga, rxBuf, npduLen);
uint8_t ackBuf[10] = {
0x06, 0x10, 0x04, 0x21, 0x00, 0x0A, 0x04, knxChannelId, rxSeqNum, 0x00
};
udp.beginPacket(knxGatewayIp, knxPort);
udp.write(ackBuf, sizeof(ackBuf));
udp.endPacket();
}
}
break;
case 0x0208:
if (len >= 8 && rxBuf[7] != 0x00) {
Serial.println("[KNX] Heartbeat fehlgeschlagen -> Tunnel getrennt");
isKnxConnected = false;
}
break;
case 0x0209:
if (len >= 8) {
uint8_t rxChannel = rxBuf[6];
if (rxChannel == knxChannelId) {
uint8_t discResp[8] = { 0x06, 0x10, 0x02, 0x0A, 0x00, 0x08, knxChannelId, 0x00 };
udp.beginPacket(knxGatewayIp, knxPort);
udp.write(discResp, sizeof(discResp));
udp.endPacket();
Serial.println("[KNX] Vom Gateway getrennt");
isKnxConnected = false;
}
}
break;
default:
break;
}
return serviceType;
}
bool connectKnx(bool verbose = false) {
IPAddress myIp = WiFi.localIP();
if (verbose) {
Serial.println("[KNX] Sende CONNECT_REQUEST an Gateway...");
Serial.printf(" Lokale IP/Port die wir dem Gateway melden: %s:%d\n", myIp.toString().c_str(), localPort);
Serial.printf(" Ziel-Gateway: %s:%d\n", knxGatewayIp, knxPort);
}
uint8_t connReq[] = {
0x06, 0x10, 0x02, 0x05, 0x00, 0x1A,
0x08, 0x01, myIp[0], myIp[1], myIp[2], myIp[3], (uint8_t)(localPort >> 8), (uint8_t)(localPort & 0xFF),
0x08, 0x01, myIp[0], myIp[1], myIp[2], myIp[3], (uint8_t)(localPort >> 8), (uint8_t)(localPort & 0xFF),
0x04, 0x04, 0x02, 0x00
};
udp.beginPacket(knxGatewayIp, knxPort);
udp.write(connReq, sizeof(connReq));
udp.endPacket();
unsigned long start = millis();
while (millis() - start < 2000) {
int size = udp.parsePacket();
if (size >= 8) {
uint8_t resp[32];
int rlen = udp.read(resp, sizeof(resp));
if (resp[2] == 0x02 && resp[3] == 0x06) {
if (resp[7] == 0x00) {
knxChannelId = resp[6];
txSeqNum = 0;
isKnxConnected = true;
lastHeartbeat = millis();
if (verbose) Serial.printf("[KNX] Verbunden (Channel %d)\n", knxChannelId);
return true;
} else {
if (verbose) Serial.printf("[KNX] Verbindung fehlgeschlagen: %s\n", knxStatusToString(resp[7]));
return false;
}
}
}
delay(10);
}
if (verbose) Serial.println("[KNX] Timeout beim Verbindungsaufbau");
isKnxConnected = false;
return false;
}
void sendConnectionStateRequest() {
IPAddress myIp = WiFi.localIP();
uint8_t req[16] = {
0x06, 0x10, 0x02, 0x07, 0x00, 0x10,
knxChannelId, 0x00,
0x08, 0x01, myIp[0], myIp[1], myIp[2], myIp[3], (uint8_t)(localPort >> 8), (uint8_t)(localPort & 0xFF)
};
udp.beginPacket(knxGatewayIp, knxPort);
udp.write(req, sizeof(req));
udp.endPacket();
}
void disconnectKnx() {
if (!isKnxConnected) return;
IPAddress myIp = WiFi.localIP();
uint8_t req[16] = {
0x06, 0x10, 0x02, 0x09, 0x00, 0x10,
knxChannelId, 0x00,
0x08, 0x01, myIp[0], myIp[1], myIp[2], myIp[3], (uint8_t)(localPort >> 8), (uint8_t)(localPort & 0xFF)
};
udp.beginPacket(knxGatewayIp, knxPort);
udp.write(req, sizeof(req));
udp.endPacket();
isKnxConnected = false;
}
void handleKnxRx() {
int packetSize = udp.parsePacket();
if (packetSize <= 0) return;
uint8_t rxBuf[64];
int len = udp.read(rxBuf, sizeof(rxBuf));
processIncomingPacket(rxBuf, len);
}
bool sendKnxValue(String groupAddress, String dptType, String rawValue) {
if (!isKnxConnected) {
if (!connectKnx()) return false;
}
uint16_t ga = parseGroupAddress(groupAddress);
uint8_t buf[64];
uint8_t payloadLen = 0;
buf[0] = 0x06; buf[1] = 0x10;
buf[2] = 0x04; buf[3] = 0x20;
buf[4] = 0x00; buf[5] = 0x00;
buf[6] = 0x04;
buf[7] = knxChannelId;
buf[8] = txSeqNum;
buf[9] = 0x00;
buf[10] = 0x11;
buf[11] = 0x00;
buf[12] = 0xBC;
buf[13] = 0xE0;
buf[14] = 0x00; buf[15] = 0x00;
buf[16] = (uint8_t)(ga >> 8);
buf[17] = (uint8_t)(ga & 0xFF);
if (dptType == "DPT1") {
buf[18] = 0x01;
buf[19] = 0x00;
buf[20] = (rawValue.toInt() > 0) ? 0x81 : 0x80;
payloadLen = 21;
}
else if (dptType == "DPT2") {
buf[18] = 0x01;
buf[19] = 0x00;
buf[20] = 0x80 | ((uint8_t)rawValue.toInt() & 0x03);
payloadLen = 21;
}
else if (dptType == "DPT3") {
int v = rawValue.toInt();
uint8_t dir = (v >= 0) ? 0x08 : 0x00;
uint8_t step = (uint8_t)abs(v) & 0x07;
buf[18] = 0x01;
buf[19] = 0x00;
buf[20] = 0x80 | dir | step;
payloadLen = 21;
}
else if (dptType == "DPT5") {
buf[18] = 0x02;
buf[19] = 0x00;
buf[20] = 0x80;
buf[21] = (uint8_t)rawValue.toInt();
payloadLen = 22;
}
else if (dptType == "DPT9") {
uint16_t dpt9Val = floatToDpt9(rawValue.toFloat());
buf[18] = 0x03;
buf[19] = 0x00;
buf[20] = 0x80;
buf[21] = (uint8_t)(dpt9Val >> 8);
buf[22] = (uint8_t)(dpt9Val & 0xFF);
payloadLen = 23;
}
else if (dptType == "DPT10") {
int h = 0, m = 0, s = 0;
int c1 = rawValue.indexOf(':');
if (c1 == -1) {
h = rawValue.toInt();
} else {
h = rawValue.substring(0, c1).toInt();
int c2 = rawValue.indexOf(':', c1 + 1);
if (c2 == -1) {
m = rawValue.substring(c1 + 1).toInt();
} else {
m = rawValue.substring(c1 + 1, c2).toInt();
s = rawValue.substring(c2 + 1).toInt();
}
}
buf[18] = 0x04;
buf[19] = 0x00;
buf[20] = 0x80;
buf[21] = (uint8_t)(h & 0x1F);
buf[22] = (uint8_t)(m & 0x3F);
buf[23] = (uint8_t)(s & 0x3F);
payloadLen = 24;
}
else if (dptType == "DPT11") {
int day = 1, month = 1, year = 26;
int p1 = rawValue.indexOf('.');
if (p1 != -1) {
day = rawValue.substring(0, p1).toInt();
int p2 = rawValue.indexOf('.', p1 + 1);
if (p2 != -1) {
month = rawValue.substring(p1 + 1, p2).toInt();
int y = rawValue.substring(p2 + 1).toInt();
year = (y >= 2000) ? (y - 2000) : y;
}
} else {
day = rawValue.toInt();
}
buf[18] = 0x04;
buf[19] = 0x00;
buf[20] = 0x80;
buf[21] = (uint8_t)(day & 0x1F);
buf[22] = (uint8_t)(month & 0x0F);
buf[23] = (uint8_t)(year & 0x7F);
payloadLen = 24;
}
else if (dptType == "DPT14") {
uint8_t db[4];
floatToDpt14Bytes(rawValue.toFloat(), db);
buf[18] = 0x05;
buf[19] = 0x00;
buf[20] = 0x80;
buf[21] = db[0];
buf[22] = db[1];
buf[23] = db[2];
buf[24] = db[3];
payloadLen = 25;
}
else if (dptType == "DPT16") {
uint8_t strLen = rawValue.length();
if (strLen > 14) strLen = 14;
buf[18] = 15;
buf[19] = 0x00;
buf[20] = 0x80;
for (int i = 0; i < 14; i++) {
buf[21 + i] = (i < strLen) ? rawValue[i] : 0x00;
}
payloadLen = 21 + 14;
}
else if (dptType == "DPT232") {
int c1 = rawValue.indexOf(',');
int c2 = (c1 == -1) ? -1 : rawValue.indexOf(',', c1 + 1);
int r = 0, g = 0, b = 0;
if (c1 != -1 && c2 != -1) {
r = rawValue.substring(0, c1).toInt();
g = rawValue.substring(c1 + 1, c2).toInt();
b = rawValue.substring(c2 + 1).toInt();
}
buf[18] = 0x04;
buf[19] = 0x00;
buf[20] = 0x80;
buf[21] = (uint8_t)r;
buf[22] = (uint8_t)g;
buf[23] = (uint8_t)b;
payloadLen = 24;
}
else {
Serial.println("[KNX TX] FEHLER - Unbekannter DPT-Typ!");
return false;
}
buf[5] = payloadLen;
uint8_t sentSeq = txSeqNum;
udp.beginPacket(knxGatewayIp, knxPort);
udp.write(buf, payloadLen);
udp.endPacket();
txSeqNum++;
unsigned long waitStart = millis();
bool ackOk = false;
while (millis() - waitStart < ACK_TIMEOUT) {
int packetSize = udp.parsePacket();
if (packetSize > 0) {
uint8_t rxBuf[64];
int len = udp.read(rxBuf, sizeof(rxBuf));
uint16_t st = processIncomingPacket(rxBuf, len);
if (st == 0x0421 && len >= 9 && rxBuf[8] == sentSeq) {
ackOk = (len >= 10 && rxBuf[9] == 0x00);
break;
}
}
delay(5);
}
if (!ackOk) isKnxConnected = false;
return ackOk;
}
bool sendKnxRead(String groupAddress) {
if (!isKnxConnected) { if (!connectKnx()) return false; }
uint16_t ga = parseGroupAddress(groupAddress);
uint8_t buf[21];
buf[0] = 0x06; buf[1] = 0x10; buf[2] = 0x04; buf[3] = 0x20; buf[4] = 0x00; buf[5] = 21;
buf[6] = 0x04; buf[7] = knxChannelId; buf[8] = txSeqNum; buf[9] = 0x00;
buf[10] = 0x11; buf[11] = 0x00; buf[12] = 0xBC; buf[13] = 0xE0;
buf[14] = 0x00; buf[15] = 0x00;
buf[16] = (uint8_t)(ga >> 8);
buf[17] = (uint8_t)(ga & 0xFF);
buf[18] = 0x01; buf[19] = 0x00; buf[20] = 0x00;
uint8_t sentSeq = txSeqNum;
udp.beginPacket(knxGatewayIp, knxPort);
udp.write(buf, sizeof(buf));
udp.endPacket();
txSeqNum++;
unsigned long waitStart = millis();
bool ackOk = false;
while (millis() - waitStart < ACK_TIMEOUT) {
int packetSize = udp.parsePacket();
if (packetSize > 0) {
uint8_t rxBuf[64];
int len = udp.read(rxBuf, sizeof(rxBuf));
uint16_t st = processIncomingPacket(rxBuf, len);
if (st == 0x0421 && len >= 10 && rxBuf[8] == sentSeq) { ackOk = (rxBuf[9] == 0x00); break; }
}
delay(5);
}
if (!ackOk) isKnxConnected = false;
return ackOk;
}
void handleSerialCommands() {
if (!Serial.available()) return;
String line = Serial.readStringUntil('\n');
line.trim();
if (line.length() == 0) return;
int sp1 = line.indexOf(' ');
String cmd = (sp1 == -1) ? line : line.substring(0, sp1);
String rest = (sp1 == -1) ? "" : line.substring(sp1 + 1);
cmd.toLowerCase();
rest.trim();
if (cmd == "senddpt") {
int p1 = rest.indexOf(' ');
int p2 = (p1 == -1) ? -1 : rest.indexOf(' ', p1 + 1);
if (p1 == -1 || p2 == -1) { Serial.println("Syntax - senddpt <ga> <dpt> <wert>"); return; }
String ga = rest.substring(0, p1);
String dpt = rest.substring(p1 + 1, p2);
String val = rest.substring(p2 + 1);
dpt.toUpperCase();
bool ok = sendKnxValue(ga, dpt, val);
Serial.println(ok ? "OK" : "FEHLER (kein ACK)");
}
else if (cmd == "readvalue") {
if (rest.length() == 0) { Serial.println("Syntax - readvalue <ga>"); return; }
uint16_t targetGa = parseGroupAddress(rest);
bool ok = sendKnxRead(rest);
if (!ok) { Serial.println("FEHLER (kein ACK)"); return; }
unsigned long waitStart = millis();
bool gotValue = false;
while (millis() - waitStart < 1000) {
int packetSize = udp.parsePacket();
if (packetSize > 0) {
uint8_t rxBuf[64];
int len = udp.read(rxBuf, sizeof(rxBuf));
uint16_t st = processIncomingPacket(rxBuf, len);
if (st == 0x0420 && len >= 21) {
uint16_t rxGa = ((uint16_t)rxBuf[16] << 8) | rxBuf[17];
uint8_t apciType = rxBuf[20] & 0xC0;
if (rxGa == targetGa && apciType == 0x40) {
Serial.println("OK - " + interpretKnxData(&rxBuf[20], rxBuf[18]));
gotValue = true;
break;
}
}
}
delay(5);
}
if (!gotValue) Serial.println("OK (gesendet), aber keine Antwort innerhalb 1s empfangen");
}
else if (cmd == "led") {
int p1 = rest.indexOf(' ');
String sub = (p1 == -1) ? rest : rest.substring(0, p1);
String arg = (p1 == -1) ? "" : rest.substring(p1 + 1);
sub.toLowerCase();
arg.trim();
if (sub == "on") { applyOnOff(true); Serial.println("LED: AN"); }
else if (sub == "off") { applyOnOff(false); Serial.println("LED: AUS"); }
else if (sub == "next") { applyEffectStep(true); Serial.println("Programm: " + String(effectNames[activeEffect])); }
else if (sub == "prev") { applyEffectStep(false); Serial.println("Programm: " + String(effectNames[activeEffect])); }
else if (sub == "effekt" || sub == "effect") {
int e = arg.toInt();
if (arg.length() == 0 || e < 0 || e > (int)NUM_EFFECTS) { Serial.println("Syntax - led effekt <0-" + String(NUM_EFFECTS) + ">"); return; }
applyEffectSet(e);
Serial.println("Programm: " + String(effectNames[activeEffect]));
}
else if (sub == "farbe" || sub == "color") {
int c1 = arg.indexOf(','), c2 = (c1 == -1) ? -1 : arg.indexOf(',', c1 + 1);
if (c1 == -1 || c2 == -1) { Serial.println("Syntax - led farbe <r,g,b> z.B. led farbe 255,128,0"); return; }
applyColor(arg.substring(0, c1).toInt(), arg.substring(c1 + 1, c2).toInt(), arg.substring(c2 + 1).toInt());
Serial.printf("Farbe: #%02X%02X%02X\n", fxR, fxG, fxB);
}
else if (sub == "helligkeit" || sub == "bright") {
if (arg.length() == 0) { Serial.println("Syntax - led helligkeit <0-100>"); return; }
applyMasterPercent(arg.toInt());
Serial.println("Master Helligkeit - " + String(targetMaster) + "%");
}
else if (sub == "speed") {
if (arg.length() == 0) { Serial.println("Syntax: led speed <1-100>"); return; }
applySpeedPercent(arg.toInt());
Serial.println("Effekt Geschwindigkeit: " + String(effectSpeed) + "%");
}
else {
Serial.println("Unbekannter led Befehl - 'help' fuer Hilfe");
}
}
else if (cmd == "help" || cmd == "?") {
Serial.println("Befehle:");
Serial.println(" senddpt <ga> <dpt> <wert> z.B. senddpt 1/1/1 DPT1 1");
Serial.println(" readvalue <ga> z.B. readvalue 1/1/1");
Serial.println(" led on / led off Controller an/aus");
Serial.println(" led next / led prev naechstes/voriges Programm");
Serial.println(" led effekt <0-" + String(NUM_EFFECTS) + "> Programm direkt waehlen (0=manuelle Farbe)");
Serial.println(" led farbe <r,g,b> z.B. led farbe 255,128,0");
Serial.println(" led helligkeit <0-100>");
Serial.println(" led speed <1-100>");
Serial.println(" DPTs: DPT1,DPT2,DPT3,DPT5,DPT9,DPT10,DPT11,DPT14,DPT16,DPT232");
}
else {
Serial.println("Unbekannter Befehl - 'help' fuer Hilfe");
}
}
