The Adafruit tutorial for flashing an LED using Lua on an ESP8266 is buggy: The main loop does not yield to the underlying operating system, and instead hogs the CPU causing eventual crash - usually after about 10 seconds for me.
Here's the code reproduced from the tutorial:
while 1 do
gpio.write(3, gpio.HIGH)
tmr.delay(1000000) -- waits a second, without allowing OS to run
gpio.write(3, gpio.LOW)
tmr.delay(1000000) -- and again
end
The NodeMCU people advocate using a node.js async callback style, where instead of delaying your thread, you would instead set an alarm for a callback to do the next thing. Using tmr.delay
and looping a lot is strongly discouraged because it upsets the OS.
I hate that callback style of coding (for reasons).
Lua has co-routines, though, apparently because people wanted to do things like write scripts that kept yielding back to a game engine and then resuming. (See this history of Lua)
I've recently been playing with effect systems in Haskell (see blog tag extensible-effects) and realised that Lua co-routines provide enough infrastructure for (some of) this.
So I hacked up a prototype.
The "user program" looks very much like the Adafruit example:
flashDelay = 200 -- ms
function flasher()
while 1 do
gpio.write(3, gpio.HIGH)
coroutine.yield(flashDelay)
gpio.write(3, gpio.LOW)
coroutine.yield(flashDelay)
end
end
and can be run like this;
driveCoroutineGood(flasher)
You can also use driveCoroutineBad
which uses the blocking tmr.delay
instead of the asynchronous tmr.alarm
, and get the same ESP-crashing behaviour as the original example.
The main difference is that calls to tmr.delay
are replaced by a call to yield
. In effect system language, that means the code is asking the effect handler (driveCoroutineGood
or driveCoroutineBad
) to perform an effect (to delay by the appropriate time) rather than performing the effect itself. How that actually happens is down to the effect handler: in the bad case, just calls tmr.delay
; and in the good case, does all the wrapping up of callbacks for tmr.alarm
.
This ends up looking almost identical to the code in this GitHub issue, tmr.delay() is synchronous and blocks the network stack.
On a very simple example, this is a bit overkill, but on a bigger example is probably more interesting: you can call coroutine.yield
deep down inside a whole stack of functions, which becomes tangly if you're trying to build this manually as an async callback.
Other callback style stuff is hopefully amenable to this - for example, socket handling.