Moritz Marquardt IT-Projekte zwischen Embedded & Web

Erste Schritte mit dem ESP8266 und NodeMCU

Der ESP8266 ist ein ziemlich cooles Ding - für etwa 3€ bekommt man hier einen kleinen programmierbaren Chip mit WLAN, der sich also ideal für IoT-Projekte anbietet. Mit einem relativ schnellen Prozessor und genügend RAM kann er für seinen Preis und seine Größe um einiges mehr als man denkt, und ersetzt damit für viele kleinere Projekte locker einen Raspberry Pi Zero oder einen Arduino. Viele Infos zu den technischen Daten haben das microcontroller.net-Wiki und Wikipedia.

Versionen

Es sind verschiedene Versionen erhältlich, über die man im offiziellen ESP8266-Wiki mehr erfahren kann - hierzulande findet man meistens den ESP-01 (bei dem leider nur 2 GPIO-Anschlüsse zugänglich sind) und ESP-12. Ziemlich sinnvoll zum Ausprobieren ist das NodeMCU-Board für etwa 5-10€ je nach Händler, da hier schon ein USB-Seriell-Konverter dabei ist und die Funktionen des Chips mit zwei Knöpfen gesteuert werden können, anstatt dauernd Kabel umzustecken - wie das funktioniert und wie die unterschiedlichen Versionen beschaltet werden müssen erkläre ich in einem späteren Post, darum bleiben wir der Einfachheit halber fürs erste beim NodeMCU:

Layout des NodeMCU-Entwicklerboards

Bild von nodemcu/nodemcu-devkit-v1.0

Verschiedene Firmwares

An sich kann man mit der GCC-Toolchain für den ESP eine eigene Firmware schreiben. Standardmäßig ist meistens die AT-Firmware aufgespielt, mit der beispielsweise ein Arduino per UART um WLAN erweitert werden kann. Will man den programmierbaren Chip direkt verwenden, empfiehlt sich die NodeMCU-Firmware (mit der man für die Programmierung die Skriptsprache Lua verwenden kann), die man sich auf dieser Seite mit den Modulen die man braucht konfigurieren kann.

Wie aktualisiere ich die Firmware auf meinem ESP8266?!

Benötigte Software

Das esptool wird zum Flashen benötigt, dazu empfehle ich minicom um über die serielle Schnittstelle mit dem Chip kommunizieren zu können. Mit dem nodemcu-uploader können wir später auf das Dateisystem unserer Lua-Firmware zugreifen.

$ sudo apt install -y python-pip python3-pip minicom
$ sudo pip3 install esptool
$ sudo pip2 install nodemcu-uploader

Wenn alles geklappt hat, sollte sudo esptool.py --port /dev/ttyUSB? chip_id nun die ID des Chips ausgeben

Damit wir nicht immer sudo verwenden müssen, können wir den Benutzer zur dialout-Gruppe hinzufügen - damit das auch funktioniert müssen wir uns danach jedoch erneut anmelden:

$ sudo adduser $USER dialout

Firmware flashen

Ich verwende einfach eine per NodeMCU Cloud-Build-Tool erstellte Firmware, selbstverständlich kann man sich diese auch lokal zusammenbauen aber das ist nicht ganz unkompliziert. Nachdem also per E-Mail die kompilierte Firmware angekommen ist, können wir (gemäß der ausgezeichneten NodeMCU-Dokumentation, hier finden sich auch Informationen wann im zweiten Befehl qio statt dio verwendet werden muss) wiefolgt die Datei auf den ESP-Chip bringen:

$ esptool.py -p /dev/ttyUSB? erase_flash
esptool.py v1.3
Connecting....
Running Cesanta flasher stub...
Erasing flash (this may take a while)...
Erase took 9.2 seconds
$ esptool.py -p /dev/ttyUSB? write_flash -fm dio 0x00000 downloaded-firmware.bin
esptool.py v1.3
Connecting....
Auto-detected Flash size: 32m
Running Cesanta flasher stub...
Flash params set to 0x0240
Wrote 413696 bytes at 0x0 in 35.9 seconds (92.3 kbit/s)...
Leaving...

Die NodeMCU-Firmware

Wenn wir uns jetzt mit folgendem Befehl mit dem Chip verbinden und den Reset-PIN ansteuern (bzw. beim NodeMCU den Reset-Knopf drücken), können wir auf die neue Firmware zugreifen:

