---

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)**

```rsc
/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)**

```rsc
/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**

```rsc
/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**

```rsc
/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)**

```rsc
/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):**

```python
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):**

```python
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.

```python
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**

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

**MikroTik: Lease prüfen**

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

**Rollback (falls nötig)**

```rsc
/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"]
```

