Summary
Improve the 'call sequencer' such that we can schedule calls even if a call sequence is currently in-progress.
Deets
The call sequencer that was produced for the benefit of controlling the Nixie clock has a deficiency: you mustn't schedule a call sequence when a call sequence is already in-progress. This would result in interleaved calls, and also consumes more timer resources.
This currently isn't a problem since the call sequencer is only used for updating the Nixie clock once per day, so there is no likelihood of two sequences coming in at once, however I do want the mechanism to be well-behaved later, when I add a server component to issue arbitrary sequences at arbitrary times.
Presently, the sequencer takes a sequence (a Lua table) and keeps an index into where we are in the sequence, and a periodic timer is used to issue the call and increment the index. When we have finished, we destroy the timer. Through the magic of closures, the timer servicing function captures the index and the call sequence, so nothing needs to be done special there to keep them alive or destroy them when done.
In this new design, we will instead have a separate sequence object, and we will append to it sequences that are requested. If there is not existing sequence object being serviced, we will create a new one and kick start the process. Otherwise, the process is pretty much the same.
function run_sequence ( sequence, period_ms ) if ( not sequence or 0 == #sequence ) then return end --silly cases -- if we have a run sequence in process, merely append these items to it if ( _tblRunSequence ) then table_append ( _tblRunSequence, sequence ) return end -- otherwise, kick off the sequence -- do the first one immediately local nIdx = 1 sequence[nIdx]() nIdx = nIdx + 1 -- if that was it, beat it if ( nIdx > #sequence ) then return end --remaining ones are to be paced out _tblRunSequence = table_clone(sequence) -- we make this global local pacingTimer = tmr.create() pacingTimer:alarm(period_ms, tmr.ALARM_AUTO, function(timer) if ( nIdx <= #_tblRunSequence ) then _tblRunSequence[nIdx]() nIdx = nIdx + 1 end if ( nIdx > #_tblRunSequence ) then -- we are completed timer:stop() timer:unregister() _tblRunSequence = nil end end) end
In this implementation, we create a global object _runsequence which will contain the stuff being serviced. This object is global, because we need to access it later in subsequent call invocations, and captures won't help us here. If it exists, we append do it and we're done. If it doesn't exist, then we clone our given sequence into it, and kick off the timer. And if the timer finds that it has run out of things to do, it destroys itself, and the global sequence object. So, if there is nothing going on, all memory should be released for garbage collection.
One quirk of Lua is that it intends to be a minimalist runtime system, and you are expected to add code to do things as needed, rather than having an especially rich standard runtime environment. In this case, notably lacking are the ability to make a copy of a Lua table -- assignments are by reference -- and the ability to append a table onto another table. So we have to write that code.
This method creates a table that is a deep-ish copy of a source table:
function table_clone(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[table_clone(orig_key)] = table_clone(orig_value) end setmetatable(copy, table_clone(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end
And this method appends the values of one table's 'array' section onto another table:
function table_append ( t, o ) for _, v in ipairs ( o ) do table.insert ( t, v ) end end
Testing
If you past the following into the Lua shell command prompt:
= _tblRunSequence run_sequence ( { function () print ( "Hey!" ) end, function () print ( "Ho!" ) end, } , 5000 ) run_sequence ( { function () print ( "Hi!" ) end, function () print ( "Yo!" ) end, } , 5000 ) = _tblRunSequence
Then you will get the following output:
> = _tblRunSequence nil > run_sequence ( >> { >> function () print ( "Hey!" ) end, >> function () print ( "Ho!" ) end, >> } >> , 5000 ) Hey! > run_sequence ( >> { >> function () print ( "Hi!" ) end, >> function () print ( "Yo!" ) end, >> } >> , 5000 ) > = _tblRunSequence table: 0x3fff0c90 > Ho! Hi! Yo! = _tblRunSequence nil >
This shows that at the start, there was no global _tblRunSequence. We registered a call sequence that simply prints some distinctive text. The first function was executed immediately because there was no sequence in-progress. Then we see that the global _tblRunSequence has been created. The sequence pacing is 5 sec, so we will have some time before it has completed. Then we register a new call sequence. We can sit back an observe that the calls have been made in the expected pacing. When the sequence is complete, we can issue a final '= _tblRunSequence' and see that the global sequencing variable has been deleted.
Tada! Since it's working, it's time to move this stuff to LFS. I did a pass with dummy_strings.lua empty so I could regenerate that list, then I set the list to the current values, and restarted. I did a =node.heap() before running init.lua, and one after, and I got 43704 - 39128 = 4576. So, with the stuff in LFS, the program is nominally using less than 5 K RAM.
Now I should be set up to be able to safely implement some sort of 'server' to issue Nixie calls over the Internet.
Next
Some sort of server?
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.