diff --git a/docs/meshtastic/ble_interface.html b/docs/meshtastic/ble_interface.html index f6eef15..7bf14a3 100644 --- a/docs/meshtastic/ble_interface.html +++ b/docs/meshtastic/ble_interface.html @@ -27,7 +27,7 @@ Expand source code -
""" Bluetooth interface
+
"""Bluetooth interface
 """
 import logging
 import pygatt
@@ -44,22 +44,27 @@ FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
 class BLEInterface(MeshInterface):
     """A not quite ready - FIXME - BLE interface to devices"""
 
-    def __init__(self, address, debugOut=None):
+    def __init__(self, address, noProto=False, debugOut=None):
         self.address = address
-        self.adapter = pygatt.GATTToolBackend()  # BGAPIBackend()
-        self.adapter.start()
-        logging.debug(f"Connecting to {self.address}")
-        self.device = self.adapter.connect(address)
+        if not noProto:
+            self.adapter = pygatt.GATTToolBackend()  # BGAPIBackend()
+            self.adapter.start()
+            logging.debug(f"Connecting to {self.address}")
+            self.device = self.adapter.connect(address)
+        else:
+            self.adapter = None
+            self.device = None
         logging.debug("Connected to device")
         # fromradio = self.device.char_read(FROMRADIO_UUID)
-        MeshInterface.__init__(self, debugOut=debugOut)
+        MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto)
 
         self._readFromRadio()  # read the initial responses
 
         def handle_data(handle, data):
             self._handleFromRadio(data)
 
-        self.device.subscribe(FROMNUM_UUID, callback=handle_data)
+        if self.device:
+            self.device.subscribe(FROMNUM_UUID, callback=handle_data)
 
     def _sendToRadioImpl(self, toRadio):
         """Send a ToRadio protobuf to the device"""
@@ -69,15 +74,18 @@ class BLEInterface(MeshInterface):
 
     def close(self):
         MeshInterface.close(self)
-        self.adapter.stop()
+        if self.adapter:
+            self.adapter.stop()
 
     def _readFromRadio(self):
-        wasEmpty = False
-        while not wasEmpty:
-            b = self.device.char_read(FROMRADIO_UUID)
-            wasEmpty = len(b) == 0
-            if not wasEmpty:
-                self._handleFromRadio(b)
+ if not self.noProto: + wasEmpty = False + while not wasEmpty: + if self.device: + b = self.device.char_read(FROMRADIO_UUID) + wasEmpty = len(b) == 0 + if not wasEmpty: + self._handleFromRadio(b)
@@ -91,13 +99,14 @@ class BLEInterface(MeshInterface):
class BLEInterface -(address, debugOut=None) +(address, noProto=False, debugOut=None)

A not quite ready - FIXME - BLE interface to devices

Constructor

Keyword Arguments: -noProto – If True, don't try to run our protocol on the link - just be a dumb serial client.

+noProto – If True, don't try to run our protocol on the +link - just be a dumb serial client.

Expand source code @@ -105,22 +114,27 @@ noProto – If True, don't try to run our protocol on the link - just be a d
class BLEInterface(MeshInterface):
     """A not quite ready - FIXME - BLE interface to devices"""
 
-    def __init__(self, address, debugOut=None):
+    def __init__(self, address, noProto=False, debugOut=None):
         self.address = address
-        self.adapter = pygatt.GATTToolBackend()  # BGAPIBackend()
-        self.adapter.start()
-        logging.debug(f"Connecting to {self.address}")
-        self.device = self.adapter.connect(address)
+        if not noProto:
+            self.adapter = pygatt.GATTToolBackend()  # BGAPIBackend()
+            self.adapter.start()
+            logging.debug(f"Connecting to {self.address}")
+            self.device = self.adapter.connect(address)
+        else:
+            self.adapter = None
+            self.device = None
         logging.debug("Connected to device")
         # fromradio = self.device.char_read(FROMRADIO_UUID)
-        MeshInterface.__init__(self, debugOut=debugOut)
+        MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto)
 
         self._readFromRadio()  # read the initial responses
 
         def handle_data(handle, data):
             self._handleFromRadio(data)
 
-        self.device.subscribe(FROMNUM_UUID, callback=handle_data)
+        if self.device:
+            self.device.subscribe(FROMNUM_UUID, callback=handle_data)
 
     def _sendToRadioImpl(self, toRadio):
         """Send a ToRadio protobuf to the device"""
@@ -130,15 +144,18 @@ noProto – If True, don't try to run our protocol on the link - just be a d
 
     def close(self):
         MeshInterface.close(self)
-        self.adapter.stop()
+        if self.adapter:
+            self.adapter.stop()
 
     def _readFromRadio(self):
-        wasEmpty = False
-        while not wasEmpty:
-            b = self.device.char_read(FROMRADIO_UUID)
-            wasEmpty = len(b) == 0
-            if not wasEmpty:
-                self._handleFromRadio(b)
+ if not self.noProto: + wasEmpty = False + while not wasEmpty: + if self.device: + b = self.device.char_read(FROMRADIO_UUID) + wasEmpty = len(b) == 0 + if not wasEmpty: + self._handleFromRadio(b)

Ancestors

    diff --git a/docs/meshtastic/globals.html b/docs/meshtastic/globals.html new file mode 100644 index 0000000..9ce2f4b --- /dev/null +++ b/docs/meshtastic/globals.html @@ -0,0 +1,380 @@ + + + + + + +meshtastic.globals API documentation + + + + + + + + + + + +
    +
    +
    +

    Module meshtastic.globals

    +
    +
    +

    Globals singleton class.

    +

    Instead of using a global, stuff your variables in this "trash can". +This is not much better than using python's globals, but it allows +us to better test meshtastic. Plus, there are some weird python +global issues/gotcha that we can hopefully avoid by using this +class instead.

    +
    + +Expand source code + +
    """Globals singleton class.
    +
    +   Instead of using a global, stuff your variables in this "trash can".
    +   This is not much better than using python's globals, but it allows
    +   us to better test meshtastic. Plus, there are some weird python
    +   global issues/gotcha that we can hopefully avoid by using this
    +   class instead.
    +
    +"""
    +
    +class Globals:
    +    """Globals class is a Singleton."""
    +    __instance = None
    +
    +    @staticmethod
    +    def getInstance():
    +        """Get an instance of the Globals class."""
    +        if Globals.__instance is None:
    +            Globals()
    +        return Globals.__instance
    +
    +    def __init__(self):
    +        """Constructor for the Globals CLass"""
    +        if Globals.__instance is not None:
    +            raise Exception("This class is a singleton")
    +        else:
    +            Globals.__instance = self
    +        self.args = None
    +        self.parser = None
    +        self.target_node = None
    +        self.channel_index = None
    +
    +    def reset(self):
    +        """Reset all of our globals. If you add a member, add it to this method, too."""
    +        self.args = None
    +        self.parser = None
    +        self.target_node = None
    +        self.channel_index = None
    +
    +    def set_args(self, args):
    +        """Set the args"""
    +        self.args = args
    +
    +    def set_parser(self, parser):
    +        """Set the parser"""
    +        self.parser = parser
    +
    +    def set_target_node(self, target_node):
    +        """Set the target_node"""
    +        self.target_node = target_node
    +
    +    def set_channel_index(self, channel_index):
    +        """Set the channel_index"""
    +        self.channel_index = channel_index
    +
    +    def get_args(self):
    +        """Get args"""
    +        return self.args
    +
    +    def get_parser(self):
    +        """Get parser"""
    +        return self.parser
    +
    +    def get_target_node(self):
    +        """Get target_node"""
    +        return self.target_node
    +
    +    def get_channel_index(self):
    +        """Get channel_index"""
    +        return self.channel_index
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Globals +
    +
    +

    Globals class is a Singleton.

    +

    Constructor for the Globals CLass

    +
    + +Expand source code + +
    class Globals:
    +    """Globals class is a Singleton."""
    +    __instance = None
    +
    +    @staticmethod
    +    def getInstance():
    +        """Get an instance of the Globals class."""
    +        if Globals.__instance is None:
    +            Globals()
    +        return Globals.__instance
    +
    +    def __init__(self):
    +        """Constructor for the Globals CLass"""
    +        if Globals.__instance is not None:
    +            raise Exception("This class is a singleton")
    +        else:
    +            Globals.__instance = self
    +        self.args = None
    +        self.parser = None
    +        self.target_node = None
    +        self.channel_index = None
    +
    +    def reset(self):
    +        """Reset all of our globals. If you add a member, add it to this method, too."""
    +        self.args = None
    +        self.parser = None
    +        self.target_node = None
    +        self.channel_index = None
    +
    +    def set_args(self, args):
    +        """Set the args"""
    +        self.args = args
    +
    +    def set_parser(self, parser):
    +        """Set the parser"""
    +        self.parser = parser
    +
    +    def set_target_node(self, target_node):
    +        """Set the target_node"""
    +        self.target_node = target_node
    +
    +    def set_channel_index(self, channel_index):
    +        """Set the channel_index"""
    +        self.channel_index = channel_index
    +
    +    def get_args(self):
    +        """Get args"""
    +        return self.args
    +
    +    def get_parser(self):
    +        """Get parser"""
    +        return self.parser
    +
    +    def get_target_node(self):
    +        """Get target_node"""
    +        return self.target_node
    +
    +    def get_channel_index(self):
    +        """Get channel_index"""
    +        return self.channel_index
    +
    +

    Static methods

    +
    +
    +def getInstance() +
    +
    +

    Get an instance of the Globals class.

    +
    + +Expand source code + +
    @staticmethod
    +def getInstance():
    +    """Get an instance of the Globals class."""
    +    if Globals.__instance is None:
    +        Globals()
    +    return Globals.__instance
    +
    +
    +
    +

    Methods

    +
    +
    +def get_args(self) +
    +
    +

    Get args

    +
    + +Expand source code + +
    def get_args(self):
    +    """Get args"""
    +    return self.args
    +
    +
    +
    +def get_channel_index(self) +
    +
    +

    Get channel_index

    +
    + +Expand source code + +
    def get_channel_index(self):
    +    """Get channel_index"""
    +    return self.channel_index
    +
    +
    +
    +def get_parser(self) +
    +
    +

    Get parser

    +
    + +Expand source code + +
    def get_parser(self):
    +    """Get parser"""
    +    return self.parser
    +
    +
    +
    +def get_target_node(self) +
    +
    +

    Get target_node

    +
    + +Expand source code + +
    def get_target_node(self):
    +    """Get target_node"""
    +    return self.target_node
    +
    +
    +
    +def reset(self) +
    +
    +

    Reset all of our globals. If you add a member, add it to this method, too.

    +
    + +Expand source code + +
    def reset(self):
    +    """Reset all of our globals. If you add a member, add it to this method, too."""
    +    self.args = None
    +    self.parser = None
    +    self.target_node = None
    +    self.channel_index = None
    +
    +
    +
    +def set_args(self, args) +
    +
    +

    Set the args

    +
    + +Expand source code + +
    def set_args(self, args):
    +    """Set the args"""
    +    self.args = args
    +
    +
    +
    +def set_channel_index(self, channel_index) +
    +
    +

    Set the channel_index

    +
    + +Expand source code + +
    def set_channel_index(self, channel_index):
    +    """Set the channel_index"""
    +    self.channel_index = channel_index
    +
    +
    +
    +def set_parser(self, parser) +
    +
    +

    Set the parser

    +
    + +Expand source code + +
    def set_parser(self, parser):
    +    """Set the parser"""
    +    self.parser = parser
    +
    +
    +
    +def set_target_node(self, target_node) +
    +
    +

    Set the target_node

    +
    + +Expand source code + +
    def set_target_node(self, target_node):
    +    """Set the target_node"""
    +    self.target_node = target_node
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/meshtastic/index.html b/docs/meshtastic/index.html index 62cab43..bfd2d02 100644 --- a/docs/meshtastic/index.html +++ b/docs/meshtastic/index.html @@ -178,9 +178,10 @@ BROADCAST_NUM = 0xffffffff BROADCAST_ADDR = "^all" -"""The numeric buildnumber (shared with android apps) specifying the level of device code we are guaranteed to understand +"""The numeric buildnumber (shared with android apps) specifying the + level of device code we are guaranteed to understand -format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20 + format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20 """ OUR_APP_VERSION = 20200 @@ -291,6 +292,10 @@ protocols = {
    +
    meshtastic.globals
    +
    +

    Globals singleton class …

    +
    meshtastic.mesh_interface

    Mesh Interface class

    @@ -339,7 +344,12 @@ protocols = {

    TCPInterface class for interfacing with http endpoint

    -
    meshtastic.test
    +
    meshtastic.test
    +
    +

    With two radios connected serially, send and receive test +messages and report back if successful.

    +
    +
    meshtastic.tests
    @@ -358,7 +368,8 @@ protocols = {
    var BROADCAST_ADDR
    -

    The numeric buildnumber (shared with android apps) specifying the level of device code we are guaranteed to understand

    +

    The numeric buildnumber (shared with android apps) specifying the +level of device code we are guaranteed to understand

    format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20

    var BROADCAST_NUM
    @@ -461,6 +472,7 @@ protocols = {
  • meshtastic.channel_pb2
  • meshtastic.deviceonly_pb2
  • meshtastic.environmental_measurement_pb2
  • +
  • meshtastic.globals
  • meshtastic.mesh_interface
  • meshtastic.mesh_pb2
  • meshtastic.mqtt_pb2
  • @@ -473,7 +485,8 @@ protocols = {
  • meshtastic.storeforward_pb2
  • meshtastic.stream_interface
  • meshtastic.tcp_interface
  • -
  • meshtastic.test
  • +
  • meshtastic.test
  • +
  • meshtastic.tests
  • meshtastic.tunnel
  • meshtastic.util
diff --git a/docs/meshtastic/mesh_interface.html b/docs/meshtastic/mesh_interface.html index 67b36d3..dfc4e96 100644 --- a/docs/meshtastic/mesh_interface.html +++ b/docs/meshtastic/mesh_interface.html @@ -68,7 +68,8 @@ class MeshInterface: """Constructor Keyword Arguments: - noProto -- If True, don't try to run our protocol on the link - just be a dumb serial client. + noProto -- If True, don't try to run our protocol on the + link - just be a dumb serial client. """ self.debugOut = debugOut self.nodes = None # FIXME @@ -82,6 +83,8 @@ class MeshInterface: self.heartbeatTimer = None random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it self.currentPacketId = random.randint(0, 0xffffffff) + self.nodesByNum = None + self.configId = None def close(self): """Shutdown this interface""" @@ -119,54 +122,55 @@ class MeshInterface: def showNodes(self, includeSelf=True, file=sys.stdout): """Show table summary of nodes in mesh""" def formatFloat(value, precision=2, unit=''): + """Format a float value with precsion.""" return f'{value:.{precision}f}{unit}' if value else None def getLH(ts): + """Format last heard""" return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') if ts else None def getTimeAgo(ts): + """Format how long ago have we heard from this node (aka timeago).""" return timeago.format(datetime.fromtimestamp(ts), datetime.now()) if ts else None rows = [] - for node in self.nodes.values(): - if not includeSelf and node['num'] == self.localNode.nodeNum: - continue + if self.nodes: + for node in self.nodes.values(): + if not includeSelf and node['num'] == self.localNode.nodeNum: + continue - row = {"N": 0} + row = {"N": 0} + + user = node.get('user') + if user: + row.update({ + "User": user['longName'], + "AKA": user['shortName'], + "ID": user['id'], + }) + + pos = node.get('position') + if pos: + row.update({ + "Latitude": formatFloat(pos.get("latitude"), 4, "°"), + "Longitude": formatFloat(pos.get("longitude"), 4, "°"), + "Altitude": formatFloat(pos.get("altitude"), 0, " m"), + "Battery": formatFloat(pos.get("batteryLevel"), 2, "%"), + }) - user = node.get('user') - if user: row.update({ - "User": user['longName'], - "AKA": user['shortName'], - "ID": user['id'], + "SNR": formatFloat(node.get("snr"), 2, " dB"), + "LastHeard": getLH(node.get("lastHeard")), + "Since": getTimeAgo(node.get("lastHeard")), }) - pos = node.get('position') - if pos: - row.update({ - "Latitude": formatFloat(pos.get("latitude"), 4, "°"), - "Longitude": formatFloat(pos.get("longitude"), 4, "°"), - "Altitude": formatFloat(pos.get("altitude"), 0, " m"), - "Battery": formatFloat(pos.get("batteryLevel"), 2, "%"), - }) + rows.append(row) - row.update({ - "SNR": formatFloat(node.get("snr"), 2, " dB"), - "LastHeard": getLH(node.get("lastHeard")), - "Since": getTimeAgo(node.get("lastHeard")), - }) - - rows.append(row) - - # Why doesn't this way work? - #rows.sort(key=lambda r: r.get('LastHeard', '0000'), reverse=True) rows.sort(key=lambda r: r.get('LastHeard') or '0000', reverse=True) for i, row in enumerate(rows): row['N'] = i+1 - table = tabulate(rows, headers='keys', missingval='N/A', - tablefmt='fancy_grid') + table = tabulate(rows, headers='keys', missingval='N/A', tablefmt='fancy_grid') print(table) return table @@ -189,18 +193,24 @@ class MeshInterface: hopLimit=defaultHopLimit, onResponse=None, channelIndex=0): - """Send a utf8 string to some other node, if the node has a display it will also be shown on the device. + """Send a utf8 string to some other node, if the node has a display it + will also be shown on the device. Arguments: text {string} -- The text to send Keyword Arguments: - destinationId {nodeId or nodeNum} -- where to send this message (default: {BROADCAST_ADDR}) - portNum -- the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list - wantAck -- True if you want the message sent in a reliable manner (with retries and ack/nak provided for delivery) - wantResponse -- True if you want the service on the other side to send an application layer response + destinationId {nodeId or nodeNum} -- where to send this + message (default: {BROADCAST_ADDR}) + portNum -- the application portnum (similar to IP port numbers) + of the destination, see portnums.proto for a list + wantAck -- True if you want the message sent in a reliable manner + (with retries and ack/nak provided for delivery) + wantResponse -- True if you want the service on the other side to + send an application layer response - Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. + Returns the sent packet. The id field will be populated in this packet + and can be used to track future message acks/naks. """ return self.sendData(text.encode("utf-8"), destinationId, portNum=portnums_pb2.PortNum.TEXT_MESSAGE_APP, @@ -219,15 +229,23 @@ class MeshInterface: """Send a data packet to some other node Keyword Arguments: - data -- the data to send, either as an array of bytes or as a protobuf (which will be automatically serialized to bytes) - destinationId {nodeId or nodeNum} -- where to send this message (default: {BROADCAST_ADDR}) - portNum -- the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list - wantAck -- True if you want the message sent in a reliable manner (with retries and ack/nak provided for delivery) - wantResponse -- True if you want the service on the other side to send an application layer response - onResponse -- A closure of the form funct(packet), that will be called when a response packet arrives - (or the transaction is NAKed due to non receipt) + data -- the data to send, either as an array of bytes or + as a protobuf (which will be automatically + serialized to bytes) + destinationId {nodeId or nodeNum} -- where to send this + message (default: {BROADCAST_ADDR}) + portNum -- the application portnum (similar to IP port numbers) + of the destination, see portnums.proto for a list + wantAck -- True if you want the message sent in a reliable + manner (with retries and ack/nak provided for delivery) + wantResponse -- True if you want the service on the other + side to send an application layer response + onResponse -- A closure of the form funct(packet), that will be + called when a response packet arrives (or the transaction + is NAKed due to non receipt) - Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. + Returns the sent packet. The id field will be populated in this packet + and can be used to track future message acks/naks. """ if getattr(data, "SerializeToString", None): logging.debug(f"Serializing protobuf as data: {stripnl(data)}") @@ -251,16 +269,18 @@ class MeshInterface: self._addResponseHandler(p.id, onResponse) return p - def sendPosition(self, latitude=0.0, longitude=0.0, altitude=0, timeSec=0, destinationId=BROADCAST_ADDR, wantAck=False, wantResponse=False): + def sendPosition(self, latitude=0.0, longitude=0.0, altitude=0, timeSec=0, + destinationId=BROADCAST_ADDR, wantAck=False, wantResponse=False): """ Send a position packet to some other node (normally a broadcast) - Also, the device software will notice this packet and use it to automatically set its notion of - the local position. + Also, the device software will notice this packet and use it to automatically + set its notion of the local position. If timeSec is not specified (recommended), we will use the local machine time. - Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. + Returns the sent packet. The id field will be populated in this packet and + can be used to track future message acks/naks. """ p = mesh_pb2.Position() if latitude != 0.0: @@ -321,8 +341,8 @@ class MeshInterface: meshPacket.want_ack = wantAck meshPacket.hop_limit = hopLimit - # if the user hasn't set an ID for this packet (likely and recommended), we should pick a new unique ID - # so the message can be tracked. + # if the user hasn't set an ID for this packet (likely and recommended), + # we should pick a new unique ID so the message can be tracked. if meshPacket.id == 0: meshPacket.id = self._generatePacketId() @@ -333,8 +353,7 @@ class MeshInterface: def waitForConfig(self): """Block until radio config is received. Returns True if config has been received.""" - success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes') - ) and self.localNode.waitForConfig() + success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes')) and self.localNode.waitForConfig() if not success: raise Exception("Timed out waiting for interface config") @@ -436,8 +455,7 @@ class MeshInterface: def _sendToRadio(self, toRadio): """Send a ToRadio protobuf to the device""" if self.noProto: - logging.warning( - f"Not sending packet because protocol use is disabled by noProto") + logging.warning(f"Not sending packet because protocol use is disabled by noProto") else: #logging.debug(f"Sending toRadio: {stripnl(toRadio)}") self._sendToRadioImpl(toRadio) @@ -448,7 +466,8 @@ class MeshInterface: def _handleConfigComplete(self): """ - Done with initial config messages, now send regular MeshPackets to ask for settings and channels + Done with initial config messages, now send regular MeshPackets + to ask for settings and channels """ self.localNode.requestConfig() @@ -469,7 +488,7 @@ class MeshInterface: failmsg = None # Check for app too old if self.myInfo.min_app_version > OUR_APP_VERSION: - failmsg = "This device needs a newer python client, please \"pip install --upgrade meshtastic\". "\ + failmsg = "This device needs a newer python client, run 'pip install --upgrade meshtastic'."\ "For more information see https://tinyurl.com/5bjsxu32" # check for firmware too old @@ -497,13 +516,15 @@ class MeshInterface: publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.node.updated", node=node, interface=self)) elif fromRadio.config_complete_id == self.configId: - # we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id + # we ignore the config_complete_id, it is unneeded for our + # stream API fromRadio.config_complete_id logging.debug(f"Config complete ID {self.configId}") self._handleConfigComplete() elif fromRadio.HasField("packet"): self._handlePacketFromRadio(fromRadio.packet) elif fromRadio.rebooted: - # Tell clients the device went away. Careful not to call the overridden subclass version that closes the serial port + # Tell clients the device went away. Careful not to call the overridden + # subclass version that closes the serial port MeshInterface._disconnected(self) self._startConfig() # redownload the node db etc... @@ -569,12 +590,12 @@ class MeshInterface: asDict["raw"] = meshPacket # from might be missing if the nodenum was zero. - if not "from" in asDict: + if "from" not in asDict: asDict["from"] = 0 logging.error( f"Device returned a packet we sent, ignoring: {stripnl(asDict)}") return - if not "to" in asDict: + if "to" not in asDict: asDict["to"] = 0 # /add fromId and toId fields based on the node ID @@ -597,8 +618,9 @@ class MeshInterface: # byte array. decoded["payload"] = meshPacket.decoded.payload - # UNKNOWN_APP is the default protobuf portnum value, and therefore if not set it will not be populated at all - # to make API usage easier, set it to prevent confusion + # UNKNOWN_APP is the default protobuf portnum value, and therefore if not + # set it will not be populated at all to make API usage easier, set + # it to prevent confusion if not "portnum" in decoded: decoded["portnum"] = portnums_pb2.PortNum.Name( portnums_pb2.PortNum.UNKNOWN_APP) @@ -607,8 +629,9 @@ class MeshInterface: topic = f"meshtastic.receive.data.{portnum}" - # decode position protobufs and update nodedb, provide decoded version as "position" in the published msg - # move the following into a 'decoders' API that clients could register? + # decode position protobufs and update nodedb, provide decoded version + # as "position" in the published msg move the following into a 'decoders' + # API that clients could register? portNumInt = meshPacket.decoded.portnum # we want portnum as an int handler = protocols.get(portNumInt) # The decoded protobuf as a dictionary (if we understand this message) @@ -667,7 +690,8 @@ nodes debugOut

Constructor

Keyword Arguments: -noProto – If True, don't try to run our protocol on the link - just be a dumb serial client.

+noProto – If True, don't try to run our protocol on the +link - just be a dumb serial client.

Expand source code @@ -686,7 +710,8 @@ noProto – If True, don't try to run our protocol on the link - just be a d """Constructor Keyword Arguments: - noProto -- If True, don't try to run our protocol on the link - just be a dumb serial client. + noProto -- If True, don't try to run our protocol on the + link - just be a dumb serial client. """ self.debugOut = debugOut self.nodes = None # FIXME @@ -700,6 +725,8 @@ noProto – If True, don't try to run our protocol on the link - just be a d self.heartbeatTimer = None random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it self.currentPacketId = random.randint(0, 0xffffffff) + self.nodesByNum = None + self.configId = None def close(self): """Shutdown this interface""" @@ -737,54 +764,55 @@ noProto – If True, don't try to run our protocol on the link - just be a d def showNodes(self, includeSelf=True, file=sys.stdout): """Show table summary of nodes in mesh""" def formatFloat(value, precision=2, unit=''): + """Format a float value with precsion.""" return f'{value:.{precision}f}{unit}' if value else None def getLH(ts): + """Format last heard""" return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') if ts else None def getTimeAgo(ts): + """Format how long ago have we heard from this node (aka timeago).""" return timeago.format(datetime.fromtimestamp(ts), datetime.now()) if ts else None rows = [] - for node in self.nodes.values(): - if not includeSelf and node['num'] == self.localNode.nodeNum: - continue + if self.nodes: + for node in self.nodes.values(): + if not includeSelf and node['num'] == self.localNode.nodeNum: + continue - row = {"N": 0} + row = {"N": 0} + + user = node.get('user') + if user: + row.update({ + "User": user['longName'], + "AKA": user['shortName'], + "ID": user['id'], + }) + + pos = node.get('position') + if pos: + row.update({ + "Latitude": formatFloat(pos.get("latitude"), 4, "°"), + "Longitude": formatFloat(pos.get("longitude"), 4, "°"), + "Altitude": formatFloat(pos.get("altitude"), 0, " m"), + "Battery": formatFloat(pos.get("batteryLevel"), 2, "%"), + }) - user = node.get('user') - if user: row.update({ - "User": user['longName'], - "AKA": user['shortName'], - "ID": user['id'], + "SNR": formatFloat(node.get("snr"), 2, " dB"), + "LastHeard": getLH(node.get("lastHeard")), + "Since": getTimeAgo(node.get("lastHeard")), }) - pos = node.get('position') - if pos: - row.update({ - "Latitude": formatFloat(pos.get("latitude"), 4, "°"), - "Longitude": formatFloat(pos.get("longitude"), 4, "°"), - "Altitude": formatFloat(pos.get("altitude"), 0, " m"), - "Battery": formatFloat(pos.get("batteryLevel"), 2, "%"), - }) + rows.append(row) - row.update({ - "SNR": formatFloat(node.get("snr"), 2, " dB"), - "LastHeard": getLH(node.get("lastHeard")), - "Since": getTimeAgo(node.get("lastHeard")), - }) - - rows.append(row) - - # Why doesn't this way work? - #rows.sort(key=lambda r: r.get('LastHeard', '0000'), reverse=True) rows.sort(key=lambda r: r.get('LastHeard') or '0000', reverse=True) for i, row in enumerate(rows): row['N'] = i+1 - table = tabulate(rows, headers='keys', missingval='N/A', - tablefmt='fancy_grid') + table = tabulate(rows, headers='keys', missingval='N/A', tablefmt='fancy_grid') print(table) return table @@ -807,18 +835,24 @@ noProto – If True, don't try to run our protocol on the link - just be a d hopLimit=defaultHopLimit, onResponse=None, channelIndex=0): - """Send a utf8 string to some other node, if the node has a display it will also be shown on the device. + """Send a utf8 string to some other node, if the node has a display it + will also be shown on the device. Arguments: text {string} -- The text to send Keyword Arguments: - destinationId {nodeId or nodeNum} -- where to send this message (default: {BROADCAST_ADDR}) - portNum -- the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list - wantAck -- True if you want the message sent in a reliable manner (with retries and ack/nak provided for delivery) - wantResponse -- True if you want the service on the other side to send an application layer response + destinationId {nodeId or nodeNum} -- where to send this + message (default: {BROADCAST_ADDR}) + portNum -- the application portnum (similar to IP port numbers) + of the destination, see portnums.proto for a list + wantAck -- True if you want the message sent in a reliable manner + (with retries and ack/nak provided for delivery) + wantResponse -- True if you want the service on the other side to + send an application layer response - Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. + Returns the sent packet. The id field will be populated in this packet + and can be used to track future message acks/naks. """ return self.sendData(text.encode("utf-8"), destinationId, portNum=portnums_pb2.PortNum.TEXT_MESSAGE_APP, @@ -837,15 +871,23 @@ noProto – If True, don't try to run our protocol on the link - just be a d """Send a data packet to some other node Keyword Arguments: - data -- the data to send, either as an array of bytes or as a protobuf (which will be automatically serialized to bytes) - destinationId {nodeId or nodeNum} -- where to send this message (default: {BROADCAST_ADDR}) - portNum -- the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list - wantAck -- True if you want the message sent in a reliable manner (with retries and ack/nak provided for delivery) - wantResponse -- True if you want the service on the other side to send an application layer response - onResponse -- A closure of the form funct(packet), that will be called when a response packet arrives - (or the transaction is NAKed due to non receipt) + data -- the data to send, either as an array of bytes or + as a protobuf (which will be automatically + serialized to bytes) + destinationId {nodeId or nodeNum} -- where to send this + message (default: {BROADCAST_ADDR}) + portNum -- the application portnum (similar to IP port numbers) + of the destination, see portnums.proto for a list + wantAck -- True if you want the message sent in a reliable + manner (with retries and ack/nak provided for delivery) + wantResponse -- True if you want the service on the other + side to send an application layer response + onResponse -- A closure of the form funct(packet), that will be + called when a response packet arrives (or the transaction + is NAKed due to non receipt) - Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. + Returns the sent packet. The id field will be populated in this packet + and can be used to track future message acks/naks. """ if getattr(data, "SerializeToString", None): logging.debug(f"Serializing protobuf as data: {stripnl(data)}") @@ -869,16 +911,18 @@ noProto – If True, don't try to run our protocol on the link - just be a d self._addResponseHandler(p.id, onResponse) return p - def sendPosition(self, latitude=0.0, longitude=0.0, altitude=0, timeSec=0, destinationId=BROADCAST_ADDR, wantAck=False, wantResponse=False): + def sendPosition(self, latitude=0.0, longitude=0.0, altitude=0, timeSec=0, + destinationId=BROADCAST_ADDR, wantAck=False, wantResponse=False): """ Send a position packet to some other node (normally a broadcast) - Also, the device software will notice this packet and use it to automatically set its notion of - the local position. + Also, the device software will notice this packet and use it to automatically + set its notion of the local position. If timeSec is not specified (recommended), we will use the local machine time. - Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. + Returns the sent packet. The id field will be populated in this packet and + can be used to track future message acks/naks. """ p = mesh_pb2.Position() if latitude != 0.0: @@ -939,8 +983,8 @@ noProto – If True, don't try to run our protocol on the link - just be a d meshPacket.want_ack = wantAck meshPacket.hop_limit = hopLimit - # if the user hasn't set an ID for this packet (likely and recommended), we should pick a new unique ID - # so the message can be tracked. + # if the user hasn't set an ID for this packet (likely and recommended), + # we should pick a new unique ID so the message can be tracked. if meshPacket.id == 0: meshPacket.id = self._generatePacketId() @@ -951,8 +995,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d def waitForConfig(self): """Block until radio config is received. Returns True if config has been received.""" - success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes') - ) and self.localNode.waitForConfig() + success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes')) and self.localNode.waitForConfig() if not success: raise Exception("Timed out waiting for interface config") @@ -1054,8 +1097,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d def _sendToRadio(self, toRadio): """Send a ToRadio protobuf to the device""" if self.noProto: - logging.warning( - f"Not sending packet because protocol use is disabled by noProto") + logging.warning(f"Not sending packet because protocol use is disabled by noProto") else: #logging.debug(f"Sending toRadio: {stripnl(toRadio)}") self._sendToRadioImpl(toRadio) @@ -1066,7 +1108,8 @@ noProto – If True, don't try to run our protocol on the link - just be a d def _handleConfigComplete(self): """ - Done with initial config messages, now send regular MeshPackets to ask for settings and channels + Done with initial config messages, now send regular MeshPackets + to ask for settings and channels """ self.localNode.requestConfig() @@ -1087,7 +1130,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d failmsg = None # Check for app too old if self.myInfo.min_app_version > OUR_APP_VERSION: - failmsg = "This device needs a newer python client, please \"pip install --upgrade meshtastic\". "\ + failmsg = "This device needs a newer python client, run 'pip install --upgrade meshtastic'."\ "For more information see https://tinyurl.com/5bjsxu32" # check for firmware too old @@ -1115,13 +1158,15 @@ noProto – If True, don't try to run our protocol on the link - just be a d publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.node.updated", node=node, interface=self)) elif fromRadio.config_complete_id == self.configId: - # we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id + # we ignore the config_complete_id, it is unneeded for our + # stream API fromRadio.config_complete_id logging.debug(f"Config complete ID {self.configId}") self._handleConfigComplete() elif fromRadio.HasField("packet"): self._handlePacketFromRadio(fromRadio.packet) elif fromRadio.rebooted: - # Tell clients the device went away. Careful not to call the overridden subclass version that closes the serial port + # Tell clients the device went away. Careful not to call the overridden + # subclass version that closes the serial port MeshInterface._disconnected(self) self._startConfig() # redownload the node db etc... @@ -1187,12 +1232,12 @@ noProto – If True, don't try to run our protocol on the link - just be a d asDict["raw"] = meshPacket # from might be missing if the nodenum was zero. - if not "from" in asDict: + if "from" not in asDict: asDict["from"] = 0 logging.error( f"Device returned a packet we sent, ignoring: {stripnl(asDict)}") return - if not "to" in asDict: + if "to" not in asDict: asDict["to"] = 0 # /add fromId and toId fields based on the node ID @@ -1215,8 +1260,9 @@ noProto – If True, don't try to run our protocol on the link - just be a d # byte array. decoded["payload"] = meshPacket.decoded.payload - # UNKNOWN_APP is the default protobuf portnum value, and therefore if not set it will not be populated at all - # to make API usage easier, set it to prevent confusion + # UNKNOWN_APP is the default protobuf portnum value, and therefore if not + # set it will not be populated at all to make API usage easier, set + # it to prevent confusion if not "portnum" in decoded: decoded["portnum"] = portnums_pb2.PortNum.Name( portnums_pb2.PortNum.UNKNOWN_APP) @@ -1225,8 +1271,9 @@ noProto – If True, don't try to run our protocol on the link - just be a d topic = f"meshtastic.receive.data.{portnum}" - # decode position protobufs and update nodedb, provide decoded version as "position" in the published msg - # move the following into a 'decoders' API that clients could register? + # decode position protobufs and update nodedb, provide decoded version + # as "position" in the published msg move the following into a 'decoders' + # API that clients could register? portNumInt = meshPacket.decoded.portnum # we want portnum as an int handler = protocols.get(portNumInt) # The decoded protobuf as a dictionary (if we understand this message) @@ -1381,14 +1428,22 @@ noProto – If True, don't try to run our protocol on the link - just be a d

Send a data packet to some other node

Keyword Arguments: -data – the data to send, either as an array of bytes or as a protobuf (which will be automatically serialized to bytes) -destinationId {nodeId or nodeNum} – where to send this message (default: {BROADCAST_ADDR}) -portNum – the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list -wantAck – True if you want the message sent in a reliable manner (with retries and ack/nak provided for delivery) -wantResponse – True if you want the service on the other side to send an application layer response -onResponse – A closure of the form funct(packet), that will be called when a response packet arrives -(or the transaction is NAKed due to non receipt)

-

Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks.

+data – the data to send, either as an array of bytes or +as a protobuf (which will be automatically +serialized to bytes) +destinationId {nodeId or nodeNum} – where to send this +message (default: {BROADCAST_ADDR}) +portNum – the application portnum (similar to IP port numbers) +of the destination, see portnums.proto for a list +wantAck – True if you want the message sent in a reliable +manner (with retries and ack/nak provided for delivery) +wantResponse – True if you want the service on the other +side to send an application layer response +onResponse – A closure of the form funct(packet), that will be +called when a response packet arrives (or the transaction +is NAKed due to non receipt)

+

Returns the sent packet. The id field will be populated in this packet +and can be used to track future message acks/naks.

Expand source code @@ -1402,15 +1457,23 @@ onResponse – A closure of the form funct(packet), that will be called when """Send a data packet to some other node Keyword Arguments: - data -- the data to send, either as an array of bytes or as a protobuf (which will be automatically serialized to bytes) - destinationId {nodeId or nodeNum} -- where to send this message (default: {BROADCAST_ADDR}) - portNum -- the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list - wantAck -- True if you want the message sent in a reliable manner (with retries and ack/nak provided for delivery) - wantResponse -- True if you want the service on the other side to send an application layer response - onResponse -- A closure of the form funct(packet), that will be called when a response packet arrives - (or the transaction is NAKed due to non receipt) + data -- the data to send, either as an array of bytes or + as a protobuf (which will be automatically + serialized to bytes) + destinationId {nodeId or nodeNum} -- where to send this + message (default: {BROADCAST_ADDR}) + portNum -- the application portnum (similar to IP port numbers) + of the destination, see portnums.proto for a list + wantAck -- True if you want the message sent in a reliable + manner (with retries and ack/nak provided for delivery) + wantResponse -- True if you want the service on the other + side to send an application layer response + onResponse -- A closure of the form funct(packet), that will be + called when a response packet arrives (or the transaction + is NAKed due to non receipt) - Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. + Returns the sent packet. The id field will be populated in this packet + and can be used to track future message acks/naks. """ if getattr(data, "SerializeToString", None): logging.debug(f"Serializing protobuf as data: {stripnl(data)}") @@ -1440,24 +1503,27 @@ onResponse – A closure of the form funct(packet), that will be called when

Send a position packet to some other node (normally a broadcast)

-

Also, the device software will notice this packet and use it to automatically set its notion of -the local position.

+

Also, the device software will notice this packet and use it to automatically +set its notion of the local position.

If timeSec is not specified (recommended), we will use the local machine time.

-

Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks.

+

Returns the sent packet. The id field will be populated in this packet and +can be used to track future message acks/naks.

Expand source code -
def sendPosition(self, latitude=0.0, longitude=0.0, altitude=0, timeSec=0, destinationId=BROADCAST_ADDR, wantAck=False, wantResponse=False):
+
def sendPosition(self, latitude=0.0, longitude=0.0, altitude=0, timeSec=0,
+                 destinationId=BROADCAST_ADDR, wantAck=False, wantResponse=False):
     """
     Send a position packet to some other node (normally a broadcast)
 
-    Also, the device software will notice this packet and use it to automatically set its notion of
-    the local position.
+    Also, the device software will notice this packet and use it to automatically
+    set its notion of the local position.
 
     If timeSec is not specified (recommended), we will use the local machine time.
 
-    Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks.
+    Returns the sent packet. The id field will be populated in this packet and
+    can be used to track future message acks/naks.
     """
     p = mesh_pb2.Position()
     if latitude != 0.0:
@@ -1483,15 +1549,21 @@ the local position.

def sendText(self, text: ~AnyStr, destinationId='^all', wantAck=False, wantResponse=False, hopLimit=3, onResponse=None, channelIndex=0)
-

Send a utf8 string to some other node, if the node has a display it will also be shown on the device.

+

Send a utf8 string to some other node, if the node has a display it +will also be shown on the device.

Arguments

text {string} – The text to send

Keyword Arguments: -destinationId {nodeId or nodeNum} – where to send this message (default: {BROADCAST_ADDR}) -portNum – the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list -wantAck – True if you want the message sent in a reliable manner (with retries and ack/nak provided for delivery) -wantResponse – True if you want the service on the other side to send an application layer response

-

Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks.

+destinationId {nodeId or nodeNum} – where to send this +message (default: {BROADCAST_ADDR}) +portNum – the application portnum (similar to IP port numbers) +of the destination, see portnums.proto for a list +wantAck – True if you want the message sent in a reliable manner +(with retries and ack/nak provided for delivery) +wantResponse – True if you want the service on the other side to +send an application layer response

+

Returns the sent packet. The id field will be populated in this packet +and can be used to track future message acks/naks.

Expand source code @@ -1503,18 +1575,24 @@ wantResponse – True if you want the service on the other side to send an a hopLimit=defaultHopLimit, onResponse=None, channelIndex=0): - """Send a utf8 string to some other node, if the node has a display it will also be shown on the device. + """Send a utf8 string to some other node, if the node has a display it + will also be shown on the device. Arguments: text {string} -- The text to send Keyword Arguments: - destinationId {nodeId or nodeNum} -- where to send this message (default: {BROADCAST_ADDR}) - portNum -- the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list - wantAck -- True if you want the message sent in a reliable manner (with retries and ack/nak provided for delivery) - wantResponse -- True if you want the service on the other side to send an application layer response + destinationId {nodeId or nodeNum} -- where to send this + message (default: {BROADCAST_ADDR}) + portNum -- the application portnum (similar to IP port numbers) + of the destination, see portnums.proto for a list + wantAck -- True if you want the message sent in a reliable manner + (with retries and ack/nak provided for delivery) + wantResponse -- True if you want the service on the other side to + send an application layer response - Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. + Returns the sent packet. The id field will be populated in this packet + and can be used to track future message acks/naks. """ return self.sendData(text.encode("utf-8"), destinationId, portNum=portnums_pb2.PortNum.TEXT_MESSAGE_APP, @@ -1562,54 +1640,55 @@ wantResponse – True if you want the service on the other side to send an a
def showNodes(self, includeSelf=True, file=sys.stdout):
     """Show table summary of nodes in mesh"""
     def formatFloat(value, precision=2, unit=''):
+        """Format a float value with precsion."""
         return f'{value:.{precision}f}{unit}' if value else None
 
     def getLH(ts):
+        """Format last heard"""
         return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') if ts else None
 
     def getTimeAgo(ts):
+        """Format how long ago have we heard from this node (aka timeago)."""
         return timeago.format(datetime.fromtimestamp(ts), datetime.now()) if ts else None
 
     rows = []
-    for node in self.nodes.values():
-        if not includeSelf and node['num'] == self.localNode.nodeNum:
-            continue
+    if self.nodes:
+        for node in self.nodes.values():
+            if not includeSelf and node['num'] == self.localNode.nodeNum:
+                continue
 
-        row = {"N": 0}
+            row = {"N": 0}
+
+            user = node.get('user')
+            if user:
+                row.update({
+                    "User": user['longName'],
+                    "AKA":  user['shortName'],
+                    "ID":   user['id'],
+                })
+
+            pos = node.get('position')
+            if pos:
+                row.update({
+                    "Latitude":  formatFloat(pos.get("latitude"),     4, "°"),
+                    "Longitude": formatFloat(pos.get("longitude"),    4, "°"),
+                    "Altitude":  formatFloat(pos.get("altitude"),     0, " m"),
+                    "Battery":   formatFloat(pos.get("batteryLevel"), 2, "%"),
+                })
 
-        user = node.get('user')
-        if user:
             row.update({
-                "User": user['longName'],
-                "AKA":  user['shortName'],
-                "ID":   user['id'],
+                "SNR":       formatFloat(node.get("snr"), 2, " dB"),
+                "LastHeard": getLH(node.get("lastHeard")),
+                "Since":     getTimeAgo(node.get("lastHeard")),
             })
 
-        pos = node.get('position')
-        if pos:
-            row.update({
-                "Latitude":  formatFloat(pos.get("latitude"),     4, "°"),
-                "Longitude": formatFloat(pos.get("longitude"),    4, "°"),
-                "Altitude":  formatFloat(pos.get("altitude"),     0, " m"),
-                "Battery":   formatFloat(pos.get("batteryLevel"), 2, "%"),
-            })
+            rows.append(row)
 
-        row.update({
-            "SNR":       formatFloat(node.get("snr"), 2, " dB"),
-            "LastHeard": getLH(node.get("lastHeard")),
-            "Since":     getTimeAgo(node.get("lastHeard")),
-        })
-
-        rows.append(row)
-
-    # Why doesn't this way work?
-    #rows.sort(key=lambda r: r.get('LastHeard', '0000'), reverse=True)
     rows.sort(key=lambda r: r.get('LastHeard') or '0000', reverse=True)
     for i, row in enumerate(rows):
         row['N'] = i+1
 
-    table = tabulate(rows, headers='keys', missingval='N/A',
-                   tablefmt='fancy_grid')
+    table = tabulate(rows, headers='keys', missingval='N/A', tablefmt='fancy_grid')
     print(table)
     return table
@@ -1625,8 +1704,7 @@ wantResponse – True if you want the service on the other side to send an a
def waitForConfig(self):
     """Block until radio config is received. Returns True if config has been received."""
-    success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes')
-                                       ) and self.localNode.waitForConfig()
+    success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes')) and self.localNode.waitForConfig()
     if not success:
         raise Exception("Timed out waiting for interface config")
diff --git a/docs/meshtastic/node.html b/docs/meshtastic/node.html index 64e0729..576b49b 100644 --- a/docs/meshtastic/node.html +++ b/docs/meshtastic/node.html @@ -27,14 +27,14 @@ Expand source code -
""" Node class
+
"""Node class
 """
 
 import logging
 import base64
 from google.protobuf.json_format import MessageToJson
 from . import portnums_pb2, apponly_pb2, admin_pb2, channel_pb2
-from .util import pskToString, stripnl, Timeout, our_exit
+from .util import pskToString, stripnl, Timeout, our_exit, fromPSK
 
 
 class Node:
@@ -50,14 +50,16 @@ class Node:
         self.radioConfig = None
         self.channels = None
         self._timeout = Timeout(maxSecs=60)
+        self.partialChannels = None
 
     def showChannels(self):
-        """Show human readable description of our channels"""
+        """Show human readable description of our channels."""
         print("Channels:")
         if self.channels:
             for c in self.channels:
-                if c.role != channel_pb2.Channel.Role.DISABLED:
-                    cStr = stripnl(MessageToJson(c.settings))
+                cStr = stripnl(MessageToJson(c.settings))
+                # only show if there is no psk (meaning disabled channel)
+                if c.settings.psk:
                     print(f"  {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}")
         publicURL = self.getURL(includeAll=False)
         adminURL = self.getURL(includeAll=True)
@@ -74,15 +76,19 @@ class Node:
         self.showChannels()
 
     def requestConfig(self):
-        """
-        Send regular MeshPackets to ask for settings and channels
-        """
+        """Send regular MeshPackets to ask for settings and channels."""
         self.radioConfig = None
         self.channels = None
         self.partialChannels = []  # We keep our channels in a temp array until finished
 
         self._requestSettings()
 
+    def turnOffEncryptionOnPrimaryChannel(self):
+        """Turn off encryption on primary channel."""
+        self.channels[0].settings.psk = fromPSK("none")
+        print("Writing modified channels to device")
+        self.writeChannel(0)
+
     def waitForConfig(self):
         """Block until radio config is received. Returns True if config has been received."""
         return self._timeout.waitForSet(self, attrs=('radioConfig', 'channels'))
@@ -110,7 +116,7 @@ class Node:
     def deleteChannel(self, channelIndex):
         """Delete the specifed channelIndex and shift other channels up"""
         ch = self.channels[channelIndex]
-        if ch.role != channel_pb2.Channel.Role.SECONDARY:
+        if ch.role not in (channel_pb2.Channel.Role.SECONDARY, channel_pb2.Channel.Role.DISABLED):
             our_exit("Warning: Only SECONDARY channels can be deleted")
 
         # we are careful here because if we move the "admin" channel the channelIndex we need to use
@@ -125,9 +131,11 @@ class Node:
             self.writeChannel(index, adminIndex=adminIndex)
             index += 1
 
-            # if we are updating the local node, we might end up *moving* the admin channel index as we are writing
+            # if we are updating the local node, we might end up
+            # *moving* the admin channel index as we are writing
             if (self.iface.localNode == self) and index >= adminIndex:
-                # We've now passed the old location for admin index (and writen it), so we can start finding it by name again
+                # We've now passed the old location for admin index
+                # (and writen it), so we can start finding it by name again
                 adminIndex = 0
 
     def getChannelByName(self, name):
@@ -186,16 +194,15 @@ class Node:
         return self._sendAdmin(p)
 
     def getURL(self, includeAll: bool = True):
-        """The sharable URL that describes the current channel
-        """
+        """The sharable URL that describes the current channel"""
         # Only keep the primary/secondary channels, assume primary is first
         channelSet = apponly_pb2.ChannelSet()
         if self.channels:
             for c in self.channels:
                 if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY):
                     channelSet.settings.append(c.settings)
-        bytes = channelSet.SerializeToString()
-        s = base64.urlsafe_b64encode(bytes).decode('ascii')
+        some_bytes = channelSet.SerializeToString()
+        s = base64.urlsafe_b64encode(some_bytes).decode('ascii')
         return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
 
     def setURL(self, url):
@@ -233,41 +240,40 @@ class Node:
             i = i + 1
 
     def _requestSettings(self):
-        """
-        Done with initial config messages, now send regular MeshPackets to ask for settings
-        """
+        """Done with initial config messages, now send regular
+           MeshPackets to ask for settings."""
         p = admin_pb2.AdminMessage()
         p.get_radio_request = True
 
         def onResponse(p):
             """A closure to handle the response packet"""
-            self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
-            logging.debug("Received radio config, now fetching channels...")
-            self._timeout.reset()  # We made foreward progress
-            self._requestChannel(0)  # now start fetching channels
+            errorFound = False
+            if 'routing' in p["decoded"]:
+                if p["decoded"]["routing"]["errorReason"] != "NONE":
+                    errorFound = True
+                    print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}')
+            if errorFound is False:
+                self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
+                logging.debug("Received radio config, now fetching channels...")
+                self._timeout.reset()  # We made foreward progress
+                self._requestChannel(0)  # now start fetching channels
 
         # Show progress message for super slow operations
         if self != self.iface.localNode:
-            logging.info(
-                "Requesting preferences from remote node (this could take a while)")
+            print("Requesting preferences from remote node (this could take a while)")
 
-        return self._sendAdmin(p,
-                               wantResponse=True,
-                               onResponse=onResponse)
+        return self._sendAdmin(p, wantResponse=True, onResponse=onResponse)
 
     def exitSimulator(self):
-        """
-        Tell a simulator node to exit (this message is ignored for other nodes)
-        """
+        """Tell a simulator node to exit (this message
+           is ignored for other nodes)"""
         p = admin_pb2.AdminMessage()
         p.exit_simulator = True
 
         return self._sendAdmin(p)
 
     def reboot(self, secs: int = 10):
-        """
-        Tell the node to reboot
-        """
+        """Tell the node to reboot."""
         p = admin_pb2.AdminMessage()
         p.reboot_seconds = secs
         logging.info(f"Telling node to reboot in {secs} seconds")
@@ -278,6 +284,7 @@ class Node:
         """Fixup indexes and add disabled channels as needed"""
 
         # Add extra disabled channels as needed
+        # TODO: These 2 lines seem to not do anything.
         for index, ch in enumerate(self.channels):
             ch.index = index  # fixup indexes
 
@@ -296,21 +303,19 @@ class Node:
             index += 1
 
     def _requestChannel(self, channelNum: int):
-        """
-        Done with initial config messages, now send regular MeshPackets to ask for settings
-        """
+        """Done with initial config messages, now send regular
+           MeshPackets to ask for settings"""
         p = admin_pb2.AdminMessage()
         p.get_channel_request = channelNum + 1
 
         # Show progress message for super slow operations
         if self != self.iface.localNode:
-            logging.info(
-                f"Requesting channel {channelNum} info from remote node (this could take a while)")
+            logging.info(f"Requesting channel {channelNum} info from remote node (this could take a while)")
         else:
             logging.debug(f"Requesting channel {channelNum}")
 
         def onResponse(p):
-            """A closure to handle the response packet"""
+            """A closure to handle the response packet for requesting a channel"""
             c = p["decoded"]["admin"]["raw"].get_channel_response
             self.partialChannels.append(c)
             self._timeout.reset()  # We made foreward progress
@@ -320,9 +325,9 @@ class Node:
             # for stress testing, we can always download all channels
             fastChannelDownload = True
 
-            # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
-            quitEarly = (
-                c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
+            # Once we see a response that has NO settings, assume
+            # we are at the end of channels and stop fetching
+            quitEarly = (c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
 
             if quitEarly or index >= self.iface.myInfo.max_channels - 1:
                 logging.debug("Finished downloading channels")
@@ -335,13 +340,10 @@ class Node:
             else:
                 self._requestChannel(index + 1)
 
-        return self._sendAdmin(p,
-                               wantResponse=True,
-                               onResponse=onResponse)
+        return self._sendAdmin(p, wantResponse=True, onResponse=onResponse)
 
     def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False,
-                   onResponse=None,
-                   adminIndex=0):
+                   onResponse=None, adminIndex=0):
         """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
 
         if adminIndex == 0:  # unless a special channel index was used, we want to use the admin index
@@ -389,14 +391,16 @@ class Node:
         self.radioConfig = None
         self.channels = None
         self._timeout = Timeout(maxSecs=60)
+        self.partialChannels = None
 
     def showChannels(self):
-        """Show human readable description of our channels"""
+        """Show human readable description of our channels."""
         print("Channels:")
         if self.channels:
             for c in self.channels:
-                if c.role != channel_pb2.Channel.Role.DISABLED:
-                    cStr = stripnl(MessageToJson(c.settings))
+                cStr = stripnl(MessageToJson(c.settings))
+                # only show if there is no psk (meaning disabled channel)
+                if c.settings.psk:
                     print(f"  {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}")
         publicURL = self.getURL(includeAll=False)
         adminURL = self.getURL(includeAll=True)
@@ -413,15 +417,19 @@ class Node:
         self.showChannels()
 
     def requestConfig(self):
-        """
-        Send regular MeshPackets to ask for settings and channels
-        """
+        """Send regular MeshPackets to ask for settings and channels."""
         self.radioConfig = None
         self.channels = None
         self.partialChannels = []  # We keep our channels in a temp array until finished
 
         self._requestSettings()
 
+    def turnOffEncryptionOnPrimaryChannel(self):
+        """Turn off encryption on primary channel."""
+        self.channels[0].settings.psk = fromPSK("none")
+        print("Writing modified channels to device")
+        self.writeChannel(0)
+
     def waitForConfig(self):
         """Block until radio config is received. Returns True if config has been received."""
         return self._timeout.waitForSet(self, attrs=('radioConfig', 'channels'))
@@ -449,7 +457,7 @@ class Node:
     def deleteChannel(self, channelIndex):
         """Delete the specifed channelIndex and shift other channels up"""
         ch = self.channels[channelIndex]
-        if ch.role != channel_pb2.Channel.Role.SECONDARY:
+        if ch.role not in (channel_pb2.Channel.Role.SECONDARY, channel_pb2.Channel.Role.DISABLED):
             our_exit("Warning: Only SECONDARY channels can be deleted")
 
         # we are careful here because if we move the "admin" channel the channelIndex we need to use
@@ -464,9 +472,11 @@ class Node:
             self.writeChannel(index, adminIndex=adminIndex)
             index += 1
 
-            # if we are updating the local node, we might end up *moving* the admin channel index as we are writing
+            # if we are updating the local node, we might end up
+            # *moving* the admin channel index as we are writing
             if (self.iface.localNode == self) and index >= adminIndex:
-                # We've now passed the old location for admin index (and writen it), so we can start finding it by name again
+                # We've now passed the old location for admin index
+                # (and writen it), so we can start finding it by name again
                 adminIndex = 0
 
     def getChannelByName(self, name):
@@ -525,16 +535,15 @@ class Node:
         return self._sendAdmin(p)
 
     def getURL(self, includeAll: bool = True):
-        """The sharable URL that describes the current channel
-        """
+        """The sharable URL that describes the current channel"""
         # Only keep the primary/secondary channels, assume primary is first
         channelSet = apponly_pb2.ChannelSet()
         if self.channels:
             for c in self.channels:
                 if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY):
                     channelSet.settings.append(c.settings)
-        bytes = channelSet.SerializeToString()
-        s = base64.urlsafe_b64encode(bytes).decode('ascii')
+        some_bytes = channelSet.SerializeToString()
+        s = base64.urlsafe_b64encode(some_bytes).decode('ascii')
         return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
 
     def setURL(self, url):
@@ -572,41 +581,40 @@ class Node:
             i = i + 1
 
     def _requestSettings(self):
-        """
-        Done with initial config messages, now send regular MeshPackets to ask for settings
-        """
+        """Done with initial config messages, now send regular
+           MeshPackets to ask for settings."""
         p = admin_pb2.AdminMessage()
         p.get_radio_request = True
 
         def onResponse(p):
             """A closure to handle the response packet"""
-            self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
-            logging.debug("Received radio config, now fetching channels...")
-            self._timeout.reset()  # We made foreward progress
-            self._requestChannel(0)  # now start fetching channels
+            errorFound = False
+            if 'routing' in p["decoded"]:
+                if p["decoded"]["routing"]["errorReason"] != "NONE":
+                    errorFound = True
+                    print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}')
+            if errorFound is False:
+                self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
+                logging.debug("Received radio config, now fetching channels...")
+                self._timeout.reset()  # We made foreward progress
+                self._requestChannel(0)  # now start fetching channels
 
         # Show progress message for super slow operations
         if self != self.iface.localNode:
-            logging.info(
-                "Requesting preferences from remote node (this could take a while)")
+            print("Requesting preferences from remote node (this could take a while)")
 
-        return self._sendAdmin(p,
-                               wantResponse=True,
-                               onResponse=onResponse)
+        return self._sendAdmin(p, wantResponse=True, onResponse=onResponse)
 
     def exitSimulator(self):
-        """
-        Tell a simulator node to exit (this message is ignored for other nodes)
-        """
+        """Tell a simulator node to exit (this message
+           is ignored for other nodes)"""
         p = admin_pb2.AdminMessage()
         p.exit_simulator = True
 
         return self._sendAdmin(p)
 
     def reboot(self, secs: int = 10):
-        """
-        Tell the node to reboot
-        """
+        """Tell the node to reboot."""
         p = admin_pb2.AdminMessage()
         p.reboot_seconds = secs
         logging.info(f"Telling node to reboot in {secs} seconds")
@@ -617,6 +625,7 @@ class Node:
         """Fixup indexes and add disabled channels as needed"""
 
         # Add extra disabled channels as needed
+        # TODO: These 2 lines seem to not do anything.
         for index, ch in enumerate(self.channels):
             ch.index = index  # fixup indexes
 
@@ -635,21 +644,19 @@ class Node:
             index += 1
 
     def _requestChannel(self, channelNum: int):
-        """
-        Done with initial config messages, now send regular MeshPackets to ask for settings
-        """
+        """Done with initial config messages, now send regular
+           MeshPackets to ask for settings"""
         p = admin_pb2.AdminMessage()
         p.get_channel_request = channelNum + 1
 
         # Show progress message for super slow operations
         if self != self.iface.localNode:
-            logging.info(
-                f"Requesting channel {channelNum} info from remote node (this could take a while)")
+            logging.info(f"Requesting channel {channelNum} info from remote node (this could take a while)")
         else:
             logging.debug(f"Requesting channel {channelNum}")
 
         def onResponse(p):
-            """A closure to handle the response packet"""
+            """A closure to handle the response packet for requesting a channel"""
             c = p["decoded"]["admin"]["raw"].get_channel_response
             self.partialChannels.append(c)
             self._timeout.reset()  # We made foreward progress
@@ -659,9 +666,9 @@ class Node:
             # for stress testing, we can always download all channels
             fastChannelDownload = True
 
-            # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
-            quitEarly = (
-                c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
+            # Once we see a response that has NO settings, assume
+            # we are at the end of channels and stop fetching
+            quitEarly = (c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
 
             if quitEarly or index >= self.iface.myInfo.max_channels - 1:
                 logging.debug("Finished downloading channels")
@@ -674,13 +681,10 @@ class Node:
             else:
                 self._requestChannel(index + 1)
 
-        return self._sendAdmin(p,
-                               wantResponse=True,
-                               onResponse=onResponse)
+        return self._sendAdmin(p, wantResponse=True, onResponse=onResponse)
 
     def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False,
-                   onResponse=None,
-                   adminIndex=0):
+                   onResponse=None, adminIndex=0):
         """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
 
         if adminIndex == 0:  # unless a special channel index was used, we want to use the admin index
@@ -707,7 +711,7 @@ class Node:
 
def deleteChannel(self, channelIndex):
     """Delete the specifed channelIndex and shift other channels up"""
     ch = self.channels[channelIndex]
-    if ch.role != channel_pb2.Channel.Role.SECONDARY:
+    if ch.role not in (channel_pb2.Channel.Role.SECONDARY, channel_pb2.Channel.Role.DISABLED):
         our_exit("Warning: Only SECONDARY channels can be deleted")
 
     # we are careful here because if we move the "admin" channel the channelIndex we need to use
@@ -722,9 +726,11 @@ class Node:
         self.writeChannel(index, adminIndex=adminIndex)
         index += 1
 
-        # if we are updating the local node, we might end up *moving* the admin channel index as we are writing
+        # if we are updating the local node, we might end up
+        # *moving* the admin channel index as we are writing
         if (self.iface.localNode == self) and index >= adminIndex:
-            # We've now passed the old location for admin index (and writen it), so we can start finding it by name again
+            # We've now passed the old location for admin index
+            # (and writen it), so we can start finding it by name again
             adminIndex = 0
@@ -732,15 +738,15 @@ class Node: def exitSimulator(self)
-

Tell a simulator node to exit (this message is ignored for other nodes)

+

Tell a simulator node to exit (this message +is ignored for other nodes)

Expand source code
def exitSimulator(self):
-    """
-    Tell a simulator node to exit (this message is ignored for other nodes)
-    """
+    """Tell a simulator node to exit (this message
+       is ignored for other nodes)"""
     p = admin_pb2.AdminMessage()
     p.exit_simulator = True
 
@@ -791,16 +797,15 @@ class Node:
 Expand source code
 
 
def getURL(self, includeAll: bool = True):
-    """The sharable URL that describes the current channel
-    """
+    """The sharable URL that describes the current channel"""
     # Only keep the primary/secondary channels, assume primary is first
     channelSet = apponly_pb2.ChannelSet()
     if self.channels:
         for c in self.channels:
             if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY):
                 channelSet.settings.append(c.settings)
