Finding a friend with only distance and motion.
You walk into a cafe, looking for your friend. Seems like an easy task, until you see it’s so packed that you can’t see through the crowd at all, and everyone’s talking so loud that you can barely hear anything. The only things you know are your movements, and how far you are from your friend (through the special psychic bond you two share). How will you find each other?
I wanted to solve the exact same problem, but with devices instead of people (so no psychic connection for me), existing in a space of hundreds of other devices. Working the problem taught me a lot of really interesting science relating to robotics and state estimation, and I wrote this post so you can learn, too.
I have two microcontrollers (the large blue boards). Each has an inertial measurement unit (IMU, small board on top right), which gives me data including acceleration and compass heading. They also have an ultra-wideband unit (UWB, small board in slot on left), which gives the distance to the other unit in the pair.
Each of these packages will be contained within a wearable device, and can therefore prompt its wearer to move around. This is an important freedom, because it means we can use the change in distance over time to determine location during the localization process, rather than purely statically.
To get us in the mindset of doing a technical implementation, let’s understand some options we have and see why they are or aren’t a good fit for what we’re trying to do.
Ultra-wideband localization, found in location-aware products including AirTags, uses a simple call-and-response system known as time of flight. The UWB measures the time it takes for a roundtrip exchange of information, which is then turned into a distance .
The initiator sends out a timestamped pulse to the responder, which then responds in time (reported by the responder) with its own timestamp packet. The whole exchange takes time, calculated at the initiator:
The time of flight system works fine for getting the distance to the other device, but alone is insufficient for getting the bearing to the other device. This limitation is where phase difference of arrival (PDoA) ultra-wideband comes in.
PDoA uses a property of radio waves called the phase. Light and radio waves oscillate up and down as they travel; where they are in that cycle is the phase. As the wave passes any fixed point in space, its phase at that point cycles:
We use this property to determine the heading relative to the other device. By positioning two antennas a known distance apart, we can derive the angle of arrival of the other device’s messages by comparing the phase at the two antennas at each timestep:
Drag the point to move the wave source.
Failure mode: PDoA is a great system, but it has a caveat: it requires two antennas. This design restriction makes it very difficult to integrate into a wearable, where you have to deal with skin substantially attenuating signals. One antenna (what I chose for Beacons) can maybe get through with clever placement, but two is much more difficult.
Another potential way to do things is to involve external devices with known fixed positions, usually known as anchors. With at least three of them, a Beacon can measure its distance to each and solve for its location. This strategy is the same as how GPS works, using multiple circles (or spheres in 3D) of distance to derive an exact location for the device:
Drag the beacon.
Failure mode: Trilateration is easy, once you have those three anchors. In practice, this means permanently installing devices to always broadcast their locations within a space. The localization needs to work without specially-installed equipment, so I don’t see trilateration as a valid solution. However, if I were to deploy these in a known location that has a lot of traffic (like a conference venue), this strategy could act as a refining system to keep the main system grounded.
Kalman filters are the gold standard for state-space estimation in physical spaces, and are exactly what we need to solve our problem. They operate on the principle of having an internal estimation of the unobservable true state — in this case, the relative position — which is updated with new information we get from measurements in a looping process. This loop eventually converges on a reasonable estimate that we can use to guide the user. Our true state is unobservable because we can’t directly measure bearing from the sensors alone; the bearing comes from the combined information from all of our sensors.
I specifically chose an extended Kalman filter (EKF), which gives support for the nonlinear functions needed to calculate the expected bearing. The reason an EKF is our best solution is that it only requires one UWB antenna (unlike PDoA) and only two devices (unlike trilateration).
An EKF is a loop composed of two parts: a prediction and a measurement step. You have a state vector and covariance matrix you want to keep as consistent as possible with the real unobservable state (for example, the and distance to a target). We use the “hat” to say that this state vector is an estimate of the true state. doesn’t get a hat since it isn’t an estimate of anything, it just helps inform the estimate . To update and , you will ask two questions:
The state represents what the system is keeping track of, but it’s also very important to know the relations of each of the state values to each other. This is where the covariance matrix comes in. It’s a square matrix with row/column counts equal to the size of , denoted . Each entry in that matrix answers the question “how do and impact each other’s distributions?” For entries along the diagonal, this reduces to the variance of . We use the entries in to shape our belief of the distributions of the variables in during the prediction and measurement steps.
Linearization is the process of taking a nonlinear function (like a parabola) and “zooming in” really close, so close that the curved line actually looks straight, or in other words, linear. It’s an approximation so it’s not guaranteed to be optimal, but it’s good enough for our use case (and others, including GPS).
EKFs have an additional layer of complexity in return for supporting nonlinear functions like square root, which you will see are necessary for our task: they require differentiability of and to linearize around using the Jacobians of and , and respectively. Doing so lets us reuse the regular Kalman update equations, making calculations much easier. The notation expands to “we condition our guess of on the information we have at timestep ”.
We now have all of the pieces to describe how data flows through the EKF loop. We start with the current state and covariance matrix , along with our control input for the timestep, .
Our first step is to predict what the next state and covariance will be.
Notice : this is the linearization I was talking about earlier. The term is the prediction noise covariance for timestep . The noisier the prediction is, the less the filter trusts it. Next, we update and with our measurements:
is our measurement for the timestep. is the Kalman gain, which weights the measurement against the prediction by their relative variances. A higher gain means we trust the measurement more; lower means we trust our prediction more. The linearization comes back again, this time for the measurement step. plays the same role as but for measurement noise covariance.
And that is a single step of an EKF! Not too bad once you understand how everything fits together.
Now that we have an understanding of the underlying logic of an EKF, it’s time to define our variables.
Arguably the most important part of designing a filter is what state to keep track of. There are a million things going on in any nonlinear system, especially in the case of Beacons, so being selective with what gets stored saves both processing time and later headaches when things inevitably don’t work first try. For Beacons, we will store four values:
These four values (packed as ) allow us to approximate the physical system of two devices moving in a 2D plane. We initialize with the UWB distance , assuming the initial relative position lies entirely along :
It’s important to note that it’s impossible to tell the direction to the other device at this point, since at we only have one distance measurement. The filter will accumulate a belief of the direction as the user moves the device around. We initialize the covariance matrix using tuned values:
The (20 cm standard deviation) informs the EKF that the radial covariance is small and constrained by the UWB’s relatively low error, with the starting assumption that we are directly on the axis. The variance informs the EKF that the larger the distance, the more unsure it should be about the axis (since we assume we are only on the axis at ). We initialize the variances for and to as a tuned starting value. The rest of is . The filter will start changing all of its values as it learns more about the system.
Now that we know what we’ll be storing, we need to think about how we project that state prediction forward.
We have our state vector layout, now we need to project it forward each timestep using information from the control vector. Luckily this is pretty easy, using physics equations you likely learned in high school:
These equations give our predicted state update .
Note: The world-frame acceleration values and are calculated using an AHRS filter combining data from the IMU. I’ve omitted the AHRS step for brevity.
Next, we update . To do so we need our prediction Jacobian, , calculated by taking the partial derivatives of the above equations:
Diagonal entries of mean each value carries forward. The two off-diagonal s integrate via and via over a single step. We assume constant acceleration between steps.
We then follow the prediction equations defined above to get and . is a tuned matrix of constants.
Now that we have our estimates and , we need to check our work. Our measurement function is pretty simple:
All this does is take our deltas and convert them to a distance comparable to the UWB reading. Taking the partial derivative:
The pair is the unit vector from this device to the other. Movement along that direction changes the distance one-for-one; movement perpendicular to it doesn’t change the distance at all. Setting to our UWB reading, we update with the above equations. Similarly, is a tuned scalar constant. isn’t a matrix because the measurement function returns a scalar.
Whenever we want to get what the filter thinks is our current bearing, we run a simple function to extract the values we need, the bearing and the uncertainty , both in radians:
We set (no knowledge of bearing) if the distance is so small that it’s basically impossible to tell, i.e. the filter thinks is less than a millimeter. Otherwise, we take , with being the variance.
Now we have the entire pipeline! Let’s see what it can do:
Drag the Beacons (multitouch supported), or click the canvas and use WASD for 1 / IJKL for 2 / R to reset.
The colored region is the filter’s belief about where the other Beacon is.
There are tons of applications for EKFs, ranging from robotics to digital signal processing to navigation systems, and it all becomes so much more interesting when you can see how they work through all of the complicated matrix math. They are the perfect solution for Beacons, and I hope you someday experience the joy of using one, too.
I used this explanation of regular and extended Kalman filters extensively when writing this post; I highly recommend reading it yourself if you want to dive deeper!
If you want to read up on a more recent innovation, look into unscented Kalman filters! They turned out to be a bit overkill for my use case, but I would love to use them in the future.