Hier, 18:42:23
Bonjour à tous,
Je souhaite mettre à votre disposition ce code qui permet de communiquer avec KNX sur un ESP32 sans aucune bibliothèque supplémentaire. Il peut aussi bien envoyer que recevoir les télégrammes les plus courants.
Je souhaite mettre à votre disposition ce code qui permet de communiquer avec KNX sur un ESP32 sans aucune bibliothèque supplémentaire. Il peut aussi bien envoyer que recevoir les télégrammes les plus courants.
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();
}
}

