diff --git a/conf/control_panel_example.xml b/conf/control_panel_example.xml index 2f8d66aaa2..c3b284e2e1 100644 --- a/conf/control_panel_example.xml +++ b/conf/control_panel_example.xml @@ -47,6 +47,7 @@ + diff --git a/sw/ground_segment/misc/natnet2ivy.c b/sw/ground_segment/misc/natnet2ivy.c index 02f5c86b9b..e59ad6f3a0 100644 --- a/sw/ground_segment/misc/natnet2ivy.c +++ b/sw/ground_segment/misc/natnet2ivy.c @@ -723,8 +723,11 @@ static gboolean sample_data(GIOChannel *chan, GIOCondition cond, gpointer data) bytes_data += udp_socket_recv(&natnet_data, buffer_data, MAX_PACKETSIZE); // Parse NatNet data - if (bytes_data >= 2 && bytes_data >= buffer_data[1]) { - natnet_parse(buffer_data); + if (bytes_data >= 2) { + uint16_t packet_size = ((uint16_t)buffer_data[3])<<8 | (uint16_t)buffer_data[2]; + if( bytes_data - 4 >= packet_size) { // 4 bytes for message id and packet size + natnet_parse(buffer_data); + } bytes_data = 0; } diff --git a/sw/ground_segment/python/natnet3.x/NatNetClient.py b/sw/ground_segment/python/natnet3.x/NatNetClient.py new file mode 100644 index 0000000000..c45fcee8d6 --- /dev/null +++ b/sw/ground_segment/python/natnet3.x/NatNetClient.py @@ -0,0 +1,510 @@ +# +# Modified version of the NatNet 3.0 Python Client example from NatNetSDK +# + +import socket +import struct +from threading import Thread + +# Create structs for reading various object types to speed up parsing. +Vector3 = struct.Struct( '= 2 ): + # Marker ID's + for i in markerCountRange: + id = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "\tMarker ID", i, ":", id ) + + # Marker sizes + for i in markerCountRange: + size = FloatValue.unpack( data[offset:offset+4] ) + offset += 4 + self.__trace( "\tMarker Size", i, ":", size[0] ) + + # Skip padding inserted by the server + if( self.__natNetStreamVersion[0] > 2 ): + offset += 4 + + if( self.__natNetStreamVersion[0] >= 2 ): + markerError, = FloatValue.unpack( data[offset:offset+4] ) + offset += 4 + self.__trace( "\tMarker Error:", markerError ) + + # Version 2.6 and later + if( ( ( self.__natNetStreamVersion[0] == 2 ) and ( self.__natNetStreamVersion[1] >= 6 ) ) or self.__natNetStreamVersion[0] > 2 or self.__natNetStreamVersion[0] == 0 ): + param, = struct.unpack( 'h', data[offset:offset+2] ) + trackingValid = ( param & 0x01 ) != 0 + offset += 2 + self.__trace( "\tTracking Valid:", 'True' if trackingValid else 'False' ) + + return offset + + # Unpack a skeleton object from a data packet + def __unpackSkeleton( self, data ): + offset = 0 + + id = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "ID:", id ) + + rigidBodyCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Rigid Body Count:", rigidBodyCount ) + for j in range( 0, rigidBodyCount ): + offset += self.__unpackRigidBody( data[offset:] ) + + return offset + + # Unpack data from a motion capture frame message + def __unpackMocapData( self, data ): + self.__trace( "Begin MoCap Frame\n-----------------\n" ) + + data = memoryview( data ) + offset = 0 + + # Frame number (4 bytes) + frameNumber = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Frame #:", frameNumber ) + + # Marker set count (4 bytes) + markerSetCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Marker Set Count:", markerSetCount ) + + for i in range( 0, markerSetCount ): + # Model name + modelName, separator, remainder = bytes(data[offset:]).partition( b'\0' ) + offset += len( modelName ) + 1 + self.__trace( "Model Name:", modelName.decode( 'utf-8' ) ) + + # Marker count (4 bytes) + markerCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Marker Count:", markerCount ) + + for j in range( 0, markerCount ): + pos = Vector3.unpack( data[offset:offset+12] ) + offset += 12 + #self.__trace( "\tMarker", j, ":", pos[0],",", pos[1],",", pos[2] ) + + # Unlabeled markers count (4 bytes) + unlabeledMarkersCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Unlabeled Markers Count:", unlabeledMarkersCount ) + + for i in range( 0, unlabeledMarkersCount ): + pos = Vector3.unpack( data[offset:offset+12] ) + offset += 12 + self.__trace( "\tMarker", i, ":", pos[0],",", pos[1],",", pos[2] ) + + # Rigid body count (4 bytes) + rigidBodyCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Rigid Body Count:", rigidBodyCount ) + + for i in range( 0, rigidBodyCount ): + offset += self.__unpackRigidBody( data[offset:] ) + + # Version 2.1 and later + skeletonCount = 0 + if( ( self.__natNetStreamVersion[0] == 2 and self.__natNetStreamVersion[1] > 0 ) or self.__natNetStreamVersion[0] > 2 ): + skeletonCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Skeleton Count:", skeletonCount ) + for i in range( 0, skeletonCount ): + offset += self.__unpackSkeleton( data[offset:] ) + + # Labeled markers (Version 2.3 and later) + labeledMarkerCount = 0 + if( ( self.__natNetStreamVersion[0] == 2 and self.__natNetStreamVersion[1] > 3 ) or self.__natNetStreamVersion[0] > 2 ): + labeledMarkerCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Labeled Marker Count:", labeledMarkerCount ) + for i in range( 0, labeledMarkerCount ): + id = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + pos = Vector3.unpack( data[offset:offset+12] ) + offset += 12 + size = FloatValue.unpack( data[offset:offset+4] ) + offset += 4 + + # Version 2.6 and later + if( ( self.__natNetStreamVersion[0] == 2 and self.__natNetStreamVersion[1] >= 6 ) or self.__natNetStreamVersion[0] > 2 or self.__natNetStreamVersion[0] == 0 ): + param, = struct.unpack( 'h', data[offset:offset+2] ) + offset += 2 + occluded = ( param & 0x01 ) != 0 + pointCloudSolved = ( param & 0x02 ) != 0 + modelSolved = ( param & 0x04 ) != 0 + + # Version 3.0 and later + if( self.__natNetStreamVersion[0] >= 3 or self.__natNetStreamVersion[0] == 0 ): + residual, = FloatValue.unpack( data[offset:offset+4] ) + offset += 4 + self.__trace( "Residual:", residual ) + + # Force Plate data (version 2.9 and later) + if( ( self.__natNetStreamVersion[0] == 2 and self.__natNetStreamVersion[1] >= 9 ) or self.__natNetStreamVersion[0] > 2 ): + forcePlateCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Force Plate Count:", forcePlateCount ) + for i in range( 0, forcePlateCount ): + # ID + forcePlateID = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Force Plate", i, ":", forcePlateID ) + + # Channel Count + forcePlateChannelCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + # Channel Data + for j in range( 0, forcePlateChannelCount ): + self.__trace( "\tChannel", j, ":", forcePlateID ) + forcePlateChannelFrameCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + for k in range( 0, forcePlateChannelFrameCount ): + forcePlateChannelVal = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "\t\t", forcePlateChannelVal ) + + # Device data (version 2.11 and later) + if( ( self.__natNetStreamVersion[0] == 2 and self.__natNetStreamVersion[1] >= 11 ) or self.__natNetStreamVersion[0] > 2 ): + deviceCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Device Count:", deviceCount ) + for i in range( 0, deviceCount ): + # ID + deviceID = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "Device", i, ":", deviceID ) + + # Channel Count + deviceChannelCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + # Channel Data + for j in range( 0, deviceChannelCount ): + self.__trace( "\tChannel", j, ":", deviceID ) + deviceChannelFrameCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + for k in range( 0, deviceChannelFrameCount ): + deviceChannelVal = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + self.__trace( "\t\t", deviceChannelVal ) + + # Timecode + timecode = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + timecodeSub = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + # Timestamp (increased to double precision in 2.7 and later) + if( ( self.__natNetStreamVersion[0] == 2 and self.__natNetStreamVersion[1] >= 7 ) or self.__natNetStreamVersion[0] > 2 ): + timestamp, = DoubleValue.unpack( data[offset:offset+8] ) + offset += 8 + else: + timestamp, = FloatValue.unpack( data[offset:offset+4] ) + offset += 4 + + # Hires Timestamp (Version 3.0 and later) + if( self.__natNetStreamVersion[0] >= 3 or self.__natNetStreamVersion[0] == 0 ): + stampCameraExposure = int.from_bytes( data[offset:offset+8], byteorder='little' ) + offset += 8 + stampDataReceived = int.from_bytes( data[offset:offset+8], byteorder='little' ) + offset += 8 + stampTransmit = int.from_bytes( data[offset:offset+8], byteorder='little' ) + offset += 8 + + # Frame parameters + param, = struct.unpack( 'h', data[offset:offset+2] ) + isRecording = ( param & 0x01 ) != 0 + trackedModelsChanged = ( param & 0x02 ) != 0 + offset += 2 + + # Send information to any listener. + if self.newFrameListener is not None: + self.newFrameListener( frameNumber, markerSetCount, unlabeledMarkersCount, rigidBodyCount, skeletonCount, + labeledMarkerCount, timecode, timecodeSub, timestamp, isRecording, trackedModelsChanged ) + + # Unpack a marker set description packet + def __unpackMarkerSetDescription( self, data ): + offset = 0 + + name, separator, remainder = bytes(data[offset:]).partition( b'\0' ) + offset += len( name ) + 1 + self.__trace( "Markerset Name:", name.decode( 'utf-8' ) ) + + markerCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + for i in range( 0, markerCount ): + name, separator, remainder = bytes(data[offset:]).partition( b'\0' ) + offset += len( name ) + 1 + self.__trace( "\tMarker Name:", name.decode( 'utf-8' ) ) + + return offset + + # Unpack a rigid body description packet + def __unpackRigidBodyDescription( self, data ): + offset = 0 + + # Version 2.0 or higher + if( self.__natNetStreamVersion[0] >= 2 ): + name, separator, remainder = bytes(data[offset:]).partition( b'\0' ) + offset += len( name ) + 1 + self.__trace( "\tMarker Name:", name.decode( 'utf-8' ) ) + + id = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + parentID = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + timestamp = Vector3.unpack( data[offset:offset+12] ) + offset += 12 + + return offset + + # Unpack a skeleton description packet + def __unpackSkeletonDescription( self, data ): + offset = 0 + + name, separator, remainder = bytes(data[offset:]).partition( b'\0' ) + offset += len( name ) + 1 + self.__trace( "\tMarker Name:", name.decode( 'utf-8' ) ) + + id = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + rigidBodyCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + for i in range( 0, rigidBodyCount ): + offset += self.__unpackRigidBodyDescription( data[offset:] ) + + return offset + + # Unpack a data description packet + def __unpackDataDescriptions( self, data ): + offset = 0 + datasetCount = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + + for i in range( 0, datasetCount ): + type = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + if( type == 0 ): + offset += self.__unpackMarkerSetDescription( data[offset:] ) + elif( type == 1 ): + offset += self.__unpackRigidBodyDescription( data[offset:] ) + elif( type == 2 ): + offset += self.__unpackSkeletonDescription( data[offset:] ) + + def __dataThreadFunction( self, sock ): + sock.settimeout(0.5) + while self.running: + # Block for input + try: + data, addr = sock.recvfrom( 32768 ) # 32k byte buffer size + if( len( data ) >= 4): + self.__processMessage( data ) + except socket.timeout: + pass + + def __processMessage( self, data ): + self.__trace( "Begin Packet\n------------\n" ) + + messageID = int.from_bytes( data[0:2], byteorder='little' ) + self.__trace( "Message ID:", messageID ) + + packetSize = int.from_bytes( data[2:4], byteorder='little' ) + self.__trace( "Packet Size:", packetSize ) + + if not len( data ) - 4 >= packetSize: + # Not enough data + return + + offset = 4 + if( messageID == self.NAT_FRAMEOFDATA ): + self.__unpackMocapData( data[offset:] ) + elif( messageID == self.NAT_MODELDEF ): + self.__unpackDataDescriptions( data[offset:] ) + elif( messageID == self.NAT_PINGRESPONSE ): + offset += 256 # Skip the sending app's Name field + offset += 4 # Skip the sending app's Version info + self.__natNetStreamVersion = struct.unpack( 'BBBB', data[offset:offset+4] ) + offset += 4 + elif( messageID == self.NAT_RESPONSE ): + if( packetSize == 4 ): + commandResponse = int.from_bytes( data[offset:offset+4], byteorder='little' ) + offset += 4 + else: + message, separator, remainder = bytes(data[offset:]).partition( b'\0' ) + offset += len( message ) + 1 + self.__trace( "Command response:", message.decode( 'utf-8' ) ) + elif( messageID == self.NAT_UNRECOGNIZED_REQUEST ): + self.__trace( "Received 'Unrecognized request' from server" ) + elif( messageID == self.NAT_MESSAGESTRING ): + message, separator, remainder = bytes(data[offset:]).partition( b'\0' ) + offset += len( message ) + 1 + self.__trace( "Received message from server:", message.decode( 'utf-8' ) ) + else: + self.__trace( "ERROR: Unrecognized packet type" ) + + self.__trace( "End Packet\n----------\n" ) + + def sendCommand( self, command, commandStr, socket, address ): + # Compose the message in our known message format + if( command == self.NAT_REQUEST_MODELDEF or command == self.NAT_REQUEST_FRAMEOFDATA ): + packetSize = 0 + commandStr = "" + elif( command == self.NAT_REQUEST ): + packetSize = len( commandStr ) + 1 + elif( command == self.NAT_PING ): + commandStr = "Ping" + packetSize = len( commandStr ) + 1 + + data = command.to_bytes( 2, byteorder='little' ) + data += packetSize.to_bytes( 2, byteorder='little' ) + + data += commandStr.encode( 'utf-8' ) + data += b'\0' + + socket.sendto( data, address ) + + def run( self ): + # Set running flag to True + self.running = True + + # Create the data socket + self.dataSocket = self.__createDataSocket( self.dataPort ) + if( self.dataSocket is None ): + print( "Could not open data channel" ) + exit + + # Create the command socket + self.commandSocket = self.__createCommandSocket() + if( self.commandSocket is None ): + print( "Could not open command channel" ) + exit + + # Create a separate thread for receiving data packets + dataThread = Thread( target = self.__dataThreadFunction, args = (self.dataSocket, )) + dataThread.start() + + # Create a separate thread for receiving command packets + commandThread = Thread( target = self.__dataThreadFunction, args = (self.commandSocket, )) + commandThread.start() + + self.sendCommand( self.NAT_REQUEST_MODELDEF, "", self.commandSocket, (self.serverIPAddress, self.commandPort) ) + + def stop( self ): + self.running = False + diff --git a/sw/ground_segment/python/natnet3.x/natnet2ivy.py b/sw/ground_segment/python/natnet3.x/natnet2ivy.py new file mode 100755 index 0000000000..ee239d2964 --- /dev/null +++ b/sw/ground_segment/python/natnet3.x/natnet2ivy.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2017 Gautier Hattenberger +# +# This file is part of paparazzi. +# +# paparazzi is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# paparazzi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with paparazzi; see the file COPYING. If not, see +# . +# + +''' +Forward rigid body position from NatNet (Optitrack positioning system) +to the IVY bus as a REMOTE_GPS_LOCAL message + +As the NatNetClient is only compatible with Python 3.x, the Ivy python +should be installed for this version, eventually by hand as paparazzi +packages are only providing an install for Python 2.x (although the +source code itself is compatile for both version) + +Manual installation of Ivy: + 1. git clone https://gitlab.com/ivybus/ivy-python.git + 2. cd ivy-python + 3. sudo python3 setup.py install +Otherwise, you can use PYTHONPATH if you don't want to install the code +in your system + +''' + + +from __future__ import print_function + +import sys +from os import path, getenv +from time import time, sleep +import numpy as np +from collections import deque +import argparse + +# import NatNet client +from NatNetClient import NatNetClient + +# if PAPARAZZI_HOME not set, then assume the tree containing this +# file is a reasonable substitute +PPRZ_HOME = getenv("PAPARAZZI_HOME", path.normpath(path.join(path.dirname(path.abspath(__file__)), '../../../../'))) +sys.path.append(PPRZ_HOME + "/var/lib/python") +sys.path.append(PPRZ_HOME + "/var/lib/python/pprzlink") # seems needed for messages_xml_map file +from pprzlink.ivy import IvyMessagesInterface +from pprzlink.message import PprzMessage + +# parse args +parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) +parser.add_argument('-ac', action='append', nargs=2, + metavar=('rigid_id','ac_id'), help='pair of rigid body and A/C id (multiple possible)') +parser.add_argument('-b', '--ivy_bus', dest='ivy_bus', help="Ivy bus address and port") +parser.add_argument('-s', '--server', dest='server', default="127.0.0.1", help="NatNet server IP address") +parser.add_argument('-m', '--multicast_addr', dest='multicast', default="239.255.42.99", help="NatNet server multicast address") +parser.add_argument('-dp', '--data_port', dest='data_port', type=int, default=1511, help="NatNet server data socket UDP port") +parser.add_argument('-cp', '--command_port', dest='command_port', type=int, default=1510, help="NatNet server command socket UDP port") +parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help="display debug messages") +parser.add_argument('-f', '--freq', dest='freq', default=10, type=int, help="transmit frequency") +parser.add_argument('-vs', '--vel_samples', dest='vel_samples', default=4, type=int, help="amount of samples to compute velocity (should be greater than 2)") +args = parser.parse_args() + +if args.ac is None: + print("At least one pair of rigid boby / AC id must be declared") + exit() + +# dictionary of ID associations +id_dict = dict(args.ac) + +# initial time per AC +timestamp = dict([(ac_id, time()) for ac_id in id_dict.keys()]) +period = 1. / args.freq + +# initial track per AC +track = dict([(ac_id, deque()) for ac_id in id_dict.keys()]) + +# start ivy interface +if args.ivy_bus is not None: + ivy = IvyMessagesInterface("natnet2ivy", ivy_bus=args.ivy_bus) +else: + ivy = IvyMessagesInterface("natnet2ivy") + +# store track function +def store_track(ac_id, pos, t): + if ac_id in id_dict.keys(): + track[ac_id].append((pos, t)) + if len(track[ac_id]) > args.vel_samples: + track[ac_id].popleft() + +# compute velocity from track +# returns zero if not enough samples +def compute_velocity(ac_id): + vel = [ 0., 0., 0. ] + if len(track[ac_id]) >= args.vel_samples: + nb = -1 + for (p2, t2) in track[ac_id]: + nb = nb + 1 + if nb == 0: + p1 = p2 + t1 = t2 + else: + dt = t2 - t1 + vel[0] = (p2[0] - p1[0]) / dt + vel[1] = (p2[1] - p1[1]) / dt + vel[2] = (p2[2] - p1[2]) / dt + p1 = p2 + t1 = t2 + if nb > 0: + vel[0] / nb + vel[1] / nb + vel[2] / nb + return vel + + +# This is a callback function that gets connected to the NatNet client. It is called once per rigid body per frame +def receiveRigidBodyFrame( ac_id, pos, quat ): + t = time() + i = str(ac_id) + if i not in id_dict.keys(): + return + + store_track(i, pos, t) + if abs(t - timestamp[i]) < period: + return # too early for next message + timestamp[i] = t + + msg = PprzMessage("datalink", "REMOTE_GPS_LOCAL") + msg['ac_id'] = id_dict[i] + msg['pad'] = 0 + msg['enu_x'] = pos[0] + msg['enu_y'] = pos[1] + msg['enu_z'] = pos[2] + vel = compute_velocity(i) + msg['enu_xd'] = vel[0] + msg['enu_yd'] = vel[1] + msg['enu_zd'] = vel[2] + msg['tow'] = int(timestamp[i]) # TODO convert to GPS itow ? + # convert quaternion to psi euler angle + dcm_0_0 = 1.0 - 2.0 * (quat[1] * quat[1] + quat[2] * quat[2]) + dcm_1_0 = 2.0 * (quat[0] * quat[1] - quat[3] * quat[2]) + msg['course'] = 180. * np.arctan2(dcm_1_0, dcm_0_0) / 3.14 + ivy.send(msg) + + +# start natnet interface +natnet = NatNetClient( + server=args.server, + rigidBodyListener=receiveRigidBodyFrame, + dataPort=args.data_port, + commandPort=args.command_port, + verbose=args.verbose) + + +print("Starting Natnet3.x to Ivy interface at %s" % (args.server)) +try: + # Start up the streaming client. + # This will run perpetually, and operate on a separate thread. + natnet.run() + while True: + sleep(1) +except (KeyboardInterrupt, SystemExit): + print("Shutting down ivy and natnet interfaces...") + natnet.stop() + ivy.shutdown() +