-    bytes = channelSet.SerializeToString()
-    s = base64.urlsafe_b64encode(bytes).decode('ascii')
+    some_bytes = channelSet.SerializeToString()
+    s = base64.urlsafe_b64encode(some_bytes).decode('ascii')
     return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
@@ -808,15 +813,13 @@ class Node: def reboot(self, secs: int = 10)
-

Tell the node to reboot

+

Tell the node to reboot.

Expand source code
def reboot(self, secs: int = 10):
-    """
-    Tell the node to reboot
-    """
+    """Tell the node to reboot."""
     p = admin_pb2.AdminMessage()
     p.reboot_seconds = secs
     logging.info(f"Telling node to reboot in {secs} seconds")
@@ -828,15 +831,13 @@ class Node:
 def requestConfig(self)
 
 
-

Send regular MeshPackets to ask for settings and channels

+

Send regular MeshPackets to ask for settings and channels.

Expand source code
def requestConfig(self):
-    """
-    Send regular MeshPackets to ask for settings and channels
-    """
+    """Send regular MeshPackets to ask for settings and channels."""
     self.radioConfig = None
     self.channels = None
     self.partialChannels = []  # We keep our channels in a temp array until finished
@@ -935,18 +936,19 @@ class Node:
 def showChannels(self)
 
 
