SELFSCRUM

Ein Lern-, Organisations- und Betriebssystem für alternative Schulen

Lua Controller programmieren

2021-03-08 Minetest

Lua-Controller bieten eine tolle Möglichkeit: Man kann sie im Spiel programmieren, so dass man Systeme aufbauen kann, Ereignisse verarbeiten und Schaltausgänge oder andere digitale Geräte steuern.

Lua Controller werden, wie der Name schon sagt, in Lua programmiert, das ist die Sprache, in der Minetest auch weitgehend selbst geschrieben ist.

Die Controller bieten 4 Ein- und Ausgänge (Port A bis D).

Auf die Eingänge kann über eine Ereignissteuerung reagiert werden, die Ausgänge können auf true oder false gesetzt werden, also an oder aus bei Mesecons.

Lua Controller sind auch noch aus einem anderen Grund wichtig - sie sind das Bindeglied zwischen Mesecons und Digilines. Letztere sind nicht einfach nur “Stromkabel” wie Mesecons, sondern sie können zum Austausch von Signalen benutzt werden. Dabei werden über Kanäle bestimmte Geräte abgefragt oder angesprochen. So kann man zum Beispiel im Spiel eine Digilines-Tastatur nutzen, um ein Passwort abzufragen. Wenn die Eingabe stimmt, kann man z.B. eine Falltür damit öffnen. So lassen sich schnell auch aufwändigere Steuerungen herstellen.

Leider ist der Code-Editor der Lua Controller nicht besonders komfortabel. Daher ist es am besten, wenn man sich den Code erst einmal in einer guten Entwicklungsumgebung (zum Beispiel Microsoft’s Visual Studio Code ) zurechtbaut und dann zum Testen ins Spiel kopiert.

Ausgänge

Wie gesagt, hat der Lua Controller 4 Ausgänge. Diese sind wie schon auf dem Controller zu sehen mit A bis D bezeichnet. Im Code nutzen wir kleine Buchstaben, um einen Ausgang zu aktivieren oder deaktivieren. Im Initialzustand sind alle 4 Ausgänge aus.

aus

Wenn wir jetzt im Code einen Port, wie die Ausgänge heißen, aktivieren, schaltet unsere Leuchte ein.

port.c = true

an

Ihr könnt auch mehrere Ports schalten. Dafür gibt es auch eine Kurzform.

port = {a=false, b=true, c=false, d=true} 

Allerdings werden die Ports erst am Ende der Code-Sequenz ausgewertet. Der folgende Code hat daher keine sichtbare Wirkung, wenn der Port C zu Beginn aus ist:

port.c = true
port.c = false

Die Ausgänge sind allein nicht besonders spannend. Damit eine sinnvolle Anwendung möglich ist, muss der Controller auf Ereignisse reagieren können. Bevor wir diese Dynamik erkunden, zunächst noch der Hinweis, dass der Zustand jedes Ports auch abgefragt werden kann. Das dazu gehörende Register heißt pin.

port.c = pin.a

Hier wird der Ausgang C gleich dem Eingang A gesetzt. Allerdings nur einmal beim initialen Ausführen. Spätere Zustandsänderungen an Eingang A werden von diesem Code noch nicht berücksichtigt. Dafür brauchen wir einen Auslöser.

Auslöser

Der einfachste Auslöser ist ein wiederkehrendes Signal, das den gespeicherten Code in Abständen ausführt. Dies nennt man Takt. Da bei “richtigen” Computern so ein Signal die reguläre Befehlsbearbeitung unterbricht, heißen solche Signale auch “interrupt”. So auch hier. Der folgende Code wird alle 2 Sekunden ausgeführt.

interrupt(2)
port.c = not pin.c

Könnt ihr euch vorstellen, was hier passiert? Die folgende Abbildung zeigt es.

blinken

Der Code kehrt also den Zustand von pin C durch das not um und setzt damit wieder port C. So entsteht der Blinkeffekt.

Damit haben wir auch den ersten Logikbefehl eingeführt, den ihr vielleicht schon über die anderen Mesecon-Würfel kennengelernt habt. Ein not kehrt das anliegende Signal um. Der Code hier funktioniert also genauso wie der NOT-Würfel. Ganz ähnlich gilt das auch für and und or, die jeweils zwei Signaleingänge nach einer bestimmten Logik verknüpfen. and ist am Ausgang an, wenn beide Eingänge an sind, während or an ist, sobald einer der beiden oder beide Eingänge an sind. Für die anderen Bausteine wie NAND, NOR, XOR gibt es keine direkte Code-Entsprechung, man kann sie aber durch logische Verknüpfungen auch im Code abbilden. Erklärungen dafür findet man reichlich im Internet, zum Beispiel hier. In Code sieht eine Logikschaltung so aus:

port.d = port.a and port.b
port.c = port.a or port.b

