Note

As of 2021, this document follows the Appropriate Language nomenclature directive from Bluetooth SIG. The SDK project names, APIs and code are not yet ported and use the old nomenclature.

For details on how to correlate between the two, please open the document Appropriate Language Mapping Tables.

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 3-4 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 Central role), rtls_slave (RTLS Peripheral role) and rtls_passive (RTLS Passive role) projects that are part of the SimpleLink™ CC13X2 / CC26X2 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

Hardware required

This module requires the following kits:

Getting started with AoA booster pack

These chapters in the TI BLE5-Stack User's Guide

  • TI BLE5-Stack Quick Start
  • The CC13x2 or CC26x2 SDK Platform
  • Application
  • BLE5-Stack (especially the sections dedicated to periodic advertising and periodic scanning)
  • RTLS Toolbox
  • Network Processor Interface (NPI)

Project readme files:

  • rtls_master Readme located in <SimpleLink CC13X2 / CC26X2 SDK> → examples → rtos → Board → ble5stack → rtls_master folder within the CC13X2 / CC26X2 SDK.
  • All relevant information to rtls_slave and rtls_passive is contained in the rtls_master readme
  • rtls_agent Readme located in <SimpleLink CC13X2 / CC26X2 SDK> → tools → ble5stack → rtls_agent folder within the CC13X2 / CC26X2 SDK. This will be covered in detail in Task 2.

Software for desktop development

Getting started – Desktop

At the end of this task you must have:

  • SimpleLink CC13X2-26X2 SDK installed
  • Working Python environment
  • Used rtls_example.py script that sets up an RTLS network.
  • A basic knowledge of the positioning techniques
  • Used the RTLS toolbox and the RTLS UI

Install the Software

  1. Run the SimpleLink CC13X2 / CC26X2 SDK installer.
  2. Install Python 3.7 from the Python Download page.
  3. Setup the Python environment as described in the README.md in <SimpleLink CC13X2 / CC26X2 SDK> → tools → ble5stack → rtls_agent folder.
  4. If a bash environment doesn't exist on your system, install Git bash

This gives you:

  • The SDK with TI-RTOS included at <SIMPLELINK_CC13X2_26X2_SDK_INSTALL_DIR> which defaults to C:\ti\simplelink_cc13x2_26x2_sdk_x_xx_xx_xx.
  • Python 3.7 environment with all dependencies required by the RTLS Node Manager

Load the software

  • Load Board #1 + BOOSTXL-AoA with rtls_passive project:
    <SimpleLink CC13X2 / CC26X2 SDK> → examples → rtos → CC26X2R1_LAUNCHXL → ble5stack → rtls_passive
  • Load Board #2 with rtls_slave project:
    <SimpleLink CC13X2 / CC26X2 SDK> → examples → rtos → CC26X2R1_LAUNCHXL → ble5stack → rtls_slave
  • Load Board #3 with rtls_master project:
    <SimpleLink CC13X2 / CC26X2 SDK> → examples → rtos → CC26X2R1_LAUNCHXL → ble5stack → rtls_master

Localization Techniques

A Real Time Localization System can be defined as a system capable of determining the position of a target within a defined physical area in real time. The physical area is normally defined through deployment of reference/locator nodes.

There are two fundamentally different approaches to location finding:

Trilateration, where you know the distance between a reference node and a target node. RSSI based techniques are typical examples of trilateration.

Triangulation, where you know the direction from a reference node to a target node. Angle of Arrival is a technique that can be used to measure the angle from the receiver to the transmitter.

What is a node?

A node in this case is referred to as a localization capable embedded device. For the demos in the SDK, nodes are LaunchPads

Quiz!

Select the localization techniques based on trilateration...

Select the localization techniques based on triangulation...

RTLS Toolbox Introduction

In the previous paragraph, we discussed how multiple AoA nodes can combine angle information to perform triangulation. It is important to remember in the pictures above, it is not possible for one single node to localize an object using the TI sample applications. A single AoA node only produces one angle. By nature this is an ambiguous measurement. If there are at least two nodes providing AoA data, then localization can occur. This requires a fourth device that is capable of combining the samples from the individual nodes and finding the intersection.

The intersection between the angles (AoA) is the estimated location of the device. An overview of the topology is shown below. In the diagram below, the black, blue, and red boxes represent CC26x2R LaunchPads while the grey box is a PC.

Note

In connectionless AoA there are no Central/Peripheral roles. There is the transmitter who sends the CTE over periodic advertisement packets and the receiver who synchronized with the advertiser and receives the CTE packets. The transmitter role is implemented by the device running the rtls_slave example. The receiver role is implemented by the device running the rtls_master example.

For a comprehensive presentation of the RTLS toolbox and its software components, please see the TI BLE5-Stack User's Guide

RTLS Roles and Topology

Each node in an RTLS network utilizes the software components listed above in a different role to perform a specific task related to localization. There are three examples: rtls_master, rtls_slave, and rtls_passive. The capabilities of these examples are explained below.

Connection AoA

RTLS Central

The RTLS Central runs a full BLE-Stack and acts as a BLE Central device. It will scan and connect to the RTLS Peripheral over BLE. Once a connection is established the RTLS Central will do the following:

  • Share the connection parameters (access address, Central sleep clock accuracy, and CRC init) with the PC.
  • Use the BLE link to share AoA parameters with the Peripheral device.
  • (Possibly) Receives packets with CTE and performs in-phase and quadrature component (IQ) sampling
  • The RTLS Central does not send out AoA packets, but configures the Peripheral to do so.

RTLS Peripheral

The RTLS Peripheral runs a full BLE-Stack and acts as a BLE Peripheral device. This is the device that is to be located. The Peripheral device will advertise and enter a connection with the RTLS Central.

  • Advertises special string to be detected by rtls_master (covered in detail in Task 3)
  • BLE-Stack Peripheral role
  • Sends data packets with AoA tone embedded using Constant Tone Extension (CTE). More information on CTE can be found in the Bluetooth 5.1 Core Spec
  • Wireless/battery operated, not connected to PC

RTLS Passive

The RTLS Passive does not actively participate in the BLE connection between the RTLS Central and Peripheral. Instead, it uses the Micro BLE-Stack in connection monitoring mode to follow the connection. To do this, the Passive device relies on the Central to distribute the connection parameters once a connection is formed. The Passive node does the following:

  • Receives packets with CTE and performs in-phase and quadrature component (IQ) sampling
  • Uses Micro BLE-Stack in Connection Monitoring mode to follow connection between Central and Peripheral

Note: The RTLS Passive can be used but is not necessary.

PC/Central Processing Node

The PC node is responsible for controlling the embedded RTLS nodes by sending commands and processing events. In the SDK, this is realized by a combination of a Python layer that implements the UNPI Central role and a server that translates UNPI commands to a socket interface that is used by the User Interface (UI) application running in the browser. In a final product, these algorithms may be implemented on an embedded device or even perhaps the RTLS Central node.

The PC implements the following functionality in the RTLS GUI:

  • Communicate via REST API's with RTLS_Util to issue commands and extract received RTLS Data from devices
  • Graphing and logging data
  • Enumerate devices
  • Distribute connection parameters to Passive nodes

Quiz!

Is it possible to have multiple RTLS Passive devices in a connection-AoA network?

Why would multiple Passive devices be desirable in a connection-AoA network? (select all that apply)

Connection-less AoA

RTLS Central (RTLS Receiver)

The RTLS Central runs a full BLE-Stack and acts as a BLE observer device. It will scan and synchronize to the RTLS Peripheral periodic advertisements. Once synchronized, the RTLS Central will receive packets with CTE and performs in-phase and quadrature component (IQ) sampling

RTLS Peripheral (RTLS Transmitter)

The RTLS Peripheral runs a full BLE-Stack and acts as a BLE broadcaster device. This is the device that is to be located. The Peripheral device will advertise periodic advertisements with a Constant Tone Extension (CTE) appended. More information on CTE can be found in the Bluetooth 5.1 Core Spec

PC/Central Processing Node

The PC node is responsible for controlling the embedded RTLS nodes by sending commands and processing events. In the SDK, this is realized by a combination of a Python layer that implements the UNPI Central role and a server that translates UNPI commands to a socket interface that is used by the User Interface (UI) application running in the browser. In a final product, these algorithms may be implemented on an embedded device or even perhaps the RTLS Central node.

The PC implements the following functionality in the RTLS GUI:

  • Communicate via REST API's with RTLS_Util to issue commands and extract received RTLS Data from devices
  • Graphing and logging data
  • Enumerate devices

Quiz!

How many RTLS Passive devices should be used in a connectionless-AoA network?

Running the RTLS Visual Demo

In this sub-part, we are going to use the UI (User Interface). This tool runs on a computer. You will find RTLS_UI in <SimpleLink CC13X2 / CC26X2 SDK> → tools → ble5stack → rtls_agent → rtls_ui folder.

  1. Apply any fixes from the RTLS known issues page on E2E

  2. Build the projects and flash the LaunchPads as described before

  3. Connect the Central and Passive devices to the computer. The Peripheral device must be powered but does not require to be connected to the computer.

  4. Execute the program rtls_ui (stored <SimpleLink CC13X2 / CC26X2 SDK> → tools → ble5stack → rtls_agent → rtls_ui)

  5. This will open your default web-browser and connect you to a local server. If your default browser is not supported (typically if your default web browser is IE), you will have to copy-paste the address of the server in a supported web browser.

    Review RTLS UI's welcome page, then click on "Get Started!"

  6. Select the feature to enable (Connection-AoA or Connectionless AoA). Only one feature should be selected. Once done, click on "Continue"

  7. The UI will then display the devices detected. The UI only detects the RTLS receivers (i.e. the Central and Passive devices for connection-AoA, and the Central device for connectionless-AoA). The UI will NOT display the Peripheral device(s) eventually connected to the computer.

  8. Select the launchpad(s) to use. Only the Central and (eventually) Passive devices have to be selected. In the case of connectionless-AoA, only one Central should be selected. Once done, click on "Continue"

  9. The system is ready! Click on "Auto Play" to start the demo:

  10. For connection-AoA, this will automatically:

    • launch a scan to detect the Peripheral device
    • connect the Peripheral device
    • enable continuous connection information monitoring
    • enable AOA measurement


    For connectionless-AoA, this will automatically:

    • launch a scan to find the periodic advertisements sent by the RTLS Peripheral
    • synchronize with the periodic advertisements
    • enable RSSI and TX Power monitoring
    • enable AOA measurement


    Here is the screen obtained for connection-AoA:


    Here is the screen obtained for connectionless-AoA:

    Some logs are stored by the RTLS UI in the folder rtls_ui\logs. These logs keep track of the UART messages by the RTLS UI and the nodes.

Running the RTLS Non-Visual Demo

In this sub-part we are going to use a Python script to directly interact with the nodes. This gives power to the developer to define custom behavior of the RTLS network and control the devices directly. Specifically, this will cover setting up the required Python dependencies and running the python scripts provided in the SDK. For connection-AoA, we recommend to first use the script rtls_example_with_rtls_util.py. For connection-AoA, use the script rtls_connectionless_aoa_example_with_rtls_util.py.

