Accurate positioning

Home > Documentation > Tutorials > Tutorial 1: Ready to range

Tutorial 1: Ready to range

Go to Arduino versionGo to Python version

Ready to range

This is the first general Pozyx tutorial. If you haven’t read the getting started, please do so and install the necessary tools and libraries.

This example requires two Pozyx devices and one supported Arduino device. Remember that we recommend the Arduino Uno Rev. 3. If all tools are installed, open up the ready to range example in the Arduino IDE under File > Examples > Pozyx > ready_to_range.

This example requires two Pozyx devices. If all tools are installed, open up ready_to_range.py in the Pozyx library’s tutorial folder. Probably, the path to this file will be "Downloads/Pozyx-Python-library/tutorials/ready_to_range.py". You can run the script from either command line or a text editor that allows running the scripts as well. If you're on Windows and have installed Python, you might be able to simply double-click it.

In this example, the distance between the two devices is measured and the onboard LEDs will show in which range the devices are to each other. The destination Pozyx's LEDs will be doing the same, commanded by our local Pozyx. The LEDs' changes with respect to the distance is sketched in the figure below:

Plug and play

To see the tutorial in action before we delve into the code, the parameters will need to be changed to match your destination device's ID.

You also have to set the Arduino IDE’s serial monitor’s baudrate to 115200. At the top of the Arduino sketch, we find the sketch’s parameters:

At the bottom of the Python script, in the ifmain structure, we find the script’s parameters. You can see that the first one is the serial port address of your device. Finding the serial port is described in getting started, but you can automate the port selection using serial_port = get_serial_ports()[0].device. This automatic serial port selection will be used in the next examples.

uint16_t destination_id = 0x6670; // the network ID of the ranging destination
signed int range_step_mm = 1000;  // distance between each LED lighting up.

uint8_t ranging_protocol = POZYX_RANGE_PROTOCOL_PRECISION; // the ranging protocol to use.

uint16_t remote_id = 0x605D;      // the network ID of the remote device
bool remote = false;              // whether to use the given remote device for ranging
if __name__ == "__main__":
    port = 'COM1'            # COM port of the Pozyx device

    remote_id = 0x605D       # the network ID of the remote device
    remote = False           # whether to use the given remote device for ranging
    if not remote:
        remote_id = None

    destination_id = 0x1000  # network ID of the ranging destination
    range_step_mm = 1000     # distance between each LED lighting up.

    ranging_protocol = POZYX_RANGE_PROTOCOL_PRECISION #ranging protocol

You’ll have to change the destination_id parameter to the ID of your destination Pozyx. Optionally, you can change the range_step_mm as well, which will make the distance range indicated by the LEDs either higher or lower depending on your change. remote_id and remote will allow measuring the distance between two remote Pozyx devices, and will be explained at the end of this tutorial, so don't change those for now.

The ranging_protocol is a new feature that allows changing between two ranging protocols: POZYX_RANGE_PROTOCOL_FAST and POZYX_RANGE_PROTOCOL_PRECISION. Precision is slower, but more precise and can be used on longer ranges. The fast protocol needs to warm up for about 100ms before. If this has piqued your interest, definitely check the system performance section in the datasheet!

If you’ve correctly changed your device ID, run the example! You should see that the LEDs of your two Pozyx devices now burn identically, and change when you move them closer or further apart. The output you’re receiving will look like this:

16691ms, 7564mm, -93dBm
16753ms, 7718mm, -91dBm
16830ms, 7779mm, -92dBm

The first value is the device’s timestamp of the range measurement. The second value then is the measured distance between both devices. The third value, expressed in dBm, signifies the signal strength, varying between -79 and -103 dB typically. You might be reading either:

0ms, 0mm, 0dB
ERROR: ranging

If this is the case, you probably miswrote the ID (did you write it with the 0x prefix?) or your device is not in range currently. As the LEDs will only indicate differences of a meter by default, keeping the distance between a couple of meters for this first try is recommended. If the error persists despite having put in everything correctly, check out the troubleshooting guide.

If you’re getting gibberish, you probably forgot to set your serial monitor’s baudrate to 115200.

The code explained

Now that you’ve had a taste of what exactly this example provides, let’s look at the code that provides this functionality. We’ll begin with the imports and the setup functions, and then go into the continuous loop.


#include <Pozyx.h>
#include <Pozyx_definitions.h>
#include <Wire.h>
from pypozyx import *

We can see that the sketch requires both “Pozyx.h” and “Pozyx_definitions.h”, and the "Wire.h" file for the I2C.. “Pozyx.h” provides the library’s functionality, “while Pozyx_definitions.h” provides constants such as POZYX_SUCCESS and POZYX_FAILURE, register addresses… these are useful to keep your code working with both current, previous, and future versions of the library.

We import everything from pypozyx. This import is made to provide you with the Pozyx constants, data containers, and the PozyxSerial class. This allows for general Pozyx use, as you have every library function available to you this way through PozyxSerial. If you need access to the register addresses, you can import pypozyx.constants.registers. For the bitmasks, import pypozyx.constants.bitmasks.

