1#!/usr/bin/env python3 2# 3# Copyright (c) 2018, The OpenThread Authors. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 1. Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# 2. Redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# 3. Neither the name of the copyright holder nor the 14# names of its contributors may be used to endorse or promote products 15# derived from this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27# POSSIBILITY OF SUCH DAMAGE. 28# 29 30import sys 31import os 32import time 33import re 34import random 35import weakref 36import subprocess 37import socket 38import asyncore 39import inspect 40 41# ---------------------------------------------------------------------------------------------------------------------- 42# wpantund properties 43 44WPAN_STATE = 'NCP:State' 45WPAN_NAME = 'Network:Name' 46WPAN_PANID = 'Network:PANID' 47WPAN_XPANID = 'Network:XPANID' 48WPAN_KEY = 'Network:Key' 49WPAN_KEY_INDEX = 'Network:KeyIndex' 50WPAN_CHANNEL = 'NCP:Channel' 51WPAN_HW_ADDRESS = 'NCP:HardwareAddress' 52WPAN_EXT_ADDRESS = 'NCP:ExtendedAddress' 53WPAN_POLL_INTERVAL = 'NCP:SleepyPollInterval' 54WPAN_NODE_TYPE = 'Network:NodeType' 55WPAN_ROLE = 'Network:Role' 56WPAN_PARTITION_ID = 'Network:PartitionId' 57WPAN_NCP_VERSION = 'NCP:Version' 58WPAN_NCP_MCU_POWER_STATE = "NCP:MCUPowerState" 59WPAN_NETWORK_ALLOW_JOIN = 'com.nestlabs.internal:Network:AllowingJoin' 60WPAN_NETWORK_PASSTHRU_PORT = 'com.nestlabs.internal:Network:PassthruPort' 61WPAN_RCP_VERSION = "POSIXApp:RCPVersion" 62 63WPAN_IP6_LINK_LOCAL_ADDRESS = "IPv6:LinkLocalAddress" 64WPAN_IP6_MESH_LOCAL_ADDRESS = "IPv6:MeshLocalAddress" 65WPAN_IP6_MESH_LOCAL_PREFIX = "IPv6:MeshLocalPrefix" 66WPAN_IP6_ALL_ADDRESSES = "IPv6:AllAddresses" 67WPAN_IP6_MULTICAST_ADDRESSES = "IPv6:MulticastAddresses" 68WPAN_IP6_INTERFACE_ROUTES = "IPv6:Routes" 69 70WPAN_DAEMON_OFF_MESH_ROUTE_AUTO_ADD_ON_INTERFACE = "Daemon:OffMeshRoute:AutoAddOnInterface" 71WPAN_DAEMON_OFF_MESH_ROUTE_FILTER_SELF_AUTO_ADDED = "Daemon:OffMeshRoute:FilterSelfAutoAdded" 72WPAN_DAEMON_ON_MESH_PREFIX_AUTO_ADD_AS_INTERFACE_ROUTE = "Daemon:OnMeshPrefix:AutoAddAsInterfaceRoute" 73 74WPAN_THREAD_RLOC16 = "Thread:RLOC16" 75WPAN_THREAD_ROUTER_ID = "Thread:RouterID" 76WPAN_THREAD_LEADER_ADDRESS = "Thread:Leader:Address" 77WPAN_THREAD_LEADER_ROUTER_ID = "Thread:Leader:RouterID" 78WPAN_THREAD_LEADER_WEIGHT = "Thread:Leader:Weight" 79WPAN_THREAD_LEADER_LOCAL_WEIGHT = "Thread:Leader:LocalWeight" 80WPAN_THREAD_LEADER_NETWORK_DATA = "Thread:Leader:NetworkData" 81WPAN_THREAD_STABLE_LEADER_NETWORK_DATA = "Thread:Leader:StableNetworkData" 82WPAN_THREAD_NETWORK_DATA = "Thread:NetworkData" 83WPAN_THREAD_CHILD_TABLE = "Thread:ChildTable" 84WPAN_THREAD_CHILD_TABLE_ASVALMAP = "Thread:ChildTable:AsValMap" 85WPAN_THREAD_CHILD_TABLE_ADDRESSES = "Thread:ChildTable:Addresses" 86WPAN_THREAD_NEIGHBOR_TABLE = "Thread:NeighborTable" 87WPAN_THREAD_NEIGHBOR_TABLE_ASVALMAP = "Thread:NeighborTable:AsValMap" 88WPAN_THREAD_NEIGHBOR_TABLE_ERR_RATES = "Thread:NeighborTable:ErrorRates" 89WPAN_THREAD_NEIGHBOR_TABLE_ERR_RATES_AVVALMAP = "Thread:NeighborTable:ErrorRates:AsValMap" 90WPAN_THREAD_ROUTER_TABLE = "Thread:RouterTable" 91WPAN_THREAD_ROUTER_TABLE_ASVALMAP = "Thread:RouterTable:AsValMap" 92WPAN_THREAD_CHILD_TIMEOUT = "Thread:ChildTimeout" 93WPAN_THREAD_PARENT = "Thread:Parent" 94WPAN_THREAD_PARENT_ASVALMAP = "Thread:Parent:AsValMap" 95WPAN_THREAD_NETWORK_DATA_VERSION = "Thread:NetworkDataVersion" 96WPAN_THREAD_STABLE_NETWORK_DATA = "Thread:StableNetworkData" 97WPAN_THREAD_STABLE_NETWORK_DATA_VERSION = "Thread:StableNetworkDataVersion" 98WPAN_THREAD_PREFERRED_ROUTER_ID = "Thread:PreferredRouterID" 99WPAN_THREAD_COMMISSIONER_ENABLED = "Thread:Commissioner:Enabled" 100WPAN_THREAD_DEVICE_MODE = "Thread:DeviceMode" 101WPAN_THREAD_OFF_MESH_ROUTES = "Thread:OffMeshRoutes" 102WPAN_THREAD_ON_MESH_PREFIXES = "Thread:OnMeshPrefixes" 103WPAN_THREAD_ROUTER_ROLE_ENABLED = "Thread:RouterRole:Enabled" 104WPAN_THREAD_CONFIG_FILTER_RLOC_ADDRESSES = "Thread:Config:FilterRLOCAddresses" 105WPAN_THREAD_ROUTER_UPGRADE_THRESHOLD = "Thread:RouterUpgradeThreshold" 106WPAN_THREAD_ROUTER_DOWNGRADE_THRESHOLD = "Thread:RouterDowngradeThreshold" 107WPAN_THREAD_ACTIVE_DATASET = "Thread:ActiveDataset" 108WPAN_THREAD_ACTIVE_DATASET_ASVALMAP = "Thread:ActiveDataset:AsValMap" 109WPAN_THREAD_PENDING_DATASET = "Thread:PendingDataset" 110WPAN_THREAD_PENDING_DATASET_ASVALMAP = "Thread:PendingDataset:AsValMap" 111WPAN_THREAD_ADDRESS_CACHE_TABLE = "Thread:AddressCacheTable" 112WPAN_THREAD_ADDRESS_CACHE_TABLE_ASVALMAP = "Thread:AddressCacheTable:AsValMap" 113WPAN_THREAD_JOINER_DISCERNER_VALUE = "Joiner:Discerner:Value" 114WPAN_THREAD_JOINER_DISCERNER_BIT_LENGTH = "Joiner:Discerner:BitLength" 115WPAN_THREAD_COMMISSIONER_JOINERS = "Commissioner:Joiners" 116 117WPAN_OT_LOG_LEVEL = "OpenThread:LogLevel" 118WPAN_OT_SLAAC_ENABLED = "OpenThread:SLAAC:Enabled" 119WPAN_OT_STEERING_DATA_ADDRESS = "OpenThread:SteeringData:Address" 120WPAN_OT_STEERING_DATA_SET_WHEN_JOINABLE = "OpenThread:SteeringData:SetWhenJoinable" 121WPAN_OT_MSG_BUFFER_COUNTERS = "OpenThread:MsgBufferCounters" 122WPAN_OT_MSG_BUFFER_COUNTERS_AS_STRING = "OpenThread:MsgBufferCounters:AsString" 123WPAN_OT_DEBUG_TEST_ASSERT = "OpenThread:Debug:TestAssert" 124WPAN_OT_DEBUG_TEST_WATCHDOG = "OpenThread:Debug:TestWatchdog" 125WPAN_OT_SUPPORTED_RADIO_LINKS = "OpenThread:SupportedRadioLinks" 126WPAN_OT_NEIGHBOR_TABLE_MULTI_RADIO_INFO = "OpenThread:NeighborTable::MultiRadioInfo" 127WPAN_OT_TREL_TEST_MODE_ENABLE = "OpenThread:Trel:TestMode:Enable" 128 129WPAN_MAC_ALLOWLIST_ENABLED = "MAC:Allowlist:Enabled" 130WPAN_MAC_ALLOWLIST_ENTRIES = "MAC:Allowlist:Entries" 131WPAN_MAC_ALLOWLIST_ENTRIES_ASVALMAP = "MAC:Allowlist:Entries:AsValMap" 132WPAN_MAC_DENYLIST_ENABLED = "MAC:Denylist:Enabled" 133WPAN_MAC_DENYLIST_ENTRIES = "MAC:Denylist:Entries" 134WPAN_MAC_DENYLIST_ENTRIES_ASVALMAP = "MAC:Denylist:Entries:AsValMap" 135 136WPAN_MAC_FILTER_FIXED_RSSI = "MAC:Filter:FixedRssi" 137WPAN_MAC_FILTER_ENTRIES = "MAC:Filter:Entries" 138WPAN_MAC_FILTER_ENTRIES_ASVALMAP = "MAC:Filter:Entries:AsValMap" 139 140WPAN_CHILD_SUPERVISION_INTERVAL = "ChildSupervision:Interval" 141WPAN_CHILD_SUPERVISION_CHECK_TIMEOUT = "ChildSupervision:CheckTimeout" 142 143WPAN_JAM_DETECTION_STATUS = "JamDetection:Status" 144WPAN_JAM_DETECTION_ENABLE = "JamDetection:Enable" 145WPAN_JAM_DETECTION_RSSI_THRESHOLD = "JamDetection:RssiThreshold" 146WPAN_JAM_DETECTION_WINDOW = "JamDetection:Window" 147WPAN_JAM_DETECTION_BUSY_PERIOD = "JamDetection:BusyPeriod" 148WPAN_JAM_DETECTION_DEBUG_HISTORY_BITMAP = "JamDetection:Debug:HistoryBitmap" 149 150WPAN_CHANNEL_MONITOR_SAMPLE_INTERVAL = "ChannelMonitor:SampleInterval" 151WPAN_CHANNEL_MONITOR_RSSI_THRESHOLD = "ChannelMonitor:RssiThreshold" 152WPAN_CHANNEL_MONITOR_SAMPLE_WINDOW = "ChannelMonitor:SampleWindow" 153WPAN_CHANNEL_MONITOR_SAMPLE_COUNT = "ChannelMonitor:SampleCount" 154WPAN_CHANNEL_MONITOR_CHANNEL_QUALITY = "ChannelMonitor:ChannelQuality" 155WPAN_CHANNEL_MONITOR_CHANNEL_QUALITY_ASVALMAP = "ChannelMonitor:ChannelQuality:AsValMap" 156 157WPAN_CHANNEL_MANAGER_NEW_CHANNEL = "ChannelManager:NewChannel" 158WPAN_CHANNEL_MANAGER_DELAY = "ChannelManager:Delay" 159WPAN_CHANNEL_MANAGER_CHANNEL_SELECT = "ChannelManager:ChannelSelect" 160WPAN_CHANNEL_MANAGER_AUTO_SELECT_ENABLED = "ChannelManager:AutoSelect:Enabled" 161WPAN_CHANNEL_MANAGER_AUTO_SELECT_INTERVAL = "ChannelManager:AutoSelect:Interval" 162WPAN_CHANNEL_MANAGER_SUPPORTED_CHANNEL_MASK = "ChannelManager:SupportedChannelMask" 163WPAN_CHANNEL_MANAGER_FAVORED_CHANNEL_MASK = "ChannelManager:FavoredChannelMask" 164 165WPAN_NCP_COUNTER_ALL_MAC = "NCP:Counter:AllMac" 166WPAN_NCP_COUNTER_ALL_MAC_ASVALMAP = "NCP:Counter:AllMac:AsValMap" 167WPAN_NCP_COUNTER_TX_PKT_TOTAL = "NCP:Counter:TX_PKT_TOTAL" 168WPAN_NCP_COUNTER_TX_PKT_UNICAST = "NCP:Counter:TX_PKT_UNICAST" 169WPAN_NCP_COUNTER_TX_PKT_BROADCAST = "NCP:Counter:TX_PKT_BROADCAST" 170WPAN_NCP_COUNTER_TX_PKT_ACK_REQ = "NCP:Counter:TX_PKT_ACK_REQ" 171WPAN_NCP_COUNTER_TX_PKT_ACKED = "NCP:Counter:TX_PKT_ACKED" 172WPAN_NCP_COUNTER_TX_PKT_NO_ACK_REQ = "NCP:Counter:TX_PKT_NO_ACK_REQ" 173WPAN_NCP_COUNTER_TX_PKT_DATA = "NCP:Counter:TX_PKT_DATA" 174WPAN_NCP_COUNTER_TX_PKT_DATA_POLL = "NCP:Counter:TX_PKT_DATA_POLL" 175WPAN_NCP_COUNTER_TX_PKT_BEACON = "NCP:Counter:TX_PKT_BEACON" 176WPAN_NCP_COUNTER_TX_PKT_BEACON_REQ = "NCP:Counter:TX_PKT_BEACON_REQ" 177WPAN_NCP_COUNTER_TX_PKT_OTHER = "NCP:Counter:TX_PKT_OTHER" 178WPAN_NCP_COUNTER_TX_PKT_RETRY = "NCP:Counter:TX_PKT_RETRY" 179WPAN_NCP_COUNTER_TX_ERR_CCA = "NCP:Counter:TX_ERR_CCA" 180WPAN_NCP_COUNTER_TX_ERR_ABORT = "NCP:Counter:TX_ERR_ABORT" 181WPAN_NCP_COUNTER_RX_PKT_TOTAL = "NCP:Counter:RX_PKT_TOTAL" 182WPAN_NCP_COUNTER_RX_PKT_UNICAST = "NCP:Counter:RX_PKT_UNICAST" 183WPAN_NCP_COUNTER_RX_PKT_BROADCAST = "NCP:Counter:RX_PKT_BROADCAST" 184WPAN_NCP_COUNTER_RX_PKT_DATA = "NCP:Counter:RX_PKT_DATA" 185WPAN_NCP_COUNTER_RX_PKT_DATA_POLL = "NCP:Counter:RX_PKT_DATA_POLL" 186WPAN_NCP_COUNTER_RX_PKT_BEACON = "NCP:Counter:RX_PKT_BEACON" 187WPAN_NCP_COUNTER_RX_PKT_BEACON_REQ = "NCP:Counter:RX_PKT_BEACON_REQ" 188WPAN_NCP_COUNTER_RX_PKT_OTHER = "NCP:Counter:RX_PKT_OTHER" 189WPAN_NCP_COUNTER_RX_PKT_FILT_WL = "NCP:Counter:RX_PKT_FILT_WL" 190WPAN_NCP_COUNTER_RX_PKT_FILT_DA = "NCP:Counter:RX_PKT_FILT_DA" 191WPAN_NCP_COUNTER_RX_ERR_EMPTY = "NCP:Counter:RX_ERR_EMPTY" 192WPAN_NCP_COUNTER_RX_ERR_UKWN_NBR = "NCP:Counter:RX_ERR_UKWN_NBR" 193WPAN_NCP_COUNTER_RX_ERR_NVLD_SADDR = "NCP:Counter:RX_ERR_NVLD_SADDR" 194WPAN_NCP_COUNTER_RX_ERR_SECURITY = "NCP:Counter:RX_ERR_SECURITY" 195WPAN_NCP_COUNTER_RX_ERR_BAD_FCS = "NCP:Counter:RX_ERR_BAD_FCS" 196WPAN_NCP_COUNTER_RX_ERR_OTHER = "NCP:Counter:RX_ERR_OTHER" 197WPAN_NCP_COUNTER_TX_IP_SEC_TOTAL = "NCP:Counter:TX_IP_SEC_TOTAL" 198WPAN_NCP_COUNTER_TX_IP_INSEC_TOTAL = "NCP:Counter:TX_IP_INSEC_TOTAL" 199WPAN_NCP_COUNTER_TX_IP_DROPPED = "NCP:Counter:TX_IP_DROPPED" 200WPAN_NCP_COUNTER_RX_IP_SEC_TOTAL = "NCP:Counter:RX_IP_SEC_TOTAL" 201WPAN_NCP_COUNTER_RX_IP_INSEC_TOTAL = "NCP:Counter:RX_IP_INSEC_TOTAL" 202WPAN_NCP_COUNTER_RX_IP_DROPPED = "NCP:Counter:RX_IP_DROPPED" 203WPAN_NCP_COUNTER_TX_SPINEL_TOTAL = "NCP:Counter:TX_SPINEL_TOTAL" 204WPAN_NCP_COUNTER_RX_SPINEL_TOTAL = "NCP:Counter:RX_SPINEL_TOTAL" 205WPAN_NCP_COUNTER_RX_SPINEL_ERR = "NCP:Counter:RX_SPINEL_ERR" 206WPAN_NCP_COUNTER_IP_TX_SUCCESS = "NCP:Counter:IP_TX_SUCCESS" 207WPAN_NCP_COUNTER_IP_RX_SUCCESS = "NCP:Counter:IP_RX_SUCCESS" 208WPAN_NCP_COUNTER_IP_TX_FAILURE = "NCP:Counter:IP_TX_FAILURE" 209WPAN_NCP_COUNTER_IP_RX_FAILURE = "NCP:Counter:IP_RX_FAILURE" 210 211# ---------------------------------------------------------------------------------------------------------------------- 212# Valid state values 213 214STATE_UNINITIALIZED = '"uninitialized"' 215STATE_FAULT = '"uninitialized:fault"' 216STATE_UPGRADING = '"uninitialized:upgrading"' 217STATE_DEEP_SLEEP = '"offline:deep-sleep"' 218STATE_OFFLINE = '"offline"' 219STATE_COMMISSIONED = '"offline:commissioned"' 220STATE_ASSOCIATING = '"associating"' 221STATE_CREDENTIALS_NEEDED = '"associating:credentials-needed"' 222STATE_ASSOCIATED = '"associated"' 223STATE_ISOLATED = '"associated:no-parent"' 224STATE_NETWAKE_ASLEEP = '"associated:netwake-asleep"' 225STATE_NETWAKE_WAKING = '"associated:netwake-waking"' 226 227# ----------------------------------------------------------------------------------------------------------------------- 228# MCU Power state from `WPAN_NCP_MCU_POWER_STATE` 229 230MCU_POWER_STATE_ON = '"on"' 231MCU_POWER_STATE_LOW_POWER = '"low-power"' 232MCU_POWER_STATE_OFF = '"off"' 233 234# ----------------------------------------------------------------------------------------------------------------------- 235# Node Radio Link Types (Use as input to `Node()` initializer) 236 237NODE_15_4 = "-15.4" 238NODE_TREL = "-trel" 239NODE_15_4_TREL = "-15.4-trel" 240 241# ----------------------------------------------------------------------------------------------------------------------- 242# Node types (from `WPAN_NODE_TYPE` property) 243 244NODE_TYPE_UNKNOWN = '"unknown"' 245NODE_TYPE_LEADER = '"leader"' 246NODE_TYPE_ROUTER = '"router"' 247NODE_TYPE_END_DEVICE = '"end-device"' 248NODE_TYPE_SLEEPY_END_DEVICE = '"sleepy-end-device"' 249NODE_TYPE_COMMISSIONER = '"commissioner"' 250NODE_TYPE_NEST_LURKER = '"nl-lurker"' 251 252# ----------------------------------------------------------------------------------------------------------------------- 253# Node types used by `Node.join()` 254 255JOIN_TYPE_ROUTER = 'r' 256JOIN_TYPE_END_DEVICE = 'e' 257JOIN_TYPE_SLEEPY_END_DEVICE = 's' 258 259# ----------------------------------------------------------------------------------------------------------------------- 260# Address Cache Table Entry States 261 262ADDRESS_CACHE_ENTRY_STATE_CACHED = "cached" 263ADDRESS_CACHE_ENTRY_STATE_SNOOPED = "snooped" 264ADDRESS_CACHE_ENTRY_STATE_QUERY = "query" 265ADDRESS_CACHE_ENTRY_STATE_RETRY_QUERY = "retry-query" 266 267# ----------------------------------------------------------------------------------------------------------------------- 268# Bit Flags for Thread Device Mode `WPAN_THREAD_DEVICE_MODE` 269 270THREAD_MODE_FLAG_FULL_NETWORK_DATA = (1 << 0) 271THREAD_MODE_FLAG_FULL_THREAD_DEV = (1 << 1) 272THREAD_MODE_FLAG_RX_ON_WHEN_IDLE = (1 << 3) 273 274# ----------------------------------------------------------------------------------------------------------------------- 275# Radio Link type 276 277RADIO_LINK_IEEE_802_15_4 = "IEEE_802_15_4" 278RADIO_LINK_TREL_UDP6 = "TREL_UDP6" 279RADIO_LINK_TOBLE = "TOBLE" 280 281_OT_BUILDDIR = os.getenv('top_builddir', '../..') 282_WPANTUND_PREFIX = os.getenv('WPANTUND_PREFIX', '/usr/local') 283 284# ----------------------------------------------------------------------------------------------------------------------- 285 286 287def _log(text, new_line=True, flush=True): 288 sys.stdout.write(text) 289 if new_line: 290 sys.stdout.write('\n') 291 if flush: 292 sys.stdout.flush() 293 294 295# ----------------------------------------------------------------------------------------------------------------------- 296# Node class 297 298 299class Node(object): 300 """ A wpantund OT NCP instance """ 301 # defines the default verbosity setting (can be changed per `Node`) 302 _VERBOSE = os.getenv('TORANJ_VERBOSE', 'no').lower() in ['true', '1', 't', 'y', 'yes', 'on'] 303 _SPEED_UP_FACTOR = 1 # defines the default time speed up factor 304 305 # path to `wpantund`, `wpanctl` and `ot-ncp-ftd` 306 _WPANTUND = '%s/sbin/wpantund' % _WPANTUND_PREFIX 307 _WPANCTL = '%s/bin/wpanctl' % _WPANTUND_PREFIX 308 309 _OT_NCP_FTD = '%s/examples/apps/ncp/ot-ncp-ftd' % _OT_BUILDDIR 310 311 # determines if the wpantund logs are saved in file or sent to stdout 312 _TUND_LOG_TO_FILE = True 313 # name of wpantund log file (if # name of wpantund _TUND_LOG_TO_FILE is 314 # True) 315 _TUND_LOG_FNAME = 'wpantund-logs' 316 317 # interface name 318 _INTFC_NAME_PREFIX = 'utun' if sys.platform == 'darwin' else 'wpan' 319 _START_INDEX = 4 if sys.platform == 'darwin' else 1 320 321 _cur_index = _START_INDEX 322 _all_nodes = weakref.WeakSet() 323 324 def __init__(self, radios=None, verbose=_VERBOSE): 325 """Creates a new `Node` instance""" 326 327 index = Node._cur_index 328 Node._cur_index += 1 329 330 self._index = index 331 self._interface_name = self._INTFC_NAME_PREFIX + str(index) 332 self._verbose = verbose 333 334 ncp_socket_path = 'system:{}{} {} --time-speed={}'.format(self._OT_NCP_FTD, '' if radios is None else radios, 335 index, self._SPEED_UP_FACTOR) 336 337 cmd = self._WPANTUND + \ 338 ' -o Config:NCP:SocketPath \"{}\"'.format(ncp_socket_path) + \ 339 ' -o Config:TUN:InterfaceName {}'.format(self._interface_name) + \ 340 ' -o Config:NCP:DriverName spinel' + \ 341 ' -o Daemon:SyslogMask \"all -debug\"' 342 343 if Node._TUND_LOG_TO_FILE: 344 self._tund_log_file = open(self._TUND_LOG_FNAME + str(index) + '.log', 'wb') 345 else: 346 self._tund_log_file = None 347 348 if self._verbose: 349 _log('$ Node{}.__init__() cmd: {}'.format(index, cmd)) 350 351 self._wpantund_process = subprocess.Popen(cmd, shell=True, stderr=self._tund_log_file) 352 353 self._wpanctl_cmd = self._WPANCTL + ' -I ' + self._interface_name + ' ' 354 355 # map from local_port to `AsyncReceiver` object 356 self._recvers = weakref.WeakValueDictionary() 357 Node._all_nodes.add(self) 358 359 def __del__(self): 360 self._wpantund_process.poll() 361 if self._wpantund_process.returncode is None: 362 self._wpantund_process.terminate() 363 self._wpantund_process.wait() 364 365 def __repr__(self): 366 return 'Node (index={}, interface_name={})'.format(self._index, self._interface_name) 367 368 @property 369 def index(self): 370 return self._index 371 372 @property 373 def interface_name(self): 374 return self._interface_name 375 376 @property 377 def tund_log_file(self): 378 return self._tund_log_file 379 380 # ------------------------------------------------------------------------------------------------------------------ 381 # Executing a `wpanctl` command 382 383 def wpanctl(self, cmd): 384 """ Runs a wpanctl command on the given wpantund/OT-NCP instance and returns the output """ 385 386 if self._verbose: 387 _log('$ Node{}.wpanctl(\'{}\')'.format(self._index, cmd), new_line=False) 388 389 result = subprocess.check_output(self._wpanctl_cmd + cmd, shell=True, stderr=subprocess.STDOUT) 390 391 if len(result) >= 1 and result[-1] == '\n': # remove the last char if it is '\n', 392 result = result[:-1] 393 394 if self._verbose: 395 if '\n' in result: 396 _log(':') 397 for line in result.splitlines(): 398 _log(' ' + line) 399 else: 400 _log(' -> \'{}\''.format(result)) 401 402 return result 403 404 # ------------------------------------------------------------------------------------------------------------------ 405 # APIs matching `wpanctl` commands. 406 407 def get(self, prop_name, value_only=True): 408 return self.wpanctl('get ' + ('-v ' if value_only else '') + prop_name) 409 410 def set(self, prop_name, value, binary_data=False): 411 return self._update_prop('set', prop_name, value, binary_data) 412 413 def add(self, prop_name, value, binary_data=False): 414 return self._update_prop('add', prop_name, value, binary_data) 415 416 def remove(self, prop_name, value, binary_data=False): 417 return self._update_prop('remove', prop_name, value, binary_data) 418 419 def _update_prop(self, action, prop_name, value, binary_data): 420 return self.wpanctl(action + ' ' + prop_name + ' ' + ('-d ' if binary_data else '') + '-v ' + 421 value) # use -v to handle values starting with `-`. 422 423 def reset(self): 424 return self.wpanctl('reset') 425 426 def status(self): 427 return self.wpanctl('status') 428 429 def leave(self): 430 return self.wpanctl('leave') 431 432 def form(self, 433 name, 434 channel=None, 435 channel_mask=None, 436 panid=None, 437 xpanid=None, 438 key=None, 439 key_index=None, 440 node_type=None, 441 mesh_local_prefix=None, 442 legacy_prefix=None): 443 return self.wpanctl('form \"' + name + '\"' + (' -c {}'.format(channel) if channel is not None else '') + 444 (' -m {}'.format(channel_mask) if channel_mask is not None else '') + 445 (' -p {}'.format(panid) if panid is not None else '') + 446 (' -x {}'.format(xpanid) if xpanid is not None else '') + 447 (' -k {}'.format(key) if key is not None else '') + 448 (' -i {}'.format(key_index) if key_index is not None else '') + 449 (' -T {}'.format(node_type) if node_type is not None else '') + 450 (' -M {}'.format(mesh_local_prefix) if mesh_local_prefix is not None else '') + 451 (' -L {}'.format(legacy_prefix) if legacy_prefix is not None else '')) 452 453 def join(self, name, channel=None, node_type=None, panid=None, xpanid=None, key=None): 454 return self.wpanctl('join \"' + name + '\"' + (' -c {}'.format(channel) if channel is not None else '') + 455 (' -T {}'.format(node_type) if node_type is not None else '') + 456 (' -p {}'.format(panid) if panid is not None else '') + 457 (' -x {}'.format(xpanid) if xpanid is not None else '') + 458 (' -k {}'.format(key) if key is not None else '') + (' -n')) 459 460 def active_scan(self, channel=None): 461 return self.wpanctl('scan' + (' -c {}'.format(channel) if channel is not None else '')) 462 463 def energy_scan(self, channel=None): 464 return self.wpanctl('scan -e' + (' -c {}'.format(channel) if channel is not None else '')) 465 466 def discover_scan(self, channel=None, joiner_only=False, enable_filtering=False, panid_filter=None): 467 return self.wpanctl('scan -d' + (' -c {}'.format(channel) if channel is not None else '') + 468 (' -j' if joiner_only else '') + (' -f' if enable_filtering else '') + 469 (' -p {}'.format(panid_filter) if panid_filter is not None else '')) 470 471 def permit_join(self, duration_sec=None, port=None, udp=True, tcp=True): 472 if not udp and not tcp: # incorrect use! 473 return '' 474 traffic_type = '' 475 if udp and not tcp: 476 traffic_type = ' --udp' 477 if tcp and not udp: 478 traffic_type = ' --tcp' 479 if port is not None and duration_sec is None: 480 duration_sec = '240' 481 482 return self.wpanctl('permit-join' + (' {}'.format(duration_sec) if duration_sec is not None else '') + 483 (' {}'.format(port) if port is not None else '') + traffic_type) 484 485 def config_gateway(self, prefix, default_route=False, priority=None): 486 return self.wpanctl('config-gateway ' + prefix + (' -d' if default_route else '') + 487 (' -P {}'.format(priority) if priority is not None else '')) 488 489 def add_prefix(self, 490 prefix, 491 prefix_len=None, 492 priority=None, 493 stable=True, 494 on_mesh=False, 495 slaac=False, 496 dhcp=False, 497 configure=False, 498 default_route=False, 499 preferred=False): 500 return self.wpanctl('add-prefix ' + prefix + (' -l {}'.format(prefix_len) if prefix_len is not None else '') + 501 (' -P {}'.format(priority) if priority is not None else '') + (' -s' if stable else '') + 502 (' -f' if preferred else '') + (' -a' if slaac else '') + (' -d' if dhcp else '') + 503 (' -c' if configure else '') + (' -r' if default_route else '') + 504 (' -o' if on_mesh else '')) 505 506 def remove_prefix(self, prefix, prefix_len=None): 507 return self.wpanctl('remove-prefix ' + prefix + 508 (' -l {}'.format(prefix_len) if prefix_len is not None else '')) 509 510 def add_route(self, route_prefix, prefix_len=None, priority=None, stable=True): 511 """route priority [(>0 for high, 0 for medium, <0 for low)]""" 512 return self.wpanctl('add-route ' + route_prefix + 513 (' -l {}'.format(prefix_len) if prefix_len is not None else '') + 514 (' -p {}'.format(priority) if priority is not None else '') + ('' if stable else ' -n')) 515 516 def remove_route(self, route_prefix, prefix_len=None, priority=None, stable=True): 517 """route priority [(>0 for high, 0 for medium, <0 for low)]""" 518 return self.wpanctl('remove-route ' + route_prefix + 519 (' -l {}'.format(prefix_len) if prefix_len is not None else '') + 520 (' -p {}'.format(priority) if priority is not None else '')) 521 522 def commissioner_start(self): 523 return self.wpanctl('commissioner start') 524 525 def commissioner_add_joiner(self, eui64, pskd, timeout='100'): 526 return self.wpanctl('commissioner joiner-add {} {} {}'.format(eui64, timeout, pskd)) 527 528 def commissioner_add_joiner_with_discerner(self, discerner_value, discerner_bit_len, pskd, timeout='100'): 529 return self.wpanctl('commissioner joiner-add-discerner {} {} {} {}'.format(discerner_value, discerner_bit_len, 530 timeout, pskd)) 531 532 def joiner_join(self, pskd): 533 return self.wpanctl('joiner --join {}'.format(pskd)) 534 535 def joiner_attach(self): 536 return self.wpanctl('joiner --attach') 537 538 # ------------------------------------------------------------------------------------------------------------------ 539 # Helper methods 540 541 def is_associated(self): 542 return self.get(WPAN_STATE) == STATE_ASSOCIATED 543 544 def join_node(self, node, node_type=JOIN_TYPE_ROUTER, should_set_key=True): 545 """Join a network specified by another node, `node` should be a Node""" 546 547 if not node.is_associated(): 548 return "{} is not associated".format(node) 549 550 return self.join(node.get(WPAN_NAME)[1:-1], 551 channel=node.get(WPAN_CHANNEL), 552 node_type=node_type, 553 panid=node.get(WPAN_PANID), 554 xpanid=node.get(WPAN_XPANID), 555 key=node.get(WPAN_KEY)[1:-1] if should_set_key else None) 556 557 def allowlist_node(self, node): 558 """Adds a given node (of type `Node`) to the allowlist of `self` and enables allowlisting on `self`""" 559 560 self.add(WPAN_MAC_ALLOWLIST_ENTRIES, node.get(WPAN_EXT_ADDRESS)[1:-1]) 561 self.set(WPAN_MAC_ALLOWLIST_ENABLED, '1') 562 563 def un_allowlist_node(self, node): 564 """Removes a given node (of node `Node) from the allowlist""" 565 self.remove(WPAN_MAC_ALLOWLIST_ENTRIES, node.get(WPAN_EXT_ADDRESS)[1:-1]) 566 567 def is_in_scan_result(self, scan_result): 568 """Checks if node is in the scan results 569 `scan_result` must be an array of `ScanResult` object (see `parse_scan_result`). 570 """ 571 joinable = (self.get(WPAN_NETWORK_ALLOW_JOIN) == 'true') 572 panid = self.get(WPAN_PANID) 573 xpanid = self.get(WPAN_XPANID)[2:] 574 name = self.get(WPAN_NAME)[1:-1] 575 channel = self.get(WPAN_CHANNEL) 576 ext_address = self.get(WPAN_EXT_ADDRESS)[1:-1] 577 578 for item in scan_result: 579 if all([ 580 item.network_name == name, item.panid == panid, item.xpanid == xpanid, 581 item.channel == channel, item.ext_address == ext_address, 582 (item.type == ScanResult.TYPE_DISCOVERY_SCAN) or (item.joinable == joinable) 583 ]): 584 return True 585 586 return False 587 588 def find_ip6_address_with_prefix(self, prefix): 589 """Find an IPv6 address on node matching a given prefix. 590 `prefix` should be an string containing the prefix. 591 Returns a string containing the IPv6 address matching the prefix or empty string if no address found. 592 """ 593 if len(prefix) > 2 and prefix[-1] == ':' and prefix[-2] == ':': 594 prefix = prefix[:-1] 595 all_addrs = parse_list(self.get(WPAN_IP6_ALL_ADDRESSES)) 596 matched_addr = [addr for addr in all_addrs if addr.startswith(prefix)] 597 return matched_addr[0] if len(matched_addr) >= 1 else '' 598 599 def add_ip6_address_on_interface(self, address, prefix_len=64): 600 """Adds an IPv6 interface on the network interface. 601 `address` should be string containing the IPv6 address. 602 `prefix_len` is an `int` specifying the prefix length. 603 NOTE: this method uses linux `ip` command. 604 """ 605 cmd = 'ip -6 addr add ' + address + \ 606 '/{} dev '.format(prefix_len) + self.interface_name 607 if self._verbose: 608 _log('$ Node{} \'{}\')'.format(self._index, cmd)) 609 610 result = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) 611 return result 612 613 def remove_ip6_address_on_interface(self, address, prefix_len=64): 614 """Removes an IPv6 interface on the network interface. 615 `address` should be string containing the IPv6 address. 616 `prefix_len` is an `int` specifying the prefix length. 617 NOTE: this method uses linux `ip` command. 618 """ 619 cmd = 'ip -6 addr del ' + address + \ 620 '/{} dev '.format(prefix_len) + self.interface_name 621 if self._verbose: 622 _log('$ Node{} \'{}\')'.format(self._index, cmd)) 623 624 result = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) 625 return result 626 627 # ------------------------------------------------------------------------------------------------------------------ 628 # class methods 629 630 @classmethod 631 def init_all_nodes(cls, disable_logs=not _VERBOSE, wait_time=15): 632 """Issues a `wpanctl.leave` on all `Node` objects and waits for them to be ready""" 633 random.seed(123456) 634 time.sleep(0.5) 635 for node in Node._all_nodes: 636 start_time = time.time() 637 while True: 638 try: 639 node._wpantund_process.poll() 640 if node._wpantund_process.returncode is not None: 641 print('Node {} wpantund instance has terminated unexpectedly'.format(node)) 642 if disable_logs: 643 node.set(WPAN_OT_LOG_LEVEL, '0') 644 node.leave() 645 except subprocess.CalledProcessError as e: 646 if (node._verbose): 647 _log(' -> \'{}\' exit code: {}'.format(e.output, e.returncode)) 648 interval = time.time() - start_time 649 if interval > wait_time: 650 print('Took too long to init node {} ({}>{} sec)'.format(node, interval, wait_time)) 651 raise 652 except BaseException: 653 raise 654 else: 655 break 656 time.sleep(0.4) 657 658 @classmethod 659 def finalize_all_nodes(cls): 660 """Finalizes all previously created `Node` instances (stops the wpantund process)""" 661 for node in Node._all_nodes: 662 node._wpantund_process.terminate() 663 node._wpantund_process.wait() 664 665 @classmethod 666 def set_time_speedup_factor(cls, factor): 667 """Sets up the time speed up factor - should be set before creating any `Node` objects""" 668 if len(Node._all_nodes) != 0: 669 raise Node._NodeError('set_time_speedup_factor() cannot be called after creating a `Node`') 670 Node._SPEED_UP_FACTOR = factor 671 672 # ------------------------------------------------------------------------------------------------------------------ 673 # IPv6 message Sender and Receiver class 674 675 class _NodeError(Exception): 676 pass 677 678 def prepare_tx(self, src, dst, data=40, count=1, mcast_hops=None): 679 """Prepares an IPv6 msg transmission. 680 681 - `src` and `dst` can be either a string containing IPv6 address, or a tuple (ipv6 address as string, port), 682 if no port is given, a random port number is used. 683 - `data` can be either a string containing the message to be sent, or an int indicating size of the message (a 684 random message with the given length will be used). 685 - `count` gives number of times the message will be sent (default is 1). 686 - `mcast_hops` specifies multicast hop limit (only applicable for multicast tx). 687 688 Returns an `AsyncSender` object. 689 690 """ 691 if isinstance(src, tuple): 692 src_addr = src[0] 693 src_port = src[1] 694 else: 695 src_addr = src 696 src_port = random.randint(49152, 65535) 697 698 if isinstance(dst, tuple): 699 dst_addr = dst[0] 700 dst_port = dst[1] 701 else: 702 dst_addr = dst 703 dst_port = random.randint(49152, 65535) 704 705 if isinstance(data, int): 706 # create a random message with the given length. 707 all_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,><?;:[]=-+)(*&^%$#@' 708 msg = ''.join(random.choice(all_chars) for _ in range(data)) 709 else: 710 msg = data 711 712 return AsyncSender(self, src_addr, src_port, dst_addr, dst_port, msg, count, mcast_hops) 713 714 def _get_receiver(self, local_port): 715 # Gets or creates a receiver (an `AsyncReceiver`) tied to given port 716 # number 717 if local_port in self._recvers: 718 receiver = self._recvers[local_port] 719 else: 720 receiver = AsyncReceiver(self, local_port) 721 self._recvers[local_port] = receiver 722 return receiver 723 724 def _remove_recver(self, recvr): 725 # Removes a receiver from weak dictionary - called when the receiver is 726 # done and its socket is closed 727 local_port = recvr.local_port 728 if local_port in self._recvers: 729 del self._recvers[local_port] 730 731 def prepare_rx(self, sender): 732 """Prepare to receive messages from a sender (an `AsyncSender`)""" 733 receiver = self._get_receiver(sender.dst_port) 734 receiver._add_sender(sender.src_addr, sender.src_port, sender.msg, sender.count) 735 return receiver 736 737 def prepare_listener(self, local_port, timeout=1): 738 """Prepares a listener (an `AsyncReceiver`) listening on the given `local_port` for given `timeout` (sec)""" 739 receiver = self._get_receiver(local_port) 740 receiver._set_listen_timeout(timeout) 741 return receiver 742 743 @staticmethod 744 def perform_async_tx_rx(timeout=20): 745 """Called to perform all previously prepared async rx/listen and tx operations""" 746 try: 747 start_time = time.time() 748 while asyncore.socket_map: 749 elapsed_time = time.time() - start_time 750 if elapsed_time > timeout: 751 print('Performing aysnc tx/tx took too long ({}>{} sec)'.format(elapsed_time, timeout)) 752 raise Node._NodeError('perform_tx_rx timed out ({}>{} sec)'.format(elapsed_time, timeout)) 753 # perform a single asyncore loop 754 asyncore.loop(timeout=0.5, count=1) 755 except BaseException: 756 print('Failed to perform async rx/tx') 757 raise 758 759 760# ----------------------------------------------------------------------------------------------------------------------- 761# `AsyncSender` and `AsyncReceiver classes 762 763_SO_BINDTODEVICE = 25 764 765 766def _is_ipv6_addr_link_local(ip_addr): 767 """Indicates if a given IPv6 address is link-local""" 768 return ip_addr.lower().startswith('fe80::') 769 770 771def _create_socket_address(ip_address, port): 772 """Convert a given IPv6 address (string) and port number into a socket address""" 773 # `socket.getaddrinfo()` returns a list of `(family, socktype, proto, canonname, sockaddr)` where `sockaddr` 774 # (at index 4) can be used as input in socket methods (like `sendto()`, `bind()`, etc.). 775 return socket.getaddrinfo(ip_address, port)[0][4] 776 777 778class AsyncSender(asyncore.dispatcher): 779 """ An IPv6 async message sender - use `Node.prepare_tx()` to create one""" 780 781 def __init__(self, node, src_addr, src_port, dst_addr, dst_port, msg, count, mcast_hops=None): 782 self._node = node 783 self._src_addr = src_addr 784 self._src_port = src_port 785 self._dst_addr = dst_addr 786 self._dst_port = dst_port 787 self._msg = msg 788 self._count = count 789 self._dst_sock_addr = _create_socket_address(dst_addr, dst_port) 790 self._tx_buffer = self._msg 791 self._tx_counter = 0 792 793 # Create a socket, bind it to the node's interface 794 sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 795 sock.setsockopt(socket.SOL_SOCKET, _SO_BINDTODEVICE, node.interface_name + '\0') 796 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 797 798 # Set the IPV6_MULTICAST_HOPS 799 if mcast_hops is not None: 800 sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, mcast_hops) 801 802 # Bind the socket to the given src address 803 if _is_ipv6_addr_link_local(src_addr): 804 # If src is a link local address it requires the interface name to 805 # be specified. 806 src_sock_addr = _create_socket_address(src_addr + '%' + node.interface_name, src_port) 807 else: 808 src_sock_addr = _create_socket_address(src_addr, src_port) 809 sock.bind(src_sock_addr) 810 811 asyncore.dispatcher.__init__(self, sock) 812 813 # Property getters 814 815 @property 816 def node(self): 817 return self._node 818 819 @property 820 def src_addr(self): 821 return self._src_addr 822 823 @property 824 def src_port(self): 825 return self._src_port 826 827 @property 828 def dst_addr(self): 829 return self._dst_addr 830 831 @property 832 def dst_port(self): 833 return self._dst_port 834 835 @property 836 def msg(self): 837 return self._msg 838 839 @property 840 def count(self): 841 return self._count 842 843 @property 844 def was_successful(self): 845 """Indicates if the transmission of IPv6 messages finished successfully""" 846 return self._tx_counter == self._count 847 848 # asyncore.dispatcher callbacks 849 850 def readable(self): 851 return False 852 853 def writable(self): 854 return True 855 856 def handle_write(self): 857 sent_len = self.sendto(self._tx_buffer, self._dst_sock_addr) 858 859 if self._node._verbose: 860 if sent_len < 30: 861 info_text = '{} bytes ("{}")'.format(sent_len, self._tx_buffer[:sent_len]) 862 else: 863 info_text = '{} bytes'.format(sent_len) 864 _log('- Node{} sent {} to [{}]:{} from [{}]:{}'.format(self._node._index, info_text, self._dst_addr, 865 self._dst_port, self._src_addr, self._src_port)) 866 867 self._tx_buffer = self._tx_buffer[sent_len:] 868 869 if len(self._tx_buffer) == 0: 870 self._tx_counter += 1 871 if self._tx_counter < self._count: 872 self._tx_buffer = self._msg 873 else: 874 self.handle_close() 875 876 def handle_close(self): 877 self.close() 878 879 880# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 881 882 883class AsyncReceiver(asyncore.dispatcher): 884 """ An IPv6 async message receiver - use `prepare_rx()` to create one""" 885 886 _MAX_RECV_SIZE = 2048 887 888 class _SenderInfo(object): 889 890 def __init__(self, sender_addr, sender_port, msg, count): 891 self._sender_addr = sender_addr 892 self._sender_port = sender_port 893 self._msg = msg 894 self._count = count 895 self._rx_counter = 0 896 897 def _check_received(self, msg, sender_addr, sender_port): 898 if self._msg == msg and self._sender_addr == sender_addr and self._sender_port == sender_port: 899 self._rx_counter += 1 900 return self._did_recv_all() 901 902 def _did_recv_all(self): 903 return self._rx_counter >= self._count 904 905 def __init__(self, node, local_port): 906 self._node = node 907 self._local_port = local_port 908 self._senders = [] # list of `_SenderInfo` objects 909 # contains all received messages as a list of (pkt, (src_addr, 910 # src_port)) 911 self._all_rx = [] 912 self._timeout = 0 # listen timeout (zero means forever) 913 self._started = False 914 self._start_time = 0 915 916 # Create a socket, bind it to the node's interface 917 sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 918 sock.setsockopt(socket.SOL_SOCKET, _SO_BINDTODEVICE, node.interface_name + '\0') 919 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 920 921 # Bind the socket to any IPv6 address with the given local port 922 local_sock_addr = _create_socket_address('::', local_port) 923 sock.bind(local_sock_addr) 924 925 asyncore.dispatcher.__init__(self, sock) 926 927 def _add_sender(self, sender_addr, sender_port, msg, count): 928 self._senders.append(AsyncReceiver._SenderInfo(sender_addr, sender_port, msg, count)) 929 930 def _set_listen_timeout(self, timeout): 931 self._timeout = timeout 932 933 # Property getters 934 935 @property 936 def node(self): 937 return self._node 938 939 @property 940 def local_port(self): 941 return self._local_port 942 943 @property 944 def all_rx_msg(self): 945 """returns all received messages as a list of (msg, (src_addr, src_port))""" 946 return self._all_rx 947 948 @property 949 def was_successful(self): 950 """Indicates if all expected IPv6 messages were received successfully""" 951 return len(self._senders) == 0 or all([sender._did_recv_all() for sender in self._senders]) 952 953 # asyncore.dispatcher callbacks 954 955 def readable(self): 956 if not self._started: 957 self._start_time = time.time() 958 self._started = True 959 if self._timeout != 0 and time.time() - self._start_time >= self._timeout: 960 self.handle_close() 961 if self._node._verbose: 962 _log('- Node{} finished listening on port {} for {} sec, received {} msg(s)'.format( 963 self._node._index, self._local_port, self._timeout, len(self._all_rx))) 964 return False 965 return True 966 967 def writable(self): 968 return False 969 970 def handle_read(self): 971 (msg, src_sock_addr) = self.recvfrom(AsyncReceiver._MAX_RECV_SIZE) 972 src_addr = src_sock_addr[0] 973 src_port = src_sock_addr[1] 974 975 if (_is_ipv6_addr_link_local(src_addr)): 976 if '%' in src_addr: 977 # remove the interface name from address 978 src_addr = src_addr.split('%')[0] 979 980 if self._node._verbose: 981 if len(msg) < 30: 982 info_text = '{} bytes ("{}")'.format(len(msg), msg) 983 else: 984 info_text = '{} bytes'.format(len(msg)) 985 _log('- Node{} received {} on port {} from [{}]:{}'.format(self._node._index, info_text, self._local_port, 986 src_addr, src_port)) 987 988 self._all_rx.append((msg, (src_addr, src_port))) 989 990 if all([sender._check_received(msg, src_addr, src_port) for sender in self._senders]): 991 self.handle_close() 992 993 def handle_close(self): 994 self.close() 995 # remove the receiver from the node once the socket is closed 996 self._node._remove_recver(self) 997 998 999# ----------------------------------------------------------------------------------------------------------------------- 1000 1001 1002class VerifyError(Exception): 1003 pass 1004 1005 1006_is_in_verify_within = False 1007 1008 1009def verify(condition): 1010 """Verifies that a `condition` is true, otherwise raises a VerifyError""" 1011 global _is_in_verify_within 1012 if not condition: 1013 calling_frame = inspect.currentframe().f_back 1014 error_message = 'verify() failed at line {} in "{}"'.format(calling_frame.f_lineno, 1015 calling_frame.f_code.co_filename) 1016 if not _is_in_verify_within: 1017 print(error_message) 1018 raise VerifyError(error_message) 1019 1020 1021def verify_within(condition_checker_func, wait_time, delay_time=0.1): 1022 """Verifies that a given function `condition_checker_func` passes successfully within a given wait timeout. 1023 `wait_time` is maximum time waiting for condition_checker to pass (in seconds). 1024 `delay_time` specifies a delay interval added between failed attempts (in seconds). 1025 """ 1026 global _is_in_verify_within 1027 start_time = time.time() 1028 old_is_in_verify_within = _is_in_verify_within 1029 _is_in_verify_within = True 1030 while True: 1031 try: 1032 condition_checker_func() 1033 except VerifyError as e: 1034 if time.time() - start_time > wait_time: 1035 print('Took too long to pass the condition ({}>{} sec)'.format(time.time() - start_time, wait_time)) 1036 print(e.message) 1037 raise e 1038 except BaseException: 1039 raise 1040 else: 1041 break 1042 if delay_time != 0: 1043 time.sleep(delay_time) 1044 _is_in_verify_within = old_is_in_verify_within 1045 1046 1047# ----------------------------------------------------------------------------------------------------------------------- 1048# Parsing `wpanctl` output 1049 1050# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1051 1052 1053class ScanResult(object): 1054 """ This object encapsulates a scan result (active/discover/energy scan)""" 1055 1056 TYPE_ACTIVE_SCAN = 'active-scan' 1057 TYPE_DISCOVERY_SCAN = 'discover-scan' 1058 TYPE_ENERGY_SCAN = 'energy-scan' 1059 1060 def __init__(self, result_text): 1061 1062 items = [item.strip() for item in result_text.split('|')] 1063 1064 if len(items) == 8: 1065 self._type = ScanResult.TYPE_ACTIVE_SCAN 1066 self._index = items[0] 1067 self._joinable = (items[1] == 'YES') 1068 self._network_name = items[2][1:-1] 1069 self._panid = items[3] 1070 self._channel = items[4] 1071 self._xpanid = items[5] 1072 self._ext_address = items[6] 1073 self._rssi = items[7] 1074 elif len(items) == 7: 1075 self._type = ScanResult.TYPE_DISCOVERY_SCAN 1076 self._index = items[0] 1077 self._network_name = items[1][1:-1] 1078 self._panid = items[2] 1079 self._channel = items[3] 1080 self._xpanid = items[4] 1081 self._ext_address = items[5] 1082 self._rssi = items[6] 1083 elif len(items) == 2: 1084 self._type = ScanResult.TYPE_ENERGY_SCAN 1085 self._channel = items[0] 1086 self._rssi = items[1] 1087 else: 1088 raise ValueError('"{}" does not seem to be a valid scan result string'.result_text) 1089 1090 @property 1091 def type(self): 1092 return self._type 1093 1094 @property 1095 def joinable(self): 1096 return self._joinable 1097 1098 @property 1099 def network_name(self): 1100 return self._network_name 1101 1102 @property 1103 def panid(self): 1104 return self._panid 1105 1106 @property 1107 def channel(self): 1108 return self._channel 1109 1110 @property 1111 def xpanid(self): 1112 return self._xpanid 1113 1114 @property 1115 def ext_address(self): 1116 return self._ext_address 1117 1118 @property 1119 def rssi(self): 1120 return self._rssi 1121 1122 def __repr__(self): 1123 return 'ScanResult({})'.format(self.__dict__) 1124 1125 1126def parse_scan_result(scan_result): 1127 """ Parses scan result string and returns an array of `ScanResult` objects""" 1128 return [ScanResult(item) for item in scan_result.split('\n')[2:]] # skip first two lines which are table headers 1129 1130 1131def parse_list(list_string): 1132 """ 1133 Parses IPv6/prefix/route list string (output of wpanctl get for properties WPAN_IP6_ALL_ADDRESSES, 1134 IP6_MULTICAST_ADDRESSES, WPAN_THREAD_ON_MESH_PREFIXES, ...) 1135 Returns an array of strings each containing an IPv6/prefix/route entry. 1136 """ 1137 # List string example (get(WPAN_IP6_ALL_ADDRESSES) output): 1138 # 1139 # '[\n 1140 # \t"fdf4:5632:4940:0:8798:8701:85d4:e2be prefix_len:64 origin:ncp valid:forever preferred:forever"\n 1141 # \t"fe80::2092:9358:97ea:71c6 prefix_len:64 origin:ncp valid:forever preferred:forever"\n 1142 # ]' 1143 # 1144 # We split the lines ('\n' as separator) and skip the first and last lines which are '[' and ']'. 1145 # For each line, skip the first two characters (which are '\t"') and last character ('"'), then split the string 1146 # using whitespace as separator. The first entry is the IPv6 address. 1147 # 1148 return [line[2:-1].split()[0] for line in list_string.split('\n')[1:-1]] 1149 1150 1151# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1152 1153 1154class OnMeshPrefix(object): 1155 """ This object encapsulates an on-mesh prefix""" 1156 1157 def __init__(self, text): 1158 1159 # Example of expected text: 1160 # 1161 # '\t"fd00:abba:cafe:: prefix_len:64 origin:user stable:yes flags:0x31' 1162 # ' [on-mesh:1 def-route:0 config:0 dhcp:0 slaac:1 pref:1 prio:med] rloc:0x0000"' 1163 1164 m = re.match( 1165 r'\t"([0-9a-fA-F:]+)\s*prefix_len:(\d+)\s+origin:(\w*)\s+stable:(\w*).* \[' + 1166 r'on-mesh:(\d)\s+def-route:(\d)\s+config:(\d)\s+dhcp:(\d)\s+slaac:(\d)\s+pref:(\d)\s+.*prio:(\w*)\]' + 1167 r'\s+rloc:(0x[0-9a-fA-F]+)', text) 1168 verify(m is not None) 1169 data = m.groups() 1170 1171 self._prefix = data[0] 1172 self._prefix_len = data[1] 1173 self._origin = data[2] 1174 self._stable = (data[3] == 'yes') 1175 self._on_mesh = (data[4] == '1') 1176 self._def_route = (data[5] == '1') 1177 self._config = (data[6] == '1') 1178 self._dhcp = (data[7] == '1') 1179 self._slaac = (data[8] == '1') 1180 self._preferred = (data[9] == '1') 1181 self._priority = (data[10]) 1182 self._rloc16 = (data[11]) 1183 1184 @property 1185 def prefix(self): 1186 return self._prefix 1187 1188 @property 1189 def prefix_len(self): 1190 return self._prefix_len 1191 1192 @property 1193 def origin(self): 1194 return self._origin 1195 1196 @property 1197 def priority(self): 1198 return self._priority 1199 1200 def is_stable(self): 1201 return self._stable 1202 1203 def is_on_mesh(self): 1204 return self._on_mesh 1205 1206 def is_def_route(self): 1207 return self._def_route 1208 1209 def is_config(self): 1210 return self._config 1211 1212 def is_dhcp(self): 1213 return self._dhcp 1214 1215 def is_slaac(self): 1216 return self._slaac 1217 1218 def is_preferred(self): 1219 return self._preferred 1220 1221 def rloc16(self): 1222 return self._rloc16 1223 1224 def __repr__(self): 1225 return 'OnMeshPrefix({})'.format(self.__dict__) 1226 1227 1228def parse_on_mesh_prefix_result(on_mesh_prefix_list): 1229 """ Parses on-mesh prefix list string and returns an array of `OnMeshPrefix` objects""" 1230 return [OnMeshPrefix(item) for item in on_mesh_prefix_list.split('\n')[1:-1]] 1231 1232 1233# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1234 1235 1236class ChildEntry(object): 1237 """ This object encapsulates a child entry""" 1238 1239 def __init__(self, text): 1240 1241 # Example of expected text: 1242 # 1243 # `\t"E24C5F67F4B8CBB9, RLOC16:d402, NetDataVer:175, LQIn:3, AveRssi:-20, LastRssi:-20, Timeout:120, Age:0, ` 1244 # `RxOnIdle:no, FTD:no, SecDataReq:yes, FullNetData:yes"` 1245 # 1246 1247 # We get rid of the first two chars `\t"' and last char '"', split the rest using whitespace as separator. 1248 # Then remove any ',' at end of items in the list. 1249 items = [item[:-1] if item[-1] == ',' else item for item in text[2:-1].split()] 1250 1251 # First item in the extended address 1252 self._ext_address = items[0] 1253 1254 # Convert the rest into a dictionary by splitting using ':' as 1255 # separator 1256 dict = {item.split(':')[0]: item.split(':')[1] for item in items[1:]} 1257 1258 self._rloc16 = dict['RLOC16'] 1259 self._timeout = dict['Timeout'] 1260 self._rx_on_idle = (dict['RxOnIdle'] == 'yes') 1261 self._ftd = (dict['FTD'] == 'yes') 1262 self._sec_data_req = (dict['SecDataReq'] == 'yes') 1263 self._full_net_data = (dict['FullNetData'] == 'yes') 1264 1265 @property 1266 def ext_address(self): 1267 return self._ext_address 1268 1269 @property 1270 def rloc16(self): 1271 return self._rloc16 1272 1273 @property 1274 def timeout(self): 1275 return self._timeout 1276 1277 def is_rx_on_when_idle(self): 1278 return self._rx_on_idle 1279 1280 def is_ftd(self): 1281 return self._ftd 1282 1283 def is_sec_data_req(self): 1284 return self._sec_data_req 1285 1286 def is_full_net_data(self): 1287 return self._full_net_data 1288 1289 def __repr__(self): 1290 return 'ChildEntry({})'.format(self.__dict__) 1291 1292 1293def parse_child_table_result(child_table_list): 1294 """ Parses child table list string and returns an array of `ChildEntry` objects""" 1295 return [ChildEntry(item) for item in child_table_list.split('\n')[1:-1]] 1296 1297 1298# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1299 1300 1301class NeighborEntry(object): 1302 """ This object encapsulates a neighbor entry""" 1303 1304 def __init__(self, text): 1305 1306 # Example of expected text: 1307 # 1308 # `\t"5AC95ED4646D6565, RLOC16:9403, LQIn:3, AveRssi:-20, LastRssi:-20, Age:0, LinkFC:8, MleFC:0, IsChild:yes,' 1309 # 'RxOnIdle:no, FTD:no, SecDataReq:yes, FullNetData:yes"' 1310 # 1311 1312 # We get rid of the first two chars `\t"' and last char '"', split the rest using whitespace as separator. 1313 # Then remove any ',' at end of items in the list. 1314 items = [item[:-1] if item[-1] == ',' else item for item in text[2:-1].split()] 1315 1316 # First item in the extended address 1317 self._ext_address = items[0] 1318 1319 # Convert the rest into a dictionary by splitting the text using ':' as 1320 # separator 1321 dict = {item.split(':')[0]: item.split(':')[1] for item in items[1:]} 1322 1323 self._rloc16 = dict['RLOC16'] 1324 self._is_child = (dict['IsChild'] == 'yes') 1325 self._rx_on_idle = (dict['RxOnIdle'] == 'yes') 1326 self._ftd = (dict['FTD'] == 'yes') 1327 1328 @property 1329 def ext_address(self): 1330 return self._ext_address 1331 1332 @property 1333 def rloc16(self): 1334 return self._rloc16 1335 1336 def is_rx_on_when_idle(self): 1337 return self._rx_on_idle 1338 1339 def is_ftd(self): 1340 return self._ftd 1341 1342 def is_child(self): 1343 return self._is_child 1344 1345 def __repr__(self): 1346 return 'NeighborEntry({})'.format(self.__dict__) 1347 1348 1349def parse_neighbor_table_result(neighbor_table_list): 1350 """ Parses neighbor table list string and returns an array of `NeighborEntry` objects""" 1351 return [NeighborEntry(item) for item in neighbor_table_list.split('\n')[1:-1]] 1352 1353 1354# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1355 1356 1357class RouterTableEntry(object): 1358 """ This object encapsulates a router table entry""" 1359 1360 def __init__(self, text): 1361 1362 # Example of expected text: 1363 # 1364 # `\t"8A970B3251810826, RLOC16:4000, RouterId:16, NextHop:43, PathCost:1, LQIn:3, LQOut:3, Age:3, LinkEst:yes"` 1365 # 1366 1367 # We get rid of the first two chars `\t"' and last char '"', split the rest using whitespace as separator. 1368 # Then remove any ',' at end of items in the list. 1369 items = [item[:-1] if item[-1] == ',' else item for item in text[2:-1].split()] 1370 1371 # First item in the extended address 1372 self._ext_address = items[0] 1373 1374 # Convert the rest into a dictionary by splitting the text using ':' as 1375 # separator 1376 dict = {item.split(':')[0]: item.split(':')[1] for item in items[1:]} 1377 1378 self._rloc16 = int(dict['RLOC16'], 16) 1379 self._router_id = int(dict['RouterId'], 0) 1380 self._next_hop = int(dict['NextHop'], 0) 1381 self._path_cost = int(dict['PathCost'], 0) 1382 self._age = int(dict['Age'], 0) 1383 self._le = (dict['LinkEst'] == 'yes') 1384 1385 @property 1386 def ext_address(self): 1387 return self._ext_address 1388 1389 @property 1390 def rloc16(self): 1391 return self._rloc16 1392 1393 @property 1394 def router_id(self): 1395 return self._router_id 1396 1397 @property 1398 def next_hop(self): 1399 return self._next_hop 1400 1401 @property 1402 def path_cost(self): 1403 return self._path_cost 1404 1405 def is_link_established(self): 1406 return self._le 1407 1408 def __repr__(self): 1409 return 'RouterTableEntry({})'.format(self.__dict__) 1410 1411 1412def parse_router_table_result(router_table_list): 1413 """ Parses router table list string and returns an array of `RouterTableEntry` objects""" 1414 return [RouterTableEntry(item) for item in router_table_list.split('\n')[1:-1]] 1415 1416 1417# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1418 1419 1420class AddressCacheEntry(object): 1421 """ This object encapsulates an address cache entry""" 1422 1423 def __init__(self, text): 1424 1425 # Example of expected text: 1426 # 1427 # '\t"fd00:1234::100:8 -> 0xfffe, Age:1, State:query, CanEvict:no, Timeout:3, RetryDelay:15"` 1428 # '\t"fd00:1234::3:2 -> 0x2000, Age:0, State:cached, LastTrans:0, ML-EID:fd40:ea58:a88c:0:b7ab:4919:aa7b:11a3"` 1429 1430 # We get rid of the first two chars `\t"' and last char '"', split the rest using whitespace as separator. 1431 # Then remove any ',' at end of items in the list. 1432 items = [item[:-1] if item[-1] == ',' else item for item in text[2:-1].split()] 1433 1434 # First item in the extended address 1435 self._address = items[0] 1436 self._rloc16 = int(items[2], 16) 1437 1438 # Convert the rest into a dictionary by splitting the text using ':' as 1439 # separator 1440 dict = {item.split(':')[0]: item.split(':')[1] for item in items[3:]} 1441 1442 self._age = int(dict['Age'], 0) 1443 1444 self._state = dict['State'] 1445 1446 if self._state == ADDRESS_CACHE_ENTRY_STATE_CACHED: 1447 self._last_trans = int(dict.get("LastTrans", "-1"), 0) 1448 else: 1449 self._can_evict = (dict['CanEvict'] == 'yes') 1450 self._timeout = int(dict['Timeout']) 1451 self._retry_delay = int(dict['RetryDelay']) 1452 1453 @property 1454 def address(self): 1455 return self._address 1456 1457 @property 1458 def rloc16(self): 1459 return self._rloc16 1460 1461 @property 1462 def age(self): 1463 return self._age 1464 1465 @property 1466 def state(self): 1467 return self._state 1468 1469 def can_evict(self): 1470 return self._can_evict 1471 1472 @property 1473 def timeout(self): 1474 return self._timeout 1475 1476 @property 1477 def retry_delay(self): 1478 return self._retry_delay 1479 1480 @property 1481 def last_trans(self): 1482 return self._last_trans 1483 1484 def __repr__(self): 1485 return 'AddressCacheEntry({})'.format(self.__dict__) 1486 1487 1488def parse_address_cache_table_result(addr_cache_table_list): 1489 """ Parses address cache table list string and returns an array of `AddressCacheEntry` objects""" 1490 return [AddressCacheEntry(item) for item in addr_cache_table_list.split('\n')[1:-1]] 1491 1492 1493# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1494 1495 1496class InterfaceRoute(object): 1497 """ This object encapsulates an interface route entry""" 1498 1499 def __init__(self, text): 1500 1501 # Example of expected text: 1502 # 1503 # '\t"fd00:abba::/64 metric:256 "' 1504 # 1505 1506 # We get rid of the first two chars `\t"' and last char '"', split the rest using whitespace as separator. 1507 # Then remove any ',' at end of items in the list. 1508 items = [item[:-1] if item[-1] == ',' else item for item in text[2:-1].split()] 1509 1510 # First item in the extended address 1511 self._route_prefix = items[0].split('/')[0] 1512 self._prefix_len = int(items[0].split('/')[1], 0) 1513 self._metric = int(items[1].split(':')[1], 0) 1514 1515 @property 1516 def route_prefix(self): 1517 return self._route_prefix 1518 1519 @property 1520 def prefix_len(self): 1521 return self._prefix_len 1522 1523 @property 1524 def metric(self): 1525 return self._metric 1526 1527 def __repr__(self): 1528 return 'InterfaceRoute({})'.format(self.__dict__) 1529 1530 1531def parse_interface_routes_result(interface_routes_list): 1532 """ Parses interface routes list string and returns an array of `InterfaceRoute` objects""" 1533 return [InterfaceRoute(item) for item in interface_routes_list.split('\n')[1:-1]] 1534 1535 1536# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1537 1538 1539class MultiRadioEntry(object): 1540 """ This object encapsulates a multi radio info entry""" 1541 1542 def __init__(self, text): 1543 1544 # Example of expected text: 1545 # 1546 # `\t"0EB758375B4976E7, RLOC16:f403, Radios:[IEEE_802_15_4(200), TREL_UDP6(255)]"` 1547 # 1548 1549 # We get rid of the first two chars `\t"' and last char '"', split the rest using whitespace as separator. 1550 # Then remove any ',' at end of items in the list. 1551 items = [item[:-1] if item[-1] == ',' else item for item in text[2:-1].split()] 1552 1553 # First item is the extended address 1554 self._ext_address = items[0] 1555 1556 # Second items is 'RLCO16:{rloc}' 1557 self._rloc16 = items[1].split(':')[1] 1558 1559 # Join back rest of items, split using ":" to get list of radios of form "[IEEE_802_15_4(200) TREL_UDP6(255)]" 1560 radios = " ".join(items[2:]).split(":")[1] 1561 1562 if radios != "[]": 1563 # Get rid of `[ and `]`, then split using " ", then convert to dictionary mapping radio type 1564 # to its preference value. 1565 self._radios = {radio.split("(")[0]: radio.split("(")[1][:-1] for radio in radios[1:-1].split(' ')} 1566 else: 1567 self._radios = {} 1568 1569 @property 1570 def ext_address(self): 1571 return self._ext_address 1572 1573 @property 1574 def rloc16(self): 1575 return self._rloc16 1576 1577 @property 1578 def radios(self): 1579 return self._radios 1580 1581 def supports(self, radio_type): 1582 return radio_type in self._radios 1583 1584 def preference(self, radio_type): 1585 return int(self._radios[radio_type], 0) if self.supports(radio_type) else None 1586 1587 def __repr__(self): 1588 return 'MultiRadioEntry({})'.format(self.__dict__) 1589 1590 1591def parse_multi_radio_result(multi_radio_list): 1592 """ Parses multi radio neighbor list string and returns an array of `MultiRadioEntry` objects""" 1593 return [MultiRadioEntry(item) for item in multi_radio_list.split('\n')[1:-1]] 1594