From 7e63a4a83b6ed552b7e4ff377ca3ace3ccfac9fc Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Tue, 6 Apr 2021 10:36:48 +0800 Subject: [PATCH] 1.2.23 --- docs/meshtastic/apponly_pb2.html | 107 +-- docs/meshtastic/index.html | 1140 +----------------------- docs/meshtastic/mqtt_pb2.html | 197 +++++ docs/meshtastic/node.html | 1228 ++++++++++++++++++++++++++ docs/meshtastic/radioconfig_pb2.html | 96 +- docs/meshtastic/remote_hardware.html | 48 +- docs/meshtastic/util.html | 97 +- setup.py | 2 +- 8 files changed, 1623 insertions(+), 1292 deletions(-) create mode 100644 docs/meshtastic/mqtt_pb2.html create mode 100644 docs/meshtastic/node.html diff --git a/docs/meshtastic/apponly_pb2.html b/docs/meshtastic/apponly_pb2.html index ba968d5..ae2fe7f 100644 --- a/docs/meshtastic/apponly_pb2.html +++ b/docs/meshtastic/apponly_pb2.html @@ -40,7 +40,6 @@ from google.protobuf import symbol_database as _symbol_database _sym_db = _symbol_database.Default() -from . import mesh_pb2 as mesh__pb2 from . import channel_pb2 as channel__pb2 @@ -50,59 +49,13 @@ DESCRIPTOR = _descriptor.FileDescriptor( syntax='proto3', serialized_options=b'\n\023com.geeksville.meshB\rAppOnlyProtosH\003', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\rapponly.proto\x1a\nmesh.proto\x1a\rchannel.proto\"V\n\x0fServiceEnvelope\x12\x1b\n\x06packet\x18\x01 \x01(\x0b\x32\x0b.MeshPacket\x12\x12\n\nchannel_id\x18\x02 \x01(\t\x12\x12\n\ngateway_id\x18\x03 \x01(\t\"0\n\nChannelSet\x12\"\n\x08settings\x18\x01 \x03(\x0b\x32\x10.ChannelSettingsB&\n\x13\x63om.geeksville.meshB\rAppOnlyProtosH\x03\x62\x06proto3' + serialized_pb=b'\n\rapponly.proto\x1a\rchannel.proto\"0\n\nChannelSet\x12\"\n\x08settings\x18\x01 \x03(\x0b\x32\x10.ChannelSettingsB&\n\x13\x63om.geeksville.meshB\rAppOnlyProtosH\x03\x62\x06proto3' , - dependencies=[mesh__pb2.DESCRIPTOR,channel__pb2.DESCRIPTOR,]) + dependencies=[channel__pb2.DESCRIPTOR,]) -_SERVICEENVELOPE = _descriptor.Descriptor( - name='ServiceEnvelope', - full_name='ServiceEnvelope', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='packet', full_name='ServiceEnvelope.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, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='channel_id', full_name='ServiceEnvelope.channel_id', 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, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='gateway_id', full_name='ServiceEnvelope.gateway_id', 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, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=44, - serialized_end=130, -) - - _CHANNELSET = _descriptor.Descriptor( name='ChannelSet', full_name='ChannelSet', @@ -130,23 +83,14 @@ _CHANNELSET = _descriptor.Descriptor( extension_ranges=[], oneofs=[ ], - serialized_start=132, - serialized_end=180, + serialized_start=32, + serialized_end=80, ) -_SERVICEENVELOPE.fields_by_name['packet'].message_type = mesh__pb2._MESHPACKET _CHANNELSET.fields_by_name['settings'].message_type = channel__pb2._CHANNELSETTINGS -DESCRIPTOR.message_types_by_name['ServiceEnvelope'] = _SERVICEENVELOPE DESCRIPTOR.message_types_by_name['ChannelSet'] = _CHANNELSET _sym_db.RegisterFileDescriptor(DESCRIPTOR) -ServiceEnvelope = _reflection.GeneratedProtocolMessageType('ServiceEnvelope', (_message.Message,), { - 'DESCRIPTOR' : _SERVICEENVELOPE, - '__module__' : 'apponly_pb2' - # @@protoc_insertion_point(class_scope:ServiceEnvelope) - }) -_sym_db.RegisterMessage(ServiceEnvelope) - ChannelSet = _reflection.GeneratedProtocolMessageType('ChannelSet', (_message.Message,), { 'DESCRIPTOR' : _CHANNELSET, '__module__' : 'apponly_pb2' @@ -194,40 +138,6 @@ DESCRIPTOR._options = None -
-class ServiceEnvelope -(*args, **kwargs) -
-
-

A ProtocolMessage

-

Ancestors

- -

Class variables

-
-
var DESCRIPTOR
-
-
-
-
-

Instance variables

-
-
var channel_id
-
-

Field ServiceEnvelope.channel_id

-
-
var gateway_id
-
-

Field ServiceEnvelope.gateway_id

-
-
var packet
-
-

Field ServiceEnvelope.packet

-
-
-
@@ -251,15 +161,6 @@ DESCRIPTOR._options = None
  • settings
  • -
  • -

    ServiceEnvelope

    - -
  • diff --git a/docs/meshtastic/index.html b/docs/meshtastic/index.html index 72da120..7f233c7 100644 --- a/docs/meshtastic/index.html +++ b/docs/meshtastic/index.html @@ -155,7 +155,8 @@ import base64 import platform import socket from . import mesh_pb2, portnums_pb2, apponly_pb2, admin_pb2, environmental_measurement_pb2, remote_hardware_pb2, channel_pb2, radioconfig_pb2, util -from .util import fixme, catchAndIgnore, stripnl, DeferredExecution +from .util import fixme, catchAndIgnore, stripnl, DeferredExecution, Timeout +from .node import Node from pubsub import pub from dotmap import DotMap from typing import * @@ -202,351 +203,6 @@ class KnownProtocol(NamedTuple): onReceive: Callable = None -class Timeout: - def __init__(self, maxSecs = 20): - self.expireTime = 0 - self.sleepInterval = 0.1 - self.expireTimeout = maxSecs - - def reset(self): - """Restart the waitForSet timer""" - self.expireTime = time.time() + self.expireTimeout - - def waitForSet(self, target, attrs=()): - """Block until the specified attributes are set. Returns True if config has been received.""" - self.reset() - while time.time() < self.expireTime: - if all(map(lambda a: getattr(target, a, None), attrs)): - return True - time.sleep(self.sleepInterval) - return False - - -def pskToString(psk: bytes): - """Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string""" - if len(psk) == 0: - return "unencrypted" - elif len(psk) == 1: - b = psk[0] - if b == 0: - return "unencrypted" - elif b == 1: - return "default" - else: - return f"simple{b - 1}" - else: - return "secret" - - -class Node: - """A model of a (local or remote) node in the mesh - - Includes methods for radioConfig and channels - """ - - def __init__(self, iface, nodeNum): - """Constructor""" - self.iface = iface - self.nodeNum = nodeNum - self.radioConfig = None - self.channels = None - self._timeout = Timeout(maxSecs = 60) - - def showChannels(self): - """Show human readable description of our channels""" - print("Channels:") - for c in self.channels: - if c.role != channel_pb2.Channel.Role.DISABLED: - cStr = stripnl(MessageToJson(c.settings)) - print( - f" {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}") - publicURL = self.getURL(includeAll=False) - adminURL = self.getURL(includeAll=True) - print(f"\nPrimary channel URL: {publicURL}") - if adminURL != publicURL: - print(f"Complete URL (includes all channels): {adminURL}") - - def showInfo(self): - """Show human readable description of our node""" - print( - f"Preferences: {stripnl(MessageToJson(self.radioConfig.preferences))}\n") - self.showChannels() - - def requestConfig(self): - """ - Send regular MeshPackets to ask for settings and channels - """ - self.radioConfig = None - self.channels = None - self.partialChannels = [] # We keep our channels in a temp array until finished - - self._requestSettings() - - def waitForConfig(self): - """Block until radio config is received. Returns True if config has been received.""" - return self._timeout.waitForSet(self, attrs=('radioConfig', 'channels')) - - def writeConfig(self): - """Write the current (edited) radioConfig to the device""" - if self.radioConfig == None: - raise Exception("No RadioConfig has been read") - - p = admin_pb2.AdminMessage() - p.set_radio.CopyFrom(self.radioConfig) - - self._sendAdmin(p) - logging.debug("Wrote config") - - def writeChannel(self, channelIndex, adminIndex=0): - """Write the current (edited) channel to the device""" - - p = admin_pb2.AdminMessage() - p.set_channel.CopyFrom(self.channels[channelIndex]) - - self._sendAdmin(p, adminIndex=adminIndex) - logging.debug(f"Wrote channel {channelIndex}") - - def deleteChannel(self, channelIndex): - """Delete the specifed channelIndex and shift other channels up""" - ch = self.channels[channelIndex] - if ch.role != channel_pb2.Channel.Role.SECONDARY: - raise Exception("Only SECONDARY channels can be deleted") - - # we are careful here because if we move the "admin" channel the channelIndex we need to use - # for sending admin channels will also change - adminIndex = self.iface.localNode._getAdminChannelIndex() - - self.channels.pop(channelIndex) - self._fixupChannels() # expand back to 8 channels - - index = channelIndex - while index < self.iface.myInfo.max_channels: - self.writeChannel(index, adminIndex=adminIndex) - index += 1 - - # if we are updating the local node, we might end up *moving* the admin channel index as we are writing - if (self.iface.localNode == self) and index >= adminIndex: - # We've now passed the old location for admin index (and writen it), so we can start finding it by name again - adminIndex = 0 - - def getChannelByName(self, name): - """Try to find the named channel or return None""" - for c in (self.channels or []): - if c.settings and c.settings.name == name: - return c - return None - - def getDisabledChannel(self): - """Return the first channel that is disabled (i.e. available for some new use)""" - for c in self.channels: - if c.role == channel_pb2.Channel.Role.DISABLED: - return c - return None - - def _getAdminChannelIndex(self): - """Return the channel number of the admin channel, or 0 if no reserved channel""" - c = self.getChannelByName("admin") - if c: - return c.index - else: - return 0 - - def setOwner(self, long_name, short_name=None): - """Set device owner name""" - nChars = 3 - minChars = 2 - if long_name is not None: - long_name = long_name.strip() - if short_name is None: - words = long_name.split() - if len(long_name) <= nChars: - short_name = long_name - elif len(words) >= minChars: - short_name = ''.join(map(lambda word: word[0], words)) - else: - trans = str.maketrans(dict.fromkeys('aeiouAEIOU')) - short_name = long_name[0] + long_name[1:].translate(trans) - if len(short_name) < nChars: - short_name = long_name[:nChars] - - p = admin_pb2.AdminMessage() - - if long_name is not None: - p.set_owner.long_name = long_name - if short_name is not None: - short_name = short_name.strip() - if len(short_name) > nChars: - short_name = short_name[:nChars] - p.set_owner.short_name = short_name - - return self._sendAdmin(p) - - def getURL(self, includeAll: bool = True): - """The sharable URL that describes the current channel - """ - # Only keep the primary/secondary channels, assume primary is first - channelSet = apponly_pb2.ChannelSet() - for c in self.channels: - if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY): - channelSet.settings.append(c.settings) - bytes = channelSet.SerializeToString() - s = base64.urlsafe_b64encode(bytes).decode('ascii') - return f"https://www.meshtastic.org/d/#{s}".replace("=", "") - - def setURL(self, url): - """Set mesh network URL""" - if self.radioConfig == None: - raise Exception("No RadioConfig has been read") - - # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set} - # Split on '/#' to find the base64 encoded channel settings - splitURL = url.split("/#") - b64 = splitURL[-1] - - # We normally strip padding to make for a shorter URL, but the python parser doesn't like - # that. So add back any missing padding - # per https://stackoverflow.com/a/9807138 - missing_padding = len(b64) % 4 - if missing_padding: - b64 += '=' * (4 - missing_padding) - - decodedURL = base64.urlsafe_b64decode(b64) - channelSet = apponly_pb2.ChannelSet() - channelSet.ParseFromString(decodedURL) - - i = 0 - for chs in channelSet.settings: - ch = channel_pb2.Channel() - ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY - ch.index = i - ch.settings.CopyFrom(chs) - self.channels[ch.index] = ch - self.writeChannel(ch.index) - i = i + 1 - - def _requestSettings(self): - """ - Done with initial config messages, now send regular MeshPackets to ask for settings - """ - p = admin_pb2.AdminMessage() - p.get_radio_request = True - - def onResponse(p): - """A closure to handle the response packet""" - self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response - logging.debug("Received radio config, now fetching channels...") - self._timeout.reset() # We made foreward progress - self._requestChannel(0) # now start fetching channels - - # Show progress message for super slow operations - if self != self.iface.localNode: - logging.info( - "Requesting preferences from remote node (this could take a while)") - - return self._sendAdmin(p, - wantResponse=True, - onResponse=onResponse) - - def exitSimulator(self): - """ - Tell a simulator node to exit (this message is ignored for other nodes) - """ - p = admin_pb2.AdminMessage() - p.exit_simulator = True - - return self._sendAdmin(p) - - def reboot(self, secs: int = 10): - """ - Tell the node to reboot - """ - p = admin_pb2.AdminMessage() - p.reboot_seconds = secs - logging.info(f"Telling node to reboot in {secs} seconds") - - return self._sendAdmin(p) - - def _fixupChannels(self): - """Fixup indexes and add disabled channels as needed""" - - # Add extra disabled channels as needed - for index, ch in enumerate(self.channels): - ch.index = index # fixup indexes - - self._fillChannels() - - def _fillChannels(self): - """Mark unused channels as disabled""" - - # Add extra disabled channels as needed - index = len(self.channels) - while index < self.iface.myInfo.max_channels: - ch = channel_pb2.Channel() - ch.role = channel_pb2.Channel.Role.DISABLED - ch.index = index - self.channels.append(ch) - index += 1 - - def _requestChannel(self, channelNum: int): - """ - Done with initial config messages, now send regular MeshPackets to ask for settings - """ - p = admin_pb2.AdminMessage() - p.get_channel_request = channelNum + 1 - - # Show progress message for super slow operations - if self != self.iface.localNode: - logging.info( - f"Requesting channel {channelNum} info from remote node (this could take a while)") - else: - logging.debug(f"Requesting channel {channelNum}") - - def onResponse(p): - """A closure to handle the response packet""" - c = p["decoded"]["admin"]["raw"].get_channel_response - self.partialChannels.append(c) - self._timeout.reset() # We made foreward progress - logging.debug(f"Received channel {stripnl(c)}") - index = c.index - - # for stress testing, we can always download all channels - fastChannelDownload = True - - # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching - quitEarly = ( - c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload - - if quitEarly or index >= self.iface.myInfo.max_channels - 1: - logging.debug("Finished downloading channels") - - self.channels = self.partialChannels - self._fixupChannels() - - # FIXME, the following should only be called after we have settings and channels - self.iface._connected() # Tell everone else we are ready to go - else: - self._requestChannel(index + 1) - - return self._sendAdmin(p, - wantResponse=True, - onResponse=onResponse) - - def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False, - onResponse=None, - adminIndex=0): - """Send an admin message to the specified node (or the local node if destNodeNum is zero)""" - - if adminIndex == 0: # unless a special channel index was used, we want to use the admin index - adminIndex = self.iface.localNode._getAdminChannelIndex() - - return self.iface.sendData(p, self.nodeNum, - portNum=portnums_pb2.PortNum.ADMIN_APP, - wantAck=True, - wantResponse=wantResponse, - onResponse=onResponse, - channelIndex=adminIndex) - - class MeshInterface: """Interface class for meshtastic devices @@ -758,7 +414,7 @@ class MeshInterface: def waitForConfig(self): """Block until radio config is received. Returns True if config has been received.""" success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes') - ) and self.localNode.waitForConfig() + ) and self.localNode.waitForConfig() if not success: raise Exception("Timed out waiting for interface config") @@ -1318,7 +974,7 @@ class TCPInterface(StreamInterface): try: self.socket.shutdown(socket.SHUT_RDWR) except: - pass # Ignore errors in shutdown, because we might have a race with the server + pass # Ignore errors in shutdown, because we might have a race with the server self.socket.close() def _writeBytes(self, b): @@ -1408,6 +1064,14 @@ protocols = {

    Generated protocol buffer code.

    +
    meshtastic.mqtt_pb2
    +
    +

    Generated protocol buffer code.

    +
    +
    meshtastic.node
    +
    +

    an API for Meshtastic devices …

    +
    meshtastic.portnums_pb2

    Generated protocol buffer code.

    @@ -1457,34 +1121,6 @@ protocols = {
    -

    Functions

    -
    -
    -def pskToString(psk: bytes) -
    -
    -

    Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string

    -
    - -Expand source code - -
    def pskToString(psk: bytes):
    -    """Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string"""
    -    if len(psk) == 0:
    -        return "unencrypted"
    -    elif len(psk) == 1:
    -        b = psk[0]
    -        if b == 0:
    -            return "unencrypted"
    -        elif b == 1:
    -            return "default"
    -        else:
    -            return f"simple{b - 1}"
    -    else:
    -        return "secret"
    -
    -
    -

    Classes

    @@ -1826,7 +1462,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d def waitForConfig(self): """Block until radio config is received. Returns True if config has been received.""" success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes') - ) and self.localNode.waitForConfig() + ) and self.localNode.waitForConfig() if not success: raise Exception("Timed out waiting for interface config") @@ -2395,660 +2031,13 @@ wantResponse – True if you want the service on the other side to send an a
    def waitForConfig(self):
         """Block until radio config is received. Returns True if config has been received."""
         success = self._timeout.waitForSet(self, attrs=('myInfo', 'nodes')
    -                         ) and self.localNode.waitForConfig()
    +                                       ) and self.localNode.waitForConfig()
         if not success:
             raise Exception("Timed out waiting for interface config")
    -
    -class Node -(iface, nodeNum) -
    -
    -

    A model of a (local or remote) node in the mesh

    -

    Includes methods for radioConfig and channels

    -

    Constructor

    -
    - -Expand source code - -
    class Node:
    -    """A model of a (local or remote) node in the mesh
    -
    -    Includes methods for radioConfig and channels
    -    """
    -
    -    def __init__(self, iface, nodeNum):
    -        """Constructor"""
    -        self.iface = iface
    -        self.nodeNum = nodeNum
    -        self.radioConfig = None
    -        self.channels = None
    -        self._timeout = Timeout(maxSecs = 60)
    -
    -    def showChannels(self):
    -        """Show human readable description of our channels"""
    -        print("Channels:")
    -        for c in self.channels:
    -            if c.role != channel_pb2.Channel.Role.DISABLED:
    -                cStr = stripnl(MessageToJson(c.settings))
    -                print(
    -                    f"  {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}")
    -        publicURL = self.getURL(includeAll=False)
    -        adminURL = self.getURL(includeAll=True)
    -        print(f"\nPrimary channel URL: {publicURL}")
    -        if adminURL != publicURL:
    -            print(f"Complete URL (includes all channels): {adminURL}")
    -
    -    def showInfo(self):
    -        """Show human readable description of our node"""
    -        print(
    -            f"Preferences: {stripnl(MessageToJson(self.radioConfig.preferences))}\n")
    -        self.showChannels()
    -
    -    def requestConfig(self):
    -        """
    -        Send regular MeshPackets to ask for settings and channels
    -        """
    -        self.radioConfig = None
    -        self.channels = None
    -        self.partialChannels = []  # We keep our channels in a temp array until finished
    -
    -        self._requestSettings()
    -
    -    def waitForConfig(self):
    -        """Block until radio config is received. Returns True if config has been received."""
    -        return self._timeout.waitForSet(self, attrs=('radioConfig', 'channels'))
    -
    -    def writeConfig(self):
    -        """Write the current (edited) radioConfig to the device"""
    -        if self.radioConfig == None:
    -            raise Exception("No RadioConfig has been read")
    -
    -        p = admin_pb2.AdminMessage()
    -        p.set_radio.CopyFrom(self.radioConfig)
    -
    -        self._sendAdmin(p)
    -        logging.debug("Wrote config")
    -
    -    def writeChannel(self, channelIndex, adminIndex=0):
    -        """Write the current (edited) channel to the device"""
    -
    -        p = admin_pb2.AdminMessage()
    -        p.set_channel.CopyFrom(self.channels[channelIndex])
    -
    -        self._sendAdmin(p, adminIndex=adminIndex)
    -        logging.debug(f"Wrote channel {channelIndex}")
    -
    -    def deleteChannel(self, channelIndex):
    -        """Delete the specifed channelIndex and shift other channels up"""
    -        ch = self.channels[channelIndex]
    -        if ch.role != channel_pb2.Channel.Role.SECONDARY:
    -            raise Exception("Only SECONDARY channels can be deleted")
    -
    -        # we are careful here because if we move the "admin" channel the channelIndex we need to use
    -        # for sending admin channels will also change
    -        adminIndex = self.iface.localNode._getAdminChannelIndex()
    -
    -        self.channels.pop(channelIndex)
    -        self._fixupChannels()  # expand back to 8 channels
    -
    -        index = channelIndex
    -        while index < self.iface.myInfo.max_channels:
    -            self.writeChannel(index, adminIndex=adminIndex)
    -            index += 1
    -
    -            # if we are updating the local node, we might end up *moving* the admin channel index as we are writing
    -            if (self.iface.localNode == self) and index >= adminIndex:
    -                # We've now passed the old location for admin index (and writen it), so we can start finding it by name again
    -                adminIndex = 0
    -
    -    def getChannelByName(self, name):
    -        """Try to find the named channel or return None"""
    -        for c in (self.channels or []):
    -            if c.settings and c.settings.name == name:
    -                return c
    -        return None
    -
    -    def getDisabledChannel(self):
    -        """Return the first channel that is disabled (i.e. available for some new use)"""
    -        for c in self.channels:
    -            if c.role == channel_pb2.Channel.Role.DISABLED:
    -                return c
    -        return None
    -
    -    def _getAdminChannelIndex(self):
    -        """Return the channel number of the admin channel, or 0 if no reserved channel"""
    -        c = self.getChannelByName("admin")
    -        if c:
    -            return c.index
    -        else:
    -            return 0
    -
    -    def setOwner(self, long_name, short_name=None):
    -        """Set device owner name"""
    -        nChars = 3
    -        minChars = 2
    -        if long_name is not None:
    -            long_name = long_name.strip()
    -            if short_name is None:
    -                words = long_name.split()
    -                if len(long_name) <= nChars:
    -                    short_name = long_name
    -                elif len(words) >= minChars:
    -                    short_name = ''.join(map(lambda word: word[0], words))
    -                else:
    -                    trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
    -                    short_name = long_name[0] + long_name[1:].translate(trans)
    -                    if len(short_name) < nChars:
    -                        short_name = long_name[:nChars]
    -
    -        p = admin_pb2.AdminMessage()
    -
    -        if long_name is not None:
    -            p.set_owner.long_name = long_name
    -        if short_name is not None:
    -            short_name = short_name.strip()
    -            if len(short_name) > nChars:
    -                short_name = short_name[:nChars]
    -            p.set_owner.short_name = short_name
    -
    -        return self._sendAdmin(p)
    -
    -    def getURL(self, includeAll: bool = True):
    -        """The sharable URL that describes the current channel
    -        """
    -        # Only keep the primary/secondary channels, assume primary is first
    -        channelSet = apponly_pb2.ChannelSet()
    -        for c in self.channels:
    -            if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY):
    -                channelSet.settings.append(c.settings)
    -        bytes = channelSet.SerializeToString()
    -        s = base64.urlsafe_b64encode(bytes).decode('ascii')
    -        return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
    -
    -    def setURL(self, url):
    -        """Set mesh network URL"""
    -        if self.radioConfig == None:
    -            raise Exception("No RadioConfig has been read")
    -
    -        # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
    -        # Split on '/#' to find the base64 encoded channel settings
    -        splitURL = url.split("/#")
    -        b64 = splitURL[-1]
    -
    -        # We normally strip padding to make for a shorter URL, but the python parser doesn't like
    -        # that.  So add back any missing padding
    -        # per https://stackoverflow.com/a/9807138
    -        missing_padding = len(b64) % 4
    -        if missing_padding:
    -            b64 += '=' * (4 - missing_padding)
    -
    -        decodedURL = base64.urlsafe_b64decode(b64)
    -        channelSet = apponly_pb2.ChannelSet()
    -        channelSet.ParseFromString(decodedURL)
    -
    -        i = 0
    -        for chs in channelSet.settings:
    -            ch = channel_pb2.Channel()
    -            ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
    -            ch.index = i
    -            ch.settings.CopyFrom(chs)
    -            self.channels[ch.index] = ch
    -            self.writeChannel(ch.index)
    -            i = i + 1
    -
    -    def _requestSettings(self):
    -        """
    -        Done with initial config messages, now send regular MeshPackets to ask for settings
    -        """
    -        p = admin_pb2.AdminMessage()
    -        p.get_radio_request = True
    -
    -        def onResponse(p):
    -            """A closure to handle the response packet"""
    -            self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
    -            logging.debug("Received radio config, now fetching channels...")
    -            self._timeout.reset() # We made foreward progress
    -            self._requestChannel(0)  # now start fetching channels
    -
    -        # Show progress message for super slow operations
    -        if self != self.iface.localNode:
    -            logging.info(
    -                "Requesting preferences from remote node (this could take a while)")
    -
    -        return self._sendAdmin(p,
    -                               wantResponse=True,
    -                               onResponse=onResponse)
    -
    -    def exitSimulator(self):
    -        """
    -        Tell a simulator node to exit (this message is ignored for other nodes)
    -        """
    -        p = admin_pb2.AdminMessage()
    -        p.exit_simulator = True
    -
    -        return self._sendAdmin(p)
    -
    -    def reboot(self, secs: int = 10):
    -        """
    -        Tell the node to reboot
    -        """
    -        p = admin_pb2.AdminMessage()
    -        p.reboot_seconds = secs
    -        logging.info(f"Telling node to reboot in {secs} seconds")
    -
    -        return self._sendAdmin(p)
    -
    -    def _fixupChannels(self):
    -        """Fixup indexes and add disabled channels as needed"""
    -
    -        # Add extra disabled channels as needed
    -        for index, ch in enumerate(self.channels):
    -            ch.index = index  # fixup indexes
    -
    -        self._fillChannels()
    -
    -    def _fillChannels(self):
    -        """Mark unused channels as disabled"""
    -
    -        # Add extra disabled channels as needed
    -        index = len(self.channels)
    -        while index < self.iface.myInfo.max_channels:
    -            ch = channel_pb2.Channel()
    -            ch.role = channel_pb2.Channel.Role.DISABLED
    -            ch.index = index
    -            self.channels.append(ch)
    -            index += 1
    -
    -    def _requestChannel(self, channelNum: int):
    -        """
    -        Done with initial config messages, now send regular MeshPackets to ask for settings
    -        """
    -        p = admin_pb2.AdminMessage()
    -        p.get_channel_request = channelNum + 1
    -
    -        # Show progress message for super slow operations
    -        if self != self.iface.localNode:
    -            logging.info(
    -                f"Requesting channel {channelNum} info from remote node (this could take a while)")
    -        else:
    -            logging.debug(f"Requesting channel {channelNum}")
    -
    -        def onResponse(p):
    -            """A closure to handle the response packet"""
    -            c = p["decoded"]["admin"]["raw"].get_channel_response
    -            self.partialChannels.append(c)
    -            self._timeout.reset() # We made foreward progress            
    -            logging.debug(f"Received channel {stripnl(c)}")
    -            index = c.index
    -
    -            # for stress testing, we can always download all channels
    -            fastChannelDownload = True
    -
    -            # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
    -            quitEarly = (
    -                c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
    -
    -            if quitEarly or index >= self.iface.myInfo.max_channels - 1:
    -                logging.debug("Finished downloading channels")
    -
    -                self.channels = self.partialChannels
    -                self._fixupChannels()
    -
    -                # FIXME, the following should only be called after we have settings and channels
    -                self.iface._connected()  # Tell everone else we are ready to go
    -            else:
    -                self._requestChannel(index + 1)
    -
    -        return self._sendAdmin(p,
    -                               wantResponse=True,
    -                               onResponse=onResponse)
    -
    -    def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False,
    -                   onResponse=None,
    -                   adminIndex=0):
    -        """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
    -
    -        if adminIndex == 0:  # unless a special channel index was used, we want to use the admin index
    -            adminIndex = self.iface.localNode._getAdminChannelIndex()
    -
    -        return self.iface.sendData(p, self.nodeNum,
    -                                   portNum=portnums_pb2.PortNum.ADMIN_APP,
    -                                   wantAck=True,
    -                                   wantResponse=wantResponse,
    -                                   onResponse=onResponse,
    -                                   channelIndex=adminIndex)
    -
    -

    Methods

    -
    -
    -def deleteChannel(self, channelIndex) -
    -
    -

    Delete the specifed channelIndex and shift other channels up

    -
    - -Expand source code - -
    def deleteChannel(self, channelIndex):
    -    """Delete the specifed channelIndex and shift other channels up"""
    -    ch = self.channels[channelIndex]
    -    if ch.role != channel_pb2.Channel.Role.SECONDARY:
    -        raise Exception("Only SECONDARY channels can be deleted")
    -
    -    # we are careful here because if we move the "admin" channel the channelIndex we need to use
    -    # for sending admin channels will also change
    -    adminIndex = self.iface.localNode._getAdminChannelIndex()
    -
    -    self.channels.pop(channelIndex)
    -    self._fixupChannels()  # expand back to 8 channels
    -
    -    index = channelIndex
    -    while index < self.iface.myInfo.max_channels:
    -        self.writeChannel(index, adminIndex=adminIndex)
    -        index += 1
    -
    -        # if we are updating the local node, we might end up *moving* the admin channel index as we are writing
    -        if (self.iface.localNode == self) and index >= adminIndex:
    -            # We've now passed the old location for admin index (and writen it), so we can start finding it by name again
    -            adminIndex = 0
    -
    -
    -
    -def exitSimulator(self) -
    -
    -

    Tell a simulator node to exit (this message is ignored for other nodes)

    -
    - -Expand source code - -
    def exitSimulator(self):
    -    """
    -    Tell a simulator node to exit (this message is ignored for other nodes)
    -    """
    -    p = admin_pb2.AdminMessage()
    -    p.exit_simulator = True
    -
    -    return self._sendAdmin(p)
    -
    -
    -
    -def getChannelByName(self, name) -
    -
    -

    Try to find the named channel or return None

    -
    - -Expand source code - -
    def getChannelByName(self, name):
    -    """Try to find the named channel or return None"""
    -    for c in (self.channels or []):
    -        if c.settings and c.settings.name == name:
    -            return c
    -    return None
    -
    -
    -
    -def getDisabledChannel(self) -
    -
    -

    Return the first channel that is disabled (i.e. available for some new use)

    -
    - -Expand source code - -
    def getDisabledChannel(self):
    -    """Return the first channel that is disabled (i.e. available for some new use)"""
    -    for c in self.channels:
    -        if c.role == channel_pb2.Channel.Role.DISABLED:
    -            return c
    -    return None
    -
    -
    -
    -def getURL(self, includeAll: bool = True) -
    -
    -

    The sharable URL that describes the current channel

    -
    - -Expand source code - -
    def getURL(self, includeAll: bool = True):
    -    """The sharable URL that describes the current channel
    -    """
    -    # Only keep the primary/secondary channels, assume primary is first
    -    channelSet = apponly_pb2.ChannelSet()
    -    for c in self.channels:
    -        if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY):
    -            channelSet.settings.append(c.settings)
    -    bytes = channelSet.SerializeToString()
    -    s = base64.urlsafe_b64encode(bytes).decode('ascii')
    -    return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
    -
    -
    -
    -def reboot(self, secs: int = 10) -
    -
    -

    Tell the node to reboot

    -
    - -Expand source code - -
    def reboot(self, secs: int = 10):
    -    """
    -    Tell the node to reboot
    -    """
    -    p = admin_pb2.AdminMessage()
    -    p.reboot_seconds = secs
    -    logging.info(f"Telling node to reboot in {secs} seconds")
    -
    -    return self._sendAdmin(p)
    -
    -
    -
    -def requestConfig(self) -
    -
    -

    Send regular MeshPackets to ask for settings and channels

    -
    - -Expand source code - -
    def requestConfig(self):
    -    """
    -    Send regular MeshPackets to ask for settings and channels
    -    """
    -    self.radioConfig = None
    -    self.channels = None
    -    self.partialChannels = []  # We keep our channels in a temp array until finished
    -
    -    self._requestSettings()
    -
    -
    -
    -def setOwner(self, long_name, short_name=None) -
    -
    -

    Set device owner name

    -
    - -Expand source code - -
    def setOwner(self, long_name, short_name=None):
    -    """Set device owner name"""
    -    nChars = 3
    -    minChars = 2
    -    if long_name is not None:
    -        long_name = long_name.strip()
    -        if short_name is None:
    -            words = long_name.split()
    -            if len(long_name) <= nChars:
    -                short_name = long_name
    -            elif len(words) >= minChars:
    -                short_name = ''.join(map(lambda word: word[0], words))
    -            else:
    -                trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
    -                short_name = long_name[0] + long_name[1:].translate(trans)
    -                if len(short_name) < nChars:
    -                    short_name = long_name[:nChars]
    -
    -    p = admin_pb2.AdminMessage()
    -
    -    if long_name is not None:
    -        p.set_owner.long_name = long_name
    -    if short_name is not None:
    -        short_name = short_name.strip()
    -        if len(short_name) > nChars:
    -            short_name = short_name[:nChars]
    -        p.set_owner.short_name = short_name
    -
    -    return self._sendAdmin(p)
    -
    -
    -
    -def setURL(self, url) -
    -
    -

    Set mesh network URL

    -
    - -Expand source code - -
    def setURL(self, url):
    -    """Set mesh network URL"""
    -    if self.radioConfig == None:
    -        raise Exception("No RadioConfig has been read")
    -
    -    # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
    -    # Split on '/#' to find the base64 encoded channel settings
    -    splitURL = url.split("/#")
    -    b64 = splitURL[-1]
    -
    -    # We normally strip padding to make for a shorter URL, but the python parser doesn't like
    -    # that.  So add back any missing padding
    -    # per https://stackoverflow.com/a/9807138
    -    missing_padding = len(b64) % 4
    -    if missing_padding:
    -        b64 += '=' * (4 - missing_padding)
    -
    -    decodedURL = base64.urlsafe_b64decode(b64)
    -    channelSet = apponly_pb2.ChannelSet()
    -    channelSet.ParseFromString(decodedURL)
    -
    -    i = 0
    -    for chs in channelSet.settings:
    -        ch = channel_pb2.Channel()
    -        ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
    -        ch.index = i
    -        ch.settings.CopyFrom(chs)
    -        self.channels[ch.index] = ch
    -        self.writeChannel(ch.index)
    -        i = i + 1
    -
    -
    -
    -def showChannels(self) -
    -
    -

    Show human readable description of our channels

    -
    - -Expand source code - -
    def showChannels(self):
    -    """Show human readable description of our channels"""
    -    print("Channels:")
    -    for c in self.channels:
    -        if c.role != channel_pb2.Channel.Role.DISABLED:
    -            cStr = stripnl(MessageToJson(c.settings))
    -            print(
    -                f"  {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}")
    -    publicURL = self.getURL(includeAll=False)
    -    adminURL = self.getURL(includeAll=True)
    -    print(f"\nPrimary channel URL: {publicURL}")
    -    if adminURL != publicURL:
    -        print(f"Complete URL (includes all channels): {adminURL}")
    -
    -
    -
    -def showInfo(self) -
    -
    -

    Show human readable description of our node

    -
    - -Expand source code - -
    def showInfo(self):
    -    """Show human readable description of our node"""
    -    print(
    -        f"Preferences: {stripnl(MessageToJson(self.radioConfig.preferences))}\n")
    -    self.showChannels()
    -
    -
    -
    -def waitForConfig(self) -
    -
    -

    Block until radio config is received. Returns True if config has been received.

    -
    - -Expand source code - -
    def waitForConfig(self):
    -    """Block until radio config is received. Returns True if config has been received."""
    -    return self._timeout.waitForSet(self, attrs=('radioConfig', 'channels'))
    -
    -
    -
    -def writeChannel(self, channelIndex, adminIndex=0) -
    -
    -

    Write the current (edited) channel to the device

    -
    - -Expand source code - -
    def writeChannel(self, channelIndex, adminIndex=0):
    -    """Write the current (edited) channel to the device"""
    -
    -    p = admin_pb2.AdminMessage()
    -    p.set_channel.CopyFrom(self.channels[channelIndex])
    -
    -    self._sendAdmin(p, adminIndex=adminIndex)
    -    logging.debug(f"Wrote channel {channelIndex}")
    -
    -
    -
    -def writeConfig(self) -
    -
    -

    Write the current (edited) radioConfig to the device

    -
    - -Expand source code - -
    def writeConfig(self):
    -    """Write the current (edited) radioConfig to the device"""
    -    if self.radioConfig == None:
    -        raise Exception("No RadioConfig has been read")
    -
    -    p = admin_pb2.AdminMessage()
    -    p.set_radio.CopyFrom(self.radioConfig)
    -
    -    self._sendAdmin(p)
    -    logging.debug("Wrote config")
    -
    -
    -
    -
    class ResponseHandler (callback: Callable) @@ -3441,7 +2430,7 @@ hostname {string} – Hostname/IP address of the device to connect to

    -
    -class Timeout -(maxSecs=20) -
    -
    -
    -
    - -Expand source code - -
    class Timeout:
    -    def __init__(self, maxSecs = 20):
    -        self.expireTime = 0
    -        self.sleepInterval = 0.1
    -        self.expireTimeout = maxSecs
    -
    -    def reset(self):
    -        """Restart the waitForSet timer"""
    -        self.expireTime = time.time() + self.expireTimeout
    -
    -    def waitForSet(self, target, attrs=()):
    -        """Block until the specified attributes are set. Returns True if config has been received."""
    -        self.reset()
    -        while time.time() < self.expireTime:
    -            if all(map(lambda a: getattr(target, a, None), attrs)):
    -                return True
    -            time.sleep(self.sleepInterval)
    -        return False
    -
    -

    Methods

    -
    -
    -def reset(self) -
    -
    -

    Restart the waitForSet timer

    -
    - -Expand source code - -
    def reset(self):
    -    """Restart the waitForSet timer"""
    -    self.expireTime = time.time() + self.expireTimeout
    -
    -
    -
    -def waitForSet(self, target, attrs=()) -
    -
    -

    Block until the specified attributes are set. Returns True if config has been received.

    -
    - -Expand source code - -
    def waitForSet(self, target, attrs=()):
    -    """Block until the specified attributes are set. Returns True if config has been received."""
    -    self.reset()
    -    while time.time() < self.expireTime:
    -        if all(map(lambda a: getattr(target, a, None), attrs)):
    -            return True
    -        time.sleep(self.sleepInterval)
    -    return False
    -
    -
    -
    -
    @@ -3561,6 +2484,8 @@ hostname {string} – Hostname/IP address of the device to connect to

    meshtastic.deviceonly_pb2
  • meshtastic.environmental_measurement_pb2
  • meshtastic.mesh_pb2
  • +
  • meshtastic.mqtt_pb2
  • +
  • meshtastic.node
  • meshtastic.portnums_pb2
  • meshtastic.radioconfig_pb2
  • meshtastic.remote_hardware
  • @@ -3577,11 +2502,6 @@ hostname {string} – Hostname/IP address of the device to connect to

    defaultHopLimit -
  • Functions

    - -
  • Classes

    diff --git a/docs/meshtastic/mqtt_pb2.html b/docs/meshtastic/mqtt_pb2.html new file mode 100644 index 0000000..a6afba8 --- /dev/null +++ b/docs/meshtastic/mqtt_pb2.html @@ -0,0 +1,197 @@ + + + + + + +meshtastic.mqtt_pb2 API documentation + + + + + + + + + + + +
    +
    +
    +

    Module meshtastic.mqtt_pb2

    +
    +
    +

    Generated protocol buffer code.

    +
    + +Expand source code + +
    # -*- coding: utf-8 -*-
    +# Generated by the protocol buffer compiler.  DO NOT EDIT!
    +# source: mqtt.proto
    +"""Generated protocol buffer code."""
    +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()
    +
    +
    +from . import mesh_pb2 as mesh__pb2
    +
    +
    +DESCRIPTOR = _descriptor.FileDescriptor(
    +  name='mqtt.proto',
    +  package='',
    +  syntax='proto3',
    +  serialized_options=b'\n\023com.geeksville.meshB\nMQTTProtosH\003',
    +  create_key=_descriptor._internal_create_key,
    +  serialized_pb=b'\n\nmqtt.proto\x1a\nmesh.proto\"V\n\x0fServiceEnvelope\x12\x1b\n\x06packet\x18\x01 \x01(\x0b\x32\x0b.MeshPacket\x12\x12\n\nchannel_id\x18\x02 \x01(\t\x12\x12\n\ngateway_id\x18\x03 \x01(\tB#\n\x13\x63om.geeksville.meshB\nMQTTProtosH\x03\x62\x06proto3'
    +  ,
    +  dependencies=[mesh__pb2.DESCRIPTOR,])
    +
    +
    +
    +
    +_SERVICEENVELOPE = _descriptor.Descriptor(
    +  name='ServiceEnvelope',
    +  full_name='ServiceEnvelope',
    +  filename=None,
    +  file=DESCRIPTOR,
    +  containing_type=None,
    +  create_key=_descriptor._internal_create_key,
    +  fields=[
    +    _descriptor.FieldDescriptor(
    +      name='packet', full_name='ServiceEnvelope.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,  create_key=_descriptor._internal_create_key),
    +    _descriptor.FieldDescriptor(
    +      name='channel_id', full_name='ServiceEnvelope.channel_id', 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,  create_key=_descriptor._internal_create_key),
    +    _descriptor.FieldDescriptor(
    +      name='gateway_id', full_name='ServiceEnvelope.gateway_id', 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,  create_key=_descriptor._internal_create_key),
    +  ],
    +  extensions=[
    +  ],
    +  nested_types=[],
    +  enum_types=[
    +  ],
    +  serialized_options=None,
    +  is_extendable=False,
    +  syntax='proto3',
    +  extension_ranges=[],
    +  oneofs=[
    +  ],
    +  serialized_start=26,
    +  serialized_end=112,
    +)
    +
    +_SERVICEENVELOPE.fields_by_name['packet'].message_type = mesh__pb2._MESHPACKET
    +DESCRIPTOR.message_types_by_name['ServiceEnvelope'] = _SERVICEENVELOPE
    +_sym_db.RegisterFileDescriptor(DESCRIPTOR)
    +
    +ServiceEnvelope = _reflection.GeneratedProtocolMessageType('ServiceEnvelope', (_message.Message,), {
    +  'DESCRIPTOR' : _SERVICEENVELOPE,
    +  '__module__' : 'mqtt_pb2'
    +  # @@protoc_insertion_point(class_scope:ServiceEnvelope)
    +  })
    +_sym_db.RegisterMessage(ServiceEnvelope)
    +
    +
    +DESCRIPTOR._options = None
    +# @@protoc_insertion_point(module_scope)
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class ServiceEnvelope +(*args, **kwargs) +
    +
    +

    A ProtocolMessage

    +

    Ancestors

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

    Class variables

    +
    +
    var DESCRIPTOR
    +
    +
    +
    +
    +

    Instance variables

    +
    +
    var channel_id
    +
    +

    Field ServiceEnvelope.channel_id

    +
    +
    var gateway_id
    +
    +

    Field ServiceEnvelope.gateway_id

    +
    +
    var packet
    +
    +

    Field ServiceEnvelope.packet

    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/meshtastic/node.html b/docs/meshtastic/node.html new file mode 100644 index 0000000..911e5d1 --- /dev/null +++ b/docs/meshtastic/node.html @@ -0,0 +1,1228 @@ + + + + + + +meshtastic.node API documentation + + + + + + + + + + + +
    +
    +
    +

    Module meshtastic.node

    +
    +
    +

    an API for Meshtastic devices

    +

    Primary class: SerialInterface +Install with pip: "pip3 install meshtastic" +Source code on github

    +

    properties of SerialInterface:

    +
      +
    • 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.
    • +
    • nodesByNum - like "nodes" but keyed by nodeNum instead of nodeId
    • +
    • myInfo - Contains read-only information about the local radio device (software version, hardware version, etc)
    • +
    +

    Published PubSub topics

    +

    We use a publish-subscribe model to communicate asynchronous events. +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.text(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.position(packet)
    • +
    • meshtastic.receive.user(packet)
    • +
    • meshtastic.receive.data.portnum(packet) (where portnum is an integer or well known PortNum enum)
    • +
    • meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc…)
    • +
    +

    We receive position, user, or data packets from the mesh. +You probably only care about meshtastic.receive.data. +The first argument for +that publish will be the packet. +Text or binary data packets (from sendData or sendText) will both arrive this way. +If you print packet +you'll see the fields in the dictionary. +decoded.data.payload will contain the raw bytes that were sent. +If the packet was sent with +sendText, decoded.data.text will also be populated with the decoded string. +For ASCII these two strings will be the same, but for +unicode scripts they can be different.

    +

    Example Usage

    +
    import meshtastic
    +from pubsub import pub
    +
    +def onReceive(packet, interface): # called when a packet arrives
    +    print(f"Received: {packet}")
    +
    +def onConnection(interface, topic=pub.AUTO_TOPIC): # called when we (re)connect to the radio
    +    # defaults to broadcast, specify a destination ID if you wish
    +    interface.sendText("hello mesh")
    +
    +pub.subscribe(onReceive, "meshtastic.receive")
    +pub.subscribe(onConnection, "meshtastic.connection.established")
    +# By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
    +interface = meshtastic.SerialInterface()
    +
    +
    +
    + +Expand source code + +
    """
    +# an API for Meshtastic devices
    +
    +Primary class: SerialInterface
    +Install with pip: "[pip3 install meshtastic](https://pypi.org/project/meshtastic/)"
    +Source code on [github](https://github.com/meshtastic/Meshtastic-python)
    +
    +properties of SerialInterface:
    +
    +- 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.
    +- nodesByNum - like "nodes" but keyed by nodeNum instead of nodeId
    +- myInfo - Contains read-only information about the local radio device (software version, hardware version, etc)
    +
    +# Published PubSub topics
    +
    +We use a [publish-subscribe](https://pypubsub.readthedocs.io/en/v4.0.3/) model to communicate asynchronous events.  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.text(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.position(packet)
    +- meshtastic.receive.user(packet)
    +- meshtastic.receive.data.portnum(packet) (where portnum is an integer or well known PortNum enum)
    +- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...)
    +
    +We receive position, user, or data packets from the mesh.  You probably only care about meshtastic.receive.data.  The first argument for 
    +that publish will be the packet.  Text or binary data packets (from sendData or sendText) will both arrive this way.  If you print packet 
    +you'll see the fields in the dictionary.  decoded.data.payload will contain the raw bytes that were sent.  If the packet was sent with 
    +sendText, decoded.data.text will **also** be populated with the decoded string.  For ASCII these two strings will be the same, but for 
    +unicode scripts they can be different.
    +
    +# Example Usage
    +```
    +import meshtastic
    +from pubsub import pub
    +
    +def onReceive(packet, interface): # called when a packet arrives
    +    print(f"Received: {packet}")
    +
    +def onConnection(interface, topic=pub.AUTO_TOPIC): # called when we (re)connect to the radio
    +    # defaults to broadcast, specify a destination ID if you wish
    +    interface.sendText("hello mesh")
    +
    +pub.subscribe(onReceive, "meshtastic.receive")
    +pub.subscribe(onConnection, "meshtastic.connection.established")
    +# By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
    +interface = meshtastic.SerialInterface()
    +
    +```
    +
    +"""
    +
    +import pygatt
    +import google.protobuf.json_format
    +import serial
    +import threading
    +import logging
    +import sys
    +import random
    +import traceback
    +import time
    +import base64
    +import platform
    +import socket
    +from . import mesh_pb2, portnums_pb2, apponly_pb2, admin_pb2, environmental_measurement_pb2, remote_hardware_pb2, channel_pb2, radioconfig_pb2, util
    +from .util import fixme, catchAndIgnore, stripnl, DeferredExecution, Timeout
    +from pubsub import pub
    +from dotmap import DotMap
    +from typing import *
    +from google.protobuf.json_format import MessageToJson
    +
    +
    +
    +def pskToString(psk: bytes):
    +    """Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string"""
    +    if len(psk) == 0:
    +        return "unencrypted"
    +    elif len(psk) == 1:
    +        b = psk[0]
    +        if b == 0:
    +            return "unencrypted"
    +        elif b == 1:
    +            return "default"
    +        else:
    +            return f"simple{b - 1}"
    +    else:
    +        return "secret"
    +
    +
    +class Node:
    +    """A model of a (local or remote) node in the mesh
    +
    +    Includes methods for radioConfig and channels
    +    """
    +
    +    def __init__(self, iface, nodeNum):
    +        """Constructor"""
    +        self.iface = iface
    +        self.nodeNum = nodeNum
    +        self.radioConfig = None
    +        self.channels = None
    +        self._timeout = Timeout(maxSecs=60)
    +
    +    def showChannels(self):
    +        """Show human readable description of our channels"""
    +        print("Channels:")
    +        for c in self.channels:
    +            if c.role != channel_pb2.Channel.Role.DISABLED:
    +                cStr = stripnl(MessageToJson(c.settings))
    +                print(
    +                    f"  {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}")
    +        publicURL = self.getURL(includeAll=False)
    +        adminURL = self.getURL(includeAll=True)
    +        print(f"\nPrimary channel URL: {publicURL}")
    +        if adminURL != publicURL:
    +            print(f"Complete URL (includes all channels): {adminURL}")
    +
    +    def showInfo(self):
    +        """Show human readable description of our node"""
    +        print(
    +            f"Preferences: {stripnl(MessageToJson(self.radioConfig.preferences))}\n")
    +        self.showChannels()
    +
    +    def requestConfig(self):
    +        """
    +        Send regular MeshPackets to ask for settings and channels
    +        """
    +        self.radioConfig = None
    +        self.channels = None
    +        self.partialChannels = []  # We keep our channels in a temp array until finished
    +
    +        self._requestSettings()
    +
    +    def waitForConfig(self):
    +        """Block until radio config is received. Returns True if config has been received."""
    +        return self._timeout.waitForSet(self, attrs=('radioConfig', 'channels'))
    +
    +    def writeConfig(self):
    +        """Write the current (edited) radioConfig to the device"""
    +        if self.radioConfig == None:
    +            raise Exception("No RadioConfig has been read")
    +
    +        p = admin_pb2.AdminMessage()
    +        p.set_radio.CopyFrom(self.radioConfig)
    +
    +        self._sendAdmin(p)
    +        logging.debug("Wrote config")
    +
    +    def writeChannel(self, channelIndex, adminIndex=0):
    +        """Write the current (edited) channel to the device"""
    +
    +        p = admin_pb2.AdminMessage()
    +        p.set_channel.CopyFrom(self.channels[channelIndex])
    +
    +        self._sendAdmin(p, adminIndex=adminIndex)
    +        logging.debug(f"Wrote channel {channelIndex}")
    +
    +    def deleteChannel(self, channelIndex):
    +        """Delete the specifed channelIndex and shift other channels up"""
    +        ch = self.channels[channelIndex]
    +        if ch.role != channel_pb2.Channel.Role.SECONDARY:
    +            raise Exception("Only SECONDARY channels can be deleted")
    +
    +        # we are careful here because if we move the "admin" channel the channelIndex we need to use
    +        # for sending admin channels will also change
    +        adminIndex = self.iface.localNode._getAdminChannelIndex()
    +
    +        self.channels.pop(channelIndex)
    +        self._fixupChannels()  # expand back to 8 channels
    +
    +        index = channelIndex
    +        while index < self.iface.myInfo.max_channels:
    +            self.writeChannel(index, adminIndex=adminIndex)
    +            index += 1
    +
    +            # if we are updating the local node, we might end up *moving* the admin channel index as we are writing
    +            if (self.iface.localNode == self) and index >= adminIndex:
    +                # We've now passed the old location for admin index (and writen it), so we can start finding it by name again
    +                adminIndex = 0
    +
    +    def getChannelByName(self, name):
    +        """Try to find the named channel or return None"""
    +        for c in (self.channels or []):
    +            if c.settings and c.settings.name == name:
    +                return c
    +        return None
    +
    +    def getDisabledChannel(self):
    +        """Return the first channel that is disabled (i.e. available for some new use)"""
    +        for c in self.channels:
    +            if c.role == channel_pb2.Channel.Role.DISABLED:
    +                return c
    +        return None
    +
    +    def _getAdminChannelIndex(self):
    +        """Return the channel number of the admin channel, or 0 if no reserved channel"""
    +        c = self.getChannelByName("admin")
    +        if c:
    +            return c.index
    +        else:
    +            return 0
    +
    +    def setOwner(self, long_name, short_name=None):
    +        """Set device owner name"""
    +        nChars = 3
    +        minChars = 2
    +        if long_name is not None:
    +            long_name = long_name.strip()
    +            if short_name is None:
    +                words = long_name.split()
    +                if len(long_name) <= nChars:
    +                    short_name = long_name
    +                elif len(words) >= minChars:
    +                    short_name = ''.join(map(lambda word: word[0], words))
    +                else:
    +                    trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
    +                    short_name = long_name[0] + long_name[1:].translate(trans)
    +                    if len(short_name) < nChars:
    +                        short_name = long_name[:nChars]
    +
    +        p = admin_pb2.AdminMessage()
    +
    +        if long_name is not None:
    +            p.set_owner.long_name = long_name
    +        if short_name is not None:
    +            short_name = short_name.strip()
    +            if len(short_name) > nChars:
    +                short_name = short_name[:nChars]
    +            p.set_owner.short_name = short_name
    +
    +        return self._sendAdmin(p)
    +
    +    def getURL(self, includeAll: bool = True):
    +        """The sharable URL that describes the current channel
    +        """
    +        # Only keep the primary/secondary channels, assume primary is first
    +        channelSet = apponly_pb2.ChannelSet()
    +        for c in self.channels:
    +            if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY):
    +                channelSet.settings.append(c.settings)
    +        bytes = channelSet.SerializeToString()
    +        s = base64.urlsafe_b64encode(bytes).decode('ascii')
    +        return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
    +
    +    def setURL(self, url):
    +        """Set mesh network URL"""
    +        if self.radioConfig == None:
    +            raise Exception("No RadioConfig has been read")
    +
    +        # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
    +        # Split on '/#' to find the base64 encoded channel settings
    +        splitURL = url.split("/#")
    +        b64 = splitURL[-1]
    +
    +        # We normally strip padding to make for a shorter URL, but the python parser doesn't like
    +        # that.  So add back any missing padding
    +        # per https://stackoverflow.com/a/9807138
    +        missing_padding = len(b64) % 4
    +        if missing_padding:
    +            b64 += '=' * (4 - missing_padding)
    +
    +        decodedURL = base64.urlsafe_b64decode(b64)
    +        channelSet = apponly_pb2.ChannelSet()
    +        channelSet.ParseFromString(decodedURL)
    +
    +        i = 0
    +        for chs in channelSet.settings:
    +            ch = channel_pb2.Channel()
    +            ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
    +            ch.index = i
    +            ch.settings.CopyFrom(chs)
    +            self.channels[ch.index] = ch
    +            self.writeChannel(ch.index)
    +            i = i + 1
    +
    +    def _requestSettings(self):
    +        """
    +        Done with initial config messages, now send regular MeshPackets to ask for settings
    +        """
    +        p = admin_pb2.AdminMessage()
    +        p.get_radio_request = True
    +
    +        def onResponse(p):
    +            """A closure to handle the response packet"""
    +            self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
    +            logging.debug("Received radio config, now fetching channels...")
    +            self._timeout.reset()  # We made foreward progress
    +            self._requestChannel(0)  # now start fetching channels
    +
    +        # Show progress message for super slow operations
    +        if self != self.iface.localNode:
    +            logging.info(
    +                "Requesting preferences from remote node (this could take a while)")
    +
    +        return self._sendAdmin(p,
    +                               wantResponse=True,
    +                               onResponse=onResponse)
    +
    +    def exitSimulator(self):
    +        """
    +        Tell a simulator node to exit (this message is ignored for other nodes)
    +        """
    +        p = admin_pb2.AdminMessage()
    +        p.exit_simulator = True
    +
    +        return self._sendAdmin(p)
    +
    +    def reboot(self, secs: int = 10):
    +        """
    +        Tell the node to reboot
    +        """
    +        p = admin_pb2.AdminMessage()
    +        p.reboot_seconds = secs
    +        logging.info(f"Telling node to reboot in {secs} seconds")
    +
    +        return self._sendAdmin(p)
    +
    +    def _fixupChannels(self):
    +        """Fixup indexes and add disabled channels as needed"""
    +
    +        # Add extra disabled channels as needed
    +        for index, ch in enumerate(self.channels):
    +            ch.index = index  # fixup indexes
    +
    +        self._fillChannels()
    +
    +    def _fillChannels(self):
    +        """Mark unused channels as disabled"""
    +
    +        # Add extra disabled channels as needed
    +        index = len(self.channels)
    +        while index < self.iface.myInfo.max_channels:
    +            ch = channel_pb2.Channel()
    +            ch.role = channel_pb2.Channel.Role.DISABLED
    +            ch.index = index
    +            self.channels.append(ch)
    +            index += 1
    +
    +    def _requestChannel(self, channelNum: int):
    +        """
    +        Done with initial config messages, now send regular MeshPackets to ask for settings
    +        """
    +        p = admin_pb2.AdminMessage()
    +        p.get_channel_request = channelNum + 1
    +
    +        # Show progress message for super slow operations
    +        if self != self.iface.localNode:
    +            logging.info(
    +                f"Requesting channel {channelNum} info from remote node (this could take a while)")
    +        else:
    +            logging.debug(f"Requesting channel {channelNum}")
    +
    +        def onResponse(p):
    +            """A closure to handle the response packet"""
    +            c = p["decoded"]["admin"]["raw"].get_channel_response
    +            self.partialChannels.append(c)
    +            self._timeout.reset()  # We made foreward progress
    +            logging.debug(f"Received channel {stripnl(c)}")
    +            index = c.index
    +
    +            # for stress testing, we can always download all channels
    +            fastChannelDownload = True
    +
    +            # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
    +            quitEarly = (
    +                c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
    +
    +            if quitEarly or index >= self.iface.myInfo.max_channels - 1:
    +                logging.debug("Finished downloading channels")
    +
    +                self.channels = self.partialChannels
    +                self._fixupChannels()
    +
    +                # FIXME, the following should only be called after we have settings and channels
    +                self.iface._connected()  # Tell everone else we are ready to go
    +            else:
    +                self._requestChannel(index + 1)
    +
    +        return self._sendAdmin(p,
    +                               wantResponse=True,
    +                               onResponse=onResponse)
    +
    +    def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False,
    +                   onResponse=None,
    +                   adminIndex=0):
    +        """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
    +
    +        if adminIndex == 0:  # unless a special channel index was used, we want to use the admin index
    +            adminIndex = self.iface.localNode._getAdminChannelIndex()
    +
    +        return self.iface.sendData(p, self.nodeNum,
    +                                   portNum=portnums_pb2.PortNum.ADMIN_APP,
    +                                   wantAck=True,
    +                                   wantResponse=wantResponse,
    +                                   onResponse=onResponse,
    +                                   channelIndex=adminIndex)
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +def pskToString(psk: bytes) +
    +
    +

    Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string

    +
    + +Expand source code + +
    def pskToString(psk: bytes):
    +    """Given an array of PSK bytes, decode them into a human readable (but privacy protecting) string"""
    +    if len(psk) == 0:
    +        return "unencrypted"
    +    elif len(psk) == 1:
    +        b = psk[0]
    +        if b == 0:
    +            return "unencrypted"
    +        elif b == 1:
    +            return "default"
    +        else:
    +            return f"simple{b - 1}"
    +    else:
    +        return "secret"
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Node +(iface, nodeNum) +
    +
    +

    A model of a (local or remote) node in the mesh

    +

    Includes methods for radioConfig and channels

    +

    Constructor

    +
    + +Expand source code + +
    class Node:
    +    """A model of a (local or remote) node in the mesh
    +
    +    Includes methods for radioConfig and channels
    +    """
    +
    +    def __init__(self, iface, nodeNum):
    +        """Constructor"""
    +        self.iface = iface
    +        self.nodeNum = nodeNum
    +        self.radioConfig = None
    +        self.channels = None
    +        self._timeout = Timeout(maxSecs=60)
    +
    +    def showChannels(self):
    +        """Show human readable description of our channels"""
    +        print("Channels:")
    +        for c in self.channels:
    +            if c.role != channel_pb2.Channel.Role.DISABLED:
    +                cStr = stripnl(MessageToJson(c.settings))
    +                print(
    +                    f"  {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}")
    +        publicURL = self.getURL(includeAll=False)
    +        adminURL = self.getURL(includeAll=True)
    +        print(f"\nPrimary channel URL: {publicURL}")
    +        if adminURL != publicURL:
    +            print(f"Complete URL (includes all channels): {adminURL}")
    +
    +    def showInfo(self):
    +        """Show human readable description of our node"""
    +        print(
    +            f"Preferences: {stripnl(MessageToJson(self.radioConfig.preferences))}\n")
    +        self.showChannels()
    +
    +    def requestConfig(self):
    +        """
    +        Send regular MeshPackets to ask for settings and channels
    +        """
    +        self.radioConfig = None
    +        self.channels = None
    +        self.partialChannels = []  # We keep our channels in a temp array until finished
    +
    +        self._requestSettings()
    +
    +    def waitForConfig(self):
    +        """Block until radio config is received. Returns True if config has been received."""
    +        return self._timeout.waitForSet(self, attrs=('radioConfig', 'channels'))
    +
    +    def writeConfig(self):
    +        """Write the current (edited) radioConfig to the device"""
    +        if self.radioConfig == None:
    +            raise Exception("No RadioConfig has been read")
    +
    +        p = admin_pb2.AdminMessage()
    +        p.set_radio.CopyFrom(self.radioConfig)
    +
    +        self._sendAdmin(p)
    +        logging.debug("Wrote config")
    +
    +    def writeChannel(self, channelIndex, adminIndex=0):
    +        """Write the current (edited) channel to the device"""
    +
    +        p = admin_pb2.AdminMessage()
    +        p.set_channel.CopyFrom(self.channels[channelIndex])
    +
    +        self._sendAdmin(p, adminIndex=adminIndex)
    +        logging.debug(f"Wrote channel {channelIndex}")
    +
    +    def deleteChannel(self, channelIndex):
    +        """Delete the specifed channelIndex and shift other channels up"""
    +        ch = self.channels[channelIndex]
    +        if ch.role != channel_pb2.Channel.Role.SECONDARY:
    +            raise Exception("Only SECONDARY channels can be deleted")
    +
    +        # we are careful here because if we move the "admin" channel the channelIndex we need to use
    +        # for sending admin channels will also change
    +        adminIndex = self.iface.localNode._getAdminChannelIndex()
    +
    +        self.channels.pop(channelIndex)
    +        self._fixupChannels()  # expand back to 8 channels
    +
    +        index = channelIndex
    +        while index < self.iface.myInfo.max_channels:
    +            self.writeChannel(index, adminIndex=adminIndex)
    +            index += 1
    +
    +            # if we are updating the local node, we might end up *moving* the admin channel index as we are writing
    +            if (self.iface.localNode == self) and index >= adminIndex:
    +                # We've now passed the old location for admin index (and writen it), so we can start finding it by name again
    +                adminIndex = 0
    +
    +    def getChannelByName(self, name):
    +        """Try to find the named channel or return None"""
    +        for c in (self.channels or []):
    +            if c.settings and c.settings.name == name:
    +                return c
    +        return None
    +
    +    def getDisabledChannel(self):
    +        """Return the first channel that is disabled (i.e. available for some new use)"""
    +        for c in self.channels:
    +            if c.role == channel_pb2.Channel.Role.DISABLED:
    +                return c
    +        return None
    +
    +    def _getAdminChannelIndex(self):
    +        """Return the channel number of the admin channel, or 0 if no reserved channel"""
    +        c = self.getChannelByName("admin")
    +        if c:
    +            return c.index
    +        else:
    +            return 0
    +
    +    def setOwner(self, long_name, short_name=None):
    +        """Set device owner name"""
    +        nChars = 3
    +        minChars = 2
    +        if long_name is not None:
    +            long_name = long_name.strip()
    +            if short_name is None:
    +                words = long_name.split()
    +                if len(long_name) <= nChars:
    +                    short_name = long_name
    +                elif len(words) >= minChars:
    +                    short_name = ''.join(map(lambda word: word[0], words))
    +                else:
    +                    trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
    +                    short_name = long_name[0] + long_name[1:].translate(trans)
    +                    if len(short_name) < nChars:
    +                        short_name = long_name[:nChars]
    +
    +        p = admin_pb2.AdminMessage()
    +
    +        if long_name is not None:
    +            p.set_owner.long_name = long_name
    +        if short_name is not None:
    +            short_name = short_name.strip()
    +            if len(short_name) > nChars:
    +                short_name = short_name[:nChars]
    +            p.set_owner.short_name = short_name
    +
    +        return self._sendAdmin(p)
    +
    +    def getURL(self, includeAll: bool = True):
    +        """The sharable URL that describes the current channel
    +        """
    +        # Only keep the primary/secondary channels, assume primary is first
    +        channelSet = apponly_pb2.ChannelSet()
    +        for c in self.channels:
    +            if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY):
    +                channelSet.settings.append(c.settings)
    +        bytes = channelSet.SerializeToString()
    +        s = base64.urlsafe_b64encode(bytes).decode('ascii')
    +        return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
    +
    +    def setURL(self, url):
    +        """Set mesh network URL"""
    +        if self.radioConfig == None:
    +            raise Exception("No RadioConfig has been read")
    +
    +        # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
    +        # Split on '/#' to find the base64 encoded channel settings
    +        splitURL = url.split("/#")
    +        b64 = splitURL[-1]
    +
    +        # We normally strip padding to make for a shorter URL, but the python parser doesn't like
    +        # that.  So add back any missing padding
    +        # per https://stackoverflow.com/a/9807138
    +        missing_padding = len(b64) % 4
    +        if missing_padding:
    +            b64 += '=' * (4 - missing_padding)
    +
    +        decodedURL = base64.urlsafe_b64decode(b64)
    +        channelSet = apponly_pb2.ChannelSet()
    +        channelSet.ParseFromString(decodedURL)
    +
    +        i = 0
    +        for chs in channelSet.settings:
    +            ch = channel_pb2.Channel()
    +            ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
    +            ch.index = i
    +            ch.settings.CopyFrom(chs)
    +            self.channels[ch.index] = ch
    +            self.writeChannel(ch.index)
    +            i = i + 1
    +
    +    def _requestSettings(self):
    +        """
    +        Done with initial config messages, now send regular MeshPackets to ask for settings
    +        """
    +        p = admin_pb2.AdminMessage()
    +        p.get_radio_request = True
    +
    +        def onResponse(p):
    +            """A closure to handle the response packet"""
    +            self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
    +            logging.debug("Received radio config, now fetching channels...")
    +            self._timeout.reset()  # We made foreward progress
    +            self._requestChannel(0)  # now start fetching channels
    +
    +        # Show progress message for super slow operations
    +        if self != self.iface.localNode:
    +            logging.info(
    +                "Requesting preferences from remote node (this could take a while)")
    +
    +        return self._sendAdmin(p,
    +                               wantResponse=True,
    +                               onResponse=onResponse)
    +
    +    def exitSimulator(self):
    +        """
    +        Tell a simulator node to exit (this message is ignored for other nodes)
    +        """
    +        p = admin_pb2.AdminMessage()
    +        p.exit_simulator = True
    +
    +        return self._sendAdmin(p)
    +
    +    def reboot(self, secs: int = 10):
    +        """
    +        Tell the node to reboot
    +        """
    +        p = admin_pb2.AdminMessage()
    +        p.reboot_seconds = secs
    +        logging.info(f"Telling node to reboot in {secs} seconds")
    +
    +        return self._sendAdmin(p)
    +
    +    def _fixupChannels(self):
    +        """Fixup indexes and add disabled channels as needed"""
    +
    +        # Add extra disabled channels as needed
    +        for index, ch in enumerate(self.channels):
    +            ch.index = index  # fixup indexes
    +
    +        self._fillChannels()
    +
    +    def _fillChannels(self):
    +        """Mark unused channels as disabled"""
    +
    +        # Add extra disabled channels as needed
    +        index = len(self.channels)
    +        while index < self.iface.myInfo.max_channels:
    +            ch = channel_pb2.Channel()
    +            ch.role = channel_pb2.Channel.Role.DISABLED
    +            ch.index = index
    +            self.channels.append(ch)
    +            index += 1
    +
    +    def _requestChannel(self, channelNum: int):
    +        """
    +        Done with initial config messages, now send regular MeshPackets to ask for settings
    +        """
    +        p = admin_pb2.AdminMessage()
    +        p.get_channel_request = channelNum + 1
    +
    +        # Show progress message for super slow operations
    +        if self != self.iface.localNode:
    +            logging.info(
    +                f"Requesting channel {channelNum} info from remote node (this could take a while)")
    +        else:
    +            logging.debug(f"Requesting channel {channelNum}")
    +
    +        def onResponse(p):
    +            """A closure to handle the response packet"""
    +            c = p["decoded"]["admin"]["raw"].get_channel_response
    +            self.partialChannels.append(c)
    +            self._timeout.reset()  # We made foreward progress
    +            logging.debug(f"Received channel {stripnl(c)}")
    +            index = c.index
    +
    +            # for stress testing, we can always download all channels
    +            fastChannelDownload = True
    +
    +            # Once we see a response that has NO settings, assume we are at the end of channels and stop fetching
    +            quitEarly = (
    +                c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload
    +
    +            if quitEarly or index >= self.iface.myInfo.max_channels - 1:
    +                logging.debug("Finished downloading channels")
    +
    +                self.channels = self.partialChannels
    +                self._fixupChannels()
    +
    +                # FIXME, the following should only be called after we have settings and channels
    +                self.iface._connected()  # Tell everone else we are ready to go
    +            else:
    +                self._requestChannel(index + 1)
    +
    +        return self._sendAdmin(p,
    +                               wantResponse=True,
    +                               onResponse=onResponse)
    +
    +    def _sendAdmin(self, p: admin_pb2.AdminMessage, wantResponse=False,
    +                   onResponse=None,
    +                   adminIndex=0):
    +        """Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
    +
    +        if adminIndex == 0:  # unless a special channel index was used, we want to use the admin index
    +            adminIndex = self.iface.localNode._getAdminChannelIndex()
    +
    +        return self.iface.sendData(p, self.nodeNum,
    +                                   portNum=portnums_pb2.PortNum.ADMIN_APP,
    +                                   wantAck=True,
    +                                   wantResponse=wantResponse,
    +                                   onResponse=onResponse,
    +                                   channelIndex=adminIndex)
    +
    +

    Methods

    +
    +
    +def deleteChannel(self, channelIndex) +
    +
    +

    Delete the specifed channelIndex and shift other channels up

    +
    + +Expand source code + +
    def deleteChannel(self, channelIndex):
    +    """Delete the specifed channelIndex and shift other channels up"""
    +    ch = self.channels[channelIndex]
    +    if ch.role != channel_pb2.Channel.Role.SECONDARY:
    +        raise Exception("Only SECONDARY channels can be deleted")
    +
    +    # we are careful here because if we move the "admin" channel the channelIndex we need to use
    +    # for sending admin channels will also change
    +    adminIndex = self.iface.localNode._getAdminChannelIndex()
    +
    +    self.channels.pop(channelIndex)
    +    self._fixupChannels()  # expand back to 8 channels
    +
    +    index = channelIndex
    +    while index < self.iface.myInfo.max_channels:
    +        self.writeChannel(index, adminIndex=adminIndex)
    +        index += 1
    +
    +        # if we are updating the local node, we might end up *moving* the admin channel index as we are writing
    +        if (self.iface.localNode == self) and index >= adminIndex:
    +            # We've now passed the old location for admin index (and writen it), so we can start finding it by name again
    +            adminIndex = 0
    +
    +
    +
    +def exitSimulator(self) +
    +
    +

    Tell a simulator node to exit (this message is ignored for other nodes)

    +
    + +Expand source code + +
    def exitSimulator(self):
    +    """
    +    Tell a simulator node to exit (this message is ignored for other nodes)
    +    """
    +    p = admin_pb2.AdminMessage()
    +    p.exit_simulator = True
    +
    +    return self._sendAdmin(p)
    +
    +
    +
    +def getChannelByName(self, name) +
    +
    +

    Try to find the named channel or return None

    +
    + +Expand source code + +
    def getChannelByName(self, name):
    +    """Try to find the named channel or return None"""
    +    for c in (self.channels or []):
    +        if c.settings and c.settings.name == name:
    +            return c
    +    return None
    +
    +
    +
    +def getDisabledChannel(self) +
    +
    +

    Return the first channel that is disabled (i.e. available for some new use)

    +
    + +Expand source code + +
    def getDisabledChannel(self):
    +    """Return the first channel that is disabled (i.e. available for some new use)"""
    +    for c in self.channels:
    +        if c.role == channel_pb2.Channel.Role.DISABLED:
    +            return c
    +    return None
    +
    +
    +
    +def getURL(self, includeAll: bool = True) +
    +
    +

    The sharable URL that describes the current channel

    +
    + +Expand source code + +
    def getURL(self, includeAll: bool = True):
    +    """The sharable URL that describes the current channel
    +    """
    +    # Only keep the primary/secondary channels, assume primary is first
    +    channelSet = apponly_pb2.ChannelSet()
    +    for c in self.channels:
    +        if c.role == channel_pb2.Channel.Role.PRIMARY or (includeAll and c.role == channel_pb2.Channel.Role.SECONDARY):
    +            channelSet.settings.append(c.settings)
    +    bytes = channelSet.SerializeToString()
    +    s = base64.urlsafe_b64encode(bytes).decode('ascii')
    +    return f"https://www.meshtastic.org/d/#{s}".replace("=", "")
    +
    +
    +
    +def reboot(self, secs: int = 10) +
    +
    +

    Tell the node to reboot

    +
    + +Expand source code + +
    def reboot(self, secs: int = 10):
    +    """
    +    Tell the node to reboot
    +    """
    +    p = admin_pb2.AdminMessage()
    +    p.reboot_seconds = secs
    +    logging.info(f"Telling node to reboot in {secs} seconds")
    +
    +    return self._sendAdmin(p)
    +
    +
    +
    +def requestConfig(self) +
    +
    +

    Send regular MeshPackets to ask for settings and channels

    +
    + +Expand source code + +
    def requestConfig(self):
    +    """
    +    Send regular MeshPackets to ask for settings and channels
    +    """
    +    self.radioConfig = None
    +    self.channels = None
    +    self.partialChannels = []  # We keep our channels in a temp array until finished
    +
    +    self._requestSettings()
    +
    +
    +
    +def setOwner(self, long_name, short_name=None) +
    +
    +

    Set device owner name

    +
    + +Expand source code + +
    def setOwner(self, long_name, short_name=None):
    +    """Set device owner name"""
    +    nChars = 3
    +    minChars = 2
    +    if long_name is not None:
    +        long_name = long_name.strip()
    +        if short_name is None:
    +            words = long_name.split()
    +            if len(long_name) <= nChars:
    +                short_name = long_name
    +            elif len(words) >= minChars:
    +                short_name = ''.join(map(lambda word: word[0], words))
    +            else:
    +                trans = str.maketrans(dict.fromkeys('aeiouAEIOU'))
    +                short_name = long_name[0] + long_name[1:].translate(trans)
    +                if len(short_name) < nChars:
    +                    short_name = long_name[:nChars]
    +
    +    p = admin_pb2.AdminMessage()
    +
    +    if long_name is not None:
    +        p.set_owner.long_name = long_name
    +    if short_name is not None:
    +        short_name = short_name.strip()
    +        if len(short_name) > nChars:
    +            short_name = short_name[:nChars]
    +        p.set_owner.short_name = short_name
    +
    +    return self._sendAdmin(p)
    +
    +
    +
    +def setURL(self, url) +
    +
    +

    Set mesh network URL

    +
    + +Expand source code + +
    def setURL(self, url):
    +    """Set mesh network URL"""
    +    if self.radioConfig == None:
    +        raise Exception("No RadioConfig has been read")
    +
    +    # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set}
    +    # Split on '/#' to find the base64 encoded channel settings
    +    splitURL = url.split("/#")
    +    b64 = splitURL[-1]
    +
    +    # We normally strip padding to make for a shorter URL, but the python parser doesn't like
    +    # that.  So add back any missing padding
    +    # per https://stackoverflow.com/a/9807138
    +    missing_padding = len(b64) % 4
    +    if missing_padding:
    +        b64 += '=' * (4 - missing_padding)
    +
    +    decodedURL = base64.urlsafe_b64decode(b64)
    +    channelSet = apponly_pb2.ChannelSet()
    +    channelSet.ParseFromString(decodedURL)
    +
    +    i = 0
    +    for chs in channelSet.settings:
    +        ch = channel_pb2.Channel()
    +        ch.role = channel_pb2.Channel.Role.PRIMARY if i == 0 else channel_pb2.Channel.Role.SECONDARY
    +        ch.index = i
    +        ch.settings.CopyFrom(chs)
    +        self.channels[ch.index] = ch
    +        self.writeChannel(ch.index)
    +        i = i + 1
    +
    +
    +
    +def showChannels(self) +
    +
    +

    Show human readable description of our channels

    +
    + +Expand source code + +
    def showChannels(self):
    +    """Show human readable description of our channels"""
    +    print("Channels:")
    +    for c in self.channels:
    +        if c.role != channel_pb2.Channel.Role.DISABLED:
    +            cStr = stripnl(MessageToJson(c.settings))
    +            print(
    +                f"  {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}")
    +    publicURL = self.getURL(includeAll=False)
    +    adminURL = self.getURL(includeAll=True)
    +    print(f"\nPrimary channel URL: {publicURL}")
    +    if adminURL != publicURL:
    +        print(f"Complete URL (includes all channels): {adminURL}")
    +
    +
    +
    +def showInfo(self) +
    +
    +

    Show human readable description of our node

    +
    + +Expand source code + +
    def showInfo(self):
    +    """Show human readable description of our node"""
    +    print(
    +        f"Preferences: {stripnl(MessageToJson(self.radioConfig.preferences))}\n")
    +    self.showChannels()
    +
    +
    +
    +def waitForConfig(self) +
    +
    +

    Block until radio config is received. Returns True if config has been received.

    +
    + +Expand source code + +
    def waitForConfig(self):
    +    """Block until radio config is received. Returns True if config has been received."""
    +    return self._timeout.waitForSet(self, attrs=('radioConfig', 'channels'))
    +
    +
    +
    +def writeChannel(self, channelIndex, adminIndex=0) +
    +
    +

    Write the current (edited) channel to the device

    +
    + +Expand source code + +
    def writeChannel(self, channelIndex, adminIndex=0):
    +    """Write the current (edited) channel to the device"""
    +
    +    p = admin_pb2.AdminMessage()
    +    p.set_channel.CopyFrom(self.channels[channelIndex])
    +
    +    self._sendAdmin(p, adminIndex=adminIndex)
    +    logging.debug(f"Wrote channel {channelIndex}")
    +
    +
    +
    +def writeConfig(self) +
    +
    +

    Write the current (edited) radioConfig to the device

    +
    + +Expand source code + +
    def writeConfig(self):
    +    """Write the current (edited) radioConfig to the device"""
    +    if self.radioConfig == None:
    +        raise Exception("No RadioConfig has been read")
    +
    +    p = admin_pb2.AdminMessage()
    +    p.set_radio.CopyFrom(self.radioConfig)
    +
    +    self._sendAdmin(p)
    +    logging.debug("Wrote config")
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/meshtastic/radioconfig_pb2.html b/docs/meshtastic/radioconfig_pb2.html index 3583dd7..6712b4a 100644 --- a/docs/meshtastic/radioconfig_pb2.html +++ b/docs/meshtastic/radioconfig_pb2.html @@ -49,7 +49,7 @@ DESCRIPTOR = _descriptor.FileDescriptor( syntax='proto3', serialized_options=b'\n\023com.geeksville.meshB\021RadioConfigProtosH\003', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x11radioconfig.proto\"\xe0\x0f\n\x0bRadioConfig\x12\x31\n\x0bpreferences\x18\x01 \x01(\x0b\x32\x1c.RadioConfig.UserPreferences\x1a\x9d\x0f\n\x0fUserPreferences\x12\x1f\n\x17position_broadcast_secs\x18\x01 \x01(\r\x12\x1b\n\x13send_owner_interval\x18\x02 \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\x11\n\twifi_ssid\x18\x0c \x01(\t\x12\x15\n\rwifi_password\x18\r \x01(\t\x12\x14\n\x0cwifi_ap_mode\x18\x0e \x01(\x08\x12\x1b\n\x06region\x18\x0f \x01(\x0e\x32\x0b.RegionCode\x12&\n\x0e\x63harge_current\x18\x10 \x01(\x0e\x32\x0e.ChargeCurrent\x12\x11\n\tis_router\x18% \x01(\x08\x12\x14\n\x0cis_low_power\x18& \x01(\x08\x12\x16\n\x0e\x66ixed_position\x18\' \x01(\x08\x12\x17\n\x0fserial_disabled\x18( \x01(\x08\x12(\n\x0elocation_share\x18 \x01(\x0e\x32\x10.LocationSharing\x12$\n\rgps_operation\x18! \x01(\x0e\x32\r.GpsOperation\x12\x1b\n\x13gps_update_interval\x18\" \x01(\r\x12\x18\n\x10gps_attempt_time\x18$ \x01(\r\x12\x18\n\x10\x66requency_offset\x18) \x01(\x02\x12\x15\n\rfactory_reset\x18\x64 \x01(\x08\x12\x19\n\x11\x64\x65\x62ug_log_enabled\x18\x65 \x01(\x08\x12\x17\n\x0fignore_incoming\x18g \x03(\r\x12\x1c\n\x14serialplugin_enabled\x18x \x01(\x08\x12\x19\n\x11serialplugin_echo\x18y \x01(\x08\x12\x18\n\x10serialplugin_rxd\x18z \x01(\r\x12\x18\n\x10serialplugin_txd\x18{ \x01(\r\x12\x1c\n\x14serialplugin_timeout\x18| \x01(\r\x12\x19\n\x11serialplugin_mode\x18} \x01(\r\x12\'\n\x1f\x65xt_notification_plugin_enabled\x18~ \x01(\x08\x12)\n!ext_notification_plugin_output_ms\x18\x7f \x01(\r\x12\'\n\x1e\x65xt_notification_plugin_output\x18\x80\x01 \x01(\r\x12\'\n\x1e\x65xt_notification_plugin_active\x18\x81\x01 \x01(\x08\x12.\n%ext_notification_plugin_alert_message\x18\x82\x01 \x01(\x08\x12+\n\"ext_notification_plugin_alert_bell\x18\x83\x01 \x01(\x08\x12\"\n\x19range_test_plugin_enabled\x18\x84\x01 \x01(\x08\x12!\n\x18range_test_plugin_sender\x18\x85\x01 \x01(\r\x12\x1f\n\x16range_test_plugin_save\x18\x86\x01 \x01(\x08\x12%\n\x1cstore_forward_plugin_enabled\x18\x94\x01 \x01(\x08\x12%\n\x1cstore_forward_plugin_records\x18\x89\x01 \x01(\r\x12=\n4environmental_measurement_plugin_measurement_enabled\x18\x8c\x01 \x01(\x08\x12\x38\n/environmental_measurement_plugin_screen_enabled\x18\x8d\x01 \x01(\x08\x12\x44\n;environmental_measurement_plugin_read_error_count_threshold\x18\x8e\x01 \x01(\r\x12\x39\n0environmental_measurement_plugin_update_interval\x18\x8f\x01 \x01(\r\x12;\n2environmental_measurement_plugin_recovery_interval\x18\x90\x01 \x01(\r\x12;\n2environmental_measurement_plugin_display_farenheit\x18\x91\x01 \x01(\x08\x12v\n,environmental_measurement_plugin_sensor_type\x18\x92\x01 \x01(\x0e\x32?.RadioConfig.UserPreferences.EnvironmentalMeasurementSensorType\x12\x34\n+environmental_measurement_plugin_sensor_pin\x18\x93\x01 \x01(\r\"/\n\"EnvironmentalMeasurementSensorType\x12\t\n\x05\x44HT11\x10\x00J\x06\x08\x88\x01\x10\x89\x01*f\n\nRegionCode\x12\t\n\x05Unset\x10\x00\x12\x06\n\x02US\x10\x01\x12\t\n\x05\x45U433\x10\x02\x12\t\n\x05\x45U865\x10\x03\x12\x06\n\x02\x43N\x10\x04\x12\x06\n\x02JP\x10\x05\x12\x07\n\x03\x41NZ\x10\x06\x12\x06\n\x02KR\x10\x07\x12\x06\n\x02TW\x10\x08\x12\x06\n\x02RU\x10\t*\xd1\x01\n\rChargeCurrent\x12\x0b\n\x07MAUnset\x10\x00\x12\t\n\x05MA100\x10\x01\x12\t\n\x05MA190\x10\x02\x12\t\n\x05MA280\x10\x03\x12\t\n\x05MA360\x10\x04\x12\t\n\x05MA450\x10\x05\x12\t\n\x05MA550\x10\x06\x12\t\n\x05MA630\x10\x07\x12\t\n\x05MA700\x10\x08\x12\t\n\x05MA780\x10\t\x12\t\n\x05MA880\x10\n\x12\t\n\x05MA960\x10\x0b\x12\n\n\x06MA1000\x10\x0c\x12\n\n\x06MA1080\x10\r\x12\n\n\x06MA1160\x10\x0e\x12\n\n\x06MA1240\x10\x0f\x12\n\n\x06MA1320\x10\x10*j\n\x0cGpsOperation\x12\x0e\n\nGpsOpUnset\x10\x00\x12\x13\n\x0fGpsOpStationary\x10\x01\x12\x0f\n\x0bGpsOpMobile\x10\x02\x12\x11\n\rGpsOpTimeOnly\x10\x03\x12\x11\n\rGpsOpDisabled\x10\x04*@\n\x0fLocationSharing\x12\x0c\n\x08LocUnset\x10\x00\x12\x0e\n\nLocEnabled\x10\x01\x12\x0f\n\x0bLocDisabled\x10\x02\x42*\n\x13\x63om.geeksville.meshB\x11RadioConfigProtosH\x03\x62\x06proto3' + serialized_pb=b'\n\x11radioconfig.proto\"\x8c\x10\n\x0bRadioConfig\x12\x31\n\x0bpreferences\x18\x01 \x01(\x0b\x32\x1c.RadioConfig.UserPreferences\x1a\xc9\x0f\n\x0fUserPreferences\x12\x1f\n\x17position_broadcast_secs\x18\x01 \x01(\r\x12\x1b\n\x13send_owner_interval\x18\x02 \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\x11\n\twifi_ssid\x18\x0c \x01(\t\x12\x15\n\rwifi_password\x18\r \x01(\t\x12\x14\n\x0cwifi_ap_mode\x18\x0e \x01(\x08\x12\x1b\n\x06region\x18\x0f \x01(\x0e\x32\x0b.RegionCode\x12&\n\x0e\x63harge_current\x18\x10 \x01(\x0e\x32\x0e.ChargeCurrent\x12\x11\n\tis_router\x18% \x01(\x08\x12\x14\n\x0cis_low_power\x18& \x01(\x08\x12\x16\n\x0e\x66ixed_position\x18\' \x01(\x08\x12\x17\n\x0fserial_disabled\x18( \x01(\x08\x12(\n\x0elocation_share\x18 \x01(\x0e\x32\x10.LocationSharing\x12$\n\rgps_operation\x18! \x01(\x0e\x32\r.GpsOperation\x12\x1b\n\x13gps_update_interval\x18\" \x01(\r\x12\x18\n\x10gps_attempt_time\x18$ \x01(\r\x12\x18\n\x10\x66requency_offset\x18) \x01(\x02\x12\x13\n\x0bmqtt_server\x18* \x01(\t\x12\x15\n\rmqtt_disabled\x18+ \x01(\x08\x12\x15\n\rfactory_reset\x18\x64 \x01(\x08\x12\x19\n\x11\x64\x65\x62ug_log_enabled\x18\x65 \x01(\x08\x12\x17\n\x0fignore_incoming\x18g \x03(\r\x12\x1c\n\x14serialplugin_enabled\x18x \x01(\x08\x12\x19\n\x11serialplugin_echo\x18y \x01(\x08\x12\x18\n\x10serialplugin_rxd\x18z \x01(\r\x12\x18\n\x10serialplugin_txd\x18{ \x01(\r\x12\x1c\n\x14serialplugin_timeout\x18| \x01(\r\x12\x19\n\x11serialplugin_mode\x18} \x01(\r\x12\'\n\x1f\x65xt_notification_plugin_enabled\x18~ \x01(\x08\x12)\n!ext_notification_plugin_output_ms\x18\x7f \x01(\r\x12\'\n\x1e\x65xt_notification_plugin_output\x18\x80\x01 \x01(\r\x12\'\n\x1e\x65xt_notification_plugin_active\x18\x81\x01 \x01(\x08\x12.\n%ext_notification_plugin_alert_message\x18\x82\x01 \x01(\x08\x12+\n\"ext_notification_plugin_alert_bell\x18\x83\x01 \x01(\x08\x12\"\n\x19range_test_plugin_enabled\x18\x84\x01 \x01(\x08\x12!\n\x18range_test_plugin_sender\x18\x85\x01 \x01(\r\x12\x1f\n\x16range_test_plugin_save\x18\x86\x01 \x01(\x08\x12%\n\x1cstore_forward_plugin_enabled\x18\x94\x01 \x01(\x08\x12%\n\x1cstore_forward_plugin_records\x18\x89\x01 \x01(\r\x12=\n4environmental_measurement_plugin_measurement_enabled\x18\x8c\x01 \x01(\x08\x12\x38\n/environmental_measurement_plugin_screen_enabled\x18\x8d\x01 \x01(\x08\x12\x44\n;environmental_measurement_plugin_read_error_count_threshold\x18\x8e\x01 \x01(\r\x12\x39\n0environmental_measurement_plugin_update_interval\x18\x8f\x01 \x01(\r\x12;\n2environmental_measurement_plugin_recovery_interval\x18\x90\x01 \x01(\r\x12;\n2environmental_measurement_plugin_display_farenheit\x18\x91\x01 \x01(\x08\x12v\n,environmental_measurement_plugin_sensor_type\x18\x92\x01 \x01(\x0e\x32?.RadioConfig.UserPreferences.EnvironmentalMeasurementSensorType\x12\x34\n+environmental_measurement_plugin_sensor_pin\x18\x93\x01 \x01(\r\"/\n\"EnvironmentalMeasurementSensorType\x12\t\n\x05\x44HT11\x10\x00J\x06\x08\x88\x01\x10\x89\x01*f\n\nRegionCode\x12\t\n\x05Unset\x10\x00\x12\x06\n\x02US\x10\x01\x12\t\n\x05\x45U433\x10\x02\x12\t\n\x05\x45U865\x10\x03\x12\x06\n\x02\x43N\x10\x04\x12\x06\n\x02JP\x10\x05\x12\x07\n\x03\x41NZ\x10\x06\x12\x06\n\x02KR\x10\x07\x12\x06\n\x02TW\x10\x08\x12\x06\n\x02RU\x10\t*\xd1\x01\n\rChargeCurrent\x12\x0b\n\x07MAUnset\x10\x00\x12\t\n\x05MA100\x10\x01\x12\t\n\x05MA190\x10\x02\x12\t\n\x05MA280\x10\x03\x12\t\n\x05MA360\x10\x04\x12\t\n\x05MA450\x10\x05\x12\t\n\x05MA550\x10\x06\x12\t\n\x05MA630\x10\x07\x12\t\n\x05MA700\x10\x08\x12\t\n\x05MA780\x10\t\x12\t\n\x05MA880\x10\n\x12\t\n\x05MA960\x10\x0b\x12\n\n\x06MA1000\x10\x0c\x12\n\n\x06MA1080\x10\r\x12\n\n\x06MA1160\x10\x0e\x12\n\n\x06MA1240\x10\x0f\x12\n\n\x06MA1320\x10\x10*j\n\x0cGpsOperation\x12\x0e\n\nGpsOpUnset\x10\x00\x12\x13\n\x0fGpsOpStationary\x10\x01\x12\x0f\n\x0bGpsOpMobile\x10\x02\x12\x11\n\rGpsOpTimeOnly\x10\x03\x12\x11\n\rGpsOpDisabled\x10\x04*@\n\x0fLocationSharing\x12\x0c\n\x08LocUnset\x10\x00\x12\x0e\n\nLocEnabled\x10\x01\x12\x0f\n\x0bLocDisabled\x10\x02\x42*\n\x13\x63om.geeksville.meshB\x11RadioConfigProtosH\x03\x62\x06proto3' ) _REGIONCODE = _descriptor.EnumDescriptor( @@ -112,8 +112,8 @@ _REGIONCODE = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=2040, - serialized_end=2142, + serialized_start=2084, + serialized_end=2186, ) _sym_db.RegisterEnumDescriptor(_REGIONCODE) @@ -213,8 +213,8 @@ _CHARGECURRENT = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=2145, - serialized_end=2354, + serialized_start=2189, + serialized_end=2398, ) _sym_db.RegisterEnumDescriptor(_CHARGECURRENT) @@ -254,8 +254,8 @@ _GPSOPERATION = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=2356, - serialized_end=2462, + serialized_start=2400, + serialized_end=2506, ) _sym_db.RegisterEnumDescriptor(_GPSOPERATION) @@ -285,8 +285,8 @@ _LOCATIONSHARING = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=2464, - serialized_end=2528, + serialized_start=2508, + serialized_end=2572, ) _sym_db.RegisterEnumDescriptor(_LOCATIONSHARING) @@ -343,8 +343,8 @@ _RADIOCONFIG_USERPREFERENCES_ENVIRONMENTALMEASUREMENTSENSORTYPE = _descriptor.En ], containing_type=None, serialized_options=None, - serialized_start=1983, - serialized_end=2030, + serialized_start=2027, + serialized_end=2074, ) _sym_db.RegisterEnumDescriptor(_RADIOCONFIG_USERPREFERENCES_ENVIRONMENTALMEASUREMENTSENSORTYPE) @@ -526,196 +526,210 @@ _RADIOCONFIG_USERPREFERENCES = _descriptor.Descriptor( is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='factory_reset', full_name='RadioConfig.UserPreferences.factory_reset', index=24, + name='mqtt_server', full_name='RadioConfig.UserPreferences.mqtt_server', index=24, + number=42, 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, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='mqtt_disabled', full_name='RadioConfig.UserPreferences.mqtt_disabled', index=25, + number=43, 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, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='factory_reset', full_name='RadioConfig.UserPreferences.factory_reset', index=26, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='debug_log_enabled', full_name='RadioConfig.UserPreferences.debug_log_enabled', index=25, + name='debug_log_enabled', full_name='RadioConfig.UserPreferences.debug_log_enabled', index=27, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='ignore_incoming', full_name='RadioConfig.UserPreferences.ignore_incoming', index=26, + name='ignore_incoming', full_name='RadioConfig.UserPreferences.ignore_incoming', index=28, number=103, type=13, cpp_type=3, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='serialplugin_enabled', full_name='RadioConfig.UserPreferences.serialplugin_enabled', index=27, + name='serialplugin_enabled', full_name='RadioConfig.UserPreferences.serialplugin_enabled', index=29, number=120, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='serialplugin_echo', full_name='RadioConfig.UserPreferences.serialplugin_echo', index=28, + name='serialplugin_echo', full_name='RadioConfig.UserPreferences.serialplugin_echo', index=30, number=121, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='serialplugin_rxd', full_name='RadioConfig.UserPreferences.serialplugin_rxd', index=29, + name='serialplugin_rxd', full_name='RadioConfig.UserPreferences.serialplugin_rxd', index=31, number=122, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='serialplugin_txd', full_name='RadioConfig.UserPreferences.serialplugin_txd', index=30, + name='serialplugin_txd', full_name='RadioConfig.UserPreferences.serialplugin_txd', index=32, number=123, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='serialplugin_timeout', full_name='RadioConfig.UserPreferences.serialplugin_timeout', index=31, + name='serialplugin_timeout', full_name='RadioConfig.UserPreferences.serialplugin_timeout', index=33, number=124, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='serialplugin_mode', full_name='RadioConfig.UserPreferences.serialplugin_mode', index=32, + name='serialplugin_mode', full_name='RadioConfig.UserPreferences.serialplugin_mode', index=34, number=125, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='ext_notification_plugin_enabled', full_name='RadioConfig.UserPreferences.ext_notification_plugin_enabled', index=33, + name='ext_notification_plugin_enabled', full_name='RadioConfig.UserPreferences.ext_notification_plugin_enabled', index=35, number=126, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='ext_notification_plugin_output_ms', full_name='RadioConfig.UserPreferences.ext_notification_plugin_output_ms', index=34, + name='ext_notification_plugin_output_ms', full_name='RadioConfig.UserPreferences.ext_notification_plugin_output_ms', index=36, number=127, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='ext_notification_plugin_output', full_name='RadioConfig.UserPreferences.ext_notification_plugin_output', index=35, + name='ext_notification_plugin_output', full_name='RadioConfig.UserPreferences.ext_notification_plugin_output', index=37, number=128, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='ext_notification_plugin_active', full_name='RadioConfig.UserPreferences.ext_notification_plugin_active', index=36, + name='ext_notification_plugin_active', full_name='RadioConfig.UserPreferences.ext_notification_plugin_active', index=38, number=129, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='ext_notification_plugin_alert_message', full_name='RadioConfig.UserPreferences.ext_notification_plugin_alert_message', index=37, + name='ext_notification_plugin_alert_message', full_name='RadioConfig.UserPreferences.ext_notification_plugin_alert_message', index=39, number=130, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='ext_notification_plugin_alert_bell', full_name='RadioConfig.UserPreferences.ext_notification_plugin_alert_bell', index=38, + name='ext_notification_plugin_alert_bell', full_name='RadioConfig.UserPreferences.ext_notification_plugin_alert_bell', index=40, number=131, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='range_test_plugin_enabled', full_name='RadioConfig.UserPreferences.range_test_plugin_enabled', index=39, + name='range_test_plugin_enabled', full_name='RadioConfig.UserPreferences.range_test_plugin_enabled', index=41, number=132, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='range_test_plugin_sender', full_name='RadioConfig.UserPreferences.range_test_plugin_sender', index=40, + name='range_test_plugin_sender', full_name='RadioConfig.UserPreferences.range_test_plugin_sender', index=42, number=133, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='range_test_plugin_save', full_name='RadioConfig.UserPreferences.range_test_plugin_save', index=41, + name='range_test_plugin_save', full_name='RadioConfig.UserPreferences.range_test_plugin_save', index=43, number=134, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='store_forward_plugin_enabled', full_name='RadioConfig.UserPreferences.store_forward_plugin_enabled', index=42, + name='store_forward_plugin_enabled', full_name='RadioConfig.UserPreferences.store_forward_plugin_enabled', index=44, number=148, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='store_forward_plugin_records', full_name='RadioConfig.UserPreferences.store_forward_plugin_records', index=43, + name='store_forward_plugin_records', full_name='RadioConfig.UserPreferences.store_forward_plugin_records', index=45, number=137, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='environmental_measurement_plugin_measurement_enabled', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_measurement_enabled', index=44, + name='environmental_measurement_plugin_measurement_enabled', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_measurement_enabled', index=46, number=140, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='environmental_measurement_plugin_screen_enabled', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_screen_enabled', index=45, + name='environmental_measurement_plugin_screen_enabled', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_screen_enabled', index=47, number=141, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='environmental_measurement_plugin_read_error_count_threshold', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_read_error_count_threshold', index=46, + name='environmental_measurement_plugin_read_error_count_threshold', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_read_error_count_threshold', index=48, number=142, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='environmental_measurement_plugin_update_interval', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_update_interval', index=47, + name='environmental_measurement_plugin_update_interval', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_update_interval', index=49, number=143, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='environmental_measurement_plugin_recovery_interval', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_recovery_interval', index=48, + name='environmental_measurement_plugin_recovery_interval', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_recovery_interval', index=50, number=144, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='environmental_measurement_plugin_display_farenheit', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_display_farenheit', index=49, + name='environmental_measurement_plugin_display_farenheit', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_display_farenheit', index=51, number=145, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='environmental_measurement_plugin_sensor_type', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_sensor_type', index=50, + name='environmental_measurement_plugin_sensor_type', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_sensor_type', index=52, number=146, 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, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( - name='environmental_measurement_plugin_sensor_pin', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_sensor_pin', index=51, + name='environmental_measurement_plugin_sensor_pin', full_name='RadioConfig.UserPreferences.environmental_measurement_plugin_sensor_pin', index=53, number=147, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, @@ -735,7 +749,7 @@ _RADIOCONFIG_USERPREFERENCES = _descriptor.Descriptor( oneofs=[ ], serialized_start=89, - serialized_end=2038, + serialized_end=2082, ) _RADIOCONFIG = _descriptor.Descriptor( @@ -766,7 +780,7 @@ _RADIOCONFIG = _descriptor.Descriptor( oneofs=[ ], serialized_start=22, - serialized_end=2038, + serialized_end=2082, ) _RADIOCONFIG_USERPREFERENCES.fields_by_name['region'].enum_type = _REGIONCODE diff --git a/docs/meshtastic/remote_hardware.html b/docs/meshtastic/remote_hardware.html index 7524b03..8c05cc6 100644 --- a/docs/meshtastic/remote_hardware.html +++ b/docs/meshtastic/remote_hardware.html @@ -34,9 +34,8 @@ def onGPIOreceive(packet, interface): """Callback for received GPIO responses FIXME figure out how to do closures with methods in python""" - pb = remote_hardware_pb2.HardwareMessage() - pb.ParseFromString(packet["decoded"]["data"]["payload"]) - print(f"Received RemoteHardware typ={pb.typ}, gpio_value={pb.gpio_value}") + hw = packet["decoded"]["remotehw"] + print(f'Received RemoteHardware typ={hw["typ"]}, gpio_value={hw["gpioValue"]}') class RemoteHardwareClient: @@ -56,14 +55,18 @@ class RemoteHardwareClient: ch = iface.localNode.getChannelByName("gpio") if not ch: raise Exception( - "No gpio channel found, please create before using this (secured) service") + "No gpio channel found, please create on the sending and receive nodes to use this (secured) service (--ch-add gpio --info then --seturl)") self.channelIndex = ch.index pub.subscribe( - onGPIOreceive, "meshtastic.receive.data.REMOTE_HARDWARE_APP") + onGPIOreceive, "meshtastic.receive.remotehw") - def _sendHardware(self, nodeid, r): - return self.iface.sendData(r, nodeid, portnums_pb2.REMOTE_HARDWARE_APP, wantAck=True, channelIndex=self.channelIndex) + def _sendHardware(self, nodeid, r, wantResponse=False, onResponse=None): + if not nodeid: + raise Exception( + "You must set a destination node ID for this operation (use --dest \!xxxxxxxxx)") + return self.iface.sendData(r, nodeid, portnums_pb2.REMOTE_HARDWARE_APP, + wantAck=True, channelIndex=self.channelIndex, wantResponse=wantResponse, onResponse=onResponse) def writeGPIOs(self, nodeid, mask, vals): """ @@ -76,12 +79,12 @@ class RemoteHardwareClient: r.gpio_value = vals return self._sendHardware(nodeid, r) - def readGPIOs(self, nodeid, mask): + def readGPIOs(self, nodeid, mask, onResponse = None): """Read the specified bits from GPIO inputs on the device""" r = remote_hardware_pb2.HardwareMessage() r.typ = remote_hardware_pb2.HardwareMessage.Type.READ_GPIOS r.gpio_mask = mask - return self._sendHardware(nodeid, r) + return self._sendHardware(nodeid, r, wantResponse=True, onResponse=onResponse) def watchGPIOs(self, nodeid, mask): """Watch the specified bits from GPIO inputs on the device for changes""" @@ -112,9 +115,8 @@ class RemoteHardwareClient: """Callback for received GPIO responses FIXME figure out how to do closures with methods in python""" - pb = remote_hardware_pb2.HardwareMessage() - pb.ParseFromString(packet["decoded"]["data"]["payload"]) - print(f"Received RemoteHardware typ={pb.typ}, gpio_value={pb.gpio_value}")
    + hw = packet["decoded"]["remotehw"] + print(f'Received RemoteHardware typ={hw["typ"]}, gpio_value={hw["gpioValue"]}') @@ -154,14 +156,18 @@ code for how you can connect to your own custom meshtastic services

    ch = iface.localNode.getChannelByName("gpio") if not ch: raise Exception( - "No gpio channel found, please create before using this (secured) service") + "No gpio channel found, please create on the sending and receive nodes to use this (secured) service (--ch-add gpio --info then --seturl)") self.channelIndex = ch.index pub.subscribe( - onGPIOreceive, "meshtastic.receive.data.REMOTE_HARDWARE_APP") + onGPIOreceive, "meshtastic.receive.remotehw") - def _sendHardware(self, nodeid, r): - return self.iface.sendData(r, nodeid, portnums_pb2.REMOTE_HARDWARE_APP, wantAck=True, channelIndex=self.channelIndex) + def _sendHardware(self, nodeid, r, wantResponse=False, onResponse=None): + if not nodeid: + raise Exception( + "You must set a destination node ID for this operation (use --dest \!xxxxxxxxx)") + return self.iface.sendData(r, nodeid, portnums_pb2.REMOTE_HARDWARE_APP, + wantAck=True, channelIndex=self.channelIndex, wantResponse=wantResponse, onResponse=onResponse) def writeGPIOs(self, nodeid, mask, vals): """ @@ -174,12 +180,12 @@ code for how you can connect to your own custom meshtastic services

    r.gpio_value = vals return self._sendHardware(nodeid, r) - def readGPIOs(self, nodeid, mask): + def readGPIOs(self, nodeid, mask, onResponse = None): """Read the specified bits from GPIO inputs on the device""" r = remote_hardware_pb2.HardwareMessage() r.typ = remote_hardware_pb2.HardwareMessage.Type.READ_GPIOS r.gpio_mask = mask - return self._sendHardware(nodeid, r) + return self._sendHardware(nodeid, r, wantResponse=True, onResponse=onResponse) def watchGPIOs(self, nodeid, mask): """Watch the specified bits from GPIO inputs on the device for changes""" @@ -191,7 +197,7 @@ code for how you can connect to your own custom meshtastic services

    Methods

    -def readGPIOs(self, nodeid, mask) +def readGPIOs(self, nodeid, mask, onResponse=None)

    Read the specified bits from GPIO inputs on the device

    @@ -199,12 +205,12 @@ code for how you can connect to your own custom meshtastic services

    Expand source code -
    def readGPIOs(self, nodeid, mask):
    +
    def readGPIOs(self, nodeid, mask, onResponse = None):
         """Read the specified bits from GPIO inputs on the device"""
         r = remote_hardware_pb2.HardwareMessage()
         r.typ = remote_hardware_pb2.HardwareMessage.Type.READ_GPIOS
         r.gpio_mask = mask
    -    return self._sendHardware(nodeid, r)
    + return self._sendHardware(nodeid, r, wantResponse=True, onResponse=onResponse)
    diff --git a/docs/meshtastic/util.html b/docs/meshtastic/util.html index 524e511..40235ad 100644 --- a/docs/meshtastic/util.html +++ b/docs/meshtastic/util.html @@ -30,9 +30,7 @@ import serial import serial.tools.list_ports from queue import Queue -import threading -import sys -import logging +import threading, sys, time, logging """Some devices such as a seger jlink we never want to accidentally open""" blacklistVids = dict.fromkeys([0x1366]) @@ -76,6 +74,26 @@ class dotdict(dict): __delattr__ = dict.__delitem__ +class Timeout: + def __init__(self, maxSecs=20): + self.expireTime = 0 + self.sleepInterval = 0.1 + self.expireTimeout = maxSecs + + def reset(self): + """Restart the waitForSet timer""" + self.expireTime = time.time() + self.expireTimeout + + def waitForSet(self, target, attrs=()): + """Block until the specified attributes are set. Returns True if config has been received.""" + self.reset() + while time.time() < self.expireTime: + if all(map(lambda a: getattr(target, a, None), attrs)): + return True + time.sleep(self.sleepInterval) + return False + + class DeferredExecution(): """A thread that accepts closures to run, and runs them as they are received""" @@ -227,6 +245,72 @@ class DeferredExecution():
    +
    +class Timeout +(maxSecs=20) +
    +
    +
    +
    + +Expand source code + +
    class Timeout:
    +    def __init__(self, maxSecs=20):
    +        self.expireTime = 0
    +        self.sleepInterval = 0.1
    +        self.expireTimeout = maxSecs
    +
    +    def reset(self):
    +        """Restart the waitForSet timer"""
    +        self.expireTime = time.time() + self.expireTimeout
    +
    +    def waitForSet(self, target, attrs=()):
    +        """Block until the specified attributes are set. Returns True if config has been received."""
    +        self.reset()
    +        while time.time() < self.expireTime:
    +            if all(map(lambda a: getattr(target, a, None), attrs)):
    +                return True
    +            time.sleep(self.sleepInterval)
    +        return False
    +
    +

    Methods

    +
    +
    +def reset(self) +
    +
    +

    Restart the waitForSet timer

    +
    + +Expand source code + +
    def reset(self):
    +    """Restart the waitForSet timer"""
    +    self.expireTime = time.time() + self.expireTimeout
    +
    +
    +
    +def waitForSet(self, target, attrs=()) +
    +
    +

    Block until the specified attributes are set. Returns True if config has been received.

    +
    + +Expand source code + +
    def waitForSet(self, target, attrs=()):
    +    """Block until the specified attributes are set. Returns True if config has been received."""
    +    self.reset()
    +    while time.time() < self.expireTime:
    +        if all(map(lambda a: getattr(target, a, None), attrs)):
    +            return True
    +        time.sleep(self.sleepInterval)
    +    return False
    +
    +
    +
    +
    class dotdict (*args, **kwargs) @@ -279,6 +363,13 @@ class DeferredExecution():
  • +

    Timeout

    + +
  • +
  • dotdict

  • diff --git a/setup.py b/setup.py index 44fa10a..f5b42f8 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ with open("README.md", "r") as fh: # This call to setup() does all the work setup( name="meshtastic", - version="1.2.20", + version="1.2.23", description="Python API & client shell for talking to Meshtastic devices", long_description=long_description, long_description_content_type="text/markdown",