Statescript

A simple, yet powerful scripting language to control the timing of environmental input and output events with high temporal resolution in hardware.

StateScript allows you to define temporally precise sequences of events and their triggers, which is useful with devices such as lights, levers, beam breaks, lasers, stimulation sources, audio, and solenoid pumps. Scripts are sent to our Environmental Control Unit and compiled to run in real-time in hardware. Controlling the experimental environment has never been so easy.

Getting Started

The StateScript module can either be opened from Trodes (by pressing the ‘StateScript’ button) or in standalone mode (by starting the StateScript program itself). The graphical interface has two list panels on the top half of the screen. The left panel (‘Protocols’) shows the contents of the folder where you keep your statescript files. The right panel (‘Callbacks’) shows the contents of the folder where you keep your high-level observer scripts, written either in Python or Matlab. The paths that these two list boxes point to can be edited using File->Script Folders.

If you double-click on a statescipt in the ‘Protocols’ list, a StateScript editor will open, allowing you to view and edit the script. To send the script to the ECU, you must first connect to the ECU via a USB connection. To connect, click the ‘Controller’ button at the top of the screen and select ‘ECU’ in the Port list. When you click ‘Connect’ the bottom panel on the screen will turn white, showing the output of the serial communication between the ECU and the computer. If the connection is successful, a series of commands will automatically be sent to the ECU, and the ECU will return three “~” characters to indicate successful processing of commands. At that point, you can click on a script in the ‘Protocols’ list and click ‘start’ (in standalone mode) or ‘Send script’ (in Trodes slave mode). If your script compiles, the ECU will again return three “~” characters after every ‘;’ in the script. Once compiled, any callbacks in the script are active.

You can also execute commands directly into the prompt using the single-line entry near the bottom of the screen. Make sure you end each command with a “;”.

Observer Scripts

While statescripts are used to handle time-sensitive events (like turn on light in 50ms), you can use “observer scripts” to do more complex operations that require a full scripting environment but does not need to occur within a very short time window (for example, computing a new reward probability based on a complex mathematical formula). For this, you can use either Python or Matlab. The StateScript interface module will start the interpreter and display the text input/output in the “Python” or “Matlab” tab, depending on which interpreter you decide to use. Once the interpreter is running, it will receive all text output from the ECU and can send text-based commands back.

To get started with observer scripts, you first need to tell the StateScript module where the interpreter executable file is on your computer. To do this, go to Edit->Observer language and a pop up window will appear. In the drop down menu, select either Python or Matlab, and then enter the full path to the location of the interpreter below. You only need to do this once. The program will remember the next time it is opened.

Next, you need to set the location of the Callback scripts. Go to File->Script folders->Callbacks and navigate either to the “matlabObserver” folder or the “pythonObserver” folder located in the “Resources” subdirectory of the main Trodes suite folder. If you are using Matlab, you should add this folder to the Matlab path. Both of these folders come with an included example showing you how to write an observer script. In the “Callbacks” window, highlight the example script (“example.py” for python), and then the button to start the interpreter will become enabled on the top of the window. Press the button, and switch over to the interpreter output tab. Here you can see the output of your script. Once you have written your own observer script, select a statescript that is paired with it (has the expected disp() statements,etc) and send it to the ECU.

A Simple Example

This complete example shows how to define variables, callbacks, and ‘if .. else’ blocks.

%all variables are global
int count = 0
int countThreshold = 10

%this block is executed if digital input 1 goes from low to high
callback portin[1] up

if (count < countThreshold) do

%digital output 1 is turned to a high state
portout[1] = 1
count = count + 1

%this block is scheduled to be exectuted in 500ms.
%The ‘in’ keyword after ‘do’ is used to schedule for later.
do in 500
   portout[myPort] = 0

end

%a message is sent back to the computer.
%This is executed before the 500ms delay above is finished
disp (‘Lever press’)

else do
   disp(‘Ten presses completed’) %a message is sent back to the computer
   count = 0 %reset the counter

end

end;%semicolons are used to compile/execute everything since the last semicolon.

Language Overview

1. Callbacks

Callbacks are executed when the hardware input state changes. The user can create callbacks for individual digital input ports. A callback may not be nested inside another block.

%Digital port callbacks

%this callback is executed if digital input 2 goes from low to high (use the “up” keyword)
callback portin[2] up

portout[1] = 1

end

%this callback is executed if digital input 2 goes from high to low (use the “down” keyword)
callback portin[2] down

portout[1] = 0

end;

%Analog port callbacks

%This line turns on threshold detection for analog input X, and sets the threshold (mV) thresh on 1 190

%This callback is executed if the analog threshold on analog channel 1 is crossed (low to high)
callback analogin(1) up
callback portout(1) = 1
end 

%this callback is executed if the analog threshold on analog channel 1 is crossed (high to low) callback analogin(1) down
callback analogin(1) down
callback portout(1) = 0

end

2. Functions

Functions are blocks that are executed with the ‘trigger()’ command. A trigger may not be nested inside another block.

%function 1 (executed with ‘trigger(1)’)
function 1

portout[1] = flip %’flip’ will switch the binary output state

end

%function 2 (executed with ‘trigger(2)’)
function 2

portout[2] = flip

end

%this callback is executed if digital input 2 goes from low to high
callback portin[2] up

trigger(1) %this will execute function 1

end;

3. Variables

Variables are global, which means they are accessible from all blocks. They must be defined outside all blocks. Currently only integer variables are supported.

%Variables are declared outside all blocks.
int currentPort = 1;

function 1

portout[currentPort] = flip %variables can be used in portout[]
currentPort = currentPort+1

end;

4. ‘Do in’ blocks

The workhorse of StateScript timing control, ‘do in’ blocks allow you to schedule an entire block of code to be executed later with 1 ms precision.

