From bbbd33e292a5753ff9cbfb94402a6753e4643884 Mon Sep 17 00:00:00 2001 From: geeksville Date: Tue, 28 Apr 2020 14:51:39 -0700 Subject: [PATCH] add first cut at docs --- README.md | 4 +- TODO.md | 3 +- bin/regen-docs.sh | 2 + test-release.sh => bin/test-release.sh | 5 +- upload-release.sh => bin/upload-release.sh | 2 + doc/meshtastic/index.html | 725 ++++++++ doc/meshtastic/mesh_pb2.html | 1941 ++++++++++++++++++++ meshtastic/__init__.py | 262 ++- meshtastic/__main__.py | 2 +- meshtastic/interface.py | 226 --- setup.py | 2 +- 11 files changed, 2938 insertions(+), 236 deletions(-) create mode 100755 bin/regen-docs.sh rename test-release.sh => bin/test-release.sh (72%) rename upload-release.sh => bin/upload-release.sh (69%) create mode 100644 doc/meshtastic/index.html create mode 100644 doc/meshtastic/mesh_pb2.html delete mode 100644 meshtastic/interface.py diff --git a/README.md b/README.md index 5a2855e..f9f6fa0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Meshtastic-python -A python client for using Meshtastic devices. This small library (and example application) provides an easy API for sending and receiving messages over mesh radios. It also provides access to any of the operations/data available in the device user interface or the Android application. Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in. +A python client for using Meshtastic devices. This small library (and example application) provides an easy API for sending and receiving messages over mesh radios. It also provides access to any of the operations/data available in the device user interface or the Android application. Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in. You probably don't want this yet because it is a pre-alpha WIP. +For the API documentation [click here](./doc/meshtastic/index.html). For the rough notes/implementation plan see [TODO](./TODO.md). - diff --git a/TODO.md b/TODO.md index 6427585..832790e 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,8 @@ - add fromId and toId to received messages dictionaries - update nodedb as nodes change - make docs decent -- keep everything in dicts +- radioConfig - getter/setter syntax: https://www.python-course.eu/python3_properties.php +- DONE keep everything in dicts - document properties/fields - include examples in readme. hello.py, textchat.py, replymessage.py all as one little demo - have python client turn off radio sleep (use 0 for X to mean restore defaults) diff --git a/bin/regen-docs.sh b/bin/regen-docs.sh new file mode 100755 index 0000000..139d19a --- /dev/null +++ b/bin/regen-docs.sh @@ -0,0 +1,2 @@ +rm -rf doc +pdoc3 --html --output-dir doc meshtastic \ No newline at end of file diff --git a/test-release.sh b/bin/test-release.sh similarity index 72% rename from test-release.sh rename to bin/test-release.sh index 2a21eaf..e8c6bff 100755 --- a/test-release.sh +++ b/bin/test-release.sh @@ -1,9 +1,8 @@ rm dist/* set -e -pydoc3 -w meshtastic -mv *.html doc - +bin/regen-docs.sh +pandoc --from=markdown --to=rst --output=README README.md python3 setup.py sdist bdist_wheel python3 -m twine check dist/* # test the upload diff --git a/upload-release.sh b/bin/upload-release.sh similarity index 69% rename from upload-release.sh rename to bin/upload-release.sh index af7cdaf..3136311 100755 --- a/upload-release.sh +++ b/bin/upload-release.sh @@ -1,5 +1,7 @@ rm dist/* set -e + +bin/regen-docs.sh pandoc --from=markdown --to=rst --output=README README.md python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* \ No newline at end of file diff --git a/doc/meshtastic/index.html b/doc/meshtastic/index.html new file mode 100644 index 0000000..6a4d9ed --- /dev/null +++ b/doc/meshtastic/index.html @@ -0,0 +1,725 @@ + + + + + + +meshtastic API documentation + + + + + + + + + +
+
+
+

Package meshtastic

+
+
+

an API for Meshtastic devices

+

Primary class: StreamInterface

+

properties of StreamInterface:

+
    +
  • radioConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to +the device.
  • +
  • nodes - The database of received nodes. +Includes always up-to-date location and username information for each +node in the mesh. +This is a read-only datastructure.
  • +
  • myNodeInfo - You probably don't want this.
  • +
+

Published PubSub topics

+

We use a publish-subscribe model to communicate asynchronous events [https://pypubsub.readthedocs.io/en/v4.0.3/ ]. +Available +topics:

+
    +
  • meshtastic.connection.established - published once we've successfully connected to the radio and downloaded the node DB
  • +
  • meshtastic.connection.lost - published once we've lost our link to the radio
  • +
  • meshtastic.receive.position(packet) - delivers a received packet as a dictionary, if you only care about a particular +type of packet, you should subscribe to the full topic name. +If you want to see all packets, simply subscribe to "meshtastic.receive".
  • +
  • meshtastic.receive.user(packet)
  • +
  • meshtastic.receive.data(packet)
  • +
  • meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc…)
  • +
+

Example Usage:

