trick/docs/tutorial/TutVariableServer.md
Hong Chen b2d7721d20
1501 update python 2 to 3 in documents (#1516)
* Updated example code from Python 2 to 3 in How to Get Simulation Time from the Variable Server How-To Guide.

* Removed one line of commented code.

* Modified the examples in Trick Variable Server Tutorial from Python2 to 3 mainly due to socket send in bytes plus print statement update need for Python 3.

* Fixed 2 missed print statements.
2023-06-12 10:26:25 -05:00

24 KiB

HomeTutorial Home → Variable Server

Trick Variable Server

Contents


This tutorial section will demonstrate how to write a Trick variable server client. We'll be writing the clients in Python, but they can be written in any language. We'll start out with a minimal client and then proceed to a more realistic example. We'll be interfacing with our Cannon ball simulation.


What is The Variable Server?

Every Trick simulation contains a Variable Server, a TCP/IP network service for interacting with the simulation while it's running. Like the input-file processor, the variable server uses a Python interpreter that's bound to the simulation's variables and functions. So, just as in an input file, one can set variable values and call functions from a variable server client. A variable server specific API also exists to get simulation data back to the client.

The Trick Sim Control Panel, and Trick-View are, for example, both variable server clients.

Variable Server Sessions

Each variable server connection creates a variable server session, whose configuration identifies what, when, and how data will be sent from the server to the client. A session configuration consists of the following information:

  • A list of names of the variables whose values are to be sent in messages to the client.
  • The rate at which messages are transmitted to the client.
  • How messages are encoded. (ASCII or binary).
  • Whether messages are guaranteed to be time homogenous.
  • Whether message transmission is synchronous with the main simulation thread.
  • Whether data transmission is paused (inactive), or unpaused (active).
  • The debug state of the connection.
  • An optional name, to identify the connection when debug messages are enabled.

VarServerSessions

The primary purpose of the variable server API is to configure the sessions.

Approach

Calling functions and setting simulation variables with the variable server client is a similar process to doing the same with the input file. The client sends Python code to the variable server, where it's executed to call functions, set variables, or both. In the following sections, we'll see examples of these. We'll also learn how to use the variable server API to get data back to the client.

A Simple Variable Server Client

The listing below implements a very simple variable server client for our cannonball simulation. It connects to the simulation, requests cannonball position data, and prints the periodic responses to the screen.

Listing - CannonDisplay_Rev1.py

#!/usr/bin/python3
import sys
import socket

# 1.0 Process the command line arguments.
if ( len(sys.argv) == 2) :
    trick_varserver_port = int(sys.argv[1])
else :
    print( "Usage: python<version_number> CannonDisplay_Rev1.py <port_number>")
    sys.exit()

# 2.0 Connect to the variable server.
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect( ("localhost", trick_varserver_port) )
insock = client_socket.makefile("r")

# 3.0 Request the cannon ball position.
client_socket.send( b"trick.var_pause()\n" )
client_socket.send( b"trick.var_ascii()\n" )
client_socket.send( b"trick.var_add(\"dyn.cannon.pos[0]\") \n" +
                    b"trick.var_add(\"dyn.cannon.pos[1]\") \n"
                  )
client_socket.send( b"trick.var_unpause()\n" )

# 4.0 Repeatedly read and process the responses from the variable server.
while(True):
    line = insock.readline()
    if line == '':
        break

    print(line)

Running the Client

To run the variable server client :

  • Create a new file called CannonDisplay_Rev1.py in your home directory, and copy the contents of the above listing above into it.
  • Make the file executable. Example: % chmod +x CannonDisplay_Rev1.py.
  • Execute, but don't "Start" the cannonball simulation.
  • Find the variable server port number in the bottom left hand corner of the Sim Control Panel, as shown below.

Cannon

  • Execute the script with the port number as an argument. Example: $ ~/CannonDisplay_Rev1.py 50774 &
  • "Start" the cannonball simulation.

The output of the script will display three columns of numbers. The left most number is the variable server message type. Here, a message type of 0 indicates that the message is the (tab delimited) list of the values we requested. The two columns to the right of the message number are the values of dyn.cannon.pos[0] and dyn.cannon.pos[1], in the order that they were specified in the script.

0	55.85863854409634	24.0875895

0	60.18876556301853	25.2730495

0	64.51889258194073	26.36040950000001

0	68.84901960086293	27.34966950000001

0	73.17914661978513	28.24082950000001

How the Client Works

The script first gets the variable server's port number, and creates a TCP/IP connection to it. The script then configures the variable server session, with the commands listed below, to periodically send the cannonball position with the following commands:

  • trick.var_pause()
  • trick.var_ascii()
  • trick.var_add("dyn.cannon.pos[0]")
  • trick.var_add("dyn.cannon.pos[1]")
  • trick.var_unpause()

The var_pause, and var_unpause commands are generally used at the beginning, and ending of variable server session configurations. var_pause tells the variable server to stop sending data, if it is. var_unpause, tells the variable server to start sending data.

The var_ascii command then tells the variable server to send messages using an ASCII encoding (rather than binary).

The two var_add commands add "dyn.cannon.pos[0]" and "dyn.cannon.pos[1]" to the session variable list.

⚠️ Please notice that the quotes around the variable names must be escaped with the '' (backslash) character.

client_socket.send( b"trick.var_add(\"dyn.cannon.pos[0]\") \n" +
                    b"trick.var_add(\"dyn.cannon.pos[1]\") \n"
                  )

When the var_unpause command is executed, messages containing the values of the variables listed in the session variable list will be repeatedly created, and sent to the client.

By default, the variable server sends data every 0.1 seconds (that is, 10 hertz). This is equivalent to commanding: var_cycle(0.1).

The script then enters a while-loop that repeatedly 1) waits for, 2) reads, and 3) prints the raw responses from the variable server. The responses are encoded in ASCII, as specified by var_ascii, and are of the following format:

