Dies ist eine alte Version des Dokuments!


Ikea PS 2014 Lampe

https://github.com/julisa99/IKEAPS2014

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, der den Faden bewegt und Lampe schliesst. Öffnen durch Schwerkraft
  • 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

Verklabelung

  • LIMIT_SWITCH (oben) → D7
  • HALL_SENSOR (magnet) → D5
  • SERVO_SIGNAL → D4
  • NEOPIXEL_DATA → D2
  • 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

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();
}
 
Nach oben
ikea/ps2014.1764606695.txt.gz · Zuletzt geändert: von gerald
chimeric.de = chi`s home Creative Commons License Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0
DFmW2CEce3htPL1uNQuHUVu4Tk6WXigFQp   Dogecoin Donations Accepted Here    DFmW2CEce3htPL1uNQuHUVu4Tk6WXigFQp  DFmW2CEce3htPL1uNQuHUVu4Tk6WXigFQp