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