Q-SYS Lua Design Patterns
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!
Control / EventHandler
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
end
Note: 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.
Example
-- 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)
Ternary
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'
end
becomes:
c.Color = c.Boolean and 'red' or 'green'
Conjecture / counterexample
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.
Check condition
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
end
Best candidate
We 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 12
If true, set true - anti-pattern
Instead of:
if(Controls.MyButton.Boolean) then
Controls.MyLED.Boolean = true
else
Controls.MyLED.Boolean = false
end
write:
Controls.MyLED.Boolean = Controls.MyButton.Boolean
TCP Socket Template
This 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
end
We can then handle data reception events separately:
Sock.Data = function()
-- process data here
-- see "Line-by-line socket data"
-- or "Fixed-length socket data"
end
And 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()
Line-by-line socket data
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)
end
However, 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)
end
Aside 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.