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();
}