Assumptions and Notation

Before starting this task the following is assumed

  • A command prompt supporting bash or Git bash is open and running.
  • Unix style slashes will be used throughout. If it is necessary to run these steps in the Windows Command Prompt (cmd.exe), then / should be replaced with \.
  • Various command prompts will search your [System Path variable][1] to find Python. If you have a pre-existing Python version in your path this may be selected over the newly installed version. To prevent mixing the two up, we will use virtual environments.
  • We assume that Mac users don't have another instance of Python 3 installed. If this is not the case, then based on the PATH variable an older version of Python may be selected with invoking the python3 command. Be sure to invoke the correct version of Python.
  • Here we re-hash some of the instructions from the rtls_agent/readme.html. Some steps may be redundant if you already followed this and have the python environment setup.
  • Please have a look to the README file stored in tools\ble5stack\rtls_agent\rtls_util
  1. Install Python per steps in Getting started
  2. Open a command prompt (Git Bash is recommended)
  3. Create a Python virtual environment

    • Navigate to the SDK folder (e.g. C:\ti\simplelink_cc13x2_26x2_sdk_x_xx_xx_xx\tools\ble5stack\rtls_agent)
    • Execute py -3.7 -m venv .venv (windows) or python3 -m venv .venv (mac).
    • This will create a folder called .venv in the current directory that includes a copy of the python interpreter and a sandbox for installing packages.
    • Activate virtual environment using source .venv/Scripts/activate (bash) or .venv\Scripts\activate.bat (Windows cmd)
    • Observe that when a venv is activated (.venv) will appear before each cmd prompt
    • Notice that once the virtual environment is activated, the python command will use the local Python interpreter in the venv. See Virtual Environments for more info.
  4. Install RTLS packages

    This step will use package.bat (for Windows) or package.sh (for Linux / Mac) to install the RTLS packages.

    Workaround to deal with hard-coding in package install scripts

    Unfortunately the package install script hard-codes the python 3 location. This does not work if you have created a virtual environment as per the previous step. To fix this:

    • In Windows, make the following change (code on right is fixed version):
    • In Linux/Mac, make the following change (code on right is fixed version):
    • In Windows, run winpty ./package.bat -c -b -u -i (winpty must be omitted if you are using PowerShell or CMD terminal). In Linux / Mac run package.sh -c -b -u -i
  5. Install required external Python Dependencies into the newly created virtual environment

    • Execute python -m pip --proxy www.proxy.com install -r requirements.txt
    • Note that above --proxy www.proxy.com is only required if behind a proxy.
    • www.proxy.com is an example of a proxy. It should be replaced with the web address of your specific proxy if applicable.
    • This will install the required external Python packages that are needed by the RTLS Python suite (these are listed in requirements.txt).
  6. Open the python script (examples/rtls_example_with_rtls_util.py or examples/rtls_connectionless_aoa_example_with_rtls_util.py, find and update the following lines.

    devices = [
      {"com_port": "COM37", "baud_rate": 460800, "name": "CC26x2 Central"},
      {"com_port": "COM29", "baud_rate": 460800, "name": "CC26x2 Passive"},
    

    Make sure to list all the Central and Passive LaunchPads used an to update properly the COM ports used. It is not required to add the Peripheral device to this list.

    Notes

    • For connectionless-AoA, only one RTLS receiver shoul be used
    • The "name" field above does not affect the functionality. It is simply used for logging purposes and therefore it is not required to modify this if not desired.
  7. Save the file and run.

    • For connection-AoA, run the command python -u examples/rtls_example_with_rtls_util.py

      • The script will scan for RTLS devices, connect, and then print continuous connection information (RSSI, channel) for 15 seconds.
      • See below for sample output snippet (note that the addresses and COM ports will be different).
      • The out-of-box demo only reports continuous connection information (CCI). The following sections will help you to leverage all the capabilities of the provided scripts.
       Master : <RTLSNode(CC26x2 Central, started 22068)>
       Passives : [<RTLSNode(CC26x2 Passive, started 23740)>]
       All : [<RTLSNode(CC26x2 Passive, started 23740)>, <RTLSNode(CC26x2 Central, started 22068)>]
       Devices Reset
       Start scan for 15 sec
       Scan Results: [{'addr': '80:6F:B0:1E:3A:8B', 'addrType': 0, 'rssi': -61}]
       Connection Success
       CCI Callback Set
       [08:10:2020 13:59:38:441627] : {"name": "CC26x2 Passive", "type": "conn_info", "identifier": "80:6F:B0:1E:37:14", "payload": {"rssi": -71, "channel": 13}}
       [08:10:2020 13:59:38:443650] : {"name": "CC26x2 Central", "type": "conn_info", "identifier": "80:6F:B0:1E:3F:06", "payload": {"rssi": -51, "channel": 2}}
       CCI Started
       Going to sleep for 5 sec
       [08:10:2020 13:59:38:544616] : {"name": "CC26x2 Passive", "type": "conn_info", "identifier": "80:6F:B0:1E:37:14", "payload": {"rssi": -72, "channel": 24}}
       [08:10:2020 13:59:38:544616] : {"name": "CC26x2 Central", "type": "conn_info", "identifier": "80:6F:B0:1E:3F:06", "payload": {"rssi": -51, "channel": 13}}
       [08:10:2020 13:59:38:644620] : {"name": "CC26x2 Passive", "type": "conn_info", "identifier": "80:6F:B0:1E:37:14", "payload": {"rssi": -75, "channel": 35}}
       [08:10:2020 13:59:38:644620] : {"name": "CC26x2 Central", "type": "conn_info", "identifier": "80:6F:B0:1E:3F:06", "payload": {"rssi": -50, "channel": 24}}
       [08:10:2020 13:59:38:746616] : {"name": "CC26x2 Passive", "type": "conn_info", "identifier": "80:6F:B0:1E:37:14", "payload": {"rssi": -74, "channel": 9}}
       [08:10:2020 13:59:38:746616] : {"name": "CC26x2 Central", "type": "conn_info", "identifier": "80:6F:B0:1E:3F:06", "payload": {"rssi": -49, "channel": 35}}
       [08:10:2020 13:59:38:845621] : {"name": "CC26x2 Passive", "type": "conn_info", "identifier": "80:6F:B0:1E:37:14", "payload": {"rssi": -70, "channel": 20}}
      
       ...
      
       [08:10:2020 13:59:43:257107] : {"name": "CC26x2 Passive", "type": "conn_info", "identifier": "80:6F:B0:1E:37:14", "payload": {"rssi": -71, "channel": 23}}
       [08:10:2020 13:59:43:259117] : {"name": "CC26x2 Central", "type": "conn_info", "identifier": "80:6F:B0:1E:3F:06", "payload": {"rssi": -49, "channel": 12}}
       [08:10:2020 13:59:43:355919] : {"name": "CC26x2 Passive", "type": "conn_info", "identifier": "80:6F:B0:1E:37:14", "payload": {"rssi": -76, "channel": 34}}
       [08:10:2020 13:59:43:359918] : {"name": "CC26x2 Central", "type": "conn_info", "identifier": "80:6F:B0:1E:3F:06", "payload": {"rssi": -49, "channel": 23}}
       [08:10:2020 13:59:43:458910] : {"name": "CC26x2 Passive", "type": "conn_info", "identifier": "80:6F:B0:1E:37:14", "payload": {"rssi": -74, "channel": 8}}
       [08:10:2020 13:59:43:461956] : {"name": "CC26x2 Central", "type": "conn_info", "identifier": "80:6F:B0:1E:3F:06", "payload": {"rssi": -49, "channel": 34}}
       Try to stop CCI result parsing thread
       STOP Command Received
       CCI Stopped
       Master Disconnected
       Done
      

      Log obtained for connection-AoA

      The python script also creates the folder rtls_example_with_rtls_util_log (in the examples folder). The logs stored in this folder keep track of the UART messages exchange by the python script with the nodes.

    • For connectionless-AoA, run the command python -u examples/rtls_connectionless_aoa_example_with_rtls_util.py

      • The script will scan for the periodic advertisements of an RTLS device and synchronize with them.
      • After this it enables connectionless-AoA reception and prints RTLS information for 15 seconds.
      • See below for sample output snippet (note that the addresses and COM ports will be different).
       Master : <RTLSNode(CC26x2 Central, started 16888)>
       Passives : []
       All : [<RTLSNode(CC26x2 Central, started 16888)>]
       Devices Reset
       Periodic Advertise Callback Set
       Connectionless AOA Callback Set
       Scan results found: [{'addr': '80:6F:B0:1E:55:F7', 'addrType': 0, 'rssi': -58, 'advSID': 255, 'periodicAdvInt': 0}, {'addr': '80:6F:B0:1E:55:F7', 'addrType': 0, 'rssi': -63, 'advSID': 1, 'periodicAdvInt': 80}]
       Filtering scan results for periodic advertisers
       Using advertisers list to create sync
       slave {'addr': '80:6F:B0:1E:55:F7', 'addrType': 0, 'rssi': -63, 'advSID': 1, 'periodicAdvInt': 80} has been added to advertisers list
       [01:06:2021 11:09:29:517241] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -61, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:29:619238] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -61, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:29:874271] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -63, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:30:025459] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -64, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:30:126719] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -61, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:30:179694] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -61, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:30:480957] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -60, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
      
       ...
      
       Sync created with first slave on advertisers list: {'addr': '80:6F:B0:1E:55:F7', 'addrType': 0, 'rssi': -63, 'advSID': 1, 'periodicAdvInt': 80}. Periodic advertise report enabled automatically
       [01:06:2021 11:09:43:133728] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -61, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       Connectionless AOA started
       [01:06:2021 11:09:43:233755] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -45, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:43:294923] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -45, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:43:295888] : {"name": "CC26x2 Central", "type": "RTLS_CMD_CL_AOA_RESULT_RAW", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"syncHandle": 0, "rssi": -46, "antenna": 1, "channel": 1, "offset": 128, "samplesLength": 624, "samples": [{"q": -53, "i": -492}, {"q": 133, "i": -435}, {"q": 270, "i": -198}, {"q": 263, "i": 130}, {"q": 122, "i": 443}, {"q": -143, "i": 644}, {"q": -402, "i": 603}, {"q": -600, "i": 391}, {"q": -689, "i": 56}, {"q": -651, "i": -236}, {"q": -509, "i": -472}, {"q": -256, "i": -637}, {"q": 12, "i": -686}, {"q": 278, "i": -631}, {"q": 513, "i": -449}, {"q": 627, "i": -210}, {"q": 645, "i": 63}, {"q": 582, "i": 351}, {"q": 498, "i": 547}, {"q": 391, "i": 680}, {"q": 264, "i": 727}, {"q": 80, "i": 721}, {"q": -187, "i": 655}, {"q": -432, "i": 529}, {"q": -618, "i": 334}, {"q": -712, "i": 48}, {"q": -669, "i": -227}, {"q": -522, "i": -473}, {"q": -263, "i": -665}, {"q": 18, "i": -719}, {"q": 294, "i": -654}, {"q": 536, "i": -448}]}}
      
       ...
      
       [01:06:2021 11:09:48:697894] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -46, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:48:754894] : {"name": "CC26x2 Central", "type": "RTLS_CMD_CL_AOA_RESULT_RAW", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"syncHandle": 0, "rssi": -45, "antenna": 1, "channel": 6, "offset": 160, "samplesLength": 624, "samples": [{"q": 196, "i": 651}, {"q": 14, "i": 691}, {"q": -109, "i": 647}, {"q": -193, "i": 529}, {"q": -260, "i": 390}, {"q": -344, "i": 220}, {"q": -404, "i": 61}, {"q": -413, "i": -99}, {"q": -339, "i": -267}, {"q": -203, "i": -379}, {"q": -9, "i": -436}, {"q": 162, "i": -408}, {"q": 305, "i": -313}, {"q": 416, "i": -143}, {"q": 453, "i": 51}, {"q": 410, "i": 243}, {"q": 237, "i": 365}, {"q": -1, "i": 331}, {"q": -281, "i": 100}, {"q": -436, "i": -200}, {"q": -426, "i": -488}, {"q": -215, "i": -685}, {"q": 71, "i": -711}, {"q": 390, "i": -591}, {"q": 597, "i": -390}, {"q": 696, "i": -134}, {"q": 672, "i": 180}, {"q": 536, "i": 438}, {"q": 316, "i": 626}, {"q": 12, "i": 706}, {"q": -257, "i": 637}, {"q": -511, "i": 438}]}}
       [01:06:2021 11:09:48:787894] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -45, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:48:863896] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -45, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:48:873894] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -46, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:48:884895] : {"name": "CC26x2 Central", "type": "padv_event", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"opcode": 15, "syncHandle": 0, "txPower": 0, "rssi": -45, "cteType": 0, "dataStatus": 0, "dataLen": 11, "data": "50:65:72:69:6F:64:69:63:41:64:76"}}
       [01:06:2021 11:09:48:887905] : {"name": "CC26x2 Central", "type": "RTLS_CMD_CL_AOA_RESULT_RAW", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"syncHandle": 0, "rssi": -45, "antenna": 1, "channel": 6, "offset": 192, "samplesLength": 624, "samples": [{"q": -669, "i": 214}, {"q": -761, "i": 16}, {"q": -786, "i": -149}, {"q": -728, "i": -265}, {"q": -569, "i": -409}, {"q": -361, "i": -546}, {"q": -109, "i": -651}, {"q": 185, "i": -660}, {"q": 420, "i": -551}, {"q": 594, "i": -344}, {"q": 678, "i": -42}, {"q": 640, "i": 234}, {"q": 470, "i": 494}, {"q": 236, "i": 635}, {"q": -36, "i": 667}, {"q": -331, "i": 578}, {"q": -529, "i": 427}, {"q": -643, "i": 235}, {"q": -636, "i": 80}, {"q": -553, "i": -54}, {"q": -412, "i": -194}, {"q": -269, "i": -310}, {"q": -118, "i": -398}, {"q": 59, "i": -435}, {"q": 214, "i": -390}, {"q": 353, "i": -261}, {"q": 425, "i": -103}, {"q": 434, "i": 67}, {"q": 366, "i": 244}, {"q": 242, "i": 371}, {"q": 44, "i": 450}, {"q": -146, "i": 436}]}}
       Connectionless AOA stopped
       Sync terminated for sync handle: 0
       Try to stop Periodic Advertise event parsing thread
       STOP Command Received
       Try to stop Connectionless AOA results parsing thread
       [01:06:2021 11:09:48:974897] : {"name": "CC26x2 Central", "type": "RTLS_CMD_CL_AOA_RESULT_RAW", "identifier": "80:6F:B0:1E:3D:B4", "payload": {"syncHandle": 0, "rssi": -45, "antenna": 1, "channel": 6, "offset": 288, "samplesLength": 624, "samples": [{"q": 586, "i": -400}, {"q": 719, "i": -243}, {"q": 782, "i": -90}, {"q": 765, "i": 46}, {"q": 668, "i": 230}, {"q": 515, "i": 420}, {"q": 304, "i": 589}, {"q": 19, "i": 688}, {"q": -246, "i": 652}, {"q": -476, "i": 508}, {"q": -653, "i": 253}, {"q": -702, "i": -19}, {"q": -623, "i": -314}, {"q": -452, "i": -524}, {"q": -207, "i": -646}, {"q": 99, "i": -663}, {"q": 338, "i": -597}, {"q": 504, "i": -468}, {"q": 550, "i": -334}, {"q": 526, "i": -188}, {"q": 457, "i": -8}, {"q": 379, "i": 156}, {"q": 279, "i": 300}, {"q": 125, "i": 403}, {"q": -45, "i": 419}, {"q": -237, "i": 351}, {"q": -366, "i": 229}, {"q": -433, "i": 73}, {"q": -428, "i": -113}, {"q": -353, "i": -273}, {"q": -201, "i": -423}, {"q": -20, "i": -485}]}}
      
       ...
      
       [01:06:2021 11:10:14:535918] : Added new set of IQ into .\rtls_connectionless_aoa_example_with_rtls_util_log\rtls_raw_iq_samples_806fb01e3db4_0.csv
       STOP Command Received
       Done
      

      Log obtained for connectionless-AoA

      The python script also creates the folder rtls_connectionless_aoa_example_with_rtls_util_log (in the examples folder). The logs stored in this folder keep track of the UART messages exchange by the python script with the nodes. This folder also contains the raw IQ samples received from the rtls_master.