$ minicom -b 115200 -D /dev/ttyUSB?
NodeMCU custom build by frightanic.com
        branch: master
        commit: b96e31477ca1e207aa1c0cdc334539b1f7d3a7f0
        SSL: false
        modules: dht,file,gpio,http,net,node,ow,rtctime,tmr,uart,wifi
 build  built on: 2017-02-17 11:36
 powered by Lua 5.1.4 on SDK 2.0.0(656edbf)
lua: cannot open init.lua
> print("Hello World")
Hello World
>

Mit Strg+A und danach Q kann minicom wieder verlassen werden.

Dateien schreiben

Die Firmware hat eine Art Dateisystem, in das wir unsere Lua-Dateien schreiben müssen:

$ echo 'print("Hello World!")' > file.lua
$ nodemcu-uploader upload --compile file.lua

Vorsicht: das funktioniert nicht wenn man in einem anderen Fenster noch minicom offen hat!

Die so erstellte Datei (dank des --compile-Flags nicht file.lua sondern file.lc) können wir jetzt (wenn wir uns wieder via minicom verbinden) mit dofile("test.lc") ausführen - oder alternativ direkt mit nodemcu-uploader file do test.lc.

Um den Upload aller Dateien aus einem Verzeichnis zu vereinfachen verwende ich recht gerne folgendes Skript:

#!/bin/sh
NODEMCUCOMMAND=nodemcu-uploader -p /dev/ttyUSB?

find * -type f -name '*.lua' -exec $NODEMCUCOMMAND upload --compile {} +
find * -type f -not -name '*.lua' -not -name '.*' -not -path 'Makefile' -exec $NODEMCUCOMMAND upload {} +

Als /usr/local/bin/nodemcu-upload-all gespeichert und mit chmod a+x /usr/local/bin/nodemcu-upload-all als ausführbar markiert lässt sich nun jedes Verzeichnis mit nodemcu-upload-all auf den ESP8266 hochladen.

Eine stabile init.lua

init.lua ist die wichtigste Datei in NodeMCU - ist sie kaputt, startet der Chip immer wieder neu, und der einzige Ausweg ist ein erneutes Flashen der Firmware, was jedes mal ungefähr eine Minute dauert. Um das zu umgehen verwende ich folgende init-Datei, mit dem schönen Nebeneffekt dass das eigentliche Hauptskript um einiges übersichtlicher wird - schließlich müssen wir nicht mehr extra auf die WLAN-Verbindung warten:

print("ESP8266 booted. Reason: " .. node.bootreason())
-- Check for GPIO14 override
gpio.mode(5, gpio.INPUT, gpio.PULLUP)
if gpio.read(5) == 0 then
  print("D5 is low (connected to GND), not doing anything.")
