Introduction
This module is a study of the Angle of Arrival (AoA). AoA is a technique for finding the direction that an incoming packet is coming from. The estimated time to complete this lab is between 1-2 hours. It is assumed that the reader has a basic knowledge of embedded C tool chains and general C programming concepts.
This lab is based on the rtls_master, rtls_slave and rtls_passive projects that are part of
the SimpleLink™ CC2640R2 SDK.
First, the lab will cover an overview on how to get started with the AoA projects. Subsequent tasks of this lab will guide the user on how to customize these projects in order to answer most of the common AoA questions.
Prerequisites
Other SimpleLink Academy Labs
- Completion of Realtime Localization System Introduction
Required Training
It is required that users complete all steps of the RTLS Introduction lab before moving on to the Angle of Arrival lab. It is also assumed that the user has already installed the software, setup the hardware, and read the recommended chapters covered by the introduction lab. For brevity, only new content will be listed here.
Software for desktop development
- SimpleLink™ CC2640R2 SDK 3.20.00
- Required tools and version are listed in the CC2640R2 SDK 3.20.00 Release Notes
Hardware
This module requires the following kits:
- 3x CC2640R2-LAUNCHXL
- 1x BOOSTXL-AoA
Getting started with AoA booster pack
Getting started – Desktop
Install the Software
This lab requires that you have completed all the steps in the RTLS Introduction lab. This means that you must have
- CC2640R2 SDK installed
- Working Python environment
- Used
rtls_example.pyscript that sets up an RTLS network.
Introduction to AoA
Bluetooth Core Specification Version 5.1 introduces AoA/AoD which are
covered under Direction Finding Using Bluetooth Low Energy section.
AoA is for receivers that have RF switches, multiple antennas, can switch antennas and capture I/Q samples when receiving direction finding packets.
AoD is for transmitters that have RF switches, multiple antennas and can switch antennas when transmitting direction finding packets. And recieivers only have one antenna but can capture I/Q samples when receiving direction finding packets.
In the table below and throughout this lab CTE is used in place of Constant Tone Extension as defined by the BLE specification.
| Direction Finding Method | Transmitter | Receiver |
|---|---|---|
| AoA | Single antenna, transmit CTE | Multiple antennas, RF switches, can switch antennas while capturing I/Q data of the CTE |
| AoD | Multiple antennas, RF switches, transmits CTE while switching antennas | Single antenna, can capture I/Q data of the CTE |
On top of that, the Bluetooth Core Specification version 5.1 specifies the following states can support sending direction finding packets:
- Periodic advertising; also called
Connectionless CTE - Connection; also called
Connection CTE
The theory behind AoA/AoD and Connectionless/Connection CTE is the same, therefore in this SimpleLink Academy training, we will only focus on Connection CTE AoA.
We will explain the AoA theory first and the walk through our SimpleLink CC2640R2F SDK offering.
AoA measurement is typically a 3-step process:
- Collect phase information by sampling the I/Q
- Calculate the phase difference among the antennas
- Covert the phase difference into Angle of Arrival
1. Collect phase information by sampling the I/Q
When two (or more) antennas are placed at a given distance apart from each other, their received RF signals will have a phase difference that is proportional to the difference between their respective distances from the transmitter.
Typically, the signal from one antenna will be a delayed version of the signal from the other antenna. If they are sufficiently close (less than Λ(wavelength)/2 between phase centers), you can always determine which is closest to the transmitter.
These path length differences will depend on the direction of the incoming RF waves relative to the antennas in the array. In order to measure the phase difference accurately, the radio wave packet must contain a section of constant tone with whitening disabled where there are no phase shifts caused by modulation.
Constant Tone Extension (CTE)
The constant tone extension is a section of consecutive 1's without whitening, which is effectively a +250kHz wave on top of the carrier wave. In the Bluetooth Core Specification Version 5.1, both periodic advertising packets and connection packets can contain a constant tone extension(CTE) after the CRC. The CTE can only be sent using uncoded PHY.

2. Calculate the phase difference among the antennas
Phase Difference (Φ) is measured by connecting at least two antennas to the same receiver sequentially (more antennas can be added).


In order to get a good estimate of Φ (phase), all other intentional phase shifts in the signal should be removed. Connection CTE AoA solution achieves this by adding CTE at the end of packets.
![]() |
|---|
| Connection CTE Packet Format |
Data Physical Packet format
The grey colored part of the packet shown above means
it's optional. The Constant Tone Extension is only enabled when the CP bit
in the Data Physical PDU header is set. The detail of the Constant Tone
Extension is then specified by the CTEInfo field in the in the Data
Physical PDU.
This gives the receiver time to synchronize the demodulator first, and then store the I/Q samples from the CTE into radio RAM. The I/Q data is then extracted by the application layer.
I/Q samples are used to estimate phase difference among antennas. When the receiver gets AoA packets, the RF core will trigger an event that will lead to start of the antenna switching. The RF core will start sampling the I/Q data after the guard period of the CTE and the sampled data will be stored in the radio RAM.
By comparing the I/Q data collected from different antennas, users can get the relative phase difference among antennas.
3. Convert the phase difference into Angle of Arrival
Last step is converting the phase shift (Φ) back to AoA (Θ). Keep in mind if Φ is negative, this means that antenna 2 is ahead of antenna 1 and the Θ.
The angle between the incident wave and antenna array is Θ. Base on the picture below we know that the sin(Θ) = r/d, and d is the distance between antenna 1 and antenna 2 which is known. Then all we need to find out is r.
r is the distance to antenna 2 that the incident wave needs to travel after arriving at antenna 1. We have found that the phase difference between antenna 1 and antenna 2 is Φ, so the extra distance r is equal to wavelength of the incoming signal * Φ/(2Π).
r= Λ* Φ/(2Π)