int blinkDelay = 500

callback portin[1] up

portout[1] = 1 %turn on light now
do in blinkDelay
   portout[1] = 0
%turn off light in 500 ms

end

end;

5. ‘If…else’ blocks

The if…else block in StateScript allows conditional block execution. Standard boolean logic notation (&&, ||, <, >, <=, >=, ==) are used to define conditions. ‘If’ statements can be combined with ‘do in’ statements.

int currentCorrectChoice = 1
int numberOfCorrect = 0

callback portin[1] up

%Combo if, do in statement
if (currentCorrectChoice == 1 && numberOfCorrect > 3 ) do in 500

numberOfCorrect = numberOfCorrect+1
portout[1] = 1

%turn off reward pump 1500ms after this block is executed
%(2000 ms after callback execution)
do in 1500
portout[1] = 0

end

else do

numberOfCorrect = 0

end

end;

6. ‘While…then’ blocks

The while…then block is used to repeatedly schedule a block of code at regular intervals until a condition is no longer true. Every ‘while’ statement must be accompanied by a ‘do every’ statement.

int count
int loopInterval
int done

function 1

%only start blinking if it is not already blinking
if (done == 1) do

count = 0
done = 0
loopInterval = 500

%blink the light 16 times
%as long as the condition is true, the block is re-scheduled
%every loopInterval milliseconds
while count < 16 do every loopInterval

portout[1] = flip
count = count+1

%we can make the loop interval change as the while loop is going
loopInterval = loopInterval-10

%when the condition is no longer true, the ‘then’ statement
%is exectuted to reset conditions
then do

portout[1] = 1
done = 1
loopInterval = 500

end

end;

7. Output control

Currently only digital port output is supported. Analog output control coming soon.

portout[1] = 1 %Digital port callbacks
portout[currentBlinkingPort] = 0 %turn a variable-defined port to a low state
portout[currentBlinkingPort] = flip %flip a port state
myVariable = portout[1] %store the state of the digital output port
myVariable = portin[1] %store the state of the digital input port

analogout[1] = 500 %set the analog output in analog out port 1 to 500 mV
analogout [myPort] = 500 %set a variable-defined analog port output
myVariable = analogout[1] %store the state of the analog output port
myVariable = analogin[1] %store the state of the analog input port

8. Built-in functions

Built-in functions are included to allow extra features and custom hardware behaviors.

%’clock()’ is used to get the current clock value
a = clock()
a = clock(reset)
%used to reset the system clock to 0

%thresh
thresh on 1 190 %IN MILLIVOLTS This line turns on threshold detection for analog input X, and sets the threshold (mV)

%’disp()’ is used to send text back to the computer interface
disp(a) %display a variable
disp(‘Rewarded!’) %display a string

%’random()’ is used to generate random numbers
a = random(99) %returns a random number between 0 and 99

%’sound()’ is used to play an audio file stored on an SD card
sound(‘beep1’)
sound(stop) %stops audio playback before it is done
volume(10)
%change the current audio volume

%’trigger()’ is used to trigger a defined function
trigger(2) %execute function 2

%By default, hardware port status updates
%are sent to the computer every time a digital port changes
updates off %turn all DIO auto status updates off
updates off 1 %turn all DIO auto status updates off for input port 1
updates on %turn all DIO auto status updates on

9. The semicolon ( ; )

In most of the examples above, a semicolon, followed by a carriage return, is placed after the last line of the script. When a script is sent to the ECU, the ECU stores multiple lines until a semicolon is given. This tells the ECU to compile everything up to that point (since the last semicolon). Semicolons should not be placed inside any blocks. A single command followed by a semicolon is executed immediately (portout[1] = 1;). The hardware will return ‘~~~’ if everything compiled ok. Otherwise it will return an error message.

Considerations

Asynchronous execution

StateScript execution is asynchronous, which can be confusing to programmers who are accustomed to synchronous execution. When a program executes asynchronously, it does not wait for one line of code to finish before moving on to the next line. Using a ‘do’ block without an ‘in’ statement forces immediate, synchronous execution of the block, but a ‘do in X’ statement simply adds the block of code for later execution. The compiler moves on to executing lines that follow the scheduled block before the scheduled block is executed.

%What is the final state of the output port?
function 1

portout[1] = 1 %executed 1st
do in 500
   portout[1] = flip
%executed 3rd
end
portout[1] = 0 %executed 2nd

end;

Considering this asynchronous behavior is especially important for while…then loops. These loops are not the same as traditional while loops, where code following the loop block is not executed until the loop is finished. Instead, only the first iteration of the loop is executed immediately (and synchronously), at which point the second iteration is scheduled. When the next scheduled iteration exectutes, the condition is first checked, and if the condition is true the block is executed and the next iteration is scheduled. Therefore, any variables that are counting the number of iterations, etc., need to be reset in the while loops’ ‘then’ section, which is executed once the condition is no longer true.

Shown is an example with two while loops, one nested inside the other. The counters for these loops are managed in the ‘then’ statements:

int pulseCounter = 0
int trainCounter = 0
callback portin[1] up
%create 10 pulse trains, where the trains
%start every 100 ms and each individual
%train contains 5 pulses at 10 ms intervals  
%outer while loop 10 pulse trains
while trainCounter < 10 do every 100
%nested inner while loop for each train
while pulseCounter < 5 do every 10
%create a single 1 ms pulse
portout[1] = 1
do in 1
   portout[1] = 0

end
pulseCounter = pulseCounter + 1
then do %cleanup for inner loop
%reset inner loop counter
pulseCounter = 0
%iterate outer loop counter here
trainCounter = trainCounter + 1
end
then do %cleanup for outer loop
%%reset outer loop counter
trainCounter = 0
end
end;