+
import meshtastic
+from pubsub import pub
+
+def onReceive(packet):
+    print(f"Received: {packet}")
+
+interface = StreamInterface() # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
+pub.subscribe(onReceive, "meshtastic.receive")
+interface.sendData("hello world") # defaults to broadcast, specify a destination ID if you wish
+
+
+ +Expand source code + +
"""
+## an API for Meshtastic devices
+
+Primary class: StreamInterface
+
+properties of StreamInterface:
+
+- radioConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to 
+the device.
+- nodes - The database of received nodes.  Includes always up-to-date location and username information for each 
+node in the mesh.  This is a read-only datastructure.
+- myNodeInfo - You probably don't want this.
+
+## Published PubSub topics
+
+We use a publish-subscribe model to communicate asynchronous events [https://pypubsub.readthedocs.io/en/v4.0.3/ ].  Available
+topics:
+
+- meshtastic.connection.established - published once we've successfully connected to the radio and downloaded the node DB
+- meshtastic.connection.lost - published once we've lost our link to the radio
+- meshtastic.receive.position(packet) - delivers a received packet as a dictionary, if you only care about a particular 
+type of packet, you should subscribe to the full topic name.  If you want to see all packets, simply subscribe to "meshtastic.receive".
+- meshtastic.receive.user(packet)
+- meshtastic.receive.data(packet)
+- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...)
+
+Example Usage:
+
+```
+import meshtastic
+from pubsub import pub
+
+def onReceive(packet):
+    print(f"Received: {packet}")
+
+interface = StreamInterface() # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
+pub.subscribe(onReceive, "meshtastic.receive")
+interface.sendData("hello world") # defaults to broadcast, specify a destination ID if you wish
+```
+
+"""
+
+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 pubsub import pub
+
+START1 = 0x94
+START2 = 0xc3
+HEADER_LEN = 4
+MAX_TO_FROM_RADIO_SIZE = 512
+
+BROADCAST_ADDR = "all"  # A special ID that means broadcast
+
+
+MY_CONFIG_ID = 42
+
+
+class MeshInterface:
+    """Interface class for meshtastic devices
+    """
+
+    def __init__(self, debugOut=None):
+        """Constructor"""
+        self.debugOut = debugOut
+        self.nodes = None  # FIXME
+        self._startConfig()
+
+    def sendText(self, text, destinationId=BROADCAST_ADDR):
+        """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} -- where to send this message (default: {BROADCAST_ADDR})
+        """
+        self.sendData(text.encode("utf-8"), destinationId,
+                      dataType=mesh_pb2.Data.CLEAR_TEXT)
+
+    def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE):
+        """Send a data packet to some other node"""
+        meshPacket = mesh_pb2.MeshPacket()
+        meshPacket.payload.data.payload = byteData
+        meshPacket.payload.data.typ = dataType
+        self.sendPacket(meshPacket, destinationId)
+
+    def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR):
+        """Send a MeshPacket to the specified node (or if unspecified, broadcast). 
+        You probably don't want this - use sendData instead."""
+        toRadio = mesh_pb2.ToRadio()
+        # FIXME add support for non broadcast addresses
+        meshPacket.to = 255
+        toRadio.packet.CopyFrom(meshPacket)
+        self._sendToRadio(toRadio)
+
+    def _disconnected(self):
+        """Called by subclasses to tell clients this interface has disconnected"""
+        pub.sendMessage("meshtastic.connection.lost")
+
+    def _startConfig(self):
+        """Start device packets flowing"""
+        self.myInfo = None
+        self.nodes = {}  # nodes keyed by ID
+        self._nodesByNum = {}  # nodes keyed by nodenum
+        self.radioConfig = None
+
+        startConfig = mesh_pb2.ToRadio()
+        startConfig.want_config_id = MY_CONFIG_ID  # we don't use this value
+        self._sendToRadio(startConfig)
+
+    def _sendToRadio(self, toRadio):
+        """Send a ToRadio protobuf to the device"""
+        logging.error(f"Subclass must provide toradio: {toRadio}")
+
+    def _handleFromRadio(self, fromRadioBytes):
+        """
+        Handle a packet that arrived from the radio(update model and publish events)
+
+        Called by subclasses."""
+        fromRadio = mesh_pb2.FromRadio()
+        fromRadio.ParseFromString(fromRadioBytes)
+        json = google.protobuf.json_format.MessageToJson(fromRadio)
+        logging.debug(f"Received: {json}")
+        if fromRadio.HasField("my_info"):
+            self.myInfo = fromRadio.my_info
+        elif fromRadio.HasField("radio"):
+            self.radioConfig = fromRadio.radio
+        elif fromRadio.HasField("node_info"):
+            node = fromRadio.node_info
+            self._nodesByNum[node.num] = node
+            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")
+        elif fromRadio.HasField("packet"):
+            self._handlePacketFromRadio(fromRadio.packet)
+        else:
+            logging.warn("Unexpected FromRadio payload")
+
+    def _handlePacketFromRadio(self, meshPacket):
+        """Handle a MeshPacket that just arrived from the radio
+
+        Will publish one of the following events:
+        - meshtastic.receive.position(packet = MeshPacket dictionary)
+        - meshtastic.receive.user(packet = MeshPacket dictionary)
+        - meshtastic.receive.data(packet = MeshPacket dictionary)
+        """
+        # FIXME, update node DB as needed
+        json = google.protobuf.json_format.MessageToDict(meshPacket)
+        if meshPacket.payload.HasField("position"):
+            pub.sendMessage("meshtastic.receive.position", packet=json)
+        if meshPacket.payload.HasField("user"):
+            pub.sendMessage("meshtastic.receive.user",
+                            packet=json)
+        if meshPacket.payload.HasField("data"):
+            pub.sendMessage("meshtastic.receive.data",
+                            packet=json)
+
+
+class StreamInterface(MeshInterface):
+    """Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
+
+    def __init__(self, devPath=None, debugOut=None):
+        """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})
+
+        Raises:
+            Exception: [description]
+            Exception: [description]
+        """
+
+        if devPath is None:
+            ports = list(filter(lambda port: port.vid != None,
+                                serial.tools.list_ports.comports()))
+            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
+
+        logging.debug(f"Connecting to {devPath}")
+        self._rxBuf = bytes()  # empty
+        self._wantExit = False
+        self.stream = serial.Serial(
+            devPath, 921600, exclusive=True, timeout=0.5)
+        self._rxThread = threading.Thread(target=self.__reader, args=())
+        self._rxThread.start()
+        MeshInterface.__init__(self, debugOut=debugOut)
+
+    def _sendToRadio(self, toRadio):
+        """Send a ToRadio protobuf to the device"""
+        logging.debug(f"Sending: {toRadio}")
+        b = toRadio.SerializeToString()
+        bufLen = len(b)
+        header = bytes([START1, START2, (bufLen >> 8) & 0xff,  bufLen & 0xff])
+        self.stream.write(header)
+        self.stream.write(b)
+        self.stream.flush()
+
+    def close(self):
+        """Close a connection to the device"""
+        logging.debug("Closing serial stream")
+        # pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
+        self._wantExit = True
+
+    def __reader(self):
+        """The reader thread that reads bytes from our stream"""
+        empty = bytes()
+
+        while not self._wantExit:
+            b = self.stream.read(1)
+            if len(b) > 0:
+                # logging.debug(f"read returned {b}")
+                c = b[0]
+                ptr = len(self._rxBuf)
+
+                # Assume we want to append this byte, fixme use bytearray instead
+                self._rxBuf = self._rxBuf + b
+
+                if ptr == 0:  # looking for START1
+                    if c != START1:
+                        self._rxBuf = empty  # failed to find start
+                        if self.debugOut != None:
+                            try:
+                                self.debugOut.write(b.decode("utf-8"))
+                            except:
+                                self.debugOut.write('?')
+
+                elif ptr == 1:  # looking for START2
+                    if c != START2:
+                        self.rfBuf = empty  # failed to find start2
+                elif ptr >= HEADER_LEN:  # we've at least got a header
+                    # big endian length follos header
+                    packetlen = (self._rxBuf[2] << 8) + self._rxBuf[3]
+
+                    if ptr == HEADER_LEN:  # we _just_ finished reading the header, validate length
+                        if packetlen > MAX_TO_FROM_RADIO_SIZE:
+                            self.rfBuf = empty  # length ws out out bounds, restart
+
+                    if len(self._rxBuf) != 0 and ptr + 1 == packetlen + HEADER_LEN:
+                        try:
+                            self._handleFromRadio(self._rxBuf[HEADER_LEN:])
+                        except Exception as ex:
+                            logging.warn(
+                                f"Error handling FromRadio, possibly corrupted? {ex}")
+                            traceback.print_exc()
+                        self._rxBuf = empty
+        logging.debug("reader is exiting")
+        self.stream.close()
+        self._disconnected()
+
+
+
+

Sub-modules

+
+
meshtastic.mesh_pb2
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class MeshInterface +(debugOut=None) +
+
+

Interface class for meshtastic devices

+

Constructor

+
+ +Expand source code + +
class MeshInterface:
+    """Interface class for meshtastic devices
+    """
+
+    def __init__(self, debugOut=None):
+        """Constructor"""
+        self.debugOut = debugOut
+        self.nodes = None  # FIXME
+        self._startConfig()
+
+    def sendText(self, text, destinationId=BROADCAST_ADDR):
+        """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} -- where to send this message (default: {BROADCAST_ADDR})
+        """
+        self.sendData(text.encode("utf-8"), destinationId,
+                      dataType=mesh_pb2.Data.CLEAR_TEXT)
+
+    def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE):
+        """Send a data packet to some other node"""
+        meshPacket = mesh_pb2.MeshPacket()
+        meshPacket.payload.data.payload = byteData
+        meshPacket.payload.data.typ = dataType
+        self.sendPacket(meshPacket, destinationId)
+
+    def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR):
+        """Send a MeshPacket to the specified node (or if unspecified, broadcast). 
+        You probably don't want this - use sendData instead."""
+        toRadio = mesh_pb2.ToRadio()
+        # FIXME add support for non broadcast addresses
+        meshPacket.to = 255
+        toRadio.packet.CopyFrom(meshPacket)
+        self._sendToRadio(toRadio)
+
+    def _disconnected(self):
+        """Called by subclasses to tell clients this interface has disconnected"""
+        pub.sendMessage("meshtastic.connection.lost")
+
+    def _startConfig(self):
+        """Start device packets flowing"""
+        self.myInfo = None
+        self.nodes = {}  # nodes keyed by ID
+        self._nodesByNum = {}  # nodes keyed by nodenum
+        self.radioConfig = None
+
+        startConfig = mesh_pb2.ToRadio()
+        startConfig.want_config_id = MY_CONFIG_ID  # we don't use this value
+        self._sendToRadio(startConfig)
+
+    def _sendToRadio(self, toRadio):
+        """Send a ToRadio protobuf to the device"""
+        logging.error(f"Subclass must provide toradio: {toRadio}")
+
+    def _handleFromRadio(self, fromRadioBytes):
+        """
+        Handle a packet that arrived from the radio(update model and publish events)
+
+        Called by subclasses."""
+        fromRadio = mesh_pb2.FromRadio()
+        fromRadio.ParseFromString(fromRadioBytes)
+        json = google.protobuf.json_format.MessageToJson(fromRadio)
+        logging.debug(f"Received: {json}")
+        if fromRadio.HasField("my_info"):
+            self.myInfo = fromRadio.my_info
+        elif fromRadio.HasField("radio"):
+            self.radioConfig = fromRadio.radio
+        elif fromRadio.HasField("node_info"):
+            node = fromRadio.node_info
+            self._nodesByNum[node.num] = node
+            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")
+        elif fromRadio.HasField("packet"):
+            self._handlePacketFromRadio(fromRadio.packet)
+        else:
+            logging.warn("Unexpected FromRadio payload")
+
+    def _handlePacketFromRadio(self, meshPacket):
+        """Handle a MeshPacket that just arrived from the radio
+
+        Will publish one of the following events:
+        - meshtastic.receive.position(packet = MeshPacket dictionary)
+        - meshtastic.receive.user(packet = MeshPacket dictionary)
+        - meshtastic.receive.data(packet = MeshPacket dictionary)
+        """
+        # FIXME, update node DB as needed
+        json = google.protobuf.json_format.MessageToDict(meshPacket)
+        if meshPacket.payload.HasField("position"):
+            pub.sendMessage("meshtastic.receive.position", packet=json)
+        if meshPacket.payload.HasField("user"):
+            pub.sendMessage("meshtastic.receive.user",
+                            packet=json)
+        if meshPacket.payload.HasField("data"):
+            pub.sendMessage("meshtastic.receive.data",
+                            packet=json)
+
+

Subclasses

+ +

Methods

+
+
+def sendData(self, byteData, destinationId='all', dataType=0) +
+
+

Send a data packet to some other node

