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: