Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

12-twr: initial import #249

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions 12-twr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## Goal: Verify UWB Two Way Ranging

Task #01 - TWR Aloha example
==========================

### Description

TWR aloha example is functional and reports distance estimation values within +-20cm when in LOS conditions.

### Testing procedure: Local

- On two or more devices build and flash the `examples/twr_aloha` application with the following command

$ make -C examples/twr_aloha flash term

- Choose two devices, configure one as a responder, and recover its short address

> twr lst on
[twr]: start listening
> ifconfig
Iface 3 HWaddr: 02:95 Channel: 5 NID: DE:CA

Long HWaddr: 08:2B:31:07:CC:74:02:95
TX-Power: 8.5dBm TC-PGdelay: 0xb5

- The second device will be the initiator, perform a SS-TWR exchange against the responder,
perform 100 exchanges with 100ms intervals, and parse the average `d_cm`

> twr req -c 100 -i 100 -p ss 02:95
[twr]: start ranging
{"t": 23721, "src": "8E:2B", "dst": "02:95", "d_cm": 380}
{"t": 23821, "src": "8E:2B", "dst": "02:95", "d_cm": 371}
{"t": 23921, "src": "8E:2B", "dst": "02:95", "d_cm": 375}


- With the same initiator, perform a DS-TWR exchange against the responder
perform 100 exchanges with 100ms intervals, and parse the average `d_cm`

> twr req -c 100 -i 100 -p ds 02:95
> [twr]: start ranging
{"t": 23721, "src": "8E:2B", "dst": "02:95", "d_cm": 380}
{"t": 23821, "src": "8E:2B", "dst": "02:95", "d_cm": 371}
{"t": 23921, "src": "8E:2B", "dst": "02:95", "d_cm": 375}

### Testing procedure: IoT-LAB

Not all devices on IoT-LAB are in LOS conditions, and the antennas are not facing
in the same direction, this can yield to higher than expected errors in the distance
values. For those cases a dataset is provided with the TWR results between all IoT-LAB
devices on the Lille site. These values used Decawave PANS R2 firmware. Pairs of nodes
where Decawave reports mean errors above 30cm should be ignored, since not considered
LOS, see heatmap below or provided [dataset](./static/data_full.csv)

|![Decawave-Lille](./static/deca_mean_err_heatmap.jpg)|
|:---------------------------------------------------------------------:|
| Distance (cm) mean error |
| Decawave PANS R2- distance - IoT-LAB lille |

### Result

When in LOS devices report distance values within +-20cm
18,186 changes: 18,186 additions & 0 deletions 12-twr/static/data_full.csv

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions 12-twr/static/data_mean_err.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
src,02:95,04:20,08:A6,0B:A1,0C:28,12:2E,48:27,80:89,8E:2B,D1:B6,D4:B6,D6:B0,DB:27,DC:19
02:95,0,5,1,4,6,33,14,2,36,2,3,40,7,1
04:20,5,0,5,5,17,2,1,9,1,4,3,5,3,11
08:A6,1,6,0,4,8,8,13,14,3,7,4,4,3,9
0B:A1,4,5,4,0,1,5,12,14,6,3,6,50,1,18
0C:28,7,19,7,2,0,7,8,5,12,1,6,11,7,24
12:2E,33,3,10,5,5,0,3,1,7,15,1,9,10,13
48:27,14,1,13,11,9,4,0,19,5,1,2,13,7,2
80:89,2,9,14,15,6,2,19,0,12,4,7,12,5,2
8E:2B,38,2,3,5,11,7,4,12,0,10,1,3,3,4
D1:B6,2,3,7,3,1,15,1,4,10,0,6,15,1,2
D4:B6,2,2,5,5,5,2,1,6,1,8,0,17,2,1
D6:B0,33,5,4,50,12,9,13,12,3,15,16,0,6,1
DB:27,7,3,4,1,7,10,8,5,2,1,4,6,0,10
DC:19,1,10,9,17,24,12,2,2,4,1,1,1,9,0
Binary file added 12-twr/static/deca_mean_err_heatmap.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
126 changes: 126 additions & 0 deletions 12-twr/test_spec12.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from pathlib import Path
import os
import pytest
import math
import pandas as pd
from statistics import mean
from typing import List, Tuple, Union

from riotctrl_shell.twr import TwrCmd, TwrRequestParser, TwrIfconfigParser
from riotctrl_shell.sys import Reboot
from testutils.iotlab import IoTLABExperiment


DECAWAVE_DWM_SIMPLE_LILLE_CSV = os.path.join(
os.path.dirname(Path(__file__).resolve()), "static", "data_mean_err.csv"
)
APP = 'examples/twr_aloha'
pytestmark = pytest.mark.rc_only()

DEFAULT_TWR_ITVL = 100
DEFAULT_TWR_COUNT = 50
DEFAULT_TWR_TIMEOUT = DEFAULT_TWR_ITVL * DEFAULT_TWR_COUNT + 5 * 1000

DISTANCE_ERROR_LOS = 30


class Shell(Reboot, TwrCmd):
"""Convenience class inheriting from the Reboot and TwrCmd shell"""

_netif = {
"netif": None,
"hwaddr": None,
"hwaddr64": None,
"panid": None,
"channel": None,
}

def parse_netif(self):
parser = TwrIfconfigParser()
self._netif = parser.parse(self.ifconfig())

def hwaddr(self):
return self._netif["hwaddr"]

def netif(self):
return self._netif["netif"]

def hwaddr64(self):
return self._netif["hwaddr64"]

def panid(self):
return self._netif["panid"]

def channel(self):
return self._netif["channel"]