Jetzt wird es interessant - wir können durch den Takt bedingt die Ausgänge auch durch Signale an den Eingängen steuern lassen. Der Code prüft ja, wie die Eingänge sind. Das heißt, wenn wir am Eingang einen Schalter anbauen, folgt die Reaktion auch am Ausgang.

interrupt(2)
port.c = not pin.c
port.d = pin.b

Bemerkt ihr, dass sich das Blinken ändert, wenn man den Schalter betätigt? Irgendwie sorgen beide Auslöser anscheinend für die Ausführung des Codes. Vielleicht wäre es ja besser, wenn man das Intervall verkürzt? Dann ist die Verzögerung auch kleiner? Lua kann zum Glück auch mit Fließkommazahlen umgehen:

interrupt(0.1)
port.c = not pin.c
port.d = pin.b

Oh nein! Was ist jetzt passiert? Der Controller wird rot und reagiert nicht mehr?!

overheat

Was man hier sieht, ist ein “Overheat” Schutz. Wie bei echten Prozessoren wird die Leistung gebremst, wenn der Prozessor zu heiß wird. In Minetest hört der Lua Controller ganz auf zu arbeiten. Das ist nicht nur ein Modell eines echten Prozessors - wenn der Lua Controller mehr als 20 Befehle in der Sekunde ausführt, belastet das auch den Server ganz ordentlich. Daher schaltet das Minetest-Betriebssystem den Prozessor einfach ab. Aber keine Sorge - ist ja alles nur virtuell. Daher haben wir keinen Schmorgeruch in der Nase und mit dem nächsten Speichern des Codes ist der Prozessor wieder einsatzbereit. Die (virtuelle) Prozessortemperatur können wir übrigens abfragen. die Variable heat enthält den Wert der gerade gemessenen Ausführungsgeschwindigkeit.

Aber wie kommt man nun an die Ereignisse ohne Verzögerung? Auch dafür gibt es einen Mechanismus.

Ereignisse

Ereignis heißt auf Englisch “Event” und genauso heißt auch unser nächstes Konstrukt im Code. Es ermöglicht uns, auf Ereignisse von außen zu reagieren, statt selbst aktiv nachzufragen wie beim Interrupt. Außerdem haben wir so den Vorteil, dass wir gezielt auf verschiedene Ereignisse unterschiedlich reagieren können. Folgende Ereignistypen sind im Lua Controller vorgesehen:

TypHinweise
programEntsteht nur nach dem Ende des Codierens. Kann genutzt werden, um etwas einmalig zu initialisieren.
onTritt auf, wenn ein Pin aktiviert wurde. In event.pin.name steht dann der Port (als Großbuchstabe).
offTritt auf, wenn ein Pin deaktiviert wurde. In event.pin.name steht dann der Port (als Großbuchstabe).
interruptEntsteht, wenn der Interrupt-Timer abgelaufen ist. Optional steht in event.iid der Name des Timers, der den Interrupt ausgelöst hat.
digilineSignalisiert ein eintreffendes Datenpaket auf dem Digiline-Datenbus. in event.channel, steht, von welchem Kanal die Nachricht kommt und in event.msg steht die Nachricht selber. digiline-Events sind nicht an einen bestimmten Port gebunden!

Wir können mit dem event-Objekt nun gezielt auf diese Typen filtern und so die Reaktion unterscheiden. Mit diesem Code ändert sich das Blinken nicht mehr, wenn man den Schalter betätigt.

interrupt(2,"c_interrupt")
if event.type == "interrupt" and event.iid == "c_interrupt" then
  port.c = not pin.c
elseif event.pin.name == "B" then
  port.d = (event.type == "on")
end

Hier ist jetzt eine ganze Menge passiert. Zunächst hat unser interrupt einen zweiten Parameter bekommen. Da jetzt auf verschiedene Events gelauscht wird, braucht der Interrupt auch einen Namen, damit nichts durcheinandergerät. Dieser Name wird beim event auch abgefragt.

Falls es kein Interrupt mit diesem Namen war, wird nun geprüft, ob es ein event auf Pin B gab. Der Port D wird dann auf die logische Bedingung “wahr” gesetzt, wenn der Ereignistyp “on” war. Damit erzielen wir den gewünschten Effekt.

Dieses Beispiel verbirgt die Tatsache, dass wir beim Einsatz von Events keine Interrupts benötigen! Das Event-Objekt hat selber eine Steuerung eingebaut, die dafür sorgt, dass der Code aufgerufen wird, wenn ein Ereignis eintritt. Wenn wir also nur den Schalter abfragen wollen, wird der Code einfacher:

if event.pin.name == "B" then
  port.d = (event.type == "on")
end

Gedächtnis

So wie der Code jetzt aufgebaut ist, hat er kein Gedächtnis. Mal angenommen, wir wollen über einen Trittplatte zählen, wie viele Leute unsere Höhle besucht haben. Dann brauchen wir einen Speicher, der diese Information dauerhaft behält.