Note
In rtls_passive example, you might notice that step 3 is not implemented.
The reason is simply because by step 2 using our
BOOSTXL-AoA we already have
linear result from +/-90 degree using antenna pair[1,2] and pair[2,3].
Therefore all we need is to have a look up table to do the fine tuning.
This saves computation time/current consumption compared to doing another arcsin function.
We also tried to do arcsin, but the result is pretty much the same as adding gain and offset after step 2.
What technique does AoA use to identify the direction of incident wave?
Task 1 – Running the AoA application
The SimpleLink™ CC2640R2 SDK
has three examples dedicated to performing AoA (rtls_master,
rtls_slave and rtls_passive).
In the TI BLE-Stack User's Guide → RTLS Toolbox → Angle of Arrival → AoA application Overview, we have covered how the application works.
Therefore, after following the instruction on Realtime Localization System Introduction, you should be able to get it up and running.
Task 2 – Modify AoA application to use only one antenna array
The out of box SimpleLink™ CC2640R2 SDK
rtls_passive example will stay on one antenna array when receiving
one AoA packet and
then change to another antenna array for the next AoA packet.
Following code contains the logic for antenna array switching.
// Switch ant array
if (gAoaReport.antConfig == antA1Config)
{
gAoaReport.antConfig = antA2Config;
gAoaReport.antResult = antA2Result;
}
else if (gAoaReport.antConfig == antA2Config)
{
gAoaReport.antConfig = antA1Config;
gAoaReport.antResult = antA1Result;
}
else
{
// Should not get here
return;
}
AOA.c::AOA_setupNextRun()
The angle is then decided once both arrays have received AoA packets and judge by the rssi value of the received packet to decide whether the application should trust angle derived from array 1 or array 2.
// Signal strength is higher on A1 vs A2
if (antA1Result->rssi > antA2Result->rssi)
{
// Use AoA from Antenna Array A1
AoA_ma.array[AoA_ma.idx] = AoA_A1;
AoA_ma.currentAoA = AoA_A1;
AoA_ma.currentAntennaArray = 1;
AoA_ma.currentRssi = antA1Result->rssi;
AoA_ma.currentSignalStrength = signalStrength_A1;
AoA_ma.currentCh = antA1Result->ch;
AoA.currentangle = AoA_A1;
}
// Signal strength is higher on A2 vs A1
else
{
// Use AoA from Antenna Array A2
AoA_ma.array[AoA_ma.idx] = AoA_A2;
AoA_ma.currentAoA = AoA_A2;
AoA_ma.currentAntennaArray = 2;
AoA_ma.currentRssi = antA2Result->rssi;
AoA_ma.currentSignalStrength = signalStrength_A2;
AoA_ma.currentCh = antA2Result->ch;
AoA.currentangle = AoA_A2;
}
rtls_ctrl_aoa.c::RTLSCtrl_estimateAngle()
However, this might not be how you want to design or utilize your antennas. Therefore, this section we will walk you through the changes if you only want one antenna array, like Automotive Bluetooth low energy car access satellite node reference design
We will go from this configuration:

