https://github.com/julisa99/IKEAPS2014
Feetech FS5103R 3 kg.cm 25T RC Servo 360 Grad kontinuierliche Drehung Standard Analog Radmotor für Arduino Raspberry
Arduino-IDE
Verkabelung
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
String mit 50 neopixel, verkabelt (kein Strip)
von oben nach unten, durchnummeriert 1-50. im Uhrzeigersinn.
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, Basisfunktionen) ✔ 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/detach to avoid jitter * - NeoPixel 50x on D2 (groups: 0-9,10-19,20-29,30-39,40-49) * - WiFiManager for WLAN setup * - Web server for basic control + MQTT settings * - OTA update via /update * - MQTT command topic: lampe/ikea2014/cmd * - MQTT status topic: lampe/ikea2014/status * - EEPROM persistence for MQTT and default target turns * * Serial debug is verbose. Set monitor to 115200. ******************************************************/ #include <ESP8266WiFi.h> #include <WiFiManager.h> #include <Ticker.h> #include <ESP8266WebServer.h> #include <ESP8266HTTPUpdateServer.h> #include <PubSubClient.h> #include <EEPROM.h> #include <Servo.h> #include <Adafruit_NeoPixel.h> // ---------------- CONFIG ---------------- #define DEVICE_NAME "lampe_ikea2014" #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 224 // 4 bytes int // Pins (as wired) const uint8_t PIN_LIMIT = D7; // end switch, active LOW const uint8_t PIN_HALL = D5; // hall sensor (interrupt FALLING) const uint8_t PIN_SERVO = D4; // FS90R signal const uint8_t PIN_PIXEL = D2; // NeoPixel data const uint8_t PIN_PIR = D1; // optional // Servo microseconds for FS90R const int SERVO_STOP_US = 1500; const int SERVO_CW_US = 1000; // adjust if direction inverted const int SERVO_CCW_US = 2000; // NeoPixel const uint16_t NUM_PIXELS = 50; Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_PIXEL, NEO_GRB + NEO_KHZ800); // Groups (0-9,10-19,20-29,30-39,40-49) 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/saved from EEPROM) int targetTurns = 5; // State machine enum class State { IDLE, HOMING, MOVING, STOPPED, ERROR }; volatile State state = State::IDLE; // 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("lampe/ikea2014"); // 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;i<maxlen;i++) { 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;i<maxlen;i++) { 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, (v>>24)&0xFF); EEPROM.write(addr+1, (v>>16)&0xFF); EEPROM.write(addr+2, (v>>8)&0xFF); EEPROM.write(addr+3, (v>>0)&0xFF); } 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, 1000, 2000); servoAttached = true; Serial.println("[SERVO] attached"); } } void servoDetachIfNeeded() { if (servoAttached) { servoMotor.detach(); servoAttached = false; Serial.println("[SERVO] detached"); } } 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("[SERVO] STOP (neutral + detach)"); } void servoUp() { servoWriteMicro(SERVO_CW_US); Serial.println("[SERVO] UP (homing direction) -> pwm " + String(SERVO_CW_US)); } void servoDown() { servoWriteMicro(SERVO_CCW_US); Serial.println("[SERVO] DOWN -> pwm " + String(SERVO_CCW_US)); } // -------------- NeoPixel helpers -------------- uint32_t col(uint8_t r,uint8_t g,uint8_t b){ return pixels.Color(r,g,b); } void clearAll() { for (uint16_t i=0;i<NUM_PIXELS;i++) pixels.setPixelColor(i,0); pixels.show(); } void setRange(uint8_t start, uint8_t count, uint32_t color) { for (uint8_t i=0;i<count;i++) pixels.setPixelColor(start+i, color); pixels.show(); } void heartbeat() { static bool on=false; on=!on; if(on) setRange(GROUP5_START, GROUP5_COUNT, col(0,12,0)); else setRange(GROUP5_START, GROUP5_COUNT, 0); } // -------------- MQTT helpers -------------- void mqttPublishState() { if (!mqtt.connected()) return; String topic = mqttBaseTopic + "/status"; String payload = String("state:") + String((int)state) + ",pulses:" + String(hallPulses) + ",homed:" + (homed?"1":"0"); mqtt.publish(topic.c_str(), payload.c_str(), true); Serial.println("[MQTT] Published state -> " + payload); } void mqttPublishTopic(const char* suffix, const char* msg) { if (!mqtt.connected()) { Serial.println("[MQTT] publish skip (not connected): " + String(suffix)); return; } String topic = mqttBaseTopic + "/" + String(suffix); mqtt.publish(topic.c_str(), msg); Serial.println("[MQTT] Published " + topic + " -> " + String(msg)); } // -------------- MQTT callback -------------- void mqttCallback(char* topic, byte* payload, unsigned int length) { String msg; for (unsigned int i=0;i<length;i++) msg += (char)payload[i]; msg.trim(); Serial.println("[MQTT] msg on " + String(topic) + " : " + msg); if (msg.equalsIgnoreCase("HOME")) { if (state==State::IDLE || state==State::STOPPED) { Serial.println("[CMD] HOME requested -> switching to HOMING"); state = State::HOMING; } else Serial.println("[CMD] HOME ignored (bad state)"); } else if (msg.startsWith("MOVE:")) { int n = msg.substring(5).toInt(); if (n<=0) n = targetTurns; hallPulses = 0; targetTurns = n; Serial.println("[CMD] MOVE requested -> targetTurns=" + String(targetTurns)); state = State::MOVING; } else if (msg.equalsIgnoreCase("MOVE")) { hallPulses = 0; Serial.println("[CMD] MOVE (use saved targetTurns=" + String(targetTurns) + ")"); state = State::MOVING; } else if (msg.equalsIgnoreCase("STOP")) { Serial.println("[CMD] STOP requested"); state = State::STOPPED; servoStop(); } else if (msg.startsWith("SET_TARGET:")) { int v = msg.substring(11).toInt(); if (v>0) { targetTurns = v; eepromWriteInt(EEPROM_OFF_TARGET, targetTurns); EEPROM.commit(); Serial.println("[CMD] SET_TARGET saved -> " + String(targetTurns)); mqttPublishTopic("debug","TARGET_UPDATED"); } } else { Serial.println("[MQTT] Unknown command: " + msg); } } // -------------- MQTT connect try -------------- void mqttTryConnect() { if (mqtt.connected()) return; if (strlen(mqttHost) == 0) { Serial.println("[MQTT] No broker configured, skip connect"); return; } mqtt.setServer(mqttHost, mqttPort); Serial.println("[MQTT] Connecting to " + String(mqttHost) + ":" + String(mqttPort)); if (mqtt.connect(DEVICE_NAME, mqttUser, mqttPass)) { String cmdTopic = mqttBaseTopic + "/cmd"; mqtt.setCallback(mqttCallback); mqtt.subscribe(cmdTopic.c_str()); Serial.println("[MQTT] Connected and subscribed to " + cmdTopic); mqttPublishTopic("debug","connected"); } else { Serial.println("[MQTT] Connect failed"); } } // -------------- Web handlers -------------- void handleRoot() { Serial.println("[WEB] / requested"); String html = "<html><head><meta name='viewport' content='width=device-width'><title>" + String(DEVICE_NAME) + "</title></head><body>"; html += "<h3>" + String(DEVICE_NAME) + "</h3>"; html += "<p>State: " + String((int)state) + "</p>"; html += "<p>Homed: " + String(homed ? \"yes\" : \"no\") + "</p>"; html += "<p>Pulses: " + String(hallPulses) + "</p>"; html += "<p><button onclick='fetch(\"/cmd?c=HOME\")'>HOME</button> <button onclick='fetch(\"/cmd?c=MOVE\")'>MOVE</button> <button onclick='fetch(\"/cmd?c=STOP\")'>STOP</button></p>"; html += "<h4>MQTT Settings</h4>"; html += \"<form method='POST' action='/setmqtt'>Broker: <input name='broker' value='\" + String(mqttHost) + \"' size=30><br>Port: <input name='port' value='\" + String(mqttPort) + \"' size=6><br>User: <input name='user' value='\" + String(mqttUser) + \"' size=20><br>Pass: <input name='pass' value='' size=20><br>Topic base: <input name='topic' value='\" + mqttBaseTopic + \"' size=30><br><input type='submit' value='Save'></form>\"; html += \"</body></html>\"; server.send(200, "text/html", html); } void handleCmdWeb() { Serial.println("[WEB] /cmd called"); if (!server.hasArg("c")) { server.send(400, "text/plain", "no cmd"); return; } String c = server.arg("c"); if (c == "HOME") { Serial.println("[WEB] HOME"); state = State::HOMING; server.send(200, "text/plain", "HOMING"); } else if (c == "MOVE") { Serial.println("[WEB] MOVE"); hallPulses = 0; state = State::MOVING; server.send(200, "text/plain", "MOVING"); } else if (c == "STOP") { Serial.println("[WEB] STOP"); state = State::STOPPED; servoStop(); server.send(200, "text/plain", "STOPPED"); } else server.send(400, "text/plain", "unknown"); } void handleSetMqtt() { Serial.println("[WEB] /setmqtt POST"); if (!server.hasArg("broker")) { server.send(400, "text/plain", "missing"); return; } String b = server.arg("broker"); String p = server.arg("port"); String u = server.arg("user"); String pw = server.arg("pass"); String t = server.arg("topic"); b.toCharArray(mqttHost, sizeof(mqttHost)); mqttPort = (uint16_t)p.toInt(); u.toCharArray(mqttUser, sizeof(mqttUser)); pw.toCharArray(mqttPass, sizeof(mqttPass)); mqttBaseTopic = t; eepromWriteString(EEPROM_OFF_MQTT_HOST, mqttHost, 64); eepromWriteInt(EEPROM_OFF_MQTT_PORT, mqttPort); eepromWriteString(EEPROM_OFF_MQTT_USER, mqttUser, 64); eepromWriteString(EEPROM_OFF_MQTT_PASS, mqttPass, 64); EEPROM.commit(); Serial.println("[WEB] Saved MQTT config to EEPROM: host=" + String(mqttHost) + " port=" + String(mqttPort)); server.send(200, "text/plain", "Saved"); mqttTryConnect(); } // -------------- Setup -------------- void setup() { Serial.begin(115200); delay(50); Serial.println("\n\n[BOOT] lampe_ikea2014 starting..."); EEPROM.begin(EEPROM_SIZE); eepromReadString(EEPROM_OFF_MQTT_HOST, mqttHost, sizeof(mqttHost)); mqttPort = (uint16_t)eepromReadInt(EEPROM_OFF_MQTT_PORT); if (mqttPort == 0) mqttPort = 1883; eepromReadString(EEPROM_OFF_MQTT_USER, mqttUser, sizeof(mqttUser)); eepromReadString(EEPROM_OFF_MQTT_PASS, mqttPass, sizeof(mqttPass)); int storedTarget = (int)eepromReadInt(EEPROM_OFF_TARGET); if (storedTarget > 0) targetTurns = storedTarget; Serial.println("[BOOT] EEPROM:"); Serial.println(String("[BOOT] mqttHost: ") + (strlen(mqttHost)?mqttHost:"<empty>")); Serial.println(String("[BOOT] mqttPort: ") + String(mqttPort)); Serial.println(String("[BOOT] mqttUser: ") + (strlen(mqttUser)?mqttUser:"<empty>")); Serial.println(String("[BOOT] targetTurns: ") + String(targetTurns)); // pins & ISRs pinMode(PIN_LIMIT, INPUT_PULLUP); pinMode(PIN_HALL, INPUT_PULLUP); pinMode(PIN_PIR, INPUT); attachInterrupt(digitalPinToInterrupt(PIN_HALL), hallISR, FALLING); attachInterrupt(digitalPinToInterrupt(PIN_LIMIT), limitISR, FALLING); Serial.println("[BOOT] Pins configured and ISRs attached"); // NeoPixel pixels.begin(); pixels.setBrightness(120); clearAll(); Serial.println("[BOOT] NeoPixel ready"); // servo initial state: neutral & detached servoMotor.attach(PIN_SERVO, 1000, 2000); servoMotor.writeMicroseconds(SERVO_STOP_US); delay(5); servoMotor.detach(); servoAttached = false; Serial.println("[BOOT] Servo initialized (neutral & detached)"); // WiFiManager (blocking until wifi configured or known) Serial.println("[BOOT] Starting WiFiManager (autoConnect)..."); wifiManager.autoConnect(DEVICE_NAME); Serial.println("[BOOT] WiFi connected, IP: " + WiFi.localIP().toString()); // web + OTA httpUpdater.setup(&server); server.on("/", handleRoot); server.on("/cmd", handleCmdWeb); server.on("/setmqtt", HTTP_POST, handleSetMqtt); server.begin(); Serial.println("[BOOT] Webserver + OTA ready"); // MQTT mqtt.setClient(wifiClient); mqtt.setCallback(mqttCallback); mqttTryConnect(); // heartbeat heartbeatTicker.attach(1.0, heartbeat); Serial.println("[BOOT] Heartbeat started"); // start homing if needed (non-blocking) if (digitalRead(PIN_LIMIT) == HIGH) { state = State::HOMING; Serial.println("[BOOT] Limit not pressed -> start HOMING state"); } else { homed = true; state = State::IDLE; Serial.println("[BOOT] Limit pressed -> already homed, state IDLE"); } } // -------------- Main loop (non-blocking FSM) -------------- void loop() { server.handleClient(); if (!mqtt.connected()) mqttTryConnect(); else mqtt.loop(); 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("[LIMIT] Valid limit event (debounced)"); // 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::HOMING) { // set hallPulses to 0 at home hallPulses = 0; homed = true; servoStop(); state = State::IDLE; Serial.println("[HOMING] Completed at boot or runtime -> homed=true, state=IDLE"); mqttPublishTopic("debug","HOMED"); } else { // limit pressed unexpected Serial.println("[LIMIT] limit hit while not homing (state=" + String((int)state) + ")"); } } else { Serial.println("[LIMIT] Ignored bounce (within debounce window)"); } } // state machine static State lastState = State::ERROR; // force initial print if (lastState != state) { Serial.println("[STATE] " + String((int)lastState) + " -> " + String((int)state)); lastState = state; } switch (state) { case State::IDLE: // ensure servo detached (no PWM) servoStop(); // do nothing else break; case State::HOMING: // run servo up until limit interrupt occurs (limitRawTriggered processed above) // ensure servo is running up servoUp(); break; case State::MOVING: // run down until pulses reached or error servoDown(); { long goal = (long)targetTurns * pulsesPerRevolution; if ((long)hallPulses >= goal) { Serial.println("[MOVING] Goal reached: pulses=" + String(hallPulses) + " goal=" + String(goal)); servoStop(); state = State::IDLE; mqttPublishTopic("debug","MOVED"); } // stall detection if (millis() - lastPulseTimeMs > STALL_TIMEOUT_MS) { Serial.println("[ERROR] Stall detected: no hall pulses for " + String(STALL_TIMEOUT_MS) + " ms"); servoStop(); state = State::ERROR; mqttPublishTopic("error","stall_detected"); } // safety: if limit is pressed unexpectedly -> error if (digitalRead(PIN_LIMIT) == LOW) { Serial.println("[ERROR] Limit hit while moving -> emergency stop"); servoStop(); state = State::ERROR; mqttPublishTopic("error","limit_hit_during_move"); } } break; case State::STOPPED: servoStop(); // stay stopped until command break; case State::ERROR: // motor stopped; require manual HOME to recover servoStop(); // optionally blink LEDs to indicate error setRange(GROUP1_START, GROUP1_COUNT, col(16,0,0)); // red top group 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("[HALL] Pulse detected, total=" + String(hallPulses) + " lastPulseTimeMs=" + String(lastPulseTimeMs)); // publish per-pulse (optional) mqttPublishTopic("pulse", String(hallPulses).c_str()); } yield(); }