mirror of
https://github.com/meshtastic/python.git
synced 2026-01-06 14:57:58 -05:00
minor formatting; added unit tests for Node()
This commit is contained in:
@@ -91,9 +91,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
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from . import portnums_pb2, channel_pb2, radioconfig_pb2
|
||||
from .globals import Globals
|
||||
|
||||
|
||||
"""We only import the tunnel code if we are on a platform that can run it"""
|
||||
"""We only import the tunnel code if we are on a platform that can run it. """
|
||||
have_tunnel = platform.system() == 'Linux'
|
||||
|
||||
def onReceive(packet, interface):
|
||||
@@ -375,7 +375,9 @@ def onConnected(interface):
|
||||
print(f"Deleting channel {channelIndex}")
|
||||
ch = getNode().deleteChannel(channelIndex)
|
||||
|
||||
ch_changes = [args.ch_longslow, args.ch_longfast, args.ch_mediumslow, args.ch_mediumfast, args.ch_shortslow, args.ch_shortfast]
|
||||
ch_changes = [args.ch_longslow, args.ch_longfast,
|
||||
args.ch_mediumslow, args.ch_mediumfast,
|
||||
args.ch_shortslow, args.ch_shortfast]
|
||||
any_primary_channel_changes = any(x for x in ch_changes)
|
||||
if args.ch_set or any_primary_channel_changes or args.ch_enable or args.ch_disable:
|
||||
closeNow = True
|
||||
@@ -593,7 +595,7 @@ def common():
|
||||
|
||||
|
||||
def initParser():
|
||||
""" Initialize the command line argument parsing."""
|
||||
"""Initialize the command line argument parsing."""
|
||||
our_globals = Globals.getInstance()
|
||||
parser = our_globals.get_parser()
|
||||
args = our_globals.get_args()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Bluetooth interface
|
||||
"""Bluetooth interface
|
||||
"""
|
||||
import logging
|
||||
import pygatt
|
||||
|
||||
@@ -39,7 +39,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
|
||||
@@ -92,12 +93,15 @@ 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 = []
|
||||
@@ -137,8 +141,7 @@ class MeshInterface:
|
||||
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
|
||||
|
||||
@@ -161,18 +164,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,
|
||||
@@ -191,15 +200,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)}")
|
||||
@@ -223,16 +240,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:
|
||||
@@ -293,8 +312,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()
|
||||
|
||||
@@ -305,8 +324,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")
|
||||
|
||||
@@ -408,8 +426,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)
|
||||
@@ -420,7 +437,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()
|
||||
|
||||
@@ -441,7 +459,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
|
||||
@@ -469,13 +487,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,8 +589,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)
|
||||
@@ -579,8 +600,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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Node class
|
||||
"""Node class
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -24,7 +24,7 @@ class Node:
|
||||
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:
|
||||
@@ -47,9 +47,7 @@ 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
|
||||
@@ -104,9 +102,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):
|
||||
@@ -165,8 +165,7 @@ 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:
|
||||
@@ -212,9 +211,8 @@ 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
|
||||
|
||||
@@ -233,26 +231,20 @@ class Node:
|
||||
|
||||
# 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")
|
||||
@@ -263,6 +255,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
|
||||
|
||||
@@ -281,21 +274,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
|
||||
@@ -305,9 +296,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")
|
||||
@@ -320,13 +311,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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Stream Interface base class
|
||||
"""Stream Interface base class
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
@@ -25,7 +25,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]
|
||||
@@ -53,12 +54,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
|
||||
@@ -105,7 +108,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
|
||||
@@ -151,8 +155,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:
|
||||
@@ -160,15 +163,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()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" TCPInterface class for interfacing with http endpoint
|
||||
"""TCPInterface class for interfacing with http endpoint
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
@@ -9,7 +9,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:
|
||||
@@ -33,8 +34,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:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" With two radios connected serially, send and receive test
|
||||
messages and report back if successful.
|
||||
"""With two radios connected serially, send and receive test
|
||||
messages and report back if successful.
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
34
meshtastic/tests/test_node.py
Normal file
34
meshtastic/tests/test_node.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""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()
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Code for IP tunnel over a mesh
|
||||
"""Code for IP tunnel over a mesh
|
||||
|
||||
# Note python-pytuntap was too buggy
|
||||
# using pip3 install pytap2
|
||||
@@ -88,8 +88,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)
|
||||
@@ -117,8 +117,8 @@ class Tunnel:
|
||||
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)
|
||||
|
||||
@@ -137,8 +137,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)
|
||||
@@ -157,14 +157,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
|
||||
|
||||
@@ -200,13 +198,11 @@ 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"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Utility functions.
|
||||
"""Utility functions.
|
||||
"""
|
||||
import traceback
|
||||
from queue import Queue
|
||||
@@ -39,7 +39,8 @@ def fromPSK(valstr):
|
||||
|
||||
|
||||
def fromStr(valstr):
|
||||
"""try to parse as int, float or bool (and fallback to a string as last resort)
|
||||
"""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:
|
||||
@@ -82,13 +83,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}")
|
||||
|
||||
|
||||
@@ -174,7 +175,7 @@ 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:')
|
||||
|
||||
Reference in New Issue
Block a user