source: ReferenceDesigns/w3_802.11/python/wlan_exp/transport/node.py

Last change on this file was 6320, checked in by chunter, 5 years ago

1.8.0 release wlan-exp

File size: 30.1 KB
Line 
1# -*- coding: utf-8 -*-
2"""
3------------------------------------------------------------------------------
4Mango 802.11 Reference Design Experiments Framework - Transport Node
5------------------------------------------------------------------------------
6License:   Copyright 2019 Mango Communications, Inc. All rights reserved.
7           Use and distribution subject to terms in LICENSE.txt
8------------------------------------------------------------------------------
9
10This module provides class definition for Transport Node.
11
12Functions (see below for more information):
13    WlanExpTransportNode()        -- Base class for Transport nodes
14    WlanExpTransportNodeFactory() -- Base class for creating a WlanExpTransportNode
15
16Integer constants:
17    NODE_TYPE, NODE_ID, NODE_HW_GEN, NODE_SERIAL_NUM,
18      NODE_FPGA_DNA -- Node hardware parameter constants
19
20If additional hardware parameters are needed for sub-classes of WlanExpTransportNode,
21please make sure that the values of these hardware parameters are not reused.
22
23"""
24
25from . import cmds
26from . 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
34NODE_TYPE               = 0
35NODE_ID                 = 1
36NODE_HW_GEN             = 2
37NODE_SERIAL_NUM         = 3
38NODE_FPGA_DNA           = 4
39
40
41
42class 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
599class 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
Note: See TracBrowser for help on using the repository browser.