1#!/usr/bin/env python 2# 3# Copyright (c) 2022, 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""" 30>> Thread Host Controller Interface 31>> Device : OpenThread_Sim THCI 32>> Class : OpenThread_Sim 33""" 34 35import ipaddress 36import paramiko 37import socket 38import time 39import win32api 40 41from simulation.config import load_config 42from THCI.IThci import IThci 43from THCI.OpenThread import OpenThreadTHCI, watched 44 45config = load_config() 46ot_subpath = {item['tag']: item['subpath'] for item in config['ot_build']['ot']} 47 48 49class SSHHandle(object): 50 KEEPALIVE_INTERVAL = 30 51 52 def __init__(self, ip, port, username, password, device, node_id): 53 self.ip = ip 54 self.port = int(port) 55 self.username = username 56 self.password = password 57 self.node_id = node_id 58 self.__handle = None 59 self.__stdin = None 60 self.__stdout = None 61 62 self.__connect(device) 63 64 # Close the SSH connection only when Harness exits 65 win32api.SetConsoleCtrlHandler(self.__disconnect, True) 66 67 @watched 68 def __connect(self, device): 69 if self.__handle is not None: 70 return 71 72 self.__handle = paramiko.SSHClient() 73 self.__handle.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 74 try: 75 self.log('Connecting to %s:%s with username=%s', self.ip, self.port, self.username) 76 self.__handle.connect(self.ip, port=self.port, username=self.username, password=self.password) 77 except paramiko.AuthenticationException: 78 if not self.password: 79 self.__handle.get_transport().auth_none(self.username) 80 else: 81 raise Exception('Password error') 82 83 # Avoid SSH connection lost after inactivity for a while 84 self.__handle.get_transport().set_keepalive(self.KEEPALIVE_INTERVAL) 85 86 self.__stdin, self.__stdout, _ = self.__handle.exec_command(device + ' ' + str(self.node_id)) 87 88 # Receive the output in non-blocking mode 89 self.__stdout.channel.setblocking(0) 90 91 # Some commands such as `udp send <ip> -x <hex>` send binary data 92 # The UDP packet receiver will output the data in binary to stdout 93 self.__stdout._set_mode('rb') 94 95 def __disconnect(self, dwCtrlType): 96 if self.__handle is None: 97 return 98 99 # Exit ot-cli-ftd and close the SSH connection 100 self.send('exit\n') 101 self.__stdin.close() 102 self.__stdout.close() 103 self.__handle.close() 104 105 def close(self): 106 # Do nothing, because disconnecting and then connecting will automatically factory reset all states 107 # compared to real devices, which is not the intended behavior 108 pass 109 110 def send(self, cmd): 111 self.__stdin.write(cmd) 112 self.__stdin.flush() 113 114 def recv(self): 115 outputs = [] 116 while True: 117 try: 118 outputs.append(self.__stdout.read(1)) 119 except socket.timeout: 120 break 121 return ''.join(outputs) 122 123 def log(self, fmt, *args): 124 try: 125 msg = fmt % args 126 print('%d@%s - %s - %s' % (self.node_id, self.ip, time.strftime('%b %d %H:%M:%S'), msg)) 127 except Exception: 128 pass 129 130 131class OpenThread_Sim(OpenThreadTHCI, IThci): 132 __handle = None 133 134 @watched 135 def _connect(self): 136 self.__lines = [] 137 138 # Only actually connect once. 139 if self.__handle is None: 140 self.log('SSH connecting ...') 141 self.__handle = SSHHandle(self.ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword, 142 self.device, self.node_id) 143 144 self.log('connected to %s successfully', self.telnetIp) 145 146 @watched 147 def _disconnect(self): 148 pass 149 150 @watched 151 def _parseConnectionParams(self, params): 152 discovery_add = params.get('SerialPort') 153 if '@' not in discovery_add: 154 raise ValueError('%r in the field `add` is invalid' % discovery_add) 155 156 prefix, self.ssh_ip = discovery_add.split('@') 157 self.tag, self.node_id = prefix.split('_') 158 self.node_id = int(self.node_id) 159 # Let it crash if it is an invalid IP address 160 ipaddress.ip_address(self.ssh_ip) 161 162 # Do not use `os.path.join` as it uses backslash as the separator on Windows 163 global config 164 self.device = '/'.join([config['ot_path'], ot_subpath[self.tag], 'examples/apps/cli/ot-cli-ftd']) 165 166 self.connectType = 'ip' 167 self.telnetIp = self.port = discovery_add 168 169 ssh = config['ssh'] 170 self.telnetPort = ssh['port'] 171 self.telnetUsername = ssh['username'] 172 self.telnetPassword = ssh['password'] 173 174 def _cliReadLine(self): 175 if len(self.__lines) > 1: 176 return self.__lines.pop(0) 177 178 tail = '' 179 if len(self.__lines) != 0: 180 tail = self.__lines.pop() 181 182 tail += self.__handle.recv() 183 184 self.__lines += self._lineSepX.split(tail) 185 if len(self.__lines) > 1: 186 return self.__lines.pop(0) 187 188 return None 189 190 def _cliWriteLine(self, line): 191 self.__handle.send(line + '\n') 192 193 def _onCommissionStart(self): 194 pass 195 196 def _onCommissionStop(self): 197 pass 198