Message Boards Message Boards

Interacting with real-time simulations using the embedded TCP/IP server

Posted 1 year ago

All simulations created with System Modeler contains an embedded TCP/IP server that be used to control the simulation (start/pause/stop), set top-level input values, query it for variables and variable values and stream real-time data. It uses its own custom protocol documented here.

At first sight it might seem daunting to implement a custom network protcol, but it actually doesn't require that much code. Here I'm going to sketch out how to do it in Python, but it should be fairly easy to replicate in any high-level language.

To start off we define the packet header, according to the documentation it consists of 8 bytes:

  • 1: Protocol version
  • 2: Packet type
  • 3 - 4: Unused
  • 5 - 8: Payload length

Expressing that in the format used by pack and unpack in the Python struct module it becomes:

WSMCOM_HEADER_FORMAT = "<BBBBI" # The header format
WSMCOM_HEADER_SIZE = struct.calcsize(WSMCOM_HEADER_FORMAT) # Number of bytes in the header

Then we define the various constants needed to create the header in this example (there are more packet types related simulation data sessions).

WSMCOM_VERSION = 1 # The protocol version
WSMCOM_HELLO_SCS = 1 # Packet type to initiate a control session
WSMCOM_CMD = 3 # Packet type for commands
WSMCOM_CMD_REPLY = 4 # Packet type for the response to a successful action
WSMCOM_CMD_ERROR = 5 # Packet type for the response to an unsuccessful action

To talk to the server we are going to need functions for sending and receiving packets according to the specified format. So we define a sendWSMComPacket function that takes a socket (sock), a packet type (pktType) and an optional payload (payload) as arguments. It then packs the information in the header using struct.pack and sends the header data together with the optional payload data on the given socket.

def sendWSMComPacket(sock, pktType, payload = bytearray()):
    header = struct.pack(WSMCOM_HEADER_FORMAT, WSMCOM_VERSION, pktType, 0, 0, len(payload))
    sock.sendall(header)
    if len(payload):
        sock.sendall(payload)

The receiving function receiveWSMComReply only takes a socket (sock) as argument. To start with it receives WSMCOM_HEADER_SIZE bytes, i.e. the header data. The header data is the unpacked into its parts using struct.unpack. Now we know the payload lenght and can read the rest of the data in the packet. Here we have used a small utility function receiveData that receives a fixed number of bytes from a socket.

def receiveWSMComReply(sock):
    headerData = receiveData(sock, WSMCOM_HEADER_SIZE)
    (version, packetType, field1, field2, payloadLength) = struct.unpack(WSMCOM_HEADER_FORMAT, headerData)
    payload = receiveData(sock, payloadLength)
    return (version, packetType, payload)

We now have all the building blocks to create a small client for simulation control session part of the protocol. We start with creating the connection to the server:

with socket.create_connection((address, int(port))) as conn:

To initiate a simulation control sessions, we need to send a WSMCOM_HELLO_SCS packet and recevie the reply:

sendWSMComPacket(conn, WSMCOM_HELLO_SCS)
(version, packetType, payload) = receiveWSMComReply(conn)

We then check that we are using the same protcol version and that the session was initialized successfuly (that the reply was of type WSMCOM_CMD_REPLY)

if version != WSMCOM_VERSION:
sys.exit("Incompatible protocol version {} expected {}.".format(version, WSMCOM_VERSION))
if packetType != WSMCOM_CMD_REPLY:
sys.exit("Unexpected reply (type = {}): {}".format(packetType, payload.decode('utf-8')))

We can print the payload which is going to be the assigned client id. We do not need it for this example, but it's used for setting up a simulation data session.

print("Assigned client ID: {}".format(payload.decode('utf-8')))

Finnaly we read in commands and send them as a WSMCOM_CMD packets and receives and prints the reply.

while True:
cmd = input("Enter command: ")
if len(cmd) == 0:
break
sendWSMComPacket(conn, WSMCOM_CMD, cmd.encode('utf-8'))
(version, packetType, payload) = receiveWSMComReply(conn)
if packetType == WSMCOM_CMD_REPLY:
print("Reply: {}".format(payload.decode('utf-8')))
elif packetType == WSMCOM_CMD_ERROR:
print("Error: {}".format(payload.decode('utf-8')))
else:
print("Unexpected packet type = {}: {}".format(packetType, payload.decode('utf-8')))

I have attached the complete Python code for this, it takes IP:PORT as an argument. When starting a simulation Simulation Center the IP and PORT are listed in the simulation log:

[GENERAL   ] Server listening on 127.0.0.1:63597

A sample session connected to a real time simulation of Modelica.Blocks.Math.Gain with k=2.0 (the gain) could look like this:

Connecting to 127.0.0.1:63597
Assigned client ID: {3}
Enter command: getVariableNames()
Reply: {"u", "y", "k"}
Enter command: getVariableValues({"y"})
Reply: {0}
Enter command: getInputVariableNames()
Reply: {"u"}
Enter command: setInputValues({"u", 2.3})
Reply: {true}
Enter command: getVariableValues({"y"})
Reply: {4.6}

Here we used getVariableNames to list all variables in the model and getVariableValues to get the value of the ouput (y) from the gain block. We then list all the input variables with getInputVariableNames and set the value of the input (u) to 2.3. Finally we use getVariableValues again to observe the result on the output.

Attachments:
POSTED BY: Otto Tronarp

enter image description here -- you have earned Featured Contributor Badge enter image description here Your exceptional post has been selected for our editorial column Staff Picks http://wolfr.am/StaffPicks and Your Profile is now distinguished by a Featured Contributor Badge and is displayed on the Featured Contributor Board. Thank you!

POSTED BY: EDITORIAL BOARD
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract