Building a local positioning system to track runners using Ultra-Wideband
Pangram verdict · v3.3
We believe that this entire text is human-written.
AI likelihood · overall
HumanArticle text · 1,679 words · 1 segments analyzed
door Robin Paret Geschreven op 2026-07-07 Leestijd: 17 minuten Klik hier om in het nederlands te lezen English 12Urenloop is an annual running race at the Sint-Pieters square in Ghent. Participating student association teams run a relay race for 12 hours, trying to complete as many laps as possible. Each team is assigned one baton, so they can only have one runner on the track at a time. Zeus WPI has been doing the lap counting at the 12Urenloop for a while: first through a manual counting system where runners’ bib numbers were entered by hand, then from 2011 onwards with an automatic counting system based on bluetooth modules in the batons. For more background on the historical hardware and software, you can read the 2011 blog post. Between 2019 and 2022 the entire stack was overhauled, with more modern hardware, an estimated position of runners shown on the website, and extensive redundancy, fault tolerance and monitoring. There’s also a detailed blog post about that. The live tracking site on 12urenloop.be, showing the associations’ team logos estimated positions moving on the track What is UWB? Around the end of October this year I saw some online demos of UltraWideBand hardware using easily obtainable and relatively cheap hardware. UltraWideBand is a radio communication standard focused on centimeter-level localization of transmitters/receivers. This is possible partly because it uses a large chunk of the radio spectrum (500 MHz), which makes it very good at ignoring signal reflections. That means it can identify the very start of a received packet. If you pair that with a clock accurate to the picosecond level, and compare the time a packet was sent with the time it was received, you can calculate how long that packet was in the air (at the speed of light) between transmitter and receiver. Multiply that time by the speed of light and you get the distance between transmitter and receiver. If you can measure that distance to a moving UWB transmitter (tag) from multiple UWB transmitters at fixed positions, you can calculate the tag’s position. This is the same principle GPS uses, just on a smaller scale. This technology is used in, among other things, iPhones and AirTags, to show the distance and direction to an AirTag. It’s also used in newer car keys that unlock the car when you get close, because a UWB signal can be encrypted and is therefore resistant to relay attacks. What does this tech make possible? There had already been talk since 2021 about using UWB at the 12Urenloop, because it offers some major advantages: Exact position measurement: the current bluetooth system gives a good estimate of progress on the circuit (by calculating speed), but no actual position measurements. With positioning, a lap can be counted at the exact moment someone crosses the start line. This isn’t possible with existing RFID systems used for running races, because at the 12Urenloop there’s a start line per team (at each association’s tent). More statistics about individual runners: maximum/minimum speed, cornering speed, etc. A cleaner signal, since there’s a lot of noise from bluetooth signals coming from the hundreds/thousands of spectators. What you build yourself is usually cheaper Ready-made RTLS (real-time location system) solutions exist, with all the hardware and software you need, but these are outside the budget of a race organized by students (a starter kit from the Ghent-based Pozyx already costs around 5000 euros). In recent years the price of UWB modules has dropped sharply, to around 19-25 euros per module for officially packaged Qorvo modules (used by Pozyx and interoperable with Apple’s U1 chip), and even as low as 12 euros for aftermarket packaged modules (clones) with an integrated microcontroller. So it seemed like a good time to explore how feasible and useful a UWB system could be at the 12Urenloop. On closer inspection, there’s practically no open-source software that supports positioning multiple tags, at least for microcontrollers outside the handful Qorvo supports. Almost all available code focuses on tracking a single tag with multiple anchors, since that’s also a lot simpler. When there are multiple tags and anchors, all UWB modules need to coordinate when they’re allowed to transmit; all communication happens over a single channel, so nothing can talk over anything else. Synchronization between modules needs to be within a few milliseconds to achieve a measurement rate of 1-10 times per second per tag. With that in mind, we ordered 5 DWM3000 UWB modules so we could build a setup with 3 anchors at fixed positions, and 2 tags whose position we determine. To drive the UWB modules we chose the ESP32-WROOM, because they’re cheap (4 euros), have bluetooth and wifi, and were already available in our basement clubroom. The code was written in the Arduino framework, so it’s also easy to port to other microcontrollers. Two DWM3000 devboard shields on a Wemos D1 Uno (ESP32-WROOM board with arduino Uno form factor) Tuning & testing The DW3000 chipset on the UWB module is controlled over SPI, by reading and writing hundreds of registers and about a dozen commands. Qorvo (the manufacturer) provides an SDK to make certain functions easier to implement, but it only works for a handful of microcontrollers such as the NRF52 and STM32 families — not Espressif chips. Fortunately there’s a port of the driver library to the Arduino framework. The focus of the first few weeks was to determine whether the DWM3000 UWB module was suitable for use at the 12Urenloop: is the module’s range far enough (the goal was at least 30 meters), is the measured distance accurate enough, and how often can you measure position when there are 15 tags (one per runner) on the circuit? After a lot of tuning of registers for the radio frame structure and transmit power, we managed to measure distance up to about 50 meters. With larger antennas on the anchors, this could probably be improved a lot further. Range does get worse when there’s no line of sight between the two modules, or when there’s a lot of metal nearby (both cause reflections of the radio signal). A graph of distance and RSSI values of a line of sight range test in open field, walking 40 meters forward, turning around a few times and coming back. After a bit of calibration we managed to measure distance between 2 modules with about 2 centimeters of tolerance within 5 meters. At longer distances, a bit more calibration per module is needed to stay accurate, after that it can stay between 10 centimeters of tolerance using a double sided ranging scheme (both UWB modules measure the distance to each other). After these initial results, it was time to do a positioning test: 2 anchor modules were placed in the corner of our clubroom, and the third, tag module took turns measuring its distance to each anchor. The anchors send those distances to the tag over wifi via MQTT. The triangulated position was visualized in real time by a dashboard built with Bevy. Left is the triangulation application’s live output, Right is our club room’s webcam To get this system working with 2 tags at high speed, without anyone talking over each other on the radio channel, synchronization is needed between anchors: they need to know when it’s their turn to send out a message, and when they need to wait so the rest can take their measurements. In the implementation, this works through a clock shared between all anchors: each anchor is assigned a time slot within an interval. With an interval of 1 second and time slots of 10 milliseconds, for example, you can make 100 distance measurements per second. Oscilloscope trace of an anchor (blue) and tag (red)’s transmit status (on or off), you can see the anchor is sending a ranging request to 2 tags, taking turns every assigned time slot. The red blip you see directly under each blue blip is the response of the red tag to the request from the blue anchor. the other red blips are responses to the other 2 anchors in the system, they are also sending out ranging requests in their assigned timeslots Distribution of the clock (a count of ticks on the UWB chip) happens through the existing UWB messages. Every time a message is sent between an anchor and a tag, they exchange their clock values, and the highest clock value is adopted by both parties (monotonically increasing clock). The crazy setup with 5 UWB modules hooked up to a single laptop, we didn’t have an easy way to connect all UWB modules to one of our ESP32’s Here’s a demo under ideal conditions with 3 anchors and 2 tags: measurements are coordinated at a rate of 60 Hz, which comes out to 60 / 2 / 3 = 10 position measurements per second. In theory, this timing could therefore update the position of 20 tags once per second. The graphics from the triangulation application was overlayed on a video recording of the test for analysis, some miscalibration remains here in certain regions To aggregate all these measurements in real time at the 12Urenloop and build a live-tracking visualization, some more software is needed. The current system uses 7 stations with Raspberry Pis spread around the circuit, connected via Ethernet. To test this prototype at this year’s edition of the 12Urenloop, the 3 anchors were connected to the Raspberry Pis via USB. A publisher script reads the measurement results from the anchors and sends those messages to a central RabbitMQ server in our control room (container). The real-time Bevy dashboard reads all the messages and performs all the triangulation logic, which lap counting can then be based on. Supporting more anchors It’s estimated that 7-10 anchors will be needed to position tags across the entire 12Urenloop circuit, but that means at most 3 anchors will ever be within range of a tag at the same time. If every anchor still needed the channel to contact every tag individually, the measurement rate would drop quickly as the number of anchors