diff --git a/conf/tools/display_objects.xml b/conf/tools/display_objects.xml
new file mode 100644
index 0000000000..519ffee263
--- /dev/null
+++ b/conf/tools/display_objects.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/sw/ground_segment/python/natnet3.x/NatNetClient.py b/sw/ground_segment/python/natnet3.x/NatNetClient.py
index 9e7ceedabd..0acfaebc1f 100644
--- a/sw/ground_segment/python/natnet3.x/NatNetClient.py
+++ b/sw/ground_segment/python/natnet3.x/NatNetClient.py
@@ -13,7 +13,7 @@ FloatValue = struct.Struct( '
+#
+# 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
+# .
+#
+
+'''
+Receive marker set from optitrack system and display a convex hull for
+each selected objects in the GCS with the SHAPE message
+
+Exemple to display "Building_XYZ" object at Enac:
+ ./display_objects.py -r 43.5640917,1.4829201 -up z_up -n 'Building_.*'
+
+'''
+
+import sys
+from os import path, getenv
+from time import time, sleep
+import numpy as np
+import re
+from scipy.spatial import ConvexHull
+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")
+from pprzlink.ivy import IvyMessagesInterface
+from pprzlink.message import PprzMessage
+
+# parse args
+parser = argparse.ArgumentParser()
+parser.add_argument('-n', '--name', dest='name', required=True, help="Regular expression of the names of the object to display")
+parser.add_argument('-r', '--ref', dest='ref', required=True, help="Reference position in lat,long format in degrees")
+parser.add_argument('-c', '--color', dest='color', default='white', help="Display color")
+parser.add_argument('-sn', '--show_name', dest='show_name', action='store_true', help="Display the name of the object")
+parser.add_argument('-t', '--timeout', dest='timeout', default=2., type=float, help="Timeout for removing object from scene")
+parser.add_argument('-th', '--threshold', dest='threshold', default=0.1, type=float, help="Threshold distance to consider a moving object in meter")
+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('-vv', '--very_verbose', dest='very_verbose', action='store_true', help="display debug messages and optitrack debug messages")
+parser.add_argument('-f', '--freq', dest='freq', default=1., type=float, help="transmit frequency")
+parser.add_argument('-rp', '--refresh_period', dest='refresh_period', default=4., type=float, help="refresh period at which message is sent event if the object is not moving")
+parser.add_argument('-up', '--up_axis', dest='up_axis', choices=['y_up', 'z_up'], default='y_up', help="Optitrack Up axis: y_up or z_up.")
+parser.add_argument('-o', '--old_natnet', dest='old_natnet', action='store_true', help="Change the NatNet version to 2.9")
+
+args = parser.parse_args()
+
+# message period
+period = 1./args.freq
+# mean earth radius from IUGG
+R_earth = 6371008.7714
+# reference position lat0, long0
+lat0, long0 = args.ref.split(",")
+lat0 = float(lat0)
+R_cos_lat0 = R_earth*np.cos(np.deg2rad(lat0))
+long0 = float(long0)
+# dictionary of id and received time by set name
+markerset = {}
+current_index = 1
+# index of horizontal X and Y coordinates
+X_AXIS = 0
+if args.up_axis == 'y_up':
+ Y_AXIS = 2
+ Y_SIGN = -1
+else:
+ Y_AXIS = 1
+ Y_SIGN = 1
+
+# start ivy interface
+if args.ivy_bus is not None:
+ ivy = IvyMessagesInterface("display_objects", ivy_bus=args.ivy_bus)
+else:
+ ivy = IvyMessagesInterface("display_objects")
+
+def is_moving(old_pos, new_pos):
+ '''
+ check if the markerset position have changed (at least one of the marker)
+ '''
+ try:
+ for po, pn in zip(old_pos, new_pos):
+ if abs(po[0]-pn[0]) > args.threshold or abs(po[1]-pn[1]) > args.threshold or abs(po[2] - pn[2]) > args.threshold:
+ return True # at least one marker has moved
+ return False # object has not moved
+ except:
+ # if failing, update position
+ return True
+
+def receiveMarkerSet(name, posList):
+ '''
+ callback for markerset with name and marker position as input
+ '''
+ global current_index
+
+ # check if name is matching regexp
+ name = name.decode('utf-8')
+ if re.fullmatch(args.name, name) is not None:
+
+ # check if message should be sent (first time or period)
+ send = False
+ now = time()
+ if name in markerset:
+ dt = now - markerset[name]['time']
+ dt_refresh = now - markerset[name]['time_refresh']
+ if dt >= period:
+ # period elapsed, check if moved
+ markerset[name]['time'] = now
+ moved = is_moving(markerset[name]['pos'], posList)
+ if moved:
+ send = True
+ markerset[name]['pos'] = posList
+ if dt_refresh >= args.refresh_period:
+ # refresh period elapsed, send anyway
+ send = True
+ else:
+ send = True
+ markerset[name] = {'time_refresh': now, 'time': now, 'id': current_index, 'pos': posList }
+ current_index += 1
+
+ if args.very_verbose:
+ print(name, posList, time)
+
+ if send:
+ if args.verbose and (not args.very_verbose):
+ print(name, posList, now)
+
+ # build list of 2D points and compute convex hull
+ points = [(pos[X_AXIS], Y_SIGN*pos[Y_AXIS]) for pos in posList]
+ hull = ConvexHull(points)
+ # build lists of polygon corners to display in lat long
+ latitudes = [ int(1e7 * (lat0 + np.rad2deg(points[i][1] / R_earth))) for i in hull.vertices ]
+ longitudes = [ int(1e7 * (long0 + np.rad2deg(points[i][0] / R_cos_lat0))) for i in hull.vertices ]
+
+ # send SHAPE message
+ shape = PprzMessage("ground", "SHAPE")
+ shape['id'] = markerset[name]['id']
+ shape['linecolor'] = '"{}"'.format(args.color)
+ shape['fillcolor'] = '"{}"'.format(args.color)
+ shape['opacity'] = 1 # light
+ shape['shape'] = 1 # polygon
+ shape['status'] = 0 # create or update
+ shape['latarr'] = latitudes
+ shape['lonarr'] = longitudes
+ shape['radius'] = 0. # not relevant
+ if args.show_name:
+ shape['text'] = name
+ else:
+ shape['text'] = '" "'
+ ivy.send(shape)
+ markerset[name]['time_refresh'] = now
+ sleep(0.01)
+
+def check_timeout():
+ '''
+ check timeout and if needed, remove object from scene and list
+ '''
+ now = time()
+ for name in list(markerset.keys()):
+ if now - markerset[name]['time'] > args.timeout:
+ shape = PprzMessage("ground", "SHAPE")
+ shape['id'] = markerset[name]['id']
+ shape['status'] = 1 # delete
+ # fill the rest even if not needed
+ shape['linecolor'] = '" "'
+ shape['fillcolor'] = '" "'
+ shape['opacity'] = 0
+ shape['shape'] = 1
+ shape['latarr'] = [0]
+ shape['lonarr'] = [0]
+ shape['radius'] = 0.
+ shape['text'] = '" "'
+ ivy.send(shape)
+ del markerset[name]
+
+
+# start natnet interface
+natnet_version = (3,0,0,0)
+if args.old_natnet:
+ natnet_version = (2,9,0,0)
+natnet = NatNetClient(
+ server=args.server,
+ markerSetListener=receiveMarkerSet,
+ dataPort=args.data_port,
+ commandPort=args.command_port,
+ verbose=args.very_verbose,
+ version=natnet_version)
+
+
+print("Starting Object Display 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)
+ check_timeout()
+except (KeyboardInterrupt, SystemExit):
+ print("Shutting down ivy and natnet interfaces...")
+ natnet.stop()
+ ivy.shutdown()
+except OSError:
+ print("Natnet connection error")
+ natnet.stop()
+ ivy.stop()
+ exit(-1)
+