Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
| Nächste Überarbeitung | Vorhergehende Überarbeitung | ||
| ikea:ps2014 [2025/11/12 17:13] – angelegt gerald | ikea:ps2014 [2025/12/01 22:41] (aktuell) – [Anforderungen / Eigenschaften nach dem Umbau] gerald | ||
|---|---|---|---|
| Zeile 5: | Zeile 5: | ||
| [[https:// | [[https:// | ||
| + | |||
| + | Feetech FS5103R 3 kg.cm 25T RC Servo 360 Grad kontinuierliche Drehung Standard Analog Radmotor für Arduino Raspberry | ||
| + | |||
| + | |||
| + | ===== Mein Projekt dazu ===== | ||
| + | |||
| + | ==== Anforderungen / Eigenschaften nach dem Umbau ==== | ||
| + | |||
| + | Arduino-IDE | ||
| + | |||
| + | * Servo-Motor, | ||
| + | * 50 Neopixel (10 Lichter pro Reihe (für je einen Arm) in 5 Reihen) | ||
| + | * HAL-Sensor auf dem Fadenwickler zur Messung der Umdrehungen | ||
| + | * Schalter für die obere Endposition | ||
| + | * PIR zur Bewegungserkennung unter der Lampe | ||
| + | * ESP8266 (m1n1) zur Steuerung. WLAN | ||
| + | * Webserver für Einstellungen und Steuerung | ||
| + | * MQTT | ||
| + | * OTA-Firmware-Updates | ||
| + | * HA-Autorecovery | ||
| + | |||
| + | |||
| + | Verkabelung | ||
| + | |||
| + | * LIMIT_SWITCH (oben) | ||
| + | * HALL_SENSOR (magnet) -> D5 | ||
| + | * SERVO_SIGNAL | ||
| + | * NEOPIXEL_DATA | ||
| + | * PIR -> D1 | ||
| + | |||
| + | zwischen ESP und Neopixel einen 330–470 Ω Widerstand | ||
| + | |||
| + | einen 1000 µF Puffer-Kondensator 5V → GND am Anfang des LED-Strips (verhindert Stromspitzen) | ||
| + | |||
| + | kein Pull-Up-Widerstand bei HAL | ||
| + | |||
| + | Hall-Sensor OUT ist typischerweise Open-Collector. Verwende einen externen 10 kΩ Pull-Up an 3.3 V (also zwischen OUT und 3.3V). Nicht an 5V ziehen! | ||
| + | |||
| + | End-Switch: Optional: 100nF Kondensator direkt am Schalter gegen GND | ||
| + | |||
| + | === neoPixel === | ||
| + | |||
| + | String mit 50 neopixel, verkabelt (kein Strip) | ||
| + | |||
| + | von oben nach unten, durchnummeriert 1-50. im Uhrzeigersinn. | ||
| + | |||
| + | - 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 | ||
| + | - 11, 50, 43, 42, 35, 34, 27, 26, 19, 18 | ||
| + | - 12, 49, 44, 41, 36, 33, 28, 25, 20, 17 | ||
| + | - 13, 48, 45, 40, 37, 32, 29, 24, 21, 16 | ||
| + | - 14, 47, 46, 39, 38, 31, 30, 23, 22, 15 | ||
| + | |||
| + | |||
| + | === Code === | ||
| + | |||
| + | |||
| + | Stand: | ||
| + | < | ||
| + | ✔ WLAN konfigurieren per WiFiManager | ||
| + | ✔ Kleinen Webserver hosten | ||
| + | ✔ OTA-Updates | ||
| + | ✔ MQTT mit drei Kommandos | ||
| + | ✔ Servo hochfahren → bis Endschalter | ||
| + | ✔ Servo runterfahren → per Hall-Sensor-Strecke | ||
| + | ✔ Hall-Sensor zählt Impulse | ||
| + | ✔ NeoPixel (Einzelgruppen, | ||
| + | ✔ Under-the-hood sauber strukturiert | ||
| + | ✔ Voll lauffähig | ||
| + | </ | ||
| + | |||
| + | |||
| + | < | ||
| + | / | ||
| + | * Lampe IKEA2014 - Full Firmware | ||
| + | * - Non-blocking state machine (IDLE, HOMING, MOVING, STOPPED, ERROR) | ||
| + | * - Endstop (D7) with debounce via ISR + software anti-bounce | ||
| + | * - Hall sensor (D5) increments pulses via ISR | ||
| + | * - Servo FS90R on D4 with attach/ | ||
| + | * - NeoPixel 50x on D2 (groups: 0-9, | ||
| + | * - WiFiManager for WLAN setup | ||
| + | * - Web server for basic control + MQTT settings | ||
| + | * - OTA update via /update | ||
| + | * - MQTT command topic: lampe/ | ||
| + | * - MQTT status topic: lampe/ | ||
| + | * - EEPROM persistence for MQTT and default target turns | ||
| + | * | ||
| + | * Serial debug is verbose. Set monitor to 115200. | ||
| + | | ||
| + | |||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | #include < | ||
| + | |||
| + | #include < | ||
| + | #include < | ||
| + | |||
| + | // ---------------- CONFIG ---------------- | ||
| + | #define DEVICE_NAME " | ||
| + | #define EEPROM_SIZE 512 | ||
| + | |||
| + | // EEPROM offsets | ||
| + | #define EEPROM_OFF_MQTT_HOST 0 // 64 bytes | ||
| + | #define EEPROM_OFF_MQTT_PORT 64 // 4 bytes (int) | ||
| + | #define EEPROM_OFF_MQTT_USER 96 // 64 bytes | ||
| + | #define EEPROM_OFF_MQTT_PASS 160 // 64 bytes | ||
| + | #define EEPROM_OFF_TARGET | ||
| + | |||
| + | // Pins (as wired) | ||
| + | const uint8_t PIN_LIMIT = D7; // end switch, active LOW | ||
| + | const uint8_t PIN_HALL | ||
| + | const uint8_t PIN_SERVO = D4; // FS90R signal | ||
| + | const uint8_t PIN_PIXEL = D2; // NeoPixel data | ||
| + | const uint8_t PIN_PIR | ||
| + | |||
| + | // Servo microseconds for FS90R | ||
| + | const int SERVO_STOP_US = 1500; | ||
| + | const int SERVO_CW_US | ||
| + | const int SERVO_CCW_US | ||
| + | |||
| + | // NeoPixel | ||
| + | const uint16_t NUM_PIXELS = 50; | ||
| + | Adafruit_NeoPixel pixels(NUM_PIXELS, | ||
| + | |||
| + | // Groups (0-9, | ||
| + | const uint8_t GROUP1_START = 0, GROUP1_COUNT = 10; | ||
| + | const uint8_t GROUP2_START = 10, GROUP2_COUNT = 10; | ||
| + | const uint8_t GROUP3_START = 20, GROUP3_COUNT = 10; | ||
| + | const uint8_t GROUP4_START = 30, GROUP4_COUNT = 10; | ||
| + | const uint8_t GROUP5_START = 40, GROUP5_COUNT = 10; | ||
| + | |||
| + | // Motion params | ||
| + | volatile long hallPulses = 0; | ||
| + | volatile unsigned long lastHallMicros = 0; | ||
| + | const unsigned long HALL_DEBOUNCE_US = 50000UL; // 50ms | ||
| + | int pulsesPerRevolution = 1; // magnets per rev | ||
| + | |||
| + | // Default target turns (will be loaded/ | ||
| + | int targetTurns = 5; | ||
| + | |||
| + | // State machine | ||
| + | enum class State { IDLE, HOMING, MOVING, STOPPED, ERROR }; | ||
| + | volatile State state = State:: | ||
| + | |||
| + | // Flags | ||
| + | volatile bool hallEvent = false; | ||
| + | volatile bool limitRawTriggered = false; | ||
| + | unsigned long lastLimitDebounceMs = 0; | ||
| + | const unsigned long LIMIT_DEBOUNCE_MS = 300; // anti-bounce window | ||
| + | bool homed = false; | ||
| + | bool servoAttached = false; | ||
| + | |||
| + | // Servo | ||
| + | Servo servoMotor; | ||
| + | |||
| + | // WiFi / MQTT / Web | ||
| + | WiFiClient wifiClient; | ||
| + | PubSubClient mqtt(wifiClient); | ||
| + | WiFiManager wifiManager; | ||
| + | ESP8266WebServer server(80); | ||
| + | ESP8266HTTPUpdateServer httpUpdater; | ||
| + | |||
| + | // MQTT settings (loaded from EEPROM) | ||
| + | char mqttHost[64] = ""; | ||
| + | uint16_t mqttPort = 1883; | ||
| + | char mqttUser[64] = ""; | ||
| + | char mqttPass[64] = ""; | ||
| + | String mqttBaseTopic = String(" | ||
| + | |||
| + | // Timing | ||
| + | unsigned long lastPublishMs = 0; | ||
| + | const unsigned long PUBLISH_INTERVAL_MS = 2000; | ||
| + | |||
| + | // Stall detection | ||
| + | unsigned long lastPulseTimeMs = 0; | ||
| + | const unsigned long STALL_TIMEOUT_MS = 3000; // ms without pulse = stall when moving | ||
| + | |||
| + | // Heartbeat ticker | ||
| + | Ticker heartbeatTicker; | ||
| + | |||
| + | // --------------- EEPROM helpers --------------- | ||
| + | void eepromReadString(int addr, char* buf, size_t maxlen) { | ||
| + | for (size_t i=0; | ||
| + | buf[i] = EEPROM.read(addr + i); | ||
| + | if (buf[i] == 0) break; | ||
| + | } | ||
| + | buf[maxlen-1] = 0; | ||
| + | } | ||
| + | void eepromWriteString(int addr, const char* s, size_t maxlen) { | ||
| + | size_t i; | ||
| + | for (i=0; | ||
| + | if (s[i] == 0) { EEPROM.write(addr + i, 0); break; } | ||
| + | EEPROM.write(addr + i, s[i]); | ||
| + | } | ||
| + | if (i==maxlen) EEPROM.write(addr + maxlen - 1, 0); | ||
| + | } | ||
| + | void eepromWriteInt(int addr, int32_t v) { | ||
| + | EEPROM.write(addr+0, | ||
| + | EEPROM.write(addr+1, | ||
| + | EEPROM.write(addr+2, | ||
| + | EEPROM.write(addr+3, | ||
| + | } | ||
| + | int32_t eepromReadInt(int addr) { | ||
| + | int32_t v = 0; | ||
| + | v |= (int32_t)EEPROM.read(addr+0) << 24; | ||
| + | v |= (int32_t)EEPROM.read(addr+1) << 16; | ||
| + | v |= (int32_t)EEPROM.read(addr+2) << 8; | ||
| + | v |= (int32_t)EEPROM.read(addr+3) << 0; | ||
| + | return v; | ||
| + | } | ||
| + | |||
| + | // -------------- ISRs (very small) -------------- | ||
| + | void ICACHE_RAM_ATTR hallISR() { | ||
| + | unsigned long now = micros(); | ||
| + | if (now - lastHallMicros < HALL_DEBOUNCE_US) return; | ||
| + | lastHallMicros = now; | ||
| + | hallPulses++; | ||
| + | hallEvent = true; | ||
| + | lastPulseTimeMs = millis(); | ||
| + | } | ||
| + | void ICACHE_RAM_ATTR limitISR() { | ||
| + | // just mark raw trigger; do debouncing in loop | ||
| + | limitRawTriggered = true; | ||
| + | } | ||
| + | |||
| + | // -------------- Servo helpers -------------- | ||
| + | void servoAttachIfNeeded() { | ||
| + | if (!servoAttached) { | ||
| + | servoMotor.attach(PIN_SERVO, | ||
| + | servoAttached = true; | ||
| + | Serial.println(" | ||
| + | } | ||
| + | } | ||
| + | void servoDetachIfNeeded() { | ||
| + | if (servoAttached) { | ||
| + | servoMotor.detach(); | ||
| + | servoAttached = false; | ||
| + | Serial.println(" | ||
| + | } | ||
| + | } | ||
| + | void servoWriteMicro(int microsVal) { | ||
| + | servoAttachIfNeeded(); | ||
| + | servoMotor.writeMicroseconds(microsVal); | ||
| + | } | ||
| + | void servoStop() { | ||
| + | // set neutral, small delay, then detach | ||
| + | servoWriteMicro(SERVO_STOP_US); | ||
| + | delay(5); | ||
| + | servoDetachIfNeeded(); | ||
| + | Serial.println(" | ||
| + | } | ||
| + | void servoUp() { | ||
| + | servoWriteMicro(SERVO_CW_US); | ||
| + | Serial.println(" | ||
| + | } | ||
| + | void servoDown() { | ||
| + | servoWriteMicro(SERVO_CCW_US); | ||
| + | Serial.println(" | ||
| + | } | ||
| + | |||
| + | // -------------- NeoPixel helpers -------------- | ||
| + | uint32_t col(uint8_t r,uint8_t g,uint8_t b){ return pixels.Color(r, | ||
| + | void clearAll() { for (uint16_t i=0; | ||
| + | void setRange(uint8_t start, uint8_t count, uint32_t color) { for (uint8_t i=0; | ||
| + | void heartbeat() { static bool on=false; on=!on; if(on) setRange(GROUP5_START, | ||
| + | |||
| + | // -------------- MQTT helpers -------------- | ||
| + | void mqttPublishState() { | ||
| + | if (!mqtt.connected()) return; | ||
| + | String topic = mqttBaseTopic + "/ | ||
| + | String payload = String(" | ||
| + | mqtt.publish(topic.c_str(), | ||
| + | Serial.println(" | ||
| + | } | ||
| + | void mqttPublishTopic(const char* suffix, const char* msg) { | ||
| + | if (!mqtt.connected()) { Serial.println(" | ||
| + | String topic = mqttBaseTopic + "/" | ||
| + | mqtt.publish(topic.c_str(), | ||
| + | Serial.println(" | ||
| + | } | ||
| + | |||
| + | // -------------- MQTT callback -------------- | ||
| + | void mqttCallback(char* topic, byte* payload, unsigned int length) { | ||
| + | String msg; | ||
| + | for (unsigned int i=0; | ||
| + | msg.trim(); | ||
| + | Serial.println(" | ||
| + | |||
| + | if (msg.equalsIgnoreCase(" | ||
| + | if (state==State:: | ||
| + | Serial.println(" | ||
| + | state = State:: | ||
| + | } else Serial.println(" | ||
| + | } else if (msg.startsWith(" | ||
| + | int n = msg.substring(5).toInt(); | ||
| + | hallPulses = 0; | ||
| + | targetTurns = n; | ||
| + | Serial.println(" | ||
| + | state = State:: | ||
| + | } else if (msg.equalsIgnoreCase(" | ||
| + | hallPulses = 0; | ||
| + | Serial.println(" | ||
| + | state = State:: | ||
| + | } else if (msg.equalsIgnoreCase(" | ||
| + | Serial.println(" | ||
| + | state = State:: | ||
| + | servoStop(); | ||
| + | } else if (msg.startsWith(" | ||
| + | int v = msg.substring(11).toInt(); | ||
| + | if (v>0) { | ||
| + | targetTurns = v; | ||
| + | eepromWriteInt(EEPROM_OFF_TARGET, | ||
| + | EEPROM.commit(); | ||
| + | Serial.println(" | ||
| + | mqttPublishTopic(" | ||
| + | } | ||
| + | } else { | ||
| + | Serial.println(" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // -------------- MQTT connect try -------------- | ||
| + | void mqttTryConnect() { | ||
| + | if (mqtt.connected()) return; | ||
| + | if (strlen(mqttHost) == 0) { | ||
| + | Serial.println(" | ||
| + | return; | ||
| + | } | ||
| + | mqtt.setServer(mqttHost, | ||
| + | Serial.println(" | ||
| + | if (mqtt.connect(DEVICE_NAME, | ||
| + | String cmdTopic = mqttBaseTopic + "/ | ||
| + | mqtt.setCallback(mqttCallback); | ||
| + | mqtt.subscribe(cmdTopic.c_str()); | ||
| + | Serial.println(" | ||
| + | mqttPublishTopic(" | ||
| + | } else { | ||
| + | Serial.println(" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // -------------- Web handlers -------------- | ||
| + | void handleRoot() { | ||
| + | Serial.println(" | ||
| + | String html = "< | ||
| + | html += "< | ||
| + | html += "< | ||
| + | html += "< | ||
| + | html += "< | ||
| + | html += "< | ||
| + | html += "< | ||
| + | html += \"< | ||
| + | html += \"</ | ||
| + | server.send(200, | ||
| + | } | ||
| + | |||
| + | void handleCmdWeb() { | ||
| + | Serial.println(" | ||
| + | if (!server.hasArg(" | ||
| + | String c = server.arg(" | ||
| + | if (c == " | ||
| + | else if (c == " | ||
| + | else if (c == " | ||
| + | else server.send(400, | ||
| + | } | ||
| + | |||
| + | void handleSetMqtt() { | ||
| + | Serial.println(" | ||
| + | if (!server.hasArg(" | ||
| + | String b = server.arg(" | ||
| + | b.toCharArray(mqttHost, | ||
| + | eepromWriteString(EEPROM_OFF_MQTT_HOST, | ||
| + | eepromWriteInt(EEPROM_OFF_MQTT_PORT, | ||
| + | eepromWriteString(EEPROM_OFF_MQTT_USER, | ||
| + | eepromWriteString(EEPROM_OFF_MQTT_PASS, | ||
| + | EEPROM.commit(); | ||
| + | Serial.println(" | ||
| + | server.send(200, | ||
| + | mqttTryConnect(); | ||
| + | } | ||
| + | |||
| + | // -------------- Setup -------------- | ||
| + | void setup() { | ||
| + | Serial.begin(115200); | ||
| + | delay(50); | ||
| + | Serial.println(" | ||
| + | |||
| + | EEPROM.begin(EEPROM_SIZE); | ||
| + | eepromReadString(EEPROM_OFF_MQTT_HOST, | ||
| + | mqttPort = (uint16_t)eepromReadInt(EEPROM_OFF_MQTT_PORT); | ||
| + | if (mqttPort == 0) mqttPort = 1883; | ||
| + | eepromReadString(EEPROM_OFF_MQTT_USER, | ||
| + | eepromReadString(EEPROM_OFF_MQTT_PASS, | ||
| + | int storedTarget = (int)eepromReadInt(EEPROM_OFF_TARGET); | ||
| + | if (storedTarget > 0) targetTurns = storedTarget; | ||
| + | |||
| + | Serial.println(" | ||
| + | Serial.println(String(" | ||
| + | Serial.println(String(" | ||
| + | Serial.println(String(" | ||
| + | Serial.println(String(" | ||
| + | |||
| + | // pins & ISRs | ||
| + | pinMode(PIN_LIMIT, | ||
| + | pinMode(PIN_HALL, | ||
| + | pinMode(PIN_PIR, | ||
| + | attachInterrupt(digitalPinToInterrupt(PIN_HALL), | ||
| + | attachInterrupt(digitalPinToInterrupt(PIN_LIMIT), | ||
| + | Serial.println(" | ||
| + | |||
| + | // NeoPixel | ||
| + | pixels.begin(); | ||
| + | Serial.println(" | ||
| + | |||
| + | // servo initial state: neutral & detached | ||
| + | servoMotor.attach(PIN_SERVO, | ||
| + | servoMotor.writeMicroseconds(SERVO_STOP_US); | ||
| + | delay(5); | ||
| + | servoMotor.detach(); | ||
| + | servoAttached = false; | ||
| + | Serial.println(" | ||
| + | |||
| + | // WiFiManager (blocking until wifi configured or known) | ||
| + | Serial.println(" | ||
| + | wifiManager.autoConnect(DEVICE_NAME); | ||
| + | Serial.println(" | ||
| + | |||
| + | // web + OTA | ||
| + | httpUpdater.setup(& | ||
| + | server.on("/", | ||
| + | server.on("/ | ||
| + | server.on("/ | ||
| + | server.begin(); | ||
| + | Serial.println(" | ||
| + | |||
| + | // MQTT | ||
| + | mqtt.setClient(wifiClient); | ||
| + | mqtt.setCallback(mqttCallback); | ||
| + | mqttTryConnect(); | ||
| + | |||
| + | // heartbeat | ||
| + | heartbeatTicker.attach(1.0, | ||
| + | Serial.println(" | ||
| + | |||
| + | // start homing if needed (non-blocking) | ||
| + | if (digitalRead(PIN_LIMIT) == HIGH) { | ||
| + | state = State:: | ||
| + | Serial.println(" | ||
| + | } else { | ||
| + | homed = true; | ||
| + | state = State:: | ||
| + | Serial.println(" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // -------------- Main loop (non-blocking FSM) -------------- | ||
| + | void loop() { | ||
| + | server.handleClient(); | ||
| + | if (!mqtt.connected()) mqttTryConnect(); | ||
| + | |||
| + | unsigned long now = millis(); | ||
| + | |||
| + | // process raw limit triggers with debounce | ||
| + | if (limitRawTriggered) { | ||
| + | limitRawTriggered = false; | ||
| + | unsigned long t = millis(); | ||
| + | if (t - lastLimitDebounceMs > LIMIT_DEBOUNCE_MS) { | ||
| + | lastLimitDebounceMs = t; | ||
| + | Serial.println(" | ||
| + | // If homing, react immediately in state machine section | ||
| + | // We also set homed flag later there. | ||
| + | // store timestamp of last pulse as safety | ||
| + | lastPulseTimeMs = millis(); | ||
| + | // Mark a software event sign that limit was hit | ||
| + | // We don't directly change state here; state machine handles it. | ||
| + | // To allow immediate reaction, we can set hallPulses=0 if homing ends. | ||
| + | if (state == State:: | ||
| + | // set hallPulses to 0 at home | ||
| + | hallPulses = 0; | ||
| + | homed = true; | ||
| + | servoStop(); | ||
| + | state = State:: | ||
| + | Serial.println(" | ||
| + | mqttPublishTopic(" | ||
| + | } else { | ||
| + | // limit pressed unexpected | ||
| + | Serial.println(" | ||
| + | } | ||
| + | } else { | ||
| + | Serial.println(" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // state machine | ||
| + | static State lastState = State:: | ||
| + | if (lastState != state) { | ||
| + | Serial.println(" | ||
| + | lastState = state; | ||
| + | } | ||
| + | |||
| + | switch (state) { | ||
| + | case State:: | ||
| + | // ensure servo detached (no PWM) | ||
| + | servoStop(); | ||
| + | // do nothing else | ||
| + | break; | ||
| + | |||
| + | case State:: | ||
| + | // run servo up until limit interrupt occurs (limitRawTriggered processed above) | ||
| + | // ensure servo is running up | ||
| + | servoUp(); | ||
| + | break; | ||
| + | |||
| + | case State:: | ||
| + | // run down until pulses reached or error | ||
| + | servoDown(); | ||
| + | { | ||
| + | long goal = (long)targetTurns * pulsesPerRevolution; | ||
| + | if ((long)hallPulses >= goal) { | ||
| + | Serial.println(" | ||
| + | servoStop(); | ||
| + | state = State:: | ||
| + | mqttPublishTopic(" | ||
| + | } | ||
| + | // stall detection | ||
| + | if (millis() - lastPulseTimeMs > STALL_TIMEOUT_MS) { | ||
| + | Serial.println(" | ||
| + | servoStop(); | ||
| + | state = State:: | ||
| + | mqttPublishTopic(" | ||
| + | } | ||
| + | // safety: if limit is pressed unexpectedly -> error | ||
| + | if (digitalRead(PIN_LIMIT) == LOW) { | ||
| + | Serial.println(" | ||
| + | servoStop(); | ||
| + | state = State:: | ||
| + | mqttPublishTopic(" | ||
| + | } | ||
| + | } | ||
| + | break; | ||
| + | |||
| + | case State:: | ||
| + | servoStop(); | ||
| + | // stay stopped until command | ||
| + | break; | ||
| + | |||
| + | case State:: | ||
| + | // motor stopped; require manual HOME to recover | ||
| + | servoStop(); | ||
| + | // optionally blink LEDs to indicate error | ||
| + | setRange(GROUP1_START, | ||
| + | break; | ||
| + | } | ||
| + | |||
| + | // publish periodic state | ||
| + | if (now - lastPublishMs > PUBLISH_INTERVAL_MS) { | ||
| + | lastPublishMs = now; | ||
| + | mqttPublishState(); | ||
| + | } | ||
| + | |||
| + | // handle hall event logging (do not print from ISR) | ||
| + | if (hallEvent) { | ||
| + | hallEvent = false; | ||
| + | Serial.println(" | ||
| + | // publish per-pulse (optional) | ||
| + | mqttPublishTopic(" | ||
| + | } | ||
| + | |||
| + | yield(); | ||
| + | } | ||
| + | |||
| + | </ | ||