Introduction

Note

Please note that this RTLS Intro lab only applies to SDK 3.10.00. We are in the process of revamping our RTLS SimpleLink Academy Lab content and plan to release at the end of August 2019.

For updated documentation RTLS for SDK 3.20.00 please refer to the TI BLE-Stack User's Guide.

This module is an introduction to the Real Time Localization System (RTLS) Toolbox as well as its suite of PC tools. The RTLS toolbox is a collection of tools for localization, direction finding, and secure range bounding.

The RTLS architecture consists of software running on the host (PC) and the embedded device (CC26xx). Embedded RTLS nodes are controlled by the host via commands over UART. Host software is responsible for sending commands to the embedded nodes and collecting the results. The suite of host software is referred to as the RTLS Node Manager.

It is assumed that the reader has a basic knowledge of embedded tool chains and general C and Python programming concepts.

This lab is based on the rtls_master rtls_slave and rtls_passive projects that are part of the SimpleLink™ CC2640R2 SDK (http://www.ti.com/tool/SIMPLELINK-CC2640R2-SDK).

This lab will focus on running the out of box demos, setting up the host Python environment, and getting started with data collection using Angle of Arrival (AoA) or Time of Flight (ToF).

Prerequisites

Software for desktop development

Hardware

This module requires the following kits:

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

  • Getting Started
  • The CC2640R2F SDK Platform
  • Application
  • BLE-Stack
  • RTLS Toolbox
  • Network Processor Interface (NPI)

Getting started with AoA booster pack

Project readme files:

  • rtls_master
  • All relevant information to rtls_slave and rtls_passive is contained in the rtls_master readme
  • rtls_agent Readme located in <SimpleLink CC2640R2 SDK> → tools → blestack → rtls_agent folder within the CC2640R2 SDK. This will be covered in detail in Task 2.

Getting started – Desktop

Install the Software

  1. Run the SimpleLink CC2640R2 SDK installer.
  2. Install Python 3.7 or later from the Python Download page.
  3. Setup the Python environment as described in the README.html in the <SimpleLink CC2640R2 SDK> → tools → blestack → 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_CC2640R2_SDK_INSTALL_DIR> which defaults to C:\ti\simplelink_cc2640r2_sdk_x_xx_xx_xx.
  • Python 3.7 environment with all dependencies required by the RTLS Node Manager

Modify/Load the software

  • Load Board #1 + BOOSTXL-AoA with rtls_passive project:
    <SimpleLink CC2640R2 SDK> → examples → rtos → CC2640R2_LAUNCHXL → blestack → rtls_passive
  • Load Board #2 with rtls_slave project:
    <SimpleLink CC2640R2 SDK> → examples → rtos → CC2640R2_LAUNCHXL → blestack → rtls_slave
  • Load Board #3 with rtls_master project:
    <SimpleLink CC2640R2 SDK> → examples → rtos → CC2640R2_LAUNCHXL → blestack → rtls_master

Building the projects

Be sure to build both the stack and application before loading projects. Note that the rtls_passive uses the micro BLE-Stack setup as a connection monitor, therefore it does not have a separate stack project.

The following tasks will show you how to get started with data collection using the RTLS toolbox. This lab will focus on the RTLS Toolbox as a whole and the Python based PC environment. Subsequent labs will cover AoA or ToF specific topics.

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. Time of Flight gives you the distance from the receiver to the transmitter.

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

RTLS Toolbox Introduction

In the Localization Techniques, we discussed how multiple AoA nodes can combine angle information to perform triangulation and how multiple ToF nodes can combine distance information to perform trilateration. 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, and a single ToF node only produces one distance. These by nature, are ambiguous measurements. If there are at least two nodes providing AoA or ToF 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) or circles (ToF) 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 CC2640R2 LaunchPads while the grey box is a PC.

Time of Flight performs which of the following localization techniques?

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

RTLS Roles and Topology

Each node in an RTLS network utilizes the software components listed above in a different way 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.

RTLS Master

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

  • Share the connection parameters (access address, master sleep clock accuracy, and CRC init) with the PC.
  • Use the BLE link to share ToF and AoA parameters with the peripheral device.
  • Implements the ToF master role
  • The RTLS master does not send out AoA packets, but configures the slave to do so.

