diff --git a/README.md b/README.md old mode 100644 new mode 100755 index bd8f951..d6bf84b --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ python -m pip install https://codeload.github.com/open-stage/python-psn/zip/refs ## Usage +### Receiving PSN data ```python import pypsn @@ -45,15 +46,54 @@ receiver.start() # start the receiving thread receiver.stop() # stop receiving +``` + +### Senfing PSN data +```python +import pypsn + +# create a psn_info object with appropriate data +psn_info = pypsn.PsnInfoPacket( + info=pypsn.PsnInfo( + timestamp=1312, + version_high=2, + version_low=0, + frame_id=1, + packet_count=1, + ), + name="system_name_001", + trackers=( + [ + pypsn.PsnTrackerInfo( + tracker_id=i, + tracker_name="tracker_" + str(i), + ) + for i in range(0, 8)) + ] + ), +) + +# convert the object to a byte string +psn_info_packet_bytes = pypsn.prepare_psn_info_packet_bytes(psn_info) + +# send the PSN info via multicast +pypsn.send_psn_packet( + psn_packet=psn_info_packet_bytes, + mcast_ip="236.10.10.10", + ip_addr="192.168.1.42", + mcast_port=56565, +) + ``` See examples folder for some more examples. ## Development, status - Supporting PSN V2 -- Parsing and sending (via [Multicast Expert](https://github.com/multiplemonomials/multicast_expert)) +- Parsing and sending (via [Multicast Expert](https://github.com/multiplemonomials/multicast_expert)) or via Unicast - Using threading module - Linux (Rpi OS incl.), Windows and macOS tested +- PSN messages recognized by GrandMa3 2.1 - Typed, no-strict - Initial pytest testing provided together with CI/CD setup diff --git a/examples/simple_send_and_receive/send_psn.py b/examples/simple_send_and_receive/send_psn.py index 8cef861..37aab36 100755 --- a/examples/simple_send_and_receive/send_psn.py +++ b/examples/simple_send_and_receive/send_psn.py @@ -1,19 +1,49 @@ #! /bin/env python3 """ -Usage: python send_psn.py 192.168.1.4 <--- change IP address +Usage: + +multicast: python send_psn.py 1 192.168.1.4 236.10.10.10 +unicast: python send_psn.py 1 192.168.1.4 + +Args: +- number of trackers to generate +- local IP adress +- multricast adress (opt) + +Variables mapping for GrandMa3 users: + +- pos.xyz -> Position X Y Z +- speed.xyz -> Speed X Y Z +- ori.xyz -> Rot X Y Z +- accel.xyz -> None +- trgtpos.xyz -> None """ import sys +import time import pypsn +try: + tracker_num = int(sys.argv[1]) + ip_addr = sys.argv[2] + mcast_ip = None + + if sys.argv[3]: + mcast_ip = sys.argv[3] + +except Exception as e: + print("Args: tracker_num ip_addr mcast_ip (opt).") + print(e) + +start_time_us = time.time_ns() // 1000 psn_info = pypsn.PsnInfoPacket( info=pypsn.PsnInfo( - timestamp=1312, + timestamp=start_time_us, version_high=2, version_low=0, - frame_id=56, + frame_id=1, packet_count=1, ), name="system_name_001", @@ -23,13 +53,19 @@ tracker_id=i, tracker_name="tracker_" + str(i), ) - for i in range(0, 7) + for i in range(0, tracker_num) ] ), ) psn_data = pypsn.PsnDataPacket( - info=psn_info.info, + info=pypsn.PsnInfo( + timestamp=start_time_us, + version_high=2, + version_low=0, + frame_id=1, + packet_count=1, + ), trackers=( [ pypsn.PsnTracker( @@ -50,6 +86,7 @@ y=0.0, z=0.0, ), + status=0.5, accel=pypsn.PsnVector3( x=0.0, y=0.0, @@ -60,15 +97,14 @@ y=0.0, z=0.0, ), - status=0, - timestamp=psn_info.info.timestamp, + timestamp=start_time_us, ) for tracker in psn_info.trackers ] ), ) -print("\n--- PSN infos to be sent ---") +print("\n--- PSN infos to be sent at 1Hz ---") print("system name: " + psn_info.name) print("timestamp: " + str(psn_info.info.timestamp)) print("version_high: " + str(psn_info.info.version_high)) @@ -79,7 +115,7 @@ for tracker in psn_info.trackers: print(str(tracker.tracker_id) + ": " + str(tracker.tracker_name)) -print("\n--- PSN data to be sent ---") +print("\n--- PSN data to be sent at 60Hz ---") print("timestamp: " + str(psn_data.info.timestamp)) print("version_high: " + str(psn_data.info.version_high)) print("version_low: " + str(psn_data.info.version_low)) @@ -108,19 +144,48 @@ + str(tracker.timestamp) ) -psn_info_packet_bytes = pypsn.prepare_psn_info_packet_bytes(psn_info) -psn_data_packet_bytes = pypsn.prepare_psn_data_packet_bytes(psn_data) +print("\n--- Sendin PSN data in loop ---") +counter = 0.0 -pypsn.send_psn_packet( - psn_packet=psn_info_packet_bytes, - mcast_ip="236.10.10.10", - ip_addr=sys.argv[1], - mcast_port=56565, -) +while True: + time.sleep(1 / 60) + elapsed_time_us = time.time_ns() // 1000 - start_time_us -pypsn.send_psn_packet( - psn_packet=psn_data_packet_bytes, - mcast_ip="236.10.10.10", - ip_addr=sys.argv[1], - mcast_port=56565, -) + if counter < 6.0: + counter += 0.1 + else: + counter = 0.0 + + psn_info.info.timestamp = elapsed_time_us + psn_info_packet_bytes = pypsn.prepare_psn_info_packet_bytes(psn_info) + + pypsn.send_psn_packet( + psn_packet=psn_info_packet_bytes, + mcast_ip=mcast_ip, + ip_addr=ip_addr, + port=56565, + ) + + psn_data.info.timestamp = elapsed_time_us + + if psn_data.info.frame_id < 255: + psn_data.info.frame_id += 1 + else: + psn_data.info.frame_id = 0 + + for tracker in psn_data.trackers: + tracker.timestamp = elapsed_time_us + tracker.pos.x = counter + tracker.speed.x = counter + tracker.ori.x = counter + tracker.accel.x = counter + tracker.trgtpos.x = counter + + psn_data_packet_bytes = pypsn.prepare_psn_data_packet_bytes(psn_data) + + pypsn.send_psn_packet( + psn_packet=psn_data_packet_bytes, + mcast_ip=mcast_ip, + ip_addr=ip_addr, + port=56565, + ) diff --git a/pypsn/__init__.py b/pypsn/__init__.py index ad94867..7d6af1b 100755 --- a/pypsn/__init__.py +++ b/pypsn/__init__.py @@ -15,7 +15,9 @@ class PsnVector3: - """_summary_""" + """ + PSN vector variable structure + """ def __init__(self, x: float, y: float, z: float): self.x = x @@ -33,7 +35,9 @@ def __iter__(self): class PsnInfo: - """_summary_""" + """ + PSN info packet variable structure + """ def __init__( self, @@ -51,7 +55,9 @@ def __init__( class PsnTrackerInfo: - """_summary_""" + """ + PSN tracker info variable structure + """ def __init__(self, tracker_id: int, tracker_name: str): self.tracker_id = tracker_id @@ -59,7 +65,9 @@ def __init__(self, tracker_id: int, tracker_name: str): class PsnTracker: - """_summary_""" + """ + PSN tracker data variable structure + """ def __init__( self, @@ -85,7 +93,9 @@ def __init__( class PsnDataPacket: - """_summary_""" + """ + PSN data packet variable structure + """ def __init__(self, info: "PsnInfo", trackers: List["PsnTracker"]): self.info = info @@ -93,7 +103,9 @@ def __init__(self, info: "PsnInfo", trackers: List["PsnTracker"]): class PsnInfoPacket: - """_summary_""" + """ + PSN info packet variable structure + """ def __init__( self, @@ -107,10 +119,8 @@ def __init__( class PsnV1Chunk(IntEnum): - """_summary_ - - Args: - IntEnum (_type_): _description_ + """ + V1 main PSN ID """ PSN_V1_INFO_PACKET = 0x503C @@ -118,10 +128,8 @@ class PsnV1Chunk(IntEnum): class PsnV2Chunck(IntEnum): - """_summary_ - - Args: - IntEnum (_type_): _description_ + """ + V2 main PSN ID """ PSN_INFO_PACKET = 0x6756 @@ -129,10 +137,8 @@ class PsnV2Chunck(IntEnum): class PsnInfoChunk(IntEnum): - """_summary_ - - Args: - IntEnum (_type_): _description_ + """ + PSN ID for info packet """ PSN_INFO_PACKET_HEADER = 0x0000 @@ -141,10 +147,8 @@ class PsnInfoChunk(IntEnum): class PsnDataChunk(IntEnum): - """_summary_ - - Args: - IntEnum (_type_): _description_ + """ + PSN ID for data packet """ PSN_DATA_PACKET_HEADER = 0x0000 @@ -152,20 +156,16 @@ class PsnDataChunk(IntEnum): class PasnTrackerListChunk(IntEnum): - """_summary_ - - Args: - IntEnum (_type_): _description_ + """ + PSN ID for trackers name """ PSN_INFO_TRACKER_NAME = 0x0000 class PsnTrackerChunk(IntEnum): - """_summary_ - - Args: - IntEnum (_type_): _description_ + """ + PSN ID for trackers coordinates """ PSN_DATA_TRACKER_POS = 0x0000 @@ -176,10 +176,8 @@ class PsnTrackerChunk(IntEnum): class PsnTrackerChunkInfo(IntEnum): - """_summary_ - - Args: - IntEnum (_type_): _description_ + """ + PSN ID for trackers status and timestamp """ PSN_DATA_TRACKER_STATUS = 0x0003 @@ -187,15 +185,16 @@ class PsnTrackerChunkInfo(IntEnum): def join_multicast_windows(mcast_grp, mcast_port, if_ip): - """_summary_ + """ + Join multicast on Windows Args: - mcast_grp (_type_): _description_ - mcast_port (_type_): _description_ - if_ip (_type_): _description_ + mcast_grp (str): multicast ip + mcast_port (str): multicast port + if_ip (str): iface ip Returns: - _type_: _description_ + socket """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -209,15 +208,16 @@ def join_multicast_windows(mcast_grp, mcast_port, if_ip): def join_multicast_posix(mcast_grp, mcast_port, if_ip): - """_summary_ + """ + Join multicast on posix OS Args: - mcast_grp (_type_): _description_ - mcast_port (_type_): _description_ - if_ip (_type_): _description_ + mcast_grp (str): multicast ip + mcast_port (str): multicast port + if_ip (str): iface ip Returns: - _type_: _description_ + socket """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) try: @@ -238,10 +238,11 @@ def join_multicast_posix(mcast_grp, mcast_port, if_ip): def determine_os(): - """_summary_ + """ + Get local OS Returns: - _type_: _description_ + OS name """ if os.name == "nt": return str(os.name) @@ -254,10 +255,8 @@ def determine_os(): class Receiver(Thread): - """_summary_ - - Args: - Thread (_type_): _description_ + """ + PSN receiver class """ def __init__(self, callback, ip_addr="0.0.0.0", mcast_port=56565, timeout=2): @@ -269,14 +268,18 @@ def __init__(self, callback, ip_addr="0.0.0.0", mcast_port=56565, timeout=2): self.socket.settimeout(timeout) def stop(self): - """_summary_""" + """ + Stop listening. + """ self.running = False if self.socket is not None: self.socket.close() self.join() def run(self): - """_summary_""" + """ + Start listnening. + """ data = "" if self.socket is None: return @@ -291,14 +294,15 @@ def run(self): def get_socket(ip_addr, mcast_port): - """_summary_ + """ + Listening for incomming psn packets Args: - ip_addr (_type_): _description_ - mcast_port (_type_): _description_ + ip_addr (str): iface ip + mcast_port (str): multicast port Returns: - _type_: _description_ + socket """ mcast_grp = "236.10.10.10" sock = None @@ -314,13 +318,14 @@ def get_socket(ip_addr, mcast_port): def parse_psn_packet(buffer): - """_summary_ + """ + Parse received data buffer Args: - buffer (_type_): _description_ + buffer (bytes): Received PSN data Returns: - _type_: _description_ + psn packet """ psn_id = unpack("