0\t<variable1-value>[\t<variable2-value>...\t <variableN-value> ]\n

Getting Values Just Once

Suppose we wanted to get the value of the initial angle of our cannon. We don't need to get it repeatedly, because it doesn't change. We just want to get it once, and then to repeatedly get the position data, which changes over time.

For this situation, we can take one of several approaches. The most straightforward is the var_send_once command, which tells the variable server to send the values sent as arguments immediately, regardless of whether var_pause was previously commanded.

To demonstrate how this works, let's add the following code to our script, right after the line where we sent the var_ascii command.

client_socket.send( b"trick.var_send_once(\"dyn.cannon.init_angle\")\n")
line = insock.readline()
print(line)

In this code, we simply ask the variable server to immediately send the value of dyn.cannon.init_angle, call insock.readline() to wait for a response, and print the response when it is received. var_send_once does not alter the session variable list in any way.

When we run the client, the first few lines of output should look something like:

5	0.5235987755982988

0	0	0

0	0	0

The var_send_once command uses a message type of 5 to allow a programmer to differentiate between normal session variables and var_send_once variables. var_send_once does not alter or interfere with the session variable list, which would allow both of these features to be used simultaneously in a sim.

The var_send_once also allows a user to request multiple variables in a single command. var_send_once can accept a comma-separated list of variables as the first argument and the number of variables in the list as the second argument. In our example, suppose we also wanted to retrieve the initial speed of the cannonball. We could retrieve both variables with a single command:

client_socket.send( b"trick.var_send_once(\"dyn.cannon.init_angle, dyn.cannon.init_speed\", 2)\n")

Now, when we run the client, we get both the init_angle and the init_speed with the first message.

5	0.5235987755982988	50

0	0	0

0	0	0

Another commonly used pattern to retrieve variables only once is to use the var_add, var_send, and var_clear commands. var_send tells the variable server to send all session variables immediately regardless of whether var_pause was previously commanded.

To demonstrate how this works, replace the code in the previous listing with the snippet below, right after the line where we sent the var_ascii command.

client_socket.send( b"trick.var_add(\"dyn.cannon.init_angle\")\n")
client_socket.send( b"trick.var_send()\n" )
line = insock.readline()
print(line)
client_socket.send( b"trick.var_clear()\n" )