-

Show human readable description of our channels

+

Show human readable description of our channels.

Expand source code
def showChannels(self):
-    """Show human readable description of our channels"""
+    """Show human readable description of our channels."""
     print("Channels:")
     if self.channels:
         for c in self.channels:
-            if c.role != channel_pb2.Channel.Role.DISABLED:
-                cStr = stripnl(MessageToJson(c.settings))
+            cStr = stripnl(MessageToJson(c.settings))
+            # only show if there is no psk (meaning disabled channel)
+            if c.settings.psk:
                 print(f"  {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}")
     publicURL = self.getURL(includeAll=False)
     adminURL = self.getURL(includeAll=True)
@@ -973,6 +975,22 @@ class Node:
     self.showChannels()
+
+def turnOffEncryptionOnPrimaryChannel(self) +
+
+

Turn off encryption on primary channel.

+
+ +Expand source code + +
def turnOffEncryptionOnPrimaryChannel(self):
+    """Turn off encryption on primary channel."""
+    self.channels[0].settings.psk = fromPSK("none")
+    print("Writing modified channels to device")
+    self.writeChannel(0)
+
+
def waitForConfig(self)
@@ -1047,7 +1065,7 @@ class Node:
  • Node

    -
      +
      • deleteChannel
      • exitSimulator
      • getChannelByName
      • @@ -1059,6 +1077,7 @@ class Node:
      • setURL
      • showChannels
      • showInfo
      • +
      • turnOffEncryptionOnPrimaryChannel
      • waitForConfig
      • writeChannel
      • writeConfig
      • diff --git a/docs/meshtastic/remote_hardware.html b/docs/meshtastic/remote_hardware.html index 11f4637..eae2177 100644 --- a/docs/meshtastic/remote_hardware.html +++ b/docs/meshtastic/remote_hardware.html @@ -67,9 +67,8 @@ class RemoteHardwareClient: def _sendHardware(self, nodeid, r, wantResponse=False, onResponse=None): if not nodeid: - # pylint: disable=W1401 raise Exception( - "You must set a destination node ID for this operation (use --dest \!xxxxxxxxx)") + r"You must set a destination node ID for this operation (use --dest \!xxxxxxxxx)") return self.iface.sendData(r, nodeid, portnums_pb2.REMOTE_HARDWARE_APP, wantAck=True, channelIndex=self.channelIndex, wantResponse=wantResponse, onResponse=onResponse) @@ -170,9 +169,8 @@ code for how you can connect to your own custom meshtastic services

        def _sendHardware(self, nodeid, r, wantResponse=False, onResponse=None): if not nodeid: - # pylint: disable=W1401 raise Exception( - "You must set a destination node ID for this operation (use --dest \!xxxxxxxxx)") + r"You must set a destination node ID for this operation (use --dest \!xxxxxxxxx)") return self.iface.sendData(r, nodeid, portnums_pb2.REMOTE_HARDWARE_APP, wantAck=True, channelIndex=self.channelIndex, wantResponse=wantResponse, onResponse=onResponse) diff --git a/docs/meshtastic/serial_interface.html b/docs/meshtastic/serial_interface.html index 9c08644..8f7ef3e 100644 --- a/docs/meshtastic/serial_interface.html +++ b/docs/meshtastic/serial_interface.html @@ -35,8 +35,8 @@ import os import stat import serial +import meshtastic.util from .stream_interface import StreamInterface -from .util import findPorts, our_exit class SerialInterface(StreamInterface): """Interface class for meshtastic devices over a serial link""" @@ -51,14 +51,14 @@ class SerialInterface(StreamInterface): """ if devPath is None: - ports = findPorts() + ports = meshtastic.util.findPorts() logging.debug(f"ports:{ports}") if len(ports) == 0: - our_exit("Warning: No Meshtastic devices detected.") + meshtastic.util.our_exit("Warning: No Meshtastic devices detected.") elif len(ports) > 1: message = "Warning: Multiple serial ports were detected so one serial port must be specified with the '--port'.\n" message += f" Ports detected:{ports}" - our_exit(message) + meshtastic.util.our_exit(message) else: devPath = ports[0] @@ -143,14 +143,14 @@ debugOut {stream} – If a stream is provided, any debug serial output from """ if devPath is None: - ports = findPorts() + ports = meshtastic.util.findPorts() logging.debug(f"ports:{ports}") if len(ports) == 0: - our_exit("Warning: No Meshtastic devices detected.") + meshtastic.util.our_exit("Warning: No Meshtastic devices detected.") elif len(ports) > 1: message = "Warning: Multiple serial ports were detected so one serial port must be specified with the '--port'.\n" message += f" Ports detected:{ports}" - our_exit(message) + meshtastic.util.our_exit(message) else: devPath = ports[0] diff --git a/docs/meshtastic/stream_interface.html b/docs/meshtastic/stream_interface.html index e466147..ff41f1c 100644 --- a/docs/meshtastic/stream_interface.html +++ b/docs/meshtastic/stream_interface.html @@ -27,7 +27,7 @@ Expand source code -
        """ Stream Interface base class
        +
        """Stream Interface base class
         """
         import logging
         import threading
        @@ -54,7 +54,8 @@ class StreamInterface(MeshInterface):
         
                 Keyword Arguments:
                     devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None})
        -            debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})
        +            debugOut {stream} -- If a stream is provided, any debug serial output from the
        +                                 device will be emitted to that stream. (default: {None})
         
                 Raises:
                     Exception: [description]
        @@ -82,12 +83,14 @@ class StreamInterface(MeshInterface):
             def connect(self):
                 """Connect to our radio
         
        -        Normally this is called automatically by the constructor, but if you passed in connectNow=False you can manually
        -        start the reading thread later.
        +        Normally this is called automatically by the constructor, but if you
        +        passed in connectNow=False you can manually start the reading thread later.
                 """
         
        -        # Send some bogus UART characters to force a sleeping device to wake, and if the reading statemachine was parsing a bad packet make sure
        -        # we write enought start bytes to force it to resync (we don't use START1 because we want to ensure it is looking for START1)
        +        # Send some bogus UART characters to force a sleeping device to wake, and
        +        # if the reading statemachine was parsing a bad packet make sure
        +        # we write enought start bytes to force it to resync (we don't use START1
        +        # because we want to ensure it is looking for START1)
                 p = bytearray([START2] * 32)
                 self._writeBytes(p)
                 time.sleep(0.1)  # wait 100ms to give device time to start running
        @@ -104,8 +107,11 @@ class StreamInterface(MeshInterface):
                 MeshInterface._disconnected(self)
         
                 logging.debug("Closing our port")
        +        # pylint: disable=E0203
                 if not self.stream is None:
        +            # pylint: disable=E0203
                     self.stream.close()
        +            # pylint: disable=W0201
                     self.stream = None
         
             def _writeBytes(self, b):
        @@ -131,7 +137,8 @@ class StreamInterface(MeshInterface):
                 """Close a connection to the device"""
                 logging.debug("Closing stream")
                 MeshInterface.close(self)
        -        # pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
        +        # pyserial cancel_read doesn't seem to work, therefore we ask the
        +        # reader thread to close things for us
                 self._wantExit = True
                 if self._rxThread != threading.current_thread():
                     self._rxThread.join()  # wait for it to exit
        @@ -177,8 +184,7 @@ class StreamInterface(MeshInterface):
                                     try:
                                         self._handleFromRadio(self._rxBuf[HEADER_LEN:])
                                     except Exception as ex:
        -                                logging.error(
        -                                    f"Error while handling message from radio {ex}")
        +                                logging.error(f"Error while handling message from radio {ex}")
                                         traceback.print_exc()
                                     self._rxBuf = empty
                         else:
        @@ -186,15 +192,12 @@ class StreamInterface(MeshInterface):
                             pass
                 except serial.SerialException as ex:
                     if not self._wantExit:  # We might intentionally get an exception during shutdown
        -                logging.warning(
        -                    f"Meshtastic serial port disconnected, disconnecting... {ex}")
        +                logging.warning(f"Meshtastic serial port disconnected, disconnecting... {ex}")
                 except OSError as ex:
                     if not self._wantExit:  # We might intentionally get an exception during shutdown
        -                logging.error(
        -                    f"Unexpected OSError, terminating meshtastic reader... {ex}")
        +                logging.error(f"Unexpected OSError, terminating meshtastic reader... {ex}")
                 except Exception as ex:
        -            logging.error(
        -                f"Unexpected exception, terminating meshtastic reader... {ex}")
        +            logging.error(f"Unexpected exception, terminating meshtastic reader... {ex}")
                 finally:
                     logging.debug("reader is exiting")
                     self._disconnected()
        @@ -218,7 +221,8 @@ class StreamInterface(MeshInterface):

        Constructor, opens a connection to self.stream

        Keyword Arguments: devPath {string} – A filepath to a device, i.e. /dev/ttyUSB0 (default: {None}) -debugOut {stream} – If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})

        +debugOut {stream} – If a stream is provided, any debug serial output from the +device will be emitted to that stream. (default: {None})

        Raises

        Exception
        @@ -238,7 +242,8 @@ debugOut {stream} – If a stream is provided, any debug serial output from Keyword Arguments: devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None}) - debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None}) + debugOut {stream} -- If a stream is provided, any debug serial output from the + device will be emitted to that stream. (default: {None}) Raises: Exception: [description] @@ -266,12 +271,14 @@ debugOut {stream} – If a stream is provided, any debug serial output from def connect(self): """Connect to our radio - Normally this is called automatically by the constructor, but if you passed in connectNow=False you can manually - start the reading thread later. + Normally this is called automatically by the constructor, but if you + passed in connectNow=False you can manually start the reading thread later. """ - # Send some bogus UART characters to force a sleeping device to wake, and if the reading statemachine was parsing a bad packet make sure - # we write enought start bytes to force it to resync (we don't use START1 because we want to ensure it is looking for START1) + # Send some bogus UART characters to force a sleeping device to wake, and + # if the reading statemachine was parsing a bad packet make sure + # we write enought start bytes to force it to resync (we don't use START1 + # because we want to ensure it is looking for START1) p = bytearray([START2] * 32) self._writeBytes(p) time.sleep(0.1) # wait 100ms to give device time to start running @@ -288,8 +295,11 @@ debugOut {stream} – If a stream is provided, any debug serial output from MeshInterface._disconnected(self) logging.debug("Closing our port") + # pylint: disable=E0203 if not self.stream is None: + # pylint: disable=E0203 self.stream.close() + # pylint: disable=W0201 self.stream = None def _writeBytes(self, b): @@ -315,7 +325,8 @@ debugOut {stream} – If a stream is provided, any debug serial output from """Close a connection to the device""" logging.debug("Closing stream") MeshInterface.close(self) - # pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us + # pyserial cancel_read doesn't seem to work, therefore we ask the + # reader thread to close things for us self._wantExit = True if self._rxThread != threading.current_thread(): self._rxThread.join() # wait for it to exit @@ -361,8 +372,7 @@ debugOut {stream} – If a stream is provided, any debug serial output from try: self._handleFromRadio(self._rxBuf[HEADER_LEN:]) except Exception as ex: - logging.error( - f"Error while handling message from radio {ex}") + logging.error(f"Error while handling message from radio {ex}") traceback.print_exc() self._rxBuf = empty else: @@ -370,15 +380,12 @@ debugOut {stream} – If a stream is provided, any debug serial output from pass except serial.SerialException as ex: if not self._wantExit: # We might intentionally get an exception during shutdown - logging.warning( - f"Meshtastic serial port disconnected, disconnecting... {ex}") + logging.warning(f"Meshtastic serial port disconnected, disconnecting... {ex}") except OSError as ex: if not self._wantExit: # We might intentionally get an exception during shutdown - logging.error( - f"Unexpected OSError, terminating meshtastic reader... {ex}") + logging.error(f"Unexpected OSError, terminating meshtastic reader... {ex}") except Exception as ex: - logging.error( - f"Unexpected exception, terminating meshtastic reader... {ex}") + logging.error(f"Unexpected exception, terminating meshtastic reader... {ex}") finally: logging.debug("reader is exiting") self._disconnected()
        @@ -407,7 +414,8 @@ debugOut {stream} – If a stream is provided, any debug serial output from """Close a connection to the device""" logging.debug("Closing stream") MeshInterface.close(self) - # pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us + # pyserial cancel_read doesn't seem to work, therefore we ask the + # reader thread to close things for us self._wantExit = True if self._rxThread != threading.current_thread(): self._rxThread.join() # wait for it to exit
@@ -418,8 +426,8 @@ debugOut {stream} – If a stream is provided, any debug serial output from

Connect to our radio

-

Normally this is called automatically by the constructor, but if you passed in connectNow=False you can manually -start the reading thread later.

+

Normally this is called automatically by the constructor, but if you +passed in connectNow=False you can manually start the reading thread later.

Expand source code @@ -427,12 +435,14 @@ start the reading thread later.

def connect(self):
     """Connect to our radio
 
-    Normally this is called automatically by the constructor, but if you passed in connectNow=False you can manually
-    start the reading thread later.
+    Normally this is called automatically by the constructor, but if you
+    passed in connectNow=False you can manually start the reading thread later.
     """
 
-    # Send some bogus UART characters to force a sleeping device to wake, and if the reading statemachine was parsing a bad packet make sure
-    # we write enought start bytes to force it to resync (we don't use START1 because we want to ensure it is looking for START1)
+    # Send some bogus UART characters to force a sleeping device to wake, and
+    # if the reading statemachine was parsing a bad packet make sure
+    # we write enought start bytes to force it to resync (we don't use START1
+    # because we want to ensure it is looking for START1)
     p = bytearray([START2] * 32)
     self._writeBytes(p)
     time.sleep(0.1)  # wait 100ms to give device time to start running
diff --git a/docs/meshtastic/tcp_interface.html b/docs/meshtastic/tcp_interface.html
index 748998c..60fffbf 100644
--- a/docs/meshtastic/tcp_interface.html
+++ b/docs/meshtastic/tcp_interface.html
@@ -27,7 +27,7 @@
 
 Expand source code
 
-
""" TCPInterface class for interfacing with http endpoint
+
"""TCPInterface class for interfacing with http endpoint
 """
 import logging
 import socket
@@ -38,7 +38,8 @@ from .stream_interface import StreamInterface
 class TCPInterface(StreamInterface):
     """Interface class for meshtastic devices over a TCP link"""
 
-    def __init__(self, hostname: AnyStr, debugOut=None, noProto=False, connectNow=True, portNumber=4403):
+    def __init__(self, hostname: AnyStr, debugOut=None, noProto=False,
+                 connectNow=True, portNumber=4403):
         """Constructor, opens a connection to a specified IP address/hostname
 
         Keyword Arguments:
@@ -62,8 +63,8 @@ class TCPInterface(StreamInterface):
         """Close a connection to the device"""
         logging.debug("Closing TCP stream")
         StreamInterface.close(self)
-        # Sometimes the socket read might be blocked in the reader thread.  Therefore we force the shutdown by closing
-        # the socket here
+        # Sometimes the socket read might be blocked in the reader thread.
+        # Therefore we force the shutdown by closing the socket here
         self._wantExit = True
         if not self.socket is None:
             try:
@@ -106,7 +107,8 @@ hostname {string} – Hostname/IP address of the device to connect to

class TCPInterface(StreamInterface): """Interface class for meshtastic devices over a TCP link""" - def __init__(self, hostname: AnyStr, debugOut=None, noProto=False, connectNow=True, portNumber=4403): + def __init__(self, hostname: AnyStr, debugOut=None, noProto=False, + connectNow=True, portNumber=4403): """Constructor, opens a connection to a specified IP address/hostname Keyword Arguments: @@ -130,8 +132,8 @@ hostname {string} – Hostname/IP address of the device to connect to

meshtastic.test API documentation - + @@ -22,12 +23,14 @@

Module meshtastic.test

-

Testing

+

With two radios connected serially, send and receive test +messages and report back if successful.

Expand source code -
""" Testing
+
"""With two radios connected serially, send and receive test
+   messages and report back if successful.
 """
 import logging
 import time
@@ -35,8 +38,11 @@ import sys
 import traceback
 from dotmap import DotMap
 from pubsub import pub
-from . import util
-from . import SerialInterface, TCPInterface, BROADCAST_NUM
+import meshtastic.util
+from .__init__ import BROADCAST_NUM
+from .serial_interface import SerialInterface
+from .tcp_interface import TCPInterface
+
 
 """The interfaces we are using for our tests"""
 interfaces = None
@@ -134,25 +140,23 @@ def runTests(numTests=50, wantAck=False, maxFailures=0):
             logging.info(
                 f"Test {testNumber} succeeded {numSuccess} successes {numFail} failures so far")
 
-        # if numFail >= 3:
-        #    for i in interfaces:
-        #        i.close()
-        #    return
-
         time.sleep(1)
 
     if numFail > maxFailures:
         logging.error("Too many failures! Test failed!")
-
-    return numFail
+        return False
+    return True
 
 
 def testThread(numTests=50):
     """Test thread"""
     logging.info("Found devices, starting tests...")
-    runTests(numTests, wantAck=True)
-    # Allow a few dropped packets
-    runTests(numTests, wantAck=False, maxFailures=5)
+    result = runTests(numTests, wantAck=True)
+    if result:
+        # Run another test
+        # Allow a few dropped packets
+        result = runTests(numTests, wantAck=False, maxFailures=1)
+    return result
 
 
 def onConnection(topic=pub.AUTO_TOPIC):
@@ -164,19 +168,18 @@ def openDebugLog(portName):
     """Open the debug log file"""
     debugname = "log" + portName.replace("/", "_")
     logging.info(f"Writing serial debugging to {debugname}")
-    return open(debugname, 'w+', buffering=1)
+    return open(debugname, 'w+', buffering=1, encoding='utf8')
 
 
-def testAll():
+def testAll(numTests=5):
     """
     Run a series of tests using devices we can find.
+    This is called from the cli with the "--test" option.
 
-    Raises:
-        Exception: If not enough devices are found
     """
-    ports = util.findPorts()
+    ports = meshtastic.util.findPorts()
     if len(ports) < 2:
-        raise Exception("Must have at least two devices connected to USB")
+        meshtastic.util.our_exit("Warning: Must have at least two devices connected to USB.")
 
     pub.subscribe(onConnection, "meshtastic.connection")
     pub.subscribe(onReceive, "meshtastic.receive")
@@ -185,11 +188,13 @@ def testAll():
         port, debugOut=openDebugLog(port), connectNow=True), ports))
 
     logging.info("Ports opened, starting test")
-    testThread()
+    result = testThread(numTests)
 
     for i in interfaces:
         i.close()
 
+    return result
+
 
 def testSimulator():
     """
@@ -200,7 +205,7 @@ def testSimulator():
     Run with
     python3 -c 'from meshtastic.test import testSimulator; testSimulator()'
     """
-    logging.basicConfig(level=logging.DEBUG if False else logging.INFO)
+    logging.basicConfig(level=logging.DEBUG)
     logging.info("Connecting to simulator on localhost!")
     try:
         iface = TCPInterface("localhost")
@@ -295,7 +300,7 @@ def testSimulator():
     """Open the debug log file"""
     debugname = "log" + portName.replace("/", "_")
     logging.info(f"Writing serial debugging to {debugname}")
-    return open(debugname, 'w+', buffering=1)
+ return open(debugname, 'w+', buffering=1, encoding='utf8')
@@ -328,17 +333,12 @@ def testSimulator(): logging.info( f"Test {testNumber} succeeded {numSuccess} successes {numFail} failures so far") - # if numFail >= 3: - # for i in interfaces: - # i.close() - # return - time.sleep(1) if numFail > maxFailures: logging.error("Too many failures! Test failed!") - - return numFail
+ return False + return True
@@ -357,29 +357,24 @@ def testSimulator():
-def testAll() +def testAll(numTests=5)
-

Run a series of tests using devices we can find.

-

Raises

-
-
Exception
-
If not enough devices are found
-
+

Run a series of tests using devices we can find. +This is called from the cli with the "–test" option.

Expand source code -
def testAll():
+
def testAll(numTests=5):
     """
     Run a series of tests using devices we can find.
+    This is called from the cli with the "--test" option.
 
-    Raises:
-        Exception: If not enough devices are found
     """
-    ports = util.findPorts()
+    ports = meshtastic.util.findPorts()
     if len(ports) < 2:
-        raise Exception("Must have at least two devices connected to USB")
+        meshtastic.util.our_exit("Warning: Must have at least two devices connected to USB.")
 
     pub.subscribe(onConnection, "meshtastic.connection")
     pub.subscribe(onReceive, "meshtastic.receive")
@@ -388,10 +383,12 @@ def testSimulator():
         port, debugOut=openDebugLog(port), connectNow=True), ports))
 
     logging.info("Ports opened, starting test")
-    testThread()
+    result = testThread(numTests)
 
     for i in interfaces:
-        i.close()
+ i.close() + + return result
@@ -466,7 +463,7 @@ python3 -c 'from meshtastic.test import testSimulator; testSimulator()'

def testThread(numTests=50): """Test thread""" logging.info("Found devices, starting tests...") - runTests(numTests, wantAck=True) - # Allow a few dropped packets - runTests(numTests, wantAck=False, maxFailures=5) + result = runTests(numTests, wantAck=True) + if result: + # Run another test + # Allow a few dropped packets + result = runTests(numTests, wantAck=False, maxFailures=1) + return result
diff --git a/docs/meshtastic/tests/conftest.html b/docs/meshtastic/tests/conftest.html new file mode 100644 index 0000000..79bb8a0 --- /dev/null +++ b/docs/meshtastic/tests/conftest.html @@ -0,0 +1,100 @@ + + + + + + +meshtastic.tests.conftest API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.conftest

+
+
+

Common pytest code (place for fixtures).

+
+ +Expand source code + +
"""Common pytest code (place for fixtures)."""
+
+import argparse
+
+import pytest
+
+from meshtastic.__main__ import Globals
+
+@pytest.fixture
+def reset_globals():
+    """Fixture to reset globals."""
+    parser = None
+    parser = argparse.ArgumentParser()
+    Globals.getInstance().reset()
+    Globals.getInstance().set_parser(parser)
+
+
+
+
+
+
+
+

Functions

+
+
+def reset_globals() +
+
+

Fixture to reset globals.

+
+ +Expand source code + +
@pytest.fixture
+def reset_globals():
+    """Fixture to reset globals."""
+    parser = None
+    parser = argparse.ArgumentParser()
+    Globals.getInstance().reset()
+    Globals.getInstance().set_parser(parser)
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/index.html b/docs/meshtastic/tests/index.html new file mode 100644 index 0000000..b04ac7d --- /dev/null +++ b/docs/meshtastic/tests/index.html @@ -0,0 +1,130 @@ + + + + + + +meshtastic.tests API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests

+
+
+
+
+

Sub-modules

+
+
meshtastic.tests.conftest
+
+

Common pytest code (place for fixtures).

+
+
meshtastic.tests.test_ble_interface
+
+

Meshtastic unit tests for ble_interface.py

+
+
meshtastic.tests.test_globals
+
+

Meshtastic unit tests for globals.py

+
+
meshtastic.tests.test_int
+
+

Meshtastic integration tests

+
+
meshtastic.tests.test_main
+
+

Meshtastic unit tests for main.py

+
+
meshtastic.tests.test_mesh_interface
+
+

Meshtastic unit tests for mesh_interface.py

+
+
meshtastic.tests.test_node
+
+

Meshtastic unit tests for node.py

+
+
meshtastic.tests.test_serial_interface
+
+

Meshtastic unit tests for serial_interface.py

+
+
meshtastic.tests.test_smoke1
+
+

Meshtastic smoke tests with a single device via USB

+
+
meshtastic.tests.test_smoke2
+
+

Meshtastic smoke tests with 2 devices connected via USB

+
+
meshtastic.tests.test_smoke_wifi
+
+

Meshtastic smoke tests a device setup with wifi …

+
+
meshtastic.tests.test_stream_interface
+
+

Meshtastic unit tests for stream_interface.py

+
+
meshtastic.tests.test_tcp_interface
+
+

Meshtastic unit tests for tcp_interface.py

+
+
meshtastic.tests.test_util
+
+

Meshtastic unit tests for util.py

+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_ble_interface.html b/docs/meshtastic/tests/test_ble_interface.html new file mode 100644 index 0000000..ae87dd6 --- /dev/null +++ b/docs/meshtastic/tests/test_ble_interface.html @@ -0,0 +1,95 @@ + + + + + + +meshtastic.tests.test_ble_interface API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_ble_interface

+
+
+

Meshtastic unit tests for ble_interface.py

+
+ +Expand source code + +
"""Meshtastic unit tests for ble_interface.py"""
+
+
+import pytest
+
+from ..ble_interface import BLEInterface
+
+@pytest.mark.unit
+def test_BLEInterface():
+    """Test that we can instantiate a BLEInterface"""
+    iface = BLEInterface('foo', debugOut=True, noProto=True)
+    iface.close()
+
+
+
+
+
+
+
+

Functions

+
+
+def test_BLEInterface() +
+
+

Test that we can instantiate a BLEInterface

+
+ +Expand source code + +
@pytest.mark.unit
+def test_BLEInterface():
+    """Test that we can instantiate a BLEInterface"""
+    iface = BLEInterface('foo', debugOut=True, noProto=True)
+    iface.close()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_globals.html b/docs/meshtastic/tests/test_globals.html new file mode 100644 index 0000000..df7d4e9 --- /dev/null +++ b/docs/meshtastic/tests/test_globals.html @@ -0,0 +1,130 @@ + + + + + + +meshtastic.tests.test_globals API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_globals

+
+
+

Meshtastic unit tests for globals.py

+
+ +Expand source code + +
"""Meshtastic unit tests for globals.py
+"""
+
+import pytest
+
+from ..globals import Globals
+
+
+@pytest.mark.unit
+def test_globals_get_instaance():
+    """Test that we can instantiate a Globals instance"""
+    ourglobals = Globals.getInstance()
+    ourglobals2 = Globals.getInstance()
+    assert ourglobals == ourglobals2
+
+
+@pytest.mark.unit
+def test_globals_there_can_be_only_one():
+    """Test that we can cannot create two Globals instances"""
+    # if we have an instance, delete it
+    Globals.getInstance()
+    with pytest.raises(Exception) as pytest_wrapped_e:
+        # try to create another instance
+        Globals()
+    assert pytest_wrapped_e.type == Exception
+
+
+
+
+
+
+
+

Functions

+
+
+def test_globals_get_instaance() +
+
+

Test that we can instantiate a Globals instance

+
+ +Expand source code + +
@pytest.mark.unit
+def test_globals_get_instaance():
+    """Test that we can instantiate a Globals instance"""
+    ourglobals = Globals.getInstance()
+    ourglobals2 = Globals.getInstance()
+    assert ourglobals == ourglobals2
+
+
+
+def test_globals_there_can_be_only_one() +
+
+

Test that we can cannot create two Globals instances

+
+ +Expand source code + +
@pytest.mark.unit
+def test_globals_there_can_be_only_one():
+    """Test that we can cannot create two Globals instances"""
+    # if we have an instance, delete it
+    Globals.getInstance()
+    with pytest.raises(Exception) as pytest_wrapped_e:
+        # try to create another instance
+        Globals()
+    assert pytest_wrapped_e.type == Exception
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_int.html b/docs/meshtastic/tests/test_int.html new file mode 100644 index 0000000..4811aad --- /dev/null +++ b/docs/meshtastic/tests/test_int.html @@ -0,0 +1,177 @@ + + + + + + +meshtastic.tests.test_int API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_int

+
+
+

Meshtastic integration tests

+
+ +Expand source code + +
"""Meshtastic integration tests"""
+import re
+import subprocess
+
+import pytest
+
+
+@pytest.mark.int
+def test_int_no_args():
+    """Test without any args"""
+    return_value, out = subprocess.getstatusoutput('meshtastic')
+    assert re.match(r'usage: meshtastic', out)
+    assert return_value == 1
+
+
+@pytest.mark.int
+def test_int_version():
+    """Test '--version'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --version')
+    assert re.match(r'[0-9]+\.[0-9]+\.[0-9]', out)
+    assert return_value == 0
+
+
+@pytest.mark.int
+def test_int_help():
+    """Test '--help'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --help')
+    assert re.match(r'usage: meshtastic ', out)
+    assert return_value == 0
+
+
+@pytest.mark.int
+def test_int_support():
+    """Test '--support'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --support')
+    assert re.search(r'System', out)
+    assert re.search(r'Python', out)
+    assert return_value == 0
+
+
+
+
+
+
+
+

Functions

+
+
+def test_int_help() +
+
+

Test '–help'.

+
+ +Expand source code + +
@pytest.mark.int
+def test_int_help():
+    """Test '--help'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --help')
+    assert re.match(r'usage: meshtastic ', out)
+    assert return_value == 0
+
+
+
+def test_int_no_args() +
+
+

Test without any args

+
+ +Expand source code + +
@pytest.mark.int
+def test_int_no_args():
+    """Test without any args"""
+    return_value, out = subprocess.getstatusoutput('meshtastic')
+    assert re.match(r'usage: meshtastic', out)
+    assert return_value == 1
+
+
+
+def test_int_support() +
+
+

Test '–support'.

+
+ +Expand source code + +
@pytest.mark.int
+def test_int_support():
+    """Test '--support'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --support')
+    assert re.search(r'System', out)
+    assert re.search(r'Python', out)
+    assert return_value == 0
+
+
+
+def test_int_version() +
+
+

Test '–version'.

