Forum: Mikrocontroller und Digitale Elektronik PWM Webrowser Pumpenmonitor / Steuerung


von Schorsch Z. (schorsch_z)


Angehängte Dateien:

Lesenswert?

Falls jemand auch eine oder mehrere PWM Pumpen hat, hier mein neues 
kleines Projekt:

https://heissa.de/web1/control.php

cat main0.py
# ######################################################################
# # 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()

von N. M. (mani)


Lesenswert?

Als Anmerkung:
Stell den Code als Datei hoch.
Die Formatierung zerhaut alles.
Und gerade bei Python wo es auf die Formatierung ankommt...

von Schorsch Z. (schorsch_z)


Angehängte Dateien:

Lesenswert?

Nun als File, wegen Formatierung

von Schorsch Z. (schorsch_z)


Lesenswert?

Habe es in github eingestellt, das ist übersichtlicher:

https://github.com/gerontec/pwm_wilo

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.