Man kann verschiedene Speicherstellen belegen, sie werden über mem, gefolgt vom Namen, angesprochen. Im folgenden Code heißt die Speicherstelle counter. Beim ersten Laden nach der Programmierung wird er auf 0 gesetzt. Danach zählt der Code jedes on-Ereignis und speichert es wieder im counter.

if event.type == "program" then
    mem.counter = 0 
elseif event.type == "on" then
    mem.counter = mem.counter + 1
end

Zählen ist schön und gut, aber was passiert nun mit dem gespeicherten Wert? Richtig interessant werden Speicherstellen erst, wenn ihre Werte verwendet werden. Dafür gebe ich hier ein erstes kleines Digiline-Beispiel. Das Thema “Digilines” ist allerdings sehr weitläufig und daher einen weiteren eigenen Blog Post wert.

Digilines

Wie schon früher erwähnt, übertragen Digilines nicht einfach nur “Strom”, sondern Daten. verschiedene Endgeräte können Daten empfangen und senden, die Daten können über eine gemeinsame Datenleitung ausgetauscht werden. Die Digilines sind blau, im Gegensatz zu den gelben Mesecon-Kabeln. Damit jedes Gerät weiß, mit welchem anderen Gerät es redet, gibt es Kanäle auf der Digiline - genau wie ein Fernsehkabel verschiedene Sender übertragen kann. Bei Digilines gilt: wenn Sender- und Empfängerkanäle denselben Namen haben, können sie miteinander kommunizieren. Daher hat jedes Digiline-Gerät immer die Möglichkeit, einen Kanalnamen zu vergeben. Diesen Namen gibt man über einen Rechtsklick ein. Hier zum Beispiel nehmen wir einen LCD-Bildschirm, der uns die gespeicherten Daten anzeigen soll.

channel

Im Code kann man diesen Kanalnamen ebenfalls verwenden. Wir erweitern unser Zählbeispiel um die Zeile digiline_send.

if event.type == "program" then
    mem.counter = 0 
elseif event.type == "on" then
    mem.counter = mem.counter + 1
    digiline_send("lcd", mem.counter)
end

Dieser Befehl sorgt dafür, dass wir an den Kanal senden, der im ersten Parameter angegeben ist. Die Botschaft steht dann im zweiten Parameter. In unserem Fall ist das die Speicherstelle mit den gezählten Ereignissen, es könnte aber auch ein Text sein. In Lua kann man auch Texte mit Variablen kombiniert ausgeben lassen, das sähe dann zum Beispiel so aus: "Ereignisse: " .. mem.counter. Die zwei Punkte verbinden die einzelnen Ausgabeteile miteinander zu einer Zeichenkette, die dann versendet werden kann.

So lassen sich schöne Steuerungen mit Display bauen. Das Gegenstück, eine virtuelle Tastatur gibt es ebenfalls in einem der vielen digilines-Pakete.

Wer übrigens mehr zum Programmieren mit Lua wissen möchte, kann dies in einem der vielen Tutorials im Netz erlernen, zum Beispiel

Die Sprachreferenz findet sich hier:

Technisches zum Selbermachen

Wer seinen eigenen Minetest Server hat, kann die folgenden Module installieren:

Quellen

github Repositories:

Module installieren

  1. Ladet euch die zip-files herunter und entpackt sie im mods-Verzeichnis eures Servers. Verschiebt die NNN-master-Verzeichnisse jeweils nach NNN.
  2. In der Modliste der Welt die Mods eintragen:
load_mod_mesecons = true
load_mod_mesecons_walllever = true
load_mod_mesecons_torch = true
load_mod_mesecons_switch = true
load_mod_mesecons_receiver = true
load_mod_mesecons_random = true
load_mod_mesecons_solarpanel = true
load_mod_mesecons_powerplant = true
load_mod_mesecons_pistons = true
load_mod_mesecons_noteblock = true
load_mod_mesecons_mvps = true
load_mod_mesecons_movestones = true
load_mod_mesecons_materials = true
load_mod_mesecons_lightstone = true
load_mod_mesecons_microcontroller = true
load_mod_mesecons_insulated = true
load_mod_mesecons_pressureplates = true
load_mod_mesecons_stickyblocks = true
load_mod_mesecons_delayer = true
load_mod_mesecons_wires = true
load_mod_mesecons_detector = true
load_mod_mesecons_hydroturbine = true
load_mod_mesecons_luacontroller = true
load_mod_mesecons_alias = true
load_mod_mesecons_lamp = true
load_mod_mesecons_blinkyplant = true
load_mod_mesecons_commandblock = true
load_mod_mesecons_button = true
load_mod_mesecons_doors = true
load_mod_mesecons_extrawires = true
load_mod_mesecons_fpga = true
load_mod_mesecons_gates = true
load_mod_digilines = true
load_mod_digiterms = true

Server neu starten und schon stehen euch die Mods zur Verfügung.