Why is it recommended to create a virtual Python environment (select all that apply)?

Building Custom RTLS Python Scripts

With the environment setup, it is time for us to use Python directly to control the RTLS nodes. The goal of this task is to explain the RTLS PC software and to walk through setting up a RTLS network.

You might want to revision or make a copy of the default rtls_example_with_rtls_util.py so it is preserved. Save it with another name like rtls_example_old.py as a backup.

RTLS Node Manager Python Overview

First, we will briefly discuss the important layers of the Python solution and their role.

/rtls_agent
    /examples/
      rtls_example_with_rtls_util.py - Example to exercise rtls_util functionality
      rtls_aoa_iq_with_rtls_util_export_into_csv.py - Example to store the IQ data (or AOA) to a file on your computer
      rtls_aoa_multi_conn_example.py - Example to exercice the multi-connections capability of the RTLS devices
    /rtls_util/
         Main interface for examples. Class that abstracts RTLSManager and RTLSNode
         functionality. Handles waiting for RTLS responses in order to provide synchronous
         API's.  Raises any unexpected functionality as an exception.
         Please review the README file for details.

    /rtls/
        /rtls/
            rtlsmanager.py - Class to manage multiple nodes in an RTLS network.
                             Subscribes to incoming data from the nodes, routes
                             outgoing data to each of the nodes. Distributes
                             connection parameters from Central node to any
                             connected Passive nodes when an connection is
                             established. Handles messages from rtls_agent_cli server
                             if one is provided.

            rtlsnode.py -    Class that implements the basic functionality of a node
                             in an RTLS network. This class will query the embedded
                             device connected to it and determine its capabilities.
                             Essentially this assigns a role in an RTLS context to
                             a COM port.

            ss_rtls.py       Defines the commands in the RTLS UNPI subsystem.
                             This file will define builder classes for the various
                             UNPI commands that the RTLS subsystem supports.

      /unpi/
          serialnode.py - Thread that manages serial communication from COM ports.
                          to higher layers.
                          Queues up messages and sends them to parser.
          unpiparser.py - Parser for Unified Network Processor Interface messages.
                          Implements UNPI frame format packing/unpacking.

RtlsUtil Summary

It is recommended to build RTLS based Python applications on top of the RtlsUtil class within rtls_util.py. This class forms the RTLS API set. A call to an RtlsUtil method translates to a sequence of one or more RTLS UNPI commands / responses from ss_rtls.py. In this way, RtlsUtil is an abstraction of both the RTLS UNPI communication as well as the RTLSManager which manages the various RTLSNode's.

Any errors in an RtlsUtil method will be reported as an exception raised to the RtlsUtil consumer (i.e. rtls_example_with_rtls_util.py). Alternatively, if the method returns, the functionality has been performed successfully.

Asynchronous localization data is received and stored into one of the following queues depending on the data:

  • RtlsUtil.aoa_results_queue (for connection-AoA results)
  • RtlsUtil.conn_info_queue (for connection monitor results)
  • RtlsUtil.cl_aoa_results_queue (for connectionless-AoA results)
  • RtlsUtil.padv_event_queue (for connectionless RSSI monitor results)

RTLS Python Program Template

rtls_example_with_rtls_util.py and rtls_connectionless_aoa_example_with_rtls_util.py from the previous task show how to perform basic initialization of RtlsUtil as well as setting up the networking and collecting localization data.

For connection-AoA, the beginning of main() has several boolean variables to enable / disable example functionality. It is possible to combine any of these modes together.

For connectionless-AoA a few variables help configuring the system Among others, the following variables are defined:

  • cl_aoa: flag to enable / disable connectionless-AoA
  • padv_report: flag to enable / disable periodic advertisement reports to be printed
  • scan_time_sec: variable to set the scanning time
  • num_of_scan_retry: variable to set the number of scan retries
  • use_adv_list: flag to select is the advertisement list will be used to synchronize with devices

Out-of-box Functionality

For connection-AoA, the out-of-box functionality, as demonstrated above, only has continuous connection information enabled. AOA can be enabled too by setting the aoa variable to True

Common Initialization

There is initialization functionality that is common to all modes. First, construct an instance of the RTLSUtil class to serve as the RTLS Node Manager interface. The first parameter is the file to log debug information to and the second parameter is the logging level.

rtlsUtil = RtlsUtil(logging_file, RtlsUtilLoggingLevel.UTIL_ALL)

Then set the timeout property to specify how long (in seconds) to wait for a response to a synchronous rtls command. A RtlsUtilTimeoutException will be raised if no response is received in this time.

rtlsUtil.timeout = 30

Next, create a dictionary of devices and pass this to RTLS.set_devices() which will create RTLSNode's for each device and an RTLSManager class using these nodes.

devices = [
  {"com_port": "COM32", "baud_rate": 460800, "name": "CC26x2 Central"},
  {"com_port": "COM26", "baud_rate": 460800, "name": "CC26x2 Passive"},
]

Note

The "name" field above does not affect the functionality. It is simply used for logging purposes and therefore it is not required to modify this if not desired.

RTLS.set_devices() will send RTLS_CMD_IDENTIFY to each node to identify it's capabilities and set the relevant Central / Passive(s).

Next, reset both nodes:

rtlsUtil.reset_devices()

