From ef17e914a917f6c23162d12c75cbcf9cfad1a01b Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 7 Mar 2018 14:41:01 -0800 Subject: [PATCH] [tests] adding `toranj` test framework (#2559) This commit adds the base implementation of `toranj`, a test framework for OpenThread and `wpantund`. - It enables testing of combined behavior of OpenThread (in NCP mode), spinel interface, and `wpantund` driver on linux. - It can be used to simulate multiple nodes forming complex network topologies, testing network interactions between many nodes (e.g., IPv6 traffic exchanges). This commit also sets up the new test-cases to run as part of travis pull request validation in OpenThread GitHub projects. --- .travis.yml | 8 + .travis/before_install.sh | 23 + .travis/script.sh | 4 + tests/toranj/README.md | 369 ++++++++ tests/toranj/openthread-core-toranj-config.h | 188 ++++ tests/toranj/start.sh | 81 ++ tests/toranj/test-001-get-set.py | 157 ++++ tests/toranj/test-002-form.py | 111 +++ tests/toranj/test-003-join.py | 91 ++ tests/toranj/test-004-scan.py | 101 +++ tests/toranj/test-005-discover-scan.py | 85 ++ .../test-006-traffic-router-end-device.py | 106 +++ .../toranj/test-007-traffic-router-sleepy.py | 113 +++ tests/toranj/test-008-permit-join.py | 81 ++ tests/toranj/test-nnn-template.py | 63 ++ tests/toranj/wpan.py | 801 ++++++++++++++++++ 16 files changed, 2382 insertions(+) create mode 100644 tests/toranj/README.md create mode 100644 tests/toranj/openthread-core-toranj-config.h create mode 100755 tests/toranj/start.sh create mode 100644 tests/toranj/test-001-get-set.py create mode 100644 tests/toranj/test-002-form.py create mode 100644 tests/toranj/test-003-join.py create mode 100644 tests/toranj/test-004-scan.py create mode 100644 tests/toranj/test-005-discover-scan.py create mode 100644 tests/toranj/test-006-traffic-router-end-device.py create mode 100644 tests/toranj/test-007-traffic-router-sleepy.py create mode 100644 tests/toranj/test-008-permit-join.py create mode 100644 tests/toranj/test-nnn-template.py create mode 100644 tests/toranj/wpan.py diff --git a/.travis.yml b/.travis.yml index e47e55241..239e4a86b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,11 @@ dist: trusty before_install: - .travis/before_install.sh +before_script: + - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then + sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; + fi + script: - .travis/script.sh @@ -118,3 +123,6 @@ matrix: - env: BUILD_TARGET="posix-ncp-spi" VERBOSE=1 os: linux compiler: gcc + - env: BUILD_TARGET="toranj-test-framework" VERBOSE=1 + os: linux + compiler: gcc diff --git a/.travis/before_install.sh b/.travis/before_install.sh index a9026991f..6811aad19 100755 --- a/.travis/before_install.sh +++ b/.travis/before_install.sh @@ -97,6 +97,29 @@ cd /tmp || die [ $BUILD_TARGET != posix -o $CC != clang ] || { sudo apt-get install clang || die } + + [ $BUILD_TARGET != toranj-test-framework ] || { + pip install --upgrade pip || die + + # packages for wpantund + sudo apt-get install dbus || die + sudo apt-get install gcc g++ libdbus-1-dev || die + sudo apt-get install autoconf-archive || die + sudo apt-get install bsdtar || die + sudo apt-get install libtool || die + sudo apt-get install libglib2.0-dev || die + sudo apt-get install libboost-dev || die + sudo apt-get install libboost-signals-dev || die + + # clone and build wpantund + git clone --depth=1 --branch=master https://github.com/openthread/wpantund.git + cd wpantund || die + ./bootstrap.sh || die + ./configure || die + sudo make install -j 8 || die + cd .. || die + } + } [ $TRAVIS_OS_NAME != osx ] || { diff --git a/.travis/script.sh b/.travis/script.sh index ff448db07..2dd4bee50 100755 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -370,3 +370,7 @@ set -x ./bootstrap || die COVERAGE=1 NODE_TYPE=ncp-sim make -f examples/Makefile-posix check || die } + +[ $BUILD_TARGET != toranj-test-framework ] || { + ./tests/toranj/start.sh +} diff --git a/tests/toranj/README.md b/tests/toranj/README.md new file mode 100644 index 000000000..119c1c06b --- /dev/null +++ b/tests/toranj/README.md @@ -0,0 +1,369 @@ +# `toranj` test framework + +`toranj` is a test framework for OpenThread and `wpantund`. + +- It enables testing of combined behavior of OpenThread (in NCP mode), spinel interface, and `wpantund` driver on linux. +- It can be used to simulate multiple nodes forming complex network topologies. +- It allows testing of network interactions between many nodes (IPv6 traffic exchanges). + +`toranj` is developed in Python. `toranj` runs wpantund natively with OpenThread in NCP mode on POSIX simulation platform. +`toranj` tests will run as part of travis pull request validation in OpenThread and/or `wpantund` GitHub projects. + + +## Setup + +`toranj` requires wpantund to be installed. Please follow [`wpantund` installation guide](https://github.com/openthread/wpantund/blob/master/INSTALL.md#wpantund-installation-guide). + +To run all tests, `start` script can be used. This script will build OpenThread with proper configuration options and starts running all test. + +```bash + cd tests/toranj/ # from OpenThread repo root + ./start.sh +``` + +Each test-case has its own script following naming model `test-nnn-name.py` (e.g., `test-001-get-set.py`). + +To run a specific test + +```bash + sudo python test-001-get-set.py +``` + +## `toranj` Components + +`wpan` python module defines the `toranj` test components. + +### `wpan.Node()` Class + +`wpan.Node()` class creates a Thread node instance. It creates a sub-process to run `wpantund` and OpenThread, and provides methods to control the node. + + +```python +>>> import wpan +>>> node1 = wpan.Node() +>>> node1 +Node (index=1, interface_name=wpan1) +>>> node2 = wpan.Node() +>>> node2 +Node (index=2, interface_name=wpan2) +``` +Note: You may need to run as `sudo` to allow `wpantund` to create tunnel interface (i.e., use `sudo python`). + +### `wpan.Node` methods providing `wpanctl` commands + +`wpan.Node()` provides methods matching all `wpanctl` commands. + +- Get the value of a `wpantund` property, set the value, or add/remove value to/from a list based property: + +```python + node.get(prop_name) + node.set(prop_name, value, binary_data=False) + node.add(prop_name, value, binary_data=False) + node.remove(prop_name, value, binary_data=False) +``` + +Example: +```python +>>> node.get(wpan.WPAN_NAME) +'"test-network"' +>>> node.set(wpan.WPAN_NAME, 'my-network') +>>> node.get(wpan.WPAN_NAME) +'"my-network"' +>>> node.set(wpan.WPAN_KEY, '65F2C35C7B543BAC1F3E26BB9F866C1D', binary_data=True) +>>> node.get(wpan.WPAN_KEY) +'[65F2C35C7B543BAC1F3E26BB9F866C1D]' +``` + +- Common network operations: +```python + node.reset() # Reset the NCP + node.status() # Get current status + node.leave() # Leave the current network, clear all persistent data + + # Form a network in given channel (if none given use a random one) + node.form(name, channel=None) + + # Join a network with given info. + # node_type can be JOIN_TYPE_ROUTER, JOIN_TYPE_END_DEVICE, JOIN_TYPE_SLEEPY_END_DEVICE + node.join(name, channel=None, node_type=None, panid=None, xpanid=None) +``` + +Example: +```python +>>> result = node.status() +>>> print result +wpan1 => [ + "NCP:State" => "offline" + "Daemon:Enabled" => true + "NCP:Version" => "OPENTHREAD/20170716-00460-ga438cef0c-dirty; NONE; Feb 12 2018 11:47:01" + "Daemon:Version" => "0.08.00d (0.07.01-191-g63265f7; Feb 2 2018 18:05:47)" + "Config:NCP:DriverName" => "spinel" + "NCP:HardwareAddress" => [18B4300000000001] +] +>>> +>>> node.form("test-network", channel=12) +'Forming WPAN "test-network" as node type "router"\nSuccessfully formed!' +>>> +>>> print node.status() +wpan1 => [ + "NCP:State" => "associated" + "Daemon:Enabled" => true + "NCP:Version" => "OPENTHREAD/20170716-00460-ga438cef0c-dirty; NONE; Feb 12 2018 11:47:01" + "Daemon:Version" => "0.08.00d (0.07.01-191-g63265f7; Feb 2 2018 18:05:47)" + "Config:NCP:DriverName" => "spinel" + "NCP:HardwareAddress" => [18B4300000000001] + "NCP:Channel" => 12 + "Network:NodeType" => "leader" + "Network:Name" => "test-network" + "Network:XPANID" => 0xA438CF5973FD86B2 + "Network:PANID" => 0x9D81 + "IPv6:MeshLocalAddress" => "fda4:38cf:5973:0:b899:3436:15c6:941d" + "IPv6:MeshLocalPrefix" => "fda4:38cf:5973::/64" + "com.nestlabs.internal:Network:AllowingJoin" => false +] +``` + +- Scan: +```python + node.active_scan(channel=None) + node.energy_scan(channel=None) + node.discover_scan(channel=None, joiner_only=False, enable_filtering=False, panid_filter=None) + node.permit_join(duration_sec=None, port=None, udp=True, tcp=True) +``` + +- On-mesh prefixes and off-mesh routes: +```python + node.config_gateway(prefix, default_route=False) + node.add_route(route_prefix, prefix_len_in_bytes=None, priority=None) + node.remove_route(route_prefix, prefix_len_in_bytes=None, priority=None) +``` + +A direct `wpanctl` command can be issued using `node.wpanctl(command)` with a given `command` string. + +`wpan` module provides variables for different `wpantund` properties. Some commonly used are: + +- Network/NCP properties: + WPAN_STATE, WPAN_NAME, WPAN_PANID, WPAN_XPANID, WPAN_KEY, WPAN_CHANNEL, WPAN_HW_ADDRESS, + WPAN_EXT_ADDRESS, WPAN_POLL_INTERVAL, WPAN_NODE_TYPE, WPAN_ROLE, WPAN_PARTITION_ID + +- IPv6 Addresses: + WPAN_IP6_LINK_LOCAL_ADDRESS, WPAN_IP6_MESH_LOCAL_ADDRESS, WPAN_IP6_MESH_LOCAL_PREFIX, + WPAN_IP6_ALL_ADDRESSES, WPAN_IP6_MULTICAST_ADDRESSES + +- Thread Properties: + WPAN_THREAD_RLOC16, WPAN_THREAD_ROUTER_ID, WPAN_THREAD_LEADER_ADDRESS, + WPAN_THREAD_LEADER_ROUTER_ID, WPAN_THREAD_LEADER_WEIGHT, WPAN_THREAD_LEADER_NETWORK_DATA, + + WPAN_THREAD_CHILD_TABLE, WPAN_THREAD_CHILD_TABLE_ADDRESSES, WPAN_THREAD_NEIGHBOR_TABLE, + WPAN_THREAD_ROUTER_TABLE + + +Method `join_node()` can be used by a node to join another node: + +```python + # `node1` joining `node2`'s network as a router + node1.join_node(node2, node_type=JOIN_TYPE_ROUTER) +``` + +Method `whitelist_node()` can be used to add a given node to the whitelist of the device and enables white-listing: + +```python + # `node2` is added to the whitelist of `node1` and white-listing is enabled on `node1` + node1.whitelist_node(node2) +``` + +#### Example (simple 3-node topology) + +Script below shows how to create a 3-node network topology with `node1` and `node2` being routers, and `node3` an end-device connected to `node2`: +```python +>>> import wpan +>>> node1 = wpan.Node() +>>> node2 = wpan.Node() +>>> node3 = wpan.Node() + +>>> wpan.Node.init_all_nodes() + +>>> node1.form("test-PAN") +'Forming WPAN "test-PAN" as node type "router"\nSuccessfully formed!' + +>>> node1.whitelist_node(node2) +>>> node2.whitelist_node(node1) + +>>> node2.join_node(node1, wpan.JOIN_TYPE_ROUTER) +'Joining "test-PAN" C474513CB487778D as node type "router"\nSuccessfully Joined!' + +>>> node3.whitelist_node(node2) +>>> node2.whitelist_node(node3) + +>>> node3.join_node(node2, wpan.JOIN_TYPE_END_DEVICE) +'Joining "test-PAN" C474513CB487778D as node type "end-device"\nSuccessfully Joined!' + +>>> print node2.get(wpan.WPAN_THREAD_NEIGHBOR_TABLE) +[ + "EAC1672C3EAB30A4, RLOC16:9401, LQIn:3, AveRssi:-20, LastRssi:-20, Age:30, LinkFC:6, MleFC:0, IsChild:yes, RxOnIdle:yes, FFD:yes, SecDataReq:yes, FullNetData:yes" + "A2042C8762576FD5, RLOC16:dc00, LQIn:3, AveRssi:-20, LastRssi:-20, Age:5, LinkFC:21, MleFC:18, IsChild:no, RxOnIdle:yes, FFD:yes, SecDataReq:no, FullNetData:yes" +] +>>> print node1.get(wpan.WPAN_THREAD_NEIGHBOR_TABLE) +[ + "960947C53415DAA1, RLOC16:9400, LQIn:3, AveRssi:-20, LastRssi:-20, Age:18, LinkFC:15, MleFC:11, IsChild:no, RxOnIdle:yes, FFD:yes, SecDataReq:no, FullNetData:yes" +] + +``` + +### IPv6 Message Exchange + +`toranj` allows a test-case to define traffic patterns (IPv6 message exchange) between different nodes. Message exchanges (tx/rx) are prepared and then an async rx/tx operation starts. The success and failure of tx/rx operations can then be verified by the test case. + +`wpan.Node` method `prepare_tx()` prepares a UDP6 transmission from a node. + +```python + node1.prepare_tx(src, dst, data, count) +``` + +- `src` and `dst` can be + - either a string containing an IPv6 address + - or a tuple (ipv6 address as string, port). if no port is given, a random port number is used. + +- `data` can be + - either a string containing the message to be sent, + - or an int indicating size of the message (a random message with the given length will be generated). + +- `count` gives number of times the message will be sent (default is 1). + +`prepare_tx` returns a `wpan.AsyncSender` object. The sender object can be used to check success/failure of tx operation. + +`wpan.Node` method `prepare_rx()` prepares a node to listen for UDP messages from a sender. + +```python + node2.prepare_rx(sender) +``` + +- `sender` should be an `wpan.AsyncSender` object returned from previous `prepare_tx`. +- `prepare_rx()` returns a `wpan.AsyncReceiver` object to help test to check success/failure of rx operation. + +After all exchanges are prepared, static method `perform_async_tx_rx()` should be used to start all previously prepared rx and tx operations. + +```python + wpan.Node.perform_async_tx_rx(timeout) +``` + +- `timeout` gives amount of time (in seconds) to wait for all operations to finish. (default is 20 seconds) + +After `perform_async_tx_rx()` is done, the `AsyncSender` and `AsyncReceiver` objects can check if operations were successful (using property `was_successful`) + +#### Example + +Sending 10 messages containing `"Hello there!"` from `node1` to `node2` using their mesh-local addresses: +```python +# `node1` and `node2` are already joined and are part of the same Thread network. + +# Get the mesh local addresses +>>> mladdr1 = node1.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] # remove `"` from start/end of string +>>> mladdr2 = node2.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] + +>>> print (mladdr1, mladdr2) +('fda4:38cf:5973:0:b899:3436:15c6:941d', 'fda4:38cf:5973:0:5836:fa55:7394:6d4b') + +# prepare a `sender` and corresponding `recver` +>>> sender = node1.prepare_tx((mladdr1, 1234), (mladdr2, 2345), "Hello there!", 10) +>>> recver = node2.prepare_rx(sender) + +# perform async message transfer +>>> wpan.Node.perform_async_tx_rx() + +# check status of `sender` and `recver` +>>> sender.was_successful +True +>>> recver.was_successful +True + +# `sender` or `recver` can provide info about the exchange + +>>> sender.src_addr +'fda4:38cf:5973:0:b899:3436:15c6:941d' +>>> sender.src_port +1234 +>>> sender.dst_addr +'fda4:38cf:5973:0:5836:fa55:7394:6d4b' +>>> sender.dst_port +2345 +>>> sender.msg +'Hello there!' +>>> sender.count +10 + +# get all received msg by `recver` as list of tuples `(msg, (src_address, src_port))` +>>> recver.all_rx_msg +[('Hello there!', ('fda4:38cf:5973:0:b899:3436:15c6:941d', 1234)), ... ] +``` + +### Logs and Verbose mode + +Every `wpan.Node()` instance will save its corresponding `wpantund` logs. By default the logs are saved in a file +`wpantun-log.log`. By setting `wpan.Node__TUND_LOG_TO_FILE` to `False` the logs are written to `stdout` as the test-cases are executed. + +When `start.sh` script is used to run all test-cases, if any test fails, to help with debugging of the issue, the last 30 lines of `wpantund` logs of every node involved in the test-case is dumped to `stdout`. + +A `wpan.Node()` instance can also provide additional logs and info as the test-cases are run (verbose mode). By default this is disabled. It can be enabled for a node instance when it is created: +```python + node = wpan.Node(verbose=True) # `node` instance will provide extra logs. +``` + +Alternatively, `wpan.Node._VERBOSE` settings can be changed to enable verbose logging for all nodes. + +Here is example of small test script and its corresponding log output with `verbose` mode enabled: + +```python +node1 = wpan.Node(verbose=True) +node2 = wpan.Node(verbose=True) + +wpan.Node.init_all_nodes() + +node1.form("toranj-net") +node2.active_scan() + +node2.join_node(node1) +verify(node2.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) + +lladdr1 = node1.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] +lladdr2 = node2.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] + +sender = node1.prepare_tx(lladdr1, lladdr2, 20) +recver = node2.prepare_rx(sender) + +wpan.Node.perform_async_tx_rx() + +``` +``` +$ Node1.__init__() cmd: /usr/local/sbin/wpantund -o Config:NCP:SocketPath "system:../../examples/apps/ncp/ot-ncp-ftd 1" -o Config:TUN:InterfaceName wpan1 -o Config:NCP:DriverName spinel -o Daemon:SyslogMask "all -debug" +$ Node2.__init__() cmd: /usr/local/sbin/wpantund -o Config:NCP:SocketPath "system:../../examples/apps/ncp/ot-ncp-ftd 2" -o Config:TUN:InterfaceName wpan2 -o Config:NCP:DriverName spinel -o Daemon:SyslogMask "all -debug" +$ Node1.wpanctl('leave') -> 'Leaving current WPAN. . .' +$ Node2.wpanctl('leave') -> 'Leaving current WPAN. . .' +$ Node1.wpanctl('form "toranj-net"'): + Forming WPAN "toranj-net" as node type "router" + Successfully formed! +$ Node2.wpanctl('scan'): + | Joinable | NetworkName | PAN ID | Ch | XPanID | HWAddr | RSSI + ---+----------+--------------------+--------+----+------------------+------------------+------ + 1 | NO | "toranj-net" | 0x9DEB | 16 | 8CC6CFC810F23E1B | BEECDAF3439DC931 | -20 +$ Node1.wpanctl('get -v NCP:State') -> '"associated"' +$ Node1.wpanctl('get -v Network:Name') -> '"toranj-net"' +$ Node1.wpanctl('get -v Network:PANID') -> '0x9DEB' +$ Node1.wpanctl('get -v Network:XPANID') -> '0x8CC6CFC810F23E1B' +$ Node1.wpanctl('get -v Network:Key') -> '[BA2733A5D81EAB8FFB3C9A7383CB6045]' +$ Node1.wpanctl('get -v NCP:Channel') -> '16' +$ Node2.wpanctl('set Network:Key -d -v BA2733A5D81EAB8FFB3C9A7383CB6045') -> '' +$ Node2.wpanctl('join "toranj-net" -c 16 -T r -p 0x9DEB -x 0x8CC6CFC810F23E1B'): + Joining "toranj-net" 8CC6CFC810F23E1B as node type "router" + Successfully Joined! +$ Node2.wpanctl('get -v NCP:State') -> '"associated"' +$ Node1.wpanctl('get -v IPv6:LinkLocalAddress') -> '"fe80::bcec:daf3:439d:c931"' +$ Node2.wpanctl('get -v IPv6:LinkLocalAddress') -> '"fe80::ec08:f348:646f:d37d"' +- Node1 sent 20 bytes (":YeQuNKjuOtd%H#ipM7P") to [fe80::ec08:f348:646f:d37d]:404 from [fe80::bcec:daf3:439d:c931]:12557 +- Node2 received 20 bytes (":YeQuNKjuOtd%H#ipM7P") on port 404 from [fe80::bcec:daf3:439d:c931]:12557 + +``` + +------ + +What does `"toranj"` mean? it's the name of a common symmetric weaving [pattern](https://en.wikipedia.org/wiki/Persian_carpet#/media/File:Toranj_-_special_circular_design_of_Iranian_carpets.JPG) used in Persian carpets. diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h new file mode 100644 index 000000000..6da446eca --- /dev/null +++ b/tests/toranj/openthread-core-toranj-config.h @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2018, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef OPENTHREAD_CORE_TORANJ_CONFIG_H_ +#define OPENTHREAD_CORE_TORANJ_CONFIG_H_ + +/** + * This header file defines the OpenThread core configuration options used in NCP build for `toranj` test framework. + * + */ + +/** + * @def OPENTHREAD_CONFIG_PLATFORM_INFO + * + * The platform-specific string to insert into the OpenThread version string. + * + */ +#define OPENTHREAD_CONFIG_PLATFORM_INFO "POSIX-toranj" + +/** + * @def OPENTHREAD_CONFIG_NUM_MESSAGE_BUFFERS + * + * The number of message buffers in the buffer pool. + * + */ +#define OPENTHREAD_CONFIG_NUM_MESSAGE_BUFFERS 256 + +/** + * @def OPENTHREAD_CONFIG_ADDRESS_CACHE_ENTRIES + * + * The number of EID-to-RLOC cache entries. + * + */ +#define OPENTHREAD_CONFIG_ADDRESS_CACHE_ENTRIES 32 + +/** + * @def OPENTHREAD_CONFIG_ADDRESS_QUERY_MAX_RETRY_DELAY + * + * Maximum retry delay for address query (in seconds). + * + * Default: 28800 seconds (480 minutes or 8 hours) + * + */ +#define OPENTHREAD_CONFIG_ADDRESS_QUERY_MAX_RETRY_DELAY 120 + +/** + * @def OPENTHREAD_CONFIG_MAX_CHILDREN + * + * The maximum number of children. + * + */ +#define OPENTHREAD_CONFIG_MAX_CHILDREN 32 + +/** + * @def OPENTHREAD_CONFIG_DEFAULT_CHILD_TIMEOUT + * + * The default child timeout value (in seconds). + * + */ +#define OPENTHREAD_CONFIG_DEFAULT_CHILD_TIMEOUT 120 + +/** + * @def OPENTHREAD_CONFIG_IP_ADDRS_PER_CHILD + * + * The maximum number of supported IPv6 address registrations per child. + * + */ +#define OPENTHREAD_CONFIG_IP_ADDRS_PER_CHILD 10 + +/** + * @def OPENTHREAD_CONFIG_MAX_EXT_IP_ADDRS + * + * The maximum number of supported IPv6 addresses allows to be externally added. + * + */ +#define OPENTHREAD_CONFIG_MAX_EXT_IP_ADDRS 8 + +/** + * @def OPENTHREAD_CONFIG_MAX_EXT_MULTICAST_IP_ADDRS + * + * The maximum number of supported IPv6 multicast addresses allows to be externally added. + * + */ +#define OPENTHREAD_CONFIG_MAX_EXT_MULTICAST_IP_ADDRS 4 + +/** + * @def OPENTHREAD_CONFIG_MAC_FILTER_SIZE + * + * The number of MAC Filter entries. + * + */ +#define OPENTHREAD_CONFIG_MAC_FILTER_SIZE 80 + +/** + * @def OPENTHREAD_CONFIG_LOG_OUTPUT + * + * Selects if, and where the LOG output goes to. + * + */ +#define OPENTHREAD_CONFIG_LOG_OUTPUT OPENTHREAD_CONFIG_LOG_OUTPUT_APP + +/** + * @def OPENTHREAD_CONFIG_LOG_LEVEL + * + * The log level (used at compile time). + * + */ +#define OPENTHREAD_CONFIG_LOG_LEVEL OT_LOG_LEVEL_INFO + +/** + * @def OPENTHREAD_CONFIG_ENABLE_DYNAMIC_LOG_LEVEL + * + * Define as 1 to enable dynamic log level control. + * + */ +#define OPENTHREAD_CONFIG_ENABLE_DYNAMIC_LOG_LEVEL 1 + +/** + * @def OPENTHREAD_CONFIG_LOG_SUFFIX + * + * Define suffix to append at the end of logs. + * + */ +#define OPENTHREAD_CONFIG_LOG_SUFFIX "\n" + +/** + * @def OPENTHREAD_CONFIG_NCP_TX_BUFFER_SIZE + * + * The size of NCP message buffer in bytes + * + */ +#define OPENTHREAD_CONFIG_NCP_TX_BUFFER_SIZE 4096 + +/** + * @def OPENTHREAD_CONFIG_ENABLE_STEERING_DATA_SET_OOB + * + * Enable setting steering data out of band. + * + */ +#define OPENTHREAD_CONFIG_ENABLE_STEERING_DATA_SET_OOB 1 + +/** + * @def OPENTHREAD_CONFIG_INFORM_PREVIOUS_PARENT_ON_REATTACH + * + * Define as 1 for a child to inform its previous parent when it attaches to a new parent. + * + * If this feature is enabled, when a device attaches to a new parent, it will send an IP message (with empty payload + * and mesh-local IP address as the source address) to its previous parent. + * + */ +#define OPENTHREAD_CONFIG_INFORM_PREVIOUS_PARENT_ON_REATTACH 1 + +/** + * @def OPENTHREAD_CONFIG_MLE_SEND_LINK_REQUEST_ON_ADV_TIMEOUT + * + * Define to 1 to send an MLE Link Request when MAX_NEIGHBOR_AGE is reached for a neighboring router. + * + */ +#define OPENTHREAD_CONFIG_MLE_SEND_LINK_REQUEST_ON_ADV_TIMEOUT 1 + + +#endif /* OPENTHREAD_CORE_TORANJ_CONFIG_H_ */ + diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh new file mode 100755 index 000000000..ce95f72e4 --- /dev/null +++ b/tests/toranj/start.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +die() { + echo " *** ERROR: " $* + exit 1 +} + +failed() { + echo " *** TEST FAILED: ERROR: " $* + tail -n 30 wpantund-logs*.log + exit 1 +} + +clean() { + sudo rm tmp/*.flash > /dev/null 2>&1 + sudo rm *.log > /dev/null 2>&1 +} + +cd $(dirname $0) +cd ../.. + +# Build OpenThread posix mode with required configuration + +./bootstrap || die +./configure \ + CPPFLAGS='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config.h\"' \ + --enable-ncp-app=all \ + --with-ncp-bus=uart \ + --with-examples=posix \ + --enable-border-router \ + --enable-child-supervision \ + --enable-diag \ + --enable-jam-detection \ + --enable-legacy \ + --enable-mac-filter \ + --enable-service \ + --enable-channel-monitor \ + --disable-docs \ + --disable-test || die + +make -j 8 || die + +# Run all the tests + +cd tests/toranj + +clean; sudo python test-001-get-set.py || failed +clean; sudo python test-002-form.py || failed +clean; sudo python test-003-join.py || failed +clean; sudo python test-004-scan.py || failed +clean; sudo python test-005-discover-scan.py || failed +clean; sudo python test-006-traffic-router-end-device.py || failed +clean; sudo python test-007-traffic-router-sleepy.py || failed +clean; sudo python test-008-permit-join.py || failed diff --git a/tests/toranj/test-001-get-set.py b/tests/toranj/test-001-get-set.py new file mode 100644 index 000000000..33dd7ec83 --- /dev/null +++ b/tests/toranj/test-001-get-set.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from wpan import verify +import wpan +import time + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: simple wpanctl get and set commands + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +node = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +verify(node.get(wpan.WPAN_STATE) == wpan.STATE_OFFLINE) + +# set some of properties and check and verify that the value is indeed changed... + +node.set(wpan.WPAN_NAME, 'test-network') +verify(node.get(wpan.WPAN_NAME) == '"test-network"') + +node.set(wpan.WPAN_NAME, 'a') +verify(node.get(wpan.WPAN_NAME) == '"a"') + +node.set(wpan.WPAN_PANID, '0xABBA') +verify(node.get(wpan.WPAN_PANID) == '0xABBA') + +node.set(wpan.WPAN_XPANID, '1020031510006016', binary_data=True) +verify(node.get(wpan.WPAN_XPANID) == '0x1020031510006016') + +node.set(wpan.WPAN_KEY, '0123456789abcdeffecdba9876543210', binary_data=True) +verify(node.get(wpan.WPAN_KEY) == '[0123456789ABCDEFFECDBA9876543210]') + +node.set(wpan.WPAN_MAC_WHITELIST_ENABLED, '1') +verify(node.get(wpan.WPAN_MAC_WHITELIST_ENABLED) == 'true') + +node.set(wpan.WPAN_MAC_WHITELIST_ENABLED, '0') +verify(node.get(wpan.WPAN_MAC_WHITELIST_ENABLED) == 'false') + +node.set(wpan.WPAN_MAC_WHITELIST_ENABLED, 'true') +verify(node.get(wpan.WPAN_MAC_WHITELIST_ENABLED) == 'true') + +# Ensure `get` is successful with all gettable properties + +all_gettable_props = [ + wpan.WPAN_STATE, + wpan.WPAN_NAME, + wpan.WPAN_PANID, + wpan.WPAN_XPANID, + wpan.WPAN_KEY, + wpan.WPAN_CHANNEL, + wpan.WPAN_HW_ADDRESS, + wpan.WPAN_EXT_ADDRESS, + wpan.WPAN_POLL_INTERVAL, + wpan.WPAN_NODE_TYPE, + wpan.WPAN_ROLE, + wpan.WPAN_PARTITION_ID, + wpan.WPAN_NCP_VERSION, + wpan.WPAN_IP6_LINK_LOCAL_ADDRESS, + wpan.WPAN_IP6_MESH_LOCAL_ADDRESS, + wpan.WPAN_IP6_MESH_LOCAL_PREFIX, + wpan.WPAN_IP6_ALL_ADDRESSES, + wpan.WPAN_IP6_MULTICAST_ADDRESSES, + wpan.WPAN_THREAD_RLOC16, + wpan.WPAN_THREAD_ROUTER_ID, + wpan.WPAN_THREAD_NETWORK_DATA, + wpan.WPAN_THREAD_CHILD_TABLE, + wpan.WPAN_THREAD_CHILD_TABLE_ASVALMAP, + wpan.WPAN_THREAD_CHILD_TABLE_ADDRESSES, + wpan.WPAN_THREAD_NEIGHBOR_TABLE, + wpan.WPAN_THREAD_NEIGHBOR_TABLE_ASVALMAP, + wpan.WPAN_THREAD_ROUTER_TABLE, + wpan.WPAN_THREAD_ROUTER_TABLE_ASVALMAP, + wpan.WPAN_THREAD_NETWORK_DATA_VERSION, + wpan.WPAN_THREAD_STABLE_NETWORK_DATA, + wpan.WPAN_THREAD_STABLE_NETWORK_DATA_VERSION, + wpan.WPAN_THREAD_DEVICE_MODE, + wpan.WPAN_THREAD_OFF_MESH_ROUTES, + wpan.WPAN_THREAD_ON_MESH_PREFIXES, + wpan.WPAN_THREAD_ROUTER_ROLE_ENABLED, + wpan.WPAN_THREAD_CONFIG_FILTER_RLOC_ADDRESSES, + wpan.WPAN_THREAD_ACTIVE_DATASET, + wpan.WPAN_THREAD_ACTIVE_DATASET_ASVALMAP, + wpan.WPAN_THREAD_PENDING_DATASET, + wpan.WPAN_THREAD_PENDING_DATASET_ASVALMAP, + wpan.WPAN_OT_LOG_LEVEL, + wpan.WPAN_OT_STEERING_DATA_ADDRESS, + wpan.WPAN_OT_STEERING_DATA_SET_WHEN_JOINABLE, + wpan.WPAN_OT_MSG_BUFFER_COUNTERS, + wpan.WPAN_OT_MSG_BUFFER_COUNTERS_AS_STRING, + wpan.WPAN_NCP_COUNTER_ALL_MAC, + wpan.WPAN_NCP_COUNTER_ALL_MAC_ASVALMAP, + wpan.WPAN_MAC_WHITELIST_ENABLED, + wpan.WPAN_MAC_WHITELIST_ENTRIES, + wpan.WPAN_MAC_WHITELIST_ENTRIES_ASVALMAP, + wpan.WPAN_MAC_BLACKLIST_ENABLED, + wpan.WPAN_MAC_BLACKLIST_ENTRIES, + wpan.WPAN_MAC_BLACKLIST_ENTRIES_ASVALMAP, + wpan.WPAN_JAM_DETECTION_STATUS, + wpan.WPAN_JAM_DETECTION_ENABLE, + wpan.WPAN_JAM_DETECTION_RSSI_THRESHOLD, + wpan.WPAN_JAM_DETECTION_WINDOW, + wpan.WPAN_JAM_DETECTION_BUSY_PERIOD, + wpan.WPAN_JAM_DETECTION_DEBUG_HISTORY_BITMAP, + wpan.WPAN_CHANNEL_MONITOR_SAMPLE_INTERVAL, + wpan.WPAN_CHANNEL_MONITOR_RSSI_THRESHOLD, + wpan.WPAN_CHANNEL_MONITOR_SAMPLE_WINDOW, + wpan.WPAN_CHANNEL_MONITOR_SAMPLE_COUNT, + wpan.WPAN_CHANNEL_MONITOR_CHANNEL_QUALITY, + wpan.WPAN_CHANNEL_MONITOR_CHANNEL_QUALITY_ASVALMAP, +] + +for prop in all_gettable_props: + node.get(prop) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/test-002-form.py b/tests/toranj/test-002-form.py new file mode 100644 index 000000000..eeee2a909 --- /dev/null +++ b/tests/toranj/test-002-form.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from wpan import verify +import wpan +import time + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: forming a Thread network + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +node = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# default values after reset +DEFAULT_KEY = '[00112233445566778899AABBCCDDEEFF]' +DEFAULT_NAME = '"OpenThread"' +DEFAULT_PANID = '0xFFFF' +DEFAULT_XPANID = '0xDEAD00BEEF00CAFE' + +verify(node.get(wpan.WPAN_STATE) == wpan.STATE_OFFLINE) +verify(node.get(wpan.WPAN_KEY) == DEFAULT_KEY) +verify(node.get(wpan.WPAN_NAME) == DEFAULT_NAME) +verify(node.get(wpan.WPAN_PANID) == DEFAULT_PANID) +verify(node.get(wpan.WPAN_XPANID) == DEFAULT_XPANID) + +# Form a network + +node.form('asha') +verify(node.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node.get(wpan.WPAN_NODE_TYPE) == wpan.NODE_TYPE_LEADER) +verify(node.get(wpan.WPAN_NAME) == '"asha"') +verify(node.get(wpan.WPAN_KEY) != DEFAULT_KEY) +verify(node.get(wpan.WPAN_PANID) != DEFAULT_PANID) +verify(node.get(wpan.WPAN_XPANID) != DEFAULT_XPANID) + + +node.leave() +verify(node.get(wpan.WPAN_STATE) == wpan.STATE_OFFLINE) + +# Form a network on a specific channel. + +node.form('ahura', channel=20) +verify(node.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node.get(wpan.WPAN_NAME) == '"ahura"') +verify(node.get(wpan.WPAN_CHANNEL) == '20') +verify(node.get(wpan.WPAN_KEY) != DEFAULT_KEY) +verify(node.get(wpan.WPAN_PANID) != DEFAULT_PANID) +verify(node.get(wpan.WPAN_XPANID) != DEFAULT_XPANID) + +node.leave() +verify(node.get(wpan.WPAN_STATE) == wpan.STATE_OFFLINE) + +# Form a network with a specific panid, xpanid and key. + +node.set(wpan.WPAN_PANID, '0x1977') +node.set(wpan.WPAN_XPANID, '1020031510006016', binary_data=True) +node.set(wpan.WPAN_KEY, '0123456789abcdeffecdba9876543210', binary_data=True) + +node.form('mazda', channel=12) +verify(node.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node.get(wpan.WPAN_NAME) == '"mazda"') +verify(node.get(wpan.WPAN_CHANNEL) == '12') +verify(node.get(wpan.WPAN_KEY) == '[0123456789ABCDEFFECDBA9876543210]') +verify(node.get(wpan.WPAN_PANID) == '0x1977') +verify(node.get(wpan.WPAN_XPANID) == '0x1020031510006016') + + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) + diff --git a/tests/toranj/test-003-join.py b/tests/toranj/test-003-join.py new file mode 100644 index 000000000..73c75a7b4 --- /dev/null +++ b/tests/toranj/test-003-join.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from wpan import verify +import wpan +import time + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: joining (as router, end-device, sleepy) - two node network + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +node1 = wpan.Node() +node2 = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# Form a network on node1 +node1.form('PAN-aB71n') +verify(node1.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node1.get(wpan.WPAN_NODE_TYPE) == wpan.NODE_TYPE_LEADER) + +# Join from node2 as a router +node2.join_node(node1, node_type=wpan.JOIN_TYPE_ROUTER) +verify(node2.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node2.get(wpan.WPAN_NAME) == node1.get(wpan.WPAN_NAME)) +verify(node2.get(wpan.WPAN_PANID) == node1.get(wpan.WPAN_PANID)) +verify(node2.get(wpan.WPAN_XPANID) == node1.get(wpan.WPAN_XPANID)) +verify(node2.get(wpan.WPAN_KEY) == node1.get(wpan.WPAN_KEY)) + +node2.leave() + +# Join from node2 as an end-device +node2.join_node(node1, node_type=wpan.JOIN_TYPE_END_DEVICE) +verify(node2.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node2.get(wpan.WPAN_NAME) == node1.get(wpan.WPAN_NAME)) +verify(node2.get(wpan.WPAN_PANID) == node1.get(wpan.WPAN_PANID)) +verify(node2.get(wpan.WPAN_XPANID) == node1.get(wpan.WPAN_XPANID)) +verify(node2.get(wpan.WPAN_KEY) == node1.get(wpan.WPAN_KEY)) + +node2.leave() + +# Join from node2 as a sleepy-end-device +node2.join_node(node1, node_type=wpan.JOIN_TYPE_SLEEPY_END_DEVICE) +verify(node2.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node2.get(wpan.WPAN_NAME) == node1.get(wpan.WPAN_NAME)) +verify(node2.get(wpan.WPAN_PANID) == node1.get(wpan.WPAN_PANID)) +verify(node2.get(wpan.WPAN_XPANID) == node1.get(wpan.WPAN_XPANID)) +verify(node2.get(wpan.WPAN_KEY) == node1.get(wpan.WPAN_KEY)) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) + diff --git a/tests/toranj/test-004-scan.py b/tests/toranj/test-004-scan.py new file mode 100644 index 000000000..0e1b8f42b --- /dev/null +++ b/tests/toranj/test-004-scan.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from wpan import verify +import wpan +import time + +#----------------------------------------------------------------------------------------------------------------------- +# Test desciption: Active scan and permit-join + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +NUM_NODES = 5 + +nodes = [] +for i in range(NUM_NODES): + nodes.append(wpan.Node()) + +scanner = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +for node in nodes: + node.form(node.interface_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# Perform active scan and check that all nodes are seen in the scan result. + +scan_result = wpan.parse_scan_result(scanner.active_scan()) + +for node in nodes: + verify(node.is_in_scan_result(scan_result)) + +# Make every other network joinable, scan and check the result. + +make_joinable = False +for node in nodes: + make_joinable = not make_joinable + if make_joinable: + node.permit_join() + +scan_result = wpan.parse_scan_result(scanner.active_scan()) + +for node in nodes: + verify(node.is_in_scan_result(scan_result)) + +# Scan from an already associated node. + +scan_result = wpan.parse_scan_result(nodes[0].active_scan()) + +for node in nodes[1:]: + verify(node.is_in_scan_result(scan_result)) + +# Scan on a specific channel + +channel = nodes[0].get(wpan.WPAN_CHANNEL) +scan_result = wpan.parse_scan_result(scanner.active_scan(channel=channel)) +verify(nodes[0].is_in_scan_result(scan_result)) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/test-005-discover-scan.py b/tests/toranj/test-005-discover-scan.py new file mode 100644 index 000000000..bc6c648a4 --- /dev/null +++ b/tests/toranj/test-005-discover-scan.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from wpan import verify +import wpan +import time + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: discover scan + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +NUM_NODES = 5 + +nodes = [] +for i in range(NUM_NODES): + nodes.append(wpan.Node()) + +scanner = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +for node in nodes: + node.form(node.interface_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# Perform active scan and check that all nodes are seen in the scan result. + +scan_result = wpan.parse_scan_result(scanner.discover_scan()) + +for node in nodes: + verify(node.is_in_scan_result(scan_result)) + +# Scan from an already associated node. + +scan_result = wpan.parse_scan_result(nodes[0].discover_scan()) + +for node in nodes[1:]: + verify(node.is_in_scan_result(scan_result)) + +# TODO: add tests for the joiner only and filtering + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) + diff --git a/tests/toranj/test-006-traffic-router-end-device.py b/tests/toranj/test-006-traffic-router-end-device.py new file mode 100644 index 000000000..43f28ce90 --- /dev/null +++ b/tests/toranj/test-006-traffic-router-end-device.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import time +import wpan +from wpan import verify + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: Traffic between router and end-device (link-local and mesh-local IPv6 addresses) + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +node1 = wpan.Node() +node2 = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +# Two-node network (node1 leader/router, node2 end-device) + +node1.form('test-PAN') +node2.join_node(node1, node_type=wpan.JOIN_TYPE_END_DEVICE) + +verify(node2.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node2.get(wpan.WPAN_NAME) == node1.get(wpan.WPAN_NAME)) +verify(node2.get(wpan.WPAN_PANID) == node1.get(wpan.WPAN_PANID)) +verify(node2.get(wpan.WPAN_XPANID) == node1.get(wpan.WPAN_XPANID)) + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# Get the link local addresses +ll1 = node1.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] +ll2 = node2.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] + +# Get the mesh-local addresses +ml1 = node1.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] +ml2 = node2.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] + +NUM_MSGS = 3 +MSG_LENS = [40, 100, 400, 800, 1000] +PORT = 1234 + +# all src and dst configuration (link-local and mesh-local) +for src,dst in [ (ll1, ll2), (ll1, ml2), (ml1, ll2), (ml1, ml2) ]: + + for msg_length in MSG_LENS: + sender = node1.prepare_tx(src, dst, msg_length, NUM_MSGS) + recver = node2.prepare_rx(sender) + + wpan.Node.perform_async_tx_rx() + + verify(sender.was_successful) + verify(recver.was_successful) + + # Send and receive at the same time on same port number + + s1 = node1.prepare_tx((src, PORT), (dst, PORT), 'Hi there!', NUM_MSGS) + r1 = node2.prepare_rx(s1) + s2 = node2.prepare_tx((dst, PORT), (src, PORT), 'Hello back to you!', NUM_MSGS) + r2 = node1.prepare_rx(s2) + + wpan.Node.perform_async_tx_rx() + + verify(s1.was_successful and r1.was_successful) + verify(s2.was_successful and r2.was_successful) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/test-007-traffic-router-sleepy.py b/tests/toranj/test-007-traffic-router-sleepy.py new file mode 100644 index 000000000..ac43c3058 --- /dev/null +++ b/tests/toranj/test-007-traffic-router-sleepy.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import time +import wpan +from wpan import verify + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: Traffic between router and sleepy-end-device (link-local and mesh-local IPv6 addresses) + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +node1 = wpan.Node() +node2 = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +# Two-node network (node1 leader/router, node2 sleepy-end-device) + +node1.form('test-PAN') +node2.join_node(node1, node_type=wpan.JOIN_TYPE_SLEEPY_END_DEVICE) + +verify(node2.get(wpan.WPAN_STATE) == wpan.STATE_ASSOCIATED) +verify(node2.get(wpan.WPAN_NAME) == node1.get(wpan.WPAN_NAME)) +verify(node2.get(wpan.WPAN_PANID) == node1.get(wpan.WPAN_PANID)) +verify(node2.get(wpan.WPAN_XPANID) == node1.get(wpan.WPAN_XPANID)) + + + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +# Get the link local addresses +ll1 = node1.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] +ll2 = node2.get(wpan.WPAN_IP6_LINK_LOCAL_ADDRESS)[1:-1] + +# Get the mesh-local addresses +ml1 = node1.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] +ml2 = node2.get(wpan.WPAN_IP6_MESH_LOCAL_ADDRESS)[1:-1] + +NUM_MSGS = 3 +MSG_LENS = [40, 100, 400, 800, 1000] +PORT = 1234 + +for poll_interval in [10, 100, 300]: + + node2.set(wpan.WPAN_POLL_INTERVAL, str(poll_interval)) + verify(node2.get(wpan.WPAN_POLL_INTERVAL) == str(poll_interval)) + + # all src and dst configuration (link-local and mesh-local) + for src,dst in [ (ll1, ll2), (ll1, ml2), (ml1, ll2), (ml1, ml2) ]: + + for msg_length in MSG_LENS: + sender = node1.prepare_tx(src, dst, msg_length, NUM_MSGS) + recver = node2.prepare_rx(sender) + + wpan.Node.perform_async_tx_rx() + + verify(sender.was_successful) + verify(recver.was_successful) + + # Send and receive at the same time on same port number + + s1 = node1.prepare_tx((src, PORT), (dst, PORT), 'Hi there!', NUM_MSGS) + r1 = node2.prepare_rx(s1) + s2 = node2.prepare_tx((dst, PORT), (src, PORT), 'Hello back to you!', NUM_MSGS) + r2 = node1.prepare_rx(s2) + + wpan.Node.perform_async_tx_rx() + + verify(s1.was_successful and r1.was_successful) + verify(s2.was_successful and r2.was_successful) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/test-008-permit-join.py b/tests/toranj/test-008-permit-join.py new file mode 100644 index 000000000..f7b469e5c --- /dev/null +++ b/tests/toranj/test-008-permit-join.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import time +import wpan +from wpan import verify + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: check wpantund `permit-join` functionality and timeout + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +node = wpan.Node() + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +node.form("permit-join-test") + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +verify(node.get(wpan.WPAN_NETWORK_ALLOW_JOIN) == 'false') + +node.permit_join() +verify(node.get(wpan.WPAN_NETWORK_ALLOW_JOIN) == 'true') + +node.permit_join('0') +verify(node.get(wpan.WPAN_NETWORK_ALLOW_JOIN) == 'false') + +node.permit_join(port='1234') +verify(node.get(wpan.WPAN_NETWORK_ALLOW_JOIN) == 'true') + +node.permit_join('0') +verify(node.get(wpan.WPAN_NETWORK_ALLOW_JOIN) == 'false') + +# check the timeout +node.permit_join('1') +verify(node.get(wpan.WPAN_NETWORK_ALLOW_JOIN) == 'true') +time.sleep(1.5) +verify(node.get(wpan.WPAN_NETWORK_ALLOW_JOIN) == 'false') + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/test-nnn-template.py b/tests/toranj/test-nnn-template.py new file mode 100644 index 000000000..9736abd6e --- /dev/null +++ b/tests/toranj/test-nnn-template.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import time +import wpan +from wpan import verify + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: TODO + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +#TODO + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +#TODO + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +#TODO + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/wpan.py b/tests/toranj/wpan.py new file mode 100644 index 000000000..5510a26d5 --- /dev/null +++ b/tests/toranj/wpan.py @@ -0,0 +1,801 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +import sys +import time +import random +import weakref +import subprocess +import socket +import asyncore + +#---------------------------------------------------------------------------------------------------------------------- +# wpantund properties + +WPAN_STATE = 'NCP:State' +WPAN_NAME = 'Network:Name' +WPAN_PANID = 'Network:PANID' +WPAN_XPANID = 'Network:XPANID' +WPAN_KEY = 'Network:Key' +WPAN_CHANNEL = 'NCP:Channel' +WPAN_HW_ADDRESS = 'NCP:HardwareAddress' +WPAN_EXT_ADDRESS = 'NCP:ExtendedAddress' +WPAN_POLL_INTERVAL = 'NCP:SleepyPollInterval' +WPAN_NODE_TYPE = 'Network:NodeType' +WPAN_ROLE = 'Network:Role' +WPAN_PARTITION_ID = 'Network:PartitionId' +WPAN_NCP_VERSION = 'NCP:Version' +WPAN_NETWORK_ALLOW_JOIN = 'com.nestlabs.internal:Network:AllowingJoin' +WPAN_NETWORK_PASSTHRU_PORT = 'com.nestlabs.internal:Network:PassthruPort' + +WPAN_IP6_LINK_LOCAL_ADDRESS = "IPv6:LinkLocalAddress" +WPAN_IP6_MESH_LOCAL_ADDRESS = "IPv6:MeshLocalAddress" +WPAN_IP6_MESH_LOCAL_PREFIX = "IPv6:MeshLocalPrefix" +WPAN_IP6_ALL_ADDRESSES = "IPv6:AllAddresses" +WPAN_IP6_MULTICAST_ADDRESSES = "IPv6:MulticastAddresses" + +WPAN_THREAD_RLOC16 = "Thread:RLOC16" +WPAN_THREAD_ROUTER_ID = "Thread:RouterID" +WPAN_THREAD_LEADER_ADDRESS = "Thread:Leader:Address" +WPAN_THREAD_LEADER_ROUTER_ID = "Thread:Leader:RouterID" +WPAN_THREAD_LEADER_WEIGHT = "Thread:Leader:Weight" +WPAN_THREAD_LEADER_LOCAL_WEIGHT = "Thread:Leader:LocalWeight" +WPAN_THREAD_LEADER_NETWORK_DATA = "Thread:Leader:NetworkData" +WPAN_THREAD_STABLE_LEADER_NETWORK_DATA = "Thread:Leader:StableNetworkData" +WPAN_THREAD_NETWORK_DATA = "Thread:NetworkData" +WPAN_THREAD_CHILD_TABLE = "Thread:ChildTable" +WPAN_THREAD_CHILD_TABLE_ASVALMAP = "Thread:ChildTable:AsValMap" +WPAN_THREAD_CHILD_TABLE_ADDRESSES = "Thread:ChildTable:Addresses" +WPAN_THREAD_NEIGHBOR_TABLE = "Thread:NeighborTable" +WPAN_THREAD_NEIGHBOR_TABLE_ASVALMAP = "Thread:NeighborTable:AsValMap" +WPAN_THREAD_ROUTER_TABLE = "Thread:RouterTable" +WPAN_THREAD_ROUTER_TABLE_ASVALMAP = "Thread:RouterTable:AsValMap" +WPAN_THREAD_NETWORK_DATA_VERSION = "Thread:NetworkDataVersion" +WPAN_THREAD_STABLE_NETWORK_DATA = "Thread:StableNetworkData" +WPAN_THREAD_STABLE_NETWORK_DATA_VERSION = "Thread:StableNetworkDataVersion" +WPAN_THREAD_PREFERRED_ROUTER_ID = "Thread:PreferredRouterID" +WPAN_THREAD_COMMISSIONER_ENABLED = "Thread:Commissioner:Enabled" +WPAN_THREAD_DEVICE_MODE = "Thread:DeviceMode" +WPAN_THREAD_OFF_MESH_ROUTES = "Thread:OffMeshRoutes" +WPAN_THREAD_ON_MESH_PREFIXES = "Thread:OnMeshPrefixes" +WPAN_THREAD_ROUTER_ROLE_ENABLED = "Thread:RouterRole:Enabled" +WPAN_THREAD_CONFIG_FILTER_RLOC_ADDRESSES = "Thread:Config:FilterRLOCAddresses" +WPAN_THREAD_ACTIVE_DATASET = "Thread:ActiveDataset" +WPAN_THREAD_ACTIVE_DATASET_ASVALMAP = "Thread:ActiveDataset:AsValMap" +WPAN_THREAD_PENDING_DATASET = "Thread:PendingDataset" +WPAN_THREAD_PENDING_DATASET_ASVALMAP = "Thread:PendingDataset:AsValMap" + +WPAN_OT_LOG_LEVEL = "OpenThread:LogLevel" +WPAN_OT_STEERING_DATA_ADDRESS = "OpenThread:SteeringData:Address" +WPAN_OT_STEERING_DATA_SET_WHEN_JOINABLE = "OpenThread:SteeringData:SetWhenJoinable" +WPAN_OT_MSG_BUFFER_COUNTERS = "OpenThread:MsgBufferCounters" +WPAN_OT_MSG_BUFFER_COUNTERS_AS_STRING = "OpenThread:MsgBufferCounters:AsString" +WPAN_OT_DEBUG_TEST_ASSERT = "OpenThread:Debug:TestAssert" +WPAN_OT_DEBUG_TEST_WATCHDOG = "OpenThread:Debug:TestWatchdog" + +WPAN_NCP_COUNTER_ALL_MAC = "NCP:Counter:AllMac" +WPAN_NCP_COUNTER_ALL_MAC_ASVALMAP = "NCP:Counter:AllMac:AsValMap" + +WPAN_MAC_WHITELIST_ENABLED = "MAC:Whitelist:Enabled" +WPAN_MAC_WHITELIST_ENTRIES = "MAC:Whitelist:Entries" +WPAN_MAC_WHITELIST_ENTRIES_ASVALMAP = "MAC:Whitelist:Entries:AsValMap" +WPAN_MAC_BLACKLIST_ENABLED = "MAC:Blacklist:Enabled" +WPAN_MAC_BLACKLIST_ENTRIES = "MAC:Blacklist:Entries" +WPAN_MAC_BLACKLIST_ENTRIES_ASVALMAP = "MAC:Blacklist:Entries:AsValMap" + +WPAN_JAM_DETECTION_STATUS = "JamDetection:Status" +WPAN_JAM_DETECTION_ENABLE = "JamDetection:Enable" +WPAN_JAM_DETECTION_RSSI_THRESHOLD = "JamDetection:RssiThreshold" +WPAN_JAM_DETECTION_WINDOW = "JamDetection:Window" +WPAN_JAM_DETECTION_BUSY_PERIOD = "JamDetection:BusyPeriod" +WPAN_JAM_DETECTION_DEBUG_HISTORY_BITMAP = "JamDetection:Debug:HistoryBitmap" + +WPAN_CHANNEL_MONITOR_SAMPLE_INTERVAL = "ChannelMonitor:SampleInterval" +WPAN_CHANNEL_MONITOR_RSSI_THRESHOLD = "ChannelMonitor:RssiThreshold" +WPAN_CHANNEL_MONITOR_SAMPLE_WINDOW = "ChannelMonitor:SampleWindow" +WPAN_CHANNEL_MONITOR_SAMPLE_COUNT = "ChannelMonitor:SampleCount" +WPAN_CHANNEL_MONITOR_CHANNEL_QUALITY = "ChannelMonitor:ChannelQuality" +WPAN_CHANNEL_MONITOR_CHANNEL_QUALITY_ASVALMAP = "ChannelMonitor:ChannelQuality:AsValMap" + +#---------------------------------------------------------------------------------------------------------------------- +# Valid state values + +STATE_UNINITIALIZED = '"uninitialized"' +STATE_FAULT = '"uninitialized:fault"' +STATE_UPGRADING = '"uninitialized:upgrading"' +STATE_DEEP_SLEEP = '"offline:deep-sleep"' +STATE_OFFLINE = '"offline"' +STATE_COMMISSIONED = '"offline:commissioned"' +STATE_ASSOCIATING = '"associating"' +STATE_CREDENTIALS_NEEDED = '"associating:credentials-needed"' +STATE_ASSOCIATED = '"associated"' +STATE_ISOLATED = '"associated:no-parent"' +STATE_NETWAKE_ASLEEP = '"associated:netwake-asleep"' +STATE_NETWAKE_WAKING = '"associated:netwake-waking"' + +#----------------------------------------------------------------------------------------------------------------------- +# Node types (from `WPAN_NODE_TYPE` property) + +NODE_TYPE_UNKNOWN = '"unknown"' +NODE_TYPE_LEADER = '"leader"' +NODE_TYPE_ROUTER = '"router"' +NODE_TYPE_END_DEVICE = '"end-device"' +NODE_TYPE_SLEEPY_END_DEVICE = '"sleepy-end-device"' +NODE_TYPE_COMMISSIONER = '"commissioner"' +NODE_TYPE_NEST_LURKER = '"nl-lurker"' + +#----------------------------------------------------------------------------------------------------------------------- + +# Node types used by `Node.join()` + +JOIN_TYPE_ROUTER = 'r' +JOIN_TYPE_END_DEVICE = 'e' +JOIN_TYPE_SLEEPY_END_DEVICE = 's' + +#----------------------------------------------------------------------------------------------------------------------- + +def _log(text, new_line=True, flush=True): + sys.stdout.write(text) + if new_line: + sys.stdout.write('\n') + if flush: + sys.stdout.flush() + +#----------------------------------------------------------------------------------------------------------------------- +# Node class + +class Node(object): + """ A wpantund OT NCP instance """ + + _VERBOSE = False # defines the default verbosity setting (can be changed per `Node`) + + # path to `wpantund`, `wpanctl` and `ot-ncp-ftd` code + _WPANTUND = '/usr/local/sbin/wpantund' + _WPANCTL = '/usr/local/bin/wpanctl' + _OT_NCP_FTD = '../../examples/apps/ncp/ot-ncp-ftd' + + _TUND_LOG_TO_FILE = True # determines if the wpantund logs are saved in file or sent to stdout + _TUND_LOG_FNAME = 'wpantund-logs' # name of wpantund log file (if # name of wpantund _TUND_LOG_TO_FILE is True) + + # interface name + _INTFC_NAME_PREFIX = 'utun' if sys.platform == 'darwin' else 'wpan' + _START_INDEX = 4 if sys.platform == 'darwin' else 1 + + _cur_index = _START_INDEX + _all_nodes = weakref.WeakSet() + + def __init__(self, verbose=_VERBOSE): + """Creates a new `Node` instance""" + + index = Node._cur_index + Node._cur_index += 1 + + self._index = index + self._interface_name = self._INTFC_NAME_PREFIX + str(index) + self._verbose = verbose + + cmd = self._WPANTUND + \ + ' -o Config:NCP:SocketPath \"system:{} {}\"'.format(self._OT_NCP_FTD, index) + \ + ' -o Config:TUN:InterfaceName {}'.format(self._interface_name) + \ + ' -o Config:NCP:DriverName spinel' + \ + ' -o Daemon:SyslogMask \"all -debug\"' + + if Node._TUND_LOG_TO_FILE: + self._tund_log_file = open(self._TUND_LOG_FNAME + str(index) + '.log', 'wb') + else: + self._tund_log_file = None + + if self._verbose: + _log('$ Node{}.__init__() cmd: {}'.format(index, cmd)) + + self._wpantund_process = subprocess.Popen(cmd, shell=True, stderr=self._tund_log_file) + + self._wpanctl_cmd = self._WPANCTL + ' -I ' + self._interface_name + ' ' + + self._recvers = weakref.WeakValueDictionary() # map from local_port to `AsyncReceiver` object + Node._all_nodes.add(self) + + def __del__(self): + self._wpantund_process.terminate() + self._tund_log_file.close() + + def __repr__(self): + return 'Node (index={}, interface_name={})'.format(self._index, self._interface_name) + + @property + def index(self): + return self._index + + @property + def interface_name(self): + return self._interface_name + + @property + def tund_log_file(self): + return self._tund_log_file + + #------------------------------------------------------------------------------------------------------------------ + # Executing a `wpanctl` command + + def wpanctl(self, cmd): + """ Runs a wpanctl command on the given wpantund/OT-NCP instance and returns the output """ + + if self._verbose: + _log('$ Node{}.wpanctl(\'{}\')'.format(self._index, cmd), new_line=False) + + result = subprocess.check_output(self._wpanctl_cmd + cmd, shell=True, stderr=subprocess.STDOUT) + + if len(result) >= 1 and result[-1] == '\n': # remove the last char if it is '\n', + result = result[:-1] + + if self._verbose: + if '\n' in result: + _log(':') + for line in result.splitlines(): + _log(' ' + line) + else: + _log(' -> \'{}\''.format(result)) + + return result + + #------------------------------------------------------------------------------------------------------------------ + # APIs matching `wpanctl` commands. + + def get(self, prop_name, value_only=True): + return self.wpanctl('get ' + ('-v ' if value_only else '') + prop_name) + + def set(self, prop_name, value, binary_data=False): + return self._update_prop('set', prop_name, value, binary_data) + + def add(self, prop_name, value, binary_data=False): + return self._update_prop('add', prop_name, value, binary_data) + + def remove(self, prop_name, value, binary_data=False): + return self._update_prop('remove', prop_name, value, binary_data) + + def _update_prop(self, action, prop_name, value, binary_data): + return self.wpanctl(action + ' ' + prop_name + ' ' + ('-d ' if binary_data else '') + + '-v ' + value) # use -v to handle values starting with `-`. + + def reset(self): + return self.wpanctl('reset') + + def status(self): + return self.wpanctl('status') + + def leave(self): + return self.wpanctl('leave') + + def form(self, name, channel=None, node_type=None): + return self.wpanctl('form \"' + name + '\"' + + (' -c {}'.format(channel) if channel is not None else '') + + (' -T {}'.format(node_type) if node_type is not None else '')) + + def join(self, name, channel=None, node_type=None, panid=None, xpanid=None): + return self.wpanctl('join \"' + name + '\"' + + (' -c {}'.format(channel) if channel is not None else '') + + (' -T {}'.format(node_type) if node_type is not None else '') + + (' -p {}'.format(panid) if panid is not None else '') + + (' -x {}'.format(xpanid) if xpanid is not None else '')) + + def active_scan(self, channel=None): + return self.wpanctl('scan' + + (' -c {}'.format(channel) if channel is not None else '')) + + def energy_scan(self, channel=None): + return self.wpanctl('scan -e' + + (' -c {}'.format(channel) if channel is not None else '')) + + def discover_scan(self, channel=None, joiner_only=False, enable_filtering=False, panid_filter=None): + return self.wpanctl('scan -d' + + (' -c {}'.format(channel) if channel is not None else '') + + (' -j' if joiner_only else '') + + (' -e' if enable_filtering else '') + + (' -p {}'.format(panid_filter) if panid_filter is not None else '')) + + def permit_join(self, duration_sec=None, port=None, udp=True, tcp=True): + if not udp and not tcp: # incorrect use! + return '' + traffic_type = '' + if udp and not tcp: + traffic_type = ' --udp' + if tcp and not udp: + traffic_type = ' --tcp' + if port is not None and duration_sec is None: + duration_sec = '240' + + return self.wpanctl('permit-join' + + (' {}'.format(duration_sec) if duration_sec is not None else '') + + (' {}'.format(port) if port is not None else '') + + traffic_type) + + def config_gateway(self, prefix, default_route=False): + return self.wpanctl('config-gateway ' + prefix + + (' -d' if default_route else '')) + + def add_route(self, route_prefix, prefix_len_in_bytes=None, priority=None): + """route priority [(>0 for high, 0 for medium, <0 for low)]""" + return self.wpanctl('add-route ' + route_prefix + + (' -l {}'.format(prefix_len_in_bytes) if prefix_len_in_bytes is not None else '') + + (' -p {}'.format(priority) if priority is not None else '')) + + def remove_route(self, route_prefix, prefix_len_in_bytes=None, priority=None): + """route priority [(>0 for high, 0 for medium, <0 for low)]""" + return self.wpanctl('remove-route ' + route_prefix + + (' -l {}'.format(prefix_len_in_bytes) if prefix_len_in_bytes is not None else '') + + (' -p {}'.format(priority) if priority is not None else '')) + + #------------------------------------------------------------------------------------------------------------------ + # Helper methods + + def is_associated(self): + return self.get(WPAN_STATE) == STATE_ASSOCIATED + + def join_node(self, node, node_type=JOIN_TYPE_ROUTER): + """Join a network specified by another node, `node` should be a Node""" + + if not node.is_associated(): + return "{} is not associated".format(node) + + name = node.get(WPAN_NAME) + panid = node.get(WPAN_PANID) + xpanid = node.get(WPAN_XPANID) + netkey = node.get(WPAN_KEY) + channel = node.get(WPAN_CHANNEL) + + self.set(WPAN_KEY, netkey[1:-1], binary_data=True) + return self.join(name[1:-1], channel=channel, node_type=node_type, panid=panid, xpanid=xpanid) + + def whitelist_node(self, node): + """Adds a given node (of type `Node`) to the whitelist of `self` and enables whitelisting on `self`""" + + self.add(WPAN_MAC_WHITELIST_ENTRIES, node.get(WPAN_EXT_ADDRESS)[1:-1]) + self.set(WPAN_MAC_WHITELIST_ENABLED, '1') + + def is_in_scan_result(self, scan_result): + """Checks if node is in the scan results + `scan_result` must be an array of `ScanResult` object (see `parse_scan_result`). + """ + joinable = (self.get(WPAN_NETWORK_ALLOW_JOIN) == 'true') + panid = self.get(WPAN_PANID) + xpanid = self.get(WPAN_XPANID)[2:] + name = self.get(WPAN_NAME)[1:-1] + channel = self.get(WPAN_CHANNEL) + ext_address = self.get(WPAN_EXT_ADDRESS)[1:-1] + + for item in scan_result: + if all( [item.network_name == name, + item.panid == panid, + item.xpanid == xpanid, + item.channel == channel, + item.ext_address == ext_address, + (item.type == ScanResult.TYPE_DISCOVERY_SCAN) or (item.joinable == joinable) ] ): + return True + + return False + + #------------------------------------------------------------------------------------------------------------------ + # class methods + + @classmethod + def init_all_nodes(cls, wait_time=20): + """Issues a `wpanctl.leave` on all `Node` objects and waits for them to be ready""" + random.seed(12345) + time.sleep(0.5) + start_time = time.time() + for node in Node._all_nodes: + while True: + try: + node.leave() + except subprocess.CalledProcessError as e: + _log(' -> \'{}\' exit code: {}'.format(e.output, e.returncode)) + if time.time() - start_time > wait_time: + print 'Took too long to init all nodes ({}>{} sec)'.format(time.time() - start_time, wait_time) + raise + except: + raise + else: + break + time.sleep(0.1) + + #------------------------------------------------------------------------------------------------------------------ + # IPv6 message Sender and Receiver class + + class _NodeError(BaseException): + pass + + def prepare_tx(self, src, dst, data=40, count=1): + """Prepares an IPv6 msg transmission. + + - `src` and `dst` can be either a string containing IPv6 address, or a tuple (ipv6 address as string, port), + if no port is given, a random port number is used. + - `data` can be either a string containing the message to be sent, or an int indicating size of the message (a + random message with the given length will be used). + - `count` gives number of times the message will be sent (default is 1). + + Returns an `AsyncSender` object. + + """ + if isinstance(src, tuple): + src_addr = src[0] + src_port = src[1] + else: + src_addr = src + src_port = random.randint(49152, 65535) + + if isinstance(dst, tuple): + dst_addr = dst[0] + dst_port = dst[1] + else: + dst_addr = dst + dst_port = random.randint(49152, 65535) + + if isinstance(data, int): + # create a random message with the given length. + all_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,> timeout: + print 'Performing aysnc tx/tx took too long ({}>{} sec)'.format(elapsed_time, timeout) + raise Node._NodeError('perform_tx_rx timed out ({}>{} sec)'.format(elapsed_time, timeout)) + # perform a single asyncore loop + asyncore.loop(timeout=1, count=1) + except: + print 'Failed to perform async rx/tx' + raise + +#----------------------------------------------------------------------------------------------------------------------- +# `AsyncSender` and `AsyncReceiver classes + +_SO_BINDTODEVICE = 25 + +def _is_ipv6_addr_link_local(ip_addr): + """Indicates if a given IPv6 address is link-local""" + return ip_addr.lower().startswith('fe80::') + +def _create_socket_address(ip_address, port): + """Convert a given IPv6 address (string) and port number into a socket address""" + # `socket.getaddrinfo() returns a list of `(family, socktype, proto, canonname, sockaddr)` where `sockaddr` + # (at index 4) can be used as input in socket methods (like `sendto()`, `bind()`, etc.). + return socket.getaddrinfo(ip_address, port)[0][4] + +class AsyncSender(asyncore.dispatcher): + """ An IPv6 async message sender - use `Node.prepare_tx()` to create one""" + + def __init__(self, node, src_addr, src_port, dst_addr, dst_port, msg, count): + self._node = node + self._src_addr = src_addr + self._src_port = src_port + self._dst_addr = dst_addr + self._dst_port = dst_port + self._msg = msg + self._count = count + self._dst_sock_addr = _create_socket_address(dst_addr, dst_port) + self._tx_buffer = self._msg + self._tx_counter = 0 + + # Create a socket, bind it to the node's interface + sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, _SO_BINDTODEVICE, node.interface_name + '\0') + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + # Bind the socket to the given src address + if _is_ipv6_addr_link_local(src_addr): + # If src is a link local address it requires the interface name to be specified. + src_sock_addr = _create_socket_address(src_addr + '%' + node.interface_name, src_port) + else: + src_sock_addr = _create_socket_address(src_addr, src_port) + sock.bind(src_sock_addr) + + asyncore.dispatcher.__init__(self, sock) + + # Property getters + + @property + def node(self): + return self._node + + @property + def src_addr(self): + return self._src_addr + + @property + def src_port(self): + return self._src_port + + @property + def dst_addr(self): + return self._dst_addr + + @property + def dst_port(self): + return self._dst_port + + @property + def msg(self): + return self._msg + + @property + def count(self): + return self._count + + @property + def was_successful(self): + """Indicates if the transmission of IPv6 messages finished successfully""" + return self._tx_counter == self._count + + # asyncore.dispatcher callbacks + + def readable(self): + return False + + def writable(self): + return True + + def handle_write(self): + sent_len = self.sendto(self._tx_buffer, self._dst_sock_addr) + + if self._node._verbose: + if sent_len < 30: + info_text = '{} bytes ("{}")'.format(sent_len, self._tx_buffer[:sent_len]) + else: + info_text = '{} bytes'.format(sent_len) + _log('- Node{} sent {} to [{}]:{} from [{}]:{}'.format(self._node._index, info_text, + self._dst_addr, self._dst_port, + self._src_addr, self._src_port)) + + self._tx_buffer = self._tx_buffer[sent_len:] + + if len(self._tx_buffer) == 0: + self._tx_counter += 1 + if self._tx_counter < self._count: + self._tx_buffer = self._msg + else: + self.handle_close() + + def handle_close(self): + self.close() + +#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +class AsyncReceiver(asyncore.dispatcher): + """ An IPv6 async message receiver - use `prepare_tx()` to create one""" + + _MAX_RECV_SIZE = 2048 + + class _SenderInfo(object): + def __init__(self, sender_addr, sender_port, msg, count): + self._sender_addr = sender_addr + self._sender_port = sender_port + self._msg = msg + self._count = count + self._rx_counter = 0 + + def _check_received(self, msg, sender_addr, sender_port): + if self._msg == msg and self._sender_addr == sender_addr and self._sender_port == sender_port: + self._rx_counter += 1 + return self._did_recv_all() + + def _did_recv_all(self): + return self._rx_counter >= self._count + + def __init__(self, node, local_port): + self._node = node + self._local_port = local_port + self._senders = [] # list of `_SenderInfo` objects + self._all_rx = [] # contains all received messages as a list of (pkt, (src_addr, src_port)) + + # Create a socket, bind it to the node's interface + sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, _SO_BINDTODEVICE, node.interface_name + '\0') + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + # Bind the socket to any IPv6 address with the given local port + local_sock_addr = _create_socket_address('::', local_port) + sock.bind(local_sock_addr) + + asyncore.dispatcher.__init__(self, sock) + + def _add_sender(self, sender_addr, sender_port, msg, count): + self._senders.append(AsyncReceiver._SenderInfo(sender_addr, sender_port, msg, count)) + + # Property getters + + @property + def node(self): + return self._node + + @property + def local_port(self): + return self._local_port + + @property + def all_rx_msg(self): + """returns all received messages as a list of (msg, (src_addr, src_port))""" + return self._all_rx + + @property + def was_successful(self): + """Indicates if all expected IPv6 messages were received successfully""" + return all([sender._did_recv_all() for sender in self._senders]) + + # asyncore.dispatcher callbacks + + def readable(self): + return True + + def writable(self): + return False + + def handle_read(self): + (msg, src_sock_addr) = self.recvfrom(AsyncReceiver._MAX_RECV_SIZE) + src_addr = src_sock_addr[0] + src_port = src_sock_addr[1] + + if (_is_ipv6_addr_link_local(src_addr)): + if '%' in src_addr: + src_addr = src_addr.split('%')[0] # remove the interface name from address + + if self._node._verbose: + if len(msg) < 30: + info_text = '{} bytes ("{}")'.format(len(msg), msg) + else: + info_text = '{} bytes'.format(len(msg)) + _log('- Node{} received {} on port {} from [{}]:{}'.format(self._node._index, info_text, + self._local_port, + src_addr, src_port)) + + self._all_rx.append((msg, (src_addr, src_port))) + + if all([sender._check_received(msg, src_addr, src_port) for sender in self._senders]): + self.handle_close() + + def handle_close(self): + self.close() + # remove the receiver from the node once the socket is closed + self._node._remove_recver(self) + +#----------------------------------------------------------------------------------------------------------------------- + +def verify(condition): + if not condition: + print 'verify() failed' + exit(1) + + +#----------------------------------------------------------------------------------------------------------------------- +# Parsing `wpanctl` output + +class ScanResult(object): + """ This object encapsulates a scan result (active/discover/energy scan)""" + + TYPE_ACTIVE_SCAN = 'active-scan' + TYPE_DISCOVERY_SCAN = 'discover-scan' + TYPE_ENERGY_SCAN = 'energy-scan' + + def __init__(self, result_text): + + items = [item.strip() for item in result_text.split('|')] + + if len(items) == 8: + self._type = ScanResult.TYPE_ACTIVE_SCAN + self._index = items[0] + self._joinable = (items[1] == 'YES') + self._network_name = items[2][1:-1] + self._panid = items[3] + self._channel = items[4] + self._xpanid = items[5] + self._ext_address = items[6] + self._rssi = items[7] + elif len(items) == 7: + self._type = ScanResult.TYPE_DISCOVERY_SCAN + self._index = items[0] + self._network_name = items[1][1:-1] + self._panid = items[2] + self._channel = items[3] + self._xpanid = items[4] + self._ext_address = items[5] + self._rssi = items[6] + elif len(items) == 2: + self._type = ScanResult.TYPE_ENERGY_SCAN + self._channel = items[0] + self._rssi = items[1] + else: + raise ValueError('"{}" does not seem to be a valid scan result string'.result_text) + + @property + def type(self): + return self._type + + @property + def joinable(self): + return self._joinable + + @property + def network_name(self): + return self._network_name + + @property + def panid(self): + return self._panid + + @property + def channel(self): + return self._channel + + @property + def xpanid(self): + return self._xpanid + + @property + def ext_address(self): + return self._ext_address + + @property + def rssi(self): + return self._rssi + + def __repr__(self): + return 'ScanResult({})'.format(self.__dict__) + +def parse_scan_result(scan_result): + """ Parses scan result string and returns an array of `ScanResult` objects""" + return [ ScanResult(item) for item in scan_result.split('\n')[2:] ] # skip first two lines which are table headers + +