src/ros_ips/positioning_plus.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 """
3 Use this class to perform indoor positioning with the metraTec IPS+ system. The position of a receiver is evaluated by
4 UWB ranging of RF beacons placed in the environment. Requires a YAML file with the configuration of beacon positions.
5 
6 Usage:
7 Initialize the class by passing the directory of the config file. Then collect a number of beacon responses to ranging
8 requests (SRG) and use the parse_srg() function to get a list of beacon objects and their respective ranges from a list
9 of SRG responses.
10 Use this list as input for the trilaterate() function to estimate the position of the receiver in 3D space.
11 
12 A few examples can be found in the main function below.
13 """
14 
15 import math
16 from scipy.optimize import minimize
17 from positioning import Positioning
18 
19 
20 class PositioningPlus(Positioning):
21  """Class to extend the functionality of the Positioning class with UWB ranging capabilities."""
22 
23  # options for minimization function used for trilateration
24  minimization_params = dict(method='TNC', # L-BFGS-B, TNC or SLSQP
25  options={'disp': True})
26 
27  def __init__(self, config_dir, min_beacons=4, max_z=None):
28  """
29  Initialize the class with zone and beacon definitions from YAML configuration file.
30  :param config_dir: String: Directory of the YAML config file
31  :param min_beacons: Int: Minimum number of beacons to use for trilateration
32  :param max_z: Float: Maximum z position the receiver can have after ranging
33  """
34  # initialize the positioning class that this class inherits
35  Positioning.__init__(self, config_dir)
36 
37  # minimum number of beacons (and ranges) to use for position estimation
38  self.min_beacons = min_beacons
39 
40  # store EIDs of configured beacons for easy access
41  self.eids = self.get_defined_beacons()
42 
43  # maximum z value the position estimate can have (used as initialization and upper bound for trilateration)
44  self.max_z = max_z
45 
46  def get_top_beacons(self, pings, n):
47  """
48  Get the top n beacons in terms of RSSI values from a list of beacon pings.
49  :param pings: [String]: list of beacon pings
50  :param n: Int: Number of top beacons to return
51  :return: [Beacon]: list of beacon objects ordered according to RSSI values
52  """
53  means = self.get_mean(pings)
54  ret = []
55  # sort mean dictionary by values and return n beacons with the highest signal strength that are configured in
56  # the config-file and meet the threshold criteria
57  for key, value in sorted(means.items(), key=lambda (k, v): v, reverse=True):
58  # look for key (EID) in every zone and return when match is found
59  for z in self.zones: # iterate over zones
60  # continue to next zone if mean RSSI value is lower than the configured threshold
61  if value < z.threshold:
62  continue
63  for b in z.beacons: # iterate over beacons inside the zone
64  if b.eid == key: # if the EID of the beacon is the same as the current key (EID) return the zone
65  ret.append(b)
66  if len(ret) >= n: # TODO continue with next key-value-pair when was found
67  return ret
68  return ret
69 
70  def get_range(self, responses):
71  # get range from a single SRG response if message is valid
72  pass
73 
74  def parse_srg(self, responses):
75  """
76  Parse a list of SRG ranging responses of UWB beacons. For each message (if it is valid) return the positions,
77  frame id and estimated ranges.
78  :param responses: [String]: list of SRG responses from UWB beacons
79  :return: [(Beacon, Float)]: list beacon-object and range pairs
80  """
81  ranges = []
82  # iterate through each response and fill output lists if ranging messages are valid
83  for r in responses:
84  # split single message on spaces
85  split = r.split(' ')
86  # skip this message if it is invalid
87  if len(split) != 6:
88  print('Encountered invalid SRG response: {}'.format(r))
89  continue
90  # get beacon object from EID
91  beacon = self.get_beacon(split[1])
92  # calculate range
93  range = float(split[2]) / 1000.
94  # append to output
95  ranges.append((beacon, range))
96  return ranges
97 
98  def in_range(self, pings):
99  """
100  Get a list of beacons that are in range and are configured in the configuration YAML file from a list of
101  beacon pings recorded over a short duration. The returned beacons (EIDs can be used for UWB ranging)
102  :param pings: [String]: List of BCN pings
103  :return: [String]: List of EIDs that can be used for ranging
104  """
105  eids = []
106  # get unique list of beacon EIDs from list of pings and their average RSSI value
107  mean = self.get_mean(pings)
108  for m in mean:
109  # check if beacon is configured and can be used for ranging
110  if m in self.eids:
111  eids.append(m)
112  return eids
113 
114  def trilaterate(self, ranges):
115  """
116  Estimate the position of the receiver by using trilateration with at least three UWB responses
117  :param ranges: [([Float, Float, Float], Float)]: list of position and range pairs (obtained from parse_srg) or
118  [(Beacon, Float)]: list of beacon object and range pairs (output from parse_srg())
119  :return: [Float, Float, Float]: 3D coordinates of estimated receiver location
120  """
121  # check whether enough points are given
122  if len(ranges) < self.min_beacons:
123  print('Not enough beacons for position estimation ({}/{})'.format(len(ranges), self.min_beacons))
124  return None
125  print('Using {} beacons for trilateration.'.format(len(ranges)))
126  # get points and distances from input and compute sum of distances
127  points, distances, dsum = [], [], 0
128  for r in ranges:
129  # get position of beacon directly from input list or alternatively from Beacon object
130  p = r[0].position if r[0].__class__.__name__ == 'Beacon' else r[0]
131  points.append(p)
132  distances.append(r[1])
133  dsum += r[1]
134  # get weighted centroid
135  initial = [0, 0, 0]
136  for p, d in zip(points, distances):
137  weight = 1. - d / dsum
138  initial = [initial[0] + p[0] * weight, initial[1] + p[1] * weight, initial[2] + p[2] * weight]
139  initial = [initial[0] / len(ranges), initial[1] / len(ranges), initial[2] / len(ranges)]
140  if self.max_z is not None and initial[2] > self.max_z:
141  initial[2] = self.max_z
142  # minimize root mean square error to get estimated position
143  print('guess: ', initial)
144  # impose upper bound on z coordinate
145  bnds = ((None, None), (None, None), (None, self.max_z)) if self.max_z is not None else None
146  res = minimize(self.mse, initial, args=(points, distances), bounds=bnds, **self.minimization_params)
147  print('final: ', res.x)
148  return res.x
149 
150  @staticmethod
151  def mse(x, points, distances):
152  """
153  Cost-function to use for position estimation. Minimize the squared error of the distance between a variable
154  point x and each beacon and the measured UWB distance. mse = SUM(distance - dist(x, points)^2
155  :param x: [Float, Float, Float]: vector with variable components to be optimized
156  :param points: [[x, y, z], ...]: list of positions of the beacons used for UWB ranging
157  :param distances: [Float]: estimated distances from UWB ranging of above beacons
158  :return: Float: mean squared error for all beacon positions
159  """
160  mse = 0.
161  for p, d in zip(points, distances):
162  d_calc = math.sqrt((x[0]-p[0])**2 + (x[1]-p[1])**2 + (x[2]-p[2])**2)
163  mse += (d_calc-d)**2
164  return mse # / len(points)
165 
166 
167 if __name__ == '__main__':
168  """Testing the class and its methods."""
169  # initialize class
170  pos = PositioningPlus('/home/metratec/catkin_ws/src/ros_ips/config/zones.yml')
171  # create a list of pings that would usually be collected from the receiver and stored in a buffer
172  dummy_pings = ['BCN 00124B00090593E6 -060', 'BCN 00124B00090593E6 -070', 'BCN 00124B00090593E6 -070',
173  'BCN 00124B00050CD41E -090', 'BCN 00124B00050CD41E -070', 'BCN 00124B00050CD41E -050',
174  'BCN 00124B00050CDC0A -070', 'BCN 00124B00050CDC0A -060', 'BCN 00124B00050CDC0A -090']
175  # get the beacon object with a specified EID
176  # Note: the above beacons have to be defined in the config file for this to work
177  beacon = pos.get_beacon('00124B00090593E6')
178  # get the current zone the receiver is in based on the passed list of pings
179  zone = pos.get_zone(dummy_pings)
180 
181  # create a list of UWB ranging responses that would usually be collected from a receiver and stored in a buffer
182  resp = ['SRG 00124B00050CDA71 01650 -076 -081 047\r', 'SRG 00124B00090593E6 04300 -076 -080 105\r',
183  'SRG 00124B00050CD41E 00800 -079 -082 049\r']
184  # get beacons in range for ranging
185  in_range = pos.in_range(dummy_pings)
186  # get beacon objects and respective ranges from a list of UWB ranging responses
187  ranges = pos.parse_srg(in_range)
188  # use trilateration (with at least 3 points) to estimate the receiver location from beacon positions and ranges
189  tril = pos.trilaterate(ranges)
190  # break here to investigate variables etc.
191  breakpoint = 0
def __init__(self, config_dir, min_beacons=4, max_z=None)


ros_ips
Author(s): Christian Arndt
autogenerated on Sat May 12 2018 02:24:12