If you run a 3DOF motion simulator and want to build a bridge from OpenSR to your own platform (or want to pass the info the the manufacturer of your 3DOF), I've included a simple Python UDP client example in the plugin folder. It decodes the UDP packets sent by the plugin's built-in server, the same packet layout works in any language (C#, C++, Python, etc.).
\Users\<username>\Documents\OpenSR\OutPlugins\3DOFViewSimulator\python\3dof_udp_server_test.py
A quick read through the script should be all you need to connect your motion platform.
import socket
import ctypes
import time
# --- Configuration ---
UDP_IP = "0.0.0.0"
UDP_PORT = 33442 # <--- Updated Port
TIMEOUT_SECONDS = 1.5
# --- Struct Definition ---
# /*
# This plugin outputs physics telemetry in a unified Left-Handed Z-Forward, Y-Up convention.
# All games are normalized to this format before being sent over UDP.
# Surge (+Z): Forward acceleration.
# Sway (+X): Force pushing to the right.
# Heave (+Y): Upward vertical force.
# Pitch (+X rot): Nose up.
# Roll (+Z rot): Tilt right.
# Yaw (+Y rot): Turn right.
# */
# struct OpenSR3DOFMotionPacket {
# uint32_t packetId;
# uint8_t paused;
# uint8_t stroke; // 100mmm or 150mm
# // 1. THE LAZY DEV CHANNEL (Direct Actuator Outputs)
# float posFL // mm direct target lengths (0.0 to MaxStroke)
# float posFR
# float posRL
# float posRR
# float tractionLoss_pos; // mm dedicated traction loss/rear-slide actuator move
# // 2. THE ADVANCED DEV CHANNEL (Normalized Game Telemetry)
# double pitch_X;
# double yaw_Y;
# double roll_Z;
# // local acceleration (Z-Forward, Y-Up)
# double sway_X;
# double heave_Y;
# double surge_Z;
# // Rotational/Angular Velocities (Rates)
# double pitchRate_X;
# double yawRate_Y;
# double rollRate_Z;
# };
class MotionPacket(ctypes.Structure):
_pack_ = 1
_fields_ = [
("packetId", ctypes.c_uint32),
("paused", ctypes.c_uint8),
("stroke", ctypes.c_uint8),
("posFL", ctypes.c_float),
("posFR", ctypes.c_float),
("posRL", ctypes.c_float),
("posRR", ctypes.c_float),
("tl", ctypes.c_float),
("pitch", ctypes.c_double),
("yaw", ctypes.c_double),
("roll", ctypes.c_double),
("sway", ctypes.c_double),
("heave", ctypes.c_double),
("surge", ctypes.c_double),
("pitchRate", ctypes.c_double),
("yawRate", ctypes.c_double),
("rollRate", ctypes.c_double),
# ("suspFL", ctypes.c_float), // not yet implemented
# ("suspFR", ctypes.c_float),
# ("suspRL", ctypes.c_float),
# ("suspRR", ctypes.c_float),
]
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.bind((UDP_IP, UDP_PORT))
sock.settimeout(TIMEOUT_SECONDS)
except OSError as e:
print(f"Error binding port {UDP_PORT}: {e}")
return
expected_size = ctypes.sizeof(MotionPacket)
print(f"Listening on port {UDP_PORT}...")
print(f"Packet Size: {expected_size} bytes")
# --- UPS Calculation Variables ---
last_time = time.time()
packet_count = 0
current_ups = 0
while True:
try:
data, addr = sock.recvfrom(1024)
# 1. Update Counters
packet_count += 1
curr_time = time.time()
# 2. Check if 1 second has passed
time_diff = curr_time - last_time
if time_diff >= 1.0:
current_ups = packet_count / time_diff # Accurate calc even if > 1.0s
packet_count = 0
last_time = curr_time
# 3. Process Data
if len(data) >= expected_size:
pkt = MotionPacket.from_buffer_copy(data)
status = "PAUSED" if pkt.paused else "RUNNING"
# Print Data + UPS
# I formatted UPS to show on the far left
print(f"[UPS: {int(current_ups):<4}] ID: {pkt.packetId:<8} | {status:<7} | "
f"FL: {pkt.posFL:6.2f} | FR: {pkt.posFR:6.2f} | "
f"RL: {pkt.posRL:6.2f} | RR: {pkt.posRR:6.2f} | TL: {pkt.tl:6.2f} | "
f"SURGE: {pkt.surge:6.2f} | SWAY: {pkt.sway:6.2f} | "
f"HEAVE: {pkt.heave:6.2f} | YAW: {pkt.yaw:6.2f} | YAWRATE: {pkt.yawRate:6.2f}")
else:
print(f"Size Mismatch: {len(data)} bytes (expected {expected_size})")
except socket.timeout:
# If timed out, UPS is effectively 0
if current_ups > 0:
print("\n>>> PAUSED OR NO SIGNAL (UPS: 0) <<<\n")
current_ups = 0
except KeyboardInterrupt:
break
sock.close()
if __name__ == "__main__":
main()