In this snippet of code, we add dyn.cannon.init_angle to the session variable list. Then we call var_send to tell the variable server to send us the value, and wait for the response by calling insock.readline(). When it arrives, we print it. Before the script adds the cannon position variables, we need to remove dyn.cannon.init_angle, otherwise we'll be getting this in our messages too. We can do this in one of two ways. We can 1) call var_clear to clear the the list, or 2) we can call var_remove. Specifically we could do the following:

client_socket.send( b"trick.var_remove(\"dyn.cannon.init_angle\")\n" )

So, when we run the modified client, the first three lines of the output should look something like the following.

0	0.5235987755982988

0	0	0

0	0	0

The first line contains the message type (which is zero), followed by the value of dyn.cannon.init_angle. Subsequent lines contain the position data like before.

A More Realistic Example

In the previous example we only called variable server API functions, like trick.var_add, trick.var_send, and so forth. But, we're not just limited to variable server API calls. The variable server's Python interpreter is bound to our simulation's variables and many other functions, including those that we've written ourselves. In this example we'll create a more interactive client, to initialize our simulation, and to control the simulation modes.

The listing below implements a GUI client using Python and tkinter that allows us to:

  1. Set the initial speed and angle of the cannon ball.
  2. Read and display the current simulation MODE.
  3. Set the simulation mode (using a "fire" button).
  4. Animate the flight of the cannon ball in realtime.

Listing - CannonDisplay_Rev2.py

#!/usr/bin/python3
import sys 
import socket
import math
from tkinter import *

# ----------------------------------------------------------------------
# 1.0 Process the command line arguments, to get the port number.
if ( len(sys.argv) == 2) :
    trick_varserver_port = int(sys.argv[1])
else :
    print( "Usage: vsclient <port_number>")
    sys.exit()

# ----------------------------------------------------------------------
# 2.0 Set client Parameters.
HEIGHT, WIDTH = 500, 800   # Canvas Dimensions
MARGIN = 20                # Margins around the axes
SCALE = 3                  # Scale = 3 pixels per meter
ballRadius = 5             # Ball radius in pixels

# ----------------------------------------------------------------------
# 3.0 Create constants for clarity.
MODE_FREEZE = 1 
MODE_RUN = 5 

# ----------------------------------------------------------------------
# 4.0 Create a variable to indicate that we want to "fire" the cannon,
#     and a callback function to set it.
fireCommand = False
def cannonFire():
    global fireCommand
    fireCommand = True

# ----------------------------------------------------------------------
# 5.0 Create the GUI

# 5.1 Create a Canvas to draw on.
tk = Tk()
canvas = Canvas(tk, width=WIDTH, height=HEIGHT)
tk.title("CannonBall Display")
canvas.pack()

# 5.2  Add a FIRE button, whose callback sets the fireCommand variable.
buttonFrame = Frame()
buttonFrame.pack(side=BOTTOM)
fireButton = Button(buttonFrame,text="fire",command=cannonFire)
fireButton.pack(side=LEFT)

# 5.3 Add an Initial Speed Scale.
speedScale = Scale(buttonFrame, from_=5, to=50, label="Initial Speed", orient=HORIZONTAL)
speedScale.pack(side=LEFT)
speedScale.set(50)

# 5.4 Add an Initial Angle Scale.
angleScale = Scale(buttonFrame, from_=5, to=80, label="Initial Angle", orient=HORIZONTAL)
angleScale.pack(side=LEFT)
angleScale.set(30)

# 5.5 Create coordinate axes on the canvas.
xAxis = canvas.create_line(MARGIN,HEIGHT-MARGIN,WIDTH,HEIGHT-MARGIN)
yAxis = canvas.create_line(MARGIN,HEIGHT-MARGIN,MARGIN,0)

# 5.6 Create an oval object to represent the cannonball.
cannonBall = canvas.create_oval(0,0,ballRadius,ballRadius, fill="orange")

# 5.7 Create a text field on the canvas for the simulation mode display.
modeText = canvas.create_text(WIDTH/2, 20, text="--unknown-mode--")

# 5.8 Create text fields on the canvas for time and position of impact display.
impactTimeText = canvas.create_text(WIDTH/2, 40, text="")
impactPosText =  canvas.create_text(WIDTH/2, 60, text="")

# ----------------------------------------------------------------------
# 6.0 Connect to the variable server.
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect( ("localhost", trick_varserver_port) )
insock = client_socket.makefile("r")

# ----------------------------------------------------------------------
# 7.0 Request the cannon ball position, the sim mode, and the impact info.
client_socket.send( b"trick.var_set_client_tag(\"myvsclient\") \n")
client_socket.send( b"trick.var_debug(3)\n" )
client_socket.send( b"trick.var_pause()\n" )
client_socket.send( b"trick.var_ascii()\n" )
client_socket.send( b"trick.var_add(\"dyn.cannon.pos[0]\") \n" +
                    b"trick.var_add(\"dyn.cannon.pos[1]\") \n" +
                    b"trick.var_add(\"trick_sys.sched.mode\")\n" +
                    b"trick.var_add(\"dyn.cannon.impact\") \n" +
                    b"trick.var_add(\"dyn.cannon.impactTime\") \n" )
client_socket.send( b"trick.var_unpause()\n" )

# ----------------------------------------------------------------------
# 8.0 Repeatedly read and process the responses from the variable server.
while(True):
    # 8.1 Read the response line from the variable server.
    line = insock.readline()
    if line == '':
        break

    # 8.2 Split the line into an array of value fields.
    field = line.split("\t")

    # 8.3 Get the position of the ball and update it on the canvas.
    x,y = float(field[1]), float(field[2])
    cx,cy = (x*SCALE+MARGIN), (HEIGHT-y*SCALE-MARGIN)
    canvas.coords(cannonBall,cx-ballRadius,cy-ballRadius,cx+ballRadius,cy+ballRadius)

    # 8.4 Get and display current Sim Mode.
    simMode = int(field[3])
    if simMode == MODE_FREEZE:
        canvas.itemconfigure(modeText, fill="blue", text="FREEZE")
    elif simMode == MODE_RUN:
        canvas.itemconfigure(modeText, fill="red", text="RUN")
    else:
        canvas.itemconfigure(modeText, text="--unknown-mode--")

    # 8.5 When impact occurs display the impact info, and command the sim from RUN mode to FREEZE mode.
    impact = int(field[4])
    if simMode == MODE_RUN:
        if impact:
            # 8.5.1 Display the impact info on the canvas.
            canvas.itemconfigure(impactTimeText, text="Impact time = " + field[5])
            canvas.itemconfigure(impactPosText, text="Impact pos  = (" + field[1] + "," + field[2] + ")")
            # 8.5.2 Command the sim to FREEZE mode.
            client_socket.send( b"trick.exec_freeze()\n")

    # 8.6 When the "Fire" button is pressed, command the sim from FREEZE mode to RUN mode.
    if simMode == MODE_FREEZE:
        if fireCommand:
            fireCommand = False
            fireButton.config(state=DISABLED)
            # 8.6.1 Command the sim to assign the slider values to init_speed, and init_angle.
            client_socket.send( b"dyn.cannon.init_speed = " + bytes(str(speedScale.get()), 'UTF-8') + b" \n")
            client_socket.send( b"dyn.cannon.init_angle = " + bytes(str(angleScale.get()*(math.pi/180.0)), 'UTF-8') + b" \n")
            # 8.6.2 Command the sim to re-run the cannon_init job.
            client_socket.send( b"trick.cannon_init( dyn.cannon )\n")
            # 8.6.3 Command the sim to RUN mode.
            client_socket.send( b"trick.exec_run()\n")
    
    # 8.7 Update the Tk graphics.
    tk.update()

# ----------------------------------------------------------------------
# 9.0 Keep the window open, when the data stops.
tk.mainloop()

Controlling the Simulation Mode from a VS Client

The current simulation mode is stored in the trick_sys.sched.mode variable. So, we request that in addition to our other variables in section 7.0 of the listing.

