diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py index d70fd74..b60b3dc 100644 --- a/meshtastic/__init__.py +++ b/meshtastic/__init__.py @@ -47,12 +47,13 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # called when we (re)connect pub.subscribe(onReceive, "meshtastic.receive") pub.subscribe(onConnection, "meshtastic.connection.established") # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0 -interface = meshtastic.StreamInterface() +interface = meshtastic.SerialInterface() ``` """ +import socket import pygatt import google.protobuf.json_format import serial @@ -417,9 +418,8 @@ class BLEInterface(MeshInterface): class StreamInterface(MeshInterface): """Interface class for meshtastic devices over a stream link (serial, TCP, etc)""" - def __init__(self, devPath=None, debugOut=None, noProto=False, connectNow=True): - """Constructor, opens a connection to a specified serial port, or if unspecified try to - find one Meshtastic device by probing + def __init__(self, debugOut=None, noProto=False, connectNow=True): + """Constructor, opens a connection to self.stream Keyword Arguments: devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None}) @@ -430,35 +430,12 @@ class StreamInterface(MeshInterface): Exception: [description] """ - if devPath is None: - ports = util.findPorts() - if len(ports) == 0: - raise Exception("No Meshtastic devices detected") - elif len(ports) > 1: - raise Exception( - f"Multiple ports detected, you must specify a device, such as {ports[0].device}") - else: - devPath = ports[0] - - logging.debug(f"Connecting to {devPath}") - self.devPath = devPath + if not hasattr(self, 'stream'): + raise Exception( + "StreamInterface is now abstract (to update existing code create SerialInterface instead)") self._rxBuf = bytes() # empty self._wantExit = False - # Note: we provide None for port here, because we will be opening it later - self.stream = serial.Serial( - None, 921600, exclusive=True, timeout=0.5) - - # rts=False Needed to prevent TBEAMs resetting on OSX, because rts is connected to reset - self.stream.port = devPath - # OS-X seems to have a bug in its serial driver. It ignores that we asked for no RTSCTS - # control and will always drive RTS either high or low (rather than letting the CP102 leave - # it as an open-collector floating pin). Since it is going to drive it anyways we want to make - # sure it is driven low, so that the TBEAM won't reset - if platform.system() == 'Darwin': - self.stream.rts = False - self.stream.open() - self._rxThread = threading.Thread(target=self.__reader, args=()) MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto) @@ -481,11 +458,19 @@ class StreamInterface(MeshInterface): self._rxThread.start() + def _disconnected(self): + """We override the superclass implementation to close our port""" + MeshInterface._disconnected(self) + + logging.debug("Closing our port") + self.stream.close() + def _sendToRadio(self, toRadio): """Send a ToRadio protobuf to the device""" logging.debug(f"Sending: {toRadio}") b = toRadio.SerializeToString() bufLen = len(b) + # We convert into a string, because the TCP code doesn't work with byte arrays header = bytes([START1, START2, (bufLen >> 8) & 0xff, bufLen & 0xff]) self.stream.write(header) self.stream.write(b) @@ -543,14 +528,82 @@ class StreamInterface(MeshInterface): traceback.print_exc() self._rxBuf = empty else: - # logging.debug(f"timeout on {self.devPath}") + # logging.debug(f"timeout") pass except serial.SerialException as ex: logging.warn( f"Meshtastic serial port disconnected, disconnecting... {ex}") finally: logging.debug("reader is exiting") - if platform.system() == 'Darwin': - self.stream.rts = True # Return RTS high, so that the reset button still works - self.stream.close() self._disconnected() + + +class SerialInterface(StreamInterface): + """Interface class for meshtastic devices over a serial link""" + + def __init__(self, devPath=None, debugOut=None, noProto=False, connectNow=True): + """Constructor, opens a connection to a specified serial port, or if unspecified try to + find one Meshtastic device by probing + + 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}) + """ + + if devPath is None: + ports = util.findPorts() + if len(ports) == 0: + raise Exception("No Meshtastic devices detected") + elif len(ports) > 1: + raise Exception( + f"Multiple ports detected, you must specify a device, such as {ports[0].device}") + else: + devPath = ports[0] + + logging.debug(f"Connecting to {devPath}") + + # Note: we provide None for port here, because we will be opening it later + self.stream = serial.Serial( + None, 921600, exclusive=True, timeout=0.5) + + # rts=False Needed to prevent TBEAMs resetting on OSX, because rts is connected to reset + self.stream.port = devPath + # OS-X seems to have a bug in its serial driver. It ignores that we asked for no RTSCTS + # control and will always drive RTS either high or low (rather than letting the CP102 leave + # it as an open-collector floating pin). Since it is going to drive it anyways we want to make + # sure it is driven low, so that the TBEAM won't reset + if platform.system() == 'Darwin': + self.stream.rts = False + self.stream.open() + + StreamInterface.__init__( + self, debugOut=debugOut, noProto=noProto, connectNow=connectNow) + + def _disconnected(self): + """We override the superclass implementation to close our port""" + + if platform.system() == 'Darwin': + self.stream.rts = True # Return RTS high, so that the reset button still works + + StreamInterface._disconnected(self) + + +class TCPInterface(StreamInterface): + """Interface class for meshtastic devices over a TCP link""" + + def __init__(self, hostname, debugOut=None, noProto=False, connectNow=True, portNumber=4403): + """Constructor, opens a connection to a specified IP address/hostname + + Keyword Arguments: + hostname {string} -- Hostname/IP address of the device to connect to + """ + + logging.debug(f"Connecting to {hostname}") + + server_address = (hostname, portNumber) + sock = socket.create_connection(server_address) + + self.stream = sock.makefile('rw') + + StreamInterface.__init__( + self, debugOut=debugOut, noProto=noProto, connectNow=connectNow) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 624b845..0291557 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1,7 +1,7 @@ #!python3 import argparse -from . import StreamInterface, BLEInterface, test +from . import SerialInterface, TCPInterface, BLEInterface, test import logging import sys from pubsub import pub @@ -162,10 +162,15 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument( - "--device", + "--port", help="The port the Meshtastic device is connected to, i.e. /dev/ttyUSB0. If unspecified, we'll try to find it.", default=None) + parser.add_argument( + "--host", + help="The hostname/ipaddr of the device to connect to (over TCP)", + default=None) + parser.add_argument( "--seriallog", help="Log device serial output to either 'stdout', 'none' or a filename to append to. Defaults to stdout.", @@ -205,8 +210,8 @@ def main(): parser.add_argument("--test", help="Run stress test against all connected Meshtastic devices", action="store_true") - parser.add_argument("--ble", help="hack for testing BLE code (BLE is not yet supported for this tool)", - action="store_true") + parser.add_argument("--ble", help="BLE mac address to connect to (BLE is not yet supported for this tool)", + default=None) parser.add_argument("--noproto", help="Don't start the API, just function as a dumb serial terminal.", action="store_true") @@ -233,10 +238,13 @@ def main(): subscribe() if args.ble: - client = BLEInterface(args.device, debugOut=logfile) + client = BLEInterface(args.ble, debugOut=logfile) + elif args.host: + client = TCPInterface( + args.host, debugOut=logfile, noProto=args.noproto) else: - client = StreamInterface( - args.device, debugOut=logfile, noProto=args.noproto) + client = SerialInterface( + args.port, debugOut=logfile, noProto=args.noproto) if __name__ == "__main__": diff --git a/meshtastic/test.py b/meshtastic/test.py index c80906a..fc862c8 100644 --- a/meshtastic/test.py +++ b/meshtastic/test.py @@ -1,6 +1,6 @@ import logging from . import util -from . import StreamInterface, BROADCAST_NUM +from . import SerialInterface, BROADCAST_NUM from pubsub import pub import time import sys @@ -128,7 +128,7 @@ def testAll(): pub.subscribe(onConnection, "meshtastic.connection") pub.subscribe(onReceive, "meshtastic.receive") global interfaces - interfaces = list(map(lambda port: StreamInterface( + interfaces = list(map(lambda port: SerialInterface( port, debugOut=openDebugLog(port), connectNow=False), ports)) for i in interfaces: i.connect()