Tutorial 2: Ready to localize (Arduino)
Ready to localize
This is the second Pozyx tutorial, in which we’ll go through the process of performing (remote) positioning with the Pozyx system. If you missed the first one, check that one out first, as each tutorial assumes knowledge of the concepts explained in the ones before.
For this example, you need to own at least the contents of the Ready to Localize kit and a supported Arduino device. Remember that we recommend the Arduino Uno Rev. 3. If all tools are installed, open up the ready to localize example in the Arduino IDE under File > Examples > Pozyx > ready_to_localize.
In this example, we will first set up and measure the anchor locations to perform positioning, after which we’ll be able to get accurate position data of the tag, relative to the anchor setup. We will go in detail through other Pozyx core concepts as well, such as how to check the Pozyx’ device list, and how to read its error status. Mastering these debugging tools now will make using Pozyx in your own projects easier
Anchor setup and measurement
Anchor setup
The Pozyx positioning system requires that the four anchors are placed inside the area where you wish to do positioning. In the guide 'Where to place the anchors?', it is explained how to place the anchors for the best possible positioning accuracy. The rules of thumb for the anchor placement were:
Place the anchors high and in line-of-sight of the user.
Spread the anchors around the user, never all on a straight line!
Place the anchors vertically with the antenna at the top, power plug at the bottom.
For 3D positioning: place the anchors at different heights.
It's also important to keep metallic objects out of immediate range of the antennas.
Before you install the anchors (with the provided Velcros or screws) on the walls or ceiling, it is usually a good idea to make a small sketch of the room, writing down the anchors’ IDs and sketching where they will be placed. You can find the network ID as the hexadecimal number on the label adorning your anchor.
Remember that, for optimal antenna performance, it is recommended to place the anchors vertically with their antenna at the top, and to orient your tags that will be positioning vertically as well. Also make sure that no heavy metal objects are placed near the antenna, as this might degrade performance. We stress this again because it's just that important.
Measurement
While there are different ways to measure your distance, if you are serious about using Pozyx we recommend to have a laser measurer. This will be much more convenient, and much more accurate, than trying to measure out several meters between your anchors with a conventional foldable or extendable We recommend placing them in an approximate rectangle as in the image above. This allows you to simplify both measurement and setup, and to follow along better with the example.
The Pozyx coordinate system
Pozyx determines its position relative to the anchor coordinates you supplied it with. This gives you the freedom to position and rotate your axis in any way you want. This is shown in the images below:
You can see that the coordinate system on the left, which is conventional to the room’s shape and orientation, is made by using anchor 0x256B as an element of the x-axis, giving it a zero y-coordinate. Then the y-axis is perpendicular to this, and anchor 0x3325 therefore has a non-zero x-component to fit into this orthogonal system. Anchor 0x1156 is selected as the origin, again for convenience. This origin doesn’t need to be one of the anchors, however, and you could use the center of the room as the origin point just as well, modifying the anchor’s coordinates accordingly: anchors 0x1156 and 0x3325 would have a negative x-component, while 0x2568 and 0x4244 have a positive coordinate. In turn, 0x1156 and 0x256B have a negative y-component while 0x3325 and 0x4244 will have a positive one.
In the right example, the origin is chosen to be anchor 0x3325’s location, and the x-axis is matched with the path between anchors 0x3325 and 0x1156.
Let’s go over the plan of attack in measuring out your setup, following the approach shown on the left:
Use the antenna’s center as Pozyx’s position in space as best as you can.
Pick one of your anchors as the origin, like we did 0x1156.
Pick the anchor on the same wall to be the one defining your x-axis. We picked 0x2568.
Measure out the horizontal distance between both anchors. Not a direct distance as this will include the vertical distance as well (Pythagoras). This will be that anchor’s x-coordinate. In our case, this was 4.5 m.
Now measure the distance to the opposite wall. Mark this point on the wall, as this is the x-axis’s zero value on that wall. If both walls are parallel, and there are anchors attached directly to this wall, you can set their y-coordinate to this distance. In our case, we could set 0x4244’s y-coordinate directly to 3.5 m.
Now measure the x-coordinates of every anchor on that wall directly using the point you marked earlier, again assuming both walls are parallel. Measurements can be complicated if they are not, so use more reference points then.
If there are anchors a bit apart from the wall, like 0x3325, be sure to account for this in in its y-coordinate.
In this example, we’ve used the approach described above. We’ll also assume the anchors are on heights between 1.1 and 2 meter, leading to these coordinates for each anchor:
0x1156: (0, 0, 1500)
0x256B: (4500, 0, 1800)
0x3325: (500, 3300, 1100)
0x4244: (4450, 3500, 2000)
These are the coordinates used in the example, but you can’t copy these! You will have to change both anchor IDs and coordinates to match your own setup. All these coordinates are expressed in millimeters.
Plug and play
To get the example working and see what exactly is happening before we delve into the code, we’ll need to change the parameters to match your device IDs and the coordinates you measured.
Don’t forget 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. You can see that there’s an array for the anchor IDs and each of the anchor coordinates. You’ll have to translate your measurement to these parameters, where each anchor’s properties has the same index in its respective array. We translated the example setup from above, as a reference.
uint16_t remote_id = 0x6000;
bool remote = false; // set this to true to use the remote ID
boolean use_processing = false; // set this to true to output data for the processing sketch
uint8_t num_anchors = 4; // the number of anchors
uint16_t anchors[4] = {0x1156, 0x256B, 0x3325, 0x4244}; // the network id of the anchors: change these to the network ids of your anchors.
int32_t anchors_x[4] = {0, 4500, 500, 4450}; // anchor x-coorindates in mm
int32_t anchors_y[4] = {0, 0, 3300, 3500}; // anchor y-coordinates in mm
int32_t heights[4] = {1500, 1800, 1100, 2000}; // anchor z-coordinates in mm
uint8_t algorithm = POZYX_POS_ALG_UWB_ONLY;
uint8_t dimension = POZYX_3D;
int32_t height = 1000;
You will also see the remote_id, remote, algorithm, dimension, and use_processing parameters. remote_id and remote should be familiar from the first tutorial. use_processing will be used for the visualization. algorithm, dimension, and height will allow for customization regarding the positioning, and we'll get back to these when we're looking at the code in detail. Leave these parameters unchanged for now, which will result in 3D positioning being done with the UWB-only algorithm.
Now that you’ve done all the plugging, it’s time for play. Run the example, and if all goes well, you’re now looking at coordinates filling up the window, after the manual anchor configuration is checked and printed. As you are using your local device, the ID will be set to 0.
POS ID 0x0000, x(mm): 1231 y(mm): 2354 z(mm): 1167
POS ID 0x0000, x(mm): 1236 y(mm): 2241 z(mm): 1150
etc...
That’s that! You’re now getting accurate position coordinates from the Pozyx! You might also be seeing one of the following:
POS ID 0x0000, x(mm): 0 y(mm): 0 z(mm): 0
or
ERROR configuration on ID 0x0000, error code 0xXX
or
ERROR positioning on ID 0x0000, error code 0xXX
or
Coordinates that are nothing even remotely close to what they should be
If so, you probably miswrote one of the anchor IDs or entered either wrong or mixed up coordinates, causing the positioning algorithm to fail. If the error persists despite having put in everything correctly, check out the troubleshooting guide. Now that you've seen the positioning in action, let's look at the code that made this possible.
The code explained
We will now cover the essential code to get positioning working, but there's a lot more code in the file. The extras segment will cover how to access the Pozyx's device list, how to retrieve error codes returned by the Pozyx, and how to retrieve additional sensor data each time you position.
Setup and manual calibration
Both the serial communication and Pozyx are initialized in the setup function.
void setup(){
Serial.begin(115200);
if(Pozyx.begin() == POZYX_FAILURE){
Serial.println(F("ERROR: Unable to connect to POZYX shield"));
Serial.println(F("Reset required"));
delay(100);
abort();
}
if(!remote){
remote_id = NULL;
}
Serial.println(F("----------POZYX POSITIONING V1.0----------"));
Serial.println(F("NOTES:"));
Serial.println(F("- No parameters required."));
Serial.println();
Serial.println(F("- System will auto start anchor configuration"));
Serial.println();
Serial.println(F("- System will auto start positioning"));
Serial.println(F("----------POZYX POSITIONING V1.0----------"));
Serial.println();
Serial.println(F("Performing manual anchor configuration:"));
// clear all previous devices in the device list
Pozyx.clearDevices(remote_id);
// sets the anchor manually
setAnchorsManual();
printCalibrationResult();
delay(2000);
Serial.println(F("Starting positioning: "));
}
The setup function is straightforward. After its initialization of the Pozyx, it performs the anchor configuration. It first clears the Pozyx’s device list using clearDevices(), and then manually adds the anchors in setAnchorsManual() we set up and measured out. Let’s look at how this is done:
void setAnchorsManual(){
for(int i = 0; i < num_anchors; i++){
device_coordinates_t anchor;
anchor.network_id = anchors[i];
anchor.flag = 0x1;
anchor.pos.x = anchors_x[i];
anchor.pos.y = anchors_y[i];
anchor.pos.z = heights[i];
Pozyx.addDevice(anchor, remote_id);
}
if (num_anchors > 4){
Pozyx.setSelectionOfAnchors(POZYX_ANCHOR_SEL_AUTO, num_anchors);
}
}
We define each anchor as a device_coordinates_t object, which takes an ID, a flag to indicate what kind of device it is – POZYX_ANCHOR or POZYX_TAG - and the device’s coordinates. We then add this anchor to the device’s stored device list, which it will use when positioning. If you’d use more than four anchors, the device’s anchor selection is set to automatically use all available anchors from this set.
Loop
Now that we’ve properly configured the device’s device list with our set of anchors, let’s look at just how easy the actual positioning is in the short loop() function:
void loop(){
coordinates_t position;
int status;
if(remote){
status = Pozyx.doRemotePositioning(remote_id, &position, dimension, height, algorithm);
}else{
status = Pozyx.doPositioning(&position, dimension, height, algorithm);
}
if (status == POZYX_SUCCESS){
// prints out the result
printCoordinates(position);
}else{
// prints out the error code
printErrorCode("positioning");
}
}
We first create a new object that will contain the Pozyx’s measured coordinates, after which we call the Pozyx’s doPositioning function, which will perform the positioning algorithm and store the coordinates, measured respectively to the anchors, in the position object. That’s essentially the entire positioning loop! If doPositioning returns POZYX_SUCCESS, the position will be printed in a human readable way. We see that we can pass the positioning’s algorithm, dimension, and Pozyx height (used in 2.5D) as parameters in the positioning. There are three dimensions supported by Pozyx: POZYX_2D, POZYX_2_5D, and POZYX_3D.
In 2D the anchors and tags must all be located in the same horizontal plane. This is not the case for semi-3D or 3D positioning. In semi-3D positioning the height of the tag must be supplied in the height parameter (for example, when the tag is mounted on a driving robot with fixed height). The reason why semi-3D exists is because in many cases it is not possible to obtain an accurate estimate for the z-coordinate in 3D-positioning. This is a result of how the anchors are placed and is explained in the guide 'Where to place the anchors?'. As a final parameter you can supply which algorithm to use. Possible values are POZYX_POS_ALG_UWB_ONLY and POZYX_POS_ALG_TRACKING. By default POZYX_POS_ALG_UWB_ONLY is used. For more information about the algorithms we refer to the register documentation for POZYX_POS_ALG. We've defined the algorithm as a parameter, so you can change the algorithm in the paramaters section instead of directly doing so in the function.
Some example usages:
status = Pozyx.doPositioning(&position, POZYX_2_5D, 1000);
semi-3D positioning with the height of the tag fixed at 1000mm (about 3.3feet).
status = Pozyx.doPositioning(&position, POZYX_3D);
3D positioning, this requires at least 4 anchors. Note that the anchors must be placed at different heights to obtain a good accuracy of the z-coordinate.
Remote positioning
Positioning the Pozyx attached to your PC means that, except for when you have a long cable, you'll also need to be able to move your PC around if you want to position over a larger area than your desk. This would also mean that if you'd want to track objects, you'd need to attach a processing unit to these objects as well, instead of only the Pozyx. Luckily, the Pozyx attached to your computer can act as a master device, commanding one or more remote 'slaves'.
To position a remote device, you need to do the same as on a local device: put anchors in its device list that it will use for the positioning. A common misconception about the use of Pozyx is that configuring the anchors on your local device will make remote devices capable of positioning straight off the bat, but this isn't the case. You will notice that the addDevice function in setAnchorsManual adds the device to a remote device if remote positioning is enabled, thusly configuring the anchors on the remote device and not on the local one.
Extras
While not necessary for positioning, the added functionality in the code can go a long way for extending the use of Pozyx or when things go wrong, so going over these extras is recommended if you want to take things further without needing to figure things out yourself.
Printing the configuration result
To find out whether the calibration was in fact successful, we will retrieve the Pozyx’s device list. This is the reverse of the configuration step, as we now retrieve the IDs and coordinates in turn from the Pozyx. Pozyx requires this to be done in several steps:
Firstly we retrieve the size of the device list through getDeviceListSize.
We use this size to create an appropriately sized device list container
We retrieve the IDs in the device’s device list using this container with getDeviceIds. You can also use getTagIds and getAnchorIds to have more control over which device IDs you’re getting.
Now we can get the device coordinates belonging to each of these IDs using getDeviceCoordinates
This is what is done in the code below, and we print the anchor’s ID with its retrieved coordinates. If these don’t match the anchors you passed to the device, something went wrong and it is recommended to try again. If this keeps failing, try going through the troubleshooting.
void printCalibrationResult(){
uint8_t list_size;
int status;
status = Pozyx.getDeviceListSize(&list_size, remote_id);
Serial.print("list size: ");
Serial.println(status*list_size);
if(list_size == 0){
printErrorCode("configuration");
return;
}
uint16_t device_ids[list_size];
status &= Pozyx.getDeviceIds(device_ids, list_size, remote_id);
Serial.println(F("Calibration result:"));
Serial.print(F("Anchors found: "));
Serial.println(list_size);
coordinates_t anchor_coor;
for(int i = 0; i < list_size; i++)
{
Serial.print("ANCHOR,");
Serial.print("0x");
Serial.print(device_ids[i], HEX);
Serial.print(",");
Pozyx.getDeviceCoordinates(device_ids[i], &anchor_coor, remote_id);
Serial.print(anchor_coor.x);
Serial.print(",");
Serial.print(anchor_coor.y);
Serial.print(",");
Serial.println(anchor_coor.z);
}
}
Printing the error code
Pozyx’s operation can misbehave due to various reasons. Other devices not being in range, being on different settings, or another firmware version… As you’re getting started with Pozyx, it’s hard to keep track of where exactly things go wrong, and it’s for this reason that Pozyx keeps track of what went wrong in the error status register, POZYX_ERRORCODE. The error code can be read in two ways, one uses the getErrorCode function to directly read out the value from the error register, while the other, through getSystemError, returns a more verbose error message, representing the relative error textually. For example, as the error code would be 0x05, getSystemError would return "Error 0x05: Error reading from a register from the I2C bus". While the latter is ideal when working in a printed environment, if you work with visualizations it’s less than ideal to handle this entire string and you can perform custom functionality with the error code.
void printErrorCode(String operation){
uint8_t error_code;
if (remote_id == NULL){
Pozyx.getErrorCode(&error_code);
Serial.print("ERROR ");
Serial.print(operation);
Serial.print(", local error code: 0x");
Serial.println(error_code, HEX);
return;
}
int status = Pozyx.getErrorCode(&error_code, remote_id);
if(status == POZYX_SUCCESS){
Serial.print("ERROR ");
Serial.print(operation);
Serial.print(" on ID 0x");
Serial.print(remote_id, HEX);
Serial.print(", error code: 0x");
Serial.println(error_code, HEX);
}else{
Pozyx.getErrorCode(&error_code);
Serial.print("ERROR ");
Serial.print(operation);
Serial.print(", couldn't retrieve remote error code, local error: 0x");
Serial.println(error_code, HEX);
}
}
In this example, simple but comprehensive error checking is executed. If the positioning is purely local, the local error is read. When remote positioning fails, indicated by the positioning function returning POZYX_FAILURE, the error register is read. If the error couldn’t be read remotely, the error is read locally. We are using getErrorCode instead of getSystemError because this allows us to efficiently send the error data to the visualization, and customize our error output. You can find the resulting error codes’ meaning at the register documentation of POZYX_ERRORCODE.
Adding sensor information
Sensor information, such as orientation or acceleration, can easily be added to the code, as to return this sensor data every positioning loop. Adding orientation and/or acceleration allows you to get a better insight in the object you're tracking, but you'll have to account for the reduced positioning update rate caused by this additional operation. Especially remotely, this will delay your update rate. In this example code, not present in the actual script, we'll retrieve both orientation and acceleration.
void printOrientationAcceleration(){
orientation = euler_angles_t;
acceleration = acceleration_t;
Pozyx.getEulerAngles_deg(&orientation, remote_id);
Pozyx.getAcceleration_mg(&acceleration, remote_id);
Serial.print("Orientation: Heading:");
Serial.print(orientation.heading);
Serial.print(", Roll:");
Serial.print(orientation.roll);
Serial.print(", Pitch:");
Serial.print(orientation.pitch);
Serial.print(", acceleration: X:");
Serial.print(acceleration.x);
Serial.print(", Y:");
Serial.print(acceleration.y);
Serial.print(", Z:");
Serial.print(acceleration.z);
}