#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "esp32-hal-rmt.h"
#include
// =====================================================
// WLAN / MQTT
// =====================================================
const char* WIFI_SSID = "Hackerspace";
const char* WIFI_PASSWORD = "****";
const char* MQTT_HOST = "daniel-stingl.de";
const int MQTT_PORT = ****;
const char* MQTT_USER = "***";
const char* MQTT_PASS = "***";
const char* MQTT_TOPIC_TUER = "/hshb/status/klingel/tuer";
const char* MQTT_TOPIC_TOR = "/hshb/status/klingel/tor";
const char* MQTT_TOPIC_STATUS = "/hshb/status/klingel/status";
const char* MQTT_TOPIC_TUER_TEST = "/hshb/status/klingel/tuertest";
const char* MQTT_TOPIC_TOR_TEST = "/hshb/status/klingel/tortest";
// =====================================================
// OTA / Web
// =====================================================
const char* OTA_HOSTNAME = "hshb-klingel";
const char* OTA_USER = "****";
const char* OTA_PASS = "****";
// =====================================================
// NTP
// =====================================================
const char* NTP_SERVER = "pool.ntp.org";
const long GMT_OFFSET_SEC = 3600;
const int DAYLIGHT_OFFSET_SEC = 3600;
// =====================================================
// MAC
// =====================================================
uint8_t newMACAddress[] = {0x30, 0xAF, 0xA0, 0x05, 0x1D, 0x63};
// =====================================================
// Pins
// =====================================================
constexpr int bellPin = 5; // Trigger-Ausgang
constexpr int busPin = 32; // RMT RX
const int adcTorPin = 33; // ADC
const int testBellPin = 26; // Taster
// =====================================================
// RMT / Nachrichten
// =====================================================
constexpr uint8_t hackerspaceBell[] = {0x25, 0x2E, 0xC4, 0x00, 0xE8};
constexpr uint8_t hackerspaceBell2[] = {0x25, 0x2E, 0xE2, 0x00, 0xCA};
constexpr size_t bufferSize = 16;
constexpr size_t maxSyms = RMT_MEM_NUM_BLOCKS_2 * RMT_SYMBOLS_PER_CHANNEL_BLOCK;
rmt_data_t symbols[maxSyms];
size_t symbolCount = maxSyms;
enum { P_BAD = -1, P_ZERO = 0, P_ONE = 1, P_START = 2, P_STOP = 3 };
// =====================================================
// Zeiten / Zustände
// =====================================================
unsigned long lastBellTriggered = 0;
unsigned long lastTorTriggerMillis = 0;
unsigned long lastButtonTriggerMillis = 0;
unsigned long lastDebugMillis = 0;
unsigned long lastStatusMillis = 0;
unsigned long bellPulseStartMillis = 0;
unsigned long lastWifiReconnectMs = 0;
unsigned long lastMqttReconnectMs = 0;
unsigned long lastNtpSyncMs = 0;
const unsigned long BellDebounceMs = 300;
const unsigned long TorDebounceMs = 11000;
const unsigned long ButtonDebounceMs = 300;
const unsigned long Debuginterval = 1000;
const unsigned long StatusMs = 5000;
const unsigned long BellPulseMs = 10;
const unsigned long WifiRetryMs = 10000;
const unsigned long MqttRetryMs = 5000;
const unsigned long NtpResyncMs = 6UL * 60UL * 60UL * 1000UL;
const unsigned long LedEventDurationMs = 60000; // 60 Sekunden
const int adcTorThreshold = 1500;
bool bellPulseActive = false;
bool timeValid = false;
bool lastButtonState = HIGH;
bool lastTorState = false;
String lastState = "IDLE";
String lastSource = "-";
String lastDecodedHex = "-";
uint32_t lastDecodedLen = 0;
int adcTorValue = 0;
// =========================
// LEDs
// =========================
const int ledTuerPin = 23;
const int ledTorPin = 19;
const int ledFreq = 5000;
const int ledResolution = 8;
enum LedMode {
LED_MODE_STANDBY,
LED_MODE_SHOW_TUER,
LED_MODE_SHOW_TOR
};
LedMode ledMode = LED_MODE_STANDBY;
unsigned long ledModeUntilMs = 0;
unsigned long lastLedUpdateMs = 0;
unsigned long lastBlinkToggleMs = 0;
bool ledBlinkState = false;
// =====================================================
// Objekte
// =====================================================
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
Preferences preferences;
WebServer httpServer(80);
HTTPUpdateServer httpUpdater;
// =====================================================
// Vorwärtsdeklarationen
// =====================================================
bool connectWiFi();
bool ensureMqttConnected();
bool syncTime();
bool ensureTimeValid();
bool publishUnixTimeToTopic(const char* topic);
bool publishStatus();
void markMqttPendingTuer();
void markMqttPendingTor();
bool isMqttPendingTuer();
bool isMqttPendingTor();
void clearMqttPendingTuer();
void clearMqttPendingTor();
void processPendingMqtt();
void setupHttpRoutes();
void setupArduinoOta();
void triggerBell(bool fromButton);
void triggerTorEvent(const char* source);
void mqttCallback(char* topic, byte* payload, unsigned int length);
String buildDebugJson();
int pulseType(uint16_t us);
size_t collectLows(const rmt_data_t *symbols, size_t symbolCount, uint16_t *lows);
int decodeByte(const uint16_t *pulses);
bool decodeMessage(const rmt_data_t *symbols, size_t symbolCount, uint8_t *msg, size_t &len);
bool messageContains(const uint8_t *msg, size_t len, const uint8_t *what, size_t whatLen);
// =====================================================
// WLAN / Zeit / MQTT
// =====================================================
bool connectWiFi() {
if (WiFi.status() == WL_CONNECTED) return true;
unsigned long now = millis();
if (now - lastWifiReconnectMs < WifiRetryMs) return false;
lastWifiReconnectMs = now;
Serial.println("Verbinde mit WLAN...");
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 10000) {
delay(250);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WLAN verbunden");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
return true;
}
Serial.println("WLAN Verbindung fehlgeschlagen");
return false;
}
bool syncTime() {
if (WiFi.status() != WL_CONNECTED) return false;
Serial.println("Synchronisiere Zeit per NTP...");
configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);
time_t now = time(nullptr);
int retry = 0;
while (now < 100000 && retry < 20) {
delay(500);
Serial.print(".");
now = time(nullptr);
retry++;
}
Serial.println();
if (now < 100000) {
Serial.println("NTP Zeit konnte nicht geholt werden");
timeValid = false;
return false;
}
Serial.print("Aktuelle Unix-Zeit: ");
Serial.println((unsigned long)now);
lastNtpSyncMs = millis();
timeValid = true;
return true;
}
bool ensureTimeValid() {
time_t now = time(nullptr);
if (now > 100000 && timeValid) {
if (millis() - lastNtpSyncMs < NtpResyncMs) {
return true;
}
}
return syncTime();
}
bool ensureMqttConnected() {
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
if (mqttClient.connected()) return true;
if (WiFi.status() != WL_CONNECTED) return false;
unsigned long now = millis();
if (now - lastMqttReconnectMs < MqttRetryMs) return false;
lastMqttReconnectMs = now;
String clientId = "esp32-klingel-";
clientId += String((uint32_t)ESP.getEfuseMac(), HEX);
Serial.println("Verbinde MQTT...");
if (mqttClient.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) {
Serial.println("MQTT verbunden");
mqttClient.subscribe(MQTT_TOPIC_TUER_TEST);
mqttClient.subscribe(MQTT_TOPIC_TOR_TEST);
Serial.println("MQTT Test-Topics subscribed");
return true;
}
Serial.print("MQTT Verbindung fehlgeschlagen, rc=");
Serial.println(mqttClient.state());
return false;
}
bool publishUnixTimeToTopic(const char* topic) {
if (!connectWiFi()) return false;
if (!ensureTimeValid()) return false;
if (!ensureMqttConnected()) return false;
time_t now = time(nullptr);
uint64_t nowMs = ((uint64_t)now) * 1000ULL + (millis() % 1000ULL);
char payload[32];
snprintf(payload, sizeof(payload), "%llu", (unsigned long long)nowMs);
bool ok = mqttClient.publish(topic, payload, true);
if (ok) {
Serial.print("MQTT gesendet an ");
Serial.print(topic);
Serial.print(" mit Payload: ");
Serial.println(payload);
} else {
Serial.print("MQTT publish fehlgeschlagen für ");
Serial.println(topic);
}
mqttClient.loop();
return ok;
}
void updateLeds() {
unsigned long now = millis();
if (ledMode != LED_MODE_STANDBY && (long)(now - ledModeUntilMs) >= 0) {
ledMode = LED_MODE_STANDBY;
}
// ===== Langsames sinusförmiges Fade im Standby =====
if (ledMode == LED_MODE_STANDBY) {
if (now - lastLedUpdateMs >= 25) {
lastLedUpdateMs = now;
static float phase = 0.0f;
// kleiner = langsamer
phase += 0.02f;
if (phase > 2.0f * PI) {
phase -= 2.0f * PI;
}
int brightness = (int)((sin(phase) * 0.5f + 0.5f) * 255.0f);
ledcWrite(ledTuerPin, brightness);
ledcWrite(ledTorPin, brightness);
}
return;
}
// ===== Event-Blinken =====
if (now - lastBlinkToggleMs >= 100) {
lastBlinkToggleMs = now;
ledBlinkState = !ledBlinkState;
}
if (ledMode == LED_MODE_SHOW_TUER) {
ledcWrite(ledTuerPin, ledBlinkState ? 255 : 0);
ledcWrite(ledTorPin, 255);
return;
}
if (ledMode == LED_MODE_SHOW_TOR) {
ledcWrite(ledTuerPin, 255);
ledcWrite(ledTorPin, ledBlinkState ? 255 : 0);
return;
}
}
bool publishStatus() {
if (!connectWiFi()) return false;
if (!ensureMqttConnected()) return false;
String json = buildDebugJson();
bool ok = mqttClient.publish(MQTT_TOPIC_STATUS, json.c_str(), true);
if (ok) {
Serial.println("MQTT Status (JSON) gesendet");
} else {
Serial.println("MQTT Status publish fehlgeschlagen");
}
mqttClient.loop();
return ok;
}
// =====================================================
// Pending Flags
// =====================================================
bool isMqttPendingTuer() {
preferences.begin("klingel", true);
bool pending = preferences.getBool("mqttTuer", false);
preferences.end();
return pending;
}
bool isMqttPendingTor() {
preferences.begin("klingel", true);
bool pending = preferences.getBool("mqttTor", false);
preferences.end();
return pending;
}
void clearMqttPendingTuer() {
preferences.begin("klingel", false);
preferences.putBool("mqttTuer", false);
preferences.end();
}
void clearMqttPendingTor() {
preferences.begin("klingel", false);
preferences.putBool("mqttTor", false);
preferences.end();
}
void markMqttPendingTuer() {
preferences.begin("klingel", false);
preferences.putBool("mqttTuer", true);
preferences.end();
}
void markMqttPendingTor() {
preferences.begin("klingel", false);
preferences.putBool("mqttTor", true);
preferences.end();
}
void processPendingMqtt() {
if (isMqttPendingTuer()) {
if (publishUnixTimeToTopic(MQTT_TOPIC_TUER)) {
clearMqttPendingTuer();
}
}
if (isMqttPendingTor()) {
if (publishUnixTimeToTopic(MQTT_TOPIC_TOR)) {
clearMqttPendingTor();
}
}
}
// =====================================================
// Web / Debug / OTA
// =====================================================
String buildDebugJson() {
String json;
json.reserve(1700);
json += "{";
json += "\"wifi\":\"";
json += (WiFi.status() == WL_CONNECTED ? "connected" : "disconnected");
json += "\",";
json += "\"ip\":\"";
json += WiFi.localIP().toString();
json += "\",";
json += "\"rssi\":";
json += WiFi.RSSI();
json += ",";
json += "\"uptime\":";
json += millis() / 1000UL;
json += ",";
json += "\"free_heap\":";
json += ESP.getFreeHeap();
json += ",";
json += "\"state\":\"";
json += lastState;
json += "\",";
json += "\"source\":\"";
json += lastSource;
json += "\",";
json += "\"adcTorValue\":";
json += adcTorValue;
json += ",";
json += "\"adcTorThreshold\":";
json += adcTorThreshold;
json += ",";
json += "\"lastTorState\":";
json += (lastTorState ? "true" : "false");
json += ",";
json += "\"lastDecodedLen\":";
json += lastDecodedLen;
json += ",";
json += "\"lastDecodedHex\":\"";
json += lastDecodedHex;
json += "\",";
json += "\"mqttPendingTuer\":";
json += (isMqttPendingTuer() ? "true" : "false");
json += ",";
json += "\"mqttPendingTor\":";
json += (isMqttPendingTor() ? "true" : "false");
json += ",";
json += "\"bellPulseActive\":";
json += (bellPulseActive ? "true" : "false");
json += ",";
json += "\"hostname\":\"";
json += OTA_HOSTNAME;
json += "\",";
json += "\"mac\":\"";
json += WiFi.macAddress();
json += "\"";
json += "}";
return json;
}
void setupHttpRoutes() {
httpServer.on("/", HTTP_GET, []() {
String html;
html.reserve(3200);
html += "";
html += "";
html += "HSHB Klingel ";
html += "HSHB Klingel
";
html += "IP: " + WiFi.localIP().toString() + "
";
html += "RSSI: " + String(WiFi.RSSI()) + "
";
html += "Uptime: " + String(millis() / 1000UL) + " s
";
html += "State: " + lastState + "
";
html += "Source: " + lastSource + "
";
html += "ADC Tor: " + String(adcTorValue) + "
";
html += "ADC Threshold: " + String(adcTorThreshold) + "
";
html += "Last Tor State: " + String(lastTorState ? "true" : "false") + "
";
html += "Decoded Len: " + String(lastDecodedLen) + "
";
html += "Decoded Hex: " + lastDecodedHex + "
";
html += "MQTT pending Tür: " + String(isMqttPendingTuer() ? "true" : "false") + "
";
html += "MQTT pending Tor: " + String(isMqttPendingTor() ? "true" : "false") + "
";
html += "Bell Pulse: " + String(bellPulseActive ? "true" : "false") + "
";
html += "MAC: " + WiFi.macAddress() + "
";
html += "";
html += "";
html += "";
httpServer.send(200, "text/html", html);
});
httpServer.on("/debug", HTTP_GET, []() {
httpServer.send(200, "application/json", buildDebugJson());
});
httpUpdater.setup(&httpServer, "/update", OTA_USER, OTA_PASS);
httpServer.begin();
}
void setupArduinoOta() {
ArduinoOTA.setHostname(OTA_HOSTNAME);
ArduinoOTA.setPassword(OTA_PASS);
ArduinoOTA.begin();
}
// =====================================================
// MQTT Callback
// =====================================================
void mqttCallback(char* topic, byte* payload, unsigned int length) {
String topicStr = String(topic);
String payloadStr;
for (unsigned int i = 0; i < length; i++) {
payloadStr += (char)payload[i];
}
payloadStr.trim();
Serial.print("MQTT empfangen | topic=");
Serial.print(topicStr);
Serial.print(" | payload=");
Serial.println(payloadStr);
if (payloadStr != "test") return;
if (topicStr == MQTT_TOPIC_TUER_TEST) {
Serial.println("MQTT TEST -> TUER");
triggerBell(true);
} else if (topicStr == MQTT_TOPIC_TOR_TEST) {
Serial.println("MQTT TEST -> TOR");
triggerTorEvent("mqtt-test");
}
}
// =====================================================
// Klingeltrigger
// =====================================================
void triggerBell(bool fromButton) {
unsigned long now = millis();
if (now - lastBellTriggered < BellDebounceMs) return;
Serial.println("Bell Triggered!");
ledMode = LED_MODE_SHOW_TUER;
ledModeUntilMs = now + LedEventDurationMs;
lastBellTriggered = now;
digitalWrite(bellPin, HIGH);
bellPulseStartMillis = now;
bellPulseActive = true;
lastSource = fromButton ? "button" : "bus";
lastState = "BELL";
markMqttPendingTuer();
processPendingMqtt();
publishStatus();
}
void triggerTorEvent(const char* source) {
unsigned long now = millis();
if (now - lastTorTriggerMillis < TorDebounceMs) {
Serial.println("TOR blockiert: 11s Sperre aktiv");
return;
}
lastTorTriggerMillis = now;
ledMode = LED_MODE_SHOW_TOR;
ledModeUntilMs = now + LedEventDurationMs;
lastState = "TOR";
lastSource = source;
Serial.print("TOR TRIGGER! source=");
Serial.println(source);
markMqttPendingTor();
processPendingMqtt();
publishStatus();
}
// =====================================================
// RMT Hilfsfunktionen
// =====================================================
int pulseType(uint16_t us) {
if (us >= 32 && us <= 52) return P_ONE;
if (us >= 103 && us <= 123) return P_ZERO;
if (us >= 223 && us <= 243) return P_START;
if (us >= 76 && us <= 96) return P_STOP;
return P_BAD;
}
size_t collectLows(const rmt_data_t *symbols, size_t symbolCount, uint16_t *lows) {
size_t n = 0;
for (size_t i = 0; i < symbolCount && n < maxSyms; ++i) {
if (symbols[i].level0 && !symbols[i].level1 &&
symbols[i].duration0 >= 17 && symbols[i].duration0 <= 33 &&
symbols[i].duration1) {
lows[n++] = symbols[i].duration1;
} else if (!symbols[i].level0 && symbols[i].level1 &&
symbols[i].duration1 >= 17 && symbols[i].duration1 <= 33 &&
symbols[i].duration0) {
lows[n++] = symbols[i].duration0;
} else if (!symbols[i].level0 && pulseType(symbols[i].duration0) != P_BAD) {
lows[n++] = symbols[i].duration0;
} else if (!symbols[i].level1 && pulseType(symbols[i].duration1) != P_BAD) {
lows[n++] = symbols[i].duration1;
}
}
return n;
}
int decodeByte(const uint16_t *pulses) {
if (pulseType(pulses[0]) != P_START) return -1;
uint8_t value = 0;
for (int i = 0; i < 8; ++i) {
int bit = pulseType(pulses[i + 1]);
if (bit < 0 || bit > 1) return -1;
value |= bit << i;
}
return value;
}
bool decodeMessage(const rmt_data_t *symbols, size_t symbolCount, uint8_t *msg, size_t &len) {
uint16_t lows[maxSyms];
size_t lowCount = collectLows(symbols, symbolCount, lows);
len = 0;
for (size_t i = 0; i + 9 < lowCount && len < bufferSize;) {
if (pulseType(lows[i]) != P_START) {
++i;
continue;
}
int value = decodeByte(lows + i);
if (value < 0) {
++i;
continue;
}
msg[len++] = value;
i += 10;
if (i < lowCount && lows[i] >= 76) {
++i;
}
}
return len > 0;
}
bool messageContains(const uint8_t *msg, size_t len, const uint8_t *what, size_t whatLen) {
if (whatLen > len) return false;
for (size_t i = 0; i + whatLen <= len; ++i) {
if (!memcmp(msg + i, what, whatLen)) return true;
}
return false;
}
// =====================================================
// Setup
// =====================================================
void setup() {
Serial.begin(115200);
delay(500);
ledcAttach(ledTuerPin, ledFreq, ledResolution);
ledcAttach(ledTorPin, ledFreq, ledResolution);
ledcWrite(ledTuerPin, 0);
ledcWrite(ledTorPin, 0);
pinMode(bellPin, OUTPUT);
digitalWrite(bellPin, LOW);
pinMode(testBellPin, INPUT_PULLUP);
pinMode(adcTorPin, INPUT);
analogReadResolution(12);
analogSetPinAttenuation(adcTorPin, ADC_11db);
WiFi.mode(WIFI_STA);
esp_wifi_set_mac(WIFI_IF_STA, &newMACAddress[0]);
mqttClient.setServer(MQTT_HOST, MQTT_PORT);
mqttClient.setCallback(mqttCallback);
mqttClient.setBufferSize(2048);
mqttClient.setKeepAlive(30);
mqttClient.setSocketTimeout(2);
connectWiFi();
ensureTimeValid();
ensureMqttConnected();
if (MDNS.begin(OTA_HOSTNAME)) {
MDNS.addService("http", "tcp", 80);
}
setupHttpRoutes();
setupArduinoOta();
rmtInit(busPin, RMT_RX_MODE, RMT_MEM_NUM_BLOCKS_2, 1000000);
rmtSetRxMinThreshold(busPin, 3);
rmtSetRxMaxThreshold(busPin, 1000);
rmtReadAsync(busPin, symbols, &symbolCount);
publishStatus();
}
// =====================================================
// Loop
// =====================================================
void loop() {
unsigned long now = millis();
updateLeds();
connectWiFi();
ensureMqttConnected();
mqttClient.loop();
httpServer.handleClient();
ArduinoOTA.handle();
adcTorValue = analogRead(adcTorPin);
bool currentTorState = (adcTorValue > adcTorThreshold);
// nur steigende Flanke
if (!lastTorState && currentTorState) {
Serial.print("TOR Flanke erkannt, ADC=");
Serial.println(adcTorValue);
triggerTorEvent("adc");
}
lastTorState = currentTorState;
bool currentButtonState = digitalRead(testBellPin);
if (lastButtonState == HIGH && currentButtonState == LOW &&
(now - lastButtonTriggerMillis > ButtonDebounceMs)) {
lastButtonTriggerMillis = now;
triggerBell(true);
}
lastButtonState = currentButtonState;
if (rmtReceiveCompleted(busPin)) {
uint8_t msg[bufferSize];
size_t len = 0;
decodeMessage(symbols, symbolCount, msg, len);
lastDecodedLen = len;
lastDecodedHex = "";
for (size_t i = 0; i < len; ++i) {
char tmp[4];
snprintf(tmp, sizeof(tmp), "%02X", msg[i]);
if (i) lastDecodedHex += " ";
lastDecodedHex += tmp;
}
if (lastDecodedHex.length() == 0) lastDecodedHex = "-";
symbolCount = maxSyms;
rmtReadAsync(busPin, symbols, &symbolCount);
if (messageContains(msg, len, hackerspaceBell, sizeof(hackerspaceBell)) ||
messageContains(msg, len, hackerspaceBell2, sizeof(hackerspaceBell2))) {
triggerBell(false);
}
}
if (bellPulseActive && (now - bellPulseStartMillis > BellPulseMs)) {
digitalWrite(bellPin, LOW);
bellPulseActive = false;
if (lastState == "BELL") {
lastState = "IDLE";
}
}
if (now - lastStatusMillis > StatusMs) {
lastStatusMillis = now;
publishStatus();
}
if (now - lastDebugMillis > Debuginterval) {
lastDebugMillis = now;
Serial.print("state=");
Serial.print(lastState);
Serial.print(" source=");
Serial.print(lastSource);
Serial.print(" adc=");
Serial.print(adcTorValue);
Serial.print(" torState=");
Serial.println(currentTorState ? "1" : "0");
}
delay(1);
}