Sequence Recipes
This page documents a number of common patterns we’ve found useful when writing control sequences.
Checking if a Channel Has Received Telemetry
Synnax’s sequences operate in a separate environment than any data acquisition/output that may feed data to them. This means that sequences are not guaranteed to have a defined value for every channel requested in the “Read From” field of the sequence.
For example, imagine we’re writing a control sequence that reads a set of analog inputs from a National Instruments (NI) device that is connected to pressure transducers, and writing to a set of digital outputs on another NI device that control a solenoid valve.
In this setup, we’ll create an NI Analog Read Task that samples the pressure channels at 100 Hz, then we’ll create an NI Digital Write Task to write to the outputs of the device. We won’t start any of them, so we’re not acquiring data or controlling outputs yet.
First, we’ll create the following control sequence that runs at 10 Hz:
if analog_input_1 > 50 then
set("digital_output_1", true)
else
set("digital_output_1", false)
end
We’ll set the sequence rate to 10 Hz, hit the play button, and see that it immediately crashes with the following error:
[sy.sequence.runtime] [string "if analog_input_1 > 50 then"]:1: attempt to compare number with nil
Why isn’t analog_input_1
defined if we specified it in the list of channels to read
from? This is because our analog data acquisition task is not running, so we’re not
supplying any fresh values for the sequence to pull from. To fix this, we should add the
following check at the top of our sequence code:
-- Return early and don't make any control decisions
-- if we don't have incoming telemetry.
if analog_input_1 == nil then
return
end
if analog_input_1 > 50 then
set("digital_output_1", true)
else
set("digital_output_1", false)
end
Now we can start our sequence just fine, and as soon as we start our analog read and digital write tasks, the sequence will receive fresh values and start executing the conditional block wih valid pressure values.
We recommend always checking if a channel value is defined before using it, as it will prevent any unexpected issues from arising.
Initializing Variables
In some cases, it’s useful to initialize one or more variables a single time during the
first loop in a sequence. This is particularly relevant for building State
Machines. To do this, we can use the built-in iteration
variable
along with a conditional check surrounding the block where we define our first
variables:
if iteration == 1 then
state = "pressurizing"
end
-- Rest of sequence logic
Now, state
will only be set to "pressurizing"
once during the first iteration of the
sequence.
Fixed Delays Between Commands
It’s often necessary to maintain a fixed delay between commanding two actuators. The first consideration to start with is the timing precision requirements for your system. If your loop rate is set at 5 Hz, iterations will occur at 200 ms intervals. Achieving a precise 150 ms delay between commands is impossible. Make sure to set your loop rate to an even multiple of the delay you’re trying to achieve.
To achieve a fixed delay, we can track the time at which we last wrote to the actuators, and then check if the current time minus the last command time is greater than the desired delay. If it is, we can command the actuators and update the last command time.
if last_trigger ~= nil and elapsed_time - last_trigger > 0.15 then
set("press_vlv_cmd", false)
set("vent_vlv_cmd", true)
last_trigger = nil
else if pressure > 100 then
last_trigger = elapsed_time
set("press_vlv_cmd", true)
set("vent_vlv_cmd", false)
end
State Machines
State machines are one of the most powerful patterns we can use in our control sequences, as they can allow us to separate distinct control phases into isolated blocks of logic. In this recipe, we’ll build a simple sequence that pressurizes a tank, holds pressure for a few seconds, and then vents the tank:
-- Set the initial state of the sequence to "pressurizing"
if iteration == 1 then
state = "pressurizing"
end
-- Make sure we're receiving pressure values
if tank_pressure == nil then
return
end
if state == "pressurizing" then
-- If we're not at our target pressure, then
-- open the press valve and close the vent valve
if tank_pressure < 50 then
set("press_vlv_cmd", true)
set("vent_vlv_cmd", false)
-- If we've hit our target pressure, move into the holding
-- state and mark the time at which we started holding.
else
state = "holding"
-- store the elapsed time at which we entered a hold
-- state so we can hold for a fixed amount of time
hold_start = elapsed_time
end
else if state == "holding" then
-- If 5 seconds or more have elapsed, move into the vent
-- state
if elapsed_time - hold_start > 5 then
state = "venting"
end
else if state == "venting" then
-- If we're still below ambient, then open the vent
-- valve and close the press valve.
if tank_pressure > 3 then
set("press_vlv_cmd", false)
set("vent_vlv_cmd", true)
end
end
Building state machines within the Synnax control sequence editor means defining an
initial state, using a chain of conditional logic to execute specific control code
within the state, and then transitioning to a different phase by assigning to the
state
variable when ready.
Abort Sequence
It’s common to have several sequences that perform different tasks. For example, we may have one sequence that maintains pressure in a tank, another that manages valve timings for a pneumatic system, and a third that listens for abort conditions to automatically shut down in the event of an anomaly.
Let’s take this example of a pressure maintenance sequence:
-- Set the initial state of the sequence to "pressurizing"
if tank_pressure < 100 then
set("press_vlv_cmd", true)
set("vent_vlv_cmd", false)
else if tank_pressure > 120 then
set("press_vlv_cmd", false)
set("vent_vlv_cmd", true)
end
Now we’d like an abort sequence that listens for a number of over-pressure and over-temperature conditions and shuts down the system. We do this in four steps:
- Set a low control authority so we don’t interfere with the main sequence.
- Check if sensor conditions are out of bounds.
- Take absolute control over relevant actuators.
- Command actuators to safe positions.
--- Set a low control authority on a first iteration.
if iteration == 1 then
set_authority("press_vlv_cmd", 1)
set_authority("vent_vlv_cmd", 1)
end
--- Check for sensor conditions.
if tank_pressure > 120 or tank_temperature > 100 then
--- Take absolute control over relevant actuators.
set_authority("press_vlv_cmd", 255)
set_authority("vent_vlv_cmd", 255)
--- Command actuators to safe positions.
set("press_vlv_cmd", false)
set("vent_vlv_cmd", true)
end
Now we can start both our main sequence and our abort sequence and they’ll both operate independently. If the abort sequence detects an over-pressure or over-temperature condition, it will take absolute control over the actuators and command them to safe positions, while the main sequence will continue to operate normally.
Keep in mind that if you pause the abort sequence, it will release control of the actuators and allow the main sequence to resume.