RTLS Slave

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

  • Advertises special string to be detected by rtls_master (covered in detail in Task 3)
  • BLE-Stack peripheral role
  • Implements ToF slave role
  • Sends data packets with AoA tone embedded using Constant Tone Extension (CTE)
  • Wireless/battery operated, not connected to PC

RTLS Passive

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

  • Implement ToF passive role
  • 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 master and slave

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 master role and a rtls_agent_cli server that translates UNPI commands to a socket interface that is used by the GUI Composer application running in the browser. In a final product, these algorithms may be implemented on an embedded device or even perhaps the RTLS master node.

The PC implements the following roles in Python:

  • UNPI master
  • COM port interface
  • Implementation of RTLS UNPI subsystem/command set
  • rtls_agent_cli server

The PC implements the following roles in GUI Composer/Javascript:

  • rtls_agent_cli client on localhost
  • Graphing and logging data from rtls_agent_cli
  • Parsing JSON objects to extract RTLS data
  • Issue commands to RTLS nodes via rtls_agent_cli to UNPI conversion
  • Enumerate devices
  • Distribute connection parameters to passive nodes

Quiz!

Is it possible to have multiple RTLS passive devices in a network? [quiz] v Yes, the PC program will distribute connection information to any device that has RTLS capabilities of passive. --> Since it is a passive role and not a participant any number of devices can listen to a connection so long as they have parameters. x No, only one device can track the connection between master and slave due to timing requirements. --> This is incorrect, any device with connection parameters (access address, crc init, etc) can follow the connection. [quiz]

Why would multiple passive devices be desirable? (select all that apply)

Task 1 – Running the RTLS Demo

In this task, we will use the pre-compiled rtls_agent_cli.exe located at
<SimpleLink CC2640R2 SDK> → tools → blestack → rtls_agent It is recommended to familiarize yourself with the readme in the same folder before starting.

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

  2. See the image below for a recommended layout of the nodes during evaluation, this is a 2D image where all devices are laying on a flat surface "pointing" as shown in the picture. In this case each node should be placed on a box so that it does not sit directly on the ground. Note that a desk environment generally has sub-optimal RF conditions and should be avoided.

  1. Build the projects and flash the LaunchPads as described in Modify/Load the software

  2. Run the pre-compiled rtls_agent_cli.exe, press 'a' to auto detect the LaunchPads connected to your PC. This will trigger the rtls_agent_cli.exe to send RTLS_CMD_IDENTIFY to each node.

    • Once the nodes are identified, press enter to start the agent.
    • Note that it is normal to receive NoRsp from ports if they are not programmed with RTLS software. Additionally, the COM ports used by the JTAG debugger will not respond, so NoRsp is expected there as well.
    • As long as RTLS_MASTER and RTLS_PASSIVE are detected, then it is okay to move on.
  3. Navigate to TI GUI Composer Gallery and search for "RTLS Monitor" Use version 0.9.3.

    • Select the RTLS_Monitor and click on it, this will open it in the browser view of GUI composer. (We recommend using the latest version of Chrome.)
  4. Click the connect button, this will connect to the rtls_agent_cli running on localhost.

  5. On success, you should be able to see graph window displaying sample data.

    • It will take ~30 seconds before the graph starts showing data.
    • We will cover more about what is happening during that time and what it takes to setup a RTLS network in Task 3.
    • If evaluating in ToF mode, you should not move the nodes until the GUI starts receiving data, because ToF is calibrating. Once the GUI starts graphing, it is okay to move the nodes around. The default calibration distance is 1m.

Automatic RTLS

If the "Automatic RTLS" checkbox is enabled then the GUI will connect to the first RTLS slave device it finds and will immediately start AoA or ToF. When this box is unchecked it is possible to scan and select the slave device manually via Bluetooth Address listed in the "Remote devices" combobox.

This is helpful if there are multiple RTLS slaves in the area.

Bonus

Define RTLS_LOCATIONING_AOA in all three example projects. (For rtls_master and rtls_slave, it is found in build_config.opt. For rtls_passive, define it in the preprocessor defines in the project properties.) Repeat the steps above. What changed?

Quiz!

What localization technique does the out of the box software employ?

Review the log, what devices are providing ToF distance measurements?

What is the format of the individual logs being sent between the rtls_agent_cli server and GUI Composer?