The procedures above will appear, from a UNPI perspective, as the following:

RTLS Network Setup Procedure

At this point you should have a basic understanding of RTLS classes. Next we will cover the minimum commands required to setup an RTLS network. This procedure is slightly different for connection-AoA and connection-less AoA.

RTLS Network Setup Procedure for connection-AoA

For connection-AoA a BLE connection is a prerequisite for performing localization. The sequence diagram below shows the UNPI commands required to establish a connection between rtls_master and rtls_slave.

As covered in the BLE connections lab, before connecting, a scan must be performed to see if the desired device is nearby. This is initiated by RtlsUtil.scan(). The scanning device will inspect the advertisement and optionally the scan response data to determine if it wishes to connect to a given advertiser. Usually the scanner is looking for a given token or string in the broadcast data of the advertiser. The RTLS Central will look for the string {'R','T','L','S','S','l','a','v','e'} starting at the 3rd byte of the Peripheral's advertisement data. If the advertising device matches the filter, then it will be reported to the PC/Node Manager as RTLS_CMD_SCAN responses which are returned from RtlsUtil.scan() as a list of devices. If the advertising device does not match the filter, it will be discarded.

RtlsUtil.ble_connect() can be used to form a connection to one of the devices in the scan results. This will inform rtls_master to form a connection by issuing an RTLS_CMD_CONNECT along with the peer device's address and address type. The address information can be extracted from the RTLS_CMD_SCAN responses coming from the Central node.

If the connection is successful, the RTLS_CMD_CONNECT response will be received with status of RTLS_SUCCESS and RtlsUtil.ble_connect() will return. The RTLS examples do not consider a connection to be established between Central and Peripheral until the devices have paired and formed an L2CAP Connection Oriented Channel (CoC). The L2CAP CoC is used to send RTLS sync related information between Central and Peripheral. This can include AoA parameters or a command to enable AoA.

Immediately after the BLE connection is established (i.e. GAP_LINK_ESTABLISHED_EVENT received from the stack), the rtls_master will share the connection parameters with the PC/Node Manager via RTLS_CMD_CONN_PARAMS. This information is needed by the connection monitor inside rtls_passive in order to follow the connection between RTLS Central and Peripheral.

Distributing Connection Parameters

The RTLSManager Python class will immediately relay any connection parameters received (RTLS_CMD_CONN_PARAMS) to all of the Passive nodes connected. This does not need to be done manually.

RTLS Passive Connection Lost

As its name suggests, the Passive device does not have an active role in the BLE connection. The Passive has only the capability to listen the link but is not able to require a retransmission if a packet is not properly received. As a result, and especially in noisy environments, the Passive can lose track of the connection. In that case, the Passive device needs to re-receive the (up to date) connection info to be able to listen the BLE link. This mechanism is not implemented in the out-of-the-box examples and you will be required to restart the demo if it happens.

RTLS Network Setup Procedure for connectionless-AoA

For connectionless-AoA the RTLS_master (receiver) has to synchronize with the periodic advertisements of the RTLS_slave (transmitter). The sequence diagram below shows the UNPI commands required to synchronize the rtls_master and rtls_slave.

The way receiver can synchronize with a periodic advertiser sender is detailed in the Generic Access Profile (GAP) section of the TI BLE5-Stack User's Guide. A scan must first be performed in order to identify the devices sending periodic advertisements nearby. After this, the advertiser(s) to synchronize with is(are) selected. The periodic advertisers found can be filtered based on the type of CTE sent. Once synchronized with the periodic advertiser, the receiver can start sampling the CTE appended to the periodic advertisement packets and extract AoA information.

Setting up RTLS Network in Python

Now that we understand the basics behind the RTLS networks and how to set them up, let's review how the rtls_example_with_rtls_util.py and rtls_connectionless_aoa_example_with_rtls_util.py sample apps set up the RTLS networks.

Note that the scripts will do some additional processing after the network is setup. This is will be covered later in this lab.

Setting up RTLS Network in Python for connection-AoA

The commands required to setup a network belong to the RTLS UNPI subsystem and can be found in the ss_rtls.py file. The sending and receiving of these commands is abstracted through the RtlsUtil class.

Scanning for Devices

We will use the rtls_example_with_rtls_util.py as a starting point. From the sections above, we know that after the nodes are identified, we want to tell the Central to scan. This is initiated as such:

  scan_results = rtlsUtil.scan(scan_time_sec)

Alternatively, it is possible to only scan for a specific device address by passing a second "address" parameter as such:

  scan_results = rtlsUtil.scan(scan_time_sec, slave_bd_addr)

After the scan completes (runs for scan_time_sec), the results are returned in scan_results as a list of the following dictionaries:

{
    "addr" : 6 byte address as string,
    "addrType" : address type as int,
    "rssi": int,
    "advSID": int,
    "periodicAdvInt": int (0 means this advertisement is not a periodic advertisement)
}

Asynchronous vs Synchronous commands in UNPI

The following provides more information about the RTLS UNPI commands. As mentioned above, all of this is abstracted through RtlsUtil so can be skipped if desired.

You might have noticed that RTLS_CMD_SCAN is used to tell the node to start scanning, receive status, and receive scan results. This is possible within UNPI because each message can be one of the following types

  • Synchronous request
  • Synchronous response
  • Asynchronous request

In the case of RTLS_CMD_SCAN the message that initiates the scan on the rtls_master is a synchronous request. The message that returns the status of the scan start call is a synchronous response, and the message that returns scan results is an asynchronous request. See the NPI chapter in the TI BLE5-Stack User's Guide for more information.

Connecting to a Device

Now, we have collected a list of scan results and are ready to connect. It is required to specify an address to connect to. The address can be hard-coded:

  slave_bd_addr = "80:6F:B0:1E:38:C3"
  rtlsUtil.ble_connect(slave_bd_addr, connect_interval_mSec)

If you don't know the address, you can read it from the UART display of the Peripheral device. Open a Serial terminal (like putty or Tera Term) on the user/UART port of the rtls_slave LaunchPad. Use 115200 baud, 8N1. It should show the following text:

Initialized
Dev Addr: 0x806FB01E3A8B
Advertising

Alternatively, the address can be extracted from the scan results as such:

  rtlsUtil.ble_connect(scan_results[0], connect_interval_mSec)

Connection Interval

It is also necessary to pass a connection interval into RtlsUtil.ble_connect(). The ramifications of this parameter will be discussed in the various RTLS mode documentation sections where relevant. The out-of-box example uses 100 milliseconds by default.

Remember, the rtls_master will automatically send the connection parameters once a BLE connection is formed with the rtls_slave. The RTLSManager python class will intercept this and distribute it to all rtls_passive nodes so we don't have to do this in our program. Upon receiving the connection parameters, the rtls_passive connection monitor will begin following the connection between Central and Peripheral. Note that it may take some time to establish a connection as this does include LE Secure Connections pairing as well as opening an L2CAP Connection Oriented Channel.

Setting up RTLS Network in Python for connectionless-AoA

As for connection-AoA, the commands required to setup a network belong to the RTLS UNPI subsystem and can be found in the ss_rtls.py file. The sending and receiving of these commands is abstracted through the RtlsUtil class.

Scanning for Devices (connectionless-AoA)

Synchronize with the devices in the Periodic Advertiser List

When one wants to synchronize with the devices in the Periodic Advertiser List, there is no need to perform this initial scan and/or filter the advertiser list. In other words, you could directly refer to the section Create the synchronization with a periodic advertiser. However, we recommend you to go through all the steps in order to better understanding our offering.

We will use the rtls_connectionless_aoa_example_with_rtls_util.py as a starting point. From the sections above, we know that after the nodes are identified, we want to tell the receiver (rtls_master) to scan. This is initiated as such:

  scan_results = rtlsUtil.scan(scan_time_sec)

Alternatively, it is possible to only scan for a specific device address and advertising set ID (SID).

  scan_results = rtlsUtil.scan(scan_time_sec, advertiser_addr, advertiser_advSID)

After the scan completes (runs for scan_time_sec), the results are returned in scan_results as a list of the following dictionaries:

{
    "addr" : 6 byte address as string,
    "addrType" : address type as int,
    "rssi": int,
    "advSID": int,
    "periodicAdvInt": int (0 means this advertisement is not a periodic advertisement)
}

If you look at the content of scan_results, you will see that the rtls_slave device is reported twice. The device is reported once as a periodic advertiser (periodicAdvInt value is not 0), once a legacy advertiser (periodicAdvInt value is 0). This is because the out-of-the-box rtls_slave project is configured to send out several advertisement sets. The legacy advertisement is used to establish the connection when using connection-AoA. For connectionless-AoA the legacy should be filter out.

Scan results found: [{'addr': '80:6F:B0:1E:55:F7', 'addrType': 0, 'rssi': -58, 'advSID': 255, 'periodicAdvInt': 0}, {'addr': '80:6F:B0:1E:55:F7', 'addrType': 0, 'rssi': -63, 'advSID': 1, 'periodicAdvInt': 80}]

Extract of the log displayed by the python script. Two advertisements different are detected for device 80:6F:B0:1E:55:F7. The first one is a legacy advertisement, the second one is a periodic advertisement we can synchronize with.

Select a periodic advertiser to synchronize with

Now, we have collected a list of scan results. We are only interested in periodic advertisements. The other ones can be filtered out. The function filter_scan_results defined in rtls_connectionless_aoa_example_with_rtls_util.py can be used to do so.

  advertiser_list = filter_scan_results(scan_results)

In addition we could filter the advertisements based on the address of the advertisers.

  advertiserAddressToKeep = "80:6F:B0:1E:55:F7"
  advertiser_list = filter_scan_results(scan_results, advertiserAddressToKeep)

Add the advertiser to the Periodic Advertiser List

    advertiser = advertiser_list[0]
    rtlsUtil.padv_add_device_to_periodic_adv_list(advertiser)

The advertiser passed to the function padv_add_device_to_periodic_adv_list should be a periodic advertiser.

Other python procedures that may be useful:

    periodicAdvLisSize = rtlsUtil.padv_read_periodic_adv_list_size()

Get the number of devices inside the Periodic Advertiser List of the device

    advertiser = advertiser_list[0]
    rtlsUtil.padv_remove_device_from_periodic_adv_list(advertiser)

Remove one specific advertiser from the Periodic Advertiser List of the device

    rtlsUtil.padv_clear_periodic_adv_list()

Remove all the advertisers from the Periodic Advertiser List of the device

The python procedure allows to read the content of the Periodic Advertiser List is not implemented. We recommend keeping a list within the python script of the devices added to the Periodic Advertiser List. In the out-of-the-box script, all the periodic advertisers found are added to the Periodic Advertiser List so no specific list is kept.

Create the synchronization with a periodic advertiser