The only simulation modes that are available to our client are FREEZE, and RUN. The variable server isn't available in other modes. The numeric values of these modes are:

  • MODE_FREEZE = 1
  • MODE_RUN = 5

To set the simulation mode, we need to use the following functions:

  • trick.exec_run() - commands the sim to RUN mode.
  • trick.exec_freeze() - commands the sim to FREEZE mode.

as in sections 8.5, and 8.6 of the listing.

Don't set trick_sys.sched.mode.

Initializing the Simulation from a VS Client

To set simulation values, we simply create and send Python assignment statements.

 client_socket.send( b"dyn.cannon.init_speed = " + str(speedScale.get()) + " \n")
 client_socket.send( b"dyn.cannon.init_angle = " + str(angleScale.get()*(math.pi/180.0)) ) 

Just because the variable server isn't available during INITIALIZATION mode, doesn't mean we can't initialize our sim. We can just call our initialization jobs directly.

client_socket.send( b"trick.cannon_init( dyn.cannon )\n")

Starting a Client From The Input File

Rather than having to start a client each and every time from the command line, we can easily start it from the input file using the function trick.var_server_get_port() as illustrated in the following input file script.

#==================================
# Start the variable server client.
#==================================
varServerPort = trick.var_server_get_port();
CannonDisplay_path = os.environ['HOME'] + "/CannonDisplay_Rev2.py"
if (os.path.isfile(CannonDisplay_path)) :
    CannonDisplay_cmd = CannonDisplay_path + " " + str(varServerPort) + " &" ;
    print(CannonDisplay_cmd)
    os.system( CannonDisplay_cmd);
else :
    print('Oops! Can\'t find ' + CannonDisplay_path )

Add this to the bottom of RUN_test/input.py to give it a try.


Appendix

Variable Server Message Types

Name Value Meaning
VS_IP_ERROR -1 Protocol Error
VS_VAR_LIST 0 A list of variable values.
VS_VAR_EXISTS 1 Response to var_exists( variable_name )
VS_SIE_RESOURCE 2 Response to send_sie_resource
VS_LIST_SIZE 3 Response to var_send_list_size or send_event_data
VS_STDIO 4 Values Redirected from stdio if var_set_send_stdio is enabled
VS_SEND_ONCE 5 Response to var_send_once

The Variable Server API

The following functions are a subset of variable server API functions that are used in this tutorial:

var_add( variable_name ) - Add a name to the session variable list. The value of the added variable will transmitted in subsequent variable server messages.

var_ascii() - Set data response messages to the following ASCII encoded format (default):

0\t<variable1-value>[\t<variable2-value>...\t <variableN-value> ]\n

Where:

  • N is the number of variables in the session variable list.
  • \t is a tab character.

var_binary() - Set response encoding to binary.

var_cycle( period ) - Set data response message period in seconds. (default = 0.1 seconds, i.e., 10 hertz)

var_pause() - Halt periodic responses.

var_unpause() - Resume periodic responses.

var_send() - Send response immediately.

var_send_once( variable_name ) - Immediately send the value of variable_name

var_send_once( variable_list, num_variables ) - Immediately send the value of all variables in the comma separated variable_list, or an error if the number of variables in the list does not match num_variables

var_clear() - Clear the session variable list.

var_exit() - End the connection to the variable server.

var_remove( variable_name ) - Remove the given name from the session variable list.

var_set_client_tag( text ) - Name the current connection, for debugging.

var_debug( level ) - Set the debug level. Set level to 3 for all debug messages, and 0 for no debug messages.

var_sync( mode )

Set the synchronization mode of the variable server session, where the modes are:

  • 0 = fully asynchronous (default)

    This means that periodic data messages are not guaranteed to be time homogeneous. That is, data may not all be associated with a the exact same sim time. The variable server data messages are written from a thread other than the main thread.

  • 1 = sync data gather, async socket write

    This means that periodic data messages are guaranteed to be time homogeneous, but are written from a thread other than the main simulation thread.

  • 2 = sync data gather, sync socket write

    This means that periodic data messages are guaranteed to be time homogeneous, but are written from the main simulation thread.

Next Page