Emulating the Arduino flow

As a Python script isn't executed as an Arduino sketch, we emulate this with the following code. The setup function is run once, after which the loop function is executed continuously. We also pass the PozyxSerial object as a parameter, which makes the Pozyx not exclusive to this class if we want to add other functionality elsewhere. The other parameters defined above are passed, this has the purpose of not making this ReadyToRange class exclusive to this script, and stimulates reuse.

pozyx = PozyxSerial(port)
      r = ReadyToRange(pozyx, destination_id, range_step_mm, remote_id)
      while True:


This function will run only once, when the Arduino is powered on or reset. It sets up the serial communication at 115200 baud, so be sure to set your Serial Monitor to this same baudrate if you hadn’t already. The Pozyx is then initialized by calling Pozyx.begin(), which checks whether the Pozyx is connected and working properly.

This function will be run only once at the script’s execution, as you can see in the ifmain section, after this the loop will be continuously called. The PozyxSerial is initialized with the port we supplied in the parameters, so it’s important to provide the correct port, as you’ll notice when trying to run the code with an incorrect port. This initialization also checks for correct operation of the connected Pozyx.

void setup(){

  if(Pozyx.begin() == POZYX_FAILURE){
    Serial.println("ERROR: Unable to connect to POZYX shield");
    Serial.println("Reset required");
  // setting the remote_id back to NULL will use the local Pozyx
  if (!remote){
    remote_id = NULL;
  Serial.println("------------POZYX RANGING V1.0------------");
  Serial.println("- Change the parameters:");
  Serial.println("\tdestination_id (target device)");
  Serial.println("\trange_step (mm)");
  Serial.println("- Approach target device to see range and");
  Serial.println("led control");
  Serial.println("------------POZYX RANGING V1.0------------");
  Serial.println("START Ranging:");

  // make sure the pozyx system has no control over the LEDs, we're the boss
  uint8_t led_config = 0x0;
  // do the same with the remote device
  Pozyx.setLedConfig(led_config, destination_id);
def setup(self):
    """Sets up both the ranging and destination Pozyx's LED configuration"""
    print("------------POZYX RANGING V1.0 - -----------")
    print("NOTES: ")
    print(" - Change the parameters: ")
    print("\tdestination_id(target device)")
    print("- Approach target device to see range and")
    print("led control")
    print("- -----------POZYX RANGING V1.0 ------------")
    print("START Ranging: ")

    # make sure the local/remote pozyx system has no control over the LEDs.
    led_config = 0x0
    self.pozyx.setLedConfig(led_config, self.remote_id)
    # do the same for the destination.
    self.pozyx.setLedConfig(led_config, self.destination_id)

Depending on the value of the remote parameters, we set the remote_id to use either the local or a remote Pozyx. After initializing the Pozyx, we configure both the local and destination Pozyx’s LEDs to be in our control instead of displaying system status, which is their default behavior. This is done by setting the POZYX_CONFIG_LEDS register’s value to 0b00000000 on both devices. While the library’s functionality hides away a lot of low-level functionality, it’s important to know how to access registers directly low-level and to find your way in the register overview for your own personal goals. These core low-level functions are:

  • static int regRead(uint8_t reg_address, uint8_t *pData, int size);
  • static int regWrite(uint8_t reg_address, const uint8_t *pData, int size);
  • static int regFunction(uint8_t reg_address, uint8_t *params, int param_size, uint8_t *pData, int size);

and the remote equivalents

  • static int remoteRegWrite(uint16_t destination, uint8_t reg_address, uint8_t *pData, int size);
  • static int remoteRegRead(uint16_t destination, uint8_t reg_address, uint8_t *pData, int size);
  • static int remoteRegFunction(uint16_t destination, uint8_t reg_address, uint8_t *params, int param_size, uint8_t *pData, int size);
  • getRead(address, data, remote_id=None)
  • setWrite(address, data, remote_id=None)
  • useFunction(function, params=None, data=None, remote_id=None)

You can see that these work both locally and remotely through the remote_id parameter, so you can access the registers on remote devices as well.


Let’s move on to the loop function, which will run continuously and executes the ranging functionality.

void loop(){
  device_range_t range;
  int status = 0;

  // let's perform ranging with the destination
    status = Pozyx.doRanging(destination_id, &range);
    status = Pozyx.doRemoteRanging(remote_id, destination_id, &range);

  if (status == POZYX_SUCCESS){
    Serial.print("ms, ");
    Serial.print("mm, ");

    // now control some LEDs; the closer the two devices are, the more LEDs will be lit
    if (ledControl(range.distance) == POZYX_FAILURE){
      Serial.println("ERROR: setting (remote) leds");
    Serial.println("ERROR: ranging");
def loop(self):
    """Performs ranging and sets the LEDs accordingly"""
    device_range = DeviceRange()
    status = self.pozyx.doRanging(self.destination_id, device_range, self.remote_id)
    if status == POZYX_SUCCESS:
        if self.ledControl(device_range.distance) == POZYX_FAILURE:
            print("ERROR: setting (remote) leds")
        print("ERROR: ranging")

In the loop function, ranging is performed with the Pozyx library’s doRanging function. The range information will be contained in device_range.

When we want to perform ranging on a remote Pozyx, we use doRemoteRanging instead. The device_range variable is an instance of the device_range_t structure. Looking at its definition (in Pozyx.h) we can see that the device range consists of three variables providing information about the measurement. By using the __attribute__((packed)) attribute, the struct’s bytes are packed together.

typedef struct __attribute__((packed))_device_range {
        uint32_t timestamp;
        uint32_t distance;
        int8_t RSS;

The device_range variable is an instance of the DeviceRange class. Looking at its definition (in device.py in the pypozyx/structures folder, we can see that the device range consists of three variables providing information about the measurement. The library also provides human-readable string conversion of its important data structures, and print(device_range) prints the range data in our desired format. The ByteStructure parent class provides functionality that makes sure the structure's bytes are packed together.

class DeviceRange(ByteStructure):
    def __init__(self, timestamp=0, distance=0, RSS=0):
        """Initializes the DeviceRange object."""
        self.timestamp = timestamp
        self.distance = distance
        self.RSS = RSS

In a wireless system, we don’t want to waste transmission time on unused padding bytes, after all. The library provides similar structures for all relevant data structures related to Pozyx, which you'll encounter if you follow the other tutorials and get into further reading. The measurement consists of the timestamp in milliseconds, the measured distance in mm and the RSS-value (the received signal strength) expressed in dBm. Depending on the UWB settings, -103 dBm is more or less the lowest RSS at which reception is possible, and -80 is a normal value for devices close to each other. Upon a successful measurement, these three variables are printed and the LEDs are updated with the ledControl function.


int ledControl(uint32_t range){
  int status = POZYX_SUCCESS;
  // set the LEDs of the pozyx device
  status &= Pozyx.setLed(4, (range < range_step_mm), remote_id);
  status &= Pozyx.setLed(3, (range < 2*range_step_mm), remote_id);
  status &= Pozyx.setLed(2, (range < 3*range_step_mm), remote_id);
  status &= Pozyx.setLed(1, (range < 4*range_step_mm), remote_id);

  // set the LEDs of the destination pozyx device
  status &= Pozyx.setLed(4, (range < range_step_mm), destination_id);
  status &= Pozyx.setLed(3, (range < 2*range_step_mm), destination_id);
  status &= Pozyx.setLed(2, (range < 3*range_step_mm), destination_id);
  status &= Pozyx.setLed(1, (range < 4*range_step_mm), destination_id);

  // status will be zero if setting the LEDs failed somewhere along the way
  return status;
def ledControl(self, distance):
    """Sets LEDs according to the distance between two devices"""
    status = POZYX_SUCCESS
    ids = [self.remote_id, self.destination_id]
    # set the leds of both local/remote and destination pozyx device
    for id in ids:
        status &= self.pozyx.setLed(4, (distance < range_step_mm), id)
        status &= self.pozyx.setLed(3, (distance < 2 * range_step_mm), id)
        status &= self.pozyx.setLed(2, (distance < 3 * range_step_mm), id)
        status &= self.pozyx.setLed(1, (distance < 4 * range_step_mm), id)
    return status

The ledControl function turns the LEDs on or off if they fall within a certain range, using a simple boolean expression, for example range < 3*range_step_mm, which returns true if the distance is within three times the range_step_mm parameter.
Nothing is stopping you from designing your own ledControl function, where you can use other indexes or, as a challenge, even use the four LEDs as 4 bits indicating up to sixteen range_step_mm differences instead of four.

Remote operation

As you have probably seen when looking at the code and the register functions, Pozyx is capable of communicating with remote Pozyx devices: thanks to UWB communication, we can perform every local functionality on a remote device as well. In this tutorial, this can be done by changing the remote_id and remote parameters. Setting remote = true; will perform all the functionality on a device with ID remote_id, so you will need to set that ID correctly as well and own three devices to perform remote ranging.

A general rule with the Pozyx API is that almost every function has remote_id as a keyword argument, which allows the writing of code that can easily switch between being executed locally or remotely, as seen in this example. When remote_id is uninitialized, the function will be executed locally.

The disadvantages of working remotely need to be kept in mind, however. The possibility of losing connection, something that is much harder with a cable. A delay, as the UWB communication between both devices is not instantaneous. And you need a local controller, which is an extra device in your setup. In turn, you gain a lot of mobility, as the remote device only needs to be powered, not needing a controller.

This concludes the first Pozyx example. In the next example, we’ll cover positioning and the necessary setup to perform positioning successfully.

Home > Documentation > Tutorials > Tutorial 1: Ready to range