title: MikroTik ↔ ESP im Nullfeld
slug: mikrotik_esp_postmortem_nullfeld
lang: de
summary: Ehrliches Post-Mortem + Ein-Seiten-Handbuch: Wie wir den ESP sicher ins VLAN50 bekommen (ohne Endlos-Loops). Mit Einsicht, Minimal-Schritten, Checklisten und Code, der wirklich auf MicroPython lÀuft.
tags: [Crumbforest, ESP32, MikroTik, WLAN, VLAN, Thonny, Debian, macOS, Nullfeld, Haltung]


đŸŒČ MikroTik ↔ ESP im Nullfeld

Was wirklich passiert ist. Warum es hakte. Wie es jetzt stabil geht.

Warum dieser Text (Einsicht)

Ich habe dir zu oft „zweite Schritte“ gegeben, bevor der erste stand: Platzhalter-URLs, zu viele Varianten, Code mit PC-Python-Gewohnheiten (z. B. bytes.decode(errors=
)), und „kurz offen testen“ ohne sofortiges, klares Wieder-Absichern. Das erzeugt Loops, Stress und Energieverschwendung — genau das, was wir im Crumbforest vermeiden wollen.

Merksatz: Nicht jede Frage verlangt sofort eine Lösung. Aber jede Frage verlangt Respekt.
Heute: eine einzige, geprĂŒfte Minimal-Route, ohne Überraschungen.


TL;DR – Minimal-Route in 5 Schritten

  1. MikroTik (v6): virtuelles 2,4-GHz-AP (SSID ESP-Wald) auf Kanal 1 oder 11, sichtbar, WPA2-PSK/AES, VLAN-Tag 50.
  2. Bridge: vAP in Bridge, VLAN-Interface vlan50-esp, IP 192.168.50.1/24, DHCP an, NAT nach draußen.
  3. ESP (MicroPython): AP_IF aus, nur STA_IF. Scannen, dann verbinden. Kein errors= in decode().
  4. Thonny (macOS & Debian): Board „MicroPython (ESP32)“, serieller Port korrekt, auf Debian dialout-Gruppe nicht vergessen.
  5. Absichern & Logik: Fallback-AP am ESP aus, sauberes Passwort (ASCII), Heartbeat mit Backoff statt wilder Retries.

Was uns konkret ausgebremst hat (Post-Mortem)

  • SSID/Radio-Mismatches: 5 GHz, versteckte SSID, Kanal 12/13. ESP sieht dann „nichts“.
  • Sicherheitsprofil-Drift: mal offen, mal WPA-Mix; ESP mag WPA2-PSK / AES only.
  • boot.py-Loops: Auto-Connect beim Boot mit alten Parametern → „Wifi Internal Error“, Endlos-Reset.
  • MicroPython-Stolpersteine: bytes.decode(errors=
) existiert nicht; fĂŒhrt zu TypeError.
  • Zu viel auf einmal: VLAN, Offloader-Ideen, Browser-APIs, Placeholders → keine gerade Linie.
  • Energie-Kosten (CO₂): Jeder Loop, jedes sinnlose Re-Flashen ist vermeidbar, wenn die Reihenfolge stimmt.

1) MikroTik v6 – eine saubere, stabile Konfig

WinBox → New Terminal → die Blöcke nacheinander ausfĂŒhren.
SSID/Passwort bei Bedarf anpassen (nur ASCII, 8–63 Zeichen).

A. 2,4 GHz-Master (wlan1) fix & sichtbar (Kanal 1)

/interface wireless
set [find default-name=wlan1] band=2ghz-b/g/n mode=ap-bridge \
    ssid="MikroTik-2G" frequency=2412 channel-width=20mhz \
    hide-ssid=no country=germany frequency-mode=regulatory-domain disabled=no

B. Security-Profile (WPA2-PSK / AES)

/interface wireless security-profiles
:if ([:len [find where name="esp-sec"]] = 0) do={
  add name=esp-sec authentication-types=wpa2-psk unicast-ciphers=aes-ccm \
      group-ciphers=aes-ccm wpa2-pre-shared-key="espwald123" supplicant-identity="mikrotik"
} else={
  set esp-sec authentication-types=wpa2-psk unicast-ciphers=aes-ccm \
      group-ciphers=aes-ccm wpa2-pre-shared-key="espwald123"
}

