scripts/positioning_plus.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 """
3 Use this node to perform indoor positioning using the metraTec IPS+ tracking system. Prerequisites for using this node
4 is a running receiver-node that handles communication with the receiver and thus with the beacons in the vicinity.
5 Also, make sure that tf is broadcasting transformations between all the different coordinate frames used in the config
6 file and the coordinate frame of the receiver (which is specified via rosparam, see below).
7 
8 Subscribed topics:
9  - ips/receiver/raw (indoor_positioning/StringStamped):
10  Raw messages received by the UWB receiver
11 
12 Published topics:
13  - ips/receiver/send (std_msgs/String):
14  Message to be sent to the receiver, e.g. 'SRG <EID> \r' ranging request
15  - ips/receiver/position (geometry_msgs/PointStamped):
16  Estimated position of the receiver after UWB ranging and trilateration
17 
18 Parameters:
19  - ~config_file (string, default='PKG_DIR/config/zones.yml'):
20  Path to the configuration file of zones and beacons relative to the package directory
21  - ~frame_id (string, default='map'):
22  Coordinate frame the receiver position should be estimated in
23  - ~rate (double, default=0.1):
24  The publishing rate in messages per second
25  - ~bcn_len (int, default=2*number_of_beacons):
26  Buffer length for BCN messages
27  - ~srg_len (int, default=number_of_beacons):
28  Buffer length for SRG messages
29  - ~min_beacons (int, default=3):
30  Minimum number of beacons to be used for UWB ranging. Should be 3 (two possible points) or 4
31  - ~max_beacons (int, default=6):
32  Maximum number of beacons to be used for UWB ranging. Higher values lead to higher computation time.
33  - ~rssi_thresh (int, default=-127):
34  Minimum RSSI value a beacon should have to be used for ranging. Default uses all beacons.
35  - ~max_z (double, default=None):
36  Maximum z-coordinate the receiver should have after ranging. Used as bounds for trilateration.
37  - ~dilation (double, default=0.0):
38  Only position estimates within a dilated convex hull around the beacons used for ranging are published to
39  minimize errors and improve accuracy. This is the dilation distance in m.
40 """
41 
42 import os
43 import rospy
44 import rospkg
45 import tf
46 from std_msgs.msg import String
47 from geometry_msgs.msg import PointStamped
48 from indoor_positioning.msg import StringStamped
49 from indoor_positioning.positioning_plus import PositioningPlus
50 
51 
52 class IPSplus:
53  """Configure ROS node for metraTec IPS+ indoor positioning system with UWB ranging functionality."""
54  # parameters specifying wait for SRG responses and timeout to avoid infinite wait for SRG response
55  srg_sleep = 0.1 # interval in which to check for responses in seconds
56  srg_timeout = 20 # number of times to check for responses. After that, continue with next beacon
57 
58  def __init__(self):
59  """
60  Initialize instance variables with values from ROS parameter server (or default values) and zone/beacon
61  configuration from YAML file.
62  """
63  # get directory of config file
64  config_dir = rospy.get_param('~config_file') if rospy.has_param('~config_file') else 'config/zones.yml'
65  abs_dir = os.path.join(rospkg.RosPack().get_path('indoor_positioning'), config_dir)
66  # get minimum number of beacons to use for ranging
67  min_beacons = rospy.get_param('~min_beacons') if rospy.has_param('~min_beacons') else 4
68  # get maximum bumber of beacons to use for ranging
69  self.max_beacons = rospy.get_param('~max_beacons') if rospy.has_param('~max_beacons') else 6
70  # get minimum RSSI value a beacon should have for ranging
71  self.rssi_thresh = rospy.get_param('~rssi_thresh') if rospy.has_param('~rssi_thresh') else -127
72  # get maximum z position the receiver can have
73  max_z = rospy.get_param('~max_z') if rospy.has_param('~max_z') else None
74  # Expansion (in m) for polygon connecting the beacons that are used for ranging
75  dilation = rospy.get_param('~dilation') if rospy.has_param('~dilation') else 0.0
76  # initialize positioning class
77  self.positioning = PositioningPlus(abs_dir, min_beacons=min_beacons, max_z=max_z, dilation=dilation)
78  # get number of beacons specified in zones.yml file for default buffer values
79  n_beacons = self.positioning.n_beacons
80 
81  # publisher for estimated position of UWB trilateration
82  self.position_pub = rospy.Publisher('ips/receiver/position', PointStamped, queue_size=1)
83  # publisher for serial messages to be sent to the receiver
84  self.receiver_send_pub = rospy.Publisher('ips/receiver/send', String, queue_size=n_beacons)
85  # subscribe to raw messages of receiver
86  self.receiver_sub = rospy.Subscriber('ips/receiver/raw', StringStamped, self.callback)
87 
88  # number of messages to keep
89  self.bcn_buffer_length = rospy.get_param('~bcn_len') if rospy.has_param('~bcn_len') else 2*n_beacons
90  self.bcn_buffer_length = 2*n_beacons if self.bcn_buffer_length == -1 else self.bcn_buffer_length
91  # list of incoming messages
92  self.bcn_buffer = []
93  # timestamp from last received message
94  self.bcn_last_time = None
95 
96  # number of messages to keep
97  self.srg_buffer_length = rospy.get_param('~srg_len') if rospy.has_param('srg_len') else n_beacons
98  self.srg_buffer_length = n_beacons if self.srg_buffer_length == -1 else self.srg_buffer_length
99  # list of incoming messages
100  self.srg_buffer = []
101  # timestamp from last received message
102  self.srg_last_time = None
103  # flag that is set when a SRG message is received
104  self.srg_wait = True
105 
106  # initialize tf transform listener
107  self.tf = tf.TransformListener()
108  # get map frame to do positioning in from parameter server. Defaults to "map" if not specified
109  self.frame_id = rospy.get_param('~frame_id') if rospy.has_param('~frame_id') else 'map'
110 
111  # set publishing rate
112  self.rate = rospy.Rate(rospy.get_param('~rate')) if rospy.has_param('~rate') else rospy.Rate(0.1)
113 
114  def callback(self, msg):
115  """
116  Append incoming messages to list of previous message. Differentiate between BCN messages (regular beacon pings)
117  and SRG messages (responses of UWB ranging responses)
118  :param msg: String, message of subscribed topic
119  """
120  # sort message and add to buffer depending on prefix of the message
121  if msg.data.split(' ')[0] == 'BCN':
122  buff_len = self.bcn_buffer_length
123  buff = self.bcn_buffer
124  self.bcn_last_time = msg.header.stamp
125  elif msg.data.split(' ')[0] == 'SRG':
126  buff_len = self.srg_buffer_length
127  buff = self.srg_buffer
128  self.srg_last_time = msg.header.stamp
129  self.srg_wait = False
130  else:
131  return
132  # append message to buffer
133  buff.append(msg.data)
134  # delete oldest message if buffer is full
135  if len(buff) > buff_len:
136  del(buff[0])
137 
138  def publish(self):
139  """Publish the estimated position of the receiver"""
140  while not rospy.is_shutdown():
141  # estimate current position
142  position = self.estimate_position()
143  # publish estimated position
144  if position is not None and len(position) == 3:
145  point = PointStamped()
146  point.header.stamp = rospy.Time.now()
147  point.header.frame_id = self.frame_id
148  point.point.x, point.point.y, point.point.z = position[0], position[1], position[2]
149  self.position_pub.publish(point)
150  self.rate.sleep()
151 
152  def estimate_position(self):
153  """
154  Estimate the position of the receiver using UWB ranging responses.
155  :return: [Float, Float, Float]: estimated position of the UWB receiver [x, y, z]
156  """
157  while not rospy.is_shutdown():
158  # get all beacons in range (in an ordered list, according to RSSI value)
159  beacons = self.positioning.in_range(self.bcn_buffer, threshold=self.rssi_thresh)
160  print('Beacons found: {}'.format(beacons))
161  # send ranging request to all beacons
162  iters = 0
163  for b in beacons:
164  request = 'SRG ' + b + '\r'
165  self.srg_wait = True
166  self.receiver_send_pub.publish(request)
167  # wait until SRG response arrives
168  while self.srg_wait:
169  # stop waiting after a specific amount of iterations to avoid infinite loop
170  if iters >= self.srg_timeout:
171  print('Abort ranging with {} due to timeout.'.format(b))
172  break
173  # wait for SRG response
174  rospy.sleep(self.srg_sleep)
175  # increment number of iterations
176  iters += 1
177  # reset number of iterations for next beacon
178  iters = 0
179  # stop if maximum number of beacons is reached
180  if len(self.srg_buffer) >= self.max_beacons:
181  break
182  print('Using the following responses for ranging: {}'.format(self.srg_buffer))
183  # get ranges for all SRG responses
184  ranges = self.positioning.parse_srg(self.srg_buffer)
185  # transform beacon positions into frame specified by self.frame_id (~frame_id)
186  transformed_ranges = []
187  for i, r in enumerate(ranges):
188  # skip points that are already in the correct frame
189  if r[0].frame_id != self.frame_id:
190  # transform points into correct frame if tf knows the respective transform
191  try:
192  # look for transform from beacon frame to receiver frame
193  (trans, rot) = self.tf.lookupTransform('/' + self.frame_id, '/' + r[0].frame_id, rospy.Time(0))
194  # transform point
195  tp = r[0].position[:]
196  tp.append(0)
197  tp = tf.transformations.quaternion_multiply(
198  tf.transformations.quaternion_multiply(rot, tp),
199  tf.transformations.quaternion_conjugate(rot)
200  )
201  tp = [tp[0]+trans[0], tp[1]+trans[1], tp[2]+trans[2]]
202  print(tp)
203  transformed_ranges.append((tp, r[1]))
204  except (tf.LookupException, tf.ConnectivityException, tf.ExtrapolationException):
205  print('Cannot transform {} into {}. Check your tf tree!'.format(r[0].frame_id, self.frame_id))
206  continue
207  else:
208  transformed_ranges.append((r[0].position, r[1]))
209  # estimate position
210  ret, position = self.positioning.trilaterate(transformed_ranges)
211  # clear SRG message buffer
212  self.srg_buffer = []
213  # return estimated position
214  print('Trilateration in Polygon: ' + str(ret))
215  return position if ret else None
216 
217 
218 if __name__ == '__main__':
219  # start node
220  rospy.init_node('positioning_plus', anonymous=False)
221  # initialize IPSReceiver class
222  ips = IPSplus()
223  try:
224  # publish receiver messages
225  ips.publish()
226  except rospy.ROSInterruptException:
227  pass


indoor_positioning
Author(s): Christian Arndt
autogenerated on Mon Jun 10 2019 13:33:13