To this configuration (using antenna array #1):

The steps below will guide you through the process of removing one of the antenna
array. For this example we will be removing A2, but these steps are also
applicable in case that A1 needs to be removed.
- Start from the
rtls_passive_cc2640r2lp_appproject and rename it tortls_passive_cc2640r2lp_a1_app - Exclude the files
ant_array2_config_boostxl_rev1v1.candant_array2_config_boostxl_rev1v1.hfrom the build. - Comment out all of the antenna array 2 dependencies from
AOA.c
//Comment out all the followings lines
//line 64
#include "ant_array2_config_boostxl_rev1v1.h"
// Remove config and result declarations line ~170
AoA_AntennaConfig *antA2Config = &BOOSTXL_AoA_Config_ArrayA2;
AoA_AntennaResult *antA2Result = &BOOSTXL_AoA_Result_ArrayA2;
//line 399, 402 and 405 in AOA_init()
BOOSTXL_AoA_AntennaPattern_A2_init();
antA2Result->updated = false;
aoaResults->antA2Result = antA2Result;
//line 730~733 in AOA_getActiveAnt()
else if (gAoaReport.antConfig == antA2Config)
{
return 2;
}
//line 756~760 in AOA_setupNextRun()
else if (gAoaReport.antConfig == antA2Config)
{
gAoaReport.antConfig = antA1Config;
gAoaReport.antResult = antA1Result;
}
Change the following lines in AOA.c to take antenna 1 instead.
//line 753 and 754 in AOA_setupNextRun() // Switch ant array if (gAoaReport.antConfig == antA1Config) { gAoaReport.antConfig = antA1Config; gAoaReport.antResult = antA1Result; }c
Replace the content of RTLSCtrl_postProcessAoa in
rtls_ctrl_aoa.cwith the provided code snippet. What we have done here is to remove all the antenna array 2 dependency.void RTLSCtrl_postProcessAoa(rtlsAoa_t *aoaControlBlock, int8_t rssi, uint8_t channel) { uint8_t status; if (aoaControlBlock->aoaParams.aoaRole == AOA_ROLE_PASSIVE) { status = AOA_postProcess(rssi, channel); if (status) { switch(aoaControlBlock->aoaParams.resultMode) { case AOA_MODE_ANGLE: { AoA_Sample aoaTempResult; rtlsAoaResultAngle_t aoaResult; AOA_getPairAngles(); { gAoaResults.antA1Result->updated = false; const AoA_AntennaResult *antA1Result = gAoaResults.antA1Result; aoaTempResult = RTLSCtrl_estimateAngle(antA1Result); aoaResult.angle = aoaTempResult.angle; aoaResult.antenna = aoaTempResult.antenna; aoaResult.rssi = aoaTempResult.rssi; aoaResult.channel = aoaTempResult.channel; RTLSHost_sendMsg(RTLS_CMD_AOA_RESULT_ANGLE, HOST_ASYNC_RSP, (uint8_t *)&aoaResult, sizeof(rtlsAoaResultAngle_t)); } } break; case AOA_MODE_PAIR_ANGLES: { rtlsAoaResultPairAngles_t aoaResult; int16_t *pairAngle; uint8_t antenna; AOA_getPairAngles(); if (gAoaResults.antA1Result->updated == true) { pairAngle = gAoaResults.antA1Result->pairAngle; gAoaResults.antA1Result->updated = false; antenna = 1; } else { // Should not get here return; } aoaResult.rssi = rssi; aoaResult.channel = channel; aoaResult.antenna = antenna; for (int i = 0; i < AOA_NUM_ANTENNAS; i++) { aoaResult.pairAngle[i] = pairAngle[i]; } RTLSHost_sendMsg(RTLS_CMD_AOA_RESULT_PAIR_ANGLES, HOST_ASYNC_RSP, (uint8_t *)&aoaResult, sizeof(rtlsAoaResultPairAngles_t)); } break; case AOA_MODE_RAW: { rtlsAoaResultRaw_t *aoaResult; uint16_t numAoaSamples; uint16_t samplesToOutput; AoA_IQSample *pIter; aoaResult = RTLSCtrl_malloc(sizeof(rtlsAoaResultRaw_t) + (MAX_SAMPLES_SINGLE_CHUNK * sizeof(AoA_IQSample))); pIter = AOA_getRawSamples(); // If the array is not empty if (pIter != NULL && aoaResult != NULL) { // Calculate how many samples we will be outputting numAoaSamples = AOA_calcNumOfCteSamples(aoaControlBlock->aoaParams.cteTime, aoaControlBlock->aoaParams.cteScanOvs, aoaControlBlock->aoaParams.cteOffset); aoaResult->channel = channel; aoaResult->rssi = rssi; aoaResult->samplesLength = numAoaSamples; aoaResult->antenna = AOA_getActiveAnt(); aoaResult->offset = 0; do { // If the remainder is larger than buff size, tx maximum buff size if (aoaResult->samplesLength - aoaResult->offset > MAX_SAMPLES_SINGLE_CHUNK) { samplesToOutput = MAX_SAMPLES_SINGLE_CHUNK; } else { // If not, then output the remaining data samplesToOutput = aoaResult->samplesLength - aoaResult->offset; } // Copy the samples to output buffer for (int i = 0; i < samplesToOutput; i++) { aoaResult->samples[i] = pIter[i]; } RTLSHost_sendMsg(RTLS_CMD_AOA_RESULT_RAW, HOST_ASYNC_RSP, (uint8_t *)aoaResult, sizeof(rtlsAoaResultRaw_t) + (sizeof(AoA_IQSample) * samplesToOutput)); aoaResult->offset += samplesToOutput; pIter += MAX_SAMPLES_SINGLE_CHUNK; } while (aoaResult->offset < aoaResult->samplesLength); } if (aoaResult) { RTLSUTIL_FREE(aoaResult); } } break; default: break; } // Once all post processing is complete, set up the next run AOA_setupNextRun(); } } }rtls_ctrl_aoa.c:: RTLSCtrl_postProcessAoa
Replace the content of RTLSCtrl_estimateAngle in
rtls_ctrl_aoa.cwith the provided code snippet. What we have done here is to remove all the antenna array 2 dependency and modify the function call to take one array only when making an angle decision.//line 122 under local function section AoA_Sample RTLSCtrl_estimateAngle(const AoA_AntennaResult *antA1Result);// The function itself AoA_Sample RTLSCtrl_estimateAngle(const AoA_AntennaResult *antA1Result) { AoA_Sample AoA; static AoA_movingAverage AoA_ma; uint8_t AoA_ma_size = sizeof(AoA_ma.array) / sizeof(AoA_ma.array[0]); // Calculate AoA for each antenna array const int16_t AoA_A1 = ((antA1Result->pairAngle[0] + antA1Result->pairAngle[1]) / 2) + antA1Result->channelOffset[antA1Result->ch]; // Calculate average signal strength const int16_t signalStrength_A1 = (antA1Result->signalStrength[0] + antA1Result->signalStrength[1]) / 2; // Use AoA from Antenna Array A1 AoA_ma.array[AoA_ma.idx] = AoA_A1; AoA_ma.currentAoA = AoA_A1; AoA_ma.currentAntennaArray = 1; AoA_ma.currentRssi = antA1Result->rssi; AoA_ma.currentSignalStrength = signalStrength_A1; AoA_ma.currentCh = antA1Result->ch; AoA.currentangle = AoA_A1; // Add new AoA to moving average AoA_ma.array[AoA_ma.idx] = AoA_ma.currentAoA; // Calculate new moving average AoA_ma.AoAsum = 0; for(uint8_t i = 0; i < AoA_ma_size; i++) { AoA_ma.AoAsum += AoA_ma.array[i]; } AoA_ma.AoA = AoA_ma.AoAsum / AoA_ma_size; // Update moving average index if(AoA_ma.idx >= (AoA_ma_size - 1)) { AoA_ma.idx = 0; } else { AoA_ma.idx++; } // Return results AoA.angle = AoA_ma.AoA; AoA.rssi = AoA_ma.currentRssi; AoA.signalStrength = AoA_ma.currentSignalStrength; AoA.channel = AoA_ma.currentCh; AoA.antenna = AoA_ma.currentAntennaArray; return AoA; }rtls_ctrl_aoa.c::RTLSCtrl_estimateAngle
When complete, re-run the rtls_example.py, you will observe that all results show antenna 1. See below for example. Note the device address will be different for your launchpad.
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 34, "rssi": -60, "antenna": 1, "channel": 21}}
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 36, "rssi": -66, "antenna": 1, "channel": 4}}
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 36, "rssi": -62, "antenna": 1, "channel": 14}}
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 37, "rssi": -60, "antenna": 1, "channel": 24}}
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 37, "rssi": -64, "antenna": 1, "channel": 34}}
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 34, "rssi": -65, "antenna": 1, "channel": 7}}
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 35, "rssi": -60, "antenna": 1, "channel": 17}}
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 32, "rssi": -61, "antenna": 1, "channel": 27}}
PASSIVE: 54:6C:0E:A0:3B:F8 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_AOA_RESULT_ANGLE", "payload": {"angle": 32, "rssi": -67, "antenna": 1, "channel": 0}}
Task 3 – Modify AoA application to use only two antennas
Now we have successfully moved from using 2 antenna arrays to 1. We will use the example from Task 2 as the baseline and make the proper changes so we only use 2 antennas from the antenna array 1.
Task 2 is using all 3 antennas from
A1:
And the end goal is using the following:

