mirror of
https://github.com/meshtastic/python.git
synced 2026-04-21 07:24:08 -04:00
fix formatting
This commit is contained in:
@@ -55,11 +55,19 @@ interface = meshtastic.SerialInterface()
|
||||
|
||||
"""
|
||||
|
||||
import socket
|
||||
import pygatt
|
||||
import google.protobuf.json_format
|
||||
import serial, threading, logging, sys, random, traceback, time, base64, platform
|
||||
from . import mesh_pb2, portnums_pb2, util
|
||||
import serial
|
||||
import threading
|
||||
import logging
|
||||
import sys
|
||||
import random
|
||||
import traceback
|
||||
import time
|
||||
import base64
|
||||
import platform
|
||||
import socket
|
||||
from . import mesh_pb2, portnums_pb2, apponly_pb2, util
|
||||
from pubsub import pub
|
||||
from dotmap import DotMap
|
||||
|
||||
@@ -79,7 +87,8 @@ MY_CONFIG_ID = 42
|
||||
|
||||
format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20
|
||||
"""
|
||||
OUR_APP_VERSION = 20200
|
||||
OUR_APP_VERSION = 20200
|
||||
|
||||
|
||||
def catchAndIgnore(reason, closure):
|
||||
"""Call a closure but if it throws an excpetion print it and continue"""
|
||||
@@ -88,6 +97,7 @@ def catchAndIgnore(reason, closure):
|
||||
except BaseException as ex:
|
||||
logging.error(f"Exception thrown in {reason}: {ex}")
|
||||
|
||||
|
||||
class MeshInterface:
|
||||
"""Interface class for meshtastic devices
|
||||
|
||||
@@ -108,7 +118,7 @@ class MeshInterface:
|
||||
self.nodes = None # FIXME
|
||||
self.isConnected = threading.Event()
|
||||
self.noProto = noProto
|
||||
random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it
|
||||
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._startConfig()
|
||||
|
||||
@@ -117,7 +127,8 @@ class MeshInterface:
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if exc_type is not None and exc_value is not None:
|
||||
logging.error(f'An exception of type {exc_type} with value {exc_value} has occurred')
|
||||
logging.error(
|
||||
f'An exception of type {exc_type} with value {exc_value} has occurred')
|
||||
if traceback is not None:
|
||||
logging.error(f'Traceback: {traceback}')
|
||||
self.close()
|
||||
@@ -265,6 +276,7 @@ class MeshInterface:
|
||||
"""Set device owner name"""
|
||||
nChars = 3
|
||||
minChars = 2
|
||||
fixme("update for 1.2")
|
||||
if long_name is not None:
|
||||
long_name = long_name.strip()
|
||||
if short_name is None:
|
||||
@@ -292,27 +304,30 @@ class MeshInterface:
|
||||
def channelURL(self):
|
||||
"""The sharable URL that describes the current channel
|
||||
"""
|
||||
bytes = self.radioConfig.channel_settings.SerializeToString()
|
||||
channelSet = apponly_pb2.ChannelSet()
|
||||
fixme("fill channelSet from self.channels")
|
||||
bytes = channelSet.SerializeToString()
|
||||
s = base64.urlsafe_b64encode(bytes).decode('ascii')
|
||||
return f"https://www.meshtastic.org/c/#{s}"
|
||||
|
||||
def setURL(self, url, write=True):
|
||||
def setURL(self, url):
|
||||
"""Set mesh network URL"""
|
||||
if self.radioConfig == None:
|
||||
raise Exception("No RadioConfig has been read")
|
||||
|
||||
# URLs are of the form https://www.meshtastic.org/c/#{base64_channel_settings}
|
||||
# URLs are of the form https://www.meshtastic.org/c/#{base64_channel_set}
|
||||
# Split on '/#' to find the base64 encoded channel settings
|
||||
splitURL = url.split("/#")
|
||||
decodedURL = base64.urlsafe_b64decode(splitURL[-1])
|
||||
self.radioConfig.channel_settings.ParseFromString(decodedURL)
|
||||
if write:
|
||||
self.writeConfig()
|
||||
channelSet = apponly_pb2.ChannelSet()
|
||||
channelSet.ParseFromString(decodedURL)
|
||||
fixme("set self.channels")
|
||||
self.writeChannels()
|
||||
|
||||
def _waitConnected(self):
|
||||
"""Block until the initial node db download is complete, or timeout
|
||||
and raise an exception"""
|
||||
if not self.isConnected.wait(5.0): # timeout after 5 seconds
|
||||
if not self.isConnected.wait(5.0): # timeout after 5 seconds
|
||||
raise Exception("Timed out waiting for connection completion")
|
||||
|
||||
def _generatePacketId(self):
|
||||
@@ -326,13 +341,15 @@ class MeshInterface:
|
||||
def _disconnected(self):
|
||||
"""Called by subclasses to tell clients this interface has disconnected"""
|
||||
self.isConnected.clear()
|
||||
catchAndIgnore("disconnection publish", lambda: pub.sendMessage("meshtastic.connection.lost", interface=self))
|
||||
catchAndIgnore("disconnection publish", lambda: pub.sendMessage(
|
||||
"meshtastic.connection.lost", interface=self))
|
||||
|
||||
def _connected(self):
|
||||
"""Called by this class to tell clients we are now fully connected to a node
|
||||
"""
|
||||
self.isConnected.set()
|
||||
catchAndIgnore("connection publish", lambda: pub.sendMessage("meshtastic.connection.established", interface=self))
|
||||
catchAndIgnore("connection publish", lambda: pub.sendMessage(
|
||||
"meshtastic.connection.established", interface=self))
|
||||
|
||||
def _startConfig(self):
|
||||
"""Start device packets flowing"""
|
||||
@@ -348,7 +365,8 @@ class MeshInterface:
|
||||
def _sendToRadio(self, toRadio):
|
||||
"""Send a ToRadio protobuf to the device"""
|
||||
if self.noProto:
|
||||
logging.warn(f"Not sending packet because protocol use is disabled by noProto")
|
||||
logging.warn(
|
||||
f"Not sending packet because protocol use is disabled by noProto")
|
||||
else:
|
||||
logging.debug(f"Sending toRadio: {toRadio}")
|
||||
self._sendToRadioImpl(toRadio)
|
||||
@@ -357,6 +375,15 @@ class MeshInterface:
|
||||
"""Send a ToRadio protobuf to the device"""
|
||||
logging.error(f"Subclass must provide toradio: {toRadio}")
|
||||
|
||||
def _handleConfigComplete(self):
|
||||
"""
|
||||
Done with initial config messages, now send regular MeshPackets to ask for settings and channels
|
||||
"""
|
||||
self._requestSettings()
|
||||
self._requestChannels()
|
||||
# FIXME, the following should only be called after we have settings and channels
|
||||
self._connected() # Tell everone else we are ready to go
|
||||
|
||||
def _handleFromRadio(self, fromRadioBytes):
|
||||
"""
|
||||
Handle a packet that arrived from the radio(update model and publish events)
|
||||
@@ -383,10 +410,11 @@ class MeshInterface:
|
||||
self.nodesByNum[node["num"]] = node
|
||||
if "user" in node: # Some nodes might not have user/ids assigned yet
|
||||
self.nodes[node["user"]["id"]] = node
|
||||
pub.sendMessage("meshtastic.node.updated", node=node, interface=self)
|
||||
pub.sendMessage("meshtastic.node.updated",
|
||||
node=node, interface=self)
|
||||
elif fromRadio.config_complete_id == MY_CONFIG_ID:
|
||||
# we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id
|
||||
self._connected()
|
||||
self._handleConfigComplete()
|
||||
elif fromRadio.HasField("packet"):
|
||||
self._handlePacketFromRadio(fromRadio.packet)
|
||||
elif fromRadio.rebooted:
|
||||
@@ -465,7 +493,8 @@ class MeshInterface:
|
||||
# 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 asDict["decoded"]:
|
||||
asDict["decoded"]["portnum"] = portnums_pb2.PortNum.Name(portnums_pb2.PortNum.UNKNOWN_APP)
|
||||
asDict["decoded"]["portnum"] = portnums_pb2.PortNum.Name(
|
||||
portnums_pb2.PortNum.UNKNOWN_APP)
|
||||
|
||||
portnum = asDict["decoded"]["portnum"]
|
||||
|
||||
@@ -482,7 +511,8 @@ class MeshInterface:
|
||||
# Usually btw this problem is caused by apps sending binary data but setting the payload type to
|
||||
# text.
|
||||
try:
|
||||
asDict["decoded"]["text"] = meshPacket.decoded.payload.decode("utf-8")
|
||||
asDict["decoded"]["text"] = meshPacket.decoded.payload.decode(
|
||||
"utf-8")
|
||||
except Exception as ex:
|
||||
logging.error(f"Malformatted utf8 in text message: {ex}")
|
||||
|
||||
@@ -511,7 +541,8 @@ class MeshInterface:
|
||||
self.nodes[u["id"]] = n
|
||||
|
||||
logging.debug(f"Publishing topic {topic}")
|
||||
catchAndIgnore(f"publishing {topic}", lambda: pub.sendMessage(topic, packet=asDict, interface=self))
|
||||
catchAndIgnore(f"publishing {topic}", lambda: pub.sendMessage(
|
||||
topic, packet=asDict, interface=self))
|
||||
|
||||
|
||||
# Our standard BLE characteristics
|
||||
@@ -600,7 +631,7 @@ class StreamInterface(MeshInterface):
|
||||
time.sleep(0.1) # wait 100ms to give device time to start running
|
||||
|
||||
self._rxThread.start()
|
||||
if not self.noProto: # Wait for the db download if using the protocol
|
||||
if not self.noProto: # Wait for the db download if using the protocol
|
||||
self._waitConnected()
|
||||
|
||||
def _disconnected(self):
|
||||
@@ -686,13 +717,16 @@ class StreamInterface(MeshInterface):
|
||||
# logging.debug(f"timeout")
|
||||
pass
|
||||
except serial.SerialException as ex:
|
||||
if not self._wantExit: # We might intentionally get an exception during shutdown
|
||||
logging.warn(f"Meshtastic serial port disconnected, disconnecting... {ex}")
|
||||
if not self._wantExit: # We might intentionally get an exception during shutdown
|
||||
logging.warn(
|
||||
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}")
|
||||
if not self._wantExit: # We might intentionally get an exception during shutdown
|
||||
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()
|
||||
@@ -740,6 +774,7 @@ class SerialInterface(StreamInterface):
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow)
|
||||
|
||||
|
||||
class TCPInterface(StreamInterface):
|
||||
"""Interface class for meshtastic devices over a TCP link"""
|
||||
|
||||
@@ -766,9 +801,9 @@ class TCPInterface(StreamInterface):
|
||||
def close(self):
|
||||
"""Close a connection to the device"""
|
||||
logging.debug("Closing TCP stream")
|
||||
# Sometimes the socket read might be blocked in the reader thread. Therefore we force the shutdown by closing
|
||||
# Sometimes the socket read might be blocked in the reader thread. Therefore we force the shutdown by closing
|
||||
# the socket here
|
||||
self._wantExit = True
|
||||
self._wantExit = True
|
||||
if not self.socket is None:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
self.socket.close()
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#!python3
|
||||
|
||||
import argparse, platform, logging, sys, codecs, base64
|
||||
import argparse
|
||||
import platform
|
||||
import logging
|
||||
import sys
|
||||
import codecs
|
||||
import base64
|
||||
from . import SerialInterface, TCPInterface, BLEInterface, test, remote_hardware
|
||||
from pubsub import pub
|
||||
from . import mesh_pb2, portnums_pb2
|
||||
@@ -20,6 +25,7 @@ args = None
|
||||
"""The parser for arguments"""
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
|
||||
def onReceive(packet, interface):
|
||||
"""Callback invoked when a packet arrives"""
|
||||
logging.debug(f"Received: {packet}")
|
||||
@@ -54,6 +60,7 @@ def onConnection(interface, topic=pub.AUTO_TOPIC):
|
||||
trueTerms = {"t", "true", "yes"}
|
||||
falseTerms = {"f", "false", "no"}
|
||||
|
||||
|
||||
def fromStr(valstr):
|
||||
"""try to parse as int, float or bool (and fallback to a string as last resort)
|
||||
|
||||
@@ -63,7 +70,8 @@ def fromStr(valstr):
|
||||
valstr (string): A user provided string
|
||||
"""
|
||||
if(valstr.startswith('0x')):
|
||||
val = bytes.fromhex(valstr[2:]) # if needed convert to string with asBytes.decode('utf-8')
|
||||
# if needed convert to string with asBytes.decode('utf-8')
|
||||
val = bytes.fromhex(valstr[2:])
|
||||
elif valstr in trueTerms:
|
||||
val = True
|
||||
elif valstr in falseTerms:
|
||||
@@ -94,7 +102,7 @@ def setRouter(interface, on):
|
||||
|
||||
# FIXME as of 1.1.24 of the device code, the following is all deprecated. After that release
|
||||
# has been out a while, just set is_router and warn the user about deprecation
|
||||
#
|
||||
#
|
||||
prefs.is_low_power = True
|
||||
prefs.gps_operation = mesh_pb2.GpsOpMobile
|
||||
|
||||
@@ -135,17 +143,21 @@ def setRouter(interface, on):
|
||||
prefs.gps_update_interval = 0
|
||||
|
||||
|
||||
#Returns formatted value
|
||||
# Returns formatted value
|
||||
def formatFloat(value, formatStr="{:.2f}", unit="", default="N/A"):
|
||||
return formatStr.format(value)+unit if value else default
|
||||
|
||||
#Returns Last Heard Time in human readable format
|
||||
# Returns Last Heard Time in human readable format
|
||||
|
||||
|
||||
def getLH(ts, default="N/A"):
|
||||
return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') if ts else default
|
||||
|
||||
#Print Nodes
|
||||
# Print Nodes
|
||||
|
||||
|
||||
def printNodes(nodes):
|
||||
#Create the table and define the structure
|
||||
# Create the table and define the structure
|
||||
table = EasyTable("Nodes")
|
||||
table.setCorners("/", "\\", "\\", "/")
|
||||
table.setOuterStructure("|", "-")
|
||||
@@ -153,19 +165,20 @@ def printNodes(nodes):
|
||||
|
||||
tableData = []
|
||||
for node in nodes:
|
||||
#aux var to get not defined keys
|
||||
LH= getLH(node['position'].get("time"))
|
||||
lat=formatFloat(node['position'].get("latitude"), "{:.4f}", "°")
|
||||
lon=formatFloat(node['position'].get("longitude"), "{:.4f}", "°")
|
||||
alt=formatFloat(node['position'].get("altitude"), "{:.0f}", " m")
|
||||
batt=formatFloat(node['position'].get("batteryLevel"), "{:.2f}", "%")
|
||||
snr=formatFloat(node.get("snr"), "{:.2f}", " dB")
|
||||
tableData.append({"User":node['user']['longName'],
|
||||
"Position":"Lat:"+lat+", Lon:"+lon+", Alt:"+alt,
|
||||
"Battery":batt, "SNR":snr, "LastHeard":LH})
|
||||
# aux var to get not defined keys
|
||||
LH = getLH(node['position'].get("time"))
|
||||
lat = formatFloat(node['position'].get("latitude"), "{:.4f}", "°")
|
||||
lon = formatFloat(node['position'].get("longitude"), "{:.4f}", "°")
|
||||
alt = formatFloat(node['position'].get("altitude"), "{:.0f}", " m")
|
||||
batt = formatFloat(node['position'].get("batteryLevel"), "{:.2f}", "%")
|
||||
snr = formatFloat(node.get("snr"), "{:.2f}", " dB")
|
||||
tableData.append({"User": node['user']['longName'],
|
||||
"Position": "Lat:"+lat+", Lon:"+lon+", Alt:"+alt,
|
||||
"Battery": batt, "SNR": snr, "LastHeard": LH})
|
||||
table.setData(tableData)
|
||||
table.displayTable()
|
||||
|
||||
|
||||
def onConnected(interface):
|
||||
"""Callback invoked when we connect to a radio"""
|
||||
closeNow = False # Should we drop the connection after we finish?
|
||||
@@ -227,7 +240,8 @@ def onConnected(interface):
|
||||
for wrpair in (args.gpiowrb or []):
|
||||
bitmask |= 1 << int(wrpair[0])
|
||||
bitval |= int(wrpair[1]) << int(wrpair[0])
|
||||
print(f"Writing GPIO mask 0x{bitmask:x} with value 0x{bitval:x} to {args.dest}")
|
||||
print(
|
||||
f"Writing GPIO mask 0x{bitmask:x} with value 0x{bitval:x} to {args.dest}")
|
||||
rhc.writeGPIOs(args.dest, bitmask, bitval)
|
||||
|
||||
if args.gpiord:
|
||||
@@ -238,10 +252,10 @@ def onConnected(interface):
|
||||
if args.gpiowatch:
|
||||
bitmask = int(args.gpiowatch)
|
||||
print(f"Watching GPIO mask 0x{bitmask:x} from {args.dest}")
|
||||
rhc.watchGPIOs(args.dest, bitmask)
|
||||
rhc.watchGPIOs(args.dest, bitmask)
|
||||
|
||||
if args.set or args.setstr or args.setchan or args.setch_longslow or args.setch_shortfast \
|
||||
or args.seturl or args.router != None:
|
||||
or args.seturl or args.router != None:
|
||||
closeNow = True
|
||||
|
||||
def setPref(attributes, name, valStr):
|
||||
@@ -263,7 +277,7 @@ def onConnected(interface):
|
||||
"""Set one of the simple modem_config only based channels"""
|
||||
ch = mesh_pb2.ChannelSettings()
|
||||
ch.modem_config = modem_config
|
||||
ch.psk = bytes([1]) # Use default channel psk 1
|
||||
ch.psk = bytes([1]) # Use default channel psk 1
|
||||
interface.radioConfig.channel_settings.CopyFrom(ch)
|
||||
|
||||
if args.router != None:
|
||||
@@ -280,10 +294,12 @@ def onConnected(interface):
|
||||
|
||||
# handle the simple channel set commands
|
||||
if args.setch_longslow:
|
||||
setSimpleChannel(mesh_pb2.ChannelSettings.ModemConfig.Bw125Cr48Sf4096)
|
||||
setSimpleChannel(
|
||||
mesh_pb2.ChannelSettings.ModemConfig.Bw125Cr48Sf4096)
|
||||
|
||||
if args.setch_shortfast:
|
||||
setSimpleChannel(mesh_pb2.ChannelSettings.ModemConfig.Bw500Cr45Sf128)
|
||||
setSimpleChannel(
|
||||
mesh_pb2.ChannelSettings.ModemConfig.Bw500Cr45Sf128)
|
||||
|
||||
# Handle the channel settings
|
||||
for pref in (args.setchan or []):
|
||||
@@ -316,9 +332,10 @@ def onConnected(interface):
|
||||
url = pyqrcode.create(interface.channelURL)
|
||||
print(url.terminal())
|
||||
|
||||
if have_tunnel and args.tunnel :
|
||||
if have_tunnel and args.tunnel:
|
||||
from . import tunnel
|
||||
closeNow = False # Even if others said we could close, stay open if the user asked for a tunnel
|
||||
# Even if others said we could close, stay open if the user asked for a tunnel
|
||||
closeNow = False
|
||||
tunnel.Tunnel(interface, subnet=args.tunnel_net)
|
||||
|
||||
except Exception as ex:
|
||||
@@ -328,6 +345,7 @@ def onConnected(interface):
|
||||
if (not args.seriallog) and closeNow:
|
||||
interface.close() # after running command then exit
|
||||
|
||||
|
||||
def onNode(node):
|
||||
"""Callback invoked when the node DB changes"""
|
||||
print(f"Node changed: {node}")
|
||||
@@ -383,6 +401,7 @@ def common():
|
||||
client = SerialInterface(
|
||||
args.port, debugOut=logfile, noProto=args.noproto)
|
||||
|
||||
|
||||
def initParser():
|
||||
global parser, args
|
||||
|
||||
@@ -403,7 +422,7 @@ def initParser():
|
||||
parser.add_argument("--info", help="Read and display the radio config information",
|
||||
action="store_true")
|
||||
|
||||
parser.add_argument("--nodes", help="Print Node List in a pretty formatted table",
|
||||
parser.add_argument("--nodes", help="Print Node List in a pretty formatted table",
|
||||
action="store_true")
|
||||
|
||||
parser.add_argument("--qr", help="Display the QR code that corresponds to the current channel",
|
||||
@@ -439,7 +458,7 @@ def initParser():
|
||||
parser.add_argument(
|
||||
"--sendping", help="Send a ping message (which requests a reply)", action="store_true")
|
||||
|
||||
#parser.add_argument(
|
||||
# parser.add_argument(
|
||||
# "--repeat", help="Normally the send commands send only one message, use this option to request repeated sends")
|
||||
|
||||
parser.add_argument(
|
||||
@@ -486,21 +505,24 @@ def initParser():
|
||||
|
||||
if have_tunnel:
|
||||
parser.add_argument('--tunnel',
|
||||
action='store_true', help="Create a TUN tunnel device for forwarding IP packets over the mesh")
|
||||
action='store_true', help="Create a TUN tunnel device for forwarding IP packets over the mesh")
|
||||
parser.add_argument(
|
||||
"--subnet", dest='tunnel_net', help="Read from a GPIO mask", default=None)
|
||||
|
||||
parser.set_defaults(router=None)
|
||||
|
||||
parser.add_argument('--version', action='version', version=f"{pkg_resources.require('meshtastic')[0].version}")
|
||||
parser.add_argument('--version', action='version',
|
||||
version=f"{pkg_resources.require('meshtastic')[0].version}")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Perform command line meshtastic operations"""
|
||||
initParser()
|
||||
common()
|
||||
|
||||
|
||||
def tunnelMain():
|
||||
"""Run a meshtastic IP tunnel"""
|
||||
global args
|
||||
@@ -508,5 +530,6 @@ def tunnelMain():
|
||||
args.tunnel = True
|
||||
common()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# sudo bin/run.sh --port /dev/ttyUSB0 --setch-shortfast
|
||||
# sudo bin/run.sh --port /dev/ttyUSB0 --tunnel --debug
|
||||
# ssh -Y root@192.168.10.151 (or dietpi), default password p
|
||||
# ncat -e /bin/cat -k -u -l 1235
|
||||
# ncat -e /bin/cat -k -u -l 1235
|
||||
# ncat -u 10.115.64.152 1235
|
||||
# ping -c 1 -W 20 10.115.64.152
|
||||
# ping -i 30 -W 30 10.115.64.152
|
||||
@@ -15,7 +15,8 @@
|
||||
|
||||
from . import portnums_pb2
|
||||
from pubsub import pub
|
||||
import logging, threading
|
||||
import logging
|
||||
import threading
|
||||
|
||||
# A new non standard log level that is lower level than DEBUG
|
||||
LOG_TRACE = 5
|
||||
@@ -26,8 +27,8 @@ tunnelInstance = None
|
||||
"""A list of chatty UDP services we should never accidentally
|
||||
forward to our slow network"""
|
||||
udpBlacklist = {
|
||||
1900, # SSDP
|
||||
5353, # multicast DNS
|
||||
1900, # SSDP
|
||||
5353, # multicast DNS
|
||||
}
|
||||
|
||||
"""A list of TCP services to block"""
|
||||
@@ -35,31 +36,36 @@ tcpBlacklist = {}
|
||||
|
||||
"""A list of protocols we ignore"""
|
||||
protocolBlacklist = {
|
||||
0x02, # IGMP
|
||||
0x80, # Service-Specific Connection-Oriented Protocol in a Multilink and Connectionless Environment
|
||||
0x02, # IGMP
|
||||
0x80, # Service-Specific Connection-Oriented Protocol in a Multilink and Connectionless Environment
|
||||
}
|
||||
|
||||
|
||||
def hexstr(barray):
|
||||
"""Print a string of hex digits"""
|
||||
return ":".join('{:02x}'.format(x) for x in barray)
|
||||
|
||||
|
||||
def ipstr(barray):
|
||||
"""Print a string of ip digits"""
|
||||
return ".".join('{}'.format(x) for x in barray)
|
||||
|
||||
|
||||
def readnet_u16(p, offset):
|
||||
"""Read big endian u16 (network byte order)"""
|
||||
return p[offset] * 256 + p[offset + 1]
|
||||
|
||||
|
||||
def onTunnelReceive(packet, interface):
|
||||
"""Callback for received tunneled messages from mesh
|
||||
|
||||
|
||||
FIXME figure out how to do closures with methods in python"""
|
||||
tunnelInstance.onReceive(packet)
|
||||
|
||||
|
||||
class Tunnel:
|
||||
"""A TUN based IP tunnel over meshtastic"""
|
||||
|
||||
|
||||
def __init__(self, iface, subnet=None, netmask="255.255.0.0"):
|
||||
"""
|
||||
Constructor
|
||||
@@ -77,7 +83,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)
|
||||
@@ -85,24 +92,26 @@ class Tunnel:
|
||||
for node in self.iface.nodes.values():
|
||||
nodeId = node["user"]["id"]
|
||||
ip = self._nodeNumToIp(node["num"])
|
||||
logging.info(f"Node { nodeId } has IP address { ip }")
|
||||
logging.info(f"Node { nodeId } has IP address { ip }")
|
||||
|
||||
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)
|
||||
self.tun.ifconfig(address=myAddr, netmask=netmask, mtu=200)
|
||||
logging.debug(f"starting TUN reader, our IP address is {myAddr}")
|
||||
self._rxThread = threading.Thread(target=self.__tunReader, args=(), daemon=True)
|
||||
self._rxThread = threading.Thread(
|
||||
target=self.__tunReader, args=(), daemon=True)
|
||||
self._rxThread.start()
|
||||
|
||||
def onReceive(self, packet):
|
||||
p = packet["decoded"]["data"]["payload"]
|
||||
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)}")
|
||||
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
|
||||
if not self._shouldFilterPacket(p):
|
||||
@@ -114,36 +123,43 @@ class Tunnel:
|
||||
srcaddr = p[12:16]
|
||||
destAddr = p[16:20]
|
||||
subheader = 20
|
||||
ignore = False # Assume we will be forwarding the packet
|
||||
ignore = False # Assume we will be forwarding the packet
|
||||
if protocol in protocolBlacklist:
|
||||
ignore = True
|
||||
logging.log(LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}")
|
||||
elif protocol == 0x01: # ICMP
|
||||
logging.log(
|
||||
LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}")
|
||||
elif protocol == 0x01: # ICMP
|
||||
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}")
|
||||
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)
|
||||
elif protocol == 0x11: # UDP
|
||||
# tap.write(pingback)
|
||||
elif protocol == 0x11: # UDP
|
||||
srcport = readnet_u16(p, subheader)
|
||||
destport = readnet_u16(p, subheader + 2)
|
||||
if destport in udpBlacklist:
|
||||
ignore = True
|
||||
logging.log(LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
|
||||
logging.log(
|
||||
LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
|
||||
else:
|
||||
logging.debug(f"forwarding udp srcport={srcport}, destport={destport}")
|
||||
elif protocol == 0x06: # TCP
|
||||
logging.debug(
|
||||
f"forwarding udp srcport={srcport}, destport={destport}")
|
||||
elif protocol == 0x06: # TCP
|
||||
srcport = readnet_u16(p, subheader)
|
||||
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
|
||||
|
||||
@@ -179,14 +195,13 @@ 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}")
|
||||
self.iface.sendData(p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck = False)
|
||||
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):
|
||||
self.tun.close()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user