def _get_node_position(node: Shell) -> Union[List[float], Tuple[float, ...]]:
"""Returns the position of an IoTLAB node"""
infos = IoTLABExperiment.get_nodes_position(node.riotctrl.env['IOTLAB_EXP_ID'])
for info in infos:
if info['network_address'] == node.riotctrl.env['IOTLAB_NODE']:
return info['position']
return (None, None, None)


def _get_nodes_distance_cm(node_0: Shell, node_1: Shell):
"""Returns euclidean distance between two IoTLAB nodes in cm"""
node_0_pos = _get_node_position(node_0)
node_1_pos = _get_node_position(node_1)
# calculate euclidean distance
d_m = math.sqrt(
math.pow(node_0_pos[0] - node_1_pos[0], 2)
+ math.pow(node_0_pos[1] - node_1_pos[1], 2)
+ math.pow(node_0_pos[2] - node_1_pos[2], 2)
)
return round(d_m * 100)


@pytest.mark.iotlab_creds
@pytest.mark.flaky(reruns=3, reruns_delay=30)
# nodes passed to riot_ctrl fixture
@pytest.mark.parametrize(
'nodes, iotlab_site, proto',
[
pytest.param(['dwm1001', 'dwm1001'], "lille", "ss"),
pytest.param(['dwm1001', 'dwm1001'], "lille", "ds"),
],
indirect=['nodes', 'iotlab_site'],
)
def test_task01(riot_ctrl, proto):
nodes = (
riot_ctrl(0, APP, Shell),
riot_ctrl(1, APP, Shell),
)

# load Decawave distance error
df = pd.read_csv(DECAWAVE_DWM_SIMPLE_LILLE_CSV, index_col=0)

for node in nodes:
node.parse_netif()

for initiator, responder in zip(nodes, nodes[::-1]):
responder.twr_listen(on=True)
out = initiator.twr_request(
addr=responder.hwaddr(),
count=DEFAULT_TWR_COUNT,
itvl=DEFAULT_TWR_ITVL,
proto=proto,
timeout=DEFAULT_TWR_TIMEOUT,
)
req_parser = TwrRequestParser()
d_cm = req_parser.parse(out)

assert mean(d_cm) > 0
# If ran on IoTLAB also check that error is within margin
if 'IOTLAB_NODE' in responder.riotctrl.env:
d_error_cm = abs(mean(d_cm) - _get_nodes_distance_cm(responder, initiator))
# sanity check distance error should not be above 1m
assert d_error_cm < 100
deca_d_error_cm = df[initiator.hwaddr()][responder.hwaddr()]
# if Decawave PANS R2 error is under 30cm consider it ~LOS conditions,
# and therefore RIOT should show a similar error
if deca_d_error_cm < DISTANCE_ERROR_LOS:
assert d_error_cm < DISTANCE_ERROR_LOS
responder.twr_listen(on=False)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pytest-rerunfailures
riotctrl
scapy
paho-mqtt
pandas
20 changes: 20 additions & 0 deletions testutils/iotlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class IoTLABExperiment:
'arduino-zero': {'name': 'arduino-zero', 'radio': 'xbee'},
'b-l072z-lrwan1': {'name': 'st-lrwan1', 'radio': 'sx1276'},
'b-l475e-iot01a': {'name': 'st-iotnode', 'radio': 'multi'},
'dwm1001': {'name': 'dwm1001', 'radio': 'dw1000'},
'firefly': {'name': 'firefly', 'radio': 'multi'},
'frdm-kw41z': {'name': 'frdm-kw41z', 'radio': 'multi'},
'iotlab-a8-m3': {'name': 'a8', 'radio': 'at86rf231'},
Expand Down Expand Up @@ -180,3 +181,22 @@ def _get_nodes(self):
"""Return all nodes reserved by the experiment"""
ret = get_experiment(Api(*self.user_credentials()), self.exp_id)
return ret['nodes']

@staticmethod
def get_nodes_position(exp_id):
"""Return nodes positions"""
info = []
if exp_id:
ret = get_experiment(Api(*get_user_credentials()), exp_id, 'nodes')
for item in ret['items']:
info.append(
{
'network_address': item['network_address'],
'position': (
float(item['x']),
float(item['y']),
float(item['z']),
),
}
)
return info
47 changes: 47 additions & 0 deletions testutils/tests/test_iotlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,50 @@ def test_start_error(monkeypatch):
ctrls[0].env.pop("BOARD")
with pytest.raises(ValueError):
exp.start()


@pytest.mark.parametrize(
"exp_nodes",
[
(
[
{
"network_address": "dwm1001-13.lille.iot-lab.info",
"x": "25.71",
"y": "9.39",
"z": "9.22",
},
{
"network_address": "dwm1001-11.lille.iot-lab.info",
"x": "27.51",
"y": "7.1",
"z": "9.51",
},
]
)
],
)
def test_get_nodes_position(monkeypatch, exp_nodes):
monkeypatch.setattr(
testutils.iotlab.IoTLABExperiment,
"user_credentials",
lambda cls: ("user", "password"),
)
monkeypatch.setattr(testutils.iotlab, "Api", lambda user, password: None)
monkeypatch.setattr(
testutils.iotlab,
"submit_experiment",
lambda api, name, duration, resources: {"id": 12345},
)
monkeypatch.setattr(
testutils.iotlab,
"get_experiment",
lambda api, exp_id, option: {"items": exp_nodes},
)
info = testutils.iotlab.IoTLABExperiment.get_nodes_position(12345)
assert info[0]['network_address'] == "dwm1001-13.lille.iot-lab.info"
assert info[0]['position'] == (25.71, 9.39, 9.22)
assert info[1]['network_address'] == "dwm1001-11.lille.iot-lab.info"
assert info[1]['position'] == (27.51, 7.1, 9.51)
info = testutils.iotlab.IoTLABExperiment.get_nodes_position(None)
assert len(info) == 0