The following steps will guide you through this process:
Comment out the following in ant_array1_config_boostxl_rev1v1.c.
//line 57 #define AOA_Ax_ANT3 AOA_PIN(IOID_30) //line 131~144 {// v23 .a = 1, .b = 2, .sign = 1, .offset = -5, .gain = 0.9, }, {// v13 .a = 0, .b = 2, .sign = 1, .offset = -20, .gain = 0.50, },Replace the antenna toggling patten
antennaPattern_A1in ant_array1_config_boostxl_rev1v1.c with the provided pattern to removeA1.3.AoA_Pattern antennaPattern_A1 = { .numPatterns = 32, .initialPattern = AOA_A1_SEL | AOA_Ax_ANT2, .toggles = { AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT1, // A1.1 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 stay with antenna 2 at the end AOA_A1_SEL | AOA_Ax_ANT2, // A1.2 } };Using different antennas instead of A1.1 and A1.2
Our example code always uses antenna A1.2 or A2.2 to receive packets before the CTE. This is set by the code below
.initialPattern = AOA_A1_SEL | AOA_Ax_ANT2,antennaPattern_A1[] :: ant_array1_config_boostxl_rev1v1.c
Therefore, for users using antennas other than A1.2 or A2.2, you will need to modify the initialPattern too.
Change the AOA_NUM_ANTENNAS to 2 in AOA.h
// line 58 #define AOA_NUM_ANTENNAS 2The IOs that control the antenna switches are initialized in AOA.c::AOA_openPins Therefore, we need to modify this part of the code since we are using one less IO. Change the pinMask to the following
//line 295 and line 321 pinMask = (1 << IOID_27 | 1 << IOID_28 | 1 << IOID_29 );
Fundamental done
Now the application only uses antenna A1.1 and A1.2! Very well done! And the updated software still works with RTLS_Monitor GUI.
Task 4 – AoA I/Q Samples
Python solution
The python solution for rtls_aoa in exercise 4 is provided at the end of this training.
So far we have worked on the application code to modify the
rtls_passive to work with custom HW. We will now spend some
time looking into the I/Q data.
The reason for checking I/Q data is that different HW design will yield different result which will affect your own angle calculation algorithm. For example, if the rf switches you are using need longer settling time than the ones on BOOSTXL-AoA, then you will need to discard more I/Q samples when doing calculations.
To decide how many I/Q samples you can use, you will need to extract I/Q samples and calculate phase to determine at which samples antennas are changing.
We will first take a look at where I/Q samples are stored and in what format.
I/Q is stored in the radio core RAM(RFC_RAM_BASE + 0x0000C000) and each I/Q pair takes up 32 bits space where I is stored in the lower 16 bits and Q is stored in higher 16 bits.
void AOA_getRxIQ(uint8_t *packetId, AoA_IQSample **samples)
{
// Should only be called when RF Core is awake
*packetId = (*((uint8_t*)AOA_PACKET_RAM));
*samples = packetId ? (AoA_IQSample *)&HWREG(RFC_RAM_BASE + 0x0000C000):NULL;
}
In AOA.c
I/Q samples resolution
I and Q samples only have 13 bits resolution even though they occupy 16 bits space in radio core RAM. Since they only have 13 bits resolution, the maximum and minimum value you will observe as signed integers are [4095, -4096].
To extract the I/Q data from embedded devices over UART, we need to make sure that we have enough time to flush the I/Q data before next AoA packet arrives.
To achieve this, there are two options:
Increase the connection interval. In the
rtls_masterreadme file, it's mentioned that the connection interval should be larger than 300ms to acommodate outputting all the samples.Increase the UART baurate to 2x, 4x or 8x 115200.
To achieve this, navigate to source/ti/blestack/npi/src/unified/npi_task.c::uint8_t NPITask_Params_init(uint8_t portType, NPI_Params *params) and add the following under the
#if defined(NPI_USE_UART)params->portParams.uartParams.baudrate = 4 * 115200;Change baudrate to 4x 115200
In this lab we will increase connection interval to 500ms and leave the baurate as is. The connection interval can be controlled by a parameter that is passed into the connect API.
Change the following parameter in the rtls_example.py to 400.
# connection interval in units of 1.25msec configured in connection request, default is 400 (500ms)
conn_interval = 400
This parameter will later be used in the connect API as shown below
master_node.rtls.connect(address_type, address, conn_interval)
Post Processing on Embedded Device
For users that will do I/Q data post process on chip, there is no limitation on the connection interval.
Then we will build on top of the python script that is used in Realtime Localization System Introduction
First we create a csv file to store I/Q data for analyzing later if needed.
You can change the filename to whichever suits you better.
import csv
from collections import namedtuple
# prepare csv file to save data
filename = 'sla_rtls_raw_IQ_toggles.csv'
outfile = open(filename, 'w', newline='')
csv_fieldnames = ['pkt', 'sample_idx', 'rssi', 'ant_array', 'channel', 'i', 'q']
csv_writer = csv.DictWriter(outfile, fieldnames=csv_fieldnames)
SampleRow = namedtuple('CsvRow', csv_fieldnames)
dump_rows = []
pkt_limit = 5
pkt_cnt = 0
csv_writer.writeheader()
Create a csv file to log I/Q data
We need to start the setup of AoA parameters and start AoA once the slave has connected to the master.
To extract I/Q data, the AoA operation mode needs to be AOA_MODE_RAW.
When using rtls_example.py, you need to modify the aoa_run_mode to the following:
aoa_run_mode = 'AOA_MODE_RAW'
Change the code of rtls_example.py to enable AOA and disable TOF.
tof_supported = False
aoa_supported = True
This is the mode that is used after a connection has been established.
if msg.command == 'RTLS_CMD_CONNECT' and msg.type == 'AsyncReq':
if msg.payload.status == 'RTLS_SUCCESS':
if aoa_supported:
# Find the role based on capabilities of sending node
role = 'AOA_MASTER' if sending_node.capabilities.get('RTLS_MASTER',
False) else 'AOA_PASSIVE'
# Send AoA params
sending_node.rtls.aoa_set_params(role,
aoa_run_mode,
aoa_cte_scan_ovs,
aoa_cte_offset,
aoa_cte_length)
else:
# If the connection failed, keep scanning
master_node.rtls.scan()
In order to process the RAW IQ samples, we need to first calculate how
many samples a connection event will produce. By migrating some of the code
from AOA.c to Python, we can calculate how many IQ samples will be produced
during a connection event for different oversampling rates.
# This the max number of samples that can be stored in the RF Core RAM
AOA_RES_MAX_SIZE = 511
# Note this is a direct port of AOA.c::AOA_calcNumOfCteSamples
def num_iqsamples_per_evt(cte_scan_ovs, cte_offset, cte_time):
samp_per_evt = (((cte_time * 8) - cte_offset) * cte_scan_ovs)
if samp_per_evt > AOA_RES_MAX_SIZE:
samp_per_evt = AOA_RES_MAX_SIZE
return samp_per_evt
When initializing AOA variables, we can set the number of samples per event
aoa_iq_samples_per_ce = num_iqsamples_per_evt(aoa_cte_scan_ovs, aoa_cte_offset, aoa_cte_time)
For a refresher on the AOA parameters used above and how CTE sampling works, refer to the BLE-Stack User's Guide, see RTLS Toolbox > Angle of Arrival chapter.
We have now calculated the number of IQ samples that are generated per event.
Because of the size of the IQ pairs, they are fragmented before being sent over
UART. The code below will re-assemble each connection event's IQ samples, sort
them and dump them to a CSV file. This will continue for a number of connection
events specified by pkt_limit (default = 5), but you can disable the limitation
by setting it to None.
# Saving I/Q samples into csv file
if msg.command == 'RTLS_CMD_AOA_RESULT_RAW':
payload = msg.payload
# Extract first sample index in this payload
offset = payload.offset
# If we have data, and offset is 0, we are done with one dump
if offset == 0 and len(dump_rows):
pkt_cnt += 1
# Make sure the samples are in order
dump_rows = sorted(dump_rows, key=lambda sample: sample.sample_idx)
# Write to file when we collected all IQ samples
if len(dump_rows) == aoa_iq_samples_per_ce:
for sample_row in dump_rows:
csv_writer.writerow(sample_row._asdict())
# Reset payload storage
dump_rows = []
# Stop script now if there was a limit configured
if pkt_limit is not None and pkt_cnt > pkt_limit:
break
# Save samples for writing when dump is complete
for sub_idx, sample in enumerate(payload.samples):
sample = SampleRow(pkt=pkt_cnt, sample_idx=offset + sub_idx, rssi=payload.rssi,
ant_array=payload.antenna, channel=payload.channel, i=sample.i, q=sample.q)
dump_rows.append(sample)
Extract I/Q data and write to a csv file
After running the script, you can see the following format in the csv file.

Here we are only interested in the payload part of the AOA_MODE_RAW command. The AOA_MODE_RAW payload has the following structure. In the python script provided above, we have extracted most of the information and save them to a csv file.
struct = Struct(
"rssi" / Int8sl,
"antenna" / Int8ul,
"channel" / Int8ul,
"offset" / Int16ul,
"samplesLength" / Int16ul,
"samples" / GreedyRange(Struct(
"q" / Int16sl,
"i" / Int16sl,
)),
)
New Python Packages
For the script below you will have to install the following new Python packages
- pandas
- matplotlib
They can be installed with pip. Consult the RTLS Intro lab for more information on how to install packages from pip.
import matplotlib.pyplot as plt
import pandas as pd
import math
# This the max number of samples that can be stored in the RF Core RAM
AOA_RES_MAX_SIZE = 511
# Note this is a direct port of AOA.c::AOA_calcNumOfCteSamples
def num_iqsamples_per_evt(cte_scan_ovs, cte_offset, cte_time):
samp_per_evt = (((cte_time * 8) - cte_offset) * cte_scan_ovs)
if samp_per_evt > AOA_RES_MAX_SIZE:
samp_per_evt = AOA_RES_MAX_SIZE
return samp_per_evt
def cal_magnitude(q_value,i_value):
return math.sqrt(math.pow(q_value,2)+math.pow(i_value,2))
def cal_phase(q_value,i_value):
return math.degrees(math.atan2(q_value,i_value))
if __name__ == "__main__":
df = pd.read_csv('sla_rtls_raw_IQ_toggles.csv')
aoa_cte_scan_ovs = 4
aoa_cte_offset = 4
aoa_cte_time = 20
aoa_iq_samples_per_ce = num_iqsamples_per_evt(aoa_cte_scan_ovs, aoa_cte_offset, aoa_cte_time)
df['phase'] = df.apply(lambda row: cal_phase(row['q'], row['i']), axis=1)
df['magnitude'] = df.apply(lambda row: cal_magnitude(row['q'], row['i']), axis=1)
# Plot all the I/Q collected. Each channel will have a plot which contains I/Q samples.
# If you have collected I/Q data on 37 data channels, then there will be 37 windows popped up
grouped = df.groupby('channel')
axes = grouped[['i', 'q']].plot(title="I/Q samples", grid=True)
# Create 4 plots and each plot has x number of subplots. x = number of channels
indexed = df.set_index(['channel', 'sample_idx'])
indexed.unstack(level=0)[['phase']].plot(subplots=True, title="Phase information", xlim=[0,aoa_iq_samples_per_ce], ylim=[-190,+190])
indexed.unstack(level=0)[['magnitude']].plot(subplots=True, title="Signal magnitude", xlim=[0,aoa_iq_samples_per_ce])
indexed.unstack(level=0)[['i']].plot(subplots=True, title="I samples", xlim=[0,aoa_iq_samples_per_ce])
indexed.unstack(level=0)[['q']].plot(subplots=True, title="Q samples", xlim=[0,aoa_iq_samples_per_ce])
plt.show()
This script can be executed as is.
After executing the small script provided above, if the angle between rtls_passive
rtls_slave is not 0 degree, you can see that magnitude of the sine wave is not the same
when you swap betwen antenna 1 and antenna 2 as shown below.

Another alternative to observe the magnitude of received signal is to calculate it based on I/Q data. Since each I/Q pair is a vector, the magnitude of a vector can be calculated by sqrt(I^2+Q^2).
Using the script provided above, you will see a plot called Signal magnitude. In this plot, you can clear see that received signal magnitude from antenna 2 is almost double as from antenna 1.

More information!
If there is no antenna toggling, then the I/Q samples will form a perfect sine wave. To prove it, change the antenna toggling pattern from 2 antennas to 1 antenna(A1.2), and see how the I/Q plots look like.
AoA_Pattern antennaPattern_A1 = {
.numPatterns = 32,
.initialPattern = AOA_A1_SEL | AOA_Ax_ANT2,
.toggles =
{
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
AOA_A1_SEL | AOA_Ax_ANT2, // A1.2
}
};
Task 5 – Antenna Switch Settling Time
Now that we are equipped with all the needed tools to manipulate I/Q data, we will try to identify when the antenna is toggling in order to help determine which I/Q samples can be used.
As mentioned in Calculate the phase difference among the antennas, I/Q samples can be presented with constellation diagram, each I/Q pair is a vector and its angle is within a range of +180 ~ -180. Therefore when we have an I/Q pair, to obtain the angle information, we can use arctan to get angle of a vector. Then based on the I and Q value, we can find out the corresponding angle.
| I > 0 | I < 0 | |
|---|---|---|
| Q >=0 | angle = arctan(Q/I) | angle = 180 + arctan(Q/I) |
| Q < 0 | angle = arctan(Q/I) | angle = -180 + arctan(Q/I) |
Luckily in python, the math.atan2 function takes care of the aforementioned logic.
Let's take a look at the Phase Information plot.

A perfect sine wave, assuming it starts at 0 degree, the phase will first go towards +180 and then, once it crosses +180, it will start from -180 and then go back to 0 degrees. From the picture provided below, you can see that in the first 8us(32 samples), there is no abrupt phase change.
When the rf switches and the angle of incident wave is not 0 degree, you will see abrupt phase change. As shown below after 2 periods, we started to switch antennas. Then the phase we obtained is no long linear.
Hence by looking at the phase information plot, you will know how many I/Q samples you can keep per period.

#
# Copyright (c) 2018-2019, Texas Instruments Incorporated
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# * Neither the name of Texas Instruments Incorporated nor the names of
# its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
import queue
import time
from rtls import RTLSManager, RTLSNode
import csv
from collections import namedtuple
# Un-comment the below to get raw serial transaction logs
# import logging, sys
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG,
# format='[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s')
# This the max number of samples that can be stored in the RF Core RAM
AOA_RES_MAX_SIZE = 511
# Note this is a direct port of AOA.c::AOA_calcNumOfCteSamples
def num_iqsamples_per_evt(cte_scan_ovs, cte_offset, cte_time):
samp_per_evt = (((cte_time * 8) - cte_offset) * cte_scan_ovs)
if samp_per_evt > AOA_RES_MAX_SIZE:
samp_per_evt = AOA_RES_MAX_SIZE
return samp_per_evt
if __name__ == '__main__':
# Initialize, but don't start RTLS Nodes to give to the RTLSManager
my_nodes = [RTLSNode('COM8', 115200), RTLSNode('COM81', 115200)]
# Initialize references to the connected devices
master_node = None
passive_nodes = []
# Initialize references to the connected devices
address = None
address_type = None
# ToF related settings
samples_per_burst = 256 # Should be dividable by 16. Hint: in a 100ms interval, there are about 300~ samples
tof_freq_list = [2408, 2412, 2418, 2424] #Other options: 2414, 2420
tof_num_freq = len(tof_freq_list)
auto_tof_rssi = -55
# Select tof_sample_mode TOF_MODE_DIST or TOF_MODE_RAW
tof_sample_mode = 'TOF_MODE_DIST'
tof_run_mode = 'TOF_MODE_CONT'
seed = 0
samplesPerFreq = 1000
calibDistance = 1 # 1 meter
constSyncwords = False
# AoA related settings
aoa_run_mode = 'AOA_MODE_RAW'
aoa_cte_scan_ovs = 4
aoa_cte_offset = 4
aoa_cte_time = 20
aoa_iq_samples_per_ce = num_iqsamples_per_evt(aoa_cte_scan_ovs, aoa_cte_offset, aoa_cte_time)
print("IQ samples per Connection event: " + str(aoa_iq_samples_per_ce))
# Auto detect AoA or ToF support related
tof_supported = False
aoa_supported = True
# If slave addr is None, the script will connect to the first RTLS slave
# that it found. If you wish to connect to a specific device
# (in the case of multiple RTLS slaves) then you may specify the address
# explicitly as given in the comment to the right
slave_addr = None # '54:6C:0E:83:3F:3D'
# Scan list
scanResultList = list()
# prepare csv file to save data
filename = 'sla_rtls_raw_IQ_toggles.csv'
outfile = open(filename, 'w', newline='')
csv_fieldnames = ['pkt', 'sample_idx', 'rssi', 'ant_array', 'channel', 'i', 'q']
csv_writer = csv.DictWriter(outfile, fieldnames=csv_fieldnames)
SampleRow = namedtuple('CsvRow', csv_fieldnames)
dump_rows = []
pkt_limit = 5
pkt_cnt = 0
csv_writer.writeheader()
# connection interval in units of 1.25msec configured in connection request, default is 80 (100ms)
conn_interval = 400
# Initialize manager reference, because on Exception we need to stop the manager to stop all the threads.
manager = None
try:
# Start an RTLSManager instance without WebSocket server enabled
manager = RTLSManager(my_nodes, websocket_port=None)
# Create a subscriber object for RTLSManager messages
subscriber = manager.create_subscriber()
# Tell the manager to automatically distribute connection parameters
manager.auto_params = True
# Start RTLS Node threads, Serial threads, and manager thread
manager.start()
# Wait until nodes have responded to automatic identify command and get reference
# to single master RTLSNode and list of passive RTLSNode instances
master_node, passive_nodes, failed = manager.wait_identified()
if len(failed):
print(f"ERROR: {len(failed)} nodes could not be identified. Are they programmed?")
# Exit if no master node exists
if not master_node:
raise RuntimeError("No RTLS Master node connected")
# Combined list for lookup
all_nodes = passive_nodes + [master_node]
# Initialize application variables on nodes
for node in all_nodes:
node.tof_initialized = False
node.seed_initialized = False
node.aoa_initialized = False
node.tof_calibration_configured = False
node.tof_calibrated = False
node.tof_started = False
#
# At this point the connected devices are initialized and ready
#
# Display list of connected devices and their capabilities
print(f"{master_node.identifier} {', '.join([cap for cap, available in master_node.capabilities.items() if available])}")
# Iterate over Passives and detect their capabilities
for pn in passive_nodes:
print(f"{pn.identifier} {', '.join([cap for cap, available in pn.capabilities.items() if available])}")
# Check over aggregated capabilities to see if they make sense
capabilities_per_node = [[cap for cap, avail in node.capabilities.items() if avail] for node in all_nodes]
tof_supported = all('TOF_PASSIVE' in node_caps or 'TOF_MASTER' in node_caps for node_caps in capabilities_per_node)
# Assume AoA if all nodes are not ToF
aoa_supported = all(not ('TOF_PASSIVE' in node_caps or 'TOF_MASTER' in node_caps) for node_caps in capabilities_per_node)
# Check that Nodes all must be either AoA or ToF
if not (tof_supported or aoa_supported):
raise RuntimeError("All nodes must be either AoA or ToF")
# Need at least 1 passive for AoA
if aoa_supported and len(passive_nodes) == 0:
raise RuntimeError('Need at least 1 passive for AoA')
# Send an example command to each of them, from commands listed at the bottom of rtls/ss_rtls.py
for n in all_nodes:
n.rtls.identify()
while True:
# Get messages from manager
try:
identifier, msg_pri, msg = subscriber.pend(block=True, timeout=0.05).as_tuple()
# Get reference to RTLSNode based on identifier in message
sending_node = manager[identifier]
if sending_node in passive_nodes:
print(f"PASSIVE: {identifier} --> {msg.as_json()}")
else:
print(f"MASTER: {identifier} --> {msg.as_json()}")
# If we received an error, print it.
if msg.command == 'RTLS_EVT_ERROR':
print(f"Received RTLS_EVT_ERROR with status: {msg.payload.status}")
# If we received an assert, print it.
if msg.command == 'RTLS_EVT_ASSERT' and msg.type == 'AsyncReq':
raise RuntimeError(f"Received HCI H/W Assert with code: {msg.payload.cause}")
# After identify is received, we start scanning
if msg.command == 'RTLS_CMD_IDENTIFY':
master_node.rtls.scan()
# Once we start scaning, we will save the address of the
# last scan response
if msg.command == 'RTLS_CMD_SCAN' and msg.type == 'AsyncReq':
# Slave address none means that we connect to any slave
if slave_addr is None:
address = msg.payload.addr
address_type = msg.payload.addrType
else:
scanResultList.append(msg.payload.addr)
scanResultList.append(msg.payload.addrType)
# Once the scan has stopped and we have a valid address, then
# connect
if msg.command == 'RTLS_CMD_SCAN_STOP':
if slave_addr is None:
if address is not None and address_type is not None:
master_node.rtls.connect(address_type, address, conn_interval)
elif slave_addr in scanResultList:
i = scanResultList.index(slave_addr)
master_node.rtls.connect(scanResultList[i + 1], scanResultList[i], conn_interval)
scanResultList.clear()
else:
# If we didn't find the device, keep scanning.
master_node.rtls.scan()
if msg.command == 'RTLS_CMD_CONNECT' and msg.type == 'AsyncReq':
if msg.payload.status == 'RTLS_SUCCESS':
if aoa_supported:
# Find the role based on capabilities of sending node
role = 'AOA_MASTER' if sending_node.capabilities.get('RTLS_MASTER',
False) else 'AOA_PASSIVE'
# Send AoA params
sending_node.rtls.aoa_set_params(role,
aoa_run_mode,
aoa_cte_scan_ovs,
aoa_cte_offset,
aoa_cte_time)
else:
# If the connection failed, keep scanning
master_node.rtls.scan()
# Count the number of nodes that have ToF initialized
if msg.command == 'RTLS_CMD_TOF_SET_PARAMS' and msg.payload.status == 'RTLS_SUCCESS':
sending_node.tof_initialized = True
if not passive_nodes:
if tof_sample_mode == 'TOF_MODE_DIST':
master_node.rtls.tof_calib(True, samplesPerFreq, calibDistance)
else:
# If we are not in TOF_MODE_DIST we can start immediately
master_node.rtls.tof_start(True)
master_node.tof_started = True
else:
# If all nodes have responded then we are ready to move on
if all([n.tof_initialized for n in all_nodes]):
# Send request for seed to master
master_node.rtls.tof_get_sec_seed()
if msg.command == 'RTLS_CMD_AOA_SET_PARAMS' and msg.payload.status == 'RTLS_SUCCESS':
sending_node.aoa_initialized = True
if all([n.aoa_initialized for n in all_nodes]):
# Start AoA on the master and passive nodes
for node in all_nodes:
node.rtls.aoa_start(True)
# Wait for security seed
if msg.command == 'RTLS_CMD_TOF_GET_SEC_SEED' and msg.payload.seed is not 0:
seed = msg.payload.seed
for node in passive_nodes:
node.rtls.tof_set_sec_seed(seed)
# Wait until passives have security seed set
if msg.command == 'RTLS_CMD_TOF_SET_SEC_SEED' and msg.payload.status == 'RTLS_SUCCESS':
sending_node.seed_initialized = True
# Only need to calibrate in distance mode
if tof_sample_mode == 'TOF_MODE_DIST':
if sending_node in passive_nodes:
sending_node.tof_calibration_configured = True
sending_node.rtls.tof_calib(True, samplesPerFreq, calibDistance)
# Once all passives have calibration configured, we can configure master
if all([n.tof_calibration_configured for n in passive_nodes]):
# Master will do infinite calibration until passive is done
master_node.rtls.tof_calib(True, 0, calibDistance)
else:
# If we are not in TOF_MODE_DIST we can start immediately
if sending_node in passive_nodes:
sending_node.rtls.tof_start(True)
sending_node.tof_started = True
# Passives must start well before Master does since they must "hear" the first ToF exchange
if all([n.tof_started for n in passive_nodes]):
master_node.rtls.tof_start(True)
master_node.tof_started = True
if msg.command == 'RTLS_CMD_TOF_CALIBRATE' and msg.type == 'SyncRsp':
if sending_node in passive_nodes:
sending_node.rtls.tof_start(True)
sending_node.tof_started = True
# Passives must start well before Master does since they must "hear" the first ToF exchange
if all([n.tof_started for n in passive_nodes]):
master_node.rtls.tof_start(True)
master_node.tof_started = True
else:
if not passive_nodes:
master_node.rtls.tof_start(True)
master_node.tof_started = True
if msg.command == 'RTLS_CMD_TOF_CALIBRATE' and msg.type == 'AsyncReq':
if sending_node in passive_nodes:
sending_node.tof_calibrated = True
# Once all nodes are calibrated we can stop master calibration
if all([n.tof_calibrated for n in passive_nodes]):
master_node.rtls.tof_calib(False, 0, calibDistance)
# Saving I/Q samples into csv file
if msg.command == 'RTLS_CMD_AOA_RESULT_RAW':
payload = msg.payload
# Extract first sample index in this payload
offset = payload.offset
# If we have data, and offset is 0, we are done with one dump
if offset == 0 and len(dump_rows):
pkt_cnt += 1
# Make sure the samples are in order
dump_rows = sorted(dump_rows, key=lambda sample: sample.sample_idx)
# Write to file when we collected all IQ samples
if len(dump_rows) == aoa_iq_samples_per_ce:
for sample_row in dump_rows:
csv_writer.writerow(sample_row._asdict())
# Reset payload storage
dump_rows = []
# Stop script now if there was a limit configured
if pkt_limit is not None and pkt_cnt > pkt_limit:
break
# Save samples for writing when dump is complete
for sub_idx, sample in enumerate(payload.samples):
sample = SampleRow(pkt=pkt_cnt, sample_idx=offset + sub_idx, rssi=payload.rssi,
ant_array=payload.antenna, channel=payload.channel, i=sample.i, q=sample.q)
dump_rows.append(sample)
except queue.Empty:
pass
finally:
outfile.flush()
outfile.close()
if manager:
manager.stop()
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