C. Virtuelles AP + VLAN-Tag 50

/interface wireless
:if ([:len [find where name="wlan1-esp"]] = 0) do={
  add name=wlan1-esp master-interface=wlan1 ssid="ESP-Wald" \
      security-profile=esp-sec vlan-mode=use-tag vlan-id=50 disabled=no
} else={
  set wlan1-esp ssid="ESP-Wald" security-profile=esp-sec vlan-mode=use-tag vlan-id=50 disabled=no
}

D. vAP in Bridge, VLAN-Interface, IP, DHCP, NAT

/interface bridge port
:if ([:len [find where interface="wlan1-esp"]] = 0) do={ add bridge=bridge interface=wlan1-esp }

/interface vlan
:if ([:len [find where name="vlan50-esp"]] = 0) do={ add name=vlan50-esp interface=bridge vlan-id=50 }

/ip address
:if ([:len [find where interface="vlan50-esp" && address~"192.168.50.1/24"]] = 0) do={
  add address=192.168.50.1/24 interface=vlan50-esp comment="ESP-Wald GW"
}

/ip pool
:if ([:len [find where name="pool50"]] = 0) do={ add name=pool50 ranges=192.168.50.100-192.168.50.200 }

/ip dhcp-server
:if ([:len [find where name="dhcp50"]] = 0) do={
  add name=dhcp50 interface=vlan50-esp address-pool=pool50 lease-time=1h disabled=no
} else={
  set dhcp50 interface=vlan50-esp address-pool=pool50 disabled=no
}

/ip dhcp-server network
:if ([:len [find where address="192.168.50.0/24"]] = 0) do={
  add address=192.168.50.0/24 gateway=192.168.50.1 dns-server=192.168.50.1
}

/ip firewall nat
:if ([:len [find where chain=srcnat && out-interface=ether1 && action=masquerade]] = 0) do={
  add chain=srcnat out-interface=ether1 action=masquerade comment="ESP-Wald NAT"
}

E. (Optional) Trennung zum Admin-Netz (88/24)

/ip firewall filter
:if ([:len [find where chain=forward && src-address="192.168.50.0/24" && dst-address="192.168.88.0/24" && action=drop]] = 0) do={
  add chain=forward src-address=192.168.50.0/24 dst-address=192.168.88.0/24 action=drop comment="Isolate ESP-Wald from 88/24"
}

2) ESP (MicroPython) – wirklich minimal & robust

A. Auto-Loops abstellen (falls boot.py verbindet):

import os
try: os.rename('boot.py','boot.py.off')
except OSError: pass

B. Fallback-AP AUS, nur STA_IF, Scan anzeigen, verbinden (ohne PC-Python-Keywords):

import network, time

def b2s(b):
    try: return b.decode()
    except: 
        try: return "".join(chr(x) for x in b if 32 <= x < 127)
        except: return str(b)

SSID="ESP-Wald"
PWD ="espwald123"  # nach Router-Setup anpassen

ap  = network.WLAN(network.AP_IF); ap.active(False)
sta = network.WLAN(network.STA_IF); sta.active(True)
try: sta.disconnect()
except: pass
time.sleep(0.2)

nets = sta.scan() or []
print("🔎 Netze:", len(nets))
print("\n".join("  - {:<24} ch={} rssi={} auth={}".format(b2s(s[0]), s[2], s[3], s[4]) for s in nets))

sta.connect(SSID, PWD)
t0 = time.ticks_ms()
while not sta.isconnected() and time.ticks_diff(time.ticks_ms(), t0) < 20000:
    print("status:", sta.status()); time.sleep(0.5)

print("Result:", sta.isconnected(), sta.ifconfig() if sta.isconnected() else None)

Status-Orientierung (typisch ESP32/MicroPython):
0=IDLE, 1=CONNECTING, -2=NO_AP_FOUND, -3=WRONG_PASSWORD, 1010=GOT_IP.

C. Wenn „Wifi Internal Error“ bleibt:

  • USB stromlos (10 s), dann wieder an.
  • ap.active(False), sta.active(False); sta.active(True) erneut.
  • Als letzter Schritt: esptool.py erase_flash & MicroPython sauber flashen.

