Code :
#include <WiFi.h>
#include <WiFiUdp.h>
#include <WebServer.h>
#include <time.h>
const char* projectName = "KNX Stack";
const char* ssid = "WLAN SSID";
const char* password = "WLAN Passwort";
const char* ntpServer = "pool.ntp.org";
const char* tzInfo = "CET-1CEST,M3.5.0,M10.5.0/3";
const char* knxGatewayIp = "IP KNX Gateway";
const uint16_t knxPort = 3671;
const uint16_t localPort = 3671;
WiFiUDP udp;
WebServer server(80);
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;
void hexDump(const char* label, const uint8_t* buf, int len) {
Serial.printf("%s [%d Bytes]: ", label, len);
for (int i = 0; i < len; i++) {
Serial.printf("%02X ", buf[i]);
}
Serial.println();
}
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);
}
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) {
bool bitVal = data[0] & 0x01;
return prefix + "DPT1: " + (bitVal ? "EIN (1)" : "AUS (0)");
}
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[80];
snprintf(buf, sizeof(buf), "DPT10 Zeit : %02d:%02d:%02d / DPT11 Datum : %02d.%02d.20%02d",
h, mnt, s, day, month, year);
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++;
}
uint16_t processIncomingPacket(uint8_t* rxBuf, int len) {
if (len < 6) return 0;
uint16_t serviceType = (rxBuf[2] << 8) | rxBuf[3];
hexDump("[KNX RX]", rxBuf, len);
switch (serviceType) {
case 0x0421:
if (len >= 10) {
Serial.printf(" -> TUNNELING_ACK | Channel : %d | Seq : %d | Status : 0x%02X (%s)\n",
rxBuf[7], rxBuf[8], rxBuf[9], knxStatusToString(rxBuf[9]));
}
break;
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];
Serial.printf(" -> TUNNELING_REQUEST (Bus Telegramm) | Channel : %d | Seq : %d | GA : %s\n",
rxChannel, rxSeqNum, gaToString(ga).c_str());
Serial.println(" " + interpretKnxData(&rxBuf[20], npduLen));
if (rxChannel == knxChannelId) {
addKnxLogEntry(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();
hexDump("[KNX TX] ACK an Bus Telegramm", ackBuf, sizeof(ackBuf));
}
}
break;
case 0x0208:
if (len >= 8) {
Serial.printf(" -> CONNECTIONSTATE_RESPONSE | Status : 0x%02X (%s)\n", rxBuf[7], knxStatusToString(rxBuf[7]));
if (rxBuf[7] != 0x00) {
Serial.println(" !!! Heartbeat fehlgeschlagen -> Tunnel als getrennt markiert !!!");
isKnxConnected = false;
}
}
break;
case 0x0209:
if (len >= 8) {
uint8_t rxChannel = rxBuf[6];
Serial.printf(" -> DISCONNECT_REQUEST vom Gateway | Channel: %d\n", rxChannel);
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();
hexDump("[KNX TX] DISCONNECT_RESPONSE", discResp, sizeof(discResp));
isKnxConnected = false;
}
}
break;
default:
Serial.printf(" -> Unbehandelter ServiceType: 0x%04X\n", serviceType);
break;
}
return serviceType;
}
bool connectKnx() {
Serial.println("[KNX] Sende CONNECT_REQUEST an Gateway...");
IPAddress myIp = WiFi.localIP();
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
};
hexDump("[KNX TX] CONNECT_REQUEST", connReq, sizeof(connReq));
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));
hexDump("[KNX RX] CONNECT_RESPONSE", resp, rlen);
if (resp[2] == 0x02 && resp[3] == 0x06) {
if (resp[7] == 0x00) {
knxChannelId = resp[6];
txSeqNum = 0;
isKnxConnected = true;
lastHeartbeat = millis();
Serial.printf("ERFOLGREICH! Channel ID : %d\n", knxChannelId);
if (rlen >= 16 && resp[8] == 0x08) {
IPAddress dataIp(resp[10], resp[11], resp[12], resp[13]);
uint16_t dataPort = (resp[14] << 8) | resp[15];
Serial.printf(" [Info] Gateway Data-Endpoint laut CONNECT_RESPONSE: %s:%d\n",
dataIp.toString().c_str(), dataPort);
}
return true;
} else {
Serial.printf("FEHLER! Status Code: 0x%02X (%s)\n", resp[7], knxStatusToString(resp[7]));
return false;
}
}
}
delay(10);
}
Serial.println("TIMEOUT! Keine CONNECT_RESPONSE vom Gateway erhalten.");
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)
};
hexDump("[KNX TX] CONNECTIONSTATE_REQUEST (Heartbeat)", req, sizeof(req));
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)
};
hexDump("[KNX TX] DISCONNECT_REQUEST", req, sizeof(req));
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 == "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 {
Serial.println("[KNX TX] FEHLER - Unbekannter DPT Typ!");
return false;
}
buf[5] = payloadLen;
Serial.printf("[KNX TX] GA: %s (0x%04X) | DPT: %s | Val: %s | Seq: %d\n",
groupAddress.c_str(), ga, dptType.c_str(), rawValue.c_str(), txSeqNum);
hexDump("[KNX TX] TUNNELING_REQUEST", buf, 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) {
Serial.println("!!! KEIN passendes TUNNELING_ACK erhalten (Timeout nach 1000ms) !!!");
}
return ackOk;
}
String escapeJson(String s) {
String out;
for (size_t i = 0; i < s.length(); i++) {
char c = s[i];
if (c == '"' || c == '\\') out += '\\';
out += c;
}
return out;
}
void handleRoot() {
String html = "<html><head><meta charset='UTF-8'>";
html += "<title>" + String(projectName) + "</title>";
html += "<style>body{font-family:Arial,sans-serif;margin:20px;background:#f4f4f9;color:#333}";
html += ".card{background:#fff;padding:20px;border-radius:8px;box-shadow:0 2px 5px rgba(0,0,0,0.1);max-width:650px;margin:auto}";
html += "h1{text-align:center;margin-bottom:2px;}";
html += "#dtNow{text-align:center;color:#666;margin-top:0;margin-bottom:16px;font-size:14px;}";
html += "input,select,button{width:100%;padding:10px;margin:10px 0;box-sizing:border-box;border:1px solid #ccc;border-radius:4px;font-size:16px}";
html += "button{background:#007bff;color:#fff;font-weight:bold;border:none;cursor:pointer}button:hover{background:#0056b3}";
html += "#statusMsg{margin-top:10px;font-weight:bold;color:#28a745;text-align:center;}";
html += "table{width:100%;border-collapse:collapse;font-size:13px;margin-top:10px;}";
html += "th,td{border-bottom:1px solid #eee;padding:6px 4px;text-align:left;}";
html += "th{background:#f0f0f5;}";
html += "</style></head><body>";
html += "<div class='card'>";
html += "<h1>" + String(projectName) + "</h1>";
html += "<p id='dtNow'>...</p>";
html += "<p>KNX Status: <span id='knxStatus'>" + String(isKnxConnected ? "<b style='color:green;'>Verbunden (Channel " + String(knxChannelId) + ")" : "Getrennt") + "";
html += "<label>Gruppenadresse (z.B. 1/0/0):</label>";
html += "<input type='text' id='ga' value='1/0/0'>";
html += "<label>Datenpunkttyp (DPT):</label>";
html += "<select id='dpt'>";
html += "<option value='DPT1'>DPT 1 (1 Bit Schalten 0/1)</option>";
html += "<option value='DPT5'>DPT 5 (1 Byte Wert 0..255)</option>";
html += "<option value='DPT9'>DPT 9 (2 Byte Float Temp/Wert)</option>";
html += "<option value='DPT10'>DPT 10 (Zeit, z.B. 14:30:00)</option>";
html += "<option value='DPT11'>DPT 11 (Datum, z.B. 24.12.26)</option>";
html += "<option value='DPT14'>DPT 14 (4 Byte Float IEEE754)</option>";
html += "<option value='DPT16'>DPT 16 (14 Byte Text)</option>";
html += "</select>";
html += "<label>Wert / Payload:</label>";
html += "<input type='text' id='val' value='1' placeholder='z.B. 1, 128, 21.5, 14:30:00, 24.12.26 oder Text'>";
html += "<button type='button' onclick='sendTelegram()'>KNX Telegramm Senden</button>";
html += "<button type='button' onclick='reconnectKnx()' style='background:#6c757d;'>KNX Reconnect</button>";
html += "<div id='statusMsg'></div>";
html += "<h3>Letzte empfangene Telegramme</h3>";
html += "<table><thead><tr><th>Zeit</th><th>GA</th><th>Bytes</th><th>Hex</th><th>Wert</th></tr></thead>";
html += "<tbody id='logBody'>Lade...</td></tr></tbody></table>";
html += "</div>";
html += "<script>";
html += "function sendTelegram(){";
html += " var ga = document.getElementById('ga').value;";
html += " var dpt = document.getElementById('dpt').value;";
html += " var val = document.getElementById('val').value;";
html += " document.getElementById('statusMsg').innerText = 'Sende...';";
html += " fetch('/send?ga=' + encodeURIComponent(ga) + '&dpt=' + encodeURIComponent(dpt) + '&val=' + encodeURIComponent(val))";
html += " .then(response => response.text())";
html += " .then(data => { document.getElementById('statusMsg').innerText = data; })";
html += " .catch(err => { document.getElementById('statusMsg').innerText = 'Fehler beim Senden!'; });";
html += "}";
html += "function reconnectKnx(){";
html += " fetch('/reconnect').then(r=>r.text()).then(d=>{ updateStatus(); });";
html += "}";
html += "function updateStatus(){";
html += " fetch('/status').then(r=>r.json()).then(d=>{";
html += " document.getElementById('dtNow').innerText = d.datetime;";
html += " document.getElementById('knxStatus').innerHTML = d.connected ? (\"<b style='color:green;'>Verbunden (Channel \" + d.channel + \")</b>\") : \"<b style='color:red;'>Getrennt</b>\";";
html += " var rows = '';";
html += " d.log.forEach(function(e){ rows += '<tr><td>'+e.time+'</td><td>'+e.ga+'</td><td>'+e.len+'</td><td>'+e.hex+'</td><td>'+e.value+'</td></tr>'; });";
html += " document.getElementById('logBody').innerHTML = rows || '<tr><td colspan=5>Noch keine Telegramme empfangen</td></tr>';";
html += " }).catch(function(){});";
html += "}";
html += "setInterval(updateStatus, 1000);";
html += "updateStatus();";
html += "</script>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleSend() {
if (server.hasArg("ga") && server.hasArg("dpt") && server.hasArg("val")) {
String ga = server.arg("ga");
String dpt = server.arg("dpt");
String val = server.arg("val");
bool ok = sendKnxValue(ga, dpt, val);
server.send(200, "text/plain", ok ? "Telegramm gesendet und per ACK bestaetigt!" : "Gesendet, aber KEIN ACK erhalten, siehe Serial Monitor!");
} else {
server.send(400, "text/plain", "Fehlende Parameter!");
}
}
void handleReconnect() {
connectKnx();
server.send(200, "text/plain", "Reconnected!");
}
void handleStatus() {
String json = "{";
json += "\"project\":\"" + escapeJson(String(projectName)) + "\",";
json += "\"datetime\":\"" + escapeJson(getFormattedDateTime()) + "\",";
json += "\"connected\":" + String(isKnxConnected ? "true" : "false") + ",";
json += "\"channel\":" + String(knxChannelId) + ",";
json += "\"log\":[";
for (int i = 0; i < knxLogFilled; i++) {
int idx = (knxLogNext - 1 - i + KNX_LOG_SIZE) % KNX_LOG_SIZE;
KnxLogEntry &e = knxLog[idx];
String hex = "";
for (int b = 0; b < e.len; b++) {
char hb[4];
snprintf(hb, sizeof(hb), "%02X ", e.data[b]);
hex += hb;
}
hex.trim();
if (i > 0) json += ",";
json += "{";
json += "\"time\":\"" + escapeJson(String(e.timeStr)) + "\",";
json += "\"ga\":\"" + escapeJson(gaToString(e.ga)) + "\",";
json += "\"len\":" + String(e.len) + ",";
json += "\"hex\":\"" + escapeJson(hex) + "\",";
json += "\"value\":\"" + escapeJson(interpretKnxData(e.data, e.len)) + "\"";
json += "}";
}
json += "]}";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.printf("\n===> %s <===\n", projectName);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWLAN Verbunden!");
Serial.print("IP Adresse - "); Serial.println(WiFi.localIP());
Serial.print("RSSI: "); Serial.println(WiFi.RSSI());
configTzTime(tzInfo, ntpServer);
Serial.println("Warte auf Zeitsynchronisation (NTP)...");
struct tm timeinfo;
if (getLocalTime(&timeinfo, 5000)) {
Serial.println("Zeit synchronisiert - " + getFormattedDateTime());
} else {
Serial.println("Zeit konnte nicht synchronisiert werden (wird spaeter automatisch nachgeholt).");
}
udp.begin(localPort);
connectKnx();
server.on("/", handleRoot);
server.on("/send", handleSend);
server.on("/reconnect", handleReconnect);
server.on("/status", handleStatus);
server.begin();
}
void loop() {
server.handleClient();
handleKnxRx();
if (isKnxConnected && (millis() - lastHeartbeat > HEARTBEAT_INTERVAL)) {
sendConnectionStateRequest();
lastHeartbeat = millis();
}
}