The function rtlsUtil.padv_create_sync() is used to create the synchronization. This functions requires five parameters that are described here:

  • advertiser (called Peripheral). This parameter contains the description of the advertiser to synchronize with.

      advertiser = advertiser_list[0]
    

    Note: When synchronizing with the advertiser(s) of the Periodic Advertiser List this parameter is not used (see next bullet). In that case we recommend using a "dummy advertiser" instead:

      # Dummy advertiser to send when using periodic advertise list for sync
      dummy_advertiser = {'addr': 'FF:FF:FF:FF:FF:FF',
                          'addrType': 0,
                          'rssi': 0,
                          'advSID': 0,
                          'periodicAdvInt': 0
                          }
    
  • options. There are two option to create the synchronization, one is using the Periodic Advertiser List and the other one is using the specified advertiser. In additon, one can choose if the periodic avertisements reports should be initially enabled or disabled.

      # Options constants:
      #   Clear Bit 0 - Use the advSID, advAddrType, and advAddress parameters to determine which advertiser to listen to.
      #   Set Bit 0   - Use the Periodic Advertiser List to determine which advertiser to listen to.
      #   Clear Bit 1 - Reporting initially enabled.
      #   Set Bit 1   - Reporting initially disabled.
    
      USE_GIVEN_ADDRESS_AND_REPORT_ENABLE = 0
      USE_LIST_AND_REPORT_ENABLE = 1
      USE_GIVEN_ADDRESS_AND_REPORT_DISABLE = 2
      USE_LIST_AND_REPORT_DISABLE = 3
    

    The reports may not be enabled at the beginning to save some computation to the MCU. In that case the MCU keeps receiving the periodic advertisings but does not execute a callback at each reception.

    If the reports are not enabled at synchronization creation, they can only be enabled after the synchronization is established:

        rtlsUtil.padv_create_sync(advertiser,
                                  USE_GIVEN_ADDRESS_AND_REPORT_DISABLE,
                                  sync_skip,
                                  sync_timeout,
                                  sync_cte_type)
    
        # Scan again for sync established event. scan again if not sync established event occurred
        sync_est_status = scan_for_sync_est(scan_time_sec, rtlsUtil, advertiser, num_of_scan_retry)
    
        # Enable periodic advertise reports
        rtlsUtil.padv_periodic_receive_enable(rtlsUtil.padv_get_sync_handle_by_slave(advertiser))
    
  • skip. This is the maximum number of periodic advertising events that can be skipped after a successful receive. This is mainly used for energy savings and can be kept to 0 for a first evaluation.

      sync_skip = 0         # The maximum number of periodic advertising events that can be skipped after
                            # a successful receive (Range: 0x0000 to 0x01F3)
    
  • syncTimeout. Maximum time allowed between the successfull reception of two periodic advertisements. Synchronization is considered as lost if this time expires. This is mainly used for energy savings and can be kept to 10 times the periodic advertisement interval for a first evaluation.

      # Synchronization timeout for the periodic advertising train Range: 0x000A to 0x4000 Time = N*10 ms Time Range
      # For this example, the timeout value set to be 10 times bigger then the periodic advertise interval.
      sync_timeout = int((advertiser['periodicAdvInt'] * 1.25) * 10)
    
  • syncCteType. This parameter allows to synchronize only with periodic advertisings containing a CTE. Here we want to synchronize with periodic advertising appended with an AoA CTEs only.

      sync_cte_type = 0     # Clear All Bits(0) - Sync All
                            # Set Bit 0(1) - Do not sync to packets with an AoA CTE
                            # Set Bit 1(2) - Do not sync to packets with an AoD CTE with 1 us slots
                            # Set Bit 2(4) - Do not sync to packets with an AoD CTE with 2 us slots
                            # Set Bit 4(16) - Do not sync to packets without a CTE
    

Here is the way to call the function rtlsUtil.padv_create_sync():

  rtlsUtil.padv_create_sync(advertiser,
                            USE_GIVEN_ADDRESS_AND_REPORT_DISABLE,
                            sync_skip,
                            sync_timeout,
                            sync_cte_type)

The synchronization is now created and ready to be established!

Establish the synchronization with a periodic advertiser

This is done by launching a new scan.

  • In the cases where you want to synchronize with the device specified (not with the devices in the Periodic Advertiser List), the function scan_for_sync_est defined in rtls_connectionless_aoa_example_with_rtls_util.py can be leveraged.

      sync_advertiser_list = []
      scan_time_sec = 10     # Scanning time duration in seconds
      um_of_scan_retry = 5   # Number of scan retry in case sync established event didn't occurred
    
      sync_est_status = scan_for_sync_est(scan_time_sec, rtlsUtil, advertiser, num_of_scan_retry)
      if sync_est_status:
          print(f"Sync established with: {advertiser}")
          sync_advertiser_list.append(advertiser)
      else:
          print(f"Failed to establish sync with: {advertiser}")
    
  • In the cases where you want to synchronize with the devices in the Periodic Advertiser List, use directly rtlsUtil.scan(), then verify if all the expected synchronizations have been established using the function rtlsUtil.padv_get_sync_handle_by_slave(). In our example, we added all the periodic advertisers we found during scanning to the Periodic Advertiser List (so we directly use the advertiser_list to verify if all the synchronizations have been established):

    # Scan again for sync established event
    rtlsUtil.scan(scan_time_sec)
    time.sleep(3)
    
    # verify if the sync handle is different from 0
    synced_advertiser = None
    for advertiser in advertiser_list:
        if rtlsUtil.padv_get_sync_handle_by_slave(advertiser) == 0:
            synced_advertiser = advertiser
            print(f"Sync created with first advertiser on advertisers list: {synced_advertiser}. "
                  f"Periodic advertise report enabled automatically")
    
In summary...

Connectionless-AoA requires synchronizing the rtls_master device with periodic advertisers. Periodic advertisers can be detected through a scanning procedure. The scanning procedure detects both periodic advertisers and legacy advertisers. Two ways exist to synchronize with periodic advertiser(s).

  • Synchronization with one advertiser. Based on the result of the scanning procedure, one periodic advertiser is selected. Synchronization is first created. Then synchronization is established.
  • Synchronization with all the periodic advertiser(s) in the Periodic Advertiser List of the rtls_master device. To do so some periodic advertisers detected during the scanning procedure are selected then added to the Periodic Advertiser List. Once the Periodic Advertiser List is ready, the synchronization must be created then established.

Enabling Localization

Enabling Localization with connection-AoA

This section uses the rtls_example_with_rtls_util.py python script.

Enabling connection-AoA

Now that the connection has been formed and the connection parameter information has been distributed, it is time to enable AoA.

  aoa_params = {
      "aoa_run_mode": "AOA_MODE_ANGLE",  ## AOA_MODE_ANGLE, AOA_MODE_PAIR_ANGLES, AOA_MODE_RAW
      "aoa_cc26x2": {
          "aoa_slot_durations": 1,
          "aoa_sample_rate": 1,
          "aoa_sample_size": 1,
          "aoa_sampling_control": int('0x10', 16),
          ## bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering,
          ## bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
          "aoa_sampling_enable": 1,
          "aoa_pattern_len": 3,
          "aoa_ant_pattern": [0, 1, 2]
      }
  }
  rtlsUtil.aoa_set_params(aoa_params)
  print("AOA Params Set")

  ## Setup thread to pull out received data from devices on screen
  th_aoa_results_parsing = threading.Thread(target=results_parsing, args=(rtlsUtil.aoa_results_queue,))
  th_aoa_results_parsing.setDaemon(True)
  th_aoa_results_parsing.start()
  print("AOA Callback Set")

  rtlsUtil.aoa_start(cte_length=20, cte_interval=1)
  print("AOA Started")
Continuous Connection Information (to be used with connection-AoA)

CCI is the default functionality of the out-of-box rtls_example_with_rtls_util.py. It is the most simplistic method and only provides a received signal strength indicator (RSSI) and frequency channel index for each BLE connection event.

CCI can be enabled on both the Central and the Passive(s) by sending a RTLS_CMD_CONN_INFO UNPI request. This is abstracted and initiated from RtlsUtil as such:

  rtlsUtil.cci_start()

After being enabled, the respective node will send a RTLS_EVT_CONN_INFO UNPI Asynchronous Response after each Central-Peripheral connection event. This response contains the RSSI and the channel of the connection event. The Central will send this after participating in the connection event with the Peripheral and the Passive will send this after it observes the connection event.

RtlsUtil will receive each response and append it to the RtlsUtil.conn_info_queue. This queue can then be processed as desired. The out-of-box example periodically reads and prints from this queue in results_parsing() in a separate thread.

This procedure is shown here:

Stopping the Example

After enabling the localization mode(s), rtls_example_with_rtls_util.py will sleep for 5 seconds then gracefully stop all ongoing over-the-air procedures and any spawned result-processing threads. If it is desired to run for longer than 15 seconds, simply update the timeout_sec in the following code:

  timeout_sec = 15
  print("Going to sleep for {} sec".format(timeout_sec))
  timeout = time.time() + timeout_sec
  while timeout >= time.time():
      time.sleep(0.1)
Updating Connection Interval

It is possible to dynamically update the connection interval of the Central-Peripheral BLE connection after the connection has been established via RtlsUtil.set_connection_interval():

new_connect_interval_mSec = 80
rtlsUtil.set_connection_interval(new_connect_interval_mSec)

The new connection parameters are automatically distributed from the node manager to the Passive devices so that they capable of maintaining synchronization with the connection after the update occurs. The procedure is shown here:

Enabling Localization with connectionless-AoA

This section uses the rtls_connectionless_aoa_example_with_rtls_util.py python script.

Enabling connectionless-AoA

Now that the device is synchronized with a periodic advertiser and that periodic advertising reports are enabled, we can enable connection-less AoA.

  rtlsUtil.cl_aoa_start(cl_aoa_params, advertiser)
  print("Connectionless AOA started")

Connetionless-AoA has to be started for each advertiser we are synchronized with.

Stopping the connectionless-AoA example

After enabling connectionless-AoA, rtls_connectionless_aoa_example_with_rtls_util.py will sleep for 15 seconds then gracefully stop connectionless-AoAs. If it is desired to run for longer than 15 seconds, simply update the timeout_sec in the following code:

  ## Sleep code to see in the screen receives data from devices
  timeout_sec = 15
  print("Going to sleep for {} sec".format(timeout_sec))
  timeout = time.time() + timeout_sec
  while timeout >= time.time():
      time.sleep(0.01)

  rtlsUtil.cl_aoa_stop(cl_aoa_params, advertiser)
  print("Connectionless AOA stopped")

This does not finish the synchronization(s) with the periodic advertisers. If you desire to finish the synchronization with the periodic advertisers, you need to use the funtion rtlsUtil.padv_terminate_sync().

  for s_advertiser in sync_advertiser_list:
      sync_handle = rtlsUtil.padv_get_sync_handle_by_slave(s_advertiser)
      rtlsUtil.padv_terminate_sync(sync_handle)
      print(f"Sync terminated for sync handle: {sync_handle}")

Introduction to AoA