3) Thonny & Ports (macOS / Debian)

  • macOS: Thonny → unten rechts Port wĂ€hlen (/dev/cu.usbserial
), Interpreter: MicroPython (ESP32).
  • Debian:

  • User in die dialout-Gruppe: sudo usermod -aG dialout $USER && newgrp dialout

  • USB-Bridges: CH340/CP210x udev-Regeln oft schon dabei; notfalls dmesg checken (/dev/ttyUSB0/ACM0).
  • Thonny genau wie oben – MicroPython (ESP32) wĂ€hlen, Port setzen.
  • „Zwei Links, und plötzlich ging’s“: Sie fĂŒhren dich genau durch diese Minimal-Route (richtiger Interpreter, korrekter Port, Board-Auswahl), ohne Zusatz-Ablenkungen. Das war der Knackpunkt.

4) Checkliste gegen Loops (und fĂŒr weniger CO₂)

  • Vor dem ersten Versuch

  • SSID sichtbar, Kanal 1/11, WPA2-PSK AES only, ASCII-Passwort.

  • vAP tagged (VLAN 50), DHCP an, NAT gesetzt.
  • boot.py temporĂ€r deaktivieren.
  • Beim Verbinden

  • Fallback-AP am ESP aus (AP_IF False).

  • Scan prĂŒfen → ist die SSID wirklich da?
  • Erst verbinden → IP prĂŒfen → dann absichern/weiterbauen.
  • Fehlerbild → Maßnahme

  • NO_AP_FOUND → Kanal/Hidden/5 GHz prĂŒfen.

  • WRONG_PASSWORD → wirklich WPA2-PSK/AES? Passwort ASCII?
  • CONNECTING→Internal Error → AP_IF aus, STA neu, ggf. Reflash.
  • Energie sparen

  • Keine Endlos-Retries: Backoff (1→2→4
 s).

  • Scan nur bei Bedarf (nicht im 1-Sek-Loop).
  • „Offen testen“ nur kurz und sofort wieder sichern.

5) Mini-Heartbeat (HTTP) — erst mit konkreter URL nutzen

Nur Beispiel. Erst wenn du deinen Endpunkt hast (z. B. http://192.168.50.10:8080/ingest), einsetzen.

import time, ujson as json
try:
    import urequests as rq
except:
    rq = None

URL = "http://192.168.50.10:8080/ingest"  # ← DEIN konkreter Host/Path

def post(data, tries=3):
    if rq is None: return False
    wait=1
    for _ in range(tries):
        try:
            r=rq.post(URL, headers={"Content-Type":"application/json"}, data=json.dumps(data))
            sc=r.status_code; r.close()
            if 200 <= sc < 300: return True
        except: pass
        time.sleep(wait); wait=min(wait*2, 60)
    return False

while True:
    ok = post({"id":"esp32-forest","ts":time.time(),"kind":"heartbeat"})
    print("beat:", "ok" if ok else "fail")
    time.sleep(60)

Haltung (warum wir’s so machen)

  • Kinder zuerst: klare Reihenfolge, kein RĂ€tselraten.
  • Nullfeld-Disziplin: erst sehen, dann handeln.
  • Verantwortung: offene Netze nur explizit & kurz; Logs statt Mythen; weniger Loops = weniger CO₂.
  • Crew-Respekt: keine „0815“-Rezepte; wir testen das, was wir empfehlen.

Anhang – Schnellbefehle

ESP: Fallback-AP sicher aus

import network; network.WLAN(network.AP_IF).active(False)

MikroTik: Lease prĂŒfen

/ip dhcp-server lease print where address~"192.168.50."
/interface wireless registration-table print

Rollback (falls nötig)

/ip dhcp-server remove [find where name="dhcp50"]
/ip pool remove [find where name="pool50"]
/ip dhcp-server network remove [find where address="192.168.50.0/24"]
/ip address remove [find where interface="vlan50-esp"]
/interface vlan remove [find where name="vlan50-esp"]
/interface bridge port remove [find where interface="wlan1-esp"]
/interface wireless remove [find where name="wlan1-esp"]
/interface wireless security-profiles remove [find where name="esp-sec"]