+
+ +Expand source code + +
def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE):
+    """Send a data packet to some other node"""
+    meshPacket = mesh_pb2.MeshPacket()
+    meshPacket.payload.data.payload = byteData
+    meshPacket.payload.data.typ = dataType
+    self.sendPacket(meshPacket, destinationId)
+
+
+
+def sendPacket(self, meshPacket, destinationId='all') +
+
+

Send a MeshPacket to the specified node (or if unspecified, broadcast). +You probably don't want this - use sendData instead.

+
+ +Expand source code + +
def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR):
+    """Send a MeshPacket to the specified node (or if unspecified, broadcast). 
+    You probably don't want this - use sendData instead."""
+    toRadio = mesh_pb2.ToRadio()
+    # FIXME add support for non broadcast addresses
+    meshPacket.to = 255
+    toRadio.packet.CopyFrom(meshPacket)
+    self._sendToRadio(toRadio)
+
+
+
+def sendText(self, text, destinationId='all') +
+
+

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} – where to send this message (default: {BROADCAST_ADDR})

+
+ +Expand source code + +
def sendText(self, text, destinationId=BROADCAST_ADDR):
+    """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} -- where to send this message (default: {BROADCAST_ADDR})
+    """
+    self.sendData(text.encode("utf-8"), destinationId,
+                  dataType=mesh_pb2.Data.CLEAR_TEXT)
+
+
+
+
+
+class StreamInterface +(devPath=None, debugOut=None) +
+
+

Interface class for meshtastic devices over a stream link (serial, TCP, etc)

+

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})

+

Raises

