This is a collection of design patterns (and some anti-patterns) for writing Lua code in Q-SYS.
This may also stray from strictly covering design patterns and into general tips / etc, but I trust you won't mind!
The obvious way to attach some logic to a Q-SYS control is using an event handler. The attached function will run whenever the control changes:
Controls.MyButton.EventHandler = function(c)
if(c.Boolean) then
c.Color = 'red'
else
c.Color = 'green'
end
endNote: This example could be made more succinct using the Ternary pattern.
The problem with this is that it doesn't run at start-up - so the following pattern exists for ensuring the logic is always consistent:
- Define a function to execute the logic.
- Attach this function as an Event Handler.
- Run this function at start-up.
-- Define function
function MyButtonHandler(c)
if(c.Boolean) then
c.Color = 'red'
else
c.Color = 'green'
end
end
-- Attach event handler
Controls.MyButton.EventHandler = MyButtonHandler
-- Run the function now
MyButtonHandler(Controls.MyButton)Sometimes, as in the example above, we wish to turn a boolean value (such as the state of a button) into two different values - one for true, and one for false.
In the example above, we convert Controls.MyButton.Boolean into 'red' or 'white'.
As long as the value we wish to use for true is not false or nil, we can exploit Lua's and and or operators to achieve this in a more succinct way, such that:
if(c.Boolean) then
c.Color = 'red'
else
c.Color = 'green'
endbecomes:
c.Color = c.Boolean and 'red' or 'green'Often, we wish to find the best candidate from a set, or establish whether some condition is true for all members of a set. A simple logical pattern for achieving this is to:
- Make an assumption about the result that can be disproved by an individual member.
- Test the assumption against each member, and amend the assumption accordingly.
For example, if we wish to check that all of the elements in a table array are true:
MyTableArray = { true, true, true, false, true }then we can start by assuming they are all true, and then check each element to see if it is false:
-- assume all members are false
local allAreTrue = true
-- for each member
for _,e in pairs(MyTableArray) do
-- if it is false (not true)
if(not e) then
-- our assumption is false
allAreTrue = false
end
endWe can also use this pattern to find the best candidate from a set, if we make a new assumption when our old one is disproven. For example, if we wish to find the index of the highest number in a table array:
MyTableArray = { 9, 3, 4, 6, 7, 12, 3, 7 }then we could start by assuming that the first member is the highest number, and then check each of the remaining numbers in turn to see if they are higher:
-- assume the first element is the best (highest)
local bestTableIndex = 1
local highestNumber = MyTableArray[1]
for index, number in pairs(MyTableArray) do
-- if this number is higher than the highest we've seen so far
if(number > highestNumber) then
-- make a new assumption that this is the best (highest) number
bestTableIndex = index
highestNumber = number
end
end
print(bestTableIndex, highestNumber)
-- > 6 12Instead of:
if(Controls.MyButton.Boolean) then
Controls.MyLED.Boolean = true
else
Controls.MyLED.Boolean = false
endwrite:
Controls.MyLED.Boolean = Controls.MyButton.BooleanThis is a standard boilerplate for creating TCP connections in Q-SYS Lua that provides several advantages over the example given in the help file:
- It's shorter.
- Handling of socket events and socket data are treated as two concerns.
-- Create a new socket
Sock = TcpSocket.New()TCP socket events are typically matched against the TcpSocket.Events table - however, the event itself is just a string, such as "CONNECTED".
Rather than look up each socket event individually, we can just print that underlying string:
-- Whenever the socket status changes, print the new status
Sock.EventHandler = print
-- > userdata: 00000250FF9FB928 CONNECTED The only downside of this is that it also attempts to print the socket itself as a string (this is the "userdata" we see). For debugging, this is likely sufficient. For a final plugin or reusable component, we may wish to link this to a Status control instead - assuming we have a status indicator control called Status:
Sock.EventHandler = function(_, evt)
if(evt == TcpSocket.Events.Connected) then
Controls.Status.Value = 0
elseif(evt == TcpSocket.Events.Reconnect) then
Controls.Status.Value = 5
else
Controls.Status.Value = 2
end
Controls.Status.String = evt
endWe can then handle data reception events separately:
Sock.Data = function()
-- process data here
-- see "Line-by-line socket data"
-- or "Fixed-length socket data"
endAnd finally we can set up the socket connection. Assuming we have a text control for the device IP / hostname, we can attach this using the Control / EventHandler pattern:
function reconnect()
if(Sock.IsConnected) then Sock:Disconnect() end
if(Controls.IP.String ~= '') then
Sock:Connect(Controls.IP.String, 80)
end
end
Controls.IP.EventHandler = reconnect
reconnect()The example given in the help file for reading socket data line-by-line is:
print( "socket has data" )
message = sock:ReadLine(TcpSocket.EOL.Any)
while (message ~= nil) do
print( "reading until CrLf got "..message )
message = sock:ReadLine(TcpSocket.EOL.Any)
endHowever, this involves repetition of the message = sock:ReadLine... line.
We can avoid this by defining an anonymous function for a for ... in loop to call. (See "generic for" in the Lua Reference Manual).
print( "socket has data" )
local function read()
return Sock:ReadLine(TcpSocket.EOL.Any)
end
for message in read do
print( "reading until CrLf got " .. message)
endAside from the deduplication, this pattern also has the advantage that the logic inside the read() function can be made more complex for protocols that require it.