diff --git a/.gitignore b/.gitignore index 99ac520..fd7c7e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ README build dist -*.egg-info \ No newline at end of file +*.egg-info +log_* \ No newline at end of file diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py index b128103..38ecb84 100644 --- a/meshtastic/__init__.py +++ b/meshtastic/__init__.py @@ -47,13 +47,14 @@ interface = meshtastic.StreamInterface() # By default will try to find a meshtas import google.protobuf.json_format import serial -import serial.tools.list_ports import threading import logging import sys import traceback from . import mesh_pb2 +from . import util from pubsub import pub +from dotmap import DotMap START1 = 0x94 START2 = 0xc3 @@ -68,12 +69,19 @@ MY_CONFIG_ID = 42 class MeshInterface: """Interface class for meshtastic devices + + Properties: + + isConnected + nodes + debugOut """ def __init__(self, debugOut=None): """Constructor""" self.debugOut = debugOut self.nodes = None # FIXME + self.isConnected = False self._startConfig() def sendText(self, text, destinationId=BROADCAST_ADDR): @@ -106,8 +114,15 @@ class MeshInterface: def _disconnected(self): """Called by subclasses to tell clients this interface has disconnected""" + self.isConnected = False pub.sendMessage("meshtastic.connection.lost") + def _connected(self): + """Called by this class to tell clients we are now fully connected to a node + """ + self.isConnected = True + pub.sendMessage("meshtastic.connection.established") + def _startConfig(self): """Start device packets flowing""" self.myInfo = None @@ -142,7 +157,7 @@ class MeshInterface: self.nodes[node.user.id] = node 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 - pub.sendMessage("meshtastic.connection.established") + self._connected() elif fromRadio.HasField("packet"): self._handlePacketFromRadio(fromRadio.packet) elif fromRadio.rebooted: @@ -160,15 +175,16 @@ class MeshInterface: - meshtastic.receive.data(packet = MeshPacket dictionary) """ # FIXME, update node DB as needed - json = google.protobuf.json_format.MessageToDict(meshPacket) + # We provide our objects as DotMaps - which work with . notation or as dictionaries + asObj = DotMap(google.protobuf.json_format.MessageToDict(meshPacket)) if meshPacket.payload.HasField("position"): - pub.sendMessage("meshtastic.receive.position", packet=json) + pub.sendMessage("meshtastic.receive.position", packet=asObj) if meshPacket.payload.HasField("user"): pub.sendMessage("meshtastic.receive.user", - packet=json) + packet=asObj) if meshPacket.payload.HasField("data"): pub.sendMessage("meshtastic.receive.data", - packet=json) + packet=asObj) class StreamInterface(MeshInterface): @@ -188,15 +204,14 @@ class StreamInterface(MeshInterface): """ if devPath is None: - ports = list(filter(lambda port: port.vid != None, - serial.tools.list_ports.comports())) + 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].device + devPath = ports[0] logging.debug(f"Connecting to {devPath}") self._rxBuf = bytes() # empty diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 5f4076d..359aaf9 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1,7 +1,7 @@ #!python3 import argparse -from . import StreamInterface +from . import StreamInterface, test import logging import sys from pubsub import pub @@ -47,20 +47,26 @@ def main(): parser.add_argument("--debug", help="Show API library debug log messages", action="store_true") + parser.add_argument("--test", help="Run stress test against all connected Meshtastic devices", + action="store_true") + args = parser.parse_args() logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) - if args.seriallog == "stdout": - logfile = sys.stdout - elif args.seriallog == "none": - logging.debug("Not logging serial output") - logfile = None + if args.test: + test.testAll() else: - logging.info(f"Logging serial output to {args.seriallog}") - logfile = open(args.seriallog, 'w+', buffering=1) # line buffering + if args.seriallog == "stdout": + logfile = sys.stdout + elif args.seriallog == "none": + logging.debug("Not logging serial output") + logfile = None + else: + logging.info(f"Logging serial output to {args.seriallog}") + logfile = open(args.seriallog, 'w+', buffering=1) # line buffering - subscribe() - client = StreamInterface(args.device, debugOut=logfile) + subscribe() + client = StreamInterface(args.device, debugOut=logfile) if __name__ == "__main__": diff --git a/meshtastic/test.py b/meshtastic/test.py new file mode 100644 index 0000000..9ac4a5c --- /dev/null +++ b/meshtastic/test.py @@ -0,0 +1,104 @@ +import logging +from . import util +from . import StreamInterface +from pubsub import pub +import time + +"""The interfaces we are using for our tests""" +interfaces = None + +"""A list of all packets we received while the current test was running""" +receivedPackets = None + +testsRunning = False + +testNumber = 0 + + +def onReceive(packet): + """Callback invoked when a packet arrives""" + print(f"Received: {packet}") + if packet.payload.data.typ == "CLEAR_TEXT": + # We only care a about clear text packets + receivedPackets.extend(packet) + + +def onNode(node): + """Callback invoked when the node DB changes""" + print(f"Node changed: {node}") + + +def subscribe(): + """Subscribe to the topics the user probably wants to see, prints output to stdout""" + + pub.subscribe(onNode, "meshtastic.node") + + +def testSend(fromInterface, toInterface): + """ + Sends one test packet between two nodes and then returns success or failure + + Arguments: + fromInterface {[type]} -- [description] + toInterface {[type]} -- [description] + + Returns: + boolean -- True for success + """ + global receivedPackets + receivedPackets = [] + fromNode = fromInterface.myInfo.my_node_num + toNode = toInterface.myInfo.my_node_num + logging.info(f"Sending test packet from {fromNode} to {toNode}") + fromInterface.sendText(f"Test {testNumber}", toNode) + time.sleep(10) + if (len(receivedPackets) < 1): + logging.error("Test failed, expected packet not received") + return True + else: + logging.info("Test succeeded") + return False + + +def startTests(): + logging.info("Found devices, starting tests...") + while True: + global testNumber + testNumber = testNumber + 1 + testSend(interfaces[0], interfaces[1]) + time.sleep(20) + + +def onConnection(topic=pub.AUTO_TOPIC): + """Callback invoked when we connect/disconnect from a radio""" + print(f"Connection changed: {topic.getName()}") + + global testsRunning + if (all(iface.isConnected for iface in interfaces) and not testsRunning): + testsRunning = True + startTests() + + +def openDebugLog(portName): + debugname = "log" + portName.replace("/", "_") + logging.info(f"Writing serial debugging to {debugname}") + return open(debugname, 'w+', buffering=1) + + +def testAll(): + """ + Run a series of tests using devices we can find. + + Raises: + Exception: If not enough devices are found + """ + ports = util.findPorts() + if (len(ports) != 2): + raise Exception("Must have at least two devices connected to USB") + + pub.subscribe(onConnection, "meshtastic.connection") + pub.subscribe(onReceive, "meshtastic.receive") + global interfaces + interfaces = list(map(lambda port: StreamInterface( + port, debugOut=openDebugLog(port)), ports)) + logging.info("Ports opened, waiting for device to complete connection") diff --git a/meshtastic/util.py b/meshtastic/util.py new file mode 100644 index 0000000..967f289 --- /dev/null +++ b/meshtastic/util.py @@ -0,0 +1,16 @@ + +import serial +import serial.tools.list_ports + + +def findPorts(): + """Find all ports that might have meshtastic devices + + Returns: + list -- a list of device paths + """ + l = list(map(lambda port: port.device, + filter(lambda port: port.vid != None, + serial.tools.list_ports.comports()))) + l.sort() + return l diff --git a/setup.py b/setup.py index 80c9a7f..bd4d469 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,8 @@ setup( ], packages=["meshtastic"], include_package_data=True, - install_requires=["pyserial>=3.4", "protobuf>=3.6.1", "pypubsub>=4.0.3"], + install_requires=["pyserial>=3.4", "protobuf>=3.6.1", + "pypubsub>=4.0.3", "dotmap>=1.3.14"], python_requires='>=3', entry_points={ "console_scripts": [