Assessing RTLS performance using the out of the box demos

The RTLS demos provided by TI are intended for evaluation use only and serve as a starting point for localization development. Better performance and accuracy can be achieved by utilizing the foundational demo toolkit provided by TI and developing advanced, customized algorithms to process AoA and ToF data. See the AoA and ToF specific labs for detailed information on demo results that can be used as a performance baseline as well as tips on improving performance.

The demo you have run, is not optimized for performance. It instead serves as a starting point for localization development. It is possible to achieve much better performance and accuracy than is seen here by developing advanced algorithms to process AoA and ToF data. TI is actively looking into algorithm development, and encourages their customers to do so too. This is a strong way of differentiating a localization product. See the AoA and ToF specific labs for more discussion and explanation of the results

Task 2 – Use RTLS Node Manager Directly

This task will bypass the GUI Composer application and interact directly with the RTLS Python APIs to collect data. 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 rtls_example.py template script described in the SDK.

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 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, if you already followed it, some steps may be redundant.
  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_cc2640r2_sdk_x_xx_xx_xx\tools\blestack\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 required Python Dependencies into the newly created virtual environment

    • Execute python -m pip --proxy www.proxy.com install -r requirements.dev.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 Python packages that are needed by the RTLS Python suite (these are listed in requirements.dev.txt).
  5. Open the examples/rtls_example.py, go to line 44, and edit the line my_nodes = [RTLSNode('COM17', 115200), RTLSNode('COM12', 115200)] to use the COM ports of the master and passive LaunchPads.

  6. Save the file and run it using python -u examples/rtls_example.py.

    • The script will detect if AoA or ToF mode is enabled, and then connect, pair, and start AoA or ToF based on which one is supported.
    • From here the code will print out localization data in a loop. It can be killed with the keyboard interrupt (ctrl+c)
    • See below for sample output (note that the addresses and COM ports will be different). After the expected output below the nodes will report AoA or ToF data depending on how they are configured.

Pip and requirements files

The RTLS and UNPI layers of the of the rtls_agent are now bundled as separate python packages. This is why there is a requirements.txt and a requirements.dev.txt. Using requirements.txt will trigger the contents of /rtls and /unpi to be installed as read-only. Alternatively requirements.dev.txt will set these packages up to be editable. It is recommended to requirements.dev.txt during development, and requirements.txt once the packages are finalized.