+
+
Exception
+
[description]
+
Exception
+
[description]
+
+
+ +Expand source code + +
class StreamInterface(MeshInterface):
+    """Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
+
+    def __init__(self, devPath=None, debugOut=None):
+        """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})
+
+        Raises:
+            Exception: [description]
+            Exception: [description]
+        """
+
+        if devPath is None:
+            ports = list(filter(lambda port: port.vid != None,
+                                serial.tools.list_ports.comports()))
+            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
+
+        logging.debug(f"Connecting to {devPath}")
+        self._rxBuf = bytes()  # empty
+        self._wantExit = False
+        self.stream = serial.Serial(
+            devPath, 921600, exclusive=True, timeout=0.5)
+        self._rxThread = threading.Thread(target=self.__reader, args=())
+        self._rxThread.start()
+        MeshInterface.__init__(self, debugOut=debugOut)
+
+    def _sendToRadio(self, toRadio):
+        """Send a ToRadio protobuf to the device"""
+        logging.debug(f"Sending: {toRadio}")
+        b = toRadio.SerializeToString()
+        bufLen = len(b)
+        header = bytes([START1, START2, (bufLen >> 8) & 0xff,  bufLen & 0xff])
+        self.stream.write(header)
+        self.stream.write(b)
+        self.stream.flush()
+
+    def close(self):
+        """Close a connection to the device"""
+        logging.debug("Closing serial stream")
+        # pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
+        self._wantExit = True
+
+    def __reader(self):
+        """The reader thread that reads bytes from our stream"""
+        empty = bytes()
+
+        while not self._wantExit:
+            b = self.stream.read(1)
+            if len(b) > 0:
+                # logging.debug(f"read returned {b}")
+                c = b[0]
+                ptr = len(self._rxBuf)
+
+                # Assume we want to append this byte, fixme use bytearray instead
+                self._rxBuf = self._rxBuf + b
+
+                if ptr == 0:  # looking for START1
+                    if c != START1:
+                        self._rxBuf = empty  # failed to find start
+                        if self.debugOut != None:
+                            try:
+                                self.debugOut.write(b.decode("utf-8"))
+                            except:
+                                self.debugOut.write('?')
+
+                elif ptr == 1:  # looking for START2
+                    if c != START2:
+                        self.rfBuf = empty  # failed to find start2
+                elif ptr >= HEADER_LEN:  # we've at least got a header
+                    # big endian length follos header
+                    packetlen = (self._rxBuf[2] << 8) + self._rxBuf[3]
+
+                    if ptr == HEADER_LEN:  # we _just_ finished reading the header, validate length
+                        if packetlen > MAX_TO_FROM_RADIO_SIZE:
+                            self.rfBuf = empty  # length ws out out bounds, restart
+
+                    if len(self._rxBuf) != 0 and ptr + 1 == packetlen + HEADER_LEN:
+                        try:
+                            self._handleFromRadio(self._rxBuf[HEADER_LEN:])
+                        except Exception as ex:
+                            logging.warn(
+                                f"Error handling FromRadio, possibly corrupted? {ex}")
+                            traceback.print_exc()
+                        self._rxBuf = empty
+        logging.debug("reader is exiting")
+        self.stream.close()
+        self._disconnected()
+
+

Ancestors

+ +

Methods

+
+
+def close(self) +
+
+

Close a connection to the device

+
+ +Expand source code + +
def close(self):
+    """Close a connection to the device"""
+    logging.debug("Closing serial stream")
+    # pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
+    self._wantExit = True
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/doc/meshtastic/mesh_pb2.html b/doc/meshtastic/mesh_pb2.html new file mode 100644 index 0000000..b69077c --- /dev/null +++ b/doc/meshtastic/mesh_pb2.html @@ -0,0 +1,1941 @@ + + + + + + +meshtastic.mesh_pb2 API documentation + + + + + + + + + +
+
+
+

Module meshtastic.mesh_pb2

+
+
+
+ +Expand source code + +
# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: mesh.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='mesh.proto',
+  package='',
+  syntax='proto3',
+  serialized_options=_b('\n\023com.geeksville.meshB\nMeshProtos'),
+  serialized_pb=_b('\n\nmesh.proto\"f\n\x08Position\x12\x10\n\x08latitude\x18\x01 \x01(\x01\x12\x11\n\tlongitude\x18\x02 \x01(\x01\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x15\n\rbattery_level\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x06 \x01(\r\"g\n\x04\x44\x61ta\x12\x17\n\x03typ\x18\x01 \x01(\x0e\x32\n.Data.Type\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\"5\n\x04Type\x12\n\n\x06OPAQUE\x10\x00\x12\x0e\n\nCLEAR_TEXT\x10\x01\x12\x11\n\rCLEAR_READACK\x10\x02\"J\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x0f\n\x07macaddr\x18\x04 \x01(\x0c\"\x1f\n\x0eRouteDiscovery\x12\r\n\x05route\x18\x02 \x03(\x05\"i\n\tSubPacket\x12\x1b\n\x08position\x18\x01 \x01(\x0b\x32\t.Position\x12\x13\n\x04\x64\x61ta\x18\x03 \x01(\x0b\x32\x05.Data\x12\x13\n\x04user\x18\x04 \x01(\x0b\x32\x05.User\x12\x15\n\rwant_response\x18\x05 \x01(\x08\"p\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x05\x12\n\n\x02to\x18\x02 \x01(\x05\x12\x1b\n\x07payload\x18\x03 \x01(\x0b\x32\n.SubPacket\x12\x0f\n\x07rx_time\x18\x04 \x01(\r\x12\x0e\n\x06rx_snr\x18\x05 \x01(\x11\x12\n\n\x02id\x18\x06 \x01(\r\"\xd4\x01\n\x0f\x43hannelSettings\x12\x10\n\x08tx_power\x18\x01 \x01(\x05\x12\x32\n\x0cmodem_config\x18\x03 \x01(\x0e\x32\x1c.ChannelSettings.ModemConfig\x12\x0b\n\x03psk\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\"`\n\x0bModemConfig\x12\x12\n\x0e\x42w125Cr45Sf128\x10\x00\x12\x12\n\x0e\x42w500Cr45Sf128\x10\x01\x12\x14\n\x10\x42w31_25Cr48Sf512\x10\x02\x12\x13\n\x0f\x42w125Cr48Sf4096\x10\x03\"\xd7\x03\n\x0bRadioConfig\x12\x31\n\x0bpreferences\x18\x01 \x01(\x0b\x32\x1c.RadioConfig.UserPreferences\x12*\n\x10\x63hannel_settings\x18\x02 \x01(\x0b\x32\x10.ChannelSettings\x1a\xe8\x02\n\x0fUserPreferences\x12\x1f\n\x17position_broadcast_secs\x18\x01 \x01(\r\x12\x1b\n\x13send_owner_interval\x18\x02 \x01(\r\x12\x1a\n\x12num_missed_to_fail\x18\x03 \x01(\r\x12\x1b\n\x13wait_bluetooth_secs\x18\x04 \x01(\r\x12\x16\n\x0escreen_on_secs\x18\x05 \x01(\r\x12\x1a\n\x12phone_timeout_secs\x18\x06 \x01(\r\x12\x1d\n\x15phone_sds_timeout_sec\x18\x07 \x01(\r\x12\x1d\n\x15mesh_sds_timeout_secs\x18\x08 \x01(\r\x12\x10\n\x08sds_secs\x18\t \x01(\r\x12\x0f\n\x07ls_secs\x18\n \x01(\r\x12\x15\n\rmin_wake_secs\x18\x0b \x01(\r\x12\x18\n\x10keep_all_packets\x18\x64 \x01(\x08\x12\x18\n\x10promiscuous_mode\x18\x65 \x01(\x08\"o\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\x05\x12\x13\n\x04user\x18\x02 \x01(\x0b\x32\x05.User\x12\x1b\n\x08position\x18\x03 \x01(\x0b\x32\t.Position\x12\x0b\n\x03snr\x18\x05 \x01(\x05\x12\x17\n\x0f\x66requency_error\x18\x06 \x01(\x05\"\xc4\x01\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\x05\x12\x0f\n\x07has_gps\x18\x02 \x01(\x08\x12\x14\n\x0cnum_channels\x18\x03 \x01(\x05\x12\x0e\n\x06region\x18\x04 \x01(\t\x12\x10\n\x08hw_model\x18\x05 \x01(\t\x12\x18\n\x10\x66irmware_version\x18\x06 \x01(\t\x12\x12\n\nerror_code\x18\x07 \x01(\r\x12\x15\n\rerror_address\x18\x08 \x01(\r\x12\x13\n\x0b\x65rror_count\x18\t \x01(\r\"\xd5\x01\n\x0b\x44\x65viceState\x12\x1b\n\x05radio\x18\x01 \x01(\x0b\x32\x0c.RadioConfig\x12\x1c\n\x07my_node\x18\x02 \x01(\x0b\x32\x0b.MyNodeInfo\x12\x14\n\x05owner\x18\x03 \x01(\x0b\x32\x05.User\x12\x1a\n\x07node_db\x18\x04 \x03(\x0b\x32\t.NodeInfo\x12\"\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x0b.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12$\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x0b.MeshPacket\"\x1e\n\x0b\x44\x65\x62ugString\x12\x0f\n\x07message\x18\x01 \x01(\t\"\xe5\x01\n\tFromRadio\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x1d\n\x06packet\x18\x02 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x1e\n\x07my_info\x18\x03 \x01(\x0b\x32\x0b.MyNodeInfoH\x00\x12\x1e\n\tnode_info\x18\x04 \x01(\x0b\x32\t.NodeInfoH\x00\x12\x1d\n\x05radio\x18\x06 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12$\n\x0c\x64\x65\x62ug_string\x18\x07 \x01(\x0b\x32\x0c.DebugStringH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x08 \x01(\rH\x00\x42\t\n\x07variant\"\x8c\x01\n\x07ToRadio\x12\x1d\n\x06packet\x18\x01 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x64 \x01(\rH\x00\x12!\n\tset_radio\x18\x65 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1a\n\tset_owner\x18\x66 \x01(\x0b\x32\x05.UserH\x00\x42\t\n\x07variant*\x17\n\tConstants\x12\n\n\x06Unused\x10\x00\x42!\n\x13\x63om.geeksville.meshB\nMeshProtosb\x06proto3')
+)
+
+_CONSTANTS = _descriptor.EnumDescriptor(
+  name='Constants',
+  full_name='Constants',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='Unused', index=0, number=0,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=2177,
+  serialized_end=2200,
+)
+_sym_db.RegisterEnumDescriptor(_CONSTANTS)
+
+Constants = enum_type_wrapper.EnumTypeWrapper(_CONSTANTS)
+Unused = 0
+
+
+_DATA_TYPE = _descriptor.EnumDescriptor(
+  name='Type',
+  full_name='Data.Type',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='OPAQUE', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='CLEAR_TEXT', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='CLEAR_READACK', index=2, number=2,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=168,
+  serialized_end=221,
+)
+_sym_db.RegisterEnumDescriptor(_DATA_TYPE)
+
+_CHANNELSETTINGS_MODEMCONFIG = _descriptor.EnumDescriptor(
+  name='ModemConfig',
+  full_name='ChannelSettings.ModemConfig',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='Bw125Cr45Sf128', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='Bw500Cr45Sf128', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='Bw31_25Cr48Sf512', index=2, number=2,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='Bw125Cr48Sf4096', index=3, number=3,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=670,
+  serialized_end=766,
+)
+_sym_db.RegisterEnumDescriptor(_CHANNELSETTINGS_MODEMCONFIG)
+
+
+_POSITION = _descriptor.Descriptor(
+  name='Position',
+  full_name='Position',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='latitude', full_name='Position.latitude', index=0,
+      number=1, type=1, cpp_type=5, label=1,
+      has_default_value=False, default_value=float(0),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='longitude', full_name='Position.longitude', index=1,
+      number=2, type=1, cpp_type=5, label=1,
+      has_default_value=False, default_value=float(0),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='altitude', full_name='Position.altitude', index=2,
+      number=3, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='battery_level', full_name='Position.battery_level', index=3,
+      number=4, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='time', full_name='Position.time', index=4,
+      number=6, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=14,
+  serialized_end=116,
+)
+
+
+_DATA = _descriptor.Descriptor(
+  name='Data',
+  full_name='Data',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='typ', full_name='Data.typ', index=0,
+      number=1, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='payload', full_name='Data.payload', index=1,
+      number=2, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b(""),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+    _DATA_TYPE,
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=118,
+  serialized_end=221,
+)
+
+
+_USER = _descriptor.Descriptor(
+  name='User',
+  full_name='User',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='id', full_name='User.id', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='long_name', full_name='User.long_name', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='short_name', full_name='User.short_name', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='macaddr', full_name='User.macaddr', index=3,
+      number=4, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b(""),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=223,
+  serialized_end=297,
+)
+
+
+_ROUTEDISCOVERY = _descriptor.Descriptor(
+  name='RouteDiscovery',
+  full_name='RouteDiscovery',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='route', full_name='RouteDiscovery.route', index=0,
+      number=2, type=5, cpp_type=1, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=299,
+  serialized_end=330,
+)
+
+
+_SUBPACKET = _descriptor.Descriptor(
+  name='SubPacket',
+  full_name='SubPacket',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='position', full_name='SubPacket.position', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='data', full_name='SubPacket.data', index=1,
+      number=3, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='user', full_name='SubPacket.user', index=2,
+      number=4, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='want_response', full_name='SubPacket.want_response', index=3,
+      number=5, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=332,
+  serialized_end=437,
+)
+
+
+_MESHPACKET = _descriptor.Descriptor(
+  name='MeshPacket',
+  full_name='MeshPacket',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='from', full_name='MeshPacket.from', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='to', full_name='MeshPacket.to', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='payload', full_name='MeshPacket.payload', index=2,
+      number=3, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='rx_time', full_name='MeshPacket.rx_time', index=3,
+      number=4, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='rx_snr', full_name='MeshPacket.rx_snr', index=4,
+      number=5, type=17, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='id', full_name='MeshPacket.id', index=5,
+      number=6, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=439,
+  serialized_end=551,
+)
+
+
+_CHANNELSETTINGS = _descriptor.Descriptor(
+  name='ChannelSettings',
+  full_name='ChannelSettings',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='tx_power', full_name='ChannelSettings.tx_power', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='modem_config', full_name='ChannelSettings.modem_config', index=1,
+      number=3, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='psk', full_name='ChannelSettings.psk', index=2,
+      number=4, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b(""),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='name', full_name='ChannelSettings.name', index=3,
+      number=5, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+    _CHANNELSETTINGS_MODEMCONFIG,
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=554,
+  serialized_end=766,
+)
+
+
+_RADIOCONFIG_USERPREFERENCES = _descriptor.Descriptor(
+  name='UserPreferences',
+  full_name='RadioConfig.UserPreferences',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='position_broadcast_secs', full_name='RadioConfig.UserPreferences.position_broadcast_secs', index=0,
+      number=1, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='send_owner_interval', full_name='RadioConfig.UserPreferences.send_owner_interval', index=1,
+      number=2, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='num_missed_to_fail', full_name='RadioConfig.UserPreferences.num_missed_to_fail', index=2,
+      number=3, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='wait_bluetooth_secs', full_name='RadioConfig.UserPreferences.wait_bluetooth_secs', index=3,
+      number=4, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='screen_on_secs', full_name='RadioConfig.UserPreferences.screen_on_secs', index=4,
+      number=5, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='phone_timeout_secs', full_name='RadioConfig.UserPreferences.phone_timeout_secs', index=5,
+      number=6, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='phone_sds_timeout_sec', full_name='RadioConfig.UserPreferences.phone_sds_timeout_sec', index=6,
+      number=7, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='mesh_sds_timeout_secs', full_name='RadioConfig.UserPreferences.mesh_sds_timeout_secs', index=7,
+      number=8, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='sds_secs', full_name='RadioConfig.UserPreferences.sds_secs', index=8,
+      number=9, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='ls_secs', full_name='RadioConfig.UserPreferences.ls_secs', index=9,
+      number=10, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='min_wake_secs', full_name='RadioConfig.UserPreferences.min_wake_secs', index=10,
+      number=11, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='keep_all_packets', full_name='RadioConfig.UserPreferences.keep_all_packets', index=11,
+      number=100, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='promiscuous_mode', full_name='RadioConfig.UserPreferences.promiscuous_mode', index=12,
+      number=101, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=880,
+  serialized_end=1240,
+)
+
+_RADIOCONFIG = _descriptor.Descriptor(
+  name='RadioConfig',
+  full_name='RadioConfig',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='preferences', full_name='RadioConfig.preferences', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='channel_settings', full_name='RadioConfig.channel_settings', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[_RADIOCONFIG_USERPREFERENCES, ],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=769,
+  serialized_end=1240,
+)
+
+
+_NODEINFO = _descriptor.Descriptor(
+  name='NodeInfo',
+  full_name='NodeInfo',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='num', full_name='NodeInfo.num', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='user', full_name='NodeInfo.user', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='position', full_name='NodeInfo.position', index=2,
+      number=3, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='snr', full_name='NodeInfo.snr', index=3,
+      number=5, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='frequency_error', full_name='NodeInfo.frequency_error', index=4,
+      number=6, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1242,
+  serialized_end=1353,
+)
+
+
+_MYNODEINFO = _descriptor.Descriptor(
+  name='MyNodeInfo',
+  full_name='MyNodeInfo',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='my_node_num', full_name='MyNodeInfo.my_node_num', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='has_gps', full_name='MyNodeInfo.has_gps', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='num_channels', full_name='MyNodeInfo.num_channels', index=2,
+      number=3, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='region', full_name='MyNodeInfo.region', index=3,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='hw_model', full_name='MyNodeInfo.hw_model', index=4,
+      number=5, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='firmware_version', full_name='MyNodeInfo.firmware_version', index=5,
+      number=6, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='error_code', full_name='MyNodeInfo.error_code', index=6,
+      number=7, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='error_address', full_name='MyNodeInfo.error_address', index=7,
+      number=8, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='error_count', full_name='MyNodeInfo.error_count', index=8,
+      number=9, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1356,
+  serialized_end=1552,
+)
+
+
+_DEVICESTATE = _descriptor.Descriptor(
+  name='DeviceState',
+  full_name='DeviceState',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='radio', full_name='DeviceState.radio', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='my_node', full_name='DeviceState.my_node', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='owner', full_name='DeviceState.owner', index=2,
+      number=3, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='node_db', full_name='DeviceState.node_db', index=3,
+      number=4, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='receive_queue', full_name='DeviceState.receive_queue', index=4,
+      number=5, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='version', full_name='DeviceState.version', index=5,
+      number=8, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='rx_text_message', full_name='DeviceState.rx_text_message', index=6,
+      number=7, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1555,
+  serialized_end=1768,
+)
+
+
+_DEBUGSTRING = _descriptor.Descriptor(
+  name='DebugString',
+  full_name='DebugString',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='message', full_name='DebugString.message', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1770,
+  serialized_end=1800,
+)
+
+
+_FROMRADIO = _descriptor.Descriptor(
+  name='FromRadio',
+  full_name='FromRadio',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='num', full_name='FromRadio.num', index=0,
+      number=1, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='packet', full_name='FromRadio.packet', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='my_info', full_name='FromRadio.my_info', index=2,
+      number=3, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='node_info', full_name='FromRadio.node_info', index=3,
+      number=4, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='radio', full_name='FromRadio.radio', index=4,
+      number=6, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='debug_string', full_name='FromRadio.debug_string', index=5,
+      number=7, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='config_complete_id', full_name='FromRadio.config_complete_id', index=6,
+      number=8, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+    _descriptor.OneofDescriptor(
+      name='variant', full_name='FromRadio.variant',
+      index=0, containing_type=None, fields=[]),
+  ],
+  serialized_start=1803,
+  serialized_end=2032,
+)
+
+
+_TORADIO = _descriptor.Descriptor(
+  name='ToRadio',
+  full_name='ToRadio',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='packet', full_name='ToRadio.packet', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='want_config_id', full_name='ToRadio.want_config_id', index=1,
+      number=100, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='set_radio', full_name='ToRadio.set_radio', index=2,
+      number=101, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='set_owner', full_name='ToRadio.set_owner', index=3,
+      number=102, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+    _descriptor.OneofDescriptor(
+      name='variant', full_name='ToRadio.variant',
+      index=0, containing_type=None, fields=[]),
+  ],
+  serialized_start=2035,
+  serialized_end=2175,
+)
+
+_DATA.fields_by_name['typ'].enum_type = _DATA_TYPE
+_DATA_TYPE.containing_type = _DATA
+_SUBPACKET.fields_by_name['position'].message_type = _POSITION
+_SUBPACKET.fields_by_name['data'].message_type = _DATA
+_SUBPACKET.fields_by_name['user'].message_type = _USER
+_MESHPACKET.fields_by_name['payload'].message_type = _SUBPACKET
+_CHANNELSETTINGS.fields_by_name['modem_config'].enum_type = _CHANNELSETTINGS_MODEMCONFIG
+_CHANNELSETTINGS_MODEMCONFIG.containing_type = _CHANNELSETTINGS
+_RADIOCONFIG_USERPREFERENCES.containing_type = _RADIOCONFIG
+_RADIOCONFIG.fields_by_name['preferences'].message_type = _RADIOCONFIG_USERPREFERENCES
+_RADIOCONFIG.fields_by_name['channel_settings'].message_type = _CHANNELSETTINGS
+_NODEINFO.fields_by_name['user'].message_type = _USER
+_NODEINFO.fields_by_name['position'].message_type = _POSITION
+_DEVICESTATE.fields_by_name['radio'].message_type = _RADIOCONFIG
+_DEVICESTATE.fields_by_name['my_node'].message_type = _MYNODEINFO
+_DEVICESTATE.fields_by_name['owner'].message_type = _USER
+_DEVICESTATE.fields_by_name['node_db'].message_type = _NODEINFO
+_DEVICESTATE.fields_by_name['receive_queue'].message_type = _MESHPACKET
+_DEVICESTATE.fields_by_name['rx_text_message'].message_type = _MESHPACKET
+_FROMRADIO.fields_by_name['packet'].message_type = _MESHPACKET
+_FROMRADIO.fields_by_name['my_info'].message_type = _MYNODEINFO
+_FROMRADIO.fields_by_name['node_info'].message_type = _NODEINFO
+_FROMRADIO.fields_by_name['radio'].message_type = _RADIOCONFIG
+_FROMRADIO.fields_by_name['debug_string'].message_type = _DEBUGSTRING
+_FROMRADIO.oneofs_by_name['variant'].fields.append(
+  _FROMRADIO.fields_by_name['packet'])
+_FROMRADIO.fields_by_name['packet'].containing_oneof = _FROMRADIO.oneofs_by_name['variant']
+_FROMRADIO.oneofs_by_name['variant'].fields.append(
+  _FROMRADIO.fields_by_name['my_info'])
+_FROMRADIO.fields_by_name['my_info'].containing_oneof = _FROMRADIO.oneofs_by_name['variant']
+_FROMRADIO.oneofs_by_name['variant'].fields.append(
+  _FROMRADIO.fields_by_name['node_info'])
+_FROMRADIO.fields_by_name['node_info'].containing_oneof = _FROMRADIO.oneofs_by_name['variant']
+_FROMRADIO.oneofs_by_name['variant'].fields.append(
+  _FROMRADIO.fields_by_name['radio'])
+_FROMRADIO.fields_by_name['radio'].containing_oneof = _FROMRADIO.oneofs_by_name['variant']
+_FROMRADIO.oneofs_by_name['variant'].fields.append(
+  _FROMRADIO.fields_by_name['debug_string'])
+_FROMRADIO.fields_by_name['debug_string'].containing_oneof = _FROMRADIO.oneofs_by_name['variant']
+_FROMRADIO.oneofs_by_name['variant'].fields.append(
+  _FROMRADIO.fields_by_name['config_complete_id'])
+_FROMRADIO.fields_by_name['config_complete_id'].containing_oneof = _FROMRADIO.oneofs_by_name['variant']
+_TORADIO.fields_by_name['packet'].message_type = _MESHPACKET
+_TORADIO.fields_by_name['set_radio'].message_type = _RADIOCONFIG
+_TORADIO.fields_by_name['set_owner'].message_type = _USER
+_TORADIO.oneofs_by_name['variant'].fields.append(
+  _TORADIO.fields_by_name['packet'])
+_TORADIO.fields_by_name['packet'].containing_oneof = _TORADIO.oneofs_by_name['variant']
+_TORADIO.oneofs_by_name['variant'].fields.append(
+  _TORADIO.fields_by_name['want_config_id'])
+_TORADIO.fields_by_name['want_config_id'].containing_oneof = _TORADIO.oneofs_by_name['variant']
+_TORADIO.oneofs_by_name['variant'].fields.append(
+  _TORADIO.fields_by_name['set_radio'])
+_TORADIO.fields_by_name['set_radio'].containing_oneof = _TORADIO.oneofs_by_name['variant']
+_TORADIO.oneofs_by_name['variant'].fields.append(
+  _TORADIO.fields_by_name['set_owner'])
+_TORADIO.fields_by_name['set_owner'].containing_oneof = _TORADIO.oneofs_by_name['variant']
+DESCRIPTOR.message_types_by_name['Position'] = _POSITION
+DESCRIPTOR.message_types_by_name['Data'] = _DATA
+DESCRIPTOR.message_types_by_name['User'] = _USER
+DESCRIPTOR.message_types_by_name['RouteDiscovery'] = _ROUTEDISCOVERY
+DESCRIPTOR.message_types_by_name['SubPacket'] = _SUBPACKET
+DESCRIPTOR.message_types_by_name['MeshPacket'] = _MESHPACKET
+DESCRIPTOR.message_types_by_name['ChannelSettings'] = _CHANNELSETTINGS
+DESCRIPTOR.message_types_by_name['RadioConfig'] = _RADIOCONFIG
+DESCRIPTOR.message_types_by_name['NodeInfo'] = _NODEINFO
+DESCRIPTOR.message_types_by_name['MyNodeInfo'] = _MYNODEINFO
+DESCRIPTOR.message_types_by_name['DeviceState'] = _DEVICESTATE
+DESCRIPTOR.message_types_by_name['DebugString'] = _DEBUGSTRING
+DESCRIPTOR.message_types_by_name['FromRadio'] = _FROMRADIO
+DESCRIPTOR.message_types_by_name['ToRadio'] = _TORADIO
+DESCRIPTOR.enum_types_by_name['Constants'] = _CONSTANTS
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Position = _reflection.GeneratedProtocolMessageType('Position', (_message.Message,), dict(
+  DESCRIPTOR = _POSITION,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:Position)
+  ))
+_sym_db.RegisterMessage(Position)
+
+Data = _reflection.GeneratedProtocolMessageType('Data', (_message.Message,), dict(
+  DESCRIPTOR = _DATA,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:Data)
+  ))
+_sym_db.RegisterMessage(Data)
+
+User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), dict(
+  DESCRIPTOR = _USER,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:User)
+  ))
+_sym_db.RegisterMessage(User)
+
+RouteDiscovery = _reflection.GeneratedProtocolMessageType('RouteDiscovery', (_message.Message,), dict(
+  DESCRIPTOR = _ROUTEDISCOVERY,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:RouteDiscovery)
+  ))
+_sym_db.RegisterMessage(RouteDiscovery)
+
+SubPacket = _reflection.GeneratedProtocolMessageType('SubPacket', (_message.Message,), dict(
+  DESCRIPTOR = _SUBPACKET,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:SubPacket)
+  ))
+_sym_db.RegisterMessage(SubPacket)
+
+MeshPacket = _reflection.GeneratedProtocolMessageType('MeshPacket', (_message.Message,), dict(
+  DESCRIPTOR = _MESHPACKET,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:MeshPacket)
+  ))
+_sym_db.RegisterMessage(MeshPacket)
+
+ChannelSettings = _reflection.GeneratedProtocolMessageType('ChannelSettings', (_message.Message,), dict(
+  DESCRIPTOR = _CHANNELSETTINGS,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:ChannelSettings)
+  ))
+_sym_db.RegisterMessage(ChannelSettings)
+
+RadioConfig = _reflection.GeneratedProtocolMessageType('RadioConfig', (_message.Message,), dict(
+
+  UserPreferences = _reflection.GeneratedProtocolMessageType('UserPreferences', (_message.Message,), dict(
+    DESCRIPTOR = _RADIOCONFIG_USERPREFERENCES,
+    __module__ = 'mesh_pb2'
+    # @@protoc_insertion_point(class_scope:RadioConfig.UserPreferences)
+    ))
+  ,
+  DESCRIPTOR = _RADIOCONFIG,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:RadioConfig)
+  ))
+_sym_db.RegisterMessage(RadioConfig)
+_sym_db.RegisterMessage(RadioConfig.UserPreferences)
+
+NodeInfo = _reflection.GeneratedProtocolMessageType('NodeInfo', (_message.Message,), dict(
+  DESCRIPTOR = _NODEINFO,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:NodeInfo)
+  ))
+_sym_db.RegisterMessage(NodeInfo)
+
+MyNodeInfo = _reflection.GeneratedProtocolMessageType('MyNodeInfo', (_message.Message,), dict(
+  DESCRIPTOR = _MYNODEINFO,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:MyNodeInfo)
+  ))
+_sym_db.RegisterMessage(MyNodeInfo)
+
+DeviceState = _reflection.GeneratedProtocolMessageType('DeviceState', (_message.Message,), dict(
+  DESCRIPTOR = _DEVICESTATE,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:DeviceState)
+  ))
+_sym_db.RegisterMessage(DeviceState)
+
+DebugString = _reflection.GeneratedProtocolMessageType('DebugString', (_message.Message,), dict(
+  DESCRIPTOR = _DEBUGSTRING,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:DebugString)
+  ))
+_sym_db.RegisterMessage(DebugString)
+
+FromRadio = _reflection.GeneratedProtocolMessageType('FromRadio', (_message.Message,), dict(
+  DESCRIPTOR = _FROMRADIO,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:FromRadio)
+  ))
+_sym_db.RegisterMessage(FromRadio)
+
+ToRadio = _reflection.GeneratedProtocolMessageType('ToRadio', (_message.Message,), dict(
+  DESCRIPTOR = _TORADIO,
+  __module__ = 'mesh_pb2'
+  # @@protoc_insertion_point(class_scope:ToRadio)
+  ))
+_sym_db.RegisterMessage(ToRadio)
+
+
+DESCRIPTOR._options = None
+# @@protoc_insertion_point(module_scope)
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ChannelSettings +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var Bw125Cr45Sf128
+
+
+
+
var Bw125Cr48Sf4096
+
+
+
+
var Bw31_25Cr48Sf512
+
+
+
+
var Bw500Cr45Sf128
+
+
+
+
var DESCRIPTOR
+
+
+
+
var MODEM_CONFIG_FIELD_NUMBER
+
+
+
+
var ModemConfig
+
+
+
+
var NAME_FIELD_NUMBER
+
+
+
+
var PSK_FIELD_NUMBER
+
+
+
+
var TX_POWER_FIELD_NUMBER
+
+
+
+
+
+
+class Data +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var CLEAR_READACK
+
+
+
+
var CLEAR_TEXT
+
+
+
+
var DESCRIPTOR
+
+
+
+
var OPAQUE
+
+
+
+
var PAYLOAD_FIELD_NUMBER
+
+
+
+
var TYP_FIELD_NUMBER
+
+
+
+
var Type
+
+
+
+
+
+
+class DebugString +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var MESSAGE_FIELD_NUMBER
+
+
+
+
+
+
+class DeviceState +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var MY_NODE_FIELD_NUMBER
+
+
+
+
var NODE_DB_FIELD_NUMBER
+
+
+
+
var OWNER_FIELD_NUMBER
+
+
+
+
var RADIO_FIELD_NUMBER
+
+
+
+
var RECEIVE_QUEUE_FIELD_NUMBER
+
+
+
+
var RX_TEXT_MESSAGE_FIELD_NUMBER
+
+
+
+
var VERSION_FIELD_NUMBER
+
+
+
+
+
+
+class FromRadio +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var CONFIG_COMPLETE_ID_FIELD_NUMBER
+
+
+
+
var DEBUG_STRING_FIELD_NUMBER
+
+
+
+
var DESCRIPTOR
+
+
+
+
var MY_INFO_FIELD_NUMBER
+
+
+
+
var NODE_INFO_FIELD_NUMBER
+
+
+
+
var NUM_FIELD_NUMBER
+
+
+
+
var PACKET_FIELD_NUMBER
+
+
+
+
var RADIO_FIELD_NUMBER
+
+
+
+
+
+
+class MeshPacket +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var FROM_FIELD_NUMBER
+
+
+
+
var ID_FIELD_NUMBER
+
+
+
+
var PAYLOAD_FIELD_NUMBER
+
+
+
+
var RX_SNR_FIELD_NUMBER
+
+
+
+
var RX_TIME_FIELD_NUMBER
+
+
+
+
var TO_FIELD_NUMBER
+
+
+
+
+
+
+class MyNodeInfo +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var ERROR_ADDRESS_FIELD_NUMBER
+
+
+
+
var ERROR_CODE_FIELD_NUMBER
+
+
+
+
var ERROR_COUNT_FIELD_NUMBER
+
+
+
+
var FIRMWARE_VERSION_FIELD_NUMBER
+
+
+
+
var HAS_GPS_FIELD_NUMBER
+
+
+
+
var HW_MODEL_FIELD_NUMBER
+
+
+
+
var MY_NODE_NUM_FIELD_NUMBER
+
+
+
+
var NUM_CHANNELS_FIELD_NUMBER
+
+
+
+
var REGION_FIELD_NUMBER
+
+
+
+
+
+
+class NodeInfo +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var FREQUENCY_ERROR_FIELD_NUMBER
+
+
+
+
var NUM_FIELD_NUMBER
+
+
+
+
var POSITION_FIELD_NUMBER
+
+
+
+
var SNR_FIELD_NUMBER
+
+
+
+
var USER_FIELD_NUMBER
+
+
+
+
+
+
+class Position +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var ALTITUDE_FIELD_NUMBER
+
+
+
+
var BATTERY_LEVEL_FIELD_NUMBER
+
+
+
+
var DESCRIPTOR
+
+
+
+
var LATITUDE_FIELD_NUMBER
+
+
+
+
var LONGITUDE_FIELD_NUMBER
+
+
+
+
var TIME_FIELD_NUMBER
+
+
+
+
+
+
+class RadioConfig +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var CHANNEL_SETTINGS_FIELD_NUMBER
+
+
+
+
var DESCRIPTOR
+
+
+
+
var PREFERENCES_FIELD_NUMBER
+
+
+
+
var UserPreferences
+
+

A ProtocolMessage

+
+
+
+
+class RouteDiscovery +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var ROUTE_FIELD_NUMBER
+
+
+
+
+
+
+class SubPacket +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DATA_FIELD_NUMBER
+
+
+
+
var DESCRIPTOR
+
+
+
+
var POSITION_FIELD_NUMBER
+
+
+
+
var USER_FIELD_NUMBER
+
+
+
+
var WANT_RESPONSE_FIELD_NUMBER
+
+
+
+
+
+
+class ToRadio +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var PACKET_FIELD_NUMBER
+
+
+
+
var SET_OWNER_FIELD_NUMBER
+
+
+
+
var SET_RADIO_FIELD_NUMBER
+
+
+
+
var WANT_CONFIG_ID_FIELD_NUMBER
+
+
+
+
+
+
+class User +(...) +
+
+

A ProtocolMessage

+

Ancestors

+
    +
  • google.protobuf.pyext._message.CMessage
  • +
  • google.protobuf.message.Message
  • +
+

Class variables

+
+
var DESCRIPTOR
+
+
+
+
var ID_FIELD_NUMBER
+
+
+
+
var LONG_NAME_FIELD_NUMBER
+
+
+
+
var MACADDR_FIELD_NUMBER
+
+
+
+
var SHORT_NAME_FIELD_NUMBER
+
+
+
+
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py index f916634..f9b728b 100644 --- a/meshtastic/__init__.py +++ b/meshtastic/__init__.py @@ -1,4 +1,262 @@ +""" +## an API for Meshtastic devices -"""API for Meshtastic devices""" +Primary class: StreamInterface -from .interface import StreamInterface +properties of StreamInterface: + +- radioConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to +the device. +- nodes - The database of received nodes. Includes always up-to-date location and username information for each +node in the mesh. This is a read-only datastructure. +- myNodeInfo - You probably don't want this. + +## Published PubSub topics + +We use a publish-subscribe model to communicate asynchronous events [https://pypubsub.readthedocs.io/en/v4.0.3/ ]. Available +topics: + +- meshtastic.connection.established - published once we've successfully connected to the radio and downloaded the node DB +- meshtastic.connection.lost - published once we've lost our link to the radio +- meshtastic.receive.position(packet) - delivers a received packet as a dictionary, if you only care about a particular +type of packet, you should subscribe to the full topic name. If you want to see all packets, simply subscribe to "meshtastic.receive". +- meshtastic.receive.user(packet) +- meshtastic.receive.data(packet) +- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...) + +Example Usage: + +``` +import meshtastic +from pubsub import pub + +def onReceive(packet): + print(f"Received: {packet}") + +interface = StreamInterface() # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0 +pub.subscribe(onReceive, "meshtastic.receive") +interface.sendData("hello world") # defaults to broadcast, specify a destination ID if you wish +``` + +""" + +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 pubsub import pub + +START1 = 0x94 +START2 = 0xc3 +HEADER_LEN = 4 +MAX_TO_FROM_RADIO_SIZE = 512 + +BROADCAST_ADDR = "all" # A special ID that means broadcast + + +MY_CONFIG_ID = 42 + + +class MeshInterface: + """Interface class for meshtastic devices + """ + + def __init__(self, debugOut=None): + """Constructor""" + self.debugOut = debugOut + self.nodes = None # FIXME + self._startConfig() + + def sendText(self, text, destinationId=BROADCAST_ADDR): + """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} -- where to send this message (default: {BROADCAST_ADDR}) + """ + self.sendData(text.encode("utf-8"), destinationId, + dataType=mesh_pb2.Data.CLEAR_TEXT) + + def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE): + """Send a data packet to some other node""" + meshPacket = mesh_pb2.MeshPacket() + meshPacket.payload.data.payload = byteData + meshPacket.payload.data.typ = dataType + self.sendPacket(meshPacket, destinationId) + + def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR): + """Send a MeshPacket to the specified node (or if unspecified, broadcast). + You probably don't want this - use sendData instead.""" + toRadio = mesh_pb2.ToRadio() + # FIXME add support for non broadcast addresses + meshPacket.to = 255 + toRadio.packet.CopyFrom(meshPacket) + self._sendToRadio(toRadio) + + def _disconnected(self): + """Called by subclasses to tell clients this interface has disconnected""" + pub.sendMessage("meshtastic.connection.lost") + + def _startConfig(self): + """Start device packets flowing""" + self.myInfo = None + self.nodes = {} # nodes keyed by ID + self._nodesByNum = {} # nodes keyed by nodenum + self.radioConfig = None + + startConfig = mesh_pb2.ToRadio() + startConfig.want_config_id = MY_CONFIG_ID # we don't use this value + self._sendToRadio(startConfig) + + def _sendToRadio(self, toRadio): + """Send a ToRadio protobuf to the device""" + logging.error(f"Subclass must provide toradio: {toRadio}") + + def _handleFromRadio(self, fromRadioBytes): + """ + Handle a packet that arrived from the radio(update model and publish events) + + Called by subclasses.""" + fromRadio = mesh_pb2.FromRadio() + fromRadio.ParseFromString(fromRadioBytes) + json = google.protobuf.json_format.MessageToJson(fromRadio) + logging.debug(f"Received: {json}") + if fromRadio.HasField("my_info"): + self.myInfo = fromRadio.my_info + elif fromRadio.HasField("radio"): + self.radioConfig = fromRadio.radio + elif fromRadio.HasField("node_info"): + node = fromRadio.node_info + self._nodesByNum[node.num] = node + 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") + elif fromRadio.HasField("packet"): + self._handlePacketFromRadio(fromRadio.packet) + else: + logging.warn("Unexpected FromRadio payload") + + def _handlePacketFromRadio(self, meshPacket): + """Handle a MeshPacket that just arrived from the radio + + Will publish one of the following events: + - meshtastic.receive.position(packet = MeshPacket dictionary) + - meshtastic.receive.user(packet = MeshPacket dictionary) + - meshtastic.receive.data(packet = MeshPacket dictionary) + """ + # FIXME, update node DB as needed + json = google.protobuf.json_format.MessageToDict(meshPacket) + if meshPacket.payload.HasField("position"): + pub.sendMessage("meshtastic.receive.position", packet=json) + if meshPacket.payload.HasField("user"): + pub.sendMessage("meshtastic.receive.user", + packet=json) + if meshPacket.payload.HasField("data"): + pub.sendMessage("meshtastic.receive.data", + packet=json) + + +class StreamInterface(MeshInterface): + """Interface class for meshtastic devices over a stream link (serial, TCP, etc)""" + + def __init__(self, devPath=None, debugOut=None): + """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}) + + Raises: + Exception: [description] + Exception: [description] + """ + + if devPath is None: + ports = list(filter(lambda port: port.vid != None, + serial.tools.list_ports.comports())) + 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 + + logging.debug(f"Connecting to {devPath}") + self._rxBuf = bytes() # empty + self._wantExit = False + self.stream = serial.Serial( + devPath, 921600, exclusive=True, timeout=0.5) + self._rxThread = threading.Thread(target=self.__reader, args=()) + self._rxThread.start() + MeshInterface.__init__(self, debugOut=debugOut) + + def _sendToRadio(self, toRadio): + """Send a ToRadio protobuf to the device""" + logging.debug(f"Sending: {toRadio}") + b = toRadio.SerializeToString() + bufLen = len(b) + header = bytes([START1, START2, (bufLen >> 8) & 0xff, bufLen & 0xff]) + self.stream.write(header) + self.stream.write(b) + self.stream.flush() + + def close(self): + """Close a connection to the device""" + logging.debug("Closing serial stream") + # pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us + self._wantExit = True + + def __reader(self): + """The reader thread that reads bytes from our stream""" + empty = bytes() + + while not self._wantExit: + b = self.stream.read(1) + if len(b) > 0: + # logging.debug(f"read returned {b}") + c = b[0] + ptr = len(self._rxBuf) + + # Assume we want to append this byte, fixme use bytearray instead + self._rxBuf = self._rxBuf + b + + if ptr == 0: # looking for START1 + if c != START1: + self._rxBuf = empty # failed to find start + if self.debugOut != None: + try: + self.debugOut.write(b.decode("utf-8")) + except: + self.debugOut.write('?') + + elif ptr == 1: # looking for START2 + if c != START2: + self.rfBuf = empty # failed to find start2 + elif ptr >= HEADER_LEN: # we've at least got a header + # big endian length follos header + packetlen = (self._rxBuf[2] << 8) + self._rxBuf[3] + + if ptr == HEADER_LEN: # we _just_ finished reading the header, validate length + if packetlen > MAX_TO_FROM_RADIO_SIZE: + self.rfBuf = empty # length ws out out bounds, restart + + if len(self._rxBuf) != 0 and ptr + 1 == packetlen + HEADER_LEN: + try: + self._handleFromRadio(self._rxBuf[HEADER_LEN:]) + except Exception as ex: + logging.warn( + f"Error handling FromRadio, possibly corrupted? {ex}") + traceback.print_exc() + self._rxBuf = empty + logging.debug("reader is exiting") + self.stream.close() + self._disconnected() diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index c755512..5f4076d 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1,7 +1,7 @@ #!python3 import argparse -from .interface import StreamInterface +from . import StreamInterface import logging import sys from pubsub import pub diff --git a/meshtastic/interface.py b/meshtastic/interface.py deleted file mode 100644 index 37623f9..0000000 --- a/meshtastic/interface.py +++ /dev/null @@ -1,226 +0,0 @@ - -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 pubsub import pub - -START1 = 0x94 -START2 = 0xc3 -HEADER_LEN = 4 -MAX_TO_FROM_RADIO_SIZE = 512 - -BROADCAST_ADDR = "all" # A special ID that means broadcast - -""" - -properties: - -- radioConfig - getter/setter syntax: https://www.python-course.eu/python3_properties.php -- nodes - the database of received nodes -- myNodeInfo -- myNodeId - -# PubSub topics - -Use a pubsub model to communicate events [https://pypubsub.readthedocs.io/en/v4.0.3/ ] - -- meshtastic.connection.established - published once we've successfully connected to the radio and downloaded the node DB -- meshtastic.connection.lost - published once we've lost our link to the radio -- meshtastic.receive.position(packet = MeshPacket) -- meshtastic.receive.user(packet = MeshPacket) -- meshtastic.receive.data(packet = MeshPacket) -- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...) -- meshtastic.debug(message = string) -- meshtastic.send(packet = MeshPacket) - Not yet implemented, instead call sendPacket(...) on MeshInterface - -""" - -MY_CONFIG_ID = 42 - - -class MeshInterface: - """Interface class for meshtastic devices""" - - def __init__(self, debugOut=sys.stdout): - """Constructor""" - self.debugOut = debugOut - self.nodes = None # FIXME - self._startConfig() - - def sendText(self, text, destinationId=BROADCAST_ADDR): - """Send a utf8 string to some other node, if the node has a display it will also be shown on the device.""" - self.sendData(text.encode("utf-8"), destinationId, - dataType=mesh_pb2.Data.CLEAR_TEXT) - - def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE): - """Send a data packet to some other node""" - meshPacket = mesh_pb2.MeshPacket() - meshPacket.payload.data.payload = byteData - meshPacket.payload.data.typ = dataType - self.sendPacket(meshPacket, destinationId) - - def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR): - """Send a MeshPacket to the specified node (or if unspecified, broadcast). - You probably don't want this - use sendData instead.""" - toRadio = mesh_pb2.ToRadio() - # FIXME add support for non broadcast addresses - meshPacket.to = 255 - toRadio.packet.CopyFrom(meshPacket) - self._sendToRadio(toRadio) - - def _disconnected(self): - """Called by subclasses to tell clients this interface has disconnected""" - pub.sendMessage("meshtastic.connection.lost") - - def _startConfig(self): - """Start device packets flowing""" - self.myInfo = None - self.nodes = {} # nodes keyed by ID - self._nodesByNum = {} # nodes keyed by nodenum - self.radioConfig = None - - startConfig = mesh_pb2.ToRadio() - startConfig.want_config_id = MY_CONFIG_ID # we don't use this value - self._sendToRadio(startConfig) - - def _sendToRadio(self, toRadio): - """Send a ToRadio protobuf to the device""" - logging.error(f"Subclass must provide toradio: {toRadio}") - - def _handleFromRadio(self, fromRadioBytes): - """ - Handle a packet that arrived from the radio(update model and publish events) - - Called by subclasses.""" - fromRadio = mesh_pb2.FromRadio() - fromRadio.ParseFromString(fromRadioBytes) - json = google.protobuf.json_format.MessageToJson(fromRadio) - logging.debug(f"Received: {json}") - if fromRadio.HasField("my_info"): - self.myInfo = fromRadio.my_info - elif fromRadio.HasField("radio"): - self.radioConfig = fromRadio.radio - elif fromRadio.HasField("node_info"): - node = fromRadio.node_info - self._nodesByNum[node.num] = node - 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") - elif fromRadio.HasField("packet"): - self._handlePacketFromRadio(fromRadio.packet) - else: - logging.warn("Unexpected FromRadio payload") - - def _handlePacketFromRadio(self, meshPacket): - """Handle a MeshPacket that just arrived from the radio - - Will publish one of the following events: - - meshtastic.receive.position(packet = MeshPacket dictionary) - - meshtastic.receive.user(packet = MeshPacket dictionary) - - meshtastic.receive.data(packet = MeshPacket dictionary) - """ - # FIXME, update node DB as needed - json = google.protobuf.json_format.MessageToDict(meshPacket) - if meshPacket.payload.HasField("position"): - pub.sendMessage("meshtastic.receive.position", packet=json) - if meshPacket.payload.HasField("user"): - pub.sendMessage("meshtastic.receive.user", - packet=json) - if meshPacket.payload.HasField("data"): - pub.sendMessage("meshtastic.receive.data", - packet=json) - - -class StreamInterface(MeshInterface): - """Interface class for meshtastic devices over a stream link(serial, TCP, etc)""" - - def __init__(self, devPath=None, debugOut=sys.stdout): - """Constructor, opens a connection to a specified serial port, or if unspecified try to find one Meshtastic device by probing""" - - if devPath is None: - ports = list(filter(lambda port: port.vid != None, - serial.tools.list_ports.comports())) - 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 - - logging.debug(f"Connecting to {devPath}") - self._rxBuf = bytes() # empty - self._wantExit = False - self.stream = serial.Serial( - devPath, 921600, exclusive=True, timeout=0.5) - self._rxThread = threading.Thread(target=self.__reader, args=()) - self._rxThread.start() - MeshInterface.__init__(self, debugOut=debugOut) - - def _sendToRadio(self, toRadio): - """Send a ToRadio protobuf to the device""" - logging.debug(f"Sending: {toRadio}") - b = toRadio.SerializeToString() - bufLen = len(b) - header = bytes([START1, START2, (bufLen >> 8) & 0xff, bufLen & 0xff]) - self.stream.write(header) - self.stream.write(b) - self.stream.flush() - - def close(self): - """Close a connection to the device""" - logging.debug("Closing serial stream") - # pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us - self._wantExit = True - - def __reader(self): - """The reader thread that reads bytes from our stream""" - empty = bytes() - - while not self._wantExit: - b = self.stream.read(1) - if len(b) > 0: - # logging.debug(f"read returned {b}") - c = b[0] - ptr = len(self._rxBuf) - - # Assume we want to append this byte, fixme use bytearray instead - self._rxBuf = self._rxBuf + b - - if ptr == 0: # looking for START1 - if c != START1: - self._rxBuf = empty # failed to find start - if self.debugOut != None: - try: - self.debugOut.write(b.decode("utf-8")) - except: - self.debugOut.write('?') - - elif ptr == 1: # looking for START2 - if c != START2: - self.rfBuf = empty # failed to find start2 - elif ptr >= HEADER_LEN: # we've at least got a header - # big endian length follos header - packetlen = (self._rxBuf[2] << 8) + self._rxBuf[3] - - if ptr == HEADER_LEN: # we _just_ finished reading the header, validate length - if packetlen > MAX_TO_FROM_RADIO_SIZE: - self.rfBuf = empty # length ws out out bounds, restart - - if len(self._rxBuf) != 0 and ptr + 1 == packetlen + HEADER_LEN: - try: - self._handleFromRadio(self._rxBuf[HEADER_LEN:]) - except Exception as ex: - logging.warn( - f"Error handling FromRadio, possibly corrupted? {ex}") - traceback.print_exc() - self._rxBuf = empty - logging.debug("reader is exiting") - self.stream.close() - self._disconnected() diff --git a/setup.py b/setup.py index 7e01f5e..543ac30 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup( python_requires='>=3', entry_points={ "console_scripts": [ - "meshtastic=meshtastic.__main__:main", + "meshtastic=meshtastic.__main__:main" ] }, )