else
  print("D5 is high (not connected to GND), continuing as normal.")

  -- Connect to WiFi or stop execution for WiFi setup
  wifi.setmode(wifi.STATION)
  local wifi_config = wifi.sta.getdefaultconfig(true)
  -- Load wifi config from wifi.lua
  if (wifi_config.ssid == "" and (wifi_config.bssid == nil or wifi_config.bssid == "ff:ff:ff:ff:ff:ff") and file.exists("wifi.lc")) then
    print("Using Wifi configuration from wifi.lc.")
    dofile("wifi.lc")
  elseif (wifi_config.ssid == "" and (wifi_config.bssid == nil or wifi_config.bssid == "ff:ff:ff:ff:ff:ff") and file.exists("wifi.lua")) then
    print("Using Wifi configuration from wifi.lua.")
    dofile("wifi.lua")
  elseif (wifi_config.ssid == "" and (wifi_config.bssid == nil or wifi_config.bssid == "ff:ff:ff:ff:ff:ff"))
    print("No stored Wifi configuration and no wifi.lua or wifi.lc. Please configure Wifi for this node.")
  else
    print("Using stored Wifi configuration.")
  end
  wifi_config = wifi.sta.getdefaultconfig(true)
  if (wifi_config.ssid == "" and (wifi_config.bssid == nil or wifi_config.bssid == "ff:ff:ff:ff:ff:ff")) then
    print("Wifi is not configured. Please manually set a default configuration.")
  else
    if (wifi_config.auto ~= true and wifi.sta.status() ~= 5) then
      wifi.sta.connect() -- Connect automatically even if "auto" is not set.
    end

    -- Wait for connection...
    if (wifi_config.ssid == "") then
      print("Waiting for connection to \"" .. wifi_config.bssid .. "\"...")
    else
      print("Waiting for connection to " .. wifi_config.ssid .. "...")
    end
    tmr.create():alarm(50, tmr.ALARM_AUTO, function(timer)
      if wifi.sta.status() ~= wifi.STA_CONNECTING then

        -- Not connecting anymore. Find out if it was success or error.

        local wifi_types = {"STA_CONNECTING", "STA_WRONGPWD", "STA_APNOTFOUND",
                            "STA_FAIL", "STA_GOTIP"}
        wifi_types[0] = "STA_IDLE"
        if (wifi_types[wifi.sta.status()] == nil) then
          wifi_types[wifi.sta.status()] = "UNKNOWN (" .. wifi.sta.status() .. ")"
        end

        -- Try to get an IP, show the connection status.
        ip = wifi.sta.getip()
        if (ip ~= nil) then
          print("Connection status: " .. wifi_types[wifi.sta.status()] ..
                " (IP: " .. ip .. ")")

          -- Run start file after successful network connection
          if (file.exists("start.lc")) then
            dofile("start.lc")
          elseif (file.exists("start.lua")) then
            dofile("start.lua")
          end
        else -- No IP, no success... :(
          print("Connection status: " .. wifi_types[wifi.sta.status()])
          print("Connection failed. Rebooting in 30 seconds.")
          tmr.create():alarm(30000, tmr.ALARM_SINGLE, function(timer)
            node.restart()
          end)
        end

        tmr.unregister(timer)

      end
    end)
  end
end

Update 31.3.2018: Das Skript kann jetzt die WiFi-Konfiguration aus wifi.lua lesen und gibt ausführlichere Logs aus wenn die Konfiguration nicht gesetzt ist.

Was tut das?

  • Das Skript erwartet, dass die WiFi-Konfiguration...
    • ...entweder bereits auf dem Chip hinterlegt ist. Das erledigen wir wiefolgt in der seriellen Konsole:
      wifi.sta.config({ ssid = "mein-wlan", pwd = "mein-wlan-schlüssel", auto = true, save = true })
    • ...oder in der Datei wifi.lua (oder wifi.lc) steht:
      local wifi_config = {
      ssid = "mein-wlan",
      pwd = "mein-wlan-schlüssel",
      auto = true,
      save = true
      }
      wifi.sta.config(wifi_config)
  • Sobald die WLAN-Verbindung erfolgreich hergestellt wurde, wird die Datei start.lua ausgeführt. Das ist damit unsere neue init.lua, in der Dinge kaputt gehen dürfen.
  • Solange die WLAN-Verbindung fehlschlägt versucht der Chip es alle 30 Sekunden neu.
  • Sollte die start.lua den Chip zum crashen bringen (oder das WLAN falsch konfiguriert sein, usw.), können wir den Pin D5 (bzw. GPIO14 wenn man nicht das NodeMCU-Board verwendet) mit dem danebenliegenden Ground-Pin verbinden. Damit landet der Chip nach einem Reset direkt in der Eingabezeile und führt keine weiteren Dateien aus.

Hallo, Welt!

Mit einer Firmware mit dem Modul HTTP (sowie den von vornherein ausgewählten) können wir jetzt ganz einfach eine HTTP-Anfrage senden:

http.get("http://echo.jsontest.com/Hello/World", nil, function(code, data)
  print("HTTP " .. code)
  print(data)
end)

Diese Datei speichern wir in einem leeren Ordner zusammen mit unserer init.lua von vorhin, nennen sie start.lua und laden sie mit folgenden Befehlen auf den Chip hoch und führen sie aus:

$ nodemcu-upload-all
opening port /dev/ttyUSB0 with 115200 baud
Preparing esp for transfer.
[…]
All done!
$ minicom -b 115200 -D /dev/ttyUSB?
> dofile("start.lc")
HTTP 200
{"Hello": "World"}

Jedes mal wenn der ESP8266 von nun an bootet wird die Anfrage an echo.jsontest.com ausgeführt und das Ergebnis ausgegeben.

Der Anfang (und das Schlusswort)

Huch - auf einmal hab ich wieder einen Blog! In Zukunft gibt's hier querbeet alles was irgendetwas mit Computern zu tun haben könnte - Linux, Webentwicklung, Elektronik, und so weiter und so fort. Bei mehr Ideen: einfach her damit! :D
Zum ESP8266 kommen auf jeden Fall noch ein paar mehr Projekte, Beispiele für die Beschaltung des Chips und mehr allgemeine Infos (vor allem zu den anderen Typen), ich hoffe aber das hier war schon mal recht aufschlussreich für die Einrichtung - für alles weitere ist als ersten Anlaufpunkt die NodeMCU-Dokumentation glücklicherweise unglaublich gut.

Nächster Beitrag