States
Pynapse has an internal state machine that logs all events and keeps track of the current state. New triggers coming in are filtered through this state machine, so that only the slot methods associated with the current state can run.
There is a special 'Always' state - slot methods in the Always state can trigger on any polling loop. This is a useful state to add user mode controls (start/pause/stop).
States are 'classes' in the Python code that have the special comment #StateID = ?
at the end of the class definition. The number defined here is the id number associated with
this state. This value will be timestamped and stored with the data in the data tank. If the parser
finds a ?
, it will automatically assign a number for you. Otherwise enter
an integer to lock the StateID in place, for example #StateID = 555
.
See Working with StateIDs for more information.
Slot Methods for Responding to State Changes
These state slots capture state machine changes. They are available as method definitions inside Pynapse states, including the Always state. Write a method with this name to react to the corresponding event.
Example
Turn on an output only while in a particular state.
class StartTrial: # StateID = ?
# turn on MyOutput when entering state
def s_State_enter():
p_Output.MyOutput.turnOn()
# when MyInput passes 'Time to Active', switch to ActiveState
def s_MyInput_active():
p_State.switch(ActiveState)
# turn off MyOutput when exiting state
def s_State_exit():
p_Output.MyOutput.turnOff()
class ActiveState: # StateID = ?
# when MyInput passes 'Time to Pass', switch to PassState
def s_MyInput_pass():
p_State.switch('PassState')
def s_MyInput1_fail():
p_State.switch(FailState)
In this example, use s_State_change() to track order of state execution. Suppose there are many states that exit to TargetState. If the state that exited to TargetState is TargetOldState, we want to do something.
class Always: # StateID = 0
def s_State_change(newstateidx, oldstateidx):
print('new state', newstateidx, 'old state', oldstateidx)
if newstateidx == TargetState and oldstateidx == TargetOldState:
print('do something')
State Timeouts
The Pynapse state machine has a built-in Timer that is used as a timeout within the current state that moves to another state if it fires. Timeouts are usually set in the s_State_enter() slot method but can be set anywhere in the State. Timeouts can also be canceled.
Example
If the user fails to press a button (MyInput) within five seconds, we want to move to a NoTrial state and wait there for ten seconds before starting a new trial.
class StartTrial: # StateID = ?
# if no input is received after 5 seconds, switch to NoTrial state
def s_State_enter():
p_State.setTimeout(5, NoTrial)
def s_MyInput_rise():
p_State.switch(TrialState)
class NoTrial: # StateID = ?
# wait 10 seconds, return to StartTrial
def s_State_enter():
p_State.setTimeout(10, StartTrial)
class TrialState: # StateID = ?
# turn on an output
def s_State_enter():
p_Output.MyOutput.turnOn()
Methods
All state methods have the form p_State.{METHOD}
. Type p_
in the Pynapse Code Editor
and let the code completion do the work for you.
State Control
switch
p_State.switch(newstate)
Tell Pynapse to move to a new state. All states are python 'classes'. The input to switch
can
be either the class or the string class name you want to switch to. See the example below
Note
The function that contains the switch
command does not exit immediately after switching
the internal state. This can have unintended consequences, particularly if you are using
the Sync to State Change option for outputs or timers. Best practice is to use the
switch
command last, right before the function exits.
See Synchronizing Events for information.
Example
Switch between states when MyInput goes high.
class StartTrial: # StateID = ?
def s_MyInput_active():
p_State.switch(ActiveState)
class ActiveState: # StateID = ?
def s_MyInput_pass():
# you can also switch states with a string name
p_State.switch('PassState')
def s_MyInput_fail():
p_State.switch(FailState)
setTimeout
p_State.setTimeout(secs, stateOnTimeout)
Switch to a default state after a certain period of time.
Important
There can only be one active timeout per state. If you need to set a new timeout within the state, use the cancelTimeout method first.
Example
Toggle between the FirstState and SecondState until MyInput rises in FirstState.
class FirstState: # StateID = ?
# if no input is received in 5 seconds, switch to SecondState
def s_State_enter():
p_State.setTimeout(5, SecondState)
def s_MyInput_rise():
p_State.switch(EndState)
class SecondState: # StateID = ?
# wait 5 seconds, return to FirstState
def s_State_enter():
p_State.setTimeout(5, FirstState)
class EndState: # StateID = ?
def s_State_enter():
print('done')
cancelTimeout
p_State.cancelTimeout()
Cancel the current timeout. Can be called anywhere within the state.
Example
Give the subject 15 seconds to press MyInput 10 times. If successful, cancel the state timeout and give the subject unlimited time to reach 20 presses before moving to the success state.
class TrialState: # StateID = ?
def s_State_enter():
# reset counter
p_Global.count.write(0)
# if target isn't reached in 15 seconds switch to DefaultState
p_State.setTimeout(15, DefaultState)
def s_MyInput_rise():
# increment counter
p_Global.count.inc()
# if we reached our first target, cancel timeout
if p_Global.count.read() == 10:
p_State.cancelTimeout()
elif p_Global.count.read() == 20:
p_State.switch(SuccessState)
class DefaultState: # StateID = ?
def s_State_enter():
print('default')
class SuccessState: # StateID = ?
def s_State_enter():
print('success')
Status
isCurrent
p_State.isCurrent(stname)
Check if the current state is the given name. This is useful if you have a lot of States defined but want to do similar actions in multiple states for a given slot method. You can move the logic into the Always state and avoid repeating yourself. See the example below.
Or if you want to
Example
In a long list of states, we want to turn the MyOutput on in just two of them.
class Always: #StateID = 0
def s_MyInput1_rise():
if p_State.isCurrent(State8) or p_State.isCurrent(State20):
p_Output.MyOutput.turnOn()
In this second example, the target state is dynamically set by a global variable. When MyInput2 turns on, the slot method is captured in the Always state and only continues (turns on Output2) if the current state matches the target state. This target state can be set on the user interface or somewhere else in the code using the Globals asset. This could also be tied to a Control asset.
class Always: #StateID = 0
def s_MyInput2_rise():
if p_State.isCurrent(p_Global.target_state.read()):
print('current state is the target state set in the user interface')
p_Output.MyOutput2.turnOn()
isNotCurrent
p_State.isNotCurrent(stname)
Check if the current state is not given name. This is useful if you have a lot of States defined but don't want to include the same identical slot method in all of them except a small number of states. You can include this logic check within the Always state. See the example below.
Example
When MyInput turns on, turn on MyOutput in all states unless we're in the DontStim state.
class Always: #StateID = 0
def s_MyInput_rise():
if p_State.isNotCurrent(DontStim):
p_Output.MyOutput.turnOn()