# ###################################################################### # # main.py – WILO PUMPE STEUERUNG (Mit Tacho-Timeout auf 0 µs korrigiert) # ###################################################################### import time import gc import network from machine import Pin, PWM, Timer, WDT, ADC from umqtt.simple import MQTTClient import sys import os import ujson import utime # ==================== KONFIGURATION ==================== WATCHDOG_ENABLED = True WATCHDOG_TIMEOUT = 8000 # ms PWM_MIN_HARD = 0 PWM_MAX = 64000 RAMP_DURATION = 16.0 # Sekunden für sanfte Rampe TARGET_PWM = 29000 INTERVAL_SECONDS = 900 # 15 Minuten BOOST_DURATION = 5 # 5 Sekunden # RPM-BERECHNUNG: Wilo Para ST (Annahme 8 Pulse/Umdrehung) PULSES_PER_REVOLUTION = 8 TACHO_TIMEOUT_MS = 50 # 50 ms Timeout. Setzt Tacho-Werte auf 0. mqtt_server = '192.168.178.23' client_id = 'picow2' topic_sub_pump = b'heatp/pump' topic_pub = b'heatp/pico120' topic_pins = b'heatp/pins' FIRMWARE_VERSION = "v2.3-final-tacho-fix" start_time = time.time() # ==================== GLOBALE VARIABLEN ==================== current_pwm = PWM_MAX target_pwm = PWM_MAX ramp_start_time = None ramp_start_value = None last_boost_start = 0 boost_active = False timers = [] client = None # GLOBALE VARIABLEN FÜR PIN 5 FLANKENMESSUNG last_pin5_time_us = utime.ticks_us() pin5_flank_time_us = 0 pin5_high_time_us = 0 pin5_low_time_us = 0 pump_frequency_hz = 0.0 pump_rpm = 0 last_pulse_time_us = utime.ticks_us() # Zeit des letzten Pulses # ==================== FLANKENZEIT-MESSUNG CALLBACK ==================== def pin5_callback(pin): """Speichert die Dauer der HIGH- und LOW-Flanke separat.""" global last_pin5_time_us, pin5_flank_time_us, pin5_high_time_us, pin5_low_time_us, last_pulse_time_us current_time_us = utime.ticks_us() time_diff_us = utime.ticks_diff(current_time_us, last_pin5_time_us) # Filtert Rauschen (kurze Pulse) if time_diff_us > 100: # Wenn der Pin JETZT HIGH ist, hat gerade der LOW-Zustand geendet if pin.value() == 1: pin5_low_time_us = time_diff_us # Wenn der Pin JETZT LOW ist, hat gerade der HIGH-Zustand geendet else: pin5_high_time_us = time_diff_us pin5_flank_time_us = time_diff_us last_pulse_time_us = current_time_us # Letzter Puls aufgezeichnet last_pin5_time_us = current_time_us # ==================== HARDWARE & PIN-DEFINITIONEN ==================== pwm0 = PWM(Pin(0), freq=150) pwm0.duty_u16(PWM_MAX) LED = Pin("LED", Pin.OUT) LED.on() feedback_pin7 = Pin(7, Pin.IN, Pin.PULL_UP) test_pin1 = Pin(1, Pin.IN, Pin.PULL_UP) pump_feedback_pin19 = Pin(19, Pin.IN) feedback_pin5 = Pin(5, Pin.IN, Pin.PULL_UP) # Interrupt-Konfiguration für PIN 5 (misst jede Flanke) feedback_pin5.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=pin5_callback) # ADC Pins adc26 = ADC(Pin(26)) adc27 = ADC(Pin(27)) adc28 = ADC(Pin(28)) # WLAN Setup sta = network.WLAN(network.STA_IF) sta.active(True) sta.connect("f24", "9876543210") # Watchdog Setup if WATCHDOG_ENABLED: wdt = WDT(timeout=WATCHDOG_TIMEOUT) def feed_watchdog(): if WATCHDOG_ENABLED and wdt: wdt.feed() # ---------------------------------------------------------------------- ## 🔗 MQTT LOGGING und Hilfsfunktionen # ---------------------------------------------------------------------- def mqtt_log(msg): try: if client: ts = time.localtime() payload = f"{ts[3]:02d}:{ts[4]:02d}:{ts[5]:02d} - {msg}" client.publish(b'heatp/log', payload.encode()) except: pass feed_watchdog() def get_mem_percent(): try: gc.collect() return int(gc.mem_alloc() / (gc.mem_alloc() + gc.mem_free()) * 100) except: return 0.0 def read_adc_voltage(adc): try: raw = adc.read_u16() return round((raw / 65535) * 3.3, 3) except: return 0.0 # ---------------------------------------------------------------------- ## 📊 MQTT-Payload-Erzeugung (Mit Tacho-Timeout & JSON-Fix) # ---------------------------------------------------------------------- def publish_all_pins(t): global pump_frequency_hz, pump_rpm, last_pulse_time_us # ZUSÄTZLICH: Zugriff auf alle globalen Tacho-Variablen für Timeout-Reset global pin5_flank_time_us, pin5_high_time_us, pin5_low_time_us # --- TACHO TIMEOUT CHECK --- now_us = utime.ticks_us() time_since_last_pulse_ms = utime.ticks_diff(now_us, last_pulse_time_us) / 1000 # Wenn seit dem letzten Puls mehr Zeit vergangen ist als das Timeout erlaubt, ist die Pumpe AUS if time_since_last_pulse_ms > TACHO_TIMEOUT_MS: pump_frequency_hz = 0.0 pump_rpm = 0 # KORREKTUR: Globale Tacho-Variablen auf 0 setzen, um 0 µs im JSON zu senden. pin5_flank_time_us = 0 pin5_high_time_us = 0 pin5_low_time_us = 0 T_us_local = 0 # Local T_us for legacy short status # --- RPM-BERECHNUNG (NUR wenn kein Timeout) --- else: T_us_local = pin5_flank_time_us if T_us_local > 0: # Vermeide Division durch Null, falls T_us unerwartet Null ist try: freq = 1.0 / (T_us_local * 0.000001) pump_frequency_hz = round(freq, 2) divisor_factor = 60.0 / PULSES_PER_REVOLUTION rpm = int(divisor_factor / (T_us_local * 0.000001)) pump_rpm = rpm except: pump_frequency_hz = 0.0 pump_rpm = 0 else: # Fallback, sollte durch Timeout abgefangen werden pump_frequency_hz = 0.0 pump_rpm = 0 # --- ENDE RPM-BERECHNUNG --- try: ip = sta.ifconfig()[0] if sta.isconnected() else "0.0.0.0" wlan_status = 1 if sta.isconnected() else 0 mem_pct = get_mem_percent() uptime = int(time.time() - start_time) mp_version = f"{os.uname().sysname} v{os.uname().version.split()[0]}" build_date = os.uname().version.split(';')[1].strip() if ';' in os.uname().version else "unknown" machine = os.uname().machine full_fw = f"{FIRMWARE_VERSION} | {mp_version} | {machine} | {build_date}" pins = { "FW": full_fw, "UPTIME": uptime, "WLAN": wlan_status, "LED": LED.value(), "PWM": current_pwm, "PIN0": current_pwm, "PIN1": test_pin1.value(), "PIN7": feedback_pin7.value(), # TACHO / FEEDBACK (Jetzt 0 bei Timeout) "PIN5": feedback_pin5.value(), "PIN5_Flank_us": pin5_flank_time_us, "PIN5_HIGH_us": pin5_high_time_us, "PIN5_LOW_us": pin5_low_time_us, "PIN5_Freq_Hz": pump_frequency_hz, "PumpRPM": pump_rpm, "PIN19": pump_feedback_pin19.value(), "PumpFeedback": pump_feedback_pin19.value(), # ADC PINS "PIN26": read_adc_voltage(adc26), "PIN27": read_adc_voltage(adc27), "PIN28": read_adc_voltage(adc28), } # Schleife über die restlichen GPIOs for gp in [2,3,4,6,8,9,10,11,12,13,14,15,16,17,18,20,21]: if gp in (5, 19): continue try: p = Pin(gp, Pin.IN) val = p.value() p.deinit() pins[f"PIN{gp}"] = val except: pins[f"PIN{gp}"] = 0 # JSON-Serialisierung json_str = ujson.dumps(pins) client.publish(topic_pins, json_str.encode()) # Statusmeldung (kurze Form) pin7_state = "LOW" if feedback_pin7.value() == 0 else "HIGH" pin5_state = "LOW" if feedback_pin5.value() == 0 else "HIGH" pump_state = "LOW" if pump_feedback_pin19.value() == 0 else "HIGH" status = f"PWM:{current_pwm},IP:{ip},MEM:{mem_pct}%,PIN7:{pin7_state},PIN5:{pin5_state},PUMP:{pump_state},T_US:{pin5_flank_time_us}" client.publish(topic_pub, status.encode()) except Exception as e: mqtt_log(f"publish_all_pins error: {e}") feed_watchdog() # ---------------------------------------------------------------------- ## ⏱️ Steuerung und MQTT-Logik # ---------------------------------------------------------------------- def update_pwm_ramp(t): global current_pwm, target_pwm, ramp_start_time, ramp_start_value if ramp_start_time is None: if current_pwm == target_pwm: pwm0.duty_u16(current_pwm) return ramp_start_time = time.time() ramp_start_value = current_pwm elapsed = time.time() - ramp_start_time if elapsed >= RAMP_DURATION: current_pwm = target_pwm ramp_start_time = None pwm0.duty_u16(current_pwm) return progress = elapsed / RAMP_DURATION new_pwm = int(ramp_start_value + (target_pwm - ramp_start_value) * progress) new_pwm = max(PWM_MIN_HARD, min(PWM_MAX, new_pwm)) if new_pwm != current_pwm: current_pwm = new_pwm pwm0.duty_u16(current_pwm) def boost_cycle(t): global target_pwm, ramp_start_time, last_boost_start, boost_active now = time.time() if not boost_active and now - last_boost_start >= INTERVAL_SECONDS: # BOOST START mqtt_log("15-Min-Boost: 5s auf 100%") target_pwm = PWM_MAX ramp_start_time = None pwm0.duty_u16(PWM_MAX) LED.on() last_boost_start = now boost_active = True if boost_active and now - last_boost_start >= BOOST_DURATION: # BOOST ENDE mqtt_log(f"Boost Ende → 16s Rampe auf {TARGET_PWM}") target_pwm = TARGET_PWM ramp_start_time = None boost_active = False def sub_cb(topic, msg): global target_pwm, ramp_start_time, last_boost_start, boost_active try: if topic == topic_sub_pump: cmd = msg.decode().strip().lower() if cmd == "reset": mqtt_log("REMOTE RESET → Watchdog läuft aus in 8s") for t in timers: try: t.deinit() except: pass timers.clear() try: sta.active(False) except: pass while True: pass if cmd == "off": target_pwm = 0 ramp_start_time = None pwm0.duty_u16(0) LED.off() boost_active = False mqtt_log("Pumpe AUS") elif cmd in ("auto", ""): boost_active = True last_boost_start = time.time() - INTERVAL_SECONDS + 10 LED.on() mqtt_log("Auto reaktiviert") elif cmd.isdigit(): val = int(cmd) target_pwm = max(0, val) ramp_start_time = None boost_active = False if target_pwm > 0: LED.on() else: LED.off() mqtt_log(f"Manuell → {val}") elif cmd == "on": target_pwm = PWM_MAX ramp_start_time = None boost_active = False LED.on() mqtt_log("Manuell → 100%") else: mqtt_log(f"Unbekannt: {cmd}") publish_all_pins(None) feed_watchdog() except Exception as e: mqtt_log(f"sub_cb error: {e}") def mqtt_connect(): try: # ANNAHME: Broker braucht keine Auth c = MQTTClient(client_id, mqtt_server, keepalive=300) c.set_callback(sub_cb) c.connect() mqtt_log("MQTT verbunden") c.subscribe(topic_sub_pump) return c except: return None def reconnect(): global client for _ in range(5): client = mqtt_connect() if client: return feed_watchdog() mqtt_log("FATAL → Reset") while True: pass def add_timer(period, callback): t = Timer() t.init(period=period, mode=Timer.PERIODIC, callback=callback) timers.append(t) # ---------------------------------------------------------------------- ## 🚀 INIT & HAUPTSCHLEIFE # ---------------------------------------------------------------------- client = mqtt_connect() LED.on() while not sta.isconnected(): feed_watchdog() ip = sta.ifconfig()[0] mqtt_log(f"Start – IP: {ip} | FW: {FIRMWARE_VERSION}") publish_all_pins(None) add_timer(200, update_pwm_ramp) add_timer(1000, boost_cycle) add_timer(5000, publish_all_pins) add_timer(240000, lambda t: client.ping() if client else None) add_timer(3600000, lambda t: (gc.collect(), mqtt_log(f"GC: {gc.mem_free()}"))) while True: try: if client: client.check_msg() feed_watchdog() except Exception as e: mqtt_log(f"Error: {e}") reconnect()