[6320] | 1 | # -*- coding: utf-8 -*- |
---|
| 2 | """ |
---|
| 3 | ------------------------------------------------------------------------------ |
---|
| 4 | Mango 802.11 Reference Design Experiments Framework - Transport Node |
---|
| 5 | ------------------------------------------------------------------------------ |
---|
| 6 | License: Copyright 2019 Mango Communications, Inc. All rights reserved. |
---|
| 7 | Use and distribution subject to terms in LICENSE.txt |
---|
| 8 | ------------------------------------------------------------------------------ |
---|
| 9 | |
---|
| 10 | This module provides class definition for Transport Node. |
---|
| 11 | |
---|
| 12 | Functions (see below for more information): |
---|
| 13 | WlanExpTransportNode() -- Base class for Transport nodes |
---|
| 14 | WlanExpTransportNodeFactory() -- Base class for creating a WlanExpTransportNode |
---|
| 15 | |
---|
| 16 | Integer constants: |
---|
| 17 | NODE_TYPE, NODE_ID, NODE_HW_GEN, NODE_SERIAL_NUM, |
---|
| 18 | NODE_FPGA_DNA -- Node hardware parameter constants |
---|
| 19 | |
---|
| 20 | If additional hardware parameters are needed for sub-classes of WlanExpTransportNode, |
---|
| 21 | please make sure that the values of these hardware parameters are not reused. |
---|
| 22 | |
---|
| 23 | """ |
---|
| 24 | |
---|
| 25 | from . import cmds |
---|
| 26 | from . import exception as ex |
---|
| 27 | |
---|
| 28 | |
---|
| 29 | __all__ = ['WlanExpTransportNode', 'WlanExpTransportNodeFactory'] |
---|
| 30 | |
---|
| 31 | |
---|
| 32 | # Node Parameter Identifiers |
---|
| 33 | # - The C counterparts are found in *_node.h |
---|
| 34 | NODE_TYPE = 0 |
---|
| 35 | NODE_ID = 1 |
---|
| 36 | NODE_HW_GEN = 2 |
---|
| 37 | NODE_SERIAL_NUM = 3 |
---|
| 38 | NODE_FPGA_DNA = 4 |
---|
| 39 | |
---|
| 40 | |
---|
| 41 | |
---|
| 42 | class WlanExpTransportNode(object): |
---|
| 43 | """Base Class for Transport node. |
---|
| 44 | |
---|
| 45 | The Transport node represents one node in a network. This class is the |
---|
| 46 | primary interface for interacting with nodes by providing methods for |
---|
| 47 | sending commands and checking status of nodes. |
---|
| 48 | |
---|
| 49 | By default, the base Transport node provides many useful node attributes |
---|
| 50 | as well as a transport component. |
---|
| 51 | |
---|
| 52 | Attributes: |
---|
| 53 | node_type -- Unique type of the Transport node |
---|
| 54 | node_id -- Unique identification for this node |
---|
| 55 | name -- User specified name for this node (supplied by user scripts) |
---|
| 56 | description -- String description of this node (auto-generated) |
---|
| 57 | serial_number -- Node's serial number, read from EEPROM on hardware |
---|
| 58 | fpga_dna -- Node's FPGA'a unique identification (on select hardware) |
---|
| 59 | |
---|
| 60 | transport -- Node's transport object |
---|
| 61 | transport_broadcast -- Node's broadcast transport object |
---|
| 62 | """ |
---|
| 63 | network_config = None |
---|
| 64 | |
---|
| 65 | node_type = None |
---|
| 66 | node_id = None |
---|
| 67 | name = None |
---|
| 68 | description = None |
---|
| 69 | serial_number = None |
---|
| 70 | sn_str = None |
---|
| 71 | fpga_dna = None |
---|
| 72 | |
---|
| 73 | transport = None |
---|
| 74 | transport_broadcast = None |
---|
| 75 | transport_tracker = None |
---|
| 76 | |
---|
| 77 | def __init__(self, network_config=None): |
---|
| 78 | if network_config is not None: |
---|
| 79 | self.network_config = network_config |
---|
| 80 | else: |
---|
| 81 | from . import config |
---|
| 82 | |
---|
| 83 | self.network_config = config.NetworkConfiguration() |
---|
| 84 | |
---|
| 85 | self.transport_tracker = 0 |
---|
| 86 | |
---|
| 87 | |
---|
| 88 | def __del__(self): |
---|
| 89 | """Clear the transport object to close any open socket connections |
---|
| 90 | in the event the node is deleted""" |
---|
| 91 | if self.transport: |
---|
| 92 | self.transport.transport_close() |
---|
| 93 | self.transport = None |
---|
| 94 | |
---|
| 95 | if self.transport_broadcast: |
---|
| 96 | self.transport_broadcast.transport_close() |
---|
| 97 | self.transport_broadcast = None |
---|
| 98 | |
---|
| 99 | |
---|
| 100 | def set_init_configuration(self, serial_number, node_id, node_name, |
---|
| 101 | ip_address, unicast_port, broadcast_port): |
---|
| 102 | """Set the initial configuration of the node.""" |
---|
| 103 | from . import util |
---|
| 104 | import wlan_exp.platform as platform |
---|
| 105 | |
---|
| 106 | host_id = self.network_config.get_param('host_id') |
---|
| 107 | tx_buf_size = self.network_config.get_param('tx_buffer_size') |
---|
| 108 | rx_buf_size = self.network_config.get_param('rx_buffer_size') |
---|
| 109 | tport_type = self.network_config.get_param('transport_type') |
---|
| 110 | |
---|
| 111 | (sn, sn_str) = util.get_serial_number(serial_number) |
---|
| 112 | p = platform.lookup_platform_by_serial_num(serial_number) |
---|
| 113 | if p: |
---|
| 114 | self.platform_id = p.platform_id |
---|
| 115 | else: |
---|
| 116 | print('WARNING: no platform found for serial number {}'.format(serial_number)) |
---|
| 117 | self.platform_id = -1 |
---|
| 118 | |
---|
| 119 | if (tport_type == 'python'): |
---|
| 120 | from . import transport_eth_ip_udp_py as unicast_tp |
---|
| 121 | from . import transport_eth_ip_udp_py_broadcast as broadcast_tp |
---|
| 122 | |
---|
| 123 | if self.transport is None: |
---|
| 124 | self.transport = unicast_tp.TransportEthIpUdpPy() |
---|
| 125 | |
---|
| 126 | if self.transport_broadcast is None: |
---|
| 127 | self.transport_broadcast = broadcast_tp.TransportEthIpUdpPyBroadcast(self.network_config) |
---|
| 128 | else: |
---|
| 129 | print("Transport not defined\n") |
---|
| 130 | |
---|
| 131 | # Set Node information |
---|
| 132 | self.node_id = node_id |
---|
| 133 | self.name = node_name |
---|
| 134 | self.serial_number = sn |
---|
| 135 | self.sn_str = sn_str |
---|
| 136 | |
---|
| 137 | # Set Node Unicast Transport information |
---|
| 138 | self.transport.transport_open(tx_buf_size, rx_buf_size) |
---|
| 139 | self.transport.set_ip_address(ip_address) |
---|
| 140 | self.transport.set_unicast_port(unicast_port) |
---|
| 141 | self.transport.set_broadcast_port(broadcast_port) |
---|
| 142 | self.transport.set_src_id(host_id) |
---|
| 143 | self.transport.set_dest_id(node_id) |
---|
| 144 | |
---|
| 145 | # Set Node Broadcast Transport information |
---|
| 146 | self.transport_broadcast.transport_open(tx_buf_size, rx_buf_size) |
---|
| 147 | self.transport_broadcast.set_ip_address(ip_address) |
---|
| 148 | self.transport_broadcast.set_unicast_port(unicast_port) |
---|
| 149 | self.transport_broadcast.set_broadcast_port(broadcast_port) |
---|
| 150 | self.transport_broadcast.set_src_id(host_id) |
---|
| 151 | self.transport_broadcast.set_dest_id(0xFFFF) |
---|
| 152 | |
---|
| 153 | |
---|
| 154 | def configure_node(self, jumbo_frame_support=False): |
---|
| 155 | """Get remaining information from the node and set remaining parameters.""" |
---|
| 156 | |
---|
| 157 | self.transport.ping(self) |
---|
| 158 | |
---|
| 159 | # Retrieve the hardware node's NODE_INFO and PLATFORM_NODE_INFO structs |
---|
| 160 | # This method intentionally retrieves and applies the node_info structs |
---|
| 161 | # in two steps. In other implementations the node_info structs might |
---|
| 162 | # be known a priori, and could be applied to the node object without |
---|
| 163 | # any over-the-wire handshake |
---|
| 164 | hw_node_info = self.get_node_info() |
---|
| 165 | |
---|
| 166 | # hw_node_info is tuple of InfoStruct instances for the Node Info structs |
---|
| 167 | # returned by the node. The first struct is always a platform-agnostic |
---|
| 168 | # NODE_INFO as defined in info.py. Other structs in tuple are produced |
---|
| 169 | # and consumed by platform code |
---|
| 170 | |
---|
| 171 | self.update_node_info(hw_node_info) |
---|
| 172 | |
---|
| 173 | # Set description |
---|
| 174 | self.description = self.__repr__() |
---|
| 175 | |
---|
| 176 | def update_node_info(self, node_info_struct): |
---|
| 177 | raise NotImplementedError('ERROR: superclass does not handle update_node_info!') |
---|
| 178 | |
---|
| 179 | def get_type_ids(self): |
---|
| 180 | """Get the type of the node. The node type identifies the node's |
---|
| 181 | platform ID and the software application IDs in CPU High and Low. |
---|
| 182 | This method returns the minimum info requires for the init flow |
---|
| 183 | to select the correct class of wlan_exp node object""" |
---|
| 184 | |
---|
| 185 | node_type_ids = self.send_cmd(cmds.NodeGetType()) |
---|
| 186 | |
---|
| 187 | return node_type_ids |
---|
| 188 | |
---|
| 189 | |
---|
| 190 | def get_node_info(self): |
---|
| 191 | """Get the Hardware Information from the node.""" |
---|
| 192 | return self.send_cmd(cmds.GetNodeInfo()) |
---|
| 193 | |
---|
| 194 | def test_mtu(self, mtu): |
---|
| 195 | """Tests that the Node->Host link supports a given MTU. The current version |
---|
| 196 | of this method does not test the the Host->Node link due to a limitation in |
---|
| 197 | the Eth Rx handling via the wlan_mac_queue subsystem. Queue buffers are |
---|
| 198 | limited to 4kB, sufficient for all current uses including all wlan_exp |
---|
| 199 | host->node messages, but insufficent to test a full 9kB jumbo MTU |
---|
| 200 | """ |
---|
| 201 | from wlan_exp.transport.exception import TransportError |
---|
| 202 | |
---|
| 203 | try: |
---|
| 204 | return self.send_cmd(cmds.TestTransportMTU(mtu)) |
---|
| 205 | |
---|
| 206 | except TransportError: |
---|
| 207 | # TransportError catches timeout, either when node does not respond |
---|
| 208 | # or node does responsd but host drops the response for being too |
---|
| 209 | # big. Usually a TransportError should halt the script. In this |
---|
| 210 | # special case we catch the timeout to return False so the init |
---|
| 211 | # code can continue its node init using the last known-good MTU |
---|
| 212 | return False |
---|
| 213 | |
---|
| 214 | def set_max_resp_words(self, max_words): |
---|
| 215 | """Sets the maximum number of payload words the node may include |
---|
| 216 | in any response packet. The value must be derived from the MTU of the |
---|
| 217 | host, node, and network""" |
---|
| 218 | |
---|
| 219 | return self.send_cmd(cmds.TransportSetMaxRespWords(max_words)) |
---|
| 220 | |
---|
| 221 | def set_name(self, name): |
---|
| 222 | """Set the name of the node. |
---|
| 223 | |
---|
| 224 | The name provided will affect the Python environment only |
---|
| 225 | (ie it will update strings in child classes but will not be |
---|
| 226 | transmitted to the node.) |
---|
| 227 | |
---|
| 228 | Args: |
---|
| 229 | name (str): User provided name of the node. |
---|
| 230 | """ |
---|
| 231 | self.name = name |
---|
| 232 | self.description = self.__repr__() |
---|
| 233 | |
---|
| 234 | |
---|
| 235 | |
---|
| 236 | # ------------------------------------------------------------------------- |
---|
| 237 | # Commands for the Node |
---|
| 238 | # ------------------------------------------------------------------------- |
---|
| 239 | def identify(self): |
---|
| 240 | """Identify the node |
---|
| 241 | |
---|
| 242 | The node will physically identify itself by: |
---|
| 243 | |
---|
| 244 | * Blinking the Hex Display (for approx 10 seconds) |
---|
| 245 | * Output Node ID and IP adress to UART output |
---|
| 246 | """ |
---|
| 247 | self.send_cmd(cmds.NodeIdentify(self.sn_str)) |
---|
| 248 | |
---|
| 249 | def ping(self): |
---|
| 250 | """'Ping' the node |
---|
| 251 | |
---|
| 252 | Send an empty packet to the node via the transport to test connectivity |
---|
| 253 | between the host and the node. This is the simplest command that can |
---|
| 254 | be processed by the node and is similar to the "ping" command used |
---|
| 255 | check network connectivity. |
---|
| 256 | """ |
---|
| 257 | self.transport.ping(self, output=True) |
---|
| 258 | |
---|
| 259 | def get_temp(self): |
---|
| 260 | """Get the temperature of the node.""" |
---|
| 261 | (curr_temp, _, _) = self.send_cmd(cmds.NodeGetTemperature()) # Min / Max temp not used |
---|
| 262 | return curr_temp |
---|
| 263 | |
---|
| 264 | def setup_network_inf(self): |
---|
| 265 | """Setup the transport network information for the node.""" |
---|
| 266 | self.send_cmd_broadcast(cmds.NodeSetupNetwork(self)) |
---|
| 267 | |
---|
| 268 | def reset_network_inf(self): |
---|
| 269 | """Reset the transport network information for the node.""" |
---|
| 270 | #self.send_cmd_broadcast(cmds.NodeResetNetwork(self.serial_number)) |
---|
| 271 | self.send_cmd_broadcast(cmds.NodeResetNetwork(self.sn_str)) |
---|
| 272 | |
---|
| 273 | # ------------------------------------------------------------------------- |
---|
| 274 | # Transmit / Receive methods for the Node |
---|
| 275 | # ------------------------------------------------------------------------- |
---|
| 276 | def send_cmd(self, cmd, max_attempts=2, max_req_size=None, timeout=None): |
---|
| 277 | """Send the provided command. |
---|
| 278 | |
---|
| 279 | Args: |
---|
| 280 | cmd -- Class of command to send |
---|
| 281 | max_attempts -- Maximum number of attempts to send a given command |
---|
| 282 | max_req_size -- Maximum request size (applys only to Buffer Commands) |
---|
| 283 | timeout -- Maximum time to wait for a response from the node |
---|
| 284 | """ |
---|
| 285 | from . import transport |
---|
| 286 | |
---|
| 287 | resp_type = cmd.get_resp_type() |
---|
| 288 | |
---|
| 289 | if (resp_type == transport.TRANSPORT_NO_RESP): |
---|
| 290 | payload = cmd.serialize() |
---|
| 291 | self.transport.send(payload, robust=False) |
---|
| 292 | |
---|
| 293 | elif (resp_type == transport.TRANSPORT_RESP): |
---|
| 294 | resp = self._receive_resp(cmd, max_attempts, timeout) |
---|
| 295 | return cmd.process_resp(resp) |
---|
| 296 | |
---|
| 297 | elif (resp_type == transport.TRANSPORT_BUFFER): |
---|
| 298 | resp = self._receive_buffer(cmd, max_attempts, max_req_size, timeout) |
---|
| 299 | return cmd.process_resp(resp) |
---|
| 300 | |
---|
| 301 | else: |
---|
| 302 | raise ex.TransportError(self.transport, |
---|
| 303 | "Unknown response type for command") |
---|
| 304 | |
---|
| 305 | |
---|
| 306 | def _receive_resp(self, cmd, max_attempts, timeout): |
---|
| 307 | """Internal method to receive a response for a given command payload""" |
---|
| 308 | from . import message |
---|
| 309 | |
---|
| 310 | reply = b'' |
---|
| 311 | done = False |
---|
| 312 | resp = message.Resp() |
---|
| 313 | |
---|
| 314 | payload = cmd.serialize() |
---|
| 315 | self.transport.send(payload) |
---|
| 316 | |
---|
| 317 | while not done: |
---|
| 318 | try: |
---|
| 319 | reply = self.transport.receive(timeout) |
---|
| 320 | self._receive_success() |
---|
| 321 | except ex.TransportError: |
---|
| 322 | self._receive_failure() |
---|
| 323 | |
---|
| 324 | if self._receive_failure_exceeded(max_attempts): |
---|
| 325 | raise ex.TransportError(self.transport, |
---|
| 326 | "Max retransmissions without reply from node") |
---|
| 327 | |
---|
| 328 | self.transport.send(payload) |
---|
| 329 | else: |
---|
| 330 | resp.deserialize(reply) |
---|
| 331 | done = True |
---|
| 332 | |
---|
| 333 | return resp |
---|
| 334 | |
---|
| 335 | |
---|
| 336 | def _receive_buffer(self, cmd, max_attempts, max_req_size, timeout): |
---|
| 337 | """Internal method to receive a buffer for a given command payload. |
---|
| 338 | |
---|
| 339 | Depending on the size of the buffer, the framework will split a |
---|
| 340 | single large request into multiple smaller requests based on the |
---|
| 341 | max_req_size. This is to: |
---|
| 342 | 1) Minimize the probability that the OS drops a packet |
---|
| 343 | 2) Minimize the time that the Ethernet interface on the node is busy |
---|
| 344 | and cannot service other requests |
---|
| 345 | |
---|
| 346 | To see performance data, set the 'display_perf' flag to True. |
---|
| 347 | """ |
---|
| 348 | from . import message |
---|
| 349 | |
---|
| 350 | display_perf = False |
---|
| 351 | print_warnings = True |
---|
| 352 | print_debug_msg = False |
---|
| 353 | |
---|
| 354 | reply = b'' |
---|
| 355 | |
---|
| 356 | start_byte = cmd.get_buffer_start_byte() |
---|
| 357 | |
---|
| 358 | |
---|
| 359 | #FIXME: It's possible I got lost in the labrinth, but I *think* |
---|
| 360 | # total_size here could wind up being cmds.CMD_PARAM_LOG_GET_ALL_ENTRIES, |
---|
| 361 | # which is 0xFFFFFFFF. What in the sam hill does the while loop below |
---|
| 362 | # do for such a chonkster of a total_size? |
---|
| 363 | #FIXME FIXME: Oh, obviously, this is even more subtle. LogGetEvents() |
---|
| 364 | # notices if you set the size to CMD_PARAM_LOG_GET_ALL_ENTRIES and silently |
---|
| 365 | # overwrites the value with CMD_BUFFER_GET_SIZE_FROM_DATA, which is some |
---|
| 366 | # transport parameter? Anyway, that guy happens to also be 0xFFFFFFFFF |
---|
| 367 | # so alls mediocre that ends mediocre. |
---|
| 368 | total_size = cmd.get_buffer_size() |
---|
| 369 | |
---|
| 370 | tmp_resp = None |
---|
| 371 | resp = None |
---|
| 372 | |
---|
| 373 | if max_req_size is not None: |
---|
| 374 | fragment_size = max_req_size |
---|
| 375 | else: |
---|
| 376 | fragment_size = total_size |
---|
| 377 | |
---|
| 378 | # To not hurt the performance of the transport, do not request more |
---|
| 379 | # data than can fit in the RX buffer |
---|
| 380 | if (fragment_size > self.transport.rx_buffer_size): |
---|
| 381 | fragment_size = self.transport.rx_buffer_size |
---|
| 382 | |
---|
| 383 | # Allocate a complete response buffer |
---|
| 384 | resp = message.Buffer(start_byte, total_size) |
---|
| 385 | resp.timestamp_in_hdr = cmd.timestamp_in_hdr |
---|
| 386 | |
---|
| 387 | if display_perf: |
---|
| 388 | import time |
---|
| 389 | print("Receive buffer") |
---|
| 390 | start_time = time.time() |
---|
| 391 | |
---|
| 392 | # If the transfer is more than the fragment size, then split the transaction |
---|
| 393 | if (total_size > fragment_size): |
---|
| 394 | size = fragment_size |
---|
| 395 | start_idx = start_byte |
---|
| 396 | num_bytes = 0 |
---|
| 397 | |
---|
| 398 | while (num_bytes < total_size): |
---|
| 399 | # Create fragmented command |
---|
| 400 | if (print_debug_msg): |
---|
| 401 | print("\nFRAGMENT: {0:10d}/{1:10d}\n".format(num_bytes, total_size)) |
---|
| 402 | |
---|
| 403 | # Handle the case of the last fragment |
---|
| 404 | if ((num_bytes + size) > total_size): |
---|
| 405 | size = total_size - num_bytes |
---|
| 406 | |
---|
| 407 | # Update the command with the location and size of fragment |
---|
| 408 | cmd.update_start_byte(start_idx) |
---|
| 409 | cmd.update_size(size) |
---|
| 410 | |
---|
| 411 | # Send the updated command |
---|
| 412 | # FIXME: So this is recursive, yes? send_cmd is already in our |
---|
| 413 | # callstack if we are here. |
---|
| 414 | tmp_resp = self.send_cmd(cmd) |
---|
| 415 | tmp_size = tmp_resp.get_buffer_size() |
---|
| 416 | |
---|
| 417 | if (tmp_size == size): |
---|
| 418 | # Add the response to the buffer and increment loop variables |
---|
| 419 | resp.merge(tmp_resp) |
---|
| 420 | num_bytes += size |
---|
| 421 | start_idx += size |
---|
| 422 | else: |
---|
| 423 | #FIXME, either I'm misunderstanding or we always will end up |
---|
| 424 | #in this else at the end of a retrieval when trying to retrieve |
---|
| 425 | # a log that has wrapped. |
---|
| 426 | #This is what my above FIXME is about -- total_size here appears |
---|
| 427 | #to be unaware of the magic 0xFFFFFFFF isn't really a size |
---|
| 428 | #it should be enforcing. |
---|
| 429 | |
---|
| 430 | # Exit the loop because communication has totally failed for |
---|
| 431 | # the fragment and there is no point to request the next |
---|
| 432 | # fragment. Only return the truncated buffer. |
---|
| 433 | if (print_warnings): |
---|
| 434 | msg = "WARNING: Command did not return a complete fragment.\n" |
---|
| 435 | msg += " Requested : {0:10d}\n".format(size) |
---|
| 436 | msg += " Received : {0:10d}\n".format(tmp_size) |
---|
| 437 | msg += "Returning truncated buffer." |
---|
| 438 | print(msg) |
---|
| 439 | |
---|
| 440 | break |
---|
| 441 | else: |
---|
| 442 | # Normal buffer receive flow |
---|
| 443 | payload = cmd.serialize() |
---|
| 444 | self.transport.send(payload) |
---|
| 445 | |
---|
| 446 | while not resp.is_buffer_complete(): |
---|
| 447 | try: |
---|
| 448 | reply = self.transport.receive(timeout) |
---|
| 449 | self._receive_success() |
---|
| 450 | except ex.TransportError: |
---|
| 451 | self._receive_failure() |
---|
| 452 | if print_warnings: |
---|
| 453 | print("WARNING: Transport timeout. Requesting missing data.") |
---|
| 454 | |
---|
| 455 | # If there is a timeout, then request missing part of the buffer |
---|
| 456 | if self._receive_failure_exceeded(max_attempts): |
---|
| 457 | if print_warnings: |
---|
| 458 | print("ERROR: Max re-transmissions without reply from node.") |
---|
| 459 | raise ex.TransportError(self.transport, |
---|
| 460 | "Max retransmissions without reply from node") |
---|
| 461 | |
---|
| 462 | # Get the missing locations |
---|
| 463 | locations = resp.get_missing_byte_locations() |
---|
| 464 | |
---|
| 465 | if print_debug_msg: |
---|
| 466 | print(resp) |
---|
| 467 | print(resp.tracker) |
---|
| 468 | print("Missing Locations in Buffer:") |
---|
| 469 | print(locations) |
---|
| 470 | |
---|
| 471 | # Send commands to fill in the buffer |
---|
| 472 | for location in locations: |
---|
| 473 | if (print_debug_msg): |
---|
| 474 | print("\nLOCATION: {0:10d} {1:10d}\n".format(location[0], location[2])) |
---|
| 475 | |
---|
| 476 | # Update the command with the new location |
---|
| 477 | cmd.update_start_byte(location[0]) |
---|
| 478 | cmd.update_size(location[2]) |
---|
| 479 | |
---|
| 480 | if (location[2] < 0): |
---|
| 481 | print("ERROR: Issue with finding missing bytes in response:") |
---|
| 482 | print("Response Tracker:") |
---|
| 483 | print(resp.tracker) |
---|
| 484 | print("\nMissing Locations:") |
---|
| 485 | print(locations) |
---|
| 486 | raise Exception() |
---|
| 487 | |
---|
| 488 | # Use the standard send to get a Buffer with missing data. |
---|
| 489 | # This avoids any race conditions when requesting |
---|
| 490 | # multiple missing locations. Make sure that max_attempts |
---|
| 491 | # are set to 1 for the re-request to not get in to an |
---|
| 492 | # infinite loop |
---|
| 493 | try: |
---|
| 494 | location_resp = self.send_cmd(cmd, max_attempts=max_attempts) |
---|
| 495 | self._receive_success() |
---|
| 496 | except ex.TransportError: |
---|
| 497 | # Timed out on a re-request. There is an error so |
---|
| 498 | # just clean up the response and get out of the loop. |
---|
| 499 | if print_warnings: |
---|
| 500 | print("WARNING: Transport timeout. Returning truncated buffer.") |
---|
| 501 | print(" Timeout requesting missing location: {1} bytes @ {0}".format(location[0], location[2])) |
---|
| 502 | |
---|
| 503 | self._receive_failure() |
---|
| 504 | resp.trim() |
---|
| 505 | return resp |
---|
| 506 | |
---|
| 507 | if print_debug_msg: |
---|
| 508 | print("Adding Response:") |
---|
| 509 | print(location_resp) |
---|
| 510 | print(resp) |
---|
| 511 | |
---|
| 512 | # Add the response to the buffer |
---|
| 513 | resp.merge(location_resp) |
---|
| 514 | |
---|
| 515 | if print_debug_msg: |
---|
| 516 | print("Buffer after merge:") |
---|
| 517 | print(resp) |
---|
| 518 | print(resp.tracker) |
---|
| 519 | |
---|
| 520 | else: |
---|
| 521 | resp.add_data_to_buffer(reply) |
---|
| 522 | |
---|
| 523 | # Trim the final buffer in case there were missing fragments |
---|
| 524 | resp.trim() |
---|
| 525 | |
---|
| 526 | if display_perf: |
---|
| 527 | print(" Receive time: {0}".format(time.time() - start_time)) |
---|
| 528 | |
---|
| 529 | return resp |
---|
| 530 | |
---|
| 531 | |
---|
| 532 | def send_cmd_broadcast(self, cmd): |
---|
| 533 | """Send the provided command over the broadcast transport. |
---|
| 534 | |
---|
| 535 | Currently, broadcast commands cannot have a response. |
---|
| 536 | |
---|
| 537 | Args: |
---|
| 538 | cmd -- Class of command to send |
---|
| 539 | """ |
---|
| 540 | self.transport_broadcast.send(payload=cmd.serialize()) |
---|
| 541 | |
---|
| 542 | |
---|
| 543 | def receive_resp(self, timeout=None): |
---|
| 544 | """Return a list of responses that are sitting in the host's |
---|
| 545 | receive queue. It will empty the queue and return them all the |
---|
| 546 | calling method.""" |
---|
| 547 | from . import message |
---|
| 548 | |
---|
| 549 | output = [] |
---|
| 550 | |
---|
| 551 | resp = self.transport.receive(timeout) |
---|
| 552 | |
---|
| 553 | if resp: |
---|
| 554 | # Create a list of response object if the list of bytes is a |
---|
| 555 | # concatenation of many responses |
---|
| 556 | done = False |
---|
| 557 | |
---|
| 558 | while not done: |
---|
| 559 | msg = message.Resp() |
---|
| 560 | msg.deserialize(resp) |
---|
| 561 | resp_len = msg.sizeof() |
---|
| 562 | |
---|
| 563 | if resp_len < len(resp): |
---|
| 564 | resp = resp[(resp_len):] |
---|
| 565 | else: |
---|
| 566 | done = True |
---|
| 567 | |
---|
| 568 | output.append(msg) |
---|
| 569 | |
---|
| 570 | return output |
---|
| 571 | |
---|
| 572 | |
---|
| 573 | |
---|
| 574 | # ------------------------------------------------------------------------- |
---|
| 575 | # Transport Tracker |
---|
| 576 | # ------------------------------------------------------------------------- |
---|
| 577 | def _receive_success(self): |
---|
| 578 | """Internal method - Successfully received a packet.""" |
---|
| 579 | self.transport_tracker = 0 |
---|
| 580 | |
---|
| 581 | |
---|
| 582 | def _receive_failure(self): |
---|
| 583 | """Internal method - Had a receive failure.""" |
---|
| 584 | self.transport_tracker += 1 |
---|
| 585 | |
---|
| 586 | |
---|
| 587 | def _receive_failure_exceeded(self, max_attempts): |
---|
| 588 | """Internal method - More recieve failures than max_attempts.""" |
---|
| 589 | if (self.transport_tracker < max_attempts): |
---|
| 590 | return False |
---|
| 591 | else: |
---|
| 592 | return True |
---|
| 593 | |
---|
| 594 | |
---|
| 595 | # End Class |
---|
| 596 | |
---|
| 597 | |
---|
| 598 | |
---|
| 599 | class WlanExpTransportNodeFactory(WlanExpTransportNode): |
---|
| 600 | """Sub-class of Transport Node used to help with node configuration and setup. |
---|
| 601 | |
---|
| 602 | This class will maintian the dictionary of Node Types. The dictionary |
---|
| 603 | contains the 32-bit Node Type as a key and the corresponding class name |
---|
| 604 | as a value. |
---|
| 605 | |
---|
| 606 | To add new Node Types, you can sub-class WlanExpTransportNodeFactory and add your own |
---|
| 607 | Node Types. |
---|
| 608 | |
---|
| 609 | Attributes: |
---|
| 610 | type_dict -- Dictionary of Node Types to class names |
---|
| 611 | """ |
---|
| 612 | type_dict = None |
---|
| 613 | |
---|
| 614 | |
---|
| 615 | def __init__(self, network_config=None): |
---|
| 616 | |
---|
| 617 | super(WlanExpTransportNodeFactory, self).__init__(network_config) |
---|
| 618 | |
---|
| 619 | # Initialize the list of node class/type mappingings |
---|
| 620 | # New mappings will be added by the context which creates the |
---|
| 621 | # instance of this factory class |
---|
| 622 | self.class_type_map = [] |
---|
| 623 | |
---|
| 624 | def setup(self, node_dict): |
---|
| 625 | self.set_init_configuration(serial_number=node_dict['serial_number'], |
---|
| 626 | node_id=node_dict['node_id'], |
---|
| 627 | node_name=node_dict['node_name'], |
---|
| 628 | ip_address=node_dict['ip_address'], |
---|
| 629 | unicast_port=node_dict['unicast_port'], |
---|
| 630 | broadcast_port=node_dict['broadcast_port']) |
---|
| 631 | |
---|
| 632 | def create_node(self, network_config=None, network_reset=True): |
---|
| 633 | """Based on the Node Type, dynamically create and return the correct node.""" |
---|
| 634 | |
---|
| 635 | node = None |
---|
| 636 | |
---|
| 637 | # Initialize the node network interface |
---|
| 638 | if network_reset: |
---|
| 639 | # Send broadcast command to reset the node network interface |
---|
| 640 | self.reset_network_inf() |
---|
| 641 | |
---|
| 642 | # Send broadcast command to initialize the node network interface |
---|
| 643 | self.setup_network_inf() |
---|
| 644 | |
---|
| 645 | try: |
---|
| 646 | # Send unicast command to get the node type |
---|
| 647 | node_type_ids = self.get_type_ids() |
---|
| 648 | |
---|
| 649 | # Lookup the appropriate Python class for this node type |
---|
| 650 | # The return value is the actual class (not an instance) that can |
---|
| 651 | # be used to create a new new object |
---|
| 652 | node_class = self.get_class_for_node_type_ids(node_type_ids) |
---|
| 653 | |
---|
| 654 | if node_class is not None: |
---|
| 655 | node = node_class() |
---|
| 656 | |
---|
| 657 | node.set_init_configuration(serial_number=self.sn_str, |
---|
| 658 | node_id=self.node_id, |
---|
| 659 | node_name=self.name, |
---|
| 660 | ip_address=self.transport.ip_address, |
---|
| 661 | unicast_port=self.transport.unicast_port, |
---|
| 662 | broadcast_port=self.transport.broadcast_port) |
---|
| 663 | |
---|
| 664 | # Store the platform/application IDs as node parameters |
---|
| 665 | # These are verified against the IDs returned in the NODE_INFO during init |
---|
| 666 | node.platform_id = node_type_ids[0] |
---|
| 667 | node.high_sw_id = node_type_ids[1] |
---|
| 668 | node.low_sw_id = node_type_ids[2] |
---|
| 669 | |
---|
| 670 | # Copy the network_config MTU to the node's transport object |
---|
| 671 | # The node itself will report an MTU during init, the lesser |
---|
| 672 | # of the network and node MTUs will be used to set the final |
---|
| 673 | # transport.mtu used to configure the node's max response size |
---|
| 674 | node.transport.mtu = network_config.get_param('mtu') |
---|
| 675 | |
---|
| 676 | msg = "Initializing {0}".format(node.sn_str) |
---|
| 677 | if node.name is not None: |
---|
| 678 | msg += " as {0}".format(node.name) |
---|
| 679 | print(msg) |
---|
| 680 | |
---|
| 681 | else: |
---|
| 682 | raise Exception('ERROR: no matching node class for node type IDs {}'.format(node_type_ids)) |
---|
| 683 | |
---|
| 684 | except ex.TransportError as err: |
---|
| 685 | msg = "ERROR: Node {0}\n".format(self.sn_str) |
---|
| 686 | msg += " Node is not responding. Please ensure that the \n" |
---|
| 687 | msg += " node is powered on and is properly configured.\n" |
---|
| 688 | print(msg) |
---|
| 689 | print(err) |
---|
| 690 | |
---|
| 691 | return node |
---|
| 692 | |
---|
| 693 | def add_node_type_class(self, class_type_mapping): |
---|
| 694 | """Adds a new node type / node class mapping. The argument must be a dictionary |
---|
| 695 | with type ID and class name keys. The factory instance searches the list of |
---|
| 696 | mappings to find the appropriate Python class for a given node during init.""" |
---|
| 697 | |
---|
| 698 | self.class_type_map.append(class_type_mapping) |
---|
| 699 | |
---|
| 700 | def get_class_for_node_type_ids(self, node_type_ids): |
---|
| 701 | """Lookup the Python class for the given node type IDs. The default |
---|
| 702 | mapping of type IDs to node classes is implemented in the factory |
---|
| 703 | __init()__ method. User code can override/supplement the default |
---|
| 704 | mapping before calling init_nodes() to use custom node classes. |
---|
| 705 | |
---|
| 706 | Args: |
---|
| 707 | node_type_ids: 3-tuple of integer IDs: |
---|
| 708 | (platform_id, high_sw_id, low_sw_id) |
---|
| 709 | """ |
---|
| 710 | |
---|
| 711 | # In the current wlan_exp code the node class only depends on |
---|
| 712 | # the application running in CPU High (AP, STA, IBSS). Future |
---|
| 713 | # extensions may add new node classes based on platform and |
---|
| 714 | # CPU Low application |
---|
| 715 | |
---|
| 716 | # Find the first matching class/type mapping |
---|
| 717 | # self.class_type_map is a list of dictionaries with the default |
---|
| 718 | # class/type maps inserted first. |
---|
| 719 | high_sw_id = node_type_ids[1] |
---|
| 720 | |
---|
| 721 | for c in self.class_type_map: |
---|
| 722 | if c['high_sw_id'] == high_sw_id: |
---|
| 723 | # Found matching type-class map |
---|
| 724 | # print('Node IDs platform={0}, high_sw={1}, low_sw={2} match class {3}'.format(node_type_ids[0], node_type_ids[1], node_type_ids[2], c['node_class'])) |
---|
| 725 | return c['node_class'] |
---|
| 726 | |
---|
| 727 | # No matching type-class found |
---|
| 728 | print('WARNING: no node class found for IDs platform={0}, high_sw={1}, low_sw{2}'.format( |
---|
| 729 | node_type_ids[0], node_type_ids[1], node_type_ids[2])) |
---|
| 730 | |
---|
| 731 | return None |
---|
| 732 | |
---|
| 733 | |
---|
| 734 | |
---|
| 735 | |
---|
| 736 | |
---|
| 737 | |
---|
| 738 | |
---|
| 739 | |
---|
| 740 | |
---|
| 741 | |
---|
| 742 | |
---|
| 743 | |
---|
| 744 | |
---|
| 745 | |
---|
| 746 | |
---|
| 747 | |
---|
| 748 | |
---|