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 (ECU) and compiled to run in real-time in hardware. Controlling the experimental environment has never been so easy.
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 “;”.
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.
This complete example shows how to define variables, callbacks, and ‘if .. else’ blocks.
#!text %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 up if (count < countThreshold) do %digital output 1 is turned to a high state portout = 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.
Callbacks are executed when the hardware input state changes. The user can create callbacks for individual digital or analog input ports. A callback may not be nested inside another block.
#!text %Digital port callbacks %this callback is executed if digital input 2 goes from low to high (use the "up" keyword) callback portin up portout = 1 end %this callback is executed if digital input 2 goes from high to low (use the "down" keyword) callback portin down portout = 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 up portout = 1 end %this callback is executed if the analog threshold on analog channel 1 is crossed (high to low) callback analogin down portout = 0 end
Functions are blocks that are executed with the ‘trigger()’ command. A trigger may not be nested inside another block.
#!text %function 1 (exectuted with 'trigger(1)') function 1 portout = flip %'flip' will switch the binary output state end %function 2 (exectuted with 'trigger(2)') function 2 portout = flip end %this callback is executed if digital input 2 goes from low to high callback portin up trigger(1) %this will execute function 1 end;
Variables are global, which means they are accessible from all blocks. They must be defined outside all blocks. Currently only integer variables are supported.
#!text %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.
#!text int blinkDelay = 500 callback portin up portout = 1 %turn on light now do in blinkDelay portout = 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.
#!text int currentCorrectChoice = 1 int numberOfCorrect = 0 callback portin up %Combo if, do in statement if (currentCorrectChoice == 1 && numberOfCorrect > 3 ) do in 500 numberOfCorrect = numberOfCorrect+1 portout = 1 %turn off reward pump 1500ms after this block is executed %(2000 ms after callback execution) do in 1500 portout = 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.
#!text 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 = 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 done = 1 loopInterval = 500 end end;
7. Output and input control
Digital and analog port control is supported.
#!text portout = 1 %turn digital output port 1 to a high state portout[currentBlinkingPort] = 0 %turn a variable-defined port to a low state portout[currentBlinkingPort] = flip %flip a port state myVariable = portout %store the state of the digital output port myVariable = portin %store the state of the digital input port analogout = 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 %store the state of the analog output port myVariable = analogin %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.
#!text %'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;). The hardware will return ‘~~~’ if everything compiled ok. Otherwise it will return an error message.
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.
#!text %What is the final state of the output port? function 1 portout = 1 %exectuted 1st do in 500 portout = flip %exectuted 3rd end portout = 0 %exectuted 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:
#!text int pulseCounter = 0 int trainCounter = 0 callback portin 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 do in 1 portout = 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;