Bluetooth Core Specification Version 5.1 introduces AoA/AoD which are covered under Direction Finding Using Bluetooth Low Energy Device 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 receivers only have one antenna but can capture I/Q samples when receiving direction finding packets.

Direction Finding Method Transmitter Receiver
AoA Single antenna, transmit CTE Multiple antennas, RF switches, can switches antennas while capture I/Q data of the CTE
AoD Multiple antennas, RF switches, transmit 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:

  1. Periodic advertising; also called Connectionless CTE
  2. 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 CC13X2 / CC26X2 SDK offering.

AoA measurement is typically a 3-step process:

  1. Collect phase information by sampling the I/Q
  2. Calculate the phase difference among the antennas
  3. Convert 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).

The picture shown below is a constellation diagram which illustrates signal vectors from 2 antennas. If all the antennas are positioned in line and with a fixed distance d, the phase difference Φ between adjacent antennas will be constant.

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 (Θ). If Φ is negative, this means that antenna 2 is ahead of antenna 1. In this case Θ is negative too but this does not cause any mathematical problem as sin() and arcsin() functions are defined both for positive and negative numbers. To avoid any unnecessary complications we will consider here Φ to be positive.

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Π)

Therfore Θ can be represented as below:

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

Have you moved the capacitor C51 of your LaunchPad to use external antenna?

If you have no idea what I am talking about, it's time for you to read the "Preparations" chapter of the Angle of Arrival BoosterPack Getting Started guide :)

The SimpleLink CC13X2-26X2 SDK has three examples dedicated to performing AoA (rtls_master, rtls_slave and rtls_passive).

In the TI BLE5-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 running the RTLS visual demo, you should be able to get it up and running.

An important remark before starting

Connectionless-AoA support has been added with SDK version 4.40. The same BOOSTXL-AoA can be used for connection-AoA an connectionless-AoA.

As a result, you can choose to run the example with two modes (connection-AoA or connectionless-AoA) and four different topologies:

  • connection-AoA with both the Central and the Passive are performing AoA measurements
  • connection-AoA with only the Passive is performing the measurements
  • connection-AoA with only the Central is performing the measurements
  • connectionless-AoA

In this task, you will configure the example to suit best your needs. In the rest of the lab, we will provide the steps for connection-AoA assuming you are using both the Central and the Passive to perform the measurement. If this is not the case, you can basically ignore the steps that do not concern you.

  1. Hardware settings:

    Two BOOSTXL-AoA are required. One BOOSTXL-AoA must be on the Passive device, the other must be on the Central device.

  2. Embedded Software settings:

    In theory, no modification is required by the out-of-the-box software to run the demo. The guidance are provided for training purposes only.

    • rtls_passive: The Passive's RTLS Control module needs the capability to act as RTLS Passive and AoA receiver. This capabilities are set modifying (if needed) the field rtlsCapab of the rtlsConfig structure (this must be done into the main.c file which is stored under the Startup folder). The capabilities we need for the lab are only RTLS_CAP_RTLS_PASSIVE and RTLS_CAP_AOA_RX.

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_PASSIVE | RTLS_CAP_AOA_RX);
      

      main.c::main()

      Note: The IOs and the switching pattern used by the antennas are provided through the overrides.

    • rtls_master: Ensure the Central's RTLS Control module has the capability to act as a Central (RTLS_CAP_RTLS_MASTER) and to receive the CTE (RTLS_CAP_AOA_RX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_MASTER | RTLS_CAP_AOA_RX);
      

      main.c::main()

      Note: The python script configures the Central device in order to select the appropriate antennas and switching pattern. The default parameters are as following:

      "aoa_cc26x2": {
        "aoa_slot_durations": 1,
        "aoa_sample_rate": 1,
        "aoa_sample_size": 1,
        "aoa_sampling_control": int('0x10', 16),
        ## bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering,
        ## bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
        "aoa_sampling_enable": 1,
        "aoa_pattern_len": 3,
        "aoa_ant_pattern": [0, 1, 2]
      }
      

      aoa_params used by default by the python script

    • rtls_slave: Ensure the Peripheral's RTLS Control module has the capability to act as a Peripheral (RTLS_CAP_RTLS_SLAVE) and to send out the CTE (RTLS_CAP_AOA_TX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_SLAVE | RTLS_CAP_AOA_TX);
      

      main.c::main()

      Once you have done the required modification on the embedded code, flash the LaunchPads and power-cycle them

  3. RTLS UI settings:

    Run the RTLS UI as explained before.

    Verify if the Passive's and Central's capabilities listed by the RTLS UI match the settings you have previously done.

  1. Hardware settings:

    The BOOSTXL-AoA must be on the Passive device.

  2. Embedded Software settings:

    • rtls_passive: The Passive's RTLS Control module needs the capability to act as RTLS Passive and AoA receiver. This capabilities are set modifying (if needed) the field rtlsCapab of the rtlsConfig structure (this must be done into the main.c file which is stored under the Startup folder). The capabilities we need for the lab are only RTLS_CAP_RTLS_PASSIVE and RTLS_CAP_AOA_RX.

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_PASSIVE | RTLS_CAP_AOA_RX);
      

      main.c::main()

      Note: The IOs and the switching pattern used by the antennas are provided through the overrides.

    • rtls_master: Ensure the Central's RTLS Control module has the capability to act as a Central (RTLS_CAP_RTLS_MASTER).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_MASTER);
      

      main.c::main()

    • rtls_slave: Ensure the Peripheral's RTLS Control module has the capability to act as a Peripheral (RTLS_CAP_RTLS_SLAVE) and to send out the CTE (RTLS_CAP_AOA_TX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_SLAVE | RTLS_CAP_AOA_TX);
      

      main.c::main()

      Once you have done the required modification on the embedded code, flash the LaunchPads and power-cycle them

      Note: The python script configures the Passive device in order to select the appropriate antennas and switching pattern. The default parameters are as following:

      "aoa_cc26x2": {
        "aoa_slot_durations": 1,
        "aoa_sample_rate": 1,
        "aoa_sample_size": 1,
        "aoa_sampling_control": int('0x10', 16),
        ## bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering,
        ## bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
        "aoa_sampling_enable": 1,
        "aoa_pattern_len": 3,
        "aoa_ant_pattern": [0, 1, 2]
      }
      

      aoa_params used by default by the python script

  3. RTLS UI settings:

    Run the RTLS UI as explained before.

    Verify if the Passive's and Central's capabilities listed by the RTLS UI match the settings you have previously done.

  1. Hardware settings:

    In this case, the Passive device is not required. The BOOSTXL-AoA must be on the Central device.

  2. Embedded Software settings:

    In theory, no modification is required by the out-of-the-box software to run the demo. The guidance are provided for training purposes only.

    • rtls_passive: As previously said, the Passive device is not required (no need to turn it on or to flash any image on it).

    • rtls_master: The Central's RTLS Control module needs the capability to act as RTLS Central and AoA receiver. This capabilities are set modifying (if needed) the field rtlsCapab of the rtlsConfig structure (this must be done into the main.c file which is stored under the Startup folder). The capabilities we need for the lab are only RTLS_CAP_RTLS_MASTER and RTLS_CAP_AOA_RX.

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_MASTER | RTLS_CAP_AOA_RX);
      

      main.c::main()

      Note: The python script configures the Central device in order to select the appropriate antennas and switching pattern. The default parameters are as following:

      "aoa_cc26x2": {
        "aoa_slot_durations": 1,
        "aoa_sample_rate": 1,
        "aoa_sample_size": 1,
        "aoa_sampling_control": int('0x10', 16),
        ## bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering,
        ## bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
        "aoa_sampling_enable": 1,
        "aoa_pattern_len": 3,
        "aoa_ant_pattern": [0, 1, 2]
      }
      

      aoa_params used by default by the python script

    • rtls_slave: Ensure the Peripheral's RTLS Control module has the capability to act as a Peripheral (RTLS_CAP_RTLS_SLAVE) and to send out the CTE (RTLS_CAP_AOA_TX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_SLAVE | RTLS_CAP_AOA_TX);
      

      main.c::main()

      Once you have done the required modification on the embedded code, flash the LaunchPads and power-cycle them

  3. RTLS UI settings:

    Run the RTLS UI as explained before. Only the Central is supposed to be connected to the computer. As a result, only the Central device is supposed to be detected by the RTLS UI.

    Verify if the Central's capabilities listed by the RTLS UI match the settings you have previously done.

A few elements to verify before using connectionless-AoA

  • Make sure you are using SDK 4.40 or newer
  • If you are using the python script, make sure you are using the proper one (rtls_connectionless_aoa_example_with_rtls_util.py)
  • If you are using the RTLS_UI, make sure you have selected the "CL AoA" mode.
  • In the rtls_master and rtls_slave projects, make sure the following symbols are defined (they should be defined in the file Tools > Defines > rtls_xxxx_app.opt of the project)
    • For rtls_master: RTLS_CTE, USE_RTLS, USE_PERIODIC_SCAN, USE_PERIODIC_RTLS
    • For rtls_slave: RTLS_CTE, USE_RTLS, USE_PERIODIC_ADV
  1. Hardware settings:

    In this case, the Passive device is not supported. The BOOSTXL-AoA must be on the Central device.

  2. Embedded Software settings:

    In theory, no modification is required by the out-of-the-box software to run the demo. The guidance are provided for training purposes only.

    • rtls_passive: As previously said, the Passive device is not supported (no need to turn it on or to flash any image on it).

    • rtls_master: The Central's RTLS Control module needs the capability to act as RTLS Central and AoA receiver. This capabilities are set modifying (if needed) the field rtlsCapab of the rtlsConfig structure (this must be done into the main.c file which is stored under the Startup folder). The capabilities we need for the lab are RTLS_CAP_RTLS_MASTER, RTLS_CAP_AOA_RX and RTLS_CAP_CL_AOA.

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_MASTER | RTLS_CAP_AOA_RX | RTLS_CAP_CL_AOA);
      

      main.c :: main()

      Note: The python script configures the Central device in order to select the appropriate antennas and switching pattern. The default parameters are as following:

      cl_aoa_params = {
          "cl_aoa_role": "AOA_MASTER",  # AOA_MASTER - currently only Central role supported
          "cl_aoa_result_mode": "AOA_MODE_RAW",  # AOA_MODE_ANGLE, AOA_MODE_PAIR_ANGLES, AOA_MODE_RAW
          "cl_aoa_slot_durations": 2,
          "cl_aoa_sample_rate": 1,  # 1Mhz (BT5.1 spec), 2Mhz, 3Mhz or 4Mhz - this enables oversampling
          "cl_aoa_sample_size": 1,  # 8 bit sample (as defined by BT5.1 spec), 16 bit sample (higher accuracy)
          "cl_aoa_sampling_control": int('0x10', 16),
          # bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering (use this mode for post process angle calculation),
          # bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
          "max_sample_cte": 1,
          "cl_aoa_pattern_len": 3,
          "cl_aoa_ant_pattern": [0, 1, 2]
      }
      

      default cl_aoa_params used by the python script

    • rtls_slave: Ensure the Peripheral's RTLS Control module has the capability to act as a Peripheral (RTLS_CAP_RTLS_SLAVE) and to send out the CTE (RTLS_CAP_AOA_TX).

      rtlsConfig.rtlsCapab = (rtlsCapabilities_e)(RTLS_CAP_RTLS_SLAVE | RTLS_CAP_AOA_TX);
      

      main.c :: main()

  3. RTLS UI settings:

    Run the RTLS UI as explained before. Only the Central is supposed to be connected to the computer. As a result, only the Central device is supposed to be detected by the RTLS UI.

    Verify if the Central's capabilities listed by the RTLS UI match the settings you have previously done.

