diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py index 8f82a25..d4be93b 100644 --- a/meshtastic/__init__.py +++ b/meshtastic/__init__.py @@ -57,21 +57,13 @@ 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 +import serial, threading, logging, sys, random, traceback, time, base64, platform, 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 from pubsub import pub from dotmap import DotMap from typing import * +from google.protobuf.json_format import MessageToJson START1 = 0x94 START2 = 0xc3 @@ -110,6 +102,206 @@ class KnownProtocol(NamedTuple): onReceive: Callable = None +def waitForSet(target, sleep=.1, maxsecs=20, attrs=()): + """Block until the specified attributes are set. Returns True if config has been received.""" + for _ in range(int(maxsecs/sleep)): + if all(map(lambda a: getattr(target, a, None), attrs)): + return True + time.sleep(sleep) + return False + + +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 + + def showInfo(self): + """Show human readable description of our node""" + print(self.radioConfig) + print("Channels:") + for c in self.channels: + if c.role != channel_pb2.Channel.Role.DISABLED: + cStr = MessageToJson(c.settings).replace("\n", "") + print(f" {channel_pb2.Channel.Role.Name(c.role)} {cStr}") + print(f"\nChannel URL {self.channelURL}") + + 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() + self._requestChannel(0) + + def waitForConfig(self): + """Block until radio config is received. Returns True if config has been received.""" + return 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): + """Write the current (edited) channel to the device""" + + p = admin_pb2.AdminMessage() + p.set_channel.CopyFrom(self.channels[channelIndex]) + + self._sendAdmin(p) + logging.debug("Wrote channel {channelIndex}") + + 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) + + @property + def channelURL(self): + """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.DISABLED: + 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 + + return self._sendAdmin(p, + wantResponse=True, + onResponse=onResponse) + + 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 + 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) + 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: + self.channels = self.partialChannels + # 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): + """Send an admin message to the specified node (or the local node if destNodeNum is zero)""" + + return self.iface.sendData(p, self.nodeNum, + portNum=portnums_pb2.PortNum.ADMIN_APP, + wantAck=True, + wantResponse=wantResponse, + onResponse=onResponse) + + class MeshInterface: """Interface class for meshtastic devices @@ -130,6 +322,7 @@ class MeshInterface: self.nodes = None # FIXME self.isConnected = threading.Event() self.noProto = noProto + self.localNode = Node(self, -1) # We fixup nodenum later self.myInfo = None # We don't have device info yet self.responseHandlers = {} # A map from request ID to the handler self.failure = None # If we've encountered a fatal exception it will be kept here @@ -148,6 +341,14 @@ class MeshInterface: logging.error(f'Traceback: {traceback}') self.close() + def showInfo(self): + """Show human readable summary about this object""" + print(self.myInfo) + self.localNode.showInfo() + print("Nodes in mesh:") + for n in self.nodes.values(): + print(stripnl(n)) + def sendText(self, text: AnyStr, destinationId=BROADCAST_ADDR, wantAck=False, @@ -198,7 +399,7 @@ class MeshInterface: if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN: raise Exception("Data payload too big") - if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers + if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers raise Exception("A non-zero port number must be specified") meshPacket = mesh_pb2.MeshPacket() @@ -206,7 +407,8 @@ class MeshInterface: meshPacket.decoded.portnum = portNum meshPacket.decoded.want_response = wantResponse - p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit) + p = self._sendPacket(meshPacket, destinationId, + wantAck=wantAck, hopLimit=hopLimit) if onResponse is not None: self._addResponseHandler(p.id, onResponse) return p @@ -283,43 +485,9 @@ class MeshInterface: self._sendToRadio(toRadio) return meshPacket - def waitForConfig(self, sleep=.1, maxsecs=20, attrs=('myInfo', 'nodes', 'radioConfig', 'channels')): + def waitForConfig(self): """Block until radio config is received. Returns True if config has been received.""" - for _ in range(int(maxsecs/sleep)): - if all(map(lambda a: getattr(self, a, None), attrs)): - return True - time.sleep(sleep) - return False - - def _sendAdmin(self, p: admin_pb2.AdminMessage, destNodeNum = 0): - """Send an admin message to the specified node (or the local node if destNodeNum is zero)""" - - if destNodeNum == 0: - destNodeNum = self.myInfo.my_node_num - - return self.sendData(p, destNodeNum, - portNum=portnums_pb2.PortNum.ADMIN_APP, - wantAck=True) - - 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): - """Write the current (edited) channel to the device""" - - p = admin_pb2.AdminMessage() - p.set_channel.CopyFrom(self.channels[channelIndex]) - - self._sendAdmin(p) - logging.debug("Wrote channel {channelIndex}") + return self.localNode.waitForConfig() and waitForSet(self, attrs=('myInfo', 'nodes')) def getMyNodeInfo(self): if self.myInfo is None: @@ -344,80 +512,6 @@ class MeshInterface: return user.get('shortName', None) return None - 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) - - @property - def channelURL(self): - """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.DISABLED: - 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 _waitConnected(self): """Block until the initial node db download is complete, or timeout and raise an exception""" @@ -445,18 +539,19 @@ class MeshInterface: def _connected(self): """Called by this class to tell clients we are now fully connected to a node """ - self.isConnected.set() - catchAndIgnore("connection publish", lambda: pub.sendMessage( - "meshtastic.connection.established", interface=self)) + # (because I'm lazy) _connected might be called when remote Node + # objects complete their config reads, don't generate redundant isConnected + # for the local interface + if not self.isConnected.is_set(): + self.isConnected.set() + catchAndIgnore("connection publish", lambda: pub.sendMessage( + "meshtastic.connection.established", interface=self)) def _startConfig(self): """Start device packets flowing""" self.myInfo = None self.nodes = {} # nodes keyed by ID self.nodesByNum = {} # nodes keyed by nodenum - self.radioConfig = None - self.channels = None - self.partialChannels = [] # We keep our channels in a temp array until finished startConfig = mesh_pb2.ToRadio() startConfig.want_config_id = MY_CONFIG_ID # we don't use this value @@ -479,59 +574,7 @@ class MeshInterface: """ Done with initial config messages, now send regular MeshPackets to ask for settings and channels """ - self._requestSettings() - self._requestChannel(0) - - 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 - - return self.sendData(p, self.myInfo.my_node_num, - portNum=portnums_pb2.PortNum.ADMIN_APP, - wantAck=True, - wantResponse=True, - onResponse=onResponse) - - 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 - 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) - 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.myInfo.max_channels - 1: - self.channels = self.partialChannels - # FIXME, the following should only be called after we have settings and channels - self._connected() # Tell everone else we are ready to go - else: - self._requestChannel(index + 1) - - return self.sendData(p, self.myInfo.my_node_num, - portNum=portnums_pb2.PortNum.ADMIN_APP, - wantAck=True, - wantResponse=True, - onResponse=onResponse) + self.localNode.requestConfig() def _handleFromRadio(self, fromRadioBytes): """ @@ -543,6 +586,7 @@ class MeshInterface: asDict = google.protobuf.json_format.MessageToDict(fromRadio) if fromRadio.HasField("my_info"): self.myInfo = fromRadio.my_info + self.localNode.nodeNum = self.myInfo.my_node_num logging.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}") failmsg = None @@ -647,7 +691,8 @@ class MeshInterface: # from might be missing if the nodenum was zero. if not "from" in asDict: asDict["from"] = 0 - logging.error(f"Device returned a packet we sent, ignoring: {stripnl(asDict)}") + logging.error( + f"Device returned a packet we sent, ignoring: {stripnl(asDict)}") return if not "to" in asDict: asDict["to"] = 0 diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index a58c400..3251c4c 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -17,7 +17,6 @@ import pkg_resources from datetime import datetime import timeago from easy_table import EasyTable -from google.protobuf.json_format import MessageToJson """We only import the tunnel code if we are on a platform that can run it""" have_tunnel = platform.system() == 'Linux' @@ -74,7 +73,7 @@ def fromStr(valstr): Args: valstr (string): A user provided string """ - if(len(valstr) == 0): # Treat an emptystring as an empty bytes + if(len(valstr) == 0): # Treat an emptystring as an empty bytes val = bytes() elif(valstr.startswith('0x')): # if needed convert to string with asBytes.decode('utf-8') @@ -110,13 +109,17 @@ def formatFloat(value, formatStr="{:.2f}", unit="", default="N/A"): def getLH(ts, default="N/A"): return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') if ts else default -#Returns time ago for the last heard +# Returns time ago for the last heard + + def getTimeAgo(ts, default="N/A"): return timeago.format(datetime.fromtimestamp(ts), datetime.now()) if ts else default -#Print Nodes +# Print Nodes + + def printNodes(nodes, myId): - #Create the table and define the structure + # Create the table and define the structure table = EasyTable("Nodes") table.setCorners("/", "\\", "\\", "/") table.setOuterStructure("|", "-") @@ -126,22 +129,22 @@ def printNodes(nodes, myId): for node in nodes: if node['user']['id'] == myId: continue - #aux var to get not defined keys - lat=formatFloat(node['position'].get("latitude"), "{:.4f}", "°") - lon=formatFloat(node['position'].get("longitude"), "{:.4f}", "°") - alt=formatFloat(node['position'].get("altitude"), "{:.0f}", " m") - batt=formatFloat(node['position'].get("batteryLevel"), "{:.2f}", "%") - snr=formatFloat(node.get("snr"), "{:.2f}", " dB") - LH= getLH(node['position'].get("time")) + # aux var to get not defined keys + lat = formatFloat(node['position'].get("latitude"), "{:.4f}", "°") + lon = formatFloat(node['position'].get("longitude"), "{:.4f}", "°") + alt = formatFloat(node['position'].get("altitude"), "{:.0f}", " m") + batt = formatFloat(node['position'].get("batteryLevel"), "{:.2f}", "%") + snr = formatFloat(node.get("snr"), "{:.2f}", " dB") + LH = getLH(node['position'].get("time")) timeAgo = getTimeAgo(node['position'].get("time")) - tableData.append({"N":0, "User":node['user']['longName'], - "AKA":node['user']['shortName'], "ID":node['user']['id'], - "Position":lat+", "+lon+", "+alt, - "Battery":batt, "SNR":snr, - "LastHeard":LH, "Since":timeAgo}) - - Rows = sorted(tableData, key=lambda k: k['LastHeard'], reverse=True) - RowsOk = sorted(Rows, key=lambda k:k ['LastHeard'].startswith("N/A")) + tableData.append({"N": 0, "User": node['user']['longName'], + "AKA": node['user']['shortName'], "ID": node['user']['id'], + "Position": lat+", "+lon+", "+alt, + "Battery": batt, "SNR": snr, + "LastHeard": LH, "Since": timeAgo}) + + Rows = sorted(tableData, key=lambda k: k['LastHeard'], reverse=True) + RowsOk = sorted(Rows, key=lambda k: k['LastHeard'].startswith("N/A")) for i in range(len(RowsOk)): RowsOk[i]['N'] = i+1 table.setData(RowsOk) @@ -171,7 +174,7 @@ def onConnected(interface): try: global args print("Connected to radio") - prefs = interface.radioConfig.preferences + prefs = interface.localNode.radioConfig.preferences if args.settime or args.setlat or args.setlon or args.setalt: closeNow = True @@ -254,14 +257,13 @@ def onConnected(interface): if args.seturl: closeNow = True - interface.setURL(args.seturl) + interface.localNode.setURL(args.seturl) # handle changing channels - if args.setchan or args.setch_longslow or args.setch_shortfast \ - or args.seturl != None: + if args.setchan or args.setch_longslow or args.setch_shortfast: closeNow = True - ch = interface.channels[channelIndex] + ch = interface.localNode.channels[channelIndex] def setSimpleChannel(modem_config): """Set one of the simple modem_config only based channels""" @@ -287,25 +289,16 @@ def onConnected(interface): setPref(ch.settings, pref[0], pref[1]) print("Writing modified channels to device") - interface.writeChannel(channelIndex) + interface.localNode.writeChannel(channelIndex) if args.info: closeNow = True - print(interface.myInfo) - print(interface.radioConfig) - print("Channels:") - for c in interface.channels: - if c.role != channel_pb2.Channel.Role.DISABLED: - cStr = MessageToJson(c.settings).replace("\n", "") - print(f" {channel_pb2.Channel.Role.Name(c.role)} {cStr}") - print(f"\nChannel URL {interface.channelURL}") - print("Nodes in mesh:") - for n in interface.nodes.values(): - print(stripnl(n)) + interface.showInfo() if args.nodes: closeNow = True - printNodes(interface.nodes.values(), interface.getMyNodeInfo()['user']['id']) + printNodes(interface.nodes.values(), + interface.getMyNodeInfo()['user']['id']) if args.qr: closeNow = True @@ -345,7 +338,7 @@ def common(): global args logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) - if len(sys.argv)==1: + if len(sys.argv) == 1: parser.print_help(sys.stderr) sys.exit(1) else: @@ -377,7 +370,8 @@ def common(): logfile = None else: logging.info(f"Logging serial output to {args.seriallog}") - logfile = open(args.seriallog, 'w+', buffering=1) # line buffering + logfile = open(args.seriallog, 'w+', + buffering=1) # line buffering subscribe() if args.ble: @@ -389,7 +383,8 @@ def common(): client = SerialInterface( args.port, debugOut=logfile, noProto=args.noproto) - sys.exit(0) + # don't call exit, background threads might be running still + # sys.exit(0) def initParser(): diff --git a/meshtastic/channel_pb2.py b/meshtastic/channel_pb2.py index 2b1d8a6..771d16d 100644 --- a/meshtastic/channel_pb2.py +++ b/meshtastic/channel_pb2.py @@ -19,7 +19,7 @@ DESCRIPTOR = _descriptor.FileDescriptor( syntax='proto3', serialized_options=b'\n\023com.geeksville.meshB\rChannelProtosH\003', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\rchannel.proto\"\xe6\x02\n\x0f\x43hannelSettings\x12\x10\n\x08tx_power\x18\x01 \x01(\x05\x12\x32\n\x0cmodem_config\x18\x03 \x01(\x0e\x32\x1c.ChannelSettings.ModemConfig\x12\x11\n\tbandwidth\x18\x06 \x01(\r\x12\x15\n\rspread_factor\x18\x07 \x01(\r\x12\x13\n\x0b\x63oding_rate\x18\x08 \x01(\r\x12\x13\n\x0b\x63hannel_num\x18\t \x01(\r\x12\x0b\n\x03psk\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\n\n\x02id\x18\n \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x10 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x11 \x01(\x08\"`\n\x0bModemConfig\x12\x12\n\x0e\x42w125Cr45Sf128\x10\x00\x12\x12\n\x0e\x42w500Cr45Sf128\x10\x01\x12\x14\n\x10\x42w31_25Cr48Sf512\x10\x02\x12\x13\n\x0f\x42w125Cr48Sf4096\x10\x03\"\x8b\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\r\x12\"\n\x08settings\x18\x02 \x01(\x0b\x32\x10.ChannelSettings\x12\x1b\n\x04role\x18\x03 \x01(\x0e\x32\r.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42&\n\x13\x63om.geeksville.meshB\rChannelProtosH\x03\x62\x06proto3' + serialized_pb=b'\n\rchannel.proto\"\xe6\x02\n\x0f\x43hannelSettings\x12\x10\n\x08tx_power\x18\x01 \x01(\x05\x12\x32\n\x0cmodem_config\x18\x03 \x01(\x0e\x32\x1c.ChannelSettings.ModemConfig\x12\x11\n\tbandwidth\x18\x06 \x01(\r\x12\x15\n\rspread_factor\x18\x07 \x01(\r\x12\x13\n\x0b\x63oding_rate\x18\x08 \x01(\r\x12\x13\n\x0b\x63hannel_num\x18\t \x01(\r\x12\x0b\n\x03psk\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\n\n\x02id\x18\n \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x10 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x11 \x01(\x08\"`\n\x0bModemConfig\x12\x12\n\x0e\x42w125Cr45Sf128\x10\x00\x12\x12\n\x0e\x42w500Cr45Sf128\x10\x01\x12\x14\n\x10\x42w31_25Cr48Sf512\x10\x02\x12\x13\n\x0f\x42w125Cr48Sf4096\x10\x03\"\x8b\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12\"\n\x08settings\x18\x02 \x01(\x0b\x32\x10.ChannelSettings\x12\x1b\n\x04role\x18\x03 \x01(\x0e\x32\r.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42&\n\x13\x63om.geeksville.meshB\rChannelProtosH\x03\x62\x06proto3' ) @@ -203,7 +203,7 @@ _CHANNEL = _descriptor.Descriptor( fields=[ _descriptor.FieldDescriptor( name='index', full_name='Channel.index', index=0, - number=1, type=13, cpp_type=3, label=1, + number=1, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, diff --git a/meshtastic/mesh_pb2.py b/meshtastic/mesh_pb2.py index 488f536..9ffa17d 100644 --- a/meshtastic/mesh_pb2.py +++ b/meshtastic/mesh_pb2.py @@ -21,7 +21,7 @@ DESCRIPTOR = _descriptor.FileDescriptor( syntax='proto3', serialized_options=b'\n\023com.geeksville.meshB\nMeshProtosH\003', create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\nmesh.proto\x1a\x0eportnums.proto\"v\n\x08Position\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x15\n\rbattery_level\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\t \x01(\x07J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\t\"J\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x0f\n\x07macaddr\x18\x04 \x01(\x0c\"\x1f\n\x0eRouteDiscovery\x12\r\n\x05route\x18\x02 \x03(\x07\"\x8e\x02\n\x07Routing\x12(\n\rroute_request\x18\x01 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0broute_reply\x18\x02 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0c\x65rror_reason\x18\x03 \x01(\x0e\x32\x0e.Routing.ErrorH\x00\"~\n\x05\x45rror\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08NO_ROUTE\x10\x01\x12\x0b\n\x07GOT_NAK\x10\x02\x12\x0b\n\x07TIMEOUT\x10\x03\x12\x10\n\x0cNO_INTERFACE\x10\x04\x12\x12\n\x0eMAX_RETRANSMIT\x10\x05\x12\x0e\n\nNO_CHANNEL\x10\x06\x12\r\n\tTOO_LARGE\x10\x07\x42\t\n\x07variant\"{\n\x04\x44\x61ta\x12\x19\n\x07portnum\x18\x01 \x01(\x0e\x32\x08.PortNum\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x15\n\rwant_response\x18\x03 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x04 \x01(\x07\x12\x0e\n\x06source\x18\x05 \x01(\x07\x12\x12\n\nrequest_id\x18\x06 \x01(\x07\"\xcf\x02\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x07\x12\n\n\x02to\x18\x02 \x01(\x07\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\r\x12\x18\n\x07\x64\x65\x63oded\x18\x04 \x01(\x0b\x32\x05.DataH\x00\x12\x13\n\tencrypted\x18\x05 \x01(\x0cH\x00\x12\n\n\x02id\x18\x06 \x01(\x07\x12\x0f\n\x07rx_time\x18\x07 \x01(\x07\x12\x0e\n\x06rx_snr\x18\x08 \x01(\x02\x12\x11\n\thop_limit\x18\n \x01(\r\x12\x10\n\x08want_ack\x18\x0b \x01(\x08\x12&\n\x08priority\x18\x0c \x01(\x0e\x32\x14.MeshPacket.Priority\"[\n\x08Priority\x12\t\n\x05UNSET\x10\x00\x12\x07\n\x03MIN\x10\x01\x12\x0e\n\nBACKGROUND\x10\n\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10@\x12\x0c\n\x08RELIABLE\x10\x46\x12\x07\n\x03\x41\x43K\x10x\x12\x07\n\x03MAX\x10\x7f\x42\x10\n\x0epayloadVariant\"h\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x13\n\x04user\x18\x02 \x01(\x0b\x32\x05.User\x12\x1b\n\x08position\x18\x03 \x01(\x0b\x32\t.Position\x12\x0b\n\x03snr\x18\x07 \x01(\x02\x12\x10\n\x08next_hop\x18\x05 \x01(\r\"\xa6\x02\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\r\x12\x0f\n\x07has_gps\x18\x02 \x01(\x08\x12\x11\n\tnum_bands\x18\x03 \x01(\r\x12\x14\n\x0cmax_channels\x18\x0f \x01(\r\x12\x12\n\x06region\x18\x04 \x01(\tB\x02\x18\x01\x12\x10\n\x08hw_model\x18\x05 \x01(\t\x12\x18\n\x10\x66irmware_version\x18\x06 \x01(\t\x12&\n\nerror_code\x18\x07 \x01(\x0e\x32\x12.CriticalErrorCode\x12\x15\n\rerror_address\x18\x08 \x01(\r\x12\x13\n\x0b\x65rror_count\x18\t \x01(\r\x12\x1c\n\x14message_timeout_msec\x18\r \x01(\r\x12\x17\n\x0fmin_app_version\x18\x0e \x01(\r\"\xb5\x01\n\tLogRecord\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x1f\n\x05level\x18\x04 \x01(\x0e\x32\x10.LogRecord.Level\"X\n\x05Level\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x32\x12\t\n\x05\x45RROR\x10(\x12\x0b\n\x07WARNING\x10\x1e\x12\x08\n\x04INFO\x10\x14\x12\t\n\x05\x44\x45\x42UG\x10\n\x12\t\n\x05TRACE\x10\x05\"\xe9\x01\n\tFromRadio\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x1d\n\x06packet\x18\x0b \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x1e\n\x07my_info\x18\x03 \x01(\x0b\x32\x0b.MyNodeInfoH\x00\x12\x1e\n\tnode_info\x18\x04 \x01(\x0b\x32\t.NodeInfoH\x00\x12 \n\nlog_record\x18\x07 \x01(\x0b\x32\n.LogRecordH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x08 \x01(\rH\x00\x12\x12\n\x08rebooted\x18\t \x01(\x08H\x00\x42\x10\n\x0epayloadVariantJ\x04\x08\x02\x10\x03J\x04\x08\x06\x10\x07\"l\n\x07ToRadio\x12\x1d\n\x06packet\x18\x02 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x64 \x01(\rH\x00\x42\x10\n\x0epayloadVariantJ\x04\x08\x01\x10\x02J\x04\x08\x65\x10\x66J\x04\x08\x66\x10gJ\x04\x08g\x10h*.\n\tConstants\x12\n\n\x06Unused\x10\x00\x12\x15\n\x10\x44\x41TA_PAYLOAD_LEN\x10\xf0\x01*\xaf\x01\n\x11\x43riticalErrorCode\x12\x08\n\x04None\x10\x00\x12\x0e\n\nTxWatchdog\x10\x01\x12\x12\n\x0eSleepEnterWait\x10\x02\x12\x0b\n\x07NoRadio\x10\x03\x12\x0f\n\x0bUnspecified\x10\x04\x12\x13\n\x0fUBloxInitFailed\x10\x05\x12\x0c\n\x08NoAXP192\x10\x06\x12\x17\n\x13InvalidRadioSetting\x10\x07\x12\x12\n\x0eTransmitFailed\x10\x08\x42#\n\x13\x63om.geeksville.meshB\nMeshProtosH\x03\x62\x06proto3' + serialized_pb=b'\n\nmesh.proto\x1a\x0eportnums.proto\"v\n\x08Position\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x15\n\rbattery_level\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\t \x01(\x07J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\t\"J\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x0f\n\x07macaddr\x18\x04 \x01(\x0c\"\x1f\n\x0eRouteDiscovery\x12\r\n\x05route\x18\x02 \x03(\x07\"\x8e\x02\n\x07Routing\x12(\n\rroute_request\x18\x01 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0broute_reply\x18\x02 \x01(\x0b\x32\x0f.RouteDiscoveryH\x00\x12&\n\x0c\x65rror_reason\x18\x03 \x01(\x0e\x32\x0e.Routing.ErrorH\x00\"~\n\x05\x45rror\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08NO_ROUTE\x10\x01\x12\x0b\n\x07GOT_NAK\x10\x02\x12\x0b\n\x07TIMEOUT\x10\x03\x12\x10\n\x0cNO_INTERFACE\x10\x04\x12\x12\n\x0eMAX_RETRANSMIT\x10\x05\x12\x0e\n\nNO_CHANNEL\x10\x06\x12\r\n\tTOO_LARGE\x10\x07\x42\t\n\x07variant\"{\n\x04\x44\x61ta\x12\x19\n\x07portnum\x18\x01 \x01(\x0e\x32\x08.PortNum\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\x12\x15\n\rwant_response\x18\x03 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x04 \x01(\x07\x12\x0e\n\x06source\x18\x05 \x01(\x07\x12\x12\n\nrequest_id\x18\x06 \x01(\x07\"\xcf\x02\n\nMeshPacket\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x07\x12\n\n\x02to\x18\x02 \x01(\x07\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\r\x12\x18\n\x07\x64\x65\x63oded\x18\x04 \x01(\x0b\x32\x05.DataH\x00\x12\x13\n\tencrypted\x18\x05 \x01(\x0cH\x00\x12\n\n\x02id\x18\x06 \x01(\x07\x12\x0f\n\x07rx_time\x18\x07 \x01(\x07\x12\x0e\n\x06rx_snr\x18\x08 \x01(\x02\x12\x11\n\thop_limit\x18\n \x01(\r\x12\x10\n\x08want_ack\x18\x0b \x01(\x08\x12&\n\x08priority\x18\x0c \x01(\x0e\x32\x14.MeshPacket.Priority\"[\n\x08Priority\x12\t\n\x05UNSET\x10\x00\x12\x07\n\x03MIN\x10\x01\x12\x0e\n\nBACKGROUND\x10\n\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10@\x12\x0c\n\x08RELIABLE\x10\x46\x12\x07\n\x03\x41\x43K\x10x\x12\x07\n\x03MAX\x10\x7f\x42\x10\n\x0epayloadVariant\"h\n\x08NodeInfo\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x13\n\x04user\x18\x02 \x01(\x0b\x32\x05.User\x12\x1b\n\x08position\x18\x03 \x01(\x0b\x32\t.Position\x12\x0b\n\x03snr\x18\x07 \x01(\x02\x12\x10\n\x08next_hop\x18\x05 \x01(\r\"\xa6\x02\n\nMyNodeInfo\x12\x13\n\x0bmy_node_num\x18\x01 \x01(\r\x12\x0f\n\x07has_gps\x18\x02 \x01(\x08\x12\x11\n\tnum_bands\x18\x03 \x01(\r\x12\x14\n\x0cmax_channels\x18\x0f \x01(\r\x12\x12\n\x06region\x18\x04 \x01(\tB\x02\x18\x01\x12\x10\n\x08hw_model\x18\x05 \x01(\t\x12\x18\n\x10\x66irmware_version\x18\x06 \x01(\t\x12&\n\nerror_code\x18\x07 \x01(\x0e\x32\x12.CriticalErrorCode\x12\x15\n\rerror_address\x18\x08 \x01(\r\x12\x13\n\x0b\x65rror_count\x18\t \x01(\r\x12\x1c\n\x14message_timeout_msec\x18\r \x01(\r\x12\x17\n\x0fmin_app_version\x18\x0e \x01(\r\"\xb5\x01\n\tLogRecord\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x07\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x1f\n\x05level\x18\x04 \x01(\x0e\x32\x10.LogRecord.Level\"X\n\x05Level\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08\x43RITICAL\x10\x32\x12\t\n\x05\x45RROR\x10(\x12\x0b\n\x07WARNING\x10\x1e\x12\x08\n\x04INFO\x10\x14\x12\t\n\x05\x44\x45\x42UG\x10\n\x12\t\n\x05TRACE\x10\x05\"\xe9\x01\n\tFromRadio\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\x1d\n\x06packet\x18\x0b \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x1e\n\x07my_info\x18\x03 \x01(\x0b\x32\x0b.MyNodeInfoH\x00\x12\x1e\n\tnode_info\x18\x04 \x01(\x0b\x32\t.NodeInfoH\x00\x12 \n\nlog_record\x18\x07 \x01(\x0b\x32\n.LogRecordH\x00\x12\x1c\n\x12\x63onfig_complete_id\x18\x08 \x01(\rH\x00\x12\x12\n\x08rebooted\x18\t \x01(\x08H\x00\x42\x10\n\x0epayloadVariantJ\x04\x08\x02\x10\x03J\x04\x08\x06\x10\x07\"l\n\x07ToRadio\x12\x1d\n\x06packet\x18\x02 \x01(\x0b\x32\x0b.MeshPacketH\x00\x12\x18\n\x0ewant_config_id\x18\x64 \x01(\rH\x00\x42\x10\n\x0epayloadVariantJ\x04\x08\x01\x10\x02J\x04\x08\x65\x10\x66J\x04\x08\x66\x10gJ\x04\x08g\x10h*.\n\tConstants\x12\n\n\x06Unused\x10\x00\x12\x15\n\x10\x44\x41TA_PAYLOAD_LEN\x10\xf0\x01*\xbd\x01\n\x11\x43riticalErrorCode\x12\x08\n\x04None\x10\x00\x12\x0e\n\nTxWatchdog\x10\x01\x12\x12\n\x0eSleepEnterWait\x10\x02\x12\x0b\n\x07NoRadio\x10\x03\x12\x0f\n\x0bUnspecified\x10\x04\x12\x13\n\x0fUBloxInitFailed\x10\x05\x12\x0c\n\x08NoAXP192\x10\x06\x12\x17\n\x13InvalidRadioSetting\x10\x07\x12\x12\n\x0eTransmitFailed\x10\x08\x12\x0c\n\x08\x42rownout\x10\tB#\n\x13\x63om.geeksville.meshB\nMeshProtosH\x03\x62\x06proto3' , dependencies=[portnums__pb2.DESCRIPTOR,]) @@ -103,11 +103,16 @@ _CRITICALERRORCODE = _descriptor.EnumDescriptor( serialized_options=None, type=None, create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='Brownout', index=9, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), ], containing_type=None, serialized_options=None, serialized_start=1977, - serialized_end=2152, + serialized_end=2166, ) _sym_db.RegisterEnumDescriptor(_CRITICALERRORCODE) @@ -123,6 +128,7 @@ UBloxInitFailed = 5 NoAXP192 = 6 InvalidRadioSetting = 7 TransmitFailed = 8 +Brownout = 9 _ROUTING_ERROR = _descriptor.EnumDescriptor( diff --git a/proto b/proto index 7c025b9..6dac309 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 7c025b9a4d54bb410ec17ee653122861b413f177 +Subproject commit 6dac3099be6cd27848b92365e11b0d84202a6405