+
+ +Expand source code + +
@pytest.mark.int
+def test_int_version():
+    """Test '--version'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --version')
+    assert re.match(r'[0-9]+\.[0-9]+\.[0-9]', out)
+    assert return_value == 0
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_main.html b/docs/meshtastic/tests/test_main.html new file mode 100644 index 0000000..fa4ee62 --- /dev/null +++ b/docs/meshtastic/tests/test_main.html @@ -0,0 +1,3606 @@ + + + + + + +meshtastic.tests.test_main API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_main

+
+
+

Meshtastic unit tests for main.py

+
+ +Expand source code + +
"""Meshtastic unit tests for __main__.py"""
+
+import sys
+import os
+import re
+
+from unittest.mock import patch, MagicMock
+import pytest
+
+from meshtastic.__main__ import initParser, main, Globals, onReceive, onConnection
+import meshtastic.radioconfig_pb2
+from ..serial_interface import SerialInterface
+from ..tcp_interface import TCPInterface
+from ..ble_interface import BLEInterface
+from ..node import Node
+from ..channel_pb2 import Channel
+
+
+@pytest.mark.unit
+def test_main_init_parser_no_args(capsys, reset_globals):
+    """Test no arguments"""
+    sys.argv = ['']
+    Globals.getInstance().set_args(sys.argv)
+    initParser()
+    out, err = capsys.readouterr()
+    assert out == ''
+    assert err == ''
+
+
+@pytest.mark.unit
+def test_main_init_parser_version(capsys, reset_globals):
+    """Test --version"""
+    sys.argv = ['', '--version']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        initParser()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+    out, err = capsys.readouterr()
+    assert re.match(r'[0-9]+\.[0-9]+\.[0-9]', out)
+    assert err == ''
+
+
+@pytest.mark.unit
+def test_main_main_version(capsys, reset_globals):
+    """Test --version"""
+    sys.argv = ['', '--version']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+    out, err = capsys.readouterr()
+    assert re.match(r'[0-9]+\.[0-9]+\.[0-9]', out)
+    assert err == ''
+
+
+@pytest.mark.unit
+def test_main_main_no_args(reset_globals):
+    """Test with no args"""
+    sys.argv = ['']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+
+
+@pytest.mark.unit
+def test_main_support(capsys, reset_globals):
+    """Test --support"""
+    sys.argv = ['', '--support']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+    out, err = capsys.readouterr()
+    assert re.search(r'System', out, re.MULTILINE)
+    assert re.search(r'Platform', out, re.MULTILINE)
+    assert re.search(r'Machine', out, re.MULTILINE)
+    assert re.search(r'Executable', out, re.MULTILINE)
+    assert err == ''
+
+
+@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=[])
+def test_main_ch_index_no_devices(patched_find_ports, capsys, reset_globals):
+    """Test --ch-index 1"""
+    sys.argv = ['', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert Globals.getInstance().get_channel_index() == 1
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    out, err = capsys.readouterr()
+    assert re.search(r'Warning: No Meshtastic devices detected', out, re.MULTILINE)
+    assert err == ''
+    patched_find_ports.assert_called()
+
+
+@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=[])
+def test_main_test_no_ports(patched_find_ports, reset_globals):
+    """Test --test with no hardware"""
+    sys.argv = ['', '--test']
+    Globals.getInstance().set_args(sys.argv)
+
+    assert Globals.getInstance().get_target_node() is None
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    patched_find_ports.assert_called()
+
+
+@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1'])
+def test_main_test_one_port(patched_find_ports, reset_globals):
+    """Test --test with one fake port"""
+    sys.argv = ['', '--test']
+    Globals.getInstance().set_args(sys.argv)
+
+    assert Globals.getInstance().get_target_node() is None
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    patched_find_ports.assert_called()
+
+
+@pytest.mark.unit
+@patch('meshtastic.test.testAll', return_value=True)
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1', '/dev/ttyFake2'])
+def test_main_test_two_ports_success(patched_find_ports, patched_test_all, reset_globals):
+    """Test --test two fake ports and testAll() is a simulated success"""
+    sys.argv = ['', '--test']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+    # TODO: why does this fail? patched_find_ports.assert_called()
+    patched_test_all.assert_called()
+
+
+@pytest.mark.unit
+@patch('meshtastic.test.testAll', return_value=False)
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1', '/dev/ttyFake2'])
+def test_main_test_two_ports_fails(patched_find_ports, patched_test_all, reset_globals):
+    """Test --test two fake ports and testAll() is a simulated failure"""
+    sys.argv = ['', '--test']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    # TODO: why does this fail? patched_find_ports.assert_called()
+    patched_test_all.assert_called()
+
+
+@pytest.mark.unit
+def test_main_info(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_info_with_tcp_interface(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info', '--host', 'meshtastic.local']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=TCPInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.tcp_interface.TCPInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_info_with_ble_interface(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info', '--ble', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=BLEInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.ble_interface.BLEInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_no_proto(capsys, reset_globals):
+    """Test --noproto (using --info for output)"""
+    sys.argv = ['', '--info', '--noproto']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+
+    # Override the time.sleep so there is no loop
+    def my_sleep(amount):
+        sys.exit(0)
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface):
+        with patch('time.sleep', side_effect=my_sleep):
+            with pytest.raises(SystemExit) as pytest_wrapped_e:
+                main()
+            assert pytest_wrapped_e.type == SystemExit
+            assert pytest_wrapped_e.value.code == 0
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+            assert err == ''
+
+
+@pytest.mark.unit
+def test_main_info_with_seriallog_stdout(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info', '--seriallog', 'stdout']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_info_with_seriallog_output_txt(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info', '--seriallog', 'output.txt']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+    # do some cleanup
+    os.remove('output.txt')
+
+
+@pytest.mark.unit
+def test_main_qr(capsys, reset_globals):
+    """Test --qr"""
+    sys.argv = ['', '--qr']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    # TODO: could mock/check url
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Primary channel URL', out, re.MULTILINE)
+        # if a qr code is generated it will have lots of these
+        assert re.search(r'\[7m', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_nodes(capsys, reset_globals):
+    """Test --nodes"""
+    sys.argv = ['', '--nodes']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showNodes():
+        print('inside mocked showNodes')
+    iface.showNodes.side_effect = mock_showNodes
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showNodes', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_set_owner_to_bob(capsys, reset_globals):
+    """Test --set-owner bob"""
+    sys.argv = ['', '--set-owner', 'bob']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Setting device owner to bob', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_set_ham_to_KI123(capsys, reset_globals):
+    """Test --set-ham KI123"""
+    sys.argv = ['', '--set-ham', 'KI123']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_turnOffEncryptionOnPrimaryChannel():
+        print('inside mocked turnOffEncryptionOnPrimaryChannel')
+    def mock_setOwner(name, is_licensed):
+        print('inside mocked setOwner')
+    mocked_node.turnOffEncryptionOnPrimaryChannel.side_effect = mock_turnOffEncryptionOnPrimaryChannel
+    mocked_node.setOwner.side_effect = mock_setOwner
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Setting HAM ID to KI123', out, re.MULTILINE)
+        assert re.search(r'inside mocked setOwner', out, re.MULTILINE)
+        assert re.search(r'inside mocked turnOffEncryptionOnPrimaryChannel', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_reboot(capsys, reset_globals):
+    """Test --reboot"""
+    sys.argv = ['', '--reboot']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_reboot():
+        print('inside mocked reboot')
+    mocked_node.reboot.side_effect = mock_reboot
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked reboot', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_sendtext(capsys, reset_globals):
+    """Test --sendtext"""
+    sys.argv = ['', '--sendtext', 'hello']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendText(text, dest, wantAck):
+        print('inside mocked sendText')
+    iface.sendText.side_effect = mock_sendText
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Sending text message', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendText', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_sendtext_with_dest(capsys, reset_globals):
+    """Test --sendtext with --dest"""
+    sys.argv = ['', '--sendtext', 'hello', '--dest', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendText(text, dest, wantAck):
+        print('inside mocked sendText')
+    iface.sendText.side_effect = mock_sendText
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Sending text message', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendText', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_sendping(capsys, reset_globals):
+    """Test --sendping"""
+    sys.argv = ['', '--sendping']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendData(payload, dest, portNum, wantAck, wantResponse):
+        print('inside mocked sendData')
+    iface.sendData.side_effect = mock_sendData
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Sending ping message', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendData', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_setlat(capsys, reset_globals):
+    """Test --sendlat"""
+    sys.argv = ['', '--setlat', '37.5']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_writeConfig():
+        print('inside mocked writeConfig')
+    mocked_node.writeConfig.side_effect = mock_writeConfig
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendPosition(lat, lon, alt):
+        print('inside mocked sendPosition')
+    iface.sendPosition.side_effect = mock_sendPosition
+    iface.localNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Fixing latitude', out, re.MULTILINE)
+        assert re.search(r'Setting device position', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendPosition', out, re.MULTILINE)
+        # TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_setlon(capsys, reset_globals):
+    """Test --setlon"""
+    sys.argv = ['', '--setlon', '-122.1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_writeConfig():
+        print('inside mocked writeConfig')
+    mocked_node.writeConfig.side_effect = mock_writeConfig
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendPosition(lat, lon, alt):
+        print('inside mocked sendPosition')
+    iface.sendPosition.side_effect = mock_sendPosition
+    iface.localNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Fixing longitude', out, re.MULTILINE)
+        assert re.search(r'Setting device position', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendPosition', out, re.MULTILINE)
+        # TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_setalt(capsys, reset_globals):
+    """Test --setalt"""
+    sys.argv = ['', '--setalt', '51']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_writeConfig():
+        print('inside mocked writeConfig')
+    mocked_node.writeConfig.side_effect = mock_writeConfig
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendPosition(lat, lon, alt):
+        print('inside mocked sendPosition')
+    iface.sendPosition.side_effect = mock_sendPosition
+    iface.localNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Fixing altitude', out, re.MULTILINE)
+        assert re.search(r'Setting device position', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendPosition', out, re.MULTILINE)
+        # TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_set_team_valid(capsys, reset_globals):
+    """Test --set-team"""
+    sys.argv = ['', '--set-team', 'CYAN']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_setOwner(team):
+        print('inside mocked setOwner')
+    mocked_node.setOwner.side_effect = mock_setOwner
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.localNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with patch('meshtastic.mesh_pb2.Team') as mm:
+            mm.Name.return_value = 'FAKENAME'
+            mm.Value.return_value = 'FAKEVAL'
+            main()
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'Setting team to', out, re.MULTILINE)
+            assert err == ''
+            mo.assert_called()
+            mm.Name.assert_called()
+            mm.Value.assert_called()
+
+
+@pytest.mark.unit
+def test_main_set_team_invalid(capsys, reset_globals):
+    """Test --set-team using an invalid team name"""
+    sys.argv = ['', '--set-team', 'NOTCYAN']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+
+    def throw_an_exception(exc):
+        raise ValueError("Fake exception.")
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with patch('meshtastic.mesh_pb2.Team') as mm:
+            mm.Value.side_effect = throw_an_exception
+            main()
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'ERROR: Team', out, re.MULTILINE)
+            assert err == ''
+            mo.assert_called()
+            mm.Value.assert_called()
+
+
+@pytest.mark.unit
+def test_main_seturl(capsys, reset_globals):
+    """Test --seturl (url used below is what is generated after a factory_reset)"""
+    sys.argv = ['', '--seturl', 'https://www.meshtastic.org/d/#CgUYAyIBAQ']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_set_valid(capsys, reset_globals):
+    """Test --set with valid field"""
+    sys.argv = ['', '--set', 'wifi_ssid', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Set wifi_ssid to foo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_set_with_invalid(capsys, reset_globals):
+    """Test --set with invalid field"""
+    sys.argv = ['', '--set', 'foo', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_user_prefs = MagicMock()
+    mocked_user_prefs.DESCRIPTOR.fields_by_name.get.return_value = None
+
+    mocked_node = MagicMock(autospec=Node)
+    mocked_node.radioConfig.preferences = ( mocked_user_prefs )
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'does not have an attribute called foo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+# TODO: write some negative --configure tests
+@pytest.mark.unit
+def test_main_configure(capsys, reset_globals):
+    """Test --configure with valid file"""
+    sys.argv = ['', '--configure', 'example_config.yaml']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Setting device owner', out, re.MULTILINE)
+        assert re.search(r'Setting channel url', out, re.MULTILINE)
+        assert re.search(r'Fixing altitude', out, re.MULTILINE)
+        assert re.search(r'Fixing latitude', out, re.MULTILINE)
+        assert re.search(r'Fixing longitude', out, re.MULTILINE)
+        assert re.search(r'Writing modified preferences', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_add_valid(capsys, reset_globals):
+    """Test --ch-add with valid channel name, and that channel name does not already exist"""
+    sys.argv = ['', '--ch-add', 'testing']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_channel = MagicMock(autospec=Channel)
+    # TODO: figure out how to get it to print the channel name instead of MagicMock
+
+    mocked_node = MagicMock(autospec=Node)
+    # set it up so we do not already have a channel named this
+    mocked_node.getChannelByName.return_value = False
+    # set it up so we have free channels
+    mocked_node.getDisabledChannel.return_value = mocked_channel
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_add_invalid_name_too_long(capsys, reset_globals):
+    """Test --ch-add with invalid channel name, name too long"""
+    sys.argv = ['', '--ch-add', 'testingtestingtesting']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_channel = MagicMock(autospec=Channel)
+    # TODO: figure out how to get it to print the channel name instead of MagicMock
+
+    mocked_node = MagicMock(autospec=Node)
+    # set it up so we do not already have a channel named this
+    mocked_node.getChannelByName.return_value = False
+    # set it up so we have free channels
+    mocked_node.getDisabledChannel.return_value = mocked_channel
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Channel name must be shorter', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_add_but_name_already_exists(capsys, reset_globals):
+    """Test --ch-add with a channel name that already exists"""
+    sys.argv = ['', '--ch-add', 'testing']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    # set it up so we do not already have a channel named this
+    mocked_node.getChannelByName.return_value = True
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: This node already has', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_add_but_no_more_channels(capsys, reset_globals):
+    """Test --ch-add with but there are no more channels"""
+    sys.argv = ['', '--ch-add', 'testing']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    # set it up so we do not already have a channel named this
+    mocked_node.getChannelByName.return_value = False
+    # set it up so we have free channels
+    mocked_node.getDisabledChannel.return_value = None
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: No free channels were found', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_del(capsys, reset_globals):
+    """Test --ch-del with valid secondary channel to be deleted"""
+    sys.argv = ['', '--ch-del', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Deleting channel', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_del_no_ch_index_specified(capsys, reset_globals):
+    """Test --ch-del without a valid ch-index"""
+    sys.argv = ['', '--ch-del']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_del_primary_channel(capsys, reset_globals):
+    """Test --ch-del on ch-index=0"""
+    sys.argv = ['', '--ch-del', '--ch-index', '0']
+    Globals.getInstance().set_args(sys.argv)
+    Globals.getInstance().set_channel_index(1)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Cannot delete primary channel', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_enable_valid_secondary_channel(capsys, reset_globals):
+    """Test --ch-enable with --ch-index"""
+    sys.argv = ['', '--ch-enable', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Writing modified channels', out, re.MULTILINE)
+        assert err == ''
+        assert Globals.getInstance().get_channel_index() == 1
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_disable_valid_secondary_channel(capsys, reset_globals):
+    """Test --ch-disable with --ch-index"""
+    sys.argv = ['', '--ch-disable', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Writing modified channels', out, re.MULTILINE)
+        assert err == ''
+        assert Globals.getInstance().get_channel_index() == 1
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_enable_without_a_ch_index(capsys, reset_globals):
+    """Test --ch-enable without --ch-index"""
+    sys.argv = ['', '--ch-enable']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+        assert err == ''
+        assert Globals.getInstance().get_channel_index() is None
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_enable_primary_channel(capsys, reset_globals):
+    """Test --ch-enable with --ch-index = 0"""
+    sys.argv = ['', '--ch-enable', '--ch-index', '0']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Cannot enable/disable PRIMARY', out, re.MULTILINE)
+        assert err == ''
+        assert Globals.getInstance().get_channel_index() == 0
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_range_options(capsys, reset_globals):
+    """Test changing the various range options."""
+    range_options = ['--ch-longslow', '--ch-longfast', '--ch-mediumslow',
+                     '--ch-mediumfast', '--ch-shortslow', '--ch-shortfast']
+    for range_option in range_options:
+        sys.argv = ['', f"{range_option}" ]
+        Globals.getInstance().set_args(sys.argv)
+
+        mocked_node = MagicMock(autospec=Node)
+
+        iface = MagicMock(autospec=SerialInterface)
+        iface.getNode.return_value = mocked_node
+
+        with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+            main()
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'Writing modified channels', out, re.MULTILINE)
+            assert err == ''
+            mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_ch_longsfast_on_non_primary_channel(capsys, reset_globals):
+    """Test --ch-longfast --ch-index 1"""
+    sys.argv = ['', '--ch-longfast', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Standard channel settings', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+# PositionFlags:
+# Misc info that might be helpful (this info will grow stale, just
+# a snapshot of the values.) The radioconfig_pb2.PositionFlags.Name and bit values are:
+# POS_UNDEFINED 0
+# POS_ALTITUDE 1
+# POS_ALT_MSL 2
+# POS_GEO_SEP 4
+# POS_DOP 8
+# POS_HVDOP 16
+# POS_BATTERY 32
+# POS_SATINVIEW 64
+# POS_SEQ_NOS 128
+# POS_TIMESTAMP 256
+
+@pytest.mark.unit
+def test_main_pos_fields_no_args(capsys, reset_globals):
+    """Test --pos-fields no args (which shows settings)"""
+    sys.argv = ['', '--pos-fields']
+    Globals.getInstance().set_args(sys.argv)
+
+    pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags)
+
+    with patch('meshtastic.serial_interface.SerialInterface') as mo:
+        with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc:
+            # kind of cheating here, we are setting up the node
+            mocked_node = MagicMock(autospec=Node)
+            anode = mocked_node()
+            anode.radioConfig.preferences.position_flags = 35
+            Globals.getInstance().set_target_node(anode)
+
+            mrc.values.return_value = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256]
+            # Note: When you use side_effect and a list, each call will use a value from the front of the list then
+            # remove that value from the list. If there are three values in the list, we expect it to be called
+            # three times.
+            mrc.Name.side_effect = [ 'POS_ALTITUDE', 'POS_ALT_MSL', 'POS_BATTERY' ]
+
+            main()
+
+            mrc.Name.assert_called()
+            mrc.values.assert_called()
+            mo.assert_called()
+
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'POS_ALTITUDE POS_ALT_MSL POS_BATTERY', out, re.MULTILINE)
+            assert err == ''
+
+
+@pytest.mark.unit
+def test_main_pos_fields_arg_of_zero(capsys, reset_globals):
+    """Test --pos-fields an arg of 0 (which shows list)"""
+    sys.argv = ['', '--pos-fields', '0']
+    Globals.getInstance().set_args(sys.argv)
+
+    pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags)
+
+    with patch('meshtastic.serial_interface.SerialInterface') as mo:
+        with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc:
+
+            def throw_value_error_exception(exc):
+                raise ValueError()
+            mrc.Value.side_effect = throw_value_error_exception
+            mrc.keys.return_value = [ 'POS_UNDEFINED', 'POS_ALTITUDE', 'POS_ALT_MSL',
+                                      'POS_GEO_SEP', 'POS_DOP', 'POS_HVDOP', 'POS_BATTERY',
+                                      'POS_SATINVIEW', 'POS_SEQ_NOS', 'POS_TIMESTAMP']
+
+            main()
+
+            mrc.Value.assert_called()
+            mrc.keys.assert_called()
+            mo.assert_called()
+
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'ERROR: supported position fields are:', out, re.MULTILINE)
+            assert re.search(r"['POS_UNDEFINED', 'POS_ALTITUDE', 'POS_ALT_MSL', 'POS_GEO_SEP',"\
+                              "'POS_DOP', 'POS_HVDOP', 'POS_BATTERY', 'POS_SATINVIEW', 'POS_SEQ_NOS',"\
+                              "'POS_TIMESTAMP']", out, re.MULTILINE)
+            assert err == ''
+
+
+@pytest.mark.unit
+def test_main_pos_fields_valid_values(capsys, reset_globals):
+    """Test --pos-fields with valid values"""
+    sys.argv = ['', '--pos-fields', 'POS_GEO_SEP', 'POS_ALT_MSL']
+    Globals.getInstance().set_args(sys.argv)
+
+    pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags)
+
+    with patch('meshtastic.serial_interface.SerialInterface') as mo:
+        with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc:
+
+            mrc.Value.side_effect = [ 4, 2 ]
+
+            main()
+
+            mrc.Value.assert_called()
+            mo.assert_called()
+
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'Setting position fields to 6', out, re.MULTILINE)
+            assert re.search(r'Set position_flags to 6', out, re.MULTILINE)
+            assert re.search(r'Writing modified preferences to device', out, re.MULTILINE)
+            assert err == ''
+
+
+@pytest.mark.unit
+def test_main_get_with_valid_values(capsys, reset_globals):
+    """Test --get with valid values (with string, number, boolean)"""
+    sys.argv = ['', '--get', 'ls_secs', '--get', 'wifi_ssid', '--get', 'fixed_position']
+    Globals.getInstance().set_args(sys.argv)
+
+    with patch('meshtastic.serial_interface.SerialInterface') as mo:
+
+        # kind of cheating here, we are setting up the node
+        mocked_node = MagicMock(autospec=Node)
+        anode = mocked_node()
+        anode.radioConfig.preferences.wifi_ssid = 'foo'
+        anode.radioConfig.preferences.ls_secs = 300
+        anode.radioConfig.preferences.fixed_position = False
+        Globals.getInstance().set_target_node(anode)
+
+        main()
+
+        mo.assert_called()
+
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'ls_secs: 300', out, re.MULTILINE)
+        assert re.search(r'wifi_ssid: foo', out, re.MULTILINE)
+        assert re.search(r'fixed_position: False', out, re.MULTILINE)
+        assert err == ''
+
+
+@pytest.mark.unit
+def test_main_get_with_invalid(capsys, reset_globals):
+    """Test --get with invalid field"""
+    sys.argv = ['', '--get', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_user_prefs = MagicMock()
+    mocked_user_prefs.DESCRIPTOR.fields_by_name.get.return_value = None
+
+    mocked_node = MagicMock(autospec=Node)
+    mocked_node.radioConfig.preferences = ( mocked_user_prefs )
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'does not have an attribute called foo', out, re.MULTILINE)
+        assert re.search(r'Choices in sorted order are', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+@pytest.mark.unit
+def test_main_setchan(capsys, reset_globals):
+    """Test --setchan (deprecated)"""
+    sys.argv = ['', '--setchan', 'a', 'b']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface):
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+
+
+@pytest.mark.unit
+def test_main_onReceive_empty(reset_globals):
+    """Test onReceive"""
+    sys.argv = ['']
+    Globals.getInstance().set_args(sys.argv)
+    iface = MagicMock(autospec=SerialInterface)
+    packet = {'decoded': 'foo'}
+    onReceive(packet, iface)
+    # TODO: how do we know we actually called it?
+
+
+@pytest.mark.unit
+def test_main_onConnection(reset_globals, capsys):
+    """Test onConnection"""
+    sys.argv = ['']
+    Globals.getInstance().set_args(sys.argv)
+    iface = MagicMock(autospec=SerialInterface)
+    class TempTopic:
+        """ temp class for topic """
+        def getName(self):
+            """ return the fake name of a topic"""
+            return 'foo'
+    mytopic = TempTopic()
+    onConnection(iface, mytopic)
+    out, err = capsys.readouterr()
+    assert re.search(r'Connection changed: foo', out, re.MULTILINE)
+    assert err == ''
+
+
+
+
+
+
+
+

Functions

+
+
+def test_main_ch_add_but_name_already_exists(capsys, reset_globals) +
+
+

Test –ch-add with a channel name that already exists

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_add_but_name_already_exists(capsys, reset_globals):
+    """Test --ch-add with a channel name that already exists"""
+    sys.argv = ['', '--ch-add', 'testing']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    # set it up so we do not already have a channel named this
+    mocked_node.getChannelByName.return_value = True
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: This node already has', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_ch_add_but_no_more_channels(capsys, reset_globals) +
+
+

Test –ch-add with but there are no more channels

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_add_but_no_more_channels(capsys, reset_globals):
+    """Test --ch-add with but there are no more channels"""
+    sys.argv = ['', '--ch-add', 'testing']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    # set it up so we do not already have a channel named this
+    mocked_node.getChannelByName.return_value = False
+    # set it up so we have free channels
+    mocked_node.getDisabledChannel.return_value = None
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: No free channels were found', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_ch_add_invalid_name_too_long(capsys, reset_globals) +
+
+

Test –ch-add with invalid channel name, name too long

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_add_invalid_name_too_long(capsys, reset_globals):
+    """Test --ch-add with invalid channel name, name too long"""
+    sys.argv = ['', '--ch-add', 'testingtestingtesting']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_channel = MagicMock(autospec=Channel)
+    # TODO: figure out how to get it to print the channel name instead of MagicMock
+
+    mocked_node = MagicMock(autospec=Node)
+    # set it up so we do not already have a channel named this
+    mocked_node.getChannelByName.return_value = False
+    # set it up so we have free channels
+    mocked_node.getDisabledChannel.return_value = mocked_channel
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Channel name must be shorter', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_ch_add_valid(capsys, reset_globals) +
+
+

Test –ch-add with valid channel name, and that channel name does not already exist

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_add_valid(capsys, reset_globals):
+    """Test --ch-add with valid channel name, and that channel name does not already exist"""
+    sys.argv = ['', '--ch-add', 'testing']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_channel = MagicMock(autospec=Channel)
+    # TODO: figure out how to get it to print the channel name instead of MagicMock
+
+    mocked_node = MagicMock(autospec=Node)
+    # set it up so we do not already have a channel named this
+    mocked_node.getChannelByName.return_value = False
+    # set it up so we have free channels
+    mocked_node.getDisabledChannel.return_value = mocked_channel
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_ch_del(capsys, reset_globals) +
+
+

Test –ch-del with valid secondary channel to be deleted

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_del(capsys, reset_globals):
+    """Test --ch-del with valid secondary channel to be deleted"""
+    sys.argv = ['', '--ch-del', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Deleting channel', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_ch_del_no_ch_index_specified(capsys, reset_globals) +
+
+

Test –ch-del without a valid ch-index

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_del_no_ch_index_specified(capsys, reset_globals):
+    """Test --ch-del without a valid ch-index"""
+    sys.argv = ['', '--ch-del']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_ch_del_primary_channel(capsys, reset_globals) +
+
+

Test –ch-del on ch-index=0

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_del_primary_channel(capsys, reset_globals):
+    """Test --ch-del on ch-index=0"""
+    sys.argv = ['', '--ch-del', '--ch-index', '0']
+    Globals.getInstance().set_args(sys.argv)
+    Globals.getInstance().set_channel_index(1)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Cannot delete primary channel', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_ch_disable_valid_secondary_channel(capsys, reset_globals) +
+
+

Test –ch-disable with –ch-index

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_disable_valid_secondary_channel(capsys, reset_globals):
+    """Test --ch-disable with --ch-index"""
+    sys.argv = ['', '--ch-disable', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Writing modified channels', out, re.MULTILINE)
+        assert err == ''
+        assert Globals.getInstance().get_channel_index() == 1
+        mo.assert_called()
+
+
+
+def test_main_ch_enable_primary_channel(capsys, reset_globals) +
+
+

Test –ch-enable with –ch-index = 0

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_enable_primary_channel(capsys, reset_globals):
+    """Test --ch-enable with --ch-index = 0"""
+    sys.argv = ['', '--ch-enable', '--ch-index', '0']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Cannot enable/disable PRIMARY', out, re.MULTILINE)
+        assert err == ''
+        assert Globals.getInstance().get_channel_index() == 0
+        mo.assert_called()
+
+
+
+def test_main_ch_enable_valid_secondary_channel(capsys, reset_globals) +
+
+

Test –ch-enable with –ch-index

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_enable_valid_secondary_channel(capsys, reset_globals):
+    """Test --ch-enable with --ch-index"""
+    sys.argv = ['', '--ch-enable', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Writing modified channels', out, re.MULTILINE)
+        assert err == ''
+        assert Globals.getInstance().get_channel_index() == 1
+        mo.assert_called()
+
+
+
+def test_main_ch_enable_without_a_ch_index(capsys, reset_globals) +
+
+

Test –ch-enable without –ch-index

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_enable_without_a_ch_index(capsys, reset_globals):
+    """Test --ch-enable without --ch-index"""
+    sys.argv = ['', '--ch-enable']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+        assert err == ''
+        assert Globals.getInstance().get_channel_index() is None
+        mo.assert_called()
+
+
+
+def test_main_ch_index_no_devices(patched_find_ports, capsys, reset_globals) +
+
+

Test –ch-index 1

+
+ +Expand source code + +
@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=[])
+def test_main_ch_index_no_devices(patched_find_ports, capsys, reset_globals):
+    """Test --ch-index 1"""
+    sys.argv = ['', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert Globals.getInstance().get_channel_index() == 1
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    out, err = capsys.readouterr()
+    assert re.search(r'Warning: No Meshtastic devices detected', out, re.MULTILINE)
+    assert err == ''
+    patched_find_ports.assert_called()
+
+
+
+def test_main_ch_longsfast_on_non_primary_channel(capsys, reset_globals) +
+
+

Test –ch-longfast –ch-index 1

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_longsfast_on_non_primary_channel(capsys, reset_globals):
+    """Test --ch-longfast --ch-index 1"""
+    sys.argv = ['', '--ch-longfast', '--ch-index', '1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Warning: Standard channel settings', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_ch_range_options(capsys, reset_globals) +
+
+

Test changing the various range options.

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_ch_range_options(capsys, reset_globals):
+    """Test changing the various range options."""
+    range_options = ['--ch-longslow', '--ch-longfast', '--ch-mediumslow',
+                     '--ch-mediumfast', '--ch-shortslow', '--ch-shortfast']
+    for range_option in range_options:
+        sys.argv = ['', f"{range_option}" ]
+        Globals.getInstance().set_args(sys.argv)
+
+        mocked_node = MagicMock(autospec=Node)
+
+        iface = MagicMock(autospec=SerialInterface)
+        iface.getNode.return_value = mocked_node
+
+        with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+            main()
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'Writing modified channels', out, re.MULTILINE)
+            assert err == ''
+            mo.assert_called()
+
+
+
+def test_main_configure(capsys, reset_globals) +
+
+

Test –configure with valid file

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_configure(capsys, reset_globals):
+    """Test --configure with valid file"""
+    sys.argv = ['', '--configure', 'example_config.yaml']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Setting device owner', out, re.MULTILINE)
+        assert re.search(r'Setting channel url', out, re.MULTILINE)
+        assert re.search(r'Fixing altitude', out, re.MULTILINE)
+        assert re.search(r'Fixing latitude', out, re.MULTILINE)
+        assert re.search(r'Fixing longitude', out, re.MULTILINE)
+        assert re.search(r'Writing modified preferences', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_get_with_invalid(capsys, reset_globals) +
+
+

Test –get with invalid field

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_get_with_invalid(capsys, reset_globals):
+    """Test --get with invalid field"""
+    sys.argv = ['', '--get', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_user_prefs = MagicMock()
+    mocked_user_prefs.DESCRIPTOR.fields_by_name.get.return_value = None
+
+    mocked_node = MagicMock(autospec=Node)
+    mocked_node.radioConfig.preferences = ( mocked_user_prefs )
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'does not have an attribute called foo', out, re.MULTILINE)
+        assert re.search(r'Choices in sorted order are', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_get_with_valid_values(capsys, reset_globals) +
+
+

Test –get with valid values (with string, number, boolean)

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_get_with_valid_values(capsys, reset_globals):
+    """Test --get with valid values (with string, number, boolean)"""
+    sys.argv = ['', '--get', 'ls_secs', '--get', 'wifi_ssid', '--get', 'fixed_position']
+    Globals.getInstance().set_args(sys.argv)
+
+    with patch('meshtastic.serial_interface.SerialInterface') as mo:
+
+        # kind of cheating here, we are setting up the node
+        mocked_node = MagicMock(autospec=Node)
+        anode = mocked_node()
+        anode.radioConfig.preferences.wifi_ssid = 'foo'
+        anode.radioConfig.preferences.ls_secs = 300
+        anode.radioConfig.preferences.fixed_position = False
+        Globals.getInstance().set_target_node(anode)
+
+        main()
+
+        mo.assert_called()
+
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'ls_secs: 300', out, re.MULTILINE)
+        assert re.search(r'wifi_ssid: foo', out, re.MULTILINE)
+        assert re.search(r'fixed_position: False', out, re.MULTILINE)
+        assert err == ''
+
+
+
+def test_main_info(capsys, reset_globals) +
+
+

Test –info

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_info(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_info_with_ble_interface(capsys, reset_globals) +
+
+

Test –info

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_info_with_ble_interface(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info', '--ble', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=BLEInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.ble_interface.BLEInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_info_with_seriallog_output_txt(capsys, reset_globals) +
+
+

Test –info

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_info_with_seriallog_output_txt(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info', '--seriallog', 'output.txt']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+    # do some cleanup
+    os.remove('output.txt')
+
+
+
+def test_main_info_with_seriallog_stdout(capsys, reset_globals) +
+
+

Test –info

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_info_with_seriallog_stdout(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info', '--seriallog', 'stdout']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_info_with_tcp_interface(capsys, reset_globals) +
+
+

Test –info

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_info_with_tcp_interface(capsys, reset_globals):
+    """Test --info"""
+    sys.argv = ['', '--info', '--host', 'meshtastic.local']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=TCPInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+    with patch('meshtastic.tcp_interface.TCPInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_init_parser_no_args(capsys, reset_globals) +
+
+

Test no arguments

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_init_parser_no_args(capsys, reset_globals):
+    """Test no arguments"""
+    sys.argv = ['']
+    Globals.getInstance().set_args(sys.argv)
+    initParser()
+    out, err = capsys.readouterr()
+    assert out == ''
+    assert err == ''
+
+
+
+def test_main_init_parser_version(capsys, reset_globals) +
+
+

Test –version

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_init_parser_version(capsys, reset_globals):
+    """Test --version"""
+    sys.argv = ['', '--version']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        initParser()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+    out, err = capsys.readouterr()
+    assert re.match(r'[0-9]+\.[0-9]+\.[0-9]', out)
+    assert err == ''
+
+
+
+def test_main_main_no_args(reset_globals) +
+
+

Test with no args

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_main_no_args(reset_globals):
+    """Test with no args"""
+    sys.argv = ['']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+
+
+
+def test_main_main_version(capsys, reset_globals) +
+
+

Test –version

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_main_version(capsys, reset_globals):
+    """Test --version"""
+    sys.argv = ['', '--version']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+    out, err = capsys.readouterr()
+    assert re.match(r'[0-9]+\.[0-9]+\.[0-9]', out)
+    assert err == ''
+
+
+
+def test_main_no_proto(capsys, reset_globals) +
+
+

Test –noproto (using –info for output)

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_no_proto(capsys, reset_globals):
+    """Test --noproto (using --info for output)"""
+    sys.argv = ['', '--info', '--noproto']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showInfo():
+        print('inside mocked showInfo')
+    iface.showInfo.side_effect = mock_showInfo
+
+    # Override the time.sleep so there is no loop
+    def my_sleep(amount):
+        sys.exit(0)
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface):
+        with patch('time.sleep', side_effect=my_sleep):
+            with pytest.raises(SystemExit) as pytest_wrapped_e:
+                main()
+            assert pytest_wrapped_e.type == SystemExit
+            assert pytest_wrapped_e.value.code == 0
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
+            assert err == ''
+
+
+
+def test_main_nodes(capsys, reset_globals) +
+
+

Test –nodes

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_nodes(capsys, reset_globals):
+    """Test --nodes"""
+    sys.argv = ['', '--nodes']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_showNodes():
+        print('inside mocked showNodes')
+    iface.showNodes.side_effect = mock_showNodes
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked showNodes', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_onConnection(reset_globals, capsys) +
+
+

Test onConnection

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_onConnection(reset_globals, capsys):
+    """Test onConnection"""
+    sys.argv = ['']
+    Globals.getInstance().set_args(sys.argv)
+    iface = MagicMock(autospec=SerialInterface)
+    class TempTopic:
+        """ temp class for topic """
+        def getName(self):
+            """ return the fake name of a topic"""
+            return 'foo'
+    mytopic = TempTopic()
+    onConnection(iface, mytopic)
+    out, err = capsys.readouterr()
+    assert re.search(r'Connection changed: foo', out, re.MULTILINE)
+    assert err == ''
+
+
+
+def test_main_onReceive_empty(reset_globals) +
+
+

Test onReceive

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_onReceive_empty(reset_globals):
+    """Test onReceive"""
+    sys.argv = ['']
+    Globals.getInstance().set_args(sys.argv)
+    iface = MagicMock(autospec=SerialInterface)
+    packet = {'decoded': 'foo'}
+    onReceive(packet, iface)
+    # TODO: how do we know we actually called it?
+
+
+
+def test_main_pos_fields_arg_of_zero(capsys, reset_globals) +
+
+

Test –pos-fields an arg of 0 (which shows list)

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_pos_fields_arg_of_zero(capsys, reset_globals):
+    """Test --pos-fields an arg of 0 (which shows list)"""
+    sys.argv = ['', '--pos-fields', '0']
+    Globals.getInstance().set_args(sys.argv)
+
+    pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags)
+
+    with patch('meshtastic.serial_interface.SerialInterface') as mo:
+        with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc:
+
+            def throw_value_error_exception(exc):
+                raise ValueError()
+            mrc.Value.side_effect = throw_value_error_exception
+            mrc.keys.return_value = [ 'POS_UNDEFINED', 'POS_ALTITUDE', 'POS_ALT_MSL',
+                                      'POS_GEO_SEP', 'POS_DOP', 'POS_HVDOP', 'POS_BATTERY',
+                                      'POS_SATINVIEW', 'POS_SEQ_NOS', 'POS_TIMESTAMP']
+
+            main()
+
+            mrc.Value.assert_called()
+            mrc.keys.assert_called()
+            mo.assert_called()
+
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'ERROR: supported position fields are:', out, re.MULTILINE)
+            assert re.search(r"['POS_UNDEFINED', 'POS_ALTITUDE', 'POS_ALT_MSL', 'POS_GEO_SEP',"\
+                              "'POS_DOP', 'POS_HVDOP', 'POS_BATTERY', 'POS_SATINVIEW', 'POS_SEQ_NOS',"\
+                              "'POS_TIMESTAMP']", out, re.MULTILINE)
+            assert err == ''
+
+
+
+def test_main_pos_fields_no_args(capsys, reset_globals) +
+
+

Test –pos-fields no args (which shows settings)

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_pos_fields_no_args(capsys, reset_globals):
+    """Test --pos-fields no args (which shows settings)"""
+    sys.argv = ['', '--pos-fields']
+    Globals.getInstance().set_args(sys.argv)
+
+    pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags)
+
+    with patch('meshtastic.serial_interface.SerialInterface') as mo:
+        with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc:
+            # kind of cheating here, we are setting up the node
+            mocked_node = MagicMock(autospec=Node)
+            anode = mocked_node()
+            anode.radioConfig.preferences.position_flags = 35
+            Globals.getInstance().set_target_node(anode)
+
+            mrc.values.return_value = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256]
+            # Note: When you use side_effect and a list, each call will use a value from the front of the list then
+            # remove that value from the list. If there are three values in the list, we expect it to be called
+            # three times.
+            mrc.Name.side_effect = [ 'POS_ALTITUDE', 'POS_ALT_MSL', 'POS_BATTERY' ]
+
+            main()
+
+            mrc.Name.assert_called()
+            mrc.values.assert_called()
+            mo.assert_called()
+
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'POS_ALTITUDE POS_ALT_MSL POS_BATTERY', out, re.MULTILINE)
+            assert err == ''
+
+
+
+def test_main_pos_fields_valid_values(capsys, reset_globals) +
+
+

Test –pos-fields with valid values

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_pos_fields_valid_values(capsys, reset_globals):
+    """Test --pos-fields with valid values"""
+    sys.argv = ['', '--pos-fields', 'POS_GEO_SEP', 'POS_ALT_MSL']
+    Globals.getInstance().set_args(sys.argv)
+
+    pos_flags = MagicMock(autospec=meshtastic.radioconfig_pb2.PositionFlags)
+
+    with patch('meshtastic.serial_interface.SerialInterface') as mo:
+        with patch('meshtastic.radioconfig_pb2.PositionFlags', return_value=pos_flags) as mrc:
+
+            mrc.Value.side_effect = [ 4, 2 ]
+
+            main()
+
+            mrc.Value.assert_called()
+            mo.assert_called()
+
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'Setting position fields to 6', out, re.MULTILINE)
+            assert re.search(r'Set position_flags to 6', out, re.MULTILINE)
+            assert re.search(r'Writing modified preferences to device', out, re.MULTILINE)
+            assert err == ''
+
+
+
+def test_main_qr(capsys, reset_globals) +
+
+

Test –qr

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_qr(capsys, reset_globals):
+    """Test --qr"""
+    sys.argv = ['', '--qr']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    # TODO: could mock/check url
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Primary channel URL', out, re.MULTILINE)
+        # if a qr code is generated it will have lots of these
+        assert re.search(r'\[7m', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_reboot(capsys, reset_globals) +
+
+

Test –reboot

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_reboot(capsys, reset_globals):
+    """Test --reboot"""
+    sys.argv = ['', '--reboot']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_reboot():
+        print('inside mocked reboot')
+    mocked_node.reboot.side_effect = mock_reboot
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'inside mocked reboot', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_sendping(capsys, reset_globals) +
+
+

Test –sendping

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_sendping(capsys, reset_globals):
+    """Test --sendping"""
+    sys.argv = ['', '--sendping']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendData(payload, dest, portNum, wantAck, wantResponse):
+        print('inside mocked sendData')
+    iface.sendData.side_effect = mock_sendData
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Sending ping message', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendData', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_sendtext(capsys, reset_globals) +
+
+

Test –sendtext

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_sendtext(capsys, reset_globals):
+    """Test --sendtext"""
+    sys.argv = ['', '--sendtext', 'hello']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendText(text, dest, wantAck):
+        print('inside mocked sendText')
+    iface.sendText.side_effect = mock_sendText
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Sending text message', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendText', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_sendtext_with_dest(capsys, reset_globals) +
+
+

Test –sendtext with –dest

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_sendtext_with_dest(capsys, reset_globals):
+    """Test --sendtext with --dest"""
+    sys.argv = ['', '--sendtext', 'hello', '--dest', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendText(text, dest, wantAck):
+        print('inside mocked sendText')
+    iface.sendText.side_effect = mock_sendText
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Sending text message', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendText', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_set_ham_to_KI123(capsys, reset_globals) +
+
+

Test –set-ham KI123

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_set_ham_to_KI123(capsys, reset_globals):
+    """Test --set-ham KI123"""
+    sys.argv = ['', '--set-ham', 'KI123']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_turnOffEncryptionOnPrimaryChannel():
+        print('inside mocked turnOffEncryptionOnPrimaryChannel')
+    def mock_setOwner(name, is_licensed):
+        print('inside mocked setOwner')
+    mocked_node.turnOffEncryptionOnPrimaryChannel.side_effect = mock_turnOffEncryptionOnPrimaryChannel
+    mocked_node.setOwner.side_effect = mock_setOwner
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Setting HAM ID to KI123', out, re.MULTILINE)
+        assert re.search(r'inside mocked setOwner', out, re.MULTILINE)
+        assert re.search(r'inside mocked turnOffEncryptionOnPrimaryChannel', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_set_owner_to_bob(capsys, reset_globals) +
+
+

Test –set-owner bob

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_set_owner_to_bob(capsys, reset_globals):
+    """Test --set-owner bob"""
+    sys.argv = ['', '--set-owner', 'bob']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Setting device owner to bob', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_set_team_invalid(capsys, reset_globals) +
+
+

Test –set-team using an invalid team name

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_set_team_invalid(capsys, reset_globals):
+    """Test --set-team using an invalid team name"""
+    sys.argv = ['', '--set-team', 'NOTCYAN']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+
+    def throw_an_exception(exc):
+        raise ValueError("Fake exception.")
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with patch('meshtastic.mesh_pb2.Team') as mm:
+            mm.Value.side_effect = throw_an_exception
+            main()
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'ERROR: Team', out, re.MULTILINE)
+            assert err == ''
+            mo.assert_called()
+            mm.Value.assert_called()
+
+
+
+def test_main_set_team_valid(capsys, reset_globals) +
+
+

Test –set-team

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_set_team_valid(capsys, reset_globals):
+    """Test --set-team"""
+    sys.argv = ['', '--set-team', 'CYAN']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_setOwner(team):
+        print('inside mocked setOwner')
+    mocked_node.setOwner.side_effect = mock_setOwner
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.localNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with patch('meshtastic.mesh_pb2.Team') as mm:
+            mm.Name.return_value = 'FAKENAME'
+            mm.Value.return_value = 'FAKEVAL'
+            main()
+            out, err = capsys.readouterr()
+            assert re.search(r'Connected to radio', out, re.MULTILINE)
+            assert re.search(r'Setting team to', out, re.MULTILINE)
+            assert err == ''
+            mo.assert_called()
+            mm.Name.assert_called()
+            mm.Value.assert_called()
+
+
+
+def test_main_set_valid(capsys, reset_globals) +
+
+

Test –set with valid field

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_set_valid(capsys, reset_globals):
+    """Test --set with valid field"""
+    sys.argv = ['', '--set', 'wifi_ssid', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Set wifi_ssid to foo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_set_with_invalid(capsys, reset_globals) +
+
+

Test –set with invalid field

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_set_with_invalid(capsys, reset_globals):
+    """Test --set with invalid field"""
+    sys.argv = ['', '--set', 'foo', 'foo']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_user_prefs = MagicMock()
+    mocked_user_prefs.DESCRIPTOR.fields_by_name.get.return_value = None
+
+    mocked_node = MagicMock(autospec=Node)
+    mocked_node.radioConfig.preferences = ( mocked_user_prefs )
+
+    iface = MagicMock(autospec=SerialInterface)
+    iface.getNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'does not have an attribute called foo', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_setalt(capsys, reset_globals) +
+
+

Test –setalt

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_setalt(capsys, reset_globals):
+    """Test --setalt"""
+    sys.argv = ['', '--setalt', '51']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_writeConfig():
+        print('inside mocked writeConfig')
+    mocked_node.writeConfig.side_effect = mock_writeConfig
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendPosition(lat, lon, alt):
+        print('inside mocked sendPosition')
+    iface.sendPosition.side_effect = mock_sendPosition
+    iface.localNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Fixing altitude', out, re.MULTILINE)
+        assert re.search(r'Setting device position', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendPosition', out, re.MULTILINE)
+        # TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_setchan(capsys, reset_globals) +
+
+

Test –setchan (deprecated)

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_setchan(capsys, reset_globals):
+    """Test --setchan (deprecated)"""
+    sys.argv = ['', '--setchan', 'a', 'b']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface):
+        with pytest.raises(SystemExit) as pytest_wrapped_e:
+            main()
+        assert pytest_wrapped_e.type == SystemExit
+        assert pytest_wrapped_e.value.code == 1
+
+
+
+def test_main_setlat(capsys, reset_globals) +
+
+

Test –sendlat

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_setlat(capsys, reset_globals):
+    """Test --sendlat"""
+    sys.argv = ['', '--setlat', '37.5']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_writeConfig():
+        print('inside mocked writeConfig')
+    mocked_node.writeConfig.side_effect = mock_writeConfig
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendPosition(lat, lon, alt):
+        print('inside mocked sendPosition')
+    iface.sendPosition.side_effect = mock_sendPosition
+    iface.localNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Fixing latitude', out, re.MULTILINE)
+        assert re.search(r'Setting device position', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendPosition', out, re.MULTILINE)
+        # TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_setlon(capsys, reset_globals) +
+
+

Test –setlon

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_setlon(capsys, reset_globals):
+    """Test --setlon"""
+    sys.argv = ['', '--setlon', '-122.1']
+    Globals.getInstance().set_args(sys.argv)
+
+    mocked_node = MagicMock(autospec=Node)
+    def mock_writeConfig():
+        print('inside mocked writeConfig')
+    mocked_node.writeConfig.side_effect = mock_writeConfig
+
+    iface = MagicMock(autospec=SerialInterface)
+    def mock_sendPosition(lat, lon, alt):
+        print('inside mocked sendPosition')
+    iface.sendPosition.side_effect = mock_sendPosition
+    iface.localNode.return_value = mocked_node
+
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert re.search(r'Fixing longitude', out, re.MULTILINE)
+        assert re.search(r'Setting device position', out, re.MULTILINE)
+        assert re.search(r'inside mocked sendPosition', out, re.MULTILINE)
+        # TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_seturl(capsys, reset_globals) +
+
+

Test –seturl (url used below is what is generated after a factory_reset)

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_seturl(capsys, reset_globals):
+    """Test --seturl (url used below is what is generated after a factory_reset)"""
+    sys.argv = ['', '--seturl', 'https://www.meshtastic.org/d/#CgUYAyIBAQ']
+    Globals.getInstance().set_args(sys.argv)
+
+    iface = MagicMock(autospec=SerialInterface)
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        main()
+        out, err = capsys.readouterr()
+        assert re.search(r'Connected to radio', out, re.MULTILINE)
+        assert err == ''
+        mo.assert_called()
+
+
+
+def test_main_support(capsys, reset_globals) +
+
+

Test –support

+
+ +Expand source code + +
@pytest.mark.unit
+def test_main_support(capsys, reset_globals):
+    """Test --support"""
+    sys.argv = ['', '--support']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+    out, err = capsys.readouterr()
+    assert re.search(r'System', out, re.MULTILINE)
+    assert re.search(r'Platform', out, re.MULTILINE)
+    assert re.search(r'Machine', out, re.MULTILINE)
+    assert re.search(r'Executable', out, re.MULTILINE)
+    assert err == ''
+
+
+
+def test_main_test_no_ports(patched_find_ports, reset_globals) +
+
+

Test –test with no hardware

+
+ +Expand source code + +
@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=[])
+def test_main_test_no_ports(patched_find_ports, reset_globals):
+    """Test --test with no hardware"""
+    sys.argv = ['', '--test']
+    Globals.getInstance().set_args(sys.argv)
+
+    assert Globals.getInstance().get_target_node() is None
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    patched_find_ports.assert_called()
+
+
+
+def test_main_test_one_port(patched_find_ports, reset_globals) +
+
+

Test –test with one fake port

+
+ +Expand source code + +
@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1'])
+def test_main_test_one_port(patched_find_ports, reset_globals):
+    """Test --test with one fake port"""
+    sys.argv = ['', '--test']
+    Globals.getInstance().set_args(sys.argv)
+
+    assert Globals.getInstance().get_target_node() is None
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    patched_find_ports.assert_called()
+
+
+
+def test_main_test_two_ports_fails(patched_find_ports, patched_test_all, reset_globals) +
+
+

Test –test two fake ports and testAll() is a simulated failure

+
+ +Expand source code + +
@pytest.mark.unit
+@patch('meshtastic.test.testAll', return_value=False)
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1', '/dev/ttyFake2'])
+def test_main_test_two_ports_fails(patched_find_ports, patched_test_all, reset_globals):
+    """Test --test two fake ports and testAll() is a simulated failure"""
+    sys.argv = ['', '--test']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    # TODO: why does this fail? patched_find_ports.assert_called()
+    patched_test_all.assert_called()
+
+
+
+def test_main_test_two_ports_success(patched_find_ports, patched_test_all, reset_globals) +
+
+

Test –test two fake ports and testAll() is a simulated success

+
+ +Expand source code + +
@pytest.mark.unit
+@patch('meshtastic.test.testAll', return_value=True)
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1', '/dev/ttyFake2'])
+def test_main_test_two_ports_success(patched_find_ports, patched_test_all, reset_globals):
+    """Test --test two fake ports and testAll() is a simulated success"""
+    sys.argv = ['', '--test']
+    Globals.getInstance().set_args(sys.argv)
+
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        main()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+    # TODO: why does this fail? patched_find_ports.assert_called()
+    patched_test_all.assert_called()
+
+
+
+
+
+

Classes

+
+
+class Channel +(**kwargs) +
+
+

Abstract base class for protocol messages.

+

Protocol message classes are almost always generated by the protocol +compiler. +These generated types subclass Message and implement the methods +shown below.

+

Ancestors

+
    +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var DISABLED
+
+
+
+
var INDEX_FIELD_NUMBER
+
+
+
+
var PRIMARY
+
+
+
+
var ROLE_FIELD_NUMBER
+
+
+
+
var Role
+
+
+
+
var SECONDARY
+
+
+
+
var SETTINGS_FIELD_NUMBER
+
+
+
+
+

Static methods

+
+
+def FromString(s) +
+
+
+
+ +Expand source code + +
def FromString(s):
+  message = cls()
+  message.MergeFromString(s)
+  return message
+
+
+
+def RegisterExtension(extension_handle) +
+
+
+
+ +Expand source code + +
def RegisterExtension(extension_handle):
+  extension_handle.containing_type = cls.DESCRIPTOR
+  # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available.
+  # pylint: disable=protected-access
+  cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle)
+  _AttachFieldHelpers(cls, extension_handle)
+
+
+
+

Instance variables

+
+
var index
+
+

Getter for index.

+
+ +Expand source code + +
def getter(self):
+  # TODO(protobuf-team): This may be broken since there may not be
+  # default_value.  Combine with has_default_value somehow.
+  return self._fields.get(field, default_value)
+
+
+
var role
+
+

Getter for role.

+
+ +Expand source code + +
def getter(self):
+  # TODO(protobuf-team): This may be broken since there may not be
+  # default_value.  Combine with has_default_value somehow.
+  return self._fields.get(field, default_value)
+
+
+
var settings
+
+

Getter for settings.

+
+ +Expand source code + +
def getter(self):
+  field_value = self._fields.get(field)
+  if field_value is None:
+    # Construct a new object to represent this field.
+    field_value = field._default_constructor(self)
+
+    # Atomically check if another thread has preempted us and, if not, swap
+    # in the new object we just created.  If someone has preempted us, we
+    # take that object and discard ours.
+    # WARNING:  We are relying on setdefault() being atomic.  This is true
+    #   in CPython but we haven't investigated others.  This warning appears
+    #   in several other locations in this file.
+    field_value = self._fields.setdefault(field, field_value)
+  return field_value
+
+
+
+

Methods

+
+
+def ByteSize(self) +
+
+
+
+ +Expand source code + +
def ByteSize(self):
+  if not self._cached_byte_size_dirty:
+    return self._cached_byte_size
+
+  size = 0
+  descriptor = self.DESCRIPTOR
+  if descriptor.GetOptions().map_entry:
+    # Fields of map entry should always be serialized.
+    size = descriptor.fields_by_name['key']._sizer(self.key)
+    size += descriptor.fields_by_name['value']._sizer(self.value)
+  else:
+    for field_descriptor, field_value in self.ListFields():
+      size += field_descriptor._sizer(field_value)
+    for tag_bytes, value_bytes in self._unknown_fields:
+      size += len(tag_bytes) + len(value_bytes)
+
+  self._cached_byte_size = size
+  self._cached_byte_size_dirty = False
+  self._listener_for_children.dirty = False
+  return size
+
+
+
+def Clear(self) +
+
+
+
+ +Expand source code + +
def _Clear(self):
+  # Clear fields.
+  self._fields = {}
+  self._unknown_fields = ()
+  # pylint: disable=protected-access
+  if self._unknown_field_set is not None:
+    self._unknown_field_set._clear()
+    self._unknown_field_set = None
+
+  self._oneofs = {}
+  self._Modified()
+
+
+
+def ClearField(self, field_name) +
+
+
+
+ +Expand source code + +
def ClearField(self, field_name):
+  try:
+    field = message_descriptor.fields_by_name[field_name]
+  except KeyError:
+    try:
+      field = message_descriptor.oneofs_by_name[field_name]
+      if field in self._oneofs:
+        field = self._oneofs[field]
+      else:
+        return
+    except KeyError:
+      raise ValueError('Protocol message %s has no "%s" field.' %
+                       (message_descriptor.name, field_name))
+
+  if field in self._fields:
+    # To match the C++ implementation, we need to invalidate iterators
+    # for map fields when ClearField() happens.
+    if hasattr(self._fields[field], 'InvalidateIterators'):
+      self._fields[field].InvalidateIterators()
+
+    # Note:  If the field is a sub-message, its listener will still point
+    #   at us.  That's fine, because the worst than can happen is that it
+    #   will call _Modified() and invalidate our byte size.  Big deal.
+    del self._fields[field]
+
+    if self._oneofs.get(field.containing_oneof, None) is field:
+      del self._oneofs[field.containing_oneof]
+
+  # Always call _Modified() -- even if nothing was changed, this is
+  # a mutating method, and thus calling it should cause the field to become
+  # present in the parent message.
+  self._Modified()
+
+
+
+def DiscardUnknownFields(self) +
+
+
+
+ +Expand source code + +
def _DiscardUnknownFields(self):
+  self._unknown_fields = []
+  self._unknown_field_set = None      # pylint: disable=protected-access
+  for field, value in self.ListFields():
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      if _IsMapField(field):
+        if _IsMessageMapField(field):
+          for key in value:
+            value[key].DiscardUnknownFields()
+      elif field.label == _FieldDescriptor.LABEL_REPEATED:
+        for sub_message in value:
+          sub_message.DiscardUnknownFields()
+      else:
+        value.DiscardUnknownFields()
+
+
+
+def FindInitializationErrors(self) +
+
+

Finds required fields which are not initialized.

+

Returns

+

A list of strings. +Each string is a path to an uninitialized field from +the top-level message, e.g. "foo.bar[5].baz".

+
+ +Expand source code + +
def FindInitializationErrors(self):
+  """Finds required fields which are not initialized.
+
+  Returns:
+    A list of strings.  Each string is a path to an uninitialized field from
+    the top-level message, e.g. "foo.bar[5].baz".
+  """
+
+  errors = []  # simplify things
+
+  for field in required_fields:
+    if not self.HasField(field.name):
+      errors.append(field.name)
+
+  for field, value in self.ListFields():
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      if field.is_extension:
+        name = '(%s)' % field.full_name
+      else:
+        name = field.name
+
+      if _IsMapField(field):
+        if _IsMessageMapField(field):
+          for key in value:
+            element = value[key]
+            prefix = '%s[%s].' % (name, key)
+            sub_errors = element.FindInitializationErrors()
+            errors += [prefix + error for error in sub_errors]
+        else:
+          # ScalarMaps can't have any initialization errors.
+          pass
+      elif field.label == _FieldDescriptor.LABEL_REPEATED:
+        for i in range(len(value)):
+          element = value[i]
+          prefix = '%s[%d].' % (name, i)
+          sub_errors = element.FindInitializationErrors()
+          errors += [prefix + error for error in sub_errors]
+      else:
+        prefix = name + '.'
+        sub_errors = value.FindInitializationErrors()
+        errors += [prefix + error for error in sub_errors]
+
+  return errors
+
+
+
+def HasField(self, field_name) +
+
+
+
+ +Expand source code + +
def HasField(self, field_name):
+  try:
+    field = hassable_fields[field_name]
+  except KeyError:
+    raise ValueError(error_msg % (message_descriptor.full_name, field_name))
+
+  if isinstance(field, descriptor_mod.OneofDescriptor):
+    try:
+      return HasField(self, self._oneofs[field].name)
+    except KeyError:
+      return False
+  else:
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      value = self._fields.get(field)
+      return value is not None and value._is_present_in_parent
+    else:
+      return field in self._fields
+
+
+
+def IsInitialized(self, errors=None) +
+
+

Checks if all required fields of a message are set.

+

Args

+
+
errors
+
A list which, if provided, will be populated with the field +paths of all missing required fields.
+
+

Returns

+

True iff the specified message has all required fields set.

+
+ +Expand source code + +
def IsInitialized(self, errors=None):
+  """Checks if all required fields of a message are set.
+
+  Args:
+    errors:  A list which, if provided, will be populated with the field
+             paths of all missing required fields.
+
+  Returns:
+    True iff the specified message has all required fields set.
+  """
+
+  # Performance is critical so we avoid HasField() and ListFields().
+
+  for field in required_fields:
+    if (field not in self._fields or
+        (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and
+         not self._fields[field]._is_present_in_parent)):
+      if errors is not None:
+        errors.extend(self.FindInitializationErrors())
+      return False
+
+  for field, value in list(self._fields.items()):  # dict can change size!
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      if field.label == _FieldDescriptor.LABEL_REPEATED:
+        if (field.message_type.has_options and
+            field.message_type.GetOptions().map_entry):
+          continue
+        for element in value:
+          if not element.IsInitialized():
+            if errors is not None:
+              errors.extend(self.FindInitializationErrors())
+            return False
+      elif value._is_present_in_parent and not value.IsInitialized():
+        if errors is not None:
+          errors.extend(self.FindInitializationErrors())
+        return False
+
+  return True
+
+
+
+def ListFields(self) +
+
+
+
+ +Expand source code + +
def ListFields(self):
+  all_fields = [item for item in self._fields.items() if _IsPresent(item)]
+  all_fields.sort(key = lambda item: item[0].number)
+  return all_fields
+
+
+
+def MergeFrom(self, msg) +
+
+
+
+ +Expand source code + +
def MergeFrom(self, msg):
+  if not isinstance(msg, cls):
+    raise TypeError(
+        'Parameter to MergeFrom() must be instance of same class: '
+        'expected %s got %s.' % (_FullyQualifiedClassName(cls),
+                                 _FullyQualifiedClassName(msg.__class__)))
+
+  assert msg is not self
+  self._Modified()
+
+  fields = self._fields
+
+  for field, value in msg._fields.items():
+    if field.label == LABEL_REPEATED:
+      field_value = fields.get(field)
+      if field_value is None:
+        # Construct a new object to represent this field.
+        field_value = field._default_constructor(self)
+        fields[field] = field_value
+      field_value.MergeFrom(value)
+    elif field.cpp_type == CPPTYPE_MESSAGE:
+      if value._is_present_in_parent:
+        field_value = fields.get(field)
+        if field_value is None:
+          # Construct a new object to represent this field.
+          field_value = field._default_constructor(self)
+          fields[field] = field_value
+        field_value.MergeFrom(value)
+    else:
+      self._fields[field] = value
+      if field.containing_oneof:
+        self._UpdateOneofState(field)
+
+  if msg._unknown_fields:
+    if not self._unknown_fields:
+      self._unknown_fields = []
+    self._unknown_fields.extend(msg._unknown_fields)
+    # pylint: disable=protected-access
+    if self._unknown_field_set is None:
+      self._unknown_field_set = containers.UnknownFieldSet()
+    self._unknown_field_set._extend(msg._unknown_field_set)
+
+
+
+def MergeFromString(self, serialized) +
+
+
+
+ +Expand source code + +
def MergeFromString(self, serialized):
+  serialized = memoryview(serialized)
+  length = len(serialized)
+  try:
+    if self._InternalParse(serialized, 0, length) != length:
+      # The only reason _InternalParse would return early is if it
+      # encountered an end-group tag.
+      raise message_mod.DecodeError('Unexpected end-group tag.')
+  except (IndexError, TypeError):
+    # Now ord(buf[p:p+1]) == ord('') gets TypeError.
+    raise message_mod.DecodeError('Truncated message.')
+  except struct.error as e:
+    raise message_mod.DecodeError(e)
+  return length   # Return this for legacy reasons.
+
+
+
+def SerializePartialToString(self, **kwargs) +
+
+
+
+ +Expand source code + +
def SerializePartialToString(self, **kwargs):
+  out = BytesIO()
+  self._InternalSerialize(out.write, **kwargs)
+  return out.getvalue()
+
+
+
+def SerializeToString(self, **kwargs) +
+
+
+
+ +Expand source code + +
def SerializeToString(self, **kwargs):
+  # Check if the message has all of its required fields set.
+  if not self.IsInitialized():
+    raise message_mod.EncodeError(
+        'Message %s is missing required fields: %s' % (
+        self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors())))
+  return self.SerializePartialToString(**kwargs)
+
+
+
+def SetInParent(self) +
+
+

Sets the _cached_byte_size_dirty bit to true, +and propagates this to our listener iff this was a state change.

+
+ +Expand source code + +
def Modified(self):
+  """Sets the _cached_byte_size_dirty bit to true,
+  and propagates this to our listener iff this was a state change.
+  """
+
+  # Note:  Some callers check _cached_byte_size_dirty before calling
+  #   _Modified() as an extra optimization.  So, if this method is ever
+  #   changed such that it does stuff even when _cached_byte_size_dirty is
+  #   already true, the callers need to be updated.
+  if not self._cached_byte_size_dirty:
+    self._cached_byte_size_dirty = True
+    self._listener_for_children.dirty = True
+    self._is_present_in_parent = True
+    self._listener.Modified()
+
+
+
+def UnknownFields(self) +
+
+
+
+ +Expand source code + +
def _UnknownFields(self):
+  if self._unknown_field_set is None:  # pylint: disable=protected-access
+    # pylint: disable=protected-access
+    self._unknown_field_set = containers.UnknownFieldSet()
+  return self._unknown_field_set    # pylint: disable=protected-access
+
+
+
+def WhichOneof(self, oneof_name) +
+
+

Returns the name of the currently set field inside a oneof, or None.

+
+ +Expand source code + +
def WhichOneof(self, oneof_name):
+  """Returns the name of the currently set field inside a oneof, or None."""
+  try:
+    field = message_descriptor.oneofs_by_name[oneof_name]
+  except KeyError:
+    raise ValueError(
+        'Protocol message has no oneof "%s" field.' % oneof_name)
+
+  nested_field = self._oneofs.get(field, None)
+  if nested_field is not None and self.HasField(nested_field.name):
+    return nested_field.name
+  else:
+    return None
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_mesh_interface.html b/docs/meshtastic/tests/test_mesh_interface.html new file mode 100644 index 0000000..29ba4d6 --- /dev/null +++ b/docs/meshtastic/tests/test_mesh_interface.html @@ -0,0 +1,117 @@ + + + + + + +meshtastic.tests.test_mesh_interface API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_mesh_interface

+
+
+

Meshtastic unit tests for mesh_interface.py

+
+ +Expand source code + +
"""Meshtastic unit tests for mesh_interface.py"""
+
+import re
+
+import pytest
+
+from ..mesh_interface import MeshInterface
+
+
+@pytest.mark.unit
+def test_MeshInterface(capsys):
+    """Test that we can instantiate a MeshInterface"""
+    iface = MeshInterface(noProto=True)
+    iface.showInfo()
+    iface.localNode.showInfo()
+    iface.showNodes()
+    iface.close()
+    out, err = capsys.readouterr()
+    assert re.search(r'Owner: None \(None\)', out, re.MULTILINE)
+    assert re.search(r'Nodes', out, re.MULTILINE)
+    assert re.search(r'Preferences', out, re.MULTILINE)
+    assert re.search(r'Channels', out, re.MULTILINE)
+    assert re.search(r'Primary channel URL', out, re.MULTILINE)
+    assert err == ''
+
+
+
+
+
+
+
+

Functions

+
+
+def test_MeshInterface(capsys) +
+
+

Test that we can instantiate a MeshInterface

+
+ +Expand source code + +
@pytest.mark.unit
+def test_MeshInterface(capsys):
+    """Test that we can instantiate a MeshInterface"""
+    iface = MeshInterface(noProto=True)
+    iface.showInfo()
+    iface.localNode.showInfo()
+    iface.showNodes()
+    iface.close()
+    out, err = capsys.readouterr()
+    assert re.search(r'Owner: None \(None\)', out, re.MULTILINE)
+    assert re.search(r'Nodes', out, re.MULTILINE)
+    assert re.search(r'Preferences', out, re.MULTILINE)
+    assert re.search(r'Channels', out, re.MULTILINE)
+    assert re.search(r'Primary channel URL', out, re.MULTILINE)
+    assert err == ''
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_node.html b/docs/meshtastic/tests/test_node.html new file mode 100644 index 0000000..7574dca --- /dev/null +++ b/docs/meshtastic/tests/test_node.html @@ -0,0 +1,951 @@ + + + + + + +meshtastic.tests.test_node API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_node

+
+
+

Meshtastic unit tests for node.py

+
+ +Expand source code + +
"""Meshtastic unit tests for node.py"""
+
+import re
+
+from unittest.mock import patch, MagicMock
+import pytest
+
+from ..node import Node
+from ..serial_interface import SerialInterface
+from ..admin_pb2 import AdminMessage
+
+
+@pytest.mark.unit
+def test_node(capsys):
+    """Test that we can instantiate a Node"""
+    anode = Node('foo', 'bar')
+    anode.showChannels()
+    anode.showInfo()
+    out, err = capsys.readouterr()
+    assert re.search(r'Preferences', out)
+    assert re.search(r'Channels', out)
+    assert re.search(r'Primary channel URL', out)
+    assert err == ''
+
+
+@pytest.mark.unit
+def test_node_reqquestConfig():
+    """Test run requestConfig"""
+    iface = MagicMock(autospec=SerialInterface)
+    amesg = MagicMock(autospec=AdminMessage)
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with patch('meshtastic.admin_pb2.AdminMessage', return_value=amesg):
+            anode = Node(mo, 'bar')
+            anode.requestConfig()
+
+
+
+
+
+
+
+

Functions

+
+
+def test_node(capsys) +
+
+

Test that we can instantiate a Node

+
+ +Expand source code + +
@pytest.mark.unit
+def test_node(capsys):
+    """Test that we can instantiate a Node"""
+    anode = Node('foo', 'bar')
+    anode.showChannels()
+    anode.showInfo()
+    out, err = capsys.readouterr()
+    assert re.search(r'Preferences', out)
+    assert re.search(r'Channels', out)
+    assert re.search(r'Primary channel URL', out)
+    assert err == ''
+
+
+
+def test_node_reqquestConfig() +
+
+

Test run requestConfig

+
+ +Expand source code + +
@pytest.mark.unit
+def test_node_reqquestConfig():
+    """Test run requestConfig"""
+    iface = MagicMock(autospec=SerialInterface)
+    amesg = MagicMock(autospec=AdminMessage)
+    with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
+        with patch('meshtastic.admin_pb2.AdminMessage', return_value=amesg):
+            anode = Node(mo, 'bar')
+            anode.requestConfig()
+
+
+
+
+
+

Classes

+
+
+class AdminMessage +(**kwargs) +
+
+

Abstract base class for protocol messages.

+

Protocol message classes are almost always generated by the protocol +compiler. +These generated types subclass Message and implement the methods +shown below.

+

Ancestors

+
    +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var CONFIRM_SET_CHANNEL_FIELD_NUMBER
+
+
+
+
var CONFIRM_SET_RADIO_FIELD_NUMBER
+
+
+
+
var DESCRIPTOR
+
+
+
+
var EXIT_SIMULATOR_FIELD_NUMBER
+
+
+
+
var GET_CHANNEL_REQUEST_FIELD_NUMBER
+
+
+
+
var GET_CHANNEL_RESPONSE_FIELD_NUMBER
+
+
+
+
var GET_RADIO_REQUEST_FIELD_NUMBER
+
+
+
+
var GET_RADIO_RESPONSE_FIELD_NUMBER
+
+
+
+
var REBOOT_SECONDS_FIELD_NUMBER
+
+
+
+
var SET_CHANNEL_FIELD_NUMBER
+
+
+
+
var SET_OWNER_FIELD_NUMBER
+
+
+
+
var SET_RADIO_FIELD_NUMBER
+
+
+
+
+

Static methods

+
+
+def FromString(s) +
+
+
+
+ +Expand source code + +
def FromString(s):
+  message = cls()
+  message.MergeFromString(s)
+  return message
+
+
+
+def RegisterExtension(extension_handle) +
+
+
+
+ +Expand source code + +
def RegisterExtension(extension_handle):
+  extension_handle.containing_type = cls.DESCRIPTOR
+  # TODO(amauryfa): Use cls.MESSAGE_FACTORY.pool when available.
+  # pylint: disable=protected-access
+  cls.DESCRIPTOR.file.pool._AddExtensionDescriptor(extension_handle)
+  _AttachFieldHelpers(cls, extension_handle)
+
+
+
+

Instance variables

+
+
var confirm_set_channel
+
+

Getter for confirm_set_channel.

+
+ +Expand source code + +
def getter(self):
+  # TODO(protobuf-team): This may be broken since there may not be
+  # default_value.  Combine with has_default_value somehow.
+  return self._fields.get(field, default_value)
+
+
+
var confirm_set_radio
+
+

Getter for confirm_set_radio.

+
+ +Expand source code + +
def getter(self):
+  # TODO(protobuf-team): This may be broken since there may not be
+  # default_value.  Combine with has_default_value somehow.
+  return self._fields.get(field, default_value)
+
+
+
var exit_simulator
+
+

Getter for exit_simulator.

+
+ +Expand source code + +
def getter(self):
+  # TODO(protobuf-team): This may be broken since there may not be
+  # default_value.  Combine with has_default_value somehow.
+  return self._fields.get(field, default_value)
+
+
+
var get_channel_request
+
+

Getter for get_channel_request.

+
+ +Expand source code + +
def getter(self):
+  # TODO(protobuf-team): This may be broken since there may not be
+  # default_value.  Combine with has_default_value somehow.
+  return self._fields.get(field, default_value)
+
+
+
var get_channel_response
+
+

Getter for get_channel_response.

+
+ +Expand source code + +
def getter(self):
+  field_value = self._fields.get(field)
+  if field_value is None:
+    # Construct a new object to represent this field.
+    field_value = field._default_constructor(self)
+
+    # Atomically check if another thread has preempted us and, if not, swap
+    # in the new object we just created.  If someone has preempted us, we
+    # take that object and discard ours.
+    # WARNING:  We are relying on setdefault() being atomic.  This is true
+    #   in CPython but we haven't investigated others.  This warning appears
+    #   in several other locations in this file.
+    field_value = self._fields.setdefault(field, field_value)
+  return field_value
+
+
+
var get_radio_request
+
+

Getter for get_radio_request.

+
+ +Expand source code + +
def getter(self):
+  # TODO(protobuf-team): This may be broken since there may not be
+  # default_value.  Combine with has_default_value somehow.
+  return self._fields.get(field, default_value)
+
+
+
var get_radio_response
+
+

Getter for get_radio_response.

+
+ +Expand source code + +
def getter(self):
+  field_value = self._fields.get(field)
+  if field_value is None:
+    # Construct a new object to represent this field.
+    field_value = field._default_constructor(self)
+
+    # Atomically check if another thread has preempted us and, if not, swap
+    # in the new object we just created.  If someone has preempted us, we
+    # take that object and discard ours.
+    # WARNING:  We are relying on setdefault() being atomic.  This is true
+    #   in CPython but we haven't investigated others.  This warning appears
+    #   in several other locations in this file.
+    field_value = self._fields.setdefault(field, field_value)
+  return field_value
+
+
+
var reboot_seconds
+
+

Getter for reboot_seconds.

+
+ +Expand source code + +
def getter(self):
+  # TODO(protobuf-team): This may be broken since there may not be
+  # default_value.  Combine with has_default_value somehow.
+  return self._fields.get(field, default_value)
+
+
+
var set_channel
+
+

Getter for set_channel.

+
+ +Expand source code + +
def getter(self):
+  field_value = self._fields.get(field)
+  if field_value is None:
+    # Construct a new object to represent this field.
+    field_value = field._default_constructor(self)
+
+    # Atomically check if another thread has preempted us and, if not, swap
+    # in the new object we just created.  If someone has preempted us, we
+    # take that object and discard ours.
+    # WARNING:  We are relying on setdefault() being atomic.  This is true
+    #   in CPython but we haven't investigated others.  This warning appears
+    #   in several other locations in this file.
+    field_value = self._fields.setdefault(field, field_value)
+  return field_value
+
+
+
var set_owner
+
+

Getter for set_owner.

+
+ +Expand source code + +
def getter(self):
+  field_value = self._fields.get(field)
+  if field_value is None:
+    # Construct a new object to represent this field.
+    field_value = field._default_constructor(self)
+
+    # Atomically check if another thread has preempted us and, if not, swap
+    # in the new object we just created.  If someone has preempted us, we
+    # take that object and discard ours.
+    # WARNING:  We are relying on setdefault() being atomic.  This is true
+    #   in CPython but we haven't investigated others.  This warning appears
+    #   in several other locations in this file.
+    field_value = self._fields.setdefault(field, field_value)
+  return field_value
+
+
+
var set_radio
+
+

Getter for set_radio.

+
+ +Expand source code + +
def getter(self):
+  field_value = self._fields.get(field)
+  if field_value is None:
+    # Construct a new object to represent this field.
+    field_value = field._default_constructor(self)
+
+    # Atomically check if another thread has preempted us and, if not, swap
+    # in the new object we just created.  If someone has preempted us, we
+    # take that object and discard ours.
+    # WARNING:  We are relying on setdefault() being atomic.  This is true
+    #   in CPython but we haven't investigated others.  This warning appears
+    #   in several other locations in this file.
+    field_value = self._fields.setdefault(field, field_value)
+  return field_value
+
+
+
+

Methods

+
+
+def ByteSize(self) +
+
+
+
+ +Expand source code + +
def ByteSize(self):
+  if not self._cached_byte_size_dirty:
+    return self._cached_byte_size
+
+  size = 0
+  descriptor = self.DESCRIPTOR
+  if descriptor.GetOptions().map_entry:
+    # Fields of map entry should always be serialized.
+    size = descriptor.fields_by_name['key']._sizer(self.key)
+    size += descriptor.fields_by_name['value']._sizer(self.value)
+  else:
+    for field_descriptor, field_value in self.ListFields():
+      size += field_descriptor._sizer(field_value)
+    for tag_bytes, value_bytes in self._unknown_fields:
+      size += len(tag_bytes) + len(value_bytes)
+
+  self._cached_byte_size = size
+  self._cached_byte_size_dirty = False
+  self._listener_for_children.dirty = False
+  return size
+
+
+
+def Clear(self) +
+
+
+
+ +Expand source code + +
def _Clear(self):
+  # Clear fields.
+  self._fields = {}
+  self._unknown_fields = ()
+  # pylint: disable=protected-access
+  if self._unknown_field_set is not None:
+    self._unknown_field_set._clear()
+    self._unknown_field_set = None
+
+  self._oneofs = {}
+  self._Modified()
+
+
+
+def ClearField(self, field_name) +
+
+
+
+ +Expand source code + +
def ClearField(self, field_name):
+  try:
+    field = message_descriptor.fields_by_name[field_name]
+  except KeyError:
+    try:
+      field = message_descriptor.oneofs_by_name[field_name]
+      if field in self._oneofs:
+        field = self._oneofs[field]
+      else:
+        return
+    except KeyError:
+      raise ValueError('Protocol message %s has no "%s" field.' %
+                       (message_descriptor.name, field_name))
+
+  if field in self._fields:
+    # To match the C++ implementation, we need to invalidate iterators
+    # for map fields when ClearField() happens.
+    if hasattr(self._fields[field], 'InvalidateIterators'):
+      self._fields[field].InvalidateIterators()
+
+    # Note:  If the field is a sub-message, its listener will still point
+    #   at us.  That's fine, because the worst than can happen is that it
+    #   will call _Modified() and invalidate our byte size.  Big deal.
+    del self._fields[field]
+
+    if self._oneofs.get(field.containing_oneof, None) is field:
+      del self._oneofs[field.containing_oneof]
+
+  # Always call _Modified() -- even if nothing was changed, this is
+  # a mutating method, and thus calling it should cause the field to become
+  # present in the parent message.
+  self._Modified()
+
+
+
+def DiscardUnknownFields(self) +
+
+
+
+ +Expand source code + +
def _DiscardUnknownFields(self):
+  self._unknown_fields = []
+  self._unknown_field_set = None      # pylint: disable=protected-access
+  for field, value in self.ListFields():
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      if _IsMapField(field):
+        if _IsMessageMapField(field):
+          for key in value:
+            value[key].DiscardUnknownFields()
+      elif field.label == _FieldDescriptor.LABEL_REPEATED:
+        for sub_message in value:
+          sub_message.DiscardUnknownFields()
+      else:
+        value.DiscardUnknownFields()
+
+
+
+def FindInitializationErrors(self) +
+
+

Finds required fields which are not initialized.

+

Returns

+

A list of strings. +Each string is a path to an uninitialized field from +the top-level message, e.g. "foo.bar[5].baz".

+
+ +Expand source code + +
def FindInitializationErrors(self):
+  """Finds required fields which are not initialized.
+
+  Returns:
+    A list of strings.  Each string is a path to an uninitialized field from
+    the top-level message, e.g. "foo.bar[5].baz".
+  """
+
+  errors = []  # simplify things
+
+  for field in required_fields:
+    if not self.HasField(field.name):
+      errors.append(field.name)
+
+  for field, value in self.ListFields():
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      if field.is_extension:
+        name = '(%s)' % field.full_name
+      else:
+        name = field.name
+
+      if _IsMapField(field):
+        if _IsMessageMapField(field):
+          for key in value:
+            element = value[key]
+            prefix = '%s[%s].' % (name, key)
+            sub_errors = element.FindInitializationErrors()
+            errors += [prefix + error for error in sub_errors]
+        else:
+          # ScalarMaps can't have any initialization errors.
+          pass
+      elif field.label == _FieldDescriptor.LABEL_REPEATED:
+        for i in range(len(value)):
+          element = value[i]
+          prefix = '%s[%d].' % (name, i)
+          sub_errors = element.FindInitializationErrors()
+          errors += [prefix + error for error in sub_errors]
+      else:
+        prefix = name + '.'
+        sub_errors = value.FindInitializationErrors()
+        errors += [prefix + error for error in sub_errors]
+
+  return errors
+
+
+
+def HasField(self, field_name) +
+
+
+
+ +Expand source code + +
def HasField(self, field_name):
+  try:
+    field = hassable_fields[field_name]
+  except KeyError:
+    raise ValueError(error_msg % (message_descriptor.full_name, field_name))
+
+  if isinstance(field, descriptor_mod.OneofDescriptor):
+    try:
+      return HasField(self, self._oneofs[field].name)
+    except KeyError:
+      return False
+  else:
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      value = self._fields.get(field)
+      return value is not None and value._is_present_in_parent
+    else:
+      return field in self._fields
+
+
+
+def IsInitialized(self, errors=None) +
+
+

Checks if all required fields of a message are set.

+

Args

+
+
errors
+
A list which, if provided, will be populated with the field +paths of all missing required fields.
+
+

Returns

+

True iff the specified message has all required fields set.

+
+ +Expand source code + +
def IsInitialized(self, errors=None):
+  """Checks if all required fields of a message are set.
+
+  Args:
+    errors:  A list which, if provided, will be populated with the field
+             paths of all missing required fields.
+
+  Returns:
+    True iff the specified message has all required fields set.
+  """
+
+  # Performance is critical so we avoid HasField() and ListFields().
+
+  for field in required_fields:
+    if (field not in self._fields or
+        (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and
+         not self._fields[field]._is_present_in_parent)):
+      if errors is not None:
+        errors.extend(self.FindInitializationErrors())
+      return False
+
+  for field, value in list(self._fields.items()):  # dict can change size!
+    if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE:
+      if field.label == _FieldDescriptor.LABEL_REPEATED:
+        if (field.message_type.has_options and
+            field.message_type.GetOptions().map_entry):
+          continue
+        for element in value:
+          if not element.IsInitialized():
+            if errors is not None:
+              errors.extend(self.FindInitializationErrors())
+            return False
+      elif value._is_present_in_parent and not value.IsInitialized():
+        if errors is not None:
+          errors.extend(self.FindInitializationErrors())
+        return False
+
+  return True
+
+
+
+def ListFields(self) +
+
+
+
+ +Expand source code + +
def ListFields(self):
+  all_fields = [item for item in self._fields.items() if _IsPresent(item)]
+  all_fields.sort(key = lambda item: item[0].number)
+  return all_fields
+
+
+
+def MergeFrom(self, msg) +
+
+
+
+ +Expand source code + +
def MergeFrom(self, msg):
+  if not isinstance(msg, cls):
+    raise TypeError(
+        'Parameter to MergeFrom() must be instance of same class: '
+        'expected %s got %s.' % (_FullyQualifiedClassName(cls),
+                                 _FullyQualifiedClassName(msg.__class__)))
+
+  assert msg is not self
+  self._Modified()
+
+  fields = self._fields
+
+  for field, value in msg._fields.items():
+    if field.label == LABEL_REPEATED:
+      field_value = fields.get(field)
+      if field_value is None:
+        # Construct a new object to represent this field.
+        field_value = field._default_constructor(self)
+        fields[field] = field_value
+      field_value.MergeFrom(value)
+    elif field.cpp_type == CPPTYPE_MESSAGE:
+      if value._is_present_in_parent:
+        field_value = fields.get(field)
+        if field_value is None:
+          # Construct a new object to represent this field.
+          field_value = field._default_constructor(self)
+          fields[field] = field_value
+        field_value.MergeFrom(value)
+    else:
+      self._fields[field] = value
+      if field.containing_oneof:
+        self._UpdateOneofState(field)
+
+  if msg._unknown_fields:
+    if not self._unknown_fields:
+      self._unknown_fields = []
+    self._unknown_fields.extend(msg._unknown_fields)
+    # pylint: disable=protected-access
+    if self._unknown_field_set is None:
+      self._unknown_field_set = containers.UnknownFieldSet()
+    self._unknown_field_set._extend(msg._unknown_field_set)
+
+
+
+def MergeFromString(self, serialized) +
+
+
+
+ +Expand source code + +
def MergeFromString(self, serialized):
+  serialized = memoryview(serialized)
+  length = len(serialized)
+  try:
+    if self._InternalParse(serialized, 0, length) != length:
+      # The only reason _InternalParse would return early is if it
+      # encountered an end-group tag.
+      raise message_mod.DecodeError('Unexpected end-group tag.')
+  except (IndexError, TypeError):
+    # Now ord(buf[p:p+1]) == ord('') gets TypeError.
+    raise message_mod.DecodeError('Truncated message.')
+  except struct.error as e:
+    raise message_mod.DecodeError(e)
+  return length   # Return this for legacy reasons.
+
+
+
+def SerializePartialToString(self, **kwargs) +
+
+
+
+ +Expand source code + +
def SerializePartialToString(self, **kwargs):
+  out = BytesIO()
+  self._InternalSerialize(out.write, **kwargs)
+  return out.getvalue()
+
+
+
+def SerializeToString(self, **kwargs) +
+
+
+
+ +Expand source code + +
def SerializeToString(self, **kwargs):
+  # Check if the message has all of its required fields set.
+  if not self.IsInitialized():
+    raise message_mod.EncodeError(
+        'Message %s is missing required fields: %s' % (
+        self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors())))
+  return self.SerializePartialToString(**kwargs)
+
+
+
+def SetInParent(self) +
+
+

Sets the _cached_byte_size_dirty bit to true, +and propagates this to our listener iff this was a state change.

+
+ +Expand source code + +
def Modified(self):
+  """Sets the _cached_byte_size_dirty bit to true,
+  and propagates this to our listener iff this was a state change.
+  """
+
+  # Note:  Some callers check _cached_byte_size_dirty before calling
+  #   _Modified() as an extra optimization.  So, if this method is ever
+  #   changed such that it does stuff even when _cached_byte_size_dirty is
+  #   already true, the callers need to be updated.
+  if not self._cached_byte_size_dirty:
+    self._cached_byte_size_dirty = True
+    self._listener_for_children.dirty = True
+    self._is_present_in_parent = True
+    self._listener.Modified()
+
+
+
+def UnknownFields(self) +
+
+
+
+ +Expand source code + +
def _UnknownFields(self):
+  if self._unknown_field_set is None:  # pylint: disable=protected-access
+    # pylint: disable=protected-access
+    self._unknown_field_set = containers.UnknownFieldSet()
+  return self._unknown_field_set    # pylint: disable=protected-access
+
+
+
+def WhichOneof(self, oneof_name) +
+
+

Returns the name of the currently set field inside a oneof, or None.

+
+ +Expand source code + +
def WhichOneof(self, oneof_name):
+  """Returns the name of the currently set field inside a oneof, or None."""
+  try:
+    field = message_descriptor.oneofs_by_name[oneof_name]
+  except KeyError:
+    raise ValueError(
+        'Protocol message has no oneof "%s" field.' % oneof_name)
+
+  nested_field = self._oneofs.get(field, None)
+  if nested_field is not None and self.HasField(nested_field.name):
+    return nested_field.name
+  else:
+    return None
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_serial_interface.html b/docs/meshtastic/tests/test_serial_interface.html new file mode 100644 index 0000000..f3c8981 --- /dev/null +++ b/docs/meshtastic/tests/test_serial_interface.html @@ -0,0 +1,186 @@ + + + + + + +meshtastic.tests.test_serial_interface API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_serial_interface

+
+
+

Meshtastic unit tests for serial_interface.py

+
+ +Expand source code + +
"""Meshtastic unit tests for serial_interface.py"""
+
+import re
+
+
+from unittest.mock import patch
+import pytest
+
+from ..serial_interface import SerialInterface
+
+@pytest.mark.unit
+@patch('serial.Serial')
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyUSBfake'])
+def test_SerialInterface_single_port(mocked_findPorts, mocked_serial):
+    """Test that we can instantiate a SerialInterface with a single port"""
+    iface = SerialInterface(noProto=True)
+    iface.showInfo()
+    iface.localNode.showInfo()
+    iface.close()
+    mocked_findPorts.assert_called()
+    mocked_serial.assert_called()
+
+
+@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=[])
+def test_SerialInterface_no_ports(mocked_findPorts, capsys):
+    """Test that we can instantiate a SerialInterface with no ports"""
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        SerialInterface(noProto=True)
+    mocked_findPorts.assert_called()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    out, err = capsys.readouterr()
+    assert re.search(r'Warning: No Meshtastic devices detected', out, re.MULTILINE)
+    assert err == ''
+
+
+@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyUSBfake1', '/dev/ttyUSBfake2'])
+def test_SerialInterface_multiple_ports(mocked_findPorts, capsys):
+    """Test that we can instantiate a SerialInterface with two ports"""
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        SerialInterface(noProto=True)
+    mocked_findPorts.assert_called()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    out, err = capsys.readouterr()
+    assert re.search(r'Warning: Multiple serial ports were detected', out, re.MULTILINE)
+    assert err == ''
+
+
+
+
+
+
+
+

Functions

+
+
+def test_SerialInterface_multiple_ports(mocked_findPorts, capsys) +
+
+

Test that we can instantiate a SerialInterface with two ports

+
+ +Expand source code + +
@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyUSBfake1', '/dev/ttyUSBfake2'])
+def test_SerialInterface_multiple_ports(mocked_findPorts, capsys):
+    """Test that we can instantiate a SerialInterface with two ports"""
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        SerialInterface(noProto=True)
+    mocked_findPorts.assert_called()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    out, err = capsys.readouterr()
+    assert re.search(r'Warning: Multiple serial ports were detected', out, re.MULTILINE)
+    assert err == ''
+
+
+
+def test_SerialInterface_no_ports(mocked_findPorts, capsys) +
+
+

Test that we can instantiate a SerialInterface with no ports

+
+ +Expand source code + +
@pytest.mark.unit
+@patch('meshtastic.util.findPorts', return_value=[])
+def test_SerialInterface_no_ports(mocked_findPorts, capsys):
+    """Test that we can instantiate a SerialInterface with no ports"""
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        SerialInterface(noProto=True)
+    mocked_findPorts.assert_called()
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+    out, err = capsys.readouterr()
+    assert re.search(r'Warning: No Meshtastic devices detected', out, re.MULTILINE)
+    assert err == ''
+
+
+
+def test_SerialInterface_single_port(mocked_findPorts, mocked_serial) +
+
+

Test that we can instantiate a SerialInterface with a single port

+
+ +Expand source code + +
@pytest.mark.unit
+@patch('serial.Serial')
+@patch('meshtastic.util.findPorts', return_value=['/dev/ttyUSBfake'])
+def test_SerialInterface_single_port(mocked_findPorts, mocked_serial):
+    """Test that we can instantiate a SerialInterface with a single port"""
+    iface = SerialInterface(noProto=True)
+    iface.showInfo()
+    iface.localNode.showInfo()
+    iface.close()
+    mocked_findPorts.assert_called()
+    mocked_serial.assert_called()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_smoke1.html b/docs/meshtastic/tests/test_smoke1.html new file mode 100644 index 0000000..67e3022 --- /dev/null +++ b/docs/meshtastic/tests/test_smoke1.html @@ -0,0 +1,1822 @@ + + + + + + +meshtastic.tests.test_smoke1 API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_smoke1

+
+
+

Meshtastic smoke tests with a single device via USB

+
+ +Expand source code + +
"""Meshtastic smoke tests with a single device via USB"""
+import re
+import subprocess
+import time
+import os
+
+# Do not like using hard coded sleeps, but it probably makes
+# sense to pause for the radio at apprpriate times
+import pytest
+
+from ..util import findPorts
+
+# seconds to pause after running a meshtastic command
+PAUSE_AFTER_COMMAND = 2
+PAUSE_AFTER_REBOOT = 7
+
+
+@pytest.mark.smoke1
+def test_smoke1_reboot():
+    """Test reboot"""
+    return_value, _ = subprocess.getstatusoutput('meshtastic --reboot')
+    assert return_value == 0
+    # pause for the radio to reset (10 seconds for the pause, and a few more seconds to be back up)
+    time.sleep(18)
+
+
+@pytest.mark.smoke1
+def test_smoke1_info():
+    """Test --info"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Owner', out, re.MULTILINE)
+    assert re.search(r'^My info', out, re.MULTILINE)
+    assert re.search(r'^Nodes in mesh', out, re.MULTILINE)
+    assert re.search(r'^Preferences', out, re.MULTILINE)
+    assert re.search(r'^Channels', out, re.MULTILINE)
+    assert re.search(r'^  PRIMARY', out, re.MULTILINE)
+    assert re.search(r'^Primary channel URL', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_sendping():
+    """Test --sendping"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --sendping')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Sending ping message', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_get_with_invalid_setting():
+    """Test '--get a_bad_setting'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --get a_bad_setting')
+    assert re.search(r'Choices in sorted order', out)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_set_with_invalid_setting():
+    """Test '--set a_bad_setting'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set a_bad_setting foo')
+    assert re.search(r'Choices in sorted order', out)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_ch_set_with_invalid_settingpatch_find_ports():
+    """Test '--ch-set with a_bad_setting'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set invalid_setting foo --ch-index 0')
+    assert re.search(r'Choices in sorted order', out)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_pos_fields():
+    """Test --pos-fields (with some values POS_ALTITUDE POS_ALT_MSL POS_BATTERY)"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --pos-fields POS_ALTITUDE POS_ALT_MSL POS_BATTERY')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Setting position fields to 35', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --pos-fields')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'POS_ALTITUDE', out, re.MULTILINE)
+    assert re.search(r'POS_ALT_MSL', out, re.MULTILINE)
+    assert re.search(r'POS_BATTERY', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_test_with_arg_but_no_hardware():
+    """Test --test
+       Note: Since only one device is connected, it will not do much.
+    """
+    return_value, out = subprocess.getstatusoutput('meshtastic --test')
+    assert re.search(r'^Warning: Must have at least two devices', out, re.MULTILINE)
+    assert return_value == 1
+
+
+@pytest.mark.smoke1
+def test_smoke1_debug():
+    """Test --debug"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info --debug')
+    assert re.search(r'^Owner', out, re.MULTILINE)
+    assert re.search(r'^DEBUG:root', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_seriallog_to_file():
+    """Test --seriallog to a file creates a file"""
+    filename = 'tmpoutput.txt'
+    if os.path.exists(f"{filename}"):
+        os.remove(f"{filename}")
+    return_value, _ = subprocess.getstatusoutput(f'meshtastic --info --seriallog {filename}')
+    assert os.path.exists(f"{filename}")
+    assert return_value == 0
+    os.remove(f"{filename}")
+
+
+@pytest.mark.smoke1
+def test_smoke1_qr():
+    """Test --qr"""
+    filename = 'tmpqr'
+    if os.path.exists(f"{filename}"):
+        os.remove(f"{filename}")
+    return_value, _ = subprocess.getstatusoutput(f'meshtastic --qr > {filename}')
+    assert os.path.exists(f"{filename}")
+    # not really testing that a valid qr code is created, just that the file size
+    # is reasonably big enough for a qr code
+    assert os.stat(f"{filename}").st_size > 20000
+    assert return_value == 0
+    os.remove(f"{filename}")
+
+
+@pytest.mark.smoke1
+def test_smoke1_nodes():
+    """Test --nodes"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --nodes')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^│   N │ User', out, re.MULTILINE)
+    assert re.search(r'^│   1 │', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_send_hello():
+    """Test --sendtext hello"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --sendtext hello')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Sending text message hello to \^all', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_port():
+    """Test --port"""
+    # first, get the ports
+    ports = findPorts()
+    # hopefully there is just one
+    assert len(ports) == 1
+    port = ports[0]
+    return_value, out = subprocess.getstatusoutput(f'meshtastic --port {port} --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Owner', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_set_is_router_true():
+    """Test --set is_router true"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set is_router true')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set is_router to true', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --get is_router')
+    assert re.search(r'^is_router: True', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_set_location_info():
+    """Test --setlat, --setlon and --setalt """
+    return_value, out = subprocess.getstatusoutput('meshtastic --setlat 32.7767 --setlon -96.7970 --setalt 1337')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Fixing altitude', out, re.MULTILINE)
+    assert re.search(r'^Fixing latitude', out, re.MULTILINE)
+    assert re.search(r'^Fixing longitude', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out2 = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'1337', out2, re.MULTILINE)
+    assert re.search(r'32.7767', out2, re.MULTILINE)
+    assert re.search(r'-96.797', out2, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_set_is_router_false():
+    """Test --set is_router false"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set is_router false')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set is_router to false', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --get is_router')
+    assert re.search(r'^is_router: False', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_set_owner():
+    """Test --set-owner name"""
+    # make sure the owner is not Joe
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-owner Bob')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Setting device owner to Bob', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search(r'Owner: Joe', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-owner Joe')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Setting device owner to Joe', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'Owner: Joe', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_set_team():
+    """Test --set-team """
+    # unset the team
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-team CLEAR')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Setting team to CLEAR', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-team CYAN')
+    assert re.search(r'Setting team to CYAN', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'CYAN', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_ch_values():
+    """Test --ch-longslow, --ch-longfast, --ch-mediumslow, --ch-mediumsfast,
+       --ch-shortslow, and --ch-shortfast arguments
+    """
+    exp = {
+            '--ch-longslow': 'Bw125Cr48Sf4096',
+            # TODO: not sure why these fail thru tests, but ok manually
+            #'--ch-longfast': 'Bw31_25Cr48Sf512',
+            #'--ch-mediumslow': 'Bw250Cr46Sf2048',
+            #'--ch-mediumfast': 'Bw250Cr47Sf1024',
+            # TODO '--ch-shortslow': '?',
+            '--ch-shortfast': 'Bw500Cr45Sf128'
+          }
+
+    for key, val in exp.items():
+        print(key, val)
+        return_value, out = subprocess.getstatusoutput(f'meshtastic {key}')
+        assert re.match(r'Connected to radio', out)
+        assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+        assert return_value == 0
+        # pause for the radio (might reboot)
+        time.sleep(PAUSE_AFTER_REBOOT)
+        return_value, out = subprocess.getstatusoutput('meshtastic --info')
+        assert re.search(val, out, re.MULTILINE)
+        assert return_value == 0
+        # pause for the radio
+        time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_ch_set_name():
+    """Test --ch-set name"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search(r'MyChannel', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set name MyChannel')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set name MyChannel --ch-index 0')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set name to MyChannel', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'MyChannel', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_ch_set_downlink_and_uplink():
+    """Test -ch-set downlink_enabled X and --ch-set uplink_enabled X"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false --ch-index 0')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search(r'uplinkEnabled', out, re.MULTILINE)
+    assert not re.search(r'downlinkEnabled', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set downlink_enabled true --ch-set uplink_enabled true --ch-index 0')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set downlink_enabled to true', out, re.MULTILINE)
+    assert re.search(r'^Set uplink_enabled to true', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'uplinkEnabled', out, re.MULTILINE)
+    assert re.search(r'downlinkEnabled', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_ch_add_and_ch_del():
+    """Test --ch-add"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing')
+    assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-index 1 --ch-del')
+    assert re.search(r'Deleting channel 1', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+    # make sure the secondar channel is not there
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert not re.search(r'SECONDARY', out, re.MULTILINE)
+    assert not re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_ch_enable_and_disable():
+    """Test --ch-enable and --ch-disable"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing')
+    assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    # ensure they need to specify a --ch-index
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-disable')
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-disable --ch-index 1')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'DISABLED', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-enable --ch-index 1')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_ch_del_a_disabled_non_primary_channel():
+    """Test --ch-del will work on a disabled non-primary channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing')
+    assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    # ensure they need to specify a --ch-index
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-disable')
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert not re.search(r'DISABLED', out, re.MULTILINE)
+    assert not re.search(r'SECONDARY', out, re.MULTILINE)
+    assert not re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_attempt_to_delete_primary_channel():
+    """Test that we cannot delete the PRIMARY channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 0')
+    assert re.search(r'Warning: Cannot delete primary channel', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_attempt_to_disable_primary_channel():
+    """Test that we cannot disable the PRIMARY channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-disable --ch-index 0')
+    assert re.search(r'Warning: Cannot enable', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_attempt_to_enable_primary_channel():
+    """Test that we cannot enable the PRIMARY channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-enable --ch-index 0')
+    assert re.search(r'Warning: Cannot enable', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_ensure_ch_del_second_of_three_channels():
+    """Test that when we delete the 2nd of 3 channels, that it deletes the correct channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing1', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing2')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'testing2', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'testing2', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_ensure_ch_del_third_of_three_channels():
+    """Test that when we delete the 3rd of 3 channels, that it deletes the correct channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing1', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing2')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'testing2', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 2')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'testing1', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_ch_set_modem_config():
+    """Test --ch-set modem_config"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set modem_config Bw31_25Cr48Sf512')
+    assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search(r'Bw31_25Cr48Sf512', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set modem_config Bw31_25Cr48Sf512 --ch-index 0')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set modem_config to Bw31_25Cr48Sf512', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'Bw31_25Cr48Sf512', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_seturl_default():
+    """Test --seturl with default value"""
+    # set some channel value so we no longer have a default channel
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set name foo --ch-index 0')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    # ensure we no longer have a default primary channel
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search('CgUYAyIBAQ', out, re.MULTILINE)
+    assert return_value == 0
+    url = "https://www.meshtastic.org/d/#CgUYAyIBAQ"
+    return_value, out = subprocess.getstatusoutput(f"meshtastic --seturl {url}")
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search('CgUYAyIBAQ', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_seturl_invalid_url():
+    """Test --seturl with invalid url"""
+    # Note: This url is no longer a valid url.
+    url = "https://www.meshtastic.org/c/#GAMiENTxuzogKQdZ8Lz_q89Oab8qB0RlZmF1bHQ="
+    return_value, out = subprocess.getstatusoutput(f"meshtastic --seturl {url}")
+    assert re.match(r'Connected to radio', out)
+    assert re.search('Warning: There were no settings', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+@pytest.mark.smoke1
+def test_smoke1_configure():
+    """Test --configure"""
+    _ , out = subprocess.getstatusoutput(f"meshtastic --configure example_config.yaml")
+    assert re.match(r'Connected to radio', out)
+    assert re.search('^Setting device owner to Bob TBeam', out, re.MULTILINE)
+    assert re.search('^Fixing altitude at 304 meters', out, re.MULTILINE)
+    assert re.search('^Fixing latitude at 35.8', out, re.MULTILINE)
+    assert re.search('^Fixing longitude at -93.8', out, re.MULTILINE)
+    assert re.search('^Setting device position', out, re.MULTILINE)
+    assert re.search('^Set region to 1', out, re.MULTILINE)
+    assert re.search('^Set is_always_powered to true', out, re.MULTILINE)
+    assert re.search('^Set send_owner_interval to 2', out, re.MULTILINE)
+    assert re.search('^Set screen_on_secs to 31536000', out, re.MULTILINE)
+    assert re.search('^Set wait_bluetooth_secs to 31536000', out, re.MULTILINE)
+    assert re.search('^Writing modified preferences to device', out, re.MULTILINE)
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+
+
+@pytest.mark.smoke1
+def test_smoke1_set_ham():
+    """Test --set-ham
+       Note: Do a factory reset after this setting so it is very short-lived.
+    """
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-ham KI1234')
+    assert re.search(r'Setting HAM ID', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'Owner: KI1234', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_set_wifi_settings():
+    """Test --set wifi_ssid and --set wifi_password"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set wifi_ssid "some_ssid" --set wifi_password "temp1234"')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set wifi_ssid to some_ssid', out, re.MULTILINE)
+    assert re.search(r'^Set wifi_password to temp1234', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --get wifi_ssid --get wifi_password')
+    assert re.search(r'^wifi_ssid: some_ssid', out, re.MULTILINE)
+    assert re.search(r'^wifi_password: sekrit', out, re.MULTILINE)
+    assert return_value == 0
+
+
+@pytest.mark.smoke1
+def test_smoke1_factory_reset():
+    """Test factory reset"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set factory_reset true')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set factory_reset to true', out, re.MULTILINE)
+    assert re.search(r'^Writing modified preferences to device', out, re.MULTILINE)
+    assert return_value == 0
+    # NOTE: The radio may not be responsive after this, may need to do a manual reboot
+    # by pressing the button
+
+
+
+
+
+
+
+

Functions

+
+
+def test_ch_set_with_invalid_settingpatch_find_ports() +
+
+

Test '–ch-set with a_bad_setting'.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_ch_set_with_invalid_settingpatch_find_ports():
+    """Test '--ch-set with a_bad_setting'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set invalid_setting foo --ch-index 0')
+    assert re.search(r'Choices in sorted order', out)
+    assert return_value == 0
+
+
+
+def test_get_with_invalid_setting() +
+
+

Test '–get a_bad_setting'.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_get_with_invalid_setting():
+    """Test '--get a_bad_setting'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --get a_bad_setting')
+    assert re.search(r'Choices in sorted order', out)
+    assert return_value == 0
+
+
+
+def test_set_with_invalid_setting() +
+
+

Test '–set a_bad_setting'.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_set_with_invalid_setting():
+    """Test '--set a_bad_setting'."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set a_bad_setting foo')
+    assert re.search(r'Choices in sorted order', out)
+    assert return_value == 0
+
+
+
+def test_smoke1_attempt_to_delete_primary_channel() +
+
+

Test that we cannot delete the PRIMARY channel.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_attempt_to_delete_primary_channel():
+    """Test that we cannot delete the PRIMARY channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 0')
+    assert re.search(r'Warning: Cannot delete primary channel', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+
+def test_smoke1_attempt_to_disable_primary_channel() +
+
+

Test that we cannot disable the PRIMARY channel.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_attempt_to_disable_primary_channel():
+    """Test that we cannot disable the PRIMARY channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-disable --ch-index 0')
+    assert re.search(r'Warning: Cannot enable', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+
+def test_smoke1_attempt_to_enable_primary_channel() +
+
+

Test that we cannot enable the PRIMARY channel.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_attempt_to_enable_primary_channel():
+    """Test that we cannot enable the PRIMARY channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-enable --ch-index 0')
+    assert re.search(r'Warning: Cannot enable', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+
+def test_smoke1_ch_add_and_ch_del() +
+
+

Test –ch-add

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ch_add_and_ch_del():
+    """Test --ch-add"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing')
+    assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-index 1 --ch-del')
+    assert re.search(r'Deleting channel 1', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+    # make sure the secondar channel is not there
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert not re.search(r'SECONDARY', out, re.MULTILINE)
+    assert not re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_ch_del_a_disabled_non_primary_channel() +
+
+

Test –ch-del will work on a disabled non-primary channel.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ch_del_a_disabled_non_primary_channel():
+    """Test --ch-del will work on a disabled non-primary channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing')
+    assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    # ensure they need to specify a --ch-index
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-disable')
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert not re.search(r'DISABLED', out, re.MULTILINE)
+    assert not re.search(r'SECONDARY', out, re.MULTILINE)
+    assert not re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+
+def test_smoke1_ch_enable_and_disable() +
+
+

Test –ch-enable and –ch-disable

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ch_enable_and_disable():
+    """Test --ch-enable and --ch-disable"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing')
+    assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    # ensure they need to specify a --ch-index
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-disable')
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-disable --ch-index 1')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'DISABLED', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-enable --ch-index 1')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+ +
+

Test -ch-set downlink_enabled X and –ch-set uplink_enabled X

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ch_set_downlink_and_uplink():
+    """Test -ch-set downlink_enabled X and --ch-set uplink_enabled X"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set downlink_enabled false --ch-set uplink_enabled false --ch-index 0')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search(r'uplinkEnabled', out, re.MULTILINE)
+    assert not re.search(r'downlinkEnabled', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set downlink_enabled true --ch-set uplink_enabled true --ch-index 0')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set downlink_enabled to true', out, re.MULTILINE)
+    assert re.search(r'^Set uplink_enabled to true', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'uplinkEnabled', out, re.MULTILINE)
+    assert re.search(r'downlinkEnabled', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_ch_set_modem_config() +
+
+

Test –ch-set modem_config

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ch_set_modem_config():
+    """Test --ch-set modem_config"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set modem_config Bw31_25Cr48Sf512')
+    assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search(r'Bw31_25Cr48Sf512', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set modem_config Bw31_25Cr48Sf512 --ch-index 0')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set modem_config to Bw31_25Cr48Sf512', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'Bw31_25Cr48Sf512', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_ch_set_name() +
+
+

Test –ch-set name

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ch_set_name():
+    """Test --ch-set name"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search(r'MyChannel', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set name MyChannel')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set name MyChannel --ch-index 0')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set name to MyChannel', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'MyChannel', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_ch_values() +
+
+

Test –ch-longslow, –ch-longfast, –ch-mediumslow, –ch-mediumsfast, +–ch-shortslow, and –ch-shortfast arguments

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ch_values():
+    """Test --ch-longslow, --ch-longfast, --ch-mediumslow, --ch-mediumsfast,
+       --ch-shortslow, and --ch-shortfast arguments
+    """
+    exp = {
+            '--ch-longslow': 'Bw125Cr48Sf4096',
+            # TODO: not sure why these fail thru tests, but ok manually
+            #'--ch-longfast': 'Bw31_25Cr48Sf512',
+            #'--ch-mediumslow': 'Bw250Cr46Sf2048',
+            #'--ch-mediumfast': 'Bw250Cr47Sf1024',
+            # TODO '--ch-shortslow': '?',
+            '--ch-shortfast': 'Bw500Cr45Sf128'
+          }
+
+    for key, val in exp.items():
+        print(key, val)
+        return_value, out = subprocess.getstatusoutput(f'meshtastic {key}')
+        assert re.match(r'Connected to radio', out)
+        assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
+        assert return_value == 0
+        # pause for the radio (might reboot)
+        time.sleep(PAUSE_AFTER_REBOOT)
+        return_value, out = subprocess.getstatusoutput('meshtastic --info')
+        assert re.search(val, out, re.MULTILINE)
+        assert return_value == 0
+        # pause for the radio
+        time.sleep(PAUSE_AFTER_COMMAND)
+
+
+
+def test_smoke1_configure() +
+
+

Test –configure

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_configure():
+    """Test --configure"""
+    _ , out = subprocess.getstatusoutput(f"meshtastic --configure example_config.yaml")
+    assert re.match(r'Connected to radio', out)
+    assert re.search('^Setting device owner to Bob TBeam', out, re.MULTILINE)
+    assert re.search('^Fixing altitude at 304 meters', out, re.MULTILINE)
+    assert re.search('^Fixing latitude at 35.8', out, re.MULTILINE)
+    assert re.search('^Fixing longitude at -93.8', out, re.MULTILINE)
+    assert re.search('^Setting device position', out, re.MULTILINE)
+    assert re.search('^Set region to 1', out, re.MULTILINE)
+    assert re.search('^Set is_always_powered to true', out, re.MULTILINE)
+    assert re.search('^Set send_owner_interval to 2', out, re.MULTILINE)
+    assert re.search('^Set screen_on_secs to 31536000', out, re.MULTILINE)
+    assert re.search('^Set wait_bluetooth_secs to 31536000', out, re.MULTILINE)
+    assert re.search('^Writing modified preferences to device', out, re.MULTILINE)
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+
+
+
+def test_smoke1_debug() +
+
+

Test –debug

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_debug():
+    """Test --debug"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info --debug')
+    assert re.search(r'^Owner', out, re.MULTILINE)
+    assert re.search(r'^DEBUG:root', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_ensure_ch_del_second_of_three_channels() +
+
+

Test that when we delete the 2nd of 3 channels, that it deletes the correct channel.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ensure_ch_del_second_of_three_channels():
+    """Test that when we delete the 2nd of 3 channels, that it deletes the correct channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing1', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing2')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'testing2', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'testing2', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+
+def test_smoke1_ensure_ch_del_third_of_three_channels() +
+
+

Test that when we delete the 3rd of 3 channels, that it deletes the correct channel.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_ensure_ch_del_third_of_three_channels():
+    """Test that when we delete the 3rd of 3 channels, that it deletes the correct channel."""
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'SECONDARY', out, re.MULTILINE)
+    assert re.search(r'testing1', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-add testing2')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'testing2', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 2')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'testing1', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-del --ch-index 1')
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+
+def test_smoke1_factory_reset() +
+
+

Test factory reset

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_factory_reset():
+    """Test factory reset"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set factory_reset true')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set factory_reset to true', out, re.MULTILINE)
+    assert re.search(r'^Writing modified preferences to device', out, re.MULTILINE)
+    assert return_value == 0
+    # NOTE: The radio may not be responsive after this, may need to do a manual reboot
+    # by pressing the button
+
+
+
+def test_smoke1_info() +
+
+

Test –info

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_info():
+    """Test --info"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Owner', out, re.MULTILINE)
+    assert re.search(r'^My info', out, re.MULTILINE)
+    assert re.search(r'^Nodes in mesh', out, re.MULTILINE)
+    assert re.search(r'^Preferences', out, re.MULTILINE)
+    assert re.search(r'^Channels', out, re.MULTILINE)
+    assert re.search(r'^  PRIMARY', out, re.MULTILINE)
+    assert re.search(r'^Primary channel URL', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_nodes() +
+
+

Test –nodes

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_nodes():
+    """Test --nodes"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --nodes')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^│   N │ User', out, re.MULTILINE)
+    assert re.search(r'^│   1 │', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_port() +
+
+

Test –port

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_port():
+    """Test --port"""
+    # first, get the ports
+    ports = findPorts()
+    # hopefully there is just one
+    assert len(ports) == 1
+    port = ports[0]
+    return_value, out = subprocess.getstatusoutput(f'meshtastic --port {port} --info')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Owner', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_pos_fields() +
+
+

Test –pos-fields (with some values POS_ALTITUDE POS_ALT_MSL POS_BATTERY)

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_pos_fields():
+    """Test --pos-fields (with some values POS_ALTITUDE POS_ALT_MSL POS_BATTERY)"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --pos-fields POS_ALTITUDE POS_ALT_MSL POS_BATTERY')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Setting position fields to 35', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --pos-fields')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'POS_ALTITUDE', out, re.MULTILINE)
+    assert re.search(r'POS_ALT_MSL', out, re.MULTILINE)
+    assert re.search(r'POS_BATTERY', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_qr() +
+
+

Test –qr

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_qr():
+    """Test --qr"""
+    filename = 'tmpqr'
+    if os.path.exists(f"{filename}"):
+        os.remove(f"{filename}")
+    return_value, _ = subprocess.getstatusoutput(f'meshtastic --qr > {filename}')
+    assert os.path.exists(f"{filename}")
+    # not really testing that a valid qr code is created, just that the file size
+    # is reasonably big enough for a qr code
+    assert os.stat(f"{filename}").st_size > 20000
+    assert return_value == 0
+    os.remove(f"{filename}")
+
+
+
+def test_smoke1_reboot() +
+
+

Test reboot

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_reboot():
+    """Test reboot"""
+    return_value, _ = subprocess.getstatusoutput('meshtastic --reboot')
+    assert return_value == 0
+    # pause for the radio to reset (10 seconds for the pause, and a few more seconds to be back up)
+    time.sleep(18)
+
+
+
+def test_smoke1_send_hello() +
+
+

Test –sendtext hello

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_send_hello():
+    """Test --sendtext hello"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --sendtext hello')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Sending text message hello to \^all', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_sendping() +
+
+

Test –sendping

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_sendping():
+    """Test --sendping"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --sendping')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Sending ping message', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_seriallog_to_file() +
+
+

Test –seriallog to a file creates a file

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_seriallog_to_file():
+    """Test --seriallog to a file creates a file"""
+    filename = 'tmpoutput.txt'
+    if os.path.exists(f"{filename}"):
+        os.remove(f"{filename}")
+    return_value, _ = subprocess.getstatusoutput(f'meshtastic --info --seriallog {filename}')
+    assert os.path.exists(f"{filename}")
+    assert return_value == 0
+    os.remove(f"{filename}")
+
+
+
+def test_smoke1_set_ham() +
+
+

Test –set-ham +Note: Do a factory reset after this setting so it is very short-lived.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_set_ham():
+    """Test --set-ham
+       Note: Do a factory reset after this setting so it is very short-lived.
+    """
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-ham KI1234')
+    assert re.search(r'Setting HAM ID', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'Owner: KI1234', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_set_is_router_false() +
+
+

Test –set is_router false

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_set_is_router_false():
+    """Test --set is_router false"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set is_router false')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set is_router to false', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --get is_router')
+    assert re.search(r'^is_router: False', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_set_is_router_true() +
+
+

Test –set is_router true

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_set_is_router_true():
+    """Test --set is_router true"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set is_router true')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set is_router to true', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --get is_router')
+    assert re.search(r'^is_router: True', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_set_location_info() +
+
+

Test –setlat, –setlon and –setalt

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_set_location_info():
+    """Test --setlat, --setlon and --setalt """
+    return_value, out = subprocess.getstatusoutput('meshtastic --setlat 32.7767 --setlon -96.7970 --setalt 1337')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Fixing altitude', out, re.MULTILINE)
+    assert re.search(r'^Fixing latitude', out, re.MULTILINE)
+    assert re.search(r'^Fixing longitude', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out2 = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'1337', out2, re.MULTILINE)
+    assert re.search(r'32.7767', out2, re.MULTILINE)
+    assert re.search(r'-96.797', out2, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_set_owner() +
+
+

Test –set-owner name

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_set_owner():
+    """Test --set-owner name"""
+    # make sure the owner is not Joe
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-owner Bob')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Setting device owner to Bob', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search(r'Owner: Joe', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-owner Joe')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Setting device owner to Joe', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'Owner: Joe', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_set_team() +
+
+

Test –set-team

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_set_team():
+    """Test --set-team """
+    # unset the team
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-team CLEAR')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Setting team to CLEAR', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+    return_value, out = subprocess.getstatusoutput('meshtastic --set-team CYAN')
+    assert re.search(r'Setting team to CYAN', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_REBOOT)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'CYAN', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_set_wifi_settings() +
+
+

Test –set wifi_ssid and –set wifi_password

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_set_wifi_settings():
+    """Test --set wifi_ssid and --set wifi_password"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --set wifi_ssid "some_ssid" --set wifi_password "temp1234"')
+    assert re.match(r'Connected to radio', out)
+    assert re.search(r'^Set wifi_ssid to some_ssid', out, re.MULTILINE)
+    assert re.search(r'^Set wifi_password to temp1234', out, re.MULTILINE)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --get wifi_ssid --get wifi_password')
+    assert re.search(r'^wifi_ssid: some_ssid', out, re.MULTILINE)
+    assert re.search(r'^wifi_password: sekrit', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_seturl_default() +
+
+

Test –seturl with default value

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_seturl_default():
+    """Test --seturl with default value"""
+    # set some channel value so we no longer have a default channel
+    return_value, out = subprocess.getstatusoutput('meshtastic --ch-set name foo --ch-index 0')
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    # ensure we no longer have a default primary channel
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert not re.search('CgUYAyIBAQ', out, re.MULTILINE)
+    assert return_value == 0
+    url = "https://www.meshtastic.org/d/#CgUYAyIBAQ"
+    return_value, out = subprocess.getstatusoutput(f"meshtastic --seturl {url}")
+    assert re.match(r'Connected to radio', out)
+    assert return_value == 0
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search('CgUYAyIBAQ', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+def test_smoke1_seturl_invalid_url() +
+
+

Test –seturl with invalid url

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_seturl_invalid_url():
+    """Test --seturl with invalid url"""
+    # Note: This url is no longer a valid url.
+    url = "https://www.meshtastic.org/c/#GAMiENTxuzogKQdZ8Lz_q89Oab8qB0RlZmF1bHQ="
+    return_value, out = subprocess.getstatusoutput(f"meshtastic --seturl {url}")
+    assert re.match(r'Connected to radio', out)
+    assert re.search('Warning: There were no settings', out, re.MULTILINE)
+    assert return_value == 1
+    # pause for the radio
+    time.sleep(PAUSE_AFTER_COMMAND)
+
+
+
+def test_smoke1_test_with_arg_but_no_hardware() +
+
+

Test –test +Note: Since only one device is connected, it will not do much.

+
+ +Expand source code + +
@pytest.mark.smoke1
+def test_smoke1_test_with_arg_but_no_hardware():
+    """Test --test
+       Note: Since only one device is connected, it will not do much.
+    """
+    return_value, out = subprocess.getstatusoutput('meshtastic --test')
+    assert re.search(r'^Warning: Must have at least two devices', out, re.MULTILINE)
+    assert return_value == 1
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_smoke2.html b/docs/meshtastic/tests/test_smoke2.html new file mode 100644 index 0000000..2cfb2e7 --- /dev/null +++ b/docs/meshtastic/tests/test_smoke2.html @@ -0,0 +1,127 @@ + + + + + + +meshtastic.tests.test_smoke2 API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_smoke2

+
+
+

Meshtastic smoke tests with 2 devices connected via USB

+
+ +Expand source code + +
"""Meshtastic smoke tests with 2 devices connected via USB"""
+import re
+import subprocess
+
+import pytest
+
+
+@pytest.mark.smoke2
+def test_smoke2_info():
+    """Test --info with 2 devices connected serially"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'Warning: Multiple', out, re.MULTILINE)
+    assert return_value == 1
+
+
+@pytest.mark.smoke2
+def test_smoke2_test():
+    """Test --test"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --test')
+    assert re.search(r'Writing serial debugging', out, re.MULTILINE)
+    assert re.search(r'Ports opened', out, re.MULTILINE)
+    assert re.search(r'Running 5 tests', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+
+
+
+
+

Functions

+
+
+def test_smoke2_info() +
+
+

Test –info with 2 devices connected serially

+
+ +Expand source code + +
@pytest.mark.smoke2
+def test_smoke2_info():
+    """Test --info with 2 devices connected serially"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info')
+    assert re.search(r'Warning: Multiple', out, re.MULTILINE)
+    assert return_value == 1
+
+
+
+def test_smoke2_test() +
+
+

Test –test

+
+ +Expand source code + +
@pytest.mark.smoke2
+def test_smoke2_test():
+    """Test --test"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --test')
+    assert re.search(r'Writing serial debugging', out, re.MULTILINE)
+    assert re.search(r'Ports opened', out, re.MULTILINE)
+    assert re.search(r'Running 5 tests', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_smoke_wifi.html b/docs/meshtastic/tests/test_smoke_wifi.html new file mode 100644 index 0000000..3e2e28e --- /dev/null +++ b/docs/meshtastic/tests/test_smoke_wifi.html @@ -0,0 +1,115 @@ + + + + + + +meshtastic.tests.test_smoke_wifi API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_smoke_wifi

+
+
+

Meshtastic smoke tests a device setup with wifi.

+

Need to have run the following on an esp32 device: +meshtastic –set wifi_ssid 'foo' –set wifi_password 'sekret'

+
+ +Expand source code + +
"""Meshtastic smoke tests a device setup with wifi.
+
+   Need to have run the following on an esp32 device:
+      meshtastic --set wifi_ssid 'foo' --set wifi_password 'sekret'
+"""
+import re
+import subprocess
+
+import pytest
+
+
+@pytest.mark.smokewifi
+def test_smokewifi_info():
+    """Test --info"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info --host meshtastic.local')
+    assert re.search(r'^Owner', out, re.MULTILINE)
+    assert re.search(r'^My info', out, re.MULTILINE)
+    assert re.search(r'^Nodes in mesh', out, re.MULTILINE)
+    assert re.search(r'^Preferences', out, re.MULTILINE)
+    assert re.search(r'^Channels', out, re.MULTILINE)
+    assert re.search(r'^  PRIMARY', out, re.MULTILINE)
+    assert re.search(r'^Primary channel URL', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+
+
+
+
+

Functions

+
+
+def test_smokewifi_info() +
+
+

Test –info

+
+ +Expand source code + +
@pytest.mark.smokewifi
+def test_smokewifi_info():
+    """Test --info"""
+    return_value, out = subprocess.getstatusoutput('meshtastic --info --host meshtastic.local')
+    assert re.search(r'^Owner', out, re.MULTILINE)
+    assert re.search(r'^My info', out, re.MULTILINE)
+    assert re.search(r'^Nodes in mesh', out, re.MULTILINE)
+    assert re.search(r'^Preferences', out, re.MULTILINE)
+    assert re.search(r'^Channels', out, re.MULTILINE)
+    assert re.search(r'^  PRIMARY', out, re.MULTILINE)
+    assert re.search(r'^Primary channel URL', out, re.MULTILINE)
+    assert return_value == 0
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_stream_interface.html b/docs/meshtastic/tests/test_stream_interface.html new file mode 100644 index 0000000..5eebdad --- /dev/null +++ b/docs/meshtastic/tests/test_stream_interface.html @@ -0,0 +1,98 @@ + + + + + + +meshtastic.tests.test_stream_interface API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_stream_interface

+
+
+

Meshtastic unit tests for stream_interface.py

+
+ +Expand source code + +
"""Meshtastic unit tests for stream_interface.py"""
+
+
+import pytest
+
+from ..stream_interface import StreamInterface
+
+
+@pytest.mark.unit
+def test_StreamInterface():
+    """Test that we can instantiate a StreamInterface"""
+    with pytest.raises(Exception) as pytest_wrapped_e:
+        StreamInterface(noProto=True)
+    assert pytest_wrapped_e.type == Exception
+
+
+
+
+
+
+
+

Functions

+
+
+def test_StreamInterface() +
+
+

Test that we can instantiate a StreamInterface

+
+ +Expand source code + +
@pytest.mark.unit
+def test_StreamInterface():
+    """Test that we can instantiate a StreamInterface"""
+    with pytest.raises(Exception) as pytest_wrapped_e:
+        StreamInterface(noProto=True)
+    assert pytest_wrapped_e.type == Exception
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_tcp_interface.html b/docs/meshtastic/tests/test_tcp_interface.html new file mode 100644 index 0000000..0753c11 --- /dev/null +++ b/docs/meshtastic/tests/test_tcp_interface.html @@ -0,0 +1,120 @@ + + + + + + +meshtastic.tests.test_tcp_interface API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_tcp_interface

+
+
+

Meshtastic unit tests for tcp_interface.py

+
+ +Expand source code + +
"""Meshtastic unit tests for tcp_interface.py"""
+
+import re
+
+from unittest.mock import patch
+import pytest
+
+from ..tcp_interface import TCPInterface
+
+
+@pytest.mark.unit
+def test_TCPInterface(capsys):
+    """Test that we can instantiate a TCPInterface"""
+    with patch('socket.socket') as mock_socket:
+        iface = TCPInterface(hostname='localhost', noProto=True)
+        iface.showInfo()
+        iface.localNode.showInfo()
+        out, err = capsys.readouterr()
+        assert re.search(r'Owner: None \(None\)', out, re.MULTILINE)
+        assert re.search(r'Nodes', out, re.MULTILINE)
+        assert re.search(r'Preferences', out, re.MULTILINE)
+        assert re.search(r'Channels', out, re.MULTILINE)
+        assert re.search(r'Primary channel URL', out, re.MULTILINE)
+        assert err == ''
+        assert mock_socket.called
+        iface.close()
+
+
+
+
+
+
+
+

Functions

+
+
+def test_TCPInterface(capsys) +
+
+

Test that we can instantiate a TCPInterface

+
+ +Expand source code + +
@pytest.mark.unit
+def test_TCPInterface(capsys):
+    """Test that we can instantiate a TCPInterface"""
+    with patch('socket.socket') as mock_socket:
+        iface = TCPInterface(hostname='localhost', noProto=True)
+        iface.showInfo()
+        iface.localNode.showInfo()
+        out, err = capsys.readouterr()
+        assert re.search(r'Owner: None \(None\)', out, re.MULTILINE)
+        assert re.search(r'Nodes', out, re.MULTILINE)
+        assert re.search(r'Preferences', out, re.MULTILINE)
+        assert re.search(r'Channels', out, re.MULTILINE)
+        assert re.search(r'Primary channel URL', out, re.MULTILINE)
+        assert err == ''
+        assert mock_socket.called
+        iface.close()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tests/test_util.html b/docs/meshtastic/tests/test_util.html new file mode 100644 index 0000000..864c8b5 --- /dev/null +++ b/docs/meshtastic/tests/test_util.html @@ -0,0 +1,455 @@ + + + + + + +meshtastic.tests.test_util API documentation + + + + + + + + + + + +
+
+
+

Module meshtastic.tests.test_util

+
+
+

Meshtastic unit tests for util.py

+
+ +Expand source code + +
"""Meshtastic unit tests for util.py"""
+
+import re
+
+import pytest
+
+from meshtastic.util import fixme, stripnl, pskToString, our_exit, support_info, genPSK256, fromStr, fromPSK
+
+
+@pytest.mark.unit
+def test_genPSK256():
+    """Test genPSK256"""
+    assert genPSK256() != ''
+
+
+@pytest.mark.unit
+def test_fromStr():
+    """Test fromStr"""
+    assert fromStr('') == b''
+    assert fromStr('0x12') == b'\x12'
+    assert fromStr('t')
+    assert fromStr('T')
+    assert fromStr('true')
+    assert fromStr('True')
+    assert fromStr('yes')
+    assert fromStr('Yes')
+    assert fromStr('f') is False
+    assert fromStr('F') is False
+    assert fromStr('false') is False
+    assert fromStr('False') is False
+    assert fromStr('no') is False
+    assert fromStr('No') is False
+    assert fromStr('100.01') == 100.01
+    assert fromStr('123') == 123
+    assert fromStr('abc') == 'abc'
+
+
+@pytest.mark.unit
+def test_fromPSK():
+    """Test fromPSK"""
+    assert fromPSK('random') != ''
+    assert fromPSK('none') == b'\x00'
+    assert fromPSK('default') == b'\x01'
+    assert fromPSK('simple22') == b'\x17'
+    assert fromPSK('trash') == 'trash'
+
+
+@pytest.mark.unit
+def test_stripnl():
+    """Test stripnl"""
+    assert stripnl('') == ''
+    assert stripnl('a\n') == 'a'
+    assert stripnl(' a \n ') == 'a'
+    assert stripnl('a\nb') == 'a b'
+
+
+@pytest.mark.unit
+def test_pskToString_empty_string():
+    """Test pskToString empty string"""
+    assert pskToString('') == 'unencrypted'
+
+
+@pytest.mark.unit
+def test_pskToString_string():
+    """Test pskToString string"""
+    assert pskToString('hunter123') == 'secret'
+
+
+@pytest.mark.unit
+def test_pskToString_one_byte_zero_value():
+    """Test pskToString one byte that is value of 0"""
+    assert pskToString(bytes([0x00])) == 'unencrypted'
+
+
+@pytest.mark.unit
+def test_pskToString_one_byte_non_zero_value():
+    """Test pskToString one byte that is non-zero"""
+    assert pskToString(bytes([0x01])) == 'default'
+
+
+@pytest.mark.unit
+def test_pskToString_many_bytes():
+    """Test pskToString many bytes"""
+    assert pskToString(bytes([0x02, 0x01])) == 'secret'
+
+
+@pytest.mark.unit
+def test_pskToString_simple():
+    """Test pskToString simple"""
+    assert pskToString(bytes([0x03])) == 'simple2'
+
+
+@pytest.mark.unit
+def test_our_exit_zero_return_value():
+    """Test our_exit with a zero return value"""
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        our_exit("Warning: Some message", 0)
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+
+
+@pytest.mark.unit
+def test_our_exit_non_zero_return_value():
+    """Test our_exit with a non-zero return value"""
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        our_exit("Error: Some message", 1)
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+
+
+@pytest.mark.unit
+def test_fixme():
+    """Test fixme"""
+    with pytest.raises(Exception) as pytest_wrapped_e:
+        fixme("some exception")
+    assert pytest_wrapped_e.type == Exception
+
+
+@pytest.mark.unit
+def test_support_info(capsys):
+    """Test support_info"""
+    support_info()
+    out, err = capsys.readouterr()
+    assert re.search(r'System', out, re.MULTILINE)
+    assert re.search(r'Platform', out, re.MULTILINE)
+    assert re.search(r'Machine', out, re.MULTILINE)
+    assert re.search(r'Executable', out, re.MULTILINE)
+    assert err == ''
+
+
+
+
+
+
+
+

Functions

+
+
+def test_fixme() +
+
+

Test fixme

+
+ +Expand source code + +
@pytest.mark.unit
+def test_fixme():
+    """Test fixme"""
+    with pytest.raises(Exception) as pytest_wrapped_e:
+        fixme("some exception")
+    assert pytest_wrapped_e.type == Exception
+
+
+
+def test_fromPSK() +
+
+

Test fromPSK

+
+ +Expand source code + +
@pytest.mark.unit
+def test_fromPSK():
+    """Test fromPSK"""
+    assert fromPSK('random') != ''
+    assert fromPSK('none') == b'\x00'
+    assert fromPSK('default') == b'\x01'
+    assert fromPSK('simple22') == b'\x17'
+    assert fromPSK('trash') == 'trash'
+
+
+
+def test_fromStr() +
+
+

Test fromStr

+
+ +Expand source code + +
@pytest.mark.unit
+def test_fromStr():
+    """Test fromStr"""
+    assert fromStr('') == b''
+    assert fromStr('0x12') == b'\x12'
+    assert fromStr('t')
+    assert fromStr('T')
+    assert fromStr('true')
+    assert fromStr('True')
+    assert fromStr('yes')
+    assert fromStr('Yes')
+    assert fromStr('f') is False
+    assert fromStr('F') is False
+    assert fromStr('false') is False
+    assert fromStr('False') is False
+    assert fromStr('no') is False
+    assert fromStr('No') is False
+    assert fromStr('100.01') == 100.01
+    assert fromStr('123') == 123
+    assert fromStr('abc') == 'abc'
+
+
+
+def test_genPSK256() +
+
+

Test genPSK256

+
+ +Expand source code + +
@pytest.mark.unit
+def test_genPSK256():
+    """Test genPSK256"""
+    assert genPSK256() != ''
+
+
+
+def test_our_exit_non_zero_return_value() +
+
+

Test our_exit with a non-zero return value

+
+ +Expand source code + +
@pytest.mark.unit
+def test_our_exit_non_zero_return_value():
+    """Test our_exit with a non-zero return value"""
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        our_exit("Error: Some message", 1)
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 1
+
+
+
+def test_our_exit_zero_return_value() +
+
+

Test our_exit with a zero return value

+
+ +Expand source code + +
@pytest.mark.unit
+def test_our_exit_zero_return_value():
+    """Test our_exit with a zero return value"""
+    with pytest.raises(SystemExit) as pytest_wrapped_e:
+        our_exit("Warning: Some message", 0)
+    assert pytest_wrapped_e.type == SystemExit
+    assert pytest_wrapped_e.value.code == 0
+
+
+
+def test_pskToString_empty_string() +
+
+

Test pskToString empty string

+
+ +Expand source code + +
@pytest.mark.unit
+def test_pskToString_empty_string():
+    """Test pskToString empty string"""
+    assert pskToString('') == 'unencrypted'
+
+
+
+def test_pskToString_many_bytes() +
+
+

Test pskToString many bytes

+
+ +Expand source code + +
@pytest.mark.unit
+def test_pskToString_many_bytes():
+    """Test pskToString many bytes"""
+    assert pskToString(bytes([0x02, 0x01])) == 'secret'
+
+
+
+def test_pskToString_one_byte_non_zero_value() +
+
+

Test pskToString one byte that is non-zero

+
+ +Expand source code + +
@pytest.mark.unit
+def test_pskToString_one_byte_non_zero_value():
+    """Test pskToString one byte that is non-zero"""
+    assert pskToString(bytes([0x01])) == 'default'
+
+
+
+def test_pskToString_one_byte_zero_value() +
+
+

Test pskToString one byte that is value of 0

+
+ +Expand source code + +
@pytest.mark.unit
+def test_pskToString_one_byte_zero_value():
+    """Test pskToString one byte that is value of 0"""
+    assert pskToString(bytes([0x00])) == 'unencrypted'
+
+
+
+def test_pskToString_simple() +
+
+

Test pskToString simple

+
+ +Expand source code + +
@pytest.mark.unit
+def test_pskToString_simple():
+    """Test pskToString simple"""
+    assert pskToString(bytes([0x03])) == 'simple2'
+
+
+
+def test_pskToString_string() +
+
+

Test pskToString string

+
+ +Expand source code + +
@pytest.mark.unit
+def test_pskToString_string():
+    """Test pskToString string"""
+    assert pskToString('hunter123') == 'secret'
+
+
+
+def test_stripnl() +
+
+

Test stripnl

+
+ +Expand source code + +
@pytest.mark.unit
+def test_stripnl():
+    """Test stripnl"""
+    assert stripnl('') == ''
+    assert stripnl('a\n') == 'a'
+    assert stripnl(' a \n ') == 'a'
+    assert stripnl('a\nb') == 'a b'
+
+
+
+def test_support_info(capsys) +
+
+

Test support_info

+
+ +Expand source code + +
@pytest.mark.unit
+def test_support_info(capsys):
+    """Test support_info"""
+    support_info()
+    out, err = capsys.readouterr()
+    assert re.search(r'System', out, re.MULTILINE)
+    assert re.search(r'Platform', out, re.MULTILINE)
+    assert re.search(r'Machine', out, re.MULTILINE)
+    assert re.search(r'Executable', out, re.MULTILINE)
+    assert err == ''
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/meshtastic/tunnel.html b/docs/meshtastic/tunnel.html index 61e0224..27414d5 100644 --- a/docs/meshtastic/tunnel.html +++ b/docs/meshtastic/tunnel.html @@ -39,7 +39,7 @@ Expand source code -
""" Code for IP tunnel over a mesh
+
"""Code for IP tunnel over a mesh
 
 # Note python-pytuntap was too buggy
 # using pip3 install pytap2
@@ -59,6 +59,9 @@
 import logging
 import threading
 from pubsub import pub
+
+from pytap2 import TapDevice
+
 from . import portnums_pb2
 
 # A new non standard log level that is lower level than DEBUG
@@ -126,8 +129,8 @@ class Tunnel:
         global tunnelInstance
         tunnelInstance = self
 
-        logging.info(
-            "Starting IP to mesh tunnel (you must be root for this *pre-alpha* feature to work).  Mesh members:")
+        logging.info("Starting IP to mesh tunnel (you must be root for this *pre-alpha* "\
+                     "feature to work).  Mesh members:")
 
         pub.subscribe(onTunnelReceive, "meshtastic.receive.data.IP_TUNNEL_APP")
         myAddr = self._nodeNumToIp(self.iface.myInfo.my_node_num)
@@ -139,7 +142,6 @@ class Tunnel:
 
         logging.debug("creating TUN device with MTU=200")
         # FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data
-        from pytap2 import TapDevice
         self.tun = TapDevice(name="mesh")
         self.tun.up()
         self.tun.ifconfig(address=myAddr, netmask=netmask, mtu=200)
@@ -149,14 +151,15 @@ class Tunnel:
         self._rxThread.start()
 
     def onReceive(self, packet):
+        """onReceive"""
         p = packet["decoded"]["payload"]
         if packet["from"] == self.iface.myInfo.my_node_num:
             logging.debug("Ignoring message we sent")
         else:
             logging.debug(
                 f"Received mesh tunnel message type={type(p)} len={len(p)}")
-            # we don't really need to check for filtering here (sender should have checked), but this provides
-            # useful debug printing on types of packets received
+            # we don't really need to check for filtering here (sender should have checked),
+            # but this provides useful debug printing on types of packets received
             if not self._shouldFilterPacket(p):
                 self.tun.write(p)
 
@@ -175,8 +178,8 @@ class Tunnel:
             icmpType = p[20]
             icmpCode = p[21]
             checksum = p[22:24]
-            logging.debug(
-                f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}")
+            # pylint: disable=line-too-long
+            logging.debug(f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}")
             # reply to pings (swap src and dest but keep rest of packet unchanged)
             #pingback = p[:12]+p[16:20]+p[12:16]+p[20:]
             # tap.write(pingback)
@@ -195,14 +198,12 @@ class Tunnel:
             destport = readnet_u16(p, subheader + 2)
             if destport in tcpBlacklist:
                 ignore = True
-                logging.log(
-                    LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
+                logging.log(LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
             else:
-                logging.debug(
-                    f"forwarding tcp srcport={srcport}, destport={destport}")
+                logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}")
         else:
-            logging.warning(
-                f"forwarding unexpected protocol 0x{protocol:02x}, src={ipstr(srcaddr)}, dest={ipstr(destAddr)}")
+            logging.warning(f"forwarding unexpected protocol 0x{protocol:02x}, "\
+                             "src={ipstr(srcaddr)}, dest={ipstr(destAddr)}")
 
         return ignore
 
@@ -238,15 +239,14 @@ class Tunnel:
         """Forward the provided IP packet into the mesh"""
         nodeId = self._ipToNodeId(destAddr)
         if nodeId is not None:
-            logging.debug(
-                f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}")
+            logging.debug(f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}")
             self.iface.sendData(
                 p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck=False)
         else:
-            logging.warning(
-                f"Dropping packet because no node found for destIP={ipstr(destAddr)}")
+            logging.warning(f"Dropping packet because no node found for destIP={ipstr(destAddr)}")
 
     def close(self):
+        """Close"""
         self.tun.close()
@@ -370,8 +370,8 @@ subnet is used to construct our network number (normally 10.115.x.x)

global tunnelInstance tunnelInstance = self - logging.info( - "Starting IP to mesh tunnel (you must be root for this *pre-alpha* feature to work). Mesh members:") + logging.info("Starting IP to mesh tunnel (you must be root for this *pre-alpha* "\ + "feature to work). Mesh members:") pub.subscribe(onTunnelReceive, "meshtastic.receive.data.IP_TUNNEL_APP") myAddr = self._nodeNumToIp(self.iface.myInfo.my_node_num) @@ -383,7 +383,6 @@ subnet is used to construct our network number (normally 10.115.x.x)

logging.debug("creating TUN device with MTU=200") # FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data - from pytap2 import TapDevice self.tun = TapDevice(name="mesh") self.tun.up() self.tun.ifconfig(address=myAddr, netmask=netmask, mtu=200) @@ -393,14 +392,15 @@ subnet is used to construct our network number (normally 10.115.x.x)

self._rxThread.start() def onReceive(self, packet): + """onReceive""" p = packet["decoded"]["payload"] if packet["from"] == self.iface.myInfo.my_node_num: logging.debug("Ignoring message we sent") else: logging.debug( f"Received mesh tunnel message type={type(p)} len={len(p)}") - # we don't really need to check for filtering here (sender should have checked), but this provides - # useful debug printing on types of packets received + # we don't really need to check for filtering here (sender should have checked), + # but this provides useful debug printing on types of packets received if not self._shouldFilterPacket(p): self.tun.write(p) @@ -419,8 +419,8 @@ subnet is used to construct our network number (normally 10.115.x.x)

icmpType = p[20] icmpCode = p[21] checksum = p[22:24] - logging.debug( - f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}") + # pylint: disable=line-too-long + logging.debug(f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}") # reply to pings (swap src and dest but keep rest of packet unchanged) #pingback = p[:12]+p[16:20]+p[12:16]+p[20:] # tap.write(pingback) @@ -439,14 +439,12 @@ subnet is used to construct our network number (normally 10.115.x.x)

destport = readnet_u16(p, subheader + 2) if destport in tcpBlacklist: ignore = True - logging.log( - LOG_TRACE, f"ignoring blacklisted TCP port {destport}") + logging.log(LOG_TRACE, f"ignoring blacklisted TCP port {destport}") else: - logging.debug( - f"forwarding tcp srcport={srcport}, destport={destport}") + logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}") else: - logging.warning( - f"forwarding unexpected protocol 0x{protocol:02x}, src={ipstr(srcaddr)}, dest={ipstr(destAddr)}") + logging.warning(f"forwarding unexpected protocol 0x{protocol:02x}, "\ + "src={ipstr(srcaddr)}, dest={ipstr(destAddr)}") return ignore @@ -482,15 +480,14 @@ subnet is used to construct our network number (normally 10.115.x.x)

"""Forward the provided IP packet into the mesh""" nodeId = self._ipToNodeId(destAddr) if nodeId is not None: - logging.debug( - f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}") + logging.debug(f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}") self.iface.sendData( p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck=False) else: - logging.warning( - f"Dropping packet because no node found for destIP={ipstr(destAddr)}") + logging.warning(f"Dropping packet because no node found for destIP={ipstr(destAddr)}") def close(self): + """Close""" self.tun.close()

Methods

@@ -499,12 +496,13 @@ subnet is used to construct our network number (normally 10.115.x.x)

def close(self)
-
+

Close

Expand source code
def close(self):
+    """Close"""
     self.tun.close()
@@ -512,20 +510,21 @@ subnet is used to construct our network number (normally 10.115.x.x)

def onReceive(self, packet)
-
+

onReceive

Expand source code
def onReceive(self, packet):
+    """onReceive"""
     p = packet["decoded"]["payload"]
     if packet["from"] == self.iface.myInfo.my_node_num:
         logging.debug("Ignoring message we sent")
     else:
         logging.debug(
             f"Received mesh tunnel message type={type(p)} len={len(p)}")
-        # we don't really need to check for filtering here (sender should have checked), but this provides
-        # useful debug printing on types of packets received
+        # we don't really need to check for filtering here (sender should have checked),
+        # but this provides useful debug printing on types of packets received
         if not self._shouldFilterPacket(p):
             self.tun.write(p)
@@ -543,13 +542,11 @@ subnet is used to construct our network number (normally 10.115.x.x)

"""Forward the provided IP packet into the mesh""" nodeId = self._ipToNodeId(destAddr) if nodeId is not None: - logging.debug( - f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}") + logging.debug(f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}") self.iface.sendData( p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck=False) else: - logging.warning( - f"Dropping packet because no node found for destIP={ipstr(destAddr)}") + logging.warning(f"Dropping packet because no node found for destIP={ipstr(destAddr)}")
diff --git a/docs/meshtastic/util.html b/docs/meshtastic/util.html index f7608e2..b43de2a 100644 --- a/docs/meshtastic/util.html +++ b/docs/meshtastic/util.html @@ -27,10 +27,11 @@ Expand source code -
""" Utility functions.
+
"""Utility functions.
 """
 import traceback
 from queue import Queue
+import os
 import sys
 import time
 import platform
@@ -44,6 +45,56 @@ import pkg_resources
 blacklistVids = dict.fromkeys([0x1366])
 
 
+def genPSK256():
+    """Generate a random preshared key"""
+    return os.urandom(32)
+
+
+def fromPSK(valstr):
+    """A special version of fromStr that assumes the user is trying to set a PSK.
+    In that case we also allow "none", "default" or "random" (to have python generate one), or simpleN
+    """
+    if valstr == "random":
+        return genPSK256()
+    elif valstr == "none":
+        return bytes([0])  # Use the 'no encryption' PSK
+    elif valstr == "default":
+        return bytes([1])  # Use default channel psk
+    elif valstr.startswith("simple"):
+        # Use one of the single byte encodings
+        return bytes([int(valstr[6:]) + 1])
+    else:
+        return fromStr(valstr)
+
+
+def fromStr(valstr):
+    """Try to parse as int, float or bool (and fallback to a string as last resort)
+
+    Returns: an int, bool, float, str or byte array (for strings of hex digits)
+
+    Args:
+        valstr (string): A user provided string
+    """
+    if len(valstr) == 0:  # Treat an emptystring as an empty bytes
+        val = bytes()
+    elif valstr.startswith('0x'):
+        # if needed convert to string with asBytes.decode('utf-8')
+        val = bytes.fromhex(valstr[2:])
+    elif valstr.lower() in {"t", "true", "yes"}:
+        val = True
+    elif valstr.lower() in {"f", "false", "no"}:
+        val = False
+    else:
+        try:
+            val = int(valstr)
+        except ValueError:
+            try:
+                val = float(valstr)
+            except ValueError:
+                val = valstr  # Not a float or an int, assume string
+    return val
+
+
 def pskToString(psk: bytes):
     """Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string"""
     if len(psk) == 0:
@@ -61,13 +112,13 @@ def pskToString(psk: bytes):
 
 
 def stripnl(s):
-    """remove newlines from a string (and remove extra whitespace)"""
+    """Remove newlines from a string (and remove extra whitespace)"""
     s = str(s).replace("\n", " ")
     return ' '.join(s.split())
 
 
 def fixme(message):
-    """raise an exception for things that needs to be fixed"""
+    """Raise an exception for things that needs to be fixed"""
     raise Exception(f"FIXME: {message}")
 
 
@@ -153,7 +204,8 @@ def our_exit(message, return_value = 1):
 
 
 def support_info():
-    """Print out info that is helping in support of the cli."""
+    """Print out info that helps troubleshooting of the cli."""
+    print('')
     print('If having issues with meshtastic cli or python library')
     print('or wish to make feature requests, visit:')
     print('https://github.com/meshtastic/Meshtastic-python/issues')
@@ -162,13 +214,14 @@ def support_info():
     print('   Platform: {0}'.format(platform.platform()))
     print('   Release: {0}'.format(platform.uname().release))
     print('   Machine: {0}'.format(platform.uname().machine))
+    print('   Encoding (stdin): {0}'.format(sys.stdin.encoding))
+    print('   Encoding (stdout): {0}'.format(sys.stdout.encoding))
     print(' meshtastic: v{0}'.format(pkg_resources.require('meshtastic')[0].version))
     print(' Executable: {0}'.format(sys.argv[0]))
     print(' Python: {0} {1} {2}'.format(platform.python_version(),
           platform.python_implementation(), platform.python_compiler()))
     print('')
-    print('Please add the output from the command: meshtastic --info')
-    print('')
+ print('Please add the output from the command: meshtastic --info')
@@ -223,16 +276,100 @@ def support_info(): def fixme(message)
-

raise an exception for things that needs to be fixed

+

Raise an exception for things that needs to be fixed

Expand source code
def fixme(message):
-    """raise an exception for things that needs to be fixed"""
+    """Raise an exception for things that needs to be fixed"""
     raise Exception(f"FIXME: {message}")
+
+def fromPSK(valstr) +
+
+

A special version of fromStr that assumes the user is trying to set a PSK. +In that case we also allow "none", "default" or "random" (to have python generate one), or simpleN

+
+ +Expand source code + +
def fromPSK(valstr):
+    """A special version of fromStr that assumes the user is trying to set a PSK.
+    In that case we also allow "none", "default" or "random" (to have python generate one), or simpleN
+    """
+    if valstr == "random":
+        return genPSK256()
+    elif valstr == "none":
+        return bytes([0])  # Use the 'no encryption' PSK
+    elif valstr == "default":
+        return bytes([1])  # Use default channel psk
+    elif valstr.startswith("simple"):
+        # Use one of the single byte encodings
+        return bytes([int(valstr[6:]) + 1])
+    else:
+        return fromStr(valstr)
+
+
+
+def fromStr(valstr) +
+
+

Try to parse as int, float or bool (and fallback to a string as last resort)

+

Returns: an int, bool, float, str or byte array (for strings of hex digits)

+

Args

+
+
valstr : string
+
A user provided string
+
+
+ +Expand source code + +
def fromStr(valstr):
+    """Try to parse as int, float or bool (and fallback to a string as last resort)
+
+    Returns: an int, bool, float, str or byte array (for strings of hex digits)
+
+    Args:
+        valstr (string): A user provided string
+    """
+    if len(valstr) == 0:  # Treat an emptystring as an empty bytes
+        val = bytes()
+    elif valstr.startswith('0x'):
+        # if needed convert to string with asBytes.decode('utf-8')
+        val = bytes.fromhex(valstr[2:])
+    elif valstr.lower() in {"t", "true", "yes"}:
+        val = True
+    elif valstr.lower() in {"f", "false", "no"}:
+        val = False
+    else:
+        try:
+            val = int(valstr)
+        except ValueError:
+            try:
+                val = float(valstr)
+            except ValueError:
+                val = valstr  # Not a float or an int, assume string
+    return val
+
+
+
+def genPSK256() +
+
+

Generate a random preshared key

+
+ +Expand source code + +
def genPSK256():
+    """Generate a random preshared key"""
+    return os.urandom(32)
+
+
def our_exit(message, return_value=1)
@@ -280,13 +417,13 @@ return_value defaults to 1 (non-successful)

def stripnl(s)
-

remove newlines from a string (and remove extra whitespace)

+

Remove newlines from a string (and remove extra whitespace)

Expand source code
def stripnl(s):
-    """remove newlines from a string (and remove extra whitespace)"""
+    """Remove newlines from a string (and remove extra whitespace)"""
     s = str(s).replace("\n", " ")
     return ' '.join(s.split())
@@ -295,13 +432,14 @@ return_value defaults to 1 (non-successful)

def support_info()
-

Print out info that is helping in support of the cli.

+

Print out info that helps troubleshooting of the cli.

Expand source code
def support_info():
-    """Print out info that is helping in support of the cli."""
+    """Print out info that helps troubleshooting of the cli."""
+    print('')
     print('If having issues with meshtastic cli or python library')
     print('or wish to make feature requests, visit:')
     print('https://github.com/meshtastic/Meshtastic-python/issues')
@@ -310,13 +448,14 @@ return_value defaults to 1 (non-successful)

print(' Platform: {0}'.format(platform.platform())) print(' Release: {0}'.format(platform.uname().release)) print(' Machine: {0}'.format(platform.uname().machine)) + print(' Encoding (stdin): {0}'.format(sys.stdin.encoding)) + print(' Encoding (stdout): {0}'.format(sys.stdout.encoding)) print(' meshtastic: v{0}'.format(pkg_resources.require('meshtastic')[0].version)) print(' Executable: {0}'.format(sys.argv[0])) print(' Python: {0} {1} {2}'.format(platform.python_version(), platform.python_implementation(), platform.python_compiler())) print('') - print('Please add the output from the command: meshtastic --info') - print('')
+ print('Please add the output from the command: meshtastic --info')
@@ -482,6 +621,9 @@ return_value defaults to 1 (non-successful)

  • catchAndIgnore
  • findPorts
  • fixme
  • +
  • fromPSK
  • +
  • fromStr
  • +
  • genPSK256
  • our_exit
  • pskToString
  • stripnl
  • diff --git a/setup.py b/setup.py index 2996dc9..54dff39 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ with open("README.md", "r") as fh: # This call to setup() does all the work setup( name="meshtastic", - version="1.2.44", + version="1.2.45", description="Python API & client shell for talking to Meshtastic devices", long_description=long_description, long_description_content_type="text/markdown",