54:6C:0E:9F:12:27 TOF_MASTER, RTLS_MASTER
54:6C:0E:83:3A:A4 CM, TOF_PASSIVE, RTLS_PASSIVE
PASSIVE: 54:6C:0E:83:3A:A4 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_IDENTIFY", "payload": {"capabilities": {"CM": true, "AOA_TX": false, "AOA_RX": false, "TOF_SLAVE": false, "TOF_PASSIVE": true, "TOF_MASTER": false, "RTLS_SLAVE": false, "RTLS_MASTER": false, "RTLS_PASSIVE": true}, "identifier": "54:6C:0E:83:3A:A4"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_IDENTIFY", "payload": {"capabilities": {"CM": false, "AOA_TX": false, "AOA_RX": false, "TOF_SLAVE": false, "TOF_PASSIVE": false, "TOF_MASTER": true, "RTLS_SLAVE": false, "RTLS_MASTER": true, "RTLS_PASSIVE": false}, "identifier": "54:6C:0E:9F:12:27"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_SCAN", "payload": {"status": "RTLS_SUCCESS"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_SCAN", "payload": {"eventType": 0, "addrType": 0, "addr": "54:6C:0E:83:3F:3D", "rssi": -52, "dataLen": 11, "data": "0A:09:52:54:4C:53:53:6C:61:76:65"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_SCAN", "payload": {"status": "RTLS_SUCCESS"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_SCAN_STOP", "payload": {"status": "RTLS_SUCCESS"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_CONNECT", "payload": {"status": "RTLS_SUCCESS"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_CONN_PARAMS", "payload": {"accessAddress": 2480792542, "connInterval": 160, "hopValue": 8, "mSCA": 0, "currChan": 8, "chanMap": [255, 255, 255, 255, 31], "crcInit": 6270394}}
PASSIVE: 54:6C:0E:83:3A:A4 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_CONN_PARAMS", "payload": {"status": "RTLS_SUCCESS"}}
PASSIVE: 54:6C:0E:83:3A:A4 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_CONNECT", "payload": {"status": "RTLS_SUCCESS"}}

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

Task 3 – 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 make a copy of the default rtls_example.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
    /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 master 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.

RTLS Python Program Template

It is recommended to build RTLS based Python applications on top of the RTLSManager class within rtlsmanager.py. Together with the the RTLSNode class this forms the RTLS API set. The rtls_example.py from the previous task shows how perform basic initialization of the RTLSNodes and RTLSManager as well as setting up the networking and collecting localization data.

Here we highlight some of the important parts of the example:

First, construct an instance of the RTLSNode class for the master and passive device connected to PC. The first parameter is the user/UART COM port, and the second is the baudrate which defaults to 115200.

my_nodes = [RTLSNode('COM14', 115200), RTLSNode('COM47', 115200)]

Instantiate the RTLSManager will the newly created nodes, but do not connect a rtls_agent_cli.

manager = RTLSManager(my_nodes, wssport=None)

Create a subscriber instance, and add it to the manager. Essentially this tells the manager to add messages it receives to the example programs queue to be processed. Tell the manager to automatically distribute connection parameters to all of the passive nodes. Then start the manager.

subscriber = manager.create_subscriber()
# Tell the manager to automatically distribute connection parameters
manager.auto_params = True
# Start RTLS Node threads, Serial threads, and manager thread
manager.start()

Poll the capabilities of each node, assign master and create list of passives.

# Wait until nodes have responded to automatic identify command and get reference
# to single master RTLSNode and list of passive RTLSNode instances
master_node, passive_nodes, failed = manager.wait_identified()

Now that we have instantiated the classes, and got the capabilities of the nodes, we are ready to create the main loop.

while True:
    # Get messages from manager
    try:
        # Pend on activity from one of the nodes with a given timeout.
        identifier, msg_pri, msg = subscriber.pend(block=True, timeout=0.05).as_tuple()

        # We received data from one of the nodes
        # First determine which node sent the information.
        from_node = node_msg.identifier

        # Print the message as JSON, map MASTER and PASSIVE based on identifier (BD_ADDR)
        if sending_node in passive_nodes:
            print(f"PASSIVE: {identifier} --> {msg.as_json()}")
        else:
            print(f"MASTER: {identifier} --> {msg.as_json()}")

        # Perform further processing here.

    except queue.Empty:
        pass

RTLS Network Setup Procedure

The preceding code snippets in this task form a template program built on top of the RTLSNode and RTLSManager classes. 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.

In which Python file would you find the UNPI command definitions for the ToF?

The embedded examples in the SDK will synchronize localization measurements on a connection. In this case a measurement can be ToF interleaved with the BLE connection events or AoA measuring the CTE embedded in the connection packets. In both cases 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. The scanning device will inspect the advertising 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 master will look for the string {'R','T','L','S','S','l','a','v','e'} starting at the 3rd byte of the slave'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, if not, it will be discarded.

If the Node Manager wishes to form a connection to one of the devices in the scan results it can tell the rtls_master to do so 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 master node.

If the connection is successful, the RTLS_CMD_CONNECT response will be received with status of RTLS_SUCCESS. The RTLS examples do not consider a connection to be established between master and slave 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 master and slave. This can include AoA parameters or ToF parameters, or a command to enable AoA or ToF.

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 master and slave.

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.

Setting up RTLS Network in Python

Now that we understand the basics behind the RTLS network and how to set it up, let's review how the rtls_example.py sample app sets up the RTLS network. Note that the rtls_example.py will do some additional processing after the network is setup based on AoA or ToF. This is outside of the scope of this lab and will be covered in the following AoA and ToF labs respectively.

The commands required to setup a network belong to the RTLS UNPI subsystem and can be found in the ss_rtls.py file.

We will use the rtls_example.py as a starting point. From the sections above, we know that after the nodes are identified, we want to tell the master to scan.

You can send a command to an RTLSNode instance by calling the builder class associated with the command. For example, if you have an RTLSNode instance called master_node you can tell it to scan like so:

  master_node.rtls.scan()

After telling the node to scan, you will receive scan responses, the scan response packets are defined like this in Python:

struct = Struct(
    "eventType" / Int8ul,
    "addrType" / Enum(Int8ul),
    "addr" / NiceBytes(ReverseBytes(Byte[6])),
    "rssi" / Int8sl,
    "dataLen" / Int8ul,
    "data" / NiceBytes(Byte[this.dataLen])
)

In the last section, we covered that the rtls_master embedded node will only report scan responses from devices that advertise as RTLS slave. Therefore, there isn't much need to inspect the data payload unless desired. Instead, it is adequate to store the addr and addrType fields from the scan response as this is needed for the connect request. Assuming you have a node message like the one from rtls_example.py(see top of while True loop) then you can parse it like this:

if msg.command == 'RTLS_CMD_SCAN' and msg.type == 'AsyncReq':
    address = msg.payload.addr
    address_type = msg.payload.addrType

The code above will identify a scan result message then store the address and address type in a variable.

Asynchronous vs Synchronous commands in UNPI

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 BLE-Stack User's Guide for more information.

Now, we have collected a list of scan results and are ready to connect. First, we must wait until the rtls_master finishes scanning. When scanning is complete, a message of type RTLS_CMD_SCAN_STOP will be sent to the PC from the rtls_master node. By default, the code below will connect to the first device that has {'R','T','L','S','S','l','a','v','e'} as part of the advertising data.

If there may be multiple rtls_slave devices nearby, it is best to specify your specific via address. If there is only one rtls_slave nearby this step can be skipped.

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

0x546C0E833F3D
Advertising

Based on this, you should set the following global variable (remove the 0x, and separate by :)

slave_addr = '54:6C:0E:83:3F:3D' # Slave addr should be set here

Assuming again that we have a node message like the ones in rtls_example.py the code for connecting is as below.

# Once the scan has stopped and we have a valid address, then
# connect
if msg.command == 'RTLS_CMD_SCAN_STOP':
    if address is not None and address_type is not None and (slave_addr is None or slave_addr == address):
        master_node.rtls.connect(address_type, address)
    else:
        # If we didn't find the device, keep scanning.
        master_node.rtls.scan()

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 connection monitor will begin following the connection between master and slave. 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.

Equipped with the knowledge above, modify rtls_example.py to connect to your slave device. The expected output is show here:

54:6C:0E:9F:12:27 TOF_MASTER, RTLS_MASTER
54:6C:0E:83:3A:A4 CM, TOF_PASSIVE, RTLS_PASSIVE
PASSIVE: 54:6C:0E:83:3A:A4 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_IDENTIFY", "payload": {"capabilities": {"CM": true, "AOA_TX": false, "AOA_RX": false, "TOF_SLAVE": false, "TOF_PASSIVE": true, "TOF_MASTER": false, "RTLS_SLAVE": false, "RTLS_MASTER": false, "RTLS_PASSIVE": true}, "identifier": "54:6C:0E:83:3A:A4"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_IDENTIFY", "payload": {"capabilities": {"CM": false, "AOA_TX": false, "AOA_RX": false, "TOF_SLAVE": false, "TOF_PASSIVE": false, "TOF_MASTER": true, "RTLS_SLAVE": false, "RTLS_MASTER": true, "RTLS_PASSIVE": false}, "identifier": "54:6C:0E:9F:12:27"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_SCAN", "payload": {"status": "RTLS_SUCCESS"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_SCAN", "payload": {"status": "RTLS_SUCCESS"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_SCAN", "payload": {"eventType": 0, "addrType": 0, "addr": "54:6C:0E:83:3F:3D", "rssi": -54, "dataLen": 11, "data": "0A:09:52:54:4C:53:53:6C:61:76:65"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_SCAN_STOP", "payload": {"status": "RTLS_SUCCESS"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_CONNECT", "payload": {"status": "RTLS_SUCCESS"}}
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_CONN_PARAMS", "payload": {"accessAddress": 1387477511, "connInterval": 160, "hopValue": 14, "mSCA": 0, "currChan": 14, "chanMap": [255, 255, 255, 255, 31], "crcInit": 212647}}
PASSIVE: 54:6C:0E:83:3A:A4 --> {"originator": "Nwp", "type": "SyncRsp", "subsystem": "RTLS", "command": "RTLS_CMD_CONN_PARAMS", "payload": {"status": "RTLS_SUCCESS"}}
PASSIVE: 54:6C:0E:83:3A:A4 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_CONNECT", "payload": {"status": "RTLS_SUCCESS"}}
Connection established, all systems go!!!
MASTER: 54:6C:0E:9F:12:27 --> {"originator": "Nwp", "type": "AsyncReq", "subsystem": "RTLS", "command": "RTLS_CMD_CONNECT", "payload": {"status": "RTLS_SUCCESS"}}
Connection established, all systems go!!!

Solution is below.

import queue
import time
from rtls import RTLSManager, RTLSNode

# Un-comment the below to get raw serial transaction logs
# import logging, sys
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG,
#                     format='[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s')


if __name__ == '__main__':
    # Initialize, but don't start RTLS Nodes to give to the RTLSManager
    my_nodes = [RTLSNode('COM5', 115200), RTLSNode('COM8', 115200)]

    # Initialize references to the connected devices
    master_node = None
    passive_nodes = []
    address = None
    address_type = None
    # If slave addr is None, the script will connect to the first RTLS slave
    # that it found. If you wish to connect to a specific device
    # (in the case of multiple RTLS slaves) then you may specify the address
    # explicitly as given in the comment to the right
    slave_addr = None #'54:6C:0E:83:3F:3D'

    # Initialize manager reference, because on Exception we need to stop the manager to stop all the threads.
    manager = None
    try:
        # Start an RTLSManager instance without WebSocket server enabled
        manager = RTLSManager(my_nodes, websocket_port=None)
        # Create a subscriber object for RTLSManager messages
        subscriber = manager.create_subscriber()
        # Tell the manager to automatically distribute connection parameters
        manager.auto_params = True
        # Start RTLS Node threads, Serial threads, and manager thread
        manager.start()

        # Wait until nodes have responded to automatic identify command and get reference
        # to single master RTLSNode and list of passive RTLSNode instances
        master_node, passive_nodes, failed = manager.wait_identified()

        if len(failed):
            print(f"ERROR: {len(failed)} nodes could not be identified. Are they programmed?")

        # Exit if no master node exists
        if not master_node:
            raise RuntimeError("No RTLS Master node connected")

        # Combined list for lookup
        all_nodes = passive_nodes + [master_node]

        #
        # At this point the connected devices are initialized and ready
        #

        # Display list of connected devices
        print(f"{master_node.identifier} {', '.join([cap for cap, available in master_node.capabilities.items() if available])}")

        # Iterate over Passives and detect their capabilities
        for pn in passive_nodes:
            print(f"{pn.identifier} {', '.join([cap for cap, available in pn.capabilities.items() if available])}")

        # Send an example command to each of them, from commands listed at the bottom of rtls/ss_rtls.py
        for n in all_nodes:
            n.rtls.identify()

        while True:
            # Get messages from manager
            try:
                identifier, msg_pri, msg = subscriber.pend(block=True, timeout=0.05).as_tuple()

                # Get reference to RTLSNode based on identifier in message
                sending_node = manager[identifier]

                if sending_node in passive_nodes:
                    print(f"PASSIVE: {identifier} --> {msg.as_json()}")
                else:
                    print(f"MASTER: {identifier} --> {msg.as_json()}")

                # After identify is received, we start scanning
                if msg.command == 'RTLS_CMD_IDENTIFY':
                      master_node.rtls.scan()

                # Once we start scaning, we will save the address of the
                # last scan response
                if msg.command == 'RTLS_CMD_SCAN' and msg.type == 'AsyncReq':
                    address = msg.payload.addr
                    address_type = msg.payload.addrType

                # Once the scan has stopped and we have a valid address, then
                # connect
                if msg.command == 'RTLS_CMD_SCAN_STOP':
                    if address is not None and address_type is not None and (slave_addr is None or slave_addr == address):
                        master_node.rtls.connect(address_type, address)
                    else:
                        # If we didn't find the device, keep scanning.
                        master_node.rtls.scan()

                # Once we are connected, then we can do stuff
                if msg.command == 'RTLS_CMD_CONNECT' and msg.type == 'AsyncReq' and msg.payload.status == 'RTLS_SUCCESS':
                    print ("Connection established, all systems go!!!")

            except queue.Empty:
                pass

    finally:
        if manager:
            manager.stop()

You made it to the end

Excellent work!

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