AoA accuracy with Connectionless-AoA

The estimation algorithms for connectionless-AoA are not provided in the SDK. As a consequence, the results displayed by the RTLS UI and the Python scripts may look inaccurate.

Now, test your setting

Same procedure as what we did previously

If AoA values are inaccurate...

Keep in mind that an office environment is not suitable for AoA evaluation. Many metallic and RF reflective surfaces are around you and provoke multipath. Try to find a more open environment.

AoA inaccuracies might also be caused by a bad antenna connection. Verify that the C51 capacitor of your LaunchPad is correctly positioned.

Task 2 – Modify AoA application to use the other antenna array

Using the python scripts...

From now on the lab only gives the direction on how to modify the python scripts. To keep it simple, we will always use rtls_example_with_rtls_util.py (for connection-AoA) and rtls_connectionless_aoa_example_with_rtls_util.py (for connctionless-AoA). The same procedure applies for all the python scripts.

The out of box SimpleLink CC13X2-26X2 SDK example will always use the first antenna array.

  • We will go from this configuration (using antenna array #1):

  • To this configuration (using antenna array #2):

In the python script, modify the aoa_params structure as followed:

    aoa_params = {
      "aoa_run_mode": "AOA_MODE_RAW",
      "aoa_cc26x2": {
          "aoa_slot_durations": 1,
          "aoa_sample_rate": 1,
          "aoa_sample_size": 1,
          "aoa_sampling_control": int('0x10', 16),
          ## bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering,
          ## bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
          "aoa_sampling_enable": 1,
          "aoa_pattern_len": 3,
          "aoa_ant_pattern": [3, 4, 5]
      }
    }

aoa_params used to activate the second antenna array

In the python script, modify the cl_aoa_params structure as followed:

    cl_aoa_params = {
        "cl_aoa_role": "AOA_MASTER",  # AOA_MASTER - currently only Central role supported
        "cl_aoa_result_mode": "AOA_MODE_RAW",
        "cl_aoa_slot_durations": 2,
        "cl_aoa_sample_rate": 1,  # 1Mhz (BT5.1 spec), 2Mhz, 3Mhz or 4Mhz - this enables oversampling
        "cl_aoa_sample_size": 1,  # 8 bit sample (as defined by BT5.1 spec), 16 bit sample (higher accuracy)
        "cl_aoa_sampling_control": int('0x21', 16),
        # bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering (use this mode for post process angle calculation),
        # bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
        "max_sample_cte": 1,
        "cl_aoa_pattern_len": 3,
        "cl_aoa_ant_pattern": [3, 4, 5]
    }

cl_aoa_params used to activate the second antenna array

Now, test your setting

You have two options available:

  1. Use the same procedure as described previously. In the log you should observe that the antenna 2 is used.

  2. Have a look at DIO27, DIO28, DIO29, DIO30. The antenna switching is controlled by DIO27 to DIO30. Using a logic analyzer, you can determine which antennas are used. The following truth table gives the control logic of the antennas of the BOOSTXL-AoA:

DIO27 DIO28 DIO29 DIO30 Antenna Used
0 1 0 0 A2.1
0 0 1 0 A2.2
0 0 0 1 A2.3
1 1 0 0 A1.1
1 0 1 0 A1.2
1 0 0 1 A1.3

*0: 0.0V to 0.2V, 1: 2.5V to 5.0V

Task 3 – Modify AoA application to use only two antennas

We will use the out of the box example as the baseline and make the proper changes so we only use 2 antennas from the antenna array 1.

  • Out of the box software is using all 3 antennas from A1:

  • And the end goal is using the following:

The antenna toggling pattern is controlled by radio core, therefore the pattern and the pins are passed on to radio core through override list.

More information!

Even though the radio core does the antenna toggling, the application layer is still responsible to initialize the pins to the correct configuration and the array switch if the feature is required.

The pin initialization is taken care in AOA.c::AOA_initAntArray.

The pins used by the rtls_master for antenna switching are set by the symbol ANTENNA_IO_MASK and the table antennaTbl (both are defined in the file ble_user_config.c).

In the python script, modify the aoa_params structure as followed:

    aoa_params = {
      "aoa_run_mode": "AOA_MODE_RAW",
      "aoa_cc26x2": {
          "aoa_slot_durations": 1,
          "aoa_sample_rate": 1,
          "aoa_sample_size": 1,
          "aoa_sampling_control": int('0x10', 16),
          ## bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering,
          ## bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
          "aoa_sampling_enable": 1,
          "aoa_pattern_len": 2,
          "aoa_ant_pattern": [0, 1]
      }
    }

aoa_params used to activate only two antennas

Antenna pattern can only be changed in AOA_MODE_RAW

If the field aoa_run_mode is set to AOA_MODE_ANGLE, you cannot change the antenna pattern field (aoa_ant_pattern). It must be 0,1,2 when using antenna array 1 or 3, 4, 5 for antenna array 2. This is because in AOA_MODE_ANGLE the code running on rtls_master and rtls_passive calculates the angle from the raw data. The out-of-the-box code only works for 3 antennas, hence this limitation.

AOA_MODE_RAW with RAW_RF

When selecting AOA_MODE_RAW with RAW_RF, the parameters selected for aoa_sample_rate and aoa_sample_size have no effect. The system will default to 4MHz sampling rate with 13 bits I/Q data.

In the python script, modify the cl_aoa_params structure as followed:

    cl_aoa_params = {
        "cl_aoa_role": "AOA_MASTER",  # AOA_MASTER - currently only Central role supported
        "cl_aoa_result_mode": "AOA_MODE_RAW",
        "cl_aoa_slot_durations": 2,
        "cl_aoa_sample_rate": 1,  # 1Mhz (BT5.1 spec), 2Mhz, 3Mhz or 4Mhz - this enables oversampling
        "cl_aoa_sample_size": 1,  # 8 bit sample (as defined by BT5.1 spec), 16 bit sample (higher accuracy)
        "cl_aoa_sampling_control": int('0x10', 16),
        # bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering (use this mode for post process angle calculation),
        # bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
        "max_sample_cte": 1,
        "cl_aoa_pattern_len": 2,
        "cl_aoa_ant_pattern": [0, 1]
    }

cl_aoa_params used to activate only two antennas

Antenna pattern can only be changed in AOA_MODE_RAW

If the field cl_aoa_result_mode is set to AOA_MODE_ANGLE, you cannot change the antenna pattern field (cl_aoa_ant_pattern). It must be 0,1,2 when using antenna array 1 or 3, 4, 5 for antenna array 2. This is because in AOA_MODE_ANGLE the code running on rtls_master calculates the angle from the raw data. The out-of-the-box code only works for 3 antennas, hence this limitation.

AOA_MODE_RAW with RAW_RF

When selecting AOA_MODE_RAW with RAW_RF, the parameters selected for cl_aoa_sample_rate and cl_aoa_sample_size have no effect. The system will default to 4MHz sampling rate with 13 bits I/Q data.

Fundamental done

Now the application only uses antenna A1.1 and A1.2! Very well done!

Task 4 – Export raw IQ samples to CSV file

Python solution

The goal of this task is to understand how the scripts rtls_aoa_iq_with_rtls_util_export_into_csv.py (for connection-AoA) and rtls_connectionless_aoa_example_with_rtls_util.py(for connectionless-AoA) export raw IQ samples to CSV files. Both scripts are provided in the SDK under tools\ble5stack\rtls_agent\examples.

This section guides you step by step in order to rewrite the rtls_aoa_iq_with_rtls_util_export_into_csv.py script. You can directly jump to the next task if you are not interested in this part... however it is always cool to discover how the magic happens :)

1- Where do the I/Q Samples come from?

So far we have worked on the application code to modify the rtls_passive and/or the rtls_master 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, in what format and how they are accessed.

  • For the Passive device

    I/Q is stored in the radio core RAM(RFC_CTE_RFE_RAM_DATA) 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_getRfIqSamples(RF_Handle rfHandle, RF_CmdHandle cmdHandle, RF_EventMask events)
      {
        uint8_t state = 0;
        uint8_t lastCapture;
        uint32_t *cteData;
        uint32_t *extCteData = NULL;
    
        if (events & RF_EventCmdDone)
        {
          lastCapture = HWREGB(RFC_CTE_LAST_CAPTURE);
          // check which RAM is capturing samples
          if ((lastCapture == RFC_CTE_CAPTURE_RAM_MCE) || (lastCapture == RFC_CTE_CAPTURE_RAM_RFE))
          {
            // wait while buffer is busy
            do
            {
              state = (lastCapture == RFC_CTE_CAPTURE_RAM_MCE)?HWREGB(RFC_CTE_MCE_RAM_STATE):HWREGB(  RFC_CTE_RFE_RAM_STATE);
            } while ((state == RFC_CTE_RAM_STATE_BUSY) || (state == RFC_CTE_RAM_STATE_DUAL_BUSY));
    
            // check if buffer is ready for reading
            if ((state == RFC_CTE_RAM_STATE_READY) || (state == RFC_CTE_RAM_STATE_DUAL_READY))
            {
              if (lastCapture == RFC_CTE_CAPTURE_RAM_MCE)
              {
                // get the MCE buffer address
                cteData = (uint32_t *)RFC_CTE_MCE_RAM_DATA;
                extCteData = (uint32_t *)RFC_CTE_RFE_RAM_DATA;
              }
              else
              {
                // get the RFE buffer address
                cteData = (uint32_t *)RFC_CTE_RFE_RAM_DATA;
              }
    
              // This will trigger the upper layer which polls samplesReady
              if (cteData != NULL && extCteData == NULL)
              {
                memcpy(gAoaReport.samples, cteData, AOA_RES_MAX_SIZE * sizeof(AoA_IQSample));
                gAoaReport.sampleState = SAMPLES_READY;
              }
    
              // release the RAM so it should be available for next CTE
              if (lastCapture == RFC_CTE_CAPTURE_RAM_MCE)
              {
                HWREGB(RFC_CTE_MCE_RAM_STATE) = RFC_CTE_RAM_STATE_EMPTY;
              }
              else
              {
                HWREGB(RFC_CTE_RFE_RAM_STATE) = RFC_CTE_RAM_STATE_EMPTY;
              }
            }
          }
        }
    
        // If we don't have samples at this point then they are not valid
        if (gAoaReport.sampleState != SAMPLES_READY)
        {
          gAoaReport.sampleState = SAMPLES_NOT_VALID;
        }
      }
    

    AOA.c

    In rtls_passive, the function AOA_getRfIqSamples is executed (more or less directly) by the function AOA_postProcess (which is itself called by RTLSCtrl_postProcessAoa, i.e. at the end of each connection event).

  • For the Central device

    For the Central device, the BLE stack is responsible for reading the I/Q samples from the radio core. The tables containing the I/Q samples (or at least pointers on them) are sent back by the BLE Stack through messages stored in the ICall queue. The message containing I/Q samples are handled by the function RTLSMaster_processRtlsSrvMsg (in rtls_master.c). This leads to the execution of the function RTLSCtrl_aoaResultEvt (in rtls_ctrl.c) which in turn enqueues a message of type AOA_RESULTS_EVENT in the RTLS message-queue.

    Finally (and as it is done on the Passive's side) the AOA_RESULTS_EVENT triggers the function AOA_postProcess. The Central does not execute the function AOA_getRfIqSamples (the I/Q samples are part of the pEvt structure passed to AOA_postProcess).

I/Q samples resolution

I and Q samples only have 13 bits resolution even though they occupy 16 bits space in radio core RAM (this applies for both the Passive and the Central devices). Since they only have 13 bits resolution, the maximum and minimum value you will observe as signed integers are [4095, -4096].

2- How to send the I/Q samples to the computer?

  1. If you haven't done it yet, change the COM port used by the script. Of course, if you are not using the Passive device, you can comment the corresponding line.

     {"com_port": "COM12", "baud_rate": 460800, "name": "CC26x2 Central"},
     {"com_port": "COM14", "baud_rate": 460800, "name": "CC26x2 Passive"},
    

    Set up the COM ports used

  2. [Optional] Increase / decrease the frequency reception of the CTEs.

    • For connection-AoA, this can be done by changing the connection interval. The smaller the connection interval is, the greater the frequency reception of the CTEs is.
    • For connection-less AoA, this can be done in two different ways:
      • Change the periodic advertising interval (on the advertiser / rtls_slave side). The smaller the advertising interval is, the greater the frequency reception of the CTEs is.
      • Allow the scanner (rtls_master) to skip some advertisements

    We recommend to keep the default frequency reception of the CTEs to be sure we have enough time to flush the I/Q data before next AoA packet arrives. If you decice to increase the frequency reception of the CTEs, you may be required to increase the baudrate of the UART connection between the computer and the locator devices.

    For SDK 3.30 and later, the default UART baudrate is set to 460800. This value is set and can be modified in the function RTLSHost_openHostIf (in the file rtls_host_npi.c, stored in the folder RTLSCtrl.)

         npiPortParams.portParams.uartParams.baudRate = 921600;
    

    Change baudrate (for example) to 921600

    Note: The procedure is the same for both Passive and Central.

    If you modify the baudrate in the embedded code, don't forget to modify the baudrate used by the python script to communicate with the devices...

           {"com_port": "COM12", "baud_rate": 921600, "name": "CC26x2 Central"},
           {"com_port": "COM14", "baud_rate": 921600, "name": "CC26x2 Passive"},
    

    Set up the baudrates used

    Here is an example showing how to increase connection interval to 500ms.

    → Change the connect_interval_mSec parameter to the following code in rtls_example_with_rtls_util.py.

     connect_interval_mSec = 500 #100
    

    Set up connection interval to be 500ms

    In this example we allow the scanner to skip a fix number of periodic advertising events. The number of periodic advertising events the device is allowed must be specified at synchronization creation. Here we allow the device to skip 5 periodic advertising events. The timeout of the synchronization should be increased too to avoid unwanted synchronization lost.

     sync_skip = 5  # The maximum number of periodic advertising events that can be skipped after a successful receive (Range: 0x0000 to 0x01F3)
    
     sync_timeout = int((advertiser['periodicAdvInt'] * 1.25) * sync_skip * 10)
    
     rtlsUtil.padv_create_sync(dummy_advertiser,
                               USE_LIST_AND_REPORT_ENABLE,
                               sync_skip,
                               sync_timeout,
                               sync_cte_type)
    

    Set up connection interval to be 500ms

    This modification must be done in rtls_slave.c. Here we increase the periodic advertising interval to 500 ms.

       // Periodic Advertising Intervals
       #define PERIODIC_ADV_INTERVAL_MIN    400  // 500 ms
       #define PERIODIC_ADV_INTERVAL_MAX    400  // 500 ms
    
       // Set Periodic Advertising parameters
       GapAdv_periodicAdvParams_t perParams = {PERIODIC_ADV_INTERVAL_MIN,
                                               PERIODIC_ADV_INTERVAL_MAX, 0x40};
       status = GapAdv_SetPeriodicAdvParams(advHandleNCNS, &perParams);
    

    rtls_slave.c – Change periodic advertising interval

    Once this change is done, build the project and flash it on the device.

    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.

  3. Now we will build on top of the python script that is used in running the RTLS non-visual demo

    First we create a csv file to store I/Q data for analyzing later if needed. Normally, this code is already written in your rtls_aoa_iq_with_rtls_util_export_into_csv.py file, in the function initialize_csv_file. This function assumes that you have properly imported the csv package and declared the global variables csv_writer and csv_row. You can change the filename to whichever suits you better.

     global csv_writer
     global csv_row
    
     # Prepare csv file to save data
     data_time = datetime.datetime.now().strftime("%m_%d_%Y_%H_%M_%S")
     filename = f"{data_time}_rtls_raw_iq_samples.csv"
     outfile = open(filename, 'w', newline='')
    
     csv_fieldnames = ['pkt', 'sample_idx', 'rssi', 'ant_array', 'channel', 'i', 'q']
     csv_row = namedtuple('csv_row', csv_fieldnames)
    
     csv_writer = csv.DictWriter(outfile, fieldnames=csv_fieldnames)
     csv_writer.writeheader()
    

    Create a csv file to log I/Q data

  4. The devices must be reset, then the Central can scan and connect

         ## Reset the Passive and the Central
         rtlsUtil.reset_devices()
    
         ##...
    
         ## Ask the Central to scan
         scan_results = rtlsUtil.scan(scan_time_sec)
    
         ##...
    
         ## Ask the Central to connect a device
         rtlsUtil.ble_connect(scan_results[0], connect_interval_mSec)
    

    Set the parameter to configure the devices

  5. Once the Peripheral and the Central are connected, we can start the setup of AoA parameters

    To extract I/Q data, the AoA operation mode needs to be AOA_MODE_RAW. If needed, modify in the aoa_params structure, modify the aoa_run_mode to the following:

         aoa_params = {
             "aoa_run_mode": "AOA_MODE_RAW",  ## AOA_MODE_ANGLE, AOA_MODE_PAIR_ANGLES, AOA_MODE_RAW
             "aoa_cc26x2": {
                 "aoa_slot_durations": 1,
                 "aoa_sample_rate": 1,
                 "aoa_sample_size": 1,
                 "aoa_sampling_control": int('0x10', 16),
                 ## bit 0   - 0x00 - default filtering, 0x01 - RAW_RF no filtering,
                 ## bit 4,5 - default: 0x10 - ONLY_ANT_1, optional: 0x20 - ONLY_ANT_2
                 "aoa_sampling_enable": 1,
                 "aoa_pattern_len": 3,
                 "aoa_ant_pattern": [0, 1, 2]
             }
         }
    

    Set the parameter to configure the devices

  6. Let's have a look to the mode that used after a BLE connection has been established. Basically, the data are written to the .csv file until a STOP command is received.

         try:
             data = q.get(block=True, timeout=0.5)
             if isinstance(data, dict):
                 data_time = datetime.datetime.now().strftime("[%m:%d:%Y %H:%M:%S:%f] :")
                 print(f"{data_time} {json.dumps(data)}")
    
                 offset = data['payload'].offset
                 payload = data['payload']
    
                 # 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 s: s.sample_idx)
    
                     # Write to file
                     for sample_row in dump_rows:
                         csv_writer.writerow(sample_row._asdict())
    
                     # Reset payload storage
                     dump_rows = []
    
                 # Save samples for writing when dump is complete
                 for sub_idx, sample in enumerate(payload.samples):
                     sample = csv_row(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)
    
             elif isinstance(data, str) and data == "STOP":
                 print("STOP Command Received")
    
                 # If we have data, and offset is 0, we are done with one dump
                 if len(dump_rows):
                     # Make sure the samples are in order
                     dump_rows = sorted(dump_rows, key=lambda s: s.sample_idx)
    
                     # Write to file
                     for sample_row in dump_rows:
                         csv_writer.writerow(sample_row._asdict())
    
                 break
             else:
                 pass
         except queue.Empty:
             continue
    

    Script's main loop (in results_parsing subfunction)

    Log file

    By default, the script produces a log file. This file is stored in the same path as the .csv file (i.e. in the directory from where the script was run) and can be useful for debug.

  7. After running the script, you can see the following format in the csv file.

    Remark 1: The names of the columns are chosen when the CSV is initialized.

     csv_fieldnames = ['pkt', 'sample_idx', 'rssi', 'ant_array', 'channel', 'i', 'q']
     csv_row = namedtuple('csv_row', csv_fieldnames)
    
     csv_writer = csv.DictWriter(outfile, fieldnames=csv_fieldnames)
     csv_writer.writeheader()
    

    Remark 2: The data written in each column are chosen by the following code.

     # Samples are prepared
     sample = csv_row(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)
    
     #...
    
     # Samples are written to the CSV file later
     for sample_row in dump_rows:
         csv_writer.writerow(sample_row._asdict())
    

3- How to treat the I/Q samples on the computer?

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,
      )),
  )

ss_rtls.py::class AoaResultRaw

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 running the RTLS non-visual demo chapter for more information on how to install packages from pip.

import matplotlib.pyplot as plt
import pandas as pd
import math

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_samples.csv')
    AOA_RES_MAX_SIZE=81

    # We only want one set of data per channel
    df = df.drop_duplicates(subset=['channel','sample_idx'] , keep='first')

    # We Calculate the phase and the margin
    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')
    grouped[['i', 'q']].plot(title="I/Q samples", grid=True)
    plt.show()

    # 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_RES_MAX_SIZE], ylim=[-190,+190])
    indexed.unstack(level=0)[['magnitude']].plot(subplots=True, title="Signal magnitude", xlim=[0,AOA_RES_MAX_SIZE])
    indexed.unstack(level=0)[['i']].plot(subplots=True, title="I samples", xlim=[0,AOA_RES_MAX_SIZE])
    indexed.unstack(level=0)[['q']].plot(subplots=True, title="Q samples", xlim=[0,AOA_RES_MAX_SIZE])
    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 between 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.

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.

That's all folks!

You earned a coffee ☕.
Now go off and make something awesome with Angle Of Arrival!

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.