SELFSCRUM

A Learning and Operating System for Alternative Schools

Lua Controller Programming

2021-03-08 Minetest

Lua controllers offer a great opportunity: you can program them in-game so that you can build systems, process events, and control switching outputs or other digital devices.

Lua controllers are, as the name suggests, programmed in Lua, which is the language in which Minetest itself is also largely written.

The controllers provide 4 inputs and outputs (port A to D).

The inputs can be reacted to via event control, the outputs can be set to true or false, i.e. on or off for mesecons.

Lua controllers are also important for another reason - they are the link between Mesecons and Digilines. The latter are not just “power cables” like mesecons, but they can be used to exchange signals. In doing so, channels are used to query or address specific devices. For example, you can use a Digilines keyboard in the game to query a password. If the input is correct, you can use it to open a trapdoor, for example. This is a quick way to create more elaborate controls.

Unfortunately the code editor of the Lua controllers is not very comfortable. Therefore, it is best to first build the code in a good development environment (for example Microsoft’s Visual Studio Code ) and then copy it into the game for testing.

Outputs

As mentioned, the Lua controller has 4 outputs. These are labeled A through D as already seen on the controller. In the code we use small letters to enable or disable an output. In the initial state all 4 outputs are off.

off

If we now activate a port in the code, as the outputs are called, our light turns on.

port.c = true

on

You can also switch multiple ports. There is also a short form for this.

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

However, the ports are not evaluated until the end of the code sequence. Therefore, the following code has no visible effect if port C is off at the beginning:

port.c = true
port.c = false

The outputs are not particularly exciting on their own. For any meaningful application to be possible, the controller must be able to respond to events. Before we explore these dynamics, let’s first note that the state of each port can also be queried. The corresponding register is called pin.

port.c = pin.a

Here the output C is set equal to the input A. But only once at the initial execution. Later state changes at input A are not yet considered by this code. For this we need a trigger.

Trigger

The simplest trigger is a recurring signal that executes the stored code at intervals. This is called a clock. Since in “real” computers such a signal interrupts the regular instruction processing, such signals are also called “interrupt”. This is also the case here. The following code is executed every 2 seconds.

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

Can you imagine what happens here? The following figure shows it.

blink

So the code reverses the state of pin C by the not and thus sets port C again. This is how the blinking effect is created.

With this we have also introduced the first logic command, which you may have already learned about the other Mesecon cubes. A not reverses the applied signal. So the code here works just like the NOT cube. The same is true for and and or, which connect two signal inputs according to a certain logic. and is on at the output if both inputs are on, while or is on as soon as one of the two or both inputs are on. For the other blocks like NAND, NOR, XOR there is no direct code correspondence, but they can be mapped in the code by logical operations. Explanations for this can be found abundantly on the Internet, for example here. In code, a logic circuit looks like this:

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

Now it gets interesting - we can let the outputs be controlled by signals at the inputs due to the clock. The code checks how the inputs are. That means, if we add a switch at the input, the reaction follows also at the output.

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

Do you notice that the flashing changes when the switch is pressed? Somehow both triggers seem to cause the code to execute. Maybe it would be better to shorten the interval? Then the delay is also smaller? Luckily Lua can handle floating point numbers:

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

Oh no. What happened now? The controller turns red and doesn’t respond anymore?!

overheat

What you see here is an “overheat” protection. As with real processors, performance is slowed down when the processor gets too hot. In Minetest, the Lua controller stops working altogether. This is not just a model of a real processor - if the Lua controller executes more than 20 instructions per second, it puts quite a load on the server. Therefore, the Minetest operating system simply shuts down the processor. But don’t worry - it’s all virtual. Therefore, we don’t have the smell of stewing in our noses and with the next saving of the code, the processor is ready for use again. By the way, we can query the (virtual) processor temperature. The variable heat contains the value of the just measured execution speed.

But how to get the events without delay? There is a mechanism for that too.

Events

Event is the name of our next construct in the code. It allows us to react to events from the outside instead of actively asking for them ourselves like with the interrupt. It also gives us the advantage of being able to react differently to different events in a targeted way. The following event types are provided in the Lua controller:

typeNotes
programArises only after the end of coding. Can be used to initialize something once.
onOccurs when a pin has been activated. In event.pin.name is then the port (as uppercase letter).
offOccurs when a pin has been disabled. The event.pin.name will then contain the port (as an uppercase letter).
interruptOccurs when the interrupt timer has expired. Optionally event.iid contains the name of the timer that triggered the interrupt.
digilineSignals an incoming data packet on the digiline data bus. event.channel contains the channel from which the message comes and event.msg contains the message itself. digiline events are not bound to a specific port!

We can now use the event object to filter specifically on these types to distinguish the response. With this code, the blinking does not change when the switch is pressed.

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

Now here is where a whole lot has happened. First, our interrupt got a second parameter. Since we are now listening for different events, the interrupt also needs a name so that nothing gets mixed up. This name is also queried at the event.

If it was not an interrupt with this name, it is now checked whether there was an event on pin B. The port D is then set to the log. Port D is then set to the logical condition “true” if the event type was “on”. Thus we achieve the desired effect.

This example hides the fact that we do not need interrupts when using events! The event object itself has a control built in that ensures that the code is called when an event occurs. So if we just want to query the switch, the code becomes simpler:

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

Memory

The way the code is structured now, it has no memory. Let’s say we want to count how many people have visited our cave via a footpad. Then we need a memory that keeps this information permanently.

You can allocate different memory locations, they are addressed by mem followed by the name. In the following code the memory location is called counter. The first time it is loaded after programming, it is set to 0. After that the code counts each on event and stores it again in counter.

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

Counting is all well and good, but what happens to the stored value now? Memory locations become really interesting when their values are used. For this I give here a first small digiline example. However, the topic “Digilines” is very wide and therefore worth another blog post of its own.

Digilines

As mentioned earlier, Digilines do not simply transmit “power”, but data. different terminals can receive and transmit data, and the data can be exchanged over a common data line. Digilines are blue, unlike the yellow Mesecon cables. So that each device knows which other device it is talking to, there are channels on the digiline - just as a television cable can transmit different channels. With Digilines, if the transmitter and receiver channels have the same name, they can communicate with each other. Therefore, each Digiline device always has the possibility to assign a channel name. You enter this name by right-clicking on it. Here for example we take a LCD screen, which should show us the stored data.

channel

In the code you can also use this channel name. We extend our counting example with the line 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

This command makes us send to the channel specified in the first parameter. The message is then in the second parameter. In our case this is the memory location with the counted events, but it could also be a text. In Lua you can also output texts combined with variables, this would look like this: "Events: " ... mem.counter. The two dots connect the individual output parts to a string, which can then be sent.

This is a great way to build nice controls with a display. The counterpart, a virtual keyboard is also available in one of the many digilines packages.

By the way, if you want to know more about programming with Lua, you can learn it in one of the many tutorials on the net, for example

The language reference can be found here:

Technical stuff to do yourself

If you have your own Minetest server, you can install the following modules:

Sources

github repositories:

Install modules

  1. download the zip-files and unpack them in the mods-directory of your server. Move the NNN-master directories to NNN respectively.
  2. enter the mods in the modlist of the world:
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

Restart the server and the mods are available.